From 820578708cb50f162687d6752c2595da043bd294 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 9 Jan 2025 13:04:15 +0300 Subject: [PATCH] Updated on 2026-08-14 --- .../datasource/api/common/config/ApiConfig.kt | 2 + .../api/common/config/TangemVisaAuth.kt | 20 ++++++ .../datasource/api/visa/TangemVisaApi.kt | 5 ++ .../datasource/api/visa/TangemVisaAuthApi.kt | 47 +++++++++++++ .../models/response/GenerateNonceResponse.kt | 10 +++ .../api/visa/models/response/JWTResponse.kt | 16 +++++ .../tangem/datasource/di/ApiConfigsModule.kt | 6 +- .../com/tangem/datasource/di/NetworkModule.kt | 16 +++++ .../managers/ProdApiConfigsManagerTest.kt | 11 +++ .../data/visa/DefaultVisaAuthRepository.kt | 70 +++++++++++++++++++ .../com/tangem/data/visa/di/VisaDataModule.kt | 12 ++++ domain/visa/build.gradle.kts | 2 + .../domain/visa/model/VisaAuthChallenge.kt | 31 ++++++++ .../domain/visa/model/VisaAuthSession.kt | 4 ++ .../visa/model/VisaAuthSignedChallenge.kt | 15 ++++ .../domain/visa/model/VisaAuthTokens.kt | 10 +++ .../visa/repository/VisaAuthRepository.kt | 14 ++++ 17 files changed, 290 insertions(+), 1 deletion(-) create mode 100644 core/datasource/src/main/java/com/tangem/datasource/api/common/config/TangemVisaAuth.kt create mode 100644 core/datasource/src/main/java/com/tangem/datasource/api/visa/TangemVisaApi.kt create mode 100644 core/datasource/src/main/java/com/tangem/datasource/api/visa/TangemVisaAuthApi.kt create mode 100644 core/datasource/src/main/java/com/tangem/datasource/api/visa/models/response/GenerateNonceResponse.kt create mode 100644 core/datasource/src/main/java/com/tangem/datasource/api/visa/models/response/JWTResponse.kt create mode 100644 data/visa/src/main/kotlin/com/tangem/data/visa/DefaultVisaAuthRepository.kt create mode 100644 domain/visa/src/main/kotlin/com/tangem/domain/visa/model/VisaAuthChallenge.kt create mode 100644 domain/visa/src/main/kotlin/com/tangem/domain/visa/model/VisaAuthSession.kt create mode 100644 domain/visa/src/main/kotlin/com/tangem/domain/visa/model/VisaAuthSignedChallenge.kt create mode 100644 domain/visa/src/main/kotlin/com/tangem/domain/visa/model/VisaAuthTokens.kt create mode 100644 domain/visa/src/main/kotlin/com/tangem/domain/visa/repository/VisaAuthRepository.kt diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/config/ApiConfig.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/config/ApiConfig.kt index 65bc7a1e66..a31ec2e6d8 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/common/config/ApiConfig.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/config/ApiConfig.kt @@ -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 } } diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/config/TangemVisaAuth.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/config/TangemVisaAuth.kt new file mode 100644 index 0000000000..102bf875d4 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/config/TangemVisaAuth.kt @@ -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>() +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/visa/TangemVisaApi.kt b/core/datasource/src/main/java/com/tangem/datasource/api/visa/TangemVisaApi.kt new file mode 100644 index 0000000000..cd538c51eb --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/visa/TangemVisaApi.kt @@ -0,0 +1,5 @@ +package com.tangem.datasource.api.visa + +interface TangemVisaApi { + // TODO +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/visa/TangemVisaAuthApi.kt b/core/datasource/src/main/java/com/tangem/datasource/api/visa/TangemVisaAuthApi.kt new file mode 100644 index 0000000000..858390cf7c --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/visa/TangemVisaAuthApi.kt @@ -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 +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/visa/models/response/GenerateNonceResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/visa/models/response/GenerateNonceResponse.kt new file mode 100644 index 0000000000..002e43b16e --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/visa/models/response/GenerateNonceResponse.kt @@ -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, +) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/visa/models/response/JWTResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/visa/models/response/JWTResponse.kt new file mode 100644 index 0000000000..e4fc0d2579 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/visa/models/response/JWTResponse.kt @@ -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, +) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/di/ApiConfigsModule.kt b/core/datasource/src/main/java/com/tangem/datasource/di/ApiConfigsModule.kt index c5f506627e..7daabbc3e8 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/di/ApiConfigsModule.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/di/ApiConfigsModule.kt @@ -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() } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/di/NetworkModule.kt b/core/datasource/src/main/java/com/tangem/datasource/di/NetworkModule.kt index 8b38ca983a..84cc46d45c 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/di/NetworkModule.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/di/NetworkModule.kt @@ -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( + id = ApiConfig.ID.TangemVisaAuth, + moshi = moshi, + context = context, + apiConfigsManager = apiConfigsManager, + ) + } + @Deprecated("use createApi instead") private inline fun provideTangemTechApiInternal( moshi: Moshi, diff --git a/core/datasource/src/test/kotlin/com/tangem/datasource/api/common/config/managers/ProdApiConfigsManagerTest.kt b/core/datasource/src/test/kotlin/com/tangem/datasource/api/common/config/managers/ProdApiConfigsManagerTest.kt index 7c874508d8..db06842d74 100644 --- a/core/datasource/src/test/kotlin/com/tangem/datasource/api/common/config/managers/ProdApiConfigsManagerTest.kt +++ b/core/datasource/src/test/kotlin/com/tangem/datasource/api/common/config/managers/ProdApiConfigsManagerTest.kt @@ -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] diff --git a/data/visa/src/main/kotlin/com/tangem/data/visa/DefaultVisaAuthRepository.kt b/data/visa/src/main/kotlin/com/tangem/data/visa/DefaultVisaAuthRepository.kt new file mode 100644 index 0000000000..a0fb323d81 --- /dev/null +++ b/data/visa/src/main/kotlin/com/tangem/data/visa/DefaultVisaAuthRepository.kt @@ -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, + ) + } +} \ No newline at end of file diff --git a/data/visa/src/main/kotlin/com/tangem/data/visa/di/VisaDataModule.kt b/data/visa/src/main/kotlin/com/tangem/data/visa/di/VisaDataModule.kt index a8587868f9..8299b95fb4 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/visa/di/VisaDataModule.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/visa/di/VisaDataModule.kt @@ -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 } \ No newline at end of file diff --git a/domain/visa/build.gradle.kts b/domain/visa/build.gradle.kts index 8032c2e995..d5f66c4889 100644 --- a/domain/visa/build.gradle.kts +++ b/domain/visa/build.gradle.kts @@ -20,4 +20,6 @@ dependencies { /** Libs - Other */ implementation(deps.jodatime) implementation(deps.androidx.paging.runtime) + implementation(deps.moshi) + implementation(deps.moshi.kotlin) } \ No newline at end of file diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/visa/model/VisaAuthChallenge.kt b/domain/visa/src/main/kotlin/com/tangem/domain/visa/model/VisaAuthChallenge.kt new file mode 100644 index 0000000000..536b7207dc --- /dev/null +++ b/domain/visa/src/main/kotlin/com/tangem/domain/visa/model/VisaAuthChallenge.kt @@ -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, + ) +} \ No newline at end of file diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/visa/model/VisaAuthSession.kt b/domain/visa/src/main/kotlin/com/tangem/domain/visa/model/VisaAuthSession.kt new file mode 100644 index 0000000000..67437218f5 --- /dev/null +++ b/domain/visa/src/main/kotlin/com/tangem/domain/visa/model/VisaAuthSession.kt @@ -0,0 +1,4 @@ +package com.tangem.domain.visa.model + +@JvmInline +value class VisaAuthSession(val sessionId: String) \ No newline at end of file diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/visa/model/VisaAuthSignedChallenge.kt b/domain/visa/src/main/kotlin/com/tangem/domain/visa/model/VisaAuthSignedChallenge.kt new file mode 100644 index 0000000000..a16916a887 --- /dev/null +++ b/domain/visa/src/main/kotlin/com/tangem/domain/visa/model/VisaAuthSignedChallenge.kt @@ -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() +} \ No newline at end of file diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/visa/model/VisaAuthTokens.kt b/domain/visa/src/main/kotlin/com/tangem/domain/visa/model/VisaAuthTokens.kt new file mode 100644 index 0000000000..83b0a5b091 --- /dev/null +++ b/domain/visa/src/main/kotlin/com/tangem/domain/visa/model/VisaAuthTokens.kt @@ -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, +) \ No newline at end of file diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/visa/repository/VisaAuthRepository.kt b/domain/visa/src/main/kotlin/com/tangem/domain/visa/repository/VisaAuthRepository.kt new file mode 100644 index 0000000000..a280b47b2c --- /dev/null +++ b/domain/visa/src/main/kotlin/com/tangem/domain/visa/repository/VisaAuthRepository.kt @@ -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 +} \ No newline at end of file