Updated on 2026-08-14

This commit is contained in:
Tangem 2025-10-28 11:52:48 +04:00
parent 52925902c2
commit 1ac0556dc8
19 changed files with 491 additions and 264 deletions

View file

@ -15,6 +15,8 @@ import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.core.ui.components.NavigationBar3ButtonsScrim
import com.tangem.core.ui.decompose.ComposableBottomSheetComponent
import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.features.tangempay.components.cardDetails.DefaultTangemPayCardDetailsBlockComponent
import com.tangem.features.tangempay.components.cardDetails.TangemPayCardDetailsBlockComponent
import com.tangem.features.tangempay.components.txHistory.DefaultTangemPayTxHistoryComponent
import com.tangem.features.tangempay.components.txHistory.TangemPayTxHistoryDetailsComponent
import com.tangem.features.tangempay.entity.TangemPayDetailsNavigation
@ -44,6 +46,11 @@ internal class TangemPayDetailsComponent(
),
)
private val cardDetailsBlockComponent = DefaultTangemPayCardDetailsBlockComponent(
appComponentContext = child("cardDetailsBlockComponent"),
params = TangemPayCardDetailsBlockComponent.Params(params = params),
)
@Composable
override fun Content(modifier: Modifier) {
val state by model.uiState.collectAsStateWithLifecycle()
@ -53,6 +60,7 @@ internal class TangemPayDetailsComponent(
TangemPayDetailsScreen(
state = state,
txHistoryComponent = txHistoryComponent,
cardDetailsBlockComponent = cardDetailsBlockComponent,
modifier = modifier,
)
bottomSheet.child?.instance?.BottomSheet()

View file

@ -0,0 +1,24 @@
package com.tangem.features.tangempay.components.cardDetails
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.features.tangempay.entity.TangemPayCardDetailsUM
import com.tangem.features.tangempay.model.TangemPayCardDetailsBlockModel
import com.tangem.features.tangempay.ui.CardDetailsItem
import kotlinx.coroutines.flow.StateFlow
internal class DefaultTangemPayCardDetailsBlockComponent(
appComponentContext: AppComponentContext,
params: TangemPayCardDetailsBlockComponent.Params,
) : AppComponentContext by appComponentContext, TangemPayCardDetailsBlockComponent {
private val model: TangemPayCardDetailsBlockModel = getOrCreateModel(params = params)
override val state: StateFlow<TangemPayCardDetailsUM> = model.uiState
@Composable
override fun CardDetailsBlockContent(state: TangemPayCardDetailsUM, modifier: Modifier) {
CardDetailsItem(state, modifier)
}
}

View file

@ -0,0 +1,20 @@
package com.tangem.features.tangempay.components.cardDetails
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.tangem.features.tangempay.entity.TangemPayCardDetailsUM
import com.tangem.features.tangempay.ui.CardDetailsItem
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
internal class PreviewTangemPayCardDetailsBlockComponent(
state: TangemPayCardDetailsUM,
) : TangemPayCardDetailsBlockComponent {
override val state: StateFlow<TangemPayCardDetailsUM> = MutableStateFlow(state)
@Composable
override fun CardDetailsBlockContent(state: TangemPayCardDetailsUM, modifier: Modifier) {
CardDetailsItem(state, modifier)
}
}

View file

@ -0,0 +1,16 @@
package com.tangem.features.tangempay.components.cardDetails
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.tangem.features.tangempay.components.TangemPayDetailsContainerComponent
import com.tangem.features.tangempay.entity.TangemPayCardDetailsUM
import kotlinx.coroutines.flow.StateFlow
internal interface TangemPayCardDetailsBlockComponent {
val state: StateFlow<TangemPayCardDetailsUM>
@Composable
fun CardDetailsBlockContent(state: TangemPayCardDetailsUM, modifier: Modifier = Modifier)
data class Params(val params: TangemPayDetailsContainerComponent.Params)
}

View file

@ -2,6 +2,8 @@ package com.tangem.features.tangempay.di
import com.tangem.features.tangempay.components.DefaultTangemPayDetailsContainerComponent
import com.tangem.features.tangempay.components.TangemPayDetailsContainerComponent
import com.tangem.features.tangempay.model.listener.CardDetailsEventListener
import com.tangem.features.tangempay.model.listener.DefaultCardDetailsEventListener
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
@ -17,4 +19,8 @@ internal interface TangemPayDetailsFeatureModule {
fun bindTangemPayDetailsContainerComponentFactory(
factory: DefaultTangemPayDetailsContainerComponent.Factory,
): TangemPayDetailsContainerComponent.Factory
@Binds
@Singleton
fun bindCardDetailsEventListener(impl: DefaultCardDetailsEventListener): CardDetailsEventListener
}

View file

@ -2,6 +2,7 @@ package com.tangem.features.tangempay.di
import com.tangem.core.decompose.di.ModelComponent
import com.tangem.core.decompose.model.Model
import com.tangem.features.tangempay.model.TangemPayCardDetailsBlockModel
import com.tangem.features.tangempay.model.TangemPayChangePinModel
import com.tangem.features.tangempay.model.TangemPayDetailsModel
import com.tangem.features.tangempay.model.TangemPayTxHistoryDetailsModel
@ -35,4 +36,9 @@ internal interface TangemPayModelModule {
@IntoMap
@ClassKey(TangemPayChangePinModel::class)
fun bindTangemPayChangePinModel(model: TangemPayChangePinModel): Model
@Binds
@IntoMap
@ClassKey(TangemPayCardDetailsBlockModel::class)
fun bindTangemPayCardDetailsBlockModel(model: TangemPayCardDetailsBlockModel): Model
}

View file

@ -0,0 +1,33 @@
package com.tangem.features.tangempay.entity
import com.tangem.core.ui.extensions.TextReference
import com.tangem.features.tangempay.details.impl.R
import com.tangem.features.tangempay.utils.CardDetailsFormatUtil
import com.tangem.utils.StringsSigns
private const val CARD_NUMBER_SART_DIGITS_COUNT = 12
private const val DATE_PART_LENGTH = 2
private const val CVV_LENGTH = 3
internal class TangemPayCardDetailsBlockStateFactory(
private val cardNumberEnd: String,
private val onReveal: () -> Unit,
private val onCopy: (String) -> Unit,
) {
private val cardStartMasked = maskedBlock(CARD_NUMBER_SART_DIGITS_COUNT)
private val dateMasked = maskedBlock(DATE_PART_LENGTH)
private val cvvMasked = maskedBlock(CVV_LENGTH)
fun getInitialState() = TangemPayCardDetailsUM(
number = CardDetailsFormatUtil.formatCardNumber(cardNumber = "$cardStartMasked$cardNumberEnd"),
expiry = CardDetailsFormatUtil.formatDate(month = dateMasked, year = dateMasked),
cvv = cvvMasked,
buttonText = TextReference.Res(R.string.tangempay_card_details_reveal_text),
onClick = onReveal,
onCopy = onCopy,
isHidden = true,
)
private fun maskedBlock(count: Int) = StringsSigns.DOT.repeat(count)
}

View file

@ -8,30 +8,16 @@ import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.features.tangempay.details.impl.R
import com.tangem.features.tangempay.utils.CardDetailsFormatUtil
import com.tangem.utils.StringsSigns
import kotlinx.collections.immutable.persistentListOf
private const val CARD_NUMBER_SART_DIGITS_COUNT = 12
private const val DATE_PART_LENGTH = 2
private const val CVV_LENGTH = 3
@Suppress("LongParameterList")
internal class TangemPayDetailsStateFactory(
private val cardNumberEnd: String,
private val onBack: () -> Unit,
private val onRefresh: (ShowRefreshState) -> Unit,
private val onReceive: () -> Unit,
private val onReveal: () -> Unit,
private val onCopy: (String) -> Unit,
private val onClickChangePin: () -> Unit,
private val onClickFreezeCard: () -> Unit,
) {
private val cardStartMasked = maskedBlock(CARD_NUMBER_SART_DIGITS_COUNT)
private val dateMasked = maskedBlock(DATE_PART_LENGTH)
private val cvvMasked = maskedBlock(CVV_LENGTH)
fun getInitialState() = TangemPayDetailsUM(
topBarConfig = TangemPayDetailsTopBarConfig(
onBackClick = onBack,
@ -58,18 +44,6 @@ internal class TangemPayDetailsStateFactory(
),
),
),
cardDetailsUM = TangemPayCardDetailsUM(
number = CardDetailsFormatUtil.formatCardNumber(cardNumber = "$cardStartMasked$cardNumberEnd"),
expiry = CardDetailsFormatUtil.formatDate(month = dateMasked, year = dateMasked),
cvv = cvvMasked,
buttonText = TextReference.Res(R.string.tangempay_card_details_reveal_text),
onClick = onReveal,
onCopy = onCopy,
isHidden = true,
),
isBalanceHidden = false,
)
private fun maskedBlock(count: Int) = StringsSigns.DOT.repeat(count)
}

View file

@ -9,11 +9,10 @@ internal data class TangemPayDetailsUM(
val topBarConfig: TangemPayDetailsTopBarConfig,
val pullToRefreshConfig: PullToRefreshConfig,
val balanceBlockState: TangemPayDetailsBalanceBlockState,
val cardDetailsUM: TangemPayCardDetailsUM,
val isBalanceHidden: Boolean,
)
data class TangemPayCardDetailsUM(
internal data class TangemPayCardDetailsUM(
val number: String = "",
val expiry: String = "",
val cvv: String = "",

View file

@ -0,0 +1,103 @@
package com.tangem.features.tangempay.model
import androidx.compose.runtime.Stable
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.core.decompose.ui.UiMessageSender
import com.tangem.core.ui.clipboard.ClipboardManager
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.message.SnackbarMessage
import com.tangem.domain.pay.repository.CardDetailsRepository
import com.tangem.features.tangempay.components.cardDetails.TangemPayCardDetailsBlockComponent
import com.tangem.features.tangempay.details.impl.R
import com.tangem.features.tangempay.entity.TangemPayCardDetailsBlockStateFactory
import com.tangem.features.tangempay.entity.TangemPayCardDetailsUM
import com.tangem.features.tangempay.model.listener.CardDetailsEvent
import com.tangem.features.tangempay.model.listener.CardDetailsEventListener
import com.tangem.features.tangempay.model.transformers.DetailsHiddenStateTransformer
import com.tangem.features.tangempay.model.transformers.DetailsRevealProgressStateTransformer
import com.tangem.features.tangempay.model.transformers.DetailsRevealedStateTransformer
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.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import javax.inject.Inject
@Stable
@ModelScoped
internal class TangemPayCardDetailsBlockModel @Inject constructor(
paramsContainer: ParamsContainer,
override val dispatchers: CoroutineDispatcherProvider,
private val cardDetailsRepository: CardDetailsRepository,
private val clipboardManager: ClipboardManager,
private val uiMessageSender: UiMessageSender,
private val cardDetailsEventListener: CardDetailsEventListener,
) : Model() {
private val params: TangemPayCardDetailsBlockComponent.Params = paramsContainer.require()
private val stateFactory = TangemPayCardDetailsBlockStateFactory(
cardNumberEnd = params.params.config.cardNumberEnd,
onReveal = ::revealCardDetails,
onCopy = ::copyData,
)
val uiState: StateFlow<TangemPayCardDetailsUM>
field = MutableStateFlow(stateFactory.getInitialState())
private val revealCardDetailsJobHolder = JobHolder()
init {
modelScope.launch {
cardDetailsEventListener.event.collectLatest { event ->
when (event) {
CardDetailsEvent.Hide -> hideCardDetails()
CardDetailsEvent.Show -> revealCardDetails()
}
}
}
}
private fun revealCardDetails() {
modelScope.launch {
uiState.update(
transformer = DetailsRevealProgressStateTransformer(onClickHide = ::hideCardDetails),
)
cardDetailsRepository.revealCardDetails()
.onRight {
uiState.update(
transformer = DetailsRevealedStateTransformer(
details = it,
onClickHide = ::hideCardDetails,
),
)
}
.onLeft {
uiState.update(transformer = DetailsHiddenStateTransformer(stateFactory))
showError()
}
}.saveIn(revealCardDetailsJobHolder)
}
fun hideCardDetails() {
modelScope.launch {
revealCardDetailsJobHolder.cancel()
uiState.update(transformer = DetailsHiddenStateTransformer(stateFactory))
}
}
private fun showError() {
uiMessageSender.send(
SnackbarMessage(TextReference.Res(R.string.tangempay_card_details_error_text)),
)
}
private fun copyData(text: String) {
clipboardManager.setText(text = text.filterNot { it.isWhitespace() }, isSensitive = true)
}
}

View file

@ -8,21 +8,20 @@ import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.core.decompose.navigation.Router
import com.tangem.core.decompose.ui.UiMessageSender
import com.tangem.core.ui.clipboard.ClipboardManager
import com.tangem.core.ui.components.containers.pullToRefresh.PullToRefreshConfig.ShowRefreshState
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.message.SnackbarMessage
import com.tangem.domain.models.TokenReceiveConfig
import com.tangem.domain.pay.DataForReceiveFactory
import com.tangem.domain.pay.repository.CardDetailsRepository
import com.tangem.domain.visa.model.TangemPayTxHistoryItem
import com.tangem.features.tangempay.components.TangemPayDetailsContainerComponent
import com.tangem.features.tangempay.details.impl.R
import com.tangem.features.tangempay.entity.TangemPayDetailsErrorType
import com.tangem.features.tangempay.entity.TangemPayDetailsNavigation
import com.tangem.features.tangempay.entity.TangemPayDetailsStateFactory
import com.tangem.features.tangempay.entity.TangemPayDetailsUM
import com.tangem.features.tangempay.model.transformers.*
import com.tangem.features.tangempay.model.listener.CardDetailsEvent
import com.tangem.features.tangempay.model.listener.CardDetailsEventListener
import com.tangem.features.tangempay.model.transformers.DetailsBalanceTransformer
import com.tangem.features.tangempay.model.transformers.TangemPayDetailsRefreshTransformer
import com.tangem.features.tangempay.navigation.TangemPayDetailsInnerRoute
import com.tangem.features.tangempay.utils.TangemPayErrorMessageFactory
import com.tangem.features.tangempay.utils.TangemPayTxHistoryUiActions
@ -45,19 +44,16 @@ internal class TangemPayDetailsModel @Inject constructor(
private val router: Router,
private val cardDetailsRepository: CardDetailsRepository,
private val dataForReceiveFactory: DataForReceiveFactory,
private val clipboardManager: ClipboardManager,
private val uiMessageSender: UiMessageSender,
private val cardDetailsEventListener: CardDetailsEventListener,
) : Model(), TangemPayTxHistoryUiActions {
private val params: TangemPayDetailsContainerComponent.Params = paramsContainer.require()
private val stateFactory = TangemPayDetailsStateFactory(
cardNumberEnd = params.config.cardNumberEnd,
onBack = router::pop,
onRefresh = ::onRefreshSwipe,
onReceive = ::onClickReceive,
onReveal = ::revealCardDetails,
onCopy = ::copyData,
onClickChangePin = ::onClickChangePin,
onClickFreezeCard = ::onClickFreezeCard,
)
@ -67,7 +63,6 @@ internal class TangemPayDetailsModel @Inject constructor(
private val refreshStateJobHolder = JobHolder()
private val fetchBalanceJobHolder = JobHolder()
private val revealCardDetailsJobHolder = JobHolder()
val bottomSheetNavigation: SlotNavigation<TangemPayDetailsNavigation> = SlotNavigation()
@ -112,50 +107,13 @@ internal class TangemPayDetailsModel @Inject constructor(
private fun onRefreshSwipe(refreshState: ShowRefreshState) {
modelScope.launch {
hideCardDetails()
cardDetailsEventListener.send(CardDetailsEvent.Hide)
uiState.update(TangemPayDetailsRefreshTransformer(isRefreshing = refreshState.value))
fetchBalance().join()
uiState.update(TangemPayDetailsRefreshTransformer(isRefreshing = false))
}.saveIn(refreshStateJobHolder)
}
private fun revealCardDetails() {
modelScope.launch {
uiState.update(
transformer = DetailsRevealProgressStateTransformer(onClickHide = ::hideCardDetails),
)
cardDetailsRepository.revealCardDetails()
.onRight {
uiState.update(
transformer = DetailsRevealedStateTransformer(
details = it,
onClickHide = ::hideCardDetails,
),
)
}
.onLeft {
uiState.update(transformer = DetailsHiddenStateTransformer(stateFactory))
showError()
}
}.saveIn(revealCardDetailsJobHolder)
}
private fun hideCardDetails() {
modelScope.launch {
revealCardDetailsJobHolder.cancel()
uiState.update(transformer = DetailsHiddenStateTransformer(stateFactory))
}
}
private fun copyData(text: String) {
clipboardManager.setText(text = text.filterNot { it.isWhitespace() }, isSensitive = true)
}
private fun showError() {
uiMessageSender.send(
SnackbarMessage(TextReference.Res(R.string.tangempay_card_details_error_text)),
)
}
override fun onTransactionClick(item: TangemPayTxHistoryItem) {
bottomSheetNavigation.activate(TangemPayDetailsNavigation.TransactionDetails(item))
}

View file

@ -0,0 +1,17 @@
package com.tangem.features.tangempay.model.listener
import kotlinx.coroutines.flow.Flow
/**
* Card details must be hidden after some UI actions from the external component
*/
internal interface CardDetailsEventListener {
val event: Flow<CardDetailsEvent>
suspend fun send(event: CardDetailsEvent)
}
internal enum class CardDetailsEvent {
Hide, Show
}

View file

@ -0,0 +1,22 @@
package com.tangem.features.tangempay.model.listener
import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
internal class DefaultCardDetailsEventListener @Inject constructor() : CardDetailsEventListener {
private val _event = MutableSharedFlow<CardDetailsEvent>(
replay = 1,
extraBufferCapacity = 0,
onBufferOverflow = BufferOverflow.DROP_OLDEST,
)
override val event: Flow<CardDetailsEvent> = _event
override suspend fun send(event: CardDetailsEvent) {
_event.emit(event)
}
}

View file

@ -1,14 +1,14 @@
package com.tangem.features.tangempay.model.transformers
import com.tangem.features.tangempay.entity.TangemPayDetailsStateFactory
import com.tangem.features.tangempay.entity.TangemPayDetailsUM
import com.tangem.features.tangempay.entity.TangemPayCardDetailsBlockStateFactory
import com.tangem.features.tangempay.entity.TangemPayCardDetailsUM
import com.tangem.utils.transformer.Transformer
internal class DetailsHiddenStateTransformer(
private val stateFactory: TangemPayDetailsStateFactory,
) : Transformer<TangemPayDetailsUM> {
private val stateFactory: TangemPayCardDetailsBlockStateFactory,
) : Transformer<TangemPayCardDetailsUM> {
override fun transform(prevState: TangemPayDetailsUM): TangemPayDetailsUM {
return prevState.copy(cardDetailsUM = stateFactory.getInitialState().cardDetailsUM)
override fun transform(prevState: TangemPayCardDetailsUM): TangemPayCardDetailsUM {
return stateFactory.getInitialState()
}
}

View file

@ -2,21 +2,19 @@ package com.tangem.features.tangempay.model.transformers
import com.tangem.core.ui.extensions.TextReference
import com.tangem.features.tangempay.details.impl.R
import com.tangem.features.tangempay.entity.TangemPayDetailsUM
import com.tangem.features.tangempay.entity.TangemPayCardDetailsUM
import com.tangem.utils.transformer.Transformer
internal class DetailsRevealProgressStateTransformer(
private val onClickHide: (() -> Unit),
) : Transformer<TangemPayDetailsUM> {
) : Transformer<TangemPayCardDetailsUM> {
override fun transform(prevState: TangemPayDetailsUM): TangemPayDetailsUM {
override fun transform(prevState: TangemPayCardDetailsUM): TangemPayCardDetailsUM {
return prevState.copy(
cardDetailsUM = prevState.cardDetailsUM.copy(
buttonText = TextReference.Res(R.string.tangempay_card_details_hide_text),
onClick = onClickHide,
isLoading = true,
isHidden = true,
),
buttonText = TextReference.Res(R.string.tangempay_card_details_hide_text),
onClick = onClickHide,
isLoading = true,
isHidden = true,
)
}
}

View file

@ -4,25 +4,23 @@ import com.tangem.core.ui.extensions.TextReference
import com.tangem.domain.pay.model.TangemPayCardDetails
import com.tangem.features.tangempay.details.impl.R
import com.tangem.features.tangempay.entity.TangemPayCardDetailsUM
import com.tangem.features.tangempay.entity.TangemPayDetailsUM
import com.tangem.features.tangempay.utils.CardDetailsFormatUtil
import com.tangem.utils.transformer.Transformer
internal class DetailsRevealedStateTransformer(
private val details: TangemPayCardDetails,
private val onClickHide: (() -> Unit),
) : Transformer<TangemPayDetailsUM> {
) : Transformer<TangemPayCardDetailsUM> {
override fun transform(prevState: TangemPayDetailsUM): TangemPayDetailsUM {
val cardDetailsUM = TangemPayCardDetailsUM(
override fun transform(prevState: TangemPayCardDetailsUM): TangemPayCardDetailsUM {
return TangemPayCardDetailsUM(
number = CardDetailsFormatUtil.formatCardNumber(cardNumber = details.pan),
expiry = CardDetailsFormatUtil.formatDate(month = details.expirationMonth, year = details.expirationYear),
cvv = details.cvv,
onClick = onClickHide,
buttonText = TextReference.Res(R.string.tangempay_card_details_hide_text),
onCopy = prevState.cardDetailsUM.onCopy,
onCopy = prevState.onCopy,
isHidden = false,
)
return prevState.copy(cardDetailsUM = cardDetailsUM)
}
}

View file

@ -0,0 +1,19 @@
package com.tangem.features.tangempay.ui
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.tangem.core.ui.res.TangemTheme
import com.tangem.features.tangempay.entity.TangemPayCardDetailsUM
@Composable
internal fun CardDetailsItem(state: TangemPayCardDetailsUM, modifier: Modifier = Modifier) {
TangemPayCardDetailsBlock(
modifier = modifier
.padding(top = TangemTheme.dimens.spacing12)
.padding(horizontal = TangemTheme.dimens.spacing16)
.fillMaxWidth(),
state = state,
)
}

View file

@ -0,0 +1,164 @@
package com.tangem.features.tangempay.ui
import androidx.compose.animation.core.LinearEasing
import androidx.compose.animation.core.RepeatMode
import androidx.compose.animation.core.animateFloat
import androidx.compose.animation.core.infiniteRepeatable
import androidx.compose.animation.core.rememberInfiniteTransition
import androidx.compose.animation.core.tween
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme
import com.tangem.features.tangempay.details.impl.R
import com.tangem.features.tangempay.entity.TangemPayCardDetailsUM
private const val REVEAL_ANIMATION_MILLIS = 500
@Composable
internal fun TangemPayCardDetailsBlock(state: TangemPayCardDetailsUM, modifier: Modifier = Modifier) {
val alpha = if (state.isLoading) {
val infiniteTransition = rememberInfiniteTransition()
val animation by infiniteTransition.animateFloat(
initialValue = 1f,
targetValue = 0.3f,
animationSpec = infiniteRepeatable(
animation = tween(REVEAL_ANIMATION_MILLIS, easing = LinearEasing),
repeatMode = RepeatMode.Reverse,
),
)
animation
} else {
1f
}
Column(
modifier = modifier
.background(
color = TangemTheme.colors.background.primary,
shape = TangemTheme.shapes.roundedCornersMedium,
),
) {
CardDetailsBlockHeader(state)
CardDetailsTextContainer(
modifier = Modifier
.graphicsLayer { this.alpha = alpha }
.fillMaxWidth()
.padding(start = 12.dp, end = 12.dp, top = 4.dp),
text = state.number,
onCopy = { state.onCopy(state.number) },
isHidden = state.isHidden,
)
Row(
modifier = Modifier
.fillMaxWidth()
.padding(start = 12.dp, end = 12.dp, bottom = 12.dp, top = 8.dp),
horizontalArrangement = Arrangement.spacedBy(12.dp),
) {
CardDetailsTextContainer(
modifier = Modifier
.graphicsLayer { this.alpha = alpha }
.weight(1f),
text = state.expiry,
onCopy = { state.onCopy(state.expiry) },
isHidden = state.isHidden,
)
CardDetailsTextContainer(
modifier = Modifier
.graphicsLayer { this.alpha = alpha }
.weight(1f),
text = state.cvv,
onCopy = { state.onCopy(state.cvv) },
isHidden = state.isHidden,
)
}
}
}
@Composable
private fun CardDetailsBlockHeader(state: TangemPayCardDetailsUM, modifier: Modifier = Modifier) {
Row(
modifier = modifier
.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
) {
Text(
modifier = Modifier
.padding(start = 12.dp, top = 12.dp),
text = stringResourceSafe(R.string.tangempay_card_details_title),
color = TangemTheme.colors.text.tertiary,
style = TangemTheme.typography.subtitle2,
)
Text(
modifier = Modifier
.padding(top = 8.dp, end = 4.dp)
.clip(TangemTheme.shapes.roundedCornersMedium)
.clickable(onClick = state.onClick)
.padding(horizontal = 8.dp, vertical = 4.dp),
text = state.buttonText.resolveReference(),
color = TangemTheme.colors.text.accent,
style = TangemTheme.typography.body2,
)
}
}
@Composable
private fun CardDetailsTextContainer(
text: String,
isHidden: Boolean,
onCopy: () -> Unit,
modifier: Modifier = Modifier,
) {
Row(
modifier = modifier
.heightIn(min = 48.dp)
.background(color = TangemTheme.colors.field.primary, shape = RoundedCornerShape(16.dp)),
verticalAlignment = Alignment.CenterVertically,
) {
Text(
modifier = Modifier.padding(vertical = 4.dp, horizontal = 12.dp),
text = text,
maxLines = 1,
color = if (isHidden) TangemTheme.colors.text.disabled else TangemTheme.colors.text.primary1,
style = TangemTheme.typography.body2,
)
if (!isHidden) {
Spacer(modifier = Modifier.weight(1f))
IconButton(
modifier = Modifier
.padding(end = TangemTheme.dimens.spacing8)
.size(TangemTheme.dimens.size32),
onClick = onCopy,
) {
Icon(
modifier = Modifier.size(TangemTheme.dimens.size24),
painter = painterResource(id = R.drawable.ic_copy_new_24),
tint = TangemTheme.colors.icon.informative,
contentDescription = null,
)
}
}
}
}

View file

@ -2,20 +2,14 @@ package com.tangem.features.tangempay.ui
import android.content.res.Configuration
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.*
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.painterResource
@ -39,6 +33,8 @@ import com.tangem.core.ui.extensions.*
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.test.TokenDetailsTopBarTestTags
import com.tangem.features.tangempay.components.cardDetails.PreviewTangemPayCardDetailsBlockComponent
import com.tangem.features.tangempay.components.cardDetails.TangemPayCardDetailsBlockComponent
import com.tangem.features.tangempay.components.txHistory.PreviewTangemPayTxHistoryComponent
import com.tangem.features.tangempay.components.txHistory.TangemPayTxHistoryComponent
import com.tangem.features.tangempay.details.impl.R
@ -46,12 +42,11 @@ import com.tangem.features.tangempay.entity.*
import com.tangem.utils.StringsSigns.DASH_SIGN
import kotlinx.collections.immutable.persistentListOf
private const val REVEAL_ANIMATION_MILLIS = 500
@Composable
internal fun TangemPayDetailsScreen(
state: TangemPayDetailsUM,
txHistoryComponent: TangemPayTxHistoryComponent,
cardDetailsBlockComponent: TangemPayCardDetailsBlockComponent,
modifier: Modifier = Modifier,
snackbarHostState: SnackbarHostState = remember { SnackbarHostState() },
) {
@ -70,6 +65,7 @@ internal fun TangemPayDetailsScreen(
val listState = rememberLazyListState()
val bottomBarHeight = with(LocalDensity.current) { WindowInsets.systemBars.getBottom(this).toDp() }
val txHistoryState by txHistoryComponent.state.collectAsStateWithLifecycle()
val cardDetailsState by cardDetailsBlockComponent.state.collectAsStateWithLifecycle()
TangemPullToRefreshContainer(
config = state.pullToRefreshConfig,
@ -94,19 +90,11 @@ internal fun TangemPayDetailsScreen(
)
},
)
item(
key = TangemPayCardDetailsUM::class.java,
content = {
TangemPayCardDetailsBlock(
modifier = modifier
.padding(top = TangemTheme.dimens.spacing12)
.padding(horizontal = TangemTheme.dimens.spacing16)
.fillMaxWidth(),
state = state.cardDetailsUM,
)
},
)
with(cardDetailsBlockComponent) {
item(TangemPayCardDetailsUM::class.java) {
CardDetailsBlockContent(state = cardDetailsState)
}
}
with(txHistoryComponent) { txHistoryContent(listState = listState, state = txHistoryState) }
}
}
@ -218,136 +206,6 @@ private fun CryptoBalance(
}
// endregion
// region Card details block
@Composable
private fun TangemPayCardDetailsBlock(state: TangemPayCardDetailsUM, modifier: Modifier = Modifier) {
val alpha = if (state.isLoading) {
val infiniteTransition = rememberInfiniteTransition()
val animation by infiniteTransition.animateFloat(
initialValue = 1f,
targetValue = 0.3f,
animationSpec = infiniteRepeatable(
animation = tween(REVEAL_ANIMATION_MILLIS, easing = LinearEasing),
repeatMode = RepeatMode.Reverse,
),
)
animation
} else {
1f
}
Column(
modifier = modifier
.background(
color = TangemTheme.colors.background.primary,
shape = TangemTheme.shapes.roundedCornersMedium,
),
) {
CardDetailsBlockHeader(state)
CardDetailsTextContainer(
modifier = Modifier
.graphicsLayer { this.alpha = alpha }
.fillMaxWidth()
.padding(start = 12.dp, end = 12.dp, top = 4.dp),
text = state.number,
onCopy = { state.onCopy(state.number) },
isHidden = state.isHidden,
)
Row(
modifier = Modifier
.fillMaxWidth()
.padding(start = 12.dp, end = 12.dp, bottom = 12.dp, top = 8.dp),
horizontalArrangement = Arrangement.spacedBy(12.dp),
) {
CardDetailsTextContainer(
modifier = Modifier
.graphicsLayer { this.alpha = alpha }
.weight(1f),
text = state.expiry,
onCopy = { state.onCopy(state.expiry) },
isHidden = state.isHidden,
)
CardDetailsTextContainer(
modifier = Modifier
.graphicsLayer { this.alpha = alpha }
.weight(1f),
text = state.cvv,
onCopy = { state.onCopy(state.cvv) },
isHidden = state.isHidden,
)
}
}
}
@Composable
private fun CardDetailsBlockHeader(state: TangemPayCardDetailsUM, modifier: Modifier = Modifier) {
Row(
modifier = modifier
.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
) {
Text(
modifier = Modifier
.padding(start = 12.dp, top = 12.dp),
text = stringResourceSafe(R.string.tangempay_card_details_title),
color = TangemTheme.colors.text.tertiary,
style = TangemTheme.typography.subtitle2,
)
Text(
modifier = Modifier
.padding(top = 8.dp, end = 4.dp)
.clip(TangemTheme.shapes.roundedCornersMedium)
.clickable(onClick = state.onClick)
.padding(horizontal = 8.dp, vertical = 4.dp),
text = state.buttonText.resolveReference(),
color = TangemTheme.colors.text.accent,
style = TangemTheme.typography.body2,
)
}
}
@Composable
private fun CardDetailsTextContainer(
text: String,
isHidden: Boolean,
onCopy: () -> Unit,
modifier: Modifier = Modifier,
) {
Row(
modifier = modifier
.heightIn(min = 48.dp)
.background(color = TangemTheme.colors.field.primary, shape = RoundedCornerShape(16.dp)),
verticalAlignment = Alignment.CenterVertically,
) {
Text(
modifier = Modifier.padding(vertical = 4.dp, horizontal = 12.dp),
text = text,
maxLines = 1,
color = if (isHidden) TangemTheme.colors.text.disabled else TangemTheme.colors.text.primary1,
style = TangemTheme.typography.body2,
)
if (!isHidden) {
Spacer(modifier = Modifier.weight(1f))
IconButton(
modifier = Modifier
.padding(end = TangemTheme.dimens.spacing8)
.size(TangemTheme.dimens.size32),
onClick = onCopy,
) {
Icon(
modifier = Modifier.size(TangemTheme.dimens.size24),
painter = painterResource(id = R.drawable.ic_copy_new_24),
tint = TangemTheme.colors.icon.informative,
contentDescription = null,
)
}
}
}
}
// endregion
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun TangemPayDetailsTopAppBar(config: TangemPayDetailsTopBarConfig, modifier: Modifier = Modifier) {
@ -411,6 +269,16 @@ private fun TangemPayDetailsScreenPreview(
txHistoryComponent = PreviewTangemPayTxHistoryComponent(
txHistoryUM = PreviewTangemPayTxHistoryComponent.contentUM,
),
cardDetailsBlockComponent = PreviewTangemPayCardDetailsBlockComponent(
TangemPayCardDetailsUM(
number = "•••• •••• •••• 1245",
expiry = "••/••",
cvv = "•••",
onCopy = {},
onClick = {},
buttonText = TextReference.Res(R.string.tangempay_card_details_hide_text),
),
),
)
}
}
@ -432,28 +300,12 @@ private class TangemPayDetailsUMProvider : CollectionPreviewParameterProvider<Ta
fiatBalance = "$1234.56",
isBalanceFlickering = false,
),
cardDetailsUM = TangemPayCardDetailsUM(
number = "•••• •••• •••• 1245",
expiry = "••/••",
cvv = "•••",
onCopy = {},
onClick = {},
buttonText = TextReference.Res(R.string.tangempay_card_details_reveal_text),
),
isBalanceHidden = false,
),
TangemPayDetailsUM(
topBarConfig = TangemPayDetailsTopBarConfig(onBackClick = {}, items = null),
pullToRefreshConfig = PullToRefreshConfig(isRefreshing = false, onRefresh = {}),
balanceBlockState = TangemPayDetailsBalanceBlockState.Loading(actionButtons = persistentListOf()),
cardDetailsUM = TangemPayCardDetailsUM(
number = "•••• •••• •••• 1245",
expiry = "••/••",
cvv = "•••",
onCopy = {},
onClick = {},
buttonText = TextReference.Res(R.string.tangempay_card_details_hide_text),
),
isBalanceHidden = false,
),
),
@ -468,6 +320,16 @@ private fun TangemPayDetailsTxHistoryScreenPreview(
TangemPayDetailsScreen(
state = TangemPayDetailsUMProvider().values.first(),
txHistoryComponent = PreviewTangemPayTxHistoryComponent(txHistoryUM = state),
cardDetailsBlockComponent = PreviewTangemPayCardDetailsBlockComponent(
TangemPayCardDetailsUM(
number = "•••• •••• •••• 1245",
expiry = "••/••",
cvv = "•••",
onCopy = {},
onClick = {},
buttonText = TextReference.Res(R.string.tangempay_card_details_hide_text),
),
),
)
}
}