From a8faaffcab816cce7bc63c7d4bbde5fff100bd7b Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 19 Dec 2025 18:28:10 +0400 Subject: [PATCH] Updated on 2026-08-14 --- .../models/response/CardDetailsResponse.kt | 1 + .../pay/models/response/CustomerMeResponse.kt | 1 + core/res/src/main/res/values/strings.xml | 4 + .../repository/DefaultOnboardingRepository.kt | 1 + .../DefaultTangemPayCardDetailsRepository.kt | 68 ++++-- .../tangem/data/pay/util/RainCryptoUtil.kt | 20 ++ .../domain/pay/TangemPayDetailsConfig.kt | 1 + .../tangem/domain/pay/model/CustomerInfo.kt | 1 + .../TangemPayCardDetailsRepository.kt | 2 + .../components/TangemPayDetailsComponent.kt | 8 + .../components/TangemPayViewPinComponent.kt | 44 ++++ .../tangempay/di/TangemPayModelModule.kt | 6 + .../entity/TangemPayDetailsNavigation.kt | 6 + .../entity/TangemPayDetailsStateFactory.kt | 4 +- .../tangempay/entity/TangemPayViewPinUM.kt | 23 ++ .../tangempay/model/TangemPayDetailsModel.kt | 25 ++- .../tangempay/model/TangemPayViewPinModel.kt | 59 +++++ .../TangemPayViewPinErrorStateTransformer.kt | 35 +++ ...TangemPayViewPinSuccessStateTransformer.kt | 18 ++ .../features/tangempay/ui/PinDigitBox.kt | 49 +++++ .../tangempay/ui/TangemPayChangePinScreen.kt | 39 ---- .../tangempay/ui/TangemPayViewPinContent.kt | 204 ++++++++++++++++++ .../tangempay/utils/TangemPayDetailIntents.kt | 2 +- .../TangemPayUpdateInfoStateTransformer.kt | 1 + 24 files changed, 561 insertions(+), 61 deletions(-) create mode 100644 features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/components/TangemPayViewPinComponent.kt create mode 100644 features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/entity/TangemPayViewPinUM.kt create mode 100644 features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayViewPinModel.kt create mode 100644 features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/transformers/TangemPayViewPinErrorStateTransformer.kt create mode 100644 features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/transformers/TangemPayViewPinSuccessStateTransformer.kt create mode 100644 features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/ui/PinDigitBox.kt create mode 100644 features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayViewPinContent.kt diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/CardDetailsResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/CardDetailsResponse.kt index 978baa11f9..007bcf7428 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/CardDetailsResponse.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/CardDetailsResponse.kt @@ -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) diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/CustomerMeResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/CustomerMeResponse.kt index baf6d5d265..cc0edd82a4 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/CustomerMeResponse.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/CustomerMeResponse.kt @@ -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?, ) } \ No newline at end of file diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index 07f6bd26c8..7e87d0d441 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -1083,6 +1083,7 @@ Reset the card I understand that after performing this action, I will no longer have access to the current wallet I realize that I can\'t use this card to recover my access code on the other cards of the current wallet + I understand that I will completely lose access to my Tangem Pay Card and all funds on it without the possibility of recovery 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. 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. All Tangem devices have been reset. @@ -1470,6 +1471,7 @@ All set! Your card is ready to use. Add card to Google Pay Add card to Apple Pay + PIN code Share your address or show QR-code Technical issues detected. Please try again later or contact support. Receive unavailable now @@ -1478,6 +1480,8 @@ Swap any asset in your portfolio for card Card details Unfreeze Card + Come back to the app if you forget it. + Your PIN code Withdraw Withdraw unavailable now You can\'t initiate swap or new withdrawal till the current one is finished diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultOnboardingRepository.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultOnboardingRepository.kt index 348ae72b3b..e11aa1c09b 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultOnboardingRepository.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultOnboardingRepository.kt @@ -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 diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultTangemPayCardDetailsRepository.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultTangemPayCardDetailsRepository.kt index 6bb55b562c..a05f92ee7a 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultTangemPayCardDetailsRepository.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultTangemPayCardDetailsRepository.kt @@ -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 { + 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 { 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 diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/util/RainCryptoUtil.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/util/RainCryptoUtil.kt index 4fa2681263..ce23bc2504 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/pay/util/RainCryptoUtil.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/util/RainCryptoUtil.kt @@ -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 } diff --git a/domain/visa/models/src/main/kotlin/com/tangem/domain/pay/TangemPayDetailsConfig.kt b/domain/visa/models/src/main/kotlin/com/tangem/domain/pay/TangemPayDetailsConfig.kt index da3de9f475..88f7b5556c 100644 --- a/domain/visa/models/src/main/kotlin/com/tangem/domain/pay/TangemPayDetailsConfig.kt +++ b/domain/visa/models/src/main/kotlin/com/tangem/domain/pay/TangemPayDetailsConfig.kt @@ -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, diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/pay/model/CustomerInfo.kt b/domain/visa/src/main/kotlin/com/tangem/domain/pay/model/CustomerInfo.kt index d747e41066..234366b079 100644 --- a/domain/visa/src/main/kotlin/com/tangem/domain/pay/model/CustomerInfo.kt +++ b/domain/visa/src/main/kotlin/com/tangem/domain/pay/model/CustomerInfo.kt @@ -32,5 +32,6 @@ data class CustomerInfo( val currencyCode: String, val customerWalletAddress: String, val depositAddress: String?, + val isPinSet: Boolean, ) } \ No newline at end of file diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/pay/repository/TangemPayCardDetailsRepository.kt b/domain/visa/src/main/kotlin/com/tangem/domain/pay/repository/TangemPayCardDetailsRepository.kt index 73a4b8e035..088b34d193 100644 --- a/domain/visa/src/main/kotlin/com/tangem/domain/pay/repository/TangemPayCardDetailsRepository.kt +++ b/domain/visa/src/main/kotlin/com/tangem/domain/pay/repository/TangemPayCardDetailsRepository.kt @@ -15,6 +15,8 @@ interface TangemPayCardDetailsRepository { suspend fun revealCardDetails(userWalletId: UserWalletId): Either + suspend fun getPin(userWalletId: UserWalletId, cardId: String): Either + suspend fun setPin(userWalletId: UserWalletId, pin: String): Either suspend fun isAddToWalletDone(userWalletId: UserWalletId): Either diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/components/TangemPayDetailsComponent.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/components/TangemPayDetailsComponent.kt index 44a23b3ed8..fb099938ee 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/components/TangemPayDetailsComponent.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/components/TangemPayDetailsComponent.kt @@ -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, + ), + ) } } } \ No newline at end of file diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/components/TangemPayViewPinComponent.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/components/TangemPayViewPinComponent.kt new file mode 100644 index 0000000000..f12eb316e9 --- /dev/null +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/components/TangemPayViewPinComponent.kt @@ -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() +} \ No newline at end of file diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/di/TangemPayModelModule.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/di/TangemPayModelModule.kt index 2d4297613c..2150e54279 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/di/TangemPayModelModule.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/di/TangemPayModelModule.kt @@ -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 } \ No newline at end of file diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/entity/TangemPayDetailsNavigation.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/entity/TangemPayDetailsNavigation.kt index 86009f09f0..5f10a9ed77 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/entity/TangemPayDetailsNavigation.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/entity/TangemPayDetailsNavigation.kt @@ -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() } \ No newline at end of file diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/entity/TangemPayDetailsStateFactory.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/entity/TangemPayDetailsStateFactory.kt index 9853165dad..55b429059a 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/entity/TangemPayDetailsStateFactory.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/entity/TangemPayDetailsStateFactory.kt @@ -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( diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/entity/TangemPayViewPinUM.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/entity/TangemPayViewPinUM.kt new file mode 100644 index 0000000000..8ee03d3db4 --- /dev/null +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/entity/TangemPayViewPinUM.kt @@ -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() +} \ No newline at end of file diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayDetailsModel.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayDetailsModel.kt index df186cb3f8..2c2b69be13 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayDetailsModel.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayDetailsModel.kt @@ -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( diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayViewPinModel.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayViewPinModel.kt new file mode 100644 index 0000000000..14631dc022 --- /dev/null +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayViewPinModel.kt @@ -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() + + val uiState: StateFlow + 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() + } +} \ No newline at end of file diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/transformers/TangemPayViewPinErrorStateTransformer.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/transformers/TangemPayViewPinErrorStateTransformer.kt new file mode 100644 index 0000000000..eacc9656fd --- /dev/null +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/transformers/TangemPayViewPinErrorStateTransformer.kt @@ -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 { + + 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, + ) + } +} \ No newline at end of file diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/transformers/TangemPayViewPinSuccessStateTransformer.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/transformers/TangemPayViewPinSuccessStateTransformer.kt new file mode 100644 index 0000000000..9258baaf5a --- /dev/null +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/transformers/TangemPayViewPinSuccessStateTransformer.kt @@ -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 { + + override fun transform(prevState: TangemPayViewPinUM): TangemPayViewPinUM { + return TangemPayViewPinUM.Content( + pin = pin, + onClickChangePin = onClickChangePin, + onDismiss = prevState.onDismiss, + ) + } +} \ No newline at end of file diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/ui/PinDigitBox.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/ui/PinDigitBox.kt new file mode 100644 index 0000000000..ece1240de8 --- /dev/null +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/ui/PinDigitBox.kt @@ -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, + ) + } + } +} \ No newline at end of file diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayChangePinScreen.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayChangePinScreen.kt index f052057177..c1eaca6abc 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayChangePinScreen.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayChangePinScreen.kt @@ -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, - ) - } - } } \ No newline at end of file diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayViewPinContent.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayViewPinContent.kt new file mode 100644 index 0000000000..bcd40588a5 --- /dev/null +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayViewPinContent.kt @@ -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( + 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 = {}, + ), + ) + } +} \ No newline at end of file diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/utils/TangemPayDetailIntents.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/utils/TangemPayDetailIntents.kt index 912bfefbd5..9e96a75104 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/utils/TangemPayDetailIntents.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/utils/TangemPayDetailIntents.kt @@ -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() diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TangemPayUpdateInfoStateTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TangemPayUpdateInfoStateTransformer.kt index f9127d04ef..a47a0aec3a 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TangemPayUpdateInfoStateTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TangemPayUpdateInfoStateTransformer.kt @@ -66,6 +66,7 @@ internal class TangemPayUpdateInfoStateTransformer( openDetails( TangemPayDetailsConfig( cardId = productInstance.cardId, + isPinSet = cardInfo.isPinSet, cardFrozenState = cardFrozenState, customerWalletAddress = cardInfo.customerWalletAddress, cardNumberEnd = cardInfo.lastFourDigits,