Updated on 2026-08-14
This commit is contained in:
commit
f66092d82c
52 changed files with 1448 additions and 464 deletions
|
|
@ -200,6 +200,7 @@ class MainActivity : AppCompatActivity(), ActivityResultCallbackHolder {
|
|||
}
|
||||
|
||||
splashScreen.setKeepOnScreenCondition { viewModel.isSplashScreenShown }
|
||||
splashScreen.setOnExitAnimationListener { provider -> provider.remove() }
|
||||
|
||||
installActivityDependencies()
|
||||
observeAppThemeModeUpdates()
|
||||
|
|
|
|||
|
|
@ -4,12 +4,7 @@ import android.app.Activity
|
|||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.imePadding
|
||||
import androidx.compose.foundation.layout.navigationBarsPadding
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.statusBarsPadding
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.ui.Alignment
|
||||
|
|
@ -32,11 +27,7 @@ import com.tangem.core.ui.components.haze.ProvideHaze
|
|||
import com.tangem.core.ui.components.snackbar.TangemSnackbarHost
|
||||
import com.tangem.core.ui.components.snackbar.TangemTopSnackbarHost
|
||||
import com.tangem.core.ui.message.EventMessageEffect
|
||||
import com.tangem.core.ui.res.LocalRedesignEnabled
|
||||
import com.tangem.core.ui.res.LocalRootBackgroundColor
|
||||
import com.tangem.core.ui.res.LocalSnackbarHostState
|
||||
import com.tangem.core.ui.res.LocalTopSnackbarHostState
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.*
|
||||
import com.tangem.core.ui.security.ProvideSecureFlagController
|
||||
import com.tangem.tap.routing.component.RoutingComponent
|
||||
import com.tangem.tap.routing.transitions.RoutingTransitionAnimationFactory
|
||||
|
|
@ -124,20 +115,34 @@ private fun childrenAnimation(
|
|||
backHandler: BackHandler,
|
||||
onBack: () -> Unit,
|
||||
): StackAnimation<AppRoute, RoutingComponent.Child> {
|
||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
||||
val routeAnimation = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
||||
predictiveBackAnimation(
|
||||
backHandler = backHandler,
|
||||
onBack = onBack,
|
||||
selector = { backEvent, _, _ ->
|
||||
androidPredictiveBackAnimatable(backEvent)
|
||||
},
|
||||
fallbackAnimation = stackAnimation {
|
||||
RoutingTransitionAnimationFactory.create(it.configuration)
|
||||
fallbackAnimation = stackAnimation<AppRoute, RoutingComponent.Child> { child ->
|
||||
RoutingTransitionAnimationFactory.create(child.configuration)
|
||||
},
|
||||
)
|
||||
} else {
|
||||
stackAnimation {
|
||||
RoutingTransitionAnimationFactory.create(it.configuration)
|
||||
stackAnimation { child ->
|
||||
RoutingTransitionAnimationFactory.create(child.configuration)
|
||||
}
|
||||
}
|
||||
|
||||
return skipAnimationWhileInitial(routeAnimation)
|
||||
}
|
||||
|
||||
private fun skipAnimationWhileInitial(
|
||||
delegate: StackAnimation<AppRoute, RoutingComponent.Child>,
|
||||
): StackAnimation<AppRoute, RoutingComponent.Child> = StackAnimation { stack, animModifier, content ->
|
||||
if (stack.active.configuration is AppRoute.Initial) {
|
||||
Box(modifier = animModifier) {
|
||||
content(stack.active)
|
||||
}
|
||||
} else {
|
||||
delegate(stack, animModifier, content)
|
||||
}
|
||||
}
|
||||
|
|
@ -3,6 +3,7 @@
|
|||
<style name="BaseAppTheme" parent="Theme.MaterialComponents.DayNight.NoActionBar" />
|
||||
|
||||
<style name="AppTheme" parent="BaseAppTheme">
|
||||
<item name="android:windowBackground">@color/background_secondary</item>
|
||||
<item name="colorPrimary">@color/menu_accent_color</item>
|
||||
<item name="colorSecondary">@color/menu_accent_color</item>
|
||||
<item name="android:textColor">@color/text_primary_1</item>
|
||||
|
|
|
|||
|
|
@ -125,15 +125,21 @@ private fun EarnBlockContent(state: EarnBlockUM.Content, modifier: Modifier = Mo
|
|||
.padding(end = TangemTheme.dimens2.x2),
|
||||
)
|
||||
|
||||
val subtitle = state.subtitleUM
|
||||
if (subtitle is EarnBlockUM.SubtitleUM.Text) {
|
||||
EarnBlockSubtitle(
|
||||
val subtitleModifier = Modifier
|
||||
.layoutId(TangemRowLayoutId.START_BOTTOM)
|
||||
.padding(end = TangemTheme.dimens2.x2)
|
||||
when (val subtitle = state.subtitleUM) {
|
||||
is EarnBlockUM.SubtitleUM.Text -> EarnBlockSubtitle(
|
||||
subtitle = subtitle,
|
||||
type = state.type,
|
||||
modifier = Modifier
|
||||
.layoutId(TangemRowLayoutId.START_BOTTOM)
|
||||
.padding(end = TangemTheme.dimens2.x2),
|
||||
modifier = subtitleModifier,
|
||||
)
|
||||
is EarnBlockUM.SubtitleUM.AccentedText -> EarnBlockAccentedSubtitle(
|
||||
subtitle = subtitle,
|
||||
type = state.type,
|
||||
modifier = subtitleModifier,
|
||||
)
|
||||
null -> Unit
|
||||
}
|
||||
|
||||
EarnBlockTrailing(type = state.type, trailingUM = state.trailingUM, onClick = state.onClick)
|
||||
|
|
@ -253,7 +259,6 @@ private fun EarnBlockTrailing(type: Type, trailingUM: EarnBlockUM.TrailingUM?, o
|
|||
)
|
||||
}
|
||||
is EarnBlockUM.TrailingUM.Balance -> {
|
||||
if (!trailingUM.isBalanceHidden) {
|
||||
val fiatModifier = Modifier.layoutId(TangemRowLayoutId.END_TOP).let {
|
||||
if (type == Type.Staking) it.testTag(TokenDetailsScreenTestTags.STAKING_FIAT_AMOUNT) else it
|
||||
}
|
||||
|
|
@ -261,19 +266,18 @@ private fun EarnBlockTrailing(type: Type, trailingUM: EarnBlockUM.TrailingUM?, o
|
|||
if (type == Type.Staking) it.testTag(TokenDetailsScreenTestTags.STAKING_TOKEN_AMOUNT) else it
|
||||
}
|
||||
Text(
|
||||
text = trailingUM.fiatValue.resolveAnnotatedReference(),
|
||||
text = trailingUM.fiatValue.orMaskWithStars(trailingUM.isBalanceHidden).resolveAnnotatedReference(),
|
||||
style = TangemTheme.typography2.bodySemibold16,
|
||||
color = TangemTheme.colors2.text.neutral.primary,
|
||||
modifier = fiatModifier,
|
||||
)
|
||||
Text(
|
||||
text = trailingUM.cryptoValue.resolveReference(),
|
||||
text = trailingUM.cryptoValue.orMaskWithStars(trailingUM.isBalanceHidden).resolveReference(),
|
||||
style = TangemTheme.typography2.captionMedium12,
|
||||
color = TangemTheme.colors2.text.neutral.secondary,
|
||||
modifier = cryptoModifier,
|
||||
)
|
||||
}
|
||||
}
|
||||
null -> Unit
|
||||
}
|
||||
}
|
||||
|
|
@ -325,6 +329,29 @@ private fun EarnBlockSubtitle(subtitle: EarnBlockUM.SubtitleUM.Text, type: Type,
|
|||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun EarnBlockAccentedSubtitle(
|
||||
subtitle: EarnBlockUM.SubtitleUM.AccentedText,
|
||||
type: Type,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val baseText = subtitle.text.resolveReference()
|
||||
val accentText = subtitle.accent.resolveReference()
|
||||
val accentColor = type.accentText()
|
||||
Text(
|
||||
text = buildAnnotatedString {
|
||||
append(baseText)
|
||||
if (baseText.isNotEmpty() && !baseText.last().isWhitespace()) append(' ')
|
||||
withStyle(SpanStyle(color = accentColor)) {
|
||||
append(accentText)
|
||||
}
|
||||
},
|
||||
style = subtitle.style.textStyle,
|
||||
color = TangemTheme.colors2.text.neutral.tertiary,
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun EarnBlockIcon(type: Type, iconUM: EarnBlockUM.IconUM, modifier: Modifier = Modifier) {
|
||||
Box(
|
||||
|
|
|
|||
|
|
@ -72,6 +72,12 @@ sealed interface EarnBlockUM {
|
|||
val loader: Loader? = null,
|
||||
) : SubtitleUM
|
||||
|
||||
data class AccentedText(
|
||||
val text: TextReference,
|
||||
val accent: TextReference,
|
||||
val style: Style,
|
||||
) : SubtitleUM
|
||||
|
||||
data class Loader(val tone: LoaderTone)
|
||||
|
||||
enum class Style { Large, Small }
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -1517,6 +1517,8 @@
|
|||
<string name="staking_details_apy">APY</string>
|
||||
<string name="staking_details_auto_claiming_rewards_daily_text">Rewards automatically accumulate in your staking balance daily.</string>
|
||||
<string name="staking_details_autocompound_rewards_earned">Rewards are compounded to your staking balance. Funds earned: %s</string>
|
||||
<string name="staking_details_autocompound_rewards_compounded">Rewards are compounded to your staking balance.</string>
|
||||
<string name="staking_details_autocompound_funds_earned">Funds earned: %s</string>
|
||||
<string name="staking_details_available">Available</string>
|
||||
<string name="staking_details_average_reward_rate">Average Reward Rate</string>
|
||||
<string name="staking_details_banner_text">How Staking Works?</string>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1484,13 +1484,15 @@ internal class TokenDetailsModel @Inject constructor(
|
|||
flow2 = availabilityFlow,
|
||||
flow3 = entryInfoFlow,
|
||||
flow4 = selectedAppCurrencyFlow,
|
||||
) { status, availability, entryInfo, appCurrency ->
|
||||
flow5 = redesignStateController.isBalanceHidden,
|
||||
) { status, availability, entryInfo, appCurrency, isBalanceHidden ->
|
||||
redesignStateController.update(
|
||||
UpdateStakingNotificationTransformer(
|
||||
cryptoCurrencyStatus = status,
|
||||
stakingAvailability = availability,
|
||||
stakingEntryInfo = entryInfo,
|
||||
appCurrency = appCurrency,
|
||||
isBalanceHidden = isBalanceHidden,
|
||||
clickIntents = this,
|
||||
),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -13,8 +13,11 @@ import com.tangem.core.ui.res.TangemTheme
|
|||
import com.tangem.features.tokendetails.impl.R
|
||||
import com.tangem.utils.transformer.Transformer
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.update
|
||||
import javax.inject.Inject
|
||||
|
||||
|
|
@ -26,6 +29,10 @@ internal class TokenDetailsStateController @Inject constructor() {
|
|||
|
||||
val value: TokenDetailsUM get() = uiState.value
|
||||
|
||||
val isBalanceHidden: Flow<Boolean> = uiState
|
||||
.map { it.isBalanceHidden }
|
||||
.distinctUntilChanged()
|
||||
|
||||
fun update(function: (TokenDetailsUM) -> TokenDetailsUM) {
|
||||
uiState.update(function = function)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import com.tangem.common.getRewardStakingBalance
|
|||
import com.tangem.common.getTotalStakingBalance
|
||||
import com.tangem.common.ui.earn.EarnBlockUM
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.orMaskWithStars
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.core.ui.extensions.wrappedList
|
||||
|
|
@ -34,11 +35,12 @@ internal class UpdateStakingNotificationTransformer(
|
|||
private val stakingAvailability: StakingAvailability,
|
||||
private val stakingEntryInfo: StakingEntryInfo?,
|
||||
private val appCurrency: AppCurrency,
|
||||
private val isBalanceHidden: Boolean,
|
||||
private val clickIntents: TokenDetailsClickIntents,
|
||||
) : Transformer<TokenDetailsUM> {
|
||||
|
||||
override fun transform(prevState: TokenDetailsUM): TokenDetailsUM {
|
||||
return prevState.copy(earnBlockState = buildEarnBlock(prevState.isBalanceHidden))
|
||||
return prevState.copy(earnBlockState = buildEarnBlock(isBalanceHidden))
|
||||
}
|
||||
|
||||
private fun buildEarnBlock(isBalanceHidden: Boolean): EarnBlockUM? {
|
||||
|
|
@ -56,12 +58,12 @@ internal class UpdateStakingNotificationTransformer(
|
|||
iconUM = EarnBlockUM.IconUM.Plain(iconRes = CoreUiR.drawable.ic_staking_disable_40),
|
||||
titleUM = EarnBlockUM.TitleUM(
|
||||
text = resourceReference(CoreResR.string.common_staking),
|
||||
style = EarnBlockUM.TitleUM.Style.Small,
|
||||
style = EarnBlockUM.TitleUM.Style.Large,
|
||||
tone = EarnBlockUM.TitleUM.Tone.Disabled,
|
||||
),
|
||||
subtitleUM = EarnBlockUM.SubtitleUM.Text(
|
||||
text = resourceReference(CoreResR.string.staking_notification_network_error_text),
|
||||
style = EarnBlockUM.SubtitleUM.Style.Large,
|
||||
style = EarnBlockUM.SubtitleUM.Style.Small,
|
||||
tone = EarnBlockUM.SubtitleUM.Tone.Disabled,
|
||||
),
|
||||
trailingUM = null,
|
||||
|
|
@ -117,12 +119,12 @@ internal class UpdateStakingNotificationTransformer(
|
|||
iconUM = EarnBlockUM.IconUM.Glowing(iconRes = CoreUiR.drawable.ic_staking_40),
|
||||
titleUM = EarnBlockUM.TitleUM(
|
||||
text = resourceReference(id = CoreResR.string.common_staking),
|
||||
style = EarnBlockUM.TitleUM.Style.Small,
|
||||
style = EarnBlockUM.TitleUM.Style.Large,
|
||||
tone = EarnBlockUM.TitleUM.Tone.Primary,
|
||||
),
|
||||
subtitleUM = EarnBlockUM.SubtitleUM.Text(
|
||||
text = stakeAvailableSubtitle(availability.option.displayRewardInfo),
|
||||
style = EarnBlockUM.SubtitleUM.Style.Large,
|
||||
style = EarnBlockUM.SubtitleUM.Style.Small,
|
||||
tone = EarnBlockUM.SubtitleUM.Tone.Disabled,
|
||||
),
|
||||
trailingUM = EarnBlockUM.TrailingUM.Button(
|
||||
|
|
@ -165,10 +167,10 @@ internal class UpdateStakingNotificationTransformer(
|
|||
iconUM = EarnBlockUM.IconUM.Glowing(iconRes = CoreUiR.drawable.ic_staking_40),
|
||||
titleUM = EarnBlockUM.TitleUM(
|
||||
text = resourceReference(CoreResR.string.staking_enabled),
|
||||
style = EarnBlockUM.TitleUM.Style.Small,
|
||||
style = EarnBlockUM.TitleUM.Style.Large,
|
||||
tone = EarnBlockUM.TitleUM.Tone.Primary,
|
||||
),
|
||||
subtitleUM = getRewardSubtitle(status, rewardFiatAmount),
|
||||
subtitleUM = getRewardSubtitle(status, rewardFiatAmount, isBalanceHidden),
|
||||
trailingUM = EarnBlockUM.TrailingUM.Balance(
|
||||
fiatValue = fiatAmount.formatStyled {
|
||||
fiat(
|
||||
|
|
@ -196,6 +198,7 @@ internal class UpdateStakingNotificationTransformer(
|
|||
private fun getRewardSubtitle(
|
||||
status: CryptoCurrencyStatus,
|
||||
stakingRewardAmount: BigDecimal?,
|
||||
isBalanceHidden: Boolean,
|
||||
): EarnBlockUM.SubtitleUM? {
|
||||
val blockchainId = status.currency.network.rawId
|
||||
val isCoin = status.currency.id.isCoin
|
||||
|
|
@ -224,16 +227,20 @@ internal class UpdateStakingNotificationTransformer(
|
|||
-> return null
|
||||
RewardBlockType.EthereumEarnedRewards -> {
|
||||
val cryptoRewardAmount = (stakingBalance as? StakingBalance.Data.P2PEthPool)?.totalRewards
|
||||
resourceReference(
|
||||
R.string.staking_details_autocompound_rewards_earned,
|
||||
return EarnBlockUM.SubtitleUM.AccentedText(
|
||||
text = resourceReference(R.string.staking_details_autocompound_rewards_compounded),
|
||||
accent = resourceReference(
|
||||
R.string.staking_details_autocompound_funds_earned,
|
||||
wrappedList(
|
||||
cryptoRewardAmount.format {
|
||||
crypto(
|
||||
symbol = status.currency.symbol,
|
||||
decimals = status.currency.decimals,
|
||||
)
|
||||
},
|
||||
}.orMaskWithStars(isBalanceHidden),
|
||||
),
|
||||
),
|
||||
style = EarnBlockUM.SubtitleUM.Style.Small,
|
||||
)
|
||||
}
|
||||
RewardBlockType.RewardsRequirementsError,
|
||||
|
|
@ -241,18 +248,18 @@ internal class UpdateStakingNotificationTransformer(
|
|||
-> resourceReference(
|
||||
R.string.staking_details_rewards_to_claim,
|
||||
wrappedList(
|
||||
stakingRewardAmount.format { fiat(appCurrency.code, appCurrency.symbol) },
|
||||
stakingRewardAmount.format { fiat(appCurrency.code, appCurrency.symbol) }
|
||||
.orMaskWithStars(isBalanceHidden),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
val isAccent = rewardBlockType == RewardBlockType.Rewards ||
|
||||
rewardBlockType == RewardBlockType.RewardsRequirementsError ||
|
||||
rewardBlockType == RewardBlockType.EthereumEarnedRewards
|
||||
rewardBlockType == RewardBlockType.RewardsRequirementsError
|
||||
|
||||
return EarnBlockUM.SubtitleUM.Text(
|
||||
text = text,
|
||||
style = EarnBlockUM.SubtitleUM.Style.Large,
|
||||
style = EarnBlockUM.SubtitleUM.Style.Small,
|
||||
tone = if (isAccent) EarnBlockUM.SubtitleUM.Tone.Accent else EarnBlockUM.SubtitleUM.Tone.Disabled,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,15 +4,23 @@ import com.google.common.truth.Truth.assertThat
|
|||
import com.tangem.common.ui.earn.EarnBlockUM
|
||||
import com.tangem.core.ui.components.containers.pullToRefresh.PullToRefreshConfig
|
||||
import com.tangem.core.ui.components.marketprice.MarketPriceBlockState
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.models.StatusSource
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||
import com.tangem.domain.models.network.Network
|
||||
import com.tangem.domain.models.staking.BalanceItem
|
||||
import com.tangem.domain.models.staking.BalanceType
|
||||
import com.tangem.domain.models.staking.StakingBalance
|
||||
import com.tangem.domain.models.staking.StakingID
|
||||
import com.tangem.domain.models.staking.YieldBalanceItem
|
||||
import com.tangem.domain.models.staking.YieldToken
|
||||
import com.tangem.domain.staking.model.StakingAvailability
|
||||
import com.tangem.domain.staking.model.StakingEntryInfo
|
||||
import com.tangem.domain.staking.model.StakingOption
|
||||
import com.tangem.utils.StringsSigns.THREE_STARS
|
||||
import com.tangem.feature.tokendetails.presentation.tokendetails.model.TokenDetailsClickIntents
|
||||
import com.tangem.feature.tokendetails.presentation.tokendetails.state.AddFundsUM
|
||||
import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsBalanceBlockUM
|
||||
|
|
@ -84,29 +92,114 @@ class UpdateStakingNotificationTransformerTest {
|
|||
assertThat(content.trailingUM).isInstanceOf(EarnBlockUM.TrailingUM.Button::class.java)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN active staked balance AND balance hidden WHEN transform THEN trailing balance hidden`() {
|
||||
// Arrange
|
||||
val status = buildStatus(
|
||||
networkRawId = "ethereum",
|
||||
symbol = "ETH",
|
||||
isCoin = false,
|
||||
stakingBalance = stakeKitBalance(staked = BigDecimal("100"), rewards = BigDecimal("5")),
|
||||
)
|
||||
val transformer = createTransformer(
|
||||
availability = availableOption(BigDecimal("4.2")),
|
||||
entryInfo = StakingEntryInfo(tokenSymbol = "ETH"),
|
||||
cryptoCurrencyStatus = status,
|
||||
isBalanceHidden = true,
|
||||
)
|
||||
|
||||
// Act
|
||||
val result = transformer.transform(initialState())
|
||||
|
||||
// Assert
|
||||
val content = result.earnBlockState as EarnBlockUM.Content
|
||||
val trailing = content.trailingUM as EarnBlockUM.TrailingUM.Balance
|
||||
assertThat(trailing.isBalanceHidden).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN rewards to claim AND balance hidden WHEN transform THEN reward amount masked with stars`() {
|
||||
// Arrange
|
||||
val status = buildStatus(
|
||||
networkRawId = "ethereum",
|
||||
symbol = "ETH",
|
||||
isCoin = false,
|
||||
stakingBalance = stakeKitBalance(staked = BigDecimal("100"), rewards = BigDecimal("5")),
|
||||
)
|
||||
val transformer = createTransformer(
|
||||
availability = availableOption(BigDecimal("4.2")),
|
||||
entryInfo = StakingEntryInfo(tokenSymbol = "ETH"),
|
||||
cryptoCurrencyStatus = status,
|
||||
isBalanceHidden = true,
|
||||
)
|
||||
|
||||
// Act
|
||||
val result = transformer.transform(initialState())
|
||||
|
||||
// Assert
|
||||
val content = result.earnBlockState as EarnBlockUM.Content
|
||||
val subtitle = content.subtitleUM as EarnBlockUM.SubtitleUM.Text
|
||||
assertThat(rewardFormatArg(subtitle.text)).isEqualTo(THREE_STARS)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN rewards to claim AND balance visible WHEN transform THEN reward amount not masked`() {
|
||||
// Arrange
|
||||
val status = buildStatus(
|
||||
networkRawId = "ethereum",
|
||||
symbol = "ETH",
|
||||
isCoin = false,
|
||||
stakingBalance = stakeKitBalance(staked = BigDecimal("100"), rewards = BigDecimal("5")),
|
||||
)
|
||||
val transformer = createTransformer(
|
||||
availability = availableOption(BigDecimal("4.2")),
|
||||
entryInfo = StakingEntryInfo(tokenSymbol = "ETH"),
|
||||
cryptoCurrencyStatus = status,
|
||||
isBalanceHidden = false,
|
||||
)
|
||||
|
||||
// Act
|
||||
val result = transformer.transform(initialState())
|
||||
|
||||
// Assert
|
||||
val content = result.earnBlockState as EarnBlockUM.Content
|
||||
val subtitle = content.subtitleUM as EarnBlockUM.SubtitleUM.Text
|
||||
assertThat(rewardFormatArg(subtitle.text)).isNotEqualTo(THREE_STARS)
|
||||
}
|
||||
|
||||
private fun rewardFormatArg(text: TextReference): Any? =
|
||||
(text as TextReference.Res).formatArgs.data.firstOrNull()
|
||||
|
||||
private fun createTransformer(
|
||||
availability: StakingAvailability,
|
||||
entryInfo: StakingEntryInfo?,
|
||||
cryptoCurrencyStatus: CryptoCurrencyStatus = buildStatus(),
|
||||
isBalanceHidden: Boolean = false,
|
||||
) = UpdateStakingNotificationTransformer(
|
||||
cryptoCurrencyStatus = buildStatus(),
|
||||
cryptoCurrencyStatus = cryptoCurrencyStatus,
|
||||
stakingAvailability = availability,
|
||||
stakingEntryInfo = entryInfo,
|
||||
appCurrency = AppCurrency.Default,
|
||||
isBalanceHidden = isBalanceHidden,
|
||||
clickIntents = clickIntents,
|
||||
)
|
||||
|
||||
private fun buildStatus(): CryptoCurrencyStatus {
|
||||
private fun buildStatus(
|
||||
networkRawId: String = "solana",
|
||||
symbol: String = "SOL",
|
||||
isCoin: Boolean = true,
|
||||
stakingBalance: StakingBalance = mockk(relaxed = true),
|
||||
): CryptoCurrencyStatus {
|
||||
val network = mockk<Network>(relaxed = true) {
|
||||
every { rawId } returns "solana"
|
||||
every { rawId } returns networkRawId
|
||||
every { isTestnet } returns false
|
||||
}
|
||||
val currency = mockk<CryptoCurrency.Coin>(relaxed = true) {
|
||||
every { symbol } returns "SOL"
|
||||
every { this@mockk.symbol } returns symbol
|
||||
every { decimals } returns 9
|
||||
every { this@mockk.network } returns network
|
||||
every { id.isCoin } returns true
|
||||
every { id.isCoin } returns isCoin
|
||||
}
|
||||
val stakingBalance = mockk<StakingBalance>(relaxed = true)
|
||||
val value = mockk<CryptoCurrencyStatus.Value>(relaxed = true) {
|
||||
every { this@mockk.stakingBalance } returns stakingBalance
|
||||
every { fiatRate } returns BigDecimal.ONE
|
||||
|
|
@ -115,6 +208,35 @@ class UpdateStakingNotificationTransformerTest {
|
|||
return CryptoCurrencyStatus(currency = currency, value = value)
|
||||
}
|
||||
|
||||
/** Builds an active StakeKit balance with the given [staked] and [rewards] amounts. */
|
||||
private fun stakeKitBalance(staked: BigDecimal, rewards: BigDecimal): StakingBalance.Data.StakeKit {
|
||||
val stakingId = StakingID(integrationId = "ethereum-eth-native-staking", address = "0xabc")
|
||||
return StakingBalance.Data.StakeKit(
|
||||
stakingId = stakingId,
|
||||
source = StatusSource.ACTUAL,
|
||||
balance = YieldBalanceItem(
|
||||
integrationId = stakingId.integrationId,
|
||||
items = listOf(
|
||||
balanceItem(type = BalanceType.STAKED, amount = staked),
|
||||
balanceItem(type = BalanceType.REWARDS, amount = rewards),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun balanceItem(type: BalanceType, amount: BigDecimal): BalanceItem = BalanceItem(
|
||||
groupId = "group",
|
||||
token = YieldToken.ETH,
|
||||
type = type,
|
||||
amount = amount,
|
||||
rawCurrencyId = null,
|
||||
validatorAddress = null,
|
||||
date = null,
|
||||
pendingActions = emptyList(),
|
||||
pendingActionsConstraints = emptyList(),
|
||||
isPending = false,
|
||||
)
|
||||
|
||||
private fun availableOption(apy: BigDecimal): StakingAvailability.Available {
|
||||
val option = mockk<StakingOption>(relaxed = true) {
|
||||
every { this@mockk.apy } returns apy
|
||||
|
|
@ -122,7 +244,7 @@ class UpdateStakingNotificationTransformerTest {
|
|||
return StakingAvailability.Available(option = option)
|
||||
}
|
||||
|
||||
private fun initialState(): TokenDetailsUM = TokenDetailsUM(
|
||||
private fun initialState(isBalanceHidden: Boolean = false): TokenDetailsUM = TokenDetailsUM(
|
||||
topAppBarUM = TokenDetailsTopAppBarUM(
|
||||
titleState = TitleState.Simple(tokenName = "Solana"),
|
||||
subtitle = stringReference("Solana network"),
|
||||
|
|
@ -134,7 +256,7 @@ class UpdateStakingNotificationTransformerTest {
|
|||
earnBlockState = null,
|
||||
marketPriceBlockState = mockk<MarketPriceBlockState>(relaxed = true),
|
||||
pullToRefreshConfig = mockk<PullToRefreshConfig>(relaxed = true),
|
||||
isBalanceHidden = false,
|
||||
isBalanceHidden = isBalanceHidden,
|
||||
isMarketPriceAvailable = false,
|
||||
addFundsUM = AddFundsUM.Loading,
|
||||
transferUM = TransferUM.Loading,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue