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

@ -18,6 +18,7 @@ data class CardDetailsResponse(
@Json(name = "card_number_end") val cardNumberEnd: String,
@Json(name = "pan") val pan: Secret,
@Json(name = "cvv") val cvv: Secret,
@Json(name = "pin") val pin: Secret?,
)
@JsonClass(generateAdapter = true)

View file

@ -97,5 +97,6 @@ data class CustomerMeResponse(
@Json(name = "card_type") val cardType: String,
@Json(name = "card_status") val cardStatus: String,
@Json(name = "card_number_end") val cardNumberEnd: String,
@Json(name = "is_pin_set") val isPinSet: Boolean?,
)
}

View file

@ -1083,6 +1083,7 @@
<string name="reset_card_to_factory_button_title">Reset the card</string>
<string name="reset_card_to_factory_condition_1">I understand that after performing this action, I will no longer have access to the current wallet</string>
<string name="reset_card_to_factory_condition_2">I realize that I can\'t use this card to recover my access code on the other cards of the current wallet</string>
<string name="reset_card_to_factory_condition_3">I understand that I will completely lose access to my Tangem Pay Card and all funds on it without the possibility of recovery</string>
<string name="reset_card_with_backup_to_factory_message">Factory Reset will completely delete the wallet from the selected card or ring. You will not be able to restore the current wallet or use the card or ring to recover the access code.</string>
<string name="reset_card_without_backup_to_factory_message">Factory Reset will completely delete the wallet from the selected card or ring and remove it from the app. You will not be able to restore the current wallet.</string>
<string name="reset_cards_dialog_complete_description">All Tangem devices have been reset.</string>
@ -1470,6 +1471,7 @@
<string name="tangempay_card_details_open_wallet_step_5">All set! Your card is ready to use.</string>
<string name="tangempay_card_details_open_wallet_title">Add card to Google Pay</string>
<string name="tangempay_card_details_open_wallet_title_apple">Add card to Apple Pay</string>
<string name="tangempay_card_details_pin_code">PIN code</string>
<string name="tangempay_card_details_receive_description">Share your address or show QR-code</string>
<string name="tangempay_card_details_receive_error_description">Technical issues detected. Please try again later or contact support.</string>
<string name="tangempay_card_details_receive_error_title">Receive unavailable now</string>
@ -1478,6 +1480,8 @@
<string name="tangempay_card_details_swap_description">Swap any asset in your portfolio for card</string>
<string name="tangempay_card_details_title">Card details</string>
<string name="tangempay_card_details_unfreeze_card">Unfreeze Card</string>
<string name="tangempay_card_details_view_pin_code_description">Come back to the app if you forget it.</string>
<string name="tangempay_card_details_view_pin_code_title">Your PIN code</string>
<string name="tangempay_card_details_withdraw">Withdraw</string>
<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>

View file

