Updated on 2026-08-14

This commit is contained in:
Tangem 2025-02-11 13:35:31 +03:00
parent 19fb8f2bcd
commit ecdb72e55c
58 changed files with 1082 additions and 346 deletions

View file

@ -15,7 +15,7 @@ import retrofit2.converter.moshi.MoshiConverterFactory
object MoshiConverter {
val networkMoshi: Moshi = Moshi.Builder()
.add(KotlinJsonAdapterFactory())
.addLast(KotlinJsonAdapterFactory())
.add(BigDecimalAdapter())
.add(TangemSdkAdapter.ByteArrayAdapter())
.build()

View file

@ -1,8 +1,11 @@
package com.tangem.datasource.api.common.config
import com.tangem.utils.ProviderSuspend
import com.tangem.utils.version.AppVersionProvider
internal class TangemVisa : ApiConfig() {
internal class TangemVisa(
private val appVersionProvider: AppVersionProvider,
) : ApiConfig() {
override val defaultEnvironment: ApiEnvironment = ApiEnvironment.PROD
@ -16,5 +19,8 @@ internal class TangemVisa : ApiConfig() {
headers = createHeaders(),
)
private fun createHeaders() = mapOf<String, ProviderSuspend<String>>()
private fun createHeaders() = mapOf(
"version" to ProviderSuspend { appVersionProvider.versionName },
"platform" to ProviderSuspend { "Android" },
)
}

View file

@ -1,8 +1,11 @@
package com.tangem.datasource.api.common.config
import com.tangem.utils.ProviderSuspend
import com.tangem.utils.version.AppVersionProvider
internal class TangemVisaAuth : ApiConfig() {
internal class TangemVisaAuth(
private val appVersionProvider: AppVersionProvider,
) : ApiConfig() {
override val defaultEnvironment: ApiEnvironment = ApiEnvironment.STAGE
@ -16,5 +19,8 @@ internal class TangemVisaAuth : ApiConfig() {
headers = createHeaders(),
)
private fun createHeaders() = mapOf<String, ProviderSuspend<String>>()
private fun createHeaders() = mapOf(
"version" to ProviderSuspend { appVersionProvider.versionName },
"platform" to ProviderSuspend { "Android" },
)
}

View file

@ -1,21 +1,67 @@
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.response.CardActivationRemoteStateResponse
import com.tangem.datasource.api.visa.models.response.CardWalletDataToSignResponse
import com.tangem.datasource.api.visa.models.response.CustomerWalletDataToSignResponse
import retrofit2.http.Body
import retrofit2.http.GET
import retrofit2.http.Header
import retrofit2.http.POST
import retrofit2.http.Query
import java.util.concurrent.TimeUnit
interface TangemVisaApi {
@GET("activation-status")
@GET("product_instance/activation_status")
suspend fun getRemoteActivationStatus(
@Header("Authorization") authHeader: String,
): CardActivationRemoteStateResponse
@Query("customer_id") customerId: String,
@Query("product_instance_id") productInstanceId: String,
@Query("card_id") cardId: String,
@Query("card_public_key") cardPublicKey: String,
): ApiResponse<CardActivationRemoteStateResponse>
@ReadTimeout(duration = 20, TimeUnit.MINUTES)
@GET("activation-status")
@GET("product_instance/activation_status")
suspend fun getRemoteActivationStatusLongPoll(
@Header("Authorization") authHeader: String,
): CardActivationRemoteStateResponse
@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")
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>
@GET("product_instance/customer_wallet_acceptance")
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>
@POST("product_instance/activation_by_card_wallet")
suspend fun activateByCardWallet(
@Header("Authorization") authHeader: String,
@Body body: ActivationByCardWalletRequest,
): ApiResponse<Unit>
@POST("product_instance/activation_by_customer_wallet")
suspend fun activateByCustomerWallet(
@Header("Authorization") authHeader: String,
@Body body: ActivationByCustomerWalletRequest,
): ApiResponse<Unit>
}

View file

@ -3,45 +3,29 @@ package com.tangem.datasource.api.visa
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.FormUrlEncoded
import retrofit2.http.Headers
import retrofit2.http.POST
import retrofit2.http.Query
interface TangemVisaAuthApi {
@FormUrlEncoded
@Headers("Content-Type: application/x-www-form-urlencoded")
@POST("auth/clients/mobile-app-android/nonce-challenge")
@POST("auth/card_wallet")
suspend fun generateNonceByWalletAddress(
@Field("customer_id") customerId: String? = null,
@Field("customer_wallet_address") customerWalletAddress: String,
@Query("card_wallet_address") cardWalletAddress: String,
): GenerateNonceResponse
@FormUrlEncoded
@Headers("Content-Type: application/x-www-form-urlencoded")
@POST("auth/clients/mobile-app-android/nonce-challenge")
@POST("auth/card_id")
suspend fun generateNonceByCard(
@Field("card_id") cardId: String,
@Field("card_public_key") cardPublicKey: String,
@Query("card_id") cardId: String,
@Query("card_public_key") cardPublicKey: String,
): GenerateNonceResponse
@FormUrlEncoded
@Headers("Content-Type: application/x-www-form-urlencoded")
@POST("auth/protocol/openid-connect/token")
@POST("auth/get_token")
suspend fun getAccessToken(
@Field("client_id") clientId: String = "mobile-app-android",
@Field("grant_type") grantType: String = "password",
@Field("session_id") sessionId: String,
@Field("signature") signature: String,
@Field("salt") salt: String?,
@Query("session_id") sessionId: String,
@Query("signature") signature: String,
@Query("salt") salt: String?,
): JWTResponse
@FormUrlEncoded
@Headers("Content-Type: application/x-www-form-urlencoded")
@POST("auth/protocol/openid-connect/token")
suspend fun refreshAccessToken(
@Field("client_id") clientId: String = "mobile-app-android",
@Field("grant_type") grantType: String = "refresh_token",
@Field("refresh_token") refreshToken: String,
): JWTResponse
@POST("auth/refresh_token")
suspend fun refreshAccessToken(@Field("refresh_token") refreshToken: String): JWTResponse
}

View file

@ -0,0 +1,40 @@
package com.tangem.datasource.api.visa.models.request
import com.squareup.moshi.Json
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,
) {
@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)
data class CardWalletConfirmation(
@Json(name = "challenge") val challenge: String,
@Json(name = "wallet_salt") val walletSalt: String,
@Json(name = "wallet_signature") val walletSignature: String,
@Json(name = "card_salt") val cardSalt: String,
@Json(name = "card_signature") val cardSignature: String,
)
@JsonClass(generateAdapter = true)
data class Otp(
@Json(name = "root_otp") val rootOtp: String,
@Json(name = "counter") val counter: Int,
)
}

