Updated on 2026-08-14

This commit is contained in:
Tangem 2026-01-13 16:42:46 +04:00
parent 9ee6611585
commit ba28cf5b6d
29 changed files with 761 additions and 41 deletions

View file

@ -23,6 +23,7 @@ import com.tangem.feature.wallet.child.wallet.model.WalletModel
import com.tangem.feature.wallet.navigation.WalletRoute
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletDialogConfig
import com.tangem.feature.wallet.presentation.wallet.ui.WalletScreen
import com.tangem.feature.wallet.presentation.wallet.ui.components.visa.KycRejectedComponent
import com.tangem.feature.walletsettings.component.RenameWalletComponent
import com.tangem.features.biometry.AskBiometryComponent
import com.tangem.features.feed.entry.components.FeedEntryComponent
@ -126,6 +127,17 @@ internal class WalletComponent @AssistedInject constructor(
),
)
}
is WalletDialogConfig.KycRejected -> {
KycRejectedComponent(
appComponentContext = childByContext(componentContext),
params = KycRejectedComponent.Params(
walletId = dialogConfig.walletId,
customerId = dialogConfig.customerId,
callbacks = model.tangemPayKycRejectedCallbacks,
onDismiss = model.innerWalletRouter.dialogNavigation::dismiss,
),
)
}
}
},
)

View file

