Updated on 2026-08-14

This commit is contained in:
Tangem 2025-12-09 15:11:17 +03:00
commit aeb60e0cec
49 changed files with 402 additions and 192 deletions

View file

@ -1,6 +0,0 @@
package com.tangem.tap.common.extensions
fun String.removePrefixOrNull(prefix: String): String? = when {
startsWith(prefix) -> substring(prefix.length)
else -> null
}

View file

@ -15,7 +15,6 @@ import com.tangem.crypto.hdWallet.DerivationPath
import com.tangem.crypto.hdWallet.bip32.ExtendedPublicKey import com.tangem.crypto.hdWallet.bip32.ExtendedPublicKey
import com.tangem.domain.card.common.visa.VisaUtilities import com.tangem.domain.card.common.visa.VisaUtilities
import com.tangem.domain.visa.error.VisaActivationError import com.tangem.domain.visa.error.VisaActivationError
import com.tangem.operations.ScanTask
import com.tangem.operations.derivation.DeriveWalletPublicKeyTask import com.tangem.operations.derivation.DeriveWalletPublicKeyTask
import com.tangem.operations.sign.SignHashCommand import com.tangem.operations.sign.SignHashCommand
@ -92,25 +91,7 @@ class TangemPaySignWithdrawalHashTask(
?: targetWalletPublicKey.toDecompressedPublicKey(), ?: targetWalletPublicKey.toDecompressedPublicKey(),
).asRSVLegacyEVM().toHexString().lowercase() ).asRSVLegacyEVM().toHexString().lowercase()
scanCard( callback(CompletionResult.Success(rsvSignature))
session = session,
callback = callback,
signedData = rsvSignature,
)
}
is CompletionResult.Failure -> {
callback(CompletionResult.Failure(result.error))
}
}
}
}
private fun scanCard(signedData: String, session: CardSession, callback: CompletionCallback<String>) {
val scanTask = ScanTask()
scanTask.run(session) { result ->
when (result) {
is CompletionResult.Success -> {
callback(CompletionResult.Success(signedData))
} }
is CompletionResult.Failure -> { is CompletionResult.Failure -> {
callback(CompletionResult.Failure(result.error)) callback(CompletionResult.Failure(result.error))

View file

@ -660,8 +660,13 @@ internal class ChildFactory @Inject constructor(
createComponentChild( createComponentChild(
context = context, context = context,
params = when (val mode = route.mode) { params = when (val mode = route.mode) {
is AppRoute.TangemPayOnboarding.Mode.ContinueOnboarding -> ContinueOnboarding is AppRoute.TangemPayOnboarding.Mode.ContinueOnboarding -> ContinueOnboarding(
is AppRoute.TangemPayOnboarding.Mode.Deeplink -> Deeplink(deeplink = mode.deeplink) userWalletId = mode.userWalletId,
)
is AppRoute.TangemPayOnboarding.Mode.Deeplink -> Deeplink(
deeplink = mode.deeplink,
userWalletId = mode.userWalletId,
)
}, },
componentFactory = tangemPayOnboardingComponentFactory, componentFactory = tangemPayOnboardingComponentFactory,
) )

View file

@ -431,10 +431,13 @@ sealed class AppRoute(val path: String) : Route {
@Serializable @Serializable
data class Deeplink( data class Deeplink(
val deeplink: String, val deeplink: String,
val userWalletId: UserWalletId?,
) : Mode() ) : Mode()
@Serializable @Serializable
object ContinueOnboarding : Mode() data class ContinueOnboarding(
val userWalletId: UserWalletId?,
) : Mode()
} }
} }

View file

@ -48,7 +48,7 @@ class TokenItemStateConverter(
private val iconStateProvider: (CryptoCurrencyStatus) -> CurrencyIconState = { private val iconStateProvider: (CryptoCurrencyStatus) -> CurrencyIconState = {
CryptoCurrencyToIconStateConverter().convert(it) CryptoCurrencyToIconStateConverter().convert(it)
}, },
private val onApyLabelClick: ((CryptoCurrencyStatus, String) -> Unit)? = null, private val onApyLabelClick: ((CryptoCurrencyStatus, ApySource, String) -> Unit)? = null,
private val titleStateProvider: (CryptoCurrencyStatus) -> TokenItemState.TitleState = { currencyStatus -> private val titleStateProvider: (CryptoCurrencyStatus) -> TokenItemState.TitleState = { currencyStatus ->
createTitleState( createTitleState(
currencyStatus = currencyStatus, currencyStatus = currencyStatus,
@ -166,7 +166,7 @@ class TokenItemStateConverter(
currencyStatus: CryptoCurrencyStatus, currencyStatus: CryptoCurrencyStatus,
yieldModuleApyMap: Map<String, String>, yieldModuleApyMap: Map<String, String>,
stakingApyMap: Map<String, List<Yield.Validator>>, stakingApyMap: Map<String, List<Yield.Validator>>,
onApyLabelClick: ((CryptoCurrencyStatus, String) -> Unit)?, onApyLabelClick: ((CryptoCurrencyStatus, ApySource, String) -> Unit)?,
): TokenItemState.TitleState { ): TokenItemState.TitleState {
return when (val value = currencyStatus.value) { return when (val value = currencyStatus.value) {
is CryptoCurrencyStatus.Loading, is CryptoCurrencyStatus.Loading,
@ -192,7 +192,7 @@ class TokenItemStateConverter(
earnApy = apyInfo?.text, earnApy = apyInfo?.text,
earnApyIsActive = apyInfo?.isActive == true, earnApyIsActive = apyInfo?.isActive == true,
onApyLabelClick = if (apyInfo?.apy != null && onApyLabelClick != null) { onApyLabelClick = if (apyInfo?.apy != null && onApyLabelClick != null) {
{ onApyLabelClick.invoke(currencyStatus, apyInfo.apy) } { onApyLabelClick.invoke(currencyStatus, apyInfo.source, apyInfo.apy) }
} else { } else {
null null
}, },
@ -224,6 +224,7 @@ class TokenItemStateConverter(
), ),
isActive = isActive, isActive = isActive,
apy = yieldSupplyApy, apy = yieldSupplyApy,
source = ApySource.YIELD_SUPPLY,
) )
} }
} }
@ -249,6 +250,7 @@ class TokenItemStateConverter(
), ),
isActive = stakingInfo.isActive, isActive = stakingInfo.isActive,
apy = apyString, apy = apyString,
source = ApySource.STAKING,
) )
} }
} }
@ -433,5 +435,11 @@ class TokenItemStateConverter(
val text: TextReference?, val text: TextReference?,
val isActive: Boolean, val isActive: Boolean,
val apy: String?, val apy: String?,
val source: ApySource,
) )
enum class ApySource {
STAKING,
YIELD_SUPPLY,
}
} }

View file

@ -9,4 +9,8 @@ fun String.uriValidate(): Boolean {
val regex = DEEPLINK_VALIDATION_REGEX.toRegex() val regex = DEEPLINK_VALIDATION_REGEX.toRegex()
return !regex.containsMatchIn(this) return !regex.containsMatchIn(this)
}
fun String.addHexPrefix(): String {
return if (this.startsWith("0x")) this else "0x$this"
} }

View file

