diff --git a/app/src/main/java/com/tangem/tap/data/DefaultVisaAuthTokenStorage.kt b/app/src/main/java/com/tangem/tap/data/DefaultVisaAuthTokenStorage.kt new file mode 100644 index 0000000000..7e975e8f02 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/data/DefaultVisaAuthTokenStorage.kt @@ -0,0 +1,54 @@ +package com.tangem.tap.data + +import android.content.Context +import com.squareup.moshi.Moshi +import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory +import com.tangem.common.services.secure.SecureStorage +import com.tangem.datasource.local.visa.VisaAuthTokenStorage +import com.tangem.domain.visa.model.VisaAuthTokens +import com.tangem.sdk.storage.AndroidSecureStorage +import com.tangem.sdk.storage.createEncryptedSharedPreferences +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.coroutines.withContext +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +internal class DefaultVisaAuthTokenStorage @Inject constructor( + @ApplicationContext applicationContext: Context, + private val dispatcherProvider: CoroutineDispatcherProvider, +) : VisaAuthTokenStorage { + + private val secureStorage = AndroidSecureStorage( + preferences = SecureStorage.createEncryptedSharedPreferences( + context = applicationContext, + storageName = "visa_auth_storage", + ), + ) + + private val moshi = Moshi.Builder() + .add(KotlinJsonAdapterFactory()) + .build() + + private val tokensAdapter = moshi.adapter(VisaAuthTokens::class.java) + + override suspend fun store(tokens: VisaAuthTokens) = withContext(dispatcherProvider.io) { + val json = tokensAdapter.toJson(tokens) + + secureStorage.store( + json.encodeToByteArray(throwOnInvalidSequence = true), + VISA_AUTH_TOKENS_KEY, + ) + } + + override suspend fun get(): VisaAuthTokens? = withContext(dispatcherProvider.io) { + secureStorage.get(VISA_AUTH_TOKENS_KEY) + ?.decodeToString(throwOnInvalidSequence = true) + ?.let(tokensAdapter::fromJson) + } + + private companion object { + const val VISA_AUTH_TOKENS_KEY = "visa_auth_tokens" + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/di/data/VisaStorageModule.kt b/app/src/main/java/com/tangem/tap/di/data/VisaStorageModule.kt new file mode 100644 index 0000000000..7d04ce793d --- /dev/null +++ b/app/src/main/java/com/tangem/tap/di/data/VisaStorageModule.kt @@ -0,0 +1,18 @@ +package com.tangem.tap.di.data + +import com.tangem.datasource.local.visa.VisaAuthTokenStorage +import com.tangem.tap.data.DefaultVisaAuthTokenStorage +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal interface VisaStorageModule { + + @Binds + @Singleton + fun bindVisaStorage(impl: DefaultVisaAuthTokenStorage): VisaAuthTokenStorage +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/visa/VisaCardScanHandler.kt b/app/src/main/java/com/tangem/tap/domain/visa/VisaCardScanHandler.kt index efea30664c..c231b0ba49 100644 --- a/app/src/main/java/com/tangem/tap/domain/visa/VisaCardScanHandler.kt +++ b/app/src/main/java/com/tangem/tap/domain/visa/VisaCardScanHandler.kt @@ -7,11 +7,10 @@ import com.tangem.common.core.TangemSdkError import com.tangem.common.extensions.toHexString import com.tangem.crypto.hdWallet.DerivationPath import com.tangem.crypto.hdWallet.bip32.ExtendedPublicKey +import com.tangem.datasource.local.visa.VisaAuthTokenStorage import com.tangem.domain.common.visa.VisaUtilities -import com.tangem.domain.visa.model.VisaActivationInput -import com.tangem.domain.visa.model.VisaAuthSignedChallenge -import com.tangem.domain.visa.model.VisaCardActivationStatus -import com.tangem.domain.visa.model.toSignedChallenge +import com.tangem.domain.visa.model.* +import com.tangem.domain.visa.repository.VisaActivationRepository import com.tangem.domain.visa.repository.VisaAuthRepository import com.tangem.operations.attestation.AttestCardKeyCommand import com.tangem.operations.attestation.AttestCardKeyResponse @@ -25,6 +24,8 @@ import kotlin.coroutines.resume internal class VisaCardScanHandler @Inject constructor( private val visaAuthRepository: VisaAuthRepository, + private val visaActivationRepository: VisaActivationRepository, + private val visaAuthTokenStorage: VisaAuthTokenStorage, ) { suspend fun handleVisaCardScan(session: CardSession): CompletionResult { @@ -153,6 +154,8 @@ internal class VisaCardScanHandler @Inject constructor( return handleCardAuthorization(session) } + visaAuthTokenStorage.store(authorizationTokensResponse) + Timber.i("Authorized using Wallet public key successfully") return CompletionResult.Success(VisaCardActivationStatus.Activated(authorizationTokensResponse)) @@ -207,7 +210,33 @@ internal class VisaCardScanHandler @Inject constructor( ) } - TODO() // implement card activation status handling ([REDACTED_TASK_KEY]) + visaAuthTokenStorage.store(authorizationTokensResponse) + + val activationRemoteState = visaActivationRepository.getActivationRemoteState() + + val error = when (activationRemoteState) { + VisaActivationRemoteState.BlockedForActivation -> VisaActivationError.BlockedForActivation + VisaActivationRemoteState.Activated -> VisaActivationError.InvalidActivationState + else -> null + } + + if (error != null) { + return CompletionResult.Failure(TangemSdkError.Underlying(error.message)) + } + + val activationInput = VisaActivationInput( + cardId = card.cardId, + cardPublicKey = card.cardPublicKey, + isAccessCodeSet = card.isAccessCodeSet, + ) + + return CompletionResult.Success( + VisaCardActivationStatus.ActivationStarted( + activationInput = activationInput, + authTokens = authorizationTokensResponse, + remoteState = activationRemoteState, + ), + ) } private suspend fun signChallengeWithWallet( diff --git a/app/src/main/java/com/tangem/tap/network/auth/DefaultVisaAuthProvider.kt b/app/src/main/java/com/tangem/tap/network/auth/DefaultVisaAuthProvider.kt new file mode 100644 index 0000000000..6685813017 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/network/auth/DefaultVisaAuthProvider.kt @@ -0,0 +1,14 @@ +package com.tangem.tap.network.auth + +import com.tangem.datasource.api.common.visa.TangemVisaAuthProvider +import com.tangem.datasource.local.visa.VisaAuthTokenStorage +import javax.inject.Inject + +internal class DefaultVisaAuthProvider @Inject constructor( + private val authStorage: VisaAuthTokenStorage, +) : TangemVisaAuthProvider { + + override suspend fun getAuthHeader(): String { + return authStorage.get()?.accessToken?.let { "Bearer $it" } ?: "Error in the app!" + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/network/auth/di/AuthModule.kt b/app/src/main/java/com/tangem/tap/network/auth/di/AuthModule.kt index 62e564e8f0..829f08d9cc 100644 --- a/app/src/main/java/com/tangem/tap/network/auth/di/AuthModule.kt +++ b/app/src/main/java/com/tangem/tap/network/auth/di/AuthModule.kt @@ -1,17 +1,21 @@ package com.tangem.tap.network.auth.di import com.tangem.datasource.api.common.AuthProvider +import com.tangem.datasource.api.common.visa.TangemVisaAuthProvider import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.lib.auth.ExpressAuthProvider import com.tangem.lib.auth.StakeKitAuthProvider +import com.tangem.tap.network.auth.* import com.tangem.tap.network.auth.DefaultAppVersionProvider import com.tangem.tap.network.auth.DefaultAuthProvider import com.tangem.tap.network.auth.DefaultExpressAuthProvider import com.tangem.tap.network.auth.DefaultStakeKitAuthProvider +import com.tangem.tap.network.auth.DefaultVisaAuthProvider import com.tangem.utils.version.AppVersionProvider +import dagger.Binds import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -20,7 +24,7 @@ import javax.inject.Singleton @Module @InstallIn(SingletonComponent::class) -class AuthModule { +internal class AuthModule { @Provides @Singleton @@ -51,4 +55,13 @@ class AuthModule { fun provideAppVersionProvider(): AppVersionProvider { return DefaultAppVersionProvider() } +} + +@Module +@InstallIn(SingletonComponent::class) +internal interface AuthBindModule { + + @Binds + @Singleton + fun bindVisaAuthProvider(authStorage: DefaultVisaAuthProvider): TangemVisaAuthProvider } \ No newline at end of file diff --git a/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt b/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt index d5993f39b7..c81997cf54 100644 --- a/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt +++ b/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt @@ -323,11 +323,16 @@ sealed class AppRoute(val path: String) : Route { val userWalletId: UserWalletId, ) : AppRoute(path = "/swap_crypto/${userWalletId.stringValue}") - // Onboarding V2 + /** + * Onboarding V2 + * @property scanResponse scan response, determines onboarding route by the product type + * @property startFromBackup (MultiWallet param, doesn't affect other types) start onboarding from backup + * @property mode (MultiWallet param, doesn't affect other types) onboarding mode + */ @Serializable data class Onboarding( val scanResponse: ScanResponse, - val startFromBackup: Boolean, + val startFromBackup: Boolean = false, val mode: Mode = Mode.Onboarding, ) : AppRoute(path = "/onboarding_v2${if (startFromBackup) "/backup" else ""}") { 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 a31ec2e6d8..100cc04382 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 @@ -25,6 +25,7 @@ sealed class ApiConfig { TangemTech, StakeKit, TangemVisaAuth, + TangemVisa, } private fun initializeId(): ID { @@ -33,6 +34,7 @@ sealed class ApiConfig { is TangemTech -> ID.TangemTech is StakeKit -> ID.StakeKit is TangemVisaAuth -> ID.TangemVisaAuth + is TangemVisa -> ID.TangemVisa } } diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/config/TangemVisa.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/config/TangemVisa.kt new file mode 100644 index 0000000000..4497034a6e --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/config/TangemVisa.kt @@ -0,0 +1,30 @@ +package com.tangem.datasource.api.common.config + +import com.tangem.datasource.api.common.visa.TangemVisaAuthProvider +import com.tangem.utils.Provider +import kotlinx.coroutines.runBlocking + +internal class TangemVisa( + private val authProvider: TangemVisaAuthProvider, +) : ApiConfig() { + + override val defaultEnvironment: ApiEnvironment = ApiEnvironment.PROD + + override val environmentConfigs = listOf( + createProdEnvironment(), + ) + + private fun createProdEnvironment(): ApiEnvironmentConfig = ApiEnvironmentConfig( + environment = ApiEnvironment.PROD, + baseUrl = "https://bff.tangem.com/", + headers = createHeaders(), + ) + + private fun createHeaders() = mapOf( + "Authorization" to Provider { + // This is safe because it's used by interceptor which runs on the IO thread + // (maybe change to ProviderSuspend implementation) + runBlocking { authProvider.getAuthHeader() } + }, + ) +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/visa/TangemVisaAuthProvider.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/visa/TangemVisaAuthProvider.kt new file mode 100644 index 0000000000..62197282df --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/visa/TangemVisaAuthProvider.kt @@ -0,0 +1,6 @@ +package com.tangem.datasource.api.common.visa + +interface TangemVisaAuthProvider { + + suspend fun getAuthHeader(): String +} \ 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 index cd538c51eb..afdded620a 100644 --- 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 @@ -1,5 +1,10 @@ package com.tangem.datasource.api.visa +import com.tangem.datasource.api.visa.models.response.CardActivationRemoteStateResponse +import retrofit2.http.GET + interface TangemVisaApi { - // TODO + + @GET("activation-status") + suspend fun getRemoteActivationStatus(): CardActivationRemoteStateResponse } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/visa/models/response/CardActivationRemoteStateResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/visa/models/response/CardActivationRemoteStateResponse.kt new file mode 100644 index 0000000000..1b4c053a91 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/visa/models/response/CardActivationRemoteStateResponse.kt @@ -0,0 +1,9 @@ +package com.tangem.datasource.api.visa.models.response + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class CardActivationRemoteStateResponse( + @Json(name = "state") val state: 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 7daabbc3e8..a9576c3042 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 @@ -5,6 +5,7 @@ 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 +import com.tangem.datasource.api.common.visa.TangemVisaAuthProvider import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage import com.tangem.lib.auth.ExpressAuthProvider import com.tangem.lib.auth.StakeKitAuthProvider @@ -42,5 +43,10 @@ internal object ApiConfigsModule { @Provides @IntoSet - fun provideTangemVisaConfig(): ApiConfig = TangemVisaAuth() + fun provideTangemAuthVisaConfig(): ApiConfig = TangemVisaAuth() + + @Provides + @IntoSet + fun provideTangemVisaConfig(tangemVisaAuthProvider: TangemVisaAuthProvider): ApiConfig = + TangemVisa(tangemVisaAuthProvider) } \ 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 84cc46d45c..ae75ab68d8 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.TangemVisaApi import com.tangem.datasource.api.visa.TangemVisaAuthApi import com.tangem.datasource.local.logs.AppLogsStore import com.tangem.datasource.local.preferences.AppPreferencesStore @@ -172,16 +173,43 @@ internal object NetworkModule { @Provides @Singleton - fun provideTangemVisaApi( + fun provideTangemVisaAuthApi( @NetworkMoshi moshi: Moshi, @ApplicationContext context: Context, apiConfigsManager: ApiConfigsManager, + appLogsStore: AppLogsStore, ): TangemVisaAuthApi { return createApi( id = ApiConfig.ID.TangemVisaAuth, moshi = moshi, context = context, apiConfigsManager = apiConfigsManager, + clientBuilder = { + addInterceptor( + NetworkLogsSaveInterceptor(appLogsStore), + ) + }, + ) + } + + @Provides + @Singleton + fun provideTangemVisaApi( + @NetworkMoshi moshi: Moshi, + @ApplicationContext context: Context, + apiConfigsManager: ApiConfigsManager, + appLogsStore: AppLogsStore, + ): TangemVisaApi { + return createApi( + id = ApiConfig.ID.TangemVisa, + moshi = moshi, + context = context, + apiConfigsManager = apiConfigsManager, + clientBuilder = { + addInterceptor( + NetworkLogsSaveInterceptor(appLogsStore), + ) + }, ) } diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/visa/VisaAuthTokenStorage.kt b/core/datasource/src/main/java/com/tangem/datasource/local/visa/VisaAuthTokenStorage.kt new file mode 100644 index 0000000000..92ffe1073b --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/local/visa/VisaAuthTokenStorage.kt @@ -0,0 +1,10 @@ +package com.tangem.datasource.local.visa + +import com.tangem.domain.visa.model.VisaAuthTokens + +interface VisaAuthTokenStorage { + + suspend fun store(tokens: VisaAuthTokens) + + suspend fun get(): VisaAuthTokens? +} \ No newline at end of file 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 db06842d74..fc392118f3 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 @@ -10,12 +10,14 @@ import com.tangem.datasource.api.common.config.ApiConfig.Companion.EXTERNAL_BUIL import com.tangem.datasource.api.common.config.ApiConfig.Companion.INTERNAL_BUILD_TYPE import com.tangem.datasource.api.common.config.ApiConfig.Companion.MOCKED_BUILD_TYPE import com.tangem.datasource.api.common.config.ApiConfig.Companion.RELEASE_BUILD_TYPE +import com.tangem.datasource.api.common.visa.TangemVisaAuthProvider import com.tangem.lib.auth.ExpressAuthProvider import com.tangem.lib.auth.StakeKitAuthProvider import com.tangem.utils.Provider import com.tangem.utils.version.AppVersionProvider import io.mockk.every import io.mockk.mockk +import kotlinx.coroutines.runBlocking import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @@ -28,6 +30,7 @@ private val appVersionProvider = mockk() private val expressAuthProvider = mockk() private val stakeKitAuthProvider = mockk() private val appAuthProvider = mockk() +private val visaAuthProvider = mockk() // Don't forget to add new config !!! private val API_CONFIGS = setOf( @@ -53,6 +56,7 @@ internal class ProdApiConfigsManagerTest(private val model: Model) { every { stakeKitAuthProvider.getApiKey() } returns STAKE_KIT_API_KEY every { appAuthProvider.getCardId() } returns APP_CARD_ID every { appAuthProvider.getCardPublicKey() } returns APP_CARD_PUBLIC_KEY + every { runBlocking { visaAuthProvider.getAuthHeader() } } returns VISA_AUTH_HEADER // TODO } @Test @@ -77,6 +81,7 @@ internal class ProdApiConfigsManagerTest(private val model: Model) { const val STAKE_KIT_API_KEY = "stake_kit_api_key" const val APP_CARD_ID = "app_card_id" const val APP_CARD_PUBLIC_KEY = "app_public_key" + const val VISA_AUTH_HEADER = "Bearer visa_auth_header" @JvmStatic @Parameterized.Parameters @@ -86,6 +91,7 @@ internal class ProdApiConfigsManagerTest(private val model: Model) { is TangemTech -> createTangemTechModel() is StakeKit -> createStakeKitModel() is TangemVisaAuth -> createVisaAuthModel() + is TangemVisa -> createVisaModel() } } @@ -183,6 +189,19 @@ internal class ProdApiConfigsManagerTest(private val model: Model) { ) } + private fun createVisaModel(): Model { + return Model( + id = ApiConfig.ID.TangemVisa, + expected = ApiEnvironmentConfig( + environment = ApiEnvironment.PROD, + baseUrl = "https://bff.tangem.com/", + headers = mapOf( + "Authorization" to Provider { VISA_AUTH_HEADER }, + ), + ), + ) + } + 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/DefaultVisaActivationRepository.kt b/data/visa/src/main/kotlin/com/tangem/data/visa/DefaultVisaActivationRepository.kt new file mode 100644 index 0000000000..5bab1a1690 --- /dev/null +++ b/data/visa/src/main/kotlin/com/tangem/data/visa/DefaultVisaActivationRepository.kt @@ -0,0 +1,23 @@ +package com.tangem.data.visa + +import com.tangem.data.visa.converter.VisaActivationStatusConverter +import com.tangem.datasource.api.visa.TangemVisaApi +import com.tangem.domain.visa.model.VisaActivationRemoteState +import com.tangem.domain.visa.repository.VisaActivationRepository +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.withContext +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +internal class DefaultVisaActivationRepository @Inject constructor( + private val visaApi: TangemVisaApi, + private val dispatcherProvider: CoroutineDispatcherProvider, + private val visaActivationStatusConverter: VisaActivationStatusConverter, +) : VisaActivationRepository { + + override suspend fun getActivationRemoteState(): VisaActivationRemoteState = withContext(dispatcherProvider.io) { + visaActivationStatusConverter.convert(visaApi.getRemoteActivationStatus()) + // TODO implement refreshing access token if it's expired + } +} \ No newline at end of file diff --git a/data/visa/src/main/kotlin/com/tangem/data/visa/converter/VisaActivationStatusConverter.kt b/data/visa/src/main/kotlin/com/tangem/data/visa/converter/VisaActivationStatusConverter.kt new file mode 100644 index 0000000000..d82a09116a --- /dev/null +++ b/data/visa/src/main/kotlin/com/tangem/data/visa/converter/VisaActivationStatusConverter.kt @@ -0,0 +1,15 @@ +package com.tangem.data.visa.converter + +import com.tangem.datasource.api.visa.models.response.CardActivationRemoteStateResponse +import com.tangem.domain.visa.model.VisaActivationRemoteState +import com.tangem.utils.converter.Converter +import javax.inject.Inject + +class VisaActivationStatusConverter @Inject constructor() : + Converter { + + override fun convert(value: CardActivationRemoteStateResponse): VisaActivationRemoteState { + // TODO Will be implemented in the future + return VisaActivationRemoteState.Activated + } +} \ 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 8299b95fb4..1ffd8cec35 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,9 @@ package com.tangem.data.visa.di +import com.tangem.data.visa.DefaultVisaActivationRepository import com.tangem.data.visa.DefaultVisaAuthRepository import com.tangem.data.visa.DummyVisaRepository +import com.tangem.domain.visa.repository.VisaActivationRepository import com.tangem.domain.visa.repository.VisaAuthRepository import com.tangem.domain.visa.repository.VisaRepository import dagger.Binds @@ -33,4 +35,8 @@ internal interface VisaDataBindsModule { @Binds @Singleton fun bindVisaAuthRepository(repository: DefaultVisaAuthRepository): VisaAuthRepository + + @Binds + @Singleton + fun bindVisaActivationRepository(repository: DefaultVisaActivationRepository): VisaActivationRepository } \ No newline at end of file diff --git a/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/VisaActivationError.kt b/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/VisaActivationError.kt new file mode 100644 index 0000000000..410b9f0b52 --- /dev/null +++ b/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/VisaActivationError.kt @@ -0,0 +1,6 @@ +package com.tangem.domain.visa.model + +enum class VisaActivationError(val message: String) { + BlockedForActivation("Card is blocked for activation"), + InvalidActivationState("Invalid activation state"), +} \ 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 deleted file mode 100644 index 536b7207dc..0000000000 --- a/domain/visa/src/main/kotlin/com/tangem/domain/visa/model/VisaAuthChallenge.kt +++ /dev/null @@ -1,31 +0,0 @@ -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 deleted file mode 100644 index 67437218f5..0000000000 --- a/domain/visa/src/main/kotlin/com/tangem/domain/visa/model/VisaAuthSession.kt +++ /dev/null @@ -1,4 +0,0 @@ -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 deleted file mode 100644 index a16916a887..0000000000 --- a/domain/visa/src/main/kotlin/com/tangem/domain/visa/model/VisaAuthSignedChallenge.kt +++ /dev/null @@ -1,15 +0,0 @@ -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 deleted file mode 100644 index 83b0a5b091..0000000000 --- a/domain/visa/src/main/kotlin/com/tangem/domain/visa/model/VisaAuthTokens.kt +++ /dev/null @@ -1,10 +0,0 @@ -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/VisaActivationRepository.kt b/domain/visa/src/main/kotlin/com/tangem/domain/visa/repository/VisaActivationRepository.kt new file mode 100644 index 0000000000..12db7d7bbe --- /dev/null +++ b/domain/visa/src/main/kotlin/com/tangem/domain/visa/repository/VisaActivationRepository.kt @@ -0,0 +1,8 @@ +package com.tangem.domain.visa.repository + +import com.tangem.domain.visa.model.VisaActivationRemoteState + +interface VisaActivationRepository { + + suspend fun getActivationRemoteState(): VisaActivationRemoteState +} \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/entry/impl/model/OnboardingEntryModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/entry/impl/model/OnboardingEntryModel.kt index 5bf0212a80..ef564c6194 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/entry/impl/model/OnboardingEntryModel.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/entry/impl/model/OnboardingEntryModel.kt @@ -69,6 +69,10 @@ internal class OnboardingEntryModel @Inject constructor( onDone = ::onMultiWalletOnboardingDone, mode = multiWalletNavigationMode, ) + ProductType.Visa -> OnboardingRoute.Visa( + scanResponse = scanResponse, + titleProvider = titleProvider, + ) else -> error("Unsupported") } } diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/entry/impl/routing/OnboardingChildFactory.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/entry/impl/routing/OnboardingChildFactory.kt index 3721bc7927..a7488de99a 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/entry/impl/routing/OnboardingChildFactory.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/entry/impl/routing/OnboardingChildFactory.kt @@ -4,12 +4,14 @@ import com.tangem.core.decompose.context.AppComponentContext import com.tangem.features.managetokens.component.OnboardingManageTokensComponent import com.tangem.features.onboarding.v2.done.api.OnboardingDoneComponent import com.tangem.features.onboarding.v2.multiwallet.api.OnboardingMultiWalletComponent +import com.tangem.features.onboarding.v2.visa.api.OnboardingVisaComponent import javax.inject.Inject internal class OnboardingChildFactory @Inject constructor( private val multiWalletComponentFactory: OnboardingMultiWalletComponent.Factory, private val onboardingManageTokensComponentFactory: OnboardingManageTokensComponent.Factory, private val onboardingDoneComponentFactory: OnboardingDoneComponent.Factory, + private val onBoardingVisaComponentFactory: OnboardingVisaComponent.Factory, ) { fun createChild(route: OnboardingRoute, childContext: AppComponentContext, onManageTokensDone: () -> Unit): Any { @@ -24,6 +26,13 @@ internal class OnboardingChildFactory @Inject constructor( mode = route.mode, ), ) + is OnboardingRoute.Visa -> onBoardingVisaComponentFactory.create( + context = childContext, + params = OnboardingVisaComponent.Params( + scanResponse = route.scanResponse, + titleProvider = route.titleProvider, + ), + ) is OnboardingRoute.ManageTokens -> onboardingManageTokensComponentFactory.create( context = childContext, params = OnboardingManageTokensComponent.Params( diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/entry/impl/routing/OnboardingRoute.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/entry/impl/routing/OnboardingRoute.kt index 01618cbde2..090724dff0 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/entry/impl/routing/OnboardingRoute.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/entry/impl/routing/OnboardingRoute.kt @@ -18,6 +18,11 @@ sealed class OnboardingRoute : Route { val onDone: (UserWallet) -> Unit, ) : OnboardingRoute() + data class Visa( + val titleProvider: TitleProvider, + val scanResponse: ScanResponse, + ) : OnboardingRoute() + data class ManageTokens( val userWallet: UserWallet, ) : OnboardingRoute() diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/entry/impl/ui/OnboardingEntry.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/entry/impl/ui/OnboardingEntry.kt index 1c174b06a6..e9d2be883a 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/entry/impl/ui/OnboardingEntry.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/entry/impl/ui/OnboardingEntry.kt @@ -47,6 +47,11 @@ internal inline fun OnboardingEntry( modifier = modifier, ) } + is OnboardingRoute.Visa -> { + (it.instance as ComposableContentComponent).Content( + modifier = modifier, + ) + } OnboardingRoute.None -> {} } } diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/api/OnboardingVisaComponent.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/api/OnboardingVisaComponent.kt index eb6a9c353d..19cf83fac8 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/api/OnboardingVisaComponent.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/api/OnboardingVisaComponent.kt @@ -3,11 +3,15 @@ package com.tangem.features.onboarding.v2.visa.api import com.tangem.core.decompose.factory.ComponentFactory import com.tangem.core.decompose.navigation.inner.InnerNavigationHolder import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.domain.models.scan.ScanResponse import com.tangem.features.onboarding.v2.TitleProvider interface OnboardingVisaComponent : ComposableContentComponent, InnerNavigationHolder { - data class Params(val titleProvider: TitleProvider) + data class Params( + val scanResponse: ScanResponse, + val titleProvider: TitleProvider, + ) interface Factory : ComponentFactory } \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/accesscode/OnboardingVisaAccessCodeComponent.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/accesscode/OnboardingVisaAccessCodeComponent.kt index 0a5bb21947..5ffff0e28f 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/accesscode/OnboardingVisaAccessCodeComponent.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/accesscode/OnboardingVisaAccessCodeComponent.kt @@ -2,23 +2,42 @@ package com.tangem.features.onboarding.v2.visa.impl.child.accesscode import androidx.activity.compose.BackHandler import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier +import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.decompose.model.getOrCreateModel import com.tangem.core.ui.decompose.ComposableContentComponent import com.tangem.features.onboarding.v2.visa.impl.child.accesscode.model.OnboardingVisaAccessCodeModel +import com.tangem.features.onboarding.v2.visa.impl.child.accesscode.ui.OnboardingVisaAccessCode import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.launch internal class OnboardingVisaAccessCodeComponent( appComponentContext: AppComponentContext, private val params: Params, ) : ComposableContentComponent, AppComponentContext by appComponentContext { - private val model: OnboardingVisaAccessCodeModel = getOrCreateModel(params) + private val model: OnboardingVisaAccessCodeModel = getOrCreateModel() + + init { + componentScope.launch { + model.onBack.collect { params.onBack() } + } + componentScope.launch { + params.parentBackEvent.collect { + model.onBack() + } + } + } @Composable override fun Content(modifier: Modifier) { + val state by model.uiState.collectAsStateWithLifecycle() + BackHandler(onBack = model::onBack) + + OnboardingVisaAccessCode(state, modifier) } data class Params( diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/accesscode/model/OnboardingVisaAccessCodeModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/accesscode/model/OnboardingVisaAccessCodeModel.kt index d75af43b85..9f99596cfb 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/accesscode/model/OnboardingVisaAccessCodeModel.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/accesscode/model/OnboardingVisaAccessCodeModel.kt @@ -3,22 +3,75 @@ package com.tangem.features.onboarding.v2.visa.impl.child.accesscode.model import androidx.compose.runtime.Stable import com.tangem.core.decompose.di.ComponentScoped import com.tangem.core.decompose.model.Model -import com.tangem.core.decompose.model.ParamsContainer -import com.tangem.features.onboarding.v2.visa.impl.child.accesscode.OnboardingVisaAccessCodeComponent +import com.tangem.features.onboarding.v2.visa.impl.child.accesscode.ui.state.OnboardingVisaAccessCodeUM import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.flow.* +import kotlinx.coroutines.launch import javax.inject.Inject @Stable @ComponentScoped internal class OnboardingVisaAccessCodeModel @Inject constructor( override val dispatchers: CoroutineDispatcherProvider, - private val paramsContainer: ParamsContainer, ) : Model() { - @Suppress("UnusedPrivateMember") - private val params = paramsContainer.require() + private val _uiState = MutableStateFlow(getInitialState()) + val uiState = _uiState.asStateFlow() + val onBack = MutableSharedFlow() fun onBack() { + when (uiState.value.step) { + OnboardingVisaAccessCodeUM.Step.Enter -> modelScope.launch { onBack.emit(Unit) } + OnboardingVisaAccessCodeUM.Step.ReEnter -> + _uiState.update { it.copy(step = OnboardingVisaAccessCodeUM.Step.Enter) } + } + } + + private fun getInitialState(): OnboardingVisaAccessCodeUM { + return OnboardingVisaAccessCodeUM( + onAccessCodeFirstChange = { _uiState.update { it.copy(accessCodeFirst = it.accessCodeFirst) } }, + onAccessCodeSecondChange = { _uiState.update { it.copy(accessCodeSecond = it.accessCodeSecond) } }, + onAccessCodeHideClick = { _uiState.update { it.copy(accessCodeHidden = !it.accessCodeHidden) } }, + onContinue = { onContinue() }, + ) + } + + private fun onContinue() { + if (checkAccessCode().not()) return + + when (uiState.value.step) { + OnboardingVisaAccessCodeUM.Step.Enter -> { + _uiState.update { it.copy(step = OnboardingVisaAccessCodeUM.Step.ReEnter) } + } + OnboardingVisaAccessCodeUM.Step.ReEnter -> { + startActivationProcess() + } + } + } + + private fun checkAccessCode(): Boolean { + val first = uiState.value.accessCodeFirst.text + val second = uiState.value.accessCodeSecond.text + + when { + first.length < ACCESS_CODE_MIN_LENGTH -> { + _uiState.update { it.copy(atLeastMinCharsError = true) } + return false + } + first != second -> { + _uiState.update { it.copy(codesNotMatchError = true) } + return false + } + } + + return true + } + + private fun startActivationProcess() { TODO() } + + private companion object { + const val ACCESS_CODE_MIN_LENGTH = 4 + } } \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/accesscode/ui/OnboardingVisaAccessCode.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/accesscode/ui/OnboardingVisaAccessCode.kt index cf16e457eb..f5c8364420 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/accesscode/ui/OnboardingVisaAccessCode.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/accesscode/ui/OnboardingVisaAccessCode.kt @@ -134,7 +134,7 @@ private fun Content( state.onAccessCodeFirstChange }, label = stringResource(id = R.string.onboarding_wallet_info_title_third), - isError = state.codesNotMatchError || state.atLeast4CharError, + isError = state.codesNotMatchError || state.atLeastMinCharsError, visualTransformation = if (state.accessCodeHidden) { PasswordVisualTransformation() } else { @@ -143,7 +143,7 @@ private fun Content( caption = when { state.codesNotMatchError && reEnterAccessCodeState -> stringResource(R.string.onboarding_access_codes_doesnt_match) - state.atLeast4CharError && !reEnterAccessCodeState -> + state.atLeastMinCharsError && !reEnterAccessCodeState -> stringResource(R.string.onboarding_access_code_too_short) else -> null }, diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/accesscode/ui/state/OnboardingVisaAccessCodeUM.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/accesscode/ui/state/OnboardingVisaAccessCodeUM.kt index 68719daa53..0cf7770ac0 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/accesscode/ui/state/OnboardingVisaAccessCodeUM.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/accesscode/ui/state/OnboardingVisaAccessCodeUM.kt @@ -7,7 +7,7 @@ data class OnboardingVisaAccessCodeUM( val accessCodeFirst: TextFieldValue = TextFieldValue(""), val accessCodeSecond: TextFieldValue = TextFieldValue(""), val codesNotMatchError: Boolean = false, - val atLeast4CharError: Boolean = false, + val atLeastMinCharsError: Boolean = false, val accessCodeHidden: Boolean = true, val onAccessCodeFirstChange: (TextFieldValue) -> Unit = {}, val onAccessCodeSecondChange: (TextFieldValue) -> Unit = {}, diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/di/ComponentModule.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/di/ComponentModule.kt new file mode 100644 index 0000000000..abb9a0cecf --- /dev/null +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/di/ComponentModule.kt @@ -0,0 +1,18 @@ +package com.tangem.features.onboarding.v2.visa.impl.di + +import com.tangem.features.onboarding.v2.visa.api.OnboardingVisaComponent +import com.tangem.features.onboarding.v2.visa.impl.DefaultOnboardingVisaComponent +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal interface ComponentModule { + + @Binds + @Singleton + fun bindComponentFactory(factory: DefaultOnboardingVisaComponent.Factory): OnboardingVisaComponent.Factory +} \ No newline at end of file