Updated on 2026-08-14
This commit is contained in:
parent
113c9cb46e
commit
4192ccab33
34 changed files with 444 additions and 81 deletions
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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<VisaCardActivationStatus> {
|
||||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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!"
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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 ""}") {
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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() }
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
package com.tangem.datasource.api.common.visa
|
||||
|
||||
interface TangemVisaAuthProvider {
|
||||
|
||||
suspend fun getAuthHeader(): String
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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<TangemVisaAuthApi>(
|
||||
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<TangemVisaApi>(
|
||||
id = ApiConfig.ID.TangemVisa,
|
||||
moshi = moshi,
|
||||
context = context,
|
||||
apiConfigsManager = apiConfigsManager,
|
||||
clientBuilder = {
|
||||
addInterceptor(
|
||||
NetworkLogsSaveInterceptor(appLogsStore),
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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?
|
||||
}
|
||||
|
|
@ -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<AppVersionProvider>()
|
|||
private val expressAuthProvider = mockk<ExpressAuthProvider>()
|
||||
private val stakeKitAuthProvider = mockk<StakeKitAuthProvider>()
|
||||
private val appAuthProvider = mockk<AuthProvider>()
|
||||
private val visaAuthProvider = mockk<TangemVisaAuthProvider>()
|
||||
|
||||
// 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]
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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<CardActivationRemoteStateResponse, VisaActivationRemoteState> {
|
||||
|
||||
override fun convert(value: CardActivationRemoteStateResponse): VisaActivationRemoteState {
|
||||
// TODO Will be implemented in the future
|
||||
return VisaActivationRemoteState.Activated
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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"),
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
package com.tangem.domain.visa.model
|
||||
|
||||
@JvmInline
|
||||
value class VisaAuthSession(val sessionId: String)
|
||||
|
|
@ -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()
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
package com.tangem.domain.visa.repository
|
||||
|
||||
import com.tangem.domain.visa.model.VisaActivationRemoteState
|
||||
|
||||
interface VisaActivationRepository {
|
||||
|
||||
suspend fun getActivationRemoteState(): VisaActivationRemoteState
|
||||
}
|
||||
|
|
@ -69,6 +69,10 @@ internal class OnboardingEntryModel @Inject constructor(
|
|||
onDone = ::onMultiWalletOnboardingDone,
|
||||
mode = multiWalletNavigationMode,
|
||||
)
|
||||
ProductType.Visa -> OnboardingRoute.Visa(
|
||||
scanResponse = scanResponse,
|
||||
titleProvider = titleProvider,
|
||||
)
|
||||
else -> error("Unsupported")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -47,6 +47,11 @@ internal inline fun OnboardingEntry(
|
|||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
is OnboardingRoute.Visa -> {
|
||||
(it.instance as ComposableContentComponent).Content(
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
OnboardingRoute.None -> {}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<Params, OnboardingVisaComponent>
|
||||
}
|
||||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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<OnboardingVisaAccessCodeComponent.Params>()
|
||||
private val _uiState = MutableStateFlow(getInitialState())
|
||||
val uiState = _uiState.asStateFlow()
|
||||
val onBack = MutableSharedFlow<Unit>()
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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 = {},
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue