Updated on 2026-08-14

This commit is contained in:
Tangem 2025-01-28 13:28:17 +03:00
parent 3406a353b0
commit 112d1e8a90
11 changed files with 164 additions and 87 deletions

View file

@ -26,7 +26,6 @@ import com.tangem.domain.models.scan.CardDTO
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.visa.model.VisaActivationInput
import com.tangem.domain.visa.model.VisaAuthChallenge
import com.tangem.domain.visa.model.VisaCardActivationResponse
import com.tangem.domain.visa.model.VisaDataForApprove
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.features.onboarding.v2.OnboardingV2FeatureToggles
@ -41,6 +40,7 @@ import com.tangem.operations.usersetttings.SetUserCodeRecoveryAllowedTask
import com.tangem.operations.wallet.CreateWalletResponse
import com.tangem.sdk.api.CreateProductWalletTaskResponse
import com.tangem.sdk.api.TangemSdkManager
import com.tangem.sdk.api.visa.VisaCardActivationResponse
import com.tangem.tap.derivationsFinder
import com.tangem.tap.domain.tasks.product.CreateProductWalletTask
import com.tangem.tap.domain.tasks.product.ResetBackupCardTask

View file

@ -19,7 +19,6 @@ import com.tangem.domain.models.scan.CardDTO
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.visa.model.VisaActivationInput
import com.tangem.domain.visa.model.VisaAuthChallenge
import com.tangem.domain.visa.model.VisaCardActivationResponse
import com.tangem.domain.visa.model.VisaDataForApprove
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.operations.derivation.DerivationTaskResponse
@ -28,6 +27,7 @@ import com.tangem.operations.sign.SignHashResponse
import com.tangem.operations.wallet.CreateWalletResponse
import com.tangem.sdk.api.CreateProductWalletTaskResponse
import com.tangem.sdk.api.TangemSdkManager
import com.tangem.sdk.api.visa.VisaCardActivationResponse
import com.tangem.tap.domain.sdk.mocks.MockProvider
@Suppress("TooManyFunctions")

View file

