Updated on 2026-08-14

This commit is contained in:
Tangem 2026-06-19 16:07:08 +05:00
parent a0e5656330
commit d82dbdd1e6
12 changed files with 256 additions and 129 deletions

View file

@ -40,6 +40,18 @@ import kotlin.time.Duration.Companion.minutes
private const val TAG = "PaymentAccountStatusFetcher"
/**
* Reorders cards to match [previousOrder] (by [TangemPayCard.id]), appending any card absent from it at the
* end while preserving the relative order among the new ones. Keeps the card layout stable when the backend
* reorders `productInstances` (e.g. after a rename bumps `updated_at`). Returns the receiver unchanged when
* [previousOrder] is empty (first load backend order).
*/
internal fun List<TangemPayCard>.stableOrder(previousOrder: List<String>): List<TangemPayCard> {
if (previousOrder.isEmpty()) return this
val indexById = previousOrder.withIndex().associate { (index, id) -> id to index }
return sortedBy { indexById[it.id] ?: Int.MAX_VALUE }
}
@Suppress("LongParameterList", "LargeClass")
internal class DefaultPaymentAccountStatusFetcher @Inject constructor(
private val paymentAccountStatusesStore: PaymentAccountStatusesStore,
@ -365,13 +377,18 @@ internal class DefaultPaymentAccountStatusFetcher @Inject constructor(
// placeholder for every locally tracked in-flight issuance order alongside the real cards.
val issuingCards = buildIssuingCards(userWalletId)
// Keep the card order stable across refetches: the backend orders `productInstances` by a mutable
// field (a rename bumps `updated_at`), which would otherwise make the renamed card jump. Anchor on
// the previously shown order and append newly seen cards at the end.
val orderedCards = tangemPayCards.stableOrder(previousRealCardOrder(userWalletId))
return PaymentAccountStatusValue.Loaded(
source = StatusSource.ACTUAL,
customerId = customerId,
depositAddress = cryptoBalance.depositAddress,
cryptoCurrency = tangemPayCurrencyFactory.create(userWalletId),
fiatRate = fiatRate,
cards = tangemPayCards + issuingCards,
cards = orderedCards + issuingCards,
balance = PaymentAccountStatusValue.Balance(
fiatBalance = fiatBalance,
cryptoBalance = cryptoBalance,
@ -381,6 +398,21 @@ internal class DefaultPaymentAccountStatusFetcher @Inject constructor(
)
}
/**
* Order of real (product-instance-backed) cards from the previously stored status, used as the stable
* anchor for [stableOrder]. Issuing placeholders are excluded they carry synthetic order ids and are
* always appended last. Empty on the first load (no prior [PaymentAccountStatusValue.Loaded]), which makes
* [stableOrder] fall back to the backend order.
*/
private suspend fun previousRealCardOrder(userWalletId: UserWalletId): List<String> {
val previousValue = paymentAccountStatusesStore.getSyncOrNull(userWalletId)?.value
return (previousValue as? PaymentAccountStatusValue.Loaded)
?.cards
?.filterNot { it.state == TangemPayCardState.Issuing }
?.map { it.id }
.orEmpty()
}
private suspend fun getCardState(cardId: String, userWalletId: UserWalletId): TangemPayCardState {
val closingOrderId = closeCardRepository.getCloseOrderId(userWalletId, cardId).getOrNull()
val reissueOrderId = reissueCardRepository.getReissueOrderId(userWalletId, cardId).getOrNull()

View file

@ -0,0 +1,80 @@
package com.tangem.data.pay.flow
import com.google.common.truth.Truth.assertThat
import com.tangem.domain.models.pay.TangemPayCard
import com.tangem.domain.models.pay.TangemPayCardFrozenState
import com.tangem.domain.models.pay.TangemPayCardState
import com.tangem.test.core.ProvideTestModels
import org.junit.jupiter.api.TestInstance
import org.junit.jupiter.params.ParameterizedTest
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
internal class StableOrderTest {
@ParameterizedTest
@ProvideTestModels
fun stableOrder(model: Model) {
// Act
val result = model.cards.stableOrder(model.previousOrder).map { it.id }
// Assert
assertThat(result).containsExactlyElementsIn(model.expectedIds).inOrder()
}
private fun provideTestModels() = listOf(
Model(
name = "GIVEN no previous order WHEN stableOrder THEN backend order is kept",
cards = listOf(card("a"), card("b"), card("c")),
previousOrder = emptyList(),
expectedIds = listOf("a", "b", "c"),
),
Model(
// The reported bug: a rename bumps updated_at, so the backend moves the renamed card.
name = "GIVEN backend reordered existing cards WHEN stableOrder THEN previous order is preserved",
cards = listOf(card("b"), card("c"), card("a")),
previousOrder = listOf("a", "b", "c"),
expectedIds = listOf("a", "b", "c"),
),
Model(
name = "GIVEN a newly seen card WHEN stableOrder THEN it is appended at the end",
cards = listOf(card("c"), card("a"), card("b")),
previousOrder = listOf("a", "b"),
expectedIds = listOf("a", "b", "c"),
),
Model(
name = "GIVEN several new cards WHEN stableOrder THEN they keep their backend order at the end",
cards = listOf(card("d"), card("b"), card("a"), card("c")),
previousOrder = listOf("a", "b"),
expectedIds = listOf("a", "b", "d", "c"),
),
Model(
name = "GIVEN previous order references a gone card WHEN stableOrder THEN the stale id is ignored",
cards = listOf(card("b"), card("a")),
previousOrder = listOf("a", "x", "b"),
expectedIds = listOf("a", "b"),
),
)
internal data class Model(
val name: String,
val cards: List<TangemPayCard>,
val previousOrder: List<String>,
val expectedIds: List<String>,
) {
override fun toString(): String = name
}
private companion object {
fun card(id: String): TangemPayCard = TangemPayCard(
id = id,
productInstanceId = id,
cardStatus = TangemPayCard.Status.ACTIVE,
hasPinCode = false,
displayName = null,
limit = null,
frozenState = TangemPayCardFrozenState.Unfrozen,
lastDigits = "0000",
state = TangemPayCardState.Active,
)
}
}