Updated on 2026-08-14

This commit is contained in:
Tangem 2026-05-06 22:27:57 +05:00
parent 3b1e642150
commit 27820ec8ad
24 changed files with 356 additions and 142 deletions

View file

@ -1690,6 +1690,7 @@
<string name="tangempay_card_details_withdraw_error_title">Withdraw unavailable now</string>
<string name="tangempay_card_details_withdraw_in_progress_description">You can\'t initiate swap or new withdrawal till the current one is finished</string>
<string name="tangempay_card_details_withdraw_in_progress_title">Withdrawal in progress</string>
<string name="tangempay_card_edit_name_placeholder">Card name</string>
<string name="tangempay_card_limit_setup_amount_subtitle">Set a limit from %s</string>
<string name="tangempay_card_limit_setup_error_message">We couldnt set the limit. Please try again</string>
<string name="tangempay_card_page_daily_limit_change">Change</string>

View file

@ -29,6 +29,7 @@ import com.tangem.domain.pay.repository.*
import com.tangem.domain.pay.usecase.GetPaymentAccountCryptoCurrencyStatusUseCase
import com.tangem.domain.pay.usecase.ProduceTangemPayInitialDataUseCase
import com.tangem.domain.pay.usecase.SetTangemPayCardLimitUseCase
import com.tangem.domain.pay.usecase.UpdateTangemPayCardNameUseCase
import com.tangem.domain.tangempay.GetTangemPayCurrencyStatusUseCase
import com.tangem.domain.tangempay.GetTangemPayCustomerIdUseCase
import com.tangem.domain.tangempay.TangemPayWithdrawUseCase
@ -148,6 +149,14 @@ internal interface TangemPayDataModule {
return SetTangemPayCardLimitUseCase(cardDetailsRepository, paymentAccountStatusFetcher)
}
@Provides
fun provideUpdateTangemPayCardNameUseCase(
cardDetailsRepository: TangemPayCardDetailsRepository,
paymentAccountStatusFetcher: PaymentAccountStatusFetcher,
): UpdateTangemPayCardNameUseCase {
return UpdateTangemPayCardNameUseCase(cardDetailsRepository, paymentAccountStatusFetcher)
}
@Provides
@Singleton
fun provideGetTangemPayCryptoCurrencyStatusUseCase(

View file

@ -1,8 +1,14 @@
package com.tangem.domain.pay.flow
import arrow.core.Either
import com.tangem.domain.core.flow.FlowFetcher
import com.tangem.domain.models.wallet.UserWalletId
interface PaymentAccountStatusFetcher : FlowFetcher<PaymentAccountStatusFetcher.Params> {
suspend operator fun invoke(userWalletId: UserWalletId): Either<Throwable, Unit> {
return invoke(Params(userWalletId))
}
data class Params(val userWalletId: UserWalletId)
}

View file

@ -17,9 +17,6 @@ class SetTangemPayCardLimitUseCase(
amount: BigDecimal,
): Either<UniversalError, Unit> {
return cardDetailsRepository.updateCardLimit(cardId, userWalletId, amount.toPlainString())
.onRight {
val params = PaymentAccountStatusFetcher.Params(userWalletId)
paymentAccountStatusFetcher.invoke(params)
}
.onRight { paymentAccountStatusFetcher.invoke(userWalletId) }
}
}

View file

@ -0,0 +1,22 @@
package com.tangem.domain.pay.usecase
import arrow.core.Either
import com.tangem.core.error.UniversalError
import com.tangem.domain.models.account.CardDisplayName
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.pay.flow.PaymentAccountStatusFetcher
import com.tangem.domain.pay.repository.TangemPayCardDetailsRepository
class UpdateTangemPayCardNameUseCase(
private val cardDetailsRepository: TangemPayCardDetailsRepository,
private val paymentAccountStatusFetcher: PaymentAccountStatusFetcher,
) {
suspend operator fun invoke(
cardId: String,
userWalletId: UserWalletId,
displayName: CardDisplayName,
): Either<UniversalError, Unit> {
return cardDetailsRepository.updateCardDisplayName(cardId, userWalletId, displayName)
.onRight { paymentAccountStatusFetcher(userWalletId) }
}
}

View file

