Updated on 2026-08-14

This commit is contained in:
Tangem 2025-04-10 13:34:13 +03:00
parent 08f86f4d52
commit de501988d0
15 changed files with 148 additions and 17 deletions

View file

@ -7,10 +7,13 @@ import com.squareup.moshi.JsonClass
data class CardActivationRemoteStateResponse(
@Json(name = "activation_status") val status: String,
@Json(name = "activation_order") val activationOrder: ActivationOrder?,
@Json(name = "stepChangeCode") val stepChangeCode: Int?,
@Json(name = "updatedAt") val updatedAt: String?,
) {
@JsonClass(generateAdapter = true)
data class ActivationOrder(
@Json(name = "id") val id: String,
@Json(name = "customer_id") val customerId: String,
@Json(name = "customer_wallet_address") val customerWalletAddress: String,
)
}

View file

@ -109,6 +109,8 @@
<string name="common_claim_rewards">報酬を受け取る</string>
<string name="common_close">閉じる</string>
<string name="common_confirm">確認</string>
<string name="common_contact_tangem_support">Tangemサポートへ問い合わせる</string>
<string name="common_contact_visa_support">Visaサポートへ問い合わせる</string>
<string name="common_continue">続ける</string>
<string name="common_copy">コピー</string>
<string name="common_copy_address">アドレスをコピー</string>
@ -515,6 +517,7 @@
<string name="nft_details_token_standard">トークン標準</string>
<string name="nft_details_traits">特徴</string>
<string name="nft_empty_search">結果がありません。別のリクエストをお試しください。</string>
<string name="nft_receive_choose_network">ネットワークを選択</string>
<string name="nft_receive_subtitle">私のウォレットへ</string>
<string name="nft_receive_title">NFTを受け取る</string>
<string name="nft_wallet_count">%1$dコレクションの%2$dNFT</string>
@ -1031,12 +1034,14 @@
<string name="visa_onboarding_in_progress_description">長くはかかりません。アカウントを設定しています。</string>
<string name="visa_onboarding_in_progress_issuer_description">長くはかかりません。アクティベーションを完了しています。</string>
<string name="visa_onboarding_in_progress_title">準備完了です!</string>
<string name="visa_onboarding_pin_not_accepted">PINの認証に失敗しました。もう一度お試しいただくか、別のコードを使用してください。</string>
<string name="visa_onboarding_pin_validation_error_message">無効な暗証番号:連続や繰り返しを避けてください</string>
<string name="visa_onboarding_wallet_connect_title">ウェブサイトに移動</string>
<string name="visa_onboarding_welcome_back_description">アカウントの設定を続けましょう。</string>
<string name="visa_onboarding_welcome_back_title">お帰りなさい!</string>
<string name="visa_onboarding_welcome_description">手順に従ってアカウントを設定してください。</string>
<string name="visa_onboarding_welcome_title">ようこそ!</string>
<string name="visa_tx_dispute_button">この取引に異議を唱える</string>
<string name="visa_unlock_notification_button">ロック解除</string>
<string name="visa_unlock_notification_subtitle">カードをスキャンしてアクセスロックを解除する</string>
<string name="visa_unlock_notification_title">ロック解除が必要</string>

View file

@ -1020,6 +1020,7 @@
<string name="user_wallet_list_rename_popup_title">Переименование кошелька</string>
<string name="user_wallet_list_unlock_all">Разблокировать все</string>
<string name="user_wallet_list_unlock_all_with">Разблокировать все с %s</string>
<string name="visa_onboarding_pin_not_accepted">ПИН не принят. Попробуйте ещё раз или введите другой код.</string>
<string name="wallet_balance_blockchain_unreachable_try_later">Блокчейн недоступен. Попробуйте позже.</string>
<string name="wallet_balance_missing_derivation">Отсканируйте карту или кольцо</string>
<string name="wallet_been_activated_message">Этот кошелек уже был активирован ранее.\nЕсли это сделали не вы, свяжитесь со службой поддержки.\nTangem никогда не продает кошелек вместе с предустановленным кодом доступа.</string>

View file

