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 bcbcd299b2..56359bc593 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 @@ -109,9 +109,10 @@ interface TangemPayApi { @Body body: WithdrawRequest, ): ApiResponse - @PATCH("v1/card") - suspend fun updateCardDisplayName( + @PATCH("v1/customer/card/{card_id}") + suspend fun updateCard( @Header("Authorization") authHeader: String, - @Body body: UpdateCardDisplayNameRequest, + @Body body: UpdateCardRequest, + @Path("card_id") cardId: String, ): ApiResponse } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/request/UpdateCardDisplayNameRequest.kt b/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/request/UpdateCardDisplayNameRequest.kt deleted file mode 100644 index b5c9e74e58..0000000000 --- a/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/request/UpdateCardDisplayNameRequest.kt +++ /dev/null @@ -1,9 +0,0 @@ -package com.tangem.datasource.api.pay.models.request - -import com.squareup.moshi.Json -import com.squareup.moshi.JsonClass - -@JsonClass(generateAdapter = true) -data class UpdateCardDisplayNameRequest( - @Json(name = "display_name") val displayName: String, -) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/request/UpdateCardRequest.kt b/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/request/UpdateCardRequest.kt new file mode 100644 index 0000000000..3dc16b32d0 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/request/UpdateCardRequest.kt @@ -0,0 +1,15 @@ +package com.tangem.datasource.api.pay.models.request + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class UpdateCardRequest( + @Json(name = "display_name") val displayName: String? = null, + @Json(name = "card_limit") val cardLimit: CardLimit? = null, +) { + @JsonClass(generateAdapter = true) + data class CardLimit( + @Json(name = "amount") val amount: 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 5bfd090643..b3f833f9fd 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -1676,6 +1676,8 @@ Daily limit is set Daily limit Card settings + Set a limit from %1$s + We couldn’t set the limit. Please try again. Change PIN-code Come back to the app if you forget it. Set a limit from %s to %s 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 2c3e46bd19..97394190aa 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 @@ -29,6 +29,7 @@ import com.tangem.domain.pay.flow.PaymentAccountStatusSupplier import com.tangem.domain.pay.repository.* import com.tangem.domain.pay.usecase.GetPaymentAccountCryptoCurrencyStatusUseCase import com.tangem.domain.pay.usecase.ProduceTangemPayInitialDataUseCase +import com.tangem.domain.pay.usecase.SetTangemPayCardLimitUseCase import com.tangem.domain.pay.usecase.TangemPayMainScreenCustomerInfoUseCase import com.tangem.domain.tangempay.GetTangemPayCurrencyStatusUseCase import com.tangem.domain.tangempay.GetTangemPayCustomerIdUseCase @@ -150,6 +151,14 @@ internal interface TangemPayDataModule { ) {} } + @Provides + fun provideSetTangemPayCardLimitUseCase( + cardDetailsRepository: TangemPayCardDetailsRepository, + paymentAccountStatusFetcher: PaymentAccountStatusFetcher, + ): SetTangemPayCardLimitUseCase { + return SetTangemPayCardLimitUseCase(cardDetailsRepository, paymentAccountStatusFetcher) + } + @Provides @Singleton fun provideGetTangemPayCryptoCurrencyStatusUseCase( diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultTangemPayCardDetailsRepository.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultTangemPayCardDetailsRepository.kt index 814476fa34..97657f4c0d 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultTangemPayCardDetailsRepository.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultTangemPayCardDetailsRepository.kt @@ -14,8 +14,8 @@ 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.UpdateCardRequest import com.tangem.datasource.api.pay.models.request.SetPinRequest -import com.tangem.datasource.api.pay.models.request.UpdateCardDisplayNameRequest import com.tangem.datasource.api.pay.models.response.FreezeUnfreezeCardResponse import com.tangem.datasource.api.pay.models.response.OrderResponse.Result.Status import com.tangem.datasource.local.visa.TangemPayCardFrozenStateStore @@ -41,7 +41,7 @@ import kotlin.time.Duration.Companion.seconds private const val TAG = "TangemPay: CardDetailsRepository" -@Suppress("LongParameterList") +@Suppress("LongParameterList", "LargeClass") internal class DefaultTangemPayCardDetailsRepository @Inject constructor( private val tangemPayApi: TangemPayApi, private val requestHelper: TangemPayRequestPerformer, @@ -321,15 +321,43 @@ internal class DefaultTangemPayCardDetailsRepository @Inject constructor( } override suspend fun updateCardDisplayName( + cardId: String, userWalletId: UserWalletId, displayName: CardDisplayName, ): Either { return catch( block = { requestHelper.performRequest(userWalletId) { authHeader -> - tangemPayApi.updateCardDisplayName( + tangemPayApi.updateCard( authHeader = authHeader, - body = UpdateCardDisplayNameRequest(displayName = displayName.value), + body = UpdateCardRequest( + displayName = displayName.value, + ), + cardId = cardId, + ) + }.fold( + ifLeft = { error -> error.left() }, + ifRight = { Unit.right() }, + ) + }, + catch = ::catchException, + ) + } + + override suspend fun updateCardLimit( + cardId: String, + userWalletId: UserWalletId, + limit: String, + ): Either { + return catch( + block = { + requestHelper.performRequest(userWalletId) { authHeader -> + tangemPayApi.updateCard( + authHeader = authHeader, + body = UpdateCardRequest( + cardLimit = UpdateCardRequest.CardLimit(limit), + ), + cardId = cardId, ) }.fold( ifLeft = { error -> error.left() }, diff --git a/domain/models/src/main/kotlin/com/tangem/domain/models/account/PaymentAccountStatusValue.kt b/domain/models/src/main/kotlin/com/tangem/domain/models/account/PaymentAccountStatusValue.kt index 6187861ae1..15029d713b 100644 --- a/domain/models/src/main/kotlin/com/tangem/domain/models/account/PaymentAccountStatusValue.kt +++ b/domain/models/src/main/kotlin/com/tangem/domain/models/account/PaymentAccountStatusValue.kt @@ -2,6 +2,7 @@ package com.tangem.domain.models.account import com.tangem.domain.models.StatusSource import com.tangem.domain.models.TotalFiatBalance +import com.tangem.domain.models.account.PaymentAccountStatusValue.Loaded import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.kyc.KycStatus @@ -185,4 +186,10 @@ sealed class PaymentAccountStatusValue { val tokenContractAddress: String, val balance: SerializedBigDecimal, ) -} \ No newline at end of file +} + +fun Loaded.hasCardWithId(cardId: String): Boolean = cards.any { it.id == cardId } + +fun Loaded.findCardWithId(cardId: String): TangemPayCard? = cards.firstOrNull { it.id == cardId } + +fun Loaded.requireCardWithId(cardId: String): TangemPayCard = requireNotNull(findCardWithId(cardId)) \ No newline at end of file diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/pay/repository/TangemPayCardDetailsRepository.kt b/domain/visa/src/main/kotlin/com/tangem/domain/pay/repository/TangemPayCardDetailsRepository.kt index 80aa3ea5c9..6778223272 100644 --- a/domain/visa/src/main/kotlin/com/tangem/domain/pay/repository/TangemPayCardDetailsRepository.kt +++ b/domain/visa/src/main/kotlin/com/tangem/domain/pay/repository/TangemPayCardDetailsRepository.kt @@ -34,7 +34,14 @@ interface TangemPayCardDetailsRepository { suspend fun cardFrozenStateSync(cardId: String): TangemPayCardFrozenState? suspend fun updateCardDisplayName( + cardId: String, userWalletId: UserWalletId, displayName: CardDisplayName, ): Either + + suspend fun updateCardLimit( + cardId: String, + userWalletId: UserWalletId, + limit: String, + ): Either } \ No newline at end of file diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/pay/usecase/SetTangemPayCardLimitUseCase.kt b/domain/visa/src/main/kotlin/com/tangem/domain/pay/usecase/SetTangemPayCardLimitUseCase.kt new file mode 100644 index 0000000000..9c66f8d75e --- /dev/null +++ b/domain/visa/src/main/kotlin/com/tangem/domain/pay/usecase/SetTangemPayCardLimitUseCase.kt @@ -0,0 +1,25 @@ +package com.tangem.domain.pay.usecase + +import arrow.core.Either +import com.tangem.core.error.UniversalError +import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.pay.flow.PaymentAccountStatusFetcher +import com.tangem.domain.pay.repository.TangemPayCardDetailsRepository +import java.math.BigDecimal + +class SetTangemPayCardLimitUseCase( + private val cardDetailsRepository: TangemPayCardDetailsRepository, + private val paymentAccountStatusFetcher: PaymentAccountStatusFetcher, +) { + suspend operator fun invoke( + cardId: String, + userWalletId: UserWalletId, + amount: BigDecimal, + ): Either { + return cardDetailsRepository.updateCardLimit(cardId, userWalletId, amount.toPlainString()) + .onRight { + val params = PaymentAccountStatusFetcher.Params(userWalletId) + paymentAccountStatusFetcher.invoke(params) + } + } +} \ No newline at end of file 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 de2c4bf4a2..7271f7fd3a 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 @@ -13,6 +13,8 @@ import com.tangem.core.decompose.context.AppComponentContext 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.limit.setup.TangemPayCardLimitSetupComponent +import com.tangem.features.tangempay.limit.setup.TangemPayCardLimitSetupSuccessComponent import com.tangem.features.tangempay.navigation.TangemPayDetailsInnerRoute import com.tangem.features.tokenreceive.TokenReceiveComponent import dagger.assisted.Assisted @@ -84,6 +86,16 @@ internal class DefaultTangemPayCardPageComponent @AssistedInject constructor( config = params.config, ), ) + TangemPayDetailsInnerRoute.LimitSetup -> TangemPayCardLimitSetupComponent( + appComponentContext = childByContext(componentContext = componentContext, router = innerRouter), + params = TangemPayDetailsContainerComponent.Params( + userWalletId = params.userWalletId, + config = params.config, + ), + ) + TangemPayDetailsInnerRoute.LimitSetupSuccess -> TangemPayCardLimitSetupSuccessComponent( + appComponentContext = childByContext(componentContext = componentContext, router = innerRouter), + ) } private fun onChildBack() { diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/components/DefaultTangemPayDetailsContainerComponent.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/components/DefaultTangemPayDetailsContainerComponent.kt index 35ea8d9089..272c8d3b39 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/components/DefaultTangemPayDetailsContainerComponent.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/components/DefaultTangemPayDetailsContainerComponent.kt @@ -16,6 +16,8 @@ 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.components.express.ExpressTransactionsComponentProvider +import com.tangem.features.tangempay.limit.setup.TangemPayCardLimitSetupComponent +import com.tangem.features.tangempay.limit.setup.TangemPayCardLimitSetupSuccessComponent import com.tangem.features.tangempay.navigation.TangemPayDetailsInnerRoute import com.tangem.features.tokenreceive.TokenReceiveComponent import dagger.assisted.Assisted @@ -82,6 +84,13 @@ internal class DefaultTangemPayDetailsContainerComponent @AssistedInject constru config = params.config, ), ) + TangemPayDetailsInnerRoute.LimitSetup -> TangemPayCardLimitSetupComponent( + appComponentContext = childByContext(componentContext = componentContext, router = innerRouter), + params = params, + ) + TangemPayDetailsInnerRoute.LimitSetupSuccess -> TangemPayCardLimitSetupSuccessComponent( + appComponentContext = childByContext(componentContext = componentContext, router = innerRouter), + ) } private fun onChildBack() { 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 d9b0f67529..1766696a25 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 @@ -6,6 +6,7 @@ import com.tangem.features.tangempay.model.TangemPayAddFundsModel import com.tangem.features.tangempay.model.TangemPayAddToWalletModel import com.tangem.features.tangempay.model.TangemPayCardDetailsBlockModel import com.tangem.features.tangempay.model.TangemPayCardPageModel +import com.tangem.features.tangempay.limit.setup.TangemPayCardLimitSetupModel import com.tangem.features.tangempay.model.TangemPayChangePinModel import com.tangem.features.tangempay.model.TangemPayDetailsModel import com.tangem.features.tangempay.model.TangemPayEditDisplayNameModel @@ -77,4 +78,9 @@ internal interface TangemPayModelModule { @IntoMap @ClassKey(TangemPayReissueCardModel::class) fun bindTangemPayReissueCardModel(model: TangemPayReissueCardModel): Model + + @Binds + @IntoMap + @ClassKey(TangemPayCardLimitSetupModel::class) + fun bindTangemPayCardLimitSetupModel(model: TangemPayCardLimitSetupModel): Model } \ No newline at end of file diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/limit/setup/TangemPayCardLimitSetupComponent.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/limit/setup/TangemPayCardLimitSetupComponent.kt new file mode 100644 index 0000000000..2916fd93ed --- /dev/null +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/limit/setup/TangemPayCardLimitSetupComponent.kt @@ -0,0 +1,29 @@ +package com.tangem.features.tangempay.limit.setup + +import androidx.activity.compose.BackHandler +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +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.ComposableContentComponent +import com.tangem.features.tangempay.components.TangemPayDetailsContainerComponent + +internal class TangemPayCardLimitSetupComponent( + appComponentContext: AppComponentContext, + params: TangemPayDetailsContainerComponent.Params, +) : AppComponentContext by appComponentContext, ComposableContentComponent { + + private val model: TangemPayCardLimitSetupModel = getOrCreateModel(params) + + @Composable + override fun Content(modifier: Modifier) { + val state by model.uiState.collectAsStateWithLifecycle() + BackHandler(onBack = router::pop) + TangemPayCardLimitSetupScreen( + state = state, + modifier = modifier, + ) + } +} \ No newline at end of file diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/limit/setup/TangemPayCardLimitSetupModel.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/limit/setup/TangemPayCardLimitSetupModel.kt new file mode 100644 index 0000000000..509ea0514e --- /dev/null +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/limit/setup/TangemPayCardLimitSetupModel.kt @@ -0,0 +1,195 @@ +package com.tangem.features.tangempay.limit.setup + +import androidx.compose.runtime.Stable +import com.tangem.core.decompose.di.ModelScoped +import com.tangem.core.decompose.model.Model +import com.tangem.core.decompose.model.ParamsContainer +import com.tangem.core.decompose.navigation.Router +import com.tangem.core.decompose.ui.UiMessageSender +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.WrappedList +import com.tangem.core.ui.format.bigdecimal.anyDecimals +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.DialogMessage +import com.tangem.domain.models.StatusSource +import com.tangem.domain.models.account.PaymentAccountStatusValue +import com.tangem.domain.models.account.findCardWithId +import com.tangem.domain.models.account.requireCardWithId +import com.tangem.domain.models.pay.TangemPayCardLimitPeriod +import com.tangem.domain.pay.flow.PaymentAccountStatusSupplier +import com.tangem.domain.pay.usecase.SetTangemPayCardLimitUseCase +import com.tangem.features.tangempay.components.TangemPayDetailsContainerComponent +import com.tangem.features.tangempay.details.impl.R +import com.tangem.features.tangempay.navigation.TangemPayDetailsInnerRoute +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toPersistentList +import kotlinx.coroutines.flow.* +import kotlinx.coroutines.launch +import java.math.BigDecimal +import java.util.Currency +import javax.inject.Inject + +@Stable +@ModelScoped +internal class TangemPayCardLimitSetupModel @Inject constructor( + paramsContainer: ParamsContainer, + override val dispatchers: CoroutineDispatcherProvider, + private val router: Router, + private val paymentAccountStatusSupplier: PaymentAccountStatusSupplier, + private val setTangemPayCardLimitUseCase: SetTangemPayCardLimitUseCase, + private val uiMessageSender: UiMessageSender, +) : Model() { + + private val params: TangemPayDetailsContainerComponent.Params = paramsContainer.require() + + val uiState: StateFlow + field = MutableStateFlow( + TangemPayCardLimitSetupUM( + isInitialDataLoading = true, + amountFieldModel = TangemPayCardLimitSetupUM.AmountFieldModel( + value = "", + decimals = 0, + onValueChange = {}, + ), + subtitle = TextReference.EMPTY, + currencyCode = "", + presets = persistentListOf(), + isSubmitButtonEnabled = false, + isSubmitButtonLoading = false, + onSubmitClick = ::onSubmitClick, + onBackClick = router::pop, + ), + ) + + init { + observeCardState() + } + + private fun observeCardState() { + paymentAccountStatusSupplier.invoke(params.userWalletId) + .map { it.value } + .filterIsInstance() + .filter { status -> + status.source == StatusSource.ACTUAL && status.findCardWithId(params.config.cardId) != null + } + .withIndex() + .onEach { (index, status) -> + val card = status.requireCardWithId(params.config.cardId) + + val currentLimit = card.limit?.actualCardLimit + ?.takeIf { it.period == TangemPayCardLimitPeriod.DAY } + ?.amount + + val adminLimit = card.limit?.adminCardLimit + ?.takeIf { it.period == TangemPayCardLimitPeriod.DAY } + ?.amount + + val currency = getJavaCurrencyByCode(status.currencyCode) + uiState.update { state -> + val amount = if (index == 0) { + currentLimit?.stripTrailingZeros()?.toPlainString().orEmpty() + } else { + state.amountFieldModel.value + } + state.copy( + isInitialDataLoading = false, + amountFieldModel = TangemPayCardLimitSetupUM.AmountFieldModel( + value = amount, + decimals = currency.defaultFractionDigits, + onValueChange = ::onAmountChange, + ), + subtitle = buildSubtitle(adminLimit, currency), + currencyCode = currency.symbol, + presets = buildPresets(currency), + isSubmitButtonEnabled = isValid(amount), + ) + } + } + .launchIn(modelScope) + } + + private fun onAmountChange(newValue: String) { + uiState.update { state -> + state.copy( + amountFieldModel = state.amountFieldModel.copy(value = newValue), + isSubmitButtonEnabled = isValid(newValue), + ) + } + } + + private fun onPresetClick(preset: BigDecimal) { + val amount = uiState.value.amountFieldModel.value.toBigDecimalOrNull() ?: return + val newAmount = if (preset == BigDecimal.ZERO) BigDecimal.ZERO else amount + preset + onAmountChange(newAmount.stripTrailingZeros().toPlainString()) + } + + private fun onSubmitClick() { + val amount = uiState.value.amountFieldModel.value.toBigDecimalOrNull() ?: return + modelScope.launch { + uiState.update { it.copy(isSubmitButtonLoading = true) } + setTangemPayCardLimitUseCase( + cardId = params.config.cardId, + userWalletId = params.userWalletId, + amount = amount, + ).fold( + ifLeft = { + uiState.update { state -> state.copy(isSubmitButtonLoading = false) } + uiMessageSender.send( + DialogMessage( + title = TextReference.Res(R.string.common_something_went_wrong), + message = TextReference.Res(R.string.tangempay_card_limit_setup_error_message), + ), + ) + }, + ifRight = { + uiState.update { state -> state.copy(isSubmitButtonLoading = false) } + router.push(TangemPayDetailsInnerRoute.LimitSetupSuccess) + }, + ) + } + } + + private fun isValid(value: String): Boolean { + val amount = value.toBigDecimalOrNull() ?: return false + return amount >= BigDecimal.ZERO + } + + private fun buildSubtitle(maxLimit: BigDecimal?, currency: Currency): TextReference { + return if (maxLimit == null) { + TextReference.Res( + id = R.string.tangempay_card_limit_setup_amount_subtitle, + formatArgs = WrappedList( + listOf( + BigDecimal.ZERO.format { fiat(currency.currencyCode, currency.symbol).anyDecimals(0) }, + ), + ), + ) + } else { + TextReference.Res( + id = R.string.tangempay_daily_limit_hint, + formatArgs = WrappedList( + listOf( + BigDecimal.ZERO.format { fiat(currency.currencyCode, currency.symbol).anyDecimals(0) }, + maxLimit.format { fiat(currency.currencyCode, currency.symbol).anyDecimals(0) }, + ), + ), + ) + } + } + + private fun buildPresets(currency: Currency) = listOf( + BigDecimal.ZERO, + BigDecimal("5000"), + BigDecimal("10000"), + BigDecimal("25000"), + ).map { preset -> + val label = preset.format { fiat(currency.currencyCode, currency.symbol).anyDecimals(0) } + TangemPayCardLimitSetupUM.LimitPresetUM( + label = if (preset == BigDecimal.ZERO) "0" else "+$label", + onClick = { onPresetClick(preset) }, + ) + }.toPersistentList() +} \ No newline at end of file diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/limit/setup/TangemPayCardLimitSetupScreen.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/limit/setup/TangemPayCardLimitSetupScreen.kt new file mode 100644 index 0000000000..3ac9114bd2 --- /dev/null +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/limit/setup/TangemPayCardLimitSetupScreen.kt @@ -0,0 +1,182 @@ +package com.tangem.features.tangempay.limit.setup + +import android.content.res.Configuration +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material3.Scaffold +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.text.input.KeyboardType +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.SpacerH12 +import com.tangem.core.ui.components.TextShimmer +import com.tangem.core.ui.components.appbar.TangemTopAppBar +import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM +import com.tangem.core.ui.components.fields.AmountTextField +import com.tangem.core.ui.components.fields.visualtransformations.AmountVisualTransformation +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.extensions.stringResourceSafe +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.core.ui.utils.rememberDecimalFormat +import com.tangem.features.tangempay.details.impl.R +import kotlinx.collections.immutable.ImmutableList + +@Composable +internal fun TangemPayCardLimitSetupScreen(state: TangemPayCardLimitSetupUM, modifier: Modifier = Modifier) { + Scaffold( + modifier = modifier, + topBar = { + TangemTopAppBar( + modifier = Modifier.statusBarsPadding(), + title = stringResourceSafe(R.string.tangempay_card_page_daily_limit_title), + startButton = TopAppBarButtonUM.Close(onCloseClick = state.onBackClick), + ) + }, + containerColor = TangemTheme.colors.background.secondary, + ) { scaffoldPaddings -> + Content( + state = state, + modifier = Modifier.padding(scaffoldPaddings), + ) + } +} + +@Composable +private fun Content(state: TangemPayCardLimitSetupUM, modifier: Modifier = Modifier) { + Column( + modifier = modifier + .fillMaxSize() + .imePadding(), + verticalArrangement = Arrangement.spacedBy(12.dp), + ) { + AmountBlock( + modifier = Modifier.padding(horizontal = 16.dp), + state = state, + ) + Spacer(modifier = Modifier.weight(1f)) + PrimaryButton( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + text = stringResourceSafe(R.string.tangempay_daily_limit_set_button), + enabled = state.isSubmitButtonEnabled, + showProgress = state.isSubmitButtonLoading, + onClick = state.onSubmitClick, + ) + PresetsRow(presets = state.presets) + } +} + +@Composable +private fun AmountBlock(state: TangemPayCardLimitSetupUM, modifier: Modifier = Modifier) { + Column( + modifier = modifier + .fillMaxWidth() + .clip(RoundedCornerShape(14.dp)) + .background(TangemTheme.colors.background.action) + .padding(vertical = 48.dp, horizontal = 16.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Text( + text = stringResourceSafe(R.string.common_amount), + style = TangemTheme.typography.subtitle2, + color = TangemTheme.colors.text.tertiary, + ) + SpacerH12() + if (state.isInitialDataLoading) { + TextShimmer( + style = TangemTheme.typography.head, + text = "$5000", + ) + } else { + AmountTextField( + value = state.amountFieldModel.value, + decimals = state.amountFieldModel.decimals, + onValueChange = state.amountFieldModel.onValueChange, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), + visualTransformation = AmountVisualTransformation( + decimals = state.amountFieldModel.decimals, + symbol = state.currencyCode, + currencyCode = state.currencyCode, + decimalFormat = rememberDecimalFormat(), + symbolColor = if (state.amountFieldModel.value.isBlank()) { + TangemTheme.colors.text.disabled + } else { + TangemTheme.colors.text.primary1 + }, + ), + textStyle = TangemTheme.typography.head.copy( + textAlign = TextAlign.Center, + ), + isAutoResize = true, + ) + } + Spacer(modifier = Modifier.padding(top = 8.dp)) + Text( + text = state.subtitle.resolveReference(), + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.secondary, + ) + } +} + +@Composable +private fun PresetsRow(presets: ImmutableList) { + if (presets.isEmpty()) return + Row( + modifier = Modifier + .fillMaxWidth() + .background(color = TangemTheme.colors.button.secondary) + .padding(horizontal = 8.dp, vertical = 12.dp), + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + presets.forEach { preset -> + PresetChip( + preset = preset, + modifier = Modifier.weight(1f), + ) + } + } +} + +@Composable +private fun PresetChip(preset: TangemPayCardLimitSetupUM.LimitPresetUM, modifier: Modifier = Modifier) { + Row( + modifier = modifier + .clip(RoundedCornerShape(16.dp)) + .background(TangemTheme.colors.background.primary) + .clickable(onClick = preset.onClick) + .padding(horizontal = 12.dp, vertical = 4.dp) + .wrapContentHeight(), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + modifier = Modifier.fillMaxWidth(), + text = preset.label, + style = TangemTheme.typography.caption1, + color = TangemTheme.colors.text.primary1, + textAlign = TextAlign.Center, + maxLines = 1, + ) + } +} + +@Preview +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun preview() = TangemThemePreview { + TangemPayCardLimitSetupScreen( + state = TangemPayCardLimitSetupUM.stub(), + ) +} \ No newline at end of file diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/limit/setup/TangemPayCardLimitSetupSuccessComponent.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/limit/setup/TangemPayCardLimitSetupSuccessComponent.kt new file mode 100644 index 0000000000..fd424be559 --- /dev/null +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/limit/setup/TangemPayCardLimitSetupSuccessComponent.kt @@ -0,0 +1,26 @@ +package com.tangem.features.tangempay.limit.setup + +import androidx.activity.compose.BackHandler +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.features.tangempay.navigation.TangemPayDetailsInnerRoute + +internal class TangemPayCardLimitSetupSuccessComponent( + appComponentContext: AppComponentContext, +) : AppComponentContext by appComponentContext, ComposableContentComponent { + + @Composable + override fun Content(modifier: Modifier) { + BackHandler(onBack = ::backToDetails) + TangemPayCardLimitSetupSuccessScreen( + modifier = modifier, + onDoneClick = ::backToDetails, + ) + } + + private fun backToDetails() { + router.popTo(route = TangemPayDetailsInnerRoute.Details) + } +} \ No newline at end of file diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/limit/setup/TangemPayCardLimitSetupSuccessScreen.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/limit/setup/TangemPayCardLimitSetupSuccessScreen.kt new file mode 100644 index 0000000000..e615ad70f8 --- /dev/null +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/limit/setup/TangemPayCardLimitSetupSuccessScreen.kt @@ -0,0 +1,102 @@ +package com.tangem.features.tangempay.limit.setup + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.navigationBarsPadding +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.statusBarsPadding +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.graphics.Color +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.vectorResource +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.SpacerH12 +import com.tangem.core.ui.components.SpacerH32 +import com.tangem.core.ui.components.appbar.TangemTopAppBar +import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM +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 + +@Composable +internal fun TangemPayCardLimitSetupSuccessScreen(onDoneClick: () -> Unit, modifier: Modifier = Modifier) { + Column( + modifier = modifier + .fillMaxSize() + .navigationBarsPadding(), + ) { + TangemTopAppBar( + modifier = Modifier.statusBarsPadding(), + startButton = TopAppBarButtonUM.Close(onCloseClick = onDoneClick), + ) + Column( + modifier = Modifier.fillMaxSize(), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + SuccessContent( + modifier = Modifier + .fillMaxWidth() + .weight(1f), + ) + PrimaryButton( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp) + .padding(bottom = 16.dp), + text = stringResourceSafe(R.string.common_done), + onClick = onDoneClick, + ) + } + } +} + +@Composable +private fun SuccessContent(modifier: Modifier = Modifier) { + Column( + modifier = modifier, + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Icon( + imageVector = ImageVector.vectorResource(R.drawable.ic_success_blue_76), + tint = Color.Unspecified, + contentDescription = null, + modifier = Modifier.size(76.dp), + ) + SpacerH32() + Text( + modifier = Modifier.padding(horizontal = 32.dp), + text = stringResourceSafe(R.string.tangempay_card_page_daily_limit_success_title), + style = TangemTheme.typography.h2, + color = TangemTheme.colors.text.primary1, + textAlign = TextAlign.Center, + ) + SpacerH12() + Text( + modifier = Modifier.padding(horizontal = 32.dp), + text = stringResourceSafe(R.string.tangempay_card_page_daily_limit_success_description), + style = TangemTheme.typography.body1, + color = TangemTheme.colors.text.secondary, + textAlign = TextAlign.Center, + ) + } +} + +@Preview(showBackground = true) +@Composable +private fun Preview() { + TangemThemePreview { + TangemPayCardLimitSetupSuccessScreen(onDoneClick = {}) + } +} \ No newline at end of file diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/limit/setup/TangemPayCardLimitSetupUM.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/limit/setup/TangemPayCardLimitSetupUM.kt new file mode 100644 index 0000000000..e77e83b186 --- /dev/null +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/limit/setup/TangemPayCardLimitSetupUM.kt @@ -0,0 +1,64 @@ +package com.tangem.features.tangempay.limit.setup + +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 TangemPayCardLimitSetupUM( + val isInitialDataLoading: Boolean, + val amountFieldModel: AmountFieldModel, + val subtitle: TextReference, + val currencyCode: String, + val presets: ImmutableList, + val isSubmitButtonEnabled: Boolean, + val isSubmitButtonLoading: Boolean, + val onSubmitClick: () -> Unit, + val onBackClick: () -> Unit, +) { + + @Immutable + internal data class AmountFieldModel( + val value: String, + val decimals: Int, + val onValueChange: (String) -> Unit, + ) + + @Immutable + internal data class LimitPresetUM( + val label: String, + val onClick: () -> Unit, + ) + + companion object { + fun stub( + isLoading: Boolean = false, + amountFieldModel: AmountFieldModel = AmountFieldModel( + value = "5000", + decimals = 2, + onValueChange = {}, + ), + subtitle: TextReference = TextReference.Str("Set a limit from $0 to $50,000"), + currencyCode: String = "$", + presets: ImmutableList = persistentListOf( + LimitPresetUM(label = "$0", onClick = {}), + LimitPresetUM(label = "$5,000", onClick = {}), + LimitPresetUM(label = "$10,000", onClick = {}), + LimitPresetUM(label = "$25,000", onClick = {}), + ), + submitButtonEnabled: Boolean = true, + submitButtonLoading: Boolean = false, + ): TangemPayCardLimitSetupUM = TangemPayCardLimitSetupUM( + isInitialDataLoading = isLoading, + amountFieldModel = amountFieldModel, + subtitle = subtitle, + currencyCode = currencyCode, + presets = presets, + isSubmitButtonEnabled = submitButtonEnabled, + isSubmitButtonLoading = submitButtonLoading, + onSubmitClick = {}, + onBackClick = {}, + ) + } +} \ 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 1e357c518b..2789a75e07 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 @@ -21,6 +21,8 @@ import com.tangem.core.ui.message.SnackbarMessage import com.tangem.domain.models.StatusSource import com.tangem.domain.models.TokenReceiveConfig import com.tangem.domain.models.account.PaymentAccountStatusValue +import com.tangem.domain.models.account.hasCardWithId +import com.tangem.domain.models.account.requireCardWithId import com.tangem.domain.models.pay.TangemPayCard import com.tangem.domain.models.pay.TangemPayCardLimitPeriod import com.tangem.domain.pay.flow.PaymentAccountStatusSupplier @@ -87,9 +89,9 @@ internal class TangemPayCardPageModel @Inject constructor( val status = state.value if (status is PaymentAccountStatusValue.Loaded && status.source == StatusSource.ACTUAL && - status.cards.isNotEmpty() + status.hasCardWithId(params.config.cardId) ) { - val card = status.cards.first() + val card = status.requireCardWithId(params.config.cardId) val limit = card.limit?.actualCardLimit?.takeIf { it.period == TangemPayCardLimitPeriod.DAY } val dailyLimitState = if (limit != null) { TangemPayDailyLimitBlockState.Content( @@ -97,14 +99,14 @@ internal class TangemPayCardPageModel @Inject constructor( val symbol = getJavaCurrencyByCode(status.currencyCode).symbol fiat(status.currencyCode, symbol) }, - onChangeClick = {}, // TODO v_rodionov: #[REDACTED_TASK_KEY] + onChangeClick = { router.push(TangemPayDetailsInnerRoute.LimitSetup) }, ) } else { TangemPayDailyLimitBlockState.Error } uiState.update { it.copy(dailyLimitState = dailyLimitState, settings = buildSettings(card)) } } else { - // TODO v_rodionov: #[REDACTED_TASK_KEY] show error state + uiState.update { it.copy(dailyLimitState = TangemPayDailyLimitBlockState.Error) } } } .launchIn(modelScope) diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayEditDisplayNameModel.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayEditDisplayNameModel.kt index a559c7a3d9..f0ed2d6e98 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayEditDisplayNameModel.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayEditDisplayNameModel.kt @@ -61,15 +61,19 @@ internal class TangemPayEditDisplayNameModel @Inject constructor( .onRight { cardDisplayName -> uiState.update { it.copy(isLoading = true) } modelScope.launch { - cardDetailsRepository.updateCardDisplayName(params.userWalletId, cardDisplayName) - .onRight { router.pop() } - .onLeft { - uiState.update { state -> state.copy(isLoading = false) } - showError( - titleRes = R.string.tangem_pay_card_details_unable_to_rename_card_title, - messageRes = R.string.tangempay_card_details_unable_to_rename_card_description, - ) - } + cardDetailsRepository.updateCardDisplayName( + cardId = params.config.cardId, + userWalletId = params.userWalletId, + displayName = cardDisplayName, + ).onRight { + router.pop() + }.onLeft { + uiState.update { state -> state.copy(isLoading = false) } + showError( + titleRes = R.string.tangem_pay_card_details_unable_to_rename_card_title, + messageRes = R.string.tangempay_card_details_unable_to_rename_card_description, + ) + } } } .onLeft { diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/navigation/TangemPayDetailsInnerRoute.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/navigation/TangemPayDetailsInnerRoute.kt index 168ea88da5..89facef290 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/navigation/TangemPayDetailsInnerRoute.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/navigation/TangemPayDetailsInnerRoute.kt @@ -19,4 +19,10 @@ internal sealed class TangemPayDetailsInnerRoute : Route { @Serializable data object EditCardDisplayName : TangemPayDetailsInnerRoute() + + @Serializable + data object LimitSetup : TangemPayDetailsInnerRoute() + + @Serializable + data object LimitSetupSuccess : TangemPayDetailsInnerRoute() } \ No newline at end of file