Updated on 2026-08-14

This commit is contained in:
Tangem 2025-11-06 15:31:30 +05:00
parent e037233aa5
commit 804e4eca23
31 changed files with 635 additions and 147 deletions

View file

@ -1,6 +1,7 @@
package com.tangem.data.pay
import arrow.core.Either
import arrow.core.Either.Companion.catch
import com.squareup.moshi.Moshi
import com.tangem.blockchain.blockchains.ethereum.Chain
import com.tangem.blockchainsdk.utils.ExcludedBlockchains
@ -16,7 +17,6 @@ import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.pay.TangemPayTopUpData
import com.tangem.domain.pay.TangemPayTopUpDataFactory
import kotlinx.coroutines.CancellationException
import timber.log.Timber
import java.math.BigDecimal
import javax.inject.Inject
@ -68,10 +68,10 @@ internal class DefaultTangemPayTopUpDataFactory @Inject constructor(
cryptoBalance: BigDecimal,
fiatBalance: BigDecimal,
): Either<UniversalError, TangemPayTopUpData> {
return try {
return catch {
val wallet = tangemPayWalletsManager.getDefaultWalletForTangemPayBlocking()
val currency = getCurrency(wallet, chainId)
val result = TangemPayTopUpData(
TangemPayTopUpData(
currency = currency,
walletId = wallet.walletId,
cryptoBalance = cryptoBalance,
@ -79,18 +79,9 @@ internal class DefaultTangemPayTopUpDataFactory @Inject constructor(
depositAddress = depositAddress,
receiveAddress = listOf(ReceiveAddressModel(nameService = NameService.Default, value = depositAddress)),
)
Either.Right(result)
} catch (exception: Exception) {
when (exception) {
is CancellationException -> {
throw exception
}
else -> {
Timber.tag(TAG).e(exception)
Either.Left(errorConverter.convert(exception))
}
}
}.mapLeft { exception ->
Timber.tag(TAG).e(exception)
errorConverter.convert(exception)
}
}
}

View file

@ -1,13 +1,13 @@
package com.tangem.data.pay.di
import com.tangem.data.pay.DefaultTangemPayTopUpDataFactory
import com.tangem.data.pay.repository.DefaultCardDetailsRepository
import com.tangem.data.pay.repository.DefaultTangemPayCardDetailsRepository
import com.tangem.data.pay.repository.DefaultKycRepository
import com.tangem.data.pay.repository.DefaultTangemPayTxHistoryRepository
import com.tangem.data.pay.repository.DefaultOnboardingRepository
import com.tangem.data.pay.usecase.DefaultGetTangemPayCurrencyStatusUseCase
import com.tangem.domain.pay.TangemPayTopUpDataFactory
import com.tangem.domain.pay.repository.CardDetailsRepository
import com.tangem.domain.pay.repository.TangemPayCardDetailsRepository
import com.tangem.domain.pay.repository.KycRepository
import com.tangem.domain.pay.repository.OnboardingRepository
import com.tangem.domain.pay.usecase.ProduceTangemPayInitialDataUseCase
@ -40,7 +40,7 @@ internal interface TangemPayDataModule {
@Binds
@Singleton
fun bindCardDetailsRepository(repository: DefaultCardDetailsRepository): CardDetailsRepository
fun bindCardDetailsRepository(repository: DefaultTangemPayCardDetailsRepository): TangemPayCardDetailsRepository
@Binds
@Singleton

View file

@ -137,8 +137,18 @@ internal class DefaultOnboardingRepository @Inject constructor(
} else {
null
}
val productInstance = response?.productInstance?.let { instance ->
ProductInstance(
id = instance.id,
cardId = instance.cardId,
status = when (instance.status) {
CustomerMeResponse.ProductInstance.Status.ACTIVE -> ProductInstance.Status.ACTIVE
else -> ProductInstance.Status.INACTIVE
},
)
}
return CustomerInfo(
productInstance = response?.productInstance?.let { ProductInstance(id = it.id, status = it.status) },
productInstance = productInstance,
isKycApproved = response?.kyc?.status == APPROVED_KYC_STATUS,
cardInfo = cardInfo,
)

View file

@ -9,24 +9,29 @@ import com.tangem.datasource.api.common.config.ApiEnvironment
import com.tangem.datasource.api.common.config.managers.ApiConfigsManager
import com.tangem.datasource.api.pay.TangemPayApi
import com.tangem.datasource.api.pay.models.request.CardDetailsRequest
import com.tangem.datasource.api.pay.models.request.FreezeUnfreezeCardRequest
import com.tangem.datasource.api.pay.models.request.SetPinRequest
import com.tangem.datasource.api.pay.models.response.FreezeUnfreezeCardResponse
import com.tangem.datasource.local.visa.TangemPayStorage
import com.tangem.domain.pay.model.SetPinResult
import com.tangem.domain.pay.model.TangemPayCardBalance
import com.tangem.domain.pay.model.TangemPayCardDetails
import com.tangem.domain.pay.repository.CardDetailsRepository
import com.tangem.domain.pay.repository.TangemPayCardDetailsRepository
import com.tangem.domain.visa.error.VisaApiError
import timber.log.Timber
import javax.inject.Inject
private const val TAG = "TangemPay: CardDetailsRepository"
internal class DefaultCardDetailsRepository @Inject constructor(
@Suppress("OptionalUnit")
internal class DefaultTangemPayCardDetailsRepository @Inject constructor(
private val tangemPayApi: TangemPayApi,
private val requestHelper: TangemPayRequestPerformer,
private val visaLibLoader: VisaLibLoader,
private val apiConfigsManager: ApiConfigsManager,
private val rainCryptoUtil: RainCryptoUtil,
private val storage: TangemPayStorage,
) : CardDetailsRepository {
) : TangemPayCardDetailsRepository {
override suspend fun getCardBalance(): Either<UniversalError, TangemPayCardBalance> {
return requestHelper.runWithErrorLogs(TAG) {
@ -112,6 +117,34 @@ internal class DefaultCardDetailsRepository @Inject constructor(
}
}
override suspend fun freezeCard(cardId: String): Either<UniversalError, Unit> {
return requestHelper.makeSafeRequest {
tangemPayApi.freezeCard(authHeader = it, body = FreezeUnfreezeCardRequest(cardId = cardId))
}.map { response ->
val result = response.result
if (result != null && result.status == FreezeUnfreezeCardResponse.Status.COMPLETED) {
Either.Right(Unit)
} else {
Timber.e("Card freeze failed with status: ${response.result?.status}")
Either.Left(VisaApiError.UnknownWithoutCode)
}
}
}
override suspend fun unfreezeCard(cardId: String): Either<UniversalError, Unit> {
return requestHelper.makeSafeRequest {
tangemPayApi.unfreezeCard(authHeader = it, body = FreezeUnfreezeCardRequest(cardId = cardId))
}.map { response ->
val result = response.result
if (result != null && result.status == FreezeUnfreezeCardResponse.Status.COMPLETED) {
Either.Right(Unit)
} else {
Timber.e("Card unfreeze failed with status: ${response.result?.status}")
Either.Left(VisaApiError.UnknownWithoutCode)
}
}
}
private suspend fun getPublicKeyBase64(): String {
val env = apiConfigsManager.getEnvironmentConfig(ApiConfig.ID.TangemPay).environment
return when (env) {

View file

@ -1,6 +1,7 @@
package com.tangem.data.pay.repository
import arrow.core.Either
import arrow.core.Either.Companion.catch
import com.squareup.moshi.Moshi
import com.tangem.core.error.UniversalError
import com.tangem.data.pay.util.TangemPayErrorConverter
@ -11,6 +12,7 @@ import com.tangem.datasource.api.common.response.getOrThrow
import com.tangem.datasource.di.NetworkMoshi
import com.tangem.datasource.local.visa.TangemPayStorage
import com.tangem.domain.pay.datasource.TangemPayAuthDataSource
import com.tangem.domain.visa.error.VisaApiError
import com.tangem.domain.visa.model.VisaAuthTokens
import com.tangem.domain.visa.model.getAuthHeader
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
@ -21,7 +23,6 @@ import kotlinx.coroutines.sync.withLock
import timber.log.Timber
import javax.inject.Inject
@Suppress("LongParameterList")
internal class TangemPayRequestPerformer @Inject constructor(
@NetworkMoshi moshi: Moshi,
private val dispatchers: CoroutineDispatcherProvider,
@ -54,6 +55,16 @@ internal class TangemPayRequestPerformer @Inject constructor(
}
}
suspend fun <T : Any> makeSafeRequest(
requestBlock: suspend (header: String) -> ApiResponse<T>,
): Either<VisaApiError, T> {
return catch { request(requestBlock) }
.mapLeft { exception ->
Timber.tag("TangemPayRequestPerformer").e(exception)
errorConverter.convert(exception)
}
}
suspend fun <T : Any> request(requestBlock: suspend (header: String) -> ApiResponse<T>): T =
withContext(dispatchers.io) {
performRequest(

View file

@ -1,17 +1,16 @@
package com.tangem.data.pay.util
import com.squareup.moshi.Moshi
import com.tangem.core.error.UniversalError
import com.tangem.datasource.api.common.response.ApiResponseError
import com.tangem.datasource.api.pay.models.response.VisaErrorResponseJsonAdapter
import com.tangem.datasource.api.pay.models.response.VisaErrorResponse
import com.tangem.domain.visa.error.VisaApiError
import com.tangem.utils.converter.Converter
class TangemPayErrorConverter(moshi: Moshi) : Converter<Throwable, UniversalError> {
class TangemPayErrorConverter(moshi: Moshi) : Converter<Throwable, VisaApiError> {
private val visaErrorAdapter = VisaErrorResponseJsonAdapter(moshi)
private val visaErrorAdapter by lazy { moshi.adapter(VisaErrorResponse::class.java) }
override fun convert(value: Throwable): UniversalError {
override fun convert(value: Throwable): VisaApiError {
return if (value is ApiResponseError.HttpException) {
val errorBody = value.errorBody ?: return VisaApiError.UnknownWithoutCode
return runCatching {