Updated on 2026-08-14

This commit is contained in:
Tangem 2025-12-10 16:31:57 +03:00
commit 4df207e832
59 changed files with 492 additions and 234 deletions

View file

@ -45,7 +45,7 @@ class DetailsPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
val contactSupportButton: KNode = child {
hasTestTag(DetailsScreenTestTags.SCREEN_ITEM)
hasText(getResourceString(R.string.details_row_title_contact_to_support))
hasText(getResourceString(R.string.common_contact_support))
}
val toSButton: KNode = child {
hasTestTag(DetailsScreenTestTags.SCREEN_ITEM)

View file

@ -18,8 +18,12 @@ import com.tangem.tap.domain.userWalletList.utils.toUserWallets
import com.tangem.tap.domain.userWalletList.utils.updateWith
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import timber.log.Timber
@Suppress("LargeClass")
@OptIn(ExperimentalCoroutinesApi::class)
internal class BiometricUserWalletsListManager(
private val keysRepository: UserWalletsKeysRepository,
@ -29,6 +33,9 @@ internal class BiometricUserWalletsListManager(
) : UserWalletsListManager.Lockable {
private val state = MutableStateFlow(State())
private var hasSavedWallets: Boolean? = null
private val savedWalletMutex = Mutex()
override val isLockable: Boolean = true
override val userWallets: Flow<List<UserWallet>>
@ -61,7 +68,21 @@ internal class BiometricUserWalletsListManager(
get() = state.value.isLocked
override val hasUserWallets: Boolean
get() = keysRepository.hasSavedEncryptionKeys()
get() {
return runBlocking {
// workaround to avoid calling hasSavedEncryptionKeys many times because of performance
savedWalletMutex.withLock {
val hasSavedWalletsLocal = hasSavedWallets
if (hasSavedWalletsLocal == null || !hasSavedWalletsLocal) {
val hasKeys = keysRepository.hasSavedEncryptionKeys()
hasSavedWallets = hasKeys
hasKeys
} else {
true
}
}
}
}
override val walletsCount: Int
get() = state.value.userWallets.size
@ -194,6 +215,9 @@ internal class BiometricUserWalletsListManager(
}
override suspend fun clear(): CompletionResult<Unit> {
savedWalletMutex.withLock {
hasSavedWallets = null
}
return sensitiveInformationRepository.clear()
.flatMap { publicInformationRepository.clear() }
.map {

View file

@ -35,8 +35,8 @@ import com.tangem.core.ui.extensions.isNullOrEmpty
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.utils.singleEvent
import com.tangem.core.ui.test.SendScreenTestTags
import com.tangem.core.ui.utils.singleEvent
@Composable
fun NavigationButtonsBlock(
@ -78,7 +78,9 @@ fun NavigationButtonsBlockV2(
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12),
) {
PreviousButton(navigationUM?.prevButton)
NavigationPrimaryButton(navigationUM?.primaryButton, modifier = Modifier.weight(1f))
key(navigationUM?.source) {
NavigationPrimaryButton(navigationUM?.primaryButton, modifier = Modifier.weight(1f))
}
}
}
}

View file

@ -7,6 +7,7 @@ import com.tangem.core.ui.extensions.TextReference
@Immutable
sealed class NavigationUM {
data class Content(
val source: String,
val title: TextReference,
val subtitle: TextReference?,
@DrawableRes val backIconRes: Int,

View file

@ -29,7 +29,7 @@
},
{
"name": "zklink",
"version": "5.31"
"version": "undefined"
},
{
"name": "plasma",

View file

@ -49,7 +49,7 @@
},
{
"name": "NEW_ONRAMP_MAIN_ENABLED",
"version": "undefined"
"version": "5.31.0"
},
{
"name": "ACCOUNTS_FEATURE_ENABLED",

View file

@ -248,6 +248,7 @@
<string name="common_coming_soon">Coming soon</string>
<string name="common_confirm">Confirm</string>
<string name="common_connecting">Connecting</string>
<string name="common_contact_support">Contact support</string>
<string name="common_contact_tangem_support">Contact Tangem Support</string>
<string name="common_contact_visa_support">Contact Visa Support</string>
<string name="common_continue">Continue</string>
@ -1390,6 +1391,8 @@
<string name="swapping_to_title">You receive</string>
<string name="swapping_token_list_title">Choose token</string>
<string name="swapping_token_not_available">not available</string>
<string name="tangem_pay_beta_notification_subtitle">We would be happy to receive your feedback</string>
<string name="tangem_pay_beta_notification_title">Tangem Pay is now in beta</string>
<string name="tangem_pay_card_frozen">Card frozen</string>
<string name="tangem_pay_card_payment">Card payment</string>
<string name="tangem_pay_deposit">Deposit</string>

View file

@ -61,7 +61,6 @@ fun NetworkTitle(
) {
Box(
modifier = Modifier
.weight(weight = 1f)
.heightIn(min = TangemTheme.dimens.size20),
contentAlignment = Alignment.CenterStart,
content = title,
@ -99,4 +98,4 @@ private fun NetworkTitlePreview(@PreviewParameter(NetworkTitleIconVisibilityProv
}
}
private object NetworkTitleIconVisibilityProvider : CollectionPreviewParameterProvider<Boolean>(listOf(true, false))
private class NetworkTitleIconVisibilityProvider : CollectionPreviewParameterProvider<Boolean>(listOf(true, false))

View file

@ -124,4 +124,4 @@ private fun NetworkTitleItemPreview(@PreviewParameter(GroupTitleItemProvider::cl
}
}
private object GroupTitleItemProvider : CollectionPreviewParameterProvider<Boolean>(collection = listOf(true, false))
private class GroupTitleItemProvider : CollectionPreviewParameterProvider<Boolean>(collection = listOf(true, false))

View file

@ -1,17 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="26dp"
android:height="16dp"
android:viewportWidth="26"
android:viewportHeight="16">
<path
android:pathData="M3,0L23,0A3,3 0,0 1,26 3L26,13A3,3 0,0 1,23 16L3,16A3,3 0,0 1,0 13L0,3A3,3 0,0 1,3 0z"
android:strokeAlpha="0.5"
android:fillColor="#474E71"
android:fillAlpha="0.4"/>
<path
android:pathData="M7.521,11.139H5.788L4.488,6.193C4.427,5.966 4.296,5.765 4.103,5.67C3.622,5.431 3.093,5.242 2.515,5.146V4.956H5.306C5.692,4.956 5.981,5.242 6.029,5.574L6.703,9.142L8.435,4.956H10.12L7.521,11.139ZM11.082,11.139H9.445L10.793,4.956H12.429L11.082,11.139ZM14.549,6.668C14.597,6.335 14.886,6.145 15.223,6.145C15.753,6.097 16.33,6.192 16.811,6.43L17.1,5.099C16.619,4.908 16.089,4.813 15.608,4.813C14.02,4.813 12.864,5.669 12.864,6.858C12.864,7.762 13.683,8.237 14.261,8.523C14.886,8.808 15.127,8.998 15.078,9.284C15.078,9.712 14.597,9.902 14.116,9.902C13.538,9.902 12.96,9.759 12.431,9.521L12.142,10.853C12.72,11.091 13.345,11.186 13.924,11.186C15.705,11.233 16.811,10.378 16.811,9.093C16.811,7.476 14.549,7.381 14.549,6.668ZM22.54,11.139L21.241,4.956H19.845C19.556,4.956 19.267,5.146 19.171,5.431L16.764,11.139H18.449L18.785,10.236H20.855L21.048,11.139H22.54ZM20.083,6.621L20.564,8.951H19.216L20.083,6.621Z"
android:strokeAlpha="0.5"
android:fillColor="#ffffff"
android:fillType="evenOdd"
android:fillAlpha="0.5"/>
</vector>

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

View file

@ -155,9 +155,10 @@ internal class DefaultCurrencyChecksRepository(
override suspend fun getRentExemptionError(
userWalletId: UserWalletId,
currencyStatus: CryptoCurrencyStatus,
currencyStatus: CryptoCurrencyStatus?,
balanceAfterTransaction: BigDecimal,
): CryptoCurrencyWarning.Rent? {
if (currencyStatus == null) return null
val rentData = walletManagersFacade.getRentInfo(userWalletId, currencyStatus.currency.network) ?: return null
return when {
balanceAfterTransaction.isZero() -> null

View file

@ -0,0 +1,64 @@
package com.tangem.data.pay
import arrow.core.Either
import arrow.core.Either.Companion.catch
import com.squareup.moshi.Moshi
import com.tangem.blockchain.blockchains.ethereum.Chain
import com.tangem.blockchainsdk.utils.ExcludedBlockchains
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.datasource.di.NetworkMoshi
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.pay.TangemPayCryptoCurrencyFactory
import timber.log.Timber
import javax.inject.Inject
private const val TAG = "TangemPay: DefaultTangemPayCryptoCurrencyFactory"
/**
* Custom token parameters. Will be used only for F&F.
*/
private const val TOKEN_ID = "usd-coin"
private const val TOKEN_NAME = "USDC"
private const val TOKEN_CONTRACT_ADDRESS = "0x3c499c542cef5e3811e1192ce70d8cc03d5c3359"
private const val TOKEN_DECIMALS = 6
internal class DefaultTangemPayCryptoCurrencyFactory @Inject constructor(
@NetworkMoshi moshi: Moshi,
excludedBlockchains: ExcludedBlockchains,
) : TangemPayCryptoCurrencyFactory {
private val cryptoCurrencyFactory by lazy(mode = LazyThreadSafetyMode.NONE) {
CryptoCurrencyFactory(excludedBlockchains)
}
private val networkFactory by lazy(mode = LazyThreadSafetyMode.NONE) {
NetworkFactory(excludedBlockchains)
}
private val errorConverter by lazy(mode = LazyThreadSafetyMode.NONE) { TangemPayErrorConverter(moshi) }
override fun create(userWallet: UserWallet, chainId: Int): Either<UniversalError, CryptoCurrency> {
return catch {
val chain = requireNotNull(Chain.entries.find { it.id == chainId }) { "Can not find chain with $chainId" }
val blockchain = requireNotNull(chain.blockchain)
val network = networkFactory.create(
blockchain = blockchain,
extraDerivationPath = null,
userWallet = userWallet,
)
cryptoCurrencyFactory.createToken(
network = requireNotNull(network),
rawId = CryptoCurrency.RawID(TOKEN_ID),
name = TOKEN_NAME,
symbol = TOKEN_NAME,
contractAddress = TOKEN_CONTRACT_ADDRESS,
decimals = TOKEN_DECIMALS,
)
}.mapLeft { exception ->
Timber.tag(TAG).e(exception)
errorConverter.convert(exception)
}
}
}

View file

@ -1,85 +0,0 @@
package com.tangem.data.pay
import arrow.core.Either
import arrow.core.Either.Companion.catch
import com.squareup.moshi.Moshi
import com.tangem.blockchain.blockchains.ethereum.Chain
import com.tangem.blockchainsdk.utils.ExcludedBlockchains
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.datasource.di.NetworkMoshi
import com.tangem.domain.models.ReceiveAddressModel
import com.tangem.domain.models.ReceiveAddressModel.NameService
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.pay.TangemPayTopUpData
import com.tangem.domain.pay.TangemPaySwapDataFactory
import timber.log.Timber
import java.math.BigDecimal
import javax.inject.Inject
private const val TAG = "TangemPay: DefaultDataForTopUpFactory"
/**
* Custom token parameters. Will be used only for F&F.
*/
private const val TOKEN_ID = "usd-coin"
private const val TOKEN_NAME = "USDC"
private const val TOKEN_CONTRACT_ADDRESS = "0x3c499c542cef5e3811e1192ce70d8cc03d5c3359"
private const val TOKEN_DECIMALS = 6
internal class DefaultTangemPaySwapDataFactory @Inject constructor(
@NetworkMoshi moshi: Moshi,
excludedBlockchains: ExcludedBlockchains,
) : TangemPaySwapDataFactory {
private val cryptoCurrencyFactory by lazy(mode = LazyThreadSafetyMode.NONE) {
CryptoCurrencyFactory(excludedBlockchains)
}
private val networkFactory by lazy(mode = LazyThreadSafetyMode.NONE) {
NetworkFactory(excludedBlockchains)
}
private val errorConverter by lazy(mode = LazyThreadSafetyMode.NONE) { TangemPayErrorConverter(moshi) }
private fun getCurrency(userWallet: UserWallet, chainId: Int): CryptoCurrency {
val chain = requireNotNull(Chain.entries.find { it.id == chainId }) { "Can not find chain with $chainId" }
val blockchain = requireNotNull(chain.blockchain)
val network = networkFactory.create(
blockchain = blockchain,
extraDerivationPath = null,
userWallet = userWallet,
)
return cryptoCurrencyFactory.createToken(
network = requireNotNull(network),
rawId = CryptoCurrency.RawID(TOKEN_ID),
name = TOKEN_NAME,
symbol = TOKEN_NAME,
contractAddress = TOKEN_CONTRACT_ADDRESS,
decimals = TOKEN_DECIMALS,
)
}
override fun create(
userWallet: UserWallet,
depositAddress: String,
chainId: Int,
cryptoBalance: BigDecimal,
fiatBalance: BigDecimal,
): Either<UniversalError, TangemPayTopUpData> {
return catch {
val currency = getCurrency(userWallet, chainId)
TangemPayTopUpData(
currency = currency,
walletId = userWallet.walletId,
cryptoBalance = cryptoBalance,
fiatBalance = fiatBalance,
depositAddress = depositAddress,
receiveAddress = listOf(ReceiveAddressModel(nameService = NameService.Default, value = depositAddress)),
)
}.mapLeft { exception ->
Timber.tag(TAG).e(exception)
errorConverter.convert(exception)
}
}
}

View file

@ -1,10 +1,10 @@
package com.tangem.data.pay.di
import com.tangem.data.pay.DefaultTangemPaySwapDataFactory
import com.tangem.data.pay.DefaultTangemPayCryptoCurrencyFactory
import com.tangem.data.pay.repository.*
import com.tangem.data.pay.usecase.DefaultGetTangemPayCurrencyStatusUseCase
import com.tangem.data.pay.usecase.DefaultTangemPayWithdrawUseCase
import com.tangem.domain.pay.TangemPaySwapDataFactory
import com.tangem.domain.pay.TangemPayCryptoCurrencyFactory
import com.tangem.domain.pay.repository.*
import com.tangem.domain.pay.usecase.ProduceTangemPayInitialDataUseCase
import com.tangem.domain.pay.usecase.TangemPayMainScreenCustomerInfoUseCase
@ -48,7 +48,9 @@ internal interface TangemPayDataModule {
@Binds
@Singleton
fun bindTangemPaySwapDataFactory(factory: DefaultTangemPaySwapDataFactory): TangemPaySwapDataFactory
fun bindTangemPayCryptoCurrencyFactory(
factory: DefaultTangemPayCryptoCurrencyFactory,
): TangemPayCryptoCurrencyFactory
@Binds
@Singleton

View file

@ -55,6 +55,7 @@ internal class DefaultTangemPayCardDetailsRepository @Inject constructor(
fiatBalance = result.fiat.availableBalance,
currencyCode = result.fiat.currency,
cryptoBalance = result.crypto.balance,
availableForWithdrawal = result.availableForWithdrawal.amount,
chainId = result.crypto.chainId,
depositAddress = result.crypto.depositAddress,
contractAddress = result.crypto.tokenContractAddress,

View file

@ -78,5 +78,7 @@ sealed interface FeedbackEmailType {
val providerName: String,
val txId: String,
) : Visa()
data class FeatureIsBeta(override val walletMetaInfo: WalletMetaInfo) : Visa()
}
}

View file

@ -49,6 +49,10 @@ internal class FeedbackDataBuilder {
builder.appendKeyValue("Total saved wallets", userWalletsInfo.totalUserWallets.toString())
}
fun addUserWalletId(userWalletId: String) {
builder.appendKeyValue("User Wallet ID", userWalletId)
}
fun addUserWalletMetaInfo(walletMetaInfo: WalletMetaInfo) {
builder.appendKeyValue("Mobile Wallet is backed up", walletMetaInfo.hotWalletIsBackedUp?.toString())
builder.appendKeyValue("Card ID", walletMetaInfo.cardId)

View file

@ -65,6 +65,7 @@ class SendFeedbackEmailUseCase(
is FeedbackEmailType.CardAttestationFailed,
is FeedbackEmailType.Visa.Dispute,
is FeedbackEmailType.Visa.DisputeV2,
is FeedbackEmailType.Visa.FeatureIsBeta,
-> this
is FeedbackEmailType.DirectUserRequest,
is FeedbackEmailType.RateCanBeBetter,

View file

@ -38,6 +38,7 @@ internal class EmailMessageBodyResolver(
is FeedbackEmailType.Visa.Dispute -> addVisaRequestBody(type.walletMetaInfo, type.visaTxDetails)
is FeedbackEmailType.Visa.DisputeV2 -> addTangemPayRequestBody(type.walletMetaInfo, type.item)
is FeedbackEmailType.Visa.Withdrawal -> addTangemPayWithdrawalRequestBody(type)
is FeedbackEmailType.Visa.FeatureIsBeta -> addTangemPayBetaRequestBody(type.walletMetaInfo)
}
return build()
@ -52,6 +53,15 @@ internal class EmailMessageBodyResolver(
addTangemPayTxInfo(item)
}
private fun FeedbackDataBuilder.addTangemPayBetaRequestBody(walletMetaInfo: WalletMetaInfo) {
addPhoneInfoBody()
addDelimiter()
walletMetaInfo.userWalletId?.let { userWalletId ->
addUserWalletId(userWalletId = userWalletId.stringValue)
addDelimiter()
}
}
private suspend fun FeedbackDataBuilder.addTangemPayWithdrawalRequestBody(
type: FeedbackEmailType.Visa.Withdrawal,
) {

View file

@ -26,6 +26,8 @@ internal class EmailMessageTitleResolver(private val resources: Resources) {
is FeedbackEmailType.Visa.DisputeV2,
is FeedbackEmailType.Visa.FailedIssueCard,
is FeedbackEmailType.Visa.Withdrawal,
is FeedbackEmailType.Visa.FeatureIsBeta,
is FeedbackEmailType.PreActivatedWallet,
-> R.string.feedback_preface_support
is FeedbackEmailType.RateCanBeBetter -> R.string.feedback_preface_rate_negative
is FeedbackEmailType.ScanningProblem -> R.string.feedback_preface_scan_failed
@ -33,7 +35,6 @@ internal class EmailMessageTitleResolver(private val resources: Resources) {
is FeedbackEmailType.StakingProblem,
is FeedbackEmailType.SwapProblem,
-> R.string.feedback_preface_tx_failed
is FeedbackEmailType.PreActivatedWallet -> R.string.feedback_preface_support
}
return resources.getStringSafe(resId)

View file

@ -15,6 +15,7 @@ import com.tangem.domain.feedback.models.FeedbackEmailType
internal class EmailSubjectResolver(private val resources: Resources) {
/** Resolve email message body by [type] */
@Suppress("CyclomaticComplexMethod")
fun resolve(type: FeedbackEmailType): String {
return when (type) {
is FeedbackEmailType.DirectUserRequest -> {
@ -43,8 +44,8 @@ internal class EmailSubjectResolver(private val resources: Resources) {
is FeedbackEmailType.Visa.Dispute,
is FeedbackEmailType.Visa.DisputeV2,
-> "[Visa] [DISPUTE] {auto-filled subject}"
is FeedbackEmailType.Visa.Withdrawal,
-> "[Visa] [WITHDRAWAL] {auto-filled subject}"
is FeedbackEmailType.Visa.Withdrawal -> "[Visa] [WITHDRAWAL] {auto-filled subject}"
is FeedbackEmailType.Visa.FeatureIsBeta -> "[VISA] [FEEDBACK]"
}
}
}

View file

@ -14,9 +14,11 @@ class GetCurrencyCheckUseCase(
private val dispatchers: CoroutineDispatcherProvider,
) {
@Suppress("LongParameterList")
suspend operator fun invoke(
userWalletId: UserWalletId,
currencyStatus: CryptoCurrencyStatus,
feeCurrencyStatus: CryptoCurrencyStatus?,
amount: BigDecimal?,
fee: BigDecimal?,
feeCurrencyBalanceAfterTransaction: BigDecimal?,
@ -31,7 +33,7 @@ class GetCurrencyCheckUseCase(
val existentialDeposit = currencyChecksRepository.getExistentialDeposit(userWalletId, network)
val rentWarning = currencyChecksRepository.getRentExemptionError(
userWalletId = userWalletId,
currencyStatus = currencyStatus,
currencyStatus = feeCurrencyStatus,
balanceAfterTransaction = feeCurrencyBalanceAfterTransaction ?: BigDecimal.ZERO,
)
val isAccountFunded = recipientAddress?.let {

View file

@ -58,7 +58,7 @@ interface CurrencyChecksRepository {
*/
suspend fun getRentExemptionError(
userWalletId: UserWalletId,
currencyStatus: CryptoCurrencyStatus,
currencyStatus: CryptoCurrencyStatus?,
balanceAfterTransaction: BigDecimal,
): CryptoCurrencyWarning.Rent?
}

View file

@ -0,0 +1,11 @@
package com.tangem.domain.pay
import arrow.core.Either
import com.tangem.core.error.UniversalError
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.wallet.UserWallet
interface TangemPayCryptoCurrencyFactory {
fun create(userWallet: UserWallet, chainId: Int): Either<UniversalError, CryptoCurrency>
}

View file

@ -6,6 +6,7 @@ data class TangemPayCardBalance(
val fiatBalance: BigDecimal,
val currencyCode: String,
val cryptoBalance: BigDecimal,
val availableForWithdrawal: BigDecimal,
val chainId: Int,
val depositAddress: String?,
val contractAddress: String,

View file

@ -1,24 +1,10 @@
package com.tangem.domain.pay
package com.tangem.domain.pay.model
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,
fiatBalance: BigDecimal,
): Either<UniversalError, TangemPayTopUpData>
}
data class TangemPayTopUpData(
val walletId: UserWalletId,
val currency: CryptoCurrency,

View file

@ -87,7 +87,7 @@ internal class ItemsBuilder @Inject constructor(private val router: Router) {
DetailsItemUM.Basic.Item(
id = "support_email",
block = BlockUM(
text = resourceReference(R.string.details_row_title_contact_to_support),
text = resourceReference(R.string.common_contact_support),
iconRes = R.drawable.ic_comment_24,
onClick = onSupportEmailClick,
),

View file

@ -20,28 +20,26 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import androidx.compose.ui.unit.Dp
import com.tangem.common.ui.charts.state.MarketChartDataProducer
import com.tangem.core.ui.components.*
import com.tangem.core.ui.components.appbar.TangemTopAppBar
import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
import com.tangem.core.ui.components.buttons.segmentedbutton.SegmentedButtons
import com.tangem.core.ui.components.currency.icon.CoinIcon
import com.tangem.core.ui.components.marketprice.PriceChangeInPercent
import com.tangem.core.ui.components.marketprice.PriceChangeType
import com.tangem.core.ui.event.EventEffect
import com.tangem.core.ui.event.StateEvent
import com.tangem.core.ui.event.consumedEvent
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.domain.markets.PriceChangeInterval
import com.tangem.features.markets.details.impl.ui.components.*
import com.tangem.features.markets.details.impl.ui.preview.MarketsTokenDetailsPreview
import com.tangem.features.markets.details.impl.ui.state.ExchangesBottomSheetContent
import com.tangem.features.markets.details.impl.ui.state.InfoBottomSheetContent
import com.tangem.features.markets.details.impl.ui.state.MarketsTokenDetailsUM
@ -317,36 +315,16 @@ fun PriceChangeInterval.getText(): TextReference {
}
}
@Preview
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
// region Preview
@Composable
private fun Preview() {
@Preview(showBackground = true, widthDp = 360)
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
private fun MarketsTokenDetailsContent_Preview(
@PreviewParameter(MarketsTokenDetailsContentPreviewProvider::class) params: MarketsTokenDetailsUM,
) {
TangemThemePreview {
MarketsTokenDetailsContent(
state = MarketsTokenDetailsUM(
tokenName = "Token Name",
priceText = "$0.00000000324",
dateTimeText = stringReference("Today"),
priceChangePercentText = "52.00%",
iconUrl = "",
priceChangeType = PriceChangeType.UP,
chartState = MarketsTokenDetailsUM.ChartState(
dataProducer = MarketChartDataProducer.build { },
onLoadRetryClick = {},
status = MarketsTokenDetailsUM.ChartState.Status.LOADING,
onMarkerPointSelected = { _, _ -> },
),
selectedInterval = PriceChangeInterval.H24,
onSelectedIntervalChange = { },
body = MarketsTokenDetailsUM.Body.Loading,
bottomSheetConfig = TangemBottomSheetConfig(
isShown = false,
onDismissRequest = {},
content = TangemBottomSheetConfigContent.Empty,
),
markerSet = false,
triggerPriceChange = consumedEvent(),
),
state = params,
onHeaderSizeChange = {},
onBackClick = {},
backgroundColor = TangemTheme.colors.background.tertiary,
@ -356,4 +334,13 @@ private fun Preview() {
addTopBarStatusBarPadding = false,
)
}
}
}
private class MarketsTokenDetailsContentPreviewProvider : PreviewParameterProvider<MarketsTokenDetailsUM> {
override val values: Sequence<MarketsTokenDetailsUM>
get() = sequenceOf(
MarketsTokenDetailsPreview.loadingState,
MarketsTokenDetailsPreview.contentState,
)
}
// endregion

View file

@ -8,6 +8,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.components.RectangleShimmer
import com.tangem.core.ui.components.block.information.GridItems
import com.tangem.core.ui.components.block.information.InformationBlock
@ -51,12 +52,16 @@ internal fun InsightsBlock(state: InsightsUM, modifier: Modifier = Modifier) {
currentInterval = it
state.onIntervalChanged(it)
},
modifier = Modifier.width(IntrinsicSize.Min),
) {
Box(
Modifier
.fillMaxSize()
.align(Alignment.Center)
.padding(vertical = TangemTheme.dimens.spacing4),
.padding(
horizontal = 14.dp,
vertical = 4.dp,
),
) {
Text(
modifier = Modifier.align(Alignment.Center),

View file

@ -57,12 +57,16 @@ internal fun PricePerformanceBlock(state: PricePerformanceUM, modifier: Modifier
currentInterval = it
state.onIntervalChanged(it)
},
modifier = Modifier.width(IntrinsicSize.Min),
) {
Box(
Modifier
.fillMaxSize()
.align(Alignment.Center)
.padding(vertical = TangemTheme.dimens.spacing4),
.padding(
horizontal = 14.dp,
vertical = TangemTheme.dimens.spacing4,
),
) {
Text(
modifier = Modifier.align(Alignment.Center),

View file

@ -0,0 +1,129 @@
package com.tangem.features.markets.details.impl.ui.preview
import com.tangem.common.ui.charts.state.MarketChartDataProducer
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
import com.tangem.core.ui.components.marketprice.PriceChangeType
import com.tangem.core.ui.event.consumedEvent
import com.tangem.core.ui.extensions.stringReference
import com.tangem.domain.markets.PriceChangeInterval
import com.tangem.features.markets.details.impl.ui.state.*
import kotlinx.collections.immutable.persistentListOf
internal object MarketsTokenDetailsPreview {
private val infoPoint = InfoPointUM(
title = stringReference("1"),
value = "2",
change = InfoPointUM.ChangeType.DOWN,
onInfoClick = {},
)
val loadingState = MarketsTokenDetailsUM(
tokenName = "Token Name",
priceText = "$0.00000000324",
dateTimeText = stringReference("Today"),
priceChangePercentText = "52.00%",
iconUrl = "",
priceChangeType = PriceChangeType.UP,
chartState = MarketsTokenDetailsUM.ChartState(
dataProducer = MarketChartDataProducer.build { },
onLoadRetryClick = {},
status = MarketsTokenDetailsUM.ChartState.Status.LOADING,
onMarkerPointSelected = { _, _ -> },
),
selectedInterval = PriceChangeInterval.H24,
onSelectedIntervalChange = { },
body = MarketsTokenDetailsUM.Body.Loading,
bottomSheetConfig = TangemBottomSheetConfig(
isShown = false,
onDismissRequest = {},
content = TangemBottomSheetConfigContent.Empty,
),
markerSet = false,
triggerPriceChange = consumedEvent(),
)
val contentState = MarketsTokenDetailsUM(
tokenName = "Token Name",
priceText = "$0.00000000324",
dateTimeText = stringReference("Today"),
priceChangePercentText = "52.00%",
iconUrl = "",
priceChangeType = PriceChangeType.UP,
chartState = MarketsTokenDetailsUM.ChartState(
dataProducer = MarketChartDataProducer.build { },
onLoadRetryClick = {},
status = MarketsTokenDetailsUM.ChartState.Status.LOADING,
onMarkerPointSelected = { _, _ -> },
),
selectedInterval = PriceChangeInterval.H24,
onSelectedIntervalChange = { },
body = MarketsTokenDetailsUM.Body.Content(
description = MarketsTokenDetailsUM.Description(
shortDescription = stringReference("markets_token_details_description_short"),
fullDescription = stringReference("markets_token_details_description_full"),
onReadMoreClick = {},
),
infoBlocks = MarketsTokenDetailsUM.InformationBlocks(
insights = InsightsUM(
h24Info = persistentListOf(
infoPoint,
infoPoint,
infoPoint,
),
weekInfo = persistentListOf(
infoPoint,
infoPoint,
infoPoint,
),
monthInfo = persistentListOf(
infoPoint,
infoPoint,
infoPoint,
),
onInfoClick = {},
onIntervalChanged = {},
),
securityScore = SecurityScoreUM(
score = 2.3f,
description = stringReference("markets_token_details_security_score_description"),
onInfoClick = {},
),
metrics = MetricsUM(
metrics = persistentListOf(
infoPoint,
infoPoint,
infoPoint,
),
),
pricePerformance = PricePerformanceUM(
h24 = PricePerformanceUM.Value(
low = "1",
high = "2",
indicatorFraction = 0.3f,
),
month = PricePerformanceUM.Value(
low = "1",
high = "2",
indicatorFraction = 0.3f,
),
all = PricePerformanceUM.Value(
low = "1",
high = "2",
indicatorFraction = 0.3f,
),
onIntervalChanged = {},
),
listedOn = ListedOnUM.Empty,
links = null,
),
),
bottomSheetConfig = TangemBottomSheetConfig(
isShown = false,
onDismissRequest = {},
content = TangemBottomSheetConfigContent.Empty,
),
markerSet = true,
triggerPriceChange = consumedEvent(),
)
}

View file

@ -17,14 +17,15 @@ internal class OnrampOffersStateFactory(
fun getOffersState(offers: List<OnrampOffersBlock>): OnrampV2MainComponentUM {
val currentState = currentStateProvider.invoke()
return when (currentState) {
is OnrampV2MainComponentUM.InitialLoading -> currentState
is OnrampV2MainComponentUM.Content -> {
if (currentState.offersBlockState is OnrampOffersBlockUM.Loading && offers.isEmpty()) {
return currentState
}
currentState.copy(
offersBlockState = mapOnrampOffersBlockToUM(offersBlocks = offers),
)
}
is OnrampV2MainComponentUM.InitialLoading -> {
currentState
}
}
}

View file

@ -64,6 +64,7 @@ internal class OnrampV2AmountStateFactory(
currencySymbol = currency.unit,
onAmountValueChanged = onrampIntents::onAmountValueChanged,
),
offersBlockState = OnrampOffersBlockUM.Loading,
)
}

View file

@ -523,6 +523,7 @@ internal class SendConfirmModel @Inject constructor(
params.callback.onResult(
state.copy(
navigationUM = NavigationUM.Content(
source = CommonSendRoute.Confirm.javaClass.simpleName,
title = resourceReference(id = R.string.common_send),
subtitle = null,
backIconRes = when (confirmUM) {

View file

@ -52,6 +52,7 @@ internal class SendConfirmSuccessModel @Inject constructor(
params.callback.onResult(
state.copy(
navigationUM = NavigationUM.Content(
source = CommonSendRoute.ConfirmSuccess.javaClass.simpleName,
title = stringReference(""),
subtitle = null,
backIconRes = R.drawable.ic_close_24,

View file

@ -371,11 +371,12 @@ internal class NFTSendConfirmModel @Inject constructor(
flow = uiState,
flow2 = params.currentRoute,
transform = { state, route -> state to route },
).onEach { (state, _) ->
).onEach { (state, route) ->
val confirmUM = state.confirmUM
params.callback.onResult(
state.copy(
navigationUM = NavigationUM.Content(
source = CommonSendRoute.Confirm.javaClass.simpleName,
title = resourceReference(R.string.nft_send),
subtitle = null,
backIconRes = when (confirmUM) {

View file

@ -50,6 +50,7 @@ internal class NFTSendSuccessModel @Inject constructor(
params.callback.onResult(
state.copy(
navigationUM = NavigationUM.Content(
source = CommonSendRoute.ConfirmSuccess.javaClass.simpleName,
title = stringReference(""),
subtitle = null,
backIconRes = R.drawable.ic_close_24,

View file

@ -378,6 +378,7 @@ internal class SendAmountModel @Inject constructor(
setSendWithSwapAvailability()
params.callback.onNavigationResult(
NavigationUM.Content(
source = CommonSendRoute.Amount::class.java.simpleName,
title = resourceReference(R.string.send_amount_label),
subtitle = null,
backIconRes = if (route.isEditMode) {

View file

@ -35,6 +35,7 @@ import com.tangem.features.send.v2.api.entity.PredefinedValues
import com.tangem.features.send.v2.api.subcomponents.destination.SendDestinationComponentParams
import com.tangem.features.send.v2.api.subcomponents.destination.SendDestinationComponentParams.DestinationBlockParams
import com.tangem.features.send.v2.api.subcomponents.destination.entity.DestinationUM
import com.tangem.features.send.v2.common.CommonSendRoute
import com.tangem.features.send.v2.impl.R
import com.tangem.features.send.v2.subcomponents.destination.analytics.EnterAddressSource
import com.tangem.features.send.v2.subcomponents.destination.analytics.SendDestinationAnalyticEvents
@ -359,6 +360,7 @@ internal class SendDestinationModel @Inject constructor(
).onEach { (state, route) ->
params.callback.onNavigationResult(
NavigationUM.Content(
source = CommonSendRoute.Destination::class.java.simpleName,
title = params.title,
subtitle = null,
backIconRes = if (route.isEditMode) {

View file

@ -136,6 +136,7 @@ internal class NotificationsModel @Inject constructor(
val currencyCheck = getCurrencyCheckUseCase(
userWalletId = userWalletId,
currencyStatus = cryptoCurrencyStatus,
feeCurrencyStatus = feeCryptoCurrencyStatus,
amount = sendingAmount,
fee = feeValue,
recipientAddress = destinationAddress,

View file

@ -772,6 +772,7 @@ internal class StakingModel @Inject constructor(
val currencyStatus = getCurrencyCheckUseCase(
userWalletId = userWalletId,
currencyStatus = cryptoCurrencyStatus,
feeCurrencyStatus = feeCryptoCurrencyStatus,
amount = amount,
fee = fee,
feeCurrencyBalanceAfterTransaction = balanceAfterTransaction,

View file

@ -720,6 +720,7 @@ internal class SwapAmountModel @Inject constructor(
).filter { (_, route) -> route is SendWithSwapRoute.Amount }.onEach { (state, route) ->
params.callback.onNavigationResult(
NavigationUM.Content(
source = SendWithSwapRoute.Amount::class.java.simpleName,
title = resourceReference(R.string.common_amount),
subtitle = null,
backIconRes = if (route.isEditMode) {

View file

@ -428,6 +428,7 @@ internal class SendWithSwapConfirmModel @Inject constructor(
route = SendWithSwapRoute.Confirm,
sendWithSwapUM = state.copy(
navigationUM = NavigationUM.Content(
source = SendWithSwapRoute.Confirm.javaClass.simpleName,
title = resourceReference(id = R.string.send_with_swap_confirm_title),
subtitle = null,
backIconRes = R.drawable.ic_back_24,

View file

@ -12,6 +12,7 @@ import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.features.swap.v2.impl.R
import com.tangem.features.swap.v2.impl.common.entity.ConfirmUM
import com.tangem.features.swap.v2.impl.sendviaswap.SendWithSwapRoute
import com.tangem.features.swap.v2.impl.sendviaswap.entity.SendWithSwapUM
import com.tangem.features.swap.v2.impl.sendviaswap.success.SendWithSwapSuccessComponent
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
@ -40,6 +41,7 @@ internal class SendWithSwapSuccessModel @Inject constructor(
private fun configConfirmSuccessNavigation() {
params.callback.onNavigationResult(
NavigationUM.Content(
source = SendWithSwapRoute.Success.javaClass.simpleName,
title = TextReference.EMPTY,
subtitle = null,
backIconRes = R.drawable.ic_close_24,

View file

@ -55,6 +55,7 @@ import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountUM
import com.tangem.features.swap.v2.impl.amount.ui.preview.SwapAmountContentPreview
import com.tangem.features.swap.v2.impl.common.entity.ConfirmUM
import com.tangem.features.swap.v2.impl.common.entity.SwapQuoteUM
import com.tangem.features.swap.v2.impl.sendviaswap.SendWithSwapRoute
import com.tangem.features.swap.v2.impl.sendviaswap.entity.SendWithSwapUM
import kotlinx.collections.immutable.persistentListOf
import java.math.BigDecimal
@ -368,6 +369,7 @@ private fun SendWithSwapSuccessContent_Preview() {
),
),
navigationUM = NavigationUM.Content(
source = SendWithSwapRoute.Success.javaClass.simpleName,
title = TextReference.EMPTY,
subtitle = null,
backIconRes = R.drawable.ic_close_24,

View file

@ -92,6 +92,7 @@ internal class SwapInteractorImpl @AssistedInject constructor(
private val amountFormatter: AmountFormatter,
private val rampStateManager: RampStateManager,
private val singleAccountStatusListSupplier: SingleAccountStatusListSupplier,
private val getFeePaidCryptoCurrencyStatusSyncUseCase: GetFeePaidCryptoCurrencyStatusSyncUseCase,
private val accountsFeatureToggles: AccountsFeatureToggles,
@Assisted private val userWalletId: UserWalletId,
) : SwapInteractor {
@ -631,9 +632,14 @@ internal class SwapInteractorImpl @AssistedInject constructor(
} else {
amount
}
val feePaidCurrencyStatus = getFeePaidCryptoCurrencyStatusSyncUseCase(
userWalletId = userWalletId,
cryptoCurrencyStatus = fromTokenStatus,
).getOrNull()
val currencyCheck = getCurrencyCheckUseCase(
userWalletId = userWalletId,
currencyStatus = fromTokenStatus,
feeCurrencyStatus = feePaidCurrencyStatus,
amount = amountToRequest.value,
fee = fee,
feeCurrencyBalanceAfterTransaction = balanceAfterTransaction,

View file

@ -5,7 +5,7 @@ import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.core.ui.decompose.ComposableBottomSheetComponent
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.pay.TangemPayTopUpData
import com.tangem.domain.pay.model.TangemPayTopUpData
import com.tangem.features.tangempay.model.TangemPayAddFundsModel
import com.tangem.features.tangempay.ui.TangemPayAddFundsContent
import java.math.BigDecimal

View file

@ -1,8 +1,10 @@
package com.tangem.features.tangempay.entity
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.components.buttons.actions.ActionButtonConfig
import com.tangem.core.ui.components.containers.pullToRefresh.PullToRefreshConfig
import com.tangem.core.ui.components.dropdownmenu.TangemDropdownMenuItem
import com.tangem.core.ui.components.notifications.NotificationConfig
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.themedColor
import com.tangem.core.ui.res.TangemTheme
@ -21,6 +23,7 @@ internal class TangemPayDetailsStateFactory(
private val converter: TangemPayCardFrozenStateConverter,
) {
@Suppress("LongMethod")
fun getInitialState(): TangemPayDetailsUM {
val cardFrozenStateItem = when (cardFrozenState) {
is TangemPayCardFrozenState.Pending -> null
@ -88,6 +91,16 @@ internal class TangemPayDetailsStateFactory(
isBalanceHidden = false,
addFundsEnabled = true,
cardFrozenState = converter.convert(cardFrozenState),
betaNotificationConfig = NotificationConfig(
title = resourceReference(R.string.tangem_pay_beta_notification_title),
subtitle = resourceReference(R.string.tangem_pay_beta_notification_subtitle),
iconResId = R.drawable.img_visa_notification,
buttonsState = NotificationConfig.ButtonsState.SecondaryButtonConfig(
text = resourceReference(R.string.common_contact_support),
onClick = intents::onContactSupportClicked,
),
iconSize = 36.dp,
),
)
}
}

View file

@ -2,6 +2,7 @@ package com.tangem.features.tangempay.entity
import com.tangem.core.ui.components.buttons.actions.ActionButtonConfig
import com.tangem.core.ui.components.containers.pullToRefresh.PullToRefreshConfig
import com.tangem.core.ui.components.notifications.NotificationConfig
import com.tangem.core.ui.extensions.TextReference
import com.tangem.domain.visa.model.TangemPayCardFrozenState
import kotlinx.collections.immutable.ImmutableList
@ -14,6 +15,7 @@ internal data class TangemPayDetailsUM(
val isBalanceHidden: Boolean,
val addFundsEnabled: Boolean,
val cardFrozenState: CardFrozenState,
val betaNotificationConfig: NotificationConfig,
)
internal data class TangemPayCardDetailsUM(

View file

@ -4,7 +4,10 @@ import androidx.compose.runtime.Stable
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.models.ReceiveAddressModel
import com.tangem.domain.models.ReceiveAddressModel.NameService
import com.tangem.domain.pay.TangemPayCryptoCurrencyFactory
import com.tangem.domain.pay.model.TangemPayTopUpData
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
import com.tangem.features.tangempay.components.TangemPayAddFundsComponent
import com.tangem.features.tangempay.entity.TangemPayAddFundsUM
@ -17,7 +20,7 @@ import javax.inject.Inject
internal class TangemPayAddFundsModel @Inject constructor(
paramsContainer: ParamsContainer,
override val dispatchers: CoroutineDispatcherProvider,
private val tangemPaySwapDataFactory: TangemPaySwapDataFactory,
private val tangemPayCryptoCurrencyFactory: TangemPayCryptoCurrencyFactory,
private val getUserWalletUseCase: GetUserWalletUseCase,
) : Model() {
@ -26,17 +29,25 @@ 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,
fiatBalance = params.fiatBalance,
).getOrNull()
val userWallet = getUserWalletUseCase(params.walletId).getOrNull()
val currency = userWallet?.let {
tangemPayCryptoCurrencyFactory.create(userWallet = userWallet, chainId = params.chainId).getOrNull()
}
val data = currency?.let {
TangemPayTopUpData(
currency = currency,
walletId = params.walletId,
cryptoBalance = params.cryptoBalance,
fiatBalance = params.fiatBalance,
depositAddress = params.depositAddress,
receiveAddress = listOf(
ReceiveAddressModel(
nameService = NameService.Default,
value = params.depositAddress,
),
),
)
}
return TangemPayAddFundsUMConverter(listener = params.listener).convert(data)
}

View file

@ -17,10 +17,13 @@ import com.tangem.core.ui.components.containers.pullToRefresh.PullToRefreshConfi
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.message.SnackbarMessage
import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase
import com.tangem.domain.feedback.SendFeedbackEmailUseCase
import com.tangem.domain.feedback.models.FeedbackEmailType
import com.tangem.domain.feedback.models.WalletMetaInfo
import com.tangem.domain.models.TokenReceiveConfig
import com.tangem.domain.pay.TangemPaySwapDataFactory
import com.tangem.domain.pay.TangemPayTopUpData
import com.tangem.domain.pay.TangemPayCryptoCurrencyFactory
import com.tangem.domain.pay.model.TangemPayCardBalance
import com.tangem.domain.pay.model.TangemPayTopUpData
import com.tangem.domain.pay.repository.CustomerOrderRepository
import com.tangem.domain.pay.repository.TangemPayCardDetailsRepository
import com.tangem.domain.tangempay.TangemPayAnalyticsEvents
@ -70,9 +73,10 @@ internal class TangemPayDetailsModel @Inject constructor(
private val uiMessageSender: UiMessageSender,
private val cardDetailsEventListener: CardDetailsEventListener,
private val txHistoryUpdateListener: TangemPayTxHistoryUpdateListener,
private val tangemPaySwapDataFactory: TangemPaySwapDataFactory,
private val tangemPayCryptoCurrencyFactory: TangemPayCryptoCurrencyFactory,
private val orderRepository: CustomerOrderRepository,
private val getUserWalletUseCase: GetUserWalletUseCase,
private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase,
) : Model(), TangemPayTxHistoryUiActions, TangemPayDetailIntents, AddFundsListener {
private val params: TangemPayDetailsContainerComponent.Params = paramsContainer.require()
@ -236,27 +240,22 @@ 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,
fiatBalance = currentBalance.fiatBalance,
).getOrNull()
if (data != null) {
val userWallet = getUserWalletUseCase(params.userWalletId).getOrNull()
val currency = userWallet?.let {
tangemPayCryptoCurrencyFactory.create(userWallet = userWallet, chainId = params.config.chainId)
.getOrNull()
}
if (currency != null) {
router.push(
AppRoute.Swap(
currencyFrom = data.currency,
userWalletId = data.walletId,
currencyFrom = currency,
userWalletId = params.userWalletId,
isInitialReverseOrder = false,
screenSource = AnalyticsParam.ScreensSources.TangemPay.value,
tangemPayInput = AppRoute.Swap.TangemPayInput(
cryptoAmount = data.cryptoBalance,
fiatAmount = data.fiatBalance,
depositAddress = data.depositAddress,
cryptoAmount = currentBalance.availableForWithdrawal,
fiatAmount = currentBalance.availableForWithdrawal,
depositAddress = depositAddress,
isWithdrawal = true,
),
),
@ -277,7 +276,13 @@ internal class TangemPayDetailsModel @Inject constructor(
Timber.e(e)
return@launch
}
uiState.update(DetailsBalanceTransformer(balance = result))
uiState.update(
transformer = DetailsBalanceTransformer(
balance = result,
userWallet = getUserWalletUseCase(params.userWalletId).getOrNull(),
cryptoCurrencyFactory = tangemPayCryptoCurrencyFactory,
),
)
}.saveIn(fetchBalanceJobHolder)
}
@ -305,6 +310,16 @@ internal class TangemPayDetailsModel @Inject constructor(
}.saveIn(addToWalletBannerJobHolder)
}
override fun onContactSupportClicked() {
modelScope.launch {
sendFeedbackEmailUseCase.invoke(
type = FeedbackEmailType.Visa.FeatureIsBeta(
walletMetaInfo = WalletMetaInfo(userWalletId = params.userWalletId),
),
)
}
}
override fun onRefreshSwipe(refreshState: ShowRefreshState) {
modelScope.launch {
uiState.update(TangemPayDetailsRefreshTransformer(isRefreshing = refreshState.value))

View file

@ -2,17 +2,24 @@ package com.tangem.features.tangempay.model.transformers
import arrow.core.Either
import com.tangem.core.error.UniversalError
import com.tangem.core.ui.format.bigdecimal.crypto
import com.tangem.core.ui.format.bigdecimal.fiat
import com.tangem.core.ui.format.bigdecimal.format
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.pay.TangemPayCryptoCurrencyFactory
import com.tangem.domain.pay.model.TangemPayCardBalance
import com.tangem.features.tangempay.entity.TangemPayDetailsBalanceBlockState
import com.tangem.features.tangempay.entity.TangemPayDetailsUM
import com.tangem.utils.transformer.Transformer
import kotlinx.collections.immutable.persistentListOf
import java.math.BigDecimal
import java.util.Currency
internal class DetailsBalanceTransformer(
private val balance: Either<UniversalError, TangemPayCardBalance>,
private val cryptoCurrencyFactory: TangemPayCryptoCurrencyFactory,
private val userWallet: UserWallet?,
) : Transformer<TangemPayDetailsUM> {
override fun transform(prevState: TangemPayDetailsUM): TangemPayDetailsUM {
@ -21,22 +28,32 @@ internal class DetailsBalanceTransformer(
TangemPayDetailsBalanceBlockState.Error(actionButtons = persistentListOf())
}
is Either.Right<TangemPayCardBalance> -> {
TangemPayDetailsBalanceBlockState.Content(
isBalanceFlickering = false,
fiatBalance = getBalanceText(balance.value),
// TODO [REDACTED_TASK_KEY]: Add crypto balance when the BFF is ready
cryptoBalance = "",
actionButtons = prevState.balanceBlockState.actionButtons,
)
val cryptoCurrency = userWallet?.let {
cryptoCurrencyFactory.create(userWallet, balance.value.chainId).getOrNull()
}
if (cryptoCurrency == null) {
TangemPayDetailsBalanceBlockState.Error(actionButtons = persistentListOf())
} else {
TangemPayDetailsBalanceBlockState.Content(
isBalanceFlickering = false,
fiatBalance = getFiatBalanceText(balance.value),
cryptoBalance = getCryptoBalanceText(balance.value.cryptoBalance, cryptoCurrency),
actionButtons = prevState.balanceBlockState.actionButtons,
)
}
}
}
return prevState.copy(balanceBlockState = balance)
}
private fun getBalanceText(balance: TangemPayCardBalance): String {
private fun getFiatBalanceText(balance: TangemPayCardBalance): String {
val currency = Currency.getInstance(balance.currencyCode)
return balance.fiatBalance.format {
fiat(fiatCurrencyCode = currency.currencyCode, fiatCurrencySymbol = currency.symbol)
}
}
private fun getCryptoBalanceText(cryptoBalance: BigDecimal, cryptoCurrency: CryptoCurrency): String {
return cryptoBalance.format { crypto(cryptoCurrency = cryptoCurrency) }
}
}

View file

@ -1,7 +1,7 @@
package com.tangem.features.tangempay.model.transformers
import com.tangem.core.ui.extensions.TextReference
import com.tangem.domain.pay.TangemPayTopUpData
import com.tangem.domain.pay.model.TangemPayTopUpData
import com.tangem.features.tangempay.components.AddFundsListener
import com.tangem.features.tangempay.details.impl.R
import com.tangem.features.tangempay.entity.TangemPayAddFundsItemUM

View file

@ -31,11 +31,10 @@ import com.tangem.core.ui.components.containers.pullToRefresh.PullToRefreshConfi
import com.tangem.core.ui.components.containers.pullToRefresh.TangemPullToRefreshContainer
import com.tangem.core.ui.components.dropdownmenu.TangemDropdownItem
import com.tangem.core.ui.components.dropdownmenu.TangemDropdownMenu
import com.tangem.core.ui.components.notifications.Notification
import com.tangem.core.ui.components.notifications.NotificationConfig
import com.tangem.core.ui.components.text.applyBladeBrush
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.orMaskWithStars
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.extensions.*
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.test.TokenDetailsTopBarTestTags
@ -81,7 +80,9 @@ internal fun TangemPayDetailsScreen(
) {
item(TangemPayCardDetailsUM::class.java) {
cardDetailsBlockComponent.CardDetailsBlockContent(
modifier = Modifier.padding(horizontal = 16.dp).padding(top = 8.dp),
modifier = Modifier
.padding(horizontal = 16.dp)
.padding(top = 8.dp),
state = cardDetailsState,
)
}
@ -116,7 +117,7 @@ internal fun TangemPayDetailsScreen(
key = TangemPayDetailsBalanceBlockState::class.java,
content = {
TangemPayDetailsBalanceBlock(
modifier = modifier
modifier = Modifier
.padding(horizontal = TangemTheme.dimens.spacing16)
.padding(top = 12.dp)
.fillMaxWidth(),
@ -125,15 +126,32 @@ internal fun TangemPayDetailsScreen(
)
},
)
item(
key = "TANGEM_PAY_IS_IN_BETA",
content = {
TangemPayBetaBlock(
modifier = modifier
.padding(horizontal = TangemTheme.dimens.spacing16)
.padding(top = 12.dp)
.fillMaxWidth(),
config = state.betaNotificationConfig,
)
},
)
with(txHistoryComponent) { txHistoryContent(listState = listState, state = txHistoryState) }
}
}
}
}
@Composable
private fun TangemPayBetaBlock(config: NotificationConfig, modifier: Modifier = Modifier) {
Notification(modifier = modifier, config = config)
}
// region Balance block
@Composable
internal fun TangemPayDetailsBalanceBlock(
private fun TangemPayDetailsBalanceBlock(
state: TangemPayDetailsBalanceBlockState,
isBalanceHidden: Boolean,
modifier: Modifier = Modifier,
@ -157,12 +175,11 @@ internal fun TangemPayDetailsBalanceBlock(
state = state,
isBalanceHidden = isBalanceHidden,
)
// TODO [REDACTED_TASK_KEY]: Uncomment after adding crypto balance when the BFF is ready
// CryptoBalance(
// modifier = Modifier.padding(start = 12.dp, top = 4.dp),
// state = state,
// isBalanceHidden = isBalanceHidden,
// )
CryptoBalance(
modifier = Modifier.padding(start = 12.dp, top = 4.dp),
state = state,
isBalanceHidden = isBalanceHidden,
)
if (state.actionButtons.isNotEmpty()) {
HorizontalActionChips(
modifier = Modifier.padding(top = 12.dp),
@ -336,6 +353,16 @@ private class TangemPayDetailsUMProvider : CollectionPreviewParameterProvider<Ta
isBalanceHidden = false,
addFundsEnabled = true,
cardFrozenState = CardFrozenState.Frozen(onUnfreeze = {}),
betaNotificationConfig = NotificationConfig(
title = resourceReference(R.string.tangem_pay_beta_notification_title),
subtitle = resourceReference(R.string.tangem_pay_beta_notification_subtitle),
iconResId = R.drawable.img_visa_notification,
buttonsState = NotificationConfig.ButtonsState.SecondaryButtonConfig(
text = resourceReference(R.string.common_contact_support),
onClick = {},
),
iconSize = 36.dp,
),
),
TangemPayDetailsUM(
topBarConfig = TangemPayDetailsTopBarConfig(onBackClick = {}, items = null),
@ -345,6 +372,16 @@ private class TangemPayDetailsUMProvider : CollectionPreviewParameterProvider<Ta
isBalanceHidden = false,
addFundsEnabled = true,
cardFrozenState = CardFrozenState.Unfrozen,
betaNotificationConfig = NotificationConfig(
title = resourceReference(R.string.tangem_pay_beta_notification_title),
subtitle = resourceReference(R.string.tangem_pay_beta_notification_subtitle),
iconResId = R.drawable.img_visa_notification,
buttonsState = NotificationConfig.ButtonsState.SecondaryButtonConfig(
text = resourceReference(R.string.common_contact_support),
onClick = {},
),
iconSize = 36.dp,
),
),
),
)

View file

@ -3,6 +3,7 @@ package com.tangem.features.tangempay.utils
import com.tangem.core.ui.components.containers.pullToRefresh.PullToRefreshConfig.ShowRefreshState
internal interface TangemPayDetailIntents {
fun onContactSupportClicked()
fun onRefreshSwipe(refreshState: ShowRefreshState)
fun onClickAddFunds()
fun onClickWithdraw()

View file

@ -58,7 +58,7 @@ internal class TangemPayOnboardingModel @Inject constructor(
}
is TangemPayOnboardingComponent.Params.Deeplink -> {
repository.validateDeeplink(params.deeplink)
.onRight { isValid -> if (isValid) showOnboarding() }
.onRight { isValid -> if (isValid) showOnboarding() else back() }
.onLeft { back() }
}
}

View file

@ -93,7 +93,7 @@ internal class TangemPayClickIntentsImplementor @Inject constructor(
body = resourceReference(R.string.tangempay_failed_to_issue_card_support_description)
}
secondaryButton {
text = resourceReference(R.string.details_row_title_contact_to_support)
text = resourceReference(R.string.common_contact_support)
onClick {
onPaySupportClick()
closeBs()

View file

@ -51,7 +51,7 @@ sealed class WalletNotification(val config: NotificationConfig) {
title = resourceReference(R.string.warning_backup_errors_title),
subtitle = resourceReference(R.string.warning_backup_errors_message),
buttonsState = NotificationConfig.ButtonsState.PrimaryButtonConfig(
text = resourceReference(id = R.string.details_row_title_contact_to_support),
text = resourceReference(id = R.string.common_contact_support),
onClick = onSupportClick,
),
)