@ -528,11 +528,11 @@
<string name="nft_receive_choose_network">Choose network</string>
<string name="nft_receive_subtitle">To My wallet</string>
<string name="nft_receive_title">Receive NFT</string>
<string name="nft_traits_title">Traits</string>
<string name="nft_wallet_count">%1$d NFTs in %2$d collection</string>
<string name="nft_wallet_receive_nft">Tap here to receive first NFT</string>
<string name="nft_wallet_title">NFT collections</string>
<string name="nft_wallet_unable_to_load">Unable to load the data</string>
<string name="nft_traits_title">Traits</string>
<string name="onboarding_access_code_feature_1_description">Set up a single access code to protect all your devices.</string>
<string name="onboarding_access_code_feature_1_title">Protect</string>
<string name="onboarding_access_code_feature_2_description">Set an individual access code for each card or ring later.</string>
@ -1070,6 +1070,7 @@
<string name="visa_onboarding_pin_code_description">Set up a 4-digit code. It will be used for payments.</string>
<string name="visa_onboarding_pin_code_navigation_title">PIN code</string>
<string name="visa_onboarding_pin_code_title">Create PIN Code</string>
<string name="visa_onboarding_pin_not_accepted">PIN was not accepted. Try again or use a different code.</string>
<string name="visa_onboarding_pin_validation_error_message">Invalid PIN: avoid sequences or repeats</string>
<string name="visa_onboarding_success_screen_description">You\'re good to go!</string>
<string name="visa_onboarding_tangem_approve_description">Prepare the Tangem card and tap to approve</string>

View file

@ -1,15 +1,60 @@
package com.tangem.data.visa.converter
import com.tangem.datasource.api.visa.models.response.CardActivationRemoteStateResponse
import com.tangem.domain.visa.model.VisaActivationOrderInfo
import com.tangem.domain.visa.model.VisaActivationRemoteState
import com.tangem.utils.converter.Converter
import kotlinx.coroutines.flow.MutableStateFlow
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class VisaActivationStatusConverter @Inject constructor() :
Converter<CardActivationRemoteStateResponse, VisaActivationRemoteState> {
private val lastUpdatedAt = MutableStateFlow<String?>(null)
override fun convert(value: CardActivationRemoteStateResponse): VisaActivationRemoteState {
// handle pin code error
// either we entered pin code before with an error (WasError) or after receiving an error (InProgress)
if (
value.status == Status.AwaitingPin.stringValue &&
value.stepChangeCode != null &&
value.stepChangeCode == PIN_CODE_VALIDATION_ERROR
) {
return if (lastUpdatedAt.value != value.updatedAt) {
lastUpdatedAt.value == value.updatedAt
VisaActivationRemoteState.AwaitingPinCode(
activationOrderInfo = value.activationOrder!!.convert(),
status = VisaActivationRemoteState.AwaitingPinCode.Status.WasError,
)
} else {
VisaActivationRemoteState.AwaitingPinCode(
activationOrderInfo = value.activationOrder!!.convert(),
status = VisaActivationRemoteState.AwaitingPinCode.Status.InProgress,
)
}
}
// TODO Will be implemented in the future
return VisaActivationRemoteState.Activated
}
private fun CardActivationRemoteStateResponse.ActivationOrder.convert(): VisaActivationOrderInfo {
return VisaActivationOrderInfo(
orderId = id,
customerId = customerId,
customerWalletAddress = customerWalletAddress,
)
}
private enum class Status(val stringValue: String) {
AwaitingPin("AWAITING_PIN"),
// TODO complete statuses list when backend is ready
}
private companion object {
const val PIN_CODE_VALIDATION_ERROR = 1000
}
}

View file

@ -8,6 +8,6 @@ import kotlinx.serialization.Serializable
@JsonClass(generateAdapter = true)
data class VisaActivationOrderInfo(
@Json(name = "orderId") val orderId: String,
@Json(name = "customer_id") val customerId: String,
@Json(name = "customer_wallet_address") val customerWalletAddress: String,
@Json(name = "customerId") val customerId: String,
@Json(name = "customerWalletAddress") val customerWalletAddress: String,
)

View file

@ -20,9 +20,14 @@ sealed class VisaActivationRemoteState {
data object PaymentAccountDeploying : VisaActivationRemoteState()
@Serializable
data class WaitingPinCode(
data class AwaitingPinCode(
val activationOrderInfo: VisaActivationOrderInfo,
) : VisaActivationRemoteState()
val status: Status,
) : VisaActivationRemoteState() {
enum class Status {
WaitingForPinCode, InProgress, WasError
}
}
@Serializable
data object WaitingForActivationFinishing : VisaActivationRemoteState()
@ -43,6 +48,7 @@ class VisaActivationRemoteState_Json(
@Json(name = "type") val type: VisaActivationRemoteState_Type,
@Json(name = "requestCardWallet") val requestCardWallet: VisaCardWalletDataToSignRequest? = null,
@Json(name = "activationOrderInfo") val activationOrderInfo: VisaActivationOrderInfo? = null,
@Json(name = "awaiting_pin_code_status") val awaitingPinCodeStatus: AwaitingPinCodeStatus_Type? = null,
)
@Suppress("ClassNaming")
@ -58,6 +64,12 @@ enum class VisaActivationRemoteState_Type {
BlockedForActivation,
}
@Suppress("ClassNaming")
@JsonClass(generateAdapter = false)
enum class AwaitingPinCodeStatus_Type {
WaitingForPinCode, InProgress, WasError
}
@Suppress("ClassNaming")
class VisaActivationRemoteState_JsonAdapter {
@ -70,7 +82,17 @@ class VisaActivationRemoteState_JsonAdapter {
VisaActivationRemoteState.CustomerWalletSignatureRequired(value.activationOrderInfo!!)
VisaActivationRemoteState_Type.PaymentAccountDeploying -> VisaActivationRemoteState.PaymentAccountDeploying
VisaActivationRemoteState_Type.WaitingPinCode ->
VisaActivationRemoteState.WaitingPinCode(value.activationOrderInfo!!)
VisaActivationRemoteState.AwaitingPinCode(
activationOrderInfo = value.activationOrderInfo!!,
status = when (value.awaitingPinCodeStatus!!) {
AwaitingPinCodeStatus_Type.WaitingForPinCode ->
VisaActivationRemoteState.AwaitingPinCode.Status.WaitingForPinCode
AwaitingPinCodeStatus_Type.InProgress ->
VisaActivationRemoteState.AwaitingPinCode.Status.InProgress
AwaitingPinCodeStatus_Type.WasError ->
VisaActivationRemoteState.AwaitingPinCode.Status.WasError
},
)
VisaActivationRemoteState_Type.WaitingForActivationFinishing ->
VisaActivationRemoteState.WaitingForActivationFinishing
VisaActivationRemoteState_Type.Activated -> VisaActivationRemoteState.Activated
@ -93,10 +115,18 @@ class VisaActivationRemoteState_JsonAdapter {
)
is VisaActivationRemoteState.PaymentAccountDeploying ->
VisaActivationRemoteState_Json(VisaActivationRemoteState_Type.PaymentAccountDeploying)
is VisaActivationRemoteState.WaitingPinCode ->
is VisaActivationRemoteState.AwaitingPinCode ->
VisaActivationRemoteState_Json(
VisaActivationRemoteState_Type.WaitingPinCode,
activationOrderInfo = value.activationOrderInfo,
awaitingPinCodeStatus = when (value.status) {
VisaActivationRemoteState.AwaitingPinCode.Status.WaitingForPinCode ->
AwaitingPinCodeStatus_Type.WaitingForPinCode
VisaActivationRemoteState.AwaitingPinCode.Status.InProgress ->
AwaitingPinCodeStatus_Type.InProgress
VisaActivationRemoteState.AwaitingPinCode.Status.WasError ->
AwaitingPinCodeStatus_Type.WasError
},
)
is VisaActivationRemoteState.WaitingForActivationFinishing ->
VisaActivationRemoteState_Json(VisaActivationRemoteState_Type.WaitingForActivationFinishing)

View file

@ -167,7 +167,9 @@ internal class DefaultOnboardingVisaComponent @AssistedInject constructor(
params = OnboardingVisaOtherWalletComponent.Params(
childParams = childParams,
onDone = {
model.stackNavigation.pushNew(OnboardingVisaRoute.PinCode(activationOrderInfo = it))
model.stackNavigation.pushNew(
OnboardingVisaRoute.PinCode(activationOrderInfo = it, pinCodeValidationError = false),
)
},
),
)
@ -176,6 +178,7 @@ internal class DefaultOnboardingVisaComponent @AssistedInject constructor(
config = OnboardingVisaPinCodeComponent.Config(
scanResponse = model.currentScanResponse.value,
activationOrderInfo = route.activationOrderInfo,
wasValidationError = route.pinCodeValidationError,
),
params = OnboardingVisaPinCodeComponent.Params(
childParams = childParams,

View file

@ -92,6 +92,7 @@ internal class OnboardingVisaAccessCodeModel @Inject constructor(
_uiState.update {
it.copy(
accessCodeFirst = textFieldValue,
accessCodeSecond = TextFieldValue(),
atLeastMinCharsError = false,
)
}

View file

@ -75,9 +75,10 @@ internal class OnboardingVisaInProgressModel @Inject constructor(
VisaActivationRemoteState.WaitingForActivationFinishing,
-> {
}
is VisaActivationRemoteState.WaitingPinCode -> {
navigateToPinCode(result.activationOrderInfo)
return@launch
is VisaActivationRemoteState.AwaitingPinCode -> {
navigateToPinCodeIfNeeded(result) {
return@launch
}
}
VisaActivationRemoteState.Activated -> {
finishActivation()
@ -90,8 +91,37 @@ internal class OnboardingVisaInProgressModel @Inject constructor(
}
}
private suspend fun navigateToPinCode(activationOrderInfo: VisaActivationOrderInfo) {
onDone.emit(Params.DoneEvent.NavigateTo(OnboardingVisaRoute.PinCode(activationOrderInfo)))
private suspend inline fun navigateToPinCodeIfNeeded(
remoteState: VisaActivationRemoteState.AwaitingPinCode,
returnBlock: () -> Unit,
) {
when (remoteState.status) {
VisaActivationRemoteState.AwaitingPinCode.Status.WaitingForPinCode -> {
onDone.emit(
Params.DoneEvent.NavigateTo(
OnboardingVisaRoute.PinCode(
activationOrderInfo = remoteState.activationOrderInfo,
pinCodeValidationError = false,
),
),
)
returnBlock()
}
VisaActivationRemoteState.AwaitingPinCode.Status.WasError -> {
onDone.emit(
Params.DoneEvent.NavigateTo(
OnboardingVisaRoute.PinCode(
activationOrderInfo = remoteState.activationOrderInfo,
pinCodeValidationError = true,
),
),
)
returnBlock()
}
VisaActivationRemoteState.AwaitingPinCode.Status.InProgress -> {
/** waiting for new state */
}
}
}
private suspend fun finishActivation() {

View file

@ -54,7 +54,7 @@ internal class OnboardingVisaOtherWalletModel @Inject constructor(
visaActivationRepository.getActivationRemoteStateLongPoll()
}.getOrNull()
if (result is VisaActivationRemoteState.WaitingPinCode) {
if (result is VisaActivationRemoteState.AwaitingPinCode) {
onDone.emit(result.activationOrderInfo)
break
}

View file

@ -48,6 +48,7 @@ internal class OnboardingVisaPinCodeComponent(
data class Config(
val scanResponse: ScanResponse,
val activationOrderInfo: VisaActivationOrderInfo,
val wasValidationError: Boolean,
)
data class Params(

View file

@ -61,6 +61,11 @@ internal class OnboardingVisaPinCodeModel @Inject constructor(
submitButtonEnabled = false,
onPinCodeChange = ::onPinCodeChange,
onSubmitClick = ::onSubmitClick,
error = if (params.wasValidationError) {
resourceReference(R.string.visa_onboarding_pin_not_accepted)
} else {
null
},
)
}

View file

@ -159,8 +159,11 @@ internal class OnboardingVisaModel @Inject constructor(
VisaActivationRemoteState.WaitingForActivationFinishing -> {
OnboardingVisaRoute.InProgress(from = OnboardingVisaRoute.InProgress.From.PinCode)
}
is VisaActivationRemoteState.WaitingPinCode -> {
OnboardingVisaRoute.PinCode(activationOrderInfo = remoteState.activationOrderInfo)
is VisaActivationRemoteState.AwaitingPinCode -> {
OnboardingVisaRoute.PinCode(
activationOrderInfo = remoteState.activationOrderInfo,
pinCodeValidationError = false,
)
}
VisaActivationRemoteState.Activated,
VisaActivationRemoteState.BlockedForActivation,

View file

@ -48,5 +48,8 @@ internal sealed class OnboardingVisaRoute {
}
@Serializable
data class PinCode(val activationOrderInfo: VisaActivationOrderInfo) : OnboardingVisaRoute()
data class PinCode(
val activationOrderInfo: VisaActivationOrderInfo,
val pinCodeValidationError: Boolean,
) : OnboardingVisaRoute()
}