Updated on 2026-08-14
This commit is contained in:
parent
ecdb72e55c
commit
46a28ab22c
15 changed files with 188 additions and 15 deletions
|
|
@ -3,32 +3,37 @@ package com.tangem.tap.di.domain
|
|||
import com.tangem.domain.visa.GetVisaCurrencyUseCase
|
||||
import com.tangem.domain.visa.GetVisaTxDetailsUseCase
|
||||
import com.tangem.domain.visa.GetVisaTxHistoryUseCase
|
||||
import com.tangem.domain.visa.SetVisaPinCodeUseCase
|
||||
import com.tangem.domain.visa.repository.VisaActivationRepository
|
||||
import com.tangem.domain.visa.repository.VisaRepository
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.android.components.ViewModelComponent
|
||||
import dagger.hilt.android.scopes.ViewModelScoped
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
|
||||
@Module
|
||||
@InstallIn(ViewModelComponent::class)
|
||||
@InstallIn(SingletonComponent::class)
|
||||
internal object VisaDomainModule {
|
||||
|
||||
@Provides
|
||||
@ViewModelScoped
|
||||
fun provideVisaCurrencyUseCase(visaRepository: VisaRepository): GetVisaCurrencyUseCase {
|
||||
return GetVisaCurrencyUseCase(visaRepository)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@ViewModelScoped
|
||||
fun provideGetVisaTxHistoryUseCase(visaRepository: VisaRepository): GetVisaTxHistoryUseCase {
|
||||
return GetVisaTxHistoryUseCase(visaRepository)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@ViewModelScoped
|
||||
fun provideGetVisaTxDetailsUseCase(visaRepository: VisaRepository): GetVisaTxDetailsUseCase {
|
||||
return GetVisaTxDetailsUseCase(visaRepository)
|
||||
}
|
||||
|
||||
@Provides
|
||||
fun provideSetVisaPinCodeUseCase(
|
||||
visaActivationRepositoryFactory: VisaActivationRepository.Factory,
|
||||
): SetVisaPinCodeUseCase {
|
||||
return SetVisaPinCodeUseCase(visaActivationRepositoryFactory)
|
||||
}
|
||||
}
|
||||
|
|
@ -4,6 +4,7 @@ import com.tangem.datasource.api.common.response.ApiResponse
|
|||
import com.tangem.datasource.api.utils.ReadTimeout
|
||||
import com.tangem.datasource.api.visa.models.request.ActivationByCardWalletRequest
|
||||
import com.tangem.datasource.api.visa.models.request.ActivationByCustomerWalletRequest
|
||||
import com.tangem.datasource.api.visa.models.request.SetPinCodeRequest
|
||||
import com.tangem.datasource.api.visa.models.response.CardActivationRemoteStateResponse
|
||||
import com.tangem.datasource.api.visa.models.response.CardWalletDataToSignResponse
|
||||
import com.tangem.datasource.api.visa.models.response.CustomerWalletDataToSignResponse
|
||||
|
|
@ -64,4 +65,10 @@ interface TangemVisaApi {
|
|||
@Header("Authorization") authHeader: String,
|
||||
@Body body: ActivationByCustomerWalletRequest,
|
||||
): ApiResponse<Unit>
|
||||
|
||||
@POST("product_instance/issuer_activation")
|
||||
suspend fun setPinCode(
|
||||
@Header("Authorization") authHeader: String,
|
||||
@Body body: SetPinCodeRequest,
|
||||
): ApiResponse<Unit>
|
||||
}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
package com.tangem.datasource.api.visa.models.request
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class SetPinCodeRequest(
|
||||
@Json(name = "customer_id") val customerId: String,
|
||||
@Json(name = "activation_order_id") val activationOrderId: String,
|
||||
@Json(name = "product_instance_id") val productInstanceId: String,
|
||||
@Json(name = "data") val data: Data,
|
||||
) {
|
||||
data class Data(
|
||||
@Json(name = "session_key") val sessionKey: String,
|
||||
@Json(name = "iv") val iv: String,
|
||||
@Json(name = "encrypted_pin") val encryptedPin: String,
|
||||
)
|
||||
}
|
||||
|
|
@ -7,6 +7,7 @@ import com.tangem.datasource.api.common.response.getOrThrow
|
|||
import com.tangem.datasource.api.visa.TangemVisaApi
|
||||
import com.tangem.datasource.api.visa.models.request.ActivationByCardWalletRequest
|
||||
import com.tangem.datasource.api.visa.models.request.ActivationByCustomerWalletRequest
|
||||
import com.tangem.datasource.api.visa.models.request.SetPinCodeRequest
|
||||
import com.tangem.datasource.local.visa.VisaAuthTokenStorage
|
||||
import com.tangem.domain.visa.exception.RefreshTokenExpiredException
|
||||
import com.tangem.domain.visa.model.*
|
||||
|
|
@ -167,6 +168,30 @@ internal class DefaultVisaActivationRepository @AssistedInject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
override suspend fun sendPinCode(pinCode: VisaEncryptedPinCode) {
|
||||
withContext(dispatcherProvider.io) {
|
||||
request {
|
||||
val authTokens =
|
||||
checkNotNull(visaAuthTokenStorage.get(visaCardId.cardId)) { "Visa auth tokens are not stored" }
|
||||
val accessCodeData = accessCodeDataConverter.convert(authTokens)
|
||||
|
||||
visaApi.setPinCode(
|
||||
authHeader = authTokens.getAuthHeader(),
|
||||
body = SetPinCodeRequest(
|
||||
customerId = accessCodeData.customerId,
|
||||
productInstanceId = accessCodeData.productInstanceId,
|
||||
activationOrderId = pinCode.activationOrderId,
|
||||
data = SetPinCodeRequest.Data(
|
||||
sessionKey = pinCode.sessionId,
|
||||
iv = pinCode.iv,
|
||||
encryptedPin = pinCode.encryptedPin,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun <T : Any> request(requestBlock: suspend () -> T): T {
|
||||
return runCatching {
|
||||
requestBlock()
|
||||
|
|
|
|||
|
|
@ -43,6 +43,8 @@ class MockVisaActivationRepository @AssistedInject constructor(
|
|||
|
||||
override suspend fun approveByCustomerWallet(signedData: VisaSignedDataByCustomerWallet) {}
|
||||
|
||||
override suspend fun sendPinCode(pinCode: VisaEncryptedPinCode) {}
|
||||
|
||||
@AssistedFactory
|
||||
interface Factory : VisaActivationRepository.Factory {
|
||||
override fun create(cardId: VisaCardId): MockVisaActivationRepository
|
||||
|
|
|
|||
|
|
@ -0,0 +1,8 @@
|
|||
package com.tangem.domain.visa.model
|
||||
|
||||
data class VisaEncryptedPinCode(
|
||||
val activationOrderId: String,
|
||||
val sessionId: String,
|
||||
val iv: String,
|
||||
val encryptedPin: String,
|
||||
)
|
||||
|
|
@ -0,0 +1,75 @@
|
|||
@file:Suppress("MaximumLineLength")
|
||||
package com.tangem.domain.visa
|
||||
|
||||
import android.util.Base64
|
||||
import arrow.core.Either
|
||||
import com.tangem.domain.visa.model.VisaCardId
|
||||
import com.tangem.domain.visa.model.VisaEncryptedPinCode
|
||||
import com.tangem.domain.visa.repository.VisaActivationRepository
|
||||
import java.security.Key
|
||||
import java.security.KeyFactory
|
||||
import java.security.PublicKey
|
||||
import java.security.SecureRandom
|
||||
import java.security.spec.X509EncodedKeySpec
|
||||
import javax.crypto.Cipher
|
||||
import javax.crypto.KeyGenerator
|
||||
import javax.crypto.spec.IvParameterSpec
|
||||
import javax.crypto.spec.SecretKeySpec
|
||||
|
||||
private const val KEY_SIZE = 256
|
||||
|
||||
private const val RSA_PUB_KEY = "TODO" // TODO add RSA public key [REDACTED_TASK_KEY]
|
||||
|
||||
class SetVisaPinCodeUseCase(
|
||||
private val visaActivationRepositoryFactory: VisaActivationRepository.Factory,
|
||||
) {
|
||||
|
||||
suspend operator fun invoke(
|
||||
visaCardId: VisaCardId,
|
||||
activationOrderId: String,
|
||||
pinCode: String,
|
||||
): Either<Throwable, Unit> = Either.catch {
|
||||
val visaActivationRepository = visaActivationRepositoryFactory.create(visaCardId)
|
||||
|
||||
val sessionKey = generateSessionKey()
|
||||
val sessionId = getSessionId(RSA_PUB_KEY, sessionKey)
|
||||
|
||||
val secureRandom = SecureRandom()
|
||||
val iv = ByteArray(size = 16)
|
||||
secureRandom.nextBytes(iv)
|
||||
val ivParameterSpec = IvParameterSpec(iv)
|
||||
val ivPayload = Base64.encodeToString(iv, Base64.NO_WRAP)
|
||||
val aesKey = SecretKeySpec(sessionKey.encoded, 0, sessionKey.encoded.size, "AES")
|
||||
val aesCipher = Cipher.getInstance("AES/CBC/PKCS5PADDING")
|
||||
aesCipher.init(Cipher.ENCRYPT_MODE, aesKey, ivParameterSpec)
|
||||
val encryptedPin = Base64.encodeToString(aesCipher.doFinal(pinCode.toByteArray()), Base64.NO_WRAP)
|
||||
|
||||
visaActivationRepository.sendPinCode(
|
||||
VisaEncryptedPinCode(
|
||||
activationOrderId = activationOrderId,
|
||||
sessionId = sessionId,
|
||||
iv = ivPayload,
|
||||
encryptedPin = encryptedPin,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun generateSessionKey(): Key {
|
||||
val generator = KeyGenerator.getInstance("AES")
|
||||
generator.init(KEY_SIZE)
|
||||
return generator.generateKey()
|
||||
}
|
||||
|
||||
private fun getPublicKey(rsaPublicKey: String): PublicKey {
|
||||
val keyBytes = Base64.decode(rsaPublicKey, Base64.NO_WRAP)
|
||||
val spec = X509EncodedKeySpec(keyBytes)
|
||||
return KeyFactory.getInstance("RSA").generatePublic(spec)
|
||||
}
|
||||
|
||||
private fun getSessionId(rsaPublicKey: String, sessionKey: Key): String {
|
||||
val publicKey = getPublicKey(rsaPublicKey)
|
||||
val cipher = Cipher.getInstance("RSA")
|
||||
cipher.init(Cipher.ENCRYPT_MODE, publicKey)
|
||||
return Base64.encodeToString(cipher.doFinal(sessionKey.encoded), Base64.NO_WRAP)
|
||||
}
|
||||
}
|
||||
|
|
@ -18,6 +18,8 @@ interface VisaActivationRepository {
|
|||
|
||||
suspend fun approveByCustomerWallet(signedData: VisaSignedDataByCustomerWallet)
|
||||
|
||||
suspend fun sendPinCode(pinCode: VisaEncryptedPinCode)
|
||||
|
||||
interface Factory {
|
||||
fun create(cardId: VisaCardId): VisaActivationRepository
|
||||
}
|
||||
|
|
|
|||
|
|
@ -126,7 +126,13 @@ internal class DefaultOnboardingVisaComponent @AssistedInject constructor(
|
|||
),
|
||||
params = OnboardingVisaInProgressComponent.Params(
|
||||
childParams = childParams,
|
||||
onDone = { model.stackNavigation.push(OnboardingVisaRoute.PinCode) },
|
||||
onDone = {
|
||||
model.stackNavigation.push(
|
||||
OnboardingVisaRoute.PinCode(
|
||||
activationOrderId = "TODO", // TODO [REDACTED_TASK_KEY] [Visa] In progress screen after pin code
|
||||
),
|
||||
)
|
||||
},
|
||||
),
|
||||
)
|
||||
is OnboardingVisaRoute.OtherWalletApproveOption -> OnboardingVisaOtherWalletComponent(
|
||||
|
|
@ -137,13 +143,20 @@ internal class DefaultOnboardingVisaComponent @AssistedInject constructor(
|
|||
),
|
||||
params = OnboardingVisaOtherWalletComponent.Params(
|
||||
childParams = childParams,
|
||||
onDone = { model.stackNavigation.push(OnboardingVisaRoute.PinCode) },
|
||||
onDone = {
|
||||
model.stackNavigation.push(
|
||||
OnboardingVisaRoute.PinCode(
|
||||
activationOrderId = route.visaDataForApprove.dataToSign.request.orderId,
|
||||
),
|
||||
)
|
||||
},
|
||||
),
|
||||
)
|
||||
OnboardingVisaRoute.PinCode -> OnboardingVisaPinCodeComponent(
|
||||
is OnboardingVisaRoute.PinCode -> OnboardingVisaPinCodeComponent(
|
||||
appComponentContext = factoryContext,
|
||||
config = OnboardingVisaPinCodeComponent.Config(
|
||||
scanResponse = model.currentScanResponse.value,
|
||||
activationOrderId = route.activationOrderId,
|
||||
),
|
||||
params = OnboardingVisaPinCodeComponent.Params(
|
||||
childParams = childParams,
|
||||
|
|
|
|||
|
|
@ -46,6 +46,7 @@ internal class OnboardingVisaPinCodeComponent(
|
|||
|
||||
data class Config(
|
||||
val scanResponse: ScanResponse,
|
||||
val activationOrderId: String,
|
||||
)
|
||||
|
||||
data class Params(
|
||||
|
|
|
|||
|
|
@ -1,12 +1,15 @@
|
|||
package com.tangem.features.onboarding.v2.visa.impl.child.pincode.model
|
||||
|
||||
import androidx.compose.runtime.Stable
|
||||
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.datasource.local.visa.VisaAuthTokenStorage
|
||||
import com.tangem.domain.models.scan.ScanResponse
|
||||
import com.tangem.domain.visa.SetVisaPinCodeUseCase
|
||||
import com.tangem.domain.visa.model.VisaCardActivationStatus
|
||||
import com.tangem.domain.visa.model.VisaCardId
|
||||
import com.tangem.domain.wallets.builder.UserWalletBuilder
|
||||
import com.tangem.domain.wallets.legacy.UserWalletsListManager
|
||||
import com.tangem.domain.wallets.models.UserWallet
|
||||
|
|
@ -24,6 +27,7 @@ import javax.inject.Inject
|
|||
|
||||
@Stable
|
||||
@ComponentScoped
|
||||
@Suppress("LongParameterList")
|
||||
internal class OnboardingVisaPinCodeModel @Inject constructor(
|
||||
paramsContainer: ParamsContainer,
|
||||
override val dispatchers: CoroutineDispatcherProvider,
|
||||
|
|
@ -31,9 +35,14 @@ internal class OnboardingVisaPinCodeModel @Inject constructor(
|
|||
private val userWalletsListManager: UserWalletsListManager,
|
||||
private val authTokenStorage: VisaAuthTokenStorage,
|
||||
private val otpStorage: VisaAuthTokenStorage,
|
||||
private val setVisaPinCodeUseCase: SetVisaPinCodeUseCase,
|
||||
) : Model() {
|
||||
|
||||
private val params = paramsContainer.require<OnboardingVisaPinCodeComponent.Config>()
|
||||
private val visaCardId = VisaCardId(
|
||||
cardId = params.scanResponse.card.cardId,
|
||||
cardPublicKey = params.scanResponse.card.cardPublicKey.toHexString(),
|
||||
)
|
||||
private val _uiState = MutableStateFlow(getInitialState())
|
||||
|
||||
val uiState = _uiState.asStateFlow()
|
||||
|
|
@ -41,6 +50,7 @@ internal class OnboardingVisaPinCodeModel @Inject constructor(
|
|||
|
||||
private fun getInitialState(): OnboardingVisaPinCodeUM {
|
||||
return OnboardingVisaPinCodeUM(
|
||||
submitButtonEnabled = false,
|
||||
onPinCodeChange = ::onPinCodeChange,
|
||||
onSubmitClick = ::onSubmitClick,
|
||||
)
|
||||
|
|
@ -63,8 +73,15 @@ internal class OnboardingVisaPinCodeModel @Inject constructor(
|
|||
modelScope.launch {
|
||||
loading(true)
|
||||
|
||||
// TODO
|
||||
// make backend call
|
||||
setVisaPinCodeUseCase(
|
||||
pinCode = _uiState.value.pinCode,
|
||||
visaCardId = visaCardId,
|
||||
activationOrderId = params.activationOrderId,
|
||||
).onLeft {
|
||||
loading(false)
|
||||
return@launch
|
||||
}
|
||||
|
||||
saveWallet()
|
||||
|
||||
loading(false)
|
||||
|
|
|
|||
|
|
@ -119,7 +119,7 @@ internal class OnboardingVisaModel @Inject constructor(
|
|||
is OnboardingVisaRoute.Welcome,
|
||||
is OnboardingVisaRoute.ChooseWallet,
|
||||
OnboardingVisaRoute.InProgress,
|
||||
OnboardingVisaRoute.PinCode,
|
||||
is OnboardingVisaRoute.PinCode,
|
||||
-> {
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,5 +28,5 @@ sealed class OnboardingVisaRoute {
|
|||
data object InProgress : OnboardingVisaRoute()
|
||||
|
||||
@Serializable
|
||||
data object PinCode : OnboardingVisaRoute()
|
||||
data class PinCode(val activationOrderId: String) : OnboardingVisaRoute()
|
||||
}
|
||||
|
|
@ -8,7 +8,7 @@ internal fun OnboardingVisaRoute.stepNum() = when (this) {
|
|||
is OnboardingVisaRoute.OtherWalletApproveOption -> 4
|
||||
is OnboardingVisaRoute.TangemWalletApproveOption -> 4
|
||||
OnboardingVisaRoute.InProgress -> 5
|
||||
OnboardingVisaRoute.PinCode -> 6
|
||||
is OnboardingVisaRoute.PinCode -> 6
|
||||
}
|
||||
|
||||
internal const val ONBOARDING_VISA_STEPS_COUNT = 7
|
||||
|
|
@ -8,7 +8,7 @@ internal fun OnboardingVisaRoute.screenTitle(): TextReference = when (this) {
|
|||
is OnboardingVisaRoute.ChooseWallet -> TextReference.Str("Account activation")
|
||||
OnboardingVisaRoute.InProgress -> TextReference.Str("In progress")
|
||||
is OnboardingVisaRoute.OtherWalletApproveOption -> TextReference.Str("Wallet connection")
|
||||
OnboardingVisaRoute.PinCode -> TextReference.Str("PIN code")
|
||||
is OnboardingVisaRoute.PinCode -> TextReference.Str("PIN code")
|
||||
is OnboardingVisaRoute.TangemWalletApproveOption -> TextReference.Str("Wallet connection")
|
||||
is OnboardingVisaRoute.Welcome -> TextReference.Str("Getting started")
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue