Updated on 2026-08-14
This commit is contained in:
parent
0594e1b680
commit
a8faaffcab
24 changed files with 561 additions and 61 deletions
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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?,
|
||||
)
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -32,5 +32,6 @@ data class CustomerInfo(
|
|||
val currencyCode: String,
|
||||
val customerWalletAddress: String,
|
||||
val depositAddress: String?,
|
||||
val isPinSet: Boolean,
|
||||
)
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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 = {},
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -66,6 +66,7 @@ internal class TangemPayUpdateInfoStateTransformer(
|
|||
openDetails(
|
||||
TangemPayDetailsConfig(
|
||||
cardId = productInstance.cardId,
|
||||
isPinSet = cardInfo.isPinSet,
|
||||
cardFrozenState = cardFrozenState,
|
||||
customerWalletAddress = cardInfo.customerWalletAddress,
|
||||
cardNumberEnd = cardInfo.lastFourDigits,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue