Updated on 2026-08-14
This commit is contained in:
parent
5f1a711c8c
commit
bd13529d69
17 changed files with 550 additions and 77 deletions
|
|
@ -130,6 +130,15 @@ internal class TangemPayCardPageScreenComponent(
|
|||
onFieldCopied = model::onVaFieldCopied,
|
||||
),
|
||||
)
|
||||
is TangemPayCardNavigation.VaBankingDetailsError -> TangemPayVaBankingDetailsErrorComponent(
|
||||
appComponentContext = context,
|
||||
params = TangemPayVaBankingDetailsErrorComponent.Params(
|
||||
userWalletId = navigation.userWalletId,
|
||||
onDismiss = model.bottomSheetNavigation::dismiss,
|
||||
onContactSupport = model::onContactSupportClicked,
|
||||
onResolved = model::onVaBankingDetailsResolved,
|
||||
),
|
||||
)
|
||||
is TangemPayCardNavigation.Receive -> tokenReceiveComponentFactory.create(
|
||||
context = context,
|
||||
params = TokenReceiveComponent.Params(
|
||||
|
|
|
|||
|
|
@ -159,6 +159,15 @@ internal class TangemPayDetailsComponent(
|
|||
onFieldCopied = model::onVaFieldCopied,
|
||||
),
|
||||
)
|
||||
is TangemPayDetailsNavigation.VaBankingDetailsError -> TangemPayVaBankingDetailsErrorComponent(
|
||||
appComponentContext = context,
|
||||
params = TangemPayVaBankingDetailsErrorComponent.Params(
|
||||
userWalletId = navigation.userWalletId,
|
||||
onDismiss = model.bottomSheetNavigation::dismiss,
|
||||
onContactSupport = model::onContactSupportClicked,
|
||||
onResolved = model::onVaBankingDetailsResolved,
|
||||
),
|
||||
)
|
||||
is TangemPayDetailsNavigation.IssueAdditionalCard -> TangemPayIssueAdditionalCardComponent(
|
||||
appComponentContext = context,
|
||||
params = TangemPayIssueAdditionalCardComponent.Params(
|
||||
|
|
|
|||
|
|
@ -0,0 +1,44 @@
|
|||
package com.tangem.features.tangempay.components
|
||||
|
||||
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.decompose.ComposableBottomSheetComponent
|
||||
import com.tangem.domain.models.account.VirtualAccountOnramp
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.features.tangempay.model.TangemPayVaBankingDetailsErrorModel
|
||||
import com.tangem.features.tangempay.ui.TangemPayVaBankingDetailsErrorBottomSheet
|
||||
|
||||
/**
|
||||
* Error bottom sheet shown when VA bank credentials fail to load ([VirtualAccountOnramp.BankCredentialsError]).
|
||||
*
|
||||
* "Try again" re-fetches the payment account status while showing a loader on the button; on success the
|
||||
* resolved on-ramp is handed back via [Params.onResolved] (the parent opens the bank-transfer sheet), otherwise
|
||||
* the error stays visible with the loader cleared.
|
||||
*/
|
||||
internal class TangemPayVaBankingDetailsErrorComponent(
|
||||
appComponentContext: AppComponentContext,
|
||||
params: Params,
|
||||
) : ComposableBottomSheetComponent, AppComponentContext by appComponentContext {
|
||||
|
||||
private val model: TangemPayVaBankingDetailsErrorModel = getOrCreateModel(params = params)
|
||||
|
||||
override fun dismiss() {
|
||||
model.onDismiss()
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun BottomSheet() {
|
||||
val state by model.uiState.collectAsStateWithLifecycle()
|
||||
TangemPayVaBankingDetailsErrorBottomSheet(state = state)
|
||||
}
|
||||
|
||||
data class Params(
|
||||
val userWalletId: UserWalletId,
|
||||
val onDismiss: () -> Unit,
|
||||
val onContactSupport: () -> Unit,
|
||||
val onResolved: (VirtualAccountOnramp) -> Unit,
|
||||
)
|
||||
}
|
||||
|
|
@ -50,6 +50,11 @@ internal interface TangemPayModelModule {
|
|||
@ClassKey(TangemPayVirtualAccountDepositModel::class)
|
||||
fun bindTangemPayVirtualAccountDepositModel(model: TangemPayVirtualAccountDepositModel): Model
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@ClassKey(TangemPayVaBankingDetailsErrorModel::class)
|
||||
fun bindTangemPayVaBankingDetailsErrorModel(model: TangemPayVaBankingDetailsErrorModel): Model
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@ClassKey(TangemPayViewPinModel::class)
|
||||
|
|
|
|||
|
|
@ -48,6 +48,11 @@ internal sealed class TangemPayCardNavigation {
|
|||
val bankCredentials: BankCredentials,
|
||||
) : TangemPayCardNavigation()
|
||||
|
||||
@Serializable
|
||||
data class VaBankingDetailsError(
|
||||
val userWalletId: UserWalletId,
|
||||
) : TangemPayCardNavigation()
|
||||
|
||||
@Serializable
|
||||
data class Receive(val config: TokenReceiveConfig) : TangemPayCardNavigation()
|
||||
}
|
||||
|
|
@ -39,6 +39,11 @@ internal sealed class TangemPayDetailsNavigation {
|
|||
val bankCredentials: BankCredentials,
|
||||
) : TangemPayDetailsNavigation()
|
||||
|
||||
@Serializable
|
||||
data class VaBankingDetailsError(
|
||||
val userWalletId: UserWalletId,
|
||||
) : TangemPayDetailsNavigation()
|
||||
|
||||
@Serializable
|
||||
data class TransactionDetails(
|
||||
val transaction: TangemPayTxHistoryItem,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,17 @@
|
|||
package com.tangem.features.tangempay.entity
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
|
||||
/**
|
||||
* UI state for the "couldn't load banking details" bottom sheet (VA MVP0, TWI-1638).
|
||||
*
|
||||
* @property isRetryLoading whether the "Try again" button shows a loader while the payment account status
|
||||
* is being re-fetched. While `true` both actions are disabled.
|
||||
*/
|
||||
@Immutable
|
||||
internal data class TangemPayVaBankingDetailsErrorUM(
|
||||
val isRetryLoading: Boolean,
|
||||
val onRetryClick: () -> Unit,
|
||||
val onContactSupportClick: () -> Unit,
|
||||
val onDismiss: () -> Unit,
|
||||
)
|
||||
|
|
@ -7,6 +7,7 @@ import com.arkivanov.decompose.router.slot.dismiss
|
|||
import com.tangem.common.routing.AppRoute
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.core.analytics.models.AnalyticsParam
|
||||
import com.tangem.core.analytics.models.Basic
|
||||
import com.tangem.core.decompose.di.ModelScoped
|
||||
import com.tangem.core.decompose.model.Model
|
||||
import com.tangem.core.decompose.model.ParamsContainer
|
||||
|
|
@ -24,6 +25,9 @@ import com.tangem.core.ui.res.TangemTheme
|
|||
import com.tangem.core.ui.res.generated.icons.Icons
|
||||
import com.tangem.core.ui.res.generated.icons.ic_arrow_refresh_20
|
||||
import com.tangem.core.ui.test.TangemPayTestTags
|
||||
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.StatusSource
|
||||
import com.tangem.domain.models.TokenReceiveConfig
|
||||
import com.tangem.domain.models.account.AccountStatus
|
||||
|
|
@ -72,11 +76,12 @@ import com.tangem.core.ui.R as CoreUiR
|
|||
@ModelScoped
|
||||
internal class TangemPayCardPageModel @Inject constructor(
|
||||
paramsContainer: ParamsContainer,
|
||||
paymentAccountStatusSupplier: PaymentAccountStatusSupplier,
|
||||
private val paymentAccountStatusSupplier: PaymentAccountStatusSupplier,
|
||||
private val paymentAccountStatusFetcher: PaymentAccountStatusFetcher,
|
||||
override val dispatchers: CoroutineDispatcherProvider,
|
||||
private val router: Router,
|
||||
private val analytics: AnalyticsEventHandler,
|
||||
private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase,
|
||||
private val cardDetailsRepository: TangemPayCardDetailsRepository,
|
||||
private val uiMessageSender: UiMessageSender,
|
||||
private val changeCardFrozenStateUseCase: ChangeCardFrozenStateUseCase,
|
||||
|
|
@ -488,7 +493,16 @@ internal class TangemPayCardPageModel @Inject constructor(
|
|||
|
||||
override fun onClickBankTransfer() {
|
||||
val loaded = currentStatus.value.ifLoadedOrNull { it } ?: return
|
||||
val onramp = loaded.virtualAccount ?: return
|
||||
when (val onramp = loaded.virtualAccount) {
|
||||
null -> return
|
||||
is VirtualAccountOnramp.BankCredentialsError -> showVaBankingDetailsError()
|
||||
is VirtualAccountOnramp.Available,
|
||||
VirtualAccountOnramp.Eligible,
|
||||
-> openVirtualAccountDeposit(onramp, loaded)
|
||||
}
|
||||
}
|
||||
|
||||
private fun openVirtualAccountDeposit(onramp: VirtualAccountOnramp, loaded: PaymentAccountStatusValue.Loaded) {
|
||||
analytics.send(TangemPayAnalyticsEvents.VaTopupButtonClicked())
|
||||
bottomSheetNavigation.dismiss()
|
||||
bottomSheetNavigation.activate(
|
||||
|
|
@ -500,6 +514,31 @@ internal class TangemPayCardPageModel @Inject constructor(
|
|||
)
|
||||
}
|
||||
|
||||
private fun showVaBankingDetailsError() {
|
||||
bottomSheetNavigation.dismiss()
|
||||
bottomSheetNavigation.activate(
|
||||
TangemPayCardNavigation.VaBankingDetailsError(userWalletId = userWalletId),
|
||||
)
|
||||
}
|
||||
|
||||
fun onVaBankingDetailsResolved(onramp: VirtualAccountOnramp) {
|
||||
val loaded = currentStatus.value.ifLoadedOrNull { it } ?: return
|
||||
openVirtualAccountDeposit(onramp, loaded)
|
||||
}
|
||||
|
||||
fun onContactSupportClicked() {
|
||||
analytics.send(Basic.ButtonSupport(source = AnalyticsParam.ScreensSources.TangemPay))
|
||||
val customerId = currentStatus.value.ifLoadedOrNull { it.customerId } ?: return
|
||||
modelScope.launch {
|
||||
sendFeedbackEmailUseCase.invoke(
|
||||
type = FeedbackEmailType.Visa.FeatureIsBeta(
|
||||
walletMetaInfo = WalletMetaInfo(userWalletId = userWalletId),
|
||||
customerId = customerId,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun onVirtualAccountOrderCreated() {
|
||||
analytics.send(TangemPayAnalyticsEvents.VaSuccessScreenActivation())
|
||||
bottomSheetNavigation.dismiss()
|
||||
|
|
|
|||
|
|
@ -67,7 +67,7 @@ import javax.inject.Inject
|
|||
@ModelScoped
|
||||
internal class TangemPayDetailsModel @Inject constructor(
|
||||
paramsContainer: ParamsContainer,
|
||||
paymentAccountStatusSupplier: PaymentAccountStatusSupplier,
|
||||
private val paymentAccountStatusSupplier: PaymentAccountStatusSupplier,
|
||||
override val dispatchers: CoroutineDispatcherProvider,
|
||||
private val analytics: AnalyticsEventHandler,
|
||||
private val router: Router,
|
||||
|
|
@ -313,7 +313,16 @@ internal class TangemPayDetailsModel @Inject constructor(
|
|||
|
||||
override fun onClickBankTransfer() {
|
||||
val loaded = currentStatus.value.ifLoadedOrNull { it } ?: return
|
||||
val onramp = loaded.virtualAccount ?: return
|
||||
when (val onramp = loaded.virtualAccount) {
|
||||
null -> return
|
||||
is VirtualAccountOnramp.BankCredentialsError -> showVaBankingDetailsError()
|
||||
is VirtualAccountOnramp.Available,
|
||||
VirtualAccountOnramp.Eligible,
|
||||
-> openVirtualAccountDeposit(onramp, loaded)
|
||||
}
|
||||
}
|
||||
|
||||
private fun openVirtualAccountDeposit(onramp: VirtualAccountOnramp, loaded: PaymentAccountStatusValue.Loaded) {
|
||||
analytics.send(TangemPayAnalyticsEvents.VaTopupButtonClicked())
|
||||
bottomSheetNavigation.dismiss()
|
||||
bottomSheetNavigation.activate(
|
||||
|
|
@ -325,6 +334,18 @@ internal class TangemPayDetailsModel @Inject constructor(
|
|||
)
|
||||
}
|
||||
|
||||
private fun showVaBankingDetailsError() {
|
||||
bottomSheetNavigation.dismiss()
|
||||
bottomSheetNavigation.activate(
|
||||
TangemPayDetailsNavigation.VaBankingDetailsError(userWalletId = userWalletId),
|
||||
)
|
||||
}
|
||||
|
||||
fun onVaBankingDetailsResolved(onramp: VirtualAccountOnramp) {
|
||||
val loaded = currentStatus.value.ifLoadedOrNull { it } ?: return
|
||||
openVirtualAccountDeposit(onramp, loaded)
|
||||
}
|
||||
|
||||
fun onVirtualAccountOrderCreated() {
|
||||
analytics.send(TangemPayAnalyticsEvents.VaSuccessScreenActivation())
|
||||
bottomSheetNavigation.dismiss()
|
||||
|
|
|
|||
|
|
@ -0,0 +1,63 @@
|
|||
package com.tangem.features.tangempay.model
|
||||
|
||||
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.domain.models.account.VirtualAccountOnramp
|
||||
import com.tangem.domain.pay.flow.PaymentAccountStatusFetcher
|
||||
import com.tangem.domain.pay.flow.PaymentAccountStatusSupplier
|
||||
import com.tangem.features.tangempay.components.TangemPayVaBankingDetailsErrorComponent
|
||||
import com.tangem.features.tangempay.entity.TangemPayVaBankingDetailsErrorUM
|
||||
import com.tangem.features.tangempay.utils.ifLoadedOrNull
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
@Stable
|
||||
@ModelScoped
|
||||
internal class TangemPayVaBankingDetailsErrorModel @Inject constructor(
|
||||
paramsContainer: ParamsContainer,
|
||||
override val dispatchers: CoroutineDispatcherProvider,
|
||||
private val paymentAccountStatusFetcher: PaymentAccountStatusFetcher,
|
||||
private val paymentAccountStatusSupplier: PaymentAccountStatusSupplier,
|
||||
) : Model() {
|
||||
|
||||
private val params = paramsContainer.require<TangemPayVaBankingDetailsErrorComponent.Params>()
|
||||
|
||||
val uiState: StateFlow<TangemPayVaBankingDetailsErrorUM>
|
||||
field = MutableStateFlow(
|
||||
TangemPayVaBankingDetailsErrorUM(
|
||||
isRetryLoading = false,
|
||||
onRetryClick = ::onRetryClick,
|
||||
onContactSupportClick = params.onContactSupport,
|
||||
onDismiss = ::onDismiss,
|
||||
),
|
||||
)
|
||||
|
||||
fun onDismiss() {
|
||||
params.onDismiss()
|
||||
}
|
||||
|
||||
private fun onRetryClick() {
|
||||
if (uiState.value.isRetryLoading) return
|
||||
uiState.update { it.copy(isRetryLoading = true) }
|
||||
modelScope.launch {
|
||||
paymentAccountStatusFetcher.invoke(params.userWalletId)
|
||||
val onramp = paymentAccountStatusSupplier.invoke(params.userWalletId)
|
||||
.first()
|
||||
.ifLoadedOrNull { it.virtualAccount }
|
||||
when (onramp) {
|
||||
is VirtualAccountOnramp.Available,
|
||||
VirtualAccountOnramp.Eligible,
|
||||
-> params.onResolved(onramp)
|
||||
// Still failing (BankCredentialsError) or unavailable — keep the sheet, clear the loader.
|
||||
else -> uiState.update { it.copy(isRetryLoading = false) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -81,6 +81,9 @@ internal class TangemPayVirtualAccountDepositModel @Inject constructor(
|
|||
analytics.send(TangemPayAnalyticsEvents.VaShowDetailsFirstTimeClicked())
|
||||
createVirtualAccountOrder()
|
||||
}
|
||||
// Error onramp is intercepted before this sheet opens (a dedicated error sheet is shown instead);
|
||||
// the branch only keeps the `when` exhaustive.
|
||||
VirtualAccountOnramp.BankCredentialsError -> onDismiss()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,171 @@
|
|||
package com.tangem.features.tangempay.ui
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
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.draw.clip
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheet
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetType
|
||||
import com.tangem.core.ui.ds.topbar.TangemTopBar
|
||||
import com.tangem.core.ui.ds.topbar.TangemTopBarType
|
||||
import com.tangem.core.ui.ds2.button.Close
|
||||
import com.tangem.core.ui.ds2.button.TangemButton
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreviewRedesign
|
||||
import com.tangem.core.ui.res.generated.icons.Icons
|
||||
import com.tangem.core.ui.res.generated.icons.ic_error_28
|
||||
import com.tangem.features.tangempay.details.impl.R
|
||||
import com.tangem.features.tangempay.entity.TangemPayVaBankingDetailsErrorUM
|
||||
|
||||
@Composable
|
||||
internal fun TangemPayVaBankingDetailsErrorBottomSheet(state: TangemPayVaBankingDetailsErrorUM) {
|
||||
TangemBottomSheet<TangemBottomSheetConfigContent.Empty>(
|
||||
config = TangemBottomSheetConfig(
|
||||
isShown = true,
|
||||
onDismissRequest = state.onDismiss,
|
||||
content = TangemBottomSheetConfigContent.Empty,
|
||||
),
|
||||
type = TangemBottomSheetType.Modal,
|
||||
containerColor = TangemTheme.colors3.bg.secondary,
|
||||
title = {
|
||||
TangemTopBar(
|
||||
type = TangemTopBarType.BottomSheet,
|
||||
title = null,
|
||||
endContent = { TangemButton.Close(onClick = state.onDismiss) },
|
||||
)
|
||||
},
|
||||
content = { _ -> Content(state) },
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Content(state: TangemPayVaBankingDetailsErrorUM, modifier: Modifier = Modifier) {
|
||||
Column(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.verticalScroll(rememberScrollState())
|
||||
.padding(horizontal = TangemTheme.dimens2.x4)
|
||||
.padding(bottom = TangemTheme.dimens2.x4),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
WarningIcon(modifier = Modifier.padding(top = TangemTheme.dimens2.x4))
|
||||
TitleText(
|
||||
text = resourceReference(R.string.tangempay_va_banking_details_error_title),
|
||||
modifier = Modifier.padding(top = TangemTheme.dimens2.x8),
|
||||
)
|
||||
SubtitleText(
|
||||
text = resourceReference(R.string.tangempay_va_banking_details_error_description),
|
||||
modifier = Modifier.padding(top = TangemTheme.dimens2.x2),
|
||||
)
|
||||
TangemButton(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(top = TangemTheme.dimens2.x8),
|
||||
text = resourceReference(R.string.common_contact_support),
|
||||
variant = TangemButton.Variant.Secondary,
|
||||
size = TangemButton.Size.X12,
|
||||
isEnabled = !state.isRetryLoading,
|
||||
onClick = state.onContactSupportClick,
|
||||
)
|
||||
TangemButton(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(top = TangemTheme.dimens2.x2),
|
||||
text = resourceReference(R.string.alert_button_try_again),
|
||||
variant = TangemButton.Variant.Primary,
|
||||
size = TangemButton.Size.X12,
|
||||
isLoading = state.isRetryLoading,
|
||||
isEnabled = !state.isRetryLoading,
|
||||
onClick = state.onRetryClick,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun WarningIcon(modifier: Modifier = Modifier) {
|
||||
Box(
|
||||
modifier = modifier
|
||||
.size(TangemTheme.dimens2.x20)
|
||||
.clip(CircleShape)
|
||||
.background(TangemTheme.colors3.bg.status.warningSubtle),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Icon(
|
||||
modifier = Modifier.size(TangemTheme.dimens2.x7),
|
||||
imageVector = Icons.ic_error_28,
|
||||
contentDescription = null,
|
||||
tint = TangemTheme.colors3.icon.status.warning,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TitleText(text: TextReference, modifier: Modifier = Modifier) {
|
||||
Text(
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
text = text.resolveReference(),
|
||||
style = TangemTheme.typography3.heading.small,
|
||||
color = TangemTheme.colors3.text.primary,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SubtitleText(text: TextReference, modifier: Modifier = Modifier) {
|
||||
Text(
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
text = text.resolveReference(),
|
||||
style = TangemTheme.typography3.subheading.medium,
|
||||
color = TangemTheme.colors3.text.secondary,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun TangemPayVaBankingDetailsErrorPreview(
|
||||
@PreviewParameter(VaBankingDetailsErrorPreviewProvider::class) state: TangemPayVaBankingDetailsErrorUM,
|
||||
) {
|
||||
TangemThemePreviewRedesign {
|
||||
Content(
|
||||
state = state,
|
||||
modifier = Modifier.background(TangemTheme.colors3.bg.secondary),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private class VaBankingDetailsErrorPreviewProvider :
|
||||
CollectionPreviewParameterProvider<TangemPayVaBankingDetailsErrorUM>(
|
||||
collection = listOf(
|
||||
TangemPayVaBankingDetailsErrorUM(
|
||||
isRetryLoading = false,
|
||||
onRetryClick = {},
|
||||
onContactSupportClick = {},
|
||||
onDismiss = {},
|
||||
),
|
||||
TangemPayVaBankingDetailsErrorUM(
|
||||
isRetryLoading = true,
|
||||
onRetryClick = {},
|
||||
onContactSupportClick = {},
|
||||
onDismiss = {},
|
||||
),
|
||||
),
|
||||
)
|
||||
|
|
@ -0,0 +1,132 @@
|
|||
package com.tangem.features.tangempay.model
|
||||
|
||||
import arrow.core.Either
|
||||
import arrow.core.right
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import com.tangem.core.decompose.model.MutableParamsContainer
|
||||
import com.tangem.domain.models.account.AccountStatus
|
||||
import com.tangem.domain.models.account.PaymentAccountStatusValue
|
||||
import com.tangem.domain.models.account.VirtualAccountOnramp
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.pay.flow.PaymentAccountStatusFetcher
|
||||
import com.tangem.domain.pay.flow.PaymentAccountStatusSupplier
|
||||
import com.tangem.features.tangempay.components.TangemPayVaBankingDetailsErrorComponent
|
||||
import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider
|
||||
import io.mockk.Called
|
||||
import io.mockk.clearMocks
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.coVerify
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import io.mockk.verify
|
||||
import kotlinx.coroutines.CompletableDeferred
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.test.StandardTestDispatcher
|
||||
import kotlinx.coroutines.test.TestScope
|
||||
import kotlinx.coroutines.test.advanceUntilIdle
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
internal class TangemPayVaBankingDetailsErrorModelTest {
|
||||
|
||||
private val userWalletId = UserWalletId("1234567890ABCDEF")
|
||||
|
||||
private val paymentAccountStatusFetcher: PaymentAccountStatusFetcher = mockk()
|
||||
private val paymentAccountStatusSupplier: PaymentAccountStatusSupplier = mockk()
|
||||
private val onDismiss: () -> Unit = mockk(relaxed = true)
|
||||
private val onContactSupport: () -> Unit = mockk(relaxed = true)
|
||||
private val onResolved: (VirtualAccountOnramp) -> Unit = mockk(relaxed = true)
|
||||
|
||||
@BeforeEach
|
||||
fun resetMocks() {
|
||||
clearMocks(paymentAccountStatusFetcher, paymentAccountStatusSupplier, onDismiss, onContactSupport, onResolved)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN refetch resolves to available WHEN retry THEN onResolved called`() = runTest {
|
||||
// Arrange
|
||||
val onramp = VirtualAccountOnramp.Available(productInstanceId = "pi_1", bankCredentials = mockk())
|
||||
coEvery { paymentAccountStatusFetcher.invoke(userWalletId) } returns Unit.right()
|
||||
stubSupplier(onramp)
|
||||
val model = createModel()
|
||||
|
||||
// Act
|
||||
model.uiState.value.onRetryClick()
|
||||
advanceUntilIdle()
|
||||
|
||||
// Assert
|
||||
verify(exactly = 1) { onResolved(onramp) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN refetch still fails WHEN retry THEN onResolved not called and loading reset`() = runTest {
|
||||
// Arrange
|
||||
coEvery { paymentAccountStatusFetcher.invoke(userWalletId) } returns Unit.right()
|
||||
stubSupplier(VirtualAccountOnramp.BankCredentialsError)
|
||||
val model = createModel()
|
||||
|
||||
// Act
|
||||
model.uiState.value.onRetryClick()
|
||||
advanceUntilIdle()
|
||||
|
||||
// Assert
|
||||
verify { onResolved wasNot Called }
|
||||
assertThat(model.uiState.value.isRetryLoading).isFalse()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN refetch in progress WHEN retry twice THEN fetch invoked once and loading shown`() = runTest {
|
||||
// Arrange
|
||||
val pending = CompletableDeferred<Either<Throwable, Unit>>()
|
||||
coEvery { paymentAccountStatusFetcher.invoke(userWalletId) } coAnswers { pending.await() }
|
||||
stubSupplier(VirtualAccountOnramp.BankCredentialsError)
|
||||
val model = createModel()
|
||||
|
||||
// Act
|
||||
model.uiState.value.onRetryClick() // starts loading, fetch suspends
|
||||
advanceUntilIdle()
|
||||
model.uiState.value.onRetryClick() // gated by isRetryLoading — must be ignored
|
||||
advanceUntilIdle()
|
||||
|
||||
// Assert
|
||||
assertThat(model.uiState.value.isRetryLoading).isTrue()
|
||||
coVerify(exactly = 1) { paymentAccountStatusFetcher.invoke(userWalletId) }
|
||||
|
||||
pending.complete(Unit.right()) // let the in-flight call finish cleanly
|
||||
advanceUntilIdle()
|
||||
}
|
||||
|
||||
private fun stubSupplier(onramp: VirtualAccountOnramp) {
|
||||
val loaded = mockk<PaymentAccountStatusValue.Loaded>()
|
||||
every { loaded.virtualAccount } returns onramp
|
||||
val status = mockk<AccountStatus.Payment>()
|
||||
every { status.value } returns loaded
|
||||
every { paymentAccountStatusSupplier.invoke(userWalletId) } returns flowOf(status)
|
||||
}
|
||||
|
||||
private fun TestScope.createModel() = TangemPayVaBankingDetailsErrorModel(
|
||||
paramsContainer = MutableParamsContainer(
|
||||
TangemPayVaBankingDetailsErrorComponent.Params(
|
||||
userWalletId = userWalletId,
|
||||
onDismiss = onDismiss,
|
||||
onContactSupport = onContactSupport,
|
||||
onResolved = onResolved,
|
||||
),
|
||||
),
|
||||
dispatchers = createTestingCoroutineDispatcherProvider(),
|
||||
paymentAccountStatusFetcher = paymentAccountStatusFetcher,
|
||||
paymentAccountStatusSupplier = paymentAccountStatusSupplier,
|
||||
)
|
||||
|
||||
private fun TestScope.createTestingCoroutineDispatcherProvider(): TestingCoroutineDispatcherProvider {
|
||||
val testDispatcher = StandardTestDispatcher(testScheduler)
|
||||
return TestingCoroutineDispatcherProvider(
|
||||
main = testDispatcher,
|
||||
mainImmediate = testDispatcher,
|
||||
io = testDispatcher,
|
||||
default = testDispatcher,
|
||||
single = testDispatcher,
|
||||
)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue