Updated on 2026-08-14
This commit is contained in:
commit
3e70beab28
103 changed files with 1908 additions and 626 deletions
|
|
@ -46,6 +46,7 @@ import com.tangem.domain.tokens.operations.TokenListFactory
|
|||
import com.tangem.domain.tokens.operations.TotalFiatBalanceCalculator
|
||||
import com.tangem.hot.sdk.model.HotWalletId
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import com.tangem.utils.logging.TangemLogger
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
|
|
@ -73,7 +74,7 @@ import java.math.BigDecimal
|
|||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
// TODO: Move to :data:account:status [REDACTED_JIRA]
|
||||
@Suppress("LongParameterList")
|
||||
@Suppress("LongParameterList", "LargeClass")
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
internal class DefaultSingleAccountStatusListProducer @AssistedInject constructor(
|
||||
@Assisted private val params: SingleAccountStatusListProducer.Params,
|
||||
|
|
@ -90,17 +91,29 @@ internal class DefaultSingleAccountStatusListProducer @AssistedInject constructo
|
|||
private val analyticsExceptionHandler: AnalyticsExceptionHandler,
|
||||
) : SingleAccountStatusListProducer {
|
||||
|
||||
private val logger = TangemLogger.withTag(TAG)
|
||||
|
||||
override val fallback: Option<AccountStatusList> = none()
|
||||
|
||||
override fun produce(): Flow<AccountStatusList> {
|
||||
logger.i("produce() called for ${params.userWalletId}")
|
||||
return flattenFlow()
|
||||
.onEach { list ->
|
||||
logger.i(
|
||||
"produce()[${params.userWalletId}] emit: accounts=${list.accountStatuses.size}, " +
|
||||
"currencies=${list.flattenCurrencies().size}, " +
|
||||
"totalFiatType=${list.totalFiatBalance::class.simpleName}",
|
||||
)
|
||||
}
|
||||
.flowOn(dispatchers.default)
|
||||
}
|
||||
|
||||
@Suppress("LongMethod")
|
||||
private fun flattenFlow(): Flow<AccountStatusList> = channelFlow {
|
||||
val walletId = params.userWalletId
|
||||
logger.i("flattenFlow[$walletId]: start")
|
||||
val userWallet = userWalletsListRepository.getSyncStrict(id = params.userWalletId)
|
||||
logger.i("flattenFlow[$walletId]: userWallet resolved (type=${userWallet::class.simpleName})")
|
||||
|
||||
val flattenCurrency: MutableSharedFlow<Map<AccountCurrencyId, CryptoCurrency>> = MutableSharedFlow(
|
||||
replay = 1,
|
||||
|
|
@ -108,11 +121,20 @@ internal class DefaultSingleAccountStatusListProducer @AssistedInject constructo
|
|||
)
|
||||
|
||||
val accountListFlow: StateFlow<AccountList> = singleAccountListSupplier(walletId)
|
||||
.onEach { accountList -> flattenCurrency.tryEmit(accountList.flattenMapCurrencies()) }
|
||||
.onEach { accountList ->
|
||||
logger.i(
|
||||
"flattenFlow[$walletId]: accountList emitted accounts=${accountList.accounts.size}, " +
|
||||
"currencies=${accountList.flattenMapCurrencies().size}",
|
||||
)
|
||||
flattenCurrency.tryEmit(accountList.flattenMapCurrencies())
|
||||
}
|
||||
.stateIn(this)
|
||||
|
||||
val hasCachedNetworks = networksRepository.hasCachedStatuses(walletId)
|
||||
logger.i("flattenFlow[$walletId]: hasCachedNetworks=$hasCachedNetworks")
|
||||
if (!hasCachedNetworks) {
|
||||
val initialAccounts = accountListFlow.value.accounts.size
|
||||
logger.i("flattenFlow[$walletId]: sending Loading placeholder (accounts=$initialAccounts)")
|
||||
send(createLoadingAccountStatusList(accountListFlow.value))
|
||||
}
|
||||
|
||||
|
|
@ -121,11 +143,19 @@ internal class DefaultSingleAccountStatusListProducer @AssistedInject constructo
|
|||
flattenCurrency = flattenCurrency,
|
||||
)
|
||||
|
||||
if (userWallet.isPaymentAccountSupported()) {
|
||||
val isPaymentSupported = userWallet.isPaymentAccountSupported()
|
||||
logger.i("flattenFlow[$walletId]: isPaymentAccountSupported=$isPaymentSupported")
|
||||
if (isPaymentSupported) {
|
||||
combineWithPaymentAccount(
|
||||
accountListFlow = accountListFlow,
|
||||
cryptoCurrencyStatusFlow = cryptoCurrencyStatusFlow,
|
||||
paymentAccountStatusFlow = paymentAccountStatusSupplier.invoke(userWalletId = params.userWalletId),
|
||||
paymentAccountStatusFlow = paymentAccountStatusSupplier.invoke(userWalletId = params.userWalletId)
|
||||
.onEach { paymentStatus ->
|
||||
logger.i(
|
||||
"flattenFlow[$walletId]: paymentAccountStatus emitted " +
|
||||
"valueType=${paymentStatus.value::class.simpleName}",
|
||||
)
|
||||
},
|
||||
)
|
||||
} else {
|
||||
combineWithoutPaymentAccount(
|
||||
|
|
@ -146,6 +176,12 @@ internal class DefaultSingleAccountStatusListProducer @AssistedInject constructo
|
|||
flow2 = cryptoCurrencyStatusFlow,
|
||||
flow3 = paymentAccountStatusFlow,
|
||||
transform = { accountList, currencyStatusMap, paymentAccountStatus ->
|
||||
logger.i(
|
||||
"combineWithPayment[${params.userWalletId}] transform: " +
|
||||
"accounts=${accountList.accounts.size}, " +
|
||||
"currencyStatusMap=${currencyStatusMap.size}, " +
|
||||
"paymentType=${paymentAccountStatus.value::class.simpleName}",
|
||||
)
|
||||
val accountStatuses = accountList.accounts.map { account ->
|
||||
when (account) {
|
||||
is Account.Payment -> paymentAccountStatus
|
||||
|
|
@ -192,6 +228,11 @@ internal class DefaultSingleAccountStatusListProducer @AssistedInject constructo
|
|||
flow = accountListFlow,
|
||||
flow2 = cryptoCurrencyStatusFlow,
|
||||
transform = { accountList, currencyStatusMap ->
|
||||
logger.i(
|
||||
"combineWithoutPayment[${params.userWalletId}] transform: " +
|
||||
"accounts=${accountList.accounts.size}, " +
|
||||
"currencyStatusMap=${currencyStatusMap.size}",
|
||||
)
|
||||
val accountStatuses = accountList.accounts
|
||||
.filterIsInstance<Account.CryptoPortfolio>()
|
||||
.map { account ->
|
||||
|
|
@ -242,14 +283,18 @@ internal class DefaultSingleAccountStatusListProducer @AssistedInject constructo
|
|||
): Flow<Map<AccountCurrencyId, CryptoCurrencyStatus>> {
|
||||
val walletId = userWallet.walletId
|
||||
val networkStatusFlow: SharedFlow<Map<Network.ID, NetworkStatus>> = networkStatusFlow(walletId)
|
||||
.onEach { logger.i("flattenCurrencyStatusFlow[$walletId]: networkStatuses emitted size=${it.size}") }
|
||||
.shareIn(this, started = SharingStarted.Eagerly, replay = 1)
|
||||
val stakingBalanceFlow: SharedFlow<Map<StakingID, Set<StakingBalance>>> = stakingFlow(userWallet)
|
||||
.onEach { logger.i("flattenCurrencyStatusFlow[$walletId]: stakingBalances emitted stakingIds=${it.size}") }
|
||||
.shareIn(this, started = SharingStarted.Eagerly, replay = 1)
|
||||
val quoteStatusFlow: SharedFlow<Map<CryptoCurrency.RawID, QuoteStatus>> = quoteStatusFlow()
|
||||
.onEach { logger.i("flattenCurrencyStatusFlow[$walletId]: quoteStatuses emitted size=${it.size}") }
|
||||
.shareIn(this, started = SharingStarted.Eagerly, replay = 1)
|
||||
|
||||
return flattenCurrency
|
||||
.distinctUntilChanged()
|
||||
.onEach { logger.i("flattenCurrencyStatusFlow[$walletId]: flattenCurrency emitted size=${it.size}") }
|
||||
.flatMapLatest { a ->
|
||||
combine(
|
||||
flow = networkStatusFlow,
|
||||
|
|
@ -264,6 +309,15 @@ internal class DefaultSingleAccountStatusListProducer @AssistedInject constructo
|
|||
)
|
||||
}
|
||||
.distinctUntilChanged()
|
||||
.onEach { box ->
|
||||
logger.i(
|
||||
"flattenCurrencyStatusFlow[$walletId]: box emitted " +
|
||||
"currencies=${box.flattenCurrencyMap.size}, " +
|
||||
"networks=${box.networkStatusMap.size}, " +
|
||||
"stakings=${box.stakingBalanceMap.size}, " +
|
||||
"quotes=${box.quoteStatusMap.size}",
|
||||
)
|
||||
}
|
||||
.map { box ->
|
||||
val flattenCurrencyMap: Map<AccountCurrencyId, CryptoCurrency> = box.flattenCurrencyMap
|
||||
val networkStatusMap: Map<Network.ID, NetworkStatus> = box.networkStatusMap
|
||||
|
|
@ -402,4 +456,8 @@ internal class DefaultSingleAccountStatusListProducer @AssistedInject constructo
|
|||
interface Factory : SingleAccountStatusListProducer.Factory {
|
||||
override fun create(params: SingleAccountStatusListProducer.Params): DefaultSingleAccountStatusListProducer
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val TAG = "SingleAccountStatusListProducer"
|
||||
}
|
||||
}
|
||||
|
|
@ -6,6 +6,7 @@ import com.tangem.domain.models.account.Account
|
|||
import com.tangem.domain.models.account.PaymentAccountStatusValue
|
||||
import com.tangem.domain.pay.flow.PaymentAccountStatusProducer
|
||||
import com.tangem.domain.pay.flow.PaymentAccountStatusSupplier
|
||||
import com.tangem.utils.logging.TangemLogger
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.*
|
||||
|
||||
|
|
@ -27,27 +28,51 @@ class IsAccountsModeEnabledUseCase(
|
|||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
operator fun invoke(): Flow<Boolean> {
|
||||
TangemLogger.i("$TAG: invoke() started")
|
||||
|
||||
val cryptoMode = multiAccountListSupplier.invoke()
|
||||
.map { lists -> lists.any { it.hasMultipleCryptoPortfolios() } }
|
||||
.onEach { lists ->
|
||||
TangemLogger.i("$TAG: multiAccountListSupplier emitted ${lists.size} lists (cryptoMode branch)")
|
||||
}
|
||||
.map { lists ->
|
||||
val isCryptoMode = lists.any { it.hasMultipleCryptoPortfolios() }
|
||||
TangemLogger.i("$TAG: cryptoMode=$isCryptoMode")
|
||||
isCryptoMode
|
||||
}
|
||||
|
||||
val paymentMode = multiAccountListSupplier.invoke().flatMapLatest { lists ->
|
||||
val walletIdsWithPayment = lists.mapNotNull { list ->
|
||||
if (list.accounts.any { it is Account.Payment }) list.userWalletId else null
|
||||
}
|
||||
TangemLogger.i("$TAG: walletIdsWithPayment=${walletIdsWithPayment.size}")
|
||||
|
||||
if (walletIdsWithPayment.isEmpty()) {
|
||||
flowOf(false)
|
||||
} else {
|
||||
val flows = walletIdsWithPayment.map { walletId ->
|
||||
paymentAccountStatusSupplier.invoke(walletId)
|
||||
.onEach { status ->
|
||||
TangemLogger.i("$TAG: paymentStatus for $walletId = ${status.value::class.simpleName}")
|
||||
}
|
||||
.map { it.value.isActivePayment() }
|
||||
.onStart { emit(false) }
|
||||
.onStart {
|
||||
TangemLogger.i("$TAG: paymentAccountStatusSupplier onStart for $walletId")
|
||||
emit(false)
|
||||
}
|
||||
}
|
||||
combine(flows) { results ->
|
||||
val isPaymentMode = results.any { it }
|
||||
TangemLogger.i("$TAG: paymentMode combine result=$isPaymentMode (${results.toList()})")
|
||||
isPaymentMode
|
||||
}
|
||||
combine(flows) { results -> results.any { it } }
|
||||
}
|
||||
}
|
||||
|
||||
return combine(cryptoMode, paymentMode) { crypto, payment -> crypto || payment }
|
||||
return combine(cryptoMode, paymentMode) { crypto, payment ->
|
||||
val isEnabled = crypto || payment
|
||||
TangemLogger.i("$TAG: final combine crypto=$crypto, payment=$payment, result=$isEnabled")
|
||||
isEnabled
|
||||
}
|
||||
.distinctUntilChanged()
|
||||
}
|
||||
|
||||
|
|
@ -89,5 +114,6 @@ class IsAccountsModeEnabledUseCase(
|
|||
|
||||
private companion object {
|
||||
const val PAYMENT_STATUS_SYNC_TIMEOUT_MS = 1_000L
|
||||
const val TAG = "IsAccountsModeEnabledUseCase"
|
||||
}
|
||||
}
|
||||
|
|
@ -22,20 +22,27 @@ sealed class IntroductionProcess(
|
|||
* Tracks opening the Create Wallet introduction screen.
|
||||
*/
|
||||
class CreateWalletIntroScreenOpened(
|
||||
screenType: ScreenType,
|
||||
referralId: String?,
|
||||
) : IntroductionProcess(
|
||||
event = "Create Wallet Intro Screen Opened",
|
||||
params = buildMap {
|
||||
put(AnalyticsParam.SCREEN_TYPE, screenType.value)
|
||||
putAll(getReferralParams(referralId))
|
||||
},
|
||||
), CriticalEvent
|
||||
), CriticalEvent {
|
||||
enum class ScreenType(val value: String) {
|
||||
Cold("Cold Wallet"),
|
||||
Hot("Mobile Wallet"),
|
||||
}
|
||||
}
|
||||
|
||||
class ButtonScanCard(
|
||||
val source: AnalyticsParam.ScreensSources,
|
||||
) : IntroductionProcess(
|
||||
event = "Button - Scan Card",
|
||||
params = mapOf(
|
||||
AnalyticsParam.Key.SOURCE to source.value,
|
||||
AnalyticsParam.SOURCE to source.value,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
@ -138,6 +138,7 @@ sealed class PaymentAccountStatusValue {
|
|||
* @property depositAddress The address for deposits, if available.
|
||||
* @property fiatBalance The fiat balance details.
|
||||
* @property cryptoBalance The crypto balance details.
|
||||
* @property availableForWithdrawal The crypto amount currently available for withdrawal/swap (excludes pending/locked funds).
|
||||
* @property cards The list of user's cards.
|
||||
*/
|
||||
@Serializable
|
||||
|
|
@ -148,13 +149,14 @@ sealed class PaymentAccountStatusValue {
|
|||
val depositAddress: String?,
|
||||
val fiatBalance: FiatBalance,
|
||||
val cryptoBalance: CryptoBalance,
|
||||
val availableForWithdrawal: SerializedBigDecimal,
|
||||
val cryptoCurrency: CryptoCurrency.Token,
|
||||
val cards: List<TangemPayCard>,
|
||||
) : PaymentAccountStatusValue() {
|
||||
val cryptoCurrencyStatus: CryptoCurrencyStatus = CryptoCurrencyStatus(
|
||||
currency = cryptoCurrency,
|
||||
value = CryptoCurrencyStatus.Loaded(
|
||||
amount = cryptoBalance.balance,
|
||||
amount = availableForWithdrawal,
|
||||
fiatAmount = fiatBalance.availableBalance,
|
||||
fiatRate = BigDecimal.ONE,
|
||||
priceChange = BigDecimal.ZERO,
|
||||
|
|
|
|||
|
|
@ -22,4 +22,5 @@ data class TangemPayCard(
|
|||
@SerialName("limit") val limit: TangemPayCardLimitData?,
|
||||
@SerialName("is_frozen") val isFrozen: Boolean,
|
||||
@SerialName("last_digits") val lastDigits: String,
|
||||
@SerialName("is_reissuing") val isReissuing: Boolean,
|
||||
)
|
||||
|
|
@ -46,11 +46,13 @@ sealed class StakingAnalyticsEvent(
|
|||
data class StakeInProgressScreenOpened(
|
||||
val validator: String,
|
||||
val action: StakingActionType,
|
||||
val feeAssetType: AnalyticsParam.FeeAssetType,
|
||||
) : StakingAnalyticsEvent(
|
||||
event = "Stake In Progress Screen Opened",
|
||||
params = mapOf(
|
||||
"Validator" to validator,
|
||||
"Action" to action.asAnalyticName,
|
||||
AnalyticsParam.Key.FEE_ASSET_TYPE to feeAssetType.value,
|
||||
),
|
||||
), AppsFlyerIncludedEvent
|
||||
|
||||
|
|
|
|||
|
|
@ -9,6 +9,10 @@ android {
|
|||
namespace = "com.tangem.domain.visa"
|
||||
}
|
||||
|
||||
tasks.withType<Test>().configureEach {
|
||||
useJUnitPlatform()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
/** Project - Core */
|
||||
api(projects.core.pagination)
|
||||
|
|
@ -35,4 +39,12 @@ dependencies {
|
|||
implementation(deps.moshi)
|
||||
implementation(deps.moshi.kotlin)
|
||||
ksp(deps.moshi.kotlin.codegen)
|
||||
|
||||
/** Tests */
|
||||
testImplementation(deps.test.junit5)
|
||||
testRuntimeOnly(deps.test.junit5.engine)
|
||||
testImplementation(deps.test.mockk)
|
||||
testImplementation(deps.test.coroutine)
|
||||
testImplementation(deps.test.truth)
|
||||
testImplementation(projects.common.test)
|
||||
}
|
||||
|
|
@ -83,5 +83,6 @@ data class CustomerInfo(
|
|||
val isPinSet: Boolean,
|
||||
val fiatBalance: PaymentAccountStatusValue.FiatBalance,
|
||||
val cryptoBalance: PaymentAccountStatusValue.CryptoBalance,
|
||||
val availableForWithdrawal: BigDecimal,
|
||||
)
|
||||
}
|
||||
|
|
@ -5,4 +5,7 @@ enum class OrderStatus {
|
|||
PROCESSING,
|
||||
COMPLETED,
|
||||
CANCELED,
|
||||
}
|
||||
}
|
||||
|
||||
val OrderStatus.isFinalStatus
|
||||
get() = this == OrderStatus.COMPLETED || this == OrderStatus.CANCELED
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
package com.tangem.domain.pay.model
|
||||
|
||||
data class TangemPayReissueOrderInfo(
|
||||
data class TangemPayOrderInfo(
|
||||
val orderId: String,
|
||||
val orderStatus: OrderStatus,
|
||||
)
|
||||
|
|
@ -7,6 +7,7 @@ import com.tangem.domain.models.wallet.UserWalletId
|
|||
import com.tangem.domain.pay.model.SetPinResult
|
||||
import com.tangem.domain.pay.model.TangemPayCardBalance
|
||||
import com.tangem.domain.pay.model.TangemPayCardDetails
|
||||
import com.tangem.domain.pay.model.TangemPayOrderInfo
|
||||
import com.tangem.domain.visa.model.TangemPayCardFrozenState
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
|
|
@ -24,14 +25,12 @@ interface TangemPayCardDetailsRepository {
|
|||
|
||||
suspend fun setAddToWalletAsDone(userWalletId: UserWalletId): Either<UniversalError, Unit>
|
||||
|
||||
suspend fun freezeCard(userWalletId: UserWalletId, cardId: String): Either<UniversalError, TangemPayCardFrozenState>
|
||||
suspend fun unfreezeCard(
|
||||
userWalletId: UserWalletId,
|
||||
cardId: String,
|
||||
): Either<UniversalError, TangemPayCardFrozenState>
|
||||
suspend fun freezeCard(userWalletId: UserWalletId, cardId: String): Either<UniversalError, TangemPayOrderInfo>
|
||||
suspend fun unfreezeCard(userWalletId: UserWalletId, cardId: String): Either<UniversalError, TangemPayOrderInfo>
|
||||
|
||||
fun cardFrozenState(cardId: String): Flow<TangemPayCardFrozenState>
|
||||
suspend fun cardFrozenStateSync(cardId: String): TangemPayCardFrozenState?
|
||||
suspend fun setCardFrozenState(cardId: String, state: TangemPayCardFrozenState)
|
||||
|
||||
suspend fun updateCardDisplayName(
|
||||
cardId: String,
|
||||
|
|
@ -44,4 +43,6 @@ interface TangemPayCardDetailsRepository {
|
|||
userWalletId: UserWalletId,
|
||||
limit: String,
|
||||
): Either<UniversalError, Unit>
|
||||
|
||||
suspend fun getOrderInfo(userWalletId: UserWalletId, orderId: String): Either<UniversalError, TangemPayOrderInfo>
|
||||
}
|
||||
|
|
@ -4,19 +4,19 @@ import arrow.core.Either
|
|||
import com.tangem.core.error.UniversalError
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.models.pay.TangemPayReissueCardFee
|
||||
import com.tangem.domain.pay.model.TangemPayReissueOrderInfo
|
||||
import com.tangem.domain.pay.model.TangemPayOrderInfo
|
||||
import com.tangem.domain.visa.error.VisaApiError
|
||||
|
||||
interface TangemPayReissueCardRepository {
|
||||
|
||||
suspend fun getReissueCardFee(userWalletId: UserWalletId): Either<VisaApiError, TangemPayReissueCardFee>
|
||||
|
||||
suspend fun reissueCard(userWalletId: UserWalletId, cardId: String): Either<VisaApiError, TangemPayReissueOrderInfo>
|
||||
suspend fun reissueCard(userWalletId: UserWalletId, cardId: String): Either<VisaApiError, TangemPayOrderInfo>
|
||||
|
||||
suspend fun storeReissueOrderId(cardId: String, orderId: String): Either<UniversalError, Unit>
|
||||
|
||||
suspend fun getReissueOrderInfo(
|
||||
userWalletId: UserWalletId,
|
||||
cardId: String,
|
||||
): Either<UniversalError, TangemPayReissueOrderInfo?>
|
||||
): Either<UniversalError, TangemPayOrderInfo?>
|
||||
}
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
package com.tangem.domain.pay.usecase
|
||||
|
||||
import arrow.core.Either
|
||||
import arrow.core.raise.either
|
||||
import com.tangem.core.error.UniversalError
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.pay.repository.TangemPayCardDetailsRepository
|
||||
import com.tangem.domain.visa.error.VisaApiError
|
||||
import com.tangem.domain.visa.model.TangemPayCardFrozenState
|
||||
import com.tangem.utils.coroutines.AppCoroutineScope
|
||||
import kotlinx.coroutines.async
|
||||
|
||||
class ChangeCardFrozenStateUseCase(
|
||||
private val cardDetailsRepository: TangemPayCardDetailsRepository,
|
||||
private val startTangemPayOrderPollingUseCase: StartTangemPayOrderPollingUseCase,
|
||||
private val appCoroutineScope: AppCoroutineScope,
|
||||
) {
|
||||
suspend operator fun invoke(
|
||||
userWalletId: UserWalletId,
|
||||
cardId: String,
|
||||
isFreezing: Boolean,
|
||||
): Either<UniversalError, Unit> {
|
||||
val successState = if (isFreezing) TangemPayCardFrozenState.Frozen else TangemPayCardFrozenState.Unfrozen
|
||||
val failState = if (isFreezing) TangemPayCardFrozenState.Unfrozen else TangemPayCardFrozenState.Frozen
|
||||
return either {
|
||||
cardDetailsRepository.setCardFrozenState(cardId, TangemPayCardFrozenState.Pending)
|
||||
|
||||
val order = if (isFreezing) {
|
||||
cardDetailsRepository.freezeCard(userWalletId, cardId).bind()
|
||||
} else {
|
||||
cardDetailsRepository.unfreezeCard(userWalletId, cardId).bind()
|
||||
}
|
||||
|
||||
val isCompleted = appCoroutineScope.async {
|
||||
val isCompleted = startTangemPayOrderPollingUseCase(order, userWalletId)
|
||||
cardDetailsRepository.setCardFrozenState(cardId, if (isCompleted) successState else failState)
|
||||
isCompleted
|
||||
}.await()
|
||||
|
||||
if (!isCompleted) raise(VisaApiError.Unspecified)
|
||||
}.onLeft {
|
||||
cardDetailsRepository.setCardFrozenState(cardId, failState)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
package com.tangem.domain.pay.usecase
|
||||
|
||||
import arrow.core.Either
|
||||
import arrow.core.raise.either
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.pay.flow.PaymentAccountStatusFetcher
|
||||
import com.tangem.domain.pay.model.OrderStatus
|
||||
import com.tangem.domain.pay.repository.TangemPayReissueCardRepository
|
||||
import com.tangem.domain.visa.error.VisaApiError
|
||||
import com.tangem.utils.coroutines.AppCoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class ReissueTangemPayCardUseCase(
|
||||
private val reissueCardRepository: TangemPayReissueCardRepository,
|
||||
private val startTangemPayOrderPollingUseCase: StartTangemPayOrderPollingUseCase,
|
||||
private val paymentAccountStatusFetcher: PaymentAccountStatusFetcher,
|
||||
private val appCoroutineScope: AppCoroutineScope,
|
||||
) {
|
||||
suspend operator fun invoke(userWalletId: UserWalletId, cardId: String): Either<VisaApiError, Unit> = either {
|
||||
val order = reissueCardRepository.reissueCard(userWalletId, cardId).bind()
|
||||
|
||||
if (order.orderStatus == OrderStatus.CANCELED) {
|
||||
raise(VisaApiError.Unspecified)
|
||||
}
|
||||
|
||||
reissueCardRepository.storeReissueOrderId(cardId, order.orderId)
|
||||
paymentAccountStatusFetcher.invoke(userWalletId)
|
||||
|
||||
appCoroutineScope.launch {
|
||||
startTangemPayOrderPollingUseCase(order, userWalletId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
package com.tangem.domain.pay.usecase
|
||||
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.pay.flow.PaymentAccountStatusFetcher
|
||||
import com.tangem.domain.pay.model.OrderStatus
|
||||
import com.tangem.domain.pay.model.TangemPayOrderInfo
|
||||
import com.tangem.domain.pay.model.isFinalStatus
|
||||
import com.tangem.domain.pay.repository.TangemPayCardDetailsRepository
|
||||
import kotlinx.coroutines.delay
|
||||
|
||||
class StartTangemPayOrderPollingUseCase(
|
||||
private val cardDetailsRepository: TangemPayCardDetailsRepository,
|
||||
private val paymentAccountStatusFetcher: PaymentAccountStatusFetcher,
|
||||
) {
|
||||
suspend operator fun invoke(order: TangemPayOrderInfo, userWalletId: UserWalletId): Boolean {
|
||||
while (true) {
|
||||
val newOrder = if (order.orderStatus.isFinalStatus) {
|
||||
order
|
||||
} else {
|
||||
cardDetailsRepository.getOrderInfo(userWalletId, order.orderId).getOrNull()
|
||||
}
|
||||
|
||||
if (newOrder != null && newOrder.orderStatus.isFinalStatus) {
|
||||
paymentAccountStatusFetcher.invoke(userWalletId)
|
||||
return newOrder.orderStatus == OrderStatus.COMPLETED
|
||||
}
|
||||
|
||||
delay(POLLING_DELAY)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val POLLING_DELAY = 3000L
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,107 @@
|
|||
package com.tangem.domain.pay.usecase
|
||||
|
||||
import arrow.core.left
|
||||
import arrow.core.right
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import com.tangem.common.test.TestAppCoroutineScope
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.pay.model.OrderStatus
|
||||
import com.tangem.domain.pay.model.TangemPayOrderInfo
|
||||
import com.tangem.domain.pay.repository.TangemPayCardDetailsRepository
|
||||
import com.tangem.domain.visa.error.VisaApiError
|
||||
import com.tangem.domain.visa.model.TangemPayCardFrozenState
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.coVerify
|
||||
import io.mockk.coVerifyOrder
|
||||
import io.mockk.mockk
|
||||
import kotlinx.coroutines.test.TestScope
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
internal class ChangeCardFrozenStateUseCaseTest {
|
||||
|
||||
private val cardDetailsRepository: TangemPayCardDetailsRepository = mockk(relaxUnitFun = true)
|
||||
private val startPollingUseCase: StartTangemPayOrderPollingUseCase = mockk()
|
||||
|
||||
@Test
|
||||
fun `GIVEN freezeCard fails WHEN invoke with isFreezing=true THEN sets Pending then Unfrozen and returns Left`() =
|
||||
runTest {
|
||||
val useCase = createUseCase()
|
||||
coEvery {
|
||||
cardDetailsRepository.freezeCard(USER_WALLET_ID, CARD_ID)
|
||||
} returns VisaApiError.Unspecified.left()
|
||||
|
||||
val result = useCase(USER_WALLET_ID, CARD_ID, isFreezing = true)
|
||||
|
||||
assertThat(result.isLeft()).isTrue()
|
||||
coVerifyOrder {
|
||||
cardDetailsRepository.setCardFrozenState(CARD_ID, TangemPayCardFrozenState.Pending)
|
||||
cardDetailsRepository.setCardFrozenState(CARD_ID, TangemPayCardFrozenState.Unfrozen)
|
||||
}
|
||||
coVerify(exactly = 0) { startPollingUseCase(any(), any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN unfreezeCard fails WHEN invoke with isFreezing=false THEN sets Pending then Frozen and returns Left`() =
|
||||
runTest {
|
||||
val useCase = createUseCase()
|
||||
coEvery {
|
||||
cardDetailsRepository.unfreezeCard(USER_WALLET_ID, CARD_ID)
|
||||
} returns VisaApiError.Unspecified.left()
|
||||
|
||||
val result = useCase(USER_WALLET_ID, CARD_ID, isFreezing = false)
|
||||
|
||||
assertThat(result.isLeft()).isTrue()
|
||||
coVerifyOrder {
|
||||
cardDetailsRepository.setCardFrozenState(CARD_ID, TangemPayCardFrozenState.Pending)
|
||||
cardDetailsRepository.setCardFrozenState(CARD_ID, TangemPayCardFrozenState.Frozen)
|
||||
}
|
||||
coVerify(exactly = 0) { startPollingUseCase(any(), any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN freeze succeeds and order COMPLETED WHEN invoke with isFreezing=true THEN sets Frozen and returns Right`() =
|
||||
runTest {
|
||||
val useCase = createUseCase()
|
||||
val order = TangemPayOrderInfo(ORDER_ID, OrderStatus.COMPLETED)
|
||||
coEvery { cardDetailsRepository.freezeCard(USER_WALLET_ID, CARD_ID) } returns order.right()
|
||||
coEvery { startPollingUseCase(order, USER_WALLET_ID) } returns true
|
||||
|
||||
val result = useCase(USER_WALLET_ID, CARD_ID, isFreezing = true)
|
||||
|
||||
assertThat(result.isRight()).isTrue()
|
||||
coVerifyOrder {
|
||||
cardDetailsRepository.setCardFrozenState(CARD_ID, TangemPayCardFrozenState.Pending)
|
||||
cardDetailsRepository.setCardFrozenState(CARD_ID, TangemPayCardFrozenState.Frozen)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN unfreeze succeeds and order COMPLETED WHEN invoke with isFreezing=false THEN sets Unfrozen and returns Right`() =
|
||||
runTest {
|
||||
val useCase = createUseCase()
|
||||
val order = TangemPayOrderInfo(ORDER_ID, OrderStatus.COMPLETED)
|
||||
coEvery { cardDetailsRepository.unfreezeCard(USER_WALLET_ID, CARD_ID) } returns order.right()
|
||||
coEvery { startPollingUseCase(order, USER_WALLET_ID) } returns true
|
||||
|
||||
val result = useCase(USER_WALLET_ID, CARD_ID, isFreezing = false)
|
||||
|
||||
assertThat(result.isRight()).isTrue()
|
||||
coVerifyOrder {
|
||||
cardDetailsRepository.setCardFrozenState(CARD_ID, TangemPayCardFrozenState.Pending)
|
||||
cardDetailsRepository.setCardFrozenState(CARD_ID, TangemPayCardFrozenState.Unfrozen)
|
||||
}
|
||||
}
|
||||
|
||||
private fun TestScope.createUseCase() = ChangeCardFrozenStateUseCase(
|
||||
cardDetailsRepository = cardDetailsRepository,
|
||||
startTangemPayOrderPollingUseCase = startPollingUseCase,
|
||||
appCoroutineScope = TestAppCoroutineScope(this),
|
||||
)
|
||||
|
||||
private companion object {
|
||||
val USER_WALLET_ID = UserWalletId("aabbcc112233")
|
||||
const val CARD_ID = "card-test-id"
|
||||
const val ORDER_ID = "order-test-1"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,119 @@
|
|||
package com.tangem.domain.pay.usecase
|
||||
|
||||
import arrow.core.left
|
||||
import arrow.core.right
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.pay.flow.PaymentAccountStatusFetcher
|
||||
import com.tangem.domain.pay.model.OrderStatus
|
||||
import com.tangem.domain.pay.model.TangemPayOrderInfo
|
||||
import com.tangem.domain.pay.repository.TangemPayCardDetailsRepository
|
||||
import com.tangem.domain.visa.error.VisaApiError
|
||||
import io.mockk.*
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
internal class StartTangemPayOrderPollingUseCaseTest {
|
||||
|
||||
private val cardDetailsRepository: TangemPayCardDetailsRepository = mockk()
|
||||
private val paymentAccountStatusFetcher: PaymentAccountStatusFetcher = mockk()
|
||||
|
||||
private val useCase = StartTangemPayOrderPollingUseCase(
|
||||
cardDetailsRepository = cardDetailsRepository,
|
||||
paymentAccountStatusFetcher = paymentAccountStatusFetcher,
|
||||
)
|
||||
|
||||
@Test
|
||||
fun `GIVEN order already COMPLETED WHEN invoke THEN returns true without polling`() = runTest {
|
||||
val order = TangemPayOrderInfo(ORDER_ID, OrderStatus.COMPLETED)
|
||||
coEvery { paymentAccountStatusFetcher.invoke(USER_WALLET_ID) } returns Unit.right()
|
||||
|
||||
val result = useCase(order, USER_WALLET_ID)
|
||||
|
||||
assertThat(result).isTrue()
|
||||
coVerify(exactly = 1) { paymentAccountStatusFetcher.invoke(USER_WALLET_ID) }
|
||||
coVerify(exactly = 0) { cardDetailsRepository.getOrderInfo(any(), any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN order already CANCELED WHEN invoke THEN returns false without polling`() = runTest {
|
||||
val order = TangemPayOrderInfo(ORDER_ID, OrderStatus.CANCELED)
|
||||
coEvery { paymentAccountStatusFetcher.invoke(USER_WALLET_ID) } returns Unit.right()
|
||||
|
||||
val result = useCase(order, USER_WALLET_ID)
|
||||
|
||||
assertThat(result).isFalse()
|
||||
coVerify(exactly = 1) { paymentAccountStatusFetcher.invoke(USER_WALLET_ID) }
|
||||
coVerify(exactly = 0) { cardDetailsRepository.getOrderInfo(any(), any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN processing order WHEN poll returns COMPLETED THEN returns true and fetches status`() = runTest {
|
||||
val order = TangemPayOrderInfo(ORDER_ID, OrderStatus.PROCESSING)
|
||||
coEvery {
|
||||
cardDetailsRepository.getOrderInfo(USER_WALLET_ID, ORDER_ID)
|
||||
} returns TangemPayOrderInfo(ORDER_ID, OrderStatus.COMPLETED).right()
|
||||
coEvery { paymentAccountStatusFetcher.invoke(USER_WALLET_ID) } returns Unit.right()
|
||||
|
||||
val result = useCase(order, USER_WALLET_ID)
|
||||
|
||||
assertThat(result).isTrue()
|
||||
coVerify(exactly = 1) { cardDetailsRepository.getOrderInfo(USER_WALLET_ID, ORDER_ID) }
|
||||
coVerify(exactly = 1) { paymentAccountStatusFetcher.invoke(USER_WALLET_ID) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN processing order WHEN poll returns CANCELED THEN returns false and fetches status`() = runTest {
|
||||
val order = TangemPayOrderInfo(ORDER_ID, OrderStatus.PROCESSING)
|
||||
coEvery {
|
||||
cardDetailsRepository.getOrderInfo(USER_WALLET_ID, ORDER_ID)
|
||||
} returns TangemPayOrderInfo(ORDER_ID, OrderStatus.CANCELED).right()
|
||||
coEvery { paymentAccountStatusFetcher.invoke(USER_WALLET_ID) } returns Unit.right()
|
||||
|
||||
val result = useCase(order, USER_WALLET_ID)
|
||||
|
||||
assertThat(result).isFalse()
|
||||
coVerify(exactly = 1) { paymentAccountStatusFetcher.invoke(USER_WALLET_ID) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN new order WHEN getOrderInfo fails once then returns COMPLETED THEN returns true after two polls`() = runTest {
|
||||
val order = TangemPayOrderInfo(ORDER_ID, OrderStatus.NEW)
|
||||
coEvery {
|
||||
cardDetailsRepository.getOrderInfo(USER_WALLET_ID, ORDER_ID)
|
||||
} returnsMany listOf(
|
||||
VisaApiError.Unspecified.left(),
|
||||
TangemPayOrderInfo(ORDER_ID, OrderStatus.COMPLETED).right(),
|
||||
)
|
||||
coEvery { paymentAccountStatusFetcher.invoke(USER_WALLET_ID) } returns Unit.right()
|
||||
|
||||
val result = useCase(order, USER_WALLET_ID)
|
||||
|
||||
assertThat(result).isTrue()
|
||||
coVerify(exactly = 2) { cardDetailsRepository.getOrderInfo(USER_WALLET_ID, ORDER_ID) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN processing order WHEN multiple non-final polls then COMPLETED THEN returns true after all polls`() = runTest {
|
||||
val order = TangemPayOrderInfo(ORDER_ID, OrderStatus.PROCESSING)
|
||||
coEvery {
|
||||
cardDetailsRepository.getOrderInfo(USER_WALLET_ID, ORDER_ID)
|
||||
} returnsMany listOf(
|
||||
TangemPayOrderInfo(ORDER_ID, OrderStatus.PROCESSING).right(),
|
||||
TangemPayOrderInfo(ORDER_ID, OrderStatus.NEW).right(),
|
||||
TangemPayOrderInfo(ORDER_ID, OrderStatus.COMPLETED).right(),
|
||||
)
|
||||
coEvery { paymentAccountStatusFetcher.invoke(USER_WALLET_ID) } returns Unit.right()
|
||||
|
||||
val result = useCase(order, USER_WALLET_ID)
|
||||
|
||||
assertThat(result).isTrue()
|
||||
coVerify(exactly = 3) { cardDetailsRepository.getOrderInfo(USER_WALLET_ID, ORDER_ID) }
|
||||
coVerify(exactly = 1) { paymentAccountStatusFetcher.invoke(USER_WALLET_ID) }
|
||||
}
|
||||
|
||||
private companion object {
|
||||
val USER_WALLET_ID = UserWalletId("aabbcc112233")
|
||||
const val ORDER_ID = "order-test-1"
|
||||
}
|
||||
}
|
||||
|
|
@ -157,6 +157,7 @@ sealed class WcAnalyticEvents(
|
|||
network: Network,
|
||||
securityStatus: CheckDAppResult,
|
||||
accountDerivation: Int?,
|
||||
feeAssetType: AnalyticsParam.FeeAssetType = AnalyticsParam.FeeAssetType.Coin,
|
||||
) : WcAnalyticEvents(
|
||||
event = "Signature Request Handled",
|
||||
params = buildMap {
|
||||
|
|
@ -168,6 +169,7 @@ sealed class WcAnalyticEvents(
|
|||
accountDerivation?.let {
|
||||
put(AnalyticsParam.ACCOUNT_DERIVATION, it.toString())
|
||||
}
|
||||
put(AnalyticsParam.Key.FEE_ASSET_TYPE, feeAssetType.value)
|
||||
},
|
||||
), AppsFlyerIncludedEvent
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue