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 3c1f040b64..bcbcd299b2 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 @@ -85,6 +85,18 @@ interface TangemPayApi { @Body body: FreezeUnfreezeCardRequest, ): ApiResponse + @GET("v1/fees/{type}") + suspend fun getFee( + @Header("Authorization") authHeader: String, + @Path("type") type: String, + ): ApiResponse + + @POST("v1/customer/card/reissue") + suspend fun reissueCard( + @Header("Authorization") authHeader: String, + @Body body: ReissueCardRequest, + ): ApiResponse + @POST("v1/customer/card/withdraw/data") suspend fun getWithdrawData( @Header("Authorization") authHeader: String, diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/request/ReissueCardRequest.kt b/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/request/ReissueCardRequest.kt new file mode 100644 index 0000000000..26b2f0dd53 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/request/ReissueCardRequest.kt @@ -0,0 +1,9 @@ +package com.tangem.datasource.api.pay.models.request + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class ReissueCardRequest( + @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/FeeResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/FeeResponse.kt new file mode 100644 index 0000000000..f18a34f075 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/FeeResponse.kt @@ -0,0 +1,17 @@ +package com.tangem.datasource.api.pay.models.response + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class FeeResponse( + @Json(name = "result") val result: Result, +) { + @JsonClass(generateAdapter = true) + data class Result( + @Json(name = "type") val type: String, + @Json(name = "amount") val amount: String, + @Json(name = "currency") val currency: String, + @Json(name = "description") val description: String, + ) +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/ReissueCardResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/ReissueCardResponse.kt new file mode 100644 index 0000000000..49fb34198e --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/ReissueCardResponse.kt @@ -0,0 +1,15 @@ +package com.tangem.datasource.api.pay.models.response + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class ReissueCardResponse( + @Json(name = "result") val result: Result, +) { + @JsonClass(generateAdapter = true) + data class Result( + @Json(name = "order_id") val orderId: String, + @Json(name = "status") val status: String, + ) +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/di/TangemPayStoresModule.kt b/core/datasource/src/main/java/com/tangem/datasource/di/TangemPayStoresModule.kt index ae3097d006..135c8bfaa8 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/di/TangemPayStoresModule.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/di/TangemPayStoresModule.kt @@ -2,7 +2,9 @@ package com.tangem.datasource.di import com.tangem.datasource.local.datastore.RuntimeDataStore import com.tangem.datasource.local.visa.DefaultTangemPayCardFrozenStateStore +import com.tangem.datasource.local.visa.DefaultTangemPayReissueCardStore import com.tangem.datasource.local.visa.TangemPayCardFrozenStateStore +import com.tangem.datasource.local.visa.TangemPayReissueCardStore import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -20,4 +22,12 @@ internal object TangemPayStoresModule { dataStore = RuntimeDataStore(), ) } + + @Provides + @Singleton + fun provideTangemPayReissueCardStore(): TangemPayReissueCardStore { + return DefaultTangemPayReissueCardStore( + feeStore = RuntimeDataStore(), + ) + } } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/visa/DefaultTangemPayReissueCardStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/visa/DefaultTangemPayReissueCardStore.kt new file mode 100644 index 0000000000..d179f07eb3 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/local/visa/DefaultTangemPayReissueCardStore.kt @@ -0,0 +1,30 @@ +package com.tangem.datasource.local.visa + +import com.tangem.datasource.local.datastore.RuntimeDataStore +import com.tangem.domain.models.TangemPayReissueCardFee +import com.tangem.domain.models.wallet.UserWalletId + +internal class DefaultTangemPayReissueCardStore( + private val feeStore: RuntimeDataStore, +) : TangemPayReissueCardStore { + + override suspend fun storeReissueFee( + userWalletId: UserWalletId, + tangemPayReissueCardFee: TangemPayReissueCardFee, + ) { + feeStore.store(userWalletId.stringValue, tangemPayReissueCardFee) + } + + override suspend fun getReissueFee(userWalletId: UserWalletId): TangemPayReissueCardFee? { + return feeStore.getSyncOrNull(userWalletId.stringValue) + } + + override suspend fun storeReissueOrderId(cardId: String, orderId: String) { + // TODO v_rodionov: #[REDACTED_TASK_KEY] store orderId in app prefs + } + + override suspend fun getOrderId(cardId: String): String? { + // TODO v_rodionov: #[REDACTED_TASK_KEY] store orderId in app prefs + return null + } +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/visa/TangemPayReissueCardStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/visa/TangemPayReissueCardStore.kt new file mode 100644 index 0000000000..bc7825051f --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/local/visa/TangemPayReissueCardStore.kt @@ -0,0 +1,15 @@ +package com.tangem.datasource.local.visa + +import com.tangem.domain.models.TangemPayReissueCardFee +import com.tangem.domain.models.wallet.UserWalletId + +interface TangemPayReissueCardStore { + + suspend fun storeReissueFee(userWalletId: UserWalletId, tangemPayReissueCardFee: TangemPayReissueCardFee) + + suspend fun getReissueFee(userWalletId: UserWalletId): TangemPayReissueCardFee? + + suspend fun storeReissueOrderId(cardId: String, orderId: String) + + suspend fun getOrderId(cardId: String): String? +} \ 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 93d7812b80..4f79cdf591 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -1556,6 +1556,17 @@ Explore transaction Service fees Fee + Replace card + Replace your card? + This generates a new set of card details. Your old details will stop working. You can\'t undo this. + Replacement fee + Replace card + Replacing your digital card + Usually takes up to 5 minutes. In rare cases, up to 48 hours. + Insufficient funds to replace the card + Unable to cover fee + Deposit USDC to payment account to cover the issuing fee + Replacement fee info unreachable Keep your money safe. You can unfreeze anytime. Freeze your card? Failed to freeze the card. Try again later. diff --git a/core/ui/src/main/res/drawable/ic_update_32.xml b/core/ui/src/main/res/drawable/ic_update_32.xml new file mode 100644 index 0000000000..3be862185a --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_update_32.xml @@ -0,0 +1,9 @@ + + + diff --git a/core/ui/src/main/res/drawable/img_usdc_16.xml b/core/ui/src/main/res/drawable/img_usdc_16.xml new file mode 100644 index 0000000000..460fa8ad13 --- /dev/null +++ b/core/ui/src/main/res/drawable/img_usdc_16.xml @@ -0,0 +1,18 @@ + + + + + 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 d697221c1b..7d55c0625d 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 @@ -10,6 +10,7 @@ import com.tangem.data.pay.converter.PaymentAccountStatusValueDMConverter import com.tangem.data.pay.flow.DefaultPaymentAccountStatusFetcher import com.tangem.data.pay.flow.DefaultPaymentAccountStatusProducer import com.tangem.data.pay.repository.* +import com.tangem.domain.pay.repository.TangemPayReissueCardRepository import com.tangem.data.pay.store.PaymentAccountStatusesStore import com.tangem.data.pay.usecase.DefaultGetTangemPayCurrencyStatusUseCase import com.tangem.data.pay.usecase.DefaultGetTangemPayCustomerIdUseCase @@ -71,6 +72,10 @@ internal interface TangemPayDataModule { @Singleton fun bindCustomerOrderRepository(repository: DefaultCustomerOrderRepository): CustomerOrderRepository + @Binds + @Singleton + fun bindReissueCardRepository(repository: DefaultReissueCardRepository): TangemPayReissueCardRepository + @Binds @Singleton fun bindTangemPayCryptoCurrencyFactory( diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultReissueCardRepository.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultReissueCardRepository.kt new file mode 100644 index 0000000000..abffaa87fc --- /dev/null +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultReissueCardRepository.kt @@ -0,0 +1,104 @@ +package com.tangem.data.pay.repository + +import arrow.core.Either +import arrow.core.raise.either +import arrow.core.right +import com.tangem.core.error.UniversalError +import com.tangem.datasource.api.pay.TangemPayApi +import com.tangem.datasource.api.pay.models.request.ReissueCardRequest +import com.tangem.datasource.api.pay.models.response.OrderResponse +import com.tangem.datasource.local.visa.TangemPayReissueCardStore +import com.tangem.domain.models.TangemPayReissueCardFee +import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.pay.model.OrderStatus +import com.tangem.domain.pay.model.TangemPayReissueOrderInfo +import com.tangem.domain.pay.repository.TangemPayReissueCardRepository +import com.tangem.domain.visa.error.VisaApiError +import com.tangem.utils.coroutines.runSuspendCatching +import javax.inject.Inject + +internal class DefaultReissueCardRepository @Inject constructor( + private val tangemPayApi: TangemPayApi, + private val requestHelper: TangemPayRequestPerformer, + private val tangemPayReissueCardStore: TangemPayReissueCardStore, +) : TangemPayReissueCardRepository { + + override suspend fun getReissueCardFee(userWalletId: UserWalletId): Either = + either { + runSuspendCatching { + tangemPayReissueCardStore.getReissueFee(userWalletId)?.let { return Either.Right(it) } + } + + val response = requestHelper.performRequest(userWalletId) { authHeader -> + tangemPayApi.getFee( + authHeader = authHeader, + type = CARD_REPLACEMENT_FEE_TYPE, + ) + }.bind() + + val result = response.result + val fee = TangemPayReissueCardFee( + amount = result.amount.toBigDecimal(), + currencyCode = result.currency, + ) + + runSuspendCatching { + tangemPayReissueCardStore.storeReissueFee(userWalletId, fee) + } + + fee + } + + override suspend fun reissueCard( + userWalletId: UserWalletId, + cardId: String, + ): Either = either { + val response = requestHelper.performRequest(userWalletId) { authHeader -> + tangemPayApi.reissueCard( + authHeader = authHeader, + body = ReissueCardRequest(cardId = cardId), + ) + }.bind() + + TangemPayReissueOrderInfo(response.result.orderId, OrderStatus.fromString(response.result.status)) + } + + override suspend fun storeReissueOrderId(cardId: String, orderId: String): Either = + runSuspendCatching { + tangemPayReissueCardStore.storeReissueOrderId(cardId, orderId) + }.fold( + onSuccess = { Unit.right() }, + onFailure = { Either.Left(VisaApiError.Unspecified) }, + ) + + override suspend fun getReissueOrderInfo( + userWalletId: UserWalletId, + cardId: String, + ): Either = either { + val orderId = runSuspendCatching { tangemPayReissueCardStore.getOrderId(cardId) }.getOrNull() + + if (orderId == null) { + return null.right() + } + + val order = requestHelper.performRequest(userWalletId) { authHeader -> + tangemPayApi.getOrder(authHeader, orderId) + }.bind() + + val result = order.result ?: raise(VisaApiError.Unspecified) + + TangemPayReissueOrderInfo( + orderId = result.id, + orderStatus = when (result.status) { + OrderResponse.Result.Status.NEW -> OrderStatus.NEW + OrderResponse.Result.Status.PROCESSING -> OrderStatus.PROCESSING + OrderResponse.Result.Status.COMPLETED -> OrderStatus.COMPLETED + OrderResponse.Result.Status.CANCELED -> OrderStatus.CANCELED + }, + ) + } + + private companion object { + const val CARD_REPLACEMENT_FEE_TYPE = "CARD_REPLACEMENT" + } +} \ No newline at end of file diff --git a/domain/models/src/main/kotlin/com/tangem/domain/models/TangemPayReissueCardFee.kt b/domain/models/src/main/kotlin/com/tangem/domain/models/TangemPayReissueCardFee.kt new file mode 100644 index 0000000000..515857f8a7 --- /dev/null +++ b/domain/models/src/main/kotlin/com/tangem/domain/models/TangemPayReissueCardFee.kt @@ -0,0 +1,8 @@ +package com.tangem.domain.models + +import java.math.BigDecimal + +data class TangemPayReissueCardFee( + val amount: BigDecimal, + val currencyCode: String, +) \ No newline at end of file diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/pay/model/OrderStatus.kt b/domain/visa/src/main/kotlin/com/tangem/domain/pay/model/OrderStatus.kt index 327d8fd61f..c304832170 100644 --- a/domain/visa/src/main/kotlin/com/tangem/domain/pay/model/OrderStatus.kt +++ b/domain/visa/src/main/kotlin/com/tangem/domain/pay/model/OrderStatus.kt @@ -1,9 +1,22 @@ package com.tangem.domain.pay.model +import java.util.Locale + enum class OrderStatus { UNKNOWN, // TODO remove it after TangemPay accounts refactor TANGEM_PAY_ACCOUNTS_REFACTOR_ENABLED NEW, PROCESSING, COMPLETED, CANCELED, + ; + + companion object { + fun fromString(value: String) = when (value.uppercase(Locale.US)) { + "NEW" -> NEW + "PROCESSING" -> PROCESSING + "COMPLETED" -> COMPLETED + "CANCELED" -> CANCELED + else -> UNKNOWN + } + } } \ No newline at end of file diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/pay/model/TangemPayReissueOrderInfo.kt b/domain/visa/src/main/kotlin/com/tangem/domain/pay/model/TangemPayReissueOrderInfo.kt new file mode 100644 index 0000000000..45945f7204 --- /dev/null +++ b/domain/visa/src/main/kotlin/com/tangem/domain/pay/model/TangemPayReissueOrderInfo.kt @@ -0,0 +1,6 @@ +package com.tangem.domain.pay.model + +data class TangemPayReissueOrderInfo( + val orderId: String, + val orderStatus: OrderStatus, +) \ No newline at end of file diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/pay/repository/TangemPayReissueCardRepository.kt b/domain/visa/src/main/kotlin/com/tangem/domain/pay/repository/TangemPayReissueCardRepository.kt new file mode 100644 index 0000000000..2342faff2c --- /dev/null +++ b/domain/visa/src/main/kotlin/com/tangem/domain/pay/repository/TangemPayReissueCardRepository.kt @@ -0,0 +1,22 @@ +package com.tangem.domain.pay.repository + +import arrow.core.Either +import com.tangem.core.error.UniversalError +import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.models.TangemPayReissueCardFee +import com.tangem.domain.pay.model.TangemPayReissueOrderInfo +import com.tangem.domain.visa.error.VisaApiError + +interface TangemPayReissueCardRepository { + + suspend fun getReissueCardFee(userWalletId: UserWalletId): Either + + suspend fun reissueCard(userWalletId: UserWalletId, cardId: String): Either + + suspend fun storeReissueOrderId(cardId: String, orderId: String): Either + + suspend fun getReissueOrderInfo( + userWalletId: UserWalletId, + cardId: String, + ): Either +} \ No newline at end of file diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/tangempay/TangemPayAnalyticsEvents.kt b/domain/visa/src/main/kotlin/com/tangem/domain/tangempay/TangemPayAnalyticsEvents.kt index f7eb452cc2..03f5d83bfe 100644 --- a/domain/visa/src/main/kotlin/com/tangem/domain/tangempay/TangemPayAnalyticsEvents.kt +++ b/domain/visa/src/main/kotlin/com/tangem/domain/tangempay/TangemPayAnalyticsEvents.kt @@ -192,6 +192,21 @@ sealed class TangemPayAnalyticsEvents( event = "Visa KYC Canceled", ) + class ReplaceCardClicked : TangemPayAnalyticsEvents( + categoryName = "Visa Screen", + event = "Visa Replace Card Clicked", + ) + + class ReplaceCardConfirmationPopupOpened : TangemPayAnalyticsEvents( + categoryName = "Visa Screen", + event = "Visa Replace Card Confirmation Popup Opened", + ) + + class ReplaceCardConfirmed : TangemPayAnalyticsEvents( + categoryName = "Visa Screen", + event = "Visa Replace Card Confirmed", + ) + class MainVisaPermanentBannerClicked : TangemPayAnalyticsEvents( categoryName = "Visa Onboarding", event = "Visa Permanent Banner Clicked", diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/components/DefaultTangemPayCardPageComponent.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/components/DefaultTangemPayCardPageComponent.kt index 7b9e24e79b..de2c4bf4a2 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/components/DefaultTangemPayCardPageComponent.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/components/DefaultTangemPayCardPageComponent.kt @@ -14,6 +14,7 @@ import com.tangem.core.decompose.context.childByContext import com.tangem.core.decompose.navigation.inner.InnerRouter import com.tangem.core.ui.decompose.ComposableContentComponent import com.tangem.features.tangempay.navigation.TangemPayDetailsInnerRoute +import com.tangem.features.tokenreceive.TokenReceiveComponent import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject @@ -21,6 +22,7 @@ import dagger.assisted.AssistedInject internal class DefaultTangemPayCardPageComponent @AssistedInject constructor( @Assisted private val appComponentContext: AppComponentContext, @Assisted private val params: TangemPayCardPageComponent.Params, + private val tokenReceiveComponentFactory: TokenReceiveComponent.Factory, ) : AppComponentContext by appComponentContext, TangemPayCardPageComponent { private val stackNavigation = StackNavigation() @@ -56,6 +58,7 @@ internal class DefaultTangemPayCardPageComponent @AssistedInject constructor( TangemPayDetailsInnerRoute.Details -> TangemPayCardPageScreenComponent( appComponentContext = childByContext(componentContext = componentContext, router = innerRouter), params = params, + tokenReceiveComponentFactory = tokenReceiveComponentFactory, ) TangemPayDetailsInnerRoute.ChangePIN -> TangemPayChangePinComponent( appComponentContext = childByContext(componentContext = componentContext, router = innerRouter), diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/components/TangemPayCardPageScreenComponent.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/components/TangemPayCardPageScreenComponent.kt index e0f2994b3c..039bcab003 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/components/TangemPayCardPageScreenComponent.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/components/TangemPayCardPageScreenComponent.kt @@ -7,6 +7,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.arkivanov.decompose.ComponentContext import com.arkivanov.decompose.extensions.compose.subscribeAsState import com.arkivanov.decompose.router.slot.childSlot +import com.arkivanov.decompose.router.slot.dismiss import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.decompose.context.child import com.tangem.core.decompose.context.childByContext @@ -19,10 +20,12 @@ import com.tangem.features.tangempay.components.cardDetails.TangemPayCardDetails import com.tangem.features.tangempay.entity.TangemPayDetailsNavigation import com.tangem.features.tangempay.model.TangemPayCardPageModel import com.tangem.features.tangempay.ui.TangemPayCardPageScreen +import com.tangem.features.tokenreceive.TokenReceiveComponent internal class TangemPayCardPageScreenComponent( private val appComponentContext: AppComponentContext, private val params: TangemPayCardPageComponent.Params, + private val tokenReceiveComponentFactory: TokenReceiveComponent.Factory, ) : AppComponentContext by appComponentContext, ComposableContentComponent { private val model: TangemPayCardPageModel = getOrCreateModel(params = params) @@ -57,7 +60,9 @@ internal class TangemPayCardPageScreenComponent( TangemPayCardPageScreen( state = state, cardDetailsBlockComponent = cardDetailsBlockComponent, - cardDetailsState = cardDetailsState, + cardDetailsState = cardDetailsState.copy( + isActive = !state.isReissueInProgress, + ), modifier = modifier, ) bottomSheet.child?.instance?.BottomSheet() @@ -77,6 +82,32 @@ internal class TangemPayCardPageScreenComponent( listener = model, ), ) + is TangemPayDetailsNavigation.ReissueCard -> TangemPayReissueCardComponent( + appComponentContext = context, + params = TangemPayReissueCardComponent.Params( + listener = model, + userWalletId = params.userWalletId, + cardId = params.config.cardId, + ), + ) + is TangemPayDetailsNavigation.AddFunds -> TangemPayAddFundsComponent( + appComponentContext = context, + params = TangemPayAddFundsComponent.Params( + listener = model, + walletId = navigation.walletId, + cryptoBalance = navigation.cryptoBalance, + fiatBalance = navigation.fiatBalance, + depositAddress = navigation.depositAddress, + chainId = navigation.chainId, + ), + ) + is TangemPayDetailsNavigation.Receive -> tokenReceiveComponentFactory.create( + context = context, + params = TokenReceiveComponent.Params( + config = navigation.config, + onDismiss = model.bottomSheetNavigation::dismiss, + ), + ) else -> error("Unsupported bottom sheet navigation: $navigation") } } diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/components/TangemPayDetailsComponent.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/components/TangemPayDetailsComponent.kt index c193b44545..b59577ebe8 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/components/TangemPayDetailsComponent.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/components/TangemPayDetailsComponent.kt @@ -130,6 +130,7 @@ internal class TangemPayDetailsComponent( listener = model, ), ) + else -> error("Unsupported bottom sheet navigation: $navigation") } } } \ No newline at end of file diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/components/TangemPayReissueCardComponent.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/components/TangemPayReissueCardComponent.kt new file mode 100644 index 0000000000..13f1c7c061 --- /dev/null +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/components/TangemPayReissueCardComponent.kt @@ -0,0 +1,40 @@ +package com.tangem.features.tangempay.components + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.decompose.model.getOrCreateModel +import com.tangem.core.ui.decompose.ComposableBottomSheetComponent +import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.pay.model.TangemPayReissueOrderInfo +import com.tangem.features.tangempay.model.TangemPayReissueCardModel +import com.tangem.features.tangempay.ui.TangemPayReissueCardContent + +internal class TangemPayReissueCardComponent( + appComponentContext: AppComponentContext, + private val params: Params, +) : ComposableBottomSheetComponent, AppComponentContext by appComponentContext { + + private val model: TangemPayReissueCardModel = getOrCreateModel(params = params) + + override fun dismiss() = model.onDismiss() + + @Composable + override fun BottomSheet() { + val state by model.state.collectAsStateWithLifecycle() + TangemPayReissueCardContent(state = state) + } + + data class Params( + val listener: ReissueCardListener, + val userWalletId: UserWalletId, + val cardId: String, + ) +} + +internal interface ReissueCardListener { + fun onReissueOrderCreate(order: TangemPayReissueOrderInfo) + fun onDismissReissueCard() + fun onClickAddFunds() +} \ No newline at end of file diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/di/TangemPayModelModule.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/di/TangemPayModelModule.kt index 623eea9c2a..d9b0f67529 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/di/TangemPayModelModule.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/di/TangemPayModelModule.kt @@ -11,6 +11,7 @@ import com.tangem.features.tangempay.model.TangemPayDetailsModel import com.tangem.features.tangempay.model.TangemPayEditDisplayNameModel import com.tangem.features.tangempay.model.TangemPayTxHistoryDetailsModel import com.tangem.features.tangempay.model.TangemPayTxHistoryModel +import com.tangem.features.tangempay.model.TangemPayReissueCardModel import com.tangem.features.tangempay.model.TangemPayViewPinModel import dagger.Binds import dagger.Module @@ -71,4 +72,9 @@ internal interface TangemPayModelModule { @IntoMap @ClassKey(TangemPayEditDisplayNameModel::class) fun bindTangemPayEditDisplayNameModel(model: TangemPayEditDisplayNameModel): Model + + @Binds + @IntoMap + @ClassKey(TangemPayReissueCardModel::class) + fun bindTangemPayReissueCardModel(model: TangemPayReissueCardModel): Model } \ No newline at end of file diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/entity/TangemPayCardPageUM.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/entity/TangemPayCardPageUM.kt index 8a16bf6d94..93bda7eba7 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/entity/TangemPayCardPageUM.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/entity/TangemPayCardPageUM.kt @@ -1,39 +1,37 @@ package com.tangem.features.tangempay.entity import androidx.compose.runtime.Immutable +import com.tangem.core.ui.extensions.TextReference import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf @Immutable internal data class TangemPayCardPageUM( - val addToWalletBlockState: AddToWalletBlockState? = null, - val settings: ImmutableList = persistentListOf( - TangemPayCardPageSetting.ChangePIN, - TangemPayCardPageSetting.FreezeCard, - ), + val settings: ImmutableList, val onBackClick: () -> Unit, - val onSettingClick: (TangemPayCardPageSetting) -> Unit, + val addToWalletBlockState: AddToWalletBlockState? = null, + val isReissueInProgress: Boolean = false, ) { companion object { fun stub( addToWalletBlockState: AddToWalletBlockState? = AddToWalletBlockState(onClick = {}, onClickClose = {}), settings: ImmutableList = persistentListOf( - TangemPayCardPageSetting.ChangePIN, - TangemPayCardPageSetting.FreezeCard, - TangemPayCardPageSetting.ReplaceCard, + TangemPayCardPageSetting(TextReference.Str("Pin Code")) {}, + TangemPayCardPageSetting(TextReference.Str("Freeze Card")) {}, + TangemPayCardPageSetting(TextReference.Str("Reissue Card")) {}, ), + isReissueInProgress: Boolean = false, ) = TangemPayCardPageUM( addToWalletBlockState = addToWalletBlockState, settings = settings, onBackClick = {}, - onSettingClick = {}, + isReissueInProgress = isReissueInProgress, ) } } @Immutable -internal sealed class TangemPayCardPageSetting { - data object ChangePIN : TangemPayCardPageSetting() - data object FreezeCard : TangemPayCardPageSetting() - data object ReplaceCard : TangemPayCardPageSetting() -} \ No newline at end of file +internal data class TangemPayCardPageSetting( + val title: TextReference, + val onSettingClick: () -> Unit, +) \ No newline at end of file diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/entity/TangemPayDetailsNavigation.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/entity/TangemPayDetailsNavigation.kt index 5f10a9ed77..fdc5357eae 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/entity/TangemPayDetailsNavigation.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/entity/TangemPayDetailsNavigation.kt @@ -32,4 +32,7 @@ internal sealed class TangemPayDetailsNavigation { val userWalletId: UserWalletId, val cardId: String, ) : TangemPayDetailsNavigation() + + @Serializable + data object ReissueCard : TangemPayDetailsNavigation() } \ 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 5660713341..7eb7b01df6 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 @@ -31,6 +31,7 @@ internal data class TangemPayCardDetailsUM( val isLoading: Boolean = false, val cardFrozenState: TangemPayCardFrozenState, val displayNameState: DisplayNameState?, + val isActive: Boolean = true, ) internal sealed interface DisplayNameState { diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/entity/TangemPayReissueCardUM.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/entity/TangemPayReissueCardUM.kt new file mode 100644 index 0000000000..f08efa4032 --- /dev/null +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/entity/TangemPayReissueCardUM.kt @@ -0,0 +1,37 @@ +package com.tangem.features.tangempay.entity + +import androidx.compose.runtime.Immutable + +@Immutable +internal data class TangemPayReissueCardUM( + val feeAmount: String, + val isFeeLoading: Boolean, + val isReissuingInProgress: Boolean, + val error: TangemPayReissueCardError?, + val onConfirmClick: () -> Unit, + val onRetryFee: () -> Unit, + val onAddFundsClick: () -> Unit, + val onDismissRequest: () -> Unit, +) { + companion object { + fun stub( + feeAmount: String = "$4.25", + isFeeLoading: Boolean = false, + error: TangemPayReissueCardError = TangemPayReissueCardError.InitialDataLoading, + isReissuingInProgress: Boolean = false, + ) = TangemPayReissueCardUM( + feeAmount = feeAmount, + isFeeLoading = isFeeLoading, + error = error, + isReissuingInProgress = isReissuingInProgress, + onConfirmClick = {}, + onRetryFee = {}, + onAddFundsClick = {}, + onDismissRequest = {}, + ) + } +} + +internal enum class TangemPayReissueCardError { + InsufficientFunds, InitialDataLoading +} \ No newline at end of file diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayCardPageModel.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayCardPageModel.kt index b08ca54fbb..681148be62 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayCardPageModel.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayCardPageModel.kt @@ -4,15 +4,27 @@ import androidx.compose.runtime.Stable import com.arkivanov.decompose.router.slot.SlotNavigation import com.arkivanov.decompose.router.slot.activate import com.arkivanov.decompose.router.slot.dismiss +import com.tangem.common.routing.AppRoute +import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.core.analytics.models.AnalyticsParam import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer import com.tangem.core.decompose.navigation.Router import com.tangem.core.decompose.ui.UiMessageSender +import com.tangem.core.ui.extensions.TextReference 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.model.OrderStatus +import com.tangem.domain.pay.model.TangemPayReissueOrderInfo +import com.tangem.domain.pay.model.TangemPayTopUpData import com.tangem.domain.pay.repository.TangemPayCardDetailsRepository +import com.tangem.domain.pay.repository.TangemPayReissueCardRepository +import com.tangem.domain.tangempay.TangemPayAnalyticsEvents import com.tangem.domain.visa.model.TangemPayCardFrozenState +import com.tangem.features.tangempay.components.AddFundsListener +import com.tangem.features.tangempay.components.ReissueCardListener import com.tangem.features.tangempay.components.TangemPayCardPageComponent import com.tangem.features.tangempay.components.ViewPinListener import com.tangem.features.tangempay.details.impl.R @@ -25,6 +37,7 @@ import com.tangem.features.tangempay.utils.TangemPayMessagesFactory import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.JobHolder import com.tangem.utils.coroutines.saveIn +import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.launchIn @@ -33,43 +46,55 @@ import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import javax.inject.Inject +@Suppress("LongParameterList") @Stable @ModelScoped internal class TangemPayCardPageModel @Inject constructor( paramsContainer: ParamsContainer, override val dispatchers: CoroutineDispatcherProvider, private val router: Router, + private val analytics: AnalyticsEventHandler, private val cardDetailsRepository: TangemPayCardDetailsRepository, private val uiMessageSender: UiMessageSender, -) : Model(), ViewPinListener { + private val reissueCardRepository: TangemPayReissueCardRepository, +) : Model(), ViewPinListener, ReissueCardListener, AddFundsListener { private val params: TangemPayCardPageComponent.Params = paramsContainer.require() private var currentFrozenState: TangemPayCardFrozenState = params.config.cardFrozenState private val addToWalletBannerJobHolder = JobHolder() + private val addFundsJobHolder = JobHolder() val uiState: StateFlow field = MutableStateFlow( TangemPayCardPageUM( onBackClick = router::pop, - onSettingClick = ::onSettingClick, + settings = persistentListOf( + TangemPayCardPageSetting( + title = TextReference.Res(R.string.tangempay_card_details_change_pin), + onSettingClick = ::onClickChangePIN, + ), + TangemPayCardPageSetting( + title = TextReference.Res(R.string.tangempay_card_details_freeze_card), + onSettingClick = ::onClickFreezeOrUnfreezeCard, + ), + TangemPayCardPageSetting( + title = TextReference.Res(R.string.tangempay_card_details_reissue_card), + onSettingClick = ::onClickReissueCard, + ), + ), ), ) val bottomSheetNavigation: SlotNavigation = SlotNavigation() init { + // TODO v_rodionov: #[REDACTED_TASK_KEY] check reissue order state before card details are showed fetchAddToWalletBanner() subscribeToCardFrozenState() } - private fun onSettingClick(setting: TangemPayCardPageSetting) = when (setting) { - TangemPayCardPageSetting.ChangePIN -> onClickChangePIN() - TangemPayCardPageSetting.FreezeCard -> onClickFreezeOrUnfreezeCard() - TangemPayCardPageSetting.ReplaceCard -> Unit // TODO v_rodionov #[REDACTED_TASK_KEY] - } - private fun onClickChangePIN() { if (!params.config.isPinSet) { router.push(TangemPayDetailsInnerRoute.ChangePIN) @@ -94,6 +119,82 @@ internal class TangemPayCardPageModel @Inject constructor( } } + private fun onClickReissueCard() { + analytics.send(TangemPayAnalyticsEvents.ReplaceCardClicked()) + bottomSheetNavigation.activate(TangemPayDetailsNavigation.ReissueCard) + } + + override fun onReissueOrderCreate(order: TangemPayReissueOrderInfo) { + bottomSheetNavigation.dismiss() + onReissueOrderStatusReceived(order.orderStatus) + if (order.orderStatus != OrderStatus.CANCELED) { + modelScope.launch { + reissueCardRepository.storeReissueOrderId(params.config.cardId, order.orderId) + } + } else { + uiMessageSender.send(SnackbarMessage(resourceReference(R.string.common_something_went_wrong))) + } + } + + override fun onDismissReissueCard() { + bottomSheetNavigation.dismiss() + } + + override fun onClickAddFunds() { + bottomSheetNavigation.dismiss() + modelScope.launch { + val balance = cardDetailsRepository.getCardBalance(params.userWalletId).getOrNull() + val depositAddress = balance?.depositAddress + if (balance == null || depositAddress == null) { + uiMessageSender.send(SnackbarMessage(resourceReference(R.string.common_error))) + return@launch + } + bottomSheetNavigation.activate( + TangemPayDetailsNavigation.AddFunds( + walletId = params.userWalletId, + fiatBalance = balance.fiatBalance, + cryptoBalance = balance.cryptoBalance, + depositAddress = depositAddress, + chainId = params.config.chainId, + ), + ) + }.saveIn(addFundsJobHolder) + } + + override fun onClickReceive(data: TangemPayTopUpData) { + bottomSheetNavigation.dismiss() + val config = TokenReceiveConfig( + shouldShowWarning = true, + cryptoCurrency = data.currency, + userWalletId = data.walletId, + showMemoDisclaimer = false, + receiveAddress = data.receiveAddress, + ) + bottomSheetNavigation.activate(TangemPayDetailsNavigation.Receive(config)) + } + + override fun onClickSwap(data: TangemPayTopUpData) { + bottomSheetNavigation.dismiss() + router.push( + AppRoute.Swap( + currencyFrom = data.currency, + userWalletId = data.walletId, + isInitialReverseOrder = true, + screenSource = AnalyticsParam.ScreensSources.TangemPay.value, + tangemPayInput = AppRoute.Swap.TangemPayInput( + cryptoAmount = data.cryptoBalance, + fiatAmount = data.fiatBalance, + depositAddress = data.depositAddress, + isWithdrawal = false, + ), + ), + ) + } + + override fun onDismissAddFunds() { + bottomSheetNavigation.dismiss() + } + private fun freezeCard() { modelScope.launch { cardDetailsRepository.freezeCard( @@ -174,4 +275,18 @@ internal class TangemPayCardPageModel @Inject constructor( override fun onDismissViewPin() { bottomSheetNavigation.dismiss() } + + private fun onReissueOrderStatusReceived(orderStatus: OrderStatus) { + when (orderStatus) { + OrderStatus.NEW, OrderStatus.PROCESSING, OrderStatus.COMPLETED, OrderStatus.UNKNOWN -> { + uiState.update { state -> + state.copy( + addToWalletBlockState = null, + isReissueInProgress = true, + ) + } + } + OrderStatus.CANCELED -> Unit + } + } } \ No newline at end of file diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayReissueCardModel.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayReissueCardModel.kt new file mode 100644 index 0000000000..6c454671a1 --- /dev/null +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayReissueCardModel.kt @@ -0,0 +1,119 @@ +package com.tangem.features.tangempay.model + +import androidx.compose.runtime.Stable +import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.core.decompose.di.ModelScoped +import com.tangem.core.decompose.model.Model +import com.tangem.core.decompose.model.ParamsContainer +import com.tangem.core.decompose.ui.UiMessageSender +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.format.bigdecimal.fiat +import com.tangem.core.ui.format.bigdecimal.format +import com.tangem.core.ui.format.bigdecimal.getJavaCurrencyByCode +import com.tangem.core.ui.message.SnackbarMessage +import com.tangem.domain.pay.repository.TangemPayCardDetailsRepository +import com.tangem.domain.pay.repository.TangemPayReissueCardRepository +import com.tangem.domain.tangempay.TangemPayAnalyticsEvents +import com.tangem.features.tangempay.components.TangemPayReissueCardComponent +import com.tangem.features.tangempay.details.impl.R +import com.tangem.features.tangempay.entity.TangemPayReissueCardError +import com.tangem.features.tangempay.entity.TangemPayReissueCardUM +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import com.tangem.utils.coroutines.JobHolder +import com.tangem.utils.coroutines.saveIn +import kotlinx.coroutines.async +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import javax.inject.Inject + +@Stable +@ModelScoped +internal class TangemPayReissueCardModel @Inject constructor( + paramsContainer: ParamsContainer, + override val dispatchers: CoroutineDispatcherProvider, + private val cardDetailsRepository: TangemPayCardDetailsRepository, + private val reissueCardRepository: TangemPayReissueCardRepository, + private val uiMessageSender: UiMessageSender, + private val analytics: AnalyticsEventHandler, +) : Model() { + + private val params = paramsContainer.require() + private val reissueJobHolder = JobHolder() + private val loadDataJobHolder = JobHolder() + + val state: StateFlow + field = MutableStateFlow( + TangemPayReissueCardUM( + feeAmount = "", + isFeeLoading = true, + error = null, + isReissuingInProgress = false, + onConfirmClick = ::onConfirm, + onRetryFee = ::loadData, + onAddFundsClick = { params.listener.onClickAddFunds() }, + onDismissRequest = ::onDismiss, + ), + ) + + init { + analytics.send(TangemPayAnalyticsEvents.ReplaceCardConfirmationPopupOpened()) + loadData() + } + + fun onDismiss() { + reissueJobHolder.cancel() + params.listener.onDismissReissueCard() + } + + private fun onConfirm() { + analytics.send(TangemPayAnalyticsEvents.ReplaceCardConfirmed()) + state.update { it.copy(isReissuingInProgress = true) } + modelScope.launch { + reissueCardRepository.reissueCard( + userWalletId = params.userWalletId, + cardId = params.cardId, + ).onLeft { + uiMessageSender.send(SnackbarMessage(resourceReference(R.string.common_something_went_wrong))) + onDismiss() + }.onRight { order -> + params.listener.onReissueOrderCreate(order) + } + }.saveIn(reissueJobHolder) + } + + private fun loadData() { + state.update { it.copy(isFeeLoading = true, error = null) } + + modelScope.launch { + val (cardBalance, fee) = coroutineScope { + val balanceDeferred = async { cardDetailsRepository.getCardBalance(params.userWalletId).getOrNull() } + val feeDeferred = async { reissueCardRepository.getReissueCardFee(params.userWalletId).getOrNull() } + balanceDeferred.await() to feeDeferred.await() + } + + val error = if (fee == null || cardBalance == null) { + TangemPayReissueCardError.InitialDataLoading + } else if (cardBalance.availableForWithdrawal < fee.amount) { + TangemPayReissueCardError.InsufficientFunds + } else { + null + } + + state.update { state -> + state.copy( + feeAmount = fee?.let { + fee.amount.format { + val symbol = getJavaCurrencyByCode(fee.currencyCode).symbol + fiat(fee.currencyCode, symbol) + } + }.orEmpty(), + isFeeLoading = false, + error = error, + ) + } + }.saveIn(loadDataJobHolder) + } +} \ No newline at end of file diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayCardDetailsBlock.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayCardDetailsBlock.kt index 9cc9ea437d..b2509b7b49 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayCardDetailsBlock.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayCardDetailsBlock.kt @@ -12,12 +12,27 @@ import androidx.compose.foundation.background import androidx.compose.foundation.border import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource -import androidx.compose.foundation.layout.* +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.BasicTextField import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.material3.* +import androidx.compose.material3.ButtonColors +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue @@ -27,8 +42,6 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip -import androidx.constraintlayout.compose.ConstraintLayout -import androidx.constraintlayout.compose.Dimension import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.graphics.Brush @@ -45,6 +58,8 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider import androidx.compose.ui.unit.dp +import androidx.constraintlayout.compose.ConstraintLayout +import androidx.constraintlayout.compose.Dimension import com.tangem.core.ui.components.SpacerWMax import com.tangem.core.ui.components.buttons.common.TangemButton import com.tangem.core.ui.components.buttons.common.TangemButtonIconPosition @@ -53,12 +68,12 @@ import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.domain.models.account.CardDisplayName import com.tangem.domain.visa.model.TangemPayCardFrozenState import com.tangem.features.tangempay.details.impl.R import com.tangem.features.tangempay.entity.DisplayNameState import com.tangem.features.tangempay.entity.TangemPayCardDetailsUM import com.tangem.features.tangempay.model.CardDataType -import com.tangem.domain.models.account.CardDisplayName private const val ICON_FADE_DURATION_MS = 300 private val CustomCardBlockColor = Color(0x1F828282) @@ -101,7 +116,7 @@ internal fun TangemPayCard(state: TangemPayCardDetailsUM, modifier: Modifier = M shape = RoundedCornerShape(16.dp), ), ) { - if (shouldShowDetails) { + if (shouldShowDetails && state.isActive) { TangemPayCardDetailsShownBlock( cardNumber = state.number, expiry = state.expiry, @@ -114,11 +129,7 @@ internal fun TangemPayCard(state: TangemPayCardDetailsUM, modifier: Modifier = M ) } else { TangemPayCardDetailsHiddenBlock( - cardFrozenState = state.cardFrozenState, - isLoading = state.isLoading, - shortCardNumber = state.numberShort, - onShowDetails = state.onClick, - displayNameState = state.displayNameState, + state = state, ) } } @@ -126,16 +137,9 @@ internal fun TangemPayCard(state: TangemPayCardDetailsUM, modifier: Modifier = M @Suppress("LongMethod", "DestructuringDeclarationWithTooManyEntries") @Composable -private fun TangemPayCardDetailsHiddenBlock( - shortCardNumber: String, - cardFrozenState: TangemPayCardFrozenState, - isLoading: Boolean, - onShowDetails: () -> Unit, - displayNameState: DisplayNameState?, - modifier: Modifier = Modifier, -) { +private fun TangemPayCardDetailsHiddenBlock(state: TangemPayCardDetailsUM, modifier: Modifier = Modifier) { Box(modifier = modifier.fillMaxSize()) { - val imageResId = when (cardFrozenState) { + val imageResId = when (state.cardFrozenState) { is TangemPayCardFrozenState.Frozen -> R.drawable.img_tangem_pay_visa_frozen else -> R.drawable.img_tangem_pay_visa } @@ -144,75 +148,78 @@ private fun TangemPayCardDetailsHiddenBlock( painter = painterResource(id = imageResId), contentDescription = null, ) - ConstraintLayout( - modifier = Modifier - .align(Alignment.BottomCenter) - .padding(horizontal = 16.dp) - .padding(bottom = 8.dp) - .fillMaxWidth(), - ) { - val (displayNameRef, cardNumberRef, frozenIconRef, buttonRef) = createRefs() - if (displayNameState != null) { - CardDisplayName( - state = displayNameState, - modifier = Modifier.constrainAs(displayNameRef) { - start.linkTo(parent.start) - bottom.linkTo(cardNumberRef.top) - width = Dimension.wrapContent - }, - ) - } - - Text( - text = shortCardNumber, - style = TangemTheme.typography.subtitle2, - color = TangemTheme.colors.text.constantWhite, + if (state.isActive) { + ConstraintLayout( modifier = Modifier - .constrainAs(cardNumberRef) { - start.linkTo(parent.start) - bottom.linkTo(parent.bottom) - } - .padding(bottom = 8.dp), - ) - when (cardFrozenState) { - is TangemPayCardFrozenState.Frozen -> Icon( - modifier = Modifier - .constrainAs(frozenIconRef) { - start.linkTo(cardNumberRef.end, margin = 4.dp) - top.linkTo(cardNumberRef.top) - bottom.linkTo(cardNumberRef.bottom) - } - .padding(bottom = 8.dp) - .size(16.dp), - painter = painterResource(id = R.drawable.ic_snow_24), - contentDescription = null, - tint = TangemTheme.colors.icon.constant, - ) - TangemPayCardFrozenState.Pending -> CircularProgressIndicator( - modifier = Modifier - .constrainAs(frozenIconRef) { - start.linkTo(cardNumberRef.end, margin = 4.dp) - top.linkTo(cardNumberRef.top) - bottom.linkTo(cardNumberRef.bottom) - } - .padding(bottom = 8.dp) - .size(16.dp), - color = TangemTheme.colors.text.constantWhite, - strokeWidth = 1.dp, - ) - TangemPayCardFrozenState.Unfrozen -> Unit - } + .align(Alignment.BottomCenter) + .padding(horizontal = 16.dp) + .padding(bottom = 8.dp) + .fillMaxWidth(), + ) { + val (displayNameRef, cardNumberRef, frozenIconRef, buttonRef) = createRefs() - TangemPayCardDetailsCustomButton( - modifier = Modifier.constrainAs(buttonRef) { - end.linkTo(parent.end) - bottom.linkTo(parent.bottom) - }, - text = stringResourceSafe(id = R.string.tangempay_card_details_show_details), - onClick = onShowDetails, - showProgress = isLoading, - ) + if (state.displayNameState != null) { + CardDisplayName( + state = state.displayNameState, + modifier = Modifier.constrainAs(displayNameRef) { + start.linkTo(parent.start) + bottom.linkTo(cardNumberRef.top) + width = Dimension.wrapContent + }, + ) + } + + Text( + text = state.numberShort, + style = TangemTheme.typography.subtitle2, + color = TangemTheme.colors.text.constantWhite, + modifier = Modifier + .constrainAs(cardNumberRef) { + start.linkTo(parent.start) + bottom.linkTo(parent.bottom) + } + .padding(bottom = 8.dp), + ) + when (state.cardFrozenState) { + is TangemPayCardFrozenState.Frozen -> Icon( + modifier = Modifier + .constrainAs(frozenIconRef) { + start.linkTo(cardNumberRef.end, margin = 4.dp) + top.linkTo(cardNumberRef.top) + bottom.linkTo(cardNumberRef.bottom) + } + .padding(bottom = 8.dp) + .size(16.dp), + painter = painterResource(id = R.drawable.ic_snow_24), + contentDescription = null, + tint = TangemTheme.colors.icon.constant, + ) + TangemPayCardFrozenState.Pending -> CircularProgressIndicator( + modifier = Modifier + .constrainAs(frozenIconRef) { + start.linkTo(cardNumberRef.end, margin = 4.dp) + top.linkTo(cardNumberRef.top) + bottom.linkTo(cardNumberRef.bottom) + } + .padding(bottom = 8.dp) + .size(16.dp), + color = TangemTheme.colors.text.constantWhite, + strokeWidth = 1.dp, + ) + TangemPayCardFrozenState.Unfrozen -> Unit + } + + TangemPayCardDetailsCustomButton( + modifier = Modifier.constrainAs(buttonRef) { + end.linkTo(parent.end) + bottom.linkTo(parent.bottom) + }, + text = stringResourceSafe(id = R.string.tangempay_card_details_show_details), + onClick = state.onClick, + showProgress = state.isLoading, + ) + } } } } diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayCardPageScreen.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayCardPageScreen.kt index 2dc2540204..673b118af2 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayCardPageScreen.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayCardPageScreen.kt @@ -32,6 +32,10 @@ import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.util.fastForEach import com.tangem.core.ui.components.appbar.AppBarWithBackButton +import com.tangem.core.ui.components.notifications.Notification +import com.tangem.core.ui.components.notifications.NotificationConfig +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview @@ -46,12 +50,6 @@ import com.tangem.features.tangempay.entity.TangemPayCardPageUM import kotlinx.collections.immutable.ImmutableList private const val CONTENT_FADE_DURATION_MS = 300 -private val TangemPayCardPageSetting.titleRes - get() = when (this) { - TangemPayCardPageSetting.ChangePIN -> R.string.tangempay_card_details_change_pin - TangemPayCardPageSetting.FreezeCard -> R.string.tangempay_card_details_freeze_card - TangemPayCardPageSetting.ReplaceCard -> R.string.common_error // TODO v_rodionov #[REDACTED_TASK_KEY] - } @Composable internal fun TangemPayCardPageScreen( @@ -110,20 +108,35 @@ internal fun TangemPayCardPageScreen( enter = fadeIn(animationSpec = tween(CONTENT_FADE_DURATION_MS)), exit = fadeOut(animationSpec = tween(CONTENT_FADE_DURATION_MS)), ) { - TangemPayCardPageSettingsBlock( - settings = state.settings, - onSettingClick = state.onSettingClick, - ) + if (state.isReissueInProgress) { + TangemPayReplacingCardBlock() + } else { + TangemPayCardPageSettingsBlock( + settings = state.settings, + ) + } } } } } } +@Composable +private fun TangemPayReplacingCardBlock(modifier: Modifier = Modifier) { + Notification( + modifier = modifier, + config = NotificationConfig( + iconResId = com.tangem.core.ui.R.drawable.ic_update_32, + iconTint = NotificationConfig.IconTint.Accent, + title = resourceReference(R.string.tangempay_reissue_card_in_progress), + subtitle = resourceReference(R.string.tangempay_reissue_card_in_progress_description), + ), + ) +} + @Composable private fun TangemPayCardPageSettingsBlock( settings: ImmutableList, - onSettingClick: (TangemPayCardPageSetting) -> Unit, modifier: Modifier = Modifier, ) { Column( @@ -147,7 +160,7 @@ private fun TangemPayCardPageSettingsBlock( settings.fastForEach { item -> TangemPayCardPageSettingRow( item = item, - onClick = { onSettingClick(item) }, + onClick = item.onSettingClick, ) } } @@ -167,7 +180,7 @@ private fun TangemPayCardPageSettingRow( contentAlignment = Alignment.CenterStart, ) { Text( - text = stringResourceSafe(item.titleRes), + text = item.title.resolveReference(), style = TangemTheme.typography.subtitle1, color = TangemTheme.colors.text.primary1, ) diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayReissueCardContent.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayReissueCardContent.kt new file mode 100644 index 0000000000..9048249e1d --- /dev/null +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayReissueCardContent.kt @@ -0,0 +1,218 @@ +package com.tangem.features.tangempay.ui + +import android.content.res.Configuration +import androidx.compose.animation.AnimatedContent +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.tangem.core.ui.components.PrimaryButton +import com.tangem.core.ui.components.SpacerH16 +import com.tangem.core.ui.components.SpacerH24 +import com.tangem.core.ui.components.SpacerH8 +import com.tangem.core.ui.components.TextShimmer +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent +import com.tangem.core.ui.components.bottomsheets.modal.TangemModalBottomSheet +import com.tangem.core.ui.components.bottomsheets.modal.TangemModalBottomSheetTitle +import com.tangem.core.ui.components.notifications.Notification +import com.tangem.core.ui.components.notifications.NotificationConfig +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.stringResourceSafe +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.features.tangempay.details.impl.R +import com.tangem.features.tangempay.entity.TangemPayReissueCardError +import com.tangem.features.tangempay.entity.TangemPayReissueCardUM + +@Composable +internal fun TangemPayReissueCardContent(state: TangemPayReissueCardUM) { + TangemModalBottomSheet( + config = TangemBottomSheetConfig( + isShown = true, + onDismissRequest = state.onDismissRequest, + content = TangemBottomSheetConfigContent.Empty, + ), + containerColor = TangemTheme.colors.background.tertiary, + onBack = state.onDismissRequest, + title = { + TangemModalBottomSheetTitle( + title = TextReference.EMPTY, + endIconRes = R.drawable.ic_close_24, + onEndClick = state.onDismissRequest, + ) + }, + ) { + Content(state) + } +} + +@Composable +private fun Content(state: TangemPayReissueCardUM) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = TangemTheme.dimens.spacing16), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Box( + modifier = Modifier + .size(TangemTheme.dimens.spacing56) + .clip(CircleShape) + .background(TangemTheme.colors.icon.accent.copy(alpha = 0.1f)), + contentAlignment = Alignment.Center, + ) { + Icon( + painter = painterResource(id = R.drawable.ic_update_32), + contentDescription = null, + tint = TangemTheme.colors.icon.accent, + modifier = Modifier.size(32.dp), + ) + } + + SpacerH24() + + Text( + text = stringResourceSafe(R.string.tangempay_reissue_card_title), + style = TangemTheme.typography.h3, + color = TangemTheme.colors.text.primary1, + textAlign = TextAlign.Center, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + ) + + SpacerH8() + + Text( + text = stringResourceSafe(R.string.tangempay_reissue_card_description), + style = TangemTheme.typography.body2, + color = TangemTheme.colors.text.secondary, + textAlign = TextAlign.Center, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + ) + + SpacerH24() + + FeeBlock(state) + + if (state.error != null) { + SpacerH16() + ErrorBlock( + error = state.error, + onRetryFee = state.onRetryFee, + onAddFundsClick = state.onAddFundsClick, + ) + } + + SpacerH24() + + PrimaryButton( + modifier = Modifier.fillMaxWidth(), + text = stringResourceSafe(R.string.tangempay_reissue_card_confirm), + enabled = state.error == null && !state.isFeeLoading, + showProgress = state.isReissuingInProgress, + onClick = state.onConfirmClick, + ) + + SpacerH16() + } +} + +@Composable +private fun FeeBlock(state: TangemPayReissueCardUM) { + Row( + modifier = Modifier + .fillMaxWidth() + .background( + color = TangemTheme.colors.background.action, + shape = TangemTheme.shapes.roundedCornersMedium, + ) + .padding(TangemTheme.dimens.spacing12), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = stringResourceSafe(R.string.tangempay_reissue_card_fee_label), + style = TangemTheme.typography.body1, + color = TangemTheme.colors.text.primary1, + ) + AnimatedContent( + targetState = when { + state.isFeeLoading -> null + state.error == TangemPayReissueCardError.InitialDataLoading -> "—" + else -> state.feeAmount + }, + ) { fee -> + if (fee != null) { + Text( + text = fee, + style = TangemTheme.typography.body1, + color = TangemTheme.colors.text.tertiary, + ) + } else { + TextShimmer(style = TangemTheme.typography.body1, text = "$0.00") + } + } + } +} + +@Composable +private fun ErrorBlock(error: TangemPayReissueCardError, onAddFundsClick: () -> Unit, onRetryFee: () -> Unit) { + when (error) { + TangemPayReissueCardError.InsufficientFunds -> Notification( + config = NotificationConfig( + title = resourceReference(R.string.tangempay_reissue_card_insufficient_funds_title), + subtitle = resourceReference(R.string.tangempay_reissue_card_insufficient_funds_subtitle), + iconResId = R.drawable.img_usdc_16, + buttonsState = NotificationConfig.ButtonsState.SecondaryButtonConfig( + text = resourceReference(R.string.tangempay_card_details_add_funds), + iconResId = R.drawable.ic_plus_24, + onClick = onAddFundsClick, + ), + ), + containerColor = TangemTheme.colors.background.action, + modifier = Modifier.fillMaxWidth(), + ) + TangemPayReissueCardError.InitialDataLoading -> Notification( + config = NotificationConfig( + title = resourceReference(R.string.tangempay_reissue_card_fee_unreachable_error_title), + subtitle = resourceReference(R.string.send_fee_unreachable_error_text), + iconResId = R.drawable.img_attention_20, + buttonsState = NotificationConfig.ButtonsState.SecondaryButtonConfig( + text = resourceReference(R.string.warning_button_refresh), + onClick = onRetryFee, + ), + ), + containerColor = TangemTheme.colors.background.action, + modifier = Modifier.fillMaxWidth(), + ) + } +} + +@Preview(showBackground = true) +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun preview() = TangemThemePreview { + TangemPayReissueCardContent( + state = TangemPayReissueCardUM.stub(), + ) +} \ No newline at end of file