Updated on 2026-08-14

This commit is contained in:
Tangem 2025-01-27 12:45:57 +03:00
parent 10bb769916
commit bb32839f9d
32 changed files with 521 additions and 77 deletions

View file

@ -27,6 +27,7 @@ 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
import com.tangem.operations.ScanTask
@ -35,6 +36,7 @@ import com.tangem.operations.derivation.DeriveMultipleWalletPublicKeysTask
import com.tangem.operations.derivation.DeriveWalletPublicKeyTask
import com.tangem.operations.pins.SetUserCodeCommand
import com.tangem.operations.preflightread.PreflightReadFilter
import com.tangem.operations.sign.SignHashResponse
import com.tangem.operations.usersetttings.SetUserCodeRecoveryAllowedTask
import com.tangem.operations.wallet.CreateWalletResponse
import com.tangem.sdk.api.CreateProductWalletTaskResponse
@ -45,6 +47,7 @@ import com.tangem.tap.domain.tasks.product.ResetBackupCardTask
import com.tangem.tap.domain.tasks.product.ResetToFactorySettingsTask
import com.tangem.tap.domain.tasks.product.ScanProductTask
import com.tangem.tap.domain.tasks.visa.VisaCardActivationTask
import com.tangem.tap.domain.tasks.visa.VisaCustomerWalletApproveTask
import com.tangem.tap.domain.twins.CreateFirstTwinWalletTask
import com.tangem.tap.domain.twins.CreateSecondTwinWalletTask
import com.tangem.tap.domain.twins.FinalizeTwinTask
@ -494,6 +497,17 @@ internal class DefaultTangemSdkManager(
}
}
override suspend fun visaCustomerWalletApprove(
visaDataForApprove: VisaDataForApprove,
): CompletionResult<SignHashResponse> {
return runTaskAsyncReturnOnMain(
runnable = VisaCustomerWalletApproveTask(
visaDataForApprove = visaDataForApprove,
),
initialMessage = Message(resources.getStringSafe(R.string.initial_message_tap_header)),
)
}
// endregion
companion object {

View file

@ -20,9 +20,11 @@ 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.CreateProductWalletTaskResponse
import com.tangem.sdk.api.TangemSdkManager
@ -207,4 +209,12 @@ class MockTangemSdkManager(
): CompletionResult<VisaCardActivationResponse> {
error("Not implemented")
}
override suspend fun visaCustomerWalletApprove(
visaDataForApprove: VisaDataForApprove,
): CompletionResult<SignHashResponse> {
error("Not implemented")
}
// endregion
}

View file