View file

@ -0,0 +1,23 @@
package com.tangem.datasource.api.visa.models.request
import com.squareup.moshi.Json
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,
) {
@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,
)
}

View file

@ -5,5 +5,12 @@ import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class CardActivationRemoteStateResponse(
@Json(name = "state") val state: String,
)
@Json(name = "activation_status") val status: String,
@Json(name = "activation_order") val activationOrder: ActivationOrder?,
) {
@JsonClass(generateAdapter = true)
data class ActivationOrder(
@Json(name = "id") val id: String,
@Json(name = "customer_wallet_address") val customerWalletAddress: String,
)
}

View file

@ -0,0 +1,14 @@
package com.tangem.datasource.api.visa.models.response
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class CardWalletDataToSignResponse(
@Json(name = "dataForCardWallet") val dataForCardWallet: Data,
) {
@JsonClass(generateAdapter = true)
data class Data(
@Json(name = "hash") val hash: String,
)
}

View file

@ -0,0 +1,14 @@
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,
)
}

View file

@ -42,9 +42,10 @@ internal object ApiConfigsModule {
@Provides
@IntoSet
fun provideTangemAuthVisaConfig(): ApiConfig = TangemVisaAuth()
fun provideTangemAuthVisaConfig(appVersionProvider: AppVersionProvider): ApiConfig =
TangemVisaAuth(appVersionProvider)
@Provides
@IntoSet
fun provideTangemVisaConfig(): ApiConfig = TangemVisa()
fun provideTangemVisaConfig(appVersionProvider: AppVersionProvider): ApiConfig = TangemVisa(appVersionProvider)
}

