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