diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index ee0bc5020b..3682efb9f5 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -64,6 +64,9 @@ dependencies {
implementation(projects.domain.analytics)
implementation(projects.domain.visa)
implementation(projects.domain.onboarding)
+ implementation(projects.domain.feedback)
+ implementation(projects.domain.qrScanning)
+ implementation(projects.domain.qrScanning.models)
implementation(projects.common)
implementation(projects.core.analytics)
@@ -93,6 +96,8 @@ dependencies {
implementation(projects.data.visa)
implementation(projects.data.promo)
implementation(projects.data.onboarding)
+ implementation(projects.data.feedback)
+ implementation(projects.data.qrScanning)
/** Features */
implementation(projects.features.onboarding)
@@ -110,9 +115,9 @@ dependencies {
implementation(projects.features.wallet.impl)
implementation(projects.features.tokendetails.api)
implementation(projects.features.tokendetails.impl)
- implementation(projects.features.send.api)
implementation(projects.features.manageTokens.api)
implementation(projects.features.manageTokens.impl)
+ implementation(projects.features.send.api)
implementation(projects.features.send.impl)
implementation(projects.features.qrScanning.api)
implementation(projects.features.qrScanning.impl)
@@ -183,10 +188,6 @@ dependencies {
implementation(deps.kotsonGson)
implementation(deps.spongecastle.core)
implementation(deps.lottie)
- implementation(deps.shopify.buy) {
- exclude(group = "com.shopify.graphql.support")
- exclude(module = "joda-time")
- }
implementation(deps.compose.accompanist.appCompatTheme)
implementation(deps.compose.accompanist.systemUiController)
implementation(deps.compose.accompanist.webView)
@@ -230,6 +231,9 @@ dependencies {
implementation(deps.listenableFuture)
implementation(deps.mlKit.barcodeScanning)
+ /** Leakcanary */
+ debugImplementation(deps.leakcanary)
+
/** Excluded dependencies */
implementation("com.google.guava:guava:30.0-android") {
// excludes version 9999.0-empty-to-avoid-conflict-with-guava
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 4b57ad94ed..093b3eb247 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -130,7 +130,7 @@
diff --git a/app/src/main/assets/tangem-app-config b/app/src/main/assets/tangem-app-config
index 82f4a8aebc..980cc4cb2f 160000
--- a/app/src/main/assets/tangem-app-config
+++ b/app/src/main/assets/tangem-app-config
@@ -1 +1 @@
-Subproject commit 82f4a8aebcab924047705ad3ecd5f008f07ca0b5
+Subproject commit 980cc4cb2fb9ba44cd6798191ae7d208e4f7f74c
diff --git a/app/src/main/assets/testnet_tokens.json b/app/src/main/assets/testnet_tokens.json
index b638128b34..e10692c072 100644
--- a/app/src/main/assets/testnet_tokens.json
+++ b/app/src/main/assets/testnet_tokens.json
@@ -638,6 +638,56 @@
"networkId": "pulsechain/test"
}
]
+ },
+ {
+ "id": "zksync-ethereum",
+ "name": "zkSync",
+ "symbol": "ETH",
+ "networks": [
+ {
+ "networkId": "zksync/test"
+ }
+ ]
+ },
+ {
+ "id": "moonbeam",
+ "name": "Moonbeam",
+ "symbol": "GLMR",
+ "networks": [
+ {
+ "networkId": "moonbeam/test"
+ }
+ ]
+ },
+ {
+ "id": "manta-network-ethereum",
+ "name": "Manta Testnet",
+ "symbol": "ETH",
+ "networks": [
+ {
+ "networkId": "manta-network/test"
+ }
+ ]
+ },
+ {
+ "id": "polygon-zkevm-ethereum",
+ "name": "Polygon zkEvm Testnet",
+ "symbol": "ETH",
+ "networks": [
+ {
+ "networkId": "polygon-zkevm/test"
+ }
+ ]
+ },
+ {
+ "id": "base-ethereum",
+ "name": "Base",
+ "symbol": "ETH",
+ "networks": [
+ {
+ "networkId": "base/test"
+ }
+ ]
}
]
}
diff --git a/app/src/main/java/com/tangem/tap/MainActivity.kt b/app/src/main/java/com/tangem/tap/MainActivity.kt
index 2a453f6cc8..2cc0b685f7 100644
--- a/app/src/main/java/com/tangem/tap/MainActivity.kt
+++ b/app/src/main/java/com/tangem/tap/MainActivity.kt
@@ -57,9 +57,6 @@ import com.tangem.tap.common.OnActivityResultCallback
import com.tangem.tap.common.SnackbarHandler
import com.tangem.tap.common.apptheme.MutableAppThemeModeHolder
import com.tangem.tap.common.redux.NotificationsHandler
-import com.tangem.tap.common.shop.googlepay.GooglePayService
-import com.tangem.tap.common.shop.googlepay.GooglePayService.Companion.LOAD_PAYMENT_DATA_REQUEST_CODE
-import com.tangem.tap.common.shop.googlepay.GooglePayUtil.createPaymentsClient
import com.tangem.tap.domain.TangemSdkManager
import com.tangem.tap.domain.userWalletList.implementation.BiometricUserWalletsListManager
import com.tangem.tap.domain.walletconnect2.domain.WalletConnectInteractor
@@ -69,7 +66,6 @@ import com.tangem.tap.features.intentHandler.handlers.WalletConnectLinkIntentHan
import com.tangem.tap.features.main.MainViewModel
import com.tangem.tap.features.main.model.Toast
import com.tangem.tap.features.onboarding.products.wallet.redux.BackupAction
-import com.tangem.tap.features.shop.redux.ShopAction
import com.tangem.tap.features.welcome.ui.WelcomeFragment
import com.tangem.tap.proxy.AppStateHolder
import com.tangem.tap.proxy.redux.DaggerGraphAction
@@ -151,9 +147,6 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
@Inject
lateinit var userWalletsListManagerFeatureToggles: UserWalletsListManagerFeatureToggles
- @Inject
- lateinit var generalUserWalletsListManager: UserWalletsListManager
-
@Inject
lateinit var getPolkadotCheckHasResetUseCase: GetPolkadotCheckHasResetUseCase
@@ -188,8 +181,6 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
setContentView(R.layout.activity_main)
initContent()
- checkGooglePayAvailability()
-
checkForNotificationPermission()
observeStateUpdates()
observePolkadotAccountHealthCheck()
@@ -272,14 +263,6 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
}
- private fun checkGooglePayAvailability() {
- store.dispatch(
- ShopAction.CheckIfGooglePayAvailable(
- GooglePayService(createPaymentsClient(this), this),
- ),
- )
- }
-
private fun createAppThemeModeFlow(): SharedFlow {
val tapApplication = application as TapApplication
@@ -393,13 +376,6 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
super.onActivityResult(requestCode, resultCode, data)
onActivityResultCallbacks.forEach { it(requestCode, resultCode, data) }
- when (requestCode) {
- LOAD_PAYMENT_DATA_REQUEST_CODE -> {
- store.dispatch(
- ShopAction.BuyWithGooglePay.HandleGooglePayResponse(resultCode, data),
- )
- }
- }
}
override fun addOnActivityResultCallback(callback: OnActivityResultCallback) {
@@ -447,11 +423,10 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
private fun navigateToInitialScreenIfNeeded(intentWhichStartedActivity: Intent?) {
val backStackIsEmpty = supportFragmentManager.backStackEntryCount == 0
val isNotScannedBefore = store.state.globalState.scanResponse == null
- val isOnboardingServiceNotActive = store.state.globalState.onboardingState.onboardingStarted
- val isShopNotOpened = store.state.shopState.total != null
+ val isOnboardingServiceNotActive = !store.state.globalState.onboardingState.onboardingStarted
when {
- !backStackIsEmpty && isNotScannedBefore && isOnboardingServiceNotActive && isShopNotOpened -> {
+ !backStackIsEmpty && isNotScannedBefore && isOnboardingServiceNotActive -> {
navigateToInitialScreen(intentWhichStartedActivity)
}
backStackIsEmpty -> {
diff --git a/app/src/main/java/com/tangem/tap/TapApplication.kt b/app/src/main/java/com/tangem/tap/TapApplication.kt
index 051cb803b8..7c26fe7570 100644
--- a/app/src/main/java/com/tangem/tap/TapApplication.kt
+++ b/app/src/main/java/com/tangem/tap/TapApplication.kt
@@ -10,8 +10,10 @@ import com.orhanobut.logger.AndroidLogAdapter
import com.orhanobut.logger.Logger
import com.tangem.Log
import com.tangem.LogFormat
+import com.tangem.TangemSdkLogger
import com.tangem.blockchain.common.AccountCreator
import com.tangem.blockchain.common.datastorage.BlockchainDataStorage
+import com.tangem.blockchain.common.logging.BlockchainSDKLogger
import com.tangem.blockchain.network.BlockchainSdkRetrofitBuilder
import com.tangem.core.analytics.Analytics
import com.tangem.core.analytics.filter.OneTimeEventFilter
@@ -57,7 +59,6 @@ import com.tangem.tap.common.log.TimberFormatStrategy
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.common.redux.appReducer
import com.tangem.tap.common.redux.global.GlobalAction
-import com.tangem.tap.common.shop.TangemShopService
import com.tangem.tap.domain.configurable.warningMessage.WarningMessagesManager
import com.tangem.tap.domain.tasks.product.DerivationsFinder
import com.tangem.tap.domain.userWalletList.di.provideBiometricImplementation
@@ -82,7 +83,6 @@ lateinit var foregroundActivityObserver: ForegroundActivityObserver
lateinit var activityResultCaller: ActivityResultCaller
lateinit var preferencesStorage: PreferencesDataSource
lateinit var walletConnectRepository: WalletConnectRepository
-lateinit var shopService: TangemShopService
internal lateinit var derivationsFinder: DerivationsFinder
@HiltAndroidApp
@@ -178,6 +178,12 @@ internal class TapApplication : Application(), ImageLoaderFactory {
@Inject
lateinit var feedbackManagerFeatureToggles: FeedbackManagerFeatureToggles
+
+ @Inject
+ lateinit var blockchainSDKLogger: BlockchainSDKLogger
+
+ @Inject
+ lateinit var tangemSdkLogger: TangemSdkLogger
// endregion Injected
override fun onCreate() {
@@ -266,6 +272,8 @@ internal class TapApplication : Application(), ImageLoaderFactory {
saveTwinsOnboardingShownUseCase = saveTwinsOnboardingShownUseCase,
cardRepository = cardRepository,
feedbackManagerFeatureToggles = feedbackManagerFeatureToggles,
+ tangemSdkLogger = tangemSdkLogger,
+ blockchainSDKLogger = blockchainSDKLogger,
),
),
)
@@ -290,7 +298,6 @@ internal class TapApplication : Application(), ImageLoaderFactory {
}
private fun initWithConfigDependency(config: Config) {
- shopService = TangemShopService(this, config.shopify!!)
initAnalytics(this, config)
initFeedbackManager(this, foregroundActivityObserver, store)
}
@@ -343,13 +350,17 @@ internal class TapApplication : Application(), ImageLoaderFactory {
Log.Level.View,
Log.Level.Network,
Log.Level.Error,
+ Log.Level.Biometric,
)
return TangemLogCollector(logLevels, LogFormat.StairsFormatter())
}
val additionalFeedbackInfo = initAdditionalFeedbackInfo(context)
val tangemLogCollector = initTangemLogCollector()
- Log.addLogger(tangemLogCollector)
+
+ Log.addLogger(
+ logger = if (feedbackManagerFeatureToggles.isLocalLogsEnabled) tangemSdkLogger else tangemLogCollector,
+ )
val feedbackManager = FeedbackManager(
infoHolder = additionalFeedbackInfo,
diff --git a/app/src/main/java/com/tangem/tap/common/DialogManager.kt b/app/src/main/java/com/tangem/tap/common/DialogManager.kt
index 3339239daa..5887deeadc 100644
--- a/app/src/main/java/com/tangem/tap/common/DialogManager.kt
+++ b/app/src/main/java/com/tangem/tap/common/DialogManager.kt
@@ -112,6 +112,11 @@ class DialogManager : StoreSubscriber {
context = context,
)
}
+ is WalletConnectDialog.UnsupportedDapp -> SimpleAlertDialog.create(
+ titleRes = R.string.wallet_connect_title,
+ messageRes = R.string.wallet_connect_error_unsupported_dapp,
+ context = context,
+ )
is WalletConnectDialog.SessionProposalDialog -> {
SessionProposalDialog.create(
sessionProposal = state.dialog.sessionProposal,
@@ -121,6 +126,10 @@ class DialogManager : StoreSubscriber {
onReject = state.dialog.onReject,
)
}
+ is WalletConnectDialog.SignTransactionDialog -> SignTransactionDialog.create(
+ preparedData = state.dialog.data,
+ context = context,
+ )
is BackupDialog.AttestationFailed -> AttestationFailedDialog.create(context)
is BackupDialog.AddMoreBackupCards -> AddMoreBackupCardsDialog.create(context)
is BackupDialog.BackupInProgress -> BackupInProgressDialog.create(context)
diff --git a/app/src/main/java/com/tangem/tap/common/analytics/converters/ShopOrderToEventConverter.kt b/app/src/main/java/com/tangem/tap/common/analytics/converters/ShopOrderToEventConverter.kt
deleted file mode 100644
index fd658029b7..0000000000
--- a/app/src/main/java/com/tangem/tap/common/analytics/converters/ShopOrderToEventConverter.kt
+++ /dev/null
@@ -1,28 +0,0 @@
-package com.tangem.tap.common.analytics.converters
-
-import com.shopify.buy3.Storefront
-import com.tangem.tap.common.analytics.events.Shop
-import com.tangem.tap.features.shop.domain.models.ProductType
-import com.tangem.utils.converter.Converter
-
-/**
-[REDACTED_AUTHOR]
- */
-class ShopOrderToEventConverter : Converter, Shop.Purchased> {
-
- override fun convert(value: Pair): Shop.Purchased {
- val checkout = value.first
- val productType = value.second
-
- val sku = checkout.lineItems?.edges?.firstOrNull()?.node?.variant?.sku ?: productType.sku
- val count = when (productType) {
- ProductType.WALLET_2_CARDS -> "2"
- ProductType.WALLET_3_CARDS -> "3"
- }
- val amount = "${checkout.totalPriceV2.amount} ${checkout.totalPriceV2.currencyCode.name}"
- val code = (checkout.discountApplications.edges.firstOrNull()?.node as? Storefront.DiscountCodeApplication)
- ?.code
-
- return Shop.Purchased(sku, count, amount, code)
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/common/analytics/events/WalletConnect.kt b/app/src/main/java/com/tangem/tap/common/analytics/events/WalletConnect.kt
index ed47afa57c..6ea8b5b88f 100644
--- a/app/src/main/java/com/tangem/tap/common/analytics/events/WalletConnect.kt
+++ b/app/src/main/java/com/tangem/tap/common/analytics/events/WalletConnect.kt
@@ -5,7 +5,7 @@ import com.tangem.core.analytics.models.AnalyticsEvent
/**
[REDACTED_AUTHOR]
*/
-sealed class WalletConnect(
+internal sealed class WalletConnect(
event: String,
params: Map = mapOf(),
error: Throwable? = null,
@@ -65,7 +65,7 @@ sealed class WalletConnect(
FAIL("Fail"),
}
- companion object {
- private const val SUCCESS_CODE = "0"
+ private companion object {
+ const val SUCCESS_CODE = "0"
}
}
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/common/analytics/paramsInterceptor/CardContextInterceptor.kt b/app/src/main/java/com/tangem/tap/common/analytics/paramsInterceptor/CardContextInterceptor.kt
index 6a963e544f..c881cd0127 100644
--- a/app/src/main/java/com/tangem/tap/common/analytics/paramsInterceptor/CardContextInterceptor.kt
+++ b/app/src/main/java/com/tangem/tap/common/analytics/paramsInterceptor/CardContextInterceptor.kt
@@ -15,7 +15,7 @@ import com.tangem.tap.features.demo.DemoHelper
[REDACTED_AUTHOR]
*/
class CardContextInterceptor(
- private val scanResponse: ScanResponse?,
+ private val scanResponse: ScanResponse,
) : ParamsInterceptor {
override fun id(): String = CardContextInterceptor.id()
@@ -28,8 +28,6 @@ class CardContextInterceptor(
}
override fun intercept(params: MutableMap) {
- scanResponse ?: return
-
val card = scanResponse.card
params[AnalyticsParam.BATCH] = card.batchId
params[AnalyticsParam.PRODUCT_TYPE] = getProductType(scanResponse)
diff --git a/app/src/main/java/com/tangem/tap/common/extensions/Activity.kt b/app/src/main/java/com/tangem/tap/common/extensions/Activity.kt
index adf3e19fed..ec4f3737f0 100644
--- a/app/src/main/java/com/tangem/tap/common/extensions/Activity.kt
+++ b/app/src/main/java/com/tangem/tap/common/extensions/Activity.kt
@@ -17,6 +17,7 @@ import java.io.File
*
*
*/
+@Deprecated("Use EmailSender instead")
fun Activity.sendEmail(
email: String,
subject: String,
diff --git a/app/src/main/java/com/tangem/tap/common/extensions/Navigation.kt b/app/src/main/java/com/tangem/tap/common/extensions/Navigation.kt
index 019b203988..f6c299acb1 100644
--- a/app/src/main/java/com/tangem/tap/common/extensions/Navigation.kt
+++ b/app/src/main/java/com/tangem/tap/common/extensions/Navigation.kt
@@ -24,7 +24,6 @@ import com.tangem.tap.features.onboarding.products.twins.ui.OnboardingTwinsFragm
import com.tangem.tap.features.onboarding.products.wallet.ui.OnboardingWalletFragment
import com.tangem.tap.features.saveWallet.ui.SaveWalletBottomSheetFragment
import com.tangem.tap.features.send.ui.SendFragment
-import com.tangem.tap.features.shop.ui.ShopFragment
import com.tangem.tap.features.tokens.impl.presentation.TokensListFragment
import com.tangem.tap.features.welcome.ui.WelcomeFragment
import com.tangem.tap.proxy.redux.DaggerGraphState
@@ -122,7 +121,6 @@ fun FragmentActivity.getPreviousScreen(): AppScreen? {
private fun fragmentFactory(screen: AppScreen): Fragment {
return when (screen) {
AppScreen.Home -> HomeFragment()
- AppScreen.Shop -> ShopFragment()
AppScreen.OnboardingNote -> OnboardingNoteFragment()
AppScreen.OnboardingWallet -> OnboardingWalletFragment()
AppScreen.OnboardingTwins -> OnboardingTwinsFragment()
diff --git a/app/src/main/java/com/tangem/tap/common/extensions/WalletManager.kt b/app/src/main/java/com/tangem/tap/common/extensions/WalletManager.kt
index 11c26259ee..f4df731c74 100644
--- a/app/src/main/java/com/tangem/tap/common/extensions/WalletManager.kt
+++ b/app/src/main/java/com/tangem/tap/common/extensions/WalletManager.kt
@@ -6,6 +6,7 @@ import com.tangem.blockchain.common.WalletManager
import com.tangem.blockchain.common.address.AddressType
import com.tangem.common.services.Result
import com.tangem.domain.common.extensions.amountToCreateAccount
+import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.tap.common.TestActions
import com.tangem.tap.common.apptheme.MutableAppThemeModeHolder
import com.tangem.tap.domain.TapError
@@ -58,14 +59,13 @@ suspend fun WalletManager.safeUpdate(isDemoCard: Boolean): Result = try
}
}
-internal fun WalletManager.getTopUpUrl(): String? {
+internal fun WalletManager.getTopUpUrl(cryptoCurrency: CryptoCurrency): String? {
val globalState = store.state.globalState
val defaultAddress = wallet.address
return globalState.exchangeManager.getUrl(
action = CurrencyExchangeManager.Action.Buy,
- blockchain = wallet.blockchain,
- cryptoCurrencyName = wallet.blockchain.currency,
+ cryptoCurrency = cryptoCurrency,
fiatCurrencyName = globalState.appCurrency.code,
walletAddress = defaultAddress,
isDarkTheme = MutableAppThemeModeHolder.isDarkThemeActive,
diff --git a/app/src/main/java/com/tangem/tap/common/feedback/FeedbackData.kt b/app/src/main/java/com/tangem/tap/common/feedback/FeedbackData.kt
index 452eb173aa..7e878f6e69 100644
--- a/app/src/main/java/com/tangem/tap/common/feedback/FeedbackData.kt
+++ b/app/src/main/java/com/tangem/tap/common/feedback/FeedbackData.kt
@@ -73,15 +73,9 @@ class SendTransactionFailedEmail(
class FeedbackEmail : FeedbackData {
override val subjectResId: Int
- get() = if (isS2CCard) s2cSubject else tangemSubject
- override val mainMessageResId: Int
- get() = if (isS2CCard) s2cMainMessage else tangemMainMessage
+ get() = if (isS2CCard) R.string.feedback_subject_support else R.string.feedback_subject_support_tangem
- private val tangemSubject: Int = R.string.feedback_subject_support_tangem
- private val tangemMainMessage: Int = R.string.feedback_preface_support
-
- private val s2cSubject: Int = R.string.feedback_subject_support
- private val s2cMainMessage: Int = R.string.feedback_preface_support
+ override val mainMessageResId: Int = R.string.feedback_preface_support
private var isS2CCard = false
diff --git a/app/src/main/java/com/tangem/tap/common/redux/AppReducer.kt b/app/src/main/java/com/tangem/tap/common/redux/AppReducer.kt
index 91992dd437..7adedbed8f 100644
--- a/app/src/main/java/com/tangem/tap/common/redux/AppReducer.kt
+++ b/app/src/main/java/com/tangem/tap/common/redux/AppReducer.kt
@@ -12,7 +12,6 @@ import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsReducer
import com.tangem.tap.features.onboarding.products.wallet.redux.OnboardingWalletReducer
import com.tangem.tap.features.saveWallet.redux.SaveWalletReducer
import com.tangem.tap.features.send.redux.reducers.SendScreenReducer
-import com.tangem.tap.features.shop.redux.ShopReducer
import com.tangem.tap.features.tokens.legacy.redux.TokensReducer
import com.tangem.tap.features.welcome.redux.WelcomeReducer
import com.tangem.tap.proxy.AppStateHolder
@@ -36,7 +35,6 @@ fun appReducer(action: Action, state: AppState?, appStateHolder: AppStateHolder)
disclaimerState = DisclaimerReducer.reduce(action, state),
tokensState = TokensReducer.reduce(action, state),
walletConnectState = WalletConnectReducer.reduce(action, state.walletConnectState),
- shopState = ShopReducer.reduce(action, state.shopState),
welcomeState = WelcomeReducer.reduce(action, state),
saveWalletState = SaveWalletReducer.reduce(action, state),
daggerGraphState = DaggerGraphReducer.reduce(action, state),
diff --git a/app/src/main/java/com/tangem/tap/common/redux/AppState.kt b/app/src/main/java/com/tangem/tap/common/redux/AppState.kt
index 72726532fe..59b899ee8b 100644
--- a/app/src/main/java/com/tangem/tap/common/redux/AppState.kt
+++ b/app/src/main/java/com/tangem/tap/common/redux/AppState.kt
@@ -29,8 +29,6 @@ import com.tangem.tap.features.saveWallet.redux.SaveWalletMiddleware
import com.tangem.tap.features.saveWallet.redux.SaveWalletState
import com.tangem.tap.features.send.redux.middlewares.SendMiddleware
import com.tangem.tap.features.send.redux.states.SendState
-import com.tangem.tap.features.shop.redux.ShopMiddleware
-import com.tangem.tap.features.shop.redux.ShopState
import com.tangem.tap.features.tokens.legacy.redux.TokensState
import com.tangem.tap.features.wallet.redux.middlewares.TradeCryptoMiddleware
import com.tangem.tap.features.welcome.redux.WelcomeMiddleware
@@ -54,7 +52,6 @@ data class AppState(
val disclaimerState: DisclaimerState = DisclaimerState(),
val tokensState: TokensState = TokensState(),
val walletConnectState: WalletConnectState = WalletConnectState(),
- val shopState: ShopState = ShopState(),
val welcomeState: WelcomeState = WelcomeState(),
val saveWalletState: SaveWalletState = SaveWalletState(),
val daggerGraphState: DaggerGraphState = DaggerGraphState(),
@@ -89,7 +86,6 @@ data class AppState(
DisclaimerMiddleware().disclaimerMiddleware,
WalletConnectMiddleware().walletConnectMiddleware,
BackupMiddleware().backupMiddleware,
- ShopMiddleware().shopMiddleware,
WelcomeMiddleware().middleware,
SaveWalletMiddleware().middleware,
LockUserWalletsTimerMiddleware().middleware,
diff --git a/app/src/main/java/com/tangem/tap/common/shop/TangemShopService.kt b/app/src/main/java/com/tangem/tap/common/shop/TangemShopService.kt
deleted file mode 100644
index 1a1b5827f0..0000000000
--- a/app/src/main/java/com/tangem/tap/common/shop/TangemShopService.kt
+++ /dev/null
@@ -1,222 +0,0 @@
-package com.tangem.tap.common.shop
-
-import android.app.Application
-import android.content.Intent
-import com.google.android.gms.wallet.PaymentData
-import com.shopify.buy3.Storefront
-import com.tangem.core.analytics.Analytics
-import com.tangem.datasource.config.models.ShopifyShop
-import com.tangem.tap.common.analytics.converters.ShopOrderToEventConverter
-import com.tangem.tap.common.extensions.filterNotNull
-import com.tangem.tap.common.shop.data.TangemProduct
-import com.tangem.tap.common.shop.data.TotalSum
-import com.tangem.tap.common.shop.googlepay.GooglePayService
-import com.tangem.tap.common.shop.shopify.ShopifyService
-import com.tangem.tap.common.shop.shopify.data.CheckoutItem
-import com.tangem.tap.features.shop.domain.models.ProductType
-import com.tangem.tap.features.shop.domain.models.ProductType.Companion.SKUS_TO_DISPLAY
-import kotlinx.coroutines.async
-import kotlinx.coroutines.awaitAll
-import kotlinx.coroutines.coroutineScope
-import java.math.BigDecimal
-import java.util.Currency
-import java.util.UUID
-
-class TangemShopService(application: Application, shopifyShop: ShopifyShop) {
-
- private val shopifyService = ShopifyService(application, shopifyShop)
-
- private val checkouts = mutableMapOf()
- private val variants = mutableMapOf()
-
- private lateinit var googlePayService: GooglePayService
-
- suspend fun getProducts(): Result> {
- val result = shopifyService.getProducts()
-
- return result.mapCatching { product ->
- val availableVariants = product
- .flatMap { it.variants.edges.map { it.node } }
- .filter { SKUS_TO_DISPLAY.contains(it.sku) }
- .associateBy { ProductType.fromSku(it.sku) }
- .filterNotNull()
- variants.putAll(availableVariants)
-
- if (variants.size < SKUS_TO_DISPLAY.size) {
- return Result.failure(
- Exception(
- "Shopify: products are missing, " +
- "\nproducts available: ${variants.keys.map { it.sku }}",
- ),
- )
- }
-
- val twoCardsProduct = TangemProduct(
- type = ProductType.WALLET_2_CARDS,
- totalSum = TotalSum(
- finalValue = variants[ProductType.WALLET_2_CARDS]?.priceV2?.format(),
- beforeDiscount = variants[ProductType.WALLET_2_CARDS]?.compareAtPriceV2?.format(),
- ),
- )
- val threeCardsProduct = TangemProduct(
- type = ProductType.WALLET_3_CARDS,
- totalSum = TotalSum(
- finalValue = variants[ProductType.WALLET_3_CARDS]?.priceV2?.format(),
- beforeDiscount = variants[ProductType.WALLET_3_CARDS]?.compareAtPriceV2?.format(),
- ),
- )
- createCheckouts()
- return Result.success(listOf(twoCardsProduct, threeCardsProduct))
- }
- }
-
- private suspend fun createCheckouts() {
- variants.keys.map { coroutineScope { async { createCheckout(it) } } }.awaitAll()
- }
-
- private suspend fun createCheckout(productType: ProductType) {
- val checkoutItem = CheckoutItem(variants[productType]!!.id, 1)
- val result = shopifyService.createCheckout(listOf(checkoutItem))
- result.onSuccess { checkout ->
- checkouts[productType] = checkout
- }
- }
-
- suspend fun checkIfGooglePayAvailable(googlePayService: GooglePayService): Result {
- this.googlePayService = googlePayService
- return googlePayService.checkIfGooglePayAvailable(shopifyService.shop.merchantID != null)
- }
-
- fun buyWithGooglePay(productType: ProductType) {
- shopifyService.shop.merchantID?.let { merchantId ->
- val totalPrice = checkouts[productType]!!.totalPriceV2.amount
- googlePayService.payWithGooglePay(
- totalPriceCents = totalPrice,
- currencyCode = checkouts[productType]!!.currencyCode.name,
- merchantID = merchantId,
- )
- }
- }
-
-// fun subscribeToGooglePayResult(
-// productType: ProductType,
-// resultCallback: (Result) -> Unit
-// ) {
-// googlePayService.responseCallback = { result ->
-// result.onFailure { }
-// result.onSuccess {
-// completeTokenizedPayment(it, productType)
-// }
-// }
-// }
-
- suspend fun handleGooglePayResult(resultCode: Int, data: Intent?, productType: ProductType): Result {
- val result = googlePayService.handleResponseFromGooglePay(resultCode, data)
- result.onSuccess {
- val finalizePaymentResult = completeTokenizedPayment(it, productType)
- finalizePaymentResult.onSuccess {
- return Result.success(Unit)
- }
- return Result.failure(finalizePaymentResult.exceptionOrNull()!!)
- }
- return Result.failure(result.exceptionOrNull()!!)
- }
-
- private suspend fun completeTokenizedPayment(
- paymentData: PaymentData,
- productType: ProductType,
- ): Result {
- val checkout = checkouts[productType]!!
- val googlePayResponse =
- googlePayService.parsePaymentData(paymentData)
- ?: return Result.failure(Exception("cannot parse GPay result"))
-
- val amount =
- Storefront.MoneyInput(checkout.totalPriceV2.amount, checkout.totalPriceV2.currencyCode)
- val idempotencyKey = UUID.randomUUID().toString()
- val addressGPay = googlePayResponse.billingAddress
-
- val address = Storefront.MailingAddressInput().apply {
- lastName = addressGPay.name
- address1 = addressGPay.address1
- address2 = addressGPay.address2 + addressGPay.address3
- province = addressGPay.administrativeArea
- zip = addressGPay.postalCode
- phone = addressGPay.phoneNumber
- }
-
- val payment = Storefront.TokenizedPaymentInputV3(
- amount,
- idempotencyKey,
- address,
- paymentData.toJson(),
- Storefront.PaymentTokenType.GOOGLE_PAY,
- )
- .setTest(true)
-
- return shopifyService.completeWithTokenizedPayment(
- payment = payment,
- checkoutID = checkout.id,
- )
- }
-
- suspend fun applyPromoCode(promoCode: String): Result> {
- val products = variants.keys
- .map { coroutineScope { async { applyPromoCode(promoCode, it) } } }
- .awaitAll()
- .map { result -> result.getOrElse { return Result.failure(it) } }
-
- return Result.success(products)
- }
-
- private suspend fun applyPromoCode(promoCode: String, productType: ProductType): Result {
- val checkout = checkouts[productType] ?: return Result.failure(Exception("No checkout"))
-
- val result = if (promoCode.isBlank()) {
- shopifyService.removeDiscount(checkout.id)
- } else {
- shopifyService.applyDiscount(promoCode, checkout.id)
- }
-
- result.onSuccess {
- checkouts[productType] = it
- return Result.success(
- TangemProduct(
- productType,
- TotalSum(
- finalValue = it.totalPriceV2.format(),
- beforeDiscount = variants[productType]!!.compareAtPriceV2.format(),
- ),
- appliedDiscount = it.getAppliedDiscount(),
- ),
- )
- }
- return Result.failure(result.exceptionOrNull()!!)
- }
-
- fun getCheckoutUrl(productType: ProductType): String {
- return checkouts[productType]!!.webUrl
- }
-
- suspend fun waitForCheckout(productType: ProductType) {
- val result = shopifyService.checkout(true, checkouts[productType]!!.id)
- result.onSuccess { checkout ->
- if (checkout.order != null && checkout.lineItems != null) {
- val event = ShopOrderToEventConverter().convert(checkout to productType)
- Analytics.send(event)
- }
- }
- }
-}
-
-private fun Storefront.MoneyV2.format(): String {
- val currencySymbol = Currency.getInstance(currencyCode.name).symbol
- val amountFormatted = BigDecimal(amount).setScale(2)
- return currencySymbol + amountFormatted
-}
-
-private fun Storefront.Checkout.getAppliedDiscount(): String? {
- val discountApplication =
- discountApplications.edges.firstOrNull()?.node as? Storefront.DiscountCodeApplication
- return discountApplication?.code
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/common/shop/data/TangemProduct.kt b/app/src/main/java/com/tangem/tap/common/shop/data/TangemProduct.kt
deleted file mode 100644
index 9a01a5df51..0000000000
--- a/app/src/main/java/com/tangem/tap/common/shop/data/TangemProduct.kt
+++ /dev/null
@@ -1,9 +0,0 @@
-package com.tangem.tap.common.shop.data
-
-import com.tangem.tap.features.shop.domain.models.ProductType
-
-data class TangemProduct(
- val type: ProductType,
- val totalSum: TotalSum? = null,
- val appliedDiscount: String? = null,
-)
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/common/shop/data/TotalSum.kt b/app/src/main/java/com/tangem/tap/common/shop/data/TotalSum.kt
deleted file mode 100644
index a9e920745f..0000000000
--- a/app/src/main/java/com/tangem/tap/common/shop/data/TotalSum.kt
+++ /dev/null
@@ -1,6 +0,0 @@
-package com.tangem.tap.common.shop.data
-
-data class TotalSum(
- val finalValue: String? = null,
- val beforeDiscount: String? = null,
-)
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/common/shop/googlepay/GooglePayService.kt b/app/src/main/java/com/tangem/tap/common/shop/googlepay/GooglePayService.kt
deleted file mode 100644
index 7526224414..0000000000
--- a/app/src/main/java/com/tangem/tap/common/shop/googlepay/GooglePayService.kt
+++ /dev/null
@@ -1,147 +0,0 @@
-package com.tangem.tap.common.shop.googlepay
-
-import android.app.Activity
-import android.app.Activity.RESULT_CANCELED
-import android.app.Activity.RESULT_OK
-import android.content.Intent
-import android.util.Log
-import com.google.android.gms.common.api.ApiException
-import com.google.android.gms.wallet.*
-import com.tangem.common.core.TangemSdkError
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.withContext
-import org.json.JSONException
-import org.json.JSONObject
-import timber.log.Timber
-import kotlin.coroutines.resume
-import kotlin.coroutines.suspendCoroutine
-
-class GooglePayService(private val paymentsClient: PaymentsClient, private val activity: Activity) {
-
-// var responseCallback: ((Result) -> Unit)? = null
-
- suspend fun checkIfGooglePayAvailable(isMerchantAvailable: Boolean): Result {
- if (!isMerchantAvailable) return Result.success(false)
- val isReadyToPayJson = GooglePayUtil.isReadyToPayRequest() ?: return Result.success(false)
- val request = IsReadyToPayRequest.fromJson(isReadyToPayJson.toString())
-
- val task = paymentsClient.isReadyToPay(request)
- return withContext(Dispatchers.IO) {
- suspendCoroutine { continuation ->
- task.addOnCompleteListener { completedTask ->
- try {
- val result = completedTask.getResult(ApiException::class.java)
- continuation.resume(Result.success(true))
- } catch (exception: ApiException) {
- // Process error
- Timber.w("isReadyToPay failed: $exception")
- continuation.resume(Result.failure(exception))
- }
- }
- }
- }
- }
-
- fun payWithGooglePay(totalPriceCents: String, currencyCode: String, merchantID: String) {
- val paymentDataRequestJson = GooglePayUtil.getPaymentDataRequest(
- totalPriceCents,
- currencyCode = currencyCode,
- countryCode = "RU",
- merchantID = merchantID,
- )
- if (paymentDataRequestJson == null) {
- Timber.e("RequestPayment: can't fetch payment data request")
- return
- }
- val request = PaymentDataRequest.fromJson(paymentDataRequestJson.toString())
-
- AutoResolveHelper.resolveTask(
- paymentsClient.loadPaymentData(request),
- activity,
- LOAD_PAYMENT_DATA_REQUEST_CODE,
- )
- }
-
- fun handleResponseFromGooglePay(resultCode: Int, data: Intent?): Result {
- val result = when (resultCode) {
- RESULT_OK -> {
- val paymentData = data?.let { intent -> PaymentData.getFromIntent(intent) }
- if (paymentData == null) {
- Result.failure(Exception("No payment data"))
- } else {
- Result.success(paymentData)
- }
- }
- RESULT_CANCELED -> {
- Result.failure(TangemSdkError.UserCancelled())
- }
- AutoResolveHelper.RESULT_ERROR -> {
- val statusCode = AutoResolveHelper.getStatusFromIntent(data)?.statusCode
- if (statusCode == null) {
- Result.failure(Exception("Unknown Status"))
- } else {
- Result.failure(Exception("$statusCode"))
- }
- }
- else -> Result.failure(Exception("Unknown Status"))
- }
-// responseCallback?.invoke(result)
- return result
- }
-
- fun parsePaymentData(paymentData: PaymentData): GooglePayResponse? {
- val paymentInformation = paymentData.toJson()
-
- try {
- // Token will be null if PaymentDataRequest was not constructed using fromJson(String).
- val paymentMethodData =
- JSONObject(paymentInformation).getJSONObject("paymentMethodData")
- val addressJson = paymentMethodData.getJSONObject("info")
- .getJSONObject("billingAddress")
-
- val address = Address(
- name = addressJson.getString("name"),
- postalCode = addressJson.getString("postalCode"),
- countryCode = addressJson.getString("countryCode"),
- phoneNumber = addressJson.getString("phoneNumber"),
- address1 = addressJson.getString("address1"),
- address2 = addressJson.getString("address2"),
- address3 = addressJson.getString("address3"),
- locality = addressJson.getString("locality"),
- administrativeArea = addressJson.getString("administrativeArea"),
- sortingCode = addressJson.getString("sortingCode"),
- )
-
- val token = paymentMethodData
- .getJSONObject("tokenizationData")
- .getString("token")
-
- return GooglePayResponse(address, token)
- } catch (e: JSONException) {
- Log.e("handlePaymentSuccess", "Error: " + e.toString())
- }
- return null
- }
-
- companion object {
- const val LOAD_PAYMENT_DATA_REQUEST_CODE = 315
- }
-}
-
-data class GooglePayResponse(
- val billingAddress: Address,
- val token: String,
-)
-
-data class Address(
- val name: String,
- val postalCode: String,
- val countryCode: String,
- val phoneNumber: String,
- val address1: String,
- val address2: String,
- val address3: String,
- val locality: String,
- val administrativeArea: String,
- val sortingCode: String,
-)
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/common/shop/googlepay/GooglePayUtil.kt b/app/src/main/java/com/tangem/tap/common/shop/googlepay/GooglePayUtil.kt
deleted file mode 100644
index 1e5cfba714..0000000000
--- a/app/src/main/java/com/tangem/tap/common/shop/googlepay/GooglePayUtil.kt
+++ /dev/null
@@ -1,133 +0,0 @@
-package com.tangem.tap.common.shop.googlepay
-
-import android.app.Activity
-import com.google.android.gms.wallet.PaymentsClient
-import com.google.android.gms.wallet.Wallet
-import com.google.android.gms.wallet.WalletConstants
-import org.json.JSONArray
-import org.json.JSONException
-import org.json.JSONObject
-
-object GooglePayUtil {
- private val baseRequest = JSONObject().apply {
- put("apiVersion", 2)
- put("apiVersionMinor", 0)
- }
-
- @Suppress("MagicNumber")
- private val allowedCardNetworks = JSONArray(
- listOf(
- "AMEX",
- "DISCOVER",
- "INTERAC",
- "JCB",
- "MASTERCARD",
- "VISA",
- ),
- )
-
- private val allowedCardAuthMethods = JSONArray(
- listOf(
- "PAN_ONLY",
- "CRYPTOGRAM_3DS",
- ),
- )
-
- private val merchantInfo: JSONObject = JSONObject().put("merchantName", "Example Merchant")
-
- private fun gatewayTokenizationSpecification(merchantID: String): JSONObject {
- return JSONObject().apply {
- put("type", "PAYMENT_GATEWAY")
- put(
- "parameters",
- JSONObject(
- mapOf(
- "gateway" to "shopify",
- "gatewayMerchantId" to merchantID,
- ),
- ),
- )
- }
- }
-
- private fun baseCardPaymentMethod(): JSONObject {
- return JSONObject().apply {
- val parameters = JSONObject().apply {
- put("allowedAuthMethods", allowedCardAuthMethods)
- put("allowedCardNetworks", allowedCardNetworks)
- put("billingAddressRequired", true)
- put(
- "billingAddressParameters",
- JSONObject().apply {
- put("format", "FULL")
- },
- )
- }
-
- put("type", "CARD")
- put("parameters", parameters)
- }
- }
-
- private fun cardPaymentMethod(merchantID: String): JSONObject {
- val cardPaymentMethod = baseCardPaymentMethod()
- cardPaymentMethod.put("tokenizationSpecification", gatewayTokenizationSpecification(merchantID))
-
- return cardPaymentMethod
- }
-
- fun createPaymentsClient(activity: Activity): PaymentsClient {
- val walletOptions = Wallet.WalletOptions.Builder()
- .setEnvironment(PAYMENTS_ENVIRONMENT)
- .build()
-
- return Wallet.getPaymentsClient(activity, walletOptions)
- }
-
- fun isReadyToPayRequest(): JSONObject? {
- return try {
- baseRequest.apply {
- put("allowedPaymentMethods", JSONArray().put(baseCardPaymentMethod()))
- }
- } catch (e: JSONException) {
- null
- }
- }
-
- private fun getTransactionInfo(price: String, countryCode: String, currencyCode: String): JSONObject {
- return JSONObject().apply {
- put("totalPrice", price)
- put("totalPriceStatus", "FINAL")
- put("countryCode", countryCode)
- put("currencyCode", currencyCode)
- }
- }
-
- fun getPaymentDataRequest(
- price: String,
- countryCode: String,
- currencyCode: String,
- merchantID: String,
- ): JSONObject? {
- try {
- return baseRequest.apply {
- put("allowedPaymentMethods", JSONArray().put(cardPaymentMethod(merchantID)))
- put("transactionInfo", getTransactionInfo(price, countryCode, currencyCode))
- put("merchantInfo", merchantInfo)
-
- // An optional shipping address requirement is a top-level property of the
- // PaymentDataRequest JSON object.
- val shippingAddressParameters = JSONObject().apply {
- put("phoneNumberRequired", false)
-// put("allowedCountryCodes", JSONArray(listOf("US", "GB")))
- }
- put("shippingAddressParameters", shippingAddressParameters)
- put("shippingAddressRequired", true)
- }
- } catch (e: JSONException) {
- return null
- }
- }
-}
-
-const val PAYMENTS_ENVIRONMENT = WalletConstants.ENVIRONMENT_TEST
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/common/shop/shopify/ShopifyService.kt b/app/src/main/java/com/tangem/tap/common/shop/shopify/ShopifyService.kt
deleted file mode 100644
index 705bd067a0..0000000000
--- a/app/src/main/java/com/tangem/tap/common/shop/shopify/ShopifyService.kt
+++ /dev/null
@@ -1,267 +0,0 @@
-package com.tangem.tap.common.shop.shopify
-
-import android.app.Application
-import com.shopify.buy3.GraphCallResult
-import com.shopify.buy3.GraphClient
-import com.shopify.buy3.RetryHandler
-import com.shopify.buy3.Storefront.*
-import com.shopify.graphql.support.ID
-import com.shopify.graphql.support.Input
-import com.tangem.datasource.config.models.ShopifyShop
-import com.tangem.tap.common.shop.shopify.data.CheckoutItem
-import com.tangem.tap.common.shop.shopify.data.checkoutFieldsFragment
-import com.tangem.tap.common.shop.shopify.data.collectionFieldsFragment
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.withContext
-import java.util.concurrent.TimeUnit
-import kotlin.coroutines.resume
-import kotlin.coroutines.suspendCoroutine
-
-@Suppress("LargeClass")
-class ShopifyService(private val application: Application, val shop: ShopifyShop) {
- private val client: GraphClient by lazy { initClient() }
-
- @Suppress("MagicNumber")
- suspend fun getProducts(collectionTitleFilter: String? = null): Result> {
- val filter = collectionTitleFilter?.let { "title:\"$it\"" }
-
- val query = query { rootQuery: QueryRootQuery ->
- rootQuery.collections(
- { arg -> arg.first(250).query(filter) },
- CollectionConnectionQuery::collectionFieldsFragment,
- )
- }
- return when (val result = queryAsync(query)) {
- is GraphCallResult.Success -> {
- val products = result.response.data!!.collections.edges
- .map { it.node.products }
- .flatMap { it.edges }
- .map { it.node }
- Result.success(products)
- }
- is GraphCallResult.Failure -> {
- Result.failure(result.error)
- }
- }
- }
-
- suspend fun checkout(pollUntilOrder: Boolean, checkoutID: ID): Result {
- val query = query { rootQuery: QueryRootQuery ->
- rootQuery
- .node(checkoutID) { query ->
- query.onCheckout { checkoutQuery ->
- with(checkoutQuery) {
- checkoutFieldsFragment()
- }
- }
- }
- }
- val retryHandler = RetryHandler.build(
- delay = 1,
- timeUnit = TimeUnit.SECONDS,
- ) {
- this.retryWhen { result ->
- when (result) {
- is GraphCallResult.Success -> {
- val checkout = result.response.data?.node as? Checkout
- checkout?.order == null
- }
- is GraphCallResult.Failure -> false
- }
- }
- }
-
- val result = if (pollUntilOrder) queryAsync(query, retryHandler) else queryAsync(query)
- return when (result) {
- is GraphCallResult.Success -> {
- val checkout = result.response.data!!.node as? Checkout
- if (checkout != null) {
- Result.success(checkout)
- } else {
- Result.failure(ShopifyError.Unknown)
- }
- }
- is GraphCallResult.Failure -> {
- Result.failure(result.error)
- }
- }
- }
-
- suspend fun createCheckout(checkoutItems: List, checkoutID: ID? = null): Result {
- val storefrontLineItems: MutableList = checkoutItems
- .map { CheckoutLineItemInput(it.quantity, it.id) }.toMutableList()
-
- val query = if (checkoutID != null) {
- mutation { mutationQuery: MutationQuery ->
- mutationQuery
- .checkoutLineItemsReplace(
- storefrontLineItems,
- checkoutID,
- ) { payloadQuery: CheckoutLineItemsReplacePayloadQuery ->
- payloadQuery
- .checkout { checkoutQuery: CheckoutQuery ->
- checkoutQuery.checkoutFieldsFragment()
- }
- .userErrors { userErrorQuery: CheckoutUserErrorQuery ->
- userErrorQuery
- .field()
- .message()
- }
- }
- }
- } else {
- val input = CheckoutCreateInput()
- .setLineItemsInput(
- Input.value(storefrontLineItems),
- )
- mutation { mutationQuery: MutationQuery ->
- mutationQuery
- .checkoutCreate(
- input,
- ) { payloadQuery: CheckoutCreatePayloadQuery ->
- payloadQuery
- .checkout { checkoutQuery: CheckoutQuery ->
- checkoutQuery.checkoutFieldsFragment()
- }
- .checkoutUserErrors { userErrorQuery: CheckoutUserErrorQuery ->
- userErrorQuery
- .field()
- .message()
- }
- }
- }
- }
- return runCheckoutMutation(query)
- }
-
- suspend fun applyDiscount(discountCode: String, checkoutID: ID): Result {
- val query = mutation { mutationQuery: MutationQuery ->
- mutationQuery
- .checkoutDiscountCodeApplyV2(
- discountCode,
- checkoutID,
- ) { payloadQuery: CheckoutDiscountCodeApplyV2PayloadQuery ->
- payloadQuery
- .checkout { checkoutQuery: CheckoutQuery ->
- checkoutQuery.checkoutFieldsFragment()
- }
- .checkoutUserErrors { userErrorQuery: CheckoutUserErrorQuery ->
- userErrorQuery
- .field()
- .message()
- }
- }
- }
- return runCheckoutMutation(query)
- }
-
- suspend fun removeDiscount(checkoutID: ID): Result {
- val query = mutation { mutationQuery: MutationQuery ->
- mutationQuery
- .checkoutDiscountCodeRemove(
- checkoutID,
- ) { payloadQuery: CheckoutDiscountCodeRemovePayloadQuery ->
- payloadQuery
- .checkout { checkoutQuery: CheckoutQuery ->
- checkoutQuery.checkoutFieldsFragment()
- }
- .checkoutUserErrors { userErrorQuery: CheckoutUserErrorQuery ->
- userErrorQuery
- .field()
- .message()
- }
- }
- }
- return runCheckoutMutation(query)
- }
-
- suspend fun completeWithTokenizedPayment(payment: TokenizedPaymentInputV3, checkoutID: ID): Result {
- val query = mutation { mutationQuery: MutationQuery ->
- mutationQuery
- .checkoutCompleteWithTokenizedPaymentV3(
- checkoutID,
- payment,
- ) { payloadQuery: CheckoutCompleteWithTokenizedPaymentV3PayloadQuery ->
- payloadQuery
- .payment { paymentQuery: PaymentQuery ->
- paymentQuery
- .ready()
- .errorMessage()
- }
- .checkout { checkoutQuery: CheckoutQuery ->
- checkoutQuery
- .ready()
- }
- .checkoutUserErrors { userErrorQuery: CheckoutUserErrorQuery ->
- userErrorQuery
- .field()
- .message()
- }
- }
- }
- return runCheckoutMutation(query)
- }
-
- private suspend fun runCheckoutMutation(mutation: MutationQuery): Result {
- return when (val result = mutationQueryAsync(mutation)) {
- is GraphCallResult.Success -> {
- val checkout = result.response.data!!.checkoutCreate?.checkout
- ?: result.response.data!!.checkoutDiscountCodeApplyV2?.checkout
- ?: result.response.data!!.checkoutDiscountCodeRemove?.checkout
- ?: result.response.data!!.checkoutShippingAddressUpdateV2?.checkout
- ?: result.response.data!!.checkoutEmailUpdateV2?.checkout
- ?: result.response.data!!.checkoutShippingLineUpdate?.checkout
- ?: result.response.data!!.checkoutCompleteWithTokenizedPaymentV3.checkout
-
- Result.success(checkout)
- }
- is GraphCallResult.Failure -> Result.failure(result.error)
- }
- }
-
- private suspend fun queryAsync(
- query: QueryRootQuery,
- retryHandler: RetryHandler,
- ): GraphCallResult = withContext(Dispatchers.IO) {
- suspendCoroutine { continuation ->
- client.queryGraph(query).enqueue(retryHandler = retryHandler) { result ->
- continuation.resume(result)
- }
- }
- }
-
- private suspend fun queryAsync(query: QueryRootQuery): GraphCallResult = withContext(Dispatchers.IO) {
- suspendCoroutine { continuation ->
- client.queryGraph(query).enqueue { result ->
- continuation.resume(result)
- }
- }
- }
-
- private suspend fun mutationQueryAsync(query: MutationQuery): GraphCallResult =
- withContext(Dispatchers.IO) {
- suspendCoroutine { continuation ->
- client.mutateGraph(query).enqueue { result ->
- continuation.resume(result)
- }
- }
- }
-
- private fun initClient(): GraphClient {
- return GraphClient.build(
- application,
- shop.domain,
- shop.storefrontApiKey,
- ) {
-// httpCache(application.filesDir) {
-// cacheMaxSizeBytes = (1024 * 1024 * 10)
-// defaultCachePolicy =
-// HttpCachePolicy.Default.CACHE_FIRST.expireAfter(20, TimeUnit.MINUTES)
-// }
- }
- }
-}
-
-sealed class ShopifyError : Throwable() {
- object Unknown : ShopifyError()
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/common/shop/shopify/data/Checkout.kt b/app/src/main/java/com/tangem/tap/common/shop/shopify/data/Checkout.kt
deleted file mode 100644
index 7565db2447..0000000000
--- a/app/src/main/java/com/tangem/tap/common/shop/shopify/data/Checkout.kt
+++ /dev/null
@@ -1,135 +0,0 @@
-package com.tangem.tap.common.shop.shopify.data
-
-import com.shopify.buy3.Storefront
-
-@Suppress("LongMethod", "MagicNumber")
-fun Storefront.CheckoutQuery.checkoutFieldsFragment() {
- ready()
- webUrl()
- currencyCode()
- lineItemsSubtotalPrice { it.amount() }
- totalPriceV2 {
- it.currencyCode()
- it.amount()
- }
- lineItems({ arg -> arg.first(250) }) {
- it.edges {
- it.node {
-// it.id()
- it.title()
- it.quantity()
- it.variant {
- it.priceV2 { it.amount() }
- }
- }
- }
- }
- shippingLine {
- it.handle()
- it.title()
- it.priceV2 { it.amount() }
- }
- availableShippingRates {
- it.ready()
- it.shippingRates {
- it.handle()
- it.title()
- it.priceV2 { it.amount() }
- }
- }
- shippingAddress {
- it.address1()
- it.address2()
- it.city()
-// .company()
- it.country()
-// .countryCodeV2()
- it.firstName()
-// .formatted()
-// .formattedArea()
-// .id()
- it.lastName()
-// .latitude()
-// .longitude()
-// .name()
- it.phone()
- it.province()
-// .provinceCode()
- it.zip()
- }
- discountApplications({ arg -> arg.first(250) }) {
- it.edges {
- it.node {
- it.onDiscountCodeApplication {
- it.code()
-// .applicable()
-// .allocationMethod()
-// .targetSelection()
-// .targetType()
- it.value {
- it.onMoneyV2 {
- it.amount()
- }
- it.onPricingPercentageValue {
- it.percentage()
- }
- }
- }
- }
- }
- }
- order {
- it.cancelReason()
- it.canceledAt()
- it.currencyCode()
-// .currentSubtotalPrice()
-// .currentTotalDuties()
-// .currentTotalPrice()
-// .currentTotalTax()
- it.customerLocale()
- it.customerUrl()
-// .discountApplications()
- it.edited()
- it.email()
- it.financialStatus()
- it.fulfillmentStatus()
-// .id()
-// .lineItems()
-// .metafield()
-// .metafields()
- it.name()
- it.orderNumber()
-// .originalTotalDuties()
-// .originalTotalPrice()
- it.phone()
- it.processedAt()
- it.shippingAddress {
- it.address1()
- it.address2()
- it.city()
- it.company()
- it.country()
- it.countryCodeV2()
- it.firstName()
- it.formatted()
- it.formattedArea()
-// .id()
- it.lastName()
- it.latitude()
- it.longitude()
- it.name()
- it.phone()
- it.province()
- it.provinceCode()
- it.zip()
- }
-// .shippingDiscountAllocations()
- it.statusUrl()
-// .subtotalPriceV2()
-// .successfulFulfillments()
-// .totalPriceV2()
-// .totalRefundedV2()
-// .totalShippingPriceV2()
-// .totalTaxV2()
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/common/shop/shopify/data/CheckoutItem.kt b/app/src/main/java/com/tangem/tap/common/shop/shopify/data/CheckoutItem.kt
deleted file mode 100644
index 6d16e66908..0000000000
--- a/app/src/main/java/com/tangem/tap/common/shop/shopify/data/CheckoutItem.kt
+++ /dev/null
@@ -1,8 +0,0 @@
-package com.tangem.tap.common.shop.shopify.data
-
-import com.shopify.graphql.support.ID
-
-data class CheckoutItem(
- val id: ID,
- val quantity: Int,
-)
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/common/shop/shopify/data/Collection.kt b/app/src/main/java/com/tangem/tap/common/shop/shopify/data/Collection.kt
deleted file mode 100644
index 1527fc9558..0000000000
--- a/app/src/main/java/com/tangem/tap/common/shop/shopify/data/Collection.kt
+++ /dev/null
@@ -1,52 +0,0 @@
-package com.tangem.tap.common.shop.shopify.data
-
-import com.shopify.buy3.Storefront
-
-@Suppress("MagicNumber")
-fun Storefront.CollectionConnectionQuery.collectionFieldsFragment() {
- edges { collectionEdgeQuery ->
- collectionEdgeQuery
- .node { collectionQuery ->
- collectionQuery
- .title()
- .products(
- { arg -> arg.first(250) },
- ) { productConnectionQuery ->
- productConnectionQuery
- .edges { productEdgeQuery ->
- productEdgeQuery
- .node { productQuery ->
- productQuery.title()
- .productType()
- .description()
- .variants({ arg -> arg.first(10) }) { variantConnectionQuery ->
- variantConnectionQuery.edges { variantQuery ->
- variantQuery.node {
- it.title()
- it.sku()
- it.currentlyNotInStock()
- it.priceV2 {
- it.amount()
- it.currencyCode()
- }
- it.compareAtPriceV2 {
- it.amount()
- it.currencyCode()
- }
- it.compareAtPriceV2 {
- it.amount()
- }
- it.product {
- it.title()
- .productType()
- .description()
- }
- }
- }
- }
- }
- }
- }
- }
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/data/RuntimeUserWalletsStore.kt b/app/src/main/java/com/tangem/tap/data/RuntimeUserWalletsStore.kt
index 884fa65323..87b5944231 100644
--- a/app/src/main/java/com/tangem/tap/data/RuntimeUserWalletsStore.kt
+++ b/app/src/main/java/com/tangem/tap/data/RuntimeUserWalletsStore.kt
@@ -22,6 +22,12 @@ internal class RuntimeUserWalletsStore(
?.singleOrNull { it.walletId == key }
}
+ override suspend fun getAllSyncOrNull(): List? {
+ return walletsStateHolder.userWalletsListManager
+ ?.userWallets
+ ?.firstOrNull()
+ }
+
override suspend fun update(userWalletId: UserWalletId, update: suspend (UserWallet) -> UserWallet) {
walletsStateHolder.userWalletsListManager?.update(userWalletId, update)
}
diff --git a/app/src/main/java/com/tangem/tap/di/domain/CardDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/CardDomainModule.kt
index 76aa092682..084134986d 100644
--- a/app/src/main/java/com/tangem/tap/di/domain/CardDomainModule.kt
+++ b/app/src/main/java/com/tangem/tap/di/domain/CardDomainModule.kt
@@ -69,4 +69,12 @@ internal object CardDomainModule {
fun provideIsNeedToBackupUseCase(walletStateHolder: WalletsStateHolder): IsNeedToBackupUseCase {
return IsNeedToBackupUseCase(walletStateHolder)
}
+
+ @Provides
+ @ViewModelScoped
+ fun provideGetExtendedPublicKeyForCurrencyUseCase(
+ derivationsRepository: DerivationsRepository,
+ ): GetExtendedPublicKeyForCurrencyUseCase {
+ return GetExtendedPublicKeyForCurrencyUseCase(derivationsRepository)
+ }
}
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/di/domain/FeedbackDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/FeedbackDomainModule.kt
new file mode 100644
index 0000000000..f2aea82f57
--- /dev/null
+++ b/app/src/main/java/com/tangem/tap/di/domain/FeedbackDomainModule.kt
@@ -0,0 +1,25 @@
+package com.tangem.tap.di.domain
+
+import android.content.Context
+import com.tangem.domain.feedback.GetSupportFeedbackEmailUseCase
+import com.tangem.domain.feedback.repository.FeedbackRepository
+import dagger.Module
+import dagger.Provides
+import dagger.hilt.InstallIn
+import dagger.hilt.android.qualifiers.ApplicationContext
+import dagger.hilt.components.SingletonComponent
+import javax.inject.Singleton
+
+@Module
+@InstallIn(SingletonComponent::class)
+internal object FeedbackDomainModule {
+
+ @Provides
+ @Singleton
+ fun provideGetFeedbackToSupportUseCase(
+ feedbackRepository: FeedbackRepository,
+ @ApplicationContext context: Context,
+ ): GetSupportFeedbackEmailUseCase {
+ return GetSupportFeedbackEmailUseCase(feedbackRepository = feedbackRepository, resources = context.resources)
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/di/domain/QrScanningDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/QrScanningDomainModule.kt
index ba2c69965b..ec86de8c46 100644
--- a/app/src/main/java/com/tangem/tap/di/domain/QrScanningDomainModule.kt
+++ b/app/src/main/java/com/tangem/tap/di/domain/QrScanningDomainModule.kt
@@ -1,7 +1,9 @@
package com.tangem.tap.di.domain
-import com.tangem.feature.qrscanning.repo.QrScanningEventsRepository
-import com.tangem.feature.qrscanning.usecase.ListenToQrScanningUseCase
+import com.tangem.domain.qrscanning.repository.QrScanningEventsRepository
+import com.tangem.domain.qrscanning.usecases.EmitQrScannedEventUseCase
+import com.tangem.domain.qrscanning.usecases.ListenToQrScanningUseCase
+import com.tangem.domain.qrscanning.usecases.ParseQrCodeUseCase
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
@@ -17,4 +19,16 @@ internal object QrScanningDomainModule {
fun provideListenToQrScanUseCase(repository: QrScanningEventsRepository): ListenToQrScanningUseCase {
return ListenToQrScanningUseCase(repository)
}
+
+ @Provides
+ @Singleton
+ fun provideEmitQrScannedEventUseCase(repository: QrScanningEventsRepository): EmitQrScannedEventUseCase {
+ return EmitQrScannedEventUseCase(repository)
+ }
+
+ @Provides
+ @Singleton
+ fun provideParseQrCodeUseCase(repository: QrScanningEventsRepository): ParseQrCodeUseCase {
+ return ParseQrCodeUseCase(repository)
+ }
}
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/di/domain/SettingsDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/SettingsDomainModule.kt
index 7334227d99..3c9fb7b2a9 100644
--- a/app/src/main/java/com/tangem/tap/di/domain/SettingsDomainModule.kt
+++ b/app/src/main/java/com/tangem/tap/di/domain/SettingsDomainModule.kt
@@ -120,4 +120,24 @@ internal object SettingsDomainModule {
): ShouldShowSwapPromoTokenUseCase {
return ShouldShowSwapPromoTokenUseCase(swapPromoRepository)
}
+
+ @Provides
+ @ViewModelScoped
+ fun provideDeleteDeprecatedLogsUseCase(settingsRepository: SettingsRepository): DeleteDeprecatedLogsUseCase {
+ return DeleteDeprecatedLogsUseCase(settingsRepository)
+ }
+
+ @Provides
+ @ViewModelScoped
+ fun provideIsSendTapHelpPreviewEnabledUseCase(
+ settingsRepository: SettingsRepository,
+ ): IsSendTapHelpEnabledUseCase {
+ return IsSendTapHelpEnabledUseCase(settingsRepository = settingsRepository)
+ }
+
+ @Provides
+ @ViewModelScoped
+ fun provideNeverShowTapHelpUseCase(settingsRepository: SettingsRepository): NeverShowTapHelpUseCase {
+ return NeverShowTapHelpUseCase(settingsRepository = settingsRepository)
+ }
}
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt
index 2e373e7c69..81d84db2d2 100644
--- a/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt
+++ b/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt
@@ -6,6 +6,7 @@ import com.tangem.domain.tokens.*
import com.tangem.domain.tokens.repository.*
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.feature.swap.domain.api.SwapRepository
+import com.tangem.features.send.api.featuretoggles.SendFeatureToggles
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.Module
import dagger.Provides
@@ -15,7 +16,7 @@ import dagger.hilt.android.scopes.ViewModelScoped
@Module
@InstallIn(ViewModelComponent::class)
-@Suppress("TooManyFunctions")
+@Suppress("TooManyFunctions", "LargeClass")
internal object TokensDomainModule {
@Provides
@@ -191,6 +192,7 @@ internal object TokensDomainModule {
currenciesRepository: CurrenciesRepository,
quotesRepository: QuotesRepository,
networksRepository: NetworksRepository,
+ sendFeatureToggles: SendFeatureToggles,
dispatchers: CoroutineDispatcherProvider,
): GetCryptoCurrencyActionsUseCase {
return GetCryptoCurrencyActionsUseCase(
@@ -199,6 +201,7 @@ internal object TokensDomainModule {
currenciesRepository = currenciesRepository,
quotesRepository = quotesRepository,
networksRepository = networksRepository,
+ sendFeatureToggles = sendFeatureToggles,
dispatchers = dispatchers,
)
}
@@ -278,9 +281,17 @@ internal object TokensDomainModule {
@Provides
@ViewModelScoped
fun provideCheckTokenCompatibilityUseCase(
- repository: NetworksCompatibilityRepository,
+ networksCompatibilityRepository: NetworksCompatibilityRepository,
): CheckCurrencyCompatibilityUseCase {
- return CheckCurrencyCompatibilityUseCase(repository)
+ return CheckCurrencyCompatibilityUseCase(networksCompatibilityRepository)
+ }
+
+ @Provides
+ @ViewModelScoped
+ fun provideNeedHardenedDerivationUseCase(
+ networksCompatibilityRepository: NetworksCompatibilityRepository,
+ ): RequiresHardenedDerivationOnlyUseCase {
+ return RequiresHardenedDerivationOnlyUseCase(networksCompatibilityRepository)
}
@Provides
diff --git a/app/src/main/java/com/tangem/tap/di/domain/WalletManagersFacadeModule.kt b/app/src/main/java/com/tangem/tap/di/domain/WalletManagersFacadeModule.kt
index 3fedeef4ca..319ddd2c0e 100644
--- a/app/src/main/java/com/tangem/tap/di/domain/WalletManagersFacadeModule.kt
+++ b/app/src/main/java/com/tangem/tap/di/domain/WalletManagersFacadeModule.kt
@@ -3,11 +3,13 @@ package com.tangem.tap.di.domain
import com.squareup.moshi.Moshi
import com.tangem.blockchain.common.AccountCreator
import com.tangem.blockchain.common.datastorage.BlockchainDataStorage
+import com.tangem.blockchain.common.logging.BlockchainSDKLogger
import com.tangem.datasource.asset.AssetReader
import com.tangem.datasource.config.ConfigManager
import com.tangem.datasource.di.SdkMoshi
import com.tangem.datasource.local.userwallet.UserWalletsStore
import com.tangem.datasource.local.walletmanager.WalletManagersStore
+import com.tangem.domain.feedback.FeedbackManagerFeatureToggles
import com.tangem.domain.walletmanager.DefaultWalletManagersFacade
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.feature.onboarding.data.MnemonicRepository
@@ -32,6 +34,8 @@ internal object WalletManagersFacadeModule {
mnemonicRepository: MnemonicRepository,
assetReader: AssetReader,
@SdkMoshi moshi: Moshi,
+ blockchainSDKLogger: BlockchainSDKLogger,
+ feedbackManagerFeatureToggles: FeedbackManagerFeatureToggles,
): WalletManagersFacade {
return DefaultWalletManagersFacade(
walletManagersStore = walletManagersStore,
@@ -42,6 +46,8 @@ internal object WalletManagersFacadeModule {
moshi = moshi,
mnemonic = mnemonicRepository.generateDefaultMnemonic(),
accountCreator = accountCreator,
+ blockchainSDKLogger = blockchainSDKLogger,
+ feedbackManagerFeatureToggles = feedbackManagerFeatureToggles,
)
}
}
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/domain/TangemSdkManager.kt b/app/src/main/java/com/tangem/tap/domain/TangemSdkManager.kt
index 94b72fcf1e..4097061289 100644
--- a/app/src/main/java/com/tangem/tap/domain/TangemSdkManager.kt
+++ b/app/src/main/java/com/tangem/tap/domain/TangemSdkManager.kt
@@ -26,6 +26,8 @@ import com.tangem.operations.derivation.DerivationTaskResponse
import com.tangem.operations.derivation.DeriveMultipleWalletPublicKeysTask
import com.tangem.operations.pins.SetUserCodeCommand
import com.tangem.operations.usersetttings.SetUserCodeRecoveryAllowedTask
+import com.tangem.crypto.hdWallet.bip32.ExtendedPublicKey
+import com.tangem.operations.derivation.DeriveWalletPublicKeyTask
import com.tangem.tap.derivationsFinder
import com.tangem.tap.domain.tasks.product.CreateProductWalletTask
import com.tangem.tap.domain.tasks.product.CreateProductWalletTaskResponse
@@ -104,6 +106,7 @@ class TangemSdkManager(
suspend fun importWallet(
scanResponse: ScanResponse,
mnemonic: String,
+ passphrase: String?,
shouldReset: Boolean,
): CompletionResult {
val defaultMnemonic = try {
@@ -116,6 +119,7 @@ class TangemSdkManager(
cardTypesResolver = scanResponse.cardTypesResolver,
derivationStyleProvider = scanResponse.derivationStyleProvider,
mnemonic = defaultMnemonic,
+ passphrase = passphrase,
shouldReset = shouldReset,
),
scanResponse.card.cardId,
@@ -138,6 +142,17 @@ class TangemSdkManager(
return runTaskAsyncReturnOnMain(DeriveMultipleWalletPublicKeysTask(derivations), cardId)
}
+ suspend fun deriveExtendedPublicKey(
+ cardId: String?,
+ walletPublicKey: ByteArray,
+ derivation: DerivationPath,
+ ): CompletionResult = withContext(Dispatchers.Main) {
+ runTaskAsyncReturnOnMain(
+ DeriveWalletPublicKeyTask(walletPublicKey, derivation),
+ cardId,
+ )
+ }
+
suspend fun resetToFactorySettings(
cardId: String,
allowsRequestAccessCodeFromRepository: Boolean,
@@ -264,7 +279,7 @@ class TangemSdkManager(
allowedCardTypes = FirmwareVersion.FirmwareType.values().toList(),
maxFirmwareVersion = FirmwareVersion(major = 6, minor = 33),
batchIdFilter = CardFilter.Companion.ItemFilter.Deny(
- items = setOf("0027", "0030", "0031", "0035", "DA88"),
+ items = setOf("0027", "0030", "0031", "0035"),
),
),
)
diff --git a/app/src/main/java/com/tangem/tap/domain/TapWalletManager.kt b/app/src/main/java/com/tangem/tap/domain/TapWalletManager.kt
index 03f3022a51..f8a93381e8 100644
--- a/app/src/main/java/com/tangem/tap/domain/TapWalletManager.kt
+++ b/app/src/main/java/com/tangem/tap/domain/TapWalletManager.kt
@@ -47,6 +47,11 @@ class TapWalletManager(
config = blockchainSdkConfig,
accountCreator = store.inject(DaggerGraphState::accountCreator),
blockchainDataStorage = store.inject(DaggerGraphState::blockchainDataStorage),
+ loggers = if (store.inject(DaggerGraphState::feedbackManagerFeatureToggles).isLocalLogsEnabled) {
+ listOf(store.inject(DaggerGraphState::blockchainSDKLogger))
+ } else {
+ emptyList()
+ },
)
}
diff --git a/app/src/main/java/com/tangem/tap/domain/card/DefaultDerivationsRepository.kt b/app/src/main/java/com/tangem/tap/domain/card/DefaultDerivationsRepository.kt
index 4b2f3852b6..84d55370ed 100644
--- a/app/src/main/java/com/tangem/tap/domain/card/DefaultDerivationsRepository.kt
+++ b/app/src/main/java/com/tangem/tap/domain/card/DefaultDerivationsRepository.kt
@@ -1,12 +1,15 @@
package com.tangem.tap.domain.card
+import com.tangem.common.CompletionResult
import com.tangem.common.doOnFailure
import com.tangem.common.doOnSuccess
import com.tangem.common.extensions.ByteArrayKey
import com.tangem.crypto.hdWallet.DerivationPath
+import com.tangem.crypto.hdWallet.bip32.ExtendedPublicKey
import com.tangem.datasource.local.userwallet.UserWalletsStore
import com.tangem.domain.card.repository.DerivationsRepository
import com.tangem.domain.tokens.model.CryptoCurrency
+import com.tangem.domain.userwallets.UserWalletIdBuilder
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.operations.derivation.ExtendedPublicKeysMap
@@ -51,6 +54,28 @@ internal class DefaultDerivationsRepository(
error("This code should never be reached")
}
+ override suspend fun deriveExtendedPublicKey(
+ userWalletId: UserWalletId,
+ derivation: DerivationPath,
+ ): ExtendedPublicKey? {
+ val userWallet = userWalletsStore.getSyncOrNull(userWalletId) ?: error("User wallet not found")
+ val walletCard = userWallet.scanResponse.card.wallets.firstOrNull {
+ UserWalletIdBuilder.scanResponse(userWallet.scanResponse).build()?.value
+ .contentEquals(userWallet.walletId.value)
+ } ?: return null
+
+ val result = tangemSdkManager.deriveExtendedPublicKey(
+ cardId = null,
+ walletPublicKey = walletCard.publicKey,
+ derivation = derivation,
+ )
+
+ return when (result) {
+ is CompletionResult.Failure -> throw result.error
+ is CompletionResult.Success -> result.data
+ }
+ }
+
private suspend fun updatePublicKeys(userWalletId: UserWalletId, keys: DerivedKeys): Result {
return runCatching(dispatchers.io) {
userWalletsStore.update(
diff --git a/app/src/main/java/com/tangem/tap/domain/scanCard/DefaultScanCardProcessor.kt b/app/src/main/java/com/tangem/tap/domain/scanCard/DefaultScanCardProcessor.kt
index c72d366b37..d4b5ac160c 100644
--- a/app/src/main/java/com/tangem/tap/domain/scanCard/DefaultScanCardProcessor.kt
+++ b/app/src/main/java/com/tangem/tap/domain/scanCard/DefaultScanCardProcessor.kt
@@ -30,7 +30,6 @@ internal class DefaultScanCardProcessor : ScanCardProcessor {
analyticsSource: AnalyticsParam.ScreensSources,
cardId: String?,
onProgressStateChange: suspend (showProgress: Boolean) -> Unit,
- onScanStateChange: suspend (scanInProgress: Boolean) -> Unit,
onWalletNotCreated: suspend () -> Unit,
disclaimerWillShow: () -> Unit,
onFailure: suspend (error: TangemError) -> Unit,
@@ -41,7 +40,6 @@ internal class DefaultScanCardProcessor : ScanCardProcessor {
analyticsSource,
cardId,
onProgressStateChange,
- onScanStateChange,
onWalletNotCreated,
disclaimerWillShow,
onFailure,
@@ -52,7 +50,6 @@ internal class DefaultScanCardProcessor : ScanCardProcessor {
analyticsSource,
cardId,
onProgressStateChange,
- onScanStateChange,
onWalletNotCreated,
disclaimerWillShow,
onFailure,
diff --git a/app/src/main/java/com/tangem/tap/domain/scanCard/LegacyScanProcessor.kt b/app/src/main/java/com/tangem/tap/domain/scanCard/LegacyScanProcessor.kt
index 9164a9bfd6..ce649ece46 100644
--- a/app/src/main/java/com/tangem/tap/domain/scanCard/LegacyScanProcessor.kt
+++ b/app/src/main/java/com/tangem/tap/domain/scanCard/LegacyScanProcessor.kt
@@ -50,14 +50,12 @@ internal object LegacyScanProcessor {
analyticsSource: AnalyticsParam.ScreensSources,
cardId: String?,
onProgressStateChange: suspend (showProgress: Boolean) -> Unit,
- onScanStateChange: suspend (scanInProgress: Boolean) -> Unit,
onWalletNotCreated: suspend () -> Unit,
disclaimerWillShow: () -> Unit,
onFailure: suspend (error: TangemError) -> Unit,
onSuccess: suspend (scanResponse: ScanResponse) -> Unit,
) = withMainContext {
onProgressStateChange(true)
- onScanStateChange(true)
tangemSdkManager.changeDisplayedCardIdNumbersCount(null)
@@ -68,13 +66,11 @@ internal object LegacyScanProcessor {
result
.doOnFailure { error ->
- onScanStateChange(false)
onFailure(error)
}
.doOnSuccess { scanResponse ->
tangemSdkManager.changeDisplayedCardIdNumbersCount(scanResponse)
- onScanStateChange(false)
sendAnalytics(analyticsEvent, scanResponse)
showDisclaimerIfNeed(
diff --git a/app/src/main/java/com/tangem/tap/domain/scanCard/UseCaseScanProcessor.kt b/app/src/main/java/com/tangem/tap/domain/scanCard/UseCaseScanProcessor.kt
index 2afbd6d813..ed777bd666 100644
--- a/app/src/main/java/com/tangem/tap/domain/scanCard/UseCaseScanProcessor.kt
+++ b/app/src/main/java/com/tangem/tap/domain/scanCard/UseCaseScanProcessor.kt
@@ -3,12 +3,15 @@ package com.tangem.tap.domain.scanCard
import arrow.fx.coroutines.resourceScope
import com.tangem.common.CompletionResult
import com.tangem.common.core.TangemError
+import com.tangem.core.analytics.Analytics
import com.tangem.core.analytics.models.AnalyticsParam
import com.tangem.core.analytics.models.Basic
import com.tangem.core.navigation.AppScreen
import com.tangem.core.navigation.NavigationAction
+import com.tangem.core.navigation.StateDialog
import com.tangem.domain.card.ScanCardException
import com.tangem.domain.models.scan.ScanResponse
+import com.tangem.tap.common.extensions.dispatchDialogShow
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.extensions.inject
import com.tangem.tap.domain.scanCard.chains.*
@@ -26,9 +29,15 @@ internal object UseCaseScanProcessor {
allowsRequestAccessCodeFromRepository: Boolean = false,
): CompletionResult {
val scanCardUseCase = store.inject(DaggerGraphState::scanCardUseCase)
+
return scanCardUseCase(cardId, allowsRequestAccessCodeFromRepository)
.fold(
- ifLeft = { CompletionResult.Failure(scanCardExceptionConverter.convertBack(it)) },
+ ifLeft = {
+ val error = scanCardExceptionConverter.convertBack(it)
+
+ Analytics.send(Basic.ScanError(error))
+ CompletionResult.Failure(error)
+ },
ifRight = { CompletionResult.Success(it) },
)
}
@@ -38,25 +47,37 @@ internal object UseCaseScanProcessor {
analyticsSource: AnalyticsParam.ScreensSources,
cardId: String?,
onProgressStateChange: suspend (showProgress: Boolean) -> Unit,
- onScanStateChange: suspend (scanInProgress: Boolean) -> Unit,
onWalletNotCreated: suspend () -> Unit,
disclaimerWillShow: () -> Unit,
onFailure: suspend (error: TangemError) -> Unit,
onSuccess: suspend (scanResponse: ScanResponse) -> Unit,
) = progressScope(onProgressStateChange) {
- onScanStateChange(true)
-
val scanCardUseCase = store.inject(DaggerGraphState::scanCardUseCase)
val chains = buildList {
- add(ScanningFinishedChain { onScanStateChange(false) })
+ add(
+ FailedScansCounterChain(
+ { showMaxUnsuccessfulScansReachedDialog(analyticsSource) },
+ ),
+ )
add(AnalyticsChain(Basic.CardWasScanned(analyticsSource)))
add(DisclaimerChain(store, disclaimerWillShow))
add(CheckForOnboardingChain(store, store.state.globalState.tapWalletManager))
}
- scanCardUseCase(cardId, afterScanChains = chains)
- .map { onSuccess(it) }
- .mapLeft { proceedWithException(it, onWalletNotCreated, onFailure) }
+ scanCardUseCase(cardId, afterScanChains = chains).fold(
+ ifLeft = { proceedWithException(it, onWalletNotCreated, onFailure) },
+ ifRight = { onSuccess(it) },
+ )
+ }
+
+ private fun showMaxUnsuccessfulScansReachedDialog(source: AnalyticsParam.ScreensSources) {
+ val scanFailsSource = when (source) {
+ is AnalyticsParam.ScreensSources.SignIn -> StateDialog.ScanFailsSource.SIGN_IN
+ is AnalyticsParam.ScreensSources.Settings -> StateDialog.ScanFailsSource.SETTINGS
+ is AnalyticsParam.ScreensSources.Intro -> StateDialog.ScanFailsSource.INTRO
+ else -> StateDialog.ScanFailsSource.MAIN
+ }
+ store.dispatchDialogShow(StateDialog.ScanFailsDialog(scanFailsSource))
}
private suspend fun proceedWithException(
@@ -74,7 +95,12 @@ internal object UseCaseScanProcessor {
is ScanCardException.UserCancelled,
is ScanCardException.WrongAccessCode,
is ScanCardException.WrongCardId,
- -> onFailure(scanCardExceptionConverter.convertBack(exception))
+ -> {
+ val error = scanCardExceptionConverter.convertBack(exception)
+
+ Analytics.send(Basic.ScanError(error))
+ onFailure(error)
+ }
}
}
@@ -88,8 +114,9 @@ internal object UseCaseScanProcessor {
navigateTo(exception.onboardingRoute)
onWalletNotCreated()
}
- is ScanChainException.DisclaimerWasCanceled,
- -> onFailure(scanCardExceptionConverter.convertBack(exception))
+ is ScanChainException.DisclaimerWasCanceled -> {
+ onFailure(scanCardExceptionConverter.convertBack(exception))
+ }
}
}
diff --git a/app/src/main/java/com/tangem/tap/domain/scanCard/chains/AnalyticsChain.kt b/app/src/main/java/com/tangem/tap/domain/scanCard/chains/AnalyticsChain.kt
index 8f72ce92f8..c7c56864ec 100644
--- a/app/src/main/java/com/tangem/tap/domain/scanCard/chains/AnalyticsChain.kt
+++ b/app/src/main/java/com/tangem/tap/domain/scanCard/chains/AnalyticsChain.kt
@@ -1,11 +1,11 @@
package com.tangem.tap.domain.scanCard.chains
-import arrow.core.Either
import arrow.core.right
import com.tangem.core.analytics.Analytics
import com.tangem.core.analytics.models.AnalyticsEvent
import com.tangem.domain.card.ScanCardException
import com.tangem.domain.core.chain.Chain
+import com.tangem.domain.core.chain.ResultChain
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.tap.common.analytics.paramsInterceptor.CardContextInterceptor
@@ -19,11 +19,9 @@ import com.tangem.tap.common.analytics.paramsInterceptor.CardContextInterceptor
*/
class AnalyticsChain(
private val event: AnalyticsEvent,
-) : Chain {
+) : ResultChain() {
- override suspend fun invoke(
- previousChainResult: ScanResponse,
- ): Either {
+ override suspend fun launch(previousChainResult: ScanResponse): ScanChainResult {
val interceptor = CardContextInterceptor(previousChainResult)
val params = event.params.toMutableMap()
interceptor.intercept(params)
diff --git a/app/src/main/java/com/tangem/tap/domain/scanCard/chains/CheckForOnboardingChain.kt b/app/src/main/java/com/tangem/tap/domain/scanCard/chains/CheckForOnboardingChain.kt
index 3db1f7a79e..66f708e28d 100644
--- a/app/src/main/java/com/tangem/tap/domain/scanCard/chains/CheckForOnboardingChain.kt
+++ b/app/src/main/java/com/tangem/tap/domain/scanCard/chains/CheckForOnboardingChain.kt
@@ -1,6 +1,5 @@
package com.tangem.tap.domain.scanCard.chains
-import arrow.core.Either
import arrow.core.left
import arrow.core.right
import com.tangem.core.analytics.Analytics
@@ -9,6 +8,7 @@ import com.tangem.domain.card.ScanCardException
import com.tangem.domain.common.TapWorkarounds.canSkipBackup
import com.tangem.domain.common.util.twinsIsTwinned
import com.tangem.domain.core.chain.Chain
+import com.tangem.domain.core.chain.ResultChain
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.tap.common.extensions.addContext
import com.tangem.tap.common.extensions.dispatchOnMain
@@ -39,11 +39,9 @@ import org.rekotlin.Store
class CheckForOnboardingChain(
private val store: Store,
private val tapWalletManager: TapWalletManager,
-) : Chain {
+) : ResultChain() {
- override suspend fun invoke(
- previousChainResult: ScanResponse,
- ): Either {
+ override suspend fun launch(previousChainResult: ScanResponse): ScanChainResult {
tapWalletManager.updateConfigManager(previousChainResult)
store.dispatchOnMain(TwinCardsAction.IfTwinsPrepareState(previousChainResult))
diff --git a/app/src/main/java/com/tangem/tap/domain/scanCard/chains/DisclaimerChain.kt b/app/src/main/java/com/tangem/tap/domain/scanCard/chains/DisclaimerChain.kt
index ccf4f495bd..35ad2efbab 100644
--- a/app/src/main/java/com/tangem/tap/domain/scanCard/chains/DisclaimerChain.kt
+++ b/app/src/main/java/com/tangem/tap/domain/scanCard/chains/DisclaimerChain.kt
@@ -1,11 +1,11 @@
package com.tangem.tap.domain.scanCard.chains
-import arrow.core.Either
import arrow.core.left
import arrow.core.right
import com.tangem.core.navigation.AppScreen
import com.tangem.domain.card.ScanCardException
import com.tangem.domain.core.chain.Chain
+import com.tangem.domain.core.chain.ResultChain
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.redux.AppState
@@ -31,11 +31,9 @@ import kotlin.coroutines.resume
internal class DisclaimerChain(
private val store: Store,
private val disclaimerWillShow: () -> Unit = {},
-) : Chain {
+) : ResultChain() {
- override suspend fun invoke(
- previousChainResult: ScanResponse,
- ): Either {
+ override suspend fun launch(previousChainResult: ScanResponse): ScanChainResult {
val disclaimer = previousChainResult.card.createDisclaimer()
return if (disclaimer.isAccepted()) {
@@ -46,10 +44,7 @@ internal class DisclaimerChain(
}
}
- private suspend fun showDisclaimer(
- disclaimer: Disclaimer,
- response: ScanResponse,
- ): Either {
+ private suspend fun showDisclaimer(disclaimer: Disclaimer, response: ScanResponse): ScanChainResult {
store.dispatchOnMain(DisclaimerAction.SetDisclaimer(disclaimer))
return suspendCancellableCoroutine { continuation ->
diff --git a/app/src/main/java/com/tangem/tap/domain/scanCard/chains/FailedScansCounterChain.kt b/app/src/main/java/com/tangem/tap/domain/scanCard/chains/FailedScansCounterChain.kt
new file mode 100644
index 0000000000..9698824c24
--- /dev/null
+++ b/app/src/main/java/com/tangem/tap/domain/scanCard/chains/FailedScansCounterChain.kt
@@ -0,0 +1,29 @@
+package com.tangem.tap.domain.scanCard.chains
+
+import com.tangem.domain.card.ScanCardException
+import com.tangem.domain.core.chain.Chain
+import com.tangem.domain.models.scan.ScanResponse
+
+internal class FailedScansCounterChain(
+ private val onMaxUnsuccessfulScansReached: () -> Unit,
+ private val maxUnsuccessfulScans: Int = 3,
+) : Chain {
+
+ override suspend fun launch(previousChainResult: ScanChainResult): ScanChainResult {
+ if (previousChainResult.isLeft()) {
+ unsuccessfulScansCounter = unsuccessfulScansCounter.inc().coerceAtMost(maxUnsuccessfulScans)
+ if (unsuccessfulScansCounter == maxUnsuccessfulScans) {
+ onMaxUnsuccessfulScansReached()
+ }
+ } else {
+ unsuccessfulScansCounter = 0
+ }
+
+ return previousChainResult
+ }
+
+ private companion object {
+
+ var unsuccessfulScansCounter = 0
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/domain/scanCard/chains/ScanChainException.kt b/app/src/main/java/com/tangem/tap/domain/scanCard/chains/ScanChainException.kt
index 588dc809e2..ae5e9f8ad3 100644
--- a/app/src/main/java/com/tangem/tap/domain/scanCard/chains/ScanChainException.kt
+++ b/app/src/main/java/com/tangem/tap/domain/scanCard/chains/ScanChainException.kt
@@ -8,7 +8,11 @@ sealed class ScanChainException : ScanCardException.ChainException() {
/**
* May be returned from [DisclaimerChain]
* */
- object DisclaimerWasCanceled : ScanChainException()
+ data object DisclaimerWasCanceled : ScanChainException() {
+
+ @Suppress("UnusedPrivateMember")
+ private fun readResolve(): Any = DisclaimerWasCanceled
+ }
/**
* May be returned from [CheckForOnboardingChain]
diff --git a/app/src/main/java/com/tangem/tap/domain/scanCard/chains/ScanChainResult.kt b/app/src/main/java/com/tangem/tap/domain/scanCard/chains/ScanChainResult.kt
new file mode 100644
index 0000000000..e83f4a1578
--- /dev/null
+++ b/app/src/main/java/com/tangem/tap/domain/scanCard/chains/ScanChainResult.kt
@@ -0,0 +1,7 @@
+package com.tangem.tap.domain.scanCard.chains
+
+import arrow.core.Either
+import com.tangem.domain.card.ScanCardException
+import com.tangem.domain.models.scan.ScanResponse
+
+internal typealias ScanChainResult = Either
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/domain/scanCard/chains/ScanningFinishedChain.kt b/app/src/main/java/com/tangem/tap/domain/scanCard/chains/ScanningFinishedChain.kt
deleted file mode 100644
index a8c116cbe8..0000000000
--- a/app/src/main/java/com/tangem/tap/domain/scanCard/chains/ScanningFinishedChain.kt
+++ /dev/null
@@ -1,25 +0,0 @@
-package com.tangem.tap.domain.scanCard.chains
-
-import arrow.core.Either
-import arrow.core.right
-import com.tangem.domain.card.ScanCardException
-import com.tangem.domain.core.chain.Chain
-import com.tangem.domain.models.scan.ScanResponse
-
-/**
- * Responsible for invoking the callback at the end of the card scanning operation. Should be passed as last chain in
- * after card scanning chains. Always returns result of previous chain.
- *
- * @param onScanningFinished a suspending function to be called when the scanning process has finished.
- *
- * @see Chain for more information about the Chain interface.
- */
-internal class ScanningFinishedChain(
- private val onScanningFinished: suspend () -> Unit,
-) : Chain {
-
- override suspend fun invoke(previousChainResult: ScanResponse): Either {
- onScanningFinished()
- return previousChainResult.right()
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/domain/scanCard/repository/DefaultScanCardRepository.kt b/app/src/main/java/com/tangem/tap/domain/scanCard/repository/DefaultScanCardRepository.kt
index f74d706775..8aeacbb2c9 100644
--- a/app/src/main/java/com/tangem/tap/domain/scanCard/repository/DefaultScanCardRepository.kt
+++ b/app/src/main/java/com/tangem/tap/domain/scanCard/repository/DefaultScanCardRepository.kt
@@ -1,32 +1,27 @@
package com.tangem.tap.domain.scanCard.repository
-import arrow.core.Either
-import arrow.core.raise.either
import com.tangem.common.CompletionResult
-import com.tangem.domain.card.ScanCardException
import com.tangem.domain.card.repository.ScanCardRepository
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.tap.domain.TangemSdkManager
import com.tangem.tap.domain.scanCard.utils.ScanCardExceptionConverter
+// TODO: Move to the :data:card module
internal class DefaultScanCardRepository(
private val tangemSdkManager: TangemSdkManager,
) : ScanCardRepository {
private val exceptionConverter = ScanCardExceptionConverter()
- override suspend fun scanCard(
- cardId: String?,
- allowRequestAccessCodeFromStorage: Boolean,
- ): Either = either {
- when (
+ override suspend fun scanCard(cardId: String?, allowRequestAccessCodeFromStorage: Boolean): ScanResponse {
+ return when (
val result = tangemSdkManager.scanProduct(
cardId = cardId,
allowsRequestAccessCodeFromRepository = allowRequestAccessCodeFromStorage,
)
) {
is CompletionResult.Success -> result.data
- is CompletionResult.Failure -> raise(exceptionConverter.convert(result.error))
+ is CompletionResult.Failure -> throw exceptionConverter.convert(result.error)
}
}
}
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/domain/tasks/product/CreateProductWalletTask.kt b/app/src/main/java/com/tangem/tap/domain/tasks/product/CreateProductWalletTask.kt
index f5c1a21f38..699e7a527e 100644
--- a/app/src/main/java/com/tangem/tap/domain/tasks/product/CreateProductWalletTask.kt
+++ b/app/src/main/java/com/tangem/tap/domain/tasks/product/CreateProductWalletTask.kt
@@ -62,6 +62,7 @@ class CreateProductWalletTask(
private val cardTypesResolver: CardTypesResolver,
private val derivationStyleProvider: DerivationStyleProvider,
private val mnemonic: Mnemonic? = null,
+ private val passphrase: String? = null,
private val shouldReset: Boolean,
) : CardSessionRunnable {
@@ -82,7 +83,7 @@ class CreateProductWalletTask(
cardTypesResolver.isTangemTwins() ->
throw UnsupportedOperationException("Use the TwinCardsManager to create a wallet")
- else -> CreateWalletTangemWallet(mnemonic, shouldReset, derivationStyleProvider, cardDto)
+ else -> CreateWalletTangemWallet(mnemonic, passphrase, shouldReset, derivationStyleProvider, cardDto)
}
commandProcessor.proceed(cardDto, session) {
when (it) {
@@ -142,6 +143,7 @@ private class CreateWalletTangemNote(private val cardTypesResolver: CardTypesRes
*/
private class CreateWalletTangemWallet(
private val mnemonic: Mnemonic?,
+ private val passphrase: String?,
private val shouldReset: Boolean,
private val derivationStyleProvider: DerivationStyleProvider,
cardDTO: CardDTO,
@@ -170,7 +172,7 @@ private class CreateWalletTangemWallet(
session: CardSession,
callback: (result: CompletionResult) -> Unit,
) {
- CreateWalletsTask(cardConfig.mandatoryCurves, mnemonic).run(session) { result ->
+ CreateWalletsTask(cardConfig.mandatoryCurves, mnemonic, passphrase).run(session) { result ->
when (result) {
is CompletionResult.Success -> {
checkIfAllWalletsCreated(card, session, result.data, callback)
diff --git a/app/src/main/java/com/tangem/tap/domain/tasks/product/CreateWalletsTask.kt b/app/src/main/java/com/tangem/tap/domain/tasks/product/CreateWalletsTask.kt
index 2c75d8da36..c0ddcdeea3 100644
--- a/app/src/main/java/com/tangem/tap/domain/tasks/product/CreateWalletsTask.kt
+++ b/app/src/main/java/com/tangem/tap/domain/tasks/product/CreateWalletsTask.kt
@@ -21,6 +21,7 @@ class CreateWalletsResponse(
class CreateWalletsTask(
private val curves: List,
private val mnemonic: Mnemonic? = null,
+ private val passphrase: String?,
) : CardSessionRunnable {
private val createdWalletsResponses = mutableListOf()
@@ -41,7 +42,7 @@ class CreateWalletsTask(
callback: (result: CompletionResult) -> Unit,
) {
val extendedPrivateKey = mnemonic?.let {
- AnyMasterKeyFactory(mnemonic = it, passphrase = "").makeMasterKey(curve)
+ AnyMasterKeyFactory(mnemonic = it, passphrase = passphrase ?: "").makeMasterKey(curve)
}
CreateWalletTask(curve, extendedPrivateKey).run(session) { result ->
when (result) {
diff --git a/app/src/main/java/com/tangem/tap/domain/tasks/product/ResetToFactorySettingsTask.kt b/app/src/main/java/com/tangem/tap/domain/tasks/product/ResetToFactorySettingsTask.kt
index 96b0593c4e..09a32da708 100644
--- a/app/src/main/java/com/tangem/tap/domain/tasks/product/ResetToFactorySettingsTask.kt
+++ b/app/src/main/java/com/tangem/tap/domain/tasks/product/ResetToFactorySettingsTask.kt
@@ -33,7 +33,7 @@ class ResetToFactorySettingsTask(
}
private fun resetBackup(session: CardSession, callback: (result: CompletionResult) -> Unit) {
- if (session.environment.card?.backupStatus?.isActive != true) {
+ if (session.environment.card?.backupStatus == Card.BackupStatus.NoBackup) {
callback(CompletionResult.Success(session.environment.card!!))
return
}
diff --git a/app/src/main/java/com/tangem/tap/domain/userWalletList/model/UserWalletInformation.kt b/app/src/main/java/com/tangem/tap/domain/userWalletList/model/UserWalletInformation.kt
index fe064832d6..8a3f2fe559 100644
--- a/app/src/main/java/com/tangem/tap/domain/userWalletList/model/UserWalletInformation.kt
+++ b/app/src/main/java/com/tangem/tap/domain/userWalletList/model/UserWalletInformation.kt
@@ -18,4 +18,5 @@ internal data class UserWalletPublicInformation(
val cardsInWallet: Set,
val scanResponse: ScanResponse,
val isMultiCurrency: Boolean,
+ val hasBackupError: Boolean = false,
)
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/domain/userWalletList/utils/Mapper.kt b/app/src/main/java/com/tangem/tap/domain/userWalletList/utils/Mapper.kt
index 4116cde096..451e717e9e 100644
--- a/app/src/main/java/com/tangem/tap/domain/userWalletList/utils/Mapper.kt
+++ b/app/src/main/java/com/tangem/tap/domain/userWalletList/utils/Mapper.kt
@@ -20,6 +20,7 @@ internal val UserWallet.publicInformation: UserWalletPublicInformation
wallets = emptyList(),
),
),
+ hasBackupError = hasBackupError,
)
internal fun UserWalletPublicInformation.toUserWallet(): UserWallet {
@@ -30,6 +31,7 @@ internal fun UserWalletPublicInformation.toUserWallet(): UserWallet {
cardsInWallet = cardsInWallet,
scanResponse = scanResponse,
isMultiCurrency = isMultiCurrency,
+ hasBackupError = hasBackupError,
)
}
diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect/BnbHelper.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect/BnbHelper.kt
index 9e08d07d76..16a35bf539 100644
--- a/app/src/main/java/com/tangem/tap/domain/walletconnect/BnbHelper.kt
+++ b/app/src/main/java/com/tangem/tap/domain/walletconnect/BnbHelper.kt
@@ -14,7 +14,8 @@ import com.trustwallet.walletconnect.models.binance.WCBinanceTradeOrder
import com.trustwallet.walletconnect.models.binance.WCBinanceTransferOrder
import timber.log.Timber
-object BnbHelper {
+internal object BnbHelper {
+
fun createMessageData(order: WCBinanceTransferOrder): BinanceMessageData.Transfer {
val input = order.msgs.first().inputs.first()
val output = order.msgs.first().inputs.first()
diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect/WalletConnectManager.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect/WalletConnectManager.kt
index b99b599dfd..9078975c2c 100644
--- a/app/src/main/java/com/tangem/tap/domain/walletconnect/WalletConnectManager.kt
+++ b/app/src/main/java/com/tangem/tap/domain/walletconnect/WalletConnectManager.kt
@@ -41,7 +41,7 @@ import java.util.concurrent.TimeUnit
import kotlin.collections.set
@Suppress("LargeClass")
-class WalletConnectManager {
+internal class WalletConnectManager {
private var cardId: String? = null
@@ -348,6 +348,7 @@ class WalletConnectManager {
val hash = WalletConnectSdkHelper().signPersonalMessage(
hashToSign = data.hash,
networkId = activeData.wallet.blockchain?.toNetworkId() ?: "",
+ type = data.type,
derivationPath = activeData.wallet.derivationPath?.rawPath,
cardId = cardId,
)
@@ -522,9 +523,9 @@ class WalletConnectManager {
}
}
-typealias Topic = String
+internal typealias Topic = String
-data class WalletConnectActiveData(
+internal data class WalletConnectActiveData(
val peerId: String,
val remotePeerId: String?,
val client: WCClient,
@@ -547,7 +548,7 @@ data class WalletConnectActiveData(
}
@Suppress("MagicNumber")
-class RetryInterceptor : Interceptor {
+internal class RetryInterceptor : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val request: Request = chain.request()
val response = chain.proceed(request)
diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect/WalletConnectSdkHelper.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect/WalletConnectSdkHelper.kt
index a1c5cb5c0b..12dc242d80 100644
--- a/app/src/main/java/com/tangem/tap/domain/walletconnect/WalletConnectSdkHelper.kt
+++ b/app/src/main/java/com/tangem/tap/domain/walletconnect/WalletConnectSdkHelper.kt
@@ -7,10 +7,7 @@ import com.tangem.blockchain.blockchains.ethereum.EthereumUtils
import com.tangem.blockchain.blockchains.ethereum.EthereumUtils.toKeccak
import com.tangem.blockchain.common.*
import com.tangem.blockchain.common.transaction.Fee
-import com.tangem.blockchain.extensions.Result
-import com.tangem.blockchain.extensions.SimpleResult
-import com.tangem.blockchain.extensions.hexToBigDecimal
-import com.tangem.blockchain.extensions.isAscii
+import com.tangem.blockchain.extensions.*
import com.tangem.common.CompletionResult
import com.tangem.common.extensions.hexToBytes
import com.tangem.common.extensions.toDecompressedPublicKey
@@ -25,8 +22,9 @@ import com.tangem.tap.common.extensions.safeUpdate
import com.tangem.tap.common.extensions.toFormattedString
import com.tangem.tap.domain.walletconnect.BnbHelper.toWCBinanceTradeOrder
import com.tangem.tap.domain.walletconnect.BnbHelper.toWCBinanceTransferOrder
-import com.tangem.tap.domain.walletconnect2.domain.WcEthereumSignMessage
+import com.tangem.tap.domain.walletconnect2.domain.TransactionType
import com.tangem.tap.domain.walletconnect2.domain.WcEthereumTransaction
+import com.tangem.tap.domain.walletconnect2.domain.WcSignMessage
import com.tangem.tap.domain.walletconnect2.domain.models.EthTransactionData
import com.tangem.tap.domain.walletconnect2.domain.models.binance.WcBinanceTradeOrder
import com.tangem.tap.domain.walletconnect2.domain.models.binance.WcBinanceTransferOrder
@@ -45,6 +43,7 @@ import timber.log.Timber
import java.math.BigDecimal
import com.tangem.core.analytics.models.AnalyticsParam as CoreAnalyticsParam
+@Suppress("LargeClass")
class WalletConnectSdkHelper {
@Suppress("MagicNumber")
@@ -206,8 +205,7 @@ class WalletConnectSdkHelper {
walletPublicKey = data.walletManager.wallet.publicKey.seedKey,
derivationPath = data.walletManager.wallet.publicKey.derivationPath,
)
- val result = tangemSdkManager.runTaskAsync(command, initialMessage = Message(), cardId = cardId)
- return when (result) {
+ return when (val result = tangemSdkManager.runTaskAsync(command, initialMessage = Message(), cardId = cardId)) {
is CompletionResult.Success -> {
val hash = EthereumUtils.prepareTransactionToSend(
signature = result.data.signature,
@@ -250,8 +248,7 @@ class WalletConnectSdkHelper {
walletPublicKey = wallet.publicKey.seedKey,
derivationPath = wallet.publicKey.derivationPath,
)
- val result = tangemSdkManager.runTaskAsync(command, initialMessage = Message(), cardId = cardId)
- return when (result) {
+ return when (val result = tangemSdkManager.runTaskAsync(command, initialMessage = Message(), cardId = cardId)) {
is CompletionResult.Success -> {
val key = wallet.publicKey.blockchainKey.toDecompressedPublicKey()
getBnbResultString(
@@ -267,16 +264,17 @@ class WalletConnectSdkHelper {
}
fun prepareDataForPersonalSign(
- message: WcEthereumSignMessage,
+ message: WcSignMessage,
topic: String,
metaName: String,
id: Long,
): WcPersonalSignData {
val messageData = when (message.type) {
- WcEthereumSignMessage.WCSignType.MESSAGE,
- WcEthereumSignMessage.WCSignType.PERSONAL_MESSAGE,
+ WcSignMessage.WCSignType.MESSAGE,
+ WcSignMessage.WCSignType.PERSONAL_MESSAGE,
-> createMessageData(message)
- WcEthereumSignMessage.WCSignType.TYPED_MESSAGE -> EthereumUtils.makeTypedDataHash(message.data)
+ WcSignMessage.WCSignType.TYPED_MESSAGE -> EthereumUtils.makeTypedDataHash(message.data)
+ WcSignMessage.WCSignType.SOLANA_MESSAGE -> message.data.decodeBase58()
}
val messageString = message.data.hexToAscii()
@@ -290,7 +288,7 @@ class WalletConnectSdkHelper {
id = id,
)
return WcPersonalSignData(
- hash = messageData,
+ hash = requireNotNull(messageData) { "Message data must not be null" },
topic = topic,
id = id,
dialogData = dialogData,
@@ -298,7 +296,7 @@ class WalletConnectSdkHelper {
)
}
- private fun createMessageData(message: WcEthereumSignMessage): ByteArray {
+ private fun createMessageData(message: WcSignMessage): ByteArray {
val messageData = try {
message.data.removePrefix(HEX_PREFIX).hexToBytes()
} catch (exception: Exception) {
@@ -332,6 +330,7 @@ class WalletConnectSdkHelper {
suspend fun signPersonalMessage(
hashToSign: ByteArray,
networkId: String,
+ type: WcSignMessage.WCSignType,
derivationPath: String?,
cardId: String?,
): String? {
@@ -345,12 +344,16 @@ class WalletConnectSdkHelper {
)
return when (val result = tangemSdkManager.runTaskAsync(command, cardId)) {
is CompletionResult.Success -> {
- val hash = result.data.signature
- return EthereumUtils.prepareSignedMessageData(
- signedHash = hash,
- hashToSign = hashToSign,
- publicKey = wallet.publicKey.blockchainKey.toDecompressedPublicKey(),
- )
+ val signedHash = result.data.signature
+
+ return when (type) {
+ WcSignMessage.WCSignType.SOLANA_MESSAGE -> getSolanaResultString(signedHash)
+ else -> EthereumUtils.prepareSignedMessageData(
+ signedHash = signedHash,
+ hashToSign = hashToSign,
+ publicKey = wallet.publicKey.blockchainKey.toDecompressedPublicKey(),
+ )
+ }
}
is CompletionResult.Failure -> {
Timber.e(result.error.customMessage)
@@ -359,12 +362,46 @@ class WalletConnectSdkHelper {
}
}
- companion object {
- private const val ETH_MESSAGE_PREFIX = "\u0019Ethereum Signed Message:\n"
- private const val HEX_PREFIX = "0x"
- private const val DEFAULT_MAX_GASLIMIT = 350000
- fun getBnbResultString(publicKey: String, signature: String): String {
- return "{\"signature\":\"$signature\",\"publicKey\":\"$publicKey\"}"
+ suspend fun signTransaction(
+ hashToSign: ByteArray,
+ networkId: String,
+ type: TransactionType,
+ derivationPath: String?,
+ cardId: String?,
+ ): String? {
+ val blockchain = Blockchain.fromNetworkId(networkId) ?: return null
+ val wallet = getWalletManager(blockchain, derivationPath)?.wallet ?: return null
+
+ val command = SignHashCommand(
+ hash = hashToSign,
+ walletPublicKey = wallet.publicKey.seedKey,
+ derivationPath = wallet.publicKey.derivationPath,
+ )
+ return when (val result = tangemSdkManager.runTaskAsync(command, cardId)) {
+ is CompletionResult.Success -> {
+ val signedHash = result.data.signature
+
+ when (type) {
+ TransactionType.SOLANA_TX -> {
+ getSolanaResultString(signedHash)
+ }
+ }
+ }
+ is CompletionResult.Failure -> {
+ Timber.e(result.error.customMessage)
+ null
+ }
}
}
+
+ private fun getSolanaResultString(signedHash: ByteArray) = "{ signature: \"${signedHash.encodeBase58()}\" }"
+
+ private fun getBnbResultString(publicKey: String, signature: String) =
+ "{\"signature\":\"$signature\",\"publicKey\":\"$publicKey\"}"
+
+ private companion object {
+ const val ETH_MESSAGE_PREFIX = "\u0019Ethereum Signed Message:\n"
+ const val HEX_PREFIX = "0x"
+ const val DEFAULT_MAX_GASLIMIT = 350000
+ }
}
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect/extensions/WcPeerMeta.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect/extensions/WcPeerMeta.kt
index 3197f2729c..d227a42923 100644
--- a/app/src/main/java/com/tangem/tap/domain/walletconnect/extensions/WcPeerMeta.kt
+++ b/app/src/main/java/com/tangem/tap/domain/walletconnect/extensions/WcPeerMeta.kt
@@ -1,18 +1,18 @@
package com.tangem.tap.domain.walletconnect.extensions
-import com.tangem.tap.domain.walletconnect2.domain.WcEthereumSignMessage
import com.tangem.tap.domain.walletconnect2.domain.WcEthereumTransaction
+import com.tangem.tap.domain.walletconnect2.domain.WcSignMessage
import com.trustwallet.walletconnect.models.WCPeerMeta
import com.trustwallet.walletconnect.models.ethereum.WCEthereumSignMessage
import com.trustwallet.walletconnect.models.ethereum.WCEthereumTransaction
-fun WCPeerMeta.isDappSupported(): Boolean {
+internal fun WCPeerMeta.isDappSupported(): Boolean {
return !unsupportedDappsList.any { this.url.contains(it) }
}
private val unsupportedDappsList: List = listOf("dydx.exchange")
-fun WCEthereumTransaction.toWcEthTransaction(): WcEthereumTransaction {
+internal fun WCEthereumTransaction.toWcEthTransaction(): WcEthereumTransaction {
return WcEthereumTransaction(
from = from,
to = to,
@@ -27,13 +27,13 @@ fun WCEthereumTransaction.toWcEthTransaction(): WcEthereumTransaction {
)
}
-fun WCEthereumSignMessage.toWcEthereumSignMessage(): WcEthereumSignMessage {
- return WcEthereumSignMessage(
+internal fun WCEthereumSignMessage.toWcEthereumSignMessage(): WcSignMessage {
+ return WcSignMessage(
raw = raw,
type = when (type) {
- WCEthereumSignMessage.WCSignType.MESSAGE -> WcEthereumSignMessage.WCSignType.MESSAGE
- WCEthereumSignMessage.WCSignType.PERSONAL_MESSAGE -> WcEthereumSignMessage.WCSignType.PERSONAL_MESSAGE
- WCEthereumSignMessage.WCSignType.TYPED_MESSAGE -> WcEthereumSignMessage.WCSignType.TYPED_MESSAGE
+ WCEthereumSignMessage.WCSignType.MESSAGE -> WcSignMessage.WCSignType.MESSAGE
+ WCEthereumSignMessage.WCSignType.PERSONAL_MESSAGE -> WcSignMessage.WCSignType.PERSONAL_MESSAGE
+ WCEthereumSignMessage.WCSignType.TYPED_MESSAGE -> WcSignMessage.WCSignType.TYPED_MESSAGE
},
)
}
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect2/app/TangemWcBlockchainHelper.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect2/app/TangemWcBlockchainHelper.kt
index 3b69331ae2..63e68ba973 100644
--- a/app/src/main/java/com/tangem/tap/domain/walletconnect2/app/TangemWcBlockchainHelper.kt
+++ b/app/src/main/java/com/tangem/tap/domain/walletconnect2/app/TangemWcBlockchainHelper.kt
@@ -4,8 +4,18 @@ import com.tangem.blockchain.common.Blockchain
import com.tangem.domain.common.extensions.fromNetworkId
import com.tangem.domain.common.extensions.toNetworkId
import com.tangem.tap.domain.walletconnect2.domain.WcBlockchainHelper
+import com.tangem.tap.domain.walletconnect2.toggles.WalletConnectFeatureToggles
+
+internal class TangemWcBlockchainHelper(
+ private val featureToggles: WalletConnectFeatureToggles,
+) : WcBlockchainHelper {
+
+ private val supportedNonEvmBlockchains = if (featureToggles.isSolanaTxSignEnabled) {
+ setOf(Blockchain.Solana, Blockchain.SolanaTestnet)
+ } else {
+ emptySet()
+ }
-class TangemWcBlockchainHelper : WcBlockchainHelper {
override fun chainIdToNetworkIdOrNull(chainId: String): String? {
val parsedId = chainId.parseId() ?: return null
val blockchain = parsedId.chainIdToBlockchain()
@@ -41,48 +51,57 @@ class TangemWcBlockchainHelper : WcBlockchainHelper {
val networkId = chainIdToNetworkIdOrNull(chainId) ?: return null
return Blockchain.fromNetworkId(networkId)?.fullName
}
-}
-private fun Blockchain.getCaip2ChainId(): String? {
- if (this.isEvm()) return this.getChainId()?.toString()
+ private fun Blockchain.getCaip2ChainId(): String? {
+ if (this.isEvm()) return this.getChainId()?.toString()
- return when (this) {
- Blockchain.Solana -> "4sGjMW1sUnHzSxGspuhpqLDx6wiyjNtZ"
- Blockchain.Polkadot -> "91b171bb158e2d3848fa23a9f1c25182"
- Blockchain.Tron -> "0x2b6653dc"
- else -> null
- }
-}
-
-private fun Blockchain.getCaip2Namespace(): String? {
- return when {
- this.isEvm() -> EVM_NAMESPACE
- supportedNonEvmBlockchains.contains(this) -> this.toNetworkId()
- else -> null
- }
-}
-
-private fun String.parseId(): Pair? {
- val parsed = this.split(CHAIN_SEPARATOR)
- if (parsed.size != 2) return null
-
- return parsed[0] to parsed[1]
-}
-
-private fun Pair.chainIdToBlockchain(): Blockchain? {
- return when (first) {
- EVM_NAMESPACE -> {
- second.toIntOrNull()
- ?.let(Blockchain::fromChainId)
- }
- else -> {
- Blockchain.fromNetworkId(networkId = first)
- .takeIf(supportedNonEvmBlockchains::contains)
+ return when (this) {
+ /*
+ * The WC sample application and documentation use a commented out chain ID. However, in real dApps
+ * uncommented is used.
+ * Docs: https://docs.walletconnect.com/advanced/multichain/chain-list
+ *
+ * Blockchain.Solana -> "5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp"
+ * */
+ Blockchain.Solana -> "4sGjMW1sUnHzSxGspuhpqLDx6wiyjNtZ"
+ Blockchain.SolanaTestnet -> "z4uhcVJyU9pJkvQyS88uRDiswHXSCkY3z"
+ Blockchain.Polkadot -> "91b171bb158e2d3848fa23a9f1c25182"
+ Blockchain.Tron -> "0x2b6653dc"
+ else -> null
}
}
-}
-private val supportedNonEvmBlockchains = emptySet() // TODO: add supported networks
+ private fun Blockchain.getCaip2Namespace(): String? {
+ return when {
+ this.isEvm() -> EVM_NAMESPACE
+ supportedNonEvmBlockchains.contains(this) -> this.toNetworkId().substringBefore(TESTNET_SEPARATOR)
+ else -> null
+ }
+ }
-private const val EVM_NAMESPACE = "eip155"
-private const val CHAIN_SEPARATOR = ":"
\ No newline at end of file
+ private fun String.parseId(): Pair? {
+ val parsed = this.split(CHAIN_SEPARATOR)
+ if (parsed.size != 2) return null
+
+ return parsed[0] to parsed[1]
+ }
+
+ private fun Pair.chainIdToBlockchain(): Blockchain? {
+ return when (first) {
+ EVM_NAMESPACE -> {
+ second.toIntOrNull()
+ ?.let(Blockchain::fromChainId)
+ }
+ else -> {
+ Blockchain.fromNetworkId(networkId = first)
+ .takeIf(supportedNonEvmBlockchains::contains)
+ }
+ }
+ }
+
+ private companion object {
+ const val EVM_NAMESPACE = "eip155"
+ const val CHAIN_SEPARATOR = ":"
+ const val TESTNET_SEPARATOR = "/"
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect2/app/WalletConnectEventsHandlerImpl.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect2/app/WalletConnectEventsHandlerImpl.kt
index d6767ddc8e..909f4dafb7 100644
--- a/app/src/main/java/com/tangem/tap/domain/walletconnect2/app/WalletConnectEventsHandlerImpl.kt
+++ b/app/src/main/java/com/tangem/tap/domain/walletconnect2/app/WalletConnectEventsHandlerImpl.kt
@@ -12,7 +12,7 @@ import com.tangem.tap.features.details.ui.walletconnect.WcSessionForScreen
import com.tangem.tap.store
import timber.log.Timber
-class WalletConnectEventsHandlerImpl : WalletConnectEventsHandler {
+internal class WalletConnectEventsHandlerImpl : WalletConnectEventsHandler {
override fun onProposalReceived(proposal: WalletConnectEvents.SessionProposal, networksFormatted: String) {
store.dispatchOnMain(
GlobalAction.ShowDialog(
diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect2/data/WalletConnectRepositoryImpl.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect2/data/DefaultWalletConnectRepository.kt
similarity index 94%
rename from app/src/main/java/com/tangem/tap/domain/walletconnect2/data/WalletConnectRepositoryImpl.kt
rename to app/src/main/java/com/tangem/tap/domain/walletconnect2/data/DefaultWalletConnectRepository.kt
index 71f910ce82..a4c78ad4b9 100644
--- a/app/src/main/java/com/tangem/tap/domain/walletconnect2/data/WalletConnectRepositoryImpl.kt
+++ b/app/src/main/java/com/tangem/tap/domain/walletconnect2/data/DefaultWalletConnectRepository.kt
@@ -18,9 +18,8 @@ import kotlinx.coroutines.*
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import timber.log.Timber
-import javax.inject.Inject
-class WalletConnectRepositoryImpl @Inject constructor(
+internal class DefaultWalletConnectRepository(
private val application: Application,
private val wcRequestDeserializer: WcJrpcRequestsDeserializer,
private val analyticsHandler: AnalyticsEventHandler,
@@ -95,11 +94,23 @@ class WalletConnectRepositoryImpl @Inject constructor(
) {
// Triggered when wallet receives the session proposal sent by a Dapp
Timber.d("sessionProposal: $sessionProposal")
- this@WalletConnectRepositoryImpl.sessionProposal = sessionProposal
+ this@DefaultWalletConnectRepository.sessionProposal = sessionProposal
+
+ if (sessionProposal.name in unsupportedDApps) {
+ Timber.w("Unsupported DApp")
+ scope.launch {
+ _events.emit(
+ WalletConnectEvents.SessionApprovalError(
+ WalletConnectError.UnsupportedDApp,
+ ),
+ )
+ }
+ return
+ }
val missingNetworks = findMissingNetworks(
namespaces = sessionProposal.requiredNamespaces,
- userNamespaces = this@WalletConnectRepositoryImpl.userNamespaces ?: emptyMap(),
+ userNamespaces = this@DefaultWalletConnectRepository.userNamespaces ?: emptyMap(),
)
if (missingNetworks.isNotEmpty()) {
@@ -116,7 +127,7 @@ class WalletConnectRepositoryImpl @Inject constructor(
val optionalWithoutMissingNetworks = removeMissingNetworks(
namespaces = sessionProposal.optionalNamespaces,
- userNamespaces = this@WalletConnectRepositoryImpl.userNamespaces ?: emptyMap(),
+ userNamespaces = this@DefaultWalletConnectRepository.userNamespaces ?: emptyMap(),
)
scope.launch {
@@ -334,6 +345,7 @@ class WalletConnectRepositoryImpl @Inject constructor(
),
)
}
+
Web3Wallet.respondSessionRequest(
params = Wallet.Params.SessionRequestResponse(
sessionTopic = requestData.topic,
@@ -342,15 +354,19 @@ class WalletConnectRepositoryImpl @Inject constructor(
result = result,
),
),
- onSuccess = {},
- onError = {
+ onSuccess = { response ->
+ Timber.d("Session request responded successfully: $response")
+ },
+ onError = { error ->
+ Timber.d(error.throwable, "Error while responging session request")
+
WalletConnect.RequestHandledParams(
dAppName = session?.name ?: "",
dAppUrl = session?.url ?: "",
methodName = requestData.method,
blockchain = requestData.blockchain,
errorCode = WalletConnectError.ValidationError.error,
- errorDescription = it.throwable.message,
+ errorDescription = error.throwable.message,
)
},
)
@@ -472,4 +488,9 @@ class WalletConnectRepositoryImpl @Inject constructor(
val userChains = userNamespaces.flatMap { it.value.map { account -> account.chainId } }
return wcProvidedChains.intersect(userChains.toSet())
}
+
+ private companion object {
+
+ val unsupportedDApps = listOf("dYdX", "dYdX v4", "Apex Pro")
+ }
}
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect2/data/WalletConnectSessionsRepositoryImpl.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect2/data/DefaultWalletConnectSessionsRepository.kt
similarity index 92%
rename from app/src/main/java/com/tangem/tap/domain/walletconnect2/data/WalletConnectSessionsRepositoryImpl.kt
rename to app/src/main/java/com/tangem/tap/domain/walletconnect2/data/DefaultWalletConnectSessionsRepository.kt
index 99f4351a93..77b531934a 100644
--- a/app/src/main/java/com/tangem/tap/domain/walletconnect2/data/WalletConnectSessionsRepositoryImpl.kt
+++ b/app/src/main/java/com/tangem/tap/domain/walletconnect2/data/DefaultWalletConnectSessionsRepository.kt
@@ -3,15 +3,13 @@ package com.tangem.tap.domain.walletconnect2.data
import com.squareup.moshi.JsonAdapter
import com.squareup.moshi.Moshi
import com.squareup.moshi.Types
-import com.tangem.datasource.di.SdkMoshi
import com.tangem.datasource.files.FileReader
import com.tangem.tap.domain.walletconnect2.domain.WalletConnectSessionsRepository
import com.tangem.tap.domain.walletconnect2.domain.models.Session
import timber.log.Timber
-import javax.inject.Inject
-class WalletConnectSessionsRepositoryImpl @Inject constructor(
- @SdkMoshi private val moshi: Moshi,
+internal class DefaultWalletConnectSessionsRepository(
+ private val moshi: Moshi,
private val fileReader: FileReader,
) :
WalletConnectSessionsRepository {
diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect2/di/WalletConnectInteractorModule.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect2/di/WalletConnectInteractorModule.kt
index 8cf7905260..927eab61da 100644
--- a/app/src/main/java/com/tangem/tap/domain/walletconnect2/di/WalletConnectInteractorModule.kt
+++ b/app/src/main/java/com/tangem/tap/domain/walletconnect2/di/WalletConnectInteractorModule.kt
@@ -1,15 +1,22 @@
package com.tangem.tap.domain.walletconnect2.di
+import android.app.Application
+import com.squareup.moshi.Moshi
+import com.tangem.core.analytics.api.AnalyticsEventHandler
+import com.tangem.core.featuretoggle.manager.FeatureTogglesManager
+import com.tangem.datasource.di.SdkMoshi
+import com.tangem.datasource.files.FileReader
import com.tangem.tap.domain.walletconnect.WalletConnectSdkHelper
import com.tangem.tap.domain.walletconnect2.app.TangemWcBlockchainHelper
import com.tangem.tap.domain.walletconnect2.app.WalletConnectEventsHandlerImpl
-import com.tangem.tap.domain.walletconnect2.data.WalletConnectRepositoryImpl
-import com.tangem.tap.domain.walletconnect2.data.WalletConnectSessionsRepositoryImpl
+import com.tangem.tap.domain.walletconnect2.data.DefaultWalletConnectRepository
+import com.tangem.tap.domain.walletconnect2.data.DefaultWalletConnectSessionsRepository
import com.tangem.tap.domain.walletconnect2.domain.WalletConnectInteractor
import com.tangem.tap.domain.walletconnect2.domain.WalletConnectRepository
import com.tangem.tap.domain.walletconnect2.domain.WalletConnectSessionsRepository
+import com.tangem.tap.domain.walletconnect2.domain.WcJrpcRequestsDeserializer
+import com.tangem.tap.domain.walletconnect2.toggles.WalletConnectFeatureToggles
import com.tangem.utils.coroutines.AppCoroutineDispatcherProvider
-import dagger.Binds
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
@@ -20,19 +27,21 @@ import javax.inject.Singleton
@Module
@InstallIn(ActivityComponent::class)
-class WalletConnectInteractorModule {
+internal object WalletConnectInteractorModule {
+
@Provides
@ActivityScoped
fun provideWalletConnectInteractor(
wcRepository: WalletConnectRepository,
wcSessionsRepository: WalletConnectSessionsRepository,
+ walletConnectFeatureToggles: WalletConnectFeatureToggles,
): WalletConnectInteractor {
return WalletConnectInteractor(
handler = WalletConnectEventsHandlerImpl(),
walletConnectRepository = wcRepository,
sessionsRepository = wcSessionsRepository,
sdkHelper = WalletConnectSdkHelper(),
- blockchainHelper = TangemWcBlockchainHelper(),
+ blockchainHelper = TangemWcBlockchainHelper(walletConnectFeatureToggles),
dispatcher = AppCoroutineDispatcherProvider(),
)
}
@@ -40,15 +49,37 @@ class WalletConnectInteractorModule {
@Module
@InstallIn(SingletonComponent::class)
-interface WalletConnectModule {
+internal object WalletConnectModule {
- @Binds
+ @Provides
@Singleton
- fun bindWalletConnectRepository(repository: WalletConnectRepositoryImpl): WalletConnectRepository
+ fun provideWalletConnectFeatureToggles(featureTogglesManager: FeatureTogglesManager): WalletConnectFeatureToggles {
+ return WalletConnectFeatureToggles(featureTogglesManager)
+ }
- @Binds
+ @Provides
@Singleton
- fun bindWalletConnectSessionsRepository(
- repository: WalletConnectSessionsRepositoryImpl,
- ): WalletConnectSessionsRepository
+ fun provideWalletConnectRepository(
+ application: Application,
+ wcRequestDeserializer: WcJrpcRequestsDeserializer,
+ analyticsHandler: AnalyticsEventHandler,
+ ): WalletConnectRepository {
+ return DefaultWalletConnectRepository(
+ application = application,
+ wcRequestDeserializer = wcRequestDeserializer,
+ analyticsHandler = analyticsHandler,
+ )
+ }
+
+ @Provides
+ @Singleton
+ fun provideWalletConnectSessionsRepository(
+ @SdkMoshi moshi: Moshi,
+ fileReader: FileReader,
+ ): WalletConnectSessionsRepository {
+ return DefaultWalletConnectSessionsRepository(
+ moshi = moshi,
+ fileReader = fileReader,
+ )
+ }
}
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WalletConnectInteractor.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WalletConnectInteractor.kt
index 884d3ac5d7..3bf99ffd83 100644
--- a/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WalletConnectInteractor.kt
+++ b/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WalletConnectInteractor.kt
@@ -209,7 +209,7 @@ class WalletConnectInteractor(
val currentRequest = this.currentRequest
if (currentRequest == null || request.topic != currentRequest.topic) return
- val networkId = blockchainHelper.chainIdToNetworkIdOrNull(currentRequest.chainId ?: "") ?: return
+ val networkId = blockchainHelper.chainIdToNetworkIdOrNull(currentRequest.chainId.orEmpty()) ?: return
val signedHash = when (request) {
is WcPreparedRequest.BnbTransaction -> sdkHelper.signBnbTransaction(
data = request.preparedRequestData.data.data,
@@ -224,13 +224,19 @@ class WalletConnectInteractor(
is WcPreparedRequest.EthSign -> sdkHelper.signPersonalMessage(
hashToSign = request.preparedRequestData.hash,
networkId = networkId,
+ type = request.preparedRequestData.type,
+ derivationPath = request.derivationPath,
+ cardId = cardId,
+ )
+ is WcPreparedRequest.SignTransaction -> sdkHelper.signTransaction(
+ hashToSign = request.preparedRequestData.hashToSign,
+ networkId = networkId,
+ type = request.preparedRequestData.type,
derivationPath = request.derivationPath,
cardId = cardId,
)
}
- Timber.d("Signed hash: $signedHash")
-
val requestData = RequestData(
topic = request.topic,
requestId = request.requestId,
diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WcJrpcMethods.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WcJrpcMethods.kt
index 9a0da6c971..3cb1187509 100644
--- a/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WcJrpcMethods.kt
+++ b/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WcJrpcMethods.kt
@@ -2,15 +2,17 @@ package com.tangem.tap.domain.walletconnect2.domain
import com.squareup.moshi.*
import com.tangem.datasource.di.SdkMoshi
-import com.tangem.tap.domain.walletconnect2.domain.models.binance.WCBinanceTxConfirmParam
import com.tangem.tap.domain.walletconnect2.domain.models.binance.WcBinanceCancelOrder
import com.tangem.tap.domain.walletconnect2.domain.models.binance.WcBinanceTradeOrder
import com.tangem.tap.domain.walletconnect2.domain.models.binance.WcBinanceTransferOrder
+import com.tangem.tap.domain.walletconnect2.domain.models.binance.WcBinanceTxConfirmParam
+import com.tangem.tap.domain.walletconnect2.domain.models.solana.SolanaSignMessage
+import com.tangem.tap.domain.walletconnect2.domain.models.solana.SolanaTransactionRequest
import timber.log.Timber
import javax.inject.Inject
import javax.inject.Singleton
-enum class WcJrpcMethods(val code: String) {
+internal enum class WcJrpcMethods(val code: String) {
ETH_SIGN("eth_sign"),
ETH_PERSONAL_SIGN("personal_sign"),
@@ -18,10 +20,14 @@ enum class WcJrpcMethods(val code: String) {
ETH_SIGN_TYPE_DATA_V4("eth_signTypedData_v4"),
ETH_SIGN_TRANSACTION("eth_signTransaction"),
ETH_SEND_TRANSACTION("eth_sendTransaction"),
+
BNB_SIGN("bnb_sign"),
BNB_TRANSACTION_CONFIRM("bnb_tx_confirmation"),
SIGN_TRANSACTION("trust_signTransaction"),
WALLET_ADD_ETHEREUM_CHAIN("wallet_addEthereumChain"),
+
+ SOLANA_SIGN_TX("solana_signTransaction"),
+ SOLANA_SIGN_MESSAGE("solana_signMessage"),
;
companion object {
@@ -30,7 +36,7 @@ enum class WcJrpcMethods(val code: String) {
}
@JsonClass(generateAdapter = true)
-data class WCSignTransaction(
+data class WcSignTransaction(
@Json(name = "network")
val network: Int,
@@ -39,13 +45,7 @@ data class WCSignTransaction(
) : WcRequestData
@JsonClass(generateAdapter = true)
-data class WcAddChain(
- @Json(name = "chainId")
- val chainId: String,
-) : WcRequestData
-
-@JsonClass(generateAdapter = true)
-data class WcEthereumSignMessage(
+data class WcSignMessage(
@Json(name = "raw")
val raw: List,
@@ -53,7 +53,7 @@ data class WcEthereumSignMessage(
val type: WCSignType,
) : WcRequestData {
enum class WCSignType {
- MESSAGE, PERSONAL_MESSAGE, TYPED_MESSAGE
+ MESSAGE, PERSONAL_MESSAGE, TYPED_MESSAGE, SOLANA_MESSAGE,
}
/**
@@ -63,6 +63,7 @@ data class WcEthereumSignMessage(
* - MESSAGE: `[address, data ]`
* - TYPED_MESSAGE: `[address, data]`
* - PERSONAL_MESSAGE: `[data, address]`
+ * - SOLANA_MESSAGE: `[publicKey (address), message]`
*
* reference: https://docs.walletconnect.org/json-rpc/ethereum#eth_signtypeddata
*/
@@ -79,6 +80,21 @@ data class WcEthereumSignMessage(
}
}
+@JsonClass(generateAdapter = true)
+data class WcSignMessageData(
+ @Json(name = "address")
+ val address: String,
+
+ @Json(name = "message")
+ val message: String,
+) : WcRequestData
+
+@JsonClass(generateAdapter = true)
+data class WcAddChain(
+ @Json(name = "chainId")
+ val chainId: String,
+) : WcRequestData
+
@JsonClass(generateAdapter = true)
data class WcEthereumTransaction(
@Json(name = "from")
@@ -117,20 +133,21 @@ interface WcRequestData
data class WcCustomRequestData(val data: String) : WcRequestData
sealed class WcRequest(open val data: WcRequestData) {
- data class EthSign(override val data: WcEthereumSignMessage) : WcRequest(data)
+ data class EthSign(override val data: WcSignMessage) : WcRequest(data)
data class EthSignTransaction(override val data: WcEthereumTransaction) : WcRequest(data)
data class EthSendTransaction(override val data: WcEthereumTransaction) : WcRequest(data)
data class BnbTrade(override val data: WcBinanceTradeOrder) : WcRequest(data)
data class BnbCancel(override val data: WcBinanceCancelOrder) : WcRequest(data)
data class BnbTransfer(override val data: WcBinanceTransferOrder) : WcRequest(data)
- data class BnbTxConfirm(override val data: WCBinanceTxConfirmParam) : WcRequest(data)
- data class SignTransaction(override val data: WCSignTransaction) : WcRequest(data)
+ data class BnbTxConfirm(override val data: WcBinanceTxConfirmParam) : WcRequest(data)
+ data class SignTransaction(override val data: WcSignTransaction) : WcRequest(data)
data class AddChain(override val data: WcAddChain) : WcRequest(data)
data class CustomRequest(override val data: WcCustomRequestData) : WcRequest(data)
+ data class SolanaSignRequest(override val data: SolanaTransactionRequest) : WcRequest(data)
}
@Singleton
-class WcJrpcRequestsDeserializer @Inject constructor(@SdkMoshi private val moshi: Moshi) {
+internal class WcJrpcRequestsDeserializer @Inject constructor(@SdkMoshi private val moshi: Moshi) {
@Suppress("ComplexMethod", "LongMethod")
fun deserialize(method: String, params: String): WcRequest {
@@ -154,9 +171,9 @@ class WcJrpcRequestsDeserializer @Inject constructor(@SdkMoshi private val moshi
val deserializedParams = moshi.adapter>(
Types.newParameterizedType(List::class.java, String::class.java),
).fromJsonOrNull(params) ?: return customRequest
- val data = WcEthereumSignMessage(
+ val data = WcSignMessage(
raw = deserializedParams,
- type = WcEthereumSignMessage.WCSignType.MESSAGE,
+ type = WcSignMessage.WCSignType.MESSAGE,
)
WcRequest.EthSign(data = data)
}
@@ -164,9 +181,9 @@ class WcJrpcRequestsDeserializer @Inject constructor(@SdkMoshi private val moshi
val deserializedParams = moshi.adapter>(
Types.newParameterizedType(List::class.java, String::class.java),
).fromJsonOrNull(params) ?: return customRequest
- val data = WcEthereumSignMessage(
+ val data = WcSignMessage(
raw = deserializedParams,
- type = WcEthereumSignMessage.WCSignType.PERSONAL_MESSAGE,
+ type = WcSignMessage.WCSignType.PERSONAL_MESSAGE,
)
WcRequest.EthSign(data = data)
}
@@ -175,9 +192,9 @@ class WcJrpcRequestsDeserializer @Inject constructor(@SdkMoshi private val moshi
params.substring(params.indexOf("\"") + 1, params.indexOf("\"", startIndex = 2)),
params.substring(params.indexOfFirst { it == '{' }, params.indexOfLast { it == '}' } + 1),
)
- val data = WcEthereumSignMessage(
+ val data = WcSignMessage(
deserializedParams,
- WcEthereumSignMessage.WCSignType.TYPED_MESSAGE,
+ WcSignMessage.WCSignType.TYPED_MESSAGE,
)
Timber.d("TypedData params: $deserializedParams")
WcRequest.EthSign(data)
@@ -186,23 +203,43 @@ class WcJrpcRequestsDeserializer @Inject constructor(@SdkMoshi private val moshi
return deserializeBnb(moshi, params) ?: customRequest
}
WcJrpcMethods.BNB_TRANSACTION_CONFIRM -> {
- val deserializedParams = moshi.adapter>(
- Types.newParameterizedType(List::class.java, WCBinanceTxConfirmParam::class.java),
+ val deserializedParams = moshi.adapter>(
+ Types.newParameterizedType(List::class.java, WcBinanceTxConfirmParam::class.java),
).fromJsonFirstOrNull(params) ?: return customRequest
WcRequest.BnbTxConfirm(data = deserializedParams)
}
WcJrpcMethods.SIGN_TRANSACTION -> {
- val deserializedParams = moshi.adapter>(
- Types.newParameterizedType(List::class.java, WCSignTransaction::class.java),
+ val deserializedParams = moshi.adapter>(
+ Types.newParameterizedType(List::class.java, WcSignTransaction::class.java),
).fromJsonFirstOrNull(params) ?: return customRequest
+
WcRequest.SignTransaction(data = deserializedParams)
}
WcJrpcMethods.WALLET_ADD_ETHEREUM_CHAIN -> {
val deserializedParams: WcAddChain = moshi.adapter>(
Types.newParameterizedType(List::class.java, WcAddChain::class.java),
).fromJsonFirstOrNull(params) ?: return customRequest
+
WcRequest.AddChain(data = deserializedParams)
}
+ WcJrpcMethods.SOLANA_SIGN_TX -> {
+ val tx = moshi.adapter(SolanaTransactionRequest::class.java)
+ .fromJsonOrNull(params)
+ ?: return customRequest
+
+ WcRequest.SolanaSignRequest(data = tx)
+ }
+ WcJrpcMethods.SOLANA_SIGN_MESSAGE -> {
+ val signMessage = moshi.adapter(SolanaSignMessage::class.java)
+ .fromJsonOrNull(params)
+ ?: return customRequest
+ val data = WcSignMessage(
+ raw = listOf(signMessage.publicKey, signMessage.message),
+ type = WcSignMessage.WCSignType.SOLANA_MESSAGE,
+ )
+
+ WcRequest.EthSign(data = data)
+ }
}
}
diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WcPreparedRequest.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WcPreparedRequest.kt
index be17cc867b..64798e2d9c 100644
--- a/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WcPreparedRequest.kt
+++ b/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WcPreparedRequest.kt
@@ -30,4 +30,21 @@ sealed class WcPreparedRequest(
requestId: Long,
derivationPath: String?,
) : WcPreparedRequest(preparedRequestData, topic, requestId, derivationPath)
+
+ class SignTransaction(
+ override val preparedRequestData: WcGenericTransactionData,
+ topic: String,
+ requestId: Long,
+ derivationPath: String?,
+ ) : WcPreparedRequest(preparedRequestData, topic, requestId, derivationPath)
+}
+
+data class WcGenericTransactionData(
+ val hashToSign: ByteArray,
+ val dAppName: String,
+ val type: TransactionType,
+)
+
+enum class TransactionType {
+ SOLANA_TX,
}
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WcSessionRequestConverter.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WcSessionRequestConverter.kt
index fe67571134..12085cad12 100644
--- a/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WcSessionRequestConverter.kt
+++ b/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WcSessionRequestConverter.kt
@@ -1,12 +1,13 @@
package com.tangem.tap.domain.walletconnect2.domain
import com.tangem.tap.domain.walletconnect.WalletConnectSdkHelper
+import com.tangem.tap.domain.walletconnect2.domain.mapper.mapToTransaction
import com.tangem.tap.domain.walletconnect2.domain.models.BnbData
import com.tangem.tap.domain.walletconnect2.domain.models.EthTransactionData
import com.tangem.tap.domain.walletconnect2.domain.models.WalletConnectEvents
import com.tangem.tap.features.details.redux.walletconnect.WcEthTransactionType
-class WcSessionRequestConverter(
+internal class WcSessionRequestConverter(
private val blockchainHelper: WcBlockchainHelper,
private val sessionsRepository: WalletConnectSessionsRepository,
private val sdkHelper: WalletConnectSdkHelper,
@@ -107,6 +108,20 @@ class WcSessionRequestConverter(
derivationPath = derivationPath,
)
}
+ is WcRequest.SolanaSignRequest -> {
+ val data = request.data.mapToTransaction()
+
+ WcPreparedRequest.SignTransaction(
+ preparedRequestData = WcGenericTransactionData(
+ hashToSign = data.getSerializedMessage(),
+ dAppName = sessionRequest.metaName,
+ type = TransactionType.SOLANA_TX,
+ ),
+ topic = sessionRequest.topic,
+ requestId = sessionRequest.id,
+ derivationPath = derivationPath,
+ )
+ }
else -> null
}
}
@@ -118,6 +133,7 @@ class WcSessionRequestConverter(
is WcRequest.EthSendTransaction -> request.data.from
is WcRequest.EthSignTransaction -> request.data.from
is WcRequest.EthSign -> request.data.address
+ is WcRequest.SolanaSignRequest -> request.data.feePayer
else -> null
}
}
diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/mapper/SolanaTransactionMappings.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/mapper/SolanaTransactionMappings.kt
new file mode 100644
index 0000000000..c97506bb7e
--- /dev/null
+++ b/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/mapper/SolanaTransactionMappings.kt
@@ -0,0 +1,30 @@
+package com.tangem.tap.domain.walletconnect2.domain.mapper
+
+import com.tangem.blockchain.blockchains.solana.solanaj.core.SolanaTransaction
+import com.tangem.tap.domain.walletconnect2.domain.models.solana.SolanaTransactionRequest
+import org.p2p.solanaj.core.AccountMeta
+import org.p2p.solanaj.core.PublicKey
+import org.p2p.solanaj.core.TransactionInstruction
+
+internal fun SolanaTransactionRequest.mapToTransaction(): SolanaTransaction {
+ val from = PublicKey(feePayer)
+ val instructions = instructions.map(SolanaTransactionRequest.Instruction::mapToInstruction)
+
+ return SolanaTransaction(from)
+ .apply {
+ instructions.forEach(::addInstruction)
+ setRecentBlockHash(recentBlockhash)
+ }
+}
+
+private fun SolanaTransactionRequest.Instruction.mapToInstruction() = TransactionInstruction(
+ /* programId = */ PublicKey(programId),
+ /* keys = */ keys.map(SolanaTransactionRequest.Key::mapToAccountMeta),
+ /* data = */ data.toByteArray(),
+)
+
+private fun SolanaTransactionRequest.Key.mapToAccountMeta() = AccountMeta(
+ /* publicKey = */ PublicKey(publicKey),
+ /* isSigner = */ isSigner,
+ /* isWritable = */ isWritable,
+)
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/models/WalletConnectError.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/models/WalletConnectError.kt
index efb5215bd7..7d6504d923 100644
--- a/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/models/WalletConnectError.kt
+++ b/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/models/WalletConnectError.kt
@@ -1,6 +1,9 @@
package com.tangem.tap.domain.walletconnect2.domain.models
sealed class WalletConnectError(val error: String) : Exception() {
+
+ data object UnsupportedDApp : WalletConnectError("UnsupportedDApp")
+
data class ApprovalErrorMissingNetworks(
val missingChains: List,
) : WalletConnectError("ApprovalErrorMissingNetworks")
diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/models/binance/WcBinanceOrder.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/models/binance/WcBinanceOrder.kt
index 8325aadd72..1e3f33db29 100644
--- a/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/models/binance/WcBinanceOrder.kt
+++ b/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/models/binance/WcBinanceOrder.kt
@@ -13,7 +13,7 @@ open class WcBinanceOrder(
val msgs: List,
) : WcRequestData
-data class WCBinanceTxConfirmParam(
+data class WcBinanceTxConfirmParam(
val ok: Boolean,
val errorMsg: String?,
) : WcRequestData
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/models/solana/SolanaSignMessage.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/models/solana/SolanaSignMessage.kt
new file mode 100644
index 0000000000..a521aa55e9
--- /dev/null
+++ b/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/models/solana/SolanaSignMessage.kt
@@ -0,0 +1,13 @@
+package com.tangem.tap.domain.walletconnect2.domain.models.solana
+
+import com.squareup.moshi.Json
+import com.squareup.moshi.JsonClass
+
+@JsonClass(generateAdapter = true)
+internal data class SolanaSignMessage(
+ @Json(name = "pubkey")
+ val publicKey: String,
+
+ @Json(name = "message")
+ val message: String,
+)
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/models/solana/SolanaTransactionRequest.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/models/solana/SolanaTransactionRequest.kt
new file mode 100644
index 0000000000..7ea66042f4
--- /dev/null
+++ b/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/models/solana/SolanaTransactionRequest.kt
@@ -0,0 +1,42 @@
+package com.tangem.tap.domain.walletconnect2.domain.models.solana
+
+import com.squareup.moshi.Json
+import com.squareup.moshi.JsonClass
+import com.tangem.tap.domain.walletconnect2.domain.WcRequestData
+
+@JsonClass(generateAdapter = true)
+data class SolanaTransactionRequest(
+ @Json(name = "feePayer")
+ val feePayer: String,
+
+ @Json(name = "recentBlockhash")
+ val recentBlockhash: String,
+
+ @Json(name = "instructions")
+ val instructions: List,
+) : WcRequestData {
+
+ @JsonClass(generateAdapter = true)
+ data class Instruction(
+ @Json(name = "programId")
+ val programId: String,
+
+ @Json(name = "data")
+ val data: List,
+
+ @Json(name = "keys")
+ val keys: List,
+ )
+
+ @JsonClass(generateAdapter = true)
+ data class Key(
+ @Json(name = "isSigner")
+ val isSigner: Boolean,
+
+ @Json(name = "isWritable")
+ val isWritable: Boolean,
+
+ @Json(name = "pubkey")
+ val publicKey: String,
+ )
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect2/toggles/WalletConnectFeatureToggles.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect2/toggles/WalletConnectFeatureToggles.kt
new file mode 100644
index 0000000000..26547d68e8
--- /dev/null
+++ b/app/src/main/java/com/tangem/tap/domain/walletconnect2/toggles/WalletConnectFeatureToggles.kt
@@ -0,0 +1,11 @@
+package com.tangem.tap.domain.walletconnect2.toggles
+
+import com.tangem.core.featuretoggle.manager.FeatureTogglesManager
+
+internal class WalletConnectFeatureToggles(
+ private val featureTogglesManager: FeatureTogglesManager,
+) {
+
+ val isSolanaTxSignEnabled: Boolean
+ get() = featureTogglesManager.isFeatureEnabled(name = "WC_SOLANA_TX_SIGN_ENABLED")
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/models/AddCustomTokenViewsModels.kt b/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/models/AddCustomTokenViewsModels.kt
index 16fba8e3f2..6ba979beab 100644
--- a/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/models/AddCustomTokenViewsModels.kt
+++ b/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/models/AddCustomTokenViewsModels.kt
@@ -291,12 +291,12 @@ enum class DerivationPathSelectorType {
internal sealed class AddCustomTokenWarning(val description: TextReference) {
/** Potential scam warning */
- object PotentialScamToken : AddCustomTokenWarning(
+ data object PotentialScamToken : AddCustomTokenWarning(
description = TextReference.Res(R.string.custom_token_validation_error_not_found),
)
/** Token already added warning */
- object TokenAlreadyAdded : AddCustomTokenWarning(
+ data object TokenAlreadyAdded : AddCustomTokenWarning(
description = TextReference.Res(R.string.custom_token_validation_error_already_added),
)
@@ -305,7 +305,7 @@ internal sealed class AddCustomTokenWarning(val description: TextReference) {
description = TextReference.Res(R.string.alert_manage_tokens_unsupported_message, networkName),
)
- object WrongDerivationPath : AddCustomTokenWarning(
+ data object WrongDerivationPath : AddCustomTokenWarning(
description = TextReference.Res(R.string.custom_token_invalid_derivation_path),
)
}
diff --git a/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/ui/components/AddCustomTokenForm.kt b/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/ui/components/AddCustomTokenForm.kt
index fb6c260728..dd1293ba65 100644
--- a/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/ui/components/AddCustomTokenForm.kt
+++ b/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/ui/components/AddCustomTokenForm.kt
@@ -132,6 +132,10 @@ private fun TextField(model: AddCustomTokenInputField, isError: Boolean) {
}
}
+/**
+ * Be careful with this function – ExposedDropdownMenuBox can crash the app if it is open and user clicks system back
+ * button. It was fixed in compose-material 1.6.4.
+ */
@OptIn(ExperimentalMaterialApi::class)
@Composable
private fun SelectorField(model: AddCustomTokenSelectorField) {
diff --git a/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/viewmodels/AddCustomTokenViewModel.kt b/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/viewmodels/AddCustomTokenViewModel.kt
index 9fdc0581da..3dd2804b29 100644
--- a/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/viewmodels/AddCustomTokenViewModel.kt
+++ b/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/viewmodels/AddCustomTokenViewModel.kt
@@ -15,7 +15,6 @@ import com.tangem.blockchain.common.Token
import com.tangem.blockchain.common.derivation.DerivationStyle
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.crypto.hdWallet.DerivationPath
-import com.tangem.crypto.hdWallet.HDWalletError
import com.tangem.domain.common.DerivationStyleProvider
import com.tangem.domain.common.extensions.*
import com.tangem.domain.common.util.cardTypesResolver
@@ -674,7 +673,7 @@ internal class AddCustomTokenViewModel @Inject constructor(
private fun createDerivationPathOrNull(rawPath: String): DerivationPath? {
return try {
DerivationPath(rawPath)
- } catch (error: HDWalletError) {
+ } catch (error: Throwable) {
null
}
}
diff --git a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsAction.kt b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsAction.kt
index 627b9bf0d5..05c586372d 100644
--- a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsAction.kt
+++ b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsAction.kt
@@ -16,29 +16,31 @@ sealed class DetailsAction : Action {
val shouldSaveUserWallets: Boolean,
) : DetailsAction()
- object ReCreateTwinsWallet : DetailsAction()
+ data object ReCreateTwinsWallet : DetailsAction()
sealed class ResetToFactory : DetailsAction() {
- object Start : ResetToFactory()
- object Proceed : ResetToFactory()
+ data object Start : ResetToFactory()
+ data object Proceed : ResetToFactory()
data class AcceptCondition1(val accepted: Boolean) : ResetToFactory()
data class AcceptCondition2(val accepted: Boolean) : ResetToFactory()
- object Failure : ResetToFactory()
- object Success : ResetToFactory()
+ data object Failure : ResetToFactory()
+ data object Success : ResetToFactory()
+
+ data class LastWarningDialogVisibility(val isShown: Boolean) : ResetToFactory()
}
- object ScanCard : DetailsAction()
+ data object ScanCard : DetailsAction()
data class PrepareCardSettingsData(val card: CardDTO, val cardTypesResolver: CardTypesResolver) : DetailsAction()
- object ResetCardSettingsData : DetailsAction()
- object ScanAndSaveUserWallet : DetailsAction() {
+ data object ResetCardSettingsData : DetailsAction()
+ data object ScanAndSaveUserWallet : DetailsAction() {
- object Success : DetailsAction()
+ data object Success : DetailsAction()
data class Error(val error: TextReference?) : DetailsAction()
}
- object DismissError : DetailsAction()
+ data object DismissError : DetailsAction()
sealed class AccessCodeRecovery : DetailsAction() {
object Open : AccessCodeRecovery()
@@ -50,14 +52,14 @@ sealed class DetailsAction : Action {
}
sealed class ManageSecurity : DetailsAction() {
- object OpenSecurity : ManageSecurity()
+ data object OpenSecurity : ManageSecurity()
data class SelectOption(val option: SecurityOption) : ManageSecurity()
- object SaveChanges : ManageSecurity() {
- object Success : ManageSecurity()
- object Failure : ManageSecurity()
+ data object SaveChanges : ManageSecurity() {
+ data object Success : ManageSecurity()
+ data object Failure : ManageSecurity()
}
- object ChangeAccessCode : ManageSecurity()
+ data object ChangeAccessCode : ManageSecurity()
}
sealed class AppSettings : DetailsAction() {
@@ -65,7 +67,7 @@ sealed class DetailsAction : Action {
val enable: Boolean,
val setting: AppSetting,
) : AppSettings() {
- object Success : AppSettings()
+ data object Success : AppSettings()
data class Failure(
val prevState: Boolean,
@@ -77,7 +79,7 @@ sealed class DetailsAction : Action {
val lifecycleScope: LifecycleCoroutineScope,
) : AppSettings()
- object EnrollBiometrics : AppSettings()
+ data object EnrollBiometrics : AppSettings()
data class BiometricsStatusChanged(
val needEnrollBiometrics: Boolean,
) : AppSettings()
diff --git a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsReducer.kt b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsReducer.kt
index e829d9c937..5b3f2fafd3 100644
--- a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsReducer.kt
+++ b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsReducer.kt
@@ -114,6 +114,7 @@ private fun handlePrepareCardSettingsScreen(
null
},
isShowPasswordResetRadioButton = isShowPasswordResetRadioButton,
+ isLastWarningDialogShown = false,
)
return state.copy(cardSettingsState = cardSettingsState)
}
@@ -181,6 +182,13 @@ private fun handleEraseWallet(action: DetailsAction.ResetToFactory, state: Detai
),
)
}
+ is DetailsAction.ResetToFactory.LastWarningDialogVisibility -> {
+ state.copy(
+ cardSettingsState = cardSettingsState?.copy(
+ isLastWarningDialogShown = action.isShown,
+ ),
+ )
+ }
else -> state
}
diff --git a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsState.kt b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsState.kt
index 1eab7ec856..e1c523e759 100644
--- a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsState.kt
+++ b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsState.kt
@@ -46,6 +46,7 @@ data class CardSettingsState(
val condition2Checked: Boolean,
val accessCodeRecovery: AccessCodeRecoveryState? = null,
val isShowPasswordResetRadioButton: Boolean,
+ val isLastWarningDialogShown: Boolean,
)
data class ManageSecurityState(
diff --git a/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectMiddleware.kt b/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectMiddleware.kt
index c6422a0706..4a26922744 100644
--- a/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectMiddleware.kt
+++ b/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectMiddleware.kt
@@ -13,12 +13,12 @@ import com.tangem.domain.common.extensions.withMainContext
import com.tangem.domain.common.util.cardTypesResolver
import com.tangem.domain.common.util.derivationStyleProvider
import com.tangem.domain.models.scan.ScanResponse
+import com.tangem.domain.qrscanning.models.SourceType
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.walletconnect.WalletConnectActions
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.feature.qrscanning.QrScanningRouter
-import com.tangem.feature.qrscanning.SourceType
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.extensions.inject
import com.tangem.tap.common.redux.AppState
@@ -352,6 +352,13 @@ class WalletConnectMiddleware {
),
)
}
+ is WalletConnectError.UnsupportedDApp -> {
+ store.dispatchOnMain(
+ GlobalAction.ShowDialog(
+ WalletConnectDialog.UnsupportedDapp,
+ ),
+ )
+ }
is WalletConnectError.ExternalApprovalError -> {
Timber.e(action.error, "ExternalApprovalError ${action.error.message}")
// do not show dialog on this event
@@ -375,6 +382,7 @@ class WalletConnectMiddleware {
is WcPreparedRequest.BnbTransaction -> WalletConnectDialog.BnbTransactionDialog(request)
is WcPreparedRequest.EthTransaction -> WalletConnectDialog.RequestTransaction(request)
is WcPreparedRequest.EthSign -> WalletConnectDialog.PersonalSign(request)
+ is WcPreparedRequest.SignTransaction -> WalletConnectDialog.SignTransactionDialog(request)
}
store.dispatch(GlobalAction.ShowDialog(dialog))
}
diff --git a/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectState.kt b/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectState.kt
index 34eed45fd3..68cfd4255b 100644
--- a/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectState.kt
+++ b/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectState.kt
@@ -8,8 +8,8 @@ import com.tangem.blockchain.common.derivation.DerivationStyle
import com.tangem.core.navigation.StateDialog
import com.tangem.crypto.hdWallet.DerivationPath
import com.tangem.domain.models.scan.ScanResponse
-import com.tangem.tap.domain.walletconnect2.domain.WcEthereumSignMessage
import com.tangem.tap.domain.walletconnect2.domain.WcPreparedRequest
+import com.tangem.tap.domain.walletconnect2.domain.WcSignMessage
import com.tangem.tap.domain.walletconnect2.domain.models.WalletConnectEvents
import com.tangem.tap.features.details.ui.walletconnect.WcSessionForScreen
import com.tangem.tap.features.details.ui.walletconnect.dialogs.PersonalSignDialogData
@@ -88,11 +88,12 @@ data class WalletForSession(
sealed class WalletConnectDialog : StateDialog {
data class ClipboardOrScanQr(val clipboardUri: String) : WalletConnectDialog()
- object UnsupportedCard : WalletConnectDialog()
+ data object UnsupportedCard : WalletConnectDialog()
data class UnsupportedNetwork(val networks: List? = null) : WalletConnectDialog()
+ data object UnsupportedDapp : WalletConnectDialog()
data class AddNetwork(val networks: List) : WalletConnectDialog()
- object OpeningSessionRejected : WalletConnectDialog()
- object SessionTimeout : WalletConnectDialog()
+ data object OpeningSessionRejected : WalletConnectDialog()
+ data object SessionTimeout : WalletConnectDialog()
data class ApproveWcSession(
val session: WalletConnectSession,
val networks: List,
@@ -117,6 +118,10 @@ sealed class WalletConnectDialog : StateDialog {
data class BnbTransactionDialog(
val data: WcPreparedRequest.BnbTransaction,
) : WalletConnectDialog()
+
+ data class SignTransactionDialog(
+ val data: WcPreparedRequest.SignTransaction,
+ ) : WalletConnectDialog()
}
data class WcTransactionData(
@@ -138,7 +143,7 @@ data class WcPersonalSignData(
val topic: String,
val id: Long,
val dialogData: PersonalSignDialogData,
- val type: WcEthereumSignMessage.WCSignType,
+ val type: WcSignMessage.WCSignType,
)
sealed class BinanceMessageData(
diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsFragment.kt b/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsFragment.kt
index 57376f6c70..d050b873e5 100644
--- a/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsFragment.kt
+++ b/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsFragment.kt
@@ -5,8 +5,11 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.tangem.core.analytics.Analytics
import com.tangem.core.navigation.NavigationAction
+import com.tangem.core.navigation.email.EmailSender
import com.tangem.core.ui.screen.ComposeFragment
import com.tangem.core.ui.theme.AppThemeModeHolder
+import com.tangem.domain.feedback.FeedbackManagerFeatureToggles
+import com.tangem.domain.feedback.GetSupportFeedbackEmailUseCase
import com.tangem.domain.wallets.repository.WalletsRepository
import com.tangem.tap.common.analytics.events.Settings
import com.tangem.tap.features.details.redux.DetailsState
@@ -24,11 +27,26 @@ internal class DetailsFragment : ComposeFragment(), StoreSubscriber,
private val walletsRepository: WalletsRepository,
+ private val feedbackManagerFeatureToggles: FeedbackManagerFeatureToggles,
+ private val getSupportFeedbackEmailUseCase: GetSupportFeedbackEmailUseCase,
+ private val emailSender: EmailSender,
) {
var detailsScreenState: MutableState = mutableStateOf(updateState(store.state.detailsState))
@@ -140,7 +148,21 @@ internal class DetailsViewModel(
private fun sendFeedback() {
Analytics.send(Basic.ButtonSupport(AnalyticsParam.ScreensSources.Settings))
- store.dispatchOnMain(GlobalAction.SendEmail(FeedbackEmail()))
+ if (feedbackManagerFeatureToggles.isLocalLogsEnabled) {
+ mainScope.launch {
+ val email = getSupportFeedbackEmailUseCase()
+ emailSender.send(
+ email = EmailSender.Email(
+ address = email.address,
+ subject = email.subject,
+ message = email.message,
+ attachment = email.file,
+ ),
+ )
+ }
+ } else {
+ store.dispatchOnMain(GlobalAction.SendEmail(FeedbackEmail()))
+ }
}
private fun navigateToAppSettings() {
diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardScreen.kt b/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardScreen.kt
index 1320abaed7..35b4c9bae8 100644
--- a/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardScreen.kt
+++ b/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardScreen.kt
@@ -11,9 +11,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
-import com.tangem.core.ui.components.SpacerH12
-import com.tangem.core.ui.components.SpacerH16
-import com.tangem.core.ui.components.SpacerH24
+import com.tangem.core.ui.components.*
import com.tangem.core.ui.res.TangemTheme
import com.tangem.tap.features.details.ui.cardsettings.TextReference
import com.tangem.tap.features.details.ui.cardsettings.resolveReference
@@ -35,6 +33,8 @@ internal fun ResetCardScreen(state: ResetCardScreenState, onBackClick: () -> Uni
},
onBackClick = onBackClick,
)
+
+ LastWarningDialog(state = state)
}
@Composable
@@ -181,6 +181,26 @@ private fun ResetButton(enabled: Boolean, onResetButtonClick: () -> Unit) {
)
}
+@Composable
+private fun LastWarningDialog(state: ResetCardScreenState) {
+ if (state is ResetCardScreenState.ResetCardScreenContent && state.lastWarningDialog.isShown) {
+ BasicDialog(
+ title = stringResource(id = R.string.common_attention),
+ message = stringResource(id = R.string.card_settings_action_sheet_title),
+ dismissButton = DialogButton(
+ title = stringResource(id = R.string.card_settings_action_sheet_reset),
+ warning = true,
+ onClick = state.lastWarningDialog.onResetButtonClick,
+ ),
+ confirmButton = DialogButton(
+ title = stringResource(id = R.string.common_cancel),
+ onClick = state.lastWarningDialog.onDismiss,
+ ),
+ onDismissDialog = state.lastWarningDialog.onDismiss,
+ )
+ }
+}
+
// region Preview
@Composable
private fun ResetCardScreenSample(modifier: Modifier = Modifier) {
@@ -196,6 +216,11 @@ private fun ResetCardScreenSample(modifier: Modifier = Modifier) {
onAcceptCondition1ToggleClick = {},
onAcceptCondition2ToggleClick = {},
onResetButtonClick = {},
+ lastWarningDialog = ResetCardScreenState.ResetCardScreenContent.LastWarningDialog(
+ isShown = false,
+ onResetButtonClick = {},
+ onDismiss = {},
+ ),
),
onBackClick = {},
)
diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardScreenState.kt b/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardScreenState.kt
index a12decbeef..d70071ed49 100644
--- a/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardScreenState.kt
+++ b/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardScreenState.kt
@@ -15,9 +15,16 @@ internal sealed class ResetCardScreenState {
val onAcceptCondition1ToggleClick: (Boolean) -> Unit,
val onAcceptCondition2ToggleClick: (Boolean) -> Unit,
val onResetButtonClick: () -> Unit,
+ val lastWarningDialog: LastWarningDialog,
) : ResetCardScreenState() {
val resetButtonEnabled: Boolean
get() = accepted
+
+ data class LastWarningDialog(
+ val isShown: Boolean,
+ val onResetButtonClick: () -> Unit,
+ val onDismiss: () -> Unit,
+ )
}
internal enum class WarningsToReset {
diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardViewModel.kt b/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardViewModel.kt
index 62218c3752..02e00914e1 100644
--- a/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardViewModel.kt
+++ b/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardViewModel.kt
@@ -30,7 +30,25 @@ internal class ResetCardViewModel(private val store: Store) {
acceptCondition2Checked = state?.condition2Checked ?: false,
onAcceptCondition1ToggleClick = { store.dispatch(DetailsAction.ResetToFactory.AcceptCondition1(it)) },
onAcceptCondition2ToggleClick = { store.dispatch(DetailsAction.ResetToFactory.AcceptCondition2(it)) },
- onResetButtonClick = { store.dispatch(DetailsAction.ResetToFactory.Proceed) },
+ onResetButtonClick = { showLastWarningDialog() },
+ lastWarningDialog = ResetCardScreenState.ResetCardScreenContent.LastWarningDialog(
+ isShown = state?.isLastWarningDialogShown ?: false,
+ onResetButtonClick = ::onLastWarningDialogResetClicked,
+ onDismiss = ::onLastWarningDialogDismiss,
+ ),
)
}
+
+ private fun showLastWarningDialog() {
+ store.dispatch(DetailsAction.ResetToFactory.LastWarningDialogVisibility(isShown = true))
+ }
+
+ private fun onLastWarningDialogResetClicked() {
+ store.dispatch(DetailsAction.ResetToFactory.LastWarningDialogVisibility(isShown = false))
+ store.dispatch(DetailsAction.ResetToFactory.Proceed)
+ }
+
+ private fun onLastWarningDialogDismiss() {
+ store.dispatch(DetailsAction.ResetToFactory.LastWarningDialogVisibility(isShown = false))
+ }
}
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/WalletConnectViewModel.kt b/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/WalletConnectViewModel.kt
index c096d0531e..cb6a206195 100644
--- a/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/WalletConnectViewModel.kt
+++ b/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/WalletConnectViewModel.kt
@@ -2,8 +2,8 @@ package com.tangem.tap.features.details.ui.walletconnect
import androidx.lifecycle.*
import arrow.core.getOrElse
-import com.tangem.feature.qrscanning.SourceType
-import com.tangem.feature.qrscanning.usecase.ListenToQrScanningUseCase
+import com.tangem.domain.qrscanning.models.SourceType
+import com.tangem.domain.qrscanning.usecases.ListenToQrScanningUseCase
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectAction
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectSession
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectState
diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/dialogs/ClipboardOrScanQrDialog.kt b/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/dialogs/ClipboardOrScanQrDialog.kt
index d3f92e4286..23a605e2d5 100644
--- a/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/dialogs/ClipboardOrScanQrDialog.kt
+++ b/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/dialogs/ClipboardOrScanQrDialog.kt
@@ -7,7 +7,7 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.tangem.core.navigation.AppScreen
import com.tangem.core.navigation.NavigationAction
import com.tangem.feature.qrscanning.QrScanningRouter
-import com.tangem.feature.qrscanning.SourceType
+import com.tangem.domain.qrscanning.models.SourceType
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectAction
import com.tangem.tap.store
diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/dialogs/SignTransactionDialog.kt b/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/dialogs/SignTransactionDialog.kt
new file mode 100644
index 0000000000..a21b7a4d75
--- /dev/null
+++ b/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/dialogs/SignTransactionDialog.kt
@@ -0,0 +1,29 @@
+package com.tangem.tap.features.details.ui.walletconnect.dialogs
+
+import android.content.Context
+import androidx.appcompat.app.AlertDialog
+import com.tangem.tap.common.redux.global.GlobalAction
+import com.tangem.tap.domain.walletconnect2.domain.WcPreparedRequest
+import com.tangem.tap.features.details.redux.walletconnect.WalletConnectAction
+import com.tangem.tap.store
+import com.tangem.wallet.R
+
+internal object SignTransactionDialog {
+
+ fun create(preparedData: WcPreparedRequest.SignTransaction, context: Context): AlertDialog {
+ val message = context.getString(R.string.wallet_connect_alert_sign_message, "")
+ return AlertDialog.Builder(context).apply {
+ setTitle(context.getString(R.string.wallet_connect_title))
+ setMessage(message)
+ setPositiveButton(context.getText(R.string.common_sign)) { _, _ ->
+ store.dispatch(WalletConnectAction.PerformRequestedAction(preparedData))
+ }
+ setNegativeButton(context.getText(R.string.common_reject)) { _, _ ->
+ store.dispatch(WalletConnectAction.RejectRequest(preparedData.topic, preparedData.requestId))
+ }
+ setOnDismissListener {
+ store.dispatch(GlobalAction.HideDialog)
+ }
+ }.create()
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/features/home/redux/HomeAction.kt b/app/src/main/java/com/tangem/tap/features/home/redux/HomeAction.kt
index 987d3d51a5..2266971dad 100644
--- a/app/src/main/java/com/tangem/tap/features/home/redux/HomeAction.kt
+++ b/app/src/main/java/com/tangem/tap/features/home/redux/HomeAction.kt
@@ -1,6 +1,5 @@
package com.tangem.tap.features.home.redux
-import com.tangem.tap.common.entities.IndeterminateProgressButton
import kotlinx.coroutines.CoroutineScope
import org.rekotlin.Action
@@ -24,6 +23,4 @@ sealed class HomeAction : Action {
data class GoToShop(val userCountryCode: String?) : HomeAction()
data class UpdateCountryCode(val userCountryCode: String) : HomeAction()
-
- data class ChangeScanCardButtonState(val state: IndeterminateProgressButton) : HomeAction()
}
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/features/home/redux/HomeMiddleware.kt b/app/src/main/java/com/tangem/tap/features/home/redux/HomeMiddleware.kt
index 9a936a74e8..40110a5a22 100644
--- a/app/src/main/java/com/tangem/tap/features/home/redux/HomeMiddleware.kt
+++ b/app/src/main/java/com/tangem/tap/features/home/redux/HomeMiddleware.kt
@@ -15,12 +15,10 @@ import com.tangem.domain.userwallets.UserWalletBuilder
import com.tangem.tap.common.analytics.converters.ParamCardCurrencyConverter
import com.tangem.tap.common.analytics.events.IntroductionProcess
import com.tangem.tap.common.analytics.events.Shop
-import com.tangem.tap.common.entities.IndeterminateProgressButton
import com.tangem.tap.common.extensions.*
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.features.home.redux.HomeMiddleware.NEW_BUY_WALLET_URL
-import com.tangem.tap.features.send.redux.states.ButtonState
import com.tangem.tap.preferencesStorage
import com.tangem.tap.proxy.redux.DaggerGraphState
import com.tangem.tap.scope
@@ -32,6 +30,8 @@ import org.rekotlin.Action
import org.rekotlin.Middleware
import timber.log.Timber
+private const val HIDE_PROGRESS_DELAY = 400L
+
object HomeMiddleware {
val handler = homeMiddleware
@@ -85,17 +85,16 @@ private suspend fun readCard() {
analyticsSource = AnalyticsParam.ScreensSources.Intro,
onProgressStateChange = { showProgress ->
if (showProgress) {
- changeButtonState(ButtonState.PROGRESS)
+ store.dispatch(HomeAction.ScanInProgress(scanInProgress = true))
} else {
- changeButtonState(ButtonState.ENABLED)
+ delay(HIDE_PROGRESS_DELAY)
+ store.dispatch(HomeAction.ScanInProgress(scanInProgress = false))
}
},
- onScanStateChange = { scanInProgress ->
- store.dispatch(HomeAction.ScanInProgress(scanInProgress))
- },
onFailure = {
Timber.e(it, "Unable to scan card")
- changeButtonState(ButtonState.ENABLED)
+ delay(HIDE_PROGRESS_DELAY)
+ store.dispatch(HomeAction.ScanInProgress(scanInProgress = false))
},
onSuccess = { scanResponse ->
proceedWithScanResponse(scanResponse)
@@ -142,10 +141,6 @@ private fun sendSignedInCardAnalyticsEvent(scanResponse: ScanResponse) {
private suspend fun navigateTo(appScreen: AppScreen) {
store.dispatchOnMain(NavigationAction.NavigateTo(appScreen))
- delay(timeMillis = 200)
- changeButtonState(ButtonState.ENABLED)
-}
-
-private fun changeButtonState(state: ButtonState) {
- store.dispatchOnMain(HomeAction.ChangeScanCardButtonState(IndeterminateProgressButton(state)))
+ delay(HIDE_PROGRESS_DELAY)
+ store.dispatch(HomeAction.ScanInProgress(scanInProgress = false))
}
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/features/home/redux/HomeReducer.kt b/app/src/main/java/com/tangem/tap/features/home/redux/HomeReducer.kt
index 81f1adee30..bc71e1fef2 100644
--- a/app/src/main/java/com/tangem/tap/features/home/redux/HomeReducer.kt
+++ b/app/src/main/java/com/tangem/tap/features/home/redux/HomeReducer.kt
@@ -22,9 +22,6 @@ private fun internalReduce(action: Action, appState: AppState): HomeState {
is HomeAction.ScanInProgress -> {
state = state.copy(scanInProgress = action.scanInProgress)
}
- is HomeAction.ChangeScanCardButtonState -> {
- state = state.copy(btnScanState = action.state)
- }
is HomeAction.UpdateCountryCode -> {
state.onCountryCodeUpdate(state, action.userCountryCode)
}
diff --git a/app/src/main/java/com/tangem/tap/features/home/redux/HomeState.kt b/app/src/main/java/com/tangem/tap/features/home/redux/HomeState.kt
index 7ffd5d139e..2e584d3d35 100644
--- a/app/src/main/java/com/tangem/tap/features/home/redux/HomeState.kt
+++ b/app/src/main/java/com/tangem/tap/features/home/redux/HomeState.kt
@@ -1,15 +1,14 @@
package com.tangem.tap.features.home.redux
+import androidx.compose.runtime.Immutable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
-import com.tangem.tap.common.entities.IndeterminateProgressButton
-import com.tangem.tap.features.send.redux.states.ButtonState
import org.rekotlin.StateType
import java.util.Locale
+@Immutable
data class HomeState(
val scanInProgress: Boolean = false,
- val btnScanState: IndeterminateProgressButton = IndeterminateProgressButton(ButtonState.ENABLED),
val stories: List = initDefaultStories(),
) : StateType {
diff --git a/app/src/main/java/com/tangem/tap/features/main/MainViewModel.kt b/app/src/main/java/com/tangem/tap/features/main/MainViewModel.kt
index dc5b58c12a..6f5d289dd9 100644
--- a/app/src/main/java/com/tangem/tap/features/main/MainViewModel.kt
+++ b/app/src/main/java/com/tangem/tap/features/main/MainViewModel.kt
@@ -10,6 +10,7 @@ import com.tangem.domain.balancehiding.BalanceHidingSettings
import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase
import com.tangem.domain.balancehiding.ListenToFlipsUseCase
import com.tangem.domain.balancehiding.UpdateBalanceHidingSettingsUseCase
+import com.tangem.domain.settings.DeleteDeprecatedLogsUseCase
import com.tangem.tap.features.main.model.MainScreenState
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.hilt.android.lifecycle.HiltViewModel
@@ -17,12 +18,14 @@ import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import javax.inject.Inject
+@Suppress("LongParameterList")
@HiltViewModel
internal class MainViewModel @Inject constructor(
private val updateBalanceHidingSettingsUseCase: UpdateBalanceHidingSettingsUseCase,
private val listenToFlipsUseCase: ListenToFlipsUseCase,
private val reduxNavController: ReduxNavController,
private val fetchAppCurrenciesUseCase: FetchAppCurrenciesUseCase,
+ private val deleteDeprecatedLogsUseCase: DeleteDeprecatedLogsUseCase,
private val dispatchers: CoroutineDispatcherProvider,
getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase,
) : ViewModel(), MainIntents {
@@ -41,6 +44,10 @@ internal class MainViewModel @Inject constructor(
observeFlips()
displayBalancesHidingStatusToast()
displayHiddenBalancesModalNotification()
+
+ viewModelScope.launch(dispatchers.main) {
+ deleteDeprecatedLogsUseCase()
+ }
}
private fun updateAppCurrencies() {
diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingHelper.kt b/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingHelper.kt
index 9348207ccf..050f08d79e 100644
--- a/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingHelper.kt
+++ b/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingHelper.kt
@@ -9,6 +9,7 @@ import com.tangem.core.navigation.AppScreen
import com.tangem.core.navigation.NavigationAction
import com.tangem.domain.common.util.cardTypesResolver
import com.tangem.domain.common.util.twinsIsTwinned
+import com.tangem.domain.models.scan.CardDTO
import com.tangem.domain.models.scan.ProductType
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.userwallets.UserWalletBuilder
@@ -42,8 +43,9 @@ object OnboardingHelper {
response.cardTypesResolver.isWallet2() || response.cardTypesResolver.isShibaWallet() -> {
val emptyWallets = response.card.wallets.isEmpty()
val activationInProgress = onboardingManager?.isActivationInProgress(cardId)
- val backupNotActive = response.card.backupStatus?.isActive != true && !DemoHelper.isDemoCard(response)
- emptyWallets || activationInProgress == true || backupNotActive
+ val isNoBackup = response.card.backupStatus == CardDTO.BackupStatus.NoBackup &&
+ !DemoHelper.isDemoCard(response)
+ emptyWallets || activationInProgress == true || isNoBackup
}
response.card.wallets.isNotEmpty() -> onboardingManager?.isActivationInProgress(cardId) ?: false
@@ -73,6 +75,7 @@ object OnboardingHelper {
scanResponse: ScanResponse,
accessCode: String? = null,
backupCardsIds: List? = null,
+ hasBackupError: Boolean = false,
) {
Analytics.setContext(scanResponse)
scope.launch {
@@ -89,7 +92,7 @@ object OnboardingHelper {
val toggles = store.inject(DaggerGraphState::userWalletsListManagerFeatureToggles)
if (toggles.isGeneralManagerEnabled) {
- store.dispatchWithMain(SaveWalletAction.SaveWalletAfterBackup)
+ store.dispatchWithMain(SaveWalletAction.SaveWalletAfterBackup(hasBackupError))
} else {
store.dispatchWithMain(SaveWalletAction.Save)
}
@@ -97,7 +100,7 @@ object OnboardingHelper {
// When should not save user wallets but device has biometry and save wallet screen has not been shown,
// then open save wallet screen
tangemSdkManager.canUseBiometry && preferencesStorage.shouldShowSaveUserWalletScreen -> {
- proceedWithScanResponse(scanResponse, backupCardsIds)
+ proceedWithScanResponse(scanResponse, backupCardsIds, hasBackupError)
delay(timeMillis = 1_200)
@@ -114,7 +117,7 @@ object OnboardingHelper {
}
// If device has no biometry and save wallet screen has been shown, then go through old scenario
else -> {
- proceedWithScanResponse(scanResponse, backupCardsIds)
+ proceedWithScanResponse(scanResponse, backupCardsIds, hasBackupError)
store.dispatchOnMain(NavigationAction.NavigateTo(AppScreen.Wallet))
}
}
@@ -134,8 +137,13 @@ object OnboardingHelper {
}
}
- private suspend fun proceedWithScanResponse(scanResponse: ScanResponse, backupCardsIds: List?) {
- val userWallet = UserWalletBuilder(scanResponse)
+ private suspend fun proceedWithScanResponse(
+ scanResponse: ScanResponse,
+ backupCardsIds: List?,
+ hasBackupError: Boolean,
+ ) {
+ val userWallet = UserWalletBuilder(scanResponse = scanResponse)
+ .hasBackupError(hasBackupError)
.backupCardsIds(backupCardsIds?.toSet())
.build()
.guard {
diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/note/redux/OnboardingNoteMiddleware.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/note/redux/OnboardingNoteMiddleware.kt
index 114dedc7df..ca78c542d3 100644
--- a/app/src/main/java/com/tangem/tap/features/onboarding/products/note/redux/OnboardingNoteMiddleware.kt
+++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/note/redux/OnboardingNoteMiddleware.kt
@@ -4,8 +4,10 @@ import com.tangem.common.CompletionResult
import com.tangem.common.extensions.guard
import com.tangem.core.analytics.Analytics
import com.tangem.core.navigation.NavigationAction
+import com.tangem.data.tokens.utils.CryptoCurrencyFactory
import com.tangem.domain.common.extensions.makePrimaryWalletManager
import com.tangem.domain.common.extensions.withMainContext
+import com.tangem.domain.common.util.derivationStyleProvider
import com.tangem.tap.common.analytics.events.AnalyticsParam
import com.tangem.tap.common.analytics.events.Onboarding
import com.tangem.tap.common.entities.ProgressState
@@ -171,8 +173,13 @@ private fun handleNoteAction(appState: () -> AppState?, action: Action, dispatch
return
}
- val topUpUrl = walletManager.getTopUpUrl() ?: return
val blockchain = walletManager.wallet.blockchain
+ val cryptoCurrency = CryptoCurrencyFactory().createCoin(
+ blockchain,
+ null,
+ scanResponse.derivationStyleProvider,
+ ) ?: return
+ val topUpUrl = walletManager.getTopUpUrl(cryptoCurrency) ?: return
val currencyType = AnalyticsParam.CurrencyType.Blockchain(blockchain)
Analytics.send(Onboarding.Topup.ButtonBuyCrypto(currencyType))
diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/redux/TwinCardsMiddleware.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/redux/TwinCardsMiddleware.kt
index c2fc60baaa..e973dae678 100644
--- a/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/redux/TwinCardsMiddleware.kt
+++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/redux/TwinCardsMiddleware.kt
@@ -6,8 +6,10 @@ import com.tangem.common.extensions.guard
import com.tangem.core.analytics.Analytics
import com.tangem.core.navigation.AppScreen
import com.tangem.core.navigation.NavigationAction
+import com.tangem.data.tokens.utils.CryptoCurrencyFactory
import com.tangem.domain.common.extensions.makePrimaryWalletManager
import com.tangem.domain.common.extensions.withMainContext
+import com.tangem.domain.common.util.derivationStyleProvider
import com.tangem.domain.common.util.twinsIsTwinned
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.userwallets.UserWalletIdBuilder
@@ -293,8 +295,14 @@ private fun handle(action: Action, dispatch: DispatchFunction) {
return
}
- val topUpUrl = walletManager.getTopUpUrl() ?: return
+ val scanResponse = onboardingManager?.scanResponse ?: return
val blockchain = walletManager.wallet.blockchain
+ val cryptoCurrency = CryptoCurrencyFactory().createCoin(
+ blockchain,
+ null,
+ scanResponse.derivationStyleProvider,
+ ) ?: return
+ val topUpUrl = walletManager.getTopUpUrl(cryptoCurrency) ?: return
val currencyType = AnalyticsParam.CurrencyType.Blockchain(blockchain)
Analytics.send(Onboarding.Topup.ButtonBuyCrypto(currencyType))
diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletAction.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletAction.kt
index 6b536fc754..c624e36825 100644
--- a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletAction.kt
+++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletAction.kt
@@ -9,23 +9,23 @@ import kotlinx.coroutines.CoroutineScope
import org.rekotlin.Action
sealed class OnboardingWalletAction : Action {
- object Init : OnboardingWalletAction()
- object GetToCreateWalletStep : OnboardingWalletAction()
- object CreateWallet : OnboardingWalletAction()
+ data object Init : OnboardingWalletAction()
+ data object GetToCreateWalletStep : OnboardingWalletAction()
+ data object CreateWallet : OnboardingWalletAction()
data class WalletWasCreated(
val shouldSendAnalyticsEvent: Boolean,
val result: CompletionResult,
) : OnboardingWalletAction()
- object Done : OnboardingWalletAction()
+ data object Done : OnboardingWalletAction()
data class FinishOnboarding(val scope: CoroutineScope) : OnboardingWalletAction()
- object ResumeBackup : OnboardingWalletAction()
+ data object ResumeBackup : OnboardingWalletAction()
data class LoadArtwork(val cardArtworkUriForUnfinishedBackup: Uri? = null) : OnboardingWalletAction()
class SetArtworkUrl(val artworkUri: Uri?) : OnboardingWalletAction()
- object OnBackPressed : OnboardingWalletAction()
+ data object OnBackPressed : OnboardingWalletAction()
}
sealed class OnboardingWallet2Action : OnboardingWalletAction() {
@@ -38,6 +38,7 @@ sealed class OnboardingWallet2Action : OnboardingWalletAction() {
data class ImportWallet(
val mnemonicComponents: List,
+ val passphrase: String?,
val seedPhraseSource: SeedPhraseSource,
val callback: (CompletionResult) -> Unit,
) : OnboardingWallet2Action()
@@ -49,45 +50,46 @@ sealed class OnboardingWallet2Action : OnboardingWalletAction() {
sealed class BackupAction : Action {
- object IntroduceBackup : BackupAction()
- object StartBackup : BackupAction()
- object SkipBackup : BackupAction()
+ data object IntroduceBackup : BackupAction()
+ data object StartBackup : BackupAction()
+ data object SkipBackup : BackupAction()
- object StartAddingPrimaryCard : BackupAction()
- object ScanPrimaryCard : BackupAction()
+ data object ErrorInBackupCard : BackupAction()
+ data object StartAddingPrimaryCard : BackupAction()
+ data object ScanPrimaryCard : BackupAction()
/**
* Check for unfinished backup of standard Wallets
* See more GlobalAction.Onboarding.StartForUnfinishedBackup
*/
- object CheckForUnfinishedBackup : BackupAction()
+ data object CheckForUnfinishedBackup : BackupAction()
- object StartAddingBackupCards : BackupAction()
- object AddBackupCard : BackupAction() {
- object Success : BackupAction()
+ data object StartAddingBackupCards : BackupAction()
+ data object AddBackupCard : BackupAction() {
+ data object Success : BackupAction()
data class ChangeButtonLoading(val isLoading: Boolean) : BackupAction()
}
- object FinishAddingBackupCards : BackupAction()
+ data object FinishAddingBackupCards : BackupAction()
- object ShowAccessCodeInfoScreen : BackupAction()
- object ShowEnterAccessCodeScreen : BackupAction()
+ data object ShowAccessCodeInfoScreen : BackupAction()
+ data object ShowEnterAccessCodeScreen : BackupAction()
data class CheckAccessCode(val accessCode: String) : BackupAction()
data class SetAccessCodeError(val error: AccessCodeError?) : BackupAction()
data class SaveFirstAccessCode(val accessCode: String) : BackupAction()
data class SaveAccessCodeConfirmation(val accessCodeConfirmation: String) : BackupAction()
- object OnAccessCodeDialogClosed : BackupAction()
+ data object OnAccessCodeDialogClosed : BackupAction()
- object PrepareToWritePrimaryCard : BackupAction()
- object WritePrimaryCard : BackupAction()
+ data object PrepareToWritePrimaryCard : BackupAction()
+ data object WritePrimaryCard : BackupAction()
data class PrepareToWriteBackupCard(val cardNumber: Int) : BackupAction()
data class WriteBackupCard(val cardNumber: Int) : BackupAction()
data class FinishBackup(val withAnalytics: Boolean = true) : BackupAction()
- object DiscardBackup : BackupAction()
- object DiscardSavedBackup : BackupAction()
- object ResumeFoundUnfinishedBackup : BackupAction()
+ data object DiscardBackup : BackupAction()
+ data object DiscardSavedBackup : BackupAction()
+ data object ResumeFoundUnfinishedBackup : BackupAction()
data class ResetBackupCard(val cardId: String) : BackupAction()
}
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletMiddleware.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletMiddleware.kt
index 460734903f..5c29c96fb9 100644
--- a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletMiddleware.kt
+++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletMiddleware.kt
@@ -20,6 +20,7 @@ import com.tangem.domain.userwallets.Artwork
import com.tangem.domain.userwallets.UserWalletBuilder
import com.tangem.feature.onboarding.data.model.CreateWalletResponse
import com.tangem.feature.onboarding.presentation.wallet2.analytics.SeedPhraseSource
+import com.tangem.feature.wallet.presentation.wallet.domain.BackupValidator
import com.tangem.operations.attestation.OnlineCardVerifier
import com.tangem.operations.backup.BackupService
import com.tangem.tap.*
@@ -173,6 +174,7 @@ private fun handleWalletAction(action: Action) {
scanResponse = updatedScanResponse,
accessCode = backupState.accessCode,
backupCardsIds = backupState.backupCardIds,
+ hasBackupError = backupState.hasBackupError,
)
}
}
@@ -269,6 +271,7 @@ private fun handleWallet2Action(action: OnboardingWallet2Action) {
val mediateResult = when (
val result = tangemSdkManager.importWallet(
scanResponse = scanResponse,
+ passphrase = action.passphrase,
mnemonic = action.mnemonicComponents.joinToString(" "),
shouldReset = globalState.onboardingState.shouldResetOnCreate,
)
@@ -484,6 +487,10 @@ private fun handleBackupAction(appState: () -> AppState?, action: BackupAction)
backupService.proceedBackup { result ->
when (result) {
is CompletionResult.Success -> {
+ val backupValidator = BackupValidator()
+ if (!backupValidator.isValid(CardDTO(result.data))) {
+ store.dispatchOnMain(BackupAction.ErrorInBackupCard)
+ }
if (backupService.currentState == BackupService.State.Finished) {
store.dispatchOnMain(BackupAction.FinishBackup())
} else {
diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletReducer.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletReducer.kt
index 61bee3be0f..1a400ea778 100644
--- a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletReducer.kt
+++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletReducer.kt
@@ -112,6 +112,7 @@ private object BackupReducer {
} else {
state.copy(backupStep = BackupStep.WriteBackupCard(action.cardNumber))
}
+ is BackupAction.ErrorInBackupCard -> state.copy(hasBackupError = true)
is BackupAction.SkipBackup -> state.copy(backupStep = BackupStep.Finished)
is BackupAction.FinishBackup -> state.copy(backupStep = BackupStep.Finished)
BackupAction.OnAccessCodeDialogClosed -> state.copy(backupStep = BackupStep.AddBackupCards)
diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletState.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletState.kt
index 0920fb4a9b..64b81fc771 100644
--- a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletState.kt
+++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletState.kt
@@ -70,6 +70,7 @@ data class BackupState(
val canSkipBackup: Boolean = true,
val isInterruptedBackup: Boolean = false,
val showBtnLoading: Boolean = false,
+ val hasBackupError: Boolean = false,
)
enum class AccessCodeError {
diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/OnboardingWalletFragment.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/OnboardingWalletFragment.kt
index f7e6ab244e..5c7b1e8589 100644
--- a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/OnboardingWalletFragment.kt
+++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/OnboardingWalletFragment.kt
@@ -524,10 +524,18 @@ class OnboardingWalletFragment :
override fun importWallet(
mnemonicComponents: List,
+ passphrase: String?,
seedPhraseSource: SeedPhraseSource,
callback: (CompletionResult) -> Unit,
) {
- store.dispatch(OnboardingWallet2Action.ImportWallet(mnemonicComponents, seedPhraseSource, callback))
+ store.dispatch(
+ OnboardingWallet2Action.ImportWallet(
+ mnemonicComponents,
+ passphrase,
+ seedPhraseSource,
+ callback,
+ ),
+ )
}
override fun allowScreenshots(allow: Boolean) {
diff --git a/app/src/main/java/com/tangem/tap/features/saveWallet/redux/SaveWalletAction.kt b/app/src/main/java/com/tangem/tap/features/saveWallet/redux/SaveWalletAction.kt
index b76bdfcac5..ff18f45362 100644
--- a/app/src/main/java/com/tangem/tap/features/saveWallet/redux/SaveWalletAction.kt
+++ b/app/src/main/java/com/tangem/tap/features/saveWallet/redux/SaveWalletAction.kt
@@ -28,5 +28,5 @@ internal sealed interface SaveWalletAction : Action {
data object SaveWalletWasShown : SaveWalletAction
- data object SaveWalletAfterBackup : SaveWalletAction
+ data class SaveWalletAfterBackup(val hasBackupError: Boolean) : SaveWalletAction
}
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/features/saveWallet/redux/SaveWalletMiddleware.kt b/app/src/main/java/com/tangem/tap/features/saveWallet/redux/SaveWalletMiddleware.kt
index 9e327ce3e5..5f110e31a9 100644
--- a/app/src/main/java/com/tangem/tap/features/saveWallet/redux/SaveWalletMiddleware.kt
+++ b/app/src/main/java/com/tangem/tap/features/saveWallet/redux/SaveWalletMiddleware.kt
@@ -51,7 +51,7 @@ internal class SaveWalletMiddleware {
is SaveWalletAction.EnrollBiometrics.Enroll -> enrollBiometrics()
is SaveWalletAction.SaveWalletWasShown -> saveWalletWasShown()
is SaveWalletAction.Dismiss -> dismiss(state)
- is SaveWalletAction.SaveWalletAfterBackup -> saveWalletAfterBackup(state)
+ is SaveWalletAction.SaveWalletAfterBackup -> saveWalletAfterBackup(state, action.hasBackupError)
is SaveWalletAction.Save.Success,
is SaveWalletAction.ProvideBackupInfo,
is SaveWalletAction.CloseError,
@@ -62,12 +62,13 @@ internal class SaveWalletMiddleware {
}
}
- private fun saveWalletAfterBackup(state: SaveWalletState) {
+ private fun saveWalletAfterBackup(state: SaveWalletState, hasBackupError: Boolean) {
scope.launch {
val backupInfo = state.backupInfo ?: error("Backup info is null")
val userWallet = UserWalletBuilder(backupInfo.scanResponse)
.backupCardsIds(state.backupInfo.backupCardsIds)
+ .hasBackupError(hasBackupError)
.build()
.guard {
Timber.e("User wallet not created")
diff --git a/app/src/main/java/com/tangem/tap/features/send/ui/SendFragment.kt b/app/src/main/java/com/tangem/tap/features/send/ui/SendFragment.kt
index 775a5cc578..0402c5e2de 100644
--- a/app/src/main/java/com/tangem/tap/features/send/ui/SendFragment.kt
+++ b/app/src/main/java/com/tangem/tap/features/send/ui/SendFragment.kt
@@ -25,10 +25,14 @@ import com.tangem.core.analytics.Analytics
import com.tangem.core.navigation.AppScreen
import com.tangem.core.navigation.NavigationAction
import com.tangem.domain.appcurrency.model.AppCurrency
+import com.tangem.domain.qrscanning.models.QrResult
+import com.tangem.domain.qrscanning.models.SourceType
+import com.tangem.domain.qrscanning.usecases.ListenToQrScanningUseCase
+import com.tangem.domain.qrscanning.usecases.ParseQrCodeUseCase
import com.tangem.domain.tokens.legacy.TradeCryptoAction
+import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.feature.qrscanning.QrScanningRouter
-import com.tangem.feature.qrscanning.SourceType
-import com.tangem.feature.qrscanning.usecase.ListenToQrScanningUseCase
+import com.tangem.features.send.api.navigation.SendRouter.Companion.CRYPTO_CURRENCY_KEY
import com.tangem.sdk.extensions.hideSoftKeyboard
import com.tangem.tap.common.KeyboardObserver
import com.tangem.tap.common.analytics.events.Token
@@ -60,6 +64,7 @@ import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
+import timber.log.Timber
import java.text.DecimalFormatSymbols
import javax.inject.Inject
@@ -82,11 +87,17 @@ class SendFragment : BaseStoreFragment(R.layout.fragment_send) {
private val sendSubscriber = SendStateSubscriber(this)
private lateinit var keyboardObserver: KeyboardObserver
+ private val cryptoCurrency: CryptoCurrency?
+ get() = arguments?.getParcelable(CRYPTO_CURRENCY_KEY)
+
val binding: FragmentSendBinding by viewBinding(FragmentSendBinding::bind)
@Inject
lateinit var listenToQrScanningUseCase: ListenToQrScanningUseCase
+ @Inject
+ lateinit var parseQrCodeUseCase: ParseQrCodeUseCase
+
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
lifecycle.addObserver(viewModel)
@@ -177,13 +188,21 @@ class SendFragment : BaseStoreFragment(R.layout.fragment_send) {
listenToQrScanningUseCase(SourceType.SEND)
.getOrElse { emptyFlow() }
.flowWithLifecycle(this@SendFragment.lifecycle, minActiveState = Lifecycle.State.CREATED)
- .collect {
+ .collect { rawQr ->
delay(200)
// Delayed launch is needed in order for the UI to be drawn and to process the sent events.
// If do not use the delay, then etAmount error field is not displayed when
// inserting an incorrect amount by shareUri
- onCodeScanned(it)
+ cryptoCurrency?.let { cryptoCurrency ->
+ parseQrCodeUseCase(rawQr, cryptoCurrency = cryptoCurrency).fold(
+ ifLeft = {
+ onCodeScanned(QrResult(address = rawQr))
+ Timber.w(it)
+ },
+ ifRight = { onCodeScanned(it) },
+ )
+ } ?: onCodeScanned(QrResult(address = rawQr))
}
}
}
@@ -254,15 +273,18 @@ class SendFragment : BaseStoreFragment(R.layout.fragment_send) {
.launchIn(mainScope)
}
- private fun onCodeScanned(scannedCode: String) {
- if (scannedCode.isEmpty()) return
+ private fun onCodeScanned(parsedQr: QrResult) {
+ if (parsedQr.address.isEmpty()) return
store.dispatch(
PasteAddress(
- data = scannedCode,
+ data = parsedQr.address,
sourceType = Token.Send.AddressEntered.SourceType.QRCode,
),
)
+ parsedQr.amount?.let { amount ->
+ store.dispatchOnMain(AmountAction.SetAmount(amount, isUserInput = false))
+ }
store.dispatch(TruncateOrRestore(!binding.lSendAddress.etAddress.isFocused))
}
diff --git a/app/src/main/java/com/tangem/tap/features/send/ui/dialogs/KaspaWarningDialog.kt b/app/src/main/java/com/tangem/tap/features/send/ui/dialogs/KaspaWarningDialog.kt
index 5e9f50747f..a86e233c8b 100644
--- a/app/src/main/java/com/tangem/tap/features/send/ui/dialogs/KaspaWarningDialog.kt
+++ b/app/src/main/java/com/tangem/tap/features/send/ui/dialogs/KaspaWarningDialog.kt
@@ -2,17 +2,20 @@ package com.tangem.tap.features.send.ui.dialogs
import android.content.Context
import androidx.appcompat.app.AlertDialog
+import com.tangem.blockchain.common.Blockchain
import com.tangem.tap.features.send.redux.SendAction
import com.tangem.tap.store
import com.tangem.wallet.R
object KaspaWarningDialog {
+
fun create(context: Context, dialog: SendAction.Dialog.KaspaWarningDialog): AlertDialog {
return AlertDialog.Builder(context).apply {
setTitle(R.string.common_warning)
setMessage(
context.getString(
- R.string.kaspa_withdrawal_message_warning,
+ R.string.common_utxo_validate_withdrawal_message_warning,
+ Blockchain.Kaspa.fullName,
dialog.maxOutputs,
dialog.maxAmount.toPlainString(),
),
@@ -23,7 +26,6 @@ object KaspaWarningDialog {
setOnDismissListener {
store.dispatch(SendAction.Dialog.Hide)
}
- }
- .create()
+ }.create()
}
}
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/features/shop/data/DefaultShopRepository.kt b/app/src/main/java/com/tangem/tap/features/shop/data/DefaultShopRepository.kt
deleted file mode 100644
index 1abbd3d17c..0000000000
--- a/app/src/main/java/com/tangem/tap/features/shop/data/DefaultShopRepository.kt
+++ /dev/null
@@ -1,59 +0,0 @@
-package com.tangem.tap.features.shop.data
-
-import com.tangem.datasource.api.tangemTech.TangemTechApi
-import com.tangem.datasource.api.tangemTech.models.ShopResponse
-import com.tangem.domain.common.extensions.withIOContext
-import com.tangem.tap.features.shop.domain.ShopRepository
-import com.tangem.tap.features.shop.domain.models.SalesProduct
-import com.tangem.utils.coroutines.CoroutineDispatcherProvider
-import com.tangem.utils.coroutines.runCatching
-import timber.log.Timber
-import java.util.Locale
-
-/**
- * Default implementation of shop feature repository
- *
- * @property tangemTechApi TangemTech API
- * @property dispatchers coroutine dispatchers provider
- *
-[REDACTED_AUTHOR]
- */
-internal class DefaultShopRepository(
- private val tangemTechApi: TangemTechApi,
- private val dispatchers: CoroutineDispatcherProvider,
-) : ShopRepository {
-
- private val salesProductConverter = SalesProductConverter()
-
- override suspend fun isShopifyOrderingAvailable(): Boolean {
- return runCatching(dispatchers.io) { tangemTechApi.getShopInfo(name = SHOPIFY_NAME) }
- .fold(
- onSuccess = ShopResponse::isOrderingAvailable,
- onFailure = {
- Timber.e("Server error. isShopifyOrderingAvailable returns default value (true)")
- true
- },
- )
- }
-
- override suspend fun getSalesProductInfo(): List {
- return withIOContext {
- val salesInfo = tangemTechApi.getSalesInfo(locale = getLocaleName(), shops = SHOPIFY_NAME)
- salesProductConverter.convert(salesInfo)
- }
- }
-
- private fun getLocaleName(): String {
- return if (Locale.getDefault().language == "ru") {
- RU_LOCALE
- } else {
- EN_LOCALE
- }
- }
-
- private companion object {
- private const val SHOPIFY_NAME = "shopify"
- private const val RU_LOCALE = "ru"
- private const val EN_LOCALE = "en"
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/features/shop/data/SalesProductConverter.kt b/app/src/main/java/com/tangem/tap/features/shop/data/SalesProductConverter.kt
deleted file mode 100644
index 9d8f440da6..0000000000
--- a/app/src/main/java/com/tangem/tap/features/shop/data/SalesProductConverter.kt
+++ /dev/null
@@ -1,40 +0,0 @@
-package com.tangem.tap.features.shop.data
-
-import com.tangem.datasource.api.tangemTech.models.SalesResponse
-import com.tangem.tap.features.shop.domain.models.Notification
-import com.tangem.tap.features.shop.domain.models.ProductState
-import com.tangem.tap.features.shop.domain.models.ProductType
-import com.tangem.tap.features.shop.domain.models.SalesProduct
-import com.tangem.utils.converter.Converter
-
-internal class SalesProductConverter : Converter> {
-
- override fun convert(value: SalesResponse): List {
- return value.sales.map { sales ->
- val productState = when (sales.state) {
- "order" -> ProductState.ORDER
- "pre-order" -> ProductState.PRE_ORDER
- "sold-out" -> ProductState.SOLD_OUT
- else -> ProductState.SOLD_OUT
- }
- val productType = when (sales.product.code) {
- "pack2" -> ProductType.WALLET_2_CARDS
- "pack3" -> ProductType.WALLET_3_CARDS
- else -> ProductType.WALLET_3_CARDS
- }
- SalesProduct(
- id = sales.id,
- productType = productType,
- state = productState,
- name = sales.product.name,
- notification = sales.notification?.let { notification ->
- Notification(
- type = notification.type,
- title = notification.title,
- description = notification.description,
- )
- },
- )
- }
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/features/shop/di/ShopUseCaseModule.kt b/app/src/main/java/com/tangem/tap/features/shop/di/ShopUseCaseModule.kt
deleted file mode 100644
index a6f22bec79..0000000000
--- a/app/src/main/java/com/tangem/tap/features/shop/di/ShopUseCaseModule.kt
+++ /dev/null
@@ -1,44 +0,0 @@
-package com.tangem.tap.features.shop.di
-
-import com.tangem.datasource.api.tangemTech.TangemTechApi
-import com.tangem.tap.features.shop.data.DefaultShopRepository
-import com.tangem.tap.features.shop.domain.DefaultShopifyOrderingAvailabilityUseCase
-import com.tangem.tap.features.shop.domain.GetShopifySalesProductsUseCase
-import com.tangem.tap.features.shop.domain.ShopRepository
-import com.tangem.tap.features.shop.domain.ShopifyOrderingAvailabilityUseCase
-import com.tangem.utils.coroutines.CoroutineDispatcherProvider
-import dagger.Module
-import dagger.Provides
-import dagger.hilt.InstallIn
-import dagger.hilt.android.components.ViewModelComponent
-import dagger.hilt.android.scopes.ViewModelScoped
-
-@Module
-@InstallIn(ViewModelComponent::class)
-internal object ShopUseCaseModule {
-
- @Provides
- @ViewModelScoped
- fun provideShopifyOrderingAvailabilityUseCase(shopRepository: ShopRepository): ShopifyOrderingAvailabilityUseCase {
- return DefaultShopifyOrderingAvailabilityUseCase(
- shopRepository = shopRepository,
- )
- }
-
- @Provides
- @ViewModelScoped
- fun provideGetShopifySalesProductsUseCase(shopRepository: ShopRepository): GetShopifySalesProductsUseCase {
- return GetShopifySalesProductsUseCase(
- shopRepository = shopRepository,
- )
- }
-
- @Provides
- @ViewModelScoped
- fun provideDefaultShopRepository(
- tangemTechApi: TangemTechApi,
- dispatchers: CoroutineDispatcherProvider,
- ): ShopRepository {
- return DefaultShopRepository(tangemTechApi = tangemTechApi, dispatchers = dispatchers)
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/features/shop/di/ShopifyTogglesModule.kt b/app/src/main/java/com/tangem/tap/features/shop/di/ShopifyTogglesModule.kt
deleted file mode 100644
index 97b5839f47..0000000000
--- a/app/src/main/java/com/tangem/tap/features/shop/di/ShopifyTogglesModule.kt
+++ /dev/null
@@ -1,23 +0,0 @@
-package com.tangem.tap.features.shop.di
-
-import com.tangem.core.featuretoggle.manager.FeatureTogglesManager
-import com.tangem.tap.features.shop.toggles.DefaultShopifyFeatureToggleManager
-import com.tangem.tap.features.shop.toggles.ShopifyFeatureToggleManager
-import dagger.Module
-import dagger.Provides
-import dagger.hilt.InstallIn
-import dagger.hilt.components.SingletonComponent
-import javax.inject.Singleton
-
-@Module
-@InstallIn(SingletonComponent::class)
-internal object ShopifyTogglesModule {
-
- @Provides
- @Singleton
- fun provideDefaultShopifyFeatureToggleManager(
- featureToggleManager: FeatureTogglesManager,
- ): ShopifyFeatureToggleManager {
- return DefaultShopifyFeatureToggleManager(featureToggleManager)
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/features/shop/domain/DefaultShopifyOrderingAvailabilityUseCase.kt b/app/src/main/java/com/tangem/tap/features/shop/domain/DefaultShopifyOrderingAvailabilityUseCase.kt
deleted file mode 100644
index 41bbe3d449..0000000000
--- a/app/src/main/java/com/tangem/tap/features/shop/domain/DefaultShopifyOrderingAvailabilityUseCase.kt
+++ /dev/null
@@ -1,15 +0,0 @@
-package com.tangem.tap.features.shop.domain
-
-/**
- * Default implementation of use case to define shopify ordering availability
- *
- * @property shopRepository shop feature repository
- *
-[REDACTED_AUTHOR]
- */
-internal class DefaultShopifyOrderingAvailabilityUseCase(
- private val shopRepository: ShopRepository,
-) : ShopifyOrderingAvailabilityUseCase {
-
- override suspend fun invoke() = shopRepository.isShopifyOrderingAvailable()
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/features/shop/domain/GetShopifySalesProductsUseCase.kt b/app/src/main/java/com/tangem/tap/features/shop/domain/GetShopifySalesProductsUseCase.kt
deleted file mode 100644
index 7881b82dea..0000000000
--- a/app/src/main/java/com/tangem/tap/features/shop/domain/GetShopifySalesProductsUseCase.kt
+++ /dev/null
@@ -1,25 +0,0 @@
-package com.tangem.tap.features.shop.domain
-
-import arrow.core.Either
-import arrow.core.left
-import arrow.core.right
-import com.tangem.tap.features.shop.domain.models.SalesError
-import com.tangem.tap.features.shop.domain.models.SalesProduct
-
-/**
- * Use case to get shopify available products
- *
- * @property shopRepository shop feature repository
- */
-class GetShopifySalesProductsUseCase(
- private val shopRepository: ShopRepository,
-) {
-
- suspend operator fun invoke(): Either> {
- return try {
- shopRepository.getSalesProductInfo().right()
- } catch (e: Exception) {
- SalesError.DataError(e).left()
- }
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/features/shop/domain/ShopRepository.kt b/app/src/main/java/com/tangem/tap/features/shop/domain/ShopRepository.kt
deleted file mode 100644
index 67803630b8..0000000000
--- a/app/src/main/java/com/tangem/tap/features/shop/domain/ShopRepository.kt
+++ /dev/null
@@ -1,17 +0,0 @@
-package com.tangem.tap.features.shop.domain
-
-import com.tangem.tap.features.shop.domain.models.SalesProduct
-
-/**
- * Shop feature repository
- *
-[REDACTED_AUTHOR]
- */
-interface ShopRepository {
-
- /** Get shopify ordering availability */
- suspend fun isShopifyOrderingAvailable(): Boolean
-
- /** Get actual sales product info */
- suspend fun getSalesProductInfo(): List
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/features/shop/domain/ShopifyOrderingAvailabilityUseCase.kt b/app/src/main/java/com/tangem/tap/features/shop/domain/ShopifyOrderingAvailabilityUseCase.kt
deleted file mode 100644
index 5f6c351ce2..0000000000
--- a/app/src/main/java/com/tangem/tap/features/shop/domain/ShopifyOrderingAvailabilityUseCase.kt
+++ /dev/null
@@ -1,12 +0,0 @@
-package com.tangem.tap.features.shop.domain
-
-/**
- * Use case to define shopify ordering availability
- *
-[REDACTED_AUTHOR]
- */
-internal interface ShopifyOrderingAvailabilityUseCase {
-
- /** Get availability */
- suspend operator fun invoke(): Boolean
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/features/shop/domain/models/ProductType.kt b/app/src/main/java/com/tangem/tap/features/shop/domain/models/ProductType.kt
deleted file mode 100644
index 0987fdd8bd..0000000000
--- a/app/src/main/java/com/tangem/tap/features/shop/domain/models/ProductType.kt
+++ /dev/null
@@ -1,22 +0,0 @@
-package com.tangem.tap.features.shop.domain.models
-
-private const val TANGEM_WALLET_2_CARDS_SKU = "TG115X2-S"
-private const val TANGEM_WALLET_3_CARDS_SKU = "TG115X3-S"
-
-enum class ProductType(val sku: String) {
-
- WALLET_2_CARDS(TANGEM_WALLET_2_CARDS_SKU),
- WALLET_3_CARDS(TANGEM_WALLET_3_CARDS_SKU),
- ;
-
- companion object {
- val SKUS_TO_DISPLAY = listOf(TANGEM_WALLET_2_CARDS_SKU, TANGEM_WALLET_3_CARDS_SKU)
- fun fromSku(sku: String): ProductType? {
- return when (sku) {
- WALLET_2_CARDS.sku -> WALLET_2_CARDS
- WALLET_3_CARDS.sku -> WALLET_3_CARDS
- else -> null
- }
- }
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/features/shop/domain/models/SalesError.kt b/app/src/main/java/com/tangem/tap/features/shop/domain/models/SalesError.kt
deleted file mode 100644
index 373b8d14c8..0000000000
--- a/app/src/main/java/com/tangem/tap/features/shop/domain/models/SalesError.kt
+++ /dev/null
@@ -1,5 +0,0 @@
-package com.tangem.tap.features.shop.domain.models
-
-sealed class SalesError {
- data class DataError(val cause: Throwable) : SalesError()
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/features/shop/domain/models/SalesProduct.kt b/app/src/main/java/com/tangem/tap/features/shop/domain/models/SalesProduct.kt
deleted file mode 100644
index b3b5a083d3..0000000000
--- a/app/src/main/java/com/tangem/tap/features/shop/domain/models/SalesProduct.kt
+++ /dev/null
@@ -1,30 +0,0 @@
-package com.tangem.tap.features.shop.domain.models
-
-/**
- * Sales product
- *
- * @property id product id
- * @property productType shows TW2 cards or 3 cards
- * @property state state as order available etc
- * @property name product name
- * @property notification optional notification
- */
-data class SalesProduct(
- val id: String,
- val productType: ProductType,
- val state: ProductState,
- val name: String,
- val notification: Notification?,
-)
-
-data class Notification(
- val type: String,
- val title: String,
- val description: String,
-)
-
-enum class ProductState {
- ORDER,
- SOLD_OUT,
- PRE_ORDER,
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/features/shop/presentation/ShopViewModel.kt b/app/src/main/java/com/tangem/tap/features/shop/presentation/ShopViewModel.kt
deleted file mode 100644
index 2e39e2bd4f..0000000000
--- a/app/src/main/java/com/tangem/tap/features/shop/presentation/ShopViewModel.kt
+++ /dev/null
@@ -1,60 +0,0 @@
-package com.tangem.tap.features.shop.presentation
-
-import androidx.lifecycle.ViewModel
-import androidx.lifecycle.viewModelScope
-import com.tangem.tap.features.shop.domain.GetShopifySalesProductsUseCase
-import com.tangem.tap.features.shop.domain.ShopifyOrderingAvailabilityUseCase
-import com.tangem.tap.features.shop.redux.ShopAction
-import com.tangem.tap.proxy.AppStateHolder
-import com.tangem.utils.coroutines.CoroutineDispatcherProvider
-import com.tangem.utils.coroutines.runCatching
-import dagger.hilt.android.lifecycle.HiltViewModel
-import kotlinx.coroutines.launch
-import javax.inject.Inject
-
-/**
- * Shop screen view model
- *
- * @property shopifyOrderingAvailabilityUseCase use case to define shopify ordering availability
- * @property getShopifySalesProductsUseCase use case to get actual sales info
- * @property dispatchers coroutine dispatchers provider
- * @property appStateHolder redux state holder
- *
-[REDACTED_AUTHOR]
- */
-@HiltViewModel
-internal class ShopViewModel @Inject constructor(
- private val shopifyOrderingAvailabilityUseCase: ShopifyOrderingAvailabilityUseCase,
- private val getShopifySalesProductsUseCase: GetShopifySalesProductsUseCase,
- private val dispatchers: CoroutineDispatcherProvider,
- private val appStateHolder: AppStateHolder,
-) : ViewModel() {
-
- /** Check ordering delay block visibility */
- fun checkOrderingDelayBlockVisibility() {
- viewModelScope.launch(dispatchers.main) {
- val visibility = runCatching(dispatchers.io) { shopifyOrderingAvailabilityUseCase() }
- .fold(onSuccess = { !it }, onFailure = { false })
-
- appStateHolder.mainStore?.dispatch(action = ShopAction.SetOrderingDelayBlockVisibility(visibility))
- }
- }
-
- /**
- * Get actual sales products info
- * to configure view dynamically
- */
- fun getActualSalesInfo() {
- viewModelScope.launch(dispatchers.main) {
- val action = getShopifySalesProductsUseCase().fold(
- ifLeft = {
- ShopAction.SalesProductsError
- },
- ifRight = {
- ShopAction.SalesProductsLoaded(it)
- },
- )
- appStateHolder.mainStore?.dispatch(action)
- }
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/features/shop/redux/ShopAction.kt b/app/src/main/java/com/tangem/tap/features/shop/redux/ShopAction.kt
deleted file mode 100644
index a72324e845..0000000000
--- a/app/src/main/java/com/tangem/tap/features/shop/redux/ShopAction.kt
+++ /dev/null
@@ -1,52 +0,0 @@
-package com.tangem.tap.features.shop.redux
-
-import android.content.Intent
-import com.tangem.tap.common.redux.NotificationAction
-import com.tangem.tap.features.shop.domain.models.ProductType
-import com.tangem.tap.common.shop.data.TangemProduct
-import com.tangem.tap.common.shop.googlepay.GooglePayService
-import com.tangem.tap.features.shop.domain.models.SalesProduct
-import com.tangem.wallet.R
-import org.rekotlin.Action
-
-sealed interface ShopAction : Action {
-
- object LoadProducts : ShopAction {
- data class Success(val products: List) : ShopAction
- object Failure : ShopAction, NotificationAction {
- override val messageResource = R.string.common_server_unavailable
- }
- }
-
- data class ApplyPromoCode(val promoCode: String) : ShopAction {
- data class Success(val promoCode: String?, val products: List) : ShopAction
- object InvalidPromoCode : ShopAction
- }
-
- object BuyWithGooglePay : ShopAction {
- object UserCancelled : ShopAction
- data class HandleGooglePayResponse(val resultCode: Int, val data: Intent?) : ShopAction
-
- data class Failure(val exception: Throwable) : ShopAction
- object Success : ShopAction
- }
-
- object StartWebCheckout : ShopAction
-
- data class CheckIfGooglePayAvailable(val googlePayService: GooglePayService) : ShopAction {
- object Success : ShopAction
- object Failure : ShopAction
- }
-
- data class SelectProduct(val productType: ProductType) : ShopAction
-
- object FinishSuccessfulOrder : ShopAction
-
- object ResetState : ShopAction
-
- data class SetOrderingDelayBlockVisibility(val visibility: Boolean) : ShopAction
-
- data class SalesProductsLoaded(val salesProducts: List) : ShopAction
-
- object SalesProductsError : ShopAction
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/features/shop/redux/ShopMiddleware.kt b/app/src/main/java/com/tangem/tap/features/shop/redux/ShopMiddleware.kt
deleted file mode 100644
index f049ef01b6..0000000000
--- a/app/src/main/java/com/tangem/tap/features/shop/redux/ShopMiddleware.kt
+++ /dev/null
@@ -1,128 +0,0 @@
-package com.tangem.tap.features.shop.redux
-
-import com.tangem.core.analytics.Analytics
-import com.tangem.core.navigation.AppScreen
-import com.tangem.core.navigation.NavigationAction
-import com.tangem.tap.common.analytics.events.Shop
-import com.tangem.tap.common.extensions.dispatchOnMain
-import com.tangem.tap.common.extensions.dispatchOpenUrl
-import com.tangem.tap.common.redux.AppState
-import com.tangem.tap.scope
-import com.tangem.tap.shopService
-import com.tangem.tap.store
-import kotlinx.coroutines.launch
-import org.rekotlin.Action
-import org.rekotlin.Middleware
-import timber.log.Timber
-
-class ShopMiddleware {
-
- val shopMiddleware: Middleware = { dispatch, state ->
- { next ->
- { action ->
- handle(action)
- next(action)
- }
- }
- }
-}
-
-@Suppress("LongMethod", "ComplexMethod")
-private fun handle(action: Action) {
- val shopState = store.state.shopState
-
- if (action is NavigationAction.NavigateTo && action.screen == AppScreen.Shop) {
- store.dispatch(ShopAction.LoadProducts)
- }
-
- if (action !is ShopAction) return
-
- when (action) {
- is ShopAction.ApplyPromoCode -> {
- scope.launch {
- if (action.promoCode.isBlank() && shopState.promoCode == null) {
- store.dispatchOnMain(ShopAction.ApplyPromoCode.InvalidPromoCode)
- return@launch
- }
-
- val result = shopService.applyPromoCode(action.promoCode)
-
- result.onSuccess { products ->
- store.dispatchOnMain(
- ShopAction.ApplyPromoCode.Success(
- promoCode = products.first { it.type == shopState.selectedProduct }.appliedDiscount,
- products = products,
- ),
- )
- }
- result.onFailure { store.dispatchOnMain(ShopAction.ApplyPromoCode.InvalidPromoCode) }
- }
- }
- ShopAction.BuyWithGooglePay -> {
- shopService.buyWithGooglePay(shopState.selectedProduct)
-// shopService.subscribeToGooglePayResult(productType = shopState.selectedProduct) { result ->
-// result.onSuccess {
-// store.dispatch(ShopAction.BuyWithGooglePay.Success)
-// }
-// result.onFailure { error ->
-// if (error is TangemSdkError.UserCancelled) {
-// store.dispatch(ShopAction.BuyWithGooglePay.UserCancelled)
-// } else {
-// store.dispatch(ShopAction.BuyWithGooglePay.Failure(error))
-// }
-// }
-// }
- }
- is ShopAction.BuyWithGooglePay.HandleGooglePayResponse -> {
- scope.launch {
- val result = shopService.handleGooglePayResult(
- action.resultCode,
- action.data,
- shopState.selectedProduct,
- )
- result.onSuccess {
- store.dispatchOnMain(ShopAction.BuyWithGooglePay.Success)
- }
- result.onFailure {
- store.dispatchOnMain(ShopAction.BuyWithGooglePay.Failure(it))
- }
- }
- }
- ShopAction.LoadProducts -> {
- scope.launch {
- shopService.getProducts().fold(
- onSuccess = { store.dispatchOnMain(ShopAction.LoadProducts.Success(it)) },
- onFailure = {
- Timber.e(it)
- store.dispatchOnMain(ShopAction.LoadProducts.Failure)
- },
- )
- }
- }
- is ShopAction.CheckIfGooglePayAvailable -> {
- scope.launch {
- val isAvailable =
- shopService.checkIfGooglePayAvailable(action.googlePayService).getOrNull()
- ?: false
- val newAction = if (isAvailable) {
- ShopAction.CheckIfGooglePayAvailable.Success
- } else {
- ShopAction.CheckIfGooglePayAvailable.Failure
- }
- store.dispatchOnMain(newAction)
- }
- }
- ShopAction.StartWebCheckout -> {
- Analytics.send(Shop.Redirected(null))
- store.dispatchOpenUrl(shopService.getCheckoutUrl(shopState.selectedProduct))
- store.dispatch(ShopAction.FinishSuccessfulOrder)
- }
-
- is ShopAction.FinishSuccessfulOrder -> {
- scope.launch {
- shopService.waitForCheckout(shopState.selectedProduct)
- }
- }
- else -> {}
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/features/shop/redux/ShopReducer.kt b/app/src/main/java/com/tangem/tap/features/shop/redux/ShopReducer.kt
deleted file mode 100644
index 11061c7abf..0000000000
--- a/app/src/main/java/com/tangem/tap/features/shop/redux/ShopReducer.kt
+++ /dev/null
@@ -1,49 +0,0 @@
-package com.tangem.tap.features.shop.redux
-
-import org.rekotlin.Action
-
-object ShopReducer {
- fun reduce(action: Action, state: ShopState): ShopState = internalReduce(action, state)
-}
-
-private fun internalReduce(action: Action, state: ShopState): ShopState {
- if (action !is ShopAction) return state
-
- return when (action) {
- is ShopAction.ApplyPromoCode -> state.copy(promoCode = action.promoCode, promoCodeLoading = true)
- is ShopAction.LoadProducts.Success -> state.copy(availableProducts = action.products)
- is ShopAction.ApplyPromoCode.InvalidPromoCode -> state.copy(promoCode = null, promoCodeLoading = false)
- is ShopAction.ApplyPromoCode.Success -> {
- state.copy(
- promoCode = action.promoCode,
- availableProducts = action.products,
- promoCodeLoading = false,
- )
- }
- is ShopAction.SelectProduct -> state.copy(selectedProduct = action.productType)
-
- // TODO: change when we add support for GPay
- is ShopAction.CheckIfGooglePayAvailable.Failure -> state.copy(isGooglePayAvailable = false)
- is ShopAction.CheckIfGooglePayAvailable.Success -> state.copy(isGooglePayAvailable = false)
-
- is ShopAction.ResetState -> ShopState()
- is ShopAction.SetOrderingDelayBlockVisibility -> state.copy(isOrderingDelayBlockVisible = action.visibility)
- is ShopAction.BuyWithGooglePay,
- is ShopAction.LoadProducts,
- is ShopAction.StartWebCheckout,
- is ShopAction.CheckIfGooglePayAvailable,
- is ShopAction.BuyWithGooglePay.Failure,
- is ShopAction.BuyWithGooglePay.HandleGooglePayResponse,
- is ShopAction.BuyWithGooglePay.Success,
- is ShopAction.BuyWithGooglePay.UserCancelled,
- is ShopAction.FinishSuccessfulOrder,
- is ShopAction.LoadProducts.Failure,
- -> state
- is ShopAction.SalesProductsLoaded -> state.copy(
- salesProducts = action.salesProducts,
- )
- is ShopAction.SalesProductsError -> state.copy(
- salesProducts = emptyList(),
- )
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/features/shop/redux/ShopState.kt b/app/src/main/java/com/tangem/tap/features/shop/redux/ShopState.kt
deleted file mode 100644
index 08bac48f4d..0000000000
--- a/app/src/main/java/com/tangem/tap/features/shop/redux/ShopState.kt
+++ /dev/null
@@ -1,28 +0,0 @@
-package com.tangem.tap.features.shop.redux
-
-import com.tangem.tap.features.shop.domain.models.ProductType
-import com.tangem.tap.common.shop.data.TangemProduct
-import com.tangem.tap.features.shop.domain.models.SalesProduct
-import org.rekotlin.StateType
-
-data class ShopState(
- val availableProducts: List = emptyList(),
- val selectedProduct: ProductType = ProductType.WALLET_3_CARDS,
- val salesProducts: List = emptyList(),
- val promoCode: String? = null,
- val promoCodeLoading: Boolean = false,
- val isGooglePayAvailable: Boolean = false, // TODO: change when we add support for GPay
- val isOrderingDelayBlockVisible: Boolean = false,
-) : StateType {
- val total: String?
- get() = availableProducts.firstOrNull { it.type == selectedProduct }?.totalSum?.finalValue
-
- val priceBeforeDiscount: String?
- get() {
- val totalSum = availableProducts.firstOrNull { it.type == selectedProduct }?.totalSum
- if (totalSum?.finalValue != totalSum?.beforeDiscount) {
- return totalSum?.beforeDiscount
- }
- return null
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/features/shop/toggles/DefaultShopifyFeatureToggleManager.kt b/app/src/main/java/com/tangem/tap/features/shop/toggles/DefaultShopifyFeatureToggleManager.kt
deleted file mode 100644
index eda902f707..0000000000
--- a/app/src/main/java/com/tangem/tap/features/shop/toggles/DefaultShopifyFeatureToggleManager.kt
+++ /dev/null
@@ -1,11 +0,0 @@
-package com.tangem.tap.features.shop.toggles
-
-import com.tangem.core.featuretoggle.manager.FeatureTogglesManager
-
-internal class DefaultShopifyFeatureToggleManager(
- private val featureTogglesManager: FeatureTogglesManager,
-) : ShopifyFeatureToggleManager {
-
- override val isDynamicSalesProductsEnabled: Boolean
- get() = featureTogglesManager.isFeatureEnabled("SHOPIFY_DYNAMIC_ENABLED")
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/features/shop/toggles/ShopifyFeatureToggleManager.kt b/app/src/main/java/com/tangem/tap/features/shop/toggles/ShopifyFeatureToggleManager.kt
deleted file mode 100644
index ab3b8fc968..0000000000
--- a/app/src/main/java/com/tangem/tap/features/shop/toggles/ShopifyFeatureToggleManager.kt
+++ /dev/null
@@ -1,10 +0,0 @@
-package com.tangem.tap.features.shop.toggles
-
-/**
- * Shopify feature toggle manager that provides info about shopify toggle availability
- *
- */
-interface ShopifyFeatureToggleManager {
-
- val isDynamicSalesProductsEnabled: Boolean
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/features/shop/ui/ShopFragment.kt b/app/src/main/java/com/tangem/tap/features/shop/ui/ShopFragment.kt
deleted file mode 100644
index bfb86a0f8a..0000000000
--- a/app/src/main/java/com/tangem/tap/features/shop/ui/ShopFragment.kt
+++ /dev/null
@@ -1,251 +0,0 @@
-package com.tangem.tap.features.shop.ui
-
-import android.animation.Animator
-import android.animation.AnimatorListenerAdapter
-import android.content.Context
-import android.os.Bundle
-import android.view.View
-import android.view.View.OnFocusChangeListener
-import android.view.inputmethod.EditorInfo
-import android.view.inputmethod.InputMethodManager
-import androidx.activity.OnBackPressedCallback
-import androidx.fragment.app.viewModels
-import by.kirich1409.viewbindingdelegate.viewBinding
-import com.tangem.core.navigation.NavigationAction
-import com.tangem.tap.common.GlobalLayoutStateHandler
-import com.tangem.tap.common.KeyboardObserver
-import com.tangem.tap.common.extensions.getQuantityString
-import com.tangem.tap.common.extensions.show
-import com.tangem.tap.features.BaseStoreFragment
-import com.tangem.tap.features.shop.domain.models.ProductState
-import com.tangem.tap.features.shop.domain.models.ProductType
-import com.tangem.tap.features.shop.domain.models.SalesProduct
-import com.tangem.tap.features.shop.presentation.ShopViewModel
-import com.tangem.tap.features.shop.redux.ShopAction
-import com.tangem.tap.features.shop.redux.ShopState
-import com.tangem.tap.features.shop.toggles.ShopifyFeatureToggleManager
-import com.tangem.tap.store
-import com.tangem.wallet.R
-import com.tangem.wallet.databinding.FragmentShopBinding
-import dagger.hilt.android.AndroidEntryPoint
-import org.rekotlin.StoreSubscriber
-import javax.inject.Inject
-
-@AndroidEntryPoint
-internal class ShopFragment : BaseStoreFragment(R.layout.fragment_shop), StoreSubscriber {
-
- @Inject
- lateinit var shopifyFeatureToggleManager: ShopifyFeatureToggleManager
-
- private val binding: FragmentShopBinding by viewBinding(FragmentShopBinding::bind)
- private var cardTranslationY = 70f
-
- private lateinit var keyboardObserver: KeyboardObserver
-
- private val viewModel by viewModels()
-
- override fun subscribeToStore() {
- store.subscribe(this) { state ->
- state.skipRepeats { oldState, newState ->
- oldState.shopState == newState.shopState
- }.select { it.shopState }
- }
- storeSubscribersList.add(this)
- }
-
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
-
- if (shopifyFeatureToggleManager.isDynamicSalesProductsEnabled) {
- viewModel.getActualSalesInfo()
- } else {
- viewModel.checkOrderingDelayBlockVisibility()
- }
-
- activity?.onBackPressedDispatcher?.addCallback(
- this,
- object : OnBackPressedCallback(true) {
- override fun handleOnBackPressed() {
- store.dispatch(NavigationAction.PopBackTo())
- store.dispatch(ShopAction.ResetState)
- }
- },
- )
- }
-
- override fun onDestroyView() {
- super.onDestroyView()
- keyboardObserver.unregisterListener()
- }
-
- override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
- super.onViewCreated(view, savedInstanceState)
-
- setupCardsImages()
- setupProductSelection()
- setupPromoCodeEditText()
-
- binding.toolbar.setNavigationOnClickListener {
- requireActivity().onBackPressed()
- }
-
- keyboardObserver = KeyboardObserver(requireActivity()).apply {
- registerListener { isVisible ->
- binding.flCards.show(!isVisible)
- }
- }
- }
-
- @Suppress("MagicNumber")
- private fun setupCardsImages() {
- GlobalLayoutStateHandler(binding.imvSecond).apply {
- onStateChanged = {
- cardTranslationY = it.height * 0.15f
- binding.imvSecond.animate()
- .translationY(cardTranslationY)
- .scaleX(0.9f)
- .scaleY(0.9f)
- .start()
- binding.imvThird.animate()
- .translationY(cardTranslationY * 2)
- .scaleX(0.8f)
- .scaleY(0.8f)
- .start()
- detach()
- }
- }
- }
-
- private fun setupProductSelection() = with(binding) {
- chipProduct1.setOnCheckedChangeListener { _, isChecked ->
- if (isChecked) store.dispatch(ShopAction.SelectProduct(ProductType.WALLET_3_CARDS))
- }
- chipProduct2.setOnCheckedChangeListener { _, isChecked ->
- if (isChecked) store.dispatch(ShopAction.SelectProduct(ProductType.WALLET_2_CARDS))
- }
- chipProduct1.text = chipProduct1.getQuantityString(R.plurals.card_label_card_count, quantity = 3)
- chipProduct2.text = chipProduct2.getQuantityString(R.plurals.card_label_card_count, quantity = 2)
- }
-
- private fun setupPromoCodeEditText() = with(binding) {
- etPromoCode.setOnEditorActionListener { view, actionId, event ->
- if (actionId == EditorInfo.IME_ACTION_DONE) {
- val imm: InputMethodManager =
- requireContext().getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
- imm.hideSoftInputFromWindow(view.windowToken, 0)
- view.clearFocus()
- return@setOnEditorActionListener true
- }
- return@setOnEditorActionListener false
- }
-
- etPromoCode.onFocusChangeListener = OnFocusChangeListener { _, hasFocus ->
- if (!hasFocus) {
- store.dispatch(ShopAction.ApplyPromoCode(etPromoCode.text.toString()))
- }
- }
- }
-
- override fun newState(state: ShopState) {
- if (activity == null || view == null) return
-
- animateProductSelection(state.selectedProduct)
- handlePriceState(state)
- handlePromoCodeState(state)
- // TODO: https://tangem.slack.com/archives/C01HARKDLQ0/p1691421861756069
- // if (shopifyFeatureToggleManager.isDynamicSalesProductsEnabled) {
- // handleNotificationBlock(state)
- // } else {
- // handleOrderingDelayBlock(isVisible = state.isOrderingDelayBlockVisible)
- // }
- handleButtonsState(state)
- }
-
- private fun animateProductSelection(selectedProduct: ProductType) {
- val show = when (selectedProduct) {
- ProductType.WALLET_2_CARDS -> false
- ProductType.WALLET_3_CARDS -> true
- }
- showOrHideThirdCardWithAnimation(show)
- }
-
- private fun showOrHideThirdCardWithAnimation(show: Boolean) = with(binding) {
- val translationY = if (show) cardTranslationY * 2 else cardTranslationY
- if (show) imvThird.show()
- imvThird.animate()
- .translationY(translationY)
- .setListener(
- object : AnimatorListenerAdapter() {
- override fun onAnimationEnd(animation: Animator) {
- super.onAnimationEnd(animation)
- imvThird.show(show)
- }
- },
- )
- }
-
- private fun handlePriceState(state: ShopState) = with(binding) {
- tvTotal.text = state.total
- tvTotalBeforeDiscount.text = state.priceBeforeDiscount
-
- pbPrice.show(state.total == null)
- }
-
- private fun handlePromoCodeState(state: ShopState) = with(binding) {
- if (state.promoCode == null && !etPromoCode.hasFocus()) {
- etPromoCode.setText("")
- }
- pbPromoCode.show(state.promoCodeLoading)
- }
-
- // private fun handleOrderingDelayBlock(isVisible: Boolean) {
- // if (isVisible) binding.tvSoldOutDesc.show() else binding.tvSoldOutDesc.hide()
- // }
- //
- // private fun handleNotificationBlock(state: ShopState) {
- // if (isVisible) {
- // binding.tvSoldOutDesc.show()
- // getSelectedSalesProduct(state)?.notification?.let { notification ->
- // binding.tvSoldOutDesc.text = notification.description
- // }
- // } else {
- // binding.tvSoldOutDesc.hide()
- // }
- // }
-
- private fun handleButtonsState(state: ShopState) = with(binding) {
- btnPayGooglePay.root.show(state.isGooglePayAvailable)
- btnAlternativePayment.show(state.isGooglePayAvailable)
- btnMainAction.show(!state.isGooglePayAvailable)
-
- if (state.total != null) {
- btnAlternativePayment.setOnClickListener { store.dispatch(ShopAction.StartWebCheckout) }
- btnPayGooglePay.root.setOnClickListener { store.dispatch(ShopAction.BuyWithGooglePay) }
- btnMainAction.setOnClickListener { store.dispatch(ShopAction.StartWebCheckout) }
- if (state.salesProducts.isNotEmpty()) {
- getSelectedSalesProduct(state)?.let { selectedProduct ->
- btnMainAction.text = getMainBtnTextByProductState(
- productState = selectedProduct.state,
- )
- }
- }
- }
- }
-
- override fun handleOnBackPressed() {
- store.dispatch(ShopAction.ResetState)
- super.handleOnBackPressed()
- }
-
- private fun getSelectedSalesProduct(state: ShopState): SalesProduct? {
- return state.salesProducts.find {
- it.productType == state.selectedProduct
- }
- }
-
- private fun getMainBtnTextByProductState(productState: ProductState): String = when (productState) {
- ProductState.ORDER -> getString(R.string.shop_buy_now)
- ProductState.SOLD_OUT -> "Sold out" // getString(R.string.sold_out) // todo finalize in next PR
- ProductState.PRE_ORDER -> "Pre order" // getString(R.string.pre_order) // todo finalize in next PR
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/TradeCryptoMiddleware.kt b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/TradeCryptoMiddleware.kt
index 28a73436b5..aa15f116e8 100644
--- a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/TradeCryptoMiddleware.kt
+++ b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/TradeCryptoMiddleware.kt
@@ -87,8 +87,7 @@ object TradeCryptoMiddleware {
val exchangeManager = store.state.globalState.exchangeManager
val topUrl = exchangeManager.getUrl(
action = CurrencyExchangeManager.Action.Buy,
- blockchain = blockchain,
- cryptoCurrencyName = currency.symbol,
+ cryptoCurrency = currency,
fiatCurrencyName = action.appCurrencyCode,
walletAddress = networkAddress,
isDarkTheme = MutableAppThemeModeHolder.isDarkThemeActive,
@@ -140,8 +139,7 @@ object TradeCryptoMiddleware {
store.state.globalState.exchangeManager.getUrl(
action = CurrencyExchangeManager.Action.Sell,
- blockchain = Blockchain.fromId(currency.network.id.value),
- cryptoCurrencyName = currency.symbol,
+ cryptoCurrency = currency,
fiatCurrencyName = action.appCurrencyCode,
walletAddress = networkAddress,
isDarkTheme = MutableAppThemeModeHolder.isDarkThemeActive,
@@ -318,6 +316,7 @@ object TradeCryptoMiddleware {
SendRouter.TRANSACTION_ID_KEY to txInfo?.transactionId,
SendRouter.DESTINATION_ADDRESS_KEY to txInfo?.destinationAddress,
SendRouter.AMOUNT_KEY to txInfo?.amount,
+ SendRouter.TAG_KEY to txInfo?.tag,
)
store.dispatchOnMain(NavigationAction.NavigateTo(screen = AppScreen.Send, bundle = bundle))
}
diff --git a/app/src/main/java/com/tangem/tap/features/welcome/redux/WelcomeMiddleware.kt b/app/src/main/java/com/tangem/tap/features/welcome/redux/WelcomeMiddleware.kt
index 638776a6ed..dd239f1135 100644
--- a/app/src/main/java/com/tangem/tap/features/welcome/redux/WelcomeMiddleware.kt
+++ b/app/src/main/java/com/tangem/tap/features/welcome/redux/WelcomeMiddleware.kt
@@ -18,10 +18,7 @@ import com.tangem.domain.wallets.legacy.UserWalletsListManager.Lockable.UnlockTy
import com.tangem.domain.wallets.legacy.unlockIfLockable
import com.tangem.tap.*
import com.tangem.tap.common.analytics.converters.ParamCardCurrencyConverter
-import com.tangem.tap.common.extensions.dispatchOnMain
-import com.tangem.tap.common.extensions.dispatchWithMain
-import com.tangem.tap.common.extensions.inject
-import com.tangem.tap.common.extensions.onUserWalletSelected
+import com.tangem.tap.common.extensions.*
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.features.intentHandler.handlers.BackgroundScanIntentHandler
import com.tangem.tap.features.intentHandler.handlers.WalletConnectLinkIntentHandler
@@ -141,7 +138,7 @@ internal class WelcomeMiddleware {
val currency = ParamCardCurrencyConverter().convert(
value = scanResponse.cardTypesResolver,
)
-
+ Analytics.addContext(scanResponse)
if (currency != null) {
Analytics.send(
event = Basic.SignedIn(
diff --git a/app/src/main/java/com/tangem/tap/network/exchangeServices/BuyExchangeService.kt b/app/src/main/java/com/tangem/tap/network/exchangeServices/BuyExchangeService.kt
index 5f48db2717..e6c523e666 100644
--- a/app/src/main/java/com/tangem/tap/network/exchangeServices/BuyExchangeService.kt
+++ b/app/src/main/java/com/tangem/tap/network/exchangeServices/BuyExchangeService.kt
@@ -1,6 +1,6 @@
package com.tangem.tap.network.exchangeServices
-import com.tangem.blockchain.common.Blockchain
+import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.tap.domain.model.Currency
import com.tangem.tap.network.exchangeServices.mercuryo.MercuryoService
import com.tangem.tap.scope
@@ -38,16 +38,14 @@ internal class BuyExchangeService(
override fun getUrl(
action: CurrencyExchangeManager.Action,
- blockchain: Blockchain,
- cryptoCurrencyName: String,
+ cryptoCurrency: CryptoCurrency,
fiatCurrencyName: String,
walletAddress: String,
isDarkTheme: Boolean,
): String? {
return currentService.getUrl(
action,
- blockchain,
- cryptoCurrencyName,
+ cryptoCurrency,
fiatCurrencyName,
walletAddress,
isDarkTheme,
diff --git a/app/src/main/java/com/tangem/tap/network/exchangeServices/CurrencyExchangeManager.kt b/app/src/main/java/com/tangem/tap/network/exchangeServices/CurrencyExchangeManager.kt
index 1f0dc4ea68..e962f7ebf4 100644
--- a/app/src/main/java/com/tangem/tap/network/exchangeServices/CurrencyExchangeManager.kt
+++ b/app/src/main/java/com/tangem/tap/network/exchangeServices/CurrencyExchangeManager.kt
@@ -8,9 +8,9 @@ import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.transaction.TransactionFee
import com.tangem.blockchain.extensions.Result
import com.tangem.domain.models.scan.CardDTO
+import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.tap.common.extensions.inject
import com.tangem.tap.common.extensions.safeUpdate
-import com.tangem.tap.common.redux.global.CryptoCurrencyName
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.domain.TangemSigner
import com.tangem.tap.domain.model.Currency
@@ -48,19 +48,18 @@ class CurrencyExchangeManager(
override fun getUrl(
action: Action,
- blockchain: Blockchain,
- cryptoCurrencyName: CryptoCurrencyName,
+ cryptoCurrency: CryptoCurrency,
fiatCurrencyName: String,
walletAddress: String,
isDarkTheme: Boolean,
): String? {
+ val blockchain = Blockchain.fromId(cryptoCurrency.network.id.value)
if (blockchain.isTestnet()) return blockchain.getTestnetTopUpUrl()
val urlBuilder = getExchangeUrlBuilder(action)
return urlBuilder.getUrl(
action,
- blockchain,
- cryptoCurrencyName,
+ cryptoCurrency,
fiatCurrencyName,
walletAddress,
isDarkTheme,
diff --git a/app/src/main/java/com/tangem/tap/network/exchangeServices/ExchangeService.kt b/app/src/main/java/com/tangem/tap/network/exchangeServices/ExchangeService.kt
index c3f3587c26..e9b17488e3 100644
--- a/app/src/main/java/com/tangem/tap/network/exchangeServices/ExchangeService.kt
+++ b/app/src/main/java/com/tangem/tap/network/exchangeServices/ExchangeService.kt
@@ -1,6 +1,6 @@
package com.tangem.tap.network.exchangeServices
-import com.tangem.blockchain.common.Blockchain
+import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.tap.common.feature.Feature
import com.tangem.tap.domain.model.Currency
@@ -24,8 +24,7 @@ interface ExchangeService : Feature, Exchanger, ExchangeUrlBuilder {
override fun availableForSell(currency: Currency): Boolean = false
override fun getUrl(
action: CurrencyExchangeManager.Action,
- blockchain: Blockchain,
- cryptoCurrencyName: String,
+ cryptoCurrency: CryptoCurrency,
fiatCurrencyName: String,
walletAddress: String,
isDarkTheme: Boolean,
@@ -56,8 +55,7 @@ interface ExchangeUrlBuilder {
@Suppress("LongParameterList")
fun getUrl(
action: CurrencyExchangeManager.Action,
- blockchain: Blockchain,
- cryptoCurrencyName: String,
+ cryptoCurrency: CryptoCurrency,
fiatCurrencyName: String,
walletAddress: String,
isDarkTheme: Boolean,
diff --git a/app/src/main/java/com/tangem/tap/network/exchangeServices/mercuryo/MercuryoService.kt b/app/src/main/java/com/tangem/tap/network/exchangeServices/mercuryo/MercuryoService.kt
index be796e98e7..d653e2babe 100644
--- a/app/src/main/java/com/tangem/tap/network/exchangeServices/mercuryo/MercuryoService.kt
+++ b/app/src/main/java/com/tangem/tap/network/exchangeServices/mercuryo/MercuryoService.kt
@@ -6,7 +6,7 @@ import com.tangem.common.extensions.calculateSha512
import com.tangem.common.extensions.toHexString
import com.tangem.common.services.Result
import com.tangem.common.services.performRequest
-import com.tangem.tap.common.redux.global.CryptoCurrencyName
+import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.tap.domain.model.Currency
import com.tangem.tap.network.exchangeServices.CurrencyExchangeManager
import com.tangem.tap.network.exchangeServices.ExchangeService
@@ -57,20 +57,21 @@ internal class MercuryoService(private val environment: MercuryoEnvironment) : E
override fun getUrl(
action: CurrencyExchangeManager.Action,
- blockchain: Blockchain,
- cryptoCurrencyName: CryptoCurrencyName,
+ cryptoCurrency: CryptoCurrency,
fiatCurrencyName: String,
walletAddress: String,
isDarkTheme: Boolean,
): String {
if (action == CurrencyExchangeManager.Action.Sell) throw UnsupportedOperationException()
+ val blockchain = Blockchain.fromId(cryptoCurrency.network.id.value)
+
val builder = Uri.Builder()
.scheme(ExchangeUrlBuilder.SCHEME)
.authority("exchange.mercuryo.io")
.appendQueryParameter("widget_id", environment.widgetId)
.appendQueryParameter("type", action.name.lowercase())
- .appendQueryParameter("currency", cryptoCurrencyName)
+ .appendQueryParameter("currency", cryptoCurrency.symbol)
.appendQueryParameter("address", walletAddress)
.appendQueryParameter("signature", signature(walletAddress))
.appendQueryParameter("fix_currency", "true")
diff --git a/app/src/main/java/com/tangem/tap/network/exchangeServices/moonpay/BlockchainExt.kt b/app/src/main/java/com/tangem/tap/network/exchangeServices/moonpay/BlockchainExt.kt
new file mode 100644
index 0000000000..c5f04b9039
--- /dev/null
+++ b/app/src/main/java/com/tangem/tap/network/exchangeServices/moonpay/BlockchainExt.kt
@@ -0,0 +1,37 @@
+package com.tangem.tap.network.exchangeServices.moonpay
+
+import com.tangem.blockchain.common.Blockchain
+import com.tangem.blockchain.common.Blockchain.*
+import com.tangem.tap.network.exchangeServices.moonpay.models.MoonPaySupportedCurrency
+
+internal val Blockchain.moonPaySupportedCurrency: MoonPaySupportedCurrency?
+ get() = when (this) {
+ Algorand -> MoonPaySupportedCurrency(networkCode = "algorand", currencyCode = "algo")
+ Aptos -> MoonPaySupportedCurrency(networkCode = "aptos", currencyCode = "apt")
+ Arbitrum -> MoonPaySupportedCurrency(networkCode = "arbitrum", currencyCode = "eth_arbitrum")
+ Avalanche -> MoonPaySupportedCurrency(networkCode = "avalanche_c_chain", currencyCode = "avax_cchain")
+ Binance -> MoonPaySupportedCurrency(networkCode = "bnb_chain", currencyCode = "bnb")
+ Bitcoin -> MoonPaySupportedCurrency(networkCode = "bitcoin", currencyCode = "btc")
+ BitcoinCash -> MoonPaySupportedCurrency(networkCode = "bitcoin_cash", currencyCode = "bch")
+ BSC -> MoonPaySupportedCurrency(networkCode = "binance_smart_chain", currencyCode = "bnb_bsc")
+ Cardano -> MoonPaySupportedCurrency(networkCode = "cardano", currencyCode = "ada")
+ Cosmos -> MoonPaySupportedCurrency(networkCode = "cosmos", currencyCode = "atom")
+ Dogecoin -> MoonPaySupportedCurrency(networkCode = "dogecoin", currencyCode = "doge")
+ Ethereum -> MoonPaySupportedCurrency(networkCode = "ethereum", currencyCode = "eth")
+ EthereumClassic -> MoonPaySupportedCurrency(networkCode = "ethereum_classic", currencyCode = "etc")
+ Hedera -> MoonPaySupportedCurrency(networkCode = "hedera", currencyCode = "hbar")
+ Litecoin -> MoonPaySupportedCurrency(networkCode = "litecoin", currencyCode = "ltc")
+ Near -> MoonPaySupportedCurrency(networkCode = "near", currencyCode = "near")
+ Optimism -> MoonPaySupportedCurrency(networkCode = "optimism", currencyCode = "eth_optimism")
+ Polkadot -> MoonPaySupportedCurrency(networkCode = "polkadot", currencyCode = "dot")
+ Polygon -> MoonPaySupportedCurrency(networkCode = "polygon", currencyCode = "matic_polygon")
+ Ravencoin -> MoonPaySupportedCurrency(networkCode = "ravencoin", currencyCode = "rvn")
+ Solana -> MoonPaySupportedCurrency(networkCode = "solana", currencyCode = "sol")
+ Stellar -> MoonPaySupportedCurrency(networkCode = "stellar", currencyCode = "xlm")
+ Tezos -> MoonPaySupportedCurrency(networkCode = "tezos", currencyCode = "xtz")
+ TON -> MoonPaySupportedCurrency(networkCode = "ton", currencyCode = "ton")
+ Tron -> MoonPaySupportedCurrency(networkCode = "tron", currencyCode = "trx")
+ VeChain -> MoonPaySupportedCurrency(networkCode = "vechain", currencyCode = "vet")
+ XRP -> MoonPaySupportedCurrency(networkCode = "ripple", currencyCode = "xrp")
+ else -> null
+ }
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/network/exchangeServices/moonpay/MoonPayService.kt b/app/src/main/java/com/tangem/tap/network/exchangeServices/moonpay/MoonPayService.kt
index 947e8f1b69..228273c9bd 100644
--- a/app/src/main/java/com/tangem/tap/network/exchangeServices/moonpay/MoonPayService.kt
+++ b/app/src/main/java/com/tangem/tap/network/exchangeServices/moonpay/MoonPayService.kt
@@ -7,11 +7,12 @@ import com.tangem.common.services.Result
import com.tangem.common.services.performRequest
import com.tangem.datasource.api.common.createRetrofitInstance
import com.tangem.domain.common.extensions.withIOContext
-import com.tangem.tap.common.redux.global.CryptoCurrencyName
+import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.tap.domain.model.Currency
import com.tangem.tap.network.exchangeServices.CurrencyExchangeManager
import com.tangem.tap.network.exchangeServices.ExchangeService
import com.tangem.tap.network.exchangeServices.ExchangeUrlBuilder.Companion.SCHEME
+import com.tangem.tap.network.exchangeServices.moonpay.models.MoonPayAvailableCurrency
import javax.crypto.Mac
import javax.crypto.spec.SecretKeySpec
@@ -35,42 +36,47 @@ class MoonPayService(
override suspend fun update() {
withIOContext {
performRequest {
- val userStatusResult = performRequest { api.getUserStatus(apiKey) }
- if (userStatusResult is Result.Failure) return@performRequest
-
- val currenciesResult = performRequest { api.getCurrencies(apiKey) }
- if (currenciesResult is Result.Failure) return@performRequest
-
- val userStatus = (userStatusResult as Result.Success).data
- val currencies = (currenciesResult as Result.Success).data
-
-// val currenciesToBuy = mutableListOf()
- val currenciesToSell = mutableListOf()
-
- currencies.forEach { currencyStatus ->
- if (currencyStatus.type != "crypto" || currencyStatus.isSuspended ||
- !currencyStatus.supportsLiveMode
- ) {
- return@forEach
- }
-
- if (userStatus.countryCode == "USA") {
- if (!currencyStatus.isSupportedInUS) return@forEach
- if (currencyStatus.notAllowedUSStates.contains(userStatus.stateCode)) return@forEach
- }
- val currencyCode = currencyStatus.code.uppercase()
-// currenciesToBuy.add(currencyCode)
-
- if (currencyStatus.isSellSupported) currenciesToSell.add(currencyCode)
+ val userStatus = when (val result = performRequest { api.getUserStatus(apiKey) }) {
+ is Result.Failure -> return@performRequest
+ is Result.Success -> result.data
}
-// currenciesToBuy.sort()
- currenciesToSell.sort()
+
+ val currencies = when (val result = performRequest { api.getCurrencies(apiKey) }) {
+ is Result.Failure -> return@performRequest
+ is Result.Success -> result.data
+ }
+
+ val currenciesToSell = currencies
+ .filter { currency ->
+ checkGeneralRequirements(currency) && checkUSARequirements(userStatus, currency)
+ }
+ .mapNotNull { currency ->
+ if (!currency.isSellSupported) return@mapNotNull null
+ MoonPayAvailableCurrency(
+ currencyCode = currency.code,
+ networkCode = currency.metadata?.networkCode ?: return@mapNotNull null,
+ contractAddress = currency.metadata.contractAddress,
+ )
+ }
status = MoonPayStatus(currenciesToSell, userStatus, currencies)
}
}
}
+ private fun checkGeneralRequirements(currency: MoonPayCurrencies): Boolean {
+ return currency.type == "crypto" && !currency.isSuspended && currency.supportsLiveMode &&
+ currency.isSellSupported
+ }
+
+ private fun checkUSARequirements(userStatus: MoonPayUserStatus, currency: MoonPayCurrencies): Boolean {
+ return if (userStatus.countryCode == "USA") {
+ currency.isSupportedInUS && !currency.notAllowedUSStates.contains(userStatus.stateCode)
+ } else {
+ true
+ }
+ }
+
override fun isBuyAllowed(): Boolean = false
override fun isSellAllowed(): Boolean {
@@ -80,42 +86,57 @@ class MoonPayService(
override fun availableForBuy(currency: Currency): Boolean = false
override fun availableForSell(currency: Currency): Boolean {
- val availableForSell = status?.availableForSell ?: return false
- val metadata = status?.responseCurrencies?.filter { it.isSellSupported }?.map { it.metadata }
if (!isSellAllowed()) return false
- return when (currency) {
- is Currency.Blockchain -> {
- val blockchain = currency.blockchain
- when {
- blockchain.isTestnet() -> false
- blockchain == Blockchain.Unknown || currency.blockchain == Blockchain.BSC -> false
- else -> availableForSell.contains(currency.currencySymbol)
+ val availableForSell = status?.availableForSell ?: return false
+
+ val supportedCurrency = currency.blockchain.moonPaySupportedCurrency ?: return false
+ return availableForSell.any {
+ when (currency) {
+ is Currency.Blockchain -> {
+ it.networkCode.equals(other = supportedCurrency.networkCode, ignoreCase = true) &&
+ it.currencyCode.equals(other = supportedCurrency.currencyCode, ignoreCase = true)
+ }
+ is Currency.Token -> {
+ it.networkCode.equals(other = supportedCurrency.networkCode, ignoreCase = true) &&
+ it.contractAddress.equals(other = currency.token.contractAddress, ignoreCase = true)
}
- }
- is Currency.Token -> {
- metadata?.any { it?.contractAddress.equals(currency.token.contractAddress, ignoreCase = true) } ?: false
}
}
}
override fun getUrl(
action: CurrencyExchangeManager.Action,
- blockchain: Blockchain,
- cryptoCurrencyName: CryptoCurrencyName,
- fatCurrency: String,
+ cryptoCurrency: CryptoCurrency,
+ fiatCurrencyName: String,
walletAddress: String,
isDarkTheme: Boolean,
- ): String {
+ ): String? {
if (action == CurrencyExchangeManager.Action.Buy) throw UnsupportedOperationException()
+ val blockchain = Blockchain.fromId(cryptoCurrency.network.id.value)
+ val supportedCurrency = blockchain.moonPaySupportedCurrency ?: return null
+ val moonpayCurrency = status?.availableForSell?.firstOrNull {
+ when (cryptoCurrency) {
+ is CryptoCurrency.Coin -> {
+ it.networkCode.equals(other = supportedCurrency.networkCode, ignoreCase = true) &&
+ it.currencyCode.equals(other = supportedCurrency.currencyCode, ignoreCase = true)
+ }
+ is CryptoCurrency.Token -> {
+ it.networkCode.equals(other = supportedCurrency.networkCode, ignoreCase = true) &&
+ it.contractAddress.equals(other = cryptoCurrency.contractAddress, ignoreCase = true)
+ }
+ }
+ } ?: return null
+
val uri = Uri.Builder()
.scheme(SCHEME)
.authority(URL_SELL)
.appendQueryParameter("apiKey", apiKey)
- .appendQueryParameter("baseCurrencyCode", cryptoCurrencyName)
+ .appendQueryParameter("baseCurrencyCode", moonpayCurrency.currencyCode.uppercase())
.appendQueryParameter("refundWalletAddress", walletAddress)
- .appendQueryParameter("redirectURL", "tangem://sell-request.tangem.com")
+ .appendQueryParameter("redirectURL", "tangem://redirect_sell")
+
if (isDarkTheme) uri.appendQueryParameter("theme", "dark")
val originalQuery = uri.build().encodedQuery ?: uri.build().toString()
@@ -147,7 +168,7 @@ class MoonPayService(
}
private data class MoonPayStatus(
- val availableForSell: List,
+ val availableForSell: List,
val responseUserStatus: MoonPayUserStatus,
val responseCurrencies: List,
)
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/network/exchangeServices/moonpay/models/MoonPayAvailableCurrency.kt b/app/src/main/java/com/tangem/tap/network/exchangeServices/moonpay/models/MoonPayAvailableCurrency.kt
new file mode 100644
index 0000000000..5f7845f0c9
--- /dev/null
+++ b/app/src/main/java/com/tangem/tap/network/exchangeServices/moonpay/models/MoonPayAvailableCurrency.kt
@@ -0,0 +1,7 @@
+package com.tangem.tap.network.exchangeServices.moonpay.models
+
+internal data class MoonPayAvailableCurrency(
+ val currencyCode: String,
+ val networkCode: String,
+ val contractAddress: String?,
+)
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/network/exchangeServices/moonpay/models/MoonPaySupportedCurrency.kt b/app/src/main/java/com/tangem/tap/network/exchangeServices/moonpay/models/MoonPaySupportedCurrency.kt
new file mode 100644
index 0000000000..f858942b46
--- /dev/null
+++ b/app/src/main/java/com/tangem/tap/network/exchangeServices/moonpay/models/MoonPaySupportedCurrency.kt
@@ -0,0 +1,3 @@
+package com.tangem.tap.network.exchangeServices.moonpay.models
+
+internal data class MoonPaySupportedCurrency(val networkCode: String, val currencyCode: String?)
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/proxy/TransactionManagerImpl.kt b/app/src/main/java/com/tangem/tap/proxy/TransactionManagerImpl.kt
index 4c7a8f4461..8b3e444c59 100644
--- a/app/src/main/java/com/tangem/tap/proxy/TransactionManagerImpl.kt
+++ b/app/src/main/java/com/tangem/tap/proxy/TransactionManagerImpl.kt
@@ -9,7 +9,7 @@ import com.tangem.blockchain.blockchains.cosmos.CosmosTransactionExtras
import com.tangem.blockchain.blockchains.ethereum.EthereumTransactionExtras
import com.tangem.blockchain.blockchains.ethereum.EthereumWalletManager
import com.tangem.blockchain.blockchains.hedera.HederaTransactionBuilder
-import com.tangem.blockchain.blockchains.optimism.OptimismWalletManager
+import com.tangem.blockchain.blockchains.optimism.EthereumOptimisticRollupWalletManager
import com.tangem.blockchain.blockchains.stellar.StellarMemo
import com.tangem.blockchain.blockchains.stellar.StellarTransactionExtras
import com.tangem.blockchain.blockchains.ton.TonTransactionExtras
@@ -170,7 +170,7 @@ class TransactionManagerImpl(
val blockchain = requireNotNull(Blockchain.fromNetworkId(networkId)) { "blockchain not found" }
val walletManager = getActualWalletManager(blockchain, derivationPath)
if (walletManager is EthereumWalletManager) {
- if (walletManager is OptimismWalletManager) {
+ if (walletManager is EthereumOptimisticRollupWalletManager) {
return getFeeForOptimismBlockchain(
walletManager = walletManager,
amount = createAmount(amountToSend, currencyToSend, blockchain),
@@ -285,7 +285,7 @@ class TransactionManagerImpl(
}
private suspend fun getFeeForOptimismBlockchain(
- walletManager: OptimismWalletManager,
+ walletManager: EthereumOptimisticRollupWalletManager,
amount: Amount,
destinationAddress: String,
data: String?,
diff --git a/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphState.kt b/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphState.kt
index 0afb10ab8d..c23def20bc 100644
--- a/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphState.kt
+++ b/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphState.kt
@@ -1,7 +1,9 @@
package com.tangem.tap.proxy.redux
+import com.tangem.TangemSdkLogger
import com.tangem.blockchain.common.AccountCreator
import com.tangem.blockchain.common.datastorage.BlockchainDataStorage
+import com.tangem.blockchain.common.logging.BlockchainSDKLogger
import com.tangem.datasource.connection.NetworkConnectionManager
import com.tangem.domain.appcurrency.repository.AppCurrencyRepository
import com.tangem.domain.apptheme.repository.AppThemeModeRepository
@@ -67,4 +69,6 @@ data class DaggerGraphState(
val saveTwinsOnboardingShownUseCase: SaveTwinsOnboardingShownUseCase? = null,
val cardRepository: CardRepository? = null,
val feedbackManagerFeatureToggles: FeedbackManagerFeatureToggles? = null,
+ val tangemSdkLogger: TangemSdkLogger? = null,
+ val blockchainSDKLogger: BlockchainSDKLogger? = null,
) : StateType
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_disclaimer.xml b/app/src/main/res/layout/fragment_disclaimer.xml
index 1c237b7938..346cab4f03 100644
--- a/app/src/main/res/layout/fragment_disclaimer.xml
+++ b/app/src/main/res/layout/fragment_disclaimer.xml
@@ -40,7 +40,7 @@
android:layout_height="match_parent"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
- android:layout_marginBottom="90dp"
+ android:layout_marginBottom="74dp"
app:layout_constraintBottom_toTopOf="@id/btn_accept"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
@@ -49,7 +49,7 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/app/src/test/kotlin/com/tangem/tap/domain/card/DefaultDerivationsRepositoryTest.kt b/app/src/test/kotlin/com/tangem/tap/domain/card/DefaultDerivationsRepositoryTest.kt
index 730ddb8d96..b1e1189c8e 100644
--- a/app/src/test/kotlin/com/tangem/tap/domain/card/DefaultDerivationsRepositoryTest.kt
+++ b/app/src/test/kotlin/com/tangem/tap/domain/card/DefaultDerivationsRepositoryTest.kt
@@ -37,6 +37,7 @@ internal class DefaultDerivationsRepositoryTest {
cardsInWallet = setOf(),
isMultiCurrency = false,
scanResponse = ScanResponseMockFactory.create(cardConfig = GenericCardConfig(2), derivedKeys = emptyMap()),
+ hasBackupError = false,
)
@Test
diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/express/TangemExpressApi.kt b/core/datasource/src/main/java/com/tangem/datasource/api/express/TangemExpressApi.kt
index 61c2a6c25a..b7055574a3 100644
--- a/core/datasource/src/main/java/com/tangem/datasource/api/express/TangemExpressApi.kt
+++ b/core/datasource/src/main/java/com/tangem/datasource/api/express/TangemExpressApi.kt
@@ -50,6 +50,8 @@ interface TangemExpressApi {
@Query("rateType") rateType: String,
@Query("toAddress") toAddress: String,
@Query("requestId") requestId: String,
+ @Query("refundAddress") refundAddress: String?, // for cex only
+ @Query("refundExtraId") refundExtraId: String?, // for cex only
): ApiResponse
@GET("exchange-status")
diff --git a/core/datasource/src/main/java/com/tangem/datasource/config/ConfigManagerImpl.kt b/core/datasource/src/main/java/com/tangem/datasource/config/ConfigManagerImpl.kt
index 580334eb2b..d639cfbe6a 100644
--- a/core/datasource/src/main/java/com/tangem/datasource/config/ConfigManagerImpl.kt
+++ b/core/datasource/src/main/java/com/tangem/datasource/config/ConfigManagerImpl.kt
@@ -102,7 +102,6 @@ internal class ConfigManagerImpl @Inject constructor() : ConfigManager {
),
appsFlyerDevKey = configValues.appsFlyer.appsFlyerDevKey,
amplitudeApiKey = configValues.amplitudeApiKey,
- shopify = configValues.shopifyShop,
sprinklr = configValues.sprinklr,
walletConnectProjectId = configValues.walletConnectProjectId,
tangemComAuthorization = configValues.tangemComAuthorization,
@@ -147,6 +146,9 @@ internal class ConfigManagerImpl @Inject constructor() : ConfigManager {
blockBookRest = accessTokens.bitcoin?.blockBookRest,
),
algorand = GetBlockAccessToken(rest = accessTokens.algorand?.rest),
+ zkSync = GetBlockAccessToken(rest = accessTokens.zksync?.jsonRPC),
+ polygonZkevm = GetBlockAccessToken(rest = accessTokens.polygonZkevm?.jsonRPC),
+ base = GetBlockAccessToken(rest = accessTokens.base?.jsonRPC),
)
}
}
diff --git a/core/datasource/src/main/java/com/tangem/datasource/config/models/Config.kt b/core/datasource/src/main/java/com/tangem/datasource/config/models/Config.kt
index 8490f44237..25e8595d33 100644
--- a/core/datasource/src/main/java/com/tangem/datasource/config/models/Config.kt
+++ b/core/datasource/src/main/java/com/tangem/datasource/config/models/Config.kt
@@ -14,7 +14,6 @@ data class Config(
val isTopUpEnabled: Boolean = false,
@Deprecated("Not relevant since version 3.23")
val isCreatingTwinCardsAllowed: Boolean = false,
- val shopify: ShopifyShop? = null,
val sprinklr: SprinklrConfig? = null,
val walletConnectProjectId: String = "",
val tangemComAuthorization: String? = null,
diff --git a/core/datasource/src/main/java/com/tangem/datasource/config/models/JsonModels.kt b/core/datasource/src/main/java/com/tangem/datasource/config/models/JsonModels.kt
index eaedb32084..94987bf391 100644
--- a/core/datasource/src/main/java/com/tangem/datasource/config/models/JsonModels.kt
+++ b/core/datasource/src/main/java/com/tangem/datasource/config/models/JsonModels.kt
@@ -31,7 +31,6 @@ class ConfigValueModel(
val blockcypherTokens: Set?,
val infuraProjectId: String?,
val appsFlyer: AppsFlyer,
- val shopifyShop: ShopifyShop?,
val sprinklr: SprinklrConfig?,
val tronGridApiKey: String,
val amplitudeApiKey: String,
@@ -68,6 +67,9 @@ data class GetBlockAccessTokens(
@Json(name = "dash") val dash: GetBlockToken?,
@Json(name = "bitcoin") val bitcoin: GetBlockToken?,
@Json(name = "algorand") val algorand: GetBlockToken?,
+ @Json(name = "polygon-zkevm") val polygonZkevm: GetBlockToken?,
+ @Json(name = "zksync") val zksync: GetBlockToken?,
+ @Json(name = "base") val base: GetBlockToken?,
)
@JsonClass(generateAdapter = true)
diff --git a/core/datasource/src/main/java/com/tangem/datasource/config/models/ShopifyShop.kt b/core/datasource/src/main/java/com/tangem/datasource/config/models/ShopifyShop.kt
deleted file mode 100644
index c4a09adcfe..0000000000
--- a/core/datasource/src/main/java/com/tangem/datasource/config/models/ShopifyShop.kt
+++ /dev/null
@@ -1,14 +0,0 @@
-package com.tangem.datasource.config.models
-
-import com.squareup.moshi.Json
-import com.squareup.moshi.JsonClass
-
-@JsonClass(generateAdapter = true)
-data class ShopifyShop(
- @Json(name = "domain")
- val domain: String,
- @Json(name = "storefrontApiKeyAndroid")
- val storefrontApiKey: String,
- @Json(name = "merchantID")
- val merchantID: String?,
-)
\ No newline at end of file
diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt b/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt
index 2a6b0457d3..bec274ae30 100644
--- a/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt
+++ b/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt
@@ -73,6 +73,8 @@ object PreferencesKeys {
stringPreferencesKey(name = "POLKADOT_HEALTH_CHECKED_IMMUTABLE_ACCOUNTS")
}
+ val SEND_TAP_HELP_PREVIEW_KEY by lazy { booleanPreferencesKey(name = "sendTapHelpPreview") }
+
fun getStart2CoinTOSAcceptedKey(region: String?) = booleanPreferencesKey(name = "start2Coin_tos_accepted_$region")
}
diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/userwallet/UserWalletsStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/userwallet/UserWalletsStore.kt
index f3b1c56267..bfee20e460 100644
--- a/core/datasource/src/main/java/com/tangem/datasource/local/userwallet/UserWalletsStore.kt
+++ b/core/datasource/src/main/java/com/tangem/datasource/local/userwallet/UserWalletsStore.kt
@@ -9,6 +9,8 @@ interface UserWalletsStore {
suspend fun getSyncOrNull(key: UserWalletId): UserWallet?
+ suspend fun getAllSyncOrNull(): List?
+
@Throws
suspend fun update(userWalletId: UserWalletId, update: suspend (UserWallet) -> UserWallet)
}
\ No newline at end of file
diff --git a/core/deep-links/global/src/main/kotlin/com/tangem/core/deeplink/global/SellCurrencyDeepLink.kt b/core/deep-links/global/src/main/kotlin/com/tangem/core/deeplink/global/SellCurrencyDeepLink.kt
index 5d9047a83b..579c72689a 100644
--- a/core/deep-links/global/src/main/kotlin/com/tangem/core/deeplink/global/SellCurrencyDeepLink.kt
+++ b/core/deep-links/global/src/main/kotlin/com/tangem/core/deeplink/global/SellCurrencyDeepLink.kt
@@ -4,13 +4,14 @@ import com.tangem.core.deeplink.DeepLink
class SellCurrencyDeepLink(val onReceive: (data: Data) -> Unit) : DeepLink {
- override val uri: String = "tangem://sell-request.tangem.com"
+ override val uri: String = "tangem://redirect_sell"
override fun onReceive(params: Map) {
val data = Data(
transactionId = params["transactionId"] ?: return,
baseCurrencyAmount = params["baseCurrencyAmount"] ?: return,
depositWalletAddress = params["depositWalletAddress"] ?: return,
+ depositWalletAddressTag = params["depositWalletAddressTag"],
)
onReceive(data)
@@ -20,5 +21,6 @@ class SellCurrencyDeepLink(val onReceive: (data: Data) -> Unit) : DeepLink {
val transactionId: String,
val baseCurrencyAmount: String,
val depositWalletAddress: String,
+ val depositWalletAddressTag: String?,
)
}
\ No newline at end of file
diff --git a/core/featuretoggles/src/main/assets/configs/feature_toggles_config.json b/core/featuretoggles/src/main/assets/configs/feature_toggles_config.json
index 639da533da..7a8e690d21 100644
--- a/core/featuretoggles/src/main/assets/configs/feature_toggles_config.json
+++ b/core/featuretoggles/src/main/assets/configs/feature_toggles_config.json
@@ -3,10 +3,6 @@
"name": "NEW_CARD_SCANNING_ENABLED",
"version": "undefined"
},
- {
- "name": "SHOPIFY_DYNAMIC_ENABLED",
- "version": "undefined"
- },
{
"name": "REDESIGNED_MANAGE_TOKENS_SCREEN_ENABLED",
"version": "undefined"
@@ -22,5 +18,13 @@
{
"name": "LOCAL_USER_LOGS_ENABLED",
"version": "5.8.0"
+ },
+ {
+ "name": "GENERATE_XPUB_ENABLED",
+ "version": "undefined"
+ },
+ {
+ "name": "WC_SOLANA_TX_SIGN_ENABLED",
+ "version": "5.11.0"
}
]
diff --git a/core/navigation/build.gradle.kts b/core/navigation/build.gradle.kts
index bb00481f67..8dc7d5437d 100644
--- a/core/navigation/build.gradle.kts
+++ b/core/navigation/build.gradle.kts
@@ -1,6 +1,8 @@
plugins {
alias(deps.plugins.android.library)
alias(deps.plugins.kotlin.android)
+ alias(deps.plugins.kotlin.kapt)
+ alias(deps.plugins.hilt.android)
id("configuration")
}
@@ -9,6 +11,10 @@ android {
}
dependencies {
+ implementation(deps.hilt.android)
+ kapt(deps.hilt.kapt)
+
implementation(deps.material)
implementation(deps.reKotlin)
+ implementation(deps.timber)
}
\ No newline at end of file
diff --git a/core/navigation/src/main/java/com/tangem/core/navigation/NavigationState.kt b/core/navigation/src/main/java/com/tangem/core/navigation/NavigationState.kt
index 7ca534e15c..a0a6d64f25 100644
--- a/core/navigation/src/main/java/com/tangem/core/navigation/NavigationState.kt
+++ b/core/navigation/src/main/java/com/tangem/core/navigation/NavigationState.kt
@@ -11,7 +11,6 @@ data class NavigationState(
enum class AppScreen(val isDialogFragment: Boolean = false) {
Home,
- Shop,
Disclaimer,
OnboardingNote,
OnboardingWallet,
diff --git a/core/navigation/src/main/java/com/tangem/core/navigation/di/EmailSenderModule.kt b/core/navigation/src/main/java/com/tangem/core/navigation/di/EmailSenderModule.kt
new file mode 100644
index 0000000000..0651ea8926
--- /dev/null
+++ b/core/navigation/src/main/java/com/tangem/core/navigation/di/EmailSenderModule.kt
@@ -0,0 +1,22 @@
+package com.tangem.core.navigation.di
+
+import android.content.Context
+import com.tangem.core.navigation.email.AndroidEmailSender
+import com.tangem.core.navigation.email.EmailSender
+import dagger.Module
+import dagger.Provides
+import dagger.hilt.InstallIn
+import dagger.hilt.android.components.ActivityComponent
+import dagger.hilt.android.qualifiers.ActivityContext
+import dagger.hilt.android.scopes.ActivityScoped
+
+@Module
+@InstallIn(ActivityComponent::class)
+internal object EmailSenderModule {
+
+ @Provides
+ @ActivityScoped
+ fun provideEmailSender(@ActivityContext context: Context): EmailSender {
+ return AndroidEmailSender(context)
+ }
+}
\ No newline at end of file
diff --git a/core/navigation/src/main/java/com/tangem/core/navigation/email/AndroidEmailSender.kt b/core/navigation/src/main/java/com/tangem/core/navigation/email/AndroidEmailSender.kt
new file mode 100644
index 0000000000..d6fe4b6d35
--- /dev/null
+++ b/core/navigation/src/main/java/com/tangem/core/navigation/email/AndroidEmailSender.kt
@@ -0,0 +1,65 @@
+package com.tangem.core.navigation.email
+
+import android.content.Context
+import android.content.Intent
+import android.net.Uri
+import androidx.core.app.ShareCompat
+import androidx.core.content.ContextCompat
+import androidx.core.content.FileProvider
+import timber.log.Timber
+
+/**
+ * Implementation of email sender for Android
+ *
+ * @property context context
+ *
+[REDACTED_AUTHOR]
+ */
+internal class AndroidEmailSender(private val context: Context) : EmailSender {
+
+ override fun send(email: EmailSender.Email, onFail: ((Exception) -> Unit)?) {
+ val originalIntent = createEmailShareIntent(email)
+ val emailFilterIntent = Intent(Intent.ACTION_SENDTO, Uri.parse("mailto:"))
+
+ val packageManager = context.packageManager
+ val originalIntentResults = packageManager.queryIntentActivities(originalIntent, 0)
+ val emailFilterIntentResults = packageManager.queryIntentActivities(emailFilterIntent, 0)
+
+ val targetedIntents = originalIntentResults
+ .filter { originalResult ->
+ emailFilterIntentResults.any {
+ originalResult.activityInfo.packageName == it.activityInfo.packageName
+ }
+ }
+ .map {
+ createEmailShareIntent(email).apply { setPackage(it.activityInfo.packageName) }
+ }
+ .toMutableList()
+
+ try {
+ val chooserIntent = Intent.createChooser(targetedIntents.removeAt(0), "Send mail...").apply {
+ putExtra(Intent.EXTRA_INITIAL_INTENTS, targetedIntents.toTypedArray())
+ }
+
+ ContextCompat.startActivity(context, chooserIntent, null)
+ } catch (ex: Exception) {
+ Timber.e("Failed to send email: $ex")
+ }
+ }
+
+ private fun createEmailShareIntent(email: EmailSender.Email): Intent {
+ val builder = ShareCompat.IntentBuilder(context)
+ .setType("message/rfc822")
+ .setEmailTo(arrayOf(email.address))
+ .setSubject(email.subject)
+ .setText(email.message)
+
+ email.attachment?.let {
+ builder.setStream(
+ FileProvider.getUriForFile(context, "${context.packageName}.provider", it),
+ )
+ }
+
+ return builder.intent
+ }
+}
\ No newline at end of file
diff --git a/core/navigation/src/main/java/com/tangem/core/navigation/email/EmailSender.kt b/core/navigation/src/main/java/com/tangem/core/navigation/email/EmailSender.kt
new file mode 100644
index 0000000000..3aa803b702
--- /dev/null
+++ b/core/navigation/src/main/java/com/tangem/core/navigation/email/EmailSender.kt
@@ -0,0 +1,21 @@
+package com.tangem.core.navigation.email
+
+import java.io.File
+
+/**
+ * Email sender
+ *
+[REDACTED_AUTHOR]
+ */
+interface EmailSender {
+
+ /** Send [email] or handle exception [onFail] */
+ fun send(email: Email, onFail: ((Exception) -> Unit)? = null)
+
+ data class Email(
+ val address: String,
+ val subject: String,
+ val message: String,
+ val attachment: File? = null,
+ )
+}
\ No newline at end of file
diff --git a/core/res/src/main/res/values-de/strings.xml b/core/res/src/main/res/values-de/strings.xml
index aaccaf5396..559c09d33c 100644
--- a/core/res/src/main/res/values-de/strings.xml
+++ b/core/res/src/main/res/values-de/strings.xml
@@ -1,72 +1,59 @@
- Diese Karte ist für die Zusammenarbeit mit Tangem nicht geeignet
- Tangem Bot
- Akzeptieren
- Bilanz: %s
- Sie haben keinen Zugang zur Kamera erteilt, bitte passen Sie Ihre Datenschutzeinstellungen an
- Abbrechen
- Entfernen
- Erledigt
- Fehler
- OK
- Änderungen speichern
- Absenden
- Erfolg
- Gebühr
- Zugangscode
- Sie müssen den richtigen Zugangscode eingeben, bevor Sie die Karte scannen.
- Langes Tippen
- Dieser Mechanismus schützt vor Annäherungsangriffen auf eine Karte. Es wird eine Verzögerung zwischen dem Empfang und der Ausführung eines Befehls erzwungen. Nach der ersten signierten Transaktion wird dieses Telefon mit der Karte verknüpft und die Transaktionen werden sofort signiert
- Passcode
- Bevor Sie einen Befehl ausführen, der eine Änderung des Kartenstatus zur Folge hat, müssen Sie den Passcode eingeben.
- %s Hasch
- KartenID
- App Währung
- Emittent
- Signiert
- Details
- Nutzungsbedingungen
- Karte scannen
- Tippen Sie um den Zugangscode zu ändern
- Tippen Sie um den Passcode zu ändern
- Legen Sie die Karte zum Scannen an
- Tippen um zu signieren
- Legen Sie die Karte an
- Der Betrag enthält nicht einige Ihrer Mittel
- Ein wallet erstellen
- Betrag
- Adresse
- Die Adresse stimmt mit der Adresse Ihrer Brieftasche überein
- Tag
- Memo
- inkl. Gebühr
- Niedrig
- Normal
- Priorität
- Höchstbetrag
- Netzgebühr
- Gesamt
- %1$s und %2$s werden gesendet
- ≈ %1$s (inkl. Gebühr: %2$s)
- %s wird gesendet
- Die Transaktion wurde erfolgreich signiert und an den Blockchain-Knoten gesendet. Die Walletbilanz wird aktualisiert
- Ungültige Adresse
- Tangem Wallet
- Tangem Twin
- Die Bilanz wird aufgeladen…
- Die Transaktion läuft…
- Verifizierte Bilanz
- WalletConnect
- Das Konto ist nicht erstellt
- Diese Karte wird nicht unterstützt
- Ihre Tangem-Karte wurde für die Arbeit mit einer anderen App ausgestellt. Bitte sehen Sie sich den Namen und die Anleitung auf Ihrer Karte an und installieren Sie eine korrekte App
- Die Adresse wurde erfolgreich kopiert
- Keine Internetverbindung
- Erhalten
- von %s
- Senden
- an %s
- Tangem
- OK, ich hab\'s!
+ Diese Karte ist für die Zusammenarbeit mit Tangem nicht geeignet
+ Akzeptieren
+ Bilanz: %s
+ Sie haben keinen Zugang zur Kamera erteilt, bitte passen Sie Ihre Datenschutzeinstellungen an
+ Abbrechen
+ Entfernen
+ Erledigt
+ Fehler
+ Gebühr
+ Netzgebühr
+ OK
+ Änderungen speichern
+ Absenden
+ Erfolg
+ Zugangscode
+ Sie müssen den richtigen Zugangscode eingeben, bevor Sie die Karte scannen.
+ Langes Tippen
+ Dieser Mechanismus schützt vor Annäherungsangriffen auf eine Karte. Es wird eine Verzögerung zwischen dem Empfang und der Ausführung eines Befehls erzwungen. Nach der ersten signierten Transaktion wird dieses Telefon mit der Karte verknüpft und die Transaktionen werden sofort signiert
+ Passcode
+ Bevor Sie einen Befehl ausführen, der eine Änderung des Kartenstatus zur Folge hat, müssen Sie den Passcode eingeben.
+ %s Hasch
+ KartenID
+ App Währung
+ Emittent
+ Signiert
+ Details
+ Nutzungsbedingungen
+ Karte scannen
+ Tippen Sie um den Zugangscode zu ändern
+ Tippen Sie um den Passcode zu ändern
+ Legen Sie die Karte zum Scannen an
+ Tippen um zu signieren
+ Legen Sie die Karte an
+ Ein wallet erstellen
+ Betrag
+ Adresse
+ Die Adresse stimmt mit der Adresse Ihrer Brieftasche überein
+ Tag
+ Memo
+ inkl. Gebühr
+ Niedrig
+ Normal
+ Priorität
+ Höchstbetrag
+ Gesamt
+ %1$s und %2$s werden gesendet
+ ≈ %1$s (inkl. Gebühr: %2$s)
+ %s wird gesendet
+ Die Transaktion wurde erfolgreich signiert und an den Blockchain-Knoten gesendet. Die Walletbilanz wird aktualisiert
+ Ungültige Adresse
+ Tangem Twin
+ WalletConnect
+ Die Adresse wurde erfolgreich kopiert
+ Keine Internetverbindung
+ Tangem
+ OK, ich hab\'s!
diff --git a/core/res/src/main/res/values-fr/strings.xml b/core/res/src/main/res/values-fr/strings.xml
index 680543d79c..e7454edeb7 100644
--- a/core/res/src/main/res/values-fr/strings.xml
+++ b/core/res/src/main/res/values-fr/strings.xml
@@ -1,72 +1,59 @@
- Cette carte n\'est pas conçue pour fonctionner avec Tangem
- Tangem Bot
- J\'accepte
- Solde : %s
- Vous n\'avez pas octroyé l\'accès à votre caméra, veuillez modifier vos paramètres de confidentialité
- Annuler
- Supprimer
- Exécuté
- Erreur
- OK
- Sauvegarder les modifications
- Envoyer
- Avec succès
- Commissions
- Code d\'accès
- Vous devrez entrer le mot de passe correct avant de scanner la carte
- Tenez la carte fermement
- Ce mécanisme protège contre les attaques sans contact sur la carte. Il y a un délai entre la réception et l\'exécution de la commande. Après la première transaction signée, ce téléphone sera associé à la carte et les transactions seront signées immédiatement.
- Mot de passe
- Avant d\'exécuter une commande qui modifie l\'état de la carte, vous devrez entrer un mot de passe.
- %s hashes
- ID de la carte
- Monnaie de l\'application
- Emetteur
- Signé
- Référénces
- Conditions d\'utilisation
- Scannez la carte
- Touchez, pour modifier le code d\'accès
- Touchez, pour modifier le mot de passe
- Posez pour scanner
- Touchez pour signer
- Posez la carte
- Le montant n\'inclut pas certains de vos fonds
- Créer un portefeuille
- Somme
- Adresse
- L\'adresse est la même que celle de votre portefeuille
- Tag
- Memo
- Inclure les commissions
- Bas
- Normal
- Priorité
- Somme maximale
- Commissions du réseau
- Total
- Sera envoyé %1$s et %2$s
- ≈ %1$s (incl. les commissions : %2$s)
- Sera envoyé %s
- La transaction a été signée avec succès et envoyée au nœud de blockchain. Le solde du portefeuille sera mis à jour après un certain temps
- Adresse incorrecte
- Tangem Wallet
- Tangem Twin
- Solde est en cours de téléchargement…
- Transaction en cours…
- Solde confirmé
- WalletConnect
- Compte n\'est pas créé
- Cette carte n\'est pas prise en charge
- Votre carte Tangem a été créée pour fonctionner avec une autre application. Regardez le nom et les instructions sur votre carte et installez l\'application pertinente
- L\'adresse a été copiée avec succès
- Pas de connexion internet
- Réception
- de %s
- Envoi
- jusqu\'à %s
- Tangem
- Ok, je l\'ai!
+ Cette carte n\'est pas conçue pour fonctionner avec Tangem
+ J\'accepte
+ Solde : %s
+ Vous n\'avez pas octroyé l\'accès à votre caméra, veuillez modifier vos paramètres de confidentialité
+ Annuler
+ Supprimer
+ Exécuté
+ Erreur
+ Commissions
+ Commissions du réseau
+ OK
+ Sauvegarder les modifications
+ Envoyer
+ Avec succès
+ Code d\'accès
+ Vous devrez entrer le mot de passe correct avant de scanner la carte
+ Tenez la carte fermement
+ Ce mécanisme protège contre les attaques sans contact sur la carte. Il y a un délai entre la réception et l\'exécution de la commande. Après la première transaction signée, ce téléphone sera associé à la carte et les transactions seront signées immédiatement.
+ Mot de passe
+ Avant d\'exécuter une commande qui modifie l\'état de la carte, vous devrez entrer un mot de passe.
+ %s hashes
+ ID de la carte
+ Monnaie de l\'application
+ Emetteur
+ Signé
+ Référénces
+ Conditions d\'utilisation
+ Scannez la carte
+ Touchez, pour modifier le code d\'accès
+ Touchez, pour modifier le mot de passe
+ Posez pour scanner
+ Touchez pour signer
+ Posez la carte
+ Créer un portefeuille
+ Somme
+ Adresse
+ L\'adresse est la même que celle de votre portefeuille
+ Tag
+ Memo
+ Inclure les commissions
+ Bas
+ Normal
+ Priorité
+ Somme maximale
+ Total
+ Sera envoyé %1$s et %2$s
+ ≈ %1$s (incl. les commissions : %2$s)
+ Sera envoyé %s
+ La transaction a été signée avec succès et envoyée au nœud de blockchain. Le solde du portefeuille sera mis à jour après un certain temps
+ Adresse incorrecte
+ Tangem Twin
+ WalletConnect
+ L\'adresse a été copiée avec succès
+ Pas de connexion internet
+ Tangem
+ Ok, je l\'ai!
diff --git a/core/res/src/main/res/values-it/strings.xml b/core/res/src/main/res/values-it/strings.xml
index 6ff58173b7..e7caa7829e 100644
--- a/core/res/src/main/res/values-it/strings.xml
+++ b/core/res/src/main/res/values-it/strings.xml
@@ -1,72 +1,59 @@
- Questa carta non è progettata per funzionare con Tangem
- Tangem Bot
- Accetta
- Saldo: %s
- Non hai fornito l\'accesso alla tua videocamera, modifica le tue impostazioni sulla privacy
- Annulla
- Rimuovere
- Fatto
- Errore
- OK
- Mantieni le modifiche
- Invia
- Con successo
- Commissione
- Codice di accesso
- Prima di scansionare la carta sarà necessario inserire il codice di accesso corretto
- Mantenimento della carta
- Questo meccanismo protegge dagli avvicinamenti senza contatto sulla carta. Attiva un ritardo tra la ricezione e l\'esecuzione di un comando. Dopo la prima transazione firmata, questo telefono verrà associato alla carta e le transazioni verranno firmate immediatamente.
- Password
- Dovrai inserire una password prima di eseguire qualsiasi comando che modifichi lo stato della carta.
- Hash %s
- ID carta
- Valuta dell\'applicazione
- Emittente
- Firmato
- Requisiti
- Termini del servizio
- Scansiona carta
- Avvicina per modificare il codice di accesso
- Avvicina per modificare la password
- Avvicina per scansionare
- Avvicina per firmare
- Avvicina la carta
- L\'importo non include alcuni dei tuoi fondi
- Crea portafoglio
- Importo
- Indirizzo
- L\'indirizzo corrisponde all\'indirizzo del tuo portafoglio
- Tag
- Memo
- Includi commissione
- Insufficiente
- Normale
- Prioritario
- Importo totale
- Costi della rete
- Totale
- Sarà inviato %1$s e %2$s
- ≈ %1$s (inc. commissione: %2$s)
- Sarà inviato %s
- La transazione è stata firmata con successo e inviata al nodo blockchain. Il saldo del portafoglio verrà aggiornato dopo un po\' di tempo
- Indirizzo non valido
- Tangem Wallet
- Tangem Twin
- Il saldo sta per essere caricato…
- Transazione in corso…
- Saldo verificato
- WalletConnect
- Conto non creato
- Questa carta non è supportata
- La tua carta Tangem è stata creata per funzionare con un\'altra applicazione. Leggere il nome e le istruzioni sulla carta e installare l\'applicazione corretta
- L\'indirizzo è stato copiato con successo
- Nessuna connessione a Internet
- Riceviu
- da %s
- Inviau
- su %s
- Tangem
- Ok, ho capito!
+ Questa carta non è progettata per funzionare con Tangem
+ Accetta
+ Saldo: %s
+ Non hai fornito l\'accesso alla tua videocamera, modifica le tue impostazioni sulla privacy
+ Annulla
+ Rimuovere
+ Fatto
+ Errore
+ Commissione
+ Costi della rete
+ OK
+ Mantieni le modifiche
+ Invia
+ Con successo
+ Codice di accesso
+ Prima di scansionare la carta sarà necessario inserire il codice di accesso corretto
+ Mantenimento della carta
+ Questo meccanismo protegge dagli avvicinamenti senza contatto sulla carta. Attiva un ritardo tra la ricezione e l\'esecuzione di un comando. Dopo la prima transazione firmata, questo telefono verrà associato alla carta e le transazioni verranno firmate immediatamente.
+ Password
+ Dovrai inserire una password prima di eseguire qualsiasi comando che modifichi lo stato della carta.
+ Hash %s
+ ID carta
+ Valuta dell\'applicazione
+ Emittente
+ Firmato
+ Requisiti
+ Termini del servizio
+ Scansiona carta
+ Avvicina per modificare il codice di accesso
+ Avvicina per modificare la password
+ Avvicina per scansionare
+ Avvicina per firmare
+ Avvicina la carta
+ Crea portafoglio
+ Importo
+ Indirizzo
+ L\'indirizzo corrisponde all\'indirizzo del tuo portafoglio
+ Tag
+ Memo
+ Includi commissione
+ Insufficiente
+ Normale
+ Prioritario
+ Importo totale
+ Totale
+ Sarà inviato %1$s e %2$s
+ ≈ %1$s (inc. commissione: %2$s)
+ Sarà inviato %s
+ La transazione è stata firmata con successo e inviata al nodo blockchain. Il saldo del portafoglio verrà aggiornato dopo un po\' di tempo
+ Indirizzo non valido
+ Tangem Twin
+ WalletConnect
+ L\'indirizzo è stato copiato con successo
+ Nessuna connessione a Internet
+ Tangem
+ Ok, ho capito!
diff --git a/core/res/src/main/res/values-ru/strings-blockchain.xml b/core/res/src/main/res/values-ru/strings-blockchain.xml
index c58315db3f..f589ed1f9f 100644
--- a/core/res/src/main/res/values-ru/strings-blockchain.xml
+++ b/core/res/src/main/res/values-ru/strings-blockchain.xml
@@ -6,9 +6,9 @@
Из-за ограничений %1$s в одну транзакцию может поместиться только %2$d UTXO. Это означает, что вы можете отправить только %3$s или меньше. Вам нужно уменьшить сумму.
Недостаточно средств для совершения транзакции. Пожалуйста, пополните свой аккаунт.
Произошла ошибка. Код: %s.
- Для создания аккаунта отправьте средства на этот адрес
Чтобы использовать сеть %1$s, вы должны оплатить резерв аккаунта (%2$s %3$s), который блокируется и не используется в вашем балансе
Аккаунт получателя не активирован. Отправьте %s или более для активации аккаунта.
+ Для создания аккаунта отправьте средства на этот адрес
Минимальная сумма: %s
Сдача слишком мала
Неверная комиссия
diff --git a/core/res/src/main/res/values-ru/strings.xml b/core/res/src/main/res/values-ru/strings.xml
index ea038372cd..b4bd309592 100644
--- a/core/res/src/main/res/values-ru/strings.xml
+++ b/core/res/src/main/res/values-ru/strings.xml
@@ -1,753 +1,696 @@
- Добавить токен
- Валюты
- Отправляйте только %1$s (%2$s) в сети %3$s на этот адрес. Использование другой сети может привести к утрате средств.
- Спасибо за ваш отзыв
- Отправлено успешно
- Обратиться в поддержку
- Отправить отзыв
- Эта функция недоступна в демонстрационном режиме
- Не удалось отправить письмо
- Причина: %s
- Не могу отправить транзакцию
- Выбранный кошелёк не поддерживает сеть %1$s
- Для активации криптографии сети %1$s необходимо сбросить кошелек до заводских настроек. Пожалуйста, выведите свои средства, чтобы не потерять их, после сброса доступ к текущему кошельку будет невозможен.
- Токены в сети %1$s не поддерживаются этой картой из-за ограничений прошивки.
- У вас возникли трудности со сканированием карты?
- Эта карта не предназначена для работы с этим приложением
- Перейдите в настройки, чтобы включить биометрическую аутентификацию в приложении Tangem
- Включите биометрическую аутентификацию
- Все сохраненные коды доступа будут удалены. Вам потребуется вводить код доступа при работе с кошельком.
- При отключении функции сохранения кошелька все ранее сохраненные кошельки будут удалены из приложения.
- Сохранение кода доступа
- Подключите функцию хранения кодов доступа от карт на телефоне в зашифрованном виде, и при работе с картой вместо кода доступа будет запрашиваться биометрическая аутентификация.
- Cохранение кошелька
- Подключите функцию привязки карты в приложении, а также возможность биометрической аутентификации. Подпись транзакции все так же потребует карту.
- Тёмная
- Светлая
- Как в системе
- Тема
- Настройки приложения
- Чтобы скрыть или показать баланс, просто поверните ваше устройство вниз или отключите опцию его в разделе \"Настройки\"
- Больше не показывать
- Понятно
- Балансы скрыты
- Пожалуйста, отсканируйте карту
- Пожалуйста, попробуйте снова через 30 секунд или отсканируйте карту
- Слишком много попыток
- Вы отключили биометрическую аутентификацию на вашем телефоне и не сможете сохранять кошельки в приложении. Для сохранения кошельков, пожалуйста, включите функцию биометрической аутентификации в настройках телефона.
- Начать резервное копирование
-
- - %d карта
- - %d карты
- - %d карт
- - %d карт
-
- Отключите эту опцию, если не хотите, чтобы эта карта использовалась для сброса кодов доступа на другие карты этого кошелька. Обратите внимание, сброс кода также не будет доступен на этой карте.
- Использовать эту карту для сброса кода доступа на других картах в этом кошельке
- Отключить возможность сброса кода доступа на этой карте или других картах этого кошелька
- Восстановление кода доступа
- Сбросить
- Вы уверены, что хотите это сделать?
- Смена кода доступа
- Код доступа будет изменен только на данной карте
- Заводские настройки
- Тип безопасности
- Выбранный способ защиты приложения
- Настройки карты
- Tangem Bot
- Чат
- Отправить логи
- Принять
- Доступ запрещен
- Добавить
- Применить
- Одобрение
- Внимание
- Баланс: %s
- Баланс
- биометрическую аутентификацию
- биометрией
- Купить
- Перейти на %1$s
- Вы не предоставили доступ к камере, пожалуйста, измените настройки конфиденциальности.
- Отмена
- Закрыть
- Продолжить
- Копировать
- Скопировать адрес
- Создать
- Удалить
- Отключено
- Не нравится
- Готово
- Включить
- Включено
- Ошибка
- Обменять
- Обозреватель
- Посмотреть историю
- Посмотреть историю транзакций
- Обозреватель
- Комиссия
- Сетевые комиссии – это плата пользователя за обработку и подтверждение транзакций. Размер комиссии зависит от нагрузки на сеть, объема транзакции и приоритета исполнения. %s
- Свое
- Быстро
- По рынку
- Медленно
- Скорость и комиссия
- Получить адреса
- К провайдеру
- Импортировать
- Нравится
- Заблокирован
- Основная сеть
- Далее
- Нет
- Нет адреса
- Нет данных
- OK
- Основная карта
- Вставить
- Подробнее
- Получить
- Отклонить
- Перезагрузить
- Переименовать
- Сохранить изменения
- Искать
- Поиск токенов
- Seed-фраза
- Выберите действие
- Продать
- Отправить
- Сервер недоступен, повторите попытку позднее
- Поделиться
- Подписать
- Подписать и отправить
- Стейкинг
- Начать
- Отправить
- Успешно
- Поддержка
- Обмен
- условия участия
- Ошибка транзакции
- Транзакции
- Перевод
- Я понял
- Недоступно
- Произошла ошибка. Пожалуйста, попробуйте снова.
- Да
- Адрес контракта скопирован!
- Доступные сети
- Добавить токен
- Адрес контракта
- Адрес контракта некорректен
- Путь деривации некорректен
- Пожалуйста, выберите сеть
- Обязательное поле
- Количество знаков после запятой должно быть корректным числом не больше %d
- Своя деривация
- Например m/00\'/0000\'/0\'/0/0
- Введите свою деривацию
- Знаков после запятой
- Путь деривации
- По умолчанию
- Деривация по BIP44
- Введенный путь деривации некорректен
- Например, USD Coin
- Название токена
- Не выбрано
- Сеть
- Сеть
- Вы можете добавить токен в ручную, если он не поддерживается Tangem
- Например, USDC
- Символ
- Символ токена
- Этот токен/сеть уже находится в вашем списке
- Токены могут быть созданы кем угодно. Остерегайтесь мошеннических токенов, они могут ничего не стоить
- Остерегайтесь мошеннических токенов, они могут ничего не стоить
- Токены могут быть созданы кем угодно
- Чат
- Код доступа
- Перед сканированием карты вам нужно будет ввести правильный код доступа.
- Задержка сканирования
- Этот механизм защищает карту от бесконтактных атак. Между сканированием карты и выполнением команды будет добавлена задержка.
- Пароль
- Перед выполнением любой команды, влекущей за собой изменение состояния карты, вам необходимо будет ввести пароль.
- Реферальная программа
- Переверните экран вашего устройства вниз, чтобы быстро скрыть и отобразить балансы
- Privacy policy
- %s хэшей
- Номер карты
- Обратиться в поддержку
- Добавить еще карты
- Валюта приложения
- Скрывать балансы жестом переворота
- Эмитент
- Подписано
- Подробности
- Проверьте подключение с интернетом или переключитесь на другую сеть
- Условия использования
- К сожалению, текущая версия приложения не готова к работе с этой картой, проверьте наличие обновлений
- Вы использовали карту от другого кошелька. Приложите карту, связанную с этим кошельком.
- Мои токены
- У вас нет добавленных токенов. Добавьте токены для обмена
- Токены не найдены. Пожалуйста, попробуйте другой запрос
- Недоступен для обмена с %s
- Кроме того, в курс обмена включена комиссия сети за отправку обмененных средств на ваш адрес
- Статус
- Провайдеры проводят транзакции, обеспечивая плавный и эффективный обмен токенами
- Выберите провайдера
- Курс обмена
- Больше провайдеров на подходе.\nСледите за обновлениями!
- Чтобы вернуть ваши деньги, посетите сайт провайдера
- Операция не выполнена провайдером
- Посетите сайт провайдера для проверки
- Провайдер запрашивает прохождение верификации
- Отменен
- Подтверждено
- Подтверждение
- Подтверждение...
- Обменяно
- Обмен
- Обмен...
- Неудачно
- Депозит получен
- Ожидание депозита
- Ожидаем пополнения...
- Возвращено
- Отправляем
- Отправка средств...
- Отправлено
- Верифицировано
- Требуется верификация
- Список токенов в вашем кошельке
- Получение наилучших курсов...
- Плавающая ставка
- К провайдеру
- Пользуясь сервисом вы соглашаетесь с %s
- Пользуясь сервисом вы соглашаетесь с %1$s и %2$s
- Политикой конфиденциальности
- Провайдер
- Лучший курс
- Доступно с %s
- Доступно до %s
- Недоступно для этой пары
- Требуется разрешение
- Условиями использования
- Предоставлено
- Информация ниже не является обязательной. Вы можете стереть её, если хотите.
- Расскажите, каких функций вам не хватает, и мы постараемся вам помочь.
- Скажите, пожалуйста, какая у вас карта?
- Привет, команда поддержки,
- Пожалуйста, расскажите нам больше о вашей проблеме. Каждая маленькая деталь может помочь.
- Мои предложения
- Не могу отсканировать карту
- Обращение в поддержку
- Обращение в поддержку Tangem
- Не могу отправить транзакцию
- Купить
- Сканировать
- Чтобы изменить код доступа, приложите карту как показано выше и не убирайте до окончания операции
- Чтобы изменить пароль, приложите карту как показано выше и не убирайте до окончания операции
- Чтобы создать кошелек, приложите карту как показано выше и не убирайте до окончания операции
- Чтобы сбросить настройки до заводских, приложите карту как показано выше и не убирайте до окончания операции
- Приложите, чтобы отсканировать
- Чтобы подписать транзакцию, приложите карту как показано выше и не убирайте до окончания операции
- Приложите, чтобы подписать
- Приложите карту
- Внутренняя ошибка: не удается найти менеджер кошельков
- Вы обновили данные биометрии, отсканируйте свою карту для входа
- Чтобы начать отслеживать свои криптоактивы и транзакции, добавьте токены
- Вы успешно прошли все уроки и теперь можете получить 1INCH токены
-
- - Пройдите 3 урока и получите %d 1INCH токен на свой кошелек
- - Пройдите 3 урока и получите %d 1INCH токена на свой кошелек
- - Пройдите 3 урока и получите %d 1INCH токенов на свой кошелек
- - Пройдите 3 урока и получите %d 1INCH токенов на свой кошелек
-
- Управление токенами
- Баланс
- В сумме учтены не все монеты
- 1INCH токены будут зачислены на адрес вашего кошелька в сети %s в течение 48 часов
- Чтобы получить доступ ко всем сетям, вам необходимо отсканировать карту
- Отсканируйте карту
- Обменивайте свои токены с %1$s комиссии провайдера через Changelly с %2$s по %3$s февраля.
- Обмен с Changelly, %s комиссии
- Токены
- Добавить
- Изменить
- Основная сеть
- Не основной или основной блокчейн, на котором размещен токен
- Не основные сети
- Сети
- Выберите сети
- Кошелек
- Не удалось найти этот токен, вы можете добавить его вручную.
-
- - %1$d из %2$d кошелька
- - %1$d из %2$d кошельков
- - %1$d из %2$d кошельков
- - %1$d из %2$d кошельков
-
- например Bitcoin
- Рыночная капитализация
- Выбранный токен не доступен в кошельке на данный момент. Но не переживайте, вы можете выразить свой интерес проголосовав за его добавление.
- Голосовать
- Выберите кошелек
- Кошелёк не поддерживает более одной сети
- Вам необходимо установить единый код доступа для защиты всех ваших карт
- Защита
- Позже вы сможете установить индивидуальный код доступа для каждой карты
- Персонализация
- Код доступа можно восстановить с помощью привязанной карты. Не храните все карты в одном месте
- Восстановление
- Выберите любое слово, фразу или число в качестве кода доступа
- Создайте код доступа
- Повторно введите код доступа
- Код доступа должен состоять не менее чем из 4 символов.
- Введенные коды доступа не совпадают
- Необходимо повторить операцию, при этом карта будет сброшена к заводским настройкам
- Ошибка активации
- Вы добавили одну резервную карту. После того, как процесс будет завершен, Вы больше не сможете добавить карт. Если у Вас есть еще одна карта, добавьте ее в резервную копию. Хотите продолжить?
- Процесс резервного копирования почти завершен. Вы не можете выйти из него сейчас.
- Добавить резервную карту
- Сканировать карту #%d
- Создать резервную копию
- Сканировать основную карту
- Запросить
- Перейти к моему кошельку
- Завершение бэкапа
- Установить Код доступа
- Получить криптовалюту
- Сканировать основную карту
- Пропустить
- Как это работает?
- Давайте сгенерируем все ключи на вашей карте и создадим безопасный кошелек
- Создать кошелек
- Создать кошелек
- Другие опции
- Ваши ключи будут надежно сгенерированы внутри карты. Никакой seed-фразы, а это значит, что никто не может экспортировать или украсть ее.
- Cоздавайте ключи приватно
- Ваша карта активирована и готова к использованию
- Успешно!
- В этом случае вам будет необходимо начать процесс заново.
- Вы хотите выйти из процесса активации?
- Подготовка
- Другой кошелек уже был создан на карте, которую вы пытаетесь добавить. Хотите сбросить его и использовать карту для бэкапа?
- Код доступа
- Подключиться
- Резервная копия
- Прочитать о seed-фразе
- Запишите эти 12 слов в порядке, указанном ниже, и сохраните их в надежном месте.
- Ваша seed-фраза
- Чтобы импортировать кошелек, введите seed-фразу в поле ниже
- Создать seed-фразу
- Импорт кошелька
- Seed-фраза — это набор слов, который дает возможность восстановить кошелек. В отличие от ключей, сгенерированных картой, seed-фраза не защищена и может быть скопирована и украдена. Используйте этот вариант на свой страх и риск.
- Использовать seed-фразу
- Неверная seed-фраза. Пожалуйста, проверьте порядок слов.
- Неверная seed-фраза. Пожалуйста, проверьте орфографию.
- Устаревший
- Чтобы проверить, правильно ли вы записали seed-фразу, введите 2-е, 7-е и 11-е слова
- Итак, проверим
- Для начала работы просто запросите начисление wxDAI на свой кошелек
- Это займет несколько секунд
- Чтобы начать процесс резервного копирования, добавьте одну или две резервные карты.
- Вы можете добавить еще одну карту или завершить процесс резервного копирования
- Подготовьте резервную карту с номером %s
- Отсканируйте основную карту, чтобы начать процесс резервного копирования.
- Подготовьте основную карту с номером %s
- Поздравляем! Ваша платежная крипто карта теперь активирована!
- Ваша карта настроена и готова к использованию.
- Добавлено максимальное количество карт. Завершите процесс резервного копирования.
- Активация карты
- Резервная карта #%d
- Запросить %s
- Запрашивается
- Нет резервных карт
- Добавлена одна резервная карта
- Код доступа
- Подготовьте свою карту
- Добавлены две резервные карты
- Пополните кошелек на любую сумму, чтобы начать пользоваться картой
- Пополните кошелек более чем на %1$s %2$s, чтобы начать пользоваться картой
- Купить криптовалюту
- Показать адрес кошелька
- Активация кошелька
- Процесс связывания карт частично завершен. Вы не можете выйти из него сейчас.
- Если процесc создания кошелька каким-либо образом прервется, вам придется начинать сначала
- Вы можете сделать резервную копию своих ключей на одной или двух других пустых картах Wallet.
- Код доступа можно восстановить с помощью одной из резервных карт.
- Все резервные карты являются полнофункциональными и содержат одинаковые ключи.
- Вы сможете установить код доступа для защиты своих кошельков.
- Резервная копия карты
- Восстановление кода доступа
- Идентичные карты
- Код доступа
- Группы
- По балансу
- Сортировка токенов
- Список
- %1$s %2$s адрес в сети %3$s
- %1$s (%2$s) в сети %3$s
- Отправляйте только %s на этот адрес. Использование другой сети может привести к утрате средств.
- Участвовать
- Не удалось загрузить информацию по реферальной программе. Пожалуйста, попробуйте позже.
- Не удалось загрузить информацию по реферальной программе. Код ошибки: %s. Пожалуйста, попробуйте позже.
- Грядущие выплаты
- Ваши друзья купили
- Меньше
- Больше
- Нет грядущих выплат
-
- - за %d кошелек
- - за %d кошелька
- - за %d кошельков
- - за %d кошельков
-
- Получите ^^%1$s^^ на ваш адрес в сети %2$s %3$s ^^спустя 30 дней^^ за каждый кошелек, который купит ваш друг
- Получите
- на ваш адрес в сети %1$s%2$s за каждый кошелек, который купит ваш друг
- Вы
- Получит
- при покупке кошелька на сайте tangem.com
- %s скидку
- Ваш друг
- Персональный код скопирован!
- Ваш персональный код
- Купи Tangem Wallet со скидкой!\n%s
- Приведи друга в Tangem
- Вы приняли
- Нажимая на эту кнопку, вы принимаете
- в реферальной программе
-
- - %d кошелек
- - %d кошелька
- - %d кошельков
- - %d кошельков
-
- Пожалуйста, удерживайте карту до завершения операции
- Сбросить карту
- Я понимаю, что после выполнения этого действия у меня больше не будет доступа к текущему кошельку
- Я понимаю, что не смогу этой картой восстановить пароль на остальных картах этого кошелька, если я его забуду
- Сброс к заводским настройкам приведет к полному удалению кошелька с выбранной карты. Вы не сможете восстановить текущий кошелек или использовать данную карту для восстановления кода доступа.
- Сброс к заводским настройкам приведет к полному удалению кошелька с выбранной карты. Вы не сможете восстановить текущий кошелек.
- У вас есть карта банка другой страны, а также вид на жительство или регистрация вне РФ?
- Карты банков РФ в данный момент не принимаются
- Войдите в приложение и следите за своим балансом без сканирования карты
- Доступ в приложение
- Использовать биометрию
- Для операций с вашим кошельком будет запрашиваться биометрия вместо кода доступа карты
- Код доступа
- Похоже, что у вас отключена биометрическая аутентификация, она необходима для сохранения кошельков
- Включите биометрическую аутентификацию
- Вы хотите использовать биометрию?
- Обратите внимание, что для совершения транзакции с вашими средствами по-прежнему потребуется ваша карта
- Ошибка сканирования карты. Попробуй снова.
- Сканировать
- Отсканируйте карту, чтобы изменить ее настройки. Изменения затронут только ту карту, которую вы отсканировали, и не повлияют на другие карты, привязанные к вашему кошельку.
- Приготовьте свою карту
- Уже содержится в введенном адресе
- Сумма
- Транзакция не выполнена
- Причина: %1$s\nКод: %2$s
- Недостаточно средств для покрытия комиссии сети. Вычесть комиссию из отправляемой сумму?
- Вычесть
- %1$s в %2$s
- Адрес
- Код назначения
- Введите адрес
- Адрес совпадает с адресом кошелька
- Недопустимый Tag. Он не будет добавлен в транзакцию.
- Недопустимый Memo. Он не будет добавлен в транзакцию.
- Tag
- Memo
- Включая комиссию
- Низкая
- Нормальная
- Приоритетная
- Лимит газа
- Цена газа
- Цена газа влияет на скорость транзакции. При сильно низкой, транзакция может быть не обработана.
- Всё
- Максимальная сумма
- Комиссия не превысит
- Максимальная cумма комиссии
- Сетевая комиссия
- Сумма отправки будет уменьшена на %1$s для покрытия выбранного уровня комиссии. Получателю будет отправлено %2$s.
- Покрытие сетевой комиссии
- Недостаточно средств для перевода, так как сумма комиссии и сумма перевода в совокупности больше имеющегося баланса
- Недостаточно средств
- Увеличение комиссии
- Комиссия при переводе всего баланса выше. Для того, чтобы снизить комиссию Вы можете оставить 0.01.
- Установлена высокая комиссия
- Сумма комиссии в %s раз превышает рекомендованную. Убедитесь, что указанная комиссия верна.
- Включенная комиссия превышает сумму перевода, что приводит к отрицательному значению
- Недопустимая сумма
- Минимальная сумма отправки - %1$s. Пожалуйста, убедитесь, что остаток после отправки также не будет меньше %1$s.
- Адрес получателя не активирован. Пожалуйста, измените сумму отправки, чтобы продолжить.
- Сумма отправки не может быть менее %1$s
- Обратите внимание, что при определенных параметрах комиссии возможны задержки по вашей транзакции
- Возможны задержки по транзакции
- Лимит транзакции
- Из-за ограничений %1$s в одну транзакцию может поместиться только %2$s UTXO. Это означает, что вы можете отправить только %3$s или меньше. Вам нужно уменьшить сумму.
- Уменьшить до %s
- Уменьшить на %s
- Необязательное
- Последние
- Получатель
- Убедитесь, что вы отправляете средства на адрес кошелька %s. Ошибки могут привести к потере ваших токенов
- Мемо/ Код назначения - это код, разделяющий транзакции к общему получателю в сети криптовалют. Внимание: отсутствие мемо может привести к потере средств.
- Мои кошельки
- Отправка %s
- Всего
- %1$s и %2$s будет отправлено
- ≈ %1$s (вкл. комиссию: %2$s)
- %s будет отправлено
- Транзакция успешно подписана и отправлена в блокчейн. Баланс будет обновлен через некоторое время
- Неверный адрес
- Купить сейчас
- У меня есть промо-код…
- Tangem Wallet
- Другие способы оплаты
- Сделать предзаказ
- Итого
- Держите свои криптосбережения в безопасности. Приватные ключи надежно хранятся на карте.
- Революционный аппаратный кошелек
- До трех карт с одним кошельком
- До
- трех карт
- с одним кошельком
- Все ключи в безопасности
- Аппаратный кошелек для ваших биткоинов, эфира и многих других валют одновременно — все в одной карте
- Тысячи криптовалют
- Используйте его на ходу, в любом месте, в любое время. Без проводов и батареек. Как только понадобится крипта, просто приложите карту к телефону.
- Кошелек для каждого
- Пройдите 3 урока, получите скидку на покупку Tangem Wallet и 1INCH токены
- Пройти обучение
- Встречайте Tangem
- Обменивайте, покупайте NFT, получайте займы и делайте вклады в более чем 100 различных децентрализованных сервисах
- Поддержка Web 3.0
- Подтверждения считаются отраслевым стандартом для всех децентрализованных бирж и защищают ваш кошелек от доступа со стороны смарт-контракта без вашего разрешения. По замыслу смарт-контракты не могут получить доступ к вашим токенам, если вы не одобрите доступ со своей стороны. «Разблокируя» свои токены, вы даете смарт-контракту 1inch разрешение тратить ваши активы. Майнеры сети получают компенсацию за газ (оплачиваемый вами) за запись этого действия в блокчейне. Как только разрешение будет предоставлено, вы сможете обменять свой токен.
- Подтвердить
- Ошибка: %s
- Вы отправляете
- Дать разрешение
- Обмен этой суммы выбранных токенов может вызвать значительные колебания цены и уменьшить получаемую сумму.
- Недостаточно средств
- Комиссии
- В сумму включено: \n• комиссия провайдера сервиса\n• комиссия сети за отправку %s от биржи обратно на адрес пользователя
- В сумму включена комиссия провайдера сервиса.
- Сумма отправки будет уменьшена для покрытия выбранного уровня комиссии
- Недостаточно средств для оплаты комиссии на вашем %1$s кошельке для создания транзакции. Сначала пополните свой %2$s кошелек.
- Транзакция в процессе…
- Подождите
- Подтвердить
- Текущая транзакция
- Комиссия сети за одобрение токена будет взиматься за подтверждение того, что именно вы разрешаете использовать ваш токен для обмена.
- Дать разрешение
- Укажите лимит доступа к выбранному токену
- Количество %s
- Отправитель
- Ваш кошелек
- Чтобы продолжить, вам нужно разрешить смарт-контракту 1inch использовать ваш %s
- Безлимитно
- Открыть в обозревателе
- В процессе
- Обменять
- Обмен %s на
- Котировки включают дополнительную комиссию Tangem в размере %s. Это помогает нам предоставлять первоклассный продукт.
- Вы получите
- Другие токены
- Выберите токен
- Ваши токены
- не доступен
- Балансы скрыты
- Балансы показаны
- Отменить
-
- - %d токен
- - %d токена
- - %d токенов
- - %d токенов
-
- Скрыть
- Вы скрываете токен с главного экрана, но в любой момент сможете добавить его обратно через страницу управления токенами.
- Скрыть %s
- Скрыть токен
- %1$s токен в сети %%image%% %2$s
- Токен %1$s является основной валютой в сети %2$s и не может быть скрыт до тех пор, пока у вас в списке есть другие токены этой сети.
- Невозможно скрыть %s
- Нет цены
- Обменивайте этот токен на другие с %1$s комиссии за обслуживание с %2$s по %3$s февраля.
- Обмен с Changelly, %s комиссии
- Обменять
- контракт: %s
- У вас еще нет транзакций
- Не удалось загрузить историю транзакций.\nНажмите на кнопку перезагрузки, чтобы обновить информацию.
- Несколько адресов
- История транзакций в настоящее время не поддерживается для этого блокчейна. Но не волнуйтесь, мы работаем над этим! А пока вы можете проверить ее в обозревателе.
- Операция
- от: %s
- на: %s
- В процессе…
- Вы отсканировали ту же карту. Для создания twin-кошелька вам необходимо отсканировать карту с номером %d
- Вы отсканировали не ту twin-карту. Пожалуйста, попробуйте отсканировать другую
- Это карта, которую вы держите в руках. У парной карты номер %s.\n\nОбе карты можно использовать для вывода средств из этого кошелька.
- Один кошелек. Две карты.
- Сканировать карту #%s
- Создание кошелька
- Отсканируйте twin-карту #%s
- Подготовка карты
- Tangem Twin
- Это действие необратимо. У вас не будет доступа к старому кошельку.
- Приложите twin-карту с номером %s и не убирайте до окончания операции
- Используйте %s или отсканируйте карту, чтобы получить доступ к своему кошельку
- Добавить новый кошелек
- Вы уверены, что хотите удалить этот кошелек?
- %d выбрано
- Произошла ошибка, пожалуйста, отсканируйте свою карту для входа
- Этот кошелек уже был сохранен, вы можете добавить другой
- Мультивалютные
- Имя кошелька
- Переименование кошелька
- Одновалютные
- Мои кошельки
- Разблокировать все
- Разблокировать все с %s
- История транзакций
- Блокчейн недоступен. Попробуйте позже.
- Баланс загружается…
- Отсканируйте карту
- Транзакция подтверждается…
- Подтвержденный баланс
- Действия
- Что отправить?
- Вы хотите купить или продать криптовалюту?
- Запрос на подпись сообщения.\n\n%s
- Dapp %1$s, запрос на\nподпись транзакции с BNB.\n\n%2$s
- Торговый ордер на %1$s\nЦена: %2$s\nСумма к получению: %3$s\nСумма к оплате: %4$s
- Детали транзакции:\nОт: %1$s\nК: %2$s\nСумма: %3$s
- Транзакция с BNB успешно подписана и отправлена в Dapp.
- Буфер обмена содержит код WalletConnect. Использовать скопированное значение или отсканировать QR-код
- Запрос на создание транзакции для %1$s\n%2$s\n\nСумма: %3$s\nКомиссия: %4$s\nВсего: %5$s\nБаланс: %6$s
- Невозможно отправить транзакцию. Недостаточно средств.
- Не удалось установить сессию WalletConnect. Пожалуйста, повторите попытку позже.
- Не все токены добавлены в ваш список. Пожалуйста, добавьте их в начале, а потом попробуйте снова. Недостающие токены: \n
- Не удалось подписать сообщение.\nПожалуйста, попробуйте еще раз
- Не удалось установить сессию WalletConnect за отведённое время. Пожалуйста, повторите попытку позже.
- Запрос на подключение через WalletConnect содержит неподдерживаемые блокчеины. Неподдерживаемые блокчеины:\n
- Cоединение с этим Dapp сервисом не может быть установлено из-за его технической реализации.
- Произошла непредвиденная ошибка. Сообщение ошибки: %s Попробуйте, пожалуйста, позже. Если проблема будет продолжать возникать - обратитесь в службу поддержки.
- Неверная карта выбрана в приложении Tangem
- Не удалось создать транзакцию из данных Dapp. Код: %s
- Произошла непредвиденная ошибка. Код ошибки: %d Попробуйте, пожалуйста, позже. Если проблема будет продолжать возникать - обратитесь в службу поддержки.
- Сообщение было успешно подписано и отправлено в Dapp
- Сеть %s не найдена. Пожалуйста, добавьте её и попробуйте заново.
- Нет открытых сессий WalletConnect
- Упс. Нет сессий.
- Не удалось создать пару WalletConnect: %1$s
- Вставить из буфера обмена
- Сообщение для %1$s:\n%2$s
- Запрос на открытие сессии для\n%1$s\n\nСЕТЬ: %2$s\n\nURL: %3$s
- Операция не может быть завершена.\n\nВы уже установили сеанс WalletConnect с этими параметрами.
- Сканировать новый код
- Эту карту нельзя использовать с WalletConnect.
- Сеть не поддерживается. Пожалуйста, выберите другую сеть.
- Выберите сеть
- Dapp не предоставил необходимые данные для открытия сессии WalletConnect
- Не удалось найти сессию для обработки запроса
- Сессии WalletConnect
- Подключение к dApps
- WalletConnect
- Транзакция успешно подписана и отправлена в Dapp
- Транзакция успешно подписана и отправлена в блокчейн
- Не удалось найти хэш транзакции
- Сеть %s
- Аккаунт не создан
- Эта карта не поддерживается
- Ваша карта Tangem предназначена для работы с другим приложением. Пожалуйста, ознакомьтесь с названием и инструкциями на вашей карте и установите правильное приложение.
- Рыночная цена %s
- за 24 часа
- Сеть %s
- Адрес скопирован в буфер обмена
- Нет соединения с интернетом
- Получение
- от %s
- Отправка
- к %s
- Tangem
- Используйте %s или отсканируйте карту, чтобы разблокировать доступ к вашему кошельку
- Можно лучше
- Узнать больше
- Нравится
- Понятно!
- Очень круто!
- Обновить
- Вы находитесь в режиме демо
- Демо режим включен
- Отсканированная вами карта является картой разработчика. Не используйте ее для создания своего кошелька.
- Не для пользователя!
- Cеть %1$s использует концепцию экзистенциального депозита. Если баланс вашего счета будет ниже %2$s, то он будет деактивирован, а средства на счете уничтожены.
- Для работы с сетью необходим депозит
- Обмен будет доступен после завершения %s транзакции
- У вас есть активная транзакция
- У вас в списке нет монет доступных для обмена с %s
- Нет доступных токенов для обмена
- Чтобы совершить транзакцию, вам необходимо внести немного %1$s %2$s
- Невозможно покрыть комиссию %s
- Cервис временно недоступен
- Пожалуйста, измените сумму для обмена
- Сумма для обмена должна быть не менее %s
- Сумма для обмена должна быть не более %s
- Возможно, данная карта - образец или подделка
- Ошибка проверки подлинности
- На этой карте осталось всего %s подписей. Вам следует вывести все ваши средства.
- Малое количество подписей
- Токены на разных сетях могут иметь разные адреса. Пожалуйста, убедитесь при переводе средств, что ваш адрес соответствует сети.
-
- - Используйте вашу карту, чтобы получить адрес для %d сети
- - Используйте вашу карту, чтобы получить адреса для %d сетей
- - Используйте вашу карту, чтобы получить адреса для %d сетей
- - Используйте вашу карту, чтобы получить адреса для %d сетей
-
- Некоторые адреса отсутствуют
- В данный момент сеть недоступна. Пожалуйста, попробуйте позже.
- Сеть недоступна
- Пополните ваш кошелек
- Ваш кошелек не имеет резервной копии. Проведите эту процедуру сейчас, чтобы защитить ваши активы.
- Резервная копия отсутствует
- Эта карта ранее использовалась для подписи транзакций. Если она получена от ненадежного источника, рассмотрите возможность вывода своих средств. Если это ваша карта, дополнительных действий не требуется.
- Карта уже подписывала транзакции
- Устройство несовместимо
- Ваш отзыв мотивирует нас сделать кошелек Tangem еще лучше
- Нравится Tangem?
- Необходима плата за аренду сети
- %1$s - это монета в сети %2$s. Для совершения транзакции %3$s, вам необходимо внести немного %4$s (%5$s), чтобы покрыть комиссию сети.
- Недостаточно %1$s для оплаты комиссии сети
- Отправка средств станет доступной после завершения транзакции %s
- Транзакция в обработке
- Сеть Solana взимает арендную плату в размере %1$s каждые 2 дня. Аккаунты, которые не могут позволить себе арендную плату, удаляются из сети. Пополните свой счет более чем на %2$s, чтобы не платить арендную плату.
- Некоторые сети в настоящее время недоступны. Пожалуйста, повторите попытку позже.
- Некоторые сети недоступны
- Это Testnet карта. Он не может обрабатывать транзакции и используется только в целях тестирования и разработки.
- Только для целей тестирования
- Отказаться
- Вы не закончили резервное копирование. Хотите продолжить?
- Да, возобновить
- Отказаться
- Если сейчас отказаться, то придётся сбрасывать карты до заводских настроек, чтобы начать заново
- Возобновить резервное копирование
- Это необратимое действие
- Войти с %s
- Сканировать карту
- Используйте %s или отсканируйте карту для входа в приложение
- C возвращением!
- Обмен через %s
- Данные провайдера. Сумма к получению может измениться в зависимости от рыночных условий.
- Статус обмена
+ Добавить токен
+ Валюты
+ Отправляйте только %1$s (%2$s) в сети %3$s на этот адрес. Использование другой сети может привести к утрате средств.
+ Обратиться в поддержку
+ Эта функция недоступна в демонстрационном режиме
+ Причина: %s
+ Не могу отправить транзакцию
+ Выбранный кошелёк не поддерживает сеть %1$s
+ Для активации криптографии сети %1$s необходимо сбросить кошелек до заводских настроек. Пожалуйста, выведите свои средства, чтобы не потерять их, после сброса доступ к текущему кошельку будет невозможен.
+ Токены в сети %1$s не поддерживаются этой картой из-за ограничений прошивки.
+ У вас возникли трудности со сканированием карты?
+ Эта карта не предназначена для работы с этим приложением
+ Подключите функцию комиссии по умолчанию и при формировании транзакции на отправку средств комиссия будет выставлена автоматически, а экран комиссии пропущен. Вы всегда сможете на него вернуться.
+ Перейдите в настройки, чтобы включить биометрическую аутентификацию в приложении Tangem
+ Включите биометрическую аутентификацию
+ Все сохраненные коды доступа будут удалены. Вам потребуется вводить код доступа при работе с кошельком.
+ При отключении функции сохранения кошелька все ранее сохраненные кошельки будут удалены из приложения.
+ Сохранение кода доступа
+ Подключите функцию хранения кодов доступа от карт на телефоне в зашифрованном виде, и при работе с картой вместо кода доступа будет запрашиваться биометрическая аутентификация.
+ Cохранение кошелька
+ Подключите функцию привязки карты в приложении, а также возможность биометрической аутентификации. Подпись транзакции все так же потребует карту.
+ Тёмная
+ Светлая
+ Как в системе
+ Тема
+ Настройки приложения
+ Чтобы скрыть или показать баланс, просто поверните ваше устройство вниз или отключите опцию его в разделе \"Настройки\"
+ Больше не показывать
+ Понятно
+ Балансы скрыты
+ Пожалуйста, отсканируйте карту
+ Пожалуйста, попробуйте снова через 30 секунд или отсканируйте карту
+ Слишком много попыток
+ Вы отключили биометрическую аутентификацию на вашем телефоне и не сможете сохранять кошельки в приложении. Для сохранения кошельков, пожалуйста, включите функцию биометрической аутентификации в настройках телефона.
+ Начать резервное копирование
+
+ - %d карта
+ - %d карты
+ - %d карт
+ - %d карт
+
+ Отключите эту опцию, если не хотите, чтобы эта карта использовалась для сброса кодов доступа на другие карты этого кошелька. Обратите внимание, сброс кода также не будет доступен на этой карте.
+ Использовать эту карту для сброса кода доступа на других картах в этом кошельке
+ Восстановление кода доступа
+ Сбросить
+ Вы уверены, что хотите это сделать?
+ Смена кода доступа
+ Код доступа будет изменен только на данной карте
+ Заводские настройки
+ Тип безопасности
+ Настройки карты
+ Принять
+ Доступ запрещен
+ Применить
+ Одобрение
+ Внимание
+ Баланс: %s
+ Баланс
+ биометрическую аутентификацию
+ биометрией
+ Купить
+ Перейти на %1$s
+ Вы не предоставили доступ к камере, пожалуйста, измените настройки конфиденциальности.
+ Отмена
+ Закрыть
+ Продолжить
+ Копировать
+ Скопировать адрес
+ Создать
+ Удалить
+ Отключено
+ Готово
+ Включить
+ Включено
+ Ошибка
+ Обозреватель
+ Посмотреть историю транзакций
+ Обозреватель
+ Комиссия
+ Сетевые комиссии – это плата пользователя за обработку и подтверждение транзакций. Размер комиссии зависит от нагрузки на сеть, объема транзакции и приоритета исполнения. %s
+ Свое
+ Быстро
+ По рынку
+ Медленно
+ Скорость и комиссия
+ Получить адреса
+ К провайдеру
+ Импортировать
+ Заблокирован
+ Основная сеть
+ Сетевая комиссия
+ Далее
+ Нет
+ Нет адреса
+ OK
+ Основная карта
+ Кодовая фраза
+ Подробнее
+ Получить
+ Отклонить
+ Перезагрузить
+ Переименовать
+ Сохранить изменения
+ Искать
+ Поиск токенов
+ Seed-фраза
+ Выберите действие
+ Продать
+ Отправить
+ Сервер недоступен, повторите попытку позднее
+ Поделиться
+ Подписать
+ Подписать и отправить
+ Начать
+ Отправить
+ Успешно
+ Поддержка
+ Обмен
+ условия участия
+ Ошибка транзакции
+ Транзакции
+ Перевод
+ Я понял
+ Произошла ошибка. Пожалуйста, попробуйте снова.
+ Недоступно
+ Да
+ Адрес контракта скопирован!
+ Доступные сети
+ Добавить токен
+ Адрес контракта
+ Адрес контракта некорректен
+ Пожалуйста, выберите сеть
+ Десятичное число должно быть действительным целым числом, до %li
+ Своя деривация
+ Например m/00\'/0000\'/0\'/0/0
+ Введите свою деривацию
+ Знаков после запятой
+ Путь деривации
+ По умолчанию
+ Деривация по BIP44
+ Введенный путь деривации некорректен
+ Например, USD Coin
+ Название токена
+ Не выбрано
+ Сеть
+ Сеть
+ Вы можете добавить токен в ручную, если он не поддерживается Tangem
+ Например, USDC
+ Символ
+ Символ токена
+ Этот токен/сеть уже находится в вашем списке
+ Токены могут быть созданы кем угодно. Остерегайтесь мошеннических токенов, они могут ничего не стоить
+ Остерегайтесь мошеннических токенов, они могут ничего не стоить
+ Токены могут быть созданы кем угодно
+ Чат
+ Код доступа
+ Перед сканированием карты вам нужно будет ввести правильный код доступа.
+ Задержка сканирования
+ Этот механизм защищает карту от бесконтактных атак. Между сканированием карты и выполнением команды будет добавлена задержка.
+ Пароль
+ Перед выполнением любой команды, влекущей за собой изменение состояния карты, вам необходимо будет ввести пароль.
+ Реферальная программа
+ Переверните экран вашего устройства вниз, чтобы быстро скрыть и отобразить балансы
+ %s хэшей
+ Номер карты
+ Обратиться в поддержку
+ Добавить еще карты
+ Валюта приложения
+ Скрывать балансы жестом переворота
+ Эмитент
+ Подписано
+ Подробности
+ Проверьте подключение с интернетом или переключитесь на другую сеть
+ Условия использования
+ Вы использовали карту от другого кошелька. Приложите карту, связанную с этим кошельком.
+ Мои токены
+ У вас нет добавленных токенов. Добавьте токены для обмена
+ Недоступен для обмена с %s
+ Предоставлено
+ Статус
+ Tangem предоставляет доступ к обмену через сторонних поставщиков в соответствии с их правилами
+ Выберите провайдера
+ Произошла ошибка. Код: %s
+ К сожалению обмен указанной пары через выбранного провайдера на данный момент не возможен. Попробуйте совершить обмен позже. (Код: %s)
+ Выбранный провайдер не доступен для обмена. Попробуйте позже. (Код: %s)
+ В данный момент обмен не возможен. Попробуйте позже. (Код: %s)
+ Курс обмена
+ Обмен через %s
+ Чтобы вернуть ваши деньги, посетите сайт провайдера
+ Операция не выполнена провайдером
+ Посетите сайт провайдера для проверки
+ Провайдер запрашивает прохождение верификации
+ Отменен
+ Подтверждено
+ Подтверждение
+ Подтверждение...
+ Обменяно
+ Обмен
+ Обмен...
+ Неудачно
+ Депозит получен
+ Ожидание депозита
+ Ожидаем пополнения...
+ Возвращено
+ Отправляем
+ Отправка средств...
+ Отправлено
+ Данные провайдера. Сумма к получению может измениться в зависимости от рыночных условий.
+ Статус обмена
+ Требуется верификация
+ Список токенов в вашем кошельке
+ Получение наилучших курсов...
+ Плавающая ставка
+ Пользуясь сервисом, вы соглашаетесь с %s
+ Пользуясь сервисом, вы соглашаетесь с %1$s и %2$s
+ Больше провайдеров на подходе.\nСледите за обновлениями!
+ Политикой конфиденциальности
+ Провайдер
+ Лучший курс
+ Доступно до %s
+ Доступно с %s
+ Недоступно для этой пары
+ Требуется разрешение
+ Условиями использования
+ Токены не найдены. Пожалуйста, попробуйте другой запрос
+ ID транзакции скопирован
+ Информация ниже не является обязательной. Вы можете стереть её, если хотите.
+ Расскажите, каких функций вам не хватает, и мы постараемся вам помочь.
+ Скажите, пожалуйста, какая у вас карта?
+ Привет, команда поддержки,
+ Пожалуйста, расскажите нам больше о вашей проблеме. Каждая маленькая деталь может помочь.
+ Мои предложения
+ Не могу отсканировать карту
+ Обращение в поддержку
+ Обращение в поддержку Tangem
+ Не могу отправить транзакцию
+ Купить
+ Сканировать
+ Чтобы изменить код доступа, приложите карту как показано выше и не убирайте до окончания операции
+ Чтобы изменить пароль, приложите карту как показано выше и не убирайте до окончания операции
+ Чтобы создать кошелек, приложите карту как показано выше и не убирайте до окончания операции
+ Приложите, чтобы отсканировать
+ Приложите, чтобы подписать
+ Приложите карту
+ Вы обновили данные биометрии, отсканируйте свою карту для входа
+ Чтобы начать отслеживать свои криптоактивы и транзакции, добавьте токены
+ Управление токенами
+ Чтобы получить доступ ко всем сетям, вам необходимо отсканировать карту
+ Отсканируйте карту
+ Обменивайте свои токены с %1$s комиссии провайдера через Changelly с %2$s по %3$s февраля.
+ Обмен с Changelly, %s комиссии
+ Токены
+ Добавить
+ Изменить
+ Рыночная капитализация
+ Основная сеть
+ Не основной или основной блокчейн, на котором размещен токен
+ Не основные сети
+ Выберите сети
+ Кошелек
+ Не удалось найти этот токен, вы можете добавить его вручную.
+
+ - %1$d из %2$d кошелька
+ - %1$d из %2$d кошельков
+ - %1$d из %2$d кошельков
+ - %1$d из %2$d кошельков
+
+ например Bitcoin
+ Выбранный токен не доступен в кошельке на данный момент. Но не переживайте, вы можете выразить свой интерес проголосовав за его добавление.
+ Голосовать
+ Выберите кошелек
+ Кошелёк не поддерживает более одной сети
+ Вам необходимо установить единый код доступа для защиты всех ваших карт
+ Защита
+ Позже вы сможете установить индивидуальный код доступа для каждой карты
+ Персонализация
+ Код доступа можно восстановить с помощью привязанной карты. Не храните все карты в одном месте.
+ Восстановление
+ Выберите любое слово, фразу или число в качестве кода доступа
+ Создайте код доступа
+ Введите код доступа еще раз, чтобы избежать ошибки
+ Повторно введите код доступа
+ Код доступа должен состоять не менее чем из 4 символов.
+ Введенные коды доступа не совпадают
+ Необходимо повторить операцию, при этом карта будет сброшена к заводским настройкам
+ Ошибка активации
+ Вы добавили одну резервную карту. После того, как процесс будет завершен, Вы больше не сможете добавить карт. Если у Вас есть еще одна карта, добавьте ее в резервную копию. Хотите продолжить?
+ Процесс резервного копирования почти завершен. Вы не можете выйти из него сейчас.
+ Кодовая фраза — это расширенная функция безопасности, которую используют криптокошельки. Она добавляет дополнительное слово или фразу по вашему выбору к уже существующей seed - фразе, чтобы разблокировать совершенно новый набор адресов.
+ Добавить резервную карту
+ Сканировать карту #%d
+ Создать резервную копию
+ Сканировать основную карту
+ Перейти к моему кошельку
+ Завершение бэкапа
+ Получить криптовалюту
+ Сканировать основную карту
+ Пропустить
+ Как это работает?
+ Давайте сгенерируем все ключи на вашей карте и создадим безопасный кошелек
+ Создать кошелек
+ Создать кошелек
+ Другие опции
+ Ваши ключи будут надежно сгенерированы внутри карты. Никакой seed-фразы, а это значит, что никто не может экспортировать или украсть ее.
+ Cоздавайте ключи приватно
+ Ваша карта активирована и готова к использованию
+ Успешно!
+ В этом случае вам будет необходимо начать процесс заново.
+ Вы хотите выйти из процесса активации?
+ Подготовка
+ Другой кошелек уже был создан на карте, которую вы пытаетесь добавить. Если на нем есть средства, пожалуйста сначала выведите их, а затем сделайте сброс до заводских настроек и используйте как резервную.
+ Резервная копия
+ Прочитать о seed-фразе
+
+
+ - Запишите эти %d слова в порядке, указанном ниже, и сохраните их в надежном месте.
+ - Запишите эти %d слов в порядке, указанном ниже, и сохраните их в надежном месте.
+ - Запишите эти %d слов в порядке, указанном ниже, и сохраните их в надежном месте.
+
+ Ваша seed-фраза
+
+
+ - %d слова
+ - %d слов
+ - %d слов
+
+ Чтобы импортировать кошелек, введите seed-фразу в поле ниже
+ Создать seed-фразу
+ Импорт кошелька
+ Seed-фраза — это набор слов, который дает возможность восстановить кошелек. В отличие от ключей, сгенерированных картой, seed-фраза не защищена и может быть скопирована и украдена. Используйте этот вариант на свой страх и риск.
+ Использовать seed-фразу
+ Неверная seed-фраза. Пожалуйста, проверьте порядок слов.
+ Неверная seed-фраза. Пожалуйста, проверьте орфографию.
+ Устаревший
+ Чтобы проверить, правильно ли вы записали seed-фразу, введите 2-е, 7-е и 11-е слова
+ Итак, проверим
+ Чтобы начать процесс резервного копирования, добавьте одну или две резервные карты.
+ Вы можете добавить еще одну карту или завершить процесс резервного копирования
+ Подготовьте резервную карту с номером %s
+ Отсканируйте основную карту, чтобы начать процесс резервного копирования.
+ Подготовьте основную карту с номером %s
+ Ваша карта настроена и готова к использованию.
+ Добавлено максимальное количество карт. Завершите процесс резервного копирования.
+ Активация карты
+ Резервная карта #%d
+ Нет резервных карт
+ Добавлена одна резервная карта
+ Подготовьте свою карту
+ Добавлены две резервные карты
+ Пополните кошелек на любую сумму, чтобы начать пользоваться картой
+ Пополните кошелек более чем на %1$s %2$s, чтобы начать пользоваться картой
+ Купить криптовалюту
+ Показать адрес кошелька
+ Активация кошелька
+ Процесс связывания карт частично завершен. Вы не можете выйти из него сейчас.
+ Если процесc создания кошелька каким-либо образом прервется, вам придется начинать сначала
+ Вы можете сделать резервную копию своих ключей на одной или двух других пустых картах Wallet.
+ Код доступа можно восстановить с помощью одной из резервных карт.
+ Все резервные карты являются полнофункциональными и содержат одинаковые ключи.
+ Вы сможете установить код доступа для защиты своих кошельков.
+ Резервная копия карты
+ Восстановление кода доступа
+ Идентичные карты
+ Код доступа
+ Группы
+ По балансу
+ Сортировка токенов
+ Список
+ Выбрать из галереи
+ Нравится наше приложение?
+ %1$s (%2$s) в сети %3$s
+ Отправляйте только %s на этот адрес. Использование другой сети может привести к утрате средств.
+ Участвовать
+ Не удалось загрузить информацию по реферальной программе. Пожалуйста, попробуйте позже.
+ Не удалось загрузить информацию по реферальной программе. Код ошибки: %s. Пожалуйста, попробуйте позже.
+ Грядущие выплаты
+ Ваши друзья купили
+ Меньше
+ Больше
+ Нет грядущих выплат
+
+ - за %d кошелек
+ - за %d кошелька
+ - за %d кошельков
+ - за %d кошельков
+
+ Получите ^^%1$s^^ на ваш адрес в сети %2$s %3$s ^^спустя 30 дней^^ за каждый кошелек, который купит ваш друг
+ Вы
+ Получит
+ при покупке кошелька на сайте tangem.com
+ %s скидку
+ Ваш друг
+ Персональный код скопирован!
+ Ваш персональный код
+ Купи Tangem Wallet со скидкой!\n%s
+ Приведи друга в Tangem
+ Вы приняли
+ Нажимая на эту кнопку, вы принимаете
+ в реферальной программе
+
+ - %d кошелек
+ - %d кошелька
+ - %d кошельков
+ - %d кошельков
+
+ Сбросить карту
+ Я понимаю, что после выполнения этого действия у меня больше не будет доступа к текущему кошельку
+ Я понимаю, что не смогу этой картой восстановить пароль на остальных картах этого кошелька, если я его забуду
+ Сброс к заводским настройкам приведет к полному удалению кошелька с выбранной карты. Вы не сможете восстановить текущий кошелек или использовать данную карту для восстановления кода доступа.
+ Сброс к заводским настройкам приведет к полному удалению кошелька с выбранной карты. Вы не сможете восстановить текущий кошелек.
+ У вас есть карта банка другой страны, а также вид на жительство или регистрация вне РФ?
+ Карты банков РФ в данный момент не принимаются
+ Войдите в приложение и следите за своим балансом без сканирования карты
+ Доступ в приложение
+ Использовать биометрию
+ Для операций с вашим кошельком будет запрашиваться биометрия вместо кода доступа карты
+ Код доступа
+ Похоже, что у вас отключена биометрическая аутентификация, она необходима для сохранения кошельков
+ Включите биометрическую аутентификацию
+ Вы хотите использовать биометрию?
+ Обратите внимание, что для совершения транзакции с вашими средствами по-прежнему потребуется ваша карта
+ Сканировать
+ Отсканируйте карту, чтобы изменить ее настройки. Изменения затронут только ту карту, которую вы отсканировали, и не повлияют на другие карты, привязанные к вашему кошельку.
+ Приготовьте свою карту
+ Уже содержится в введенном адресе
+ Вычесть
+ Недостаточно средств для покрытия комиссии сети. Вычесть недостающую сумму для покрытия комиссии из отправляемой суммы?
+ Сумма комиссии в %@ раз превышает рекомендованную. Убедитесь, что указанная комиссия верна.
+ Вы указали комиссию ниже рекомендуемой, это может привести к задержке исполнения вашей транзакции. Продолжить?
+ Причина: %1$s\nКод: %2$s
+ Транзакция не выполнена
+ Сумма
+ %1$s, %2$s
+ Адрес
+ Код назначения
+ Введите адрес
+ Адрес совпадает с адресом кошелька
+ Недопустимый Tag. Он не будет добавлен в транзакцию.
+ Недопустимый Memo. Он не будет добавлен в транзакцию.
+ Tag
+ Memo
+ Включая комиссию
+ Низкая
+ Нормальная
+ Приоритетная
+ Проверьте своё интернет соединение
+ Информация о комиссии сети недоступна
+ Из
+ Лимит газа
+ Это максимальное количество газа, которое будет потрачено на выполнение транзакции или контракта. Лимит газа предотвращает неожиданные или неограниченные расходы при выполнении транзакции.
+ Цена газа
+ Это стоимость, которую вы готовы заплатить за каждую единицу газа. Чем выше цена газа, тем быстрее ваша транзакция будет обработана.
+ Всё
+ Максимальная сумма
+ Комиссия не превысит
+ Комиссия, которая будет взята за вашу транзакцию. Вы можете выставить своё собственное значение.
+ Допустим ввод только цифр
+ Сумма отправки будет уменьшена на %1$s для покрытия выбранного уровня комиссии. Получателю будет отправлено %2$s.
+ Покрытие сетевой комиссии
+ Недостаточно средств для перевода, так как сумма комиссии и сумма перевода в совокупности больше имеющегося баланса
+ Недостаточно средств
+ Аккаунт будет удален из блокчейна, если баланс упадет ниже экзистенциального депозита. Пожалуйста, убедитесь, что остаток после отправки будет не менее %s.
+ Экзистенциальный депозит
+ Сумма комиссии в %s раз превышает рекомендованную. Убедитесь, что указанная комиссия верна.
+ Установлена высокая комиссия
+ Комиссия при переводе всего баланса выше. Для того, чтобы снизить комиссию Вы можете оставить %s.
+ Комиссия увеличилась
+ Включенная комиссия превышает сумму перевода, что приводит к отрицательному значению
+ Недопустимая сумма
+ Минимальная сумма отправки - %1$s. Пожалуйста, убедитесь, что остаток после отправки также не будет меньше %2$s.
+ Адрес получателя не активирован. \nПожалуйста, измените сумму отправки, чтобы продолжить.
+ Сумма отправки не может быть менее %s
+ Уменьшить на %s
+ Уменьшить до %s
+ Обратите внимание, что при определенных параметрах комиссии возможны задержки по вашей транзакции
+ Возможны задержки по транзакции
+ Из-за ограничений %1$s в одну транзакцию может поместиться только %2$s UTXO. Это означает, что вы можете отправить только %3$s или меньше. Вам нужно уменьшить сумму.
+ Лимит транзакции
+ Необязательное
+ Пожалуйста, совместите свой QR-код с квадратом, чтобы отсканировать его. Убедитесь, что вы сканируете адрес в сети %s.
+ Последние
+ Получатель
+ Неверный адрес
+ Убедитесь, что вы отправляете средства на адрес кошелька %s. Ошибки могут привести к потере ваших токенов
+ Отправить
+ Мемо/ Код назначения - это код, разделяющий транзакции к общему получателю в сети криптовалют. Внимание: отсутствие мемо может привести к потере средств.
+ Мои кошельки
+ Это способ измерения комиссии за отправку биткоин-транзакции. Он указывает на количество самой маленькой единицы биткоина (сатоши) за каждый байт данных в транзакции. Чем выше это число, тем быстрее будет обработана транзакция сетью.
+ Отправка
+ Нажмите на любое поле, чтобы изменить его
+ Отправка %s
+ Вы отправляете %1$s, включая комиссию сети %2$s
+ Отправка %s
+ Всего
+ %1$s и %2$s будет отправлено
+ ≈ %1$s (вкл. комиссию: %2$s)
+ %s будет отправлено
+ Транзакция успешно подписана и отправлена в блокчейн. Баланс будет обновлен через некоторое время
+ Неверный адрес
+ Транзакция отправлена
+ Держите свои криптосбережения в безопасности. Приватные ключи надежно хранятся на карте.
+ Революционный аппаратный кошелек
+ До трех карт с одним кошельком
+ Все ключи в безопасности
+ Аппаратный кошелек для ваших биткоинов, эфира и многих других валют одновременно — все в одной карте
+ Тысячи криптовалют
+ Используйте его на ходу, в любом месте, в любое время. Без проводов и батареек. Как только понадобится крипта, просто приложите карту к телефону.
+ Кошелек для каждого
+ Встречайте Tangem
+ Обменивайте, покупайте NFT, получайте займы и делайте вклады в более чем 100 различных децентрализованных сервисах
+ Поддержка Web 3.0
+ В сумму включено: \n• комиссия провайдера сервиса\n• комиссия сети за отправку %s от биржи обратно на адрес пользователя
+ В сумму включена комиссия провайдера сервиса.
+ Комиссии
+ Подтверждения считаются отраслевым стандартом для всех децентрализованных бирж и защищают ваш кошелек от доступа со стороны смарт-контракта без вашего разрешения. По замыслу смарт-контракты не могут получить доступ к вашим токенам, если вы не одобрите доступ со своей стороны. «Разблокируя» свои токены, вы даете смарт-контракту 1inch разрешение тратить ваши активы. Майнеры сети получают компенсацию за газ (оплачиваемый вами) за запись этого действия в блокчейне. Как только разрешение будет предоставлено, вы сможете обменять свой токен.
+ Подтвердить
+ Вы отправляете
+ Дать разрешение
+ Обмен этой суммы выбранных токенов может вызвать значительные колебания цены и уменьшить получаемую сумму.
+ Недостаточно средств
+ Сумма отправки будет уменьшена для покрытия выбранного уровня комиссии
+ Подтвердить
+ Текущая транзакция
+ Комиссия сети за одобрение токена будет взиматься за подтверждение того, что именно вы разрешаете использовать ваш токен для обмена.
+ Дать разрешение
+ Укажите лимит доступа к выбранному токену
+ Количество %s
+ Чтобы продолжить, вам нужно разрешить смарт-контракту 1inch использовать ваш %s
+ Безлимитно
+ В процессе
+ Обменять
+ Вы получите
+ Выберите токен
+ не доступен
+ Балансы скрыты
+ Балансы показаны
+ Отменить
+ В данный момент покупка монеты %s недоступна. Но мы работаем над её добавлением.
+ У вас нет средств для отправки. Пополните счет, чтобы иметь возможность отправить с него средства.
+ Выбранная операция в данный момент недоступна. Попробуйте позже.
+ Обмен %s не доступен. Но мы работаем над его добавлением.
+ В данный момент продажа монеты %s недоступна. Но мы работаем над её добавлением.
+ Сгенерировать XPUB
+ Скрыть
+ Вы скрываете токен с главного экрана, но в любой момент сможете добавить его обратно через страницу управления токенами.
+ Скрыть %s
+ Скрыть токен
+ %1$s токен в сети %%image%% %2$s
+ Токен %1$s является основной валютой в сети %2$s и не может быть скрыт до тех пор, пока у вас в списке есть другие токены этой сети.
+ Невозможно скрыть %s
+ Обменивайте этот токен на другие с %1$s комиссии за обслуживание с %2$s по %3$s февраля.
+ Обмен с Changelly, %s комиссии
+ Обменять
+ контракт: %s
+ У вас еще нет транзакций
+ Не удалось загрузить историю транзакций.\nНажмите на кнопку перезагрузки, чтобы обновить информацию.
+ Несколько адресов
+ История транзакций в настоящее время не поддерживается для этого блокчейна. Но не волнуйтесь, мы работаем над этим! А пока вы можете проверить ее в обозревателе.
+ Операция
+ от: %s
+ на: %s
+ Вы отсканировали ту же карту. Для создания twin-кошелька вам необходимо отсканировать карту с номером %d
+ Вы отсканировали не ту twin-карту. Пожалуйста, попробуйте отсканировать другую
+ Это карта, которую вы держите в руках. У парной карты номер %s.\n\nОбе карты можно использовать для вывода средств из этого кошелька.
+ Один кошелек. Две карты.
+ Сканировать карту #%s
+ Создание кошелька
+ Отсканируйте twin-карту #%s
+ Подготовка карты
+ Tangem Twin
+ Это действие необратимо. У вас не будет доступа к старому кошельку.
+ Приложите twin-карту с номером %s и не убирайте до окончания операции
+ Используйте %s или отсканируйте карту, чтобы получить доступ к своему кошельку
+ Добавить новый кошелек
+ Вы уверены, что хотите удалить этот кошелек?
+ Произошла ошибка, пожалуйста, отсканируйте свою карту для входа
+ Этот кошелек уже был сохранен, вы можете добавить другой
+ Имя кошелька
+ Переименование кошелька
+ Разблокировать все
+ Разблокировать все с %s
+ Блокчейн недоступен. Попробуйте позже.
+ Отсканируйте карту
+ Запрос на подпись сообщения.\n\n%s
+ Dapp %1$s, запрос на\nподпись транзакции с BNB.\n\n%2$s
+ Торговый ордер на %1$s\nЦена: %2$s\nСумма к получению: %3$s\nСумма к оплате: %4$s
+ Детали транзакции:\nОт: %1$s\nК: %2$s\nСумма: %3$s
+ Буфер обмена содержит код WalletConnect. Использовать скопированное значение или отсканировать QR-код
+ Запрос на создание транзакции для %1$s\n%2$s\n\nСумма: %3$s\nКомиссия: %4$s\nВсего: %5$s\nБаланс: %6$s
+ Невозможно отправить транзакцию. Недостаточно средств.
+ Не удалось установить сессию WalletConnect. Пожалуйста, повторите попытку позже.
+ Не все токены добавлены в ваш список. Пожалуйста, добавьте их в начале, а потом попробуйте снова. Недостающие токены: \n
+ Не удалось подписать сообщение.\nПожалуйста, попробуйте еще раз
+ Не удалось установить сессию WalletConnect за отведённое время. Пожалуйста, повторите попытку позже.
+ Запрос на подключение через WalletConnect содержит неподдерживаемые блокчеины. Неподдерживаемые блокчеины:\n
+ Cоединение с этим Dapp сервисом не может быть установлено из-за его технической реализации.
+ Произошла непредвиденная ошибка. Сообщение ошибки: %s Попробуйте, пожалуйста, позже. Если проблема будет продолжать возникать - обратитесь в службу поддержки.
+ Неверная карта выбрана в приложении Tangem
+ Не удалось создать транзакцию из данных Dapp. Код: %s
+ Произошла непредвиденная ошибка. Код ошибки: %d Попробуйте, пожалуйста, позже. Если проблема будет продолжать возникать - обратитесь в службу поддержки.
+ Нет открытых сессий WalletConnect
+ Упс. Нет сессий.
+ Не удалось создать пару WalletConnect: %1$s
+ Вставить из буфера обмена
+ Сообщение для %1$s:\n%2$s
+ Запрос на открытие сессии для\n%1$s\n\nСЕТЬ: %2$s\n\nURL: %3$s
+ Операция не может быть завершена.\n\nВы уже установили сеанс WalletConnect с этими параметрами.
+ Сканировать новый код
+ Эту карту нельзя использовать с WalletConnect.
+ Сеть не поддерживается. Пожалуйста, выберите другую сеть.
+ Выберите сеть
+ Сессии WalletConnect
+ Подключение к dApps
+ WalletConnect
+ Рыночная цена %s
+ за 24 часа
+ Сеть %s
+ Адрес скопирован в буфер обмена
+ Нет соединения с интернетом
+ Tangem
+ Используйте %s или отсканируйте карту, чтобы разблокировать доступ к вашему кошельку
+ Пожалуйста, выведите все средства из этого кошелька, сбросьте его к заводским настройкам и создайте новый. Доступ к текущему кошельку будет утерян.
+ Ошибка активации
+ Можно лучше
+ Нравится
+ Понятно!
+ Очень круто!
+ Обновить
+ Вы находитесь в режиме демо
+ Демо режим включен
+ Отсканированная вами карта является картой разработчика. Не используйте ее для создания своего кошелька.
+ Не для пользователя!
+ Cеть %1$s использует концепцию экзистенциального депозита. Если баланс вашего счета будет ниже %2$s, то он будет деактивирован, а средства на счете уничтожены.
+ Для работы с сетью необходим депозит
+ Обмен будет доступен после завершения %s транзакции
+ У вас есть активная транзакция
+ Разрешение обмена в процессе и будет скоро завершено
+ Разрешение в процессе
+ Минимальная сумма обмена - %1$s. Пожалуйста, убедитесь, что остаток после обмена также не будет меньше %2$s.
+ У вас в списке нет монет доступных для обмена с %s
+ Нет доступных для обмена токенов
+ Чтобы совершить транзакцию, вам необходимо внести немного %1$s %2$s
+ Невозможно покрыть комиссию %s
+ Сумма получения не может быть менее %s
+ Cервис временно недоступен
+ Сумма для обмена должна быть не более %s
+ Сумма для обмена должна быть не менее %s
+ Пожалуйста, измените сумму для обмена
+ Возможно, данная карта - образец или подделка
+ Ошибка проверки подлинности
+ На этой карте осталось всего %s подписей. Вам следует вывести все ваши средства.
+ Малое количество подписей
+ Токены на разных сетях могут иметь разные адреса. Пожалуйста, убедитесь при переводе средств, что ваш адрес соответствует сети.
+
+ - Используйте вашу карту, чтобы получить адрес для %d сети
+ - Используйте вашу карту, чтобы получить адреса для %d сетей
+ - Используйте вашу карту, чтобы получить адреса для %d сетей
+ - Используйте вашу карту, чтобы получить адреса для %d сетей
+
+ Некоторые адреса отсутствуют
+ В данный момент сеть недоступна. Пожалуйста, попробуйте позже.
+ Сеть недоступна
+ Пополните ваш кошелек
+ Ваш кошелек не имеет резервной копии. Проведите эту процедуру сейчас, чтобы защитить ваши активы.
+ Резервная копия отсутствует
+ Эта карта ранее использовалась для подписи транзакций. Если она получена от ненадежного источника, рассмотрите возможность вывода своих средств. Если это ваша карта, дополнительных действий не требуется.
+ Карта уже подписывала транзакции
+ Ваш отзыв мотивирует нас сделать кошелек Tangem еще лучше
+ Нравится Tangem?
+ Необходима плата за аренду сети
+ %1$s - это монета в сети %2$s. Для совершения транзакции %3$s, вам необходимо внести немного %4$s (%5$s), чтобы покрыть комиссию сети.
+ Недостаточно %1$s для оплаты комиссии сети
+ Отправка средств станет доступной после завершения транзакции(-ий) в сети %s
+ Отправка средств станет доступной после завершения транзакции %s
+ Транзакция в обработке
+ Сеть Солана испытывает высокую нагрузку. Если Ваша транзакция не прошла в течение 2 минут, повторите её отправку.
+ Оповещение сети Солана
+ Сеть Solana взимает арендную плату в размере %1$s каждые 2 дня. Аккаунты, которые не могут позволить себе арендную плату, удаляются из сети. Пополните свой счет более чем на %2$s, чтобы не платить арендную плату.
+ Некоторые сети в настоящее время недоступны. Пожалуйста, повторите попытку позже.
+ Некоторые сети недоступны
+ Это Testnet карта. Он не может обрабатывать транзакции и используется только в целях тестирования и разработки.
+ Только для целей тестирования
+ Отказаться
+ Вы не закончили резервное копирование. Хотите продолжить?
+ Да, возобновить
+ Отказаться
+ Если сейчас отказаться, то придётся сбрасывать карты до заводских настроек, чтобы начать заново
+ Возобновить резервное копирование
+ Это необратимое действие
+ Войти с %s
+ Сканировать карту
+ Используйте %s или отсканируйте карту для входа в приложение
+ C возвращением!
diff --git a/core/res/src/main/res/values-zh-rTW/strings-blockchain.xml b/core/res/src/main/res/values-zh-rTW/strings-blockchain.xml
index eb43a8cf6f..7545da58e5 100644
--- a/core/res/src/main/res/values-zh-rTW/strings-blockchain.xml
+++ b/core/res/src/main/res/values-zh-rTW/strings-blockchain.xml
@@ -3,9 +3,12 @@
默認
遺留資產
獲取費用失敗
- 由於 Kaspa 的限制,只有%1$d UTXO 可以放入單次交易中。這意味著您只能發送%2$s或更少數量。您需要減少數量。
- 要創建帳戶,請將資金發送到此地址
+ 由於 %1$s 的限制,只有%2$d UTXO 可以放入單次交易中。這意味著您只能發送%3$s或更少數量。您需要減少數量。
+ 沒有足夠的資金進行交易。請先入金
+
+
目標帳戶未激活。發送 %s 或更多以激活帳戶
+ 要創建帳戶,請將資金發送到此地址
最小數量是 %s
更動太小
無效費用
diff --git a/core/res/src/main/res/values-zh-rTW/strings.xml b/core/res/src/main/res/values-zh-rTW/strings.xml
index bef56ab91b..e0b8c69381 100644
--- a/core/res/src/main/res/values-zh-rTW/strings.xml
+++ b/core/res/src/main/res/values-zh-rTW/strings.xml
@@ -1,463 +1,371 @@
- 添加自定義代幣
- 管理代幣
- 僅將 %1$s (%2$s) 從 %3$s 網絡發送到此地址。使用其他代幣和網絡可能會導致資金損失
- 謝謝您的反饋
- 成功送出
- 請求支持
- 發送反饋
- 此功能不在展示模式中提供
- 發送電子郵件失敗
- 原因:%s
- 無法發送交易
- 此卡不支持Solana網路上的代幣因為韌體限制
- 有困難在掃描卡上嗎?
- 此卡不適用於此app
- 轉到設置以在 Tangem App 中啟用生物識別身份驗證
- 啟用生物辨識
- 這將刪除所有保存的錢包訪問代碼。對錢包的任何進一步操作都需要提交訪問代碼。
- 移除已保存的卡會刪除所有已保存的錢包及其訪問密碼
- 保存訪問密碼
- 在與您的卡進行交互時,將要求進行生物識別身份驗證,而不是訪問密碼
- 將錢包保存在應用程序中
- 啟用以將所有錢包鏈接到 Tangem 應用程序。解鎖應用程序需要生物識別身份驗證。交易簽名需要輕觸您的 Tangem 卡片
- APP設置
- 請掃描卡片
- 請30秒後重試或刷卡
- 嘗試次數過多
-
- - %d 卡片
-
- 如果您不希望使用此卡重置此錢包中其他卡上的訪問密碼,請禁用此選項。請注意,這也會阻止您重置此卡上的訪問密碼。
- 允許您使用此卡重置此錢包中其他卡上的訪問密碼
- 禁用重置此卡或此錢包中其他卡上的訪問密碼的功能
- 恢復訪問密碼
- 重置
- 您確定要這麼做嗎?
- 更改訪問密碼
- 訪問密碼將僅在此卡上更改
- 回復至原廠設置
- 安全模式
- 選定的應用程序保護方法
- 卡片設置
- Tangem 機器人
- 支援
- 請評價
- 發送logs
- 請選擇一個操作
- 請評價我們的服務
- 接受
- 添加
- 注意
- 餘額: %s
- 生物識別
- 生物
- 購買
- 您尚未授予相機訪問權限,請更改您的隱私設置
- 刪除
- 關閉
- 繼續
- 複製
- 複製地址
- 創造
- 刪除
- 禁用
- 不喜歡
- 完成
- 允許
- 啟用
- 錯誤
- 交易
- 導入
- 喜歡
- 否
- OK
- 主卡片
- 拒絕
- 重新命名
- 保存設置
- 搜索
- 搜尋代幣
- 選擇
- 銷售
- 發送
- 伺服器不可用,請稍後在試
- 分享
- 簽署
- 簽署和發送
- 開始
- 提交
- 成功
- 交換
- 費用
- 條款和條件
- 交易
- 我了解
- 無法觸達
- 是
- 已複製代幣地址
- 支持的網路
- 添加代幣
- 代幣地址
- 無效地址
- 衍生路徑錯誤
- 請選擇網路
- 必填
- 十進制數必須是有效整數,不大於 %d
- 小數點
- 默認
- BIP44 幣種
- 如 USD Coin
- 名稱
- 未選擇
- 網路
- 如 USDC
- 代幣符號
- 此代幣/網路已被加入
- 請注意代幣可以被任何人創造。小心詐騙
- 交談
- 訪問密碼
- 在掃描卡之前,您必須提交正確的訪問密碼
- 長按
- 此機制可防止對卡進行鄰近攻擊。它將強制執行命令的接收和執行之間的延遲。
- 密碼
- 在執行任何需要更改卡狀態的命令之前,您必須輸入密碼
- 推薦計畫
- 隱私政策
- %s hashes
- 卡號
- 連結更多錢包
- App Currency
- 發行人
- 簽署
- 更多
- 檢查您的網路連接或切換到其他網絡
- 服務條款
- 糟糕,當前版本的應用程序無法使用此卡,請檢查更新
- 您使用了另一個錢包中的卡。點按與此錢包關聯的卡片
- 以下信息是可選的。如果不想共享,可以將其刪除
- 告訴我們您缺少哪些功能,我們會盡力幫助您
- 請告訴我們你有什麼卡
- 嗨! 幫助團隊,
- 請告訴我們更多有關您的問題。提供任何細節都會帶來幫助
- 我的建議
- 無法掃卡
- 反饋
- Tangem反饋
- 無法發送交易
- 掃描卡片
- 要更改訪問密碼,請完全按照上圖所示連接手機和卡片
- 要更改密碼,請完全按照上圖所示連接手機和卡
- 要創建錢包,請完全按照上圖所示連接手機和卡
- 要重置為出廠設置,請完全按照上圖所示連接手機和卡片
- 點擊掃描
- 要簽名,請完全按照上圖所示連接手機和卡
- 點擊簽名
- 點按卡片
- 內部錯誤:找不到錢包管理器
- 您已更新生物識別登入,掃描您的卡進入
- 管理代幣
- 總餘額
- 該金額不包括您的部分資金
- 要訪問所有的網路您需要掃描卡片
- 掃描卡片
- 代幣
- 您必須設置一個單一的訪問代碼來保護您的所有錢包
- 保護
- 您可以稍後在每張卡上設置單獨的訪問密碼
- 個人化
- 門禁密碼可以用聯動卡恢復,不要把所有卡都放在一個地方
- 恢復
- 選擇您想要的任何單詞、短語或數字作為您的訪問代碼
- 建立訪問密碼
- 重新輸入訪問密碼
- 訪問代碼的長度必須至少為 4 個字符
- 輸入的訪問密碼與初始訪問密碼不匹配
- 您已添加一張備用卡。備份過程完成後,您將無法添加更多備份卡。如果您還有一張卡,請將其添加到備份中。您想繼續備份過程嗎?
- 備份過程已部分完成。你現在不能退出
- 餘額
- 添加備用卡
- 掃描卡片 #%d
- 立即備份
- 掃描主卡
- 獲取
- 繼續至我的錢包
- 完成備份過程
- 設置PIN碼
- 接收貨幣
- 掃描主卡
- 暫時略過
- 它是如何運作的?
- 讓我們生成您卡上的所有密鑰並創建一個安全的錢包
- 創建錢包
- 創造錢包
- 其他選項
- 您的密鑰將在卡內安全生成。沒有種子短語,這意味著沒有人可以導出或竊取它。
- 私下生成密鑰
- 您的卡已激活並可以使用
- 成功!
- 這此情況,您必須要重新開始
- 您想要離開啟用程序嗎?
- 開始
- 您要添加的卡上已經創建了另一個錢包。你想重置它並將卡用於新錢包嗎?
- PIN 碼
- 連接
- 創建備份
- 閱讀更多關於助記詞的訊息
- 按照下面給出的順序寫下這 12 個單詞,並將它們存放在安全秘密的地方。
- 生成助記詞
- 導入錢包
- 使用助記詞
- Legacy
- 那麼,讓我們檢查一下
- 要開始,只需將 wxDAI 獲取到您的錢包
- 這會花上幾秒
- 要開始備份過程,最多可添加兩張備份卡。
- 您可以再添加一張卡或完成備份過程
- 準備編號為 %s 的備份卡
- 掃描主卡以啟動備份過程
- 準備編號為 %s 的主卡
- 恭喜! 您的第一張支付加密卡已啟用!
- 您的錢包卡已配置完畢,可以使用了
- 已添加至最大數量。完成備份過程
- 啟用卡片
- 備份卡 #%d
- 獲取%s
- 獲取中
- 沒有備用卡
- 添加了一張備用卡
- PIN 碼
- 準備好你的卡片
- 添加了兩張備用卡
- 要開始使用,只需為錢包充值任意金額
- 要開始,只需為錢包充值超過 %1$s %2$s
- 購買加密貨幣
- 顯示錢包地址
- 啟動錢包
- 結對過程已部分完成。你現在不能退出
- 如果創建錢包的過程以任何方式中斷,您將得重新開始
- 您最多可以額外備份兩張空白的 Tangem冷錢包
- 可以使用其中一張備用卡恢復訪問密碼
- 所有備用卡都可以使用相同的密鑰作為全功能使用
- 您將能夠設置訪問密碼來保護您的錢包
- 備援錢包
- 訪問密碼還原
- 相同的卡片
- 訪問密碼
- 參與
- 無法加載有關推薦計劃的消息。請稍後再試
- 無法加載有關推薦計劃的消息。原因:%s。請稍後再試
- 您的朋友買
- 會得到
- 對於你的朋友在你的 %1$s 網絡地址%2$s上購買的每個錢包
- 你
- 得到
- %s 折扣
- 你的朋友
- 個人促銷碼已複製!
- 您的優惠碼
- 以折扣價購買 Tangem 錢包!%s
- 推薦您的好友至Tangem
- 你接受了
- 點擊此按鈕即表示您接受
- 在推薦計劃
-
- - %d 錢包
-
- 請持有卡片直至操作完成
- 重置卡片
- 我了解執行此操作後,我將無法再訪問當前錢包
- 恢復原廠設置將從所選卡中完全刪除錢包。您將無法恢復當前錢包或使用卡恢復訪問密碼
- 恢復原廠設置將從所選卡中完全刪除錢包並將其從應用程序中刪除。您將無法恢復當前錢包
- 目前不接受俄羅斯銀行卡
- 登錄應用程序並在不掃描卡片的情況下檢查您的資產
- 訪問應用程序
- 允許使用生物辨識
- 將要求生物識別而不是訪問密碼來與您的錢包進行交互
- 訪問密碼
- 您好像禁用了生物識別,有必要保存錢包
- 啟用生物識別授權
- 您想使用生物識別技術嗎?
- 請注意,用您的資金進行交易仍需要您的卡片
- 掃卡失敗。再試一次
- 掃描卡片
- 掃描卡片以更改其設置。這些更改只會影響您掃描過的卡,不會影響綁定到您錢包的其他卡。
- 準備好您的卡!
- 數量
- 地址
- 地址與錢包地址相同
- 標籤無效。它不會被添加到交易中
- Memo無效。 它不會被添加到交易中
- Tag
- Memo
- 包含費用
- 低
- 正常
- 優先
- 最大值
- 網路費
- 發送 %s
- 總計
- %1$s 和 %2$s 將被發送
- ≈ %1$s (費用包含: %2$s)
- %s 將被發送
- 交易已成功簽署並發送至區塊鏈節點。錢包餘額稍後更新
- 無效地址
- 現在購買
- 輸入折扣碼
- Tangem Wallet
- 其他付款方式
- 總計
- 安全地存儲您的加密貨幣,同時將私鑰保存在您的卡中
- 創新式的硬體錢包
- 最多 3張實體卡片 到一個錢包
- 最多
- 3張實體卡片
- 到一個錢包
- 更多的安全備份
- 一個冷錢包,可同時將您的比特幣、以太幣和其他幣種-全部存在一張卡中
- 千種虛擬貨幣
- 隨時隨地使用它,不需要接線或電池,只需要你的卡片和手機
- 適合每個人的冷錢包
- 認識Tangem
- 交易、購買NFT、自由穿梭於100+去中心化服務
- 兼容Web3.0
- 批准被視為所有去中心化交易所的行業標準,並保護您的錢包在未經您許可的情況下不被智能合約訪問。按照設計,智能合約無法訪問您的代幣,除非您從您的終端批准訪問。通過“解鎖”您的代幣,您將獲得 1inch 智能合約使用您的資產的權限。網絡的礦工將獲得Gas Fee(由您支付)作為補償,以在區塊鏈上記錄此操作。一旦獲得許可,您就可以交易您的代幣。
- 批准
- 錯誤: %s
- 賦予權限
- 在此代幣交換的數量將對價格產生重大影響,並降低您收到的數量
- 餘額不足
- 您的 %1$s 錢包中沒有足夠的資金來創建交易。首先為您的 %2$s 錢包充值
- 交易進行中...
- 等待中
- 允許
- 賦予權限
- 數量 %s
- 花費者
- 您的錢包
- 要繼續,您需要允許 1inch 智能合約使用您的 %s
- 在瀏覽器中查看
- 進行中
- 交易
- 交易 %s 至
- 此外,報價包括%s的 Tangem 費用。這有助於我們提供一流的產品
- 其他代幣
- 選擇代幣
- 您的代幣
- 無法使用
-
- - %d 代幣
-
- 隱藏
- 您即將在主屏幕上隱藏此代幣。您可以隨時通過管理代幣頁面將其添加回來。
- 隱藏 %s
- 隱藏代幣
- %1$s 代幣是 %2$s 網絡上的主要貨幣,只要列表中還有該網絡上的其他代幣,它就無法被隱藏。
- 無法隱藏 %s
- 無費用
- 您還沒有任何交易
- 無法加載交易
- 進行中…
- 您掃描了同一張卡片。要創建雙錢包,您需要掃描編號為 %d 的卡
- 你掃描錯了胞胎卡。請嘗試另一個
- 這一個是你手裡拿著的,另一個是編號為 %s 的,這兩張卡都可以用來從這個錢包中提取資金
- 一個錢包,兩張卡片
- 掃描卡片 #%s
- 創建錢包
- 掃描 #%s 雙胞胎卡
- 準備卡片
- Tangem Twin
- 這個動作是不可逆的。您將無法訪問舊錢包
- 將您的 iPhone 靠近編號為 %s 的雙胞胎卡
- 添加新錢包
- 您確定要刪除此錢包?
- 已選擇 %d
- 發生錯誤,請掃卡登錄
- 此錢包已保存,您可以再添加一個
- 多幣種
- 錢包名稱
- 重新命名錢包
- 單一幣種
- 我的錢包
- 用 %s 解鎖全部
- 交易記錄
- 區塊鍊無法使用。稍後再試
- 餘額加載中
- 掃描卡片
- 交易進行中
- 檢視餘額
- 動作
- 選擇錢包選項
- 您想要購買或賣出交易貨幣?
- 請求籤署消息。%s
- Dapp %1$s,請求\n簽署 BNB 交易。\n%2$s
- %1$s 的交易訂單\n價格: %2$s\n接收金額:%3$s\n支付數量: %4$s
- 交易明細:\n從: %1$s\n到: %2$s\n數量: %3$s
- BNB 交易已成功簽署並發送至 Dapp
- 剪貼板包含 WalletConnect 代碼。使用複制的值或掃描二維碼
- 請求為 %1$s 創建交易\n%2$s\n\n數量: %3$s\n費用: %4$s\n全部的: %5$s\n餘額: %6$s
- 無法交易,無足夠資金
- 未能建立 WalletConnect 連接。請稍後再試
- 並非所有代幣都已添加到您的列表中。請先添加它們,然後重試。缺少標記:\n
- 無法簽署消息。請重試
- 無法建立 WalletConnect 連接:超時錯誤。請稍後再試
- 會話請求包含不支持 WalletConnect 連接的區塊鏈。不支持的區塊鏈:\n
- 由於技術問題,無法與此 Dapp 建立連接
- 我們遇到了未知錯誤。錯誤信息:%s。如果問題仍然存在-請隨時聯繫我們的客服人員
- 在 Tangem App 中選擇了錯誤的卡
- 無法從 Dapp 數據創建交易。代碼: %s
- 我們遇到了未知錯誤。錯誤代碼:%d。如果問題仍然存在-請隨時聯繫我們的支持人員
- 消息已成功簽名並發送至Dapp
- 沒有 %s 網路,請先加入後再試一次
- 沒有已連結的WalletConnect
- Ooops, 沒有連接
- 從剪貼板貼上
- 給 %1$s 的消息:%2$s
- 請求開始會話\n%1$s\n\n網絡: %2$s\n\n網址:%3$s
- 無法完成執行,您已經使用此參數建立了 WalletConnect 連接
- 掃描新密碼
- 此卡不能用於建立 WalletConnect 連接
- 不支持此網絡。請選擇其他網絡
- 選擇網路
- Dapp 沒有提供必要的數據來建立 WalletConnect 連接
- 找不到請求的連接
- WalletConnect 連接
- 連結到dApps
- WalletConnect
- 交易已成功簽署並發送至 Dapp
- 交易已成功簽署並發送至區塊鏈
- 未能找到交易哈希
- %s 網路
- 帳號尚未被創造
- 不支持此卡
- 您的 Tangem 卡是為與不同的應用程序一起工作而設計的。請查看卡片上的名稱和說明,並安裝正確的應用程序
- %s市場價格
- 地址已復製到剪貼板
- 無網路
- 接收中
- 來自%s
- 發送中
- 至%s
- Tangem
- 可以更好
- 了解更多
- 了解
- 真的很酷!
- 此卡可能是生產樣本或偽造品
- 認證檢查失敗
- 網路無法使用
- Solana 網絡每 2 天收取 %1$s 的費用。無法付此費用的帳戶將從網絡中清除。向您的帳戶存入超過 %2$s 即可免費使用
- 捨棄
- 您有一個備份中斷了,您想繼續嗎?
- 是的,恢復
- 捨棄
- 如果您現在要放棄備份,則必須將卡重置為出廠設置才能重新開始
- 恢復備份
- 此動作不可逆
- 使用 %s 登錄
- 掃描卡片
- 使用 %s 或掃描卡片以訪問該應用程序
- 歡迎回來!
+ 添加自定義代幣
+ 管理代幣
+ 僅將 %1$s (%2$s) 從 %3$s 網絡發送到此地址。使用其他代幣和網絡可能會導致資金損失
+ 請求支持
+ 此功能不在展示模式中提供
+ 原因:%s
+ 無法發送交易
+ 此卡不支持%1$s網路上的代幣因為韌體限制
+ 有困難在掃描卡上嗎?
+ 此卡不適用於此app
+ 轉到設置以在 Tangem App 中啟用生物識別身份驗證
+ 啟用生物辨識
+ 這將刪除所有保存的錢包訪問代碼。對錢包的任何進一步操作都需要提交訪問代碼。
+ 移除已保存的卡會刪除所有已保存的錢包及其訪問密碼
+ 保存訪問密碼
+ 在與您的卡進行交互時,將要求進行生物識別身份驗證,而不是訪問密碼
+ 將錢包保存在應用程序中
+ 啟用以將所有錢包鏈接到 Tangem 應用程序。解鎖應用程序需要生物識別身份驗證。交易簽名需要輕觸您的 Tangem 卡片
+ APP設置
+ 請掃描卡片
+ 請30秒後重試或刷卡
+ 嘗試次數過多
+
+ - %d 卡片
+
+ 如果您不希望使用此卡重置此錢包中其他卡上的訪問密碼,請禁用此選項。請注意,這也會阻止您重置此卡上的訪問密碼。
+ 允許您使用此卡重置此錢包中其他卡上的訪問密碼
+ 恢復訪問密碼
+ 重置
+ 您確定要這麼做嗎?
+ 更改訪問密碼
+ 訪問密碼將僅在此卡上更改
+ 回復至原廠設置
+ 安全模式
+ 卡片設置
+ 接受
+ 注意
+ 餘額: %s
+ 餘額
+ 生物識別
+ 生物
+ 購買
+ 您尚未授予相機訪問權限,請更改您的隱私設置
+ 刪除
+ 關閉
+ 繼續
+ 複製
+ 複製地址
+ 創造
+ 刪除
+ 禁用
+ 完成
+ 允許
+ 啟用
+ 錯誤
+ 費用
+ 導入
+ 網路費
+ 否
+ OK
+ 主卡片
+ 拒絕
+ 重新命名
+ 保存設置
+ 搜索
+ 搜尋代幣
+ 選擇
+ 銷售
+ 發送
+ 伺服器不可用,請稍後在試
+ 分享
+ 簽署
+ 簽署和發送
+ 開始
+ 提交
+ 成功
+ 交換
+ 條款和條件
+ 交易
+ 我了解
+ 無法觸達
+ 是
+ 已複製代幣地址
+ 支持的網路
+ 添加代幣
+ 代幣地址
+ 無效地址
+ 請選擇網路
+ 小數點
+ 默認
+ BIP44 幣種
+ 如 USD Coin
+ 名稱
+ 未選擇
+ 網路
+ 如 USDC
+ 代幣符號
+ 此代幣/網路已被加入
+ 請注意代幣可以被任何人創造。小心詐騙
+ 交談
+ 訪問密碼
+ 在掃描卡之前,您必須提交正確的訪問密碼
+ 長按
+ 此機制可防止對卡進行鄰近攻擊。它將強制執行命令的接收和執行之間的延遲。
+ 密碼
+ 在執行任何需要更改卡狀態的命令之前,您必須輸入密碼
+ 推薦計畫
+ %s hashes
+ 卡號
+ 連結更多錢包
+ App Currency
+ 發行人
+ 簽署
+ 更多
+ 檢查您的網路連接或切換到其他網絡
+ 服務條款
+ 您使用了另一個錢包中的卡。點按與此錢包關聯的卡片
+ 以下信息是可選的。如果不想共享,可以將其刪除
+ 告訴我們您缺少哪些功能,我們會盡力幫助您
+ 請告訴我們你有什麼卡
+ 嗨! 幫助團隊,
+ 請告訴我們更多有關您的問題。提供任何細節都會帶來幫助
+ 我的建議
+ 無法掃卡
+ 反饋
+ Tangem反饋
+ 無法發送交易
+ 掃描卡片
+ 要更改訪問密碼,請完全按照上圖所示連接手機和卡片
+ 要更改密碼,請完全按照上圖所示連接手機和卡
+ 要創建錢包,請完全按照上圖所示連接手機和卡
+ 點擊掃描
+ 點擊簽名
+ 點按卡片
+ 您已更新生物識別登入,掃描您的卡進入
+ 管理代幣
+ 要訪問所有的網路您需要掃描卡片
+ 掃描卡片
+ 代幣
+ 保護
+ 您可以稍後在每張卡上設置單獨的訪問密碼
+ 個人化
+ 恢復
+ 選擇您想要的任何單詞、短語或數字作為您的訪問代碼
+ 建立訪問密碼
+ 重新輸入訪問密碼
+ 訪問代碼的長度必須至少為 4 個字符
+ 輸入的訪問密碼與初始訪問密碼不匹配
+ 您已添加一張備用卡。備份過程完成後,您將無法添加更多備份卡。如果您還有一張卡,請將其添加到備份中。您想繼續備份過程嗎?
+ 備份過程已部分完成。你現在不能退出
+ 添加備用卡
+ 掃描卡片 #%d
+ 立即備份
+ 掃描主卡
+ 繼續至我的錢包
+ 完成備份過程
+ 接收貨幣
+ 掃描主卡
+ 暫時略過
+ 它是如何運作的?
+ 讓我們生成您卡上的所有密鑰並創建一個安全的錢包
+ 創建錢包
+ 創造錢包
+ 其他選項
+ 您的密鑰將在卡內安全生成。沒有種子短語,這意味著沒有人可以導出或竊取它。
+ 私下生成密鑰
+ 您的卡已激活並可以使用
+ 成功!
+ 這此情況,您必須要重新開始
+ 您想要離開啟用程序嗎?
+ 開始
+ 您要添加的卡上已經創建了另一個錢包。你想重置它並將卡用於新錢包嗎?
+ 創建備份
+ 閱讀更多關於助記詞的訊息
+
+ - 按照下面給出的順序寫下這 %d 個單詞,並將它們存放在安全秘密的地方。
+
+ 生成助記詞
+ 導入錢包
+ 使用助記詞
+ Legacy
+ 那麼,讓我們檢查一下
+ 要開始備份過程,最多可添加兩張備份卡。
+ 您可以再添加一張卡或完成備份過程
+ 準備編號為 %s 的備份卡
+ 掃描主卡以啟動備份過程
+ 準備編號為 %s 的主卡
+ 您的錢包卡已配置完畢,可以使用了
+ 已添加至最大數量。完成備份過程
+ 啟用卡片
+ 備份卡 #%d
+ 沒有備用卡
+ 添加了一張備用卡
+ 準備好你的卡片
+ 添加了兩張備用卡
+ 要開始使用,只需為錢包充值任意金額
+ 要開始,只需為錢包充值超過 %1$s %2$s
+ 購買加密貨幣
+ 顯示錢包地址
+ 啟動錢包
+ 結對過程已部分完成。你現在不能退出
+ 如果創建錢包的過程以任何方式中斷,您將得重新開始
+ 您最多可以額外備份兩張空白的 Tangem冷錢包
+ 可以使用其中一張備用卡恢復訪問密碼
+ 所有備用卡都可以使用相同的密鑰作為全功能使用
+ 您將能夠設置訪問密碼來保護您的錢包
+ 備援錢包
+ 訪問密碼還原
+ 相同的卡片
+ 訪問密碼
+ 參與
+ 無法加載有關推薦計劃的消息。請稍後再試
+ 無法加載有關推薦計劃的消息。原因:%s。請稍後再試
+ 您的朋友買
+ 你
+ 得到
+ %s 折扣
+ 你的朋友
+ 個人促銷碼已複製!
+ 您的優惠碼
+ 以折扣價購買 Tangem 錢包!%s
+ 推薦您的好友至Tangem
+ 你接受了
+ 點擊此按鈕即表示您接受
+ 在推薦計劃
+
+ - %d 錢包
+
+ 重置卡片
+ 我了解執行此操作後,我將無法再訪問當前錢包
+ 恢復原廠設置將從所選卡中完全刪除錢包。您將無法恢復當前錢包或使用卡恢復訪問密碼
+ 恢復原廠設置將從所選卡中完全刪除錢包並將其從應用程序中刪除。您將無法恢復當前錢包
+ 目前不接受俄羅斯銀行卡
+ 登錄應用程序並在不掃描卡片的情況下檢查您的資產
+ 訪問應用程序
+ 允許使用生物辨識
+ 將要求生物識別而不是訪問密碼來與您的錢包進行交互
+ 訪問密碼
+ 您好像禁用了生物識別,有必要保存錢包
+ 啟用生物識別授權
+ 您想使用生物識別技術嗎?
+ 請注意,用您的資金進行交易仍需要您的卡片
+ 掃描卡片
+ 掃描卡片以更改其設置。這些更改只會影響您掃描過的卡,不會影響綁定到您錢包的其他卡。
+ 準備好您的卡!
+ 數量
+ 地址
+ 地址與錢包地址相同
+ 標籤無效。它不會被添加到交易中
+ Memo無效。 它不會被添加到交易中
+ Tag
+ Memo
+ 包含費用
+ 低
+ 正常
+ 優先
+ 最大值
+ 發送 %s
+ 總計
+ %1$s 和 %2$s 將被發送
+ ≈ %1$s (費用包含: %2$s)
+ %s 將被發送
+ 交易已成功簽署並發送至區塊鏈節點。錢包餘額稍後更新
+ 無效地址
+ 安全地存儲您的加密貨幣,同時將私鑰保存在您的卡中
+ 創新式的硬體錢包
+ 最多 3張實體卡片 到一個錢包
+ 更多的安全備份
+ 一個冷錢包,可同時將您的比特幣、以太幣和其他幣種-全部存在一張卡中
+ 千種虛擬貨幣
+ 隨時隨地使用它,不需要接線或電池,只需要你的卡片和手機
+ 適合每個人的冷錢包
+ 認識Tangem
+ 交易、購買NFT、自由穿梭於100+去中心化服務
+ 兼容Web3.0
+ 批准被視為所有去中心化交易所的行業標準,並保護您的錢包在未經您許可的情況下不被智能合約訪問。按照設計,智能合約無法訪問您的代幣,除非您從您的終端批准訪問。通過“解鎖”您的代幣,您將獲得 1inch 智能合約使用您的資產的權限。網絡的礦工將獲得Gas Fee(由您支付)作為補償,以在區塊鏈上記錄此操作。一旦獲得許可,您就可以交易您的代幣。
+ 批准
+ 賦予權限
+ 在此代幣交換的數量將對價格產生重大影響,並降低您收到的數量
+ 餘額不足
+ 允許
+ 賦予權限
+ 數量 %s
+ 要繼續,您需要允許 1inch 智能合約使用您的 %s
+ 進行中
+ 交易
+ 選擇代幣
+ 無法使用
+ 隱藏
+ 您即將在主屏幕上隱藏此代幣。您可以隨時通過管理代幣頁面將其添加回來。
+ 隱藏 %s
+ 隱藏代幣
+ %1$s 代幣是 %2$s 網絡上的主要貨幣,只要列表中還有該網絡上的其他代幣,它就無法被隱藏。
+ 無法隱藏 %s
+ 您還沒有任何交易
+ 無法加載交易
+ 您掃描了同一張卡片。要創建雙錢包,您需要掃描編號為 %d 的卡
+ 你掃描錯了胞胎卡。請嘗試另一個
+ 這一個是你手裡拿著的,另一個是編號為 %s 的,這兩張卡都可以用來從這個錢包中提取資金
+ 一個錢包,兩張卡片
+ 掃描卡片 #%s
+ 創建錢包
+ 掃描 #%s 雙胞胎卡
+ 準備卡片
+ Tangem Twin
+ 這個動作是不可逆的。您將無法訪問舊錢包
+ 將您的 iPhone 靠近編號為 %s 的雙胞胎卡
+ 添加新錢包
+ 您確定要刪除此錢包?
+ 發生錯誤,請掃卡登錄
+ 此錢包已保存,您可以再添加一個
+ 錢包名稱
+ 重新命名錢包
+ 用 %s 解鎖全部
+ 區塊鍊無法使用。稍後再試
+ 掃描卡片
+ 請求籤署消息。%s
+ Dapp %1$s,請求\n簽署 BNB 交易。\n%2$s
+ %1$s 的交易訂單\n價格: %2$s\n接收金額:%3$s\n支付數量: %4$s
+ 交易明細:\n從: %1$s\n到: %2$s\n數量: %3$s
+ 剪貼板包含 WalletConnect 代碼。使用複制的值或掃描二維碼
+ 請求為 %1$s 創建交易\n%2$s\n\n數量: %3$s\n費用: %4$s\n全部的: %5$s\n餘額: %6$s
+ 無法交易,無足夠資金
+ 未能建立 WalletConnect 連接。請稍後再試
+ 並非所有代幣都已添加到您的列表中。請先添加它們,然後重試。缺少標記:\n
+ 無法簽署消息。請重試
+ 無法建立 WalletConnect 連接:超時錯誤。請稍後再試
+ 會話請求包含不支持 WalletConnect 連接的區塊鏈。不支持的區塊鏈:\n
+ 由於技術問題,無法與此 Dapp 建立連接
+ 我們遇到了未知錯誤。錯誤信息:%s。如果問題仍然存在-請隨時聯繫我們的客服人員
+ 在 Tangem App 中選擇了錯誤的卡
+ 無法從 Dapp 數據創建交易。代碼: %s
+ 我們遇到了未知錯誤。錯誤代碼:%d。如果問題仍然存在-請隨時聯繫我們的支持人員
+ 沒有已連結的WalletConnect
+ Ooops, 沒有連接
+ 從剪貼板貼上
+ 給 %1$s 的消息:%2$s
+ 請求開始會話\n%1$s\n\n網絡: %2$s\n\n網址:%3$s
+ 無法完成執行,您已經使用此參數建立了 WalletConnect 連接
+ 掃描新密碼
+ 此卡不能用於建立 WalletConnect 連接
+ 不支持此網絡。請選擇其他網絡
+ 選擇網路
+ WalletConnect 連接
+ 連結到Dapps
+ WalletConnect
+ %s市場價格
+ 地址已復製到剪貼板
+ 無網路
+ Tangem
+ 可以更好
+ 了解
+ 真的很酷!
+ 此卡可能是生產樣本或偽造品
+ 認證檢查失敗
+ 網路無法使用
+ Solana 網絡每 2 天收取 %1$s 的費用。無法付此費用的帳戶將從網絡中清除。向您的帳戶存入超過 %2$s 即可免費使用
+ 捨棄
+ 您有一個備份中斷了,您想繼續嗎?
+ 是的,恢復
+ 捨棄
+ 如果您現在要放棄備份,則必須將卡重置為出廠設置才能重新開始
+ 恢復備份
+ 此動作不可逆
+ 使用 %s 登錄
+ 掃描卡片
+ 使用 %s 或掃描卡片以訪問該應用程序
+ 歡迎回來!
diff --git a/core/res/src/main/res/values/strings-blockchain.xml b/core/res/src/main/res/values/strings-blockchain.xml
index 2aeb56c771..ef4e2b2edb 100644
--- a/core/res/src/main/res/values/strings-blockchain.xml
+++ b/core/res/src/main/res/values/strings-blockchain.xml
@@ -6,10 +6,9 @@
Due to %1$s limitations only %2$d UTXOs can fit in a single transaction. This means you can only send %3$s or less. You need to reduce the amount.
Not enough funds for the transaction. Please top up your account.
An error occurred. Code: %s.
- Due to Kaspa limitations only %1$d UTXOs can fit in a single transaction. This means you can only send %2$s or less. You need to reduce the amount.
- To create account send funds to this address
To use the %1$s network, you must pay the account reserve (%2$s %3$s), which locks up and hides that amount indefinitely
Destination account is not active. Send %s or more to activate the account.
+ To create account send funds to this address
Minimum amount is %s
Change is too small
Invalid Fee
diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml
index cada81a5ff..9310a0153a 100644
--- a/core/res/src/main/res/values/strings.xml
+++ b/core/res/src/main/res/values/strings.xml
@@ -1,762 +1,691 @@
- Add custom token
- Manage tokens
- Send only %1$s (%2$s) from %3$s network to this address. Using other tokens and networks may result in loss of funds.
- Thank you for your feedback
- Sent successfully
- Request support
- Send feedback
- This feature is disabled in Demo mode
- Failed to send the email
- Reason: %s
- Can\'t send a transaction
- The selected does not support the %1$s network
- To activate the %1$s blockchain\'s cryptographic encryption, you\'ll need to reset the wallet to factory settings. Please withdraw your funds before doing so to ensure that you don\'t lose them, and then complete the reset process. Access to the current wallet will not be possible after the reset.
- Tokens in %1$s network are not supported by this card due to firmware limitation.
- Are you having difficulty scanning your card?
- This card is not designed to work with this app
- Go to settings to enable biometric authentication in the Tangem App
- Enable biometric authentication
- This will delete all the saved wallet access codes. Any further operation with the wallet will require submitting the access code.
- Removing the saved card deletes all the saved wallets and their access codes from the app.
- Save Access Code
- Biometric authentication will be requested instead of the access code for interactions with your card.
- Keep the wallet in the app
- Enable to link all the wallets to Tangem app. Biometric authentication will be required for unlocking the app. Transaction signing requires tapping your Tangem card.
- Dark
- Light
- System default
- Theme
- App Settings
- To hide or show your balances, simply flip your device screen down, or switch it off in Settings
- Don\'t show again
- Got it
- Balances are hidden
- Please scan the card
- Please try again in 30 seconds or scan the card
- Too many attempts
- You have disabled biometric authentication on your phone and will not be able to save wallets in the app. To save wallets, please enable the biometric authentication function in your phone settings.
- Start backup process
-
- - %d card
- - %d cards
-
- Disable this option if you don\'t want this card to be used to reset access codes on other cards in this wallet. Please note that this will also prevent you from resetting the access code on this card.
- Allows you to use this card to reset access code on other cards in this wallet
- Disable the ability to reset the access code on this card or other cards in this wallet
- Access code recovery
- Reset
- Are you sure you want to do this?
- Change Access Code
- Access code will be changed on this card only
- Reset to Factory Settings
- Security Mode
- Selected application protection method
- Card Settings
- Tangem Bot
- Support
- Send logs
- Accept
- Access denied
- Add
- Apply
- Approval
- Attention
- Balance: %s
- Balance
- biometric authentication
- biometrics
- Buy
- Go to %1$s
- You have not given access to your camera, please adjust your privacy settings
- Cancel
- Close
- Continue
- Copy
- Copy address
- Create
- Delete
- Disabled
- Dislike
- Done
- Enable
- Enabled
- Error
- Exchange
- Explore
- Explore history
- Explore transaction history
- Explorer
- Fee
- Network fees are charges users pay to process and confirm transactions. The fee amount can be affected by network congestion, transaction size, and execution priority. %s
- Custom
- Fast
- Market
- Slow
- Speed and fee
- Go to provider
- Get addresses
- Import
- Learn & Earn
- Like
- Locked
- Main network
- Next
- No
- No address
- No data
- OK
- Primary Card
- Paste
- Read more
- Receive
- Reject
- Reload
- Rename
- Save changes
- Search
- Search tokens
- Seed phrase
- Select action
- Sell
- Send
- The server is not available, please try again later
- Share
- Sign
- Sign and send
- Stake
- Start
- Submit
- Success
- Support
- Swap
- terms and conditions
- Transaction failed
- Transactions
- Transfer
- I understand
- Unreachable
- There was an error. Please try again.
- Yes
- Contract address copied!
- Available networks
- Add token
- Contract address
- Contract address is invalid
- Derivation path is invalid
- Please select the network
- Required field
- Decimal number must be a valid integer, no higher than %d
- Custom derivation
- E. g. m/00\'/0000\'/0\'/0/0
- Enter custom derivation
- Decimals
- Derivation Path
- Default
- BIP44 coin type
- The derivation path you\'ve entered is not valid
- E.g. USD Coin
- Name
- Not selected
- Network
- Token network
- You can manually add a token that is not natively supported by Tangem
- E.g. USDC
- Symbol
- Token symbol
- This token/network has already been added to your list
- Note that tokens can be created by anyone. Be aware of adding scam tokens, they can cost nothing.
- Be aware of adding scam tokens, they can cost nothing
- Note that tokens can be created by anyone
- Chat
- Access code
- You will have to submit the correct access code before scanning the card
- Long Tap
- This mechanism protects against proximity attacks on a card. It will enforce a delay between reception and execution of a command.
- Passcode
- Before executing any command entailing a change of the card state, you will have to enter the passcode.
- Referral program
- Flip your device screen down to quickly hide and show balances
- Privacy policy
- %s hashes
- Card ID
- Contact to Support
- Link More Cards
- App Currency
- Flip-to-Hide Balances
- Issuer
- Signed
- Details
- Check your internet connection or switch to a different network
- Terms of Service
- Oops, the current version of the application is not ready to work with this card, please check for updates.
- You have used a card from another wallet. Tap the card associated with this wallet
- My tokens
- You haven\'t added any tokens yet. Add tokens via Market to swap
- No tokens found. Please try another request
- Cannot be swapped for %s
- Additionally, the network fee for sending the exchanged funds back to your address is included in the rate
- Status
- Providers facilitate transactions, ensuring smooth and efficient token swaps
- Choose provider
- More providers are coming soon.\nStay tuned!
- Estimated amount
- Exchange by %s
- Provided by
- Visit provider’s website to refund your money
- Operation failed by provider
- Visit provider’s website for verification
- KYC verification required by provider
- Canceled
- Confirmed
- Confirming
- Confirming…
- Exchanged
- Exchanging
- Exchanging…
- Failed
- Deposit received
- Awaiting deposit
- Awaiting deposit…
- Refunded
- Sending to you
- Sending to you…
- Sent
- Provider-sourced data. Estimated amount subject to change due to market conditions.
- Exchange status
- Verified
- Verification required
- List of all tokens added to your wallet
- Fetching best rates...
- Floating rate
- Go to provider
- By using swap functionality, you agree with provider’s %s
- By using swap functionality, you agree with provider’s %1$s and %2$s
- Privacy Policy
- Provider
- Best rate
- Available from %s
- Available up to %s
- Unavailable for this pair
- Permission Required
- Terms of Use
- The following information is optional. You can erase it if you don\'t want to share it.
- Tell us what functions you are missing, and we will try to help you.
- Please tell us what card do you have
- Hi support team,
- Please tell us more about your issue. Every small detail can help.
- My suggestions
- Can\'t scan a card
- Feedback
- Tangem feedback
- Can\'t send a transaction
- Order card
- Scan card
- To change the access code tap the card as shown above and do not remove until the end of the operation
- To change the passcode tap the card as shown above and do not remove until the end of the operation
- To create the wallet tap the card as shown above and do not remove until the end of the operation
- To reset to factory settings tap the card as shown above and do not remove until the end of the operation
- Tap to scan
- To sign tap the card as shown above and do not remove until the end of the operation
- Tap to sign
- Tap the card
- Internal error: wallet manager not found
- You have updated biometrics, scan your card to enter
- To begin tracking your crypto assets and transactions, add tokens
- You have completed all of the lessons, and are now eligible to receive your 1INCH tokens
-
- - Complete three lessons and receive %d 1INCH token to your wallet
- - Complete three lessons and receive %d 1INCH tokens to your wallet
-
- Manage tokens
- Total balance
- The amount does not include some of your funds
- 1INCH tokens will be credited to your %s wallet address within 48 hours
- To access all the networks you need to scan the card
- Scan your card
- Enjoy %1$s service fees on swaps via Changelly from February %2$s-%3$s
- Swap with Changelly, %s fees
- Tokens
- Add
- Edit
- Couldn’t find this token, you can add it manually
- Coin market cap
- Blockchain the cryptocurrency was initially created
- Native network
- Using non-native networks for tokens enables cross-blockchain interoperability, allowing assets to be utilized in diverse decentralized applications and smart contracts across platforms. However, this often involves a custodian or smart contract to hold the original asset securely, introducing centralization and counterparty risk.
- Not original or primary blockchain the token is hosted
- Non-native networks
- Networks
- Choose networks
- Wallet
- Couldn’t find this token, you can add it manually
-
- - %1$d of %2$d wallet
- - %1$d of %2$d wallets
-
- e.g. BTC I trust, hodl I must
- Coin market cap
- The selected token is currently unavailable for actions within the crypto wallet. But worry not, you can express your interest by upvoting it.
- Upvote
- Choose wallet
- The wallet doesn\'t support more than one network
- You have to set up a single access code to protect all your wallets
- Protect
- You can set up an individual access code on each card later
- Personalize
- The access code can be restored with a linked card, don\'t keep all cards in one place
- Restore
- Choose any word, phrase, or number you want as your access code
- Create Access Code
- Re-enter your Access Code
- Access code must be at least 4 characters long
- Entered access code didn\'t match the initial access code
- Please repeat the operation. The card will be reset to factory settings.
- Activation error
- You\'ve added one backup card. When backup process is finished you can\'t add more backup cards. If you have one more card, add it to backup. Do you like to continue the backup process?
- The backup process is partly complete. You can\'t exit it now.
- Add a backup card
- Scan the card #%d
- Backup now
- Scan the primary card
- Claim
- Continue to my wallet
- Finalize the backup
- Set PIN code
- Receive crypto
- Scan primary card
- Skip for later
- How does it work?
- Let\'s generate all the keys on your card and create a secure wallet
- Create wallet
- Create a wallet
- Other options
- Your keys will be securely generated inside the card. There is no seed phrase, which means nobody can export or steal it.
- Generate keys privately
- Your card is activated and ready to be used
- Success!
- In this case, you will need to start from the beginning.
- Do you want to exit the activation process?
- Getting started
- Another wallet has already been created on the card you\'re trying to add. Do you want to reset it and use the card for a new wallet?
- Pin code
- Connect
- Creating a backup
- Read more about seed phrase
- Write these 12 words down in the order given below and store them in a safe and secret place.
- Your seed phrase
- To import your wallet, enter your seed phrase in the field below
- Generate seed phrase
- Import wallet
- A seed phrase is a series of words that allows you to recover your wallet. Unlike the keys generated by the card, seed phrases are unprotected and can be copied and stolen. Use this option at your own risk.
- Use seed phrase
- Invalid seed phrase. Please check the word order.
- Invalid seed phrase. Please check your spelling.
- Legacy
- To check whether you’ve written down your seed phrase correctly, please enter the 2nd, 7th and 11th words
- So, let’s check
- To get started, simply claim wxDAI to your wallet
- It will take a few seconds
- To start the backup process add up to two backup cards.
- You can add one more card or finalize the backup process
- Prepare the backup card with number %s
- Scan the primary card to start the backup process.
- Prepare the primary card with number %s
- Congratulations! Your first payment crypto card has been activated!
- Your wallet card is configured and ready for use.
- Max number of cards added. Finalize the backup process.
- Activating card
- Backup card #%d
- Claim %s
- Claiming
- No backup cards
- One backup card added
- PIN Code
- Prepare your card
- Two backup cards added
- To get started, simply top up the wallet with any amount
- To get started, simply top up the wallet with more than %1$s %2$s
- Buy crypto
- Show the wallet\'s address
- Activate a wallet
- The twinning process is partly complete. You can\'t exit it now.
- If the process of creating the wallet gets interrupted in any way, you\'ll have to start over
- You can backup your keys up to two other blank Tangem Wallet cards.
- Access code can be restored with one of backup cards.
- All the backup cards can be used as full-functional with the identical keys.
- You will be able to set an access code to protect your wallets.
- Backup wallet
- Access code restore
- Identical cards
- Access code
- Group
- By balance
- Organize tokens
- Ungroup
- Select from the gallery
- Settings
- Camera access denied
- You have not given access to your camera
- %1$s %2$s address on %3$s network
- %1$s (%2$s) on %3$s network
- Send only %s to this address. Sending any other currency will result in its irreversible loss.
- Participate
- Failed to load the information about the referral program. Please try again later.
- Failed to load the information about the referral program. Error code: %s. Please try again later.
- Upcoming payments
- Your friends bought
- Less
- More
- No upcoming payments
-
- - for %d wallet
- - for %d wallets
-
- Will get ^^%1$s^^ for each wallet bought by your friend on your %2$s network address %3$s ^^30 days after^^ that
- Will get
- for each wallet bought by your friend on your %1$s network address%2$s
- You
- Will get a
- when buying a wallet on tangem.com
- %s discount
- Your friend
- Personal code copied!
- Your personal code
- Buy Tangem Wallet with discount!\n%s
- Refer your friends to Tangem
- You\'ve accepted
- By tapping this button you accept
- of the referral program
-
- - %d wallet
- - %d wallets
-
- Please hold the card until the operation complete
- Reset the Card
- I understand that after performing this action, I will no longer have access to the current wallet
- I realize that I can\'t use this card to recover my access code on the other cards of the current wallet
- Factory Reset will completely delete the wallet from the selected card. You will not be able to restore the current wallet or use the card to recover the access code.
- Factory Reset will completely delete the wallet from the selected card and remove it from the app. You will not be able to restore the current wallet.
- Do you have a bank card from another country and a residence permit or registration outside the Russian Federation?
- Russian bank cards are not currently accepted
- Log into the app and check your balance without scanning the card
- Access the app
- Allow to use biometrics
- Biometrics will be requested instead of the access code for interactions with your wallet
- Access code
- It looks like you have biometric authentication disabled, it is necessary to save wallets
- Enable biometric authorization
- Would you like to use biometrics?
- Note that making a transaction with your funds will still require your card
- Scan card failed. Try again
- Scan Card
- Scan the card to change its settings. The changes will impact only the card you\'ve scanned and will not affect other cards tied to your wallet.
- Get your card ready!
- Already included in the entered address
- Amount
- The transaction is not completed
- Reason: %1$s\nCode: %2$s
- Not enough funds to cover the network commission. Subtract the commission from the amount sent?
- Subtract
- Вы указали комиссию ниже рекомендуемой, это может привести к задержке исполнения вашей транзакции. Продолжить?
- Confirm
- %1$s at %2$s
- Address
- Destination Tag
- Enter address
- Address is the same as wallet address
- Invalid Tag. It won\'t be added to the transaction.
- Invalid Memo. It won\'t be added to the transaction.
- Tag
- Memo
- Include fee
- Low
- Normal
- Priority
- Check your network connection
- Network fee info unreachable
- From
- Gas limit
- Gas Limit is auto-calculated; raise it during network congestion
- Gas price
- Gas Price affects transaction speed. If it\'s too low, the transaction might not be processed
- Max
- Maximum amount
- Max fee
- Maximum fee amount
- Numbers only for Destination Tag
- Network fee
- Sending amount will be reduced by %1$s to cover the selected commission level. The recipient will get %2$s.
- Network fee coverage
- Insufficient funds for the transfer, as the total of the fee and transfer amount exceeds the existing balance
- Total exceeds balance
- Fee is increased
- The fee for transferring the entire balance is higher. To reduce the commission, you can leave 0.01.
- Custom fee is high
- The commission amount is %s times the recommended amount. Make sure that the custom settings are correct.
- The included commission exceeds the transfer amount, leading to a negative value
- Invalid amount
- The minimum sending amount is %1$s. Please ensure that the remaining balance after sending will not be less than %1$s.
- Target account is not created. Please change the amount to send.
- The amount to send must be at least %1$s
- Kindly be aware that your transaction may experience delays under specific fee settings
- Transaction delays are possible
- Transaction limitation
- Due to %1$s limitations only %2$s UTXOs can fit in a single transaction. This means you can only send %3$s or less. You need to reduce the amount.
- Existential deposit
- The account will be wiped from the blockchain if a balance goes below the existential deposit. Please ensure that the remaining balance after sending will not be less than %s.
- Reduce by %s
- Reduce to %s
- Optional
- Please align your QR code with the square to scan it. Ensure you scan %s network address.
- Recent
- Recipient
- Send to
- Not a valid address
- Ensure that you are sending funds to an %s wallet address. Errors may result in the loss of your tokens
- A Memo/Destination Tag is a unique ID for differentiating transactions sent to the same recipient on the same network. Caution: Omitting a memo may lead to misplaced funds
- My wallets
- Sending %s
- Total
- %1$s and %2$s will be sent
- ≈ %1$s (inc. fee: %2$s)
- %s will be sent
- Transaction has been successfully signed and sent to the blockchain node. Wallet balance will be updated in a while
- Invalid address
- Transaction sent
- %s (%s)
- Buy now
- I have a promo code…
- Tangem Wallet
- Other payment methods
- Pre-order now
- Total
- Store your crypto assets secure while keeping private keys contained in your card
- Revolutionary Hardware Wallet
- Up to 3 physical cards to one wallet
- Up to
- 3 physical cards
- to one wallet
- Ultra Secure Backup
- A hardware wallet for your Bitcoin, Ethereum and many more currencies simultaneously – all in one card
- Thousands of Currencies
- Use it on the go, anywhere, anytime. No wires or batteries. Just tap the card to your phone when you need your crypto.
- The Wallet for Everyone
- Take three lessons, get a discount on your Tangem Wallet, and receive 1INCH tokens to your wallet
- Learn
- Meet Tangem
- Exchange, buy NFT\'s, make loans and deposits in more than 100 different decentralized services
- Web 3.0 Compatible
- All decentralized exchanges require approvals to prevent smart contracts from accessing your wallet without your permission. By design, smart contracts can\'t access your tokens unless you approve. By \"unlocking\" your tokens, you authorize the 1-inch smart contract to spend them. The network\'s miners receive a gas fee (paid by you) to record this action on the blockchain. You can swap your token after giving approval.
- Approve
- Error: %s
- You swap
- Give Permission
- Swapping this amount of selected tokens will cause a significant price impact and reduce your outcome.
- Insufficient funds
- Fees
- The amount includes:\n• service provider\'s fee\n• network fee for sending %s from the exchange back to the user\'s address.
- The amount includes the service provider\'s fee.
- Sending amount will be reduced to cover the selected fee level
- Insufficient funds in your %1$s wallet to cover fees. Top up your %2$s wallet first.
- Transaction in progress...
- Waiting
- Approve
- Current transaction
- The network will charge a token approval fee to verify that you are authorizing the use of your token for the swap.
- Give Permission
- Specify the approve limit for the selected token
- Amount %s
- Spender
- Your wallet
- To continue, grant 1inch smart contracts permission to use your %s
- Unlimited
- View in Explorer
- In progress
- Swap
- Swap %s for
- Quotes include an additional Tangem commission of %s. This helps us deliver a top-of-the-line product.
- You receive
- Other tokens
- Choose token
- Your tokens
- not available
- Balances hidden
- Balances shown
- Undo
-
- - %d token
- - %d tokens
-
- Hide
- You are about to hide this token from the main screen. You can add it back anytime through the manage tokens page.
- Hide %s
- Hide token
- %1$s token in %%image%% %2$s network
- The %1$s token is the main currency on the %2$s network and cannot be hidden as long as you have other tokens on this network in the list.
- Unable to hide %s
- No rate
- Exchange this token for another at %1$s service fees from February %2$s-%3$s.
- Swap with Changelly, %s fees
- Swap now
- contract: %s
- You don\'t have any transactions yet
- Failed to load transaction history.\nClick on reload button to update the information.
- Multiple addresses
- Transaction history is currently not supported for this blockchain. But don\'t worry, we\'re working on it! In the meantime you can check it in the explorer.
- Operation
- from: %s
- to: %s
- In progress…
- You\'ve scanned the same card. To create a twin wallet you need to scan the card with number %d
- You\'ve scanned wrong twin card. Please try another one
- This one that you are holding in your hands and the other one with number %s.\n\nBoth cards can be used to extract funds from this wallet.
- One wallet. Two cards.
- Scan the card #%s
- Creating wallet
- Scan the #%s twin card
- Preparing card
- Tangem Twin
- This action is irreversible. You will not have access to the old wallet.
- Tap the twin card with number %s and do not remove until the end of the operation
- Use %s or scan a card to have an access to your wallet
- Add new wallet
- Are you sure you want to delete this wallet?
- %d selected
- An error has occurred, please scan your card to log in
- This wallet has already been saved, you can add another one
- Multi-currency
- Wallet name
- Rename Wallet
- Single-currency
- My Wallets
- Unlock all
- Unlock all with %s
- Transaction history
- Blockchain is unreachable. Try later
- Balance is loading…
- Scan the card
- Transaction is in progress…
- Verified Balance
- Actions
- Choose wallet option
- Do you want to buy or sell crypto?
- Requesting to sign a message.\n\n%s
- Dapp %1$s, requesting to\nsign BNB transaction.\n\n%2$s
- Trade order for %1$s\nPrice: %2$s\nAmount to receive: %3$s\nAmount to pay: %4$s
- Transaction details:\nFrom: %1$s\nTo: %2$s\nAmount: %3$s
- The BNB transaction has been successfully signed and sent to the Dapp
- Clipboard contain WalletConnect code. Use copied value or scan QR-code
- Request to create transaction for %1$s\n%2$s\n\nAmount: %3$s\nFee: %4$s\nTotal: %5$s\nBalance: %6$s
- Can\'t send transaction. Not enough funds.
- Failed to establish WalletConnect session. Please, try again later.
- Not all tokens were added to your list. Please add them first and try again. Missing tokens:\n
- Failed to sign message.\nPlease, try again
- Failed to establish WalletConnect session: timeout error. Please, try again later.
- Session request contains unsupported blockchains for WalletConnect connection. Unsupported blockchains:\n
- Connection with this Dapp cannot be established due to its technical implementation.
- We\'ve encountered unknown error. Error message: %s. If the problem persists — feel free to contact our support
- Wrong card selected in Tangem App
- Failed to create transaction from Dapp data. Code: %s
- We\'ve encountered unknown error. Error code: %d. If the problem persists — feel free to contact our support
- The message has been successfully signed and sent to the Dapp
- %s network not found. Please, add it first and try again.
- No opened WalletConnect sessions
- Ooops. No Sessions.
- Failed to pairing WalletConnect session: %1$s
- Paste from clipboard
- Message for %1$s:\n%2$s
- Request to start a session for\n%1$s\n\nNETWORK: %2$s\n\nURL: %3$s
- The operation couldn\'t be completed.\n\nYou have already established a WalletConnect session with this parameters.
- Scan new code
- This card can\'t be used to establish WalletConnect session
- This network is not supported. Please select another network.
- Select network
- Dapp didn\'t provide essential data to establish WalletConnect session
- Failed to find session for request
- WalletConnect Sessions
- Connect to dApps
- WalletConnect
- The transaction has been successfully signed and sent to the Dapp
- The transaction has been succesfully signed and sent to the blockchain
- Failed to find transaction hash
- %s network
- Account is not created
- This card is not supported
- Your Tangem card was made to work with a different application. Please see the name and instructions on your card, and install the correct app
- %s Market Price
- last 24h
- %s network
- Address was copied to clipboard
- No internet connection
- Receiving
- from %s
- Sending
- to %s
- Tangem
- Use %s or scan a card to unlock access to your wallet
- Could be better
- Learn more
- Like it
- Ok, Got it!
- Really cool!
- Refresh
- You are currently in the Demo mode
- Demo mode active
- The card you scanned is a developer card. Do not use it to create your wallet.
- Not for users!
- %1$s network requires an Existential Deposit. If your account drops below %2$s, it will be deactivated, and any remaining funds will be destroyed.
- Network requires Existential Deposit
- Swap will be available after the %s transaction is complete
- You have active transaction
- You do not have any %s exchangeable coins in your list
- No available tokens to swap
- To make a transaction you need to deposit some %1$s %2$s
- Unable to cover %s fee
- Service temporarily unavailable
- Please change the amount to swap
- The amount to swap must be at least %s
- The amount of tokens to be swapped must not exceed %s
- This card might be a production sample or counterfeit
- Authenticity check failed
- Only %s signatures are left on this card. You must withdraw all of your funds.
- Low signature count
- Tokens on different networks can have different addresses. Double-check that your address matches the network when you transfer funds.
-
- - Use your card to get an address for %d network
- - Use your card to get an addresses for %d networks
-
- Some addresses are missing
- The network is currently unreachable. Please try again later.
- Network is unreachable
- Top up your wallet
- Your wallet hasn\'t been backed up. Carry out this procedure to protect your assets now.
- Missing backup
- This card has been previously used for transactions. If received from an untrusted source, consider withdrawing all funds. If it\'s your card, no action is required.
- Card has already signed transactions
- Device incompatibility detected
- Your review keeps us motivated to make Tangem Wallet even better
- Enjoying Tangem?
- Network rent fee required
- %1$s is an asset in the %2$s network. To make a %3$s transaction, you must deposit some %4$s (%5$s) to cover the network fee.
- Insufficient %1$s to cover network fee
- Sending funds will be available once the %s transaction is complete
- Transaction pending
- Solana network charges a rent of %1$s every 2 days. Accounts that can\'t afford the rent are purged from the network. Deposit your account with more than %2$s to use it for free.
- Some networks currently are unreachable. Please try again later.
- Some networks are unreachable
- This is a Testnet card. It cannot process transactions and should only be used for testing and development purposes.
- For testing purposes only
- Discard
- You have an interrupted backup. Do you want to resume?
- Yes, resume
- Discard
- If you will discard the backup now, then you will have to reset the cards to factory settings to start over again
- Resume backup
- This is an irreversible action
- Log in with %s
- Scan card
- Use %s or scan a card to access the app
- Welcome back!
+ Add custom token
+ Manage tokens
+ Send only %1$s (%2$s) from %3$s network to this address. Using other tokens and networks may result in loss of funds.
+ Request support
+ This feature is disabled in Demo mode
+ Reason: %s
+ Can\'t send a transaction
+ The selected does not support the %1$s network
+ To activate the %1$s blockchain\'s cryptographic encryption, you\'ll need to reset the wallet to factory settings. Please withdraw your funds before doing so to ensure that you don\'t lose them, and then complete the reset process. Access to the current wallet will not be possible after the reset.
+ Tokens in %1$s network are not supported by this card due to firmware limitation.
+ Are you having difficulty scanning your card?
+ This card is not designed to work with this app
+ Default Fee
+ Enable Default Fee to set transaction fees automatically and skip the Fee page when sending funds. You can always go back to this page if necessary.
+ Go to settings to enable biometric authentication in the Tangem App
+ Enable biometric authentication
+ This will delete all the saved wallet access codes. Any further operation with the wallet will require submitting the access code.
+ Removing the saved card deletes all the saved wallets and their access codes from the app.
+ Save Access Code
+ Biometric authentication will be requested instead of the access code for interactions with your card.
+ Keep the wallet in the app
+ Enable to link all the wallets to Tangem app. Biometric authentication will be required for unlocking the app. Transaction signing requires tapping your Tangem card.
+ Dark
+ Light
+ System default
+ Theme
+ App Settings
+ To hide or show your balances, simply flip your device screen down, or switch it off in Settings
+ Don\'t show again
+ Got it
+ Balances are hidden
+ Please scan the card
+ Please try again in 30 seconds or scan the card
+ Too many attempts
+ You have disabled biometric authentication on your phone and will not be able to save wallets in the app. To save wallets, please enable the biometric authentication function in your phone settings.
+ Start backup process
+
+ - %d card
+ - %d cards
+
+ Disable this option if you don\'t want this card to be used to reset access codes on other cards in this wallet. Please note that this will also prevent you from resetting the access code on this card.
+ Allows you to use this card to reset access code on other cards in this wallet
+ Access code recovery
+ Reset
+ Are you sure you want to do this?
+ Change Access Code
+ Access code will be changed on this card only
+ Reset to Factory Settings
+ Security Mode
+ Card Settings
+ Accept
+ Access denied
+ Apply
+ Approval
+ Attention
+ Balance: %s
+ Balance
+ biometric authentication
+ biometrics
+ Buy
+ Go to %1$s
+ You have not given access to your camera, please adjust your privacy settings
+ Cancel
+ Close
+ Continue
+ Copy
+ Copy address
+ Create
+ Delete
+ Disabled
+ Done
+ Enable
+ Enabled
+ Error
+ Explore
+ Explore transaction history
+ Explorer
+ Fee
+ Network fees are charges users pay to process and confirm transactions. The fee amount can be affected by network congestion, transaction size, and execution priority. %s
+ Custom
+ Fast
+ Market
+ Slow
+ Speed and fee
+ Get addresses
+ Go to provider
+ Import
+ Locked
+ Main network
+ Network fee
+ Next
+ No
+ No address
+ OK
+ Primary Card
+ Passphrase
+ Read more
+ Receive
+ Reject
+ Reload
+ Rename
+ Save changes
+ Search
+ Search tokens
+ Seed phrase
+ Select action
+ Sell
+ Send
+ The server is not available, please try again later
+ Share
+ Sign
+ Sign and send
+ Start
+ Submit
+ Success
+ Support
+ Swap
+ terms and conditions
+ Transaction failed
+ Transactions
+ Transfer
+ I understand
+ There was an error. Please try again.
+ Unreachable
+ Yes
+ Contract address copied!
+ Available networks
+ Add token
+ Contract address
+ Contract address is invalid
+ Please select the network
+ Decimal must be a valid integer, up to %li
+ Custom derivation
+ E. g. m/00\'/0000\'/0\'/0/0
+ Enter custom derivation
+ Decimals
+ Derivation Path
+ Default
+ BIP44 coin type
+ The derivation path you\'ve entered is not valid
+ E.g. USD Coin
+ Name
+ Not selected
+ Network
+ Token network
+ You can manually add a token that is not natively supported by Tangem
+ E.g. USDC
+ Symbol
+ Token symbol
+ This token/network has already been added to your list
+ Note that tokens can be created by anyone. Be aware of adding scam tokens, they can cost nothing.
+ Be aware of adding scam tokens, they can cost nothing
+ Note that tokens can be created by anyone
+ Chat
+ Access code
+ You will have to submit the correct access code before scanning the card
+ Long Tap
+ This mechanism protects against proximity attacks on a card. It will enforce a delay between reception and execution of a command.
+ Passcode
+ Before executing any command entailing a change of the card state, you will have to enter the passcode.
+ Referral program
+ Flip your device screen down to quickly hide and show balances
+ %s hashes
+ Card ID
+ Contact to Support
+ Link More Cards
+ App Currency
+ Flip-to-Hide Balances
+ Issuer
+ Signed
+ Details
+ Check your internet connection or switch to a different network
+ Terms of Service
+ You have used a card from another wallet. Tap the card associated with this wallet
+ My tokens
+ You haven\'t added any tokens yet. Add tokens via Market to swap
+ Cannot be swapped for %s
+ Provided by
+ Status
+ Tangem offers token swaps via 3rd-party providers according to each provider\'s terms
+ Choose provider
+ An error occurred. Code: %s
+ Oops! Swapping the selected pair through the chosen provider is temporarily unavailable. Please try again later. (Code: %s)
+ Selected provider is unavailable at the moment. Please try again later. (Code: %s)
+ Swaps are unavailable at the moment. Please try again later. (Code: %s)
+ Estimated amount
+ Exchange by %s
+ Visit provider’s website to refund your money
+ Operation failed by provider
+ Visit provider’s website for verification
+ KYC verification required by provider
+ Canceled
+ Confirmed
+ Confirming
+ Confirming...
+ Exchanged
+ Exchanging
+ Exchanging...
+ Failed
+ Deposit received
+ Awaiting deposit
+ Awaiting deposit...
+ Refunded
+ Sending to you
+ Sending to you...
+ Sent
+ Provider-sourced data. Estimated amount subject to change due to market conditions.
+ Exchange status
+ Verification required
+ List of all tokens added to your wallet
+ Fetching best rates...
+ Floating rate
+ By using swap functionality, you agree with provider’s %s
+ By using swap functionality, you agree with provider’s %1$s and %2$s
+ More providers are coming soon.\nStay tuned!
+ Privacy Policy
+ Provider
+ Best rate
+ Available up to %s
+ Available from %s
+ Unavailable for this pair
+ Permission Required
+ Terms of Use
+ No tokens found. Please try another request
+ ID: %s
+ Transaction ID copied
+ The following information is optional. You can erase it if you don\'t want to share it.
+ Tell us what functions you are missing, and we will try to help you.
+ Please tell us what card do you have
+ Hi support team,
+ Please tell us more about your issue. Every small detail can help.
+ My suggestions
+ Can\'t scan a card
+ Feedback
+ Tangem feedback
+ Can\'t send a transaction
+ Order card
+ Scan card
+ To change the access code tap the card as shown above and do not remove until the end of the operation
+ To change the passcode tap the card as shown above and do not remove until the end of the operation
+ To create the wallet tap the card as shown above and do not remove until the end of the operation
+ Tap to scan
+ Tap to sign
+ Tap the card
+ You have updated biometrics, scan your card to enter
+ To begin tracking your crypto assets and transactions, add tokens
+ Manage tokens
+ To access all the networks you need to scan the card
+ Scan your card
+ Enjoy %1$s service fees on swaps via Changelly from February %2$s-%3$s
+ Swap with Changelly, %s fees
+ Tokens
+ Add
+ Edit
+ Coin market cap
+ Blockchain the cryptocurrency was initially created
+ Native network
+ Using non-native networks for tokens enables cross-blockchain interoperability, allowing assets to be utilized in diverse decentralized applications and smart contracts across platforms. However, this often involves a custodian or smart contract to hold the original asset securely, introducing centralization and counterparty risk.
+ Not original or primary blockchain the token is hosted
+ Non-native networks
+ Choose networks
+ Wallet
+ Couldn’t find this token, you can add it manually
+
+ - %1$d of %2$d wallet
+ - %1$d of %2$d wallets
+
+ e.g. BTC I trust, hodl I must
+ The selected token is currently unavailable for actions within the crypto wallet. But worry not, you can express your interest by upvoting it.
+ Upvote
+ Choose wallet
+ The wallet doesn\'t support more than one network
+ You have to set up a single access code to protect all your cards
+ Protect
+ You can set up an individual access code on each card later
+ Personalize
+ The access code can be restored with a linked card. Don’t keep all cards at one place.
+ Restore
+ Choose any word, phrase, or number you want as your access code
+ Create Access Code
+ Enter your access code one more time to avoid a mistake
+ Re-enter your Access Code
+ Access code must be at least 4 characters long
+ Entered access code didn\'t match the initial access code
+ Please repeat the operation. The card will be reset to factory settings.
+ Activation error
+ You\'ve added one backup card. When backup process is finished you can\'t add more backup cards. If you have one more card, add it to backup. Do you like to continue the backup process?
+ The backup process is partly complete. You can\'t exit it now.
+ The passphrase is an advanced security feature that crypto wallets use. It adds an extra word or phrase of your own choosing to your already existing recovery phrase to unlock a brand-new set of addresses.
+ Add a backup card
+ Scan the card #%d
+ Backup now
+ Scan the primary card
+ Continue to my wallet
+ Finalize the backup
+ Receive crypto
+ Scan primary card
+ Skip for later
+ How does it work?
+ Let\'s generate all the keys on your card and create a secure wallet
+ Create wallet
+ Create a wallet
+ Other options
+ Your keys will be securely generated inside the card. There is no seed phrase, which means nobody can export or steal it.
+ Generate keys privately
+ Your card is activated and ready to be used
+ Success!
+ In this case, you will need to start from the beginning.
+ Do you want to exit the activation process?
+ Getting started
+ Another wallet has already been created on the card you\'re trying to add. If you have funds in this wallet, please withdraw it and then reset this card and add it as a backup.
+ Creating a backup
+ Read more about seed phrase
+
+
+ - Write these %d words down in the order given below and store them in a safe and secret place.
+
+ Your seed phrase
+
+
+ - %d words
+
+ To import your wallet, enter your seed phrase in the field below
+ Generate seed phrase
+ Import wallet
+ A seed phrase is a series of words that allows you to recover your wallet. Unlike the keys generated by the card, seed phrases are unprotected and can be copied and stolen. Use this option at your own risk.
+ Use seed phrase
+ Invalid seed phrase. Please check the word order.
+ Invalid seed phrase. Please check your spelling.
+ Legacy
+ To check whether you’ve written down your seed phrase correctly, please enter the 2nd, 7th and 11th words
+ So, let’s check
+ To start the backup process add up to two backup cards.
+ You can add one more card or finalize the backup process
+ Prepare the backup card with number %s
+ Scan the primary card to start the backup process.
+ Prepare the primary card with number %s
+ Your wallet card is configured and ready for use.
+ Max number of cards added. Finalize the backup process.
+ Activating card
+ Backup card #%d
+ No backup cards
+ One backup card added
+ Prepare your card
+ Two backup cards added
+ To get started, simply top up the wallet with any amount
+ To get started, simply top up the wallet with more than %1$s %2$s
+ Buy crypto
+ Show the wallet\'s address
+ Activate a wallet
+ The twinning process is partly complete. You can\'t exit it now.
+ If the process of creating the wallet gets interrupted in any way, you\'ll have to start over
+ You can backup your keys up to two other blank Tangem Wallet cards.
+ Access code can be restored with one of backup cards.
+ All the backup cards can be used as full-functional with the identical keys.
+ You will be able to set an access code to protect your wallets.
+ Backup wallet
+ Access code restore
+ Identical cards
+ Access code
+ Group
+ By balance
+ Organize tokens
+ Ungroup
+ Select from the gallery
+ Settings
+ You have not given access to your camera
+ Camera access denied
+ Enjoying our app?
+ %1$s (%2$s) on %3$s network
+ Send only %s to this address. Sending any other currency will result in its irreversible loss.
+ Participate
+ Failed to load the information about the referral program. Please try again later.
+ Failed to load the information about the referral program. Error code: %s. Please try again later.
+ Upcoming payments
+ Your friends bought
+ Less
+ More
+ No upcoming payments
+
+ - for %d wallet
+ - for %d wallets
+
+ Will get ^^%1$s^^ for each wallet bought by your friend on your %2$s network address %3$s ^^30 days after^^ that
+ You
+ Will get a
+ when buying a wallet on tangem.com
+ %s discount
+ Your friend
+ Personal code copied!
+ Your personal code
+ Buy Tangem Wallet with discount!\n%s
+ Refer your friends to Tangem
+ You\'ve accepted
+ By tapping this button you accept
+ of the referral program
+
+ - %d wallet
+ - %d wallets
+
+ Reset the Card
+ I understand that after performing this action, I will no longer have access to the current wallet
+ I realize that I can\'t use this card to recover my access code on the other cards of the current wallet
+ Factory Reset will completely delete the wallet from the selected card. You will not be able to restore the current wallet or use the card to recover the access code.
+ Factory Reset will completely delete the wallet from the selected card and remove it from the app. You will not be able to restore the current wallet.
+ Do you have a bank card from another country and a residence permit or registration outside the Russian Federation?
+ Russian bank cards are not currently accepted
+ Log into the app and check your balance without scanning the card
+ Access the app
+ Allow to use biometrics
+ Biometrics will be requested instead of the access code for interactions with your wallet
+ Access code
+ It looks like you have biometric authentication disabled, it is necessary to save wallets
+ Enable biometric authorization
+ Would you like to use biometrics?
+ Note that making a transaction with your funds will still require your card
+ Scan Card
+ Scan the card to change its settings. The changes will impact only the card you\'ve scanned and will not affect other cards tied to your wallet.
+ Get your card ready!
+ Already included in the entered address
+ Subtract
+ Not enough funds to cover the network fee. Do you want to subtract the amount required to cover the fee?
+ The commission amount is %s times the recommended amount. Make sure that the custom settings are correct.
+ You specified a commission below the recommended amount, which could cause a delay in your transaction. Continue?
+ Reason: %1$s\nCode: %2$s
+ The transaction is not completed
+ Amount
+ %1$s, %2$s
+ Address
+ Destination Tag
+ Enter address
+ Address is the same as wallet address
+ Invalid Tag. It won\'t be added to the transaction.
+ Invalid Memo. It won\'t be added to the transaction.
+ Tag
+ Memo
+ Include fee
+ Low
+ Normal
+ Priority
+ Check your network connection
+ Network fee info unreachable
+ From
+ Gas limit
+ This is the maximum amount of gas that will be spent to complete a transaction or contract. A gas limit prevents unexpected or unlimited charges when executing a transaction.
+ Gas price
+ This is the cost you are willing to pay for each unit of gas. The higher the gas price, the faster your transaction will be processed.
+ Max
+ Maximum amount
+ Max fee
+ The fee that will be charged for your transaction. You can set your own value.
+ Numbers only for Destination Tag
+ Sending amount will be reduced by %1$s to cover the selected commission level. The recipient will get %2$s.
+ Network fee coverage
+ Insufficient funds for the transfer, as the total of the fee and transfer amount exceeds the existing balance
+ Total exceeds balance
+ The account will be wiped from the blockchain if a balance goes below the existential deposit. Please ensure that the remaining balance after sending will not be less than %s.
+ Existential deposit
+ The commission amount is %s times the recommended amount. Make sure that the custom settings are correct.
+ Custom fee is high
+ The fee for transferring the entire balance is higher. To reduce the commission, you can leave %s.
+ Fee is increased
+ The included commission exceeds the transfer amount, leading to a negative value
+ Invalid amount
+ The minimum sending amount is %1$s. Please ensure that the remaining balance after sending will not be less than %2$s.
+ Target account is not created. Please change the amount to send.
+ The amount to send must be at least %s
+ Reduce by %s
+ Reduce to %s
+ Kindly be aware that your transaction may experience delays under specific fee settings
+ Transaction delays are possible
+ Due to %1$s limitations only %2$s UTXOs can fit in a single transaction. This means you can only send %3$s or less. You need to reduce the amount.
+ Transaction limitation
+ Optional
+ Please align your QR code with the square to scan it. Ensure you scan %s network address.
+ Recent
+ Recipient
+ Not a valid address
+ Ensure that you are sending funds to an %s wallet address. Errors may result in the loss of your tokens
+ Send to
+ A Memo/Destination Tag is a unique ID for differentiating transactions sent to the same recipient on the same network. Caution: Omitting a memo may lead to misplaced funds
+ My wallets
+ The fee for a Bitcoin transaction is measured by the number of the smallest Bitcoin unit (Satoshi) per byte of data. The higher this number, the faster the transaction will be processed.
+ Satoshi per vbyte
+ Sending...
+ Tap any field to change it
+ Send %s
+ You are sending %1$s including a network fee of %2$s
+ Sending %s
+ Total
+ %1$s and %2$s will be sent
+ ≈ %1$s (inc. fee: %2$s)
+ %s will be sent
+ Transaction has been successfully signed and sent to the blockchain node. Wallet balance will be updated in a while
+ Invalid address
+ %1$s (%2$s)
+ Transaction sent
+ Store your crypto assets secure while keeping private keys contained in your card
+ Revolutionary Hardware Wallet
+ Up to 3 physical cards to one wallet
+ Ultra Secure Backup
+ A hardware wallet for your Bitcoin, Ethereum and many more currencies simultaneously – all in one card
+ Thousands of Currencies
+ Use it on the go, anywhere, anytime. No wires or batteries. Just tap the card to your phone when you need your crypto.
+ The Wallet for Everyone
+ Meet Tangem
+ Exchange, buy NFT\'s, make loans and deposits in more than 100 different decentralized services
+ Web 3.0 Compatible
+ The amount includes:\n• service provider\'s fee\n• network fee for sending %s from the exchange back to the user\'s address.
+ The amount includes the service provider\'s fee.
+ Fees
+ All decentralized exchanges require approvals to prevent smart contracts from accessing your wallet without your permission. By design, smart contracts can\'t access your tokens unless you approve. By \"unlocking\" your tokens, you authorize the 1-inch smart contract to spend them. The network\'s miners receive a gas fee (paid by you) to record this action on the blockchain. You can swap your token after giving approval.
+ Approve
+ You swap
+ Give Permission
+ Swapping this amount of selected tokens will cause a significant price impact and reduce your outcome.
+ Insufficient funds
+ Sending amount will be reduced to cover the selected fee level
+ Approve
+ Current transaction
+ The network will charge a token approval fee to verify that you are authorizing the use of your token for the swap.
+ Give Permission
+ Specify the approve limit for the selected token
+ Amount %s
+ To continue, grant 1inch smart contracts permission to use your %s
+ Unlimited
+ In progress
+ Swap
+ You receive
+ Choose token
+ not available
+ Balances hidden
+ Balances shown
+ Undo
+ The purchase of the %s is currently unavailable. But we are working on adding it.
+ You do not have funds to send. Top up your account to be able to send funds from it.
+ This operation is currently unavailable. Please try again later.
+ %s swap is not available. But we are working on adding it.
+ Sell of the %s coin is currently unavailable. But we are working on adding it.
+ Generate XPUB
+ Hide
+ You are about to hide this token from the main screen. You can add it back anytime through the manage tokens page.
+ Hide %s
+ Hide token
+ %1$s token in %%image%% %2$s network
+ The %1$s token is the main currency on the %2$s network and cannot be hidden as long as you have other tokens on this network in the list.
+ Unable to hide %s
+ Exchange this token for another at %1$s service fees from February %2$s-%3$s.
+ Swap with Changelly, %s fees
+ Swap now
+ contract: %s
+ You don\'t have any transactions yet
+ Failed to load transaction history.\nClick on reload button to update the information.
+ Multiple addresses
+ Transaction history is currently not supported for this blockchain. But don\'t worry, we\'re working on it! In the meantime you can check it in the explorer.
+ Operation
+ from: %s
+ to: %s
+ You\'ve scanned the same card. To create a twin wallet you need to scan the card with number %d
+ You\'ve scanned wrong twin card. Please try another one
+ This one that you are holding in your hands and the other one with number %s.\n\nBoth cards can be used to extract funds from this wallet.
+ One wallet. Two cards.
+ Scan the card #%s
+ Creating wallet
+ Scan the #%s twin card
+ Preparing card
+ Tangem Twin
+ This action is irreversible. You will not have access to the old wallet.
+ Tap the twin card with number %s and do not remove until the end of the operation
+ Use %s or scan a card to have an access to your wallet
+ Add new wallet
+ Are you sure you want to delete this wallet?
+ An error has occurred, please scan your card to log in
+ This wallet has already been saved, you can add another one
+ Wallet name
+ Rename Wallet
+ Unlock all
+ Unlock all with %s
+ Blockchain is unreachable. Try later
+ Scan the card
+ Requesting to sign a message.\n\n%s
+ Dapp %1$s, requesting to\nsign BNB transaction.\n\n%2$s
+ Trade order for %1$s\nPrice: %2$s\nAmount to receive: %3$s\nAmount to pay: %4$s
+ Transaction details:\nFrom: %1$s\nTo: %2$s\nAmount: %3$s
+ Clipboard contain WalletConnect code. Use copied value or scan QR-code
+ Request to create transaction for %1$s\n%2$s\n\nAmount: %3$s\nFee: %4$s\nTotal: %5$s\nBalance: %6$s
+ Can\'t send transaction. Not enough funds.
+ Failed to establish WalletConnect session. Please, try again later.
+ Not all tokens were added to your list. Please add them first and try again. Missing tokens:\n
+ Failed to sign message.\nPlease, try again
+ Failed to establish WalletConnect session: timeout error. Please, try again later.
+ Session request contains unsupported blockchains for WalletConnect connection. Unsupported blockchains:\n
+ Connection with this Dapp cannot be established due to its technical implementation.
+ We\'ve encountered unknown error. Error message: %s. If the problem persists — feel free to contact our support
+ Wrong card selected in Tangem App
+ Failed to create transaction from Dapp data. Code: %s
+ We\'ve encountered unknown error. Error code: %d. If the problem persists — feel free to contact our support
+ No opened WalletConnect sessions
+ Ooops. No Sessions.
+ Failed to pairing WalletConnect session: %1$s
+ Paste from clipboard
+ Message for %1$s:\n%2$s
+ Request to start a session for\n%1$s\n\nNETWORK: %2$s\n\nURL: %3$s
+ The operation couldn\'t be completed.\n\nYou have already established a WalletConnect session with this parameters.
+ Scan new code
+ This card can\'t be used to establish WalletConnect session
+ This network is not supported. Please select another network.
+ Select network
+ WalletConnect Sessions
+ Connect to dApps
+ WalletConnect
+ %s Market Price
+ last 24h
+ %s network
+ Address was copied to clipboard
+ No internet connection
+ Tangem
+ Use %s or scan a card to unlock access to your wallet
+ Please withdraw all funds from this wallet, reset it to factory settings, and create a new one. Access to the current wallet will be lost.
+ Activation error
+ Could be better
+ Like it
+ Ok, Got it!
+ Really cool!
+ Refresh
+ You are currently in the Demo mode
+ Demo mode active
+ The card you scanned is a developer card. Do not use it to create your wallet.
+ Not for users!
+ %1$s network requires an Existential Deposit. If your account drops below %2$s, it will be deactivated, and any remaining funds will be destroyed.
+ Network requires Existential Deposit
+ Swap will be available after the %s transaction is complete
+ You have active transaction
+ Swap approval is underway and will be completed shortly
+ Approval in progress
+ The minimum swapping amount is %1$s. Please ensure that the remaining balance after the swap will not be less than %2$s.
+ You do not have any %s exchangeable coins in your list
+ No available tokens to swap
+ To make a transaction you need to deposit some %1$s %2$s
+ Unable to cover %s fee
+ The amount to receive must be at least %s
+ Service temporarily unavailable
+ The amount of tokens to be swapped must not exceed %s
+ The amount to swap must be at least %s
+ Please change the amount to swap
+ This card might be a production sample or counterfeit
+ Authenticity check failed
+ Only %s signatures are left on this card. You must withdraw all of your funds.
+ Low signature count
+ Tokens on different networks can have different addresses. Double-check that your address matches the network when you transfer funds.
+
+ - Use your card to get an address for %d network
+ - Use your card to get an addresses for %d networks
+
+ Some addresses are missing
+ The network is currently unreachable. Please try again later.
+ Network is unreachable
+ Top up your wallet
+ Your wallet hasn\'t been backed up. Carry out this procedure to protect your assets now.
+ Missing backup
+ This card has been previously used for transactions. If received from an untrusted source, consider withdrawing all funds. If it\'s your card, no action is required.
+ Card has already signed transactions
+ Your review keeps us motivated to make Tangem Wallet even better
+ Enjoying Tangem?
+ Network rent fee required
+ %1$s is an asset in the %2$s network. To make a %3$s transaction, you must deposit some %4$s (%5$s) to cover the network fee.
+ Insufficient %1$s to cover network fee
+ Sending funds will be available once the pending transaction(s) in network %s is complete
+ Sending funds will be available once the %s transaction is complete
+ Transaction pending
+ The Solana network is congested. If your transaction is not processed within 2 minutes, please repeat the transaction.
+ Solana Network Alert
+ Solana network charges a rent of %1$s every 2 days. Accounts that can\'t afford the rent are purged from the network. Deposit your account with more than %2$s to use it for free.
+ Some networks currently are unreachable. Please try again later.
+ Some networks are unreachable
+ This is a Testnet card. It cannot process transactions and should only be used for testing and development purposes.
+ For testing purposes only
+ Discard
+ You have an interrupted backup. Do you want to resume?
+ Yes, resume
+ Discard
+ If you will discard the backup now, then you will have to reset the cards to factory settings to start over again
+ Resume backup
+ This is an irreversible action
+ Log in with %s
+ Scan card
+ Use %s or scan a card to access the app
+ Welcome back!
diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/TextFields.kt b/core/ui/src/main/java/com/tangem/core/ui/components/TextFields.kt
index e954cbd32d..f8312aa659 100644
--- a/core/ui/src/main/java/com/tangem/core/ui/components/TextFields.kt
+++ b/core/ui/src/main/java/com/tangem/core/ui/components/TextFields.kt
@@ -1,5 +1,6 @@
package com.tangem.core.ui.components
+import androidx.annotation.DrawableRes
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.core.tween
@@ -59,6 +60,40 @@ fun OutlineTextField(
)
}
+@Composable
+fun OutlineTextFieldWithIcon(
+ value: TextFieldValue,
+ onValueChange: (TextFieldValue) -> Unit,
+ @DrawableRes iconResId: Int,
+ iconColor: Color,
+ onIconClick: () -> Unit,
+ modifier: Modifier = Modifier,
+ label: String? = null,
+ placeholder: String? = null,
+ enabled: Boolean = true,
+ isError: Boolean = false,
+ keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
+ keyboardActions: KeyboardActions = KeyboardActions(),
+ interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
+) {
+ TangemTextFieldWithIcon(
+ modifier = modifier,
+ value = value,
+ onValueChange = onValueChange,
+ iconResId = iconResId,
+ iconColor = iconColor,
+ singleLine = true,
+ label = label,
+ placeholder = placeholder,
+ enabled = enabled,
+ isError = isError,
+ keyboardOptions = keyboardOptions,
+ keyboardActions = keyboardActions,
+ interactionSource = interactionSource,
+ onIconClick = onIconClick,
+ )
+}
+
// region Defaults
@Suppress("LongMethod")
@Composable
@@ -168,6 +203,84 @@ private fun TangemTextField(
}
}
+@Suppress("LongMethod")
+@Composable
+private fun TangemTextFieldWithIcon(
+ value: TextFieldValue,
+ singleLine: Boolean,
+ onValueChange: (TextFieldValue) -> Unit,
+ @DrawableRes iconResId: Int,
+ iconColor: Color,
+ modifier: Modifier = Modifier,
+ label: String? = null,
+ placeholder: String? = null,
+ enabled: Boolean = true,
+ isError: Boolean = false,
+ visualTransformation: VisualTransformation = VisualTransformation.None,
+ keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
+ keyboardActions: KeyboardActions = KeyboardActions(),
+ interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
+ colors: TangemTextFieldColors = TangemTextFieldsDefault.defaultTextFieldColors,
+ size: TangemTextFieldSize = TangemTextFieldSize.Default,
+ onIconClick: () -> Unit = {},
+) {
+ Column(modifier = modifier) {
+ OutlinedTextField(
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(IntrinsicSize.Min)
+ .heightIn(size.toHeightDp()),
+ value = value,
+ textStyle = TangemTheme.typography.body1,
+ onValueChange = onValueChange,
+ singleLine = singleLine,
+ enabled = enabled,
+ isError = isError,
+ visualTransformation = visualTransformation,
+ keyboardActions = keyboardActions,
+ keyboardOptions = keyboardOptions,
+ interactionSource = interactionSource,
+ shape = size.toShape(),
+ colors = colors,
+ label = {
+ if (!label.isNullOrEmpty()) {
+ Text(
+ text = label,
+ style = TangemTheme.typography.caption2,
+ color = colors.labelColor(
+ enabled = enabled,
+ error = isError,
+ interactionSource = interactionSource,
+ ).value,
+ )
+ }
+ },
+ placeholder = {
+ if (!placeholder.isNullOrEmpty()) {
+ Text(
+ text = placeholder,
+ style = TangemTheme.typography.body1,
+ color = colors.placeholderColor(enabled = enabled).value,
+ )
+ }
+ },
+ trailingIcon = {
+ IconButton(
+ modifier = Modifier.size(32.dp),
+ onClick = onIconClick,
+ ) {
+ Icon(
+ modifier = Modifier.size(24.dp),
+ painter = painterResource(id = iconResId),
+ tint = iconColor,
+ contentDescription = "Clear input",
+ )
+ }
+ },
+ )
+ }
+}
+
private enum class TangemTextFieldSize {
Default,
}
@@ -378,6 +491,16 @@ private fun OutlineTextFieldSample(modifier: Modifier = Modifier) {
caption = "Supporting text",
)
Divider(modifier = Modifier.padding(vertical = 8.dp))
+ OutlineTextFieldWithIcon(
+ value = TextFieldValue(),
+ onValueChange = { /* no-op */ },
+ iconResId = R.drawable.ic_alert_24,
+ iconColor = TangemTheme.colors.icon.informative,
+ label = "Default without value",
+ placeholder = "Placeholder",
+ onIconClick = {},
+ )
+ Divider(modifier = Modifier.padding(vertical = 8.dp))
}
}
diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/Warnings.kt b/core/ui/src/main/java/com/tangem/core/ui/components/Warnings.kt
index d0d3315f20..fe2a681d08 100644
--- a/core/ui/src/main/java/com/tangem/core/ui/components/Warnings.kt
+++ b/core/ui/src/main/java/com/tangem/core/ui/components/Warnings.kt
@@ -102,7 +102,7 @@ fun ClickableWarningCard(
* >Figma component
*/
@Composable
-fun RefreshableWaringCard(
+fun RefreshableWarningCard(
title: String,
description: String,
icon: @Composable (() -> Unit)? = null,
@@ -193,7 +193,7 @@ private fun WarningsPreview() {
onClick = {},
)
SpacerH32()
- RefreshableWaringCard(
+ RefreshableWarningCard(
title = "Exchange rate has expired",
description = "To access all the networks, you need to scan the card.",
onClick = {},
diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/appbar/AppBarWithBackButtonAndIcon.kt b/core/ui/src/main/java/com/tangem/core/ui/components/appbar/AppBarWithBackButtonAndIcon.kt
index c9e0526b4e..405217433f 100644
--- a/core/ui/src/main/java/com/tangem/core/ui/components/appbar/AppBarWithBackButtonAndIcon.kt
+++ b/core/ui/src/main/java/com/tangem/core/ui/components/appbar/AppBarWithBackButtonAndIcon.kt
@@ -18,6 +18,7 @@ fun AppBarWithBackButtonAndIcon(
onBackClick: () -> Unit,
modifier: Modifier = Modifier,
text: String? = null,
+ subtitle: String? = null,
@DrawableRes backIconRes: Int? = null,
@DrawableRes iconRes: Int? = null,
onIconClick: (() -> Unit)? = null,
@@ -27,6 +28,7 @@ fun AppBarWithBackButtonAndIcon(
onBackClick = onBackClick,
modifier = modifier,
text = text,
+ subtitle = subtitle,
backIconRes = backIconRes,
backgroundColor = backgroundColor,
iconContent = {
diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/appbar/AppBarWithBackButtonAndIconContent.kt b/core/ui/src/main/java/com/tangem/core/ui/components/appbar/AppBarWithBackButtonAndIconContent.kt
index 6402068907..09b6158abb 100644
--- a/core/ui/src/main/java/com/tangem/core/ui/components/appbar/AppBarWithBackButtonAndIconContent.kt
+++ b/core/ui/src/main/java/com/tangem/core/ui/components/appbar/AppBarWithBackButtonAndIconContent.kt
@@ -1,10 +1,7 @@
package com.tangem.core.ui.components.appbar
import androidx.annotation.DrawableRes
-import androidx.compose.animation.AnimatedContent
-import androidx.compose.animation.fadeIn
-import androidx.compose.animation.fadeOut
-import androidx.compose.animation.togetherWith
+import androidx.compose.animation.*
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
@@ -37,6 +34,7 @@ fun AppBarWithBackButtonAndIconContent(
onBackClick: () -> Unit,
modifier: Modifier = Modifier,
text: String? = null,
+ subtitle: String? = null,
@DrawableRes backIconRes: Int? = null,
backIconTint: Color = TangemTheme.colors.icon.primary1,
backgroundColor: Color = TangemTheme.colors.background.secondary,
@@ -44,9 +42,10 @@ fun AppBarWithBackButtonAndIconContent(
) {
Row(
modifier = modifier
+ .height(TangemTheme.dimens.size56)
.background(color = backgroundColor)
.fillMaxWidth()
- .padding(all = TangemTheme.dimens.spacing16),
+ .padding(horizontal = TangemTheme.dimens.spacing16),
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing16),
verticalAlignment = Alignment.CenterVertically,
) {
@@ -54,6 +53,7 @@ fun AppBarWithBackButtonAndIconContent(
painter = painterResource(backIconRes ?: R.drawable.ic_back_24),
contentDescription = null,
modifier = Modifier
+ .padding(vertical = TangemTheme.dimens.spacing16)
.size(size = TangemTheme.dimens.size24)
.clickable(
interactionSource = remember { MutableInteractionSource() },
@@ -61,20 +61,37 @@ fun AppBarWithBackButtonAndIconContent(
) { onBackClick() },
tint = backIconTint,
)
- AnimatedContent(
- targetState = text,
+ Column(
+ verticalArrangement = Arrangement.Center,
modifier = Modifier.weight(1f),
- transitionSpec = { fadeIn().togetherWith(fadeOut()) },
- label = "Toolbar title change",
) {
- if (!it.isNullOrBlank()) {
+ AnimatedVisibility(
+ visible = !text.isNullOrBlank(),
+ enter = fadeIn(),
+ exit = fadeOut(),
+ label = "Toolbar title change",
+ ) {
Text(
- text = it,
+ text = text.orEmpty(),
color = TangemTheme.colors.text.primary1,
maxLines = 1,
style = TangemTheme.typography.subtitle1,
)
}
+ AnimatedVisibility(
+ visible = !subtitle.isNullOrBlank(),
+ enter = fadeIn().plus(expandVertically()),
+ exit = fadeOut().plus(shrinkVertically()),
+ label = "Toolbar subtitle change",
+ ) {
+ Text(
+ text = subtitle.orEmpty(),
+ color = TangemTheme.colors.text.secondary,
+ maxLines = 1,
+ style = TangemTheme.typography.caption2,
+ modifier = Modifier.animateContentSize(),
+ )
+ }
}
iconContent()
}
@@ -111,6 +128,7 @@ private fun PreviewAppBarWithBackButtonAndIconInDarkTheme() {
TangemTheme(isDark = true) {
AppBarWithBackButtonAndIconContent(
text = "Title",
+ subtitle = "Subtitle",
onBackClick = {},
iconContent = {
Row {
diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/buttons/common/TangemButtonsDefaults.kt b/core/ui/src/main/java/com/tangem/core/ui/components/buttons/common/TangemButtonsDefaults.kt
index db255b710f..5a555dfcf7 100644
--- a/core/ui/src/main/java/com/tangem/core/ui/components/buttons/common/TangemButtonsDefaults.kt
+++ b/core/ui/src/main/java/com/tangem/core/ui/components/buttons/common/TangemButtonsDefaults.kt
@@ -8,7 +8,7 @@ import androidx.compose.runtime.ReadOnlyComposable
import androidx.compose.ui.graphics.Color
import com.tangem.core.ui.res.TangemTheme
-internal object TangemButtonsDefaults {
+object TangemButtonsDefaults {
val elevation: ButtonElevation
@Composable
diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/buttons/segmentedbutton/SegmentedButton.kt b/core/ui/src/main/java/com/tangem/core/ui/components/buttons/segmentedbutton/SegmentedButton.kt
index 44d622f9eb..712fc8cb52 100644
--- a/core/ui/src/main/java/com/tangem/core/ui/components/buttons/segmentedbutton/SegmentedButton.kt
+++ b/core/ui/src/main/java/com/tangem/core/ui/components/buttons/segmentedbutton/SegmentedButton.kt
@@ -47,11 +47,15 @@ inline fun SegmentedButtons(
selectedColor: Color = TangemTheme.colors.background.action,
dividerColor: Color = TangemTheme.colors.stroke.primary,
showIndication: Boolean = true,
+ selectedItem: T? = null,
crossinline buttonContent: @Composable (T) -> Unit,
) {
if (config.isEmpty() || config.size == 1) return
- var selected by remember { mutableIntStateOf(0) }
+ var selected by remember {
+ val selectedIndex = if (selectedItem == null) 0 else config.indexOf(selectedItem)
+ mutableIntStateOf(selectedIndex)
+ }
Row(
modifier = modifier
diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/fields/AmountTextField.kt b/core/ui/src/main/java/com/tangem/core/ui/components/fields/AmountTextField.kt
index 4bf1b72d84..75836f873a 100644
--- a/core/ui/src/main/java/com/tangem/core/ui/components/fields/AmountTextField.kt
+++ b/core/ui/src/main/java/com/tangem/core/ui/components/fields/AmountTextField.kt
@@ -1,20 +1,27 @@
package com.tangem.core.ui.components.fields
+import androidx.annotation.FloatRange
import androidx.compose.foundation.background
-import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
-import androidx.compose.material3.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Alignment.Companion.TopCenter
import androidx.compose.ui.Alignment.Companion.TopStart
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.text.AnnotatedString
+import androidx.compose.ui.text.ParagraphIntrinsics
import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.font.createFontFamilyResolver
import androidx.compose.ui.text.input.KeyboardType
+import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.text.style.TextDirection
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
@@ -25,20 +32,21 @@ import java.text.DecimalFormat
/**
* Simple text field for amount input.
- * Validates and trims input text using [DecimalFormat]. Formats visual output using [AmountVisualTransformation].
- * Can display aligned placeholder and currency symbol [symbol].
+ * Validates and trims input text using [DecimalFormat]. Formats visual output using [visualTransformation].
+ * Can display aligned placeholder.
*
* @param value initial text
* @param decimals number of decimal places
* @param onValueChange callback
* @param textStyle text and placeholder styles
* @param modifier modifier
- * @param symbol currency symbol
* @param color text color
- * @param placeholderAlignment alignment of placeholder
- * @param showPlaceholder show placeholder
+ * @param visualTransformation text visual transformation
* @param keyboardOptions keyboard options
- *
+ * @param keyboardActions keyboard actions
+ * @param isEnabled is field editing enabled
+ * @param isAutoResize is text font auto resize
+ * @param reduceFactor font resize factor
* @see [SimpleTextField] for standard text field
*/
@Composable
@@ -48,59 +56,60 @@ fun AmountTextField(
onValueChange: (String) -> Unit,
textStyle: TextStyle,
modifier: Modifier = Modifier,
- symbol: String? = null,
color: Color = TangemTheme.colors.text.primary1,
- placeholderAlignment: Alignment = TopStart,
- showPlaceholder: Boolean = true,
+ visualTransformation: VisualTransformation = AmountVisualTransformation(decimals),
keyboardOptions: KeyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Number,
),
keyboardActions: KeyboardActions = KeyboardActions.Default,
+ isEnabled: Boolean = true,
+ isAutoResize: Boolean = false,
+ @FloatRange(from = 0.0, to = 1.0, fromInclusive = false, toInclusive = false)
+ reduceFactor: Double = 0.9,
) {
val decimalFormat = rememberDecimalFormat()
-
- val placeholderTextAlign = if (placeholderAlignment == TopCenter) {
- TextAlign.Center
- } else {
- TextAlign.Start
- }
- SimpleTextField(
- value = value,
- onValueChange = { newText ->
- if (decimalFormat.isValidSymbols(newText)) {
- val trimmed = decimalFormat.getValidatedNumberWithFixedDecimals(newText, decimals)
- onValueChange(trimmed)
+ BoxWithConstraints(modifier = modifier) {
+ var fontSize = textStyle.fontSize
+ if (isAutoResize) {
+ val calculateIntrinsics = @Composable {
+ val transformedText = visualTransformation.filter(AnnotatedString(value)).text.text
+ ParagraphIntrinsics(
+ text = transformedText,
+ style = textStyle.copy(fontSize = fontSize),
+ density = LocalDensity.current,
+ fontFamilyResolver = createFontFamilyResolver(LocalContext.current),
+ )
}
- },
- modifier = modifier
- .background(TangemTheme.colors.background.action),
- textStyle = textStyle,
- color = color,
- keyboardOptions = keyboardOptions,
- keyboardActions = keyboardActions,
- singleLine = true,
- visualTransformation = AmountVisualTransformation(decimals, symbol, decimalFormat),
- decorationBox = { innerTextField ->
- Box {
- if (value.isBlank() && showPlaceholder) {
- val placeholder = if (symbol != null) {
- decimalFormat.defaultFormat().plus(" $symbol")
- } else {
- decimalFormat.defaultFormat()
- }
- Text(
- text = placeholder,
- style = textStyle,
- color = TangemTheme.colors.text.disabled,
- textAlign = placeholderTextAlign,
- modifier = Modifier
- .align(placeholderAlignment),
- )
+ var intrinsics = calculateIntrinsics()
+ with(LocalDensity.current) {
+ while (intrinsics.maxIntrinsicWidth > maxWidth.toPx()) {
+ fontSize *= reduceFactor
+ intrinsics = calculateIntrinsics()
}
- innerTextField()
}
- },
- )
+ }
+ val textColor = if (value.isBlank()) TangemTheme.colors.text.disabled else color
+ SimpleTextField(
+ value = value,
+ onValueChange = { newText ->
+ if (decimalFormat.isValidSymbols(newText)) {
+ val trimmed = decimalFormat.getValidatedNumberWithFixedDecimals(newText, decimals)
+ onValueChange(trimmed)
+ }
+ },
+ textStyle = textStyle.copy(
+ fontSize = fontSize,
+ textDirection = TextDirection.ContentOrLtr,
+ ),
+ color = textColor,
+ keyboardOptions = keyboardOptions,
+ keyboardActions = keyboardActions,
+ singleLine = true,
+ readOnly = !isEnabled,
+ visualTransformation = visualTransformation,
+ modifier = Modifier.background(TangemTheme.colors.background.action),
+ )
+ }
}
private fun DecimalFormat.isValidSymbols(text: String): Boolean {
@@ -119,9 +128,6 @@ private fun AmountTextFieldPreview(
AmountTextField(
value = text,
decimals = amount.decimals,
- symbol = amount.symbol,
- placeholderAlignment = amount.placeholderAlignment,
- showPlaceholder = amount.showPlaceholder,
onValueChange = { text = it },
textStyle = TangemTheme.typography.h2.copy(
color = TangemTheme.colors.text.primary1,
@@ -135,28 +141,24 @@ private fun AmountTextFieldPreview(
private class AmountTextFieldPreviewProvider : PreviewParameterProvider {
override val values = sequenceOf(
AmountTextFieldPreviewData(
- symbol = "USD",
value = "1000000,123123",
decimals = 3,
placeholderAlignment = TopStart,
showPlaceholder = true,
),
AmountTextFieldPreviewData(
- symbol = null,
value = "1000000.123123",
decimals = 6,
placeholderAlignment = TopStart,
showPlaceholder = false,
),
AmountTextFieldPreviewData(
- symbol = "$",
value = null,
decimals = 2,
showPlaceholder = true,
placeholderAlignment = TopCenter,
),
AmountTextFieldPreviewData(
- symbol = null,
value = null,
decimals = 2,
showPlaceholder = true,
@@ -166,7 +168,6 @@ private class AmountTextFieldPreviewProvider : PreviewParameterProvider 0) {
+ transformedOffset.coerceIn(0, minOf(symbolOffset, formattedText.length))
+ } else {
+ transformedOffset.coerceIn(0, formattedText.length)
+ }
+ return transformedOffset
}
override fun transformedToOriginal(offset: Int): Int {
diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowEnterAmount.kt b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowEnterAmount.kt
index dd7c375e77..28c9082f90 100644
--- a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowEnterAmount.kt
+++ b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowEnterAmount.kt
@@ -17,10 +17,12 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
import com.tangem.core.ui.components.fields.AmountTextField
+import com.tangem.core.ui.components.fields.visualtransformations.AmountVisualTransformation
import com.tangem.core.ui.components.inputrow.inner.DividerContainer
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.TangemTheme
+import com.tangem.core.ui.utils.rememberDecimalFormat
/**
* Input row for entering amount. Manages correct amount format and validation
@@ -76,7 +78,11 @@ fun InputRowEnterAmount(
AmountTextField(
value = text,
decimals = decimals,
- symbol = symbol,
+ visualTransformation = AmountVisualTransformation(
+ decimals = decimals,
+ symbol = symbol,
+ decimalFormat = rememberDecimalFormat(),
+ ),
onValueChange = onValueChange,
color = textColor,
textStyle = TangemTheme.typography.body2,
diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowEnterInfoAmount.kt b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowEnterInfoAmount.kt
index 40276abe7a..f3d8a0c1cc 100644
--- a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowEnterInfoAmount.kt
+++ b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowEnterInfoAmount.kt
@@ -12,23 +12,28 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import com.tangem.core.ui.components.fields.AmountTextField
+import com.tangem.core.ui.components.fields.visualtransformations.AmountVisualTransformation
import com.tangem.core.ui.components.inputrow.inner.DividerContainer
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.TangemTheme
+import com.tangem.core.ui.utils.rememberDecimalFormat
/**
* `Input Row Enter Info` for entering amount. Manages correct amount format and validation
* @param title title reference
* @param text primary text reference
+ * @param decimals amount text decimal count
* @param onValueChange text change callback
* @param modifier modifier
+ * @param symbol amount symbol
+ * @param info info text
* @param titleColor title color
* @param textColor text color
- * @param isSingleLine text
- * @param visualTransformation applied transformation to text
+ * @param infoColor info text color
* @param keyboardOptions keyboard options for field
+ * @param keyboardActions keyboard actions for field
* @param showDivider show divider
*
* @see [InputRowEnterInfo]
@@ -52,6 +57,7 @@ fun InputRowEnterInfoAmount(
keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
keyboardActions: KeyboardActions = KeyboardActions.Default,
showDivider: Boolean = false,
+ isReadOnly: Boolean = false,
) {
DividerContainer(
modifier = modifier,
@@ -71,9 +77,14 @@ fun InputRowEnterInfoAmount(
AmountTextField(
value = text,
decimals = decimals,
- symbol = symbol,
+ visualTransformation = AmountVisualTransformation(
+ decimals = decimals,
+ symbol = symbol,
+ decimalFormat = rememberDecimalFormat(),
+ ),
onValueChange = onValueChange,
color = textColor,
+ isEnabled = !isReadOnly,
textStyle = TangemTheme.typography.body2,
keyboardOptions = keyboardOptions,
keyboardActions = keyboardActions,
diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/inner/PasteButton.kt b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/inner/PasteButton.kt
index 1d7d0fab95..88ed54192f 100644
--- a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/inner/PasteButton.kt
+++ b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/inner/PasteButton.kt
@@ -36,7 +36,12 @@ import com.tangem.core.ui.utils.DEFAULT_ANIMATION_DURATION
fun PasteButton(isPasteButtonVisible: Boolean, onClick: (String) -> Unit, modifier: Modifier = Modifier) {
val clipboardManager = LocalClipboardManager.current
val hapticFeedback = LocalHapticFeedback.current
-
+ val isPasteEnabled = !clipboardManager.getText()?.text.isNullOrEmpty()
+ val color = if (isPasteEnabled) {
+ TangemTheme.colors.button.primary
+ } else {
+ TangemTheme.colors.button.secondary
+ }
AnimatedVisibility(
visible = isPasteButtonVisible,
label = "Paste Button Visibility Animation",
@@ -50,7 +55,7 @@ fun PasteButton(isPasteButtonVisible: Boolean, onClick: (String) -> Unit, modifi
color = TangemTheme.colors.text.primary2,
modifier = Modifier
.background(
- color = TangemTheme.colors.button.primary,
+ color = color,
shape = TangemTheme.shapes.roundedCornersXMedium,
)
.padding(
@@ -60,6 +65,7 @@ fun PasteButton(isPasteButtonVisible: Boolean, onClick: (String) -> Unit, modifi
.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = rememberRipple(radius = TangemTheme.dimens.radius8),
+ enabled = isPasteEnabled,
onClick = {
hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
onClick(
diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/rows/SelectorRowItem.kt b/core/ui/src/main/java/com/tangem/core/ui/components/rows/SelectorRowItem.kt
index 667ec44c08..bedec66060 100644
--- a/core/ui/src/main/java/com/tangem/core/ui/components/rows/SelectorRowItem.kt
+++ b/core/ui/src/main/java/com/tangem/core/ui/components/rows/SelectorRowItem.kt
@@ -14,11 +14,11 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
-import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.style.TextAlign
-import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import com.tangem.core.ui.R
+import com.tangem.core.ui.components.atoms.text.EllipsisText
+import com.tangem.core.ui.components.atoms.text.TextEllipsis
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.TangemTheme
@@ -27,12 +27,14 @@ import com.tangem.core.ui.res.TangemTheme
fun SelectorRowItem(
@StringRes titleRes: Int,
@DrawableRes iconRes: Int,
- onSelect: () -> Unit,
modifier: Modifier = Modifier,
- preEllipsize: TextReference? = null,
- postEllipsize: TextReference? = null,
+ paddingValues: PaddingValues = PaddingValues(TangemTheme.dimens.spacing12),
+ preDot: TextReference? = null,
+ postDot: TextReference? = null,
+ ellipsizeOffset: Int? = null,
isSelected: Boolean = false,
showDivider: Boolean = true,
+ onSelect: (() -> Unit)? = null,
) {
val iconTint by animateColorAsState(
targetValue = if (isSelected) {
@@ -43,45 +45,39 @@ fun SelectorRowItem(
label = "Selector icon tint change",
)
- val textStyle = if (isSelected) {
- TangemTheme.typography.subtitle2
- } else {
- TangemTheme.typography.body2
- }
-
Box(
modifier = modifier
.fillMaxWidth()
- .clickable { onSelect() },
+ .then(
+ if (onSelect != null) {
+ Modifier.clickable { onSelect() }
+ } else {
+ Modifier
+ },
+ ),
) {
- Row(modifier = Modifier.fillMaxWidth()) {
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(paddingValues),
+ verticalAlignment = Alignment.CenterVertically,
+ ) {
Icon(
painter = painterResource(iconRes),
tint = iconTint,
contentDescription = null,
- modifier = Modifier
- .padding(
- start = TangemTheme.dimens.spacing12,
- top = TangemTheme.dimens.spacing12,
- bottom = TangemTheme.dimens.spacing12,
- ),
)
Text(
text = stringResource(titleRes),
- style = textStyle,
+ style = TangemTheme.typography.body2,
color = TangemTheme.colors.text.primary1,
- modifier = Modifier
- .padding(
- start = TangemTheme.dimens.spacing8,
- top = TangemTheme.dimens.spacing14,
- bottom = TangemTheme.dimens.spacing14,
- ),
+ modifier = Modifier.padding(start = TangemTheme.dimens.spacing8),
)
- if (preEllipsize != null && postEllipsize != null) {
+ if (preDot != null && postDot != null) {
SelectorValueContent(
- amount = preEllipsize,
- symbol = postEllipsize,
- textStyle = textStyle,
+ preDot = preDot,
+ postDot = postDot,
+ ellipsizeOffset = ellipsizeOffset,
)
}
}
@@ -99,33 +95,37 @@ fun SelectorRowItem(
}
@Composable
-private fun RowScope.SelectorValueContent(amount: TextReference, symbol: TextReference, textStyle: TextStyle) {
- Text(
- text = amount.resolveReference(),
- style = textStyle,
+private fun RowScope.SelectorValueContent(
+ preDot: TextReference,
+ postDot: TextReference,
+ ellipsizeOffset: Int? = null,
+) {
+ val ellipsis = if (ellipsizeOffset == null) {
+ TextEllipsis.End
+ } else {
+ TextEllipsis.OffsetEnd(ellipsizeOffset)
+ }
+ EllipsisText(
+ text = preDot.resolveReference(),
+ style = TangemTheme.typography.body2,
color = TangemTheme.colors.text.primary1,
textAlign = TextAlign.End,
- overflow = TextOverflow.Ellipsis,
- maxLines = 1,
+ ellipsis = ellipsis,
modifier = Modifier
.weight(1f)
- .padding(
- start = TangemTheme.dimens.spacing4,
- top = TangemTheme.dimens.spacing14,
- bottom = TangemTheme.dimens.spacing14,
- ),
+ .padding(start = TangemTheme.dimens.spacing4),
)
Text(
- text = symbol.resolveReference(),
- style = textStyle,
+ text = "•",
+ style = TangemTheme.typography.caption2,
color = TangemTheme.colors.text.primary1,
- modifier = Modifier
- .padding(
- start = TangemTheme.dimens.spacing1,
- end = TangemTheme.dimens.spacing12,
- top = TangemTheme.dimens.spacing14,
- bottom = TangemTheme.dimens.spacing14,
- ),
+ textAlign = TextAlign.Center,
+ modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing4),
+ )
+ Text(
+ text = postDot.resolveReference(),
+ style = TangemTheme.typography.body2,
+ color = TangemTheme.colors.text.tertiary,
)
}
@@ -136,8 +136,9 @@ private fun SelectorRowItemPreview_Light() {
SelectorRowItem(
titleRes = R.string.common_fee_selector_option_slow,
iconRes = R.drawable.ic_tortoise_24,
- preEllipsize = TextReference.Str("1000"),
- postEllipsize = TextReference.Str("$"),
+ preDot = TextReference.Str("1000 ETH"),
+ postDot = TextReference.Str("1000 $"),
+ ellipsizeOffset = 4,
isSelected = true,
onSelect = { },
)
@@ -151,8 +152,9 @@ private fun SelectorRowItemPreview_Dark() {
SelectorRowItem(
titleRes = R.string.common_fee_selector_option_slow,
iconRes = R.drawable.ic_tortoise_24,
- preEllipsize = TextReference.Str("1000"),
- postEllipsize = TextReference.Str("$"),
+ preDot = TextReference.Str("1000 ETH"),
+ postDot = TextReference.Str("1000 $"),
+ ellipsizeOffset = 4,
isSelected = true,
onSelect = { },
)
diff --git a/core/ui/src/main/java/com/tangem/core/ui/extensions/BlockchainIcons.kt b/core/ui/src/main/java/com/tangem/core/ui/extensions/BlockchainIcons.kt
index f49147879e..1a95df0142 100644
--- a/core/ui/src/main/java/com/tangem/core/ui/extensions/BlockchainIcons.kt
+++ b/core/ui/src/main/java/com/tangem/core/ui/extensions/BlockchainIcons.kt
@@ -57,6 +57,16 @@ fun getActiveIconRes(blockchainId: String): Int {
"aurora", "aurora/test" -> R.drawable.img_aurora_22
"areon", "areon/test" -> R.drawable.img_areon_22
"pls", "pls/test" -> R.drawable.img_pls_22
+ "zkSyncEra", "zkSyncEra/test" -> R.drawable.img_zksync_22
+ "moonbeam", "moonbeam/test" -> R.drawable.img_moonbeam_22
+ "manta", "manta/test" -> R.drawable.img_manta_22
+ "polygonZkEVM", "polygonZkEVM/test" -> R.drawable.img_polygon_22
+ "moonriver", "moonriver/test" -> R.drawable.img_moonriver_22
+ "mantle", "mantle/test" -> R.drawable.img_mantle_22
+ "flare", "flare/test" -> R.drawable.img_flare_22
+ "taraxa", "taraxa/test" -> R.drawable.img_taraxa_22
+ "radiant" -> R.drawable.img_radiant_22
+ "base" -> R.drawable.img_base_22
else -> R.drawable.ic_alert_24
}
}
@@ -115,6 +125,16 @@ fun getActiveIconResByNetworkId(networkId: String): Int {
"aurora", "aurora/test" -> R.drawable.img_aurora_22
"areon", "areon/test" -> R.drawable.img_areon_22
"pls", "pls/test" -> R.drawable.img_pls_22
+ "zksync", "zksync/test" -> R.drawable.img_zksync_22
+ "moonbeam", "moonbeam/test" -> R.drawable.img_moonbeam_22
+ "manta-network", "manta-network/test" -> R.drawable.img_manta_22
+ "polygon-zkevm", "polygon-zkevm/test" -> R.drawable.img_polygon_22
+ "moonriver", "moonriver/test" -> R.drawable.img_moonriver_22
+ "mantle", "mantle/test" -> R.drawable.img_mantle_22
+ "flare-network", "flare-network/test" -> R.drawable.img_flare_22
+ "taraxa", "taraxa/test" -> R.drawable.img_taraxa_22
+ "radiant" -> R.drawable.img_radiant_22
+ "base" -> R.drawable.img_base_22
else -> R.drawable.ic_alert_24
}
}
@@ -170,6 +190,16 @@ fun getActiveIconResByCoinId(coinId: String): Int {
"aurora-near" -> R.drawable.img_aurora_22
"areon" -> R.drawable.img_areon_22
"pls" -> R.drawable.img_pls_22
+ "zksync-ethereum" -> R.drawable.img_zksync_22
+ "moonbeam" -> R.drawable.img_moonbeam_22
+ "manta-network-ethereum" -> R.drawable.img_manta_22
+ "polygon-zkevm-ethereum" -> R.drawable.img_polygon_22
+ "moonriver" -> R.drawable.img_moonriver_22
+ "mantle" -> R.drawable.img_mantle_22
+ "flare-networks" -> R.drawable.img_flare_22
+ "taraxa" -> R.drawable.img_taraxa_22
+ "radiant" -> R.drawable.img_radiant_22
+ "base" -> R.drawable.img_base_22
else -> R.drawable.ic_alert_24
}
}
@@ -190,7 +220,7 @@ fun getGreyedOutIconRes(blockchainId: String): Int {
"XRP" -> R.drawable.ic_xrp_22
"XLM", "XLM/test" -> R.drawable.ic_stellar_16
"AVALANCHE", "AVALANCHE/test" -> R.drawable.ic_avalanche_22
- "POLYGON", "POLYGON/test" -> R.drawable.ic_polygon_16
+ "POLYGON", "POLYGON/test" -> R.drawable.ic_polygon_22
"SOLANA", "SOLANA/test" -> R.drawable.ic_solana_16
"FTM", "FTM/test" -> R.drawable.ic_fantom_22
"BSC", "BSC/test", "BINANCE", "BINANCE/test" -> R.drawable.ic_bsc_16
@@ -228,6 +258,16 @@ fun getGreyedOutIconRes(blockchainId: String): Int {
"aurora", "aurora/test" -> R.drawable.ic_aurora_22
"areon", "areon/test" -> R.drawable.ic_areon_22
"pls", "pls/test" -> R.drawable.ic_pls_22
+ "zkSyncEra", "zkSyncEra/test" -> R.drawable.ic_zksync_22
+ "moonbeam", "moonbeam/test" -> R.drawable.ic_moonbeam_22
+ "manta", "manta/test" -> R.drawable.ic_manta_22
+ "polygonZkEVM", "polygonZkEVM/test" -> R.drawable.ic_polygon_22
+ "moonriver", "moonriver/test" -> R.drawable.ic_moonriver_22
+ "mantle", "mantle/test" -> R.drawable.ic_mantle_22
+ "flare", "flare/test" -> R.drawable.ic_flare_22
+ "taraxa", "taraxa/test" -> R.drawable.ic_taraxa_22
+ "radiant" -> R.drawable.ic_radiant_22
+ "base", "base/test" -> R.drawable.ic_base_22
else -> R.drawable.ic_alert_24
}
}
@@ -249,7 +289,7 @@ fun getGreyedOutIconResByNetworkId(networkId: String): Int {
"tezos" -> R.drawable.ic_tezos_16
"xrp", "ripple" -> R.drawable.ic_xrp_22
"stellar", "stellar/test" -> R.drawable.ic_stellar_16
- "polygon-pos", "polygon-pos/test" -> R.drawable.ic_polygon_16
+ "polygon-pos", "polygon-pos/test" -> R.drawable.ic_polygon_22
"solana", "solana/test" -> R.drawable.ic_solana_16
"fantom", "fantom/test" -> R.drawable.ic_fantom_22
"dogecoin" -> R.drawable.ic_dogecoin_16
@@ -286,6 +326,16 @@ fun getGreyedOutIconResByNetworkId(networkId: String): Int {
"aurora", "aurora/test" -> R.drawable.ic_aurora_22
"areon", "areon/test" -> R.drawable.ic_areon_22
"pls", "pls/test" -> R.drawable.ic_pls_22
+ "zksync", "zksync/test" -> R.drawable.ic_zksync_22
+ "moonbeam", "moonbeam/test" -> R.drawable.ic_moonbeam_22
+ "manta-network", "manta-network/test" -> R.drawable.ic_manta_22
+ "polygon-zkevm", "polygon-zkevm/test" -> R.drawable.ic_polygon_22
+ "moonriver", "moonriver/test" -> R.drawable.ic_moonriver_22
+ "mantle", "mantle/test" -> R.drawable.ic_mantle_22
+ "flare-network", "flare-network/test" -> R.drawable.ic_flare_22
+ "taraxa", "taraxa/test" -> R.drawable.ic_taraxa_22
+ "radiant" -> R.drawable.ic_radiant_22
+ "base", "base/test" -> R.drawable.ic_base_22
else -> R.drawable.ic_alert_24
}
}
\ No newline at end of file
diff --git a/core/ui/src/main/java/com/tangem/core/ui/extensions/Fragment.kt b/core/ui/src/main/java/com/tangem/core/ui/extensions/Fragment.kt
index 3a0c083a41..e58126708e 100644
--- a/core/ui/src/main/java/com/tangem/core/ui/extensions/Fragment.kt
+++ b/core/ui/src/main/java/com/tangem/core/ui/extensions/Fragment.kt
@@ -1,14 +1,43 @@
package com.tangem.core.ui.extensions
+import android.R
+import android.content.Context
+import android.graphics.Color.*
import android.view.WindowManager
import androidx.annotation.ColorRes
import androidx.core.content.ContextCompat
+import androidx.core.view.WindowCompat
import androidx.fragment.app.Fragment
+import kotlin.math.sqrt
+@Deprecated("Use only in legacy fragments")
fun Fragment.setStatusBarColor(@ColorRes colorResId: Int) {
with(requireActivity().window) {
clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS)
addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS)
statusBarColor = ContextCompat.getColor(requireContext(), colorResId)
+ val view = view ?: return
+ val windowInsetsController = WindowCompat.getInsetsController(this, view)
+ windowInsetsController.isAppearanceLightStatusBars = luminance(requireContext(), colorResId)
}
+}
+
+// TODO replace by android.graphics.luminance() after bump min API to 24
+@Suppress("MagicNumber")
+fun luminance(context: Context, @ColorRes colorRes: Int): Boolean {
+ val color = context.resources.getColor(colorRes, null)
+ if (R.color.transparent == color) return true
+ var rtnValue = false
+ val rgb = intArrayOf(red(color), green(color), blue(color))
+ val brightness = sqrt(
+ rgb[0] * rgb[0] * .241 +
+ rgb[1] * rgb[1] * .691 +
+ rgb[2] * rgb[2] * .068,
+ ).toInt()
+
+ // color is light
+ if (brightness >= 200) {
+ rtnValue = true
+ }
+ return rtnValue
}
\ No newline at end of file
diff --git a/core/ui/src/main/java/com/tangem/core/ui/extensions/String.kt b/core/ui/src/main/java/com/tangem/core/ui/extensions/String.kt
index f5cc3cb971..be264409bc 100644
--- a/core/ui/src/main/java/com/tangem/core/ui/extensions/String.kt
+++ b/core/ui/src/main/java/com/tangem/core/ui/extensions/String.kt
@@ -29,4 +29,6 @@ fun String.toQrCode(sizePx: Int = 256, paddingPx: Int = 0): Bitmap {
}
}
return bmp
-}
\ No newline at end of file
+}
+
+fun String.capitalize(): String = replaceFirstChar { if (it.isLowerCase()) it.titlecase() else it.toString() }
\ No newline at end of file
diff --git a/core/ui/src/main/java/com/tangem/core/ui/res/TangemDimens.kt b/core/ui/src/main/java/com/tangem/core/ui/res/TangemDimens.kt
index 13ec4eb908..79a2e16273 100644
--- a/core/ui/src/main/java/com/tangem/core/ui/res/TangemDimens.kt
+++ b/core/ui/src/main/java/com/tangem/core/ui/res/TangemDimens.kt
@@ -125,6 +125,8 @@ data class TangemDimens internal constructor(
val spacing56: Dp = 56.dp,
val spacing60: Dp = 60.dp,
val spacing70: Dp = 70.dp,
+ val spacing72: Dp = 72.dp,
+ val spacing76: Dp = 76.dp,
val spacing80: Dp = 80.dp,
val spacing92: Dp = 92.dp,
val spacing94: Dp = 94.dp,
diff --git a/core/ui/src/main/java/com/tangem/core/ui/utils/BigDecimalFormatter.kt b/core/ui/src/main/java/com/tangem/core/ui/utils/BigDecimalFormatter.kt
index 020cc01c45..42309223b1 100644
--- a/core/ui/src/main/java/com/tangem/core/ui/utils/BigDecimalFormatter.kt
+++ b/core/ui/src/main/java/com/tangem/core/ui/utils/BigDecimalFormatter.kt
@@ -1,8 +1,10 @@
package com.tangem.core.ui.utils
import com.tangem.domain.tokens.model.CryptoCurrency
+import timber.log.Timber
import java.math.BigDecimal
import java.math.RoundingMode
+import java.text.DecimalFormat
import java.text.NumberFormat
import java.util.Currency
import java.util.Locale
@@ -66,6 +68,26 @@ object BigDecimalFormatter {
.replace(formatterCurrency.getSymbol(locale), fiatCurrencySymbol)
}
+ fun formatFiatEditableAmount(
+ fiatAmount: String?,
+ fiatCurrencyCode: String,
+ fiatCurrencySymbol: String,
+ locale: Locale = Locale.getDefault(),
+ ): String {
+ if (fiatAmount == null) return EMPTY_BALANCE_SIGN
+
+ val formatterCurrency = getCurrency(fiatCurrencyCode)
+ val numberFormatter = NumberFormat.getCurrencyInstance(locale).apply {
+ currency = formatterCurrency
+ }
+ val formatter = requireNotNull(numberFormatter as? DecimalFormat) {
+ Timber.e("NumberFormat is null")
+ return EMPTY_BALANCE_SIGN
+ }
+ return "${formatter.positivePrefix}$fiatAmount${formatter.positiveSuffix}"
+ .replace(formatterCurrency.getSymbol(locale), fiatCurrencySymbol)
+ }
+
fun formatPercent(
percent: BigDecimal,
useAbsoluteValue: Boolean,
@@ -83,6 +105,8 @@ object BigDecimalFormatter {
return formatter.format(value)
}
+ fun formatWithSymbol(amount: String, symbol: String) = "$amount\u2009$symbol"
+
private fun getCurrency(code: String): Currency {
return runCatching { Currency.getInstance(code) }
.getOrElse { e ->
diff --git a/core/ui/src/main/java/com/tangem/core/ui/utils/DateTimeFormatters.kt b/core/ui/src/main/java/com/tangem/core/ui/utils/DateTimeFormatters.kt
index ce8b600e55..0ea40469e2 100644
--- a/core/ui/src/main/java/com/tangem/core/ui/utils/DateTimeFormatters.kt
+++ b/core/ui/src/main/java/com/tangem/core/ui/utils/DateTimeFormatters.kt
@@ -59,11 +59,7 @@ object DateTimeFormatters {
DateTimeFormat.forPattern("dd.MM.yyyy HH:mm")
}
- fun formatTime(formatter: DateTimeFormatter = timeFormatter, time: DateTime): String {
- return formatter.print(time)
- }
-
- fun formatDate(formatter: DateTimeFormatter = dateFormatter, date: DateTime): String {
+ fun formatDate(date: DateTime, formatter: DateTimeFormatter = dateFormatter): String {
return formatter.print(date)
}
}
\ No newline at end of file
diff --git a/core/ui/src/main/java/com/tangem/core/ui/utils/DateUtils.kt b/core/ui/src/main/java/com/tangem/core/ui/utils/DateUtils.kt
index b0237e897a..8d596fc41f 100644
--- a/core/ui/src/main/java/com/tangem/core/ui/utils/DateUtils.kt
+++ b/core/ui/src/main/java/com/tangem/core/ui/utils/DateUtils.kt
@@ -29,5 +29,5 @@ fun Long.toDateFormat(formatter: DateTimeFormatter = DateTimeFormatters.dateForm
* Returns formatted time according to [formatter].
*/
fun Long.toTimeFormat(formatter: DateTimeFormatter = DateTimeFormatters.timeFormatter): String {
- return DateTimeFormatters.formatTime(formatter = formatter, time = DateTime(this, DateTimeZone.getDefault()))
+ return DateTimeFormatters.formatDate(date = DateTime(this, DateTimeZone.getDefault()), formatter = formatter)
}
\ No newline at end of file
diff --git a/core/ui/src/main/java/com/tangem/core/ui/utils/DecimalFormatterExt.kt b/core/ui/src/main/java/com/tangem/core/ui/utils/DecimalFormatterExt.kt
index 8cc2cf7bda..7db27cf7f4 100644
--- a/core/ui/src/main/java/com/tangem/core/ui/utils/DecimalFormatterExt.kt
+++ b/core/ui/src/main/java/com/tangem/core/ui/utils/DecimalFormatterExt.kt
@@ -59,7 +59,7 @@ fun DecimalFormat.getValidatedNumberWithFixedDecimals(text: String, decimals: In
return if (filteredChars.count { it == decimalSeparator } == 1) {
val beforeDecimal = filteredChars.substringBefore(decimalSeparator)
val afterDecimal = filteredChars.substringAfter(decimalSeparator)
- beforeDecimal + decimalSeparator + afterDecimal.take(decimals)
+ decimals.getWithIntegerDecimals(beforeDecimal, decimalSeparator, afterDecimal)
}
// If there is no dot, just take all digits
else {
@@ -82,7 +82,7 @@ fun DecimalFormat.formatWithThousands(text: String, decimals: Int): String {
.joinToString(thousandsSeparator.toString())
.reversed()
val afterDecimal = localizedText.substringAfter(decimalSeparator)
- beforeDecimal + decimalSeparator + afterDecimal.take(decimals)
+ decimals.getWithIntegerDecimals(beforeDecimal, decimalSeparator, afterDecimal)
}
// If there is no dot, just take all digits
else {
@@ -150,4 +150,10 @@ fun BigDecimal.parseBigDecimal(decimals: Int, roundingMode: RoundingMode = Round
} catch (e: Exception) {
""
}
+}
+
+private fun Int.getWithIntegerDecimals(before: String, separator: Char, after: String): String = if (this == 0) {
+ before
+} else {
+ before + separator + after.take(this)
}
\ No newline at end of file
diff --git a/core/ui/src/main/res/drawable/ic_base_22.xml b/core/ui/src/main/res/drawable/ic_base_22.xml
new file mode 100644
index 0000000000..7c135649e4
--- /dev/null
+++ b/core/ui/src/main/res/drawable/ic_base_22.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+
+
diff --git a/core/ui/src/main/res/drawable/ic_flare_22.xml b/core/ui/src/main/res/drawable/ic_flare_22.xml
new file mode 100644
index 0000000000..96c5c7eea9
--- /dev/null
+++ b/core/ui/src/main/res/drawable/ic_flare_22.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/core/ui/src/main/res/drawable/ic_manta_22.xml b/core/ui/src/main/res/drawable/ic_manta_22.xml
new file mode 100644
index 0000000000..0cdfc8df09
--- /dev/null
+++ b/core/ui/src/main/res/drawable/ic_manta_22.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/core/ui/src/main/res/drawable/ic_mantle_22.xml b/core/ui/src/main/res/drawable/ic_mantle_22.xml
new file mode 100644
index 0000000000..b53a906acb
--- /dev/null
+++ b/core/ui/src/main/res/drawable/ic_mantle_22.xml
@@ -0,0 +1,54 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/core/ui/src/main/res/drawable/ic_moonbeam_22.xml b/core/ui/src/main/res/drawable/ic_moonbeam_22.xml
new file mode 100644
index 0000000000..71e2200421
--- /dev/null
+++ b/core/ui/src/main/res/drawable/ic_moonbeam_22.xml
@@ -0,0 +1,48 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/core/ui/src/main/res/drawable/ic_moonriver_22.xml b/core/ui/src/main/res/drawable/ic_moonriver_22.xml
new file mode 100644
index 0000000000..0a44cf4e25
--- /dev/null
+++ b/core/ui/src/main/res/drawable/ic_moonriver_22.xml
@@ -0,0 +1,36 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/core/ui/src/main/res/drawable/ic_polygon_16.xml b/core/ui/src/main/res/drawable/ic_polygon_16.xml
deleted file mode 100644
index ddc6ff8adf..0000000000
--- a/core/ui/src/main/res/drawable/ic_polygon_16.xml
+++ /dev/null
@@ -1,10 +0,0 @@
-
-
-
diff --git a/core/ui/src/main/res/drawable/ic_polygon_22.xml b/core/ui/src/main/res/drawable/ic_polygon_22.xml
new file mode 100644
index 0000000000..73d1249583
--- /dev/null
+++ b/core/ui/src/main/res/drawable/ic_polygon_22.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/core/ui/src/main/res/drawable/ic_radiant_22.xml b/core/ui/src/main/res/drawable/ic_radiant_22.xml
new file mode 100644
index 0000000000..53d3f81f81
--- /dev/null
+++ b/core/ui/src/main/res/drawable/ic_radiant_22.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
diff --git a/core/ui/src/main/res/drawable/ic_taraxa_22.xml b/core/ui/src/main/res/drawable/ic_taraxa_22.xml
new file mode 100644
index 0000000000..7ee9b27337
--- /dev/null
+++ b/core/ui/src/main/res/drawable/ic_taraxa_22.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/core/ui/src/main/res/drawable/ic_zksync_22.xml b/core/ui/src/main/res/drawable/ic_zksync_22.xml
new file mode 100644
index 0000000000..16964a64d6
--- /dev/null
+++ b/core/ui/src/main/res/drawable/ic_zksync_22.xml
@@ -0,0 +1,14 @@
+
+
+
+
diff --git a/core/ui/src/main/res/drawable/img_base_22.xml b/core/ui/src/main/res/drawable/img_base_22.xml
new file mode 100644
index 0000000000..a426d0c8b2
--- /dev/null
+++ b/core/ui/src/main/res/drawable/img_base_22.xml
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
diff --git a/core/ui/src/main/res/drawable/img_flare_22.xml b/core/ui/src/main/res/drawable/img_flare_22.xml
new file mode 100644
index 0000000000..50e0da994b
--- /dev/null
+++ b/core/ui/src/main/res/drawable/img_flare_22.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
diff --git a/core/ui/src/main/res/drawable/img_manta_22.xml b/core/ui/src/main/res/drawable/img_manta_22.xml
new file mode 100644
index 0000000000..93200c11c3
--- /dev/null
+++ b/core/ui/src/main/res/drawable/img_manta_22.xml
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/core/ui/src/main/res/drawable/img_mantle_22.xml b/core/ui/src/main/res/drawable/img_mantle_22.xml
new file mode 100644
index 0000000000..93dbe6ccb3
--- /dev/null
+++ b/core/ui/src/main/res/drawable/img_mantle_22.xml
@@ -0,0 +1,61 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/core/ui/src/main/res/drawable/img_moonbeam_22.xml b/core/ui/src/main/res/drawable/img_moonbeam_22.xml
new file mode 100644
index 0000000000..7c010ab7e4
--- /dev/null
+++ b/core/ui/src/main/res/drawable/img_moonbeam_22.xml
@@ -0,0 +1,55 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/core/ui/src/main/res/drawable/img_moonriver_22.xml b/core/ui/src/main/res/drawable/img_moonriver_22.xml
new file mode 100644
index 0000000000..c2ee947935
--- /dev/null
+++ b/core/ui/src/main/res/drawable/img_moonriver_22.xml
@@ -0,0 +1,43 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/core/ui/src/main/res/drawable/img_polygon_22.xml b/core/ui/src/main/res/drawable/img_polygon_22.xml
index efb2fff8c1..455e3cbe53 100644
--- a/core/ui/src/main/res/drawable/img_polygon_22.xml
+++ b/core/ui/src/main/res/drawable/img_polygon_22.xml
@@ -1,13 +1,29 @@
+
+
+ android:pathData="M11,0L11,0A11,11 0,0 1,22 11L22,11A11,11 0,0 1,11 22L11,22A11,11 0,0 1,0 11L0,11A11,11 0,0 1,11 0z"
+ android:fillColor="#F5F0FD"/>
+ android:pathData="M7.435,8.666C7.694,8.514 8.03,8.514 8.314,8.666L10.328,9.833L11.697,10.594L13.712,11.761C13.97,11.913 14.306,11.913 14.59,11.761L16.192,10.848C16.45,10.696 16.631,10.417 16.631,10.112V8.311C16.631,8.006 16.476,7.727 16.192,7.575L14.616,6.687C14.358,6.535 14.022,6.535 13.738,6.687L12.162,7.575C11.904,7.727 11.723,8.006 11.723,8.311V9.478L10.354,8.691V7.524C10.354,7.22 10.509,6.941 10.793,6.789L13.712,5.114C13.97,4.962 14.306,4.962 14.59,5.114L17.561,6.789C17.845,6.941 18,7.22 18,7.524V10.899C18,11.203 17.845,11.482 17.561,11.634L14.59,13.309C14.332,13.461 13.996,13.461 13.712,13.309L11.697,12.167L10.328,11.38L8.314,10.239C8.055,10.087 7.72,10.087 7.435,10.239L5.86,11.127C5.601,11.279 5.421,11.558 5.421,11.863V13.664C5.421,13.968 5.576,14.247 5.86,14.4L7.435,15.313C7.694,15.465 8.03,15.465 8.314,15.313L9.889,14.425C10.148,14.273 10.328,13.994 10.328,13.689V12.522L11.697,13.309V14.476C11.697,14.78 11.542,15.059 11.258,15.211L8.288,16.886C8.03,17.038 7.694,17.038 7.41,16.886L4.439,15.211C4.181,15.059 4,14.78 4,14.476V11.101C4,10.797 4.155,10.518 4.439,10.366L7.435,8.666Z">
+
+
+
+
+
+
+
+
+
diff --git a/core/ui/src/main/res/drawable/img_radiant_22.xml b/core/ui/src/main/res/drawable/img_radiant_22.xml
new file mode 100644
index 0000000000..8b5ea3383f
--- /dev/null
+++ b/core/ui/src/main/res/drawable/img_radiant_22.xml
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/core/ui/src/main/res/drawable/img_taraxa_22.xml b/core/ui/src/main/res/drawable/img_taraxa_22.xml
new file mode 100644
index 0000000000..3faf5ca9f0
--- /dev/null
+++ b/core/ui/src/main/res/drawable/img_taraxa_22.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
diff --git a/core/ui/src/main/res/drawable/img_zksync_22.xml b/core/ui/src/main/res/drawable/img_zksync_22.xml
new file mode 100644
index 0000000000..ffb720dae9
--- /dev/null
+++ b/core/ui/src/main/res/drawable/img_zksync_22.xml
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
+
diff --git a/core/utils/src/main/AndroidManifest.xml b/core/utils/src/main/AndroidManifest.xml
deleted file mode 100644
index 02f6bf24ad..0000000000
--- a/core/utils/src/main/AndroidManifest.xml
+++ /dev/null
@@ -1,2 +0,0 @@
-
-
\ No newline at end of file
diff --git a/core/utils/src/main/java/com/tangem/utils/coroutines/JobHolder.kt b/core/utils/src/main/java/com/tangem/utils/coroutines/JobHolder.kt
index 42c4c7b296..14e64f1b62 100644
--- a/core/utils/src/main/java/com/tangem/utils/coroutines/JobHolder.kt
+++ b/core/utils/src/main/java/com/tangem/utils/coroutines/JobHolder.kt
@@ -11,10 +11,11 @@ class JobHolder {
private var job: Job? = null
- /** Update current [job] */
- fun update(job: Job?) {
+ /** Update current [JobHolder.job] and return new [job] */
+ fun update(job: Job): Job {
this.job?.cancel()
this.job = job
+ return job
}
/** Cancel current [job] */
@@ -23,4 +24,4 @@ class JobHolder {
}
}
-fun Job.saveIn(jobHolder: JobHolder) = jobHolder.update(job = this)
\ No newline at end of file
+fun Job.saveIn(jobHolder: JobHolder): Job = jobHolder.update(job = this)
\ No newline at end of file
diff --git a/data/card/src/main/java/com/tangem/data/card/sdk/DefaultCardSdkProvider.kt b/data/card/src/main/java/com/tangem/data/card/sdk/DefaultCardSdkProvider.kt
index d37487c40e..d8427a907f 100644
--- a/data/card/src/main/java/com/tangem/data/card/sdk/DefaultCardSdkProvider.kt
+++ b/data/card/src/main/java/com/tangem/data/card/sdk/DefaultCardSdkProvider.kt
@@ -43,7 +43,7 @@ internal class DefaultCardSdkProvider @Inject constructor() : CardSdkProvider, C
allowedCardTypes = FirmwareVersion.FirmwareType.values().toList(),
maxFirmwareVersion = FirmwareVersion(major = 6, minor = 33),
batchIdFilter = CardFilter.Companion.ItemFilter.Deny(
- items = setOf("0027", "0030", "0031", "0035", "DA88"),
+ items = setOf("0027", "0030", "0031", "0035"),
),
),
)
diff --git a/data/feedback/.gitignore b/data/feedback/.gitignore
new file mode 100644
index 0000000000..42afabfd2a
--- /dev/null
+++ b/data/feedback/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/data/feedback/build.gradle.kts b/data/feedback/build.gradle.kts
new file mode 100644
index 0000000000..306603b7c2
--- /dev/null
+++ b/data/feedback/build.gradle.kts
@@ -0,0 +1,42 @@
+plugins {
+ alias(deps.plugins.android.library)
+ alias(deps.plugins.kotlin.android)
+ alias(deps.plugins.kotlin.kapt)
+ id("configuration")
+}
+
+android {
+ namespace = "com.tangem.data.feedback"
+}
+
+dependencies {
+
+ // region AndroidX libraries
+ implementation(deps.androidx.datastore)
+ // endregion
+
+ // region DI
+ implementation(deps.hilt.android)
+ kapt(deps.hilt.kapt)
+ // endregion
+
+ // region Tangem libraries
+ implementation(deps.tangem.blockchain)
+ implementation(deps.tangem.card.core)
+ // endregion
+
+ // Other libraries
+ implementation(deps.timber)
+ // endregion
+
+ // region Core modules
+ implementation(projects.core.datasource)
+ // endregion
+
+ implementation(projects.core.utils)
+
+ implementation(projects.domain.feedback)
+ implementation(projects.domain.legacy)
+ implementation(projects.domain.models)
+ implementation(projects.domain.wallets.models)
+}
\ No newline at end of file
diff --git a/data/feedback/src/main/java/com/tangem/data/feedback/DefaultFeedbackRepository.kt b/data/feedback/src/main/java/com/tangem/data/feedback/DefaultFeedbackRepository.kt
new file mode 100644
index 0000000000..9f6c1aa883
--- /dev/null
+++ b/data/feedback/src/main/java/com/tangem/data/feedback/DefaultFeedbackRepository.kt
@@ -0,0 +1,109 @@
+package com.tangem.data.feedback
+
+import android.content.Context
+import android.content.pm.PackageInfo
+import android.os.Build
+import com.tangem.data.feedback.converters.BlockchainInfoConverter
+import com.tangem.data.feedback.converters.CardInfoConverter
+import com.tangem.datasource.local.preferences.AppPreferencesStore
+import com.tangem.datasource.local.preferences.PreferencesKeys
+import com.tangem.datasource.local.preferences.utils.getObjectMap
+import com.tangem.datasource.local.userwallet.UserWalletsStore
+import com.tangem.datasource.local.walletmanager.WalletManagersStore
+import com.tangem.domain.feedback.models.*
+import com.tangem.domain.feedback.repository.FeedbackRepository
+import com.tangem.domain.wallets.models.UserWallet
+import timber.log.Timber
+import java.io.File
+import java.io.FileWriter
+import java.io.StringWriter
+
+/**
+ * Implementation of [FeedbackRepository]
+ *
+ * @property appPreferencesStore application preferences store
+ * @property userWalletsStore user wallets store
+ * @property walletManagersStore wallet managers store
+ * @property context context for getting app version
+ *
+[REDACTED_AUTHOR]
+ */
+internal class DefaultFeedbackRepository(
+ private val appPreferencesStore: AppPreferencesStore,
+ private val userWalletsStore: UserWalletsStore,
+ private val walletManagersStore: WalletManagersStore,
+ private val context: Context,
+) : FeedbackRepository {
+
+ override suspend fun getUserWalletsInfo(): UserWalletsInfo {
+ return UserWalletsInfo(
+ selectedUserWalletId = getSelectedUserWallet().walletId.stringValue,
+ totalUserWallets = userWalletsStore.getAllSyncOrNull()?.size ?: error("No user wallets found"),
+ )
+ }
+
+ override suspend fun getCardInfo(): CardInfo {
+ return CardInfoConverter.convert(value = getSelectedUserWallet())
+ }
+
+ override suspend fun getBlockchainInfoList(): List {
+ return walletManagersStore
+ .getAllSync(userWalletId = getSelectedUserWallet().walletId)
+ .map(BlockchainInfoConverter::convert)
+ }
+
+ override fun getPhoneInfo(): PhoneInfo {
+ return PhoneInfo(
+ phoneModel = Build.MODEL,
+ osVersion = Build.VERSION.SDK_INT.toString(),
+ appVersion = getAppVersion(),
+ )
+ }
+
+ override suspend fun getAppLogs(): List {
+ return appPreferencesStore.getObjectMap(key = PreferencesKeys.APP_LOGS_KEY)
+ .map { AppLogModel(timestamp = it.key.toLong(), message = it.value) }
+ .sortedBy(AppLogModel::timestamp)
+ }
+
+ override suspend fun createLogFile(logs: String): File? {
+ return try {
+ val file = File(context.filesDir, LOGS_FILE)
+ file.delete()
+ file.createNewFile()
+
+ val stringWriter = StringWriter()
+
+ stringWriter.append(logs)
+
+ val fileWriter = FileWriter(file)
+ fileWriter.write(stringWriter.toString())
+ fileWriter.close()
+
+ file
+ } catch (ex: Exception) {
+ Timber.e(ex, "Logs file isn't created")
+ null
+ }
+ }
+
+ private fun getAppVersion(): String {
+ return runCatching { context.packageManager.getPackageInfo(context.packageName, 0) }
+ .fold(
+ onSuccess = PackageInfo::versionName,
+ onFailure = {
+ Timber.e(it)
+ "x.y.z"
+ },
+ )
+ }
+
+ private fun getSelectedUserWallet(): UserWallet {
+ return userWalletsStore.selectedUserWalletOrNull
+ ?: error("UserWallet is not selected")
+ }
+
+ private companion object {
+ const val LOGS_FILE = "logs.txt"
+ }
+}
\ No newline at end of file
diff --git a/data/feedback/src/main/java/com/tangem/data/feedback/converters/BlockchainInfoConverter.kt b/data/feedback/src/main/java/com/tangem/data/feedback/converters/BlockchainInfoConverter.kt
new file mode 100644
index 0000000000..de7f9b6d04
--- /dev/null
+++ b/data/feedback/src/main/java/com/tangem/data/feedback/converters/BlockchainInfoConverter.kt
@@ -0,0 +1,43 @@
+package com.tangem.data.feedback.converters
+
+import com.tangem.blockchain.common.Wallet
+import com.tangem.blockchain.common.WalletManager
+import com.tangem.blockchain.common.address.Address
+import com.tangem.domain.feedback.models.BlockchainInfo
+import com.tangem.domain.feedback.models.BlockchainInfo.Addresses.Multiple.AddressInfo
+import com.tangem.utils.converter.Converter
+import com.tangem.domain.feedback.models.BlockchainInfo.Addresses as BlockchainAddresses
+
+/**
+ * Converter from [WalletManager] to [BlockchainInfo]
+ *
+[REDACTED_AUTHOR]
+ */
+internal object BlockchainInfoConverter : Converter {
+
+ override fun convert(value: WalletManager): BlockchainInfo {
+ return BlockchainInfo(
+ blockchain = value.wallet.blockchain.fullName,
+ derivationPath = value.wallet.publicKey.derivationPath?.rawPath ?: "",
+ outputsCount = value.outputsCount?.toString(),
+ host = value.currentHost,
+ addresses = value.wallet.mapAddresses(Address::value),
+ explorerLinks = value.wallet.mapAddresses { value.wallet.getExploreUrl(it.value) },
+ tokens = value.cardTokens.map { token ->
+ BlockchainInfo.TokenInfo(id = token.id, name = token.name, contractAddress = token.contractAddress)
+ },
+ )
+ }
+
+ private inline fun Wallet.mapAddresses(map: (Address) -> String): BlockchainAddresses {
+ return if (addresses.size == 1) {
+ BlockchainAddresses.Single(value = map(addresses.first()))
+ } else {
+ BlockchainAddresses.Multiple(
+ addresses.map {
+ AddressInfo(type = it.type.name, value = map(it))
+ },
+ )
+ }
+ }
+}
\ No newline at end of file
diff --git a/data/feedback/src/main/java/com/tangem/data/feedback/converters/CardInfoConverter.kt b/data/feedback/src/main/java/com/tangem/data/feedback/converters/CardInfoConverter.kt
new file mode 100644
index 0000000000..7d16454467
--- /dev/null
+++ b/data/feedback/src/main/java/com/tangem/data/feedback/converters/CardInfoConverter.kt
@@ -0,0 +1,29 @@
+package com.tangem.data.feedback.converters
+
+import com.tangem.domain.common.TapWorkarounds.isStart2Coin
+import com.tangem.domain.feedback.models.CardInfo
+import com.tangem.domain.wallets.models.UserWallet
+import com.tangem.utils.converter.Converter
+
+/**
+ * Converter from [UserWallet] to [CardInfo]
+ *
+[REDACTED_AUTHOR]
+ */
+internal object CardInfoConverter : Converter {
+
+ override fun convert(value: UserWallet): CardInfo {
+ return with(value.scanResponse) {
+ CardInfo(
+ cardId = card.cardId,
+ firmwareVersion = card.firmwareVersion.stringValue,
+ cardBlockchain = walletData?.blockchain,
+ signedHashesList = card.wallets.map {
+ CardInfo.SignedHashes(curve = it.curve.curve, total = it.totalSignedHashes?.toString())
+ },
+ isImported = value.isImported,
+ isStart2Coin = value.scanResponse.card.isStart2Coin,
+ )
+ }
+ }
+}
\ No newline at end of file
diff --git a/data/feedback/src/main/java/com/tangem/data/feedback/di/FeedbackRepositoryModule.kt b/data/feedback/src/main/java/com/tangem/data/feedback/di/FeedbackRepositoryModule.kt
new file mode 100644
index 0000000000..5a00938959
--- /dev/null
+++ b/data/feedback/src/main/java/com/tangem/data/feedback/di/FeedbackRepositoryModule.kt
@@ -0,0 +1,30 @@
+package com.tangem.data.feedback.di
+
+import android.content.Context
+import com.tangem.data.feedback.DefaultFeedbackRepository
+import com.tangem.datasource.local.preferences.AppPreferencesStore
+import com.tangem.datasource.local.userwallet.UserWalletsStore
+import com.tangem.datasource.local.walletmanager.WalletManagersStore
+import com.tangem.domain.feedback.repository.FeedbackRepository
+import dagger.Module
+import dagger.Provides
+import dagger.hilt.InstallIn
+import dagger.hilt.android.qualifiers.ApplicationContext
+import dagger.hilt.components.SingletonComponent
+import javax.inject.Singleton
+
+@Module
+@InstallIn(SingletonComponent::class)
+internal object FeedbackRepositoryModule {
+
+ @Provides
+ @Singleton
+ fun provideFeedbackRepository(
+ appPreferencesStore: AppPreferencesStore,
+ userWalletsStore: UserWalletsStore,
+ walletManagersStore: WalletManagersStore,
+ @ApplicationContext context: Context,
+ ): FeedbackRepository {
+ return DefaultFeedbackRepository(appPreferencesStore, userWalletsStore, walletManagersStore, context)
+ }
+}
\ No newline at end of file
diff --git a/data/qr-scanning/.gitignore b/data/qr-scanning/.gitignore
new file mode 100644
index 0000000000..42afabfd2a
--- /dev/null
+++ b/data/qr-scanning/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/data/qr-scanning/build.gradle.kts b/data/qr-scanning/build.gradle.kts
new file mode 100644
index 0000000000..1c801589b7
--- /dev/null
+++ b/data/qr-scanning/build.gradle.kts
@@ -0,0 +1,34 @@
+plugins {
+ alias(deps.plugins.android.library)
+ alias(deps.plugins.kotlin.android)
+ alias(deps.plugins.kotlin.kapt)
+ alias(deps.plugins.hilt.android)
+ id("configuration")
+}
+
+android {
+ namespace = "com.tangem.data.qrscanning"
+}
+
+dependencies {
+
+ /** Domain */
+ implementation(projects.domain.qrScanning)
+ implementation(projects.domain.qrScanning.models)
+ implementation(projects.domain.tokens.models)
+
+ implementation(projects.core.ui)
+
+ /** SdK */
+ implementation(deps.tangem.blockchain)
+
+ /** DI */
+ implementation(deps.hilt.android)
+ kapt(deps.hilt.kapt)
+
+ /** Tests */
+ testImplementation(deps.test.junit)
+ testImplementation(deps.test.coroutine)
+ testImplementation(deps.test.mockk)
+ testImplementation(deps.test.truth)
+}
\ No newline at end of file
diff --git a/data/qr-scanning/src/main/java/com/tangem/data/qrscanning/di/QrScanningDataModule.kt b/data/qr-scanning/src/main/java/com/tangem/data/qrscanning/di/QrScanningDataModule.kt
new file mode 100644
index 0000000000..efe697f667
--- /dev/null
+++ b/data/qr-scanning/src/main/java/com/tangem/data/qrscanning/di/QrScanningDataModule.kt
@@ -0,0 +1,20 @@
+package com.tangem.data.qrscanning.di
+
+import com.tangem.data.qrscanning.repository.DefaultQrScanningEventsRepository
+import com.tangem.domain.qrscanning.repository.QrScanningEventsRepository
+import dagger.Module
+import dagger.Provides
+import dagger.hilt.InstallIn
+import dagger.hilt.components.SingletonComponent
+import javax.inject.Singleton
+
+@Module
+@InstallIn(SingletonComponent::class)
+internal object QrScanningDataModule {
+
+ @Provides
+ @Singleton
+ fun provideQrScanningEventsRepository(): QrScanningEventsRepository {
+ return DefaultQrScanningEventsRepository()
+ }
+}
\ No newline at end of file
diff --git a/data/qr-scanning/src/main/java/com/tangem/data/qrscanning/repository/DefaultQrScanningEventsRepository.kt b/data/qr-scanning/src/main/java/com/tangem/data/qrscanning/repository/DefaultQrScanningEventsRepository.kt
new file mode 100644
index 0000000000..87729e8bc4
--- /dev/null
+++ b/data/qr-scanning/src/main/java/com/tangem/data/qrscanning/repository/DefaultQrScanningEventsRepository.kt
@@ -0,0 +1,145 @@
+package com.tangem.data.qrscanning.repository
+
+import com.tangem.blockchain.common.Blockchain
+import com.tangem.domain.qrscanning.models.QrResult
+import com.tangem.domain.qrscanning.models.SourceType
+import com.tangem.domain.qrscanning.repository.QrScanningEventsRepository
+import com.tangem.domain.tokens.model.CryptoCurrency
+import kotlinx.coroutines.flow.MutableSharedFlow
+import kotlinx.coroutines.flow.filter
+import kotlinx.coroutines.flow.map
+import java.math.BigDecimal
+import java.net.URLDecoder
+
+internal class DefaultQrScanningEventsRepository : QrScanningEventsRepository {
+
+ private data class QrScanningEvent(val type: SourceType, val qrCode: String)
+
+ private val scannedEvents = MutableSharedFlow()
+
+ override suspend fun emitResult(type: SourceType, qrCode: String) {
+ scannedEvents.emit(QrScanningEvent(type, qrCode))
+ }
+
+ override fun subscribeToScanningResults(type: SourceType) = scannedEvents
+ .filter { it.type == type }
+ .map { it.qrCode }
+
+ override fun parseQrCode(qrCode: String, cryptoCurrency: CryptoCurrency): QrResult {
+ val withoutSchema = stripSchema(qrCode, cryptoCurrency)
+
+ // A poor man's ERC-681 parser: we want to extract only the destination address, and we don't care
+ // about other parts of the ERC-681 payload string like `chain_id` and/or `function_name`.
+ //
+ // We're extracting the destination address by parsing the given string until we meet
+ // any of the possible string delimiters (@ ? /).
+ val address = withoutSchema.takeWhile { char ->
+ char != CHAIN_DELIMITER && char != FUNCTION_DELIMITER && char != PARAM_DELIMITER
+ }
+
+ val result = QrResult(address = address)
+
+ extractParameters(withoutSchema)
+ .forEach {
+ when (it.key) {
+ Parameter.Amount -> {
+ // According to BIP-0021, the value is specified in decimals. No conversion needed
+ result.amount = it.value.toBigDecimalOrNull()
+ }
+ Parameter.Message,
+ Parameter.Memo,
+ -> {
+ result.memo = URLDecoder.decode(it.value, "UTF-8")
+ }
+ Parameter.Address -> {
+ // Overrides destination address for token transfers (ERC-681)
+ if (cryptoCurrency is CryptoCurrency.Token) {
+ // `address` parameter is used only if the contract address, encoded in the QR,
+ // matches the contract address of the token.
+ // Otherwise, the scanned string is likely malformed, and we stop the entire parsing routine
+ if (cryptoCurrency.contractAddress.equals(address, ignoreCase = true)) {
+ result.address = it.value
+ } else {
+ return QrResult()
+ }
+ }
+ }
+ Parameter.Value,
+ Parameter.Uint256,
+ -> {
+ // Extra convert parses scientific notation to decimal
+ // This is necessary to be able comparing BigDecimal values
+ result.amount = it.value.toBigDecimalOrNull()
+ ?.toPlainString()?.toBigDecimalOrNull()
+ ?.divide(BigDecimal.TEN.pow(cryptoCurrency.decimals))
+ }
+ }
+ }
+
+ return result
+ }
+
+ private fun stripSchema(raw: String, currency: CryptoCurrency): String {
+ val qrSchemas = Blockchain.fromId(currency.network.id.value).getShareScheme()
+
+ // The most specific (i.e. the most lengthy) prefixes always come first
+ qrSchemas
+ .sortedByDescending { it.length }
+ .forEach { schema ->
+ val stripped = raw.split(schema)
+
+ if (stripped.size > 1) return stripped.last()
+ }
+
+ return raw
+ }
+
+ private fun extractParameters(from: String): Map {
+ val parametersBlock = from.substringAfter(PARAM_DELIMITER)
+ if (parametersBlock.isBlank()) return emptyMap()
+
+ val paramList = parametersBlock.split(PARAMS_DELIMITER)
+ .mapNotNull { param ->
+ val parameterWithValue = param.split(PARAM_VALUE_DELIMITER)
+ if (parameterWithValue.size == 2) {
+ val name = Parameter.getParam(parameterWithValue.first())
+ val value = parameterWithValue.last()
+
+ if (name != null) {
+ name to value
+ } else {
+ null
+ }
+ } else {
+ null
+ }
+ }.associate { it }
+
+ return paramList
+ }
+
+ private enum class Parameter {
+ Amount,
+ Message,
+ Memo,
+ Address,
+ Value,
+ Uint256,
+ ;
+
+ companion object {
+ fun getParam(name: String): Parameter? {
+ return Parameter.entries.firstOrNull { it.name.equals(name, ignoreCase = true) }
+ }
+ }
+ }
+
+ private companion object {
+ // See https://eips.ethereum.org/EIPS/eip-681 for details.
+ const val CHAIN_DELIMITER = '@' // ERC-681 [ "@" chain_id ]
+ const val FUNCTION_DELIMITER = '/' // ERC-681 [ "/" function_name ]
+ const val PARAM_DELIMITER = '?' // BIP-021, ERC-681
+ const val PARAMS_DELIMITER = '&'
+ const val PARAM_VALUE_DELIMITER = '='
+ }
+}
\ No newline at end of file
diff --git a/data/qr-scanning/src/test/java/com/tangem/data/qrscanning/DefaultQrScanningEventsRepositoryTest.kt b/data/qr-scanning/src/test/java/com/tangem/data/qrscanning/DefaultQrScanningEventsRepositoryTest.kt
new file mode 100644
index 0000000000..ad1f6244a7
--- /dev/null
+++ b/data/qr-scanning/src/test/java/com/tangem/data/qrscanning/DefaultQrScanningEventsRepositoryTest.kt
@@ -0,0 +1,253 @@
+package com.tangem.data.qrscanning
+
+import com.google.common.truth.Truth
+import com.tangem.blockchain.common.Blockchain
+import com.tangem.data.qrscanning.repository.DefaultQrScanningEventsRepository
+import com.tangem.domain.qrscanning.models.QrResult
+import com.tangem.domain.tokens.model.CryptoCurrency
+import com.tangem.domain.tokens.model.Network
+import io.mockk.every
+import io.mockk.mockk
+import org.junit.Test
+import java.math.BigDecimal
+
+internal class DefaultQrScanningEventsRepositoryTest {
+
+ private val repository = DefaultQrScanningEventsRepository()
+
+ private val cryptoCurrencyId = mockk()
+ private val network = mockk()
+ private val cryptoCurrency = CryptoCurrency.Coin(
+ id = cryptoCurrencyId,
+ network = network,
+ name = "blockchain",
+ symbol = "symbol",
+ decimals = 18,
+ iconUrl = null,
+ isCustom = false,
+ )
+ private val tokenCryptoCurrency = CryptoCurrency.Token(
+ id = cryptoCurrencyId,
+ network = network,
+ name = "blockchain",
+ symbol = "symbol",
+ decimals = 7,
+ iconUrl = null,
+ isCustom = false,
+ contractAddress = "0x89205A3A3b2A69De6Dbf7f01ED13B2108B2c43e7",
+ )
+
+ private val garbage = "some_garbage="
+
+ private val schema1 = "bitcoin"
+ private val schema2 = "ethereum"
+
+ private val address1 = "bc1pw83rs5s75na2g7ec8yqgekr3ae209ye7ck2ftakjnh8tv3xzw8ls6wgt62"
+ private val address2 = "0xD1220A0cf47c7B9Be7A2E6BA89F429762e7b9aDb"
+ private val address3 = "pay-0xD1220A0cf47c7B9Be7A2E6BA89F429762e7b9aDb"
+ private val address4 = "0x89205A3A3b2A69De6Dbf7f01ED13B2108B2c43e7"
+
+ private val function = "/transfer"
+
+ private val someParam = "someParam"
+ private val someParamValue = "someParamValue"
+
+ private val addressParam = "address"
+ private val addressParamValue = "0xc00f86ab93cd0bd3a60213583d0fe35aaa1ace23"
+
+ private val amountParam = "amount"
+ private val someAmountParamValue = "amount"
+ private val amountParamValue = "123.123"
+
+ private val valueParam = "value"
+ private val someValueParamValue = "amount"
+ private val valueParamValue2 = "1.88e10"
+ private val valueParamValue3 = "1.68E11"
+ private val valueParamValue4 = "23000000000"
+
+ private val memoParam = "memo"
+ private val memoParamValue = "a%20random%20memo"
+ private val memoParamValueUtf8 = "a random memo"
+
+ private val messageParam = "message"
+ private val messageParamValue = "some%20message"
+ private val messageParamValueUft8 = "some message"
+ private val messageParamValue2 = "message"
+
+ @Test
+ fun testBip021() {
+ every { network.id } returns Network.ID(Blockchain.Bitcoin.id)
+ positiveCase(
+ "$schema1:$address1",
+ QrResult(address = address1),
+ cryptoCurrency,
+ )
+ positiveCase(
+ "$garbage$schema1:$address1",
+ QrResult(address = address1),
+ cryptoCurrency,
+ )
+ positiveCase(
+ "$address1?$someParam=$someParamValue",
+ QrResult(address = address1),
+ cryptoCurrency,
+ )
+ positiveCase(
+ "$address1?$someParam=$someParamValue&$amountParam=$someAmountParamValue",
+ QrResult(address = address1),
+ cryptoCurrency,
+ )
+ positiveCase(
+ "$address1?$someParam=$someParamValue&$amountParam=$amountParamValue",
+ QrResult(address = address1, amount = BigDecimal(amountParamValue)),
+ cryptoCurrency,
+ )
+ positiveCase(
+ "$address1?$someParam=$someParamValue&$amountParam=$amountParamValue&$memoParam=$memoParamValue",
+ QrResult(address = address1, amount = BigDecimal(amountParamValue), memo = memoParamValueUtf8),
+ cryptoCurrency,
+ )
+ positiveCase(
+ "$address1?$someParam=$someParamValue&$messageParam=$messageParamValue",
+ QrResult(address = address1, memo = messageParamValueUft8),
+ cryptoCurrency,
+ )
+ positiveCase(
+ "$address1?$someParam=$someParamValue&$messageParam=$messageParamValue2",
+ QrResult(address = address1, memo = messageParamValue2),
+ cryptoCurrency,
+ )
+ negativeCase(
+ "$address1?$someParam=$someParamValue&$amountParam=$amountParamValue",
+ QrResult(address = address1, memo = messageParamValue2),
+ cryptoCurrency,
+ )
+ }
+
+ @Test
+ fun testErc681Coin() {
+ every { network.id } returns Network.ID(Blockchain.Ethereum.id)
+ positiveCase(
+ address2,
+ QrResult(address = address2),
+ cryptoCurrency,
+ )
+ positiveCase(
+ "$address2?$someParam=$someParamValue",
+ QrResult(address = address2),
+ cryptoCurrency,
+ )
+ positiveCase(
+ "$schema2:$address2",
+ QrResult(address = address2),
+ cryptoCurrency,
+ )
+ positiveCase(
+ "$schema2:$address3",
+ QrResult(address = address2),
+ cryptoCurrency,
+ )
+ positiveCase(
+ "$garbage$schema2:$address2",
+ QrResult(address = address2),
+ cryptoCurrency,
+ )
+ positiveCase(
+ "$garbage$schema2:$address2$function?$addressParam=$addressParamValue",
+ QrResult(address = address2),
+ cryptoCurrency,
+ )
+ positiveCase(
+ "$garbage$schema2:$address2?$someParam=$someParamValue&$valueParam=$someAmountParamValue",
+ QrResult(address = address2),
+ cryptoCurrency,
+ )
+ positiveCase(
+ "$garbage$schema2:$address2?$someParam=$someParamValue&$valueParam=$valueParamValue2",
+ QrResult(address = address2, amount = BigDecimal("0.0000000188")),
+ cryptoCurrency,
+ )
+ positiveCase(
+ "$garbage$schema2:$address2?$someParam=$someParamValue&$valueParam=$valueParamValue3",
+ QrResult(address = address2, amount = BigDecimal("0.000000168")),
+ cryptoCurrency,
+ )
+ positiveCase(
+ "$garbage$schema2:$address2?$someParam=$someParamValue&$valueParam=$valueParamValue4",
+ QrResult(address = address2, amount = BigDecimal("0.000000023")),
+ cryptoCurrency,
+ )
+ }
+
+ @Test
+ fun testErc681Token() {
+ every { network.id } returns Network.ID(Blockchain.Ethereum.id)
+ positiveCase(
+ address2,
+ QrResult(address = address2),
+ tokenCryptoCurrency,
+ )
+ positiveCase(
+ "$address2?$someParam=$someParamValue",
+ QrResult(address = address2),
+ tokenCryptoCurrency,
+ )
+ positiveCase(
+ "$schema2:$address2",
+ QrResult(address = address2),
+ tokenCryptoCurrency,
+ )
+ positiveCase(
+ "$schema2:$address3",
+ QrResult(address = address2),
+ tokenCryptoCurrency,
+ )
+ positiveCase(
+ "$garbage$schema2:$address2",
+ QrResult(address = address2),
+ tokenCryptoCurrency,
+ )
+ positiveCase(
+ "$schema2:$address4$function?$addressParam=$addressParamValue",
+ QrResult(address = addressParamValue),
+ tokenCryptoCurrency,
+ )
+ positiveCase(
+ "$address2?$someParam=$someParamValue&$valueParam=$someValueParamValue",
+ QrResult(address = address2),
+ tokenCryptoCurrency,
+ )
+ positiveCase(
+ "$address2?$someParam=$someParamValue&$valueParam=$valueParamValue2",
+ QrResult(address = address2, amount = BigDecimal("1880")),
+ tokenCryptoCurrency,
+ )
+ positiveCase(
+ "$address2?$someParam=$someParamValue&$valueParam=$valueParamValue3",
+ QrResult(address = address2, amount = BigDecimal("16800")),
+ tokenCryptoCurrency,
+ )
+ positiveCase(
+ "$address2?$someParam=$someParamValue&$valueParam=$valueParamValue4",
+ QrResult(address = address2, amount = BigDecimal("2300")),
+ tokenCryptoCurrency,
+ )
+ negativeCase(
+ "$address2?$someParam=$someParamValue&$amountParam=$amountParamValue",
+ QrResult(address = address2, amount = BigDecimal("123.123"), memo = memoParamValueUtf8),
+ tokenCryptoCurrency,
+ )
+ }
+
+ private fun positiveCase(input: String, expected: QrResult, cryptoCurrency: CryptoCurrency) {
+ val actual = repository.parseQrCode(input, cryptoCurrency)
+ Truth.assertThat(actual.address).isEqualTo(expected.address)
+ Truth.assertThat(actual.amount).isEqualTo(expected.amount)
+ Truth.assertThat(actual.memo).isEqualTo(expected.memo)
+ }
+
+ private fun negativeCase(input: String, expected: QrResult, cryptoCurrency: CryptoCurrency) {
+ val actual = repository.parseQrCode(input, cryptoCurrency)
+ Truth.assertThat(actual).isNotEqualTo(expected)
+ }
+}
\ No newline at end of file
diff --git a/data/settings/src/main/java/com/tangem/data/settings/DefaultSettingsRepository.kt b/data/settings/src/main/java/com/tangem/data/settings/DefaultSettingsRepository.kt
index 4e7ff03252..71e8f8d654 100644
--- a/data/settings/src/main/java/com/tangem/data/settings/DefaultSettingsRepository.kt
+++ b/data/settings/src/main/java/com/tangem/data/settings/DefaultSettingsRepository.kt
@@ -3,10 +3,8 @@ package com.tangem.data.settings
import com.tangem.data.source.preferences.PreferencesDataSource
import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.datasource.local.preferences.PreferencesKeys
-import com.tangem.datasource.local.preferences.utils.getObjectMap
import com.tangem.datasource.local.preferences.utils.getSyncOrDefault
import com.tangem.datasource.local.preferences.utils.store
-import com.tangem.domain.settings.models.AppLogsModel
import com.tangem.domain.settings.repositories.SettingsRepository
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.withContext
@@ -36,11 +34,6 @@ internal class DefaultSettingsRepository(
)
}
- override suspend fun getAppLogs(): List {
- return appPreferencesStore.getObjectMap(key = PreferencesKeys.APP_LOGS_KEY)
- .map { AppLogsModel(timestamp = it.key.toLong(), message = it.value) }
- }
-
override suspend fun updateAppLogs(message: String) {
val newLogs = DateTime.now().millis.toString() to message
@@ -51,22 +44,35 @@ internal class DefaultSettingsRepository(
}
}
- override suspend fun deleteDeprecatedLogs() {
- val threeDaysAgo = getThreeDaysAgoTimestamp()
-
+ override suspend fun deleteDeprecatedLogs(maxSize: Int) {
appPreferencesStore.editData { preferences ->
val savedLogs = preferences.getObjectMap(PreferencesKeys.APP_LOGS_KEY)
+ var sum = 0
preferences.setObjectMap(
key = PreferencesKeys.APP_LOGS_KEY,
- value = savedLogs.filterKeys { it.toLong() > threeDaysAgo },
+ value = savedLogs.entries
+ .sortedBy(Map.Entry::key)
+ .takeLastWhile {
+ sum += it.value.length
+ sum < maxSize
+ }
+ .associate { it.key to it.value },
)
}
}
- private fun getThreeDaysAgoTimestamp(): Long = DateTime.now().minusDays(THREE_DAYS).millis
+ override suspend fun isSendTapHelpPreviewEnabled(): Boolean {
+ return appPreferencesStore.getSyncOrDefault(
+ key = PreferencesKeys.SEND_TAP_HELP_PREVIEW_KEY,
+ default = true,
+ )
+ }
- private companion object {
- const val THREE_DAYS = 3
+ override suspend fun setSendTapHelpPreviewAvailability(isEnabled: Boolean) {
+ appPreferencesStore.store(
+ key = PreferencesKeys.SEND_TAP_HELP_PREVIEW_KEY,
+ value = isEnabled,
+ )
}
}
\ No newline at end of file
diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultNetworksCompatibilityRepository.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultNetworksCompatibilityRepository.kt
index f17fe671b9..006ca60bb9 100644
--- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultNetworksCompatibilityRepository.kt
+++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultNetworksCompatibilityRepository.kt
@@ -1,8 +1,10 @@
package com.tangem.data.tokens.repository
import com.tangem.blockchain.common.Blockchain
+import com.tangem.common.card.EllipticCurve
import com.tangem.data.tokens.utils.getNetwork
import com.tangem.datasource.local.userwallet.UserWalletsStore
+import com.tangem.domain.common.configs.CardConfig
import com.tangem.domain.common.extensions.*
import com.tangem.domain.common.util.cardTypesResolver
import com.tangem.domain.common.util.derivationStyleProvider
@@ -71,6 +73,15 @@ internal class DefaultNetworksCompatibilityRepository(
}
}
+ override suspend fun requiresHardenedDerivationOnly(networkId: String, userWalletId: UserWalletId): Boolean {
+ val scanResponse = getWalletOrThrow(userWalletId).scanResponse
+ val config = CardConfig.createConfig(scanResponse.card)
+ val blockchain = Blockchain.fromNetworkId(networkId) ?: return false
+
+ return config.primaryCurve(blockchain) == EllipticCurve.Ed25519Slip0010 &&
+ scanResponse.cardTypesResolver.isWallet2()
+ }
+
override fun areTokensSupportedByNetwork(networkId: String): Boolean {
return Blockchain.fromNetworkId(networkId)?.canHandleTokens() ?: false
}
diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/QuotesUnsupportedCurrenciesIdAdapter.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/QuotesUnsupportedCurrenciesIdAdapter.kt
index e6d573b353..0e69a31330 100644
--- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/QuotesUnsupportedCurrenciesIdAdapter.kt
+++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/QuotesUnsupportedCurrenciesIdAdapter.kt
@@ -51,7 +51,11 @@ internal class QuotesUnsupportedCurrenciesIdAdapter {
private val UNSUPPORTED_IDS_WITH_REPLACEMENTS = mapOf(
"optimistic-ethereum" to "ethereum",
"arbitrum-one" to "ethereum",
+ "zksync-ethereum" to "ethereum",
+ "manta-network-ethereum" to "ethereum",
+ "polygon-zkevm-ethereum" to "ethereum",
"aurora-ethereum" to "ethereum",
+ "base-ethereum" to "ethereum",
)
}
}
\ No newline at end of file
diff --git a/data/txhistory/src/main/kotlin/com/tangem/data/txhistory/repository/DefaultTxHistoryRepository.kt b/data/txhistory/src/main/kotlin/com/tangem/data/txhistory/repository/DefaultTxHistoryRepository.kt
index 9f4801ff8c..eada356d1e 100644
--- a/data/txhistory/src/main/kotlin/com/tangem/data/txhistory/repository/DefaultTxHistoryRepository.kt
+++ b/data/txhistory/src/main/kotlin/com/tangem/data/txhistory/repository/DefaultTxHistoryRepository.kt
@@ -21,6 +21,7 @@ import com.tangem.domain.walletmanager.utils.SdkPageConverter
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.models.UserWalletId
import kotlinx.coroutines.flow.Flow
+import timber.log.Timber
class DefaultTxHistoryRepository(
private val cacheRegistry: CacheRegistry,
@@ -68,6 +69,7 @@ class DefaultTxHistoryRepository(
return pager.flow
}
+ @Deprecated("Replace with getTxExploreUrl [UserWalletId, Network] instead")
override fun getTxExploreUrl(txHash: String, networkId: Network.ID): String {
val blockchain = Blockchain.fromId(networkId.value)
return when (val txExploreState = blockchain.getExploreTxUrl(txHash)) {
@@ -76,22 +78,40 @@ class DefaultTxHistoryRepository(
}
}
+ override suspend fun getTxExploreUrl(userWalletId: UserWalletId, network: Network): String {
+ val blockchain = Blockchain.fromId(network.id.value)
+ val walletManager = walletManagersFacade.getOrCreateWalletManager(
+ userWalletId = userWalletId,
+ network = network,
+ )
+ val lastTxHash = walletManager?.wallet?.recentTransactions?.last()?.hash.orEmpty()
+ return when (val txExploreState = blockchain?.getExploreTxUrl(lastTxHash)) {
+ is TxExploreState.Url -> txExploreState.url
+ else -> ""
+ }
+ }
+
override suspend fun getFixedSizeTxHistoryItems(
userWalletId: UserWalletId,
currency: CryptoCurrency,
pageSize: Int,
refresh: Boolean,
): List {
- cacheRegistry.invokeOnExpire(
- key = getTxHistoryPageKey(currency, userWalletId, Page.Initial),
- skipCache = refresh,
- block = { fetchFixedSizeTxHistoryItems(userWalletId, currency, pageSize) },
- )
- val txs = txHistoryItemsStore.getSyncOrNull(
- key = TxHistoryItemsStore.Key(userWalletId, currency),
- page = Page.Initial,
- )?.items
- return txs ?: emptyList()
+ return try {
+ cacheRegistry.invokeOnExpire(
+ key = getTxHistoryPageKey(currency, userWalletId, Page.Initial),
+ skipCache = refresh,
+ block = { fetchFixedSizeTxHistoryItems(userWalletId, currency, pageSize) },
+ )
+ val txs = txHistoryItemsStore.getSyncOrNull(
+ key = TxHistoryItemsStore.Key(userWalletId, currency),
+ page = Page.Initial,
+ )?.items
+ txs ?: emptyList()
+ } catch (e: Throwable) {
+ Timber.e(e, "Unable to load the transaction history for the requested page: ${Page.Initial}")
+ emptyList()
+ }
}
private fun getTxHistoryPageKey(currency: CryptoCurrency, userWalletId: UserWalletId, page: Page): String {
diff --git a/data/txhistory/src/main/kotlin/com/tangem/data/txhistory/repository/paging/TxHistoryPagingSource.kt b/data/txhistory/src/main/kotlin/com/tangem/data/txhistory/repository/paging/TxHistoryPagingSource.kt
index 2709c26dfc..d534e238ec 100644
--- a/data/txhistory/src/main/kotlin/com/tangem/data/txhistory/repository/paging/TxHistoryPagingSource.kt
+++ b/data/txhistory/src/main/kotlin/com/tangem/data/txhistory/repository/paging/TxHistoryPagingSource.kt
@@ -2,10 +2,8 @@ package com.tangem.data.txhistory.repository.paging
import androidx.paging.PagingSource
import androidx.paging.PagingState
-import com.tangem.blockchain.common.Blockchain
import com.tangem.data.common.cache.CacheRegistry
import com.tangem.datasource.local.txhistory.TxHistoryItemsStore
-import com.tangem.domain.common.extensions.fromNetworkId
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.txhistory.models.Page
import com.tangem.domain.txhistory.models.PaginationWrapper
@@ -82,17 +80,9 @@ internal class TxHistoryPagingSource(
}
private suspend fun PaginationWrapper.addRecentTransactions(): PaginationWrapper {
- val blockchain = Blockchain.fromNetworkId(sourceParams.currency.network.backendId)
-
- if (blockchain == null) {
- Timber.e("Blockchain not found")
- return this
- }
-
val recentItems = walletManagersFacade.getRecentTransactions(
userWalletId = sourceParams.userWalletId,
- blockchain = blockchain,
- derivationPath = sourceParams.currency.network.derivationPath.value,
+ currency = sourceParams.currency,
)
.filterUnconfirmedTransaction()
.filterIfTxAlreadyAdded(apiItems = items)
diff --git a/data/visa/build.gradle.kts b/data/visa/build.gradle.kts
index d41cf5f9c7..ac55319dfe 100644
--- a/data/visa/build.gradle.kts
+++ b/data/visa/build.gradle.kts
@@ -26,7 +26,7 @@ dependencies {
implementation(projects.domain.legacy)
/** Project - Libs */
- implementation(projects.libs.visa)
+ debugImplementation(projects.libs.visa)
/** Libs - Other */
implementation(deps.kotlin.coroutines)
diff --git a/data/visa/src/main/kotlin/com/tangem/data/visa/DefaultVisaRepository.kt b/data/visa/src/debug/kotlin/com/tangem/data/visa/DefaultVisaRepository.kt
similarity index 86%
rename from data/visa/src/main/kotlin/com/tangem/data/visa/DefaultVisaRepository.kt
rename to data/visa/src/debug/kotlin/com/tangem/data/visa/DefaultVisaRepository.kt
index d14a201a2b..a817781b53 100644
--- a/data/visa/src/main/kotlin/com/tangem/data/visa/DefaultVisaRepository.kt
+++ b/data/visa/src/debug/kotlin/com/tangem/data/visa/DefaultVisaRepository.kt
@@ -7,6 +7,7 @@ import arrow.fx.coroutines.parZip
import com.tangem.blockchain.common.address.Address
import com.tangem.blockchain.common.address.AddressType
import com.tangem.common.card.EllipticCurve
+import com.tangem.common.extensions.hexToBytes
import com.tangem.common.extensions.toHexString
import com.tangem.data.common.cache.CacheRegistry
import com.tangem.data.visa.utils.VisaConfig
@@ -58,7 +59,6 @@ internal class DefaultVisaRepository(
override suspend fun getVisaCurrency(userWalletId: UserWalletId, isRefresh: Boolean): VisaCurrency {
val address = makeAddress(userWalletId)
- // val address = "0x143fe062a538176aa0bf162f13d390208f90898f" // for testing
fetchVisaCurrencyIfExpired(address, isRefresh)
@@ -69,7 +69,7 @@ internal class DefaultVisaRepository(
private suspend fun fetchVisaCurrencyIfExpired(address: String, isRefresh: Boolean) {
cacheRegistry.invokeOnExpire(
- key = getBalancesAndLimitsKey(address),
+ key = getVisaCurrencyKey(address),
skipCache = isRefresh,
block = { fetchVisaCurrency(address) },
)
@@ -78,12 +78,12 @@ internal class DefaultVisaRepository(
private suspend fun fetchVisaCurrency(address: String) {
parZip(
dispatchers.io,
- { visaContractInfoProvider.getBalancesAndLimits(address) },
+ { visaContractInfoProvider.getContractInfo(address) },
{ getFiatRate() },
- { balancesAndLimits, fiatRate ->
+ { contractInfo, fiatRate ->
fetchedCurrencies.update { value ->
value.apply {
- put(address, currencyFactory.create(balancesAndLimits, fiatRate))
+ put(address, currencyFactory.create(contractInfo, fiatRate))
}
}
},
@@ -96,8 +96,7 @@ internal class DefaultVisaRepository(
isRefresh: Boolean,
): Flow> {
val userWallet = findVisaUserWallet(userWalletId)
- val cardPubKey = getCardPubKey(userWallet).toHexString()
- // val cardPubKey = "03DEF02B1FECC8BD3CFD52CE93235194479E1DE931EF0F55DC194967E7CCC3D12C" // for testing
+ val cardPubKey = getCardPubKey(userWallet)
val pager = Pager(
config = PagingConfig(
pageSize = pageSize,
@@ -124,8 +123,7 @@ internal class DefaultVisaRepository(
override suspend fun getTxDetails(userWalletId: UserWalletId, txId: String): VisaTxDetails {
return withContext(dispatchers.io) {
val userWallet = findVisaUserWallet(userWalletId)
- val cardPubKey = getCardPubKey(userWallet).toHexString()
- // val cardPubKey = "03DEF02B1FECC8BD3CFD52CE93235194479E1DE931EF0F55DC194967E7CCC3D12C" // for testing
+ val cardPubKey = getCardPubKey(userWallet)
val transaction = fetchedHistoryItems.value[cardPubKey]?.firstOrNull {
it.transactionId.toString() == txId
}
@@ -139,6 +137,8 @@ internal class DefaultVisaRepository(
}
private suspend fun makeAddress(userWalletId: UserWalletId): String {
+ if (IS_DEMO_MODE_ENABLED) return DEMO_ADDRESS
+
val userWallet = findVisaUserWallet(userWalletId)
val walletAddresses = makeWalletAddresses(userWallet)
val walletAddress = walletAddresses.firstOrNull { it.type == AddressType.Default }
@@ -161,16 +161,18 @@ internal class DefaultVisaRepository(
private fun makeWalletAddresses(userWallet: UserWallet): Set {
val walletBlockchain = userWallet.scanResponse.cardTypesResolver.getBlockchain()
- return walletBlockchain.makeAddresses(getCardPubKey(userWallet))
+ return walletBlockchain.makeAddresses(getCardPubKey(userWallet).hexToBytes())
}
- private fun getCardPubKey(userWallet: UserWallet): ByteArray {
+ private fun getCardPubKey(userWallet: UserWallet): String {
+ if (IS_DEMO_MODE_ENABLED) return DEMO_PUBLIC_KEY
+
val cardWallet = userWallet.scanResponse.card.wallets.firstOrNull {
it.curve == EllipticCurve.Secp256k1
}
requireNotNull(cardWallet) { "Secp256k1 card wallet not found" }
- return cardWallet.publicKey
+ return cardWallet.publicKey.toHexString()
}
private suspend fun findVisaUserWallet(userWalletId: UserWalletId): UserWallet {
@@ -184,7 +186,15 @@ internal class DefaultVisaRepository(
return userWallet
}
- private fun getBalancesAndLimitsKey(address: String): String {
- return "visa_balances_and_limits_$address"
+ private fun getVisaCurrencyKey(address: String): String {
+ return "visa_currency_$address"
+ }
+
+ private companion object {
+ // Must be `false` in production
+ const val IS_DEMO_MODE_ENABLED = false
+
+ const val DEMO_ADDRESS = "0x40d8194b7168723ece51fa34d16825c60ba03dfa"
+ const val DEMO_PUBLIC_KEY = "02C2BBA0DA1E066EA968C1EB129499F6DEBC5FD82D70D61DCAF691CDB69AF5D8B9"
}
}
\ No newline at end of file
diff --git a/data/visa/src/debug/kotlin/com/tangem/data/visa/di/ImplementedVisaDataModule.kt b/data/visa/src/debug/kotlin/com/tangem/data/visa/di/ImplementedVisaDataModule.kt
new file mode 100644
index 0000000000..a3022a444a
--- /dev/null
+++ b/data/visa/src/debug/kotlin/com/tangem/data/visa/di/ImplementedVisaDataModule.kt
@@ -0,0 +1,53 @@
+package com.tangem.data.visa.di
+
+import com.squareup.moshi.Moshi
+import com.tangem.data.common.cache.CacheRegistry
+import com.tangem.data.visa.BuildConfig
+import com.tangem.data.visa.DefaultVisaRepository
+import com.tangem.datasource.api.tangemTech.TangemTechApi
+import com.tangem.datasource.di.NetworkMoshi
+import com.tangem.datasource.local.userwallet.UserWalletsStore
+import com.tangem.domain.visa.repository.VisaRepository
+import com.tangem.lib.visa.VisaContractInfoProvider
+import com.tangem.lib.visa.api.VisaApiBuilder
+import com.tangem.utils.coroutines.CoroutineDispatcherProvider
+import dagger.Module
+import dagger.Provides
+import dagger.hilt.InstallIn
+import dagger.hilt.components.SingletonComponent
+import javax.inject.Singleton
+
+@Module
+@InstallIn(SingletonComponent::class)
+internal object ImplementedVisaDataModule {
+
+ @Provides
+ @Singleton
+ @ImplementedVisaRepository
+ fun provideVisaRepository(
+ @NetworkMoshi moshi: Moshi,
+ tangemTechApi: TangemTechApi,
+ cacheRegistry: CacheRegistry,
+ userWalletsStore: UserWalletsStore,
+ dispatchers: CoroutineDispatcherProvider,
+ ): VisaRepository {
+ val contractInfoProvider = VisaContractInfoProvider.Builder(
+ isNetworkLoggingEnabled = BuildConfig.LOG_ENABLED,
+ dispatchers = dispatchers,
+ ).build()
+ val visaApi = VisaApiBuilder(
+ useDevApi = true,
+ isNetworkLoggingEnabled = BuildConfig.LOG_ENABLED,
+ moshi = moshi,
+ ).build()
+
+ return DefaultVisaRepository(
+ contractInfoProvider,
+ tangemTechApi,
+ visaApi,
+ cacheRegistry,
+ userWalletsStore,
+ dispatchers,
+ )
+ }
+}
\ No newline at end of file
diff --git a/data/visa/src/main/kotlin/com/tangem/data/visa/utils/CurrencyUtils.kt b/data/visa/src/debug/kotlin/com/tangem/data/visa/utils/CurrencyUtils.kt
similarity index 100%
rename from data/visa/src/main/kotlin/com/tangem/data/visa/utils/CurrencyUtils.kt
rename to data/visa/src/debug/kotlin/com/tangem/data/visa/utils/CurrencyUtils.kt
diff --git a/data/visa/src/main/kotlin/com/tangem/data/visa/utils/VisaConfig.kt b/data/visa/src/debug/kotlin/com/tangem/data/visa/utils/VisaConfig.kt
similarity index 81%
rename from data/visa/src/main/kotlin/com/tangem/data/visa/utils/VisaConfig.kt
rename to data/visa/src/debug/kotlin/com/tangem/data/visa/utils/VisaConfig.kt
index e450a769cc..6b3e3f3f8b 100644
--- a/data/visa/src/main/kotlin/com/tangem/data/visa/utils/VisaConfig.kt
+++ b/data/visa/src/debug/kotlin/com/tangem/data/visa/utils/VisaConfig.kt
@@ -6,9 +6,7 @@ internal object VisaConfig {
const val NETWORK_NAME = "Polygon PoS"
- const val TOKEN_SYMBOL = "USDT"
const val TOKEN_ID = "tether"
- const val TOKEN_DECIMALS = 8
val fiatCurrency = AppCurrency(
code = "EUR",
diff --git a/data/visa/src/main/kotlin/com/tangem/data/visa/utils/VisaCurrencyFactory.kt b/data/visa/src/debug/kotlin/com/tangem/data/visa/utils/VisaCurrencyFactory.kt
similarity index 68%
rename from data/visa/src/main/kotlin/com/tangem/data/visa/utils/VisaCurrencyFactory.kt
rename to data/visa/src/debug/kotlin/com/tangem/data/visa/utils/VisaCurrencyFactory.kt
index 92bb35d307..3c9f55e37f 100644
--- a/data/visa/src/main/kotlin/com/tangem/data/visa/utils/VisaCurrencyFactory.kt
+++ b/data/visa/src/debug/kotlin/com/tangem/data/visa/utils/VisaCurrencyFactory.kt
@@ -1,7 +1,7 @@
package com.tangem.data.visa.utils
import com.tangem.domain.visa.model.VisaCurrency
-import com.tangem.lib.visa.model.VisaBalancesAndLimits
+import com.tangem.lib.visa.model.VisaContractInfo
import org.joda.time.DateTime
import org.joda.time.DateTimeZone
import org.joda.time.Instant
@@ -10,21 +10,22 @@ import java.math.BigInteger
internal class VisaCurrencyFactory {
- fun create(balancesAndLimits: VisaBalancesAndLimits, fiatRate: BigDecimal?): VisaCurrency {
+ fun create(contractInfo: VisaContractInfo, fiatRate: BigDecimal?): VisaCurrency {
val now = Instant.now()
- val currentLimit = if (balancesAndLimits.limitsChangeDate > now) {
- balancesAndLimits.oldLimits
+ val currentLimit = if (contractInfo.limitsChangeDate > now) {
+ contractInfo.oldLimits
} else {
- balancesAndLimits.newLimits
+ contractInfo.newLimits
}
+ val remainingOtpLimit = getRemainingOtp(currentLimit, now)
return VisaCurrency(
- symbol = VisaConfig.TOKEN_SYMBOL,
+ symbol = contractInfo.token.symbol,
networkName = VisaConfig.NETWORK_NAME,
- decimals = VisaConfig.TOKEN_DECIMALS,
+ decimals = contractInfo.token.decimals,
fiatRate = fiatRate,
fiatCurrency = VisaConfig.fiatCurrency,
- balances = with(balancesAndLimits) {
+ balances = with(contractInfo) {
VisaCurrency.Balances(
total = balances.total,
verified = balances.verified,
@@ -35,15 +36,15 @@ internal class VisaCurrencyFactory {
)
},
limits = VisaCurrency.Limits(
- remainingOtp = getRemainingOtp(currentLimit, now),
- remainingNoOtp = getRemainingNoOtp(currentLimit, now),
+ remainingOtp = remainingOtpLimit,
+ remainingNoOtp = minOf(remainingOtpLimit, getRemainingNoOtp(currentLimit, now)),
singleTransaction = currentLimit.singleTransactionLimit,
expirationDate = getLimitsExpirationDate(currentLimit, now),
),
)
}
- private fun getRemainingOtp(currentLimit: VisaBalancesAndLimits.Limits, now: Instant): BigDecimal {
+ private fun getRemainingOtp(currentLimit: VisaContractInfo.Limits, now: Instant): BigDecimal {
if (currentLimit.expirationDate >= now) {
return currentLimit.spendLimit.limit - currentLimit.spendLimit.spent
}
@@ -51,7 +52,7 @@ internal class VisaCurrencyFactory {
return currentLimit.spendLimit.limit
}
- private fun getRemainingNoOtp(currentLimit: VisaBalancesAndLimits.Limits, now: Instant): BigDecimal {
+ private fun getRemainingNoOtp(currentLimit: VisaContractInfo.Limits, now: Instant): BigDecimal {
if (currentLimit.expirationDate >= now) {
return currentLimit.noOtpLimit.limit - currentLimit.noOtpLimit.spent
}
@@ -59,7 +60,7 @@ internal class VisaCurrencyFactory {
return currentLimit.noOtpLimit.limit
}
- private fun getLimitsExpirationDate(currentLimits: VisaBalancesAndLimits.Limits, now: Instant): DateTime {
+ private fun getLimitsExpirationDate(currentLimits: VisaContractInfo.Limits, now: Instant): DateTime {
val expirationDate = if (currentLimits.expirationDate >= now) {
currentLimits.expirationDate.toDateTime()
} else {
diff --git a/data/visa/src/main/kotlin/com/tangem/data/visa/utils/VisaTxDetailsFactory.kt b/data/visa/src/debug/kotlin/com/tangem/data/visa/utils/VisaTxDetailsFactory.kt
similarity index 100%
rename from data/visa/src/main/kotlin/com/tangem/data/visa/utils/VisaTxDetailsFactory.kt
rename to data/visa/src/debug/kotlin/com/tangem/data/visa/utils/VisaTxDetailsFactory.kt
diff --git a/data/visa/src/main/kotlin/com/tangem/data/visa/utils/VisaTxHistoryItemFactory.kt b/data/visa/src/debug/kotlin/com/tangem/data/visa/utils/VisaTxHistoryItemFactory.kt
similarity index 100%
rename from data/visa/src/main/kotlin/com/tangem/data/visa/utils/VisaTxHistoryItemFactory.kt
rename to data/visa/src/debug/kotlin/com/tangem/data/visa/utils/VisaTxHistoryItemFactory.kt
diff --git a/data/visa/src/main/kotlin/com/tangem/data/visa/utils/VisaTxHistoryPagingSource.kt b/data/visa/src/debug/kotlin/com/tangem/data/visa/utils/VisaTxHistoryPagingSource.kt
similarity index 100%
rename from data/visa/src/main/kotlin/com/tangem/data/visa/utils/VisaTxHistoryPagingSource.kt
rename to data/visa/src/debug/kotlin/com/tangem/data/visa/utils/VisaTxHistoryPagingSource.kt
diff --git a/data/visa/src/main/kotlin/com/tangem/data/visa/DummyVisaRepository.kt b/data/visa/src/main/kotlin/com/tangem/data/visa/DummyVisaRepository.kt
new file mode 100644
index 0000000000..e965d71e93
--- /dev/null
+++ b/data/visa/src/main/kotlin/com/tangem/data/visa/DummyVisaRepository.kt
@@ -0,0 +1,28 @@
+package com.tangem.data.visa
+
+import androidx.paging.PagingData
+import com.tangem.domain.visa.model.VisaCurrency
+import com.tangem.domain.visa.model.VisaTxDetails
+import com.tangem.domain.visa.model.VisaTxHistoryItem
+import com.tangem.domain.visa.repository.VisaRepository
+import com.tangem.domain.wallets.models.UserWalletId
+import kotlinx.coroutines.flow.Flow
+
+internal class DummyVisaRepository : VisaRepository {
+
+ override suspend fun getVisaCurrency(userWalletId: UserWalletId, isRefresh: Boolean): VisaCurrency {
+ TODO("Not implemented for this build type")
+ }
+
+ override suspend fun getTxHistory(
+ userWalletId: UserWalletId,
+ pageSize: Int,
+ isRefresh: Boolean,
+ ): Flow> {
+ TODO("Not implemented for this build type")
+ }
+
+ override suspend fun getTxDetails(userWalletId: UserWalletId, txId: String): VisaTxDetails {
+ TODO("Not implemented for this build type")
+ }
+}
\ No newline at end of file
diff --git a/data/visa/src/main/kotlin/com/tangem/data/visa/di/ImplementedVisaRepository.kt b/data/visa/src/main/kotlin/com/tangem/data/visa/di/ImplementedVisaRepository.kt
new file mode 100644
index 0000000000..074af6e72d
--- /dev/null
+++ b/data/visa/src/main/kotlin/com/tangem/data/visa/di/ImplementedVisaRepository.kt
@@ -0,0 +1,6 @@
+package com.tangem.data.visa.di
+
+import javax.inject.Qualifier
+
+@Qualifier
+internal annotation class ImplementedVisaRepository
\ No newline at end of file
diff --git a/data/visa/src/main/kotlin/com/tangem/data/visa/di/ImplementedVisaRepositoryModule.kt b/data/visa/src/main/kotlin/com/tangem/data/visa/di/ImplementedVisaRepositoryModule.kt
new file mode 100644
index 0000000000..96de38914e
--- /dev/null
+++ b/data/visa/src/main/kotlin/com/tangem/data/visa/di/ImplementedVisaRepositoryModule.kt
@@ -0,0 +1,16 @@
+package com.tangem.data.visa.di
+
+import com.tangem.domain.visa.repository.VisaRepository
+import dagger.BindsOptionalOf
+import dagger.Module
+import dagger.hilt.InstallIn
+import dagger.hilt.components.SingletonComponent
+
+@Module
+@InstallIn(SingletonComponent::class)
+internal interface ImplementedVisaRepositoryModule {
+
+ @BindsOptionalOf
+ @ImplementedVisaRepository
+ fun bindImplementedVisaRepository(): VisaRepository
+}
\ No newline at end of file
diff --git a/data/visa/src/main/kotlin/com/tangem/data/visa/di/VisaDataModule.kt b/data/visa/src/main/kotlin/com/tangem/data/visa/di/VisaDataModule.kt
index a0227da794..a8587868f9 100644
--- a/data/visa/src/main/kotlin/com/tangem/data/visa/di/VisaDataModule.kt
+++ b/data/visa/src/main/kotlin/com/tangem/data/visa/di/VisaDataModule.kt
@@ -1,21 +1,14 @@
package com.tangem.data.visa.di
-import com.squareup.moshi.Moshi
-import com.tangem.data.common.cache.CacheRegistry
-import com.tangem.data.visa.BuildConfig
-import com.tangem.data.visa.DefaultVisaRepository
-import com.tangem.datasource.api.tangemTech.TangemTechApi
-import com.tangem.datasource.di.NetworkMoshi
-import com.tangem.datasource.local.userwallet.UserWalletsStore
+import com.tangem.data.visa.DummyVisaRepository
import com.tangem.domain.visa.repository.VisaRepository
-import com.tangem.lib.visa.VisaContractInfoProvider
-import com.tangem.lib.visa.api.VisaApiBuilder
-import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
+import java.util.Optional
import javax.inject.Singleton
+import kotlin.jvm.optionals.getOrNull
@Module
@InstallIn(SingletonComponent::class)
@@ -24,29 +17,8 @@ internal object VisaDataModule {
@Provides
@Singleton
fun provideVisaRepository(
- @NetworkMoshi moshi: Moshi,
- tangemTechApi: TangemTechApi,
- cacheRegistry: CacheRegistry,
- userWalletsStore: UserWalletsStore,
- dispatchers: CoroutineDispatcherProvider,
+ @ImplementedVisaRepository implementedVisaRepository: Optional,
): VisaRepository {
- val contractInfoProvider = VisaContractInfoProvider.Builder(
- isNetworkLoggingEnabled = BuildConfig.LOG_ENABLED,
- dispatchers = dispatchers,
- ).build()
- val visaApi = VisaApiBuilder(
- useDevApi = true,
- isNetworkLoggingEnabled = BuildConfig.LOG_ENABLED,
- moshi = moshi,
- ).build()
-
- return DefaultVisaRepository(
- contractInfoProvider,
- tangemTechApi,
- visaApi,
- cacheRegistry,
- userWalletsStore,
- dispatchers,
- )
+ return implementedVisaRepository.getOrNull() ?: DummyVisaRepository()
}
}
\ No newline at end of file
diff --git a/domain/card/src/main/kotlin/com/tangem/domain/card/GetExtendedPublicKeyForCurrencyUseCase.kt b/domain/card/src/main/kotlin/com/tangem/domain/card/GetExtendedPublicKeyForCurrencyUseCase.kt
new file mode 100644
index 0000000000..4f107a0948
--- /dev/null
+++ b/domain/card/src/main/kotlin/com/tangem/domain/card/GetExtendedPublicKeyForCurrencyUseCase.kt
@@ -0,0 +1,30 @@
+package com.tangem.domain.card
+
+import arrow.core.Either
+import com.tangem.crypto.NetworkType
+import com.tangem.crypto.hdWallet.DerivationPath
+import com.tangem.domain.card.repository.DerivationsRepository
+import com.tangem.domain.tokens.model.Network
+import com.tangem.domain.wallets.models.UserWalletId
+
+/**
+ * Derivates an exteneded public key (xpub) based on blockchain hardened derivation
+ */
+class GetExtendedPublicKeyForCurrencyUseCase(
+ private val derivationsRepository: DerivationsRepository,
+) {
+ suspend operator fun invoke(
+ userWalletId: UserWalletId,
+ derivation: Network.DerivationPath,
+ ): Either {
+ return Either.catch {
+ val derivationPath = requireNotNull(derivation.value?.let { DerivationPath(it) }) {
+ error("Derivation is null")
+ }
+ val hardenedNodes = derivationPath.nodes.filter { it.isHardened }
+ val hardenedDerivation = DerivationPath(hardenedNodes)
+ derivationsRepository.deriveExtendedPublicKey(userWalletId, hardenedDerivation)
+ ?.serialize(NetworkType.Mainnet).orEmpty()
+ }
+ }
+}
\ No newline at end of file
diff --git a/domain/card/src/main/kotlin/com/tangem/domain/card/ScanCardException.kt b/domain/card/src/main/kotlin/com/tangem/domain/card/ScanCardException.kt
index 3f153ee689..667e29f062 100644
--- a/domain/card/src/main/kotlin/com/tangem/domain/card/ScanCardException.kt
+++ b/domain/card/src/main/kotlin/com/tangem/domain/card/ScanCardException.kt
@@ -1,14 +1,20 @@
package com.tangem.domain.card
-// TODO: May be add new error types
sealed class ScanCardException : Exception() {
- object UserCancelled : ScanCardException()
- object WrongAccessCode : ScanCardException()
+ data object UserCancelled : ScanCardException() {
+ @Suppress("UnusedPrivateMember")
+ private fun readResolve(): Any = UserCancelled
+ }
+
+ data object WrongAccessCode : ScanCardException() {
+ @Suppress("UnusedPrivateMember")
+ private fun readResolve(): Any = WrongAccessCode
+ }
open class ChainException : ScanCardException()
- data class UnknownException(override val cause: Exception) : ScanCardException()
+ data class UnknownException(override val cause: Throwable) : ScanCardException()
data class WrongCardId(val cardId: String) : ScanCardException()
}
\ No newline at end of file
diff --git a/domain/card/src/main/kotlin/com/tangem/domain/card/ScanCardProcessor.kt b/domain/card/src/main/kotlin/com/tangem/domain/card/ScanCardProcessor.kt
index 5b3824d63e..ff96eda7f5 100644
--- a/domain/card/src/main/kotlin/com/tangem/domain/card/ScanCardProcessor.kt
+++ b/domain/card/src/main/kotlin/com/tangem/domain/card/ScanCardProcessor.kt
@@ -16,7 +16,6 @@ interface ScanCardProcessor {
analyticsSource: AnalyticsParam.ScreensSources,
cardId: String? = null,
onProgressStateChange: suspend (showProgress: Boolean) -> Unit = {},
- onScanStateChange: suspend (scanInProgress: Boolean) -> Unit = {},
onWalletNotCreated: suspend () -> Unit = {},
disclaimerWillShow: () -> Unit = {},
onFailure: suspend (error: TangemError) -> Unit = {},
diff --git a/domain/card/src/main/kotlin/com/tangem/domain/card/ScanCardUseCase.kt b/domain/card/src/main/kotlin/com/tangem/domain/card/ScanCardUseCase.kt
index 69dbc7a617..04abfb1fce 100644
--- a/domain/card/src/main/kotlin/com/tangem/domain/card/ScanCardUseCase.kt
+++ b/domain/card/src/main/kotlin/com/tangem/domain/card/ScanCardUseCase.kt
@@ -1,9 +1,7 @@
package com.tangem.domain.card
import arrow.core.Either
-import arrow.core.EitherNel
-import arrow.core.flatMap
-import com.tangem.TangemSdk
+import arrow.core.raise.either
import com.tangem.domain.card.repository.CardSdkConfigRepository
import com.tangem.domain.card.repository.ScanCardRepository
import com.tangem.domain.core.chain.Chain
@@ -11,12 +9,7 @@ import com.tangem.domain.core.chain.ChainProcessor
import com.tangem.domain.models.scan.ScanResponse
/**
- * Use case responsible for scanning a card and returning a [ScanResponse] object
- *
- * @property scanCardRepository a repository object implementing [ScanCardRepository] interface
- * @property cardSdkConfigRepository an instance of [TangemSdk] to configure the display format of the card ID
- *
- * @constructor create a new instance of [ScanCardUseCase] with the given dependencies
+ * Use case responsible for scanning a card and returning a [ScanResponse] object.
*/
class ScanCardUseCase(
private val scanCardRepository: ScanCardRepository,
@@ -24,37 +17,41 @@ class ScanCardUseCase(
) {
/** A [ChainProcessor] object to launch the after-scan chains */
- private val scanChainProcessor by lazy {
- ChainProcessor()
- }
+ private val scanChainProcessor = ChainProcessor()
/**
- * Scan a card and return a [ScanResponse] object.
+ * Scan a card.
*
- * @param cardId an optional card ID to scan. If null, can scan any card present
- * @param allowRequestAccessCodeFromStorage whether to prompt the user for an access code if needed
- * @param afterScanChains a list of chains that should be executed after a successful card scan
- * operation. Defaults to an empty array
+ * @param cardId An optional card ID to scan. If `null`, can scan any card present
+ * @param allowRequestAccessCodeFromStorage Whether to prompt the user for an access code if needed
+ * @param afterScanChains A list of chains that should be executed after a card scan
+ * operation. Defaults to an empty array.
*
- * @return a [EitherNel] object with either a non-empty list of [ScanCardException] or a [ScanResponse]
+ * @return [Either] object with either a [ScanCardException] or a [ScanResponse] object.
*/
suspend operator fun invoke(
cardId: String? = null,
allowRequestAccessCodeFromStorage: Boolean = false,
- afterScanChains: List> = emptyList(),
- ): Either {
+ afterScanChains: List> = emptyList(),
+ ): Either = either {
cardSdkConfigRepository.resetCardIdDisplayFormat()
- scanChainProcessor.addChains(afterScanChains)
+ scanChainProcessor.setChains(afterScanChains)
- return scanCardRepository.scanCard(
- cardId = cardId,
- allowRequestAccessCodeFromStorage = allowRequestAccessCodeFromStorage,
- )
- .onRight { scanResponse ->
- cardSdkConfigRepository.updateCardIdDisplayFormat(scanResponse.productType)
- }
- .flatMap { response ->
- scanChainProcessor.launchChains(initial = response)
+ val maybeScanResponse = scanCard(cardId, allowRequestAccessCodeFromStorage)
+ val scanResponse = scanChainProcessor.launchChains(maybeScanResponse).bind()
+
+ cardSdkConfigRepository.updateCardIdDisplayFormat(scanResponse.productType)
+
+ scanResponse
+ }
+
+ private suspend fun scanCard(
+ cardId: String?,
+ allowRequestAccessCodeFromStorage: Boolean,
+ ): Either {
+ return Either.catch { scanCardRepository.scanCard(cardId, allowRequestAccessCodeFromStorage) }
+ .mapLeft { e ->
+ e as? ScanCardException ?: ScanCardException.UnknownException(e)
}
}
}
\ No newline at end of file
diff --git a/domain/card/src/main/kotlin/com/tangem/domain/card/repository/DerivationsRepository.kt b/domain/card/src/main/kotlin/com/tangem/domain/card/repository/DerivationsRepository.kt
index cd1af0a789..209b68817a 100644
--- a/domain/card/src/main/kotlin/com/tangem/domain/card/repository/DerivationsRepository.kt
+++ b/domain/card/src/main/kotlin/com/tangem/domain/card/repository/DerivationsRepository.kt
@@ -1,5 +1,7 @@
package com.tangem.domain.card.repository
+import com.tangem.crypto.hdWallet.DerivationPath
+import com.tangem.crypto.hdWallet.bip32.ExtendedPublicKey
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.wallets.models.UserWalletId
@@ -7,4 +9,7 @@ interface DerivationsRepository {
@Throws
suspend fun derivePublicKeys(userWalletId: UserWalletId, currencies: List)
+
+ @Throws
+ suspend fun deriveExtendedPublicKey(userWalletId: UserWalletId, derivation: DerivationPath): ExtendedPublicKey?
}
\ No newline at end of file
diff --git a/domain/card/src/main/kotlin/com/tangem/domain/card/repository/ScanCardRepository.kt b/domain/card/src/main/kotlin/com/tangem/domain/card/repository/ScanCardRepository.kt
index 2de00090cf..90f2016383 100644
--- a/domain/card/src/main/kotlin/com/tangem/domain/card/repository/ScanCardRepository.kt
+++ b/domain/card/src/main/kotlin/com/tangem/domain/card/repository/ScanCardRepository.kt
@@ -1,6 +1,5 @@
package com.tangem.domain.card.repository
-import arrow.core.Either
import com.tangem.domain.card.ScanCardException
import com.tangem.domain.models.scan.ScanResponse
@@ -15,10 +14,8 @@ interface ScanCardRepository {
* @param cardId an optional card ID to scan. If null, the repository should scan any card present.
* @param allowRequestAccessCodeFromStorage whether the access code can be requested from
* internal storage during the scan process
- * @return an [Either] that contains a [ScanCardException] in case of an error or a [ScanResponse].
+ * @return a [ScanResponse] object with the result of the scan.
+ * @throws [ScanCardException] if the scan process fails.
*/
- suspend fun scanCard(
- cardId: String?,
- allowRequestAccessCodeFromStorage: Boolean,
- ): Either
+ suspend fun scanCard(cardId: String?, allowRequestAccessCodeFromStorage: Boolean): ScanResponse
}
\ No newline at end of file
diff --git a/domain/core/src/main/kotlin/com/tangem/domain/core/chain/Chain.kt b/domain/core/src/main/kotlin/com/tangem/domain/core/chain/Chain.kt
deleted file mode 100644
index a7e0d5bc38..0000000000
--- a/domain/core/src/main/kotlin/com/tangem/domain/core/chain/Chain.kt
+++ /dev/null
@@ -1,18 +0,0 @@
-package com.tangem.domain.core.chain
-
-import arrow.core.Either
-
-/**
- * A chain in the [ChainProcessor] class for processing a chain of operations with the ability to handle errors.
- * @param E the type of error
- * @param R the type of result
- */
-interface Chain {
-
- /**
- * Invokes the chain with the previous chain result as input and returns an [Either] result.
- * @param previousChainResult the previous chain result as an [R]
- * @return the result of the chain processing as an [Either]
- */
- suspend operator fun invoke(previousChainResult: R): Either
-}
\ No newline at end of file
diff --git a/domain/core/src/main/kotlin/com/tangem/domain/core/chain/ChainProcessor.kt b/domain/core/src/main/kotlin/com/tangem/domain/core/chain/ChainProcessor.kt
index c1cac09eb3..4e95318b6c 100644
--- a/domain/core/src/main/kotlin/com/tangem/domain/core/chain/ChainProcessor.kt
+++ b/domain/core/src/main/kotlin/com/tangem/domain/core/chain/ChainProcessor.kt
@@ -1,7 +1,6 @@
package com.tangem.domain.core.chain
import arrow.core.Either
-import arrow.core.raise.either
// TODO: Create Ior implementation
// TODO: Create accumulate implementation
@@ -10,7 +9,7 @@ import arrow.core.raise.either
* @param E the type of error
* @param R the type of result
*/
-class ChainProcessor {
+class ChainProcessor {
/**
* The list of chains to be executed in order.
@@ -18,18 +17,20 @@ class ChainProcessor {
private val chains: MutableList> = mutableListOf()
/**
- * Adds chains to the existing list of chains to be executed.
- * @param chains the chains to be added to the list
+ * Sets the list of chains to be executed.
+ * @param chains the chains to be set
*/
- fun addChains(chains: List>) {
+ fun setChains(chains: List>) {
+ this.chains.clear()
this.chains.addAll(chains)
}
- suspend fun launchChains(initial: R): Either {
- return either {
- chains.fold(initial) { prevChainResult, chain ->
- chain.invoke(prevChainResult).bind()
- }
+ /**
+ * Launches the chains with the [initial] value.
+ * */
+ suspend fun launchChains(initial: Either): Either {
+ return chains.fold(initial) { prevChainResult, chain ->
+ chain.launch(prevChainResult)
}
}
}
\ No newline at end of file
diff --git a/domain/core/src/main/kotlin/com/tangem/domain/core/chain/Chains.kt b/domain/core/src/main/kotlin/com/tangem/domain/core/chain/Chains.kt
new file mode 100644
index 0000000000..02b5da3924
--- /dev/null
+++ b/domain/core/src/main/kotlin/com/tangem/domain/core/chain/Chains.kt
@@ -0,0 +1,44 @@
+package com.tangem.domain.core.chain
+
+import arrow.core.Either
+import arrow.core.flatMap
+
+/**
+ * A chain in the [ChainProcessor] class for processing a chain of operations with the ability to handle errors.
+ * @param E the type of error
+ * @param R the type of result
+ */
+interface Chain {
+
+ /**
+ * Invokes the chain with the previous chain result as input and returns an [Either] result.
+ *
+ * @param previousChainResult the previous chain result as an [Either]
+ * @return the result of the chain processing as an [Either]
+ */
+ suspend fun launch(previousChainResult: Either): Either
+}
+
+/**
+ * A chain in the [ChainProcessor] class for processing a chain of operations with the ability to handle errors.
+ *
+ * This chain will be launched only if the previous chains in the [ChainProcessor] have
+ * returned a right side of [Either].
+ *
+ * @param E the type of error
+ * @param R the type of result
+ */
+abstract class ResultChain : Chain {
+
+ final override suspend fun launch(previousChainResult: Either): Either {
+ return previousChainResult.flatMap { launch(it) }
+ }
+
+ /**
+ * Invokes the chain with the previous chain result as input and returns a result.
+ *
+ * @param previousChainResult the previous chain result
+ * @return the result of the chain processing as an [Either]
+ */
+ abstract suspend fun launch(previousChainResult: R): Either
+}
\ No newline at end of file
diff --git a/domain/feedback/.gitignore b/domain/feedback/.gitignore
new file mode 100644
index 0000000000..42afabfd2a
--- /dev/null
+++ b/domain/feedback/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/domain/feedback/build.gradle.kts b/domain/feedback/build.gradle.kts
new file mode 100644
index 0000000000..3e7859b754
--- /dev/null
+++ b/domain/feedback/build.gradle.kts
@@ -0,0 +1,16 @@
+plugins {
+ alias(deps.plugins.android.library)
+ alias(deps.plugins.kotlin.android)
+ id("configuration")
+}
+
+android {
+ namespace = "com.tangem.domain.feedback"
+}
+
+dependencies {
+ implementation(deps.arrow.core)
+ implementation(deps.jodatime)
+
+ implementation(projects.core.res)
+}
\ No newline at end of file
diff --git a/domain/feedback/src/main/java/com/tangem/domain/feedback/FeedbackDataBuilder.kt b/domain/feedback/src/main/java/com/tangem/domain/feedback/FeedbackDataBuilder.kt
new file mode 100644
index 0000000000..5350902e33
--- /dev/null
+++ b/domain/feedback/src/main/java/com/tangem/domain/feedback/FeedbackDataBuilder.kt
@@ -0,0 +1,111 @@
+package com.tangem.domain.feedback
+
+import com.tangem.domain.feedback.models.BlockchainInfo
+import com.tangem.domain.feedback.models.CardInfo
+import com.tangem.domain.feedback.models.PhoneInfo
+import com.tangem.domain.feedback.models.UserWalletsInfo
+import com.tangem.domain.feedback.utils.breakLine
+import com.tangem.domain.feedback.models.BlockchainInfo.Addresses as BlockchainAddresses
+
+internal class FeedbackDataBuilder {
+
+ private val builder = StringBuilder()
+
+ fun addUserWalletsInfo(userWalletsInfo: UserWalletsInfo) {
+ builder.appendKeyValue("User Wallet ID", userWalletsInfo.selectedUserWalletId)
+ builder.appendKeyValue("Total saved wallets", userWalletsInfo.totalUserWallets.toString())
+ }
+
+ fun addCardInfo(cardInfo: CardInfo) {
+ builder.appendKeyValue("Card ID", cardInfo.cardId)
+ builder.appendKeyValue("Firmware version", cardInfo.firmwareVersion)
+ builder.appendKeyValue("Imported wallet", if (cardInfo.isImported) "yes" else "no")
+ builder.appendKeyValue("Card Blockchain", cardInfo.cardBlockchain)
+ builder.appendSignedHashes(cardInfo.signedHashesList)
+ }
+
+ fun addBlockchainInfoList(blockchainInfoList: List) {
+ blockchainInfoList.forEachBlockchain { isLastIndex ->
+ builder.appendKeyValue("Blockchain", blockchain)
+ builder.appendKeyValue("Derivation path", derivationPath)
+
+ // enable later
+ // if (walletInfo.blockchain == Blockchain.Bitcoin) {
+ // builder.appendKeyValue("XPUB", infoHolder.extendedPublicKey)
+ // }
+
+ builder.appendKeyValue("Outputs count", outputsCount)
+
+ if (tokens.isNotEmpty()) {
+ builder.append("Tokens:")
+ builder.breakLine()
+ tokens.forEach { token ->
+ builder.appendKeyValue("ID", token.id ?: "[custom token]")
+ builder.appendKeyValue("Name", token.name)
+ builder.appendKeyValue("Contract address", token.contractAddress)
+ }
+ }
+
+ builder.appendKeyValue("Host", host)
+
+ builder.appendAddresses(
+ key = "Wallet address${addresses.isMultiple(suffix = "es")}",
+ addresses = addresses,
+ )
+
+ builder.appendAddresses(
+ key = "Explorer link${explorerLinks.isMultiple(suffix = "s")}",
+ addresses = explorerLinks,
+ )
+
+ if (!isLastIndex) builder.appendDelimiter()
+ }
+ }
+
+ fun addPhoneInfo(phoneInfo: PhoneInfo) {
+ builder.appendKeyValue("Phone model", phoneInfo.phoneModel)
+ builder.appendKeyValue("OS version", phoneInfo.osVersion)
+ builder.appendKeyValue("App version", phoneInfo.appVersion)
+ }
+
+ fun addDelimiter(): StringBuilder = builder.appendDelimiter()
+
+ fun build(): String = builder.trimEnd().toString()
+
+ private fun StringBuilder.appendKeyValue(key: String, value: String?) {
+ if (value.isNullOrBlank()) return
+
+ val keyValuePrefix = if (key.isBlank()) "" else "$key: "
+ append("$keyValuePrefix$value\n")
+ }
+
+ private fun StringBuilder.appendSignedHashes(signedHashesList: List) {
+ signedHashesList.forEach {
+ appendKeyValue("Signed hashes [${it.curve}]", it.total)
+ }
+ }
+
+ private fun StringBuilder.appendAddresses(key: String, addresses: BlockchainAddresses) {
+ appendKeyValue(
+ key = key,
+ value = when (addresses) {
+ is BlockchainInfo.Addresses.Multiple -> {
+ addresses.values.joinToString(separator = "\n", prefix = "\n") {
+ "${it.type} — ${it.value}"
+ }
+ }
+ is BlockchainInfo.Addresses.Single -> addresses.value
+ },
+ )
+ }
+
+ private fun BlockchainAddresses.isMultiple(suffix: String): String {
+ return if (this is BlockchainAddresses.Multiple) suffix else ""
+ }
+
+ private fun StringBuilder.appendDelimiter(): StringBuilder = append("----------\n")
+
+ private inline fun List.forEachBlockchain(action: BlockchainInfo.(Boolean) -> Unit) {
+ return forEachIndexed { index, info -> info.action(index == lastIndex) }
+ }
+}
\ No newline at end of file
diff --git a/domain/feedback/src/main/java/com/tangem/domain/feedback/GetSupportFeedbackEmailUseCase.kt b/domain/feedback/src/main/java/com/tangem/domain/feedback/GetSupportFeedbackEmailUseCase.kt
new file mode 100644
index 0000000000..6ab491c734
--- /dev/null
+++ b/domain/feedback/src/main/java/com/tangem/domain/feedback/GetSupportFeedbackEmailUseCase.kt
@@ -0,0 +1,113 @@
+package com.tangem.domain.feedback
+
+import android.content.res.Resources
+import com.tangem.domain.feedback.models.CardInfo
+import com.tangem.domain.feedback.models.SupportFeedbackEmail
+import com.tangem.domain.feedback.repository.FeedbackRepository
+import com.tangem.domain.feedback.utils.skipLine
+import org.joda.time.DateTime
+import org.joda.time.format.DateTimeFormatterBuilder
+import java.util.Locale
+
+/**
+ * Get email with feedback for support
+ *
+ * @property feedbackRepository feedback repository
+ * @property resources resources for getting strings
+ *
+[REDACTED_AUTHOR]
+ */
+class GetSupportFeedbackEmailUseCase(
+ private val feedbackRepository: FeedbackRepository,
+ private val resources: Resources,
+) {
+
+ // 00.00 00:00:00.000
+ private val dateFormatter = DateTimeFormatterBuilder()
+ .appendDayOfMonth(2)
+ .appendLiteral('.')
+ .appendMonthOfYear(2)
+ .appendLiteral(' ')
+ .appendHourOfDay(2)
+ .appendLiteral(':')
+ .appendMinuteOfHour(2)
+ .appendLiteral(':')
+ .appendSecondOfMinute(2)
+ .appendLiteral('.')
+ .appendMillisOfSecond(3)
+ .toFormatter()
+ .withLocale(Locale.getDefault())
+
+ suspend operator fun invoke(): SupportFeedbackEmail {
+ val cardInfo = feedbackRepository.getCardInfo()
+
+ return SupportFeedbackEmail(
+ address = getEmail(isStart2Coin = cardInfo.isStart2Coin),
+ subject = getSubject(isStart2Coin = cardInfo.isStart2Coin),
+ message = createMessage(cardInfo),
+ file = feedbackRepository.createLogFile(logs = getLogs()),
+ )
+ }
+
+ private fun getEmail(isStart2Coin: Boolean): String {
+ return if (isStart2Coin) START2COIN_SUPPORT_EMAIL else TANGEM_SUPPORT_EMAIL
+ }
+
+ private fun getSubject(isStart2Coin: Boolean): String {
+ return resources.getString(
+ if (isStart2Coin) {
+ R.string.feedback_subject_support
+ } else {
+ R.string.feedback_subject_support_tangem
+ },
+ )
+ }
+
+ private suspend fun createMessage(cardInfo: CardInfo): String {
+ return StringBuilder().apply {
+ append(resources.getString(R.string.feedback_preface_support))
+ skipLine()
+ append(resources.getString(R.string.feedback_data_collection_message))
+ skipLine()
+ append(
+ FeedbackDataBuilder().apply {
+ addUserWalletsInfo(userWalletsInfo = feedbackRepository.getUserWalletsInfo())
+ addDelimiter()
+ addCardInfo(cardInfo)
+ addDelimiter()
+ addBlockchainInfoList(blockchainInfoList = feedbackRepository.getBlockchainInfoList())
+ addDelimiter()
+ addPhoneInfo(phoneInfo = feedbackRepository.getPhoneInfo())
+ }.build(),
+ )
+ }.toString()
+ }
+
+ private suspend fun getLogs(): String {
+ val builder = StringBuilder()
+
+ var sum = 0
+ val appLogs = feedbackRepository.getAppLogs()
+ for (i in appLogs.lastIndex downTo 0) {
+ val log = appLogs[i]
+ val date = dateFormatter.print(DateTime(log.timestamp))
+
+ val formattedLog = "$date: ${log.message}\n"
+
+ sum += formattedLog.length
+ if (sum < GMAIL_MAX_FILE_SIZE) {
+ builder.insert(0, formattedLog)
+ } else {
+ break
+ }
+ }
+
+ return builder.toString()
+ }
+
+ private companion object {
+ const val START2COIN_SUPPORT_EMAIL = "cardsupport@start2coin.com"
+ const val TANGEM_SUPPORT_EMAIL = "support@tangem.com"
+ const val GMAIL_MAX_FILE_SIZE = 24_900_000 // ≈ 25 MB
+ }
+}
\ No newline at end of file
diff --git a/domain/feedback/src/main/java/com/tangem/domain/feedback/models/AppLogModel.kt b/domain/feedback/src/main/java/com/tangem/domain/feedback/models/AppLogModel.kt
new file mode 100644
index 0000000000..6688c29a1f
--- /dev/null
+++ b/domain/feedback/src/main/java/com/tangem/domain/feedback/models/AppLogModel.kt
@@ -0,0 +1,3 @@
+package com.tangem.domain.feedback.models
+
+data class AppLogModel(val timestamp: Long, val message: String)
\ No newline at end of file
diff --git a/domain/feedback/src/main/java/com/tangem/domain/feedback/models/BlockchainInfo.kt b/domain/feedback/src/main/java/com/tangem/domain/feedback/models/BlockchainInfo.kt
new file mode 100644
index 0000000000..da0a0df2a5
--- /dev/null
+++ b/domain/feedback/src/main/java/com/tangem/domain/feedback/models/BlockchainInfo.kt
@@ -0,0 +1,28 @@
+package com.tangem.domain.feedback.models
+
+data class BlockchainInfo(
+ val blockchain: String,
+ val derivationPath: String,
+ val outputsCount: String?,
+ val host: String,
+ val addresses: Addresses,
+ val explorerLinks: Addresses,
+ val tokens: List,
+) {
+
+ sealed class Addresses {
+
+ data class Single(val value: String) : Addresses()
+
+ data class Multiple(val values: List) : Addresses() {
+
+ data class AddressInfo(val type: String, val value: String)
+ }
+ }
+
+ data class TokenInfo(
+ val id: String?,
+ val name: String,
+ val contractAddress: String,
+ )
+}
\ No newline at end of file
diff --git a/domain/feedback/src/main/java/com/tangem/domain/feedback/models/CardInfo.kt b/domain/feedback/src/main/java/com/tangem/domain/feedback/models/CardInfo.kt
new file mode 100644
index 0000000000..7ae84ac608
--- /dev/null
+++ b/domain/feedback/src/main/java/com/tangem/domain/feedback/models/CardInfo.kt
@@ -0,0 +1,13 @@
+package com.tangem.domain.feedback.models
+
+data class CardInfo(
+ val cardId: String,
+ val firmwareVersion: String,
+ val cardBlockchain: String?,
+ val signedHashesList: List,
+ val isImported: Boolean,
+ val isStart2Coin: Boolean,
+) {
+
+ data class SignedHashes(val curve: String, val total: String?)
+}
\ No newline at end of file
diff --git a/domain/feedback/src/main/java/com/tangem/domain/feedback/models/PhoneInfo.kt b/domain/feedback/src/main/java/com/tangem/domain/feedback/models/PhoneInfo.kt
new file mode 100644
index 0000000000..578d9e2e00
--- /dev/null
+++ b/domain/feedback/src/main/java/com/tangem/domain/feedback/models/PhoneInfo.kt
@@ -0,0 +1,7 @@
+package com.tangem.domain.feedback.models
+
+data class PhoneInfo(
+ val phoneModel: String,
+ val osVersion: String,
+ val appVersion: String,
+)
\ No newline at end of file
diff --git a/domain/feedback/src/main/java/com/tangem/domain/feedback/models/SupportFeedbackEmail.kt b/domain/feedback/src/main/java/com/tangem/domain/feedback/models/SupportFeedbackEmail.kt
new file mode 100644
index 0000000000..35faf18f6f
--- /dev/null
+++ b/domain/feedback/src/main/java/com/tangem/domain/feedback/models/SupportFeedbackEmail.kt
@@ -0,0 +1,10 @@
+package com.tangem.domain.feedback.models
+
+import java.io.File
+
+data class SupportFeedbackEmail(
+ val address: String,
+ val subject: String,
+ val message: String,
+ val file: File? = null,
+)
\ No newline at end of file
diff --git a/domain/feedback/src/main/java/com/tangem/domain/feedback/models/UserWalletsInfo.kt b/domain/feedback/src/main/java/com/tangem/domain/feedback/models/UserWalletsInfo.kt
new file mode 100644
index 0000000000..0438e80ae4
--- /dev/null
+++ b/domain/feedback/src/main/java/com/tangem/domain/feedback/models/UserWalletsInfo.kt
@@ -0,0 +1,6 @@
+package com.tangem.domain.feedback.models
+
+data class UserWalletsInfo(
+ val selectedUserWalletId: String,
+ val totalUserWallets: Int,
+)
\ No newline at end of file
diff --git a/domain/feedback/src/main/java/com/tangem/domain/feedback/repository/FeedbackRepository.kt b/domain/feedback/src/main/java/com/tangem/domain/feedback/repository/FeedbackRepository.kt
new file mode 100644
index 0000000000..58281067c1
--- /dev/null
+++ b/domain/feedback/src/main/java/com/tangem/domain/feedback/repository/FeedbackRepository.kt
@@ -0,0 +1,19 @@
+package com.tangem.domain.feedback.repository
+
+import com.tangem.domain.feedback.models.*
+import java.io.File
+
+interface FeedbackRepository {
+
+ suspend fun getUserWalletsInfo(): UserWalletsInfo
+
+ suspend fun getCardInfo(): CardInfo
+
+ suspend fun getBlockchainInfoList(): List
+
+ fun getPhoneInfo(): PhoneInfo
+
+ suspend fun getAppLogs(): List
+
+ suspend fun createLogFile(logs: String): File?
+}
\ No newline at end of file
diff --git a/domain/feedback/src/main/java/com/tangem/domain/feedback/utils/StringBuilderExt.kt b/domain/feedback/src/main/java/com/tangem/domain/feedback/utils/StringBuilderExt.kt
new file mode 100644
index 0000000000..a2e3cb68ee
--- /dev/null
+++ b/domain/feedback/src/main/java/com/tangem/domain/feedback/utils/StringBuilderExt.kt
@@ -0,0 +1,5 @@
+package com.tangem.domain.feedback.utils
+
+fun StringBuilder.breakLine(count: Int = 1): StringBuilder = append("\n".repeat(count))
+
+fun StringBuilder.skipLine(): StringBuilder = breakLine(count = 2)
\ No newline at end of file
diff --git a/domain/legacy/src/main/java/com/tangem/domain/common/extensions/Blockchain.kt b/domain/legacy/src/main/java/com/tangem/domain/common/extensions/Blockchain.kt
index ce72fcde55..36caa85c7f 100644
--- a/domain/legacy/src/main/java/com/tangem/domain/common/extensions/Blockchain.kt
+++ b/domain/legacy/src/main/java/com/tangem/domain/common/extensions/Blockchain.kt
@@ -93,6 +93,27 @@ fun Blockchain.Companion.fromNetworkId(networkId: String): Blockchain? {
"areon-network/test" -> Blockchain.AreonTestnet
"pulsechain" -> Blockchain.PulseChain
"pulsechain/test" -> Blockchain.PulseChainTestnet
+ "zksync" -> Blockchain.ZkSyncEra
+ "zksync/test" -> Blockchain.ZkSyncEraTestnet
+ "moonbeam" -> Blockchain.Moonbeam
+ "moonbeam/test" -> Blockchain.MoonbeamTestnet
+ "manta-network" -> Blockchain.Manta
+ "manta-network/test" -> Blockchain.MantaTestnet
+ "polygon-zkevm" -> Blockchain.PolygonZkEVM
+ "polygon-zkevm/test" -> Blockchain.PolygonZkEVMTestnet
+ "nexa" -> Blockchain.Nexa // FIXME
+ "nexa/test" -> Blockchain.NexaTestnet // FIXME
+ "radiant" -> Blockchain.Radiant
+ "moonriver" -> Blockchain.Moonriver
+ "moonriver/test" -> Blockchain.MoonriverTestnet
+ "mantle" -> Blockchain.Mantle
+ "mantle/test" -> Blockchain.MantleTestnet
+ "flare-network" -> Blockchain.Flare
+ "flare-network/test" -> Blockchain.FlareTestnet
+ "taraxa" -> Blockchain.Taraxa
+ "taraxa/test" -> Blockchain.TaraxaTestnet
+ "base" -> Blockchain.Base
+ "base/test" -> Blockchain.BaseTestnet
else -> null
}
}
@@ -187,6 +208,27 @@ fun Blockchain.toNetworkId(): String {
Blockchain.AreonTestnet -> "areon-network/test"
Blockchain.PulseChain -> "pulsechain"
Blockchain.PulseChainTestnet -> "pulsechain/test"
+ Blockchain.ZkSyncEra -> "zksync"
+ Blockchain.ZkSyncEraTestnet -> "zksync/test"
+ Blockchain.Moonbeam -> "moonbeam"
+ Blockchain.MoonbeamTestnet -> "moonbeam/test"
+ Blockchain.Manta -> "manta-network"
+ Blockchain.MantaTestnet -> "manta-network/test"
+ Blockchain.PolygonZkEVM -> "polygon-zkevm"
+ Blockchain.PolygonZkEVMTestnet -> "polygon-zkevm/test"
+ Blockchain.Nexa -> "nexa" // FIXME
+ Blockchain.NexaTestnet -> "nexa/test" // FIXME
+ Blockchain.Radiant -> "radiant"
+ Blockchain.Moonriver -> "moonriver"
+ Blockchain.MoonriverTestnet -> "moonriver/test"
+ Blockchain.Mantle -> "mantle"
+ Blockchain.MantleTestnet -> "mantle/test"
+ Blockchain.Flare -> "flare-network"
+ Blockchain.FlareTestnet -> "flare-network/test"
+ Blockchain.Taraxa -> "taraxa"
+ Blockchain.TaraxaTestnet -> "taraxa/test"
+ Blockchain.Base -> "base"
+ Blockchain.BaseTestnet -> "base/test"
}
}
@@ -249,6 +291,17 @@ fun Blockchain.toCoinId(): String {
Blockchain.Aurora, Blockchain.AuroraTestnet -> "aurora-ethereum"
Blockchain.Areon, Blockchain.AreonTestnet -> "areon-network"
Blockchain.PulseChain, Blockchain.PulseChainTestnet -> "pulsechain"
+ Blockchain.ZkSyncEra, Blockchain.ZkSyncEraTestnet -> "zksync-ethereum"
+ Blockchain.Moonbeam, Blockchain.MoonbeamTestnet -> "moonbeam"
+ Blockchain.Manta, Blockchain.MantaTestnet -> "manta-network-ethereum"
+ Blockchain.PolygonZkEVM, Blockchain.PolygonZkEVMTestnet -> "polygon-zkevm-ethereum"
+ Blockchain.Nexa, Blockchain.NexaTestnet -> "nexa" // FIXME
+ Blockchain.Radiant -> "radiant"
+ Blockchain.Moonriver, Blockchain.MoonriverTestnet -> "moonriver"
+ Blockchain.Mantle, Blockchain.MantleTestnet -> "mantle"
+ Blockchain.Flare, Blockchain.FlareTestnet -> "flare-networks"
+ Blockchain.Taraxa, Blockchain.TaraxaTestnet -> "taraxa"
+ Blockchain.Base, Blockchain.BaseTestnet -> "base-ethereum"
}
}
@@ -275,5 +328,7 @@ private const val NODL_AMOUNT_TO_CREATE_ACCOUNT = 1.5
private val excludedBlockchains = listOf(
Blockchain.Unknown,
- Blockchain.Playa3ull,
+ Blockchain.Nexa,
+ Blockchain.NexaTestnet,
+ Blockchain.Radiant,
)
\ No newline at end of file
diff --git a/domain/legacy/src/main/java/com/tangem/domain/userwallets/UserWalletBuilder.kt b/domain/legacy/src/main/java/com/tangem/domain/userwallets/UserWalletBuilder.kt
index 182653ef00..325fb2efa7 100644
--- a/domain/legacy/src/main/java/com/tangem/domain/userwallets/UserWalletBuilder.kt
+++ b/domain/legacy/src/main/java/com/tangem/domain/userwallets/UserWalletBuilder.kt
@@ -11,6 +11,7 @@ class UserWalletBuilder(
private val getCardImageUseCase: GetCardImageUseCase = GetCardImageUseCase(),
) {
private var backupCardsIds: Set = emptySet()
+ private var hasBackupError: Boolean = false
private val CardDTO.isBackupNotAllowed: Boolean
get() = !this.settings.isBackupAllowed
@@ -41,6 +42,13 @@ class UserWalletBuilder(
}
}
+ /**
+ * Sets if UserWallet has any backup errors (wrong curves etc). Use in onboarding
+ */
+ fun hasBackupError(hasBackupError: Boolean) = this.apply {
+ this.hasBackupError = hasBackupError
+ }
+
suspend fun build(): UserWallet? {
return with(scanResponse) {
UserWalletIdBuilder.scanResponse(scanResponse)
@@ -53,6 +61,7 @@ class UserWalletBuilder(
cardsInWallet = backupCardsIds.plus(card.cardId),
scanResponse = this,
isMultiCurrency = cardTypesResolver.isMultiwalletAllowed(),
+ hasBackupError = hasBackupError,
)
}
}
diff --git a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/DefaultWalletManagersFacade.kt b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/DefaultWalletManagersFacade.kt
index bb0cc5a3ad..276758748b 100644
--- a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/DefaultWalletManagersFacade.kt
+++ b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/DefaultWalletManagersFacade.kt
@@ -11,6 +11,7 @@ import com.tangem.blockchain.common.address.Address
import com.tangem.blockchain.common.address.AddressType
import com.tangem.blockchain.common.address.EstimationFeeAddressFactory
import com.tangem.blockchain.common.datastorage.BlockchainDataStorage
+import com.tangem.blockchain.common.logging.BlockchainSDKLogger
import com.tangem.blockchain.common.pagination.Page
import com.tangem.blockchain.common.transaction.Fee
import com.tangem.blockchain.common.transaction.TransactionFee
@@ -25,6 +26,7 @@ import com.tangem.datasource.local.userwallet.UserWalletsStore
import com.tangem.datasource.local.walletmanager.WalletManagersStore
import com.tangem.domain.common.util.hasDerivation
import com.tangem.domain.demo.DemoConfig
+import com.tangem.domain.feedback.FeedbackManagerFeatureToggles
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.Network
import com.tangem.domain.tokens.model.warnings.CryptoCurrencyWarning
@@ -53,15 +55,18 @@ class DefaultWalletManagersFacade(
configManager: ConfigManager,
blockchainDataStorage: BlockchainDataStorage,
accountCreator: AccountCreator,
+ blockchainSDKLogger: BlockchainSDKLogger,
+ feedbackManagerFeatureToggles: FeedbackManagerFeatureToggles,
) : WalletManagersFacade {
private val demoConfig by lazy { DemoConfig() }
private val resultFactory by lazy { UpdateWalletManagerResultFactory() }
private val walletManagerFactory by lazy {
WalletManagerFactory(
- configManager,
- accountCreator,
- blockchainDataStorage,
+ configManager = configManager,
+ accountCreator = accountCreator,
+ blockchainDataStorage = blockchainDataStorage,
+ blockchainSDKLogger = if (feedbackManagerFeatureToggles.isLocalLogsEnabled) blockchainSDKLogger else null,
)
}
private val sdkTokenConverter by lazy { SdkTokenConverter() }
@@ -515,27 +520,28 @@ class DefaultWalletManagersFacade(
override suspend fun getRecentTransactions(
userWalletId: UserWalletId,
- blockchain: Blockchain,
- derivationPath: String?,
+ currency: CryptoCurrency,
): List {
- val walletManager = getOrCreateWalletManager(
- userWalletId = userWalletId,
- blockchain = blockchain,
- derivationPath = derivationPath,
- )
- val feePaidCurrency = blockchain.feePaidCurrency()
+ val walletManager = getOrCreateWalletManager(userWalletId = userWalletId, network = currency.network)
if (walletManager == null) {
- Timber.e("Unable to get a wallet manager for blockchain: $blockchain")
+ Timber.e("Unable to get a wallet manager for blockchain: ${currency.network.id}")
return emptyList()
}
val transactionDataConverter = TransactionDataToTxHistoryItemConverter(
walletAddresses = SdkAddressToAddressConverter.convertList(walletManager.wallet.addresses).toSet(),
- feePaidCurrency = feePaidCurrency,
+ feePaidCurrency = walletManager.wallet.blockchain.feePaidCurrency(),
)
- return walletManager.wallet.recentTransactions.mapNotNull(transactionDataConverter::convert)
+ return walletManager.wallet.recentTransactions
+ .filter { transaction ->
+ when (currency) {
+ is CryptoCurrency.Coin -> transaction.amount.type is AmountType.Coin
+ is CryptoCurrency.Token -> transaction.contractAddress == currency.contractAddress
+ }
+ }
+ .mapNotNull(transactionDataConverter::convert)
}
override suspend fun tokenBalance(
diff --git a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/WalletManagersFacade.kt b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/WalletManagersFacade.kt
index 543ef9006e..4636317765 100644
--- a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/WalletManagersFacade.kt
+++ b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/WalletManagersFacade.kt
@@ -227,12 +227,8 @@ interface WalletManagersFacade {
network: Network,
): SimpleResult
- /** Get recent transactions of [userWalletId] for [blockchain] with [derivationPath] */
- suspend fun getRecentTransactions(
- userWalletId: UserWalletId,
- blockchain: Blockchain,
- derivationPath: String?,
- ): List
+ /** Get recent transactions of [userWalletId] for [currency] */
+ suspend fun getRecentTransactions(userWalletId: UserWalletId, currency: CryptoCurrency): List
@Suppress("LongParameterList")
suspend fun tokenBalance(
diff --git a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/utils/WalletManagerFactory.kt b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/utils/WalletManagerFactory.kt
index e0377cf6a3..89a63b05f6 100644
--- a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/utils/WalletManagerFactory.kt
+++ b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/utils/WalletManagerFactory.kt
@@ -5,6 +5,7 @@ import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.DerivationParams
import com.tangem.blockchain.common.WalletManager
import com.tangem.blockchain.common.datastorage.BlockchainDataStorage
+import com.tangem.blockchain.common.logging.BlockchainSDKLogger
import com.tangem.crypto.hdWallet.DerivationPath
import com.tangem.datasource.config.ConfigManager
import com.tangem.domain.common.DerivationStyleProvider
@@ -18,10 +19,16 @@ internal class WalletManagerFactory(
configManager: ConfigManager,
accountCreator: AccountCreator,
blockchainDataStorage: BlockchainDataStorage,
+ blockchainSDKLogger: BlockchainSDKLogger? = null,
) {
private val sdkWalletManagerFactory by lazy {
- BlockchainWalletManagerFactory(configManager.config.blockchainSdkConfig, accountCreator, blockchainDataStorage)
+ BlockchainWalletManagerFactory(
+ config = configManager.config.blockchainSdkConfig,
+ accountCreator = accountCreator,
+ blockchainDataStorage = blockchainDataStorage,
+ loggers = listOfNotNull(blockchainSDKLogger),
+ )
}
fun createWalletManager(
diff --git a/domain/models/src/main/kotlin/com/tangem/domain/models/scan/CardDTO.kt b/domain/models/src/main/kotlin/com/tangem/domain/models/scan/CardDTO.kt
index 8863fe8a03..ff2b5be1d0 100644
--- a/domain/models/src/main/kotlin/com/tangem/domain/models/scan/CardDTO.kt
+++ b/domain/models/src/main/kotlin/com/tangem/domain/models/scan/CardDTO.kt
@@ -303,7 +303,7 @@ data class CardDTO(
data class Active(val cardCount: Int) : BackupStatus()
- object NoBackup : BackupStatus()
+ data object NoBackup : BackupStatus()
val isActive: Boolean
get() = this is Active
diff --git a/domain/qr-scanning/.gitignore b/domain/qr-scanning/.gitignore
new file mode 100644
index 0000000000..42afabfd2a
--- /dev/null
+++ b/domain/qr-scanning/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/domain/qr-scanning/build.gradle.kts b/domain/qr-scanning/build.gradle.kts
new file mode 100644
index 0000000000..f1ea5e63ec
--- /dev/null
+++ b/domain/qr-scanning/build.gradle.kts
@@ -0,0 +1,19 @@
+plugins {
+ alias(deps.plugins.android.library)
+ alias(deps.plugins.kotlin.android)
+ id("configuration")
+}
+
+android {
+ namespace = "com.tangem.domain.qrscanning"
+}
+
+dependencies {
+
+ /** Domain */
+ implementation(projects.domain.qrScanning.models)
+ implementation(projects.domain.tokens.models)
+
+ implementation(deps.kotlin.coroutines)
+ implementation(deps.arrow.core)
+}
\ No newline at end of file
diff --git a/domain/qr-scanning/models/.gitignore b/domain/qr-scanning/models/.gitignore
new file mode 100644
index 0000000000..42afabfd2a
--- /dev/null
+++ b/domain/qr-scanning/models/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/domain/qr-scanning/models/build.gradle.kts b/domain/qr-scanning/models/build.gradle.kts
new file mode 100644
index 0000000000..7ff7fb7522
--- /dev/null
+++ b/domain/qr-scanning/models/build.gradle.kts
@@ -0,0 +1,4 @@
+plugins {
+ alias(deps.plugins.kotlin.jvm)
+ id("configuration")
+}
\ No newline at end of file
diff --git a/domain/qr-scanning/models/src/main/java/com/tangem/domain/qrscanning/models/QrResult.kt b/domain/qr-scanning/models/src/main/java/com/tangem/domain/qrscanning/models/QrResult.kt
new file mode 100644
index 0000000000..a97a4a7161
--- /dev/null
+++ b/domain/qr-scanning/models/src/main/java/com/tangem/domain/qrscanning/models/QrResult.kt
@@ -0,0 +1,9 @@
+package com.tangem.domain.qrscanning.models
+
+import java.math.BigDecimal
+
+data class QrResult(
+ var address: String = "",
+ var amount: BigDecimal? = null,
+ var memo: String? = null,
+)
\ No newline at end of file
diff --git a/features/qr-scanning/api/src/main/java/com/tangem/feature/qrscanning/SourceType.kt b/domain/qr-scanning/models/src/main/java/com/tangem/domain/qrscanning/models/SourceType.kt
similarity index 56%
rename from features/qr-scanning/api/src/main/java/com/tangem/feature/qrscanning/SourceType.kt
rename to domain/qr-scanning/models/src/main/java/com/tangem/domain/qrscanning/models/SourceType.kt
index 96640d74e6..87be83fd0c 100644
--- a/features/qr-scanning/api/src/main/java/com/tangem/feature/qrscanning/SourceType.kt
+++ b/domain/qr-scanning/models/src/main/java/com/tangem/domain/qrscanning/models/SourceType.kt
@@ -1,4 +1,4 @@
-package com.tangem.feature.qrscanning
+package com.tangem.domain.qrscanning.models
enum class SourceType {
WALLET_CONNECT,
diff --git a/domain/qr-scanning/src/main/java/com/tangem/domain/qrscanning/repository/QrScanningEventsRepository.kt b/domain/qr-scanning/src/main/java/com/tangem/domain/qrscanning/repository/QrScanningEventsRepository.kt
new file mode 100644
index 0000000000..15ca99b951
--- /dev/null
+++ b/domain/qr-scanning/src/main/java/com/tangem/domain/qrscanning/repository/QrScanningEventsRepository.kt
@@ -0,0 +1,15 @@
+package com.tangem.domain.qrscanning.repository
+
+import com.tangem.domain.qrscanning.models.QrResult
+import com.tangem.domain.qrscanning.models.SourceType
+import com.tangem.domain.tokens.model.CryptoCurrency
+import kotlinx.coroutines.flow.Flow
+
+interface QrScanningEventsRepository {
+
+ suspend fun emitResult(type: SourceType, qrCode: String)
+
+ fun subscribeToScanningResults(type: SourceType): Flow
+
+ fun parseQrCode(qrCode: String, cryptoCurrency: CryptoCurrency): QrResult
+}
\ No newline at end of file
diff --git a/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/usecase/EmitQrScannedEventUseCase.kt b/domain/qr-scanning/src/main/java/com/tangem/domain/qrscanning/usecases/EmitQrScannedEventUseCase.kt
similarity index 65%
rename from features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/usecase/EmitQrScannedEventUseCase.kt
rename to domain/qr-scanning/src/main/java/com/tangem/domain/qrscanning/usecases/EmitQrScannedEventUseCase.kt
index cc7f7bc6c2..ebe0e5980f 100644
--- a/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/usecase/EmitQrScannedEventUseCase.kt
+++ b/domain/qr-scanning/src/main/java/com/tangem/domain/qrscanning/usecases/EmitQrScannedEventUseCase.kt
@@ -1,12 +1,12 @@
-package com.tangem.feature.qrscanning.usecase
+package com.tangem.domain.qrscanning.usecases
import arrow.core.Either
import arrow.core.left
import arrow.core.right
-import com.tangem.feature.qrscanning.SourceType
-import com.tangem.feature.qrscanning.repo.QrScanningEventsRepository
+import com.tangem.domain.qrscanning.models.SourceType
+import com.tangem.domain.qrscanning.repository.QrScanningEventsRepository
-internal class EmitQrScannedEventUseCase(
+class EmitQrScannedEventUseCase(
private val repository: QrScanningEventsRepository,
) {
suspend operator fun invoke(type: SourceType, qrCode: String): Either {
diff --git a/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/usecase/ListenToQrScanningUseCase.kt b/domain/qr-scanning/src/main/java/com/tangem/domain/qrscanning/usecases/ListenToQrScanningUseCase.kt
similarity index 69%
rename from features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/usecase/ListenToQrScanningUseCase.kt
rename to domain/qr-scanning/src/main/java/com/tangem/domain/qrscanning/usecases/ListenToQrScanningUseCase.kt
index e3ef9f9dc1..3c42400420 100644
--- a/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/usecase/ListenToQrScanningUseCase.kt
+++ b/domain/qr-scanning/src/main/java/com/tangem/domain/qrscanning/usecases/ListenToQrScanningUseCase.kt
@@ -1,12 +1,11 @@
-package com.tangem.feature.qrscanning.usecase
+package com.tangem.domain.qrscanning.usecases
import arrow.core.Either
import arrow.core.left
import arrow.core.right
-import com.tangem.feature.qrscanning.SourceType
-import com.tangem.feature.qrscanning.repo.QrScanningEventsRepository
+import com.tangem.domain.qrscanning.models.SourceType
+import com.tangem.domain.qrscanning.repository.QrScanningEventsRepository
import kotlinx.coroutines.flow.Flow
-import java.lang.Exception
class ListenToQrScanningUseCase(
val repository: QrScanningEventsRepository,
diff --git a/domain/qr-scanning/src/main/java/com/tangem/domain/qrscanning/usecases/ParseQrCodeUseCase.kt b/domain/qr-scanning/src/main/java/com/tangem/domain/qrscanning/usecases/ParseQrCodeUseCase.kt
new file mode 100644
index 0000000000..0849c8bf1c
--- /dev/null
+++ b/domain/qr-scanning/src/main/java/com/tangem/domain/qrscanning/usecases/ParseQrCodeUseCase.kt
@@ -0,0 +1,20 @@
+package com.tangem.domain.qrscanning.usecases
+
+import arrow.core.Either
+import arrow.core.left
+import arrow.core.right
+import com.tangem.domain.qrscanning.models.QrResult
+import com.tangem.domain.qrscanning.repository.QrScanningEventsRepository
+import com.tangem.domain.tokens.model.CryptoCurrency
+
+class ParseQrCodeUseCase(
+ val repository: QrScanningEventsRepository,
+) {
+ operator fun invoke(qrCode: String, cryptoCurrency: CryptoCurrency): Either {
+ return try {
+ repository.parseQrCode(qrCode, cryptoCurrency).right()
+ } catch (e: Exception) {
+ e.left()
+ }
+ }
+}
\ No newline at end of file
diff --git a/domain/settings/build.gradle.kts b/domain/settings/build.gradle.kts
index 00cd0a439e..0c8b0de6e1 100644
--- a/domain/settings/build.gradle.kts
+++ b/domain/settings/build.gradle.kts
@@ -5,5 +5,6 @@ plugins {
dependencies {
implementation(deps.kotlin.coroutines)
+ implementation(deps.arrow.core)
implementation(projects.domain.balanceHiding.models)
}
\ No newline at end of file
diff --git a/domain/settings/src/main/java/com/tangem/domain/settings/DeleteDeprecatedLogsUseCase.kt b/domain/settings/src/main/java/com/tangem/domain/settings/DeleteDeprecatedLogsUseCase.kt
new file mode 100644
index 0000000000..627d3f1124
--- /dev/null
+++ b/domain/settings/src/main/java/com/tangem/domain/settings/DeleteDeprecatedLogsUseCase.kt
@@ -0,0 +1,16 @@
+package com.tangem.domain.settings
+
+import com.tangem.domain.settings.repositories.SettingsRepository
+
+class DeleteDeprecatedLogsUseCase(
+ private val settingsRepository: SettingsRepository,
+) {
+
+ suspend operator fun invoke() {
+ settingsRepository.deleteDeprecatedLogs(maxSize = MAX_LOGS_SIZE)
+ }
+
+ private companion object {
+ const val MAX_LOGS_SIZE = 24_900_000 // ≈ 25 MB
+ }
+}
\ No newline at end of file
diff --git a/domain/settings/src/main/java/com/tangem/domain/settings/IsSendTapHelpEnabledUseCase.kt b/domain/settings/src/main/java/com/tangem/domain/settings/IsSendTapHelpEnabledUseCase.kt
new file mode 100644
index 0000000000..cb59249538
--- /dev/null
+++ b/domain/settings/src/main/java/com/tangem/domain/settings/IsSendTapHelpEnabledUseCase.kt
@@ -0,0 +1,14 @@
+package com.tangem.domain.settings
+
+import arrow.core.Either
+import com.tangem.domain.settings.repositories.SettingsRepository
+
+/**
+ * Checks if tap help is enabled
+ *
+ * @property settingsRepository settings repository
+ */
+class IsSendTapHelpEnabledUseCase(private val settingsRepository: SettingsRepository) {
+
+ suspend operator fun invoke() = Either.catch { settingsRepository.isSendTapHelpPreviewEnabled() }
+}
\ No newline at end of file
diff --git a/domain/settings/src/main/java/com/tangem/domain/settings/NeverShowTapHelpUseCase.kt b/domain/settings/src/main/java/com/tangem/domain/settings/NeverShowTapHelpUseCase.kt
new file mode 100644
index 0000000000..603c9d7402
--- /dev/null
+++ b/domain/settings/src/main/java/com/tangem/domain/settings/NeverShowTapHelpUseCase.kt
@@ -0,0 +1,16 @@
+package com.tangem.domain.settings
+
+import arrow.core.Either
+import com.tangem.domain.settings.repositories.SettingsRepository
+
+/**
+ * Never to show tap help
+ *
+ * @property settingsRepository settings repository
+ */
+class NeverShowTapHelpUseCase(private val settingsRepository: SettingsRepository) {
+
+ suspend operator fun invoke() = Either.catch {
+ settingsRepository.setSendTapHelpPreviewAvailability(isEnabled = false)
+ }
+}
\ No newline at end of file
diff --git a/domain/settings/src/main/java/com/tangem/domain/settings/models/AppLogsModel.kt b/domain/settings/src/main/java/com/tangem/domain/settings/models/AppLogsModel.kt
deleted file mode 100644
index 746974930f..0000000000
--- a/domain/settings/src/main/java/com/tangem/domain/settings/models/AppLogsModel.kt
+++ /dev/null
@@ -1,3 +0,0 @@
-package com.tangem.domain.settings.models
-
-data class AppLogsModel(val timestamp: Long, val message: String)
\ No newline at end of file
diff --git a/domain/settings/src/main/java/com/tangem/domain/settings/repositories/SettingsRepository.kt b/domain/settings/src/main/java/com/tangem/domain/settings/repositories/SettingsRepository.kt
index f686561f3e..63a3ab3247 100644
--- a/domain/settings/src/main/java/com/tangem/domain/settings/repositories/SettingsRepository.kt
+++ b/domain/settings/src/main/java/com/tangem/domain/settings/repositories/SettingsRepository.kt
@@ -1,7 +1,5 @@
package com.tangem.domain.settings.repositories
-import com.tangem.domain.settings.models.AppLogsModel
-
interface SettingsRepository {
suspend fun shouldShowSaveUserWalletScreen(): Boolean
@@ -10,12 +8,13 @@ interface SettingsRepository {
suspend fun setWalletScrollPreviewAvailability(isEnabled: Boolean)
- @Throws
- suspend fun getAppLogs(): List
-
@Throws
suspend fun updateAppLogs(message: String)
@Throws
- suspend fun deleteDeprecatedLogs()
+ suspend fun deleteDeprecatedLogs(maxSize: Int)
+
+ suspend fun isSendTapHelpPreviewEnabled(): Boolean
+
+ suspend fun setSendTapHelpPreviewAvailability(isEnabled: Boolean)
}
\ No newline at end of file
diff --git a/domain/tokens/build.gradle.kts b/domain/tokens/build.gradle.kts
index e32512bb66..5d91ab4124 100644
--- a/domain/tokens/build.gradle.kts
+++ b/domain/tokens/build.gradle.kts
@@ -21,6 +21,7 @@ dependencies {
implementation(projects.domain.settings)
implementation(projects.features.swap.domain.api)
implementation(projects.features.swap.domain.models)
+ implementation(projects.features.send.api)
/** Project - Other */
implementation(projects.core.utils)
diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyActionsUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyActionsUseCase.kt
index bbbc8c7071..07cdfdabdc 100644
--- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyActionsUseCase.kt
+++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyActionsUseCase.kt
@@ -10,6 +10,7 @@ import com.tangem.domain.tokens.repository.NetworksRepository
import com.tangem.domain.tokens.repository.QuotesRepository
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.models.UserWalletId
+import com.tangem.features.send.api.featuretoggles.SendFeatureToggles
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.isNullOrZero
import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -21,12 +22,14 @@ import java.math.BigDecimal
*
* @property rampManager Ramp manager to check ramp availability
*/
+@Suppress("LongParameterList")
class GetCryptoCurrencyActionsUseCase(
private val rampManager: RampStateManager,
private val marketCryptoCurrencyRepository: MarketCryptoCurrencyRepository,
private val currenciesRepository: CurrenciesRepository,
private val quotesRepository: QuotesRepository,
private val networksRepository: NetworksRepository,
+ private val sendFeatureToggles: SendFeatureToggles,
private val dispatchers: CoroutineDispatcherProvider,
) {
@@ -198,12 +201,16 @@ class GetCryptoCurrencyActionsUseCase(
tokenStatus: CryptoCurrencyStatus,
coinStatus: CryptoCurrencyStatus?,
): Boolean {
- return when (feePaidCurrency) {
- FeePaidCurrency.Coin -> !tokenStatus.value.amount.isZero() && coinStatus?.value?.amount.isZero()
- FeePaidCurrency.SameCurrency -> tokenStatus.value.amount.isZero()
- is FeePaidCurrency.Token -> {
- val feePaidTokenBalance = feePaidCurrency.balance
- !tokenStatus.value.amount.isZero() && feePaidTokenBalance.isZero()
+ return if (sendFeatureToggles.isRedesignedSendEnabled) {
+ tokenStatus.value.amount.isZero()
+ } else {
+ when (feePaidCurrency) {
+ FeePaidCurrency.Coin -> !tokenStatus.value.amount.isZero() && coinStatus?.value?.amount.isZero()
+ FeePaidCurrency.SameCurrency -> tokenStatus.value.amount.isZero()
+ is FeePaidCurrency.Token -> {
+ val feePaidTokenBalance = feePaidCurrency.balance
+ !tokenStatus.value.amount.isZero() && feePaidTokenBalance.isZero()
+ }
}
}
}
diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCurrencyWarningsUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCurrencyWarningsUseCase.kt
index 6e18152f25..eded0146fc 100644
--- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCurrencyWarningsUseCase.kt
+++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCurrencyWarningsUseCase.kt
@@ -197,7 +197,7 @@ class GetCurrencyWarningsUseCase(
coinCurrency = coinStatus.currency,
)
}
- feePaidCurrency is FeePaidCurrency.SameCurrency && !tokenStatus.value.amount.isZero() -> {
+ feePaidCurrency is FeePaidCurrency.SameCurrency && tokenStatus.value.amount.isZero() -> {
CryptoCurrencyWarning.BalanceNotEnoughForFee(
tokenCurrency = tokenStatus.currency,
coinCurrency = coinStatus.currency,
diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/RequiresHardenedDerivationOnlyUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/RequiresHardenedDerivationOnlyUseCase.kt
new file mode 100644
index 0000000000..db8dad152a
--- /dev/null
+++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/RequiresHardenedDerivationOnlyUseCase.kt
@@ -0,0 +1,26 @@
+package com.tangem.domain.tokens
+
+import arrow.core.Either
+import arrow.core.raise.catch
+import arrow.core.raise.either
+import com.tangem.domain.tokens.repository.NetworksCompatibilityRepository
+import com.tangem.domain.wallets.models.UserWalletId
+
+/**
+ * Ed25519Slip0010 requires only hardened derivations on Wallet 2
+ */
+class RequiresHardenedDerivationOnlyUseCase(
+ private val repository: NetworksCompatibilityRepository,
+) {
+
+ suspend operator fun invoke(networkId: String, userWalletId: UserWalletId): Either {
+ return either {
+ catch(
+ block = {
+ repository.requiresHardenedDerivationOnly(networkId, userWalletId)
+ },
+ catch = { throwable -> raise(throwable) },
+ )
+ }
+ }
+}
\ No newline at end of file
diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/legacy/TradeCryptoAction.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/legacy/TradeCryptoAction.kt
index 1657a3ea05..092ad6d3ba 100644
--- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/legacy/TradeCryptoAction.kt
+++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/legacy/TradeCryptoAction.kt
@@ -41,8 +41,9 @@ sealed class TradeCryptoAction : Action {
data class Swap(val cryptoCurrency: CryptoCurrency) : TradeCryptoAction()
data class TransactionInfo(
- val amount: String,
- val destinationAddress: String,
val transactionId: String,
+ val destinationAddress: String,
+ val amount: String,
+ val tag: String? = null,
)
}
\ No newline at end of file
diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/NetworksCompatibilityRepository.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/NetworksCompatibilityRepository.kt
index 1d0f6946b1..a5e2083e05 100644
--- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/NetworksCompatibilityRepository.kt
+++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/NetworksCompatibilityRepository.kt
@@ -17,5 +17,7 @@ interface NetworksCompatibilityRepository {
@Throws(IllegalArgumentException::class)
suspend fun getSupportedNetworks(userWalletId: UserWalletId): List
+ suspend fun requiresHardenedDerivationOnly(networkId: String, userWalletId: UserWalletId): Boolean
+
fun areTokensSupportedByNetwork(networkId: String): Boolean
}
\ No newline at end of file
diff --git a/domain/transaction/src/main/java/com/tangem/domain/transaction/error/SendTransactionError.kt b/domain/transaction/src/main/java/com/tangem/domain/transaction/error/SendTransactionError.kt
index 753f45e4b4..22b7991290 100644
--- a/domain/transaction/src/main/java/com/tangem/domain/transaction/error/SendTransactionError.kt
+++ b/domain/transaction/src/main/java/com/tangem/domain/transaction/error/SendTransactionError.kt
@@ -8,7 +8,7 @@ sealed class SendTransactionError {
data class DataError(val message: String?) : SendTransactionError()
- data class NetworkError(val message: String?) : SendTransactionError()
+ data class NetworkError(val message: String?, val code: String?) : SendTransactionError()
data class BlockchainSdkError(val code: Int, val message: String?) : SendTransactionError()
diff --git a/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/SendTransactionUseCase.kt b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/SendTransactionUseCase.kt
index 059a672c70..d2c9cbd085 100644
--- a/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/SendTransactionUseCase.kt
+++ b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/SendTransactionUseCase.kt
@@ -67,7 +67,12 @@ class SendTransactionUseCase(
}
private fun handleError(result: SimpleResult.Failure): SendTransactionError {
- if (ResultChecker.isNetworkError(result)) return SendTransactionError.NetworkError(result.error.message)
+ if (ResultChecker.isNetworkError(result)) {
+ return SendTransactionError.NetworkError(
+ code = result.error.message,
+ message = result.error.customMessage,
+ )
+ }
val error = result.error as? BlockchainSdkError ?: return SendTransactionError.UnknownError()
return when (error) {
is BlockchainSdkError.WrappedTangemError -> {
diff --git a/domain/txhistory/src/main/kotlin/com/tangem/domain/txhistory/repository/TxHistoryRepository.kt b/domain/txhistory/src/main/kotlin/com/tangem/domain/txhistory/repository/TxHistoryRepository.kt
index b010a54db4..652b15c85b 100644
--- a/domain/txhistory/src/main/kotlin/com/tangem/domain/txhistory/repository/TxHistoryRepository.kt
+++ b/domain/txhistory/src/main/kotlin/com/tangem/domain/txhistory/repository/TxHistoryRepository.kt
@@ -24,6 +24,9 @@ interface TxHistoryRepository {
fun getTxExploreUrl(txHash: String, networkId: Network.ID): String
+ /** Get transaction url in explorer via last transaction from wallet's recentTransactions list */
+ suspend fun getTxExploreUrl(userWalletId: UserWalletId, network: Network): String
+
@Throws(TxHistoryListError::class)
suspend fun getFixedSizeTxHistoryItems(
userWalletId: UserWalletId,
diff --git a/domain/txhistory/src/main/kotlin/com/tangem/domain/txhistory/usecase/GetExplorerTransactionUrlUseCase.kt b/domain/txhistory/src/main/kotlin/com/tangem/domain/txhistory/usecase/GetExplorerTransactionUrlUseCase.kt
index 974d439c7e..cedb309901 100644
--- a/domain/txhistory/src/main/kotlin/com/tangem/domain/txhistory/usecase/GetExplorerTransactionUrlUseCase.kt
+++ b/domain/txhistory/src/main/kotlin/com/tangem/domain/txhistory/usecase/GetExplorerTransactionUrlUseCase.kt
@@ -6,10 +6,12 @@ import arrow.core.raise.either
import com.tangem.domain.tokens.model.Network
import com.tangem.domain.txhistory.models.TxStatusError
import com.tangem.domain.txhistory.repository.TxHistoryRepository
+import com.tangem.domain.wallets.models.UserWalletId
class GetExplorerTransactionUrlUseCase(
private val repository: TxHistoryRepository,
) {
+ @Deprecated("Replace with invoke [UserWalletId, Network]")
operator fun invoke(txHash: String, networkId: Network.ID): Either {
return either {
catch(
@@ -22,4 +24,17 @@ class GetExplorerTransactionUrlUseCase(
)
}
}
+
+ suspend operator fun invoke(userWalletId: UserWalletId, network: Network): Either {
+ return either {
+ catch(
+ block = {
+ repository.getTxExploreUrl(userWalletId, network).ifEmpty {
+ raise(TxStatusError.EmptyUrlError)
+ }
+ },
+ catch = { raise(TxStatusError.DataError(it)) },
+ )
+ }
+ }
}
\ No newline at end of file
diff --git a/domain/txhistory/src/main/kotlin/com/tangem/domain/txhistory/usecase/GetFixedTxHistoryItemsUseCase.kt b/domain/txhistory/src/main/kotlin/com/tangem/domain/txhistory/usecase/GetFixedTxHistoryItemsUseCase.kt
index aa93bec246..8b5a463618 100644
--- a/domain/txhistory/src/main/kotlin/com/tangem/domain/txhistory/usecase/GetFixedTxHistoryItemsUseCase.kt
+++ b/domain/txhistory/src/main/kotlin/com/tangem/domain/txhistory/usecase/GetFixedTxHistoryItemsUseCase.kt
@@ -33,4 +33,15 @@ class GetFixedTxHistoryItemsUseCase(
}
}.mapLeft { TxHistoryListError.DataError(it) }
}
+
+ suspend fun getSync(
+ userWalletId: UserWalletId,
+ currency: CryptoCurrency,
+ pageSize: Int = DEFAULT_PAGE_SIZE,
+ refresh: Boolean = false,
+ ): Either> {
+ return Either.catch {
+ repository.getFixedSizeTxHistoryItems(userWalletId, currency, pageSize, refresh)
+ }.mapLeft { TxHistoryListError.DataError(it) }
+ }
}
\ No newline at end of file
diff --git a/domain/wallets/models/src/main/java/com/tangem/domain/wallets/models/UserWallet.kt b/domain/wallets/models/src/main/java/com/tangem/domain/wallets/models/UserWallet.kt
index 710566e799..4904076698 100644
--- a/domain/wallets/models/src/main/java/com/tangem/domain/wallets/models/UserWallet.kt
+++ b/domain/wallets/models/src/main/java/com/tangem/domain/wallets/models/UserWallet.kt
@@ -20,6 +20,7 @@ data class UserWallet(
val artworkUrl: String,
val cardsInWallet: Set,
val isMultiCurrency: Boolean,
+ val hasBackupError: Boolean,
val scanResponse: ScanResponse, // TODO: Replace with [com.tangem.domain.models.scan.CardDTO]
) {
diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/state/factory/AddCustomTokenStateFactory.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/state/factory/AddCustomTokenStateFactory.kt
index 7fe3b1dc8c..6ad1ac8e68 100644
--- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/state/factory/AddCustomTokenStateFactory.kt
+++ b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/state/factory/AddCustomTokenStateFactory.kt
@@ -2,6 +2,7 @@ package com.tangem.managetokens.presentation.addcustomtoken.state.factory
import com.tangem.core.ui.event.consumedEvent
import com.tangem.core.ui.event.triggeredEvent
+import com.tangem.crypto.hdWallet.DerivationPath
import com.tangem.domain.tokens.error.AddCustomTokenError
import com.tangem.domain.tokens.model.Network
import com.tangem.domain.wallets.models.UserWallet
@@ -15,6 +16,7 @@ import kotlinx.collections.immutable.persistentSetOf
import kotlinx.collections.immutable.toPersistentList
import kotlinx.collections.immutable.toPersistentSet
+@Suppress("LargeClass")
internal class AddCustomTokenStateFactory(
private val currentStateProvider: Provider,
private val clickIntents: AddCustomTokenClickIntents,
@@ -72,8 +74,11 @@ internal class AddCustomTokenStateFactory(
)
}
- private fun getListOfDerivations(supportedNetworks: List): List {
- return supportedNetworks.mapNotNull { network ->
+ private fun getListOfDerivations(
+ networksListToGenerateDerivations: List,
+ filterOnlyHardenedDerivations: Boolean = false,
+ ): List {
+ return networksListToGenerateDerivations.mapNotNull { network ->
network.derivationPath.value?.let { rawPath ->
Derivation(
networkName = network.name,
@@ -82,6 +87,12 @@ internal class AddCustomTokenStateFactory(
networkId = network.backendId,
onDerivationSelected = clickIntents::onDerivationSelected,
)
+ }.takeIf { derivation ->
+ if (filterOnlyHardenedDerivations) {
+ derivation?.let { allNodesHardened(createDerivationPathOrNull(it.path)) } ?: false
+ } else {
+ true
+ }
}
}
}
@@ -218,41 +229,52 @@ internal class AddCustomTokenStateFactory(
)
}
- fun updateStateOnNetworkSelected(networkItemState: NetworkItemState, supportsTokens: Boolean): AddCustomTokenState {
+ fun updateStateOnNetworkSelected(
+ networkItemState: NetworkItemState,
+ supportsTokens: Boolean,
+ networks: List,
+ requiresHardenedDerivationOnly: Boolean,
+ ): AddCustomTokenState {
val uiState = currentStateProvider()
val tokenData = if (supportsTokens) {
- uiState.tokenData
- ?: CustomTokenData(
- contractAddressTextField = TextFieldState.Editable(
- value = "",
- isEnabled = true,
- onValueChange = clickIntents::onContractAddressChange,
- onFocusExit = clickIntents::onContractAddressFocusExit,
- ),
- nameTextField = TextFieldState.Editable(
- value = "",
- isEnabled = false,
- onValueChange = clickIntents::onTokenNameChange,
- onFocusExit = clickIntents::onTokenNameFocusExit,
- ),
- symbolTextField = TextFieldState.Editable(
- value = "",
- isEnabled = false,
- onValueChange = clickIntents::onSymbolChange,
- onFocusExit = clickIntents::onSymbolFocusExit,
- ),
- decimalsTextField = TextFieldState.Editable(
- value = "",
- isEnabled = false,
- onValueChange = clickIntents::onDecimalsChange,
- onFocusExit = clickIntents::onDecimalsFocusExit,
- ),
- )
+ uiState.tokenData ?: CustomTokenData(
+ contractAddressTextField = TextFieldState.Editable(
+ value = "",
+ isEnabled = true,
+ onValueChange = clickIntents::onContractAddressChange,
+ onFocusExit = clickIntents::onContractAddressFocusExit,
+ ),
+ nameTextField = TextFieldState.Editable(
+ value = "",
+ isEnabled = false,
+ onValueChange = clickIntents::onTokenNameChange,
+ onFocusExit = clickIntents::onTokenNameFocusExit,
+ ),
+ symbolTextField = TextFieldState.Editable(
+ value = "",
+ isEnabled = false,
+ onValueChange = clickIntents::onSymbolChange,
+ onFocusExit = clickIntents::onSymbolFocusExit,
+ ),
+ decimalsTextField = TextFieldState.Editable(
+ value = "",
+ isEnabled = false,
+ onValueChange = clickIntents::onDecimalsChange,
+ onFocusExit = clickIntents::onDecimalsFocusExit,
+ ),
+ )
} else {
null
}
+
+ val derivations = getListOfDerivations(networks, requiresHardenedDerivationOnly)
+ val chooseDerivationState = createChooseDerivationState(derivations)
+
return uiState.copy(
- chooseNetworkState = uiState.chooseNetworkState.copy(selectedNetwork = networkItemState),
+ chooseNetworkState = uiState.chooseNetworkState.copy(
+ selectedNetwork = networkItemState,
+ ),
+ chooseDerivationState = chooseDerivationState,
tokenData = tokenData,
addTokenButton = uiState.addTokenButton.copy(isEnabled = true),
)
@@ -291,6 +313,47 @@ internal class AddCustomTokenStateFactory(
)
}
+ fun updateOnCustomDerivationEntered(input: String, requiresHardenedDerivationOnly: Boolean): AddCustomTokenState {
+ val uiState = currentStateProvider()
+ val path = createDerivationPathOrNull(input)
+ val isWrongDerivationForWallet2 = isWrongDerivationForWallet2(
+ requiresHardenedDerivationOnly = requiresHardenedDerivationOnly,
+ derivationPath = path,
+ )
+ val enterDerivationState = uiState.chooseDerivationState?.enterCustomDerivationState?.copy(
+ confirmButtonEnabled = path != null && !isWrongDerivationForWallet2,
+ derivationIncorrect = input.isNotBlank() && path == null || isWrongDerivationForWallet2,
+ )
+ return uiState.copy(
+ chooseDerivationState = uiState.chooseDerivationState?.copy(
+ enterCustomDerivationState = enterDerivationState,
+ ),
+ )
+ }
+
+ private fun isWrongDerivationForWallet2(
+ requiresHardenedDerivationOnly: Boolean,
+ derivationPath: DerivationPath?,
+ ): Boolean {
+ return if (requiresHardenedDerivationOnly) {
+ !allNodesHardened(derivationPath)
+ } else {
+ false
+ }
+ }
+
+ private fun allNodesHardened(derivationPath: DerivationPath?): Boolean {
+ return derivationPath?.nodes?.all { it.isHardened } ?: false
+ }
+
+ private fun createDerivationPathOrNull(rawPath: String): DerivationPath? {
+ return try {
+ DerivationPath(rawPath)
+ } catch (error: Throwable) {
+ null
+ }
+ }
+
fun updateStateOnLoadingTokenInfo(contractAddress: String): AddCustomTokenState {
return currentStateProvider().copy(
tokenData = CustomTokenData(
diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/viewmodels/AddCustomTokenViewModel.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/viewmodels/AddCustomTokenViewModel.kt
index 1b107076ea..44e55b947d 100644
--- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/viewmodels/AddCustomTokenViewModel.kt
+++ b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/viewmodels/AddCustomTokenViewModel.kt
@@ -9,8 +9,6 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import arrow.core.getOrElse
import com.tangem.core.analytics.api.AnalyticsEventHandler
-import com.tangem.crypto.hdWallet.DerivationPath
-import com.tangem.crypto.hdWallet.HDWalletError
import com.tangem.domain.common.util.derivationStyleProvider
import com.tangem.domain.tokens.*
import com.tangem.domain.tokens.model.CryptoCurrency
@@ -56,6 +54,7 @@ internal class AddCustomTokenViewModel @Inject constructor(
private val validateContractAddressUseCase: ValidateContractAddressUseCase,
private val getNetworksSupportedByWallet: GetNetworksSupportedByWallet,
private val areTokensSupportedByNetworkUseCase: AreTokensSupportedByNetworkUseCase,
+ private val requiresHardenedDerivationOnlyUseCase: RequiresHardenedDerivationOnlyUseCase,
private val analyticsEventHandler: AnalyticsEventHandler,
) : ViewModel(), AddCustomTokenClickIntents, DefaultLifecycleObserver {
@@ -135,13 +134,25 @@ internal class AddCustomTokenViewModel @Inject constructor(
private fun selectNetwork(networkItemState: NetworkItemState) {
viewModelScope.launch(dispatchers.io) {
- val selectedWalletId = getSelectedWalletSyncUseCase().getOrNull()?.walletId
+ // TODO [REDACTED_TASK_KEY]
+ val selectedWalletId = getSelectedWalletSyncUseCase().getOrNull()?.walletId ?: return@launch
val supportsTokens = areTokensSupportedByNetworkUseCase(
networkId = networkItemState.id,
userWalletId = selectedWalletId,
).getOrNull() ?: false
+
+ val networksForDerivations = getSupportedNetworks(selectedWalletId)
+
withContext(dispatchers.main) {
- uiState = stateFactory.updateStateOnNetworkSelected(networkItemState, supportsTokens)
+ uiState = stateFactory.updateStateOnNetworkSelected(
+ networkItemState = networkItemState,
+ supportsTokens = supportsTokens,
+ networks = networksForDerivations,
+ requiresHardenedDerivationOnly = requiresHardenedDerivationOnly(
+ networkId = networkItemState.id,
+ userWalletId = selectedWalletId,
+ ),
+ )
}
}
}
@@ -347,6 +358,8 @@ internal class AddCustomTokenViewModel @Inject constructor(
}
override fun onCustomDerivationChange(input: String) {
+ val selectedWallet = getSelectedWalletSyncUseCase().getOrNull() ?: return
+
analyticsEventHandler.send(ManageTokens.CustomTokenDerivationSelected(ManageTokens.Derivation.CUSTOM.value))
uiState = uiState.copy(
chooseDerivationState = uiState.chooseDerivationState?.copy(
@@ -356,25 +369,19 @@ internal class AddCustomTokenViewModel @Inject constructor(
),
)
debouncer.debounce(waitMs = DEFAULT_WAIT_TIME_MS, coroutineScope = viewModelScope + dispatchers.io) {
- val path = createDerivationPathOrNull(input)
- val enterDerivationState = uiState.chooseDerivationState?.enterCustomDerivationState?.copy(
- confirmButtonEnabled = path != null,
- derivationIncorrect = input.isNotBlank() && path == null,
- )
- uiState = uiState.copy(
- chooseDerivationState = uiState.chooseDerivationState?.copy(
- enterCustomDerivationState = enterDerivationState,
- ),
+ val networkId = uiState.chooseNetworkState.selectedNetwork?.id ?: return@debounce
+ uiState = stateFactory.updateOnCustomDerivationEntered(
+ input = input,
+ requiresHardenedDerivationOnly = requiresHardenedDerivationOnly(networkId, selectedWallet.walletId),
)
}
}
- private fun createDerivationPathOrNull(rawPath: String): DerivationPath? {
- return try {
- DerivationPath(rawPath)
- } catch (error: HDWalletError) {
- null
- }
+ private suspend fun requiresHardenedDerivationOnly(networkId: String, userWalletId: UserWalletId): Boolean {
+ return requiresHardenedDerivationOnlyUseCase.invoke(
+ networkId = networkId,
+ userWalletId = userWalletId,
+ ).getOrElse { false }
}
override fun onCustomDerivationSelected() {
diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/components/TokensList.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/components/TokensList.kt
index d6c2b505ea..ca26ef0621 100644
--- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/components/TokensList.kt
+++ b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/components/TokensList.kt
@@ -25,7 +25,7 @@ internal fun TokensList(
LazyColumn(modifier = modifier) {
item {
Text(
- text = stringResource(id = R.string.manage_tokens_title),
+ text = stringResource(id = R.string.manage_tokens_list_header_title),
style = TangemTheme.typography.h3,
color = TangemTheme.colors.text.primary1,
modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing16),
diff --git a/features/onboarding/src/main/java/com/tangem/feature/onboarding/data/DefaultMnemonicRepository.kt b/features/onboarding/src/main/java/com/tangem/feature/onboarding/data/DefaultMnemonicRepository.kt
index 71ac3393b7..09bc1f8964 100644
--- a/features/onboarding/src/main/java/com/tangem/feature/onboarding/data/DefaultMnemonicRepository.kt
+++ b/features/onboarding/src/main/java/com/tangem/feature/onboarding/data/DefaultMnemonicRepository.kt
@@ -4,6 +4,7 @@ import com.tangem.crypto.bip39.DefaultMnemonic
import com.tangem.crypto.bip39.EntropyLength
import com.tangem.crypto.bip39.Mnemonic
import com.tangem.crypto.bip39.Wordlist
+import com.tangem.feature.onboarding.domain.models.MnemonicType
/**
[REDACTED_AUTHOR]
@@ -20,5 +21,12 @@ class DefaultMnemonicRepository(
return DefaultMnemonic(EntropyLength.Bits128Length, bip39Wordlist)
}
+ override fun generateMnemonic(mnemonicType: MnemonicType): Mnemonic {
+ return when (mnemonicType) {
+ MnemonicType.Mnemonic12 -> DefaultMnemonic(EntropyLength.Bits128Length, bip39Wordlist)
+ MnemonicType.Mnemonic24 -> DefaultMnemonic(EntropyLength.Bits256Length, bip39Wordlist)
+ }
+ }
+
override fun createMnemonic(mnemonicString: String): Mnemonic = DefaultMnemonic(mnemonicString, bip39Wordlist)
}
\ No newline at end of file
diff --git a/features/onboarding/src/main/java/com/tangem/feature/onboarding/data/MnemonicRepository.kt b/features/onboarding/src/main/java/com/tangem/feature/onboarding/data/MnemonicRepository.kt
index b1b7ebfa2c..389ca4d949 100644
--- a/features/onboarding/src/main/java/com/tangem/feature/onboarding/data/MnemonicRepository.kt
+++ b/features/onboarding/src/main/java/com/tangem/feature/onboarding/data/MnemonicRepository.kt
@@ -2,6 +2,7 @@ package com.tangem.feature.onboarding.data
import com.tangem.common.core.TangemSdkError
import com.tangem.crypto.bip39.Mnemonic
+import com.tangem.feature.onboarding.domain.models.MnemonicType
/**
[REDACTED_AUTHOR]
@@ -13,6 +14,9 @@ interface MnemonicRepository {
@Throws(TangemSdkError.MnemonicException::class)
fun generateDefaultMnemonic(): Mnemonic
+ @Throws(TangemSdkError.MnemonicException::class)
+ fun generateMnemonic(mnemonicType: MnemonicType): Mnemonic
+
@Throws(TangemSdkError.MnemonicException::class)
fun createMnemonic(mnemonicString: String): Mnemonic
}
\ No newline at end of file
diff --git a/features/onboarding/src/main/java/com/tangem/feature/onboarding/domain/DefaultSeedPhraseInteractor.kt b/features/onboarding/src/main/java/com/tangem/feature/onboarding/domain/DefaultSeedPhraseInteractor.kt
index 92ed7ae69c..a0ef98bf30 100644
--- a/features/onboarding/src/main/java/com/tangem/feature/onboarding/domain/DefaultSeedPhraseInteractor.kt
+++ b/features/onboarding/src/main/java/com/tangem/feature/onboarding/domain/DefaultSeedPhraseInteractor.kt
@@ -4,6 +4,7 @@ import com.tangem.common.core.TangemSdkError
import com.tangem.crypto.bip39.Mnemonic
import com.tangem.crypto.bip39.MnemonicErrorResult
import com.tangem.feature.onboarding.data.MnemonicRepository
+import com.tangem.feature.onboarding.domain.models.MnemonicType
import com.tangem.feature.onboarding.presentation.wallet2.model.SeedPhraseField
import com.tangem.utils.extensions.isNotWhitespace
import kotlinx.collections.immutable.ImmutableList
@@ -13,27 +14,23 @@ import kotlinx.collections.immutable.toPersistentList
/**
[REDACTED_AUTHOR]
*/
-internal class DefaultSeedPhraseInteractor constructor(
+internal class DefaultSeedPhraseInteractor(
private val repository: MnemonicRepository,
) : SeedPhraseInteractor {
- private var currentMnemonic: Mnemonic? = null
private val partWordFinder: PartWordFinder = PartWordFinder()
override suspend fun generateMnemonic(): Result {
return runCatching {
- repository.generateDefaultMnemonic().apply {
- currentMnemonic = this
- }
+ repository.generateDefaultMnemonic()
}
}
- override suspend fun getMnemonicComponents(): Result> {
+ override suspend fun generateMnemonics(mnemonicTypes: List): Result