Updated on 2026-08-14

This commit is contained in:
Tangem 2026-05-29 14:00:59 +02:00
commit 13091cfffa
284 changed files with 13280 additions and 212 deletions

View file

@ -13,7 +13,6 @@ import com.tangem.features.tangempay.components.cardDetails.DefaultTangemPayCard
import com.tangem.features.tangempay.components.cardDetails.TangemPayCardDetailsBlockComponent
import com.tangem.features.tangempay.model.TangemPayAddToWalletModel
import com.tangem.features.tangempay.ui.TangemPayAddToWalletScreen
import com.tangem.features.tangempay.utils.firstCard
import com.tangem.features.tangempay.utils.userWalletId
internal class TangemPayAddToWalletComponent(
@ -26,7 +25,7 @@ internal class TangemPayAddToWalletComponent(
private val cardDetailsBlockComponent = DefaultTangemPayCardDetailsBlockComponent(
appComponentContext = child("cardDetailsBlockComponent"),
params = TangemPayCardDetailsBlockComponent.Params(
card = params.initialStatus.firstCard(),
initialStatus = params.initialStatus,
userWalletId = params.initialStatus.userWalletId,
isEditingNameEnabled = false,
),

View file

@ -35,7 +35,7 @@ internal class TangemPayCardPageScreenComponent(
private val cardDetailsBlockComponent = DefaultTangemPayCardDetailsBlockComponent(
appComponentContext = child("cardDetailsBlockComponent"),
params = TangemPayCardDetailsBlockComponent.Params(
card = params.initialStatus.firstCard(),
initialStatus = params.initialStatus,
userWalletId = params.initialStatus.userWalletId,
isEditingNameEnabled = true,
),

View file

@ -14,7 +14,6 @@ import com.tangem.features.tangempay.components.cardDetails.TangemPayCardDetails
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.firstCard
import com.tangem.features.tangempay.utils.userWalletId
internal class TangemPayEditDisplayNameComponent(
@ -27,7 +26,7 @@ internal class TangemPayEditDisplayNameComponent(
private val cardDetailsBlockComponent = DefaultTangemPayCardDetailsBlockComponent(
appComponentContext = child("editDisplayNameCardDetails"),
params = TangemPayCardDetailsBlockComponent.Params(
card = params.initialStatus.firstCard(),
initialStatus = params.initialStatus,
userWalletId = params.initialStatus.userWalletId,
isEditingNameEnabled = false,
),

View file

@ -3,7 +3,7 @@ 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.pay.TangemPayCard
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
@ -16,7 +16,7 @@ internal interface TangemPayCardDetailsBlockComponent {
fun CardDetailsBlockContent(state: TangemPayCardDetailsUM, modifier: Modifier)
data class Params(
val card: TangemPayCard,
val initialStatus: AccountStatus.Payment,
val userWalletId: UserWalletId,
val isEditingNameEnabled: Boolean,
)

View file

@ -1,13 +1,18 @@
package com.tangem.features.tangempay.entity
import androidx.annotation.DrawableRes
import androidx.compose.runtime.Immutable
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.features.tangempay.details.impl.R
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import com.tangem.core.ui.R as CoreUiR
@Immutable
internal data class TangemPayCardPageUM(
val settings: ImmutableList<TangemPayCardPageSetting>,
val settingsV2: ImmutableList<TangemPayCardPageSettingV2>,
val onBackClick: () -> Unit,
val dailyLimitState: TangemPayDailyLimitBlockState,
val addToWalletBlockState: AddToWalletBlockState? = null,
@ -15,7 +20,11 @@ internal data class TangemPayCardPageUM(
) {
companion object {
fun stub(
addToWalletBlockState: AddToWalletBlockState? = AddToWalletBlockState(onClick = {}, onClickClose = {}),
addToWalletBlockState: AddToWalletBlockState? = AddToWalletBlockState(
onClick = {},
onClickClose = {},
shouldUseMagicEffect = false,
),
settings: ImmutableList<TangemPayCardPageSetting> = persistentListOf(
TangemPayCardPageSetting(TextReference.Str("Pin Code")) {},
TangemPayCardPageSetting(TextReference.Str("Freeze Card")) {},
@ -23,9 +32,11 @@ internal data class TangemPayCardPageUM(
),
isReissueInProgress: Boolean = false,
dailyLimitState: TangemPayDailyLimitBlockState = TangemPayDailyLimitBlockState.Content.stub(),
settingsV2: ImmutableList<TangemPayCardPageSettingV2> = TangemPayCardPageSettingV2.stubList(),
) = TangemPayCardPageUM(
addToWalletBlockState = addToWalletBlockState,
settings = settings,
settingsV2 = settingsV2,
onBackClick = {},
isReissueInProgress = isReissueInProgress,
dailyLimitState = dailyLimitState,
@ -38,4 +49,49 @@ internal data class TangemPayCardPageSetting(
val title: TextReference,
val testTag: String? = null,
val onSettingClick: () -> Unit,
)
)
@Immutable
internal data class TangemPayCardPageSettingV2(
val id: Id,
val title: TextReference,
val isLoading: Boolean = false,
val isEnabled: Boolean = true,
val testTag: String? = null,
val onClick: () -> Unit,
@param:DrawableRes val iconRes: Int,
) {
enum class Id {
Details, Freeze, ChangePin
}
companion object {
fun stubList(isFrozen: Boolean = false): ImmutableList<TangemPayCardPageSettingV2> = persistentListOf(
TangemPayCardPageSettingV2(
id = Id.Details,
title = resourceReference(R.string.details_title),
onClick = {},
iconRes = CoreUiR.drawable.ic_visa_card_details_24,
),
TangemPayCardPageSettingV2(
id = Id.Freeze,
title = resourceReference(
if (isFrozen) {
R.string.tangem_pay_freeze_card_unfreeze
} else {
R.string.tangem_pay_freeze_card_freeze
},
),
onClick = {},
iconRes = CoreUiR.drawable.ic_freeze_24,
),
TangemPayCardPageSettingV2(
id = Id.ChangePin,
title = resourceReference(R.string.tangem_pay_pin_code_title),
onClick = {},
iconRes = CoreUiR.drawable.ic_card_pin_24,
),
)
}
}

View file

@ -1,5 +1,6 @@
package com.tangem.features.tangempay.entity
import androidx.compose.runtime.Immutable
import androidx.compose.ui.text.input.TextFieldValue
import com.tangem.core.ui.components.buttons.actions.ActionButtonConfig
import com.tangem.core.ui.components.containers.pullToRefresh.PullToRefreshConfig
@ -34,6 +35,7 @@ internal data class TangemPayCardDetailsUM(
val isActionsAvailable: Boolean = false,
)
@Immutable
internal sealed interface DisplayNameState {
val displayName: String
@ -61,6 +63,7 @@ internal sealed interface DisplayNameState {
}
}
@Immutable
internal sealed class TangemPayDetailsBalanceBlockState {
abstract val actionButtons: ImmutableList<ActionButtonConfig>
@ -94,4 +97,5 @@ internal sealed class TangemPayDetailsBalanceBlockState {
internal data class AddToWalletBlockState(
val onClick: () -> Unit,
val onClickClose: () -> Unit,
val shouldUseMagicEffect: Boolean,
)

View file

@ -12,10 +12,7 @@ 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.hasCardWithId
import com.tangem.domain.models.account.requireCardWithId
import com.tangem.domain.models.pay.TangemPayCardFrozenState
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
@ -30,11 +27,14 @@ import com.tangem.features.tangempay.model.transformers.DetailsRevealProgressSta
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 kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
@ -59,14 +59,15 @@ internal class TangemPayCardDetailsBlockModel @Inject constructor(
) : Model() {
private val params: TangemPayCardDetailsBlockComponent.Params = paramsContainer.require()
private val card = params.card
private val initialCard = params.initialStatus.firstCard()
private var frozenStateJob: Job? = null
private val stateFactory = TangemPayCardDetailsBlockStateFactory(
cardNumberEnd = card.lastDigits,
displayName = card.displayName,
cardNumberEnd = initialCard.lastDigits,
displayName = initialCard.displayName,
isEditingNameEnabled = params.isEditingNameEnabled,
onEditNameClick = ::startEditingDisplayName,
onReveal = ::revealCardDetails,
onReveal = ::requestReveal,
onCopy = ::copyData,
)
@ -77,8 +78,7 @@ internal class TangemPayCardDetailsBlockModel @Inject constructor(
private val showCardDetailsTimerJobHolder = JobHolder()
init {
subscribeToCardChanges(cardId = card.id, userWalletId = params.userWalletId)
subscribeToCardFrozenState()
subscribeToCardChanges()
modelScope.launch {
cardDetailsEventListener.event.collectLatest { event ->
when (event) {
@ -89,17 +89,14 @@ internal class TangemPayCardDetailsBlockModel @Inject constructor(
}
}
private fun subscribeToCardChanges(cardId: String, userWalletId: UserWalletId) {
paymentAccountStatusSupplier.invoke(userWalletId)
private fun subscribeToCardChanges() {
paymentAccountStatusSupplier.invoke(params.userWalletId)
.onEach { state ->
val status = state.value
if (status is PaymentAccountStatusValue.Loaded &&
status.source == StatusSource.ACTUAL &&
status.hasCardWithId(cardId)
) {
val card = status.requireCardWithId(cardId)
if (status is PaymentAccountStatusValue.Loaded && status.source == StatusSource.ACTUAL) {
val card = state.findCard(initialCard.id, params.initialStatus) ?: return@onEach
if (card.isReissuing) {
hideCardDetails()
requestHide()
}
card.displayName?.let { uiState.update(TangemPayCardDetailsUpdateNameTransformer(it)) }
uiState.update { uiState ->
@ -109,13 +106,15 @@ internal class TangemPayCardDetailsBlockModel @Inject constructor(
isActionsAvailable = !card.isReissuing,
)
}
subscribeToCardFrozenState(card.id)
}
}
.launchIn(modelScope)
}
private fun subscribeToCardFrozenState() {
cardDetailsRepository.cardFrozenState(card.id)
private fun subscribeToCardFrozenState(cardId: String) {
frozenStateJob?.cancel()
frozenStateJob = cardDetailsRepository.cardFrozenState(cardId)
.onEach { cardFrozenState ->
if (cardFrozenState == TangemPayCardFrozenState.Pending) {
uiState.update { state -> state.copy(cardFrozenState = TangemPayCardFrozenState.Pending) }
@ -124,18 +123,26 @@ internal class TangemPayCardDetailsBlockModel @Inject constructor(
.launchIn(modelScope)
}
private fun requestReveal() {
modelScope.launch { cardDetailsEventListener.send(CardDetailsEvent.Show) }
}
private fun requestHide() {
modelScope.launch { cardDetailsEventListener.send(CardDetailsEvent.Hide) }
}
private fun revealCardDetails() {
analytics.send(TangemPayAnalyticsEvents.ViewCardDetailsClicked())
modelScope.launch {
uiState.transformerUpdate(
transformer = DetailsRevealProgressStateTransformer(onClickHide = ::hideCardDetails),
transformer = DetailsRevealProgressStateTransformer(onClickHide = ::requestHide),
)
cardDetailsRepository.revealCardDetails(params.userWalletId)
.onRight { cardDetails ->
uiState.transformerUpdate(
transformer = DetailsRevealedStateTransformer(
details = cardDetails,
onClickHide = ::hideCardDetails,
onClickHide = ::requestHide,
),
)
launchShowDetailsTimer()
@ -150,11 +157,11 @@ internal class TangemPayCardDetailsBlockModel @Inject constructor(
private fun launchShowDetailsTimer() {
modelScope.launch {
delay(SHOW_DETAILS_TIME)
hideCardDetails()
requestHide()
}.saveIn(showCardDetailsTimerJobHolder)
}
fun hideCardDetails() {
private fun hideCardDetails() {
revealCardDetailsJobHolder.cancel()
uiState.transformerUpdate(transformer = DetailsHiddenStateTransformer(stateFactory))
}

View file

@ -12,6 +12,7 @@ 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.DesignFeatureToggles
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.format.bigdecimal.fiat
@ -23,8 +24,6 @@ import com.tangem.core.ui.test.TangemPayTestTags
import com.tangem.domain.models.StatusSource
import com.tangem.domain.models.TokenReceiveConfig
import com.tangem.domain.models.account.PaymentAccountStatusValue
import com.tangem.domain.models.account.hasCardWithId
import com.tangem.domain.models.account.requireCardWithId
import com.tangem.domain.models.pay.TangemPayCard
import com.tangem.domain.models.pay.TangemPayCardLimitPeriod
import com.tangem.domain.models.pay.isFrozen
@ -39,19 +38,20 @@ 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.listener.CardDetailsEvent
import com.tangem.features.tangempay.model.listener.CardDetailsEventListener
import com.tangem.features.tangempay.navigation.TangemPayCardDetailsInnerRoute
import com.tangem.features.tangempay.utils.TangemPayMessagesFactory
import com.tangem.features.tangempay.utils.cryptoCurrency
import com.tangem.features.tangempay.utils.firstCard
import com.tangem.features.tangempay.utils.userWalletId
import com.tangem.features.tangempay.utils.*
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.flow.*
import kotlinx.coroutines.launch
import javax.inject.Inject
import com.tangem.core.ui.R as CoreUiR
@Suppress("LongParameterList")
@Stable
@ -65,23 +65,30 @@ internal class TangemPayCardPageModel @Inject constructor(
private val cardDetailsRepository: TangemPayCardDetailsRepository,
private val uiMessageSender: UiMessageSender,
private val changeCardFrozenStateUseCase: ChangeCardFrozenStateUseCase,
private val designFeatureToggles: DesignFeatureToggles,
private val cardDetailsEventListener: CardDetailsEventListener,
) : Model(), ViewPinListener, ReissueCardListener, AddFundsListener {
private val params: TangemPayCardPageComponent.Params = paramsContainer.require()
private val cardId: String = params.initialStatus.firstCard().id
private val userWalletId = params.initialStatus.userWalletId
private val cryptoCurrency = params.initialStatus.cryptoCurrency
private val addToWalletBannerJobHolder = JobHolder()
private val addFundsJobHolder = JobHolder()
private val frozenStateJobHolder = JobHolder()
private val currentStatus = MutableStateFlow(params.initialStatus)
private val initialCardId = params.initialStatus.firstCard().id
private val userWalletId = currentStatus.value.userWalletId
private val cryptoCurrency
get() = currentStatus.value.cryptoCurrency
val uiState: StateFlow<TangemPayCardPageUM>
field = MutableStateFlow(
TangemPayCardPageUM(
onBackClick = router::pop,
dailyLimitState = TangemPayDailyLimitBlockState.Loading,
settings = persistentListOf(),
settingsV2 = persistentListOf(),
),
)
@ -90,15 +97,14 @@ internal class TangemPayCardPageModel @Inject constructor(
init {
analytics.send(TangemPayAnalyticsEvents.CardManagementScreenOpened())
fetchAddToWalletBanner()
modelScope.launch { subscribeOnDetailsState() }
paymentAccountStatusSupplier.invoke(userWalletId)
.onEach { state ->
currentStatus.update { state }
val status = state.value
if (status is PaymentAccountStatusValue.Loaded &&
status.source == StatusSource.ACTUAL &&
status.hasCardWithId(cardId)
) {
val card = status.requireCardWithId(cardId)
if (status is PaymentAccountStatusValue.Loaded && status.source == StatusSource.ACTUAL) {
val card = state.findCard(initialCardId, params.initialStatus) ?: return@onEach
val limit = card.limit?.actualCardLimit?.takeIf { it.period == TangemPayCardLimitPeriod.DAY }
val dailyLimitState = if (limit != null) {
TangemPayDailyLimitBlockState.Content(
@ -115,6 +121,7 @@ internal class TangemPayCardPageModel @Inject constructor(
uiState.copy(
dailyLimitState = dailyLimitState,
settings = buildSettings(card),
settingsV2 = buildSettingsV2(card),
isReissueInProgress = card.isReissuing,
)
}
@ -126,6 +133,7 @@ internal class TangemPayCardPageModel @Inject constructor(
}
private fun buildSettings(card: TangemPayCard): ImmutableList<TangemPayCardPageSetting> {
if (designFeatureToggles.isRedesignEnabled) return persistentListOf()
return persistentListOf(
TangemPayCardPageSetting(
title = TextReference.Res(R.string.tangempay_card_details_change_pin),
@ -150,6 +158,64 @@ internal class TangemPayCardPageModel @Inject constructor(
)
}
private suspend fun subscribeOnDetailsState() {
if (!designFeatureToggles.isRedesignEnabled) return
cardDetailsEventListener.event.collect { event ->
val isDetailsShown = event == CardDetailsEvent.Show
uiState.update { state ->
state.copy(
settingsV2 = state.settingsV2
.map { setting ->
if (setting.id == TangemPayCardPageSettingV2.Id.Details) {
setting.copy(isEnabled = !isDetailsShown)
} else {
setting
}
}
.toImmutableList(),
)
}
}
}
private fun buildSettingsV2(card: TangemPayCard): ImmutableList<TangemPayCardPageSettingV2> {
if (!designFeatureToggles.isRedesignEnabled) return persistentListOf()
return persistentListOf(
TangemPayCardPageSettingV2(
id = TangemPayCardPageSettingV2.Id.Details,
title = TextReference.Res(R.string.details_title),
onClick = ::onClickViewDetails,
iconRes = CoreUiR.drawable.ic_visa_card_details_24,
),
TangemPayCardPageSettingV2(
id = TangemPayCardPageSettingV2.Id.Freeze,
title = TextReference.Res(
if (card.isFrozen) {
R.string.tangem_pay_freeze_card_unfreeze
} else {
R.string.tangem_pay_freeze_card_freeze
},
),
onClick = { onClickFreezeOrUnfreezeCard(card.isFrozen) },
iconRes = CoreUiR.drawable.ic_freeze_24,
testTag = TangemPayTestTags.FREEZE_CARD_ROW,
),
TangemPayCardPageSettingV2(
id = TangemPayCardPageSettingV2.Id.ChangePin,
title = TextReference.Res(R.string.tangempay_card_details_change_pin),
onClick = { onClickChangePIN(card.hasPinCode) },
iconRes = CoreUiR.drawable.ic_card_pin_24,
testTag = TangemPayTestTags.CHANGE_PIN_ROW,
),
)
}
private fun onClickViewDetails() {
modelScope.launch(dispatchers.default) {
cardDetailsEventListener.send(CardDetailsEvent.Show)
}
}
private fun onClickLimitChange() {
analytics.send(TangemPayAnalyticsEvents.LimitChangeClicked())
router.push(TangemPayCardDetailsInnerRoute.LimitSetup)
@ -159,10 +225,11 @@ internal class TangemPayCardPageModel @Inject constructor(
if (!isPinSet) {
router.push(TangemPayCardDetailsInnerRoute.ChangePIN)
} else {
val card = currentStatus.value.findCard(initialCardId, params.initialStatus) ?: return
bottomSheetNavigation.activate(
TangemPayCardNavigation.ViewPinCode(
userWalletId = userWalletId,
cardId = cardId,
cardId = card.id,
),
)
}
@ -244,10 +311,11 @@ internal class TangemPayCardPageModel @Inject constructor(
}
private fun freezeCard() {
val card = currentStatus.value.findCard(initialCardId, params.initialStatus) ?: return
modelScope.launch {
changeCardFrozenStateUseCase(
userWalletId = userWalletId,
cardId = cardId,
cardId = card.id,
isFreezing = true,
).onLeft {
val message = SnackbarMessage(resourceReference(R.string.tangem_pay_freeze_card_failed))
@ -260,10 +328,11 @@ internal class TangemPayCardPageModel @Inject constructor(
}
private fun unfreezeCard() {
val card = currentStatus.value.findCard(initialCardId, params.initialStatus) ?: return
modelScope.launch {
changeCardFrozenStateUseCase(
userWalletId = userWalletId,
cardId = cardId,
cardId = card.id,
isFreezing = false,
).onLeft {
val message = SnackbarMessage(resourceReference(R.string.tangem_pay_unfreeze_card_failed))
@ -284,6 +353,7 @@ internal class TangemPayCardPageModel @Inject constructor(
addToWalletBlockState = AddToWalletBlockState(
onClick = ::onClickAddToWallet,
onClickClose = ::onClickCloseBanner,
shouldUseMagicEffect = false,
),
)
}

View file

@ -15,7 +15,11 @@ internal class DetailsAddToWalletBannerTransformer(
addToWalletBlockState = if (isDone) {
null
} else {
AddToWalletBlockState(onClick = onClickBanner, onClickClose = onClickCloseBanner)
AddToWalletBlockState(
onClick = onClickBanner,
onClickClose = onClickCloseBanner,
shouldUseMagicEffect = true,
)
},
)
}

View file

@ -18,7 +18,12 @@ import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.ds.message.TangemMessage
import com.tangem.core.ui.ds.message.TangemMessageEffect
import com.tangem.core.ui.extensions.clickableSingle
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.LocalRedesignEnabled
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.features.tangempay.details.impl.R
@ -32,6 +37,15 @@ private const val GRADIENT_RADIUS = 200F
@Composable
internal fun TangemPayAddToWalletBlock(state: AddToWalletBlockState, modifier: Modifier = Modifier) {
if (LocalRedesignEnabled.current) {
TangemPayAddToWalletBlockV2(state = state, modifier = modifier)
} else {
TangemPayAddToWalletBlockV1(state = state, modifier = modifier)
}
}
@Composable
internal fun TangemPayAddToWalletBlockV1(state: AddToWalletBlockState, modifier: Modifier = Modifier) {
Box(
modifier = modifier
.fillMaxWidth()
@ -55,7 +69,9 @@ internal fun TangemPayAddToWalletBlock(state: AddToWalletBlockState, modifier: M
Image(
painter = painterResource(R.drawable.img_google_wallet_48),
contentDescription = null,
modifier = Modifier.size(36.dp).clip(CircleShape),
modifier = Modifier
.size(36.dp)
.clip(CircleShape),
)
Spacer(Modifier.width(12.dp))
@ -97,11 +113,26 @@ internal fun TangemPayAddToWalletBlock(state: AddToWalletBlockState, modifier: M
}
}
@Composable
internal fun TangemPayAddToWalletBlockV2(state: AddToWalletBlockState, modifier: Modifier = Modifier) {
TangemMessage(
modifier = modifier.clickableSingle(onClick = state.onClick),
onCloseClick = state.onClickClose,
title = resourceReference(R.string.tangempay_card_details_open_wallet_notification_title),
subtitle = resourceReference(R.string.tangempay_card_details_open_wallet_notification_subtitle),
messageEffect = if (state.shouldUseMagicEffect) {
TangemMessageEffect.Magic
} else {
TangemMessageEffect.None
},
)
}
@Preview(showBackground = true)
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun PreviewTangemPayAddToWalletBlock() {
TangemThemePreview {
TangemPayAddToWalletBlock(AddToWalletBlockState({}, {}))
TangemPayAddToWalletBlock(AddToWalletBlockState(onClick = {}, onClickClose = {}, shouldUseMagicEffect = false))
}
}

View file

@ -1,5 +1,6 @@
package com.tangem.features.tangempay.ui
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.EaseInOut
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.tween
@ -13,10 +14,7 @@ import androidx.compose.foundation.text.BasicTextField
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
@ -26,9 +24,11 @@ import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.text.TextRange
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.TextFieldValue
@ -37,18 +37,20 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
import androidx.compose.ui.unit.dp
import androidx.constraintlayout.compose.ConstrainedLayoutReference
import androidx.constraintlayout.compose.ConstraintLayout
import androidx.constraintlayout.compose.ConstraintLayoutScope
import androidx.constraintlayout.compose.Dimension
import com.tangem.core.ui.components.SpacerW4
import com.tangem.core.ui.components.SpacerWMax
import com.tangem.core.ui.components.buttons.common.TangemButton
import com.tangem.core.ui.components.buttons.common.TangemButtonIconPosition
import com.tangem.core.ui.components.buttons.common.TangemButtonSize
import com.tangem.core.ui.ds.image.TangemIconUM
import com.tangem.core.ui.ds2.button.TangemButton
import com.tangem.core.ui.extensions.conditional
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.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
@ -129,23 +131,7 @@ private fun TangemPayCardDetailsHiddenBlock(state: TangemPayCardDetailsUM, modif
painter = painterResource(id = imageResId),
contentDescription = null,
)
Row(
modifier = Modifier.padding(16.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Icon(
painter = painterResource(R.drawable.ic_cloud_fill_16),
tint = TangemTheme.colors.icon.constant,
contentDescription = null,
)
SpacerW4()
Text(
text = stringResourceSafe(R.string.tangempay_digital_card),
style = TangemTheme.typography.subtitle2,
color = TangemTheme.colors.text.constantWhite,
)
}
CardTopBlock()
if (state.isActionsAvailable) {
ConstraintLayout(
@ -167,18 +153,11 @@ private fun TangemPayCardDetailsHiddenBlock(state: TangemPayCardDetailsUM, modif
},
)
}
Text(
text = state.numberShort,
style = TangemTheme.typography.subtitle2,
color = TangemTheme.colors.text.constantWhite,
modifier = Modifier
.constrainAs(cardNumberRef) {
start.linkTo(parent.start)
bottom.linkTo(parent.bottom)
}
.padding(bottom = 8.dp),
CardNumberBlock(
numberShort = state.numberShort,
cardNumberRef = cardNumberRef,
)
when (state.cardFrozenState) {
TangemPayCardFrozenState.Frozen -> Icon(
modifier = Modifier
@ -209,23 +188,93 @@ private fun TangemPayCardDetailsHiddenBlock(state: TangemPayCardDetailsUM, modif
)
TangemPayCardFrozenState.Unfrozen -> Unit
}
TangemPayCardDetailsCustomButton(
AnimatedVisibility(
modifier = Modifier
.constrainAs(buttonRef) {
end.linkTo(parent.end)
bottom.linkTo(parent.bottom)
}
.testTag(TangemPayTestTags.CARD_DETAILS_SHOW_BUTTON),
text = stringResourceSafe(id = R.string.tangempay_card_details_show_details),
onClick = state.onClick,
showProgress = state.isLoading,
)
visible = !LocalRedesignEnabled.current || state.isLoading,
) {
TangemPayCardDetailsCustomButton(
text = stringResourceSafe(id = R.string.tangempay_card_details_show_details),
onClick = state.onClick,
showProgress = state.isLoading,
)
}
}
}
}
}
@Composable
private fun CardTopBlock(modifier: Modifier = Modifier) {
Row(
modifier = modifier.padding(16.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(4.dp),
) {
if (LocalRedesignEnabled.current) {
Text(
text = stringResourceSafe(R.string.tangempay_digital_card),
style = TangemTheme.typography3.body.medium,
color = TangemTheme.colors3.text.staticDark.primary,
)
Icon(
modifier = Modifier.size(TangemTheme.dimens2.x5),
imageVector = ImageVector.vectorResource(R.drawable.ic_cloud_fill_16),
tint = TangemTheme.colors3.icon.staticDark,
contentDescription = null,
)
} else {
Icon(
painter = painterResource(R.drawable.ic_cloud_fill_16),
tint = TangemTheme.colors.icon.constant,
contentDescription = null,
)
Text(
text = stringResourceSafe(R.string.tangempay_digital_card),
style = TangemTheme.typography.subtitle2,
color = TangemTheme.colors.text.constantWhite,
)
}
}
}
@Composable
private fun ConstraintLayoutScope.CardNumberBlock(
numberShort: String,
cardNumberRef: ConstrainedLayoutReference,
modifier: Modifier = Modifier,
) {
if (LocalRedesignEnabled.current) {
Text(
text = numberShort,
style = TangemTheme.typography3.body.medium,
color = TangemTheme.colors3.text.staticDark.primary,
modifier = modifier
.constrainAs(cardNumberRef) {
start.linkTo(parent.start)
bottom.linkTo(parent.bottom)
}
.padding(bottom = 8.dp),
)
} else {
Text(
text = numberShort,
style = TangemTheme.typography.subtitle2,
color = TangemTheme.colors.text.constantWhite,
modifier = modifier
.constrainAs(cardNumberRef) {
start.linkTo(parent.start)
bottom.linkTo(parent.bottom)
}
.padding(bottom = 8.dp),
)
}
}
@Composable
private fun CardDisplayName(state: DisplayNameState, modifier: Modifier = Modifier) {
when (state) {
@ -236,36 +285,67 @@ private fun CardDisplayName(state: DisplayNameState, modifier: Modifier = Modifi
@Composable
private fun DisplayOnlyCardDisplayName(state: DisplayNameState.Display, modifier: Modifier = Modifier) {
Row(
modifier = modifier.conditional(
condition = state.isEditingEnabled,
modifier = { clickable(onClick = state.onClick) },
),
horizontalArrangement = Arrangement.spacedBy(6.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Text(
text = state.displayName,
style = TangemTheme.typography.caption1.copy(color = TangemTheme.colors.text.constantWhite),
maxLines = 1,
)
if (state.isEditingEnabled) {
Icon(
painter = painterResource(id = R.drawable.ic_edit_new_12),
contentDescription = null,
modifier = Modifier.size(10.dp),
tint = TangemTheme.colors.text.constantWhite,
if (LocalRedesignEnabled.current) {
Row(
modifier = modifier.conditional(
condition = state.isEditingEnabled,
modifier = { clickable(onClick = state.onClick) },
),
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens2.x1),
verticalAlignment = Alignment.CenterVertically,
) {
Text(
text = state.displayName,
style = TangemTheme.typography3.body.medium,
color = TangemTheme.colors3.text.staticDark.secondary,
maxLines = 1,
)
if (state.isEditingEnabled) {
Icon(
imageVector = ImageVector.vectorResource(id = R.drawable.ic_edit_card_20),
contentDescription = null,
modifier = Modifier.size(TangemTheme.dimens2.x5),
tint = TangemTheme.colors3.icon.staticDark,
)
}
}
} else {
Row(
modifier = modifier.conditional(
condition = state.isEditingEnabled,
modifier = { clickable(onClick = state.onClick) },
),
horizontalArrangement = Arrangement.spacedBy(6.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Text(
text = state.displayName,
style = TangemTheme.typography.caption1.copy(color = TangemTheme.colors.text.constantWhite),
maxLines = 1,
)
if (state.isEditingEnabled) {
Icon(
painter = painterResource(id = R.drawable.ic_edit_new_12),
contentDescription = null,
modifier = Modifier.size(10.dp),
tint = TangemTheme.colors.text.constantWhite,
)
}
}
}
}
@Composable
private fun EditingCardDisplayName(state: DisplayNameState.Editing, modifier: Modifier = Modifier) {
val isRedesignEnabled = LocalRedesignEnabled.current
val focusRequester = remember { FocusRequester() }
val placeholder = stringResourceSafe(R.string.tangempay_card_edit_name_placeholder)
val textStyle = TangemTheme.typography.caption1.copy(color = TangemTheme.colors.text.constantWhite)
val textStyle = if (isRedesignEnabled) {
TangemTheme.typography3.body.medium.copy(color = TangemTheme.colors3.text.staticDark.primary)
} else {
TangemTheme.typography.caption1.copy(color = TangemTheme.colors.text.constantWhite)
}
val textMeasurer = rememberTextMeasurer()
val measuredText = state.editingValue.text.ifEmpty { placeholder }
val textWidthDp = with(LocalDensity.current) {
@ -280,7 +360,11 @@ private fun EditingCardDisplayName(state: DisplayNameState.Editing, modifier: Mo
.focusRequester(focusRequester),
textStyle = textStyle,
singleLine = true,
cursorBrush = SolidColor(TangemTheme.colors.text.constantWhite),
cursorBrush = if (isRedesignEnabled) {
SolidColor(TangemTheme.colors3.text.staticDark.primary)
} else {
SolidColor(TangemTheme.colors.text.constantWhite)
},
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done),
keyboardActions = KeyboardActions(
onDone = if (state.isSubmitEnabled) {
@ -294,7 +378,13 @@ private fun EditingCardDisplayName(state: DisplayNameState.Editing, modifier: Mo
if (state.editingValue.text.isEmpty()) {
Text(
text = placeholder,
style = textStyle.copy(color = TangemTheme.colors.text.tertiary),
style = textStyle.copy(
color = if (isRedesignEnabled) {
TangemTheme.colors3.text.staticDark.secondary
} else {
TangemTheme.colors.text.tertiary
},
),
)
}
innerTextField()
@ -317,10 +407,7 @@ private fun TangemPayCardDetailsShownBlock(
onHideDetails: () -> Unit,
modifier: Modifier = Modifier,
) {
Column(
modifier = modifier
.fillMaxSize(),
) {
Column(modifier = modifier.fillMaxSize()) {
CardDetailsTextContainer(
modifier = Modifier
.padding(top = 16.dp, bottom = 12.dp, start = 16.dp, end = 16.dp)
@ -361,14 +448,31 @@ private fun TangemPayCardDetailsShownBlock(
Spacer(modifier = Modifier.weight(1f))
Row {
SpacerWMax()
TangemPayCardDetailsCustomButton(
modifier = Modifier
.padding(end = 16.dp, bottom = 8.dp)
.testTag(TangemPayTestTags.CARD_DETAILS_HIDE_BUTTON),
text = stringResourceSafe(id = R.string.tangempay_card_details_hide_details),
onClick = onHideDetails,
showProgress = false,
)
if (LocalRedesignEnabled.current) {
// Must use dark theme locally for button cause card is dark
CompositionLocalProvider(LocalIsInDarkTheme provides true) {
TangemThemeRedesign {
TangemButton(
modifier = Modifier
.padding(end = 16.dp, bottom = 8.dp)
.testTag(TangemPayTestTags.CARD_DETAILS_HIDE_BUTTON),
variant = TangemButton.Variant.Material,
size = TangemButton.Size.X8,
onClick = onHideDetails,
iconStart = TangemIconUM.Icon(iconRes = R.drawable.ic_close_20),
)
}
}
} else {
TangemPayCardDetailsCustomButton(
modifier = Modifier
.padding(end = 16.dp, bottom = 8.dp)
.testTag(TangemPayTestTags.CARD_DETAILS_HIDE_BUTTON),
text = stringResourceSafe(id = R.string.tangempay_card_details_hide_details),
onClick = onHideDetails,
showProgress = false,
)
}
}
}
}

View file

@ -16,6 +16,7 @@ import androidx.compose.material3.Scaffold
import androidx.compose.material3.ScaffoldDefaults
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@ -26,8 +27,10 @@ import androidx.compose.ui.util.fastForEach
import com.tangem.core.ui.components.appbar.AppBarWithBackButton
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.LocalRedesignEnabled
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.res.TangemThemePreviewRedesign
import com.tangem.domain.models.pay.TangemPayCardFrozenState
import com.tangem.features.tangempay.components.cardDetails.PreviewTangemPayCardDetailsBlockComponent
import com.tangem.features.tangempay.components.cardDetails.TangemPayCardDetailsBlockComponent
@ -44,6 +47,7 @@ internal fun TangemPayCardPageScreen(
cardDetailsState: TangemPayCardDetailsUM,
modifier: Modifier = Modifier,
) {
val isRedesignEnabled = LocalRedesignEnabled.current
Scaffold(
modifier = modifier,
topBar = {
@ -53,7 +57,11 @@ internal fun TangemPayCardPageScreen(
)
},
contentWindowInsets = ScaffoldDefaults.contentWindowInsets.exclude(WindowInsets.navigationBars),
containerColor = TangemTheme.colors.background.secondary,
containerColor = if (isRedesignEnabled) {
TangemTheme.colors3.bg.primary
} else {
TangemTheme.colors.background.secondary
},
) { scaffoldPaddings ->
val bottomBarHeight = with(LocalDensity.current) { WindowInsets.systemBars.getBottom(this).toDp() }
LazyColumn(
@ -73,6 +81,14 @@ internal fun TangemPayCardPageScreen(
state = cardDetailsState,
)
}
if (isRedesignEnabled && state.settingsV2.isNotEmpty()) {
cardPageItem("Settings buttons") {
TangemPayCardPageSettingsButtonsBlock(
modifier = Modifier.fillMaxWidth(),
settings = state.settingsV2,
)
}
}
if (state.isReissueInProgress) {
cardPageItem(key = "Reissue") {
TangemPayReplacingCardBlock()
@ -104,6 +120,7 @@ private fun TangemPayCardPageSettingsBlock(
settings: ImmutableList<TangemPayCardPageSetting>,
modifier: Modifier = Modifier,
) {
if (LocalRedesignEnabled.current) return
Column(
modifier = modifier
.fillMaxWidth()
@ -176,11 +193,27 @@ private fun LazyListScope.cardPageItem(
@Preview
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun preview() = TangemThemePreview {
TangemPayCardPageScreen(
state = TangemPayCardPageUM.stub(),
cardDetailsBlockComponent = PreviewTangemPayCardDetailsBlockComponent(
TangemPayCardDetailsUM(
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(
number = "•••• •••• •••• 1245",
numberShort = "··1245",
expiry = "••/••",
@ -191,23 +224,52 @@ private fun preview() = TangemThemePreview {
displayNameState = DisplayNameState.Display(
displayName = "Tangem Pay Card",
onClick = {},
isEditingEnabled = true,
isEditingEnabled = false,
),
),
),
cardDetailsState = TangemPayCardDetailsUM(
number = "•••• •••• •••• 1245",
numberShort = "··1245",
expiry = "••/••",
cvv = "•••",
onCopy = { _, _ -> },
onClick = {},
cardFrozenState = TangemPayCardFrozenState.Unfrozen,
displayNameState = DisplayNameState.Display(
displayName = "Tangem Pay Card",
onClick = {},
isEditingEnabled = false,
),
),
)
)
}
}
@Preview
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun TangemPayCardPageScreenPreviewV2() {
TangemThemePreviewRedesign {
CompositionLocalProvider(LocalRedesignEnabled provides true) {
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,
),
),
)
}
}
}

View file

@ -0,0 +1,94 @@
package com.tangem.features.tangempay.ui
import android.content.res.Configuration
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.util.fastForEach
import com.tangem.core.ui.ds.image.TangemIconUM
import com.tangem.core.ui.ds2.button.TangemButton
import com.tangem.core.ui.extensions.resolveAnnotatedReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreviewRedesign
import com.tangem.features.tangempay.entity.TangemPayCardPageSettingV2
import kotlinx.collections.immutable.ImmutableList
@Composable
internal fun TangemPayCardPageSettingsButtonsBlock(
settings: ImmutableList<TangemPayCardPageSettingV2>,
modifier: Modifier = Modifier,
) {
if (settings.isEmpty()) return
Row(
modifier = modifier.padding(vertical = TangemTheme.dimens2.x6),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center,
) {
settings.fastForEach { setting ->
TangemPaySettingButton(
setting = setting,
modifier = Modifier.then(if (setting.testTag != null) Modifier.testTag(setting.testTag) else Modifier),
)
}
}
}
@Composable
private fun TangemPaySettingButton(setting: TangemPayCardPageSettingV2, modifier: Modifier = Modifier) {
Column(
modifier = modifier.padding(horizontal = TangemTheme.dimens2.x6),
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens2.x2),
horizontalAlignment = Alignment.CenterHorizontally,
) {
TangemButton(
variant = TangemButton.Variant.Material,
size = TangemButton.Size.X14,
onClick = setting.onClick,
iconStart = TangemIconUM.Icon(
iconRes = setting.iconRes,
tintReference = { TangemTheme.colors3.icon.primary },
),
isLoading = setting.isLoading,
isEnabled = setting.isEnabled,
)
Text(
text = setting.title.resolveAnnotatedReference(),
style = TangemTheme.typography3.subheading.medium,
color = TangemTheme.colors3.text.primary,
)
}
}
@Preview(showBackground = true)
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun TangemPayCardPageSettingsButtonsBlockPreview() {
TangemThemePreviewRedesign {
TangemPayCardPageSettingsButtonsBlock(
modifier = Modifier
.fillMaxWidth()
.background(TangemTheme.colors3.bg.secondary),
settings = TangemPayCardPageSettingV2.stubList(),
)
}
}
@Preview(showBackground = true, name = "Frozen card")
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES, name = "Frozen card — dark")
@Composable
private fun TangemPayCardPageSettingsButtonsBlockFrozenPreview() {
TangemThemePreviewRedesign {
TangemPayCardPageSettingsButtonsBlock(
modifier = Modifier
.fillMaxWidth()
.background(TangemTheme.colors3.bg.secondary),
settings = TangemPayCardPageSettingV2.stubList(isFrozen = true),
)
}
}

View file

@ -4,6 +4,7 @@ import android.content.res.Configuration
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.LocalMinimumInteractiveComponentSize
import androidx.compose.material3.Text
@ -11,7 +12,11 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
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.layout.layoutId
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
@ -19,15 +24,31 @@ import com.tangem.core.ui.components.*
import com.tangem.core.ui.components.buttons.common.TangemButtonSize
import com.tangem.core.ui.components.notifications.Notification
import com.tangem.core.ui.components.notifications.NotificationConfig
import com.tangem.core.ui.ds.row.TangemRowContainer
import com.tangem.core.ui.ds.row.TangemRowLayoutId
import com.tangem.core.ui.ds2.button.TangemButton
import com.tangem.core.ui.ds2.shimmers.TextShimmer
import com.tangem.core.ui.ds2.shimmers.TextShimmerStyle
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.LocalRedesignEnabled
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.res.TangemThemePreviewRedesign
import com.tangem.features.tangempay.details.impl.R
import com.tangem.features.tangempay.entity.TangemPayDailyLimitBlockState
@Composable
internal fun TangemPayDailyLimitBlock(state: TangemPayDailyLimitBlockState, modifier: Modifier = Modifier) {
if (LocalRedesignEnabled.current) {
CurrentLimitBlockV2(state, modifier)
} else {
TangemPayDailyLimitBlockV1(state, modifier)
}
}
@Composable
private fun TangemPayDailyLimitBlockV1(state: TangemPayDailyLimitBlockState, modifier: Modifier = Modifier) {
Column(
modifier = modifier
.fillMaxWidth()
@ -43,12 +64,12 @@ internal fun TangemPayDailyLimitBlock(state: TangemPayDailyLimitBlockState, modi
color = TangemTheme.colors.text.tertiary,
)
SpacerH4()
CurrentLimitBlock(state)
CurrentLimitBlockV1(state)
}
}
@Composable
private fun CurrentLimitBlock(state: TangemPayDailyLimitBlockState) {
private fun CurrentLimitBlockV1(state: TangemPayDailyLimitBlockState) {
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
@ -107,8 +128,147 @@ private fun CurrentLimitBlock(state: TangemPayDailyLimitBlockState) {
}
}
@Composable
private fun CurrentLimitBlockV2(state: TangemPayDailyLimitBlockState, modifier: Modifier = Modifier) {
TangemRowContainer(
modifier = modifier
.clip(RoundedCornerShape(TangemTheme.dimens2.x6))
.background(color = TangemTheme.colors3.bg.secondary),
contentPadding = PaddingValues(TangemTheme.dimens2.x4),
) {
LimitHeadIcon(
modifier = Modifier.layoutId(TangemRowLayoutId.HEAD),
state = state,
)
TitleLimit(
modifier = Modifier
.padding(start = TangemTheme.dimens2.x3)
.layoutId(TangemRowLayoutId.START_TOP),
state = state,
)
SubtitleLimit(
modifier = Modifier
.padding(start = TangemTheme.dimens2.x3)
.layoutId(TangemRowLayoutId.START_BOTTOM),
state = state,
)
if (state is TangemPayDailyLimitBlockState.Content) {
CompositionLocalProvider(LocalMinimumInteractiveComponentSize provides 0.dp) {
TangemButton(
modifier = Modifier
.padding(start = TangemTheme.dimens2.x3)
.layoutId(TangemRowLayoutId.TAIL),
variant = TangemButton.Variant.Secondary,
text = resourceReference(R.string.common_edit),
onClick = state.onChangeClick,
size = TangemButton.Size.X10,
)
}
}
}
}
@Composable
private fun LimitHeadIcon(state: TangemPayDailyLimitBlockState, modifier: Modifier = Modifier) {
Box(
modifier = modifier
.size(TangemTheme.dimens2.x10)
.background(
color = when (state) {
is TangemPayDailyLimitBlockState.Content,
TangemPayDailyLimitBlockState.Loading,
-> TangemTheme.colors3.bg.status.infoSubtle
TangemPayDailyLimitBlockState.Error -> TangemTheme.colors3.bg.status.warningSubtle
},
shape = CircleShape,
),
contentAlignment = Alignment.Center,
) {
when (state) {
is TangemPayDailyLimitBlockState.Content,
TangemPayDailyLimitBlockState.Loading,
-> {
Icon(
imageVector = ImageVector.vectorResource(R.drawable.ic_limit_new_20),
contentDescription = null,
tint = TangemTheme.colors3.icon.brand,
)
}
TangemPayDailyLimitBlockState.Error -> {
Icon(
imageVector = ImageVector.vectorResource(R.drawable.ic_warning_20),
contentDescription = null,
tint = TangemTheme.colors3.icon.status.warning,
)
}
}
}
}
@Composable
private fun TitleLimit(state: TangemPayDailyLimitBlockState, modifier: Modifier = Modifier) {
when (state) {
is TangemPayDailyLimitBlockState.Content,
TangemPayDailyLimitBlockState.Loading,
-> {
Text(
modifier = modifier,
text = stringResourceSafe(R.string.tangempay_card_page_daily_limit_title),
style = TangemTheme.typography3.body.medium,
color = TangemTheme.colors3.text.secondary,
)
}
TangemPayDailyLimitBlockState.Error -> {
Text(
modifier = modifier,
text = stringResourceSafe(R.string.tangempay_card_page_daily_limit_error_title),
style = TangemTheme.typography3.body.medium,
color = TangemTheme.colors3.text.primary,
)
}
}
}
@Composable
private fun SubtitleLimit(state: TangemPayDailyLimitBlockState, modifier: Modifier = Modifier) {
when (state) {
TangemPayDailyLimitBlockState.Error -> {
Text(
modifier = modifier,
text = stringResourceSafe(R.string.tangempay_card_page_daily_limit_error_subtitle),
style = TangemTheme.typography3.body.medium,
color = TangemTheme.colors3.text.secondary,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
}
is TangemPayDailyLimitBlockState.Content -> {
Text(
modifier = modifier,
text = state.limit,
style = TangemTheme.typography3.body.medium,
color = TangemTheme.colors3.text.primary,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
}
TangemPayDailyLimitBlockState.Loading -> {
TextShimmer(
radius = TangemTheme.dimens2.x25,
modifier = modifier,
style = TextShimmerStyle.BODY,
text = "$50,000",
)
}
}
}
@Composable
internal fun TangemPayDailyLimitErrorBlock(modifier: Modifier = Modifier) {
if (LocalRedesignEnabled.current) return
Notification(
config = NotificationConfig(
title = resourceReference(R.string.tangempay_card_page_daily_limit_error_title),
@ -123,13 +283,33 @@ internal fun TangemPayDailyLimitErrorBlock(modifier: Modifier = Modifier) {
@Preview
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun preview() = TangemThemePreview {
Column(
verticalArrangement = Arrangement.spacedBy(16.dp),
) {
TangemPayDailyLimitBlock(state = TangemPayDailyLimitBlockState.Content.stub())
TangemPayDailyLimitBlock(state = TangemPayDailyLimitBlockState.Error)
TangemPayDailyLimitBlock(state = TangemPayDailyLimitBlockState.Loading)
TangemPayDailyLimitErrorBlock()
private fun Preview() {
TangemThemePreview {
Column(
verticalArrangement = Arrangement.spacedBy(16.dp),
) {
TangemPayDailyLimitBlock(state = TangemPayDailyLimitBlockState.Content.stub())
TangemPayDailyLimitBlock(state = TangemPayDailyLimitBlockState.Error)
TangemPayDailyLimitBlock(state = TangemPayDailyLimitBlockState.Loading)
TangemPayDailyLimitErrorBlock()
}
}
}
@Preview
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun PreviewV2() {
TangemThemePreviewRedesign {
CompositionLocalProvider(LocalRedesignEnabled provides true) {
Column(
verticalArrangement = Arrangement.spacedBy(16.dp),
) {
TangemPayDailyLimitBlock(state = TangemPayDailyLimitBlockState.Content.stub())
TangemPayDailyLimitBlock(state = TangemPayDailyLimitBlockState.Error)
TangemPayDailyLimitBlock(state = TangemPayDailyLimitBlockState.Loading)
TangemPayDailyLimitErrorBlock()
}
}
}
}

View file

@ -457,7 +457,11 @@ private class TangemPayDetailsUMProvider : CollectionPreviewParameterProvider<Ta
),
isBalanceHidden = false,
addFundsEnabled = true,
addToWalletBlockState = AddToWalletBlockState(onClick = {}, onClickClose = {}),
addToWalletBlockState = AddToWalletBlockState(
onClick = {},
onClickClose = {},
shouldUseMagicEffect = false,
),
accountDeactivatedNotificationConfig = null,
),
TangemPayDetailsUM(

View file

@ -1,7 +1,9 @@
package com.tangem.features.tangempay.utils
import com.tangem.domain.models.StatusSource
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.currency.CryptoCurrency
import com.tangem.domain.models.pay.TangemPayCard
import com.tangem.domain.models.wallet.UserWalletId
@ -23,4 +25,29 @@ internal fun AccountStatus.Payment.requireLoaded(): PaymentAccountStatusValue.Lo
value as? PaymentAccountStatusValue.Loaded
?: error("Card-detail subflow requires Loaded status, got ${value::class.simpleName}")
internal fun AccountStatus.Payment.firstCard(): TangemPayCard = requireLoaded().cards.first()
internal fun AccountStatus.Payment.firstCard(): TangemPayCard = requireLoaded().cards.first()
internal inline fun <T> AccountStatus.Payment.ifLoadedOrNull(call: (PaymentAccountStatusValue.Loaded) -> T): T? {
val value = value
return if (value is PaymentAccountStatusValue.Loaded) {
call(value)
} else {
null
}
}
internal fun AccountStatus.Payment.findCard(
initialCardId: String,
initialStatus: AccountStatus.Payment,
): TangemPayCard? {
val value = value
if (value !is PaymentAccountStatusValue.Loaded || value.source != StatusSource.ACTUAL) return null
val initialCard = value.findCardWithId(initialCardId)
val newCards = initialStatus.ifLoadedOrNull { status ->
val initialCardIds = status.cards.mapTo(mutableSetOf()) { it.id }
value.cards.filterNot { it.id in initialCardIds }
}
return initialCard ?: newCards?.firstOrNull()
}

View file

@ -12,6 +12,7 @@ import com.tangem.core.ui.ds2.loader.TangemLoaderSize
import com.tangem.core.ui.ds2.row.TangemRowContentLead
import com.tangem.core.ui.ds2.row.TangemRowVerticalAlignment
import com.tangem.core.ui.ds2.shimmers.TextShimmerStyle
import com.tangem.core.ui.ds2.topnavigation.TangemTopNavigation
internal sealed interface StoryBookPage
@ -228,6 +229,68 @@ internal data class TangemFadeStory(
val onBlurToggle: () -> Unit,
) : DsStoryBookPage
@Suppress("BooleanPropertyNaming")
internal data class TangemTopNavigationStory(
val contentAlign: TangemTopNavigation.ContentAlign,
val background: Background,
val contentMode: ContentMode,
val hasBack: Boolean,
val hasSubtitle: Boolean,
val longTitle: Boolean,
val endButton: EndButton,
val endGroup: EndGroup,
val useStatusBarInsets: Boolean,
val isBlurEnabled: Boolean,
val onContentAlignChange: (TangemTopNavigation.ContentAlign) -> Unit,
val onBackgroundChange: (Background) -> Unit,
val onContentModeChange: (ContentMode) -> Unit,
val onBackToggle: () -> Unit,
val onSubtitleToggle: () -> Unit,
val onLongTitleToggle: () -> Unit,
val onEndButtonChange: (EndButton) -> Unit,
val onEndGroupChange: (EndGroup) -> Unit,
val onStatusBarInsetsToggle: () -> Unit,
val onBlurToggle: () -> Unit,
) : DsStoryBookPage {
/**
* What goes inside the center `contentColumn` slot.
*
* - [Plain] basic Title / Subtitle text, drives the `hasSubtitle` + `longTitle` toggles.
* - [Rich] `AnnotatedString` with multi-color spans, an inline swap emoji, and emoji-only
* second line. Demonstrates that the slot accepts arbitrary composables, not just plain text.
*/
enum class ContentMode(val label: String) {
Plain("plain"),
Rich("rich"),
}
/** Backdrop the navigation preview sits on, to verify haze/blur behavior. */
enum class Background(val label: String) {
Rainbow("rainbow"),
BgPrimary("bg.primary"),
BgSecondary("bg.secondary"),
BgBrand("bg.brand"),
BgInverse("bg.inverse"),
}
/** Trailing slot variant — None / Close / Loader / Custom pill ("How it works?"). */
enum class EndButton(val label: String) {
None("none"),
Close("close"),
Loader("loader"),
HowItWorks("How it works?"),
}
/** Secondary-actions pill: 0..3 icon buttons. */
enum class EndGroup(val label: String) {
None("0"),
One("1"),
Two("2"),
Three("3"),
}
}
internal data class TangemBadgeV2Story(
val variant: TangemBadge.Variant,
val status: TangemBadge.Status,

View file

@ -21,6 +21,7 @@ import com.tangem.feature.tester.presentation.storybook.page.ds.fade.tangemFadeS
import com.tangem.feature.tester.presentation.storybook.page.ds.loader.tangemLoaderStoryFactory
import com.tangem.feature.tester.presentation.storybook.page.ds.row.tangemRowStoryFactory
import com.tangem.feature.tester.presentation.storybook.page.ds.shimmer.tangemShimmerStoryFactory
import com.tangem.feature.tester.presentation.storybook.page.ds.topnavigation.tangemTopNavigationStoryFactory
private data class DsStoryItem(val title: String, val factory: StoryPageFactory)
@ -31,6 +32,7 @@ private fun buildDsStories() = listOf(
DsStoryItem(title = "📋 TangemRow", factory = tangemRowStoryFactory),
DsStoryItem(title = "✨ TangemShimmer", factory = tangemShimmerStoryFactory),
DsStoryItem(title = "🌫️ TangemFade", factory = tangemFadeStoryFactory),
DsStoryItem(title = "🧭 TangemTopNavigation", factory = tangemTopNavigationStoryFactory),
)
@Composable

View file

@ -0,0 +1,54 @@
package com.tangem.feature.tester.presentation.storybook.page.ds.topnavigation
import com.tangem.core.ui.ds2.topnavigation.TangemTopNavigation
import com.tangem.feature.tester.presentation.storybook.entity.TangemTopNavigationStory
import com.tangem.feature.tester.presentation.storybook.viewmodel.StateUpdater
import com.tangem.feature.tester.presentation.storybook.viewmodel.storyPageFactory
internal fun StateUpdater<TangemTopNavigationStory>.build(): TangemTopNavigationStory {
return TangemTopNavigationStory(
contentAlign = TangemTopNavigation.ContentAlign.Start,
background = TangemTopNavigationStory.Background.Rainbow,
contentMode = TangemTopNavigationStory.ContentMode.Plain,
hasBack = true,
hasSubtitle = true,
longTitle = false,
endButton = TangemTopNavigationStory.EndButton.Close,
endGroup = TangemTopNavigationStory.EndGroup.None,
useStatusBarInsets = true,
isBlurEnabled = true,
onContentAlignChange = { align ->
updateStory { it.copy(contentAlign = align) }
},
onBackgroundChange = { background ->
updateStory { it.copy(background = background) }
},
onContentModeChange = { mode ->
updateStory { it.copy(contentMode = mode) }
},
onBackToggle = {
updateStory { it.copy(hasBack = !it.hasBack) }
},
onSubtitleToggle = {
updateStory { it.copy(hasSubtitle = !it.hasSubtitle) }
},
onLongTitleToggle = {
updateStory { it.copy(longTitle = !it.longTitle) }
},
onEndButtonChange = { endButton ->
updateStory { it.copy(endButton = endButton) }
},
onEndGroupChange = { endGroup ->
updateStory { it.copy(endGroup = endGroup) }
},
onStatusBarInsetsToggle = {
updateStory { it.copy(useStatusBarInsets = !it.useStatusBarInsets) }
},
onBlurToggle = {
updateStory { it.copy(isBlurEnabled = !it.isBlurEnabled) }
},
)
}
internal val tangemTopNavigationStoryFactory
get() = storyPageFactory(StateUpdater<TangemTopNavigationStory>::build)

View file

@ -0,0 +1,523 @@
@file:Suppress("MagicNumber")
package com.tangem.feature.tester.presentation.storybook.page.ds.topnavigation
import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.Spring
import androidx.compose.animation.core.spring
import androidx.compose.animation.expandVertically
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.shrinkVertically
import androidx.compose.animation.togetherWith
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.asPaddingValues
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.statusBars
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.TileMode
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.withStyle
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import com.google.accompanist.systemuicontroller.rememberSystemUiController
import com.tangem.core.ui.components.haze.hazeSourceTangem
import com.tangem.core.ui.ds.image.TangemIconUM
import com.tangem.core.ui.ds2.button.Back
import com.tangem.core.ui.ds2.button.Close
import com.tangem.core.ui.ds2.button.TangemButton
import com.tangem.core.ui.ds2.topnavigation.TangemNavigationText
import com.tangem.core.ui.ds2.topnavigation.TangemTopNavigation
import com.tangem.core.ui.extensions.rememberLastNonNull
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.res.LocalHazeState
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.generated.icons.Icons
import com.tangem.core.ui.res.generated.icons.ic_arrow_swap_horizontal_20
import com.tangem.core.ui.res.generated.icons.ic_scan_20
import com.tangem.core.ui.res.generated.icons.ic_sign_usd_20
import com.tangem.feature.tester.presentation.storybook.entity.TangemTopNavigationStory
import com.tangem.feature.tester.presentation.storybook.entity.TangemTopNavigationStory.Background
import com.tangem.feature.tester.presentation.storybook.entity.TangemTopNavigationStory.ContentMode
import com.tangem.feature.tester.presentation.storybook.entity.TangemTopNavigationStory.EndButton
import com.tangem.feature.tester.presentation.storybook.entity.TangemTopNavigationStory.EndGroup
private const val SHORT_TITLE = "Title"
private const val LONG_TITLE = "Very long top navigation title that should ellipsize"
private const val SUBTITLE = "Subtitle"
private const val PREVIEW_HEIGHT_DP = 220
// Match the production TangemTopNavigation's spring stiffness so the story animates identically.
private val SlotAlphaSpec = spring<Float>(stiffness = Spring.StiffnessMediumLow)
private val SlotSizeSpec = spring<androidx.compose.ui.unit.IntSize>(stiffness = Spring.StiffnessMediumLow)
private val TitleEnterTransition = fadeIn(animationSpec = SlotAlphaSpec)
private val TitleExitTransition = fadeOut(animationSpec = SlotAlphaSpec)
private val SubtitleEnterTransition =
fadeIn(animationSpec = SlotAlphaSpec) + expandVertically(animationSpec = SlotSizeSpec)
private val SubtitleExitTransition =
fadeOut(animationSpec = SlotAlphaSpec) + shrinkVertically(animationSpec = SlotSizeSpec)
@Composable
internal fun TangemTopNavigationStory(state: TangemTopNavigationStory, modifier: Modifier = Modifier) {
val hazeState = LocalHazeState.current
DisposableEffect(state.isBlurEnabled) {
val wasBlurEnabled = hazeState.blurEnabled
hazeState.blurEnabled = state.isBlurEnabled
onDispose { hazeState.blurEnabled = wasBlurEnabled }
}
// Make the system status bar transparent for the lifetime of this story so the preview backdrop
// visually merges with the system clock/icons area — same as a real edge-to-edge screen.
// TesterActivity reapplies its solid bar color on its next recomposition (on exit).
val systemUiController = rememberSystemUiController()
DisposableEffect(systemUiController) {
systemUiController.setStatusBarColor(color = Color.Transparent, darkIcons = false)
onDispose { /* TesterActivity reapplies its color on next recomposition */ }
}
// TesterActivity wraps its NavHost in `Modifier.systemBarsPadding()`, which pushes this story
// down below the status bar. To still let the preview cover the status bar, we shift it up by
// the real status bar height and grow its height by the same amount.
val statusBarDp = WindowInsets.statusBars.asPaddingValues().calculateTopPadding()
Box(
modifier = modifier
.fillMaxSize()
.background(TangemTheme.colors.background.primary),
) {
ComponentPreview(
state = state,
statusBarDp = statusBarDp,
modifier = Modifier.align(Alignment.TopCenter),
)
Column(
modifier = Modifier
.padding(top = PREVIEW_HEIGHT_DP.dp + 16.dp)
.verticalScroll(rememberScrollState())
.padding(bottom = 16.dp),
verticalArrangement = Arrangement.spacedBy(16.dp),
) {
ContentAlignSelector(selected = state.contentAlign, onSelect = state.onContentAlignChange)
ContentModeSelector(selected = state.contentMode, onSelect = state.onContentModeChange)
EndButtonSelector(selected = state.endButton, onSelect = state.onEndButtonChange)
EndGroupSelector(selected = state.endGroup, onSelect = state.onEndGroupChange)
BackgroundSelector(selected = state.background, onSelect = state.onBackgroundChange)
Toggles(state = state)
}
}
}
@Composable
private fun ComponentPreview(state: TangemTopNavigationStory, statusBarDp: Dp, modifier: Modifier = Modifier) {
// The preview Box is grown by `statusBarDp` and shifted up by the same amount so its backdrop
// bleeds into (now transparent) system status bar area. The top navigation itself uses a
// matching `WindowInsets(top = statusBarDp)` so its content clears the system clock/icons,
// exactly like in a real edge-to-edge screen.
Box(
modifier = modifier
.fillMaxWidth()
.height(PREVIEW_HEIGHT_DP.dp + statusBarDp)
.offset(y = -statusBarDp),
) {
PreviewBackground(
background = state.background,
modifier = Modifier
.fillMaxSize()
.hazeSourceTangem(zIndex = 0f),
)
TangemTopNavigation(
// The bar's outer Box (and its 96dp top fade) needs to reach the very top of the
// preview, while only the inner row sits below the status bar. So we DON'T add outer
// padding — instead we push the row down via `windowInsets`.
//
// Catch: TesterActivity wraps the NavHost in `Modifier.systemBarsPadding()`, which
// marks the system bars insets as consumed for descendants. The bar's internal
// `windowInsetsPadding(windowInsets)` subtracts that consumption (`statusBarDp`)
// from whatever we pass. To still get an effective `statusBarDp` of row padding, we
// pre-compensate by passing `2 * statusBarDp` here.
modifier = Modifier.align(Alignment.TopCenter),
contentAlign = state.contentAlign,
windowInsets = if (state.useStatusBarInsets) WindowInsets(top = statusBarDp * 2) else WindowInsets(0),
startButton = if (state.hasBack) {
{ TangemButton.Back(onClick = {}) }
} else {
null
},
endButtonsGroup = endGroupContent(state.endGroup),
endButton = endButtonContent(state.endButton),
contentColumn = {
when (state.contentMode) {
ContentMode.Plain -> PlainContent(state)
ContentMode.Rich -> RichContent()
}
},
)
}
}
@Composable
private fun ColumnScope.PlainContent(state: TangemTopNavigationStory) {
// Mirror the production `TitleSubtitle` helper so toggling the subtitle / swapping the title
// animates the same way users see in real screens. The raw `contentColumn` slot doesn't wrap
// children in any animation by itself.
val title = if (state.longTitle) LONG_TITLE else SHORT_TITLE
AnimatedContent(
targetState = title,
transitionSpec = { TitleEnterTransition togetherWith TitleExitTransition },
label = "TangemTopNavigationStory.title",
) { current ->
TangemNavigationText(text = current, role = TangemNavigationText.Role.Title)
}
val subtitle: String? = if (state.hasSubtitle) SUBTITLE else null
val displayedSubtitle = rememberLastNonNull(subtitle)
AnimatedVisibility(
visible = subtitle != null,
enter = SubtitleEnterTransition,
exit = SubtitleExitTransition,
) {
displayedSubtitle?.let { text ->
Column {
Spacer(Modifier.height(2.dp))
TangemNavigationText(text = text, role = TangemNavigationText.Role.Subtitle)
}
}
}
}
/**
* Rich content variant proves the `contentColumn` slot accepts an arbitrary composable, not just
* a plain string. Uses [AnnotatedString] with per-span colors and an inline swap emoji to mimic
* the multi-color "Title in 🔄 Title?" example from the design board.
*/
@Composable
private fun RichContent() {
val titleSpan = buildAnnotatedString {
append("Title in \uD83D\uDD04 ")
withStyle(SpanStyle(color = Color(0xFFFF4D4F))) { append("Title?") }
}
val subtitleSpan = buildAnnotatedString {
withStyle(SpanStyle(color = TangemTheme.colors3.text.secondary)) { append("Subtitle in ") }
withStyle(SpanStyle(color = Color(0xFF34C759))) { append("Subtitle?") }
}
TangemNavigationText(text = titleSpan, role = TangemNavigationText.Role.Title)
Spacer(Modifier.height(2.dp))
TangemNavigationText(text = subtitleSpan, role = TangemNavigationText.Role.Subtitle)
}
private fun endButtonContent(endButton: EndButton): (@Composable () -> Unit)? = when (endButton) {
EndButton.None -> null
EndButton.Close -> {
{ TangemButton.Close(onClick = {}) }
}
EndButton.Loader -> {
{ TangemButton(variant = TangemButton.Variant.Material, isLoading = true, onClick = {}) }
}
EndButton.HowItWorks -> {
{
TangemButton(
variant = TangemButton.Variant.Material,
text = stringReference("How it works?"),
onClick = {},
)
}
}
}
private fun endGroupContent(endGroup: EndGroup): (@Composable RowScope.() -> Unit)? = when (endGroup) {
EndGroup.None -> null
EndGroup.One -> {
{
TangemButton(
variant = TangemButton.Variant.Ghost,
iconStart = TangemIconUM.Icon(Icons.ic_arrow_swap_horizontal_20),
onClick = {},
)
}
}
EndGroup.Two -> {
{
TangemButton(
variant = TangemButton.Variant.Ghost,
iconStart = TangemIconUM.Icon(Icons.ic_arrow_swap_horizontal_20),
onClick = {},
)
TangemButton(
variant = TangemButton.Variant.Ghost,
iconStart = TangemIconUM.Icon(Icons.ic_scan_20),
onClick = {},
)
}
}
EndGroup.Three -> {
{
TangemButton(
variant = TangemButton.Variant.Ghost,
iconStart = TangemIconUM.Icon(Icons.ic_arrow_swap_horizontal_20),
onClick = {},
)
TangemButton(
variant = TangemButton.Variant.Ghost,
iconStart = TangemIconUM.Icon(Icons.ic_sign_usd_20),
onClick = {},
)
TangemButton(
variant = TangemButton.Variant.Ghost,
iconStart = TangemIconUM.Icon(Icons.ic_scan_20),
onClick = {},
)
}
}
}
@Composable
private fun PreviewBackground(background: Background, modifier: Modifier = Modifier) {
when (background) {
Background.Rainbow -> RainbowBackdrop(modifier)
Background.BgPrimary -> Box(modifier.background(TangemTheme.colors3.bg.primary))
Background.BgSecondary -> Box(modifier.background(TangemTheme.colors3.bg.secondary))
Background.BgBrand -> Box(modifier.background(TangemTheme.colors3.bg.brand))
Background.BgInverse -> Box(modifier.background(TangemTheme.colors3.bg.inverse))
}
}
@Composable
private fun RainbowBackdrop(modifier: Modifier = Modifier) {
val bands = remember {
listOf(
Color(0xFFFF1744),
Color(0xFFFF9100),
Color(0xFFFFEA00),
Color(0xFF00E676),
Color(0xFF00B8D4),
Color(0xFF2962FF),
Color(0xFFD500F9),
)
}
val stops = remember(bands) {
buildList {
bands.forEachIndexed { index, color ->
val start = index.toFloat() / bands.size
val end = (index + 1).toFloat() / bands.size
add(start to color)
add(end to color)
}
}.toTypedArray()
}
val tilePx = with(LocalDensity.current) { 160.dp.toPx() }
Box(
modifier = modifier.background(
brush = Brush.linearGradient(
colorStops = stops,
start = Offset(0f, 0f),
end = Offset(tilePx, tilePx),
tileMode = TileMode.Repeated,
),
),
)
}
@Composable
private fun ContentAlignSelector(
selected: TangemTopNavigation.ContentAlign,
onSelect: (TangemTopNavigation.ContentAlign) -> Unit,
) {
Section(label = "Content align") {
ChipGrid(
items = TangemTopNavigation.ContentAlign.entries,
label = { it.name },
isSelected = { it == selected },
onSelect = onSelect,
)
}
}
@Composable
private fun ContentModeSelector(selected: ContentMode, onSelect: (ContentMode) -> Unit) {
Section(label = "Content mode") {
ChipGrid(
items = ContentMode.entries,
label = { it.label },
isSelected = { it == selected },
onSelect = onSelect,
)
}
}
@Composable
private fun EndButtonSelector(selected: EndButton, onSelect: (EndButton) -> Unit) {
Section(label = "End button") {
ChipGrid(
items = EndButton.entries,
label = { it.label },
isSelected = { it == selected },
onSelect = onSelect,
)
}
}
@Composable
private fun EndGroupSelector(selected: EndGroup, onSelect: (EndGroup) -> Unit) {
Section(label = "End buttons group (pill)") {
ChipGrid(
items = EndGroup.entries,
label = { it.label },
isSelected = { it == selected },
onSelect = onSelect,
)
}
}
@Composable
private fun BackgroundSelector(selected: Background, onSelect: (Background) -> Unit) {
Section(label = "Background") {
ChipGrid(
items = Background.entries,
label = { it.label },
isSelected = { it == selected },
onSelect = onSelect,
)
}
}
@Composable
private fun Toggles(state: TangemTopNavigationStory) {
Section(label = "Flags") {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp),
) {
ToggleRow(label = "back button", checked = state.hasBack, onToggle = state.onBackToggle)
ToggleRow(label = "subtitle", checked = state.hasSubtitle, onToggle = state.onSubtitleToggle)
ToggleRow(
label = "long title (ellipsis test)",
checked = state.longTitle,
onToggle = state.onLongTitleToggle,
)
ToggleRow(
label = "status bar insets",
checked = state.useStatusBarInsets,
onToggle = state.onStatusBarInsetsToggle,
)
ToggleRow(label = "blur (haze)", checked = state.isBlurEnabled, onToggle = state.onBlurToggle)
}
}
}
@Composable
private fun Section(label: String, content: @Composable () -> Unit) {
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
Text(
modifier = Modifier.padding(horizontal = 16.dp),
text = label,
style = TangemTheme.typography.subtitle1,
color = TangemTheme.colors.text.primary1,
)
content()
}
}
@Composable
private fun <T> ChipGrid(items: List<T>, label: (T) -> String, isSelected: (T) -> Boolean, onSelect: (T) -> Unit) {
val shape = RoundedCornerShape(50)
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp)
.clip(shape)
.background(TangemTheme.colors2.surface.level2)
.border(
width = 1.dp,
color = TangemTheme.colors2.border.neutral.secondary,
shape = shape,
)
.padding(4.dp),
horizontalArrangement = Arrangement.spacedBy(4.dp),
) {
items.forEach { item ->
Chip(
label = label(item),
selected = isSelected(item),
onClick = { onSelect(item) },
modifier = Modifier.weight(1f),
)
}
}
}
@Composable
private fun Chip(label: String, selected: Boolean, onClick: () -> Unit, modifier: Modifier = Modifier) {
val chipShape = RoundedCornerShape(50)
Box(
contentAlignment = Alignment.Center,
modifier = modifier
.clip(chipShape)
.background(
if (selected) TangemTheme.colors2.surface.level3 else TangemTheme.colors2.surface.level2,
)
.clickable(onClick = onClick)
.padding(vertical = 8.dp, horizontal = 4.dp),
) {
Text(
text = label,
style = TangemTheme.typography.caption2,
color = if (selected) TangemTheme.colors.text.primary1 else TangemTheme.colors.text.secondary,
)
}
}
@Composable
private fun ToggleRow(label: String, checked: Boolean, onToggle: () -> Unit) {
Row(
modifier = Modifier
.fillMaxWidth()
.clip(RoundedCornerShape(12.dp))
.background(TangemTheme.colors2.surface.level2)
.clickable(onClick = onToggle)
.padding(horizontal = 12.dp, vertical = 12.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween,
) {
Text(
text = label,
style = TangemTheme.typography.body2,
color = TangemTheme.colors.text.primary1,
)
Text(
text = if (checked) "ON" else "OFF",
style = TangemTheme.typography.caption2,
color = if (checked) TangemTheme.colors.text.accent else TangemTheme.colors.text.secondary,
)
}
}

View file

@ -43,6 +43,7 @@ import com.tangem.feature.tester.presentation.storybook.page.ds.fade.TangemFadeS
import com.tangem.feature.tester.presentation.storybook.page.ds.loader.TangemLoaderStory
import com.tangem.feature.tester.presentation.storybook.page.ds.row.TangemRowStory
import com.tangem.feature.tester.presentation.storybook.page.ds.shimmer.TangemShimmerStory
import com.tangem.feature.tester.presentation.storybook.page.ds.topnavigation.TangemTopNavigationStory
import com.tangem.feature.tester.presentation.storybook.page.headerrow.TangemHeaderRowStory
import com.tangem.feature.tester.presentation.storybook.page.message.TangemMessageStory
import com.tangem.feature.tester.presentation.storybook.page.opportunities.OpportunitiesBGStory
@ -93,6 +94,7 @@ internal fun StoryBookScreen(state: StoryBookUM, modifier: Modifier = Modifier)
is TangemRowStory -> TangemRowStory(state = storyState)
is TangemShimmerStory -> TangemShimmerStory(state = storyState)
is TangemFadeStory -> TangemFadeStory(state = storyState)
is TangemTopNavigationStory -> TangemTopNavigationStory(state = storyState)
}
}
}