@ -69,6 +69,10 @@ internal class DefaultTangemPayDetailsContainerComponent @AssistedInject constru
context = childByContext(componentContext = componentContext, router = innerRouter),
params = TangemPayCardPageComponent.Params(userWalletId = params.userWalletId, config = params.config),
)
TangemPayAccountDetailsInnerRoute.AddToWallet -> TangemPayAddToWalletComponent(
appComponentContext = childByContext(componentContext = componentContext, router = innerRouter),
params = params,
)
}
private fun onChildBack() {

View file

@ -25,7 +25,7 @@ internal class TangemPayAddToWalletComponent(
appComponentContext = child("cardDetailsBlockComponent"),
params = TangemPayCardDetailsBlockComponent.Params(
params = params,
isDisplayCardNameEnabled = false,
isEditingNameEnabled = false,
),
)

View file

@ -39,7 +39,7 @@ internal class TangemPayCardPageScreenComponent(
appComponentContext = child("cardDetailsBlockComponent"),
params = TangemPayCardDetailsBlockComponent.Params(
params = containerParams,
isDisplayCardNameEnabled = true,
isEditingNameEnabled = true,
),
)

View file

@ -24,7 +24,7 @@ internal class TangemPayEditDisplayNameComponent(
private val cardDetailsBlockComponent = DefaultTangemPayCardDetailsBlockComponent(
appComponentContext = child("editDisplayNameCardDetails"),
params = TangemPayCardDetailsBlockComponent.Params(params = params, isDisplayCardNameEnabled = true),
params = TangemPayCardDetailsBlockComponent.Params(params = params, isEditingNameEnabled = false),
)
@Composable
@ -33,7 +33,7 @@ internal class TangemPayEditDisplayNameComponent(
val cardDetailsState by cardDetailsBlockComponent.state.collectAsStateWithLifecycle()
val editingCardDetailsState = cardDetailsState.copy(
displayNameState = DisplayNameState.Editing(
displayName = state.editingValue,
displayName = state.editingValue.text,
editingValue = state.editingValue,
onValueChanged = state.onValueChanged,
onSubmit = state.onDoneClick,

View file

@ -16,6 +16,6 @@ internal interface TangemPayCardDetailsBlockComponent {
data class Params(
val params: TangemPayDetailsContainerComponent.Params,
val isDisplayCardNameEnabled: Boolean,
val isEditingNameEnabled: Boolean,
)
}

View file

@ -1,6 +1,7 @@
package com.tangem.features.tangempay.entity
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.domain.models.account.CardDisplayName
import com.tangem.domain.visa.model.TangemPayCardFrozenState
import com.tangem.features.tangempay.details.impl.R
import com.tangem.features.tangempay.model.CardDataType
@ -8,21 +9,33 @@ import com.tangem.utils.StringsSigns
internal class TangemPayCardDetailsBlockStateFactory(
private val cardNumberEnd: String,
private val displayNameState: DisplayNameState?,
private val displayName: CardDisplayName?,
private val isEditingNameEnabled: Boolean,
private val onEditNameClick: () -> Unit,
private val onReveal: () -> Unit,
private val onCopy: (String, CardDataType) -> Unit,
) {
fun getInitialState() = TangemPayCardDetailsUM(
number = "",
numberShort = "${StringsSigns.ASTERISK}$cardNumberEnd",
expiry = "",
cvv = "",
buttonText = resourceReference(R.string.tangempay_card_details_reveal_text),
onClick = onReveal,
onCopy = onCopy,
isHidden = true,
cardFrozenState = TangemPayCardFrozenState.Unfrozen,
displayNameState = displayNameState,
)
fun getInitialState(): TangemPayCardDetailsUM {
return TangemPayCardDetailsUM(
number = "",
numberShort = "${StringsSigns.ASTERISK}$cardNumberEnd",
expiry = "",
cvv = "",
buttonText = resourceReference(R.string.tangempay_card_details_reveal_text),
onClick = onReveal,
onCopy = onCopy,
isHidden = true,
cardFrozenState = TangemPayCardFrozenState.Unfrozen,
displayNameState = if (displayName != null) {
DisplayNameState.Display(
displayName = displayName.value,
onClick = onEditNameClick,
isEditingEnabled = isEditingNameEnabled,
)
} else {
null
},
)
}
}

View file

@ -59,6 +59,7 @@ internal class TangemPayDetailsStateFactory(
),
isBalanceHidden = false,
addFundsEnabled = true,
addToWalletBlockState = null,
accountDeactivatedNotificationConfig = NotificationConfig(
title = resourceReference(R.string.tangempay_account_deactivated_message_title),
subtitle = resourceReference(R.string.tangempay_account_deactivated_message_subtitle),

View file

@ -1,5 +1,6 @@
package com.tangem.features.tangempay.entity
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
import com.tangem.core.ui.components.notifications.NotificationConfig
@ -12,6 +13,7 @@ internal data class TangemPayDetailsUM(
val topBarConfig: TangemPayDetailsTopBarConfig,
val pullToRefreshConfig: PullToRefreshConfig,
val balanceBlockState: TangemPayDetailsBalanceBlockState,
val addToWalletBlockState: AddToWalletBlockState?,
val isBalanceHidden: Boolean,
val addFundsEnabled: Boolean,
val accountDeactivatedNotificationConfig: NotificationConfig?,
@ -39,15 +41,23 @@ internal sealed interface DisplayNameState {
data class Display(
override val displayName: String,
val onClick: () -> Unit,
val isEditingEnabled: Boolean,
) : DisplayNameState
data class Editing(
override val displayName: String,
val editingValue: String,
val onValueChanged: (String) -> Unit,
val editingValue: TextFieldValue,
val onValueChanged: (TextFieldValue) -> Unit,
val onSubmit: () -> Unit,
val onDismiss: () -> Unit,
) : DisplayNameState
fun copySealed(displayName: String): DisplayNameState {
return when (this) {
is Display -> copy(displayName = displayName)
is Editing -> copy(displayName = displayName)
}
}
}
internal sealed class TangemPayDetailsBalanceBlockState {

View file

@ -1,9 +1,11 @@
package com.tangem.features.tangempay.entity
import androidx.compose.ui.text.input.TextFieldValue
internal data class TangemPayEditDisplayNameUM(
val editingValue: String,
val editingValue: TextFieldValue,
val isLoading: Boolean,
val onValueChanged: (String) -> Unit,
val onValueChanged: (TextFieldValue) -> Unit,
val onDoneClick: () -> Unit,
val onDismiss: () -> Unit,
)

View file

@ -10,11 +10,16 @@ import com.tangem.core.decompose.ui.UiMessageSender
import com.tangem.core.ui.clipboard.ClipboardManager
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.message.SnackbarMessage
import com.tangem.domain.models.StatusSource
import com.tangem.domain.models.account.PaymentAccountStatusValue
import com.tangem.domain.models.account.hasCardWithId
import com.tangem.domain.models.account.requireCardWithId
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.pay.flow.PaymentAccountStatusSupplier
import com.tangem.domain.pay.repository.TangemPayCardDetailsRepository
import com.tangem.domain.tangempay.TangemPayAnalyticsEvents
import com.tangem.features.tangempay.components.cardDetails.TangemPayCardDetailsBlockComponent
import com.tangem.features.tangempay.details.impl.R
import com.tangem.features.tangempay.entity.DisplayNameState
import com.tangem.features.tangempay.entity.TangemPayCardDetailsBlockStateFactory
import com.tangem.features.tangempay.entity.TangemPayCardDetailsUM
import com.tangem.features.tangempay.model.listener.CardDetailsEvent
@ -22,10 +27,12 @@ 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.features.tangempay.model.transformers.TangemPayCardDetailsUpdateNameTransformer
import com.tangem.features.tangempay.navigation.TangemPayCardDetailsInnerRoute
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.delay
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
@ -46,20 +53,16 @@ internal class TangemPayCardDetailsBlockModel @Inject constructor(
private val cardDetailsEventListener: CardDetailsEventListener,
private val analytics: AnalyticsEventHandler,
private val router: Router,
private val paymentAccountStatusSupplier: PaymentAccountStatusSupplier,
) : Model() {
private val params: TangemPayCardDetailsBlockComponent.Params = paramsContainer.require()
private val stateFactory = TangemPayCardDetailsBlockStateFactory(
cardNumberEnd = params.params.config.cardNumberEnd,
displayNameState = if (params.isDisplayCardNameEnabled && params.params.config.displayName != null) {
DisplayNameState.Display(
displayName = requireNotNull(params.params.config.displayName).value,
onClick = ::startEditingDisplayName,
)
} else {
null
},
displayName = params.params.config.displayName,
isEditingNameEnabled = params.isEditingNameEnabled,
onEditNameClick = ::startEditingDisplayName,
onReveal = ::revealCardDetails,
onCopy = ::copyData,
)
@ -71,6 +74,7 @@ internal class TangemPayCardDetailsBlockModel @Inject constructor(
private val showCardDetailsTimerJobHolder = JobHolder()
init {
subscribeToCardNameChanges(cardId = params.params.config.cardId, userWalletId = params.params.userWalletId)
subscribeToCardFrozenState()
modelScope.launch {
cardDetailsEventListener.event.collectLatest { event ->
@ -82,6 +86,23 @@ internal class TangemPayCardDetailsBlockModel @Inject constructor(
}
}
private fun subscribeToCardNameChanges(cardId: String, userWalletId: UserWalletId) {
paymentAccountStatusSupplier.invoke(userWalletId)
.onEach { state ->
val status = state.value
if (status is PaymentAccountStatusValue.Loaded &&
status.source == StatusSource.ACTUAL &&
status.hasCardWithId(cardId)
) {
val card = status.requireCardWithId(cardId)
val displayName = card.displayName ?: return@onEach
uiState.update(TangemPayCardDetailsUpdateNameTransformer(displayName))
}
}
.launchIn(modelScope)
}
private fun subscribeToCardFrozenState() {
cardDetailsRepository
.cardFrozenState(params.params.config.cardId)

View file

@ -39,10 +39,7 @@ import com.tangem.features.tangempay.entity.TangemPayDetailsStateFactory
import com.tangem.features.tangempay.entity.TangemPayDetailsUM
import com.tangem.features.tangempay.model.listener.CardDetailsEvent
import com.tangem.features.tangempay.model.listener.CardDetailsEventListener
import com.tangem.features.tangempay.model.transformers.DetailBalanceVisibilityTransformer
import com.tangem.features.tangempay.model.transformers.DetailsBalanceTransformer
import com.tangem.features.tangempay.model.transformers.TangemPayDetailsRefreshTransformer
import com.tangem.features.tangempay.model.transformers.TangemPayFreezeUnfreezeStateTransformer
import com.tangem.features.tangempay.model.transformers.*
import com.tangem.features.tangempay.navigation.TangemPayAccountDetailsInnerRoute
import com.tangem.features.tangempay.utils.TangemPayDetailIntents
import com.tangem.features.tangempay.utils.TangemPayMessagesFactory
@ -56,7 +53,10 @@ import com.tangem.utils.coroutines.saveIn
import com.tangem.utils.logging.TangemLogger
import com.tangem.utils.transformer.update
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import javax.inject.Inject
@ -100,6 +100,7 @@ internal class TangemPayDetailsModel @Inject constructor(
private val refreshStateJobHolder = JobHolder()
private val fetchBalanceJobHolder = JobHolder()
private val addToWalletBannerJobHolder = JobHolder()
private var balance: TangemPayCardBalance? = null
@ -116,6 +117,7 @@ internal class TangemPayDetailsModel @Inject constructor(
fetchBalance()
if (!params.config.isTangemPayDeactivated) {
subscribeToCardFrozenState()
fetchAddToWalletBanner()
}
}
@ -231,6 +233,24 @@ internal class TangemPayDetailsModel @Inject constructor(
}.saveIn(fetchBalanceJobHolder)
}
private fun fetchAddToWalletBanner() {
modelScope.launch {
val isDone = try {
cardDetailsRepository.isAddToWalletDone(params.userWalletId).getOrNull() == true
} catch (e: Exception) {
TangemLogger.e("Error", e)
return@launch
}
uiState.update(
transformer = DetailsAddToWalletBannerTransformer(
onClickBanner = ::onClickAddToWalletBlock,
onClickCloseBanner = ::onClickCloseAddToWalletBlock,
isDone = isDone,
),
)
}.saveIn(addToWalletBannerJobHolder)
}
private fun handleBalanceHiding() {
getBalanceHidingSettingsUseCase().onEach {
uiState.update(DetailBalanceVisibilityTransformer(isHidden = it.isBalanceHidden))
@ -260,6 +280,28 @@ internal class TangemPayDetailsModel @Inject constructor(
}.saveIn(refreshStateJobHolder)
}
private fun onClickAddToWalletBlock() {
analytics.send(TangemPayAnalyticsEvents.AddToWalletClicked())
router.push(TangemPayAccountDetailsInnerRoute.AddToWallet)
}
private fun onClickCloseAddToWalletBlock() {
modelScope.launch {
try {
cardDetailsRepository.setAddToWalletAsDone(params.userWalletId)
} catch (e: Exception) {
TangemLogger.e("Error", e)
}
uiState.update(
transformer = DetailsAddToWalletBannerTransformer(
onClickBanner = ::onClickAddToWalletBlock,
onClickCloseBanner = ::onClickCloseAddToWalletBlock,
isDone = true,
),
)
}.saveIn(addToWalletBannerJobHolder)
}
private fun onOpenMenu() {
analytics.send(TangemPayAnalyticsEvents.CardSettingsClicked())
}

View file

@ -1,6 +1,8 @@
package com.tangem.features.tangempay.model
import androidx.compose.runtime.Stable
import androidx.compose.ui.text.TextRange
import androidx.compose.ui.text.input.TextFieldValue
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
@ -8,15 +10,19 @@ import com.tangem.core.decompose.navigation.Router
import com.tangem.core.decompose.ui.UiMessageSender
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.message.DialogMessage
import com.tangem.domain.models.StatusSource
import com.tangem.domain.models.account.CardDisplayName
import com.tangem.domain.pay.repository.TangemPayCardDetailsRepository
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.wallet.UserWalletId
import com.tangem.domain.pay.flow.PaymentAccountStatusSupplier
import com.tangem.domain.pay.usecase.UpdateTangemPayCardNameUseCase
import com.tangem.features.tangempay.components.TangemPayDetailsContainerComponent
import com.tangem.features.tangempay.details.impl.R
import com.tangem.features.tangempay.entity.TangemPayEditDisplayNameUM
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import javax.inject.Inject
@ -26,8 +32,9 @@ internal class TangemPayEditDisplayNameModel @Inject constructor(
paramsContainer: ParamsContainer,
override val dispatchers: CoroutineDispatcherProvider,
private val router: Router,
private val cardDetailsRepository: TangemPayCardDetailsRepository,
private val updateCardNameUseCase: UpdateTangemPayCardNameUseCase,
private val uiMessageSender: UiMessageSender,
private val paymentAccountStatusSupplier: PaymentAccountStatusSupplier,
) : Model() {
private val params: TangemPayDetailsContainerComponent.Params = paramsContainer.require()
@ -37,7 +44,10 @@ internal class TangemPayEditDisplayNameModel @Inject constructor(
val uiState: StateFlow<TangemPayEditDisplayNameUM>
field = MutableStateFlow(
TangemPayEditDisplayNameUM(
editingValue = originalDisplayName,
editingValue = TextFieldValue(
text = originalDisplayName,
selection = TextRange(originalDisplayName.length),
),
isLoading = false,
onValueChanged = ::onValueChanged,
onDoneClick = ::onDoneClick,
@ -45,23 +55,51 @@ internal class TangemPayEditDisplayNameModel @Inject constructor(
),
)
private fun onValueChanged(value: String) {
if (value.length <= CardDisplayName.MAX_LENGTH) {
init {
subscribeToCardNameChanges(params.config.cardId, params.userWalletId)
}
private fun subscribeToCardNameChanges(cardId: String, userWalletId: UserWalletId) {
paymentAccountStatusSupplier.invoke(userWalletId)
.onEach { state ->
val status = state.value
if (status is PaymentAccountStatusValue.Loaded &&
status.source == StatusSource.ACTUAL &&
status.hasCardWithId(cardId)
) {
val card = status.requireCardWithId(cardId)
val displayName = card.displayName ?: return@onEach
uiState.update { uiState ->
uiState.copy(
editingValue = TextFieldValue(
text = displayName.value,
selection = TextRange(displayName.value.length),
),
)
}
}
}
.launchIn(modelScope)
}
private fun onValueChanged(value: TextFieldValue) {
if (value.text.length <= CardDisplayName.MAX_LENGTH) {
uiState.update { it.copy(editingValue = value) }
}
}
private fun onDoneClick() {
val currentValue = uiState.value.editingValue
val currentValue = uiState.value.editingValue.text
if (currentValue.trim() == originalDisplayName.trim()) {
router.pop()
return
}
CardDisplayName(currentValue)
.onRight { cardDisplayName ->
uiState.update { it.copy(isLoading = true) }
modelScope.launch {
cardDetailsRepository.updateCardDisplayName(
uiState.update { it.copy(isLoading = true) }
updateCardNameUseCase(
cardId = params.config.cardId,
userWalletId = params.userWalletId,
displayName = cardDisplayName,

View file

@ -0,0 +1,22 @@
package com.tangem.features.tangempay.model.transformers
import com.tangem.features.tangempay.entity.AddToWalletBlockState
import com.tangem.features.tangempay.entity.TangemPayDetailsUM
import com.tangem.utils.transformer.Transformer
internal class DetailsAddToWalletBannerTransformer(
private val onClickBanner: () -> Unit,
private val onClickCloseBanner: () -> Unit,
private val isDone: Boolean,
) : Transformer<TangemPayDetailsUM> {
override fun transform(prevState: TangemPayDetailsUM): TangemPayDetailsUM {
return prevState.copy(
addToWalletBlockState = if (isDone) {
null
} else {
AddToWalletBlockState(onClick = onClickBanner, onClickClose = onClickCloseBanner)
},
)
}
}

View file

@ -0,0 +1,15 @@
package com.tangem.features.tangempay.model.transformers
import com.tangem.domain.models.account.CardDisplayName
import com.tangem.features.tangempay.entity.TangemPayCardDetailsUM
import com.tangem.utils.transformer.Transformer
internal class TangemPayCardDetailsUpdateNameTransformer(
private val displayName: CardDisplayName,
) : Transformer<TangemPayCardDetailsUM> {
override fun transform(prevState: TangemPayCardDetailsUM): TangemPayCardDetailsUM {
val displayNameState = prevState.displayNameState ?: return prevState
return prevState.copy(displayNameState = displayNameState.copySealed(displayName = displayName.value))
}
}

View file

@ -10,4 +10,7 @@ internal sealed class TangemPayAccountDetailsInnerRoute : Route {
@Serializable
data object CardDetails : TangemPayAccountDetailsInnerRoute()
@Serializable
data object AddToWallet : TangemPayAccountDetailsInnerRoute()
}

View file

@ -1,24 +1,22 @@
package com.tangem.features.tangempay.ui
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.EaseInOut
import androidx.compose.animation.core.MutableTransitionState
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
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.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
@ -46,6 +44,7 @@ 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.extensions.conditional
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme
@ -58,7 +57,7 @@ import com.tangem.features.tangempay.entity.DisplayNameState
import com.tangem.features.tangempay.entity.TangemPayCardDetailsUM
import com.tangem.features.tangempay.model.CardDataType
private const val ICON_FADE_DURATION_MS = 300
private const val TEXT_WIDTH_PADDING = 2
private val CustomCardBlockColor = Color(0x1F828282)
@Suppress("MagicNumber")
@ -228,80 +227,55 @@ private fun TangemPayCardDetailsHiddenBlock(state: TangemPayCardDetailsUM, modif
@Composable
private fun CardDisplayName(state: DisplayNameState, modifier: Modifier = Modifier) {
val isDisplayMode = state is DisplayNameState.Display
Row(
modifier = modifier.then(
if (state is DisplayNameState.Display) {
Modifier.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = null,
onClick = state.onClick,
)
} else {
Modifier
},
),
verticalAlignment = Alignment.CenterVertically,
) {
when (state) {
is DisplayNameState.Display -> DisplayOnlyCardDisplayName(state = state)
is DisplayNameState.Editing -> EditingCardDisplayName(state = state)
}
val iconVisibleState = remember {
MutableTransitionState(initialState = !isDisplayMode).apply {
targetState = isDisplayMode
}
}
AnimatedVisibility(
visibleState = iconVisibleState,
enter = fadeIn(animationSpec = tween(durationMillis = ICON_FADE_DURATION_MS)),
exit = fadeOut(animationSpec = tween(durationMillis = ICON_FADE_DURATION_MS)),
) {
Row(verticalAlignment = Alignment.CenterVertically) {
Spacer(modifier = Modifier.width(6.dp))
Icon(
painter = painterResource(id = com.tangem.core.ui.R.drawable.ic_edit_new_12),
contentDescription = null,
modifier = Modifier.size(10.dp),
tint = TangemTheme.colors.text.constantWhite,
)
}
}
when (state) {
is DisplayNameState.Display -> DisplayOnlyCardDisplayName(modifier = modifier, state = state)
is DisplayNameState.Editing -> EditingCardDisplayName(modifier = modifier, state = state)
}
}
@Composable
private fun DisplayOnlyCardDisplayName(state: DisplayNameState.Display, modifier: Modifier = Modifier) {
Text(
text = state.displayName,
style = TangemTheme.typography.caption1.copy(color = TangemTheme.colors.text.constantWhite),
maxLines = 1,
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,
)
}
}
}
@Composable
private fun EditingCardDisplayName(state: DisplayNameState.Editing, modifier: Modifier = Modifier) {
val focusRequester = remember { FocusRequester() }
var textFieldValue by remember(state.editingValue) {
mutableStateOf(
TextFieldValue(text = state.editingValue, selection = TextRange(state.editingValue.length)),
)
}
val placeholder = stringResourceSafe(R.string.tangempay_card_edit_name_placeholder)
val textStyle = TangemTheme.typography.caption1.copy(color = TangemTheme.colors.text.constantWhite)
val textMeasurer = rememberTextMeasurer()
val measuredText = state.editingValue.text.ifEmpty { placeholder }
val textWidthDp = with(LocalDensity.current) {
textMeasurer.measure(textFieldValue.text, textStyle).size.width.toDp() + 2.dp
textMeasurer.measure(measuredText, textStyle).size.width.toDp() + TEXT_WIDTH_PADDING.dp
}
BasicTextField(
value = textFieldValue,
value = state.editingValue,
onValueChange = { newValue ->
if (newValue.text.length in 0..CardDisplayName.MAX_LENGTH) {
textFieldValue = newValue
state.onValueChanged(newValue.text)
if (newValue.text.length <= CardDisplayName.MAX_LENGTH) {
state.onValueChanged(newValue)
}
},
modifier = modifier
@ -312,6 +286,17 @@ private fun EditingCardDisplayName(state: DisplayNameState.Editing, modifier: Mo
cursorBrush = SolidColor(TangemTheme.colors.text.constantWhite),
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done),
keyboardActions = KeyboardActions(onDone = { state.onSubmit() }),
decorationBox = { innerTextField ->
Box {
if (state.editingValue.text.isEmpty()) {
Text(
text = placeholder,
style = textStyle.copy(color = TangemTheme.colors.text.tertiary),
)
}
innerTextField()
}
},
)
LaunchedEffect(Unit) { focusRequester.requestFocus() }
@ -492,7 +477,13 @@ private class TangemPayCardDetailsUMProvider : CollectionPreviewParameterProvide
onCopy = { _, _ -> },
isHidden = true,
cardFrozenState = TangemPayCardFrozenState.Frozen,
displayNameState = DisplayNameState.Display(displayName = "Tangem", onClick = {}),
displayNameState = DisplayNameState.Editing(
displayName = "Tangem",
editingValue = TextFieldValue(text = "movet", selection = TextRange("movet".length)),
onValueChanged = {},
onSubmit = {},
onDismiss = {},
),
),
TangemPayCardDetailsUM(
isLoading = false,
@ -505,7 +496,13 @@ private class TangemPayCardDetailsUMProvider : CollectionPreviewParameterProvide
onCopy = { _, _ -> },
isHidden = true,
cardFrozenState = TangemPayCardFrozenState.Unfrozen,
displayNameState = DisplayNameState.Display(displayName = "Tangem Pay Card", onClick = {}),
displayNameState = DisplayNameState.Editing(
displayName = "Tangem",
editingValue = TextFieldValue(text = ""),
onValueChanged = {},
onSubmit = {},
onDismiss = {},
),
),
TangemPayCardDetailsUM(
isLoading = false,
@ -518,7 +515,11 @@ private class TangemPayCardDetailsUMProvider : CollectionPreviewParameterProvide
onCopy = { _, _ -> },
isHidden = true,
cardFrozenState = TangemPayCardFrozenState.Pending,
displayNameState = DisplayNameState.Display(displayName = "Tangem Pay Card", onClick = {}),
displayNameState = DisplayNameState.Display(
displayName = "Tangem Pay Card",
onClick = {},
isEditingEnabled = true,
),
),
TangemPayCardDetailsUM(
isLoading = false,
@ -531,7 +532,11 @@ private class TangemPayCardDetailsUMProvider : CollectionPreviewParameterProvide
expiry = "12/34",
cvv = "123",
cardFrozenState = TangemPayCardFrozenState.Unfrozen,
displayNameState = DisplayNameState.Display(displayName = "Tangem Pay Card", onClick = {}),
displayNameState = DisplayNameState.Display(
displayName = "Tangem Pay Card",
onClick = {},
isEditingEnabled = false,
),
),
),
)

View file

@ -8,18 +8,7 @@ import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.foundation.background
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.PaddingValues
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.exclude
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.navigationBars
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.foundation.layout.systemBars
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyItemScope
import androidx.compose.foundation.lazy.LazyListScope
@ -46,11 +35,7 @@ import com.tangem.domain.visa.model.TangemPayCardFrozenState
import com.tangem.features.tangempay.components.cardDetails.PreviewTangemPayCardDetailsBlockComponent
import com.tangem.features.tangempay.components.cardDetails.TangemPayCardDetailsBlockComponent
import com.tangem.features.tangempay.details.impl.R
import com.tangem.features.tangempay.entity.DisplayNameState
import com.tangem.features.tangempay.entity.TangemPayCardDetailsUM
import com.tangem.features.tangempay.entity.TangemPayCardPageSetting
import com.tangem.features.tangempay.entity.TangemPayCardPageUM
import com.tangem.features.tangempay.entity.TangemPayDailyLimitBlockState
import com.tangem.features.tangempay.entity.*
import kotlinx.collections.immutable.ImmutableList
private const val CONTENT_FADE_DURATION_MS = 300
@ -217,7 +202,11 @@ private fun preview() = TangemThemePreview {
onCopy = { _, _ -> },
onClick = {},
cardFrozenState = TangemPayCardFrozenState.Unfrozen,
displayNameState = DisplayNameState.Display(displayName = "Tangem Pay Card", onClick = {}),
displayNameState = DisplayNameState.Display(
displayName = "Tangem Pay Card",
onClick = {},
isEditingEnabled = true,
),
),
),
cardDetailsState = TangemPayCardDetailsUM(
@ -228,7 +217,11 @@ private fun preview() = TangemThemePreview {
onCopy = { _, _ -> },
onClick = {},
cardFrozenState = TangemPayCardFrozenState.Unfrozen,
displayNameState = DisplayNameState.Display(displayName = "Tangem Pay Card", onClick = {}),
displayNameState = DisplayNameState.Display(
displayName = "Tangem Pay Card",
onClick = {},
isEditingEnabled = false,
),
),
)
}

View file

@ -56,10 +56,7 @@ import com.tangem.features.tangempay.components.express.PreviewEmptyExpressTrans
import com.tangem.features.tangempay.components.txHistory.PreviewTangemPayTxHistoryComponent
import com.tangem.features.tangempay.components.txHistory.TangemPayTxHistoryComponent
import com.tangem.features.tangempay.details.impl.R
import com.tangem.features.tangempay.entity.TangemPayDetailsBalanceBlockState
import com.tangem.features.tangempay.entity.TangemPayDetailsTopBarConfig
import com.tangem.features.tangempay.entity.TangemPayDetailsUM
import com.tangem.features.tangempay.entity.TangemPayTxHistoryUM
import com.tangem.features.tangempay.entity.*
import com.tangem.features.tokendetails.ExpressTransactionsComponent
import com.tangem.utils.StringsSigns.DASH_SIGN
import kotlinx.collections.immutable.persistentListOf
@ -116,6 +113,17 @@ internal fun TangemPayDetailsScreen(
SpacerH12()
},
)
if (state.addToWalletBlockState != null) {
item(
key = AddToWalletBlockState::class.java,
content = {
TangemPayAddToWalletBlock(
state = state.addToWalletBlockState,
modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing16),
)
},
)
}
if (state.accountDeactivatedNotificationConfig != null) {
item(
key = "DEACTIVATION_MESSAGE",
@ -420,6 +428,7 @@ private class TangemPayDetailsUMProvider : CollectionPreviewParameterProvider<Ta
),
isBalanceHidden = false,
addFundsEnabled = true,
addToWalletBlockState = AddToWalletBlockState(onClick = {}, onClickClose = {}),
accountDeactivatedNotificationConfig = null,
),
TangemPayDetailsUM(
@ -434,6 +443,7 @@ private class TangemPayDetailsUMProvider : CollectionPreviewParameterProvider<Ta
),
isBalanceHidden = false,
addFundsEnabled = true,
addToWalletBlockState = null,
accountDeactivatedNotificationConfig = null,
),
),

View file

@ -75,7 +75,7 @@ internal fun TangemPayEditDisplayNameScreen(
textReference = resourceReference(R.string.common_done),
onClick = state.onDoneClick,
shouldShowProgress = state.isLoading,
isEnabled = !state.isLoading && state.editingValue.isNotBlank(),
isEnabled = !state.isLoading && state.editingValue.text.isNotBlank(),
),
)
}