Updated on 2026-08-14

This commit is contained in:
Tangem 2026-04-24 05:01:25 -07:00
parent bc3443c479
commit 2e0bdac248
21 changed files with 752 additions and 30 deletions

View file

@ -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>
}

View file

@ -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,
)

View file

@ -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,
)
}

View file

@ -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 couldnt 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>

View file

@ -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(

View file

@ -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() },

View file

@ -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))

View file

@ -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>
}

View file

@ -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)
}
}
}

View file

@ -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() {

View file

@ -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() {

View file

@ -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
}

View file

@ -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,
)
}
}

View file

@ -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()
}

View file

@ -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(),
)
}

View file

@ -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)
}
}

View file

@ -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 = {})
}
}

View file

@ -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 = {},
)
}
}

View file

@ -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)

View file

@ -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 {

View file

@ -19,4 +19,10 @@ internal sealed class TangemPayDetailsInnerRoute : Route {
@Serializable
data object EditCardDisplayName : TangemPayDetailsInnerRoute()
@Serializable
data object LimitSetup : TangemPayDetailsInnerRoute()
@Serializable
data object LimitSetupSuccess : TangemPayDetailsInnerRoute()
}