@ -14,6 +14,7 @@ import com.tangem.datasource.local.visa.VisaAuthTokenStorage
import com.tangem.datasource.local.visa.VisaOTPStorage
import com.tangem.datasource.local.visa.hasSavedOTP
import com.tangem.domain.common.visa.VisaUtilities
import com.tangem.domain.models.scan.CardDTO
import com.tangem.domain.visa.model.*
import com.tangem.domain.visa.repository.VisaActivationRepository
import com.tangem.domain.visa.repository.VisaAuthRepository
@ -23,6 +24,7 @@ import com.tangem.operations.pins.SetUserCodeCommand
import com.tangem.operations.sign.SignHashCommand
import com.tangem.operations.sign.SignHashResponse
import com.tangem.operations.wallet.CreateWalletTask
import com.tangem.sdk.api.visa.VisaCardActivationResponse
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
@ -268,15 +270,19 @@ class VisaCardActivationTask @AssistedInject constructor(
TangemSdkError.Underlying(VisaActivationError.MissingRootOTP.message),
)
val activationResponse = VisaCardActivationResponse(
signedActivationOrder = signedOrder,
rootOTP = VisaRootOTP(otp.toHexString()),
)
return setupAccessCode().map {
val card =
session.environment.card ?: return CompletionResult.Failure(TangemSdkError.MissingPreflightRead())
return setupAccessCode(session).map { activationResponse }
VisaCardActivationResponse(
signedActivationOrder = signedOrder,
rootOTP = VisaRootOTP(otp.toHexString()),
newCardDTO = CardDTO(card),
)
}
}
private suspend fun SessionContext.setupAccessCode(session: CardSession): CompletionResult<Unit> {
private suspend fun SessionContext.setupAccessCode(): CompletionResult<Unit> {
val card = session.environment.card ?: return CompletionResult.Failure(TangemSdkError.MissingPreflightRead())
if (card.isAccessCodeSet) {

View file

@ -1,6 +0,0 @@
package com.tangem.domain.visa.model
data class VisaCardActivationResponse(
val signedActivationOrder: SignedActivationOrder,
val rootOTP: VisaRootOTP,
)

View file

@ -29,7 +29,6 @@ import com.tangem.features.onboarding.v2.visa.impl.model.OnboardingVisaModel
import com.tangem.features.onboarding.v2.visa.impl.route.ONBOARDING_VISA_STEPS_COUNT
import com.tangem.features.onboarding.v2.visa.impl.route.OnboardingVisaRoute
import com.tangem.features.onboarding.v2.visa.impl.route.screenTitle
import com.tangem.features.onboarding.v2.visa.impl.route.stepNum
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
@ -44,10 +43,6 @@ internal class DefaultOnboardingVisaComponent @AssistedInject constructor(
private val model: OnboardingVisaModel = getOrCreateModel(params)
private val innerNavigationState = instanceKeeper.getOrCreateSimple(key = "innerNavigationState") {
MutableStateFlow(OnboardingVisaInnerNavigationState(stackSize = model.initialStepNum))
}
private val currentChildBackEventHandle = instanceKeeper.getOrCreateSimple(key = "currentChildBackEventHandle") {
MutableSharedFlow<Unit>()
}
@ -57,12 +52,10 @@ internal class DefaultOnboardingVisaComponent @AssistedInject constructor(
parentBackEvent = currentChildBackEventHandle,
)
private val stackNavigation = StackNavigation<OnboardingVisaRoute>()
private val childStack: Value<ChildStack<OnboardingVisaRoute, ComposableContentComponent>> =
childStack(
key = "innerStack",
source = stackNavigation,
source = model.stackNavigation,
serializer = null,
initialConfiguration = model.initialRoute,
handleBackButton = true,
@ -75,7 +68,7 @@ internal class DefaultOnboardingVisaComponent @AssistedInject constructor(
)
override val innerNavigation: InnerNavigation = object : InnerNavigation {
override val state: StateFlow<InnerNavigationState> = innerNavigationState
override val state: StateFlow<InnerNavigationState> = model.innerNavigationState
override fun pop(onComplete: (Boolean) -> Unit) {
if (childStack.value.active.configuration is OnboardingVisaRoute.AccessCode) {
@ -91,7 +84,7 @@ internal class DefaultOnboardingVisaComponent @AssistedInject constructor(
childStack.observe(lifecycle) { stack ->
val currentRoute = stack.active.configuration
params.titleProvider.changeTitle(currentRoute.screenTitle())
innerNavigationState.update { it.copy(stackSize = currentRoute.stepNum()) }
model.updateStepForNewRoute(currentRoute)
}
}
@ -106,74 +99,51 @@ internal class DefaultOnboardingVisaComponent @AssistedInject constructor(
params = OnboardingVisaWelcomeComponent.Params(
isWelcomeBack = route.isWelcomeBack,
childParams = childParams,
onDone = { stackNavigation.push(OnboardingVisaRoute.AccessCode) },
onDone = { model.navigateFromWelcome(route) },
),
)
OnboardingVisaRoute.AccessCode -> OnboardingVisaAccessCodeComponent(
appComponentContext = factoryContext,
params = OnboardingVisaAccessCodeComponent.Params(
childParams = childParams,
onDone = {
stackNavigation.push(
if (it.walletFound) {
OnboardingVisaRoute.TangemWalletApproveOption(
visaDataForApprove = it.visaDataForApprove,
allowNavigateBack = false,
)
} else {
OnboardingVisaRoute.ChooseWallet(it.visaDataForApprove)
},
)
},
onDone = { model.navigateFromAccessCode(it) },
),
config = OnboardingVisaAccessCodeComponent.Config(
scanResponse = params.scanResponse,
scanResponse = model.currentScanResponse.value,
),
)
is OnboardingVisaRoute.ChooseWallet -> OnboardingVisaChooseWalletComponent(
appComponentContext = factoryContext,
params = OnboardingVisaChooseWalletComponent.Params(
childParams = childParams,
onEvent = { event ->
stackNavigation.push(
when (event) {
OnboardingVisaChooseWalletComponent.Params.Event.TangemWallet ->
OnboardingVisaRoute.TangemWalletApproveOption(
visaDataForApprove = route.visaDataForApprove,
allowNavigateBack = true,
)
OnboardingVisaChooseWalletComponent.Params.Event.OtherWallet ->
OnboardingVisaRoute.OtherWalletApproveOption(route.visaDataForApprove)
},
)
},
onEvent = { event -> model.navigateFromChooseWallet(route, event) },
),
)
OnboardingVisaRoute.InProgress -> OnboardingVisaInProgressComponent(
appComponentContext = factoryContext,
config = OnboardingVisaInProgressComponent.Config(
scanResponse = params.scanResponse,
scanResponse = model.currentScanResponse.value,
),
params = OnboardingVisaInProgressComponent.Params(
childParams = childParams,
onDone = { stackNavigation.push(OnboardingVisaRoute.PinCode) },
onDone = { model.stackNavigation.push(OnboardingVisaRoute.PinCode) },
),
)
is OnboardingVisaRoute.OtherWalletApproveOption -> OnboardingVisaOtherWalletComponent(
appComponentContext = factoryContext,
config = OnboardingVisaOtherWalletComponent.Config(
scanResponse = params.scanResponse,
scanResponse = model.currentScanResponse.value,
visaDataForApprove = route.visaDataForApprove,
),
params = OnboardingVisaOtherWalletComponent.Params(
childParams = childParams,
onDone = { stackNavigation.push(OnboardingVisaRoute.PinCode) },
onDone = { model.stackNavigation.push(OnboardingVisaRoute.PinCode) },
),
)
OnboardingVisaRoute.PinCode -> OnboardingVisaPinCodeComponent(
appComponentContext = factoryContext,
config = OnboardingVisaPinCodeComponent.Config(
scanResponse = params.scanResponse,
scanResponse = model.currentScanResponse.value,
),
params = OnboardingVisaPinCodeComponent.Params(
childParams = childParams,
@ -187,36 +157,16 @@ internal class DefaultOnboardingVisaComponent @AssistedInject constructor(
),
params = OnboardingVisaApproveComponent.Params(
childParams = childParams,
onDone = { stackNavigation.push(OnboardingVisaRoute.InProgress) },
onDone = { model.stackNavigation.push(OnboardingVisaRoute.InProgress) },
),
)
}
}
private fun onChildBack() {
if (childStack.value.backStack.size == 1) {
// TODO show dialog
return
}
when (val route = childStack.value.active.configuration) {
OnboardingVisaRoute.AccessCode,
is OnboardingVisaRoute.OtherWalletApproveOption,
-> stackNavigation.pop()
is OnboardingVisaRoute.TangemWalletApproveOption -> {
if (route.allowNavigateBack) {
stackNavigation.pop()
}
}
is OnboardingVisaRoute.Welcome,
is OnboardingVisaRoute.ChooseWallet,
OnboardingVisaRoute.InProgress,
OnboardingVisaRoute.PinCode,
-> {
}
}
}
private fun onChildBack() = model.onChildBack(
currentRoute = childStack.value.active.configuration,
lastRoute = childStack.value.backStack.size == 1,
)
@Composable
override fun Content(modifier: Modifier) {

View file

@ -60,5 +60,6 @@ internal class OnboardingVisaAccessCodeComponent(
data class DoneEvent(
val visaDataForApprove: VisaDataForApprove,
val walletFound: Boolean,
val newScanResponse: ScanResponse,
)
}

View file

@ -14,7 +14,10 @@ import com.tangem.features.onboarding.v2.visa.impl.child.accesscode.OnboardingVi
import com.tangem.features.onboarding.v2.visa.impl.child.accesscode.ui.state.OnboardingVisaAccessCodeUM
import com.tangem.sdk.api.TangemSdkManager
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import javax.inject.Inject
@ -146,6 +149,9 @@ internal class OnboardingVisaAccessCodeModel @Inject constructor(
approveHash = "48b55c482123a10ad9022f9f4c5dd95c",
),
walletFound = false, // TODO
newScanResponse = params.scanResponse.copy(
card = result.data.newCardDTO,
),
),
)
}

View file

@ -16,7 +16,6 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
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.platform.LocalDensity
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
@ -160,7 +159,7 @@ private fun PinCode(
Box(
modifier = Modifier
.background(
color = Color(color = 0xFFF5F5F5),
color = TangemTheme.colors.field.primary,
shape = TangemTheme.shapes.roundedCornersXMedium,
)
.padding(

View file

@ -1,13 +1,23 @@
package com.tangem.features.onboarding.v2.visa.impl.model
import androidx.compose.runtime.Stable
import com.arkivanov.decompose.router.stack.StackNavigation
import com.arkivanov.decompose.router.stack.pop
import com.arkivanov.decompose.router.stack.push
import com.tangem.core.decompose.di.ComponentScoped
import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.domain.visa.model.VisaActivationRemoteState
import com.tangem.domain.visa.model.VisaCardActivationStatus
import com.tangem.features.onboarding.v2.visa.api.OnboardingVisaComponent
import com.tangem.features.onboarding.v2.visa.impl.OnboardingVisaInnerNavigationState
import com.tangem.features.onboarding.v2.visa.impl.child.accesscode.OnboardingVisaAccessCodeComponent
import com.tangem.features.onboarding.v2.visa.impl.child.choosewallet.OnboardingVisaChooseWalletComponent
import com.tangem.features.onboarding.v2.visa.impl.route.OnboardingVisaRoute
import com.tangem.features.onboarding.v2.visa.impl.route.stepNum
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import javax.inject.Inject
@Stable
@ -20,7 +30,107 @@ internal class OnboardingVisaModel @Inject constructor(
@Suppress("UnusedPrivateMember")
private val params = paramsContainer.require<OnboardingVisaComponent.Params>()
val initialRoute = OnboardingVisaRoute.Welcome(isWelcomeBack = false)
private val _currentScanResponse = MutableStateFlow(params.scanResponse)
val initialStepNum = initialRoute.stepNum()
val initialRoute = initializeRoute()
val stackNavigation = StackNavigation<OnboardingVisaRoute>()
private val _innerNavigationState =
MutableStateFlow(OnboardingVisaInnerNavigationState(stackSize = initialRoute.stepNum()))
val currentScanResponse = _currentScanResponse.asStateFlow()
val innerNavigationState = _innerNavigationState.asStateFlow()
fun updateStepForNewRoute(route: OnboardingVisaRoute) {
_innerNavigationState.value = innerNavigationState.value.copy(
stackSize = route.stepNum(),
)
}
fun navigateFromWelcome(route: OnboardingVisaRoute.Welcome) {
if (route.isWelcomeBack) {
val scanResponse = _currentScanResponse.value
val activationStatus = scanResponse.visaCardActivationStatus as? VisaCardActivationStatus
.ActivationStarted ?: error("Activation status is not correct for welcome back route")
when (activationStatus.remoteState) {
VisaActivationRemoteState.CardWalletSignatureRequired,
VisaActivationRemoteState.CustomerWalletSignatureRequired,
-> OnboardingVisaRoute.AccessCode
VisaActivationRemoteState.WaitingPinCode -> OnboardingVisaRoute.PinCode
VisaActivationRemoteState.WaitingForActivationFinishing -> OnboardingVisaRoute.InProgress
VisaActivationRemoteState.PaymentAccountDeploying -> OnboardingVisaRoute.InProgress
else -> error("Remote state is not correct for welcome back route")
}
} else {
stackNavigation.push(OnboardingVisaRoute.AccessCode)
}
}
fun navigateFromAccessCode(result: OnboardingVisaAccessCodeComponent.DoneEvent) {
_currentScanResponse.value = result.newScanResponse
stackNavigation.push(
if (result.walletFound) {
OnboardingVisaRoute.TangemWalletApproveOption(
visaDataForApprove = result.visaDataForApprove,
allowNavigateBack = false,
)
} else {
OnboardingVisaRoute.ChooseWallet(result.visaDataForApprove)
},
)
}
fun navigateFromChooseWallet(
route: OnboardingVisaRoute.ChooseWallet,
event: OnboardingVisaChooseWalletComponent.Params.Event,
) {
stackNavigation.push(
when (event) {
OnboardingVisaChooseWalletComponent.Params.Event.TangemWallet ->
OnboardingVisaRoute.TangemWalletApproveOption(
visaDataForApprove = route.visaDataForApprove,
allowNavigateBack = true,
)
OnboardingVisaChooseWalletComponent.Params.Event.OtherWallet ->
OnboardingVisaRoute.OtherWalletApproveOption(route.visaDataForApprove)
},
)
}
fun onChildBack(currentRoute: OnboardingVisaRoute, lastRoute: Boolean) {
if (lastRoute) {
// TODO show dialog
return
}
when (currentRoute) {
OnboardingVisaRoute.AccessCode,
is OnboardingVisaRoute.OtherWalletApproveOption,
-> stackNavigation.pop()
is OnboardingVisaRoute.TangemWalletApproveOption -> {
if (currentRoute.allowNavigateBack) {
stackNavigation.pop()
}
}
is OnboardingVisaRoute.Welcome,
is OnboardingVisaRoute.ChooseWallet,
OnboardingVisaRoute.InProgress,
OnboardingVisaRoute.PinCode,
-> {
}
}
}
private fun initializeRoute(): OnboardingVisaRoute {
val activationStatus = params.scanResponse.visaCardActivationStatus
return when (activationStatus) {
is VisaCardActivationStatus.ActivationStarted -> OnboardingVisaRoute.Welcome(isWelcomeBack = true)
is VisaCardActivationStatus.NotStartedActivation -> OnboardingVisaRoute.Welcome(isWelcomeBack = false)
else -> error("Visa activation status is not correct for onboarding flow")
}
}
}

View file

@ -17,13 +17,13 @@ import com.tangem.domain.models.scan.CardDTO
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.visa.model.VisaActivationInput
import com.tangem.domain.visa.model.VisaAuthChallenge
import com.tangem.domain.visa.model.VisaCardActivationResponse
import com.tangem.domain.visa.model.VisaDataForApprove
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.operations.derivation.DerivationTaskResponse
import com.tangem.operations.preflightread.PreflightReadFilter
import com.tangem.operations.sign.SignHashResponse
import com.tangem.operations.wallet.CreateWalletResponse
import com.tangem.sdk.api.visa.VisaCardActivationResponse
@Suppress("TooManyFunctions")
interface TangemSdkManager {

View file

@ -0,0 +1,11 @@
package com.tangem.sdk.api.visa
import com.tangem.domain.models.scan.CardDTO
import com.tangem.domain.visa.model.SignedActivationOrder
import com.tangem.domain.visa.model.VisaRootOTP
data class VisaCardActivationResponse(
val signedActivationOrder: SignedActivationOrder,
val rootOTP: VisaRootOTP,
val newCardDTO: CardDTO,
)