Updated on 2026-08-14

This commit is contained in:
Tangem 2025-02-11 15:18:31 +03:00
parent 46a28ab22c
commit c83081cc11
28 changed files with 411 additions and 153 deletions

View file

@ -33,7 +33,6 @@ internal class VisaCardScanHandler @Inject constructor(
private class SessionContext(
val visaActivationRepository: VisaActivationRepository,
val cardId: String,
val session: CardSession,
)
@ -54,7 +53,6 @@ internal class VisaCardScanHandler @Inject constructor(
val context = SessionContext(
visaActivationRepository = visaActivationRepository,
cardId = card.cardId,
session = session,
)
@ -140,7 +138,9 @@ internal class VisaCardScanHandler @Inject constructor(
Timber.i(
"Failed to get Access token for Wallet public key authoziation. Authorizing using Card Pub key",
)
return handleCardAuthorization()
return handleCardAuthorization(
cardWalletAddress = walletAddress.value,
)
}
val signChallengeResult = signChallengeWithWallet(
@ -153,6 +153,7 @@ internal class VisaCardScanHandler @Inject constructor(
is CompletionResult.Success -> {
Timber.i("Challenge signed with Wallet public key")
handleWalletAuthorizationTokens(
cardWalletAddress = walletAddress.value,
signedChallenge = challengeResponse
.toSignedChallenge(signChallengeResult.data.signature.toHexString()),
)
@ -165,6 +166,7 @@ internal class VisaCardScanHandler @Inject constructor(
}
private suspend fun SessionContext.handleWalletAuthorizationTokens(
cardWalletAddress: String,
signedChallenge: VisaAuthSignedChallenge,
): CompletionResult<VisaCardActivationStatus> {
val authorizationTokensResponse = runCatching {
@ -173,7 +175,9 @@ internal class VisaCardScanHandler @Inject constructor(
Timber.i(
"Failed to get Access token for Wallet public key authoziation. Authorizing using Card Pub key",
)
return handleCardAuthorization()
return handleCardAuthorization(
cardWalletAddress = cardWalletAddress,
)
}
Timber.i("Authorized using Wallet public key successfully")
@ -181,7 +185,9 @@ internal class VisaCardScanHandler @Inject constructor(
return CompletionResult.Success(VisaCardActivationStatus.Activated(authorizationTokensResponse))
}
private suspend fun SessionContext.handleCardAuthorization(): CompletionResult<VisaCardActivationStatus> {
private suspend fun SessionContext.handleCardAuthorization(
cardWalletAddress: String,
): CompletionResult<VisaCardActivationStatus> {
val card = session.environment.card ?: return CompletionResult.Failure(TangemSdkError.MissingPreflightRead())
Timber.i("Requesting authorization challenge to sign")
@ -258,6 +264,7 @@ internal class VisaCardScanHandler @Inject constructor(
activationInput = activationInput,
authTokens = authorizationTokensResponse,
remoteState = activationRemoteState,
cardWalletAddress = cardWalletAddress,
),
)
}

View file

@ -18,7 +18,7 @@ class MockVisaActivationRepository @AssistedInject constructor(
}
override suspend fun getActivationRemoteStateLongPoll(): VisaActivationRemoteState {
return VisaActivationRemoteState.WaitingPinCode
return VisaActivationRemoteState.PaymentAccountDeploying
}
override suspend fun getCardWalletAcceptanceData(

View file

@ -0,0 +1,11 @@
package com.tangem.domain.visa.model
import com.squareup.moshi.Json
import kotlinx.serialization.Serializable
@Serializable
data class VisaActivationOrderInfo(
@Json(name = "orderId") val orderId: String,
@Json(name = "customer_id") val customerId: String,
@Json(name = "customer_wallet_address") val customerWalletAddress: String,
)

View file

@ -13,14 +13,16 @@ sealed class VisaActivationRemoteState {
@Serializable
data class CustomerWalletSignatureRequired(
val request: VisaCardWalletDataToSignRequest,
val activationOrderInfo: VisaActivationOrderInfo,
) : VisaActivationRemoteState()
@Serializable
data object PaymentAccountDeploying : VisaActivationRemoteState()
@Serializable
data object WaitingPinCode : VisaActivationRemoteState()
data class WaitingPinCode(
val activationOrderInfo: VisaActivationOrderInfo,
) : VisaActivationRemoteState()
@Serializable
data object WaitingForActivationFinishing : VisaActivationRemoteState()
@ -39,7 +41,8 @@ sealed class VisaActivationRemoteState {
@Suppress("ClassNaming")
class VisaActivationRemoteState_Json(
@Json(name = "type") val type: VisaActivationRemoteState_Type,
@Json(name = "request") val request: VisaCardWalletDataToSignRequest? = null,
@Json(name = "requestCardWallet") val requestCardWallet: VisaCardWalletDataToSignRequest? = null,
@Json(name = "activationOrderInfo") val activationOrderInfo: VisaActivationOrderInfo? = null,
)
@Suppress("ClassNaming")
@ -62,11 +65,12 @@ class VisaActivationRemoteState_JsonAdapter {
fun fromJson(value: VisaActivationRemoteState_Json): VisaActivationRemoteState {
return when (value.type) {
VisaActivationRemoteState_Type.CardWalletSignatureRequired ->
VisaActivationRemoteState.CardWalletSignatureRequired(value.request!!)
VisaActivationRemoteState.CardWalletSignatureRequired(value.requestCardWallet!!)
VisaActivationRemoteState_Type.CustomerWalletSignatureRequired ->
VisaActivationRemoteState.CustomerWalletSignatureRequired(value.request!!)
VisaActivationRemoteState.CustomerWalletSignatureRequired(value.activationOrderInfo!!)
VisaActivationRemoteState_Type.PaymentAccountDeploying -> VisaActivationRemoteState.PaymentAccountDeploying
VisaActivationRemoteState_Type.WaitingPinCode -> VisaActivationRemoteState.WaitingPinCode
VisaActivationRemoteState_Type.WaitingPinCode ->
VisaActivationRemoteState.WaitingPinCode(value.activationOrderInfo!!)
VisaActivationRemoteState_Type.WaitingForActivationFinishing ->
VisaActivationRemoteState.WaitingForActivationFinishing
VisaActivationRemoteState_Type.Activated -> VisaActivationRemoteState.Activated
@ -80,17 +84,20 @@ class VisaActivationRemoteState_JsonAdapter {
is VisaActivationRemoteState.CardWalletSignatureRequired ->
VisaActivationRemoteState_Json(
type = VisaActivationRemoteState_Type.CardWalletSignatureRequired,
request = value.request,
requestCardWallet = value.request,
)
is VisaActivationRemoteState.CustomerWalletSignatureRequired ->
VisaActivationRemoteState_Json(
type = VisaActivationRemoteState_Type.CustomerWalletSignatureRequired,
request = value.request,
activationOrderInfo = value.activationOrderInfo,
)
is VisaActivationRemoteState.PaymentAccountDeploying ->
VisaActivationRemoteState_Json(VisaActivationRemoteState_Type.PaymentAccountDeploying)
is VisaActivationRemoteState.WaitingPinCode ->
VisaActivationRemoteState_Json(VisaActivationRemoteState_Type.WaitingPinCode)
VisaActivationRemoteState_Json(
VisaActivationRemoteState_Type.WaitingPinCode,
activationOrderInfo = value.activationOrderInfo,
)
is VisaActivationRemoteState.WaitingForActivationFinishing ->
VisaActivationRemoteState_Json(VisaActivationRemoteState_Type.WaitingForActivationFinishing)
is VisaActivationRemoteState.Activated -> VisaActivationRemoteState_Json(

View file

@ -19,6 +19,7 @@ sealed class VisaCardActivationStatus {
val activationInput: VisaActivationInput,
val authTokens: VisaAuthTokens,
val remoteState: VisaActivationRemoteState,
val cardWalletAddress: String,
) : VisaCardActivationStatus()
@Serializable
@ -43,6 +44,7 @@ class VisaCardActivationStatus_Json(
@Json(name = "activationInput") val activationInput: VisaActivationInput? = null,
@Json(name = "authTokens") val authTokens: VisaAuthTokens? = null,
@Json(name = "remoteState") val remoteState: VisaActivationRemoteState? = null,
@Json(name = "cardWalletAddress") val cardWalletAddress: String? = null,
)
// do not rename instances
@ -67,6 +69,7 @@ class VisaCardActivationStatus_JsonAdapter {
value.activationInput!!,
value.authTokens!!,
value.remoteState!!,
value.cardWalletAddress!!,
)
VisaCardActivationStatus_Type.NotStartedActivation -> VisaCardActivationStatus.NotStartedActivation(
value.activationInput!!,
@ -88,6 +91,7 @@ class VisaCardActivationStatus_JsonAdapter {
value.activationInput,
value.authTokens,
value.remoteState,
value.cardWalletAddress,
)
is VisaCardActivationStatus.NotStartedActivation -> VisaCardActivationStatus_Json(
VisaCardActivationStatus_Type.NotStartedActivation,

View file

@ -1,13 +1,3 @@
package com.tangem.domain.visa.model
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import kotlinx.serialization.Serializable
@Serializable
@JsonClass(generateAdapter = true)
data class VisaCardWalletDataToSignRequest(
@Json(name = "orderId") val orderId: String,
@Json(name = "customer_id") val customerId: String,
@Json(name = "customer_wallet_address") val customerWalletAddress: String,
)
typealias VisaCardWalletDataToSignRequest = VisaActivationOrderInfo

View file

@ -1,5 +1,8 @@
package com.tangem.domain.visa.model
import kotlinx.serialization.Serializable
@Serializable
data class VisaDataToSignByCardWallet(
val request: VisaCardWalletDataToSignRequest,
val hashToSign: String,

View file

@ -96,17 +96,36 @@ internal class DefaultOnboardingVisaComponent @AssistedInject constructor(
return when (route) {
is OnboardingVisaRoute.Welcome -> OnboardingVisaWelcomeComponent(
appComponentContext = factoryContext,
config = OnboardingVisaWelcomeComponent.Config.Welcome,
params = OnboardingVisaWelcomeComponent.Params(
isWelcomeBack = route.isWelcomeBack,
childParams = childParams,
onDone = { model.navigateFromWelcome(route) },
onDone = { model.stackNavigation.push(OnboardingVisaRoute.AccessCode) },
),
)
is OnboardingVisaRoute.WelcomeBack -> OnboardingVisaWelcomeComponent(
appComponentContext = factoryContext,
config = OnboardingVisaWelcomeComponent.Config.WelcomeBack(
activationInput = route.activationInput,
dataToSignRequest = route.dataToSignByCardWalletRequest,
scanResponse = model.currentScanResponse.value,
),
params = OnboardingVisaWelcomeComponent.Params(
childParams = childParams,
onDone = {
val activationReadyEvent =
(it as? OnboardingVisaWelcomeComponent.DoneEvent.WelcomeBackDone)?.activationReadyEvent
if (activationReadyEvent != null) {
model.navigateFromActivationScreen(activationReadyEvent)
}
},
),
)
OnboardingVisaRoute.AccessCode -> OnboardingVisaAccessCodeComponent(
appComponentContext = factoryContext,
params = OnboardingVisaAccessCodeComponent.Params(
childParams = childParams,
onDone = { model.navigateFromAccessCode(it) },
onDone = { model.navigateFromActivationScreen(it) },
),
config = OnboardingVisaAccessCodeComponent.Config(
scanResponse = model.currentScanResponse.value,
@ -129,7 +148,7 @@ internal class DefaultOnboardingVisaComponent @AssistedInject constructor(
onDone = {
model.stackNavigation.push(
OnboardingVisaRoute.PinCode(
activationOrderId = "TODO", // TODO [REDACTED_TASK_KEY] [Visa] In progress screen after pin code
activationOrderInfo = TODO(), // TODO [REDACTED_TASK_KEY] [Visa] In progress screen after pin code
),
)
},
@ -139,14 +158,14 @@ internal class DefaultOnboardingVisaComponent @AssistedInject constructor(
appComponentContext = factoryContext,
config = OnboardingVisaOtherWalletComponent.Config(
scanResponse = model.currentScanResponse.value,
visaDataForApprove = route.visaDataForApprove,
preparationDataForApprove = route.preparationDataForApprove,
),
params = OnboardingVisaOtherWalletComponent.Params(
childParams = childParams,
onDone = {
model.stackNavigation.push(
OnboardingVisaRoute.PinCode(
activationOrderId = route.visaDataForApprove.dataToSign.request.orderId,
activationOrderInfo = TODO(),
),
)
},
@ -156,7 +175,7 @@ internal class DefaultOnboardingVisaComponent @AssistedInject constructor(
appComponentContext = factoryContext,
config = OnboardingVisaPinCodeComponent.Config(
scanResponse = model.currentScanResponse.value,
activationOrderId = route.activationOrderId,
activationOrderInfo = route.activationOrderInfo,
),
params = OnboardingVisaPinCodeComponent.Params(
childParams = childParams,
@ -166,7 +185,8 @@ internal class DefaultOnboardingVisaComponent @AssistedInject constructor(
is OnboardingVisaRoute.TangemWalletApproveOption -> OnboardingVisaApproveComponent(
appComponentContext = factoryContext,
config = OnboardingVisaApproveComponent.Config(
visaDataForApprove = route.visaDataForApprove,
customerWalletCardId = route.foundWalletCardId,
preparationDataForApprove = route.preparationDataForApprove,
scanResponse = model.currentScanResponse.value,
),
params = OnboardingVisaApproveComponent.Params(

View file

@ -8,11 +8,12 @@ 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.ComposableContentComponent
import com.tangem.core.ui.security.DisableScreenshotsDisposableEffect
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.visa.model.VisaDataForApprove
import com.tangem.features.onboarding.v2.visa.impl.DefaultOnboardingVisaComponent
import com.tangem.features.onboarding.v2.visa.impl.child.accesscode.model.OnboardingVisaAccessCodeModel
import com.tangem.features.onboarding.v2.visa.impl.child.accesscode.ui.OnboardingVisaAccessCode
import com.tangem.features.onboarding.v2.visa.impl.common.ActivationReadyEvent
import kotlinx.coroutines.launch
internal class OnboardingVisaAccessCodeComponent(
@ -43,7 +44,7 @@ internal class OnboardingVisaAccessCodeComponent(
BackHandler(onBack = model::onBack)
// DisableScreenshotsDisposableEffect()
DisableScreenshotsDisposableEffect()
OnboardingVisaAccessCode(state, modifier)
}
@ -54,12 +55,6 @@ internal class OnboardingVisaAccessCodeComponent(
data class Params(
val childParams: DefaultOnboardingVisaComponent.ChildParams,
val onDone: (DoneEvent) -> Unit,
)
data class DoneEvent(
val visaDataForApprove: VisaDataForApprove,
val walletFound: Boolean,
val newScanResponse: ScanResponse,
val onDone: (ActivationReadyEvent) -> Unit,
)
}

View file

@ -7,17 +7,14 @@ import com.tangem.common.extensions.toHexString
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.common.visa.VisaUtilities
import com.tangem.domain.common.visa.VisaWalletPublicKeyUtility
import com.tangem.domain.visa.model.VisaCardActivationStatus
import com.tangem.domain.visa.model.VisaCardId
import com.tangem.domain.visa.model.VisaCustomerWalletDataToSignRequest
import com.tangem.domain.visa.model.VisaDataForApprove
import com.tangem.domain.visa.repository.VisaActivationRepository
import com.tangem.domain.visa.repository.VisaAuthRepository
import com.tangem.domain.wallets.usecase.GetWalletsUseCase
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.common.ActivationReadyEvent
import com.tangem.sdk.api.TangemSdkManager
import com.tangem.sdk.api.visa.VisaCardActivationTaskMode
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
@ -37,7 +34,6 @@ internal class OnboardingVisaAccessCodeModel @Inject constructor(
@Suppress("UnusedPrivateMember")
private val tangemSdkManager: TangemSdkManager,
private val visaAuthRepository: VisaAuthRepository,
private val getWalletsUseCase: GetWalletsUseCase,
) : Model() {
private val params: OnboardingVisaAccessCodeComponent.Config = paramsContainer.require()
@ -55,7 +51,7 @@ internal class OnboardingVisaAccessCodeModel @Inject constructor(
val uiState = _uiState.asStateFlow()
val onBack = MutableSharedFlow<Unit>()
val onDone = MutableSharedFlow<OnboardingVisaAccessCodeComponent.DoneEvent>()
val onDone = MutableSharedFlow<ActivationReadyEvent>()
fun onBack() {
if (uiState.value.buttonLoading) return
@ -166,61 +162,25 @@ internal class OnboardingVisaAccessCodeModel @Inject constructor(
return@launch
}
// load data to sign by customer wallet for the next step
val dataToSign = runCatching {
visaActivationRepository.getCustomerWalletAcceptanceData(
VisaCustomerWalletDataToSignRequest(
orderId = result.data.signedActivationData.dataToSign.request.orderId,
cardWalletAddress = result.data.signedActivationData.cardWalletAddress,
),
)
}.getOrElse {
loading(false)
// TODO show alert
return@launch
}
val targetAddress = result.data.signedActivationData.dataToSign.request.customerWalletAddress
val foundCardId = tryToFindExistingWalletCardId(targetAddress)
modelScope.launch {
onDone.emit(
OnboardingVisaAccessCodeComponent.DoneEvent(
visaDataForApprove = VisaDataForApprove(
targetAddress = targetAddress,
customerWalletCardId = foundCardId,
dataToSign = dataToSign,
ActivationReadyEvent(
customerWalletDataToSignRequest = VisaCustomerWalletDataToSignRequest(
orderId = result.data.signedActivationData.dataToSign.request.orderId,
cardWalletAddress = result.data.signedActivationData.cardWalletAddress,
),
walletFound = foundCardId != null,
newScanResponse = params.scanResponse.copy(
card = result.data.newCardDTO,
),
customerWalletTargetAddress = targetAddress,
),
)
}
}
}
private fun tryToFindExistingWalletCardId(targetAddress: String): String? {
val wallets = getWalletsUseCase.invokeSync().filter { it.isLocked.not() }
return wallets.firstOrNull { wallet ->
wallet.scanResponse.card.wallets.any {
val derivedKey = it.derivedKeys[VisaUtilities.visaDefaultDerivationPath] ?: return@any false
VisaWalletPublicKeyUtility.validateExtendedPublicKey(
targetAddress = targetAddress,
extendedPublicKey = derivedKey,
).onLeft {
return@any VisaWalletPublicKeyUtility.findKeyWithoutDerivation(
targetAddress = targetAddress,
card = wallet.scanResponse.card,
).isRight()
}.isRight()
}
}?.cardId
}
private fun loading(state: Boolean) {
_uiState.update { it.copy(buttonLoading = state) }
}

View file

@ -10,10 +10,10 @@ import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.visa.model.VisaDataForApprove
import com.tangem.features.onboarding.v2.visa.impl.DefaultOnboardingVisaComponent
import com.tangem.features.onboarding.v2.visa.impl.child.approve.model.OnboardingVisaApproveModel
import com.tangem.features.onboarding.v2.visa.impl.child.approve.ui.OnboardingVisaApprove
import com.tangem.features.onboarding.v2.visa.impl.common.PreparationDataForApprove
import kotlinx.coroutines.launch
internal class OnboardingVisaApproveComponent(
@ -43,7 +43,8 @@ internal class OnboardingVisaApproveComponent(
}
data class Config(
val visaDataForApprove: VisaDataForApprove,
val customerWalletCardId: String?,
val preparationDataForApprove: PreparationDataForApprove,
val scanResponse: ScanResponse,
)

View file

@ -7,6 +7,7 @@ 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.VisaCardId
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
@ -51,8 +52,20 @@ internal class OnboardingVisaApproveModel @Inject constructor(
loading(true)
modelScope.launch {
val dataToSign = runCatching {
visaActivationRepository.getCustomerWalletAcceptanceData(params.preparationDataForApprove.request)
}.getOrElse {
loading(false)
// TODO show dialog
return@launch
}
val result = tangemSdkManager.visaCustomerWalletApprove(
visaDataForApprove = params.visaDataForApprove,
visaDataForApprove = VisaDataForApprove(
customerWalletCardId = params.customerWalletCardId,
targetAddress = params.preparationDataForApprove.customerWalletAddress,
dataToSign = dataToSign,
),
) as? CompletionResult.Success ?: run {
loading(false)
// TODO show dialog

View file

@ -39,7 +39,7 @@ internal class OnboardingVisaInProgressModel @Inject constructor(
visaActivationRepository.getActivationRemoteStateLongPoll()
}.getOrNull()
if (result == VisaActivationRemoteState.WaitingPinCode) {
if (result is VisaActivationRemoteState.WaitingPinCode) {
onDone.emit(Unit)
break
}

View file

@ -10,10 +10,10 @@ import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.visa.model.VisaDataForApprove
import com.tangem.features.onboarding.v2.visa.impl.DefaultOnboardingVisaComponent
import com.tangem.features.onboarding.v2.visa.impl.child.otherwallet.model.OnboardingVisaOtherWalletModel
import com.tangem.features.onboarding.v2.visa.impl.child.otherwallet.ui.OnboardingVisaOtherWallet
import com.tangem.features.onboarding.v2.visa.impl.common.PreparationDataForApprove
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
@ -45,7 +45,7 @@ internal class OnboardingVisaOtherWalletComponent(
data class Config(
val scanResponse: ScanResponse,
val visaDataForApprove: VisaDataForApprove,
val preparationDataForApprove: PreparationDataForApprove,
)
data class Params(

View file

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

View file

@ -11,6 +11,7 @@ import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.core.ui.security.DisableScreenshotsDisposableEffect
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.visa.model.VisaActivationOrderInfo
import com.tangem.features.onboarding.v2.visa.impl.DefaultOnboardingVisaComponent
import com.tangem.features.onboarding.v2.visa.impl.child.pincode.model.OnboardingVisaPinCodeModel
import com.tangem.features.onboarding.v2.visa.impl.child.pincode.ui.OnboardingVisaPinCode
@ -46,7 +47,7 @@ internal class OnboardingVisaPinCodeComponent(
data class Config(
val scanResponse: ScanResponse,
val activationOrderId: String,
val activationOrderInfo: VisaActivationOrderInfo,
)
data class Params(

View file

@ -76,7 +76,7 @@ internal class OnboardingVisaPinCodeModel @Inject constructor(
setVisaPinCodeUseCase(
pinCode = _uiState.value.pinCode,
visaCardId = visaCardId,
activationOrderId = params.activationOrderId,
activationOrderId = params.activationOrderInfo.orderId,
).onLeft {
loading(false)
return@launch

View file

@ -2,32 +2,39 @@ package com.tangem.features.onboarding.v2.visa.impl.child.welcome
import androidx.activity.compose.BackHandler
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
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.ComposableContentComponent
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.visa.model.VisaActivationInput
import com.tangem.domain.visa.model.VisaCardWalletDataToSignRequest
import com.tangem.features.onboarding.v2.visa.impl.DefaultOnboardingVisaComponent
import com.tangem.features.onboarding.v2.visa.impl.child.welcome.model.OnboardingVisaWelcomeModel
import com.tangem.features.onboarding.v2.visa.impl.child.welcome.ui.OnboardingVisaWelcome
import com.tangem.features.onboarding.v2.visa.impl.child.welcome.ui.state.OnboardingVisaWelcomeUM
import com.tangem.features.onboarding.v2.visa.impl.common.ActivationReadyEvent
import kotlinx.coroutines.launch
internal class OnboardingVisaWelcomeComponent(
appComponentContext: AppComponentContext,
config: Config,
private val params: Params,
) : ComposableContentComponent, AppComponentContext by appComponentContext {
private val model: OnboardingVisaWelcomeModel = getOrCreateModel(config)
init {
componentScope.launch {
model.onDone.collect { params.onDone(it) }
}
}
@Composable
override fun Content(modifier: Modifier) {
val state = remember(this) {
OnboardingVisaWelcomeUM(
mode = if (params.isWelcomeBack) {
OnboardingVisaWelcomeUM.Mode.WelcomeBack
} else {
OnboardingVisaWelcomeUM.Mode.Hello
},
userName = "User", // TODO:
onContinueClick = params.onDone,
)
}
val state by model.uiState.collectAsStateWithLifecycle()
BackHandler(onBack = remember(this) { { params.childParams.onBack() } })
@ -37,9 +44,26 @@ internal class OnboardingVisaWelcomeComponent(
)
}
sealed class Config {
data object Welcome : Config()
data class WelcomeBack(
val activationInput: VisaActivationInput,
val dataToSignRequest: VisaCardWalletDataToSignRequest,
val scanResponse: ScanResponse,
) : Config()
}
data class Params(
val childParams: DefaultOnboardingVisaComponent.ChildParams,
val isWelcomeBack: Boolean,
val onDone: () -> Unit,
val onDone: (DoneEvent) -> Unit,
)
sealed class DoneEvent {
data object WelcomeDone : DoneEvent()
data class WelcomeBackDone(
val activationReadyEvent: ActivationReadyEvent,
) : DoneEvent()
}
}

View file

@ -0,0 +1,116 @@
package com.tangem.features.onboarding.v2.visa.impl.child.welcome.model
import com.tangem.common.CompletionResult
import com.tangem.common.extensions.toHexString
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.VisaCardId
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.ui.state.OnboardingVisaWelcomeUM
import com.tangem.features.onboarding.v2.visa.impl.common.ActivationReadyEvent
import com.tangem.sdk.api.TangemSdkManager
import com.tangem.sdk.api.visa.VisaCardActivationTaskMode
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
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
@ComponentScoped
internal class OnboardingVisaWelcomeModel @Inject constructor(
paramsContainer: ParamsContainer,
override val dispatchers: CoroutineDispatcherProvider,
private val tangemSdkManager: TangemSdkManager,
private val visaActivationRepositoryFactory: VisaActivationRepository.Factory,
) : Model() {
private val params = paramsContainer.require<Config>()
private val _uiState = MutableStateFlow(getInitialState())
val uiState = _uiState.asStateFlow()
val onDone = MutableSharedFlow<DoneEvent>()
private fun getInitialState(): OnboardingVisaWelcomeUM {
return OnboardingVisaWelcomeUM(
mode = if (params is Config.WelcomeBack) {
OnboardingVisaWelcomeUM.Mode.WelcomeBack
} else {
OnboardingVisaWelcomeUM.Mode.Hello
},
onContinueClick = ::onContinueClick,
)
}
private fun onContinueClick() {
if (params !is Config.WelcomeBack) {
modelScope.launch { onDone.emit(DoneEvent.WelcomeDone) }
return
}
val visaActivationRepository = visaActivationRepositoryFactory.create(
VisaCardId(
cardId = params.scanResponse.card.cardId,
cardPublicKey = params.scanResponse.card.cardPublicKey.toHexString(),
),
)
loading(true)
modelScope.launch {
val dataToSignByCardWallet = runCatching {
visaActivationRepository.getCardWalletAcceptanceData(params.dataToSignRequest)
}.getOrElse {
loading(false)
// TODO show alert
return@launch
}
val result = tangemSdkManager.activateVisaCard(
mode = VisaCardActivationTaskMode.SignOnly(dataToSignByCardWallet = dataToSignByCardWallet),
activationInput = params.activationInput,
) as? CompletionResult.Success ?: run {
loading(false)
// TODO show alert
return@launch
}
runCatching {
visaActivationRepository.activateCard(result.data.signedActivationData)
}.onFailure {
loading(false)
// TODO show alert
return@launch
}
val targetAddress = result.data.signedActivationData.dataToSign.request.customerWalletAddress
modelScope.launch {
onDone.emit(
DoneEvent.WelcomeBackDone(
ActivationReadyEvent(
customerWalletDataToSignRequest = VisaCustomerWalletDataToSignRequest(
orderId = result.data.signedActivationData.dataToSign.request.orderId,
cardWalletAddress = result.data.signedActivationData.cardWalletAddress,
),
newScanResponse = params.scanResponse.copy(
card = result.data.newCardDTO,
),
customerWalletTargetAddress = targetAddress,
),
),
)
}
}
}
private fun loading(state: Boolean) {
_uiState.update { it.copy(continueButtonLoading = state) }
}
}

View file

@ -81,7 +81,8 @@ internal fun OnboardingVisaWelcome(state: OnboardingVisaWelcomeUM, modifier: Mod
modifier = Modifier
.padding(start = 16.dp, end = 16.dp, bottom = 16.dp)
.fillMaxWidth(),
text = "Start Activation",
text = "Continue Activation",
showProgress = state.continueButtonLoading,
iconResId = R.drawable.ic_tangem_24,
onClick = state.onContinueClick,
)

View file

@ -3,6 +3,7 @@ package com.tangem.features.onboarding.v2.visa.impl.child.welcome.ui.state
internal data class OnboardingVisaWelcomeUM(
val mode: Mode = Mode.Hello,
val userName: String = "",
val continueButtonLoading: Boolean = false,
val onContinueClick: () -> Unit = {},
) {
enum class Mode {

View file

@ -0,0 +1,10 @@
package com.tangem.features.onboarding.v2.visa.impl.common
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.visa.model.VisaCustomerWalletDataToSignRequest
internal data class ActivationReadyEvent(
val customerWalletDataToSignRequest: VisaCustomerWalletDataToSignRequest,
val customerWalletTargetAddress: String,
val newScanResponse: ScanResponse,
)

View file

@ -0,0 +1,10 @@
package com.tangem.features.onboarding.v2.visa.impl.common
import com.tangem.domain.visa.model.VisaCustomerWalletDataToSignRequest
import kotlinx.serialization.Serializable
@Serializable
internal data class PreparationDataForApprove(
val customerWalletAddress: String,
val request: VisaCustomerWalletDataToSignRequest,
)

View file

@ -10,6 +10,7 @@ import com.tangem.features.onboarding.v2.visa.impl.child.choosewallet.model.Onbo
import com.tangem.features.onboarding.v2.visa.impl.child.inprogress.model.OnboardingVisaInProgressModel
import com.tangem.features.onboarding.v2.visa.impl.child.otherwallet.model.OnboardingVisaOtherWalletModel
import com.tangem.features.onboarding.v2.visa.impl.child.pincode.model.OnboardingVisaPinCodeModel
import com.tangem.features.onboarding.v2.visa.impl.child.welcome.model.OnboardingVisaWelcomeModel
import com.tangem.features.onboarding.v2.visa.impl.model.OnboardingVisaModel
import dagger.Binds
import dagger.Module
@ -66,4 +67,9 @@ internal interface ModelModule {
@IntoMap
@ClassKey(OnboardingVisaModel::class)
fun provideModel7(model: OnboardingVisaModel): Model
@Binds
@IntoMap
@ClassKey(OnboardingVisaWelcomeModel::class)
fun provideModel8(model: OnboardingVisaWelcomeModel): Model
}

View file

@ -7,12 +7,17 @@ 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.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.VisaCustomerWalletDataToSignRequest
import com.tangem.domain.wallets.usecase.GetWalletsUseCase
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.common.ActivationReadyEvent
import com.tangem.features.onboarding.v2.visa.impl.common.PreparationDataForApprove
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
@ -25,6 +30,7 @@ import javax.inject.Inject
internal class OnboardingVisaModel @Inject constructor(
paramsContainer: ParamsContainer,
override val dispatchers: CoroutineDispatcherProvider,
private val getWalletsUseCase: GetWalletsUseCase,
) : Model() {
@Suppress("UnusedPrivateMember")
@ -49,37 +55,24 @@ internal class OnboardingVisaModel @Inject constructor(
)
}
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) {
is VisaActivationRemoteState.CardWalletSignatureRequired,
is 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) {
fun navigateFromActivationScreen(result: ActivationReadyEvent) {
_currentScanResponse.value = result.newScanResponse
val foundWalletCardId = tryToFindExistingWalletCardId(result.customerWalletTargetAddress)
val preparationDataForApprove = PreparationDataForApprove(
customerWalletAddress = result.customerWalletTargetAddress,
request = result.customerWalletDataToSignRequest,
)
stackNavigation.push(
if (result.walletFound) {
if (foundWalletCardId != null) {
OnboardingVisaRoute.TangemWalletApproveOption(
visaDataForApprove = result.visaDataForApprove,
foundWalletCardId = foundWalletCardId,
preparationDataForApprove = preparationDataForApprove,
allowNavigateBack = false,
)
} else {
OnboardingVisaRoute.ChooseWallet(result.visaDataForApprove)
OnboardingVisaRoute.ChooseWallet(
preparationDataForApprove = preparationDataForApprove,
)
},
)
}
@ -92,11 +85,14 @@ internal class OnboardingVisaModel @Inject constructor(
when (event) {
OnboardingVisaChooseWalletComponent.Params.Event.TangemWallet ->
OnboardingVisaRoute.TangemWalletApproveOption(
visaDataForApprove = route.visaDataForApprove,
preparationDataForApprove = route.preparationDataForApprove,
foundWalletCardId = null,
allowNavigateBack = true,
)
OnboardingVisaChooseWalletComponent.Params.Event.OtherWallet ->
OnboardingVisaRoute.OtherWalletApproveOption(route.visaDataForApprove)
OnboardingVisaRoute.OtherWalletApproveOption(
preparationDataForApprove = route.preparationDataForApprove,
)
},
)
}
@ -116,6 +112,7 @@ internal class OnboardingVisaModel @Inject constructor(
stackNavigation.pop()
}
}
is OnboardingVisaRoute.WelcomeBack,
is OnboardingVisaRoute.Welcome,
is OnboardingVisaRoute.ChooseWallet,
OnboardingVisaRoute.InProgress,
@ -129,9 +126,72 @@ internal class OnboardingVisaModel @Inject constructor(
val activationStatus = params.scanResponse.visaCardActivationStatus
return when (activationStatus) {
is VisaCardActivationStatus.ActivationStarted -> OnboardingVisaRoute.Welcome(isWelcomeBack = true)
is VisaCardActivationStatus.NotStartedActivation -> OnboardingVisaRoute.Welcome(isWelcomeBack = false)
is VisaCardActivationStatus.ActivationStarted -> {
when (val remoteState = activationStatus.remoteState) {
is VisaActivationRemoteState.CardWalletSignatureRequired -> OnboardingVisaRoute.WelcomeBack(
activationInput = activationStatus.activationInput,
dataToSignByCardWalletRequest = remoteState.request,
)
is VisaActivationRemoteState.CustomerWalletSignatureRequired ->
remoteState.getRoute(activationStatus)
VisaActivationRemoteState.PaymentAccountDeploying -> OnboardingVisaRoute.InProgress // TODO
VisaActivationRemoteState.WaitingForActivationFinishing -> OnboardingVisaRoute.InProgress // TODO
is VisaActivationRemoteState.WaitingPinCode -> OnboardingVisaRoute.PinCode(
activationOrderInfo = remoteState.activationOrderInfo,
)
VisaActivationRemoteState.Activated,
VisaActivationRemoteState.BlockedForActivation,
-> error("Activation status is not correct for onboarding flow")
}
}
is VisaCardActivationStatus.NotStartedActivation -> OnboardingVisaRoute.Welcome
else -> error("Visa activation status is not correct for onboarding flow")
}
}
private fun VisaActivationRemoteState.CustomerWalletSignatureRequired.getRoute(
activationStatus: VisaCardActivationStatus.ActivationStarted,
): OnboardingVisaRoute {
val foundWalletCardId = tryToFindExistingWalletCardId(this.activationOrderInfo.customerWalletAddress)
val request = VisaCustomerWalletDataToSignRequest(
orderId = this.activationOrderInfo.orderId,
cardWalletAddress = activationStatus.cardWalletAddress,
)
val preparationDataForApprove = PreparationDataForApprove(
customerWalletAddress = this.activationOrderInfo.customerWalletAddress,
request = request,
)
return if (foundWalletCardId != null) {
OnboardingVisaRoute.TangemWalletApproveOption(
preparationDataForApprove = preparationDataForApprove,
foundWalletCardId = foundWalletCardId,
allowNavigateBack = false,
)
} else {
OnboardingVisaRoute.ChooseWallet(
preparationDataForApprove = preparationDataForApprove,
)
}
}
private fun tryToFindExistingWalletCardId(targetAddress: String): String? {
val wallets = getWalletsUseCase.invokeSync().filter { it.isLocked.not() }
return wallets.firstOrNull { wallet ->
wallet.scanResponse.card.wallets.any {
val derivedKey = it.derivedKeys[VisaUtilities.visaDefaultDerivationPath] ?: return@any false
VisaWalletPublicKeyUtility.validateExtendedPublicKey(
targetAddress = targetAddress,
extendedPublicKey = derivedKey,
).onLeft {
return@any VisaWalletPublicKeyUtility.findKeyWithoutDerivation(
targetAddress = targetAddress,
card = wallet.scanResponse.card,
).isRight()
}.isRight()
}
}?.cardId
}
}

View file

@ -1,32 +1,46 @@
package com.tangem.features.onboarding.v2.visa.impl.route
import com.tangem.domain.visa.model.VisaDataForApprove
import com.tangem.domain.visa.model.VisaActivationInput
import com.tangem.domain.visa.model.VisaActivationOrderInfo
import com.tangem.domain.visa.model.VisaCardWalletDataToSignRequest
import com.tangem.features.onboarding.v2.visa.impl.common.PreparationDataForApprove
import kotlinx.serialization.Serializable
@Serializable
sealed class OnboardingVisaRoute {
internal sealed class OnboardingVisaRoute {
@Serializable
data class Welcome(val isWelcomeBack: Boolean) : OnboardingVisaRoute()
data object Welcome : OnboardingVisaRoute()
@Serializable
data class WelcomeBack(
val activationInput: VisaActivationInput,
val dataToSignByCardWalletRequest: VisaCardWalletDataToSignRequest,
) : OnboardingVisaRoute()
@Serializable
data object AccessCode : OnboardingVisaRoute()
@Serializable
data class ChooseWallet(val visaDataForApprove: VisaDataForApprove) : OnboardingVisaRoute()
data class ChooseWallet(
val preparationDataForApprove: PreparationDataForApprove,
) : OnboardingVisaRoute()
@Serializable
data class TangemWalletApproveOption(
val visaDataForApprove: VisaDataForApprove,
val preparationDataForApprove: PreparationDataForApprove,
val foundWalletCardId: String?,
val allowNavigateBack: Boolean,
) : OnboardingVisaRoute()
@Serializable
data class OtherWalletApproveOption(val visaDataForApprove: VisaDataForApprove) : OnboardingVisaRoute()
data class OtherWalletApproveOption(
val preparationDataForApprove: PreparationDataForApprove,
) : OnboardingVisaRoute()
@Serializable
data object InProgress : OnboardingVisaRoute()
@Serializable
data class PinCode(val activationOrderId: String) : OnboardingVisaRoute()
data class PinCode(val activationOrderInfo: VisaActivationOrderInfo) : OnboardingVisaRoute()
}

View file

@ -2,7 +2,9 @@ package com.tangem.features.onboarding.v2.visa.impl.route
@Suppress("MagicNumber")
internal fun OnboardingVisaRoute.stepNum() = when (this) {
is OnboardingVisaRoute.Welcome -> 1
is OnboardingVisaRoute.Welcome,
is OnboardingVisaRoute.WelcomeBack,
-> 1
OnboardingVisaRoute.AccessCode -> 2
is OnboardingVisaRoute.ChooseWallet -> 3
is OnboardingVisaRoute.OtherWalletApproveOption -> 4

View file

@ -10,5 +10,7 @@ internal fun OnboardingVisaRoute.screenTitle(): TextReference = when (this) {
is OnboardingVisaRoute.OtherWalletApproveOption -> TextReference.Str("Wallet connection")
is OnboardingVisaRoute.PinCode -> TextReference.Str("PIN code")
is OnboardingVisaRoute.TangemWalletApproveOption -> TextReference.Str("Wallet connection")
is OnboardingVisaRoute.Welcome -> TextReference.Str("Getting started")
is OnboardingVisaRoute.Welcome,
is OnboardingVisaRoute.WelcomeBack,
-> TextReference.Str("Getting started")
}