From 804e4eca23e7175b7f9d9eae48396f2380845d82 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 6 Nov 2025 15:31:30 +0500 Subject: [PATCH] Updated on 2026-08-14 --- .../tangem/datasource/api/pay/TangemPayApi.kt | 12 ++ .../request/FreezeUnfreezeCardRequest.kt | 7 ++ .../pay/models/response/CustomerMeResponse.kt | 43 ++++++- .../response/FreezeUnfreezeCardResponse.kt | 31 +++++ core/res/src/main/res/values/strings.xml | 5 + core/ui/src/main/res/drawable/ic_snow_24.xml | 12 ++ .../pay/DefaultTangemPayTopUpDataFactory.kt | 21 +--- .../tangem/data/pay/di/TangemPayDataModule.kt | 6 +- .../repository/DefaultOnboardingRepository.kt | 12 +- ... DefaultTangemPayCardDetailsRepository.kt} | 39 ++++++- .../repository/TangemPayRequestPerformer.kt | 13 ++- .../data/pay/util/TangemPayErrorConverter.kt | 9 +- .../domain/pay/TangemPayDetailsConfig.kt | 2 + .../tangem/domain/pay/model/CustomerInfo.kt | 10 +- ...y.kt => TangemPayCardDetailsRepository.kt} | 5 +- ...YieldSupplyGetRewardsBalanceUseCaseTest.kt | 8 +- .../entity/TangemPayDetailsStateFactory.kt | 79 +++++++++---- .../entity/TangemPayDetailsTopBarConfig.kt | 15 ++- .../tangempay/entity/TangemPayDetailsUM.kt | 16 ++- .../model/TangemPayCardDetailsBlockModel.kt | 4 +- .../model/TangemPayChangePinModel.kt | 4 +- .../tangempay/model/TangemPayDetailsModel.kt | 77 +++++++++++-- .../transformers/DetailsBalanceTransformer.kt | 6 +- .../TangemPayAddFundsUMConverter.kt | 4 +- ...ngemPayFreezeUnfreezeProcessTransformer.kt | 21 ++++ ...TangemPayFreezeUnfreezeStateTransformer.kt | 73 ++++++++++++ .../tangempay/ui/TangemPayAddFundsContent.kt | 3 +- .../tangempay/ui/TangemPayDetailsScreen.kt | 107 ++++++++++++++---- .../utils/TangemPayErrorMessageFactory.kt | 30 ----- .../utils/TangemPayMessagesFactory.kt | 70 ++++++++++++ .../TangemPayInitialStateTransformer.kt | 38 ++++--- 31 files changed, 635 insertions(+), 147 deletions(-) create mode 100644 core/datasource/src/main/java/com/tangem/datasource/api/pay/models/request/FreezeUnfreezeCardRequest.kt create mode 100644 core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/FreezeUnfreezeCardResponse.kt create mode 100644 core/ui/src/main/res/drawable/ic_snow_24.xml rename data/visa/src/main/kotlin/com/tangem/data/pay/repository/{DefaultCardDetailsRepository.kt => DefaultTangemPayCardDetailsRepository.kt} (75%) rename domain/visa/src/main/kotlin/com/tangem/domain/pay/repository/{CardDetailsRepository.kt => TangemPayCardDetailsRepository.kt} (77%) create mode 100644 features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/transformers/TangemPayFreezeUnfreezeProcessTransformer.kt create mode 100644 features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/transformers/TangemPayFreezeUnfreezeStateTransformer.kt delete mode 100644 features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/utils/TangemPayErrorMessageFactory.kt create mode 100644 features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/utils/TangemPayMessagesFactory.kt diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/pay/TangemPayApi.kt b/core/datasource/src/main/java/com/tangem/datasource/api/pay/TangemPayApi.kt index bb456d3a78..044c788bc7 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/pay/TangemPayApi.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/pay/TangemPayApi.kt @@ -153,4 +153,16 @@ interface TangemPayApi { @Header("Authorization") authHeader: String, @Body body: SetPinRequest, ): ApiResponse + + @POST("v1/customer/card/freeze") + suspend fun freezeCard( + @Header("Authorization") authHeader: String, + @Body body: FreezeUnfreezeCardRequest, + ): ApiResponse + + @POST("v1/customer/card/unfreeze") + suspend fun unfreezeCard( + @Header("Authorization") authHeader: String, + @Body body: FreezeUnfreezeCardRequest, + ): ApiResponse } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/request/FreezeUnfreezeCardRequest.kt b/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/request/FreezeUnfreezeCardRequest.kt new file mode 100644 index 0000000000..c0103ec683 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/request/FreezeUnfreezeCardRequest.kt @@ -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) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/CustomerMeResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/CustomerMeResponse.kt index 65a28d997e..63671044ad 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/CustomerMeResponse.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/CustomerMeResponse.kt @@ -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( diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/FreezeUnfreezeCardResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/FreezeUnfreezeCardResponse.kt new file mode 100644 index 0000000000..7ec1155bdc --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/FreezeUnfreezeCardResponse.kt @@ -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, + } +} \ No newline at end of file diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index 2dcc96ef01..dd4481e3db 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -1357,6 +1357,7 @@ You receive Choose token not available + Card frozen Deposit Dispute Explore transaction @@ -1364,7 +1365,9 @@ Fee Keep your money safe if your card is lost or stolen. You can unfreeze anytime. Freeze your card? + Failed to freeze the card. Try again later. Freeze + Your card is frozen. Get Help Completed Declined @@ -1373,6 +1376,8 @@ This fee goes to cover the cost of handling your transfer. Keep using your money. You can freeze anytime. Unfreeze your card? + Failed to unfreeze the card. Try again later. + Your card is unfrozen. Withdrawal Add funds Top-up options diff --git a/core/ui/src/main/res/drawable/ic_snow_24.xml b/core/ui/src/main/res/drawable/ic_snow_24.xml new file mode 100644 index 0000000000..7561e7996b --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_snow_24.xml @@ -0,0 +1,12 @@ + + + + + diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/DefaultTangemPayTopUpDataFactory.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/DefaultTangemPayTopUpDataFactory.kt index fafac4cce8..086f18ce33 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/pay/DefaultTangemPayTopUpDataFactory.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/DefaultTangemPayTopUpDataFactory.kt @@ -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 { - 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) } } } \ No newline at end of file diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/di/TangemPayDataModule.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/di/TangemPayDataModule.kt index 43a2e87cc9..9952a6cbeb 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/pay/di/TangemPayDataModule.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/di/TangemPayDataModule.kt @@ -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 diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultOnboardingRepository.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultOnboardingRepository.kt index 2f5582be20..5167b4adee 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultOnboardingRepository.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultOnboardingRepository.kt @@ -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, ) diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultCardDetailsRepository.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultTangemPayCardDetailsRepository.kt similarity index 75% rename from data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultCardDetailsRepository.kt rename to data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultTangemPayCardDetailsRepository.kt index 3478231e55..56b74801a3 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultCardDetailsRepository.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultTangemPayCardDetailsRepository.kt @@ -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 { return requestHelper.runWithErrorLogs(TAG) { @@ -112,6 +117,34 @@ internal class DefaultCardDetailsRepository @Inject constructor( } } + override suspend fun freezeCard(cardId: String): Either { + 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 { + 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) { diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/repository/TangemPayRequestPerformer.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/repository/TangemPayRequestPerformer.kt index cd24329183..32af571dc3 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/pay/repository/TangemPayRequestPerformer.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/repository/TangemPayRequestPerformer.kt @@ -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 makeSafeRequest( + requestBlock: suspend (header: String) -> ApiResponse, + ): Either { + return catch { request(requestBlock) } + .mapLeft { exception -> + Timber.tag("TangemPayRequestPerformer").e(exception) + errorConverter.convert(exception) + } + } + suspend fun request(requestBlock: suspend (header: String) -> ApiResponse): T = withContext(dispatchers.io) { performRequest( diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/util/TangemPayErrorConverter.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/util/TangemPayErrorConverter.kt index d5f0ebf004..7cf0044f76 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/pay/util/TangemPayErrorConverter.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/util/TangemPayErrorConverter.kt @@ -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 { +class TangemPayErrorConverter(moshi: Moshi) : Converter { - 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 { diff --git a/domain/visa/models/src/main/kotlin/com/tangem/domain/pay/TangemPayDetailsConfig.kt b/domain/visa/models/src/main/kotlin/com/tangem/domain/pay/TangemPayDetailsConfig.kt index b42e77c698..b8d8fcc74d 100644 --- a/domain/visa/models/src/main/kotlin/com/tangem/domain/pay/TangemPayDetailsConfig.kt +++ b/domain/visa/models/src/main/kotlin/com/tangem/domain/pay/TangemPayDetailsConfig.kt @@ -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, diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/pay/model/CustomerInfo.kt b/domain/visa/src/main/kotlin/com/tangem/domain/pay/model/CustomerInfo.kt index 9937d2e1ff..455e70bd12 100644 --- a/domain/visa/src/main/kotlin/com/tangem/domain/pay/model/CustomerInfo.kt +++ b/domain/visa/src/main/kotlin/com/tangem/domain/pay/model/CustomerInfo.kt @@ -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, diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/pay/repository/CardDetailsRepository.kt b/domain/visa/src/main/kotlin/com/tangem/domain/pay/repository/TangemPayCardDetailsRepository.kt similarity index 77% rename from domain/visa/src/main/kotlin/com/tangem/domain/pay/repository/CardDetailsRepository.kt rename to domain/visa/src/main/kotlin/com/tangem/domain/pay/repository/TangemPayCardDetailsRepository.kt index 426b1814a8..9732d15868 100644 --- a/domain/visa/src/main/kotlin/com/tangem/domain/pay/repository/CardDetailsRepository.kt +++ b/domain/visa/src/main/kotlin/com/tangem/domain/pay/repository/TangemPayCardDetailsRepository.kt @@ -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 @@ -17,4 +17,7 @@ interface CardDetailsRepository { suspend fun isAddToWalletDone(): Either suspend fun setAddToWalletAsDone(): Either + + suspend fun freezeCard(cardId: String): Either + suspend fun unfreezeCard(cardId: String): Either } \ No newline at end of file diff --git a/domain/yield-supply/src/test/java/com/tangem/domain/yield/supply/usecase/YieldSupplyGetRewardsBalanceUseCaseTest.kt b/domain/yield-supply/src/test/java/com/tangem/domain/yield/supply/usecase/YieldSupplyGetRewardsBalanceUseCaseTest.kt index d592d15e50..254c8c6d4a 100644 --- a/domain/yield-supply/src/test/java/com/tangem/domain/yield/supply/usecase/YieldSupplyGetRewardsBalanceUseCaseTest.kt +++ b/domain/yield-supply/src/test/java/com/tangem/domain/yield/supply/usecase/YieldSupplyGetRewardsBalanceUseCaseTest.kt @@ -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", ), ) diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/entity/TangemPayDetailsStateFactory.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/entity/TangemPayDetailsStateFactory.kt index 102e05307a..a51d398d92 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/entity/TangemPayDetailsStateFactory.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/entity/TangemPayDetailsStateFactory.kt @@ -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, + ) + } } \ No newline at end of file diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/entity/TangemPayDetailsTopBarConfig.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/entity/TangemPayDetailsTopBarConfig.kt index 85908dce5c..146ace6860 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/entity/TangemPayDetailsTopBarConfig.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/entity/TangemPayDetailsTopBarConfig.kt @@ -5,5 +5,16 @@ import kotlinx.collections.immutable.ImmutableList internal data class TangemPayDetailsTopBarConfig( val onBackClick: () -> Unit, - val items: ImmutableList?, -) \ No newline at end of file + val items: ImmutableList?, +) + +internal data class TangemPayDetailsTopBarMenuItem( + val type: TangemPayDetailsTopBarMenuItemType, + val dropdownItem: TangemDropdownMenuItem, +) + +internal enum class TangemPayDetailsTopBarMenuItemType { + ChangePin, + FreezeCard, + UnfreezeCard, +} \ No newline at end of file diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/entity/TangemPayDetailsUM.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/entity/TangemPayDetailsUM.kt index 2e59edf389..46a09a01fd 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/entity/TangemPayDetailsUM.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/entity/TangemPayDetailsUM.kt @@ -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 + abstract val frozenState: CardFrozenState data class Loading( override val actionButtons: ImmutableList, + override val frozenState: CardFrozenState, ) : TangemPayDetailsBalanceBlockState() data class Content( override val actionButtons: ImmutableList, + 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, + 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, diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayCardDetailsBlockModel.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayCardDetailsBlockModel.kt index ab79f60985..e4ca16b23d 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayCardDetailsBlockModel.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayCardDetailsBlockModel.kt @@ -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, diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayChangePinModel.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayChangePinModel.kt index 223519f8f0..826d5f4c0a 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayChangePinModel.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayChangePinModel.kt @@ -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 diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayDetailsModel.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayDetailsModel.kt index c77a21d442..22afc3b1eb 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayDetailsModel.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayDetailsModel.kt @@ -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 @@ -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)) } } \ No newline at end of file diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/transformers/DetailsBalanceTransformer.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/transformers/DetailsBalanceTransformer.kt index 61b1413c22..a357c0434d 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/transformers/DetailsBalanceTransformer.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/transformers/DetailsBalanceTransformer.kt @@ -18,7 +18,10 @@ internal class DetailsBalanceTransformer( override fun transform(prevState: TangemPayDetailsUM): TangemPayDetailsUM { val balance = when (balance) { is Either.Left -> { - TangemPayDetailsBalanceBlockState.Error(persistentListOf()) + TangemPayDetailsBalanceBlockState.Error( + actionButtons = persistentListOf(), + frozenState = prevState.balanceBlockState.frozenState, + ) } is Either.Right -> { 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, ) } } diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/transformers/TangemPayAddFundsUMConverter.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/transformers/TangemPayAddFundsUMConverter.kt index 1f2c4478f2..3206ef6bc0 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/transformers/TangemPayAddFundsUMConverter.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/transformers/TangemPayAddFundsUMConverter.kt @@ -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, ) diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/transformers/TangemPayFreezeUnfreezeProcessTransformer.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/transformers/TangemPayFreezeUnfreezeProcessTransformer.kt new file mode 100644 index 0000000000..8b696b4b6f --- /dev/null +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/transformers/TangemPayFreezeUnfreezeProcessTransformer.kt @@ -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 { + 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) + } +} \ No newline at end of file diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/transformers/TangemPayFreezeUnfreezeStateTransformer.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/transformers/TangemPayFreezeUnfreezeStateTransformer.kt new file mode 100644 index 0000000000..c1a0d6ca7a --- /dev/null +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/transformers/TangemPayFreezeUnfreezeStateTransformer.kt @@ -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 { + + 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?, + ): ImmutableList? { + 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, + ) + } + } +} \ No newline at end of file diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayAddFundsContent.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayAddFundsContent.kt index badb84e945..5abb62dfac 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayAddFundsContent.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayAddFundsContent.kt @@ -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) } diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayDetailsScreen.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayDetailsScreen.kt index e3b08338ea..a9814206d6 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayDetailsScreen.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayDetailsScreen.kt @@ -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 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() } - } - } - } - } -} \ No newline at end of file diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/utils/TangemPayMessagesFactory.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/utils/TangemPayMessagesFactory.kt new file mode 100644 index 0000000000..94b52cde6c --- /dev/null +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/utils/TangemPayMessagesFactory.kt @@ -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() + } + } + } + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TangemPayInitialStateTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TangemPayInitialStateTransformer.kt index a83f41f964..3ae2ad80f0 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TangemPayInitialStateTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TangemPayInitialStateTransformer.kt @@ -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)