diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/CustomerMeResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/CustomerMeResponse.kt
index 906a69fa12..61fdefe5b2 100644
--- a/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/CustomerMeResponse.kt
+++ b/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/CustomerMeResponse.kt
@@ -2,6 +2,7 @@ package com.tangem.datasource.api.pay.models.response
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
+import java.math.BigDecimal
@JsonClass(generateAdapter = true)
data class CustomerMeResponse(
@@ -30,6 +31,8 @@ data class CustomerMeResponse(
@Json(name = "updated_at") val updatedAt: String,
@Json(name = "payment_account_id") val paymentAccountId: String,
@Json(name = "display_name") val displayName: String?,
+ @Json(name = "actual_card_limit") val actualCardLimit: CardLimit?,
+ @Json(name = "admin_card_limit") val adminCardLimit: CardLimit?,
) {
@JsonClass(generateAdapter = false)
enum class Status {
@@ -71,6 +74,12 @@ data class CustomerMeResponse(
}
}
+ @JsonClass(generateAdapter = true)
+ data class CardLimit(
+ @Json(name = "amount") val amount: BigDecimal,
+ @Json(name = "period_type") val periodType: String,
+ )
+
@JsonClass(generateAdapter = true)
data class PaymentAccount(
@Json(name = "id") val id: String,
diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml
index e316bc77d9..2306a9be2e 100644
--- a/core/res/src/main/res/values/strings.xml
+++ b/core/res/src/main/res/values/strings.xml
@@ -1643,6 +1643,11 @@
You can\'t initiate swap or new withdrawal till the current one is finished
Withdrawal in progress
Card settings
+ Daily limit
+ Current limit
+ Change
+ Daily limit unavailable
+ We couldn\'t load your daily limit. Please try again.
Change PIN-code
Come back to the app if you forget it.
Digital card
diff --git a/core/ui/src/main/res/drawable/ic_limit_20.xml b/core/ui/src/main/res/drawable/ic_limit_20.xml
new file mode 100644
index 0000000000..4fc3491e34
--- /dev/null
+++ b/core/ui/src/main/res/drawable/ic_limit_20.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultOnboardingRepository.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultOnboardingRepository.kt
index 600f2f3ebc..aa7f791149 100644
--- a/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultOnboardingRepository.kt
+++ b/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultOnboardingRepository.kt
@@ -25,6 +25,8 @@ import com.tangem.domain.pay.datasource.TangemPayAuthDataSource
import com.tangem.domain.pay.model.CustomerInfo
import com.tangem.domain.pay.model.CustomerInfo.CardInfo
import com.tangem.domain.pay.model.CustomerInfo.ProductInstance
+import com.tangem.domain.pay.model.TangemPayCardLimit
+import com.tangem.domain.pay.model.TangemPayCardLimitPeriod
import com.tangem.domain.pay.repository.OnboardingRepository
import com.tangem.domain.tangempay.TangemPayAnalyticsEvents
import com.tangem.domain.visa.error.VisaApiError
@@ -207,6 +209,8 @@ internal class DefaultOnboardingRepository @Inject constructor(
cardId = instance.cardId,
frozenState = cardFrozenState,
displayName = if (displayName != null) CardDisplayName(displayName).getOrElse { null } else null,
+ actualCardLimit = instance.actualCardLimit?.parseCardLimit(),
+ adminCardLimit = instance.adminCardLimit?.parseCardLimit(),
)
}
return CustomerInfo(
@@ -219,6 +223,13 @@ internal class DefaultOnboardingRepository @Inject constructor(
}
}
+ private fun CustomerMeResponse.CardLimit.parseCardLimit(): TangemPayCardLimit {
+ return TangemPayCardLimit(
+ amount = amount,
+ period = TangemPayCardLimitPeriod.fromString(periodType),
+ )
+ }
+
private fun sendKycAnalytics(kycStatus: KycStatus) {
val event = when (kycStatus) {
KycStatus.APPROVED -> TangemPayAnalyticsEvents.KycPassedAndOrderCreated()
diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/pay/model/CustomerInfo.kt b/domain/visa/src/main/kotlin/com/tangem/domain/pay/model/CustomerInfo.kt
index 7c93887879..4f04e19fa0 100644
--- a/domain/visa/src/main/kotlin/com/tangem/domain/pay/model/CustomerInfo.kt
+++ b/domain/visa/src/main/kotlin/com/tangem/domain/pay/model/CustomerInfo.kt
@@ -51,6 +51,8 @@ data class CustomerInfo(
val cardId: String,
val frozenState: TangemPayCardFrozenState,
val displayName: CardDisplayName?,
+ val actualCardLimit: TangemPayCardLimit?,
+ val adminCardLimit: TangemPayCardLimit?,
)
data class CardInfo(
diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/pay/model/TangemPayCardLimit.kt b/domain/visa/src/main/kotlin/com/tangem/domain/pay/model/TangemPayCardLimit.kt
new file mode 100644
index 0000000000..63ba23859b
--- /dev/null
+++ b/domain/visa/src/main/kotlin/com/tangem/domain/pay/model/TangemPayCardLimit.kt
@@ -0,0 +1,32 @@
+package com.tangem.domain.pay.model
+
+import java.math.BigDecimal
+import java.util.Locale
+
+data class TangemPayCardLimit(
+ val amount: BigDecimal,
+ val period: TangemPayCardLimitPeriod,
+)
+
+enum class TangemPayCardLimitPeriod {
+ DAY,
+ WEEK,
+ MONTH,
+ YEAR,
+ ALL_TIME,
+ AUTHORIZATION,
+ UNKNOWN,
+ ;
+
+ companion object {
+ fun fromString(value: String) = when (value.uppercase(Locale.US)) {
+ "DAY" -> DAY
+ "WEEK" -> WEEK
+ "MONTH" -> MONTH
+ "YEAR" -> YEAR
+ "ALL_TIME" -> ALL_TIME
+ "AUTHORIZATION" -> AUTHORIZATION
+ else -> UNKNOWN
+ }
+ }
+}
\ No newline at end of file
diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/entity/TangemPayCardPageUM.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/entity/TangemPayCardPageUM.kt
index 93bda7eba7..77b7bab3d6 100644
--- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/entity/TangemPayCardPageUM.kt
+++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/entity/TangemPayCardPageUM.kt
@@ -9,6 +9,7 @@ import kotlinx.collections.immutable.persistentListOf
internal data class TangemPayCardPageUM(
val settings: ImmutableList,
val onBackClick: () -> Unit,
+ val dailyLimitState: TangemPayDailyLimitBlockState,
val addToWalletBlockState: AddToWalletBlockState? = null,
val isReissueInProgress: Boolean = false,
) {
@@ -21,11 +22,13 @@ internal data class TangemPayCardPageUM(
TangemPayCardPageSetting(TextReference.Str("Reissue Card")) {},
),
isReissueInProgress: Boolean = false,
+ dailyLimitState: TangemPayDailyLimitBlockState = TangemPayDailyLimitBlockState.Content.stub(),
) = TangemPayCardPageUM(
addToWalletBlockState = addToWalletBlockState,
settings = settings,
onBackClick = {},
isReissueInProgress = isReissueInProgress,
+ dailyLimitState = dailyLimitState,
)
}
}
diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/entity/TangemPayDailyLimitBlockState.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/entity/TangemPayDailyLimitBlockState.kt
new file mode 100644
index 0000000000..841bad5227
--- /dev/null
+++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/entity/TangemPayDailyLimitBlockState.kt
@@ -0,0 +1,22 @@
+package com.tangem.features.tangempay.entity
+
+import androidx.compose.runtime.Immutable
+
+@Immutable
+internal sealed interface TangemPayDailyLimitBlockState {
+ data object Loading : TangemPayDailyLimitBlockState
+
+ data object Error : TangemPayDailyLimitBlockState
+
+ data class Content(
+ val limit: String,
+ val onChangeClick: () -> Unit,
+ ) : TangemPayDailyLimitBlockState {
+ companion object {
+ fun stub() = Content(
+ limit = "$5,000",
+ onChangeClick = {},
+ )
+ }
+ }
+}
\ 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 681148be62..4371b252e4 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
@@ -14,11 +14,16 @@ import com.tangem.core.decompose.navigation.Router
import com.tangem.core.decompose.ui.UiMessageSender
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
+import com.tangem.core.ui.format.bigdecimal.fiat
+import com.tangem.core.ui.format.bigdecimal.format
+import com.tangem.core.ui.format.bigdecimal.getJavaCurrencyByCode
import com.tangem.core.ui.message.SnackbarMessage
import com.tangem.domain.models.TokenReceiveConfig
import com.tangem.domain.pay.model.OrderStatus
+import com.tangem.domain.pay.model.TangemPayCardLimitPeriod
import com.tangem.domain.pay.model.TangemPayReissueOrderInfo
import com.tangem.domain.pay.model.TangemPayTopUpData
+import com.tangem.domain.pay.repository.OnboardingRepository
import com.tangem.domain.pay.repository.TangemPayCardDetailsRepository
import com.tangem.domain.pay.repository.TangemPayReissueCardRepository
import com.tangem.domain.tangempay.TangemPayAnalyticsEvents
@@ -33,6 +38,7 @@ import com.tangem.features.tangempay.entity.TangemPayCardPageSetting
import com.tangem.features.tangempay.entity.TangemPayCardPageUM
import com.tangem.features.tangempay.entity.TangemPayDetailsNavigation
import com.tangem.features.tangempay.navigation.TangemPayDetailsInnerRoute
+import com.tangem.features.tangempay.entity.TangemPayDailyLimitBlockState
import com.tangem.features.tangempay.utils.TangemPayMessagesFactory
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.coroutines.JobHolder
@@ -55,6 +61,7 @@ internal class TangemPayCardPageModel @Inject constructor(
private val router: Router,
private val analytics: AnalyticsEventHandler,
private val cardDetailsRepository: TangemPayCardDetailsRepository,
+ private val onboardingRepository: OnboardingRepository,
private val uiMessageSender: UiMessageSender,
private val reissueCardRepository: TangemPayReissueCardRepository,
) : Model(), ViewPinListener, ReissueCardListener, AddFundsListener {
@@ -70,6 +77,7 @@ internal class TangemPayCardPageModel @Inject constructor(
field = MutableStateFlow(
TangemPayCardPageUM(
onBackClick = router::pop,
+ dailyLimitState = TangemPayDailyLimitBlockState.Loading,
settings = persistentListOf(
TangemPayCardPageSetting(
title = TextReference.Res(R.string.tangempay_card_details_change_pin),
@@ -92,6 +100,7 @@ internal class TangemPayCardPageModel @Inject constructor(
init {
// TODO v_rodionov: #[REDACTED_TASK_KEY] check reissue order state before card details are showed
fetchAddToWalletBanner()
+ fetchCardLimit()
subscribeToCardFrozenState()
}
@@ -256,6 +265,39 @@ internal class TangemPayCardPageModel @Inject constructor(
}.saveIn(addToWalletBannerJobHolder)
}
+ private fun fetchCardLimit() {
+ modelScope.launch {
+ onboardingRepository.getCustomerInfo(params.userWalletId)
+ .onRight { info ->
+ val productInstance = info.productInstance
+ val cardInfo = info.cardInfo
+ val actualCardLimit = productInstance?.actualCardLimit
+ val dailyLimitState = if (
+ productInstance != null &&
+ cardInfo != null &&
+ actualCardLimit?.period == TangemPayCardLimitPeriod.DAY
+ ) {
+ val limit = actualCardLimit.amount.format {
+ val symbol = getJavaCurrencyByCode(cardInfo.currencyCode).symbol
+ fiat(cardInfo.currencyCode, symbol)
+ }
+ TangemPayDailyLimitBlockState.Content(
+ limit = limit,
+ onChangeClick = {}, // TODO v_rodionov: #[REDACTED_TASK_KEY]
+ )
+ } else {
+ TangemPayDailyLimitBlockState.Error
+ }
+ uiState.update { it.copy(dailyLimitState = dailyLimitState) }
+ }
+ .onLeft {
+ uiState.update { state ->
+ state.copy(dailyLimitState = TangemPayDailyLimitBlockState.Error)
+ }
+ }
+ }
+ }
+
private fun onClickAddToWallet() {
router.push(TangemPayDetailsInnerRoute.AddToWallet)
}
diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayCardPageScreen.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayCardPageScreen.kt
index 673b118af2..a9a05b551e 100644
--- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayCardPageScreen.kt
+++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayCardPageScreen.kt
@@ -21,6 +21,8 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.foundation.layout.systemBars
import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.LazyItemScope
+import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.material3.Scaffold
import androidx.compose.material3.ScaffoldDefaults
import androidx.compose.material3.Text
@@ -47,6 +49,7 @@ import com.tangem.features.tangempay.entity.DisplayNameState
import com.tangem.features.tangempay.entity.TangemPayCardDetailsUM
import com.tangem.features.tangempay.entity.TangemPayCardPageSetting
import com.tangem.features.tangempay.entity.TangemPayCardPageUM
+import com.tangem.features.tangempay.entity.TangemPayDailyLimitBlockState
import kotlinx.collections.immutable.ImmutableList
private const val CONTENT_FADE_DURATION_MS = 300
@@ -88,33 +91,23 @@ internal fun TangemPayCardPageScreen(
)
}
if (state.addToWalletBlockState != null) {
- item(key = "GooglePay") {
- val visibleState = remember { MutableTransitionState(false).apply { targetState = true } }
- AnimatedVisibility(
- visibleState = visibleState,
- enter = fadeIn(animationSpec = tween(CONTENT_FADE_DURATION_MS)),
- exit = fadeOut(animationSpec = tween(CONTENT_FADE_DURATION_MS)),
- ) {
- TangemPayAddToWalletBlock(
- state = state.addToWalletBlockState,
- )
- }
+ cardPageItem(key = "GooglePay") {
+ TangemPayAddToWalletBlock(state = state.addToWalletBlockState)
}
}
- item(key = "Settings") {
- val visibleState = remember { MutableTransitionState(false).apply { targetState = true } }
- AnimatedVisibility(
- visibleState = visibleState,
- enter = fadeIn(animationSpec = tween(CONTENT_FADE_DURATION_MS)),
- exit = fadeOut(animationSpec = tween(CONTENT_FADE_DURATION_MS)),
- ) {
- if (state.isReissueInProgress) {
- TangemPayReplacingCardBlock()
- } else {
- TangemPayCardPageSettingsBlock(
- settings = state.settings,
- )
- }
+ cardPageItem(key = "Limit") {
+ TangemPayDailyLimitBlock(state = state.dailyLimitState)
+ }
+ if (state.dailyLimitState == TangemPayDailyLimitBlockState.Error) {
+ cardPageItem(key = "LimitError") {
+ TangemPayDailyLimitErrorBlock()
+ }
+ }
+ cardPageItem(key = "Settings") {
+ if (state.isReissueInProgress) {
+ TangemPayReplacingCardBlock()
+ } else {
+ TangemPayCardPageSettingsBlock(settings = state.settings)
}
}
}
@@ -187,6 +180,26 @@ private fun TangemPayCardPageSettingRow(
}
}
+private fun LazyListScope.cardPageItem(
+ key: Any? = null,
+ contentType: Any? = null,
+ content: @Composable LazyItemScope.() -> Unit,
+) {
+ item(
+ key = key,
+ contentType = contentType,
+ ) {
+ val visibleState = remember { MutableTransitionState(false).apply { targetState = true } }
+ AnimatedVisibility(
+ visibleState = visibleState,
+ enter = fadeIn(animationSpec = tween(CONTENT_FADE_DURATION_MS)),
+ exit = fadeOut(animationSpec = tween(CONTENT_FADE_DURATION_MS)),
+ ) {
+ content()
+ }
+ }
+}
+
@Preview
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayDailyLimitBlock.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayDailyLimitBlock.kt
new file mode 100644
index 0000000000..c79c7aa582
--- /dev/null
+++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayDailyLimitBlock.kt
@@ -0,0 +1,140 @@
+package com.tangem.features.tangempay.ui
+
+import android.content.res.Configuration
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.material3.Icon
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import com.tangem.core.ui.components.SecondaryButton
+import com.tangem.core.ui.components.SpacerH12
+import com.tangem.core.ui.components.SpacerW12
+import com.tangem.core.ui.components.SpacerW8
+import com.tangem.core.ui.components.TextShimmer
+import com.tangem.core.ui.components.buttons.common.TangemButtonSize
+import com.tangem.core.ui.components.notifications.Notification
+import com.tangem.core.ui.components.notifications.NotificationConfig
+import com.tangem.core.ui.extensions.resourceReference
+import com.tangem.core.ui.extensions.stringResourceSafe
+import com.tangem.core.ui.res.TangemTheme
+import com.tangem.core.ui.res.TangemThemePreview
+import com.tangem.features.tangempay.details.impl.R
+import com.tangem.features.tangempay.entity.TangemPayDailyLimitBlockState
+
+@Composable
+internal fun TangemPayDailyLimitBlock(state: TangemPayDailyLimitBlockState, modifier: Modifier = Modifier) {
+ Column(
+ modifier = modifier
+ .fillMaxWidth()
+ .background(
+ color = TangemTheme.colors.background.primary,
+ shape = TangemTheme.shapes.roundedCornersMedium,
+ )
+ .padding(TangemTheme.dimens.spacing12),
+ ) {
+ Text(
+ text = stringResourceSafe(R.string.tangempay_card_page_daily_limit_title),
+ style = TangemTheme.typography.subtitle2,
+ color = TangemTheme.colors.text.tertiary,
+ )
+ SpacerH12()
+ CurrentLimitBlock(state)
+ }
+}
+
+@Composable
+private fun CurrentLimitBlock(state: TangemPayDailyLimitBlockState) {
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ verticalAlignment = Alignment.CenterVertically,
+ ) {
+ Box(
+ modifier = Modifier
+ .size(36.dp)
+ .background(
+ color = TangemTheme.colors.icon.accent.copy(alpha = 0.1f),
+ shape = CircleShape,
+ ),
+ contentAlignment = Alignment.Center,
+ ) {
+ Icon(
+ painter = painterResource(R.drawable.ic_limit_20),
+ contentDescription = null,
+ tint = TangemTheme.colors.icon.accent,
+ )
+ }
+ SpacerW12()
+ Column(
+ modifier = Modifier.weight(1f),
+ ) {
+ Text(
+ text = stringResourceSafe(R.string.tangempay_card_page_daily_limit_current_limit),
+ style = TangemTheme.typography.subtitle2,
+ color = TangemTheme.colors.text.tertiary,
+ )
+ when (state) {
+ TangemPayDailyLimitBlockState.Error,
+ is TangemPayDailyLimitBlockState.Content,
+ -> Text(
+ text = if (state is TangemPayDailyLimitBlockState.Content) state.limit else "—",
+ style = TangemTheme.typography.subtitle1,
+ color = TangemTheme.colors.text.primary1,
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis,
+ )
+ TangemPayDailyLimitBlockState.Loading -> TextShimmer(
+ style = TangemTheme.typography.subtitle1,
+ text = "$50,000",
+ )
+ }
+ }
+ SpacerW8()
+ if (state is TangemPayDailyLimitBlockState.Content) {
+ SecondaryButton(
+ text = stringResourceSafe(R.string.tangempay_card_page_daily_limit_change),
+ onClick = state.onChangeClick,
+ size = TangemButtonSize.Small,
+ )
+ }
+ }
+}
+
+@Composable
+internal fun TangemPayDailyLimitErrorBlock(modifier: Modifier = Modifier) {
+ Notification(
+ config = NotificationConfig(
+ title = resourceReference(R.string.tangempay_card_page_daily_limit_error_title),
+ subtitle = resourceReference(R.string.tangempay_card_page_daily_limit_error_description),
+ iconResId = R.drawable.img_attention_20,
+ ),
+ containerColor = TangemTheme.colors.background.action,
+ modifier = modifier.fillMaxWidth(),
+ )
+}
+
+@Preview
+@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
+@Composable
+private fun preview() = TangemThemePreview {
+ Column(
+ verticalArrangement = Arrangement.spacedBy(16.dp),
+ ) {
+ TangemPayDailyLimitBlock(state = TangemPayDailyLimitBlockState.Content.stub())
+ TangemPayDailyLimitBlock(state = TangemPayDailyLimitBlockState.Error)
+ TangemPayDailyLimitBlock(state = TangemPayDailyLimitBlockState.Loading)
+ TangemPayDailyLimitErrorBlock()
+ }
+}
\ No newline at end of file