@ -9,7 +9,6 @@ import com.tangem.core.error.UniversalError
import com.tangem.data.common.currency.CryptoCurrencyFactory import com.tangem.data.common.currency.CryptoCurrencyFactory
import com.tangem.data.common.network.NetworkFactory import com.tangem.data.common.network.NetworkFactory
import com.tangem.data.pay.util.TangemPayErrorConverter import com.tangem.data.pay.util.TangemPayErrorConverter
import com.tangem.data.pay.util.TangemPayWalletsManager
import com.tangem.datasource.di.NetworkMoshi import com.tangem.datasource.di.NetworkMoshi
import com.tangem.domain.models.ReceiveAddressModel import com.tangem.domain.models.ReceiveAddressModel
import com.tangem.domain.models.ReceiveAddressModel.NameService import com.tangem.domain.models.ReceiveAddressModel.NameService
@ -32,7 +31,6 @@ private const val TOKEN_DECIMALS = 6
internal class DefaultTangemPaySwapDataFactory @Inject constructor( internal class DefaultTangemPaySwapDataFactory @Inject constructor(
@NetworkMoshi moshi: Moshi, @NetworkMoshi moshi: Moshi,
private val tangemPayWalletsManager: TangemPayWalletsManager,
excludedBlockchains: ExcludedBlockchains, excludedBlockchains: ExcludedBlockchains,
) : TangemPaySwapDataFactory { ) : TangemPaySwapDataFactory {
@ -63,17 +61,17 @@ internal class DefaultTangemPaySwapDataFactory @Inject constructor(
} }
override fun create( override fun create(
userWallet: UserWallet,
depositAddress: String, depositAddress: String,
chainId: Int, chainId: Int,
cryptoBalance: BigDecimal, cryptoBalance: BigDecimal,
fiatBalance: BigDecimal, fiatBalance: BigDecimal,
): Either<UniversalError, TangemPayTopUpData> { ): Either<UniversalError, TangemPayTopUpData> {
return catch { return catch {
val wallet = tangemPayWalletsManager.getDefaultWalletForTangemPayBlocking() val currency = getCurrency(userWallet, chainId)
val currency = getCurrency(wallet, chainId)
TangemPayTopUpData( TangemPayTopUpData(
currency = currency, currency = currency,
walletId = wallet.walletId, walletId = userWallet.walletId,
cryptoBalance = cryptoBalance, cryptoBalance = cryptoBalance,
fiatBalance = fiatBalance, fiatBalance = fiatBalance,
depositAddress = depositAddress, depositAddress = depositAddress,

View file

@ -18,6 +18,7 @@ import com.tangem.domain.pay.repository.TangemPaySwapRepository
import com.tangem.domain.visa.error.VisaApiError import com.tangem.domain.visa.error.VisaApiError
import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.features.hotwallet.HotWalletFeatureToggles import com.tangem.features.hotwallet.HotWalletFeatureToggles
import com.tangem.utils.extensions.addHexPrefix
import java.math.BigDecimal import java.math.BigDecimal
import java.math.RoundingMode import java.math.RoundingMode
import java.util.Currency import java.util.Currency
@ -66,7 +67,7 @@ internal class DefaultTangemPaySwapRepository @Inject constructor(
recipientAddress = receiverAddress, recipientAddress = receiverAddress,
adminSalt = result.salt, adminSalt = result.salt,
senderAddress = result.senderAddress, senderAddress = result.senderAddress,
adminSignature = signatureResult.signature, adminSignature = signatureResult.signature.addHexPrefix(),
) )
tangemPayApi.withdraw(authHeader = authHeader, body = request) tangemPayApi.withdraw(authHeader = authHeader, body = request)
} }

View file

@ -21,7 +21,6 @@ import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import javax.inject.Inject import javax.inject.Inject
private const val INITIAL_CURSOR = "initial_cursor_key" private const val INITIAL_CURSOR = "initial_cursor_key"
private const val TAG = "TangemPay: TangemPayTxHistoryRepository:"
internal class DefaultTangemPayTxHistoryRepository @Inject constructor( internal class DefaultTangemPayTxHistoryRepository @Inject constructor(
private val requestPerformer: TangemPayRequestPerformer, private val requestPerformer: TangemPayRequestPerformer,
@ -106,12 +105,14 @@ internal class DefaultTangemPayTxHistoryRepository @Inject constructor(
cursor: String?, cursor: String?,
pageSize: Int, pageSize: Int,
) { ) {
requestPerformer.runWithErrorLogs(TAG) { requestPerformer.performRequest(userWalletId = userWalletId) { authHeader ->
val result = requestPerformer.request(userWalletId) { authHeader -> visaApi.getTangemPayTxHistory(authHeader = authHeader, limit = pageSize, cursor = cursor)
visaApi.getTangemPayTxHistory(authHeader = authHeader, limit = pageSize, cursor = cursor) }.onLeft {
}.result error(it.toString())
}.onRight { response ->
val result = response.result
val items = txHistoryItemConverter.convertList(result.transactions).filterNotNull() val items = txHistoryItemConverter.convertList(result.transactions).filterNotNull()
txHistoryItemsStore.store(key = customerWalletAddress, cursor = cursor ?: INITIAL_CURSOR, value = items) txHistoryItemsStore.store(key = customerWalletAddress, cursor = cursor ?: INITIAL_CURSOR, value = items)
}.onLeft { error(it.toString()) } }
} }
} }

View file

@ -21,11 +21,11 @@ import com.tangem.domain.visa.model.TangemPayAuthTokens
import com.tangem.domain.visa.model.getAuthHeader import com.tangem.domain.visa.model.getAuthHeader
import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import timber.log.Timber import timber.log.Timber
import java.util.concurrent.ConcurrentHashMap
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton import javax.inject.Singleton
@ -40,7 +40,7 @@ internal class TangemPayRequestPerformer @Inject constructor(
private val tangemPayStorage: TangemPayStorage, private val tangemPayStorage: TangemPayStorage,
) { ) {
private val customerWalletAddress = MutableStateFlow<String?>(null) private val customerWalletAddresses = ConcurrentHashMap<UserWalletId, String>()
private val tokensMutex = Mutex() private val tokensMutex = Mutex()
private val errorConverter = TangemPayErrorConverter(moshi) private val errorConverter = TangemPayErrorConverter(moshi)
@ -110,7 +110,7 @@ internal class TangemPayRequestPerformer @Inject constructor(
} }
suspend fun getCustomerWalletAddress(userWalletId: UserWalletId): String { suspend fun getCustomerWalletAddress(userWalletId: UserWalletId): String {
val existingAddress = customerWalletAddress.value val existingAddress = customerWalletAddresses[userWalletId]
if (existingAddress != null) { if (existingAddress != null) {
return existingAddress return existingAddress
} }
@ -118,7 +118,7 @@ internal class TangemPayRequestPerformer @Inject constructor(
userWalletId = userWalletId, userWalletId = userWalletId,
) ?: error("Can not find customer address") ) ?: error("Can not find customer address")
customerWalletAddress.value = storedAddress customerWalletAddresses[userWalletId] = storedAddress
return storedAddress return storedAddress
} }

View file

@ -15,12 +15,14 @@ class TangemPayWalletsManager @Inject constructor(
private val hotWalletFeatureToggles: HotWalletFeatureToggles, private val hotWalletFeatureToggles: HotWalletFeatureToggles,
) { ) {
@Deprecated("Don't use and put userWallet in features that need it")
suspend fun getDefaultWalletForTangemPay(): UserWallet.Cold { suspend fun getDefaultWalletForTangemPay(): UserWallet.Cold {
val userWalletsFlow = if (useNewRepository()) repository.userWallets else manager.userWallets val userWalletsFlow = if (useNewRepository()) repository.userWallets else manager.userWallets
val userWallets = userWalletsFlow.filter { !it.isNullOrEmpty() }.first() val userWallets = userWalletsFlow.filter { !it.isNullOrEmpty() }.first()
return findColdWallet(userWallets) return findColdWallet(userWallets)
} }
@Deprecated("Don't use and put userWallet in features that need it")
fun getDefaultWalletForTangemPayBlocking(): UserWallet.Cold { fun getDefaultWalletForTangemPayBlocking(): UserWallet.Cold {
val userWallets = if (useNewRepository()) repository.userWallets.value else manager.userWalletsSync val userWallets = if (useNewRepository()) repository.userWallets.value else manager.userWalletsSync
return findColdWallet(userWallets) return findColdWallet(userWallets)
@ -29,7 +31,8 @@ class TangemPayWalletsManager @Inject constructor(
private fun useNewRepository(): Boolean = hotWalletFeatureToggles.isHotWalletEnabled private fun useNewRepository(): Boolean = hotWalletFeatureToggles.isHotWalletEnabled
private fun findColdWallet(userWallets: List<UserWallet>?): UserWallet.Cold { private fun findColdWallet(userWallets: List<UserWallet>?): UserWallet.Cold {
return userWallets?.find { it is UserWallet.Cold } as? UserWallet.Cold return userWallets?.find {
?: error("Cannot find cold user wallet") it is UserWallet.Cold && it.isMultiCurrency
} as? UserWallet.Cold ?: error("Cannot find cold user wallet")
} }
} }

View file

@ -4,12 +4,14 @@ import arrow.core.Either
import com.tangem.core.error.UniversalError import com.tangem.core.error.UniversalError
import com.tangem.domain.models.ReceiveAddressModel import com.tangem.domain.models.ReceiveAddressModel
import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.models.wallet.UserWalletId
import java.math.BigDecimal import java.math.BigDecimal
interface TangemPaySwapDataFactory { interface TangemPaySwapDataFactory {
fun create( fun create(
userWallet: UserWallet,
depositAddress: String, depositAddress: String,
chainId: Int, chainId: Int,
cryptoBalance: BigDecimal, cryptoBalance: BigDecimal,

View file

@ -3,6 +3,11 @@ package com.tangem.domain.pay.model
import com.tangem.domain.visa.model.TangemPayCardFrozenState import com.tangem.domain.visa.model.TangemPayCardFrozenState
import java.math.BigDecimal import java.math.BigDecimal
sealed class MainCustomerInfoContentState {
object Loading : MainCustomerInfoContentState()
data class Content(val info: MainScreenCustomerInfo) : MainCustomerInfoContentState()
}
data class MainScreenCustomerInfo( data class MainScreenCustomerInfo(
val info: CustomerInfo, val info: CustomerInfo,
val orderStatus: OrderStatus, val orderStatus: OrderStatus,

View file

@ -2,16 +2,13 @@ package com.tangem.domain.pay.usecase
import arrow.core.Either import arrow.core.Either
import arrow.core.left import arrow.core.left
import arrow.core.raise.catch
import arrow.core.right import arrow.core.right
import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.pay.model.CustomerInfo import com.tangem.domain.pay.model.*
import com.tangem.domain.pay.model.MainScreenCustomerInfo
import com.tangem.domain.pay.model.OrderStatus
import com.tangem.domain.pay.model.TangemPayCustomerInfoError
import com.tangem.domain.pay.repository.CustomerOrderRepository import com.tangem.domain.pay.repository.CustomerOrderRepository
import com.tangem.domain.pay.repository.OnboardingRepository import com.tangem.domain.pay.repository.OnboardingRepository
import com.tangem.domain.visa.error.VisaApiError import com.tangem.domain.visa.error.VisaApiError
import kotlinx.coroutines.flow.*
import timber.log.Timber import timber.log.Timber
private const val TAG = "TangemPayMainScreenCustomerInfoUseCase" private const val TAG = "TangemPayMainScreenCustomerInfoUseCase"
@ -26,32 +23,50 @@ class TangemPayMainScreenCustomerInfoUseCase(
private val tangemPayOnboardingRepository: OnboardingRepository, private val tangemPayOnboardingRepository: OnboardingRepository,
) { ) {
suspend operator fun invoke( val state: StateFlow<Map<UserWalletId, Either<TangemPayCustomerInfoError, MainCustomerInfoContentState>>>
userWalletId: UserWalletId, field = MutableStateFlow(value = mapOf())
): Either<TangemPayCustomerInfoError, MainScreenCustomerInfo> = catch(
block = { suspend fun fetch(userWalletId: UserWalletId) {
Timber.tag(TAG).d("checkCustomerWallet") Timber.tag(TAG).i("fetch: $userWalletId")
repository.checkCustomerWallet(userWalletId) repository.checkCustomerWallet(userWalletId)
.fold( .fold(
ifLeft = { error -> ifLeft = { error ->
Timber.tag(TAG).e("Failed to check customer wallet ${error.javaClass.simpleName}") Timber.tag(TAG).e("Failed checkCustomerWallet for $userWalletId: ${error.javaClass.simpleName}")
TangemPayCustomerInfoError.UnknownError.left() updateState(userWalletId, TangemPayCustomerInfoError.UnknownError.left())
}, },
ifRight = { hasTangemPay -> ifRight = { hasTangemPay ->
Timber.tag(TAG).i("checkCustomerWallet $hasTangemPay") Timber.tag(TAG).i("checkCustomerWallet for $userWalletId: $hasTangemPay")
if (hasTangemPay) { if (hasTangemPay) {
proceedWithPaeraCustomerResult(userWalletId) val oldResult = state.value[userWalletId]
} else { if (oldResult == null) {
TangemPayCustomerInfoError.UnknownError.left() // ignore if there's no TangemPay updateState(userWalletId, MainCustomerInfoContentState.Loading.right())
} }
},
) val result = proceedWithPaeraCustomerResult(userWalletId)
}, .map(MainCustomerInfoContentState::Content)
catch = { error -> updateState(userWalletId, result)
Timber.tag(TAG).e(error) } else {
TangemPayCustomerInfoError.UnknownError.left() // ignore if there's no TangemPay
}, updateState(userWalletId, TangemPayCustomerInfoError.UnknownError.left())
) }
},
)
}
operator fun invoke(
userWalletId: UserWalletId,
): Flow<Either<TangemPayCustomerInfoError, MainCustomerInfoContentState>> {
return state.mapNotNull { map -> map[userWalletId] }
}
private fun updateState(
userWalletId: UserWalletId,
either: Either<TangemPayCustomerInfoError, MainCustomerInfoContentState>,
) {
state.update { currentMap ->
currentMap.toMutableMap().apply { this[userWalletId] = either }
}
}
private suspend fun proceedWithPaeraCustomerResult( private suspend fun proceedWithPaeraCustomerResult(
userWalletId: UserWalletId, userWalletId: UserWalletId,
@ -70,14 +85,13 @@ class TangemPayMainScreenCustomerInfoUseCase(
private suspend fun proceedWithoutOrder( private suspend fun proceedWithoutOrder(
userWalletId: UserWalletId, userWalletId: UserWalletId,
): Either<TangemPayCustomerInfoError, MainScreenCustomerInfo> { ): Either<TangemPayCustomerInfoError, MainScreenCustomerInfo> {
Timber.tag(TAG).d("proceedWithoutOrder")
return repository.getCustomerInfo(userWalletId) return repository.getCustomerInfo(userWalletId)
.mapLeft { error -> .mapLeft { error ->
Timber.tag(TAG).e("mapErrorForCustomer: $error") Timber.tag(TAG).e("mapErrorForCustomer: $error")
error.mapErrorForCustomer() error.mapErrorForCustomer()
} }
.map { customerInfo -> .map { customerInfo ->
Timber.tag(TAG).d("customerInfo") Timber.tag(TAG).i("customerInfo")
if (customerInfo.cardInfo == null && customerInfo.isKycApproved) { if (customerInfo.cardInfo == null && customerInfo.isKycApproved) {
// If order id wasn't saved -> start order creation and get customer info // If order id wasn't saved -> start order creation and get customer info
repository.createOrder(userWalletId) repository.createOrder(userWalletId)

View file

@ -7,6 +7,11 @@ import kotlinx.coroutines.flow.update
internal typealias ChangedCurrencies = Map<ManagedCryptoCurrency.Token, Set<Network>> internal typealias ChangedCurrencies = Map<ManagedCryptoCurrency.Token, Set<Network>>
internal data class CurrencyUpdates(
val toAdd: ChangedCurrencies = emptyMap(),
val toRemove: ChangedCurrencies = emptyMap(),
)
internal class ChangedCurrenciesManager { internal class ChangedCurrenciesManager {
val currenciesToAdd: MutableStateFlow<ChangedCurrencies> = MutableStateFlow(emptyMap()) val currenciesToAdd: MutableStateFlow<ChangedCurrencies> = MutableStateFlow(emptyMap())

View file

@ -329,6 +329,10 @@ internal class ManageTokensListManager @AssistedInject constructor(
val updatedUiItem = uiBatch.data[currencyIndex].toggleExpanded( val updatedUiItem = uiBatch.data[currencyIndex].toggleExpanded(
currency = currencyBatch.data[currencyIndex], currency = currencyBatch.data[currencyIndex],
isEditable = batches.canEditItems, isEditable = batches.canEditItems,
updates = CurrencyUpdates(
toAdd = currenciesToAdd.value,
toRemove = currenciesToRemove.value,
),
onSelectCurrencyNetwork = { networkId, isSelected -> onSelectCurrencyNetwork = { networkId, isSelected ->
selectNetwork(currencyBatch.key, currency, networkId, isSelected) selectNetwork(currencyBatch.key, currency, networkId, isSelected)
}, },

View file

@ -5,20 +5,28 @@ import com.tangem.domain.managetokens.model.ManagedCryptoCurrency.SourceNetwork
import com.tangem.domain.models.network.Network import com.tangem.domain.models.network.Network
import com.tangem.features.managetokens.entity.item.CurrencyItemUM.Basic.NetworksUM import com.tangem.features.managetokens.entity.item.CurrencyItemUM.Basic.NetworksUM
import com.tangem.features.managetokens.entity.item.CurrencyNetworkUM import com.tangem.features.managetokens.entity.item.CurrencyNetworkUM
import com.tangem.features.managetokens.utils.list.CurrencyUpdates
import com.tangem.features.managetokens.utils.ui.getIconRes import com.tangem.features.managetokens.utils.ui.getIconRes
import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableList
internal fun ManagedCryptoCurrency.Token.toUiNetworksModel( internal fun ManagedCryptoCurrency.Token.toUiNetworksModel(
isExpanded: Boolean, isExpanded: Boolean,
isItemsEditable: Boolean, isItemsEditable: Boolean,
updates: CurrencyUpdates,
onSelectedStateChange: (SourceNetwork, Boolean) -> Unit, onSelectedStateChange: (SourceNetwork, Boolean) -> Unit,
onLongTap: (SourceNetwork) -> Unit, onLongTap: (SourceNetwork) -> Unit,
): NetworksUM { ): NetworksUM {
return if (isExpanded) { return if (isExpanded) {
NetworksUM.Expanded( NetworksUM.Expanded(
networks = availableNetworks.map { networks = availableNetworks.map { sourceNetwork ->
it.toCurrencyNetworkModel( val isSelectedByDefault = sourceNetwork.network in addedIn
isSelected = it.network in addedIn, val isSelected = when (sourceNetwork.network) {
in updates.toAdd[this].orEmpty() -> true
in updates.toRemove[this].orEmpty() -> false
else -> isSelectedByDefault
}
sourceNetwork.toCurrencyNetworkModel(
isSelected = isSelected,
isEditable = isItemsEditable, isEditable = isItemsEditable,
onSelectedStateChange = onSelectedStateChange, onSelectedStateChange = onSelectedStateChange,
onLongTap = onLongTap, onLongTap = onLongTap,

View file

@ -4,12 +4,14 @@ import com.tangem.domain.managetokens.model.ManagedCryptoCurrency
import com.tangem.domain.managetokens.model.ManagedCryptoCurrency.SourceNetwork import com.tangem.domain.managetokens.model.ManagedCryptoCurrency.SourceNetwork
import com.tangem.features.managetokens.entity.item.CurrencyItemUM import com.tangem.features.managetokens.entity.item.CurrencyItemUM
import com.tangem.features.managetokens.entity.item.CurrencyItemUM.Basic.NetworksUM import com.tangem.features.managetokens.entity.item.CurrencyItemUM.Basic.NetworksUM
import com.tangem.features.managetokens.utils.list.CurrencyUpdates
import com.tangem.features.managetokens.utils.mapper.toUiNetworksModel import com.tangem.features.managetokens.utils.mapper.toUiNetworksModel
import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableList
internal fun CurrencyItemUM.toggleExpanded( internal fun CurrencyItemUM.toggleExpanded(
currency: ManagedCryptoCurrency, currency: ManagedCryptoCurrency,
isEditable: Boolean, isEditable: Boolean,
updates: CurrencyUpdates,
onSelectCurrencyNetwork: (SourceNetwork, Boolean) -> Unit, onSelectCurrencyNetwork: (SourceNetwork, Boolean) -> Unit,
onLongTap: (SourceNetwork) -> Unit, onLongTap: (SourceNetwork) -> Unit,
): CurrencyItemUM { ): CurrencyItemUM {
@ -30,6 +32,7 @@ internal fun CurrencyItemUM.toggleExpanded(
networks = currency.toUiNetworksModel( networks = currency.toUiNetworksModel(
isExpanded = isExpanded, isExpanded = isExpanded,
isItemsEditable = isEditable, isItemsEditable = isEditable,
updates = updates,
onSelectedStateChange = onSelectCurrencyNetwork, onSelectedStateChange = onSelectCurrencyNetwork,
onLongTap = onLongTap, onLongTap = onLongTap,
), ),

View file

@ -29,13 +29,13 @@ internal class OnrampSettingsModel @Inject constructor(
paramsContainer: ParamsContainer, paramsContainer: ParamsContainer,
) : Model() { ) : Model() {
private val params: OnrampSettingsComponent.Params = paramsContainer.require()
val state: StateFlow<OnrampSettingsUM> val state: StateFlow<OnrampSettingsUM>
field = MutableStateFlow(getInitialState()) field = MutableStateFlow(getInitialState())
val bottomSheetNavigation: SlotNavigation<OnrampSettingsConfig> = SlotNavigation() val bottomSheetNavigation: SlotNavigation<OnrampSettingsConfig> = SlotNavigation()
private val params: OnrampSettingsComponent.Params = paramsContainer.require()
init { init {
analyticsEventHandler.send(OnrampAnalyticsEvent.SettingsOpened) analyticsEventHandler.send(OnrampAnalyticsEvent.SettingsOpened)
subscribeOnUpdateState() subscribeOnUpdateState()

View file

@ -84,6 +84,7 @@ internal class TangemPayDetailsComponent(
appComponentContext = context, appComponentContext = context,
params = TangemPayTxHistoryDetailsComponent.Params( params = TangemPayTxHistoryDetailsComponent.Params(
transaction = navigation.transaction, transaction = navigation.transaction,
isBalanceHidden = navigation.isBalanceHidden,
userWalletId = params.userWalletId, userWalletId = params.userWalletId,
onDismiss = model.bottomSheetNavigation::dismiss, onDismiss = model.bottomSheetNavigation::dismiss,
), ),

View file

@ -1,10 +1,11 @@
package com.tangem.features.tangempay.components.txHistory package com.tangem.features.tangempay.components.txHistory
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.model.getOrCreateModel import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.core.ui.decompose.ComposableBottomSheetComponent import com.tangem.core.ui.decompose.ComposableBottomSheetComponent
import com.tangem.core.ui.extensions.*
import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.visa.model.TangemPayTxHistoryItem import com.tangem.domain.visa.model.TangemPayTxHistoryItem
import com.tangem.features.tangempay.model.TangemPayTxHistoryDetailsModel import com.tangem.features.tangempay.model.TangemPayTxHistoryDetailsModel
@ -23,11 +24,13 @@ internal class TangemPayTxHistoryDetailsComponent(
@Composable @Composable
override fun BottomSheet() { override fun BottomSheet() {
TangemPayTxHistoryDetailsContent(state = model.uiState) val state by model.uiState.collectAsStateWithLifecycle()
TangemPayTxHistoryDetailsContent(state = state)
} }
data class Params( data class Params(
val transaction: TangemPayTxHistoryItem, val transaction: TangemPayTxHistoryItem,
val isBalanceHidden: Boolean,
val userWalletId: UserWalletId, val userWalletId: UserWalletId,
val onDismiss: () -> Unit, val onDismiss: () -> Unit,
) )

View file

@ -22,5 +22,8 @@ internal sealed class TangemPayDetailsNavigation {
) : TangemPayDetailsNavigation() ) : TangemPayDetailsNavigation()
@Serializable @Serializable
data class TransactionDetails(val transaction: TangemPayTxHistoryItem) : TangemPayDetailsNavigation() data class TransactionDetails(
val transaction: TangemPayTxHistoryItem,
val isBalanceHidden: Boolean,
) : TangemPayDetailsNavigation()
} }

View file

@ -41,7 +41,6 @@ internal class TangemPayDetailsStateFactory(
), ),
) )
} }
return TangemPayDetailsUM( return TangemPayDetailsUM(
topBarConfig = TangemPayDetailsTopBarConfig( topBarConfig = TangemPayDetailsTopBarConfig(
onBackClick = onBack, onBackClick = onBack,

View file

@ -8,6 +8,7 @@ import com.tangem.core.ui.extensions.TextReference
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
internal data class TangemPayTxHistoryDetailsUM( internal data class TangemPayTxHistoryDetailsUM(
val isBalanceHidden: Boolean,
val title: TextReference, val title: TextReference,
val iconState: ImageReference, val iconState: ImageReference,
val transactionTitle: TextReference, val transactionTitle: TextReference,

View file

@ -5,6 +5,7 @@ import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.domain.pay.TangemPaySwapDataFactory import com.tangem.domain.pay.TangemPaySwapDataFactory
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
import com.tangem.features.tangempay.components.TangemPayAddFundsComponent import com.tangem.features.tangempay.components.TangemPayAddFundsComponent
import com.tangem.features.tangempay.entity.TangemPayAddFundsUM import com.tangem.features.tangempay.entity.TangemPayAddFundsUM
import com.tangem.features.tangempay.model.transformers.TangemPayAddFundsUMConverter import com.tangem.features.tangempay.model.transformers.TangemPayAddFundsUMConverter
@ -17,6 +18,7 @@ internal class TangemPayAddFundsModel @Inject constructor(
paramsContainer: ParamsContainer, paramsContainer: ParamsContainer,
override val dispatchers: CoroutineDispatcherProvider, override val dispatchers: CoroutineDispatcherProvider,
private val tangemPaySwapDataFactory: TangemPaySwapDataFactory, private val tangemPaySwapDataFactory: TangemPaySwapDataFactory,
private val getUserWalletUseCase: GetUserWalletUseCase,
) : Model() { ) : Model() {
private val params = paramsContainer.require<TangemPayAddFundsComponent.Params>() private val params = paramsContainer.require<TangemPayAddFundsComponent.Params>()
@ -24,7 +26,11 @@ internal class TangemPayAddFundsModel @Inject constructor(
val uiState: TangemPayAddFundsUM = getInitialState() val uiState: TangemPayAddFundsUM = getInitialState()
private fun getInitialState(): TangemPayAddFundsUM { private fun getInitialState(): TangemPayAddFundsUM {
val userWallet = requireNotNull(
getUserWalletUseCase(params.walletId).getOrNull(),
) { "User wallet not found for id: ${params.walletId}" }
val data = tangemPaySwapDataFactory.create( val data = tangemPaySwapDataFactory.create(
userWallet = userWallet,
depositAddress = params.depositAddress, depositAddress = params.depositAddress,
chainId = params.chainId, chainId = params.chainId,
cryptoBalance = params.cryptoBalance, cryptoBalance = params.cryptoBalance,

View file

@ -18,14 +18,15 @@ import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.message.SnackbarMessage import com.tangem.core.ui.message.SnackbarMessage
import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase
import com.tangem.domain.models.TokenReceiveConfig import com.tangem.domain.models.TokenReceiveConfig
import com.tangem.domain.pay.TangemPayTopUpData
import com.tangem.domain.pay.TangemPaySwapDataFactory import com.tangem.domain.pay.TangemPaySwapDataFactory
import com.tangem.domain.pay.TangemPayTopUpData
import com.tangem.domain.pay.model.TangemPayCardBalance import com.tangem.domain.pay.model.TangemPayCardBalance
import com.tangem.domain.pay.repository.CustomerOrderRepository import com.tangem.domain.pay.repository.CustomerOrderRepository
import com.tangem.domain.pay.repository.TangemPayCardDetailsRepository import com.tangem.domain.pay.repository.TangemPayCardDetailsRepository
import com.tangem.domain.tangempay.TangemPayAnalyticsEvents import com.tangem.domain.tangempay.TangemPayAnalyticsEvents
import com.tangem.domain.visa.model.TangemPayCardFrozenState import com.tangem.domain.visa.model.TangemPayCardFrozenState
import com.tangem.domain.visa.model.TangemPayTxHistoryItem import com.tangem.domain.visa.model.TangemPayTxHistoryItem
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
import com.tangem.features.tangempay.TangemPayConstants import com.tangem.features.tangempay.TangemPayConstants
import com.tangem.features.tangempay.components.AddFundsListener import com.tangem.features.tangempay.components.AddFundsListener
import com.tangem.features.tangempay.components.TangemPayDetailsContainerComponent import com.tangem.features.tangempay.components.TangemPayDetailsContainerComponent
@ -55,7 +56,7 @@ import kotlinx.coroutines.launch
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
@Suppress("LongParameterList") @Suppress("LongParameterList", "LargeClass")
@Stable @Stable
@ModelScoped @ModelScoped
internal class TangemPayDetailsModel @Inject constructor( internal class TangemPayDetailsModel @Inject constructor(
@ -71,6 +72,7 @@ internal class TangemPayDetailsModel @Inject constructor(
private val txHistoryUpdateListener: TangemPayTxHistoryUpdateListener, private val txHistoryUpdateListener: TangemPayTxHistoryUpdateListener,
private val tangemPaySwapDataFactory: TangemPaySwapDataFactory, private val tangemPaySwapDataFactory: TangemPaySwapDataFactory,
private val orderRepository: CustomerOrderRepository, private val orderRepository: CustomerOrderRepository,
private val getUserWalletUseCase: GetUserWalletUseCase,
) : Model(), TangemPayTxHistoryUiActions, TangemPayDetailIntents, AddFundsListener { ) : Model(), TangemPayTxHistoryUiActions, TangemPayDetailIntents, AddFundsListener {
private val params: TangemPayDetailsContainerComponent.Params = paramsContainer.require() private val params: TangemPayDetailsContainerComponent.Params = paramsContainer.require()
@ -234,7 +236,11 @@ internal class TangemPayDetailsModel @Inject constructor(
if (hasActiveWithdrawal) { if (hasActiveWithdrawal) {
showBottomSheetError(TangemPayDetailsErrorType.WithdrawInProgress) showBottomSheetError(TangemPayDetailsErrorType.WithdrawInProgress)
} else { } else {
val userWallet = requireNotNull(
getUserWalletUseCase(params.userWalletId).getOrNull(),
) { "User wallet not found: ${params.userWalletId}" }
val data = tangemPaySwapDataFactory.create( val data = tangemPaySwapDataFactory.create(
userWallet = userWallet,
depositAddress = depositAddress, depositAddress = depositAddress,
chainId = params.config.chainId, chainId = params.config.chainId,
cryptoBalance = currentBalance.cryptoBalance, cryptoBalance = currentBalance.cryptoBalance,
@ -367,7 +373,12 @@ internal class TangemPayDetailsModel @Inject constructor(
} }
override fun onTransactionClick(item: TangemPayTxHistoryItem) { override fun onTransactionClick(item: TangemPayTxHistoryItem) {
bottomSheetNavigation.activate(TangemPayDetailsNavigation.TransactionDetails(item)) bottomSheetNavigation.activate(
configuration = TangemPayDetailsNavigation.TransactionDetails(
transaction = item,
isBalanceHidden = uiState.value.isBalanceHidden,
),
)
} }
override fun onClickTermsAndLimits() { override fun onClickTermsAndLimits() {

View file

@ -5,6 +5,7 @@ import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.core.navigation.url.UrlOpener import com.tangem.core.navigation.url.UrlOpener
import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase
import com.tangem.domain.feedback.GetWalletMetaInfoUseCase import com.tangem.domain.feedback.GetWalletMetaInfoUseCase
import com.tangem.domain.feedback.SendFeedbackEmailUseCase import com.tangem.domain.feedback.SendFeedbackEmailUseCase
import com.tangem.domain.feedback.models.FeedbackEmailType import com.tangem.domain.feedback.models.FeedbackEmailType
@ -14,39 +15,56 @@ import com.tangem.features.tangempay.components.txHistory.TangemPayTxHistoryDeta
import com.tangem.features.tangempay.entity.TangemPayTxHistoryDetailsUM import com.tangem.features.tangempay.entity.TangemPayTxHistoryDetailsUM
import com.tangem.features.tangempay.model.transformers.TangemPayTxHistoryDetailsConverter import com.tangem.features.tangempay.model.transformers.TangemPayTxHistoryDetailsConverter
import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import javax.inject.Inject import javax.inject.Inject
@Stable @Stable
@ModelScoped @ModelScoped
@Suppress("LongParameterList")
internal class TangemPayTxHistoryDetailsModel @Inject constructor( internal class TangemPayTxHistoryDetailsModel @Inject constructor(
override val dispatchers: CoroutineDispatcherProvider, override val dispatchers: CoroutineDispatcherProvider,
private val getUserWalletsUseCase: GetWalletsUseCase, private val getUserWalletsUseCase: GetWalletsUseCase,
private val getWalletMetaInfoUseCase: GetWalletMetaInfoUseCase, private val getWalletMetaInfoUseCase: GetWalletMetaInfoUseCase,
private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase, private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase,
private val urlOpener: UrlOpener, private val urlOpener: UrlOpener,
private val balanceHidingSettings: GetBalanceHidingSettingsUseCase,
paramsContainer: ParamsContainer, paramsContainer: ParamsContainer,
) : Model() { ) : Model() {
private val params = paramsContainer.require<TangemPayTxHistoryDetailsComponent.Params>() private val params = paramsContainer.require<TangemPayTxHistoryDetailsComponent.Params>()
val uiState: TangemPayTxHistoryDetailsUM = TangemPayTxHistoryDetailsConverter.convert( val uiState: StateFlow<TangemPayTxHistoryDetailsUM>
TangemPayTxHistoryDetailsConverter.Input( field = MutableStateFlow(
item = params.transaction, value = TangemPayTxHistoryDetailsConverter.convert(
onExplorerClick = ::openExplorer, value = TangemPayTxHistoryDetailsConverter.Input(
onDisputeClick = ::dispute, item = params.transaction,
onDismiss = ::dismiss, isBalanceHidden = params.isBalanceHidden,
), onExplorerClick = ::openExplorer,
) onDisputeClick = ::dispute,
onDismiss = ::dismiss,
),
),
)
init {
subscribeToBalanceHiding()
}
fun dismiss() { fun dismiss() {
params.onDismiss() params.onDismiss()
} }
fun openExplorer(txHash: String?) { private fun subscribeToBalanceHiding() {
balanceHidingSettings.isBalanceHidden()
.onEach { isBalanceHidden -> uiState.update { it.copy(isBalanceHidden = isBalanceHidden) } }
.launchIn(modelScope)
}
private fun openExplorer(txHash: String?) {
txHash?.let(urlOpener::openUrlExternalBrowser) txHash?.let(urlOpener::openUrlExternalBrowser)
} }
fun dispute() { private fun dispute() {
modelScope.launch { modelScope.launch {
val userWalletId = params.userWalletId val userWalletId = params.userWalletId
val userWallet = getUserWalletsUseCase.invokeSync() val userWallet = getUserWalletsUseCase.invokeSync()

View file

@ -6,6 +6,7 @@ import com.tangem.core.ui.extensions.themedColor
import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemTheme
import com.tangem.domain.visa.model.TangemPayCardFrozenState import com.tangem.domain.visa.model.TangemPayCardFrozenState
import com.tangem.features.tangempay.details.impl.R import com.tangem.features.tangempay.details.impl.R
import com.tangem.features.tangempay.entity.TangemPayDetailsBalanceBlockState
import com.tangem.features.tangempay.entity.TangemPayDetailsTopBarMenuItem import com.tangem.features.tangempay.entity.TangemPayDetailsTopBarMenuItem
import com.tangem.features.tangempay.entity.TangemPayDetailsTopBarMenuItemType.FreezeCard import com.tangem.features.tangempay.entity.TangemPayDetailsTopBarMenuItemType.FreezeCard
import com.tangem.features.tangempay.entity.TangemPayDetailsTopBarMenuItemType.UnfreezeCard import com.tangem.features.tangempay.entity.TangemPayDetailsTopBarMenuItemType.UnfreezeCard
@ -31,9 +32,20 @@ internal class TangemPayFreezeUnfreezeStateTransformer(
it.type == FreezeCard || it.type == UnfreezeCard it.type == FreezeCard || it.type == UnfreezeCard
} }
val dropdownMenuItems = createUpdatedMenuItems(filteredItems?.toPersistentList()) val dropdownMenuItems = createUpdatedMenuItems(filteredItems?.toPersistentList())
val balanceBlockState = if (prevState.balanceBlockState is TangemPayDetailsBalanceBlockState.Content) {
val actionButtons = prevState.balanceBlockState.actionButtons.map {
it.copy(isEnabled = cardFrozenState == TangemPayCardFrozenState.Unfrozen)
}
prevState.balanceBlockState.copy(
actionButtons = actionButtons.toPersistentList(),
)
} else {
prevState.balanceBlockState
}
return prevState.copy( return prevState.copy(
topBarConfig = prevState.topBarConfig.copy(items = dropdownMenuItems), topBarConfig = prevState.topBarConfig.copy(items = dropdownMenuItems),
cardFrozenState = converter.convert(cardFrozenState), cardFrozenState = converter.convert(cardFrozenState),
balanceBlockState = balanceBlockState,
) )
} }

View file

@ -29,6 +29,7 @@ internal object TangemPayTxHistoryDetailsConverter :
override fun convert(value: Input): TangemPayTxHistoryDetailsUM { override fun convert(value: Input): TangemPayTxHistoryDetailsUM {
val transaction = value.item val transaction = value.item
return TangemPayTxHistoryDetailsUM( return TangemPayTxHistoryDetailsUM(
isBalanceHidden = value.isBalanceHidden,
title = transaction.extractDate(), title = transaction.extractDate(),
iconState = transaction.extractIcon(), iconState = transaction.extractIcon(),
transactionTitle = transaction.extractTransactionTitle(), transactionTitle = transaction.extractTransactionTitle(),
@ -254,6 +255,7 @@ internal object TangemPayTxHistoryDetailsConverter :
data class Input( data class Input(
val item: TangemPayTxHistoryItem, val item: TangemPayTxHistoryItem,
val isBalanceHidden: Boolean,
val onExplorerClick: (String?) -> Unit, val onExplorerClick: (String?) -> Unit,
val onDisputeClick: () -> Unit, val onDisputeClick: () -> Unit,
val onDismiss: () -> Unit, val onDismiss: () -> Unit,

View file

@ -99,19 +99,6 @@ internal fun TangemPayDetailsScreen(
} }
else -> Unit else -> Unit
} }
item(
key = TangemPayDetailsBalanceBlockState::class.java,
content = {
TangemPayDetailsBalanceBlock(
modifier = modifier
.padding(horizontal = TangemTheme.dimens.spacing16)
.padding(top = 12.dp)
.fillMaxWidth(),
state = state.balanceBlockState,
isBalanceHidden = state.isBalanceHidden,
)
},
)
if (state.addToWalletBlockState != null) { if (state.addToWalletBlockState != null) {
item( item(
key = AddToWalletBlockState::class.java, key = AddToWalletBlockState::class.java,
@ -125,6 +112,19 @@ internal fun TangemPayDetailsScreen(
}, },
) )
} }
item(
key = TangemPayDetailsBalanceBlockState::class.java,
content = {
TangemPayDetailsBalanceBlock(
modifier = modifier
.padding(horizontal = TangemTheme.dimens.spacing16)
.padding(top = 12.dp)
.fillMaxWidth(),
state = state.balanceBlockState,
isBalanceHidden = state.isBalanceHidden,
)
},
)
with(txHistoryComponent) { txHistoryContent(listState = listState, state = txHistoryState) } with(txHistoryComponent) { txHistoryContent(listState = listState, state = txHistoryState) }
} }
} }

View file

@ -77,7 +77,7 @@ internal fun TangemPayTxHistoryDetailsContent(state: TangemPayTxHistoryDetailsUM
) )
Text( Text(
modifier = Modifier.padding(top = 8.dp), modifier = Modifier.padding(top = 8.dp),
text = state.transactionAmount, text = state.transactionAmount.orMaskWithStars(state.isBalanceHidden),
style = TangemTheme.typography.head, style = TangemTheme.typography.head,
color = state.transactionAmountColor.resolveReference(), color = state.transactionAmountColor.resolveReference(),
) )
@ -158,6 +158,7 @@ private fun TangemPayTxHistoryDetailsContentPreview(
private class TangemPayTxHistoryDetailsUMProvider : CollectionPreviewParameterProvider<TangemPayTxHistoryDetailsUM>( private class TangemPayTxHistoryDetailsUMProvider : CollectionPreviewParameterProvider<TangemPayTxHistoryDetailsUM>(
listOf( listOf(
TangemPayTxHistoryDetailsUM( TangemPayTxHistoryDetailsUM(
isBalanceHidden = true,
title = stringReference("12 June • 12:40"), title = stringReference("12 June • 12:40"),
iconState = ImageReference.Res(R.drawable.ic_category_24), iconState = ImageReference.Res(R.drawable.ic_category_24),
transactionTitle = stringReference("Starbucks"), transactionTitle = stringReference("Starbucks"),
@ -180,6 +181,7 @@ private class TangemPayTxHistoryDetailsUMProvider : CollectionPreviewParameterPr
dismiss = {}, dismiss = {},
), ),
TangemPayTxHistoryDetailsUM( TangemPayTxHistoryDetailsUM(
isBalanceHidden = true,
title = stringReference("12 June • 12:40"), title = stringReference("12 June • 12:40"),
iconState = ImageReference.Res(R.drawable.ic_category_24), iconState = ImageReference.Res(R.drawable.ic_category_24),
transactionTitle = stringReference("Starbucks"), transactionTitle = stringReference("Starbucks"),
@ -205,6 +207,7 @@ private class TangemPayTxHistoryDetailsUMProvider : CollectionPreviewParameterPr
dismiss = {}, dismiss = {},
), ),
TangemPayTxHistoryDetailsUM( TangemPayTxHistoryDetailsUM(
isBalanceHidden = false,
title = stringReference("12 June • 12:40"), title = stringReference("12 June • 12:40"),
iconState = ImageReference.Res(R.drawable.ic_category_24), iconState = ImageReference.Res(R.drawable.ic_category_24),
transactionTitle = stringReference("Starbucks"), transactionTitle = stringReference("Starbucks"),
@ -226,6 +229,7 @@ private class TangemPayTxHistoryDetailsUMProvider : CollectionPreviewParameterPr
dismiss = {}, dismiss = {},
), ),
TangemPayTxHistoryDetailsUM( TangemPayTxHistoryDetailsUM(
isBalanceHidden = false,
title = stringReference("12 June • 12:40"), title = stringReference("12 June • 12:40"),
iconState = ImageReference.Res(R.drawable.ic_percent_24), iconState = ImageReference.Res(R.drawable.ic_percent_24),
transactionTitle = stringReference("Fee"), transactionTitle = stringReference("Fee"),
@ -248,6 +252,7 @@ private class TangemPayTxHistoryDetailsUMProvider : CollectionPreviewParameterPr
dismiss = {}, dismiss = {},
), ),
TangemPayTxHistoryDetailsUM( TangemPayTxHistoryDetailsUM(
isBalanceHidden = false,
title = stringReference("12 June • 12:40"), title = stringReference("12 June • 12:40"),
iconState = ImageReference.Res(R.drawable.ic_arrow_down_24), iconState = ImageReference.Res(R.drawable.ic_arrow_down_24),
transactionTitle = stringReference("Deposit"), transactionTitle = stringReference("Deposit"),
@ -266,6 +271,7 @@ private class TangemPayTxHistoryDetailsUMProvider : CollectionPreviewParameterPr
dismiss = {}, dismiss = {},
), ),
TangemPayTxHistoryDetailsUM( TangemPayTxHistoryDetailsUM(
isBalanceHidden = false,
title = stringReference("12 June • 12:40"), title = stringReference("12 June • 12:40"),
iconState = ImageReference.Res(R.drawable.ic_arrow_up_24), iconState = ImageReference.Res(R.drawable.ic_arrow_up_24),
transactionTitle = stringReference("Withdrawal"), transactionTitle = stringReference("Withdrawal"),

View file

@ -13,6 +13,9 @@ dependencies {
implementation(projects.core.decompose) implementation(projects.core.decompose)
implementation(projects.core.ui) implementation(projects.core.ui)
/** Domain */
implementation(projects.domain.models)
/** Compose */ /** Compose */
implementation(deps.compose.runtime) implementation(deps.compose.runtime)
} }

View file

@ -2,15 +2,22 @@ package com.tangem.features.tangempay.components
import com.tangem.core.decompose.factory.ComponentFactory import com.tangem.core.decompose.factory.ComponentFactory
import com.tangem.core.ui.decompose.ComposableContentComponent import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.domain.models.wallet.UserWalletId
interface TangemPayOnboardingComponent : ComposableContentComponent { interface TangemPayOnboardingComponent : ComposableContentComponent {
sealed class Params { sealed class Params {
abstract val userWalletId: UserWalletId?
data class Deeplink( data class Deeplink(
val deeplink: String, val deeplink: String,
override val userWalletId: UserWalletId?,
) : Params() ) : Params()
object ContinueOnboarding : Params() data class ContinueOnboarding(
override val userWalletId: UserWalletId?,
) : Params()
} }
interface Factory : ComponentFactory<Params, TangemPayOnboardingComponent> interface Factory : ComponentFactory<Params, TangemPayOnboardingComponent>

View file

@ -30,6 +30,7 @@ dependencies {
/** Domain */ /** Domain */
implementation(projects.domain.visa) implementation(projects.domain.visa)
implementation(projects.domain.wallets)
/** Data **/ /** Data **/
implementation(projects.data.visa) implementation(projects.data.visa)

View file

@ -1,10 +1,11 @@
package com.tangem.features.tangempay.deeplink package com.tangem.features.tangempay.deeplink
import android.net.Uri import android.net.Uri
import dagger.assisted.Assisted
import com.tangem.common.routing.AppRoute import com.tangem.common.routing.AppRoute
import com.tangem.features.tangempay.TangemPayFeatureToggles
import com.tangem.common.routing.AppRouter import com.tangem.common.routing.AppRouter
import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase
import com.tangem.features.tangempay.TangemPayFeatureToggles
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject import dagger.assisted.AssistedInject
@ -12,11 +13,16 @@ internal class DefaultOnboardVisaDeepLinkHandler @AssistedInject constructor(
@Assisted uri: Uri, @Assisted uri: Uri,
appRouter: AppRouter, appRouter: AppRouter,
tangemPayFeatureToggles: TangemPayFeatureToggles, tangemPayFeatureToggles: TangemPayFeatureToggles,
getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase,
) : OnboardVisaDeepLinkHandler { ) : OnboardVisaDeepLinkHandler {
init { init {
if (tangemPayFeatureToggles.isTangemPayEnabled) { if (tangemPayFeatureToggles.isTangemPayEnabled) {
val mode = AppRoute.TangemPayOnboarding.Mode.Deeplink(uri.toString()) val userWallet = getSelectedWalletSyncUseCase.invoke().getOrNull()
val mode = AppRoute.TangemPayOnboarding.Mode.Deeplink(
deeplink = uri.toString(),
userWalletId = userWallet?.walletId,
)
appRouter.push(AppRoute.TangemPayOnboarding(mode)) appRouter.push(AppRoute.TangemPayOnboarding(mode))
} else { } else {
appRouter.push(AppRoute.Home()) appRouter.push(AppRoute.Home())

View file

@ -9,9 +9,13 @@ import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.core.decompose.navigation.Router import com.tangem.core.decompose.navigation.Router
import com.tangem.core.navigation.url.UrlOpener import com.tangem.core.navigation.url.UrlOpener
import com.tangem.data.pay.util.TangemPayWalletsManager import com.tangem.data.pay.util.TangemPayWalletsManager
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.models.wallet.isMultiCurrency
import com.tangem.domain.pay.repository.OnboardingRepository import com.tangem.domain.pay.repository.OnboardingRepository
import com.tangem.domain.pay.usecase.ProduceTangemPayInitialDataUseCase import com.tangem.domain.pay.usecase.ProduceTangemPayInitialDataUseCase
import com.tangem.domain.tangempay.TangemPayAnalyticsEvents import com.tangem.domain.tangempay.TangemPayAnalyticsEvents
import com.tangem.domain.wallets.usecase.GetSelectedWalletUseCase
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
import com.tangem.features.tangempay.TangemPayConstants import com.tangem.features.tangempay.TangemPayConstants
import com.tangem.features.tangempay.components.TangemPayOnboardingComponent import com.tangem.features.tangempay.components.TangemPayOnboardingComponent
import com.tangem.features.tangempay.model.transformers.TangemPayOnboardingButtonLoadingTransformer import com.tangem.features.tangempay.model.transformers.TangemPayOnboardingButtonLoadingTransformer
@ -37,6 +41,8 @@ internal class TangemPayOnboardingModel @Inject constructor(
private val produceInitialDataUseCase: ProduceTangemPayInitialDataUseCase, private val produceInitialDataUseCase: ProduceTangemPayInitialDataUseCase,
private val urlOpener: UrlOpener, private val urlOpener: UrlOpener,
private val tangemPayWalletsManager: TangemPayWalletsManager, private val tangemPayWalletsManager: TangemPayWalletsManager,
private val getUserWalletUseCase: GetUserWalletUseCase,
private val getSelectedWalletUseCase: GetSelectedWalletUseCase,
) : Model() { ) : Model() {
private val params = paramsContainer.require<TangemPayOnboardingComponent.Params>() private val params = paramsContainer.require<TangemPayOnboardingComponent.Params>()
@ -74,8 +80,9 @@ internal class TangemPayOnboardingModel @Inject constructor(
private suspend fun checkCustomerInfo() { private suspend fun checkCustomerInfo() {
// TODO implement selector // TODO implement selector
val userWalletId = getUserWalletForPay(params.userWalletId)
repository.getCustomerInfo( repository.getCustomerInfo(
userWalletId = tangemPayWalletsManager.getDefaultWalletForTangemPayBlocking().walletId, userWalletId = userWalletId,
) )
// selector // selector
.onRight { customerInfo -> .onRight { customerInfo ->
@ -92,6 +99,24 @@ internal class TangemPayOnboardingModel @Inject constructor(
.onLeft { back() } .onLeft { back() }
} }
private fun getUserWalletForPay(userWalletId: UserWalletId?): UserWalletId {
val userWallet = userWalletId?.let { getUserWalletUseCase(it).getOrNull() }
return if (userWallet?.isMultiCurrency == true) {
userWallet.walletId
} else {
tryGetSelectedWalletId()
}
}
private fun tryGetSelectedWalletId(): UserWalletId {
val selectedWallet = getSelectedWalletUseCase.sync().getOrNull()
return if (selectedWallet?.isMultiCurrency == true) {
selectedWallet.walletId
} else {
tangemPayWalletsManager.getDefaultWalletForTangemPayBlocking().walletId
}
}
private fun onTermsClick() { private fun onTermsClick() {
analytics.send(TangemPayAnalyticsEvents.ViewTermsClicked) analytics.send(TangemPayAnalyticsEvents.ViewTermsClicked)
urlOpener.openUrl(TangemPayConstants.TERMS_AND_LIMITS_LINK) urlOpener.openUrl(TangemPayConstants.TERMS_AND_LIMITS_LINK)
@ -103,7 +128,7 @@ internal class TangemPayOnboardingModel @Inject constructor(
uiState.transformerUpdate(TangemPayOnboardingButtonLoadingTransformer(isLoading = true)) uiState.transformerUpdate(TangemPayOnboardingButtonLoadingTransformer(isLoading = true))
modelScope.launch { modelScope.launch {
// TODO implement selector // TODO implement selector
val userWalletId = tangemPayWalletsManager.getDefaultWalletForTangemPayBlocking().walletId val userWalletId = getUserWalletForPay(params.userWalletId)
val result = produceInitialDataUseCase(userWalletId) val result = produceInitialDataUseCase(userWalletId)
if (result.isLeft()) { if (result.isLeft()) {
Timber.e("Error producing initial data: ${result.leftOrNull()?.message}") Timber.e("Error producing initial data: ${result.leftOrNull()?.message}")
@ -135,7 +160,7 @@ internal class TangemPayOnboardingModel @Inject constructor(
router.replaceAll( router.replaceAll(
AppRoute.Wallet, AppRoute.Wallet,
AppRoute.Kyc( AppRoute.Kyc(
userWalletId = tangemPayWalletsManager.getDefaultWalletForTangemPayBlocking().walletId, userWalletId = getUserWalletForPay(params.userWalletId),
), ),
) )
} }

View file

@ -1,26 +0,0 @@
package com.tangem.feature.wallet.child.wallet.model
import arrow.core.Either
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.pay.model.MainScreenCustomerInfo
import com.tangem.domain.pay.model.TangemPayCustomerInfoError
import com.tangem.domain.pay.usecase.TangemPayMainScreenCustomerInfoUseCase
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
internal class TangemPayMainInfoManager @Inject constructor(
private val tangemPayMainScreenCustomerInfoUseCase: TangemPayMainScreenCustomerInfoUseCase,
) {
val mainScreenCustomerInfo:
StateFlow<Pair<UserWalletId, Either<TangemPayCustomerInfoError, MainScreenCustomerInfo>>?>
field = MutableStateFlow(null)
suspend fun refreshTangemPayInfo(userWalletId: UserWalletId) {
val info = tangemPayMainScreenCustomerInfoUseCase(userWalletId)
mainScreenCustomerInfo.value = Pair(userWalletId, info)
}
}

View file

@ -22,6 +22,7 @@ import com.tangem.domain.nft.ObserveAndClearNFTCacheIfNeedUseCase
import com.tangem.domain.notifications.GetIsHuaweiDeviceWithoutGoogleServicesUseCase import com.tangem.domain.notifications.GetIsHuaweiDeviceWithoutGoogleServicesUseCase
import com.tangem.domain.notifications.repository.NotificationsRepository import com.tangem.domain.notifications.repository.NotificationsRepository
import com.tangem.domain.pay.repository.OnboardingRepository import com.tangem.domain.pay.repository.OnboardingRepository
import com.tangem.domain.pay.usecase.TangemPayMainScreenCustomerInfoUseCase
import com.tangem.domain.settings.* import com.tangem.domain.settings.*
import com.tangem.domain.tokens.RefreshMultiCurrencyWalletQuotesUseCase import com.tangem.domain.tokens.RefreshMultiCurrencyWalletQuotesUseCase
import com.tangem.domain.wallets.usecase.* import com.tangem.domain.wallets.usecase.*
@ -96,7 +97,7 @@ internal class WalletModel @Inject constructor(
private val tangemPayOnboardingRepository: OnboardingRepository, private val tangemPayOnboardingRepository: OnboardingRepository,
private val yieldSupplyFeatureToggles: YieldSupplyFeatureToggles, private val yieldSupplyFeatureToggles: YieldSupplyFeatureToggles,
private val accountsFeatureToggles: AccountsFeatureToggles, private val accountsFeatureToggles: AccountsFeatureToggles,
private val tangemPayMainInfoManager: TangemPayMainInfoManager, private val tangemPayMainScreenCustomerInfoUseCase: TangemPayMainScreenCustomerInfoUseCase,
private val trackingContextProxy: TrackingContextProxy, private val trackingContextProxy: TrackingContextProxy,
val screenLifecycleProvider: ScreenLifecycleProvider, val screenLifecycleProvider: ScreenLifecycleProvider,
val innerWalletRouter: InnerWalletRouter, val innerWalletRouter: InnerWalletRouter,
@ -375,7 +376,10 @@ internal class WalletModel @Inject constructor(
}.distinctUntilChanged(), }.distinctUntilChanged(),
transform = ::Pair, transform = ::Pair,
).onEach { (inBackground, userWalletId) -> ).onEach { (inBackground, userWalletId) ->
if (inBackground) return@onEach if (inBackground) {
updateTangemPayJobHolder.cancel()
return@onEach
}
val savedCustomerInfo = val savedCustomerInfo =
tangemPayOnboardingRepository.getSavedCustomerInfo(userWalletId) tangemPayOnboardingRepository.getSavedCustomerInfo(userWalletId)
@ -386,15 +390,15 @@ internal class WalletModel @Inject constructor(
if (isShouldLaunchPeriodicUpdate) { if (isShouldLaunchPeriodicUpdate) {
updateTangemPayJobHolder.cancel() updateTangemPayJobHolder.cancel()
modelScope.launch { modelScope.launch {
tangemPayMainInfoManager.refreshTangemPayInfo(userWalletId) tangemPayMainScreenCustomerInfoUseCase.fetch(userWalletId)
while (isActive) { while (isActive) {
delay(TANGEM_PAY_UPDATE_INTERVAL) delay(TANGEM_PAY_UPDATE_INTERVAL)
tangemPayMainInfoManager.refreshTangemPayInfo(userWalletId) tangemPayMainScreenCustomerInfoUseCase.fetch(userWalletId)
} }
}.saveIn(updateTangemPayJobHolder) }.saveIn(updateTangemPayJobHolder)
} else { } else {
// Don't refresh customer info periodically if the card was already issued, only update on swipe to refresh // Don't refresh customer info periodically if the card was already issued, only update on swipe to refresh
tangemPayMainInfoManager.refreshTangemPayInfo(userWalletId) tangemPayMainScreenCustomerInfoUseCase.fetch(userWalletId)
} }
}.launchIn(modelScope) }.launchIn(modelScope)
} }

View file

@ -13,8 +13,8 @@ import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.models.wallet.requireColdWallet import com.tangem.domain.models.wallet.requireColdWallet
import com.tangem.domain.pay.repository.OnboardingRepository import com.tangem.domain.pay.repository.OnboardingRepository
import com.tangem.domain.pay.usecase.ProduceTangemPayInitialDataUseCase import com.tangem.domain.pay.usecase.ProduceTangemPayInitialDataUseCase
import com.tangem.domain.pay.usecase.TangemPayMainScreenCustomerInfoUseCase
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
import com.tangem.feature.wallet.child.wallet.model.TangemPayMainInfoManager
import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController
import com.tangem.features.tangempay.TangemPayFeatureToggles import com.tangem.features.tangempay.TangemPayFeatureToggles
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -43,7 +43,7 @@ internal class TangemPayClickIntentsImplementor @Inject constructor(
private val getWalletMetainfoUseCase: GetWalletMetaInfoUseCase, private val getWalletMetainfoUseCase: GetWalletMetaInfoUseCase,
private val getUserWalletUseCase: GetUserWalletUseCase, private val getUserWalletUseCase: GetUserWalletUseCase,
private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase, private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase,
private val tangemPayInfoManager: TangemPayMainInfoManager, private val tangemPayMainScreenCustomerInfoUseCase: TangemPayMainScreenCustomerInfoUseCase,
private val uiMessageSender: UiMessageSender, private val uiMessageSender: UiMessageSender,
) : BaseWalletClickIntents(), TangemPayIntents { ) : BaseWalletClickIntents(), TangemPayIntents {
@ -54,13 +54,13 @@ internal class TangemPayClickIntentsImplementor @Inject constructor(
) { ) {
return return
} }
tangemPayInfoManager.refreshTangemPayInfo(userWalletId) tangemPayMainScreenCustomerInfoUseCase.fetch(userWalletId)
} }
override fun onRefreshPayToken(userWalletId: UserWalletId) { override fun onRefreshPayToken(userWalletId: UserWalletId) {
modelScope.launch { modelScope.launch {
produceInitialDataTangemPay.invoke(userWalletId) produceInitialDataTangemPay.invoke(userWalletId)
tangemPayInfoManager.refreshTangemPayInfo(userWalletId) tangemPayMainScreenCustomerInfoUseCase.fetch(userWalletId)
} }
} }

View file

@ -2,11 +2,11 @@ package com.tangem.feature.wallet.child.wallet.model.intents
import arrow.core.getOrElse import arrow.core.getOrElse
import com.tangem.common.ui.expressStatus.ExpressStatusBottomSheetConfig import com.tangem.common.ui.expressStatus.ExpressStatusBottomSheetConfig
import com.tangem.common.ui.tokens.TokenItemStateConverter.ApySource
import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.analytics.models.event.MainScreenAnalyticsEvent import com.tangem.core.analytics.models.event.MainScreenAnalyticsEvent
import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.di.ModelScoped
import com.tangem.domain.models.account.Account import com.tangem.domain.models.account.Account
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.staking.StakingBalance import com.tangem.domain.models.staking.StakingBalance
import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWallet
@ -50,7 +50,12 @@ internal interface WalletContentClickIntents {
fun onTokenItemLongClick(userWalletId: UserWalletId, cryptoCurrencyStatus: CryptoCurrencyStatus) fun onTokenItemLongClick(userWalletId: UserWalletId, cryptoCurrencyStatus: CryptoCurrencyStatus)
fun onApyLabelClick(userWalletId: UserWalletId, currencyStatus: CryptoCurrencyStatus, apy: String) fun onApyLabelClick(
userWalletId: UserWalletId,
currencyStatus: CryptoCurrencyStatus,
apySource: ApySource,
apy: String,
)
fun onAccountExpandClick(account: Account) fun onAccountExpandClick(account: Account)
@ -162,11 +167,17 @@ internal class WalletContentClickIntentsImplementor @Inject constructor(
} }
} }
override fun onApyLabelClick(userWalletId: UserWalletId, currencyStatus: CryptoCurrencyStatus, apy: String) { override fun onApyLabelClick(
val navigationAction = if (currencyStatus.currency is CryptoCurrency.Token) { userWalletId: UserWalletId,
NavigationAction.YieldSupply(currencyStatus.value.yieldSupplyStatus?.isActive == true) currencyStatus: CryptoCurrencyStatus,
} else { apySource: ApySource,
NavigationAction.Staking apy: String,
) {
val navigationAction = when (apySource) {
ApySource.STAKING -> NavigationAction.Staking
ApySource.YIELD_SUPPLY -> {
NavigationAction.YieldSupply(currencyStatus.value.yieldSupplyStatus?.isActive == true)
}
} }
sendApyLabelClickAnalytics(navigationAction, currencyStatus) sendApyLabelClickAnalytics(navigationAction, currencyStatus)

View file

@ -111,8 +111,14 @@ internal class DefaultWalletRouter @Inject constructor(
) )
} }
override fun openTangemPayOnboarding() { override fun openTangemPayOnboarding(userWalletId: UserWalletId) {
router.push(AppRoute.TangemPayOnboarding(AppRoute.TangemPayOnboarding.Mode.ContinueOnboarding)) router.push(
AppRoute.TangemPayOnboarding(
AppRoute.TangemPayOnboarding.Mode.ContinueOnboarding(
userWalletId = userWalletId,
),
),
)
} }
override fun openTangemPayDetails(userWalletId: UserWalletId, config: TangemPayDetailsConfig) { override fun openTangemPayDetails(userWalletId: UserWalletId, config: TangemPayDetailsConfig) {

View file

@ -63,7 +63,7 @@ internal interface InnerWalletRouter {
fun openTokenReceiveBottomSheet(tokenReceiveConfig: TokenReceiveConfig) fun openTokenReceiveBottomSheet(tokenReceiveConfig: TokenReceiveConfig)
fun openTangemPayOnboarding() fun openTangemPayOnboarding(userWalletId: UserWalletId)
fun openTangemPayDetails(userWalletId: UserWalletId, config: TangemPayDetailsConfig) fun openTangemPayDetails(userWalletId: UserWalletId, config: TangemPayDetailsConfig)

View file

@ -9,6 +9,8 @@ internal sealed class TangemPayState {
object Empty : TangemPayState() object Empty : TangemPayState()
data object Loading : TangemPayState()
data class Progress( data class Progress(
val title: TextReference, val title: TextReference,
val description: TextReference, val description: TextReference,

View file

@ -0,0 +1,15 @@
package com.tangem.feature.wallet.presentation.wallet.state.transformers
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.feature.wallet.presentation.wallet.state.model.TangemPayState
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState
internal class TangemPayLoadingStateTransformer(userWalletId: UserWalletId) : WalletStateTransformer(userWalletId) {
override fun transform(prevState: WalletState): WalletState {
return if (prevState is WalletState.MultiCurrency.Content) {
prevState.copy(tangemPayState = TangemPayState.Loading)
} else {
prevState
}
}
}

View file

@ -49,9 +49,15 @@ internal class TokenListStateConverter(
clickIntents.onTokenItemLongClick(selectedWallet.walletId, currencyStatus) clickIntents.onTokenItemLongClick(selectedWallet.walletId, currencyStatus)
} }
private val onApyLabelClick: (currencyStatus: CryptoCurrencyStatus, apy: String) -> Unit = private val onApyLabelClick:
{ currencyStatus, apy -> (currencyStatus: CryptoCurrencyStatus, apySource: TokenItemStateConverter.ApySource, apy: String) -> Unit =
clickIntents.onApyLabelClick(selectedWallet.walletId, currencyStatus, apy) { currencyStatus, apySource, apy ->
clickIntents.onApyLabelClick(
userWalletId = selectedWallet.walletId,
currencyStatus = currencyStatus,
apySource = apySource,
apy = apy,
)
} }
private fun tokenStatusConverter(accountId: AccountId? = null) = TokenItemStateConverter( private fun tokenStatusConverter(accountId: AccountId? = null) = TokenItemStateConverter(
@ -60,7 +66,7 @@ internal class TokenListStateConverter(
stakingApyMap = stakingApyMap, stakingApyMap = stakingApyMap,
onItemClick = { _, status -> onTokenClick(accountId, status) }, onItemClick = { _, status -> onTokenClick(accountId, status) },
onItemLongClick = { _, status -> onTokenLongClick(accountId, status) }, onItemLongClick = { _, status -> onTokenLongClick(accountId, status) },
onApyLabelClick = { status, apy -> onApyLabelClick(status, apy) }, onApyLabelClick = { status, apySource, apy -> onApyLabelClick(status, apySource, apy) },
) )
override fun convert(value: WalletTokensListState): WalletTokensListState { override fun convert(value: WalletTokensListState): WalletTokensListState {

View file

@ -2,24 +2,24 @@ package com.tangem.feature.wallet.presentation.wallet.subscribers
import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.pay.model.MainCustomerInfoContentState
import com.tangem.domain.pay.model.MainScreenCustomerInfo import com.tangem.domain.pay.model.MainScreenCustomerInfo
import com.tangem.domain.pay.model.TangemPayCustomerInfoError import com.tangem.domain.pay.model.TangemPayCustomerInfoError
import com.tangem.domain.pay.repository.TangemPayCardDetailsRepository import com.tangem.domain.pay.repository.TangemPayCardDetailsRepository
import com.tangem.domain.pay.usecase.TangemPayMainScreenCustomerInfoUseCase
import com.tangem.domain.visa.model.TangemPayCardFrozenState import com.tangem.domain.visa.model.TangemPayCardFrozenState
import com.tangem.feature.wallet.child.wallet.model.TangemPayMainInfoManager
import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents
import com.tangem.feature.wallet.presentation.router.InnerWalletRouter import com.tangem.feature.wallet.presentation.router.InnerWalletRouter
import com.tangem.feature.wallet.presentation.wallet.analytics.utils.WalletTangemPayAnalyticsEventSender import com.tangem.feature.wallet.presentation.wallet.analytics.utils.WalletTangemPayAnalyticsEventSender
import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController
import com.tangem.feature.wallet.presentation.wallet.state.transformers.TangemPayHiddenStateTransformer import com.tangem.feature.wallet.presentation.wallet.state.transformers.*
import com.tangem.feature.wallet.presentation.wallet.state.transformers.TangemPayRefreshNeededStateTransformer
import com.tangem.feature.wallet.presentation.wallet.state.transformers.TangemPayUnavailableStateTransformer
import com.tangem.feature.wallet.presentation.wallet.state.transformers.TangemPayUpdateInfoStateTransformer
import dagger.assisted.Assisted import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject import dagger.assisted.AssistedInject
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.* import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.onEach
import timber.log.Timber import timber.log.Timber
@Suppress("LongParameterList") @Suppress("LongParameterList")
@ -29,7 +29,7 @@ internal class TangemPayMainSubscriber @AssistedInject constructor(
private val clickIntents: WalletClickIntents, private val clickIntents: WalletClickIntents,
private val innerWalletRouter: InnerWalletRouter, private val innerWalletRouter: InnerWalletRouter,
private val cardDetailsRepository: TangemPayCardDetailsRepository, private val cardDetailsRepository: TangemPayCardDetailsRepository,
private val tangemPayMainInfoManager: TangemPayMainInfoManager, private val tangemPayMainScreenCustomerInfoUseCase: TangemPayMainScreenCustomerInfoUseCase,
private val analytics: WalletTangemPayAnalyticsEventSender, private val analytics: WalletTangemPayAnalyticsEventSender,
) : WalletSubscriber() { ) : WalletSubscriber() {
@ -38,11 +38,10 @@ internal class TangemPayMainSubscriber @AssistedInject constructor(
} }
private fun subscribeOnTangemPayInfoUpdates(): Flow<*> { private fun subscribeOnTangemPayInfoUpdates(): Flow<*> {
return tangemPayMainInfoManager.mainScreenCustomerInfo return tangemPayMainScreenCustomerInfoUseCase(userWalletId = userWallet.walletId)
.filterNotNull()
.filter { it.first == userWallet.walletId }
.distinctUntilChanged() .distinctUntilChanged()
.onEach { (userWalletId, mainInfoData) -> .onEach { mainInfoData ->
val userWalletId = userWallet.walletId
mainInfoData.onLeft { tangemPayError -> mainInfoData.onLeft { tangemPayError ->
when (tangemPayError) { when (tangemPayError) {
TangemPayCustomerInfoError.RefreshNeededError -> { TangemPayCustomerInfoError.RefreshNeededError -> {
@ -66,13 +65,23 @@ internal class TangemPayMainSubscriber @AssistedInject constructor(
) )
} }
} }
}.onRight { data -> }.onRight { contentState -> handleContentState(state = contentState) }
updateTangemPay(data, userWalletId)
analytics.send(customerInfo = data)
}
} }
} }
private suspend fun handleContentState(state: MainCustomerInfoContentState) {
val userWalletId = userWallet.walletId
when (state) {
MainCustomerInfoContentState.Loading -> stateController.update(
transformer = TangemPayLoadingStateTransformer(userWalletId),
)
is MainCustomerInfoContentState.Content -> {
updateTangemPay(data = state.info, userWalletId = userWalletId)
analytics.send(customerInfo = state.info)
}
}
}
private suspend fun updateTangemPay(data: MainScreenCustomerInfo, userWalletId: UserWalletId) { private suspend fun updateTangemPay(data: MainScreenCustomerInfo, userWalletId: UserWalletId) {
val cardFrozenState = val cardFrozenState =
data.info.productInstance?.cardId?.let { cardDetailsRepository.cardFrozenStateSync(it) } data.info.productInstance?.cardId?.let { cardDetailsRepository.cardFrozenStateSync(it) }
@ -82,7 +91,7 @@ internal class TangemPayMainSubscriber @AssistedInject constructor(
userWalletId = userWalletId, userWalletId = userWalletId,
value = data, value = data,
cardFrozenState = cardFrozenState, cardFrozenState = cardFrozenState,
onClickKyc = innerWalletRouter::openTangemPayOnboarding, onClickKyc = { innerWalletRouter.openTangemPayOnboarding(userWalletId) },
onIssuingCard = clickIntents::onIssuingCardClicked, onIssuingCard = clickIntents::onIssuingCardClicked,
onIssuingFailed = clickIntents::onIssuingFailedClicked, onIssuingFailed = clickIntents::onIssuingFailedClicked,
openDetails = { config -> openDetails = { config ->

View file

@ -224,9 +224,9 @@ private fun WalletContent(
contentType = selectedWallet.tangemPayState::class.java, contentType = selectedWallet.tangemPayState::class.java,
) { ) {
TangemPayMainScreenBlock( TangemPayMainScreenBlock(
selectedWallet.tangemPayState, state = selectedWallet.tangemPayState,
isBalanceHidden = state.isHidingMode, isBalanceHidden = state.isHidingMode,
itemModifier, modifier = itemModifier,
) )
} }
} }

View file

@ -0,0 +1,37 @@
package com.tangem.feature.wallet.presentation.wallet.ui.components.visa
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.components.CircleShimmer
import com.tangem.core.ui.components.RectangleShimmer
import com.tangem.core.ui.components.SpacerWMax
import com.tangem.core.ui.res.TangemTheme
@Composable
internal fun TangemPayLoadingScreenBlock(modifier: Modifier = Modifier) {
Row(
modifier = modifier
.fillMaxWidth()
.background(color = TangemTheme.colors.background.primary, shape = TangemTheme.shapes.roundedCornersXMedium)
.padding(horizontal = 12.dp, vertical = 16.dp),
verticalAlignment = Alignment.CenterVertically,
) {
CircleShimmer(modifier = Modifier.size(36.dp))
Column(
modifier = Modifier.padding(start = 12.dp),
verticalArrangement = Arrangement.spacedBy(2.dp),
) {
RectangleShimmer(modifier = Modifier.padding(vertical = 4.dp).sizeIn(minWidth = 70.dp, minHeight = 12.dp))
RectangleShimmer(modifier = Modifier.padding(vertical = 2.dp).sizeIn(minWidth = 52.dp, minHeight = 12.dp))
}
SpacerWMax()
Column(verticalArrangement = Arrangement.spacedBy(2.dp)) {
RectangleShimmer(modifier = Modifier.padding(vertical = 4.dp).sizeIn(minWidth = 40.dp, minHeight = 12.dp))
RectangleShimmer(modifier = Modifier.padding(vertical = 2.dp).sizeIn(minWidth = 40.dp, minHeight = 12.dp))
}
}
}

View file

@ -23,6 +23,7 @@ internal fun TangemPayMainScreenBlock(state: TangemPayState, isBalanceHidden: Bo
is TangemPayState.RefreshNeeded -> TangemPayRefreshBlock(state, modifier) is TangemPayState.RefreshNeeded -> TangemPayRefreshBlock(state, modifier)
is TangemPayState.TemporaryUnavailable -> TangemPayUnavailableBlock(state, modifier) is TangemPayState.TemporaryUnavailable -> TangemPayUnavailableBlock(state, modifier)
is TangemPayState.FailedIssue -> TangemPayFailedIssueState(state, modifier) is TangemPayState.FailedIssue -> TangemPayFailedIssueState(state, modifier)
TangemPayState.Loading -> TangemPayLoadingScreenBlock(modifier)
} }
} }
@ -32,6 +33,8 @@ internal fun TangemPayMainScreenBlock(state: TangemPayState, isBalanceHidden: Bo
private fun TangemPayMainScreenBlockPreview() { private fun TangemPayMainScreenBlockPreview() {
TangemThemePreview { TangemThemePreview {
Column(verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8)) { Column(verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8)) {
TangemPayMainScreenBlock(state = TangemPayState.Loading, isBalanceHidden = false)
TangemPayMainScreenBlock( TangemPayMainScreenBlock(
Progress( Progress(
title = TextReference.Res(R.string.tangempay_kyc_in_progress_notification_title), title = TextReference.Res(R.string.tangempay_kyc_in_progress_notification_title),