Updated on 2026-08-14

This commit is contained in:
Tangem 2025-11-06 15:31:30 +05:00
parent e037233aa5
commit 804e4eca23
31 changed files with 635 additions and 147 deletions

View file

@ -153,4 +153,16 @@ interface TangemPayApi {
@Header("Authorization") authHeader: String,
@Body body: SetPinRequest,
): ApiResponse<SetPinResponse>
@POST("v1/customer/card/freeze")
suspend fun freezeCard(
@Header("Authorization") authHeader: String,
@Body body: FreezeUnfreezeCardRequest,
): ApiResponse<FreezeUnfreezeCardResponse>
@POST("v1/customer/card/unfreeze")
suspend fun unfreezeCard(
@Header("Authorization") authHeader: String,
@Body body: FreezeUnfreezeCardRequest,
): ApiResponse<FreezeUnfreezeCardResponse>
}

View file

@ -0,0 +1,7 @@
package com.tangem.datasource.api.pay.models.request
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class FreezeUnfreezeCardRequest(@Json(name = "card_id") val cardId: String)

View file

@ -28,10 +28,49 @@ data class CustomerMeResponse(
@Json(name = "cid") val cid: String,
@Json(name = "card_id") val cardId: String,
@Json(name = "card_wallet_address") val cardWalletAddress: String,
@Json(name = "status") val status: String,
@Json(name = "status") val status: Status,
@Json(name = "updated_at") val updatedAt: String,
@Json(name = "payment_account_id") val paymentAccountId: String,
)
) {
@JsonClass(generateAdapter = false)
enum class Status {
@Json(name = "new")
NEW,
@Json(name = "ready_for_manufacturing")
READY_FOR_MANUFACTURING,
@Json(name = "manufacturing")
MANUFACTURING,
@Json(name = "sent_to_delivery")
SENT_TO_DELIVERY,
@Json(name = "delivered")
DELIVERED,
@Json(name = "activating")
ACTIVATING,
@Json(name = "active")
ACTIVE,
@Json(name = "blocked")
BLOCKED,
@Json(name = "deactivating")
DEACTIVATING,
@Json(name = "deactivated")
DEACTIVATED,
@Json(name = "canceled")
CANCELED,
@Json(name = "unknown")
UNKNOWN,
}
}
@JsonClass(generateAdapter = true)
data class PaymentAccount(

View file

@ -0,0 +1,31 @@
package com.tangem.datasource.api.pay.models.response
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
class FreezeUnfreezeCardResponse(
@Json(name = "result") val result: Result?,
@Json(name = "error") val error: String?,
) {
@JsonClass(generateAdapter = true)
data class Result(
@Json(name = "order_id") val orderId: String,
@Json(name = "status") val status: Status,
)
@JsonClass(generateAdapter = false)
enum class Status {
@Json(name = "NEW")
NEW,
@Json(name = "PROCESSING")
PROCESSING,
@Json(name = "COMPLETED")
COMPLETED,
@Json(name = "CANCELED")
CANCELED,
}
}

View file

@ -1357,6 +1357,7 @@
<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_card_frozen">Card frozen</string>
<string name="tangem_pay_deposit">Deposit</string>
<string name="tangem_pay_dispute">Dispute</string>
<string name="tangem_pay_explore_transaction">Explore transaction</string>
@ -1364,7 +1365,9 @@
<string name="tangem_pay_fee_title">Fee</string>
<string name="tangem_pay_freeze_card_alert_body">Keep your money safe if your card is lost or stolen. You can unfreeze anytime.</string>
<string name="tangem_pay_freeze_card_alert_title">Freeze your card?</string>
<string name="tangem_pay_freeze_card_failed">Failed to freeze the card. Try again later.</string>
<string name="tangem_pay_freeze_card_freeze">Freeze</string>
<string name="tangem_pay_freeze_card_success">Your card is frozen.</string>
<string name="tangem_pay_get_help">Get Help</string>
<string name="tangem_pay_status_completed">Completed</string>
<string name="tangem_pay_status_declined">Declined</string>
@ -1373,6 +1376,8 @@
<string name="tangem_pay_transaction_fee_notification_text">This fee goes to cover the cost of handling your transfer.</string>
<string name="tangem_pay_unfreeze_card_alert_body">Keep using your money. You can freeze anytime.</string>
<string name="tangem_pay_unfreeze_card_alert_title">Unfreeze your card?</string>
<string name="tangem_pay_unfreeze_card_failed">Failed to unfreeze the card. Try again later.</string>
<string name="tangem_pay_unfreeze_card_success">Your card is unfrozen.</string>
<string name="tangem_pay_withdrawal">Withdrawal</string>
<string name="tangempay_card_details_add_funds">Add funds</string>
<string name="tangempay_card_details_add_funds_subtitle">Top-up options</string>

View file

@ -0,0 +1,12 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:autoMirrored="true"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#1E1E1E"
android:pathData="M6.876,18.737C6.365,18.704 6.11,18.305 6.176,17.805L6.31,16.284L4.634,17.261C4.167,17.528 3.679,17.417 3.413,16.962C3.157,16.517 3.302,16.007 3.768,15.752L5.433,14.786L4.056,14.131C3.59,13.942 3.379,13.521 3.601,13.076C3.801,12.677 4.245,12.521 4.689,12.766L6.854,13.976L7.841,13.398L10.705,12L7.841,10.601L6.854,10.035L4.7,11.234C4.245,11.478 3.79,11.323 3.601,10.934C3.379,10.479 3.59,10.057 4.056,9.869L5.433,9.214L3.757,8.248C3.302,7.993 3.146,7.504 3.413,7.049C3.668,6.594 4.178,6.483 4.634,6.75L6.299,7.715L6.176,6.183C6.121,5.695 6.376,5.296 6.876,5.262C7.298,5.24 7.664,5.551 7.675,6.061L7.719,8.526L8.707,9.103L11.349,10.89L11.127,7.693V6.561L9.018,5.284C8.574,5.018 8.485,4.563 8.729,4.197C9.007,3.775 9.473,3.753 9.873,4.063L11.127,4.929V2.998C11.127,2.465 11.482,2.088 11.993,2.088C12.526,2.088 12.87,2.454 12.87,2.998V4.929L14.113,4.063C14.524,3.753 14.99,3.775 15.267,4.197C15.5,4.563 15.423,5.018 14.979,5.284L12.87,6.561V7.693L12.648,10.879L15.289,9.103L16.277,8.537L16.322,6.061C16.333,5.551 16.699,5.251 17.132,5.262C17.62,5.296 17.876,5.695 17.809,6.195L17.687,7.715L19.363,6.75C19.829,6.472 20.329,6.594 20.584,7.049C20.85,7.504 20.706,7.971 20.229,8.248L18.564,9.214L19.94,9.869C20.395,10.057 20.617,10.479 20.395,10.923C20.195,11.323 19.752,11.478 19.296,11.234L17.143,10.035L16.155,10.601L13.291,12L16.155,13.398L17.143,13.976L19.308,12.777C19.752,12.521 20.184,12.688 20.395,13.076C20.617,13.521 20.395,13.942 19.94,14.131L18.553,14.786L20.229,15.752C20.684,16.007 20.828,16.517 20.573,16.962C20.318,17.417 19.829,17.528 19.363,17.261L17.687,16.284L17.82,17.805C17.876,18.305 17.62,18.704 17.121,18.737C16.688,18.76 16.333,18.46 16.322,17.938L16.277,15.474L15.289,14.897L12.648,13.132L12.87,16.295V17.439L14.979,18.715C15.423,18.982 15.511,19.437 15.267,19.803C14.99,20.225 14.524,20.247 14.113,19.936L12.87,19.07V21.002C12.87,21.546 12.526,21.912 11.993,21.912C11.482,21.912 11.127,21.535 11.127,21.002V19.07L9.873,19.936C9.473,20.236 8.996,20.225 8.729,19.803C8.485,19.437 8.574,18.982 9.018,18.715L11.127,17.439V16.295L11.349,13.121L8.707,14.897L7.719,15.474L7.675,17.938C7.664,18.449 7.309,18.76 6.876,18.737Z" />
</vector>

View file

@ -1,6 +1,7 @@
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
@ -16,7 +17,6 @@ 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.TangemPayTopUpDataFactory
import kotlinx.coroutines.CancellationException
import timber.log.Timber
import java.math.BigDecimal
import javax.inject.Inject
@ -68,10 +68,10 @@ internal class DefaultTangemPayTopUpDataFactory @Inject constructor(
cryptoBalance: BigDecimal,
fiatBalance: BigDecimal,
): Either<UniversalError, TangemPayTopUpData> {
return try {
return catch {
val wallet = tangemPayWalletsManager.getDefaultWalletForTangemPayBlocking()
val currency = getCurrency(wallet, chainId)
val result = TangemPayTopUpData(
TangemPayTopUpData(
currency = currency,
walletId = wallet.walletId,
cryptoBalance = cryptoBalance,
@ -79,18 +79,9 @@ internal class DefaultTangemPayTopUpDataFactory @Inject constructor(
depositAddress = depositAddress,
receiveAddress = listOf(ReceiveAddressModel(nameService = NameService.Default, value = depositAddress)),
)
Either.Right(result)
} catch (exception: Exception) {
when (exception) {
is CancellationException -> {
throw exception
}
else -> {
Timber.tag(TAG).e(exception)
Either.Left(errorConverter.convert(exception))
}
}
}.mapLeft { exception ->
Timber.tag(TAG).e(exception)
errorConverter.convert(exception)
}
}
}

View file

@ -1,13 +1,13 @@
package com.tangem.data.pay.di
import com.tangem.data.pay.DefaultTangemPayTopUpDataFactory
import com.tangem.data.pay.repository.DefaultCardDetailsRepository
import com.tangem.data.pay.repository.DefaultTangemPayCardDetailsRepository
import com.tangem.data.pay.repository.DefaultKycRepository
import com.tangem.data.pay.repository.DefaultTangemPayTxHistoryRepository
import com.tangem.data.pay.repository.DefaultOnboardingRepository
import com.tangem.data.pay.usecase.DefaultGetTangemPayCurrencyStatusUseCase
import com.tangem.domain.pay.TangemPayTopUpDataFactory
import com.tangem.domain.pay.repository.CardDetailsRepository
import com.tangem.domain.pay.repository.TangemPayCardDetailsRepository
import com.tangem.domain.pay.repository.KycRepository
import com.tangem.domain.pay.repository.OnboardingRepository
import com.tangem.domain.pay.usecase.ProduceTangemPayInitialDataUseCase
@ -40,7 +40,7 @@ internal interface TangemPayDataModule {
@Binds
@Singleton
fun bindCardDetailsRepository(repository: DefaultCardDetailsRepository): CardDetailsRepository
fun bindCardDetailsRepository(repository: DefaultTangemPayCardDetailsRepository): TangemPayCardDetailsRepository
@Binds
@Singleton

View file

@ -137,8 +137,18 @@ internal class DefaultOnboardingRepository @Inject constructor(
} else {
null
}
val productInstance = response?.productInstance?.let { instance ->
ProductInstance(
id = instance.id,
cardId = instance.cardId,
status = when (instance.status) {
CustomerMeResponse.ProductInstance.Status.ACTIVE -> ProductInstance.Status.ACTIVE
else -> ProductInstance.Status.INACTIVE
},
)
}
return CustomerInfo(
productInstance = response?.productInstance?.let { ProductInstance(id = it.id, status = it.status) },
productInstance = productInstance,
isKycApproved = response?.kyc?.status == APPROVED_KYC_STATUS,
cardInfo = cardInfo,
)

View file

@ -9,24 +9,29 @@ import com.tangem.datasource.api.common.config.ApiEnvironment
import com.tangem.datasource.api.common.config.managers.ApiConfigsManager
import com.tangem.datasource.api.pay.TangemPayApi
import com.tangem.datasource.api.pay.models.request.CardDetailsRequest
import com.tangem.datasource.api.pay.models.request.FreezeUnfreezeCardRequest
import com.tangem.datasource.api.pay.models.request.SetPinRequest
import com.tangem.datasource.api.pay.models.response.FreezeUnfreezeCardResponse
import com.tangem.datasource.local.visa.TangemPayStorage
import com.tangem.domain.pay.model.SetPinResult
import com.tangem.domain.pay.model.TangemPayCardBalance
import com.tangem.domain.pay.model.TangemPayCardDetails
import com.tangem.domain.pay.repository.CardDetailsRepository
import com.tangem.domain.pay.repository.TangemPayCardDetailsRepository
import com.tangem.domain.visa.error.VisaApiError
import timber.log.Timber
import javax.inject.Inject
private const val TAG = "TangemPay: CardDetailsRepository"
internal class DefaultCardDetailsRepository @Inject constructor(
@Suppress("OptionalUnit")
internal class DefaultTangemPayCardDetailsRepository @Inject constructor(
private val tangemPayApi: TangemPayApi,
private val requestHelper: TangemPayRequestPerformer,
private val visaLibLoader: VisaLibLoader,
private val apiConfigsManager: ApiConfigsManager,
private val rainCryptoUtil: RainCryptoUtil,
private val storage: TangemPayStorage,
) : CardDetailsRepository {
) : TangemPayCardDetailsRepository {
override suspend fun getCardBalance(): Either<UniversalError, TangemPayCardBalance> {
return requestHelper.runWithErrorLogs(TAG) {
@ -112,6 +117,34 @@ internal class DefaultCardDetailsRepository @Inject constructor(
}
}
override suspend fun freezeCard(cardId: String): Either<UniversalError, Unit> {
return requestHelper.makeSafeRequest {
tangemPayApi.freezeCard(authHeader = it, body = FreezeUnfreezeCardRequest(cardId = cardId))
}.map { response ->
val result = response.result
if (result != null && result.status == FreezeUnfreezeCardResponse.Status.COMPLETED) {
Either.Right(Unit)
} else {
Timber.e("Card freeze failed with status: ${response.result?.status}")
Either.Left(VisaApiError.UnknownWithoutCode)
}
}
}
override suspend fun unfreezeCard(cardId: String): Either<UniversalError, Unit> {
return requestHelper.makeSafeRequest {
tangemPayApi.unfreezeCard(authHeader = it, body = FreezeUnfreezeCardRequest(cardId = cardId))
}.map { response ->
val result = response.result
if (result != null && result.status == FreezeUnfreezeCardResponse.Status.COMPLETED) {
Either.Right(Unit)
} else {
Timber.e("Card unfreeze failed with status: ${response.result?.status}")
Either.Left(VisaApiError.UnknownWithoutCode)
}
}
}
private suspend fun getPublicKeyBase64(): String {
val env = apiConfigsManager.getEnvironmentConfig(ApiConfig.ID.TangemPay).environment
return when (env) {

View file

@ -1,6 +1,7 @@
package com.tangem.data.pay.repository
import arrow.core.Either
import arrow.core.Either.Companion.catch
import com.squareup.moshi.Moshi
import com.tangem.core.error.UniversalError
import com.tangem.data.pay.util.TangemPayErrorConverter
@ -11,6 +12,7 @@ import com.tangem.datasource.api.common.response.getOrThrow
import com.tangem.datasource.di.NetworkMoshi
import com.tangem.datasource.local.visa.TangemPayStorage
import com.tangem.domain.pay.datasource.TangemPayAuthDataSource
import com.tangem.domain.visa.error.VisaApiError
import com.tangem.domain.visa.model.VisaAuthTokens
import com.tangem.domain.visa.model.getAuthHeader
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
@ -21,7 +23,6 @@ import kotlinx.coroutines.sync.withLock
import timber.log.Timber
import javax.inject.Inject
@Suppress("LongParameterList")
internal class TangemPayRequestPerformer @Inject constructor(
@NetworkMoshi moshi: Moshi,
private val dispatchers: CoroutineDispatcherProvider,
@ -54,6 +55,16 @@ internal class TangemPayRequestPerformer @Inject constructor(
}
}
suspend fun <T : Any> makeSafeRequest(
requestBlock: suspend (header: String) -> ApiResponse<T>,
): Either<VisaApiError, T> {
return catch { request(requestBlock) }
.mapLeft { exception ->
Timber.tag("TangemPayRequestPerformer").e(exception)
errorConverter.convert(exception)
}
}
suspend fun <T : Any> request(requestBlock: suspend (header: String) -> ApiResponse<T>): T =
withContext(dispatchers.io) {
performRequest(

View file

@ -1,17 +1,16 @@
package com.tangem.data.pay.util
import com.squareup.moshi.Moshi
import com.tangem.core.error.UniversalError
import com.tangem.datasource.api.common.response.ApiResponseError
import com.tangem.datasource.api.pay.models.response.VisaErrorResponseJsonAdapter
import com.tangem.datasource.api.pay.models.response.VisaErrorResponse
import com.tangem.domain.visa.error.VisaApiError
import com.tangem.utils.converter.Converter
class TangemPayErrorConverter(moshi: Moshi) : Converter<Throwable, UniversalError> {
class TangemPayErrorConverter(moshi: Moshi) : Converter<Throwable, VisaApiError> {
private val visaErrorAdapter = VisaErrorResponseJsonAdapter(moshi)
private val visaErrorAdapter by lazy { moshi.adapter(VisaErrorResponse::class.java) }
override fun convert(value: Throwable): UniversalError {
override fun convert(value: Throwable): VisaApiError {
return if (value is ApiResponseError.HttpException) {
val errorBody = value.errorBody ?: return VisaApiError.UnknownWithoutCode
return runCatching {

View file

@ -4,6 +4,8 @@ import kotlinx.serialization.Serializable
@Serializable
data class TangemPayDetailsConfig(
val cardId: String,
val isCardFrozen: Boolean,
val customerWalletAddress: String,
val cardNumberEnd: String,
val chainId: Int,

View file

@ -15,8 +15,14 @@ data class CustomerInfo(
data class ProductInstance(
val id: String,
val status: String,
)
val cardId: String,
val status: Status,
) {
enum class Status {
ACTIVE,
INACTIVE,
}
}
data class CardInfo(
val lastFourDigits: String,

View file

@ -6,7 +6,7 @@ import com.tangem.domain.pay.model.SetPinResult
import com.tangem.domain.pay.model.TangemPayCardBalance
import com.tangem.domain.pay.model.TangemPayCardDetails
interface CardDetailsRepository {
interface TangemPayCardDetailsRepository {
suspend fun getCardBalance(): Either<UniversalError, TangemPayCardBalance>
@ -17,4 +17,7 @@ interface CardDetailsRepository {
suspend fun isAddToWalletDone(): Either<UniversalError, Boolean>
suspend fun setAddToWalletAsDone(): Either<UniversalError, Unit>
suspend fun freezeCard(cardId: String): Either<UniversalError, Unit>
suspend fun unfreezeCard(cardId: String): Either<UniversalError, Unit>
}

View file

@ -107,8 +107,8 @@ class YieldSupplyGetRewardsBalanceUseCaseTest {
chainId = 1,
apy = BigDecimal.ZERO,
isActive = true,
maxFeeNative = "0",
maxFeeUSD = "0",
maxFeeNative = BigDecimal.ZERO,
maxFeeUSD = BigDecimal.ZERO,
backendId = "id",
),
)
@ -152,8 +152,8 @@ class YieldSupplyGetRewardsBalanceUseCaseTest {
chainId = 1,
apy = apy,
isActive = true,
maxFeeNative = "0",
maxFeeUSD = "0",
maxFeeNative = BigDecimal.ZERO,
maxFeeUSD = BigDecimal.ZERO,
backendId = "id",
),
)

View file

@ -4,49 +4,80 @@ import com.tangem.core.ui.components.buttons.actions.ActionButtonConfig
import com.tangem.core.ui.components.containers.pullToRefresh.PullToRefreshConfig
import com.tangem.core.ui.components.containers.pullToRefresh.PullToRefreshConfig.ShowRefreshState
import com.tangem.core.ui.components.dropdownmenu.TangemDropdownMenuItem
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.themedColor
import com.tangem.core.ui.res.TangemTheme
import com.tangem.features.tangempay.details.impl.R
import kotlinx.collections.immutable.persistentListOf
@Suppress("LongParameterList")
internal class TangemPayDetailsStateFactory(
private val onBack: () -> Unit,
private val onRefresh: (ShowRefreshState) -> Unit,
private val onAddFunds: () -> Unit,
private val onClickChangePin: () -> Unit,
private val isCardFrozen: Boolean,
private val onClickFreezeCard: () -> Unit,
private val onClickUnfreezeCard: () -> Unit,
) {
fun getInitialState() = TangemPayDetailsUM(
topBarConfig = TangemPayDetailsTopBarConfig(
onBackClick = onBack,
items = persistentListOf(
TangemDropdownMenuItem(
title = TextReference.Res(R.string.tangempay_card_details_change_pin),
fun getInitialState(): TangemPayDetailsUM {
val cardFrozenStateItem = if (isCardFrozen) {
TangemPayDetailsTopBarMenuItem(
type = TangemPayDetailsTopBarMenuItemType.UnfreezeCard,
dropdownItem = TangemDropdownMenuItem(
title = resourceReference(R.string.tangempay_card_details_unfreeze_card),
textColor = themedColor { TangemTheme.colors.text.primary1 },
onClick = onClickChangePin,
onClick = onClickUnfreezeCard,
),
TangemDropdownMenuItem(
title = TextReference.Res(R.string.tangempay_card_details_freeze_card),
)
} else {
TangemPayDetailsTopBarMenuItem(
type = TangemPayDetailsTopBarMenuItemType.FreezeCard,
dropdownItem = TangemDropdownMenuItem(
title = resourceReference(R.string.tangempay_card_details_freeze_card),
textColor = themedColor { TangemTheme.colors.text.primary1 },
onClick = onClickFreezeCard,
),
),
),
pullToRefreshConfig = PullToRefreshConfig(isRefreshing = false, onRefresh = onRefresh),
balanceBlockState = TangemPayDetailsBalanceBlockState.Loading(
actionButtons = persistentListOf(
ActionButtonConfig(
text = resourceReference(id = R.string.tangempay_card_details_add_funds),
iconResId = R.drawable.ic_arrow_down_24,
onClick = onAddFunds,
)
}
return TangemPayDetailsUM(
topBarConfig = TangemPayDetailsTopBarConfig(
onBackClick = onBack,
items = persistentListOf(
TangemPayDetailsTopBarMenuItem(
type = TangemPayDetailsTopBarMenuItemType.ChangePin,
dropdownItem = TangemDropdownMenuItem(
title = resourceReference(R.string.tangempay_card_details_change_pin),
textColor = themedColor { TangemTheme.colors.text.primary1 },
onClick = onClickChangePin,
),
),
cardFrozenStateItem,
),
),
),
addToWalletBlockState = null,
isBalanceHidden = false,
addFundsEnabled = true,
)
pullToRefreshConfig = PullToRefreshConfig(
isRefreshing = false,
onRefresh = onRefresh,
),
balanceBlockState = TangemPayDetailsBalanceBlockState.Loading(
actionButtons = persistentListOf(
ActionButtonConfig(
text = resourceReference(id = R.string.tangempay_card_details_add_funds),
iconResId = R.drawable.ic_arrow_down_24,
onClick = onAddFunds,
),
),
frozenState = if (isCardFrozen) {
CardFrozenState.Frozen(onClickUnfreezeCard)
} else {
CardFrozenState.Unfrozen
},
),
addToWalletBlockState = null,
isBalanceHidden = false,
addFundsEnabled = true,
)
}
}

View file

@ -5,5 +5,16 @@ import kotlinx.collections.immutable.ImmutableList
internal data class TangemPayDetailsTopBarConfig(
val onBackClick: () -> Unit,
val items: ImmutableList<TangemDropdownMenuItem>?,
)
val items: ImmutableList<TangemPayDetailsTopBarMenuItem>?,
)
internal data class TangemPayDetailsTopBarMenuItem(
val type: TangemPayDetailsTopBarMenuItemType,
val dropdownItem: TangemDropdownMenuItem,
)
internal enum class TangemPayDetailsTopBarMenuItemType {
ChangePin,
FreezeCard,
UnfreezeCard,
}

View file

@ -15,9 +15,9 @@ internal data class TangemPayDetailsUM(
)
internal data class TangemPayCardDetailsUM(
val number: String = "",
val expiry: String = "",
val cvv: String = "",
val number: String,
val expiry: String,
val cvv: String,
val buttonText: TextReference = TextReference.EMPTY,
val onClick: () -> Unit = {},
val onCopy: (String) -> Unit = {},
@ -28,13 +28,16 @@ internal data class TangemPayCardDetailsUM(
internal sealed class TangemPayDetailsBalanceBlockState {
abstract val actionButtons: ImmutableList<ActionButtonConfig>
abstract val frozenState: CardFrozenState
data class Loading(
override val actionButtons: ImmutableList<ActionButtonConfig>,
override val frozenState: CardFrozenState,
) : TangemPayDetailsBalanceBlockState()
data class Content(
override val actionButtons: ImmutableList<ActionButtonConfig>,
override val frozenState: CardFrozenState,
val cryptoBalance: String,
val fiatBalance: String,
val isBalanceFlickering: Boolean,
@ -42,9 +45,16 @@ internal sealed class TangemPayDetailsBalanceBlockState {
data class Error(
override val actionButtons: ImmutableList<ActionButtonConfig>,
override val frozenState: CardFrozenState,
) : TangemPayDetailsBalanceBlockState()
}
sealed class CardFrozenState {
data object Pending : CardFrozenState()
data class Frozen(val onUnfreeze: () -> Unit) : CardFrozenState()
data object Unfrozen : CardFrozenState()
}
internal data class AddToWalletBlockState(
val onClick: () -> Unit,
val onClickClose: () -> Unit,

View file

@ -8,7 +8,7 @@ import com.tangem.core.decompose.ui.UiMessageSender
import com.tangem.core.ui.clipboard.ClipboardManager
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.message.SnackbarMessage
import com.tangem.domain.pay.repository.CardDetailsRepository
import com.tangem.domain.pay.repository.TangemPayCardDetailsRepository
import com.tangem.features.tangempay.components.cardDetails.TangemPayCardDetailsBlockComponent
import com.tangem.features.tangempay.details.impl.R
import com.tangem.features.tangempay.entity.TangemPayCardDetailsBlockStateFactory
@ -33,7 +33,7 @@ import javax.inject.Inject
internal class TangemPayCardDetailsBlockModel @Inject constructor(
paramsContainer: ParamsContainer,
override val dispatchers: CoroutineDispatcherProvider,
private val cardDetailsRepository: CardDetailsRepository,
private val cardDetailsRepository: TangemPayCardDetailsRepository,
private val clipboardManager: ClipboardManager,
private val uiMessageSender: UiMessageSender,
private val cardDetailsEventListener: CardDetailsEventListener,

View file

@ -5,7 +5,7 @@ import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.navigation.Router
import com.tangem.domain.pay.model.SetPinResult
import com.tangem.domain.pay.repository.CardDetailsRepository
import com.tangem.domain.pay.repository.TangemPayCardDetailsRepository
import com.tangem.features.tangempay.entity.TangemPayChangePinUM
import kotlinx.coroutines.flow.*
import com.tangem.features.tangempay.model.transformers.PinCodeChangeTransformer
@ -22,7 +22,7 @@ import javax.inject.Inject
internal class TangemPayChangePinModel @Inject constructor(
override val dispatchers: CoroutineDispatcherProvider,
private val router: Router,
private val cardDetailsRepository: CardDetailsRepository,
private val cardDetailsRepository: TangemPayCardDetailsRepository,
) : Model() {
val uiState: StateFlow<TangemPayChangePinUM>

View file

@ -12,24 +12,25 @@ import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.core.decompose.navigation.Router
import com.tangem.core.decompose.ui.UiMessageSender
import com.tangem.core.ui.components.containers.pullToRefresh.PullToRefreshConfig.ShowRefreshState
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.message.SnackbarMessage
import com.tangem.domain.models.TokenReceiveConfig
import com.tangem.domain.pay.TangemPayTopUpData
import com.tangem.domain.pay.model.TangemPayCardBalance
import com.tangem.domain.pay.repository.CardDetailsRepository
import com.tangem.domain.pay.repository.TangemPayCardDetailsRepository
import com.tangem.domain.visa.model.TangemPayTxHistoryItem
import com.tangem.features.tangempay.components.AddFundsListener
import com.tangem.features.tangempay.components.TangemPayDetailsContainerComponent
import com.tangem.features.tangempay.details.impl.R
import com.tangem.features.tangempay.entity.TangemPayDetailsErrorType
import com.tangem.features.tangempay.entity.TangemPayDetailsNavigation
import com.tangem.features.tangempay.entity.TangemPayDetailsStateFactory
import com.tangem.features.tangempay.entity.TangemPayDetailsUM
import com.tangem.features.tangempay.model.listener.CardDetailsEvent
import com.tangem.features.tangempay.model.listener.CardDetailsEventListener
import com.tangem.features.tangempay.model.transformers.DetailsAddToWalletBannerTransformer
import com.tangem.features.tangempay.model.transformers.DetailsBalanceTransformer
import com.tangem.features.tangempay.model.transformers.TangemPayDetailsRefreshTransformer
import com.tangem.features.tangempay.model.transformers.*
import com.tangem.features.tangempay.navigation.TangemPayDetailsInnerRoute
import com.tangem.features.tangempay.utils.TangemPayErrorMessageFactory
import com.tangem.features.tangempay.utils.TangemPayMessagesFactory
import com.tangem.features.tangempay.utils.TangemPayTxHistoryUiActions
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.coroutines.JobHolder
@ -48,7 +49,7 @@ internal class TangemPayDetailsModel @Inject constructor(
paramsContainer: ParamsContainer,
override val dispatchers: CoroutineDispatcherProvider,
private val router: Router,
private val cardDetailsRepository: CardDetailsRepository,
private val cardDetailsRepository: TangemPayCardDetailsRepository,
private val uiMessageSender: UiMessageSender,
private val cardDetailsEventListener: CardDetailsEventListener,
) : Model(), TangemPayTxHistoryUiActions, AddFundsListener {
@ -60,7 +61,9 @@ internal class TangemPayDetailsModel @Inject constructor(
onRefresh = ::onRefreshSwipe,
onAddFunds = ::onClickAddFunds,
onClickChangePin = ::onClickChangePin,
isCardFrozen = params.config.isCardFrozen,
onClickFreezeCard = ::onClickFreezeCard,
onClickUnfreezeCard = ::onClickUnfreezeCard,
)
val uiState: StateFlow<TangemPayDetailsUM>
@ -84,7 +87,65 @@ internal class TangemPayDetailsModel @Inject constructor(
}
private fun onClickFreezeCard() {
// TODO [REDACTED_JIRA]
uiMessageSender.send(TangemPayMessagesFactory.createFreezeCardMessage(onFreezeClicked = ::freezeCard))
}
private fun onClickUnfreezeCard() {
uiMessageSender.send(TangemPayMessagesFactory.createUnfreezeCardMessage(onUnfreezeClicked = ::unfreezeCard))
}
private fun freezeCard() {
modelScope.launch {
uiState.update(TangemPayFreezeUnfreezeProcessTransformer)
cardDetailsRepository.freezeCard(cardId = params.config.cardId)
.onLeft {
uiMessageSender.send(SnackbarMessage(resourceReference(R.string.tangem_pay_freeze_card_failed)))
uiState.update(
TangemPayFreezeUnfreezeStateTransformer(
frozen = false,
onFreezeClick = ::onClickFreezeCard,
onUnfreezeClick = ::onClickUnfreezeCard,
),
)
}
.onRight {
uiMessageSender.send(SnackbarMessage(resourceReference(R.string.tangem_pay_freeze_card_success)))
uiState.update(
TangemPayFreezeUnfreezeStateTransformer(
frozen = true,
onFreezeClick = ::onClickFreezeCard,
onUnfreezeClick = ::onClickUnfreezeCard,
),
)
}
}
}
private fun unfreezeCard() {
modelScope.launch {
uiState.update(TangemPayFreezeUnfreezeProcessTransformer)
cardDetailsRepository.unfreezeCard(cardId = params.config.cardId)
.onLeft {
uiMessageSender.send(SnackbarMessage(resourceReference(R.string.tangem_pay_unfreeze_card_failed)))
uiState.update(
TangemPayFreezeUnfreezeStateTransformer(
frozen = true,
onFreezeClick = ::onClickFreezeCard,
onUnfreezeClick = ::onClickUnfreezeCard,
),
)
}
.onRight {
uiMessageSender.send(SnackbarMessage(resourceReference(R.string.tangem_pay_unfreeze_card_success)))
uiState.update(
TangemPayFreezeUnfreezeStateTransformer(
frozen = false,
onFreezeClick = ::onClickFreezeCard,
onUnfreezeClick = ::onClickUnfreezeCard,
),
)
}
}
}
private fun onClickAddFunds() {
@ -188,6 +249,6 @@ internal class TangemPayDetailsModel @Inject constructor(
}
private fun showBottomSheetError(type: TangemPayDetailsErrorType) {
uiMessageSender.send(message = TangemPayErrorMessageFactory.createErrorMessage(type = type))
uiMessageSender.send(message = TangemPayMessagesFactory.createErrorMessage(type = type))
}
}

View file

@ -18,7 +18,10 @@ internal class DetailsBalanceTransformer(
override fun transform(prevState: TangemPayDetailsUM): TangemPayDetailsUM {
val balance = when (balance) {
is Either.Left<UniversalError> -> {
TangemPayDetailsBalanceBlockState.Error(persistentListOf())
TangemPayDetailsBalanceBlockState.Error(
actionButtons = persistentListOf(),
frozenState = prevState.balanceBlockState.frozenState,
)
}
is Either.Right<TangemPayCardBalance> -> {
TangemPayDetailsBalanceBlockState.Content(
@ -27,6 +30,7 @@ internal class DetailsBalanceTransformer(
// TODO [REDACTED_TASK_KEY]: Add crypto balance when the BFF is ready
cryptoBalance = "",
actionButtons = prevState.balanceBlockState.actionButtons,
frozenState = prevState.balanceBlockState.frozenState,
)
}
}

View file

@ -7,7 +7,7 @@ import com.tangem.features.tangempay.details.impl.R
import com.tangem.features.tangempay.entity.TangemPayAddFundsItemUM
import com.tangem.features.tangempay.entity.TangemPayAddFundsUM
import com.tangem.features.tangempay.entity.TangemPayDetailsErrorType
import com.tangem.features.tangempay.utils.TangemPayErrorMessageFactory
import com.tangem.features.tangempay.utils.TangemPayMessagesFactory
import com.tangem.utils.converter.Converter
import kotlinx.collections.immutable.persistentListOf
@ -20,7 +20,7 @@ internal class TangemPayAddFundsUMConverter(
TangemPayAddFundsUM(
items = persistentListOf(),
dismiss = listener::onDismissAddFunds,
errorMessage = TangemPayErrorMessageFactory.createErrorMessage(
errorMessage = TangemPayMessagesFactory.createErrorMessage(
type = TangemPayDetailsErrorType.Receive,
).messageBottomSheetUMV2,
)

View file

@ -0,0 +1,21 @@
package com.tangem.features.tangempay.model.transformers
import com.tangem.features.tangempay.entity.CardFrozenState
import com.tangem.features.tangempay.entity.TangemPayDetailsBalanceBlockState
import com.tangem.features.tangempay.entity.TangemPayDetailsUM
import com.tangem.utils.transformer.Transformer
internal object TangemPayFreezeUnfreezeProcessTransformer : Transformer<TangemPayDetailsUM> {
override fun transform(prevState: TangemPayDetailsUM): TangemPayDetailsUM {
val newBalanceBlockState = when (val balanceState = prevState.balanceBlockState) {
is TangemPayDetailsBalanceBlockState.Error -> balanceState.copy(frozenState = CardFrozenState.Pending)
is TangemPayDetailsBalanceBlockState.Loading -> balanceState.copy(frozenState = CardFrozenState.Pending)
is TangemPayDetailsBalanceBlockState.Content -> balanceState.copy(
isBalanceFlickering = true,
frozenState = CardFrozenState.Pending,
)
}
return prevState.copy(balanceBlockState = newBalanceBlockState)
}
}

View file

@ -0,0 +1,73 @@
package com.tangem.features.tangempay.model.transformers
import com.tangem.core.ui.components.dropdownmenu.TangemDropdownMenuItem
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.themedColor
import com.tangem.core.ui.res.TangemTheme
import com.tangem.features.tangempay.details.impl.R
import com.tangem.features.tangempay.entity.*
import com.tangem.utils.transformer.Transformer
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toPersistentList
internal class TangemPayFreezeUnfreezeStateTransformer(
private val frozen: Boolean,
private val onFreezeClick: () -> Unit,
private val onUnfreezeClick: () -> Unit,
) : Transformer<TangemPayDetailsUM> {
private val cardFrozenState = if (frozen) CardFrozenState.Frozen(onUnfreezeClick) else CardFrozenState.Unfrozen
private val removeType =
if (frozen) TangemPayDetailsTopBarMenuItemType.FreezeCard else TangemPayDetailsTopBarMenuItemType.UnfreezeCard
private val addType =
if (frozen) TangemPayDetailsTopBarMenuItemType.UnfreezeCard else TangemPayDetailsTopBarMenuItemType.FreezeCard
override fun transform(prevState: TangemPayDetailsUM): TangemPayDetailsUM {
val dropdownMenuItems = createUpdatedMenuItems(prevState.topBarConfig.items)
val newBalanceBlockState = createUpdatedBalanceState(prevState.balanceBlockState)
return prevState.copy(
topBarConfig = prevState.topBarConfig.copy(items = dropdownMenuItems),
balanceBlockState = newBalanceBlockState,
)
}
private fun createUpdatedMenuItems(
items: ImmutableList<TangemPayDetailsTopBarMenuItem>?,
): ImmutableList<TangemPayDetailsTopBarMenuItem>? {
return items
?.filter { it.type != removeType }
?.plus(createMenuItemToAdd())
?.toPersistentList()
}
private fun createMenuItemToAdd(): TangemPayDetailsTopBarMenuItem {
return TangemPayDetailsTopBarMenuItem(
type = addType,
dropdownItem = TangemDropdownMenuItem(
title = resourceReference(
id = if (frozen) {
R.string.tangempay_card_details_unfreeze_card
} else {
R.string.tangempay_card_details_freeze_card
},
),
textColor = themedColor { TangemTheme.colors.text.primary1 },
onClick = if (frozen) onUnfreezeClick else onFreezeClick,
),
)
}
private fun createUpdatedBalanceState(
balanceState: TangemPayDetailsBalanceBlockState,
): TangemPayDetailsBalanceBlockState {
return when (balanceState) {
is TangemPayDetailsBalanceBlockState.Error -> balanceState.copy(frozenState = cardFrozenState)
is TangemPayDetailsBalanceBlockState.Loading -> balanceState.copy(frozenState = cardFrozenState)
is TangemPayDetailsBalanceBlockState.Content -> balanceState.copy(
isBalanceFlickering = false,
frozenState = cardFrozenState,
)
}
}
}

View file

@ -16,6 +16,7 @@ import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.util.fastForEach
import com.tangem.core.ui.components.SpacerH16
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
@ -70,7 +71,7 @@ internal fun TangemPayAddFundsContent(state: TangemPayAddFundsUM) {
color = TangemTheme.colors.text.tertiary,
style = TangemTheme.typography.subtitle2,
)
state.items.forEach {
state.items.fastForEach {
key(it.title) {
TangemPayTopUpItem(state = it)
}

View file

@ -9,7 +9,9 @@ import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.painterResource
@ -20,6 +22,7 @@ import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameter
import androidx.compose.ui.unit.dp
import androidx.compose.ui.util.fastForEach
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.tangem.core.ui.components.PrimaryButton
import com.tangem.core.ui.components.RectangleShimmer
import com.tangem.core.ui.components.buttons.HorizontalActionChips
import com.tangem.core.ui.components.buttons.actions.ActionButtonConfig
@ -27,9 +30,12 @@ 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.snackbar.TangemSnackbarHost
import com.tangem.core.ui.components.text.applyBladeBrush
import com.tangem.core.ui.extensions.*
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.res.TangemColorPalette
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.test.TokenDetailsTopBarTestTags
@ -48,19 +54,12 @@ internal fun TangemPayDetailsScreen(
txHistoryComponent: TangemPayTxHistoryComponent,
cardDetailsBlockComponent: TangemPayCardDetailsBlockComponent,
modifier: Modifier = Modifier,
snackbarHostState: SnackbarHostState = remember { SnackbarHostState() },
) {
Scaffold(
modifier = modifier,
topBar = { TangemPayDetailsTopAppBar(config = state.topBarConfig) },
contentWindowInsets = ScaffoldDefaults.contentWindowInsets.exclude(WindowInsets.navigationBars),
containerColor = TangemTheme.colors.background.secondary,
snackbarHost = {
TangemSnackbarHost(
modifier = Modifier.padding(start = 16.dp, end = 16.dp, bottom = 16.dp),
hostState = snackbarHostState,
)
},
) { scaffoldPaddings ->
val listState = rememberLazyListState()
val bottomBarHeight = with(LocalDensity.current) { WindowInsets.systemBars.getBottom(this).toDp() }
@ -106,6 +105,20 @@ internal fun TangemPayDetailsScreen(
item(TangemPayCardDetailsUM::class.java) {
TangemPayCardDetailsBlockItem(component = cardDetailsBlockComponent, state = cardDetailsState)
}
when (val cardFrozenState = state.balanceBlockState.frozenState) {
is CardFrozenState.Frozen -> item(CardFrozenState.Frozen::class.java) {
PrimaryButton(
modifier = Modifier
.animateItem()
.padding(horizontal = 16.dp)
.padding(top = 12.dp)
.fillMaxWidth(),
text = stringResourceSafe(R.string.tangempay_card_details_unfreeze_card),
onClick = cardFrozenState.onUnfreeze,
)
}
else -> Unit
}
with(txHistoryComponent) { txHistoryContent(listState = listState, state = txHistoryState) }
}
}
@ -127,14 +140,22 @@ internal fun TangemPayDetailsBalanceBlock(
)
.padding(vertical = 12.dp),
) {
Text(
modifier = Modifier.padding(start = 12.dp),
text = stringResourceSafe(R.string.tangempay_title),
color = TangemTheme.colors.text.tertiary,
style = TangemTheme.typography.subtitle2,
)
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 12.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
) {
Text(
text = stringResourceSafe(R.string.tangempay_title),
color = TangemTheme.colors.text.tertiary,
style = TangemTheme.typography.subtitle2,
)
TangemPayCardStatus(state = state.frozenState)
}
FiatBalance(
modifier = Modifier.padding(start = 12.dp, top = 4.dp),
modifier = Modifier.padding(start = 12.dp, top = 8.dp),
state = state,
isBalanceHidden = isBalanceHidden,
)
@ -155,6 +176,36 @@ internal fun TangemPayDetailsBalanceBlock(
}
}
@Composable
private fun TangemPayCardStatus(state: CardFrozenState, modifier: Modifier = Modifier) {
when (state) {
is CardFrozenState.Unfrozen -> Unit
is CardFrozenState.Pending -> CircularProgressIndicator(
modifier = modifier.size(18.dp),
trackColor = Color.Transparent,
color = TangemColorPalette.Azure,
strokeWidth = 2.dp,
)
is CardFrozenState.Frozen -> Row(
modifier = modifier,
horizontalArrangement = Arrangement.spacedBy(4.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Icon(
painter = painterResource(id = R.drawable.ic_snow_24),
tint = TangemTheme.colors.text.disabled,
contentDescription = null,
modifier = Modifier.size(16.dp),
)
Text(
text = stringResourceSafe(R.string.tangem_pay_card_frozen),
color = TangemTheme.colors.text.disabled,
style = TangemTheme.typography.body2,
)
}
}
}
@Composable
private fun FiatBalance(
state: TangemPayDetailsBalanceBlockState,
@ -173,18 +224,30 @@ private fun FiatBalance(
text = state.fiatBalance.orMaskWithStars(isBalanceHidden),
style = TangemTheme.typography.h2.applyBladeBrush(
isEnabled = state.isBalanceFlickering,
textColor = TangemTheme.colors.text.primary1,
textColor = getTextColor(
isCardFrozen = state.frozenState is CardFrozenState.Frozen,
defaultColor = TangemTheme.colors.text.primary1,
),
),
)
is TangemPayDetailsBalanceBlockState.Error -> Text(
modifier = modifier,
text = DASH_SIGN.orMaskWithStars(isBalanceHidden),
style = TangemTheme.typography.h2,
color = TangemTheme.colors.text.primary1,
color = getTextColor(
isCardFrozen = state.frozenState is CardFrozenState.Frozen,
defaultColor = TangemTheme.colors.text.primary1,
),
)
}
}
@ReadOnlyComposable
@Composable
private fun getTextColor(isCardFrozen: Boolean, defaultColor: Color): Color {
return if (isCardFrozen) TangemTheme.colors.text.tertiary else defaultColor
}
@Suppress("UnusedPrivateMember")
@Composable
private fun CryptoBalance(
@ -252,7 +315,7 @@ private fun TangemPayDetailsTopAppBar(config: TangemPayDetailsTopBarConfig, modi
content = {
config.items?.fastForEach {
TangemDropdownItem(
item = it,
item = it.dropdownItem,
dismissParent = { showDropdownMenu = false },
)
}
@ -310,6 +373,7 @@ private class TangemPayDetailsUMProvider : CollectionPreviewParameterProvider<Ta
cryptoBalance = "1234.56 USDT",
fiatBalance = "$1234.56",
isBalanceFlickering = false,
frozenState = CardFrozenState.Frozen(onUnfreeze = {}),
),
addToWalletBlockState = AddToWalletBlockState({}, {}),
isBalanceHidden = false,
@ -318,7 +382,10 @@ private class TangemPayDetailsUMProvider : CollectionPreviewParameterProvider<Ta
TangemPayDetailsUM(
topBarConfig = TangemPayDetailsTopBarConfig(onBackClick = {}, items = null),
pullToRefreshConfig = PullToRefreshConfig(isRefreshing = false, onRefresh = {}),
balanceBlockState = TangemPayDetailsBalanceBlockState.Loading(actionButtons = persistentListOf()),
balanceBlockState = TangemPayDetailsBalanceBlockState.Loading(
actionButtons = persistentListOf(),
frozenState = CardFrozenState.Pending,
),
addToWalletBlockState = AddToWalletBlockState({}, {}),
isBalanceHidden = false,
addFundsEnabled = true,

View file

@ -1,30 +0,0 @@
package com.tangem.features.tangempay.utils
import com.tangem.core.ui.R
import com.tangem.core.ui.components.bottomsheets.message.*
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.message.BottomSheetMessageV2
import com.tangem.core.ui.message.bottomSheetMessage
import com.tangem.features.tangempay.entity.TangemPayDetailsErrorType
internal object TangemPayErrorMessageFactory {
fun createErrorMessage(type: TangemPayDetailsErrorType): BottomSheetMessageV2 {
return when (type) {
TangemPayDetailsErrorType.Receive -> bottomSheetMessage {
infoBlock {
icon(R.drawable.img_attention_20) {
backgroundType = MessageBottomSheetUMV2.Icon.BackgroundType.Attention
}
title = TextReference.Res(R.string.tangempay_card_details_receive_error_title)
body = TextReference.Res(R.string.tangempay_card_details_receive_error_description)
}
primaryButton {
text = resourceReference(R.string.common_got_it)
onClick { closeBs() }
}
}
}
}
}

View file

@ -0,0 +1,70 @@
package com.tangem.features.tangempay.utils
import com.tangem.core.ui.R
import com.tangem.core.ui.components.bottomsheets.message.*
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.message.BottomSheetMessageV2
import com.tangem.core.ui.message.bottomSheetMessage
import com.tangem.features.tangempay.entity.TangemPayDetailsErrorType
internal object TangemPayMessagesFactory {
fun createErrorMessage(type: TangemPayDetailsErrorType): BottomSheetMessageV2 {
return when (type) {
TangemPayDetailsErrorType.Receive -> bottomSheetMessage {
infoBlock {
icon(R.drawable.img_attention_20) {
backgroundType = MessageBottomSheetUMV2.Icon.BackgroundType.Attention
}
title = TextReference.Res(R.string.tangempay_card_details_receive_error_title)
body = TextReference.Res(R.string.tangempay_card_details_receive_error_description)
}
primaryButton {
text = resourceReference(R.string.common_got_it)
onClick { closeBs() }
}
}
}
}
fun createFreezeCardMessage(onFreezeClicked: () -> Unit): BottomSheetMessageV2 {
return bottomSheetMessage {
infoBlock {
icon(R.drawable.ic_snow_24) {
type = MessageBottomSheetUMV2.Icon.Type.Accent
backgroundType = MessageBottomSheetUMV2.Icon.BackgroundType.Accent
}
title = TextReference.Res(R.string.tangem_pay_freeze_card_alert_title)
body = TextReference.Res(R.string.tangem_pay_freeze_card_alert_body)
}
primaryButton {
text = resourceReference(R.string.tangem_pay_freeze_card_freeze)
onClick {
onFreezeClicked()
closeBs()
}
}
}
}
fun createUnfreezeCardMessage(onUnfreezeClicked: () -> Unit): BottomSheetMessageV2 {
return bottomSheetMessage {
infoBlock {
icon(R.drawable.ic_snow_24) {
type = MessageBottomSheetUMV2.Icon.Type.Accent
backgroundType = MessageBottomSheetUMV2.Icon.BackgroundType.Accent
}
title = TextReference.Res(R.string.tangem_pay_unfreeze_card_alert_title)
body = TextReference.Res(R.string.tangem_pay_unfreeze_card_alert_body)
}
primaryButton {
text = resourceReference(R.string.tangempay_card_details_unfreeze_card)
onClick {
onUnfreezeClicked()
closeBs()
}
}
}
}
}

View file

@ -5,6 +5,7 @@ import com.tangem.core.ui.format.bigdecimal.fiat
import com.tangem.core.ui.format.bigdecimal.format
import com.tangem.domain.pay.TangemPayDetailsConfig
import com.tangem.domain.pay.model.CustomerInfo.CardInfo
import com.tangem.domain.pay.model.CustomerInfo.ProductInstance
import com.tangem.domain.pay.model.MainScreenCustomerInfo
import com.tangem.domain.pay.model.OrderStatus.CANCELED
import com.tangem.domain.pay.model.OrderStatus.UNKNOWN
@ -35,29 +36,36 @@ internal class TangemPayInitialStateTransformer(
private fun createInitialState(): TangemPayState {
val cardInfo = value?.info?.cardInfo
val productInstance = value?.info?.productInstance
return when {
value == null -> TangemPayState.Empty
!value.info.isKycApproved -> createKycInProgressState(onClickKyc)
cardInfo != null -> getCardInfoState(cardInfo)
cardInfo != null && productInstance != null -> getCardInfoState(cardInfo, productInstance)
value.orderStatus == UNKNOWN || value.orderStatus == CANCELED -> createIssueAvailableState(onClickIssue)
else -> createIssueProgressState()
}
}
private fun getCardInfoState(cardInfo: CardInfo): TangemPayState = TangemPayState.Card(
lastFourDigits = TextReference.Str("*${cardInfo.lastFourDigits}"),
balanceText = TextReference.Str(getBalanceText(cardInfo)),
onClick = {
openDetails(
TangemPayDetailsConfig(
customerWalletAddress = cardInfo.customerWalletAddress,
cardNumberEnd = cardInfo.lastFourDigits,
chainId = POLYGON_CHAIN_ID,
depositAddress = cardInfo.depositAddress,
),
)
},
)
private fun getCardInfoState(cardInfo: CardInfo, productInstance: ProductInstance): TangemPayState =
TangemPayState.Card(
lastFourDigits = TextReference.Str("*${cardInfo.lastFourDigits}"),
balanceText = TextReference.Str(getBalanceText(cardInfo)),
onClick = {
openDetails(
TangemPayDetailsConfig(
cardId = productInstance.cardId,
isCardFrozen = when (productInstance.status) {
ProductInstance.Status.ACTIVE -> false
ProductInstance.Status.INACTIVE -> true
},
customerWalletAddress = cardInfo.customerWalletAddress,
cardNumberEnd = cardInfo.lastFourDigits,
chainId = POLYGON_CHAIN_ID,
depositAddress = cardInfo.depositAddress,
),
)
},
)
private fun getBalanceText(cardInfo: CardInfo): String {
val currency = Currency.getInstance(cardInfo.currencyCode)