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.domain.card.common.visa.VisaUtilities
import com.tangem.domain.visa.error.VisaActivationError
import com.tangem.operations.ScanTask
import com.tangem.operations.derivation.DeriveWalletPublicKeyTask
import com.tangem.operations.sign.SignHashCommand
@ -92,25 +91,7 @@ class TangemPaySignWithdrawalHashTask(
?: targetWalletPublicKey.toDecompressedPublicKey(),
).asRSVLegacyEVM().toHexString().lowercase()
scanCard(
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))
callback(CompletionResult.Success(rsvSignature))
}
is CompletionResult.Failure -> {
callback(CompletionResult.Failure(result.error))

View file

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

View file

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

View file

@ -48,7 +48,7 @@ class TokenItemStateConverter(
private val iconStateProvider: (CryptoCurrencyStatus) -> CurrencyIconState = {
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 ->
createTitleState(
currencyStatus = currencyStatus,
@ -166,7 +166,7 @@ class TokenItemStateConverter(
currencyStatus: CryptoCurrencyStatus,
yieldModuleApyMap: Map<String, String>,
stakingApyMap: Map<String, List<Yield.Validator>>,
onApyLabelClick: ((CryptoCurrencyStatus, String) -> Unit)?,
onApyLabelClick: ((CryptoCurrencyStatus, ApySource, String) -> Unit)?,
): TokenItemState.TitleState {
return when (val value = currencyStatus.value) {
is CryptoCurrencyStatus.Loading,
@ -192,7 +192,7 @@ class TokenItemStateConverter(
earnApy = apyInfo?.text,
earnApyIsActive = apyInfo?.isActive == true,
onApyLabelClick = if (apyInfo?.apy != null && onApyLabelClick != null) {
{ onApyLabelClick.invoke(currencyStatus, apyInfo.apy) }
{ onApyLabelClick.invoke(currencyStatus, apyInfo.source, apyInfo.apy) }
} else {
null
},
@ -224,6 +224,7 @@ class TokenItemStateConverter(
),
isActive = isActive,
apy = yieldSupplyApy,
source = ApySource.YIELD_SUPPLY,
)
}
}
@ -249,6 +250,7 @@ class TokenItemStateConverter(
),
isActive = stakingInfo.isActive,
apy = apyString,
source = ApySource.STAKING,
)
}
}
@ -433,5 +435,11 @@ class TokenItemStateConverter(
val text: TextReference?,
val isActive: Boolean,
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()
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.network.NetworkFactory
import com.tangem.data.pay.util.TangemPayErrorConverter
import com.tangem.data.pay.util.TangemPayWalletsManager
import com.tangem.datasource.di.NetworkMoshi
import com.tangem.domain.models.ReceiveAddressModel
import com.tangem.domain.models.ReceiveAddressModel.NameService
@ -32,7 +31,6 @@ private const val TOKEN_DECIMALS = 6
internal class DefaultTangemPaySwapDataFactory @Inject constructor(
@NetworkMoshi moshi: Moshi,
private val tangemPayWalletsManager: TangemPayWalletsManager,
excludedBlockchains: ExcludedBlockchains,
) : TangemPaySwapDataFactory {
@ -63,17 +61,17 @@ internal class DefaultTangemPaySwapDataFactory @Inject constructor(
}
override fun create(
userWallet: UserWallet,
depositAddress: String,
chainId: Int,
cryptoBalance: BigDecimal,
fiatBalance: BigDecimal,
): Either<UniversalError, TangemPayTopUpData> {
return catch {
val wallet = tangemPayWalletsManager.getDefaultWalletForTangemPayBlocking()
val currency = getCurrency(wallet, chainId)
val currency = getCurrency(userWallet, chainId)
TangemPayTopUpData(
currency = currency,
walletId = wallet.walletId,
walletId = userWallet.walletId,
cryptoBalance = cryptoBalance,
fiatBalance = fiatBalance,
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.wallets.legacy.UserWalletsListManager
import com.tangem.features.hotwallet.HotWalletFeatureToggles
import com.tangem.utils.extensions.addHexPrefix
import java.math.BigDecimal
import java.math.RoundingMode
import java.util.Currency
@ -66,7 +67,7 @@ internal class DefaultTangemPaySwapRepository @Inject constructor(
recipientAddress = receiverAddress,
adminSalt = result.salt,
senderAddress = result.senderAddress,
adminSignature = signatureResult.signature,
adminSignature = signatureResult.signature.addHexPrefix(),
)
tangemPayApi.withdraw(authHeader = authHeader, body = request)
}

View file

@ -21,7 +21,6 @@ import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import javax.inject.Inject
private const val INITIAL_CURSOR = "initial_cursor_key"
private const val TAG = "TangemPay: TangemPayTxHistoryRepository:"
internal class DefaultTangemPayTxHistoryRepository @Inject constructor(
private val requestPerformer: TangemPayRequestPerformer,
@ -106,12 +105,14 @@ internal class DefaultTangemPayTxHistoryRepository @Inject constructor(
cursor: String?,
pageSize: Int,
) {
requestPerformer.runWithErrorLogs(TAG) {
val result = requestPerformer.request(userWalletId) { authHeader ->
visaApi.getTangemPayTxHistory(authHeader = authHeader, limit = pageSize, cursor = cursor)
}.result
requestPerformer.performRequest(userWalletId = userWalletId) { authHeader ->
visaApi.getTangemPayTxHistory(authHeader = authHeader, limit = pageSize, cursor = cursor)
}.onLeft {
error(it.toString())
}.onRight { response ->
val result = response.result
val items = txHistoryItemConverter.convertList(result.transactions).filterNotNull()
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.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext
import timber.log.Timber
import java.util.concurrent.ConcurrentHashMap
import javax.inject.Inject
import javax.inject.Singleton
@ -40,7 +40,7 @@ internal class TangemPayRequestPerformer @Inject constructor(
private val tangemPayStorage: TangemPayStorage,
) {
private val customerWalletAddress = MutableStateFlow<String?>(null)
private val customerWalletAddresses = ConcurrentHashMap<UserWalletId, String>()
private val tokensMutex = Mutex()
private val errorConverter = TangemPayErrorConverter(moshi)
@ -110,7 +110,7 @@ internal class TangemPayRequestPerformer @Inject constructor(
}
suspend fun getCustomerWalletAddress(userWalletId: UserWalletId): String {
val existingAddress = customerWalletAddress.value
val existingAddress = customerWalletAddresses[userWalletId]
if (existingAddress != null) {
return existingAddress
}
@ -118,7 +118,7 @@ internal class TangemPayRequestPerformer @Inject constructor(
userWalletId = userWalletId,
) ?: error("Can not find customer address")
customerWalletAddress.value = storedAddress
customerWalletAddresses[userWalletId] = storedAddress
return storedAddress
}

View file

@ -15,12 +15,14 @@ class TangemPayWalletsManager @Inject constructor(
private val hotWalletFeatureToggles: HotWalletFeatureToggles,
) {
@Deprecated("Don't use and put userWallet in features that need it")
suspend fun getDefaultWalletForTangemPay(): UserWallet.Cold {
val userWalletsFlow = if (useNewRepository()) repository.userWallets else manager.userWallets
val userWallets = userWalletsFlow.filter { !it.isNullOrEmpty() }.first()
return findColdWallet(userWallets)
}
@Deprecated("Don't use and put userWallet in features that need it")
fun getDefaultWalletForTangemPayBlocking(): UserWallet.Cold {
val userWallets = if (useNewRepository()) repository.userWallets.value else manager.userWalletsSync
return findColdWallet(userWallets)
@ -29,7 +31,8 @@ class TangemPayWalletsManager @Inject constructor(
private fun useNewRepository(): Boolean = hotWalletFeatureToggles.isHotWalletEnabled
private fun findColdWallet(userWallets: List<UserWallet>?): UserWallet.Cold {
return userWallets?.find { it is UserWallet.Cold } as? UserWallet.Cold
?: error("Cannot find cold user wallet")
return userWallets?.find {
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.domain.models.ReceiveAddressModel
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId
import java.math.BigDecimal
interface TangemPaySwapDataFactory {
fun create(
userWallet: UserWallet,
depositAddress: String,
chainId: Int,
cryptoBalance: BigDecimal,

View file

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

View file

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

View file

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

View file

@ -329,6 +329,10 @@ internal class ManageTokensListManager @AssistedInject constructor(
val updatedUiItem = uiBatch.data[currencyIndex].toggleExpanded(
currency = currencyBatch.data[currencyIndex],
isEditable = batches.canEditItems,
updates = CurrencyUpdates(
toAdd = currenciesToAdd.value,
toRemove = currenciesToRemove.value,
),
onSelectCurrencyNetwork = { 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.features.managetokens.entity.item.CurrencyItemUM.Basic.NetworksUM
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 kotlinx.collections.immutable.toImmutableList
internal fun ManagedCryptoCurrency.Token.toUiNetworksModel(
isExpanded: Boolean,
isItemsEditable: Boolean,
updates: CurrencyUpdates,
onSelectedStateChange: (SourceNetwork, Boolean) -> Unit,
onLongTap: (SourceNetwork) -> Unit,
): NetworksUM {
return if (isExpanded) {
NetworksUM.Expanded(
networks = availableNetworks.map {
it.toCurrencyNetworkModel(
isSelected = it.network in addedIn,
networks = availableNetworks.map { sourceNetwork ->
val isSelectedByDefault = sourceNetwork.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,
onSelectedStateChange = onSelectedStateChange,
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.features.managetokens.entity.item.CurrencyItemUM
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 kotlinx.collections.immutable.toImmutableList
internal fun CurrencyItemUM.toggleExpanded(
currency: ManagedCryptoCurrency,
isEditable: Boolean,
updates: CurrencyUpdates,
onSelectCurrencyNetwork: (SourceNetwork, Boolean) -> Unit,
onLongTap: (SourceNetwork) -> Unit,
): CurrencyItemUM {
@ -30,6 +32,7 @@ internal fun CurrencyItemUM.toggleExpanded(
networks = currency.toUiNetworksModel(
isExpanded = isExpanded,
isItemsEditable = isEditable,
updates = updates,
onSelectedStateChange = onSelectCurrencyNetwork,
onLongTap = onLongTap,
),

View file

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

View file

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

View file

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

View file

@ -22,5 +22,8 @@ internal sealed class TangemPayDetailsNavigation {
) : TangemPayDetailsNavigation()
@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(
topBarConfig = TangemPayDetailsTopBarConfig(
onBackClick = onBack,

View file

@ -8,6 +8,7 @@ import com.tangem.core.ui.extensions.TextReference
import kotlinx.collections.immutable.ImmutableList
internal data class TangemPayTxHistoryDetailsUM(
val isBalanceHidden: Boolean,
val title: TextReference,
val iconState: ImageReference,
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.ParamsContainer
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.entity.TangemPayAddFundsUM
import com.tangem.features.tangempay.model.transformers.TangemPayAddFundsUMConverter
@ -17,6 +18,7 @@ internal class TangemPayAddFundsModel @Inject constructor(
paramsContainer: ParamsContainer,
override val dispatchers: CoroutineDispatcherProvider,
private val tangemPaySwapDataFactory: TangemPaySwapDataFactory,
private val getUserWalletUseCase: GetUserWalletUseCase,
) : Model() {
private val params = paramsContainer.require<TangemPayAddFundsComponent.Params>()
@ -24,7 +26,11 @@ internal class TangemPayAddFundsModel @Inject constructor(
val uiState: TangemPayAddFundsUM = getInitialState()
private fun getInitialState(): TangemPayAddFundsUM {
val userWallet = requireNotNull(
getUserWalletUseCase(params.walletId).getOrNull(),
) { "User wallet not found for id: ${params.walletId}" }
val data = tangemPaySwapDataFactory.create(
userWallet = userWallet,
depositAddress = params.depositAddress,
chainId = params.chainId,
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.domain.balancehiding.GetBalanceHidingSettingsUseCase
import com.tangem.domain.models.TokenReceiveConfig
import com.tangem.domain.pay.TangemPayTopUpData
import com.tangem.domain.pay.TangemPaySwapDataFactory
import com.tangem.domain.pay.TangemPayTopUpData
import com.tangem.domain.pay.model.TangemPayCardBalance
import com.tangem.domain.pay.repository.CustomerOrderRepository
import com.tangem.domain.pay.repository.TangemPayCardDetailsRepository
import com.tangem.domain.tangempay.TangemPayAnalyticsEvents
import com.tangem.domain.visa.model.TangemPayCardFrozenState
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.components.AddFundsListener
import com.tangem.features.tangempay.components.TangemPayDetailsContainerComponent
@ -55,7 +56,7 @@ import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
@Suppress("LongParameterList")
@Suppress("LongParameterList", "LargeClass")
@Stable
@ModelScoped
internal class TangemPayDetailsModel @Inject constructor(
@ -71,6 +72,7 @@ internal class TangemPayDetailsModel @Inject constructor(
private val txHistoryUpdateListener: TangemPayTxHistoryUpdateListener,
private val tangemPaySwapDataFactory: TangemPaySwapDataFactory,
private val orderRepository: CustomerOrderRepository,
private val getUserWalletUseCase: GetUserWalletUseCase,
) : Model(), TangemPayTxHistoryUiActions, TangemPayDetailIntents, AddFundsListener {
private val params: TangemPayDetailsContainerComponent.Params = paramsContainer.require()
@ -234,7 +236,11 @@ internal class TangemPayDetailsModel @Inject constructor(
if (hasActiveWithdrawal) {
showBottomSheetError(TangemPayDetailsErrorType.WithdrawInProgress)
} else {
val userWallet = requireNotNull(
getUserWalletUseCase(params.userWalletId).getOrNull(),
) { "User wallet not found: ${params.userWalletId}" }
val data = tangemPaySwapDataFactory.create(
userWallet = userWallet,
depositAddress = depositAddress,
chainId = params.config.chainId,
cryptoBalance = currentBalance.cryptoBalance,
@ -367,7 +373,12 @@ internal class TangemPayDetailsModel @Inject constructor(
}
override fun onTransactionClick(item: TangemPayTxHistoryItem) {
bottomSheetNavigation.activate(TangemPayDetailsNavigation.TransactionDetails(item))
bottomSheetNavigation.activate(
configuration = TangemPayDetailsNavigation.TransactionDetails(
transaction = item,
isBalanceHidden = uiState.value.isBalanceHidden,
),
)
}
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.ParamsContainer
import com.tangem.core.navigation.url.UrlOpener
import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase
import com.tangem.domain.feedback.GetWalletMetaInfoUseCase
import com.tangem.domain.feedback.SendFeedbackEmailUseCase
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.model.transformers.TangemPayTxHistoryDetailsConverter
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import javax.inject.Inject
@Stable
@ModelScoped
@Suppress("LongParameterList")
internal class TangemPayTxHistoryDetailsModel @Inject constructor(
override val dispatchers: CoroutineDispatcherProvider,
private val getUserWalletsUseCase: GetWalletsUseCase,
private val getWalletMetaInfoUseCase: GetWalletMetaInfoUseCase,
private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase,
private val urlOpener: UrlOpener,
private val balanceHidingSettings: GetBalanceHidingSettingsUseCase,
paramsContainer: ParamsContainer,
) : Model() {
private val params = paramsContainer.require<TangemPayTxHistoryDetailsComponent.Params>()
val uiState: TangemPayTxHistoryDetailsUM = TangemPayTxHistoryDetailsConverter.convert(
TangemPayTxHistoryDetailsConverter.Input(
item = params.transaction,
onExplorerClick = ::openExplorer,
onDisputeClick = ::dispute,
onDismiss = ::dismiss,
),
)
val uiState: StateFlow<TangemPayTxHistoryDetailsUM>
field = MutableStateFlow(
value = TangemPayTxHistoryDetailsConverter.convert(
value = TangemPayTxHistoryDetailsConverter.Input(
item = params.transaction,
isBalanceHidden = params.isBalanceHidden,
onExplorerClick = ::openExplorer,
onDisputeClick = ::dispute,
onDismiss = ::dismiss,
),
),
)
init {
subscribeToBalanceHiding()
}
fun dismiss() {
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)
}
fun dispute() {
private fun dispute() {
modelScope.launch {
val userWalletId = params.userWalletId
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.domain.visa.model.TangemPayCardFrozenState
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.TangemPayDetailsTopBarMenuItemType.FreezeCard
import com.tangem.features.tangempay.entity.TangemPayDetailsTopBarMenuItemType.UnfreezeCard
@ -31,9 +32,20 @@ internal class TangemPayFreezeUnfreezeStateTransformer(
it.type == FreezeCard || it.type == UnfreezeCard
}
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(
topBarConfig = prevState.topBarConfig.copy(items = dropdownMenuItems),
cardFrozenState = converter.convert(cardFrozenState),
balanceBlockState = balanceBlockState,
)
}

View file

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

View file

@ -99,19 +99,6 @@ internal fun TangemPayDetailsScreen(
}
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) {
item(
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) }
}
}

View file

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

View file

@ -13,6 +13,9 @@ dependencies {
implementation(projects.core.decompose)
implementation(projects.core.ui)
/** Domain */
implementation(projects.domain.models)
/** Compose */
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.ui.decompose.ComposableContentComponent
import com.tangem.domain.models.wallet.UserWalletId
interface TangemPayOnboardingComponent : ComposableContentComponent {
sealed class Params {
abstract val userWalletId: UserWalletId?
data class Deeplink(
val deeplink: String,
override val userWalletId: UserWalletId?,
) : Params()
object ContinueOnboarding : Params()
data class ContinueOnboarding(
override val userWalletId: UserWalletId?,
) : Params()
}
interface Factory : ComponentFactory<Params, TangemPayOnboardingComponent>

View file

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

View file

@ -1,10 +1,11 @@
package com.tangem.features.tangempay.deeplink
import android.net.Uri
import dagger.assisted.Assisted
import com.tangem.common.routing.AppRoute
import com.tangem.features.tangempay.TangemPayFeatureToggles
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.AssistedInject
@ -12,11 +13,16 @@ internal class DefaultOnboardVisaDeepLinkHandler @AssistedInject constructor(
@Assisted uri: Uri,
appRouter: AppRouter,
tangemPayFeatureToggles: TangemPayFeatureToggles,
getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase,
) : OnboardVisaDeepLinkHandler {
init {
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))
} else {
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.navigation.url.UrlOpener
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.usecase.ProduceTangemPayInitialDataUseCase
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.components.TangemPayOnboardingComponent
import com.tangem.features.tangempay.model.transformers.TangemPayOnboardingButtonLoadingTransformer
@ -37,6 +41,8 @@ internal class TangemPayOnboardingModel @Inject constructor(
private val produceInitialDataUseCase: ProduceTangemPayInitialDataUseCase,
private val urlOpener: UrlOpener,
private val tangemPayWalletsManager: TangemPayWalletsManager,
private val getUserWalletUseCase: GetUserWalletUseCase,
private val getSelectedWalletUseCase: GetSelectedWalletUseCase,
) : Model() {
private val params = paramsContainer.require<TangemPayOnboardingComponent.Params>()
@ -74,8 +80,9 @@ internal class TangemPayOnboardingModel @Inject constructor(
private suspend fun checkCustomerInfo() {
// TODO implement selector
val userWalletId = getUserWalletForPay(params.userWalletId)
repository.getCustomerInfo(
userWalletId = tangemPayWalletsManager.getDefaultWalletForTangemPayBlocking().walletId,
userWalletId = userWalletId,
)
// selector
.onRight { customerInfo ->
@ -92,6 +99,24 @@ internal class TangemPayOnboardingModel @Inject constructor(
.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() {
analytics.send(TangemPayAnalyticsEvents.ViewTermsClicked)
urlOpener.openUrl(TangemPayConstants.TERMS_AND_LIMITS_LINK)
@ -103,7 +128,7 @@ internal class TangemPayOnboardingModel @Inject constructor(
uiState.transformerUpdate(TangemPayOnboardingButtonLoadingTransformer(isLoading = true))
modelScope.launch {
// TODO implement selector
val userWalletId = tangemPayWalletsManager.getDefaultWalletForTangemPayBlocking().walletId
val userWalletId = getUserWalletForPay(params.userWalletId)
val result = produceInitialDataUseCase(userWalletId)
if (result.isLeft()) {
Timber.e("Error producing initial data: ${result.leftOrNull()?.message}")
@ -135,7 +160,7 @@ internal class TangemPayOnboardingModel @Inject constructor(
router.replaceAll(
AppRoute.Wallet,
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.repository.NotificationsRepository
import com.tangem.domain.pay.repository.OnboardingRepository
import com.tangem.domain.pay.usecase.TangemPayMainScreenCustomerInfoUseCase
import com.tangem.domain.settings.*
import com.tangem.domain.tokens.RefreshMultiCurrencyWalletQuotesUseCase
import com.tangem.domain.wallets.usecase.*
@ -96,7 +97,7 @@ internal class WalletModel @Inject constructor(
private val tangemPayOnboardingRepository: OnboardingRepository,
private val yieldSupplyFeatureToggles: YieldSupplyFeatureToggles,
private val accountsFeatureToggles: AccountsFeatureToggles,
private val tangemPayMainInfoManager: TangemPayMainInfoManager,
private val tangemPayMainScreenCustomerInfoUseCase: TangemPayMainScreenCustomerInfoUseCase,
private val trackingContextProxy: TrackingContextProxy,
val screenLifecycleProvider: ScreenLifecycleProvider,
val innerWalletRouter: InnerWalletRouter,
@ -375,7 +376,10 @@ internal class WalletModel @Inject constructor(
}.distinctUntilChanged(),
transform = ::Pair,
).onEach { (inBackground, userWalletId) ->
if (inBackground) return@onEach
if (inBackground) {
updateTangemPayJobHolder.cancel()
return@onEach
}
val savedCustomerInfo =
tangemPayOnboardingRepository.getSavedCustomerInfo(userWalletId)
@ -386,15 +390,15 @@ internal class WalletModel @Inject constructor(
if (isShouldLaunchPeriodicUpdate) {
updateTangemPayJobHolder.cancel()
modelScope.launch {
tangemPayMainInfoManager.refreshTangemPayInfo(userWalletId)
tangemPayMainScreenCustomerInfoUseCase.fetch(userWalletId)
while (isActive) {
delay(TANGEM_PAY_UPDATE_INTERVAL)
tangemPayMainInfoManager.refreshTangemPayInfo(userWalletId)
tangemPayMainScreenCustomerInfoUseCase.fetch(userWalletId)
}
}.saveIn(updateTangemPayJobHolder)
} else {
// 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)
}

View file

@ -13,8 +13,8 @@ import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.models.wallet.requireColdWallet
import com.tangem.domain.pay.repository.OnboardingRepository
import com.tangem.domain.pay.usecase.ProduceTangemPayInitialDataUseCase
import com.tangem.domain.pay.usecase.TangemPayMainScreenCustomerInfoUseCase
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.features.tangempay.TangemPayFeatureToggles
import kotlinx.coroutines.launch
@ -43,7 +43,7 @@ internal class TangemPayClickIntentsImplementor @Inject constructor(
private val getWalletMetainfoUseCase: GetWalletMetaInfoUseCase,
private val getUserWalletUseCase: GetUserWalletUseCase,
private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase,
private val tangemPayInfoManager: TangemPayMainInfoManager,
private val tangemPayMainScreenCustomerInfoUseCase: TangemPayMainScreenCustomerInfoUseCase,
private val uiMessageSender: UiMessageSender,
) : BaseWalletClickIntents(), TangemPayIntents {
@ -54,13 +54,13 @@ internal class TangemPayClickIntentsImplementor @Inject constructor(
) {
return
}
tangemPayInfoManager.refreshTangemPayInfo(userWalletId)
tangemPayMainScreenCustomerInfoUseCase.fetch(userWalletId)
}
override fun onRefreshPayToken(userWalletId: UserWalletId) {
modelScope.launch {
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 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.models.event.MainScreenAnalyticsEvent
import com.tangem.core.decompose.di.ModelScoped
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.staking.StakingBalance
import com.tangem.domain.models.wallet.UserWallet
@ -50,7 +50,12 @@ internal interface WalletContentClickIntents {
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)
@ -162,11 +167,17 @@ internal class WalletContentClickIntentsImplementor @Inject constructor(
}
}
override fun onApyLabelClick(userWalletId: UserWalletId, currencyStatus: CryptoCurrencyStatus, apy: String) {
val navigationAction = if (currencyStatus.currency is CryptoCurrency.Token) {
NavigationAction.YieldSupply(currencyStatus.value.yieldSupplyStatus?.isActive == true)
} else {
NavigationAction.Staking
override fun onApyLabelClick(
userWalletId: UserWalletId,
currencyStatus: CryptoCurrencyStatus,
apySource: ApySource,
apy: String,
) {
val navigationAction = when (apySource) {
ApySource.STAKING -> NavigationAction.Staking
ApySource.YIELD_SUPPLY -> {
NavigationAction.YieldSupply(currencyStatus.value.yieldSupplyStatus?.isActive == true)
}
}
sendApyLabelClickAnalytics(navigationAction, currencyStatus)

View file

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

View file

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

View file

@ -9,6 +9,8 @@ internal sealed class TangemPayState {
object Empty : TangemPayState()
data object Loading : TangemPayState()
data class Progress(
val title: 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)
}
private val onApyLabelClick: (currencyStatus: CryptoCurrencyStatus, apy: String) -> Unit =
{ currencyStatus, apy ->
clickIntents.onApyLabelClick(selectedWallet.walletId, currencyStatus, apy)
private val onApyLabelClick:
(currencyStatus: CryptoCurrencyStatus, apySource: TokenItemStateConverter.ApySource, apy: String) -> Unit =
{ currencyStatus, apySource, apy ->
clickIntents.onApyLabelClick(
userWalletId = selectedWallet.walletId,
currencyStatus = currencyStatus,
apySource = apySource,
apy = apy,
)
}
private fun tokenStatusConverter(accountId: AccountId? = null) = TokenItemStateConverter(
@ -60,7 +66,7 @@ internal class TokenListStateConverter(
stakingApyMap = stakingApyMap,
onItemClick = { _, status -> onTokenClick(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 {

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.UserWalletId
import com.tangem.domain.pay.model.MainCustomerInfoContentState
import com.tangem.domain.pay.model.MainScreenCustomerInfo
import com.tangem.domain.pay.model.TangemPayCustomerInfoError
import com.tangem.domain.pay.repository.TangemPayCardDetailsRepository
import com.tangem.domain.pay.usecase.TangemPayMainScreenCustomerInfoUseCase
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.presentation.router.InnerWalletRouter
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.transformers.TangemPayHiddenStateTransformer
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 com.tangem.feature.wallet.presentation.wallet.state.transformers.*
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
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
@Suppress("LongParameterList")
@ -29,7 +29,7 @@ internal class TangemPayMainSubscriber @AssistedInject constructor(
private val clickIntents: WalletClickIntents,
private val innerWalletRouter: InnerWalletRouter,
private val cardDetailsRepository: TangemPayCardDetailsRepository,
private val tangemPayMainInfoManager: TangemPayMainInfoManager,
private val tangemPayMainScreenCustomerInfoUseCase: TangemPayMainScreenCustomerInfoUseCase,
private val analytics: WalletTangemPayAnalyticsEventSender,
) : WalletSubscriber() {
@ -38,11 +38,10 @@ internal class TangemPayMainSubscriber @AssistedInject constructor(
}
private fun subscribeOnTangemPayInfoUpdates(): Flow<*> {
return tangemPayMainInfoManager.mainScreenCustomerInfo
.filterNotNull()
.filter { it.first == userWallet.walletId }
return tangemPayMainScreenCustomerInfoUseCase(userWalletId = userWallet.walletId)
.distinctUntilChanged()
.onEach { (userWalletId, mainInfoData) ->
.onEach { mainInfoData ->
val userWalletId = userWallet.walletId
mainInfoData.onLeft { tangemPayError ->
when (tangemPayError) {
TangemPayCustomerInfoError.RefreshNeededError -> {
@ -66,13 +65,23 @@ internal class TangemPayMainSubscriber @AssistedInject constructor(
)
}
}
}.onRight { data ->
updateTangemPay(data, userWalletId)
analytics.send(customerInfo = data)
}
}.onRight { contentState -> handleContentState(state = contentState) }
}
}
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) {
val cardFrozenState =
data.info.productInstance?.cardId?.let { cardDetailsRepository.cardFrozenStateSync(it) }
@ -82,7 +91,7 @@ internal class TangemPayMainSubscriber @AssistedInject constructor(
userWalletId = userWalletId,
value = data,
cardFrozenState = cardFrozenState,
onClickKyc = innerWalletRouter::openTangemPayOnboarding,
onClickKyc = { innerWalletRouter.openTangemPayOnboarding(userWalletId) },
onIssuingCard = clickIntents::onIssuingCardClicked,
onIssuingFailed = clickIntents::onIssuingFailedClicked,
openDetails = { config ->

View file

@ -224,9 +224,9 @@ private fun WalletContent(
contentType = selectedWallet.tangemPayState::class.java,
) {
TangemPayMainScreenBlock(
selectedWallet.tangemPayState,
state = selectedWallet.tangemPayState,
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.TemporaryUnavailable -> TangemPayUnavailableBlock(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() {
TangemThemePreview {
Column(verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8)) {
TangemPayMainScreenBlock(state = TangemPayState.Loading, isBalanceHidden = false)
TangemPayMainScreenBlock(
Progress(
title = TextReference.Res(R.string.tangempay_kyc_in_progress_notification_title),