Updated on 2026-08-14

This commit is contained in:
Tangem 2025-12-19 18:28:10 +04:00
parent 0594e1b680
commit a8faaffcab
24 changed files with 561 additions and 61 deletions

View file

@ -100,6 +100,14 @@ internal class TangemPayDetailsComponent(
listener = model,
),
)
is TangemPayDetailsNavigation.ViewPinCode -> TangemPayViewPinComponent(
appComponentContext = context,
params = TangemPayViewPinComponent.Params(
walletId = navigation.userWalletId,
cardId = navigation.cardId,
listener = model,
),
)
}
}
}

View file

@ -0,0 +1,44 @@
package com.tangem.features.tangempay.components
import androidx.activity.compose.BackHandler
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.core.ui.decompose.ComposableBottomSheetComponent
import com.tangem.core.ui.security.DisableScreenshotsDisposableEffect
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.features.tangempay.model.TangemPayViewPinModel
import com.tangem.features.tangempay.ui.TangemPayViewPinContent
internal class TangemPayViewPinComponent(
appComponentContext: AppComponentContext,
params: Params,
) : ComposableBottomSheetComponent, AppComponentContext by appComponentContext {
private val model: TangemPayViewPinModel = getOrCreateModel(params = params)
override fun dismiss() {
model.onDismiss()
}
@Composable
override fun BottomSheet() {
val state by model.uiState.collectAsStateWithLifecycle()
BackHandler(onBack = ::dismiss)
DisableScreenshotsDisposableEffect()
TangemPayViewPinContent(state = state)
}
data class Params(
val listener: ViewPinListener,
val walletId: UserWalletId,
val cardId: String,
)
}
internal interface ViewPinListener {
fun onClickChangePin()
fun onDismissViewPin()
}

View file

@ -9,6 +9,7 @@ import com.tangem.features.tangempay.model.TangemPayChangePinModel
import com.tangem.features.tangempay.model.TangemPayDetailsModel
import com.tangem.features.tangempay.model.TangemPayTxHistoryDetailsModel
import com.tangem.features.tangempay.model.TangemPayTxHistoryModel
import com.tangem.features.tangempay.model.TangemPayViewPinModel
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
@ -53,4 +54,9 @@ internal interface TangemPayModelModule {
@IntoMap
@ClassKey(TangemPayAddFundsModel::class)
fun bindTangemPayAddFundsModel(model: TangemPayAddFundsModel): Model
@Binds
@IntoMap
@ClassKey(TangemPayViewPinModel::class)
fun bindTangemPayViewPinModel(model: TangemPayViewPinModel): Model
}

View file

@ -26,4 +26,10 @@ internal sealed class TangemPayDetailsNavigation {
val transaction: TangemPayTxHistoryItem,
val isBalanceHidden: Boolean,
) : TangemPayDetailsNavigation()
@Serializable
data class ViewPinCode(
val userWalletId: UserWalletId,
val cardId: String,
) : TangemPayDetailsNavigation()
}

View file

