From 10bb7699163565dd5fc6e78555967da9968de79c Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 24 Jan 2025 10:06:29 +0300 Subject: [PATCH 1/2] Updated on 2026-08-14 --- .../tap/di/domain/OnrampDomainModule.kt | 7 ++ core/datasource/build.gradle.kts | 1 + .../api/tangemTech/TangemTechApi.kt | 3 + .../tangemTech/models/HotCryptoResponse.kt | 24 ++++++ .../exchangeservice/HotCryptoLoaderModule.kt | 18 +++++ .../hotcrypto/DefaultHotCryptoLoader.kt | 81 +++++++++++++++++++ .../hotcrypto/HotCryptoLoader.kt | 26 ++++++ .../hotcrypto/HotCryptoResponseStore.kt | 16 ++++ data/onramp/build.gradle.kts | 1 + .../data/onramp/DefaultHotCryptoRepository.kt | 41 ++++++++++ .../converters/HotCryptoCurrencyConverter.kt | 78 ++++++++++++++++++ .../tangem/data/onramp/di/OnrampDataModule.kt | 19 +++++ .../tangem/data/tokens/di/TokensDataModule.kt | 3 + .../repository/DefaultCurrenciesRepository.kt | 19 +++-- .../domain/onramp/model/HotCryptoCurrency.kt | 19 +++++ .../domain/onramp/GetHotCryptoUseCase.kt | 22 +++++ .../repositories/HotCryptoRepository.kt | 16 ++++ 17 files changed, 389 insertions(+), 5 deletions(-) create mode 100644 core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/HotCryptoResponse.kt create mode 100644 core/datasource/src/main/java/com/tangem/datasource/di/exchangeservice/HotCryptoLoaderModule.kt create mode 100644 core/datasource/src/main/java/com/tangem/datasource/exchangeservice/hotcrypto/DefaultHotCryptoLoader.kt create mode 100644 core/datasource/src/main/java/com/tangem/datasource/exchangeservice/hotcrypto/HotCryptoLoader.kt create mode 100644 core/datasource/src/main/java/com/tangem/datasource/exchangeservice/hotcrypto/HotCryptoResponseStore.kt create mode 100644 data/onramp/src/main/java/com/tangem/data/onramp/DefaultHotCryptoRepository.kt create mode 100644 data/onramp/src/main/java/com/tangem/data/onramp/converters/HotCryptoCurrencyConverter.kt create mode 100644 domain/onramp/models/src/main/kotlin/com/tangem/domain/onramp/model/HotCryptoCurrency.kt create mode 100644 domain/onramp/src/main/java/com/tangem/domain/onramp/GetHotCryptoUseCase.kt create mode 100644 domain/onramp/src/main/java/com/tangem/domain/onramp/repositories/HotCryptoRepository.kt diff --git a/app/src/main/java/com/tangem/tap/di/domain/OnrampDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/OnrampDomainModule.kt index ca9f43d672..afc7a73f59 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/OnrampDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/OnrampDomainModule.kt @@ -1,6 +1,7 @@ package com.tangem.tap.di.domain import com.tangem.domain.onramp.* +import com.tangem.domain.onramp.repositories.HotCryptoRepository import com.tangem.domain.onramp.repositories.OnrampErrorResolver import com.tangem.domain.onramp.repositories.OnrampRepository import com.tangem.domain.onramp.repositories.OnrampTransactionRepository @@ -229,4 +230,10 @@ internal object OnrampDomainModule { ): FetchOnrampCountriesUseCase { return FetchOnrampCountriesUseCase(onrampRepository, onrampErrorResolver) } + + @Provides + @Singleton + fun provideGetHotCryptoTokensUseCase(hotCryptoRepository: HotCryptoRepository): GetHotCryptoUseCase { + return GetHotCryptoUseCase(hotCryptoRepository) + } } \ No newline at end of file diff --git a/core/datasource/build.gradle.kts b/core/datasource/build.gradle.kts index f886a6b930..128cbb0ab2 100644 --- a/core/datasource/build.gradle.kts +++ b/core/datasource/build.gradle.kts @@ -31,6 +31,7 @@ dependencies { implementation(projects.domain.staking.models) implementation(projects.domain.onramp.models) implementation(projects.domain.models) + implementation(projects.features.onramp.api) /** Tangem libraries */ implementation(deps.tangem.blockchain) diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/TangemTechApi.kt b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/TangemTechApi.kt index 660e465d75..96d2a236d8 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/TangemTechApi.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/TangemTechApi.kt @@ -122,6 +122,9 @@ interface TangemTechApi { @Body body: SeedPhraseNotificationDTO, ): ApiResponse + @GET("hot_crypto") + suspend fun getHotCrypto(@Query("currency") currencyId: String): ApiResponse + companion object { val marketsQuoteFields = listOf( "price", diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/HotCryptoResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/HotCryptoResponse.kt new file mode 100644 index 0000000000..14e00f6187 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/HotCryptoResponse.kt @@ -0,0 +1,24 @@ +package com.tangem.datasource.api.tangemTech.models + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import java.math.BigDecimal + +@JsonClass(generateAdapter = true) +data class HotCryptoResponse( + @Json(name = "imageHost") val imageHost: String? = null, + @Json(name = "tokens") val tokens: List, +) { + + @JsonClass(generateAdapter = true) + data class Token( + @Json(name = "id") val id: String, + @Json(name = "symbol") val symbol: String, + @Json(name = "name") val name: String, + @Json(name = "network_id") val networkId: String, + @Json(name = "current_price") val currentPrice: BigDecimal, + @Json(name = "price_change_percentage_24h") val priceChangePercentage: BigDecimal, + @Json(name = "contractAddress") val contractAddress: String? = null, // specific for token + @Json(name = "decimal_count") val decimalCount: Int? = null, // specific for token + ) +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/di/exchangeservice/HotCryptoLoaderModule.kt b/core/datasource/src/main/java/com/tangem/datasource/di/exchangeservice/HotCryptoLoaderModule.kt new file mode 100644 index 0000000000..2e651928df --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/di/exchangeservice/HotCryptoLoaderModule.kt @@ -0,0 +1,18 @@ +package com.tangem.datasource.di.exchangeservice + +import com.tangem.datasource.exchangeservice.hotcrypto.DefaultHotCryptoLoader +import com.tangem.datasource.exchangeservice.hotcrypto.HotCryptoLoader +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 HotCryptoLoaderModule { + + @Binds + @Singleton + fun bindExpressServiceLoader(defaultHotCryptoLoader: DefaultHotCryptoLoader): HotCryptoLoader +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/exchangeservice/hotcrypto/DefaultHotCryptoLoader.kt b/core/datasource/src/main/java/com/tangem/datasource/exchangeservice/hotcrypto/DefaultHotCryptoLoader.kt new file mode 100644 index 0000000000..4a8260bf8d --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/exchangeservice/hotcrypto/DefaultHotCryptoLoader.kt @@ -0,0 +1,81 @@ +package com.tangem.datasource.exchangeservice.hotcrypto + +import com.tangem.datasource.api.common.response.getOrThrow +import com.tangem.datasource.api.tangemTech.TangemTechApi +import com.tangem.datasource.api.tangemTech.models.CurrenciesResponse +import com.tangem.datasource.api.tangemTech.models.HotCryptoResponse +import com.tangem.datasource.api.tangemTech.models.UserTokensResponse +import com.tangem.datasource.local.preferences.AppPreferencesStore +import com.tangem.datasource.local.preferences.PreferencesKeys +import com.tangem.datasource.local.preferences.utils.getObjectSyncOrNull +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.features.onramp.OnrampFeatureToggles +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import com.tangem.utils.coroutines.runCatching +import timber.log.Timber +import javax.inject.Inject + +/** + * Default implementation of [HotCryptoLoader] + * + * @property tangemTechApi tangem tech api + * @property hotCryptoResponseStore store of [HotCryptoResponse] + * @property appPreferencesStore app preferences store + * @property dispatchers dispatchers + * @property onrampFeatureToggles onramp feature toggles + * +[REDACTED_AUTHOR] + */ +internal class DefaultHotCryptoLoader @Inject constructor( + private val tangemTechApi: TangemTechApi, + private val hotCryptoResponseStore: HotCryptoResponseStore, + private val appPreferencesStore: AppPreferencesStore, + private val dispatchers: CoroutineDispatcherProvider, + private val onrampFeatureToggles: OnrampFeatureToggles, +) : HotCryptoLoader { + + override suspend fun fetch(tokens: List) { + updateInternal { hotToken -> + tokens.none { + it.id == hotToken.id && it.contractAddress == hotToken.contractAddress && + it.networkId == hotToken.networkId + } + } + } + + override suspend fun update(currencies: List) { + updateInternal { hotToken -> + currencies.none { + it.id.rawCurrencyId == hotToken.id && + (it as? CryptoCurrency.Token)?.contractAddress == hotToken.contractAddress && + it.network.id.value == hotToken.networkId + } + } + } + + private suspend fun updateInternal(filterPredicate: (HotCryptoResponse.Token) -> Boolean) { + if (!onrampFeatureToggles.isHotTokensEnabled) return + + runCatching(dispatchers.io) { + tangemTechApi.getHotCrypto(currencyId = getAppCurrencyId()).getOrThrow() + } + .onSuccess { + Timber.d("HotCrypto is successfully updated") + + hotCryptoResponseStore.store( + value = it.copy(tokens = it.tokens.filter(filterPredicate)), + ) + } + .onFailure { + Timber.e(it, "Unable to fetch hot crypto") + } + } + + private suspend fun getAppCurrencyId(): String { + return appPreferencesStore.getObjectSyncOrNull( + key = PreferencesKeys.SELECTED_APP_CURRENCY_KEY, + ) + ?.id + ?: "usd" + } +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/exchangeservice/hotcrypto/HotCryptoLoader.kt b/core/datasource/src/main/java/com/tangem/datasource/exchangeservice/hotcrypto/HotCryptoLoader.kt new file mode 100644 index 0000000000..05536e4e8d --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/exchangeservice/hotcrypto/HotCryptoLoader.kt @@ -0,0 +1,26 @@ +package com.tangem.datasource.exchangeservice.hotcrypto + +import com.tangem.datasource.api.tangemTech.models.UserTokensResponse +import com.tangem.domain.tokens.model.CryptoCurrency + +/** + * Hot crypto loader + * +[REDACTED_AUTHOR] + */ +interface HotCryptoLoader { + + /** + * Fetch hot crypto + * + * @param tokens already added tokens + */ + suspend fun fetch(tokens: List) + + /** + * Update hot crypto + * + * @param currencies already added crypto currencies + */ + suspend fun update(currencies: List) +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/exchangeservice/hotcrypto/HotCryptoResponseStore.kt b/core/datasource/src/main/java/com/tangem/datasource/exchangeservice/hotcrypto/HotCryptoResponseStore.kt new file mode 100644 index 0000000000..fcfb0a901a --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/exchangeservice/hotcrypto/HotCryptoResponseStore.kt @@ -0,0 +1,16 @@ +package com.tangem.datasource.exchangeservice.hotcrypto + +import com.tangem.datasource.api.tangemTech.models.HotCryptoResponse +import com.tangem.datasource.local.datastore.RuntimeStateStore +import javax.inject.Inject +import javax.inject.Singleton + +/** + * Store of [HotCryptoResponse] + * +[REDACTED_AUTHOR] + */ +@Singleton +class HotCryptoResponseStore @Inject constructor() : RuntimeStateStore by RuntimeStateStore( + defaultValue = HotCryptoResponse(imageHost = null, tokens = emptyList()), +) \ No newline at end of file diff --git a/data/onramp/build.gradle.kts b/data/onramp/build.gradle.kts index ac0f5ce3e9..dd8b614228 100644 --- a/data/onramp/build.gradle.kts +++ b/data/onramp/build.gradle.kts @@ -26,6 +26,7 @@ dependencies { implementation(projects.domain.onramp) implementation(projects.domain.legacy) implementation(projects.domain.appTheme.models) + implementation(projects.domain.models) // region DI diff --git a/data/onramp/src/main/java/com/tangem/data/onramp/DefaultHotCryptoRepository.kt b/data/onramp/src/main/java/com/tangem/data/onramp/DefaultHotCryptoRepository.kt new file mode 100644 index 0000000000..5e296aa7dd --- /dev/null +++ b/data/onramp/src/main/java/com/tangem/data/onramp/DefaultHotCryptoRepository.kt @@ -0,0 +1,41 @@ +package com.tangem.data.onramp + +import com.tangem.blockchainsdk.utils.ExcludedBlockchains +import com.tangem.data.onramp.converters.HotCryptoCurrencyConverter +import com.tangem.datasource.exchangeservice.hotcrypto.HotCryptoResponseStore +import com.tangem.datasource.local.userwallet.UserWalletsStore +import com.tangem.domain.onramp.model.HotCryptoCurrency +import com.tangem.domain.onramp.repositories.HotCryptoRepository +import com.tangem.domain.wallets.models.UserWalletId +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map + +/** + * Default implementation of [HotCryptoRepository] + * + * @property excludedBlockchains excluded blockchains + * @property hotCryptoResponseStore store of `HotCryptoResponse` + * @property userWalletsStore store of `UserWallet` + * +[REDACTED_AUTHOR] + */ +internal class DefaultHotCryptoRepository( + private val excludedBlockchains: ExcludedBlockchains, + private val hotCryptoResponseStore: HotCryptoResponseStore, + private val userWalletsStore: UserWalletsStore, +) : HotCryptoRepository { + + override fun getCurrencies(userWalletId: UserWalletId): Flow> { + return hotCryptoResponseStore.get() + .map { + val userWallet = userWalletsStore.getSyncOrNull(userWalletId) + ?: error("UserWalletId [$userWalletId] not found") + + HotCryptoCurrencyConverter( + scanResponse = userWallet.scanResponse, + imageHost = it.imageHost, + excludedBlockchains = excludedBlockchains, + ).convertList(input = it.tokens) + } + } +} \ No newline at end of file diff --git a/data/onramp/src/main/java/com/tangem/data/onramp/converters/HotCryptoCurrencyConverter.kt b/data/onramp/src/main/java/com/tangem/data/onramp/converters/HotCryptoCurrencyConverter.kt new file mode 100644 index 0000000000..1742009b08 --- /dev/null +++ b/data/onramp/src/main/java/com/tangem/data/onramp/converters/HotCryptoCurrencyConverter.kt @@ -0,0 +1,78 @@ +package com.tangem.data.onramp.converters + +import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchainsdk.utils.ExcludedBlockchains +import com.tangem.blockchainsdk.utils.fromNetworkId +import com.tangem.data.common.currency.CryptoCurrencyFactory +import com.tangem.data.common.currency.getNetwork +import com.tangem.datasource.api.tangemTech.models.HotCryptoResponse +import com.tangem.domain.models.scan.ScanResponse +import com.tangem.domain.onramp.model.HotCryptoCurrency +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.tokens.model.Network +import com.tangem.utils.converter.Converter + +/** + * Converter from [HotCryptoResponse.Token] to [HotCryptoCurrency] + * + * @property scanResponse scan response + * @property imageHost image host + * @property excludedBlockchains excluded blockchains + * +[REDACTED_AUTHOR] + */ +internal class HotCryptoCurrencyConverter( + private val scanResponse: ScanResponse, + private val imageHost: String?, + private val excludedBlockchains: ExcludedBlockchains, +) : Converter { + + private val cryptoCurrencyFactory by lazy { CryptoCurrencyFactory(excludedBlockchains) } + + override fun convert(value: HotCryptoResponse.Token): HotCryptoCurrency { + val network = createNetwork(value.networkId) + + val contractAddress = value.contractAddress + val decimals = value.decimalCount + val currency = if (contractAddress != null && decimals != null) { + cryptoCurrencyFactory.createToken( + network = network, + rawId = value.id, + name = value.name, + symbol = value.symbol, + decimals = decimals, + contractAddress = contractAddress, + ) + } else { + cryptoCurrencyFactory.createCoin(network = network) + } + + return HotCryptoCurrency( + cryptoCurrency = currency.setupIconUrl(id = value.id), + fiatRate = value.currentPrice, + priceChange = value.priceChangePercentage, + ) + } + + private fun createNetwork(networkId: String): Network { + return requireNotNull( + getNetwork( + blockchain = requireNotNull(Blockchain.fromNetworkId(networkId)), + extraDerivationPath = null, + scanResponse = scanResponse, + excludedBlockchains = excludedBlockchains, + ), + ) + } + + private fun CryptoCurrency.setupIconUrl(id: String): CryptoCurrency { + imageHost ?: return this + + val iconUrl = "${imageHost}large/$id.png" + + return when (this) { + is CryptoCurrency.Coin -> copy(iconUrl = iconUrl) + is CryptoCurrency.Token -> copy(iconUrl = iconUrl) + } + } +} \ No newline at end of file diff --git a/data/onramp/src/main/java/com/tangem/data/onramp/di/OnrampDataModule.kt b/data/onramp/src/main/java/com/tangem/data/onramp/di/OnrampDataModule.kt index febd5c21c9..979d18d4b7 100644 --- a/data/onramp/src/main/java/com/tangem/data/onramp/di/OnrampDataModule.kt +++ b/data/onramp/src/main/java/com/tangem/data/onramp/di/OnrampDataModule.kt @@ -1,6 +1,8 @@ package com.tangem.data.onramp.di import com.squareup.moshi.Moshi +import com.tangem.blockchainsdk.utils.ExcludedBlockchains +import com.tangem.data.onramp.DefaultHotCryptoRepository import com.tangem.data.onramp.DefaultOnrampErrorResolver import com.tangem.data.onramp.DefaultOnrampRepository import com.tangem.data.onramp.DefaultOnrampTransactionRepository @@ -10,12 +12,15 @@ import com.tangem.datasource.api.express.models.response.ExpressErrorResponse import com.tangem.datasource.api.onramp.OnrampApi import com.tangem.datasource.crypto.DataSignatureVerifier import com.tangem.datasource.di.NetworkMoshi +import com.tangem.datasource.exchangeservice.hotcrypto.HotCryptoResponseStore import com.tangem.datasource.local.onramp.countries.OnrampCountriesStore import com.tangem.datasource.local.onramp.currencies.OnrampCurrenciesStore import com.tangem.datasource.local.onramp.pairs.OnrampPairsStore import com.tangem.datasource.local.onramp.paymentmethods.OnrampPaymentMethodsStore import com.tangem.datasource.local.onramp.quotes.OnrampQuotesStore import com.tangem.datasource.local.preferences.AppPreferencesStore +import com.tangem.datasource.local.userwallet.UserWalletsStore +import com.tangem.domain.onramp.repositories.HotCryptoRepository import com.tangem.domain.onramp.repositories.OnrampErrorResolver import com.tangem.domain.onramp.repositories.OnrampRepository import com.tangem.domain.onramp.repositories.OnrampTransactionRepository @@ -83,4 +88,18 @@ internal object OnrampDataModule { OnrampErrorConverter(jsonAdapter), ) } + + @Provides + @Singleton + fun provideHotCryptoRepository( + excludedBlockchains: ExcludedBlockchains, + hotCryptoResponseStore: HotCryptoResponseStore, + userWalletsStore: UserWalletsStore, + ): HotCryptoRepository { + return DefaultHotCryptoRepository( + excludedBlockchains = excludedBlockchains, + hotCryptoResponseStore = hotCryptoResponseStore, + userWalletsStore = userWalletsStore, + ) + } } \ No newline at end of file diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/di/TokensDataModule.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/di/TokensDataModule.kt index 4250ccab0e..0a2312b1a8 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/di/TokensDataModule.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/di/TokensDataModule.kt @@ -4,6 +4,7 @@ import com.tangem.blockchainsdk.utils.ExcludedBlockchains import com.tangem.data.common.cache.CacheRegistry import com.tangem.data.tokens.repository.* import com.tangem.datasource.api.tangemTech.TangemTechApi +import com.tangem.datasource.exchangeservice.hotcrypto.HotCryptoLoader import com.tangem.datasource.exchangeservice.swap.ExpressServiceLoader import com.tangem.datasource.local.network.NetworksStatusesStore import com.tangem.datasource.local.preferences.AppPreferencesStore @@ -32,6 +33,7 @@ internal object TokensDataModule { cacheRegistry: CacheRegistry, dispatchers: CoroutineDispatcherProvider, expressServiceLoader: ExpressServiceLoader, + hotCryptoLoader: HotCryptoLoader, excludedBlockchains: ExcludedBlockchains, ): CurrenciesRepository { return DefaultCurrenciesRepository( @@ -42,6 +44,7 @@ internal object TokensDataModule { appPreferencesStore = appPreferencesStore, dispatchers = dispatchers, expressServiceLoader = expressServiceLoader, + hotCryptoLoader = hotCryptoLoader, excludedBlockchains = excludedBlockchains, ) } diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt index 4fb9ef47d2..4f9c1eecb2 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt @@ -15,6 +15,7 @@ import com.tangem.datasource.api.express.models.TangemExpressValues.EMPTY_CONTRA import com.tangem.datasource.api.express.models.request.LeastTokenInfo import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.api.tangemTech.models.UserTokensResponse +import com.tangem.datasource.exchangeservice.hotcrypto.HotCryptoLoader import com.tangem.datasource.exchangeservice.swap.ExpressServiceLoader import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.datasource.local.preferences.PreferencesKeys @@ -33,10 +34,8 @@ import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWalletId import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.* import kotlinx.coroutines.flow.* -import kotlinx.coroutines.plus -import kotlinx.coroutines.withContext import timber.log.Timber import com.tangem.blockchain.common.FeePaidCurrency as FeePaidSdkCurrency @@ -48,6 +47,7 @@ internal class DefaultCurrenciesRepository( private val cacheRegistry: CacheRegistry, private val appPreferencesStore: AppPreferencesStore, private val expressServiceLoader: ExpressServiceLoader, + private val hotCryptoLoader: HotCryptoLoader, private val dispatchers: CoroutineDispatcherProvider, excludedBlockchains: ExcludedBlockchains, ) : CurrenciesRepository { @@ -601,7 +601,11 @@ internal class DefaultCurrenciesRepository( network = token.networkId, ) } - expressServiceLoader.update(userWalletId, tokens) + + coroutineScope { + launch { expressServiceLoader.update(userWalletId, tokens) } + launch { hotCryptoLoader.fetch(tokens = userTokens.tokens) } + } } private suspend fun fetchExpressAssetsByNetworkIds( @@ -619,7 +623,12 @@ internal class DefaultCurrenciesRepository( cacheRegistry.invokeOnExpire( key = getAssetsCacheKey(userWalletId), skipCache = refresh, - block = { expressServiceLoader.update(userWalletId, tokens) }, + block = { + coroutineScope { + launch { expressServiceLoader.update(userWalletId, tokens) } + launch { hotCryptoLoader.update(cryptoCurrencies) } + } + }, ) } diff --git a/domain/onramp/models/src/main/kotlin/com/tangem/domain/onramp/model/HotCryptoCurrency.kt b/domain/onramp/models/src/main/kotlin/com/tangem/domain/onramp/model/HotCryptoCurrency.kt new file mode 100644 index 0000000000..ad722d847a --- /dev/null +++ b/domain/onramp/models/src/main/kotlin/com/tangem/domain/onramp/model/HotCryptoCurrency.kt @@ -0,0 +1,19 @@ +package com.tangem.domain.onramp.model + +import com.tangem.domain.tokens.model.CryptoCurrency +import java.math.BigDecimal + +/** + * Hot crypto currency + * + * @property cryptoCurrency crypto currency + * @property fiatRate fiat rate + * @property priceChange price change + * +[REDACTED_AUTHOR] + */ +data class HotCryptoCurrency( + val cryptoCurrency: CryptoCurrency, + val fiatRate: BigDecimal, + val priceChange: BigDecimal, +) \ No newline at end of file diff --git a/domain/onramp/src/main/java/com/tangem/domain/onramp/GetHotCryptoUseCase.kt b/domain/onramp/src/main/java/com/tangem/domain/onramp/GetHotCryptoUseCase.kt new file mode 100644 index 0000000000..13062a4be6 --- /dev/null +++ b/domain/onramp/src/main/java/com/tangem/domain/onramp/GetHotCryptoUseCase.kt @@ -0,0 +1,22 @@ +package com.tangem.domain.onramp + +import com.tangem.domain.onramp.model.HotCryptoCurrency +import com.tangem.domain.onramp.repositories.HotCryptoRepository +import com.tangem.domain.wallets.models.UserWalletId +import kotlinx.coroutines.flow.Flow + +/** + * Get hot crypto currencies + * + * @property hotCryptoRepository hot crypto repository + * +[REDACTED_AUTHOR] + */ +class GetHotCryptoUseCase( + private val hotCryptoRepository: HotCryptoRepository, +) { + + operator fun invoke(userWalletId: UserWalletId): Flow> { + return hotCryptoRepository.getCurrencies(userWalletId) + } +} \ No newline at end of file diff --git a/domain/onramp/src/main/java/com/tangem/domain/onramp/repositories/HotCryptoRepository.kt b/domain/onramp/src/main/java/com/tangem/domain/onramp/repositories/HotCryptoRepository.kt new file mode 100644 index 0000000000..1e14c4dd2c --- /dev/null +++ b/domain/onramp/src/main/java/com/tangem/domain/onramp/repositories/HotCryptoRepository.kt @@ -0,0 +1,16 @@ +package com.tangem.domain.onramp.repositories + +import com.tangem.domain.onramp.model.HotCryptoCurrency +import com.tangem.domain.wallets.models.UserWalletId +import kotlinx.coroutines.flow.Flow + +/** + * Hot crypto repository + * +[REDACTED_AUTHOR] + */ +interface HotCryptoRepository { + + /** Get currencies by [userWalletId] */ + fun getCurrencies(userWalletId: UserWalletId): Flow> +} \ No newline at end of file From bb32839f9d0ee6406d14f5ae66d54fdbd2767e08 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 27 Jan 2025 12:45:57 +0300 Subject: [PATCH 2/2] Updated on 2026-08-14 --- .../sdk/impl/DefaultTangemSdkManager.kt | 14 ++ .../domain/sdk/impl/MockTangemSdkManager.kt | 10 + .../domain/tasks/product/ScanProductTask.kt | 15 +- .../tasks/visa/VisaCardActivationTask.kt | 2 +- .../visa/VisaCustomerWalletApproveTask.kt | 188 ++++++++++++++++++ .../visa/DefaultVisaActivationRepository.kt | 1 + .../domain/common/DerivationStyleProvider.kt | 4 +- .../domain/common/TangemCardTypesResolver.kt | 6 +- .../tangem/domain/common/TapWorkarounds.kt | 3 + .../domain/common/util/ScanResponseExt.kt | 8 +- .../domain/common/visa/VisaUtilities.kt | 4 +- .../common/visa/VisaWalletPublicKeyUtility.kt | 63 ++++++ .../domain/visa/model/VisaActivationError.kt | 1 + .../domain/visa/model/VisaDataForApprove.kt | 10 + features/onboarding-v2/impl/build.gradle.kts | 1 + .../impl/DefaultOnboardingVisaComponent.kt | 67 +++++-- .../OnboardingVisaAccessCodeComponent.kt | 18 +- .../model/OnboardingVisaAccessCodeModel.kt | 82 ++++++-- .../accesscode/ui/OnboardingVisaAccessCode.kt | 1 + .../ui/state/OnboardingVisaAccessCodeUM.kt | 1 + .../approve/OnboardingVisaApproveComponent.kt | 8 +- .../model/OnboardingVisaApproveModel.kt | 34 +++- .../child/approve/ui/OnboardingVisaApprove.kt | 1 + .../ui/state/OnboardingVisaApproveUM.kt | 1 + .../OnboardingVisaOtherWalletComponent.kt | 8 +- .../model/OnboardingVisaOtherWalletModel.kt | 5 + .../child/welcome/ui/OnboardingVisaWelcome.kt | 7 +- .../v2/visa/impl/route/OnboardingVisaRoute.kt | 10 +- .../onboarding/v2/visa/impl/route/Step.kt | 6 +- .../onboarding/v2/visa/impl/route/Titles.kt | 6 +- .../v2/visa/impl/ui/OnboardingVisa.kt | 9 + .../com/tangem/sdk/api/TangemSdkManager.kt | 4 + 32 files changed, 521 insertions(+), 77 deletions(-) create mode 100644 app/src/main/java/com/tangem/tap/domain/tasks/visa/VisaCustomerWalletApproveTask.kt create mode 100644 domain/legacy/src/main/java/com/tangem/domain/common/visa/VisaWalletPublicKeyUtility.kt create mode 100644 domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/VisaDataForApprove.kt create mode 100644 features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/ui/OnboardingVisa.kt diff --git a/app/src/main/java/com/tangem/tap/domain/sdk/impl/DefaultTangemSdkManager.kt b/app/src/main/java/com/tangem/tap/domain/sdk/impl/DefaultTangemSdkManager.kt index 7ce6aa9702..ac325d7207 100644 --- a/app/src/main/java/com/tangem/tap/domain/sdk/impl/DefaultTangemSdkManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/sdk/impl/DefaultTangemSdkManager.kt @@ -27,6 +27,7 @@ import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.visa.model.VisaActivationInput import com.tangem.domain.visa.model.VisaAuthChallenge import com.tangem.domain.visa.model.VisaCardActivationResponse +import com.tangem.domain.visa.model.VisaDataForApprove import com.tangem.domain.wallets.models.UserWalletId import com.tangem.features.onboarding.v2.OnboardingV2FeatureToggles import com.tangem.operations.ScanTask @@ -35,6 +36,7 @@ import com.tangem.operations.derivation.DeriveMultipleWalletPublicKeysTask import com.tangem.operations.derivation.DeriveWalletPublicKeyTask import com.tangem.operations.pins.SetUserCodeCommand import com.tangem.operations.preflightread.PreflightReadFilter +import com.tangem.operations.sign.SignHashResponse import com.tangem.operations.usersetttings.SetUserCodeRecoveryAllowedTask import com.tangem.operations.wallet.CreateWalletResponse import com.tangem.sdk.api.CreateProductWalletTaskResponse @@ -45,6 +47,7 @@ import com.tangem.tap.domain.tasks.product.ResetBackupCardTask import com.tangem.tap.domain.tasks.product.ResetToFactorySettingsTask import com.tangem.tap.domain.tasks.product.ScanProductTask import com.tangem.tap.domain.tasks.visa.VisaCardActivationTask +import com.tangem.tap.domain.tasks.visa.VisaCustomerWalletApproveTask import com.tangem.tap.domain.twins.CreateFirstTwinWalletTask import com.tangem.tap.domain.twins.CreateSecondTwinWalletTask import com.tangem.tap.domain.twins.FinalizeTwinTask @@ -494,6 +497,17 @@ internal class DefaultTangemSdkManager( } } + override suspend fun visaCustomerWalletApprove( + visaDataForApprove: VisaDataForApprove, + ): CompletionResult { + return runTaskAsyncReturnOnMain( + runnable = VisaCustomerWalletApproveTask( + visaDataForApprove = visaDataForApprove, + ), + initialMessage = Message(resources.getStringSafe(R.string.initial_message_tap_header)), + ) + } + // endregion companion object { diff --git a/app/src/main/java/com/tangem/tap/domain/sdk/impl/MockTangemSdkManager.kt b/app/src/main/java/com/tangem/tap/domain/sdk/impl/MockTangemSdkManager.kt index c0dd2c2c03..3e8dce1c1e 100644 --- a/app/src/main/java/com/tangem/tap/domain/sdk/impl/MockTangemSdkManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/sdk/impl/MockTangemSdkManager.kt @@ -20,9 +20,11 @@ import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.visa.model.VisaActivationInput import com.tangem.domain.visa.model.VisaAuthChallenge import com.tangem.domain.visa.model.VisaCardActivationResponse +import com.tangem.domain.visa.model.VisaDataForApprove import com.tangem.domain.wallets.models.UserWalletId import com.tangem.operations.derivation.DerivationTaskResponse import com.tangem.operations.preflightread.PreflightReadFilter +import com.tangem.operations.sign.SignHashResponse import com.tangem.operations.wallet.CreateWalletResponse import com.tangem.sdk.api.CreateProductWalletTaskResponse import com.tangem.sdk.api.TangemSdkManager @@ -207,4 +209,12 @@ class MockTangemSdkManager( ): CompletionResult { error("Not implemented") } + + override suspend fun visaCustomerWalletApprove( + visaDataForApprove: VisaDataForApprove, + ): CompletionResult { + error("Not implemented") + } + + // endregion } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/tasks/product/ScanProductTask.kt b/app/src/main/java/com/tangem/tap/domain/tasks/product/ScanProductTask.kt index bc2268c5a9..fdc3898fbf 100644 --- a/app/src/main/java/com/tangem/tap/domain/tasks/product/ScanProductTask.kt +++ b/app/src/main/java/com/tangem/tap/domain/tasks/product/ScanProductTask.kt @@ -311,15 +311,12 @@ private class ScanWalletProcessor( } private fun getWalletProductType(card: CardDTO): ProductType { - if (RING_BATCH_IDS.contains(card.batchId) || card.batchId.startsWith(RING_BATCH_PREFIX)) { - return ProductType.Ring - } - return if (card.firmwareVersion >= FirmwareVersion.Ed25519Slip0010Available && - card.settings.isKeysImportAllowed - ) { - ProductType.Wallet2 - } else { - ProductType.Wallet + return when { + card.isVisa -> ProductType.Visa + RING_BATCH_IDS.contains(card.batchId) || card.batchId.startsWith(RING_BATCH_PREFIX) -> ProductType.Ring + card.firmwareVersion >= FirmwareVersion.Ed25519Slip0010Available && + card.settings.isKeysImportAllowed -> ProductType.Wallet2 + else -> ProductType.Wallet } } diff --git a/app/src/main/java/com/tangem/tap/domain/tasks/visa/VisaCardActivationTask.kt b/app/src/main/java/com/tangem/tap/domain/tasks/visa/VisaCardActivationTask.kt index d1efcf68c4..709be0263d 100644 --- a/app/src/main/java/com/tangem/tap/domain/tasks/visa/VisaCardActivationTask.kt +++ b/app/src/main/java/com/tangem/tap/domain/tasks/visa/VisaCardActivationTask.kt @@ -1,12 +1,12 @@ package com.tangem.tap.domain.tasks.visa -import com.reown.util.hexToBytes import com.tangem.common.CompletionResult import com.tangem.common.card.Card import com.tangem.common.core.CardSession import com.tangem.common.core.CardSessionRunnable import com.tangem.common.core.CompletionCallback import com.tangem.common.core.TangemSdkError +import com.tangem.common.extensions.hexToBytes import com.tangem.common.extensions.toHexString import com.tangem.common.map import com.tangem.datasource.local.visa.VisaAuthTokenStorage diff --git a/app/src/main/java/com/tangem/tap/domain/tasks/visa/VisaCustomerWalletApproveTask.kt b/app/src/main/java/com/tangem/tap/domain/tasks/visa/VisaCustomerWalletApproveTask.kt new file mode 100644 index 0000000000..c9fb9dc1d0 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/domain/tasks/visa/VisaCustomerWalletApproveTask.kt @@ -0,0 +1,188 @@ +package com.tangem.tap.domain.tasks.visa + +import arrow.core.getOrElse +import com.tangem.common.CompletionResult +import com.tangem.common.card.Card +import com.tangem.common.card.CardWallet +import com.tangem.common.core.CardSession +import com.tangem.common.core.CardSessionRunnable +import com.tangem.common.core.CompletionCallback +import com.tangem.common.core.TangemSdkError +import com.tangem.common.extensions.hexToBytes +import com.tangem.crypto.hdWallet.DerivationPath +import com.tangem.crypto.hdWallet.bip32.ExtendedPublicKey +import com.tangem.domain.common.util.derivationStyleProvider +import com.tangem.domain.common.visa.VisaUtilities +import com.tangem.domain.common.visa.VisaWalletPublicKeyUtility +import com.tangem.domain.common.visa.VisaWalletPublicKeyUtility.findKeyWithoutDerivation +import com.tangem.domain.models.scan.CardDTO +import com.tangem.domain.visa.model.VisaActivationError +import com.tangem.domain.visa.model.VisaDataForApprove +import com.tangem.operations.ScanTask +import com.tangem.operations.derivation.DeriveWalletPublicKeyTask +import com.tangem.operations.sign.SignHashCommand +import com.tangem.operations.sign.SignHashResponse + +class VisaCustomerWalletApproveTask( + private val visaDataForApprove: VisaDataForApprove, +) : CardSessionRunnable { + + override fun run(session: CardSession, callback: CompletionCallback) { + val card = session.environment.card ?: run { + callback(CompletionResult.Failure(TangemSdkError.MissingPreflightRead())) + return + } + + if (VisaUtilities.isVisaCard(card.firmwareVersion.doubleValue, card.batchId).not()) { + callback(CompletionResult.Failure(TangemSdkError.Underlying("Card is identified as not Visa"))) + return + } + + if (card.settings.isHDWalletAllowed) { + proceedApprove(card, session, callback) + } else { + proceedApproveWithLegacyCard(card, session, callback) + } + } + + private fun proceedApprove(card: Card, session: CardSession, callback: CompletionCallback) { + val cardDTO = CardDTO(card) + + val derivationStyle = cardDTO.derivationStyleProvider.getDerivationStyle() ?: run { + proceedApproveWithLegacyCard( + card = card, + session = session, + callback = callback, + ) + return + } + + val derivationPath = VisaUtilities.visaDefaultDerivationPath(derivationStyle) ?: run { + callback( + CompletionResult.Failure( + TangemSdkError.Underlying("Failed to generate derivation path with provided derivation style"), + ), + ) + return + } + + val wallet = card.wallets.firstOrNull { it.curve == VisaUtilities.mandatoryCurve } ?: run { + callback(CompletionResult.Failure(TangemSdkError.Underlying(VisaActivationError.MissingWallet.message))) + return + } + + val derivationTask = DeriveWalletPublicKeyTask( + walletPublicKey = wallet.publicKey, + derivationPath = derivationPath, + ) + + derivationTask.run(session) { result -> + when (result) { + is CompletionResult.Success -> { + processDerivedKey( + wallet = wallet, + extendedPublicKey = result.data, + derivationPath = derivationPath, + session = session, + callback = callback, + ) + } + is CompletionResult.Failure -> { + callback(CompletionResult.Failure(result.error)) + } + } + } + } + + private fun processDerivedKey( + wallet: CardWallet, + extendedPublicKey: ExtendedPublicKey, + derivationPath: DerivationPath, + session: CardSession, + callback: CompletionCallback, + ) { + val validationResult = VisaWalletPublicKeyUtility.validateExtendedPublicKey( + targetAddress = visaDataForApprove.targetAddress, + extendedPublicKey = extendedPublicKey, + ) + + validationResult.onLeft { + callback(CompletionResult.Failure(TangemSdkError.Underlying(it.message))) + return + } + + signApproveData( + targetWalletPublicKey = wallet.publicKey, + derivationPath = derivationPath, + session = session, + callback = callback, + ) + } + + private fun proceedApproveWithLegacyCard( + card: Card, + session: CardSession, + callback: CompletionCallback, + ) { + val publicKey = findKeyWithoutDerivation( + targetAddress = visaDataForApprove.targetAddress, + card = CardDTO(card), + ).getOrElse { + callback(CompletionResult.Failure(TangemSdkError.Underlying(it.message))) + return + } + + signApproveData( + targetWalletPublicKey = publicKey, + derivationPath = null, + session = session, + callback = callback, + ) + } + + private fun signApproveData( + targetWalletPublicKey: ByteArray, + derivationPath: DerivationPath?, + session: CardSession, + callback: CompletionCallback, + ) { + val signTask = SignHashCommand( + hash = visaDataForApprove.approveHash.hexToBytes(), + walletPublicKey = targetWalletPublicKey, + derivationPath = derivationPath, + ) + + signTask.run(session) { result -> + when (result) { + is CompletionResult.Success -> { + scanCard( + signHashResponse = result.data, + session = session, + callback = callback, + ) + } + is CompletionResult.Failure -> { + callback(CompletionResult.Failure(result.error)) + } + } + } + } + + private fun scanCard( + signHashResponse: SignHashResponse, + session: CardSession, + callback: CompletionCallback, + ) { + val scanTask = ScanTask() + scanTask.run(session) { result -> + when (result) { + is CompletionResult.Success -> { + callback(CompletionResult.Success(signHashResponse)) + } + is CompletionResult.Failure -> { + callback(CompletionResult.Failure(result.error)) + } + } + } + } +} \ No newline at end of file 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 index 17862805b5..b11958c4f5 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/visa/DefaultVisaActivationRepository.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/visa/DefaultVisaActivationRepository.kt @@ -20,6 +20,7 @@ internal class DefaultVisaActivationRepository @AssistedInject constructor( override suspend fun getActivationRemoteState(): VisaActivationRemoteState = withContext(dispatcherProvider.io) { visaActivationStatusConverter.convert(visaApi.getRemoteActivationStatus(cardId)) + VisaActivationRemoteState.CardWalletSignatureRequired // mock // TODO implement refreshing access token if it's expired } diff --git a/domain/legacy/src/main/java/com/tangem/domain/common/DerivationStyleProvider.kt b/domain/legacy/src/main/java/com/tangem/domain/common/DerivationStyleProvider.kt index 570071b37a..e4ff4eea14 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/common/DerivationStyleProvider.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/common/DerivationStyleProvider.kt @@ -1,6 +1,7 @@ package com.tangem.domain.common import com.tangem.blockchain.common.derivation.DerivationStyle +import com.tangem.domain.common.TapWorkarounds.isWallet2 import com.tangem.domain.models.scan.CardDTO interface DerivationStyleProvider { @@ -8,14 +9,13 @@ interface DerivationStyleProvider { } internal class TangemDerivationStyleProvider( - private val cardTypesResolver: CardTypesResolver, private val card: CardDTO, ) : DerivationStyleProvider { override fun getDerivationStyle(): DerivationStyle? { return when { !card.settings.isHDWalletAllowed -> null firstBatchesOfWallet1(card) -> DerivationStyle.V1 - cardTypesResolver.isWallet2() -> DerivationStyle.V3 + card.isWallet2 -> DerivationStyle.V3 else -> DerivationStyle.V2 } } diff --git a/domain/legacy/src/main/java/com/tangem/domain/common/TangemCardTypesResolver.kt b/domain/legacy/src/main/java/com/tangem/domain/common/TangemCardTypesResolver.kt index bcf606539a..46035bd37d 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/common/TangemCardTypesResolver.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/common/TangemCardTypesResolver.kt @@ -8,6 +8,7 @@ import com.tangem.common.card.WalletData import com.tangem.domain.common.TapWorkarounds.getTangemNoteBlockchain import com.tangem.domain.common.TapWorkarounds.isStart2Coin import com.tangem.domain.common.TapWorkarounds.isTestCard +import com.tangem.domain.common.TapWorkarounds.isWallet2 import com.tangem.domain.common.visa.VisaUtilities import com.tangem.domain.models.scan.CardDTO import com.tangem.domain.models.scan.ProductType @@ -35,10 +36,7 @@ internal class TangemCardTypesResolver( return walletData == null && card.firmwareVersion <= FirmwareVersion.HDWalletAvailable } - override fun isWallet2(): Boolean { - return card.firmwareVersion >= FirmwareVersion.Ed25519Slip0010Available && - card.settings.isKeysImportAllowed - } + override fun isWallet2(): Boolean = card.isWallet2 override fun isVisaWallet(): Boolean = productType == ProductType.Visa diff --git a/domain/legacy/src/main/java/com/tangem/domain/common/TapWorkarounds.kt b/domain/legacy/src/main/java/com/tangem/domain/common/TapWorkarounds.kt index b369611972..493d724f8b 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/common/TapWorkarounds.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/common/TapWorkarounds.kt @@ -23,6 +23,9 @@ object TapWorkarounds { val CardDTO.isStart2Coin: Boolean get() = isStart2CoinIssuer(issuer.name) + val CardDTO.isWallet2: Boolean + get() = firmwareVersion >= FirmwareVersion.Ed25519Slip0010Available && settings.isKeysImportAllowed + val CardDTO.isVisa: Boolean get() = VisaUtilities.isVisaCard(this) diff --git a/domain/legacy/src/main/java/com/tangem/domain/common/util/ScanResponseExt.kt b/domain/legacy/src/main/java/com/tangem/domain/common/util/ScanResponseExt.kt index 94727f02cb..3c24fb103f 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/common/util/ScanResponseExt.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/common/util/ScanResponseExt.kt @@ -24,10 +24,10 @@ val ScanResponse.cardTypesResolver: CardTypesResolver ) val ScanResponse.derivationStyleProvider: DerivationStyleProvider - get() = TangemDerivationStyleProvider( - cardTypesResolver, - card, - ) + get() = card.derivationStyleProvider + +val CardDTO.derivationStyleProvider: DerivationStyleProvider + get() = TangemDerivationStyleProvider(this) val UserWallet.cardTypesResolver: CardTypesResolver get() = scanResponse.cardTypesResolver diff --git a/domain/legacy/src/main/java/com/tangem/domain/common/visa/VisaUtilities.kt b/domain/legacy/src/main/java/com/tangem/domain/common/visa/VisaUtilities.kt index 32d4282b20..b84ed4f673 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/common/visa/VisaUtilities.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/common/visa/VisaUtilities.kt @@ -18,12 +18,14 @@ object VisaUtilities { val visaDefaultDerivationPath = visaBlockchain.derivationPath(DerivationStyle.V3) + fun visaDefaultDerivationPath(style: DerivationStyle) = visaBlockchain.derivationPath(style) + fun isVisaCard(card: CardDTO): Boolean { return isVisaCard(card.firmwareVersion.doubleValue, card.batchId) } fun isVisaCard(firmwareVersion: Double, batchId: String): Boolean { - return FirmwareVersion.visaRange.contains(firmwareVersion) && + return firmwareVersion in FirmwareVersion.visaRange && batchId.startsWith(VISA_BATCH_START) } } \ No newline at end of file diff --git a/domain/legacy/src/main/java/com/tangem/domain/common/visa/VisaWalletPublicKeyUtility.kt b/domain/legacy/src/main/java/com/tangem/domain/common/visa/VisaWalletPublicKeyUtility.kt new file mode 100644 index 0000000000..60a095ca16 --- /dev/null +++ b/domain/legacy/src/main/java/com/tangem/domain/common/visa/VisaWalletPublicKeyUtility.kt @@ -0,0 +1,63 @@ +package com.tangem.domain.common.visa + +import arrow.core.Either +import arrow.core.raise.catch +import arrow.core.raise.either +import com.tangem.blockchain.common.address.Address +import com.tangem.crypto.hdWallet.bip32.ExtendedPublicKey +import com.tangem.domain.models.scan.CardDTO + +object VisaWalletPublicKeyUtility { + + fun validateExtendedPublicKey(targetAddress: String, extendedPublicKey: ExtendedPublicKey): Either = + either { + validatePublicKey( + targetAddress = targetAddress, + publicKey = extendedPublicKey.publicKey, + ).bind() + } + + fun findKeyWithoutDerivation(targetAddress: String, card: CardDTO): Either = either { + val wallet = findWalletOnVisaCurve(card).bind() + + validatePublicKey( + targetAddress = targetAddress, + publicKey = wallet.publicKey, + ).bind() + + wallet.publicKey + } + + private fun findWalletOnVisaCurve(card: CardDTO): Either = either { + card.wallets.firstOrNull { it.curve == VisaUtilities.mandatoryCurve } ?: raise(Error.MissingWalletOnTargetCurve) + } + + private fun validatePublicKey(targetAddress: String, publicKey: ByteArray): Either = either { + val address = generateAddressOnVisaCurve(publicKey).bind() + + if (address.value != targetAddress) { + raise(Error.AddressNotMatched) + } + } + + private fun generateAddressOnVisaCurve(walletPublicKey: ByteArray): Either = either { + val addresses = catch( + block = { + VisaUtilities.visaBlockchain.makeAddresses( + walletPublicKey = walletPublicKey, + pairPublicKey = null, + curve = VisaUtilities.mandatoryCurve, + ) + }, + catch = { raise(Error.FailedToCreateAddress) }, + ) + + addresses.firstOrNull() ?: raise(Error.FailedToCreateAddress) + } + + enum class Error(val message: String) { + AddressNotMatched("ValidationError: Address not matched"), + FailedToCreateAddress("ValidationError: Failed to create address"), + MissingWalletOnTargetCurve("ValidationError: Missing wallet on target curve"), + } +} \ 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 index 11685b3866..f115a703f5 100644 --- 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 @@ -4,5 +4,6 @@ enum class VisaActivationError(val message: String) { BlockedForActivation("Card is blocked for activation"), InvalidActivationState("Invalid activation state"), WrongCard("Wrong card tapped"), + MissingWallet("Missing wallet"), MissingRootOTP("Missing root OTP"), } \ No newline at end of file diff --git a/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/VisaDataForApprove.kt b/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/VisaDataForApprove.kt new file mode 100644 index 0000000000..5a5fe7cb4d --- /dev/null +++ b/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/VisaDataForApprove.kt @@ -0,0 +1,10 @@ +package com.tangem.domain.visa.model + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class VisaDataForApprove( + @SerialName("targetAddress") val targetAddress: String, + @SerialName("approveHash") val approveHash: String, +) \ No newline at end of file diff --git a/features/onboarding-v2/impl/build.gradle.kts b/features/onboarding-v2/impl/build.gradle.kts index 6a64d3f440..3f1c7150fc 100644 --- a/features/onboarding-v2/impl/build.gradle.kts +++ b/features/onboarding-v2/impl/build.gradle.kts @@ -41,6 +41,7 @@ dependencies { implementation(projects.domain.legacy) implementation(projects.domain.settings) implementation(projects.domain.onboarding) + implementation(projects.domain.visa) /** Tangem libraries */ implementation(projects.libs.tangemSdkApi) diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/DefaultOnboardingVisaComponent.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/DefaultOnboardingVisaComponent.kt index 31ba359b56..a174ae69e0 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/DefaultOnboardingVisaComponent.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/DefaultOnboardingVisaComponent.kt @@ -52,6 +52,11 @@ internal class DefaultOnboardingVisaComponent @AssistedInject constructor( MutableSharedFlow() } + private val childParams = ChildParams( + onBack = ::onChildBack, + parentBackEvent = currentChildBackEventHandle, + ) + private val stackNavigation = StackNavigation() private val childStack: Value> = @@ -76,7 +81,7 @@ internal class DefaultOnboardingVisaComponent @AssistedInject constructor( if (childStack.value.active.configuration is OnboardingVisaRoute.AccessCode) { componentScope.launch { currentChildBackEventHandle.emit(Unit) } } else { - stackNavigation.pop() + onChildBack() } } } @@ -90,11 +95,7 @@ internal class DefaultOnboardingVisaComponent @AssistedInject constructor( } } - private val childParams = ChildParams( - onBack = ::onChildBack, - parentBackEvent = currentChildBackEventHandle, - ) - + @Suppress("LongMethod") private fun createChild( route: OnboardingVisaRoute, factoryContext: AppComponentContext, @@ -112,10 +113,24 @@ internal class DefaultOnboardingVisaComponent @AssistedInject constructor( appComponentContext = factoryContext, params = OnboardingVisaAccessCodeComponent.Params( childParams = childParams, - onDone = { stackNavigation.push(OnboardingVisaRoute.ChooseWallet) }, + onDone = { + stackNavigation.push( + if (it.walletFound) { + OnboardingVisaRoute.TangemWalletApproveOption( + visaDataForApprove = it.visaDataForApprove, + allowNavigateBack = false, + ) + } else { + OnboardingVisaRoute.ChooseWallet(it.visaDataForApprove) + }, + ) + }, + ), + config = OnboardingVisaAccessCodeComponent.Config( + scanResponse = params.scanResponse, ), ) - OnboardingVisaRoute.ChooseWallet -> OnboardingVisaChooseWalletComponent( + is OnboardingVisaRoute.ChooseWallet -> OnboardingVisaChooseWalletComponent( appComponentContext = factoryContext, params = OnboardingVisaChooseWalletComponent.Params( childParams = childParams, @@ -123,9 +138,12 @@ internal class DefaultOnboardingVisaComponent @AssistedInject constructor( stackNavigation.push( when (event) { OnboardingVisaChooseWalletComponent.Params.Event.TangemWallet -> - OnboardingVisaRoute.TangemWalletApproveOption + OnboardingVisaRoute.TangemWalletApproveOption( + visaDataForApprove = route.visaDataForApprove, + allowNavigateBack = true, + ) OnboardingVisaChooseWalletComponent.Params.Event.OtherWallet -> - OnboardingVisaRoute.OtherWalletApproveOption + OnboardingVisaRoute.OtherWalletApproveOption(route.visaDataForApprove) }, ) }, @@ -138,8 +156,11 @@ internal class DefaultOnboardingVisaComponent @AssistedInject constructor( onDone = { stackNavigation.push(OnboardingVisaRoute.PinCode) }, ), ) - OnboardingVisaRoute.OtherWalletApproveOption -> OnboardingVisaOtherWalletComponent( + is OnboardingVisaRoute.OtherWalletApproveOption -> OnboardingVisaOtherWalletComponent( appComponentContext = factoryContext, + config = OnboardingVisaOtherWalletComponent.Config( + visaDataForApprove = route.visaDataForApprove, + ), params = OnboardingVisaOtherWalletComponent.Params( childParams = childParams, onDone = { stackNavigation.push(OnboardingVisaRoute.InProgress) }, @@ -152,8 +173,11 @@ internal class DefaultOnboardingVisaComponent @AssistedInject constructor( onDone = { params.onDone() }, ), ) - OnboardingVisaRoute.TangemWalletApproveOption -> OnboardingVisaApproveComponent( + is OnboardingVisaRoute.TangemWalletApproveOption -> OnboardingVisaApproveComponent( appComponentContext = factoryContext, + config = OnboardingVisaApproveComponent.Config( + visaDataForApprove = route.visaDataForApprove, + ), params = OnboardingVisaApproveComponent.Params( childParams = childParams, onDone = { stackNavigation.push(OnboardingVisaRoute.InProgress) }, @@ -163,15 +187,24 @@ internal class DefaultOnboardingVisaComponent @AssistedInject constructor( } private fun onChildBack() { - when (childStack.value.active.configuration) { + if (childStack.value.backStack.size == 1) { + // TODO show dialog + return + } + + when (val route = childStack.value.active.configuration) { OnboardingVisaRoute.AccessCode, - OnboardingVisaRoute.TangemWalletApproveOption, - OnboardingVisaRoute.OtherWalletApproveOption, + is OnboardingVisaRoute.OtherWalletApproveOption, -> stackNavigation.pop() + is OnboardingVisaRoute.TangemWalletApproveOption -> { + if (route.allowNavigateBack) { + stackNavigation.pop() + } + } + is OnboardingVisaRoute.Welcome, - OnboardingVisaRoute.ChooseWallet, + is OnboardingVisaRoute.ChooseWallet, OnboardingVisaRoute.InProgress, - OnboardingVisaRoute.OtherWalletApproveOption, OnboardingVisaRoute.PinCode, -> { } 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 8734fa0726..dd7a6f2ed6 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 @@ -9,6 +9,8 @@ import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.decompose.model.getOrCreateModel import com.tangem.core.ui.decompose.ComposableContentComponent import com.tangem.core.ui.security.DisableScreenshotsDisposableEffect +import com.tangem.domain.models.scan.ScanResponse +import com.tangem.domain.visa.model.VisaDataForApprove import com.tangem.features.onboarding.v2.visa.impl.DefaultOnboardingVisaComponent import com.tangem.features.onboarding.v2.visa.impl.child.accesscode.model.OnboardingVisaAccessCodeModel import com.tangem.features.onboarding.v2.visa.impl.child.accesscode.ui.OnboardingVisaAccessCode @@ -16,10 +18,11 @@ import kotlinx.coroutines.launch internal class OnboardingVisaAccessCodeComponent( appComponentContext: AppComponentContext, + config: Config, private val params: Params, ) : ComposableContentComponent, AppComponentContext by appComponentContext { - private val model: OnboardingVisaAccessCodeModel = getOrCreateModel() + private val model: OnboardingVisaAccessCodeModel = getOrCreateModel(config) init { componentScope.launch { @@ -31,7 +34,7 @@ internal class OnboardingVisaAccessCodeComponent( } } componentScope.launch { - model.onDone.collect { params.onDone() } + model.onDone.collect { params.onDone(it) } } } @@ -46,8 +49,17 @@ internal class OnboardingVisaAccessCodeComponent( OnboardingVisaAccessCode(state, modifier) } + data class Config( + val scanResponse: ScanResponse, + ) + data class Params( val childParams: DefaultOnboardingVisaComponent.ChildParams, - val onDone: () -> Unit, + val onDone: (DoneEvent) -> Unit, + ) + + data class DoneEvent( + val visaDataForApprove: VisaDataForApprove, + val walletFound: Boolean, ) } \ 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/model/OnboardingVisaAccessCodeModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/accesscode/model/OnboardingVisaAccessCodeModel.kt index 4281e33a1a..1186c22057 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 @@ -2,8 +2,15 @@ package com.tangem.features.onboarding.v2.visa.impl.child.accesscode.model import androidx.compose.runtime.Stable import androidx.compose.ui.text.input.TextFieldValue +import com.tangem.common.CompletionResult +import com.tangem.common.extensions.toHexString import com.tangem.core.decompose.di.ComponentScoped import com.tangem.core.decompose.model.Model +import com.tangem.core.decompose.model.ParamsContainer +import com.tangem.domain.visa.model.VisaCardActivationStatus +import com.tangem.domain.visa.model.VisaDataForApprove +import com.tangem.domain.visa.repository.VisaAuthRepository +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.sdk.api.TangemSdkManager import com.tangem.utils.coroutines.CoroutineDispatcherProvider @@ -14,15 +21,22 @@ import javax.inject.Inject @Stable @ComponentScoped internal class OnboardingVisaAccessCodeModel @Inject constructor( + paramsContainer: ParamsContainer, override val dispatchers: CoroutineDispatcherProvider, @Suppress("UnusedPrivateMember") private val tangemSdkManager: TangemSdkManager, + private val visaAuthRepository: VisaAuthRepository, ) : Model() { + private val params: OnboardingVisaAccessCodeComponent.Config = paramsContainer.require() + private val activationStatus = + params.scanResponse.visaCardActivationStatus as? VisaCardActivationStatus.NotStartedActivation + ?: error("Visa activation status is not set or incorrect for this step") + private val _uiState = MutableStateFlow(getInitialState()) val uiState = _uiState.asStateFlow() val onBack = MutableSharedFlow() - val onDone = MutableSharedFlow() + val onDone = MutableSharedFlow() fun onBack() { when (uiState.value.step) { @@ -97,27 +111,53 @@ internal class OnboardingVisaAccessCodeModel @Inject constructor( @Suppress("UnusedPrivateMember") private fun startActivationProcess(accessCode: String) { - modelScope.launch { onDone.emit(Unit) } + loading(true) - // modelScope.launch { - // @Suppress("UnusedPrivateMember") - // val result = tangemSdkManager.activateVisaCard( - // accessCode = accessCode, - // challengeToSign = null, - // activationInput = TODO(), - // ) - // - // when (result) { - // is CompletionResult.Success -> { - // val response = result.data - // TODO() - // } - // is CompletionResult.Failure -> { - // // show alert - // TODO() - // } - // } - // } + modelScope.launch { + val challengeToSign = runCatching { + visaAuthRepository.getCardAuthChallenge( + cardId = activationStatus.activationInput.cardId, + cardPublicKey = activationStatus.activationInput.cardPublicKey.toHexString(), + ) + }.getOrElse { + loading(false) + // show alert + return@launch + } + + val result = tangemSdkManager.activateVisaCard( + accessCode = accessCode, + challengeToSign = challengeToSign, + activationInput = activationStatus.activationInput, + ) + // + when (result) { + is CompletionResult.Success -> { + // TODO load approve data from backend + // TODO try to find wallet in the app + + modelScope.launch { + onDone.emit( + OnboardingVisaAccessCodeComponent.DoneEvent( + visaDataForApprove = VisaDataForApprove( + targetAddress = "x9F65354e595284956599F2892fA4A4a87653D6E6", + approveHash = "approve hash", + ), + walletFound = false, // TODO + ), + ) + } + } + is CompletionResult.Failure -> { + loading(false) + // show alert + } + } + } + } + + private fun loading(state: Boolean) { + _uiState.update { it.copy(buttonLoading = state) } } private companion object { 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 6fefcee506..1557683bfe 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 @@ -65,6 +65,7 @@ internal fun OnboardingVisaAccessCode(state: OnboardingVisaAccessCodeUM, modifie }, onClick = state.onContinue, iconRes = R.drawable.ic_tangem_24, + showProgress = state.buttonLoading, isIconVisible = state.step == OnboardingVisaAccessCodeUM.Step.ReEnter, ), ) 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 b707b9067c..35403457fd 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 @@ -9,6 +9,7 @@ internal data class OnboardingVisaAccessCodeUM( val codesNotMatchError: Boolean = false, val atLeastMinCharsError: Boolean = false, val accessCodeHidden: Boolean = true, + val buttonLoading: Boolean = false, val onAccessCodeFirstChange: (TextFieldValue) -> Unit = {}, val onAccessCodeSecondChange: (TextFieldValue) -> Unit = {}, val onContinue: () -> Unit = {}, diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/approve/OnboardingVisaApproveComponent.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/approve/OnboardingVisaApproveComponent.kt index b7b5fc37ab..60141475ad 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/approve/OnboardingVisaApproveComponent.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/approve/OnboardingVisaApproveComponent.kt @@ -9,6 +9,7 @@ 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.domain.visa.model.VisaDataForApprove import com.tangem.features.onboarding.v2.visa.impl.DefaultOnboardingVisaComponent import com.tangem.features.onboarding.v2.visa.impl.child.approve.model.OnboardingVisaApproveModel import com.tangem.features.onboarding.v2.visa.impl.child.approve.ui.OnboardingVisaApprove @@ -16,10 +17,11 @@ import kotlinx.coroutines.launch internal class OnboardingVisaApproveComponent( appComponentContext: AppComponentContext, + config: Config, private val params: Params, ) : ComposableContentComponent, AppComponentContext by appComponentContext { - private val model: OnboardingVisaApproveModel = getOrCreateModel() + private val model: OnboardingVisaApproveModel = getOrCreateModel(config) init { componentScope.launch { @@ -39,6 +41,10 @@ internal class OnboardingVisaApproveComponent( ) } + data class Config( + val visaDataForApprove: VisaDataForApprove, + ) + data class Params( val childParams: DefaultOnboardingVisaComponent.ChildParams, val onDone: () -> Unit, diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/approve/model/OnboardingVisaApproveModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/approve/model/OnboardingVisaApproveModel.kt index 11bd07d94b..a79fe4aa78 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/approve/model/OnboardingVisaApproveModel.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/approve/model/OnboardingVisaApproveModel.kt @@ -1,22 +1,30 @@ package com.tangem.features.onboarding.v2.visa.impl.child.approve.model import androidx.compose.runtime.Stable +import com.tangem.common.CompletionResult 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.approve.OnboardingVisaApproveComponent import com.tangem.features.onboarding.v2.visa.impl.child.approve.ui.state.OnboardingVisaApproveUM +import com.tangem.sdk.api.TangemSdkManager import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import javax.inject.Inject @Stable @ComponentScoped internal class OnboardingVisaApproveModel @Inject constructor( + paramsContainer: ParamsContainer, override val dispatchers: CoroutineDispatcherProvider, + private val tangemSdkManager: TangemSdkManager, ) : Model() { + private val params = paramsContainer.require() private val _uiState = MutableStateFlow(getInitialState()) val uiState = _uiState.asStateFlow() @@ -29,7 +37,29 @@ internal class OnboardingVisaApproveModel @Inject constructor( } private fun onApproveClick() { - // TODO - modelScope.launch { onDone.emit(Unit) } + loading(true) + + modelScope.launch { + val result = tangemSdkManager.visaCustomerWalletApprove( + visaDataForApprove = params.visaDataForApprove, + ) + + when (result) { + is CompletionResult.Success -> { + // TODO make backend call + + onDone.emit(Unit) + } + is CompletionResult.Failure -> { + loading(false) + // TODO show dialog + return@launch + } + } + } + } + + private fun loading(state: Boolean) { + _uiState.update { it.copy(approveButtonLoading = state) } } } \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/approve/ui/OnboardingVisaApprove.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/approve/ui/OnboardingVisaApprove.kt index bc1b5f0ff9..f93ebd7cd3 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/approve/ui/OnboardingVisaApprove.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/approve/ui/OnboardingVisaApprove.kt @@ -68,6 +68,7 @@ internal fun OnboardingVisaApprove(state: OnboardingVisaApproveUM, modifier: Mod textReference = TextReference.Str("Approve"), iconRes = R.drawable.ic_tangem_24, isIconVisible = true, + showProgress = state.approveButtonLoading, onClick = state.onApproveClick, ), ) diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/approve/ui/state/OnboardingVisaApproveUM.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/approve/ui/state/OnboardingVisaApproveUM.kt index 90bb2ef920..60bf3ff09d 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/approve/ui/state/OnboardingVisaApproveUM.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/approve/ui/state/OnboardingVisaApproveUM.kt @@ -1,5 +1,6 @@ package com.tangem.features.onboarding.v2.visa.impl.child.approve.ui.state internal data class OnboardingVisaApproveUM( + val approveButtonLoading: Boolean = false, val onApproveClick: () -> Unit = {}, ) \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/otherwallet/OnboardingVisaOtherWalletComponent.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/otherwallet/OnboardingVisaOtherWalletComponent.kt index 8b8784dd1d..3442f47224 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/otherwallet/OnboardingVisaOtherWalletComponent.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/otherwallet/OnboardingVisaOtherWalletComponent.kt @@ -9,6 +9,7 @@ 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.domain.visa.model.VisaDataForApprove import com.tangem.features.onboarding.v2.visa.impl.DefaultOnboardingVisaComponent import com.tangem.features.onboarding.v2.visa.impl.child.otherwallet.model.OnboardingVisaOtherWalletModel import com.tangem.features.onboarding.v2.visa.impl.child.otherwallet.ui.OnboardingVisaOtherWallet @@ -17,10 +18,11 @@ import kotlinx.coroutines.flow.onEach internal class OnboardingVisaOtherWalletComponent( appComponentContext: AppComponentContext, + config: Config, private val params: Params, ) : ComposableContentComponent, AppComponentContext by appComponentContext { - private val model: OnboardingVisaOtherWalletModel = getOrCreateModel() + private val model: OnboardingVisaOtherWalletModel = getOrCreateModel(config) init { model.onDone @@ -40,6 +42,10 @@ internal class OnboardingVisaOtherWalletComponent( ) } + data class Config( + val visaDataForApprove: VisaDataForApprove, + ) + data class Params( val childParams: DefaultOnboardingVisaComponent.ChildParams, val onDone: () -> Unit, diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/otherwallet/model/OnboardingVisaOtherWalletModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/otherwallet/model/OnboardingVisaOtherWalletModel.kt index 42254b2c08..7e5ece076a 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/otherwallet/model/OnboardingVisaOtherWalletModel.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/otherwallet/model/OnboardingVisaOtherWalletModel.kt @@ -3,6 +3,8 @@ package com.tangem.features.onboarding.v2.visa.impl.child.otherwallet.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.otherwallet.OnboardingVisaOtherWalletComponent import com.tangem.features.onboarding.v2.visa.impl.child.otherwallet.ui.state.OnboardingVisaOtherWalletUM import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.flow.MutableSharedFlow @@ -13,9 +15,12 @@ import javax.inject.Inject @Stable @ComponentScoped internal class OnboardingVisaOtherWalletModel @Inject constructor( + paramsContainer: ParamsContainer, override val dispatchers: CoroutineDispatcherProvider, ) : Model() { + @Suppress("UnusedPrivateMember") + private val config = paramsContainer.require() private val _uiState = MutableStateFlow(getInitialState()) val uiState = _uiState.asStateFlow() diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/welcome/ui/OnboardingVisaWelcome.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/welcome/ui/OnboardingVisaWelcome.kt index 26516e4075..d21c1a2f43 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/welcome/ui/OnboardingVisaWelcome.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/welcome/ui/OnboardingVisaWelcome.kt @@ -8,6 +8,7 @@ import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.layout.onSizeChanged +import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.res.painterResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @@ -95,16 +96,18 @@ fun Artwork(modifier: Modifier = Modifier) { Box(modifier) { val circleColor = TangemTheme.colors.background.secondary var cardHeightPx by remember { mutableIntStateOf(0) } + val circleRadiusPx = cardHeightPx / 2 + cardHeightPx / 8f + val circleSize = with(LocalDensity.current) { (circleRadiusPx * 2).toDp() } Canvas( modifier = Modifier .align(Alignment.Center) - .size(254.dp) + .size(circleSize) .fillMaxSize(), ) { drawCircle( color = circleColor, - radius = cardHeightPx / 2 + cardHeightPx / 8f, + radius = circleRadiusPx, center = center, ) } diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/route/OnboardingVisaRoute.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/route/OnboardingVisaRoute.kt index 8eba780fd5..64f7576d0f 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/route/OnboardingVisaRoute.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/route/OnboardingVisaRoute.kt @@ -1,5 +1,6 @@ package com.tangem.features.onboarding.v2.visa.impl.route +import com.tangem.domain.visa.model.VisaDataForApprove import kotlinx.serialization.Serializable @Serializable @@ -12,13 +13,16 @@ sealed class OnboardingVisaRoute { data object AccessCode : OnboardingVisaRoute() @Serializable - data object ChooseWallet : OnboardingVisaRoute() + data class ChooseWallet(val visaDataForApprove: VisaDataForApprove) : OnboardingVisaRoute() @Serializable - data object TangemWalletApproveOption : OnboardingVisaRoute() + data class TangemWalletApproveOption( + val visaDataForApprove: VisaDataForApprove, + val allowNavigateBack: Boolean, + ) : OnboardingVisaRoute() @Serializable - data object OtherWalletApproveOption : OnboardingVisaRoute() + data class OtherWalletApproveOption(val visaDataForApprove: VisaDataForApprove) : OnboardingVisaRoute() @Serializable data object InProgress : OnboardingVisaRoute() diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/route/Step.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/route/Step.kt index f82df04969..a4deb7fcdb 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/route/Step.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/route/Step.kt @@ -4,9 +4,9 @@ package com.tangem.features.onboarding.v2.visa.impl.route internal fun OnboardingVisaRoute.stepNum() = when (this) { is OnboardingVisaRoute.Welcome -> 1 OnboardingVisaRoute.AccessCode -> 2 - OnboardingVisaRoute.ChooseWallet -> 3 - OnboardingVisaRoute.OtherWalletApproveOption -> 4 - OnboardingVisaRoute.TangemWalletApproveOption -> 4 + is OnboardingVisaRoute.ChooseWallet -> 3 + is OnboardingVisaRoute.OtherWalletApproveOption -> 4 + is OnboardingVisaRoute.TangemWalletApproveOption -> 4 OnboardingVisaRoute.InProgress -> 5 OnboardingVisaRoute.PinCode -> 6 } diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/route/Titles.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/route/Titles.kt index 32a8e57a51..89e6c7a044 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/route/Titles.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/route/Titles.kt @@ -5,10 +5,10 @@ import com.tangem.core.ui.extensions.TextReference // TODO add translations [REDACTED_TASK_KEY] internal fun OnboardingVisaRoute.screenTitle(): TextReference = when (this) { OnboardingVisaRoute.AccessCode -> TextReference.Str("Access code") - OnboardingVisaRoute.ChooseWallet -> TextReference.Str("Account activation") + is OnboardingVisaRoute.ChooseWallet -> TextReference.Str("Account activation") OnboardingVisaRoute.InProgress -> TextReference.Str("In progress") - OnboardingVisaRoute.OtherWalletApproveOption -> TextReference.Str("Wallet connection") + is OnboardingVisaRoute.OtherWalletApproveOption -> TextReference.Str("Wallet connection") OnboardingVisaRoute.PinCode -> TextReference.Str("PIN code") - OnboardingVisaRoute.TangemWalletApproveOption -> TextReference.Str("Wallet connection") + is OnboardingVisaRoute.TangemWalletApproveOption -> TextReference.Str("Wallet connection") is OnboardingVisaRoute.Welcome -> TextReference.Str("Getting started") } \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/ui/OnboardingVisa.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/ui/OnboardingVisa.kt new file mode 100644 index 0000000000..40a9a8597d --- /dev/null +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/ui/OnboardingVisa.kt @@ -0,0 +1,9 @@ +package com.tangem.features.onboarding.v2.visa.impl.ui + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier + +@Composable +internal fun OnboardingVisa(modifier: Modifier = Modifier, childContent: @Composable (Modifier) -> Unit) { + childContent(modifier) +} \ No newline at end of file diff --git a/libs/tangem-sdk-api/src/main/kotlin/com/tangem/sdk/api/TangemSdkManager.kt b/libs/tangem-sdk-api/src/main/kotlin/com/tangem/sdk/api/TangemSdkManager.kt index d86b31b3d0..fd17f4da84 100644 --- a/libs/tangem-sdk-api/src/main/kotlin/com/tangem/sdk/api/TangemSdkManager.kt +++ b/libs/tangem-sdk-api/src/main/kotlin/com/tangem/sdk/api/TangemSdkManager.kt @@ -18,9 +18,11 @@ import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.visa.model.VisaActivationInput import com.tangem.domain.visa.model.VisaAuthChallenge import com.tangem.domain.visa.model.VisaCardActivationResponse +import com.tangem.domain.visa.model.VisaDataForApprove import com.tangem.domain.wallets.models.UserWalletId import com.tangem.operations.derivation.DerivationTaskResponse import com.tangem.operations.preflightread.PreflightReadFilter +import com.tangem.operations.sign.SignHashResponse import com.tangem.operations.wallet.CreateWalletResponse @Suppress("TooManyFunctions") @@ -153,5 +155,7 @@ interface TangemSdkManager { activationInput: VisaActivationInput, ): CompletionResult + suspend fun visaCustomerWalletApprove(visaDataForApprove: VisaDataForApprove): CompletionResult + // endregion } \ No newline at end of file