View file

@ -10,6 +10,7 @@ import com.tangem.datasource.api.common.adapter.LocalDateAdapter
import com.tangem.datasource.api.common.adapter.addStakeKitEnumFallbackAdapters
import com.tangem.datasource.local.config.providers.models.ProviderModel
import com.tangem.domain.models.scan.serialization.*
import com.tangem.domain.visa.model.VisaActivationRemoteState
import com.tangem.domain.visa.model.VisaCardActivationStatus
import dagger.Module
import dagger.Provides
@ -35,8 +36,9 @@ class MoshiModule {
.add(BigDecimalAdapter())
.add(LocalDateAdapter())
.add(DateTimeAdapter())
.add(VisaActivationRemoteState.jsonAdapter)
.add(VisaCardActivationStatus.jsonAdapter)
.add(KotlinJsonAdapterFactory())
.addLast(KotlinJsonAdapterFactory())
.addStakeKitEnumFallbackAdapters()
.build()
}
@ -59,10 +61,11 @@ class MoshiModule {
val typedAdapters = MoshiJsonConverter.getTangemSdkTypedAdapters()
return Moshi.Builder().apply {
add(VisaActivationRemoteState.jsonAdapter)
add(VisaCardActivationStatus.jsonAdapter)
adapters.forEach { this.add(it) }
typedAdapters.forEach { add(it.key, it.value) }
add(KotlinJsonAdapterFactory())
addLast(KotlinJsonAdapterFactory())
}.build()
}

View file

@ -2,11 +2,16 @@ package com.tangem.datasource.local.visa
interface VisaOTPStorage {
suspend fun saveOTP(cardId: String, otp: ByteArray)
suspend fun saveOTP(cardId: String, data: VisaOtpData)
suspend fun getOTP(cardId: String): ByteArray?
suspend fun getOTP(cardId: String): VisaOtpData?
suspend fun removeOTP(cardId: String)
}
class VisaOtpData(
val rootOTP: ByteArray,
val counter: Int,
)
suspend fun VisaOTPStorage.hasSavedOTP(cardId: String): Boolean = getOTP(cardId) != null

View file

@ -36,8 +36,8 @@ private val API_CONFIGS = setOf(
Express(configManager, expressAuthProvider, appVersionProvider),
TangemTech(appVersionProvider, appAuthProvider),
StakeKit(stakeKitAuthProvider),
TangemVisaAuth(),
TangemVisa(),
TangemVisaAuth(appVersionProvider),
TangemVisa(appVersionProvider),
)
/**
@ -188,6 +188,10 @@ internal class ProdApiConfigsManagerTest(private val model: Model) {
expected = ApiEnvironmentConfig(
environment = ApiEnvironment.STAGE,
baseUrl = "https://api-s.tangem.org/",
headers = mapOf(
"version" to ProviderSuspend { VERSION_NAME },
"platform" to ProviderSuspend { "Android" },
),
),
)
}
@ -198,7 +202,10 @@ internal class ProdApiConfigsManagerTest(private val model: Model) {
expected = ApiEnvironmentConfig(
environment = ApiEnvironment.PROD,
baseUrl = "https://bff.tangem.com/",
headers = mapOf(),
headers = mapOf(
"version" to ProviderSuspend { VERSION_NAME },
"platform" to ProviderSuspend { "Android" },
),
),
)
}