@ -39,6 +39,7 @@ import com.tangem.feature.wallet.presentation.wallet.state.model.WalletEvent.Dem
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletScreenState
import com.tangem.feature.wallet.presentation.wallet.state.transformers.*
import com.tangem.feature.wallet.presentation.wallet.state.utils.WalletEventSender
import com.tangem.feature.wallet.presentation.wallet.ui.components.visa.KycRejectedCallbacks
import com.tangem.feature.wallet.presentation.wallet.utils.ScreenLifecycleProvider
import com.tangem.features.biometry.AskBiometryComponent
import com.tangem.features.feed.entry.featuretoggle.FeedFeatureToggle
@ -107,6 +108,7 @@ internal class WalletModel @Inject constructor(
) : Model() {
val askBiometryModelCallbacks = AskBiometryModelCallbacks()
val tangemPayKycRejectedCallbacks = TangemPayKycRejectedCallbacks()
val askForPushNotificationsModelCallbacks = AskForPushNotificationsCallbacks()
val uiState: StateFlow<WalletScreenState> = stateHolder.uiState
@ -769,6 +771,20 @@ internal class WalletModel @Inject constructor(
}
}
inner class TangemPayKycRejectedCallbacks : KycRejectedCallbacks {
override fun onClickYourProfile(userWalletId: UserWalletId) {
clickIntents.onKycRejectedOpenProfileClicked(userWalletId)
}
override fun onClickGoToSupport(customerId: String) {
clickIntents.onKycRejectedGoToSupportClicked(customerId)
}
override fun onClickHideKyc(userWalletId: UserWalletId) {
clickIntents.onKycRejectedHideKycClicked(userWalletId)
}
}
inner class AskForPushNotificationsCallbacks : PushNotificationsModelCallbacks {
override fun onAllowSystemPermission() {

View file

@ -1,5 +1,6 @@
package com.tangem.feature.wallet.child.wallet.model.intents
import com.arkivanov.decompose.router.slot.activate
import com.tangem.common.routing.AppRoute
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.ui.UiMessageSender
@ -13,6 +14,7 @@ import com.tangem.core.ui.message.bottomSheetMessage
import com.tangem.domain.feedback.GetWalletMetaInfoUseCase
import com.tangem.domain.feedback.SendFeedbackEmailUseCase
import com.tangem.domain.feedback.models.FeedbackEmailType
import com.tangem.domain.feedback.models.WalletMetaInfo
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.pay.TangemPayDetailsConfig
@ -21,6 +23,7 @@ import com.tangem.domain.pay.repository.OnboardingRepository
import com.tangem.domain.pay.usecase.ProduceTangemPayInitialDataUseCase
import com.tangem.domain.pay.usecase.TangemPayMainScreenCustomerInfoUseCase
import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletDialogConfig
import com.tangem.feature.wallet.presentation.wallet.state.transformers.TangemPayHideOnboardingStateTransformer
import com.tangem.feature.wallet.presentation.wallet.state.transformers.TangemPayRefreshNeededStateTransformer
import com.tangem.feature.wallet.presentation.wallet.state.transformers.TangemPayRefreshShowProgressTransformer
@ -38,6 +41,14 @@ internal interface TangemPayIntents {
fun onKycProgressClicked(userWalletId: UserWalletId)
fun onKycRejectedClicked(userWalletId: UserWalletId, customerId: String)
fun onKycRejectedOpenProfileClicked(userWalletId: UserWalletId)
fun onKycRejectedGoToSupportClicked(customerId: String)
fun onKycRejectedHideKycClicked(userWalletId: UserWalletId)
fun onIssuingCardClicked()
fun onIssuingFailedClicked()
@ -137,6 +148,42 @@ internal class TangemPayClickIntentsImplementor @Inject constructor(
uiMessageSender.send(kycInfoBottomSheet)
}
override fun onKycRejectedOpenProfileClicked(userWalletId: UserWalletId) {
router.openTangemPayOnboarding(
mode = AppRoute.TangemPayOnboarding.Mode.ContinueOnboarding(userWalletId),
)
}
override fun onKycRejectedGoToSupportClicked(customerId: String) {
goToSupportForRejectKyc(customerId)
}
override fun onKycRejectedHideKycClicked(userWalletId: UserWalletId) {
disableTangemPay(userWalletId)
}
override fun onKycRejectedClicked(userWalletId: UserWalletId, customerId: String) {
router.dialogNavigation.activate(
configuration = WalletDialogConfig.KycRejected(
walletId = userWalletId,
customerId = customerId,
),
)
}
private fun goToSupportForRejectKyc(customerId: String) {
modelScope.launch {
sendFeedbackEmailUseCase(
type = FeedbackEmailType.Visa.KycRejected(
walletMetaInfo = WalletMetaInfo(
userWalletId = stateHolder.getSelectedWalletId(),
),
customerId = customerId,
),
)
}
}
override fun onIssuingCardClicked() {
val issuingBottomSheet = bottomSheetMessage {
infoBlock {

View file

@ -4,6 +4,7 @@ import com.tangem.core.decompose.model.Model
import com.tangem.feature.wallet.DefaultWalletEntryComponent
import com.tangem.feature.wallet.child.organizetokens.model.OrganizeTokensModel
import com.tangem.feature.wallet.child.wallet.model.WalletModel
import com.tangem.feature.wallet.presentation.wallet.ui.components.visa.KycRejectedModel
import com.tangem.feature.wallet.utils.DefaultUserWalletImageFetcher
import com.tangem.feature.wallet.utils.DefaultUserWalletsFetcher
import com.tangem.features.wallet.WalletEntryComponent
@ -36,6 +37,11 @@ internal interface WalletFeatureModule {
@ClassKey(WalletModel::class)
fun bindWalletModel(model: WalletModel): Model
@Binds
@IntoMap
@ClassKey(KycRejectedModel::class)
fun bindKycRejectedModel(model: KycRejectedModel): Model
@Binds
@IntoMap
@ClassKey(OrganizeTokensModel::class)

View file

@ -2,6 +2,7 @@ package com.tangem.feature.wallet.presentation.wallet.analytics.utils
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.domain.pay.model.CustomerInfo
import com.tangem.domain.pay.model.MainScreenCustomerInfo
import com.tangem.domain.pay.model.OrderStatus
import com.tangem.domain.tangempay.TangemPayAnalyticsEvents
@ -25,8 +26,10 @@ internal class WalletTangemPayAnalyticsEventSender @Inject constructor(
// TODO: TangemPay refactor analytics
// when statement copied from TangemPayUpdateInfoStateTransformer. Be careful when editing
val event = when {
customerInfo.orderStatus == OrderStatus.CANCELED -> return // ignore cancelled state on analytics
!customerInfo.info.isKycApproved -> return // ignore kyc not approved state on analytics
// ignore cancelled state on analytics
customerInfo.orderStatus == OrderStatus.CANCELED -> return
// ignore kyc not approved state on analytics
customerInfo.info.kycStatus != CustomerInfo.KycStatus.APPROVED -> return
cardInfo != null && productInstance != null -> return
else -> TangemPayAnalyticsEvents.IssuingBannerDisplayed()
}

View file

@ -32,4 +32,7 @@ internal sealed interface WalletDialogConfig {
val tokenAction: TokenAction,
val onWarningAcknowledged: (TokenAction) -> Unit,
) : WalletDialogConfig
@Serializable
data class KycRejected(val walletId: UserWalletId, val customerId: String) : WalletDialogConfig
}

View file

@ -16,6 +16,8 @@ import com.tangem.feature.wallet.child.wallet.model.intents.TangemPayIntents
import com.tangem.feature.wallet.presentation.wallet.state.model.TangemPayState
import com.tangem.feature.wallet.presentation.wallet.state.model.TangemPayState.Progress
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState
import com.tangem.domain.pay.model.CustomerInfo.KycStatus.APPROVED
import com.tangem.domain.pay.model.CustomerInfo
import java.util.Currency
/**
@ -43,11 +45,13 @@ internal class TangemPayUpdateInfoStateTransformer(
private fun createInitialState(): TangemPayState {
val cardInfo = value.info.cardInfo
val productInstance = value.info.productInstance
val customerId = value.info.customerId
// when statement copied to WalletTangemPayAnalyticsEventSender. Be careful when editing.
return when {
value.orderStatus == OrderStatus.CANCELED -> createCancelledState()
!value.info.isKycApproved -> createKycInProgressState()
value.info.kycStatus != APPROVED && !customerId.isNullOrEmpty() ->
createKycInProgressState(kycStatus = value.info.kycStatus, customerId = customerId)
cardInfo != null && productInstance != null -> getCardInfoState(cardInfo, productInstance)
else -> createIssueProgressState()
}
@ -80,13 +84,25 @@ internal class TangemPayUpdateInfoStateTransformer(
}
}
private fun createKycInProgressState(): TangemPayState = Progress(
title = TextReference.Res(R.string.tangempay_payment_account),
description = TextReference.Res(R.string.tangempay_kyc_in_progress),
buttonText = TextReference.Res(R.string.tangempay_kyc_in_progress_notification_button),
iconRes = R.drawable.ic_promo_kyc_36,
onButtonClick = { tangemPayClickIntents.onKycProgressClicked(userWalletId) },
)
private fun createKycInProgressState(kycStatus: CustomerInfo.KycStatus, customerId: String): TangemPayState =
Progress(
title = TextReference.Res(R.string.tangempay_payment_account),
description = when (kycStatus) {
CustomerInfo.KycStatus.REJECTED -> TextReference.Res(R.string.tangempay_kyc_has_failed)
else -> TextReference.Res(R.string.tangempay_kyc_in_progress)
},
buttonText = TextReference.Res(R.string.tangempay_kyc_in_progress_notification_button),
iconRes = R.drawable.ic_promo_kyc_36,
onButtonClick = {
when (kycStatus) {
CustomerInfo.KycStatus.REJECTED -> tangemPayClickIntents.onKycRejectedClicked(
userWalletId = userWalletId,
customerId = customerId,
)
else -> tangemPayClickIntents.onKycProgressClicked(userWalletId)
}
},
)
private fun createIssueProgressState(): TangemPayState = Progress(
title = TextReference.Res(R.string.tangempay_payment_account),

View file

@ -0,0 +1,49 @@
package com.tangem.feature.wallet.presentation.wallet.ui.components.visa
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.core.ui.components.bottomsheets.message.MessageBottomSheetV2
import com.tangem.core.ui.decompose.ComposableBottomSheetComponent
import com.tangem.domain.models.wallet.UserWalletId
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
internal class KycRejectedComponent @AssistedInject constructor(
@Assisted appComponentContext: AppComponentContext,
@Assisted params: Params,
) : ComposableBottomSheetComponent, AppComponentContext by appComponentContext {
private val model: KycRejectedModel = getOrCreateModel(params = params)
override fun dismiss() {
model.onDismiss()
}
@Composable
override fun BottomSheet() {
val state by model.uiState.collectAsStateWithLifecycle()
MessageBottomSheetV2(state = state, onDismissRequest = ::dismiss)
}
data class Params(
val callbacks: KycRejectedCallbacks,
val walletId: UserWalletId,
val customerId: String,
val onDismiss: () -> Unit,
)
@AssistedFactory
interface Factory {
fun create(appComponentContext: AppComponentContext, params: Params): KycRejectedComponent
}
}
internal interface KycRejectedCallbacks {
fun onClickYourProfile(userWalletId: UserWalletId)
fun onClickGoToSupport(customerId: String)
fun onClickHideKyc(userWalletId: UserWalletId)
}

View file

@ -0,0 +1,77 @@
package com.tangem.feature.wallet.presentation.wallet.ui.components.visa
import androidx.compose.runtime.Stable
import androidx.compose.ui.text.style.TextDecoration
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.res.R
import com.tangem.core.ui.components.bottomsheets.message.*
import com.tangem.core.ui.extensions.combinedReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.extensions.styledResourceReference
import com.tangem.core.ui.message.bottomSheetMessage
import com.tangem.core.ui.res.TangemTheme
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import javax.inject.Inject
@Stable
@ModelScoped
internal class KycRejectedModel @Inject constructor(
paramsContainer: ParamsContainer,
override val dispatchers: CoroutineDispatcherProvider,
) : Model() {
private val params = paramsContainer.require<KycRejectedComponent.Params>()
val uiState: StateFlow<MessageBottomSheetUMV2>
field = MutableStateFlow(getInitialState())
private fun getInitialState(): MessageBottomSheetUMV2 {
return bottomSheetMessage {
infoBlock {
icon(com.tangem.core.ui.R.drawable.ic_heart_broken_32) {
type = MessageBottomSheetUMV2.Icon.Type.Warning
backgroundType = MessageBottomSheetUMV2.Icon.BackgroundType.Warning
}
title = resourceReference(R.string.tangempay_kyc_rejected)
body = combinedReference(
resourceReference(R.string.tangempay_kyc_rejected_description),
stringReference(" "),
styledResourceReference(
id = R.string.tangempay_kyc_rejected_description_span,
spanStyleReference = {
TangemTheme.typography.body2.copy(TangemTheme.colors.text.accent).toSpanStyle()
.copy(textDecoration = TextDecoration.None)
},
onClick = {
params.callbacks.onClickYourProfile(userWalletId = params.walletId)
onDismiss()
},
),
)
}
primaryButton {
text = resourceReference(R.string.tangempay_go_to_support)
onClick = {
params.callbacks.onClickGoToSupport(customerId = params.customerId)
onDismiss()
}
}
secondaryButton {
text = resourceReference(R.string.tangempay_kyc_rejected_button_text)
onClick = {
params.callbacks.onClickHideKyc(userWalletId = params.walletId)
onDismiss()
}
}
}.messageBottomSheetUMV2
}
fun onDismiss() {
params.onDismiss()
}
}