Updated on 2026-08-14

This commit is contained in:
Tangem 2025-04-22 11:52:10 +03:00
parent cb23c1dea3
commit 538757a7ce
54 changed files with 622 additions and 488 deletions

View file

@ -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()
}
}
}

View file

@ -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),
)
}
}

View file

@ -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 {

View file

@ -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
}
}

View file

@ -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"),
}
}

View file

@ -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

View file

@ -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"
}