Updated on 2026-08-14

This commit is contained in:
Tangem 2025-09-24 13:41:20 +03:00
commit 9860ecc4cb
36 changed files with 230 additions and 61 deletions

1
.gitignore vendored
View file

@ -10,6 +10,7 @@
**/build/kotlin/** **/build/kotlin/**
**/build/libs/** **/build/libs/**
**/build/outputs/** **/build/outputs/**
**/build/reports/**
**/build/tmp/** **/build/tmp/**
# Local configuration file (sdk path, etc) # Local configuration file (sdk path, etc)

@ -1 +1 @@
Subproject commit ad46a043f9b98b4f4577f906920532cb370c15d4 Subproject commit 5cf863d2ad68d89a111520aca3504fe9db234c82

View file

@ -143,10 +143,8 @@ internal object TransactionDomainModule {
@Provides @Provides
@Singleton @Singleton
fun provideIsUtxoConsolidationAvailableUseCase( fun provideIsSelfSendAvailableUseCase(walletManagersFacade: WalletManagersFacade): IsSelfSendAvailableUseCase {
walletManagersFacade: WalletManagersFacade, return IsSelfSendAvailableUseCase(walletManagersFacade)
): IsUtxoConsolidationAvailableUseCase {
return IsUtxoConsolidationAvailableUseCase(walletManagersFacade)
} }
@Provides @Provides

View file

@ -57,7 +57,7 @@
}, },
{ {
"name": "NEW_ONRAMP_MAIN_ENABLED", "name": "NEW_ONRAMP_MAIN_ENABLED",
"version": "5.29.0" "version": "undefined"
}, },
{ {
"name": "ACCOUNTS_FEATURE_ENABLED", "name": "ACCOUNTS_FEATURE_ENABLED",

View file

@ -4,7 +4,7 @@ import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
data class PromotionInfoResponse( data class PromoBannerResponse(
@Json(name = "name") val name: String, @Json(name = "name") val name: String,
@Json(name = "all") val bannerState: BannerState?, @Json(name = "all") val bannerState: BannerState?,
) { ) {

View file

@ -1,6 +1,7 @@
package com.tangem.datasource.api.tangemTech package com.tangem.datasource.api.tangemTech
import com.tangem.datasource.api.common.response.ApiResponse import com.tangem.datasource.api.common.response.ApiResponse
import com.tangem.datasource.api.promotion.models.PromoBannerResponse
import com.tangem.datasource.api.promotion.models.StoryContentResponse import com.tangem.datasource.api.promotion.models.StoryContentResponse
import com.tangem.datasource.api.tangemTech.models.* import com.tangem.datasource.api.tangemTech.models.*
import com.tangem.datasource.api.tangemTech.models.account.GetWalletAccountsResponse import com.tangem.datasource.api.tangemTech.models.account.GetWalletAccountsResponse
@ -163,4 +164,12 @@ interface TangemTechApi {
@Header("If-None-Match") eTag: String? = null, @Header("If-None-Match") eTag: String? = null,
): ApiResponse<GetWalletArchivedAccountsResponse> ): ApiResponse<GetWalletArchivedAccountsResponse>
// endregion // endregion
// region promo banners
@GET("/v1/promotion")
suspend fun getPromoBanner(
@Query("programName") name: String,
@Header("Cache-Control") cacheControl: String = "max-age=600",
): ApiResponse<PromoBannerResponse>
// endregion
} }

View file

@ -11,6 +11,10 @@ import com.tangem.datasource.local.onramp.paymentmethods.DefaultOnrampPaymentMet
import com.tangem.datasource.local.onramp.paymentmethods.OnrampPaymentMethodsStore import com.tangem.datasource.local.onramp.paymentmethods.OnrampPaymentMethodsStore
import com.tangem.datasource.local.onramp.quotes.DefaultOnrampQuotesStore import com.tangem.datasource.local.onramp.quotes.DefaultOnrampQuotesStore
import com.tangem.datasource.local.onramp.quotes.OnrampQuotesStore import com.tangem.datasource.local.onramp.quotes.OnrampQuotesStore
import com.tangem.datasource.local.onramp.sepa.DefaultOnrampCurrentCountryByIPStore
import com.tangem.datasource.local.onramp.sepa.DefaultOnrampSepaAvailabilityStore
import com.tangem.datasource.local.onramp.sepa.OnrampCurrentCountryByIPStore
import com.tangem.datasource.local.onramp.sepa.OnrampSepaAvailabilityStore
import dagger.Module import dagger.Module
import dagger.Provides import dagger.Provides
import dagger.hilt.InstallIn import dagger.hilt.InstallIn
@ -50,4 +54,16 @@ internal object OnrampStoreModule {
fun provideOnrampCurrencies(): OnrampCurrenciesStore { fun provideOnrampCurrencies(): OnrampCurrenciesStore {
return DefaultOnrampCurrenciesStore(dataStore = RuntimeDataStore()) return DefaultOnrampCurrenciesStore(dataStore = RuntimeDataStore())
} }
@Provides
@Singleton
fun provideOnrampSepaAvailableStore(): OnrampSepaAvailabilityStore {
return DefaultOnrampSepaAvailabilityStore(dataStore = RuntimeDataStore())
}
@Provides
@Singleton
fun provideOnrampCurrentCountryByIPStore(): OnrampCurrentCountryByIPStore {
return DefaultOnrampCurrentCountryByIPStore(dataStore = RuntimeDataStore())
}
} }

View file

@ -44,6 +44,7 @@ internal object BlockchainSDKConfigConverter : Converter<EnvironmentConfigModel,
koinosProApiKey = value.koinosProApiKey, koinosProApiKey = value.koinosProApiKey,
alephiumApiKey = value.alephiumTangemApiKey, alephiumApiKey = value.alephiumTangemApiKey,
moralisApiKey = value.moralisApiKey, moralisApiKey = value.moralisApiKey,
etherscanApiKey = value.etherScanApiKey,
) )
} }

View file

@ -43,6 +43,7 @@ class EnvironmentConfigModel(
@Json(name = "tangemApiKey") val tangemApiKey: String?, @Json(name = "tangemApiKey") val tangemApiKey: String?,
@Json(name = "tangemApiKeyDev") val tangemApiKeyDev: String?, @Json(name = "tangemApiKeyDev") val tangemApiKeyDev: String?,
@Json(name = "tangemApiKeyStage") val tangemApiKeyStage: String?, @Json(name = "tangemApiKeyStage") val tangemApiKeyStage: String?,
@Json(name = "etherscanApiKey") val etherScanApiKey: String?,
) )
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)

View file

@ -0,0 +1,22 @@
package com.tangem.datasource.local.onramp.sepa
import com.tangem.datasource.local.datastore.core.StringKeyDataStore
import com.tangem.datasource.local.datastore.core.StringKeyDataStoreDecorator
import com.tangem.domain.onramp.model.OnrampCountry
internal class DefaultOnrampCurrentCountryByIPStore(
val dataStore: StringKeyDataStore<OnrampCountry>,
) : OnrampCurrentCountryByIPStore, StringKeyDataStoreDecorator<Unit, OnrampCountry>(
wrappedDataStore = dataStore,
) {
override suspend fun getSyncOrNull(): OnrampCountry? {
return getSyncOrNull(Unit)
}
override suspend fun store(value: OnrampCountry) {
return store(Unit, value)
}
override fun provideStringKey(key: Unit) = "KEY"
}

View file

@ -0,0 +1,20 @@
package com.tangem.datasource.local.onramp.sepa
import com.tangem.datasource.local.datastore.core.StringKeyDataStore
import com.tangem.datasource.local.datastore.core.StringKeyDataStoreDecorator
internal class DefaultOnrampSepaAvailabilityStore(
val dataStore: StringKeyDataStore<Boolean>,
) : OnrampSepaAvailabilityStore, StringKeyDataStoreDecorator<OnrampSepaAvailabilityStoreKey, Boolean>(
wrappedDataStore = dataStore,
) {
override fun provideStringKey(key: OnrampSepaAvailabilityStoreKey) = with(key) {
buildString {
append(userWallet.walletId.toString())
append("_")
append(country.code)
append("_")
append(cryptoCurrency.id.value)
}
}
}

View file

@ -0,0 +1,9 @@
package com.tangem.datasource.local.onramp.sepa
import com.tangem.domain.onramp.model.OnrampCountry
interface OnrampCurrentCountryByIPStore {
suspend fun getSyncOrNull(): OnrampCountry?
suspend fun store(value: OnrampCountry)
suspend fun clear()
}

View file

@ -0,0 +1,10 @@
package com.tangem.datasource.local.onramp.sepa
import kotlinx.coroutines.flow.Flow
interface OnrampSepaAvailabilityStore {
suspend fun getSyncOrNull(key: OnrampSepaAvailabilityStoreKey): Boolean?
fun get(key: OnrampSepaAvailabilityStoreKey): Flow<Boolean>
suspend fun store(key: OnrampSepaAvailabilityStoreKey, value: Boolean)
suspend fun clear()
}

View file

@ -0,0 +1,11 @@
package com.tangem.datasource.local.onramp.sepa
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.onramp.model.OnrampCountry
data class OnrampSepaAvailabilityStoreKey(
val userWallet: UserWallet,
val country: OnrampCountry,
val cryptoCurrency: CryptoCurrency,
)

View file

@ -27,10 +27,12 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.R import com.tangem.core.ui.R
import com.tangem.core.ui.components.* import com.tangem.core.ui.components.*
import com.tangem.core.ui.components.buttons.common.TangemButtonSize import com.tangem.core.ui.components.buttons.common.TangemButtonSize
import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resolveAnnotatedReference
import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemTheme
@ -216,7 +218,7 @@ internal fun TextsBlock(
SpacerH(height = TangemTheme.dimens.spacing2) SpacerH(height = TangemTheme.dimens.spacing2)
} }
val subtitleText = subtitle.resolveReference() val subtitleText = subtitle.resolveAnnotatedReference()
if (subtitleText.isNotEmpty()) { if (subtitleText.isNotEmpty()) {
Text( Text(
text = subtitleText, text = subtitleText,
@ -459,5 +461,15 @@ private class NotificationConfigProvider : CollectionPreviewParameterProvider<No
subtitle = resourceReference(id = R.string.information_generated_with_ai), subtitle = resourceReference(id = R.string.information_generated_with_ai),
iconResId = R.drawable.ic_magic_28, iconResId = R.drawable.ic_magic_28,
), ),
NotificationConfig(
title = resourceReference(R.string.notification_sepa_title),
subtitle = resourceReference(R.string.notification_sepa_text),
iconResId = R.drawable.img_notification_sepa,
buttonsState = NotificationConfig.ButtonsState.SecondaryButtonConfig(
text = resourceReference(R.string.notification_sepa_button),
onClick = { },
),
iconSize = 54.dp,
),
), ),
) )

View file

@ -30,6 +30,9 @@ import com.tangem.datasource.local.onramp.currencies.OnrampCurrenciesStore
import com.tangem.datasource.local.onramp.pairs.OnrampPairsStore import com.tangem.datasource.local.onramp.pairs.OnrampPairsStore
import com.tangem.datasource.local.onramp.paymentmethods.OnrampPaymentMethodsStore import com.tangem.datasource.local.onramp.paymentmethods.OnrampPaymentMethodsStore
import com.tangem.datasource.local.onramp.quotes.OnrampQuotesStore import com.tangem.datasource.local.onramp.quotes.OnrampQuotesStore
import com.tangem.datasource.local.onramp.sepa.OnrampCurrentCountryByIPStore
import com.tangem.datasource.local.onramp.sepa.OnrampSepaAvailabilityStore
import com.tangem.datasource.local.onramp.sepa.OnrampSepaAvailabilityStoreKey
import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.datasource.local.preferences.PreferencesKeys import com.tangem.datasource.local.preferences.PreferencesKeys
import com.tangem.datasource.local.preferences.utils.getObject import com.tangem.datasource.local.preferences.utils.getObject
@ -64,6 +67,8 @@ internal class DefaultOnrampRepository(
private val dispatchers: CoroutineDispatcherProvider, private val dispatchers: CoroutineDispatcherProvider,
private val appPreferencesStore: AppPreferencesStore, private val appPreferencesStore: AppPreferencesStore,
private val paymentMethodsStore: OnrampPaymentMethodsStore, private val paymentMethodsStore: OnrampPaymentMethodsStore,
private val onrampSepaAvailabilityStore: OnrampSepaAvailabilityStore,
private val onrampCurrentCountryByIPStore: OnrampCurrentCountryByIPStore,
private val pairsStore: OnrampPairsStore, private val pairsStore: OnrampPairsStore,
private val quotesStore: OnrampQuotesStore, private val quotesStore: OnrampQuotesStore,
private val countriesStore: OnrampCountriesStore, private val countriesStore: OnrampCountriesStore,
@ -123,8 +128,17 @@ internal class DefaultOnrampRepository(
result result
} }
override suspend fun getCountryByIp(userWallet: UserWallet): OnrampCountry = withContext(dispatchers.io) { override suspend fun getCountryByIp(userWallet: UserWallet, fromCache: Boolean): OnrampCountry =
onrampApi.getCountryByIp( withContext(dispatchers.io) {
if (fromCache) {
val country = onrampCurrentCountryByIPStore.getSyncOrNull()
if (country != null) {
return@withContext country
}
}
val country = onrampApi.getCountryByIp(
userWalletId = userWallet.walletId.stringValue, userWalletId = userWallet.walletId.stringValue,
refCode = ExpressUtils.getRefCode( refCode = ExpressUtils.getRefCode(
userWallet = userWallet, userWallet = userWallet,
@ -133,6 +147,10 @@ internal class DefaultOnrampRepository(
) )
.getOrThrow() .getOrThrow()
.let(countryConverter::convert) .let(countryConverter::convert)
onrampCurrentCountryByIPStore.store(country)
country
} }
override suspend fun getStatus(userWallet: UserWallet, txId: String): OnrampStatus = withContext(dispatchers.io) { override suspend fun getStatus(userWallet: UserWallet, txId: String): OnrampStatus = withContext(dispatchers.io) {
@ -263,11 +281,22 @@ internal class DefaultOnrampRepository(
override suspend fun hasSepaMethod( override suspend fun hasSepaMethod(
userWallet: UserWallet, userWallet: UserWallet,
currency: OnrampCurrency,
country: OnrampCountry, country: OnrampCountry,
cryptoCurrency: CryptoCurrency, cryptoCurrency: CryptoCurrency,
): Boolean { ): Boolean {
return withContext(dispatchers.io) { return withContext(dispatchers.io) {
val key = OnrampSepaAvailabilityStoreKey(
userWallet = userWallet,
country = country,
cryptoCurrency = cryptoCurrency,
)
val cachedValue = onrampSepaAvailabilityStore.getSyncOrNull(key)
if (cachedValue != null) {
return@withContext cachedValue
}
val onrampPairs = val onrampPairs =
safeApiCall( safeApiCall(
call = { call = {
@ -300,6 +329,8 @@ internal class DefaultOnrampRepository(
.flatMap { it.paymentMethods } .flatMap { it.paymentMethods }
.any { it == SEPA_METHOD_ID } .any { it == SEPA_METHOD_ID }
onrampSepaAvailabilityStore.store(key, hasSepaMethod)
hasSepaMethod hasSepaMethod
} }
} }

View file

@ -22,6 +22,8 @@ import com.tangem.datasource.local.onramp.currencies.OnrampCurrenciesStore
import com.tangem.datasource.local.onramp.pairs.OnrampPairsStore import com.tangem.datasource.local.onramp.pairs.OnrampPairsStore
import com.tangem.datasource.local.onramp.paymentmethods.OnrampPaymentMethodsStore import com.tangem.datasource.local.onramp.paymentmethods.OnrampPaymentMethodsStore
import com.tangem.datasource.local.onramp.quotes.OnrampQuotesStore import com.tangem.datasource.local.onramp.quotes.OnrampQuotesStore
import com.tangem.datasource.local.onramp.sepa.OnrampCurrentCountryByIPStore
import com.tangem.datasource.local.onramp.sepa.OnrampSepaAvailabilityStore
import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.datasource.local.token.UserTokensResponseStore import com.tangem.datasource.local.token.UserTokensResponseStore
import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.datasource.local.userwallet.UserWalletsStore
@ -52,6 +54,8 @@ internal object OnrampDataModule {
currenciesStore: OnrampCurrenciesStore, currenciesStore: OnrampCurrenciesStore,
walletManagersFacade: WalletManagersFacade, walletManagersFacade: WalletManagersFacade,
dataSignatureVerifier: DataSignatureVerifier, dataSignatureVerifier: DataSignatureVerifier,
onrampSepaAvailabilityStore: OnrampSepaAvailabilityStore,
onrampCurrentCountryByIPStore: OnrampCurrentCountryByIPStore,
@NetworkMoshi moshi: Moshi, @NetworkMoshi moshi: Moshi,
): OnrampRepository { ): OnrampRepository {
return DefaultOnrampRepository( return DefaultOnrampRepository(
@ -60,6 +64,8 @@ internal object OnrampDataModule {
dispatchers = dispatchers, dispatchers = dispatchers,
appPreferencesStore = appPreferencesStore, appPreferencesStore = appPreferencesStore,
paymentMethodsStore = paymentMethodsStore, paymentMethodsStore = paymentMethodsStore,
onrampSepaAvailabilityStore = onrampSepaAvailabilityStore,
onrampCurrentCountryByIPStore = onrampCurrentCountryByIPStore,
pairsStore = pairsStore, pairsStore = pairsStore,
quotesStore = quotesStore, quotesStore = quotesStore,
currenciesStore = currenciesStore, currenciesStore = currenciesStore,

View file

@ -1,5 +1,6 @@
package com.tangem.data.promo package com.tangem.data.promo
import com.tangem.data.promo.converters.PromoBannerConverter
import com.tangem.data.promo.converters.StoryContentResponseConverter import com.tangem.data.promo.converters.StoryContentResponseConverter
import com.tangem.datasource.api.common.response.getOrThrow import com.tangem.datasource.api.common.response.getOrThrow
import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.api.tangemTech.TangemTechApi
@ -12,6 +13,7 @@ import com.tangem.datasource.local.preferences.utils.store
import com.tangem.datasource.local.promo.PromoStoriesStore import com.tangem.datasource.local.promo.PromoStoriesStore
import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.promo.PromoRepository import com.tangem.domain.promo.PromoRepository
import com.tangem.domain.promo.models.PromoBanner
import com.tangem.domain.promo.models.PromoId import com.tangem.domain.promo.models.PromoId
import com.tangem.domain.promo.models.StoryContent import com.tangem.domain.promo.models.StoryContent
import com.tangem.feature.referral.domain.ReferralRepository import com.tangem.feature.referral.domain.ReferralRepository
@ -22,6 +24,7 @@ import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.mapLatest import kotlinx.coroutines.flow.mapLatest
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import kotlinx.coroutines.withTimeoutOrNull import kotlinx.coroutines.withTimeoutOrNull
import com.tangem.utils.coroutines.runCatching
internal class DefaultPromoRepository( internal class DefaultPromoRepository(
private val tangemApi: TangemTechApi, private val tangemApi: TangemTechApi,
@ -32,6 +35,7 @@ internal class DefaultPromoRepository(
) : PromoRepository { ) : PromoRepository {
private val storyContentConverter = StoryContentResponseConverter() private val storyContentConverter = StoryContentResponseConverter()
private val promoBannerConverter = PromoBannerConverter()
override fun isReadyToShowWalletPromo(userWalletId: UserWalletId, promoId: PromoId): Flow<Boolean> { override fun isReadyToShowWalletPromo(userWalletId: UserWalletId, promoId: PromoId): Flow<Boolean> {
return appPreferencesStore.get( return appPreferencesStore.get(
@ -42,7 +46,11 @@ internal class DefaultPromoRepository(
PromoId.Referral -> runCatching { PromoId.Referral -> runCatching {
!referralRepository.isReferralParticipant(userWalletId) && shouldShow !referralRepository.isReferralParticipant(userWalletId) && shouldShow
}.getOrDefault(false) }.getOrDefault(false)
PromoId.Sepa -> shouldShow PromoId.Sepa -> {
val isActive = getSepaPromoBanner()?.isActive ?: false
isActive && shouldShow
}
} }
} }
} }
@ -118,7 +126,16 @@ internal class DefaultPromoRepository(
) )
} }
private suspend fun getSepaPromoBanner(): PromoBanner? {
return runCatching(dispatchers.io) {
promoBannerConverter.convert(
tangemApi.getPromoBanner(SEPA_NAME).getOrThrow(),
)
}.getOrNull()
}
private companion object { private companion object {
const val SEPA_NAME = "sepa"
const val STORIES_LOAD_DELAY = 1000L const val STORIES_LOAD_DELAY = 1000L
} }
} }

View file

@ -1,13 +1,13 @@
package com.tangem.data.promo.converters package com.tangem.data.promo.converters
import com.tangem.datasource.api.promotion.models.PromotionInfoResponse import com.tangem.datasource.api.promotion.models.PromoBannerResponse
import com.tangem.domain.promo.models.PromoBanner import com.tangem.domain.promo.models.PromoBanner
import com.tangem.utils.converter.Converter import com.tangem.utils.converter.Converter
import org.joda.time.DateTime import org.joda.time.DateTime
class PromoResponseConverter : Converter<PromotionInfoResponse, PromoBanner?> { class PromoBannerConverter : Converter<PromoBannerResponse, PromoBanner?> {
override fun convert(value: PromotionInfoResponse): PromoBanner? { override fun convert(value: PromoBannerResponse): PromoBanner? {
val bannerState = value.bannerState ?: return null val bannerState = value.bannerState ?: return null
return PromoBanner( return PromoBanner(
name = value.name, name = value.name,

View file

@ -650,7 +650,7 @@ internal class DefaultWalletManagersFacade @Inject constructor(
} }
} }
override suspend fun checkUtxoConsolidationAvailability(userWalletId: UserWalletId, network: Network): Boolean { override suspend fun checkSelfSendAvailability(userWalletId: UserWalletId, network: Network): Boolean {
val blockchain = network.toBlockchain() val blockchain = network.toBlockchain()
val walletManager = getOrCreateWalletManager( val walletManager = getOrCreateWalletManager(
userWalletId = userWalletId, userWalletId = userWalletId,
@ -658,7 +658,7 @@ internal class DefaultWalletManagersFacade @Inject constructor(
derivationPath = network.derivationPath.value, derivationPath = network.derivationPath.value,
) ?: return false ) ?: return false
return (walletManager as? UtxoBlockchainManager)?.allowConsolidation == true return walletManager.isSelfSendAvailable
} }
override suspend fun getNFTCollections(userWalletId: UserWalletId, network: Network): List<NFTCollection> { override suspend fun getNFTCollections(userWalletId: UserWalletId, network: Network): List<NFTCollection> {

View file

@ -83,7 +83,7 @@ internal class DefaultYieldSupplyTransactionRepository(
walletManager = walletManager, walletManager = walletManager,
cryptoCurrency = cryptoCurrency, cryptoCurrency = cryptoCurrency,
callData = callData, callData = callData,
destinationAddress = walletManager.getYieldContract(), destinationAddress = walletManager.getYieldModuleAddress(),
amount = BigDecimal.ZERO.convertToSdkAmount(cryptoCurrencyStatus), amount = BigDecimal.ZERO.convertToSdkAmount(cryptoCurrencyStatus),
fee = fee, fee = fee,
) )
@ -175,7 +175,7 @@ internal class DefaultYieldSupplyTransactionRepository(
blockchain = cryptoCurrency.network.toBlockchain(), blockchain = cryptoCurrency.network.toBlockchain(),
derivationPath = cryptoCurrency.network.derivationPath.value, derivationPath = cryptoCurrency.network.derivationPath.value,
) ?: error("Wallet manager not found") ) ?: error("Wallet manager not found")
walletManager.calculateYieldContract() walletManager.calculateYieldModuleAddress()
}.onFailure(Timber::e).getOrNull() }.onFailure(Timber::e).getOrNull()
} }
@ -188,7 +188,7 @@ internal class DefaultYieldSupplyTransactionRepository(
blockchain = cryptoCurrency.network.toBlockchain(), blockchain = cryptoCurrency.network.toBlockchain(),
derivationPath = cryptoCurrency.network.derivationPath.value, derivationPath = cryptoCurrency.network.derivationPath.value,
) ?: error("Wallet manager not found") ) ?: error("Wallet manager not found")
walletManager.getYieldContract() walletManager.getYieldModuleAddress()
}.onFailure(Timber::e).getOrNull() }.onFailure(Timber::e).getOrNull()
} }

View file

@ -26,7 +26,12 @@ class GetOnrampCountryUseCase(
} }
suspend fun invokeSync(userWallet: UserWallet): Either<OnrampError, OnrampCountry> { suspend fun invokeSync(userWallet: UserWallet): Either<OnrampError, OnrampCountry> {
return Either.catch { repository.getDefaultCountrySync() ?: repository.getCountryByIp(userWallet) } return Either.catch {
repository.getDefaultCountrySync() ?: repository.getCountryByIp(
userWallet = userWallet,
fromCache = true,
)
}
.mapLeft(errorResolver::resolve) .mapLeft(errorResolver::resolve)
} }
} }

View file

@ -6,7 +6,6 @@ import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.onramp.repositories.OnrampRepository import com.tangem.domain.onramp.repositories.OnrampRepository
import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.onramp.model.OnrampCountry import com.tangem.domain.onramp.model.OnrampCountry
import com.tangem.domain.onramp.model.OnrampCurrency
class OnrampSepaAvailableUseCase( class OnrampSepaAvailableUseCase(
private val repository: OnrampRepository, private val repository: OnrampRepository,
@ -14,7 +13,6 @@ class OnrampSepaAvailableUseCase(
suspend operator fun invoke( suspend operator fun invoke(
userWallet: UserWallet, userWallet: UserWallet,
currency: OnrampCurrency,
country: OnrampCountry, country: OnrampCountry,
cryptoCurrency: CryptoCurrency, cryptoCurrency: CryptoCurrency,
): Boolean { ): Boolean {
@ -25,7 +23,6 @@ class OnrampSepaAvailableUseCase(
return Either.catch { return Either.catch {
repository.hasSepaMethod( repository.hasSepaMethod(
userWallet = userWallet, userWallet = userWallet,
currency = currency,
country = country, country = country,
cryptoCurrency = cryptoCurrency, cryptoCurrency = cryptoCurrency,
) )

View file

@ -13,14 +13,9 @@ interface OnrampRepository {
fun getCurrencies(): Flow<List<OnrampCurrency>> fun getCurrencies(): Flow<List<OnrampCurrency>>
fun getCountries(): Flow<List<OnrampCountry>> fun getCountries(): Flow<List<OnrampCountry>>
suspend fun getCountriesSync(): List<OnrampCountry>? suspend fun getCountriesSync(): List<OnrampCountry>?
suspend fun getCountryByIp(userWallet: UserWallet): OnrampCountry suspend fun getCountryByIp(userWallet: UserWallet, fromCache: Boolean = false): OnrampCountry
suspend fun getStatus(userWallet: UserWallet, txId: String): OnrampStatus suspend fun getStatus(userWallet: UserWallet, txId: String): OnrampStatus
suspend fun hasSepaMethod( suspend fun hasSepaMethod(userWallet: UserWallet, country: OnrampCountry, cryptoCurrency: CryptoCurrency): Boolean
userWallet: UserWallet,
currency: OnrampCurrency,
country: OnrampCountry,
cryptoCurrency: CryptoCurrency,
): Boolean
suspend fun fetchCurrencies(userWallet: UserWallet) suspend fun fetchCurrencies(userWallet: UserWallet)
suspend fun fetchCountries(userWallet: UserWallet): List<OnrampCountry> suspend fun fetchCountries(userWallet: UserWallet): List<OnrampCountry>
suspend fun fetchPaymentMethodsIfAbsent(userWallet: UserWallet) suspend fun fetchPaymentMethodsIfAbsent(userWallet: UserWallet)

View file

@ -154,6 +154,11 @@ sealed class StakingAnalyticsEvent(
data object TransactionError : StakingAnalyticsEvent( data object TransactionError : StakingAnalyticsEvent(
event = "Error - Transaction Rejected", event = "Error - Transaction Rejected",
) )
data class UnitializedAddress(val token: String) : StakingAnalyticsEvent(
event = "Notice - Uninitialized Address",
params = mapOf(AnalyticsParam.TOKEN_PARAM to token),
)
} }
enum class StakeScreenSource { enum class StakeScreenSource {

View file

@ -5,14 +5,14 @@ import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.models.wallet.UserWalletId
/** /**
* Gets UTXO consolidation availability * Gets self send availability
*/ */
class IsUtxoConsolidationAvailableUseCase( class IsSelfSendAvailableUseCase(
private val walletManagersFacade: WalletManagersFacade, private val walletManagersFacade: WalletManagersFacade,
) { ) {
suspend fun invokeSync(userWalletId: UserWalletId, network: Network) = suspend fun invokeSync(userWalletId: UserWalletId, network: Network) =
walletManagersFacade.checkUtxoConsolidationAvailability( walletManagersFacade.checkSelfSendAvailability(
userWalletId = userWalletId, userWalletId = userWalletId,
network = network, network = network,
) )

View file

@ -56,12 +56,11 @@ class ValidateWalletAddressUseCase(
isCurrentAddress: (String) -> Boolean, isCurrentAddress: (String) -> Boolean,
): AddressValidationResult { ): AddressValidationResult {
val decodedXAddress = BlockchainUtils.decodeRippleXAddress(address, network.rawId) val decodedXAddress = BlockchainUtils.decodeRippleXAddress(address, network.rawId)
val isUtxoConsolidationAvailable = val isSelfSendAvailable = walletManagersFacade.checkSelfSendAvailability(userWalletId, network)
walletManagersFacade.checkUtxoConsolidationAvailability(userWalletId, network)
val addressToValidate = decodedXAddress?.address ?: address val addressToValidate = decodedXAddress?.address ?: address
val current = isCurrentAddress(addressToValidate) val current = isCurrentAddress(addressToValidate)
val isForbidSelfSend = current && !isUtxoConsolidationAvailable val isForbidSelfSend = current && !isSelfSendAvailable
val isValidAddress = walletAddressServiceRepository.validateAddress(userWalletId, network, addressToValidate) val isValidAddress = walletAddressServiceRepository.validateAddress(userWalletId, network, addressToValidate)
return when { return when {

View file

@ -243,12 +243,12 @@ interface WalletManagersFacade {
suspend fun discardRequirements(userWalletId: UserWalletId, currency: CryptoCurrency): SimpleResult suspend fun discardRequirements(userWalletId: UserWalletId, currency: CryptoCurrency): SimpleResult
/** /**
* Indicates UTXO consolidation availability * Indicates self send availability
* *
* @param userWalletId selected user wallet * @param userWalletId selected user wallet
* @param network availability for network * @param network availability for network
*/ */
suspend fun checkUtxoConsolidationAvailability(userWalletId: UserWalletId, network: Network): Boolean suspend fun checkSelfSendAvailability(userWalletId: UserWalletId, network: Network): Boolean
suspend fun getNFTCollections(userWalletId: UserWalletId, network: Network): List<NFTCollection> suspend fun getNFTCollections(userWalletId: UserWalletId, network: Network): List<NFTCollection>

View file

@ -70,8 +70,11 @@ internal class ManageTokensUiManager(
val updatedBatch = uiBatchToUpdate.copy( val updatedBatch = uiBatchToUpdate.copy(
data = data.mapIndexed { index, item -> data = data.mapIndexed { index, item ->
if (item == currencyBatch.data[index]) { val uiBatch = uiBatchToUpdate.data.getOrNull(index)
return@mapIndexed uiBatchToUpdate.data[index] if (item == currencyBatch.data.getOrNull(index) &&
uiBatch != null
) {
return@mapIndexed uiBatch
} }
val previousUiItem = uiBatchToUpdate.data.getOrNull(index) val previousUiItem = uiBatchToUpdate.data.getOrNull(index)

View file

@ -63,7 +63,6 @@ internal class OnboardingNoteCreateWalletModel @Inject constructor(
when (result) { when (result) {
is CompletionResult.Success -> { is CompletionResult.Success -> {
Analytics.send(OnboardingEvent.CreateWallet.WalletCreatedSuccessfully()) Analytics.send(OnboardingEvent.CreateWallet.WalletCreatedSuccessfully())
cardRepository.startCardActivation(scanResponse.card.cardId)
createWalletAndNavigateBackWithDone(scanResponse.copy(card = result.data.card)) createWalletAndNavigateBackWithDone(scanResponse.copy(card = result.data.card))
} }
is CompletionResult.Failure -> _uiState.update { is CompletionResult.Failure -> _uiState.update {

View file

@ -20,7 +20,7 @@ import com.tangem.domain.qrscanning.usecases.ListenToQrScanningUseCase
import com.tangem.domain.qrscanning.usecases.ParseQrCodeUseCase import com.tangem.domain.qrscanning.usecases.ParseQrCodeUseCase
import com.tangem.domain.tokens.GetCryptoCurrencyUseCase import com.tangem.domain.tokens.GetCryptoCurrencyUseCase
import com.tangem.domain.tokens.GetNetworkAddressesUseCase import com.tangem.domain.tokens.GetNetworkAddressesUseCase
import com.tangem.domain.transaction.usecase.IsUtxoConsolidationAvailableUseCase import com.tangem.domain.transaction.usecase.IsSelfSendAvailableUseCase
import com.tangem.domain.transaction.usecase.ValidateWalletAddressUseCase import com.tangem.domain.transaction.usecase.ValidateWalletAddressUseCase
import com.tangem.domain.transaction.usecase.ValidateWalletMemoUseCase import com.tangem.domain.transaction.usecase.ValidateWalletMemoUseCase
import com.tangem.domain.txhistory.usecase.GetFixedTxHistoryItemsUseCase import com.tangem.domain.txhistory.usecase.GetFixedTxHistoryItemsUseCase
@ -62,7 +62,7 @@ internal class SendDestinationModel @Inject constructor(
private val getCryptoCurrencyUseCase: GetCryptoCurrencyUseCase, private val getCryptoCurrencyUseCase: GetCryptoCurrencyUseCase,
private val getNetworkAddressesUseCase: GetNetworkAddressesUseCase, private val getNetworkAddressesUseCase: GetNetworkAddressesUseCase,
private val getFixedTxHistoryItemsUseCase: GetFixedTxHistoryItemsUseCase, private val getFixedTxHistoryItemsUseCase: GetFixedTxHistoryItemsUseCase,
private val isUtxoConsolidationAvailableUseCase: IsUtxoConsolidationAvailableUseCase, private val isSelfSendAvailableUseCase: IsSelfSendAvailableUseCase,
private val listenToQrScanningUseCase: ListenToQrScanningUseCase, private val listenToQrScanningUseCase: ListenToQrScanningUseCase,
private val parseQrCodeUseCase: ParseQrCodeUseCase, private val parseQrCodeUseCase: ParseQrCodeUseCase,
private val analyticsEventHandler: AnalyticsEventHandler, private val analyticsEventHandler: AnalyticsEventHandler,
@ -197,7 +197,7 @@ internal class SendDestinationModel @Inject constructor(
waitForDelay(RECENT_LOAD_DELAY) { it } waitForDelay(RECENT_LOAD_DELAY) { it }
}.conflate(), }.conflate(),
) { destinationWalletList, txHistoryList -> ) { destinationWalletList, txHistoryList ->
val isUtxoConsolidationAvailable = isUtxoConsolidationAvailableUseCase.invokeSync( val isSelfSendAvailable = isSelfSendAvailableUseCase.invokeSync(
userWalletId = userWalletId, userWalletId = userWalletId,
network = cryptoCurrency.network, network = cryptoCurrency.network,
) )
@ -206,7 +206,7 @@ internal class SendDestinationModel @Inject constructor(
SendDestinationRecentListTransformer( SendDestinationRecentListTransformer(
cryptoCurrency = cryptoCurrency, cryptoCurrency = cryptoCurrency,
senderAddress = senderAddresses.value.firstOrNull()?.address, senderAddress = senderAddresses.value.firstOrNull()?.address,
isUtxoConsolidationAvailable = isUtxoConsolidationAvailable, isSelfSendAvailable = isSelfSendAvailable,
destinationWalletList = destinationWalletList, destinationWalletList = destinationWalletList,
txHistoryList = txHistoryList, txHistoryList = txHistoryList,
), ),

View file

@ -13,7 +13,7 @@ import kotlinx.collections.immutable.toPersistentList
internal class SendRecipientWalletListConverter( internal class SendRecipientWalletListConverter(
private val senderAddress: String?, private val senderAddress: String?,
private val isUtxoConsolidationAvailable: Boolean, private val isSelfSendAvailable: Boolean,
) : ) :
Converter<List<DestinationWalletUM?>, PersistentList<DestinationRecipientListUM>> { Converter<List<DestinationWalletUM?>, PersistentList<DestinationRecipientListUM>> {
override fun convert(value: List<DestinationWalletUM?>): PersistentList<DestinationRecipientListUM> { override fun convert(value: List<DestinationWalletUM?>): PersistentList<DestinationRecipientListUM> {
@ -31,7 +31,7 @@ internal class SendRecipientWalletListConverter(
val isNotSameAddress = it.address != senderAddress val isNotSameAddress = it.address != senderAddress
val isNotBlankAddress = it.address.isNotBlank() val isNotBlankAddress = it.address.isNotBlank()
isNotBlankAddress && isCoin && (isNotSameAddress || isUtxoConsolidationAvailable) isNotBlankAddress && isCoin && (isNotSameAddress || isSelfSendAvailable)
} }
.groupBy { item -> item.name } .groupBy { item -> item.name }
.values.map { wallets -> .values.map { wallets ->

View file

@ -11,7 +11,7 @@ import com.tangem.utils.transformer.Transformer
internal class SendDestinationRecentListTransformer( internal class SendDestinationRecentListTransformer(
private val senderAddress: String?, private val senderAddress: String?,
private val cryptoCurrency: CryptoCurrency, private val cryptoCurrency: CryptoCurrency,
private val isUtxoConsolidationAvailable: Boolean, private val isSelfSendAvailable: Boolean,
private val destinationWalletList: List<DestinationWalletUM>, private val destinationWalletList: List<DestinationWalletUM>,
private val txHistoryList: List<TxInfo>, private val txHistoryList: List<TxInfo>,
) : Transformer<DestinationUM> { ) : Transformer<DestinationUM> {
@ -21,7 +21,7 @@ internal class SendDestinationRecentListTransformer(
return state.copy( return state.copy(
wallets = SendRecipientWalletListConverter( wallets = SendRecipientWalletListConverter(
senderAddress = senderAddress, senderAddress = senderAddress,
isUtxoConsolidationAvailable = isUtxoConsolidationAvailable, isSelfSendAvailable = isSelfSendAvailable,
).convert(destinationWalletList), ).convert(destinationWalletList),
recent = SendRecipientHistoryListConverter( recent = SendRecipientHistoryListConverter(
cryptoCurrency = cryptoCurrency, cryptoCurrency = cryptoCurrency,

View file

@ -261,6 +261,9 @@ internal class StakingModel @Inject constructor(
return@launch return@launch
} }
isInitialInfoStep && noBalanceState && !isAccountInitialized -> { isInitialInfoStep && noBalanceState && !isAccountInitialized -> {
analyticsEventHandler.send(StakingAnalyticsEvent.UnitializedAddress(
token = cryptoCurrencyStatus.currency.symbol,
))
stakingEventFactory.createInitializeAccountAlert() stakingEventFactory.createInitializeAccountAlert()
return@launch return@launch
} }

View file

@ -278,7 +278,6 @@ internal class GetMultiWalletWarningsFactory @Inject constructor(
val isSepaAvailable = onrampSepaAvailableUseCase( val isSepaAvailable = onrampSepaAvailableUseCase(
userWallet = userWallet, userWallet = userWallet,
country = country, country = country,
currency = country.defaultCurrency,
cryptoCurrency = bitcoinCurrency, cryptoCurrency = bitcoinCurrency,
) )

View file

@ -5,7 +5,7 @@
# https://github.com/tangem/tangem-sdk-android/ # https://github.com/tangem/tangem-sdk-android/
# https://github.com/tangem/vico # https://github.com/tangem/vico
tangemBlockchainSdk = "develop-1236" tangemBlockchainSdk = "develop-1240"
#tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds
tangemCardSdk = "develop-564" tangemCardSdk = "develop-564"
#tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^