Updated on 2026-08-14
This commit is contained in:
parent
bc3443c479
commit
2e0bdac248
21 changed files with 752 additions and 30 deletions
|
|
@ -109,9 +109,10 @@ interface TangemPayApi {
|
|||
@Body body: WithdrawRequest,
|
||||
): ApiResponse<WithdrawResponse>
|
||||
|
||||
@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<UpdateCardDisplayNameResponse>
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
@ -1676,6 +1676,8 @@
|
|||
<string name="tangempay_card_page_daily_limit_success_title">Daily limit is set</string>
|
||||
<string name="tangempay_card_page_daily_limit_title">Daily limit</string>
|
||||
<string name="tangempay_card_page_settings_title">Card settings</string>
|
||||
<string name="tangempay_card_limit_setup_amount_subtitle">Set a limit from %1$s</string>
|
||||
<string name="tangempay_card_limit_setup_error_message">We couldn’t set the limit. Please try again.</string>
|
||||
<string name="tangempay_change_pin_code">Change PIN-code</string>
|
||||
<string name="tangempay_come_back_if_forget_pin">Come back to the app if you forget it.</string>
|
||||
<string name="tangempay_daily_limit_hint" formatted="false">Set a limit from %s to %s</string>
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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<UniversalError, Unit> {
|
||||
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<UniversalError, Unit> {
|
||||
return catch(
|
||||
block = {
|
||||
requestHelper.performRequest(userWalletId) { authHeader ->
|
||||
tangemPayApi.updateCard(
|
||||
authHeader = authHeader,
|
||||
body = UpdateCardRequest(
|
||||
cardLimit = UpdateCardRequest.CardLimit(limit),
|
||||
),
|
||||
cardId = cardId,
|
||||
)
|
||||
}.fold(
|
||||
ifLeft = { error -> error.left() },
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
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))
|
||||
|
|
@ -34,7 +34,14 @@ interface TangemPayCardDetailsRepository {
|
|||
suspend fun cardFrozenStateSync(cardId: String): TangemPayCardFrozenState?
|
||||
|
||||
suspend fun updateCardDisplayName(
|
||||
cardId: String,
|
||||
userWalletId: UserWalletId,
|
||||
displayName: CardDisplayName,
|
||||
): Either<UniversalError, Unit>
|
||||
|
||||
suspend fun updateCardLimit(
|
||||
cardId: String,
|
||||
userWalletId: UserWalletId,
|
||||
limit: String,
|
||||
): Either<UniversalError, Unit>
|
||||
}
|
||||
|
|
@ -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<UniversalError, Unit> {
|
||||
return cardDetailsRepository.updateCardLimit(cardId, userWalletId, amount.toPlainString())
|
||||
.onRight {
|
||||
val params = PaymentAccountStatusFetcher.Params(userWalletId)
|
||||
paymentAccountStatusFetcher.invoke(params)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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<TangemPayCardLimitSetupUM>
|
||||
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<PaymentAccountStatusValue.Loaded>()
|
||||
.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()
|
||||
}
|
||||
|
|
@ -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<TangemPayCardLimitSetupUM.LimitPresetUM>) {
|
||||
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(),
|
||||
)
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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 = {})
|
||||
}
|
||||
}
|
||||
|
|
@ -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<LimitPresetUM>,
|
||||
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<LimitPresetUM> = 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 = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -19,4 +19,10 @@ internal sealed class TangemPayDetailsInnerRoute : Route {
|
|||
|
||||
@Serializable
|
||||
data object EditCardDisplayName : TangemPayDetailsInnerRoute()
|
||||
|
||||
@Serializable
|
||||
data object LimitSetup : TangemPayDetailsInnerRoute()
|
||||
|
||||
@Serializable
|
||||
data object LimitSetupSuccess : TangemPayDetailsInnerRoute()
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue