Updated on 2026-08-14
This commit is contained in:
parent
edfe927c7c
commit
820578708c
17 changed files with 290 additions and 1 deletions
|
|
@ -24,6 +24,7 @@ sealed class ApiConfig {
|
|||
Express,
|
||||
TangemTech,
|
||||
StakeKit,
|
||||
TangemVisaAuth,
|
||||
}
|
||||
|
||||
private fun initializeId(): ID {
|
||||
|
|
@ -31,6 +32,7 @@ sealed class ApiConfig {
|
|||
is Express -> ID.Express
|
||||
is TangemTech -> ID.TangemTech
|
||||
is StakeKit -> ID.StakeKit
|
||||
is TangemVisaAuth -> ID.TangemVisaAuth
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,20 @@
|
|||
package com.tangem.datasource.api.common.config
|
||||
|
||||
import com.tangem.utils.Provider
|
||||
|
||||
internal class TangemVisaAuth : ApiConfig() {
|
||||
|
||||
override val defaultEnvironment: ApiEnvironment = ApiEnvironment.STAGE
|
||||
|
||||
override val environmentConfigs = listOf(
|
||||
createStageEnvironment(),
|
||||
)
|
||||
|
||||
private fun createStageEnvironment(): ApiEnvironmentConfig = ApiEnvironmentConfig(
|
||||
environment = ApiEnvironment.STAGE,
|
||||
baseUrl = "https://api-s.tangem.org/",
|
||||
headers = createHeaders(),
|
||||
)
|
||||
|
||||
private fun createHeaders() = mapOf<String, Provider<String>>()
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
package com.tangem.datasource.api.visa
|
||||
|
||||
interface TangemVisaApi {
|
||||
// TODO
|
||||
}
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
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
|
||||
|
||||
interface TangemVisaAuthApi {
|
||||
|
||||
@FormUrlEncoded
|
||||
@Headers("Content-Type: application/x-www-form-urlencoded")
|
||||
@POST("auth/clients/mobile-app-android/nonce-challenge")
|
||||
suspend fun generateNonceByWalletAddress(
|
||||
@Field("customer_id") customerId: String? = null,
|
||||
@Field("customer_wallet_address") customerWalletAddress: String,
|
||||
): GenerateNonceResponse
|
||||
|
||||
@FormUrlEncoded
|
||||
@Headers("Content-Type: application/x-www-form-urlencoded")
|
||||
@POST("auth/clients/mobile-app-android/nonce-challenge")
|
||||
suspend fun generateNonceByCard(
|
||||
@Field("card_id") cardId: String,
|
||||
@Field("card_public_key") cardPublicKey: String,
|
||||
): GenerateNonceResponse
|
||||
|
||||
@FormUrlEncoded
|
||||
@Headers("Content-Type: application/x-www-form-urlencoded")
|
||||
@POST("auth/protocol/openid-connect/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?,
|
||||
): 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
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
package com.tangem.datasource.api.visa.models.response
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class GenerateNonceResponse(
|
||||
@Json(name = "nonce") val nonce: String,
|
||||
@Json(name = "session_id") val sessionId: String,
|
||||
)
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
package com.tangem.datasource.api.visa.models.response
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class JWTResponse(
|
||||
@Json(name = "access_token") val accessToken: String,
|
||||
@Json(name = "expires_in") val expiresIn: Int,
|
||||
@Json(name = "refresh_expires_in") val refreshExpiresIn: Int,
|
||||
@Json(name = "refresh_token") val refreshToken: String,
|
||||
@Json(name = "token_type") val tokenType: String,
|
||||
@Json(name = "not-before-policy") val notBeforePolicy: Int,
|
||||
@Json(name = "session_state") val sessionState: String,
|
||||
@Json(name = "scope") val scope: String,
|
||||
)
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
package com.tangem.datasource.di
|
||||
|
||||
import com.tangem.datasource.api.common.AuthProvider
|
||||
import com.tangem.datasource.api.common.config.ApiConfig
|
||||
import com.tangem.datasource.api.common.config.*
|
||||
import com.tangem.datasource.api.common.config.Express
|
||||
import com.tangem.datasource.api.common.config.StakeKit
|
||||
import com.tangem.datasource.api.common.config.TangemTech
|
||||
|
|
@ -39,4 +39,8 @@ internal object ApiConfigsModule {
|
|||
@IntoSet
|
||||
fun provideTangemTechConfig(appVersionProvider: AppVersionProvider, authProvider: AuthProvider): ApiConfig =
|
||||
TangemTech(appVersionProvider, authProvider)
|
||||
|
||||
@Provides
|
||||
@IntoSet
|
||||
fun provideTangemVisaConfig(): ApiConfig = TangemVisaAuth()
|
||||
}
|
||||
|
|
@ -15,6 +15,7 @@ import com.tangem.datasource.api.onramp.OnrampApi
|
|||
import com.tangem.datasource.api.stakekit.StakeKitApi
|
||||
import com.tangem.datasource.api.tangemTech.TangemTechApi
|
||||
import com.tangem.datasource.api.tangemTech.TangemTechApiV2
|
||||
import com.tangem.datasource.api.visa.TangemVisaAuthApi
|
||||
import com.tangem.datasource.local.logs.AppLogsStore
|
||||
import com.tangem.datasource.local.preferences.AppPreferencesStore
|
||||
import com.tangem.datasource.utils.*
|
||||
|
|
@ -169,6 +170,21 @@ internal object NetworkModule {
|
|||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideTangemVisaApi(
|
||||
@NetworkMoshi moshi: Moshi,
|
||||
@ApplicationContext context: Context,
|
||||
apiConfigsManager: ApiConfigsManager,
|
||||
): TangemVisaAuthApi {
|
||||
return createApi<TangemVisaAuthApi>(
|
||||
id = ApiConfig.ID.TangemVisaAuth,
|
||||
moshi = moshi,
|
||||
context = context,
|
||||
apiConfigsManager = apiConfigsManager,
|
||||
)
|
||||
}
|
||||
|
||||
@Deprecated("use createApi instead")
|
||||
private inline fun <reified T> provideTangemTechApiInternal(
|
||||
moshi: Moshi,
|
||||
|
|
|
|||
|
|
@ -85,6 +85,7 @@ internal class ProdApiConfigsManagerTest(private val model: Model) {
|
|||
is Express -> createExpressModel()
|
||||
is TangemTech -> createTangemTechModel()
|
||||
is StakeKit -> createStakeKitModel()
|
||||
is TangemVisaAuth -> createVisaAuthModel()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -172,6 +173,16 @@ internal class ProdApiConfigsManagerTest(private val model: Model) {
|
|||
)
|
||||
}
|
||||
|
||||
private fun createVisaAuthModel(): Model {
|
||||
return Model(
|
||||
id = ApiConfig.ID.TangemVisaAuth,
|
||||
expected = ApiEnvironmentConfig(
|
||||
environment = ApiEnvironment.STAGE,
|
||||
baseUrl = "https://api-s.tangem.org/",
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun String.checkHeaderValueOrEmpty(): String {
|
||||
for (i in this.indices) {
|
||||
val c = this[i]
|
||||
|
|
|
|||
|
|
@ -0,0 +1,70 @@
|
|||
package com.tangem.data.visa
|
||||
|
||||
import com.tangem.datasource.api.visa.TangemVisaAuthApi
|
||||
import com.tangem.domain.visa.model.VisaAuthChallenge
|
||||
import com.tangem.domain.visa.model.VisaAuthSession
|
||||
import com.tangem.domain.visa.model.VisaAuthSignedChallenge
|
||||
import com.tangem.domain.visa.model.VisaAuthTokens
|
||||
import com.tangem.domain.visa.repository.VisaAuthRepository
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.withContext
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class DefaultVisaAuthRepository @Inject constructor(
|
||||
private val visaAuthApi: TangemVisaAuthApi,
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
) : VisaAuthRepository {
|
||||
|
||||
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),
|
||||
)
|
||||
}
|
||||
|
||||
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),
|
||||
)
|
||||
}
|
||||
|
||||
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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,10 @@
|
|||
package com.tangem.data.visa.di
|
||||
|
||||
import com.tangem.data.visa.DefaultVisaAuthRepository
|
||||
import com.tangem.data.visa.DummyVisaRepository
|
||||
import com.tangem.domain.visa.repository.VisaAuthRepository
|
||||
import com.tangem.domain.visa.repository.VisaRepository
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
|
|
@ -21,4 +24,13 @@ internal object VisaDataModule {
|
|||
): VisaRepository {
|
||||
return implementedVisaRepository.getOrNull() ?: DummyVisaRepository()
|
||||
}
|
||||
}
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
internal interface VisaDataBindsModule {
|
||||
|
||||
@Binds
|
||||
@Singleton
|
||||
fun bindVisaAuthRepository(repository: DefaultVisaAuthRepository): VisaAuthRepository
|
||||
}
|
||||
|
|
@ -20,4 +20,6 @@ dependencies {
|
|||
/** Libs - Other */
|
||||
implementation(deps.jodatime)
|
||||
implementation(deps.androidx.paging.runtime)
|
||||
implementation(deps.moshi)
|
||||
implementation(deps.moshi.kotlin)
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
package com.tangem.domain.visa.model
|
||||
|
||||
sealed class VisaAuthChallenge {
|
||||
|
||||
abstract val session: VisaAuthSession
|
||||
|
||||
data class Card(
|
||||
val challenge: String,
|
||||
override val session: VisaAuthSession,
|
||||
) : VisaAuthChallenge()
|
||||
|
||||
data class Wallet(
|
||||
val challenge: String,
|
||||
override val session: VisaAuthSession,
|
||||
) : VisaAuthChallenge()
|
||||
}
|
||||
|
||||
fun VisaAuthChallenge.Card.toSignedChallenge(signedChallenge: String, salt: String): VisaAuthSignedChallenge {
|
||||
return VisaAuthSignedChallenge.ByCardPublicKey(
|
||||
challenge = this,
|
||||
signature = signedChallenge,
|
||||
salt = salt,
|
||||
)
|
||||
}
|
||||
|
||||
fun VisaAuthChallenge.Wallet.toSignedChallenge(signedChallenge: String): VisaAuthSignedChallenge {
|
||||
return VisaAuthSignedChallenge.ByWallet(
|
||||
challenge = this,
|
||||
signature = signedChallenge,
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
package com.tangem.domain.visa.model
|
||||
|
||||
@JvmInline
|
||||
value class VisaAuthSession(val sessionId: String)
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
package com.tangem.domain.visa.model
|
||||
|
||||
sealed class VisaAuthSignedChallenge {
|
||||
|
||||
data class ByCardPublicKey(
|
||||
val challenge: VisaAuthChallenge.Card,
|
||||
val signature: String,
|
||||
val salt: String,
|
||||
) : VisaAuthSignedChallenge()
|
||||
|
||||
data class ByWallet(
|
||||
val challenge: VisaAuthChallenge.Wallet,
|
||||
val signature: String,
|
||||
) : VisaAuthSignedChallenge()
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
package com.tangem.domain.visa.model
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class VisaAuthTokens(
|
||||
@Json(name = "accessToken") val accessToken: String,
|
||||
@Json(name = "refreshToken") val refreshToken: String,
|
||||
)
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
package com.tangem.domain.visa.repository
|
||||
|
||||
import com.tangem.domain.visa.model.VisaAuthChallenge
|
||||
import com.tangem.domain.visa.model.VisaAuthSignedChallenge
|
||||
import com.tangem.domain.visa.model.VisaAuthTokens
|
||||
|
||||
interface VisaAuthRepository {
|
||||
|
||||
suspend fun getCardAuthChallenge(cardId: String, cardPublicKey: String): VisaAuthChallenge.Card
|
||||
|
||||
suspend fun getCustomerWalletAuthChallenge(cardId: String, walletPublicKey: String): VisaAuthChallenge.Wallet
|
||||
|
||||
suspend fun getAccessTokens(signedChallenge: VisaAuthSignedChallenge): VisaAuthTokens
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue