Updated on 2026-08-14

This commit is contained in:
Tangem 2025-11-28 14:12:42 +05:00
parent ff55704d32
commit 9e2eb5710b
82 changed files with 1150 additions and 401 deletions

View file

@ -27,6 +27,7 @@ dependencies {
implementation(deps.spongecastle.core)
/** Libs - Other */
implementation(deps.timber)
implementation(deps.jodatime)
implementation(deps.androidx.paging.runtime)
implementation(deps.moshi)

View file

@ -61,6 +61,7 @@ sealed class VisaApiError(
fun isUnknown() = this is UnknownWithoutCode || this is Unknown
data object RefreshTokenExpired : VisaApiError(104004001)
data object NotPaeraCustomer : VisaApiError(104004002)
companion object {
fun fromBackendError(backendErrorCode: Int): VisaApiError {

View file

@ -0,0 +1,18 @@
package com.tangem.domain.visa.model
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import kotlinx.serialization.Serializable
@Serializable
@JsonClass(generateAdapter = true)
data class TangemPayAuthTokens(
@Json(name = "access_token") val accessToken: String,
@Json(name = "expires_at") val expiresAt: Long,
@Json(name = "refresh_token") val refreshToken: String,
@Json(name = "refresh_expires_at") val refreshExpiresAt: Long,
)
fun TangemPayAuthTokens.getAuthHeader(): String {
return "Bearer $accessToken"
}

View file

@ -1,3 +1,3 @@
package com.tangem.domain.visa.model
data class TangemPayInitialCredentials(val customerWalletAddress: String, val authTokens: VisaAuthTokens)
data class TangemPayInitialCredentials(val customerWalletAddress: String, val authTokens: TangemPayAuthTokens)

View file

@ -1,12 +1,12 @@
package com.tangem.domain.pay.datasource
import arrow.core.Either
import com.tangem.domain.visa.model.TangemPayAuthTokens
import com.tangem.domain.visa.model.TangemPayInitialCredentials
import com.tangem.domain.visa.model.VisaAuthTokens
interface TangemPayAuthDataSource {
suspend fun produceInitialCredentials(cardId: String): Either<Throwable, TangemPayInitialCredentials>
suspend fun refreshAuthTokens(refreshToken: String): Either<Throwable, VisaAuthTokens>
suspend fun refreshAuthTokens(refreshToken: String): Either<Throwable, TangemPayAuthTokens>
}

View file

@ -1,5 +1,6 @@
package com.tangem.domain.pay.model
import com.tangem.domain.visa.model.TangemPayCardFrozenState
import java.math.BigDecimal
data class MainScreenCustomerInfo(
@ -16,13 +17,8 @@ data class CustomerInfo(
data class ProductInstance(
val id: String,
val cardId: String,
val status: Status,
) {
enum class Status {
ACTIVE,
INACTIVE,
}
}
val cardFrozenState: TangemPayCardFrozenState,
)
data class CardInfo(
val lastFourDigits: String,

View file

@ -0,0 +1,8 @@
package com.tangem.domain.pay.model
sealed interface TangemPayCustomerInfoError {
data object UnavailableError : TangemPayCustomerInfoError
data object RefreshNeededError : TangemPayCustomerInfoError
data object UnknownError : TangemPayCustomerInfoError
}

View file

@ -2,6 +2,7 @@ package com.tangem.domain.pay.repository
import arrow.core.Either
import com.tangem.core.error.UniversalError
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.pay.KycStartInfo
interface KycRepository {
@ -9,5 +10,5 @@ interface KycRepository {
/**
* Returns KYC data to start or continue the survey
*/
suspend fun getKycStartInfo(): Either<UniversalError, KycStartInfo>
suspend fun getKycStartInfo(userWalletId: UserWalletId): Either<UniversalError, KycStartInfo>
}

View file

@ -2,23 +2,27 @@ package com.tangem.domain.pay.repository
import arrow.core.Either
import com.tangem.core.error.UniversalError
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.pay.model.CustomerInfo
import com.tangem.domain.pay.model.MainScreenCustomerInfo
import com.tangem.domain.pay.model.OrderStatus
import com.tangem.domain.visa.error.VisaApiError
import kotlinx.coroutines.Job
interface OnboardingRepository {
suspend fun validateDeeplink(link: String): Either<UniversalError, Boolean>
suspend fun isTangemPayInitialDataProduced(): Boolean
suspend fun isTangemPayInitialDataProduced(userWalletId: UserWalletId): Boolean
suspend fun produceInitialData()
suspend fun produceInitialData(userWalletId: UserWalletId)
suspend fun getCustomerInfo(): Either<UniversalError, CustomerInfo>
suspend fun getCustomerInfo(userWalletId: UserWalletId): Either<VisaApiError, CustomerInfo>
/**
* Returns only if the user already authorised at least once
*/
suspend fun getMainScreenCustomerInfo(): Either<UniversalError, MainScreenCustomerInfo>
suspend fun createOrder(userWalletId: UserWalletId): Job
fun getSavedCustomerInfo(): CustomerInfo?
suspend fun clearOrderId(userWalletId: UserWalletId)
suspend fun getOrderId(userWalletId: UserWalletId): String?
suspend fun getOrderStatus(userWalletId: UserWalletId, orderId: String): Either<VisaApiError, OrderStatus>
suspend fun checkCustomerWallet(userWalletId: UserWalletId): Either<VisaApiError, Boolean>
fun getSavedCustomerInfo(userWalletId: UserWalletId): CustomerInfo?
}

View file

@ -2,6 +2,7 @@ package com.tangem.domain.pay.repository
import arrow.core.Either
import com.tangem.core.error.UniversalError
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.pay.model.SetPinResult
import com.tangem.domain.pay.model.TangemPayCardBalance
import com.tangem.domain.pay.model.TangemPayCardDetails
@ -10,18 +11,22 @@ import kotlinx.coroutines.flow.Flow
interface TangemPayCardDetailsRepository {
suspend fun getCardBalance(): Either<UniversalError, TangemPayCardBalance>
suspend fun getCardBalance(userWalletId: UserWalletId): Either<UniversalError, TangemPayCardBalance>
suspend fun revealCardDetails(): Either<UniversalError, TangemPayCardDetails>
suspend fun revealCardDetails(userWalletId: UserWalletId): Either<UniversalError, TangemPayCardDetails>
suspend fun setPin(pin: String): Either<UniversalError, SetPinResult>
suspend fun setPin(userWalletId: UserWalletId, pin: String): Either<UniversalError, SetPinResult>
suspend fun isAddToWalletDone(): Either<UniversalError, Boolean>
suspend fun isAddToWalletDone(userWalletId: UserWalletId): Either<UniversalError, Boolean>
suspend fun setAddToWalletAsDone(): Either<UniversalError, Unit>
suspend fun setAddToWalletAsDone(userWalletId: UserWalletId): Either<UniversalError, Unit>
suspend fun freezeCard(userWalletId: UserWalletId, cardId: String): Either<UniversalError, TangemPayCardFrozenState>
suspend fun unfreezeCard(
userWalletId: UserWalletId,
cardId: String,
): Either<UniversalError, TangemPayCardFrozenState>
suspend fun freezeCard(cardId: String): Either<UniversalError, TangemPayCardFrozenState>
suspend fun unfreezeCard(cardId: String): Either<UniversalError, TangemPayCardFrozenState>
fun cardFrozenState(cardId: String): Flow<TangemPayCardFrozenState>
suspend fun cardFrozenStateSync(cardId: String): TangemPayCardFrozenState?
}

View file

@ -2,20 +2,16 @@ package com.tangem.domain.pay.usecase
import arrow.core.Either
import arrow.core.Either.Companion.catch
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.pay.repository.OnboardingRepository
class ProduceTangemPayInitialDataUseCase(
private val repository: OnboardingRepository,
) {
suspend operator fun invoke(): Either<Throwable, Unit> {
suspend operator fun invoke(userWalletId: UserWalletId): Either<Throwable, Unit> {
return catch {
val isDataProduced = repository.isTangemPayInitialDataProduced()
if (isDataProduced) {
return@catch Unit
} else {
repository.produceInitialData()
}
repository.produceInitialData(userWalletId)
}
}
}

View file

@ -1,7 +1,19 @@
package com.tangem.domain.pay.usecase
import arrow.core.Either
import arrow.core.left
import arrow.core.raise.catch
import arrow.core.right
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.pay.model.CustomerInfo
import com.tangem.domain.pay.model.MainScreenCustomerInfo
import com.tangem.domain.pay.model.OrderStatus
import com.tangem.domain.pay.model.TangemPayCustomerInfoError
import com.tangem.domain.pay.repository.OnboardingRepository
import com.tangem.domain.visa.error.VisaApiError
import timber.log.Timber
private const val TAG = "TangemPayMainScreenCustomerInfoUseCase"
/**
* Returns tangem pay customer info for the main screen banner
@ -11,5 +23,96 @@ class TangemPayMainScreenCustomerInfoUseCase(
private val repository: OnboardingRepository,
) {
suspend operator fun invoke(): MainScreenCustomerInfo? = repository.getMainScreenCustomerInfo().getOrNull()
suspend operator fun invoke(
userWalletId: UserWalletId,
): Either<TangemPayCustomerInfoError, MainScreenCustomerInfo> = catch(
block = {
repository.checkCustomerWallet(userWalletId)
.fold(
ifLeft = { TangemPayCustomerInfoError.UnknownError.left() },
ifRight = { hasTangemPay ->
if (hasTangemPay) {
proceedWithPaeraCustomerResult(userWalletId)
} else {
TangemPayCustomerInfoError.UnknownError.left() // ignore if there's no TangemPay
}
},
)
},
catch = { error ->
Timber.tag(TAG).e(error)
TangemPayCustomerInfoError.UnknownError.left()
},
)
private suspend fun proceedWithPaeraCustomerResult(
userWalletId: UserWalletId,
): Either<TangemPayCustomerInfoError, MainScreenCustomerInfo> {
val orderId = repository.getOrderId(userWalletId)
return if (orderId != null) {
proceedWithOrderId(userWalletId = userWalletId, orderId = orderId)
} else {
proceedWithoutOrder(userWalletId = userWalletId)
}
}
private suspend fun proceedWithoutOrder(
userWalletId: UserWalletId,
): Either<TangemPayCustomerInfoError, MainScreenCustomerInfo> {
return repository.getCustomerInfo(userWalletId)
.mapLeft { error -> error.mapErrorForCustomer() }
.map { customerInfo ->
if (customerInfo.cardInfo == null) {
// If order id wasn't saved -> start order creation and get customer info
repository.createOrder(userWalletId)
}
MainScreenCustomerInfo(info = customerInfo, orderStatus = OrderStatus.UNKNOWN)
}
}
private suspend fun proceedWithOrderId(
userWalletId: UserWalletId,
orderId: String,
): Either<TangemPayCustomerInfoError, MainScreenCustomerInfo> {
return repository.getOrderStatus(userWalletId, orderId = orderId)
.fold(
ifLeft = { error ->
error.mapErrorForCustomer().left()
},
ifRight = { orderStatus ->
when (orderStatus) {
// Kyc is passed and user waits for order creation -> no need to get customer info
OrderStatus.NEW,
OrderStatus.PROCESSING,
-> MainScreenCustomerInfo(
info = CustomerInfo(productInstance = null, isKycApproved = true, cardInfo = null),
orderStatus = orderStatus,
).right()
// Order was created/cancelled -> clear order id and get customer info
OrderStatus.COMPLETED,
OrderStatus.CANCELED,
OrderStatus.UNKNOWN,
-> {
repository.clearOrderId(userWalletId)
// If order was cancelled -> start order creation
if (orderStatus == OrderStatus.CANCELED) repository.createOrder(userWalletId)
repository.getCustomerInfo(userWalletId = userWalletId)
.mapLeft { it.mapErrorForCustomer() }
.map { customerInfo ->
MainScreenCustomerInfo(info = customerInfo, orderStatus = orderStatus)
}
}
}
},
)
}
private fun VisaApiError.mapErrorForCustomer(): TangemPayCustomerInfoError {
return if (this !is VisaApiError.NotPaeraCustomer) {
TangemPayCustomerInfoError.UnavailableError
} else {
TangemPayCustomerInfoError.UnknownError
}
}
}

View file

@ -1,10 +1,12 @@
package com.tangem.domain.tangempay.repository
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.tangempay.model.TangemPayTxHistoryListBatchFlow
import com.tangem.domain.tangempay.model.TangemPayTxHistoryListBatchingContext
interface TangemPayTxHistoryRepository {
fun getTxHistoryBatchFlow(
userWalletId: UserWalletId,
batchSize: Int,
context: TangemPayTxHistoryListBatchingContext,
): TangemPayTxHistoryListBatchFlow

View file

@ -2,6 +2,7 @@ package com.tangem.domain.visa.datasource
import arrow.core.Either
import com.tangem.domain.visa.error.VisaApiError
import com.tangem.domain.visa.model.TangemPayAuthTokens
import com.tangem.domain.visa.model.VisaAuthChallenge
import com.tangem.domain.visa.model.VisaAuthSignedChallenge
import com.tangem.domain.visa.model.VisaAuthTokens
@ -20,17 +21,16 @@ interface VisaAuthRemoteDataSource {
suspend fun getCustomerWalletAuthChallenge(
customerWalletAddress: String,
customerWalletId: String,
): Either<VisaApiError, VisaAuthChallenge.Wallet>
suspend fun getTokenWithCustomerWallet(
sessionId: String,
signature: String,
nonce: String,
): Either<VisaApiError, VisaAuthTokens>
): Either<VisaApiError, TangemPayAuthTokens>
suspend fun refreshCustomerWalletAuthTokens(
refreshToken: VisaAuthTokens.RefreshToken,
): Either<VisaApiError, VisaAuthTokens>
suspend fun refreshCustomerWalletAuthTokens(refreshToken: String): Either<VisaApiError, TangemPayAuthTokens>
suspend fun getAccessTokens(signedChallenge: VisaAuthSignedChallenge): Either<VisaApiError, VisaAuthTokens>