@ -311,15 +311,12 @@ private class ScanWalletProcessor(
}
private fun getWalletProductType(card: CardDTO): ProductType {
if (RING_BATCH_IDS.contains(card.batchId) || card.batchId.startsWith(RING_BATCH_PREFIX)) {
return ProductType.Ring
}
return if (card.firmwareVersion >= FirmwareVersion.Ed25519Slip0010Available &&
card.settings.isKeysImportAllowed
) {
ProductType.Wallet2
} else {
ProductType.Wallet
return when {
card.isVisa -> ProductType.Visa
RING_BATCH_IDS.contains(card.batchId) || card.batchId.startsWith(RING_BATCH_PREFIX) -> ProductType.Ring
card.firmwareVersion >= FirmwareVersion.Ed25519Slip0010Available &&
card.settings.isKeysImportAllowed -> ProductType.Wallet2
else -> ProductType.Wallet
}
}

View file

@ -1,12 +1,12 @@
package com.tangem.tap.domain.tasks.visa
import com.reown.util.hexToBytes
import com.tangem.common.CompletionResult
import com.tangem.common.card.Card
import com.tangem.common.core.CardSession
import com.tangem.common.core.CardSessionRunnable
import com.tangem.common.core.CompletionCallback
import com.tangem.common.core.TangemSdkError
import com.tangem.common.extensions.hexToBytes
import com.tangem.common.extensions.toHexString
import com.tangem.common.map
import com.tangem.datasource.local.visa.VisaAuthTokenStorage

View file

@ -0,0 +1,188 @@
package com.tangem.tap.domain.tasks.visa
import arrow.core.getOrElse
import com.tangem.common.CompletionResult
import com.tangem.common.card.Card
import com.tangem.common.card.CardWallet
import com.tangem.common.core.CardSession
import com.tangem.common.core.CardSessionRunnable
import com.tangem.common.core.CompletionCallback
import com.tangem.common.core.TangemSdkError
import com.tangem.common.extensions.hexToBytes
import com.tangem.crypto.hdWallet.DerivationPath
import com.tangem.crypto.hdWallet.bip32.ExtendedPublicKey
import com.tangem.domain.common.util.derivationStyleProvider
import com.tangem.domain.common.visa.VisaUtilities
import com.tangem.domain.common.visa.VisaWalletPublicKeyUtility
import com.tangem.domain.common.visa.VisaWalletPublicKeyUtility.findKeyWithoutDerivation
import com.tangem.domain.models.scan.CardDTO
import com.tangem.domain.visa.model.VisaActivationError
import com.tangem.domain.visa.model.VisaDataForApprove
import com.tangem.operations.ScanTask
import com.tangem.operations.derivation.DeriveWalletPublicKeyTask
import com.tangem.operations.sign.SignHashCommand
import com.tangem.operations.sign.SignHashResponse
class VisaCustomerWalletApproveTask(
private val visaDataForApprove: VisaDataForApprove,
) : CardSessionRunnable<SignHashResponse> {
override fun run(session: CardSession, callback: CompletionCallback<SignHashResponse>) {
val card = session.environment.card ?: run {
callback(CompletionResult.Failure(TangemSdkError.MissingPreflightRead()))
return
}
if (VisaUtilities.isVisaCard(card.firmwareVersion.doubleValue, card.batchId).not()) {
callback(CompletionResult.Failure(TangemSdkError.Underlying("Card is identified as not Visa")))
return
}
if (card.settings.isHDWalletAllowed) {
proceedApprove(card, session, callback)
} else {
proceedApproveWithLegacyCard(card, session, callback)
}
}
private fun proceedApprove(card: Card, session: CardSession, callback: CompletionCallback<SignHashResponse>) {
val cardDTO = CardDTO(card)
val derivationStyle = cardDTO.derivationStyleProvider.getDerivationStyle() ?: run {
proceedApproveWithLegacyCard(
card = card,
session = session,
callback = callback,
)
return
}
val derivationPath = VisaUtilities.visaDefaultDerivationPath(derivationStyle) ?: run {
callback(
CompletionResult.Failure(
TangemSdkError.Underlying("Failed to generate derivation path with provided derivation style"),
),
)
return
}
val wallet = card.wallets.firstOrNull { it.curve == VisaUtilities.mandatoryCurve } ?: run {
callback(CompletionResult.Failure(TangemSdkError.Underlying(VisaActivationError.MissingWallet.message)))
return
}
val derivationTask = DeriveWalletPublicKeyTask(
walletPublicKey = wallet.publicKey,
derivationPath = derivationPath,
)
derivationTask.run(session) { result ->
when (result) {
is CompletionResult.Success -> {
processDerivedKey(
wallet = wallet,
extendedPublicKey = result.data,
derivationPath = derivationPath,
session = session,
callback = callback,
)
}
is CompletionResult.Failure -> {
callback(CompletionResult.Failure(result.error))
}
}
}
}
private fun processDerivedKey(
wallet: CardWallet,
extendedPublicKey: ExtendedPublicKey,
derivationPath: DerivationPath,
session: CardSession,
callback: CompletionCallback<SignHashResponse>,
) {
val validationResult = VisaWalletPublicKeyUtility.validateExtendedPublicKey(
targetAddress = visaDataForApprove.targetAddress,
extendedPublicKey = extendedPublicKey,
)
validationResult.onLeft {
callback(CompletionResult.Failure(TangemSdkError.Underlying(it.message)))
return
}
signApproveData(
targetWalletPublicKey = wallet.publicKey,
derivationPath = derivationPath,
session = session,
callback = callback,
)
}
private fun proceedApproveWithLegacyCard(
card: Card,
session: CardSession,
callback: CompletionCallback<SignHashResponse>,
) {
val publicKey = findKeyWithoutDerivation(
targetAddress = visaDataForApprove.targetAddress,
card = CardDTO(card),
).getOrElse {
callback(CompletionResult.Failure(TangemSdkError.Underlying(it.message)))
return
}
signApproveData(
targetWalletPublicKey = publicKey,
derivationPath = null,
session = session,
callback = callback,
)
}
private fun signApproveData(
targetWalletPublicKey: ByteArray,
derivationPath: DerivationPath?,
session: CardSession,
callback: CompletionCallback<SignHashResponse>,
) {
val signTask = SignHashCommand(
hash = visaDataForApprove.approveHash.hexToBytes(),
walletPublicKey = targetWalletPublicKey,
derivationPath = derivationPath,
)
signTask.run(session) { result ->
when (result) {
is CompletionResult.Success -> {
scanCard(
signHashResponse = result.data,
session = session,
callback = callback,
)
}
is CompletionResult.Failure -> {
callback(CompletionResult.Failure(result.error))
}
}
}
}
private fun scanCard(
signHashResponse: SignHashResponse,
session: CardSession,
callback: CompletionCallback<SignHashResponse>,
) {
val scanTask = ScanTask()
scanTask.run(session) { result ->
when (result) {
is CompletionResult.Success -> {
callback(CompletionResult.Success(signHashResponse))
}
is CompletionResult.Failure -> {
callback(CompletionResult.Failure(result.error))
}
}
}
}
}

View file

@ -20,6 +20,7 @@ internal class DefaultVisaActivationRepository @AssistedInject constructor(
override suspend fun getActivationRemoteState(): VisaActivationRemoteState = withContext(dispatcherProvider.io) {
visaActivationStatusConverter.convert(visaApi.getRemoteActivationStatus(cardId))
VisaActivationRemoteState.CardWalletSignatureRequired // mock
// TODO implement refreshing access token if it's expired
}

View file

@ -1,6 +1,7 @@
package com.tangem.domain.common
import com.tangem.blockchain.common.derivation.DerivationStyle
import com.tangem.domain.common.TapWorkarounds.isWallet2
import com.tangem.domain.models.scan.CardDTO
interface DerivationStyleProvider {
@ -8,14 +9,13 @@ interface DerivationStyleProvider {
}
internal class TangemDerivationStyleProvider(
private val cardTypesResolver: CardTypesResolver,
private val card: CardDTO,
) : DerivationStyleProvider {
override fun getDerivationStyle(): DerivationStyle? {
return when {
!card.settings.isHDWalletAllowed -> null
firstBatchesOfWallet1(card) -> DerivationStyle.V1
cardTypesResolver.isWallet2() -> DerivationStyle.V3
card.isWallet2 -> DerivationStyle.V3
else -> DerivationStyle.V2
}
}

View file

@ -8,6 +8,7 @@ import com.tangem.common.card.WalletData
import com.tangem.domain.common.TapWorkarounds.getTangemNoteBlockchain
import com.tangem.domain.common.TapWorkarounds.isStart2Coin
import com.tangem.domain.common.TapWorkarounds.isTestCard
import com.tangem.domain.common.TapWorkarounds.isWallet2
import com.tangem.domain.common.visa.VisaUtilities
import com.tangem.domain.models.scan.CardDTO
import com.tangem.domain.models.scan.ProductType
@ -35,10 +36,7 @@ internal class TangemCardTypesResolver(
return walletData == null && card.firmwareVersion <= FirmwareVersion.HDWalletAvailable
}
override fun isWallet2(): Boolean {
return card.firmwareVersion >= FirmwareVersion.Ed25519Slip0010Available &&
card.settings.isKeysImportAllowed
}
override fun isWallet2(): Boolean = card.isWallet2
override fun isVisaWallet(): Boolean = productType == ProductType.Visa

View file

@ -23,6 +23,9 @@ object TapWorkarounds {
val CardDTO.isStart2Coin: Boolean
get() = isStart2CoinIssuer(issuer.name)
val CardDTO.isWallet2: Boolean
get() = firmwareVersion >= FirmwareVersion.Ed25519Slip0010Available && settings.isKeysImportAllowed
val CardDTO.isVisa: Boolean
get() = VisaUtilities.isVisaCard(this)

View file

@ -24,10 +24,10 @@ val ScanResponse.cardTypesResolver: CardTypesResolver
)
val ScanResponse.derivationStyleProvider: DerivationStyleProvider
get() = TangemDerivationStyleProvider(
cardTypesResolver,
card,
)
get() = card.derivationStyleProvider
val CardDTO.derivationStyleProvider: DerivationStyleProvider
get() = TangemDerivationStyleProvider(this)
val UserWallet.cardTypesResolver: CardTypesResolver
get() = scanResponse.cardTypesResolver

View file

@ -18,12 +18,14 @@ object VisaUtilities {
val visaDefaultDerivationPath = visaBlockchain.derivationPath(DerivationStyle.V3)
fun visaDefaultDerivationPath(style: DerivationStyle) = visaBlockchain.derivationPath(style)
fun isVisaCard(card: CardDTO): Boolean {
return isVisaCard(card.firmwareVersion.doubleValue, card.batchId)
}
fun isVisaCard(firmwareVersion: Double, batchId: String): Boolean {
return FirmwareVersion.visaRange.contains(firmwareVersion) &&
return firmwareVersion in FirmwareVersion.visaRange &&
batchId.startsWith(VISA_BATCH_START)
}
}

View file

@ -0,0 +1,63 @@
package com.tangem.domain.common.visa
import arrow.core.Either
import arrow.core.raise.catch
import arrow.core.raise.either
import com.tangem.blockchain.common.address.Address
import com.tangem.crypto.hdWallet.bip32.ExtendedPublicKey
import com.tangem.domain.models.scan.CardDTO
object VisaWalletPublicKeyUtility {
fun validateExtendedPublicKey(targetAddress: String, extendedPublicKey: ExtendedPublicKey): Either<Error, Unit> =
either {
validatePublicKey(
targetAddress = targetAddress,
publicKey = extendedPublicKey.publicKey,
).bind()
}
fun findKeyWithoutDerivation(targetAddress: String, card: CardDTO): Either<Error, ByteArray> = either {
val wallet = findWalletOnVisaCurve(card).bind()
validatePublicKey(
targetAddress = targetAddress,
publicKey = wallet.publicKey,
).bind()
wallet.publicKey
}
private fun findWalletOnVisaCurve(card: CardDTO): Either<Error, CardDTO.Wallet> = either {
card.wallets.firstOrNull { it.curve == VisaUtilities.mandatoryCurve } ?: raise(Error.MissingWalletOnTargetCurve)
}
private fun validatePublicKey(targetAddress: String, publicKey: ByteArray): Either<Error, Unit> = either {
val address = generateAddressOnVisaCurve(publicKey).bind()
if (address.value != targetAddress) {
raise(Error.AddressNotMatched)
}
}
private fun generateAddressOnVisaCurve(walletPublicKey: ByteArray): Either<Error, Address> = either {
val addresses = catch(
block = {
VisaUtilities.visaBlockchain.makeAddresses(
walletPublicKey = walletPublicKey,
pairPublicKey = null,
curve = VisaUtilities.mandatoryCurve,
)
},
catch = { raise(Error.FailedToCreateAddress) },
)
addresses.firstOrNull() ?: raise(Error.FailedToCreateAddress)
}
enum class Error(val message: String) {
AddressNotMatched("ValidationError: Address not matched"),
FailedToCreateAddress("ValidationError: Failed to create address"),
MissingWalletOnTargetCurve("ValidationError: Missing wallet on target curve"),
}
}

View file

@ -4,5 +4,6 @@ enum class VisaActivationError(val message: String) {
BlockedForActivation("Card is blocked for activation"),
InvalidActivationState("Invalid activation state"),
WrongCard("Wrong card tapped"),
MissingWallet("Missing wallet"),
MissingRootOTP("Missing root OTP"),
}

View file

@ -0,0 +1,10 @@
package com.tangem.domain.visa.model
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class VisaDataForApprove(
@SerialName("targetAddress") val targetAddress: String,
@SerialName("approveHash") val approveHash: String,
)

View file

@ -41,6 +41,7 @@ dependencies {
implementation(projects.domain.legacy)
implementation(projects.domain.settings)
implementation(projects.domain.onboarding)
implementation(projects.domain.visa)
/** Tangem libraries */
implementation(projects.libs.tangemSdkApi)

View file

@ -52,6 +52,11 @@ internal class DefaultOnboardingVisaComponent @AssistedInject constructor(
MutableSharedFlow<Unit>()
}
private val childParams = ChildParams(
onBack = ::onChildBack,
parentBackEvent = currentChildBackEventHandle,
)
private val stackNavigation = StackNavigation<OnboardingVisaRoute>()
private val childStack: Value<ChildStack<OnboardingVisaRoute, ComposableContentComponent>> =
@ -76,7 +81,7 @@ internal class DefaultOnboardingVisaComponent @AssistedInject constructor(
if (childStack.value.active.configuration is OnboardingVisaRoute.AccessCode) {
componentScope.launch { currentChildBackEventHandle.emit(Unit) }
} else {
stackNavigation.pop()
onChildBack()
}
}
}
@ -90,11 +95,7 @@ internal class DefaultOnboardingVisaComponent @AssistedInject constructor(
}
}
private val childParams = ChildParams(
onBack = ::onChildBack,
parentBackEvent = currentChildBackEventHandle,
)
@Suppress("LongMethod")
private fun createChild(
route: OnboardingVisaRoute,
factoryContext: AppComponentContext,
@ -112,10 +113,24 @@ internal class DefaultOnboardingVisaComponent @AssistedInject constructor(
appComponentContext = factoryContext,
params = OnboardingVisaAccessCodeComponent.Params(
childParams = childParams,
onDone = { stackNavigation.push(OnboardingVisaRoute.ChooseWallet) },
onDone = {
stackNavigation.push(
if (it.walletFound) {
OnboardingVisaRoute.TangemWalletApproveOption(
visaDataForApprove = it.visaDataForApprove,
allowNavigateBack = false,
)
} else {
OnboardingVisaRoute.ChooseWallet(it.visaDataForApprove)
},
)
},
),
config = OnboardingVisaAccessCodeComponent.Config(
scanResponse = params.scanResponse,
),
)
OnboardingVisaRoute.ChooseWallet -> OnboardingVisaChooseWalletComponent(
is OnboardingVisaRoute.ChooseWallet -> OnboardingVisaChooseWalletComponent(
appComponentContext = factoryContext,
params = OnboardingVisaChooseWalletComponent.Params(
childParams = childParams,
@ -123,9 +138,12 @@ internal class DefaultOnboardingVisaComponent @AssistedInject constructor(
stackNavigation.push(
when (event) {
OnboardingVisaChooseWalletComponent.Params.Event.TangemWallet ->
OnboardingVisaRoute.TangemWalletApproveOption
OnboardingVisaRoute.TangemWalletApproveOption(
visaDataForApprove = route.visaDataForApprove,
allowNavigateBack = true,
)
OnboardingVisaChooseWalletComponent.Params.Event.OtherWallet ->
OnboardingVisaRoute.OtherWalletApproveOption
OnboardingVisaRoute.OtherWalletApproveOption(route.visaDataForApprove)
},
)
},
@ -138,8 +156,11 @@ internal class DefaultOnboardingVisaComponent @AssistedInject constructor(
onDone = { stackNavigation.push(OnboardingVisaRoute.PinCode) },
),
)
OnboardingVisaRoute.OtherWalletApproveOption -> OnboardingVisaOtherWalletComponent(
is OnboardingVisaRoute.OtherWalletApproveOption -> OnboardingVisaOtherWalletComponent(
appComponentContext = factoryContext,
config = OnboardingVisaOtherWalletComponent.Config(
visaDataForApprove = route.visaDataForApprove,
),
params = OnboardingVisaOtherWalletComponent.Params(
childParams = childParams,
onDone = { stackNavigation.push(OnboardingVisaRoute.InProgress) },
@ -152,8 +173,11 @@ internal class DefaultOnboardingVisaComponent @AssistedInject constructor(
onDone = { params.onDone() },
),
)
OnboardingVisaRoute.TangemWalletApproveOption -> OnboardingVisaApproveComponent(
is OnboardingVisaRoute.TangemWalletApproveOption -> OnboardingVisaApproveComponent(
appComponentContext = factoryContext,
config = OnboardingVisaApproveComponent.Config(
visaDataForApprove = route.visaDataForApprove,
),
params = OnboardingVisaApproveComponent.Params(
childParams = childParams,
onDone = { stackNavigation.push(OnboardingVisaRoute.InProgress) },
@ -163,15 +187,24 @@ internal class DefaultOnboardingVisaComponent @AssistedInject constructor(
}
private fun onChildBack() {
when (childStack.value.active.configuration) {
if (childStack.value.backStack.size == 1) {
// TODO show dialog
return
}
when (val route = childStack.value.active.configuration) {
OnboardingVisaRoute.AccessCode,
OnboardingVisaRoute.TangemWalletApproveOption,
OnboardingVisaRoute.OtherWalletApproveOption,
is OnboardingVisaRoute.OtherWalletApproveOption,
-> stackNavigation.pop()
is OnboardingVisaRoute.TangemWalletApproveOption -> {
if (route.allowNavigateBack) {
stackNavigation.pop()
}
}
is OnboardingVisaRoute.Welcome,
OnboardingVisaRoute.ChooseWallet,
is OnboardingVisaRoute.ChooseWallet,
OnboardingVisaRoute.InProgress,
OnboardingVisaRoute.OtherWalletApproveOption,
OnboardingVisaRoute.PinCode,
-> {
}

View file

@ -9,6 +9,8 @@ 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
@ -16,10 +18,11 @@ import kotlinx.coroutines.launch
internal class OnboardingVisaAccessCodeComponent(
appComponentContext: AppComponentContext,
config: Config,
private val params: Params,
) : ComposableContentComponent, AppComponentContext by appComponentContext {
private val model: OnboardingVisaAccessCodeModel = getOrCreateModel()
private val model: OnboardingVisaAccessCodeModel = getOrCreateModel(config)
init {
componentScope.launch {
@ -31,7 +34,7 @@ internal class OnboardingVisaAccessCodeComponent(
}
}
componentScope.launch {
model.onDone.collect { params.onDone() }
model.onDone.collect { params.onDone(it) }
}
}
@ -46,8 +49,17 @@ internal class OnboardingVisaAccessCodeComponent(
OnboardingVisaAccessCode(state, modifier)
}
data class Config(
val scanResponse: ScanResponse,
)
data class Params(
val childParams: DefaultOnboardingVisaComponent.ChildParams,
val onDone: () -> Unit,
val onDone: (DoneEvent) -> Unit,
)
data class DoneEvent(
val visaDataForApprove: VisaDataForApprove,
val walletFound: Boolean,
)
}

View file

@ -2,8 +2,15 @@ package com.tangem.features.onboarding.v2.visa.impl.child.accesscode.model
import androidx.compose.runtime.Stable
import androidx.compose.ui.text.input.TextFieldValue
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.VisaCardActivationStatus
import com.tangem.domain.visa.model.VisaDataForApprove
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.sdk.api.TangemSdkManager
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
@ -14,15 +21,22 @@ import javax.inject.Inject
@Stable
@ComponentScoped
internal class OnboardingVisaAccessCodeModel @Inject constructor(
paramsContainer: ParamsContainer,
override val dispatchers: CoroutineDispatcherProvider,
@Suppress("UnusedPrivateMember")
private val tangemSdkManager: TangemSdkManager,
private val visaAuthRepository: VisaAuthRepository,
) : Model() {
private val params: OnboardingVisaAccessCodeComponent.Config = paramsContainer.require()
private val activationStatus =
params.scanResponse.visaCardActivationStatus as? VisaCardActivationStatus.NotStartedActivation
?: error("Visa activation status is not set or incorrect for this step")
private val _uiState = MutableStateFlow(getInitialState())
val uiState = _uiState.asStateFlow()
val onBack = MutableSharedFlow<Unit>()
val onDone = MutableSharedFlow<Unit>()
val onDone = MutableSharedFlow<OnboardingVisaAccessCodeComponent.DoneEvent>()
fun onBack() {
when (uiState.value.step) {
@ -97,27 +111,53 @@ internal class OnboardingVisaAccessCodeModel @Inject constructor(
@Suppress("UnusedPrivateMember")
private fun startActivationProcess(accessCode: String) {
modelScope.launch { onDone.emit(Unit) }
loading(true)
// modelScope.launch {
// @Suppress("UnusedPrivateMember")
// val result = tangemSdkManager.activateVisaCard(
// accessCode = accessCode,
// challengeToSign = null,
// activationInput = TODO(),
// )
//
// when (result) {
// is CompletionResult.Success -> {
// val response = result.data
// TODO()
// }
// is CompletionResult.Failure -> {
// // show alert
// TODO()
// }
// }
// }
modelScope.launch {
val challengeToSign = runCatching {
visaAuthRepository.getCardAuthChallenge(
cardId = activationStatus.activationInput.cardId,
cardPublicKey = activationStatus.activationInput.cardPublicKey.toHexString(),
)
}.getOrElse {
loading(false)
// show alert
return@launch
}
val result = tangemSdkManager.activateVisaCard(
accessCode = accessCode,
challengeToSign = challengeToSign,
activationInput = activationStatus.activationInput,
)
//
when (result) {
is CompletionResult.Success -> {
// TODO load approve data from backend
// TODO try to find wallet in the app
modelScope.launch {
onDone.emit(
OnboardingVisaAccessCodeComponent.DoneEvent(
visaDataForApprove = VisaDataForApprove(
targetAddress = "x9F65354e595284956599F2892fA4A4a87653D6E6",
approveHash = "approve hash",
),
walletFound = false, // TODO
),
)
}
}
is CompletionResult.Failure -> {
loading(false)
// show alert
}
}
}
}
private fun loading(state: Boolean) {
_uiState.update { it.copy(buttonLoading = state) }
}
private companion object {

View file

@ -65,6 +65,7 @@ internal fun OnboardingVisaAccessCode(state: OnboardingVisaAccessCodeUM, modifie
},
onClick = state.onContinue,
iconRes = R.drawable.ic_tangem_24,
showProgress = state.buttonLoading,
isIconVisible = state.step == OnboardingVisaAccessCodeUM.Step.ReEnter,
),
)

View file

@ -9,6 +9,7 @@ internal data class OnboardingVisaAccessCodeUM(
val codesNotMatchError: Boolean = false,
val atLeastMinCharsError: Boolean = false,
val accessCodeHidden: Boolean = true,
val buttonLoading: Boolean = false,
val onAccessCodeFirstChange: (TextFieldValue) -> Unit = {},
val onAccessCodeSecondChange: (TextFieldValue) -> Unit = {},
val onContinue: () -> Unit = {},

View file

@ -9,6 +9,7 @@ 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.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
@ -16,10 +17,11 @@ import kotlinx.coroutines.launch
internal class OnboardingVisaApproveComponent(
appComponentContext: AppComponentContext,
config: Config,
private val params: Params,
) : ComposableContentComponent, AppComponentContext by appComponentContext {
private val model: OnboardingVisaApproveModel = getOrCreateModel()
private val model: OnboardingVisaApproveModel = getOrCreateModel(config)
init {
componentScope.launch {
@ -39,6 +41,10 @@ internal class OnboardingVisaApproveComponent(
)
}
data class Config(
val visaDataForApprove: VisaDataForApprove,
)
data class Params(
val childParams: DefaultOnboardingVisaComponent.ChildParams,
val onDone: () -> Unit,

View file

@ -1,22 +1,30 @@
package com.tangem.features.onboarding.v2.visa.impl.child.approve.model
import androidx.compose.runtime.Stable
import com.tangem.common.CompletionResult
import com.tangem.core.decompose.di.ComponentScoped
import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
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.sdk.api.TangemSdkManager
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
@Stable
@ComponentScoped
internal class OnboardingVisaApproveModel @Inject constructor(
paramsContainer: ParamsContainer,
override val dispatchers: CoroutineDispatcherProvider,
private val tangemSdkManager: TangemSdkManager,
) : Model() {
private val params = paramsContainer.require<OnboardingVisaApproveComponent.Config>()
private val _uiState = MutableStateFlow(getInitialState())
val uiState = _uiState.asStateFlow()
@ -29,7 +37,29 @@ internal class OnboardingVisaApproveModel @Inject constructor(
}
private fun onApproveClick() {
// TODO
modelScope.launch { onDone.emit(Unit) }
loading(true)
modelScope.launch {
val result = tangemSdkManager.visaCustomerWalletApprove(
visaDataForApprove = params.visaDataForApprove,
)
when (result) {
is CompletionResult.Success -> {
// TODO make backend call
onDone.emit(Unit)
}
is CompletionResult.Failure -> {
loading(false)
// TODO show dialog
return@launch
}
}
}
}
private fun loading(state: Boolean) {
_uiState.update { it.copy(approveButtonLoading = state) }
}
}

View file

@ -68,6 +68,7 @@ internal fun OnboardingVisaApprove(state: OnboardingVisaApproveUM, modifier: Mod
textReference = TextReference.Str("Approve"),
iconRes = R.drawable.ic_tangem_24,
isIconVisible = true,
showProgress = state.approveButtonLoading,
onClick = state.onApproveClick,
),
)

View file

@ -1,5 +1,6 @@
package com.tangem.features.onboarding.v2.visa.impl.child.approve.ui.state
internal data class OnboardingVisaApproveUM(
val approveButtonLoading: Boolean = false,
val onApproveClick: () -> Unit = {},
)

View file

@ -9,6 +9,7 @@ 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.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
@ -17,10 +18,11 @@ import kotlinx.coroutines.flow.onEach
internal class OnboardingVisaOtherWalletComponent(
appComponentContext: AppComponentContext,
config: Config,
private val params: Params,
) : ComposableContentComponent, AppComponentContext by appComponentContext {
private val model: OnboardingVisaOtherWalletModel = getOrCreateModel()
private val model: OnboardingVisaOtherWalletModel = getOrCreateModel(config)
init {
model.onDone
@ -40,6 +42,10 @@ internal class OnboardingVisaOtherWalletComponent(
)
}
data class Config(
val visaDataForApprove: VisaDataForApprove,
)
data class Params(
val childParams: DefaultOnboardingVisaComponent.ChildParams,
val onDone: () -> Unit,

View file

@ -3,6 +3,8 @@ package com.tangem.features.onboarding.v2.visa.impl.child.otherwallet.model
import androidx.compose.runtime.Stable
import com.tangem.core.decompose.di.ComponentScoped
import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.features.onboarding.v2.visa.impl.child.otherwallet.OnboardingVisaOtherWalletComponent
import com.tangem.features.onboarding.v2.visa.impl.child.otherwallet.ui.state.OnboardingVisaOtherWalletUM
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.flow.MutableSharedFlow
@ -13,9 +15,12 @@ import javax.inject.Inject
@Stable
@ComponentScoped
internal class OnboardingVisaOtherWalletModel @Inject constructor(
paramsContainer: ParamsContainer,
override val dispatchers: CoroutineDispatcherProvider,
) : Model() {
@Suppress("UnusedPrivateMember")
private val config = paramsContainer.require<OnboardingVisaOtherWalletComponent.Config>()
private val _uiState = MutableStateFlow(getInitialState())
val uiState = _uiState.asStateFlow()

View file

@ -8,6 +8,7 @@ import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
@ -95,16 +96,18 @@ fun Artwork(modifier: Modifier = Modifier) {
Box(modifier) {
val circleColor = TangemTheme.colors.background.secondary
var cardHeightPx by remember { mutableIntStateOf(0) }
val circleRadiusPx = cardHeightPx / 2 + cardHeightPx / 8f
val circleSize = with(LocalDensity.current) { (circleRadiusPx * 2).toDp() }
Canvas(
modifier = Modifier
.align(Alignment.Center)
.size(254.dp)
.size(circleSize)
.fillMaxSize(),
) {
drawCircle(
color = circleColor,
radius = cardHeightPx / 2 + cardHeightPx / 8f,
radius = circleRadiusPx,
center = center,
)
}

View file

@ -1,5 +1,6 @@
package com.tangem.features.onboarding.v2.visa.impl.route
import com.tangem.domain.visa.model.VisaDataForApprove
import kotlinx.serialization.Serializable
@Serializable
@ -12,13 +13,16 @@ sealed class OnboardingVisaRoute {
data object AccessCode : OnboardingVisaRoute()
@Serializable
data object ChooseWallet : OnboardingVisaRoute()
data class ChooseWallet(val visaDataForApprove: VisaDataForApprove) : OnboardingVisaRoute()
@Serializable
data object TangemWalletApproveOption : OnboardingVisaRoute()
data class TangemWalletApproveOption(
val visaDataForApprove: VisaDataForApprove,
val allowNavigateBack: Boolean,
) : OnboardingVisaRoute()
@Serializable
data object OtherWalletApproveOption : OnboardingVisaRoute()
data class OtherWalletApproveOption(val visaDataForApprove: VisaDataForApprove) : OnboardingVisaRoute()
@Serializable
data object InProgress : OnboardingVisaRoute()

View file

@ -4,9 +4,9 @@ package com.tangem.features.onboarding.v2.visa.impl.route
internal fun OnboardingVisaRoute.stepNum() = when (this) {
is OnboardingVisaRoute.Welcome -> 1
OnboardingVisaRoute.AccessCode -> 2
OnboardingVisaRoute.ChooseWallet -> 3
OnboardingVisaRoute.OtherWalletApproveOption -> 4
OnboardingVisaRoute.TangemWalletApproveOption -> 4
is OnboardingVisaRoute.ChooseWallet -> 3
is OnboardingVisaRoute.OtherWalletApproveOption -> 4
is OnboardingVisaRoute.TangemWalletApproveOption -> 4
OnboardingVisaRoute.InProgress -> 5
OnboardingVisaRoute.PinCode -> 6
}

View file

@ -5,10 +5,10 @@ import com.tangem.core.ui.extensions.TextReference
// TODO add translations [REDACTED_TASK_KEY]
internal fun OnboardingVisaRoute.screenTitle(): TextReference = when (this) {
OnboardingVisaRoute.AccessCode -> TextReference.Str("Access code")
OnboardingVisaRoute.ChooseWallet -> TextReference.Str("Account activation")
is OnboardingVisaRoute.ChooseWallet -> TextReference.Str("Account activation")
OnboardingVisaRoute.InProgress -> TextReference.Str("In progress")
OnboardingVisaRoute.OtherWalletApproveOption -> TextReference.Str("Wallet connection")
is OnboardingVisaRoute.OtherWalletApproveOption -> TextReference.Str("Wallet connection")
OnboardingVisaRoute.PinCode -> TextReference.Str("PIN code")
OnboardingVisaRoute.TangemWalletApproveOption -> TextReference.Str("Wallet connection")
is OnboardingVisaRoute.TangemWalletApproveOption -> TextReference.Str("Wallet connection")
is OnboardingVisaRoute.Welcome -> TextReference.Str("Getting started")
}

View file

@ -0,0 +1,9 @@
package com.tangem.features.onboarding.v2.visa.impl.ui
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
@Composable
internal fun OnboardingVisa(modifier: Modifier = Modifier, childContent: @Composable (Modifier) -> Unit) {
childContent(modifier)
}

View file

@ -18,9 +18,11 @@ 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
@Suppress("TooManyFunctions")
@ -153,5 +155,7 @@ interface TangemSdkManager {
activationInput: VisaActivationInput,
): CompletionResult<VisaCardActivationResponse>
suspend fun visaCustomerWalletApprove(visaDataForApprove: VisaDataForApprove): CompletionResult<SignHashResponse>
// endregion
}