@ -144,6 +144,7 @@ internal class DefaultOnboardingRepository @Inject constructor(
currencyCode = fiatBalance.currency,
customerWalletAddress = paymentAccount.customerWalletAddress,
depositAddress = response.depositAddress,
isPinSet = response.card?.isPinSet == true,
)
} else {
null

View file

@ -82,12 +82,14 @@ internal class DefaultTangemPayCardDetailsRepository @Inject constructor(
block = {
val publicKeyBase64 = getPublicKeyBase64()
val (secretKeyBytes, sessionId) = rainCryptoUtil.generateSecretKeyAndSessionId(publicKeyBase64)
val result = requestHelper.performRequest(userWalletId = userWalletId) { authHeader ->
tangemPayApi.revealCardDetails(
authHeader = authHeader,
body = CardDetailsRequest(sessionId = sessionId),
)
}.getOrNull()?.result ?: error("Cannot reveal card details")
val result = requireNotNull(
requestHelper.performRequest(userWalletId = userWalletId) { authHeader ->
tangemPayApi.revealCardDetails(
authHeader = authHeader,
body = CardDetailsRequest(sessionId = sessionId),
)
}.getOrNull()?.result,
)
val pan = rainCryptoUtil.decryptSecret(
base64Secret = result.pan.secret,
@ -113,6 +115,38 @@ internal class DefaultTangemPayCardDetailsRepository @Inject constructor(
)
}
override suspend fun getPin(userWalletId: UserWalletId, cardId: String): Either<UniversalError, String?> {
return catch(
block = {
val publicKeyBase64 = getPublicKeyBase64()
val (secretKeyBytes, sessionId) = rainCryptoUtil.generateSecretKeyAndSessionId(publicKeyBase64)
val result = requireNotNull(
requestHelper.performRequest(userWalletId = userWalletId) { authHeader ->
tangemPayApi.revealCardDetails(
authHeader = authHeader,
body = CardDetailsRequest(sessionId = sessionId),
)
}.getOrNull()?.result,
)
val encryptedPin = result.pin
val pin = if (encryptedPin != null) {
rainCryptoUtil.decryptPin(
base64Secret = encryptedPin.secret,
base64Iv = encryptedPin.iv,
secretKeyBytes = secretKeyBytes,
).takeIf { !it.isNullOrEmpty() }
} else {
null
}
secretKeyBytes.fill(0)
pin.right()
},
catch = ::catchException,
)
}
override suspend fun setPin(userWalletId: UserWalletId, pin: String): Either<UniversalError, SetPinResult> {
return requestHelper.runWithErrorLogs(TAG) {
val publicKeyBase64 = getPublicKeyBase64()
@ -120,16 +154,18 @@ internal class DefaultTangemPayCardDetailsRepository @Inject constructor(
val encryptedData = rainCryptoUtil.encryptPin(pin = pin, secretKeyBytes = secretKeyBytes)
secretKeyBytes.fill(0)
val status = requestHelper.request(userWalletId) { authHeader ->
tangemPayApi.setPin(
authHeader = authHeader,
body = SetPinRequest(
sessionId = sessionId,
pin = encryptedData.encryptedBase64,
iv = encryptedData.ivBase64,
),
)
}.result?.result ?: error("Cannot set pin code")
val status = requireNotNull(
requestHelper.request(userWalletId) { authHeader ->
tangemPayApi.setPin(
authHeader = authHeader,
body = SetPinRequest(
sessionId = sessionId,
pin = encryptedData.encryptedBase64,
iv = encryptedData.ivBase64,
),
)
}.result?.result,
)
when (status) {
SetPinResult.SUCCESS.name -> SetPinResult.SUCCESS
SetPinResult.PIN_TOO_WEAK.name -> SetPinResult.PIN_TOO_WEAK

View file

@ -37,6 +37,15 @@ internal class RainCryptoUtil @Inject constructor(
secretKeyBytes to sessionId
}
suspend fun decryptPin(base64Secret: String, base64Iv: String, secretKeyBytes: ByteArray): String? {
val pinBlock = decryptSecret(
base64Secret = base64Secret,
base64Iv = base64Iv,
secretKeyBytes = secretKeyBytes,
)
return extractPinFromPinBlock(pinBlock)
}
suspend fun encryptPin(pin: String, secretKeyBytes: ByteArray): EncryptedData = withContext(dispatchers.default) {
val bytes = pinBlockByteArray(pin)
try {
@ -113,6 +122,17 @@ internal class RainCryptoUtil @Inject constructor(
return hex.toByteArray(StandardCharsets.UTF_8)
}
private suspend fun extractPinFromPinBlock(pinBlock: String): String? = withContext(dispatchers.default) {
val pinLength = pinBlock[1].digitToIntOrNull()
require(pinLength == PIN_LENGTH) { "Unexpected PIN length: ${pinBlock[1]}" }
val pinStartIndex = 2
val pinEndIndex = pinStartIndex + pinLength
val pin = pinBlock.substring(pinStartIndex, pinEndIndex)
pin.takeIf { value -> value.all { it.isDigit() } && value.length == PIN_LENGTH }
}
private fun ByteArray.clear() {
for (i in indices) this[i] = 0
}

View file

@ -6,6 +6,7 @@ import kotlinx.serialization.Serializable
@Serializable
data class TangemPayDetailsConfig(
val cardId: String,
val isPinSet: Boolean,
val cardFrozenState: TangemPayCardFrozenState,
val customerWalletAddress: String,
val cardNumberEnd: String,

View file

@ -32,5 +32,6 @@ data class CustomerInfo(
val currencyCode: String,
val customerWalletAddress: String,
val depositAddress: String?,
val isPinSet: Boolean,
)
}

View file

@ -15,6 +15,8 @@ interface TangemPayCardDetailsRepository {
suspend fun revealCardDetails(userWalletId: UserWalletId): Either<UniversalError, TangemPayCardDetails>
suspend fun getPin(userWalletId: UserWalletId, cardId: String): Either<UniversalError, String?>
suspend fun setPin(userWalletId: UserWalletId, pin: String): Either<UniversalError, SetPinResult>
suspend fun isAddToWalletDone(userWalletId: UserWalletId): Either<UniversalError, Boolean>

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,