Updated on 2026-08-14
This commit is contained in:
parent
19fb8f2bcd
commit
ecdb72e55c
58 changed files with 1082 additions and 346 deletions
|
|
@ -1,49 +1,195 @@
|
|||
package com.tangem.data.visa
|
||||
|
||||
import com.tangem.common.extensions.toHexString
|
||||
import com.tangem.crypto.CryptoUtils
|
||||
import com.tangem.data.visa.converter.AccessCodeDataConverter
|
||||
import com.tangem.data.visa.converter.VisaActivationStatusConverter
|
||||
import com.tangem.datasource.api.common.visa.TangemVisaAuthProvider
|
||||
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.domain.visa.model.ActivationOrder
|
||||
import com.tangem.domain.visa.model.VisaActivationRemoteState
|
||||
import com.tangem.datasource.api.visa.models.request.ActivationByCardWalletRequest
|
||||
import com.tangem.datasource.api.visa.models.request.ActivationByCustomerWalletRequest
|
||||
import com.tangem.datasource.local.visa.VisaAuthTokenStorage
|
||||
import com.tangem.domain.visa.exception.RefreshTokenExpiredException
|
||||
import com.tangem.domain.visa.model.*
|
||||
import com.tangem.domain.visa.repository.VisaActivationRepository
|
||||
import com.tangem.domain.visa.repository.VisaAuthRepository
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
@Suppress("UnusedPrivateMember")
|
||||
@Suppress("LongParameterList")
|
||||
internal class DefaultVisaActivationRepository @AssistedInject constructor(
|
||||
@Assisted private val cardId: String,
|
||||
@Assisted private val visaCardId: VisaCardId,
|
||||
private val visaApi: TangemVisaApi,
|
||||
private val dispatcherProvider: CoroutineDispatcherProvider,
|
||||
private val visaActivationStatusConverter: VisaActivationStatusConverter,
|
||||
private val visaAuthProvider: TangemVisaAuthProvider,
|
||||
private val visaAuthTokenStorage: VisaAuthTokenStorage,
|
||||
private val accessCodeDataConverter: AccessCodeDataConverter,
|
||||
private val visaAuthRepository: VisaAuthRepository,
|
||||
) : VisaActivationRepository {
|
||||
|
||||
override suspend fun getActivationRemoteState(): VisaActivationRemoteState = withContext(dispatcherProvider.io) {
|
||||
// visaActivationStatusConverter.convert(visaApi.getRemoteActivationStatus(visaAuthProvider.getAuthHeader(cardId)))
|
||||
VisaActivationRemoteState.CardWalletSignatureRequired // mock
|
||||
// TODO implement refreshing access token if it's expired
|
||||
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,
|
||||
).getOrThrow()
|
||||
}
|
||||
|
||||
visaActivationStatusConverter.convert(result)
|
||||
}
|
||||
|
||||
override suspend fun getActivationRemoteStateLongPoll(): VisaActivationRemoteState =
|
||||
withContext(dispatcherProvider.io) {
|
||||
// visaActivationStatusConverter.convert(
|
||||
// visaApi.getRemoteActivationStatusLongPoll(visaAuthProvider.getAuthHeader(cardId)),
|
||||
// )
|
||||
val result = request {
|
||||
val authTokens =
|
||||
checkNotNull(visaAuthTokenStorage.get(visaCardId.cardId)) { "Visa auth tokens are not stored" }
|
||||
val accessCodeData = accessCodeDataConverter.convert(authTokens)
|
||||
|
||||
VisaActivationRemoteState.WaitingPinCode
|
||||
visaApi.getRemoteActivationStatusLongPoll(
|
||||
authHeader = authTokens.getAuthHeader(),
|
||||
customerId = accessCodeData.customerId,
|
||||
productInstanceId = accessCodeData.productInstanceId,
|
||||
cardId = visaCardId.cardId,
|
||||
cardPublicKey = visaCardId.cardPublicKey,
|
||||
).getOrThrow()
|
||||
}
|
||||
|
||||
visaActivationStatusConverter.convert(result)
|
||||
}
|
||||
|
||||
override suspend fun getActivationOrderToSign(): ActivationOrder = withContext(dispatcherProvider.io) {
|
||||
ActivationOrder(CryptoUtils.generateRandomBytes(length = 32).toHexString())
|
||||
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,
|
||||
).getOrThrow()
|
||||
}
|
||||
|
||||
VisaDataToSignByCardWallet(
|
||||
request = request,
|
||||
hashToSign = result.dataForCardWallet.hash,
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun getCustomerWalletAcceptanceData(
|
||||
request: VisaCustomerWalletDataToSignRequest,
|
||||
): VisaDataToSignByCustomerWallet = 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.getCustomerWalletAcceptance(
|
||||
authHeader = authTokens.getAuthHeader(),
|
||||
customerId = accessCodeData.customerId,
|
||||
productInstanceId = accessCodeData.productInstanceId,
|
||||
activationOrderId = request.orderId,
|
||||
cardWalletAddress = request.cardWalletAddress,
|
||||
).getOrThrow()
|
||||
}
|
||||
|
||||
VisaDataToSignByCustomerWallet(
|
||||
request = request,
|
||||
hashToSign = result.dataForCardWallet.hash,
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun activateCard(signedData: VisaSignedActivationDataByCardWallet) {
|
||||
withContext(dispatcherProvider.io) {
|
||||
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,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun approveByCustomerWallet(signedData: VisaSignedDataByCustomerWallet) {
|
||||
withContext(dispatcherProvider.io) {
|
||||
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,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun <T : Any> request(requestBlock: suspend () -> T): T {
|
||||
return runCatching {
|
||||
requestBlock()
|
||||
}.getOrElse { responseError ->
|
||||
if (responseError !is ApiResponseError.HttpException ||
|
||||
responseError.code != ApiResponseError.HttpException.Code.UNAUTHORIZED
|
||||
) {
|
||||
throw responseError
|
||||
}
|
||||
|
||||
val authTokens = visaAuthTokenStorage.get(visaCardId.cardId) ?: error("Auth tokens are not stored")
|
||||
val newTokens = runCatching {
|
||||
visaAuthRepository.refreshAccessTokens(authTokens.refreshToken)
|
||||
}.getOrElse { throw RefreshTokenExpiredException() }
|
||||
|
||||
visaAuthTokenStorage.store(visaCardId.cardId, newTokens)
|
||||
|
||||
requestBlock()
|
||||
}
|
||||
}
|
||||
|
||||
@AssistedFactory
|
||||
interface Factory : VisaActivationRepository.Factory {
|
||||
override fun create(cardId: String): DefaultVisaActivationRepository
|
||||
override fun create(cardId: VisaCardId): DefaultVisaActivationRepository
|
||||
}
|
||||
}
|
||||
|
|
@ -36,24 +36,22 @@ internal class DefaultVisaAuthRepository @Inject constructor(
|
|||
)
|
||||
}
|
||||
|
||||
override suspend fun getCustomerWalletAuthChallenge(
|
||||
cardId: String,
|
||||
walletPublicKey: String,
|
||||
): VisaAuthChallenge.Wallet = withContext(dispatchers.io) {
|
||||
// val response = visaAuthApi.generateNonceByWalletAddress(
|
||||
// customerId = cardId,
|
||||
// customerWalletAddress = walletPublicKey,
|
||||
// )
|
||||
//
|
||||
// VisaAuthChallenge.Wallet(
|
||||
// challenge = response.nonce,
|
||||
// session = VisaAuthSession(response.sessionId),
|
||||
// )
|
||||
VisaAuthChallenge.Wallet(
|
||||
challenge = CryptoUtils.generateRandomBytes(length = 32).toHexString(),
|
||||
session = VisaAuthSession("session"),
|
||||
)
|
||||
}
|
||||
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),
|
||||
// )
|
||||
VisaAuthChallenge.Wallet(
|
||||
challenge = CryptoUtils.generateRandomBytes(length = 32).toHexString(),
|
||||
session = VisaAuthSession("session"),
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun getAccessTokens(signedChallenge: VisaAuthSignedChallenge): VisaAuthTokens =
|
||||
withContext(dispatchers.io) {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,50 @@
|
|||
package com.tangem.data.visa
|
||||
|
||||
import com.tangem.common.extensions.toHexString
|
||||
import com.tangem.crypto.CryptoUtils
|
||||
import com.tangem.domain.visa.model.*
|
||||
import com.tangem.domain.visa.repository.VisaActivationRepository
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
|
||||
@Suppress("UnusedPrivateMember")
|
||||
class MockVisaActivationRepository @AssistedInject constructor(
|
||||
@Assisted private val visaCardId: VisaCardId,
|
||||
) : VisaActivationRepository {
|
||||
|
||||
override suspend fun getActivationRemoteState(): VisaActivationRemoteState {
|
||||
return VisaActivationRemoteState.PaymentAccountDeploying
|
||||
}
|
||||
|
||||
override suspend fun getActivationRemoteStateLongPoll(): VisaActivationRemoteState {
|
||||
return VisaActivationRemoteState.WaitingPinCode
|
||||
}
|
||||
|
||||
override suspend fun getCardWalletAcceptanceData(
|
||||
request: VisaCardWalletDataToSignRequest,
|
||||
): VisaDataToSignByCardWallet {
|
||||
return VisaDataToSignByCardWallet(
|
||||
request = request,
|
||||
hashToSign = CryptoUtils.generateRandomBytes(length = 32).toHexString(),
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun getCustomerWalletAcceptanceData(
|
||||
request: VisaCustomerWalletDataToSignRequest,
|
||||
): VisaDataToSignByCustomerWallet {
|
||||
return VisaDataToSignByCustomerWallet(
|
||||
request = request,
|
||||
CryptoUtils.generateRandomBytes(length = 32).toHexString(),
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun activateCard(signedData: VisaSignedActivationDataByCardWallet) {}
|
||||
|
||||
override suspend fun approveByCustomerWallet(signedData: VisaSignedDataByCustomerWallet) {}
|
||||
|
||||
@AssistedFactory
|
||||
interface Factory : VisaActivationRepository.Factory {
|
||||
override fun create(cardId: VisaCardId): MockVisaActivationRepository
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
package com.tangem.data.visa.converter
|
||||
|
||||
import com.squareup.moshi.Moshi
|
||||
import com.tangem.data.visa.model.AccessCodeData
|
||||
import com.tangem.datasource.di.NetworkMoshi
|
||||
import com.tangem.domain.visa.model.VisaAuthTokens
|
||||
import com.tangem.utils.converter.Converter
|
||||
import okio.ByteString.Companion.decodeBase64
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
private const val JWT_PAYLOAD_INDEX = 1
|
||||
|
||||
@Singleton
|
||||
internal class AccessCodeDataConverter @Inject constructor(
|
||||
@NetworkMoshi private val moshi: Moshi,
|
||||
) : Converter<VisaAuthTokens, AccessCodeData> {
|
||||
|
||||
private val adapter = moshi.adapter(AccessCodeData::class.java)
|
||||
|
||||
override fun convert(value: VisaAuthTokens): AccessCodeData {
|
||||
val payloadBase64 = value.accessToken.split(".").getOrNull(JWT_PAYLOAD_INDEX)
|
||||
val decodedString = payloadBase64?.decodeBase64()?.utf8()
|
||||
val data = decodedString?.let { adapter.fromJson(it) }
|
||||
requireNotNull(data) { "Invalid access token" }
|
||||
return data
|
||||
}
|
||||
}
|
||||
|
|
@ -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.DummyVisaRepository
|
||||
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
|
||||
|
|
@ -36,9 +36,16 @@ internal interface VisaDataBindsModule {
|
|||
@Singleton
|
||||
fun bindVisaAuthRepository(repository: DefaultVisaAuthRepository): VisaAuthRepository
|
||||
|
||||
// @Binds
|
||||
// @Singleton
|
||||
// fun bindVisaActivationRepositoryFactory(
|
||||
// repository: DefaultVisaActivationRepository.Factory,
|
||||
// ): VisaActivationRepository.Factory
|
||||
|
||||
// Mocked
|
||||
@Binds
|
||||
@Singleton
|
||||
fun bindVisaActivationRepositoryFactory(
|
||||
repository: DefaultVisaActivationRepository.Factory,
|
||||
repository: MockVisaActivationRepository.Factory,
|
||||
): VisaActivationRepository.Factory
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
package com.tangem.data.visa.model
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
internal data class AccessCodeData(
|
||||
@Json(name = "pid") val productInstanceId: String,
|
||||
@Json(name = "sub") val customerId: String,
|
||||
)
|
||||
Loading…
Add table
Add a link
Reference in a new issue