Updated on 2026-08-14

This commit is contained in:
Tangem 2026-02-25 22:27:04 +07:00
commit 9b043d6703
42 changed files with 648 additions and 382 deletions

View file

@ -1,7 +1,9 @@
package com.tangem.tap.data
import android.content.Context
import com.squareup.moshi.JsonAdapter
import com.squareup.moshi.Moshi
import com.squareup.moshi.Types
import com.tangem.data.pay.entity.WithdrawStoreData
import com.tangem.data.pay.util.WithdrawStateConverter
import com.tangem.data.pay.util.WithdrawStoreDataConverter
@ -19,6 +21,7 @@ import com.tangem.domain.visa.model.TangemPayAuthTokens
import com.tangem.sdk.storage.AndroidSecureStorageV2
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.withContext
import java.util.UUID
import javax.inject.Inject
@ -137,28 +140,56 @@ internal class DefaultTangemPayStorage @Inject constructor(
return appPreferencesStore.getSyncOrNull(PreferencesKeys.getTangemPayCheckCustomerByWalletId(userWalletId))
}
override suspend fun storeWithdrawOrder(userWalletId: UserWalletId, data: TangemPayWithdrawState) {
override suspend fun storeActiveWithdrawOrderId(userWalletId: UserWalletId, orderId: String) {
appPreferencesStore.editData { mutablePreferences ->
val orders = mutablePreferences.getObjectMap<TangemPayWithdrawState>(
PreferencesKeys.TANGEM_PAY_WITHDRAW_ORDERS_KEY,
)
.plus(createWithdrawOrderIdKey(userWalletId) to withdrawStoreDataConverter.convert(data))
val orders = mutablePreferences.getObjectMap<String>(PreferencesKeys.TANGEM_PAY_ACTIVE_WITHDRAW_ORDERS_KEY)
.plus(createWithdrawOrderIdKey(userWalletId) to orderId)
mutablePreferences.setObjectMap(
key = PreferencesKeys.TANGEM_PAY_WITHDRAW_ORDERS_KEY,
key = PreferencesKeys.TANGEM_PAY_ACTIVE_WITHDRAW_ORDERS_KEY,
value = orders,
)
}
}
override suspend fun getWithdrawOrder(userWalletId: UserWalletId): TangemPayWithdrawState? {
val orders = appPreferencesStore.getObjectMapSync<WithdrawStoreData>(
PreferencesKeys.TANGEM_PAY_WITHDRAW_ORDERS_KEY,
)
val data = orders[createWithdrawOrderIdKey(userWalletId)] ?: return null
return withdrawStateConverter.convert(data)
override suspend fun storeWithdrawOrder(userWalletId: UserWalletId, data: TangemPayWithdrawState) {
val listType = Types.newParameterizedType(List::class.java, WithdrawStoreData::class.java)
val mapType = Types.newParameterizedType(Map::class.java, String::class.java, listType)
val adapter: JsonAdapter<Map<String, List<WithdrawStoreData>>> = appPreferencesStore.moshi.adapter(mapType)
appPreferencesStore.editData { prefs ->
val walletKey = createWithdrawOrderIdKey(userWalletId)
val currentMap = prefs[PreferencesKeys.TANGEM_PAY_WITHDRAW_ORDERS_KEY]?.let(adapter::fromJson).orEmpty()
val updatedList = currentMap[walletKey].orEmpty() + withdrawStoreDataConverter.convert(data)
prefs[PreferencesKeys.TANGEM_PAY_WITHDRAW_ORDERS_KEY] = adapter
.toJson(currentMap + (walletKey to updatedList))
}
}
override suspend fun deleteWithdrawOrder(userWalletId: UserWalletId) {
override suspend fun getActiveWithdrawOrderId(userWalletId: UserWalletId): String? {
val orders = appPreferencesStore.getObjectMapSync<String>(PreferencesKeys.TANGEM_PAY_ACTIVE_WITHDRAW_ORDERS_KEY)
return orders[createWithdrawOrderIdKey(userWalletId)]
}
override suspend fun getWithdrawOrders(userWalletId: UserWalletId): List<TangemPayWithdrawState> {
val listType = Types.newParameterizedType(List::class.java, WithdrawStoreData::class.java)
val mapType = Types.newParameterizedType(Map::class.java, String::class.java, listType)
val adapter: JsonAdapter<Map<String, List<WithdrawStoreData>>> = appPreferencesStore.moshi.adapter(mapType)
val map = appPreferencesStore.data.firstOrNull()
?.get(PreferencesKeys.TANGEM_PAY_WITHDRAW_ORDERS_KEY)?.let(adapter::fromJson).orEmpty()
return map[createWithdrawOrderIdKey(userWalletId)].orEmpty().map(withdrawStateConverter::convert)
}
override suspend fun deleteActiveWithdrawOrder(userWalletId: UserWalletId) {
appPreferencesStore.editData { mutablePreferences ->
val orders = mutablePreferences.getObjectMap<String>(PreferencesKeys.TANGEM_PAY_ACTIVE_WITHDRAW_ORDERS_KEY)
.minus(createWithdrawOrderIdKey(userWalletId))
mutablePreferences.setObjectMap(
key = PreferencesKeys.TANGEM_PAY_ACTIVE_WITHDRAW_ORDERS_KEY,
value = orders,
)
}
}
override suspend fun deleteWithdrawOrder(userWalletId: UserWalletId, orderId: String) {
appPreferencesStore.editData { mutablePreferences ->
val orders = mutablePreferences.getObjectMap<WithdrawStoreData>(
PreferencesKeys.TANGEM_PAY_WITHDRAW_ORDERS_KEY,

View file

@ -1,9 +1,9 @@
package com.tangem.tap.di.domain
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.domain.account.repository.AccountsCRUDRepository
import com.tangem.domain.common.wallets.UserWalletsListRepository
import com.tangem.domain.redux.ReduxStateHolder
import com.tangem.domain.tokens.repository.CurrenciesRepository
import com.tangem.domain.transaction.WalletAddressServiceRepository
import com.tangem.domain.transaction.usecase.ParseSharedAddressUseCase
import com.tangem.domain.transaction.usecase.ValidateWalletAddressUseCase
@ -272,11 +272,11 @@ internal object WalletsDomainModule {
@Singleton
fun providesSetNotificationsEnabledUseCase(
walletsRepository: WalletsRepository,
currenciesRepository: CurrenciesRepository,
accountsCRUDRepository: AccountsCRUDRepository,
): SetNotificationsEnabledUseCase {
return SetNotificationsEnabledUseCase(
walletsRepository = walletsRepository,
currenciesRepository = currenciesRepository,
accountsCRUDRepository = accountsCRUDRepository,
)
}

View file

@ -19,15 +19,15 @@ import com.tangem.core.ui.R
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.toWrappedList
import com.tangem.core.ui.message.dialog.Dialogs
import com.tangem.domain.common.extensions.withMainContext
import com.tangem.domain.card.common.util.twinsIsTwinned
import com.tangem.domain.common.extensions.withMainContext
import com.tangem.domain.feedback.models.FeedbackEmailType
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.wallets.builder.UserWalletIdBuilder
import com.tangem.sdk.extensions.localizedDescriptionRes
import com.tangem.tap.common.analytics.paramsInterceptor.CardContextInterceptor
import com.tangem.tap.common.extensions.*
import com.tangem.tap.common.redux.AppDialog
import com.tangem.tap.common.extensions.dispatchNavigationAction
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.extensions.inject
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.features.disclaimer.createDisclaimer
import com.tangem.tap.features.onboarding.OnboardingHelper
@ -113,7 +113,6 @@ internal class LegacyScanProcessor @Inject constructor(
onProgressStateChange = onProgressStateChange,
onSuccess = onSuccess,
onWalletNotCreated = onWalletNotCreated,
onCancel = onCancel,
)
},
)
@ -198,90 +197,38 @@ internal class LegacyScanProcessor @Inject constructor(
scanResponse: ScanResponse,
crossinline onProgressStateChange: suspend (showProgress: Boolean) -> Unit,
crossinline onWalletNotCreated: suspend () -> Unit,
crossinline onCancel: suspend () -> Unit,
crossinline onSuccess: suspend (ScanResponse) -> Unit,
) {
checkCardWasUsedInApp(
scanResponse = scanResponse,
onCancel = {
mainScope.launch {
onProgressStateChange.invoke(false)
onCancel()
}
},
) {
if (OnboardingHelper.isOnboardingCase(scanResponse)) {
trackingContextProxy.addContext(scanResponse)
if (OnboardingHelper.isOnboardingCase(scanResponse)) {
trackingContextProxy.addContext(scanResponse)
onWalletNotCreated()
navigateTo(
AppRoute.Onboarding(
scanResponse = scanResponse,
mode = AppRoute.Onboarding.Mode.Onboarding,
),
) { onProgressStateChange(it) }
} else {
trackingContextProxy.setContext(scanResponse)
val wasTwinsOnboardingShown =
store.inject(DaggerGraphState::wasTwinsOnboardingShownUseCase).invokeSync()
if (scanResponse.twinsIsTwinned() && !wasTwinsOnboardingShown) {
onWalletNotCreated()
navigateTo(
AppRoute.Onboarding(
scanResponse = scanResponse,
mode = AppRoute.Onboarding.Mode.Onboarding,
mode = AppRoute.Onboarding.Mode.WelcomeOnlyTwin,
),
) { onProgressStateChange(it) }
} else {
trackingContextProxy.setContext(scanResponse)
val wasTwinsOnboardingShown =
store.inject(DaggerGraphState::wasTwinsOnboardingShownUseCase).invokeSync()
if (scanResponse.twinsIsTwinned() && !wasTwinsOnboardingShown) {
onWalletNotCreated()
navigateTo(
AppRoute.Onboarding(
scanResponse = scanResponse,
mode = AppRoute.Onboarding.Mode.WelcomeOnlyTwin,
),
) { onProgressStateChange(it) }
} else {
delay(DELAY_SDK_DIALOG_CLOSE)
onSuccess(scanResponse)
}
delay(DELAY_SDK_DIALOG_CLOSE)
onSuccess(scanResponse)
}
}
}
/**
* Checks if card has password and never login at this app
* Show alert in this case
*/
private suspend fun checkCardWasUsedInApp(
scanResponse: ScanResponse,
onCancel: () -> Unit,
onSuccess: suspend () -> Unit,
) {
val userWalletId = runCatching { UserWalletIdBuilder.card(scanResponse.card).build() }.getOrNull()
if (userWalletId == null) {
onSuccess()
return
}
val userTokensResponseStore = store.inject(DaggerGraphState::userTokensResponseStore)
val tokens = userTokensResponseStore.getSyncOrNull(userWalletId = userWalletId)
if (scanResponse.card.isAccessCodeSet && tokens == null) {
store.dispatchDialogShow(
AppDialog.WalletAlreadyWasUsedDialog(
onOk = { mainScope.launch { onSuccess() } },
onSupportClick = {
val cardInfo =
store.inject(DaggerGraphState::getWalletMetaInfoUseCase).invoke(scanResponse).getOrNull()
?: error("CardInfo must be not null")
scope.launch {
store.inject(DaggerGraphState::sendFeedbackEmailUseCase)
.invoke(type = FeedbackEmailType.PreActivatedWallet(cardInfo))
}
onCancel()
},
onCancel = { onCancel() },
),
)
} else {
onSuccess()
}
}
private suspend inline fun navigateTo(route: AppRoute, onProgressStateChange: (showProgress: Boolean) -> Unit) {
delay(DELAY_SDK_DIALOG_CLOSE)
store.dispatchNavigationAction { push(route) }

View file

@ -173,6 +173,7 @@ internal class DefaultTangemSdkManager(
allowsRequestAccessCodeFromRepository = allowsRequestAccessCodeFromRepository,
visaCardScanHandler = visaCardScanHandler,
visaCoroutineScope = this,
shouldCheckIsAlreadyActivated = true,
onboardingV2FeatureToggles = onboardingV2FeatureToggles,
),
cardId = cardId,

View file

@ -27,6 +27,7 @@ internal class ResetBackupCardTask(
PreflightReadTask(
readMode = PreflightReadMode.FullCardRead,
filter = UserWalletIdPreflightReadFilter(expectedUserWalletId = userWalletId),
secureStorage = session.environment.secureStorage,
).run(session) { result ->
when (result) {
is CompletionResult.Success -> resetCard(session, callback)

View file

@ -27,6 +27,7 @@ import com.tangem.domain.models.scan.CardDTO.Companion.RING_BATCH_PREFIX
import com.tangem.domain.models.scan.ProductType
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.features.onboarding.v2.OnboardingV2FeatureToggles
import com.tangem.operations.PreflightReadMode
import com.tangem.operations.ScanTask
import com.tangem.operations.backup.PrimaryCard
import com.tangem.operations.backup.StartPrimaryCardLinkingTask
@ -49,6 +50,7 @@ internal class ScanProductTask(
private val visaCardScanHandler: VisaCardScanHandler?,
private val visaCoroutineScope: CoroutineScope?,
private val onboardingV2FeatureToggles: OnboardingV2FeatureToggles?,
private val shouldCheckIsAlreadyActivated: Boolean,
override val allowsRequestAccessCodeFromRepository: Boolean = false,
) : CardSessionRunnable<ScanResponse> {
@ -106,6 +108,14 @@ internal class ScanProductTask(
}
}
override fun preflightReadMode(): PreflightReadMode {
return if (shouldCheckIsAlreadyActivated) {
PreflightReadMode.FullCardReadWithAccessCodeCheck
} else {
return super.preflightReadMode()
}
}
private fun getErrorIfExcludedCard(cardDto: CardDTO, card: Card): TangemError? {
if (cardDto.isExcluded) return TapSdkError.CardForDifferentApp()
if (cardDto.isNotSupportedInThatRelease) return TapSdkError.CardNotSupportedByRelease()

View file

@ -20,7 +20,10 @@ class FinalizeTwinTask(
WriteProtectedIssuerDataTask(twinPublicKey, issuerKeys).run(session) { result ->
when (result) {
is CompletionResult.Success ->
PreflightReadTask(PreflightReadMode.FullCardRead).run(session) { readResult ->
PreflightReadTask(
readMode = PreflightReadMode.FullCardRead,
secureStorage = session.environment.secureStorage,
).run(session) { readResult ->
when (readResult) {
is CompletionResult.Success ->
ScanProductTask(
@ -28,6 +31,7 @@ class FinalizeTwinTask(
blockchainToDeriveFinder = null,
visaCardScanHandler = null,
visaCoroutineScope = null,
shouldCheckIsAlreadyActivated = false,
onboardingV2FeatureToggles = null,
).run(session, callback)
is CompletionResult.Failure ->

View file

@ -97,6 +97,7 @@ sealed class AnalyticsParam {
data object NewsList : ScreensSources("News List")
data object NewsLink : ScreensSources("News Link")
data object NewsPage : ScreensSources("News Page")
data object Portfolio : ScreensSources("Portfolio")
}
sealed class TxSentFrom(val value: String) {
@ -292,6 +293,7 @@ sealed class AnalyticsParam {
const val ACCOUNT_DERIVATION = "Account Derivation"
const val REFERRAL = "Referral"
const val REFERRAL_ID = "Referral_ID"
const val SEARCHED = "Searched"
}
}

View file

@ -0,0 +1,29 @@
package com.tangem.core.analytics.models.event
import com.tangem.core.analytics.models.AnalyticsEvent
import com.tangem.core.analytics.models.AnalyticsParam.Key.SEARCHED
import com.tangem.core.analytics.models.AnalyticsParam.Key.SOURCE
import com.tangem.core.analytics.models.AnalyticsParam.Key.TOKEN_PARAM
import com.tangem.core.analytics.models.AnalyticsParam.ScreensSources
/**
[REDACTED_AUTHOR]
*/
sealed class SwapAnalyticsEvent(
event: String,
params: Map<String, String> = emptyMap(),
) : AnalyticsEvent("Swap", event, params) {
data class TokenSelected(
val token: String,
val source: ScreensSources,
val isSearched: Boolean,
) : SwapAnalyticsEvent(
event = "Token Selected",
params = mapOf(
TOKEN_PARAM to token,
SOURCE to source.value,
SEARCHED to if (isSearched) "True" else "False",
),
)
}

View file

@ -24,7 +24,11 @@ data class SwapPairProvider(
@Json(name = "rateTypes")
val rateTypes: List<RateType>,
)
) {
fun hasOnlyFixedRateType(): Boolean {
return rateTypes.isNotEmpty() && rateTypes.all { it == RateType.FIXED }
}
}
@JsonClass(generateAdapter = false)
enum class RateType {

View file

@ -161,7 +161,10 @@ object PreferencesKeys {
intPreferencesKey(name = "tronNetworkFeeNotificationShowCount")
}
val TANGEM_PAY_WITHDRAW_ORDERS_KEY by lazy { stringPreferencesKey(name = "tangemPayWithdrawOrders") }
val TANGEM_PAY_WITHDRAW_ORDERS_KEY by lazy { stringPreferencesKey(name = "tangemPayWithdrawOrdersKey") }
val TANGEM_PAY_ACTIVE_WITHDRAW_ORDERS_KEY by lazy {
stringPreferencesKey(name = "tangemPayActiveWithdrawOrdersKey")
}
val TANGEM_PAY_ELIGIBILITY_KEY by lazy { booleanPreferencesKey(name = "tangemPayEligibility") }
fun getShouldShowNotificationKey(key: String) = booleanPreferencesKey("showShowNotificationUM_$key")

View file

@ -29,11 +29,25 @@ interface TangemPayStorage {
suspend fun storeCheckCustomerWalletResult(userWalletId: UserWalletId, isPaeraCustomer: Boolean)
suspend fun checkCustomerWalletResult(userWalletId: UserWalletId): Boolean?
/** Called after creating withdraw order, active order id */
suspend fun storeActiveWithdrawOrderId(userWalletId: UserWalletId, orderId: String)
/** Called after creating withdraw order, saves order data */
suspend fun storeWithdrawOrder(userWalletId: UserWalletId, data: TangemPayWithdrawState)
suspend fun getWithdrawOrder(userWalletId: UserWalletId): TangemPayWithdrawState?
/** Returns single active order id. Once the order is completed, deletes id from storage.
* Only one active order allowed for a wallet */
suspend fun getActiveWithdrawOrderId(userWalletId: UserWalletId): String?
suspend fun deleteWithdrawOrder(userWalletId: UserWalletId)
/** Returns all withdraw orders saved.
* Once we get tx hash for an order, it gets deleted from this storage */
suspend fun getWithdrawOrders(userWalletId: UserWalletId): List<TangemPayWithdrawState>?
/** Deletes active withdraw order. Called after order is completed */
suspend fun deleteActiveWithdrawOrder(userWalletId: UserWalletId)
/** Deletes withdraw order data. Called after getting its tx hash */
suspend fun deleteWithdrawOrder(userWalletId: UserWalletId, orderId: String)
suspend fun getHideMainOnboardingBanner(userWalletId: UserWalletId): Boolean

View file

@ -6,6 +6,7 @@ import com.tangem.blockchainsdk.utils.toBlockchain
import com.tangem.data.staking.store.StakeKitBalancesStore
import com.tangem.domain.card.common.TapWorkarounds.isWallet2
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.scan.ProductType
import com.tangem.domain.models.staking.StakingBalance
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId
@ -136,6 +137,10 @@ internal class DefaultStakingRepository(
return false
}
if (userWallet.scanResponse.productType == ProductType.Note) {
return true
}
val blockchainId = cryptoCurrency.network.rawId
return when {
isSolana(blockchainId) -> INVALID_BATCHES_FOR_SOLANA.contains(userWallet.scanResponse.card.batchId)

View file

@ -94,9 +94,10 @@ internal class DefaultSwapRepositoryV2 @Inject constructor(
currencyStatus.currency.network.backendId == pair.to.network
}
val mappedProviders = pair.providers.mapNotNull {
expressProviders[it.providerId]
}.filterYieldSupplyProvider(statusFrom)
val mappedProviders = pair.providers
.filterNot { it.hasOnlyFixedRateType() }
.mapNotNull { expressProviders[it.providerId] }
.filterYieldSupplyProvider(statusFrom)
if (statusFrom != null && statusTo != null && mappedProviders.isNotEmpty()) {
SwapPairModel(
@ -151,9 +152,10 @@ internal class DefaultSwapRepositoryV2 @Inject constructor(
val currencyStatusFrom = createSendWithSwapCryptoCurrencyStatus(statusFromDeferred.await())
val currencyStatusTo = createSendWithSwapCryptoCurrencyStatus(statusToDeferred.await())
val mappedProvider = pair.providers.mapNotNull {
mappedProviders[it.providerId]
}.filterYieldSupplyProvider(currencyStatusFrom)
val mappedProvider = pair.providers
.filterNot { it.hasOnlyFixedRateType() }
.mapNotNull { mappedProviders[it.providerId] }
.filterYieldSupplyProvider(currencyStatusFrom)
if (currencyStatusFrom != null && currencyStatusTo != null && mappedProvider.isNotEmpty()) {
SwapPairModel(

View file

@ -2,7 +2,6 @@ package com.tangem.data.pay.repository
import arrow.core.Either
import arrow.core.left
import arrow.core.right
import com.tangem.core.error.UniversalError
import com.tangem.data.common.quote.QuotesFetcher
import com.tangem.datasource.api.pay.TangemPayApi
@ -17,7 +16,6 @@ import com.tangem.domain.pay.TangemPayWithdrawState
import com.tangem.domain.pay.WithdrawalResult
import com.tangem.domain.pay.WithdrawalSignatureResult
import com.tangem.domain.pay.datasource.TangemPayAuthDataSource
import com.tangem.domain.pay.model.OrderData
import com.tangem.domain.pay.model.OrderStatus
import com.tangem.domain.pay.repository.CustomerOrderRepository
import com.tangem.domain.pay.repository.TangemPayWithdrawRepository
@ -25,13 +23,7 @@ import com.tangem.domain.visa.error.VisaApiError
import com.tangem.feature.swap.domain.api.SwapRepository
import com.tangem.feature.swap.domain.models.ExpressDataError
import com.tangem.utils.extensions.addHexPrefix
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import kotlinx.coroutines.*
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import timber.log.Timber
@ -40,7 +32,6 @@ import java.math.RoundingMode
import java.util.Currency
import java.util.Locale
import javax.inject.Inject
import kotlin.collections.set
import kotlin.coroutines.cancellation.CancellationException
import kotlin.time.Duration.Companion.seconds
@ -113,23 +104,18 @@ internal class DefaultTangemPayWithdrawRepository @Inject constructor(
) {
val orderId = response.result?.orderId
if (orderId != null) {
val orderData = orderRepository
.getOrderData(userWalletId = userWallet.walletId, orderId = orderId).getOrNull()
val withdrawTxHash = orderData?.withdrawTxHash
tangemPayStorage.storeActiveWithdrawOrderId(userWalletId = userWallet.walletId, orderId = orderId)
val order = orderRepository.getOrderData(userWalletId = userWallet.walletId, orderId = orderId).getOrNull()
val withdrawTxHash = order?.withdrawTxHash
val storeData = TangemPayWithdrawState(
orderId = orderId,
exchangeData = exchangeData,
)
if (orderData != null && !withdrawTxHash.isNullOrEmpty()) {
finalizeWithdraw(
userWallet = userWallet,
withdrawTxHash = withdrawTxHash,
orderId = orderId,
exchangeData = exchangeData,
order = orderData,
).onLeft {
tangemPayStorage.storeWithdrawOrder(userWalletId = userWallet.walletId, data = storeData)
}
if (order != null && !withdrawTxHash.isNullOrEmpty()) {
finalizeWithdraw(userWallet = userWallet, txHash = withdrawTxHash, exchangeData = exchangeData)
.onLeft {
tangemPayStorage.storeWithdrawOrder(userWalletId = userWallet.walletId, data = storeData)
}
} else {
tangemPayStorage.storeWithdrawOrder(userWalletId = userWallet.walletId, data = storeData)
}
@ -138,10 +124,8 @@ internal class DefaultTangemPayWithdrawRepository @Inject constructor(
private suspend fun finalizeWithdraw(
userWallet: UserWallet,
withdrawTxHash: String,
orderId: String,
txHash: String,
exchangeData: TangemPayWithdrawExchangeState,
order: OrderData,
): Either<ExpressDataError, Unit> {
return swapRepository.exchangeSent(
userWallet = userWallet,
@ -149,89 +133,63 @@ internal class DefaultTangemPayWithdrawRepository @Inject constructor(
fromNetwork = exchangeData.fromNetwork,
fromAddress = exchangeData.fromAddress,
payInAddress = exchangeData.payInAddress,
txHash = withdrawTxHash,
txHash = txHash,
payInExtraId = exchangeData.payInExtraId,
)
.onRight {
val isActive = order.status == OrderStatus.NEW || order.status == OrderStatus.PROCESSING
if (!isActive) {
tangemPayStorage.deleteWithdrawOrder(userWalletId = userWallet.walletId)
} else {
tangemPayStorage.storeWithdrawOrder(
userWalletId = userWallet.walletId,
data = TangemPayWithdrawState(orderId = orderId, exchangeData = null),
)
}
}
.onLeft { error ->
Timber.tag(TAG).e(error.toString())
}
}
override suspend fun hasWithdrawOrder(userWallet: UserWallet): Boolean {
val orderExchangeData = tangemPayStorage.getWithdrawOrder(userWallet.walletId)
if (orderExchangeData == null) return false
val exchangeData = orderExchangeData.exchangeData
val orderData = orderRepository
.getOrderData(userWallet.walletId, orderId = orderExchangeData.orderId).getOrNull()
val withdrawTxHash = orderData?.withdrawTxHash
if (exchangeData != null && orderData != null && withdrawTxHash != null) {
finalizeWithdraw(
userWallet = userWallet,
withdrawTxHash = withdrawTxHash,
orderId = orderExchangeData.orderId,
exchangeData = exchangeData,
order = orderData,
)
val orderId = tangemPayStorage.getActiveWithdrawOrderId(userWallet.walletId)
if (orderId.isNullOrEmpty()) return false
val orderData = orderRepository.getOrderData(userWalletId = userWallet.walletId, orderId = orderId).getOrNull()
val isActive = orderData?.status == OrderStatus.NEW || orderData?.status == OrderStatus.PROCESSING
if (!isActive) {
tangemPayStorage.deleteActiveWithdrawOrder(userWalletId = userWallet.walletId)
}
return orderData?.status == OrderStatus.NEW || orderData?.status == OrderStatus.PROCESSING
return isActive
}
override suspend fun pollWithdrawOrderIfNeeds(userWallet: UserWallet): Either<VisaApiError, Unit> {
val storeData = tangemPayStorage.getWithdrawOrder(userWallet.walletId) ?: return Unit.right()
val exchangeData = storeData.exchangeData ?: return Unit.right()
val orderId = storeData.orderId
val order = orderRepository
.getOrderData(userWalletId = userWallet.walletId, orderId = orderId).getOrNull()
?: return Unit.right()
override suspend fun pollWithdrawOrdersIfNeeds(userWallet: UserWallet) {
tangemPayStorage.getWithdrawOrders(userWalletId = userWallet.walletId)?.forEach { state ->
withdrawPollingScope.launch {
try {
pollWithdrawOrderIfNeeds(userWallet = userWallet, data = state)
} catch (exception: CancellationException) {
throw exception
} catch (exception: Exception) {
Timber.tag(TAG).e(exception)
}
}
}
}
private suspend fun pollWithdrawOrderIfNeeds(userWallet: UserWallet, data: TangemPayWithdrawState) {
val exchangeData = data.exchangeData ?: return
val orderId = data.orderId
val order = orderRepository.getOrderData(userWalletId = userWallet.walletId, orderId = orderId).getOrNull()
?: return
val txHash = order.withdrawTxHash
if (!txHash.isNullOrEmpty()) {
finalizeWithdraw(
userWallet = userWallet,
withdrawTxHash = txHash,
orderId = storeData.orderId,
exchangeData = exchangeData,
order = order,
).onLeft {
startWithdrawOrderPolling(
userWallet = userWallet,
orderId = orderId,
storeData = storeData,
exchangeData = exchangeData,
)
}
finalizeWithdraw(userWallet = userWallet, txHash = txHash, exchangeData = exchangeData)
.onRight {
tangemPayStorage.deleteWithdrawOrder(userWalletId = userWallet.walletId, orderId = orderId)
}
.onLeft {
startWithdrawOrderPolling(userWallet = userWallet, orderId = orderId, exchangeData = exchangeData)
}
} else {
startWithdrawOrderPolling(
userWallet = userWallet,
orderId = orderId,
storeData = storeData,
exchangeData = exchangeData,
)
startWithdrawOrderPolling(userWallet = userWallet, orderId = orderId, exchangeData = exchangeData)
}
return Unit.right()
return
}
private suspend fun startWithdrawOrderPolling(
userWallet: UserWallet,
orderId: String,
storeData: TangemPayWithdrawState,
exchangeData: TangemPayWithdrawExchangeState,
) {
withdrawPollingMutex.withLock {
@ -239,33 +197,33 @@ internal class DefaultTangemPayWithdrawRepository @Inject constructor(
val pollingJob = withdrawPollingScope.launch {
try {
while (isActive && withdrawPollingJobs.containsKey(orderId)) {
while (isActive) {
delay(duration = 5.seconds)
val orderData = orderRepository
.getOrderData(userWalletId = userWallet.walletId, orderId = orderId)
orderData.onRight { order ->
if (order.status != OrderStatus.NEW && order.status != OrderStatus.PROCESSING) {
tangemPayStorage.deleteWithdrawOrder(userWallet.walletId)
withdrawPollingJobs.remove(key = orderId)
orderRepository.getOrderData(userWalletId = userWallet.walletId, orderId = orderId)
.onRight { order ->
val txHash = order.withdrawTxHash
if (txHash.isNullOrEmpty()) return@onRight
finalizeWithdraw(userWallet = userWallet, txHash = txHash, exchangeData = exchangeData)
.onRight {
tangemPayStorage.deleteWithdrawOrder(
userWalletId = userWallet.walletId,
orderId = orderId,
)
withdrawPollingMutex.withLock { withdrawPollingJobs.remove(orderId) }
return@launch
}
.onLeft { error ->
Timber.tag(TAG).e("finalizeWithdraw error: $error")
withdrawPollingMutex.withLock { withdrawPollingJobs.remove(orderId) }
return@launch
}
}
.onLeft { error ->
Timber.tag(TAG).e("getOrderData error ${error.errorCode}")
withdrawPollingMutex.withLock { withdrawPollingJobs.remove(orderId) }
return@launch
}
val txHash = order.withdrawTxHash
if (!txHash.isNullOrEmpty()) {
finalizeWithdraw(
userWallet = userWallet,
withdrawTxHash = txHash,
orderId = storeData.orderId,
exchangeData = exchangeData,
order = order,
).onRight {
withdrawPollingJobs.remove(key = orderId)
return@launch
}
}
}.onLeft { error ->
Timber.tag(TAG).e("error ${error.errorCode}")
}
}
} catch (exception: CancellationException) {
throw exception

View file

@ -59,14 +59,21 @@ class ManageCryptoCurrenciesUseCase(
accountId: AccountId,
add: CryptoCurrency? = null,
remove: CryptoCurrency? = null,
skipDerivationErrors: Boolean = true,
): Either<Throwable, Unit> {
return invoke(accountId = accountId, add = listOfNotNull(add), remove = listOfNotNull(remove))
return invoke(
accountId = accountId,
add = listOfNotNull(add),
remove = listOfNotNull(remove),
skipDerivationErrors = skipDerivationErrors,
)
}
suspend operator fun invoke(
accountId: AccountId,
add: List<CryptoCurrency> = emptyList(),
remove: List<CryptoCurrency> = emptyList(),
skipDerivationErrors: Boolean = true,
): Either<Throwable, Unit> = eitherOn(dispatchers.default) {
if (add.isEmpty() && remove.isEmpty()) {
Timber.d("No currencies to add or remove, skipping")
@ -89,7 +96,7 @@ class ManageCryptoCurrenciesUseCase(
account = accountStatus.account.copy(cryptoCurrencies = modifiedCurrencyList.total),
)
derivePublicKeys(userWalletId = userWalletId, currencies = modifiedCurrencyList.added)
val result = derivePublicKeys(userWalletId = userWalletId, currencies = modifiedCurrencyList.added)
parallelUpdatingScope.launch {
syncTokens(userWalletId, modifiedCurrencyList)
@ -98,6 +105,10 @@ class ManageCryptoCurrenciesUseCase(
refreshExpress(userWalletId = userWalletId, currencies = modifiedCurrencyList.total)
clearMetadata(userWalletId = userWalletId, currencies = modifiedCurrencyList.removed)
}
if (!skipDerivationErrors) {
result.bind()
}
}
}

View file

@ -6,7 +6,6 @@ import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.pay.TangemPayWithdrawExchangeState
import com.tangem.domain.pay.WithdrawalResult
import com.tangem.domain.visa.error.VisaApiError
import java.math.BigDecimal
interface TangemPayWithdrawRepository {
@ -21,5 +20,5 @@ interface TangemPayWithdrawRepository {
suspend fun hasWithdrawOrder(userWallet: UserWallet): Boolean
suspend fun pollWithdrawOrderIfNeeds(userWallet: UserWallet): Either<VisaApiError, Unit>
suspend fun pollWithdrawOrdersIfNeeds(userWallet: UserWallet)
}

View file

@ -25,6 +25,7 @@ dependencies {
implementation(projects.domain.walletManager)
implementation(projects.libs.blockchainSdk)
implementation(projects.libs.tangemSdkApi)
implementation(projects.domain.account)
implementation(projects.domain.models)
implementation(projects.domain.tokens)
implementation(projects.domain.card)

View file

@ -1,13 +1,13 @@
package com.tangem.domain.wallets.usecase
import arrow.core.Either
import com.tangem.domain.tokens.repository.CurrenciesRepository
import com.tangem.domain.account.repository.AccountsCRUDRepository
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.wallets.repository.WalletsRepository
class SetNotificationsEnabledUseCase(
private val walletsRepository: WalletsRepository,
private val currenciesRepository: CurrenciesRepository,
private val accountsCRUDRepository: AccountsCRUDRepository,
) {
suspend operator fun invoke(userWalletId: UserWalletId, isEnabled: Boolean): Either<Throwable, Unit> =
@ -16,7 +16,7 @@ class SetNotificationsEnabledUseCase(
userWalletId = userWalletId,
isEnabled = isEnabled,
)
currenciesRepository.syncTokens(userWalletId)
accountsCRUDRepository.syncTokens(userWalletId)
}.onLeft {
walletsRepository.setNotificationsEnabled(
userWalletId = userWalletId,

View file

@ -0,0 +1,131 @@
package com.tangem.domain.wallets.usecase
import com.google.common.truth.Truth.assertThat
import com.tangem.domain.account.repository.AccountsCRUDRepository
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.wallets.repository.WalletsRepository
import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.coVerifyOrder
import io.mockk.just
import io.mockk.mockk
import io.mockk.runs
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Test
class SetNotificationsEnabledUseCaseTest {
private lateinit var useCase: SetNotificationsEnabledUseCase
private lateinit var walletsRepository: WalletsRepository
private lateinit var accountsCRUDRepository: AccountsCRUDRepository
@Before
fun setup() {
walletsRepository = mockk()
accountsCRUDRepository = mockk()
useCase = SetNotificationsEnabledUseCase(
walletsRepository = walletsRepository,
accountsCRUDRepository = accountsCRUDRepository,
)
}
@Test
fun `GIVEN notifications enabled successfully WHEN invoke THEN return Right with Unit`() = runTest {
// GIVEN
val userWalletId = UserWalletId("0A0B0C0D")
val isEnabled = true
coEvery { walletsRepository.setNotificationsEnabled(userWalletId, isEnabled) } just runs
coEvery { accountsCRUDRepository.syncTokens(userWalletId) } just runs
// WHEN
val result = useCase(userWalletId, isEnabled)
// THEN
assertThat(result.isRight()).isTrue()
coVerifyOrder {
walletsRepository.setNotificationsEnabled(userWalletId, isEnabled)
accountsCRUDRepository.syncTokens(userWalletId)
}
}
@Test
fun `GIVEN notifications disabled successfully WHEN invoke THEN return Right with Unit`() = runTest {
// GIVEN
val userWalletId = UserWalletId("0A0B0C0D")
val isEnabled = false
coEvery { walletsRepository.setNotificationsEnabled(userWalletId, isEnabled) } just runs
coEvery { accountsCRUDRepository.syncTokens(userWalletId) } just runs
// WHEN
val result = useCase(userWalletId, isEnabled)
// THEN
assertThat(result.isRight()).isTrue()
coVerifyOrder {
walletsRepository.setNotificationsEnabled(userWalletId, isEnabled)
accountsCRUDRepository.syncTokens(userWalletId)
}
}
@Test
fun `GIVEN setNotificationsEnabled throws exception WHEN invoke THEN return Left and revert notifications`() = runTest {
// GIVEN
val userWalletId = UserWalletId("0A0B0C0D")
val isEnabled = true
val exception = RuntimeException("Network error")
coEvery { walletsRepository.setNotificationsEnabled(userWalletId, isEnabled) } throws exception
coEvery { walletsRepository.setNotificationsEnabled(userWalletId, !isEnabled) } just runs
// WHEN
val result = useCase(userWalletId, isEnabled)
// THEN
assertThat(result.isLeft()).isTrue()
result.onLeft { throwable ->
assertThat(throwable).isEqualTo(exception)
}
coVerify { walletsRepository.setNotificationsEnabled(userWalletId, !isEnabled) }
}
@Test
fun `GIVEN syncTokens throws exception WHEN invoke THEN return Left and revert notifications`() = runTest {
// GIVEN
val userWalletId = UserWalletId("0A0B0C0D")
val isEnabled = true
val exception = RuntimeException("Sync error")
coEvery { walletsRepository.setNotificationsEnabled(userWalletId, isEnabled) } just runs
coEvery { accountsCRUDRepository.syncTokens(userWalletId) } throws exception
coEvery { walletsRepository.setNotificationsEnabled(userWalletId, !isEnabled) } just runs
// WHEN
val result = useCase(userWalletId, isEnabled)
// THEN
assertThat(result.isLeft()).isTrue()
result.onLeft { throwable ->
assertThat(throwable).isEqualTo(exception)
}
coVerify { walletsRepository.setNotificationsEnabled(userWalletId, !isEnabled) }
}
@Test
fun `GIVEN disabling notifications fails WHEN invoke THEN return Left and revert to enabled`() = runTest {
// GIVEN
val userWalletId = UserWalletId("0A0B0C0D")
val isEnabled = false
val exception = RuntimeException("Network error")
coEvery { walletsRepository.setNotificationsEnabled(userWalletId, isEnabled) } throws exception
coEvery { walletsRepository.setNotificationsEnabled(userWalletId, !isEnabled) } just runs
// WHEN
val result = useCase(userWalletId, isEnabled)
// THEN
assertThat(result.isLeft()).isTrue()
result.onLeft { throwable ->
assertThat(throwable).isEqualTo(exception)
}
coVerify { walletsRepository.setNotificationsEnabled(userWalletId, true) }
}
}

View file

@ -157,6 +157,8 @@ internal class AddToPortfolioModel @Inject constructor(
allRequireForAdd.first()
// line of navigation to AddToken screen is finished; cancel the job, select a new root screen
firstPartOfNavigation.cancel()
analyticsEventHandler.send(event = eventBuilder.popupToConfirm())
navigation.replaceAll(AddToPortfolioRoutes.AddToken)
var middleNavigationJob: Job? = null

View file

@ -88,6 +88,7 @@ internal class AddTokenModel @Inject constructor(
val blockchainNames = listOf(selectedNetwork.selectedNetwork)
.mapNotNull { BlockchainUtils.getNetworkInfo(it.networkId)?.name }
analyticsEventHandler.send(analyticsEventBuilder.addToPortfolioContinue(blockchainNames))
analyticsEventHandler.send(analyticsEventBuilder.addButtonClick())
manageCryptoCurrenciesUseCase(accountId = accountId, add = cryptoCurrency)
.onLeft { throwable ->
@ -110,6 +111,11 @@ internal class AddTokenModel @Inject constructor(
}
is Account.Payment -> TODO("[REDACTED_JIRA]")
}
analyticsEventHandler.send(
event = analyticsEventBuilder.tokenAdded(status.status.currency.network.name),
)
params.callbacks.onTokenAdded(status.status)
}
uiState.value = um.toggleProgress(false)

View file

@ -15,28 +15,64 @@ internal class PortfolioAnalyticsEvent(
fun addToPortfolioClicked() = PortfolioAnalyticsEvent(
event = "Button - Add To Portfolio",
params = mapOf(
"Token" to tokenSymbol,
),
params = buildMap {
put("Token", tokenSymbol)
if (source != null) put("Source", source)
},
)
fun popupToChooseAccount() = PortfolioAnalyticsEvent(
event = "Choose Account Opened",
params = buildMap {
if (source != null) put("Source", source)
},
)
fun popupToConfirm() = PortfolioAnalyticsEvent(
event = "Add Token Screen Opened",
params = buildMap {
if (source != null) put("Source", source)
},
)
fun addToNotMainAccount() = PortfolioAnalyticsEvent(
event = "Button - Add To Account",
params = buildMap {
if (source != null) put("Source", source)
},
)
fun addToPortfolioWalletChanged() = PortfolioAnalyticsEvent(event = "Wallet Selected")
fun addButtonClick() = PortfolioAnalyticsEvent(
event = "Button - Add Token",
params = buildMap {
if (source != null) put("Source", source)
},
)
fun addToPortfolioWalletChanged() = PortfolioAnalyticsEvent(
event = "Wallet Selected",
params = buildMap {
if (source != null) put("Source", source)
},
)
fun addToPortfolioContinue(blockchainNames: List<String>) = PortfolioAnalyticsEvent(
event = "Token Network Selected",
params = mapOf(
"Count" to blockchainNames.size.toString(),
"Token" to tokenSymbol,
"blockchain" to blockchainNames.joinToString(separator = ", "),
),
params = buildMap {
put("Count", blockchainNames.size.toString())
put("Token", tokenSymbol)
put("blockchain", blockchainNames.joinToString(separator = ", "))
if (source != null) put("Source", source)
},
)
fun tokenAdded(blockchainName: String) = PortfolioAnalyticsEvent(
event = "Token Added",
params = buildMap {
put("Token", tokenSymbol)
put("Blockchain", blockchainName)
if (source != null) put("Source", source)
},
)
fun quickActionClick(actionUM: TokenActionsBSContentUM.Action, blockchainName: String) =
@ -51,7 +87,7 @@ internal class PortfolioAnalyticsEvent(
},
params = buildMap {
put("Token", tokenSymbol)
source?.let { put("Source", it) }
if (source != null) put("Source", source)
put("blockchain", blockchainName)
},
)
@ -64,10 +100,16 @@ internal class PortfolioAnalyticsEvent(
TokenActionsBSContentUM.Action.Stake -> "Popup Get token - Button Stake"
else -> "error"
},
params = buildMap {
if (source != null) put("Source", source)
},
)
fun getTokenLater() = PortfolioAnalyticsEvent(
event = "Popup Get token - Button Later",
params = buildMap {
if (source != null) put("Source", source)
},
)
}
}

View file

@ -43,8 +43,8 @@ internal class DefaultOnboardingStepperComponent @AssistedInject constructor(
componentScope.launch {
val cardInfo = getWalletMetaInfoUseCase(params.scanResponse).getOrNull() ?: return@launch
val userWalletId = cardInfo.userWalletId ?: return@launch
val visaCustomerId = getTangemPayCustomerIdUseCase(userWalletId).getOrNull()
val userWalletId = cardInfo.userWalletId
val visaCustomerId = userWalletId?.let { id -> getTangemPayCustomerIdUseCase(id).getOrNull() }
sendFeedbackEmailUseCase(
if (params.scanResponse.card.isVisa && !visaCustomerId.isNullOrEmpty()) {
FeedbackEmailType.Visa.Activation(walletMetaInfo = cardInfo, customerId = visaCustomerId)

View file

@ -1,44 +0,0 @@
package com.tangem.features.onramp.swap.availablepairs.model
import com.tangem.core.analytics.models.AnalyticsEvent
import com.tangem.core.analytics.models.AnalyticsParam.Key.BLOCKCHAIN
import com.tangem.core.analytics.models.AnalyticsParam.Key.TOKEN_PARAM
private const val SWAP_CATEGORY = "Swap"
internal sealed class AvailableSwapPairsAnalyticsEvent(
event: String,
params: Map<String, String> = emptyMap(),
) : AnalyticsEvent(SWAP_CATEGORY, event, params) {
class TokenSelected(
val token: String,
val source: String,
val isSearched: Boolean,
) : AvailableSwapPairsAnalyticsEvent(
event = "Token Selected",
params = mapOf(
TOKEN_PARAM to token,
SOURCE to source,
SEARCHED to if (isSearched) "True" else "False",
),
) {
companion object {
const val SOURCE = "Source"
const val SEARCHED = "Searched"
const val SOURCE_PORTFOLIO = "Portfolio"
const val SOURCE_MARKETS = "Markets"
}
}
class TokenAdded(
val token: String,
val blockchain: String,
) : AvailableSwapPairsAnalyticsEvent(
event = "Token Added",
params = mapOf(
TOKEN_PARAM to token,
BLOCKCHAIN to blockchain,
),
)
}

View file

@ -1,11 +1,17 @@
package com.tangem.features.onramp.swap.availablepairs.model
import com.arkivanov.decompose.router.slot.SlotNavigation
import com.arkivanov.decompose.router.slot.activate
import com.arkivanov.decompose.router.slot.dismiss
import com.tangem.blockchainsdk.utils.ExcludedBlockchains
import com.tangem.common.ui.markets.models.MarketsListItemUM
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.analytics.models.AnalyticsParam.ScreensSources
import com.tangem.core.analytics.models.event.SwapAnalyticsEvent
import com.tangem.core.decompose.model.Model
import com.tangem.core.ui.components.token.state.TokenItemState
import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.core.ui.components.fields.InputManager
import com.tangem.core.ui.R as CoreUiR
import com.tangem.core.ui.components.token.state.TokenItemState
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.capitalize
import com.tangem.core.ui.extensions.resourceReference
@ -43,9 +49,7 @@ import com.tangem.feature.swap.domain.models.domain.SwapPairLeast
import com.tangem.features.feed.components.market.details.portfolio.add.AddToPortfolioComponent
import com.tangem.features.feed.components.market.details.portfolio.add.AddToPortfolioManager
import com.tangem.features.onramp.impl.R
import com.tangem.features.onramp.tokenlist.entity.utils.OnrampTokenItemStateConverterFactory
import com.tangem.features.onramp.swap.availablepairs.AvailableSwapPairsComponent
import com.tangem.features.swap.SwapFeatureToggles
import com.tangem.features.onramp.swap.availablepairs.entity.transformers.SetErrorWarningTransformer
import com.tangem.features.onramp.swap.availablepairs.entity.transformers.SetLoadingTokenItemsTransformer
import com.tangem.features.onramp.swap.availablepairs.entity.transformers.SetNoAvailablePairsTransformer
@ -58,24 +62,22 @@ import com.tangem.features.onramp.tokenlist.entity.TokenListUM
import com.tangem.features.onramp.tokenlist.entity.TokenListUMController
import com.tangem.features.onramp.tokenlist.entity.TokenListUMTransformer
import com.tangem.features.onramp.tokenlist.entity.transformer.*
import com.tangem.features.onramp.tokenlist.entity.utils.OnrampTokenItemStateConverterFactory
import com.tangem.features.onramp.utils.UpdateSearchBarActiveStateTransformer
import com.tangem.features.onramp.utils.UpdateSearchBarCallbacksTransformer
import com.tangem.features.onramp.utils.UpdateSearchQueryTransformer
import com.tangem.blockchainsdk.utils.ExcludedBlockchains
import com.tangem.common.ui.markets.models.MarketsListItemUM
import com.tangem.features.swap.SwapFeatureToggles
import com.tangem.lib.crypto.BlockchainUtils
import com.arkivanov.decompose.router.slot.SlotNavigation
import com.arkivanov.decompose.router.slot.activate
import com.arkivanov.decompose.router.slot.dismiss
import com.tangem.utils.Provider
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.coroutines.JobHolder
import com.tangem.utils.coroutines.runSuspendCatching
import kotlinx.coroutines.ExperimentalCoroutinesApi
import com.tangem.utils.coroutines.saveIn
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import javax.inject.Inject
import com.tangem.core.ui.R as CoreUiR
private typealias AvailablePairsState = Lce<Throwable, List<SwapPairLeast>>
@ -567,9 +569,9 @@ internal class AvailableSwapPairsModel @Inject constructor(
private fun onPortfolioTokenClick(tokenItem: TokenItemState, status: CryptoCurrencyStatus) {
analyticsEventHandler.send(
AvailableSwapPairsAnalyticsEvent.TokenSelected(
SwapAnalyticsEvent.TokenSelected(
token = status.currency.symbol,
source = AvailableSwapPairsAnalyticsEvent.TokenSelected.SOURCE_PORTFOLIO,
source = ScreensSources.Portfolio,
isSearched = state.value.searchBarUM.query.isNotEmpty(),
),
)
@ -686,15 +688,9 @@ internal class AvailableSwapPairsModel @Inject constructor(
modelScope.launch {
bottomSheetNavigation.dismiss()
analyticsEventHandler.send(
AvailableSwapPairsAnalyticsEvent.TokenAdded(
SwapAnalyticsEvent.TokenSelected(
token = addedToken.symbol,
blockchain = addedToken.network.name,
),
)
analyticsEventHandler.send(
AvailableSwapPairsAnalyticsEvent.TokenSelected(
token = addedToken.symbol,
source = AvailableSwapPairsAnalyticsEvent.TokenSelected.SOURCE_MARKETS,
source = ScreensSources.Markets,
isSearched = state.value.searchBarUM.query.isNotEmpty(),
),
)
@ -746,7 +742,7 @@ internal class AvailableSwapPairsModel @Inject constructor(
.create(
scope = modelScope,
token = param,
analyticsParams = null,
analyticsParams = AddToPortfolioManager.AnalyticsParams(source = ScreensSources.Swap.value),
).apply {
setTokenNetworks(networks)
}

View file

@ -73,7 +73,17 @@ internal class ReferralInteractorImpl(
when (portfolioId) {
is PortfolioId.Account -> {
manageCryptoCurrenciesUseCase(accountId = portfolioId.accountId, add = cryptoCurrency)
manageCryptoCurrenciesUseCase(
accountId = portfolioId.accountId,
add = cryptoCurrency,
skipDerivationErrors = false,
).mapLeft {
it.mapToDomainError()
}.onLeft { error ->
if (error is ReferralError.UserCancelledException) {
throw error
}
}
}
is PortfolioId.Wallet -> {
derivePublicKeysUseCase(userWallet.walletId, listOf(cryptoCurrency)).getOrElse { throwable ->

View file

@ -5,6 +5,9 @@ import com.tangem.common.ui.notifications.NotificationUM
import com.tangem.core.ui.extensions.TextReference
import com.tangem.domain.express.models.ExpressProvider
import com.tangem.domain.swap.models.SwapDataModel
import com.tangem.features.send.v2.api.entity.FeeSelectorUM
import com.tangem.features.send.v2.api.subcomponents.destination.entity.DestinationUM
import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountUM
import kotlinx.collections.immutable.ImmutableList
@Immutable
@ -37,6 +40,9 @@ internal sealed class ConfirmUM {
val txUrl: String,
val swapDataModel: SwapDataModel,
val provider: ExpressProvider,
val amountUM: SwapAmountUM,
val destinationUM: DestinationUM,
val feeSelectorUM: FeeSelectorUM,
) : ConfirmUM()
data object Empty : ConfirmUM() {

View file

@ -20,6 +20,9 @@ internal class SendWithSwapConfirmSentStateTransformer(
txUrl = txUrl,
provider = provider,
swapDataModel = swapDataModel,
amountUM = prevState.amountUM,
destinationUM = prevState.destinationUM,
feeSelectorUM = prevState.feeSelectorUM,
),
)
}

View file

@ -93,10 +93,10 @@ internal fun SendWithSwapSuccessContent(sendWithSwapUM: SendWithSwapUM) {
@Composable
private fun SuccessContent(sendWithSwapUM: SendWithSwapUM, modifier: Modifier = Modifier) {
val confirmUM = sendWithSwapUM.confirmUM as? ConfirmUM.Success ?: return
val amountUM = sendWithSwapUM.amountUM as? SwapAmountUM.Content ?: return
val amountUM = confirmUM.amountUM as? SwapAmountUM.Content ?: return
val quoteUM = amountUM.selectedQuote as? SwapQuoteUM.Content ?: return
val destinationUM = sendWithSwapUM.destinationUM as? DestinationUM.Content ?: return
val feeSelectorUM = sendWithSwapUM.feeSelectorUM as? FeeSelectorUM.Content ?: return
val destinationUM = confirmUM.destinationUM as? DestinationUM.Content ?: return
val feeSelectorUM = confirmUM.feeSelectorUM as? FeeSelectorUM.Content ?: return
Column(
modifier = modifier
@ -393,6 +393,64 @@ private fun SendWithSwapSuccessContent_Preview() {
txExtraIdName = "Jeffry Blackwell",
),
),
amountUM = SwapAmountContentPreview.defaultState,
destinationUM = DestinationUM.Content(
isPrimaryButtonEnabled = false,
addressTextField = DestinationTextFieldUM.RecipientAddress(
value = "0x391316d97a07027a0702c8A002c8A0C25d8470",
keyboardOptions = KeyboardOptions(),
placeholder = TextReference.EMPTY,
label = resourceReference(R.string.send_recipient),
isError = false,
error = null,
isValuePasted = false,
blockchainAddress = "0x391316d97a07027a0702c8A002c8A0C25d8470",
),
memoTextField = DestinationTextFieldUM.RecipientMemo(
value = "123123123",
keyboardOptions = KeyboardOptions(),
placeholder = TextReference.EMPTY,
label = resourceReference(R.string.send_recipient),
isError = false,
error = null,
isValuePasted = false,
isEnabled = true,
disabledText = TextReference.EMPTY,
),
recent = persistentListOf(),
wallets = persistentListOf(),
networkName = "Polygon",
isValidating = false,
isInitialized = false,
isRecentHidden = false,
isAccountsMode = false,
),
feeSelectorUM = FeeSelectorUM.Content(
fees = TransactionFee.Single(
normal = Fee.Common(
BigDecimal.ONE.convertToSdkAmount(
SwapAmountContentPreview.cryptoCurrencyStatus,
),
),
),
feeItems = persistentListOf(),
selectedFeeItem = FeeItem.Market(
Fee.Common(
BigDecimal.ONE.convertToSdkAmount(
SwapAmountContentPreview.cryptoCurrencyStatus,
),
),
),
feeExtraInfo = FeeExtraInfo(
isFeeApproximate = false,
isFeeConvertibleToFiat = false,
isTronToken = false,
feeCryptoCurrencyStatus = SwapAmountContentPreview.cryptoCurrencyStatus,
),
feeFiatRateUM = null,
feeNonce = FeeNonce.None,
isPrimaryButtonEnabled = false,
),
),
navigationUM = NavigationUM.Content(
source = SendWithSwapRoute.Success.javaClass.simpleName,

View file

@ -25,9 +25,9 @@ class SwapPairInfoConverter : Converter<SwapPairsWithProviders, PairsWithProvide
contractAddress = pair.to.contractAddress,
network = pair.to.network,
),
providers = pair.providers.mapNotNull {
convertProvider(it, providersAdditionalMap)
},
providers = pair.providers
.filterNot { it.hasOnlyFixedRateType() }
.mapNotNull { convertProvider(it, providersAdditionalMap) },
)
}
return PairsWithProviders(

View file

@ -46,31 +46,12 @@ sealed class SwapEvents(
class ChooseTokenScreenResult(
val isTokenChosen: Boolean,
val token: String? = null,
val source: String? = null,
val isSearched: Boolean? = null,
) : SwapEvents(
event = "Choose Token Screen Result",
params = buildMap {
put("Token Chosen", if (isTokenChosen) "Yes" else "No")
token?.let { put("Token", it) }
source?.let { put(TOKEN_SELECTED_SOURCE, it) }
isSearched?.let { put(SEARCHED, if (it) "True" else "False") }
},
) {
companion object {
const val TOKEN_SELECTED_SOURCE = "Token Selected Source"
const val SEARCHED = "Searched"
const val SOURCE_PORTFOLIO = "Portfolio"
const val SOURCE_MARKETS = "Markets"
}
}
class TokenAdded(val token: String, val blockchain: String) : SwapEvents(
event = "Token Added",
params = mapOf(
TOKEN_PARAM to token,
BLOCKCHAIN to blockchain,
),
)
class ButtonSwapClicked(val sendToken: String, val receiveToken: String) : SwapEvents(

View file

@ -20,6 +20,7 @@ import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
@ -55,7 +56,11 @@ class SwapFeeSelectorBlockComponent @AssistedInject constructor(
init {
params.repository.state
.onEach(feeSelectorBlockComponent::updateState)
.onEach { feeSelectorBlockComponent.updateState(it) }
.launchIn(componentScope)
params.repository.forceUpdateState
.onEach { feeSelectorBlockComponent.updateState(it) }
.launchIn(componentScope)
}
@ -68,6 +73,9 @@ class SwapFeeSelectorBlockComponent @AssistedInject constructor(
val state: StateFlow<FeeSelectorUM>
get() = MutableStateFlow<FeeSelectorUM>(FeeSelectorUM.Loading)
val forceUpdateState: SharedFlow<FeeSelectorUM>
get() = MutableStateFlow<FeeSelectorUM>(FeeSelectorUM.Loading)
fun onResult(newState: FeeSelectorUM)
suspend fun loadFee(): Either<GetFeeError, TransactionFee>

View file

@ -4,7 +4,6 @@ import androidx.compose.runtime.Stable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import arrow.core.Either
import arrow.core.getOrElse
import com.arkivanov.decompose.router.slot.SlotNavigation
@ -19,13 +18,16 @@ import com.tangem.common.ui.markets.models.MarketsListItemUM
import com.tangem.core.analytics.api.AnalyticsErrorHandler
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.analytics.models.AnalyticsParam
import com.tangem.core.analytics.models.AnalyticsParam.ScreensSources
import com.tangem.core.analytics.models.Basic
import com.tangem.core.analytics.models.event.SwapAnalyticsEvent
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.core.decompose.ui.UiMessageSender
import com.tangem.core.navigation.url.UrlOpener
import com.tangem.core.ui.HoldToConfirmButtonFeatureToggles
import com.tangem.core.ui.R
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.combinedReference
import com.tangem.core.ui.extensions.resourceReference
@ -100,7 +102,6 @@ import com.tangem.feature.swap.models.UiActions
import com.tangem.feature.swap.models.market.SwapMarketsListBatchFlowManager
import com.tangem.feature.swap.models.market.state.SwapMarketState
import com.tangem.feature.swap.models.states.SwapNotificationUM
import com.tangem.core.ui.R
import com.tangem.feature.swap.router.SwapNavScreen
import com.tangem.feature.swap.router.SwapRouter
import com.tangem.feature.swap.ui.StateBuilder
@ -117,12 +118,8 @@ import com.tangem.utils.Provider
import com.tangem.utils.TangemBlogUrlBuilder.RESOURCE_TO_LEARN_ABOUT_APPROVING_IN_SWAP
import com.tangem.utils.coroutines.*
import com.tangem.utils.isNullOrZero
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.NonCancellable
import kotlinx.coroutines.delay
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import timber.log.Timber
import java.math.BigDecimal
import java.math.RoundingMode
@ -311,17 +308,13 @@ internal class SwapModel @Inject constructor(
modelScope.launch {
bottomSheetNavigation.dismiss()
analyticsEventHandler.send(
SwapEvents.ChooseTokenScreenResult(
isTokenChosen = true,
token = addedToken.symbol,
source = SwapEvents.ChooseTokenScreenResult.SOURCE_MARKETS,
isSearched = searchQueryState.value.isNotEmpty(),
),
SwapEvents.ChooseTokenScreenResult(isTokenChosen = true, token = addedToken.symbol),
)
analyticsEventHandler.send(
SwapEvents.TokenAdded(
SwapAnalyticsEvent.TokenSelected(
token = addedToken.symbol,
blockchain = addedToken.network.name,
source = ScreensSources.Markets,
isSearched = searchQueryState.value.isNotEmpty(),
),
)
searchQueryState.value = ""
@ -530,14 +523,7 @@ internal class SwapModel @Inject constructor(
dataState.fromCryptoCurrency
}
fromCryptoCurrency?.let { cryptoCurrency ->
dataState = dataState.copy(
feePaidCryptoCurrency = getFeePaidCryptoCurrencyStatusSyncUseCase(
userWalletId = userWalletId,
cryptoCurrencyStatus = cryptoCurrency,
).getOrNull(),
)
}
if (fromCryptoCurrency != null) updateFeePaidCryptoCurrencyFor(fromCryptoCurrency)
subscribeToCoinBalanceUpdatesIfNeeded()
}.onFailure { error ->
@ -774,6 +760,15 @@ internal class SwapModel @Inject constructor(
}
}
private suspend fun updateFeePaidCryptoCurrencyFor(fromToken: CryptoCurrencyStatus) {
dataState = dataState.copy(
feePaidCryptoCurrency = getFeePaidCryptoCurrencyStatusSyncUseCase(
userWalletId = userWalletId,
cryptoCurrencyStatus = fromToken,
).getOrNull(),
)
}
private fun loadQuotesTask(
fromToken: CryptoCurrencyStatus,
fromAccount: Account.CryptoPortfolio?,
@ -1332,10 +1327,13 @@ internal class SwapModel @Inject constructor(
foundToken?.currency?.symbol?.let { symbol ->
analyticsEventHandler.send(
SwapEvents.ChooseTokenScreenResult(
isTokenChosen = true,
SwapEvents.ChooseTokenScreenResult(isTokenChosen = true, token = symbol),
)
analyticsEventHandler.send(
SwapAnalyticsEvent.TokenSelected(
token = symbol,
source = SwapEvents.ChooseTokenScreenResult.SOURCE_PORTFOLIO,
source = ScreensSources.Portfolio,
isSearched = searchQueryState.value.isNotEmpty(),
),
)
@ -1399,6 +1397,9 @@ internal class SwapModel @Inject constructor(
toAccount = toAccount,
selectedProvider = null,
)
modelScope.launch {
updateFeePaidCryptoCurrencyFor(fromToken)
}
startLoadingQuotes(
fromToken = fromToken,
fromAccount = fromAccount,
@ -1541,6 +1542,7 @@ internal class SwapModel @Inject constructor(
toAccount = newToAccount,
)
isOrderReversed = !isOrderReversed
updateFeePaidCryptoCurrencyFor(newFromToken)
dataState.tokensDataState?.let { tokensDataState ->
updateTokensState(tokensDataState)
}
@ -2290,7 +2292,7 @@ internal class SwapModel @Inject constructor(
.create(
scope = modelScope,
token = param,
analyticsParams = null,
analyticsParams = AddToPortfolioManager.AnalyticsParams(source = ScreensSources.Swap.value),
).apply {
setTokenNetworks(networks)
}
@ -2372,6 +2374,8 @@ internal class SwapModel @Inject constructor(
FeeSelectorUM.Error(GetFeeError.UnknownError, isHidden = true),
)
override val forceUpdateState = MutableSharedFlow<FeeSelectorUM>()
override suspend fun loadFeeExtended(
selectedToken: CryptoCurrencyStatus?,
): Either<GetFeeError, TransactionFeeExtended> {
@ -2404,18 +2408,13 @@ internal class SwapModel @Inject constructor(
}
override fun onResult(newState: FeeSelectorUM) {
if (isPermissionNotificationShown()) {
state.value = FeeSelectorUM.Error(GetFeeError.UnknownError, isHidden = true)
return
}
if (newState is FeeSelectorUM.Error) {
state.value = newState.copy(isHidden = true)
modelScope.launch {
forceUpdateState.emit(newState.copy(isHidden = true))
}
return
}
state.value = newState
// If fee currency is same as from currency, we need to reload quotes to update fee info
val isFeeCurrencySameAsFromCurrency = newState is FeeSelectorUM.Content &&
dataState.fromCryptoCurrency?.currency?.id == newState.feeExtraInfo.feeCryptoCurrencyStatus.currency.id

View file

@ -163,10 +163,10 @@ internal class StateBuilder(
amountTextFieldValue = null,
amountEquivalent = "0 ${appCurrencyProvider.invoke().symbol}",
token = fromToken,
tokenIconUrl = uiStateHolder.sendCardData.tokenIconUrl,
coinId = uiStateHolder.sendCardData.coinId,
isNotNativeToken = uiStateHolder.sendCardData.isNotNativeToken,
tokenCurrency = uiStateHolder.sendCardData.tokenCurrency,
tokenIconUrl = fromToken.currency.iconUrl,
coinId = fromToken.currency.network.backendId,
isNotNativeToken = fromToken.currency is CryptoCurrency.Token,
tokenCurrency = fromToken.currency.symbol,
canSelectAnotherToken = uiStateHolder.sendCardData.canSelectAnotherToken,
balance = fromToken.getFormattedAmount(isNeedSymbol = false),
networkIconRes = getActiveIconRes(fromToken.currency.network.rawId),
@ -206,10 +206,10 @@ internal class StateBuilder(
amountTextFieldValue = null,
amountEquivalent = "0 ${appCurrencyProvider.invoke().symbol}",
token = fromToken,
tokenIconUrl = uiStateHolder.sendCardData.tokenIconUrl,
coinId = uiStateHolder.sendCardData.coinId,
isNotNativeToken = uiStateHolder.sendCardData.isNotNativeToken,
tokenCurrency = uiStateHolder.sendCardData.tokenCurrency,
tokenIconUrl = fromToken.currency.iconUrl,
coinId = fromToken.currency.network.backendId,
isNotNativeToken = fromToken.currency is CryptoCurrency.Token,
tokenCurrency = fromToken.currency.symbol,
canSelectAnotherToken = uiStateHolder.sendCardData.canSelectAnotherToken,
balance = fromToken.getFormattedAmount(isNeedSymbol = false),
networkIconRes = getActiveIconRes(fromToken.currency.network.rawId),

View file

@ -153,7 +153,7 @@ internal class TangemPayDetailsModel @Inject constructor(
modelScope.launch {
val userWallet = userWallet ?: getUserWalletUseCase(params.userWalletId).getOrNull()
?: return@launch
tangemPayWithdrawRepository.pollWithdrawOrderIfNeeds(userWallet)
tangemPayWithdrawRepository.pollWithdrawOrdersIfNeeds(userWallet)
}
}

View file

@ -25,14 +25,12 @@ internal class ExpressStateFactory(
fun getStateWithClosedDialog(): ExpressTransactionsBlockState {
val state = currentStateProvider()
val slot = state.dialogSlot ?: return state
return state.copy(dialogSlot = slot.copy(config = slot.config.copy(isShow = false)))
return state.copy(dialogSlot = null)
}
fun getStateWithClosedBottomSheet(): ExpressTransactionsBlockState {
val state = currentStateProvider()
val slot = state.bottomSheetSlot ?: return state
return state.copy(bottomSheetSlot = slot.copy(config = slot.config.copy(isShown = false)))
return state.copy(bottomSheetSlot = null)
}
fun getStateWithConfirmHideExpressStatus(): ExpressTransactionsBlockState {

View file

@ -33,7 +33,7 @@ sealed class WalletScreenAnalyticsEvent {
put(AnalyticsParam.BALANCE, balance.value)
tokensCount?.let { put(AnalyticsParam.TOKENS_COUNT, it.toString()) }
},
)
), AppsFlyerIncludedEvent
class TokenBalance(balance: AnalyticsParam.EmptyFull, token: String) : Basic(
event = "Token Balance",

View file

@ -48,7 +48,8 @@ internal class TokenListAnalyticsSender @Inject constructor(
if (screenLifecycleProvider.isBackgroundState.value) return
if (displayedUiState == null || displayedUiState.pullToRefreshConfig.isRefreshing) return
if (totalFiatBalance is TotalFiatBalance.Loading) {
val isFlickering = totalFiatBalance is TotalFiatBalance.Loaded && totalFiatBalance.source == StatusSource.CACHE
if (totalFiatBalance is TotalFiatBalance.Loading || isFlickering) {
startLoadingTraceIfNeeded(userWallet.walletId, flattenCurrencies)
return
}
@ -57,12 +58,10 @@ internal class TokenListAnalyticsSender @Inject constructor(
stopLoadingTraceIfNeeded(userWallet.walletId, totalFiatBalance)
}
val currenciesStatuses = flattenCurrencies
sendBalanceLoadedEventIfNeeded(totalFiatBalance, currenciesStatuses)
sendToppedUpEventIfNeeded(userWallet, totalFiatBalance, currenciesStatuses)
sendUnreachableNetworksEventIfNeeded(currenciesStatuses)
sendTokenBalancesIfNeeded(currenciesStatuses)
sendBalanceLoadedEventIfNeeded(totalFiatBalance, flattenCurrencies)
sendToppedUpEventIfNeeded(userWallet, totalFiatBalance, flattenCurrencies)
sendUnreachableNetworksEventIfNeeded(flattenCurrencies)
sendTokenBalancesIfNeeded(flattenCurrencies)
}
private suspend fun startLoadingTraceIfNeeded(

View file

@ -0,0 +1,49 @@
package com.tangem.feature.wallet.presentation.wallet.subscribers
import com.tangem.domain.account.models.AccountStatusList
import com.tangem.domain.account.status.supplier.SingleAccountStatusListSupplier
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.feature.wallet.presentation.wallet.analytics.utils.TokenListAnalyticsSender
import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.onEach
/**
* Subscriber that monitors account list changes and sends token list analytics
* when the total fiat balance changes.
*
[REDACTED_AUTHOR]
*/
internal class TokenListAnalyticsSubscriber @AssistedInject constructor(
@Assisted override val userWallet: UserWallet,
override val singleAccountStatusListSupplier: SingleAccountStatusListSupplier,
private val stateHolder: WalletStateController,
private val tokenListAnalyticsSender: TokenListAnalyticsSender,
) : BasicWalletSubscriber() {
override fun create(coroutineScope: CoroutineScope): Flow<*> = getAccountStatusListFlow()
.distinctUntilChanged { old, new -> old.totalFiatBalance == new.totalFiatBalance }
.onEach(::sendTokenListAnalytics)
private suspend fun sendTokenListAnalytics(accountStatusList: AccountStatusList) {
val displayedState = stateHolder.getWalletStateIfSelected(userWallet.walletId)
val flattenCurrencies = accountStatusList.flattenCurrencies()
tokenListAnalyticsSender.send(
displayedUiState = displayedState,
userWallet = userWallet,
flattenCurrencies = flattenCurrencies,
totalFiatBalance = accountStatusList.totalFiatBalance,
)
}
@AssistedFactory
interface Factory {
fun create(userWallet: UserWallet): TokenListAnalyticsSubscriber
}
}

View file

@ -7,7 +7,7 @@
tangemBlockchainSdk = "develop-1437"
#tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds
tangemCardSdk = "develop-578"
tangemCardSdk = "develop-582"
#tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^
tangemVico = "2.0.0-alpha.25-tangem12"
#tangemVico = "0.0.1" # Keep it! - used for local builds ^

@ -1 +1 @@
Subproject commit 35755ad66f5f8fa6bcb1c84b443826cd315495d0
Subproject commit 43fab6f690538391cae17e046ffb2ec9fe08b0c7