Updated on 2026-08-14
This commit is contained in:
parent
4d0cb6a243
commit
ebf28a282b
42 changed files with 1198 additions and 394 deletions
|
|
@ -71,9 +71,11 @@ interface TangemPayApi {
|
|||
@GET("v1/customer/balance")
|
||||
suspend fun getCardBalance(@Header("Authorization") authHeader: String): ApiResponse<CardBalanceResponse>
|
||||
|
||||
@POST("v1/customer/card/details")
|
||||
/** Card-scoped reveal. `{card_id}` = selected card id. */
|
||||
@POST("v1/customer/card/{card_id}/details")
|
||||
suspend fun revealCardDetails(
|
||||
@Header("Authorization") authHeader: String,
|
||||
@Path("card_id") cardId: String,
|
||||
@Body body: CardDetailsRequest,
|
||||
): ApiResponse<CardDetailsResponse>
|
||||
|
||||
|
|
|
|||
|
|
@ -75,7 +75,10 @@ internal class DefaultTangemPayCardDetailsRepository @Inject constructor(
|
|||
)
|
||||
}
|
||||
|
||||
override suspend fun revealCardDetails(userWalletId: UserWalletId): Either<UniversalError, TangemPayCardDetails> {
|
||||
override suspend fun revealCardDetails(
|
||||
userWalletId: UserWalletId,
|
||||
cardId: String,
|
||||
): Either<UniversalError, TangemPayCardDetails> {
|
||||
return catch(
|
||||
block = {
|
||||
val publicKeyBase64 = getPublicKeyBase64()
|
||||
|
|
@ -84,6 +87,7 @@ internal class DefaultTangemPayCardDetailsRepository @Inject constructor(
|
|||
requestHelper.performRequest(userWalletId = userWalletId) { authHeader ->
|
||||
tangemPayApi.revealCardDetails(
|
||||
authHeader = authHeader,
|
||||
cardId = cardId,
|
||||
body = CardDetailsRequest(sessionId = sessionId),
|
||||
)
|
||||
}.getOrNull()?.result,
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@ internal class MockAwareTangemPayCardDetailsRepository @Inject constructor(
|
|||
|
||||
override suspend fun revealCardDetails(
|
||||
userWalletId: UserWalletId,
|
||||
cardId: String,
|
||||
): Either<UniversalError, TangemPayCardDetails> {
|
||||
if (isMockMode) {
|
||||
return TangemPayCardDetails(
|
||||
|
|
@ -44,7 +45,7 @@ internal class MockAwareTangemPayCardDetailsRepository @Inject constructor(
|
|||
expirationMonth = MOCK_EXPIRATION_MONTH,
|
||||
).right()
|
||||
}
|
||||
return real.revealCardDetails(userWalletId)
|
||||
return real.revealCardDetails(userWalletId, cardId)
|
||||
}
|
||||
|
||||
override suspend fun getPin(userWalletId: UserWalletId, cardId: String): Either<UniversalError, String?> {
|
||||
|
|
|
|||
|
|
@ -15,7 +15,10 @@ interface TangemPayCardDetailsRepository {
|
|||
|
||||
suspend fun getCardBalance(userWalletId: UserWalletId): Either<UniversalError, TangemPayCardBalance>
|
||||
|
||||
suspend fun revealCardDetails(userWalletId: UserWalletId): Either<UniversalError, TangemPayCardDetails>
|
||||
suspend fun revealCardDetails(
|
||||
userWalletId: UserWalletId,
|
||||
cardId: String,
|
||||
): Either<UniversalError, TangemPayCardDetails>
|
||||
|
||||
suspend fun getPin(userWalletId: UserWalletId, cardId: String): Either<UniversalError, String?>
|
||||
|
||||
|
|
|
|||
|
|
@ -274,13 +274,13 @@ sealed class TangemPayAnalyticsEvents(
|
|||
event = "Visa Add Extra Card Clicked",
|
||||
)
|
||||
|
||||
class FakeDoorPopupDisplayed : TangemPayAnalyticsEvents(
|
||||
class IssueAdditionalCardPopupShown : TangemPayAnalyticsEvents(
|
||||
categoryName = "Visa Card Management",
|
||||
event = "Visa Fakedoor Popup Displayed",
|
||||
event = "Visa Extra Card Issuance Popup Displayed",
|
||||
)
|
||||
|
||||
class FakeDoorGotitClicked : TangemPayAnalyticsEvents(
|
||||
class IssueAdditionalCardConfirmed : TangemPayAnalyticsEvents(
|
||||
categoryName = "Visa Card Management",
|
||||
event = "Visa Fakedoor Gotit Clicked",
|
||||
event = "Visa Extra Card Issuance Confirmed",
|
||||
)
|
||||
}
|
||||
|
|
@ -65,9 +65,12 @@ internal class DefaultTangemPayDetailsContainerComponent @AssistedInject constru
|
|||
tokenReceiveComponentFactory = tokenReceiveComponentFactory,
|
||||
expressTransactionsComponentFactory = expressTransactionsComponentFactory,
|
||||
)
|
||||
TangemPayAccountDetailsInnerRoute.CardDetails -> tangemPayCardPageFactory.create(
|
||||
is TangemPayAccountDetailsInnerRoute.CardDetails -> tangemPayCardPageFactory.create(
|
||||
context = childByContext(componentContext = componentContext, router = innerRouter),
|
||||
params = TangemPayCardPageComponent.Params(initialStatus = params.initialStatus),
|
||||
params = TangemPayCardPageComponent.Params(
|
||||
initialStatus = params.initialStatus,
|
||||
cardId = config.cardId,
|
||||
),
|
||||
)
|
||||
TangemPayAccountDetailsInnerRoute.AddToWallet -> TangemPayAddToWalletComponent(
|
||||
appComponentContext = childByContext(componentContext = componentContext, router = innerRouter),
|
||||
|
|
|
|||
|
|
@ -2,51 +2,42 @@ package com.tangem.features.tangempay.components
|
|||
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
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.context.child
|
||||
import com.tangem.core.decompose.model.getOrCreateModel
|
||||
import com.tangem.core.ui.decompose.ComposableContentComponent
|
||||
import com.tangem.features.tangempay.components.cardDetails.DefaultTangemPayCardDetailsBlockComponent
|
||||
import com.tangem.features.tangempay.components.cardDetails.TangemPayCardDetailsBlockComponent
|
||||
import com.tangem.core.ui.res.LocalVisaRedesignEnabled
|
||||
import com.tangem.features.tangempay.model.TangemPayAddToWalletModel
|
||||
import com.tangem.features.tangempay.ui.TangemPayAddToWalletScreen
|
||||
import com.tangem.features.tangempay.ui.TangemPayAddToWalletScreenV2
|
||||
import com.tangem.features.tangempay.utils.userWalletId
|
||||
|
||||
internal class TangemPayAddToWalletComponent(
|
||||
private val appComponentContext: AppComponentContext,
|
||||
private val params: TangemPayDetailsContainerComponent.Params,
|
||||
) : AppComponentContext by appComponentContext, ComposableContentComponent {
|
||||
|
||||
private val model: TangemPayAddToWalletModel = getOrCreateModel()
|
||||
|
||||
private val cardDetailsBlockComponent = DefaultTangemPayCardDetailsBlockComponent(
|
||||
appComponentContext = child("cardDetailsBlockComponent"),
|
||||
params = TangemPayCardDetailsBlockComponent.Params(
|
||||
initialStatus = params.initialStatus,
|
||||
userWalletId = params.initialStatus.userWalletId,
|
||||
isEditingNameEnabled = false,
|
||||
shouldShowCardDetailsButtonOnCard = true,
|
||||
),
|
||||
)
|
||||
private val model: TangemPayAddToWalletModel = getOrCreateModel(params)
|
||||
|
||||
@Composable
|
||||
override fun Content(modifier: Modifier) {
|
||||
val state by model.uiState.collectAsStateWithLifecycle()
|
||||
val cardDetailsState by model.cardDetailsState.collectAsStateWithLifecycle()
|
||||
BackHandler(onBack = router::pop)
|
||||
CompositionLocalProvider(LocalVisaRedesignEnabled provides model.isRedesignEnabled()) {
|
||||
if (model.isRedesignEnabled()) {
|
||||
TangemPayAddToWalletScreenV2(
|
||||
state = state,
|
||||
cardDetailsBlockComponent = cardDetailsBlockComponent,
|
||||
cardDetailsState = cardDetailsState,
|
||||
)
|
||||
} else {
|
||||
TangemPayAddToWalletScreen(
|
||||
state = state,
|
||||
cardDetailsBlockComponent = cardDetailsBlockComponent,
|
||||
cardDetailsState = cardDetailsState,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -84,9 +84,12 @@ internal class TangemPayCardPageComponent @AssistedInject constructor(
|
|||
appComponentContext = childByContext(componentContext = componentContext, router = innerRouter),
|
||||
params = TangemPayDetailsContainerComponent.Params(initialStatus = params.initialStatus),
|
||||
)
|
||||
TangemPayCardDetailsInnerRoute.EditCardDisplayName -> TangemPayEditDisplayNameComponent(
|
||||
is TangemPayCardDetailsInnerRoute.EditCardDisplayName -> TangemPayEditDisplayNameComponent(
|
||||
appComponentContext = childByContext(componentContext = componentContext, router = innerRouter),
|
||||
params = TangemPayDetailsContainerComponent.Params(initialStatus = params.initialStatus),
|
||||
params = TangemPayCardScopedParams(
|
||||
initialStatus = params.initialStatus,
|
||||
cardId = config.cardId,
|
||||
),
|
||||
)
|
||||
TangemPayCardDetailsInnerRoute.LimitSetup -> TangemPayCardLimitSetupComponent(
|
||||
appComponentContext = childByContext(componentContext = componentContext, router = innerRouter),
|
||||
|
|
@ -106,7 +109,10 @@ internal class TangemPayCardPageComponent @AssistedInject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
data class Params(val initialStatus: AccountStatus.Payment)
|
||||
data class Params(
|
||||
val initialStatus: AccountStatus.Payment,
|
||||
val cardId: String,
|
||||
)
|
||||
|
||||
@AssistedFactory
|
||||
interface Factory : ComponentFactory<Params, TangemPayCardPageComponent> {
|
||||
|
|
|
|||
|
|
@ -10,7 +10,6 @@ import com.arkivanov.decompose.extensions.compose.subscribeAsState
|
|||
import com.arkivanov.decompose.router.slot.childSlot
|
||||
import com.arkivanov.decompose.router.slot.dismiss
|
||||
import com.tangem.core.decompose.context.AppComponentContext
|
||||
import com.tangem.core.decompose.context.child
|
||||
import com.tangem.core.decompose.context.childByContext
|
||||
import com.tangem.core.decompose.model.getOrCreateModel
|
||||
import com.tangem.core.ui.components.NavigationBar3ButtonsScrim
|
||||
|
|
@ -18,12 +17,9 @@ import com.tangem.core.ui.decompose.ComposableBottomSheetComponent
|
|||
import com.tangem.core.ui.decompose.ComposableContentComponent
|
||||
import com.tangem.core.ui.res.LocalVisaRedesignEnabled
|
||||
import com.tangem.features.tangempay.closure.TangemPayCloseCardComponent
|
||||
import com.tangem.features.tangempay.components.cardDetails.DefaultTangemPayCardDetailsBlockComponent
|
||||
import com.tangem.features.tangempay.components.cardDetails.TangemPayCardDetailsBlockComponent
|
||||
import com.tangem.features.tangempay.entity.TangemPayCardNavigation
|
||||
import com.tangem.features.tangempay.model.TangemPayCardPageModel
|
||||
import com.tangem.features.tangempay.ui.TangemPayCardPageScreen
|
||||
import com.tangem.features.tangempay.utils.firstCard
|
||||
import com.tangem.features.tangempay.utils.userWalletId
|
||||
import com.tangem.features.tokenreceive.TokenReceiveComponent
|
||||
|
||||
|
|
@ -35,16 +31,6 @@ internal class TangemPayCardPageScreenComponent(
|
|||
|
||||
private val model: TangemPayCardPageModel = getOrCreateModel(params = params)
|
||||
|
||||
private val cardDetailsBlockComponent = DefaultTangemPayCardDetailsBlockComponent(
|
||||
appComponentContext = child("cardDetailsBlockComponent"),
|
||||
params = TangemPayCardDetailsBlockComponent.Params(
|
||||
initialStatus = params.initialStatus,
|
||||
userWalletId = params.initialStatus.userWalletId,
|
||||
isEditingNameEnabled = true,
|
||||
shouldShowCardDetailsButtonOnCard = false,
|
||||
),
|
||||
)
|
||||
|
||||
private val bottomSheetSlot = childSlot(
|
||||
source = model.bottomSheetNavigation,
|
||||
serializer = TangemPayCardNavigation.serializer(),
|
||||
|
|
@ -55,15 +41,17 @@ internal class TangemPayCardPageScreenComponent(
|
|||
@Composable
|
||||
override fun Content(modifier: Modifier) {
|
||||
val state by model.uiState.collectAsStateWithLifecycle()
|
||||
val cardDetailsState by cardDetailsBlockComponent.state.collectAsStateWithLifecycle()
|
||||
val cardControllers by model.cardControllersState.collectAsStateWithLifecycle()
|
||||
val selectedCardId by model.selectedCardIdState.collectAsStateWithLifecycle()
|
||||
val bottomSheet by bottomSheetSlot.subscribeAsState()
|
||||
|
||||
CompositionLocalProvider(LocalVisaRedesignEnabled provides model.isRedesignEnabled()) {
|
||||
NavigationBar3ButtonsScrim()
|
||||
TangemPayCardPageScreen(
|
||||
state = state,
|
||||
cardDetailsBlockComponent = cardDetailsBlockComponent,
|
||||
cardDetailsState = cardDetailsState,
|
||||
cardControllers = cardControllers,
|
||||
selectedCardId = selectedCardId,
|
||||
onCardSelect = model::onCardPageSelected,
|
||||
modifier = modifier,
|
||||
)
|
||||
bottomSheet.child?.instance?.BottomSheet()
|
||||
|
|
@ -89,7 +77,7 @@ internal class TangemPayCardPageScreenComponent(
|
|||
params = TangemPayReissueCardComponent.Params(
|
||||
listener = model,
|
||||
userWalletId = params.initialStatus.userWalletId,
|
||||
cardId = params.initialStatus.firstCard().id,
|
||||
cardId = navigation.cardId,
|
||||
),
|
||||
)
|
||||
is TangemPayCardNavigation.CloseCard -> TangemPayCloseCardComponent(
|
||||
|
|
|
|||
|
|
@ -0,0 +1,12 @@
|
|||
package com.tangem.features.tangempay.components
|
||||
|
||||
import com.tangem.domain.models.account.AccountStatus
|
||||
|
||||
/**
|
||||
* Params for sub-screens that operate on one explicit card (e.g. rename) instead of implicitly
|
||||
* resolving the first card. Carries the [cardId] selected on the card page.
|
||||
*/
|
||||
internal data class TangemPayCardScopedParams(
|
||||
val initialStatus: AccountStatus.Payment,
|
||||
val cardId: String,
|
||||
)
|
||||
|
|
@ -128,6 +128,16 @@ internal class TangemPayDetailsComponent(
|
|||
listener = model,
|
||||
),
|
||||
)
|
||||
is TangemPayDetailsNavigation.IssueAdditionalCard -> TangemPayIssueAdditionalCardComponent(
|
||||
appComponentContext = context,
|
||||
params = TangemPayIssueAdditionalCardComponent.Params(
|
||||
userWalletId = navigation.walletId,
|
||||
feeAmount = navigation.feeAmount,
|
||||
feeCurrency = navigation.feeCurrency,
|
||||
fiatBalance = navigation.fiatBalance,
|
||||
listener = model,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -2,41 +2,29 @@ package com.tangem.features.tangempay.components
|
|||
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
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.context.child
|
||||
import com.tangem.core.decompose.model.getOrCreateModel
|
||||
import com.tangem.core.ui.decompose.ComposableContentComponent
|
||||
import com.tangem.features.tangempay.components.cardDetails.DefaultTangemPayCardDetailsBlockComponent
|
||||
import com.tangem.features.tangempay.components.cardDetails.TangemPayCardDetailsBlockComponent
|
||||
import com.tangem.core.ui.res.LocalVisaRedesignEnabled
|
||||
import com.tangem.features.tangempay.entity.DisplayNameState
|
||||
import com.tangem.features.tangempay.model.TangemPayEditDisplayNameModel
|
||||
import com.tangem.features.tangempay.ui.TangemPayEditDisplayNameScreen
|
||||
import com.tangem.features.tangempay.utils.userWalletId
|
||||
|
||||
internal class TangemPayEditDisplayNameComponent(
|
||||
private val appComponentContext: AppComponentContext,
|
||||
params: TangemPayDetailsContainerComponent.Params,
|
||||
params: TangemPayCardScopedParams,
|
||||
) : AppComponentContext by appComponentContext, ComposableContentComponent {
|
||||
|
||||
private val model: TangemPayEditDisplayNameModel = getOrCreateModel(params)
|
||||
|
||||
private val cardDetailsBlockComponent = DefaultTangemPayCardDetailsBlockComponent(
|
||||
appComponentContext = child("editDisplayNameCardDetails"),
|
||||
params = TangemPayCardDetailsBlockComponent.Params(
|
||||
initialStatus = params.initialStatus,
|
||||
userWalletId = params.initialStatus.userWalletId,
|
||||
isEditingNameEnabled = false,
|
||||
shouldShowCardDetailsButtonOnCard = false,
|
||||
),
|
||||
)
|
||||
|
||||
@Composable
|
||||
override fun Content(modifier: Modifier) {
|
||||
val state by model.uiState.collectAsStateWithLifecycle()
|
||||
val cardDetailsState by cardDetailsBlockComponent.state.collectAsStateWithLifecycle()
|
||||
val cardDetailsState by model.cardDetailsState.collectAsStateWithLifecycle()
|
||||
val editingCardDetailsState = cardDetailsState.copy(
|
||||
displayNameState = DisplayNameState.Editing(
|
||||
displayName = state.editingValue.text,
|
||||
|
|
@ -48,12 +36,13 @@ internal class TangemPayEditDisplayNameComponent(
|
|||
),
|
||||
)
|
||||
BackHandler(onBack = state.onDismiss)
|
||||
CompositionLocalProvider(LocalVisaRedesignEnabled provides model.isRedesignEnabled()) {
|
||||
TangemPayEditDisplayNameScreen(
|
||||
state = state,
|
||||
cardDetailsBlockComponent = cardDetailsBlockComponent,
|
||||
cardDetailsState = editingCardDetailsState,
|
||||
modifier = modifier,
|
||||
isRedesignEnabled = model.isRedesignEnabled(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
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.serialization.SerializedBigDecimal
|
||||
import com.tangem.domain.models.serialization.SerializedCurrency
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.features.tangempay.model.TangemPayIssueAdditionalCardModel
|
||||
import com.tangem.features.tangempay.ui.TangemPayIssueAdditionalCardContent
|
||||
|
||||
internal class TangemPayIssueAdditionalCardComponent(
|
||||
appComponentContext: AppComponentContext,
|
||||
params: Params,
|
||||
) : ComposableBottomSheetComponent, AppComponentContext by appComponentContext {
|
||||
|
||||
private val model: TangemPayIssueAdditionalCardModel = getOrCreateModel(params = params)
|
||||
|
||||
override fun dismiss() {
|
||||
model.onDismiss()
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun BottomSheet() {
|
||||
val state by model.uiState.collectAsStateWithLifecycle()
|
||||
TangemPayIssueAdditionalCardContent(state = state)
|
||||
}
|
||||
|
||||
data class Params(
|
||||
val userWalletId: UserWalletId,
|
||||
val feeAmount: SerializedBigDecimal,
|
||||
val feeCurrency: SerializedCurrency,
|
||||
val fiatBalance: SerializedBigDecimal,
|
||||
val listener: Listener,
|
||||
)
|
||||
|
||||
interface Listener {
|
||||
fun onIssueAdditionalCardDismissed()
|
||||
fun onIssueAdditionalCardSucceeded()
|
||||
fun onAddFundsForCardIssue()
|
||||
}
|
||||
}
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
package com.tangem.features.tangempay.components.cardDetails
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.ui.Modifier
|
||||
import com.tangem.core.decompose.context.AppComponentContext
|
||||
import com.tangem.core.decompose.model.getOrCreateModel
|
||||
import com.tangem.core.ui.res.LocalVisaRedesignEnabled
|
||||
import com.tangem.features.tangempay.entity.TangemPayCardDetailsUM
|
||||
import com.tangem.features.tangempay.model.TangemPayCardDetailsBlockModel
|
||||
import com.tangem.features.tangempay.ui.TangemPayCard
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
|
||||
internal class DefaultTangemPayCardDetailsBlockComponent(
|
||||
appComponentContext: AppComponentContext,
|
||||
params: TangemPayCardDetailsBlockComponent.Params,
|
||||
) : AppComponentContext by appComponentContext, TangemPayCardDetailsBlockComponent {
|
||||
|
||||
private val model: TangemPayCardDetailsBlockModel = getOrCreateModel(params = params)
|
||||
override val state: StateFlow<TangemPayCardDetailsUM> = model.uiState
|
||||
|
||||
@Composable
|
||||
override fun CardDetailsBlockContent(state: TangemPayCardDetailsUM, modifier: Modifier) {
|
||||
CompositionLocalProvider(LocalVisaRedesignEnabled provides model.isRedesignEnabled()) {
|
||||
TangemPayCard(state, modifier)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
package com.tangem.features.tangempay.components.cardDetails
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import com.tangem.features.tangempay.entity.TangemPayCardDetailsUM
|
||||
import com.tangem.features.tangempay.ui.TangemPayCard
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
|
||||
internal class PreviewTangemPayCardDetailsBlockComponent(
|
||||
state: TangemPayCardDetailsUM,
|
||||
) : TangemPayCardDetailsBlockComponent {
|
||||
|
||||
override val state: StateFlow<TangemPayCardDetailsUM> = MutableStateFlow(state)
|
||||
|
||||
@Composable
|
||||
override fun CardDetailsBlockContent(state: TangemPayCardDetailsUM, modifier: Modifier) {
|
||||
TangemPayCard(state, modifier)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
package com.tangem.features.tangempay.components.cardDetails
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.compose.ui.Modifier
|
||||
import com.tangem.domain.models.account.AccountStatus
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.features.tangempay.entity.TangemPayCardDetailsUM
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
|
||||
@Stable
|
||||
internal interface TangemPayCardDetailsBlockComponent {
|
||||
val state: StateFlow<TangemPayCardDetailsUM>
|
||||
|
||||
@Composable
|
||||
fun CardDetailsBlockContent(state: TangemPayCardDetailsUM, modifier: Modifier)
|
||||
|
||||
data class Params(
|
||||
val initialStatus: AccountStatus.Payment,
|
||||
val userWalletId: UserWalletId,
|
||||
val isEditingNameEnabled: Boolean,
|
||||
val shouldShowCardDetailsButtonOnCard: Boolean,
|
||||
)
|
||||
}
|
||||
|
|
@ -2,19 +2,9 @@ package com.tangem.features.tangempay.di
|
|||
|
||||
import com.tangem.core.decompose.di.ModelComponent
|
||||
import com.tangem.core.decompose.model.Model
|
||||
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.closure.TangemPayCloseCardModel
|
||||
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
|
||||
import com.tangem.features.tangempay.model.TangemPayTxHistoryDetailsModel
|
||||
import com.tangem.features.tangempay.model.TangemPayTxHistoryModel
|
||||
import com.tangem.features.tangempay.model.TangemPayReissueCardModel
|
||||
import com.tangem.features.tangempay.model.TangemPayViewPinModel
|
||||
import com.tangem.features.tangempay.model.*
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.hilt.InstallIn
|
||||
|
|
@ -45,11 +35,6 @@ internal interface TangemPayModelModule {
|
|||
@ClassKey(TangemPayChangePinModel::class)
|
||||
fun bindTangemPayChangePinModel(model: TangemPayChangePinModel): Model
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@ClassKey(TangemPayCardDetailsBlockModel::class)
|
||||
fun bindTangemPayCardDetailsBlockModel(model: TangemPayCardDetailsBlockModel): Model
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@ClassKey(TangemPayAddToWalletModel::class)
|
||||
|
|
@ -75,6 +60,11 @@ internal interface TangemPayModelModule {
|
|||
@ClassKey(TangemPayEditDisplayNameModel::class)
|
||||
fun bindTangemPayEditDisplayNameModel(model: TangemPayEditDisplayNameModel): Model
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@ClassKey(TangemPayIssueAdditionalCardModel::class)
|
||||
fun bindTangemPayIssueAdditionalCardModel(model: TangemPayIssueAdditionalCardModel): Model
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@ClassKey(TangemPayReissueCardModel::class)
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ import com.tangem.core.ui.extensions.resourceReference
|
|||
import com.tangem.domain.models.account.CardDisplayName
|
||||
import com.tangem.domain.models.pay.TangemPayCardFrozenState
|
||||
import com.tangem.features.tangempay.details.impl.R
|
||||
import com.tangem.features.tangempay.model.CardDataType
|
||||
import com.tangem.utils.StringsSigns
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ internal sealed class TangemPayCardNavigation {
|
|||
) : TangemPayCardNavigation()
|
||||
|
||||
@Serializable
|
||||
data object ReissueCard : TangemPayCardNavigation()
|
||||
data class ReissueCard(val cardId: String) : TangemPayCardNavigation()
|
||||
|
||||
@Serializable
|
||||
data class CloseCard(
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package com.tangem.features.tangempay.entity
|
|||
import com.tangem.domain.models.TokenReceiveConfig
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.serialization.SerializedBigDecimal
|
||||
import com.tangem.domain.models.serialization.SerializedCurrency
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.visa.model.TangemPayTxHistoryItem
|
||||
import kotlinx.serialization.Serializable
|
||||
|
|
@ -27,4 +28,12 @@ internal sealed class TangemPayDetailsNavigation {
|
|||
val transaction: TangemPayTxHistoryItem,
|
||||
val isBalanceHidden: Boolean,
|
||||
) : TangemPayDetailsNavigation()
|
||||
|
||||
@Serializable
|
||||
data class IssueAdditionalCard(
|
||||
val walletId: UserWalletId,
|
||||
val feeAmount: SerializedBigDecimal,
|
||||
val feeCurrency: SerializedCurrency,
|
||||
val fiatBalance: SerializedBigDecimal,
|
||||
) : TangemPayDetailsNavigation()
|
||||
}
|
||||
|
|
@ -28,6 +28,7 @@ internal class TangemPayDetailsStateFactory(
|
|||
private val intents: TangemPayDetailIntents,
|
||||
private val isRedesignEnabled: Boolean,
|
||||
private val isRemoveAccountEnabled: Boolean,
|
||||
private val isMultipleCardsEnabled: Boolean,
|
||||
) {
|
||||
fun getLoadingState(): TangemPayDetailsUM {
|
||||
return TangemPayDetailsUM(
|
||||
|
|
@ -79,17 +80,18 @@ internal class TangemPayDetailsStateFactory(
|
|||
(card == null || card.frozenState == TangemPayCardFrozenState.Unfrozen),
|
||||
),
|
||||
cardsBlockState = TangemPayDetailsBalanceBlockState.CardsBlockState(
|
||||
cards = card?.let {
|
||||
persistentListOf(
|
||||
cards = status.cards
|
||||
.let { if (isMultipleCardsEnabled) it else it.take(1) }
|
||||
.map { cardItem ->
|
||||
TangemPayDetailsBalanceBlockState.Card(
|
||||
lastDigits = card.lastDigits,
|
||||
onClick = intents::onCardClick,
|
||||
isReissuing = card.state != TangemPayCardState.Active,
|
||||
lastDigits = cardItem.lastDigits,
|
||||
onClick = { intents.onCardClick(cardItem.id) },
|
||||
isReissuing = cardItem.state != TangemPayCardState.Active,
|
||||
isEnabled = errorNotificationConfig == null,
|
||||
isFrozen = card.isFrozen,
|
||||
),
|
||||
isFrozen = cardItem.isFrozen,
|
||||
)
|
||||
} ?: persistentListOf(),
|
||||
}
|
||||
.toImmutableList(),
|
||||
onAddCardClick = intents::onAddCardClick,
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -7,9 +7,12 @@ import com.tangem.core.ui.components.containers.pullToRefresh.PullToRefreshConfi
|
|||
import com.tangem.core.ui.components.notifications.NotificationConfig
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.domain.models.pay.TangemPayCardFrozenState
|
||||
import com.tangem.features.tangempay.model.CardDataType
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
internal enum class CardDataType {
|
||||
Number, Expiry, CVV
|
||||
}
|
||||
|
||||
internal data class TangemPayDetailsUM(
|
||||
val topBarConfig: TangemPayDetailsTopBarConfig,
|
||||
val pullToRefreshConfig: PullToRefreshConfig,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,10 @@
|
|||
package com.tangem.features.tangempay.entity
|
||||
|
||||
internal data class TangemPayIssueAdditionalCardUM(
|
||||
val isBalanceInsufficient: Boolean,
|
||||
val feeText: String,
|
||||
val isLoading: Boolean,
|
||||
val onIssueClick: () -> Unit,
|
||||
val onAddFundsClick: () -> Unit,
|
||||
val onDismiss: () -> Unit,
|
||||
)
|
||||
|
|
@ -3,13 +3,19 @@ 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.core.decompose.navigation.Router
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.features.tangempay.TangemPayFeatureToggles
|
||||
import com.tangem.features.tangempay.components.TangemPayDetailsContainerComponent
|
||||
import com.tangem.features.tangempay.details.impl.R
|
||||
import com.tangem.features.tangempay.entity.TangemPayAddToWalletStepItemUM
|
||||
import com.tangem.features.tangempay.entity.TangemPayAddToWalletUM
|
||||
import com.tangem.features.tangempay.entity.TangemPayCardDetailsUM
|
||||
import com.tangem.features.tangempay.model.controller.TangemPayCardDetailsController
|
||||
import com.tangem.features.tangempay.utils.GoogleWalletUtil
|
||||
import com.tangem.features.tangempay.utils.firstCard
|
||||
import com.tangem.features.tangempay.utils.userWalletId
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
|
|
@ -19,12 +25,29 @@ import javax.inject.Inject
|
|||
@Stable
|
||||
@ModelScoped
|
||||
internal class TangemPayAddToWalletModel @Inject constructor(
|
||||
paramsContainer: ParamsContainer,
|
||||
override val dispatchers: CoroutineDispatcherProvider,
|
||||
private val router: Router,
|
||||
private val googleWalletUtil: GoogleWalletUtil,
|
||||
private val featureToggles: TangemPayFeatureToggles,
|
||||
cardDetailsControllerFactory: TangemPayCardDetailsController.Factory,
|
||||
) : Model() {
|
||||
|
||||
private val params: TangemPayDetailsContainerComponent.Params = paramsContainer.require()
|
||||
|
||||
private val cardDetailsController = cardDetailsControllerFactory.create(
|
||||
scope = modelScope,
|
||||
initialCard = params.initialStatus.firstCard(),
|
||||
userWalletId = params.initialStatus.userWalletId,
|
||||
config = TangemPayCardDetailsController.Config(
|
||||
isEditingNameEnabled = false,
|
||||
shouldShowCardDetailsButtonOnCard = true,
|
||||
),
|
||||
onEditNameClick = {},
|
||||
)
|
||||
|
||||
val cardDetailsState: StateFlow<TangemPayCardDetailsUM> = cardDetailsController.uiState
|
||||
|
||||
val uiState: StateFlow<TangemPayAddToWalletUM>
|
||||
field = MutableStateFlow(getInitialState())
|
||||
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ import com.tangem.domain.models.StatusSource
|
|||
import com.tangem.domain.models.TokenReceiveConfig
|
||||
import com.tangem.domain.models.account.AccountStatus
|
||||
import com.tangem.domain.models.account.PaymentAccountStatusValue
|
||||
import com.tangem.domain.models.account.findCardWithId
|
||||
import com.tangem.domain.models.pay.TangemPayCard
|
||||
import com.tangem.domain.models.pay.TangemPayCardLimitPeriod
|
||||
import com.tangem.domain.models.pay.TangemPayCardState
|
||||
|
|
@ -46,16 +47,23 @@ import com.tangem.features.tangempay.components.TangemPayCardPageComponent
|
|||
import com.tangem.features.tangempay.components.ViewPinListener
|
||||
import com.tangem.features.tangempay.details.impl.R
|
||||
import com.tangem.features.tangempay.entity.*
|
||||
import com.tangem.features.tangempay.model.controller.TangemPayCardDetailsController
|
||||
import com.tangem.features.tangempay.model.listener.CardDetailsEvent
|
||||
import com.tangem.features.tangempay.model.listener.CardDetailsEventListener
|
||||
import com.tangem.features.tangempay.navigation.TangemPayCardDetailsInnerRoute
|
||||
import com.tangem.features.tangempay.utils.*
|
||||
import com.tangem.features.tangempay.utils.TangemPayMessagesFactory
|
||||
import com.tangem.features.tangempay.utils.cryptoCurrency
|
||||
import com.tangem.features.tangempay.utils.ifLoadedOrNull
|
||||
import com.tangem.features.tangempay.utils.userWalletId
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import com.tangem.utils.coroutines.JobHolder
|
||||
import com.tangem.utils.coroutines.saveIn
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
|
@ -76,6 +84,7 @@ internal class TangemPayCardPageModel @Inject constructor(
|
|||
private val changeCardFrozenStateUseCase: ChangeCardFrozenStateUseCase,
|
||||
private val cardDetailsEventListener: CardDetailsEventListener,
|
||||
private val tangemPayFeatureToggles: TangemPayFeatureToggles,
|
||||
private val cardDetailsControllerFactory: TangemPayCardDetailsController.Factory,
|
||||
) : Model(), ViewPinListener, ReissueCardListener, AddFundsListener, CloseCardListener {
|
||||
|
||||
private val params: TangemPayCardPageComponent.Params = paramsContainer.require()
|
||||
|
|
@ -86,12 +95,27 @@ internal class TangemPayCardPageModel @Inject constructor(
|
|||
private val reloadLimitsJobHolder = JobHolder()
|
||||
|
||||
private val currentStatus = MutableStateFlow(params.initialStatus)
|
||||
private val initialCardId = params.initialStatus.firstCard().id
|
||||
private val userWalletId = currentStatus.value.userWalletId
|
||||
|
||||
/**
|
||||
* Currently focused card in the swipe pager. Per-card actions and the "Details" reveal target it.
|
||||
* Initialized to the card the user tapped on the account screen ([Params.cardId]); falls back to
|
||||
* the first available card if it is gone (handled in [syncCardControllers]).
|
||||
*/
|
||||
private val selectedCardId = MutableStateFlow(params.cardId)
|
||||
val selectedCardIdState: StateFlow<String> = selectedCardId
|
||||
|
||||
private val cardControllers = linkedMapOf<String, TangemPayCardDetailsController>()
|
||||
private val _cardControllersState: MutableStateFlow<ImmutableList<TangemPayCardDetailsController>> =
|
||||
MutableStateFlow(persistentListOf())
|
||||
val cardControllersState: StateFlow<ImmutableList<TangemPayCardDetailsController>> = _cardControllersState
|
||||
|
||||
private val cryptoCurrency
|
||||
get() = currentStatus.value.cryptoCurrency
|
||||
|
||||
// Multiple cards are temporarily gated behind the same toggle as close-card.
|
||||
private val isMultipleCardsEnabled: Boolean get() = tangemPayFeatureToggles.isCloseCardEnabled
|
||||
|
||||
val uiState: StateFlow<TangemPayCardPageUM>
|
||||
field = MutableStateFlow(
|
||||
TangemPayCardPageUM(
|
||||
|
|
@ -115,28 +139,24 @@ internal class TangemPayCardPageModel @Inject constructor(
|
|||
paymentAccountStatusSupplier.invoke(userWalletId)
|
||||
.onEach { state ->
|
||||
currentStatus.update { state }
|
||||
uiState.update { it.copy(dailyLimitState = buildDailyLimitState(state)) }
|
||||
|
||||
val status = state.value
|
||||
if (status is PaymentAccountStatusValue.Loaded && status.source == StatusSource.ACTUAL) {
|
||||
val card = state.findCard(initialCardId, params.initialStatus) ?: return@onEach
|
||||
uiState.update { uiState ->
|
||||
uiState.copy(
|
||||
settings = buildSettings(card),
|
||||
settingsV2 = buildSettingsV2(card),
|
||||
menuItems = buildMenuItems(isLastCard = status.cards.isLastCard()),
|
||||
cardState = card.state,
|
||||
)
|
||||
}
|
||||
}
|
||||
syncCardControllers(state)
|
||||
}
|
||||
.launchIn(modelScope)
|
||||
|
||||
combine(currentStatus, selectedCardId) { status, selectedId -> status to selectedId }
|
||||
.onEach { (status, selectedId) -> updateSelectedCardUi(status, selectedId) }
|
||||
.launchIn(modelScope)
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
cardDetailsEventListener.send(CardDetailsEvent.HideAll)
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
private fun buildDailyLimitState(state: AccountStatus.Payment): TangemPayDailyLimitBlockState {
|
||||
val status = state.value
|
||||
val card = if (status is PaymentAccountStatusValue.Loaded && status.source == StatusSource.ACTUAL) {
|
||||
state.findCard(initialCardId, params.initialStatus)
|
||||
status.findCardWithId(selectedCardId.value)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
|
@ -157,6 +177,73 @@ internal class TangemPayCardPageModel @Inject constructor(
|
|||
|
||||
fun isRedesignEnabled(): Boolean = tangemPayFeatureToggles.isRedesignEnabled
|
||||
|
||||
/** Reports the card the user swiped to so per-card UI and reveal target it. */
|
||||
fun onCardPageSelected(index: Int) {
|
||||
cardControllersState.value.getOrNull(index)?.let { selectedCardId.value = it.cardId }
|
||||
}
|
||||
|
||||
private fun childCardScope(): CoroutineScope =
|
||||
CoroutineScope(modelScope.coroutineContext + SupervisorJob(modelScope.coroutineContext[Job]))
|
||||
|
||||
private fun syncCardControllers(state: AccountStatus.Payment) {
|
||||
val status = state.value
|
||||
if (status !is PaymentAccountStatusValue.Loaded || status.source != StatusSource.ACTUAL) return
|
||||
|
||||
val cards = status.cards.let { if (isMultipleCardsEnabled) it else it.take(1) }
|
||||
val newIds = cards.mapTo(mutableSetOf()) { it.id }
|
||||
|
||||
cardControllers.keys.filterNot { it in newIds }.toList().forEach { removedId ->
|
||||
cardControllers.remove(removedId)?.let { controller ->
|
||||
controller.dispose()
|
||||
cardDetailsEventListener.send(CardDetailsEvent.Hide(removedId))
|
||||
}
|
||||
}
|
||||
|
||||
val ordered = LinkedHashMap<String, TangemPayCardDetailsController>(cards.size)
|
||||
cards.forEach { card ->
|
||||
ordered[card.id] = cardControllers[card.id] ?: cardDetailsControllerFactory.create(
|
||||
scope = childCardScope(),
|
||||
initialCard = card,
|
||||
userWalletId = userWalletId,
|
||||
config = TangemPayCardDetailsController.Config(
|
||||
isEditingNameEnabled = true,
|
||||
shouldShowCardDetailsButtonOnCard = false,
|
||||
),
|
||||
onEditNameClick = { router.push(TangemPayCardDetailsInnerRoute.EditCardDisplayName(card.id)) },
|
||||
)
|
||||
}
|
||||
cardControllers.clear()
|
||||
cardControllers.putAll(ordered)
|
||||
_cardControllersState.value = ordered.values.toList().toImmutableList()
|
||||
|
||||
if (selectedCardId.value !in newIds) {
|
||||
ordered.keys.firstOrNull()?.let { selectedCardId.value = it }
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateSelectedCardUi(state: AccountStatus.Payment, selectedId: String) {
|
||||
val status = state.value
|
||||
if (status is PaymentAccountStatusValue.Loaded && status.source == StatusSource.ACTUAL) {
|
||||
val card = status.findCardWithId(selectedId) ?: return
|
||||
uiState.update { uiState ->
|
||||
uiState.copy(
|
||||
dailyLimitState = buildDailyLimitState(state),
|
||||
settings = buildSettings(card),
|
||||
settingsV2 = buildSettingsV2(card),
|
||||
menuItems = buildMenuItems(isLastCard = status.cards.isLastCard()),
|
||||
cardState = card.state,
|
||||
)
|
||||
}
|
||||
} else {
|
||||
uiState.update { it.copy(dailyLimitState = buildDailyLimitState(state)) }
|
||||
}
|
||||
}
|
||||
|
||||
private fun selectedCard(): TangemPayCard? {
|
||||
val status = currentStatus.value.value
|
||||
return (status as? PaymentAccountStatusValue.Loaded)?.findCardWithId(selectedCardId.value)
|
||||
}
|
||||
|
||||
private fun buildSettings(card: TangemPayCard): ImmutableList<TangemPayCardPageSetting> {
|
||||
if (isRedesignEnabled()) return persistentListOf()
|
||||
return persistentListOf(
|
||||
|
|
@ -185,8 +272,9 @@ internal class TangemPayCardPageModel @Inject constructor(
|
|||
|
||||
private suspend fun subscribeOnDetailsState() {
|
||||
if (!isRedesignEnabled()) return
|
||||
cardDetailsEventListener.event.collect { event ->
|
||||
val isDetailsShown = event == CardDetailsEvent.Show
|
||||
combine(cardDetailsEventListener.event, selectedCardId) { event, selectedId ->
|
||||
event is CardDetailsEvent.Show && event.cardId == selectedId
|
||||
}.collect { isDetailsShown ->
|
||||
uiState.update { state ->
|
||||
state.copy(
|
||||
settingsV2 = state.settingsV2
|
||||
|
|
@ -277,9 +365,7 @@ internal class TangemPayCardPageModel @Inject constructor(
|
|||
}
|
||||
|
||||
private fun onClickViewDetails() {
|
||||
modelScope.launch(dispatchers.default) {
|
||||
cardDetailsEventListener.send(CardDetailsEvent.Show)
|
||||
}
|
||||
cardDetailsEventListener.send(CardDetailsEvent.Show(selectedCardId.value))
|
||||
}
|
||||
|
||||
private fun onClickReloadLimits() {
|
||||
|
|
@ -300,7 +386,7 @@ internal class TangemPayCardPageModel @Inject constructor(
|
|||
if (!isPinSet) {
|
||||
router.push(TangemPayCardDetailsInnerRoute.ChangePIN)
|
||||
} else {
|
||||
val card = currentStatus.value.findCard(initialCardId, params.initialStatus) ?: return
|
||||
val card = selectedCard() ?: return
|
||||
bottomSheetNavigation.activate(
|
||||
TangemPayCardNavigation.ViewPinCode(
|
||||
userWalletId = userWalletId,
|
||||
|
|
@ -329,7 +415,7 @@ internal class TangemPayCardPageModel @Inject constructor(
|
|||
|
||||
private fun onClickReissueCard() {
|
||||
analytics.send(TangemPayAnalyticsEvents.ReplaceCardClicked())
|
||||
bottomSheetNavigation.activate(TangemPayCardNavigation.ReissueCard)
|
||||
bottomSheetNavigation.activate(TangemPayCardNavigation.ReissueCard(cardId = selectedCardId.value))
|
||||
}
|
||||
|
||||
override fun onDismissReissueCard() {
|
||||
|
|
@ -342,7 +428,7 @@ internal class TangemPayCardPageModel @Inject constructor(
|
|||
|
||||
private fun onClickCloseCard() {
|
||||
analytics.send(TangemPayAnalyticsEvents.CloseCardClicked())
|
||||
val card = currentStatus.value.findCard(initialCardId, params.initialStatus) ?: return
|
||||
val card = selectedCard() ?: return
|
||||
bottomSheetNavigation.activate(
|
||||
TangemPayCardNavigation.CloseCard(
|
||||
userWalletId = userWalletId,
|
||||
|
|
@ -407,7 +493,7 @@ internal class TangemPayCardPageModel @Inject constructor(
|
|||
}
|
||||
|
||||
private fun freezeCard() {
|
||||
val card = currentStatus.value.findCard(initialCardId, params.initialStatus) ?: return
|
||||
val card = selectedCard() ?: return
|
||||
modelScope.launch {
|
||||
changeCardFrozenStateUseCase(
|
||||
userWalletId = userWalletId,
|
||||
|
|
@ -424,7 +510,7 @@ internal class TangemPayCardPageModel @Inject constructor(
|
|||
}
|
||||
|
||||
private fun unfreezeCard() {
|
||||
val card = currentStatus.value.findCard(initialCardId, params.initialStatus) ?: return
|
||||
val card = selectedCard() ?: return
|
||||
modelScope.launch {
|
||||
changeCardFrozenStateUseCase(
|
||||
userWalletId = userWalletId,
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ 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.TangemPayWithdrawRepository
|
||||
import com.tangem.domain.pay.usecase.GetCustomerOffersUseCase
|
||||
import com.tangem.domain.pay.usecase.ProduceTangemPayInitialDataUseCase
|
||||
import com.tangem.domain.tangempay.TangemPayAnalyticsEvents
|
||||
import com.tangem.domain.visa.model.TangemPayTxHistoryItem
|
||||
|
|
@ -39,6 +40,7 @@ import com.tangem.features.tangempay.TangemPayConstants
|
|||
import com.tangem.features.tangempay.TangemPayFeatureToggles
|
||||
import com.tangem.features.tangempay.components.AddFundsListener
|
||||
import com.tangem.features.tangempay.components.TangemPayDetailsContainerComponent
|
||||
import com.tangem.features.tangempay.components.TangemPayIssueAdditionalCardComponent
|
||||
import com.tangem.features.tangempay.details.impl.R
|
||||
import com.tangem.features.tangempay.entity.TangemPayDetailsErrorType
|
||||
import com.tangem.features.tangempay.entity.TangemPayDetailsNavigation
|
||||
|
|
@ -56,6 +58,7 @@ import com.tangem.utils.logging.TangemLogger
|
|||
import com.tangem.utils.transformer.update
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.launch
|
||||
import java.math.BigDecimal
|
||||
import javax.inject.Inject
|
||||
|
||||
@Suppress("LongParameterList", "LargeClass")
|
||||
|
|
@ -79,7 +82,12 @@ internal class TangemPayDetailsModel @Inject constructor(
|
|||
private val paymentAccountStatusFetcher: PaymentAccountStatusFetcher,
|
||||
private val produceTangemPayInitialDataUseCase: ProduceTangemPayInitialDataUseCase,
|
||||
private val onboardingRepository: OnboardingRepository,
|
||||
) : Model(), TangemPayTxHistoryUiActions, TangemPayDetailIntents, AddFundsListener {
|
||||
private val getCustomerOffers: GetCustomerOffersUseCase,
|
||||
) : Model(),
|
||||
TangemPayTxHistoryUiActions,
|
||||
TangemPayDetailIntents,
|
||||
AddFundsListener,
|
||||
TangemPayIssueAdditionalCardComponent.Listener {
|
||||
|
||||
private val params: TangemPayDetailsContainerComponent.Params = paramsContainer.require()
|
||||
|
||||
|
|
@ -91,12 +99,16 @@ internal class TangemPayDetailsModel @Inject constructor(
|
|||
val cryptoCurrency
|
||||
get() = currentStatus.value.cryptoCurrency
|
||||
|
||||
// Multiple cards are temporarily gated behind the same toggle as close-card.
|
||||
private val isMultipleCardsEnabled: Boolean get() = tangemPayFeatureToggles.isCloseCardEnabled
|
||||
|
||||
private val stateFactory = TangemPayDetailsStateFactory(
|
||||
onBack = router::pop,
|
||||
onOpenMenu = ::onOpenMenu,
|
||||
intents = this,
|
||||
isRedesignEnabled = isRedesignEnabled(),
|
||||
isRemoveAccountEnabled = tangemPayFeatureToggles.isRemoveAccountEnabled,
|
||||
isMultipleCardsEnabled = isMultipleCardsEnabled,
|
||||
)
|
||||
|
||||
val uiState: StateFlow<TangemPayDetailsUM>
|
||||
|
|
@ -349,20 +361,55 @@ internal class TangemPayDetailsModel @Inject constructor(
|
|||
urlOpener.openUrl(TangemPayConstants.TERMS_AND_LIMITS_LINK)
|
||||
}
|
||||
|
||||
override fun onCardClick() {
|
||||
override fun onCardClick(cardId: String) {
|
||||
analytics.send(TangemPayAnalyticsEvents.CardIconClicked())
|
||||
router.push(TangemPayAccountDetailsInnerRoute.CardDetails)
|
||||
router.push(TangemPayAccountDetailsInnerRoute.CardDetails(cardId = cardId))
|
||||
}
|
||||
|
||||
override fun onAddCardClick() {
|
||||
analytics.send(TangemPayAnalyticsEvents.AddExtraCardClicked())
|
||||
analytics.send(TangemPayAnalyticsEvents.FakeDoorPopupDisplayed())
|
||||
uiMessageSender.send(
|
||||
message = TangemPayMessagesFactory.createFutureFeature(
|
||||
onGotItClick = { analytics.send(TangemPayAnalyticsEvents.FakeDoorGotitClicked()) },
|
||||
if (!isMultipleCardsEnabled) {
|
||||
uiMessageSender.send(TangemPayMessagesFactory.createFutureFeature(onGotItClick = {}))
|
||||
return
|
||||
}
|
||||
val activeCardsCount = currentStatus.value.ifLoadedOrNull { loaded ->
|
||||
loaded.cards.count { it.cardStatus.isActive }
|
||||
} ?: 0
|
||||
if (activeCardsCount >= MAX_ACTIVE_CARDS) {
|
||||
uiMessageSender.send(TangemPayMessagesFactory.createMaximumCardsIssued(maxCards = MAX_ACTIVE_CARDS))
|
||||
return
|
||||
}
|
||||
modelScope.launch {
|
||||
val offer = getCustomerOffers.additionalCardOffer(userWalletId).getOrNull()
|
||||
if (offer == null) {
|
||||
uiMessageSender.send(message = TangemPayMessagesFactory.createGenericError())
|
||||
return@launch
|
||||
}
|
||||
analytics.send(TangemPayAnalyticsEvents.IssueAdditionalCardPopupShown())
|
||||
bottomSheetNavigation.activate(
|
||||
TangemPayDetailsNavigation.IssueAdditionalCard(
|
||||
walletId = userWalletId,
|
||||
feeAmount = offer.fee.amount,
|
||||
feeCurrency = offer.fee.currency,
|
||||
fiatBalance = currentStatus.value.balanceOrNull()?.fiatBalance?.availableBalance ?: BigDecimal.ZERO,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onIssueAdditionalCardDismissed() {
|
||||
bottomSheetNavigation.dismiss()
|
||||
}
|
||||
|
||||
override fun onIssueAdditionalCardSucceeded() {
|
||||
bottomSheetNavigation.dismiss()
|
||||
modelScope.launch { paymentAccountStatusFetcher.invoke(userWalletId) }
|
||||
}
|
||||
|
||||
override fun onAddFundsForCardIssue() {
|
||||
bottomSheetNavigation.dismiss()
|
||||
onClickAddFunds()
|
||||
}
|
||||
|
||||
override fun onRenewSession() {
|
||||
uiState.update(TangemPayRenewSessionTransformer(shouldShowProgress = true))
|
||||
|
|
@ -412,4 +459,8 @@ internal class TangemPayDetailsModel @Inject constructor(
|
|||
private fun showBottomSheetError(type: TangemPayDetailsErrorType) {
|
||||
uiMessageSender.send(message = TangemPayMessagesFactory.createErrorMessage(errorType = type))
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val MAX_ACTIVE_CARDS = 3
|
||||
}
|
||||
}
|
||||
|
|
@ -19,10 +19,12 @@ import com.tangem.domain.models.wallet.UserWalletId
|
|||
import com.tangem.domain.pay.flow.PaymentAccountStatusSupplier
|
||||
import com.tangem.domain.pay.usecase.UpdateTangemPayCardNameUseCase
|
||||
import com.tangem.features.tangempay.TangemPayFeatureToggles
|
||||
import com.tangem.features.tangempay.components.TangemPayDetailsContainerComponent
|
||||
import com.tangem.features.tangempay.components.TangemPayCardScopedParams
|
||||
import com.tangem.features.tangempay.details.impl.R
|
||||
import com.tangem.features.tangempay.entity.TangemPayCardDetailsUM
|
||||
import com.tangem.features.tangempay.entity.TangemPayEditDisplayNameUM
|
||||
import com.tangem.features.tangempay.utils.firstCard
|
||||
import com.tangem.features.tangempay.model.controller.TangemPayCardDetailsController
|
||||
import com.tangem.features.tangempay.utils.requireLoaded
|
||||
import com.tangem.features.tangempay.utils.userWalletId
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.flow.*
|
||||
|
|
@ -40,13 +42,27 @@ internal class TangemPayEditDisplayNameModel @Inject constructor(
|
|||
private val uiMessageSender: UiMessageSender,
|
||||
private val paymentAccountStatusSupplier: PaymentAccountStatusSupplier,
|
||||
private val featureToggles: TangemPayFeatureToggles,
|
||||
cardDetailsControllerFactory: TangemPayCardDetailsController.Factory,
|
||||
) : Model() {
|
||||
|
||||
private val params: TangemPayDetailsContainerComponent.Params = paramsContainer.require()
|
||||
private val params: TangemPayCardScopedParams = paramsContainer.require()
|
||||
|
||||
private val card = params.initialStatus.firstCard()
|
||||
private val card = params.initialStatus.requireLoaded().requireCardWithId(params.cardId)
|
||||
private val originalDisplayName = card.displayName?.value.orEmpty()
|
||||
|
||||
private val cardDetailsController = cardDetailsControllerFactory.create(
|
||||
scope = modelScope,
|
||||
initialCard = card,
|
||||
userWalletId = params.initialStatus.userWalletId,
|
||||
config = TangemPayCardDetailsController.Config(
|
||||
isEditingNameEnabled = false,
|
||||
shouldShowCardDetailsButtonOnCard = false,
|
||||
),
|
||||
onEditNameClick = {},
|
||||
)
|
||||
|
||||
val cardDetailsState: StateFlow<TangemPayCardDetailsUM> = cardDetailsController.uiState
|
||||
|
||||
val uiState: StateFlow<TangemPayEditDisplayNameUM>
|
||||
field = MutableStateFlow(
|
||||
TangemPayEditDisplayNameUM(
|
||||
|
|
|
|||
|
|
@ -0,0 +1,86 @@
|
|||
package com.tangem.features.tangempay.model
|
||||
|
||||
import androidx.compose.runtime.Stable
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
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.ui.UiMessageSender
|
||||
import com.tangem.core.ui.format.bigdecimal.fiat
|
||||
import com.tangem.core.ui.format.bigdecimal.format
|
||||
import com.tangem.domain.pay.usecase.IssueAdditionalCardUseCase
|
||||
import com.tangem.domain.tangempay.TangemPayAnalyticsEvents
|
||||
import com.tangem.domain.visa.error.VisaApiError
|
||||
import com.tangem.features.tangempay.components.TangemPayIssueAdditionalCardComponent
|
||||
import com.tangem.features.tangempay.entity.TangemPayIssueAdditionalCardUM
|
||||
import com.tangem.features.tangempay.utils.TangemPayMessagesFactory
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
@Stable
|
||||
@ModelScoped
|
||||
internal class TangemPayIssueAdditionalCardModel @Inject constructor(
|
||||
paramsContainer: ParamsContainer,
|
||||
override val dispatchers: CoroutineDispatcherProvider,
|
||||
private val issueAdditionalCard: IssueAdditionalCardUseCase,
|
||||
private val uiMessageSender: UiMessageSender,
|
||||
private val analytics: AnalyticsEventHandler,
|
||||
) : Model() {
|
||||
|
||||
private val params: TangemPayIssueAdditionalCardComponent.Params = paramsContainer.require()
|
||||
|
||||
val uiState: StateFlow<TangemPayIssueAdditionalCardUM>
|
||||
field = MutableStateFlow(buildInitialState())
|
||||
|
||||
private fun buildInitialState(): TangemPayIssueAdditionalCardUM {
|
||||
return TangemPayIssueAdditionalCardUM(
|
||||
isBalanceInsufficient = params.fiatBalance < params.feeAmount,
|
||||
feeText = formatFee(),
|
||||
isLoading = false,
|
||||
onIssueClick = ::onIssueClick,
|
||||
onAddFundsClick = { params.listener.onAddFundsForCardIssue() },
|
||||
onDismiss = ::onDismiss,
|
||||
)
|
||||
}
|
||||
|
||||
fun onDismiss() {
|
||||
params.listener.onIssueAdditionalCardDismissed()
|
||||
}
|
||||
|
||||
private fun onIssueClick() {
|
||||
analytics.send(TangemPayAnalyticsEvents.IssueAdditionalCardConfirmed())
|
||||
uiState.update { it.copy(isLoading = true) }
|
||||
modelScope.launch {
|
||||
issueAdditionalCard(userWalletId = params.userWalletId).fold(
|
||||
ifLeft = ::handleIssueError,
|
||||
ifRight = {
|
||||
uiState.update { uiState -> uiState.copy(isLoading = false) }
|
||||
params.listener.onIssueAdditionalCardSucceeded()
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleIssueError(error: VisaApiError) {
|
||||
when (error) {
|
||||
VisaApiError.CardIssueInsufficientBalance -> uiState.update { it.copy(isBalanceInsufficient = true) }
|
||||
else -> {
|
||||
uiMessageSender.send(message = TangemPayMessagesFactory.createGenericError())
|
||||
onDismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun formatFee(): String {
|
||||
return params.feeAmount.format {
|
||||
fiat(
|
||||
fiatCurrencyCode = params.feeCurrency.currencyCode,
|
||||
fiatCurrencySymbol = params.feeCurrency.symbol,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,25 +1,23 @@
|
|||
package com.tangem.features.tangempay.model
|
||||
package com.tangem.features.tangempay.model.controller
|
||||
|
||||
import androidx.compose.runtime.Stable
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
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.clipboard.ClipboardManager
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.message.SnackbarMessage
|
||||
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.pay.TangemPayCard
|
||||
import com.tangem.domain.models.pay.TangemPayCardFrozenState
|
||||
import com.tangem.domain.models.pay.TangemPayCardState
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.pay.flow.PaymentAccountStatusSupplier
|
||||
import com.tangem.domain.pay.repository.TangemPayCardDetailsRepository
|
||||
import com.tangem.domain.tangempay.TangemPayAnalyticsEvents
|
||||
import com.tangem.features.tangempay.TangemPayFeatureToggles
|
||||
import com.tangem.features.tangempay.components.cardDetails.TangemPayCardDetailsBlockComponent
|
||||
import com.tangem.features.tangempay.details.impl.R
|
||||
import com.tangem.features.tangempay.entity.CardDataType
|
||||
import com.tangem.features.tangempay.entity.TangemPayCardDetailsBlockStateFactory
|
||||
import com.tangem.features.tangempay.entity.TangemPayCardDetailsUM
|
||||
import com.tangem.features.tangempay.model.listener.CardDetailsEvent
|
||||
|
|
@ -28,51 +26,61 @@ import com.tangem.features.tangempay.model.transformers.DetailsHiddenStateTransf
|
|||
import com.tangem.features.tangempay.model.transformers.DetailsRevealProgressStateTransformer
|
||||
import com.tangem.features.tangempay.model.transformers.DetailsRevealedStateTransformer
|
||||
import com.tangem.features.tangempay.model.transformers.TangemPayCardDetailsUpdateNameTransformer
|
||||
import com.tangem.features.tangempay.navigation.TangemPayCardDetailsInnerRoute
|
||||
import com.tangem.features.tangempay.utils.findCard
|
||||
import com.tangem.features.tangempay.utils.firstCard
|
||||
import com.tangem.utils.StringsSigns
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import com.tangem.utils.coroutines.JobHolder
|
||||
import com.tangem.utils.coroutines.saveIn
|
||||
import com.tangem.utils.transformer.update
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
import com.tangem.utils.transformer.update as transformerUpdate
|
||||
|
||||
private const val SHOW_DETAILS_TIME = 30_000L
|
||||
|
||||
/**
|
||||
* Owns the UI state of a single TangemPay card-detail block (the flip card with reveal PAN/CVV,
|
||||
* freeze badge, display-name editing). Several controllers can be alive at once — e.g. one per card
|
||||
* in a swipe pager — so all logic is scoped to [cardId] and reveal/hide is coordinated through the
|
||||
* card-scoped [CardDetailsEventListener].
|
||||
*
|
||||
|
||||
* lifecycle is owned by the host model, which passes a child [scope] and calls [dispose] when the
|
||||
* card disappears.
|
||||
*/
|
||||
@Suppress("LongParameterList")
|
||||
@Stable
|
||||
@ModelScoped
|
||||
internal class TangemPayCardDetailsBlockModel @Inject constructor(
|
||||
paramsContainer: ParamsContainer,
|
||||
override val dispatchers: CoroutineDispatcherProvider,
|
||||
internal class TangemPayCardDetailsController @AssistedInject constructor(
|
||||
@Assisted private val scope: CoroutineScope,
|
||||
@Assisted private val initialCard: TangemPayCard,
|
||||
@Assisted private val userWalletId: UserWalletId,
|
||||
@Assisted private val config: Config,
|
||||
@Assisted private val onEditNameClick: () -> Unit,
|
||||
private val cardDetailsRepository: TangemPayCardDetailsRepository,
|
||||
private val clipboardManager: ClipboardManager,
|
||||
private val uiMessageSender: UiMessageSender,
|
||||
private val cardDetailsEventListener: CardDetailsEventListener,
|
||||
private val analytics: AnalyticsEventHandler,
|
||||
private val router: Router,
|
||||
private val paymentAccountStatusSupplier: PaymentAccountStatusSupplier,
|
||||
private val payFeatureToggles: TangemPayFeatureToggles,
|
||||
) : Model() {
|
||||
) {
|
||||
|
||||
val cardId: String = initialCard.id
|
||||
|
||||
private val params: TangemPayCardDetailsBlockComponent.Params = paramsContainer.require()
|
||||
private val initialCard = params.initialStatus.firstCard()
|
||||
private var frozenStateJob: Job? = null
|
||||
|
||||
private val stateFactory = TangemPayCardDetailsBlockStateFactory(
|
||||
cardNumberEnd = initialCard.lastDigits,
|
||||
displayName = initialCard.displayName,
|
||||
isEditingNameEnabled = params.isEditingNameEnabled,
|
||||
onEditNameClick = ::startEditingDisplayName,
|
||||
isEditingNameEnabled = config.isEditingNameEnabled,
|
||||
onEditNameClick = onEditNameClick,
|
||||
onReveal = ::requestReveal,
|
||||
onCopy = ::copyData,
|
||||
shouldShowCardDetailsButtonOnCard = params.shouldShowCardDetailsButtonOnCard,
|
||||
shouldShowCardDetailsButtonOnCard = config.shouldShowCardDetailsButtonOnCard,
|
||||
)
|
||||
|
||||
val uiState: StateFlow<TangemPayCardDetailsUM>
|
||||
|
|
@ -83,24 +91,27 @@ internal class TangemPayCardDetailsBlockModel @Inject constructor(
|
|||
|
||||
init {
|
||||
subscribeToCardChanges()
|
||||
modelScope.launch {
|
||||
cardDetailsEventListener.event.collectLatest { event ->
|
||||
scope.launch {
|
||||
cardDetailsEventListener.event.collect { event ->
|
||||
when (event) {
|
||||
CardDetailsEvent.Hide -> hideCardDetails()
|
||||
CardDetailsEvent.Show -> revealCardDetails()
|
||||
is CardDetailsEvent.Show -> if (event.cardId == cardId) revealCardDetails() else hideCardDetails()
|
||||
is CardDetailsEvent.Hide -> if (event.cardId == cardId) hideCardDetails()
|
||||
CardDetailsEvent.HideAll -> hideCardDetails()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun isRedesignEnabled(): Boolean = payFeatureToggles.isRedesignEnabled
|
||||
fun dispose() {
|
||||
scope.cancel()
|
||||
}
|
||||
|
||||
private fun subscribeToCardChanges() {
|
||||
paymentAccountStatusSupplier.invoke(params.userWalletId)
|
||||
paymentAccountStatusSupplier.invoke(userWalletId)
|
||||
.onEach { state ->
|
||||
val status = state.value
|
||||
if (status is PaymentAccountStatusValue.Loaded && status.source == StatusSource.ACTUAL) {
|
||||
val card = state.findCard(initialCard.id, params.initialStatus) ?: return@onEach
|
||||
val card = status.findCardWithId(cardId) ?: return@onEach
|
||||
if (card.state != TangemPayCardState.Active) {
|
||||
requestHide()
|
||||
}
|
||||
|
|
@ -115,7 +126,7 @@ internal class TangemPayCardDetailsBlockModel @Inject constructor(
|
|||
subscribeToCardFrozenState(card.id)
|
||||
}
|
||||
}
|
||||
.launchIn(modelScope)
|
||||
.launchIn(scope)
|
||||
}
|
||||
|
||||
private fun subscribeToCardFrozenState(cardId: String) {
|
||||
|
|
@ -126,24 +137,24 @@ internal class TangemPayCardDetailsBlockModel @Inject constructor(
|
|||
uiState.update { state -> state.copy(cardFrozenState = TangemPayCardFrozenState.Pending) }
|
||||
}
|
||||
}
|
||||
.launchIn(modelScope)
|
||||
.launchIn(scope)
|
||||
}
|
||||
|
||||
private fun requestReveal() {
|
||||
modelScope.launch { cardDetailsEventListener.send(CardDetailsEvent.Show) }
|
||||
cardDetailsEventListener.send(CardDetailsEvent.Show(cardId))
|
||||
}
|
||||
|
||||
private fun requestHide() {
|
||||
modelScope.launch { cardDetailsEventListener.send(CardDetailsEvent.Hide) }
|
||||
cardDetailsEventListener.send(CardDetailsEvent.Hide(cardId))
|
||||
}
|
||||
|
||||
private fun revealCardDetails() {
|
||||
analytics.send(TangemPayAnalyticsEvents.ViewCardDetailsClicked())
|
||||
modelScope.launch {
|
||||
scope.launch {
|
||||
uiState.transformerUpdate(
|
||||
transformer = DetailsRevealProgressStateTransformer(onClickHide = ::requestHide),
|
||||
)
|
||||
cardDetailsRepository.revealCardDetails(params.userWalletId)
|
||||
cardDetailsRepository.revealCardDetails(userWalletId, cardId)
|
||||
.onRight { cardDetails ->
|
||||
uiState.transformerUpdate(
|
||||
transformer = DetailsRevealedStateTransformer(
|
||||
|
|
@ -157,7 +168,7 @@ internal class TangemPayCardDetailsBlockModel @Inject constructor(
|
|||
uiState.transformerUpdate(
|
||||
transformer = DetailsHiddenStateTransformer(
|
||||
stateFactory = stateFactory,
|
||||
shouldShowCardDetailsButtonOnCard = params.shouldShowCardDetailsButtonOnCard,
|
||||
shouldShowCardDetailsButtonOnCard = config.shouldShowCardDetailsButtonOnCard,
|
||||
),
|
||||
)
|
||||
showError()
|
||||
|
|
@ -166,7 +177,7 @@ internal class TangemPayCardDetailsBlockModel @Inject constructor(
|
|||
}
|
||||
|
||||
private fun launchShowDetailsTimer() {
|
||||
modelScope.launch {
|
||||
scope.launch {
|
||||
delay(SHOW_DETAILS_TIME)
|
||||
requestHide()
|
||||
}.saveIn(showCardDetailsTimerJobHolder)
|
||||
|
|
@ -177,7 +188,7 @@ internal class TangemPayCardDetailsBlockModel @Inject constructor(
|
|||
uiState.transformerUpdate(
|
||||
transformer = DetailsHiddenStateTransformer(
|
||||
stateFactory = stateFactory,
|
||||
shouldShowCardDetailsButtonOnCard = params.shouldShowCardDetailsButtonOnCard,
|
||||
shouldShowCardDetailsButtonOnCard = config.shouldShowCardDetailsButtonOnCard,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
@ -188,10 +199,6 @@ internal class TangemPayCardDetailsBlockModel @Inject constructor(
|
|||
)
|
||||
}
|
||||
|
||||
private fun startEditingDisplayName() {
|
||||
router.push(TangemPayCardDetailsInnerRoute.EditCardDisplayName)
|
||||
}
|
||||
|
||||
private fun copyData(text: String, type: CardDataType) {
|
||||
val event = when (type) {
|
||||
CardDataType.Number -> TangemPayAnalyticsEvents.CopyCardNumberClicked()
|
||||
|
|
@ -201,8 +208,21 @@ internal class TangemPayCardDetailsBlockModel @Inject constructor(
|
|||
analytics.send(event)
|
||||
clipboardManager.setText(text = text.filterNot { it.isWhitespace() }, isSensitive = true)
|
||||
}
|
||||
}
|
||||
|
||||
internal enum class CardDataType {
|
||||
Number, Expiry, CVV
|
||||
/** Per-block configuration that does not depend on live card data. */
|
||||
data class Config(
|
||||
val isEditingNameEnabled: Boolean,
|
||||
val shouldShowCardDetailsButtonOnCard: Boolean,
|
||||
)
|
||||
|
||||
@AssistedFactory
|
||||
interface Factory {
|
||||
fun create(
|
||||
scope: CoroutineScope,
|
||||
initialCard: TangemPayCard,
|
||||
userWalletId: UserWalletId,
|
||||
config: Config,
|
||||
onEditNameClick: () -> Unit,
|
||||
): TangemPayCardDetailsController
|
||||
}
|
||||
}
|
||||
|
|
@ -3,15 +3,27 @@ package com.tangem.features.tangempay.model.listener
|
|||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
/**
|
||||
* Card details must be hidden after some UI actions from the external component
|
||||
* Coordinates reveal/hide of card details across card-detail blocks that may be shown simultaneously
|
||||
* (e.g. several cards in a pager, or the same card on the card page and the edit-name screen).
|
||||
*
|
||||
* Events are card-scoped so that one block does not flip the others; [CardDetailsEvent.HideAll] is
|
||||
* the only broadcast event.
|
||||
*/
|
||||
internal interface CardDetailsEventListener {
|
||||
|
||||
val event: Flow<CardDetailsEvent>
|
||||
|
||||
suspend fun send(event: CardDetailsEvent)
|
||||
fun send(event: CardDetailsEvent)
|
||||
}
|
||||
|
||||
internal enum class CardDetailsEvent {
|
||||
Hide, Show
|
||||
internal sealed interface CardDetailsEvent {
|
||||
|
||||
/** Reveal the details of [cardId]. Other blocks treat it as a hint to hide themselves. */
|
||||
data class Show(val cardId: String) : CardDetailsEvent
|
||||
|
||||
/** Hide the details of [cardId]. */
|
||||
data class Hide(val cardId: String) : CardDetailsEvent
|
||||
|
||||
/** Hide details of every block (e.g. leaving the card screen). */
|
||||
data object HideAll : CardDetailsEvent
|
||||
}
|
||||
|
|
@ -16,7 +16,9 @@ internal class DefaultCardDetailsEventListener @Inject constructor() : CardDetai
|
|||
)
|
||||
override val event: Flow<CardDetailsEvent> = _event
|
||||
|
||||
override suspend fun send(event: CardDetailsEvent) {
|
||||
_event.emit(event)
|
||||
// tryEmit never fails with replay=1 + DROP_OLDEST, so send can stay non-suspending and be called
|
||||
// from non-coroutine contexts (e.g. Model.onDestroy).
|
||||
override fun send(event: CardDetailsEvent) {
|
||||
_event.tryEmit(event)
|
||||
}
|
||||
}
|
||||
|
|
@ -9,7 +9,7 @@ internal sealed class TangemPayAccountDetailsInnerRoute : Route {
|
|||
data object AccountDetails : TangemPayAccountDetailsInnerRoute()
|
||||
|
||||
@Serializable
|
||||
data object CardDetails : TangemPayAccountDetailsInnerRoute()
|
||||
data class CardDetails(val cardId: String) : TangemPayAccountDetailsInnerRoute()
|
||||
|
||||
@Serializable
|
||||
data object AddToWallet : TangemPayAccountDetailsInnerRoute()
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ internal sealed class TangemPayCardDetailsInnerRoute : Route {
|
|||
data object AddToWallet : TangemPayCardDetailsInnerRoute()
|
||||
|
||||
@Serializable
|
||||
data object EditCardDisplayName : TangemPayCardDetailsInnerRoute()
|
||||
data class EditCardDisplayName(val cardId: String) : TangemPayCardDetailsInnerRoute()
|
||||
|
||||
@Serializable
|
||||
data object LimitSetup : TangemPayCardDetailsInnerRoute()
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@ import androidx.compose.foundation.lazy.rememberLazyListState
|
|||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
|
|
@ -17,7 +16,6 @@ import androidx.compose.ui.platform.LocalDensity
|
|||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.tangem.core.ui.components.PrimaryButton
|
||||
import com.tangem.core.ui.components.SecondaryButton
|
||||
import com.tangem.core.ui.components.appbar.AppBarWithBackButton
|
||||
|
|
@ -29,8 +27,6 @@ import com.tangem.core.ui.res.TangemColorPalette
|
|||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.domain.models.pay.TangemPayCardFrozenState
|
||||
import com.tangem.features.tangempay.components.cardDetails.PreviewTangemPayCardDetailsBlockComponent
|
||||
import com.tangem.features.tangempay.components.cardDetails.TangemPayCardDetailsBlockComponent
|
||||
import com.tangem.features.tangempay.details.impl.R
|
||||
import com.tangem.features.tangempay.entity.TangemPayAddToWalletStepItemUM
|
||||
import com.tangem.features.tangempay.entity.TangemPayAddToWalletUM
|
||||
|
|
@ -40,12 +36,11 @@ import kotlinx.collections.immutable.persistentListOf
|
|||
@Composable
|
||||
internal fun TangemPayAddToWalletScreen(
|
||||
state: TangemPayAddToWalletUM,
|
||||
cardDetailsBlockComponent: TangemPayCardDetailsBlockComponent,
|
||||
cardDetailsState: TangemPayCardDetailsUM,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val listState = rememberLazyListState()
|
||||
val bottomBarHeight = with(LocalDensity.current) { WindowInsets.systemBars.getBottom(this).toDp() }
|
||||
val cardDetailsState by cardDetailsBlockComponent.state.collectAsStateWithLifecycle()
|
||||
|
||||
Column(
|
||||
modifier = modifier
|
||||
|
|
@ -66,9 +61,9 @@ internal fun TangemPayAddToWalletScreen(
|
|||
contentPadding = PaddingValues(bottom = TangemTheme.dimens.spacing16 + bottomBarHeight),
|
||||
) {
|
||||
item(TangemPayCardDetailsUM::class.java) {
|
||||
cardDetailsBlockComponent.CardDetailsBlockContent(
|
||||
modifier = Modifier.padding(horizontal = 16.dp).padding(top = 8.dp),
|
||||
TangemPayCard(
|
||||
state = cardDetailsState,
|
||||
modifier = Modifier.padding(horizontal = 16.dp).padding(top = 8.dp),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -181,8 +176,7 @@ private fun PreviewTangemPayAddToWalletScreen() {
|
|||
onBackClick = {},
|
||||
onClickOpenWallet = {},
|
||||
),
|
||||
cardDetailsBlockComponent = PreviewTangemPayCardDetailsBlockComponent(
|
||||
TangemPayCardDetailsUM(
|
||||
cardDetailsState = TangemPayCardDetailsUM(
|
||||
number = "",
|
||||
numberShort = "*1245",
|
||||
expiry = "••/••",
|
||||
|
|
@ -193,7 +187,6 @@ private fun PreviewTangemPayAddToWalletScreen() {
|
|||
cardFrozenState = TangemPayCardFrozenState.Unfrozen,
|
||||
displayNameState = null,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -9,12 +9,10 @@ import androidx.compose.foundation.shape.CircleShape
|
|||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.tangem.core.ui.ds.image.TangemIconUM
|
||||
import com.tangem.core.ui.ds.topbar.TangemTopBar
|
||||
import com.tangem.core.ui.ds2.button.TangemButton
|
||||
|
|
@ -27,8 +25,6 @@ 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_cross_20
|
||||
import com.tangem.domain.models.pay.TangemPayCardFrozenState
|
||||
import com.tangem.features.tangempay.components.cardDetails.PreviewTangemPayCardDetailsBlockComponent
|
||||
import com.tangem.features.tangempay.components.cardDetails.TangemPayCardDetailsBlockComponent
|
||||
import com.tangem.features.tangempay.details.impl.R
|
||||
import com.tangem.features.tangempay.entity.TangemPayAddToWalletStepItemUM
|
||||
import com.tangem.features.tangempay.entity.TangemPayAddToWalletUM
|
||||
|
|
@ -39,11 +35,10 @@ import kotlinx.collections.immutable.persistentListOf
|
|||
@Composable
|
||||
internal fun TangemPayAddToWalletScreenV2(
|
||||
state: TangemPayAddToWalletUM,
|
||||
cardDetailsBlockComponent: TangemPayCardDetailsBlockComponent,
|
||||
cardDetailsState: TangemPayCardDetailsUM,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val scrollState = rememberScrollState()
|
||||
val cardDetailsState by cardDetailsBlockComponent.state.collectAsStateWithLifecycle()
|
||||
|
||||
Column(
|
||||
modifier = modifier
|
||||
|
|
@ -60,11 +55,11 @@ internal fun TangemPayAddToWalletScreenV2(
|
|||
.verticalScroll(scrollState)
|
||||
.padding(top = 8.dp, bottom = 12.dp),
|
||||
) {
|
||||
cardDetailsBlockComponent.CardDetailsBlockContent(
|
||||
TangemPayCard(
|
||||
state = cardDetailsState,
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 16.dp)
|
||||
.padding(bottom = 12.dp),
|
||||
state = cardDetailsState,
|
||||
)
|
||||
DynamicSpacer(scrollState = scrollState)
|
||||
AddToWalletTitle()
|
||||
|
|
@ -215,8 +210,7 @@ private fun PreviewTangemPayAddToWalletScreen() {
|
|||
onBackClick = {},
|
||||
onClickOpenWallet = {},
|
||||
),
|
||||
cardDetailsBlockComponent = PreviewTangemPayCardDetailsBlockComponent(
|
||||
TangemPayCardDetailsUM(
|
||||
cardDetailsState = TangemPayCardDetailsUM(
|
||||
number = "",
|
||||
numberShort = "*1245",
|
||||
expiry = "••/••",
|
||||
|
|
@ -227,7 +221,6 @@ private fun PreviewTangemPayAddToWalletScreen() {
|
|||
cardFrozenState = TangemPayCardFrozenState.Unfrozen,
|
||||
displayNameState = null,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -57,9 +57,9 @@ import com.tangem.core.ui.res.*
|
|||
import com.tangem.core.ui.test.TangemPayTestTags
|
||||
import com.tangem.domain.models.pay.TangemPayCardFrozenState
|
||||
import com.tangem.features.tangempay.details.impl.R
|
||||
import com.tangem.features.tangempay.entity.CardDataType
|
||||
import com.tangem.features.tangempay.entity.DisplayNameState
|
||||
import com.tangem.features.tangempay.entity.TangemPayCardDetailsUM
|
||||
import com.tangem.features.tangempay.model.CardDataType
|
||||
|
||||
private const val TEXT_WIDTH_PADDING = 2
|
||||
private const val FREEZE_ANIMATION_DURATION_MS = 600
|
||||
|
|
|
|||
|
|
@ -12,6 +12,8 @@ import androidx.compose.foundation.layout.*
|
|||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.LazyItemScope
|
||||
import androidx.compose.foundation.lazy.LazyListScope
|
||||
import androidx.compose.foundation.pager.HorizontalPager
|
||||
import androidx.compose.foundation.pager.rememberPagerState
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.ScaffoldDefaults
|
||||
import androidx.compose.material3.Text
|
||||
|
|
@ -24,7 +26,9 @@ import androidx.compose.ui.platform.testTag
|
|||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.util.fastForEach
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.tangem.core.ui.components.appbar.AppBarWithBackButton
|
||||
import com.tangem.core.ui.ds.TangemPagerIndicator
|
||||
import com.tangem.core.ui.ds.image.TangemIconUM
|
||||
import com.tangem.core.ui.ds.topbar.TangemTopBar
|
||||
import com.tangem.core.ui.ds2.button.TangemButton
|
||||
|
|
@ -34,12 +38,12 @@ import com.tangem.core.ui.extensions.stringResourceSafe
|
|||
import com.tangem.core.ui.res.*
|
||||
import com.tangem.domain.models.pay.TangemPayCardFrozenState
|
||||
import com.tangem.domain.models.pay.TangemPayCardState
|
||||
import com.tangem.features.tangempay.components.cardDetails.PreviewTangemPayCardDetailsBlockComponent
|
||||
import com.tangem.features.tangempay.components.cardDetails.TangemPayCardDetailsBlockComponent
|
||||
import com.tangem.features.tangempay.details.impl.R
|
||||
import com.tangem.features.tangempay.entity.*
|
||||
import com.tangem.features.tangempay.model.controller.TangemPayCardDetailsController
|
||||
import com.tangem.features.tangempay.ui.components.PayContextMenuBlock
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import com.tangem.core.ui.R as CoreUiR
|
||||
|
||||
private const val CONTENT_FADE_DURATION_MS = 300
|
||||
|
|
@ -47,9 +51,29 @@ private const val CONTENT_FADE_DURATION_MS = 300
|
|||
@Composable
|
||||
internal fun TangemPayCardPageScreen(
|
||||
state: TangemPayCardPageUM,
|
||||
cardDetailsBlockComponent: TangemPayCardDetailsBlockComponent,
|
||||
cardDetailsState: TangemPayCardDetailsUM,
|
||||
cardControllers: ImmutableList<TangemPayCardDetailsController>,
|
||||
selectedCardId: String,
|
||||
onCardSelect: (Int) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
TangemPayCardPageScreen(
|
||||
state = state,
|
||||
cardSection = {
|
||||
TangemPayCardSwipePager(
|
||||
controllers = cardControllers,
|
||||
selectedCardId = selectedCardId,
|
||||
onCardSelect = onCardSelect,
|
||||
)
|
||||
},
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TangemPayCardPageScreen(
|
||||
state: TangemPayCardPageUM,
|
||||
modifier: Modifier = Modifier,
|
||||
cardSection: @Composable () -> Unit,
|
||||
) {
|
||||
val isRedesignEnabled = LocalVisaRedesignEnabled.current
|
||||
Scaffold(
|
||||
|
|
@ -80,10 +104,9 @@ internal fun TangemPayCardPageScreen(
|
|||
verticalArrangement = Arrangement.spacedBy(if (isRedesignEnabled) 0.dp else TangemTheme.dimens.spacing16),
|
||||
) {
|
||||
item(key = "Card") {
|
||||
cardDetailsBlockComponent.CardDetailsBlockContent(
|
||||
modifier = Modifier.padding(top = TangemTheme.dimens.spacing8),
|
||||
state = cardDetailsState,
|
||||
)
|
||||
Box(modifier = Modifier.padding(top = TangemTheme.dimens.spacing8)) {
|
||||
cardSection()
|
||||
}
|
||||
}
|
||||
if (isRedesignEnabled && state.settingsV2.isNotEmpty() && state.cardState == TangemPayCardState.Active) {
|
||||
cardPageItem("Settings buttons") {
|
||||
|
|
@ -98,6 +121,63 @@ internal fun TangemPayCardPageScreen(
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the card visual(s). With several cards they are wrapped in a [HorizontalPager] so the user
|
||||
* can swipe to change the management context; the neighbouring cards peek at the screen edges and a
|
||||
* dots indicator below shows the position. Each page collects its own controller's state so only the
|
||||
* changed page recomposes. On settle the screen reports the new page via [onCardSelect].
|
||||
*/
|
||||
@Composable
|
||||
private fun TangemPayCardSwipePager(
|
||||
controllers: ImmutableList<TangemPayCardDetailsController>,
|
||||
selectedCardId: String,
|
||||
onCardSelect: (Int) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
when {
|
||||
controllers.isEmpty() -> Unit
|
||||
controllers.size == 1 -> CardDetailsPage(controller = controllers.first(), modifier = modifier)
|
||||
else -> {
|
||||
val initialPage = controllers.indexOfFirst { it.cardId == selectedCardId }.coerceAtLeast(0)
|
||||
val pagerState = rememberPagerState(initialPage = initialPage) { controllers.size }
|
||||
|
||||
LaunchedEffect(pagerState, controllers) {
|
||||
snapshotFlow { pagerState.settledPage }
|
||||
.distinctUntilChanged()
|
||||
.collect(onCardSelect)
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = modifier,
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
HorizontalPager(
|
||||
state = pagerState,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
// Side padding keeps the current card centered while the neighbours peek at the edges.
|
||||
contentPadding = PaddingValues(horizontal = TangemTheme.dimens.spacing32),
|
||||
pageSpacing = TangemTheme.dimens.spacing8,
|
||||
beyondViewportPageCount = 1,
|
||||
key = { controllers[it].cardId },
|
||||
) { page ->
|
||||
CardDetailsPage(controller = controllers[page])
|
||||
}
|
||||
|
||||
TangemPagerIndicator(
|
||||
pagerState = pagerState,
|
||||
modifier = Modifier.padding(top = TangemTheme.dimens.spacing12),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun CardDetailsPage(controller: TangemPayCardDetailsController, modifier: Modifier = Modifier) {
|
||||
val cardDetailsState by controller.uiState.collectAsStateWithLifecycle()
|
||||
TangemPayCard(state = cardDetailsState, modifier = modifier)
|
||||
}
|
||||
|
||||
private fun LazyListScope.cardState(state: TangemPayCardPageUM) {
|
||||
when (state.cardState) {
|
||||
TangemPayCardState.Active -> {
|
||||
|
|
@ -254,23 +334,12 @@ private fun TangemPayCardPageScreenPreviewV1() {
|
|||
TangemThemePreview {
|
||||
TangemPayCardPageScreen(
|
||||
state = TangemPayCardPageUM.stub(),
|
||||
cardDetailsBlockComponent = PreviewTangemPayCardDetailsBlockComponent(
|
||||
TangemPayCardDetailsUM(
|
||||
number = "•••• •••• •••• 1245",
|
||||
numberShort = "··1245",
|
||||
expiry = "••/••",
|
||||
cvv = "•••",
|
||||
onCopy = { _, _ -> },
|
||||
onClick = {},
|
||||
cardFrozenState = TangemPayCardFrozenState.Unfrozen,
|
||||
displayNameState = DisplayNameState.Display(
|
||||
displayName = "Tangem Pay Card",
|
||||
onClick = {},
|
||||
isEditingEnabled = true,
|
||||
),
|
||||
),
|
||||
),
|
||||
cardDetailsState = TangemPayCardDetailsUM(
|
||||
cardSection = { TangemPayCard(state = previewCardDetailsState()) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun previewCardDetailsState(): TangemPayCardDetailsUM = TangemPayCardDetailsUM(
|
||||
number = "•••• •••• •••• 1245",
|
||||
numberShort = "··1245",
|
||||
expiry = "••/••",
|
||||
|
|
@ -283,10 +352,7 @@ private fun TangemPayCardPageScreenPreviewV1() {
|
|||
onClick = {},
|
||||
isEditingEnabled = false,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
@Preview
|
||||
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
|
|
@ -299,36 +365,7 @@ private fun TangemPayCardPageScreenPreviewV2() {
|
|||
) {
|
||||
TangemPayCardPageScreen(
|
||||
state = TangemPayCardPageUM.stub(),
|
||||
cardDetailsBlockComponent = PreviewTangemPayCardDetailsBlockComponent(
|
||||
TangemPayCardDetailsUM(
|
||||
number = "•••• •••• •••• 1245",
|
||||
numberShort = "··1245",
|
||||
expiry = "••/••",
|
||||
cvv = "•••",
|
||||
onCopy = { _, _ -> },
|
||||
onClick = {},
|
||||
cardFrozenState = TangemPayCardFrozenState.Unfrozen,
|
||||
displayNameState = DisplayNameState.Display(
|
||||
displayName = "Tangem Pay Card",
|
||||
onClick = {},
|
||||
isEditingEnabled = true,
|
||||
),
|
||||
),
|
||||
),
|
||||
cardDetailsState = TangemPayCardDetailsUM(
|
||||
number = "•••• •••• •••• 1245",
|
||||
numberShort = "··1245",
|
||||
expiry = "••/••",
|
||||
cvv = "•••",
|
||||
onCopy = { _, _ -> },
|
||||
onClick = {},
|
||||
cardFrozenState = TangemPayCardFrozenState.Unfrozen,
|
||||
displayNameState = DisplayNameState.Display(
|
||||
displayName = "Tangem Pay Card",
|
||||
onClick = {},
|
||||
isEditingEnabled = false,
|
||||
),
|
||||
),
|
||||
cardSection = { TangemPayCard(state = previewCardDetailsState()) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,7 +19,6 @@ import com.tangem.core.ui.extensions.stringResourceSafe
|
|||
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_cross_20
|
||||
import com.tangem.features.tangempay.components.cardDetails.TangemPayCardDetailsBlockComponent
|
||||
import com.tangem.features.tangempay.details.impl.R
|
||||
import com.tangem.features.tangempay.entity.TangemPayCardDetailsUM
|
||||
import com.tangem.features.tangempay.entity.TangemPayEditDisplayNameUM
|
||||
|
|
@ -28,21 +27,18 @@ import com.tangem.features.tangempay.entity.TangemPayEditDisplayNameUM
|
|||
internal fun TangemPayEditDisplayNameScreen(
|
||||
isRedesignEnabled: Boolean,
|
||||
state: TangemPayEditDisplayNameUM,
|
||||
cardDetailsBlockComponent: TangemPayCardDetailsBlockComponent,
|
||||
cardDetailsState: TangemPayCardDetailsUM,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
if (isRedesignEnabled) {
|
||||
TangemPayEditDisplayNameScreenV2(
|
||||
state = state,
|
||||
cardDetailsBlockComponent = cardDetailsBlockComponent,
|
||||
cardDetailsState = cardDetailsState,
|
||||
modifier = modifier,
|
||||
)
|
||||
} else {
|
||||
TangemPayEditDisplayNameScreenV1(
|
||||
state = state,
|
||||
cardDetailsBlockComponent = cardDetailsBlockComponent,
|
||||
cardDetailsState = cardDetailsState,
|
||||
modifier = modifier,
|
||||
)
|
||||
|
|
@ -52,7 +48,6 @@ internal fun TangemPayEditDisplayNameScreen(
|
|||
@Composable
|
||||
internal fun TangemPayEditDisplayNameScreenV1(
|
||||
state: TangemPayEditDisplayNameUM,
|
||||
cardDetailsBlockComponent: TangemPayCardDetailsBlockComponent,
|
||||
cardDetailsState: TangemPayCardDetailsUM,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
|
|
@ -80,11 +75,11 @@ internal fun TangemPayEditDisplayNameScreenV1(
|
|||
}
|
||||
}
|
||||
|
||||
cardDetailsBlockComponent.CardDetailsBlockContent(
|
||||
TangemPayCard(
|
||||
state = cardDetailsState,
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 16.dp)
|
||||
.padding(top = 8.dp),
|
||||
state = cardDetailsState,
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
|
|
@ -105,7 +100,6 @@ internal fun TangemPayEditDisplayNameScreenV1(
|
|||
@Composable
|
||||
internal fun TangemPayEditDisplayNameScreenV2(
|
||||
state: TangemPayEditDisplayNameUM,
|
||||
cardDetailsBlockComponent: TangemPayCardDetailsBlockComponent,
|
||||
cardDetailsState: TangemPayCardDetailsUM,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
|
|
@ -133,9 +127,9 @@ internal fun TangemPayEditDisplayNameScreenV2(
|
|||
.fillMaxWidth()
|
||||
.verticalScroll(rememberScrollState()),
|
||||
) {
|
||||
cardDetailsBlockComponent.CardDetailsBlockContent(
|
||||
modifier = Modifier.padding(horizontal = TangemTheme.dimens2.x4, vertical = TangemTheme.dimens2.x2),
|
||||
TangemPayCard(
|
||||
state = cardDetailsState,
|
||||
modifier = Modifier.padding(horizontal = TangemTheme.dimens2.x4, vertical = TangemTheme.dimens2.x2),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,211 @@
|
|||
package com.tangem.features.tangempay.ui
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
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.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.vectorResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.core.ui.components.PrimaryButton
|
||||
import com.tangem.core.ui.components.SecondaryButtonIconEnd
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
|
||||
import com.tangem.core.ui.components.bottomsheets.modal.TangemModalBottomSheet
|
||||
import com.tangem.core.ui.components.bottomsheets.modal.TangemModalBottomSheetTitle
|
||||
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.TangemPayIssueAdditionalCardUM
|
||||
|
||||
@Composable
|
||||
internal fun TangemPayIssueAdditionalCardContent(state: TangemPayIssueAdditionalCardUM) {
|
||||
TangemModalBottomSheet<TangemBottomSheetConfigContent.Empty>(
|
||||
config = TangemBottomSheetConfig(
|
||||
isShown = true,
|
||||
onDismissRequest = state.onDismiss,
|
||||
content = TangemBottomSheetConfigContent.Empty,
|
||||
),
|
||||
onBack = state.onDismiss,
|
||||
containerColor = TangemTheme.colors.background.secondary,
|
||||
title = {
|
||||
TangemModalBottomSheetTitle(
|
||||
endIconRes = R.drawable.ic_close_24,
|
||||
onEndClick = state.onDismiss,
|
||||
)
|
||||
},
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
CardIcon()
|
||||
Text(
|
||||
modifier = Modifier.padding(top = 16.dp),
|
||||
text = stringResourceSafe(R.string.tangempay_issue_additional_card_title),
|
||||
style = TangemTheme.typography.h3,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
)
|
||||
Text(
|
||||
modifier = Modifier.padding(top = 4.dp),
|
||||
text = stringResourceSafe(R.string.tangempay_issue_additional_card_description),
|
||||
style = TangemTheme.typography.body2,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
)
|
||||
FeeRow(modifier = Modifier.padding(top = 16.dp), state = state)
|
||||
if (state.isBalanceInsufficient) {
|
||||
InsufficientFundsBlock(modifier = Modifier.padding(top = 8.dp), onAddFundsClick = state.onAddFundsClick)
|
||||
}
|
||||
PrimaryButton(
|
||||
modifier = Modifier
|
||||
.padding(vertical = 16.dp)
|
||||
.fillMaxWidth(),
|
||||
text = stringResourceSafe(R.string.tangempay_issue_card),
|
||||
showProgress = state.isLoading,
|
||||
enabled = !state.isLoading && !state.isBalanceInsufficient,
|
||||
onClick = state.onIssueClick,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun CardIcon() {
|
||||
Box(
|
||||
contentAlignment = Alignment.Center,
|
||||
modifier = Modifier
|
||||
.background(
|
||||
color = TangemTheme.colors.icon.accent.copy(alpha = 0.1f),
|
||||
shape = CircleShape,
|
||||
)
|
||||
.size(48.dp),
|
||||
) {
|
||||
Icon(
|
||||
modifier = Modifier.size(24.dp),
|
||||
imageVector = ImageVector.vectorResource(id = R.drawable.ic_credit_card_add_24),
|
||||
contentDescription = null,
|
||||
tint = TangemTheme.colors.icon.accent,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun FeeRow(state: TangemPayIssueAdditionalCardUM, modifier: Modifier = Modifier) {
|
||||
Row(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.clip(RoundedCornerShape(16.dp))
|
||||
.background(TangemTheme.colors.background.action)
|
||||
.padding(horizontal = 16.dp, vertical = 14.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Text(
|
||||
modifier = Modifier.weight(1f),
|
||||
text = stringResourceSafe(R.string.tangempay_issue_additional_card_fee_label),
|
||||
style = TangemTheme.typography.body2,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
)
|
||||
Text(
|
||||
text = state.feeText,
|
||||
style = TangemTheme.typography.body2,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun InsufficientFundsBlock(onAddFundsClick: () -> Unit, modifier: Modifier = Modifier) {
|
||||
Column(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.clip(RoundedCornerShape(16.dp))
|
||||
.background(TangemTheme.colors.background.action)
|
||||
.padding(horizontal = 16.dp, vertical = 12.dp),
|
||||
) {
|
||||
Row(verticalAlignment = Alignment.Top) {
|
||||
Image(
|
||||
modifier = Modifier.size(20.dp),
|
||||
painter = painterResource(id = R.drawable.img_usdc_16),
|
||||
contentDescription = null,
|
||||
)
|
||||
Column(modifier = Modifier.padding(start = 12.dp)) {
|
||||
Text(
|
||||
text = stringResourceSafe(R.string.tangempay_reissue_card_insufficient_funds_title),
|
||||
style = TangemTheme.typography.subtitle2,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
)
|
||||
Text(
|
||||
text = stringResourceSafe(
|
||||
R.string.tangempay_reissue_card_insufficient_funds_subtitle,
|
||||
),
|
||||
style = TangemTheme.typography.caption2,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
)
|
||||
}
|
||||
}
|
||||
SecondaryButtonIconEnd(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(top = 8.dp),
|
||||
text = stringResourceSafe(R.string.common_add_funds),
|
||||
iconResId = R.drawable.ic_plus_24,
|
||||
onClick = onAddFundsClick,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun TangemPayIssueAdditionalCardContentPreview(
|
||||
@PreviewParameter(IssueAdditionalCardPreviewProvider::class) state: TangemPayIssueAdditionalCardUM,
|
||||
) {
|
||||
TangemThemePreview {
|
||||
TangemPayIssueAdditionalCardContent(state = state)
|
||||
}
|
||||
}
|
||||
|
||||
private class IssueAdditionalCardPreviewProvider : PreviewParameterProvider<TangemPayIssueAdditionalCardUM> {
|
||||
override val values: Sequence<TangemPayIssueAdditionalCardUM>
|
||||
get() = sequenceOf(
|
||||
TangemPayIssueAdditionalCardUM(
|
||||
isBalanceInsufficient = false,
|
||||
feeText = "10 $",
|
||||
isLoading = false,
|
||||
onIssueClick = {},
|
||||
onAddFundsClick = {},
|
||||
onDismiss = {},
|
||||
),
|
||||
TangemPayIssueAdditionalCardUM(
|
||||
isBalanceInsufficient = true,
|
||||
feeText = "10 $",
|
||||
isLoading = false,
|
||||
onIssueClick = {},
|
||||
onAddFundsClick = {},
|
||||
onDismiss = {},
|
||||
),
|
||||
TangemPayIssueAdditionalCardUM(
|
||||
isBalanceInsufficient = false,
|
||||
feeText = "10 $",
|
||||
isLoading = true,
|
||||
onIssueClick = {},
|
||||
onAddFundsClick = {},
|
||||
onDismiss = {},
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
@ -9,7 +9,7 @@ internal interface TangemPayDetailIntents {
|
|||
fun onClickAddFunds()
|
||||
fun onClickWithdraw()
|
||||
fun onClickTermsAndLimits()
|
||||
fun onCardClick()
|
||||
fun onCardClick(cardId: String)
|
||||
fun onAddCardClick()
|
||||
fun onRemoveAccount()
|
||||
}
|
||||
|
|
@ -4,7 +4,9 @@ import com.tangem.core.ui.R
|
|||
import com.tangem.core.ui.components.bottomsheets.message.*
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.wrappedList
|
||||
import com.tangem.core.ui.message.BottomSheetMessage
|
||||
import com.tangem.core.ui.message.DialogMessage
|
||||
import com.tangem.core.ui.message.bottomSheetMessage
|
||||
import com.tangem.core.ui.res.generated.icons.Icons
|
||||
import com.tangem.core.ui.res.generated.icons.ic_snowflake_20
|
||||
|
|
@ -147,6 +149,32 @@ internal object TangemPayMessagesFactory {
|
|||
}
|
||||
}
|
||||
|
||||
fun createMaximumCardsIssued(maxCards: Int): BottomSheetMessage {
|
||||
return bottomSheetMessage {
|
||||
infoBlock {
|
||||
icon(R.drawable.img_attention_20) {
|
||||
backgroundType = MessageBottomSheetUM.Icon.BackgroundType.Attention
|
||||
}
|
||||
title = resourceReference(R.string.tangempay_maximum_cards_issued_title)
|
||||
body = resourceReference(
|
||||
id = R.string.tangempay_maximum_cards_issued_description,
|
||||
formatArgs = wrappedList(maxCards),
|
||||
)
|
||||
}
|
||||
primaryButton {
|
||||
text = resourceReference(R.string.common_got_it)
|
||||
onClick { closeBs() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun createGenericError(): DialogMessage {
|
||||
return DialogMessage(
|
||||
title = resourceReference(R.string.common_something_went_wrong),
|
||||
message = resourceReference(R.string.common_try_again_later),
|
||||
)
|
||||
}
|
||||
|
||||
fun createFutureFeature(onGotItClick: () -> Unit): BottomSheetMessage {
|
||||
return bottomSheetMessage {
|
||||
infoBlock {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,257 @@
|
|||
package com.tangem.features.tangempay.model.controller
|
||||
|
||||
import arrow.core.left
|
||||
import arrow.core.right
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.core.error.UniversalError
|
||||
import com.tangem.core.ui.clipboard.ClipboardManager
|
||||
import com.tangem.core.ui.message.SnackbarMessage
|
||||
import com.tangem.core.decompose.ui.UiMessageSender
|
||||
import com.tangem.domain.models.StatusSource
|
||||
import com.tangem.domain.models.account.AccountStatus
|
||||
import com.tangem.domain.models.account.CardDisplayName
|
||||
import com.tangem.domain.models.account.PaymentAccountStatusValue
|
||||
import com.tangem.domain.models.pay.TangemPayCard
|
||||
import com.tangem.domain.models.pay.TangemPayCardFrozenState
|
||||
import com.tangem.domain.models.pay.TangemPayCardState
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.pay.flow.PaymentAccountStatusSupplier
|
||||
import com.tangem.domain.pay.model.TangemPayCardDetails
|
||||
import com.tangem.domain.pay.repository.TangemPayCardDetailsRepository
|
||||
import com.tangem.domain.tangempay.TangemPayAnalyticsEvents
|
||||
import com.tangem.features.tangempay.entity.CardDataType
|
||||
import com.tangem.features.tangempay.model.listener.CardDetailsEvent
|
||||
import com.tangem.features.tangempay.model.listener.DefaultCardDetailsEventListener
|
||||
import com.tangem.utils.StringsSigns
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.coVerify
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import io.mockk.verify
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.emptyFlow
|
||||
import kotlinx.coroutines.test.TestScope
|
||||
import kotlinx.coroutines.test.advanceTimeBy
|
||||
import kotlinx.coroutines.test.advanceUntilIdle
|
||||
import kotlinx.coroutines.test.runCurrent
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
private const val SHOW_DETAILS_TIME = 30_000L
|
||||
|
||||
internal class TangemPayCardDetailsControllerTest {
|
||||
|
||||
private val userWalletId = UserWalletId("123")
|
||||
private val cardId = "card_1"
|
||||
private val otherCardId = "card_2"
|
||||
|
||||
private val repository: TangemPayCardDetailsRepository = mockk(relaxed = true)
|
||||
private val clipboardManager: ClipboardManager = mockk(relaxed = true)
|
||||
private val uiMessageSender: UiMessageSender = mockk(relaxed = true)
|
||||
private val analytics: AnalyticsEventHandler = mockk(relaxed = true)
|
||||
private val supplier: PaymentAccountStatusSupplier = mockk()
|
||||
private val supplierFlow = MutableSharedFlow<AccountStatus.Payment>(replay = 1)
|
||||
private val eventListener = DefaultCardDetailsEventListener()
|
||||
|
||||
private val details = TangemPayCardDetails(
|
||||
pan = "4242424242424242",
|
||||
cvv = "123",
|
||||
expirationYear = "2030",
|
||||
expirationMonth = "9",
|
||||
)
|
||||
|
||||
init {
|
||||
every { supplier.invoke(userWalletId) } returns supplierFlow
|
||||
every { repository.cardFrozenState(any()) } returns emptyFlow()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN card WHEN created THEN initial state is hidden with masked digits`() = runTest {
|
||||
val controller = createController(scope = backgroundScope, card = card(lastDigits = "9999"))
|
||||
runCurrent()
|
||||
|
||||
val state = controller.uiState.value
|
||||
assertThat(state.isHidden).isTrue()
|
||||
assertThat(state.numberShort).isEqualTo("${StringsSigns.ASTERISK}9999")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN Show for this card WHEN event received THEN details revealed and analytics sent`() = runTest {
|
||||
coEvery { repository.revealCardDetails(userWalletId, cardId) } returns details.right()
|
||||
val controller = createController(scope = backgroundScope)
|
||||
runCurrent()
|
||||
|
||||
eventListener.send(CardDetailsEvent.Show(cardId))
|
||||
runCurrent()
|
||||
|
||||
verify(exactly = 1) { analytics.send(TangemPayAnalyticsEvents.ViewCardDetailsClicked()) }
|
||||
coVerify(exactly = 1) { repository.revealCardDetails(userWalletId, cardId) }
|
||||
val state = controller.uiState.value
|
||||
assertThat(state.isHidden).isFalse()
|
||||
assertThat(state.cvv).isEqualTo("123")
|
||||
assertThat(state.expiry).isEqualTo("09/30")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN reveal fails WHEN event received THEN snackbar shown and state hidden`() = runTest {
|
||||
coEvery { repository.revealCardDetails(userWalletId, cardId) } returns ERROR.left()
|
||||
val controller = createController(scope = backgroundScope)
|
||||
runCurrent()
|
||||
|
||||
eventListener.send(CardDetailsEvent.Show(cardId))
|
||||
runCurrent()
|
||||
|
||||
verify(exactly = 1) { uiMessageSender.send(any<SnackbarMessage>()) }
|
||||
assertThat(controller.uiState.value.isHidden).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN revealed WHEN Show for another card THEN this card hides`() = runTest {
|
||||
coEvery { repository.revealCardDetails(userWalletId, cardId) } returns details.right()
|
||||
val controller = createController(scope = backgroundScope)
|
||||
runCurrent()
|
||||
eventListener.send(CardDetailsEvent.Show(cardId))
|
||||
runCurrent()
|
||||
|
||||
eventListener.send(CardDetailsEvent.Show(otherCardId))
|
||||
runCurrent()
|
||||
|
||||
assertThat(controller.uiState.value.isHidden).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN revealed WHEN Hide for another card THEN this card stays revealed`() = runTest {
|
||||
coEvery { repository.revealCardDetails(userWalletId, cardId) } returns details.right()
|
||||
val controller = createController(scope = backgroundScope)
|
||||
runCurrent()
|
||||
eventListener.send(CardDetailsEvent.Show(cardId))
|
||||
runCurrent()
|
||||
|
||||
eventListener.send(CardDetailsEvent.Hide(otherCardId))
|
||||
runCurrent()
|
||||
|
||||
assertThat(controller.uiState.value.isHidden).isFalse()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN revealed WHEN HideAll THEN this card hides`() = runTest {
|
||||
coEvery { repository.revealCardDetails(userWalletId, cardId) } returns details.right()
|
||||
val controller = createController(scope = backgroundScope)
|
||||
runCurrent()
|
||||
eventListener.send(CardDetailsEvent.Show(cardId))
|
||||
runCurrent()
|
||||
|
||||
eventListener.send(CardDetailsEvent.HideAll)
|
||||
runCurrent()
|
||||
|
||||
assertThat(controller.uiState.value.isHidden).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN revealed WHEN show timer elapses THEN details auto-hide`() = runTest {
|
||||
coEvery { repository.revealCardDetails(userWalletId, cardId) } returns details.right()
|
||||
val controller = createController(scope = backgroundScope)
|
||||
runCurrent()
|
||||
eventListener.send(CardDetailsEvent.Show(cardId))
|
||||
runCurrent()
|
||||
|
||||
advanceTimeBy(SHOW_DETAILS_TIME + 1)
|
||||
runCurrent()
|
||||
|
||||
assertThat(controller.uiState.value.isHidden).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN supplier emits actual loaded WHEN card present THEN frozen state and digits updated`() = runTest {
|
||||
val updatedCard = card(lastDigits = "5678", frozenState = TangemPayCardFrozenState.Frozen)
|
||||
val loaded = mockk<PaymentAccountStatusValue.Loaded> {
|
||||
every { source } returns StatusSource.ACTUAL
|
||||
every { cards } returns listOf(updatedCard)
|
||||
}
|
||||
val payment = mockk<AccountStatus.Payment> {
|
||||
every { value } returns loaded
|
||||
}
|
||||
val controller = createController(scope = backgroundScope)
|
||||
runCurrent()
|
||||
|
||||
supplierFlow.emit(payment)
|
||||
runCurrent()
|
||||
|
||||
val state = controller.uiState.value
|
||||
assertThat(state.numberShort).isEqualTo("${StringsSigns.ASTERISK}5678")
|
||||
assertThat(state.cardFrozenState).isEqualTo(TangemPayCardFrozenState.Frozen)
|
||||
assertThat(state.isActionsAvailable).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN copy cvv WHEN onCopy invoked THEN clipboard set as sensitive and analytics sent`() = runTest {
|
||||
val controller = createController(scope = backgroundScope)
|
||||
runCurrent()
|
||||
|
||||
controller.uiState.value.onCopy("1 2 3", CardDataType.CVV)
|
||||
|
||||
verify(exactly = 1) { clipboardManager.setText(text = "123", isSensitive = true) }
|
||||
verify(exactly = 1) { analytics.send(TangemPayAnalyticsEvents.CopyCardCVVClicked()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN disposed WHEN Show received THEN reveal not triggered`() = runTest {
|
||||
coEvery { repository.revealCardDetails(userWalletId, cardId) } returns details.right()
|
||||
val controller = createController(scope = backgroundScope)
|
||||
runCurrent()
|
||||
|
||||
controller.dispose()
|
||||
eventListener.send(CardDetailsEvent.Show(cardId))
|
||||
advanceUntilIdle()
|
||||
|
||||
coVerify(exactly = 0) { repository.revealCardDetails(any(), any()) }
|
||||
}
|
||||
|
||||
private fun TestScope.createController(
|
||||
scope: CoroutineScope,
|
||||
card: TangemPayCard = card(),
|
||||
config: TangemPayCardDetailsController.Config = TangemPayCardDetailsController.Config(
|
||||
isEditingNameEnabled = true,
|
||||
shouldShowCardDetailsButtonOnCard = false,
|
||||
),
|
||||
onEditNameClick: () -> Unit = {},
|
||||
): TangemPayCardDetailsController = TangemPayCardDetailsController(
|
||||
scope = scope,
|
||||
initialCard = card,
|
||||
userWalletId = userWalletId,
|
||||
config = config,
|
||||
onEditNameClick = onEditNameClick,
|
||||
cardDetailsRepository = repository,
|
||||
clipboardManager = clipboardManager,
|
||||
uiMessageSender = uiMessageSender,
|
||||
cardDetailsEventListener = eventListener,
|
||||
analytics = analytics,
|
||||
paymentAccountStatusSupplier = supplier,
|
||||
)
|
||||
|
||||
private fun card(
|
||||
id: String = cardId,
|
||||
lastDigits: String = "1234",
|
||||
displayName: CardDisplayName? = null,
|
||||
frozenState: TangemPayCardFrozenState = TangemPayCardFrozenState.Unfrozen,
|
||||
state: TangemPayCardState = TangemPayCardState.Active,
|
||||
): TangemPayCard = TangemPayCard(
|
||||
id = id,
|
||||
productInstanceId = "product_$id",
|
||||
cardStatus = TangemPayCard.Status.ACTIVE,
|
||||
hasPinCode = true,
|
||||
displayName = displayName,
|
||||
limit = null,
|
||||
frozenState = frozenState,
|
||||
lastDigits = lastDigits,
|
||||
state = state,
|
||||
)
|
||||
|
||||
private companion object {
|
||||
val ERROR = object : UniversalError {
|
||||
override val errorCode: Int = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue