Updated on 2026-08-14

This commit is contained in:
Tangem 2025-04-22 11:52:10 +03:00
parent cb23c1dea3
commit 538757a7ce
54 changed files with 622 additions and 488 deletions

View file

@ -167,7 +167,7 @@ internal class DefaultOnboardingVisaComponent @AssistedInject constructor(
params = OnboardingVisaOtherWalletComponent.Params(
childParams = childParams,
onDone = {
model.stackNavigation.pushNew(
model.stackNavigation.replaceAll(
OnboardingVisaRoute.PinCode(activationOrderInfo = it, pinCodeValidationError = false),
)
},
@ -183,7 +183,7 @@ internal class DefaultOnboardingVisaComponent @AssistedInject constructor(
params = OnboardingVisaPinCodeComponent.Params(
childParams = childParams,
onDone = {
model.stackNavigation.pushNew(
model.stackNavigation.replaceAll(
OnboardingVisaRoute.InProgress(from = OnboardingVisaRoute.InProgress.From.PinCode),
)
},
@ -199,7 +199,7 @@ internal class DefaultOnboardingVisaComponent @AssistedInject constructor(
params = OnboardingVisaApproveComponent.Params(
childParams = childParams,
onDone = {
model.stackNavigation.pushNew(
model.stackNavigation.replaceAll(
OnboardingVisaRoute.InProgress(from = OnboardingVisaRoute.InProgress.From.Approve),
)
},

View file

@ -20,7 +20,6 @@ import com.tangem.domain.visa.repository.VisaActivationRepository
import com.tangem.domain.visa.repository.VisaAuthRepository
import com.tangem.features.onboarding.v2.visa.impl.child.accesscode.OnboardingVisaAccessCodeComponent
import com.tangem.features.onboarding.v2.visa.impl.child.accesscode.ui.state.OnboardingVisaAccessCodeUM
import com.tangem.features.onboarding.v2.visa.impl.child.welcome.model.analytics.ONBOARDING_SOURCE
import com.tangem.features.onboarding.v2.visa.impl.child.welcome.model.analytics.OnboardingVisaAnalyticsEvent
import com.tangem.features.onboarding.v2.visa.impl.child.welcome.model.analytics.VisaAnalyticsEvent
import com.tangem.features.onboarding.v2.visa.impl.common.ActivationReadyEvent
@ -55,9 +54,12 @@ internal class OnboardingVisaAccessCodeModel @Inject constructor(
cardPublicKey = params.scanResponse.card.cardPublicKey.toHexString(),
),
)
private val activationStatus =
params.scanResponse.visaCardActivationStatus as? VisaCardActivationStatus.NotStartedActivation
?: error("Visa activation status is not set or incorrect for this step")
private val activationInput = when (val status = params.scanResponse.visaCardActivationStatus) {
is VisaCardActivationStatus.NotStartedActivation -> status.activationInput
is VisaCardActivationStatus.ActivationStarted -> status.activationInput
else -> error("Visa activation status is not set or incorrect for this step")
}
private val _uiState = MutableStateFlow(getInitialState())
@ -153,8 +155,8 @@ internal class OnboardingVisaAccessCodeModel @Inject constructor(
modelScope.launch {
val challengeToSign = runCatching {
visaAuthRepository.getCardAuthChallenge(
cardId = activationStatus.activationInput.cardId,
cardPublicKey = activationStatus.activationInput.cardPublicKey,
cardId = activationInput.cardId,
cardPublicKey = activationInput.cardPublicKey,
)
}.getOrElse {
loading(false)
@ -167,7 +169,7 @@ internal class OnboardingVisaAccessCodeModel @Inject constructor(
accessCode = accessCode,
authorizationChallenge = challengeToSign,
),
activationInput = activationStatus.activationInput,
activationInput = activationInput,
)
val resultData = when (result) {
@ -175,7 +177,7 @@ internal class OnboardingVisaAccessCodeModel @Inject constructor(
loading(false)
uiMessageSender.showErrorDialog(result.error.universalError)
analyticsEventsHandler.send(
VisaAnalyticsEvent.Errors(result.error.code.toString(), ONBOARDING_SOURCE),
VisaAnalyticsEvent.ErrorOnboarding(result.error.universalError),
)
return@launch
}
@ -190,14 +192,16 @@ internal class OnboardingVisaAccessCodeModel @Inject constructor(
return@launch
}
val targetAddress = result.data.signedActivationData.dataToSign.request.customerWalletAddress
val targetAddress =
result.data.signedActivationData.dataToSign.request.activationOrderInfo.customerWalletAddress
modelScope.launch {
onDone.emit(
ActivationReadyEvent(
customerWalletDataToSignRequest = VisaCustomerWalletDataToSignRequest(
orderId = result.data.signedActivationData.dataToSign.request.orderId,
cardWalletAddress = result.data.signedActivationData.cardWalletAddress,
orderId = result.data.signedActivationData.dataToSign.request.activationOrderInfo.orderId,
cardWalletAddress = result.data.signedActivationData.dataToSign.request.cardWalletAddress,
customerWalletAddress = targetAddress,
),
newScanResponse = params.scanResponse.copy(
card = result.data.newCardDTO,

View file

@ -16,7 +16,6 @@ import com.tangem.domain.visa.model.VisaDataForApprove
import com.tangem.domain.visa.repository.VisaActivationRepository
import com.tangem.features.onboarding.v2.visa.impl.child.approve.OnboardingVisaApproveComponent
import com.tangem.features.onboarding.v2.visa.impl.child.approve.ui.state.OnboardingVisaApproveUM
import com.tangem.features.onboarding.v2.visa.impl.child.welcome.model.analytics.ONBOARDING_SOURCE
import com.tangem.features.onboarding.v2.visa.impl.child.welcome.model.analytics.OnboardingVisaAnalyticsEvent
import com.tangem.features.onboarding.v2.visa.impl.child.welcome.model.analytics.VisaAnalyticsEvent
import com.tangem.sdk.api.TangemSdkManager
@ -88,9 +87,7 @@ internal class OnboardingVisaApproveModel @Inject constructor(
is CompletionResult.Failure -> {
loading(false)
uiMessageSender.showErrorDialog(result.error.universalError)
analyticsEventHandler.send(
VisaAnalyticsEvent.Errors(result.error.code.toString(), ONBOARDING_SOURCE),
)
analyticsEventHandler.send(VisaAnalyticsEvent.ErrorOnboarding(result.error.universalError))
return@launch
}
is CompletionResult.Success -> result.data

View file

@ -22,6 +22,7 @@ import com.tangem.domain.wallets.models.UserWallet
import com.tangem.features.onboarding.v2.visa.impl.child.inprogress.OnboardingVisaInProgressComponent.Config
import com.tangem.features.onboarding.v2.visa.impl.child.inprogress.OnboardingVisaInProgressComponent.Params
import com.tangem.features.onboarding.v2.visa.impl.child.welcome.model.analytics.OnboardingVisaAnalyticsEvent
import com.tangem.features.onboarding.v2.visa.impl.child.welcome.model.analytics.VisaAnalyticsEvent
import com.tangem.features.onboarding.v2.visa.impl.route.OnboardingVisaRoute
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.delay
@ -57,10 +58,14 @@ internal class OnboardingVisaInProgressModel @Inject constructor(
init {
analyticsEventHandler.send(OnboardingVisaAnalyticsEvent.ActivationInProgressScreen)
runShortPolling()
}
private fun runShortPolling() {
modelScope.launch {
while (true) {
val result = runCatching {
visaActivationRepository.getActivationRemoteStateLongPoll()
visaActivationRepository.getActivationRemoteState()
}.getOrNull() ?: continue
when (result) {
@ -68,6 +73,16 @@ internal class OnboardingVisaInProgressModel @Inject constructor(
VisaActivationRemoteState.BlockedForActivation,
-> {
uiMessageSender.showErrorDialog(VisaActivationError.InconsistentRemoteState)
analyticsEventHandler.send(
VisaAnalyticsEvent.ErrorOnboarding(VisaActivationError.InconsistentRemoteState),
)
return@launch
}
VisaActivationRemoteState.Failed -> {
uiMessageSender.showErrorDialog(VisaActivationError.FailedRemoteState)
analyticsEventHandler.send(
VisaAnalyticsEvent.ErrorOnboarding(VisaActivationError.FailedRemoteState),
)
return@launch
}
is VisaActivationRemoteState.CustomerWalletSignatureRequired,
@ -86,14 +101,14 @@ internal class OnboardingVisaInProgressModel @Inject constructor(
}
}
delay(timeMillis = 1000)
delay(timeMillis = 2000)
}
}
}
private suspend inline fun navigateToPinCodeIfNeeded(
remoteState: VisaActivationRemoteState.AwaitingPinCode,
returnBlock: () -> Unit,
complete: () -> Unit,
) {
when (remoteState.status) {
VisaActivationRemoteState.AwaitingPinCode.Status.WaitingForPinCode -> {
@ -105,7 +120,7 @@ internal class OnboardingVisaInProgressModel @Inject constructor(
),
),
)
returnBlock()
complete()
}
VisaActivationRemoteState.AwaitingPinCode.Status.WasError -> {
onDone.emit(
@ -116,10 +131,10 @@ internal class OnboardingVisaInProgressModel @Inject constructor(
),
),
)
returnBlock()
complete()
}
VisaActivationRemoteState.AwaitingPinCode.Status.InProgress -> {
/** waiting for new state */
/** waiting for the new state */
}
}
}

View file

@ -51,7 +51,7 @@ internal class OnboardingVisaOtherWalletModel @Inject constructor(
modelScope.launch {
while (true) {
val result = runCatching {
visaActivationRepository.getActivationRemoteStateLongPoll()
visaActivationRepository.getActivationRemoteState()
}.getOrNull()
if (result is VisaActivationRemoteState.AwaitingPinCode) {
@ -59,7 +59,7 @@ internal class OnboardingVisaOtherWalletModel @Inject constructor(
break
}
delay(timeMillis = 1000)
delay(timeMillis = 2000)
}
}
}

View file

@ -6,15 +6,12 @@ import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.core.decompose.ui.UiMessageSender
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.datasource.local.visa.VisaAuthTokenStorage
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.core.ui.utils.showErrorDialog
import com.tangem.domain.visa.SetVisaPinCodeUseCase
import com.tangem.domain.visa.model.VisaCardActivationStatus
import com.tangem.domain.visa.error.VisaAuthorizationAPIError
import com.tangem.domain.visa.model.VisaCardId
import com.tangem.domain.wallets.builder.UserWalletBuilder
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.features.onboarding.v2.impl.R
import com.tangem.features.onboarding.v2.visa.impl.child.pincode.OnboardingVisaPinCodeComponent
import com.tangem.features.onboarding.v2.visa.impl.child.pincode.ui.state.OnboardingVisaPinCodeUM
@ -25,7 +22,6 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import javax.inject.Inject
@Stable
@ -34,12 +30,9 @@ import javax.inject.Inject
internal class OnboardingVisaPinCodeModel @Inject constructor(
paramsContainer: ParamsContainer,
override val dispatchers: CoroutineDispatcherProvider,
private val userWalletBuilderFactory: UserWalletBuilder.Factory,
private val userWalletsListManager: UserWalletsListManager,
private val authTokenStorage: VisaAuthTokenStorage,
private val otpStorage: VisaAuthTokenStorage,
private val setVisaPinCodeUseCase: SetVisaPinCodeUseCase,
private val analyticsEventHandler: AnalyticsEventHandler,
private val uiMessageSender: UiMessageSender,
) : Model() {
private val params = paramsContainer.require<OnboardingVisaPinCodeComponent.Config>()
@ -102,11 +95,11 @@ internal class OnboardingVisaPinCodeModel @Inject constructor(
activationOrderId = params.activationOrderInfo.orderId,
).onLeft {
loading(false)
uiMessageSender.showErrorDialog(VisaAuthorizationAPIError)
return@launch
}
saveWallet()
onDone.emit(Unit)
loading(false)
}
}
@ -114,26 +107,4 @@ internal class OnboardingVisaPinCodeModel @Inject constructor(
private fun loading(state: Boolean) {
_uiState.update { it.copy(submitButtonLoading = state) }
}
private suspend fun saveWallet() {
val userWallet = createUserWallet(params.scanResponse)
userWalletsListManager.save(userWallet)
authTokenStorage.remove(params.scanResponse.card.cardId)
otpStorage.remove(params.scanResponse.card.cardId)
onDone.emit(Unit)
}
private suspend fun createUserWallet(scanResponse: ScanResponse): UserWallet = withContext(dispatchers.io) {
val newActivationStatus = VisaCardActivationStatus.Activated(
visaAuthTokens = authTokenStorage.get(scanResponse.card.cardId)
?: error("Impossible state. Wrong feature implementation"),
)
requireNotNull(
value = userWalletBuilderFactory.create(
scanResponse = scanResponse.copy(visaCardActivationStatus = newActivationStatus),
).build(),
lazyMessage = { "User wallet not created" },
)
}
}

View file

@ -22,10 +22,11 @@ internal object PinCodeValidation {
}
private fun validateNoRepeatedDigits(pinCode: String): Boolean {
return pinCode.toSet().size == PIN_CODE_LENGTH
return pinCode.toSet().size > 1
}
private fun validateNoConsecutiveDigits(pinCode: String): Boolean {
return pinCode.windowed(2).all { it[0] + 1 == it[1] }.not()
return pinCode.zipWithNext().any { it.second != it.first + 1 } &&
pinCode.zipWithNext().any { it.second != it.first - 1 }
}
}

View file

@ -15,7 +15,6 @@ import com.tangem.domain.visa.model.VisaCustomerWalletDataToSignRequest
import com.tangem.domain.visa.repository.VisaActivationRepository
import com.tangem.features.onboarding.v2.visa.impl.child.welcome.OnboardingVisaWelcomeComponent.Config
import com.tangem.features.onboarding.v2.visa.impl.child.welcome.OnboardingVisaWelcomeComponent.DoneEvent
import com.tangem.features.onboarding.v2.visa.impl.child.welcome.model.analytics.ONBOARDING_SOURCE
import com.tangem.features.onboarding.v2.visa.impl.child.welcome.model.analytics.OnboardingVisaAnalyticsEvent
import com.tangem.features.onboarding.v2.visa.impl.child.welcome.model.analytics.VisaAnalyticsEvent
import com.tangem.features.onboarding.v2.visa.impl.child.welcome.ui.state.OnboardingVisaWelcomeUM
@ -96,9 +95,7 @@ internal class OnboardingVisaWelcomeModel @Inject constructor(
is CompletionResult.Failure -> {
loading(false)
uiMessageSender.showErrorDialog(result.error.universalError)
analyticsEventsHandler.send(
VisaAnalyticsEvent.Errors(result.error.code.toString(), ONBOARDING_SOURCE),
)
analyticsEventsHandler.send(VisaAnalyticsEvent.ErrorOnboarding(result.error.universalError))
return@launch
}
is CompletionResult.Success -> result.data
@ -112,15 +109,17 @@ internal class OnboardingVisaWelcomeModel @Inject constructor(
return@launch
}
val targetAddress = result.data.signedActivationData.dataToSign.request.customerWalletAddress
val request = result.data.signedActivationData.dataToSign.request
val targetAddress = request.activationOrderInfo.customerWalletAddress
modelScope.launch {
onDone.emit(
DoneEvent.WelcomeBackDone(
ActivationReadyEvent(
customerWalletDataToSignRequest = VisaCustomerWalletDataToSignRequest(
orderId = result.data.signedActivationData.dataToSign.request.orderId,
cardWalletAddress = result.data.signedActivationData.cardWalletAddress,
orderId = request.activationOrderInfo.orderId,
cardWalletAddress = request.cardWalletAddress,
customerWalletAddress = targetAddress,
),
newScanResponse = params.scanResponse.copy(
card = result.data.newCardDTO,

View file

@ -1,6 +1,7 @@
package com.tangem.features.onboarding.v2.visa.impl.child.welcome.model.analytics
import com.tangem.core.analytics.models.AnalyticsEvent
import com.tangem.core.error.UniversalError
internal const val ONBOARDING_SOURCE = "Onboarding"
internal const val MAIN_SOURCE = "Main"
@ -10,14 +11,11 @@ internal sealed class VisaAnalyticsEvent(
params: Map<String, String> = mapOf(),
) : AnalyticsEvent("Visa", event, params) {
data class Errors(
val errorCode: String,
val source: String,
) : VisaAnalyticsEvent(
data class ErrorOnboarding(val error: UniversalError) : VisaAnalyticsEvent(
event = "Errors",
params = mapOf(
"Error Code" to errorCode,
"Source" to source,
"Error Code" to error.errorCode.toString(),
"Source" to ONBOARDING_SOURCE,
),
)
}

View file

@ -1,10 +1,10 @@
package com.tangem.features.onboarding.v2.visa.impl.model
import androidx.compose.runtime.Stable
import com.arkivanov.decompose.ExperimentalDecomposeApi
import com.arkivanov.decompose.router.stack.StackNavigation
import com.arkivanov.decompose.router.stack.pop
import com.arkivanov.decompose.router.stack.pushNew
import com.arkivanov.decompose.router.stack.replaceAll
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
@ -12,6 +12,7 @@ import com.tangem.domain.common.visa.VisaUtilities
import com.tangem.domain.common.visa.VisaWalletPublicKeyUtility
import com.tangem.domain.visa.model.VisaActivationRemoteState
import com.tangem.domain.visa.model.VisaCardActivationStatus
import com.tangem.domain.visa.model.VisaCardWalletDataToSignRequest
import com.tangem.domain.visa.model.VisaCustomerWalletDataToSignRequest
import com.tangem.domain.wallets.usecase.GetWalletsUseCase
import com.tangem.features.onboarding.v2.visa.api.OnboardingVisaComponent
@ -83,14 +84,13 @@ internal class OnboardingVisaModel @Inject constructor(
)
}
@OptIn(ExperimentalDecomposeApi::class)
fun navigateFromInProgress(event: OnboardingVisaInProgressComponent.Params.DoneEvent) {
when (event) {
OnboardingVisaInProgressComponent.Params.DoneEvent.Activated -> {
modelScope.launch { onDone.emit(Unit) }
}
is OnboardingVisaInProgressComponent.Params.DoneEvent.NavigateTo -> {
stackNavigation.pushNew(event.route)
stackNavigation.replaceAll(event.route)
}
}
}
@ -145,10 +145,17 @@ internal class OnboardingVisaModel @Inject constructor(
is VisaCardActivationStatus.ActivationStarted -> {
when (val remoteState = activationStatus.remoteState) {
is VisaActivationRemoteState.CardWalletSignatureRequired -> {
OnboardingVisaRoute.WelcomeBack(
activationInput = activationStatus.activationInput,
dataToSignByCardWalletRequest = remoteState.request,
)
if (activationStatus.activationInput.isAccessCodeSet) {
OnboardingVisaRoute.WelcomeBack(
activationInput = activationStatus.activationInput,
dataToSignByCardWalletRequest = VisaCardWalletDataToSignRequest(
activationOrderInfo = remoteState.activationOrderInfo,
cardWalletAddress = activationStatus.cardWalletAddress,
),
)
} else {
OnboardingVisaRoute.AccessCode
}
}
is VisaActivationRemoteState.CustomerWalletSignatureRequired -> {
remoteState.getRoute(activationStatus)
@ -167,6 +174,7 @@ internal class OnboardingVisaModel @Inject constructor(
}
VisaActivationRemoteState.Activated,
VisaActivationRemoteState.BlockedForActivation,
VisaActivationRemoteState.Failed,
-> error("Activation status is not correct for onboarding flow")
}
}
@ -182,6 +190,7 @@ internal class OnboardingVisaModel @Inject constructor(
val request = VisaCustomerWalletDataToSignRequest(
orderId = this.activationOrderInfo.orderId,
cardWalletAddress = activationStatus.cardWalletAddress,
customerWalletAddress = this.activationOrderInfo.customerWalletAddress,
)
val preparationDataForApprove = PreparationDataForApprove(
customerWalletAddress = this.activationOrderInfo.customerWalletAddress,