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/java/com/tangem/tap/MainActivity.kt b/app/src/main/java/com/tangem/tap/MainActivity.kt
index 116e6dcf12..c1d5853c80 100644
--- a/app/src/main/java/com/tangem/tap/MainActivity.kt
+++ b/app/src/main/java/com/tangem/tap/MainActivity.kt
@@ -52,9 +52,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
@@ -64,7 +61,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
@@ -145,9 +141,6 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
@Inject
lateinit var userWalletsListManagerFeatureToggles: UserWalletsListManagerFeatureToggles
- @Inject
- lateinit var generalUserWalletsListManager: UserWalletsListManager
-
internal val viewModel: MainViewModel by viewModels()
private lateinit var appThemeModeFlow: SharedFlow
@@ -173,8 +166,6 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
setContentView(R.layout.activity_main)
initContent()
- checkGooglePayAvailability()
-
checkForNotificationPermission()
observeStateUpdates()
@@ -256,14 +247,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
@@ -377,13 +360,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) {
@@ -432,10 +408,9 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
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
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/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/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/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 a1232dd031..cd51cff94e 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.signin.redux.SignInReducer
import com.tangem.tap.features.tokens.legacy.redux.TokensReducer
import com.tangem.tap.features.welcome.redux.WelcomeReducer
@@ -37,7 +36,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),
signInState = SignInReducer.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 892271058e..7cab6dbfbd 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.signin.redux.SignInMiddleware
import com.tangem.tap.features.signin.redux.SignInState
import com.tangem.tap.features.tokens.legacy.redux.TokensState
@@ -56,7 +54,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 signInState: SignInState = SignInState(),
@@ -92,7 +89,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/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..2d8764fc7c 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,10 @@ internal object SettingsDomainModule {
): ShouldShowSwapPromoTokenUseCase {
return ShouldShowSwapPromoTokenUseCase(swapPromoRepository)
}
+
+ @Provides
+ @ViewModelScoped
+ fun provideDeleteDeprecatedLogsUseCase(settingsRepository: SettingsRepository): DeleteDeprecatedLogsUseCase {
+ return DeleteDeprecatedLogsUseCase(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 c67960a860..7c830de869 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
@@ -278,9 +278,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/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/scanCard/DefaultScanCardProcessor.kt b/app/src/main/java/com/tangem/tap/domain/scanCard/DefaultScanCardProcessor.kt
index 1bac186d83..06df7c2274 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 {
analyticsEvent: AnalyticsEvent?,
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 {
analyticsEvent,
cardId,
onProgressStateChange,
- onScanStateChange,
onWalletNotCreated,
disclaimerWillShow,
onFailure,
@@ -52,7 +50,6 @@ internal class DefaultScanCardProcessor : ScanCardProcessor {
analyticsEvent,
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 757e78cb34..cf1c9f95be 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
@@ -48,14 +48,12 @@ internal object LegacyScanProcessor {
analyticsEvent: AnalyticsEvent?,
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)
@@ -65,13 +63,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 02e1701894..3a0902520a 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,11 +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.AnalyticsEvent
+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.*
@@ -25,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) },
)
}
@@ -37,17 +47,14 @@ internal object UseCaseScanProcessor {
analyticsEvent: AnalyticsEvent?,
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(UseCaseScanProcessor::showMaxUnsuccessfulScansReachedDialog))
if (analyticsEvent != null) {
add(AnalyticsChain(analyticsEvent))
}
@@ -55,9 +62,14 @@ internal object UseCaseScanProcessor {
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() {
+ store.dispatchDialogShow(StateDialog.ScanFailsDialog)
}
private suspend fun proceedWithException(
@@ -75,7 +87,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)
+ }
}
}
@@ -89,8 +106,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/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/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/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/walletconnect/WalletConnectMiddleware.kt b/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectMiddleware.kt
index c6422a0706..c45ec9dae3 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
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))
@@ -139,7 +147,21 @@ internal class DetailsViewModel(
private fun sendFeedback() {
Analytics.send(Basic.ButtonSupport())
- 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/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/home/redux/HomeAction.kt b/app/src/main/java/com/tangem/tap/features/home/redux/HomeAction.kt
index e87ba1ee99..cb2d157825 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
@@ -3,7 +3,6 @@ package com.tangem.tap.features.home.redux
import com.tangem.core.analytics.models.AnalyticsEvent
import com.tangem.core.analytics.models.AnalyticsParam
import com.tangem.core.analytics.models.Basic
-import com.tangem.tap.common.entities.IndeterminateProgressButton
import kotlinx.coroutines.CoroutineScope
import org.rekotlin.Action
@@ -29,6 +28,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 2bec7b7af6..8b83c455cf 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
@@ -13,12 +13,10 @@ import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.userwallets.UserWalletBuilder
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.features.signin.redux.SignInAction
import com.tangem.tap.preferencesStorage
import com.tangem.tap.proxy.redux.DaggerGraphState
@@ -31,6 +29,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
@@ -84,17 +84,16 @@ private suspend fun readCard(analyticsEvent: AnalyticsEvent?) {
analyticsEvent = analyticsEvent,
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)
@@ -113,7 +112,7 @@ private fun proceedWithScanResponse(scanResponse: ScanResponse) = scope.launch {
Timber.e(error, "Unable to save user wallet")
}
.doOnSuccess {
- scope.launch { store.onUserWalletSelected(userWallet = userWallet) }
+ scope.launch { store.onUserWalletSelected(userWallet) }
}
.doOnResult {
store.dispatchOnMain(SignInAction.SetSignInType(Basic.SignedIn.SignInType.Card))
@@ -123,10 +122,6 @@ private fun proceedWithScanResponse(scanResponse: ScanResponse) = scope.launch {
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 fd59efff07..dee9d0f733 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
@@ -72,13 +72,14 @@ object OnboardingHelper {
scanResponse: ScanResponse,
accessCode: String? = null,
backupCardsIds: List? = null,
+ hasBackupError: Boolean = false,
) {
Analytics.setContext(scanResponse)
scope.launch {
when {
// When should save user wallets, then save card without navigate to save wallet screen
store.inject(DaggerGraphState::walletsRepository).shouldSaveUserWalletsSync() -> {
- proceedWithScanResponse(scanResponse, backupCardsIds)
+ proceedWithScanResponse(scanResponse, backupCardsIds, hasBackupError)
store.dispatchOnMain(
SaveWalletAction.ProvideBackupInfo(
@@ -92,7 +93,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)
@@ -109,7 +110,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))
}
}
@@ -129,8 +130,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/wallet/redux/OnboardingWalletAction.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletAction.kt
index 6b536fc754..5bd2df0285 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() {
@@ -49,45 +49,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..16045b38c8 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,
)
}
}
@@ -484,6 +486,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/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..a76ae2c37e 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
@@ -318,6 +318,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/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..3e5a70b2a2 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
@@ -12,6 +12,7 @@ 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,46 @@ 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 ->
+ 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,21 +85,21 @@ 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
}
}
}
@@ -103,7 +108,7 @@ class MoonPayService(
action: CurrencyExchangeManager.Action,
blockchain: Blockchain,
cryptoCurrencyName: CryptoCurrencyName,
- fatCurrency: String,
+ fiatCurrencyName: String,
walletAddress: String,
isDarkTheme: Boolean,
): String {
@@ -115,7 +120,8 @@ class MoonPayService(
.appendQueryParameter("apiKey", apiKey)
.appendQueryParameter("baseCurrencyCode", cryptoCurrencyName)
.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 +153,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/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_shop.xml b/app/src/main/res/layout/fragment_shop.xml
deleted file mode 100644
index 48384d34c6..0000000000
--- a/app/src/main/res/layout/fragment_shop.xml
+++ /dev/null
@@ -1,336 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
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/config/ConfigManagerImpl.kt b/core/datasource/src/main/java/com/tangem/datasource/config/ConfigManagerImpl.kt
index 580334eb2b..8ebac44c3b 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,
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..0e572bd3e5 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,
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/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..ce4c2d9448 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"
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..79539f13a8 100644
--- a/core/res/src/main/res/values-ru/strings.xml
+++ b/core/res/src/main/res/values-ru/strings.xml
@@ -1,753 +1,671 @@
- Добавить токен
- Валюты
- Отправляйте только %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
+ Пользуясь сервисом, вы соглашаетесь с %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-фразе
+ Запишите эти 12 слов в порядке, указанном ниже, и сохраните их в надежном месте.
+
+
+ - Запишите эти %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
+ Включая комиссию
+ Низкая
+ Нормальная
+ Приоритетная
+ Лимит газа
+ Цена газа
+ Цена газа влияет на скорость транзакции. При сильно низкой, транзакция может быть не обработана.
+ Всё
+ Максимальная сумма
+ Комиссия не превысит
+ Максимальная cумма комиссии
+ Сумма отправки будет уменьшена на %1$s для покрытия выбранного уровня комиссии. Получателю будет отправлено %2$s.
+ Покрытие сетевой комиссии
+ Недостаточно средств для перевода, так как сумма комиссии и сумма перевода в совокупности больше имеющегося баланса
+ Недостаточно средств
+ Сумма комиссии в %s раз превышает рекомендованную. Убедитесь, что указанная комиссия верна.
+ Установлена высокая комиссия
+ Комиссия при переводе всего баланса выше. Для того, чтобы снизить комиссию Вы можете оставить %s.
+ Комиссия увеличилась
+ Включенная комиссия превышает сумму перевода, что приводит к отрицательному значению
+ Недопустимая сумма
+ Минимальная сумма отправки - %1$s. Пожалуйста, убедитесь, что остаток после отправки также не будет меньше %2$s.
+ Адрес получателя не активирован. \nПожалуйста, измените сумму отправки, чтобы продолжить.
+ Сумма отправки не может быть менее %s
+ Уменьшить на %s
+ Уменьшить до %s
+ Обратите внимание, что при определенных параметрах комиссии возможны задержки по вашей транзакции
+ Возможны задержки по транзакции
+ Из-за ограничений %1$s в одну транзакцию может поместиться только %2$s UTXO. Это означает, что вы можете отправить только %3$s или меньше. Вам нужно уменьшить сумму.
+ Лимит транзакции
+ Необязательное
+ Последние
+ Получатель
+ Убедитесь, что вы отправляете средства на адрес кошелька %s. Ошибки могут привести к потере ваших токенов
+ Отправить
+ Мемо/ Код назначения - это код, разделяющий транзакции к общему получателю в сети криптовалют. Внимание: отсутствие мемо может привести к потере средств.
+ Мои кошельки
+ Отправка %s
+ Всего
+ %1$s и %2$s будет отправлено
+ ≈ %1$s (вкл. комиссию: %2$s)
+ %s будет отправлено
+ Транзакция успешно подписана и отправлена в блокчейн. Баланс будет обновлен через некоторое время
+ Неверный адрес
+ Держите свои криптосбережения в безопасности. Приватные ключи надежно хранятся на карте.
+ Революционный аппаратный кошелек
+ До трех карт с одним кошельком
+ Все ключи в безопасности
+ Аппаратный кошелек для ваших биткоинов, эфира и многих других валют одновременно — все в одной карте
+ Тысячи криптовалют
+ Используйте его на ходу, в любом месте, в любое время. Без проводов и батареек. Как только понадобится крипта, просто приложите карту к телефону.
+ Кошелек для каждого
+ Встречайте Tangem
+ Обменивайте, покупайте NFT, получайте займы и делайте вклады в более чем 100 различных децентрализованных сервисах
+ Поддержка Web 3.0
+ В сумму включено: \n• комиссия провайдера сервиса\n• комиссия сети за отправку %s от биржи обратно на адрес пользователя
+ В сумму включена комиссия провайдера сервиса.
+ Комиссии
+ Подтверждения считаются отраслевым стандартом для всех децентрализованных бирж и защищают ваш кошелек от доступа со стороны смарт-контракта без вашего разрешения. По замыслу смарт-контракты не могут получить доступ к вашим токенам, если вы не одобрите доступ со своей стороны. «Разблокируя» свои токены, вы даете смарт-контракту 1inch разрешение тратить ваши активы. Майнеры сети получают компенсацию за газ (оплачиваемый вами) за запись этого действия в блокчейне. Как только разрешение будет предоставлено, вы сможете обменять свой токен.
+ Подтвердить
+ Вы отправляете
+ Дать разрешение
+ Обмен этой суммы выбранных токенов может вызвать значительные колебания цены и уменьшить получаемую сумму.
+ Недостаточно средств
+ Сумма отправки будет уменьшена для покрытия выбранного уровня комиссии
+ Подтвердить
+ Текущая транзакция
+ Комиссия сети за одобрение токена будет взиматься за подтверждение того, что именно вы разрешаете использовать ваш токен для обмена.
+ Дать разрешение
+ Укажите лимит доступа к выбранному токену
+ Количество %s
+ Чтобы продолжить, вам нужно разрешить смарт-контракту 1inch использовать ваш %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
+ 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..a8a013bebc 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,374 @@
- 添加自定義代幣
- 管理代幣
- 僅將 %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
+ 立即備份
+ 掃描主卡
+ 繼續至我的錢包
+ 完成備份過程
+ 接收貨幣
+ 掃描主卡
+ 暫時略過
+ 它是如何運作的?
+ 讓我們生成您卡上的所有密鑰並創建一個安全的錢包
+ 創建錢包
+ 創造錢包
+ 其他選項
+ 您的密鑰將在卡內安全生成。沒有種子短語,這意味著沒有人可以導出或竊取它。
+ 私下生成密鑰
+ 您的卡已激活並可以使用
+ 成功!
+ 這此情況,您必須要重新開始
+ 您想要離開啟用程序嗎?
+ 開始
+ 您要添加的卡上已經創建了另一個錢包。你想重置它並將卡用於新錢包嗎?
+ 創建備份
+ 閱讀更多關於助記詞的訊息
+ 按照下面給出的順序寫下這 12 個單詞,並將它們存放在安全秘密的地方。
+
+ - 按照下面給出的順序寫下這 %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..d91eeb01d1 100644
--- a/core/res/src/main/res/values/strings.xml
+++ b/core/res/src/main/res/values/strings.xml
@@ -1,762 +1,679 @@
- 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
+ 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 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.
+ 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. Do you want to reset it and use the card for a new wallet?
+ 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.
+
+
+ - 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 commission. Subtract the commission from the amount sent?
+ 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
+ Confirm
+ %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
+ 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
+ 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
+ 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
+ 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
+ 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/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/fields/AmountTextField.kt b/core/ui/src/main/java/com/tangem/core/ui/components/fields/AmountTextField.kt
index 4bf1b72d84..fc0ee052f3 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
@@ -15,6 +15,7 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.input.KeyboardType
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
@@ -74,7 +75,7 @@ fun AmountTextField(
},
modifier = modifier
.background(TangemTheme.colors.background.action),
- textStyle = textStyle,
+ textStyle = textStyle.copy(textDirection = TextDirection.ContentOrLtr),
color = color,
keyboardOptions = keyboardOptions,
keyboardActions = keyboardActions,
@@ -90,7 +91,7 @@ fun AmountTextField(
}
Text(
text = placeholder,
- style = textStyle,
+ style = textStyle.copy(textDirection = TextDirection.ContentOrLtr),
color = TangemTheme.colors.text.disabled,
textAlign = placeholderTextAlign,
modifier = Modifier
diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/fields/SimpleTextField.kt b/core/ui/src/main/java/com/tangem/core/ui/components/fields/SimpleTextField.kt
index 4a3fdbff20..6d31ebeaef 100644
--- a/core/ui/src/main/java/com/tangem/core/ui/components/fields/SimpleTextField.kt
+++ b/core/ui/src/main/java/com/tangem/core/ui/components/fields/SimpleTextField.kt
@@ -67,9 +67,7 @@ fun SimpleTextField(
LaunchedEffect(key1 = isSelectionChanged) {
if (isSelectionChanged) {
- textFieldValueState = textFieldValue.copy(
- selection = getValueRange(value),
- )
+ textFieldValueState = textFieldValue
}
}
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/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/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..6148da1827
--- /dev/null
+++ b/data/feedback/src/main/java/com/tangem/data/feedback/DefaultFeedbackRepository.kt
@@ -0,0 +1,105 @@
+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.AppLogModel
+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.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 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..e9a1a61019
--- /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(
+ userWalletId = value.walletId.stringValue,
+ cardId = card.cardId,
+ firmwareVersion = card.firmwareVersion.stringValue,
+ cardBlockchain = walletData?.blockchain,
+ signedHashesList = card.wallets.map {
+ CardInfo.SignedHashes(curve = it.curve.curve, total = it.totalSignedHashes?.toString())
+ },
+ 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..277d4fc31f 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,21 @@ 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
-
- private companion object {
- const val THREE_DAYS = 3
- }
}
\ 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/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 961d9acd07..4dfc1c9668 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 {
analyticsEvent: AnalyticsEvent? = null,
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/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..35740e1357
--- /dev/null
+++ b/domain/feedback/src/main/java/com/tangem/domain/feedback/FeedbackDataBuilder.kt
@@ -0,0 +1,105 @@
+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.utils.breakLine
+import com.tangem.domain.feedback.models.BlockchainInfo.Addresses as BlockchainAddresses
+
+internal class FeedbackDataBuilder {
+
+ private val builder = StringBuilder()
+
+ fun addCardInfo(cardInfo: CardInfo) {
+ builder.appendKeyValue("Card ID", cardInfo.cardId)
+ builder.appendKeyValue("Firmware version", cardInfo.firmwareVersion)
+ builder.appendKeyValue("Card Blockchain", cardInfo.cardBlockchain)
+ builder.appendSignedHashes(cardInfo.signedHashesList)
+ builder.appendKeyValue("User Wallet ID", cardInfo.userWalletId)
+ }
+
+ 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..25db3a0ef6
--- /dev/null
+++ b/domain/feedback/src/main/java/com/tangem/domain/feedback/GetSupportFeedbackEmailUseCase.kt
@@ -0,0 +1,111 @@
+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 {
+ 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..79eb5285c6
--- /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 userWalletId: String,
+ val cardId: String,
+ val firmwareVersion: String,
+ val cardBlockchain: String?,
+ val signedHashesList: List,
+ 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/repository/FeedbackRepository.kt b/domain/feedback/src/main/java/com/tangem/domain/feedback/repository/FeedbackRepository.kt
new file mode 100644
index 0000000000..5e137df28a
--- /dev/null
+++ b/domain/feedback/src/main/java/com/tangem/domain/feedback/repository/FeedbackRepository.kt
@@ -0,0 +1,20 @@
+package com.tangem.domain.feedback.repository
+
+import com.tangem.domain.feedback.models.AppLogModel
+import com.tangem.domain.feedback.models.BlockchainInfo
+import com.tangem.domain.feedback.models.CardInfo
+import com.tangem.domain.feedback.models.PhoneInfo
+import java.io.File
+
+interface FeedbackRepository {
+
+ 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..8ff12c6c8c 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,10 @@ fun Blockchain.Companion.fromNetworkId(networkId: String): Blockchain? {
"areon-network/test" -> Blockchain.AreonTestnet
"pulsechain" -> Blockchain.PulseChain
"pulsechain/test" -> Blockchain.PulseChainTestnet
+ "nexa" -> Blockchain.Nexa // FIXME
+ "nexa/testnet" -> Blockchain.NexaTestnet // FIXME
+ "zksync" -> Blockchain.ZkSyncEra // FIXME
+ "zksync/testnet" -> Blockchain.ZkSyncEraTestnet // FIXME
else -> null
}
}
@@ -187,6 +191,11 @@ fun Blockchain.toNetworkId(): String {
Blockchain.AreonTestnet -> "areon-network/test"
Blockchain.PulseChain -> "pulsechain"
Blockchain.PulseChainTestnet -> "pulsechain/test"
+ Blockchain.ZkSyncEra -> "zksync" // FIXME
+ Blockchain.ZkSyncEraTestnet -> "zksync/testnet" // FIXME
+ Blockchain.Nexa -> "nexa" // FIXME
+ Blockchain.NexaTestnet -> "nexa/testnet" // FIXME
+ Blockchain.Radiant -> "TODO() add" // FIXME
}
}
@@ -249,6 +258,11 @@ fun Blockchain.toCoinId(): String {
Blockchain.Aurora, Blockchain.AuroraTestnet -> "aurora-ethereum"
Blockchain.Areon, Blockchain.AreonTestnet -> "areon-network"
Blockchain.PulseChain, Blockchain.PulseChainTestnet -> "pulsechain"
+ Blockchain.ZkSyncEra -> "zksync" // FIXME
+ Blockchain.ZkSyncEraTestnet -> "zksync/testnet" // FIXME
+ Blockchain.Nexa -> "nexa" // FIXME
+ Blockchain.NexaTestnet -> "nexa/testnet" // FIXME
+ Blockchain.Radiant -> "TODO() add" // FIXME
}
}
@@ -276,4 +290,8 @@ private const val NODL_AMOUNT_TO_CREATE_ACCOUNT = 1.5
private val excludedBlockchains = listOf(
Blockchain.Unknown,
Blockchain.Playa3ull,
+ Blockchain.ZkSyncEra,
+ Blockchain.ZkSyncEraTestnet,
+ Blockchain.Nexa,
+ Blockchain.NexaTestnet,
)
\ 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..15b93ba08e 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() }
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/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/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..061ee78557 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,9 @@ 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)
}
\ No newline at end of file
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/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/presentation/wallet2/ui/ImportSeedPhraseScreen.kt b/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/ImportSeedPhraseScreen.kt
index 10248b767a..e0a5b674ad 100644
--- a/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/ImportSeedPhraseScreen.kt
+++ b/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/ImportSeedPhraseScreen.kt
@@ -22,7 +22,9 @@ import com.tangem.core.ui.components.PrimaryButtonIconEnd
import com.tangem.core.ui.components.TangemTextFieldsDefault
import com.tangem.core.ui.res.TangemTheme
import com.tangem.feature.onboarding.R
+import com.tangem.feature.onboarding.presentation.wallet2.model.ButtonState
import com.tangem.feature.onboarding.presentation.wallet2.model.ImportSeedPhraseState
+import com.tangem.feature.onboarding.presentation.wallet2.model.TextFieldState
import com.tangem.feature.onboarding.presentation.wallet2.ui.components.DescriptionSubTitleText
import com.tangem.feature.onboarding.presentation.wallet2.ui.components.OnboardingActionBlock
import com.tangem.feature.onboarding.presentation.wallet2.ui.components.OnboardingDescriptionBlock
@@ -67,7 +69,7 @@ fun ImportSeedPhraseScreen(state: ImportSeedPhraseState, modifier: Modifier = Mo
PrimaryButtonIconEnd(
modifier = Modifier
.fillMaxWidth(),
- text = stringResource(id = R.string.onboarding_create_wallet_button_create_wallet),
+ text = stringResource(id = R.string.common_import),
iconResId = R.drawable.ic_tangem_24,
enabled = state.buttonCreateWallet.enabled,
showProgress = state.buttonCreateWallet.showProgress,
@@ -196,6 +198,28 @@ private fun SuggestionsBlockPreview_Dark(
}
}
+@Preview
+@Composable
+private fun ImportSeedPhraseScreenPreview_Light(
+ @PreviewParameter(SuggestionsPreviewParamsProvider::class) suggestions: ImmutableList,
+) {
+ TangemTheme(isDark = false) {
+ ImportSeedPhraseScreen(
+ ImportSeedPhraseState(
+ tvSeedPhrase = TextFieldState(onTextFieldValueChanged = {}),
+ onSuggestedPhraseClick = {},
+ buttonCreateWallet = ButtonState(
+ enabled = true,
+ isClickable = true,
+ showProgress = false,
+ onClick = {},
+ ),
+ suggestionsList = suggestions,
+ ),
+ )
+ }
+}
+
private class SuggestionsPreviewParamsProvider : CollectionPreviewParameterProvider>(
collection = listOf(
persistentListOf(
diff --git a/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/YourSeedPhraseScreen.kt b/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/YourSeedPhraseScreen.kt
index 76a7e48923..23e21b9c92 100644
--- a/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/YourSeedPhraseScreen.kt
+++ b/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/YourSeedPhraseScreen.kt
@@ -1,5 +1,6 @@
package com.tangem.feature.onboarding.presentation.wallet2.ui
+import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
@@ -8,16 +9,19 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.tooling.preview.Preview
import com.tangem.core.ui.components.PrimaryButton
import com.tangem.core.ui.components.SpacerW32
import com.tangem.core.ui.res.TangemTheme
import com.tangem.feature.onboarding.R
+import com.tangem.feature.onboarding.presentation.wallet2.model.ButtonState
import com.tangem.feature.onboarding.presentation.wallet2.model.MnemonicGridItem
import com.tangem.feature.onboarding.presentation.wallet2.model.YourSeedPhraseState
import com.tangem.feature.onboarding.presentation.wallet2.ui.components.Description
import com.tangem.feature.onboarding.presentation.wallet2.ui.components.OnboardingActionBlock
import com.tangem.feature.onboarding.presentation.wallet2.ui.components.OnboardingDescriptionBlock
import kotlinx.collections.immutable.ImmutableList
+import kotlinx.collections.immutable.toImmutableList
/**
[REDACTED_AUTHOR]
@@ -88,4 +92,25 @@ private fun PhraseGreedBlock(mnemonicGridItems: ImmutableList)
}
}
}
+}
+
+@Preview
+@Composable
+private fun YourSeedPhraseScreenPreview_Light() {
+ TangemTheme(isDark = false) {
+ YourSeedPhraseScreen(
+ state = YourSeedPhraseState(
+ mnemonicGridItems = (1..12).map { MnemonicGridItem(it, it.toString()) }.toImmutableList(),
+ buttonContinue = ButtonState(
+ enabled = true,
+ isClickable = true,
+ showProgress = false,
+ onClick = {},
+ ),
+ ),
+ modifier = Modifier
+ .fillMaxSize()
+ .background(color = TangemTheme.colors.background.primary),
+ )
+ }
}
\ No newline at end of file
diff --git a/features/qr-scanning/impl/build.gradle.kts b/features/qr-scanning/impl/build.gradle.kts
index e1d37506b4..5ad7d8346d 100644
--- a/features/qr-scanning/impl/build.gradle.kts
+++ b/features/qr-scanning/impl/build.gradle.kts
@@ -10,7 +10,6 @@ android {
namespace = "com.tangem.feature.qrscanning.impl"
}
-
dependencies {
/** Core */
@@ -40,8 +39,13 @@ dependencies {
implementation(deps.hilt.android)
kapt(deps.hilt.kapt)
+ /** Api */
implementation(projects.features.qrScanning.api)
+ /** Domain */
+ implementation(projects.domain.qrScanning)
+ implementation(projects.domain.qrScanning.models)
+
/** Compose */
implementation(deps.compose.foundation)
implementation(deps.compose.ui)
diff --git a/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/di/QrScanningModule.kt b/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/di/QrScanningModule.kt
deleted file mode 100644
index ad088e8c28..0000000000
--- a/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/di/QrScanningModule.kt
+++ /dev/null
@@ -1,27 +0,0 @@
-package com.tangem.feature.qrscanning.di
-
-import com.tangem.feature.qrscanning.repo.DefaultQrScanningEventsRepository
-import com.tangem.feature.qrscanning.repo.QrScanningEventsRepository
-import com.tangem.feature.qrscanning.usecase.EmitQrScannedEventUseCase
-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 QrScanningModule {
-
- @Provides
- @Singleton
- fun provideQrScannedEventsRepository(): QrScanningEventsRepository {
- return DefaultQrScanningEventsRepository()
- }
-
- @Provides
- @Singleton
- fun provideEmitQrScannedEventUseCase(repository: QrScanningEventsRepository): EmitQrScannedEventUseCase {
- return EmitQrScannedEventUseCase(repository)
- }
-}
\ No newline at end of file
diff --git a/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/presentation/transformers/InitializeQrScanningStateTransformer.kt b/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/presentation/transformers/InitializeQrScanningStateTransformer.kt
index c4df653e5a..2f38d3e545 100644
--- a/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/presentation/transformers/InitializeQrScanningStateTransformer.kt
+++ b/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/presentation/transformers/InitializeQrScanningStateTransformer.kt
@@ -2,7 +2,7 @@ package com.tangem.feature.qrscanning.presentation.transformers
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.wrappedList
-import com.tangem.feature.qrscanning.SourceType
+import com.tangem.domain.qrscanning.models.SourceType
import com.tangem.feature.qrscanning.impl.R
import com.tangem.feature.qrscanning.presentation.QrScanningState
import com.tangem.feature.qrscanning.viewmodel.QrScanningClickIntents
diff --git a/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/repo/DefaultQrScanningEventsRepository.kt b/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/repo/DefaultQrScanningEventsRepository.kt
deleted file mode 100644
index 146bee2ef6..0000000000
--- a/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/repo/DefaultQrScanningEventsRepository.kt
+++ /dev/null
@@ -1,19 +0,0 @@
-package com.tangem.feature.qrscanning.repo
-
-import com.tangem.feature.qrscanning.SourceType
-import kotlinx.coroutines.flow.*
-
-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 }
-}
\ No newline at end of file
diff --git a/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/repo/QrScanningEventsRepository.kt b/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/repo/QrScanningEventsRepository.kt
deleted file mode 100644
index 3640e79c2a..0000000000
--- a/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/repo/QrScanningEventsRepository.kt
+++ /dev/null
@@ -1,11 +0,0 @@
-package com.tangem.feature.qrscanning.repo
-
-import com.tangem.feature.qrscanning.SourceType
-import kotlinx.coroutines.flow.Flow
-
-interface QrScanningEventsRepository {
-
- suspend fun emitResult(type: SourceType, qrCode: String)
-
- fun subscribeToScanningResults(type: SourceType): Flow
-}
\ No newline at end of file
diff --git a/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/viewmodel/BaseQrScanningClickIntents.kt b/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/viewmodel/BaseQrScanningClickIntents.kt
index 3ae564a745..a9c37e465c 100644
--- a/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/viewmodel/BaseQrScanningClickIntents.kt
+++ b/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/viewmodel/BaseQrScanningClickIntents.kt
@@ -1,7 +1,7 @@
package com.tangem.feature.qrscanning.viewmodel
import androidx.activity.result.ActivityResultLauncher
-import com.tangem.feature.qrscanning.SourceType
+import com.tangem.domain.qrscanning.models.SourceType
import com.tangem.feature.qrscanning.navigation.QrScanningInnerRouter
import kotlinx.coroutines.CoroutineScope
import kotlin.properties.Delegates
diff --git a/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/viewmodel/QrScanningClickIntents.kt b/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/viewmodel/QrScanningClickIntents.kt
index 7ecbdae562..64d837b7f4 100644
--- a/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/viewmodel/QrScanningClickIntents.kt
+++ b/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/viewmodel/QrScanningClickIntents.kt
@@ -1,8 +1,8 @@
package com.tangem.feature.qrscanning.viewmodel
+import com.tangem.domain.qrscanning.usecases.EmitQrScannedEventUseCase
import com.tangem.feature.qrscanning.presentation.QrScanningStateController
import com.tangem.feature.qrscanning.presentation.transformers.DismissBottomSheetTransformer
-import com.tangem.feature.qrscanning.usecase.EmitQrScannedEventUseCase
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.hilt.android.scopes.ViewModelScoped
import kotlinx.coroutines.launch
diff --git a/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/viewmodel/QrScanningViewModel.kt b/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/viewmodel/QrScanningViewModel.kt
index 9e17c2f6a7..4320634f4d 100644
--- a/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/viewmodel/QrScanningViewModel.kt
+++ b/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/viewmodel/QrScanningViewModel.kt
@@ -6,7 +6,7 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.tangem.feature.qrscanning.QrScanningRouter.Companion.NETWORK_KEY
import com.tangem.feature.qrscanning.QrScanningRouter.Companion.SOURCE_KEY
-import com.tangem.feature.qrscanning.SourceType
+import com.tangem.domain.qrscanning.models.SourceType
import com.tangem.feature.qrscanning.navigation.QrScanningInnerRouter
import com.tangem.feature.qrscanning.presentation.QrScanningState
import com.tangem.feature.qrscanning.presentation.QrScanningStateController
diff --git a/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/ParticipateBottomBlock.kt b/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/ParticipateBottomBlock.kt
index 57337c925a..3417b6931f 100644
--- a/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/ParticipateBottomBlock.kt
+++ b/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/ParticipateBottomBlock.kt
@@ -27,6 +27,8 @@ import androidx.compose.ui.res.pluralStringResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.tooling.preview.PreviewParameter
+import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
import androidx.core.content.ContextCompat.startActivity
import com.tangem.core.ui.components.*
import com.tangem.core.ui.res.TangemTheme
@@ -113,14 +115,22 @@ private fun Awards(expectedAwards: ExpectedAwards) {
thickness = TangemTheme.dimens.size0_5,
)
AwardText(
- startText = stringResource(id = R.string.referral_expected_awards),
+ startText = if (expectedAwards.expectedAwards.isNotEmpty()) {
+ stringResource(id = R.string.referral_expected_awards)
+ } else {
+ stringResource(id = R.string.referral_no_expected_awards)
+ },
startTextColor = TangemTheme.colors.text.tertiary,
startTextStyle = TangemTheme.typography.subtitle2,
- endText = pluralStringResource(
- id = R.plurals.referral_number_of_wallets,
- count = expectedAwards.numberOfWallets,
- expectedAwards.numberOfWallets,
- ),
+ endText = if (expectedAwards.expectedAwards.isNotEmpty()) {
+ pluralStringResource(
+ id = R.plurals.referral_number_of_wallets,
+ count = expectedAwards.numberOfWallets,
+ expectedAwards.numberOfWallets,
+ )
+ } else {
+ ""
+ },
endTextColor = TangemTheme.colors.text.tertiary,
endTextStyle = TangemTheme.typography.body2,
cornersToRound = CornersToRound.ZERO,
@@ -309,34 +319,20 @@ private fun Context.shareText(text: String) {
@Preview(widthDp = 360, showBackground = true)
@Composable
-private fun Preview_ParticipateBottomBlock_InLightTheme() {
+private fun ParticipateBottomBlockPreview_Light(
+ @PreviewParameter(ParticipateBottomBlockDataProvider::class) data: ParticipateBottomBlockData,
+) {
TangemTheme(isDark = false) {
Column(Modifier.background(TangemTheme.colors.background.secondary)) {
ParticipateBottomBlock(
- purchasedWalletCount = 3,
- code = "x4JdK",
- shareLink = "",
- expectedAwards = ExpectedAwards(
- numberOfWallets = 3,
- expectedAwards = listOf(
- ExpectedAward(
- amount = "10 USDT",
- paymentDate = "Today",
- ),
- ExpectedAward(
- amount = "20 USDT",
- paymentDate = "6 Aug 2023",
- ),
- ExpectedAward(
- amount = "30 USDT",
- paymentDate = "10 Aug 2023",
- ),
- ),
- ),
- onAgreementClick = {},
- onShowCopySnackbar = {},
- onCopyClick = {},
- onShareClick = {},
+ purchasedWalletCount = data.purchasedWalletCount,
+ code = data.code,
+ shareLink = data.shareLink,
+ expectedAwards = data.expectedAwards,
+ onAgreementClick = data.onAgreementClick,
+ onShowCopySnackbar = data.onShowCopySnackbar,
+ onCopyClick = data.onCopyClick,
+ onShareClick = data.onShareClick,
)
}
}
@@ -344,18 +340,20 @@ private fun Preview_ParticipateBottomBlock_InLightTheme() {
@Preview(widthDp = 360, showBackground = true)
@Composable
-private fun Preview_ParticipateBottomBlock_Without_Awards_InLightTheme() {
- TangemTheme(isDark = false) {
+private fun ParticipateBottomBlockPreview_Dark(
+ @PreviewParameter(ParticipateBottomBlockDataProvider::class) state: ParticipateBottomBlockData,
+) {
+ TangemTheme(isDark = true) {
Column(Modifier.background(TangemTheme.colors.background.secondary)) {
ParticipateBottomBlock(
- purchasedWalletCount = 3,
- code = "x4JdK",
- shareLink = "",
- expectedAwards = null,
- onAgreementClick = {},
- onShowCopySnackbar = {},
- onCopyClick = {},
- onShareClick = {},
+ purchasedWalletCount = state.purchasedWalletCount,
+ code = state.code,
+ shareLink = state.shareLink,
+ expectedAwards = state.expectedAwards,
+ onAgreementClick = state.onAgreementClick,
+ onShowCopySnackbar = state.onShowCopySnackbar,
+ onCopyClick = state.onCopyClick,
+ onShareClick = state.onShareClick,
)
}
}
@@ -363,26 +361,7 @@ private fun Preview_ParticipateBottomBlock_Without_Awards_InLightTheme() {
@Preview(widthDp = 360, showBackground = true)
@Composable
-private fun Preview_ParticipateBottomBlock_Without_Awards_And_Purchased_Wallets_InLightTheme() {
- TangemTheme(isDark = false) {
- Column(Modifier.background(TangemTheme.colors.background.secondary)) {
- ParticipateBottomBlock(
- purchasedWalletCount = 0,
- code = "x4JdK",
- shareLink = "",
- expectedAwards = null,
- onAgreementClick = {},
- onShowCopySnackbar = {},
- onCopyClick = {},
- onShareClick = {},
- )
- }
- }
-}
-
-@Preview(widthDp = 360, showBackground = true)
-@Composable
-private fun LessMoreButton_White() {
+private fun LessMoreButton_Light() {
TangemTheme(isDark = false) {
LessMoreButton(
isExpanded = remember {
@@ -394,19 +373,64 @@ private fun LessMoreButton_White() {
@Preview(widthDp = 360, showBackground = true)
@Composable
-private fun Preview_ParticipateBottomBlock_Without_Awards_InDarkTheme() {
+private fun LessMoreButton_Dark() {
TangemTheme(isDark = true) {
- Column(Modifier.background(TangemTheme.colors.background.secondary)) {
- ParticipateBottomBlock(
- purchasedWalletCount = 3,
- code = "x4JdK",
- shareLink = "",
- expectedAwards = null,
- onAgreementClick = {},
- onShowCopySnackbar = {},
- onCopyClick = {},
- onShareClick = {},
- )
- }
+ LessMoreButton(
+ isExpanded = remember {
+ mutableStateOf(false)
+ },
+ )
}
-}
\ No newline at end of file
+}
+
+private class ParticipateBottomBlockDataProvider : CollectionPreviewParameterProvider(
+ collection = listOf(
+ ParticipateBottomBlockData(
+ purchasedWalletCount = 3,
+ expectedAwards = ExpectedAwards(
+ numberOfWallets = 3,
+ expectedAwards = listOf(
+ ExpectedAward(
+ amount = "10 USDT",
+ paymentDate = "Today",
+ ),
+ ExpectedAward(
+ amount = "20 USDT",
+ paymentDate = "6 Aug 2023",
+ ),
+ ExpectedAward(
+ amount = "30 USDT",
+ paymentDate = "10 Aug 2023",
+ ),
+ ),
+ ),
+ ),
+ ParticipateBottomBlockData(
+ purchasedWalletCount = 3,
+ expectedAwards = ExpectedAwards(
+ numberOfWallets = 3,
+ expectedAwards = emptyList(),
+ ),
+ ),
+ ParticipateBottomBlockData(
+ purchasedWalletCount = 3,
+ expectedAwards = null,
+ ),
+ ParticipateBottomBlockData(
+ purchasedWalletCount = 0,
+ expectedAwards = null,
+ ),
+
+ ),
+)
+
+private data class ParticipateBottomBlockData(
+ val purchasedWalletCount: Int,
+ val expectedAwards: ExpectedAwards?,
+ val code: String = "x4JDK",
+ val shareLink: String = "",
+ val onAgreementClick: () -> Unit = {},
+ val onShowCopySnackbar: () -> Unit = {},
+ val onCopyClick: () -> Unit = {},
+ val onShareClick: () -> Unit = {},
+)
\ No newline at end of file
diff --git a/features/send/api/src/main/kotlin/com/tangem/features/send/api/navigation/SendRouter.kt b/features/send/api/src/main/kotlin/com/tangem/features/send/api/navigation/SendRouter.kt
index fce7f71d36..4a2940f972 100644
--- a/features/send/api/src/main/kotlin/com/tangem/features/send/api/navigation/SendRouter.kt
+++ b/features/send/api/src/main/kotlin/com/tangem/features/send/api/navigation/SendRouter.kt
@@ -11,6 +11,7 @@ interface SendRouter {
const val USER_WALLET_ID_KEY = "send_user_wallet_id"
const val TRANSACTION_ID_KEY = "send_transaction_id"
const val AMOUNT_KEY = "send_amount"
+ const val TAG_KEY = "send_tag"
const val DESTINATION_ADDRESS_KEY = "send_destination_address"
}
}
\ No newline at end of file
diff --git a/features/send/impl/build.gradle.kts b/features/send/impl/build.gradle.kts
index 062932fd25..eb9e87bdf6 100644
--- a/features/send/impl/build.gradle.kts
+++ b/features/send/impl/build.gradle.kts
@@ -69,12 +69,13 @@ dependencies {
implementation(projects.domain.card)
implementation(projects.domain.balanceHiding)
implementation(projects.domain.balanceHiding.models)
+ implementation(projects.domain.qrScanning)
+ implementation(projects.domain.qrScanning.models)
/** Feature modules */
implementation(projects.features.send.api)
implementation(projects.features.tokendetails.api)
implementation(projects.features.qrScanning.api)
- implementation(projects.features.qrScanning.impl)
/** DI */
implementation(deps.hilt.android)
diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/navigation/DefaultSendRouter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/navigation/DefaultSendRouter.kt
index db243390ea..0629979fd1 100644
--- a/features/send/impl/src/main/java/com/tangem/features/send/impl/navigation/DefaultSendRouter.kt
+++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/navigation/DefaultSendRouter.kt
@@ -5,10 +5,10 @@ import androidx.fragment.app.Fragment
import com.tangem.core.navigation.AppScreen
import com.tangem.core.navigation.NavigationAction
import com.tangem.core.navigation.ReduxNavController
+import com.tangem.domain.qrscanning.models.SourceType
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.feature.qrscanning.QrScanningRouter
-import com.tangem.feature.qrscanning.SourceType
import com.tangem.features.send.impl.presentation.SendFragment
import com.tangem.features.tokendetails.navigation.TokenDetailsRouter
diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/SendFragment.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/SendFragment.kt
index 539fd6d734..0a3521b1eb 100644
--- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/SendFragment.kt
+++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/SendFragment.kt
@@ -13,8 +13,8 @@ import com.tangem.core.ui.components.SystemBarsEffect
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.screen.ComposeFragment
import com.tangem.core.ui.theme.AppThemeModeHolder
-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.features.send.api.navigation.SendRouter
import com.tangem.features.send.impl.navigation.InnerSendRouter
import com.tangem.features.send.impl.presentation.state.StateRouter
diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendNotification.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendNotification.kt
index bf5148d51e..64817ff69e 100644
--- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendNotification.kt
+++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendNotification.kt
@@ -23,12 +23,12 @@ internal sealed class SendNotification(val config: NotificationConfig) {
),
) {
- object TotalExceedsBalance : Error(
+ data object TotalExceedsBalance : Error(
title = resourceReference(R.string.send_notification_exceed_balance_title),
subtitle = resourceReference(R.string.send_notification_exceed_balance_text),
)
- object InvalidAmount : Error(
+ data object InvalidAmount : Error(
title = resourceReference(R.string.send_notification_invalid_amount_title),
subtitle = resourceReference(R.string.send_notification_invalid_amount_text),
)
@@ -49,7 +49,7 @@ internal sealed class SendNotification(val config: NotificationConfig) {
val amountLimit: String,
val onConfirmClick: () -> Unit,
) : Error(
- title = resourceReference(R.string.send_notifiaction_transaction_limit_title),
+ title = resourceReference(R.string.send_notification_transaction_limit_title),
subtitle = resourceReference(
R.string.send_notification_transaction_limit_text,
wrappedList(cryptoCurrency, utxoLimit, amountLimit),
@@ -94,17 +94,6 @@ internal sealed class SendNotification(val config: NotificationConfig) {
subtitle = resourceReference(R.string.send_notification_existential_deposit_text, wrappedList(deposit)),
)
- data class NetworkCoverage(
- val amountReducedBy: String,
- val amountReduced: String,
- ) : Warning(
- title = resourceReference(id = R.string.send_network_fee_warning_title),
- subtitle = resourceReference(
- id = R.string.send_network_fee_warning_content,
- formatArgs = wrappedList(amountReducedBy, amountReduced),
- ),
- )
-
data object FeeTooLow : Warning(
title = resourceReference(id = R.string.send_notification_transaction_delay_title),
subtitle = resourceReference(id = R.string.send_notification_transaction_delay_text),
diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendNotificationFactory.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendNotificationFactory.kt
index 20ad57bca1..426ddab71a 100644
--- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendNotificationFactory.kt
+++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendNotificationFactory.kt
@@ -52,7 +52,6 @@ internal class SendNotificationFactory(
addDustWarningNotification(feeAmount, sendAmount)
addTransactionLimitErrorNotification(feeAmount, sendAmount)
// warnings
- addFeeCoverageNotification(sendState.isSubtract, sendAmount)
addExistentialWarningNotification(feeAmount, sendAmount)
addHighFeeWarningNotification(sendAmount, sendState.ignoreAmountReduce)
addTooLowNotification(feeState)
@@ -257,29 +256,6 @@ internal class SendNotificationFactory(
}
}
- private fun MutableList.addFeeCoverageNotification(
- isSubtract: Boolean,
- amountValue: BigDecimal,
- ) {
- val state = currentStateProvider()
- val cryptoCurrency = cryptoCurrencyStatusProvider().currency
- val feeAmount = state.feeState?.fee?.amount?.value ?: BigDecimal.ZERO
-
- val amountReducedValue = amountValue.minus(feeAmount)
- val amountReducedByValue = amountValue.minus(amountReducedValue)
- val amountReducedBy = BigDecimalFormatter.formatCryptoAmount(
- cryptoAmount = amountReducedByValue,
- cryptoCurrency = cryptoCurrency,
- )
- val amountReduced = BigDecimalFormatter.formatCryptoAmount(
- cryptoAmount = amountReducedValue,
- cryptoCurrency = cryptoCurrency,
- )
- if (isSubtract) {
- add(SendNotification.Warning.NetworkCoverage(amountReducedBy, amountReduced))
- }
- }
-
private fun MutableList.addTooLowNotification(feeState: SendStates.FeeState) {
val feeSelectorState = feeState.feeSelectorState as? FeeSelectorState.Content ?: return
val multipleFees = feeSelectorState.fees as? TransactionFee.Choosable ?: return
diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendStateFactory.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendStateFactory.kt
index cbd7ea840b..9b523c44f2 100644
--- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendStateFactory.kt
+++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendStateFactory.kt
@@ -5,6 +5,7 @@ import com.tangem.blockchain.common.TransactionData
import com.tangem.core.ui.components.currency.tokenicon.converter.CryptoCurrencyToIconStateConverter
import com.tangem.core.ui.event.consumedEvent
import com.tangem.core.ui.extensions.resourceReference
+import com.tangem.core.ui.utils.parseBigDecimal
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.txhistory.models.TxHistoryItem
@@ -31,6 +32,7 @@ internal class SendStateFactory(
private val userWalletProvider: Provider,
private val appCurrencyProvider: Provider,
private val cryptoCurrencyStatusProvider: Provider,
+ private val feeCryptoCurrencyStatusProvider: Provider,
private val validateWalletMemoUseCase: ValidateWalletMemoUseCase,
private val getExplorerTransactionUrlUseCase: GetExplorerTransactionUrlUseCase,
) {
@@ -62,7 +64,7 @@ internal class SendStateFactory(
private val feeStateConverter by lazy(LazyThreadSafetyMode.NONE) {
SendFeeStateConverter(
appCurrencyProvider = appCurrencyProvider,
- cryptoCurrencyStatusProvider = cryptoCurrencyStatusProvider,
+ feeCryptoCurrencyStatusProvider = feeCryptoCurrencyStatusProvider,
)
}
@@ -85,16 +87,18 @@ internal class SendStateFactory(
val state = currentStateProvider()
return state.copy(
amountState = state.amountState ?: amountStateConverter.convert(""),
- recipientState = state.recipientState ?: recipientStateConverter.convert(""),
+ recipientState = state.recipientState
+ ?: recipientStateConverter.convert(SendRecipientStateConverter.Data("", null)),
feeState = state.feeState ?: feeStateConverter.convert(Unit),
)
}
- fun getReadyState(amount: String, destinationAddress: String): SendUiState {
+ fun getReadyState(amount: String, destinationAddress: String, memo: String?): SendUiState {
val state = currentStateProvider()
return state.copy(
amountState = state.amountState ?: amountStateConverter.convert(amount),
- recipientState = state.recipientState ?: recipientStateConverter.convert(destinationAddress),
+ recipientState = state.recipientState
+ ?: recipientStateConverter.convert(SendRecipientStateConverter.Data(destinationAddress, memo)),
feeState = state.feeState ?: feeStateConverter.convert(Unit),
isEditingDisabled = true,
)
@@ -214,9 +218,20 @@ internal class SendStateFactory(
//endregion
//region send
- fun onSubtractSelect(isSubtract: Boolean): SendUiState {
+ fun onSubtractSelect(isSubtract: Boolean, isAmountSubtractAvailable: Boolean): SendUiState {
val state = currentStateProvider()
+ val fee = state.feeState?.fee ?: return state
+ val amountState = state.amountState ?: return state
+ val amount = amountState.amountTextField.cryptoAmount
+ val amountValue = amount.value ?: return state
+ val amountToSend = if (isSubtract && isAmountSubtractAvailable) {
+ val feeValue = fee.amount.value ?: return state
+ amountValue.minus(feeValue)
+ } else {
+ amountValue
+ }
return state.copy(
+ amountState = amountStateConverter.convert(amountToSend.parseBigDecimal(amount.decimals)),
sendState = state.sendState.copy(isSubtract = isSubtract),
)
}
diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/amount/SendAmountStateConverter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/amount/SendAmountStateConverter.kt
index bbadf98185..e1c163e2d4 100644
--- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/amount/SendAmountStateConverter.kt
+++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/amount/SendAmountStateConverter.kt
@@ -14,6 +14,7 @@ import com.tangem.features.send.impl.presentation.state.SendStates
import com.tangem.features.send.impl.presentation.state.fields.SendAmountFieldConverter
import com.tangem.utils.Provider
import com.tangem.utils.converter.Converter
+import com.tangem.utils.isNullOrZero
import kotlinx.collections.immutable.persistentListOf
internal class SendAmountStateConverter(
@@ -37,18 +38,22 @@ internal class SendAmountStateConverter(
tokenIconState = iconStateConverter.convert(status),
amountTextField = sendAmountFieldConverter.convert(value),
isPrimaryButtonEnabled = false,
- segmentedButtonConfig = persistentListOf(
- SendAmountSegmentedButtonsConfig(
- title = stringReference(status.currency.symbol),
- iconState = iconStateConverter.convert(status),
- isFiat = false,
- ),
- SendAmountSegmentedButtonsConfig(
- title = stringReference(appCurrency.code),
- iconUrl = appCurrency.iconSmallUrl,
- isFiat = true,
- ),
- ),
+ segmentedButtonConfig = if (status.value.fiatRate.isNullOrZero()) {
+ persistentListOf()
+ } else {
+ persistentListOf(
+ SendAmountSegmentedButtonsConfig(
+ title = stringReference(status.currency.symbol),
+ iconState = iconStateConverter.convert(status),
+ isFiat = false,
+ ),
+ SendAmountSegmentedButtonsConfig(
+ title = stringReference(appCurrency.code),
+ iconUrl = appCurrency.iconSmallUrl,
+ isFiat = true,
+ ),
+ )
+ },
)
}
}
\ No newline at end of file
diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/FeeConverter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/FeeConverter.kt
index 7b55e0100d..bd8e832a91 100644
--- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/FeeConverter.kt
+++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/FeeConverter.kt
@@ -13,14 +13,14 @@ import com.tangem.utils.converter.Converter
internal class FeeConverter(
private val clickIntents: SendClickIntents,
private val appCurrencyProvider: Provider,
- private val cryptoCurrencyStatusProvider: Provider,
+ private val feeCryptoCurrencyStatusProvider: Provider,
) : Converter {
private val ethereumCustomFeeConverter by lazy {
EthereumCustomFeeConverter(
clickIntents = clickIntents,
appCurrencyProvider = appCurrencyProvider,
- cryptoCurrencyStatusProvider = cryptoCurrencyStatusProvider,
+ feeCryptoCurrencyStatusProvider = feeCryptoCurrencyStatusProvider,
)
}
diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/FeeStateFactory.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/FeeStateFactory.kt
index 4154ae14a3..6a2804103f 100644
--- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/FeeStateFactory.kt
+++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/FeeStateFactory.kt
@@ -20,7 +20,7 @@ import kotlinx.collections.immutable.persistentListOf
internal class FeeStateFactory(
private val clickIntents: SendClickIntents,
private val currentStateProvider: Provider,
- private val cryptoCurrencyStatusProvider: Provider,
+ private val feeCryptoCurrencyStatusProvider: Provider,
private val appCurrencyProvider: Provider,
private val isFeeApproximateUseCase: IsFeeApproximateUseCase,
) {
@@ -28,7 +28,7 @@ internal class FeeStateFactory(
SendFeeCustomFieldConverter(
clickIntents = clickIntents,
appCurrencyProvider = appCurrencyProvider,
- cryptoCurrencyStatusProvider = cryptoCurrencyStatusProvider,
+ feeCryptoCurrencyStatusProvider = feeCryptoCurrencyStatusProvider,
)
}
@@ -36,7 +36,7 @@ internal class FeeStateFactory(
FeeConverter(
clickIntents = clickIntents,
appCurrencyProvider = appCurrencyProvider,
- cryptoCurrencyStatusProvider = cryptoCurrencyStatusProvider,
+ feeCryptoCurrencyStatusProvider = feeCryptoCurrencyStatusProvider,
)
}
@@ -140,7 +140,7 @@ internal class FeeStateFactory(
}
private fun isFeeApproximate(fee: Fee): Boolean {
- val cryptoCurrencyStatus = cryptoCurrencyStatusProvider()
+ val cryptoCurrencyStatus = feeCryptoCurrencyStatusProvider()
return isFeeApproximateUseCase(
networkId = cryptoCurrencyStatus.currency.network.id,
amountType = fee.amount.type,
diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/SendFeeCustomFieldConverter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/SendFeeCustomFieldConverter.kt
index 24c85ee305..cdd0fa60e6 100644
--- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/SendFeeCustomFieldConverter.kt
+++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/SendFeeCustomFieldConverter.kt
@@ -14,14 +14,14 @@ import kotlinx.collections.immutable.persistentListOf
internal class SendFeeCustomFieldConverter(
private val clickIntents: SendClickIntents,
private val appCurrencyProvider: Provider,
- private val cryptoCurrencyStatusProvider: Provider,
+ private val feeCryptoCurrencyStatusProvider: Provider,
) : Converter> {
private val ethereumCustomFeeConverter by lazy {
EthereumCustomFeeConverter(
clickIntents = clickIntents,
appCurrencyProvider = appCurrencyProvider,
- cryptoCurrencyStatusProvider = cryptoCurrencyStatusProvider,
+ feeCryptoCurrencyStatusProvider = feeCryptoCurrencyStatusProvider,
)
}
diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/SendFeeStateConverter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/SendFeeStateConverter.kt
index 2659080a08..864ae1b501 100644
--- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/SendFeeStateConverter.kt
+++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/SendFeeStateConverter.kt
@@ -9,16 +9,15 @@ import kotlinx.collections.immutable.persistentListOf
internal class SendFeeStateConverter(
private val appCurrencyProvider: Provider,
- private val cryptoCurrencyStatusProvider: Provider,
+ private val feeCryptoCurrencyStatusProvider: Provider,
) : Converter {
override fun convert(value: Unit): SendStates.FeeState {
- val cryptoCurrencyStatus = cryptoCurrencyStatusProvider()
return SendStates.FeeState(
feeSelectorState = FeeSelectorState.Loading,
fee = null,
notifications = persistentListOf(),
- rate = cryptoCurrencyStatus.value.fiatRate,
+ rate = feeCryptoCurrencyStatusProvider().value.fiatRate,
appCurrency = appCurrencyProvider(),
isFeeApproximate = false,
)
diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/custom/EthereumCustomFeeConverter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/custom/EthereumCustomFeeConverter.kt
index bf12d6f398..950dd0b135 100644
--- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/custom/EthereumCustomFeeConverter.kt
+++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/custom/EthereumCustomFeeConverter.kt
@@ -28,7 +28,7 @@ import java.math.RoundingMode
internal class EthereumCustomFeeConverter(
private val clickIntents: SendClickIntents,
private val appCurrencyProvider: Provider,
- private val cryptoCurrencyStatusProvider: Provider,
+ private val feeCryptoCurrencyStatusProvider: Provider,
) : Converter> {
override fun convert(value: Fee.Ethereum): ImmutableList {
@@ -152,7 +152,7 @@ internal class EthereumCustomFeeConverter(
private fun getFeeFormatted(fee: BigDecimal?): TextReference {
val appCurrency = appCurrencyProvider()
- val rate = cryptoCurrencyStatusProvider().value.fiatRate
+ val rate = feeCryptoCurrencyStatusProvider().value.fiatRate
val fiatFee = rate?.let { fee?.multiply(it) }
return stringReference(
BigDecimalFormatter.formatFiatAmount(
@@ -164,7 +164,7 @@ internal class EthereumCustomFeeConverter(
}
private fun checkExceedBalance(feeAmount: BigDecimal?): Boolean {
- val cryptoCurrencyStatus = cryptoCurrencyStatusProvider()
+ val cryptoCurrencyStatus = feeCryptoCurrencyStatusProvider()
val currencyCryptoAmount = cryptoCurrencyStatus.value.amount ?: BigDecimal.ZERO
return feeAmount == null || feeAmount.isZero() || feeAmount > currencyCryptoAmount
diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fields/SendAmountFieldConverter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fields/SendAmountFieldConverter.kt
index ff622738f7..37bdcf2eae 100644
--- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fields/SendAmountFieldConverter.kt
+++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fields/SendAmountFieldConverter.kt
@@ -31,10 +31,11 @@ internal class SendAmountFieldConverter(
val cryptoCurrencyStatus = cryptoCurrencyStatusProvider()
val cryptoDecimal = value.toBigDecimalOrDefault()
val cryptoAmount = cryptoDecimal.convertToAmount(cryptoCurrencyStatus.currency)
+ val fiatRate = cryptoCurrencyStatus.value.fiatRate
val fiatValue = if (value.isEmpty()) {
""
} else {
- val fiatDecimal = cryptoCurrencyStatus.value.fiatRate?.multiply(cryptoDecimal) ?: BigDecimal.ZERO
+ val fiatDecimal = fiatRate?.multiply(cryptoDecimal) ?: BigDecimal.ZERO
fiatDecimal.parseBigDecimal(FIAT_DECIMALS)
}
val isDoneActionEnabled = !cryptoDecimal.isZero()
@@ -52,6 +53,7 @@ internal class SendAmountFieldConverter(
fiatAmount = getAppCurrencyAmount(appCurrencyProvider()),
isError = false,
error = TextReference.Res(R.string.swapping_insufficient_funds),
+ isFiatUnavailable = fiatRate == null,
)
}
diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fields/SendTextField.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fields/SendTextField.kt
index 97682a9e21..8db8a2fc02 100644
--- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fields/SendTextField.kt
+++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fields/SendTextField.kt
@@ -27,6 +27,7 @@ internal sealed class SendTextField {
val fiatAmount: Amount,
val isFiatValue: Boolean,
val fiatValue: String,
+ val isFiatUnavailable: Boolean,
val isError: Boolean,
val error: TextReference,
) : SendTextField()
diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/recipient/SendRecipientMemoFieldConverter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/recipient/SendRecipientMemoFieldConverter.kt
index 1eeffb3762..a539d72859 100644
--- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/recipient/SendRecipientMemoFieldConverter.kt
+++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/recipient/SendRecipientMemoFieldConverter.kt
@@ -1,5 +1,6 @@
package com.tangem.features.send.impl.presentation.state.recipient
+import androidx.annotation.StringRes
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
@@ -15,13 +16,18 @@ import com.tangem.utils.converter.Converter
internal class SendRecipientMemoFieldConverter(
private val clickIntents: SendClickIntents,
private val cryptoCurrencyStatus: Provider,
-) : Converter {
+) : Converter {
- fun convertOrNull(): SendTextField.RecipientMemo? {
+ fun convertOrNull(memoValue: String?): SendTextField.RecipientMemo? {
val cryptoCurrency = cryptoCurrencyStatus().currency
+ val memo = memoValue ?: ""
return when (cryptoCurrency.network.id.value) {
- Blockchain.XRP.id -> convert(R.string.send_destination_tag_field)
+ Blockchain.XRP.id -> {
+ convert(
+ value = Data(memo = memo, label = R.string.send_destination_tag_field),
+ )
+ }
Blockchain.Binance.id,
Blockchain.TON.id,
Blockchain.Cosmos.id,
@@ -30,24 +36,30 @@ internal class SendRecipientMemoFieldConverter(
Blockchain.Stellar.id,
Blockchain.Hedera.id,
Blockchain.Algorand.id,
- -> convert(R.string.send_extras_hint_memo)
+ -> {
+ convert(
+ value = Data(memo = memo, label = R.string.send_extras_hint_memo),
+ )
+ }
else -> null
}
}
- override fun convert(value: Int): SendTextField.RecipientMemo {
+ override fun convert(value: Data): SendTextField.RecipientMemo {
return SendTextField.RecipientMemo(
- value = "",
+ value = value.memo,
onValueChange = clickIntents::onRecipientMemoValueChange,
keyboardOptions = KeyboardOptions(
imeAction = ImeAction.Done,
keyboardType = KeyboardType.Text,
),
placeholder = resourceReference(R.string.send_optional_field),
- label = resourceReference(value),
+ label = resourceReference(value.label),
error = resourceReference(R.string.send_memo_destination_tag_error),
disabledText = resourceReference(R.string.send_additional_field_already_included),
isEnabled = true,
)
}
+
+ data class Data(val memo: String, @StringRes val label: Int)
}
\ No newline at end of file
diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/recipient/SendRecipientStateConverter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/recipient/SendRecipientStateConverter.kt
index ae375c97e2..dd7db52c45 100644
--- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/recipient/SendRecipientStateConverter.kt
+++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/recipient/SendRecipientStateConverter.kt
@@ -10,7 +10,7 @@ import kotlinx.collections.immutable.persistentListOf
internal class SendRecipientStateConverter(
private val clickIntents: SendClickIntents,
private val cryptoCurrencyStatusProvider: Provider,
-) : Converter {
+) : Converter {
private val addressFieldConverter by lazy { SendRecipientAddressFieldConverter(clickIntents) }
private val memoFieldConverter by lazy {
@@ -20,14 +20,16 @@ internal class SendRecipientStateConverter(
)
}
- override fun convert(value: String): SendStates.RecipientState {
+ override fun convert(value: Data): SendStates.RecipientState {
return SendStates.RecipientState(
- addressTextField = addressFieldConverter.convert(value),
- memoTextField = memoFieldConverter.convertOrNull(),
+ addressTextField = addressFieldConverter.convert(value.address),
+ memoTextField = memoFieldConverter.convertOrNull(value.memo),
network = cryptoCurrencyStatusProvider().currency.network.name,
isPrimaryButtonEnabled = false,
wallets = persistentListOf(),
recent = persistentListOf(),
)
}
+
+ data class Data(val address: String, val memo: String? = null)
}
\ No newline at end of file
diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/amount/AmountField.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/amount/AmountField.kt
index bd6b8b54e1..d9ef26bd3c 100644
--- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/amount/AmountField.kt
+++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/amount/AmountField.kt
@@ -11,10 +11,12 @@ import androidx.compose.ui.Alignment.Companion.BottomCenter
import androidx.compose.ui.Alignment.Companion.TopCenter
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.text.style.TextDirection
import com.tangem.core.ui.components.fields.AmountTextField
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.BigDecimalFormatter
import com.tangem.core.ui.utils.defaultFormat
import com.tangem.core.ui.utils.rememberDecimalFormat
import com.tangem.features.send.impl.presentation.state.fields.SendTextField
@@ -62,10 +64,14 @@ internal fun AmountField(sendField: SendTextField.AmountField, isFiat: Boolean)
end = TangemTheme.dimens.spacing12,
),
) {
- val text = "${secondaryValue.ifEmpty { decimalFormat.defaultFormat() }} ${secondaryAmount.currencySymbol}"
+ val text = if (sendField.isFiatUnavailable) {
+ BigDecimalFormatter.EMPTY_BALANCE_SIGN
+ } else {
+ "${secondaryValue.ifEmpty { decimalFormat.defaultFormat() }} ${secondaryAmount.currencySymbol}"
+ }
Text(
text = text,
- style = TangemTheme.typography.caption2,
+ style = TangemTheme.typography.caption2.copy(textDirection = TextDirection.ContentOrLtr),
color = TangemTheme.colors.text.tertiary,
textAlign = TextAlign.Center,
modifier = Modifier
diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/amount/SendAmountContent.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/amount/SendAmountContent.kt
index fa0a7445eb..143b2e1e16 100644
--- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/amount/SendAmountContent.kt
+++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/amount/SendAmountContent.kt
@@ -7,15 +7,17 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
+import androidx.compose.ui.hapticfeedback.HapticFeedbackType
+import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.res.stringResource
import com.tangem.core.ui.components.SecondaryButton
+import com.tangem.core.ui.components.SpacerWMax
import com.tangem.core.ui.components.buttons.common.TangemButtonSize
import com.tangem.core.ui.components.buttons.segmentedbutton.SegmentedButtons
import com.tangem.core.ui.components.currency.fiaticon.FiatIcon
import com.tangem.core.ui.components.currency.tokenicon.TokenIcon
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.TangemTheme
-
import com.tangem.features.send.impl.R
import com.tangem.features.send.impl.presentation.state.SendStates
import com.tangem.features.send.impl.presentation.state.amount.SendAmountSegmentedButtonsConfig
@@ -28,6 +30,7 @@ internal fun SendAmountContent(
clickIntents: SendClickIntents,
) {
if (amountState == null) return
+ val hapticFeedback = LocalHapticFeedback.current
Column(
modifier = Modifier
.background(TangemTheme.colors.background.tertiary),
@@ -41,19 +44,29 @@ internal fun SendAmountContent(
end = TangemTheme.dimens.spacing16,
),
) {
- SegmentedButtons(
- modifier = Modifier
- .weight(1f)
- .height(TangemTheme.dimens.size40),
- config = amountState.segmentedButtonConfig,
- showIndication = false,
- onClick = { clickIntents.onCurrencyChangeClick(it.isFiat) },
- ) {
- SendAmountCurrencyButton(it)
+ if (amountState.segmentedButtonConfig.isNotEmpty()) {
+ SegmentedButtons(
+ modifier = Modifier
+ .weight(1f)
+ .height(TangemTheme.dimens.size40),
+ config = amountState.segmentedButtonConfig,
+ showIndication = false,
+ onClick = {
+ hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
+ clickIntents.onCurrencyChangeClick(it.isFiat)
+ },
+ ) {
+ SendAmountCurrencyButton(it)
+ }
+ } else {
+ SpacerWMax()
}
SecondaryButton(
text = stringResource(R.string.send_max_amount),
- onClick = clickIntents::onMaxValueClick,
+ onClick = {
+ hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
+ clickIntents.onMaxValueClick()
+ },
size = TangemButtonSize.Text,
shape = RoundedCornerShape(TangemTheme.dimens.radius26),
modifier = Modifier
diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendSpeedAndFeeContent.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendSpeedAndFeeContent.kt
index 1695763049..444d03e9a6 100644
--- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendSpeedAndFeeContent.kt
+++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendSpeedAndFeeContent.kt
@@ -18,7 +18,6 @@ import com.tangem.features.send.impl.presentation.state.fee.FeeSelectorState
import com.tangem.features.send.impl.presentation.state.fee.SendFeeNotification
import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents
import kotlinx.collections.immutable.ImmutableList
-import kotlinx.collections.immutable.toImmutableList
private const val FEE_SELECTOR_KEY = "FEE_SELECTOR_KEY"
private const val FEE_CUSTOM_KEY = "FEE_CUSTOM_KEY"
@@ -37,9 +36,8 @@ internal fun SendSpeedAndFeeContent(state: SendStates.FeeState?, clickIntents: S
),
) {
feeSelector(state, clickIntents)
- topNotifications(notifications)
customFee(feeSendState)
- middleNotifications(notifications)
+ notifications(notifications)
}
}
@@ -56,29 +54,6 @@ private fun LazyListScope.feeSelector(state: SendStates.FeeState, clickIntents:
}
}
-private fun LazyListScope.topNotifications(
- configs: ImmutableList,
- modifier: Modifier = Modifier,
-) {
- notifications(
- configs = configs.filter {
- it is SendFeeNotification.Error.ExceedsBalance ||
- it is SendFeeNotification.Warning.NetworkFeeUnreachable
- }.toImmutableList(),
- modifier = modifier,
- )
-}
-
-private fun LazyListScope.middleNotifications(
- configs: ImmutableList,
- modifier: Modifier = Modifier,
-) {
- notifications(
- configs = configs.filterIsInstance().toImmutableList(),
- modifier = modifier,
- )
-}
-
@OptIn(ExperimentalFoundationApi::class)
private fun LazyListScope.notifications(
configs: ImmutableList,
diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt
index 621edfae39..0617f6e83b 100644
--- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt
+++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt
@@ -12,9 +12,12 @@ import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase
+import com.tangem.domain.common.util.cardTypesResolver
+import com.tangem.domain.qrscanning.usecases.ParseQrCodeUseCase
import com.tangem.domain.redux.LegacyAction
import com.tangem.domain.redux.ReduxStateHolder
import com.tangem.domain.tokens.*
+import com.tangem.domain.tokens.error.CurrencyStatusError
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.tokens.repository.CurrencyChecksRepository
@@ -30,7 +33,10 @@ import com.tangem.domain.txhistory.usecase.GetFixedTxHistoryItemsUseCase
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.models.UserWalletId
-import com.tangem.domain.wallets.usecase.*
+import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
+import com.tangem.domain.wallets.usecase.GetWalletsUseCase
+import com.tangem.domain.wallets.usecase.ValidateWalletAddressUseCase
+import com.tangem.domain.wallets.usecase.ValidateWalletMemoUseCase
import com.tangem.features.send.api.navigation.SendRouter
import com.tangem.features.send.impl.navigation.InnerSendRouter
import com.tangem.features.send.impl.presentation.analytics.EnterAddressSource
@@ -66,17 +72,19 @@ internal class SendViewModel @Inject constructor(
private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
private val getWalletsUseCase: GetWalletsUseCase,
private val getCryptoCurrenciesUseCase: GetCryptoCurrenciesUseCase,
+ private val getPrimaryCurrencyStatusUpdatesUseCase: GetPrimaryCurrencyStatusUpdatesUseCase,
+ private val getFeePaidCryptoCurrencyStatusSyncUseCase: GetFeePaidCryptoCurrencyStatusSyncUseCase,
private val getFixedTxHistoryItemsUseCase: GetFixedTxHistoryItemsUseCase,
private val getFeeUseCase: GetFeeUseCase,
private val sendTransactionUseCase: SendTransactionUseCase,
private val createTransactionUseCase: CreateTransactionUseCase,
private val validateWalletAddressUseCase: ValidateWalletAddressUseCase,
- private val parseSharedAddressUseCase: ParseSharedAddressUseCase,
private val walletManagersFacade: WalletManagersFacade,
private val reduxStateHolder: ReduxStateHolder,
private val isAmountSubtractAvailableUseCase: IsAmountSubtractAvailableUseCase,
private val getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase,
private val analyticsEventHandler: AnalyticsEventHandler,
+ private val parseQrCodeUseCase: ParseQrCodeUseCase,
currencyChecksRepository: CurrencyChecksRepository,
isFeeApproximateUseCase: IsFeeApproximateUseCase,
getExplorerTransactionUrlUseCase: GetExplorerTransactionUrlUseCase,
@@ -95,6 +103,7 @@ internal class SendViewModel @Inject constructor(
private val transactionId: String? = savedStateHandle[SendRouter.TRANSACTION_ID_KEY]
private val amount: String? = savedStateHandle[SendRouter.AMOUNT_KEY]
private val destinationAddress: String? = savedStateHandle[SendRouter.DESTINATION_ADDRESS_KEY]
+ private val memo: String? = savedStateHandle[SendRouter.TAG_KEY]
private val selectedAppCurrencyFlow: StateFlow = createSelectedAppCurrencyFlow()
@@ -108,6 +117,7 @@ internal class SendViewModel @Inject constructor(
userWalletProvider = Provider { userWallet },
appCurrencyProvider = Provider(selectedAppCurrencyFlow::value),
cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus },
+ feeCryptoCurrencyStatusProvider = Provider { feeCryptoCurrencyStatus },
validateWalletMemoUseCase = validateWalletMemoUseCase,
getExplorerTransactionUrlUseCase = getExplorerTransactionUrlUseCase,
)
@@ -120,7 +130,7 @@ internal class SendViewModel @Inject constructor(
private val feeStateFactory = FeeStateFactory(
clickIntents = this,
currentStateProvider = Provider { uiState },
- cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus },
+ feeCryptoCurrencyStatusProvider = Provider { feeCryptoCurrencyStatus },
appCurrencyProvider = Provider(selectedAppCurrencyFlow::value),
isFeeApproximateUseCase = isFeeApproximateUseCase,
)
@@ -165,12 +175,14 @@ internal class SendViewModel @Inject constructor(
private var isAmountSubtractAvailable: Boolean = false
private var coinCryptoCurrencyStatus: CryptoCurrencyStatus by Delegates.notNull()
private var cryptoCurrencyStatus: CryptoCurrencyStatus by Delegates.notNull()
+ private var feeCryptoCurrencyStatus: CryptoCurrencyStatus by Delegates.notNull()
private var balanceJobHolder = JobHolder()
private var balanceHidingJobHolder = JobHolder()
private var recipientsJobHolder = JobHolder()
private var feeJobHolder = JobHolder()
private var addressValidationJobHolder = JobHolder()
+ private var memoValidationJobHolder = JobHolder()
private var sendNotificationsJobHolder = JobHolder()
private var feeNotificationsJobHolder = JobHolder()
private var qrScannerJobHolder = JobHolder()
@@ -204,7 +216,13 @@ internal class SendViewModel @Inject constructor(
ifRight = { wallet ->
userWallet = wallet
checkIfSubtractAvailable()
- getCurrenciesStatusUpdates(wallet)
+
+ val isSingleWalletWithToken = wallet.scanResponse.cardTypesResolver.isSingleWalletWithToken()
+ val isMultiCurrency = wallet.isMultiCurrency
+ getCurrenciesStatusUpdates(
+ isSingleWalletWithToken = isSingleWalletWithToken,
+ isMultiCurrency = isMultiCurrency,
+ )
},
ifLeft = {
uiState = eventStateFactory.getGenericErrorState(
@@ -227,31 +245,38 @@ internal class SendViewModel @Inject constructor(
.saveIn(balanceHidingJobHolder)
}
- private fun getCurrenciesStatusUpdates(wallet: UserWallet) {
- val isSingleWallet = wallet.scanResponse.walletData?.token != null && !wallet.isMultiCurrency
-
+ private fun getCurrenciesStatusUpdates(isSingleWalletWithToken: Boolean, isMultiCurrency: Boolean) {
if (cryptoCurrency is CryptoCurrency.Coin) {
- getCurrencyStatusUpdates(isSingleWallet = isSingleWallet)
- .onEach { currencyStatus ->
- currencyStatus.onRight {
- onDataLoaded(
- currencyStatus = it,
- coinCurrencyStatus = it,
- )
- }
+ getCurrencyStatusUpdates(
+ isSingleWalletWithToken = isSingleWalletWithToken,
+ isMultiCurrency = isMultiCurrency,
+ ).onEach { currencyStatus ->
+ currencyStatus.onRight {
+ onDataLoaded(
+ currencyStatus = it,
+ coinCurrencyStatus = it,
+ feeCurrencyStatus = getFeeCurrencyStatusSync(it, isMultiCurrency),
+ )
}
+ }
.flowOn(dispatchers.main)
.launchIn(viewModelScope)
.saveIn(balanceJobHolder)
} else {
combine(
- flow = getCoinCurrencyStatusUpdates(isSingleWallet = isSingleWallet),
- flow2 = getCurrencyStatusUpdates(isSingleWallet = isSingleWallet),
- ) { coinStatus, currencyStatus ->
- if (coinStatus.isRight() && currencyStatus.isRight()) {
+ flow = getCoinCurrencyStatusUpdates(isSingleWalletWithToken),
+ flow2 = getCurrencyStatusUpdates(
+ isSingleWalletWithToken = isSingleWalletWithToken,
+ isMultiCurrency = isMultiCurrency,
+ ),
+ ) { maybeCoinStatus, maybeCurrencyStatus ->
+ if (maybeCoinStatus.isRight() && maybeCurrencyStatus.isRight()) {
+ val currencyStatus = maybeCurrencyStatus.getOrElse { error("Currency status is unreachable") }
+ val coinStatus = maybeCoinStatus.getOrElse { error("Coin status is unreachable") }
onDataLoaded(
- currencyStatus = currencyStatus.getOrElse { error("Currency status is unreachable") },
- coinCurrencyStatus = coinStatus.getOrElse { error("Coin status is unreachable") },
+ currencyStatus = currencyStatus,
+ coinCurrencyStatus = coinStatus,
+ feeCurrencyStatus = getFeeCurrencyStatusSync(currencyStatus, isMultiCurrency),
)
}
}
@@ -261,18 +286,41 @@ internal class SendViewModel @Inject constructor(
}
}
- private fun getCoinCurrencyStatusUpdates(isSingleWallet: Boolean) = getNetworkCoinStatusUseCase(
+ private fun getCoinCurrencyStatusUpdates(isSingleWalletWithToken: Boolean) = getNetworkCoinStatusUseCase(
userWalletId = userWalletId,
networkId = cryptoCurrency.network.id,
derivationPath = cryptoCurrency.network.derivationPath,
- isSingleWalletWithTokens = isSingleWallet,
+ isSingleWalletWithTokens = isSingleWalletWithToken,
).conflate().distinctUntilChanged()
- private fun getCurrencyStatusUpdates(isSingleWallet: Boolean) = getCurrencyStatusUpdatesUseCase(
- userWalletId = userWalletId,
- currencyId = cryptoCurrency.id,
- isSingleWalletWithTokens = isSingleWallet,
- ).conflate().distinctUntilChanged()
+ private fun getCurrencyStatusUpdates(
+ isSingleWalletWithToken: Boolean,
+ isMultiCurrency: Boolean,
+ ): Flow> {
+ return if (isMultiCurrency) {
+ getCurrencyStatusUpdatesUseCase(
+ userWalletId = userWalletId,
+ currencyId = cryptoCurrency.id,
+ isSingleWalletWithTokens = isSingleWalletWithToken,
+ ).conflate().distinctUntilChanged()
+ } else {
+ getPrimaryCurrencyStatusUpdatesUseCase(userWalletId = userWalletId)
+ }
+ }
+
+ private suspend fun getFeeCurrencyStatusSync(
+ cryptoCurrencyStatus: CryptoCurrencyStatus,
+ isMultiCurrency: Boolean,
+ ): CryptoCurrencyStatus {
+ return if (isMultiCurrency) {
+ getFeePaidCryptoCurrencyStatusSyncUseCase(
+ userWalletId = userWalletId,
+ cryptoCurrencyStatus = cryptoCurrencyStatus,
+ ).getOrNull() ?: error("Fee currency is unreachable")
+ } else {
+ cryptoCurrencyStatus
+ }
+ }
private fun createSelectedAppCurrencyFlow(): StateFlow {
return getSelectedAppCurrencyUseCase()
@@ -286,19 +334,31 @@ internal class SendViewModel @Inject constructor(
)
}
- private fun onDataLoaded(currencyStatus: CryptoCurrencyStatus, coinCurrencyStatus: CryptoCurrencyStatus) {
+ private fun onDataLoaded(
+ currencyStatus: CryptoCurrencyStatus,
+ coinCurrencyStatus: CryptoCurrencyStatus,
+ feeCurrencyStatus: CryptoCurrencyStatus,
+ ) {
cryptoCurrencyStatus = currencyStatus
coinCryptoCurrencyStatus = coinCurrencyStatus
+ feeCryptoCurrencyStatus = feeCurrencyStatus
- if (transactionId != null && amount != null && destinationAddress != null) {
- uiState = stateFactory.getReadyState(amount, destinationAddress)
- stateRouter.showFee()
- } else {
- getWalletsAndRecent()
- uiState = stateFactory.getReadyState()
- stateRouter.showRecipient()
+ when {
+ uiState.sendState.isSuccess -> {
+ stateRouter.showSend()
+ }
+ transactionId != null && amount != null && destinationAddress != null -> {
+ uiState = stateFactory.getReadyState(amount, destinationAddress, memo)
+ stateRouter.showFee()
+ updateNotifications()
+ }
+ else -> {
+ getWalletsAndRecent()
+ uiState = stateFactory.getReadyState()
+ stateRouter.showRecipient()
+ updateNotifications()
+ }
}
- updateNotifications()
}
private fun getWalletsAndRecent() {
@@ -406,7 +466,7 @@ internal class SendViewModel @Inject constructor(
)
return
} else {
- uiState = stateFactory.onSubtractSelect(false)
+ uiState = stateFactory.onSubtractSelect(false, isAmountSubtractAvailable)
analyticsEventHandler.send(SendAnalyticEvents.SubtractFromAmount(false))
}
if (checkIfFeeTooLow(uiState)) {
@@ -454,13 +514,14 @@ internal class SendViewModel @Inject constructor(
// region recipient state clicks
fun onRecipientAddressScanned(address: String) {
viewModelScope.launch(dispatchers.main) {
- parseSharedAddressUseCase(address, cryptoCurrency.network).fold(
+ parseQrCodeUseCase(address, cryptoCurrency).fold(
ifRight = { parsedCode ->
onRecipientAddressValueChange(parsedCode.address, EnterAddressSource.QRCode)
parsedCode.amount?.let { onAmountValueChange(it.toPlainString()) }
parsedCode.memo?.let { onRecipientMemoValueChange(it) }
},
ifLeft = {
+ onRecipientAddressValueChange(address, EnterAddressSource.QRCode)
Timber.w(it)
},
)
@@ -475,6 +536,7 @@ internal class SendViewModel @Inject constructor(
val isValidAddress = validateAddress(value)
uiState = stateFactory.getOnRecipientAddressValidState(value, isValidAddress)
type?.let { analyticsEventHandler.send(SendAnalyticEvents.AddressEntered(it, isValidAddress)) }
+ autoNextFromRecipient(type, isValidAddress)
}
}.saveIn(addressValidationJobHolder)
}
@@ -487,7 +549,7 @@ internal class SendViewModel @Inject constructor(
val isValidAddress = validateAddress(uiState.recipientState?.addressTextField?.value.orEmpty())
uiState = stateFactory.getOnRecipientMemoValidState(value, isValidAddress)
}
- }.saveIn(addressValidationJobHolder)
+ }.saveIn(memoValidationJobHolder)
}
private suspend fun validateAddress(value: String): Boolean {
@@ -519,6 +581,12 @@ internal class SendViewModel @Inject constructor(
),
)
}
+
+ private fun autoNextFromRecipient(type: EnterAddressSource?, isValidAddress: Boolean) {
+ val isRecent = type == EnterAddressSource.RecentAddress
+ val isAddressOnly = uiState.recipientState?.memoTextField == null
+ if (isRecent && isAddressOnly && isValidAddress) onNextClick()
+ }
// endregion
// region fee
@@ -538,7 +606,7 @@ internal class SendViewModel @Inject constructor(
}
override fun onSubtractSelect() {
- uiState = stateFactory.onSubtractSelect(true)
+ uiState = stateFactory.onSubtractSelect(true, isAmountSubtractAvailable)
stateRouter.showSend()
analyticsEventHandler.send(SendAnalyticEvents.SubtractFromAmount(true))
}
@@ -645,16 +713,10 @@ internal class SendViewModel @Inject constructor(
val fee = feeState.fee ?: return
val memo = uiState.recipientState?.memoTextField?.value
val amountValue = uiState.amountState?.amountTextField?.cryptoAmount?.value ?: return
- val amountToSend = if (uiState.sendState.isSubtract && isAmountSubtractAvailable) {
- val feeValue = fee.amount.value ?: return
- amountValue.minus(feeValue)
- } else {
- amountValue
- }
viewModelScope.launch(dispatchers.main) {
createTransactionUseCase(
- amount = amountToSend.convertToAmount(cryptoCurrency),
+ amount = amountValue.convertToAmount(cryptoCurrency),
fee = fee,
memo = memo,
destination = recipient,
diff --git a/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/DataError.kt b/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/DataError.kt
index 21c40c5588..d4881c27b8 100644
--- a/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/DataError.kt
+++ b/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/DataError.kt
@@ -40,7 +40,7 @@ sealed class DataError {
data class InvalidPayoutAddressError(override val code: Int = 992) : DataError()
- object UnknownError : DataError() {
+ data object UnknownError : DataError() {
override val code: Int = -1
}
}
\ No newline at end of file
diff --git a/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/domain/Warning.kt b/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/domain/Warning.kt
index d7f7790479..318b5635de 100644
--- a/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/domain/Warning.kt
+++ b/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/domain/Warning.kt
@@ -7,4 +7,6 @@ sealed class Warning {
data class ExistentialDepositWarning(val existentialDeposit: BigDecimal) : Warning()
data class MinAmountWarning(val dustValue: BigDecimal) : Warning()
+
+ data class ReduceAmountWarning(val tezosFeeThreshold: BigDecimal) : Warning()
}
\ No newline at end of file
diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractor.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractor.kt
index d3ec1058b9..b43322b46a 100644
--- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractor.kt
+++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractor.kt
@@ -79,8 +79,6 @@ interface SwapInteractor {
*/
fun getTokenBalance(token: CryptoCurrencyStatus): SwapAmount
- fun isAvailableToSwap(networkId: String): Boolean
-
fun getSelectedWallet(): UserWallet?
suspend fun selectInitialCurrencyToSwap(
diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt
index 90148abfc1..b401d09f3c 100644
--- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt
+++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt
@@ -197,7 +197,6 @@ internal class SwapInteractorImpl @Inject constructor(
return repository.getPairs(initialCurrency, currenciesList)
}
- @Deprecated("used in old swap mechanism")
override suspend fun givePermissionToSwap(networkId: String, permissionOptions: PermissionOptions): TxState {
val derivationPath = permissionOptions.fromToken.network.derivationPath.value
val dataToSign = if (permissionOptions.approveType == SwapApproveType.UNLIMITED) {
@@ -241,7 +240,6 @@ internal class SwapInteractorImpl @Inject constructor(
}
}
- @Deprecated("used in old swap mechanism")
override suspend fun findBestQuote(
fromToken: CryptoCurrencyStatus,
toToken: CryptoCurrencyStatus,
@@ -376,6 +374,7 @@ internal class SwapInteractorImpl @Inject constructor(
val warnings = mutableListOf()
manageExistentialDepositWarning(warnings, userWalletId, amount, fromToken)
manageDustWarning(warnings, feeState, userWalletId, fromTokenStatus, amount)
+ manageReduceAmountWarning(warnings, fromTokenStatus, amount)
return warnings
}
@@ -424,6 +423,17 @@ internal class SwapInteractorImpl @Inject constructor(
}
}
+ private fun manageReduceAmountWarning(
+ warnings: MutableList,
+ fromTokenStatus: CryptoCurrencyStatus,
+ amount: SwapAmount,
+ ) {
+ val isTezos = fromTokenStatus.currency.network.id.value == Blockchain.Tezos.id
+ if (isTezos && amount.value == fromTokenStatus.value.amount) {
+ warnings.add(Warning.ReduceAmountWarning(TEZOS_FEE_THRESHOLD))
+ }
+ }
+
override suspend fun onSwap(
swapProvider: SwapProvider,
swapData: SwapDataModel?,
@@ -729,16 +739,10 @@ internal class SwapInteractorImpl @Inject constructor(
)
}
- @Deprecated("used in old swap mechanism")
override fun getTokenBalance(token: CryptoCurrencyStatus): SwapAmount {
return SwapAmount(token.value.amount ?: BigDecimal.ZERO, token.currency.decimals)
}
- @Deprecated("used in old swap mechanism")
- override fun isAvailableToSwap(networkId: String): Boolean {
- return ONE_INCH_SUPPORTED_NETWORKS.contains(networkId)
- }
-
override suspend fun selectInitialCurrencyToSwap(
initialCryptoCurrency: CryptoCurrency,
state: TokensDataStateExpress,
@@ -1584,16 +1588,6 @@ internal class SwapInteractorImpl @Inject constructor(
private const val INCREASE_GAS_LIMIT_BY = 112 // 12%
private const val INCREASE_GAS_LIMIT_FOR_SEND = 105 // 5%
private const val INFINITY_SYMBOL = "∞"
-
- private val ONE_INCH_SUPPORTED_NETWORKS = listOf(
- "ethereum",
- "binance-smart-chain",
- "polygon-pos",
- "optimistic-ethereum",
- "arbitrum-one",
- "xdai",
- "avalanche",
- "fantom",
- )
+ private val TEZOS_FEE_THRESHOLD = BigDecimal("0.01")
}
}
\ No newline at end of file
diff --git a/features/swap/presentation/build.gradle.kts b/features/swap/presentation/build.gradle.kts
index 8986bf7e0d..c78375b5e4 100644
--- a/features/swap/presentation/build.gradle.kts
+++ b/features/swap/presentation/build.gradle.kts
@@ -67,4 +67,5 @@ dependencies {
/** DI */
implementation(deps.hilt.android)
kapt(deps.hilt.kapt)
+
}
\ No newline at end of file
diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapStateHolder.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapStateHolder.kt
index 32b1388ef6..e5db542bba 100644
--- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapStateHolder.kt
+++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapStateHolder.kt
@@ -20,6 +20,7 @@ data class SwapStateHolder(
val alert: SwapWarning.GenericWarning? = null,
val changeCardsButtonState: ChangeCardsButtonState = ChangeCardsButtonState.ENABLED,
val providerState: ProviderState,
+ val reduceAmountIgnore: Boolean, // ignore warning about reducing XTZ amount by 0.01
val fee: FeeItemState = FeeItemState.Empty,
val permissionState: SwapPermissionState = SwapPermissionState.Empty,
@@ -108,7 +109,6 @@ sealed interface SwapWarning {
val title: TextReference? = null,
val message: TextReference? = null,
val type: GenericWarningType = GenericWarningType.OTHER,
- val shouldWrapMessage: Boolean = false,
val onClick: () -> Unit,
) : SwapWarning
data class GeneralError(val notificationConfig: NotificationConfig) : SwapWarning
@@ -116,6 +116,8 @@ sealed interface SwapWarning {
data class GeneralWarning(val notificationConfig: NotificationConfig) : SwapWarning
data class GeneralInformational(val notificationConfig: NotificationConfig) : SwapWarning
data class TransactionInProgressWarning(val title: TextReference, val description: TextReference) : SwapWarning
+ data class NeedReserveToCreateAccount(val notificationConfig: NotificationConfig) : SwapWarning
+ data class ReduceAmount(val notificationConfig: NotificationConfig) : SwapWarning
}
enum class GenericWarningType {
diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/UiActions.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/UiActions.kt
index e7641eca69..bd84a05cac 100644
--- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/UiActions.kt
+++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/UiActions.kt
@@ -1,6 +1,7 @@
package com.tangem.feature.swap.models
import com.tangem.domain.tokens.model.CryptoCurrency
+import com.tangem.feature.swap.domain.models.SwapAmount
import com.tangem.feature.swap.domain.models.ui.TxFee
data class UiActions(
@@ -13,6 +14,8 @@ data class UiActions(
val onChangeCardsClicked: () -> Unit,
val onBackClicked: () -> Unit,
val onMaxAmountSelected: () -> Unit,
+ val onReduceAmount: (SwapAmount) -> Unit,
+ val onReduceAmountIgnoreClick: () -> Unit,
val openPermissionBottomSheet: () -> Unit,
val onChangeApproveType: (ApproveType) -> Unit,
// region new actions
diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt
index 4566c64fae..fc73c5029a 100644
--- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt
+++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt
@@ -88,6 +88,7 @@ internal class StateBuilder(
onShowPermissionBottomSheet = actions.openPermissionBottomSheet,
providerState = ProviderState.Empty(),
shouldShowMaxAmount = false,
+ reduceAmountIgnore = false,
priceImpact = PriceImpact.Empty(),
)
}
@@ -213,7 +214,11 @@ internal class StateBuilder(
): SwapStateHolder {
if (uiStateHolder.sendCardData !is SwapCardState.SwapCardData) return uiStateHolder
if (uiStateHolder.receiveCardData !is SwapCardState.SwapCardData) return uiStateHolder
- val warnings = getWarningsForSuccessState(quoteModel, fromToken)
+ val warnings = getWarningsForSuccessState(
+ quoteModel = quoteModel,
+ fromToken = fromToken,
+ ignoreAmountReduce = uiStateHolder.reduceAmountIgnore,
+ )
val feeState = createFeeState(quoteModel.txFee, selectedFeeType)
val fromCurrencyStatus = quoteModel.fromTokenInfo.cryptoCurrencyStatus
val toCurrencyStatus = quoteModel.toTokenInfo.cryptoCurrencyStatus
@@ -311,41 +316,28 @@ internal class StateBuilder(
private fun getWarningsForSuccessState(
quoteModel: SwapState.QuotesLoadedState,
fromToken: CryptoCurrency,
+ ignoreAmountReduce: Boolean,
): List {
val warnings = mutableListOf()
- addDomainWarnings(quoteModel, warnings)
- if (!quoteModel.preparedSwapConfigState.isAllowedToSpend &&
- quoteModel.preparedSwapConfigState.feeState is SwapFeeState.Enough &&
- quoteModel.permissionState is PermissionDataState.PermissionReadyForRequest
- ) {
- warnings.add(
- SwapWarning.PermissionNeeded(
- createPermissionNotificationConfig(fromToken.symbol),
- ),
- )
- }
- when (quoteModel.preparedSwapConfigState.includeFeeInAmount) {
- is IncludeFeeInAmount.Included ->
- warnings.add(
- SwapWarning.GeneralWarning(
- createNetworkFeeCoverageNotificationConfig(),
- ),
- )
- else -> Unit
- }
- addUnableCoverFeeWarning(quoteModel, fromToken, warnings)
- // check isBalanceEnough, but for dex includeFeeInAmount always Excluded
- if (!quoteModel.preparedSwapConfigState.isBalanceEnough &&
- quoteModel.preparedSwapConfigState.includeFeeInAmount !is IncludeFeeInAmount.Included
- ) {
- warnings.add(SwapWarning.InsufficientFunds)
- }
+ maybeAddDomainWarnings(quoteModel, warnings, ignoreAmountReduce)
+ maybeAddNeedReserveToCreateAccountWarning(quoteModel, warnings)
+ maybeAddPermissionNeededWarning(quoteModel, warnings, fromToken)
+ maybeAddNetworkFeeCoverageWarning(quoteModel, warnings)
+ maybeAddUnableCoverFeeWarning(quoteModel, fromToken, warnings)
+ maybeAddInsufficientFundsWarning(quoteModel, warnings)
+ maybeAddTransactionInProgressWarning(quoteModel, warnings)
+ return warnings
+ }
+ private fun maybeAddTransactionInProgressWarning(
+ quoteModel: SwapState.QuotesLoadedState,
+ warnings: MutableList,
+ ) {
if (quoteModel.permissionState is PermissionDataState.PermissionLoading) {
warnings.add(
SwapWarning.TransactionInProgressWarning(
- title = resourceReference(R.string.swapping_pending_transaction_title),
- description = resourceReference(R.string.swapping_pending_transaction_subtitle),
+ title = resourceReference(R.string.warning_express_approval_in_progress_title),
+ description = resourceReference(R.string.warning_express_approval_in_progress_message),
),
)
} else if (quoteModel.preparedSwapConfigState.hasOutgoingTransaction) {
@@ -361,10 +353,13 @@ internal class StateBuilder(
),
)
}
- return warnings
}
- private fun addDomainWarnings(quoteModel: SwapState.QuotesLoadedState, warnings: MutableList) {
+ private fun maybeAddDomainWarnings(
+ quoteModel: SwapState.QuotesLoadedState,
+ warnings: MutableList,
+ ignoreAmountReduce: Boolean,
+ ) {
quoteModel.warnings.forEach {
when (it) {
is Warning.ExistentialDepositWarning -> {
@@ -390,7 +385,7 @@ internal class StateBuilder(
NotificationConfig(
title = resourceReference(R.string.send_notification_invalid_amount_title),
subtitle = resourceReference(
- R.string.send_notification_invalid_minimum_amount_text,
+ R.string.warning_express_dust_message,
wrappedList(
it.dustValue.toPlainString(),
it.dustValue.toPlainString(),
@@ -401,11 +396,84 @@ internal class StateBuilder(
),
)
}
+ is Warning.ReduceAmountWarning -> {
+ if (!ignoreAmountReduce) {
+ warnings.add(
+ SwapWarning.ReduceAmount(
+ notificationConfig = createReduceAmountNotificationConfig(
+ amount = it.tezosFeeThreshold.toPlainString(),
+ onConfirmClick = {
+ val fromAmount = quoteModel.fromTokenInfo.tokenAmount
+ val patchedAmount = fromAmount.copy(
+ value = fromAmount.value - it.tezosFeeThreshold,
+ )
+ actions.onReduceAmount(patchedAmount)
+ },
+ onDismissClick = actions.onReduceAmountIgnoreClick,
+ ),
+ ),
+ )
+ }
+ }
}
}
}
- private fun addUnableCoverFeeWarning(
+ private fun maybeAddNeedReserveToCreateAccountWarning(
+ quoteModel: SwapState.QuotesLoadedState,
+ warnings: MutableList,
+ ) {
+ val status = quoteModel.toTokenInfo.cryptoCurrencyStatus.value
+ if (status is CryptoCurrencyStatus.NoAccount) {
+ val amount = quoteModel.toTokenInfo.tokenAmount.value
+ val amountToCreateAccount = status.amountToCreateAccount
+
+ if (amount < amountToCreateAccount) {
+ warnings.add(
+ SwapWarning.NeedReserveToCreateAccount(
+ notificationConfig = createActivateAccountNotificationConfig(
+ status.amountToCreateAccount,
+ quoteModel.toTokenInfo.cryptoCurrencyStatus.currency.name,
+ ),
+ ),
+ )
+ }
+ }
+ }
+
+ private fun maybeAddPermissionNeededWarning(
+ quoteModel: SwapState.QuotesLoadedState,
+ warnings: MutableList,
+ fromToken: CryptoCurrency,
+ ) {
+ if (!quoteModel.preparedSwapConfigState.isAllowedToSpend &&
+ quoteModel.preparedSwapConfigState.feeState is SwapFeeState.Enough &&
+ quoteModel.permissionState is PermissionDataState.PermissionReadyForRequest
+ ) {
+ warnings.add(
+ SwapWarning.PermissionNeeded(
+ createPermissionNotificationConfig(fromToken.symbol),
+ ),
+ )
+ }
+ }
+
+ private fun maybeAddNetworkFeeCoverageWarning(
+ quoteModel: SwapState.QuotesLoadedState,
+ warnings: MutableList,
+ ) {
+ when (quoteModel.preparedSwapConfigState.includeFeeInAmount) {
+ is IncludeFeeInAmount.Included ->
+ warnings.add(
+ SwapWarning.GeneralWarning(
+ createNetworkFeeCoverageNotificationConfig(),
+ ),
+ )
+ else -> Unit
+ }
+ }
+
+ private fun maybeAddUnableCoverFeeWarning(
quoteModel: SwapState.QuotesLoadedState,
fromToken: CryptoCurrency,
warnings: MutableList,
@@ -428,7 +496,29 @@ internal class StateBuilder(
}
}
+ private fun maybeAddInsufficientFundsWarning(
+ quoteModel: SwapState.QuotesLoadedState,
+ warnings: MutableList,
+ ) {
+ // check isBalanceEnough, but for dex includeFeeInAmount always Excluded
+ if (!quoteModel.preparedSwapConfigState.isBalanceEnough &&
+ quoteModel.preparedSwapConfigState.includeFeeInAmount !is IncludeFeeInAmount.Included
+ ) {
+ warnings.add(SwapWarning.InsufficientFunds)
+ }
+ }
+
private fun getSwapButtonEnabled(quoteModel: SwapState.QuotesLoadedState): Boolean {
+ val status = quoteModel.toTokenInfo.cryptoCurrencyStatus.value
+ if (status is CryptoCurrencyStatus.NoAccount) {
+ val amount = quoteModel.toTokenInfo.tokenAmount.value
+ val amountToCreateAccount = status.amountToCreateAccount
+
+ if (amount < amountToCreateAccount) {
+ return false
+ }
+ }
+
val preparedSwapConfigState = quoteModel.preparedSwapConfigState
// check has has outgoing transaction
if (preparedSwapConfigState.hasOutgoingTransaction) return false
@@ -587,7 +677,7 @@ internal class StateBuilder(
subtitle = if (dataError is DataError.UnknownError) {
resourceReference(R.string.common_unknown_error)
} else {
- resourceReference(R.string.generic_error_code, wrappedList(dataError.code.toString()))
+ resourceReference(R.string.express_error_code, wrappedList(dataError.code.toString()))
},
iconResId = R.drawable.img_attention_20,
buttonsState = NotificationConfig.ButtonsState.SecondaryButtonConfig(
@@ -774,7 +864,7 @@ internal class StateBuilder(
SwapWarning.GeneralWarning(
notificationConfig = NotificationConfig(
title = TextReference.Res(R.string.warning_express_refresh_required_title),
- subtitle = TextReference.Res(R.string.generic_error_code, wrappedList(code)),
+ subtitle = TextReference.Res(R.string.express_error_code, wrappedList(code)),
iconResId = R.drawable.ic_alert_triangle_20,
buttonsState = NotificationConfig.ButtonsState.PrimaryButtonConfig(
text = TextReference.Res(R.string.warning_button_refresh),
@@ -823,8 +913,8 @@ internal class StateBuilder(
warnings.add(
0,
SwapWarning.TransactionInProgressWarning(
- title = resourceReference(R.string.swapping_pending_transaction_title),
- description = resourceReference(R.string.swapping_pending_transaction_subtitle),
+ title = resourceReference(R.string.warning_express_approval_in_progress_title),
+ description = resourceReference(R.string.warning_express_approval_in_progress_message),
),
)
return uiState.copy(
@@ -928,17 +1018,11 @@ internal class StateBuilder(
fun clearAlert(uiState: SwapStateHolder): SwapStateHolder = uiState.copy(alert = null)
- fun addWarning(
- uiState: SwapStateHolder,
- message: TextReference?,
- shouldWrapMessage: Boolean = false,
- onClick: () -> Unit,
- ): SwapStateHolder {
+ fun addWarning(uiState: SwapStateHolder, message: TextReference?, onClick: () -> Unit): SwapStateHolder {
val renewWarnings = uiState.warnings.filterNot { it is SwapWarning.GenericWarning }.toMutableList()
renewWarnings.add(
SwapWarning.GenericWarning(
message = message,
- shouldWrapMessage = shouldWrapMessage,
onClick = onClick,
),
)
@@ -1220,6 +1304,35 @@ internal class StateBuilder(
)
}
+ private fun createActivateAccountNotificationConfig(amount: BigDecimal, token: String): NotificationConfig {
+ return NotificationConfig(
+ title = resourceReference(
+ id = R.string.send_notification_invalid_reserve_amount_title,
+ formatArgs = wrappedList("$amount $token"),
+ ),
+ subtitle = resourceReference(R.string.send_notification_invalid_reserve_amount_text),
+ iconResId = R.drawable.img_attention_20,
+ )
+ }
+
+ private fun createReduceAmountNotificationConfig(
+ amount: String,
+ onConfirmClick: () -> Unit,
+ onDismissClick: () -> Unit,
+ ): NotificationConfig {
+ return NotificationConfig(
+ title = resourceReference(R.string.send_notification_high_fee_title),
+ subtitle = resourceReference(R.string.send_notification_high_fee_text, wrappedList(amount)),
+ iconResId = R.drawable.img_attention_20,
+ buttonsState = NotificationConfig.ButtonsState.PairButtonsConfig(
+ primaryText = resourceReference(R.string.xtz_withdrawal_message_reduce, wrappedList(amount)),
+ onPrimaryClick = onConfirmClick,
+ secondaryText = resourceReference(R.string.xtz_withdrawal_message_ignore),
+ onSecondaryClick = onDismissClick,
+ ),
+ )
+ }
+
private fun createUnableToCoverFeeNotificationConfig(
fromToken: CryptoCurrency,
feeCurrency: CryptoCurrency?,
diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt
index 6c31dea226..fa48e3f6fb 100644
--- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt
+++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt
@@ -337,14 +337,9 @@ private fun SwapWarnings(warnings: List) {
)
}
is SwapWarning.GenericWarning -> {
- val message = warning.message?.let {
- if (warning.shouldWrapMessage) {
- String.format(stringResource(id = R.string.swapping_error_wrapper), it.resolveReference())
- } else {
- it.resolveReference()
- }
- } ?: stringResource(id = R.string.common_unknown_error)
- RefreshableWaringCard(
+ val message = warning.message?.resolveReference()
+ ?: stringResource(id = R.string.common_unknown_error)
+ RefreshableWarningCard(
title = stringResource(id = R.string.common_warning),
description = message,
onClick = warning.onClick,
@@ -377,6 +372,16 @@ private fun SwapWarnings(warnings: List) {
iconTint = TangemTheme.colors.icon.accent,
)
}
+ is SwapWarning.NeedReserveToCreateAccount -> {
+ Notification(
+ config = warning.notificationConfig,
+ )
+ }
+ is SwapWarning.ReduceAmount -> {
+ Notification(
+ config = warning.notificationConfig,
+ )
+ }
is SwapWarning.TransactionInProgressWarning -> {
CardWithIcon(
title = warning.title.resolveReference(),
@@ -499,6 +504,7 @@ private val state = SwapStateHolder(
providerState = ProviderState.Loading(),
priceImpact = PriceImpact.Empty(),
shouldShowMaxAmount = true,
+ reduceAmountIgnore = false,
tosState = TosState(
tosLink = LegalState(
title = stringReference("Terms of Use"),
diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt
index 51969a1ec1..90e9ca9b8c 100644
--- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt
+++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt
@@ -27,6 +27,7 @@ import com.tangem.feature.swap.domain.BlockchainInteractor
import com.tangem.feature.swap.domain.SwapInteractor
import com.tangem.feature.swap.domain.models.DataError
import com.tangem.feature.swap.domain.models.ExpressException
+import com.tangem.feature.swap.domain.models.SwapAmount
import com.tangem.feature.swap.domain.models.domain.*
import com.tangem.feature.swap.domain.models.formatToUIRepresentation
import com.tangem.feature.swap.domain.models.ui.*
@@ -812,6 +813,10 @@ internal class SwapViewModel @Inject constructor(
}
}
+ private fun onReduceAmountClicked(newAmount: SwapAmount) {
+ onAmountChanged(newAmount.formatToUIRepresentation())
+ }
+
@Suppress("UnusedPrivateMember")
private fun onAmountSelected(selected: Boolean) {
if (selected) {
@@ -874,7 +879,14 @@ internal class SwapViewModel @Inject constructor(
}
onSearchEntered("")
},
- onMaxAmountSelected = { onMaxAmountClicked() },
+ onMaxAmountSelected = ::onMaxAmountClicked,
+ onReduceAmount = ::onReduceAmountClicked,
+ onReduceAmountIgnoreClick = {
+ uiState = uiState.copy(
+ reduceAmountIgnore = true,
+ warnings = uiState.warnings.filter { it !is SwapWarning.ReduceAmount },
+ )
+ },
openPermissionBottomSheet = {
singleTaskScheduler.cancelTask()
analyticsEventHandler.send(SwapEvents.ButtonGivePermissionClicked)
@@ -1153,11 +1165,11 @@ internal class SwapViewModel @Inject constructor(
)
}
- companion object {
- private const val loggingTag = "SwapViewModel"
- private const val INITIAL_AMOUNT = ""
- private const val UPDATE_DELAY = 10000L
- private const val DEBOUNCE_AMOUNT_DELAY = 1000L
- private const val UPDATE_BALANCE_DELAY_MILLIS = 11000L
+ private companion object {
+ const val loggingTag = "SwapViewModel"
+ const val INITIAL_AMOUNT = ""
+ const val UPDATE_DELAY = 10000L
+ const val DEBOUNCE_AMOUNT_DELAY = 1000L
+ const val UPDATE_BALANCE_DELAY_MILLIS = 11000L
}
}
\ No newline at end of file
diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/components/TokenDetailsNotification.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/components/TokenDetailsNotification.kt
index 669c2e24f6..a33cc576e3 100644
--- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/components/TokenDetailsNotification.kt
+++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/components/TokenDetailsNotification.kt
@@ -165,7 +165,7 @@ internal sealed class TokenDetailsNotification(val config: NotificationConfig) {
object TopUpWithoutReserve : Informational(
title = resourceReference(id = R.string.warning_no_account_title),
- subtitle = resourceReference(id = R.string.no_account_bnb),
+ subtitle = resourceReference(id = R.string.no_account_send_to_create),
)
class HasPendingTransactions(val coinSymbol: String) : Informational(
diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/exchange/ExchangeProvider.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/exchange/ExchangeProvider.kt
index c08898d2dc..41278b195c 100644
--- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/exchange/ExchangeProvider.kt
+++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/exchange/ExchangeProvider.kt
@@ -19,6 +19,7 @@ import androidx.compose.ui.tooling.preview.Preview
import com.tangem.core.ui.R
import com.tangem.core.ui.components.inputrow.InputRowBestRate
import com.tangem.core.ui.extensions.TextReference
+import com.tangem.core.ui.extensions.wrappedList
import com.tangem.core.ui.res.TangemTheme
@Composable
@@ -67,7 +68,7 @@ internal fun ExchangeProvider(
)
Text(
modifier = Modifier.align(Alignment.CenterVertically),
- text = "ID: $providerTxId",
+ text = stringResource(R.string.express_transaction_id, wrappedList(providerTxId)),
style = TangemTheme.typography.body2,
color = TangemTheme.colors.text.tertiary,
)
diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt
index c5d482b009..a8a0978de4 100644
--- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt
+++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt
@@ -174,9 +174,10 @@ internal class TokenDetailsViewModel @Inject constructor(
status = cryptoCurrencyStatus ?: return,
transactionInfo = data.let {
TransactionInfo(
- amount = it.baseCurrencyAmount,
transactionId = it.transactionId,
destinationAddress = it.depositWalletAddress,
+ amount = it.baseCurrencyAmount,
+ tag = it.depositWalletAddressTag,
)
},
)
diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/deeplink/WalletDeepLinksHandler.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/deeplink/WalletDeepLinksHandler.kt
index 0248645642..59e2022b58 100644
--- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/deeplink/WalletDeepLinksHandler.kt
+++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/deeplink/WalletDeepLinksHandler.kt
@@ -84,6 +84,7 @@ internal class WalletDeepLinksHandler @Inject constructor(
amount = it.baseCurrencyAmount,
destinationAddress = it.depositWalletAddress,
transactionId = it.transactionId,
+ tag = it.depositWalletAddressTag,
)
}
diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/WalletScreenAnalyticsEvent.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/WalletScreenAnalyticsEvent.kt
index 088622ed27..495f7edd82 100644
--- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/WalletScreenAnalyticsEvent.kt
+++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/WalletScreenAnalyticsEvent.kt
@@ -23,7 +23,7 @@ sealed class WalletScreenAnalyticsEvent {
override val oneTimeEventId: String = id + userWalletId.stringValue
}
- object WalletOpened : Basic(event = "Wallet Opened")
+ data object WalletOpened : Basic(event = "Wallet Opened")
class CardWasScanned(source: AnalyticsParam.ScannedFrom) : Basic(
event = "Card Was Scanned",
@@ -53,8 +53,8 @@ sealed class WalletScreenAnalyticsEvent {
params: Map = mapOf(),
) : AnalyticsEvent(category = "Main Screen", event = event, params = params) {
- object ScreenOpened : MainScreen(event = "Screen opened")
- object WalletSwipe : MainScreen(event = "Wallet Swipe")
+ data object ScreenOpened : MainScreen(event = "Screen opened")
+ data object WalletSwipe : MainScreen(event = "Wallet Swipe")
class EnableBiometrics(state: AnalyticsParam.OnOffState) : MainScreen(
event = "Enable Biometric",
@@ -66,37 +66,39 @@ sealed class WalletScreenAnalyticsEvent {
params = mapOf("Result" to result.value),
)
- object NoticeBackupYourWalletTapped : MainScreen(event = "Notice - Backup Your Wallet Tapped")
- object NoticeScanYourCardTapped : MainScreen(event = "Notice - Scan Your Card Tapped")
- object NoticeWalletLocked : MainScreen(event = "Notice - Wallet Locked")
- object WalletUnlockTapped : MainScreen(event = "Notice - Wallet Unlock Tapped")
+ data object NoticeBackupYourWalletTapped : MainScreen(event = "Notice - Backup Your Wallet Tapped")
+ data object NoticeScanYourCardTapped : MainScreen(event = "Notice - Scan Your Card Tapped")
+ data object NoticeWalletLocked : MainScreen(event = "Notice - Wallet Locked")
+ data object WalletUnlockTapped : MainScreen(event = "Notice - Wallet Unlock Tapped")
- object NetworksUnreachable : MainScreen(event = "Notice - Networks Unreachable")
+ data object NetworksUnreachable : MainScreen(event = "Notice - Networks Unreachable")
- object MissingAddresses : MainScreen(event = "Notice - Missing Addresses")
+ data object MissingAddresses : MainScreen(event = "Notice - Missing Addresses")
- object CardSignedTransactions : MainScreen(event = "Notice - Card Signed Transactions")
+ data object CardSignedTransactions : MainScreen(event = "Notice - Card Signed Transactions")
- object HowDoYouLikeTangem : MainScreen(event = "Notice - How Do You Like Tangem")
+ data object HowDoYouLikeTangem : MainScreen(event = "Notice - How Do You Like Tangem")
- object ProductSampleCard : MainScreen(event = "Notice - Product Sample Card")
+ data object ProductSampleCard : MainScreen(event = "Notice - Product Sample Card")
- object TestnetCard : MainScreen(event = "Notice - Testnet Card")
+ data object TestnetCard : MainScreen(event = "Notice - Testnet Card")
- object DemoCard : MainScreen(event = "Notice - Demo Card")
+ data object DemoCard : MainScreen(event = "Notice - Demo Card")
- object DevelopmentCard : MainScreen(event = "Notice - Development Card")
+ data object DevelopmentCard : MainScreen(event = "Notice - Development Card")
- object WalletUnlock : MainScreen(event = "Notice - Wallet Unlock")
+ data object WalletUnlock : MainScreen(event = "Notice - Wallet Unlock")
- object BackupYourWallet : MainScreen(event = "Notice - Backup Your Wallet")
+ data object BackupYourWallet : MainScreen(event = "Notice - Backup Your Wallet")
- object UnlockAllWithBiometrics : MainScreen(event = "Button - Unlock All With Biometrics")
+ data object BackupError : MainScreen(event = "Notice - Backup Error")
- object UnlockWithCardScan : MainScreen(event = "Button - Unlock With Card Scan")
+ data object UnlockAllWithBiometrics : MainScreen(event = "Button - Unlock All With Biometrics")
- object EditWalletTapped : MainScreen(event = "Button - Edit Wallet Tapped")
+ data object UnlockWithCardScan : MainScreen(event = "Button - Unlock With Card Scan")
- object DeleteWalletTapped : MainScreen(event = "Button - Delete Wallet Tapped")
+ data object EditWalletTapped : MainScreen(event = "Button - Edit Wallet Tapped")
+
+ data object DeleteWalletTapped : MainScreen(event = "Button - Delete Wallet Tapped")
}
}
\ No newline at end of file
diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/WalletWarningsAnalyticsSender.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/WalletWarningsAnalyticsSender.kt
index 899dd5a09a..e80fc87a24 100644
--- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/WalletWarningsAnalyticsSender.kt
+++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/WalletWarningsAnalyticsSender.kt
@@ -43,6 +43,7 @@ internal class WalletWarningsAnalyticsSender @Inject constructor(
is WalletNotification.Informational.DemoCard -> MainScreen.DemoCard
is WalletNotification.Informational.MissingAddresses -> MainScreen.MissingAddresses
is WalletNotification.RateApp -> MainScreen.HowDoYouLikeTangem
+ is WalletNotification.Critical.BackupError -> MainScreen.BackupError
is WalletNotification.UnlockWallets -> null // See [SelectedWalletAnalyticsSender]
is WalletNotification.Informational.NoAccount,
is WalletNotification.Warning.LowSignatures,
diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/BackupValidator.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/BackupValidator.kt
new file mode 100644
index 0000000000..caa4c9fd87
--- /dev/null
+++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/BackupValidator.kt
@@ -0,0 +1,42 @@
+package com.tangem.feature.wallet.presentation.wallet.domain
+
+import com.tangem.common.card.EllipticCurve
+import com.tangem.domain.common.configs.CardConfig
+import com.tangem.domain.models.scan.CardDTO
+import javax.inject.Inject
+
+class BackupValidator @Inject constructor() {
+
+ fun isValid(cardDTO: CardDTO): Boolean {
+ return validateBackupStatus(cardDTO) && validateCurves(cardDTO)
+ }
+
+ private fun validateCurves(cardDTO: CardDTO): Boolean {
+ val config = CardConfig.createConfig(cardDTO)
+ // / Since the curve `bls12381_G2_AUG` was added later into first generation of wallets,
+ // we cannot determine whether this curve is missing due to an error or because the user
+ // did not want to recreate the wallet.
+ val expectedCurves = config.mandatoryCurves
+ .filterNot { it == EllipticCurve.Bls12381G2Aug }
+ val curves = cardDTO.wallets.map { it.curve }
+ for (expectedCurve in expectedCurves) {
+ val cardCurvesCount = curves.count { it == expectedCurve }
+
+ // missing curve
+ if (cardCurvesCount == 0) {
+ return false
+ }
+
+ // duplicated curve
+ if (cardCurvesCount > 1) {
+ return false
+ }
+ }
+ return true
+ }
+
+ private fun validateBackupStatus(cardDTO: CardDTO): Boolean {
+ val backupStatus = cardDTO.backupStatus
+ return backupStatus != null && backupStatus !is CardDTO.BackupStatus.CardLinked
+ }
+}
\ No newline at end of file
diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetMultiWalletWarningsFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetMultiWalletWarningsFactory.kt
index 54bfd1bb5a..7432cbb84e 100644
--- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetMultiWalletWarningsFactory.kt
+++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetMultiWalletWarningsFactory.kt
@@ -37,6 +37,7 @@ internal class GetMultiWalletWarningsFactory @Inject constructor(
private val shouldShowSwapPromoWalletUseCase: ShouldShowSwapPromoWalletUseCase,
private val promoRepository: PromoRepository,
private val isNeedToBackupUseCase: IsNeedToBackupUseCase,
+ private val backupValidator: BackupValidator,
) {
private var readyForRateAppNotification = false
@@ -57,7 +58,7 @@ internal class GetMultiWalletWarningsFactory @Inject constructor(
buildList {
addSwapPromoNotification(shouldShowPromo, promoBanner, clickIntents)
- addCriticalNotifications(cardTypesResolver)
+ addCriticalNotifications(userWallet)
addInformationalNotifications(cardTypesResolver, maybeTokenList, clickIntents)
@@ -85,7 +86,13 @@ internal class GetMultiWalletWarningsFactory @Inject constructor(
)
}
- private fun MutableList.addCriticalNotifications(cardTypesResolver: CardTypesResolver) {
+ private fun MutableList.addCriticalNotifications(userWallet: UserWallet) {
+ val cardTypesResolver = userWallet.scanResponse.cardTypesResolver
+ addIf(
+ element = WalletNotification.Critical.BackupError,
+ condition = !backupValidator.isValid(userWallet.scanResponse.card) || userWallet.hasBackupError,
+ )
+
addIf(
element = WalletNotification.Critical.DevCard,
condition = !cardTypesResolver.isReleaseFirmwareType(),
diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletBottomSheetConfig.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletBottomSheetConfig.kt
index d6ec5fcc93..adfede4a1c 100644
--- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletBottomSheetConfig.kt
+++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletBottomSheetConfig.kt
@@ -36,7 +36,10 @@ sealed class WalletBottomSheetConfig(
),
iconResId = R.drawable.ic_locked_24,
primaryButtonConfig = ButtonConfig(
- text = resourceReference(id = R.string.user_wallet_list_unlock_all),
+ text = resourceReference(
+ id = R.string.user_wallet_list_unlock_all_with,
+ formatArgs = wrappedList(resourceReference(R.string.common_biometrics)),
+ ),
onClick = onUnlockClick,
),
secondaryButtonConfig = ButtonConfig(
diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletNotification.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletNotification.kt
index e7d25584d5..9f7f5ea139 100644
--- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletNotification.kt
+++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletNotification.kt
@@ -36,6 +36,11 @@ sealed class WalletNotification(val config: NotificationConfig) {
title = resourceReference(id = R.string.warning_failed_to_verify_card_title),
subtitle = resourceReference(id = R.string.warning_failed_to_verify_card_message),
)
+
+ data object BackupError : Critical(
+ title = resourceReference(R.string.warning_backup_errors_title),
+ subtitle = resourceReference(R.string.warning_backup_errors_message),
+ )
}
sealed class Warning(
diff --git a/gradle/dependencies.toml b/gradle/dependencies.toml
index b92767f469..df3dce1313 100644
--- a/gradle/dependencies.toml
+++ b/gradle/dependencies.toml
@@ -61,7 +61,6 @@ okhttp = "4.9.3"
rekotlin = "1.0.4"
retrofit = "2.8.1"
retrofitMoshiConverter = "2.6.0"
-shopifyBuySdk = "12.0.0"
spongycastleCryptoCore = "1.58.0.0"
timber = "4.7.1"
viewBindingDelegate = "1.5.9"
@@ -82,12 +81,13 @@ listenableFuture = "1.0"
swipeRefreshLayout = "1.1.0"
spr-client = "3.6.2"
web3j = "4.10.1"
+leakcanary = "2.13"
# endregion Other libraries
# region Tangem
-tangemBlockchainSdk = "release-app_5.8-536"
+tangemBlockchainSdk = "develop-538"
#tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds
-tangemCardSdk = "release-app_5.8-336"
+tangemCardSdk = "develop-337"
#tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^
# endregion Tangem
@@ -223,7 +223,6 @@ moshi = { module = "com.squareup.moshi:moshi", version.ref = "moshi" }
moshi-kotlin = { module = "com.squareup.moshi:moshi-kotlin", version.ref = "moshi" }
okHttp = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp" }
okHttp-prettyLogging = { module = "com.github.ihsanbal:LoggingInterceptor", version.ref = "okHttp-prettyLogging" }
-shopify-buy = { module = "com.shopify.mobilebuysdk:buy3", version.ref = "shopifyBuySdk" }
spongecastle-core = { module = "com.madgag.spongycastle:core", version.ref = "spongycastleCryptoCore" }
reKotlin = { module = "org.rekotlin:rekotlin", version.ref = "rekotlin" }
retrofit = { module = "com.squareup.retrofit2:retrofit", version.ref = "retrofit" }
@@ -249,4 +248,5 @@ camera-camera2 = { module = "androidx.camera:camera-camera2", version.ref = "and
camera-lifecycle = { module = "androidx.camera:camera-lifecycle", version.ref = "androidXCamera" }
camera-view = { module = "androidx.camera:camera-view", version.ref = "androidXCamera" }
web3j-core = { module = "org.web3j:core", version.ref = "web3j" }
+leakcanary = { module = "com.squareup.leakcanary:leakcanary-android", version.ref = "leakcanary" }
# endregion Other
diff --git a/settings.gradle.kts b/settings.gradle.kts
index fb31682f87..8c94ba65a8 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -138,6 +138,9 @@ include(":domain:transaction")
include(":domain:analytics")
include(":domain:visa")
include(":domain:onboarding")
+include(":domain:feedback")
+include(":domain:qr-scanning")
+include(":domain:qr-scanning:models")
// endregion Domain modules
// region Data modules
@@ -156,4 +159,6 @@ include(":data:transaction")
include(":data:visa")
include(":data:promo")
include(":data:onboarding")
+include(":data:feedback")
+include(":data:qr-scanning")
// endregion Data modules
\ No newline at end of file