@ -51,9 +51,9 @@ internal class TangemPayDetailsStateFactory(
TangemPayDetailsTopBarMenuItem(
type = TangemPayDetailsTopBarMenuItemType.ChangePin,
dropdownItem = TangemDropdownMenuItem(
title = resourceReference(R.string.tangempay_card_details_change_pin),
title = resourceReference(R.string.tangempay_card_details_pin_code),
textColor = themedColor { TangemTheme.colors.text.primary1 },
onClick = intents::onClickChangePin,
onClick = intents::onClickPinCode,
),
),
TangemPayDetailsTopBarMenuItem(

View file

@ -0,0 +1,23 @@
package com.tangem.features.tangempay.entity
import com.tangem.core.ui.components.bottomsheets.message.MessageBottomSheetUMV2
internal sealed class TangemPayViewPinUM {
open val onDismiss: () -> Unit = {}
data class Loading(
override val onDismiss: () -> Unit,
) : TangemPayViewPinUM()
data class Content(
val pin: String,
val onClickChangePin: () -> Unit,
override val onDismiss: () -> Unit,
) : TangemPayViewPinUM()
data class Error(
val errorMessage: MessageBottomSheetUMV2,
override val onDismiss: () -> Unit,
) : TangemPayViewPinUM()
}

View file

@ -33,6 +33,7 @@ import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
import com.tangem.features.tangempay.TangemPayConstants
import com.tangem.features.tangempay.components.AddFundsListener
import com.tangem.features.tangempay.components.TangemPayDetailsContainerComponent
import com.tangem.features.tangempay.components.ViewPinListener
import com.tangem.features.tangempay.details.impl.R
import com.tangem.features.tangempay.entity.TangemPayDetailsErrorType
import com.tangem.features.tangempay.entity.TangemPayDetailsNavigation
@ -77,7 +78,7 @@ internal class TangemPayDetailsModel @Inject constructor(
private val orderRepository: CustomerOrderRepository,
private val getUserWalletUseCase: GetUserWalletUseCase,
private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase,
) : Model(), TangemPayTxHistoryUiActions, TangemPayDetailIntents, AddFundsListener {
) : Model(), TangemPayTxHistoryUiActions, TangemPayDetailIntents, AddFundsListener, ViewPinListener {
private val params: TangemPayDetailsContainerComponent.Params = paramsContainer.require()
@ -124,8 +125,17 @@ internal class TangemPayDetailsModel @Inject constructor(
.launchIn(modelScope)
}
override fun onClickChangePin() {
router.push(TangemPayDetailsInnerRoute.ChangePIN)
override fun onClickPinCode() {
if (!params.config.isPinSet) {
router.push(TangemPayDetailsInnerRoute.ChangePIN)
} else {
bottomSheetNavigation.activate(
TangemPayDetailsNavigation.ViewPinCode(
userWalletId = params.userWalletId,
cardId = params.config.cardId,
),
)
}
}
override fun onClickFreezeCard() {
@ -389,6 +399,15 @@ internal class TangemPayDetailsModel @Inject constructor(
bottomSheetNavigation.dismiss()
}
override fun onClickChangePin() {
bottomSheetNavigation.dismiss()
router.push(TangemPayDetailsInnerRoute.ChangePIN)
}
override fun onDismissViewPin() {
bottomSheetNavigation.dismiss()
}
override fun onTransactionClick(item: TangemPayTxHistoryItem) {
bottomSheetNavigation.activate(
configuration = TangemPayDetailsNavigation.TransactionDetails(

View file

@ -0,0 +1,59 @@
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.domain.pay.repository.TangemPayCardDetailsRepository
import com.tangem.features.tangempay.components.TangemPayViewPinComponent
import com.tangem.features.tangempay.entity.TangemPayViewPinUM
import com.tangem.features.tangempay.model.transformers.TangemPayViewPinErrorStateTransformer
import com.tangem.features.tangempay.model.transformers.TangemPayViewPinSuccessStateTransformer
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.transformer.update
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
import javax.inject.Inject
@Stable
@ModelScoped
internal class TangemPayViewPinModel @Inject constructor(
paramsContainer: ParamsContainer,
override val dispatchers: CoroutineDispatcherProvider,
private val cardDetailsRepository: TangemPayCardDetailsRepository,
) : Model() {
private val params = paramsContainer.require<TangemPayViewPinComponent.Params>()
val uiState: StateFlow<TangemPayViewPinUM>
field = MutableStateFlow(getInitialState())
init {
getCardPin()
}
private fun getCardPin() {
modelScope.launch {
cardDetailsRepository.getPin(userWalletId = params.walletId, cardId = params.cardId)
.onRight { pin ->
if (!pin.isNullOrEmpty()) {
uiState.update(TangemPayViewPinSuccessStateTransformer(pin, params.listener::onClickChangePin))
} else {
uiState.update(TangemPayViewPinErrorStateTransformer())
}
}
.onLeft {
uiState.update(TangemPayViewPinErrorStateTransformer())
}
}
}
private fun getInitialState(): TangemPayViewPinUM {
return TangemPayViewPinUM.Loading(onDismiss = ::onDismiss)
}
fun onDismiss() {
params.listener.onDismissViewPin()
}
}

View file

@ -0,0 +1,35 @@
package com.tangem.features.tangempay.model.transformers
import com.tangem.core.ui.R
import com.tangem.core.ui.components.bottomsheets.message.MessageBottomSheetUMV2
import com.tangem.core.ui.components.bottomsheets.message.icon
import com.tangem.core.ui.components.bottomsheets.message.infoBlock
import com.tangem.core.ui.components.bottomsheets.message.onClick
import com.tangem.core.ui.components.bottomsheets.message.secondaryButton
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.message.bottomSheetMessage
import com.tangem.features.tangempay.entity.TangemPayViewPinUM
import com.tangem.utils.transformer.Transformer
internal class TangemPayViewPinErrorStateTransformer : Transformer<TangemPayViewPinUM> {
override fun transform(prevState: TangemPayViewPinUM): TangemPayViewPinUM {
return TangemPayViewPinUM.Error(
errorMessage = bottomSheetMessage {
infoBlock {
icon(R.drawable.img_attention_20) {
backgroundType = MessageBottomSheetUMV2.Icon.BackgroundType.Attention
}
title = TextReference.Res(R.string.common_error)
body = TextReference.Res(R.string.common_unknown_error)
}
secondaryButton {
text = resourceReference(R.string.common_got_it)
onClick { closeBs() }
}
}.messageBottomSheetUMV2,
onDismiss = prevState.onDismiss,
)
}
}

View file

@ -0,0 +1,18 @@
package com.tangem.features.tangempay.model.transformers
import com.tangem.features.tangempay.entity.TangemPayViewPinUM
import com.tangem.utils.transformer.Transformer
internal class TangemPayViewPinSuccessStateTransformer(
private val pin: String,
private val onClickChangePin: () -> Unit,
) : Transformer<TangemPayViewPinUM> {
override fun transform(prevState: TangemPayViewPinUM): TangemPayViewPinUM {
return TangemPayViewPinUM.Content(
pin = pin,
onClickChangePin = onClickChangePin,
onDismiss = prevState.onDismiss,
)
}
}

View file

@ -0,0 +1,49 @@
package com.tangem.features.tangempay.ui
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
@Composable
internal fun PinDigitBox(
digit: String?,
backgroundColor: Color,
borderColor: Color,
textColor: Color,
textStyle: TextStyle,
modifier: Modifier = Modifier,
) {
Box(
modifier = modifier
.size(width = 48.dp, height = 64.dp)
.background(
color = backgroundColor,
shape = RoundedCornerShape(12.dp),
)
.border(
width = 1.dp,
color = borderColor,
shape = RoundedCornerShape(12.dp),
),
contentAlignment = Alignment.Center,
) {
if (digit != null) {
Text(
text = digit,
style = textStyle,
color = textColor,
textAlign = TextAlign.Center,
)
}
}
}

View file

@ -1,11 +1,8 @@
package com.tangem.features.tangempay.ui
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
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
@ -17,11 +14,9 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Color.Companion.Transparent
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.style.TextAlign
@ -187,38 +182,4 @@ private fun PinCode(
}
},
)
}
@Composable
private fun PinDigitBox(
digit: String?,
backgroundColor: Color,
borderColor: Color,
textColor: Color,
textStyle: TextStyle,
modifier: Modifier = Modifier,
) {
Box(
modifier = modifier
.size(width = 48.dp, height = 64.dp)
.background(
color = backgroundColor,
shape = RoundedCornerShape(12.dp),
)
.border(
width = 1.dp,
color = borderColor,
shape = RoundedCornerShape(12.dp),
),
contentAlignment = Alignment.Center,
) {
if (digit != null) {
Text(
text = digit,
style = textStyle,
color = textColor,
textAlign = TextAlign.Center,
)
}
}
}

View file

@ -0,0 +1,204 @@
package com.tangem.features.tangempay.ui
import android.content.res.Configuration
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color.Companion.Transparent
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.components.SecondaryButton
import com.tangem.core.ui.components.SpacerH12
import com.tangem.core.ui.components.SpacerH24
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
import com.tangem.core.ui.components.bottomsheets.message.MessageBottomSheetV2Content
import com.tangem.core.ui.components.bottomsheets.modal.TangemModalBottomSheet
import com.tangem.core.ui.components.bottomsheets.modal.TangemModalBottomSheetTitle
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.features.tangempay.details.impl.R
import com.tangem.features.tangempay.entity.TangemPayViewPinUM
@Composable
internal fun TangemPayViewPinContent(state: TangemPayViewPinUM) {
TangemModalBottomSheet<TangemBottomSheetConfigContent.Empty>(
config = TangemBottomSheetConfig(
isShown = true,
onDismissRequest = state.onDismiss,
content = TangemBottomSheetConfigContent.Empty,
),
onBack = state.onDismiss,
containerColor = TangemTheme.colors.background.tertiary,
title = {
TangemModalBottomSheetTitle(
title = TextReference.EMPTY,
endIconRes = R.drawable.ic_close_24,
onEndClick = state.onDismiss,
)
},
) {
when (state) {
is TangemPayViewPinUM.Content -> {
PinSuccessContent(state)
}
is TangemPayViewPinUM.Error -> {
MessageBottomSheetV2Content(state.errorMessage)
}
is TangemPayViewPinUM.Loading -> {
PinLoadingContent()
}
}
}
}
@Composable
private fun PinSuccessContent(state: TangemPayViewPinUM.Content, modifier: Modifier = Modifier) {
Column(
modifier = modifier
.padding(horizontal = 16.dp)
.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Text(
text = stringResourceSafe(R.string.tangempay_card_details_view_pin_code_title),
style = TangemTheme.typography.h2,
color = TangemTheme.colors.text.primary1,
textAlign = TextAlign.Center,
)
SpacerH12()
Text(
text = stringResourceSafe(R.string.tangempay_card_details_view_pin_code_description),
style = TangemTheme.typography.body1,
color = TangemTheme.colors.text.secondary,
textAlign = TextAlign.Center,
)
SpacerH24()
Box(
modifier = Modifier
.fillMaxWidth()
.height(200.dp),
contentAlignment = Alignment.Center,
) {
PinCode(
modifier = Modifier
.align(Alignment.TopCenter),
value = state.pin,
)
SecondaryButton(
modifier = Modifier
.align(Alignment.BottomCenter)
.imePadding()
.padding(start = 16.dp, end = 16.dp, bottom = 16.dp)
.fillMaxWidth(),
text = stringResourceSafe(R.string.tangempay_change_pin_code),
onClick = state.onClickChangePin,
)
}
}
}
@Composable
private fun PinLoadingContent(modifier: Modifier = Modifier) {
Column(
modifier = modifier
.padding(horizontal = 16.dp)
.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Text(
text = stringResourceSafe(R.string.tangempay_card_details_view_pin_code_title),
style = TangemTheme.typography.h2,
color = TangemTheme.colors.text.primary1,
textAlign = TextAlign.Center,
)
SpacerH12()
Text(
text = stringResourceSafe(R.string.tangempay_card_details_view_pin_code_description),
style = TangemTheme.typography.body1,
color = TangemTheme.colors.text.secondary,
textAlign = TextAlign.Center,
)
SpacerH24()
Box(
modifier = Modifier
.fillMaxWidth()
.height(200.dp),
contentAlignment = Alignment.Center,
) {
CircularProgressIndicator(
color = TangemTheme.colors.text.disabled,
modifier = Modifier
.align(Alignment.TopCenter)
.padding(top = 16.dp)
.size(TangemTheme.dimens.size24),
)
}
}
}
@Composable
private fun PinCode(value: String, modifier: Modifier = Modifier, numbersCount: Int = 4) {
BasicTextField(
value = value,
onValueChange = {},
modifier = modifier,
textStyle = TangemTheme.typography.h1.copy(color = Transparent),
cursorBrush = SolidColor(Transparent),
decorationBox = { innerTextField ->
Box(
modifier = Modifier.fillMaxWidth(),
contentAlignment = Alignment.Center,
) {
Row(
horizontalArrangement = Arrangement.spacedBy(12.dp),
verticalAlignment = Alignment.CenterVertically,
) {
repeat(numbersCount) { index ->
val digit = value.getOrNull(index)?.toString()
PinDigitBox(
digit = digit,
backgroundColor = TangemTheme.colors.background.action,
borderColor = TangemTheme.colors.stroke.primary,
textColor = TangemTheme.colors.text.primary1,
textStyle = TangemTheme.typography.h1,
)
}
}
innerTextField()
}
},
)
}
@Preview(showBackground = true)
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun TangemPayViewPinContentPreview() {
TangemThemePreview {
TangemPayViewPinContent(
state = TangemPayViewPinUM.Content(
pin = "1234",
onClickChangePin = {},
onDismiss = {},
),
)
}
}

View file

@ -7,7 +7,7 @@ internal interface TangemPayDetailIntents {
fun onRefreshSwipe(refreshState: ShowRefreshState)
fun onClickAddFunds()
fun onClickWithdraw()
fun onClickChangePin()
fun onClickPinCode()
fun onClickFreezeCard()
fun onClickUnfreezeCard()
fun onClickTermsAndLimits()

View file

@ -66,6 +66,7 @@ internal class TangemPayUpdateInfoStateTransformer(
openDetails(
TangemPayDetailsConfig(
cardId = productInstance.cardId,
isPinSet = cardInfo.isPinSet,
cardFrozenState = cardFrozenState,
customerWalletAddress = cardInfo.customerWalletAddress,
cardNumberEnd = cardInfo.lastFourDigits,