Updated on 2026-08-14
This commit is contained in:
parent
cb23c1dea3
commit
538757a7ce
54 changed files with 622 additions and 488 deletions
|
|
@ -4,14 +4,12 @@ import arrow.core.Either
|
|||
import arrow.core.getOrElse
|
||||
import arrow.core.raise.catch
|
||||
import arrow.core.raise.either
|
||||
import com.tangem.blockchain.blockchains.ethereum.EthereumUtils
|
||||
import com.tangem.common.CompletionResult
|
||||
import com.tangem.common.card.EllipticCurve
|
||||
import com.tangem.common.core.CardSession
|
||||
import com.tangem.common.core.CardSessionRunnable
|
||||
import com.tangem.common.core.CompletionCallback
|
||||
import com.tangem.common.core.TangemError
|
||||
import com.tangem.common.core.TangemSdkError
|
||||
import com.tangem.common.core.*
|
||||
import com.tangem.common.extensions.hexToBytes
|
||||
import com.tangem.common.extensions.toDecompressedPublicKey
|
||||
import com.tangem.common.extensions.toHexString
|
||||
import com.tangem.common.map
|
||||
import com.tangem.common.timemeasure.RealtimeMonotonicTimeSource
|
||||
|
|
@ -46,7 +44,7 @@ import timber.log.Timber
|
|||
import kotlin.coroutines.resume
|
||||
import kotlin.time.measureTimedValue
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
@Suppress("LongParameterList", "LargeClass")
|
||||
class VisaCardActivationTask @AssistedInject constructor(
|
||||
@Assisted private val mode: VisaCardActivationTaskMode,
|
||||
@Assisted private val activationInput: VisaActivationInput,
|
||||
|
|
@ -97,7 +95,23 @@ class VisaCardActivationTask @AssistedInject constructor(
|
|||
context.signAuthorizationChallenge(mode.authorizationChallenge)
|
||||
}
|
||||
is VisaCardActivationTaskMode.SignOnly -> {
|
||||
context.signData(mode.dataToSignByCardWallet)
|
||||
val wallet =
|
||||
card.wallets.firstOrNull { it.curve == EllipticCurve.Secp256k1 }
|
||||
?: return CompletionResult.Failure(TangemSdkError.MissingPreflightRead())
|
||||
|
||||
val derivedPublicKey = when (val deriveKeyResult = context.deriveKey(wallet.publicKey)) {
|
||||
is CompletionResult.Failure -> {
|
||||
return CompletionResult.Failure(deriveKeyResult.error)
|
||||
}
|
||||
is CompletionResult.Success -> {
|
||||
deriveKeyResult.data
|
||||
}
|
||||
}
|
||||
|
||||
context.signData(
|
||||
mode.dataToSignByCardWallet,
|
||||
derivedPublicKey,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -138,9 +152,35 @@ class VisaCardActivationTask @AssistedInject constructor(
|
|||
private suspend fun SessionContext.processSignedAuthorizationChallenge(
|
||||
signedChallenge: VisaAuthSignedChallenge,
|
||||
): CompletionResult<VisaCardActivationResponse> {
|
||||
when (val createWalletResult = createWallet()) {
|
||||
is CompletionResult.Failure -> return CompletionResult.Failure(createWalletResult.error)
|
||||
is CompletionResult.Success -> {}
|
||||
}
|
||||
|
||||
val card =
|
||||
session.environment.card ?: return CompletionResult.Failure(TangemSdkError.MissingPreflightRead())
|
||||
|
||||
val wallet =
|
||||
card.wallets.firstOrNull { it.curve == EllipticCurve.Secp256k1 }
|
||||
?: return CompletionResult.Failure(TangemSdkError.MissingPreflightRead())
|
||||
|
||||
val derivedPublicKey = when (val deriveKeyResult = deriveKey(wallet.publicKey)) {
|
||||
is CompletionResult.Failure -> {
|
||||
return CompletionResult.Failure(deriveKeyResult.error)
|
||||
}
|
||||
is CompletionResult.Success -> {
|
||||
deriveKeyResult.data
|
||||
}
|
||||
}
|
||||
|
||||
val walletAddress = VisaWalletPublicKeyUtility.generateAddressOnSecp256k1(derivedPublicKey.publicKey)
|
||||
.getOrElse { return CompletionResult.Failure(it.tangemError) }
|
||||
.value
|
||||
|
||||
return coroutineScope {
|
||||
val dataToSignDeferred = async { getDataToSign(signedChallenge) }
|
||||
val otpTaskDeferred = async { createWallet() }
|
||||
val dataToSignDeferred =
|
||||
async { getDataToSign(signedChallenge = signedChallenge, cardWalletAddress = walletAddress) }
|
||||
val otpTaskDeferred = async { createOTP() }
|
||||
|
||||
val dataToSign = dataToSignDeferred.await()
|
||||
.getOrElse {
|
||||
|
|
@ -150,12 +190,16 @@ class VisaCardActivationTask @AssistedInject constructor(
|
|||
|
||||
otpTaskDeferred.await()
|
||||
|
||||
signData(dataToSign)
|
||||
signData(
|
||||
dataToSign = dataToSign,
|
||||
derivedPublicKey = derivedPublicKey,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun SessionContext.getDataToSign(
|
||||
signedChallenge: VisaAuthSignedChallenge,
|
||||
cardWalletAddress: String,
|
||||
): Either<TangemError, VisaDataToSignByCardWallet> = either {
|
||||
catch(
|
||||
block = {
|
||||
|
|
@ -168,7 +212,12 @@ class VisaCardActivationTask @AssistedInject constructor(
|
|||
raise(VisaActivationError.WrongRemoteState.tangemError)
|
||||
}
|
||||
|
||||
visaActivationRepository.getCardWalletAcceptanceData(remoteState.request)
|
||||
visaActivationRepository.getCardWalletAcceptanceData(
|
||||
VisaCardWalletDataToSignRequest(
|
||||
activationOrderInfo = remoteState.activationOrderInfo,
|
||||
cardWalletAddress = cardWalletAddress,
|
||||
),
|
||||
)
|
||||
},
|
||||
catch = {
|
||||
raise(VisaAuthorizationAPIError.tangemError)
|
||||
|
|
@ -182,7 +231,7 @@ class VisaCardActivationTask @AssistedInject constructor(
|
|||
val card = session.environment.card ?: return CompletionResult.Failure(TangemSdkError.MissingPreflightRead())
|
||||
|
||||
return if (card.wallets.any { it.curve == EllipticCurve.Secp256k1 }) {
|
||||
createOTP()
|
||||
CompletionResult.Success(Unit)
|
||||
} else {
|
||||
val createWalletTask = CreateWalletTask(EllipticCurve.Secp256k1)
|
||||
|
||||
|
|
@ -199,7 +248,7 @@ class VisaCardActivationTask @AssistedInject constructor(
|
|||
when (val result = timedResult.value) {
|
||||
is CompletionResult.Success -> {
|
||||
Timber.i("CreateWalletTask success")
|
||||
createOTP()
|
||||
CompletionResult.Success(Unit)
|
||||
}
|
||||
is CompletionResult.Failure -> {
|
||||
Timber.e("CreateWalletTask failure ${result.error}")
|
||||
|
|
@ -245,26 +294,15 @@ class VisaCardActivationTask @AssistedInject constructor(
|
|||
|
||||
private suspend fun SessionContext.signData(
|
||||
dataToSign: VisaDataToSignByCardWallet,
|
||||
derivedPublicKey: ExtendedPublicKey,
|
||||
): CompletionResult<VisaCardActivationResponse> {
|
||||
val card =
|
||||
session.environment.card ?: return CompletionResult.Failure(TangemSdkError.MissingPreflightRead())
|
||||
|
||||
val wallet =
|
||||
card.wallets.firstOrNull { it.curve == EllipticCurve.Secp256k1 }
|
||||
?: return CompletionResult.Failure(TangemSdkError.MissingPreflightRead())
|
||||
|
||||
val derivedPublicKey = when (val deriveKeyResult = deriveKey(wallet.publicKey)) {
|
||||
is CompletionResult.Failure -> {
|
||||
return CompletionResult.Failure(deriveKeyResult.error)
|
||||
}
|
||||
is CompletionResult.Success -> {
|
||||
deriveKeyResult.data
|
||||
}
|
||||
}
|
||||
|
||||
val walletAddress = VisaWalletPublicKeyUtility.generateAddressOnSecp256k1(derivedPublicKey.publicKey)
|
||||
.getOrElse { return CompletionResult.Failure(it.tangemError) }
|
||||
.value
|
||||
|
||||
val task = SignHashCommand(
|
||||
hash = dataToSign.hashToSign.hexToBytes(),
|
||||
walletPublicKey = wallet.publicKey,
|
||||
|
|
@ -286,8 +324,8 @@ class VisaCardActivationTask @AssistedInject constructor(
|
|||
Timber.i("SignHashCommand success")
|
||||
handleSignedData(
|
||||
dataToSign = dataToSign,
|
||||
walletAddress = walletAddress,
|
||||
response = result.data,
|
||||
derivedPublicKey = derivedPublicKey,
|
||||
)
|
||||
}
|
||||
is CompletionResult.Failure -> {
|
||||
|
|
@ -313,7 +351,7 @@ class VisaCardActivationTask @AssistedInject constructor(
|
|||
|
||||
private suspend fun SessionContext.handleSignedData(
|
||||
dataToSign: VisaDataToSignByCardWallet,
|
||||
walletAddress: String,
|
||||
derivedPublicKey: ExtendedPublicKey,
|
||||
response: SignHashResponse,
|
||||
): CompletionResult<VisaCardActivationResponse> {
|
||||
val otp = otpStorage.getOTP(cardId) ?: run {
|
||||
|
|
@ -321,11 +359,16 @@ class VisaCardActivationTask @AssistedInject constructor(
|
|||
otpStorage.getOTP(cardId) ?: return CompletionResult.Failure(VisaActivationError.MissingRootOTP.tangemError)
|
||||
}
|
||||
|
||||
val rsvSignature = EthereumUtils.prepareSignedMessageData(
|
||||
signedHash = response.signature,
|
||||
hashToSign = dataToSign.hashToSign.hexToBytes(),
|
||||
publicKey = derivedPublicKey.publicKey.toDecompressedPublicKey(),
|
||||
)
|
||||
|
||||
val signedActivationData = dataToSign.sign(
|
||||
cardWalletAddress = walletAddress,
|
||||
rootOTP = otp.rootOTP.toHexString(),
|
||||
otpCounter = otp.counter,
|
||||
signature = response.signature.toHexString(),
|
||||
signature = rsvSignature,
|
||||
)
|
||||
|
||||
return setupAccessCode().map {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package com.tangem.tap.domain.tasks.visa
|
||||
|
||||
import arrow.core.getOrElse
|
||||
import com.tangem.blockchain.blockchains.ethereum.EthereumUtils
|
||||
import com.tangem.common.CompletionResult
|
||||
import com.tangem.common.card.Card
|
||||
import com.tangem.common.card.CardWallet
|
||||
|
|
@ -10,7 +11,7 @@ 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.extensions.toDecompressedPublicKey
|
||||
import com.tangem.core.error.ext.tangemError
|
||||
import com.tangem.crypto.hdWallet.DerivationPath
|
||||
import com.tangem.crypto.hdWallet.bip32.ExtendedPublicKey
|
||||
|
|
@ -129,6 +130,7 @@ class VisaCustomerWalletApproveTask(
|
|||
targetWalletPublicKey = wallet.publicKey,
|
||||
derivationPath = derivationPath,
|
||||
session = session,
|
||||
extendedPublicKey = extendedPublicKey,
|
||||
callback = callback,
|
||||
)
|
||||
}
|
||||
|
|
@ -149,6 +151,7 @@ class VisaCustomerWalletApproveTask(
|
|||
signApproveData(
|
||||
targetWalletPublicKey = publicKey,
|
||||
derivationPath = null,
|
||||
extendedPublicKey = null,
|
||||
session = session,
|
||||
callback = callback,
|
||||
)
|
||||
|
|
@ -157,11 +160,14 @@ class VisaCustomerWalletApproveTask(
|
|||
private fun signApproveData(
|
||||
targetWalletPublicKey: ByteArray,
|
||||
derivationPath: DerivationPath?,
|
||||
extendedPublicKey: ExtendedPublicKey?,
|
||||
session: CardSession,
|
||||
callback: CompletionCallback<VisaSignedDataByCustomerWallet>,
|
||||
) {
|
||||
val hashToSign = visaDataForApprove.dataToSign.hashToSign.hexToBytes()
|
||||
|
||||
val signTask = SignHashCommand(
|
||||
hash = visaDataForApprove.dataToSign.hashToSign.hexToBytes(),
|
||||
hash = hashToSign,
|
||||
walletPublicKey = targetWalletPublicKey,
|
||||
derivationPath = derivationPath,
|
||||
)
|
||||
|
|
@ -169,11 +175,18 @@ class VisaCustomerWalletApproveTask(
|
|||
signTask.run(session) { result ->
|
||||
when (result) {
|
||||
is CompletionResult.Success -> {
|
||||
val rsvSignature = EthereumUtils.prepareSignedMessageData(
|
||||
signedHash = result.data.signature,
|
||||
hashToSign = hashToSign,
|
||||
publicKey = extendedPublicKey?.publicKey?.toDecompressedPublicKey()
|
||||
?: targetWalletPublicKey.toDecompressedPublicKey(),
|
||||
)
|
||||
|
||||
scanCard(
|
||||
session = session,
|
||||
callback = callback,
|
||||
signedData = visaDataForApprove.dataToSign.sign(
|
||||
signature = result.data.signature.toHexString(),
|
||||
signature = rsvSignature,
|
||||
customerWalletAddress = visaDataForApprove.targetAddress,
|
||||
),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -128,6 +128,8 @@ internal class VisaCardScanHandler @Inject constructor(
|
|||
|
||||
Timber.i("Requesting challenge for wallet authorization")
|
||||
val challengeResponse = runCatching {
|
||||
// TODO [REDACTED_TASK_KEY]
|
||||
error("sign and get specific error to switch to card_id flow")
|
||||
visaAuthRepository.getCardWalletAuthChallenge(cardWalletAddress = walletAddress.value)
|
||||
}.getOrElse {
|
||||
Timber.i(
|
||||
|
|
@ -180,6 +182,7 @@ internal class VisaCardScanHandler @Inject constructor(
|
|||
return CompletionResult.Success(VisaCardActivationStatus.Activated(authorizationTokensResponse))
|
||||
}
|
||||
|
||||
@Suppress("LongMethod")
|
||||
private suspend fun SessionContext.handleCardAuthorization(
|
||||
cardWalletAddress: String,
|
||||
): CompletionResult<VisaCardActivationStatus> {
|
||||
|
|
@ -232,11 +235,17 @@ internal class VisaCardScanHandler @Inject constructor(
|
|||
tokens = authorizationTokensResponse,
|
||||
)
|
||||
|
||||
val activationRemoteState = visaActivationRepository.getActivationRemoteState()
|
||||
val activationRemoteState = runCatching {
|
||||
visaActivationRepository.getActivationRemoteState()
|
||||
}.getOrElse {
|
||||
Timber.e("Failed to sign challenge with Card public key. Plain error: ${it.message}")
|
||||
return CompletionResult.Failure(VisaAuthorizationAPIError.tangemError)
|
||||
}
|
||||
|
||||
val error = when (activationRemoteState) {
|
||||
VisaActivationRemoteState.BlockedForActivation -> VisaActivationError.BlockedForActivation
|
||||
VisaActivationRemoteState.Activated -> VisaActivationError.InvalidActivationState
|
||||
VisaActivationRemoteState.Failed -> VisaActivationError.FailedRemoteState
|
||||
else -> null
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ internal class TangemVisa(
|
|||
|
||||
private fun createProdEnvironment(): ApiEnvironmentConfig = ApiEnvironmentConfig(
|
||||
environment = ApiEnvironment.PROD,
|
||||
baseUrl = "https://bff.tangem.com/",
|
||||
baseUrl = "[REDACTED_ENV_URL]",
|
||||
headers = createHeaders(),
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ internal class TangemVisaAuth(
|
|||
|
||||
private fun createStageEnvironment(): ApiEnvironmentConfig = ApiEnvironmentConfig(
|
||||
environment = ApiEnvironment.STAGE,
|
||||
baseUrl = "https://api-s.tangem.org/",
|
||||
baseUrl = "[REDACTED_ENV_URL]",
|
||||
headers = createHeaders(),
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,9 +1,11 @@
|
|||
package com.tangem.datasource.api.visa
|
||||
|
||||
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.ActivationStatusRequest
|
||||
import com.tangem.datasource.api.visa.models.request.GetCardWalletAcceptanceRequest
|
||||
import com.tangem.datasource.api.visa.models.request.GetCustomerWalletAcceptanceRequest
|
||||
import com.tangem.datasource.api.visa.models.request.SetPinCodeRequest
|
||||
import com.tangem.datasource.api.visa.models.response.*
|
||||
import retrofit2.http.Body
|
||||
|
|
@ -11,60 +13,40 @@ import retrofit2.http.GET
|
|||
import retrofit2.http.Header
|
||||
import retrofit2.http.POST
|
||||
import retrofit2.http.Query
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
interface TangemVisaApi {
|
||||
|
||||
@GET("product_instance/activation_status")
|
||||
@POST("v1/activation/status")
|
||||
suspend fun getRemoteActivationStatus(
|
||||
@Header("Authorization") authHeader: String,
|
||||
@Query("customer_id") customerId: String,
|
||||
@Query("product_instance_id") productInstanceId: String,
|
||||
@Query("card_id") cardId: String,
|
||||
@Query("card_public_key") cardPublicKey: String,
|
||||
@Body request: ActivationStatusRequest,
|
||||
): ApiResponse<CardActivationRemoteStateResponse>
|
||||
|
||||
@ReadTimeout(duration = 20, TimeUnit.MINUTES)
|
||||
@GET("product_instance/activation_status")
|
||||
suspend fun getRemoteActivationStatusLongPoll(
|
||||
@Header("Authorization") authHeader: String,
|
||||
@Query("customer_id") customerId: String,
|
||||
@Query("product_instance_id") productInstanceId: String,
|
||||
@Query("card_id") cardId: String,
|
||||
@Query("card_public_key") cardPublicKey: String,
|
||||
): ApiResponse<CardActivationRemoteStateResponse>
|
||||
|
||||
@GET("product_instance/card_wallet_acceptance")
|
||||
@POST("v1/activation/acceptance/message")
|
||||
suspend fun getCardWalletAcceptance(
|
||||
@Header("Authorization") authHeader: String,
|
||||
@Query("customer_id") customerId: String,
|
||||
@Query("product_instance_id") productInstanceId: String,
|
||||
@Query("activation_order") activationOrderId: String,
|
||||
@Query("customer_wallet_address") customerWalletAddress: String,
|
||||
): ApiResponse<CardWalletDataToSignResponse>
|
||||
@Body request: GetCardWalletAcceptanceRequest,
|
||||
): ApiResponse<VisaDataToSignResponse>
|
||||
|
||||
@GET("product_instance/customer_wallet_acceptance")
|
||||
@POST("v1/activation/acceptance/message")
|
||||
suspend fun getCustomerWalletAcceptance(
|
||||
@Header("Authorization") authHeader: String,
|
||||
@Query("customer_id") customerId: String,
|
||||
@Query("product_instance_id") productInstanceId: String,
|
||||
@Query("activation_order") activationOrderId: String,
|
||||
@Query("card_wallet_address") cardWalletAddress: String,
|
||||
): ApiResponse<CustomerWalletDataToSignResponse>
|
||||
@Body request: GetCustomerWalletAcceptanceRequest,
|
||||
): ApiResponse<VisaDataToSignResponse>
|
||||
|
||||
@POST("product_instance/activation_by_card_wallet")
|
||||
@POST("v1/activation/data")
|
||||
suspend fun activateByCardWallet(
|
||||
@Header("Authorization") authHeader: String,
|
||||
@Body body: ActivationByCardWalletRequest,
|
||||
): ApiResponse<Unit>
|
||||
|
||||
@POST("product_instance/activation_by_customer_wallet")
|
||||
@POST("v1/activation/data")
|
||||
suspend fun activateByCustomerWallet(
|
||||
@Header("Authorization") authHeader: String,
|
||||
@Body body: ActivationByCustomerWalletRequest,
|
||||
): ApiResponse<Unit>
|
||||
|
||||
@POST("product_instance/issuer_activation")
|
||||
@POST("v1/activation/pin")
|
||||
suspend fun setPinCode(
|
||||
@Header("Authorization") authHeader: String,
|
||||
@Body body: SetPinCodeRequest,
|
||||
|
|
|
|||
|
|
@ -1,32 +1,29 @@
|
|||
package com.tangem.datasource.api.visa
|
||||
|
||||
import com.tangem.datasource.api.common.response.ApiResponse
|
||||
import com.tangem.datasource.api.visa.models.request.*
|
||||
import com.tangem.datasource.api.visa.models.response.GenerateNonceResponse
|
||||
import com.tangem.datasource.api.visa.models.response.JWTResponse
|
||||
import retrofit2.http.Field
|
||||
import retrofit2.http.Body
|
||||
import retrofit2.http.POST
|
||||
import retrofit2.http.Query
|
||||
|
||||
interface TangemVisaAuthApi {
|
||||
|
||||
@POST("auth/card_wallet")
|
||||
suspend fun generateNonceByWalletAddress(
|
||||
@Query("card_wallet_address") cardWalletAddress: String,
|
||||
): GenerateNonceResponse
|
||||
@POST("v1/auth/challenge")
|
||||
suspend fun generateNonceByCardId(@Body request: GenerateNoneByCardIdRequest): GenerateNonceResponse
|
||||
|
||||
@POST("auth/card_id")
|
||||
suspend fun generateNonceByCard(
|
||||
@Query("card_id") cardId: String,
|
||||
@Query("card_public_key") cardPublicKey: String,
|
||||
): GenerateNonceResponse
|
||||
@POST("v1/auth/challenge")
|
||||
suspend fun generateNonceByCardWallet(@Body request: GenerateNoneByCardWalletRequest): GenerateNonceResponse
|
||||
|
||||
@POST("auth/get_token")
|
||||
suspend fun getAccessToken(
|
||||
@Query("session_id") sessionId: String,
|
||||
@Query("signature") signature: String,
|
||||
@Query("salt") salt: String?,
|
||||
): JWTResponse
|
||||
@POST("v1/auth/token")
|
||||
suspend fun getAccessTokenByCardId(@Body request: GetAccessTokenByCardIdRequest): JWTResponse
|
||||
|
||||
@POST("auth/refresh_token")
|
||||
suspend fun refreshAccessToken(@Field("refresh_token") refreshToken: String): ApiResponse<JWTResponse>
|
||||
@POST("v1/auth/token")
|
||||
suspend fun getAccessTokenByCardWallet(@Body request: GetAccessTokenByCardWalletRequest): JWTResponse
|
||||
|
||||
@POST("v1/auth/token/refresh")
|
||||
suspend fun refreshCardIdAccessToken(@Body request: RefreshTokenByCardIdRequest): ApiResponse<JWTResponse>
|
||||
|
||||
@POST("v1/auth/token/refresh")
|
||||
suspend fun refreshCardWalletAccessToken(@Body request: RefreshTokenByCardWalletRequest): ApiResponse<JWTResponse>
|
||||
}
|
||||
|
|
@ -5,22 +5,15 @@ import com.squareup.moshi.JsonClass
|
|||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class ActivationByCardWalletRequest(
|
||||
@Json(name = "customer_id") val customerId: String,
|
||||
@Json(name = "product_instance_id") val productInstanceId: String,
|
||||
@Json(name = "activation_order_id") val activationOrderId: String,
|
||||
@Json(name = "data") val data: Data,
|
||||
@Json(name = "order_id") val orderId: String,
|
||||
@Json(name = "card_wallet") val cardWallet: CardWallet,
|
||||
@Json(name = "deploy_acceptance_signature") val deployAcceptanceSignature: String,
|
||||
@Json(name = "otp") val otp: Otp,
|
||||
) {
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Data(
|
||||
@Json(name = "card_wallet") val cardWallet: CardWallet,
|
||||
@Json(name = "otp") val otp: Otp,
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class CardWallet(
|
||||
@Json(name = "address") val address: String,
|
||||
@Json(name = "card_wallet_confirmation") val cardWalletConfirmation: CardWalletConfirmation?,
|
||||
@Json(name = "deploy_acceptance_signature") val deployAcceptanceSignature: String,
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
|
|
|
|||
|
|
@ -5,19 +5,12 @@ import com.squareup.moshi.JsonClass
|
|||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class ActivationByCustomerWalletRequest(
|
||||
@Json(name = "customer_id") val customerId: String,
|
||||
@Json(name = "product_instance_id") val productInstanceId: String,
|
||||
@Json(name = "activation_order_id") val activationOrderId: String,
|
||||
@Json(name = "data") val data: Data,
|
||||
@Json(name = "order_id") val orderId: String,
|
||||
@Json(name = "customer_wallet") val customerWallet: CustomerWallet,
|
||||
) {
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Data(
|
||||
@Json(name = "customer_wallet") val customerWallet: CustomerWallet,
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class CustomerWallet(
|
||||
@Json(name = "address") val address: String,
|
||||
@Json(name = "deploy_acceptance_signature") val deployAcceptanceSignature: String,
|
||||
@Json(name = "address") val customerWalletAddress: String,
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
package com.tangem.datasource.api.visa.models.request
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class ActivationStatusRequest(
|
||||
@Json(name = "card_id") val cardId: String,
|
||||
@Json(name = "card_public_key") val cardPublicKey: String,
|
||||
)
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
package com.tangem.datasource.api.visa.models.request
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class GenerateNoneByCardIdRequest(
|
||||
@Json(name = "auth_type") val authType: String = "card_id",
|
||||
@Json(name = "card_id") val cardId: String,
|
||||
@Json(name = "card_public_key") val cardPublicKey: String,
|
||||
)
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
package com.tangem.datasource.api.visa.models.request
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class GenerateNoneByCardWalletRequest(
|
||||
@Json(name = "auth_type") val authType: String = "card_wallet",
|
||||
@Json(name = "card_wallet_address") val cardWalletAddress: String,
|
||||
)
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
package com.tangem.datasource.api.visa.models.request
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class GetAccessTokenByCardIdRequest(
|
||||
@Json(name = "auth_type") val authType: String = "card_id",
|
||||
@Json(name = "session_id") val sessionId: String,
|
||||
@Json(name = "signature") val signature: String,
|
||||
@Json(name = "salt") val salt: String,
|
||||
)
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
package com.tangem.datasource.api.visa.models.request
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class GetAccessTokenByCardWalletRequest(
|
||||
@Json(name = "auth_type") val authType: String = "card_wallet",
|
||||
@Json(name = "session_id") val sessionId: String,
|
||||
@Json(name = "signature") val signature: String,
|
||||
)
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
package com.tangem.datasource.api.visa.models.request
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class GetCardWalletAcceptanceRequest(
|
||||
@Json(name = "type") val type: String = "card_wallet",
|
||||
@Json(name = "customer_wallet_address") val customerWalletAddress: String,
|
||||
@Json(name = "card_wallet_address") val cardWalletAddress: String,
|
||||
)
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
package com.tangem.datasource.api.visa.models.request
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class GetCustomerWalletAcceptanceRequest(
|
||||
@Json(name = "type") val type: String = "customer_wallet",
|
||||
@Json(name = "customer_wallet_address") val customerWalletAddress: String,
|
||||
@Json(name = "card_wallet_address") val cardWalletAddress: String,
|
||||
)
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
package com.tangem.datasource.api.visa.models.request
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class RefreshTokenByCardIdRequest(
|
||||
@Json(name = "auth_type") val authType: String = "card_id",
|
||||
@Json(name = "refresh_token") val refreshToken: String,
|
||||
)
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
package com.tangem.datasource.api.visa.models.request
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class RefreshTokenByCardWalletRequest(
|
||||
@Json(name = "auth_type") val authType: String = "card_wallet",
|
||||
@Json(name = "refresh_token") val refreshToken: String,
|
||||
)
|
||||
|
|
@ -5,14 +5,8 @@ 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,
|
||||
)
|
||||
}
|
||||
@Json(name = "order_id") val orderId: String,
|
||||
@Json(name = "session_id") val sessionId: String,
|
||||
@Json(name = "iv") val iv: String,
|
||||
@Json(name = "pin") val pin: String,
|
||||
)
|
||||
|
|
@ -5,15 +5,21 @@ import com.squareup.moshi.JsonClass
|
|||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class CardActivationRemoteStateResponse(
|
||||
@Json(name = "activation_status") val status: String,
|
||||
@Json(name = "activation_order") val activationOrder: ActivationOrder?,
|
||||
@Json(name = "stepChangeCode") val stepChangeCode: Int?,
|
||||
@Json(name = "updatedAt") val updatedAt: String?,
|
||||
@Json(name = "result") val result: Result,
|
||||
) {
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Result(
|
||||
@Json(name = "status") val status: String,
|
||||
@Json(name = "order") val activationOrder: ActivationOrder?,
|
||||
@Json(name = "stepChangeCode") val stepChangeCode: Int?,
|
||||
@Json(name = "updatedAt") val updatedAt: String?,
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class ActivationOrder(
|
||||
@Json(name = "id") val id: String,
|
||||
@Json(name = "customer_id") val customerId: String,
|
||||
@Json(name = "customer_wallet_address") val customerWalletAddress: String,
|
||||
@Json(name = "card_wallet_address") val cardWalletAddress: String,
|
||||
)
|
||||
}
|
||||
|
|
@ -1,14 +0,0 @@
|
|||
package com.tangem.datasource.api.visa.models.response
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class CustomerWalletDataToSignResponse(
|
||||
@Json(name = "data_for_customer_wallet") val dataForCardWallet: Data,
|
||||
) {
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Data(
|
||||
@Json(name = "hash") val hash: String,
|
||||
)
|
||||
}
|
||||
|
|
@ -5,6 +5,11 @@ import com.squareup.moshi.JsonClass
|
|||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class GenerateNonceResponse(
|
||||
@Json(name = "nonce") val nonce: String,
|
||||
@Json(name = "session_id") val sessionId: String,
|
||||
)
|
||||
@Json(name = "result") val result: Result,
|
||||
) {
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Result(
|
||||
@Json(name = "nonce") val nonce: String,
|
||||
@Json(name = "session_id") val sessionId: String,
|
||||
)
|
||||
}
|
||||
|
|
@ -5,12 +5,17 @@ import com.squareup.moshi.JsonClass
|
|||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class JWTResponse(
|
||||
@Json(name = "access_token") val accessToken: String,
|
||||
@Json(name = "expires_in") val expiresIn: Int,
|
||||
@Json(name = "refresh_expires_in") val refreshExpiresIn: Int,
|
||||
@Json(name = "refresh_token") val refreshToken: String,
|
||||
@Json(name = "token_type") val tokenType: String,
|
||||
@Json(name = "not-before-policy") val notBeforePolicy: Int,
|
||||
@Json(name = "session_state") val sessionState: String,
|
||||
@Json(name = "scope") val scope: String,
|
||||
)
|
||||
@Json(name = "result") val result: Result,
|
||||
) {
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Result(
|
||||
@Json(name = "access_token") val accessToken: String,
|
||||
@Json(name = "expires_in") val expiresIn: Int,
|
||||
@Json(name = "refresh_expires_in") val refreshExpiresIn: Int,
|
||||
@Json(name = "refresh_token") val refreshToken: String,
|
||||
@Json(name = "token_type") val tokenType: String,
|
||||
@Json(name = "not-before-policy") val notBeforePolicy: Int,
|
||||
@Json(name = "session_state") val sessionState: String,
|
||||
@Json(name = "scope") val scope: String,
|
||||
)
|
||||
}
|
||||
|
|
@ -4,11 +4,11 @@ import com.squareup.moshi.Json
|
|||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class CardWalletDataToSignResponse(
|
||||
@Json(name = "dataForCardWallet") val dataForCardWallet: Data,
|
||||
data class VisaDataToSignResponse(
|
||||
@Json(name = "result") val result: Result,
|
||||
) {
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Data(
|
||||
data class Result(
|
||||
@Json(name = "hash") val hash: String,
|
||||
)
|
||||
}
|
||||
|
|
@ -186,7 +186,7 @@ internal class ProdApiConfigsManagerTest(private val model: Model) {
|
|||
id = ApiConfig.ID.TangemVisaAuth,
|
||||
expected = ApiEnvironmentConfig(
|
||||
environment = ApiEnvironment.STAGE,
|
||||
baseUrl = "https://api-s.tangem.org/",
|
||||
baseUrl = "[REDACTED_ENV_URL]",
|
||||
headers = mapOf(
|
||||
"version" to ProviderSuspend { VERSION_NAME },
|
||||
"platform" to ProviderSuspend { "Android" },
|
||||
|
|
@ -200,7 +200,7 @@ internal class ProdApiConfigsManagerTest(private val model: Model) {
|
|||
id = ApiConfig.ID.TangemVisa,
|
||||
expected = ApiEnvironmentConfig(
|
||||
environment = ApiEnvironment.PROD,
|
||||
baseUrl = "https://bff.tangem.com/",
|
||||
baseUrl = "[REDACTED_ENV_URL]",
|
||||
headers = mapOf(
|
||||
"version" to ProviderSuspend { VERSION_NAME },
|
||||
"platform" to ProviderSuspend { "Android" },
|
||||
|
|
|
|||
|
|
@ -1,13 +1,15 @@
|
|||
package com.tangem.data.visa
|
||||
|
||||
import com.tangem.data.visa.config.VisaLibLoader
|
||||
import com.tangem.data.visa.converter.AccessCodeDataConverter
|
||||
import com.tangem.data.visa.converter.VisaActivationStatusConverter
|
||||
import com.tangem.data.visa.converter.VisaActivationStatusConverterWithState
|
||||
import com.tangem.datasource.api.common.response.ApiResponseError
|
||||
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.ActivationStatusRequest
|
||||
import com.tangem.datasource.api.visa.models.request.GetCardWalletAcceptanceRequest
|
||||
import com.tangem.datasource.api.visa.models.request.GetCustomerWalletAcceptanceRequest
|
||||
import com.tangem.datasource.api.visa.models.request.SetPinCodeRequest
|
||||
import com.tangem.datasource.local.visa.VisaAuthTokenStorage
|
||||
import com.tangem.domain.visa.exception.RefreshTokenExpiredException
|
||||
|
|
@ -25,9 +27,7 @@ internal class DefaultVisaActivationRepository @AssistedInject constructor(
|
|||
@Assisted private val visaCardId: VisaCardId,
|
||||
private val visaApi: TangemVisaApi,
|
||||
private val dispatcherProvider: CoroutineDispatcherProvider,
|
||||
private val visaActivationStatusConverter: VisaActivationStatusConverter,
|
||||
private val visaAuthTokenStorage: VisaAuthTokenStorage,
|
||||
private val accessCodeDataConverter: AccessCodeDataConverter,
|
||||
private val visaAuthRepository: VisaAuthRepository,
|
||||
private val visaLibLoader: VisaLibLoader,
|
||||
) : VisaActivationRepository {
|
||||
|
|
@ -36,59 +36,38 @@ internal class DefaultVisaActivationRepository @AssistedInject constructor(
|
|||
val result = request {
|
||||
val authTokens =
|
||||
checkNotNull(visaAuthTokenStorage.get(visaCardId.cardId)) { "Visa auth tokens are not stored" }
|
||||
val accessCodeData = accessCodeDataConverter.convert(authTokens)
|
||||
|
||||
visaApi.getRemoteActivationStatus(
|
||||
authHeader = authTokens.getAuthHeader(),
|
||||
customerId = accessCodeData.customerId,
|
||||
productInstanceId = accessCodeData.productInstanceId,
|
||||
cardId = visaCardId.cardId,
|
||||
cardPublicKey = visaCardId.cardPublicKey,
|
||||
request = ActivationStatusRequest(
|
||||
cardId = visaCardId.cardId,
|
||||
cardPublicKey = visaCardId.cardPublicKey,
|
||||
),
|
||||
).getOrThrow()
|
||||
}
|
||||
|
||||
visaActivationStatusConverter.convert(result)
|
||||
VisaActivationStatusConverterWithState.convert(result)
|
||||
}
|
||||
|
||||
override suspend fun getActivationRemoteStateLongPoll(): VisaActivationRemoteState =
|
||||
withContext(dispatcherProvider.io) {
|
||||
val result = request {
|
||||
val authTokens =
|
||||
checkNotNull(visaAuthTokenStorage.get(visaCardId.cardId)) { "Visa auth tokens are not stored" }
|
||||
val accessCodeData = accessCodeDataConverter.convert(authTokens)
|
||||
|
||||
visaApi.getRemoteActivationStatusLongPoll(
|
||||
authHeader = authTokens.getAuthHeader(),
|
||||
customerId = accessCodeData.customerId,
|
||||
productInstanceId = accessCodeData.productInstanceId,
|
||||
cardId = visaCardId.cardId,
|
||||
cardPublicKey = visaCardId.cardPublicKey,
|
||||
).getOrThrow()
|
||||
}
|
||||
|
||||
visaActivationStatusConverter.convert(result)
|
||||
}
|
||||
|
||||
override suspend fun getCardWalletAcceptanceData(
|
||||
request: VisaCardWalletDataToSignRequest,
|
||||
): VisaDataToSignByCardWallet = withContext(dispatcherProvider.io) {
|
||||
val result = request {
|
||||
val authTokens =
|
||||
checkNotNull(visaAuthTokenStorage.get(visaCardId.cardId)) { "Visa auth tokens are not stored" }
|
||||
val accessCodeData = accessCodeDataConverter.convert(authTokens)
|
||||
|
||||
visaApi.getCardWalletAcceptance(
|
||||
authHeader = authTokens.getAuthHeader(),
|
||||
customerId = accessCodeData.customerId,
|
||||
productInstanceId = accessCodeData.productInstanceId,
|
||||
activationOrderId = request.orderId,
|
||||
customerWalletAddress = request.customerWalletAddress,
|
||||
request = GetCardWalletAcceptanceRequest(
|
||||
customerWalletAddress = request.activationOrderInfo.customerWalletAddress,
|
||||
cardWalletAddress = request.cardWalletAddress,
|
||||
),
|
||||
).getOrThrow()
|
||||
}
|
||||
|
||||
VisaDataToSignByCardWallet(
|
||||
request = request,
|
||||
hashToSign = result.dataForCardWallet.hash,
|
||||
hashToSign = result.result.hash,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -98,20 +77,19 @@ internal class DefaultVisaActivationRepository @AssistedInject constructor(
|
|||
val result = request {
|
||||
val authTokens =
|
||||
checkNotNull(visaAuthTokenStorage.get(visaCardId.cardId)) { "Visa auth tokens are not stored" }
|
||||
val accessCodeData = accessCodeDataConverter.convert(authTokens)
|
||||
|
||||
visaApi.getCustomerWalletAcceptance(
|
||||
authHeader = authTokens.getAuthHeader(),
|
||||
customerId = accessCodeData.customerId,
|
||||
productInstanceId = accessCodeData.productInstanceId,
|
||||
activationOrderId = request.orderId,
|
||||
cardWalletAddress = request.cardWalletAddress,
|
||||
request = GetCustomerWalletAcceptanceRequest(
|
||||
cardWalletAddress = request.cardWalletAddress,
|
||||
customerWalletAddress = request.customerWalletAddress,
|
||||
),
|
||||
).getOrThrow()
|
||||
}
|
||||
|
||||
VisaDataToSignByCustomerWallet(
|
||||
request = request,
|
||||
hashToSign = result.dataForCardWallet.hash,
|
||||
hashToSign = result.result.hash,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -120,27 +98,22 @@ internal class DefaultVisaActivationRepository @AssistedInject constructor(
|
|||
request {
|
||||
val authTokens =
|
||||
checkNotNull(visaAuthTokenStorage.get(visaCardId.cardId)) { "Visa auth tokens are not stored" }
|
||||
val accessCodeData = accessCodeDataConverter.convert(authTokens)
|
||||
|
||||
visaApi.activateByCardWallet(
|
||||
authHeader = authTokens.getAuthHeader(),
|
||||
body = ActivationByCardWalletRequest(
|
||||
customerId = accessCodeData.customerId,
|
||||
productInstanceId = accessCodeData.productInstanceId,
|
||||
activationOrderId = signedData.dataToSign.request.orderId,
|
||||
data = ActivationByCardWalletRequest.Data(
|
||||
cardWallet = ActivationByCardWalletRequest.CardWallet(
|
||||
address = signedData.cardWalletAddress,
|
||||
cardWalletConfirmation = null, // for second iteration
|
||||
deployAcceptanceSignature = signedData.signature,
|
||||
),
|
||||
otp = ActivationByCardWalletRequest.Otp(
|
||||
rootOtp = signedData.rootOTP,
|
||||
counter = signedData.otpCounter,
|
||||
),
|
||||
orderId = signedData.dataToSign.request.activationOrderInfo.orderId,
|
||||
cardWallet = ActivationByCardWalletRequest.CardWallet(
|
||||
address = signedData.dataToSign.request.cardWalletAddress,
|
||||
cardWalletConfirmation = null, // for second iteration
|
||||
),
|
||||
deployAcceptanceSignature = signedData.signature,
|
||||
otp = ActivationByCardWalletRequest.Otp(
|
||||
rootOtp = signedData.rootOTP,
|
||||
counter = signedData.otpCounter,
|
||||
),
|
||||
),
|
||||
)
|
||||
).getOrThrow()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -150,22 +123,17 @@ internal class DefaultVisaActivationRepository @AssistedInject constructor(
|
|||
request {
|
||||
val authTokens =
|
||||
checkNotNull(visaAuthTokenStorage.get(visaCardId.cardId)) { "Visa auth tokens are not stored" }
|
||||
val accessCodeData = accessCodeDataConverter.convert(authTokens)
|
||||
|
||||
visaApi.activateByCustomerWallet(
|
||||
authHeader = authTokens.getAuthHeader(),
|
||||
body = ActivationByCustomerWalletRequest(
|
||||
customerId = accessCodeData.customerId,
|
||||
productInstanceId = accessCodeData.productInstanceId,
|
||||
activationOrderId = signedData.dataToSign.request.orderId,
|
||||
data = ActivationByCustomerWalletRequest.Data(
|
||||
customerWallet = ActivationByCustomerWalletRequest.CustomerWallet(
|
||||
address = signedData.customerWalletAddress,
|
||||
deployAcceptanceSignature = signedData.signature,
|
||||
),
|
||||
orderId = signedData.dataToSign.request.orderId,
|
||||
customerWallet = ActivationByCustomerWalletRequest.CustomerWallet(
|
||||
deployAcceptanceSignature = signedData.signature,
|
||||
customerWalletAddress = signedData.customerWalletAddress,
|
||||
),
|
||||
),
|
||||
)
|
||||
).getOrThrow()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -175,21 +143,16 @@ internal class DefaultVisaActivationRepository @AssistedInject constructor(
|
|||
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,
|
||||
),
|
||||
orderId = pinCode.activationOrderId,
|
||||
sessionId = pinCode.sessionId,
|
||||
iv = pinCode.iv,
|
||||
pin = pinCode.encryptedPin,
|
||||
),
|
||||
)
|
||||
).getOrThrow()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
package com.tangem.data.visa
|
||||
|
||||
import com.tangem.common.extensions.toHexString
|
||||
import com.tangem.crypto.CryptoUtils
|
||||
import com.tangem.datasource.api.common.response.getOrThrow
|
||||
import com.tangem.datasource.api.visa.TangemVisaAuthApi
|
||||
import com.tangem.datasource.api.visa.models.request.*
|
||||
import com.tangem.domain.visa.model.VisaAuthChallenge
|
||||
import com.tangem.domain.visa.model.VisaAuthSession
|
||||
import com.tangem.domain.visa.model.VisaAuthSignedChallenge
|
||||
|
|
@ -20,74 +20,80 @@ internal class DefaultVisaAuthRepository @Inject constructor(
|
|||
|
||||
override suspend fun getCardAuthChallenge(cardId: String, cardPublicKey: String): VisaAuthChallenge.Card =
|
||||
withContext(dispatchers.io) {
|
||||
// val response = visaAuthApi.generateNonceByCard(
|
||||
// cardId = cardId,
|
||||
// cardPublicKey = cardPublicKey,
|
||||
// )
|
||||
//
|
||||
// VisaAuthChallenge.Card(
|
||||
// challenge = response.nonce,
|
||||
// session = VisaAuthSession(response.sessionId),
|
||||
// )
|
||||
|
||||
val response = visaAuthApi.generateNonceByCardId(
|
||||
GenerateNoneByCardIdRequest(
|
||||
cardId = cardId,
|
||||
cardPublicKey = cardPublicKey,
|
||||
),
|
||||
)
|
||||
VisaAuthChallenge.Card(
|
||||
challenge = CryptoUtils.generateRandomBytes(length = 16).toHexString(),
|
||||
session = VisaAuthSession("session"),
|
||||
challenge = response.result.nonce,
|
||||
session = VisaAuthSession(response.result.sessionId),
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun getCardWalletAuthChallenge(cardWalletAddress: String): VisaAuthChallenge.Wallet =
|
||||
withContext(dispatchers.io) {
|
||||
// val response = visaAuthApi.generateNonceByWalletAddress(
|
||||
// customerId = cardId,
|
||||
// customerWalletAddress = walletPublicKey,
|
||||
// )
|
||||
//
|
||||
// VisaAuthChallenge.Wallet(
|
||||
// challenge = response.nonce,
|
||||
// session = VisaAuthSession(response.sessionId),
|
||||
// )
|
||||
val response = visaAuthApi.generateNonceByCardWallet(
|
||||
GenerateNoneByCardWalletRequest(
|
||||
cardWalletAddress = cardWalletAddress,
|
||||
),
|
||||
)
|
||||
VisaAuthChallenge.Wallet(
|
||||
challenge = CryptoUtils.generateRandomBytes(length = 32).toHexString(),
|
||||
session = VisaAuthSession("session"),
|
||||
challenge = response.result.nonce,
|
||||
session = VisaAuthSession(response.result.sessionId),
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun getAccessTokens(signedChallenge: VisaAuthSignedChallenge): VisaAuthTokens =
|
||||
withContext(dispatchers.io) {
|
||||
// val response = when (signedChallenge) {
|
||||
// is VisaAuthSignedChallenge.ByCardPublicKey -> {
|
||||
// visaAuthApi.getAccessToken(
|
||||
// sessionId = signedChallenge.challenge.session.sessionId,
|
||||
// signature = signedChallenge.signature,
|
||||
// salt = signedChallenge.salt,
|
||||
// )
|
||||
// }
|
||||
// is VisaAuthSignedChallenge.ByWallet -> {
|
||||
// visaAuthApi.getAccessToken(
|
||||
// sessionId = signedChallenge.challenge.session.sessionId,
|
||||
// signature = signedChallenge.signature,
|
||||
// salt = null,
|
||||
// )
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// VisaAuthTokens(
|
||||
// accessToken = response.accessToken,
|
||||
// refreshToken = response.refreshToken,
|
||||
// )
|
||||
val response = when (signedChallenge) {
|
||||
is VisaAuthSignedChallenge.ByCardPublicKey -> {
|
||||
visaAuthApi.getAccessTokenByCardId(
|
||||
GetAccessTokenByCardIdRequest(
|
||||
sessionId = signedChallenge.challenge.session.sessionId,
|
||||
signature = signedChallenge.signature,
|
||||
salt = signedChallenge.salt,
|
||||
),
|
||||
)
|
||||
}
|
||||
is VisaAuthSignedChallenge.ByWallet -> {
|
||||
visaAuthApi.getAccessTokenByCardWallet(
|
||||
GetAccessTokenByCardWalletRequest(
|
||||
sessionId = signedChallenge.challenge.session.sessionId,
|
||||
signature = signedChallenge.signature,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
VisaAuthTokens(
|
||||
accessToken = "accessToken",
|
||||
refreshToken = VisaAuthTokens.RefreshToken("refreshToken"),
|
||||
accessToken = response.result.accessToken,
|
||||
refreshToken = VisaAuthTokens.RefreshToken(
|
||||
value = response.result.refreshToken,
|
||||
authType = when (signedChallenge) {
|
||||
is VisaAuthSignedChallenge.ByCardPublicKey -> VisaAuthTokens.RefreshToken.Type.CardId
|
||||
is VisaAuthSignedChallenge.ByWallet -> VisaAuthTokens.RefreshToken.Type.CardWallet
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun refreshAccessTokens(refreshToken: VisaAuthTokens.RefreshToken): VisaAuthTokens =
|
||||
withContext(dispatchers.io) {
|
||||
// TODO
|
||||
val response = when (refreshToken.authType) {
|
||||
VisaAuthTokens.RefreshToken.Type.CardId ->
|
||||
visaAuthApi.refreshCardIdAccessToken(
|
||||
RefreshTokenByCardIdRequest(refreshToken = refreshToken.value),
|
||||
)
|
||||
VisaAuthTokens.RefreshToken.Type.CardWallet ->
|
||||
visaAuthApi.refreshCardIdAccessToken(
|
||||
RefreshTokenByCardIdRequest(refreshToken = refreshToken.value),
|
||||
)
|
||||
}.getOrThrow()
|
||||
|
||||
VisaAuthTokens(
|
||||
accessToken = "accessToken",
|
||||
refreshToken = VisaAuthTokens.RefreshToken("new refreshToken"),
|
||||
accessToken = response.result.accessToken,
|
||||
refreshToken = refreshToken.copy(value = response.result.refreshToken),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -15,18 +15,15 @@ class MockVisaActivationRepository @AssistedInject constructor(
|
|||
|
||||
override suspend fun getActivationRemoteState(): VisaActivationRemoteState {
|
||||
return VisaActivationRemoteState.CardWalletSignatureRequired(
|
||||
request = VisaCardWalletDataToSignRequest(
|
||||
activationOrderInfo = VisaActivationOrderInfo(
|
||||
orderId = "orderId",
|
||||
customerId = "customerId",
|
||||
customerWalletAddress = "customerWalletAddress",
|
||||
cardWalletAddress = null,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun getActivationRemoteStateLongPoll(): VisaActivationRemoteState {
|
||||
return VisaActivationRemoteState.PaymentAccountDeploying
|
||||
}
|
||||
|
||||
override suspend fun getCardWalletAcceptanceData(
|
||||
request: VisaCardWalletDataToSignRequest,
|
||||
): VisaDataToSignByCardWallet {
|
||||
|
|
|
|||
|
|
@ -1,60 +0,0 @@
|
|||
package com.tangem.data.visa.converter
|
||||
|
||||
import com.tangem.datasource.api.visa.models.response.CardActivationRemoteStateResponse
|
||||
import com.tangem.domain.visa.model.VisaActivationOrderInfo
|
||||
import com.tangem.domain.visa.model.VisaActivationRemoteState
|
||||
import com.tangem.utils.converter.Converter
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
class VisaActivationStatusConverter @Inject constructor() :
|
||||
Converter<CardActivationRemoteStateResponse, VisaActivationRemoteState> {
|
||||
|
||||
private val lastUpdatedAt = MutableStateFlow<String?>(null)
|
||||
|
||||
override fun convert(value: CardActivationRemoteStateResponse): VisaActivationRemoteState {
|
||||
// handle pin code error
|
||||
// either we entered pin code before with an error (WasError) or after receiving an error (InProgress)
|
||||
if (
|
||||
value.status == Status.AwaitingPin.stringValue &&
|
||||
value.stepChangeCode != null &&
|
||||
value.stepChangeCode == PIN_CODE_VALIDATION_ERROR
|
||||
) {
|
||||
return if (lastUpdatedAt.value != value.updatedAt) {
|
||||
lastUpdatedAt.value == value.updatedAt
|
||||
|
||||
VisaActivationRemoteState.AwaitingPinCode(
|
||||
activationOrderInfo = value.activationOrder!!.convert(),
|
||||
status = VisaActivationRemoteState.AwaitingPinCode.Status.WasError,
|
||||
)
|
||||
} else {
|
||||
VisaActivationRemoteState.AwaitingPinCode(
|
||||
activationOrderInfo = value.activationOrder!!.convert(),
|
||||
status = VisaActivationRemoteState.AwaitingPinCode.Status.InProgress,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// TODO Will be implemented in the future
|
||||
return VisaActivationRemoteState.Activated
|
||||
}
|
||||
|
||||
private fun CardActivationRemoteStateResponse.ActivationOrder.convert(): VisaActivationOrderInfo {
|
||||
return VisaActivationOrderInfo(
|
||||
orderId = id,
|
||||
customerId = customerId,
|
||||
customerWalletAddress = customerWalletAddress,
|
||||
)
|
||||
}
|
||||
|
||||
private enum class Status(val stringValue: String) {
|
||||
AwaitingPin("AWAITING_PIN"),
|
||||
// TODO complete statuses list when backend is ready
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val PIN_CODE_VALIDATION_ERROR = 1000
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,81 @@
|
|||
package com.tangem.data.visa.converter
|
||||
|
||||
import com.tangem.datasource.api.visa.models.response.CardActivationRemoteStateResponse
|
||||
import com.tangem.domain.visa.model.VisaActivationOrderInfo
|
||||
import com.tangem.domain.visa.model.VisaActivationRemoteState
|
||||
import com.tangem.utils.converter.Converter
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
|
||||
private const val PIN_CODE_VALIDATION_ERROR = 1000
|
||||
|
||||
object VisaActivationStatusConverterWithState :
|
||||
Converter<CardActivationRemoteStateResponse, VisaActivationRemoteState> {
|
||||
|
||||
private val lastUpdatedAt = MutableStateFlow<String?>(null)
|
||||
|
||||
override fun convert(value: CardActivationRemoteStateResponse): VisaActivationRemoteState {
|
||||
// handle pin code error
|
||||
// either we entered pin code before with an error (WasError) or after receiving an error (InProgress)
|
||||
// TODO [REDACTED_TASK_KEY]
|
||||
val result = value.result
|
||||
if (result.status == Status.PinCodeRequired.stringValue && result.stepChangeCode == PIN_CODE_VALIDATION_ERROR) {
|
||||
return if (lastUpdatedAt.value != result.updatedAt) {
|
||||
lastUpdatedAt.value = result.updatedAt
|
||||
|
||||
VisaActivationRemoteState.AwaitingPinCode(
|
||||
activationOrderInfo = result.activationOrder!!.convert(),
|
||||
status = VisaActivationRemoteState.AwaitingPinCode.Status.WasError,
|
||||
)
|
||||
} else {
|
||||
VisaActivationRemoteState.AwaitingPinCode(
|
||||
activationOrderInfo = result.activationOrder!!.convert(),
|
||||
status = VisaActivationRemoteState.AwaitingPinCode.Status.InProgress,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
val status = Status.entries.find { it.stringValue == result.status } ?: error(
|
||||
"Unknown status: ${result.status}",
|
||||
)
|
||||
|
||||
return when (status) {
|
||||
Status.Activated -> VisaActivationRemoteState.Activated
|
||||
Status.Failed -> VisaActivationRemoteState.Failed
|
||||
Status.BlockedForActivation -> VisaActivationRemoteState.BlockedForActivation
|
||||
Status.CardWalletSignatureRequired ->
|
||||
VisaActivationRemoteState.CardWalletSignatureRequired(
|
||||
activationOrderInfo = result.activationOrder!!.convert(),
|
||||
)
|
||||
Status.CustomerWalletSignatureRequired ->
|
||||
VisaActivationRemoteState.CustomerWalletSignatureRequired(
|
||||
activationOrderInfo = result.activationOrder!!.convert(),
|
||||
)
|
||||
Status.PaymentAccountDeploying -> VisaActivationRemoteState.PaymentAccountDeploying
|
||||
Status.PinCodeRequired -> VisaActivationRemoteState.AwaitingPinCode(
|
||||
activationOrderInfo = result.activationOrder!!.convert(),
|
||||
status = VisaActivationRemoteState.AwaitingPinCode.Status.WaitingForPinCode,
|
||||
)
|
||||
Status.WaitingForActivation -> VisaActivationRemoteState.WaitingForActivationFinishing
|
||||
}
|
||||
}
|
||||
|
||||
private fun CardActivationRemoteStateResponse.ActivationOrder.convert(): VisaActivationOrderInfo {
|
||||
return VisaActivationOrderInfo(
|
||||
orderId = id,
|
||||
customerId = customerId,
|
||||
customerWalletAddress = customerWalletAddress,
|
||||
cardWalletAddress = cardWalletAddress.takeIf { it.isNotBlank() },
|
||||
)
|
||||
}
|
||||
|
||||
private enum class Status(val stringValue: String) {
|
||||
Activated("activated"),
|
||||
Failed("failed"),
|
||||
BlockedForActivation("blocked_for_activation"),
|
||||
CardWalletSignatureRequired("card_wallet_signature_required"),
|
||||
CustomerWalletSignatureRequired("customer_wallet_signature_required"),
|
||||
PaymentAccountDeploying("payment_account_deploying"),
|
||||
PinCodeRequired("pin_code_required"),
|
||||
WaitingForActivation("waiting_for_activation"),
|
||||
}
|
||||
}
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
package com.tangem.data.visa.di
|
||||
|
||||
import com.tangem.data.visa.DefaultVisaActivationRepository
|
||||
import com.tangem.data.visa.DefaultVisaAuthRepository
|
||||
import com.tangem.data.visa.MockVisaRepository
|
||||
import com.tangem.data.visa.MockVisaActivationRepository
|
||||
import com.tangem.domain.visa.repository.VisaActivationRepository
|
||||
import com.tangem.domain.visa.repository.VisaAuthRepository
|
||||
import com.tangem.domain.visa.repository.VisaRepository
|
||||
|
|
@ -20,19 +20,19 @@ internal interface VisaDataModule {
|
|||
@Singleton
|
||||
fun bindVisaAuthRepository(repository: DefaultVisaAuthRepository): VisaAuthRepository
|
||||
|
||||
// @Binds
|
||||
// @Singleton
|
||||
// fun bindVisaActivationRepositoryFactory(
|
||||
// repository: DefaultVisaActivationRepository.Factory,
|
||||
// ): VisaActivationRepository.Factory
|
||||
|
||||
// Mocked
|
||||
@Binds
|
||||
@Singleton
|
||||
fun bindVisaActivationRepositoryFactory(
|
||||
repository: MockVisaActivationRepository.Factory,
|
||||
repository: DefaultVisaActivationRepository.Factory,
|
||||
): VisaActivationRepository.Factory
|
||||
|
||||
// Mocked
|
||||
// @Binds
|
||||
// @Singleton
|
||||
// fun bindVisaActivationRepositoryFactory(
|
||||
// repository: MockVisaActivationRepository.Factory,
|
||||
// ): VisaActivationRepository.Factory
|
||||
|
||||
// @Binds
|
||||
// fun bindVisaRepository(repository: DefaultVisaRepository): VisaRepository
|
||||
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import com.tangem.datasource.api.common.response.ApiResponse
|
|||
import com.tangem.datasource.api.common.response.ApiResponseError
|
||||
import com.tangem.datasource.api.common.response.getOrThrow
|
||||
import com.tangem.datasource.api.visa.TangemVisaAuthApi
|
||||
import com.tangem.datasource.api.visa.models.request.RefreshTokenByCardWalletRequest
|
||||
import com.tangem.datasource.local.userwallet.UserWalletsStore
|
||||
import com.tangem.domain.common.util.cardTypesResolver
|
||||
import com.tangem.domain.visa.exception.RefreshTokenExpiredException
|
||||
|
|
@ -79,16 +80,18 @@ internal class VisaApiRequestMaker @Inject constructor(
|
|||
}
|
||||
|
||||
private suspend fun refreshAccessTokens(refreshToken: VisaAuthTokens.RefreshToken): VisaAuthTokens {
|
||||
val result = visaAuthApi.refreshAccessToken(refreshToken.value).getOrThrow()
|
||||
val result = visaAuthApi.refreshCardWalletAccessToken(
|
||||
RefreshTokenByCardWalletRequest(refreshToken = refreshToken.value),
|
||||
).getOrThrow()
|
||||
|
||||
return VisaAuthTokens(
|
||||
accessToken = result.accessToken,
|
||||
refreshToken = VisaAuthTokens.RefreshToken(result.refreshToken),
|
||||
accessToken = result.result.accessToken,
|
||||
refreshToken = refreshToken.copy(value = result.result.refreshToken),
|
||||
)
|
||||
}
|
||||
|
||||
@Throws
|
||||
private suspend fun getAuthTokens(userWalletId: UserWalletId): VisaAuthTokens {
|
||||
private fun getAuthTokens(userWalletId: UserWalletId): VisaAuthTokens {
|
||||
val userWallet = findVisaUserWallet(userWalletId)
|
||||
val status = userWallet.scanResponse.visaCardActivationStatus ?: error("Visa card activation status not found")
|
||||
|
||||
|
|
@ -99,7 +102,7 @@ internal class VisaApiRequestMaker @Inject constructor(
|
|||
return (status as? VisaCardActivationStatus.Activated)?.visaAuthTokens ?: error("Visa card is not activated")
|
||||
}
|
||||
|
||||
private suspend fun findVisaUserWallet(userWalletId: UserWalletId): UserWallet {
|
||||
private fun findVisaUserWallet(userWalletId: UserWalletId): UserWallet {
|
||||
val userWallet = requireNotNull(userWalletsStore.getSyncOrNull(userWalletId)) {
|
||||
"No user wallet found: $userWalletId"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -57,7 +57,7 @@ object VisaWalletPublicKeyUtility {
|
|||
either {
|
||||
val address = generateAddressOnSecp256k1(publicKey).bind()
|
||||
|
||||
if (address.value != targetAddress) {
|
||||
if (address.value.lowercase() != targetAddress.lowercase()) {
|
||||
raise(VisaActivationError.AddressNotMatched)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,6 +20,9 @@ dependencies {
|
|||
implementation(projects.domain.tokens.models)
|
||||
implementation(projects.domain.appCurrency.models)
|
||||
|
||||
/** Security */
|
||||
implementation(deps.spongecastle.core)
|
||||
|
||||
/** Libs - Other */
|
||||
implementation(deps.jodatime)
|
||||
implementation(deps.androidx.paging.runtime)
|
||||
|
|
|
|||
|
|
@ -44,6 +44,7 @@ enum class VisaActivationError(
|
|||
FailedToCreateAddress(104003007),
|
||||
AddressNotMatched(104003008),
|
||||
InconsistentRemoteState(104003009),
|
||||
FailedRemoteState(104003010),
|
||||
}
|
||||
|
||||
object VisaAuthorizationAPIError : UniversalError {
|
||||
|
|
|
|||
|
|
@ -10,4 +10,5 @@ data class VisaActivationOrderInfo(
|
|||
@Json(name = "orderId") val orderId: String,
|
||||
@Json(name = "customerId") val customerId: String,
|
||||
@Json(name = "customerWalletAddress") val customerWalletAddress: String,
|
||||
@Json(name = "cardWalletAddress") val cardWalletAddress: String?,
|
||||
)
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
package com.tangem.domain.visa.model
|
||||
|
||||
import com.squareup.moshi.*
|
||||
import com.tangem.domain.visa.model.VisaActivationRemoteState.*
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
|
|
@ -8,7 +9,7 @@ sealed class VisaActivationRemoteState {
|
|||
|
||||
@Serializable
|
||||
data class CardWalletSignatureRequired(
|
||||
val request: VisaCardWalletDataToSignRequest,
|
||||
val activationOrderInfo: VisaActivationOrderInfo,
|
||||
) : VisaActivationRemoteState()
|
||||
|
||||
@Serializable
|
||||
|
|
@ -38,6 +39,9 @@ sealed class VisaActivationRemoteState {
|
|||
@Serializable
|
||||
data object BlockedForActivation : VisaActivationRemoteState()
|
||||
|
||||
@Serializable
|
||||
data object Failed : VisaActivationRemoteState()
|
||||
|
||||
companion object {
|
||||
val jsonAdapter: VisaActivationRemoteState_JsonAdapter = VisaActivationRemoteState_JsonAdapter()
|
||||
}
|
||||
|
|
@ -46,7 +50,6 @@ sealed class VisaActivationRemoteState {
|
|||
@Suppress("ClassNaming")
|
||||
class VisaActivationRemoteState_Json(
|
||||
@Json(name = "type") val type: VisaActivationRemoteState_Type,
|
||||
@Json(name = "requestCardWallet") val requestCardWallet: VisaCardWalletDataToSignRequest? = null,
|
||||
@Json(name = "activationOrderInfo") val activationOrderInfo: VisaActivationOrderInfo? = null,
|
||||
@Json(name = "awaiting_pin_code_status") val awaitingPinCodeStatus: AwaitingPinCodeStatus_Type? = null,
|
||||
)
|
||||
|
|
@ -62,6 +65,7 @@ enum class VisaActivationRemoteState_Type {
|
|||
WaitingForActivationFinishing,
|
||||
Activated,
|
||||
BlockedForActivation,
|
||||
Failed,
|
||||
}
|
||||
|
||||
@Suppress("ClassNaming")
|
||||
|
|
@ -77,64 +81,66 @@ class VisaActivationRemoteState_JsonAdapter {
|
|||
fun fromJson(value: VisaActivationRemoteState_Json): VisaActivationRemoteState {
|
||||
return when (value.type) {
|
||||
VisaActivationRemoteState_Type.CardWalletSignatureRequired ->
|
||||
VisaActivationRemoteState.CardWalletSignatureRequired(value.requestCardWallet!!)
|
||||
CardWalletSignatureRequired(value.activationOrderInfo!!)
|
||||
VisaActivationRemoteState_Type.CustomerWalletSignatureRequired ->
|
||||
VisaActivationRemoteState.CustomerWalletSignatureRequired(value.activationOrderInfo!!)
|
||||
VisaActivationRemoteState_Type.PaymentAccountDeploying -> VisaActivationRemoteState.PaymentAccountDeploying
|
||||
CustomerWalletSignatureRequired(value.activationOrderInfo!!)
|
||||
VisaActivationRemoteState_Type.PaymentAccountDeploying -> PaymentAccountDeploying
|
||||
VisaActivationRemoteState_Type.WaitingPinCode ->
|
||||
VisaActivationRemoteState.AwaitingPinCode(
|
||||
AwaitingPinCode(
|
||||
activationOrderInfo = value.activationOrderInfo!!,
|
||||
status = when (value.awaitingPinCodeStatus!!) {
|
||||
AwaitingPinCodeStatus_Type.WaitingForPinCode ->
|
||||
VisaActivationRemoteState.AwaitingPinCode.Status.WaitingForPinCode
|
||||
AwaitingPinCode.Status.WaitingForPinCode
|
||||
AwaitingPinCodeStatus_Type.InProgress ->
|
||||
VisaActivationRemoteState.AwaitingPinCode.Status.InProgress
|
||||
AwaitingPinCode.Status.InProgress
|
||||
AwaitingPinCodeStatus_Type.WasError ->
|
||||
VisaActivationRemoteState.AwaitingPinCode.Status.WasError
|
||||
AwaitingPinCode.Status.WasError
|
||||
},
|
||||
)
|
||||
VisaActivationRemoteState_Type.WaitingForActivationFinishing ->
|
||||
VisaActivationRemoteState.WaitingForActivationFinishing
|
||||
VisaActivationRemoteState_Type.Activated -> VisaActivationRemoteState.Activated
|
||||
VisaActivationRemoteState_Type.BlockedForActivation -> VisaActivationRemoteState.BlockedForActivation
|
||||
WaitingForActivationFinishing
|
||||
VisaActivationRemoteState_Type.Activated -> Activated
|
||||
VisaActivationRemoteState_Type.BlockedForActivation -> BlockedForActivation
|
||||
VisaActivationRemoteState_Type.Failed -> Failed
|
||||
}
|
||||
}
|
||||
|
||||
@ToJson
|
||||
fun toJson(value: VisaActivationRemoteState): VisaActivationRemoteState_Json {
|
||||
return when (value) {
|
||||
is VisaActivationRemoteState.CardWalletSignatureRequired ->
|
||||
is CardWalletSignatureRequired ->
|
||||
VisaActivationRemoteState_Json(
|
||||
type = VisaActivationRemoteState_Type.CardWalletSignatureRequired,
|
||||
requestCardWallet = value.request,
|
||||
activationOrderInfo = value.activationOrderInfo,
|
||||
)
|
||||
is VisaActivationRemoteState.CustomerWalletSignatureRequired ->
|
||||
is CustomerWalletSignatureRequired ->
|
||||
VisaActivationRemoteState_Json(
|
||||
type = VisaActivationRemoteState_Type.CustomerWalletSignatureRequired,
|
||||
activationOrderInfo = value.activationOrderInfo,
|
||||
)
|
||||
is VisaActivationRemoteState.PaymentAccountDeploying ->
|
||||
is PaymentAccountDeploying ->
|
||||
VisaActivationRemoteState_Json(VisaActivationRemoteState_Type.PaymentAccountDeploying)
|
||||
is VisaActivationRemoteState.AwaitingPinCode ->
|
||||
is AwaitingPinCode ->
|
||||
VisaActivationRemoteState_Json(
|
||||
VisaActivationRemoteState_Type.WaitingPinCode,
|
||||
activationOrderInfo = value.activationOrderInfo,
|
||||
awaitingPinCodeStatus = when (value.status) {
|
||||
VisaActivationRemoteState.AwaitingPinCode.Status.WaitingForPinCode ->
|
||||
AwaitingPinCode.Status.WaitingForPinCode ->
|
||||
AwaitingPinCodeStatus_Type.WaitingForPinCode
|
||||
VisaActivationRemoteState.AwaitingPinCode.Status.InProgress ->
|
||||
AwaitingPinCode.Status.InProgress ->
|
||||
AwaitingPinCodeStatus_Type.InProgress
|
||||
VisaActivationRemoteState.AwaitingPinCode.Status.WasError ->
|
||||
AwaitingPinCode.Status.WasError ->
|
||||
AwaitingPinCodeStatus_Type.WasError
|
||||
},
|
||||
)
|
||||
is VisaActivationRemoteState.WaitingForActivationFinishing ->
|
||||
is WaitingForActivationFinishing ->
|
||||
VisaActivationRemoteState_Json(VisaActivationRemoteState_Type.WaitingForActivationFinishing)
|
||||
is VisaActivationRemoteState.Activated -> VisaActivationRemoteState_Json(
|
||||
is Activated -> VisaActivationRemoteState_Json(
|
||||
VisaActivationRemoteState_Type.Activated,
|
||||
)
|
||||
is VisaActivationRemoteState.BlockedForActivation ->
|
||||
is BlockedForActivation ->
|
||||
VisaActivationRemoteState_Json(VisaActivationRemoteState_Type.BlockedForActivation)
|
||||
is Failed -> VisaActivationRemoteState_Json(VisaActivationRemoteState_Type.Failed)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -12,8 +12,15 @@ data class VisaAuthTokens(
|
|||
) {
|
||||
|
||||
@Serializable
|
||||
@JvmInline
|
||||
value class RefreshToken(val value: String)
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class RefreshToken(
|
||||
@Json(name = "value") val value: String,
|
||||
@Json(name = "authType") val authType: Type,
|
||||
) {
|
||||
enum class Type {
|
||||
CardId, CardWallet,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun VisaAuthTokens.getAuthHeader(): String {
|
||||
|
|
|
|||
|
|
@ -1,3 +1,12 @@
|
|||
package com.tangem.domain.visa.model
|
||||
|
||||
typealias VisaCardWalletDataToSignRequest = VisaActivationOrderInfo
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class VisaCardWalletDataToSignRequest(
|
||||
@Json(name = "activationOrderInfo") val activationOrderInfo: VisaActivationOrderInfo,
|
||||
@Json(name = "cardWalletAddress") val cardWalletAddress: String,
|
||||
)
|
||||
|
|
@ -9,4 +9,5 @@ import kotlinx.serialization.Serializable
|
|||
data class VisaCustomerWalletDataToSignRequest(
|
||||
@Json(name = "orderId") val orderId: String,
|
||||
@Json(name = "card_wallet_address") val cardWalletAddress: String,
|
||||
@Json(name = "customer_wallet_address") val customerWalletAddress: String,
|
||||
)
|
||||
|
|
@ -8,10 +8,9 @@ data class VisaDataToSignByCardWallet(
|
|||
val hashToSign: String,
|
||||
)
|
||||
|
||||
fun VisaDataToSignByCardWallet.sign(cardWalletAddress: String, rootOTP: String, otpCounter: Int, signature: String) =
|
||||
fun VisaDataToSignByCardWallet.sign(rootOTP: String, otpCounter: Int, signature: String) =
|
||||
VisaSignedActivationDataByCardWallet(
|
||||
dataToSign = this,
|
||||
cardWalletAddress = cardWalletAddress,
|
||||
rootOTP = rootOTP,
|
||||
otpCounter = otpCounter,
|
||||
signature = signature,
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ package com.tangem.domain.visa.model
|
|||
|
||||
class VisaSignedActivationDataByCardWallet(
|
||||
val dataToSign: VisaDataToSignByCardWallet,
|
||||
val cardWalletAddress: String,
|
||||
val rootOTP: String,
|
||||
val otpCounter: Int,
|
||||
val signature: String,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
@file:Suppress("MaximumLineLength")
|
||||
|
||||
package com.tangem.domain.visa
|
||||
|
||||
import android.util.Base64
|
||||
|
|
@ -14,14 +15,12 @@ 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
|
||||
|
||||
class SetVisaPinCodeUseCase(
|
||||
private val visaActivationRepositoryFactory: VisaActivationRepository.Factory,
|
||||
) {
|
||||
class SetVisaPinCodeUseCase(private val visaActivationRepositoryFactory: VisaActivationRepository.Factory) {
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
suspend operator fun invoke(
|
||||
visaCardId: VisaCardId,
|
||||
activationOrderId: String,
|
||||
|
|
@ -29,19 +28,17 @@ class SetVisaPinCodeUseCase(
|
|||
): Either<Throwable, Unit> = Either.catch {
|
||||
val visaActivationRepository = visaActivationRepositoryFactory.create(visaCardId)
|
||||
val rsaPublicKey = visaActivationRepository.getPinCodeRsaEncryptionPublicKey()
|
||||
val formattedPin = "24$pinCode${"f".repeat(n = 8)}FF"
|
||||
|
||||
val sessionKey = generateSessionKey()
|
||||
val sessionId = getSessionId(rsaPublicKey, sessionKey)
|
||||
|
||||
val secureRandom = SecureRandom()
|
||||
val iv = ByteArray(size = 16)
|
||||
secureRandom.nextBytes(iv)
|
||||
val iv = generateIV()
|
||||
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)
|
||||
val aesCipher = Cipher.getInstance("AES/GCM/NoPadding")
|
||||
aesCipher.init(Cipher.ENCRYPT_MODE, sessionKey, ivParameterSpec)
|
||||
val encryptedPin = Base64.encodeToString(aesCipher.doFinal(formattedPin.encodeToByteArray()), Base64.NO_WRAP)
|
||||
|
||||
visaActivationRepository.sendPinCode(
|
||||
VisaEncryptedPinCode(
|
||||
|
|
@ -59,6 +56,12 @@ class SetVisaPinCodeUseCase(
|
|||
return generator.generateKey()
|
||||
}
|
||||
|
||||
private fun generateIV(): ByteArray {
|
||||
val iv = ByteArray(size = 16)
|
||||
SecureRandom().nextBytes(iv)
|
||||
return iv
|
||||
}
|
||||
|
||||
private fun getPublicKey(rsaPublicKey: String): PublicKey {
|
||||
val keyBytes = Base64.decode(rsaPublicKey, Base64.NO_WRAP)
|
||||
val spec = X509EncodedKeySpec(keyBytes)
|
||||
|
|
@ -67,8 +70,9 @@ class SetVisaPinCodeUseCase(
|
|||
|
||||
private fun getSessionId(rsaPublicKey: String, sessionKey: Key): String {
|
||||
val publicKey = getPublicKey(rsaPublicKey)
|
||||
val cipher = Cipher.getInstance("RSA")
|
||||
val cipher = Cipher.getInstance("RSA/ECB/OAEPWithSHA-1AndMGF1Padding")
|
||||
cipher.init(Cipher.ENCRYPT_MODE, publicKey)
|
||||
return Base64.encodeToString(cipher.doFinal(sessionKey.encoded), Base64.NO_WRAP)
|
||||
val base64SessionKey = Base64.encodeToString(sessionKey.encoded, Base64.NO_WRAP)
|
||||
return Base64.encodeToString(cipher.doFinal(base64SessionKey.encodeToByteArray()), Base64.NO_WRAP)
|
||||
}
|
||||
}
|
||||
|
|
@ -6,8 +6,6 @@ interface VisaActivationRepository {
|
|||
|
||||
suspend fun getActivationRemoteState(): VisaActivationRemoteState
|
||||
|
||||
suspend fun getActivationRemoteStateLongPoll(): VisaActivationRemoteState
|
||||
|
||||
suspend fun getCardWalletAcceptanceData(request: VisaCardWalletDataToSignRequest): VisaDataToSignByCardWallet
|
||||
|
||||
suspend fun getCustomerWalletAcceptanceData(
|
||||
|
|
|
|||
|
|
@ -167,7 +167,7 @@ internal class DefaultOnboardingVisaComponent @AssistedInject constructor(
|
|||
params = OnboardingVisaOtherWalletComponent.Params(
|
||||
childParams = childParams,
|
||||
onDone = {
|
||||
model.stackNavigation.pushNew(
|
||||
model.stackNavigation.replaceAll(
|
||||
OnboardingVisaRoute.PinCode(activationOrderInfo = it, pinCodeValidationError = false),
|
||||
)
|
||||
},
|
||||
|
|
@ -183,7 +183,7 @@ internal class DefaultOnboardingVisaComponent @AssistedInject constructor(
|
|||
params = OnboardingVisaPinCodeComponent.Params(
|
||||
childParams = childParams,
|
||||
onDone = {
|
||||
model.stackNavigation.pushNew(
|
||||
model.stackNavigation.replaceAll(
|
||||
OnboardingVisaRoute.InProgress(from = OnboardingVisaRoute.InProgress.From.PinCode),
|
||||
)
|
||||
},
|
||||
|
|
@ -199,7 +199,7 @@ internal class DefaultOnboardingVisaComponent @AssistedInject constructor(
|
|||
params = OnboardingVisaApproveComponent.Params(
|
||||
childParams = childParams,
|
||||
onDone = {
|
||||
model.stackNavigation.pushNew(
|
||||
model.stackNavigation.replaceAll(
|
||||
OnboardingVisaRoute.InProgress(from = OnboardingVisaRoute.InProgress.From.Approve),
|
||||
)
|
||||
},
|
||||
|
|
|
|||
|
|
@ -20,7 +20,6 @@ import com.tangem.domain.visa.repository.VisaActivationRepository
|
|||
import com.tangem.domain.visa.repository.VisaAuthRepository
|
||||
import com.tangem.features.onboarding.v2.visa.impl.child.accesscode.OnboardingVisaAccessCodeComponent
|
||||
import com.tangem.features.onboarding.v2.visa.impl.child.accesscode.ui.state.OnboardingVisaAccessCodeUM
|
||||
import com.tangem.features.onboarding.v2.visa.impl.child.welcome.model.analytics.ONBOARDING_SOURCE
|
||||
import com.tangem.features.onboarding.v2.visa.impl.child.welcome.model.analytics.OnboardingVisaAnalyticsEvent
|
||||
import com.tangem.features.onboarding.v2.visa.impl.child.welcome.model.analytics.VisaAnalyticsEvent
|
||||
import com.tangem.features.onboarding.v2.visa.impl.common.ActivationReadyEvent
|
||||
|
|
@ -55,9 +54,12 @@ internal class OnboardingVisaAccessCodeModel @Inject constructor(
|
|||
cardPublicKey = params.scanResponse.card.cardPublicKey.toHexString(),
|
||||
),
|
||||
)
|
||||
private val activationStatus =
|
||||
params.scanResponse.visaCardActivationStatus as? VisaCardActivationStatus.NotStartedActivation
|
||||
?: error("Visa activation status is not set or incorrect for this step")
|
||||
|
||||
private val activationInput = when (val status = params.scanResponse.visaCardActivationStatus) {
|
||||
is VisaCardActivationStatus.NotStartedActivation -> status.activationInput
|
||||
is VisaCardActivationStatus.ActivationStarted -> status.activationInput
|
||||
else -> error("Visa activation status is not set or incorrect for this step")
|
||||
}
|
||||
|
||||
private val _uiState = MutableStateFlow(getInitialState())
|
||||
|
||||
|
|
@ -153,8 +155,8 @@ internal class OnboardingVisaAccessCodeModel @Inject constructor(
|
|||
modelScope.launch {
|
||||
val challengeToSign = runCatching {
|
||||
visaAuthRepository.getCardAuthChallenge(
|
||||
cardId = activationStatus.activationInput.cardId,
|
||||
cardPublicKey = activationStatus.activationInput.cardPublicKey,
|
||||
cardId = activationInput.cardId,
|
||||
cardPublicKey = activationInput.cardPublicKey,
|
||||
)
|
||||
}.getOrElse {
|
||||
loading(false)
|
||||
|
|
@ -167,7 +169,7 @@ internal class OnboardingVisaAccessCodeModel @Inject constructor(
|
|||
accessCode = accessCode,
|
||||
authorizationChallenge = challengeToSign,
|
||||
),
|
||||
activationInput = activationStatus.activationInput,
|
||||
activationInput = activationInput,
|
||||
)
|
||||
|
||||
val resultData = when (result) {
|
||||
|
|
@ -175,7 +177,7 @@ internal class OnboardingVisaAccessCodeModel @Inject constructor(
|
|||
loading(false)
|
||||
uiMessageSender.showErrorDialog(result.error.universalError)
|
||||
analyticsEventsHandler.send(
|
||||
VisaAnalyticsEvent.Errors(result.error.code.toString(), ONBOARDING_SOURCE),
|
||||
VisaAnalyticsEvent.ErrorOnboarding(result.error.universalError),
|
||||
)
|
||||
return@launch
|
||||
}
|
||||
|
|
@ -190,14 +192,16 @@ internal class OnboardingVisaAccessCodeModel @Inject constructor(
|
|||
return@launch
|
||||
}
|
||||
|
||||
val targetAddress = result.data.signedActivationData.dataToSign.request.customerWalletAddress
|
||||
val targetAddress =
|
||||
result.data.signedActivationData.dataToSign.request.activationOrderInfo.customerWalletAddress
|
||||
|
||||
modelScope.launch {
|
||||
onDone.emit(
|
||||
ActivationReadyEvent(
|
||||
customerWalletDataToSignRequest = VisaCustomerWalletDataToSignRequest(
|
||||
orderId = result.data.signedActivationData.dataToSign.request.orderId,
|
||||
cardWalletAddress = result.data.signedActivationData.cardWalletAddress,
|
||||
orderId = result.data.signedActivationData.dataToSign.request.activationOrderInfo.orderId,
|
||||
cardWalletAddress = result.data.signedActivationData.dataToSign.request.cardWalletAddress,
|
||||
customerWalletAddress = targetAddress,
|
||||
),
|
||||
newScanResponse = params.scanResponse.copy(
|
||||
card = result.data.newCardDTO,
|
||||
|
|
|
|||
|
|
@ -16,7 +16,6 @@ import com.tangem.domain.visa.model.VisaDataForApprove
|
|||
import com.tangem.domain.visa.repository.VisaActivationRepository
|
||||
import com.tangem.features.onboarding.v2.visa.impl.child.approve.OnboardingVisaApproveComponent
|
||||
import com.tangem.features.onboarding.v2.visa.impl.child.approve.ui.state.OnboardingVisaApproveUM
|
||||
import com.tangem.features.onboarding.v2.visa.impl.child.welcome.model.analytics.ONBOARDING_SOURCE
|
||||
import com.tangem.features.onboarding.v2.visa.impl.child.welcome.model.analytics.OnboardingVisaAnalyticsEvent
|
||||
import com.tangem.features.onboarding.v2.visa.impl.child.welcome.model.analytics.VisaAnalyticsEvent
|
||||
import com.tangem.sdk.api.TangemSdkManager
|
||||
|
|
@ -88,9 +87,7 @@ internal class OnboardingVisaApproveModel @Inject constructor(
|
|||
is CompletionResult.Failure -> {
|
||||
loading(false)
|
||||
uiMessageSender.showErrorDialog(result.error.universalError)
|
||||
analyticsEventHandler.send(
|
||||
VisaAnalyticsEvent.Errors(result.error.code.toString(), ONBOARDING_SOURCE),
|
||||
)
|
||||
analyticsEventHandler.send(VisaAnalyticsEvent.ErrorOnboarding(result.error.universalError))
|
||||
return@launch
|
||||
}
|
||||
is CompletionResult.Success -> result.data
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ import com.tangem.domain.wallets.models.UserWallet
|
|||
import com.tangem.features.onboarding.v2.visa.impl.child.inprogress.OnboardingVisaInProgressComponent.Config
|
||||
import com.tangem.features.onboarding.v2.visa.impl.child.inprogress.OnboardingVisaInProgressComponent.Params
|
||||
import com.tangem.features.onboarding.v2.visa.impl.child.welcome.model.analytics.OnboardingVisaAnalyticsEvent
|
||||
import com.tangem.features.onboarding.v2.visa.impl.child.welcome.model.analytics.VisaAnalyticsEvent
|
||||
import com.tangem.features.onboarding.v2.visa.impl.route.OnboardingVisaRoute
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.delay
|
||||
|
|
@ -57,10 +58,14 @@ internal class OnboardingVisaInProgressModel @Inject constructor(
|
|||
|
||||
init {
|
||||
analyticsEventHandler.send(OnboardingVisaAnalyticsEvent.ActivationInProgressScreen)
|
||||
runShortPolling()
|
||||
}
|
||||
|
||||
private fun runShortPolling() {
|
||||
modelScope.launch {
|
||||
while (true) {
|
||||
val result = runCatching {
|
||||
visaActivationRepository.getActivationRemoteStateLongPoll()
|
||||
visaActivationRepository.getActivationRemoteState()
|
||||
}.getOrNull() ?: continue
|
||||
|
||||
when (result) {
|
||||
|
|
@ -68,6 +73,16 @@ internal class OnboardingVisaInProgressModel @Inject constructor(
|
|||
VisaActivationRemoteState.BlockedForActivation,
|
||||
-> {
|
||||
uiMessageSender.showErrorDialog(VisaActivationError.InconsistentRemoteState)
|
||||
analyticsEventHandler.send(
|
||||
VisaAnalyticsEvent.ErrorOnboarding(VisaActivationError.InconsistentRemoteState),
|
||||
)
|
||||
return@launch
|
||||
}
|
||||
VisaActivationRemoteState.Failed -> {
|
||||
uiMessageSender.showErrorDialog(VisaActivationError.FailedRemoteState)
|
||||
analyticsEventHandler.send(
|
||||
VisaAnalyticsEvent.ErrorOnboarding(VisaActivationError.FailedRemoteState),
|
||||
)
|
||||
return@launch
|
||||
}
|
||||
is VisaActivationRemoteState.CustomerWalletSignatureRequired,
|
||||
|
|
@ -86,14 +101,14 @@ internal class OnboardingVisaInProgressModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
delay(timeMillis = 1000)
|
||||
delay(timeMillis = 2000)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend inline fun navigateToPinCodeIfNeeded(
|
||||
remoteState: VisaActivationRemoteState.AwaitingPinCode,
|
||||
returnBlock: () -> Unit,
|
||||
complete: () -> Unit,
|
||||
) {
|
||||
when (remoteState.status) {
|
||||
VisaActivationRemoteState.AwaitingPinCode.Status.WaitingForPinCode -> {
|
||||
|
|
@ -105,7 +120,7 @@ internal class OnboardingVisaInProgressModel @Inject constructor(
|
|||
),
|
||||
),
|
||||
)
|
||||
returnBlock()
|
||||
complete()
|
||||
}
|
||||
VisaActivationRemoteState.AwaitingPinCode.Status.WasError -> {
|
||||
onDone.emit(
|
||||
|
|
@ -116,10 +131,10 @@ internal class OnboardingVisaInProgressModel @Inject constructor(
|
|||
),
|
||||
),
|
||||
)
|
||||
returnBlock()
|
||||
complete()
|
||||
}
|
||||
VisaActivationRemoteState.AwaitingPinCode.Status.InProgress -> {
|
||||
/** waiting for new state */
|
||||
/** waiting for the new state */
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -51,7 +51,7 @@ internal class OnboardingVisaOtherWalletModel @Inject constructor(
|
|||
modelScope.launch {
|
||||
while (true) {
|
||||
val result = runCatching {
|
||||
visaActivationRepository.getActivationRemoteStateLongPoll()
|
||||
visaActivationRepository.getActivationRemoteState()
|
||||
}.getOrNull()
|
||||
|
||||
if (result is VisaActivationRemoteState.AwaitingPinCode) {
|
||||
|
|
@ -59,7 +59,7 @@ internal class OnboardingVisaOtherWalletModel @Inject constructor(
|
|||
break
|
||||
}
|
||||
|
||||
delay(timeMillis = 1000)
|
||||
delay(timeMillis = 2000)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,15 +6,12 @@ import com.tangem.core.analytics.api.AnalyticsEventHandler
|
|||
import com.tangem.core.decompose.di.ModelScoped
|
||||
import com.tangem.core.decompose.model.Model
|
||||
import com.tangem.core.decompose.model.ParamsContainer
|
||||
import com.tangem.core.decompose.ui.UiMessageSender
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.datasource.local.visa.VisaAuthTokenStorage
|
||||
import com.tangem.domain.models.scan.ScanResponse
|
||||
import com.tangem.core.ui.utils.showErrorDialog
|
||||
import com.tangem.domain.visa.SetVisaPinCodeUseCase
|
||||
import com.tangem.domain.visa.model.VisaCardActivationStatus
|
||||
import com.tangem.domain.visa.error.VisaAuthorizationAPIError
|
||||
import com.tangem.domain.visa.model.VisaCardId
|
||||
import com.tangem.domain.wallets.builder.UserWalletBuilder
|
||||
import com.tangem.domain.wallets.legacy.UserWalletsListManager
|
||||
import com.tangem.domain.wallets.models.UserWallet
|
||||
import com.tangem.features.onboarding.v2.impl.R
|
||||
import com.tangem.features.onboarding.v2.visa.impl.child.pincode.OnboardingVisaPinCodeComponent
|
||||
import com.tangem.features.onboarding.v2.visa.impl.child.pincode.ui.state.OnboardingVisaPinCodeUM
|
||||
|
|
@ -25,7 +22,6 @@ import kotlinx.coroutines.flow.MutableStateFlow
|
|||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import javax.inject.Inject
|
||||
|
||||
@Stable
|
||||
|
|
@ -34,12 +30,9 @@ import javax.inject.Inject
|
|||
internal class OnboardingVisaPinCodeModel @Inject constructor(
|
||||
paramsContainer: ParamsContainer,
|
||||
override val dispatchers: CoroutineDispatcherProvider,
|
||||
private val userWalletBuilderFactory: UserWalletBuilder.Factory,
|
||||
private val userWalletsListManager: UserWalletsListManager,
|
||||
private val authTokenStorage: VisaAuthTokenStorage,
|
||||
private val otpStorage: VisaAuthTokenStorage,
|
||||
private val setVisaPinCodeUseCase: SetVisaPinCodeUseCase,
|
||||
private val analyticsEventHandler: AnalyticsEventHandler,
|
||||
private val uiMessageSender: UiMessageSender,
|
||||
) : Model() {
|
||||
|
||||
private val params = paramsContainer.require<OnboardingVisaPinCodeComponent.Config>()
|
||||
|
|
@ -102,11 +95,11 @@ internal class OnboardingVisaPinCodeModel @Inject constructor(
|
|||
activationOrderId = params.activationOrderInfo.orderId,
|
||||
).onLeft {
|
||||
loading(false)
|
||||
uiMessageSender.showErrorDialog(VisaAuthorizationAPIError)
|
||||
return@launch
|
||||
}
|
||||
|
||||
saveWallet()
|
||||
|
||||
onDone.emit(Unit)
|
||||
loading(false)
|
||||
}
|
||||
}
|
||||
|
|
@ -114,26 +107,4 @@ internal class OnboardingVisaPinCodeModel @Inject constructor(
|
|||
private fun loading(state: Boolean) {
|
||||
_uiState.update { it.copy(submitButtonLoading = state) }
|
||||
}
|
||||
|
||||
private suspend fun saveWallet() {
|
||||
val userWallet = createUserWallet(params.scanResponse)
|
||||
userWalletsListManager.save(userWallet)
|
||||
authTokenStorage.remove(params.scanResponse.card.cardId)
|
||||
otpStorage.remove(params.scanResponse.card.cardId)
|
||||
onDone.emit(Unit)
|
||||
}
|
||||
|
||||
private suspend fun createUserWallet(scanResponse: ScanResponse): UserWallet = withContext(dispatchers.io) {
|
||||
val newActivationStatus = VisaCardActivationStatus.Activated(
|
||||
visaAuthTokens = authTokenStorage.get(scanResponse.card.cardId)
|
||||
?: error("Impossible state. Wrong feature implementation"),
|
||||
)
|
||||
|
||||
requireNotNull(
|
||||
value = userWalletBuilderFactory.create(
|
||||
scanResponse = scanResponse.copy(visaCardActivationStatus = newActivationStatus),
|
||||
).build(),
|
||||
lazyMessage = { "User wallet not created" },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -22,10 +22,11 @@ internal object PinCodeValidation {
|
|||
}
|
||||
|
||||
private fun validateNoRepeatedDigits(pinCode: String): Boolean {
|
||||
return pinCode.toSet().size == PIN_CODE_LENGTH
|
||||
return pinCode.toSet().size > 1
|
||||
}
|
||||
|
||||
private fun validateNoConsecutiveDigits(pinCode: String): Boolean {
|
||||
return pinCode.windowed(2).all { it[0] + 1 == it[1] }.not()
|
||||
return pinCode.zipWithNext().any { it.second != it.first + 1 } &&
|
||||
pinCode.zipWithNext().any { it.second != it.first - 1 }
|
||||
}
|
||||
}
|
||||
|
|
@ -15,7 +15,6 @@ import com.tangem.domain.visa.model.VisaCustomerWalletDataToSignRequest
|
|||
import com.tangem.domain.visa.repository.VisaActivationRepository
|
||||
import com.tangem.features.onboarding.v2.visa.impl.child.welcome.OnboardingVisaWelcomeComponent.Config
|
||||
import com.tangem.features.onboarding.v2.visa.impl.child.welcome.OnboardingVisaWelcomeComponent.DoneEvent
|
||||
import com.tangem.features.onboarding.v2.visa.impl.child.welcome.model.analytics.ONBOARDING_SOURCE
|
||||
import com.tangem.features.onboarding.v2.visa.impl.child.welcome.model.analytics.OnboardingVisaAnalyticsEvent
|
||||
import com.tangem.features.onboarding.v2.visa.impl.child.welcome.model.analytics.VisaAnalyticsEvent
|
||||
import com.tangem.features.onboarding.v2.visa.impl.child.welcome.ui.state.OnboardingVisaWelcomeUM
|
||||
|
|
@ -96,9 +95,7 @@ internal class OnboardingVisaWelcomeModel @Inject constructor(
|
|||
is CompletionResult.Failure -> {
|
||||
loading(false)
|
||||
uiMessageSender.showErrorDialog(result.error.universalError)
|
||||
analyticsEventsHandler.send(
|
||||
VisaAnalyticsEvent.Errors(result.error.code.toString(), ONBOARDING_SOURCE),
|
||||
)
|
||||
analyticsEventsHandler.send(VisaAnalyticsEvent.ErrorOnboarding(result.error.universalError))
|
||||
return@launch
|
||||
}
|
||||
is CompletionResult.Success -> result.data
|
||||
|
|
@ -112,15 +109,17 @@ internal class OnboardingVisaWelcomeModel @Inject constructor(
|
|||
return@launch
|
||||
}
|
||||
|
||||
val targetAddress = result.data.signedActivationData.dataToSign.request.customerWalletAddress
|
||||
val request = result.data.signedActivationData.dataToSign.request
|
||||
val targetAddress = request.activationOrderInfo.customerWalletAddress
|
||||
|
||||
modelScope.launch {
|
||||
onDone.emit(
|
||||
DoneEvent.WelcomeBackDone(
|
||||
ActivationReadyEvent(
|
||||
customerWalletDataToSignRequest = VisaCustomerWalletDataToSignRequest(
|
||||
orderId = result.data.signedActivationData.dataToSign.request.orderId,
|
||||
cardWalletAddress = result.data.signedActivationData.cardWalletAddress,
|
||||
orderId = request.activationOrderInfo.orderId,
|
||||
cardWalletAddress = request.cardWalletAddress,
|
||||
customerWalletAddress = targetAddress,
|
||||
),
|
||||
newScanResponse = params.scanResponse.copy(
|
||||
card = result.data.newCardDTO,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package com.tangem.features.onboarding.v2.visa.impl.child.welcome.model.analytics
|
||||
|
||||
import com.tangem.core.analytics.models.AnalyticsEvent
|
||||
import com.tangem.core.error.UniversalError
|
||||
|
||||
internal const val ONBOARDING_SOURCE = "Onboarding"
|
||||
internal const val MAIN_SOURCE = "Main"
|
||||
|
|
@ -10,14 +11,11 @@ internal sealed class VisaAnalyticsEvent(
|
|||
params: Map<String, String> = mapOf(),
|
||||
) : AnalyticsEvent("Visa", event, params) {
|
||||
|
||||
data class Errors(
|
||||
val errorCode: String,
|
||||
val source: String,
|
||||
) : VisaAnalyticsEvent(
|
||||
data class ErrorOnboarding(val error: UniversalError) : VisaAnalyticsEvent(
|
||||
event = "Errors",
|
||||
params = mapOf(
|
||||
"Error Code" to errorCode,
|
||||
"Source" to source,
|
||||
"Error Code" to error.errorCode.toString(),
|
||||
"Source" to ONBOARDING_SOURCE,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
@ -1,10 +1,10 @@
|
|||
package com.tangem.features.onboarding.v2.visa.impl.model
|
||||
|
||||
import androidx.compose.runtime.Stable
|
||||
import com.arkivanov.decompose.ExperimentalDecomposeApi
|
||||
import com.arkivanov.decompose.router.stack.StackNavigation
|
||||
import com.arkivanov.decompose.router.stack.pop
|
||||
import com.arkivanov.decompose.router.stack.pushNew
|
||||
import com.arkivanov.decompose.router.stack.replaceAll
|
||||
import com.tangem.core.decompose.di.ModelScoped
|
||||
import com.tangem.core.decompose.model.Model
|
||||
import com.tangem.core.decompose.model.ParamsContainer
|
||||
|
|
@ -12,6 +12,7 @@ import com.tangem.domain.common.visa.VisaUtilities
|
|||
import com.tangem.domain.common.visa.VisaWalletPublicKeyUtility
|
||||
import com.tangem.domain.visa.model.VisaActivationRemoteState
|
||||
import com.tangem.domain.visa.model.VisaCardActivationStatus
|
||||
import com.tangem.domain.visa.model.VisaCardWalletDataToSignRequest
|
||||
import com.tangem.domain.visa.model.VisaCustomerWalletDataToSignRequest
|
||||
import com.tangem.domain.wallets.usecase.GetWalletsUseCase
|
||||
import com.tangem.features.onboarding.v2.visa.api.OnboardingVisaComponent
|
||||
|
|
@ -83,14 +84,13 @@ internal class OnboardingVisaModel @Inject constructor(
|
|||
)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalDecomposeApi::class)
|
||||
fun navigateFromInProgress(event: OnboardingVisaInProgressComponent.Params.DoneEvent) {
|
||||
when (event) {
|
||||
OnboardingVisaInProgressComponent.Params.DoneEvent.Activated -> {
|
||||
modelScope.launch { onDone.emit(Unit) }
|
||||
}
|
||||
is OnboardingVisaInProgressComponent.Params.DoneEvent.NavigateTo -> {
|
||||
stackNavigation.pushNew(event.route)
|
||||
stackNavigation.replaceAll(event.route)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -145,10 +145,17 @@ internal class OnboardingVisaModel @Inject constructor(
|
|||
is VisaCardActivationStatus.ActivationStarted -> {
|
||||
when (val remoteState = activationStatus.remoteState) {
|
||||
is VisaActivationRemoteState.CardWalletSignatureRequired -> {
|
||||
OnboardingVisaRoute.WelcomeBack(
|
||||
activationInput = activationStatus.activationInput,
|
||||
dataToSignByCardWalletRequest = remoteState.request,
|
||||
)
|
||||
if (activationStatus.activationInput.isAccessCodeSet) {
|
||||
OnboardingVisaRoute.WelcomeBack(
|
||||
activationInput = activationStatus.activationInput,
|
||||
dataToSignByCardWalletRequest = VisaCardWalletDataToSignRequest(
|
||||
activationOrderInfo = remoteState.activationOrderInfo,
|
||||
cardWalletAddress = activationStatus.cardWalletAddress,
|
||||
),
|
||||
)
|
||||
} else {
|
||||
OnboardingVisaRoute.AccessCode
|
||||
}
|
||||
}
|
||||
is VisaActivationRemoteState.CustomerWalletSignatureRequired -> {
|
||||
remoteState.getRoute(activationStatus)
|
||||
|
|
@ -167,6 +174,7 @@ internal class OnboardingVisaModel @Inject constructor(
|
|||
}
|
||||
VisaActivationRemoteState.Activated,
|
||||
VisaActivationRemoteState.BlockedForActivation,
|
||||
VisaActivationRemoteState.Failed,
|
||||
-> error("Activation status is not correct for onboarding flow")
|
||||
}
|
||||
}
|
||||
|
|
@ -182,6 +190,7 @@ internal class OnboardingVisaModel @Inject constructor(
|
|||
val request = VisaCustomerWalletDataToSignRequest(
|
||||
orderId = this.activationOrderInfo.orderId,
|
||||
cardWalletAddress = activationStatus.cardWalletAddress,
|
||||
customerWalletAddress = this.activationOrderInfo.customerWalletAddress,
|
||||
)
|
||||
val preparationDataForApprove = PreparationDataForApprove(
|
||||
customerWalletAddress = this.activationOrderInfo.customerWalletAddress,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue