Updated on 2026-08-14
This commit is contained in:
commit
13091cfffa
284 changed files with 13280 additions and 212 deletions
|
|
@ -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,
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,7 +15,11 @@ internal class DetailsAddToWalletBannerTransformer(
|
|||
addToWalletBlockState = if (isDone) {
|
||||
null
|
||||
} else {
|
||||
AddToWalletBlockState(onClick = onClickBanner, onClickClose = onClickCloseBanner)
|
||||
AddToWalletBlockState(
|
||||
onClick = onClickBanner,
|
||||
onClickClose = onClickCloseBanner,
|
||||
shouldUseMagicEffect = true,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue