From fefe37a890d676d905c8506cbc4f1bf6eac00191 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 31 Aug 2023 16:58:05 +0300 Subject: [PATCH 001/242] Updated on 2026-08-14 --- .../java/com/tangem/tap/TapApplication.kt | 1 + .../common/redux/global/GlobalMiddleware.kt | 2 + .../java/com/tangem/tap/di/ActivityModule.kt | 9 ++ .../tap/di/domain/TokensDomainModule.kt | 6 +- .../tokens/LoadAvailableCoinsService.kt | 105 ------------------ .../converters/CryptoCurrencyConverter.kt | 28 ++++- .../exchangeServices/DefaultRampManager.kt | 21 ++++ .../com/tangem/tap/proxy/AppStateHolder.kt | 2 + .../tap/proxy/redux/DaggerGraphState.kt | 2 + .../datasource/di/MarketCoinsStoreModule.kt | 21 ++++ .../token/DefaultUserMarketCoinsStore.kt | 18 +++ .../local/token/UserMarketCoinsStore.kt | 11 ++ .../tangem/data/tokens/di/TokensDataModule.kt | 41 +++++-- .../repository/DefaultCurrenciesRepository.kt | 14 +++ .../DefaultMarketCryptoCurrencyRepository.kt | 18 +++ .../domain/exchange/RampStateManager.kt | 11 ++ domain/tokens/build.gradle.kts | 8 +- .../domain/tokens/models/CryptoCurrency.kt | 2 + .../tokens/GetCryptoCurrencyActionsUseCase.kt | 74 ++++++++++-- .../domain/tokens/model/TokenActionsState.kt | 3 +- .../MarketCryptoCurrencyRepository.kt | 12 ++ .../viewmodels/TokenDetailsViewModel.kt | 6 +- .../wallet/viewmodels/WalletViewModel.kt | 10 +- 23 files changed, 285 insertions(+), 140 deletions(-) delete mode 100644 app/src/main/java/com/tangem/tap/domain/tokens/LoadAvailableCoinsService.kt create mode 100644 app/src/main/java/com/tangem/tap/network/exchangeServices/DefaultRampManager.kt create mode 100644 core/datasource/src/main/java/com/tangem/datasource/di/MarketCoinsStoreModule.kt create mode 100644 core/datasource/src/main/java/com/tangem/datasource/local/token/DefaultUserMarketCoinsStore.kt create mode 100644 core/datasource/src/main/java/com/tangem/datasource/local/token/UserMarketCoinsStore.kt create mode 100644 data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultMarketCryptoCurrencyRepository.kt create mode 100644 domain/legacy/src/main/java/com/tangem/domain/exchange/RampStateManager.kt create mode 100644 domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/MarketCryptoCurrencyRepository.kt diff --git a/app/src/main/java/com/tangem/tap/TapApplication.kt b/app/src/main/java/com/tangem/tap/TapApplication.kt index 65bbb228d0..79fee709bf 100644 --- a/app/src/main/java/com/tangem/tap/TapApplication.kt +++ b/app/src/main/java/com/tangem/tap/TapApplication.kt @@ -190,6 +190,7 @@ class TapApplication : Application(), ImageLoaderFactory { scanCardProcessor = scanCardProcessor, appCurrencyRepository = appCurrencyRepository, walletManagersFacade = walletManagersFacade, + appStateHolder = appStateHolder, ), ), ) diff --git a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalMiddleware.kt b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalMiddleware.kt index 287b5d35f1..4f74fdbdf9 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalMiddleware.kt @@ -131,6 +131,8 @@ private fun handleAction(action: Action, appState: () -> AppState?, dispatch: Di sellService = makeSellExchangeService(config), primaryRules = CardExchangeRules(cardProvider), ) + // TODO: for refactoring (after remove old design refactor CurrencyExchangeManager and use 1 instance) + store.state.daggerGraphState.get(DaggerGraphState::appStateHolder).exchangeService = exchangeManager store.dispatchOnMain(GlobalAction.ExchangeManager.Init.Success(exchangeManager)) store.dispatchOnMain(GlobalAction.ExchangeManager.Update) } diff --git a/app/src/main/java/com/tangem/tap/di/ActivityModule.kt b/app/src/main/java/com/tangem/tap/di/ActivityModule.kt index a894d7c4fe..58073accdf 100644 --- a/app/src/main/java/com/tangem/tap/di/ActivityModule.kt +++ b/app/src/main/java/com/tangem/tap/di/ActivityModule.kt @@ -3,8 +3,11 @@ package com.tangem.tap.di import android.content.Context import com.tangem.domain.card.ScanCardUseCase import com.tangem.domain.card.repository.CardSdkConfigRepository +import com.tangem.domain.exchange.RampStateManager import com.tangem.tap.domain.TangemSdkManager import com.tangem.tap.domain.scanCard.repository.DefaultScanCardRepository +import com.tangem.tap.network.exchangeServices.DefaultRampManager +import com.tangem.tap.proxy.AppStateHolder import com.tangem.tap.userTokensRepository import dagger.Module import dagger.Provides @@ -40,4 +43,10 @@ internal object ActivityModule { ), ) } + + @Provides + @Singleton + fun provideDefaultRampManager(appStateHolder: AppStateHolder): RampStateManager { + return DefaultRampManager(appStateHolder.exchangeService) + } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt index 1a77f50c7b..8ac2b0b3b2 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt @@ -1,7 +1,9 @@ package com.tangem.tap.di.domain +import com.tangem.domain.exchange.RampStateManager import com.tangem.domain.tokens.* import com.tangem.domain.tokens.repository.CurrenciesRepository +import com.tangem.domain.tokens.repository.MarketCryptoCurrencyRepository import com.tangem.domain.tokens.repository.NetworksRepository import com.tangem.domain.tokens.repository.QuotesRepository import com.tangem.utils.coroutines.CoroutineDispatcherProvider @@ -108,8 +110,10 @@ internal object TokensDomainModule { @Provides @ViewModelScoped fun provideGetCryptoCurrencyActionsUseCase( + rampStateManager: RampStateManager, + marketCryptoCurrencyRepository: MarketCryptoCurrencyRepository, dispatchers: CoroutineDispatcherProvider, ): GetCryptoCurrencyActionsUseCase { - return GetCryptoCurrencyActionsUseCase(dispatchers) + return GetCryptoCurrencyActionsUseCase(rampStateManager, marketCryptoCurrencyRepository, dispatchers) } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/tokens/LoadAvailableCoinsService.kt b/app/src/main/java/com/tangem/tap/domain/tokens/LoadAvailableCoinsService.kt deleted file mode 100644 index 39e845f7b2..0000000000 --- a/app/src/main/java/com/tangem/tap/domain/tokens/LoadAvailableCoinsService.kt +++ /dev/null @@ -1,105 +0,0 @@ -package com.tangem.tap.domain.tokens - -import com.squareup.moshi.JsonAdapter -import com.tangem.blockchain.common.Blockchain -import com.tangem.common.services.Result -import com.tangem.datasource.api.common.MoshiConverter -import com.tangem.datasource.api.tangemTech.TangemTechApi -import com.tangem.datasource.api.tangemTech.models.CoinsResponse -import com.tangem.datasource.asset.AssetReader -import com.tangem.domain.common.extensions.toNetworkId -import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import kotlinx.coroutines.withContext - -class LoadAvailableCoinsService( - private val tangemTechApi: TangemTechApi, - private val dispatchers: CoroutineDispatcherProvider, - private val assetReader: AssetReader, -) { - private val currenciesAdapter: JsonAdapter = - MoshiConverter.networkMoshi.adapter(CurrenciesFromJson::class.java) - - suspend fun getSupportedTokens( - isTestNet: Boolean, - supportedBlockchains: List, - page: Int, - searchInput: String?, - ): Result { - if (isTestNet) { - return Result.Success( - LoadedCoins( - currencies = getTestnetCoins().filter(searchInput), - moreAvailable = false, - ), - ) - } - - val offset = page * LOAD_PER_PAGE - return when (val result = loadCoins(supportedBlockchains, offset, searchInput)) { - is Result.Success -> { - val data = result.data - - Result.Success( - LoadedCoins( - currencies = data.coins.map { - Currency.fromCoinResponse(currency = it, imageHost = data.imageHost) - }, - moreAvailable = data.total > offset + LOAD_PER_PAGE, - ), - ) - } - is Result.Failure -> { - Result.Failure(result.error) - } - } - } - - private suspend fun loadCoins( - supportedBlockchains: List, - offset: Int, - searchInput: String?, - ): Result { - return withContext(dispatchers.io) { - runCatching { - tangemTechApi.getCoins( - networkIds = supportedBlockchains.joinToString( - separator = ",", - transform = Blockchain::toNetworkId, - ), - active = true, - searchText = searchInput, - offset = offset, - limit = LOAD_PER_PAGE, - ) - }.fold( - onSuccess = { Result.Success(it) }, - onFailure = { Result.Failure(it) }, - ) - } - } - - private fun getTestnetCoins(): List { - val json = assetReader.readJson(FILE_NAME_TESTNET_COINS) - return currenciesAdapter.fromJson(json)!!.coins - .map { Currency.fromJsonObject(it) } - } - - private fun List.filter(searchInput: String?): List { - if (searchInput.isNullOrBlank()) return this - - return filter { currency -> - currency.symbol.contains(searchInput, ignoreCase = true) || - currency.name.contains(searchInput, ignoreCase = true) - } - } - - private companion object { - const val LOAD_PER_PAGE = 100 - const val FILE_NAME_TESTNET_COINS = "testnet_tokens" - } -} - -data class LoadedCoins( - val currencies: List, - val moreAvailable: Boolean, -) \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/wallet/converters/CryptoCurrencyConverter.kt b/app/src/main/java/com/tangem/tap/features/wallet/converters/CryptoCurrencyConverter.kt index 8b22dd95e6..832de0bda1 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/converters/CryptoCurrencyConverter.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/converters/CryptoCurrencyConverter.kt @@ -1,13 +1,15 @@ package com.tangem.tap.features.wallet.converters +import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchain.common.Token import com.tangem.data.tokens.utils.CryptoCurrencyFactory import com.tangem.domain.common.util.derivationStyleProvider import com.tangem.domain.tokens.models.CryptoCurrency import com.tangem.tap.features.wallet.models.Currency import com.tangem.tap.store -import com.tangem.utils.converter.Converter +import com.tangem.utils.converter.TwoWayConverter -internal class CryptoCurrencyConverter : Converter { +internal class CryptoCurrencyConverter : TwoWayConverter { private val cryptoCurrencyFactory by lazy { CryptoCurrencyFactory() } @@ -40,4 +42,26 @@ internal class CryptoCurrencyConverter : Converter { ) } } + + override fun convertBack(value: CryptoCurrency): Currency { + val blockchain = Blockchain.fromId(value.network.id.value) + if (blockchain == Blockchain.Unknown) error("CryptoCurrencyConverter convertBack Unknown blockchain") + return when (value) { + is CryptoCurrency.Coin -> Currency.Blockchain( + blockchain = blockchain, + derivationPath = value.derivationPath, + ) + is CryptoCurrency.Token -> Currency.Token( + token = Token( + name = value.name, + symbol = value.symbol, + contractAddress = value.contractAddress, + decimals = value.decimals, + id = value.id.value, + ), + blockchain = blockchain, + derivationPath = value.derivationPath, + ) + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/network/exchangeServices/DefaultRampManager.kt b/app/src/main/java/com/tangem/tap/network/exchangeServices/DefaultRampManager.kt new file mode 100644 index 0000000000..9c9184e488 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/network/exchangeServices/DefaultRampManager.kt @@ -0,0 +1,21 @@ +package com.tangem.tap.network.exchangeServices + +import com.tangem.domain.exchange.RampStateManager +import com.tangem.domain.tokens.models.CryptoCurrency +import com.tangem.tap.features.wallet.converters.CryptoCurrencyConverter + +class DefaultRampManager(private val exchangeService: ExchangeService?) : RampStateManager { + + private val cryptoCurrencyConverter = CryptoCurrencyConverter() + override fun availableForBuy(cryptoCurrency: CryptoCurrency): Boolean { + return exchangeService?.availableForBuy( + currency = cryptoCurrencyConverter.convertBack(cryptoCurrency), + ) ?: false + } + + override fun availableForSell(cryptoCurrency: CryptoCurrency): Boolean { + return exchangeService?.availableForSell( + currency = cryptoCurrencyConverter.convertBack(cryptoCurrency), + ) ?: false + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/proxy/AppStateHolder.kt b/app/src/main/java/com/tangem/tap/proxy/AppStateHolder.kt index cdcbc7fd9a..5aa105d76a 100644 --- a/app/src/main/java/com/tangem/tap/proxy/AppStateHolder.kt +++ b/app/src/main/java/com/tangem/tap/proxy/AppStateHolder.kt @@ -14,6 +14,7 @@ import com.tangem.tap.domain.TangemSdkManager import com.tangem.tap.domain.tokens.UserTokensRepository import com.tangem.tap.domain.walletStores.WalletStoresManager import com.tangem.tap.features.wallet.redux.WalletState +import com.tangem.tap.network.exchangeServices.ExchangeService import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import org.rekotlin.Action @@ -45,6 +46,7 @@ class AppStateHolder @Inject constructor() : WalletsStateHolder, ReduxNavControl var tangemSdkManager: TangemSdkManager? = null var walletStoresManager: WalletStoresManager? = null var appFiatCurrency: FiatCurrency = FiatCurrency.Default + var exchangeService: ExchangeService? = null fun getActualCard(): CardDTO? { return scanResponse?.card diff --git a/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphState.kt b/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphState.kt index c677f70300..324719823c 100644 --- a/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphState.kt +++ b/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphState.kt @@ -16,6 +16,7 @@ import com.tangem.tap.domain.walletconnect2.domain.WalletConnectInteractor import com.tangem.tap.domain.walletconnect2.domain.WalletConnectRepository import com.tangem.tap.domain.walletconnect2.domain.WalletConnectSessionsRepository import com.tangem.tap.features.customtoken.api.featuretoggles.CustomTokenFeatureToggles +import com.tangem.tap.proxy.AppStateHolder import org.rekotlin.StateType data class DaggerGraphState( @@ -35,6 +36,7 @@ data class DaggerGraphState( val cardSdkConfigRepository: CardSdkConfigRepository? = null, val appCurrencyRepository: AppCurrencyRepository? = null, val walletManagersFacade: WalletManagersFacade? = null, + val appStateHolder: AppStateHolder? = null, ) : StateType { inline fun get(getDependency: DaggerGraphState.() -> T?): T { diff --git a/core/datasource/src/main/java/com/tangem/datasource/di/MarketCoinsStoreModule.kt b/core/datasource/src/main/java/com/tangem/datasource/di/MarketCoinsStoreModule.kt new file mode 100644 index 0000000000..bd2098a670 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/di/MarketCoinsStoreModule.kt @@ -0,0 +1,21 @@ +package com.tangem.datasource.di + +import com.tangem.datasource.local.datastore.RuntimeDataStore +import com.tangem.datasource.local.token.DefaultUserMarketCoinsStore +import com.tangem.datasource.local.token.UserMarketCoinsStore +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal object MarketCoinsStoreModule { + + @Provides + @Singleton + fun provideUserMarketCoinsStore(): UserMarketCoinsStore { + return DefaultUserMarketCoinsStore(dataStore = RuntimeDataStore()) + } +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/token/DefaultUserMarketCoinsStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/token/DefaultUserMarketCoinsStore.kt new file mode 100644 index 0000000000..c08a627df9 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/local/token/DefaultUserMarketCoinsStore.kt @@ -0,0 +1,18 @@ +package com.tangem.datasource.local.token + +import com.tangem.datasource.api.tangemTech.models.CoinsResponse +import com.tangem.datasource.local.datastore.core.StringKeyDataStore +import com.tangem.domain.wallets.models.UserWalletId + +internal class DefaultUserMarketCoinsStore( + private val dataStore: StringKeyDataStore, +) : UserMarketCoinsStore { + + override suspend fun getSyncOrNull(userWalletId: UserWalletId): CoinsResponse? { + return dataStore.getSyncOrNull(userWalletId.stringValue) + } + + override suspend fun store(userWalletId: UserWalletId, item: CoinsResponse) { + dataStore.store(userWalletId.stringValue, item) + } +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/token/UserMarketCoinsStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/token/UserMarketCoinsStore.kt new file mode 100644 index 0000000000..10895699ac --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/local/token/UserMarketCoinsStore.kt @@ -0,0 +1,11 @@ +package com.tangem.datasource.local.token + +import com.tangem.datasource.api.tangemTech.models.CoinsResponse +import com.tangem.domain.wallets.models.UserWalletId + +interface UserMarketCoinsStore { + + suspend fun getSyncOrNull(userWalletId: UserWalletId): CoinsResponse? + + suspend fun store(userWalletId: UserWalletId, item: CoinsResponse) +} \ 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 c1229cde5b..cd55cde081 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 @@ -2,14 +2,17 @@ package com.tangem.data.tokens.di import com.tangem.data.common.cache.CacheRegistry import com.tangem.data.tokens.repository.DefaultCurrenciesRepository +import com.tangem.data.tokens.repository.DefaultMarketCryptoCurrencyRepository import com.tangem.data.tokens.repository.DefaultNetworksRepository import com.tangem.data.tokens.repository.DefaultQuotesRepository import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.local.appcurrency.SelectedAppCurrencyStore import com.tangem.datasource.local.quote.QuotesStore +import com.tangem.datasource.local.token.UserMarketCoinsStore import com.tangem.datasource.local.token.UserTokensStore import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.domain.tokens.repository.CurrenciesRepository +import com.tangem.domain.tokens.repository.MarketCryptoCurrencyRepository import com.tangem.domain.tokens.repository.NetworksRepository import com.tangem.domain.tokens.repository.QuotesRepository import com.tangem.domain.walletmanager.WalletManagersFacade @@ -30,10 +33,18 @@ internal object TokensDataModule { tangemTechApi: TangemTechApi, userTokensStore: UserTokensStore, userWalletsStore: UserWalletsStore, + userMarketCoinsStore: UserMarketCoinsStore, cacheRegistry: CacheRegistry, dispatchers: CoroutineDispatcherProvider, ): CurrenciesRepository { - return DefaultCurrenciesRepository(tangemTechApi, userTokensStore, userWalletsStore, cacheRegistry, dispatchers) + return DefaultCurrenciesRepository( + tangemTechApi = tangemTechApi, + userTokensStore = userTokensStore, + userWalletsStore = userWalletsStore, + userMarketCoinsStore = userMarketCoinsStore, + cacheRegistry = cacheRegistry, + dispatchers = dispatchers, + ) } @Provides @@ -46,11 +57,11 @@ internal object TokensDataModule { dispatchers: CoroutineDispatcherProvider, ): QuotesRepository { return DefaultQuotesRepository( - tangemTechApi, - quotesStore, - selectedAppCurrencyStore, - cacheRegistry, - dispatchers, + tangemTechApi = tangemTechApi, + quotesStore = quotesStore, + selectedAppCurrencyStore = selectedAppCurrencyStore, + cacheRegistry = cacheRegistry, + dispatchers = dispatchers, ) } @@ -64,11 +75,19 @@ internal object TokensDataModule { dispatchers: CoroutineDispatcherProvider, ): NetworksRepository { return DefaultNetworksRepository( - walletManagersFacade, - userWalletsStore, - userTokensStore, - cacheRegistry, - dispatchers, + walletManagersFacade = walletManagersFacade, + userWalletsStore = userWalletsStore, + userTokensStore = userTokensStore, + cacheRegistry = cacheRegistry, + dispatchers = dispatchers, ) } + + @Provides + @Singleton + fun provideDefaultMarketCoinsRepository( + userMarketCoinsStore: UserMarketCoinsStore, + ): MarketCryptoCurrencyRepository { + return DefaultMarketCryptoCurrencyRepository(userMarketCoinsStore) + } } \ No newline at end of file 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 0df288c0c0..d90b0321e4 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 @@ -6,6 +6,7 @@ import com.tangem.data.tokens.utils.ResponseCurrenciesFactory import com.tangem.data.tokens.utils.UserTokensResponseFactory import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.api.tangemTech.models.UserTokensResponse +import com.tangem.datasource.local.token.UserMarketCoinsStore import com.tangem.datasource.local.token.UserTokensStore import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.domain.common.util.derivationStyleProvider @@ -27,6 +28,7 @@ internal class DefaultCurrenciesRepository( private val tangemTechApi: TangemTechApi, private val userTokensStore: UserTokensStore, private val userWalletsStore: UserWalletsStore, + private val userMarketCoinsStore: UserMarketCoinsStore, private val cacheRegistry: CacheRegistry, private val dispatchers: CoroutineDispatcherProvider, ) : CurrenciesRepository { @@ -172,6 +174,7 @@ internal class DefaultCurrenciesRepository( val response = tangemTechApi.getUserTokens(userWallet.walletId.stringValue) userTokensStore.store(userWallet.walletId, response) + fetchUserMarketCoinsByIds(userWallet.walletId, response) } catch (e: Throwable) { handleFetchTokensErrorOrThrow(userWallet, e) } @@ -182,6 +185,17 @@ internal class DefaultCurrenciesRepository( tangemTechApi.saveUserTokens(userWalletId.stringValue, response) } + private suspend fun fetchUserMarketCoinsByIds(userWalletId: UserWalletId, userTokens: UserTokensResponse) { + try { + val response = tangemTechApi.getCoins( + networkIds = userTokens.tokens.joinToString(separator = ",") { it.networkId }, + ) + userMarketCoinsStore.store(userWalletId, response) + } catch (e: Throwable) { + Timber.e("Unable to fetch user market coins for: ${userWalletId.stringValue} ${e.message}") + } + } + private suspend fun handleFetchTokensErrorOrThrow(userWallet: UserWallet, error: Throwable) { val errorMessage = error.message ?: throw error diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultMarketCryptoCurrencyRepository.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultMarketCryptoCurrencyRepository.kt new file mode 100644 index 0000000000..891a7c14d2 --- /dev/null +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultMarketCryptoCurrencyRepository.kt @@ -0,0 +1,18 @@ +package com.tangem.data.tokens.repository + +import com.tangem.datasource.local.token.UserMarketCoinsStore +import com.tangem.domain.tokens.models.CryptoCurrency +import com.tangem.domain.tokens.repository.MarketCryptoCurrencyRepository +import com.tangem.domain.wallets.models.UserWalletId + +class DefaultMarketCryptoCurrencyRepository( + private val userMarketCoinsStore: UserMarketCoinsStore, +) : MarketCryptoCurrencyRepository { + + override suspend fun isExchangeable(userWalletId: UserWalletId, cryptoCurrencyId: CryptoCurrency.ID): Boolean { + return userMarketCoinsStore.getSyncOrNull(userWalletId)?.coins + ?.firstOrNull { it.id == cryptoCurrencyId.rawCurrencyId } + ?.networks + ?.firstOrNull { it.networkId == cryptoCurrencyId.rawNetworkId }?.exchangeable ?: false + } +} \ No newline at end of file diff --git a/domain/legacy/src/main/java/com/tangem/domain/exchange/RampStateManager.kt b/domain/legacy/src/main/java/com/tangem/domain/exchange/RampStateManager.kt new file mode 100644 index 0000000000..b6a2e29f06 --- /dev/null +++ b/domain/legacy/src/main/java/com/tangem/domain/exchange/RampStateManager.kt @@ -0,0 +1,11 @@ +package com.tangem.domain.exchange + +import com.tangem.domain.tokens.models.CryptoCurrency + +/** + * Manager that holds info about available actions as Sell and Buy + */ +interface RampStateManager { + fun availableForBuy(cryptoCurrency: CryptoCurrency): Boolean + fun availableForSell(cryptoCurrency: CryptoCurrency): Boolean +} \ No newline at end of file diff --git a/domain/tokens/build.gradle.kts b/domain/tokens/build.gradle.kts index 6fae380259..17599c040d 100644 --- a/domain/tokens/build.gradle.kts +++ b/domain/tokens/build.gradle.kts @@ -1,13 +1,19 @@ plugins { - alias(deps.plugins.kotlin.jvm) + alias(deps.plugins.android.library) + alias(deps.plugins.kotlin.android) id("configuration") } +android { + namespace = "com.tangem.domain.tokens" +} + dependencies { /** Project - Domain */ implementation(projects.domain.core) implementation(projects.domain.models) + implementation(projects.domain.legacy) implementation(projects.domain.tokens.models) implementation(projects.domain.wallets.models) implementation(projects.domain.appCurrency.models) diff --git a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/models/CryptoCurrency.kt b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/models/CryptoCurrency.kt index 4fc784b4a8..d20a5f6091 100644 --- a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/models/CryptoCurrency.kt +++ b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/models/CryptoCurrency.kt @@ -93,6 +93,8 @@ sealed class CryptoCurrency : Serializable { val rawCurrencyId: String? = (suffix as? Suffix.RawID)?.rawId + val rawNetworkId: String = networkId.value + /** * Represents the different types of prefixes that can be associated with a cryptocurrency ID. * These prefixes can help in quickly categorizing the type of cryptocurrency. diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyActionsUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyActionsUseCase.kt index d705aacf94..8f0f5499af 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyActionsUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyActionsUseCase.kt @@ -1,34 +1,84 @@ package com.tangem.domain.tokens +import com.tangem.domain.exchange.RampStateManager import com.tangem.domain.tokens.model.TokenActionsState +import com.tangem.domain.tokens.models.CryptoCurrency +import com.tangem.domain.tokens.repository.MarketCryptoCurrencyRepository import com.tangem.domain.wallets.models.UserWalletId import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flowOn +/** + * Use case to determine which TokenActions are available for a [CryptoCurrency] + * + * @property rampManager Ramp manager to check ramp availability + */ class GetCryptoCurrencyActionsUseCase( + private val rampManager: RampStateManager, + private val marketCryptoCurrencyRepository: MarketCryptoCurrencyRepository, private val dispatchers: CoroutineDispatcherProvider, ) { - operator fun invoke(userWalletId: UserWalletId, tokenId: String): Flow { + operator fun invoke(userWalletId: UserWalletId, cryptoCurrency: CryptoCurrency): Flow { return flow { - emit(getMockState(userWalletId, tokenId)) + val actionStates = createTokenActionsState(userWalletId, cryptoCurrency) + emit(actionStates) }.flowOn(dispatchers.io) } - // TODO replace by real data - private fun getMockState(userWalletId: UserWalletId, tokenId: String): TokenActionsState { + private suspend fun createTokenActionsState( + userWalletId: UserWalletId, + cryptoCurrency: CryptoCurrency, + ): TokenActionsState { return TokenActionsState( walletId = userWalletId, - tokenId = tokenId, - states = listOf( - TokenActionsState.ActionState.Buy(true), - TokenActionsState.ActionState.Send(true), - TokenActionsState.ActionState.Receive(true), - TokenActionsState.ActionState.Sell(true), - TokenActionsState.ActionState.Swap(true), - ), + cryptoCurrencyId = cryptoCurrency.id, + states = createListOfActions(userWalletId, cryptoCurrency), ) } + + /** + * Creates list of action for expected order + * Actions priority: [Buy Send Receive Sell Swap] + */ + private suspend fun createListOfActions( + userWalletId: UserWalletId, + cryptoCurrency: CryptoCurrency, + ): List { + return buildList { + // todo add check available in swap 1inch etc if backend doen't handle it + if (marketCryptoCurrencyRepository.isExchangeable(userWalletId, cryptoCurrency.id) && + !isCustomToken(cryptoCurrency) + ) { + addFirst(TokenActionsState.ActionState.Swap(true)) + } else { + add(TokenActionsState.ActionState.Swap(false)) + } + + if (rampManager.availableForSell(cryptoCurrency)) { + addFirst(TokenActionsState.ActionState.Sell(true)) + } else { + add(TokenActionsState.ActionState.Sell(false)) + } + + addFirst(TokenActionsState.ActionState.Receive(true)) + addFirst(TokenActionsState.ActionState.Send(true)) + + if (rampManager.availableForBuy(cryptoCurrency)) { + addFirst(TokenActionsState.ActionState.Buy(true)) + } else { + add(TokenActionsState.ActionState.Buy(false)) + } + } + } + + private fun isCustomToken(currency: CryptoCurrency): Boolean { + return currency is CryptoCurrency.Token && currency.isCustom + } + + private fun MutableList.addFirst(item: T) { + this.add(0, item) + } } \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/TokenActionsState.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/TokenActionsState.kt index b2d6e6e71b..81c3d9a53f 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/TokenActionsState.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/TokenActionsState.kt @@ -1,10 +1,11 @@ package com.tangem.domain.tokens.model +import com.tangem.domain.tokens.models.CryptoCurrency import com.tangem.domain.wallets.models.UserWalletId data class TokenActionsState( val walletId: UserWalletId, - val tokenId: String, + val cryptoCurrencyId: CryptoCurrency.ID, val states: List, ) { diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/MarketCryptoCurrencyRepository.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/MarketCryptoCurrencyRepository.kt new file mode 100644 index 0000000000..a8c12b480d --- /dev/null +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/MarketCryptoCurrencyRepository.kt @@ -0,0 +1,12 @@ +package com.tangem.domain.tokens.repository + +import com.tangem.domain.tokens.models.CryptoCurrency +import com.tangem.domain.wallets.models.UserWalletId + +/** + * MarketCryptoCurrencyRepository works with data from Tangem coins backend, CoinMarketCap etc + */ +interface MarketCryptoCurrencyRepository { + + suspend fun isExchangeable(userWalletId: UserWalletId, cryptoCurrencyId: CryptoCurrency.ID): Boolean +} \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt index f4defde6c4..76357b097f 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt @@ -84,12 +84,12 @@ internal class TokenDetailsViewModel @Inject constructor( private fun updateContent(selectedWallet: UserWallet) { updateMarketPrice(selectedWallet = selectedWallet) - updateButtons(userWalletId = selectedWallet.walletId, currencyId = cryptoCurrency.id.value) + updateButtons(userWalletId = selectedWallet.walletId, currency = cryptoCurrency) updateTxHistory() } - private fun updateButtons(userWalletId: UserWalletId, currencyId: String) { - getCryptoCurrencyActionsUseCase(userWalletId = userWalletId, tokenId = currencyId) + private fun updateButtons(userWalletId: UserWalletId, currency: CryptoCurrency) { + getCryptoCurrencyActionsUseCase(userWalletId = userWalletId, cryptoCurrency = currency) .distinctUntilChanged() .onEach { uiState = stateFactory.getManageButtonsState(actions = it.states) } .flowOn(dispatchers.io) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt index 509377d260..c02df958f3 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt @@ -201,7 +201,6 @@ internal class WalletViewModel @Inject constructor( private fun updateSingleCurrencyContent(index: Int, isRefreshing: Boolean) { val wallet = getWallet(index) val blockchain = getCardTypeResolver(index).getBlockchain() - updateButtons(userWalletId = wallet.walletId, currencyId = blockchain.id) updateTxHistory( blockchain = blockchain, derivationStyle = wallet.scanResponse.derivationStyleProvider.getDerivationStyle(), @@ -244,15 +243,18 @@ internal class WalletViewModel @Inject constructor( isRefreshing = isRefreshing, ) - either.onRight { status -> cryptoCurrencyStatus = status } + either.onRight { status -> + cryptoCurrencyStatus = status + updateButtons(userWalletId = userWalletId, currency = status.currency) + } } .flowOn(dispatchers.io) .launchIn(viewModelScope) .saveIn(marketPriceJobHolder) } - private fun updateButtons(userWalletId: UserWalletId, currencyId: String) { - getCryptoCurrencyActionsUseCase(userWalletId = userWalletId, tokenId = currencyId) + private fun updateButtons(userWalletId: UserWalletId, currency: CryptoCurrency) { + getCryptoCurrencyActionsUseCase(userWalletId = userWalletId, cryptoCurrency = currency) .distinctUntilChanged() .onEach { uiState = stateFactory.getSingleCurrencyManageButtonsState(actions = it.states) } .flowOn(dispatchers.io) From 56bf8bf3e906b79e5a2919ce8b5fd7dbf2884651 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 31 Aug 2023 17:41:00 +0300 Subject: [PATCH 002/242] Updated on 2026-08-14 --- .../features/details/redux/DetailsAction.kt | 4 +- .../features/details/redux/DetailsReducer.kt | 3 +- .../features/details/redux/DetailsState.kt | 4 +- .../details/ui/details/DetailsViewModel.kt | 65 +++++++++++++------ .../tap/features/wallet/ui/WalletFragment.kt | 15 +---- 5 files changed, 50 insertions(+), 41 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsAction.kt b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsAction.kt index 5782c7b0e2..c2112f12fe 100644 --- a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsAction.kt +++ b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsAction.kt @@ -1,8 +1,7 @@ package com.tangem.tap.features.details.redux -import com.tangem.blockchain.common.Wallet -import com.tangem.domain.models.scan.CardDTO import com.tangem.domain.common.CardTypesResolver +import com.tangem.domain.models.scan.CardDTO import com.tangem.domain.models.scan.ScanResponse import com.tangem.tap.common.entities.FiatCurrency import org.rekotlin.Action @@ -11,7 +10,6 @@ sealed class DetailsAction : Action { data class PrepareScreen( val scanResponse: ScanResponse, - val wallets: List, ) : DetailsAction() object ReCreateTwinsWallet : DetailsAction() diff --git a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsReducer.kt b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsReducer.kt index e28b9cd398..71e4d6b2dd 100644 --- a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsReducer.kt +++ b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsReducer.kt @@ -9,7 +9,7 @@ import com.tangem.tap.preferencesStorage import com.tangem.tap.store import com.tangem.tap.tangemSdkManager import org.rekotlin.Action -import java.util.* +import java.util.EnumSet object DetailsReducer { fun reduce(action: Action, state: AppState): DetailsState = internalReduce(action, state) @@ -49,7 +49,6 @@ private fun internalReduce(action: Action, state: AppState): DetailsState { private fun handlePrepareScreen(action: DetailsAction.PrepareScreen): DetailsState { return DetailsState( scanResponse = action.scanResponse, - wallets = action.wallets, createBackupAllowed = action.scanResponse.card.backupStatus == CardDTO.BackupStatus.NoBackup, appCurrency = store.state.globalState.appCurrency, appSettingsState = AppSettingsState( diff --git a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsState.kt b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsState.kt index 6fd17a4119..6ad80685b2 100644 --- a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsState.kt +++ b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsState.kt @@ -1,16 +1,14 @@ package com.tangem.tap.features.details.redux -import com.tangem.blockchain.common.Wallet import com.tangem.domain.models.scan.CardDTO import com.tangem.domain.models.scan.ScanResponse import com.tangem.tap.common.entities.Button import com.tangem.tap.common.entities.FiatCurrency import org.rekotlin.StateType -import java.util.* +import java.util.EnumSet data class DetailsState( val scanResponse: ScanResponse? = null, - val wallets: List = emptyList(), val cardSettingsState: CardSettingsState? = null, val privacyPolicyUrl: String? = null, val createBackupAllowed: Boolean = false, diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsViewModel.kt b/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsViewModel.kt index 8301791338..208d964f58 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsViewModel.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsViewModel.kt @@ -7,16 +7,25 @@ import com.tangem.core.navigation.AppScreen import com.tangem.core.navigation.NavigationAction import com.tangem.domain.common.util.cardTypesResolver import com.tangem.tap.common.analytics.events.Settings +import com.tangem.tap.common.extensions.dispatchWithMain import com.tangem.tap.common.feedback.FeedbackEmail import com.tangem.tap.common.feedback.SupportInfo import com.tangem.tap.common.redux.AppState import com.tangem.tap.common.redux.global.GlobalAction +import com.tangem.tap.features.details.redux.DetailsAction import com.tangem.tap.features.details.redux.DetailsState import com.tangem.tap.features.disclaimer.redux.DisclaimerAction import com.tangem.tap.features.home.LocaleRegionProvider import com.tangem.tap.features.home.RUSSIA_COUNTRY_CODE import com.tangem.tap.features.wallet.redux.WalletAction +import com.tangem.tap.scope +import com.tangem.tap.userWalletsListManager import com.tangem.wallet.BuildConfig +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach import org.rekotlin.Store class DetailsViewModel(private val store: Store) { @@ -24,29 +33,13 @@ class DetailsViewModel(private val store: Store) { var detailsScreenState: MutableState = mutableStateOf(updateState(store.state.detailsState)) private set - @Suppress("ComplexMethod") - fun updateState(state: DetailsState): DetailsScreenState { - val cardTypesResolver = state.scanResponse?.cardTypesResolver - val settings = SettingsElement.values().mapNotNull { - when (it) { - SettingsElement.WalletConnect -> { - if (cardTypesResolver?.isMultiwalletAllowed() == true) it else null - } - SettingsElement.SendFeedback -> it - SettingsElement.LinkMoreCards -> if (state.createBackupAllowed) it else null - SettingsElement.PrivacyPolicy -> { - if (state.privacyPolicyUrl != null) it else null - } - SettingsElement.AppSettings -> if (state.appSettingsState.isBiometricsAvailable) it else null - SettingsElement.AppCurrency -> if (cardTypesResolver?.isMultiwalletAllowed() != true) it else null - SettingsElement.ReferralProgram -> if (cardTypesResolver?.isTangemWallet() == true) it else null - SettingsElement.TesterMenu -> if (BuildConfig.TESTER_MENU_ENABLED) it else null - else -> it - } - } + init { + bootstrapScreenState() + } + fun updateState(state: DetailsState): DetailsScreenState { return DetailsScreenState( - elements = settings, + elements = createSettingsItems(state), tangemLinks = getSocialLinks(), tangemVersion = getTangemAppVersion(), appCurrency = state.appCurrency.name, @@ -55,6 +48,26 @@ class DetailsViewModel(private val store: Store) { ) } + @Suppress("ComplexMethod") + private fun createSettingsItems(state: DetailsState): List { + val scanResponse = state.scanResponse ?: return emptyList() + val cardTypesResolver = scanResponse.cardTypesResolver + + return SettingsElement.values().mapNotNull { + when (it) { + SettingsElement.WalletConnect -> if (cardTypesResolver.isMultiwalletAllowed()) it else null + SettingsElement.SendFeedback -> it + SettingsElement.LinkMoreCards -> if (state.createBackupAllowed) it else null + SettingsElement.PrivacyPolicy -> if (state.privacyPolicyUrl != null) it else null + SettingsElement.AppSettings -> if (state.appSettingsState.isBiometricsAvailable) it else null + SettingsElement.AppCurrency -> if (cardTypesResolver.isMultiwalletAllowed()) null else it + SettingsElement.ReferralProgram -> if (cardTypesResolver.isTangemWallet()) it else null + SettingsElement.TesterMenu -> if (BuildConfig.TESTER_MENU_ENABLED) it else null + else -> it + } + } + } + private fun handleSocialNetworkClick(link: SocialNetworkLink) { Analytics.send(Settings.ButtonSocialNetwork(link.network)) store.dispatch(NavigationAction.OpenUrl(link.url)) @@ -118,4 +131,14 @@ class DetailsViewModel(private val store: Store) { val versionName: String = BuildConfig.VERSION_NAME return "$versionName ($versionCode)" } + + private fun bootstrapScreenState() { + userWalletsListManager.selectedUserWallet + .distinctUntilChanged() + .onEach { selectedUserWallet -> + store.dispatchWithMain(DetailsAction.PrepareScreen(selectedUserWallet.scanResponse)) + } + .flowOn(Dispatchers.IO) + .launchIn(scope) + } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/wallet/ui/WalletFragment.kt b/app/src/main/java/com/tangem/tap/features/wallet/ui/WalletFragment.kt index cec1e05dd8..2164a5028d 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/ui/WalletFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/ui/WalletFragment.kt @@ -43,7 +43,6 @@ import com.tangem.tap.common.utils.SafeStoreSubscriber import com.tangem.tap.domain.configurable.warningMessage.WarningMessage import com.tangem.tap.domain.statePrinter.printScanResponseState import com.tangem.tap.domain.statePrinter.printWalletState -import com.tangem.tap.features.details.redux.DetailsAction import com.tangem.tap.features.wallet.redux.ErrorType import com.tangem.tap.features.wallet.redux.ProgressState import com.tangem.tap.features.wallet.redux.WalletAction @@ -313,17 +312,9 @@ class WalletFragment : Fragment(R.layout.fragment_wallet), SafeStoreSubscriber { store.dispatch(GlobalAction.UpdateFeedbackInfo(store.state.walletState.walletManagers)) - store.state.globalState.scanResponse?.let { scanResponse -> - store.dispatch( - DetailsAction.PrepareScreen( - scanResponse = scanResponse, - wallets = store.state.walletState.walletManagers.map { it.wallet }, - ), - ) - store.dispatch(NavigationAction.NavigateTo(AppScreen.Details)) - true - } - false + store.dispatch(NavigationAction.NavigateTo(AppScreen.Details)) + + true } else -> super.onOptionsItemSelected(item) } From 537731103fd42180b7c93196f781d20a2fe29492 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 1 Sep 2023 11:39:27 +0500 Subject: [PATCH 003/242] Updated on 2026-08-14 --- .../viewmodels/TokensListViewModel.kt | 2 +- .../com/tangem/core/ui/res/TangemDimens.kt | 2 +- .../models/remove/RemoveCurrencyError.kt | 3 - .../domain/tokens/RemoveCurrencyUseCase.kt | 10 +- features/tokendetails/impl/build.gradle.kts | 1 + .../tokendetails/TokenDetailsPreviewData.kt | 17 +- .../state/TokenDetailsAppBarMenuConfig.kt | 14 ++ .../tokendetails/state/TokenDetailsState.kt | 2 + .../state/TokenDetailsTopAppBarConfig.kt | 4 +- .../tokendetails/state/TokenInfoBlockState.kt | 2 +- .../components/TokenDetailsDialogConfig.kt | 81 +++++++ .../TokenDetailsSkeletonStateConverter.kt | 17 +- .../state/factory/TokenDetailsStateFactory.kt | 34 +++ .../tokendetails/ui/TokenDetailsScreen.kt | 3 + .../ui/components/DropdownMenu.kt | 229 ++++++++++++++++++ .../ui/components/TokenDetailsDialogs.kt | 37 +++ .../ui/components/TokenDetailsTopAppBar.kt | 84 ++++++- .../viewmodels/TokenDetailsClickIntents.kt | 8 +- .../viewmodels/TokenDetailsViewModel.kt | 30 ++- .../ui/components/TokenActionsBottomSheet.kt | 2 +- gradle/dependencies.toml | 1 + 21 files changed, 557 insertions(+), 26 deletions(-) create mode 100644 features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsAppBarMenuConfig.kt create mode 100644 features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/components/TokenDetailsDialogConfig.kt create mode 100644 features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/DropdownMenu.kt create mode 100644 features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenDetailsDialogs.kt diff --git a/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/viewmodels/TokensListViewModel.kt b/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/viewmodels/TokensListViewModel.kt index 1cac67b7b2..78ff6ff6ce 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/viewmodels/TokensListViewModel.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/viewmodels/TokensListViewModel.kt @@ -378,7 +378,7 @@ internal class TokensListViewModel @Inject constructor( val supportedTokens = scanResponse.card.supportedTokens(cardTypesResolver) // refactor this later by moving all this logic in card config - if (!supportedTokens.contains(Blockchain.Solana)) { + if (blockchain == Blockchain.Solana && !supportedTokens.contains(Blockchain.Solana)) { return SupportTokensState.SolanaNetworkUnsupported } val canHandleToken = scanResponse.card.canHandleToken( diff --git a/core/ui/src/main/java/com/tangem/core/ui/res/TangemDimens.kt b/core/ui/src/main/java/com/tangem/core/ui/res/TangemDimens.kt index b4fd692f7e..bacd7c615f 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/res/TangemDimens.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/res/TangemDimens.kt @@ -4,7 +4,7 @@ import androidx.compose.runtime.Immutable import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp -@Suppress("ConstructorParameterNaming") +@Suppress("ConstructorParameterNaming", "MagicNumber") @Immutable data class TangemDimens internal constructor( // region Elevation diff --git a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/models/remove/RemoveCurrencyError.kt b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/models/remove/RemoveCurrencyError.kt index 906f30f1fd..0c6f1a0561 100644 --- a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/models/remove/RemoveCurrencyError.kt +++ b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/models/remove/RemoveCurrencyError.kt @@ -1,8 +1,5 @@ package com.tangem.domain.tokens.models.remove -import com.tangem.domain.tokens.models.CryptoCurrency - sealed class RemoveCurrencyError : Throwable() { - data class HasLinkedTokens(val currency: CryptoCurrency) : RemoveCurrencyError() data class DataError(override val cause: Throwable) : RemoveCurrencyError() } \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/RemoveCurrencyUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/RemoveCurrencyUseCase.kt index 71dc767ed2..dba5642239 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/RemoveCurrencyUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/RemoveCurrencyUseCase.kt @@ -3,7 +3,6 @@ package com.tangem.domain.tokens import arrow.core.Either import arrow.core.raise.catch import arrow.core.raise.either -import arrow.core.raise.ensure import com.tangem.domain.tokens.models.CryptoCurrency import com.tangem.domain.tokens.models.remove.RemoveCurrencyError import com.tangem.domain.tokens.repository.CurrenciesRepository @@ -20,10 +19,6 @@ class RemoveCurrencyUseCase( currency: CryptoCurrency, ): Either { return either { - ensure( - condition = !currency.hasLinkedTokens(userWalletId), - raise = { RemoveCurrencyError.HasLinkedTokens(currency) }, - ) catch( block = { currenciesRepository.removeCurrency(userWalletId, currency) }, catch = { raise(RemoveCurrencyError.DataError(it)) }, @@ -31,10 +26,11 @@ class RemoveCurrencyUseCase( } } - private suspend fun CryptoCurrency.hasLinkedTokens(userWalletId: UserWalletId): Boolean { + suspend fun hasLinkedTokens(userWalletId: UserWalletId, currency: CryptoCurrency): Boolean { val walletCurrencies = currenciesRepository .getMultiCurrencyWalletCurrenciesSync(userWalletId = userWalletId, refresh = false) - return this is CryptoCurrency.Coin && walletCurrencies.any { it != this && it.network.id == this.network.id } + return currency is CryptoCurrency.Coin && + walletCurrencies.any { it != currency && it.network.id == currency.network.id } } } \ No newline at end of file diff --git a/features/tokendetails/impl/build.gradle.kts b/features/tokendetails/impl/build.gradle.kts index 1957acae29..7f3fcd1357 100644 --- a/features/tokendetails/impl/build.gradle.kts +++ b/features/tokendetails/impl/build.gradle.kts @@ -27,6 +27,7 @@ dependencies { implementation(deps.compose.paging) implementation(deps.compose.ui) implementation(deps.compose.ui.tooling) + implementation(deps.compose.ui.utils) implementation(deps.arrow.core) implementation(deps.jodatime) diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/TokenDetailsPreviewData.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/TokenDetailsPreviewData.kt index b4af868ab8..1d46a74430 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/TokenDetailsPreviewData.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/TokenDetailsPreviewData.kt @@ -2,6 +2,9 @@ package com.tangem.feature.tokendetails.presentation.tokendetails import com.tangem.core.ui.components.marketprice.MarketPriceBlockState import com.tangem.core.ui.components.transactions.state.TxHistoryState +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.res.TangemTheme +import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsAppBarMenuConfig import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsBalanceBlockState import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsTopAppBarConfig @@ -13,7 +16,18 @@ import kotlinx.coroutines.flow.MutableStateFlow internal object TokenDetailsPreviewData { - val tokenDetailsTopAppBarConfig = TokenDetailsTopAppBarConfig(onBackClick = {}, onMoreClick = {}) + val tokenDetailsTopAppBarConfig = TokenDetailsTopAppBarConfig( + onBackClick = {}, + tokenDetailsAppBarMenuConfig = TokenDetailsAppBarMenuConfig( + persistentListOf( + TokenDetailsAppBarMenuConfig.MenuItem( + title = TextReference.Res(id = R.string.token_details_hide_token), + textColorProvider = { TangemTheme.colors.text.warning }, + onClick = { }, + ), + ), + ), + ) val tokenInfoBlockStateWithLongNameInMainCurrency = TokenInfoBlockState( name = "Stellar (XLM) with long name test", @@ -67,5 +81,6 @@ internal object TokenDetailsPreviewData { value = TxHistoryState.getDefaultLoadingTransactions {}, ), ), + dialogConfig = null, ) } \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsAppBarMenuConfig.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsAppBarMenuConfig.kt new file mode 100644 index 0000000000..4926ba0b4a --- /dev/null +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsAppBarMenuConfig.kt @@ -0,0 +1,14 @@ +package com.tangem.feature.tokendetails.presentation.tokendetails.state + +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.Color +import com.tangem.core.ui.extensions.TextReference +import kotlinx.collections.immutable.ImmutableList + +internal data class TokenDetailsAppBarMenuConfig(val items: ImmutableList) { + data class MenuItem( + val title: TextReference, + val textColorProvider: @Composable () -> Color, + val onClick: () -> Unit, + ) +} \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsState.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsState.kt index 7006b6c4ef..c6e26d31c5 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsState.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsState.kt @@ -2,6 +2,7 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.state import com.tangem.core.ui.components.marketprice.MarketPriceBlockState import com.tangem.core.ui.components.transactions.state.TxHistoryState +import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.TokenDetailsDialogConfig internal data class TokenDetailsState( val topAppBarConfig: TokenDetailsTopAppBarConfig, @@ -9,4 +10,5 @@ internal data class TokenDetailsState( val tokenBalanceBlockState: TokenDetailsBalanceBlockState, val marketPriceBlockState: MarketPriceBlockState, val txHistoryState: TxHistoryState, + val dialogConfig: TokenDetailsDialogConfig?, ) \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsTopAppBarConfig.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsTopAppBarConfig.kt index 8ed33f724b..88ecf53046 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsTopAppBarConfig.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsTopAppBarConfig.kt @@ -1,6 +1,6 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.state -data class TokenDetailsTopAppBarConfig( +internal data class TokenDetailsTopAppBarConfig( val onBackClick: () -> Unit, - val onMoreClick: () -> Unit, + val tokenDetailsAppBarMenuConfig: TokenDetailsAppBarMenuConfig, ) \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenInfoBlockState.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenInfoBlockState.kt index ac4c80561c..f6bec38a44 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenInfoBlockState.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenInfoBlockState.kt @@ -2,7 +2,7 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.state import androidx.annotation.DrawableRes -data class TokenInfoBlockState( +internal data class TokenInfoBlockState( val name: String, val iconUrl: String, val currency: Currency, diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/components/TokenDetailsDialogConfig.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/components/TokenDetailsDialogConfig.kt new file mode 100644 index 0000000000..02fc3679f3 --- /dev/null +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/components/TokenDetailsDialogConfig.kt @@ -0,0 +1,81 @@ +package com.tangem.feature.tokendetails.presentation.tokendetails.state.components + +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.wrappedList +import com.tangem.features.tokendetails.impl.R + +/** + * Wallet bottom sheet config + * + * @property isShow flag that determine if bottom sheet is shown + * @property onDismissRequest lambda be invoked when bottom sheet is dismissed + * @property content content config + */ +internal data class TokenDetailsDialogConfig( + val isShow: Boolean, + val onDismissRequest: () -> Unit, + val content: DialogContentConfig, +) { + + sealed class DialogContentConfig { + + abstract val title: TextReference + abstract val message: TextReference + abstract val confirmButtonConfig: ButtonConfig + abstract val cancelButtonConfig: ButtonConfig? + + data class ButtonConfig( + val text: TextReference, + val onClick: () -> Unit, + val warning: Boolean = false, + ) + + data class ConfirmHideConfig( + val currencySymbol: String, + val onConfirmClick: () -> Unit, + val onCancelClick: () -> Unit, + ) : DialogContentConfig() { + override val title: TextReference = TextReference.Res( + id = R.string.token_details_hide_alert_title, + formatArgs = wrappedList(currencySymbol), + ) + + override val message: TextReference = TextReference.Res(R.string.token_details_hide_alert_message) + + override val cancelButtonConfig: ButtonConfig = ButtonConfig( + text = TextReference.Res(R.string.common_cancel), + onClick = onCancelClick, + ) + + override val confirmButtonConfig: ButtonConfig = ButtonConfig( + text = TextReference.Res(R.string.token_details_hide_alert_hide), + onClick = onConfirmClick, + warning = true, + ) + } + + data class HasLinkedTokensConfig( + val currencySymbol: String, + val networkName: String, + val onConfirmClick: () -> Unit, + ) : DialogContentConfig() { + override val title: TextReference = TextReference.Res( + id = R.string.token_details_unable_hide_alert_title, + formatArgs = wrappedList(currencySymbol), + ) + + override val message: TextReference = TextReference.Res( + id = R.string.token_details_unable_hide_alert_message, + formatArgs = wrappedList(currencySymbol, networkName), + ) + + override val cancelButtonConfig: ButtonConfig? + get() = null + + override val confirmButtonConfig: ButtonConfig = ButtonConfig( + text = TextReference.Res(R.string.common_ok), + onClick = onConfirmClick, + ) + } + } +} \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSkeletonStateConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSkeletonStateConverter.kt index a02f4cc5fe..0bb96c3d93 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSkeletonStateConverter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSkeletonStateConverter.kt @@ -2,8 +2,11 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.state.factory import com.tangem.core.ui.components.marketprice.MarketPriceBlockState import com.tangem.core.ui.components.transactions.state.TxHistoryState +import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.iconResId +import com.tangem.core.ui.res.TangemTheme import com.tangem.domain.tokens.models.CryptoCurrency +import com.tangem.feature.tokendetails.presentation.tokendetails.state.* import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsBalanceBlockState import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsTopAppBarConfig @@ -11,6 +14,7 @@ import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenInfo import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.TokenDetailsActionButton import com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.TokenDetailsSkeletonStateConverter.SkeletonModel import com.tangem.feature.tokendetails.presentation.tokendetails.viewmodels.TokenDetailsClickIntents +import com.tangem.features.tokendetails.impl.R import com.tangem.utils.converter.Converter import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf @@ -24,7 +28,7 @@ internal class TokenDetailsSkeletonStateConverter( return TokenDetailsState( topAppBarConfig = TokenDetailsTopAppBarConfig( onBackClick = clickIntents::onBackClick, - onMoreClick = clickIntents::onMoreClick, + tokenDetailsAppBarMenuConfig = createMenu(), ), tokenInfoBlockState = TokenInfoBlockState( name = value.cryptoCurrency.name, @@ -47,9 +51,20 @@ internal class TokenDetailsSkeletonStateConverter( value = TxHistoryState.getDefaultLoadingTransactions(clickIntents::onExploreClick), ), ), + dialogConfig = null, ) } + private fun createMenu(): TokenDetailsAppBarMenuConfig = TokenDetailsAppBarMenuConfig( + items = persistentListOf( + TokenDetailsAppBarMenuConfig.MenuItem( + title = TextReference.Res(id = R.string.token_details_hide_token), + textColorProvider = { TangemTheme.colors.text.warning }, + onClick = clickIntents::onHideClick, + ), + ), + ) + private fun createButtons(): ImmutableList { return persistentListOf( TokenDetailsActionButton.Buy(enabled = false, onClick = {}), diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt index e0ad501d38..6db391a63d 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt @@ -12,6 +12,7 @@ import com.tangem.domain.txhistory.models.TxHistoryItem import com.tangem.domain.txhistory.models.TxHistoryListError import com.tangem.domain.txhistory.models.TxHistoryStateError import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState +import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.TokenDetailsDialogConfig import com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.txhistory.TokenDetailsLoadedTxHistoryConverter import com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.txhistory.TokenDetailsLoadingTxHistoryConverter import com.tangem.feature.tokendetails.presentation.tokendetails.viewmodels.TokenDetailsClickIntents @@ -81,4 +82,37 @@ internal class TokenDetailsStateFactory( ): TokenDetailsState { return loadedTxHistoryConverter.convert(txHistoryEither) } + + fun getStateWithClosedDialog(): TokenDetailsState { + val state = currentStateProvider() + return state.copy(dialogConfig = state.dialogConfig?.copy(isShow = false)) + } + + fun getStateWithConfirmHideTokenDialog(currency: CryptoCurrency): TokenDetailsState { + return currentStateProvider().copy( + dialogConfig = TokenDetailsDialogConfig( + isShow = true, + onDismissRequest = clickIntents::onDismissDialog, + content = TokenDetailsDialogConfig.DialogContentConfig.ConfirmHideConfig( + currencySymbol = currency.symbol, + onConfirmClick = clickIntents::onHideConfirmed, + onCancelClick = clickIntents::onDismissDialog, + ), + ), + ) + } + + fun getStateWithLinkedTokensDialog(currency: CryptoCurrency): TokenDetailsState { + return currentStateProvider().copy( + dialogConfig = TokenDetailsDialogConfig( + isShow = true, + onDismissRequest = clickIntents::onDismissDialog, + content = TokenDetailsDialogConfig.DialogContentConfig.HasLinkedTokensConfig( + currencySymbol = currency.symbol, + networkName = currency.network.name, + onConfirmClick = clickIntents::onDismissDialog, + ), + ), + ) + } } \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/TokenDetailsScreen.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/TokenDetailsScreen.kt index a54eef8db7..7ce9bb2274 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/TokenDetailsScreen.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/TokenDetailsScreen.kt @@ -16,6 +16,7 @@ import com.tangem.core.ui.res.TangemTheme import com.tangem.feature.tokendetails.presentation.tokendetails.TokenDetailsPreviewData import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.TokenDetailsBalanceBlock +import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.TokenDetailsDialogs import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.TokenDetailsTopAppBar import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.TokenInfoBlock @@ -56,6 +57,8 @@ internal fun TokenDetailsScreen(state: TokenDetailsState) { ) txHistoryItems(state = state.txHistoryState, txHistoryItems = txHistoryItems) } + + TokenDetailsDialogs(state = state) } } diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/DropdownMenu.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/DropdownMenu.kt new file mode 100644 index 0000000000..fa6ff8aca3 --- /dev/null +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/DropdownMenu.kt @@ -0,0 +1,229 @@ +@file:Suppress("TopLevelPropertyNaming") +package com.tangem.feature.tokendetails.presentation.tokendetails.ui.components + +import androidx.compose.animation.core.* +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.TransformOrigin +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.unit.* +import androidx.compose.ui.window.Popup +import androidx.compose.ui.window.PopupPositionProvider +import androidx.compose.ui.window.PopupProperties + +/** + * Just copy paste [DropdownMenu] from material3 with deleting vertical paddings. + */ +@Composable +internal fun TangemDropdownMenu( + expanded: Boolean, + onDismissRequest: () -> Unit, + modifier: Modifier = Modifier, + offset: DpOffset = DpOffset(0.dp, 0.dp), + properties: PopupProperties = PopupProperties(focusable = true), + content: @Composable ColumnScope.() -> Unit, +) { + val expandedStates = remember { MutableTransitionState(false) } + expandedStates.targetState = expanded + + if (expandedStates.currentState || expandedStates.targetState) { + val transformOriginState = remember { mutableStateOf(TransformOrigin.Center) } + val density = LocalDensity.current + val popupPositionProvider = DropdownMenuPositionProvider( + offset, + density, + ) { parentBounds, menuBounds -> + transformOriginState.value = calculateTransformOrigin(parentBounds, menuBounds) + } + + Popup( + onDismissRequest = onDismissRequest, + popupPositionProvider = popupPositionProvider, + properties = properties, + ) { + DropdownMenuContent( + expandedStates = expandedStates, + transformOriginState = transformOriginState, + modifier = modifier, + content = content, + ) + } + } +} + +private const val InTransitionDuration = 120 +private const val OutTransitionDuration = 75 + +@Suppress("ReusedModifierInstance", "MagicNumber") +@Composable +private fun DropdownMenuContent( + expandedStates: MutableTransitionState, + transformOriginState: MutableState, + modifier: Modifier = Modifier, + content: @Composable ColumnScope.() -> Unit, +) { + // Menu open/close animation. + val transition = updateTransition(expandedStates, "DropDownMenu") + + val scale by transition.animateFloat( + transitionSpec = { + if (false isTransitioningTo true) { + // Dismissed to expanded + tween( + durationMillis = InTransitionDuration, + easing = LinearOutSlowInEasing, + ) + } else { + // Expanded to dismissed. + tween( + durationMillis = 1, + delayMillis = OutTransitionDuration - 1, + ) + } + }, + label = "", + ) { + if (it) { + // Menu is expanded. + 1f + } else { + // Menu is dismissed. + 0.8f + } + } + + val alpha by transition.animateFloat( + transitionSpec = { + if (false isTransitioningTo true) { + // Dismissed to expanded + tween(durationMillis = 30) + } else { + // Expanded to dismissed. + tween(durationMillis = OutTransitionDuration) + } + }, + label = "", + ) { + if (it) { + // Menu is expanded. + 1f + } else { + // Menu is dismissed. + 0f + } + } + Card( + modifier = Modifier.graphicsLayer { + scaleX = scale + scaleY = scale + this.alpha = alpha + transformOrigin = transformOriginState.value + }, + elevation = CardDefaults.cardElevation(), + ) { + Column( + modifier = modifier + .width(IntrinsicSize.Max) + .verticalScroll(rememberScrollState()), + content = content, + ) + } +} + +private fun calculateTransformOrigin(parentBounds: IntRect, menuBounds: IntRect): TransformOrigin { + val pivotX = when { + menuBounds.left >= parentBounds.right -> 0f + menuBounds.right <= parentBounds.left -> 1f + menuBounds.width == 0 -> 0f + else -> { + val intersectionCenter = + ( + kotlin.math.max(parentBounds.left, menuBounds.left) + + kotlin.math.min(parentBounds.right, menuBounds.right) + ) / 2 + (intersectionCenter - menuBounds.left).toFloat() / menuBounds.width + } + } + val pivotY = when { + menuBounds.top >= parentBounds.bottom -> 0f + menuBounds.bottom <= parentBounds.top -> 1f + menuBounds.height == 0 -> 0f + else -> { + val intersectionCenter = + ( + kotlin.math.max(parentBounds.top, menuBounds.top) + + kotlin.math.min(parentBounds.bottom, menuBounds.bottom) + ) / 2 + (intersectionCenter - menuBounds.top).toFloat() / menuBounds.height + } + } + return TransformOrigin(pivotX, pivotY) +} + +private val MenuVerticalMargin = 48.dp + +@Immutable +internal data class DropdownMenuPositionProvider( + val contentOffset: DpOffset, + val density: Density, + val onPositionCalculated: (IntRect, IntRect) -> Unit = { _, _ -> }, +) : PopupPositionProvider { + override fun calculatePosition( + anchorBounds: IntRect, + windowSize: IntSize, + layoutDirection: LayoutDirection, + popupContentSize: IntSize, + ): IntOffset { + // The min margin above and below the menu, relative to the screen. + val verticalMargin = with(density) { MenuVerticalMargin.roundToPx() } + // The content offset specified using the dropdown offset parameter. + val contentOffsetX = with(density) { contentOffset.x.roundToPx() } + val contentOffsetY = with(density) { contentOffset.y.roundToPx() } + + // Compute horizontal position. + val toRight = anchorBounds.left + contentOffsetX + val toLeft = anchorBounds.right - contentOffsetX - popupContentSize.width + val toDisplayRight = windowSize.width - popupContentSize.width + val toDisplayLeft = 0 + val x = if (layoutDirection == LayoutDirection.Ltr) { + sequenceOf( + toRight, + toLeft, + // If the anchor gets outside of the window on the left, we want to position + // toDisplayLeft for proximity to the anchor. Otherwise, toDisplayRight. + if (anchorBounds.left >= 0) toDisplayRight else toDisplayLeft, + ) + } else { + sequenceOf( + toLeft, + toRight, + // If the anchor gets outside of the window on the right, we want to position + // toDisplayRight for proximity to the anchor. Otherwise, toDisplayLeft. + if (anchorBounds.right <= windowSize.width) toDisplayLeft else toDisplayRight, + ) + }.firstOrNull { + it >= 0 && it + popupContentSize.width <= windowSize.width + } ?: toLeft + + // Compute vertical position. + val toBottom = maxOf(anchorBounds.bottom + contentOffsetY, verticalMargin) + val toTop = anchorBounds.top - contentOffsetY - popupContentSize.height + val toCenter = anchorBounds.top - popupContentSize.height / 2 + val toDisplayBottom = windowSize.height - popupContentSize.height - verticalMargin + val y = sequenceOf(toBottom, toTop, toCenter, toDisplayBottom).firstOrNull { + it >= verticalMargin && + it + popupContentSize.height <= windowSize.height - verticalMargin + } ?: toTop + + onPositionCalculated( + anchorBounds, + IntRect(x, y, x + popupContentSize.width, y + popupContentSize.height), + ) + return IntOffset(x, y) + } +} \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenDetailsDialogs.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenDetailsDialogs.kt new file mode 100644 index 0000000000..174eb35567 --- /dev/null +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenDetailsDialogs.kt @@ -0,0 +1,37 @@ +package com.tangem.feature.tokendetails.presentation.tokendetails.ui.components + +import androidx.compose.runtime.Composable +import com.tangem.core.ui.components.BasicDialog +import com.tangem.core.ui.components.DialogButton +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState +import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.TokenDetailsDialogConfig + +@Composable +internal fun TokenDetailsDialogs(state: TokenDetailsState) { + val dialogConfig = state.dialogConfig + if (dialogConfig != null && dialogConfig.isShow) { + TokenDetailsDialog(config = dialogConfig) + } +} + +@Composable +private fun TokenDetailsDialog(config: TokenDetailsDialogConfig) { + BasicDialog( + message = config.content.message.resolveReference(), + confirmButton = DialogButton( + title = config.content.confirmButtonConfig.text.resolveReference(), + warning = config.content.confirmButtonConfig.warning, + onClick = config.content.confirmButtonConfig.onClick, + ), + onDismissDialog = config.onDismissRequest, + title = config.content.title.resolveReference(), + dismissButton = config.content.cancelButtonConfig?.let { cancelButtonConfig -> + DialogButton( + title = cancelButtonConfig.text.resolveReference(), + warning = cancelButtonConfig.warning, + onClick = cancelButtonConfig.onClick, + ) + }, + ) +} \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenDetailsTopAppBar.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenDetailsTopAppBar.kt index a7901787f0..b5e0839679 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenDetailsTopAppBar.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenDetailsTopAppBar.kt @@ -1,17 +1,31 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.ui.components +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.padding import androidx.compose.material3.* import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.DpOffset +import androidx.compose.ui.util.fastForEach +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.res.TangemTheme import com.tangem.feature.tokendetails.presentation.tokendetails.TokenDetailsPreviewData -import com.tangem.features.tokendetails.impl.R +import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsAppBarMenuConfig import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsTopAppBarConfig +import com.tangem.features.tokendetails.impl.R @OptIn(ExperimentalMaterial3Api::class) @Composable internal fun TokenDetailsTopAppBar(config: TokenDetailsTopAppBarConfig) { + var showDropdownMenu by rememberSaveable { mutableStateOf(false) } TopAppBar( navigationIcon = { IconButton(onClick = config.onBackClick) { @@ -24,13 +38,28 @@ internal fun TokenDetailsTopAppBar(config: TokenDetailsTopAppBarConfig) { }, title = {}, actions = { - IconButton(onClick = config.onMoreClick) { + IconButton(onClick = { showDropdownMenu = true }) { Icon( painter = painterResource(id = R.drawable.ic_more_vertical_24), tint = TangemTheme.colors.icon.primary1, contentDescription = "More", ) } + + TangemDropdownMenu( + expanded = showDropdownMenu, + modifier = Modifier.background(TangemTheme.colors.background.primary), + onDismissRequest = { showDropdownMenu = false }, + offset = DpOffset(x = TangemTheme.dimens.spacing20, y = TangemTheme.dimens.spacing10.times(-1)), + content = { + config.tokenDetailsAppBarMenuConfig.items.fastForEach { + AppBarDropdownItem( + item = it, + dismissParent = { showDropdownMenu = false }, + ) + } + }, + ) }, colors = TopAppBarDefaults.topAppBarColors( containerColor = TangemTheme.colors.background.secondary, @@ -41,6 +70,57 @@ internal fun TokenDetailsTopAppBar(config: TokenDetailsTopAppBarConfig) { ) } +@Suppress("ComposableEventParameterNaming") +@Composable +private fun AppBarDropdownItem( + item: TokenDetailsAppBarMenuConfig.MenuItem, + dismissParent: () -> Unit, + modifier: Modifier = Modifier, +) { + Text( + modifier = modifier + .clickable { + dismissParent() + item.onClick() + } + .padding(vertical = TangemTheme.dimens.spacing8, horizontal = TangemTheme.dimens.spacing16), + text = item.title.resolveReference(), + style = TangemTheme.typography.body1.copy(color = item.textColorProvider()), + ) +} + +@Preview +@Composable +private fun Preview_TokenDetailsAppBarDropdownItem_LightTheme() { + TangemTheme(isDark = false) { + AppBarDropdownItem( + modifier = Modifier.background(TangemTheme.colors.background.primary), + dismissParent = {}, + item = TokenDetailsAppBarMenuConfig.MenuItem( + title = TextReference.Res(id = R.string.token_details_hide_token), + textColorProvider = { TangemTheme.colors.text.warning }, + onClick = { }, + ), + ) + } +} + +@Preview +@Composable +private fun Preview_TokenDetailsAppBarDropdownItem_DarkTheme() { + TangemTheme(isDark = true) { + AppBarDropdownItem( + modifier = Modifier.background(TangemTheme.colors.background.primary), + dismissParent = {}, + item = TokenDetailsAppBarMenuConfig.MenuItem( + title = TextReference.Res(id = R.string.token_details_hide_token), + textColorProvider = { TangemTheme.colors.text.warning }, + onClick = { }, + ), + ) + } +} + @Preview @Composable private fun Preview_TokenDetailsTopAppBar_LightTheme() { diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsClickIntents.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsClickIntents.kt index fba6130de7..b0917c5d2a 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsClickIntents.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsClickIntents.kt @@ -6,8 +6,6 @@ interface TokenDetailsClickIntents : TxHistoryClickIntents { fun onBackClick() - fun onMoreClick() - fun onSendClick() fun onReceiveClick() @@ -15,4 +13,10 @@ interface TokenDetailsClickIntents : TxHistoryClickIntents { fun onSellClick() fun onSwapClick() + + fun onDismissDialog() + + fun onHideClick() + + fun onHideConfirmed() } \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt index 76357b097f..f1e17d7e44 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt @@ -11,6 +11,7 @@ import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.redux.ReduxStateHolder import com.tangem.domain.tokens.GetCryptoCurrencyActionsUseCase +import com.tangem.domain.tokens.RemoveCurrencyUseCase import com.tangem.domain.tokens.GetCurrencyStatusUpdatesUseCase import com.tangem.domain.tokens.legacy.TradeCryptoAction import com.tangem.domain.tokens.model.CryptoCurrencyStatus @@ -31,6 +32,7 @@ import com.tangem.utils.coroutines.saveIn import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch +import timber.log.Timber import javax.inject.Inject import kotlin.properties.Delegates @@ -45,6 +47,7 @@ internal class TokenDetailsViewModel @Inject constructor( private val txHistoryItemsUseCase: GetTxHistoryItemsUseCase, private val getExploreUrlUseCase: GetExploreUrlUseCase, private val getCryptoCurrencyActionsUseCase: GetCryptoCurrencyActionsUseCase, + private val removeCurrencyUseCase: RemoveCurrencyUseCase, private val reduxStateHolder: ReduxStateHolder, savedStateHandle: SavedStateHandle, ) : ViewModel(), DefaultLifecycleObserver, TokenDetailsClickIntents { @@ -149,10 +152,6 @@ internal class TokenDetailsViewModel @Inject constructor( router.popBackStack() } - override fun onMoreClick() { - TODO("Not yet implemented") - } - override fun onBuyClick() { val status = cryptoCurrencyStatus ?: return @@ -191,6 +190,29 @@ internal class TokenDetailsViewModel @Inject constructor( reduxStateHolder.dispatch(TradeCryptoAction.New.Swap(cryptoCurrency)) } + override fun onDismissDialog() { + uiState = stateFactory.getStateWithClosedDialog() + } + + override fun onHideClick() { + viewModelScope.launch { + val hasLinkedTokens = removeCurrencyUseCase.hasLinkedTokens(wallet.walletId, cryptoCurrency) + uiState = if (hasLinkedTokens) { + stateFactory.getStateWithLinkedTokensDialog(cryptoCurrency) + } else { + stateFactory.getStateWithConfirmHideTokenDialog(cryptoCurrency) + } + } + } + + override fun onHideConfirmed() { + viewModelScope.launch { + removeCurrencyUseCase.invoke(wallet.walletId, cryptoCurrency) + .onLeft { Timber.e(it) } + .onRight { router.popBackStack() } + } + } + override fun onExploreClick() { viewModelScope.launch { router.openUrl( diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/TokenActionsBottomSheet.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/TokenActionsBottomSheet.kt index a79d41bb23..31ea416730 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/TokenActionsBottomSheet.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/TokenActionsBottomSheet.kt @@ -65,7 +65,7 @@ private fun ActionsBottomSheetContent_Dark( @PreviewParameter(ActionsBottomSheetContentConfigProvider::class) config: ActionsBottomSheetConfig, ) { - TangemTheme(isDark = false) { + TangemTheme(isDark = true) { // Use preview of content because ModalBottomSheet isn't supported in Preview mode ActionsBottomSheetContent(actions = config.actions) } diff --git a/gradle/dependencies.toml b/gradle/dependencies.toml index 1694c5da76..96b967b35a 100644 --- a/gradle/dependencies.toml +++ b/gradle/dependencies.toml @@ -138,6 +138,7 @@ lifecycle-compose = { module = "androidx.lifecycle:lifecycle-runtime-compose", v # region Compose compose-ui = { module = "androidx.compose.ui:ui", version.ref = "compose-runtime" } compose-ui-tooling = { module = "androidx.compose.ui:ui-tooling", version.ref = "compose-runtime" } +compose-ui-utils = { module = "androidx.compose.ui:ui-util", version.ref = "compose-runtime" } compose-animation = { module = "androidx.compose.animation:animation", version.ref = "compose-runtime" } compose-foundation = { module = "androidx.compose.foundation:foundation", version.ref = "compose-foundation" } compose-material = { module = "androidx.compose.material:material", version.ref = "compose-material" } From 6d95abf3b8fd403c3313251ea12ae46a3b6fa9bd Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 29 Aug 2023 21:42:12 +0800 Subject: [PATCH 004/242] Updated on 2026-08-14 --- .../java/com/tangem/tap/TapApplication.kt | 2 - .../tangem/tap/common/CompositionCounter.kt | 40 - .../common/compose/OutlinedTextFieldWidget.kt | 277 ------- .../tangem/tap/common/compose/Undefined.kt | 24 - .../common/compose/extensions/Resources.kt | 21 - .../tap/common/extensions/Navigation.kt | 16 +- .../tap/common/extensions/ValueCallback.kt | 6 - .../CustomTokenFeatureToggles.kt | 3 - .../DefaultCustomTokenFeatureToggles.kt | 3 - .../legacy/AddCustomTokenFragment.kt | 79 -- .../legacy/compose/AddCustomTokenScreen.kt | 208 ----- .../legacy/compose/BlockchainSpinner.kt | 33 - .../legacy/compose/ComposeDialogManager.kt | 157 ---- .../legacy/compose/FormFieldViews.kt | 149 ---- .../legacy/compose/HangingOverKeyboardView.kt | 32 - .../legacy/compose/OutlinedSpinner.kt | 103 --- .../compose/SelectTokenNetworkDialog.kt | 20 - .../compose/test/ContractAddressTests.kt | 120 --- .../legacy/compose/test/TestCasesList.kt | 53 -- .../tokens/impl/di/TokensListRouterModule.kt | 5 +- .../router/DefaultTokensListRouter.kt | 12 +- .../tokens/legacy/redux/TokensAction.kt | 3 - .../tokens/legacy/redux/TokensMiddleware.kt | 62 -- .../main/res/layout/view_compose_fragment.xml | 33 - .../src/main/java/com/tangem/common/Filter.kt | 8 - .../main/java/com/tangem/common/Validator.kt | 8 - .../com/tangem/common/module/ModuleMessage.kt | 5 - .../configs/feature_toggles_config.json | 4 - .../java/com/tangem/domain/DomainDialog.kt | 19 - .../java/com/tangem/domain/DomainLayer.kt | 26 - .../com/tangem/domain/DomainModuleMessage.kt | 19 - .../java/com/tangem/domain/DomainWrapped.kt | 33 - .../tangem/domain/common/TapWorkarounds.kt | 3 - .../domain/common/form/FieldDataConverters.kt | 40 - .../domain/common/form/FieldsValidators.kt | 99 --- .../com/tangem/domain/common/form/Form.kt | 62 -- .../addCustomToken/AddCustomTokenService.kt | 52 -- .../features/addCustomToken/CustomCurrency.kt | 61 +- .../features/addCustomToken/FormFields.kt | 32 - .../redux/AddCustomTokenAction.kt | 63 -- .../addCustomToken/redux/AddCustomTokenHub.kt | 720 ------------------ .../redux/AddCustomTokenState.kt | 311 -------- .../features/addCustomToken/redux/Models.kt | 27 - .../com/tangem/domain/redux/DomainState.kt | 6 +- .../com/tangem/domain/redux/DomainStore.kt | 8 +- .../domain/redux/global/DomainGlobalAction.kt | 2 - .../domain/redux/global/DomainGlobalHub.kt | 3 - .../domain/redux/global/DomainGlobalState.kt | 2 - .../domain/redux/state/StateConverter.kt | 35 - .../tangem/domain/redux/state/StateLogger.kt | 35 - .../redux/state/StringStateConverter.kt | 8 + 51 files changed, 19 insertions(+), 3133 deletions(-) delete mode 100644 app/src/main/java/com/tangem/tap/common/CompositionCounter.kt delete mode 100644 app/src/main/java/com/tangem/tap/common/compose/OutlinedTextFieldWidget.kt delete mode 100644 app/src/main/java/com/tangem/tap/common/compose/Undefined.kt delete mode 100644 app/src/main/java/com/tangem/tap/common/compose/extensions/Resources.kt delete mode 100644 app/src/main/java/com/tangem/tap/common/extensions/ValueCallback.kt delete mode 100644 app/src/main/java/com/tangem/tap/features/customtoken/legacy/AddCustomTokenFragment.kt delete mode 100644 app/src/main/java/com/tangem/tap/features/customtoken/legacy/compose/AddCustomTokenScreen.kt delete mode 100644 app/src/main/java/com/tangem/tap/features/customtoken/legacy/compose/BlockchainSpinner.kt delete mode 100644 app/src/main/java/com/tangem/tap/features/customtoken/legacy/compose/ComposeDialogManager.kt delete mode 100644 app/src/main/java/com/tangem/tap/features/customtoken/legacy/compose/FormFieldViews.kt delete mode 100644 app/src/main/java/com/tangem/tap/features/customtoken/legacy/compose/HangingOverKeyboardView.kt delete mode 100644 app/src/main/java/com/tangem/tap/features/customtoken/legacy/compose/OutlinedSpinner.kt delete mode 100644 app/src/main/java/com/tangem/tap/features/customtoken/legacy/compose/SelectTokenNetworkDialog.kt delete mode 100644 app/src/main/java/com/tangem/tap/features/customtoken/legacy/compose/test/ContractAddressTests.kt delete mode 100644 app/src/main/java/com/tangem/tap/features/customtoken/legacy/compose/test/TestCasesList.kt delete mode 100644 app/src/main/res/layout/view_compose_fragment.xml delete mode 100644 common/src/main/java/com/tangem/common/Filter.kt delete mode 100644 common/src/main/java/com/tangem/common/Validator.kt delete mode 100644 domain/legacy/src/main/java/com/tangem/domain/DomainDialog.kt delete mode 100644 domain/legacy/src/main/java/com/tangem/domain/DomainLayer.kt delete mode 100644 domain/legacy/src/main/java/com/tangem/domain/DomainWrapped.kt delete mode 100644 domain/legacy/src/main/java/com/tangem/domain/common/form/FieldDataConverters.kt delete mode 100644 domain/legacy/src/main/java/com/tangem/domain/common/form/FieldsValidators.kt delete mode 100644 domain/legacy/src/main/java/com/tangem/domain/common/form/Form.kt delete mode 100644 domain/legacy/src/main/java/com/tangem/domain/features/addCustomToken/AddCustomTokenService.kt delete mode 100644 domain/legacy/src/main/java/com/tangem/domain/features/addCustomToken/FormFields.kt delete mode 100644 domain/legacy/src/main/java/com/tangem/domain/features/addCustomToken/redux/AddCustomTokenAction.kt delete mode 100644 domain/legacy/src/main/java/com/tangem/domain/features/addCustomToken/redux/AddCustomTokenHub.kt delete mode 100644 domain/legacy/src/main/java/com/tangem/domain/features/addCustomToken/redux/AddCustomTokenState.kt delete mode 100644 domain/legacy/src/main/java/com/tangem/domain/features/addCustomToken/redux/Models.kt delete mode 100644 domain/legacy/src/main/java/com/tangem/domain/redux/state/StateConverter.kt delete mode 100644 domain/legacy/src/main/java/com/tangem/domain/redux/state/StateLogger.kt create mode 100644 domain/legacy/src/main/java/com/tangem/domain/redux/state/StringStateConverter.kt diff --git a/app/src/main/java/com/tangem/tap/TapApplication.kt b/app/src/main/java/com/tangem/tap/TapApplication.kt index 79fee709bf..fb391f8164 100644 --- a/app/src/main/java/com/tangem/tap/TapApplication.kt +++ b/app/src/main/java/com/tangem/tap/TapApplication.kt @@ -22,7 +22,6 @@ import com.tangem.datasource.config.ConfigManager import com.tangem.datasource.config.FeaturesLocalLoader import com.tangem.datasource.config.models.Config import com.tangem.datasource.connection.NetworkConnectionManager -import com.tangem.domain.DomainLayer import com.tangem.domain.appcurrency.repository.AppCurrencyRepository import com.tangem.domain.card.ScanCardProcessor import com.tangem.domain.common.LogConfig @@ -210,7 +209,6 @@ class TapApplication : Application(), ImageLoaderFactory { activityResultCaller = foregroundActivityObserver registerActivityLifecycleCallbacks(foregroundActivityObserver.callbacks) - DomainLayer.init() preferencesStorage = preferencesDataSource walletConnectRepository = WalletConnectRepository(this) diff --git a/app/src/main/java/com/tangem/tap/common/CompositionCounter.kt b/app/src/main/java/com/tangem/tap/common/CompositionCounter.kt deleted file mode 100644 index b09112d39f..0000000000 --- a/app/src/main/java/com/tangem/tap/common/CompositionCounter.kt +++ /dev/null @@ -1,40 +0,0 @@ -package com.tangem.tap.common - -import timber.log.Timber - -class CompositionCounter( - val id: String, - count: Int = 0, -) { - var count: Int = count - private set - - fun increase(id: String): CompositionCounter { - if (this.id != id) return this - - count += 1 - return CompositionCounter(id, count) - } -} - -class CompositionLogger( - private val recomposeViewId: String, - private val tag: String = recomposeViewId, - private var turnOnForIds: List = listOf(recomposeViewId), -) { - val count: Int - get() = compositionCounter.count - - private var compositionCounter: CompositionCounter = CompositionCounter(recomposeViewId) - - fun nextComposition() { - compositionCounter = compositionCounter.increase(recomposeViewId) - log("") - } - - fun log(message: String) { - if (!turnOnForIds.contains(recomposeViewId)) return - - Timber.d("$tag[$recomposeViewId]:[${compositionCounter.count}]: $message") - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/compose/OutlinedTextFieldWidget.kt b/app/src/main/java/com/tangem/tap/common/compose/OutlinedTextFieldWidget.kt deleted file mode 100644 index 59cf01d88c..0000000000 --- a/app/src/main/java/com/tangem/tap/common/compose/OutlinedTextFieldWidget.kt +++ /dev/null @@ -1,277 +0,0 @@ -package com.tangem.tap.common.compose - -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.animateContentSize -import androidx.compose.animation.fadeIn -import androidx.compose.animation.fadeOut -import androidx.compose.animation.slideInVertically -import androidx.compose.animation.slideOutVertically -import androidx.compose.foundation.interaction.MutableInteractionSource -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.material.LinearProgressIndicator -import androidx.compose.material.OutlinedTextField -import androidx.compose.material.Text -import androidx.compose.material.TextFieldColors -import androidx.compose.runtime.Composable -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.text.TextStyle -import androidx.compose.ui.text.input.VisualTransformation -import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import com.tangem.common.module.ModuleError -import com.tangem.core.ui.res.TangemTheme -import com.tangem.domain.common.form.Field -import com.tangem.tap.common.CompositionLogger -import com.tangem.tap.common.compose.extensions.stringResourceDefault -import com.tangem.tap.domain.moduleMessage.ModuleMessageConverter - -/** -[REDACTED_AUTHOR] - */ -@Composable -fun OutlinedTextFieldWidget( - fieldData: Field.Data, - labelId: Int? = null, - label: String = "", - placeholderId: Int? = null, - placeholder: String = "", - trailingIcon: @Composable (() -> Unit)? = null, - isEnabled: Boolean = true, - isVisible: Boolean = true, - isLoading: Boolean = false, - error: ModuleError? = null, - errorConverter: ModuleMessageConverter? = null, - debounceTextChanges: Long = 400, - visualTransformation: VisualTransformation = VisualTransformation.None, - keyboardOptions: KeyboardOptions = KeyboardOptions.Default, - onTextChange: (String) -> Unit, -) { - if (!isVisible) return - - Column(modifier = Modifier.animateContentSize()) { - OutlinedProgressTextField( - fieldData = fieldData, - label = stringResourceDefault(labelId, label), - placeholder = stringResourceDefault(placeholderId, placeholder), - trailingIcon = trailingIcon, - isEnabled = isEnabled, - isLoading = isLoading, - error = error, - debounce = debounceTextChanges, - visualTransformation = visualTransformation, - keyboardOptions = keyboardOptions, - onTextChange = onTextChange, - ) - errorConverter?.let { AnimatedErrorView(errorConverter = it, error = error) } - } -} - -@Suppress("LongMethod", "NestedBlockDepth", "MagicNumber", "MaxLineLength") -@Composable -private fun OutlinedProgressTextField( - fieldData: Field.Data, - label: String = "", - placeholder: String = "", - isEnabled: Boolean = true, - isLoading: Boolean = false, - error: ModuleError? = null, - debounce: Long = 400, - visualTransformation: VisualTransformation = VisualTransformation.None, - keyboardOptions: KeyboardOptions = KeyboardOptions.Default, - colors: TextFieldColors = TangemTextFieldsDefault.defaultTextFieldColors, - interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, - trailingIcon: @Composable (() -> Unit)? = null, - onTextChange: (String) -> Unit, -) { - val logger = remember { - CompositionLogger(label, "OutlinedProgressTextField", listOf("Символ токена")) - } - logger.nextComposition() - - val textValueState = remember { mutableStateOf(fieldData.value) } - val textDebouncer = valueDebouncerAsState( - initialValue = fieldData.value, - debounce = debounce, - onEmitValueReceive = { - logger.log("DEBOUNCER: onEmitValueReceived: [$it]") - logger.log("DEBOUNCER: start RECOMPOSE by new value for textValueState.value = [$it]") - textValueState.value = it - }, - onValueChange = { - logger.log("DEBOUNCER: onValueChanged: >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> dispatch.toStore([$it])") - onTextChange(it) - }, - ) - - logger.log("RECOMPOSE ---------------------------------------------------------------START [${logger.count}]") - logger.log("RECOMPOSE --data: fieldData.value: [$fieldData]") - logger.log("RECOMPOSE --data: textValueState.value: [${textValueState.value}]") - logger.log("RECOMPOSE --data: textDebouncer.emittedValue = [${textDebouncer.emittedValue}]") - logger.log("RECOMPOSE --data: textDebouncer.debounced = [${textDebouncer.debounced}]") - - if (!fieldData.isUserInput) { - // initial value is not from an user - val isNotUserInput = "-- IS NOT USER INPUT" - logger.log("recompose $isNotUserInput") - if (textValueState.value == fieldData.value) { - logger.log("$isNotUserInput: внешние данные ОДИНАКОВЫ с данными в поле") - } else { - logger.log("$isNotUserInput: внешние данные РАЗЛИЧАЮТСЯ с данными в поле") - if (textDebouncer.emittedValue != textDebouncer.debounced || textDebouncer.emitsCountBeforeDebounce > 0) { - logger.log("$isNotUserInput: пользователь ВВОДИТ данные -> внешние данные игнорируем, ждем RECOMPOSE") - } else { - logger.log("$isNotUserInput: пользователь НЕ вводит данные -> пытаемся обработать внешние данные") - if (textValueState.value != textDebouncer.emittedValue || - textValueState.value != textDebouncer.debounced - ) { - logger.log("$isNotUserInput: даннные в поле не соответствуют данным из textDebouncer") - if (textDebouncer.emittedValue.isEmpty() && textDebouncer.debounced.isEmpty()) { - logger.log( - "$isNotUserInput: даннные в textDebouncer ПУСТЫ -> start RECOMPOSE новые данные для " + - "textValueState.value = [${fieldData.value}]", - ) - textValueState.value = fieldData.value - } else { - logger.log( - "$isNotUserInput: даннные в textDebouncer НЕ ПУСТЫ -> start RECOMPOSE новые данные для " + - "textValueState.value = [${fieldData.value}]", - ) - textValueState.value = fieldData.value - } - } else { - logger.log( - "$isNotUserInput: в пустое поле вставляются данные -> start RECOMPOSE новые данные для " + - "textValueState.value = [${fieldData.value}]", - ) - textValueState.value = fieldData.value - } - } - } - } - logger.log("recompose --------------------------------------------------------------FINISH [${logger.count}]") - - Box { - OutlinedTextField( - modifier = Modifier - .fillMaxWidth(), - value = textValueState.value, - onValueChange = { - logger.log("WIDGET: textDebouncer.emmit([$it])") - textDebouncer.emmit(it) - }, - keyboardOptions = keyboardOptions, - label = { - Text( - text = label, - style = TangemTheme.typography.caption, - color = colors.labelColor( - enabled = isEnabled, - error = error != null, - interactionSource = interactionSource, - ).value, - ) - }, - placeholder = { - Text( - text = placeholder, - style = TangemTheme.typography.body1, - color = colors.placeholderColor(enabled = isEnabled).value, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - ) - }, - trailingIcon = trailingIcon, - singleLine = true, - enabled = isEnabled, - isError = error != null, - visualTransformation = visualTransformation, - colors = colors, - interactionSource = interactionSource, - ) - AnimatedVisibility( - modifier = Modifier - .fillMaxWidth() - .align(Alignment.BottomCenter) - .padding(start = 6.dp, top = 0.dp, end = 6.dp, bottom = 6.dp), - visible = isLoading, - ) { - LinearProgressIndicator( - color = TangemTheme.colors.icon.primary1, - ) - } - } -} - -@Composable -private fun AnimatedErrorView(errorConverter: ModuleMessageConverter, error: ModuleError? = null) { - AnimatedVisibility( - visible = error != null, - enter = fadeIn() + slideInVertically(), - exit = slideOutVertically() + fadeOut(), - ) { - error?.let { - ErrorView( - text = errorConverter.convert(it).message, - style = TextStyle(fontSize = 14.sp), - ) - } - } -} - -@Preview -@Composable -private fun OutlinedTextFieldWithErrorTest() { - val context = LocalContext.current - val converter = remember { ModuleMessageConverter(context) } - - class SimpleError( - override val code: Int = 1, - override val message: String = "Error message", - override val data: Any? = null, - ) : ModuleError() - - val modifier = Modifier - .fillMaxWidth() - .padding(16.dp) - Column { - OutlinedTextFieldWidget( - fieldData = Field.Data("", false), - label = "First label", - placeholder = "1 placeholder", - error = null, - errorConverter = converter, - ) {} - OutlinedTextFieldWidget( - fieldData = Field.Data("First", false), - label = "First label", - placeholder = "1 placeholder", - error = null, - errorConverter = converter, - ) {} - OutlinedTextFieldWidget( - fieldData = Field.Data("First", false), - label = "First label", - placeholder = "1 placeholder", - isLoading = true, - error = null, - errorConverter = converter, - ) {} - OutlinedTextFieldWidget( - fieldData = Field.Data("First", false), - label = "First label", - placeholder = "1 placeholder", - error = SimpleError(), - errorConverter = converter, - ) {} - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/compose/Undefined.kt b/app/src/main/java/com/tangem/tap/common/compose/Undefined.kt deleted file mode 100644 index d5e5168be3..0000000000 --- a/app/src/main/java/com/tangem/tap/common/compose/Undefined.kt +++ /dev/null @@ -1,24 +0,0 @@ -package com.tangem.tap.common.compose - -import androidx.compose.foundation.layout.Column -import androidx.compose.material.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.unit.sp - -/** -[REDACTED_AUTHOR] - * Compose views are not typically used as a main or base view. - */ - -@Composable -fun TitleSubtitle(title: String, subtitle: String) { - Column { - Text(text = title) - Text( - text = subtitle, - fontSize = 12.sp, - color = Color.Gray, - ) - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/compose/extensions/Resources.kt b/app/src/main/java/com/tangem/tap/common/compose/extensions/Resources.kt deleted file mode 100644 index 5b8da92e24..0000000000 --- a/app/src/main/java/com/tangem/tap/common/compose/extensions/Resources.kt +++ /dev/null @@ -1,21 +0,0 @@ -package com.tangem.tap.common.compose.extensions - -import android.content.res.Resources -import androidx.annotation.StringRes -import androidx.compose.runtime.Composable -import androidx.compose.ui.platform.LocalContext - -/** -[REDACTED_AUTHOR] - */ -@Composable -fun stringResourceDefault(@StringRes id: Int?, default: String = ""): String { - val resources = LocalContext.current.resources - return try { - resources.getString(requireNotNull(id)) - } catch (ex: Resources.NotFoundException) { - default - } catch (ex: IllegalArgumentException) { - default - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/extensions/Navigation.kt b/app/src/main/java/com/tangem/tap/common/extensions/Navigation.kt index 9119734bfd..f90e296e22 100644 --- a/app/src/main/java/com/tangem/tap/common/extensions/Navigation.kt +++ b/app/src/main/java/com/tangem/tap/common/extensions/Navigation.kt @@ -6,7 +6,7 @@ import com.tangem.core.navigation.AppScreen import com.tangem.core.navigation.FragmentShareTransition import com.tangem.feature.referral.ReferralFragment import com.tangem.feature.swap.presentation.SwapFragment -import com.tangem.tap.features.customtoken.legacy.AddCustomTokenFragment +import com.tangem.tap.features.customtoken.impl.presentation.AddCustomTokenFragment import com.tangem.tap.features.details.ui.appsettings.AppSettingsFragment import com.tangem.tap.features.details.ui.cardsettings.CardSettingsFragment import com.tangem.tap.features.details.ui.cardsettings.coderecovery.AccessCodeRecoveryFragment @@ -33,7 +33,6 @@ import com.tangem.tap.proxy.redux.DaggerGraphState import com.tangem.tap.store import com.tangem.wallet.R import timber.log.Timber -import com.tangem.tap.features.customtoken.impl.presentation.AddCustomTokenFragment as RedesignedAddCustomTokenFragment fun FragmentActivity.openFragment( screen: AppScreen, @@ -155,18 +154,7 @@ private fun fragmentFactory(screen: AppScreen): Fragment { AppScreen.AccessCodeRecovery -> AccessCodeRecoveryFragment() AppScreen.Disclaimer -> DisclaimerFragment() AppScreen.AddTokens -> TokensListFragment() - - AppScreen.AddCustomToken -> { - val featureToggles = store.state.daggerGraphState.get( - getDependency = DaggerGraphState::customTokenFeatureToggles, - ) - if (featureToggles.isRedesignedScreenEnabled) { - RedesignedAddCustomTokenFragment() - } else { - AddCustomTokenFragment() - } - } - + AppScreen.AddCustomToken -> AddCustomTokenFragment() AppScreen.WalletDetails -> { val featureToggles = store.state.daggerGraphState.get( getDependency = DaggerGraphState::tokenDetailsFeatureToggles, diff --git a/app/src/main/java/com/tangem/tap/common/extensions/ValueCallback.kt b/app/src/main/java/com/tangem/tap/common/extensions/ValueCallback.kt deleted file mode 100644 index 3dd4357bba..0000000000 --- a/app/src/main/java/com/tangem/tap/common/extensions/ValueCallback.kt +++ /dev/null @@ -1,6 +0,0 @@ -package com.tangem.tap.common.extensions - -/** -[REDACTED_AUTHOR] - */ -typealias ValueCallback = (T) -> Unit \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/customtoken/api/featuretoggles/CustomTokenFeatureToggles.kt b/app/src/main/java/com/tangem/tap/features/customtoken/api/featuretoggles/CustomTokenFeatureToggles.kt index b64f9efd14..a6894e04b1 100644 --- a/app/src/main/java/com/tangem/tap/features/customtoken/api/featuretoggles/CustomTokenFeatureToggles.kt +++ b/app/src/main/java/com/tangem/tap/features/customtoken/api/featuretoggles/CustomTokenFeatureToggles.kt @@ -7,8 +7,5 @@ package com.tangem.tap.features.customtoken.api.featuretoggles */ interface CustomTokenFeatureToggles { - /** Availability of redesigned screen (internal feature) */ - val isRedesignedScreenEnabled: Boolean - val isNewCardScanningEnabled: Boolean } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/customtoken/impl/featuretoggles/DefaultCustomTokenFeatureToggles.kt b/app/src/main/java/com/tangem/tap/features/customtoken/impl/featuretoggles/DefaultCustomTokenFeatureToggles.kt index a19dde5403..cd06665b00 100644 --- a/app/src/main/java/com/tangem/tap/features/customtoken/impl/featuretoggles/DefaultCustomTokenFeatureToggles.kt +++ b/app/src/main/java/com/tangem/tap/features/customtoken/impl/featuretoggles/DefaultCustomTokenFeatureToggles.kt @@ -14,9 +14,6 @@ internal class DefaultCustomTokenFeatureToggles( private val featureTogglesManager: FeatureTogglesManager, ) : CustomTokenFeatureToggles { - override val isRedesignedScreenEnabled: Boolean - get() = featureTogglesManager.isFeatureEnabled(name = "REDESIGNED_CUSTOM_TOKEN_SCREEN_ENABLED") - override val isNewCardScanningEnabled: Boolean get() = featureTogglesManager.isFeatureEnabled(name = "NEW_CARD_SCANNING_ENABLED") } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/customtoken/legacy/AddCustomTokenFragment.kt b/app/src/main/java/com/tangem/tap/features/customtoken/legacy/AddCustomTokenFragment.kt deleted file mode 100644 index 204f95ec25..0000000000 --- a/app/src/main/java/com/tangem/tap/features/customtoken/legacy/AddCustomTokenFragment.kt +++ /dev/null @@ -1,79 +0,0 @@ -package com.tangem.tap.features.customtoken.legacy - -import android.os.Bundle -import android.view.View -import android.view.WindowManager -import androidx.appcompat.widget.Toolbar -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.runtime.MutableState -import androidx.compose.runtime.mutableStateOf -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.ComposeView -import com.tangem.core.analytics.Analytics -import com.tangem.core.ui.res.TangemTheme -import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenState -import com.tangem.domain.redux.domainStore -import com.tangem.tap.common.analytics.events.ManageTokens -import com.tangem.tap.features.BaseStoreFragment -import com.tangem.tap.features.FragmentOnBackPressedHandler -import com.tangem.tap.features.addBackPressHandler -import com.tangem.tap.features.customtoken.legacy.compose.AddCustomTokenScreen -import com.tangem.tap.features.customtoken.legacy.compose.ClosePopupTrigger -import com.tangem.wallet.R -import org.rekotlin.StoreSubscriber - -/** -[REDACTED_AUTHOR] - */ -class AddCustomTokenFragment : BaseStoreFragment(R.layout.view_compose_fragment), StoreSubscriber { - - private var state: MutableState = mutableStateOf(domainStore.state.addCustomTokensState) - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - Analytics.send(ManageTokens.CustomToken.ScreenOpened) - } - - override fun subscribeToStore() { - domainStore.subscribe(this) { state -> - state.skipRepeats { oldState, newState -> - oldState.addCustomTokensState == newState.addCustomTokensState - }.select { it.addCustomTokensState } - } - } - - override fun newState(state: AddCustomTokenState) { - if (activity == null || view == null) return - - this.state.value = state - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - - requireActivity().window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE) - view.findViewById(R.id.toolbar)?.setTitle(R.string.add_custom_token_title) - - val closePopupTrigger = initClosingPopupTriggerEvent() - view.findViewById(R.id.view_compose)?.setContent { - TangemTheme { - Box( - modifier = Modifier - .fillMaxSize(), - ) { - AddCustomTokenScreen(state, closePopupTrigger) - } - } - } - } - - private fun initClosingPopupTriggerEvent(): ClosePopupTrigger = ClosePopupTrigger().apply { - onCloseComplete = ::handleOnBackPressed - addBackPressHandler( - object : FragmentOnBackPressedHandler { - override fun handleOnBackPressed() = close() - }, - ) - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/customtoken/legacy/compose/AddCustomTokenScreen.kt b/app/src/main/java/com/tangem/tap/features/customtoken/legacy/compose/AddCustomTokenScreen.kt deleted file mode 100644 index baa487cec7..0000000000 --- a/app/src/main/java/com/tangem/tap/features/customtoken/legacy/compose/AddCustomTokenScreen.kt +++ /dev/null @@ -1,208 +0,0 @@ -package com.tangem.tap.features.customtoken.legacy.compose - -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.material.* -import androidx.compose.runtime.* -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.colorResource -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.dp -import com.tangem.core.ui.components.PrimaryButtonIconStart -import com.tangem.core.ui.components.keyboardAsState -import com.tangem.core.ui.res.TangemTheme -import com.tangem.domain.AddCustomTokenError -import com.tangem.domain.common.form.DataField -import com.tangem.domain.common.form.FieldId -import com.tangem.domain.features.addCustomToken.CustomTokenFieldId.* -import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenAction -import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenState -import com.tangem.domain.features.addCustomToken.redux.ScreenState -import com.tangem.domain.features.addCustomToken.redux.ViewStates -import com.tangem.domain.redux.domainStore -import com.tangem.tap.common.compose.AddCustomTokenWarning -import com.tangem.tap.domain.moduleMessage.ModuleMessageConverter -import com.tangem.tap.features.customtoken.legacy.compose.test.TestCase -import com.tangem.tap.features.customtoken.legacy.compose.test.TestCasesList -import com.tangem.wallet.R -import kotlinx.coroutines.launch - -@OptIn(ExperimentalMaterialApi::class) -@Composable -fun AddCustomTokenScreen(state: MutableState, closePopupTrigger: ClosePopupTrigger) { - val selectedTestCase = remember { mutableStateOf(TestCase.ContractAddress) } - - val bottomSheetScaffoldState = rememberBottomSheetScaffoldState( - bottomSheetState = BottomSheetState(BottomSheetValue.Collapsed), - ) - val coroutineScope = rememberCoroutineScope() - val toggleBottomSheet = { coroutineScope.launch { bottomSheetScaffoldState.toggle() } } - - BottomSheetScaffold( - scaffoldState = bottomSheetScaffoldState, - sheetContent = { - Surface(color = colorResource(id = R.color.lightGray5)) { - selectedTestCase.value.content(toggleBottomSheet) - } - }, - sheetPeekHeight = 0.dp, - ) { - Column { - TestCasesList( - onItemClick = { - selectedTestCase.value = it - toggleBottomSheet() - }, - ) - ScreenContent(state, closePopupTrigger) - } - } - - ComposeDialogManager() - LaunchedEffect(key1 = Unit, block = { domainStore.dispatch(AddCustomTokenAction.OnCreate) }) - DisposableEffect(key1 = Unit, effect = { onDispose { domainStore.dispatch(AddCustomTokenAction.OnDestroy) } }) -} - -@OptIn(ExperimentalMaterialApi::class) -private suspend fun BottomSheetScaffoldState.toggle() { - if (bottomSheetState.isCollapsed) { - bottomSheetState.expand() - } else { - bottomSheetState.collapse() - } -} - -@Composable -private fun ScreenContent(state: MutableState, closePopupTrigger: ClosePopupTrigger) { - val scaffoldState = rememberScaffoldState() - - Scaffold( - scaffoldState = scaffoldState, - backgroundColor = colorResource(id = R.color.backgroundLightGray), - floatingActionButton = { - HangingOverKeyboardView(keyboardState = keyboardAsState()) { - AddButton(state) - } - }, - floatingActionButtonPosition = FabPosition.Center, - ) { paddings -> - Box( - modifier = Modifier - .padding(paddings) - .fillMaxSize(), - ) { - LazyColumn( - contentPadding = PaddingValues(bottom = 90.dp), - ) { - item { - Surface( - modifier = Modifier.padding(16.dp), - shape = MaterialTheme.shapes.small, - elevation = 4.dp, - ) { - Column( - modifier = Modifier - .fillMaxWidth() - .padding(16.dp), - ) { - FormFields(state, closePopupTrigger) - } - } - } - item { Warnings(state.value.warnings.toList()) } - } - } - } -} - -@Composable -private fun FormFields(state: MutableState, closePopupTrigger: ClosePopupTrigger) { - val context = LocalContext.current - val errorConverter = remember { ModuleMessageConverter(context) } - - val stateValue = state.value - stateValue.form.fieldList.forEach { field -> - val data = ScreenFieldData.fromState(field, stateValue, errorConverter) - when (field.id) { - ContractAddress -> TokenContractAddressView(data) - Network -> TokenNetworkView(data, stateValue, closePopupTrigger) - Name -> TokenNameView(data) - Symbol -> TokenSymbolView(data) - Decimals -> TokenDecimalsView(data) - DerivationPath -> TokenDerivationPathView(data, stateValue, closePopupTrigger) - } - } -} - -@Composable -fun Warnings(warnings: List) { - if (warnings.isEmpty()) return - - val context = LocalContext.current - val warningConverter = remember { ModuleMessageConverter(context) } - - Column { - warnings.forEachIndexed { index, item -> - val modifier = when (index) { - 0 -> Modifier.padding(vertical = 0.dp) - warnings.lastIndex -> Modifier.padding(top = 8.dp, bottom = 16.dp) - else -> Modifier.padding(top = 8.dp, bottom = 0.dp) - } - AddCustomTokenWarning( - modifier = modifier - .padding(horizontal = TangemTheme.dimens.spacing16) - .fillMaxWidth(), - warning = item, - converter = warningConverter, - ) - } - } -} - -@Composable -private fun AddButton(state: MutableState) { - PrimaryButtonIconStart( - modifier = Modifier - .padding(horizontal = TangemTheme.dimens.spacing16) - .fillMaxWidth(), - text = stringResource(id = R.string.common_add), - iconResId = R.drawable.ic_plus_24, - enabled = state.value.screenState.addButton.isEnabled, - onClick = { domainStore.dispatch(AddCustomTokenAction.OnAddCustomTokenClicked) }, - ) -} - -data class ScreenFieldData( - val field: DataField<*>, - val error: AddCustomTokenError?, - val errorConverter: ModuleMessageConverter, - val viewState: ViewStates.TokenField, -) { - companion object { - fun fromState( - field: DataField<*>, - state: AddCustomTokenState, - errorConverter: ModuleMessageConverter, - ): ScreenFieldData { - return ScreenFieldData( - field = field, - error = state.getError(field.id), - errorConverter = errorConverter, - viewState = selectField(field.id, state.screenState), - ) - } - - private fun selectField(id: FieldId, screenState: ScreenState): ViewStates.TokenField { - return when (id) { - ContractAddress -> screenState.contractAddressField - Network -> screenState.network - Name -> screenState.name - Symbol -> screenState.symbol - Decimals -> screenState.decimals - DerivationPath -> screenState.derivationPath - else -> throw UnsupportedOperationException() - } - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/customtoken/legacy/compose/BlockchainSpinner.kt b/app/src/main/java/com/tangem/tap/features/customtoken/legacy/compose/BlockchainSpinner.kt deleted file mode 100644 index d6c60c2b26..0000000000 --- a/app/src/main/java/com/tangem/tap/features/customtoken/legacy/compose/BlockchainSpinner.kt +++ /dev/null @@ -1,33 +0,0 @@ -package com.tangem.tap.features.customtoken.legacy.compose - -import androidx.annotation.StringRes -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource -import com.tangem.blockchain.common.Blockchain -import com.tangem.domain.common.form.Field - -@Composable -fun BlockchainSpinner( - @StringRes title: Int, - itemList: List, - selectedItem: Field.Data, - isEnabled: Boolean = true, - textFieldConverter: (Blockchain) -> String, - dropdownItemView: @Composable ((Blockchain) -> Unit)? = null, - closePopupTrigger: ClosePopupTrigger = ClosePopupTrigger(), - onItemSelect: (Blockchain) -> Unit, -) { - OutlinedSpinner( - modifier = Modifier.fillMaxWidth(), - label = stringResource(id = title), - itemList = itemList, - selectedItem = selectedItem, - textFieldConverter = textFieldConverter, - dropdownItemView = dropdownItemView, - isEnabled = isEnabled, - onItemSelected = onItemSelect, - closePopupTrigger = closePopupTrigger, - ) -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/customtoken/legacy/compose/ComposeDialogManager.kt b/app/src/main/java/com/tangem/tap/features/customtoken/legacy/compose/ComposeDialogManager.kt deleted file mode 100644 index 453ace8c9e..0000000000 --- a/app/src/main/java/com/tangem/tap/features/customtoken/legacy/compose/ComposeDialogManager.kt +++ /dev/null @@ -1,157 +0,0 @@ -package com.tangem.tap.features.customtoken.legacy.compose - -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items -import androidx.compose.material.AlertDialog -import androidx.compose.material.Button -import androidx.compose.material.LocalTextStyle -import androidx.compose.material.MaterialTheme -import androidx.compose.material.Surface -import androidx.compose.material.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.DisposableEffect -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.MutableState -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.TextStyle -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import androidx.compose.ui.window.Dialog -import androidx.compose.ui.window.DialogProperties -import com.tangem.core.ui.components.SpacerH16 -import com.tangem.core.ui.res.TangemTheme -import com.tangem.datasource.api.tangemTech.models.CoinsResponse -import com.tangem.domain.DomainDialog -import com.tangem.domain.redux.domainStore -import com.tangem.domain.redux.global.DomainGlobalAction -import com.tangem.domain.redux.global.DomainGlobalState -import com.tangem.tap.domain.moduleMessage.ModuleMessageConverter -import com.tangem.wallet.R -import org.rekotlin.StoreSubscriber - -@Composable -internal fun ComposeDialogManager() { - val dialogSate = remember { mutableStateOf(null) } - val subscriber = remember { - object : StoreSubscriber { - override fun newState(state: DomainGlobalState) { - dialogSate.value = state.dialog - } - } - } - - ShowTheDialog(dialogSate) - - LaunchedEffect( - key1 = Unit, - block = { - domainStore.subscribe(subscriber) { state -> - state.skipRepeats { oldState, newState -> - oldState.globalState == newState.globalState - }.select { it.globalState } - } - }, - ) - DisposableEffect( - key1 = Unit, - effect = { - onDispose { domainStore.unsubscribe(subscriber) } - }, - ) -} - -@Composable -private fun ShowTheDialog(dialogState: MutableState) { - if (dialogState.value == null) return - - val context = LocalContext.current - val errorConverter = remember { ModuleMessageConverter(context) } - val onDismissRequest = { domainStore.dispatch(DomainGlobalAction.ShowDialog(null)) } - - when (val dialog = dialogState.value) { - is DomainDialog.DialogError -> ErrorDialog( - title = stringResource(id = R.string.common_error), - body = errorConverter.convert(dialog.error).message, - onDismissRequest, - ) - is DomainDialog.SelectTokenDialog -> SelectTokenNetworkDialog(dialog, onDismissRequest) - else -> {} - } -} - -/** - * Dialog with single item selection - */ -@Composable -fun SimpleDialog( - title: String, - items: List, - onSelect: (CoinsResponse.Coin.Network) -> Unit, - onDismissRequest: () -> Unit, - itemContent: @Composable (CoinsResponse.Coin.Network) -> Unit, -) { - Dialog( - properties = DialogProperties(dismissOnBackPress = false, dismissOnClickOutside = false), - onDismissRequest = { }, - ) { - Surface(modifier = Modifier.fillMaxWidth(), shape = MaterialTheme.shapes.medium) { - Column(modifier = Modifier.padding(TangemTheme.dimens.spacing22)) { - DialogTitle(title = title) - LazyColumn { - items(items = items, key = CoinsResponse.Coin.Network::networkId) { item -> - Row( - modifier = Modifier - .fillMaxWidth() - .height(56.dp) - .clickable { - onSelect(item) - onDismissRequest() - }, - verticalAlignment = Alignment.CenterVertically, - ) { itemContent(item) } - } - } - } - } - } -} - -@Composable -private fun DialogTitle(title: String) { - Text( - text = title, - style = LocalTextStyle.provides( - TextStyle( - fontWeight = FontWeight.Bold, - fontSize = 20.sp, - ), - ).value, - ) - SpacerH16() -} - -@Composable -fun ErrorDialog(title: String, body: String, onDismissRequest: () -> Unit) { - AlertDialog( - title = { DialogTitle(title) }, - text = { Text(body) }, - onDismissRequest = onDismissRequest, - confirmButton = { - Button(onClick = onDismissRequest) { - Text(text = stringResource(id = R.string.common_ok)) - } - }, - ) -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/customtoken/legacy/compose/FormFieldViews.kt b/app/src/main/java/com/tangem/tap/features/customtoken/legacy/compose/FormFieldViews.kt deleted file mode 100644 index a640e91575..0000000000 --- a/app/src/main/java/com/tangem/tap/features/customtoken/legacy/compose/FormFieldViews.kt +++ /dev/null @@ -1,149 +0,0 @@ -package com.tangem.tap.features.customtoken.legacy.compose - -import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.runtime.Composable -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.input.KeyboardType -import com.tangem.core.ui.components.SpacerH8 -import com.tangem.domain.common.form.Field -import com.tangem.domain.features.addCustomToken.TokenBlockchainField -import com.tangem.domain.features.addCustomToken.TokenDerivationPathField -import com.tangem.domain.features.addCustomToken.TokenField -import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenAction.OnTokenContractAddressChanged -import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenAction.OnTokenDecimalsChanged -import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenAction.OnTokenDerivationPathChanged -import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenAction.OnTokenNameChanged -import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenAction.OnTokenNetworkChanged -import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenAction.OnTokenSymbolChanged -import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenState -import com.tangem.domain.redux.domainStore -import com.tangem.tap.common.compose.OutlinedTextFieldWidget -import com.tangem.tap.common.compose.TitleSubtitle -import com.tangem.wallet.R - -/** -[REDACTED_AUTHOR] - */ -@Composable -fun TokenContractAddressView(screenFieldData: ScreenFieldData) { - if (!screenFieldData.viewState.isVisible) return - - val tokenField = screenFieldData.field as TokenField - - OutlinedTextFieldWidget( - fieldData = tokenField.data, - labelId = R.string.custom_token_contract_address_input_title, - placeholder = "0x0000000000000000000000000000000000000000", - isEnabled = screenFieldData.viewState.isEnabled, - isLoading = screenFieldData.viewState.isLoading, - error = screenFieldData.error, - errorConverter = screenFieldData.errorConverter, -// trailingIcon = { PasteClearButton(showFirst = tokenField.data.value.isEmpty()) } - ) { - domainStore.dispatch(OnTokenContractAddressChanged(Field.Data(it, true))) - } - SpacerH8() -} - -@Composable -fun TokenNameView(screenFieldData: ScreenFieldData) { - if (!screenFieldData.viewState.isVisible) return - - val tokenField = screenFieldData.field as TokenField - - OutlinedTextFieldWidget( - fieldData = tokenField.data, - labelId = R.string.custom_token_name_input_title, - placeholderId = R.string.custom_token_name_input_placeholder, - isEnabled = screenFieldData.viewState.isEnabled, - error = screenFieldData.error, - errorConverter = screenFieldData.errorConverter, - ) { - domainStore.dispatch(OnTokenNameChanged(Field.Data(it, true))) - } - SpacerH8() -} - -@Composable -fun TokenNetworkView( - screenFieldData: ScreenFieldData, - state: AddCustomTokenState, - closePopupTrigger: ClosePopupTrigger, -) { - if (!screenFieldData.viewState.isVisible) return - - val notSelected = stringResource(id = R.string.custom_token_network_input_not_selected) - val networkField = screenFieldData.field as TokenBlockchainField - - BlockchainSpinner( - title = R.string.custom_token_network_input_title, - itemList = networkField.itemList, - selectedItem = networkField.data, - isEnabled = screenFieldData.viewState.isEnabled, - textFieldConverter = { state.blockchainToName(it) ?: notSelected }, - closePopupTrigger = closePopupTrigger, - ) { domainStore.dispatch(OnTokenNetworkChanged(Field.Data(it, true))) } - SpacerH8() -} - -@Composable -fun TokenSymbolView(screenFieldData: ScreenFieldData) { - if (!screenFieldData.viewState.isVisible) return - - val tokenField = screenFieldData.field as TokenField - - OutlinedTextFieldWidget( - fieldData = tokenField.data, - labelId = R.string.custom_token_token_symbol_input_title, - placeholderId = R.string.custom_token_token_symbol_input_placeholder, - isEnabled = screenFieldData.viewState.isEnabled, - error = screenFieldData.error, - errorConverter = screenFieldData.errorConverter, - ) { domainStore.dispatch(OnTokenSymbolChanged(Field.Data(it, true))) } - SpacerH8() -} - -@Composable -fun TokenDecimalsView(screenFieldData: ScreenFieldData) { - if (!screenFieldData.viewState.isVisible) return - - val tokenField = screenFieldData.field as TokenField - - OutlinedTextFieldWidget( - fieldData = tokenField.data, - labelId = R.string.custom_token_decimals_input_title, - placeholder = "8", - isEnabled = screenFieldData.viewState.isEnabled, - error = screenFieldData.error, - errorConverter = screenFieldData.errorConverter, - keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), - ) { domainStore.dispatch(OnTokenDecimalsChanged(Field.Data(it, true))) } - SpacerH8() -} - -@Composable -fun TokenDerivationPathView( - screenFieldData: ScreenFieldData, - state: AddCustomTokenState, - closePopupTrigger: ClosePopupTrigger, -) { - if (!screenFieldData.viewState.isVisible) return - - val notSelected = stringResource(id = R.string.custom_token_derivation_path_default) - val networkField = screenFieldData.field as TokenDerivationPathField - - BlockchainSpinner( - title = R.string.custom_token_derivation_path_input_title, - itemList = networkField.itemList, - selectedItem = networkField.data, - isEnabled = screenFieldData.viewState.isEnabled, - textFieldConverter = { state.blockchainToName(it) ?: notSelected }, - dropdownItemView = { blockchain -> - val derivationPathName = state.blockchainToName(blockchain, true) ?: notSelected - val blockchainName = state.blockchainToName(blockchain) ?: notSelected - TitleSubtitle(derivationPathName, blockchainName) - }, - closePopupTrigger = closePopupTrigger, - ) { domainStore.dispatch(OnTokenDerivationPathChanged(Field.Data(it, true))) } - SpacerH8() -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/customtoken/legacy/compose/HangingOverKeyboardView.kt b/app/src/main/java/com/tangem/tap/features/customtoken/legacy/compose/HangingOverKeyboardView.kt deleted file mode 100644 index 6aa2994210..0000000000 --- a/app/src/main/java/com/tangem/tap/features/customtoken/legacy/compose/HangingOverKeyboardView.kt +++ /dev/null @@ -1,32 +0,0 @@ -package com.tangem.tap.features.customtoken.legacy.compose - -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.BoxScope -import androidx.compose.foundation.layout.padding -import androidx.compose.runtime.Composable -import androidx.compose.runtime.State -import androidx.compose.runtime.remember -import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.Dp -import androidx.compose.ui.unit.dp -import com.tangem.core.ui.components.Keyboard - -/** -[REDACTED_AUTHOR] - */ -@Composable -fun HangingOverKeyboardView( - modifier: Modifier = Modifier, - keyboardState: State, - spaceBetweenKeyboard: Dp = 0.dp, - content: @Composable (BoxScope.() -> Unit), -) { - val padding = remember(keyboardState) { - when (val state = keyboardState.value) { - is Keyboard.Closed -> 0.dp - is Keyboard.Opened -> state.height + spaceBetweenKeyboard - } - } - - Box(modifier.padding(bottom = padding)) { content() } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/customtoken/legacy/compose/OutlinedSpinner.kt b/app/src/main/java/com/tangem/tap/features/customtoken/legacy/compose/OutlinedSpinner.kt deleted file mode 100644 index a3ddaedf7e..0000000000 --- a/app/src/main/java/com/tangem/tap/features/customtoken/legacy/compose/OutlinedSpinner.kt +++ /dev/null @@ -1,103 +0,0 @@ -package com.tangem.tap.features.customtoken.legacy.compose - -import android.os.Handler -import android.os.Looper -import androidx.compose.material.* -import androidx.compose.runtime.Composable -import androidx.compose.runtime.key -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.ui.Modifier -import androidx.compose.ui.tooling.preview.Preview -import androidx.core.os.postDelayed -import com.tangem.blockchain.common.Blockchain -import com.tangem.common.extensions.VoidCallback -import com.tangem.domain.common.form.Field -import com.tangem.tap.common.compose.TangemTextFieldsDefault -import com.tangem.tap.common.extensions.ValueCallback - -/** -[REDACTED_AUTHOR] - */ -@Suppress("MagicNumber") -@OptIn(ExperimentalMaterialApi::class) -@Composable -internal fun OutlinedSpinner( - label: String, - itemList: List, - selectedItem: Field.Data, - onItemSelected: ValueCallback, - modifier: Modifier = Modifier, - textFieldConverter: (T) -> String = { it.toString() }, - dropdownItemView: @Composable ((T) -> Unit)? = null, - isEnabled: Boolean = true, - onClose: VoidCallback = {}, - closePopupTrigger: ClosePopupTrigger = ClosePopupTrigger(), -) { - val rIsExpanded = remember { mutableStateOf(false) } - val stateSelectedItem = remember { mutableStateOf(selectedItem.value) } - if (!selectedItem.isUserInput) { - stateSelectedItem.value = selectedItem.value - } - - val onDropDownItemSelectedInternal: (T) -> Unit = { - stateSelectedItem.value = it - rIsExpanded.value = false - onItemSelected(it) - } - val onDismissRequest = { - rIsExpanded.value = false - onClose() - } - - closePopupTrigger.close = { - onDismissRequest() - Handler(Looper.getMainLooper()).postDelayed(100) { - closePopupTrigger.onCloseComplete() - } - } - - ExposedDropdownMenuBox( - expanded = rIsExpanded.value, - onExpandedChange = { rIsExpanded.value = !rIsExpanded.value }, - ) { - OutlinedTextField( - modifier = modifier, - readOnly = true, - enabled = isEnabled, - value = textFieldConverter(stateSelectedItem.value), - onValueChange = {}, - label = { Text(label) }, - trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = rIsExpanded.value) }, - colors = TangemTextFieldsDefault.defaultTextFieldColors, - ) - - if (isEnabled) { - ExposedDropdownMenu(expanded = rIsExpanded.value, onDismissRequest = onDismissRequest) { - itemList.forEach { item -> - key(item) { - DropdownMenuItem(onClick = { onDropDownItemSelectedInternal(item) }) { - if (dropdownItemView == null) Text(textFieldConverter(item)) else dropdownItemView(item) - } - } - } - } - } - } -} - -class ClosePopupTrigger { - var close: () -> Unit = {} - var onCloseComplete: () -> Unit = {} -} - -@Preview -@Composable -private fun TestSpinnerPreview() { - OutlinedSpinner( - label = "Blockchain name", - itemList = listOf(Blockchain.values()), - selectedItem = Field.Data(Blockchain.Avalanche, false), - onItemSelected = {}, - ) -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/customtoken/legacy/compose/SelectTokenNetworkDialog.kt b/app/src/main/java/com/tangem/tap/features/customtoken/legacy/compose/SelectTokenNetworkDialog.kt deleted file mode 100644 index 441969ad5e..0000000000 --- a/app/src/main/java/com/tangem/tap/features/customtoken/legacy/compose/SelectTokenNetworkDialog.kt +++ /dev/null @@ -1,20 +0,0 @@ -package com.tangem.tap.features.customtoken.legacy.compose - -import androidx.compose.runtime.Composable -import androidx.compose.ui.res.stringResource -import com.tangem.domain.DomainDialog -import com.tangem.tap.common.compose.TitleSubtitle -import com.tangem.wallet.R - -/** -[REDACTED_AUTHOR] - */ -@Composable -fun SelectTokenNetworkDialog(dialog: DomainDialog.SelectTokenDialog, onDismissRequest: () -> Unit) { - SimpleDialog( - title = stringResource(id = R.string.custom_token_network_input_title), - items = dialog.items, - onSelect = dialog.onSelect, - onDismissRequest = onDismissRequest, - ) { network -> TitleSubtitle(dialog.networkIdConverter(network.networkId), network.contractAddress ?: "") } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/customtoken/legacy/compose/test/ContractAddressTests.kt b/app/src/main/java/com/tangem/tap/features/customtoken/legacy/compose/test/ContractAddressTests.kt deleted file mode 100644 index 3ba171f427..0000000000 --- a/app/src/main/java/com/tangem/tap/features/customtoken/legacy/compose/test/ContractAddressTests.kt +++ /dev/null @@ -1,120 +0,0 @@ -package com.tangem.tap.features.customtoken.legacy.compose.test - -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.material.Button -import androidx.compose.material.Divider -import androidx.compose.material.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import com.tangem.blockchain.common.Blockchain -import com.tangem.common.extensions.VoidCallback -import com.tangem.domain.common.form.Field -import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenAction -import com.tangem.domain.redux.domainStore - -/** -[REDACTED_AUTHOR] - */ -@Composable -fun ContractAddressTests(onItemClick: VoidCallback) { - val casesInfo = listOf( - "USDC on ETH" to "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", - "BUSD on ETH" to "0x4fabb145d64652a948d72533023f6e7a623c7c53", - "ETH on AVALANCHE" to "0xf20d962a6c8f70c731bd838a3a388d7d48fa6e15", - "USDC on ETH (invalid - cut address)" to "0xa0b86991c6218b36c1d1", - "Custom EVM" to "0x1111111111111111112111111111111111111113", - "Supported by several networks" to "0xa1faa113cbe53436df28ff0aee54275c13b40975", - "Invalid" to "!@#_ _-%%^&&*((){P P2iOWsdfFQLA", - ) - CasesListContent(casesInfo, onItemClick) -} - -@Composable -fun SolanaAddressTests(onItemClick: VoidCallback) { - val casesInfo = listOf( - "USDT (full)" to "Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB", - "USDT (valid - 2/3 of address)" to "Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8Ben", - "USDT (invalid - 1/3 of address)" to "Es9vMFrzaCERmJ", - "ETH (full)" to "2FPyTwcZLUg1MDrwsyoP4D6s1tM7hAkHYRjkNb5w6Pxk", - ) - CasesListContent(casesInfo, onItemClick) -} - -@Composable -private fun CasesListContent(casesList: List>, onItemClick: VoidCallback) { - LazyColumn( - content = { - item { - Row { - ResetContractAddressButton(onItemClick) - Text("", modifier = Modifier.weight(1f)) - ResetAllFieldsButton(onItemClick) - } - Divider() - } - items(casesList.size) { - val (info, address) = casesList[it] - ContractAddressButton(info, address, onItemClick) - } - }, - ) -} - -@Composable -fun ResetAllFieldsButton(onItemClick: VoidCallback) { - ActionButton( - name = "Reset", - onClick = { - onItemClick() - domainStore.dispatch(AddCustomTokenAction.OnTokenContractAddressChanged(Field.Data("", false))) - domainStore.dispatch(AddCustomTokenAction.OnTokenNetworkChanged(Field.Data(Blockchain.Unknown, false))) - domainStore.dispatch(AddCustomTokenAction.OnTokenNameChanged(Field.Data("", false))) - domainStore.dispatch(AddCustomTokenAction.OnTokenSymbolChanged(Field.Data("", false))) - domainStore.dispatch(AddCustomTokenAction.OnTokenDecimalsChanged(Field.Data("", false))) - domainStore.dispatch( - AddCustomTokenAction.OnTokenDerivationPathChanged( - Field.Data( - Blockchain.Unknown, - false, - ), - ), - ) - }, - ) -} - -@Composable -fun ResetContractAddressButton(onItemClick: VoidCallback) { - ActionButton( - name = "Set empty address", - onClick = { - onItemClick() - domainStore.dispatch(AddCustomTokenAction.OnTokenContractAddressChanged(Field.Data("", false))) - }, - ) -} - -@Composable -private fun ContractAddressButton(name: String, address: String, onItemClick: VoidCallback) { - ActionButton( - modifier = Modifier.fillMaxWidth(), - name = name, - onClick = { - onItemClick() - domainStore.dispatch(AddCustomTokenAction.OnTokenContractAddressChanged(Field.Data(address, false))) - }, - ) -} - -@Composable -fun ActionButton(name: String, onClick: () -> Unit, modifier: Modifier = Modifier) { - Button( - modifier = modifier.padding(horizontal = 8.dp), - onClick = onClick, - ) { Text(name, fontSize = 12.sp) } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/customtoken/legacy/compose/test/TestCasesList.kt b/app/src/main/java/com/tangem/tap/features/customtoken/legacy/compose/test/TestCasesList.kt deleted file mode 100644 index 3bae26e193..0000000000 --- a/app/src/main/java/com/tangem/tap/features/customtoken/legacy/compose/test/TestCasesList.kt +++ /dev/null @@ -1,53 +0,0 @@ -package com.tangem.tap.features.customtoken.legacy.compose.test - -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.padding -import androidx.compose.material.Button -import androidx.compose.material.Surface -import androidx.compose.material.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.colorResource -import androidx.compose.ui.unit.dp -import com.tangem.common.extensions.VoidCallback -import com.tangem.wallet.BuildConfig -import com.tangem.wallet.R - -/** -[REDACTED_AUTHOR] - */ -@Composable -fun TestCasesList(onItemClick: (TestCase) -> Unit) { - if (!BuildConfig.TEST_ACTION_ENABLED) return - - Surface(color = colorResource(id = R.color.lightGray5)) { - Column(Modifier.padding(horizontal = 16.dp)) { - listOf(TestCase.ContractAddress, TestCase.SolanaTokens) - .map { case -> TestCaseListItem(testCase = case, onItemClick = { onItemClick(case) }) } - } - } -} - -@Composable -fun TestCaseListItem(testCase: TestCase, onItemClick: () -> Unit) { - Row( - verticalAlignment = Alignment.CenterVertically, - ) { - Text( - modifier = Modifier.weight(1f), - text = testCase.description, - ) - Button( - onClick = onItemClick, - ) { Text("Start") } - } -} - -enum class TestCase(val description: String, val content: @Composable (VoidCallback) -> Unit) { - ContractAddress("Test contract address field", { ContractAddressTests(it) }), - Auto("Test contract address field", { ContractAddressTests(it) }), - SolanaTokens("Test Solana contract addresses", { SolanaAddressTests(it) }), - ; -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/tokens/impl/di/TokensListRouterModule.kt b/app/src/main/java/com/tangem/tap/features/tokens/impl/di/TokensListRouterModule.kt index 07a7a3412d..2a7da5cca4 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/impl/di/TokensListRouterModule.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/impl/di/TokensListRouterModule.kt @@ -1,6 +1,5 @@ package com.tangem.tap.features.tokens.impl.di -import com.tangem.tap.features.customtoken.api.featuretoggles.CustomTokenFeatureToggles import com.tangem.tap.features.tokens.impl.presentation.router.DefaultTokensListRouter import com.tangem.tap.features.tokens.impl.presentation.router.TokensListRouter import dagger.Module @@ -18,7 +17,5 @@ internal object TokensListRouterModule { @Provides @ViewModelScoped - fun provideTokensListRouter(customTokenFeatureToggles: CustomTokenFeatureToggles): TokensListRouter { - return DefaultTokensListRouter(customTokenFeatureToggles) - } + fun provideTokensListRouter(): TokensListRouter = DefaultTokensListRouter() } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/router/DefaultTokensListRouter.kt b/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/router/DefaultTokensListRouter.kt index 93ae4e1b17..68b277ea7e 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/router/DefaultTokensListRouter.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/router/DefaultTokensListRouter.kt @@ -6,8 +6,6 @@ import com.tangem.core.navigation.NavigationAction import com.tangem.tap.common.extensions.dispatchDialogShow import com.tangem.tap.common.extensions.dispatchNotification import com.tangem.tap.common.redux.AppDialog -import com.tangem.tap.features.customtoken.api.featuretoggles.CustomTokenFeatureToggles -import com.tangem.tap.features.tokens.legacy.redux.TokensAction import com.tangem.tap.features.wallet.redux.models.WalletDialog import com.tangem.tap.store import com.tangem.wallet.R @@ -18,20 +16,14 @@ import com.tangem.wallet.R * [REDACTED_AUTHOR] */ -internal class DefaultTokensListRouter( - private val customTokenFeatureToggles: CustomTokenFeatureToggles, -) : TokensListRouter { +internal class DefaultTokensListRouter : TokensListRouter { override fun popBackStack() { store.dispatch(NavigationAction.PopBackTo()) } override fun openAddCustomTokenScreen() { - if (customTokenFeatureToggles.isRedesignedScreenEnabled) { - store.dispatch(NavigationAction.NavigateTo(AppScreen.AddCustomToken)) - } else { - store.dispatch(TokensAction.PrepareAndNavigateToAddCustomToken) - } + store.dispatch(NavigationAction.NavigateTo(AppScreen.AddCustomToken)) } override fun showAddressCopiedNotification() { diff --git a/app/src/main/java/com/tangem/tap/features/tokens/legacy/redux/TokensAction.kt b/app/src/main/java/com/tangem/tap/features/tokens/legacy/redux/TokensAction.kt index c24f982fca..fb79c7bbde 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/legacy/redux/TokensAction.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/legacy/redux/TokensAction.kt @@ -17,7 +17,4 @@ sealed interface TokensAction : Action { // TODO: [REDACTED_TASK_KEY] Remove this action data class SaveChanges(val tokens: List, val blockchains: List) : TokensAction - - // TODO: Remove this action in 4.7 release - object PrepareAndNavigateToAddCustomToken : TokensAction } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/tokens/legacy/redux/TokensMiddleware.kt b/app/src/main/java/com/tangem/tap/features/tokens/legacy/redux/TokensMiddleware.kt index 1a5eb1fd45..db87942c84 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/legacy/redux/TokensMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/legacy/redux/TokensMiddleware.kt @@ -9,28 +9,19 @@ import com.tangem.common.extensions.ByteArrayKey import com.tangem.common.extensions.guard import com.tangem.common.extensions.toMapKey import com.tangem.common.flatMap -import com.tangem.core.analytics.Analytics -import com.tangem.core.navigation.AppScreen import com.tangem.core.navigation.NavigationAction import com.tangem.crypto.hdWallet.DerivationPath -import com.tangem.domain.DomainWrapped import com.tangem.domain.common.configs.CardConfig import com.tangem.domain.common.util.derivationStyleProvider -import com.tangem.domain.common.util.hasDerivation import com.tangem.domain.common.util.supportsHdWallet -import com.tangem.domain.features.addCustomToken.CustomCurrency -import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenAction import com.tangem.domain.models.scan.ScanResponse -import com.tangem.domain.redux.domainStore import com.tangem.operations.derivation.ExtendedPublicKeysMap import com.tangem.tap.* -import com.tangem.tap.common.analytics.events.ManageTokens import com.tangem.tap.common.extensions.dispatchDebugErrorNotification import com.tangem.tap.common.extensions.dispatchOnMain import com.tangem.tap.common.redux.AppState import com.tangem.tap.common.redux.global.GlobalAction import com.tangem.tap.domain.TapError -import com.tangem.tap.domain.model.WalletDataModel import com.tangem.tap.features.wallet.models.Currency import kotlinx.coroutines.delay import kotlinx.coroutines.launch @@ -44,7 +35,6 @@ object TokensMiddleware { { action -> when (action) { is TokensAction.SaveChanges -> handleSaveChanges(action) - is TokensAction.PrepareAndNavigateToAddCustomToken -> handleAddingCustomToken() } next(action) } @@ -231,56 +221,4 @@ object TokensMiddleware { } walletCurrenciesManager.removeCurrencies(selectedUserWallet, currencies) } - - private fun isNeedToDerive(scanResponse: ScanResponse, currency: Currency): Boolean { - return currency.derivationPath?.let { - !scanResponse.hasDerivation(currency.blockchain, it) - } ?: false - } - - private fun handleAddingCustomToken() = scope.launch { - val onAddCustomToken = fun(customCurrency: CustomCurrency) { - val scanResponse = store.state.globalState.scanResponse ?: return - - fun submitAndPopBack(scanResponse: ScanResponse, currencyList: List) { - submitAdd(scanResponse, currencyList) - // pop from the AddCustomTokenScreen - store.dispatchOnMain(NavigationAction.PopBackTo()) - store.dispatchOnMain(NavigationAction.PopBackTo()) - } - - Analytics.send(ManageTokens.CustomToken.TokenWasAdded(customCurrency)) - val currency = Currency.fromCustomCurrency(customCurrency) - val isNeedToDerive = isNeedToDerive(scanResponse, currency) - val currencyList = listOf(currency) - if (isNeedToDerive) { - deriveMissingBlockchains(scanResponse, currencyList) { - submitAndPopBack(it, currencyList) - } - } else { - submitAndPopBack(scanResponse, currencyList) - } - } - - val addedCurrencies = store.state.walletState.walletsStores - .map { walletStore -> walletStore.walletsData.map(WalletDataModel::currency) } - .flatten() - .map { currency -> - when (currency) { - is Currency.Blockchain -> DomainWrapped.Currency.Blockchain( - currency.blockchain, - currency.derivationPath, - ) - - is Currency.Token -> DomainWrapped.Currency.Token( - currency.token, - currency.blockchain, - currency.derivationPath, - ) - } - } - domainStore.dispatch(AddCustomTokenAction.Init.SetAddedCurrencies(addedCurrencies)) - domainStore.dispatch(AddCustomTokenAction.Init.SetOnAddTokenCallback(onAddCustomToken)) - store.dispatch(NavigationAction.NavigateTo(AppScreen.AddCustomToken)) - } } \ No newline at end of file diff --git a/app/src/main/res/layout/view_compose_fragment.xml b/app/src/main/res/layout/view_compose_fragment.xml deleted file mode 100644 index af9273a84d..0000000000 --- a/app/src/main/res/layout/view_compose_fragment.xml +++ /dev/null @@ -1,33 +0,0 @@ - - - - - - - - - - - - diff --git a/common/src/main/java/com/tangem/common/Filter.kt b/common/src/main/java/com/tangem/common/Filter.kt deleted file mode 100644 index 6607f4080f..0000000000 --- a/common/src/main/java/com/tangem/common/Filter.kt +++ /dev/null @@ -1,8 +0,0 @@ -package com.tangem.common - -/** -[REDACTED_AUTHOR] - */ -interface Filter { - fun filter(value: T): Boolean -} \ No newline at end of file diff --git a/common/src/main/java/com/tangem/common/Validator.kt b/common/src/main/java/com/tangem/common/Validator.kt deleted file mode 100644 index 8654e9d07e..0000000000 --- a/common/src/main/java/com/tangem/common/Validator.kt +++ /dev/null @@ -1,8 +0,0 @@ -package com.tangem.common - -/** -[REDACTED_AUTHOR] - */ -interface Validator { - fun validate(data: Data? = null): Error? -} \ No newline at end of file diff --git a/common/src/main/java/com/tangem/common/module/ModuleMessage.kt b/common/src/main/java/com/tangem/common/module/ModuleMessage.kt index 511ffa8d99..1f8414a0ca 100644 --- a/common/src/main/java/com/tangem/common/module/ModuleMessage.kt +++ b/common/src/main/java/com/tangem/common/module/ModuleMessage.kt @@ -17,11 +17,6 @@ abstract class ModuleError : Throwable(), ModuleMessage { abstract val data: Any? } -/** - * An exception marked as FbConsumeException should be submitted to Firebase.Crashlytics as a non-fatal issue. - */ -interface FbConsumeException - interface ModuleMessageConverter { fun convert(message: ModuleMessage): R } \ No newline at end of file diff --git a/core/featuretoggles/src/main/assets/configs/feature_toggles_config.json b/core/featuretoggles/src/main/assets/configs/feature_toggles_config.json index 8cd92e9d2f..2c809df137 100644 --- a/core/featuretoggles/src/main/assets/configs/feature_toggles_config.json +++ b/core/featuretoggles/src/main/assets/configs/feature_toggles_config.json @@ -3,10 +3,6 @@ "name": "OPTIMISM_SWAP_FEATURE_ENABLED", "version": "4.3.1" }, - { - "name": "REDESIGNED_CUSTOM_TOKEN_SCREEN_ENABLED", - "version": "4.7.0" - }, { "name": "NEW_CARD_SCANNING_ENABLED", "version": "4.11.0" diff --git a/domain/legacy/src/main/java/com/tangem/domain/DomainDialog.kt b/domain/legacy/src/main/java/com/tangem/domain/DomainDialog.kt deleted file mode 100644 index c1f72a19de..0000000000 --- a/domain/legacy/src/main/java/com/tangem/domain/DomainDialog.kt +++ /dev/null @@ -1,19 +0,0 @@ -package com.tangem.domain - -import com.tangem.common.extensions.VoidCallback -import com.tangem.datasource.api.tangemTech.models.CoinsResponse - -/** -[REDACTED_AUTHOR] - */ -sealed interface DomainDialog { - - data class DialogError(val error: DomainModuleError) : DomainDialog - - data class SelectTokenDialog( - val items: List, - val networkIdConverter: (String) -> String, - val onSelect: (CoinsResponse.Coin.Network) -> Unit, - val onClose: VoidCallback = {}, - ) : DomainDialog -} \ No newline at end of file diff --git a/domain/legacy/src/main/java/com/tangem/domain/DomainLayer.kt b/domain/legacy/src/main/java/com/tangem/domain/DomainLayer.kt deleted file mode 100644 index a8a52c2d4e..0000000000 --- a/domain/legacy/src/main/java/com/tangem/domain/DomainLayer.kt +++ /dev/null @@ -1,26 +0,0 @@ -package com.tangem.domain - -import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenAction -import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenState -import com.tangem.domain.redux.state.ActionStateLoggerImpl - -/** -[REDACTED_AUTHOR] - */ -object DomainLayer { - internal val actionStateLogger = ActionStateLoggerImpl() - - var onInitComplete: ((DomainModuleError?) -> Unit)? = null - - fun init() { - initActionStateLogger() - - onInitComplete?.invoke(null) - } - - private fun initActionStateLogger() { - val factory = actionStateLogger.actionStateConvertersFactory - - factory.addConverter(AddCustomTokenAction::class.java, AddCustomTokenState.Converter()) - } -} \ No newline at end of file diff --git a/domain/legacy/src/main/java/com/tangem/domain/DomainModuleMessage.kt b/domain/legacy/src/main/java/com/tangem/domain/DomainModuleMessage.kt index 27d7606e7f..d762e94382 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/DomainModuleMessage.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/DomainModuleMessage.kt @@ -1,6 +1,5 @@ package com.tangem.domain -import com.tangem.common.module.FbConsumeException import com.tangem.common.module.ModuleError import com.tangem.common.module.ModuleErrorCode import com.tangem.common.module.ModuleMessage @@ -36,32 +35,14 @@ sealed class AddCustomTokenError( ) { object FieldIsEmpty : AddCustomTokenError() - object FieldIsNotEmpty : AddCustomTokenError() object InvalidContractAddress : AddCustomTokenError() object NetworkIsNotSelected : AddCustomTokenError() object InvalidDecimalsCount : AddCustomTokenError() object InvalidDerivationPath : AddCustomTokenError() - sealed class Network : AddCustomTokenError() { - object CheckAddressRequestError : Network() - } - sealed class Warning : AddCustomTokenError() { object PotentialScamToken : Warning() object TokenAlreadyAdded : Warning() object UnsupportedSolanaToken : Warning() } - - data class SelectTokeNetworkError(val networkId: String) : - AddCustomTokenError( - message = "Unknown network [$networkId] should not be included in the network selection dialog.", - ), - FbConsumeException - - data class UnAppropriateInitialization( - val of: String, - val info: String? = null, - ) : AddCustomTokenError( - message = "The [$of], must be properly initialized. Info [$info]", - ) } \ No newline at end of file diff --git a/domain/legacy/src/main/java/com/tangem/domain/DomainWrapped.kt b/domain/legacy/src/main/java/com/tangem/domain/DomainWrapped.kt deleted file mode 100644 index 902244560d..0000000000 --- a/domain/legacy/src/main/java/com/tangem/domain/DomainWrapped.kt +++ /dev/null @@ -1,33 +0,0 @@ -package com.tangem.domain - -/** -[REDACTED_AUTHOR] - * Provides a temporary copies of the app module classes, data structures, etc. - */ -// TODO: refactoring: : after refactoring they should be unwrapped and moved -// to appropriate parts of module -@Deprecated("After refactoring they should be unwrapped and moved to appropriate parts of module") -sealed interface DomainWrapped { - - // Mirror reflection ot the com.tangem.tap.features.wallet.redux.Currency - sealed interface Currency { - val blockchain: com.tangem.blockchain.common.Blockchain - val currencySymbol: String - val derivationPath: String? - - data class Token( - val token: com.tangem.blockchain.common.Token, - override val blockchain: com.tangem.blockchain.common.Blockchain, - override val derivationPath: String?, - ) : Currency { - override val currencySymbol = token.symbol - } - - data class Blockchain( - override val blockchain: com.tangem.blockchain.common.Blockchain, - override val derivationPath: String?, - ) : Currency { - override val currencySymbol: String = blockchain.currency - } - } -} \ No newline at end of file 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 bd892377ae..0dcaa2b00a 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 @@ -1,7 +1,6 @@ package com.tangem.domain.common import com.tangem.blockchain.common.Blockchain -import com.tangem.common.card.Card import com.tangem.common.card.FirmwareVersion import com.tangem.domain.models.scan.CardDTO import java.util.* @@ -69,6 +68,4 @@ object TapWorkarounds { fun isStart2CoinIssuer(cardIssuer: String?): Boolean { return cardIssuer?.lowercase(Locale.US) == START_2_COIN_ISSUER } - - fun Card.getTangemNoteBlockchain(): Blockchain? = tangemNoteBatches[batchId] ?: null } \ No newline at end of file diff --git a/domain/legacy/src/main/java/com/tangem/domain/common/form/FieldDataConverters.kt b/domain/legacy/src/main/java/com/tangem/domain/common/form/FieldDataConverters.kt deleted file mode 100644 index b406d02f7c..0000000000 --- a/domain/legacy/src/main/java/com/tangem/domain/common/form/FieldDataConverters.kt +++ /dev/null @@ -1,40 +0,0 @@ -package com.tangem.domain.common.form - -import com.tangem.common.json.MoshiJsonConverter - -/** -[REDACTED_AUTHOR] - */ -interface DataConverterVisitor { - fun visit(data: Data?) - fun getConvertedData(): Result -} - -interface FieldDataConverter : DataConverterVisitor - -abstract class BaseFieldDataConverter : FieldDataConverter { - private val collectIds: List - get() = getIdToCollect() - - protected val collectedData: MutableMap = mutableMapOf() - - override fun visit(data: Pair>?) { - val id = data?.first ?: return - - if (collectIds.contains(id)) { - collectedData[id] = data.second.value - } - } - - abstract fun getIdToCollect(): List -} - -class FieldToJsonConverter( - private val fieldsToConvert: List = listOf(), - private val jsonConverter: MoshiJsonConverter, -) : BaseFieldDataConverter() { - - override fun getConvertedData(): String = jsonConverter.toJson(collectedData, " ") - - override fun getIdToCollect(): List = fieldsToConvert -} \ No newline at end of file diff --git a/domain/legacy/src/main/java/com/tangem/domain/common/form/FieldsValidators.kt b/domain/legacy/src/main/java/com/tangem/domain/common/form/FieldsValidators.kt deleted file mode 100644 index 067feb900f..0000000000 --- a/domain/legacy/src/main/java/com/tangem/domain/common/form/FieldsValidators.kt +++ /dev/null @@ -1,99 +0,0 @@ -package com.tangem.domain.common.form - -import com.tangem.blockchain.blockchains.ethereum.EthereumAddressService -import com.tangem.blockchain.blockchains.solana.SolanaAddressService -import com.tangem.blockchain.blockchains.tron.TronAddressService -import com.tangem.blockchain.common.Blockchain -import com.tangem.blockchain.common.address.AddressService -import com.tangem.common.Validator -import com.tangem.common.card.EllipticCurve -import com.tangem.domain.AddCustomTokenError -import timber.log.Timber - -/** -[REDACTED_AUTHOR] - */ -interface CustomTokenValidator : Validator - -class StringIsEmptyValidator : CustomTokenValidator { - override fun validate(data: String?): AddCustomTokenError? { - return if (data.isNullOrEmpty()) null else AddCustomTokenError.FieldIsNotEmpty - } -} - -class StringIsNotEmptyValidator : CustomTokenValidator { - override fun validate(data: String?): AddCustomTokenError? { - return if (data.isNullOrEmpty()) AddCustomTokenError.FieldIsEmpty else null - } -} - -class TokenContractAddressValidator : CustomTokenValidator { - - private var blockchain: Blockchain = Blockchain.Unknown - - private val successAddressValidator = object : AddressService() { - override fun makeAddress(walletPublicKey: ByteArray, curve: EllipticCurve?): String { - throw UnsupportedOperationException() - } - - override fun validate(address: String): Boolean = true - } - - fun nextValidationFor(blockchain: Blockchain) { - this.blockchain = blockchain - } - - override fun validate(data: String?): AddCustomTokenError? { - return when { - data.isNullOrEmpty() -> AddCustomTokenError.FieldIsEmpty - getAddressService().validate(data) -> null - else -> AddCustomTokenError.InvalidContractAddress - } - } - - private fun getAddressService(): AddressService { - return when (blockchain) { - Blockchain.Unknown -> successAddressValidator - Blockchain.Binance, Blockchain.BinanceTestnet -> successAddressValidator - Blockchain.Solana, Blockchain.SolanaTestnet -> SolanaAddressService() - Blockchain.Tron, Blockchain.TronTestnet -> TronAddressService() - else -> { - if (blockchain.isEvm()) { - EthereumAddressService() - } else { - Timber.e("Throw for blockchain: ${blockchain.fullName}") - throw UnsupportedOperationException() - } - } - } - } -} - -class TokenNetworkValidator : CustomTokenValidator { - override fun validate(data: Blockchain?): AddCustomTokenError? { - return when (data) { - null, Blockchain.Unknown -> AddCustomTokenError.NetworkIsNotSelected - else -> null - } - } -} - -class TokenNameValidator : CustomTokenValidator { - override fun validate(data: String?): AddCustomTokenError? = StringIsNotEmptyValidator().validate(data) -} - -class TokenSymbolValidator : CustomTokenValidator { - override fun validate(data: String?): AddCustomTokenError? = StringIsNotEmptyValidator().validate(data) -} - -class TokenDecimalsValidator : CustomTokenValidator { - override fun validate(data: String?): AddCustomTokenError? { - val decimal = data?.toIntOrNull() ?: return AddCustomTokenError.FieldIsEmpty - - return if (decimal > INVALID_DECIMALS_COUNT) AddCustomTokenError.InvalidDecimalsCount else null - } - - private companion object { - const val INVALID_DECIMALS_COUNT = 30 - } -} \ No newline at end of file diff --git a/domain/legacy/src/main/java/com/tangem/domain/common/form/Form.kt b/domain/legacy/src/main/java/com/tangem/domain/common/form/Form.kt deleted file mode 100644 index cca8b72a5f..0000000000 --- a/domain/legacy/src/main/java/com/tangem/domain/common/form/Form.kt +++ /dev/null @@ -1,62 +0,0 @@ -package com.tangem.domain.common.form - -/** -[REDACTED_AUTHOR] - */ -class Form( - fieldList: List>, -) { - private val _fieldList: MutableList> = fieldList.toMutableList() - - val fieldList: List> - get() = _fieldList.toList() - - fun getField(id: FieldId): DataField<*>? = fieldList.firstOrNull { it.id == id } - - fun getData(id: FieldId): Pair? = getField(id)?.getData() - - fun setField(field: DataField<*>) { - val oldField = getField(field.id) ?: return - val oldIndexOfField = _fieldList.indexOf(oldField) - if (oldIndexOfField == -1) return - - _fieldList.removeAt(oldIndexOfField) - _fieldList.add(oldIndexOfField, field) - } - - // convert this form data whatever you want - fun visitDataConverter(converter: FieldDataConverter<*>) { - fieldList.forEach { it.visitDataConverter(converter) } - } -} - -interface FieldId - -interface Field { - val id: FieldId - var data: Data - - data class Data( - val value: Data, - val isUserInput: Boolean, - ) -} - -typealias FieldData = Pair> - -interface DataField : Field { - fun getData(): Pair> - fun visitDataConverter(dataConverter: FieldDataConverter<*>) -} - -abstract class BaseDataField( - override val id: FieldId, - override var data: Field.Data, -) : DataField { - - override fun getData(): Pair> = id to data - - override fun visitDataConverter(dataConverter: FieldDataConverter<*>) { - dataConverter.visit(getData()) - } -} \ No newline at end of file diff --git a/domain/legacy/src/main/java/com/tangem/domain/features/addCustomToken/AddCustomTokenService.kt b/domain/legacy/src/main/java/com/tangem/domain/features/addCustomToken/AddCustomTokenService.kt deleted file mode 100644 index 1a5fc65e8c..0000000000 --- a/domain/legacy/src/main/java/com/tangem/domain/features/addCustomToken/AddCustomTokenService.kt +++ /dev/null @@ -1,52 +0,0 @@ -package com.tangem.domain.features.addCustomToken - -import com.tangem.datasource.api.tangemTech.TangemTechApi -import com.tangem.datasource.api.tangemTech.models.CoinsResponse -import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import kotlinx.coroutines.withContext - -/** -[REDACTED_AUTHOR] - */ -class AddCustomTokenService( - private val tangemTechApi: TangemTechApi, - private val dispatchers: CoroutineDispatcherProvider, - private val supportedTokenNetworkIds: List, -) { - - suspend fun findToken(contractAddress: String, networkId: String?): List { - return withContext(dispatchers.io) { - runCatching { - tangemTechApi.getCoins( - contractAddress = contractAddress, - networkIds = selectNetworksForSearch(networkId), - ) - } - .fold( - onSuccess = { response -> - var coinsList = mutableListOf() - response.coins.forEach { coin -> - val networksWithTheSameAddress = coin.networks - .filter { it.contractAddress != null || it.decimalCount != null } - .filter { it.contractAddress?.equals(contractAddress, ignoreCase = true) == true } - .filter { supportedTokenNetworkIds.contains(it.networkId) } - if (networksWithTheSameAddress.isNotEmpty()) { - val newToken = coin.copy(networks = networksWithTheSameAddress) - coinsList.add(newToken) - } - } - if (coinsList.size > 1) { - // https://tangem.slack.com/archives/GMXC6PP71/p1649672562078679 - coinsList = mutableListOf(coinsList[0]) - } - coinsList - }, - onFailure = { emptyList() }, - ) - } - } - - private fun selectNetworksForSearch(networkId: String?): String { - return networkId ?: supportedTokenNetworkIds.joinToString(",") - } -} \ No newline at end of file diff --git a/domain/legacy/src/main/java/com/tangem/domain/features/addCustomToken/CustomCurrency.kt b/domain/legacy/src/main/java/com/tangem/domain/features/addCustomToken/CustomCurrency.kt index 78ff46be86..15debdd88f 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/features/addCustomToken/CustomCurrency.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/features/addCustomToken/CustomCurrency.kt @@ -2,11 +2,7 @@ package com.tangem.domain.features.addCustomToken import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.Token -import com.tangem.blockchain.common.derivation.DerivationStyle import com.tangem.crypto.hdWallet.DerivationPath -import com.tangem.domain.common.form.BaseFieldDataConverter -import com.tangem.domain.common.form.FieldId -import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenState /** [REDACTED_AUTHOR] @@ -16,65 +12,16 @@ sealed class CustomCurrency( val derivationPath: DerivationPath?, ) { + @Deprecated("It will be removed in next releases") class CustomBlockchain( network: Blockchain, derivationPath: DerivationPath?, - ) : CustomCurrency(network, derivationPath) { - - class Converter( - private val derivationStyle: DerivationStyle?, - ) : BaseFieldDataConverter() { - override fun getConvertedData(): CustomBlockchain { - val mainNetwork = collectedData[CustomTokenFieldId.Network] as Blockchain - val derivationPathNetwork = collectedData[CustomTokenFieldId.DerivationPath] as Blockchain - val derivationPath = AddCustomTokenState.getDerivationPath( - mainNetwork, - derivationPathNetwork, - derivationStyle, - ) - return CustomBlockchain(mainNetwork, derivationPath) - } - - override fun getIdToCollect(): List = - listOf(CustomTokenFieldId.Network, CustomTokenFieldId.DerivationPath) - } - } + ) : CustomCurrency(network, derivationPath) + @Deprecated("It will be removed in next releases") class CustomToken( val token: Token, network: Blockchain, derivationPath: DerivationPath?, - ) : CustomCurrency(network, derivationPath) { - - class Converter( - private val tokenId: String?, - private val derivationStyle: DerivationStyle?, - ) : BaseFieldDataConverter() { - - override fun getConvertedData(): CustomToken { - val mainNetwork = collectedData[CustomTokenFieldId.Network] as Blockchain - val derivationPathNetwork = collectedData[CustomTokenFieldId.DerivationPath] as Blockchain - val derivationPath = AddCustomTokenState.getDerivationPath( - mainNetwork, - derivationPathNetwork, - derivationStyle, - ) - - val token = Token( - name = collectedData[CustomTokenFieldId.Name] as String, - symbol = collectedData[CustomTokenFieldId.Symbol] as String, - contractAddress = collectedData[CustomTokenFieldId.ContractAddress] as String, - decimals = (collectedData[CustomTokenFieldId.Decimals] as String).toInt(), - id = tokenId, - ) - return CustomToken( - token, - collectedData[CustomTokenFieldId.Network] as Blockchain, - derivationPath, - ) - } - - override fun getIdToCollect(): List = CustomTokenFieldId.values().toList() - } - } + ) : CustomCurrency(network, derivationPath) } \ No newline at end of file diff --git a/domain/legacy/src/main/java/com/tangem/domain/features/addCustomToken/FormFields.kt b/domain/legacy/src/main/java/com/tangem/domain/features/addCustomToken/FormFields.kt deleted file mode 100644 index e9ec16b2a1..0000000000 --- a/domain/legacy/src/main/java/com/tangem/domain/features/addCustomToken/FormFields.kt +++ /dev/null @@ -1,32 +0,0 @@ -package com.tangem.domain.features.addCustomToken - -import com.tangem.blockchain.common.Blockchain -import com.tangem.domain.common.form.BaseDataField -import com.tangem.domain.common.form.Field -import com.tangem.domain.common.form.FieldId - -/** -[REDACTED_AUTHOR] - */ -enum class CustomTokenFieldId : FieldId { - ContractAddress, - Network, - Name, - Symbol, - Decimals, - DerivationPath, -} - -data class TokenField( - override val id: FieldId, -) : BaseDataField(id, Field.Data("", false)) - -data class TokenBlockchainField( - override val id: FieldId, - val itemList: List, -) : BaseDataField(id, Field.Data(Blockchain.Unknown, false)) - -data class TokenDerivationPathField( - override val id: FieldId, - val itemList: List, -) : BaseDataField(id, Field.Data(Blockchain.Unknown, false)) \ No newline at end of file diff --git a/domain/legacy/src/main/java/com/tangem/domain/features/addCustomToken/redux/AddCustomTokenAction.kt b/domain/legacy/src/main/java/com/tangem/domain/features/addCustomToken/redux/AddCustomTokenAction.kt deleted file mode 100644 index c178c4a610..0000000000 --- a/domain/legacy/src/main/java/com/tangem/domain/features/addCustomToken/redux/AddCustomTokenAction.kt +++ /dev/null @@ -1,63 +0,0 @@ -package com.tangem.domain.features.addCustomToken.redux - -import com.tangem.blockchain.common.Blockchain -import com.tangem.datasource.api.tangemTech.models.CoinsResponse -import com.tangem.domain.AddCustomTokenError -import com.tangem.domain.DomainWrapped -import com.tangem.domain.common.form.Field -import com.tangem.domain.common.form.FieldId -import com.tangem.domain.features.addCustomToken.CustomCurrency -import com.tangem.domain.features.addCustomToken.CustomTokenFieldId -import org.rekotlin.Action - -/** -[REDACTED_AUTHOR] - */ -sealed class AddCustomTokenAction : Action { - sealed class Init : AddCustomTokenAction() { - data class SetAddedCurrencies(val addedCurrencies: List) : AddCustomTokenAction() - data class SetOnAddTokenCallback(val callback: (CustomCurrency) -> Unit) : AddCustomTokenAction() - } - - object OnCreate : AddCustomTokenAction() - - object OnDestroy : AddCustomTokenAction() - - // from user, ui - data class OnTokenContractAddressChanged(val contractAddress: Field.Data) : AddCustomTokenAction() - data class OnTokenNetworkChanged(val blockchainNetwork: Field.Data) : AddCustomTokenAction() - data class OnTokenNameChanged(val tokenName: Field.Data) : AddCustomTokenAction() - data class OnTokenSymbolChanged(val tokenSymbol: Field.Data) : AddCustomTokenAction() - data class OnTokenDerivationPathChanged( - val blockchainDerivationPath: Field.Data, - ) : AddCustomTokenAction() - - data class OnTokenDecimalsChanged(val tokenDecimals: Field.Data) : AddCustomTokenAction() - object OnAddCustomTokenClicked : AddCustomTokenAction() - - data class SetFoundTokenInfo(val foundToken: CoinsResponse.Coin?) : AddCustomTokenAction() - - // form fields - data class UpdateForm(val state: AddCustomTokenState) : AddCustomTokenAction() - - sealed class FieldError : AddCustomTokenAction() { - data class Add(val id: CustomTokenFieldId, val error: AddCustomTokenError) : FieldError() - data class Remove(val id: CustomTokenFieldId) : FieldError() - } - - // warnings - sealed class Warning : AddCustomTokenAction() { - data class Add(val warnings: Set) : Warning() - data class Remove(val warnings: Set) : Warning() - data class Replace( - val remove: Set, - val add: Set, - ) : Warning() - } - - // To change the screenState - sealed class Screen : AddCustomTokenAction() { - data class UpdateTokenFields(val pairs: List>) : Screen() - data class UpdateAddButton(val addButton: ViewStates.AddButton) : Screen() - } -} \ No newline at end of file diff --git a/domain/legacy/src/main/java/com/tangem/domain/features/addCustomToken/redux/AddCustomTokenHub.kt b/domain/legacy/src/main/java/com/tangem/domain/features/addCustomToken/redux/AddCustomTokenHub.kt deleted file mode 100644 index aa12f86b00..0000000000 --- a/domain/legacy/src/main/java/com/tangem/domain/features/addCustomToken/redux/AddCustomTokenHub.kt +++ /dev/null @@ -1,720 +0,0 @@ -package com.tangem.domain.features.addCustomToken.redux - -import android.webkit.ValueCallback -import com.tangem.blockchain.common.Blockchain -import com.tangem.common.extensions.guard -import com.tangem.datasource.api.tangemTech.models.CoinsResponse -import com.tangem.domain.AddCustomTokenError -import com.tangem.domain.AddCustomTokenError.Warning.* -import com.tangem.domain.DomainDialog -import com.tangem.domain.DomainWrapped -import com.tangem.domain.common.extensions.canHandleToken -import com.tangem.domain.common.extensions.fromNetworkId -import com.tangem.domain.common.extensions.supportedBlockchains -import com.tangem.domain.common.extensions.toNetworkId -import com.tangem.domain.common.form.* -import com.tangem.domain.common.util.cardTypesResolver -import com.tangem.domain.common.util.derivationStyleProvider -import com.tangem.domain.features.addCustomToken.* -import com.tangem.domain.features.addCustomToken.CustomTokenFieldId.* -import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenAction.* -import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenState.Companion.createInitialScreenState -import com.tangem.domain.redux.BaseStoreHub -import com.tangem.domain.redux.DomainState -import com.tangem.domain.redux.ReStoreReducer -import com.tangem.domain.redux.domainStore -import com.tangem.domain.redux.extensions.dispatchOnMain -import com.tangem.domain.redux.global.DomainGlobalAction -import com.tangem.domain.redux.global.DomainGlobalState -import com.tangem.utils.coroutines.AppCoroutineDispatcherProvider -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.delay -import kotlinx.coroutines.launch -import org.rekotlin.Action -import timber.log.Timber - -/** -[REDACTED_AUTHOR] - */ -@Suppress("LargeClass") -internal class AddCustomTokenHub : BaseStoreHub("AddCustomTokenHub") { - - private val hubState: AddCustomTokenState - get() = domainStore.state.addCustomTokensState - - override fun getReducer(): ReStoreReducer = AddCustomTokenReducer(globalState) - - override fun getHubState(storeState: DomainState): AddCustomTokenState = hubState - - override fun updateStoreState(storeState: DomainState, newHubState: AddCustomTokenState): DomainState { - return storeState.copy(addCustomTokensState = newHubState) - } - - @Suppress("ComplexMethod") - override suspend fun handleAction(action: Action, storeState: DomainState, cancel: ValueCallback) { - if (action !is AddCustomTokenAction) return - - when (action) { - is OnCreate -> { - hubState.appSavedCurrencies.guard { - return throwUnAppropriateInitialization("addedTokens") - } - } - is OnDestroy -> cancelAll() - is OnTokenContractAddressChanged -> { - validateContractAddressAndNotify(action.contractAddress.value) - } - is OnTokenNetworkChanged -> { - if (!action.blockchainNetwork.isUserInput) return - - validateContractAddressAndNotify(ContractAddress.getFieldValue()) - } - is OnTokenDerivationPathChanged -> { - updateAddButton() - } - is OnTokenNameChanged, is OnTokenSymbolChanged, is OnTokenDecimalsChanged -> { - updateAddButton() - } - is OnAddCustomTokenClicked -> { - val state = hubState - val completeData = when { - state.getCustomTokenType() == CustomTokenType.Token && state.networkIsSelected() -> { - state.gatherUserToken() - } - state.getCustomTokenType() == CustomTokenType.Blockchain && state.networkIsSelected() -> { - state.gatherBlockchain() - } - else -> null - } - - if (completeData == null) { - // normally it can't be, because the AddButton must be blocked - } else { - hubScope.launch(Dispatchers.Main) { - state.onTokenAddCallback?.invoke(completeData) - } - } - } - else -> Unit - } - } - - private suspend fun validateContractAddressAndNotify(contractAddress: String) { - val error = ContractAddress.validateValue(contractAddress) - if (Network.isFilled()) { - when (error) { - null -> { - // valid contract address - ContractAddress.removeError() - findTokenAndUpdateFields(contractAddress) - } - AddCustomTokenError.InvalidContractAddress -> { - ContractAddress.addError(error) - enableDisableTokenDetailFields(hubState.tokensAnyFieldsIsFilled()) - } - AddCustomTokenError.FieldIsEmpty -> { - ContractAddress.removeError() - clearTokenDetailsFields() - disableTokenDetailFields() - } - else -> {} - } - } else { - // is default selection (Blockchain.Unknown) - when (error) { - null -> { - // Blockchain.Unknown has always valid contract address - ContractAddress.removeError() - findTokenAndUpdateFields(contractAddress) - } - else -> { - ContractAddress.removeError() - clearTokenDetailsFields() - disableTokenDetailFields() - } - } - } - updateDerivationPath(Network.getFieldValue()) - updateWarnings() - updateAddButton() - } - - private suspend fun findTokenAndUpdateFields(contractAddress: String) { - val foundTokens = requestInfoAboutToken(contractAddress) - if (foundTokens.isEmpty()) { - // token not found - it's completely custom - dispatchOnMain(SetFoundTokenInfo(null)) - enableTokenDetailFields() - return - } - - // foundToken - contains all info about the token - val foundToken = foundTokens[0] - dispatchOnMain(SetFoundTokenInfo(foundToken)) - when { - foundToken.networks.isEmpty() -> { - Timber.e("Unexpected state -> throw to FB") - } - foundToken.networks.size == 1 -> { - // token with single contract address - val singleTokenContract = foundToken.networks[0] - fillTokenFields(foundToken, singleTokenContract) - disableTokenDetailFields() - } - else -> { - val dialog = DomainDialog.SelectTokenDialog( - items = foundToken.networks, - networkIdConverter = { networkId -> - val blockchain = Blockchain.fromNetworkId(networkId) - if (blockchain == null || blockchain == Blockchain.Unknown) { - throw AddCustomTokenError.SelectTokeNetworkError(networkId) - } - hubState.blockchainToName(blockchain) ?: "" - }, - onSelect = { selectedContract -> - hubScope.launch { - // find how to connect to the upper coroutineContext and dispatch through them - fillTokenFields(foundToken, selectedContract) - disableTokenDetailFields() - } - }, - ) - dispatchOnMain(DomainGlobalAction.ShowDialog(dialog)) - } - } - } - - private suspend fun updateDerivationPath(blockchainNetwork: Blockchain) { - val state = hubState - val derivationIsSupportedByNetwork = blockchainNetwork.isEvm() || blockchainNetwork == Blockchain.Unknown - - if (DerivationPath.isFilled() && !derivationIsSupportedByNetwork) { - // reset to default - val derivationField = DerivationPath.getField() - derivationField.data = derivationField.data.copy( - value = Blockchain.Unknown, - isUserInput = false, - ) - state.setField(derivationField) - dispatchOnMain(UpdateForm(hubState)) - } - - if (state.screenState.derivationPath.isEnabled != derivationIsSupportedByNetwork) { - val action = Screen.UpdateTokenFields( - listOf( - DerivationPath to state.screenState.derivationPath.copy( - isEnabled = derivationIsSupportedByNetwork, - ), - ), - ) - dispatchOnMain(action) - } - } - - private suspend fun updateWarnings() { - val state = hubState - val warningsAdd = mutableSetOf() - val warningsRemove = mutableSetOf() - - val tokenIsSupported = tokenIsSupported(Network.getFieldValue()) - val alreadyAdded = isPersistIntoAppSavedTokensList() - when (state.getCustomTokenType()) { - CustomTokenType.Blockchain -> { - warningsRemove.add(UnsupportedSolanaToken) - - if (alreadyAdded) warningsAdd.add(TokenAlreadyAdded) else warningsRemove.add(TokenAlreadyAdded) - - if (state.derivationPathIsSelected()) { - warningsAdd.add(PotentialScamToken) - } else { - warningsRemove.add(PotentialScamToken) - } - } - CustomTokenType.Token -> { - if (tokenIsSupported) { - warningsRemove.add(UnsupportedSolanaToken) - } else { - val validationResult = ContractAddress.validateValue(ContractAddress.getFieldValue()) - if (validationResult == AddCustomTokenError.FieldIsEmpty) { - warningsRemove.add(UnsupportedSolanaToken) - } else { - warningsAdd.add(UnsupportedSolanaToken) - } - } - - if (isPersistIntoAppSavedTokensList()) { - warningsAdd.add(TokenAlreadyAdded) - } else { - warningsRemove.add(TokenAlreadyAdded) - } - - if (state.foundToken == null) { - if (state.tokensAnyFieldsIsFilled()) { - warningsAdd.add(PotentialScamToken) - } else { - warningsRemove.add(PotentialScamToken) - } - } else { - if (state.foundToken.active) { - warningsRemove.add(PotentialScamToken) - } else { - warningsAdd.add(PotentialScamToken) - } - } - } - } - - dispatchOnMain( - Warning.Replace( - remove = warningsRemove, - add = warningsAdd, - ), - ) - } - - private suspend fun updateAddButton() { - if (isPersistIntoAppSavedTokensList()) { - TokenAlreadyAdded.add() - disableAddButton() - return - } else { - TokenAlreadyAdded.remove() - } - - val state = hubState - when { - // token - state.tokensFieldsIsFilled() && state.networkIsSelected() -> { - val error = ContractAddress.validateValue(ContractAddress.getFieldValue()) - val tokenIsSupported = tokenIsSupported(Network.getFieldValue()) - enableDisableAddButton(tokenIsSupported && error == null) - } - // token - state.tokensAnyFieldsIsFilled() -> { - disableAddButton() - } - // blockchain - else -> { - if (state.networkIsSelected()) { - if (isBlockchainPersistIntoAppSavedTokensList()) disableAddButton() else enableAddButton() - } else { - disableAddButton() - } - } - } - } - - private suspend fun requestInfoAboutToken(contractAddress: String): List { - val tangemTechServiceManager = requireNotNull(hubState.tangemTechServiceManager) - dispatchOnMain(Screen.UpdateTokenFields(listOf(ContractAddress to ViewStates.TokenField(isLoading = true)))) - - val field = hubState.getField(Network) - val selectedNetworkId: String? = field.data.value.let { - if (it == Blockchain.Unknown) null else it - }?.toNetworkId() - - // simulate loading effect. It would be better if the delay would only run if tokenManager.checkAddress() - // got the result faster than 500ms and the delay would only be the difference between them. - delay(timeMillis = 500) - - val result = tangemTechServiceManager.findToken(contractAddress, selectedNetworkId) - - dispatchOnMain(Screen.UpdateTokenFields(listOf(ContractAddress to ViewStates.TokenField(isLoading = false)))) - return result - } - - /** - * These are helper functions. - */ - private fun isPersistIntoAppSavedTokensList(): Boolean = when (hubState.getCustomTokenType()) { - CustomTokenType.Blockchain -> isBlockchainPersistIntoAppSavedTokensList() - CustomTokenType.Token -> isTokenPersistIntoAppSavedTokensList() - } - - private fun isTokenPersistIntoAppSavedTokensList(): Boolean { - val savedCurrencies = hubState.appSavedCurrencies ?: return false - - val tokenId = hubState.foundToken?.id - val tokenContractAddress = ContractAddress.getFieldValue() - val tokenNetworkId = Network.getFieldValue().toNetworkId() - val selectedDerivation = DerivationPath.getFieldValue() - - val derivationPath = getDerivationPathFromSelectedBlockchain(selectedDerivation) - savedCurrencies.forEach { wrappedCurrency -> - when (wrappedCurrency) { - is DomainWrapped.Currency.Blockchain -> Unit - is DomainWrapped.Currency.Token -> { - val sameId = tokenId == wrappedCurrency.token.id - val sameAddress = tokenContractAddress == wrappedCurrency.token.contractAddress - val sameBlockchain = Blockchain.fromNetworkId(tokenNetworkId) == wrappedCurrency.blockchain - val sameDerivationPath = derivationPath?.rawPath == wrappedCurrency.derivationPath - @Suppress("ComplexCondition") - if (sameId && sameAddress && sameBlockchain && sameDerivationPath) { - return true - } - } - } - } - return false - } - - private fun isBlockchainPersistIntoAppSavedTokensList(): Boolean { - val savedCurrencies = hubState.appSavedCurrencies ?: return false - val selectedNetwork = Network.getFieldValue() - val selectedDerivation = DerivationPath.getFieldValue() - val derivationPath = getDerivationPathFromSelectedBlockchain(selectedDerivation) - - savedCurrencies.forEach { wrappedCurrency -> - when (wrappedCurrency) { - is DomainWrapped.Currency.Blockchain -> { - val isSameBlockchain = selectedNetwork == wrappedCurrency.blockchain - val isSameDerivationPath = derivationPath?.rawPath == wrappedCurrency.derivationPath - if (isSameBlockchain && isSameDerivationPath) return true - } - - is DomainWrapped.Currency.Token -> Unit - } - } - return false - } - - private fun getDerivationPathFromSelectedBlockchain( - selectedDerivationBlockchain: Blockchain, - ): com.tangem.crypto.hdWallet.DerivationPath? = AddCustomTokenState.getDerivationPath( - mainNetwork = Network.getFieldValue(), - derivationNetwork = selectedDerivationBlockchain, - derivationStyle = hubState.cardDerivationStyle, - ) - - private suspend fun fillTokenFields(token: CoinsResponse.Coin, coinNetwork: CoinsResponse.Coin.Network) { - val blockchain = Blockchain.fromNetworkId(coinNetwork.networkId) ?: Blockchain.Unknown - Network.setFieldValue(Field.Data(blockchain, false)) - Name.setFieldValue(Field.Data(token.name, false)) - Symbol.setFieldValue(Field.Data(token.symbol, false)) - Decimals.setFieldValue(Field.Data(coinNetwork.decimalCount.toString(), false)) - dispatchOnMain(UpdateForm(hubState)) - } - - private suspend fun clearTokenDetailsFields() { - Name.setFieldValue(Field.Data("", false)) - Symbol.setFieldValue(Field.Data("", false)) - Decimals.setFieldValue(Field.Data("", false)) - dispatchOnMain(UpdateForm(hubState)) - } - - private suspend fun enableTokenDetailFields() { - enableDisableTokenDetailFields(true) - } - - private suspend fun disableTokenDetailFields() { - enableDisableTokenDetailFields(false) - } - - private suspend fun enableDisableTokenDetailFields(isEnabled: Boolean) { - val state = hubState - val action = Screen.UpdateTokenFields( - listOf( - Name to state.screenState.name.copy(isEnabled = isEnabled), - Symbol to state.screenState.symbol.copy(isEnabled = isEnabled), - Decimals to state.screenState.decimals.copy(isEnabled = isEnabled), - ), - ) - dispatchOnMain(action) - } - - private suspend fun enableAddButton() { - enableDisableAddButton(true) - } - - private suspend fun disableAddButton() { - enableDisableAddButton(false) - } - - private suspend fun enableDisableAddButton(isEnabled: Boolean) { - dispatchOnMain(Screen.UpdateAddButton(ViewStates.AddButton(isEnabled))) - } - - private fun tokenIsSupported(blockchain: Blockchain): Boolean = when (blockchain) { - Blockchain.Unknown -> true - else -> { - val scanResponse = globalState.scanResponse - scanResponse?.card?.canHandleToken( - blockchain = blockchain, - cardTypesResolver = scanResponse.cardTypesResolver, - ) ?: false - } - } - - @Throws - private fun throwUnAppropriateInitialization(objName: String) { - throw AddCustomTokenError.UnAppropriateInitialization( - "AddCustomTokenHub", - "$objName must be not NULL", - ) - } - - private suspend fun CustomTokenFieldId.addError(error: AddCustomTokenError) { - dispatchOnMain(FieldError.Add(this, error)) - } - - private suspend fun CustomTokenFieldId.removeError() { - dispatchOnMain(FieldError.Remove(this)) - } - - private inline fun CustomTokenFieldId.getField(): T { - val state = hubState - val value = when (this) { - ContractAddress -> state.getField(this) - Network -> state.getField(this) - Name -> state.getField(this) - Symbol -> state.getField(this) - Decimals -> state.getField(this) - DerivationPath -> state.getField(this) - } - return value as T - } - - private inline fun CustomTokenFieldId.getFieldValue(): T { - val value = when (this) { - ContractAddress -> getField().data.value - Network -> getField().data.value - Name -> getField().data.value - Symbol -> getField().data.value - Decimals -> getField().data.value - DerivationPath -> getField().data.value - } - return value as T - } - - private fun CustomTokenFieldId.setFieldValue(fieldData: Field.Data<*>) { - when (this) { - ContractAddress -> getField().data = fieldData as Field.Data - Network -> getField().data = fieldData as Field.Data - Name -> getField().data = fieldData as Field.Data - Symbol -> getField().data = fieldData as Field.Data - Decimals -> getField().data = fieldData as Field.Data - DerivationPath -> getField().data = fieldData as Field.Data - } - } - - private fun CustomTokenFieldId.validateValue(value: Any): AddCustomTokenError? { - return when (this) { - ContractAddress -> { - val contractAddressValidator: TokenContractAddressValidator = hubState.getValidator(ContractAddress) - contractAddressValidator.nextValidationFor(Network.getFieldValue()) - contractAddressValidator.validate(value as String) - } - Network, DerivationPath -> { - hubState.getValidator(Network).validate(value as Blockchain) - } - Name -> { - hubState.getValidator(Name).validate(value as String) - } - Symbol -> { - hubState.getValidator(Symbol).validate(value as String) - } - Decimals -> { - hubState.getValidator(Decimals).validate(value as String) - } - } - } - - private fun CustomTokenFieldId.isFilled(): Boolean { - return when (this) { - ContractAddress -> getFieldValue().isNotEmpty() - Network -> getFieldValue() != Blockchain.Unknown - Name -> getFieldValue().isNotEmpty() - Symbol -> getFieldValue().isNotEmpty() - Decimals -> getFieldValue().isNotEmpty() - DerivationPath -> getFieldValue() != Blockchain.Unknown - } - } - - private suspend fun AddCustomTokenError.Warning.add() { - dispatchOnMain(Warning.Add(setOf(this))) - } - - private suspend fun AddCustomTokenError.Warning.remove() { - dispatchOnMain(Warning.Remove(setOf(this))) - } -} - -@Suppress("ComplexMethod") -private class AddCustomTokenReducer( - private val globalState: DomainGlobalState, -) : ReStoreReducer { - - @Suppress("LongMethod") - override fun reduceAction(action: Action, state: AddCustomTokenState): AddCustomTokenState { - return when (action) { - is Init.SetAddedCurrencies -> { - state.copy(appSavedCurrencies = action.addedCurrencies) - } - is Init.SetOnAddTokenCallback -> { - state.copy(onTokenAddCallback = action.callback) - } - is OnCreate -> { - val scanResponse = requireNotNull(globalState.scanResponse) - val card = globalState.scanResponse.card - val supportedTokenNetworkIds = card.supportedBlockchains(scanResponse.cardTypesResolver) - .filter(Blockchain::canHandleTokens) - .map(Blockchain::toNetworkId) - - val tangemTechServiceManager = AddCustomTokenService( - tangemTechApi = globalState.networkServices.tangemTechService.api, - dispatchers = AppCoroutineDispatcherProvider(), - supportedTokenNetworkIds = supportedTokenNetworkIds, - ) - - state.copy( - cardDerivationStyle = globalState.scanResponse.derivationStyleProvider.getDerivationStyle(), - form = Form( - AddCustomTokenState.createFormFields( - cardTypesResolver = globalState.scanResponse.cardTypesResolver, - card = card, - type = CustomTokenType.Blockchain, - ), - ), - tangemTechServiceManager = tangemTechServiceManager, - screenState = createInitialScreenState(card.settings.isHDWalletAllowed), - ) - } - is OnDestroy -> { - val scanResponse = requireNotNull(globalState.scanResponse) - val card = scanResponse.card - state.reset(scanResponse.cardTypesResolver, card) - } - is UpdateForm -> { - updateFormState(action.state) - } - is OnTokenContractAddressChanged -> { - val field: TokenField = state.getField(ContractAddress) - field.data = action.contractAddress - updateFormState(state) - } - is OnTokenNetworkChanged -> { - val field: TokenBlockchainField = state.getField(Network) - field.data = action.blockchainNetwork - updateFormState(state) - } - is OnTokenNameChanged -> { - val field: TokenField = state.getField(Name) - field.data = action.tokenName - updateFormState(state) - } - is OnTokenSymbolChanged -> { - val field: TokenField = state.getField(Symbol) - field.data = action.tokenSymbol - updateFormState(state) - } - is OnTokenDecimalsChanged -> { - val field: TokenField = state.getField(Decimals) - field.data = action.tokenDecimals - updateFormState(state) - } - is OnTokenDerivationPathChanged -> { - val field: TokenDerivationPathField = state.getField(DerivationPath) - field.data = action.blockchainDerivationPath - updateFormState(state) - } - is FieldError.Add -> { - val newMap = state.formErrors.toMutableMap().apply { this[action.id] = action.error } - state.copy(formErrors = newMap) - } - is FieldError.Remove -> { - val newMap = state.formErrors.toMutableMap().apply { remove(action.id) } - state.copy(formErrors = newMap) - } - is SetFoundTokenInfo -> { - state.copy(foundToken = action.foundToken) - } - is Warning.Add -> { - val newList = state.warnings.toMutableSet().apply { addAll(action.warnings) } - state.copy(warnings = newList.toSet()) - } - is Warning.Remove -> { - val newList = state.warnings.toMutableSet().apply { removeAll(action.warnings) } - state.copy(warnings = newList.toSet()) - } - is Warning.Replace -> { - val newList = state.warnings.toMutableSet().apply { - removeAll(action.remove) - addAll(action.add) - } - state.copy(warnings = newList.toSet()) - } - is Screen.UpdateTokenFields -> { - var newScreenState = state.screenState - action.pairs.forEach { - newScreenState = when (it.first) { - ContractAddress -> { - if (state.screenState.contractAddressField == it.second) { - newScreenState - } else { - newScreenState.copy(contractAddressField = it.second) - } - } - Network -> { - if (state.screenState.network == it.second) { - newScreenState - } else { - newScreenState.copy(network = it.second) - } - } - Name -> { - if (state.screenState.name == it.second) { - newScreenState - } else { - newScreenState.copy(name = it.second) - } - } - Symbol -> { - if (state.screenState.symbol == it.second) { - newScreenState - } else { - newScreenState.copy(symbol = it.second) - } - } - Decimals -> { - if (state.screenState.decimals == it.second) { - newScreenState - } else { - newScreenState.copy(decimals = it.second) - } - } - DerivationPath -> { - if (state.screenState.derivationPath == it.second) { - newScreenState - } else { - newScreenState.copy(derivationPath = it.second) - } - } - else -> newScreenState - } - } - if (state.screenState == newScreenState) { - state - } else { - state.copy(screenState = newScreenState) - } - } - is Screen.UpdateAddButton -> { - val newScreenState = if (state.screenState.addButton == action.addButton) { - state.screenState - } else { - state.screenState.copy(addButton = action.addButton) - } - if (newScreenState == state.screenState) { - state - } else { - state.copy(screenState = newScreenState) - } - } - else -> state - } - } - - private fun updateFormState(state: AddCustomTokenState): AddCustomTokenState { - return state.copy(form = Form(state.form.fieldList)) - } -} \ No newline at end of file diff --git a/domain/legacy/src/main/java/com/tangem/domain/features/addCustomToken/redux/AddCustomTokenState.kt b/domain/legacy/src/main/java/com/tangem/domain/features/addCustomToken/redux/AddCustomTokenState.kt deleted file mode 100644 index 6d1b3fd2b0..0000000000 --- a/domain/legacy/src/main/java/com/tangem/domain/features/addCustomToken/redux/AddCustomTokenState.kt +++ /dev/null @@ -1,311 +0,0 @@ -package com.tangem.domain.features.addCustomToken.redux - -import com.tangem.blockchain.common.Blockchain -import com.tangem.blockchain.common.derivation.DerivationStyle -import com.tangem.common.json.MoshiJsonConverter -import com.tangem.datasource.api.tangemTech.models.CoinsResponse -import com.tangem.domain.AddCustomTokenError -import com.tangem.domain.DomainWrapped -import com.tangem.domain.common.CardTypesResolver -import com.tangem.domain.common.TapWorkarounds.isTestCard -import com.tangem.domain.common.extensions.isSupportedInApp -import com.tangem.domain.common.extensions.supportedBlockchains -import com.tangem.domain.common.extensions.supportedTokens -import com.tangem.domain.common.form.* -import com.tangem.domain.features.addCustomToken.* -import com.tangem.domain.features.addCustomToken.CustomTokenFieldId.* -import com.tangem.domain.models.scan.CardDTO -import com.tangem.domain.redux.DomainState -import com.tangem.domain.redux.state.StringActionStateConverter -import org.rekotlin.Action -import org.rekotlin.StateType - -data class AddCustomTokenState( - val appSavedCurrencies: List? = null, - val onTokenAddCallback: ((CustomCurrency) -> Unit)? = null, - val cardDerivationStyle: DerivationStyle? = null, - val form: Form = Form(listOf()), - val formValidators: Map> = createFormValidators(), - val formErrors: Map = emptyMap(), - val foundToken: CoinsResponse.Coin? = null, - val warnings: Set = emptySet(), - val screenState: ScreenState = createInitialScreenState(), - val tangemTechServiceManager: AddCustomTokenService? = null, -) : StateType { - - inline fun getField(id: FieldId): T = form.getField(id) as T - - fun setField(field: DataField<*>) { - form.setField(field) - } - - inline fun getValidator(id: FieldId): T = formValidators[id] as T - - fun getError(id: FieldId): AddCustomTokenError? = formErrors[id] - - inline fun visitDataConverter(converter: FieldDataConverter): T { - form.visitDataConverter(converter) - return converter.getConvertedData() - } - - fun blockchainToName(blockchain: Blockchain, isDerivationPath: Boolean = false): String? { - return when { - isDerivationPath -> blockchain.derivationPath(DerivationStyle.LEGACY)?.rawPath - else -> { - when (blockchain) { - Blockchain.Unknown -> null - else -> blockchain.fullName - } - } - } - } - - // except network - fun tokensFieldsIsFilled(): Boolean { - val idsToCheck = listOf(ContractAddress, Name, Symbol, Decimals) - val fieldsToCheck = form.fieldList.filter { idsToCheck.contains(it.id) } - val validator = StringIsNotEmptyValidator() - fieldsToCheck.forEach { field -> - val error = validator.validate(field.data.value?.toString()) - if (error != null) return false - } - return true - } - - // except network - fun tokensAnyFieldsIsFilled(): Boolean { - val idsToCheck = listOf(ContractAddress, Name, Symbol, Decimals) - val fieldsToCheck = form.fieldList.filter { idsToCheck.contains(it.id) } - val validator = StringIsEmptyValidator() - val errorsList = fieldsToCheck.mapNotNull { field -> - validator.validate(field.data.value?.toString()) - } - return errorsList.isNotEmpty() - } - - fun networkIsSelected(): Boolean { - val network = getField(Network) - return network.data.value != Blockchain.Unknown - } - - fun derivationPathIsSelected(): Boolean { - val network = getField(DerivationPath) - return network.data.value != Blockchain.Unknown - } - - fun getCustomTokenType(): CustomTokenType { - return if (tokensAnyFieldsIsFilled() || tokensFieldsIsFilled()) { - CustomTokenType.Token - } else { - CustomTokenType.Blockchain - } - } - - fun gatherUserToken(): CustomCurrency.CustomToken? = try { - getToken() - } catch (ex: Exception) { - null - } - - fun gatherBlockchain(): CustomCurrency.CustomBlockchain? = try { - getBlockchain() - } catch (ex: Exception) { - null - } - - fun reset(cardTypesResolver: CardTypesResolver, card: CardDTO): AddCustomTokenState { - return this.copy( - appSavedCurrencies = null, - onTokenAddCallback = null, - cardDerivationStyle = null, - form = Form(createFormFields(cardTypesResolver, card, CustomTokenType.Blockchain)), - formErrors = emptyMap(), - foundToken = null, - warnings = emptySet(), - screenState = createInitialScreenState(card.settings.isHDWalletAllowed), - tangemTechServiceManager = null, - ) - } - - private fun getToken(): CustomCurrency.CustomToken { - return CustomCurrency.CustomToken.Converter(foundToken?.id, cardDerivationStyle) - .apply { visitDataConverter(this) } - .getConvertedData() - } - - private fun getBlockchain(): CustomCurrency.CustomBlockchain { - return CustomCurrency.CustomBlockchain.Converter(cardDerivationStyle) - .apply { visitDataConverter(this) } - .getConvertedData() - } - - companion object { - - /** - * If an user select derivation path (derivationNetwork) as Blockchain.Unknown, - * then we should use a blockchain from the mainNetwork to determine a DerivationPath - */ - internal fun getDerivationPath( - mainNetwork: Blockchain, - derivationNetwork: Blockchain, - derivationStyle: DerivationStyle?, - ): com.tangem.crypto.hdWallet.DerivationPath? { - // If we allow user to select derivations, we need to provide different derivations - // (Legacy style derivations). - // But the mainNetwork derivation depends on whether a user has a card - // with legacy derivations or new style derivations. - val derivationStyleToUse = if (derivationNetwork == Blockchain.Unknown) { - derivationStyle - } else { - DerivationStyle.LEGACY - } - return when (derivationNetwork) { - Blockchain.Unknown -> mainNetwork - else -> derivationNetwork - }.derivationPath(derivationStyleToUse) - } - - internal fun createFormFields( - cardTypesResolver: CardTypesResolver, - card: CardDTO, - type: CustomTokenType, - ): List> { - return listOf( - TokenField(ContractAddress), - TokenBlockchainField(Network, getNetworksList(cardTypesResolver, card, type)), - TokenField(Name), - TokenField(Symbol), - TokenField(Decimals), - TokenDerivationPathField(DerivationPath, getSupportedDerivations(card)), - ) - } - - /** - * Serves to determine the networks (blockchains & tokens) that can be selected by Form.Networks. - * Blockchain.Unknown - is the default selection - */ - private fun getNetworksList( - cardTypesResolver: CardTypesResolver, - card: CardDTO, - type: CustomTokenType, - ): List { - val evmBlockchains = Blockchain.values() - .filter { it.isEvm() } - .filter { card.isTestCard == it.isTestnet() } - - val additionalBlockchains = listOf( - Blockchain.Binance, - Blockchain.BinanceTestnet, - Blockchain.Solana, - Blockchain.SolanaTestnet, - Blockchain.Tron, - Blockchain.TronTestnet, - ) - - val supportedByCard = when (type) { - CustomTokenType.Blockchain -> card.supportedBlockchains(cardTypesResolver) - CustomTokenType.Token -> card.supportedTokens(cardTypesResolver) - } - val typedNetworksList = (evmBlockchains + additionalBlockchains) - .filter { supportedByCard.contains(it) } - .toMutableList() - - val default = Blockchain.Unknown - typedNetworksList.add(0, default) - - return typedNetworksList.sortByName() - } - - private fun createFormValidators(): Map> { - return mapOf( - ContractAddress to TokenContractAddressValidator(), - Network to TokenNetworkValidator(), - Name to TokenNameValidator(), - Symbol to TokenSymbolValidator(), - Decimals to TokenDecimalsValidator(), - ) - } - - private fun getSupportedDerivations(card: CardDTO): List { - val evmBlockchains = Blockchain.values() - .filter { card.isTestCard == it.isTestnet() && it.isEvm() } - .filter { it.isSupportedInApp() } - - return (listOf(Blockchain.Unknown) + evmBlockchains).sortByName() - } - - internal fun createInitialScreenState(showDerivationPathField: Boolean = false): ScreenState { - return ScreenState( - contractAddressField = ViewStates.TokenField(), - network = ViewStates.TokenField(), - name = ViewStates.TokenField(isEnabled = false), - symbol = ViewStates.TokenField(isEnabled = false), - decimals = ViewStates.TokenField(isEnabled = false), - derivationPath = ViewStates.TokenField(isVisible = showDerivationPathField), - addButton = ViewStates.AddButton(isEnabled = false), - ) - } - } - - class Converter : StringActionStateConverter { - private val jsonConverter: MoshiJsonConverter = MoshiJsonConverter.INSTANCE - private var builder: StringBuilder = StringBuilder() - - override fun convert(action: Action, stateHolder: DomainState): String? { - if (action !is AddCustomTokenAction) return null - - val state = stateHolder.addCustomTokensState - val fieldConverter = - FieldToJsonConverter( - listOf( - ContractAddress, - Network, - Name, - Symbol, - Decimals, - DerivationPath, - ), - jsonConverter, - ) - state.visitDataConverter(fieldConverter) - val errors = state.formErrors.map { - "${it.key}: ${it.value::class.java.simpleName}" - } - val warnings = state.warnings.map { it::class.java.simpleName } - - printAction(action, state) - printStateValue("fields", fieldConverter.getConvertedData()) - printStateValue("fieldErrors", toJson(errors)) - printStateValue("warnings", toJson(warnings)) - printStateValue("screenState", toJson(state.screenState)) - printMessage("------------------------------------------------------") - - val printed = builder.toString() - builder = StringBuilder() - - return printed - } - - private fun printStateValue(name: String, value: String) { - printMessage("$name: $value") - } - - private fun printAction(action: AddCustomTokenAction, state: AddCustomTokenState) { - printMessage("action: $action, state: ${state::class.java.simpleName}") - } - - private fun toJson(value: Any): String { - return jsonConverter.prettyPrint(value) - } - - private fun printMessage(message: String) { - builder.append("$message\n") - } - } -} - -private fun List.sortByName(): List = this.sortedBy { it.fullName } - -enum class CustomTokenType { - Token, Blockchain -} \ No newline at end of file diff --git a/domain/legacy/src/main/java/com/tangem/domain/features/addCustomToken/redux/Models.kt b/domain/legacy/src/main/java/com/tangem/domain/features/addCustomToken/redux/Models.kt deleted file mode 100644 index 444255ef7f..0000000000 --- a/domain/legacy/src/main/java/com/tangem/domain/features/addCustomToken/redux/Models.kt +++ /dev/null @@ -1,27 +0,0 @@ -package com.tangem.domain.features.addCustomToken.redux - -/** -[REDACTED_AUTHOR] - */ -// describes state the screen, except the form fields -data class ScreenState( - val contractAddressField: ViewStates.TokenField, - val network: ViewStates.TokenField, - val name: ViewStates.TokenField, - val symbol: ViewStates.TokenField, - val decimals: ViewStates.TokenField, - val derivationPath: ViewStates.TokenField, - val addButton: ViewStates.AddButton, -) - -sealed class ViewStates { - data class TokenField( - val isLoading: Boolean = false, - val isEnabled: Boolean = true, - val isVisible: Boolean = true, - ) : ViewStates() - - data class AddButton( - val isEnabled: Boolean = true, - ) : ViewStates() -} \ No newline at end of file diff --git a/domain/legacy/src/main/java/com/tangem/domain/redux/DomainState.kt b/domain/legacy/src/main/java/com/tangem/domain/redux/DomainState.kt index 49cc235412..67be91d2dd 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/redux/DomainState.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/redux/DomainState.kt @@ -1,13 +1,9 @@ package com.tangem.domain.redux -import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenState import com.tangem.domain.redux.global.DomainGlobalState import org.rekotlin.StateType /** [REDACTED_AUTHOR] */ -data class DomainState( - val globalState: DomainGlobalState = DomainGlobalState(), - val addCustomTokensState: AddCustomTokenState = AddCustomTokenState(), -) : StateType \ No newline at end of file +data class DomainState(val globalState: DomainGlobalState = DomainGlobalState()) : StateType \ No newline at end of file diff --git a/domain/legacy/src/main/java/com/tangem/domain/redux/DomainStore.kt b/domain/legacy/src/main/java/com/tangem/domain/redux/DomainStore.kt index fbd4fe3735..a141cd5f9a 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/redux/DomainStore.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/redux/DomainStore.kt @@ -1,7 +1,5 @@ package com.tangem.domain.redux -import com.tangem.domain.DomainLayer -import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenHub import com.tangem.domain.redux.global.DomainGlobalHub import org.rekotlin.Action import org.rekotlin.Store @@ -9,10 +7,7 @@ import org.rekotlin.Store /** [REDACTED_AUTHOR] */ -private val RE_STORE_HUBS: List> = listOf( - DomainGlobalHub(), - AddCustomTokenHub(), -) +private val RE_STORE_HUBS: List> = listOf(DomainGlobalHub()) val domainStore = Store( state = DomainState(), @@ -37,7 +32,6 @@ private fun reduce(action: Action, domainState: DomainState?): DomainState { assembleReducedDomainState } } - DomainLayer.actionStateLogger.log(reducedStatesByAction) return assembleReducedDomainState } \ No newline at end of file diff --git a/domain/legacy/src/main/java/com/tangem/domain/redux/global/DomainGlobalAction.kt b/domain/legacy/src/main/java/com/tangem/domain/redux/global/DomainGlobalAction.kt index 2c9edb34a5..90bf41578e 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/redux/global/DomainGlobalAction.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/redux/global/DomainGlobalAction.kt @@ -1,6 +1,5 @@ package com.tangem.domain.redux.global -import com.tangem.domain.DomainDialog import com.tangem.domain.models.scan.ScanResponse import org.rekotlin.Action @@ -10,5 +9,4 @@ import org.rekotlin.Action // TODO: refactoring: is alias for the GlobalAction sealed class DomainGlobalAction : Action { data class SaveScanNoteResponse(val scanResponse: ScanResponse) : DomainGlobalAction() - data class ShowDialog(val stateDialog: DomainDialog?) : DomainGlobalAction() } \ No newline at end of file diff --git a/domain/legacy/src/main/java/com/tangem/domain/redux/global/DomainGlobalHub.kt b/domain/legacy/src/main/java/com/tangem/domain/redux/global/DomainGlobalHub.kt index f22a14aa64..e552550257 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/redux/global/DomainGlobalHub.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/redux/global/DomainGlobalHub.kt @@ -47,9 +47,6 @@ private class DomainGlobalReducer : ReStoreReducer { ) state.copy(scanResponse = action.scanResponse) } - is DomainGlobalAction.ShowDialog -> { - state.copy(dialog = action.stateDialog) - } else -> state } } diff --git a/domain/legacy/src/main/java/com/tangem/domain/redux/global/DomainGlobalState.kt b/domain/legacy/src/main/java/com/tangem/domain/redux/global/DomainGlobalState.kt index 36a97859c9..a424fbb683 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/redux/global/DomainGlobalState.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/redux/global/DomainGlobalState.kt @@ -2,7 +2,6 @@ package com.tangem.domain.redux.global import com.tangem.datasource.api.paymentology.PaymentologyApiService import com.tangem.datasource.api.tangemTech.TangemTechService -import com.tangem.domain.DomainDialog import com.tangem.domain.models.scan.ScanResponse /** @@ -14,7 +13,6 @@ data class DomainGlobalState( val scanResponse: ScanResponse? = null, // val networkServices: NetworkServices = NetworkServices(), - val dialog: DomainDialog? = null, ) data class NetworkServices( diff --git a/domain/legacy/src/main/java/com/tangem/domain/redux/state/StateConverter.kt b/domain/legacy/src/main/java/com/tangem/domain/redux/state/StateConverter.kt deleted file mode 100644 index 8e99e0f538..0000000000 --- a/domain/legacy/src/main/java/com/tangem/domain/redux/state/StateConverter.kt +++ /dev/null @@ -1,35 +0,0 @@ -package com.tangem.domain.redux.state - -import com.tangem.domain.redux.DomainState -import org.rekotlin.Action - -/** -[REDACTED_AUTHOR] - */ -interface StringStateConverter { - fun convert(stateHolder: StateHolder): String -} - -interface StringActionStateConverter { - fun convert(action: Action, stateHolder: StateHolder): String? -} - -class ActionStateConvertersFactory { - private val stateConverters = mutableMapOf, StringActionStateConverter>() - - fun addConverter(classOfAction: Class, converter: StringActionStateConverter) { - stateConverters[classOfAction] = converter - } - - fun getConverter(action: Action): StringActionStateConverter? { - val converter = stateConverters.firstNotNullOfOrNull { (classOfAction, converter) -> - if (classOfAction.isAssignableFrom(action::class.java)) { - converter - } else { - null - } - } ?: return null - - return converter - } -} \ No newline at end of file diff --git a/domain/legacy/src/main/java/com/tangem/domain/redux/state/StateLogger.kt b/domain/legacy/src/main/java/com/tangem/domain/redux/state/StateLogger.kt deleted file mode 100644 index 576d398ea8..0000000000 --- a/domain/legacy/src/main/java/com/tangem/domain/redux/state/StateLogger.kt +++ /dev/null @@ -1,35 +0,0 @@ -package com.tangem.domain.redux.state - -import com.tangem.domain.features.BuildConfig -import com.tangem.domain.redux.DomainState -import org.rekotlin.Action -import timber.log.Timber - -/** -[REDACTED_AUTHOR] - * Use it only in debug mode! - */ -internal interface ActionStateLogger { - fun log(reducedSates: List>) -} - -internal class ActionStateLoggerImpl : ActionStateLogger { - - val actionStateConvertersFactory = ActionStateConvertersFactory() - - override fun log(reducedSates: List>) { - if (!BuildConfig.LOG_ENABLED) return - - logStates(reducedSates) - } - - private fun logStates(reducedSates: List>) { - reducedSates.forEach { (action, domainState) -> - val messageToPrint = actionStateConvertersFactory.getConverter(action) - ?.convert(action, domainState) - ?: return@forEach - - Timber.d(messageToPrint) - } - } -} \ No newline at end of file diff --git a/domain/legacy/src/main/java/com/tangem/domain/redux/state/StringStateConverter.kt b/domain/legacy/src/main/java/com/tangem/domain/redux/state/StringStateConverter.kt new file mode 100644 index 0000000000..f0660f284f --- /dev/null +++ b/domain/legacy/src/main/java/com/tangem/domain/redux/state/StringStateConverter.kt @@ -0,0 +1,8 @@ +package com.tangem.domain.redux.state + +/** +[REDACTED_AUTHOR] + */ +interface StringStateConverter { + fun convert(stateHolder: StateHolder): String +} \ No newline at end of file From 75e2268d107e448f7e61a5b1b37174c24fbe67e3 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 4 Sep 2023 09:52:39 +0300 Subject: [PATCH 005/242] Updated on 2026-08-14 --- .../core/ui/extensions/CryptoCurrency.kt | 66 ++++---- .../com/tangem/core/ui/res/TangemTheme.kt | 2 + .../main/res/drawable/ic_custom_token_44.xml | 10 ++ .../TokenDetailsSkeletonStateConverter.kt | 8 +- .../presentation/common/WalletPreviewData.kt | 79 +++++---- .../common/component/TokenItem.kt | 9 +- .../common/component/token/TokenIcon.kt | 152 ------------------ .../component/token/icon/ContentIcon.kt | 129 +++++++++++++++ .../common/component/token/icon/IconBadge.kt | 57 +++++++ .../common/component/token/icon/TokenIcon.kt | 95 +++++++++++ .../common/state/TokenItemState.kt | 105 ++++++++---- .../CryptoCurrencyToIconStateConverter.kt | 48 ++++++ .../organizetokens/OrganizeTokensScreen.kt | 9 +- .../CryptoCurrencyToDraggableItemConverter.kt | 10 +- ...ryptoCurrencyStatusToTokenItemConverter.kt | 14 +- 15 files changed, 522 insertions(+), 271 deletions(-) create mode 100644 core/ui/src/main/res/drawable/ic_custom_token_44.xml delete mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/token/TokenIcon.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/token/icon/ContentIcon.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/token/icon/IconBadge.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/token/icon/TokenIcon.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/utils/CryptoCurrencyToIconStateConverter.kt diff --git a/core/ui/src/main/java/com/tangem/core/ui/extensions/CryptoCurrency.kt b/core/ui/src/main/java/com/tangem/core/ui/extensions/CryptoCurrency.kt index 7c32f55014..752b3cc492 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/extensions/CryptoCurrency.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/extensions/CryptoCurrency.kt @@ -1,45 +1,49 @@ package com.tangem.core.ui.extensions import androidx.annotation.DrawableRes -import com.tangem.core.ui.R +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.luminance +import androidx.core.graphics.toColorInt +import com.tangem.core.ui.res.TangemColorPalette import com.tangem.domain.tokens.models.CryptoCurrency +private const val LIGHT_LUMINANCE = 0.5f +private const val COLOR_HEX_START_INDEX = 2 +private const val COLOR_HEX_END_INDEX = 7 + /** - * Retrieves the resource ID for the network badge of a [CryptoCurrency]. + * Retrieves the resource ID for the network of a [CryptoCurrency]. * - * This property provides a way to fetch the appropriate drawable resource ID - * for the network badge of a given cryptocurrency. For coins, this will typically - * return null as they do not have network badges, while tokens will fetch the icon - * based on their associated network ID. - * - * @return Drawable resource ID for the network badge or null if the cryptocurrency is a coin. + * @return Drawable resource ID for the network. */ @get:DrawableRes -val CryptoCurrency.networkBadgeIconResId: Int? - get() = when (this) { - is CryptoCurrency.Coin -> null - is CryptoCurrency.Token -> getActiveIconRes(network.id.value) +val CryptoCurrency.networkIconResId: Int + get() = getActiveIconRes(network.id.value) + +/** + * Tries to extract a background color from the contract address of a token. + * + * @param fallbackColor The color to use as a fallback. + * @return The extracted background color or the fallback color if extraction fails or if it is a test network token. + */ +fun CryptoCurrency.Token.tryGetBackgroundForTokenIcon(fallbackColor: Color = TangemColorPalette.Black): Color { + if (network.isTestnet) return fallbackColor + + return try { + val colorHex = "#" + contractAddress.substring(range = COLOR_HEX_START_INDEX..COLOR_HEX_END_INDEX) + Color(colorHex.toColorInt()) + } catch (exception: Exception) { + fallbackColor } +} /** - * Retrieves the resource ID for the icon of a [CryptoCurrency]. + * Determines the tint color to be used for a token icon based on its background color. + * If the icon's background color is light, a dark tint is chosen; otherwise, a light tint is chosen. * - * This property provides a way to fetch the appropriate drawable resource ID - * for the icon of a given cryptocurrency. - * - * @return Drawable resource ID for the cryptocurrency icon. + * @param iconBackground The background color of the custom token icon. + * @return The tint color to be used for the icon. */ -@get:DrawableRes -val CryptoCurrency.iconResId: Int - get() = when (this) { - is CryptoCurrency.Coin -> { - val rawCoinId = id.rawCurrencyId - - if (rawCoinId != null) { - getActiveIconResByCoinId(rawCoinId, network.id.value) - } else { - R.drawable.ic_alert_24 - } - } - is CryptoCurrency.Token -> R.drawable.ic_alert_24 - } \ No newline at end of file +fun getTintForTokenIcon(iconBackground: Color): Color { + return if (iconBackground.luminance() > LIGHT_LUMINANCE) TangemColorPalette.Black else TangemColorPalette.White +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/res/TangemTheme.kt b/core/ui/src/main/java/com/tangem/core/ui/res/TangemTheme.kt index 139dc810c6..efe05116e7 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/res/TangemTheme.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/res/TangemTheme.kt @@ -88,6 +88,7 @@ private fun materialThemeColors(colors: TangemColors, isDark: Boolean): Colors { } @Composable +@ReadOnlyComposable private fun lightThemeColors(): TangemColors { return TangemColors( text = TangemColors.Text( @@ -135,6 +136,7 @@ private fun lightThemeColors(): TangemColors { } @Composable +@ReadOnlyComposable private fun darkThemeColors(): TangemColors { return TangemColors( text = TangemColors.Text( diff --git a/core/ui/src/main/res/drawable/ic_custom_token_44.xml b/core/ui/src/main/res/drawable/ic_custom_token_44.xml new file mode 100644 index 0000000000..b1d31ab0bd --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_custom_token_44.xml @@ -0,0 +1,10 @@ + + + + diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSkeletonStateConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSkeletonStateConverter.kt index 0bb96c3d93..29e967a070 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSkeletonStateConverter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSkeletonStateConverter.kt @@ -3,14 +3,10 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.state.factory import com.tangem.core.ui.components.marketprice.MarketPriceBlockState import com.tangem.core.ui.components.transactions.state.TxHistoryState import com.tangem.core.ui.extensions.TextReference -import com.tangem.core.ui.extensions.iconResId +import com.tangem.core.ui.extensions.networkIconResId import com.tangem.core.ui.res.TangemTheme import com.tangem.domain.tokens.models.CryptoCurrency import com.tangem.feature.tokendetails.presentation.tokendetails.state.* -import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsBalanceBlockState -import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState -import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsTopAppBarConfig -import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenInfoBlockState import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.TokenDetailsActionButton import com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.TokenDetailsSkeletonStateConverter.SkeletonModel import com.tangem.feature.tokendetails.presentation.tokendetails.viewmodels.TokenDetailsClickIntents @@ -38,7 +34,7 @@ internal class TokenDetailsSkeletonStateConverter( is CryptoCurrency.Token -> TokenInfoBlockState.Currency.Token( networkName = currency.network.standardType.name, blockchainName = currency.network.name, - networkIcon = currency.iconResId, + networkIcon = currency.networkIconResId, ) }, ), diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/WalletPreviewData.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/WalletPreviewData.kt index 60f3ec11ff..de4226ceca 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/WalletPreviewData.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/WalletPreviewData.kt @@ -8,13 +8,17 @@ import com.tangem.core.ui.components.transactions.state.TransactionState import com.tangem.core.ui.components.transactions.state.TxHistoryState import com.tangem.core.ui.event.consumed import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.res.TangemColorPalette import com.tangem.domain.wallets.models.UserWalletId import com.tangem.feature.wallet.presentation.common.state.TokenItemState import com.tangem.feature.wallet.presentation.common.state.TokenItemState.TokenOptionsState import com.tangem.feature.wallet.presentation.organizetokens.model.DraggableItem import com.tangem.feature.wallet.presentation.organizetokens.model.OrganizeTokensListState import com.tangem.feature.wallet.presentation.organizetokens.model.OrganizeTokensState -import com.tangem.feature.wallet.presentation.wallet.state.* +import com.tangem.feature.wallet.presentation.wallet.state.ActionsBottomSheetConfig +import com.tangem.feature.wallet.presentation.wallet.state.TokenActionButtonConfig +import com.tangem.feature.wallet.presentation.wallet.state.WalletMultiCurrencyState +import com.tangem.feature.wallet.presentation.wallet.state.WalletSingleCurrencyState import com.tangem.feature.wallet.presentation.wallet.state.components.* import com.tangem.feature.wallet.presentation.wallet.state.components.WalletTokensListState.TokensListItemState import kotlinx.collections.immutable.persistentListOf @@ -87,12 +91,34 @@ internal object WalletPreviewData { ) } + private val coinIconState + get() = TokenItemState.IconState.CoinIcon( + url = null, + fallbackResId = R.drawable.img_polygon_22, + isGrayscale = false, + ) + + private val tokenIconState + get() = TokenItemState.IconState.TokenIcon( + url = null, + networkBadgeIconResId = R.drawable.img_polygon_22, + fallbackTint = TangemColorPalette.Black, + fallbackBackground = TangemColorPalette.Meadow, + isGrayscale = false, + ) + + private val customTokenIconState + get() = TokenItemState.IconState.CustomTokenIcon( + tint = TangemColorPalette.Black, + background = TangemColorPalette.Meadow, + networkBadgeIconResId = R.drawable.img_polygon_22, + isGrayscale = false, + ) + val tokenItemVisibleState by lazy { TokenItemState.Content( id = UUID.randomUUID().toString(), - tokenIconUrl = null, - tokenIconResId = R.drawable.img_polygon_22, - networkBadgeIconResId = R.drawable.img_polygon_22, + icon = coinIconState, name = "Polygon", amount = "5,412 MATIC", hasPending = true, @@ -103,7 +129,6 @@ internal object WalletPreviewData { type = PriceChangeConfig.Type.UP, ), ), - isTestnet = false, onItemClick = {}, onItemLongClick = {}, ) @@ -112,16 +137,14 @@ internal object WalletPreviewData { val testnetTokenItemVisibleState by lazy { tokenItemVisibleState.copy( name = "Polygon testnet", - isTestnet = true, + icon = tokenIconState.copy(isGrayscale = true), ) } val tokenItemHiddenState by lazy { TokenItemState.Content( id = UUID.randomUUID().toString(), - tokenIconUrl = null, - tokenIconResId = R.drawable.img_polygon_22, - networkBadgeIconResId = R.drawable.img_polygon_22, + icon = tokenIconState, name = "Polygon", amount = "5,412 MATIC", hasPending = true, @@ -131,7 +154,6 @@ internal object WalletPreviewData { type = PriceChangeConfig.Type.UP, ), ), - isTestnet = false, onItemClick = {}, onItemLongClick = {}, ) @@ -140,11 +162,8 @@ internal object WalletPreviewData { val tokenItemDragState by lazy { TokenItemState.Draggable( id = UUID.randomUUID().toString(), - tokenIconUrl = null, - tokenIconResId = R.drawable.img_polygon_22, - networkBadgeIconResId = R.drawable.img_polygon_22, + icon = tokenIconState, name = "Polygon", - isTestnet = false, fiatAmount = "3 172,14 $", ) } @@ -152,13 +171,28 @@ internal object WalletPreviewData { val tokenItemUnreachableState by lazy { TokenItemState.Unreachable( id = UUID.randomUUID().toString(), - tokenIconUrl = null, - tokenIconResId = R.drawable.img_polygon_22, - networkBadgeIconResId = R.drawable.img_polygon_22, + icon = tokenIconState, name = "Polygon", ) } + val customTokenItemVisibleState by lazy { + tokenItemVisibleState.copy( + name = "Polygon custom", + icon = customTokenIconState.copy( + tint = TangemColorPalette.White, + background = TangemColorPalette.Black, + ), + ) + } + + val customTestnetTokenItemVisibleState by lazy { + tokenItemVisibleState.copy( + name = "Polygon custom testnet", + icon = customTokenIconState.copy(isGrayscale = true), + ) + } + val loadingTokenItemState by lazy { TokenItemState.Loading(id = "Loading#1") } private const val networksSize = 10 @@ -188,7 +222,6 @@ internal object WalletPreviewData { tokenItemState = tokenItemDragState.copy( id = "${group.id}_token_$tokenNumber", name = "Token $tokenNumber from $networkNumber network", - networkBadgeIconResId = R.drawable.img_eth_22.takeIf { i != 0 }, ), groupId = group.id, roundingMode = when { @@ -299,8 +332,6 @@ internal object WalletPreviewData { tokenItemVisibleState.copy( id = "token_1", name = "Ethereum", - tokenIconResId = R.drawable.img_eth_22, - networkBadgeIconResId = null, amount = "1,89340821 ETH", ), ), @@ -308,8 +339,6 @@ internal object WalletPreviewData { tokenItemVisibleState.copy( id = "token_2", name = "Ethereum", - tokenIconResId = R.drawable.img_eth_22, - networkBadgeIconResId = null, amount = "1,89340821 ETH", ), ), @@ -317,8 +346,6 @@ internal object WalletPreviewData { tokenItemVisibleState.copy( id = "token_3", name = "Ethereum", - tokenIconResId = R.drawable.img_eth_22, - networkBadgeIconResId = null, amount = "1,89340821 ETH", ), ), @@ -326,8 +353,6 @@ internal object WalletPreviewData { tokenItemVisibleState.copy( id = "token_4", name = "Ethereum", - tokenIconResId = R.drawable.img_eth_22, - networkBadgeIconResId = null, amount = "1,89340821 ETH", ), ), @@ -336,8 +361,6 @@ internal object WalletPreviewData { tokenItemVisibleState.copy( id = "token_5", name = "Ethereum", - tokenIconResId = R.drawable.img_eth_22, - networkBadgeIconResId = null, amount = "1,89340821 ETH", ), ), diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/TokenItem.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/TokenItem.kt index 6169aa9a8c..18cc14cbe1 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/TokenItem.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/TokenItem.kt @@ -17,16 +17,14 @@ import androidx.constraintlayout.compose.ConstrainedLayoutReference import androidx.constraintlayout.compose.ConstraintLayout import androidx.constraintlayout.compose.ConstraintLayoutScope import androidx.constraintlayout.compose.Dimension -import com.tangem.core.ui.components.* import com.tangem.core.ui.res.TangemTheme import com.tangem.feature.wallet.presentation.common.WalletPreviewData import com.tangem.feature.wallet.presentation.common.component.token.TokenCryptoInfoBlock import com.tangem.feature.wallet.presentation.common.component.token.TokenFiatInfoBlock -import com.tangem.feature.wallet.presentation.common.component.token.TokenIcon +import com.tangem.feature.wallet.presentation.common.component.token.icon.TokenIcon import com.tangem.feature.wallet.presentation.common.state.TokenItemState import org.burnoutcrew.reorderable.ReorderableLazyListState -// TODO: Add custom token state: [REDACTED_JIRA] @OptIn(ExperimentalFoundationApi::class) @Composable internal fun TokenItem( @@ -90,7 +88,7 @@ private inline fun BaseContainer( ) { ConstraintLayout( modifier = Modifier - .fillMaxSize() + .fillMaxWidth() .padding( horizontal = TangemTheme.dimens.spacing14, vertical = TangemTheme.dimens.spacing14, @@ -111,7 +109,6 @@ private fun Modifier.constrainAsOptionsItem(scope: ConstraintLayoutScope, ref: C } // region preview - @Preview @Composable private fun Preview_Tokens_LightTheme(@PreviewParameter(TokenConfigProvider::class) state: TokenItemState) { @@ -136,6 +133,8 @@ private class TokenConfigProvider : CollectionPreviewParameterProvider ContentIcon(content = state, modifier = modifier) - is TokenItemState.Loading -> LoadingIcon(modifier = modifier) - is TokenItemState.Locked -> LockedIcon(modifier = modifier) - } -} - -@Composable -private fun ContentIcon(content: TokenItemState.ContentState, modifier: Modifier = Modifier) { - BaseContainer(modifier = modifier) { - val isTestnet = when (content) { - is TokenItemState.Content -> content.isTestnet - is TokenItemState.Draggable -> content.isTestnet - is TokenItemState.Unreachable -> false - } - - val colorFilter = remember(isTestnet) { - if (isTestnet) { - ColorFilter.colorMatrix( - colorMatrix = ColorMatrix().apply { setToSaturation(GRAY_SCALE_SATURATION) }, - ) - } else { - null - } - } - - Icon( - content = content, - colorFilter = colorFilter, - modifier = Modifier.align(Alignment.BottomStart), - ) - - NetworkBadge( - iconResId = content.networkBadgeIconResId, - colorFilter = colorFilter, - modifier = Modifier.align(Alignment.TopEnd), - ) - } -} - -@Composable -private fun Icon(content: TokenItemState.ContentState, colorFilter: ColorFilter?, modifier: Modifier = Modifier) { - val iconUrl = content.tokenIconUrl - val iconData: Any = remember(iconUrl) { - if (iconUrl.isNullOrEmpty()) content.tokenIconResId else iconUrl - } - - SubcomposeAsyncImage( - modifier = modifier.iconSize(), - model = ImageRequest.Builder(context = LocalContext.current) - .data(data = iconData) - .placeholder(drawableResId = content.tokenIconResId) - .error(drawableResId = content.tokenIconResId) - .fallback(drawableResId = content.tokenIconResId) - .crossfade(enable = true) - .build(), - colorFilter = colorFilter, - contentDescription = null, - ) -} - -@Composable -private fun BoxScope.NetworkBadge( - @DrawableRes iconResId: Int?, - colorFilter: ColorFilter?, - modifier: Modifier = Modifier, -) { - AnimatedVisibility( - visible = iconResId != null, - modifier = modifier - .size(TangemTheme.dimens.size18) - .background(color = TangemTheme.colors.background.primary, shape = CircleShape), - ) { - if (iconResId == null) return@AnimatedVisibility - - Image( - modifier = Modifier - .padding(all = TangemTheme.dimens.spacing2) - .align(Alignment.Center), - painter = painterResource(id = iconResId), - colorFilter = colorFilter, - contentDescription = null, - ) - } -} - -@Composable -private fun LoadingIcon(modifier: Modifier = Modifier) { - BaseContainer(modifier) { - CircleShimmer( - modifier = Modifier - .iconSize() - .align(alignment = Alignment.BottomStart), - ) - } -} - -@Composable -private fun LockedIcon(modifier: Modifier = Modifier) { - BaseContainer(modifier) { - Box( - modifier = Modifier - .iconSize() - .align(Alignment.BottomStart), - ) { - Box( - modifier = Modifier - .matchParentSize() - .background(color = TangemTheme.colors.background.secondary, shape = CircleShape), - ) - } - } -} - -@Composable -private inline fun BaseContainer(modifier: Modifier = Modifier, content: @Composable BoxScope.() -> Unit) { - Box(modifier = modifier.size(size = TangemTheme.dimens.size40), content = content) -} - -private fun Modifier.iconSize(): Modifier = composed { - return@composed this.size(size = TangemTheme.dimens.size36) -} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/token/icon/ContentIcon.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/token/icon/ContentIcon.kt new file mode 100644 index 0000000000..c51cb1e957 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/token/icon/ContentIcon.kt @@ -0,0 +1,129 @@ +package com.tangem.feature.wallet.presentation.common.component.token.icon + +import androidx.annotation.DrawableRes +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.Icon +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import coil.compose.SubcomposeAsyncImage +import coil.request.ImageRequest +import com.tangem.feature.wallet.impl.R +import com.tangem.feature.wallet.presentation.common.state.TokenItemState + +@Composable +internal fun ContentIcon(icon: TokenItemState.IconState, colorFilter: ColorFilter?, modifier: Modifier = Modifier) { + when (icon) { + is TokenItemState.IconState.CoinIcon -> CoinIcon( + modifier = modifier, + url = icon.url, + fallbackResId = icon.fallbackResId, + colorFilter = colorFilter, + ) + is TokenItemState.IconState.TokenIcon -> TokenIcon( + modifier = modifier, + url = icon.url, + colorFilter = colorFilter, + errorIcon = { + CustomTokenIcon( + modifier = modifier, + tint = icon.fallbackTint, + background = icon.fallbackBackground, + ) + }, + ) + is TokenItemState.IconState.CustomTokenIcon -> CustomTokenIcon( + modifier = modifier, + tint = icon.tint, + background = icon.background, + ) + } +} + +@Composable +private fun CoinIcon( + url: String?, + @DrawableRes fallbackResId: Int, + colorFilter: ColorFilter?, + modifier: Modifier = Modifier, +) { + val iconData: Any = if (url.isNullOrBlank()) fallbackResId else url + + DefaultCurrencyIcon( + modifier = modifier, + iconData = iconData, + errorIcon = { + Image( + painter = painterResource(id = fallbackResId), + colorFilter = colorFilter, + contentDescription = null, + ) + }, + colorFilter = colorFilter, + ) +} + +@Composable +private fun TokenIcon( + url: String?, + colorFilter: ColorFilter?, + errorIcon: @Composable () -> Unit, + modifier: Modifier = Modifier, +) { + if (url == null) { + errorIcon() + } else { + DefaultCurrencyIcon( + modifier = modifier, + iconData = url, + errorIcon = errorIcon, + colorFilter = colorFilter, + ) + } +} + +@Composable +private fun CustomTokenIcon(tint: Color, background: Color, modifier: Modifier = Modifier) { + Box( + modifier = modifier + .background( + color = background, + shape = CircleShape, + ), + contentAlignment = Alignment.Center, + ) { + Icon( + modifier = Modifier.matchParentSize(), + painter = painterResource(id = R.drawable.ic_custom_token_44), + tint = tint, + contentDescription = null, + ) + } +} + +@Composable +private inline fun DefaultCurrencyIcon( + iconData: Any, + colorFilter: ColorFilter?, + crossinline errorIcon: @Composable () -> Unit, + modifier: Modifier = Modifier, +) { + SubcomposeAsyncImage( + modifier = modifier, + model = ImageRequest.Builder(context = LocalContext.current) + .data(iconData) + .crossfade(enable = true) + .build(), + loading = { LoadingIcon() }, + error = { errorIcon() }, + colorFilter = colorFilter, + contentDescription = null, + ) +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/token/icon/IconBadge.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/token/icon/IconBadge.kt new file mode 100644 index 0000000000..8bc2dbb218 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/token/icon/IconBadge.kt @@ -0,0 +1,57 @@ +package com.tangem.feature.wallet.presentation.common.component.token.icon + +import androidx.annotation.DrawableRes +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.res.painterResource +import com.tangem.core.ui.res.TangemTheme + +@Composable +internal fun NetworkBadge(@DrawableRes iconResId: Int, colorFilter: ColorFilter?, modifier: Modifier = Modifier) { + Box( + modifier = modifier + .size(TangemTheme.dimens.size18) + .background( + color = TangemTheme.colors.background.primary, + shape = CircleShape, + ), + ) { + Image( + modifier = Modifier + .padding(all = TangemTheme.dimens.spacing2) + .matchParentSize(), + painter = painterResource(id = iconResId), + colorFilter = colorFilter, + contentDescription = null, + ) + } +} + +@Composable +internal fun CustomBadge(modifier: Modifier = Modifier) { + Box( + modifier = modifier + .size(TangemTheme.dimens.size12) + .background( + color = TangemTheme.colors.background.primary, + shape = CircleShape, + ), + ) { + Box( + modifier = Modifier + .padding(all = TangemTheme.dimens.spacing2) + .matchParentSize() + .background( + color = TangemTheme.colors.icon.informative, + shape = CircleShape, + ), + ) + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/token/icon/TokenIcon.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/token/icon/TokenIcon.kt new file mode 100644 index 0000000000..c227566471 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/token/icon/TokenIcon.kt @@ -0,0 +1,95 @@ +package com.tangem.feature.wallet.presentation.common.component.token.icon + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxScope +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.graphics.ColorMatrix +import com.tangem.core.ui.components.CircleShimmer +import com.tangem.core.ui.res.TangemTheme +import com.tangem.feature.wallet.presentation.common.state.TokenItemState + +private const val GRAY_SCALE_SATURATION = 0f + +@Composable +internal fun TokenIcon(state: TokenItemState, modifier: Modifier = Modifier) { + BaseContainer(modifier = modifier) { + val iconModifier = Modifier + .align(Alignment.Center) + .size(TangemTheme.dimens.size36) + + when (state) { + is TokenItemState.Loading -> LoadingIcon(modifier = iconModifier) + is TokenItemState.Locked -> LockedIcon(modifier = iconModifier) + is TokenItemState.ContentState -> ContentIconContainer( + modifier = iconModifier, + icon = state.icon, + ) + } + } +} + +@Composable +internal fun LoadingIcon(modifier: Modifier = Modifier) { + CircleShimmer(modifier = modifier) +} + +@Composable +private fun LockedIcon(modifier: Modifier = Modifier) { + Box(modifier = modifier) { + Box( + modifier = Modifier + .matchParentSize() + .background( + color = TangemTheme.colors.background.secondary, + shape = CircleShape, + ), + ) + } +} + +@Composable +private fun BoxScope.ContentIconContainer(icon: TokenItemState.IconState, modifier: Modifier = Modifier) { + val networkBadgeOffset = TangemTheme.dimens.spacing4 + val colorFilter = remember(icon.isGrayscale) { + if (icon.isGrayscale) { + ColorFilter.colorMatrix( + colorMatrix = ColorMatrix().apply { setToSaturation(GRAY_SCALE_SATURATION) }, + ) + } else { + null + } + } + + ContentIcon( + modifier = modifier, + icon = icon, + colorFilter = colorFilter, + ) + + if (icon.networkBadgeIconResId != null) { + NetworkBadge( + modifier = Modifier + .offset(x = networkBadgeOffset, y = -networkBadgeOffset) + .align(Alignment.TopEnd), + iconResId = requireNotNull(icon.networkBadgeIconResId), + colorFilter = colorFilter, + ) + } + + if (icon is TokenItemState.IconState.CustomTokenIcon) { + CustomBadge(modifier = Modifier.align(Alignment.BottomEnd)) + } +} + +@Composable +private inline fun BaseContainer(modifier: Modifier = Modifier, content: @Composable BoxScope.() -> Unit) { + Box(modifier = modifier.size(size = TangemTheme.dimens.size40), content = content) +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/state/TokenItemState.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/state/TokenItemState.kt index 8310f134d5..16e58f8a87 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/state/TokenItemState.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/state/TokenItemState.kt @@ -2,6 +2,7 @@ package com.tangem.feature.wallet.presentation.common.state import androidx.annotation.DrawableRes import androidx.compose.runtime.Immutable +import androidx.compose.ui.graphics.Color import com.tangem.core.ui.components.marketprice.PriceChangeConfig /** Token item state */ @@ -18,80 +19,120 @@ internal sealed interface TokenItemState { data class Locked(override val id: String) : TokenItemState /** Content state */ - sealed class ContentState( - override val id: String, - open val tokenIconUrl: String?, - @DrawableRes open val tokenIconResId: Int, - @DrawableRes open val networkBadgeIconResId: Int?, - open val name: String, - ) : TokenItemState + sealed class ContentState : TokenItemState { + + abstract val icon: IconState + abstract val name: String + } /** * Content token state * * @property id unique id - * @property tokenIconUrl token icon url - * @property tokenIconResId token icon resource id - * @property networkBadgeIconResId network badge icon resource id, may be null if it is a coin + * @property icon token icon state * @property name token name * @property amount amount of token * @property hasPending pending tx in blockchain * @property tokenOptions state for token options - * @property isTestnet indicates whether the token is from test network or not * @property onItemClick callback which will be called when an item is clicked * @property onItemLongClick callback which will be called when an item is long clicked */ data class Content( override val id: String, - override val tokenIconUrl: String?, - @DrawableRes override val tokenIconResId: Int, - @DrawableRes override val networkBadgeIconResId: Int?, + override val icon: IconState, override val name: String, val amount: String, val hasPending: Boolean, val tokenOptions: TokenOptionsState, - val isTestnet: Boolean, val onItemClick: () -> Unit, val onItemLongClick: () -> Unit, - ) : ContentState(id, tokenIconUrl, tokenIconResId, networkBadgeIconResId, name) + ) : ContentState() /** * Draggable token state * * @property id unique id - * @property tokenIconUrl token icon url - * @property tokenIconResId token icon resource id - * @property networkBadgeIconResId network badge icon resource id, may be null if it is a coin + * @property icon token icon state * @property name token name * @property fiatAmount fiat amount of token - * @property isTestnet indicates whether the token is from test network or not */ data class Draggable( override val id: String, - override val tokenIconUrl: String?, - @DrawableRes override val tokenIconResId: Int, - @DrawableRes override val networkBadgeIconResId: Int?, + override val icon: IconState, override val name: String, val fiatAmount: String, - val isTestnet: Boolean, - ) : ContentState(id, tokenIconUrl, tokenIconResId, networkBadgeIconResId, name) + ) : ContentState() /** * Unreachable token state * * @property id token id - * @property tokenIconUrl token icon url - * @property tokenIconResId token icon resource id - * @property networkBadgeIconResId network badge icon resource id, may be null if it is a coin + * @property icon token icon state * @property name token name */ data class Unreachable( override val id: String, - override val tokenIconUrl: String?, - @DrawableRes override val tokenIconResId: Int, - @DrawableRes override val networkBadgeIconResId: Int?, + override val icon: IconState, override val name: String, - ) : ContentState(id, tokenIconUrl, tokenIconResId, networkBadgeIconResId, name) + ) : ContentState() + + /** + * Represents the various states an icon can be in. + */ + @Immutable + sealed class IconState { + + abstract val networkBadgeIconResId: Int? + abstract val isGrayscale: Boolean + + /** + * Represents a coin icon. + * + * @property url The URL where the coin icon can be fetched from. May be `null` if not found. + * @property fallbackResId The drawable resource ID to be used as a fallback if the URL is not available. + * @property isGrayscale Specifies whether to show the icon in grayscale. + */ + data class CoinIcon( + val url: String?, + @DrawableRes val fallbackResId: Int, + override val isGrayscale: Boolean, + ) : IconState() { + + override val networkBadgeIconResId: Int? = null + } + + /** + * Represents a token icon. + * + * @property url The URL where the token icon can be fetched from. May be `null` if not found. + * @property networkBadgeIconResId The drawable resource ID for the network badge. + * @property isGrayscale Specifies whether to show the icon in grayscale. + * @property fallbackTint The color to be used for tinting the fallback icon. + * @property fallbackBackground The background color to be used for the fallback icon. + */ + data class TokenIcon( + val url: String?, + @DrawableRes override val networkBadgeIconResId: Int, + override val isGrayscale: Boolean, + val fallbackTint: Color, + val fallbackBackground: Color, + ) : IconState() + + /** + * Represents a custom token icon. + * + * @property tint The color to be used for tinting the icon. + * @property background The background color to be used for the icon. + * @property networkBadgeIconResId The drawable resource ID for the network badge. + * @property isGrayscale Specifies whether to show the icon in grayscale. + */ + data class CustomTokenIcon( + val tint: Color, + val background: Color, + @DrawableRes override val networkBadgeIconResId: Int, + override val isGrayscale: Boolean, + ) : IconState() + } /** Token options state */ @Immutable diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/utils/CryptoCurrencyToIconStateConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/utils/CryptoCurrencyToIconStateConverter.kt new file mode 100644 index 0000000000..6f8c7c9b05 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/utils/CryptoCurrencyToIconStateConverter.kt @@ -0,0 +1,48 @@ +package com.tangem.feature.wallet.presentation.common.utils + +import com.tangem.core.ui.extensions.getTintForTokenIcon +import com.tangem.core.ui.extensions.networkIconResId +import com.tangem.core.ui.extensions.tryGetBackgroundForTokenIcon +import com.tangem.domain.tokens.models.CryptoCurrency +import com.tangem.feature.wallet.presentation.common.state.TokenItemState +import com.tangem.utils.converter.Converter + +internal class CryptoCurrencyToIconStateConverter : Converter { + + override fun convert(value: CryptoCurrency): TokenItemState.IconState { + return when (value) { + is CryptoCurrency.Coin -> getIconStateForCoin(value) + is CryptoCurrency.Token -> getIconStateForToken(value) + } + } + + private fun getIconStateForCoin(coin: CryptoCurrency.Coin): TokenItemState.IconState.CoinIcon { + return TokenItemState.IconState.CoinIcon( + url = coin.iconUrl, + fallbackResId = coin.networkIconResId, + isGrayscale = coin.network.isTestnet, + ) + } + + private fun getIconStateForToken(token: CryptoCurrency.Token): TokenItemState.IconState { + val background = token.tryGetBackgroundForTokenIcon() + val tint = getTintForTokenIcon(background) + + return if (token.isCustom) { + TokenItemState.IconState.CustomTokenIcon( + tint = tint, + background = background, + networkBadgeIconResId = token.networkIconResId, + isGrayscale = token.network.isTestnet, + ) + } else { + TokenItemState.IconState.TokenIcon( + url = token.iconUrl, + networkBadgeIconResId = token.networkIconResId, + isGrayscale = token.network.isTestnet, + fallbackTint = tint, + fallbackBackground = background, + ) + } + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensScreen.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensScreen.kt index 238ff49a18..4cd2ee53d0 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensScreen.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensScreen.kt @@ -1,7 +1,9 @@ package com.tangem.feature.wallet.presentation.organizetokens import androidx.activity.compose.BackHandler -import androidx.compose.animation.core.* +import androidx.compose.animation.core.LinearOutSlowInEasing +import androidx.compose.animation.core.animateDpAsState +import androidx.compose.animation.core.tween import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background import androidx.compose.foundation.layout.* @@ -37,7 +39,10 @@ import com.tangem.feature.wallet.presentation.common.component.TokenItem import com.tangem.feature.wallet.presentation.organizetokens.model.DraggableItem import com.tangem.feature.wallet.presentation.organizetokens.model.OrganizeTokensListState import com.tangem.feature.wallet.presentation.organizetokens.model.OrganizeTokensState -import org.burnoutcrew.reorderable.* +import org.burnoutcrew.reorderable.ReorderableItem +import org.burnoutcrew.reorderable.ReorderableLazyListState +import org.burnoutcrew.reorderable.rememberReorderableLazyListState +import org.burnoutcrew.reorderable.reorderable @Composable internal fun OrganizeTokensScreen(state: OrganizeTokensState, modifier: Modifier = Modifier) { diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/items/CryptoCurrencyToDraggableItemConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/items/CryptoCurrencyToDraggableItemConverter.kt index 11f4747c2b..9bad7941b4 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/items/CryptoCurrencyToDraggableItemConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/items/CryptoCurrencyToDraggableItemConverter.kt @@ -1,12 +1,11 @@ package com.tangem.feature.wallet.presentation.organizetokens.utils.converter.items import com.tangem.common.Provider -import com.tangem.core.ui.extensions.iconResId -import com.tangem.core.ui.extensions.networkBadgeIconResId import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.feature.wallet.presentation.common.state.TokenItemState +import com.tangem.feature.wallet.presentation.common.utils.CryptoCurrencyToIconStateConverter import com.tangem.feature.wallet.presentation.organizetokens.model.DraggableItem import com.tangem.feature.wallet.presentation.organizetokens.utils.common.getGroupHeaderId import com.tangem.feature.wallet.presentation.organizetokens.utils.common.getTokenItemId @@ -16,6 +15,8 @@ internal class CryptoCurrencyToDraggableItemConverter( private val appCurrencyProvider: Provider, ) : Converter { + private val iconStateConverter = CryptoCurrencyToIconStateConverter() + override fun convert(value: CryptoCurrencyStatus): DraggableItem.Token { return createDraggableToken(value, appCurrencyProvider()) } @@ -44,12 +45,9 @@ internal class CryptoCurrencyToDraggableItemConverter( return TokenItemState.Draggable( id = getTokenItemId(currency.id), - tokenIconUrl = currency.iconUrl, - tokenIconResId = currencyStatus.currency.iconResId, - networkBadgeIconResId = currencyStatus.currency.networkBadgeIconResId, + icon = iconStateConverter.convert(currency), name = currency.name, fiatAmount = getFormattedFiatAmount(currencyStatus, appCurrency), - isTestnet = currencyStatus.currency.network.isTestnet, ) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/CryptoCurrencyStatusToTokenItemConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/CryptoCurrencyStatusToTokenItemConverter.kt index b19dcf646a..02b175aad7 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/CryptoCurrencyStatusToTokenItemConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/CryptoCurrencyStatusToTokenItemConverter.kt @@ -2,12 +2,11 @@ package com.tangem.feature.wallet.presentation.wallet.utils import com.tangem.common.Provider import com.tangem.core.ui.components.marketprice.PriceChangeConfig -import com.tangem.core.ui.extensions.iconResId -import com.tangem.core.ui.extensions.networkBadgeIconResId import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.feature.wallet.presentation.common.state.TokenItemState +import com.tangem.feature.wallet.presentation.common.utils.CryptoCurrencyToIconStateConverter import com.tangem.feature.wallet.presentation.wallet.viewmodels.WalletClickIntents import com.tangem.utils.converter.Converter import java.math.BigDecimal @@ -18,6 +17,8 @@ internal class CryptoCurrencyStatusToTokenItemConverter( private val clickIntents: WalletClickIntents, ) : Converter { + private val iconStateConverter = CryptoCurrencyToIconStateConverter() + override fun convert(value: CryptoCurrencyStatus): TokenItemState { return when (value.value) { is CryptoCurrencyStatus.Loading -> TokenItemState.Loading(id = value.currency.id.value) @@ -37,9 +38,7 @@ internal class CryptoCurrencyStatusToTokenItemConverter( return TokenItemState.Content( id = currency.id.value, name = currency.name, - tokenIconUrl = currency.iconUrl, - tokenIconResId = currency.iconResId, - networkBadgeIconResId = currency.networkBadgeIconResId, + icon = iconStateConverter.convert(currency), amount = getFormattedAmount(), hasPending = value.hasCurrentNetworkTransactions, tokenOptions = if (isWalletContentHidden) { @@ -50,7 +49,6 @@ internal class CryptoCurrencyStatusToTokenItemConverter( config = getPriceChangeConfig(), ) }, - isTestnet = currency.network.isTestnet, onItemClick = { clickIntents.onTokenItemClick(currency) }, onItemLongClick = { clickIntents.onTokenItemLongClick(currency) }, ) @@ -72,9 +70,7 @@ internal class CryptoCurrencyStatusToTokenItemConverter( private fun CryptoCurrencyStatus.mapToUnreachableTokenItemState() = TokenItemState.Unreachable( id = currency.id.value, name = currency.name, - tokenIconUrl = currency.iconUrl, - tokenIconResId = currency.iconResId, - networkBadgeIconResId = currency.networkBadgeIconResId, + icon = iconStateConverter.convert(currency), ) private fun CryptoCurrencyStatus.getPriceChangeConfig(): PriceChangeConfig { From df9ba4e5f10ad15e59f57d665b3a82ba95828919 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 4 Sep 2023 16:46:04 +0800 Subject: [PATCH 006/242] Updated on 2026-08-14 --- .../java/com/tangem/tap/TapApplication.kt | 5 + .../tap/di/domain/TokensDomainModule.kt | 6 + .../impl/di/TokensListInteractorModule.kt | 1 - .../domain/DefaultTokensListInteractor.kt | 239 +----------------- .../impl/domain/TokensListInteractor.kt | 10 - .../tap/proxy/redux/DaggerGraphState.kt | 4 + .../repository/DefaultCurrenciesRepository.kt | 68 ++++- .../tokens/GetCryptoCurrenciesUseCase.kt | 24 ++ .../domain/tokens/error/GetCurrenciesError.kt | 6 + .../tokens/repository/CurrenciesRepository.kt | 20 ++ .../repository/MockCurrenciesRepository.kt | 4 + 11 files changed, 136 insertions(+), 251 deletions(-) create mode 100644 domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrenciesUseCase.kt create mode 100644 domain/tokens/src/main/kotlin/com/tangem/domain/tokens/error/GetCurrenciesError.kt diff --git a/app/src/main/java/com/tangem/tap/TapApplication.kt b/app/src/main/java/com/tangem/tap/TapApplication.kt index fb391f8164..47d99024bd 100644 --- a/app/src/main/java/com/tangem/tap/TapApplication.kt +++ b/app/src/main/java/com/tangem/tap/TapApplication.kt @@ -25,6 +25,7 @@ import com.tangem.datasource.connection.NetworkConnectionManager import com.tangem.domain.appcurrency.repository.AppCurrencyRepository import com.tangem.domain.card.ScanCardProcessor import com.tangem.domain.common.LogConfig +import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.legacy.WalletManagersRepository import com.tangem.feature.learn2earn.domain.api.Learn2earnInteractor @@ -169,6 +170,9 @@ class TapApplication : Application(), ImageLoaderFactory { @Inject lateinit var walletManagersFacade: WalletManagersFacade + @Inject + lateinit var currenciesRepository: CurrenciesRepository + override fun onCreate() { super.onCreate() @@ -190,6 +194,7 @@ class TapApplication : Application(), ImageLoaderFactory { appCurrencyRepository = appCurrencyRepository, walletManagersFacade = walletManagersFacade, appStateHolder = appStateHolder, + currenciesRepository = currenciesRepository, ), ), ) diff --git a/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt index 8ac2b0b3b2..df25867d4d 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt @@ -116,4 +116,10 @@ internal object TokensDomainModule { ): GetCryptoCurrencyActionsUseCase { return GetCryptoCurrencyActionsUseCase(rampStateManager, marketCryptoCurrencyRepository, dispatchers) } + + @Provides + @ViewModelScoped + fun provideGetCurrenciesUseCase(currenciesRepository: CurrenciesRepository): GetCryptoCurrenciesUseCase { + return GetCryptoCurrenciesUseCase(currenciesRepository = currenciesRepository) + } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/tokens/impl/di/TokensListInteractorModule.kt b/app/src/main/java/com/tangem/tap/features/tokens/impl/di/TokensListInteractorModule.kt index ddc7a2a15b..85fd22ab71 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/impl/di/TokensListInteractorModule.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/impl/di/TokensListInteractorModule.kt @@ -35,7 +35,6 @@ internal object TokensListInteractorModule { reduxStateHolder = reduxStateHolder, testnetTokensStorage = testnetTokensStorage, ), - reduxStateHolder = reduxStateHolder, ) } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/tokens/impl/domain/DefaultTokensListInteractor.kt b/app/src/main/java/com/tangem/tap/features/tokens/impl/domain/DefaultTokensListInteractor.kt index 9610727483..c470316894 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/impl/domain/DefaultTokensListInteractor.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/impl/domain/DefaultTokensListInteractor.kt @@ -1,252 +1,17 @@ package com.tangem.tap.features.tokens.impl.domain import androidx.paging.PagingData -import com.tangem.blockchain.blockchains.cardano.CardanoUtils -import com.tangem.blockchain.common.Blockchain -import com.tangem.blockchain.common.derivation.DerivationStyle -import com.tangem.common.CompletionResult -import com.tangem.common.card.EllipticCurve -import com.tangem.common.extensions.guard -import com.tangem.common.extensions.toMapKey -import com.tangem.common.flatMap -import com.tangem.crypto.hdWallet.DerivationPath -import com.tangem.domain.common.configs.CardConfig -import com.tangem.domain.common.util.derivationStyleProvider -import com.tangem.domain.common.util.supportsHdWallet -import com.tangem.domain.models.scan.ScanResponse -import com.tangem.operations.derivation.ExtendedPublicKeysMap -import com.tangem.tap.* -import com.tangem.tap.common.extensions.dispatchDebugErrorNotification -import com.tangem.tap.common.extensions.dispatchOnMain -import com.tangem.tap.common.redux.global.GlobalAction -import com.tangem.tap.domain.TapError -import com.tangem.tap.domain.model.WalletDataModel import com.tangem.tap.features.tokens.impl.domain.models.Token -import com.tangem.tap.features.tokens.legacy.redux.TokenWithBlockchain -import com.tangem.tap.features.tokens.legacy.redux.TokensMiddleware -import com.tangem.tap.features.wallet.models.Currency -import com.tangem.tap.proxy.AppStateHolder -import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow -import timber.log.Timber /** * Default implementation of tokens list interactor - * FIXME("Necessary to avoid using redux actions") * - * @property repository repository of tokens list feature - * @property reduxStateHolder redux state holder + * @property repository repository of tokens list feature */ -internal class DefaultTokensListInteractor( - private val repository: TokensListRepository, - private val reduxStateHolder: AppStateHolder, -) : TokensListInteractor { +internal class DefaultTokensListInteractor(private val repository: TokensListRepository) : TokensListInteractor { override fun getTokensList(searchText: String): Flow> { return repository.getAvailableTokens(searchText = searchText.ifBlank(defaultValue = { null })) } - - override suspend fun saveChanges(tokens: List, blockchains: List) { - val scanResponse = requireNotNull(reduxStateHolder.scanResponse) - - val derivationStyle = scanResponse.derivationStyleProvider.getDerivationStyle() - val currentTokens = store.state.tokensState.addedWallets - .toNonCustomTokensWithBlockchains(derivationStyle = derivationStyle) - - val currentBlockchains = store.state.tokensState.addedWallets - .toNonCustomBlockchains(derivationStyle = derivationStyle) - - val blockchainsToAdd = blockchains.filterNot(currentBlockchains::contains) - val blockchainsToRemove = currentBlockchains.filterNot(blockchains::contains) - - val tokensToAdd = tokens.filterNot(currentTokens::contains) - val tokensToRemove = currentTokens.filterNot { token -> tokens.any { it.token == token.token } } - - val isNothingToDoWithTokens = tokensToAdd.isEmpty() && tokensToRemove.isEmpty() - val isNothingToDoWithBlockchain = blockchainsToAdd.isEmpty() && blockchainsToRemove.isEmpty() - if (isNothingToDoWithTokens && isNothingToDoWithBlockchain) { - store.dispatchDebugErrorNotification(message = "Nothing to save") - return - } - - remove( - tokens = tokensToRemove, - blockchains = blockchainsToRemove, - derivationStyle = scanResponse.derivationStyleProvider.getDerivationStyle(), - ) - - add(tokens = tokensToAdd, blockchains = blockchainsToAdd, scanResponse = scanResponse) - } - - private fun List.toNonCustomTokensWithBlockchains( - derivationStyle: DerivationStyle?, - ): List { - return this.map(WalletDataModel::currency) - .mapNotNull { currency -> - if (currency !is Currency.Token || currency.isCustomCurrency(derivationStyle)) return@mapNotNull null - TokenWithBlockchain(token = currency.token, blockchain = currency.blockchain) - } - .distinct() - } - - private fun List.toNonCustomBlockchains(derivationStyle: DerivationStyle?): List { - return this.map(WalletDataModel::currency) - .mapNotNull { currency -> - if (currency.isCustomCurrency(derivationStyle)) return@mapNotNull null - (currency as? Currency.Blockchain)?.blockchain - } - .distinct() - } - - private suspend fun remove( - tokens: List, - blockchains: List, - derivationStyle: DerivationStyle?, - ) { - val currencies = convertToCurrencies(tokens, blockchains, derivationStyle) - if (currencies.isEmpty()) return - - val selectedUserWallet = userWalletsListManager.selectedUserWalletSync.guard { - Timber.e("Unable to remove currencies, no user wallet selected") - return - } - - walletCurrenciesManager.removeCurrencies(userWallet = selectedUserWallet, currenciesToRemove = currencies) - } - - private suspend fun add( - tokens: List, - blockchains: List, - scanResponse: ScanResponse, - ) { - val currenciesToAdd = convertToCurrencies( - tokens = tokens, - blockchains = blockchains, - derivationStyle = scanResponse.derivationStyleProvider.getDerivationStyle(), - ) - - // TODO("[REDACTED_TASK_KEY] use DerivationManager") - if (scanResponse.supportsHdWallet()) { - deriveMissingBlockchains(scanResponse, currenciesToAdd) - } else { - submitAdd(scanResponse, currenciesToAdd) - return - } - } - - private suspend fun deriveMissingBlockchains(scanResponse: ScanResponse, currencies: List) { - val config = CardConfig.createConfig(scanResponse.card) - val derivations = currencies.mapNotNull { - val curve = config.primaryCurve(it.blockchain) - curve?.let { getDerivations(curve, scanResponse, currencies) } - }.associate(transform = TokensMiddleware.DerivationData::derivations) - - if (derivations.isEmpty()) { - submitAdd(scanResponse, currencies) - return - } - - when (val result = tangemSdkManager.derivePublicKeys(cardId = null, derivations = derivations)) { - is CompletionResult.Success -> { - val newDerivedKeys = result.data.entries - val oldDerivedKeys = scanResponse.derivedKeys - - val walletKeys = (newDerivedKeys.keys + oldDerivedKeys.keys).toSet() - - val updatedDerivedKeys = walletKeys.associateWith { walletKey -> - val oldDerivations = ExtendedPublicKeysMap(map = oldDerivedKeys[walletKey] ?: emptyMap()) - val newDerivations = newDerivedKeys[walletKey] ?: ExtendedPublicKeysMap(map = emptyMap()) - ExtendedPublicKeysMap(map = oldDerivations + newDerivations) - } - - val updatedScanResponse = scanResponse.copy(derivedKeys = updatedDerivedKeys) - - store.dispatchOnMain(GlobalAction.SaveScanResponse(updatedScanResponse)) - delay(DELAY_SDK_DIALOG_CLOSE) - - submitAdd(scanResponse, currencies) - return - } - is CompletionResult.Failure -> { - store.dispatchDebugErrorNotification(TapError.CustomError(customMessage = "Error adding tokens")) - } - } - } - - private fun getDerivations( - curve: EllipticCurve, - scanResponse: ScanResponse, - currencyList: List, - ): TokensMiddleware.DerivationData? { - val wallet = scanResponse.card.wallets.firstOrNull { it.curve == curve } ?: return null - - val manageTokensCandidates = currencyList - .map(Currency::blockchain) - .distinct() - .filter { it.getSupportedCurves().contains(curve) } - .mapNotNull { it.derivationPath(scanResponse.derivationStyleProvider.getDerivationStyle()) } - - val customTokensCandidates = currencyList - .filter { it.blockchain.getSupportedCurves().contains(curve) } - .mapNotNull(Currency::derivationPath) - .map(::DerivationPath) - - val bothCandidates = (manageTokensCandidates + customTokensCandidates).distinct().toMutableList() - if (bothCandidates.isEmpty()) return null - - currencyList.find { it is Currency.Blockchain && it.blockchain == Blockchain.Cardano }?.let { currency -> - currency.derivationPath?.let { - bothCandidates.add(CardanoUtils.extendedDerivationPath(DerivationPath(it))) - } - } - - val mapKeyOfWalletPublicKey = wallet.publicKey.toMapKey() - val alreadyDerivedKeys = scanResponse.derivedKeys[mapKeyOfWalletPublicKey] ?: ExtendedPublicKeysMap(emptyMap()) - val alreadyDerivedPaths = alreadyDerivedKeys.keys.toList() - - val toDerive = bothCandidates.filterNot(alreadyDerivedPaths::contains) - if (toDerive.isEmpty()) return null - - return TokensMiddleware.DerivationData(derivations = mapKeyOfWalletPublicKey to toDerive) - } - - private suspend fun submitAdd(scanResponse: ScanResponse, currencies: List) { - val selectedUserWallet = userWalletsListManager.selectedUserWalletSync.guard { - Timber.e("Unable to add currencies, no user wallet selected") - return - } - - userWalletsListManager - .update( - userWalletId = selectedUserWallet.walletId, - update = { userWallet -> userWallet.copy(scanResponse = scanResponse) }, - ) - .flatMap { updatedUserWallet -> - walletCurrenciesManager.addCurrencies( - userWallet = updatedUserWallet, - currenciesToAdd = currencies, - ) - } - } - - private fun convertToCurrencies( - tokens: List, - blockchains: List, - derivationStyle: DerivationStyle?, - ): List { - return tokens.map { tokenWithBlockchain -> - Currency.Token( - token = tokenWithBlockchain.token, - blockchain = tokenWithBlockchain.blockchain, - derivationPath = tokenWithBlockchain.blockchain.derivationPath(derivationStyle)?.rawPath, - ) - }.plus( - blockchains.map { blockchain -> - Currency.Blockchain( - blockchain = blockchain, - derivationPath = blockchain.derivationPath(derivationStyle)?.rawPath, - ) - }, - ) - } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/tokens/impl/domain/TokensListInteractor.kt b/app/src/main/java/com/tangem/tap/features/tokens/impl/domain/TokensListInteractor.kt index fee08220bf..c363dfc3c2 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/impl/domain/TokensListInteractor.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/impl/domain/TokensListInteractor.kt @@ -1,9 +1,7 @@ package com.tangem.tap.features.tokens.impl.domain import androidx.paging.PagingData -import com.tangem.blockchain.common.Blockchain import com.tangem.tap.features.tokens.impl.domain.models.Token -import com.tangem.tap.features.tokens.legacy.redux.TokenWithBlockchain import kotlinx.coroutines.flow.Flow /** @@ -15,12 +13,4 @@ internal interface TokensListInteractor { /** Get tokens list using filter by text [searchText] */ fun getTokensList(searchText: String): Flow> - - /** - * Save added tokens - * - * @param tokens tokens list that need to save - * @param blockchains blockchains list that need to save - */ - suspend fun saveChanges(tokens: List, blockchains: List) } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphState.kt b/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphState.kt index 324719823c..7a479780e6 100644 --- a/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphState.kt +++ b/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphState.kt @@ -6,6 +6,7 @@ import com.tangem.domain.appcurrency.repository.AppCurrencyRepository import com.tangem.domain.card.ScanCardProcessor import com.tangem.domain.card.ScanCardUseCase import com.tangem.domain.card.repository.CardSdkConfigRepository +import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.features.tester.api.TesterRouter import com.tangem.features.tokendetails.featuretoggles.TokenDetailsFeatureToggles @@ -37,6 +38,9 @@ data class DaggerGraphState( val appCurrencyRepository: AppCurrencyRepository? = null, val walletManagersFacade: WalletManagersFacade? = null, val appStateHolder: AppStateHolder? = null, + + // FIXME: It is used only for TokensList screen. Remove after refactoring of TokensList + val currenciesRepository: CurrenciesRepository? = null, ) : StateType { inline fun get(getDependency: DaggerGraphState.() -> T?): T { 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 d90b0321e4..2563eb0409 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 @@ -1,9 +1,7 @@ package com.tangem.data.tokens.repository import com.tangem.data.common.cache.CacheRegistry -import com.tangem.data.tokens.utils.CardCurrenciesFactory -import com.tangem.data.tokens.utils.ResponseCurrenciesFactory -import com.tangem.data.tokens.utils.UserTokensResponseFactory +import com.tangem.data.tokens.utils.* import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.api.tangemTech.models.UserTokensResponse import com.tangem.datasource.local.token.UserMarketCoinsStore @@ -55,6 +53,53 @@ internal class DefaultCurrenciesRepository( storeAndPushTokens(userWalletId, response) } + override suspend fun addCurrencies(userWalletId: UserWalletId, currencies: List) { + return withContext(dispatchers.io) { + val savedCurrencies = requireNotNull( + value = userTokensStore.getSyncOrNull(userWalletId), + lazyMessage = { "Saved tokens empty. Can not perform add currencies action" }, + ) + + val newCoins = createCoinsForNewTokens( + userWalletId = userWalletId, + newTokens = currencies.filterIsInstance(), + savedCurrencies = savedCurrencies.tokens, + ) + + val newCurrencies = newCoins + currencies + + storeAndPushTokens( + userWalletId = userWalletId, + response = savedCurrencies.copy( + tokens = savedCurrencies.tokens + newCurrencies.map(userTokensResponseFactory::createResponseToken), + ), + ) + } + } + + private suspend fun createCoinsForNewTokens( + userWalletId: UserWalletId, + newTokens: List, + savedCurrencies: List, + ): List { + return newTokens + .filterNot { savedCurrencies.hasCoinForToken(it) } // tokens without coins + .mapNotNull { + CryptoCurrencyFactory().createCoin( + blockchain = getBlockchain(networkId = it.network.id), + derivationStyleProvider = getUserWallet(userWalletId).scanResponse.derivationStyleProvider, + ) + } + } + + private fun List.hasCoinForToken(token: CryptoCurrency.Token): Boolean { + return any { + val blockchain = getBlockchain(networkId = token.network.id) + + it.id == getCoinId(blockchain).rawCurrencyId + } + } + override suspend fun removeCurrency(userWalletId: UserWalletId, currency: CryptoCurrency) = withContext(dispatchers.io) { val savedCurrencies = requireNotNull( @@ -71,6 +116,23 @@ internal class DefaultCurrenciesRepository( ) } + override suspend fun removeCurrencies(userWalletId: UserWalletId, currencies: List) { + return withContext(dispatchers.io) { + val savedCurrencies = requireNotNull( + value = userTokensStore.getSyncOrNull(userWalletId), + lazyMessage = { "Saved tokens empty. Can not perform remove currencies action" }, + ) + + val tokens = currencies.map(userTokensResponseFactory::createResponseToken) + storeAndPushTokens( + userWalletId = userWalletId, + response = savedCurrencies.copy( + tokens = savedCurrencies.tokens.filterNot(tokens::contains), + ), + ) + } + } + override suspend fun getSingleCurrencyWalletPrimaryCurrency(userWalletId: UserWalletId): CryptoCurrency { return withContext(dispatchers.io) { val userWallet = getUserWallet(userWalletId) diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrenciesUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrenciesUseCase.kt new file mode 100644 index 0000000000..4595413fa6 --- /dev/null +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrenciesUseCase.kt @@ -0,0 +1,24 @@ +package com.tangem.domain.tokens + +import arrow.core.Either +import arrow.core.raise.catch +import arrow.core.raise.either +import com.tangem.domain.tokens.error.GetCurrenciesError +import com.tangem.domain.tokens.models.CryptoCurrency +import com.tangem.domain.tokens.repository.CurrenciesRepository +import com.tangem.domain.wallets.models.UserWalletId + +class GetCryptoCurrenciesUseCase(private val currenciesRepository: CurrenciesRepository) { + + suspend operator fun invoke( + userWalletId: UserWalletId, + refresh: Boolean = false, + ): Either> { + return either { + catch( + block = { currenciesRepository.getMultiCurrencyWalletCurrenciesSync(userWalletId, refresh) }, + catch = { raise(GetCurrenciesError.DataError(it)) }, + ) + } + } +} \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/error/GetCurrenciesError.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/error/GetCurrenciesError.kt new file mode 100644 index 0000000000..0e68977605 --- /dev/null +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/error/GetCurrenciesError.kt @@ -0,0 +1,6 @@ +package com.tangem.domain.tokens.error + +sealed class GetCurrenciesError { + + data class DataError(val cause: Throwable) : GetCurrenciesError() +} \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/CurrenciesRepository.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/CurrenciesRepository.kt index 97cbf78ede..99db8c884a 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/CurrenciesRepository.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/CurrenciesRepository.kt @@ -27,6 +27,16 @@ interface CurrenciesRepository { isSortedByBalance: Boolean, ) + /** + * Add currencies to a specific user wallet. + * + * @param userWalletId The unique identifier of the user wallet. + * @param currencies The currencies which must be added. + * @throws com.tangem.domain.core.error.DataError.UserWalletError.WrongUserWallet If single-currency user wallet + * ID provided. + */ + suspend fun addCurrencies(userWalletId: UserWalletId, currencies: List) + /** * Removes currency from a specific user wallet. * @@ -37,6 +47,16 @@ interface CurrenciesRepository { */ suspend fun removeCurrency(userWalletId: UserWalletId, currency: CryptoCurrency) + /** + * Removes currencies from a specific user wallet. + * + * @param userWalletId The unique identifier of the user wallet. + * @param currencies The currencies which must be removed. + * @throws com.tangem.domain.core.error.DataError.UserWalletError.WrongUserWallet If single-currency user wallet + * ID provided. + */ + suspend fun removeCurrencies(userWalletId: UserWalletId, currencies: List) + /** * Retrieves the primary cryptocurrency for a specific single-currency user wallet. * diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockCurrenciesRepository.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockCurrenciesRepository.kt index 73e096b201..192b8f5afc 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockCurrenciesRepository.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockCurrenciesRepository.kt @@ -40,10 +40,14 @@ internal class MockCurrenciesRepository( isTokensSortedByBalanceAfterSortingApply = isSortedByBalance } + override suspend fun addCurrencies(userWalletId: UserWalletId, currencies: List) = Unit + override suspend fun removeCurrency(userWalletId: UserWalletId, currency: CryptoCurrency) { removeCurrencyResult.onLeft { throw it } } + override suspend fun removeCurrencies(userWalletId: UserWalletId, currencies: List) = Unit + override suspend fun getMultiCurrencyWalletCurrenciesSync( userWalletId: UserWalletId, refresh: Boolean, From 6271952c4a6f479729100252a15369a5b8c07766 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 4 Sep 2023 16:50:50 +0300 Subject: [PATCH 007/242] Updated on 2026-08-14 --- .../ui/appsettings/AppSettingsFragment.kt | 55 +++++++------- .../ui/appsettings/AppSettingsScreen.kt | 17 +---- .../ui/appsettings/AppSettingsScreenState.kt | 2 +- .../ui/appsettings/AppSettingsViewModel.kt | 2 +- .../ui/cardsettings/CardSettingsFragment.kt | 56 +++++++------- .../ui/cardsettings/CardSettingsScreen.kt | 48 +++++++----- .../cardsettings/CardSettingsScreenState.kt | 8 +- .../ui/cardsettings/CardSettingsViewModel.kt | 2 +- .../ui/common/DetailsComposeElements.kt | 69 ++++++++++-------- .../details/ui/details/DetailsFragment.kt | 47 ++++++------ .../details/ui/details/DetailsScreen.kt | 73 ++++++++++--------- .../details/ui/details/DetailsScreenState.kt | 10 +-- .../details/ui/details/DetailsViewModel.kt | 2 +- .../details/ui/resetcard/ResetCardFragment.kt | 50 ++++++------- .../details/ui/resetcard/ResetCardScreen.kt | 24 ++---- .../ui/resetcard/ResetCardScreenState.kt | 2 +- .../ui/resetcard/ResetCardViewModel.kt | 2 +- .../ui/securitymode/SecurityModeFragment.kt | 50 ++++++------- .../ui/securitymode/SecurityModeScreen.kt | 17 +++-- .../securitymode/SecurityModeScreenState.kt | 4 +- .../ui/securitymode/SecurityModeViewModel.kt | 2 +- .../ui/walletconnect/WalletConnectFragment.kt | 63 ++++++++-------- .../ui/walletconnect/WalletConnectScreen.kt | 7 +- .../walletconnect/WalletConnectScreenState.kt | 2 +- .../walletconnect/WalletConnectViewModel.kt | 2 +- 25 files changed, 314 insertions(+), 302 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsFragment.kt b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsFragment.kt index 0f0e3de308..8a7e4987df 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsFragment.kt @@ -1,50 +1,55 @@ package com.tangem.tap.features.details.ui.appsettings import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup +import androidx.compose.runtime.Composable import androidx.compose.runtime.MutableState import androidx.compose.runtime.mutableStateOf -import androidx.compose.ui.platform.ComposeView -import androidx.fragment.app.Fragment +import androidx.compose.ui.Modifier import androidx.transition.TransitionInflater import com.tangem.core.navigation.NavigationAction -import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.screen.ComposeFragment +import com.tangem.core.ui.theme.AppThemeModeHolder import com.tangem.tap.features.details.redux.DetailsAction import com.tangem.tap.features.details.redux.DetailsState import com.tangem.tap.store import com.tangem.wallet.R +import dagger.hilt.android.AndroidEntryPoint import org.rekotlin.StoreSubscriber +import javax.inject.Inject + +@AndroidEntryPoint +internal class AppSettingsFragment : ComposeFragment(), StoreSubscriber { + + @Inject + override lateinit var appThemeModeHolder: AppThemeModeHolder -class AppSettingsFragment : Fragment(), StoreSubscriber { private val viewModel = AppSettingsViewModel(store) private var screenState: MutableState = mutableStateOf(viewModel.updateState(store.state.detailsState)) + @Composable + override fun ScreenContent(modifier: Modifier) { + AppSettingsScreen( + modifier = modifier, + state = screenState.value, + onBackClick = { + store.dispatch(DetailsAction.ResetCardSettingsData) + store.dispatch(NavigationAction.PopBackTo()) + }, + ) + } + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - val inflater = TransitionInflater.from(requireContext()) - enterTransition = inflater.inflateTransition(R.transition.fade) - exitTransition = inflater.inflateTransition(R.transition.fade) + viewModel.checkBiometricsStatus() } - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { - return ComposeView(requireContext()).apply { - setContent { - isTransitionGroup = true - TangemTheme { - AppSettingsScreen( - state = screenState.value, - onBackClick = { - store.dispatch(DetailsAction.ResetCardSettingsData) - store.dispatch(NavigationAction.PopBackTo()) - }, - ) - } - } - } + override fun TransitionInflater.inflateTransitions(): Boolean { + enterTransition = inflateTransition(R.transition.fade) + exitTransition = inflateTransition(R.transition.fade) + + return true } override fun onStart() { diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsScreen.kt b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsScreen.kt index 9255526f4a..327dcbe8c4 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsScreen.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsScreen.kt @@ -1,19 +1,9 @@ package com.tangem.tap.features.details.ui.appsettings import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.* import androidx.compose.material.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberUpdatedState -import androidx.compose.runtime.setValue +import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource @@ -31,8 +21,9 @@ import com.tangem.tap.features.details.ui.common.TangemSwitch import com.tangem.wallet.R @Composable -fun AppSettingsScreen(state: AppSettingsScreenState, onBackClick: () -> Unit) { +internal fun AppSettingsScreen(state: AppSettingsScreenState, onBackClick: () -> Unit, modifier: Modifier = Modifier) { SettingsScreensScaffold( + modifier = modifier, content = { AppSettings(state = state) }, titleRes = R.string.app_settings_title, onBackClick = onBackClick, diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsScreenState.kt b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsScreenState.kt index fbc4640e8f..b891b7b088 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsScreenState.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsScreenState.kt @@ -2,7 +2,7 @@ package com.tangem.tap.features.details.ui.appsettings import com.tangem.tap.features.details.redux.AppSetting -data class AppSettingsScreenState( +internal data class AppSettingsScreenState( val settings: Map = emptyMap(), val showEnrollBiometricsCard: Boolean = false, val isTogglesEnabled: Boolean = false, diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsViewModel.kt b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsViewModel.kt index 38323dcc3e..112d1db938 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsViewModel.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsViewModel.kt @@ -7,7 +7,7 @@ import com.tangem.tap.features.details.redux.DetailsAction import com.tangem.tap.features.details.redux.DetailsState import org.rekotlin.Store -class AppSettingsViewModel(private val store: Store) { +internal class AppSettingsViewModel(private val store: Store) { fun updateState(state: DetailsState): AppSettingsScreenState { return with(state.appSettingsState) { diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsFragment.kt b/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsFragment.kt index 90c489ab41..28bd5b3d26 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsFragment.kt @@ -1,51 +1,49 @@ package com.tangem.tap.features.details.ui.cardsettings -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup +import androidx.compose.runtime.Composable import androidx.compose.runtime.MutableState import androidx.compose.runtime.mutableStateOf -import androidx.compose.ui.platform.ComposeView -import androidx.fragment.app.Fragment +import androidx.compose.ui.Modifier import androidx.transition.TransitionInflater import com.tangem.core.navigation.NavigationAction -import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.screen.ComposeFragment +import com.tangem.core.ui.theme.AppThemeModeHolder import com.tangem.tap.features.details.redux.DetailsAction import com.tangem.tap.features.details.redux.DetailsState import com.tangem.tap.store +import com.tangem.wallet.R +import dagger.hilt.android.AndroidEntryPoint import org.rekotlin.StoreSubscriber +import javax.inject.Inject -class CardSettingsFragment : Fragment(), StoreSubscriber { +@AndroidEntryPoint +internal class CardSettingsFragment : ComposeFragment(), StoreSubscriber { + + @Inject + override lateinit var appThemeModeHolder: AppThemeModeHolder private val viewModel = CardSettingsViewModel(store) private var screenState: MutableState = mutableStateOf(viewModel.updateState(store.state.detailsState.cardSettingsState)) - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - - val inflater = TransitionInflater.from(requireContext()) - enterTransition = inflater.inflateTransition(android.R.transition.fade) - exitTransition = inflater.inflateTransition(android.R.transition.fade) + @Composable + override fun ScreenContent(modifier: Modifier) { + CardSettingsScreen( + modifier = modifier, + state = screenState.value, + onBackClick = { + store.dispatch(DetailsAction.ResetCardSettingsData) + store.dispatch(NavigationAction.PopBackTo()) + }, + ) } - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { - return ComposeView(requireContext()).apply { - setContent { - isTransitionGroup = true - TangemTheme { - CardSettingsScreen( - state = screenState.value, - onBackClick = { - store.dispatch(DetailsAction.ResetCardSettingsData) - store.dispatch(NavigationAction.PopBackTo()) - }, - ) - } - } - } + override fun TransitionInflater.inflateTransitions(): Boolean { + enterTransition = inflateTransition(R.transition.fade) + exitTransition = inflateTransition(R.transition.fade) + + return true } override fun onStart() { diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsScreen.kt b/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsScreen.kt index 29906fd0e0..87c845a602 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsScreen.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsScreen.kt @@ -2,13 +2,7 @@ package com.tangem.tap.features.details.ui.cardsettings import androidx.compose.foundation.Image import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.foundation.rememberScrollState @@ -18,7 +12,6 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.draw.rotate import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.res.colorResource import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview @@ -29,10 +22,15 @@ import com.tangem.tap.features.details.ui.common.SettingsScreensScaffold import com.tangem.wallet.R @Composable -fun CardSettingsScreen(state: CardSettingsScreenState, onBackClick: () -> Unit) { +internal fun CardSettingsScreen( + state: CardSettingsScreenState, + onBackClick: () -> Unit, + modifier: Modifier = Modifier, +) { val needReadCard = state.cardDetails == null SettingsScreensScaffold( + modifier = modifier, content = { if (needReadCard) { CardSettingsReadCard(state.onScanCardClick) @@ -41,14 +39,13 @@ fun CardSettingsScreen(state: CardSettingsScreenState, onBackClick: () -> Unit) } }, titleRes = R.string.card_settings_title, - backgroundColor = TangemTheme.colors.background.secondary, onBackClick = onBackClick, ) } @Suppress("MagicNumber") @Composable -fun CardSettingsReadCard(onScanCardClick: () -> Unit) { +private fun CardSettingsReadCard(onScanCardClick: () -> Unit) { Column( modifier = Modifier.fillMaxSize(), ) { @@ -84,13 +81,13 @@ fun CardSettingsReadCard(onScanCardClick: () -> Unit) { ) { Text( text = stringResource(id = R.string.scan_card_settings_title), - color = colorResource(id = R.color.text_primary_1), + color = TangemTheme.colors.text.primary1, style = TangemTheme.typography.h3, ) Spacer(modifier = Modifier.size(20.dp)) Text( text = stringResource(id = R.string.scan_card_settings_message), - color = colorResource(id = R.color.text_secondary), + color = TangemTheme.colors.text.secondary, style = TangemTheme.typography.body1, modifier = Modifier .verticalScroll(rememberScrollState()) @@ -107,7 +104,7 @@ fun CardSettingsReadCard(onScanCardClick: () -> Unit) { @Suppress("ComplexMethod") @Composable -fun CardSettings(state: CardSettingsScreenState) { +private fun CardSettings(state: CardSettingsScreenState) { if (state.cardDetails == null) return LazyColumn( @@ -166,8 +163,25 @@ fun CardSettings(state: CardSettingsScreenState) { } } +// region Preview @Composable -@Preview -private fun CardSettingsPreview() { +private fun CardSettingsScreenStateSample() { CardSettingsScreen(state = CardSettingsScreenState(onScanCardClick = {}) {}, {}) -} \ No newline at end of file +} + +@Preview(showBackground = true, widthDp = 360) +@Composable +private fun CardSettingsScreenStatePreview_Light() { + TangemTheme { + CardSettingsScreenStateSample() + } +} + +@Preview(showBackground = true, widthDp = 360) +@Composable +private fun CardSettingsScreenStatePreview_Dark() { + TangemTheme(isDark = true) { + CardSettingsScreenStateSample() + } +} +// endregion Preview \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsScreenState.kt b/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsScreenState.kt index e09b4e7c2b..4cf5e68186 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsScreenState.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsScreenState.kt @@ -11,14 +11,14 @@ import com.tangem.tap.features.details.ui.utils.toResetCardDescriptionText import com.tangem.wallet.R import com.tangem.tap.features.details.redux.CardInfo as ReduxCardInfo -data class CardSettingsScreenState( +internal data class CardSettingsScreenState( val cardDetails: List? = null, val accessCodeRecoveryState: AccessCodeRecoveryState? = null, val onScanCardClick: () -> Unit, val onElementClick: (CardInfo) -> Unit, ) -sealed class CardInfo( +internal sealed class CardInfo( val titleRes: TextReference, val subtitle: TextReference, val clickable: Boolean = false, @@ -68,7 +68,7 @@ sealed class CardInfo( } // TODO("Remove and use the same from coreUI") -sealed interface TextReference { +internal sealed interface TextReference { class Res(@StringRes val id: Int, val formatArgs: List = emptyList()) : TextReference { constructor(@StringRes id: Int, vararg formatArgs: Any) : this(id, formatArgs.toList()) } @@ -78,7 +78,7 @@ sealed interface TextReference { @Composable @ReadOnlyComposable -fun TextReference.resolveReference(): String { +internal fun TextReference.resolveReference(): String { return when (this) { is TextReference.Res -> stringResource(this.id, *this.formatArgs.toTypedArray()) is TextReference.Str -> this.value diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsViewModel.kt b/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsViewModel.kt index 805f373605..d40ed17ec9 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsViewModel.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsViewModel.kt @@ -10,7 +10,7 @@ import com.tangem.tap.features.details.redux.CardSettingsState import com.tangem.tap.features.details.redux.DetailsAction import org.rekotlin.Store -class CardSettingsViewModel(private val store: Store) { +internal class CardSettingsViewModel(private val store: Store) { fun updateState(state: CardSettingsState?): CardSettingsScreenState { return if (state?.manageSecurityState == null) { diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/common/DetailsComposeElements.kt b/app/src/main/java/com/tangem/tap/features/details/ui/common/DetailsComposeElements.kt index c92047d0bc..653c54b272 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/common/DetailsComposeElements.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/common/DetailsComposeElements.kt @@ -1,6 +1,7 @@ package com.tangem.tap.features.details.ui.common import androidx.activity.compose.BackHandler +import androidx.annotation.StringRes import androidx.compose.foundation.layout.* import androidx.compose.foundation.selection.selectable import androidx.compose.material.* @@ -12,20 +13,24 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import com.tangem.core.ui.components.PrimaryButtonIconEnd +import com.tangem.core.ui.components.SystemBarsEffect import com.tangem.core.ui.res.TangemTheme import com.tangem.wallet.R @Composable -fun SettingsScreensScaffold( - modifier: Modifier = Modifier, - content: @Composable () -> Unit, - background: @Composable (() -> Unit)? = null, - fab: @Composable (() -> Unit)? = null, - backgroundColor: Color = TangemTheme.colors.background.secondary, - titleRes: Int? = null, +internal fun SettingsScreensScaffold( onBackClick: () -> Unit, + content: @Composable () -> Unit, + modifier: Modifier = Modifier, + fab: @Composable (() -> Unit)? = null, + @StringRes titleRes: Int? = null, ) { - BackHandler(true, onBackClick) + val backgroundColor = TangemTheme.colors.background.secondary + + BackHandler(onBack = onBackClick) + SystemBarsEffect { + setSystemBarsColor(backgroundColor) + } Scaffold( topBar = { @@ -37,33 +42,30 @@ fun SettingsScreensScaffold( modifier = modifier.systemBarsPadding(), backgroundColor = backgroundColor, floatingActionButton = { fab?.invoke() }, - ) { - if (titleRes != null) { - Box(modifier = modifier.fillMaxSize()) { - background?.invoke() - - Column(modifier = modifier.fillMaxWidth()) { - Text( - text = stringResource(id = titleRes), - modifier = modifier.padding( - start = TangemTheme.dimens.spacing20, - end = TangemTheme.dimens.spacing20, - bottom = TangemTheme.dimens.spacing54, - ), - style = TangemTheme.typography.h1, - color = TangemTheme.colors.text.primary1, - ) - content() - } + ) { paddings -> + Column( + modifier = Modifier + .padding(paddings) + .fillMaxSize(), + ) { + if (titleRes != null) { + Text( + text = stringResource(id = titleRes), + modifier = Modifier + .padding(horizontal = TangemTheme.dimens.spacing20) + .padding(bottom = TangemTheme.dimens.spacing36), + style = TangemTheme.typography.h1, + color = TangemTheme.colors.text.primary1, + ) } - } else { + content() } } } @Composable -fun ScreenTitle(titleRes: Int, modifier: Modifier = Modifier) { +internal fun ScreenTitle(titleRes: Int, modifier: Modifier = Modifier) { Text( text = stringResource(id = titleRes), modifier = modifier.padding(start = 20.dp, end = 20.dp), @@ -73,7 +75,7 @@ fun ScreenTitle(titleRes: Int, modifier: Modifier = Modifier) { } @Composable -fun EmptyTopBarWithNavigation( +internal fun EmptyTopBarWithNavigation( onBackClick: () -> Unit, backgroundColor: Color = TangemTheme.colors.background.primary, ) { @@ -95,7 +97,12 @@ fun EmptyTopBarWithNavigation( } @Composable -fun DetailsMainButton(title: String, onClick: () -> Unit, modifier: Modifier = Modifier, enabled: Boolean = true) { +internal fun DetailsMainButton( + title: String, + onClick: () -> Unit, + modifier: Modifier = Modifier, + enabled: Boolean = true, +) { PrimaryButtonIconEnd( text = title, enabled = enabled, @@ -107,7 +114,7 @@ fun DetailsMainButton(title: String, onClick: () -> Unit, modifier: Modifier = M } @Composable -fun DetailsRadioButtonElement(title: String, subtitle: String, selected: Boolean, onClick: () -> Unit) { +internal fun DetailsRadioButtonElement(title: String, subtitle: String, selected: Boolean, onClick: () -> Unit) { Row( modifier = Modifier .fillMaxWidth() diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsFragment.kt b/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsFragment.kt index 371b571cdd..6b17a86209 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsFragment.kt @@ -1,45 +1,48 @@ package com.tangem.tap.features.details.ui.details import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.compose.ui.platform.ComposeView -import androidx.fragment.app.Fragment +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier import androidx.transition.TransitionInflater import com.tangem.core.analytics.Analytics import com.tangem.core.navigation.NavigationAction -import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.screen.ComposeFragment +import com.tangem.core.ui.theme.AppThemeModeHolder import com.tangem.tap.common.analytics.events.Settings import com.tangem.tap.features.details.redux.DetailsState import com.tangem.tap.store import com.tangem.wallet.R +import dagger.hilt.android.AndroidEntryPoint import org.rekotlin.StoreSubscriber +import javax.inject.Inject -class DetailsFragment : Fragment(), StoreSubscriber { +@AndroidEntryPoint +internal class DetailsFragment : ComposeFragment(), StoreSubscriber { private val detailsViewModel = DetailsViewModel(store) + @Inject + override lateinit var appThemeModeHolder: AppThemeModeHolder + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) Analytics.send(Settings.ScreenOpened()) - val inflater = TransitionInflater.from(requireContext()) - enterTransition = inflater.inflateTransition(R.transition.fade) - exitTransition = inflater.inflateTransition(R.transition.fade) } - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { - return ComposeView(requireContext()).apply { - setContent { - isTransitionGroup = true - TangemTheme { - DetailsScreen( - state = detailsViewModel.detailsScreenState.value, - onBackClick = { store.dispatch(NavigationAction.PopBackTo()) }, - ) - } - } - } + @Composable + override fun ScreenContent(modifier: Modifier) { + DetailsScreen( + modifier = modifier, + state = detailsViewModel.detailsScreenState.value, + onBackClick = { store.dispatch(NavigationAction.PopBackTo()) }, + ) + } + + override fun TransitionInflater.inflateTransitions(): Boolean { + enterTransition = inflateTransition(R.transition.fade) + exitTransition = inflateTransition(R.transition.fade) + + return true } override fun onStart() { diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsScreen.kt b/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsScreen.kt index f05682f878..b853168363 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsScreen.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsScreen.kt @@ -1,18 +1,7 @@ package com.tangem.tap.features.details.ui.details import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.BoxScope -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.defaultMinSize -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyRow import androidx.compose.foundation.lazy.items import androidx.compose.foundation.rememberScrollState @@ -27,12 +16,10 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.res.colorResource import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import com.tangem.core.ui.components.SystemBarsEffect import com.tangem.core.ui.res.TangemColorPalette import com.tangem.core.ui.res.TangemTheme import com.tangem.tap.features.details.ui.common.ScreenTitle @@ -41,20 +28,17 @@ import com.tangem.wallet.R import kotlinx.coroutines.launch @Composable -fun DetailsScreen(state: DetailsScreenState, onBackClick: () -> Unit) { - SystemBarsEffect { - setSystemBarsColor(color = TangemColorPalette.Light1) - } - +internal fun DetailsScreen(state: DetailsScreenState, onBackClick: () -> Unit, modifier: Modifier = Modifier) { SettingsScreensScaffold( + modifier = modifier, content = { Content(state = state) }, onBackClick = onBackClick, ) } @Composable -fun Content(state: DetailsScreenState) { - Box { +private fun Content(state: DetailsScreenState, modifier: Modifier = Modifier) { + Box(modifier = modifier) { Column( modifier = Modifier .fillMaxSize() @@ -78,7 +62,7 @@ fun Content(state: DetailsScreenState) { Text( text = "${stringResource(id = state.appNameRes)} ${state.tangemVersion}", style = TangemTheme.typography.caption, - color = colorResource(id = R.color.text_tertiary), + color = TangemTheme.colors.text.tertiary, modifier = Modifier.padding(start = 16.dp, end = 16.dp, bottom = 40.dp), ) } @@ -87,7 +71,7 @@ fun Content(state: DetailsScreenState) { } @Composable -fun WalletConnectDetailsItem(onItemsClick: (SettingsElement) -> Unit) { +private fun WalletConnectDetailsItem(onItemsClick: (SettingsElement) -> Unit) { Row( modifier = Modifier .defaultMinSize(minHeight = 84.dp) @@ -100,7 +84,7 @@ fun WalletConnectDetailsItem(onItemsClick: (SettingsElement) -> Unit) { painter = painterResource(id = R.drawable.ic_walletconnect), contentDescription = stringResource(id = R.string.wallet_connect_title), modifier = Modifier.padding(start = 20.dp, end = 20.dp), - tint = colorResource(id = R.color.all_colors_azure), + tint = TangemColorPalette.Azure, ) Column( modifier = Modifier.defaultMinSize(minHeight = 56.dp), @@ -111,20 +95,20 @@ fun WalletConnectDetailsItem(onItemsClick: (SettingsElement) -> Unit) { text = stringResource(id = R.string.wallet_connect_title), modifier = Modifier.padding(end = 20.dp, bottom = 4.dp), style = TangemTheme.typography.h3, - color = colorResource(id = R.color.text_primary_1), + color = TangemTheme.colors.text.primary1, ) Text( text = stringResource(id = R.string.wallet_connect_subtitle), modifier = Modifier.padding(end = 20.dp, bottom = 4.dp), style = TangemTheme.typography.body1, - color = colorResource(id = R.color.text_secondary), + color = TangemTheme.colors.text.secondary, ) } } } @Composable -fun DetailsItem(item: SettingsElement, appCurrency: String, onItemsClick: () -> Unit) { +private fun DetailsItem(item: SettingsElement, appCurrency: String, onItemsClick: () -> Unit) { Row( modifier = Modifier .height(56.dp) @@ -137,20 +121,20 @@ fun DetailsItem(item: SettingsElement, appCurrency: String, onItemsClick: () -> painter = painterResource(id = item.iconRes), contentDescription = stringResource(id = item.titleRes), modifier = Modifier.padding(start = 20.dp, end = 20.dp), - tint = colorResource(id = R.color.icon_secondary), + tint = TangemTheme.colors.icon.secondary, ) Column(modifier = Modifier.padding(end = 20.dp)) { Text( text = stringResource(id = item.titleRes), modifier = Modifier, style = TangemTheme.typography.subtitle1, - color = colorResource(id = R.color.text_primary_1), + color = TangemTheme.colors.text.primary1, ) if (item == SettingsElement.AppCurrency) { Text( text = appCurrency, style = TangemTheme.typography.body2, - color = colorResource(id = R.color.text_secondary), + color = TangemTheme.colors.text.secondary, ) } } @@ -158,7 +142,7 @@ fun DetailsItem(item: SettingsElement, appCurrency: String, onItemsClick: () -> } @Composable -fun TangemSocialAccounts(links: List, onSocialNetworkClick: (SocialNetworkLink) -> Unit) { +private fun TangemSocialAccounts(links: List, onSocialNetworkClick: (SocialNetworkLink) -> Unit) { LazyRow( modifier = Modifier.padding(start = 8.dp, end = 8.dp), verticalAlignment = Alignment.CenterVertically, @@ -170,14 +154,14 @@ fun TangemSocialAccounts(links: List, onSocialNetworkClick: ( modifier = Modifier .padding(8.dp) .clickable { onSocialNetworkClick(it) }, - tint = colorResource(id = R.color.icon_informative), + tint = TangemTheme.colors.icon.informative, ) } } } @Composable -fun BoxScope.ShowSnackbarIfNeeded(snackbarErrorState: EventError) { +private fun BoxScope.ShowSnackbarIfNeeded(snackbarErrorState: EventError) { val snackbarHostState = remember { SnackbarHostState() } val coroutineScope = rememberCoroutineScope() SnackbarHost( @@ -206,9 +190,9 @@ fun BoxScope.ShowSnackbarIfNeeded(snackbarErrorState: EventError) { } } +// region Preview @Composable -@Preview -private fun Preview() { +private fun DetailsScreenContentSample() { DetailsScreen( state = DetailsScreenState( elements = SettingsElement.values().toList(), @@ -220,4 +204,21 @@ private fun Preview() { ), onBackClick = {}, ) -} \ No newline at end of file +} + +@Preview(showBackground = true, widthDp = 360) +@Composable +private fun DetailsScreenContentPreview_Light() { + TangemTheme(isDark = false) { + DetailsScreenContentSample() + } +} + +@Preview(showBackground = true, widthDp = 360) +@Composable +private fun DetailsScreenContentPreview_Dark() { + TangemTheme(isDark = true) { + DetailsScreenContentSample() + } +} +// endregion Preview \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsScreenState.kt b/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsScreenState.kt index c0dd629819..a5e030a647 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsScreenState.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsScreenState.kt @@ -6,7 +6,7 @@ import androidx.compose.runtime.mutableStateOf import com.tangem.wallet.R @Immutable -data class DetailsScreenState( +internal data class DetailsScreenState( val elements: List, val tangemLinks: List, val tangemVersion: String, @@ -19,7 +19,7 @@ data class DetailsScreenState( } @Immutable -enum class SettingsElement( +internal enum class SettingsElement( val iconRes: Int, val titleRes: Int, ) { @@ -37,12 +37,12 @@ enum class SettingsElement( } @Immutable -data class SocialNetworkLink( +internal data class SocialNetworkLink( val network: SocialNetwork, val url: String, ) -sealed class EventError { +internal sealed class EventError { object Empty : EventError() data class DemoReferralNotAvailable(val onErrorShow: () -> Unit) : EventError() } @@ -58,7 +58,7 @@ sealed class SocialNetwork(val id: String, val iconRes: Int) { object Discord : SocialNetwork("Discord", R.drawable.ic_discord) } -object TangemSocialAccounts { +internal object TangemSocialAccounts { val accountsEn: List = listOf( SocialNetworkLink(SocialNetwork.Telegram, "https://t.me/tangem_chat"), SocialNetworkLink(SocialNetwork.Twitter, "https://twitter.com/tangem"), diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsViewModel.kt b/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsViewModel.kt index 208d964f58..c3491e7c73 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsViewModel.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsViewModel.kt @@ -28,7 +28,7 @@ import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import org.rekotlin.Store -class DetailsViewModel(private val store: Store) { +internal class DetailsViewModel(private val store: Store) { var detailsScreenState: MutableState = mutableStateOf(updateState(store.state.detailsState)) private set diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardFragment.kt b/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardFragment.kt index 7a7c723779..f7a75b1b41 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardFragment.kt @@ -1,47 +1,45 @@ package com.tangem.tap.features.details.ui.resetcard -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup +import androidx.compose.runtime.Composable import androidx.compose.runtime.MutableState import androidx.compose.runtime.mutableStateOf -import androidx.compose.ui.platform.ComposeView -import androidx.fragment.app.Fragment +import androidx.compose.ui.Modifier import androidx.transition.TransitionInflater import com.tangem.core.navigation.NavigationAction -import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.screen.ComposeFragment +import com.tangem.core.ui.theme.AppThemeModeHolder import com.tangem.tap.features.details.redux.DetailsState import com.tangem.tap.store +import com.tangem.wallet.R +import dagger.hilt.android.AndroidEntryPoint import org.rekotlin.StoreSubscriber +import javax.inject.Inject -class ResetCardFragment : Fragment(), StoreSubscriber { +@AndroidEntryPoint +internal class ResetCardFragment : ComposeFragment(), StoreSubscriber { + + @Inject + override lateinit var appThemeModeHolder: AppThemeModeHolder private val viewModel = ResetCardViewModel(store) private var screenState: MutableState = mutableStateOf(viewModel.updateState(store.state.detailsState.cardSettingsState)) - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - - val inflater = TransitionInflater.from(requireContext()) - enterTransition = inflater.inflateTransition(android.R.transition.fade) - exitTransition = inflater.inflateTransition(android.R.transition.fade) + @Composable + override fun ScreenContent(modifier: Modifier) { + ResetCardScreen( + modifier = modifier, + state = screenState.value, + onBackClick = { store.dispatch(NavigationAction.PopBackTo()) }, + ) } - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { - return ComposeView(requireContext()).apply { - setContent { - isTransitionGroup = true - TangemTheme { - ResetCardScreen( - state = screenState.value, - onBackClick = { store.dispatch(NavigationAction.PopBackTo()) }, - ) - } - } - } + override fun TransitionInflater.inflateTransitions(): Boolean { + enterTransition = inflateTransition(R.transition.fade) + exitTransition = inflateTransition(R.transition.fade) + + return true } override fun onStart() { diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardScreen.kt b/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardScreen.kt index d64bf444ca..eb656819fd 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardScreen.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardScreen.kt @@ -1,26 +1,12 @@ package com.tangem.tap.features.details.ui.resetcard -import androidx.compose.foundation.Image -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.offset -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.verticalScroll +import androidx.compose.foundation.* +import androidx.compose.foundation.layout.* import androidx.compose.material.Icon import androidx.compose.material.IconToggleButton import androidx.compose.material.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview @@ -34,17 +20,17 @@ import com.tangem.tap.features.details.ui.common.SettingsScreensScaffold import com.tangem.wallet.R @Composable -fun ResetCardScreen(state: ResetCardScreenState, onBackClick: () -> Unit) { +internal fun ResetCardScreen(state: ResetCardScreenState, onBackClick: () -> Unit, modifier: Modifier = Modifier) { SettingsScreensScaffold( + modifier = modifier, content = { ResetCardView(state = state) }, onBackClick = onBackClick, - backgroundColor = Color.Transparent, ) } @Suppress("LongMethod", "MagicNumber") @Composable -fun ResetCardView(state: ResetCardScreenState) { +private fun ResetCardView(state: ResetCardScreenState) { Column( modifier = Modifier .fillMaxSize() diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardScreenState.kt b/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardScreenState.kt index fee3e41a5b..b4f297d11c 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardScreenState.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardScreenState.kt @@ -2,7 +2,7 @@ package com.tangem.tap.features.details.ui.resetcard import com.tangem.tap.features.details.ui.cardsettings.TextReference -data class ResetCardScreenState( +internal data class ResetCardScreenState( val accepted: Boolean = false, val descriptionText: TextReference, val onAcceptWarningToggleClick: (Boolean) -> Unit, diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardViewModel.kt b/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardViewModel.kt index 9ba899bc7d..b694554734 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardViewModel.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardViewModel.kt @@ -7,7 +7,7 @@ import com.tangem.tap.features.details.ui.cardsettings.TextReference import com.tangem.tap.features.details.ui.utils.toResetCardDescriptionText import org.rekotlin.Store -class ResetCardViewModel(private val store: Store) { +internal class ResetCardViewModel(private val store: Store) { fun updateState(state: CardSettingsState?): ResetCardScreenState { val descriptionText = state?.cardInfo diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/securitymode/SecurityModeFragment.kt b/app/src/main/java/com/tangem/tap/features/details/ui/securitymode/SecurityModeFragment.kt index c3b2015297..92b2cc3f50 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/securitymode/SecurityModeFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/securitymode/SecurityModeFragment.kt @@ -1,47 +1,45 @@ package com.tangem.tap.features.details.ui.securitymode -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup +import androidx.compose.runtime.Composable import androidx.compose.runtime.MutableState import androidx.compose.runtime.mutableStateOf -import androidx.compose.ui.platform.ComposeView -import androidx.fragment.app.Fragment +import androidx.compose.ui.Modifier import androidx.transition.TransitionInflater import com.tangem.core.navigation.NavigationAction -import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.screen.ComposeFragment +import com.tangem.core.ui.theme.AppThemeModeHolder import com.tangem.tap.features.details.redux.DetailsState import com.tangem.tap.store +import com.tangem.wallet.R +import dagger.hilt.android.AndroidEntryPoint import org.rekotlin.StoreSubscriber +import javax.inject.Inject -class SecurityModeFragment : Fragment(), StoreSubscriber { +@AndroidEntryPoint +internal class SecurityModeFragment : ComposeFragment(), StoreSubscriber { + + @Inject + override lateinit var appThemeModeHolder: AppThemeModeHolder private val viewModel = SecurityModeViewModel(store) private var screenState: MutableState = mutableStateOf(viewModel.updateState(store.state.detailsState.cardSettingsState?.manageSecurityState)) - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - - val inflater = TransitionInflater.from(requireContext()) - enterTransition = inflater.inflateTransition(android.R.transition.fade) - exitTransition = inflater.inflateTransition(android.R.transition.fade) + @Composable + override fun ScreenContent(modifier: Modifier) { + SecurityModeScreen( + modifier = modifier, + state = screenState.value, + onBackClick = { store.dispatch(NavigationAction.PopBackTo()) }, + ) } - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { - return ComposeView(requireContext()).apply { - setContent { - isTransitionGroup = true - TangemTheme { - SecurityModeScreen( - state = screenState.value, - onBackClick = { store.dispatch(NavigationAction.PopBackTo()) }, - ) - } - } - } + override fun TransitionInflater.inflateTransitions(): Boolean { + enterTransition = inflateTransition(R.transition.fade) + exitTransition = inflateTransition(R.transition.fade) + + return true } override fun onStart() { diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/securitymode/SecurityModeScreen.kt b/app/src/main/java/com/tangem/tap/features/details/ui/securitymode/SecurityModeScreen.kt index ba9c01e84d..fe5164bf2f 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/securitymode/SecurityModeScreen.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/securitymode/SecurityModeScreen.kt @@ -1,10 +1,6 @@ package com.tangem.tap.features.details.ui.securitymode -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.* import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.runtime.Composable @@ -20,8 +16,13 @@ import com.tangem.tap.features.details.ui.common.SettingsScreensScaffold import com.tangem.wallet.R @Composable -fun SecurityModeScreen(state: SecurityModeScreenState, onBackClick: () -> Unit) { +internal fun SecurityModeScreen( + state: SecurityModeScreenState, + onBackClick: () -> Unit, + modifier: Modifier = Modifier, +) { SettingsScreensScaffold( + modifier = modifier, content = { SecurityModeOptions(state = state) }, // titleRes = R.string.card_settings_security_mode, onBackClick = onBackClick, @@ -29,7 +30,7 @@ fun SecurityModeScreen(state: SecurityModeScreenState, onBackClick: () -> Unit) } @Composable -fun SecurityModeOptions(state: SecurityModeScreenState) { +private fun SecurityModeOptions(state: SecurityModeScreenState) { Column( modifier = Modifier .fillMaxSize() @@ -55,7 +56,7 @@ fun SecurityModeOptions(state: SecurityModeScreenState) { } @Composable -fun SecurityOption(option: SecurityOption, state: SecurityModeScreenState) { +private fun SecurityOption(option: SecurityOption, state: SecurityModeScreenState) { val selected = option == state.selectedSecurityMode val title = option.toTitleRes() diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/securitymode/SecurityModeScreenState.kt b/app/src/main/java/com/tangem/tap/features/details/ui/securitymode/SecurityModeScreenState.kt index 97482c7c01..27c7629382 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/securitymode/SecurityModeScreenState.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/securitymode/SecurityModeScreenState.kt @@ -3,7 +3,7 @@ package com.tangem.tap.features.details.ui.securitymode import com.tangem.tap.features.details.redux.SecurityOption import com.tangem.wallet.R -data class SecurityModeScreenState( +internal data class SecurityModeScreenState( val availableOptions: List, val selectedSecurityMode: SecurityOption, val isSaveChangesEnabled: Boolean, @@ -11,7 +11,7 @@ data class SecurityModeScreenState( val onSaveChangesClicked: () -> Unit, ) -fun SecurityOption.toTitleRes(): Int { +internal fun SecurityOption.toTitleRes(): Int { return when (this) { SecurityOption.LongTap -> R.string.details_manage_security_long_tap SecurityOption.PassCode -> R.string.details_manage_security_passcode diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/securitymode/SecurityModeViewModel.kt b/app/src/main/java/com/tangem/tap/features/details/ui/securitymode/SecurityModeViewModel.kt index a0666ff19d..7aa863a7df 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/securitymode/SecurityModeViewModel.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/securitymode/SecurityModeViewModel.kt @@ -6,7 +6,7 @@ import com.tangem.tap.features.details.redux.ManageSecurityState import com.tangem.tap.features.details.redux.SecurityOption import org.rekotlin.Store -class SecurityModeViewModel(val store: Store) { +internal class SecurityModeViewModel(val store: Store) { fun updateState(state: ManageSecurityState?): SecurityModeScreenState { if (state == null) { diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/WalletConnectFragment.kt b/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/WalletConnectFragment.kt index 2c7ebdea6e..eff4598340 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/WalletConnectFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/WalletConnectFragment.kt @@ -1,24 +1,30 @@ package com.tangem.tap.features.details.ui.walletconnect import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup +import androidx.compose.runtime.Composable import androidx.compose.runtime.MutableState import androidx.compose.runtime.mutableStateOf -import androidx.compose.ui.platform.ComposeView -import androidx.fragment.app.Fragment +import androidx.compose.ui.Modifier import androidx.transition.TransitionInflater import com.tangem.core.analytics.Analytics import com.tangem.core.navigation.NavigationAction -import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.screen.ComposeFragment +import com.tangem.core.ui.theme.AppThemeModeHolder import com.tangem.tap.common.analytics.events.WalletConnect import com.tangem.tap.features.details.redux.walletconnect.WalletConnectAction import com.tangem.tap.features.details.redux.walletconnect.WalletConnectState import com.tangem.tap.store +import com.tangem.wallet.R +import dagger.hilt.android.AndroidEntryPoint import org.rekotlin.StoreSubscriber +import javax.inject.Inject + +@AndroidEntryPoint +internal class WalletConnectFragment : ComposeFragment(), StoreSubscriber { + + @Inject + override lateinit var appThemeModeHolder: AppThemeModeHolder -class WalletConnectFragment : Fragment(), StoreSubscriber { private val viewModel = WalletConnectViewModel(store) private var screenState: MutableState = mutableStateOf(viewModel.updateState(store.state.walletConnectState)) @@ -26,32 +32,31 @@ class WalletConnectFragment : Fragment(), StoreSubscriber { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) Analytics.send(WalletConnect.ScreenOpened()) - val inflater = TransitionInflater.from(requireContext()) - enterTransition = inflater.inflateTransition(android.R.transition.fade) - exitTransition = inflater.inflateTransition(android.R.transition.fade) } - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { - return ComposeView(requireContext()).apply { - setContent { - isTransitionGroup = true - TangemTheme { - WalletConnectScreen( - state = screenState.value, - onBackClick = { - if (screenState.value.isLoading) { - store.dispatch( - WalletConnectAction.FailureEstablishingSession( - store.state.walletConnectState.newSessionData?.session?.session, - ), - ) - } - store.dispatch(NavigationAction.PopBackTo()) - }, + @Composable + override fun ScreenContent(modifier: Modifier) { + WalletConnectScreen( + modifier = modifier, + state = screenState.value, + onBackClick = { + if (screenState.value.isLoading) { + store.dispatch( + WalletConnectAction.FailureEstablishingSession( + store.state.walletConnectState.newSessionData?.session?.session, + ), ) } - } - } + store.dispatch(NavigationAction.PopBackTo()) + }, + ) + } + + override fun TransitionInflater.inflateTransitions(): Boolean { + enterTransition = inflateTransition(R.transition.fade) + exitTransition = inflateTransition(R.transition.fade) + + return true } override fun onStart() { diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/WalletConnectScreen.kt b/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/WalletConnectScreen.kt index c9f4292991..409b63fb8b 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/WalletConnectScreen.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/WalletConnectScreen.kt @@ -26,10 +26,15 @@ import com.tangem.wallet.R import kotlinx.collections.immutable.persistentListOf @Composable -fun WalletConnectScreen(state: WalletConnectScreenState, onBackClick: () -> Unit) { +internal fun WalletConnectScreen( + state: WalletConnectScreenState, + onBackClick: () -> Unit, + modifier: Modifier = Modifier, +) { val context = LocalContext.current SettingsScreensScaffold( + modifier = modifier, content = { if (state.sessions.isEmpty()) { EmptyScreen(state) diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/WalletConnectScreenState.kt b/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/WalletConnectScreenState.kt index 24878f3183..d0a711e3f0 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/WalletConnectScreenState.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/WalletConnectScreenState.kt @@ -3,7 +3,7 @@ package com.tangem.tap.features.details.ui.walletconnect import com.tangem.tap.features.details.redux.walletconnect.WalletConnectSession import kotlinx.collections.immutable.ImmutableList -data class WalletConnectScreenState( +internal data class WalletConnectScreenState( val sessions: ImmutableList, val isLoading: Boolean = false, val onRemoveSession: (String) -> Unit = {}, diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/WalletConnectViewModel.kt b/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/WalletConnectViewModel.kt index 4707547c51..9713a5da28 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/WalletConnectViewModel.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/WalletConnectViewModel.kt @@ -8,7 +8,7 @@ import kotlinx.collections.immutable.toImmutableList import org.rekotlin.Store import timber.log.Timber -class WalletConnectViewModel(private val store: Store) { +internal class WalletConnectViewModel(private val store: Store) { fun updateState(state: WalletConnectState): WalletConnectScreenState { Timber.d("WC2 Sessions: ${state.wc2Sessions}") val sessions = state.sessions.map { wcSession -> WcSessionForScreen.fromSession(wcSession) } + state.wc2Sessions From ed23ec21bdeda9492822e7cbd9a6d69082e5a36f Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 1 Sep 2023 13:30:17 +0400 Subject: [PATCH 008/242] Updated on 2026-08-14 --- .../disclaimer/ui/DisclaimerFragment.kt | 2 +- .../products/twins/ui/TwinsCardsFragment.kt | 2 +- .../stateSubscribers/SendStateSubscriber.kt | 2 +- .../res/color/selector_chip_background.xml | 10 ++--- .../main/res/color/selector_chip_stroke.xml | 10 ++--- app/src/main/res/color/selector_chip_text.xml | 9 ++++ .../color/selector_edit_text_secondary.xml | 5 +++ .../main/res/drawable/ic_arrows_up_down.xml | 2 +- app/src/main/res/drawable/ic_paste.xml | 2 +- .../main/res/drawable/ic_paste_disabled.xml | 2 +- app/src/main/res/drawable/ic_qr_code_scan.xml | 2 +- app/src/main/res/drawable/shape_ellipse.xml | 2 +- .../layout_onboarding_container_bottom.xml | 2 +- .../main/res/layout/btn_expand_collapse.xml | 3 +- .../dialog_russians_cardholders_warning.xml | 4 +- .../main/res/layout/dialog_wallet_trade.xml | 8 ++-- .../main/res/layout/fragment_disclaimer.xml | 4 +- .../res/layout/fragment_onboarding_main.xml | 2 +- .../res/layout/fragment_onboarding_wallet.xml | 4 +- app/src/main/res/layout/fragment_send.xml | 10 +++-- app/src/main/res/layout/fragment_shop.xml | 4 +- .../layout_onboarding_container_bottom.xml | 2 +- .../main/res/layout/layout_pseudo_toolbar.xml | 2 +- .../main/res/layout/layout_receipt_total.xml | 6 +-- .../main/res/layout/layout_send_address.xml | 34 +++++++++------ .../main/res/layout/layout_send_amount.xml | 9 ++-- app/src/main/res/layout/layout_send_fee.xml | 1 + .../main/res/layout/layout_send_receipt.xml | 4 +- .../res/layout/test_leapfrog_fragment.xml | 2 +- .../main/res/layout/view_currency_icon.xml | 2 +- app/src/main/res/values-night/colors.xml | 42 +++++++++--------- app/src/main/res/values/colors.xml | 11 +++-- app/src/main/res/values/styles.xml | 43 ++++++++++++++----- 33 files changed, 147 insertions(+), 102 deletions(-) create mode 100644 app/src/main/res/color/selector_chip_text.xml create mode 100644 app/src/main/res/color/selector_edit_text_secondary.xml diff --git a/app/src/main/java/com/tangem/tap/features/disclaimer/ui/DisclaimerFragment.kt b/app/src/main/java/com/tangem/tap/features/disclaimer/ui/DisclaimerFragment.kt index 930f33afe6..21d6804bb5 100644 --- a/app/src/main/java/com/tangem/tap/features/disclaimer/ui/DisclaimerFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/disclaimer/ui/DisclaimerFragment.kt @@ -56,7 +56,7 @@ class DisclaimerFragment : BaseFragment(R.layout.fragment_disclaimer), StoreSubs override fun onStart() { super.onStart() - setStatusBarColor(R.color.backgroundLightGray) + setStatusBarColor(R.color.background_secondary) webViewClient.onProgressStateChanged = { store.dispatch(DisclaimerAction.OnProgressStateChanged(it)) } store.subscribe(subscriber = this) { state -> diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/ui/TwinsCardsFragment.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/ui/TwinsCardsFragment.kt index a4b12b434c..ddcdc4119b 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/ui/TwinsCardsFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/ui/TwinsCardsFragment.kt @@ -105,7 +105,7 @@ class TwinsCardsFragment : BaseOnboardingFragment() { override fun onStart() { super.onStart() - setStatusBarColor(R.color.backgroundWhite) + setStatusBarColor(R.color.background_primary) } private fun reconfigureLayoutForTwins(containerBinding: LayoutOnboardingContainerTopBinding) = diff --git a/app/src/main/java/com/tangem/tap/features/send/ui/stateSubscribers/SendStateSubscriber.kt b/app/src/main/java/com/tangem/tap/features/send/ui/stateSubscribers/SendStateSubscriber.kt index 6de86eba68..568f1529d0 100644 --- a/app/src/main/java/com/tangem/tap/features/send/ui/stateSubscribers/SendStateSubscriber.kt +++ b/app/src/main/java/com/tangem/tap/features/send/ui/stateSubscribers/SendStateSubscriber.kt @@ -256,7 +256,7 @@ class SendStateSubscriber(fragment: BaseStoreFragment) : FragmentStateSubscriber val imageRes = if (state.inputIsEnabled) R.drawable.ic_arrows_up_down else 0 tvAmountCurrency.setCompoundDrawablesWithIntrinsicBounds(0, 0, imageRes, 0) - val textColor = if (state.inputIsEnabled) R.color.blue else R.color.textGray + val textColor = if (state.inputIsEnabled) R.color.accent else R.color.text_secondary tvAmountCurrency.setTextColor(fg.getColor(textColor)) } diff --git a/app/src/main/res/color/selector_chip_background.xml b/app/src/main/res/color/selector_chip_background.xml index e0b0431767..16be87b913 100644 --- a/app/src/main/res/color/selector_chip_background.xml +++ b/app/src/main/res/color/selector_chip_background.xml @@ -1,10 +1,10 @@ - - - + + + - - + + \ No newline at end of file diff --git a/app/src/main/res/color/selector_chip_stroke.xml b/app/src/main/res/color/selector_chip_stroke.xml index e4a5d64acc..c2827e783e 100644 --- a/app/src/main/res/color/selector_chip_stroke.xml +++ b/app/src/main/res/color/selector_chip_stroke.xml @@ -1,9 +1,9 @@ - - - - - + + + + + \ No newline at end of file diff --git a/app/src/main/res/color/selector_chip_text.xml b/app/src/main/res/color/selector_chip_text.xml new file mode 100644 index 0000000000..e205f4e82d --- /dev/null +++ b/app/src/main/res/color/selector_chip_text.xml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/color/selector_edit_text_secondary.xml b/app/src/main/res/color/selector_edit_text_secondary.xml new file mode 100644 index 0000000000..df06469793 --- /dev/null +++ b/app/src/main/res/color/selector_edit_text_secondary.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_arrows_up_down.xml b/app/src/main/res/drawable/ic_arrows_up_down.xml index 6e7d34d5e6..96faa98e71 100644 --- a/app/src/main/res/drawable/ic_arrows_up_down.xml +++ b/app/src/main/res/drawable/ic_arrows_up_down.xml @@ -4,6 +4,6 @@ android:viewportWidth="14" android:viewportHeight="19"> diff --git a/app/src/main/res/drawable/ic_paste.xml b/app/src/main/res/drawable/ic_paste.xml index c49d12dc4b..b971249451 100644 --- a/app/src/main/res/drawable/ic_paste.xml +++ b/app/src/main/res/drawable/ic_paste.xml @@ -4,6 +4,6 @@ android:viewportWidth="16" android:viewportHeight="19"> diff --git a/app/src/main/res/drawable/ic_paste_disabled.xml b/app/src/main/res/drawable/ic_paste_disabled.xml index 7666c5a52d..156ddafc4b 100644 --- a/app/src/main/res/drawable/ic_paste_disabled.xml +++ b/app/src/main/res/drawable/ic_paste_disabled.xml @@ -6,6 +6,6 @@ android:viewportHeight="19"> \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_qr_code_scan.xml b/app/src/main/res/drawable/ic_qr_code_scan.xml index 34a2bf62a8..5ec455e546 100644 --- a/app/src/main/res/drawable/ic_qr_code_scan.xml +++ b/app/src/main/res/drawable/ic_qr_code_scan.xml @@ -5,5 +5,5 @@ android:viewportHeight="18"> + android:fillColor="@color/text_primary_1" /> diff --git a/app/src/main/res/drawable/shape_ellipse.xml b/app/src/main/res/drawable/shape_ellipse.xml index b5f3fc7939..ca6cad24b5 100644 --- a/app/src/main/res/drawable/shape_ellipse.xml +++ b/app/src/main/res/drawable/shape_ellipse.xml @@ -10,7 +10,7 @@ diff --git a/app/src/main/res/layout-h680dp/layout_onboarding_container_bottom.xml b/app/src/main/res/layout-h680dp/layout_onboarding_container_bottom.xml index 32d2544981..ee7f2b2cb0 100644 --- a/app/src/main/res/layout-h680dp/layout_onboarding_container_bottom.xml +++ b/app/src/main/res/layout-h680dp/layout_onboarding_container_bottom.xml @@ -85,7 +85,7 @@ android:layout_gravity="center" android:elevation="18dp" android:indeterminate="true" - android:indeterminateTint="@color/backgroundLightGray" + android:indeterminateTint="@color/background_secondary" android:visibility="invisible" /> diff --git a/app/src/main/res/layout/btn_expand_collapse.xml b/app/src/main/res/layout/btn_expand_collapse.xml index 76f0338c98..8d9c220409 100644 --- a/app/src/main/res/layout/btn_expand_collapse.xml +++ b/app/src/main/res/layout/btn_expand_collapse.xml @@ -18,6 +18,7 @@ android:layout_gravity="center" android:background="?selectableItemBackgroundBorderless" android:padding="5dp" - app:srcCompat="@drawable/ic_angle_bracket_up" /> + app:srcCompat="@drawable/ic_angle_bracket_up" + app:tint="@color/icon_primary_1" /> \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_russians_cardholders_warning.xml b/app/src/main/res/layout/dialog_russians_cardholders_warning.xml index 29bf1c052b..ae04b02d83 100644 --- a/app/src/main/res/layout/dialog_russians_cardholders_warning.xml +++ b/app/src/main/res/layout/dialog_russians_cardholders_warning.xml @@ -4,7 +4,7 @@ xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="wrap_content" - android:background="@color/backgroundWhite" + android:background="@color/background_primary" android:minHeight="420dp" tools:layout_gravity="bottom"> @@ -25,7 +25,7 @@ android:layout_width="32dp" android:layout_height="32dp" android:background="@drawable/shape_circle" - android:backgroundTint="@color/backgroundWhite" + android:backgroundTint="@color/background_primary" app:layout_constraintBottom_toBottomOf="@id/iv_cross" app:layout_constraintEnd_toEndOf="@id/iv_cross" app:layout_constraintStart_toStartOf="@id/iv_cross" diff --git a/app/src/main/res/layout/dialog_wallet_trade.xml b/app/src/main/res/layout/dialog_wallet_trade.xml index dea867e05a..96dd6d34c1 100644 --- a/app/src/main/res/layout/dialog_wallet_trade.xml +++ b/app/src/main/res/layout/dialog_wallet_trade.xml @@ -11,7 +11,7 @@ android:layout_height="56dp" android:padding="16dp" android:text="@string/wallet_choose_trade_action" - android:textColor="@color/darkGray2" + android:textColor="@color/text_secondary" android:textSize="14sp" /> @@ -39,7 +39,7 @@ android:gravity="center_vertical" android:padding="16dp" android:text="@string/common_sell" - android:textColor="@color/darkGray3" + android:textColor="@color/text_primary_1" android:textSize="14sp" android:textStyle="bold" app:drawableStartCompat="@drawable/ic_arrow_down_24" /> @@ -54,7 +54,7 @@ android:gravity="center_vertical" android:padding="16dp" android:text="@string/swapping_swap_action" - android:textColor="@color/darkGray3" + android:textColor="@color/text_primary_1" android:textSize="14sp" android:textStyle="bold" app:drawableStartCompat="@drawable/ic_exchange_vertical_24" /> diff --git a/app/src/main/res/layout/fragment_disclaimer.xml b/app/src/main/res/layout/fragment_disclaimer.xml index 328a435e5c..136a258b23 100644 --- a/app/src/main/res/layout/fragment_disclaimer.xml +++ b/app/src/main/res/layout/fragment_disclaimer.xml @@ -4,7 +4,7 @@ android:id="@+id/coordinator_details_confirm" android:layout_width="match_parent" android:layout_height="match_parent" - android:background="@color/backgroundLightGray" + android:background="@color/background_secondary" android:orientation="vertical"> diff --git a/app/src/main/res/layout/fragment_onboarding_main.xml b/app/src/main/res/layout/fragment_onboarding_main.xml index c88f910dcb..bc1cafb403 100644 --- a/app/src/main/res/layout/fragment_onboarding_main.xml +++ b/app/src/main/res/layout/fragment_onboarding_main.xml @@ -4,7 +4,7 @@ android:id="@+id/coordinator_onboarding" android:layout_width="match_parent" android:layout_height="match_parent" - android:background="@color/backgroundWhite" + android:background="@color/background_primary" android:clipChildren="false" android:clipToPadding="false" android:fitsSystemWindows="true" diff --git a/app/src/main/res/layout/fragment_onboarding_wallet.xml b/app/src/main/res/layout/fragment_onboarding_wallet.xml index 821f0d3ef8..0080b81937 100644 --- a/app/src/main/res/layout/fragment_onboarding_wallet.xml +++ b/app/src/main/res/layout/fragment_onboarding_wallet.xml @@ -5,7 +5,7 @@ android:id="@+id/coordinator_details_confirm" android:layout_width="match_parent" android:layout_height="match_parent" - android:background="@color/backgroundLightGray" + android:background="@color/background_secondary" android:clipChildren="false" android:clipToPadding="false" android:orientation="vertical"> @@ -183,7 +183,7 @@ android:layout_width="match_parent" android:layout_height="10dp" android:layout_marginBottom="16dp" - android:background="@color/backgroundLightGray" + android:background="@color/background_secondary" android:visibility="gone" app:layout_constraintBottom_toTopOf="@id/layout_buttons_common" app:layout_constraintEnd_toEndOf="parent" diff --git a/app/src/main/res/layout/fragment_send.xml b/app/src/main/res/layout/fragment_send.xml index 20949f4932..a7f8c54c42 100644 --- a/app/src/main/res/layout/fragment_send.xml +++ b/app/src/main/res/layout/fragment_send.xml @@ -5,7 +5,7 @@ android:id="@+id/coordinator_wallet" android:layout_width="match_parent" android:layout_height="match_parent" - android:background="@color/backgroundLightGray" + android:background="@color/background_secondary" android:orientation="vertical"> @@ -22,7 +22,9 @@ android:layout_width="match_parent" android:layout_height="?attr/actionBarSize" app:navigationIcon="@drawable/ic_baseline_arrow_back_24" - app:title="@string/common_send" /> + app:navigationIconTint="@color/text_primary_1" + app:title="@string/common_send" + app:titleTextColor="@color/text_primary_1" /> @@ -125,7 +127,7 @@ android:layout_gravity="center" android:elevation="18dp" android:indeterminate="true" - android:indeterminateTint="@color/backgroundLightGray" /> + android:indeterminateTint="@color/background_primary" /> diff --git a/app/src/main/res/layout/fragment_shop.xml b/app/src/main/res/layout/fragment_shop.xml index 3d6e2f4020..48384d34c6 100644 --- a/app/src/main/res/layout/fragment_shop.xml +++ b/app/src/main/res/layout/fragment_shop.xml @@ -5,7 +5,7 @@ android:id="@+id/coordinator_details_confirm" android:layout_width="match_parent" android:layout_height="match_parent" - android:background="@color/backgroundLightGray" + android:background="@color/background_secondary" android:clipChildren="false" android:clipToPadding="false" android:focusableInTouchMode="true" @@ -16,7 +16,7 @@ style="@style/ThemeOverlay.MyTheme.Toolbar.AccentColorMenu" android:layout_width="match_parent" android:layout_height="wrap_content" - android:background="@color/backgroundLightGray" + android:background="@color/background_secondary" android:fitsSystemWindows="true" app:liftOnScroll="true"> diff --git a/app/src/main/res/layout/layout_onboarding_container_bottom.xml b/app/src/main/res/layout/layout_onboarding_container_bottom.xml index 187018cab1..8f92cb1daa 100644 --- a/app/src/main/res/layout/layout_onboarding_container_bottom.xml +++ b/app/src/main/res/layout/layout_onboarding_container_bottom.xml @@ -85,7 +85,7 @@ android:layout_gravity="center" android:elevation="18dp" android:indeterminate="true" - android:indeterminateTint="@color/backgroundLightGray" + android:indeterminateTint="@color/background_secondary" android:visibility="invisible" /> diff --git a/app/src/main/res/layout/layout_pseudo_toolbar.xml b/app/src/main/res/layout/layout_pseudo_toolbar.xml index c1bc683d3a..bb2a5ce723 100644 --- a/app/src/main/res/layout/layout_pseudo_toolbar.xml +++ b/app/src/main/res/layout/layout_pseudo_toolbar.xml @@ -4,7 +4,7 @@ android:id="@+id/pseudo_toolbar" android:layout_width="match_parent" android:layout_height="?attr/actionBarSize" - android:background="@color/backgroundLightGray" + android:background="@color/background_secondary" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent"> diff --git a/app/src/main/res/layout/layout_receipt_total.xml b/app/src/main/res/layout/layout_receipt_total.xml index 7cca4f9165..5ff5d3377c 100644 --- a/app/src/main/res/layout/layout_receipt_total.xml +++ b/app/src/main/res/layout/layout_receipt_total.xml @@ -47,7 +47,7 @@ android:layout_gravity="end" android:layout_marginTop="4dp" android:gravity="end" - android:textColor="@color/darkGray1" + android:textColor="@color/text_tertiary" tools:text="123.29837729 ADA 39487593.109342039402938049 will be sent" /> @@ -64,7 +64,7 @@ android:layout_height="wrap_content" android:layout_gravity="start" android:text="@string/send_total_label" - android:textColor="@color/darkGray1" + android:textColor="@color/text_tertiary" android:textSize="14sp" android:textStyle="bold" /> @@ -74,7 +74,7 @@ android:layout_height="wrap_content" android:layout_gravity="end" android:textAllCaps="true" - android:textColor="@color/darkGray1" + android:textColor="@color/text_tertiary" android:textSize="14sp" android:textStyle="bold" tools:text="usd" /> diff --git a/app/src/main/res/layout/layout_send_address.xml b/app/src/main/res/layout/layout_send_address.xml index c6dd2873c8..8367bf3524 100644 --- a/app/src/main/res/layout/layout_send_address.xml +++ b/app/src/main/res/layout/layout_send_address.xml @@ -21,23 +21,24 @@ android:id="@+id/tilAddress" android:layout_width="0dp" android:layout_height="wrap_content" - app:boxBackgroundColor="@color/backgroundLightGray" app:errorIconDrawable="@null" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" + style="@style/SecondaryTextInputLayout" app:layout_constraintTop_toTopOf="parent"> @@ -67,10 +68,10 @@ android:id="@+id/flQrCode" android:layout_width="@dimen/btn_rounded_size" android:layout_height="@dimen/btn_rounded_size" - android:layout_marginTop="10dp" android:background="@drawable/shape_ellipse" app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintTop_toTopOf="@+id/tilAddress"> + android:layout_marginTop="4dp" + app:layout_constraintTop_toTopOf="parent"> @@ -74,7 +75,7 @@ android:drawablePadding="10dp" android:fontFamily="sans-serif-light" android:textAllCaps="true" - android:textColor="@color/blue" + android:textColor="@color/accent" android:textSize="32sp" app:drawableEndCompat="@drawable/ic_arrows_up_down" app:layout_constraintEnd_toEndOf="parent" @@ -88,7 +89,7 @@ android:layout_gravity="end" android:layout_marginTop="8dp" android:layout_marginEnd="16dp" - android:textColor="@color/darkGray1" + android:textColor="@color/text_tertiary" android:textSize="16sp" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toBottomOf="@+id/flAmountToSend" /> diff --git a/app/src/main/res/layout/layout_send_fee.xml b/app/src/main/res/layout/layout_send_fee.xml index f54f8f8bfe..bba30587f9 100644 --- a/app/src/main/res/layout/layout_send_fee.xml +++ b/app/src/main/res/layout/layout_send_fee.xml @@ -85,6 +85,7 @@ android:layout_marginStart="16dp" android:layout_marginEnd="8dp" android:text="@string/send_fee_include_description" + android:textColor="@color/text_primary_1" android:textSize="13sp" /> diff --git a/app/src/main/res/layout/layout_send_receipt.xml b/app/src/main/res/layout/layout_send_receipt.xml index 2cf18b1ed5..4a8fab6816 100644 --- a/app/src/main/res/layout/layout_send_receipt.xml +++ b/app/src/main/res/layout/layout_send_receipt.xml @@ -34,7 +34,7 @@ android:layout_height="wrap_content" android:layout_marginTop="8dp" android:text="@string/send_fee_label" - android:textColor="@color/darkGray1" + android:textColor="@color/text_tertiary" android:textStyle="bold" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/tvReceiptAmount" /> @@ -59,7 +59,7 @@ android:layout_height="wrap_content" android:layout_gravity="end" android:textAllCaps="true" - android:textColor="@color/darkGray1" + android:textColor="@color/text_tertiary" android:textStyle="bold" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toTopOf="@+id/tvReceiptFee" diff --git a/app/src/main/res/layout/test_leapfrog_fragment.xml b/app/src/main/res/layout/test_leapfrog_fragment.xml index 52b720ad27..9e0865e85a 100644 --- a/app/src/main/res/layout/test_leapfrog_fragment.xml +++ b/app/src/main/res/layout/test_leapfrog_fragment.xml @@ -4,7 +4,7 @@ android:id="@+id/coordinator_onboarding" android:layout_width="match_parent" android:layout_height="match_parent" - android:background="@color/backgroundWhite" + android:background="@color/background_primary" android:clipChildren="false" android:fitsSystemWindows="true" android:orientation="vertical"> diff --git a/app/src/main/res/layout/view_currency_icon.xml b/app/src/main/res/layout/view_currency_icon.xml index aa1330c398..7e76b227e7 100644 --- a/app/src/main/res/layout/view_currency_icon.xml +++ b/app/src/main/res/layout/view_currency_icon.xml @@ -45,7 +45,7 @@ android:layout_width="18dp" android:layout_height="18dp" android:background="@drawable/shape_circle" - android:backgroundTint="@color/backgroundWhite" + android:backgroundTint="@color/background_primary" android:contentDescription="@null" android:padding="2dp" android:visibility="gone" diff --git a/app/src/main/res/values-night/colors.xml b/app/src/main/res/values-night/colors.xml index 9be291c700..75ebc64343 100644 --- a/app/src/main/res/values-night/colors.xml +++ b/app/src/main/res/values-night/colors.xml @@ -2,32 +2,32 @@ - + #C9C9C9 - - + #1E1E1E + #000000 - + #1E1E1E - - + #F5F5F5 + #303030 - + #1ACE80 - - - - - - - - + #1ACE80 + #FFB71B + #3B3B3B + #656565 + #FFFFFF + #1E1E1E + #919191 + #FF5B5B @@ -36,13 +36,13 @@ - + #FFB71B - - - - - + #494949 + #FFFFFF + #1E1E1E + #B0B0B0 + #919191 \ No newline at end of file diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index 3509ec1ee5..733c9d3d07 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -3,7 +3,7 @@ #0029FF #0022D4 - #19C878 + #0099FF @color/accent @@ -11,7 +11,7 @@ #FFB71B - @color/backgroundLightGray + @color/background_secondary #FFFFFF #F3F3F3 @@ -19,12 +19,10 @@ #D1D1D6 #8E8E90 #666668 - #48484A + #3A3A3C #1C1C1E - #FFFFFF - #F9F9F9 #F4F5F6 #DE000000 @@ -61,9 +59,10 @@ #FFB71B #C9C9C9 #B0B0B0 + #1E1E1E #FFFFFF #656565 - #DE1010 + #FF3333 #FFB71B diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index 70af1872c5..f97ed69d87 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -3,11 +3,15 @@ @@ -22,15 +26,21 @@ + + + + + From 5dd09c485a1eb88c24f81e098a0ff8200023a73e Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 1 Sep 2023 17:02:26 +0300 Subject: [PATCH 009/242] Updated on 2026-08-14 --- core/datasource/build.gradle.kts | 1 + .../datasource/di/AppThemeModeDataModule.kt | 29 +++++++++++++++ .../DefaultSelectedAppCurrencyStore.kt | 6 +--- .../local/apptheme/AppThemeModeStore.kt | 13 +++++++ .../apptheme/DefaultAppThemeModeStore.kt | 9 +++++ .../core/KeylessDataStoreDecorator.kt | 4 +++ data/app-theme/build.gradle.kts | 4 --- .../apptheme/DefaultAppThemeModeRepository.kt | 35 +++++++++++++++++++ .../apptheme/MockAppThemeModeRepository.kt | 19 ---------- .../apptheme/di/AppThemeModeDataModule.kt | 11 ++++-- 10 files changed, 100 insertions(+), 31 deletions(-) create mode 100644 core/datasource/src/main/java/com/tangem/datasource/di/AppThemeModeDataModule.kt create mode 100644 core/datasource/src/main/java/com/tangem/datasource/local/apptheme/AppThemeModeStore.kt create mode 100644 core/datasource/src/main/java/com/tangem/datasource/local/apptheme/DefaultAppThemeModeStore.kt create mode 100644 data/app-theme/src/main/kotlin/com/tangem/data/apptheme/DefaultAppThemeModeRepository.kt delete mode 100644 data/app-theme/src/main/kotlin/com/tangem/data/apptheme/MockAppThemeModeRepository.kt diff --git a/core/datasource/build.gradle.kts b/core/datasource/build.gradle.kts index f304966f2a..3ab87d91ac 100644 --- a/core/datasource/build.gradle.kts +++ b/core/datasource/build.gradle.kts @@ -11,6 +11,7 @@ dependencies { /** Project */ implementation(projects.core.utils) implementation(projects.libs.auth) + implementation(projects.domain.appTheme.models) implementation(projects.domain.tokens.models) implementation(projects.domain.wallets.models) diff --git a/core/datasource/src/main/java/com/tangem/datasource/di/AppThemeModeDataModule.kt b/core/datasource/src/main/java/com/tangem/datasource/di/AppThemeModeDataModule.kt new file mode 100644 index 0000000000..2041fdee99 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/di/AppThemeModeDataModule.kt @@ -0,0 +1,29 @@ +package com.tangem.datasource.di + +import android.content.Context +import com.squareup.moshi.Moshi +import com.tangem.datasource.local.apptheme.AppThemeModeStore +import com.tangem.datasource.local.apptheme.DefaultAppThemeModeStore +import com.tangem.datasource.local.datastore.SharedPreferencesDataStore +import com.tangem.domain.apptheme.model.AppThemeMode +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.qualifiers.ApplicationContext +import dagger.hilt.components.SingletonComponent + +@Module +@InstallIn(SingletonComponent::class) +internal object AppThemeModeDataModule { + + @Provides + fun provideAppThemeModeStore(@ApplicationContext context: Context, @NetworkMoshi moshi: Moshi): AppThemeModeStore { + return DefaultAppThemeModeStore( + dataStore = SharedPreferencesDataStore( + preferencesName = "app_theme", + context = context, + adapter = moshi.adapter(AppThemeMode::class.java), + ), + ) + } +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/appcurrency/implementation/DefaultSelectedAppCurrencyStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/appcurrency/implementation/DefaultSelectedAppCurrencyStore.kt index b6d8f61ade..c79bf3045b 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/appcurrency/implementation/DefaultSelectedAppCurrencyStore.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/appcurrency/implementation/DefaultSelectedAppCurrencyStore.kt @@ -7,8 +7,4 @@ import com.tangem.datasource.local.datastore.core.StringKeyDataStore internal class DefaultSelectedAppCurrencyStore( dataStore: StringKeyDataStore, -) : SelectedAppCurrencyStore, KeylessDataStoreDecorator(dataStore) { - override suspend fun isEmpty(): Boolean { - return getSyncOrNull() == null - } -} \ No newline at end of file +) : SelectedAppCurrencyStore, KeylessDataStoreDecorator(dataStore) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/apptheme/AppThemeModeStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/apptheme/AppThemeModeStore.kt new file mode 100644 index 0000000000..78e1fd514c --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/local/apptheme/AppThemeModeStore.kt @@ -0,0 +1,13 @@ +package com.tangem.datasource.local.apptheme + +import com.tangem.domain.apptheme.model.AppThemeMode +import kotlinx.coroutines.flow.Flow + +interface AppThemeModeStore { + + fun get(): Flow + + suspend fun store(item: AppThemeMode) + + suspend fun isEmpty(): Boolean +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/apptheme/DefaultAppThemeModeStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/apptheme/DefaultAppThemeModeStore.kt new file mode 100644 index 0000000000..483bf4b525 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/local/apptheme/DefaultAppThemeModeStore.kt @@ -0,0 +1,9 @@ +package com.tangem.datasource.local.apptheme + +import com.tangem.datasource.local.datastore.core.KeylessDataStoreDecorator +import com.tangem.datasource.local.datastore.core.StringKeyDataStore +import com.tangem.domain.apptheme.model.AppThemeMode + +internal class DefaultAppThemeModeStore( + dataStore: StringKeyDataStore, +) : AppThemeModeStore, KeylessDataStoreDecorator(dataStore) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/datastore/core/KeylessDataStoreDecorator.kt b/core/datasource/src/main/java/com/tangem/datasource/local/datastore/core/KeylessDataStoreDecorator.kt index 5e74e2f386..6010c456b5 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/datastore/core/KeylessDataStoreDecorator.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/datastore/core/KeylessDataStoreDecorator.kt @@ -22,6 +22,10 @@ internal abstract class KeylessDataStoreDecorator( store(Unit, item) } + open suspend fun isEmpty(): Boolean { + return getSyncOrNull() == null + } + private companion object { const val STRING_KEY = "key" } diff --git a/data/app-theme/build.gradle.kts b/data/app-theme/build.gradle.kts index b0f1f17a6a..7996a21c65 100644 --- a/data/app-theme/build.gradle.kts +++ b/data/app-theme/build.gradle.kts @@ -12,13 +12,11 @@ android { dependencies { /** Project - Domain */ - implementation(projects.domain.core) implementation(projects.domain.appTheme) implementation(projects.domain.appTheme.models) /** Project - Data */ implementation(projects.core.datasource) - implementation(projects.data.common) /** Project - Utils */ implementation(projects.core.utils) @@ -29,6 +27,4 @@ dependencies { /** Other */ implementation(deps.kotlin.coroutines) - implementation(deps.timber) - implementation(deps.jodatime) } \ No newline at end of file diff --git a/data/app-theme/src/main/kotlin/com/tangem/data/apptheme/DefaultAppThemeModeRepository.kt b/data/app-theme/src/main/kotlin/com/tangem/data/apptheme/DefaultAppThemeModeRepository.kt new file mode 100644 index 0000000000..4d8ed9cf4c --- /dev/null +++ b/data/app-theme/src/main/kotlin/com/tangem/data/apptheme/DefaultAppThemeModeRepository.kt @@ -0,0 +1,35 @@ +package com.tangem.data.apptheme + +import com.tangem.datasource.local.apptheme.AppThemeModeStore +import com.tangem.domain.apptheme.model.AppThemeMode +import com.tangem.domain.apptheme.repository.AppThemeModeRepository +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.flow.* +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +internal class DefaultAppThemeModeRepository( + private val appThemeModeStore: AppThemeModeStore, + private val dispatchers: CoroutineDispatcherProvider, +) : AppThemeModeRepository { + + override fun getAppThemeMode(): Flow { + return channelFlow { + launch(dispatchers.io) { + if (appThemeModeStore.isEmpty()) { + appThemeModeStore.store(AppThemeMode.DEFAULT) + } + } + + launch(dispatchers.io) { + appThemeModeStore.get().collect(::send) + } + } + } + + override suspend fun changeAppThemeMode(mode: AppThemeMode) { + withContext(dispatchers.io) { + appThemeModeStore.store(mode) + } + } +} \ No newline at end of file diff --git a/data/app-theme/src/main/kotlin/com/tangem/data/apptheme/MockAppThemeModeRepository.kt b/data/app-theme/src/main/kotlin/com/tangem/data/apptheme/MockAppThemeModeRepository.kt deleted file mode 100644 index a4a885aab2..0000000000 --- a/data/app-theme/src/main/kotlin/com/tangem/data/apptheme/MockAppThemeModeRepository.kt +++ /dev/null @@ -1,19 +0,0 @@ -package com.tangem.data.apptheme - -import com.tangem.domain.apptheme.model.AppThemeMode -import com.tangem.domain.apptheme.repository.AppThemeModeRepository -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableStateFlow - -internal class MockAppThemeModeRepository : AppThemeModeRepository { - - private val appThemeModeFlow = MutableStateFlow(AppThemeMode.DEFAULT) - - override fun getAppThemeMode(): Flow { - return appThemeModeFlow - } - - override suspend fun changeAppThemeMode(mode: AppThemeMode) { - appThemeModeFlow.value = mode - } -} \ No newline at end of file diff --git a/data/app-theme/src/main/kotlin/com/tangem/data/apptheme/di/AppThemeModeDataModule.kt b/data/app-theme/src/main/kotlin/com/tangem/data/apptheme/di/AppThemeModeDataModule.kt index 6847fad0b8..a360ba19b6 100644 --- a/data/app-theme/src/main/kotlin/com/tangem/data/apptheme/di/AppThemeModeDataModule.kt +++ b/data/app-theme/src/main/kotlin/com/tangem/data/apptheme/di/AppThemeModeDataModule.kt @@ -1,7 +1,9 @@ package com.tangem.data.apptheme.di -import com.tangem.data.apptheme.MockAppThemeModeRepository +import com.tangem.data.apptheme.DefaultAppThemeModeRepository +import com.tangem.datasource.local.apptheme.AppThemeModeStore import com.tangem.domain.apptheme.repository.AppThemeModeRepository +import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -14,7 +16,10 @@ internal object AppThemeModeDataModule { @Provides @Singleton - fun provideAppThemeModeRepository(): AppThemeModeRepository { - return MockAppThemeModeRepository() + fun provideAppThemeModeRepository( + appThemeModeStore: AppThemeModeStore, + dispatchers: CoroutineDispatcherProvider, + ): AppThemeModeRepository { + return DefaultAppThemeModeRepository(appThemeModeStore, dispatchers) } } \ No newline at end of file From 5a169e6211756916c448a899eb7797ce6136bb76 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 5 Sep 2023 10:00:14 +0300 Subject: [PATCH 010/242] Updated on 2026-08-14 --- .../appsettings/AppSettingsAlertsFactory.kt | 28 ++ .../ui/appsettings/AppSettingsFragment.kt | 8 +- .../ui/appsettings/AppSettingsItemsFactory.kt | 48 ++++ .../ui/appsettings/AppSettingsScreen.kt | 253 +++++------------- .../ui/appsettings/AppSettingsScreenState.kt | 53 +++- .../ui/appsettings/AppSettingsViewModel.kt | 117 ++++++-- .../{EnrollBiometricsCard.kt => CardItem.kt} | 57 ++-- .../components/SettingsAlertDialog.kt | 107 +++----- .../ui/appsettings/components/SwitchItem.kt | 113 ++++++++ .../core/ui/extensions/TextReference.kt | 47 ++++ 10 files changed, 513 insertions(+), 318 deletions(-) create mode 100644 app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsAlertsFactory.kt create mode 100644 app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsItemsFactory.kt rename app/src/main/java/com/tangem/tap/features/details/ui/appsettings/components/{EnrollBiometricsCard.kt => CardItem.kt} (57%) create mode 100644 app/src/main/java/com/tangem/tap/features/details/ui/appsettings/components/SwitchItem.kt diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsAlertsFactory.kt b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsAlertsFactory.kt new file mode 100644 index 0000000000..51cfcc0ad3 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsAlertsFactory.kt @@ -0,0 +1,28 @@ +package com.tangem.tap.features.details.ui.appsettings + +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.tap.features.details.ui.appsettings.AppSettingsScreenState.Alert +import com.tangem.wallet.R + +internal class AppSettingsAlertsFactory { + + fun createDeleteSavedWalletsAlert(onDelete: () -> Unit, onDismiss: () -> Unit): Alert { + return Alert( + title = resourceReference(R.string.common_attention), + description = resourceReference(R.string.app_settings_off_saved_wallet_alert_message), + confirmText = resourceReference(R.string.common_delete), + onConfirm = onDelete, + onDismiss = onDismiss, + ) + } + + fun createDeleteSavedAccessCodesAlert(onDelete: () -> Unit, onDismiss: () -> Unit): Alert { + return Alert( + title = resourceReference(R.string.common_attention), + description = resourceReference(R.string.app_settings_off_saved_access_code_alert_message), + confirmText = resourceReference(R.string.common_delete), + onConfirm = onDelete, + onDismiss = onDismiss, + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsFragment.kt b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsFragment.kt index 8a7e4987df..ef5d881910 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsFragment.kt @@ -2,8 +2,6 @@ package com.tangem.tap.features.details.ui.appsettings import android.os.Bundle import androidx.compose.runtime.Composable -import androidx.compose.runtime.MutableState -import androidx.compose.runtime.mutableStateOf import androidx.compose.ui.Modifier import androidx.transition.TransitionInflater import com.tangem.core.navigation.NavigationAction @@ -24,14 +22,12 @@ internal class AppSettingsFragment : ComposeFragment(), StoreSubscriber = - mutableStateOf(viewModel.updateState(store.state.detailsState)) @Composable override fun ScreenContent(modifier: Modifier) { AppSettingsScreen( modifier = modifier, - state = screenState.value, + state = viewModel.uiState, onBackClick = { store.dispatch(DetailsAction.ResetCardSettingsData) store.dispatch(NavigationAction.PopBackTo()) @@ -73,6 +69,6 @@ internal class AppSettingsFragment : ComposeFragment(), StoreSubscriber Unit): Item.Card { + return Item.Card( + id = "enroll_biometrics_card", + title = resourceReference(R.string.app_settings_enable_biometrics_title), + description = resourceReference(R.string.app_settings_enable_biometrics_description), + iconResId = R.drawable.ic_alert_circle_24, + onClick = onClick, + ) + } + + fun createSaveWalletsSwitch( + isChecked: Boolean, + isEnabled: Boolean, + onCheckedChange: (Boolean) -> Unit, + ): Item.Switch { + return Item.Switch( + id = "save_wallets_switch", + title = resourceReference(R.string.app_settings_saved_wallet), + description = resourceReference(R.string.app_settings_saved_wallet_footer), + isEnabled = isEnabled, + isChecked = isChecked, + onCheckedChange = onCheckedChange, + ) + } + + fun createSaveAccessCodeSwitch( + isChecked: Boolean, + isEnabled: Boolean, + onCheckedChange: (Boolean) -> Unit, + ): Item.Switch { + return Item.Switch( + id = "save_access_codes_switch", + title = resourceReference(R.string.app_settings_saved_access_codes), + description = resourceReference(R.string.app_settings_saved_access_codes_footer), + isEnabled = isEnabled, + isChecked = isChecked, + onCheckedChange = onCheckedChange, + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsScreen.kt b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsScreen.kt index 327dcbe8c4..05405f7d3d 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsScreen.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsScreen.kt @@ -1,224 +1,113 @@ package com.tangem.tap.features.details.ui.appsettings -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.* -import androidx.compose.material.Text -import androidx.compose.runtime.* -import androidx.compose.ui.Alignment +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.rememberUpdatedState import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview -import com.tangem.core.ui.components.SpacerH24 -import com.tangem.core.ui.components.SpacerH32 -import com.tangem.core.ui.components.SpacerH4 -import com.tangem.core.ui.components.SpacerW32 +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider import com.tangem.core.ui.res.TangemTheme -import com.tangem.tap.features.details.redux.AppSetting -import com.tangem.tap.features.details.ui.appsettings.components.EnrollBiometricsCard +import com.tangem.tap.features.details.ui.appsettings.AppSettingsScreenState.Item +import com.tangem.tap.features.details.ui.appsettings.components.CardItem import com.tangem.tap.features.details.ui.appsettings.components.SettingsAlertDialog +import com.tangem.tap.features.details.ui.appsettings.components.SwitchItem import com.tangem.tap.features.details.ui.common.SettingsScreensScaffold -import com.tangem.tap.features.details.ui.common.TangemSwitch import com.tangem.wallet.R +import kotlinx.collections.immutable.persistentListOf @Composable internal fun AppSettingsScreen(state: AppSettingsScreenState, onBackClick: () -> Unit, modifier: Modifier = Modifier) { SettingsScreensScaffold( modifier = modifier, - content = { AppSettings(state = state) }, + content = { + when (state) { + is AppSettingsScreenState.Content -> AppSettings(state = state) + is AppSettingsScreenState.Loading -> Unit + } + }, titleRes = R.string.app_settings_title, onBackClick = onBackClick, ) } @Composable -private fun AppSettings(state: AppSettingsScreenState) { - var dialogType by remember { mutableStateOf(null) } - val onDialogStateChange: (AppSetting?) -> Unit = { dialogType = it } - - dialogType?.let { - SettingsAlertDialog( - element = it, - onDialogStateChange = onDialogStateChange, - onSettingToggle = { state.onSettingToggled(it, false) }, - ) +private fun AppSettings(state: AppSettingsScreenState.Content) { + val alert by rememberUpdatedState(newValue = state.alert) + alert?.let { safeAlert -> + SettingsAlertDialog(alert = safeAlert) } - Column(modifier = Modifier.fillMaxSize()) { - if (state.showEnrollBiometricsCard) { - EnrollBiometricsCard(onClick = state.onEnrollBiometrics) - SpacerH24() - } - - AppSettingsElement( - state = state, - setting = AppSetting.SaveWallets, - onDialogStateChange = onDialogStateChange, - ) - SpacerH32() - AppSettingsElement( - state = state, - setting = AppSetting.SaveAccessCode, - onDialogStateChange = onDialogStateChange, - ) - } -} - -@Suppress("LongMethod") -@Composable -private fun AppSettingsElement( - state: AppSettingsScreenState, - setting: AppSetting, - onDialogStateChange: (AppSetting?) -> Unit, -) { - val titleRes = when (setting) { - AppSetting.SaveWallets -> R.string.app_settings_saved_wallet - AppSetting.SaveAccessCode -> R.string.app_settings_saved_access_codes - } - val subtitleRes = when (setting) { - AppSetting.SaveWallets -> R.string.app_settings_saved_wallet_footer - AppSetting.SaveAccessCode -> R.string.app_settings_saved_access_codes_footer - } - val checked = state.settings[setting] ?: false - - val titleTextColor by rememberUpdatedState( - newValue = if (state.isTogglesEnabled) { - TangemTheme.colors.text.primary1 - } else { - TangemTheme.colors.text.secondary - }, - ) - val descriptionTextColor by rememberUpdatedState( - newValue = if (state.isTogglesEnabled) { - TangemTheme.colors.text.secondary - } else { - TangemTheme.colors.text.tertiary - }, - ) - - Row( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = TangemTheme.dimens.spacing20), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.SpaceBetween, - ) { - Column( - modifier = Modifier.weight(weight = .9f), - verticalArrangement = Arrangement.Center, - ) { - Text( - text = stringResource(id = titleRes), - style = TangemTheme.typography.subtitle1, - color = titleTextColor, - ) - SpacerH4() - Text( - text = stringResource(id = subtitleRes), - style = TangemTheme.typography.body2, - color = descriptionTextColor, - ) - } - SpacerW32() - TangemSwitch( - checked = checked, - enabled = state.isTogglesEnabled, - onCheckedChange = { isChecked -> - onCheckedChange( - element = setting, - enabled = isChecked, - onSettingToggled = state.onSettingToggled, - onDialogStateChange = onDialogStateChange, + LazyColumn { + items( + items = state.items, + key = Item::id, + ) { item -> + when (item) { + is Item.Card -> CardItem( + modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing16), + item = item, ) - }, - ) - } -} - -private fun onCheckedChange( - element: AppSetting, - enabled: Boolean, - onSettingToggled: (AppSetting, Boolean) -> Unit, - onDialogStateChange: (AppSetting?) -> Unit, -) { - // Show warning if user wants to disable the switch - if (!enabled) { - onDialogStateChange(element) - } else { - onSettingToggled(element, true) + is Item.Switch -> SwitchItem( + modifier = Modifier.padding( + vertical = TangemTheme.dimens.spacing16, + horizontal = TangemTheme.dimens.spacing20, + ), + item = item, + ) + } + } } } // region Preview -@Composable -private fun AppSettingsScreenSample(modifier: Modifier = Modifier) { - Column( - modifier = modifier - .background(TangemTheme.colors.background.primary), - ) { - AppSettingsScreen( - state = AppSettingsScreenState( - settings = mapOf( - AppSetting.SaveWallets to true, - AppSetting.SaveAccessCode to false, - ), - showEnrollBiometricsCard = false, - isTogglesEnabled = true, - onSettingToggled = { _, _ -> }, - onEnrollBiometrics = {}, - ), - onBackClick = { }, - ) - } -} - @Preview(showBackground = true, widthDp = 360) @Composable -private fun AppSettingsScreenPreview_Light() { +private fun AppSettingsScreenPreview_Light( + @PreviewParameter(AppSettingsScreenStateProvider::class) state: AppSettingsScreenState, +) { TangemTheme { - AppSettingsScreenSample() + AppSettingsScreen(state = state, onBackClick = {}) } } @Preview(showBackground = true, widthDp = 360) @Composable -private fun AppSettingsScreenPreview_Dark() { +private fun AppSettingsScreenPreview_Dark( + @PreviewParameter(AppSettingsScreenStateProvider::class) state: AppSettingsScreenState, +) { TangemTheme(isDark = true) { - AppSettingsScreenSample() + AppSettingsScreen(state = state, onBackClick = {}) } } -@Composable -private fun AppSettingsScreen_EnrollBiometrics_Sample(modifier: Modifier = Modifier) { - Column(modifier = modifier.background(TangemTheme.colors.background.primary)) { - AppSettingsScreen( - state = AppSettingsScreenState( - settings = mapOf( - AppSetting.SaveWallets to true, - AppSetting.SaveAccessCode to false, - ), - showEnrollBiometricsCard = true, - isTogglesEnabled = false, - onSettingToggled = { _, _ -> }, - onEnrollBiometrics = {}, - ), - onBackClick = { }, +private class AppSettingsScreenStateProvider : CollectionPreviewParameterProvider( + collection = buildList { + val itemsFactory = AppSettingsItemsFactory() + val dialogsFactory = AppSettingsAlertsFactory() + val items = persistentListOf( + itemsFactory.createEnrollBiometricsCard {}, + itemsFactory.createSaveWalletsSwitch(isChecked = true, isEnabled = true, { _ -> }), + itemsFactory.createSaveAccessCodeSwitch(isChecked = false, isEnabled = true) { _ -> }, ) - } -} -@Preview(showBackground = true, widthDp = 360) -@Composable -private fun AppSettingsScreen_EnrollBiometrics_Preview_Light() { - TangemTheme { - AppSettingsScreen_EnrollBiometrics_Sample() - } -} + AppSettingsScreenState.Content( + items = items, + alert = null, + ).let(::add) -@Preview(showBackground = true, widthDp = 360) -@Composable -private fun AppSettingsScreen_EnrollBiometrics_Preview_Dark() { - TangemTheme(isDark = true) { - AppSettingsScreen_EnrollBiometrics_Sample() - } -} + AppSettingsScreenState.Content( + items = items, + alert = dialogsFactory.createDeleteSavedWalletsAlert({}, {}), + ).let(::add) + + AppSettingsScreenState.Content( + items = items, + alert = dialogsFactory.createDeleteSavedAccessCodesAlert({}, {}), + ).let(::add) + }, +) // endregion Preview \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsScreenState.kt b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsScreenState.kt index b891b7b088..bee5e3241b 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsScreenState.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsScreenState.kt @@ -1,11 +1,48 @@ package com.tangem.tap.features.details.ui.appsettings -import com.tangem.tap.features.details.redux.AppSetting +import androidx.annotation.DrawableRes +import androidx.compose.runtime.Immutable +import com.tangem.core.ui.extensions.TextReference +import kotlinx.collections.immutable.ImmutableList -internal data class AppSettingsScreenState( - val settings: Map = emptyMap(), - val showEnrollBiometricsCard: Boolean = false, - val isTogglesEnabled: Boolean = false, - val onSettingToggled: (AppSetting, Boolean) -> Unit = { _, _ -> /* no-op */ }, - val onEnrollBiometrics: () -> Unit = { /* no-op */ }, -) \ No newline at end of file +@Immutable +internal sealed class AppSettingsScreenState { + + object Loading : AppSettingsScreenState() + + data class Content( + val items: ImmutableList, + val alert: Alert?, + ) : AppSettingsScreenState() + + @Immutable + sealed class Item { + + abstract val id: String + + data class Card( + override val id: String, + @DrawableRes val iconResId: Int, + val title: TextReference, + val description: TextReference, + val onClick: () -> Unit, + ) : Item() + + data class Switch( + override val id: String, + val title: TextReference, + val description: TextReference, + val isEnabled: Boolean, + val isChecked: Boolean, + val onCheckedChange: (Boolean) -> Unit, + ) : Item() + } + + data class Alert( + val title: TextReference, + val description: TextReference, + val confirmText: TextReference, + val onConfirm: () -> Unit, + val onDismiss: () -> Unit, + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsViewModel.kt b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsViewModel.kt index 112d1db938..98b3cead3b 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsViewModel.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsViewModel.kt @@ -1,35 +1,31 @@ package com.tangem.tap.features.details.ui.appsettings +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue import com.tangem.tap.common.extensions.dispatchOnMain import com.tangem.tap.common.redux.AppState import com.tangem.tap.features.details.redux.AppSetting +import com.tangem.tap.features.details.redux.AppSettingsState import com.tangem.tap.features.details.redux.DetailsAction import com.tangem.tap.features.details.redux.DetailsState +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.toImmutableList import org.rekotlin.Store internal class AppSettingsViewModel(private val store: Store) { - fun updateState(state: DetailsState): AppSettingsScreenState { - return with(state.appSettingsState) { - AppSettingsScreenState( - settings = mapOf( - AppSetting.SaveWallets to saveWallets, - AppSetting.SaveAccessCode to saveAccessCodes, - ), - showEnrollBiometricsCard = needEnrollBiometrics, - isTogglesEnabled = !needEnrollBiometrics && !isInProgress, - onSettingToggled = { privacySetting, enabled -> - onSettingsToggled(privacySetting, enabled) - }, - onEnrollBiometrics = { - store.dispatchOnMain(DetailsAction.AppSettings.EnrollBiometrics) - }, - ) - } - } + private val itemsFactory = AppSettingsItemsFactory() + private val alertsFactory = AppSettingsAlertsFactory() - private fun onSettingsToggled(setting: AppSetting, enable: Boolean) { - store.dispatch(DetailsAction.AppSettings.SwitchPrivacySetting(enable = enable, setting = setting)) + var uiState: AppSettingsScreenState by mutableStateOf(AppSettingsScreenState.Loading) + private set + + fun updateState(state: DetailsState) { + uiState = AppSettingsScreenState.Content( + items = buildItems(state.appSettingsState), + alert = (uiState as? AppSettingsScreenState.Content)?.alert, + ) } fun checkBiometricsStatus() { @@ -39,4 +35,85 @@ internal class AppSettingsViewModel(private val store: Store) { fun refreshBiometricsStatus() { store.dispatch(DetailsAction.AppSettings.CheckBiometricsStatus(awaitStatusChange = true)) } + + private fun buildItems(state: AppSettingsState): ImmutableList { + val items = buildList { + if (state.needEnrollBiometrics) { + itemsFactory.createEnrollBiometricsCard(onClick = ::enrollBiometrics).let(::add) + } + + if (state.isBiometricsAvailable) { + val canUseBiometrics = !state.needEnrollBiometrics && !state.isInProgress + + itemsFactory.createSaveWalletsSwitch( + isChecked = state.saveWallets, + isEnabled = canUseBiometrics, + onCheckedChange = ::onSaveWalletsToggled, + ).let(::add) + + itemsFactory.createSaveAccessCodeSwitch( + isChecked = state.saveAccessCodes, + isEnabled = canUseBiometrics, + onCheckedChange = ::onSaveAccessCodesToggled, + ).let(::add) + } + } + + return items.toImmutableList() + } + + private fun enrollBiometrics() { + store.dispatchOnMain(DetailsAction.AppSettings.EnrollBiometrics) + } + + private fun onSaveWalletsToggled(isChecked: Boolean) { + if (isChecked) { + onSettingsToggled(AppSetting.SaveWallets, enable = true) + } else { + updateContentState { + copy( + alert = alertsFactory.createDeleteSavedWalletsAlert( + onDelete = { + onSettingsToggled(AppSetting.SaveWallets, enable = false) + dismissDialog() + }, + onDismiss = ::dismissDialog, + ), + ) + } + } + } + + private fun onSaveAccessCodesToggled(isChecked: Boolean) { + if (isChecked) { + onSettingsToggled(AppSetting.SaveAccessCode, enable = true) + } else { + updateContentState { + copy( + alert = alertsFactory.createDeleteSavedAccessCodesAlert( + onDelete = { + onSettingsToggled(AppSetting.SaveAccessCode, enable = false) + dismissDialog() + }, + onDismiss = ::dismissDialog, + ), + ) + } + } + } + + private fun onSettingsToggled(setting: AppSetting, enable: Boolean) { + store.dispatch(DetailsAction.AppSettings.SwitchPrivacySetting(enable = enable, setting = setting)) + } + + private fun dismissDialog() { + updateContentState { copy(alert = null) } + } + + private fun updateContentState(block: AppSettingsScreenState.Content.() -> AppSettingsScreenState.Content) { + uiState = when (val state = uiState) { + is AppSettingsScreenState.Content -> block(state) + is AppSettingsScreenState.Loading -> state + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/components/EnrollBiometricsCard.kt b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/components/CardItem.kt similarity index 57% rename from app/src/main/java/com/tangem/tap/features/details/ui/appsettings/components/EnrollBiometricsCard.kt rename to app/src/main/java/com/tangem/tap/features/details/ui/appsettings/components/CardItem.kt index 1386a364d7..7643c0fd09 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/components/EnrollBiometricsCard.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/components/CardItem.kt @@ -1,11 +1,6 @@ package com.tangem.tap.features.details.ui.appsettings.components -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.* import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.material.Icon import androidx.compose.material.Surface @@ -14,24 +9,25 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider import androidx.compose.ui.unit.dp import com.tangem.core.ui.components.SpacerH4 import com.tangem.core.ui.components.SpacerW16 +import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.res.TangemTheme -import com.tangem.wallet.R +import com.tangem.tap.features.details.ui.appsettings.AppSettingsItemsFactory +import com.tangem.tap.features.details.ui.appsettings.AppSettingsScreenState.Item @OptIn(ExperimentalMaterialApi::class) @Composable -internal fun EnrollBiometricsCard(onClick: () -> Unit) { +internal fun CardItem(item: Item.Card, modifier: Modifier = Modifier) { Surface( - modifier = Modifier - .padding(horizontal = TangemTheme.dimens.spacing8) - .fillMaxWidth(), - color = TangemTheme.colors.background.primary, + modifier = modifier.fillMaxWidth(), + color = TangemTheme.colors.button.disabled, shape = TangemTheme.shapes.roundedCornersLarge, - onClick = onClick, + onClick = item.onClick, ) { Row( modifier = Modifier.padding(all = 16.dp), @@ -39,20 +35,20 @@ internal fun EnrollBiometricsCard(onClick: () -> Unit) { horizontalArrangement = Arrangement.SpaceEvenly, ) { Icon( - painter = painterResource(id = R.drawable.ic_alert_circle_24), + painter = painterResource(id = item.iconResId), tint = TangemTheme.colors.icon.attention, contentDescription = null, ) SpacerW16() Column { Text( - text = stringResource(id = R.string.app_settings_enable_biometrics_title), + text = item.title.resolveReference(), style = TangemTheme.typography.subtitle1, color = TangemTheme.colors.text.primary1, ) SpacerH4() Text( - text = stringResource(id = R.string.app_settings_enable_biometrics_description), + text = item.description.resolveReference(), style = TangemTheme.typography.body2, color = TangemTheme.colors.text.secondary, ) @@ -62,28 +58,29 @@ internal fun EnrollBiometricsCard(onClick: () -> Unit) { } // region Preview -@Composable -private fun EnrollBiometricsCardSample(modifier: Modifier = Modifier) { - Column( - modifier = modifier.background(TangemTheme.colors.background.secondary), - ) { - EnrollBiometricsCard(onClick = {}) - } -} - @Preview(showBackground = true, widthDp = 360) @Composable -private fun EnrollBiometricsCardPreview_Light() { +private fun CardItemPreview_Light(@PreviewParameter(CardItemProvider::class) item: Item.Card) { TangemTheme { - EnrollBiometricsCardSample() + CardItem(item = item) } } @Preview(showBackground = true, widthDp = 360) @Composable -private fun EnrollBiometricsCardPreview_Dark() { +private fun CardItemPreview_Dark(@PreviewParameter(CardItemProvider::class) item: Item.Card) { TangemTheme(isDark = true) { - EnrollBiometricsCardSample() + CardItem(item = item) } } + +private class CardItemProvider : CollectionPreviewParameterProvider( + collection = buildList { + val itemsFactory = AppSettingsItemsFactory() + + itemsFactory.createEnrollBiometricsCard( + onClick = { /* no-op */ }, + ).let(::add) + }, +) // endregion Preview \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/components/SettingsAlertDialog.kt b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/components/SettingsAlertDialog.kt index c82113133b..e592fb6cf0 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/components/SettingsAlertDialog.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/components/SettingsAlertDialog.kt @@ -1,97 +1,60 @@ package com.tangem.tap.features.details.ui.appsettings.components -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.padding -import androidx.compose.material.AlertDialog -import androidx.compose.material.Text import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview -import com.tangem.core.ui.components.TextButton -import com.tangem.core.ui.components.WarningTextButton +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider +import com.tangem.core.ui.components.BasicDialog +import com.tangem.core.ui.components.DialogButton +import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.res.TangemTheme -import com.tangem.tap.features.details.redux.AppSetting +import com.tangem.tap.features.details.ui.appsettings.AppSettingsAlertsFactory +import com.tangem.tap.features.details.ui.appsettings.AppSettingsScreenState.Alert import com.tangem.wallet.R @Composable -internal fun SettingsAlertDialog( - element: AppSetting, - onDialogStateChange: (AppSetting?) -> Unit, - onSettingToggle: () -> Unit, -) { - val text = when (element) { - AppSetting.SaveWallets -> R.string.app_settings_off_saved_wallet_alert_message - AppSetting.SaveAccessCode -> R.string.app_settings_off_saved_access_code_alert_message - } - - AlertDialog( - onDismissRequest = { onDialogStateChange(null) }, - confirmButton = { - TextButton( - modifier = Modifier.padding(bottom = TangemTheme.dimens.spacing12), - text = stringResource(id = R.string.common_cancel), - onClick = { - onDialogStateChange(null) - }, - ) - }, - dismissButton = { - WarningTextButton( - text = stringResource(id = R.string.common_delete), - onClick = { - onDialogStateChange(null) - onSettingToggle() - }, - ) - }, - title = { - Text( - text = stringResource(id = R.string.common_attention), - color = TangemTheme.colors.text.primary1, - style = TangemTheme.typography.h2, - ) - }, - text = { - Text( - text = stringResource(id = text), - color = TangemTheme.colors.text.secondary, - style = TangemTheme.typography.body2, - ) - }, - shape = TangemTheme.shapes.roundedCornersLarge, +internal fun SettingsAlertDialog(alert: Alert) { + BasicDialog( + title = alert.title.resolveReference(), + message = alert.description.resolveReference(), + isDismissable = false, + confirmButton = DialogButton( + title = alert.confirmText.resolveReference(), + warning = true, + onClick = alert.onConfirm, + ), + dismissButton = DialogButton( + title = stringResource(id = R.string.common_cancel), + onClick = alert.onDismiss, + ), + onDismissDialog = alert.onDismiss, ) } // region Preview -@Composable -private fun SettingsAlertDialogSample(modifier: Modifier = Modifier) { - Column( - modifier = modifier - .background(TangemTheme.colors.background.primary), - ) { - SettingsAlertDialog( - element = AppSetting.SaveAccessCode, - onDialogStateChange = {}, - onSettingToggle = { }, - ) - } -} - @Preview(showBackground = true, widthDp = 360) @Composable -private fun SettingsAlertDialogPreview_Light() { +private fun AlertDialogPreview_Light(@PreviewParameter(AlertDialogProvider::class) dialog: Alert) { TangemTheme { - SettingsAlertDialogSample() + SettingsAlertDialog(alert = dialog) } } @Preview(showBackground = true, widthDp = 360) @Composable -private fun SettingsAlertDialogPreview_Dark() { +private fun AlertDialogPreview_Dark(@PreviewParameter(AlertDialogProvider::class) dialog: Alert) { TangemTheme(isDark = true) { - SettingsAlertDialogSample() + SettingsAlertDialog(alert = dialog) } } + +private class AlertDialogProvider : CollectionPreviewParameterProvider( + collection = buildList { + val alertsFactory = AppSettingsAlertsFactory() + + alertsFactory.createDeleteSavedAccessCodesAlert({}, {}).let(::add) + alertsFactory.createDeleteSavedWalletsAlert({}, {}).let(::add) + }, +) // endregion Preview \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/components/SwitchItem.kt b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/components/SwitchItem.kt new file mode 100644 index 0000000000..8d918ced3a --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/components/SwitchItem.kt @@ -0,0 +1,113 @@ +package com.tangem.tap.features.details.ui.appsettings.components + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.rememberUpdatedState +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider +import com.tangem.core.ui.components.SpacerH4 +import com.tangem.core.ui.components.SpacerW32 +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.res.TangemTheme +import com.tangem.tap.features.details.ui.appsettings.AppSettingsItemsFactory +import com.tangem.tap.features.details.ui.appsettings.AppSettingsScreenState.Item +import com.tangem.tap.features.details.ui.common.TangemSwitch + +@Composable +internal fun SwitchItem(item: Item.Switch, modifier: Modifier = Modifier) { + val titleTextColor by rememberUpdatedState( + newValue = if (item.isEnabled) { + TangemTheme.colors.text.primary1 + } else { + TangemTheme.colors.text.secondary + }, + ) + val descriptionTextColor by rememberUpdatedState( + newValue = if (item.isEnabled) { + TangemTheme.colors.text.secondary + } else { + TangemTheme.colors.text.tertiary + }, + ) + + Row( + modifier = modifier, + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween, + ) { + Column( + modifier = Modifier.weight(weight = .9f), + verticalArrangement = Arrangement.Center, + ) { + Text( + text = item.title.resolveReference(), + style = TangemTheme.typography.subtitle1, + color = titleTextColor, + ) + SpacerH4() + Text( + text = item.description.resolveReference(), + style = TangemTheme.typography.body2, + color = descriptionTextColor, + ) + } + SpacerW32() + TangemSwitch( + checked = item.isChecked, + enabled = item.isEnabled, + onCheckedChange = item.onCheckedChange, + ) + } +} + +// region Preview +@Preview(showBackground = true, widthDp = 360) +@Composable +private fun SwitchItemPreview_Light(@PreviewParameter(SwitchItemProvider::class) item: Item.Switch) { + TangemTheme { + SwitchItem(item = item) + } +} + +@Preview(showBackground = true, widthDp = 360) +@Composable +private fun SwitchItemPreview_Dark(@PreviewParameter(SwitchItemProvider::class) item: Item.Switch) { + TangemTheme(isDark = true) { + SwitchItem(item = item) + } +} + +private class SwitchItemProvider : CollectionPreviewParameterProvider( + collection = buildList { + val itemsFactory = AppSettingsItemsFactory() + + itemsFactory.createSaveAccessCodeSwitch( + isChecked = true, + isEnabled = true, + onCheckedChange = { /* no-op */ }, + ).let(::add) + itemsFactory.createSaveAccessCodeSwitch( + isChecked = false, + isEnabled = true, + onCheckedChange = { /* no-op */ }, + ).let(::add) + itemsFactory.createSaveAccessCodeSwitch( + isChecked = true, + isEnabled = false, + onCheckedChange = { /* no-op */ }, + ).let(::add) + itemsFactory.createSaveAccessCodeSwitch( + isChecked = false, + isEnabled = false, + onCheckedChange = { /* no-op */ }, + ).let(::add) + }, +) +// endregion Preview \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/extensions/TextReference.kt b/core/ui/src/main/java/com/tangem/core/ui/extensions/TextReference.kt index bc8107d5f5..f62df1c8c3 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/extensions/TextReference.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/extensions/TextReference.kt @@ -50,6 +50,53 @@ sealed interface TextReference { data class Combined(val refs: WrappedList) : TextReference } +/** + * Creates a [TextReference] using a string resource ID with optional format arguments. + * + * @param id The resource ID of the string. + * @param formatArgs A list of format arguments to be applied to the string resource. + * @return A [TextReference] representing the string resource with format arguments. + */ +fun resourceReference(@StringRes id: Int, formatArgs: WrappedList = WrappedList(emptyList())): TextReference { + return TextReference.Res(id, formatArgs) +} + +/** + * Creates a [TextReference] using a plain string value. + * + * @param value The plain string value. + * @return A [TextReference] representing the provided string value. + */ +fun stringReference(value: String): TextReference { + return TextReference.Str(value) +} + +/** + * Creates a [TextReference] using a plural string resource ID with count and optional format arguments. + * + * @param id The resource ID of the plural string. + * @param count The count value to determine the plural form. + * @param formatArgs A list of format arguments to be applied to the plural string resource. + * @return A [TextReference] representing the plural string resource with count and format arguments. + */ +fun pluralReference( + @PluralsRes id: Int, + count: Int, + formatArgs: WrappedList = WrappedList(emptyList()), +): TextReference { + return TextReference.PluralRes(id, count, formatArgs) +} + +/** + * Combines multiple [TextReference] instances into a single [TextReference]. + * + * @param refs A list of [TextReference] instances to be combined. + * @return A [TextReference] representing the combined text references. + */ +fun combinedReference(refs: WrappedList): TextReference { + return TextReference.Combined(refs) +} + /** Resolve [TextReference] as [String] */ @Composable @ReadOnlyComposable From b5e2899332dc5a2195a925263152b378e6fe16c1 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 5 Sep 2023 10:58:42 +0300 Subject: [PATCH 011/242] Updated on 2026-08-14 --- .../wallet/state/WalletSingleCurrencyState.kt | 7 +- .../WalletCryptoCurrencyActionsConverter.kt | 8 +- .../WalletLoadedTokensListConverter.kt | 28 +- .../state/factory/WalletLockedConverter.kt | 4 +- .../factory/WalletRefreshStateConverter.kt | 162 ++++--- ...letSingleCurrencyLoadedBalanceConverter.kt | 32 +- .../factory/WalletSkeletonStateConverter.kt | 4 +- .../state/factory/WalletStateFactory.kt | 46 +- .../presentation/wallet/ui/WalletScreen.kt | 10 +- .../utils/CurrencyStatusErrorConverter.kt | 17 + .../utils/TokenListToWalletStateConverter.kt | 20 +- .../wallet/viewmodels/WalletViewModel.kt | 417 +++++++++--------- 12 files changed, 375 insertions(+), 380 deletions(-) create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/CurrencyStatusErrorConverter.kt diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletSingleCurrencyState.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletSingleCurrencyState.kt index 6991db467a..5b09fe934e 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletSingleCurrencyState.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletSingleCurrencyState.kt @@ -7,6 +7,7 @@ import com.tangem.core.ui.components.transactions.state.TransactionState import com.tangem.core.ui.components.transactions.state.TxHistoryState import com.tangem.feature.wallet.presentation.wallet.state.components.* import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.PersistentList import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.flow.MutableStateFlow @@ -19,7 +20,7 @@ import kotlinx.coroutines.flow.MutableStateFlow internal sealed class WalletSingleCurrencyState : WalletState.ContentState() { /** Manage buttons */ - abstract val buttons: ImmutableList + abstract val buttons: PersistentList /** Transactions history state */ abstract val txHistoryState: TxHistoryState @@ -31,7 +32,7 @@ internal sealed class WalletSingleCurrencyState : WalletState.ContentState() { override val pullToRefreshConfig: WalletPullToRefreshConfig, override val notifications: ImmutableList, override val bottomSheetConfig: WalletBottomSheetConfig?, - override val buttons: ImmutableList, + override val buttons: PersistentList, override val txHistoryState: TxHistoryState, val marketPriceBlockState: MarketPriceBlockState, ) : WalletSingleCurrencyState() @@ -41,7 +42,7 @@ internal sealed class WalletSingleCurrencyState : WalletState.ContentState() { override val topBarConfig: WalletTopBarConfig, override val walletsListConfig: WalletsListConfig, override val pullToRefreshConfig: WalletPullToRefreshConfig, - override val buttons: ImmutableList, + override val buttons: PersistentList, override val onUnlockWalletsNotificationClick: () -> Unit, override val onUnlockClick: () -> Unit, override val onScanClick: () -> Unit, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletCryptoCurrencyActionsConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletCryptoCurrencyActionsConverter.kt index 597680d5eb..38e18cdf52 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletCryptoCurrencyActionsConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletCryptoCurrencyActionsConverter.kt @@ -8,8 +8,8 @@ import com.tangem.feature.wallet.presentation.wallet.state.WalletState import com.tangem.feature.wallet.presentation.wallet.state.components.WalletManageButton import com.tangem.feature.wallet.presentation.wallet.viewmodels.WalletClickIntents import com.tangem.utils.converter.Converter -import kotlinx.collections.immutable.ImmutableList -import kotlinx.collections.immutable.toImmutableList +import kotlinx.collections.immutable.PersistentList +import kotlinx.collections.immutable.toPersistentList internal class WalletCryptoCurrencyActionsConverter( private val currentStateProvider: Provider, @@ -26,7 +26,7 @@ internal class WalletCryptoCurrencyActionsConverter( } } - private fun List.mapToManageButtons(): ImmutableList { + private fun List.mapToManageButtons(): PersistentList { return this .mapNotNull { action -> when (action) { @@ -45,6 +45,6 @@ internal class WalletCryptoCurrencyActionsConverter( is TokenActionsState.ActionState.Swap -> null } } - .toImmutableList() + .toPersistentList() } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletLoadedTokensListConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletLoadedTokensListConverter.kt index a98b75e4f8..7960bab25a 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletLoadedTokensListConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletLoadedTokensListConverter.kt @@ -9,7 +9,6 @@ import com.tangem.domain.tokens.model.TokenList import com.tangem.domain.wallets.models.UserWallet import com.tangem.feature.wallet.presentation.wallet.state.WalletMultiCurrencyState import com.tangem.feature.wallet.presentation.wallet.state.WalletState -import com.tangem.feature.wallet.presentation.wallet.state.factory.WalletLoadedTokensListConverter.LoadedTokensListModel import com.tangem.feature.wallet.presentation.wallet.utils.TokenListErrorConverter import com.tangem.feature.wallet.presentation.wallet.utils.TokenListToWalletStateConverter import com.tangem.feature.wallet.presentation.wallet.viewmodels.WalletClickIntents @@ -27,11 +26,12 @@ import com.tangem.utils.converter.Converter */ internal class WalletLoadedTokensListConverter( private val currentStateProvider: Provider, + private val tokenListErrorConverter: TokenListErrorConverter, appCurrencyProvider: Provider, cardTypeResolverProvider: Provider, currentWalletProvider: Provider, clickIntents: WalletClickIntents, -) : Converter { +) : Converter, WalletState> { private val tokenListStateConverter = TokenListToWalletStateConverter( currentStateProvider = currentStateProvider, @@ -42,26 +42,10 @@ internal class WalletLoadedTokensListConverter( clickIntents = clickIntents, ) - private val tokenListErrorStateConverter = TokenListErrorConverter( - currentStateProvider = currentStateProvider, - ) - - override fun convert(value: LoadedTokensListModel): WalletState { - return value.tokenListEither.fold( - ifLeft = tokenListErrorStateConverter::convert, - ifRight = { - tokenListStateConverter.convert( - value = TokenListToWalletStateConverter.TokensListModel( - tokenList = it, - isRefreshing = value.isRefreshing, - ), - ) - }, + override fun convert(value: Either): WalletState { + return value.fold( + ifLeft = tokenListErrorConverter::convert, + ifRight = tokenListStateConverter::convert, ) } - - data class LoadedTokensListModel( - val tokenListEither: Either, - val isRefreshing: Boolean, - ) } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletLockedConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletLockedConverter.kt index 5ec6613bb8..3e89437610 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletLockedConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletLockedConverter.kt @@ -13,7 +13,7 @@ import com.tangem.feature.wallet.presentation.wallet.state.components.WalletTopB import com.tangem.feature.wallet.presentation.wallet.state.components.WalletsListConfig import com.tangem.feature.wallet.presentation.wallet.viewmodels.WalletClickIntents import com.tangem.utils.converter.Converter -import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.PersistentList import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList @@ -99,7 +99,7 @@ internal class WalletLockedConverter( ) } - private fun createButtons(): ImmutableList { + private fun createButtons(): PersistentList { return persistentListOf( WalletManageButton.Buy(enabled = false, onClick = {}), WalletManageButton.Send(enabled = false, onClick = {}), diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletRefreshStateConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletRefreshStateConverter.kt index f820ff8444..a7d44c70d8 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletRefreshStateConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletRefreshStateConverter.kt @@ -1,124 +1,112 @@ package com.tangem.feature.wallet.presentation.wallet.state.factory import com.tangem.common.Provider -import com.tangem.core.ui.components.marketprice.MarketPriceBlockState -import com.tangem.core.ui.components.transactions.state.TxHistoryState -import com.tangem.domain.common.CardTypesResolver -import com.tangem.feature.wallet.presentation.common.state.TokenItemState import com.tangem.feature.wallet.presentation.wallet.state.WalletMultiCurrencyState import com.tangem.feature.wallet.presentation.wallet.state.WalletSingleCurrencyState import com.tangem.feature.wallet.presentation.wallet.state.WalletState -import com.tangem.feature.wallet.presentation.wallet.state.components.* -import com.tangem.feature.wallet.presentation.wallet.state.components.WalletTokensListState.TokensListItemState +import com.tangem.feature.wallet.presentation.wallet.state.components.WalletManageButton +import com.tangem.feature.wallet.presentation.wallet.state.components.WalletPullToRefreshConfig +import com.tangem.feature.wallet.presentation.wallet.state.components.WalletTokensListState import com.tangem.feature.wallet.presentation.wallet.viewmodels.WalletClickIntents import com.tangem.utils.converter.Converter -import kotlinx.collections.immutable.ImmutableList -import kotlinx.collections.immutable.toImmutableList -import kotlinx.collections.immutable.toPersistentList -import kotlinx.coroutines.flow.update +import kotlinx.collections.immutable.PersistentList +import kotlinx.collections.immutable.mutate internal class WalletRefreshStateConverter( private val currentStateProvider: Provider, - private val currentCardTypeResolverProvider: Provider, - private val clickIntents: WalletClickIntents, -) : Converter { + private val intents: WalletClickIntents, +) : Converter { - override fun convert(value: Unit): WalletState { - return when (val state = currentStateProvider()) { - is WalletMultiCurrencyState.Content -> state.getRefreshState() - is WalletSingleCurrencyState.Content -> state.getRefreshState() - else -> state - } - } + override fun convert(value: Boolean): WalletState { + val state = currentStateProvider() + val contentState = state as? WalletState.ContentState ?: return state - private fun WalletMultiCurrencyState.Content.getRefreshState(): WalletMultiCurrencyState.Content { - return copy( - walletsListConfig = createWalletsListConfig(), - pullToRefreshConfig = createPullToRefreshConfig(), - tokensListState = createTokenListState(), - ) - } - - private fun WalletSingleCurrencyState.Content.getRefreshState(): WalletSingleCurrencyState.Content { - return copy( - walletsListConfig = createWalletsListConfig(), - pullToRefreshConfig = createPullToRefreshConfig(), - buttons = buttons.mapToDisabledButton(), - txHistoryState = createTxHistoryState(), - marketPriceBlockState = MarketPriceBlockState.Loading(currencyName = marketPriceBlockState.currencyName), - ) - } - - private fun WalletState.ContentState.createWalletsListConfig(): WalletsListConfig { - val selectedWallet = walletsListConfig.wallets[walletsListConfig.selectedWalletIndex] - val additionalInfo = if (currentCardTypeResolverProvider().isMultiwalletAllowed()) { - selectedWallet.additionalInfo + return if (value) { + contentState.getRefreshingState() } else { - null + contentState.getRefreshedState() } + } - return walletsListConfig.copy( - wallets = walletsListConfig.wallets.toPersistentList().set( - index = walletsListConfig.selectedWalletIndex, - element = WalletCardState.Loading( - id = selectedWallet.id, - title = selectedWallet.title, - additionalInfo = additionalInfo, - imageResId = selectedWallet.imageResId, - onRenameClick = selectedWallet.onRenameClick, - onDeleteClick = selectedWallet.onDeleteClick, - ), - ), + private fun WalletState.ContentState.getRefreshingState(): WalletState { + return when (this) { + is WalletMultiCurrencyState.Content -> getRefreshingState() + is WalletSingleCurrencyState.Content -> getRefreshingState() + is WalletMultiCurrencyState.Locked, + is WalletSingleCurrencyState.Locked, + -> this + } + } + + private fun WalletState.ContentState.getRefreshedState(): WalletState { + return when (this) { + is WalletMultiCurrencyState.Content -> getRefreshedState() + is WalletSingleCurrencyState.Content -> getRefreshedState() + is WalletMultiCurrencyState.Locked, + is WalletSingleCurrencyState.Locked, + -> this + } + } + + private fun WalletMultiCurrencyState.Content.getRefreshingState(): WalletMultiCurrencyState { + return copy( + pullToRefreshConfig = updatePullToRefreshConfig(isRefreshing = true), + tokensListState = updateTokenListState(isRefreshing = true), ) } - private fun WalletState.ContentState.createPullToRefreshConfig(): WalletPullToRefreshConfig { - return pullToRefreshConfig.copy(isRefreshing = true) + private fun WalletSingleCurrencyState.Content.getRefreshingState(): WalletSingleCurrencyState { + return copy( + pullToRefreshConfig = updatePullToRefreshConfig(isRefreshing = true), + buttons = updateButtons(isRefreshing = true), + ) } - private fun WalletMultiCurrencyState.Content.createTokenListState(): WalletTokensListState { - return when (tokensListState) { + private fun WalletMultiCurrencyState.Content.getRefreshedState(): WalletMultiCurrencyState { + return copy( + pullToRefreshConfig = updatePullToRefreshConfig(isRefreshing = false), + tokensListState = updateTokenListState(isRefreshing = false), + ) + } + + private fun WalletSingleCurrencyState.Content.getRefreshedState(): WalletSingleCurrencyState { + return copy( + pullToRefreshConfig = updatePullToRefreshConfig(isRefreshing = false), + buttons = updateButtons(isRefreshing = false), + ) + } + + private fun WalletMultiCurrencyState.updateTokenListState(isRefreshing: Boolean): WalletTokensListState { + return when (val state = tokensListState) { is WalletTokensListState.Content -> { - WalletTokensListState.Loading( - items = tokensListState.items - .filterIsInstance() - .mapToLoadingTokenState(), - ) + val onOrganizeTokensClick = if (isRefreshing) null else intents::onOrganizeTokensClick + + state.copy(onOrganizeTokensClick = onOrganizeTokensClick) } - is WalletTokensListState.Empty -> WalletTokensListState.Loading() - is WalletTokensListState.Loading, is WalletTokensListState.Locked, - -> tokensListState + is WalletTokensListState.Loading, + is WalletTokensListState.Empty, + -> state } } - private fun List.mapToLoadingTokenState(): ImmutableList { - return this - .map { TokensListItemState.Token(state = TokenItemState.Loading(id = it.state.id)) } - .toImmutableList() - } + private fun WalletSingleCurrencyState.updateButtons(isRefreshing: Boolean): PersistentList { + val isButtonsEnabled = !isRefreshing - private fun ImmutableList.mapToDisabledButton(): ImmutableList { - return this - .mapNotNull { button -> + return buttons.mutate { + it.mapNotNull { button -> when (button) { - is WalletManageButton.Buy -> button.copy(enabled = false) - is WalletManageButton.Send -> button.copy(enabled = false) + is WalletManageButton.Buy -> button.copy(enabled = isButtonsEnabled) + is WalletManageButton.Send -> button.copy(enabled = isButtonsEnabled) + is WalletManageButton.Sell -> button.copy(enabled = isButtonsEnabled) is WalletManageButton.Receive -> button - is WalletManageButton.Sell -> button.copy(enabled = false) is WalletManageButton.Swap -> null } } - .toImmutableList() + } } - private fun WalletSingleCurrencyState.Content.createTxHistoryState(): TxHistoryState { - if (txHistoryState is TxHistoryState.Content) { - txHistoryState.contentItems.update { - TxHistoryState.getDefaultLoadingTransactions(onExploreClick = clickIntents::onExploreClick) - } - } - - return txHistoryState + private fun WalletState.ContentState.updatePullToRefreshConfig(isRefreshing: Boolean): WalletPullToRefreshConfig { + return pullToRefreshConfig.copy(isRefreshing = isRefreshing) } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletSingleCurrencyLoadedBalanceConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletSingleCurrencyLoadedBalanceConverter.kt index 6acdd08c82..18892dce3f 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletSingleCurrencyLoadedBalanceConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletSingleCurrencyLoadedBalanceConverter.kt @@ -15,7 +15,7 @@ import com.tangem.feature.wallet.presentation.wallet.state.WalletSingleCurrencyS import com.tangem.feature.wallet.presentation.wallet.state.WalletState import com.tangem.feature.wallet.presentation.wallet.state.components.WalletCardState import com.tangem.feature.wallet.presentation.wallet.state.components.WalletsListConfig -import com.tangem.feature.wallet.presentation.wallet.state.factory.WalletSingleCurrencyLoadedBalanceConverter.SingleCurrencyLoadedBalanceModel +import com.tangem.feature.wallet.presentation.wallet.utils.CurrencyStatusErrorConverter import com.tangem.utils.converter.Converter import kotlinx.collections.immutable.toPersistentList import java.math.BigDecimal @@ -25,32 +25,21 @@ internal class WalletSingleCurrencyLoadedBalanceConverter( private val cardTypeResolverProvider: Provider, private val appCurrencyProvider: Provider, private val currentWalletProvider: Provider, -) : Converter { + private val currencyStatusErrorConverter: CurrencyStatusErrorConverter, +) : Converter, WalletSingleCurrencyState.Content> { - override fun convert(value: SingleCurrencyLoadedBalanceModel): WalletSingleCurrencyState.Content { - return value.cryptoCurrencyEither.fold( - ifLeft = { convertError() }, - ifRight = { convertContent(it, value.isRefreshing) }, + override fun convert(value: Either): WalletSingleCurrencyState.Content { + return value.fold( + ifLeft = currencyStatusErrorConverter::convert, + ifRight = ::convertContent, ) } - private fun convertError(): WalletSingleCurrencyState.Content { - return requireNotNull(currentStateProvider() as? WalletSingleCurrencyState.Content) - } - - private fun convertContent( - status: CryptoCurrencyStatus, - isRefreshing: Boolean, - ): WalletSingleCurrencyState.Content { + private fun convertContent(status: CryptoCurrencyStatus): WalletSingleCurrencyState.Content { val state = requireNotNull(currentStateProvider() as? WalletSingleCurrencyState.Content) val currencyName = state.marketPriceBlockState.currencyName return state.copy( walletsListConfig = getUpdatedSelectedWallet(status = status.value, state = state), - pullToRefreshConfig = if (isRefreshing) { - state.pullToRefreshConfig.copy(isRefreshing = status.value is CryptoCurrencyStatus.Loading) - } else { - state.pullToRefreshConfig - }, marketPriceBlockState = getMarketPriceState(status = status.value, currencyName = currencyName), ) } @@ -168,9 +157,4 @@ internal class WalletSingleCurrencyLoadedBalanceConverter( fiatCurrencySymbol = appCurrency.symbol, ) } - - data class SingleCurrencyLoadedBalanceModel( - val cryptoCurrencyEither: Either, - val isRefreshing: Boolean, - ) } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletSkeletonStateConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletSkeletonStateConverter.kt index 15e42f3283..05b16b6424 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletSkeletonStateConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletSkeletonStateConverter.kt @@ -14,7 +14,7 @@ import com.tangem.feature.wallet.presentation.wallet.state.components.* import com.tangem.feature.wallet.presentation.wallet.state.factory.WalletSkeletonStateConverter.SkeletonModel import com.tangem.feature.wallet.presentation.wallet.viewmodels.WalletClickIntents import com.tangem.utils.converter.Converter -import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.PersistentList import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.flow.MutableStateFlow @@ -128,7 +128,7 @@ internal class WalletSkeletonStateConverter( return WalletPullToRefreshConfig(isRefreshing = false, onRefresh = clickIntents::onRefreshSwipe) } - private fun createButtons(): ImmutableList { + private fun createButtons(): PersistentList { return persistentListOf( WalletManageButton.Buy(enabled = false, onClick = {}), WalletManageButton.Send(enabled = false, onClick = {}), diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletStateFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletStateFactory.kt index 512d0d0cc0..4fe28bb627 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletStateFactory.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletStateFactory.kt @@ -22,6 +22,8 @@ import com.tangem.feature.wallet.presentation.wallet.state.components.WalletBott import com.tangem.feature.wallet.presentation.wallet.state.components.WalletNotification import com.tangem.feature.wallet.presentation.wallet.state.factory.txhistory.WalletLoadedTxHistoryConverter import com.tangem.feature.wallet.presentation.wallet.state.factory.txhistory.WalletLoadingTxHistoryConverter +import com.tangem.feature.wallet.presentation.wallet.utils.CurrencyStatusErrorConverter +import com.tangem.feature.wallet.presentation.wallet.utils.TokenListErrorConverter import com.tangem.feature.wallet.presentation.wallet.viewmodels.WalletClickIntents import kotlinx.collections.immutable.ImmutableList import kotlinx.coroutines.flow.Flow @@ -45,9 +47,16 @@ internal class WalletStateFactory( private val tokenActionsProvider by lazy { TokenActionsProvider(currentStateProvider = currentStateProvider) } private val skeletonConverter by lazy { WalletSkeletonStateConverter(currentStateProvider, clickIntents) } + private val tokenListErrorConverter by lazy { + TokenListErrorConverter(currentStateProvider) + } + private val currencyStatusErrorConverter by lazy { + CurrencyStatusErrorConverter(currentStateProvider) + } private val loadedTokensListConverter by lazy { WalletLoadedTokensListConverter( currentStateProvider = currentStateProvider, + tokenListErrorConverter = tokenListErrorConverter, cardTypeResolverProvider = currentCardTypeResolverProvider, currentWalletProvider = currentWalletProvider, appCurrencyProvider = appCurrencyProvider, @@ -76,6 +85,7 @@ internal class WalletStateFactory( cardTypeResolverProvider = currentCardTypeResolverProvider, appCurrencyProvider = appCurrencyProvider, currentWalletProvider = currentWalletProvider, + currencyStatusErrorConverter = currencyStatusErrorConverter, ) } @@ -91,8 +101,7 @@ internal class WalletStateFactory( private val refreshStateConverter by lazy { WalletRefreshStateConverter( currentStateProvider = currentStateProvider, - currentCardTypeResolverProvider = currentCardTypeResolverProvider, - clickIntents = clickIntents, + intents = clickIntents, ) } @@ -114,13 +123,12 @@ internal class WalletStateFactory( ) } - fun getStateByTokensList(tokenListEither: Either, isRefreshing: Boolean): WalletState { - return loadedTokensListConverter.convert( - value = WalletLoadedTokensListConverter.LoadedTokensListModel( - tokenListEither = tokenListEither, - isRefreshing = isRefreshing, - ), - ) + fun getStateByTokensList(maybeTokenList: Either): WalletState { + return loadedTokensListConverter.convert(maybeTokenList) + } + + fun getStateByTokenListError(error: TokenListError): WalletState { + return tokenListErrorConverter.convert(error) } fun getStateByNotifications(notifications: ImmutableList): WalletState { @@ -131,7 +139,9 @@ internal class WalletStateFactory( } } - fun getStateAfterContentRefreshing(): WalletState = refreshStateConverter.convert(Unit) + fun getRefreshingState(): WalletState = refreshStateConverter.convert(value = true) + + fun getRefreshedState(): WalletState = refreshStateConverter.convert(value = false) fun getStateWithOpenWalletBottomSheet(content: WalletBottomSheetConfig.BottomSheetContentConfig): WalletState { return when (val state = currentStateProvider() as WalletState.ContentState) { @@ -157,6 +167,7 @@ internal class WalletStateFactory( isBottomSheetShow = true, onBottomSheetDismiss = clickIntents::onDismissBottomSheet, ) + else -> state } } @@ -170,6 +181,7 @@ internal class WalletStateFactory( bottomSheetConfig = state.bottomSheetConfig?.copy(isShow = false), ) is WalletSingleCurrencyState.Locked -> state.copy(isBottomSheetShow = false) + else -> state } } @@ -199,18 +211,16 @@ internal class WalletStateFactory( fun getLockedState(): WalletState = lockedConverter.convert(Unit) fun getSingleCurrencyLoadedBalanceState( - cryptoCurrencyEither: Either, - isRefreshing: Boolean, + maybeCryptoCurrencyStatus: Either, ): WalletState { - return singleCurrencyLoadedBalanceConverter.convert( - value = WalletSingleCurrencyLoadedBalanceConverter.SingleCurrencyLoadedBalanceModel( - cryptoCurrencyEither = cryptoCurrencyEither, - isRefreshing = isRefreshing, - ), - ) + return singleCurrencyLoadedBalanceConverter.convert(maybeCryptoCurrencyStatus) } fun getSingleCurrencyManageButtonsState(actions: List): WalletState { return cryptoCurrencyActionsConverter.convert(value = actions) } + + fun getStateByCurrencyStatusError(error: CurrencyStatusError): WalletState { + return currencyStatusErrorConverter.convert(error) + } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt index fca76604c4..10f4dbd9e4 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt @@ -1,15 +1,15 @@ package com.tangem.feature.wallet.presentation.wallet.ui import androidx.activity.compose.BackHandler -import androidx.compose.animation.* -import androidx.compose.foundation.* import androidx.compose.foundation.layout.* -import androidx.compose.foundation.lazy.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.material.pullrefresh.pullRefresh import androidx.compose.material.pullrefresh.rememberPullRefreshState -import androidx.compose.material3.* -import androidx.compose.runtime.* +import androidx.compose.material3.FabPosition +import androidx.compose.material3.Scaffold +import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/CurrencyStatusErrorConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/CurrencyStatusErrorConverter.kt new file mode 100644 index 0000000000..352f9f4739 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/CurrencyStatusErrorConverter.kt @@ -0,0 +1,17 @@ +package com.tangem.feature.wallet.presentation.wallet.utils + +import com.tangem.common.Converter +import com.tangem.common.Provider +import com.tangem.domain.tokens.error.CurrencyStatusError +import com.tangem.feature.wallet.presentation.wallet.state.WalletSingleCurrencyState +import com.tangem.feature.wallet.presentation.wallet.state.WalletState + +// TODO: Implement this +internal class CurrencyStatusErrorConverter( + private val currentStateProvider: Provider, +) : Converter { + + override fun convert(value: CurrencyStatusError): WalletSingleCurrencyState.Content { + return requireNotNull(currentStateProvider() as? WalletSingleCurrencyState.Content) + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/TokenListToWalletStateConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/TokenListToWalletStateConverter.kt index 35aa99598f..9019ff5b9a 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/TokenListToWalletStateConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/TokenListToWalletStateConverter.kt @@ -8,7 +8,6 @@ import com.tangem.domain.wallets.models.UserWallet import com.tangem.feature.wallet.presentation.wallet.state.WalletMultiCurrencyState import com.tangem.feature.wallet.presentation.wallet.state.WalletState import com.tangem.feature.wallet.presentation.wallet.state.components.WalletsListConfig -import com.tangem.feature.wallet.presentation.wallet.utils.TokenListToWalletStateConverter.TokensListModel import com.tangem.feature.wallet.presentation.wallet.viewmodels.WalletClickIntents import com.tangem.utils.converter.Converter import kotlinx.collections.immutable.toPersistentList @@ -21,7 +20,7 @@ internal class TokenListToWalletStateConverter( private val appCurrencyProvider: Provider, private val isWalletContentHidden: Boolean, clickIntents: WalletClickIntents, -) : Converter { +) : Converter { private val tokenListToContentConverter = TokenListToContentItemsConverter( isWalletContentHidden = isWalletContentHidden, @@ -29,16 +28,11 @@ internal class TokenListToWalletStateConverter( clickIntents = clickIntents, ) - override fun convert(value: TokensListModel): WalletMultiCurrencyState.Content { + override fun convert(value: TokenList): WalletMultiCurrencyState.Content { val state = requireNotNull(currentStateProvider() as? WalletMultiCurrencyState.Content) return state.copy( - walletsListConfig = state.updateSelectedWallet(fiatBalance = value.tokenList.totalFiatBalance), - pullToRefreshConfig = if (value.isRefreshing) { - state.pullToRefreshConfig.copy(isRefreshing = getRefreshingStatus(tokenList = value.tokenList)) - } else { - state.pullToRefreshConfig - }, - tokensListState = tokenListToContentConverter.convert(value = value.tokenList), + walletsListConfig = state.updateSelectedWallet(fiatBalance = value.totalFiatBalance), + tokensListState = tokenListToContentConverter.convert(value = value), ) } @@ -58,10 +52,4 @@ internal class TokenListToWalletStateConverter( .set(index = selectedWalletIndex, element = converter.convert(fiatBalance)), ) } - - private fun getRefreshingStatus(tokenList: TokenList): Boolean { - return tokenList.totalFiatBalance is TokenList.FiatBalance.Loading - } - - data class TokensListModel(val tokenList: TokenList, val isRefreshing: Boolean) } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt index c02df958f3..1f2483b904 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt @@ -21,9 +21,7 @@ import com.tangem.domain.redux.ReduxStateHolder import com.tangem.domain.settings.CanUseBiometryUseCase import com.tangem.domain.settings.IsUserAlreadyRateAppUseCase import com.tangem.domain.settings.ShouldShowSaveWalletScreenUseCase -import com.tangem.domain.tokens.GetCryptoCurrencyActionsUseCase -import com.tangem.domain.tokens.GetPrimaryCurrencyStatusUpdatesUseCase -import com.tangem.domain.tokens.GetTokenListUseCase +import com.tangem.domain.tokens.* import com.tangem.domain.tokens.legacy.TradeCryptoAction import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.model.TokenList @@ -73,7 +71,9 @@ internal class WalletViewModel @Inject constructor( private val setAccessCodeRequestPolicyUseCase: SetAccessCodeRequestPolicyUseCase, private val getAccessCodeSavingStatusUseCase: GetAccessCodeSavingStatusUseCase, private val getTokenListUseCase: GetTokenListUseCase, - private val getPrimaryCurrencyUseCase: GetPrimaryCurrencyStatusUpdatesUseCase, + private val fetchTokenListUseCase: FetchTokenListUseCase, + private val getPrimaryCurrencyStatusUpdatesUseCase: GetPrimaryCurrencyStatusUpdatesUseCase, + private val fetchCurrencyStatusUseCase: FetchCurrencyStatusUseCase, private val getCardWasScannedUseCase: GetCardWasScannedUseCase, private val isUserAlreadyRateAppUseCase: IsUserAlreadyRateAppUseCase, private val isDemoCardUseCase: IsDemoCardUseCase, @@ -127,6 +127,7 @@ internal class WalletViewModel @Inject constructor( private val marketPriceJobHolder = JobHolder() private val buttonsJobHolder = JobHolder() private val notificationsJobHolder = JobHolder() + private val refreshContentJobHolder = JobHolder() override fun onCreate(owner: LifecycleOwner) { viewModelScope.launch(dispatchers.main) { @@ -151,6 +152,10 @@ internal class WalletViewModel @Inject constructor( wallets = sourceList val currentState = uiState + val previousSelectedWalletIndex = (currentState as? WalletState.ContentState) + ?.walletsListConfig + ?.selectedWalletIndex + val selectedWalletIndex = if (currentState is WalletLockedState) { currentState.getSelectedWalletIndex() } else { @@ -161,141 +166,13 @@ internal class WalletViewModel @Inject constructor( sourceList.indexOfFirst { it.walletId == selectedWallet.walletId } } - uiState = stateFactory.getSkeletonState(wallets = sourceList, selectedWalletIndex = selectedWalletIndex) + if (previousSelectedWalletIndex != selectedWalletIndex) { + uiState = stateFactory.getSkeletonState(wallets = sourceList, selectedWalletIndex = selectedWalletIndex) - updateContentItems(index = selectedWalletIndex) - } - - private fun updateContentItems(index: Int, isRefreshing: Boolean = false) { - val cardTypeResolver = getCardTypeResolver(index) - when { - getWallet(index).isLocked -> uiState = stateFactory.getLockedState() - cardTypeResolver.isMultiwalletAllowed() -> updateMultiCurrencyContent(index, isRefreshing) - !cardTypeResolver.isMultiwalletAllowed() -> updateSingleCurrencyContent(index, isRefreshing) + getContentItemsUpdates(index = selectedWalletIndex) } } - private fun updateMultiCurrencyContent(index: Int, isRefreshing: Boolean = false) { - val state = requireNotNull(uiState as? WalletMultiCurrencyState) { - "Impossible to update tokens list if state isn't WalletMultiCurrencyState" - } - - getTokenListUseCase(userWalletId = state.walletsListConfig.wallets[index].id) - .distinctUntilChanged() - .onEach { tokenListEither -> - uiState = stateFactory.getStateByTokensList( - tokenListEither = tokenListEither, - isRefreshing = isRefreshing, - ) - - updateNotifications( - index = index, - tokenList = tokenListEither.fold(ifLeft = { null }, ifRight = { it }), - ) - } - .flowOn(dispatchers.io) - .launchIn(viewModelScope) - .saveIn(tokensJobHolder) - } - - private fun updateSingleCurrencyContent(index: Int, isRefreshing: Boolean) { - val wallet = getWallet(index) - val blockchain = getCardTypeResolver(index).getBlockchain() - updateTxHistory( - blockchain = blockchain, - derivationStyle = wallet.scanResponse.derivationStyleProvider.getDerivationStyle(), - ) - updateMarketPrice(userWalletId = wallet.walletId, isRefreshing = isRefreshing) - updateNotifications(index) - } - - private fun updateTxHistory(blockchain: Blockchain, derivationStyle: DerivationStyle?) { - viewModelScope.launch(dispatchers.io) { - val derivationPath = blockchain.derivationPath(style = derivationStyle)?.rawPath - - val txHistoryItemsCountEither = txHistoryItemsCountUseCase( - networkId = Network.ID(blockchain.id), - derivationPath = derivationPath, - ) - - uiState = stateFactory.getLoadingTxHistoryState(itemsCountEither = txHistoryItemsCountEither) - - txHistoryItemsCountEither.onRight { - uiState = stateFactory.getLoadedTxHistoryState( - txHistoryEither = txHistoryItemsUseCase( - networkId = Network.ID(blockchain.id), - derivationPath = derivationPath, - ).map { - it.cachedIn(viewModelScope) - }, - ) - } - } - } - - // It also update wallet balance - private fun updateMarketPrice(userWalletId: UserWalletId, isRefreshing: Boolean) { - getPrimaryCurrencyUseCase(userWalletId = userWalletId) - .distinctUntilChanged() - .onEach { either -> - uiState = stateFactory.getSingleCurrencyLoadedBalanceState( - cryptoCurrencyEither = either, - isRefreshing = isRefreshing, - ) - - either.onRight { status -> - cryptoCurrencyStatus = status - updateButtons(userWalletId = userWalletId, currency = status.currency) - } - } - .flowOn(dispatchers.io) - .launchIn(viewModelScope) - .saveIn(marketPriceJobHolder) - } - - private fun updateButtons(userWalletId: UserWalletId, currency: CryptoCurrency) { - getCryptoCurrencyActionsUseCase(userWalletId = userWalletId, cryptoCurrency = currency) - .distinctUntilChanged() - .onEach { uiState = stateFactory.getSingleCurrencyManageButtonsState(actions = it.states) } - .flowOn(dispatchers.io) - .launchIn(viewModelScope) - .saveIn(buttonsJobHolder) - } - - private fun updateNotifications(index: Int, tokenList: TokenList? = null) { - notificationsListFactory.create( - cardTypesResolver = getCardTypeResolver(index = index), - tokenList = tokenList, - ) - .distinctUntilChanged() - .onEach { uiState = stateFactory.getStateByNotifications(notifications = it) } - .flowOn(dispatchers.io) - .launchIn(viewModelScope) - .saveIn(notificationsJobHolder) - } - - override fun onStop(owner: LifecycleOwner) { - viewModelScope.launch(dispatchers.io) { - saveSelectedWallet() - } - } - - private suspend fun saveSelectedWallet() { - val state = uiState - if (state is WalletState.ContentState) { - selectWalletUseCase(getWallet(index = state.walletsListConfig.selectedWalletIndex).walletId) - } - } - - private fun getWallet(index: Int): UserWallet { - return requireNotNull( - value = wallets.getOrNull(index), - lazyMessage = { "WalletsList doesn't contain element with index = $index" }, - ) - } - - private fun getCardTypeResolver(index: Int): CardTypesResolver = getWallet(index).scanResponse.cardTypesResolver - override fun onBackClick() { viewModelScope.launch(dispatchers.main) { router.popBackStack(screen = if (shouldSaveUserWalletsUseCase()) AppScreen.Welcome else AppScreen.Home) @@ -373,14 +250,9 @@ internal class WalletViewModel @Inject constructor( if (state.walletsListConfig.selectedWalletIndex == index) return - /* - * When wallet is changed it's necessary to stop the last jobs. - * If jobs aren't stopped and wallet is changed then it will update state for the prev wallet. - */ - tokensJobHolder.update(job = null) - marketPriceJobHolder.update(job = null) - buttonsJobHolder.update(job = null) - notificationsJobHolder.update(job = null) + viewModelScope.launch(dispatchers.io) { + selectWalletUseCase(getWallet(index = index).walletId) + } val cacheState = WalletStateCache.getState(userWalletId = state.walletsListConfig.wallets[index].id) if (cacheState != null) { @@ -393,50 +265,28 @@ internal class WalletViewModel @Inject constructor( cacheState } - if (cacheState.isLoadingState()) updateContentItems(index) + if (cacheState.isLoadingState()) { + getContentItemsUpdates(index) + } } else { uiState = stateFactory.getSkeletonState(wallets = wallets, selectedWalletIndex = index) - updateContentItems(index = index) - } - } - - private fun WalletState.isLoadingState(): Boolean { - // Check the base components - if (this is WalletState.ContentState) { - walletsListConfig.wallets[walletsListConfig.selectedWalletIndex] is WalletCardState.Loading || - notifications.isEmpty() - } - - // Check the special components - return when (this) { - is WalletMultiCurrencyState -> { - val hasLoadingTokens = tokensListState is WalletTokensListState.ContentState && - (tokensListState as WalletTokensListState.ContentState).items - .filterIsInstance() - .any { it.state is TokenItemState.Loading } - - tokensListState is WalletTokensListState.Loading || hasLoadingTokens - } - is WalletSingleCurrencyState -> { - this is WalletSingleCurrencyState.Content && marketPriceBlockState is MarketPriceBlockState.Loading - } - is WalletState.Initial -> false + getContentItemsUpdates(index = index) } } override fun onRefreshSwipe() { - if (uiState is WalletState.Initial || uiState is WalletLockedState) return + val selectedWalletIndex = (uiState as? WalletState.ContentState) + ?.walletsListConfig + ?.selectedWalletIndex + ?: return - viewModelScope.launch(dispatchers.io) { - uiState = stateFactory.getStateAfterContentRefreshing() - - // TODO: [REDACTED_JIRA] - delay(timeMillis = 500) - - updateContentItems( - index = requireNotNull(uiState as? WalletState.ContentState).walletsListConfig.selectedWalletIndex, - isRefreshing = true, - ) + when (uiState) { + is WalletMultiCurrencyState.Content -> refreshMultiCurrencyContent(selectedWalletIndex) + is WalletSingleCurrencyState.Content -> refreshSingleCurrencyContent(selectedWalletIndex) + is WalletState.Initial, + is WalletMultiCurrencyState.Locked, + is WalletSingleCurrencyState.Locked, + -> Unit } } @@ -486,11 +336,12 @@ internal class WalletViewModel @Inject constructor( } override fun onReloadClick() { - uiState = stateFactory.getStateAfterContentRefreshing() - updateSingleCurrencyContent( - index = requireNotNull(uiState as? WalletState.ContentState).walletsListConfig.selectedWalletIndex, - isRefreshing = true, - ) + val selectedWalletIndex = (uiState as? WalletSingleCurrencyState) + ?.walletsListConfig + ?.selectedWalletIndex + ?: return + + refreshSingleCurrencyContent(selectedWalletIndex) } override fun onExploreClick() { @@ -553,18 +404,6 @@ internal class WalletViewModel @Inject constructor( } } - private fun createSelectedAppCurrencyFlow(): StateFlow { - return getSelectedAppCurrencyUseCase() - .map { maybeAppCurrency -> - maybeAppCurrency.getOrElse { AppCurrency.Default } - } - .stateIn( - scope = viewModelScope, - started = SharingStarted.Eagerly, - initialValue = AppCurrency.Default, - ) - } - override fun onDismissBottomSheet() { uiState = stateFactory.getStateWithClosedBottomSheet() } @@ -578,4 +417,188 @@ internal class WalletViewModel @Inject constructor( ) } } + + private fun getContentItemsUpdates(index: Int) { + /* + * When wallet is changed it's necessary to stop the last jobs. + * If jobs aren't stopped and wallet is changed then it will update state for the prev wallet. + */ + tokensJobHolder.update(job = null) + marketPriceJobHolder.update(job = null) + buttonsJobHolder.update(job = null) + notificationsJobHolder.update(job = null) + refreshContentJobHolder.update(job = null) + + val wallet = getWallet(index) + + when { + wallet.isLocked -> { + uiState = stateFactory.getLockedState() + } + wallet.isMultiCurrency -> getMultiCurrencyContent(index) + !wallet.isMultiCurrency -> getSingleCurrencyContent(index) + } + } + + private fun getMultiCurrencyContent(walletIndex: Int) { + val state = requireNotNull(uiState as? WalletMultiCurrencyState) { + "Impossible to get a token list updates if state isn't WalletMultiCurrencyState" + } + + getTokenListUseCase(userWalletId = state.walletsListConfig.wallets[walletIndex].id) + .distinctUntilChanged() + .onEach { maybeTokenList -> + uiState = stateFactory.getStateByTokensList(maybeTokenList) + + updateNotifications( + index = walletIndex, + tokenList = maybeTokenList.fold(ifLeft = { null }, ifRight = { it }), + ) + } + .flowOn(dispatchers.io) + .launchIn(viewModelScope) + .saveIn(tokensJobHolder) + } + + private fun getSingleCurrencyContent(index: Int) { + val wallet = getWallet(index) + val blockchain = getCardTypeResolver(index).getBlockchain() + updateTxHistory( + blockchain = blockchain, + derivationStyle = wallet.scanResponse.derivationStyleProvider.getDerivationStyle(), + ) + updateMarketPrice(userWalletId = wallet.walletId) + updateNotifications(index) + } + + private fun updateTxHistory(blockchain: Blockchain, derivationStyle: DerivationStyle?) { + viewModelScope.launch(dispatchers.io) { + val derivationPath = blockchain.derivationPath(style = derivationStyle)?.rawPath + + val txHistoryItemsCountEither = txHistoryItemsCountUseCase( + networkId = Network.ID(blockchain.id), + derivationPath = derivationPath, + ) + + uiState = stateFactory.getLoadingTxHistoryState(itemsCountEither = txHistoryItemsCountEither) + + txHistoryItemsCountEither.onRight { + uiState = stateFactory.getLoadedTxHistoryState( + txHistoryEither = txHistoryItemsUseCase( + networkId = Network.ID(blockchain.id), + derivationPath = derivationPath, + ).map { + it.cachedIn(viewModelScope) + }, + ) + } + } + } + + // It also update wallet balance + private fun updateMarketPrice(userWalletId: UserWalletId) { + getPrimaryCurrencyStatusUpdatesUseCase(userWalletId = userWalletId) + .distinctUntilChanged() + .onEach { maybeCryptoCurrencyStatus -> + uiState = stateFactory.getSingleCurrencyLoadedBalanceState(maybeCryptoCurrencyStatus) + + maybeCryptoCurrencyStatus.onRight { status -> + cryptoCurrencyStatus = status + updateButtons(userWalletId = userWalletId, currency = status.currency) + } + } + .flowOn(dispatchers.io) + .launchIn(viewModelScope) + .saveIn(marketPriceJobHolder) + } + + private fun updateButtons(userWalletId: UserWalletId, currency: CryptoCurrency) { + getCryptoCurrencyActionsUseCase(userWalletId = userWalletId, cryptoCurrency = currency) + .distinctUntilChanged() + .onEach { uiState = stateFactory.getSingleCurrencyManageButtonsState(actions = it.states) } + .flowOn(dispatchers.io) + .launchIn(viewModelScope) + .saveIn(buttonsJobHolder) + } + + private fun updateNotifications(index: Int, tokenList: TokenList? = null) { + notificationsListFactory.create( + cardTypesResolver = getCardTypeResolver(index = index), + tokenList = tokenList, + ) + .distinctUntilChanged() + .onEach { uiState = stateFactory.getStateByNotifications(notifications = it) } + .flowOn(dispatchers.io) + .launchIn(viewModelScope) + .saveIn(notificationsJobHolder) + } + + private fun refreshMultiCurrencyContent(walletIndex: Int) { + uiState = stateFactory.getRefreshingState() + val wallet = getWallet(walletIndex) + + viewModelScope.launch(dispatchers.io) { + val result = fetchTokenListUseCase(wallet.walletId, refresh = true) + + uiState = stateFactory.getRefreshedState() + uiState = result.fold(stateFactory::getStateByTokenListError) { uiState } + }.saveIn(refreshContentJobHolder) + } + + private fun refreshSingleCurrencyContent(walletIndex: Int) { + uiState = stateFactory.getRefreshingState() + val wallet = getWallet(walletIndex) + + viewModelScope.launch(dispatchers.io) { + val result = fetchCurrencyStatusUseCase(wallet.walletId, refresh = true) + + uiState = stateFactory.getRefreshedState() + uiState = result.fold(stateFactory::getStateByCurrencyStatusError) { uiState } + }.saveIn(refreshContentJobHolder) + } + + private fun createSelectedAppCurrencyFlow(): StateFlow { + return getSelectedAppCurrencyUseCase() + .map { maybeAppCurrency -> + maybeAppCurrency.getOrElse { AppCurrency.Default } + } + .stateIn( + scope = viewModelScope, + started = SharingStarted.Eagerly, + initialValue = AppCurrency.Default, + ) + } + + private fun WalletState.isLoadingState(): Boolean { + // Check the base components + if (this is WalletState.ContentState) { + walletsListConfig.wallets[walletsListConfig.selectedWalletIndex] is WalletCardState.Loading || + notifications.isEmpty() + } + + // Check the special components + return when (this) { + is WalletMultiCurrencyState -> { + val hasLoadingTokens = tokensListState is WalletTokensListState.ContentState && + (tokensListState as WalletTokensListState.ContentState).items + .filterIsInstance() + .any { it.state is TokenItemState.Loading } + + tokensListState is WalletTokensListState.Loading || hasLoadingTokens + } + is WalletSingleCurrencyState -> { + this is WalletSingleCurrencyState.Content && marketPriceBlockState is MarketPriceBlockState.Loading + } + is WalletState.Initial -> false + } + } + + private fun getWallet(index: Int): UserWallet { + return requireNotNull( + value = wallets.getOrNull(index), + lazyMessage = { "WalletsList doesn't contain element with index = $index" }, + ) + } + + private fun getCardTypeResolver(index: Int): CardTypesResolver = getWallet(index).scanResponse.cardTypesResolver } \ No newline at end of file From 1694ec07beac2a253d627b4886c13f8536d1d492 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 4 Sep 2023 19:41:21 +0800 Subject: [PATCH 012/242] Updated on 2026-08-14 --- .../tangem/tap/features/home/HomeFragment.kt | 2 +- .../presentation/models/TokensListArgs.kt | 22 -- .../viewmodels/TokensListCryptoCurrencies.kt | 9 + .../viewmodels/TokensListMigration.kt | 192 +++++++++++++++++ .../viewmodels/TokensListViewModel.kt | 59 ++++-- .../tokens/legacy/redux/TokensAction.kt | 20 -- .../tokens/legacy/redux/TokensMiddleware.kt | 199 +++++++++++++++--- .../tokens/legacy/redux/TokensReducer.kt | 42 +--- .../tokens/legacy/redux/TokensState.kt | 13 +- .../wallet/ui/wallet/MultiWalletView.kt | 13 +- .../com/tangem/domain/tokens/TokensAction.kt | 36 ++++ .../wallet/viewmodels/WalletViewModel.kt | 1 + 12 files changed, 465 insertions(+), 143 deletions(-) delete mode 100644 app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/models/TokensListArgs.kt create mode 100644 app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/viewmodels/TokensListCryptoCurrencies.kt create mode 100644 app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/viewmodels/TokensListMigration.kt delete mode 100644 app/src/main/java/com/tangem/tap/features/tokens/legacy/redux/TokensAction.kt create mode 100644 domain/legacy/src/main/java/com/tangem/domain/tokens/TokensAction.kt diff --git a/app/src/main/java/com/tangem/tap/features/home/HomeFragment.kt b/app/src/main/java/com/tangem/tap/features/home/HomeFragment.kt index 95b558c438..6332136b24 100644 --- a/app/src/main/java/com/tangem/tap/features/home/HomeFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/home/HomeFragment.kt @@ -17,13 +17,13 @@ import com.tangem.core.analytics.Analytics import com.tangem.core.navigation.AppScreen import com.tangem.core.navigation.NavigationAction import com.tangem.core.ui.res.TangemTheme +import com.tangem.domain.tokens.TokensAction import com.tangem.feature.learn2earn.presentation.Learn2earnViewModel import com.tangem.tap.common.analytics.events.IntroductionProcess import com.tangem.tap.features.home.compose.StoriesScreen import com.tangem.tap.features.home.redux.HomeAction import com.tangem.tap.features.home.redux.HomeState import com.tangem.tap.features.home.redux.Stories -import com.tangem.tap.features.tokens.legacy.redux.TokensAction import com.tangem.tap.store import dagger.hilt.android.AndroidEntryPoint import org.rekotlin.StoreSubscriber diff --git a/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/models/TokensListArgs.kt b/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/models/TokensListArgs.kt deleted file mode 100644 index 207976d628..0000000000 --- a/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/models/TokensListArgs.kt +++ /dev/null @@ -1,22 +0,0 @@ -package com.tangem.tap.features.tokens.impl.presentation.models - -import com.tangem.blockchain.common.Blockchain -import com.tangem.tap.features.tokens.legacy.redux.TokenWithBlockchain -import com.tangem.tap.store - -/** - * Required data for tokens list screen - * FIXME("Necessary to avoid using redux state") - * -[REDACTED_AUTHOR] - */ -class TokensListArgs { - /** Tokens list screen mode */ - val isManageAccess: Boolean get() = store.state.tokensState.isManageAccess - - /** Tokens list that accessible from the main screen */ - val mainScreenTokenList: List get() = store.state.tokensState.addedTokens - - /** Blockchains list that accessible from the main screen */ - val mainScreenBlockchainList: List get() = store.state.tokensState.addedBlockchains -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/viewmodels/TokensListCryptoCurrencies.kt b/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/viewmodels/TokensListCryptoCurrencies.kt new file mode 100644 index 0000000000..da83face06 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/viewmodels/TokensListCryptoCurrencies.kt @@ -0,0 +1,9 @@ +package com.tangem.tap.features.tokens.impl.presentation.viewmodels + +import com.tangem.blockchain.common.Blockchain +import com.tangem.domain.tokens.TokenWithBlockchain + +internal data class TokensListCryptoCurrencies( + val coins: List, + val tokens: List, +) \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/viewmodels/TokensListMigration.kt b/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/viewmodels/TokensListMigration.kt new file mode 100644 index 0000000000..8ce3e83f02 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/viewmodels/TokensListMigration.kt @@ -0,0 +1,192 @@ +package com.tangem.tap.features.tokens.impl.presentation.viewmodels + +import arrow.core.Either +import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchain.common.Token +import com.tangem.blockchain.common.derivation.DerivationStyle +import com.tangem.data.tokens.utils.CryptoCurrencyFactory +import com.tangem.domain.common.util.derivationStyleProvider +import com.tangem.domain.tokens.GetCryptoCurrenciesUseCase +import com.tangem.domain.tokens.TokenWithBlockchain +import com.tangem.domain.tokens.TokensAction +import com.tangem.domain.tokens.models.CryptoCurrency +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.domain.wallets.usecase.GetSelectedWalletUseCase +import com.tangem.features.wallet.featuretoggles.WalletFeatureToggles +import com.tangem.tap.domain.model.WalletDataModel +import com.tangem.tap.features.wallet.models.Currency +import com.tangem.tap.store +import timber.log.Timber +import kotlin.properties.Delegates + +/** + * Class that divide a new and legacy logic when user uses tokens list screen + * + * @property walletFeatureToggles wallet feature toggles + * @property getSelectedWalletUseCase use case that returns selected wallet + * @property getCurrenciesUseCase use case that returns crypto currencies of a specified wallet + */ +internal class TokensListMigration( + private val walletFeatureToggles: WalletFeatureToggles, + private val getSelectedWalletUseCase: GetSelectedWalletUseCase, + private val getCurrenciesUseCase: GetCryptoCurrenciesUseCase, +) { + + private var currentNewCoins: List by Delegates.notNull() + private var currentNewTokens: List by Delegates.notNull() + private var currentUserWallet: UserWallet by Delegates.notNull() + + private val cryptoCurrencyFactory by lazy { CryptoCurrencyFactory() } + + suspend fun getCurrentCryptoCurrencies(): TokensListCryptoCurrencies { + return if (walletFeatureToggles.isRedesignedScreenEnabled) { + getNewCryptoCurrencies() + } else { + getLegacyCryptoCurrencies() + } + } + + private suspend fun getNewCryptoCurrencies(): TokensListCryptoCurrencies { + return when (val selectedWalletEither = getSelectedWalletUseCase()) { + is Either.Left -> { + Timber.e(selectedWalletEither.value.toString()) + TokensListCryptoCurrencies(coins = emptyList(), tokens = emptyList()) + } + is Either.Right -> { + currentUserWallet = selectedWalletEither.value + val derivationStyle = currentUserWallet.scanResponse.derivationStyleProvider.getDerivationStyle() + + when (val currenciesEither = getCurrenciesUseCase(userWalletId = selectedWalletEither.value.walletId)) { + is Either.Left -> { + Timber.e(currenciesEither.value.toString()) + TokensListCryptoCurrencies(coins = emptyList(), tokens = emptyList()) + } + is Either.Right -> { + TokensListCryptoCurrencies( + coins = currenciesEither.value + .filterIsInstance() + .filterNot { it.isCustomCurrency(derivationStyle) } + .also { currentNewCoins = it } + .map { Blockchain.fromId(it.network.id.value) }, + tokens = currenciesEither.value + .filterIsInstance() + .filterNot(CryptoCurrency.Token::isCustom) + .also { currentNewTokens = it } + .map { token -> + TokenWithBlockchain( + token = Token( + name = token.name, + symbol = token.symbol, + contractAddress = token.contractAddress, + decimals = token.decimals, + id = token.id.rawCurrencyId, + ), + blockchain = Blockchain.fromId(token.network.id.value), + ) + }, + ) + } + } + } + } + } + + private fun CryptoCurrency.Coin.isCustomCurrency(derivationStyle: DerivationStyle?): Boolean { + if (derivationPath == null || derivationStyle == null) return false + + return derivationPath != Blockchain.fromId(network.id.value).derivationPath(derivationStyle)?.rawPath + } + + private fun getLegacyCryptoCurrencies(): TokensListCryptoCurrencies { + val wallets = store.state.walletState.walletsDataFromStores + val derivationStyle = store.state.globalState.scanResponse?.derivationStyleProvider?.getDerivationStyle() + + return TokensListCryptoCurrencies( + coins = wallets.toNonCustomBlockchains(derivationStyle), + tokens = wallets.toNonCustomTokensWithBlockchains(derivationStyle), + ) + } + + private fun List.toNonCustomBlockchains(derivationStyle: DerivationStyle?): List { + return this + .mapNotNull { walletDataModel -> + if (walletDataModel.currency.isCustomCurrency(derivationStyle)) { + null + } else { + (walletDataModel.currency as? Currency.Blockchain)?.blockchain + } + } + .distinct() + } + + private fun List.toNonCustomTokensWithBlockchains( + derivationStyle: DerivationStyle?, + ): List { + return this + .mapNotNull { walletDataModel -> + if (walletDataModel.currency !is Currency.Token) return@mapNotNull null + if (walletDataModel.currency.isCustomCurrency(derivationStyle)) return@mapNotNull null + + TokenWithBlockchain(walletDataModel.currency.token, walletDataModel.currency.blockchain) + } + .distinct() + } + + fun onSaveButtonClick( + currentTokensList: List, + currentBlockchainList: List, + changedTokensList: MutableList, + changedBlockchainList: List, + ) { + if (walletFeatureToggles.isRedesignedScreenEnabled) { + saveByNewWay(changedTokensList = changedTokensList, changedBlockchainList = changedBlockchainList) + } else { + saveByOldWay(currentTokensList, currentBlockchainList, changedTokensList, changedBlockchainList) + } + } + + private fun saveByNewWay( + changedTokensList: MutableList, + changedBlockchainList: List, + ) { + store.dispatch( + action = TokensAction.NewSaveChanges( + currentTokens = currentNewTokens, + currentCoins = currentNewCoins, + changedTokens = changedTokensList.mapNotNull { + cryptoCurrencyFactory.createToken( + sdkToken = it.token, + blockchain = it.blockchain, + derivationStyleProvider = currentUserWallet.scanResponse.derivationStyleProvider, + ) + }, + changedCoins = changedBlockchainList.mapNotNull { + cryptoCurrencyFactory.createCoin( + blockchain = it, + derivationStyleProvider = currentUserWallet.scanResponse.derivationStyleProvider, + ) + }, + userWallet = currentUserWallet, + ), + ) + } + + private fun saveByOldWay( + currentTokensList: List, + currentBlockchainList: List, + changedTokensList: MutableList, + changedBlockchainList: List, + ) { + val scanResponse = store.state.globalState.scanResponse ?: return + + store.dispatch( + action = TokensAction.LegacySaveChanges( + currentTokens = currentTokensList, + currentBlockchains = currentBlockchainList, + changedTokens = changedTokensList, + changedBlockchains = changedBlockchainList, + scanResponse = scanResponse, + ), + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/viewmodels/TokensListViewModel.kt b/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/viewmodels/TokensListViewModel.kt index 78ff6ff6ce..05237c3172 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/viewmodels/TokensListViewModel.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/viewmodels/TokensListViewModel.kt @@ -19,6 +19,10 @@ import com.tangem.domain.common.extensions.canHandleToken import com.tangem.domain.common.extensions.fromNetworkId import com.tangem.domain.common.extensions.supportedTokens import com.tangem.domain.common.util.cardTypesResolver +import com.tangem.domain.tokens.GetCryptoCurrenciesUseCase +import com.tangem.domain.tokens.TokenWithBlockchain +import com.tangem.domain.wallets.usecase.GetSelectedWalletUseCase +import com.tangem.features.wallet.featuretoggles.WalletFeatureToggles import com.tangem.tap.common.extensions.fullNameWithoutTestnet import com.tangem.tap.common.extensions.getGreyedOutIconRes import com.tangem.tap.common.extensions.getNetworkName @@ -26,14 +30,11 @@ import com.tangem.tap.features.tokens.impl.domain.TokensListInteractor import com.tangem.tap.features.tokens.impl.domain.models.Token import com.tangem.tap.features.tokens.impl.domain.models.Token.Network import com.tangem.tap.features.tokens.impl.presentation.models.SupportTokensState -import com.tangem.tap.features.tokens.impl.presentation.models.TokensListArgs import com.tangem.tap.features.tokens.impl.presentation.router.TokensListRouter import com.tangem.tap.features.tokens.impl.presentation.states.NetworkItemState import com.tangem.tap.features.tokens.impl.presentation.states.TokenItemState import com.tangem.tap.features.tokens.impl.presentation.states.TokensListStateHolder import com.tangem.tap.features.tokens.impl.presentation.states.TokensListToolbarState -import com.tangem.tap.features.tokens.legacy.redux.TokenWithBlockchain -import com.tangem.tap.features.tokens.legacy.redux.TokensAction import com.tangem.tap.proxy.AppStateHolder import com.tangem.tap.store import com.tangem.utils.coroutines.AppCoroutineDispatcherProvider @@ -43,9 +44,11 @@ import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map +import kotlinx.coroutines.launch import kotlinx.coroutines.plus import timber.log.Timber import javax.inject.Inject +import kotlin.properties.Delegates import com.tangem.blockchain.common.Token as BlockchainToken /** @@ -59,6 +62,7 @@ import com.tangem.blockchain.common.Token as BlockchainToken * [REDACTED_AUTHOR] */ +@Suppress("LongParameterList") @HiltViewModel internal class TokensListViewModel @Inject constructor( private val interactor: TokensListInteractor, @@ -66,9 +70,12 @@ internal class TokensListViewModel @Inject constructor( private val dispatchers: AppCoroutineDispatcherProvider, private val reduxStateHolder: AppStateHolder, analyticsEventHandler: AnalyticsEventHandler, + getCurrenciesUseCase: GetCryptoCurrenciesUseCase, + getSelectedWalletUseCase: GetSelectedWalletUseCase, + walletFeatureToggles: WalletFeatureToggles, ) : ViewModel(), DefaultLifecycleObserver { - private val args = TokensListArgs() + private val isManageAccess = store.state.tokensState.isManageAccess private val analyticsSender = TokensListAnalyticsSender(analyticsEventHandler) private val actionsHandler = ActionsHandler(router = router, debouncer = Debouncer()) @@ -76,15 +83,36 @@ internal class TokensListViewModel @Inject constructor( var uiState by mutableStateOf(value = getInitialUiState()) private set - private val changedTokensList: MutableList = args.mainScreenTokenList.toMutableList() - private val changedBlockchainList: MutableList = args.mainScreenBlockchainList.toMutableList() + private var currentTokensList: List by Delegates.notNull() + private var currentBlockchainList: List by Delegates.notNull() + + private var changedTokensList: MutableList by Delegates.notNull() + private var changedBlockchainList: MutableList by Delegates.notNull() + + private val tokensListMigration = TokensListMigration( + walletFeatureToggles = walletFeatureToggles, + getSelectedWalletUseCase = getSelectedWalletUseCase, + getCurrenciesUseCase = getCurrenciesUseCase, + ) + + init { + viewModelScope.launch(dispatchers.main) { + val (currentCoins, currentTokens) = tokensListMigration.getCurrentCryptoCurrencies() + + currentBlockchainList = currentCoins + currentTokensList = currentTokens + + changedBlockchainList = currentCoins.toMutableList() + changedTokensList = currentTokens.toMutableList() + } + } override fun onCreate(owner: LifecycleOwner) { - if (args.isManageAccess) analyticsSender.sendWhenScreenOpened() + if (isManageAccess) analyticsSender.sendWhenScreenOpened() } private fun getInitialUiState(): TokensListStateHolder { - return if (args.isManageAccess) { + return if (isManageAccess) { TokensListStateHolder.ManageContent( toolbarState = getInitialToolbarState(), isLoading = true, @@ -105,7 +133,7 @@ internal class TokensListViewModel @Inject constructor( } private fun getInitialToolbarState(): TokensListToolbarState { - return if (args.isManageAccess) { + return if (isManageAccess) { TokensListToolbarState.Title.Manage( titleResId = R.string.add_tokens_title, onBackButtonClick = actionsHandler::onBackButtonClick, @@ -130,7 +158,7 @@ internal class TokensListViewModel @Inject constructor( return interactor.getTokensList(searchText = searchText).map { it.map { token -> - if (args.isManageAccess) createManageTokenContent(token) else createReadTokenContent(token) + if (isManageAccess) createManageTokenContent(token) else createReadTokenContent(token) } } } @@ -264,7 +292,12 @@ internal class TokensListViewModel @Inject constructor( fun onSaveButtonClick() { analyticsSender.sendWhenSaveButtonClicked() - store.dispatch(TokensAction.SaveChanges(changedTokensList, changedBlockchainList)) + tokensListMigration.onSaveButtonClick( + currentTokensList = currentTokensList, + currentBlockchainList = currentBlockchainList, + changedTokensList = changedTokensList, + changedBlockchainList = changedBlockchainList, + ) } private fun onSearchValueChange(newValue: String) { @@ -291,7 +324,7 @@ internal class TokensListViewModel @Inject constructor( if (isRemoveAction) { val isTokenWithSameBlockchainFound = changedTokensList.any { it.blockchain == blockchain } - val isAddedOnMainScreen = args.mainScreenBlockchainList.contains(blockchain) + val isAddedOnMainScreen = currentBlockchainList.contains(blockchain) if (isTokenWithSameBlockchainFound) { router.openUnableHideMainTokenAlert( @@ -341,7 +374,7 @@ internal class TokensListViewModel @Inject constructor( val isRemoveAction = changedTokensList.contains(token) if (isRemoveAction) { - val isAddedOnMainScreen = args.mainScreenTokenList.contains(token) + val isAddedOnMainScreen = currentTokensList.contains(token) if (isAddedOnMainScreen) { router.openRemoveWalletAlert( diff --git a/app/src/main/java/com/tangem/tap/features/tokens/legacy/redux/TokensAction.kt b/app/src/main/java/com/tangem/tap/features/tokens/legacy/redux/TokensAction.kt deleted file mode 100644 index fb79c7bbde..0000000000 --- a/app/src/main/java/com/tangem/tap/features/tokens/legacy/redux/TokensAction.kt +++ /dev/null @@ -1,20 +0,0 @@ -package com.tangem.tap.features.tokens.legacy.redux - -import com.tangem.blockchain.common.Blockchain -import com.tangem.blockchain.common.derivation.DerivationStyle -import com.tangem.tap.domain.model.WalletDataModel -import org.rekotlin.Action - -sealed interface TokensAction : Action { - - /** Single way to pass data to the screen */ - sealed interface SetArgs : TokensAction { - - data class ManageAccess(val wallets: List, val derivationStyle: DerivationStyle?) : SetArgs - - object ReadAccess : SetArgs - } - - // TODO: [REDACTED_TASK_KEY] Remove this action - data class SaveChanges(val tokens: List, val blockchains: List) : TokensAction -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/tokens/legacy/redux/TokensMiddleware.kt b/app/src/main/java/com/tangem/tap/features/tokens/legacy/redux/TokensMiddleware.kt index db87942c84..012bdf1618 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/legacy/redux/TokensMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/legacy/redux/TokensMiddleware.kt @@ -15,6 +15,10 @@ import com.tangem.domain.common.configs.CardConfig import com.tangem.domain.common.util.derivationStyleProvider import com.tangem.domain.common.util.supportsHdWallet import com.tangem.domain.models.scan.ScanResponse +import com.tangem.domain.tokens.TokenWithBlockchain +import com.tangem.domain.tokens.TokensAction +import com.tangem.domain.tokens.models.CryptoCurrency +import com.tangem.domain.wallets.models.UserWalletId import com.tangem.operations.derivation.ExtendedPublicKeysMap import com.tangem.tap.* import com.tangem.tap.common.extensions.dispatchDebugErrorNotification @@ -23,6 +27,7 @@ import com.tangem.tap.common.redux.AppState import com.tangem.tap.common.redux.global.GlobalAction import com.tangem.tap.domain.TapError import com.tangem.tap.features.wallet.models.Currency +import com.tangem.tap.proxy.redux.DaggerGraphState import kotlinx.coroutines.delay import kotlinx.coroutines.launch import org.rekotlin.Middleware @@ -34,28 +39,69 @@ object TokensMiddleware { { next -> { action -> when (action) { - is TokensAction.SaveChanges -> handleSaveChanges(action) + is TokensAction.LegacySaveChanges -> handleLegacySaveChanges(action) + is TokensAction.NewSaveChanges -> handleNewSaveChanges(action) } next(action) } } } - private fun handleSaveChanges(action: TokensAction.SaveChanges) { + private fun handleNewSaveChanges(action: TokensAction.NewSaveChanges) { scope.launch { - val scanResponse = store.state.globalState.scanResponse ?: return@launch + val scanResponse = action.userWallet.scanResponse - val currentTokens = store.state.tokensState.addedTokens - val currentBlockchains = store.state.tokensState.addedBlockchains + val currentTokens = action.currentTokens + val currentBlockchains = action.currentCoins - val blockchainsToAdd = action.blockchains.filterNot(currentBlockchains::contains) - val blockchainsToRemove = - store.state.tokensState.addedBlockchains.filterNot(action.blockchains::contains) + val blockchainsToAdd = action.changedCoins.filterNot(currentBlockchains::contains) + val blockchainsToRemove = currentBlockchains.filterNot(action.changedCoins::contains) - val tokensToAdd = action.tokens.filterNot(currentTokens::contains) - val tokensToRemove = currentTokens.filterNot { token -> action.tokens.any { it.token == token.token } } + val tokensToAdd = action.changedTokens.filterNot(currentTokens::contains) + val tokensToRemove = currentTokens.filterNot { token -> action.changedTokens.any { it == token } } - removeCurrenciesIfNeeded( + removeNewCurrenciesIfNeeded( + userWalletId = action.userWallet.walletId, + currencies = blockchainsToRemove + tokensToRemove, + ) + + val isNothingToDoWithTokens = tokensToAdd.isEmpty() && tokensToRemove.isEmpty() + val isNothingToDoWithBlockchain = blockchainsToAdd.isEmpty() && blockchainsToRemove.isEmpty() + if (isNothingToDoWithTokens && isNothingToDoWithBlockchain) { + store.dispatchDebugErrorNotification(message = "Nothing to save") + store.dispatchOnMain(NavigationAction.PopBackTo()) + return@launch + } + + val currencyList = blockchainsToAdd + tokensToAdd + + if (scanResponse.supportsHdWallet()) { + deriveMissingCoins(scanResponse = scanResponse, currencyList = currencyList) { + submitNewAdd(userWalletId = action.userWallet.walletId, currencyList = currencyList) + store.dispatchOnMain(NavigationAction.PopBackTo()) + } + } else { + submitNewAdd(userWalletId = action.userWallet.walletId, currencyList = currencyList) + store.dispatchOnMain(NavigationAction.PopBackTo()) + } + } + } + + private fun handleLegacySaveChanges(action: TokensAction.LegacySaveChanges) { + scope.launch { + val scanResponse = action.scanResponse + + val currentTokens = action.currentTokens + val currentBlockchains = action.currentBlockchains + + val blockchainsToAdd = action.changedBlockchains.filterNot(currentBlockchains::contains) + val blockchainsToRemove = currentBlockchains.filterNot(action.changedBlockchains::contains) + + val tokensToAdd = action.changedTokens.filterNot(currentTokens::contains) + val tokensToRemove = + currentTokens.filterNot { token -> action.changedTokens.any { it.token == token.token } } + + removeLegacyCurrenciesIfNeeded( currencies = convertToCurrencies( blockchains = blockchainsToRemove, tokens = tokensToRemove, @@ -79,11 +125,11 @@ object TokensMiddleware { if (scanResponse.supportsHdWallet()) { deriveMissingBlockchains(scanResponse, currencyList) { - submitAdd(it, currencyList) + submitLegacyAdd(it, currencyList) store.dispatchOnMain(NavigationAction.PopBackTo()) } } else { - submitAdd(scanResponse, currencyList) + submitLegacyAdd(scanResponse, currencyList) store.dispatchOnMain(NavigationAction.PopBackTo()) } } @@ -94,15 +140,14 @@ object TokensMiddleware { tokens: List, derivationStyle: DerivationStyle?, ): List { - return blockchains.map { - Currency.Blockchain(it, it.derivationPath(derivationStyle)?.rawPath) - } + tokens.map { - Currency.Token( - it.token, - it.blockchain, - it.blockchain.derivationPath(derivationStyle)?.rawPath, - ) - } + return blockchains.map { Currency.Blockchain(it, it.derivationPath(derivationStyle)?.rawPath) } + + tokens.map { + Currency.Token( + token = it.token, + blockchain = it.blockchain, + derivationPath = it.blockchain.derivationPath(derivationStyle)?.rawPath, + ) + } } private fun deriveMissingBlockchains( @@ -113,7 +158,7 @@ object TokensMiddleware { val config = CardConfig.createConfig(scanResponse.card) val derivationDataList = currencyList.mapNotNull { val curve = config.primaryCurve(it.blockchain) - curve?.let { getDerivations(curve, scanResponse, currencyList) } + curve?.let { getLegacyDerivations(curve, scanResponse, currencyList) } } val derivations = derivationDataList.associate { it.derivations } if (derivations.isEmpty()) { @@ -153,7 +198,55 @@ object TokensMiddleware { } } - private fun getDerivations( + private fun deriveMissingCoins( + scanResponse: ScanResponse, + currencyList: List, + onSuccess: (ScanResponse) -> Unit, + ) { + val config = CardConfig.createConfig(scanResponse.card) + val derivationDataList = currencyList.mapNotNull { + config.primaryCurve(blockchain = Blockchain.fromId(it.network.id.value)) + ?.let { curve -> getNewDerivations(curve, scanResponse, currencyList) } + } + val derivations = derivationDataList.associate(DerivationData::derivations) + if (derivations.isEmpty()) { + onSuccess(scanResponse) + return + } + + scope.launch { + val result = tangemSdkManager.derivePublicKeys( + cardId = null, + derivations = derivations, + ) + when (result) { + is CompletionResult.Success -> { + val newDerivedKeys = result.data.entries + val oldDerivedKeys = scanResponse.derivedKeys + + val walletKeys = (newDerivedKeys.keys + oldDerivedKeys.keys).toSet() + + val updatedDerivedKeys = walletKeys.associateWith { walletKey -> + val oldDerivations = ExtendedPublicKeysMap(oldDerivedKeys[walletKey] ?: emptyMap()) + val newDerivations = newDerivedKeys[walletKey] ?: ExtendedPublicKeysMap(emptyMap()) + ExtendedPublicKeysMap(oldDerivations + newDerivations) + } + val updatedScanResponse = scanResponse.copy( + derivedKeys = updatedDerivedKeys, + ) + store.dispatchOnMain(GlobalAction.SaveScanResponse(updatedScanResponse)) + delay(DELAY_SDK_DIALOG_CLOSE) + + onSuccess(updatedScanResponse) + } + is CompletionResult.Failure -> { + store.dispatchDebugErrorNotification(TapError.CustomError("Error adding tokens")) + } + } + } + } + + private fun getLegacyDerivations( curve: EllipticCurve, scanResponse: ScanResponse, currencyList: List, @@ -190,9 +283,48 @@ object TokensMiddleware { return DerivationData(derivations = mapKeyOfWalletPublicKey to toDerive) } + private fun getNewDerivations( + curve: EllipticCurve, + scanResponse: ScanResponse, + currencyList: List, + ): DerivationData? { + val wallet = scanResponse.card.wallets.firstOrNull { it.curve == curve } ?: return null + + val manageTokensCandidates = currencyList + .map { Blockchain.fromId(it.network.id.value) } + .distinct() + .filter { it.getSupportedCurves().contains(curve) } + .mapNotNull { it.derivationPath(scanResponse.derivationStyleProvider.getDerivationStyle()) } + + val customTokensCandidates = currencyList + .filter { Blockchain.fromId(it.network.id.value).getSupportedCurves().contains(curve) } + .mapNotNull(CryptoCurrency::derivationPath) + .map(::DerivationPath) + + val bothCandidates = (manageTokensCandidates + customTokensCandidates).distinct().toMutableList() + if (bothCandidates.isEmpty()) return null + + currencyList.find { it is CryptoCurrency.Coin && Blockchain.fromId(it.network.id.value) == Blockchain.Cardano } + ?.let { currency -> + currency.derivationPath?.let { + bothCandidates.add(CardanoUtils.extendedDerivationPath(DerivationPath(it))) + } + } + + val mapKeyOfWalletPublicKey = wallet.publicKey.toMapKey() + val alreadyDerivedKeys: ExtendedPublicKeysMap = + scanResponse.derivedKeys[mapKeyOfWalletPublicKey] ?: ExtendedPublicKeysMap(emptyMap()) + val alreadyDerivedPaths = alreadyDerivedKeys.keys.toList() + + val toDerive = bothCandidates.filterNot { alreadyDerivedPaths.contains(it) } + if (toDerive.isEmpty()) return null + + return DerivationData(derivations = mapKeyOfWalletPublicKey to toDerive) + } + class DerivationData(val derivations: Pair>) - private fun submitAdd(scanResponse: ScanResponse, currencyList: List) { + private fun submitLegacyAdd(scanResponse: ScanResponse, currencyList: List) { val selectedUserWallet = userWalletsListManager.selectedUserWalletSync.guard { Timber.e("Unable to add currencies, no user wallet selected") return @@ -213,7 +345,15 @@ object TokensMiddleware { } } - private suspend fun removeCurrenciesIfNeeded(currencies: List) { + private fun submitNewAdd(userWalletId: UserWalletId, currencyList: List) { + val currenciesRepository = store.state.daggerGraphState.get(DaggerGraphState::currenciesRepository) + + scope.launch { + currenciesRepository.addCurrencies(userWalletId = userWalletId, currencies = currencyList) + } + } + + private suspend fun removeLegacyCurrenciesIfNeeded(currencies: List) { if (currencies.isEmpty()) return val selectedUserWallet = userWalletsListManager.selectedUserWalletSync.guard { Timber.e("Unable to remove currencies, no user wallet selected") @@ -221,4 +361,11 @@ object TokensMiddleware { } walletCurrenciesManager.removeCurrencies(selectedUserWallet, currencies) } + + private suspend fun removeNewCurrenciesIfNeeded(userWalletId: UserWalletId, currencies: List) { + if (currencies.isEmpty()) return + val currenciesRepository = store.state.daggerGraphState.get(DaggerGraphState::currenciesRepository) + + currenciesRepository.removeCurrencies(userWalletId = userWalletId, currencies = currencies) + } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/tokens/legacy/redux/TokensReducer.kt b/app/src/main/java/com/tangem/tap/features/tokens/legacy/redux/TokensReducer.kt index 8e33fdc8f4..389a02219a 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/legacy/redux/TokensReducer.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/legacy/redux/TokensReducer.kt @@ -1,11 +1,7 @@ package com.tangem.tap.features.tokens.legacy.redux -import com.tangem.blockchain.common.Blockchain -import com.tangem.blockchain.common.derivation.DerivationStyle +import com.tangem.domain.tokens.TokensAction import com.tangem.tap.common.redux.AppState -import com.tangem.tap.domain.model.WalletDataModel -import com.tangem.tap.features.wallet.models.Currency -import com.tangem.tap.features.wallet.models.Currency.Token import org.rekotlin.Action object TokensReducer { @@ -16,40 +12,8 @@ private fun internalReduce(action: Action, state: AppState): TokensState { if (action !is TokensAction) return state.tokensState return when (action) { - is TokensAction.SetArgs.ManageAccess -> { - state.tokensState.copy( - isManageAccess = true, - addedWallets = action.wallets, - addedBlockchains = action.wallets.toNonCustomBlockchains(action.derivationStyle), - addedTokens = action.wallets.toNonCustomTokensWithBlockchains(action.derivationStyle), - ) - } - - is TokensAction.SetArgs.ReadAccess -> { - state.tokensState.copy(isManageAccess = false) - } - + is TokensAction.SetArgs.ManageAccess -> state.tokensState.copy(isManageAccess = true) + is TokensAction.SetArgs.ReadAccess -> state.tokensState.copy(isManageAccess = false) else -> state.tokensState } -} - -private fun List.toNonCustomBlockchains(derivationStyle: DerivationStyle?): List { - return mapNotNull { walletDataModel -> - if (walletDataModel.currency.isCustomCurrency(derivationStyle)) { - null - } else { - (walletDataModel.currency as? Currency.Blockchain)?.blockchain - } - }.distinct() -} - -private fun List.toNonCustomTokensWithBlockchains( - derivationStyle: DerivationStyle?, -): List { - return mapNotNull { walletDataModel -> - if (walletDataModel.currency !is Token) return@mapNotNull null - if (walletDataModel.currency.isCustomCurrency(derivationStyle)) return@mapNotNull null - - TokenWithBlockchain(walletDataModel.currency.token, walletDataModel.currency.blockchain) - }.distinct() } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/tokens/legacy/redux/TokensState.kt b/app/src/main/java/com/tangem/tap/features/tokens/legacy/redux/TokensState.kt index 156fd0c922..14c5d9d721 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/legacy/redux/TokensState.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/legacy/redux/TokensState.kt @@ -1,16 +1,5 @@ package com.tangem.tap.features.tokens.legacy.redux -import com.tangem.blockchain.common.Blockchain -import com.tangem.blockchain.common.Token -import com.tangem.tap.domain.model.WalletDataModel import org.rekotlin.StateType -data class TokensState( - val isManageAccess: Boolean = false, - val addedWallets: List = emptyList(), - val addedTokens: List = emptyList(), - val addedBlockchains: List = emptyList(), -) : StateType - -// TODO: [REDACTED_TASK_KEY] Remove this class -data class TokenWithBlockchain(val token: Token, val blockchain: Blockchain) \ No newline at end of file +data class TokensState(val isManageAccess: Boolean = false) : StateType \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/wallet/ui/wallet/MultiWalletView.kt b/app/src/main/java/com/tangem/tap/features/wallet/ui/wallet/MultiWalletView.kt index 8f17e52682..290bd68881 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/ui/wallet/MultiWalletView.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/ui/wallet/MultiWalletView.kt @@ -6,7 +6,7 @@ import com.badoo.mvicore.modelWatcher import com.tangem.core.analytics.Analytics import com.tangem.core.navigation.AppScreen import com.tangem.core.navigation.NavigationAction -import com.tangem.domain.common.util.derivationStyleProvider +import com.tangem.domain.tokens.TokensAction import com.tangem.tap.common.analytics.events.MainScreen import com.tangem.tap.common.analytics.events.Portfolio import com.tangem.tap.common.entities.FiatCurrency @@ -14,7 +14,6 @@ import com.tangem.tap.common.extensions.getQuantityString import com.tangem.tap.common.extensions.hide import com.tangem.tap.common.extensions.show import com.tangem.tap.domain.model.TotalFiatBalance -import com.tangem.tap.features.tokens.legacy.redux.TokensAction import com.tangem.tap.features.wallet.redux.ErrorType import com.tangem.tap.features.wallet.redux.WalletAction import com.tangem.tap.features.wallet.redux.WalletState @@ -106,14 +105,8 @@ class MultiWalletView : WalletView() { binding.btnAddToken.setOnClickListener { Analytics.send(Portfolio.ButtonManageTokens()) - store.dispatch( - TokensAction.SetArgs.ManageAccess( - wallets = state.walletsDataFromStores, - derivationStyle = store.state.globalState.scanResponse - ?.derivationStyleProvider?.getDerivationStyle(), - ), - ) - store.dispatch(NavigationAction.NavigateTo(AppScreen.AddTokens)) + store.dispatch(action = TokensAction.SetArgs.ManageAccess) + store.dispatch(action = NavigationAction.NavigateTo(screen = AppScreen.AddTokens)) } handleErrorStates(state = state, binding = binding, fragment = fragment) } diff --git a/domain/legacy/src/main/java/com/tangem/domain/tokens/TokensAction.kt b/domain/legacy/src/main/java/com/tangem/domain/tokens/TokensAction.kt new file mode 100644 index 0000000000..4dcca75874 --- /dev/null +++ b/domain/legacy/src/main/java/com/tangem/domain/tokens/TokensAction.kt @@ -0,0 +1,36 @@ +package com.tangem.domain.tokens + +import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchain.common.Token +import com.tangem.domain.models.scan.ScanResponse +import com.tangem.domain.tokens.models.CryptoCurrency +import com.tangem.domain.wallets.models.UserWallet +import org.rekotlin.Action + +sealed interface TokensAction : Action { + + /** Single way to pass data to the screen */ + sealed interface SetArgs : TokensAction { + object ManageAccess : SetArgs + object ReadAccess : SetArgs + } + + @Deprecated("Action is used for saving data by old way. It will be removed after deleting of legacy wallet screen") + data class LegacySaveChanges( + val currentTokens: List, + val currentBlockchains: List, + val changedTokens: List, + val changedBlockchains: List, + val scanResponse: ScanResponse, + ) : TokensAction + + data class NewSaveChanges( + val currentTokens: List, + val currentCoins: List, + val changedTokens: List, + val changedCoins: List, + val userWallet: UserWallet, + ) : TokensAction +} + +data class TokenWithBlockchain(val token: Token, val blockchain: Blockchain) \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt index 1f2483b904..4dc7b9293c 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt @@ -332,6 +332,7 @@ internal class WalletViewModel @Inject constructor( } override fun onManageTokensClick() { + reduxStateHolder.dispatch(action = TokensAction.SetArgs.ManageAccess) router.openManageTokensScreen() } From 1061426e87ec96b9bda4514c51f820e12f960e85 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 5 Sep 2023 12:08:56 +0300 Subject: [PATCH 013/242] Updated on 2026-08-14 --- .../features/details/redux/DetailsReducer.kt | 9 +- .../features/details/redux/DetailsState.kt | 2 +- .../ui/appsettings/AppSettingsItemsFactory.kt | 11 +++ .../ui/appsettings/AppSettingsScreen.kt | 6 ++ .../ui/appsettings/AppSettingsScreenState.kt | 8 ++ .../ui/appsettings/AppSettingsViewModel.kt | 10 +++ .../ui/appsettings/components/ButtonItem.kt | 80 ++++++++++++++++++ .../details/ui/details/DetailsScreen.kt | 82 +++++++++++-------- .../details/ui/details/DetailsScreenState.kt | 10 +-- .../details/ui/details/DetailsViewModel.kt | 5 -- 10 files changed, 176 insertions(+), 47 deletions(-) create mode 100644 app/src/main/java/com/tangem/tap/features/details/ui/appsettings/components/ButtonItem.kt diff --git a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsReducer.kt b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsReducer.kt index 71e4d6b2dd..77c3972fac 100644 --- a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsReducer.kt +++ b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsReducer.kt @@ -39,8 +39,11 @@ private fun internalReduce(action: Action, state: AppState): DetailsState { is DetailsAction.AppSettings -> { handlePrivacyAction(action, detailsState) } - is DetailsAction.ChangeAppCurrency -> - detailsState.copy(appCurrency = action.fiatCurrency) + is DetailsAction.ChangeAppCurrency -> detailsState.copy( + appSettingsState = detailsState.appSettingsState.copy( + selectedFiatCurrency = action.fiatCurrency, + ), + ) is DetailsAction.AccessCodeRecovery -> handleAccessCodeRecoveryAction(action, detailsState) else -> detailsState } @@ -50,11 +53,11 @@ private fun handlePrepareScreen(action: DetailsAction.PrepareScreen): DetailsSta return DetailsState( scanResponse = action.scanResponse, createBackupAllowed = action.scanResponse.card.backupStatus == CardDTO.BackupStatus.NoBackup, - appCurrency = store.state.globalState.appCurrency, appSettingsState = AppSettingsState( isBiometricsAvailable = tangemSdkManager.canUseBiometry, saveWallets = preferencesStorage.shouldSaveUserWallets, saveAccessCodes = preferencesStorage.shouldSaveAccessCodes, + selectedFiatCurrency = store.state.globalState.appCurrency, ), ) } diff --git a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsState.kt b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsState.kt index 6ad80685b2..3dcf9ddd33 100644 --- a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsState.kt +++ b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsState.kt @@ -12,7 +12,6 @@ data class DetailsState( val cardSettingsState: CardSettingsState? = null, val privacyPolicyUrl: String? = null, val createBackupAllowed: Boolean = false, - val appCurrency: FiatCurrency = FiatCurrency.Default, val appSettingsState: AppSettingsState = AppSettingsState(), ) : StateType @@ -55,6 +54,7 @@ data class AppSettingsState( val isBiometricsAvailable: Boolean = false, val needEnrollBiometrics: Boolean = false, val isInProgress: Boolean = false, + val selectedFiatCurrency: FiatCurrency = FiatCurrency.Default, ) enum class SecurityOption { LongTap, PassCode, AccessCode } diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsItemsFactory.kt b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsItemsFactory.kt index 464411faa7..a6a23da4b5 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsItemsFactory.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsItemsFactory.kt @@ -1,6 +1,7 @@ package com.tangem.tap.features.details.ui.appsettings import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.stringReference import com.tangem.tap.features.details.ui.appsettings.AppSettingsScreenState.Item import com.tangem.wallet.R @@ -45,4 +46,14 @@ internal class AppSettingsItemsFactory { onCheckedChange = onCheckedChange, ) } + + fun createSelectAppCurrencyButton(currentAppCurrencyName: String, onClick: () -> Unit): Item.Button { + return Item.Button( + id = "select_app_currency_button", + title = resourceReference(R.string.details_row_title_currency), + description = stringReference(currentAppCurrencyName), + isEnabled = true, + onClick = onClick, + ) + } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsScreen.kt b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsScreen.kt index 05405f7d3d..aff04f20a9 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsScreen.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsScreen.kt @@ -12,6 +12,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider import com.tangem.core.ui.res.TangemTheme import com.tangem.tap.features.details.ui.appsettings.AppSettingsScreenState.Item +import com.tangem.tap.features.details.ui.appsettings.components.ButtonItem import com.tangem.tap.features.details.ui.appsettings.components.CardItem import com.tangem.tap.features.details.ui.appsettings.components.SettingsAlertDialog import com.tangem.tap.features.details.ui.appsettings.components.SwitchItem @@ -51,6 +52,10 @@ private fun AppSettings(state: AppSettingsScreenState.Content) { modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing16), item = item, ) + is Item.Button -> ButtonItem( + modifier = Modifier.padding(vertical = TangemTheme.dimens.spacing8), + item = item, + ) is Item.Switch -> SwitchItem( modifier = Modifier.padding( vertical = TangemTheme.dimens.spacing16, @@ -90,6 +95,7 @@ private class AppSettingsScreenStateProvider : CollectionPreviewParameterProvide val dialogsFactory = AppSettingsAlertsFactory() val items = persistentListOf( itemsFactory.createEnrollBiometricsCard {}, + itemsFactory.createSelectAppCurrencyButton(currentAppCurrencyName = "US Dollar") {}, itemsFactory.createSaveWalletsSwitch(isChecked = true, isEnabled = true, { _ -> }), itemsFactory.createSaveAccessCodeSwitch(isChecked = false, isEnabled = true) { _ -> }, ) diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsScreenState.kt b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsScreenState.kt index bee5e3241b..4802004976 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsScreenState.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsScreenState.kt @@ -36,6 +36,14 @@ internal sealed class AppSettingsScreenState { val isChecked: Boolean, val onCheckedChange: (Boolean) -> Unit, ) : Item() + + data class Button( + override val id: String, + val title: TextReference, + val description: TextReference, + val isEnabled: Boolean, + val onClick: () -> Unit, + ) : Item() } data class Alert( diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsViewModel.kt b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsViewModel.kt index 98b3cead3b..5aedd6f7b7 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsViewModel.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsViewModel.kt @@ -9,6 +9,7 @@ import com.tangem.tap.features.details.redux.AppSetting import com.tangem.tap.features.details.redux.AppSettingsState import com.tangem.tap.features.details.redux.DetailsAction import com.tangem.tap.features.details.redux.DetailsState +import com.tangem.tap.features.wallet.redux.WalletAction import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList import org.rekotlin.Store @@ -42,6 +43,11 @@ internal class AppSettingsViewModel(private val store: Store) { itemsFactory.createEnrollBiometricsCard(onClick = ::enrollBiometrics).let(::add) } + itemsFactory.createSelectAppCurrencyButton( + currentAppCurrencyName = state.selectedFiatCurrency.name, + onClick = ::showAppCurrencySelector, + ).let(::add) + if (state.isBiometricsAvailable) { val canUseBiometrics = !state.needEnrollBiometrics && !state.isInProgress @@ -66,6 +72,10 @@ internal class AppSettingsViewModel(private val store: Store) { store.dispatchOnMain(DetailsAction.AppSettings.EnrollBiometrics) } + private fun showAppCurrencySelector() { + store.dispatchOnMain(WalletAction.AppCurrencyAction.ChooseAppCurrency) + } + private fun onSaveWalletsToggled(isChecked: Boolean) { if (isChecked) { onSettingsToggled(AppSetting.SaveWallets, enable = true) diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/components/ButtonItem.kt b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/components/ButtonItem.kt new file mode 100644 index 0000000000..4010f633ed --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/components/ButtonItem.kt @@ -0,0 +1,80 @@ +package com.tangem.tap.features.details.ui.appsettings.components + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material.ExperimentalMaterialApi +import androidx.compose.material.Surface +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.res.TangemTheme +import com.tangem.tap.features.details.ui.appsettings.AppSettingsItemsFactory +import com.tangem.tap.features.details.ui.appsettings.AppSettingsScreenState.Item + +@OptIn(ExperimentalMaterialApi::class) +@Composable +internal fun ButtonItem(item: Item.Button, modifier: Modifier = Modifier) { + Surface( + modifier = modifier.fillMaxWidth(), + color = TangemTheme.colors.background.secondary, + onClick = item.onClick, + ) { + Column( + modifier = Modifier.padding( + horizontal = TangemTheme.dimens.spacing20, + vertical = TangemTheme.dimens.spacing8, + ), + verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing4), + horizontalAlignment = Alignment.Start, + ) { + Text( + modifier = Modifier.fillMaxWidth(), + text = item.title.resolveReference(), + style = TangemTheme.typography.subtitle1, + color = TangemTheme.colors.text.primary1, + ) + Text( + modifier = Modifier.fillMaxWidth(), + text = item.description.resolveReference(), + style = TangemTheme.typography.body2, + color = TangemTheme.colors.text.secondary, + ) + } + } +} + +// region Preview +@Preview(showBackground = true, widthDp = 360) +@Composable +private fun ButtonItemPreview_Light(@PreviewParameter(ButtonItemProvider::class) item: Item.Button) { + TangemTheme { + ButtonItem(item = item) + } +} + +@Preview(showBackground = true, widthDp = 360) +@Composable +private fun ButtonItemPreview_Dark(@PreviewParameter(ButtonItemProvider::class) item: Item.Button) { + TangemTheme(isDark = true) { + ButtonItem(item = item) + } +} + +private class ButtonItemProvider : CollectionPreviewParameterProvider( + collection = buildList { + val itemsFactory = AppSettingsItemsFactory() + + itemsFactory.createSelectAppCurrencyButton( + currentAppCurrencyName = "US Dollar", + onClick = { /* no-op */ }, + ).let(::add) + }, +) +// endregion Preview \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsScreen.kt b/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsScreen.kt index b853168363..2326b3db65 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsScreen.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsScreen.kt @@ -20,6 +20,8 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import com.tangem.core.ui.components.SpacerH +import com.tangem.core.ui.components.SpacerHMax import com.tangem.core.ui.res.TangemColorPalette import com.tangem.core.ui.res.TangemTheme import com.tangem.tap.features.details.ui.common.ScreenTitle @@ -44,39 +46,53 @@ private fun Content(state: DetailsScreenState, modifier: Modifier = Modifier) { .fillMaxSize() .verticalScroll(rememberScrollState()), ) { - ScreenTitle(titleRes = R.string.details_title, Modifier.padding(bottom = 52.dp)) - state.elements.map { element -> - if (element == SettingsElement.WalletConnect) { - WalletConnectDetailsItem(onItemsClick = state.onItemsClick) - } else { - DetailsItem( - item = element, - appCurrency = state.appCurrency, - onItemsClick = { state.onItemsClick(element) }, - ) - } - } - Spacer(modifier = Modifier.weight(1f)) - TangemSocialAccounts(state.tangemLinks, state.onSocialNetworkClick) - Spacer(modifier = Modifier.size(12.dp)) - Text( - text = "${stringResource(id = state.appNameRes)} ${state.tangemVersion}", - style = TangemTheme.typography.caption, - color = TangemTheme.colors.text.tertiary, - modifier = Modifier.padding(start = 16.dp, end = 16.dp, bottom = 40.dp), + ScreenTitle(titleRes = R.string.details_title) + SpacerH(height = TangemTheme.dimens.spacing36) + SettingsItems( + items = state.elements, + onItemsClick = state.onItemsClick, ) + SpacerHMax() + TangemSocialAccounts( + links = state.tangemLinks, + onSocialNetworkClick = state.onSocialNetworkClick, + ) + SpacerH(height = TangemTheme.dimens.spacing16) + TangemAppVersion( + appNameRes = state.appNameRes, + version = state.tangemVersion, + ) + SpacerH(height = TangemTheme.dimens.spacing24) } ShowSnackbarIfNeeded(state.showErrorSnackbar.value) } } @Composable -private fun WalletConnectDetailsItem(onItemsClick: (SettingsElement) -> Unit) { +private fun SettingsItems(items: List, onItemsClick: (SettingsElement) -> Unit) { + items.forEach { item -> + val onItemClick = remember(item) { + { onItemsClick(item) } + } + + if (item == SettingsElement.WalletConnect) { + WalletConnectDetailsItem(onItemClick) + } else { + DetailsItem( + item = item, + onItemClick = onItemClick, + ) + } + } +} + +@Composable +private fun WalletConnectDetailsItem(onItemClick: () -> Unit) { Row( modifier = Modifier .defaultMinSize(minHeight = 84.dp) .fillMaxWidth() - .clickable { onItemsClick(SettingsElement.WalletConnect) }, + .clickable(onClick = onItemClick), horizontalArrangement = Arrangement.Start, verticalAlignment = Alignment.CenterVertically, ) { @@ -108,12 +124,12 @@ private fun WalletConnectDetailsItem(onItemsClick: (SettingsElement) -> Unit) { } @Composable -private fun DetailsItem(item: SettingsElement, appCurrency: String, onItemsClick: () -> Unit) { +private fun DetailsItem(item: SettingsElement, onItemClick: () -> Unit) { Row( modifier = Modifier .height(56.dp) .fillMaxWidth() - .clickable(onClick = onItemsClick), + .clickable(onClick = onItemClick), horizontalArrangement = Arrangement.Start, verticalAlignment = Alignment.CenterVertically, ) { @@ -130,13 +146,6 @@ private fun DetailsItem(item: SettingsElement, appCurrency: String, onItemsClick style = TangemTheme.typography.subtitle1, color = TangemTheme.colors.text.primary1, ) - if (item == SettingsElement.AppCurrency) { - Text( - text = appCurrency, - style = TangemTheme.typography.body2, - color = TangemTheme.colors.text.secondary, - ) - } } } } @@ -190,6 +199,16 @@ private fun BoxScope.ShowSnackbarIfNeeded(snackbarErrorState: EventError) { } } +@Composable +private fun TangemAppVersion(appNameRes: Int, version: String, modifier: Modifier = Modifier) { + Text( + modifier = modifier.padding(horizontal = TangemTheme.dimens.spacing16), + text = "${stringResource(id = appNameRes)} $version", + style = TangemTheme.typography.caption, + color = TangemTheme.colors.text.tertiary, + ) +} + // region Preview @Composable private fun DetailsScreenContentSample() { @@ -198,7 +217,6 @@ private fun DetailsScreenContentSample() { elements = SettingsElement.values().toList(), tangemLinks = TangemSocialAccounts.accountsEn, tangemVersion = "Tangem 2.14.12 (343)", - appCurrency = "Dollar", onItemsClick = {}, onSocialNetworkClick = {}, ), diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsScreenState.kt b/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsScreenState.kt index a5e030a647..f6ea3f1f19 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsScreenState.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsScreenState.kt @@ -10,7 +10,6 @@ internal data class DetailsScreenState( val elements: List, val tangemLinks: List, val tangemVersion: String, - val appCurrency: String, val onItemsClick: (SettingsElement) -> Unit, val onSocialNetworkClick: (SocialNetworkLink) -> Unit, val showErrorSnackbar: MutableState = mutableStateOf(EventError.Empty), @@ -24,14 +23,13 @@ internal enum class SettingsElement( val titleRes: Int, ) { WalletConnect(R.drawable.ic_walletconnect, R.string.wallet_connect_title), - Chat(R.drawable.ic_chat, R.string.details_chat), - SendFeedback(R.drawable.ic_comment, R.string.details_row_title_send_feedback), + LinkMoreCards(R.drawable.ic_more_cards, R.string.details_row_title_create_backup), ReferralProgram(R.drawable.ic_add_friends, R.string.details_referral_title), CardSettings(R.drawable.ic_card_settings, R.string.card_settings_title), - AppCurrency(R.drawable.ic_currency, R.string.details_row_title_currency), AppSettings(R.drawable.ic_settings, R.string.app_settings_title), - LinkMoreCards(R.drawable.ic_more_cards, R.string.details_row_title_create_backup), - TermsOfService(R.drawable.ic_text, R.string.disclaimer_title), // General Terms of Service of the App + Chat(R.drawable.ic_chat, R.string.details_chat), + SendFeedback(R.drawable.ic_comment, R.string.details_row_title_send_feedback), + TermsOfService(R.drawable.ic_text, R.string.disclaimer_title), // General Terms of Service of the App, PrivacyPolicy(R.drawable.ic_lock_24, R.string.details_row_privacy_policy), TesterMenu(R.drawable.ic_alert_24, R.string.tester_menu), } diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsViewModel.kt b/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsViewModel.kt index c3491e7c73..7877dff881 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsViewModel.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsViewModel.kt @@ -42,7 +42,6 @@ internal class DetailsViewModel(private val store: Store) { elements = createSettingsItems(state), tangemLinks = getSocialLinks(), tangemVersion = getTangemAppVersion(), - appCurrency = state.appCurrency.name, onItemsClick = { handleClickingSettingsItem(it) }, onSocialNetworkClick = { handleSocialNetworkClick(it) }, ) @@ -60,7 +59,6 @@ internal class DetailsViewModel(private val store: Store) { SettingsElement.LinkMoreCards -> if (state.createBackupAllowed) it else null SettingsElement.PrivacyPolicy -> if (state.privacyPolicyUrl != null) it else null SettingsElement.AppSettings -> if (state.appSettingsState.isBiometricsAvailable) it else null - SettingsElement.AppCurrency -> if (cardTypesResolver.isMultiwalletAllowed()) null else it SettingsElement.ReferralProgram -> if (cardTypesResolver.isTangemWallet()) it else null SettingsElement.TesterMenu -> if (BuildConfig.TESTER_MENU_ENABLED) it else null else -> it @@ -91,9 +89,6 @@ internal class DetailsViewModel(private val store: Store) { Analytics.send(Settings.ButtonCardSettings()) store.dispatch(NavigationAction.NavigateTo(AppScreen.CardSettings)) } - SettingsElement.AppCurrency -> { - store.dispatch(WalletAction.AppCurrencyAction.ChooseAppCurrency) - } SettingsElement.AppSettings -> { Analytics.send(Settings.ButtonAppSettings()) store.dispatch(NavigationAction.NavigateTo(AppScreen.AppSettings)) From 343c3ad9fb37adf87e13c4af9a3c05b691a76048 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 4 Sep 2023 13:31:17 +0800 Subject: [PATCH 014/242] Updated on 2026-08-14 --- .../tap/di/domain/TokensDomainModule.kt | 16 +++ .../middlewares/TradeCryptoMiddleware.kt | 106 +++++++++++++++++- .../repository/DefaultCurrenciesRepository.kt | 24 ++++ .../tokens/utils/ResponseCurrenciesFactory.kt | 2 +- .../tokens/GetNetworkCoinStatusUseCase.kt | 53 +++++++++ .../domain/tokens/legacy/TradeCryptoAction.kt | 9 +- .../CurrenciesStatusesOperations.kt | 15 +++ .../tokens/repository/CurrenciesRepository.kt | 9 ++ .../repository/MockCurrenciesRepository.kt | 5 + .../viewmodels/TokenDetailsViewModel.kt | 39 ++++++- .../state/factory/TokenActionsProvider.kt | 20 ++-- .../WalletCryptoCurrencyActionsConverter.kt | 5 +- .../state/factory/WalletStateFactory.kt | 6 +- ...ryptoCurrencyStatusToTokenItemConverter.kt | 2 +- .../wallet/viewmodels/WalletClickIntents.kt | 7 +- .../wallet/viewmodels/WalletViewModel.kt | 59 ++++++++-- 16 files changed, 340 insertions(+), 37 deletions(-) create mode 100644 domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetNetworkCoinStatusUseCase.kt diff --git a/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt index df25867d4d..b8046288d6 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt @@ -117,6 +117,22 @@ internal object TokensDomainModule { return GetCryptoCurrencyActionsUseCase(rampStateManager, marketCryptoCurrencyRepository, dispatchers) } + @Provides + @ViewModelScoped + fun provideGetCurrencyStatusByNetworkUseCase( + currenciesRepository: CurrenciesRepository, + quotesRepository: QuotesRepository, + networksRepository: NetworksRepository, + dispatchers: CoroutineDispatcherProvider, + ): GetNetworkCoinStatusUseCase { + return GetNetworkCoinStatusUseCase( + currenciesRepository = currenciesRepository, + quotesRepository = quotesRepository, + networksRepository = networksRepository, + dispatchers = dispatchers, + ) + } + @Provides @ViewModelScoped fun provideGetCurrenciesUseCase(currenciesRepository: CurrenciesRepository): GetCryptoCurrenciesUseCase { diff --git a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/TradeCryptoMiddleware.kt b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/TradeCryptoMiddleware.kt index 2528753b4e..02068b2149 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/TradeCryptoMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/TradeCryptoMiddleware.kt @@ -18,9 +18,11 @@ import com.tangem.feature.swap.presentation.SwapFragment import com.tangem.tap.common.analytics.events.AnalyticsParam import com.tangem.tap.common.analytics.events.Token import com.tangem.tap.common.extensions.dispatchDebugErrorNotification +import com.tangem.tap.common.extensions.dispatchErrorNotification import com.tangem.tap.common.extensions.dispatchOnMain import com.tangem.tap.common.extensions.dispatchOpenUrl import com.tangem.tap.common.redux.AppState +import com.tangem.tap.domain.TapError import com.tangem.tap.domain.tokens.getIconUrl import com.tangem.tap.features.demo.DemoHelper import com.tangem.tap.features.home.RUSSIA_COUNTRY_CODE @@ -39,7 +41,10 @@ import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json import com.tangem.feature.swap.domain.models.domain.Currency as SwapCurrency +@Suppress("LargeClass") class TradeCryptoMiddleware { + + @Suppress("LongMethod", "CyclomaticComplexMethod") fun handle(state: () -> AppState?, action: TradeCryptoAction) { if (DemoHelper.tryHandle(state, action)) return @@ -52,11 +57,10 @@ class TradeCryptoMiddleware { openSwap(currency = store.state.walletState.selectedWalletData?.currency?.toSwapCurrency()) } is TradeCryptoAction.New.Buy -> proceedNewBuyAction(state, action) - TradeCryptoAction.New.Send -> store.dispatch(WalletAction.Send()) is TradeCryptoAction.New.Sell -> proceedNewSellAction(action) - is TradeCryptoAction.New.Swap -> { - openSwap(currency = action.cryptoCurrency.toSwapCurrency()) - } + is TradeCryptoAction.New.Swap -> openSwap(currency = action.cryptoCurrency.toSwapCurrency()) + is TradeCryptoAction.New.SendToken -> handleNewSendToken(action = action) + is TradeCryptoAction.New.SendCoin -> handleNewSendCoin(action = action) } } @@ -301,4 +305,98 @@ class TradeCryptoMiddleware { ) } } + + private fun handleNewSendToken(action: TradeCryptoAction.New.SendToken) { + val cryptoStatus = action.tokenStatus + val currency = cryptoStatus.currency + val blockchain = Blockchain.fromId(currency.network.id.value) + + scope.launch { + val walletManager = store.state.daggerGraphState + .get(DaggerGraphState::walletManagersFacade) + .getOrCreateWalletManager( + userWallet = action.userWallet, + blockchain = blockchain, + derivationPath = blockchain.derivationPath( + style = action.userWallet.scanResponse.derivationStyleProvider.getDerivationStyle(), + ), + ) + + if (walletManager == null) { + val error = TapError.UnsupportedState(stateError = "WalletManager is null") + FirebaseCrashlytics.getInstance().recordException(IllegalStateException(error.stateError)) + store.dispatchErrorNotification(error) + return@launch + } + + val sendableAmounts = walletManager.wallet.amounts.values.filter { it.type is AmountType.Token } + when (currency) { + is CryptoCurrency.Coin -> error("Action.tokenStatus.currency is Coin") + is CryptoCurrency.Token -> { + store.dispatchOnMain( + action = PrepareSendScreen( + walletManager = walletManager, + coinAmount = walletManager.wallet.amounts[AmountType.Coin], + coinRate = action.coinFiatRate, + tokenAmount = sendableAmounts.first(), + tokenRate = cryptoStatus.value.fiatRate, + ), + ) + } + } + + store.dispatchOnMain(NavigationAction.NavigateTo(AppScreen.Send)) + } + } + + private fun handleNewSendCoin(action: TradeCryptoAction.New.SendCoin) { + val cryptoStatus = action.coinStatus + val currency = cryptoStatus.currency + val blockchain = Blockchain.fromId(currency.network.id.value) + + scope.launch { + val walletManager = store.state.daggerGraphState + .get(DaggerGraphState::walletManagersFacade) + .getOrCreateWalletManager( + userWallet = action.userWallet, + blockchain = blockchain, + derivationPath = blockchain.derivationPath( + style = action.userWallet.scanResponse.derivationStyleProvider.getDerivationStyle(), + ), + ) + + if (walletManager == null) { + val error = TapError.UnsupportedState(stateError = "WalletManager is null") + FirebaseCrashlytics.getInstance().recordException(IllegalStateException(error.stateError)) + store.dispatchErrorNotification(error) + return@launch + } + + val sendableAmounts = walletManager.wallet.amounts.values.filter { it.type == AmountType.Coin } + when (currency) { + is CryptoCurrency.Coin -> { + val amountToSend = sendableAmounts.find { it.currencySymbol == currency.symbol } + + if (amountToSend == null) { + val error = TapError.UnsupportedState(stateError = "Amount to send is null") + FirebaseCrashlytics.getInstance() + .recordException(IllegalStateException(error.stateError)) + store.dispatchErrorNotification(error) + return@launch + } + + store.dispatchOnMain( + action = PrepareSendScreen( + walletManager = walletManager, + coinAmount = amountToSend, + coinRate = cryptoStatus.value.fiatRate, + ), + ) + } + is CryptoCurrency.Token -> error("Action.tokenStatus.currency is Token") + } + + store.dispatchOnMain(NavigationAction.NavigateTo(AppScreen.Send)) + } + } } \ No newline at end of file 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 2563eb0409..0e83c656d4 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 @@ -1,5 +1,6 @@ package com.tangem.data.tokens.repository +import com.tangem.blockchain.common.Blockchain import com.tangem.data.common.cache.CacheRegistry import com.tangem.data.tokens.utils.* import com.tangem.datasource.api.tangemTech.TangemTechApi @@ -7,10 +8,12 @@ import com.tangem.datasource.api.tangemTech.models.UserTokensResponse import com.tangem.datasource.local.token.UserMarketCoinsStore import com.tangem.datasource.local.token.UserTokensStore import com.tangem.datasource.local.userwallet.UserWalletsStore +import com.tangem.domain.common.extensions.toNetworkId import com.tangem.domain.common.util.derivationStyleProvider import com.tangem.domain.core.error.DataError import com.tangem.domain.demo.DemoConfig import com.tangem.domain.tokens.models.CryptoCurrency +import com.tangem.domain.tokens.models.Network import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWalletId @@ -190,6 +193,27 @@ internal class DefaultCurrenciesRepository( responseCurrenciesFactory.createCurrency(id, response, userWallet.scanResponse.card) } + override suspend fun getNetworkCoin(userWalletId: UserWalletId, networkId: Network.ID): CryptoCurrency.Coin { + val userWallet = getUserWallet(userWalletId) + ensureIsCorrectUserWallet(userWallet = userWallet, isMultiCurrencyWalletExpected = true) + + fetchTokensIfCacheExpired(userWallet = userWallet, refresh = false) + + val storedTokens = requireNotNull(userTokensStore.getSyncOrNull(userWallet.walletId)) { + "Unable to find tokens response for user wallet with provided ID: $userWalletId" + } + + val storedCoin = storedTokens.tokens.find { it.networkId == Blockchain.fromId(networkId.value).toNetworkId() } + ?: error("Coin in this network $networkId not found") + + val coin = responseCurrenciesFactory.createCurrency( + responseToken = storedCoin, + card = userWallet.scanResponse.card, + ) + + return coin as? CryptoCurrency.Coin ?: error("Unable to create currency") + } + override fun isTokensGrouped(userWalletId: UserWalletId): Flow { return channelFlow { ensureIsCorrectUserWallet(userWalletId, isMultiCurrencyWalletExpected = true) diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/ResponseCurrenciesFactory.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/ResponseCurrenciesFactory.kt index f3e7832865..514ef69b10 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/ResponseCurrenciesFactory.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/ResponseCurrenciesFactory.kt @@ -28,7 +28,7 @@ internal class ResponseCurrenciesFactory(private val demoConfig: DemoConfig) { return response.tokens.mapNotNull { createCurrency(it, card) } } - private fun createCurrency(responseToken: UserTokensResponse.Token, card: CardDTO): CryptoCurrency? { + fun createCurrency(responseToken: UserTokensResponse.Token, card: CardDTO): CryptoCurrency? { var blockchain = Blockchain.fromNetworkId(responseToken.networkId) if (blockchain == null || blockchain == Blockchain.Unknown) { Timber.e("Unable to find a blockchain with the network ID: ${responseToken.networkId}") diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetNetworkCoinStatusUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetNetworkCoinStatusUseCase.kt new file mode 100644 index 0000000000..23ecf06a99 --- /dev/null +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetNetworkCoinStatusUseCase.kt @@ -0,0 +1,53 @@ +package com.tangem.domain.tokens + +import arrow.core.Either +import com.tangem.domain.tokens.error.CurrencyStatusError +import com.tangem.domain.tokens.error.mapper.mapToCurrencyError +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.tokens.models.Network +import com.tangem.domain.tokens.operations.CurrenciesStatusesOperations +import com.tangem.domain.tokens.repository.CurrenciesRepository +import com.tangem.domain.tokens.repository.NetworksRepository +import com.tangem.domain.tokens.repository.QuotesRepository +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.flow.* + +class GetNetworkCoinStatusUseCase( + private val currenciesRepository: CurrenciesRepository, + private val quotesRepository: QuotesRepository, + private val networksRepository: NetworksRepository, + private val dispatchers: CoroutineDispatcherProvider, +) { + + operator fun invoke( + userWalletId: UserWalletId, + networkId: Network.ID, + ): Flow> { + return flow { + emitAll( + flow = getCurrency( + userWalletId = userWalletId, + networkId = networkId, + ), + ) + } + .flowOn(dispatchers.io) + } + + private suspend fun getCurrency( + userWalletId: UserWalletId, + networkId: Network.ID, + ): Flow> { + val operations = CurrenciesStatusesOperations( + currenciesRepository = currenciesRepository, + quotesRepository = quotesRepository, + networksRepository = networksRepository, + userWalletId = userWalletId, + ) + + return operations.getNetworkCoinFlow(networkId).map { maybeCurrency -> + maybeCurrency.mapLeft(CurrenciesStatusesOperations.Error::mapToCurrencyError) + } + } +} \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/legacy/TradeCryptoAction.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/legacy/TradeCryptoAction.kt index 3ef7c99d04..e9c3430207 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/legacy/TradeCryptoAction.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/legacy/TradeCryptoAction.kt @@ -4,6 +4,7 @@ import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.models.CryptoCurrency import com.tangem.domain.wallets.models.UserWallet import org.rekotlin.Action +import java.math.BigDecimal sealed class TradeCryptoAction : Action { @@ -36,7 +37,13 @@ sealed class TradeCryptoAction : Action { val appCurrencyCode: String, ) : New() - object Send : New() + data class SendToken( + val userWallet: UserWallet, + val tokenStatus: CryptoCurrencyStatus, + val coinFiatRate: BigDecimal?, + ) : New() + + data class SendCoin(val userWallet: UserWallet, val coinStatus: CryptoCurrencyStatus) : New() data class Swap(val cryptoCurrency: CryptoCurrency) : New() } diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesOperations.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesOperations.kt index cb8885e6b5..e72a1769bb 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesOperations.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesOperations.kt @@ -80,6 +80,15 @@ internal class CurrenciesStatusesOperations( return getCurrencyStatusFlow(currency) } + suspend fun getNetworkCoinFlow(networkId: Network.ID): Flow> { + val currency = recover( + block = { getNetworkCoin(networkId) }, + recover = { return flowOf(it.left()) }, + ) + + return getCurrencyStatusFlow(currency) + } + suspend fun getPrimaryCurrencyStatusFlow(): Flow> { val currency = recover( block = { getPrimaryCurrency() }, @@ -177,6 +186,12 @@ internal class CurrenciesStatusesOperations( .bind() } + private suspend fun Raise.getNetworkCoin(networkId: Network.ID): CryptoCurrency { + return Either.catch { currenciesRepository.getNetworkCoin(userWalletId, networkId) } + .mapLeft { Error.DataError(it) } + .bind() + } + private suspend fun Raise.getPrimaryCurrency(): CryptoCurrency { return catch( block = { currenciesRepository.getSingleCurrencyWalletPrimaryCurrency(userWalletId) }, diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/CurrenciesRepository.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/CurrenciesRepository.kt index 99db8c884a..ce523cb34a 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/CurrenciesRepository.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/CurrenciesRepository.kt @@ -1,6 +1,7 @@ package com.tangem.domain.tokens.repository import com.tangem.domain.tokens.models.CryptoCurrency +import com.tangem.domain.tokens.models.Network import com.tangem.domain.wallets.models.UserWalletId import kotlinx.coroutines.flow.Flow @@ -103,6 +104,14 @@ interface CurrenciesRepository { */ suspend fun getMultiCurrencyWalletCurrency(userWalletId: UserWalletId, id: CryptoCurrency.ID): CryptoCurrency + /** + * Get the coin for a specific network. + * + * @param userWalletId The unique identifier of the user wallet. + * @param networkId The unique identifier of the network. + */ + suspend fun getNetworkCoin(userWalletId: UserWalletId, networkId: Network.ID): CryptoCurrency.Coin + /** * Determines whether the tokens within a specific multi-currency user wallet are grouped. * diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockCurrenciesRepository.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockCurrenciesRepository.kt index 192b8f5afc..8238bc90d6 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockCurrenciesRepository.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockCurrenciesRepository.kt @@ -4,6 +4,7 @@ import arrow.core.Either import arrow.core.getOrElse import com.tangem.domain.core.error.DataError import com.tangem.domain.tokens.models.CryptoCurrency +import com.tangem.domain.tokens.models.Network import com.tangem.domain.wallets.models.UserWalletId import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.first @@ -74,6 +75,10 @@ internal class MockCurrenciesRepository( return token } + override suspend fun getNetworkCoin(userWalletId: UserWalletId, networkId: Network.ID): CryptoCurrency.Coin { + TODO("Not yet implemented") + } + override fun isTokensGrouped(userWalletId: UserWalletId): Flow { return isGrouped.map { it.getOrElse { e -> throw e } } } diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt index f1e17d7e44..27b52fe003 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt @@ -11,8 +11,9 @@ import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.redux.ReduxStateHolder import com.tangem.domain.tokens.GetCryptoCurrencyActionsUseCase -import com.tangem.domain.tokens.RemoveCurrencyUseCase import com.tangem.domain.tokens.GetCurrencyStatusUpdatesUseCase +import com.tangem.domain.tokens.GetNetworkCoinStatusUseCase +import com.tangem.domain.tokens.RemoveCurrencyUseCase import com.tangem.domain.tokens.legacy.TradeCryptoAction import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.models.CryptoCurrency @@ -48,6 +49,7 @@ internal class TokenDetailsViewModel @Inject constructor( private val getExploreUrlUseCase: GetExploreUrlUseCase, private val getCryptoCurrencyActionsUseCase: GetCryptoCurrencyActionsUseCase, private val removeCurrencyUseCase: RemoveCurrencyUseCase, + private val getNetworkCoinStatusUseCase: GetNetworkCoinStatusUseCase, private val reduxStateHolder: ReduxStateHolder, savedStateHandle: SavedStateHandle, ) : ViewModel(), DefaultLifecycleObserver, TokenDetailsClickIntents { @@ -169,7 +171,40 @@ internal class TokenDetailsViewModel @Inject constructor( } override fun onSendClick() { - reduxStateHolder.dispatch(TradeCryptoAction.New.Send) + val cryptoCurrencyStatus = cryptoCurrencyStatus ?: return + + when (cryptoCurrencyStatus.currency) { + is CryptoCurrency.Coin -> { + reduxStateHolder.dispatch( + action = TradeCryptoAction.New.SendCoin( + userWallet = wallet, + coinStatus = cryptoCurrencyStatus, + ), + ) + } + is CryptoCurrency.Token -> sendToken(status = cryptoCurrencyStatus) + } + } + + private fun sendToken(status: CryptoCurrencyStatus) { + viewModelScope.launch(dispatchers.io) { + getNetworkCoinStatusUseCase( + userWalletId = wallet.walletId, + networkId = status.currency.network.id, + ) + .take(count = 1) + .collectLatest { + it.onRight { coinStatus -> + reduxStateHolder.dispatch( + action = TradeCryptoAction.New.SendToken( + userWallet = wallet, + tokenStatus = status, + coinFiatRate = coinStatus.value.fiatRate, + ), + ) + } + } + } } override fun onReceiveClick() { diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/TokenActionsProvider.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/TokenActionsProvider.kt index f9a7dbfc4e..37c3309f8b 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/TokenActionsProvider.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/TokenActionsProvider.kt @@ -1,36 +1,32 @@ package com.tangem.feature.wallet.presentation.wallet.state.factory -import com.tangem.common.Provider +import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.feature.wallet.impl.R import com.tangem.feature.wallet.presentation.common.state.TokenItemState import com.tangem.feature.wallet.presentation.wallet.state.TokenActionButtonConfig -import com.tangem.feature.wallet.presentation.wallet.state.WalletState +import com.tangem.feature.wallet.presentation.wallet.viewmodels.WalletClickIntents import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList /** * Converter from loaded [TokenItemState.Content] to ImmutableList<[TokenActionButtonConfig]> * - * @property currentStateProvider current ui state provider + * @property clickIntents screen click intents * */ -@Suppress("UnusedPrivateMember") -internal class TokenActionsProvider( - private val currentStateProvider: Provider, -) { +internal class TokenActionsProvider(private val clickIntents: WalletClickIntents) { - @Suppress("UnusedPrivateMember") - fun provideActions(tokenId: String): ImmutableList { + fun provideActions(cryptoCurrencyStatus: CryptoCurrencyStatus): ImmutableList { // TODO: [REDACTED_JIRA] - return mockTokenActionButtonConfig().toImmutableList() + return mockTokenActionButtonConfig(cryptoCurrencyStatus).toImmutableList() } - private fun mockTokenActionButtonConfig(): List { + private fun mockTokenActionButtonConfig(cryptoCurrencyStatus: CryptoCurrencyStatus): List { return listOf( TokenActionButtonConfig( text = "Send", iconResId = R.drawable.ic_plus_24, - onClick = {}, + onClick = { clickIntents.onMultiCurrencySendClick(cryptoCurrencyStatus) }, ), TokenActionButtonConfig( text = "Buy", diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletCryptoCurrencyActionsConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletCryptoCurrencyActionsConverter.kt index 38e18cdf52..b4b8f00fbf 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletCryptoCurrencyActionsConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletCryptoCurrencyActionsConverter.kt @@ -40,7 +40,10 @@ internal class WalletCryptoCurrencyActionsConverter( WalletManageButton.Sell(enabled = action.enabled, onClick = clickIntents::onSellClick) } is TokenActionsState.ActionState.Send -> { - WalletManageButton.Send(enabled = action.enabled, onClick = clickIntents::onSendClick) + WalletManageButton.Send( + enabled = action.enabled, + onClick = clickIntents::onSingleCurrencySendClick, + ) } is TokenActionsState.ActionState.Swap -> null } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletStateFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletStateFactory.kt index 4fe28bb627..fa22261f52 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletStateFactory.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletStateFactory.kt @@ -44,7 +44,7 @@ internal class WalletStateFactory( private val clickIntents: WalletClickIntents, ) { - private val tokenActionsProvider by lazy { TokenActionsProvider(currentStateProvider = currentStateProvider) } + private val tokenActionsProvider by lazy { TokenActionsProvider(clickIntents) } private val skeletonConverter by lazy { WalletSkeletonStateConverter(currentStateProvider, clickIntents) } private val tokenListErrorConverter by lazy { @@ -185,12 +185,12 @@ internal class WalletStateFactory( } } - fun getStateWithTokenActionBottomSheet(tokenId: String): WalletState { + fun getStateWithTokenActionBottomSheet(currencyStatus: CryptoCurrencyStatus): WalletState { return when (val state = currentStateProvider() as WalletState.ContentState) { is WalletMultiCurrencyState.Content -> state.copy( tokenActionsBottomSheet = ActionsBottomSheetConfig( isShow = true, - actions = tokenActionsProvider.provideActions(tokenId = tokenId), + actions = tokenActionsProvider.provideActions(currencyStatus), onDismissRequest = clickIntents::onDismissActionsBottomSheet, ), ) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/CryptoCurrencyStatusToTokenItemConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/CryptoCurrencyStatusToTokenItemConverter.kt index 02b175aad7..a2e85d00a2 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/CryptoCurrencyStatusToTokenItemConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/CryptoCurrencyStatusToTokenItemConverter.kt @@ -50,7 +50,7 @@ internal class CryptoCurrencyStatusToTokenItemConverter( ) }, onItemClick = { clickIntents.onTokenItemClick(currency) }, - onItemLongClick = { clickIntents.onTokenItemLongClick(currency) }, + onItemLongClick = { clickIntents.onTokenItemLongClick(cryptoCurrencyStatus = this) }, ) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletClickIntents.kt index c3ba714c76..34e8f722a8 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletClickIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletClickIntents.kt @@ -1,6 +1,7 @@ package com.tangem.feature.wallet.presentation.wallet.viewmodels import com.tangem.core.ui.components.transactions.intents.TxHistoryClickIntents +import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.models.CryptoCurrency import com.tangem.domain.wallets.models.UserWalletId @@ -39,7 +40,7 @@ internal interface WalletClickIntents : TxHistoryClickIntents { fun onTokenItemClick(currency: CryptoCurrency) - fun onTokenItemLongClick(currency: CryptoCurrency) + fun onTokenItemLongClick(cryptoCurrencyStatus: CryptoCurrencyStatus) fun onDismissActionsBottomSheet() @@ -47,7 +48,9 @@ internal interface WalletClickIntents : TxHistoryClickIntents { fun onDeleteClick(userWalletId: UserWalletId) - fun onSendClick() + fun onSingleCurrencySendClick(cryptoCurrencyStatus: CryptoCurrencyStatus? = null) + + fun onMultiCurrencySendClick(cryptoCurrencyStatus: CryptoCurrencyStatus) fun onReceiveClick() diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt index 4dc7b9293c..5b948416b6 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt @@ -74,6 +74,7 @@ internal class WalletViewModel @Inject constructor( private val fetchTokenListUseCase: FetchTokenListUseCase, private val getPrimaryCurrencyStatusUpdatesUseCase: GetPrimaryCurrencyStatusUpdatesUseCase, private val fetchCurrencyStatusUseCase: FetchCurrencyStatusUseCase, + private val getNetworkCoinStatusUseCase: GetNetworkCoinStatusUseCase, private val getCardWasScannedUseCase: GetCardWasScannedUseCase, private val isUserAlreadyRateAppUseCase: IsUserAlreadyRateAppUseCase, private val isDemoCardUseCase: IsDemoCardUseCase, @@ -121,7 +122,7 @@ internal class WalletViewModel @Inject constructor( var uiState: WalletState by uiStateHolder(initialState = stateFactory.getInitialState()) private var wallets: List by Delegates.notNull() - private var cryptoCurrencyStatus: CryptoCurrencyStatus? = null + private var singleWalletCryptoCurrencyStatus: CryptoCurrencyStatus? = null private val tokensJobHolder = JobHolder() private val marketPriceJobHolder = JobHolder() @@ -300,7 +301,7 @@ internal class WalletViewModel @Inject constructor( override fun onBuyClick() { val state = uiState as? WalletState.ContentState ?: return - val status = cryptoCurrencyStatus ?: return + val status = singleWalletCryptoCurrencyStatus ?: return val wallet = getWallet(index = state.walletsListConfig.selectedWalletIndex) reduxStateHolder.dispatch( @@ -312,8 +313,48 @@ internal class WalletViewModel @Inject constructor( ) } - override fun onSendClick() { - reduxStateHolder.dispatch(TradeCryptoAction.New.Send) + override fun onSingleCurrencySendClick(cryptoCurrencyStatus: CryptoCurrencyStatus?) { + val state = uiState as? WalletState.ContentState ?: return + + val userWallet = getWallet(index = state.walletsListConfig.selectedWalletIndex) + val coinStatus = if (userWallet.isMultiCurrency) cryptoCurrencyStatus else singleWalletCryptoCurrencyStatus + + reduxStateHolder.dispatch( + action = TradeCryptoAction.New.SendCoin( + userWallet = userWallet, + coinStatus = coinStatus ?: return, + ), + ) + } + + override fun onMultiCurrencySendClick(cryptoCurrencyStatus: CryptoCurrencyStatus) { + if (cryptoCurrencyStatus.currency is CryptoCurrency.Coin) { + onSingleCurrencySendClick(cryptoCurrencyStatus = cryptoCurrencyStatus) + return + } + + val state = uiState as? WalletState.ContentState ?: return + + viewModelScope.launch(dispatchers.io) { + val userWallet = getWallet(index = state.walletsListConfig.selectedWalletIndex) + + getNetworkCoinStatusUseCase( + userWalletId = userWallet.walletId, + networkId = cryptoCurrencyStatus.currency.network.id, + ) + .take(count = 1) + .collectLatest { + it.onRight { coinStatus -> + reduxStateHolder.dispatch( + action = TradeCryptoAction.New.SendToken( + userWallet = getWallet(index = state.walletsListConfig.selectedWalletIndex), + tokenStatus = cryptoCurrencyStatus, + coinFiatRate = coinStatus.value.fiatRate, + ), + ) + } + } + } } override fun onReceiveClick() { @@ -321,7 +362,7 @@ internal class WalletViewModel @Inject constructor( } override fun onSellClick() { - val status = cryptoCurrencyStatus ?: return + val status = singleWalletCryptoCurrencyStatus ?: return reduxStateHolder.dispatch( TradeCryptoAction.New.Sell( @@ -384,10 +425,8 @@ internal class WalletViewModel @Inject constructor( router.openTokenDetails(currency = currency) } - override fun onTokenItemLongClick(currency: CryptoCurrency) { - uiState = stateFactory.getStateWithTokenActionBottomSheet( - tokenId = currency.id.value, - ) + override fun onTokenItemLongClick(cryptoCurrencyStatus: CryptoCurrencyStatus) { + uiState = stateFactory.getStateWithTokenActionBottomSheet(cryptoCurrencyStatus) } override fun onRenameClick(userWalletId: UserWalletId, name: String) { @@ -504,7 +543,7 @@ internal class WalletViewModel @Inject constructor( uiState = stateFactory.getSingleCurrencyLoadedBalanceState(maybeCryptoCurrencyStatus) maybeCryptoCurrencyStatus.onRight { status -> - cryptoCurrencyStatus = status + singleWalletCryptoCurrencyStatus = status updateButtons(userWalletId = userWalletId, currency = status.currency) } } From f289bbab97651692edaa8e76ace090a1d67a8b1e Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 6 Sep 2023 12:57:59 +0300 Subject: [PATCH 015/242] Updated on 2026-08-14 --- .../java/com/tangem/tap/DeviceFlipDetector.kt | 56 +++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 app/src/main/java/com/tangem/tap/DeviceFlipDetector.kt diff --git a/app/src/main/java/com/tangem/tap/DeviceFlipDetector.kt b/app/src/main/java/com/tangem/tap/DeviceFlipDetector.kt new file mode 100644 index 0000000000..fe0dc2f8fc --- /dev/null +++ b/app/src/main/java/com/tangem/tap/DeviceFlipDetector.kt @@ -0,0 +1,56 @@ +package com.tangem.tap + +import android.content.Context +import android.hardware.Sensor +import android.hardware.SensorEvent +import android.hardware.SensorEventListener +import android.hardware.SensorManager +import android.os.SystemClock +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.callbackFlow + +@ExperimentalCoroutinesApi +class DeviceFlipDetector(context: Context) { + + private val sensorManager = context.getSystemService(Context.SENSOR_SERVICE) as SensorManager + private var gravitySensor = sensorManager.getDefaultSensor(Sensor.TYPE_GRAVITY) + + private val zAxisThreshold = -6 + private val throttleTimeMs = 3000 + private var lastTriggerTime = 0L + private var isScreenDown = false + + fun deviceFlipEvents(): Flow = callbackFlow { + val listener = object : SensorEventListener { + override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) { + /* no-op */ + } + + override fun onSensorChanged(event: SensorEvent?) { + event?.let { + val currentTime = SystemClock.elapsedRealtime() + val zAxisValue = it.values[2] + + if (zAxisValue < zAxisThreshold && !isScreenDown) { + isScreenDown = true + lastTriggerTime = currentTime + } else if (zAxisValue >= zAxisThreshold) { + if (isScreenDown && currentTime - lastTriggerTime <= throttleTimeMs) { + lastTriggerTime = currentTime + trySend(Unit) + } + isScreenDown = false + } + } + } + } + + gravitySensor?.let { + sensorManager.registerListener(listener, it, SensorManager.SENSOR_DELAY_NORMAL) + } + + awaitClose { sensorManager.unregisterListener(listener) } + } +} \ No newline at end of file From 93b0b05e697a9efe4d4f804001b058440ae47a11 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 7 Sep 2023 10:51:42 +0300 Subject: [PATCH 016/242] Updated on 2026-08-14 --- .../java/com/tangem/tap/TapApplication.kt | 5 + .../tap/common/redux/global/GlobalAction.kt | 2 + .../tap/common/redux/global/GlobalReducer.kt | 3 + .../tap/common/redux/global/GlobalState.kt | 2 + .../features/details/redux/DetailsAction.kt | 5 + .../details/redux/DetailsMiddleware.kt | 12 + .../features/details/redux/DetailsReducer.kt | 6 + .../features/details/redux/DetailsState.kt | 2 + .../appsettings/AppSettingsAlertsFactory.kt | 28 -- .../appsettings/AppSettingsDialogsFactory.kt | 58 ++++ .../ui/appsettings/AppSettingsItemsFactory.kt | 17 ++ .../ui/appsettings/AppSettingsScreen.kt | 34 +-- .../ui/appsettings/AppSettingsScreenState.kt | 30 ++- .../ui/appsettings/AppSettingsViewModel.kt | 30 ++- .../components/SettingsAlertDialog.kt | 34 +-- .../{ButtonItem.kt => SettingsButtonItem.kt} | 6 +- .../{CardItem.kt => SettingsCardItem.kt} | 6 +- .../components/SettingsSelectorDialog.kt | 58 ++++ .../{SwitchItem.kt => SettingsSwitchItem.kt} | 6 +- .../ui/dialogs/ResetBackupCardDialog.kt | 2 +- .../tap/proxy/redux/DaggerGraphState.kt | 2 + core/res/src/main/res/values-ru/strings.xml | 12 +- core/res/src/main/res/values/strings.xml | 14 +- .../com/tangem/core/ui/components/Dialogs.kt | 247 +++++++++++++++--- .../domain/apptheme/model/AppThemeMode.kt | 5 + 25 files changed, 494 insertions(+), 132 deletions(-) delete mode 100644 app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsAlertsFactory.kt create mode 100644 app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsDialogsFactory.kt rename app/src/main/java/com/tangem/tap/features/details/ui/appsettings/components/{ButtonItem.kt => SettingsButtonItem.kt} (94%) rename app/src/main/java/com/tangem/tap/features/details/ui/appsettings/components/{CardItem.kt => SettingsCardItem.kt} (94%) create mode 100644 app/src/main/java/com/tangem/tap/features/details/ui/appsettings/components/SettingsSelectorDialog.kt rename app/src/main/java/com/tangem/tap/features/details/ui/appsettings/components/{SwitchItem.kt => SettingsSwitchItem.kt} (95%) diff --git a/app/src/main/java/com/tangem/tap/TapApplication.kt b/app/src/main/java/com/tangem/tap/TapApplication.kt index 47d99024bd..99a413b1af 100644 --- a/app/src/main/java/com/tangem/tap/TapApplication.kt +++ b/app/src/main/java/com/tangem/tap/TapApplication.kt @@ -23,6 +23,7 @@ import com.tangem.datasource.config.FeaturesLocalLoader import com.tangem.datasource.config.models.Config import com.tangem.datasource.connection.NetworkConnectionManager import com.tangem.domain.appcurrency.repository.AppCurrencyRepository +import com.tangem.domain.apptheme.repository.AppThemeModeRepository import com.tangem.domain.card.ScanCardProcessor import com.tangem.domain.common.LogConfig import com.tangem.domain.tokens.repository.CurrenciesRepository @@ -173,6 +174,9 @@ class TapApplication : Application(), ImageLoaderFactory { @Inject lateinit var currenciesRepository: CurrenciesRepository + @Inject + lateinit var appThemeModeRepository: AppThemeModeRepository + override fun onCreate() { super.onCreate() @@ -195,6 +199,7 @@ class TapApplication : Application(), ImageLoaderFactory { walletManagersFacade = walletManagersFacade, appStateHolder = appStateHolder, currenciesRepository = currenciesRepository, + appThemeModeRepository = appThemeModeRepository, ), ), ) diff --git a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalAction.kt b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalAction.kt index 2d75cf1723..5e159ae358 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalAction.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalAction.kt @@ -6,6 +6,7 @@ import com.tangem.common.CompletionResult import com.tangem.common.core.TangemError import com.tangem.datasource.config.ConfigManager import com.tangem.datasource.config.models.ChatConfig +import com.tangem.domain.apptheme.model.AppThemeMode import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.tap.common.analytics.topup.TopUpController @@ -103,4 +104,5 @@ sealed class GlobalAction : Action { } data class UpdateUserWalletsListManager(val manager: UserWalletsListManager) : GlobalAction() + data class ChangeAppThemeMode(val appThemeMode: AppThemeMode) : GlobalAction() } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalReducer.kt b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalReducer.kt index d26e36149c..a380ee16b0 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalReducer.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalReducer.kt @@ -91,6 +91,9 @@ fun globalReducer(action: Action, state: AppState, appStateHolder: AppStateHolde userWalletsListManager = action.manager, ) } + is GlobalAction.ChangeAppThemeMode -> globalState.copy( + appThemeMode = action.appThemeMode, + ) else -> globalState } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalState.kt b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalState.kt index 1e5a3cfce0..f5a30ba673 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalState.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalState.kt @@ -1,6 +1,7 @@ package com.tangem.tap.common.redux.global import com.tangem.datasource.config.ConfigManager +import com.tangem.domain.apptheme.model.AppThemeMode import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.tap.common.analytics.topup.TopUpController @@ -29,6 +30,7 @@ data class GlobalState( val userCountryCode: String? = null, val userWalletsListManager: UserWalletsListManager? = null, val topUpController: TopUpController? = null, + val appThemeMode: AppThemeMode = AppThemeMode.DEFAULT, ) : StateType typealias CryptoCurrencyName = String diff --git a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsAction.kt b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsAction.kt index c2112f12fe..1e719df7c7 100644 --- a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsAction.kt +++ b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsAction.kt @@ -1,5 +1,6 @@ package com.tangem.tap.features.details.redux +import com.tangem.domain.apptheme.model.AppThemeMode import com.tangem.domain.common.CardTypesResolver import com.tangem.domain.models.scan.CardDTO import com.tangem.domain.models.scan.ScanResponse @@ -68,6 +69,10 @@ sealed class DetailsAction : Action { data class BiometricsStatusChanged( val needEnrollBiometrics: Boolean, ) : AppSettings() + + data class ChangeAppThemeMode( + val appThemeMode: AppThemeMode, + ) : AppSettings() } data class ChangeAppCurrency(val fiatCurrency: FiatCurrency) : DetailsAction() diff --git a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsMiddleware.kt b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsMiddleware.kt index 5778b35562..2f6f8ec7cc 100644 --- a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsMiddleware.kt @@ -9,6 +9,7 @@ import com.tangem.common.flatMap import com.tangem.core.analytics.Analytics import com.tangem.core.navigation.AppScreen import com.tangem.core.navigation.NavigationAction +import com.tangem.domain.apptheme.model.AppThemeMode import com.tangem.domain.common.TapWorkarounds.isTangemTwins import com.tangem.domain.common.util.cardTypesResolver import com.tangem.domain.models.scan.ScanResponse @@ -221,6 +222,9 @@ class DetailsMiddleware { is DetailsAction.AppSettings.EnrollBiometrics -> { enrollBiometrics() } + is DetailsAction.AppSettings.ChangeAppThemeMode -> { + changeAppThemeMode(action.appThemeMode) + } is DetailsAction.AppSettings.SwitchPrivacySetting.Success, is DetailsAction.AppSettings.SwitchPrivacySetting.Failure, is DetailsAction.AppSettings.BiometricsStatusChanged, @@ -252,6 +256,14 @@ class DetailsMiddleware { store.dispatchOnMain(NavigationAction.OpenBiometricsSettings) } + private fun changeAppThemeMode(appThemeMode: AppThemeMode) { + val repository = store.state.daggerGraphState.get(DaggerGraphState::appThemeModeRepository) + + scope.launch { + repository.changeAppThemeMode(appThemeMode) + } + } + private fun toggleSaveWallets(state: DetailsState, enable: Boolean) = scope.launch { // Nothing to change if (preferencesStorage.shouldSaveUserWallets == enable) { diff --git a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsReducer.kt b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsReducer.kt index 77c3972fac..1b70773cc1 100644 --- a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsReducer.kt +++ b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsReducer.kt @@ -58,6 +58,7 @@ private fun handlePrepareScreen(action: DetailsAction.PrepareScreen): DetailsSta saveWallets = preferencesStorage.shouldSaveUserWallets, saveAccessCodes = preferencesStorage.shouldSaveAccessCodes, selectedFiatCurrency = store.state.globalState.appCurrency, + selectedThemeMode = store.state.globalState.appThemeMode, ), ) } @@ -196,6 +197,11 @@ private fun handlePrivacyAction(action: DetailsAction.AppSettings, state: Detail needEnrollBiometrics = action.needEnrollBiometrics, ), ) + is DetailsAction.AppSettings.ChangeAppThemeMode -> state.copy( + appSettingsState = state.appSettingsState.copy( + selectedThemeMode = action.appThemeMode, + ), + ) is DetailsAction.AppSettings.EnrollBiometrics, is DetailsAction.AppSettings.CheckBiometricsStatus, -> state diff --git a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsState.kt b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsState.kt index 3dcf9ddd33..14e533ed54 100644 --- a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsState.kt +++ b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsState.kt @@ -1,5 +1,6 @@ package com.tangem.tap.features.details.redux +import com.tangem.domain.apptheme.model.AppThemeMode import com.tangem.domain.models.scan.CardDTO import com.tangem.domain.models.scan.ScanResponse import com.tangem.tap.common.entities.Button @@ -55,6 +56,7 @@ data class AppSettingsState( val needEnrollBiometrics: Boolean = false, val isInProgress: Boolean = false, val selectedFiatCurrency: FiatCurrency = FiatCurrency.Default, + val selectedThemeMode: AppThemeMode = AppThemeMode.DEFAULT, ) enum class SecurityOption { LongTap, PassCode, AccessCode } diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsAlertsFactory.kt b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsAlertsFactory.kt deleted file mode 100644 index 51cfcc0ad3..0000000000 --- a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsAlertsFactory.kt +++ /dev/null @@ -1,28 +0,0 @@ -package com.tangem.tap.features.details.ui.appsettings - -import com.tangem.core.ui.extensions.resourceReference -import com.tangem.tap.features.details.ui.appsettings.AppSettingsScreenState.Alert -import com.tangem.wallet.R - -internal class AppSettingsAlertsFactory { - - fun createDeleteSavedWalletsAlert(onDelete: () -> Unit, onDismiss: () -> Unit): Alert { - return Alert( - title = resourceReference(R.string.common_attention), - description = resourceReference(R.string.app_settings_off_saved_wallet_alert_message), - confirmText = resourceReference(R.string.common_delete), - onConfirm = onDelete, - onDismiss = onDismiss, - ) - } - - fun createDeleteSavedAccessCodesAlert(onDelete: () -> Unit, onDismiss: () -> Unit): Alert { - return Alert( - title = resourceReference(R.string.common_attention), - description = resourceReference(R.string.app_settings_off_saved_access_code_alert_message), - confirmText = resourceReference(R.string.common_delete), - onConfirm = onDelete, - onDismiss = onDismiss, - ) - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsDialogsFactory.kt b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsDialogsFactory.kt new file mode 100644 index 0000000000..43e66b065a --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsDialogsFactory.kt @@ -0,0 +1,58 @@ +package com.tangem.tap.features.details.ui.appsettings + +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.domain.apptheme.model.AppThemeMode +import com.tangem.tap.features.details.ui.appsettings.AppSettingsScreenState.Dialog +import com.tangem.wallet.R +import kotlinx.collections.immutable.toImmutableList + +internal class AppSettingsDialogsFactory { + + fun createDeleteSavedWalletsAlert(onDelete: () -> Unit, onDismiss: () -> Unit): Dialog.Alert { + return Dialog.Alert( + title = resourceReference(R.string.common_attention), + description = resourceReference(R.string.app_settings_off_saved_wallet_alert_message), + confirmText = resourceReference(R.string.common_delete), + onConfirm = onDelete, + onDismiss = onDismiss, + ) + } + + fun createDeleteSavedAccessCodesAlert(onDelete: () -> Unit, onDismiss: () -> Unit): Dialog.Alert { + return Dialog.Alert( + title = resourceReference(R.string.common_attention), + description = resourceReference(R.string.app_settings_off_saved_access_code_alert_message), + confirmText = resourceReference(R.string.common_delete), + onConfirm = onDelete, + onDismiss = onDismiss, + ) + } + + fun createThemeModeSelectorDialog( + selectedModeIndex: Int, + onSelect: (AppThemeMode) -> Unit, + onDismiss: () -> Unit, + ): Dialog.Selector { + val modes = AppThemeMode.available + + return Dialog.Selector( + title = resourceReference(R.string.app_settings_theme_selector_title), + selectedItemIndex = selectedModeIndex, + items = modes.map { mode -> + resourceReference( + id = when (mode) { + AppThemeMode.FORCE_DARK -> R.string.app_settings_theme_mode_dark + AppThemeMode.FORCE_LIGHT -> R.string.app_settings_theme_mode_light + AppThemeMode.FOLLOW_SYSTEM -> R.string.app_settings_theme_mode_system + }, + ) + }.toImmutableList(), + onSelect = { index -> + val mode = AppThemeMode.available[index] + + onSelect(mode) + }, + onDismiss = onDismiss, + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsItemsFactory.kt b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsItemsFactory.kt index a6a23da4b5..783325f318 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsItemsFactory.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsItemsFactory.kt @@ -2,6 +2,7 @@ package com.tangem.tap.features.details.ui.appsettings import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.stringReference +import com.tangem.domain.apptheme.model.AppThemeMode import com.tangem.tap.features.details.ui.appsettings.AppSettingsScreenState.Item import com.tangem.wallet.R @@ -56,4 +57,20 @@ internal class AppSettingsItemsFactory { onClick = onClick, ) } + + fun createSelectThemeModeButton(currentThemeMode: AppThemeMode, onClick: () -> Unit): Item.Button { + return Item.Button( + id = "select_theme_mode_button", + title = resourceReference(R.string.app_settings_theme_selector_title), + description = resourceReference( + id = when (currentThemeMode) { + AppThemeMode.FORCE_DARK -> R.string.app_settings_theme_mode_dark + AppThemeMode.FORCE_LIGHT -> R.string.app_settings_theme_mode_light + AppThemeMode.FOLLOW_SYSTEM -> R.string.app_settings_theme_mode_system + }, + ), + isEnabled = true, + onClick = onClick, + ) + } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsScreen.kt b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsScreen.kt index aff04f20a9..314d4634b3 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsScreen.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsScreen.kt @@ -11,11 +11,9 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider import com.tangem.core.ui.res.TangemTheme +import com.tangem.domain.apptheme.model.AppThemeMode import com.tangem.tap.features.details.ui.appsettings.AppSettingsScreenState.Item -import com.tangem.tap.features.details.ui.appsettings.components.ButtonItem -import com.tangem.tap.features.details.ui.appsettings.components.CardItem -import com.tangem.tap.features.details.ui.appsettings.components.SettingsAlertDialog -import com.tangem.tap.features.details.ui.appsettings.components.SwitchItem +import com.tangem.tap.features.details.ui.appsettings.components.* import com.tangem.tap.features.details.ui.common.SettingsScreensScaffold import com.tangem.wallet.R import kotlinx.collections.immutable.persistentListOf @@ -37,9 +35,11 @@ internal fun AppSettingsScreen(state: AppSettingsScreenState, onBackClick: () -> @Composable private fun AppSettings(state: AppSettingsScreenState.Content) { - val alert by rememberUpdatedState(newValue = state.alert) - alert?.let { safeAlert -> - SettingsAlertDialog(alert = safeAlert) + val dialog by rememberUpdatedState(newValue = state.dialog) + when (val safeDialog = dialog) { + is AppSettingsScreenState.Dialog.Alert -> SettingsAlertDialog(dialog = safeDialog) + is AppSettingsScreenState.Dialog.Selector -> SettingsSelectorDialog(dialog = safeDialog) + null -> Unit } LazyColumn { @@ -48,15 +48,15 @@ private fun AppSettings(state: AppSettingsScreenState.Content) { key = Item::id, ) { item -> when (item) { - is Item.Card -> CardItem( + is Item.Card -> SettingsCardItem( modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing16), item = item, ) - is Item.Button -> ButtonItem( + is Item.Button -> SettingsButtonItem( modifier = Modifier.padding(vertical = TangemTheme.dimens.spacing8), item = item, ) - is Item.Switch -> SwitchItem( + is Item.Switch -> SettingsSwitchItem( modifier = Modifier.padding( vertical = TangemTheme.dimens.spacing16, horizontal = TangemTheme.dimens.spacing20, @@ -92,27 +92,17 @@ private fun AppSettingsScreenPreview_Dark( private class AppSettingsScreenStateProvider : CollectionPreviewParameterProvider( collection = buildList { val itemsFactory = AppSettingsItemsFactory() - val dialogsFactory = AppSettingsAlertsFactory() val items = persistentListOf( itemsFactory.createEnrollBiometricsCard {}, itemsFactory.createSelectAppCurrencyButton(currentAppCurrencyName = "US Dollar") {}, itemsFactory.createSaveWalletsSwitch(isChecked = true, isEnabled = true, { _ -> }), itemsFactory.createSaveAccessCodeSwitch(isChecked = false, isEnabled = true) { _ -> }, + itemsFactory.createSelectThemeModeButton(AppThemeMode.DEFAULT, {}), ) AppSettingsScreenState.Content( items = items, - alert = null, - ).let(::add) - - AppSettingsScreenState.Content( - items = items, - alert = dialogsFactory.createDeleteSavedWalletsAlert({}, {}), - ).let(::add) - - AppSettingsScreenState.Content( - items = items, - alert = dialogsFactory.createDeleteSavedAccessCodesAlert({}, {}), + dialog = null, ).let(::add) }, ) diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsScreenState.kt b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsScreenState.kt index 4802004976..108b30fec8 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsScreenState.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsScreenState.kt @@ -12,7 +12,7 @@ internal sealed class AppSettingsScreenState { data class Content( val items: ImmutableList, - val alert: Alert?, + val dialog: Dialog?, ) : AppSettingsScreenState() @Immutable @@ -46,11 +46,25 @@ internal sealed class AppSettingsScreenState { ) : Item() } - data class Alert( - val title: TextReference, - val description: TextReference, - val confirmText: TextReference, - val onConfirm: () -> Unit, - val onDismiss: () -> Unit, - ) + @Immutable + sealed class Dialog { + + abstract val onDismiss: () -> Unit + + data class Alert( + val title: TextReference, + val description: TextReference, + val confirmText: TextReference, + val onConfirm: () -> Unit, + override val onDismiss: () -> Unit, + ) : Dialog() + + data class Selector( + val title: TextReference, + val selectedItemIndex: Int, + val items: ImmutableList, + val onSelect: (Int) -> Unit, + override val onDismiss: () -> Unit, + ) : Dialog() + } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsViewModel.kt b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsViewModel.kt index 5aedd6f7b7..10e6eeaf51 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsViewModel.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsViewModel.kt @@ -3,6 +3,7 @@ package com.tangem.tap.features.details.ui.appsettings import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue +import com.tangem.domain.apptheme.model.AppThemeMode import com.tangem.tap.common.extensions.dispatchOnMain import com.tangem.tap.common.redux.AppState import com.tangem.tap.features.details.redux.AppSetting @@ -17,7 +18,7 @@ import org.rekotlin.Store internal class AppSettingsViewModel(private val store: Store) { private val itemsFactory = AppSettingsItemsFactory() - private val alertsFactory = AppSettingsAlertsFactory() + private val dialogsFactory = AppSettingsDialogsFactory() var uiState: AppSettingsScreenState by mutableStateOf(AppSettingsScreenState.Loading) private set @@ -25,7 +26,7 @@ internal class AppSettingsViewModel(private val store: Store) { fun updateState(state: DetailsState) { uiState = AppSettingsScreenState.Content( items = buildItems(state.appSettingsState), - alert = (uiState as? AppSettingsScreenState.Content)?.alert, + dialog = (uiState as? AppSettingsScreenState.Content)?.dialog, ) } @@ -63,6 +64,10 @@ internal class AppSettingsViewModel(private val store: Store) { onCheckedChange = ::onSaveAccessCodesToggled, ).let(::add) } + + itemsFactory.createSelectThemeModeButton(state.selectedThemeMode) { + showThemeModeSelector(state.selectedThemeMode) + }.let(::add) } return items.toImmutableList() @@ -76,13 +81,28 @@ internal class AppSettingsViewModel(private val store: Store) { store.dispatchOnMain(WalletAction.AppCurrencyAction.ChooseAppCurrency) } + private fun showThemeModeSelector(selectedMode: AppThemeMode) { + updateContentState { + copy( + dialog = dialogsFactory.createThemeModeSelectorDialog( + selectedModeIndex = selectedMode.ordinal, + onSelect = { mode -> + store.dispatchOnMain(DetailsAction.AppSettings.ChangeAppThemeMode(mode)) + dismissDialog() + }, + onDismiss = ::dismissDialog, + ), + ) + } + } + private fun onSaveWalletsToggled(isChecked: Boolean) { if (isChecked) { onSettingsToggled(AppSetting.SaveWallets, enable = true) } else { updateContentState { copy( - alert = alertsFactory.createDeleteSavedWalletsAlert( + dialog = dialogsFactory.createDeleteSavedWalletsAlert( onDelete = { onSettingsToggled(AppSetting.SaveWallets, enable = false) dismissDialog() @@ -100,7 +120,7 @@ internal class AppSettingsViewModel(private val store: Store) { } else { updateContentState { copy( - alert = alertsFactory.createDeleteSavedAccessCodesAlert( + dialog = dialogsFactory.createDeleteSavedAccessCodesAlert( onDelete = { onSettingsToggled(AppSetting.SaveAccessCode, enable = false) dismissDialog() @@ -117,7 +137,7 @@ internal class AppSettingsViewModel(private val store: Store) { } private fun dismissDialog() { - updateContentState { copy(alert = null) } + updateContentState { copy(dialog = null) } } private fun updateContentState(block: AppSettingsScreenState.Content.() -> AppSettingsScreenState.Content) { diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/components/SettingsAlertDialog.kt b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/components/SettingsAlertDialog.kt index e592fb6cf0..1dce42e622 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/components/SettingsAlertDialog.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/components/SettingsAlertDialog.kt @@ -9,52 +9,52 @@ import com.tangem.core.ui.components.BasicDialog import com.tangem.core.ui.components.DialogButton import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.res.TangemTheme -import com.tangem.tap.features.details.ui.appsettings.AppSettingsAlertsFactory -import com.tangem.tap.features.details.ui.appsettings.AppSettingsScreenState.Alert +import com.tangem.tap.features.details.ui.appsettings.AppSettingsDialogsFactory +import com.tangem.tap.features.details.ui.appsettings.AppSettingsScreenState.Dialog import com.tangem.wallet.R @Composable -internal fun SettingsAlertDialog(alert: Alert) { +internal fun SettingsAlertDialog(dialog: Dialog.Alert) { BasicDialog( - title = alert.title.resolveReference(), - message = alert.description.resolveReference(), + title = dialog.title.resolveReference(), + message = dialog.description.resolveReference(), isDismissable = false, confirmButton = DialogButton( - title = alert.confirmText.resolveReference(), + title = dialog.confirmText.resolveReference(), warning = true, - onClick = alert.onConfirm, + onClick = dialog.onConfirm, ), dismissButton = DialogButton( title = stringResource(id = R.string.common_cancel), - onClick = alert.onDismiss, + onClick = dialog.onDismiss, ), - onDismissDialog = alert.onDismiss, + onDismissDialog = dialog.onDismiss, ) } // region Preview @Preview(showBackground = true, widthDp = 360) @Composable -private fun AlertDialogPreview_Light(@PreviewParameter(AlertDialogProvider::class) dialog: Alert) { +private fun AlertDialogPreview_Light(@PreviewParameter(AlertDialogProvider::class) dialog: Dialog.Alert) { TangemTheme { - SettingsAlertDialog(alert = dialog) + SettingsAlertDialog(dialog = dialog) } } @Preview(showBackground = true, widthDp = 360) @Composable -private fun AlertDialogPreview_Dark(@PreviewParameter(AlertDialogProvider::class) dialog: Alert) { +private fun AlertDialogPreview_Dark(@PreviewParameter(AlertDialogProvider::class) dialog: Dialog.Alert) { TangemTheme(isDark = true) { - SettingsAlertDialog(alert = dialog) + SettingsAlertDialog(dialog = dialog) } } -private class AlertDialogProvider : CollectionPreviewParameterProvider( +private class AlertDialogProvider : CollectionPreviewParameterProvider( collection = buildList { - val alertsFactory = AppSettingsAlertsFactory() + val dialogsFactory = AppSettingsDialogsFactory() - alertsFactory.createDeleteSavedAccessCodesAlert({}, {}).let(::add) - alertsFactory.createDeleteSavedWalletsAlert({}, {}).let(::add) + dialogsFactory.createDeleteSavedAccessCodesAlert({}, {}).let(::add) + dialogsFactory.createDeleteSavedWalletsAlert({}, {}).let(::add) }, ) // endregion Preview \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/components/ButtonItem.kt b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/components/SettingsButtonItem.kt similarity index 94% rename from app/src/main/java/com/tangem/tap/features/details/ui/appsettings/components/ButtonItem.kt rename to app/src/main/java/com/tangem/tap/features/details/ui/appsettings/components/SettingsButtonItem.kt index 4010f633ed..946c62d2f2 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/components/ButtonItem.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/components/SettingsButtonItem.kt @@ -20,7 +20,7 @@ import com.tangem.tap.features.details.ui.appsettings.AppSettingsScreenState.Ite @OptIn(ExperimentalMaterialApi::class) @Composable -internal fun ButtonItem(item: Item.Button, modifier: Modifier = Modifier) { +internal fun SettingsButtonItem(item: Item.Button, modifier: Modifier = Modifier) { Surface( modifier = modifier.fillMaxWidth(), color = TangemTheme.colors.background.secondary, @@ -55,7 +55,7 @@ internal fun ButtonItem(item: Item.Button, modifier: Modifier = Modifier) { @Composable private fun ButtonItemPreview_Light(@PreviewParameter(ButtonItemProvider::class) item: Item.Button) { TangemTheme { - ButtonItem(item = item) + SettingsButtonItem(item = item) } } @@ -63,7 +63,7 @@ private fun ButtonItemPreview_Light(@PreviewParameter(ButtonItemProvider::class) @Composable private fun ButtonItemPreview_Dark(@PreviewParameter(ButtonItemProvider::class) item: Item.Button) { TangemTheme(isDark = true) { - ButtonItem(item = item) + SettingsButtonItem(item = item) } } diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/components/CardItem.kt b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/components/SettingsCardItem.kt similarity index 94% rename from app/src/main/java/com/tangem/tap/features/details/ui/appsettings/components/CardItem.kt rename to app/src/main/java/com/tangem/tap/features/details/ui/appsettings/components/SettingsCardItem.kt index 7643c0fd09..1560b14a43 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/components/CardItem.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/components/SettingsCardItem.kt @@ -22,7 +22,7 @@ import com.tangem.tap.features.details.ui.appsettings.AppSettingsScreenState.Ite @OptIn(ExperimentalMaterialApi::class) @Composable -internal fun CardItem(item: Item.Card, modifier: Modifier = Modifier) { +internal fun SettingsCardItem(item: Item.Card, modifier: Modifier = Modifier) { Surface( modifier = modifier.fillMaxWidth(), color = TangemTheme.colors.button.disabled, @@ -62,7 +62,7 @@ internal fun CardItem(item: Item.Card, modifier: Modifier = Modifier) { @Composable private fun CardItemPreview_Light(@PreviewParameter(CardItemProvider::class) item: Item.Card) { TangemTheme { - CardItem(item = item) + SettingsCardItem(item = item) } } @@ -70,7 +70,7 @@ private fun CardItemPreview_Light(@PreviewParameter(CardItemProvider::class) ite @Composable private fun CardItemPreview_Dark(@PreviewParameter(CardItemProvider::class) item: Item.Card) { TangemTheme(isDark = true) { - CardItem(item = item) + SettingsCardItem(item = item) } } diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/components/SettingsSelectorDialog.kt b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/components/SettingsSelectorDialog.kt new file mode 100644 index 0000000000..8c1a93e307 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/components/SettingsSelectorDialog.kt @@ -0,0 +1,58 @@ +package com.tangem.tap.features.details.ui.appsettings.components + +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider +import com.tangem.core.ui.components.DialogButton +import com.tangem.core.ui.components.SelectorDialog +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.res.TangemTheme +import com.tangem.tap.features.details.ui.appsettings.AppSettingsDialogsFactory +import com.tangem.tap.features.details.ui.appsettings.AppSettingsScreenState.Dialog +import com.tangem.wallet.R +import kotlinx.collections.immutable.toImmutableList + +@Composable +internal fun SettingsSelectorDialog(dialog: Dialog.Selector) { + SelectorDialog( + title = dialog.title.resolveReference(), + selectedItemIndex = dialog.selectedItemIndex, + items = dialog.items.map { it.resolveReference() }.toImmutableList(), + confirmButton = DialogButton( + title = stringResource(R.string.common_cancel), + onClick = dialog.onDismiss, + ), + onSelect = dialog.onSelect, + onDismissDialog = dialog.onDismiss, + ) +} + +// region Preview +@Preview(showBackground = true, widthDp = 360) +@Composable +private fun SettingsSelectorDialogPreview_Light(@PreviewParameter(DialogProvider::class) param: Dialog.Selector) { + TangemTheme(isDark = false) { + SettingsSelectorDialog(param) + } +} + +@Preview(showBackground = true, widthDp = 360) +@Composable +private fun SettingsSelectorDialogPreview_Dark(@PreviewParameter(DialogProvider::class) param: Dialog.Selector) { + TangemTheme(isDark = true) { + SettingsSelectorDialog(param) + } +} + +private class DialogProvider : CollectionPreviewParameterProvider( + collection = listOf( + AppSettingsDialogsFactory().createThemeModeSelectorDialog( + selectedModeIndex = 0, + onSelect = {}, + onDismiss = {}, + ), + ), +) +// endregion Preview \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/components/SwitchItem.kt b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/components/SettingsSwitchItem.kt similarity index 95% rename from app/src/main/java/com/tangem/tap/features/details/ui/appsettings/components/SwitchItem.kt rename to app/src/main/java/com/tangem/tap/features/details/ui/appsettings/components/SettingsSwitchItem.kt index 8d918ced3a..78084743b7 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/components/SwitchItem.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/components/SettingsSwitchItem.kt @@ -21,7 +21,7 @@ import com.tangem.tap.features.details.ui.appsettings.AppSettingsScreenState.Ite import com.tangem.tap.features.details.ui.common.TangemSwitch @Composable -internal fun SwitchItem(item: Item.Switch, modifier: Modifier = Modifier) { +internal fun SettingsSwitchItem(item: Item.Switch, modifier: Modifier = Modifier) { val titleTextColor by rememberUpdatedState( newValue = if (item.isEnabled) { TangemTheme.colors.text.primary1 @@ -72,7 +72,7 @@ internal fun SwitchItem(item: Item.Switch, modifier: Modifier = Modifier) { @Composable private fun SwitchItemPreview_Light(@PreviewParameter(SwitchItemProvider::class) item: Item.Switch) { TangemTheme { - SwitchItem(item = item) + SettingsSwitchItem(item = item) } } @@ -80,7 +80,7 @@ private fun SwitchItemPreview_Light(@PreviewParameter(SwitchItemProvider::class) @Composable private fun SwitchItemPreview_Dark(@PreviewParameter(SwitchItemProvider::class) item: Item.Switch) { TangemTheme(isDark = true) { - SwitchItem(item = item) + SettingsSwitchItem(item = item) } } diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/dialogs/ResetBackupCardDialog.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/dialogs/ResetBackupCardDialog.kt index 0da9a7063a..0e7acbaa00 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/dialogs/ResetBackupCardDialog.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/dialogs/ResetBackupCardDialog.kt @@ -15,7 +15,7 @@ object ResetBackupCardDialog { setPositiveButton(R.string.common_cancel) { _, _ -> /* no-op */ } - setNegativeButton(R.string.common_reset) { _, _ -> + setNegativeButton(R.string.card_settings_action_sheet_reset) { _, _ -> store.dispatch(BackupAction.ResetBackupCard(cardId)) } setOnDismissListener { diff --git a/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphState.kt b/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphState.kt index 7a479780e6..7be3ae88ba 100644 --- a/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphState.kt +++ b/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphState.kt @@ -3,6 +3,7 @@ package com.tangem.tap.proxy.redux import com.tangem.datasource.asset.AssetReader import com.tangem.datasource.connection.NetworkConnectionManager import com.tangem.domain.appcurrency.repository.AppCurrencyRepository +import com.tangem.domain.apptheme.repository.AppThemeModeRepository import com.tangem.domain.card.ScanCardProcessor import com.tangem.domain.card.ScanCardUseCase import com.tangem.domain.card.repository.CardSdkConfigRepository @@ -38,6 +39,7 @@ data class DaggerGraphState( val appCurrencyRepository: AppCurrencyRepository? = null, val walletManagersFacade: WalletManagersFacade? = null, val appStateHolder: AppStateHolder? = null, + val appThemeModeRepository: AppThemeModeRepository? = null, // FIXME: It is used only for TokensList screen. Remove after refactoring of TokensList val currenciesRepository: CurrenciesRepository? = null, diff --git a/core/res/src/main/res/values-ru/strings.xml b/core/res/src/main/res/values-ru/strings.xml index 6277622e78..c7cf8aeddc 100644 --- a/core/res/src/main/res/values-ru/strings.xml +++ b/core/res/src/main/res/values-ru/strings.xml @@ -28,11 +28,16 @@ Подключите функцию хранения кодов доступа от карт на телефоне в зашифрованном виде, и при работе с картой вместо кода доступа будет запрашиваться биометрическая аутентификация. Cохранение кошелька Подключите функцию привязки карты в приложении, а также возможность биометрической аутентификации. Подпись транзакции все так же потребует карту. + Тёмная + Светлая + Как в системе + Тема Настройки приложения Пожалуйста, отсканируйте карту Пожалуйста, попробуйте снова через 30 секунд или отсканируйте карту Слишком много попыток Вы отключили биометрическую аутентификацию на вашем телефоне и не сможете сохранять кошельки в приложении. Для сохранения кошельков, пожалуйста, включите функцию биометрической аутентификации в настройках телефона. + Начать резервное копирование %d карта %d карты @@ -43,6 +48,7 @@ Использовать эту карту для сброса кода доступа на других картах в этом кошельке Отключить возможность сброса кода доступа на этой карте или других картах этого кошелька Восстановление кода доступа + Сбросить Вы уверены, что хотите это сделать? Смена кода доступа Код доступа будет изменен только на данной карте @@ -96,7 +102,6 @@ Отклонить Перезагрузить Переименовать - Сбросить Сохранить изменения Искать Поиск токенов @@ -196,7 +201,7 @@ Управление токенами Чтобы защитить свои активы, мы советуем вам выполнить эту процедуру - Бэкап кошелька не был произведен + Резервное копирование не выполнено Баланс В сумме учтены не все монеты 1INCH токены будут зачислены на адрес вашего кошелька в сети %s в течение 48 часов @@ -210,6 +215,7 @@ Вам надо сгенерировать адреса для %d новых сетей, используя вашу карту Некоторые адреса отсутствуют + Невозможно покрыть %1$s комиссию Вам необходимо установить единый код доступа для защиты всех ваших карт Защита Позже вы сможете установить индивидуальный код доступа для каждой карты @@ -407,7 +413,7 @@ Расплачивайтесь Отправляйте Храните - Встречайте\nTangem + Встречайте Tangem Обменивайте, покупайте NFT, получайте займы и делайте вклады в более чем 100 различных децентрализованных сервисах Поддержка DeFi Подтверждения считаются отраслевым стандартом для всех децентрализованных бирж и защищают ваш кошелек от доступа со стороны смарт-контракта без вашего разрешения. По замыслу смарт-контракты не могут получить доступ к вашим токенам, если вы не одобрите доступ со своей стороны. «Разблокируя» свои токены, вы даете смарт-контракту 1inch разрешение тратить ваши активы. Майнеры сети получают компенсацию за газ (оплачиваемый вами) за запись этого действия в блокчейне. Как только разрешение будет предоставлено, вы сможете обменять свой токен. diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index 186b92d9ea..02f330b04b 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -28,11 +28,16 @@ Biometric authentication will be requested instead of the access code for interactions with your card. Keep the wallet in the app Enable to link all the wallets to Tangem app. Biometric authentication will be required for unlocking the app. Transaction signing requires tapping your Tangem card. + Dark + Light + System default + Theme App Settings Please scan the card Please try again in 30 seconds or scan the card Too many attempts You have disabled biometric authentication on your phone and will not be able to save wallets in the app. To save wallets, please enable the biometric authentication function in your phone settings. + Start backup process %d card %d cards @@ -41,6 +46,7 @@ Allows you to use this card to reset access code on other cards in this wallet Disable the ability to reset the access code on this card or other cards in this wallet Access code recovery + Reset Are you sure you want to do this? Change Access Code Access code will be changed on this card only @@ -95,7 +101,6 @@ Reject Reload Rename - Reset Save changes Search Search tokens @@ -194,7 +199,7 @@ Manage tokens To protect your assets, we advise you to carry out this procedure - Your wallet has not been backed up + Your wallet hasn\'t been backed up Total balance The amount does not include some of your funds 1INCH tokens will be credited to your %s wallet address within 48 hours @@ -206,6 +211,7 @@ You need to generate addresses for %d new networks using your card Some addresses are missing + Unable to cover %1$s fee You have to set up a single access code to protect all your wallets Protect You can set up an individual access code on each card later @@ -233,7 +239,7 @@ Skip for later How does it work? Let\'s generate all the keys on your card and create a secure wallet - Create a wallet + Create wallet Create a wallet Other options Your keys will be securely generated inside the card. There is no seed phrase, which means nobody can export or steal it. @@ -400,7 +406,7 @@ Pay Send Store - Meet\nTangem + Meet Tangem Exchange, buy NFT\'s, make loans and deposits in more than 100 different decentralized services DeFi Compatible Approvals are considered an industry standard across all decentralized exchanges and protect your wallet from being accessed by a smart contract without your permission. By design, smart contracts can\'t access your tokens unless you approve access from your end. By \"unlocking\" your tokens, you are give permission to the 1inch smart contract to spend your assets. The miners of the network are compensated with a gas fee (paid by you) to record this action on the blockchain. Once permission has been granted you will be able to swap your token. diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/Dialogs.kt b/core/ui/src/main/java/com/tangem/core/ui/components/Dialogs.kt index 24a2316d60..41e2d008b6 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/Dialogs.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/Dialogs.kt @@ -1,44 +1,33 @@ package com.tangem.core.ui.components +import androidx.compose.foundation.LocalIndication import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.material.RadioButton +import androidx.compose.material.RadioButtonDefaults import androidx.compose.material.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.Immutable +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.DialogProperties import com.tangem.core.ui.R +import com.tangem.core.ui.components.SelctorDialogParamsProvider.SelectorDialogParams import com.tangem.core.ui.res.TangemTheme - -/** - * Dialog button params - * - * @param title Button text. If not provided default values will be used - * @param warning If true then button text will be in theme warning color - * @param enabled If false button will be disabled - * @param onClick Button click callback - */ -data class DialogButton( - val title: String? = null, - val warning: Boolean = false, - val enabled: Boolean = true, - val onClick: () -> Unit, -) - -/** - * Additional params for dialog text field - */ -data class AdditionalTextInputDialogParams( - val label: String? = null, - val placeholder: String? = null, - val caption: String? = null, - val enabled: Boolean = true, - val isError: Boolean = false, -) +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toImmutableList /** * Simple alert dialog with a message and 'OK' button @@ -129,6 +118,54 @@ fun TextInputDialog( ) } +@Composable +fun SelectorDialog( + selectedItemIndex: Int, + items: ImmutableList, + confirmButton: DialogButton, + onSelect: (index: Int) -> Unit, + onDismissDialog: () -> Unit, + title: String? = null, + isDismissable: Boolean = true, +) { + TangemDialog( + type = DialogType.Selector(selectedItemIndex, items, onSelect), + confirmButton = confirmButton, + title = title, + onDismissDialog = onDismissDialog, + properties = DialogProperties( + dismissOnBackPress = isDismissable, + dismissOnClickOutside = isDismissable, + ), + ) +} + +/** + * Dialog button params + * + * @param title Button text. If not provided default values will be used + * @param warning If true then button text will be in theme warning color + * @param enabled If false button will be disabled + * @param onClick Button click callback + */ +data class DialogButton( + val title: String? = null, + val warning: Boolean = false, + val enabled: Boolean = true, + val onClick: () -> Unit, +) + +/** + * Additional params for dialog text field + */ +data class AdditionalTextInputDialogParams( + val label: String? = null, + val placeholder: String? = null, + val caption: String? = null, + val enabled: Boolean = true, + val isError: Boolean = false, +) + // region Defaults @Composable private fun TangemDialog( @@ -146,15 +183,18 @@ private fun TangemDialog( shape = TangemTheme.shapes.roundedCornersLarge, color = TangemTheme.colors.background.plain, ) - .padding(all = TangemTheme.dimens.spacing24), + .padding(vertical = TangemTheme.dimens.spacing24), ) { if (title != null) { Text( - modifier = Modifier.fillMaxWidth(), + modifier = Modifier + .padding(horizontal = TangemTheme.dimens.spacing24) + .fillMaxWidth(), text = title, style = when (type) { is DialogType.Message -> TangemTheme.typography.h2 is DialogType.TextInput -> TangemTheme.typography.h3 + is DialogType.Selector -> TangemTheme.typography.h2 }, color = TangemTheme.colors.text.primary1, ) @@ -162,7 +202,13 @@ private fun TangemDialog( } DialogContent(type = type) SpacerH24() - DialogButtons(confirmButton = confirmButton, dismissButton = dismissButton) + DialogButtons( + modifier = Modifier + .padding(horizontal = TangemTheme.dimens.spacing24) + .fillMaxWidth(), + confirmButton = confirmButton, + dismissButton = dismissButton, + ) } } } @@ -177,7 +223,9 @@ private fun DialogContent(type: DialogType, modifier: Modifier = Modifier) { when (type) { is DialogType.Message -> { Text( - modifier = Modifier.fillMaxWidth(), + modifier = Modifier + .padding(horizontal = TangemTheme.dimens.spacing24) + .fillMaxWidth(), text = type.message, style = TangemTheme.typography.body2, color = TangemTheme.colors.text.secondary, @@ -185,7 +233,9 @@ private fun DialogContent(type: DialogType, modifier: Modifier = Modifier) { } is DialogType.TextInput -> { OutlineTextField( - modifier = Modifier.fillMaxWidth(), + modifier = Modifier + .padding(horizontal = TangemTheme.dimens.spacing24) + .fillMaxWidth(), value = type.value, label = type.params.label, placeholder = type.params.placeholder, @@ -197,6 +247,14 @@ private fun DialogContent(type: DialogType, modifier: Modifier = Modifier) { }, ) } + is DialogType.Selector -> { + SelectorDialogContent( + modifier = Modifier.fillMaxWidth(), + selectedItemIndex = type.selectedItemIndex, + items = type.items, + onSelect = type.onSelect, + ) + } } } } @@ -204,7 +262,7 @@ private fun DialogContent(type: DialogType, modifier: Modifier = Modifier) { @Composable private fun DialogButtons(confirmButton: DialogButton, dismissButton: DialogButton?, modifier: Modifier = Modifier) { Row( - modifier = modifier.fillMaxWidth(), + modifier = modifier, horizontalArrangement = Arrangement.spacedBy( space = TangemTheme.dimens.spacing4, alignment = Alignment.End, @@ -252,14 +310,72 @@ private fun DialogButton( } } -private sealed interface DialogType { - data class Message(val message: String) : DialogType +@Composable +private fun SelectorDialogContent( + selectedItemIndex: Int, + items: ImmutableList, + onSelect: (index: Int) -> Unit, + modifier: Modifier = Modifier, +) { + LazyColumn(modifier = modifier) { + itemsIndexed(items = items) { index, itemText -> + val onClick = remember(index) { + { onSelect(index) } + } + + val interactionSource = remember { MutableInteractionSource() } + + Row( + modifier = Modifier + .clickable( + interactionSource = interactionSource, + indication = LocalIndication.current, + onClick = onClick, + ) + .padding( + vertical = TangemTheme.dimens.spacing16, + horizontal = TangemTheme.dimens.spacing18, + ) + .fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing16), + verticalAlignment = Alignment.CenterVertically, + ) { + RadioButton( + modifier = Modifier.size(TangemTheme.dimens.size24), + selected = index == selectedItemIndex, + onClick = onClick, + colors = RadioButtonDefaults.colors( + selectedColor = TangemTheme.colors.icon.accent, + unselectedColor = TangemTheme.colors.icon.secondary, + ), + interactionSource = interactionSource, + ) + Text( + text = itemText, + style = TangemTheme.typography.body1, + color = TangemTheme.colors.text.primary1, + ) + } + } + } +} + +@Immutable +private sealed class DialogType { + + data class Message(val message: String) : DialogType() data class TextInput( val value: TextFieldValue, val onValueChange: (TextFieldValue) -> Unit, val params: AdditionalTextInputDialogParams = AdditionalTextInputDialogParams(), - ) : DialogType + ) : DialogType() + + data class Selector( + val selectedItemIndex: Int, + val items: ImmutableList, + val onSelect: (index: Int) -> Unit, + ) : DialogType() } // endregion Defaults @@ -381,4 +497,65 @@ private fun TextInputDialogPreview_Dark() { TextInputDialogSample() } } + +@Preview(showBackground = true, widthDp = 360) +@Composable +private fun SelectorDialogPreview_Light( + @PreviewParameter(SelctorDialogParamsProvider::class) param: SelectorDialogParams, +) { + TangemTheme(isDark = false) { + SelectorDialog( + title = param.title, + items = param.items, + selectedItemIndex = param.selectedItemIndex, + confirmButton = DialogButton(title = "Cancel", onClick = {}), + onSelect = {}, + onDismissDialog = {}, + ) + } +} + +@Preview(showBackground = true, widthDp = 360) +@Composable +private fun SelectorDialogPreview_Dark( + @PreviewParameter(SelctorDialogParamsProvider::class) param: SelectorDialogParams, +) { + TangemTheme(isDark = true) { + SelectorDialog( + title = param.title, + items = param.items, + selectedItemIndex = param.selectedItemIndex, + confirmButton = DialogButton(title = "Cancel", onClick = {}), + onSelect = {}, + onDismissDialog = {}, + ) + } +} + +private class SelctorDialogParamsProvider : CollectionPreviewParameterProvider( + collection = listOf( + SelectorDialogParams( + title = "Theme", + selectedItemIndex = 0, + persistentListOf("Light", "Dark", "Follow system"), + ), + SelectorDialogParams( + title = null, + selectedItemIndex = 2, + persistentListOf("Light", "Dark", "Follow system"), + ), + SelectorDialogParams( + title = "Count", + selectedItemIndex = 8, + List(size = 10) { it.toString() }.toImmutableList(), + ), + ), +) { + + data class SelectorDialogParams( + val title: String?, + val selectedItemIndex: Int, + val items: ImmutableList, + ) +} // endregion Preview \ No newline at end of file diff --git a/domain/app-theme/models/src/main/kotlin/com/tangem/domain/apptheme/model/AppThemeMode.kt b/domain/app-theme/models/src/main/kotlin/com/tangem/domain/apptheme/model/AppThemeMode.kt index f6ff6269f7..db744ebb4c 100644 --- a/domain/app-theme/models/src/main/kotlin/com/tangem/domain/apptheme/model/AppThemeMode.kt +++ b/domain/app-theme/models/src/main/kotlin/com/tangem/domain/apptheme/model/AppThemeMode.kt @@ -26,5 +26,10 @@ enum class AppThemeMode { * The default [AppThemeMode]. */ val DEFAULT: AppThemeMode = FORCE_LIGHT + + /** + * List of available [AppThemeMode]s. + * */ + val available: List = values().toList() } } \ No newline at end of file From 164b6e3c71c249520a47b0434c0f832f7ea5f89c Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 7 Sep 2023 13:04:47 +0300 Subject: [PATCH 017/242] Updated on 2026-08-14 --- .../datasource/api/common/LocalDateAdapter.kt | 29 ++ .../api/tangemTech/models/ReferralResponse.kt | 18 +- .../com/tangem/datasource/di/MoshiModule.kt | 2 + .../com/tangem/datasource/di/NetworkModule.kt | 2 +- core/res/src/main/res/values-ru/strings.xml | 2 - .../src/main/res/values-zh-rTW/strings.xml | 2 - core/res/src/main/res/values/strings.xml | 2 - .../referral/converters/ReferralConverter.kt | 57 +++- .../referral/data/ReferralRepositoryImpl.kt | 2 +- .../referral/domain/ReferralInteractorImpl.kt | 12 +- .../referral/domain/ReferralRepository.kt | 2 +- .../referral/domain/models/ReferralData.kt | 11 + .../referral/models/ReferralStateHolder.kt | 3 + .../ui/AgreementBottomSheetContent.kt | 10 +- .../tangem/feature/referral/ui/AwardItems.kt | 71 +++++ .../feature/referral/ui/CornersToRound.kt | 27 ++ .../referral/ui/ParticipateBottomBlock.kt | 282 ++++++++++++++++-- .../feature/referral/ui/ReferralScreen.kt | 99 +++++- .../referral/viewmodels/ReferralViewModel.kt | 16 +- 19 files changed, 584 insertions(+), 65 deletions(-) create mode 100644 core/datasource/src/main/java/com/tangem/datasource/api/common/LocalDateAdapter.kt create mode 100644 features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/AwardItems.kt create mode 100644 features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/CornersToRound.kt diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/LocalDateAdapter.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/LocalDateAdapter.kt new file mode 100644 index 0000000000..b11c76fccd --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/LocalDateAdapter.kt @@ -0,0 +1,29 @@ +package com.tangem.datasource.api.common + +import com.squareup.moshi.FromJson +import com.squareup.moshi.JsonAdapter +import com.squareup.moshi.JsonReader +import com.squareup.moshi.JsonWriter +import com.squareup.moshi.ToJson +import org.joda.time.LocalDate +import org.joda.time.format.DateTimeFormat + +class LocalDateAdapter : JsonAdapter() { + + private val formatter = DateTimeFormat.forPattern("yyyy-MM-dd") + + @FromJson + override fun fromJson(reader: JsonReader): LocalDate? { + val dateString = reader.nextString() + return LocalDate.parse(dateString, formatter) + } + + @ToJson + override fun toJson(writer: JsonWriter, value: LocalDate?) { + if (value != null) { + writer.value(formatter.print(value)) + } else { + writer.nullValue() + } + } +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/ReferralResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/ReferralResponse.kt index 712c98c40a..2abb6e2a57 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/ReferralResponse.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/ReferralResponse.kt @@ -1,14 +1,12 @@ package com.tangem.datasource.api.tangemTech.models import com.squareup.moshi.Json +import org.joda.time.LocalDate -/** - * Main response class for referral API - * contains all necessary info about users program status - */ data class ReferralResponse( @Json(name = "conditions") val conditions: Conditions, @Json(name = "referral") val referral: Referral?, + @Json(name = "expectedAwards") val expectedAwards: ExpectedAwards?, ) { data class Conditions( @@ -45,4 +43,16 @@ data class ReferralResponse( @Json(name = "walletsPurchased") val walletsPurchased: Int, @Json(name = "termsAcceptedAt") val termsAcceptedAt: String?, ) + + data class ExpectedAwards( + @Json(name = "numberOfWallets") val numberOfWallets: Int, + @Json(name = "list") val list: List, + ) { + + data class AwardItem( + @Json(name = "currency") val currency: String, + @Json(name = "paymentDate") val paymentDate: LocalDate, + @Json(name = "amount") val amount: Int, + ) + } } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/di/MoshiModule.kt b/core/datasource/src/main/java/com/tangem/datasource/di/MoshiModule.kt index 65668982d4..bf3d94095f 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/di/MoshiModule.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/di/MoshiModule.kt @@ -4,6 +4,7 @@ import com.squareup.moshi.Moshi import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory import com.tangem.common.json.MoshiJsonConverter import com.tangem.datasource.api.common.BigDecimalAdapter +import com.tangem.datasource.api.common.LocalDateAdapter import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -20,6 +21,7 @@ class MoshiModule { fun provideNetworkMoshi(): Moshi { return Moshi.Builder() .add(BigDecimalAdapter()) + .add(LocalDateAdapter()) .add(KotlinJsonAdapterFactory()) .build() } diff --git a/core/datasource/src/main/java/com/tangem/datasource/di/NetworkModule.kt b/core/datasource/src/main/java/com/tangem/datasource/di/NetworkModule.kt index 9c11afc771..10abba599f 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/di/NetworkModule.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/di/NetworkModule.kt @@ -28,7 +28,7 @@ class NetworkModule { fun provideTangemTechApi(@NetworkMoshi moshi: Moshi): TangemTechApi { return Retrofit.Builder() .addConverterFactory(MoshiConverterFactory.create(moshi)) - .baseUrl(PROD_TANGEM_TECH_BASE_URL) + .baseUrl(if (BuildConfig.DEBUG) DEV_TANGEM_TECH_BASE_URL else PROD_TANGEM_TECH_BASE_URL) .client( OkHttpClient.Builder() .addHeaders( diff --git a/core/res/src/main/res/values-ru/strings.xml b/core/res/src/main/res/values-ru/strings.xml index c7cf8aeddc..743f1f19ad 100644 --- a/core/res/src/main/res/values-ru/strings.xml +++ b/core/res/src/main/res/values-ru/strings.xml @@ -325,8 +325,6 @@ за %d кошельков Получите ^^%1$s^^ на ваш адрес в сети %2$s %3$s ^^спустя 30 дней^^ за каждый кошелек, который купит ваш друг - Получите - на ваш адрес в сети %1$s%2$s за каждый кошелек, который купит ваш друг Вы Получит при покупке кошелька на сайте tangem.com diff --git a/core/res/src/main/res/values-zh-rTW/strings.xml b/core/res/src/main/res/values-zh-rTW/strings.xml index 294f3413b7..4af7ef5160 100644 --- a/core/res/src/main/res/values-zh-rTW/strings.xml +++ b/core/res/src/main/res/values-zh-rTW/strings.xml @@ -259,8 +259,6 @@ 無法加載有關推薦計劃的消息。請稍後再試 無法加載有關推薦計劃的消息。原因:%s。請稍後再試 您的朋友買 - 會得到 - 對於你的朋友在你的 %1$s 網絡地址%2$s上購買的每個錢包 得到 %s 折扣 diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index 02f330b04b..6e8c21c2a4 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -320,8 +320,6 @@ for %d wallets Will get ^^%1$s^^ for each wallet bought by your friend on your %2$s network address %3$s ^^30 days after^^ that - Will get - for each wallet bought by your friend on your %1$s network address%2$s You Will get a when buying a wallet on tangem.com diff --git a/features/referral/data/src/main/java/com/tangem/feature/referral/converters/ReferralConverter.kt b/features/referral/data/src/main/java/com/tangem/feature/referral/converters/ReferralConverter.kt index 11ee37cff4..921f57b107 100644 --- a/features/referral/data/src/main/java/com/tangem/feature/referral/converters/ReferralConverter.kt +++ b/features/referral/data/src/main/java/com/tangem/feature/referral/converters/ReferralConverter.kt @@ -1,13 +1,17 @@ package com.tangem.feature.referral.converters +import android.text.format.DateUtils import com.tangem.datasource.api.tangemTech.models.ReferralResponse -import com.tangem.feature.referral.domain.models.DiscountType -import com.tangem.feature.referral.domain.models.ReferralData -import com.tangem.feature.referral.domain.models.ReferralInfo -import com.tangem.feature.referral.domain.models.TokenData +import com.tangem.feature.referral.domain.models.* import com.tangem.utils.converter.Converter +import com.tangem.utils.extensions.isToday +import com.tangem.utils.extensions.isYesterday import com.tangem.utils.safeValueOf +import org.joda.time.DateTime +import org.joda.time.DateTimeZone +import org.joda.time.format.DateTimeFormatterBuilder import org.joda.time.format.ISODateTimeFormat +import java.util.Locale import javax.inject.Inject class ReferralConverter @Inject constructor() : Converter { @@ -16,6 +20,7 @@ class ReferralConverter @Inject constructor() : Converter { + + /** Example, 2 Aug, 2023 */ + private val dateFormatter by lazy { + DateTimeFormatterBuilder() + .appendDayOfMonth(1) + .appendLiteral(' ') + .appendMonthOfYearShortText() + .appendLiteral(", ") + .appendYear(4, 4) + .toFormatter() + .withLocale(Locale.getDefault()) + } + + override fun convert(value: ReferralResponse.ExpectedAwards): ExpectedAwards { + return ExpectedAwards( + numberOfWallets = value.numberOfWallets, + expectedAwards = value.list.map { + ExpectedAward( + paymentDate = it.paymentDate.toDateTimeAtStartOfDay().millis.toDateFormat(), + amount = "${it.amount} ${it.currency}", + ) + }, + ) + } + + private fun Long.toDateFormat(): String { + val localDate = DateTime(this, DateTimeZone.getDefault()) + return if (localDate.isToday() || localDate.isYesterday()) { + DateUtils.getRelativeTimeSpanString( + this, + DateTime.now().millis, + DateUtils.DAY_IN_MILLIS, + DateUtils.FORMAT_ABBREV_RELATIVE, + ).toString() + } else { + dateFormatter.print(localDate) + } + } +} + private class TokenConverter : Converter { override fun convert(value: ReferralResponse.Conditions.Award.Token): TokenData { diff --git a/features/referral/data/src/main/java/com/tangem/feature/referral/data/ReferralRepositoryImpl.kt b/features/referral/data/src/main/java/com/tangem/feature/referral/data/ReferralRepositoryImpl.kt index 7f8d7470e7..8f8d3a5c42 100644 --- a/features/referral/data/src/main/java/com/tangem/feature/referral/data/ReferralRepositoryImpl.kt +++ b/features/referral/data/src/main/java/com/tangem/feature/referral/data/ReferralRepositoryImpl.kt @@ -22,7 +22,7 @@ internal class ReferralRepositoryImpl @Inject constructor( override val isDemoMode: Boolean get() = demoModeDatasource.isDemoModeActive - override suspend fun getReferralStatus(walletId: String): ReferralData { + override suspend fun getReferralData(walletId: String): ReferralData { return withContext(coroutineDispatcher.io) { referralConverter.convert( referralApi.getReferralStatus( diff --git a/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/ReferralInteractorImpl.kt b/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/ReferralInteractorImpl.kt index 6280321c45..a62fbcca35 100644 --- a/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/ReferralInteractorImpl.kt +++ b/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/ReferralInteractorImpl.kt @@ -20,9 +20,11 @@ internal class ReferralInteractorImpl( get() = repository.isDemoMode override suspend fun getReferralStatus(): ReferralData { - val refStatus = repository.getReferralStatus(userWalletManager.getWalletId()) - saveRefTokens(refStatus.tokens) - return refStatus + val referralData = repository.getReferralData(userWalletManager.getWalletId()) + + saveReferralTokens(referralData.tokens) + + return referralData } override suspend fun startReferral(): ReferralData { @@ -37,7 +39,7 @@ internal class ReferralInteractorImpl( address = publicAddress, ) } else { - error("tokens for ref is empty") + error("Tokens for ref is empty") } } @@ -53,7 +55,7 @@ internal class ReferralInteractorImpl( return derivationPath } - private fun saveRefTokens(tokens: List) { + private fun saveReferralTokens(tokens: List) { tokensForReferral.clear() tokensForReferral.addAll(tokens) } diff --git a/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/ReferralRepository.kt b/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/ReferralRepository.kt index ad770f8c2e..c3b5eaa9ca 100644 --- a/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/ReferralRepository.kt +++ b/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/ReferralRepository.kt @@ -7,7 +7,7 @@ interface ReferralRepository { val isDemoMode: Boolean /** Returns data object of [ReferralData] depends on user program status */ - suspend fun getReferralStatus(walletId: String): ReferralData + suspend fun getReferralData(walletId: String): ReferralData /** Starts user referral program */ suspend fun startReferral(walletId: String, networkId: String, tokenId: String, address: String): ReferralData diff --git a/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/models/ReferralData.kt b/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/models/ReferralData.kt index f5cc061cb0..751af6a116 100644 --- a/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/models/ReferralData.kt +++ b/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/models/ReferralData.kt @@ -18,6 +18,7 @@ sealed interface ReferralData { override val tosLink: String, override val tokens: List, val referral: ReferralInfo, + val expectedAwards: ExpectedAwards?, ) : ReferralData /** Data class that used if user is not participant of program */ @@ -47,6 +48,16 @@ data class ReferralInfo( val termsAcceptedAt: DateTime?, ) +data class ExpectedAwards( + val numberOfWallets: Int, + val expectedAwards: List, +) + +data class ExpectedAward( + val paymentDate: String, + val amount: String, +) + enum class DiscountType { PERCENTAGE, VALUE } \ No newline at end of file diff --git a/features/referral/presentation/src/main/java/com/tangem/feature/referral/models/ReferralStateHolder.kt b/features/referral/presentation/src/main/java/com/tangem/feature/referral/models/ReferralStateHolder.kt index 90e8def3d6..dc7b06e5ef 100644 --- a/features/referral/presentation/src/main/java/com/tangem/feature/referral/models/ReferralStateHolder.kt +++ b/features/referral/presentation/src/main/java/com/tangem/feature/referral/models/ReferralStateHolder.kt @@ -1,5 +1,7 @@ package com.tangem.feature.referral.models +import com.tangem.feature.referral.domain.models.ExpectedAwards + internal data class ReferralStateHolder( val headerState: HeaderState, val referralInfoState: ReferralInfoState, @@ -26,6 +28,7 @@ internal data class ReferralStateHolder( val code: String, val shareLink: String, override val url: String, + val expectedAwards: ExpectedAwards?, ) : ReferralInfoState, ReferralInfoContentState data class NonParticipantContent( diff --git a/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/AgreementBottomSheetContent.kt b/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/AgreementBottomSheetContent.kt index 2a1c5b1e86..c803919829 100644 --- a/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/AgreementBottomSheetContent.kt +++ b/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/AgreementBottomSheetContent.kt @@ -9,6 +9,7 @@ import androidx.compose.foundation.verticalScroll import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalConfiguration +import androidx.compose.ui.platform.LocalInspectionMode import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @@ -43,13 +44,16 @@ internal fun AgreementBottomSheetContent(url: String) { @Composable private fun AgreementHtmlView(url: String) { val state = rememberWebViewState(url) + val isInPreviewMode = LocalInspectionMode.current WebView( state = state, modifier = Modifier.background(TangemTheme.colors.background.secondary), onCreated = { - it.settings.apply { - javaScriptEnabled = false - allowFileAccess = false + if (!isInPreviewMode) { + it.settings.apply { + javaScriptEnabled = false + allowFileAccess = false + } } }, ) diff --git a/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/AwardItems.kt b/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/AwardItems.kt new file mode 100644 index 0000000000..7085ca0a34 --- /dev/null +++ b/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/AwardItems.kt @@ -0,0 +1,71 @@ +package com.tangem.feature.referral.ui + +import androidx.compose.foundation.layout.* +import androidx.compose.material.Surface +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.tooling.preview.Preview +import com.tangem.core.ui.res.TangemTheme + +@Suppress("LongParameterList") +@Composable +internal fun AwardText( + startText: String, + startTextColor: Color, + startTextStyle: TextStyle, + endText: String, + endTextColor: Color, + endTextStyle: TextStyle, + cornersToRound: CornersToRound, +) { + Surface( + shape = cornersToRound.getShape(), + color = TangemTheme.colors.background.primary, + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .heightIn(TangemTheme.dimens.size48) + .padding( + horizontal = TangemTheme.dimens.spacing16, + vertical = TangemTheme.dimens.spacing12, + ), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = startText, + color = startTextColor, + maxLines = 1, + style = startTextStyle, + ) + + Text( + text = endText, + color = endTextColor, + maxLines = 1, + style = endTextStyle, + ) + } + } +} + +@Preview(widthDp = 360, showBackground = true) +@Composable +private fun Preview_AwardItem_Light() { + TangemTheme { + AwardText( + startText = "startText", + startTextColor = TangemTheme.colors.text.tertiary, + startTextStyle = TangemTheme.typography.subtitle2, + endText = "endText", + endTextColor = TangemTheme.colors.text.primary1, + endTextStyle = TangemTheme.typography.subtitle2, + cornersToRound = CornersToRound.TOP_2, + ) + } +} \ No newline at end of file diff --git a/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/CornersToRound.kt b/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/CornersToRound.kt new file mode 100644 index 0000000000..b5070cdb28 --- /dev/null +++ b/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/CornersToRound.kt @@ -0,0 +1,27 @@ +package com.tangem.feature.referral.ui + +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.runtime.Composable +import androidx.compose.ui.unit.dp +import com.tangem.core.ui.res.TangemTheme + +internal enum class CornersToRound { + + ALL_4, + TOP_2, + BOTTOM_2, + ZERO, + ; + + @Suppress("TopLevelComposableFunctions") + @Composable + fun getShape(): RoundedCornerShape { + val radius = TangemTheme.dimens.radius12 + return when (this) { + ALL_4 -> RoundedCornerShape(radius) + TOP_2 -> RoundedCornerShape(topStart = radius, topEnd = radius) + BOTTOM_2 -> RoundedCornerShape(bottomStart = radius, bottomEnd = radius) + ZERO -> RoundedCornerShape(0.dp) + } + } +} \ No newline at end of file diff --git a/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/ParticipateBottomBlock.kt b/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/ParticipateBottomBlock.kt index 169fb81271..911aa890dd 100644 --- a/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/ParticipateBottomBlock.kt +++ b/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/ParticipateBottomBlock.kt @@ -2,15 +2,19 @@ package com.tangem.feature.referral.ui import android.content.Context import android.content.Intent +import androidx.compose.animation.* import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.Divider +import androidx.compose.material.Icon +import androidx.compose.material.Surface import androidx.compose.material.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier @@ -19,14 +23,16 @@ import androidx.compose.ui.hapticfeedback.HapticFeedbackType import androidx.compose.ui.platform.LocalClipboardManager import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalHapticFeedback +import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.pluralStringResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.tooling.preview.Preview import androidx.core.content.ContextCompat.startActivity -import com.tangem.core.ui.components.PrimaryStartIconButton -import com.tangem.core.ui.components.SmallInfoCard +import com.tangem.core.ui.components.* import com.tangem.core.ui.res.TangemTheme +import com.tangem.feature.referral.domain.models.ExpectedAward +import com.tangem.feature.referral.domain.models.ExpectedAwards import com.tangem.feature.referral.presentation.R @Suppress("LongParameterList") @@ -36,6 +42,7 @@ internal fun ParticipateBottomBlock( purchasedWalletCount: Int, code: String, shareLink: String, + expectedAwards: ExpectedAwards?, onAgreementClick: () -> Unit, onShowCopySnackbar: () -> Unit, onCopyClick: () -> Unit, @@ -50,14 +57,6 @@ internal fun ParticipateBottomBlock( .padding(horizontal = TangemTheme.dimens.spacing16), verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing16), ) { - SmallInfoCard( - startText = stringResource(id = R.string.referral_friends_bought_title), - endText = pluralStringResource( - id = R.plurals.referral_wallets_purchased_count, - count = purchasedWalletCount, - purchasedWalletCount, - ), - ) PersonalCodeCard(code = code) AdditionalButtons( code = code, @@ -66,10 +65,187 @@ internal fun ParticipateBottomBlock( onCopyClick = onCopyClick, onShareClick = onShareClick, ) + CounterAndAwards(purchasedWalletCount = purchasedWalletCount, expectedAwards = expectedAwards) AgreementText(firstPartResId = R.string.referral_tos_enroled_prefix, onClick = onAgreementClick) } } +@Composable +private fun CounterAndAwards(purchasedWalletCount: Int, expectedAwards: ExpectedAwards?) { + Column { + Counter(purchasedWalletCount, expectedAwards) + + if (expectedAwards != null) { + Awards(expectedAwards) + } else if (purchasedWalletCount != 0) { + EmptyUpcomingPayments() + } + } +} + +@Composable +private fun Counter(purchasedWalletCount: Int, expectedAwards: ExpectedAwards?) { + val isExpectedAwardsPresent = expectedAwards != null + + AwardText( + startText = stringResource(id = R.string.referral_friends_bought_title), + startTextColor = TangemTheme.colors.text.tertiary, + startTextStyle = TangemTheme.typography.subtitle2, + endText = pluralStringResource( + id = R.plurals.referral_wallets_purchased_count, + count = purchasedWalletCount, + purchasedWalletCount, + ), + endTextColor = TangemTheme.colors.text.primary1, + endTextStyle = TangemTheme.typography.body2, + cornersToRound = if (isExpectedAwardsPresent || purchasedWalletCount != 0) { + CornersToRound.TOP_2 + } else { + CornersToRound.ALL_4 + }, + ) +} + +@Suppress("MagicNumber") +@Composable +private fun Awards(expectedAwards: ExpectedAwards) { + val elementsCountToShowInLessMode = 3 + val isExpanded = remember { mutableStateOf(false) } + + Divider( + color = TangemTheme.colors.stroke.primary, + thickness = TangemTheme.dimens.size0_5, + ) + AwardText( + startText = stringResource(id = R.string.referral_expected_awards), + startTextColor = TangemTheme.colors.text.tertiary, + startTextStyle = TangemTheme.typography.subtitle2, + endText = pluralStringResource( + id = R.plurals.referral_number_of_wallets, + count = expectedAwards.numberOfWallets, + expectedAwards.numberOfWallets, + ), + endTextColor = TangemTheme.colors.text.tertiary, + endTextStyle = TangemTheme.typography.body2, + cornersToRound = CornersToRound.ZERO, + ) + + val initialItems = expectedAwards.expectedAwards.take(elementsCountToShowInLessMode) + val extraItems = expectedAwards.expectedAwards.drop(elementsCountToShowInLessMode) + + initialItems.forEachIndexed { index, expectedAward -> + AwardText( + startText = expectedAward.paymentDate, + startTextColor = TangemTheme.colors.text.primary1, + startTextStyle = TangemTheme.typography.subtitle2, + endText = expectedAward.amount, + endTextColor = TangemTheme.colors.text.primary1, + endTextStyle = TangemTheme.typography.subtitle2, + cornersToRound = if (index == initialItems.size - 1 && extraItems.isEmpty()) { + CornersToRound.BOTTOM_2 + } else { + CornersToRound.ZERO + }, + ) + } + + AnimatedVisibility( + visible = isExpanded.value, + enter = fadeIn() + expandVertically(), + exit = shrinkVertically() + fadeOut(), + ) { + ExtraItems(extraItems = extraItems) + } + + if (expectedAwards.expectedAwards.size > elementsCountToShowInLessMode) { + LessMoreButton(isExpanded = isExpanded) + } +} + +@Composable +private fun EmptyUpcomingPayments() { + Divider( + color = TangemTheme.colors.stroke.primary, + thickness = TangemTheme.dimens.size0_5, + ) + AwardText( + startText = stringResource(id = R.string.referral_expected_awards), + startTextColor = TangemTheme.colors.text.tertiary, + startTextStyle = TangemTheme.typography.subtitle2, + endText = "", + endTextColor = TangemTheme.colors.text.tertiary, + endTextStyle = TangemTheme.typography.body2, + cornersToRound = CornersToRound.BOTTOM_2, + ) +} + +@Composable +private fun LessMoreButton(isExpanded: MutableState) { + Surface( + shape = RoundedCornerShape( + bottomStart = TangemTheme.dimens.radius12, + bottomEnd = TangemTheme.dimens.radius12, + ), + ) { + Column( + modifier = Modifier.background(TangemTheme.colors.background.primary), + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .heightIn(TangemTheme.dimens.size48) + .clickable { isExpanded.value = !isExpanded.value } + .padding( + horizontal = TangemTheme.dimens.spacing16, + vertical = TangemTheme.dimens.spacing12, + ), + horizontalArrangement = Arrangement.Start, + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = if (isExpanded.value) { + stringResource(id = R.string.referral_less) + } else { + stringResource(id = R.string.referral_more) + }, + color = TangemTheme.colors.text.tertiary, + style = TangemTheme.typography.subtitle2, + ) + + val chevronIcon = if (isExpanded.value) { + painterResource(id = com.tangem.core.ui.R.drawable.ic_chevron_up_24) + } else { + painterResource(id = com.tangem.core.ui.R.drawable.ic_chevron_24) + } + Icon( + modifier = Modifier.size(TangemTheme.dimens.size20), + painter = chevronIcon, + tint = TangemTheme.colors.text.tertiary, + contentDescription = null, + ) + } + } + } +} + +@Composable +private fun ExtraItems(extraItems: List) { + Column { + extraItems.forEach { expectedAward -> + AwardText( + startText = expectedAward.paymentDate, + startTextColor = TangemTheme.colors.text.primary1, + startTextStyle = TangemTheme.typography.subtitle2, + endText = expectedAward.amount, + endTextColor = TangemTheme.colors.text.primary1, + endTextStyle = TangemTheme.typography.subtitle2, + cornersToRound = CornersToRound.ZERO, + + ) + } + } +} + @Composable private fun PersonalCodeCard(code: String) { Column( @@ -157,11 +333,28 @@ private fun Context.shareText(text: String) { @Composable private fun Preview_ParticipateBottomBlock_InLightTheme() { TangemTheme(isDark = false) { - Column(Modifier.background(TangemTheme.colors.background.primary)) { + Column(Modifier.background(TangemTheme.colors.background.secondary)) { ParticipateBottomBlock( purchasedWalletCount = 3, code = "x4JdK", shareLink = "", + expectedAwards = ExpectedAwards( + numberOfWallets = 3, + expectedAwards = listOf( + ExpectedAward( + amount = "10 USDT", + paymentDate = "Today", + ), + ExpectedAward( + amount = "20 USDT", + paymentDate = "6 Aug 2023", + ), + ExpectedAward( + amount = "30 USDT", + paymentDate = "10 Aug 2023", + ), + ), + ), onAgreementClick = {}, onShowCopySnackbar = {}, onCopyClick = {}, @@ -173,13 +366,64 @@ private fun Preview_ParticipateBottomBlock_InLightTheme() { @Preview(widthDp = 360, showBackground = true) @Composable -private fun Preview_ParticipateBottomBlock_InDarkTheme() { - TangemTheme(isDark = true) { - Column(Modifier.background(TangemTheme.colors.background.primary)) { +private fun Preview_ParticipateBottomBlock_Without_Awards_InLightTheme() { + TangemTheme(isDark = false) { + Column(Modifier.background(TangemTheme.colors.background.secondary)) { ParticipateBottomBlock( purchasedWalletCount = 3, code = "x4JdK", shareLink = "", + expectedAwards = null, + onAgreementClick = {}, + onShowCopySnackbar = {}, + onCopyClick = {}, + onShareClick = {}, + ) + } + } +} + +@Preview(widthDp = 360, showBackground = true) +@Composable +private fun Preview_ParticipateBottomBlock_Without_Awards_And_Purchased_Wallets_InLightTheme() { + TangemTheme(isDark = false) { + Column(Modifier.background(TangemTheme.colors.background.secondary)) { + ParticipateBottomBlock( + purchasedWalletCount = 0, + code = "x4JdK", + shareLink = "", + expectedAwards = null, + onAgreementClick = {}, + onShowCopySnackbar = {}, + onCopyClick = {}, + onShareClick = {}, + ) + } + } +} + +@Preview(widthDp = 360, showBackground = true) +@Composable +private fun LessMoreButton_White() { + TangemTheme(isDark = false) { + LessMoreButton( + isExpanded = remember { + mutableStateOf(false) + }, + ) + } +} + +@Preview(widthDp = 360, showBackground = true) +@Composable +private fun Preview_ParticipateBottomBlock_Without_Awards_InDarkTheme() { + TangemTheme(isDark = true) { + Column(Modifier.background(TangemTheme.colors.background.secondary)) { + ParticipateBottomBlock( + purchasedWalletCount = 3, + code = "x4JdK", + shareLink = "", + expectedAwards = null, onAgreementClick = {}, onShowCopySnackbar = {}, onCopyClick = {}, diff --git a/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/ReferralScreen.kt b/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/ReferralScreen.kt index 7c3e8b2ddf..6dcec255cf 100644 --- a/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/ReferralScreen.kt +++ b/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/ReferralScreen.kt @@ -19,6 +19,7 @@ import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.style.TextAlign @@ -31,6 +32,8 @@ import com.tangem.core.ui.components.SpacerH32 import com.tangem.core.ui.components.appbar.AppBarWithBackButton import com.tangem.core.ui.res.TangemColorPalette import com.tangem.core.ui.res.TangemTheme +import com.tangem.feature.referral.domain.models.ExpectedAward +import com.tangem.feature.referral.domain.models.ExpectedAwards import com.tangem.feature.referral.models.DemoModeException import com.tangem.feature.referral.models.ReferralStateHolder import com.tangem.feature.referral.models.ReferralStateHolder.* @@ -159,6 +162,7 @@ private fun ReferralInfo( purchasedWalletCount = state.purchasedWalletCount, code = state.code, shareLink = state.shareLink, + expectedAwards = state.expectedAwards, onAgreementClick = onAgreementClick, onShowCopySnackbar = onShowCopySnackbar, onCopyClick = stateHolder.analytics.onCopyClicked, @@ -253,25 +257,47 @@ private fun Condition(@DrawableRes iconResId: Int, infoBlock: @Composable () -> private fun InfoForYou(award: String, networkName: String, address: String? = null) { ConditionInfo(title = stringResource(id = R.string.referral_point_currencies_title)) { Text( - text = buildAnnotatedString { - append(stringResource(id = R.string.referral_point_currencies_description_prefix)) - withStyle(SpanStyle(color = TangemTheme.colors.text.primary1)) { - append(" $award ") - } - append( - String.format( - stringResource(id = R.string.referral_point_currencies_description_suffix), - networkName, - if (!address.isNullOrBlank()) " $address" else "", - ), - ) - }, + formatAwardConditionsString( + quantity = award, + network = networkName, + address = if (!address.isNullOrBlank()) " $address" else "", + ), color = TangemTheme.colors.text.tertiary, style = TangemTheme.typography.body2, ) } } +@Composable +private fun formatAwardConditionsString(quantity: String, network: String, address: String): AnnotatedString { + val rawString = stringResource(R.string.referral_point_currencies_description, quantity, network, address) + + val pattern = Regex("\\^\\^(.*?)\\^\\^") + var startIndex = 0 + val annotatedString = buildAnnotatedString { + pattern.findAll(rawString).forEach { matchResult -> + val index = matchResult.range.first + val matchedValue = matchResult.groups[1]?.value ?: "" + + // appends unformatted part + append(rawString.substring(startIndex, index)) + + // applies style on ^^-wrapped parts + withStyle(SpanStyle(color = TangemTheme.colors.text.primary1)) { + append(matchedValue) + } + + // goes to next part + startIndex = matchResult.range.last + 1 + } + + // appends remaining ending if exists + append(rawString.substring(startIndex)) + } + + return annotatedString +} + @Composable private fun InfoForYourFriend(discount: String) { ConditionInfo(title = stringResource(id = R.string.referral_point_discount_title)) { @@ -464,6 +490,7 @@ private fun Preview_ReferralScreen_Participant_InLightTheme() { code = "x4JdK", shareLink = "", url = "", + expectedAwards = null, ), errorSnackbar = null, analytics = Analytics( @@ -492,6 +519,52 @@ private fun Preview_ReferralScreen_Participant_InDarkTheme() { code = "x4JdK", shareLink = "", url = "", + expectedAwards = null, + ), + errorSnackbar = null, + analytics = Analytics( + onAgreementClicked = {}, + onCopyClicked = {}, + onShareClicked = {}, + ), + ), + ) + } +} + +@Preview(widthDp = 360, showBackground = true) +@Composable +private fun Preview_ReferralScreen_Participant_With_Referrals_InLightTheme() { + TangemTheme(isDark = false) { + ReferralScreen( + stateHolder = ReferralStateHolder( + headerState = HeaderState(onBackClicked = {}), + referralInfoState = ReferralInfoState.ParticipantContent( + award = "10 USDT", + networkName = "Tron", + address = "ma80...zk8q2", + discount = "10%", + purchasedWalletCount = 3, + code = "x4JdK", + shareLink = "", + url = "", + expectedAwards = ExpectedAwards( + numberOfWallets = 5, + expectedAwards = listOf( + ExpectedAward( + amount = "10 USDT", + paymentDate = "Today", + ), + ExpectedAward( + amount = "20 USDT", + paymentDate = "6 Aug 2023", + ), + ExpectedAward( + amount = "30 USDT", + paymentDate = "10 Aug 2023", + ), + ), + ), ), errorSnackbar = null, analytics = Analytics( diff --git a/features/referral/presentation/src/main/java/com/tangem/feature/referral/viewmodels/ReferralViewModel.kt b/features/referral/presentation/src/main/java/com/tangem/feature/referral/viewmodels/ReferralViewModel.kt index b2f802e181..82c0d08bb8 100644 --- a/features/referral/presentation/src/main/java/com/tangem/feature/referral/viewmodels/ReferralViewModel.kt +++ b/features/referral/presentation/src/main/java/com/tangem/feature/referral/viewmodels/ReferralViewModel.kt @@ -36,7 +36,7 @@ internal class ReferralViewModel @Inject constructor( private var referralRouter: ReferralRouter by Delegates.notNull() - private val lastReferralData = mutableStateOf(null) + private var lastReferralData: ReferralData? = null init { loadReferralData() @@ -67,7 +67,7 @@ internal class ReferralViewModel @Inject constructor( viewModelScope.launch(dispatchers.main) { runCatching(dispatchers.io) { referralInteractor.getReferralStatus().apply { - lastReferralData.value = this + lastReferralData = this } } .onSuccess(::showContent) @@ -84,14 +84,13 @@ internal class ReferralViewModel @Inject constructor( viewModelScope.launch(dispatchers.main) { runCatching(dispatchers.io) { referralInteractor.startReferral() } .onSuccess(::showContent) - .onFailure { - if (it is UserCancelledException) { - val lastRefData = lastReferralData.value - if (lastRefData != null) { - showContent(lastRefData) + .onFailure { throwable -> + if (throwable is UserCancelledException) { + lastReferralData?.let { referralData -> + showContent(referralData) } } else { - showErrorSnackbar(it) + showErrorSnackbar(throwable) } } } @@ -130,6 +129,7 @@ internal class ReferralViewModel @Inject constructor( code = referral.promocode, shareLink = referral.shareLink, url = tosLink, + expectedAwards = expectedAwards, ) is ReferralData.NonParticipantData -> ReferralInfoState.NonParticipantContent( award = getAwardValue(), From 9cac096ce2876a8141ffdd64c487b6ab86db8ab4 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 5 Sep 2023 11:26:59 +0500 Subject: [PATCH 018/242] Updated on 2026-08-14 --- data/tokens/build.gradle.kts | 1 + .../data/tokens/utils/NetworkStatusFactory.kt | 47 +------ .../model/CryptoCurrencyTransaction.kt | 21 +-- .../utils/UpdateWalletManagerResultFactory.kt | 130 +++++++++++------- domain/tokens/build.gradle.kts | 1 + .../tokens/model/CryptoCurrencyStatus.kt | 9 +- .../domain/tokens/model/NetworkStatus.kt | 3 +- .../tokendetails/TokenDetailsPreviewData.kt | 1 + .../tokendetails/state/TokenDetailsState.kt | 3 + .../TokenDetailsLoadedBalanceConverter.kt | 9 ++ .../TokenDetailsSkeletonStateConverter.kt | 1 + .../state/factory/TokenDetailsStateFactory.kt | 2 + ...ilsTxHistoryToTransactionStateConverter.kt | 94 +++++++++++++ .../tokendetails/ui/TokenDetailsScreen.kt | 30 ++++ 14 files changed, 239 insertions(+), 113 deletions(-) create mode 100644 features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/txhistory/TokenDetailsTxHistoryToTransactionStateConverter.kt diff --git a/data/tokens/build.gradle.kts b/data/tokens/build.gradle.kts index a45be27508..37aab60a23 100644 --- a/data/tokens/build.gradle.kts +++ b/data/tokens/build.gradle.kts @@ -17,6 +17,7 @@ dependencies { implementation(projects.domain.models) implementation(projects.domain.tokens) implementation(projects.domain.tokens.models) + implementation(projects.domain.txhistory.models) implementation(projects.domain.wallets.models) /** Project - Data */ diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/NetworkStatusFactory.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/NetworkStatusFactory.kt index 0127ed7330..faed2bda5c 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/NetworkStatusFactory.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/NetworkStatusFactory.kt @@ -2,9 +2,9 @@ package com.tangem.data.tokens.utils import com.tangem.domain.tokens.model.NetworkAddress import com.tangem.domain.tokens.model.NetworkStatus -import com.tangem.domain.tokens.model.PendingTransaction import com.tangem.domain.tokens.models.CryptoCurrency import com.tangem.domain.tokens.models.Network +import com.tangem.domain.txhistory.models.TxHistoryItem import com.tangem.domain.walletmanager.model.CryptoCurrencyAmount import com.tangem.domain.walletmanager.model.CryptoCurrencyTransaction import com.tangem.domain.walletmanager.model.UpdateWalletManagerResult @@ -31,7 +31,6 @@ internal class NetworkStatusFactory { address = getNetworkAddress(result.defaultAddress, result.addresses), amounts = formatAmounts(result.currenciesAmounts, currencies), pendingTransactions = formatTransactions( - networksAddresses = result.addresses, transactions = result.currentTransactions, currencies = currencies, ), @@ -67,10 +66,9 @@ internal class NetworkStatusFactory { } private fun formatTransactions( - networksAddresses: Set, transactions: Set, currencies: Set, - ): Map> { + ): Map> { if (transactions.isEmpty()) return emptyMap() return currencies @@ -87,48 +85,13 @@ internal class NetworkStatusFactory { } } - currency.id to createCurrentTransactions(networksAddresses, currencyTransactions) + currency.id to createCurrentTransactions(currencyTransactions) } .toMap() } - private fun createCurrentTransactions( - networksAddresses: Set, - transactions: Set, - ): Set { - return transactions.mapNotNullTo(hashSetOf()) { createCurrentTransaction(networksAddresses, it) } - } - - private fun createCurrentTransaction( - networksAddresses: Set, - transaction: CryptoCurrencyTransaction, - ): PendingTransaction? { - val direction = when { - transaction.toAddress in networksAddresses -> PendingTransaction.Direction.Incoming( - fromAddress = transaction.fromAddress, - ) - transaction.fromAddress in networksAddresses -> PendingTransaction.Direction.Outgoing( - toAddress = transaction.toAddress, - ) - else -> { - Timber.e( - """ - Unable to find transaction direction - |- To address: ${transaction.toAddress} - |- From address: ${transaction.fromAddress} - |- Network addresses: $networksAddresses - """.trimIndent(), - ) - - return null - } - } - - return PendingTransaction( - amount = transaction.amount, - direction = direction, - sentAt = transaction.sentAt, - ) + private fun createCurrentTransactions(transactions: Set): Set { + return transactions.mapTo(hashSetOf()) { it.txHistoryItem } } private fun getNetworkAddress(defaultAddress: String, availableAddresses: Set): NetworkAddress { diff --git a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/model/CryptoCurrencyTransaction.kt b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/model/CryptoCurrencyTransaction.kt index c0b69b941f..94c3bba81b 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/model/CryptoCurrencyTransaction.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/model/CryptoCurrencyTransaction.kt @@ -1,28 +1,17 @@ package com.tangem.domain.walletmanager.model -import org.joda.time.DateTime -import java.math.BigDecimal +import com.tangem.domain.txhistory.models.TxHistoryItem +// TODO: [REDACTED_JIRA] move to txhistory module sealed class CryptoCurrencyTransaction { - abstract val amount: BigDecimal - abstract val fromAddress: String? - abstract val toAddress: String? - abstract val sentAt: DateTime + abstract val txHistoryItem: TxHistoryItem - data class Coin( - override val amount: BigDecimal, - override val fromAddress: String?, - override val toAddress: String?, - override val sentAt: DateTime, - ) : CryptoCurrencyTransaction() + data class Coin(override val txHistoryItem: TxHistoryItem) : CryptoCurrencyTransaction() data class Token( val tokenId: String?, val tokenContractAddress: String, - override val amount: BigDecimal, - override val fromAddress: String?, - override val toAddress: String?, - override val sentAt: DateTime, + override val txHistoryItem: TxHistoryItem, ) : CryptoCurrencyTransaction() } \ No newline at end of file diff --git a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/utils/UpdateWalletManagerResultFactory.kt b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/utils/UpdateWalletManagerResultFactory.kt index 4d644e4b7c..effa52cfdb 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/utils/UpdateWalletManagerResultFactory.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/utils/UpdateWalletManagerResultFactory.kt @@ -3,37 +3,37 @@ package com.tangem.domain.walletmanager.utils import com.tangem.blockchain.common.* import com.tangem.blockchain.common.address.Address import com.tangem.domain.common.extensions.amountToCreateAccount +import com.tangem.domain.txhistory.models.TxHistoryItem import com.tangem.domain.walletmanager.model.CryptoCurrencyAmount import com.tangem.domain.walletmanager.model.CryptoCurrencyTransaction import com.tangem.domain.walletmanager.model.UpdateWalletManagerResult -import org.joda.time.DateTime -import org.joda.time.DateTimeZone -import org.joda.time.Instant import timber.log.Timber import java.math.BigDecimal -import java.util.Calendar +import java.util.concurrent.TimeUnit internal class UpdateWalletManagerResultFactory { fun getResult(walletManager: WalletManager): UpdateWalletManagerResult.Verified { val wallet = walletManager.wallet + val addresses = getAvailableAddresses(wallet.addresses) return UpdateWalletManagerResult.Verified( defaultAddress = wallet.address, - addresses = getAvailableAddresses(wallet.addresses), + addresses = addresses, currenciesAmounts = getTokensAmounts(wallet.amounts.values.toSet()), - currentTransactions = getCurrentTransactions(wallet.recentTransactions.toSet()), + currentTransactions = getCurrentTransactions(addresses, wallet.recentTransactions.toSet()), ) } fun getDemoResult(walletManager: WalletManager, demoAmount: Amount): UpdateWalletManagerResult.Verified { val wallet = walletManager.wallet + val addresses = getAvailableAddresses(wallet.addresses) return UpdateWalletManagerResult.Verified( defaultAddress = wallet.address, - addresses = getAvailableAddresses(wallet.addresses), + addresses = addresses, currenciesAmounts = getDemoTokensAmounts(demoAmount, walletManager.cardTokens), - currentTransactions = getCurrentTransactions(wallet.recentTransactions.toSet()), + currentTransactions = getCurrentTransactions(addresses, wallet.recentTransactions.toSet()), ) } @@ -70,12 +70,15 @@ internal class UpdateWalletManagerResultFactory { } } - private fun getCurrentTransactions(recentTransactions: Set): Set { + private fun getCurrentTransactions( + walletAddresses: Set, + recentTransactions: Set, + ): Set { val unconfirmedTransactions = recentTransactions.filter { it.status == TransactionStatus.Unconfirmed } - return unconfirmedTransactions.mapNotNullTo(hashSetOf(), ::createCurrencyTransaction) + return unconfirmedTransactions.mapNotNullTo(hashSetOf()) { createCurrencyTransaction(walletAddresses, it) } } private fun createCurrencyAmount(amount: Amount): CryptoCurrencyAmount? { @@ -92,31 +95,78 @@ internal class UpdateWalletManagerResultFactory { } } - private fun createCurrencyTransaction(data: TransactionData): CryptoCurrencyTransaction? { - val fromAddress = takeAddressIfNotUnknown(data.sourceAddress) - val toAddress = takeAddressIfNotUnknown(data.destinationAddress) - val amount = getTransactionAmountValue(data.amount) ?: return null - val sentAt = getTransactionSentTime(data.date) ?: return null - + private fun createCurrencyTransaction( + walletAddresses: Set, + data: TransactionData, + ): CryptoCurrencyTransaction? { return when (val type = data.amount.type) { - is AmountType.Coin -> CryptoCurrencyTransaction.Coin( - amount = amount, - fromAddress = fromAddress, - toAddress = toAddress, - sentAt = sentAt, - ) - is AmountType.Token -> CryptoCurrencyTransaction.Token( - tokenId = type.token.id, - tokenContractAddress = type.token.contractAddress, - amount = amount, - fromAddress = fromAddress, - toAddress = toAddress, - sentAt = sentAt, - ) + is AmountType.Coin -> { + val txHistoryItem = createTxHistoryItem(walletAddresses, data) ?: return null + CryptoCurrencyTransaction.Coin(txHistoryItem) + } + is AmountType.Token -> { + val txHistoryItem = createTxHistoryItem(walletAddresses, data) ?: return null + CryptoCurrencyTransaction.Token( + tokenId = type.token.id, + tokenContractAddress = type.token.contractAddress, + txHistoryItem = txHistoryItem, + ) + } is AmountType.Reserve -> null } } + private fun createTxHistoryItem(walletAddresses: Set, data: TransactionData): TxHistoryItem? { + val direction = extractDirection(walletAddresses, data) ?: run { + Timber.w("Can not determine address for $data") + return null + } + val hash = data.hash ?: return null + val millis = data.date?.timeInMillis ?: return null + val amount = getTransactionAmountValue(data.amount) ?: return null + + return TxHistoryItem( + txHash = hash, + timestampInMillis = TimeUnit.SECONDS.toMillis(millis), + direction = direction, + status = when (data.status) { + TransactionStatus.Confirmed -> TxHistoryItem.TxStatus.Confirmed + TransactionStatus.Unconfirmed -> TxHistoryItem.TxStatus.Unconfirmed + }, + type = TxHistoryItem.TransactionType.Transfer, + amount = amount, + ) + } + + private fun extractDirection( + walletAddresses: Set, + data: TransactionData, + ): TxHistoryItem.TransactionDirection? { + val fromAddress = data.sourceAddress + val toAddress = data.destinationAddress + + return when { + toAddress in walletAddresses -> { + TxHistoryItem.TransactionDirection.Incoming(TxHistoryItem.Address.Single(fromAddress)) + } + fromAddress in walletAddresses -> { + TxHistoryItem.TransactionDirection.Outgoing(TxHistoryItem.Address.Single(toAddress)) + } + else -> { + Timber.e( + """ + Unable to find transaction direction + |- To address: ${data.destinationAddress} + |- From address: ${data.sourceAddress} + |- Network addresses: $walletAddresses + """.trimIndent(), + ) + + return null + } + } + } + private fun getAvailableAddresses(addresses: Set
): Set { return addresses.mapTo(hashSetOf()) { it.value } } @@ -140,24 +190,4 @@ internal class UpdateWalletManagerResultFactory { return value } - - private fun getTransactionSentTime(date: Calendar?): DateTime? { - if (date == null) { - Timber.e("Transaction date must not be null") - return null - } - - val instant = Instant.ofEpochMilli(date.timeInMillis) - val timeZone = DateTimeZone.forTimeZone(date.timeZone) - - return instant.toDateTime(timeZone) - } - - private fun takeAddressIfNotUnknown(address: String): String? { - return address.takeIf { it.isNotBlank() && it != UNKNOWN_TRANSACTION_ADDRESS } - } - - private companion object { - const val UNKNOWN_TRANSACTION_ADDRESS = "unknown" - } } \ No newline at end of file diff --git a/domain/tokens/build.gradle.kts b/domain/tokens/build.gradle.kts index 17599c040d..c146658684 100644 --- a/domain/tokens/build.gradle.kts +++ b/domain/tokens/build.gradle.kts @@ -15,6 +15,7 @@ dependencies { implementation(projects.domain.models) implementation(projects.domain.legacy) implementation(projects.domain.tokens.models) + implementation(projects.domain.txhistory.models) implementation(projects.domain.wallets.models) implementation(projects.domain.appCurrency.models) diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/CryptoCurrencyStatus.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/CryptoCurrencyStatus.kt index b71f6496b5..1ccd0e1e61 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/CryptoCurrencyStatus.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/CryptoCurrencyStatus.kt @@ -1,6 +1,7 @@ package com.tangem.domain.tokens.model import com.tangem.domain.tokens.models.CryptoCurrency +import com.tangem.domain.txhistory.models.TxHistoryItem import java.math.BigDecimal /** @@ -39,7 +40,7 @@ data class CryptoCurrencyStatus( open val hasCurrentNetworkTransactions: Boolean = false /** The pending cryptocurrency transactions. */ - open val pendingTransactions: Set = emptySet() + open val pendingTransactions: Set = emptySet() /** The network address */ open val networkAddress: NetworkAddress? = null @@ -74,7 +75,7 @@ data class CryptoCurrencyStatus( override val fiatRate: BigDecimal, override val priceChange: BigDecimal, override val hasCurrentNetworkTransactions: Boolean, - override val pendingTransactions: Set, + override val pendingTransactions: Set, override val networkAddress: NetworkAddress?, ) : Status() @@ -95,7 +96,7 @@ data class CryptoCurrencyStatus( override val fiatRate: BigDecimal?, override val priceChange: BigDecimal?, override val hasCurrentNetworkTransactions: Boolean, - override val pendingTransactions: Set, + override val pendingTransactions: Set, override val networkAddress: NetworkAddress?, ) : Status() @@ -110,7 +111,7 @@ data class CryptoCurrencyStatus( data class NoQuote( override val amount: BigDecimal, override val hasCurrentNetworkTransactions: Boolean, - override val pendingTransactions: Set, + override val pendingTransactions: Set, override val networkAddress: NetworkAddress?, ) : Status() } \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/NetworkStatus.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/NetworkStatus.kt index e9d5ec7405..87be054645 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/NetworkStatus.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/NetworkStatus.kt @@ -2,6 +2,7 @@ package com.tangem.domain.tokens.model import com.tangem.domain.tokens.models.CryptoCurrency import com.tangem.domain.tokens.models.Network +import com.tangem.domain.txhistory.models.TxHistoryItem import java.math.BigDecimal /** @@ -44,7 +45,7 @@ data class NetworkStatus( data class Verified( val address: NetworkAddress, val amounts: Map, - val pendingTransactions: Map>, + val pendingTransactions: Map>, ) : Status() /** diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/TokenDetailsPreviewData.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/TokenDetailsPreviewData.kt index 1d46a74430..360e77ad20 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/TokenDetailsPreviewData.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/TokenDetailsPreviewData.kt @@ -82,5 +82,6 @@ internal object TokenDetailsPreviewData { ), ), dialogConfig = null, + pendingTxs = persistentListOf(), ) } \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsState.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsState.kt index c6e26d31c5..7aece5e270 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsState.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsState.kt @@ -1,14 +1,17 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.state import com.tangem.core.ui.components.marketprice.MarketPriceBlockState +import com.tangem.core.ui.components.transactions.state.TransactionState import com.tangem.core.ui.components.transactions.state.TxHistoryState import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.TokenDetailsDialogConfig +import kotlinx.collections.immutable.PersistentList internal data class TokenDetailsState( val topAppBarConfig: TokenDetailsTopAppBarConfig, val tokenInfoBlockState: TokenInfoBlockState, val tokenBalanceBlockState: TokenDetailsBalanceBlockState, val marketPriceBlockState: MarketPriceBlockState, + val pendingTxs: PersistentList, val txHistoryState: TxHistoryState, val dialogConfig: TokenDetailsDialogConfig?, ) \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsLoadedBalanceConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsLoadedBalanceConverter.kt index 19c2d0c8b4..1d09bb6fae 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsLoadedBalanceConverter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsLoadedBalanceConverter.kt @@ -10,14 +10,22 @@ import com.tangem.domain.tokens.error.CurrencyStatusError import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsBalanceBlockState import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState +import com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.txhistory.TokenDetailsTxHistoryToTransactionStateConverter import com.tangem.utils.converter.Converter +import kotlinx.collections.immutable.toPersistentList import java.math.BigDecimal internal class TokenDetailsLoadedBalanceConverter( private val currentStateProvider: Provider, private val appCurrencyProvider: Provider, + private val symbol: String, + private val decimals: Int, ) : Converter, TokenDetailsState> { + private val txHistoryItemConverter by lazy { + TokenDetailsTxHistoryToTransactionStateConverter(symbol, decimals) + } + override fun convert(value: Either): TokenDetailsState { return value.fold(ifLeft = { convertError() }, ifRight = ::convert) } @@ -33,6 +41,7 @@ internal class TokenDetailsLoadedBalanceConverter( return state.copy( tokenBalanceBlockState = getBalanceState(state.tokenBalanceBlockState, status), marketPriceBlockState = getMarketPriceState(status = status.value, currencyName = currencyName), + pendingTxs = status.value.pendingTransactions.map(txHistoryItemConverter::convert).toPersistentList(), ) } diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSkeletonStateConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSkeletonStateConverter.kt index 29e967a070..9d666add03 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSkeletonStateConverter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSkeletonStateConverter.kt @@ -42,6 +42,7 @@ internal class TokenDetailsSkeletonStateConverter( actionButtons = createButtons(), ), marketPriceBlockState = MarketPriceBlockState.Loading(value.cryptoCurrency.name), + pendingTxs = persistentListOf(), txHistoryState = TxHistoryState.Content( contentItems = MutableStateFlow( value = TxHistoryState.getDefaultLoadingTransactions(clickIntents::onExploreClick), diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt index 6db391a63d..eb3ee645d1 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt @@ -34,6 +34,8 @@ internal class TokenDetailsStateFactory( TokenDetailsLoadedBalanceConverter( currentStateProvider = currentStateProvider, appCurrencyProvider = appCurrencyProvider, + symbol = symbol, + decimals = decimals, ) } diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/txhistory/TokenDetailsTxHistoryToTransactionStateConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/txhistory/TokenDetailsTxHistoryToTransactionStateConverter.kt new file mode 100644 index 0000000000..e0e714eea4 --- /dev/null +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/txhistory/TokenDetailsTxHistoryToTransactionStateConverter.kt @@ -0,0 +1,94 @@ +package com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.txhistory + +import com.tangem.core.ui.components.transactions.state.TransactionState +import com.tangem.core.ui.extensions.TextReference +import com.tangem.domain.txhistory.models.TxHistoryItem +import com.tangem.features.tokendetails.impl.R +import com.tangem.utils.converter.Converter +import com.tangem.utils.toBriefAddressFormat +import com.tangem.utils.toFormattedCurrencyString +import org.joda.time.DateTime +import org.joda.time.DateTimeZone +import org.joda.time.format.DateTimeFormatterBuilder +import java.math.BigDecimal +import java.util.Locale + +internal class TokenDetailsTxHistoryToTransactionStateConverter( + private val symbol: String, + private val decimals: Int, +) : Converter { + + /** Example, 13:35 */ + private val timeFormatter by lazy { + DateTimeFormatterBuilder() + .appendHourOfDay(1) + .appendLiteral(':') + .appendMinuteOfHour(2) + .toFormatter() + .withLocale(Locale.getDefault()) + } + + override fun convert(value: TxHistoryItem): TransactionState { + return when (value.type) { + TxHistoryItem.TransactionType.Transfer -> { + when (val direction = value.direction) { + is TxHistoryItem.TransactionDirection.Incoming -> { + createIncomingTransferTransaction(value, direction) + } + is TxHistoryItem.TransactionDirection.Outgoing -> { + createOutgoingTransferTransaction(value, direction) + } + } + } + } + } + + private fun createIncomingTransferTransaction( + item: TxHistoryItem, + direction: TxHistoryItem.TransactionDirection.Incoming, + ): TransactionState { + return when (item.status) { + TxHistoryItem.TxStatus.Confirmed -> TransactionState.Receive( + txHash = item.txHash, + address = direction.extractAddress(), + amount = item.amount.toCryptoCurrencyFormat(), + timestamp = timeFormatter.print(DateTime(item.timestampInMillis, DateTimeZone.getDefault())), + ) + TxHistoryItem.TxStatus.Unconfirmed -> TransactionState.Receiving( + txHash = item.txHash, + address = direction.extractAddress(), + amount = item.amount.toCryptoCurrencyFormat(), + timestamp = timeFormatter.print(DateTime(item.timestampInMillis, DateTimeZone.getDefault())), + ) + } + } + + private fun createOutgoingTransferTransaction( + item: TxHistoryItem, + direction: TxHistoryItem.TransactionDirection.Outgoing, + ): TransactionState { + return when (item.status) { + TxHistoryItem.TxStatus.Confirmed -> TransactionState.Send( + txHash = item.txHash, + address = direction.extractAddress(), + amount = item.amount.toCryptoCurrencyFormat(), + timestamp = timeFormatter.print(DateTime(item.timestampInMillis, DateTimeZone.getDefault())), + ) + TxHistoryItem.TxStatus.Unconfirmed -> TransactionState.Sending( + txHash = item.txHash, + address = direction.extractAddress(), + amount = item.amount.toCryptoCurrencyFormat(), + timestamp = timeFormatter.print(DateTime(item.timestampInMillis, DateTimeZone.getDefault())), + ) + } + } + + private fun BigDecimal.toCryptoCurrencyFormat(): String { + return toFormattedCurrencyString(currency = symbol, decimals = decimals) + } + + private fun TxHistoryItem.TransactionDirection.extractAddress(): TextReference = when (val addr = address) { + TxHistoryItem.Address.Multiple -> TextReference.Res(R.string.transaction_history_multiple_addresses) + is TxHistoryItem.Address.Single -> TextReference.Str(addr.rawAddress.toBriefAddressFormat()) + } +} \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/TokenDetailsScreen.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/TokenDetailsScreen.kt index 7ce9bb2274..9dbb266f47 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/TokenDetailsScreen.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/TokenDetailsScreen.kt @@ -1,15 +1,23 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.ui +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.material3.Scaffold import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.util.fastForEach import androidx.paging.compose.collectAsLazyPagingItems import com.tangem.core.ui.components.marketprice.MarketPriceBlock import com.tangem.core.ui.components.marketprice.MarketPriceBlockState +import com.tangem.core.ui.components.transactions.Transaction +import com.tangem.core.ui.components.transactions.state.TransactionState import com.tangem.core.ui.components.transactions.state.TxHistoryState import com.tangem.core.ui.components.transactions.txHistoryItems import com.tangem.core.ui.res.TangemTheme @@ -19,6 +27,7 @@ import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.T import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.TokenDetailsDialogs import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.TokenDetailsTopAppBar import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.TokenInfoBlock +import kotlinx.collections.immutable.PersistentList @Composable internal fun TokenDetailsScreen(state: TokenDetailsState) { @@ -55,6 +64,14 @@ internal fun TokenDetailsScreen(state: TokenDetailsState) { contentType = MarketPriceBlockState::class.java, content = { MarketPriceBlock(modifier = itemModifier, state = state.marketPriceBlockState) }, ) + if (state.txHistoryState is TxHistoryState.NotSupported && state.pendingTxs.isNotEmpty()) { + item { + PendingTxsBlock( + pendingTxs = state.pendingTxs, + modifier = itemModifier, + ) + } + } txHistoryItems(state = state.txHistoryState, txHistoryItems = txHistoryItems) } @@ -62,6 +79,19 @@ internal fun TokenDetailsScreen(state: TokenDetailsState) { } } +@Composable +private fun PendingTxsBlock(pendingTxs: PersistentList, modifier: Modifier = Modifier) { + Column( + modifier = modifier + .clip(shape = TangemTheme.shapes.roundedCornersXMedium) + .background(color = TangemTheme.colors.background.primary), + verticalArrangement = Arrangement.spacedBy(space = TangemTheme.dimens.spacing8), + horizontalAlignment = Alignment.Start, + ) { + pendingTxs.fastForEach { Transaction(state = it) } + } +} + @Preview @Composable private fun Preview_TokenDetailsScreen_LightTheme() { From 65d9b4818be80266568569efc0b71b52bec4645e Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 7 Sep 2023 20:47:47 +0800 Subject: [PATCH 019/242] Updated on 2026-08-14 --- .../repository/DefaultNetworksRepository.kt | 38 +++-- .../usecase/GetSelectedWalletUseCase.kt | 3 +- .../factory/WalletDeleteStateConverter.kt | 50 ++++++ .../state/factory/WalletLockedConverter.kt | 104 ++++------- .../factory/WalletRenameStateConverter.kt | 33 ++++ ...letSingleCurrencyLoadedBalanceConverter.kt | 28 ++- .../factory/WalletSkeletonStateConverter.kt | 85 +++++---- .../state/factory/WalletStateFactory.kt | 25 ++- .../factory/WalletsUnlockStateConverter.kt | 142 +++++++++++++++ .../WalletLoadedTxHistoryConverter.kt | 37 ++-- .../WalletLoadingTxHistoryConverter.kt | 8 +- .../wallet/ui/components/common/WalletCard.kt | 58 +++++-- .../ui/components/common/WalletSideEffects.kt | 4 +- .../wallet/ui/utils/ScrollOffsetCollector.kt | 12 +- .../utils/TokenListToWalletStateConverter.kt | 23 ++- .../wallet/viewmodels/WalletClickIntents.kt | 4 + .../WalletNotificationsListFactory.kt | 2 +- .../wallet/viewmodels/WalletStateCache.kt | 6 +- .../wallet/viewmodels/WalletViewModel.kt | 146 ++++++++++++---- .../viewmodels/WalletsUpdateActionResolver.kt | 161 ++++++++++++++++++ 20 files changed, 769 insertions(+), 200 deletions(-) create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletDeleteStateConverter.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletRenameStateConverter.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletsUnlockStateConverter.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletsUpdateActionResolver.kt diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultNetworksRepository.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultNetworksRepository.kt index 5ac3840c7f..b8861a9533 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultNetworksRepository.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultNetworksRepository.kt @@ -13,6 +13,7 @@ import com.tangem.domain.tokens.models.CryptoCurrency import com.tangem.domain.tokens.models.Network import com.tangem.domain.tokens.repository.NetworksRepository import com.tangem.domain.walletmanager.WalletManagersFacade +import com.tangem.domain.walletmanager.model.UpdateWalletManagerResult import com.tangem.domain.wallets.models.UserWalletId import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.extensions.addOrReplace @@ -68,25 +69,29 @@ internal class DefaultNetworksRepository( networks: Set, refresh: Boolean, ) { - cacheRegistry.invokeOnExpire( - key = getNetworksStatusesCacheKey(userWalletId), - skipCache = refresh, - block = { fetchNetworksStatuses(userWalletId, networks) }, - ) - } - - private suspend fun fetchNetworksStatuses(userWalletId: UserWalletId, networks: Set) { coroutineScope { networks .map { networkId -> async { - fetchNetworkStatus(userWalletId, networkId) + fetchNetworkStatusIfCacheExpired(userWalletId, networkId, refresh) } } .awaitAll() } } + private suspend fun fetchNetworkStatusIfCacheExpired( + userWalletId: UserWalletId, + networkId: Network.ID, + refresh: Boolean, + ) { + cacheRegistry.invokeOnExpire( + key = getNetworksStatusesCacheKey(userWalletId, networkId), + skipCache = refresh, + block = { fetchNetworkStatus(userWalletId, networkId) }, + ) + } + private suspend fun fetchNetworkStatus(userWalletId: UserWalletId, networkId: Network.ID) { val currencies = getCurrencies(userWalletId) .asSequence() @@ -97,6 +102,17 @@ internal class DefaultNetworksRepository( networkId = networkId, extraTokens = currencies.filterIsInstance().toSet(), ) + + // Invalidate cache key if wallet manager update failed + when (result) { + is UpdateWalletManagerResult.Verified, + is UpdateWalletManagerResult.NoAccount, + -> Unit + is UpdateWalletManagerResult.Unreachable, + is UpdateWalletManagerResult.MissedDerivation, + -> cacheRegistry.invalidate(getNetworksStatusesCacheKey(userWalletId, networkId)) + } + val networkStatus = networkStatusFactory.createNetworkStatus( networkId = networkId, result = result, @@ -126,5 +142,7 @@ internal class DefaultNetworksRepository( } } - private fun getNetworksStatusesCacheKey(userWalletId: UserWalletId): String = "network_status_$userWalletId" + private fun getNetworksStatusesCacheKey(userWalletId: UserWalletId, nerworkId: Network.ID): String { + return "network_status_${userWalletId}_${nerworkId.value}" + } } \ No newline at end of file diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetSelectedWalletUseCase.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetSelectedWalletUseCase.kt index 89d6fe1405..b56de58375 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetSelectedWalletUseCase.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetSelectedWalletUseCase.kt @@ -8,7 +8,8 @@ import com.tangem.domain.wallets.models.GetSelectedWalletError import com.tangem.domain.wallets.models.UserWallet /** - * Use case for getting selected wallet + * Use case for getting selected wallet. + * Important! If all wallets is locked, use case returns a error. * * @property walletsStateHolder state holder for getting static initialized 'userWalletsListManager' * diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletDeleteStateConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletDeleteStateConverter.kt new file mode 100644 index 0000000000..c6ccfcd05d --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletDeleteStateConverter.kt @@ -0,0 +1,50 @@ +package com.tangem.feature.wallet.presentation.wallet.state.factory + +import com.tangem.common.Provider +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.feature.wallet.presentation.wallet.state.WalletState +import com.tangem.feature.wallet.presentation.wallet.state.components.WalletCardState +import com.tangem.feature.wallet.presentation.wallet.state.factory.WalletDeleteStateConverter.DeleteWalletModel +import com.tangem.feature.wallet.presentation.wallet.viewmodels.WalletsUpdateActionResolver +import com.tangem.utils.converter.Converter +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.toImmutableList + +/** + * Converter that responds on wallet deleting action. Returns [WalletState] without deleted wallet. + * + * @property currentStateProvider current state provider + */ +internal class WalletDeleteStateConverter( + private val currentStateProvider: Provider, +) : Converter { + + override fun convert(value: DeleteWalletModel): WalletState { + return when (val state = currentStateProvider()) { + is WalletState.ContentState -> { + value.cacheState.copySealed( + walletsListConfig = state.walletsListConfig.copy( + selectedWalletIndex = value.action.selectedWalletIndex, + wallets = state.walletsListConfig.wallets.deleteWallet(id = value.action.deletedWalletId), + ), + pullToRefreshConfig = value.cacheState.pullToRefreshConfig.copy(isRefreshing = false), + ) + } + is WalletState.Initial -> state + } + } + + private fun List.deleteWallet(id: UserWalletId): ImmutableList { + return this + .mapIndexedNotNull { index, currentWallet -> + if (currentWallet.id == id) return@mapIndexedNotNull null + getOrNull(index) ?: return@mapIndexedNotNull null + } + .toImmutableList() + } + + data class DeleteWalletModel( + val cacheState: WalletState.ContentState, + val action: WalletsUpdateActionResolver.Action.DeleteWallet, + ) +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletLockedConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletLockedConverter.kt index 3e89437610..79b2d32206 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletLockedConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletLockedConverter.kt @@ -1,110 +1,78 @@ package com.tangem.feature.wallet.presentation.wallet.state.factory import com.tangem.common.Provider -import com.tangem.domain.common.CardTypesResolver -import com.tangem.domain.wallets.models.UserWallet -import com.tangem.feature.wallet.presentation.wallet.domain.WalletAdditionalInfoFactory import com.tangem.feature.wallet.presentation.wallet.state.WalletMultiCurrencyState import com.tangem.feature.wallet.presentation.wallet.state.WalletSingleCurrencyState import com.tangem.feature.wallet.presentation.wallet.state.WalletState -import com.tangem.feature.wallet.presentation.wallet.state.components.WalletCardState import com.tangem.feature.wallet.presentation.wallet.state.components.WalletManageButton +import com.tangem.feature.wallet.presentation.wallet.state.components.WalletPullToRefreshConfig import com.tangem.feature.wallet.presentation.wallet.state.components.WalletTopBarConfig -import com.tangem.feature.wallet.presentation.wallet.state.components.WalletsListConfig import com.tangem.feature.wallet.presentation.wallet.viewmodels.WalletClickIntents import com.tangem.utils.converter.Converter import kotlinx.collections.immutable.PersistentList -import kotlinx.collections.immutable.persistentListOf -import kotlinx.collections.immutable.toImmutableList +import kotlinx.collections.immutable.toPersistentList internal class WalletLockedConverter( private val currentStateProvider: Provider, - private val currentCardTypeResolverProvider: Provider, - private val currentWalletProvider: Provider, private val clickIntents: WalletClickIntents, ) : Converter { override fun convert(value: Unit): WalletState { return when (val state = currentStateProvider()) { - is WalletState.ContentState -> { - val cardTypeResolver = currentCardTypeResolverProvider() - - if (cardTypeResolver.isMultiwalletAllowed()) { - state.toMultiCurrencyLockedState(cardTypeResolver) - } else { - state.toSingleCurrencyLockedState(cardTypeResolver) - } - } - is WalletState.Initial -> state + is WalletMultiCurrencyState.Content -> state.toMultiCurrencyLockedState() + is WalletSingleCurrencyState.Content -> state.toSingleCurrencyLockedState() + is WalletMultiCurrencyState.Locked, + is WalletSingleCurrencyState.Locked, + is WalletState.Initial, + -> state } } - private fun WalletState.ContentState.toMultiCurrencyLockedState( - cardTypeResolver: CardTypesResolver, - ): WalletMultiCurrencyState.Locked { + private fun WalletMultiCurrencyState.Content.toMultiCurrencyLockedState(): WalletState { return WalletMultiCurrencyState.Locked( onBackClick = onBackClick, - topBarConfig = createTopBarConfig(), - walletsListConfig = createWalletsListConfig(cardTypeResolver), - pullToRefreshConfig = pullToRefreshConfig, + topBarConfig = topBarConfig.updateCallback(), + walletsListConfig = walletsListConfig, + pullToRefreshConfig = pullToRefreshConfig.stopRefreshing(), onUnlockWalletsNotificationClick = clickIntents::onUnlockWalletNotificationClick, onUnlockClick = clickIntents::onUnlockWalletClick, - onScanClick = clickIntents::onScanCardClick, + onScanClick = clickIntents::onScanToUnlockWalletClick, ) } - private fun WalletState.ContentState.toSingleCurrencyLockedState( - cardTypeResolver: CardTypesResolver, - ): WalletSingleCurrencyState.Locked { + private fun WalletSingleCurrencyState.Content.toSingleCurrencyLockedState(): WalletState { return WalletSingleCurrencyState.Locked( onBackClick = onBackClick, - topBarConfig = createTopBarConfig(), - walletsListConfig = createWalletsListConfig(cardTypeResolver), - pullToRefreshConfig = pullToRefreshConfig, - buttons = createButtons(), + topBarConfig = topBarConfig.updateCallback(), + walletsListConfig = walletsListConfig, + pullToRefreshConfig = pullToRefreshConfig.stopRefreshing(), + buttons = buttons.disableButtons(), onUnlockWalletsNotificationClick = clickIntents::onUnlockWalletNotificationClick, onUnlockClick = clickIntents::onUnlockWalletClick, - onScanClick = clickIntents::onScanCardClick, + onScanClick = clickIntents::onScanToUnlockWalletClick, onExploreClick = clickIntents::onExploreClick, ) } - private fun WalletState.ContentState.createTopBarConfig(): WalletTopBarConfig { - return topBarConfig.copy(onMoreClick = clickIntents::onUnlockWalletNotificationClick) + private fun WalletTopBarConfig.updateCallback(): WalletTopBarConfig { + return copy(onMoreClick = clickIntents::onUnlockWalletNotificationClick) } - private fun WalletState.ContentState.createWalletsListConfig( - cardTypeResolver: CardTypesResolver, - ): WalletsListConfig { - return walletsListConfig.copy( - wallets = walletsListConfig.wallets - .map { walletCardState -> - WalletCardState.LockedContent( - id = walletCardState.id, - title = walletCardState.title, - additionalInfo = if (cardTypeResolver.isMultiwalletAllowed()) { - WalletAdditionalInfoFactory.resolve( - cardTypesResolver = cardTypeResolver, - wallet = currentWalletProvider(), - ) - } else { - null - }, - imageResId = walletCardState.imageResId, - onRenameClick = walletCardState.onRenameClick, - onDeleteClick = walletCardState.onDeleteClick, - ) + private fun WalletPullToRefreshConfig.stopRefreshing(): WalletPullToRefreshConfig { + return copy(isRefreshing = false) + } + + private fun PersistentList.disableButtons(): PersistentList { + return this + .map { button -> + when (button) { + is WalletManageButton.Buy -> button.copy(enabled = false) + is WalletManageButton.Sell -> button.copy(enabled = false) + is WalletManageButton.Send -> button.copy(enabled = false) + is WalletManageButton.Swap -> button.copy(enabled = false) + is WalletManageButton.Receive -> button } - .toImmutableList(), - ) - } - - private fun createButtons(): PersistentList { - return persistentListOf( - WalletManageButton.Buy(enabled = false, onClick = {}), - WalletManageButton.Send(enabled = false, onClick = {}), - WalletManageButton.Receive(onClick = {}), - WalletManageButton.Sell(enabled = false, onClick = {}), - ) + } + .toPersistentList() } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletRenameStateConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletRenameStateConverter.kt new file mode 100644 index 0000000000..946afa55df --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletRenameStateConverter.kt @@ -0,0 +1,33 @@ +package com.tangem.feature.wallet.presentation.wallet.state.factory + +import com.tangem.common.Provider +import com.tangem.feature.wallet.presentation.wallet.state.WalletState +import com.tangem.feature.wallet.presentation.wallet.state.components.WalletsListConfig +import com.tangem.utils.converter.Converter +import kotlinx.collections.immutable.toImmutableList + +internal class WalletRenameStateConverter( + private val currentStateProvider: Provider, +) : Converter { + + override fun convert(value: String): WalletState { + return when (val state = currentStateProvider()) { + is WalletState.ContentState -> { + state.copySealed( + walletsListConfig = state.walletsListConfig.renameSelectedWallet(name = value), + ) + } + is WalletState.Initial -> state + } + } + + private fun WalletsListConfig.renameSelectedWallet(name: String): WalletsListConfig { + return copy( + wallets = wallets + .mapIndexed { index, walletCard -> + if (index == selectedWalletIndex) walletCard.copySealed(title = name) else walletCard + } + .toImmutableList(), + ) + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletSingleCurrencyLoadedBalanceConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletSingleCurrencyLoadedBalanceConverter.kt index 18892dce3f..b67b7cedb8 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletSingleCurrencyLoadedBalanceConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletSingleCurrencyLoadedBalanceConverter.kt @@ -11,6 +11,7 @@ import com.tangem.domain.tokens.error.CurrencyStatusError import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.wallets.models.UserWallet import com.tangem.feature.wallet.presentation.wallet.domain.WalletAdditionalInfoFactory +import com.tangem.feature.wallet.presentation.wallet.state.WalletMultiCurrencyState import com.tangem.feature.wallet.presentation.wallet.state.WalletSingleCurrencyState import com.tangem.feature.wallet.presentation.wallet.state.WalletState import com.tangem.feature.wallet.presentation.wallet.state.components.WalletCardState @@ -26,22 +27,31 @@ internal class WalletSingleCurrencyLoadedBalanceConverter( private val appCurrencyProvider: Provider, private val currentWalletProvider: Provider, private val currencyStatusErrorConverter: CurrencyStatusErrorConverter, -) : Converter, WalletSingleCurrencyState.Content> { +) : Converter, WalletState> { - override fun convert(value: Either): WalletSingleCurrencyState.Content { + override fun convert(value: Either): WalletState { return value.fold( ifLeft = currencyStatusErrorConverter::convert, ifRight = ::convertContent, ) } - private fun convertContent(status: CryptoCurrencyStatus): WalletSingleCurrencyState.Content { - val state = requireNotNull(currentStateProvider() as? WalletSingleCurrencyState.Content) - val currencyName = state.marketPriceBlockState.currencyName - return state.copy( - walletsListConfig = getUpdatedSelectedWallet(status = status.value, state = state), - marketPriceBlockState = getMarketPriceState(status = status.value, currencyName = currencyName), - ) + private fun convertContent(status: CryptoCurrencyStatus): WalletState { + return when (val state = currentStateProvider()) { + is WalletSingleCurrencyState.Content -> { + val currencyName = state.marketPriceBlockState.currencyName + + state.copy( + walletsListConfig = getUpdatedSelectedWallet(status = status.value, state = state), + marketPriceBlockState = getMarketPriceState(status = status.value, currencyName = currencyName), + ) + } + is WalletMultiCurrencyState.Content, + is WalletMultiCurrencyState.Locked, + is WalletSingleCurrencyState.Locked, + is WalletState.Initial, + -> state + } } private fun getMarketPriceState(status: CryptoCurrencyStatus.Status, currencyName: String): MarketPriceBlockState { diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletSkeletonStateConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletSkeletonStateConverter.kt index 05b16b6424..166a46f8fa 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletSkeletonStateConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletSkeletonStateConverter.kt @@ -1,8 +1,10 @@ package com.tangem.feature.wallet.presentation.wallet.state.factory +import androidx.annotation.DrawableRes import com.tangem.common.Provider import com.tangem.core.ui.components.marketprice.MarketPriceBlockState import com.tangem.core.ui.components.transactions.state.TxHistoryState +import com.tangem.core.ui.extensions.TextReference import com.tangem.domain.common.util.cardTypesResolver import com.tangem.domain.wallets.models.UserWallet import com.tangem.feature.wallet.presentation.wallet.domain.WalletAdditionalInfoFactory @@ -33,12 +35,12 @@ internal class WalletSkeletonStateConverter( ) : Converter { override fun convert(value: SkeletonModel): WalletState.ContentState { - val cardTypeResolver = value.wallets[value.selectedWalletIndex].scanResponse.cardTypesResolver + val selectedWallet = value.wallets[value.selectedWalletIndex] - return if (cardTypeResolver.isMultiwalletAllowed()) { + return if (selectedWallet.isMultiCurrency) { createMultiCurrencyState(value = value) } else { - createSingleCurrencyState(value = value, currencyName = cardTypeResolver.getBlockchain().currency) + createSingleCurrencyState(value = value, currencyName = selectedWallet.getPrimaryCurrencyName()) } } @@ -74,6 +76,10 @@ internal class WalletSkeletonStateConverter( ) } + private fun UserWallet.getPrimaryCurrencyName(): String { + return scanResponse.cardTypesResolver.getBlockchain().currency + } + private fun createTopBarConfig(): WalletTopBarConfig { return WalletTopBarConfig( onScanCardClick = clickIntents::onScanCardClick, @@ -84,46 +90,63 @@ internal class WalletSkeletonStateConverter( private fun createWalletsListConfig(value: SkeletonModel): WalletsListConfig { return WalletsListConfig( selectedWalletIndex = value.selectedWalletIndex, - wallets = value.wallets.map(::createWalletState).toImmutableList(), + wallets = value.wallets.mapIndexed(::createWalletCardState).toImmutableList(), onWalletChange = clickIntents::onWalletChange, ) } - private fun createWalletState(wallet: UserWallet): WalletCardState { - val state = currentStateProvider() - - // If it isn't first initialization (example, when user unlocks wallet) - return if (state is WalletState.ContentState) { - val initializedWallet = state.walletsListConfig.wallets.first { it.id == wallet.walletId } - - // If wallet is initialized, return it, otherwise return loading state - if (initializedWallet !is WalletCardState.Loading) { - initializedWallet.copySealed(title = wallet.name) - } else { - createWalletLoadingState(wallet) - } - } else { - createWalletLoadingState(wallet) - } + /** + * Create wallet card state by [index] and [wallet]. + * If current wallet card state is initialized, then method returns it. + * Otherwise, returns loading wallet card state. + */ + private fun createWalletCardState(index: Int, wallet: UserWallet): WalletCardState { + return currentStateProvider().getInitializedWalletCardState(index) ?: wallet.mapToWalletCardState() } - private fun createWalletLoadingState(wallet: UserWallet): WalletCardState { - val cardTypeResolver = wallet.scanResponse.cardTypesResolver + private fun WalletState.getInitializedWalletCardState(index: Int): WalletCardState? { + return (this as? WalletState.ContentState)?.walletsListConfig?.wallets?.getOrNull(index) + } - return WalletCardState.Loading( - id = wallet.walletId, - title = wallet.name, - additionalInfo = if (cardTypeResolver.isMultiwalletAllowed()) { - WalletAdditionalInfoFactory.resolve(cardTypesResolver = cardTypeResolver, wallet = wallet) - } else { - null - }, - imageResId = WalletImageResolver.resolve(cardTypesResolver = cardTypeResolver), + private fun UserWallet.mapToWalletCardState(): WalletCardState { + return if (isLocked) mapToLockedWalletCardState() else mapToLoadingWalletCardState() + } + + private fun UserWallet.mapToLockedWalletCardState(): WalletCardState { + return WalletCardState.LockedContent( + id = walletId, + title = name, + additionalInfo = createAdditionalInfo(), + imageResId = createImageResId(), onRenameClick = clickIntents::onRenameClick, onDeleteClick = clickIntents::onDeleteClick, ) } + private fun UserWallet.mapToLoadingWalletCardState(): WalletCardState { + return WalletCardState.Loading( + id = walletId, + title = name, + additionalInfo = createAdditionalInfo(), + imageResId = createImageResId(), + onRenameClick = clickIntents::onRenameClick, + onDeleteClick = clickIntents::onDeleteClick, + ) + } + + private fun UserWallet.createAdditionalInfo(): TextReference? { + return if (isMultiCurrency) { + WalletAdditionalInfoFactory.resolve(cardTypesResolver = scanResponse.cardTypesResolver, wallet = this) + } else { + null + } + } + + @DrawableRes + private fun UserWallet.createImageResId(): Int? { + return WalletImageResolver.resolve(cardTypesResolver = scanResponse.cardTypesResolver) + } + private fun createPullToRefreshConfig(): WalletPullToRefreshConfig { return WalletPullToRefreshConfig(isRefreshing = false, onRefresh = clickIntents::onRefreshSwipe) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletStateFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletStateFactory.kt index fa22261f52..ad2916e9b1 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletStateFactory.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletStateFactory.kt @@ -25,6 +25,7 @@ import com.tangem.feature.wallet.presentation.wallet.state.factory.txhistory.Wal import com.tangem.feature.wallet.presentation.wallet.utils.CurrencyStatusErrorConverter import com.tangem.feature.wallet.presentation.wallet.utils.TokenListErrorConverter import com.tangem.feature.wallet.presentation.wallet.viewmodels.WalletClickIntents +import com.tangem.feature.wallet.presentation.wallet.viewmodels.WalletsUpdateActionResolver import kotlinx.collections.immutable.ImmutableList import kotlinx.coroutines.flow.Flow @@ -45,8 +46,15 @@ internal class WalletStateFactory( ) { private val tokenActionsProvider by lazy { TokenActionsProvider(clickIntents) } + private val skeletonConverter by lazy { WalletSkeletonStateConverter(currentStateProvider, clickIntents) } + private val walletsUnlockStateConverter by lazy { WalletsUnlockStateConverter(currentStateProvider, clickIntents) } + + private val walletRenameStateConverter by lazy { WalletRenameStateConverter(currentStateProvider) } + + private val walletDeleteStateConverter by lazy { WalletDeleteStateConverter(currentStateProvider) } + private val tokenListErrorConverter by lazy { TokenListErrorConverter(currentStateProvider) } @@ -92,8 +100,6 @@ internal class WalletStateFactory( private val lockedConverter by lazy { WalletLockedConverter( currentStateProvider = currentStateProvider, - currentCardTypeResolverProvider = currentCardTypeResolverProvider, - currentWalletProvider = currentWalletProvider, clickIntents = clickIntents, ) } @@ -123,6 +129,21 @@ internal class WalletStateFactory( ) } + fun getStateWithUpdatedWalletName(name: String): WalletState = walletRenameStateConverter.convert(value = name) + + fun getUnlockedState(action: WalletsUpdateActionResolver.Action.UnlockWallet): WalletState { + return walletsUnlockStateConverter.convert(value = action) + } + + fun getStateWithoutDeletedWallet( + cacheState: WalletState.ContentState, + action: WalletsUpdateActionResolver.Action.DeleteWallet, + ): WalletState { + return walletDeleteStateConverter.convert( + value = WalletDeleteStateConverter.DeleteWalletModel(cacheState = cacheState, action = action), + ) + } + fun getStateByTokensList(maybeTokenList: Either): WalletState { return loadedTokensListConverter.convert(maybeTokenList) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletsUnlockStateConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletsUnlockStateConverter.kt new file mode 100644 index 0000000000..cb9d0d7e7e --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletsUnlockStateConverter.kt @@ -0,0 +1,142 @@ +package com.tangem.feature.wallet.presentation.wallet.state.factory + +import com.tangem.common.Provider +import com.tangem.core.ui.components.marketprice.MarketPriceBlockState +import com.tangem.core.ui.components.transactions.state.TxHistoryState +import com.tangem.core.ui.extensions.TextReference +import com.tangem.domain.common.util.cardTypesResolver +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.feature.wallet.presentation.wallet.domain.WalletAdditionalInfoFactory +import com.tangem.feature.wallet.presentation.wallet.domain.WalletImageResolver +import com.tangem.feature.wallet.presentation.wallet.state.WalletMultiCurrencyState +import com.tangem.feature.wallet.presentation.wallet.state.WalletSingleCurrencyState +import com.tangem.feature.wallet.presentation.wallet.state.WalletState +import com.tangem.feature.wallet.presentation.wallet.state.components.* +import com.tangem.feature.wallet.presentation.wallet.viewmodels.WalletClickIntents +import com.tangem.utils.converter.Converter +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toImmutableList +import kotlinx.coroutines.flow.MutableStateFlow +import com.tangem.feature.wallet.presentation.wallet.viewmodels.WalletsUpdateActionResolver.Action.UnlockWallet as UnlockWalletAction + +/** + * Converter that responds on wallets unlocking action. Returns [WalletState] with unlocked wallets. + * + * @property currentStateProvider current ui state provider + * @property clickIntents screen click intents + * +[REDACTED_AUTHOR] + */ +internal class WalletsUnlockStateConverter( + private val currentStateProvider: Provider, + private val clickIntents: WalletClickIntents, +) : Converter { + + override fun convert(value: UnlockWalletAction): WalletState { + return when (val state = currentStateProvider()) { + is WalletMultiCurrencyState.Locked -> state.toMultiCurrencyContentState(value) + is WalletSingleCurrencyState.Locked -> state.toSingleCurrencyContentState(value) + is WalletState.Initial, + is WalletMultiCurrencyState.Content, + is WalletSingleCurrencyState.Content, + -> state + } + } + + private fun WalletMultiCurrencyState.Locked.toMultiCurrencyContentState(action: UnlockWalletAction): WalletState { + return WalletMultiCurrencyState.Content( + onBackClick = onBackClick, + topBarConfig = topBarConfig.updateCallback(), + walletsListConfig = walletsListConfig.unlockWallets(action), + pullToRefreshConfig = pullToRefreshConfig.stopRefreshing(), + tokensListState = WalletTokensListState.Loading(), + notifications = persistentListOf(), + bottomSheetConfig = null, + tokenActionsBottomSheet = null, + onManageTokensClick = clickIntents::onManageTokensClick, + ) + } + + private fun WalletSingleCurrencyState.Locked.toSingleCurrencyContentState(action: UnlockWalletAction): WalletState { + return WalletSingleCurrencyState.Content( + onBackClick = onBackClick, + topBarConfig = topBarConfig.updateCallback(), + walletsListConfig = walletsListConfig.unlockWallets(action), + pullToRefreshConfig = pullToRefreshConfig.stopRefreshing(), + notifications = persistentListOf(), + bottomSheetConfig = null, + buttons = buttons, + marketPriceBlockState = MarketPriceBlockState.Loading( + currencyName = action.selectedWallet.getPrimaryCurrencyName(), + ), + txHistoryState = TxHistoryState.Content( + contentItems = MutableStateFlow( + value = TxHistoryState.getDefaultLoadingTransactions(clickIntents::onExploreClick), + ), + ), + ) + } + + private fun WalletTopBarConfig.updateCallback(): WalletTopBarConfig { + return copy(onMoreClick = clickIntents::onDetailsClick) + } + + private fun WalletsListConfig.unlockWallets(action: UnlockWalletAction): WalletsListConfig { + return this.copy( + selectedWalletIndex = action.selectedWalletIndex, + wallets = wallets.unlockWallets(action), + ) + } + + private fun List.unlockWallets(action: UnlockWalletAction): ImmutableList { + return this + .map { prevWallet -> + if (prevWallet is WalletCardState.LockedContent && action.isUnlockedWallet(prevWallet.id)) { + prevWallet.mapToLoadingWalletCardState( + userWallet = action.getUnlockWallet(prevWallet.id), + ) + } else { + prevWallet + } + } + .toImmutableList() + } + + private fun UnlockWalletAction.isUnlockedWallet(walletId: UserWalletId): Boolean { + return unlockedWallets.any { it.walletId == walletId } + } + + private fun UnlockWalletAction.getUnlockWallet(walletId: UserWalletId): UserWallet { + return unlockedWallets.firstOrNull { it.walletId == walletId } + ?: error("Unlocked wallet with id $walletId not found") + } + + private fun WalletCardState.mapToLoadingWalletCardState(userWallet: UserWallet): WalletCardState { + return WalletCardState.Loading( + id = id, + title = title, + additionalInfo = userWallet.createAdditionalInfo(), + imageResId = WalletImageResolver.resolve(cardTypesResolver = userWallet.scanResponse.cardTypesResolver), + onRenameClick = onRenameClick, + onDeleteClick = onDeleteClick, + ) + } + + private fun UserWallet.createAdditionalInfo(): TextReference? { + return if (isMultiCurrency) { + WalletAdditionalInfoFactory.resolve(cardTypesResolver = scanResponse.cardTypesResolver, wallet = this) + } else { + null + } + } + + private fun WalletPullToRefreshConfig.stopRefreshing(): WalletPullToRefreshConfig { + return copy(isRefreshing = false) + } + + private fun UserWallet.getPrimaryCurrencyName(): String { + return scanResponse.cardTypesResolver.getBlockchain().currency + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/txhistory/WalletLoadedTxHistoryConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/txhistory/WalletLoadedTxHistoryConverter.kt index e585ac71f5..9c56350d24 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/txhistory/WalletLoadedTxHistoryConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/txhistory/WalletLoadedTxHistoryConverter.kt @@ -7,6 +7,7 @@ import com.tangem.core.ui.components.transactions.state.TxHistoryState import com.tangem.domain.common.CardTypesResolver import com.tangem.domain.txhistory.models.TxHistoryItem import com.tangem.domain.txhistory.models.TxHistoryListError +import com.tangem.feature.wallet.presentation.wallet.state.WalletMultiCurrencyState import com.tangem.feature.wallet.presentation.wallet.state.WalletSingleCurrencyState import com.tangem.feature.wallet.presentation.wallet.state.WalletState import com.tangem.feature.wallet.presentation.wallet.viewmodels.WalletClickIntents @@ -41,18 +42,34 @@ internal class WalletLoadedTxHistoryConverter( } private fun convertError(error: TxHistoryListError): WalletState { - return requireNotNull(currentStateProvider() as? WalletSingleCurrencyState.Content).copy( - txHistoryState = when (error) { - is TxHistoryListError.DataError -> { - TxHistoryState.Error(onReloadClick = clickIntents::onReloadClick) - } - }, - ) + return when (val state = currentStateProvider()) { + is WalletSingleCurrencyState.Content -> { + state.copy( + txHistoryState = when (error) { + is TxHistoryListError.DataError -> { + TxHistoryState.Error(onReloadClick = clickIntents::onReloadClick) + } + }, + ) + } + is WalletMultiCurrencyState.Content, + is WalletMultiCurrencyState.Locked, + is WalletSingleCurrencyState.Locked, + is WalletState.Initial, + -> state + } } private fun convert(items: Flow>): WalletState { - return requireNotNull(currentStateProvider() as? WalletSingleCurrencyState.Content).copy( - txHistoryState = walletTxHistoryItemFlowConverter.convert(value = items), - ) + return when (val state = currentStateProvider()) { + is WalletSingleCurrencyState.Content -> { + state.copy(txHistoryState = walletTxHistoryItemFlowConverter.convert(value = items)) + } + is WalletMultiCurrencyState.Content, + is WalletMultiCurrencyState.Locked, + is WalletSingleCurrencyState.Locked, + is WalletState.Initial, + -> state + } } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/txhistory/WalletLoadingTxHistoryConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/txhistory/WalletLoadingTxHistoryConverter.kt index b8b5ab1a7d..027a2bf2aa 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/txhistory/WalletLoadingTxHistoryConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/txhistory/WalletLoadingTxHistoryConverter.kt @@ -51,11 +51,11 @@ internal class WalletLoadingTxHistoryConverter( } } - private fun convert(value: Int): WalletSingleCurrencyState.Content { - val state = requireNotNull(currentStateProvider() as? WalletSingleCurrencyState.Content) - val txHistoryContent = requireNotNull(state.txHistoryState as? Content) + private fun convert(value: Int): WalletState { + val state = currentStateProvider() + val txHistoryContent = (state as? WalletSingleCurrencyState.Content)?.txHistoryState as? Content - txHistoryContent.contentItems.update { + txHistoryContent?.contentItems?.update { PagingData.from( data = listOf(TxHistoryItemState.Title(onExploreClick = clickIntents::onExploreClick)) + MutableList( diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletCard.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletCard.kt index 494e36cee2..fa5c096897 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletCard.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletCard.kt @@ -31,6 +31,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider +import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.DpOffset import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp @@ -163,22 +164,14 @@ private fun CardContainer( var isRenameWalletDialogVisible by rememberSaveable { mutableStateOf(value = false) } - DropdownMenu( - expanded = isMenuVisible, + ManageWalletContextMenu( + isMenuVisible = isMenuVisible, + pressOffset = pressOffset, + itemHeight = itemHeight, onDismissRequest = { isMenuVisible = false }, - modifier = Modifier.background(color = TangemTheme.colors.background.secondary), - offset = pressOffset.copy(y = pressOffset.y - itemHeight), - ) { - MenuItem( - textResId = R.string.common_rename, - imageVector = Icons.Outlined.Edit, - onClick = { - isMenuVisible = false - isRenameWalletDialogVisible = true - }, - ) - MenuItem(textResId = R.string.common_delete, imageVector = Icons.Outlined.Delete, onClick = onDeleteClick) - } + onShowRenameWalletDialogClick = { isRenameWalletDialogVisible = true }, + onDeleteClick = onDeleteClick, + ) if (isRenameWalletDialogVisible) { RenameWalletDialogContent( @@ -192,6 +185,41 @@ private fun CardContainer( } } +@Suppress("LongParameterList") +@Composable +private fun ManageWalletContextMenu( + isMenuVisible: Boolean, + pressOffset: DpOffset, + itemHeight: Dp, + onDismissRequest: () -> Unit, + onShowRenameWalletDialogClick: () -> Unit, + onDeleteClick: () -> Unit, +) { + DropdownMenu( + expanded = isMenuVisible, + onDismissRequest = onDismissRequest, + modifier = Modifier.background(color = TangemTheme.colors.background.secondary), + offset = pressOffset.copy(y = pressOffset.y - itemHeight), + ) { + MenuItem( + textResId = R.string.common_rename, + imageVector = Icons.Outlined.Edit, + onClick = { + onDismissRequest() + onShowRenameWalletDialogClick() + }, + ) + MenuItem( + textResId = R.string.common_delete, + imageVector = Icons.Outlined.Delete, + onClick = { + onDismissRequest() + onDeleteClick() + }, + ) + } +} + @Composable private fun MenuItem(@StringRes textResId: Int, imageVector: ImageVector, onClick: () -> Unit) { DropdownMenuItem( diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletSideEffects.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletSideEffects.kt index 0d415eaa74..db3972e010 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletSideEffects.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletSideEffects.kt @@ -19,7 +19,9 @@ import com.tangem.feature.wallet.presentation.wallet.ui.utils.ScrollOffsetCollec @Composable internal fun WalletSideEffects(lazyListState: LazyListState, walletsListConfig: WalletsListConfig) { LaunchedEffect(key1 = walletsListConfig.selectedWalletIndex) { - lazyListState.scrollToItem(walletsListConfig.selectedWalletIndex) + if (!lazyListState.isScrollInProgress) { + lazyListState.animateScrollToItem(walletsListConfig.selectedWalletIndex) + } } val dragInteraction = lazyListState.interactionSource.interactions.collectAsState(initial = null) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/utils/ScrollOffsetCollector.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/utils/ScrollOffsetCollector.kt index 1cfc37798f..ba5548fe3e 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/utils/ScrollOffsetCollector.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/utils/ScrollOffsetCollector.kt @@ -1,5 +1,6 @@ package com.tangem.feature.wallet.presentation.wallet.ui.utils +import androidx.compose.foundation.interaction.DragInteraction import androidx.compose.foundation.interaction.Interaction import androidx.compose.foundation.lazy.LazyListItemInfo import androidx.compose.foundation.lazy.LazyListState @@ -27,7 +28,8 @@ internal class ScrollOffsetCollector( private val LazyListItemInfo.halfItemSize get() = size.div(other = 2) override suspend fun emit(value: List) { - if (!lazyListState.isScrollInProgress || dragInteraction.value == null || value.size <= 1) return + if (isNotUserInteraction() || value.size <= 1) return + val firstItem = value.firstOrNull() ?: return val lastItem = value.lastOrNull() ?: return @@ -37,4 +39,12 @@ internal class ScrollOffsetCollector( callback(lastItem.index - 1) } } + + /** + * Sometimes the list is scrolled programmatically. Example: selecting a specific wallet when a user opens the + * screen for the first time or scans a new wallet. Therefore [ScrollOffsetCollector] should not respond to changes. + */ + private fun isNotUserInteraction(): Boolean { + return !lazyListState.isScrollInProgress || dragInteraction.value !is DragInteraction.Start + } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/TokenListToWalletStateConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/TokenListToWalletStateConverter.kt index 9019ff5b9a..8a05b70ea7 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/TokenListToWalletStateConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/TokenListToWalletStateConverter.kt @@ -6,6 +6,7 @@ import com.tangem.domain.common.CardTypesResolver import com.tangem.domain.tokens.model.TokenList import com.tangem.domain.wallets.models.UserWallet import com.tangem.feature.wallet.presentation.wallet.state.WalletMultiCurrencyState +import com.tangem.feature.wallet.presentation.wallet.state.WalletSingleCurrencyState import com.tangem.feature.wallet.presentation.wallet.state.WalletState import com.tangem.feature.wallet.presentation.wallet.state.components.WalletsListConfig import com.tangem.feature.wallet.presentation.wallet.viewmodels.WalletClickIntents @@ -20,7 +21,7 @@ internal class TokenListToWalletStateConverter( private val appCurrencyProvider: Provider, private val isWalletContentHidden: Boolean, clickIntents: WalletClickIntents, -) : Converter { +) : Converter { private val tokenListToContentConverter = TokenListToContentItemsConverter( isWalletContentHidden = isWalletContentHidden, @@ -28,12 +29,20 @@ internal class TokenListToWalletStateConverter( clickIntents = clickIntents, ) - override fun convert(value: TokenList): WalletMultiCurrencyState.Content { - val state = requireNotNull(currentStateProvider() as? WalletMultiCurrencyState.Content) - return state.copy( - walletsListConfig = state.updateSelectedWallet(fiatBalance = value.totalFiatBalance), - tokensListState = tokenListToContentConverter.convert(value = value), - ) + override fun convert(value: TokenList): WalletState { + return when (val state = currentStateProvider()) { + is WalletMultiCurrencyState.Content -> { + state.copy( + walletsListConfig = state.updateSelectedWallet(fiatBalance = value.totalFiatBalance), + tokensListState = tokenListToContentConverter.convert(value = value), + ) + } + is WalletMultiCurrencyState.Locked, + is WalletSingleCurrencyState.Content, + is WalletSingleCurrencyState.Locked, + is WalletState.Initial, + -> state + } } private fun WalletMultiCurrencyState.updateSelectedWallet(fiatBalance: TokenList.FiatBalance): WalletsListConfig { diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletClickIntents.kt index 34e8f722a8..0dc8e4b35b 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletClickIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletClickIntents.kt @@ -12,6 +12,10 @@ internal interface WalletClickIntents : TxHistoryClickIntents { fun onScanCardClick() + fun onScanCardNotificationClick() + + fun onScanToUnlockWalletClick() + fun onDetailsClick() fun onBackupCardClick() diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletNotificationsListFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletNotificationsListFactory.kt index fb70c489e4..1d981cf79a 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletNotificationsListFactory.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletNotificationsListFactory.kt @@ -59,7 +59,7 @@ internal class WalletNotificationsListFactory( } if (tokenList != null && tokenList.hasMissedDerivations()) { - add(element = WalletNotification.ScanCard(onClick = clickIntents::onScanCardClick)) + add(element = WalletNotification.ScanCard(onClick = clickIntents::onScanCardNotificationClick)) } if (isUserAlreadyRateAppCallback()) { diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletStateCache.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletStateCache.kt index c5d4e10424..f508bacc5a 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletStateCache.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletStateCache.kt @@ -10,13 +10,13 @@ import com.tangem.feature.wallet.presentation.wallet.state.WalletState */ internal object WalletStateCache { - private val states = mutableMapOf() + private val states = mutableMapOf() /** Get state by [userWalletId] */ - fun getState(userWalletId: UserWalletId): WalletState? = states[userWalletId] + fun getState(userWalletId: UserWalletId): WalletState.ContentState? = states[userWalletId] /** Add or update [state] by [userWalletId] */ - fun update(userWalletId: UserWalletId, state: WalletState) { + fun update(userWalletId: UserWalletId, state: WalletState.ContentState) { states[userWalletId] = state } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt index 5b948416b6..537366f56b 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt @@ -47,6 +47,7 @@ import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.JobHolder import com.tangem.utils.coroutines.saveIn import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.delay import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch @@ -63,7 +64,7 @@ import kotlin.properties.Delegates internal class WalletViewModel @Inject constructor( private val getWalletsUseCase: GetWalletsUseCase, private val saveWalletUseCase: SaveWalletUseCase, - private val getSelectedWalletUseCase: GetSelectedWalletUseCase, + getSelectedWalletUseCase: GetSelectedWalletUseCase, private val selectWalletUseCase: SelectWalletUseCase, private val updateWalletUseCase: UpdateWalletUseCase, private val deleteWalletUseCase: DeleteWalletUseCase, @@ -130,6 +131,11 @@ internal class WalletViewModel @Inject constructor( private val notificationsJobHolder = JobHolder() private val refreshContentJobHolder = JobHolder() + private val walletsUpdateActionResolver = WalletsUpdateActionResolver( + currentStateProvider = Provider { uiState }, + getSelectedWalletUseCase = getSelectedWalletUseCase, + ) + override fun onCreate(owner: LifecycleOwner) { viewModelScope.launch(dispatchers.main) { delay(timeMillis = 1_800) @@ -148,29 +154,48 @@ internal class WalletViewModel @Inject constructor( } private fun updateWallets(sourceList: List) { - if (sourceList.isEmpty()) return - wallets = sourceList - val currentState = uiState - val previousSelectedWalletIndex = (currentState as? WalletState.ContentState) - ?.walletsListConfig - ?.selectedWalletIndex + if (sourceList.isEmpty()) return - val selectedWalletIndex = if (currentState is WalletLockedState) { - currentState.getSelectedWalletIndex() - } else { - val selectedWallet = getSelectedWalletUseCase().fold( - ifLeft = { error("Selected wallet is null") }, - ifRight = { it }, - ) - sourceList.indexOfFirst { it.walletId == selectedWallet.walletId } + when (val action = walletsUpdateActionResolver.resolve(sourceList)) { + is WalletsUpdateActionResolver.Action.InitialWallets -> { + loadAndUpdateState(index = action.selectedWalletIndex) + } + is WalletsUpdateActionResolver.Action.UpdateWalletName -> { + uiState = stateFactory.getStateWithUpdatedWalletName(name = action.name) + } + is WalletsUpdateActionResolver.Action.UnlockWallet -> { + uiState = stateFactory.getUnlockedState(action) + + getContentItemsUpdates(index = action.selectedWalletIndex) + } + is WalletsUpdateActionResolver.Action.DeleteWallet -> { + deleteWalletAndUpdateState(action = action) + } + is WalletsUpdateActionResolver.Action.AddWallet -> { + loadAndUpdateState(index = action.selectedWalletIndex) + } + is WalletsUpdateActionResolver.Action.Unknown -> Unit } + } - if (previousSelectedWalletIndex != selectedWalletIndex) { - uiState = stateFactory.getSkeletonState(wallets = sourceList, selectedWalletIndex = selectedWalletIndex) + private fun loadAndUpdateState(index: Int) { + uiState = stateFactory.getSkeletonState(wallets = wallets, selectedWalletIndex = index) - getContentItemsUpdates(index = selectedWalletIndex) + getContentItemsUpdates(index = index) + } + + private fun deleteWalletAndUpdateState(action: WalletsUpdateActionResolver.Action.DeleteWallet) { + val cacheState = WalletStateCache.getState(userWalletId = action.selectedWalletId) + if (cacheState != null) { + uiState = stateFactory.getStateWithoutDeletedWallet(cacheState, action) + + if (cacheState.isLoadingState()) { + getContentItemsUpdates(action.selectedWalletIndex) + } + } else { + loadAndUpdateState(index = action.selectedWalletIndex) } } @@ -181,23 +206,56 @@ internal class WalletViewModel @Inject constructor( } override fun onScanCardClick() { + viewModelScope.launch(dispatchers.io) { + scanCardProcessor.scan() + .doOnSuccess { + // If card's public key is null then user wallet will be null + val userWallet = UserWalletBuilder(scanResponse = it).build() + + if (userWallet != null) { + saveWalletUseCase(userWallet = userWallet, canOverride = false) + } + } + } + } + + override fun onScanCardNotificationClick() { + scanToUpdateSelectedWallet( + onSuccessSave = { + // Reload currencies with missed derivation + fetchTokenListUseCase(userWalletId = it.walletId) + }, + ) + } + + override fun onScanToUnlockWalletClick() { + scanToUpdateSelectedWallet() + } + + private fun scanToUpdateSelectedWallet(onSuccessSave: suspend (UserWallet) -> Unit = {}) { + val state = uiState as? WalletState.ContentState ?: return + val prevRequestPolicyStatus = getBiometricsStatusUseCase() // Update access the code policy according access code saving status setAccessCodeRequestPolicyUseCase(isBiometricsRequestPolicy = getAccessCodeSavingStatusUseCase()) viewModelScope.launch(dispatchers.io) { - scanCardProcessor.scan(allowsRequestAccessCodeFromRepository = true) + scanCardProcessor.scan( + cardId = getWallet(state.walletsListConfig.selectedWalletIndex).cardId, + allowsRequestAccessCodeFromRepository = true, + ) .doOnSuccess { // If card's public key is null then user wallet will be null val userWallet = UserWalletBuilder(scanResponse = it).build() if (userWallet != null) { - saveWalletUseCase(userWallet) + saveWalletUseCase(userWallet = userWallet, canOverride = true) .onLeft { // Rollback policy if card saving was failed setAccessCodeRequestPolicyUseCase(prevRequestPolicyStatus) } + .onRight { onSuccessSave(userWallet) } } else { // Rollback policy if card saving was failed setAccessCodeRequestPolicyUseCase(prevRequestPolicyStatus) @@ -245,10 +303,7 @@ internal class WalletViewModel @Inject constructor( } override fun onWalletChange(index: Int) { - val state = requireNotNull(uiState as? WalletState.ContentState) { - "Impossible to change wallet if state isn't WalletState.ContentState" - } - + val state = uiState as? WalletState.ContentState ?: return if (state.walletsListConfig.selectedWalletIndex == index) return viewModelScope.launch(dispatchers.io) { @@ -256,15 +311,26 @@ internal class WalletViewModel @Inject constructor( } val cacheState = WalletStateCache.getState(userWalletId = state.walletsListConfig.wallets[index].id) - if (cacheState != null) { - uiState = if (cacheState is WalletState.ContentState) { - cacheState.copySealed( - walletsListConfig = state.walletsListConfig.copy(selectedWalletIndex = index), - pullToRefreshConfig = state.pullToRefreshConfig.copy(isRefreshing = false), - ) - } else { - cacheState - } + if (cacheState != null && cacheState !is WalletLockedState) { + uiState = cacheState.copySealed( + walletsListConfig = state.walletsListConfig.copy( + selectedWalletIndex = index, + wallets = state.walletsListConfig.wallets + .mapIndexed { mapIndex, currentWallet -> + val cacheWallet = cacheState.walletsListConfig.wallets.getOrNull(mapIndex) + + if (currentWallet is WalletCardState.Loading && cacheWallet != null && + cacheWallet.isLoaded() + ) { + cacheWallet + } else { + currentWallet + } + } + .toImmutableList(), + ), + pullToRefreshConfig = cacheState.pullToRefreshConfig.copy(isRefreshing = false), + ) if (cacheState.isLoadingState()) { getContentItemsUpdates(index) @@ -275,6 +341,10 @@ internal class WalletViewModel @Inject constructor( } } + private fun WalletCardState.isLoaded(): Boolean { + return this !is WalletCardState.Loading && this !is WalletCardState.LockedContent + } + override fun onRefreshSwipe() { val selectedWalletIndex = (uiState as? WalletState.ContentState) ?.walletsListConfig @@ -436,10 +506,11 @@ internal class WalletViewModel @Inject constructor( } override fun onDeleteClick(userWalletId: UserWalletId) { + val state = uiState as? WalletState.ContentState ?: return + viewModelScope.launch(dispatchers.io) { val either = deleteWalletUseCase(userWalletId) - val state = requireNotNull(uiState as? WalletState.ContentState) if (state.walletsListConfig.wallets.size <= 1 && either.isRight()) onBackClick() } } @@ -611,9 +682,10 @@ internal class WalletViewModel @Inject constructor( private fun WalletState.isLoadingState(): Boolean { // Check the base components - if (this is WalletState.ContentState) { - walletsListConfig.wallets[walletsListConfig.selectedWalletIndex] is WalletCardState.Loading || - notifications.isEmpty() + if (this is WalletState.ContentState && + walletsListConfig.wallets[walletsListConfig.selectedWalletIndex] is WalletCardState.Loading + ) { + return true } // Check the special components diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletsUpdateActionResolver.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletsUpdateActionResolver.kt new file mode 100644 index 0000000000..dbcfee3f06 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletsUpdateActionResolver.kt @@ -0,0 +1,161 @@ +package com.tangem.feature.wallet.presentation.wallet.viewmodels + +import com.tangem.common.Provider +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.domain.wallets.usecase.GetSelectedWalletUseCase +import com.tangem.feature.wallet.presentation.wallet.state.WalletLockedState +import com.tangem.feature.wallet.presentation.wallet.state.WalletState +import com.tangem.feature.wallet.presentation.wallet.state.components.WalletCardState + +/** + * Resolver that determines which update action will be performed + * + * @property currentStateProvider current state provider + * @property getSelectedWalletUseCase use case that returns selected wallet + */ +internal class WalletsUpdateActionResolver( + private val currentStateProvider: Provider, + private val getSelectedWalletUseCase: GetSelectedWalletUseCase, +) { + + fun resolve(wallets: List): Action { + val selectedWallet = wallets.getSelectedWallet() + + return when (val state = currentStateProvider()) { + is WalletState.Initial -> { + Action.InitialWallets( + selectedWalletIndex = wallets.indexOfWallet(id = selectedWallet.walletId), + ) + } + is WalletState.ContentState -> { + getActionToUpdateContent(state = state, wallets = wallets, selectedWallet = selectedWallet) + } + } + } + + private fun List.getSelectedWallet(): UserWallet { + val hasUnlockedWallet = any { !it.isLocked } + return if (hasUnlockedWallet) { + val selectedWalletId = getSelectedWalletUseCase().fold(ifLeft = ::error, ifRight = UserWallet::walletId) + + firstOrNull { it.walletId == selectedWalletId } + ?: error("Wallets don't contain a wallet with id: $selectedWalletId") + } else { + lastOrNull() ?: error("Wallets is empty") + } + } + + private fun getActionToUpdateContent( + state: WalletState.ContentState, + wallets: List, + selectedWallet: UserWallet, + ): Action { + return if (isWalletsCountChanged(state, wallets)) { + getActionToChangeWallets(state = state, wallets = wallets, selectedWallet = selectedWallet) + } else { + getActionToUpdateCurrentWallet(state = state, wallets = wallets, selectedWallet = selectedWallet) + } + } + + private fun isWalletsCountChanged(state: WalletState.ContentState, wallets: List): Boolean { + val prevWalletsSize = state.walletsListConfig.wallets.size + val walletsSize = wallets.size + + return prevWalletsSize != walletsSize + } + + private fun getActionToChangeWallets( + state: WalletState.ContentState, + wallets: List, + selectedWallet: UserWallet, + ): Action { + val prevWalletsSize = state.walletsListConfig.wallets.size + + return when { + prevWalletsSize > wallets.size -> { + Action.DeleteWallet( + selectedWalletId = selectedWallet.walletId, + selectedWalletIndex = wallets.indexOfWallet(id = selectedWallet.walletId), + deletedWalletId = state.walletsListConfig.wallets.getDeletedWalletId(wallets), + ) + } + prevWalletsSize < wallets.size -> { + Action.AddWallet( + selectedWalletIndex = wallets.indexOfWallet(id = selectedWallet.walletId), + ) + } + else -> Action.Unknown + } + } + + private fun List.getDeletedWalletId(wallets: List): UserWalletId { + return this + .map(WalletCardState::id) + .firstOrNull { !wallets.map(UserWallet::walletId).contains(it) } + ?: error("Deleted wallet id is not found. Wallets contains all previous wallets ids") + } + + private fun getActionToUpdateCurrentWallet( + state: WalletState.ContentState, + wallets: List, + selectedWallet: UserWallet, + ): Action { + val selectedWalletName = selectedWallet.name + + if (state.getPrevSelectedWalletName() != selectedWalletName) { + return Action.UpdateWalletName(selectedWalletName) + } + + if (state is WalletLockedState && !selectedWallet.isLocked) { + return Action.UnlockWallet( + selectedWalletIndex = wallets.indexOfWallet(id = selectedWallet.walletId), + selectedWallet = selectedWallet, + unlockedWallets = wallets.filterNot(UserWallet::isLocked), + ) + } + + return Action.Unknown + } + + private fun WalletState.ContentState.getPrevSelectedWalletName(): String { + val prevSelectedWalletIndex = walletsListConfig.selectedWalletIndex + val prevSelectedWallet = walletsListConfig.wallets.getOrNull(prevSelectedWalletIndex) + ?: error("Previous selected wallet is not found") + + return prevSelectedWallet.title + } + + private fun List.indexOfWallet(id: UserWalletId): Int { + val selectedIndex = indexOfFirst { it.walletId == id } + + return if (selectedIndex == -1) { + error("Wallets don't contain a wallet with id: $id") + } else { + selectedIndex + } + } + + sealed class Action { + + data class InitialWallets(val selectedWalletIndex: Int) : Action() + + data class UpdateWalletName(val name: String) : Action() + + data class UnlockWallet( + val selectedWalletIndex: Int, + val selectedWallet: UserWallet, + val unlockedWallets: List, + ) : Action() + + data class DeleteWallet( + val selectedWalletId: UserWalletId, + val selectedWalletIndex: Int, + val deletedWalletId: UserWalletId, + ) : Action() + + data class AddWallet(val selectedWalletIndex: Int) : Action() + + object Unknown : Action() + } +} \ No newline at end of file From 25dc4f27e59aa71aee5eb1580a2ab573d581e723 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 7 Sep 2023 18:40:38 +0300 Subject: [PATCH 020/242] Updated on 2026-08-14 --- .../ui/components/buttons/actions/Actions.kt | 5 +- .../tokens/model/CryptoCurrencyStatus.kt | 18 ++- .../presentation/common/WalletPreviewData.kt | 9 +- .../common/component/TokenItem.kt | 47 ++++-- .../component/token/TokenCryptoInfoBlock.kt | 3 +- .../common/state/TokenItemState.kt | 6 +- .../CryptoCurrencyToIconStateConverter.kt | 24 +-- .../organizetokens/OrganizeTokensScreen.kt | 145 ++++++++++-------- .../OrganizeTokensStateHolder.kt | 2 +- .../organizetokens/OrganizeTokensViewModel.kt | 19 ++- .../organizetokens/model/DraggableItem.kt | 9 +- .../model/OrganizeTokensListState.kt | 2 +- .../model/OrganizeTokensState.kt | 2 +- .../utils/CryptoCurrenciesIdsResolver.kt | 2 +- .../utils/common/DraggableItemOperations.kt | 4 +- .../utils/common/DraggableItemsOperations.kt | 57 ++++++- .../OrganiseTokensListStateOperations.kt | 3 +- .../CryptoCurrencyToDraggableItemConverter.kt | 11 +- .../NetworkGroupToDraggableItemsConverter.kt | 2 +- .../utils/dnd/DragAndDropAdapter.kt | 63 +++----- .../utils/dnd/DraggableGroupsOperations.kt | 61 +------- .../state/components/WalletTokensListState.kt | 36 ++++- .../factory/WalletRefreshStateConverter.kt | 17 +- .../state/factory/WalletStateFactory.kt | 5 +- .../presentation/wallet/ui/WalletScreen.kt | 15 +- .../MultiCurrencyOrganizeButton.kt | 22 ++- .../multicurrency/OrganizeTokensButton.kt | 29 ---- ...ryptoCurrencyStatusToTokenItemConverter.kt | 4 +- .../wallet/utils/TokenListErrorConverter.kt | 2 +- .../utils/TokenListToContentItemsConverter.kt | 56 ++++--- 30 files changed, 364 insertions(+), 316 deletions(-) delete mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/multicurrency/OrganizeTokensButton.kt diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/buttons/actions/Actions.kt b/core/ui/src/main/java/com/tangem/core/ui/components/buttons/actions/Actions.kt index 2167ad385c..02cf8f05ba 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/buttons/actions/Actions.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/buttons/actions/Actions.kt @@ -20,7 +20,6 @@ import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider import com.tangem.core.ui.R import com.tangem.core.ui.components.SpacerW8 -import com.tangem.core.ui.components.buttons.common.* import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.res.TangemTheme @@ -99,7 +98,7 @@ private fun Button( val iconTint by animateColorAsState( targetValue = when { !config.enabled -> TangemTheme.colors.icon.informative - config.dimContent -> TangemTheme.colors.icon.secondary + config.dimContent -> TangemTheme.colors.icon.informative else -> TangemTheme.colors.icon.primary1 }, label = "Update tint color", @@ -117,7 +116,7 @@ private fun Button( val textColor by animateColorAsState( targetValue = when { !config.enabled -> TangemTheme.colors.text.disabled - config.dimContent -> TangemTheme.colors.text.secondary + config.dimContent -> TangemTheme.colors.text.tertiary else -> TangemTheme.colors.text.primary1 }, label = "Update text color", diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/CryptoCurrencyStatus.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/CryptoCurrencyStatus.kt index 1ccd0e1e61..5f4f6aa6d0 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/CryptoCurrencyStatus.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/CryptoCurrencyStatus.kt @@ -21,8 +21,10 @@ data class CryptoCurrencyStatus( /** * Represents the various states a token can have, encapsulating different information based on the state. + * + * @property isError Indicates whether this status represents an error status. */ - sealed class Status { + sealed class Status(val isError: Boolean) { /** The amount of the token. */ open val amount: BigDecimal? = null @@ -47,16 +49,16 @@ data class CryptoCurrencyStatus( } /** Represents the Loading state of a token, typically while fetching its details. */ - object Loading : Status() + object Loading : Status(isError = false) /** Represents a state where the token is not reachable. */ - object Unreachable : Status() + object Unreachable : Status(isError = true) /** Represents a state where the token's derivation is missed. */ - object MissedDerivation : Status() + object MissedDerivation : Status(isError = true) /** Represents a state where there is no account associated with the token. */ - object NoAccount : Status() + object NoAccount : Status(isError = false) /** * Represents a Loaded state of a token with complete information. @@ -77,7 +79,7 @@ data class CryptoCurrencyStatus( override val hasCurrentNetworkTransactions: Boolean, override val pendingTransactions: Set, override val networkAddress: NetworkAddress?, - ) : Status() + ) : Status(isError = false) /** * Represents a Custom state of a token, typically used for user-defined tokens. @@ -98,7 +100,7 @@ data class CryptoCurrencyStatus( override val hasCurrentNetworkTransactions: Boolean, override val pendingTransactions: Set, override val networkAddress: NetworkAddress?, - ) : Status() + ) : Status(isError = false) /** * Represents a state where the token is available, but there is no current quote available for it. @@ -113,5 +115,5 @@ data class CryptoCurrencyStatus( override val hasCurrentNetworkTransactions: Boolean, override val pendingTransactions: Set, override val networkAddress: NetworkAddress?, - ) : Status() + ) : Status(isError = false) } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/WalletPreviewData.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/WalletPreviewData.kt index de4226ceca..7562581a91 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/WalletPreviewData.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/WalletPreviewData.kt @@ -8,6 +8,7 @@ import com.tangem.core.ui.components.transactions.state.TransactionState import com.tangem.core.ui.components.transactions.state.TxHistoryState import com.tangem.core.ui.event.consumed import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.res.TangemColorPalette import com.tangem.domain.wallets.models.UserWalletId import com.tangem.feature.wallet.presentation.common.state.TokenItemState @@ -164,7 +165,7 @@ internal object WalletPreviewData { id = UUID.randomUUID().toString(), icon = tokenIconState, name = "Polygon", - fiatAmount = "3 172,14 $", + info = stringReference(value = "3 172,14 $"), ) } @@ -232,7 +233,7 @@ internal object WalletPreviewData { ) } - val divider = DraggableItem.GroupPlaceholder(id = "divider_$networkNumber") + val divider = DraggableItem.Placeholder(id = "divider_$networkNumber") buildList { add(group) @@ -267,7 +268,7 @@ internal object WalletPreviewData { ), dndConfig = OrganizeTokensState.DragAndDropConfig( onItemDragged = { _, _ -> }, - onDragStart = {}, + onItemDragStart = {}, canDragItemOver = { _, _ -> false }, onItemDragEnd = {}, ), @@ -365,7 +366,7 @@ internal object WalletPreviewData { ), ), ), - onOrganizeTokensClick = {}, + organizeTokensButton = WalletTokensListState.OrganizeTokensButtonState.Visible(isEnabled = true, {}), ), pullToRefreshConfig = WalletPullToRefreshConfig( isRefreshing = false, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/TokenItem.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/TokenItem.kt index 18cc14cbe1..14b4a2065a 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/TokenItem.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/TokenItem.kt @@ -3,11 +3,15 @@ package com.tangem.feature.wallet.presentation.common.component import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background import androidx.compose.foundation.combinedClickable -import androidx.compose.foundation.layout.* +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.defaultMinSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable import androidx.compose.runtime.Stable import androidx.compose.runtime.remember import androidx.compose.ui.Modifier +import androidx.compose.ui.composed import androidx.compose.ui.hapticfeedback.HapticFeedbackType import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.tooling.preview.Preview @@ -25,27 +29,13 @@ import com.tangem.feature.wallet.presentation.common.component.token.icon.TokenI import com.tangem.feature.wallet.presentation.common.state.TokenItemState import org.burnoutcrew.reorderable.ReorderableLazyListState -@OptIn(ExperimentalFoundationApi::class) @Composable internal fun TokenItem( state: TokenItemState, modifier: Modifier = Modifier, reorderableTokenListState: ReorderableLazyListState? = null, ) { - val hapticFeedback = LocalHapticFeedback.current - val containerModifier: Modifier = remember(state) { - when (state) { - is TokenItemState.Content -> modifier.combinedClickable( - onClick = state.onItemClick, - onLongClick = { - hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress) - state.onItemLongClick() - }, - ) - else -> modifier - } - } - BaseContainer(modifier = containerModifier) { + BaseContainer(modifier = modifier.tokenClickable(state)) { val (iconRef, cryptoInfoRef, fiatInfoRef) = createRefs() TokenIcon( @@ -108,6 +98,31 @@ private fun Modifier.constrainAsOptionsItem(scope: ConstraintLayoutScope, ref: C } } +@OptIn(ExperimentalFoundationApi::class) +private fun Modifier.tokenClickable(state: TokenItemState): Modifier = composed { + when (state) { + is TokenItemState.Content -> { + val hapticFeedback = LocalHapticFeedback.current + val onLongClick = remember(state) { + { + hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress) + state.onItemLongClick() + } + } + + this.combinedClickable( + onClick = state.onItemClick, + onLongClick = onLongClick, + ) + } + is TokenItemState.Draggable, + is TokenItemState.Unreachable, + is TokenItemState.Loading, + is TokenItemState.Locked, + -> this + } +} + // region preview @Preview @Composable diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/token/TokenCryptoInfoBlock.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/token/TokenCryptoInfoBlock.kt index 40740ab8c7..f7dd28c4b6 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/token/TokenCryptoInfoBlock.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/token/TokenCryptoInfoBlock.kt @@ -10,6 +10,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.composed import androidx.compose.ui.res.painterResource import com.tangem.core.ui.components.RectangleShimmer +import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemTypography import com.tangem.feature.wallet.impl.R @@ -40,7 +41,7 @@ private fun ContentBlock(state: TokenItemState.ContentState, modifier: Modifier AmountText( amount = when (state) { is TokenItemState.Content -> if (state.tokenOptions is TokenOptionsState.Hidden) DOTS else state.amount - is TokenItemState.Draggable -> state.fiatAmount + is TokenItemState.Draggable -> state.info.resolveReference() is TokenItemState.Unreachable -> null }, ) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/state/TokenItemState.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/state/TokenItemState.kt index 16e58f8a87..a5da0a39f9 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/state/TokenItemState.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/state/TokenItemState.kt @@ -4,6 +4,7 @@ import androidx.annotation.DrawableRes import androidx.compose.runtime.Immutable import androidx.compose.ui.graphics.Color import com.tangem.core.ui.components.marketprice.PriceChangeConfig +import com.tangem.core.ui.extensions.TextReference /** Token item state */ @Immutable @@ -19,6 +20,7 @@ internal sealed interface TokenItemState { data class Locked(override val id: String) : TokenItemState /** Content state */ + @Immutable sealed class ContentState : TokenItemState { abstract val icon: IconState @@ -54,13 +56,13 @@ internal sealed interface TokenItemState { * @property id unique id * @property icon token icon state * @property name token name - * @property fiatAmount fiat amount of token + * @property info token info (e.g. fiat balance or status) */ data class Draggable( override val id: String, override val icon: IconState, override val name: String, - val fiatAmount: String, + val info: TextReference, ) : ContentState() /** diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/utils/CryptoCurrencyToIconStateConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/utils/CryptoCurrencyToIconStateConverter.kt index 6f8c7c9b05..33d33a28ac 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/utils/CryptoCurrencyToIconStateConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/utils/CryptoCurrencyToIconStateConverter.kt @@ -3,28 +3,32 @@ package com.tangem.feature.wallet.presentation.common.utils import com.tangem.core.ui.extensions.getTintForTokenIcon import com.tangem.core.ui.extensions.networkIconResId import com.tangem.core.ui.extensions.tryGetBackgroundForTokenIcon +import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.models.CryptoCurrency import com.tangem.feature.wallet.presentation.common.state.TokenItemState import com.tangem.utils.converter.Converter -internal class CryptoCurrencyToIconStateConverter : Converter { +internal class CryptoCurrencyToIconStateConverter : Converter { - override fun convert(value: CryptoCurrency): TokenItemState.IconState { - return when (value) { - is CryptoCurrency.Coin -> getIconStateForCoin(value) - is CryptoCurrency.Token -> getIconStateForToken(value) + override fun convert(value: CryptoCurrencyStatus): TokenItemState.IconState { + return when (val currency = value.currency) { + is CryptoCurrency.Coin -> getIconStateForCoin(currency, value.value.isError) + is CryptoCurrency.Token -> getIconStateForToken(currency, value.value.isError) } } - private fun getIconStateForCoin(coin: CryptoCurrency.Coin): TokenItemState.IconState.CoinIcon { + private fun getIconStateForCoin( + coin: CryptoCurrency.Coin, + isUnreachable: Boolean, + ): TokenItemState.IconState.CoinIcon { return TokenItemState.IconState.CoinIcon( url = coin.iconUrl, fallbackResId = coin.networkIconResId, - isGrayscale = coin.network.isTestnet, + isGrayscale = coin.network.isTestnet || isUnreachable, ) } - private fun getIconStateForToken(token: CryptoCurrency.Token): TokenItemState.IconState { + private fun getIconStateForToken(token: CryptoCurrency.Token, isErrorStatus: Boolean): TokenItemState.IconState { val background = token.tryGetBackgroundForTokenIcon() val tint = getTintForTokenIcon(background) @@ -33,13 +37,13 @@ internal class CryptoCurrencyToIconStateConverter : Converter item.id }, ) { index, item -> val onDragStart = remember(item) { - { dndConfig.onDragStart(item) } + { dndConfig.onItemDragStart(item) } } DraggableItem( @@ -132,7 +133,6 @@ private fun TokenList( } } -@OptIn(ExperimentalFoundationApi::class) @Composable private fun LazyItemScope.DraggableItem( index: Int, @@ -140,15 +140,18 @@ private fun LazyItemScope.DraggableItem( reorderableState: ReorderableLazyListState, onDragStart: () -> Unit, ) { + var isDragging by remember { + mutableStateOf(value = false) + } + + val itemModifier = Modifier.applyShapeAndShadow(item.roundingMode, item.showShadow) + ReorderableItem( - defaultDraggingModifier = Modifier.animateItemPlacement( - animationSpec = tween(easing = LinearOutSlowInEasing), - ), - state = reorderableState, + reorderableState = reorderableState, index = index, key = item.id, - ) { isDragging -> - val itemModifier = Modifier.applyShapeAndShadow(item.roundingMode, item.showShadow) + ) { isItemDragging -> + isDragging = isItemDragging when (item) { is DraggableItem.GroupHeader -> DraggableNetworkGroupItem( @@ -162,10 +165,12 @@ private fun LazyItemScope.DraggableItem( reorderableTokenListState = reorderableState, ) // Should be presented in the list but remain invisible - is DraggableItem.GroupPlaceholder -> Box(modifier = Modifier.fillMaxWidth()) + is DraggableItem.Placeholder -> Box(modifier = Modifier.fillMaxWidth()) } + } - LaunchedEffect(isDragging) { + DisposableEffect(isDragging) { + onDispose { if (isDragging) { onDragStart() } @@ -288,57 +293,71 @@ private fun Actions(config: OrganizeTokensState.ActionsConfig, modifier: Modifie } } -private fun Modifier.applyShapeAndShadow(roundingMode: DraggableItem.RoundingMode, showShadow: Boolean): Modifier = - composed { +private fun Modifier.applyShapeAndShadow(roundingMode: DraggableItem.RoundingMode, showShadow: Boolean): Modifier { + return composed { val radius by animateDpAsState( - targetValue = if (roundingMode !is DraggableItem.RoundingMode.None) { - TangemTheme.dimens.radius16 - } else { - TangemTheme.dimens.radius0 + targetValue = when (roundingMode) { + is DraggableItem.RoundingMode.None -> TangemTheme.dimens.radius0 + is DraggableItem.RoundingMode.All -> TangemTheme.dimens.radius12 + is DraggableItem.RoundingMode.Bottom, + is DraggableItem.RoundingMode.Top, + -> TangemTheme.dimens.radius16 }, label = "item_shape_radius", ) - val shape = when (roundingMode) { - is DraggableItem.RoundingMode.None -> RectangleShape - is DraggableItem.RoundingMode.Top -> RoundedCornerShape( - topStart = radius, - topEnd = radius, - ) - is DraggableItem.RoundingMode.Bottom -> RoundedCornerShape( - bottomStart = radius, - bottomEnd = radius, - ) - is DraggableItem.RoundingMode.All -> RoundedCornerShape( - size = radius, - ) - } - - val paddingValue = TangemTheme.dimens.spacing4 - val padding = if (roundingMode.showGap) { - when (roundingMode) { - is DraggableItem.RoundingMode.None -> null - is DraggableItem.RoundingMode.All -> PaddingValues(vertical = paddingValue) - is DraggableItem.RoundingMode.Top -> PaddingValues(top = paddingValue) - is DraggableItem.RoundingMode.Bottom -> PaddingValues(bottom = paddingValue) - } - } else { - null - } + val elevation by animateDpAsState( + targetValue = if (showShadow) { + TangemTheme.dimens.elevation8 + } else { + TangemTheme.dimens.elevation0 + }, + label = "item_elevation", + ) this - .let { - if (padding != null) { - it.padding(padding) - } else { - it - } - } + .padding(paddingValues = getItemGap(roundingMode)) .shadow( - elevation = if (showShadow) TangemTheme.dimens.elevation12 else TangemTheme.dimens.elevation0, - shape = shape, + elevation = elevation, + shape = getItemShape(roundingMode, radius), clip = true, ) } +} + +@Composable +@ReadOnlyComposable +private fun getItemGap(roundingMode: DraggableItem.RoundingMode): PaddingValues { + val paddingValue = TangemTheme.dimens.spacing4 + + return if (roundingMode.showGap) { + when (roundingMode) { + is DraggableItem.RoundingMode.None -> PaddingValues(all = 0.dp) + is DraggableItem.RoundingMode.All -> PaddingValues(vertical = paddingValue) + is DraggableItem.RoundingMode.Top -> PaddingValues(top = paddingValue) + is DraggableItem.RoundingMode.Bottom -> PaddingValues(bottom = paddingValue) + } + } else { + PaddingValues(all = 0.dp) + } +} + +@Stable +private fun getItemShape(roundingMode: DraggableItem.RoundingMode, radius: Dp): Shape { + return when (roundingMode) { + is DraggableItem.RoundingMode.None -> RectangleShape + is DraggableItem.RoundingMode.Top -> RoundedCornerShape( + topStart = radius, + topEnd = radius, + ) + is DraggableItem.RoundingMode.Bottom -> RoundedCornerShape( + bottomStart = radius, + bottomEnd = radius, + ) + is DraggableItem.RoundingMode.All -> RoundedCornerShape( + size = radius, + ) + } +} // region Preview diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensStateHolder.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensStateHolder.kt index ec8c8655c2..4552ef0050 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensStateHolder.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensStateHolder.kt @@ -110,7 +110,7 @@ internal class OrganizeTokensStateHolder( ), dndConfig = OrganizeTokensState.DragAndDropConfig( onItemDragged = dragAndDropIntents::onItemDragged, - onDragStart = dragAndDropIntents::onItemDraggingStart, + onItemDragStart = dragAndDropIntents::onItemDraggingStart, onItemDragEnd = dragAndDropIntents::onItemDraggingEnd, canDragItemOver = dragAndDropIntents::canDragItemOver, ), diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensViewModel.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensViewModel.kt index 5c24a976b3..e490a4a9b6 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensViewModel.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensViewModel.kt @@ -20,13 +20,14 @@ import com.tangem.feature.wallet.presentation.organizetokens.utils.common.disabl import com.tangem.feature.wallet.presentation.organizetokens.utils.dnd.DragAndDropAdapter import com.tangem.feature.wallet.presentation.router.InnerWalletRouter import com.tangem.feature.wallet.presentation.router.WalletRoute +import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import javax.inject.Inject +@Suppress("LongParameterList") @HiltViewModel internal class OrganizeTokensViewModel @Inject constructor( private val getTokenListUseCase: GetTokenListUseCase, @@ -34,6 +35,7 @@ internal class OrganizeTokensViewModel @Inject constructor( private val toggleTokenListSortingUseCase: ToggleTokenListSortingUseCase, private val applyTokenListSortingUseCase: ApplyTokenListSortingUseCase, private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, + private val dispatchers: CoroutineDispatcherProvider, savedStateHandle: SavedStateHandle, ) : ViewModel(), OrganizeTokensIntents { @@ -43,7 +45,6 @@ internal class OrganizeTokensViewModel @Inject constructor( private val dragAndDropAdapter = DragAndDropAdapter( listStateProvider = Provider { uiState.value.itemsState }, - scope = viewModelScope, ) private val stateHolder = OrganizeTokensStateHolder( @@ -72,7 +73,7 @@ internal class OrganizeTokensViewModel @Inject constructor( } override fun onSortClick() { - viewModelScope.launch(Dispatchers.Default) { + viewModelScope.launch(dispatchers.default) { val list = tokenList ?: return@launch toggleTokenListSortingUseCase(list).fold( @@ -86,7 +87,7 @@ internal class OrganizeTokensViewModel @Inject constructor( } override fun onGroupClick() { - viewModelScope.launch(Dispatchers.Default) { + viewModelScope.launch(dispatchers.default) { val list = tokenList ?: return@launch toggleTokenListGroupingUseCase(list).fold( @@ -100,7 +101,7 @@ internal class OrganizeTokensViewModel @Inject constructor( } override fun onApplyClick() { - viewModelScope.launch(Dispatchers.Default) { + viewModelScope.launch(dispatchers.default) { stateHolder.updateStateToDisplayProgress() val listState = uiState.value.itemsState @@ -117,7 +118,9 @@ internal class OrganizeTokensViewModel @Inject constructor( ifLeft = stateHolder::updateStateWithError, ifRight = { stateHolder.updateStateToHideProgress() - withContext(Dispatchers.Main) { router.popBackStack() } + withContext( + dispatchers.main, + ) { router.popBackStack() } }, ) } @@ -128,9 +131,9 @@ internal class OrganizeTokensViewModel @Inject constructor( } private fun bootstrapTokenList() { - viewModelScope.launch(Dispatchers.Default) { + viewModelScope.launch(dispatchers.default) { val maybeTokenList = getTokenListUseCase(userWalletId) - .first { it.getOrNull()?.totalFiatBalance is TokenList.FiatBalance.Loaded } + .first { it.getOrNull()?.totalFiatBalance !is TokenList.FiatBalance.Loading } maybeTokenList.fold( ifLeft = stateHolder::updateStateWithError, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/model/DraggableItem.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/model/DraggableItem.kt index 5d367c0436..3424a965b6 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/model/DraggableItem.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/model/DraggableItem.kt @@ -51,12 +51,11 @@ internal sealed class DraggableItem { } /** - * Helper item used to detect possible positions where a network group can be placed. - * Used only on [OrganizeTokensListState.GroupedByNetwork] and placed between network groups. + * Helper item used to detect possible positions where a draggable item can be placed. * * @property id ID of the placeholder * */ - data class GroupPlaceholder( + data class Placeholder( override val id: String, ) : DraggableItem() { override val showShadow: Boolean = false @@ -109,7 +108,7 @@ internal sealed class DraggableItem { * @return updated [DraggableItem] * */ fun updateRoundingMode(mode: RoundingMode): DraggableItem = when (this) { - is GroupPlaceholder -> this + is Placeholder -> this is GroupHeader -> this.copy(roundingMode = mode) is Token -> this.copy(roundingMode = mode) } @@ -122,7 +121,7 @@ internal sealed class DraggableItem { * @return updated [DraggableItem] * */ fun updateShadowVisibility(show: Boolean): DraggableItem = when (this) { - is GroupPlaceholder -> this + is Placeholder -> this is GroupHeader -> this.copy(showShadow = show) is Token -> this.copy(showShadow = show) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/model/OrganizeTokensListState.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/model/OrganizeTokensListState.kt index 3f87cab410..5d16b5035c 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/model/OrganizeTokensListState.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/model/OrganizeTokensListState.kt @@ -13,7 +13,7 @@ internal sealed class OrganizeTokensListState { ) : OrganizeTokensListState() data class Ungrouped( - override val items: PersistentList, + override val items: PersistentList, ) : OrganizeTokensListState() object Empty : OrganizeTokensListState() { diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/model/OrganizeTokensState.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/model/OrganizeTokensState.kt index 860c4c5b24..a1ae0ff965 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/model/OrganizeTokensState.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/model/OrganizeTokensState.kt @@ -33,6 +33,6 @@ internal data class OrganizeTokensState( val onItemDragged: (ItemPosition, ItemPosition) -> Unit, val canDragItemOver: (ItemPosition, ItemPosition) -> Boolean, val onItemDragEnd: () -> Unit, - val onDragStart: (DraggableItem) -> Unit, + val onItemDragStart: (DraggableItem) -> Unit, ) } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/CryptoCurrenciesIdsResolver.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/CryptoCurrenciesIdsResolver.kt index 71690752fb..5c7d1de901 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/CryptoCurrenciesIdsResolver.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/CryptoCurrenciesIdsResolver.kt @@ -11,7 +11,7 @@ internal class CryptoCurrenciesIdsResolver { val draggableTokens = when (listState) { is OrganizeTokensListState.Empty -> return emptyList() is OrganizeTokensListState.GroupedByNetwork -> listState.items.filterIsInstance() - is OrganizeTokensListState.Ungrouped -> listState.items + is OrganizeTokensListState.Ungrouped -> listState.items.filterIsInstance() } val currenciesStatuses = when (tokenList) { is TokenList.GroupedByNetwork -> tokenList.groups.flatMap { it.currencies } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/common/DraggableItemOperations.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/common/DraggableItemOperations.kt index acfcac13b7..1db8f4356d 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/common/DraggableItemOperations.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/common/DraggableItemOperations.kt @@ -2,6 +2,6 @@ package com.tangem.feature.wallet.presentation.organizetokens.utils.common import com.tangem.feature.wallet.presentation.organizetokens.model.DraggableItem -internal fun getGroupPlaceholder(index: Int): DraggableItem.GroupPlaceholder { - return DraggableItem.GroupPlaceholder(id = "placeholder_${index.inc()}") +internal fun getGroupPlaceholder(index: Int): DraggableItem.Placeholder { + return DraggableItem.Placeholder(id = "placeholder_${index.inc()}") } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/common/DraggableItemsOperations.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/common/DraggableItemsOperations.kt index fee7cb1033..7a0beac577 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/common/DraggableItemsOperations.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/common/DraggableItemsOperations.kt @@ -3,20 +3,22 @@ package com.tangem.feature.wallet.presentation.organizetokens.utils.common import com.tangem.feature.wallet.presentation.organizetokens.model.DraggableItem internal fun List.uniteItems(): List { - val lastItemIndex = this.lastIndex + val items = prepareItems() + val lastItemIndex = items.lastIndex - return this.mapIndexed { index, item -> + return prepareItems().mapIndexed { index, item -> val mode = when (index) { - 0 -> DraggableItem.RoundingMode.Top() + // 1 index is used because the first item is always a placeholder, check `prepareItems()` function + 1 -> DraggableItem.RoundingMode.Top() lastItemIndex -> DraggableItem.RoundingMode.Bottom() else -> when (item) { + is DraggableItem.Placeholder -> DraggableItem.RoundingMode.None is DraggableItem.GroupHeader -> DraggableItem.RoundingMode.Top(showGap = true) - is DraggableItem.Token -> if (this[index + 1] is DraggableItem.GroupPlaceholder) { + is DraggableItem.Token -> if (items[index + 1] is DraggableItem.Placeholder) { DraggableItem.RoundingMode.Bottom(showGap = true) } else { DraggableItem.RoundingMode.None } - is DraggableItem.GroupPlaceholder -> DraggableItem.RoundingMode.None } } @@ -24,4 +26,49 @@ internal fun List.uniteItems(): List { .updateRoundingMode(mode) .updateShadowVisibility(show = false) } +} + +internal fun List.divideMovingItem(movingItem: DraggableItem): List { + val mutableList = this.toMutableList() + val listIterator = mutableList.listIterator() + + while (listIterator.hasNext()) { + val item = listIterator.next() + + if (item.id == movingItem.id) { + val dividedItem = movingItem + .updateRoundingMode(DraggableItem.RoundingMode.All()) + .updateShadowVisibility(show = true) + + listIterator.set(dividedItem) + break + } + } + + return mutableList +} + +/** + * !!! Workaround !!! + * + * We need to add a [DraggableItem.Placeholder] (since it's not draggable) as the first item of the list, because the + * [DND library](https://github.com/aclassen/ComposeReorderable) glitches when a user tries to drag the first item. + * + * @since 07.09.2023 + * */ +private fun List.prepareItems(): List { + val firstPlaceholderId = "initial_placeholder" + val items = this + + return mutableListOf().apply { + add(DraggableItem.Placeholder(firstPlaceholderId)) + + val itemsWithoutFirstPlaceholder = if (items.firstOrNull()?.id == firstPlaceholderId) { + items.drop(n = 1) + } else { + items + } + + addAll(itemsWithoutFirstPlaceholder) + } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/common/OrganiseTokensListStateOperations.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/common/OrganiseTokensListStateOperations.kt index 4c041f01fe..3b6cf65d8d 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/common/OrganiseTokensListStateOperations.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/common/OrganiseTokensListStateOperations.kt @@ -5,7 +5,6 @@ import com.tangem.feature.wallet.presentation.organizetokens.model.OrganizeToken import kotlinx.collections.immutable.PersistentList import kotlinx.collections.immutable.toPersistentList -@Suppress("UNCHECKED_CAST") internal inline fun OrganizeTokensListState.updateItems( update: (PersistentList) -> List, ): OrganizeTokensListState { @@ -13,7 +12,7 @@ internal inline fun OrganizeTokensListState.updateItems( return when (this) { is OrganizeTokensListState.GroupedByNetwork -> copy(items = updatedItems) - is OrganizeTokensListState.Ungrouped -> copy(items = updatedItems as PersistentList) + is OrganizeTokensListState.Ungrouped -> copy(items = updatedItems) is OrganizeTokensListState.Empty -> this } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/items/CryptoCurrencyToDraggableItemConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/items/CryptoCurrencyToDraggableItemConverter.kt index 9bad7941b4..8dad69b926 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/items/CryptoCurrencyToDraggableItemConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/items/CryptoCurrencyToDraggableItemConverter.kt @@ -1,9 +1,12 @@ package com.tangem.feature.wallet.presentation.organizetokens.utils.converter.items import com.tangem.common.Provider +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.feature.wallet.impl.R import com.tangem.feature.wallet.presentation.common.state.TokenItemState import com.tangem.feature.wallet.presentation.common.utils.CryptoCurrencyToIconStateConverter import com.tangem.feature.wallet.presentation.organizetokens.model.DraggableItem @@ -45,9 +48,13 @@ internal class CryptoCurrencyToDraggableItemConverter( return TokenItemState.Draggable( id = getTokenItemId(currency.id), - icon = iconStateConverter.convert(currency), + icon = iconStateConverter.convert(currencyStatus), name = currency.name, - fiatAmount = getFormattedFiatAmount(currencyStatus, appCurrency), + info = if (currencyStatus.value.isError) { + resourceReference(id = R.string.common_unreachable) + } else { + stringReference(getFormattedFiatAmount(currencyStatus, appCurrency)) + }, ) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/items/NetworkGroupToDraggableItemsConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/items/NetworkGroupToDraggableItemsConverter.kt index ddd09e1da9..e57076e83f 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/items/NetworkGroupToDraggableItemsConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/items/NetworkGroupToDraggableItemsConverter.kt @@ -36,6 +36,6 @@ internal class NetworkGroupToDraggableItemsConverter( ) private fun createTokens(group: NetworkGroup): List { - return itemConverter.convertList(group.currencies.toList()) + return itemConverter.convertList(group.currencies) } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/dnd/DragAndDropAdapter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/dnd/DragAndDropAdapter.kt index c73798c77e..45dcb02cee 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/dnd/DragAndDropAdapter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/dnd/DragAndDropAdapter.kt @@ -4,21 +4,17 @@ import com.tangem.common.Provider import com.tangem.feature.wallet.presentation.organizetokens.DragAndDropIntents import com.tangem.feature.wallet.presentation.organizetokens.model.DraggableItem import com.tangem.feature.wallet.presentation.organizetokens.model.OrganizeTokensListState +import com.tangem.feature.wallet.presentation.organizetokens.utils.common.divideMovingItem import com.tangem.feature.wallet.presentation.organizetokens.utils.common.uniteItems import com.tangem.feature.wallet.presentation.organizetokens.utils.common.updateItems import kotlinx.collections.immutable.mutate -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.channels.BufferOverflow -import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow -import kotlinx.coroutines.launch import org.burnoutcrew.reorderable.ItemPosition internal class DragAndDropAdapter( private val listStateProvider: Provider, - private val scope: CoroutineScope, ) : DragAndDropIntents { private val draggableGroupsOperations = DraggableGroupsOperations() @@ -37,9 +33,12 @@ internal class DragAndDropAdapter( get() = listStateFlowInternal override fun canDragItemOver(dragOver: ItemPosition, dragging: ItemPosition): Boolean { - val items = (currentListState as? OrganizeTokensListState.GroupedByNetwork) - ?.items - ?: return true // If ungrouped then item can be moved anywhere + val items = when (val listState = currentListState) { + is OrganizeTokensListState.GroupedByNetwork -> listState.items + is OrganizeTokensListState.Empty, + is OrganizeTokensListState.Ungrouped, + -> return true // If ungrouped then item can be moved anywhere + } val (dragOverItem, draggingItem) = findItemsToMove( items = items, @@ -54,7 +53,7 @@ internal class DragAndDropAdapter( return when (draggingItem) { is DraggableItem.GroupHeader -> checkCanMoveHeaderOver(dragOver, dragOverItem, items.lastIndex) is DraggableItem.Token -> checkCanMoveTokenOver(draggingItem, dragOverItem) - is DraggableItem.GroupPlaceholder -> false + is DraggableItem.Placeholder -> false } } @@ -64,11 +63,11 @@ internal class DragAndDropAdapter( updateListState { when (item) { - is DraggableItem.GroupPlaceholder -> items + is DraggableItem.Placeholder -> items is DraggableItem.GroupHeader -> draggableGroupsOperations.collapseGroup(items, item) is DraggableItem.Token -> when (this) { - is OrganizeTokensListState.GroupedByNetwork -> draggableGroupsOperations.divideGroups(items, item) - is OrganizeTokensListState.Ungrouped -> divideTokens(items, item) + is OrganizeTokensListState.GroupedByNetwork -> items.divideMovingItem(item) + is OrganizeTokensListState.Ungrouped -> items.divideMovingItem(item) is OrganizeTokensListState.Empty -> items } } @@ -76,21 +75,17 @@ internal class DragAndDropAdapter( } override fun onItemDraggingEnd() { - scope.launch(Dispatchers.IO) { - val draggingItem = currentDraggingItem ?: return@launch + val draggingItem = currentDraggingItem ?: return - delay(FINISH_DRAGGING_DELAY_MILLIS) - - updateListState { - when (draggingItem) { - is DraggableItem.GroupHeader -> draggableGroupsOperations.expandGroups(items) - is DraggableItem.Token -> items.uniteItems() - is DraggableItem.GroupPlaceholder -> items - } + updateListState { + when (draggingItem) { + is DraggableItem.GroupHeader -> draggableGroupsOperations.expandGroups(items) + is DraggableItem.Token -> items.uniteItems() + is DraggableItem.Placeholder -> items } - - currentDraggingItem = null } + + currentDraggingItem = null } override fun onItemDragged(from: ItemPosition, to: ItemPosition) = updateListState { @@ -137,7 +132,7 @@ internal class DragAndDropAdapter( return when { moveOverItemPosition.index == 0 -> true moveOverItemPosition.index == lastItemIndex -> true - moveOverItem is DraggableItem.GroupPlaceholder -> true + moveOverItem is DraggableItem.Placeholder -> true else -> false } } @@ -147,23 +142,7 @@ internal class DragAndDropAdapter( return when (moveOverItem) { is DraggableItem.GroupHeader -> false // Token item can not be moved to group item is DraggableItem.Token -> item.groupId == moveOverItem.groupId // Token item can not be moved over its group - is DraggableItem.GroupPlaceholder -> false + is DraggableItem.Placeholder -> false } } - - @Suppress("UNCHECKED_CAST") // Erased type - private fun divideTokens( - items: List, - movingItem: DraggableItem.Token, - ): List { - return items.map { token -> - token - .updateRoundingMode(DraggableItem.RoundingMode.All(showGap = true)) - .updateShadowVisibility(show = token.id == movingItem.id) - } as List - } - - private companion object { - const val FINISH_DRAGGING_DELAY_MILLIS = 200L - } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/dnd/DraggableGroupsOperations.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/dnd/DraggableGroupsOperations.kt index d28133195a..3018d416a5 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/dnd/DraggableGroupsOperations.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/dnd/DraggableGroupsOperations.kt @@ -1,6 +1,7 @@ package com.tangem.feature.wallet.presentation.organizetokens.utils.dnd import com.tangem.feature.wallet.presentation.organizetokens.model.DraggableItem +import com.tangem.feature.wallet.presentation.organizetokens.utils.common.divideMovingItem import com.tangem.feature.wallet.presentation.organizetokens.utils.common.getGroupPlaceholder import com.tangem.feature.wallet.presentation.organizetokens.utils.common.uniteItems @@ -20,7 +21,7 @@ internal class DraggableGroupsOperations { it is DraggableItem.Token && it.groupId == movingGroup.id } - return divideGroups(itemsWithoutGroupTokens, movingGroup) + return itemsWithoutGroupTokens.divideMovingItem(movingGroup) } fun expandGroups(items: List): List { @@ -45,62 +46,4 @@ internal class DraggableGroupsOperations { return expandedGroups } - - fun divideGroups(items: List, movingItem: DraggableItem): List { - val lastItemIndex = items.lastIndex - - return items.mapIndexed { index, item -> - when { - // Case when current item is the moving item - item.id == movingItem.id -> { - item - .updateRoundingMode(DraggableItem.RoundingMode.All(showGap = true)) - .updateShadowVisibility(show = true) - } - // Case when moving item is a token and current item is the group of the moving token - movingItem is DraggableItem.Token && item.id == movingItem.groupId -> { - item - .updateRoundingMode(DraggableItem.RoundingMode.All(showGap = true)) - .updateShadowVisibility(show = true) - } - // Case when both moving item and current item are tokens and belong to the same group - movingItem is DraggableItem.Token && - item is DraggableItem.Token && item.groupId == movingItem.groupId -> { - item - .updateRoundingMode(DraggableItem.RoundingMode.All(showGap = true)) - .updateShadowVisibility(show = false) - } - // Case when current item is the first item in the list - index == 0 -> { - item - .updateRoundingMode(DraggableItem.RoundingMode.Top()) - .updateShadowVisibility(show = false) - } - // Case when current item is the last item in the list - index == lastItemIndex -> { - item - .updateRoundingMode(DraggableItem.RoundingMode.Bottom()) - .updateShadowVisibility(show = false) - } - // Case when previous item is a GroupPlaceholder - items[index - 1] is DraggableItem.GroupPlaceholder -> { - item - .updateRoundingMode(DraggableItem.RoundingMode.Top(showGap = true)) - .updateShadowVisibility(show = false) - } - // Case when next item is a GroupPlaceholder - items[index + 1] is DraggableItem.GroupPlaceholder -> { - item - .updateRoundingMode(DraggableItem.RoundingMode.Bottom(showGap = true)) - .updateShadowVisibility(show = false) - } - // Default case when none of the above conditions are met - else -> { - item - .updateRoundingMode(DraggableItem.RoundingMode.None) - .updateShadowVisibility(show = false) - } - } - } - } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/components/WalletTokensListState.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/components/WalletTokensListState.kt index 8b973054f2..0cb0188b10 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/components/WalletTokensListState.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/components/WalletTokensListState.kt @@ -5,6 +5,7 @@ import com.tangem.feature.wallet.impl.R import com.tangem.feature.wallet.presentation.common.state.TokenItemState import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf +import javax.annotation.concurrent.Immutable /** * Wallet tokens list state @@ -20,11 +21,10 @@ internal sealed class WalletTokensListState { * Wallet content token list state * * @property items content items - * @property onOrganizeTokensClick lambda be invoked when organize tokens button is clicked */ sealed class ContentState( open val items: ImmutableList, - open val onOrganizeTokensClick: (() -> Unit)?, + open val organizeTokensButton: OrganizeTokensButtonState, ) : WalletTokensListState() /** @@ -37,18 +37,18 @@ internal sealed class WalletTokensListState { TokensListItemState.Token(state = TokenItemState.Loading(id = FIRST_LOADING_TOKEN_ID)), TokensListItemState.Token(state = TokenItemState.Loading(id = SECOND_LOADING_TOKEN_ID)), ), - ) : ContentState(items = items, onOrganizeTokensClick = null) + ) : ContentState(items = items, organizeTokensButton = OrganizeTokensButtonState.Hidden) /** * Content state * * @property items content items - * @property onOrganizeTokensClick lambda be invoked when organize tokens button is clicked + * @property organizeTokensButton represents the state of the 'Organize Tokens' button */ data class Content( override val items: ImmutableList, - override val onOrganizeTokensClick: (() -> Unit)?, - ) : ContentState(items, onOrganizeTokensClick) + override val organizeTokensButton: OrganizeTokensButtonState, + ) : ContentState(items, organizeTokensButton) /** Locked content state */ object Locked : ContentState( @@ -56,10 +56,32 @@ internal sealed class WalletTokensListState { TokensListItemState.NetworkGroupTitle(value = TextReference.Res(id = R.string.main_tokens)), TokensListItemState.Token(state = TokenItemState.Locked(id = LOCKED_TOKEN_ID)), ), - onOrganizeTokensClick = null, + organizeTokensButton = OrganizeTokensButtonState.Hidden, ) + /** + * Represents the state of the 'Organize Tokens' button. + */ + @Immutable + sealed class OrganizeTokensButtonState { + + /** Represents the state where the 'Organize Tokens' button is hidden. */ + object Hidden : OrganizeTokensButtonState() + + /** + * Represents the state where the 'Organize Tokens' button is visible. + * + * @property isEnabled Indicates if the button is enabled or not. + * @property onClick Callback to be executed when the button is clicked. + */ + data class Visible( + val isEnabled: Boolean, + val onClick: () -> Unit, + ) : OrganizeTokensButtonState() + } + /** Tokens list item state */ + @Immutable sealed interface TokensListItemState { /** diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletRefreshStateConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletRefreshStateConverter.kt index a7d44c70d8..2344194b89 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletRefreshStateConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletRefreshStateConverter.kt @@ -7,14 +7,12 @@ import com.tangem.feature.wallet.presentation.wallet.state.WalletState import com.tangem.feature.wallet.presentation.wallet.state.components.WalletManageButton import com.tangem.feature.wallet.presentation.wallet.state.components.WalletPullToRefreshConfig import com.tangem.feature.wallet.presentation.wallet.state.components.WalletTokensListState -import com.tangem.feature.wallet.presentation.wallet.viewmodels.WalletClickIntents import com.tangem.utils.converter.Converter import kotlinx.collections.immutable.PersistentList import kotlinx.collections.immutable.mutate internal class WalletRefreshStateConverter( private val currentStateProvider: Provider, - private val intents: WalletClickIntents, ) : Converter { override fun convert(value: Boolean): WalletState { @@ -77,16 +75,21 @@ internal class WalletRefreshStateConverter( } private fun WalletMultiCurrencyState.updateTokenListState(isRefreshing: Boolean): WalletTokensListState { - return when (val state = tokensListState) { + return when (val listState = tokensListState) { is WalletTokensListState.Content -> { - val onOrganizeTokensClick = if (isRefreshing) null else intents::onOrganizeTokensClick - - state.copy(onOrganizeTokensClick = onOrganizeTokensClick) + when (listState.organizeTokensButton) { + is WalletTokensListState.OrganizeTokensButtonState.Hidden -> listState + is WalletTokensListState.OrganizeTokensButtonState.Visible -> listState.copy( + organizeTokensButton = listState.organizeTokensButton.copy( + isEnabled = !isRefreshing, + ), + ) + } } is WalletTokensListState.Locked, is WalletTokensListState.Loading, is WalletTokensListState.Empty, - -> state + -> listState } } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletStateFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletStateFactory.kt index ad2916e9b1..4a282a682c 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletStateFactory.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletStateFactory.kt @@ -105,10 +105,7 @@ internal class WalletStateFactory( } private val refreshStateConverter by lazy { - WalletRefreshStateConverter( - currentStateProvider = currentStateProvider, - intents = clickIntents, - ) + WalletRefreshStateConverter(currentStateProvider) } private val cryptoCurrencyActionsConverter by lazy { diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt index 10f4dbd9e4..1e02c0f412 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt @@ -26,10 +26,11 @@ import com.tangem.feature.wallet.presentation.wallet.state.WalletMultiCurrencySt import com.tangem.feature.wallet.presentation.wallet.state.WalletSingleCurrencyState import com.tangem.feature.wallet.presentation.wallet.state.WalletState import com.tangem.feature.wallet.presentation.wallet.state.components.WalletTokensListState +import com.tangem.feature.wallet.presentation.wallet.state.components.WalletTokensListState.OrganizeTokensButtonState import com.tangem.feature.wallet.presentation.wallet.ui.components.TokenActionsBottomSheet import com.tangem.feature.wallet.presentation.wallet.ui.components.WalletsList import com.tangem.feature.wallet.presentation.wallet.ui.components.common.* -import com.tangem.feature.wallet.presentation.wallet.ui.components.multicurrency.organizeButton +import com.tangem.feature.wallet.presentation.wallet.ui.components.multicurrency.organizeTokensButton import com.tangem.feature.wallet.presentation.wallet.ui.components.singlecurrency.controlButtons import com.tangem.feature.wallet.presentation.wallet.ui.components.singlecurrency.marketPriceBlock import com.tangem.feature.wallet.presentation.wallet.ui.utils.changeWalletAnimator @@ -110,9 +111,15 @@ private fun WalletContent(state: WalletState.ContentState) { contentItems(state = state, txHistoryItems = txHistoryItems, modifier = movableItemModifier) if (state is WalletMultiCurrencyState) { - val tokensListState = state.tokensListState - if (tokensListState is WalletTokensListState.ContentState) { - organizeButton(onClick = tokensListState.onOrganizeTokensClick, modifier = itemModifier) + val contentTokenListState = state.tokensListState as? WalletTokensListState.ContentState + val organizeTokensButton = contentTokenListState?.organizeTokensButton + + if (organizeTokensButton is OrganizeTokensButtonState.Visible) { + organizeTokensButton( + modifier = itemModifier, + isEnabled = organizeTokensButton.isEnabled, + onClick = organizeTokensButton.onClick, + ) } } } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/multicurrency/MultiCurrencyOrganizeButton.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/multicurrency/MultiCurrencyOrganizeButton.kt index 210bfd423c..8b981deeb7 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/multicurrency/MultiCurrencyOrganizeButton.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/multicurrency/MultiCurrencyOrganizeButton.kt @@ -1,8 +1,11 @@ package com.tangem.feature.wallet.presentation.wallet.ui.components.multicurrency -import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.ui.Modifier +import com.tangem.core.ui.components.buttons.actions.ActionButtonConfig +import com.tangem.core.ui.components.buttons.actions.RoundedActionButton +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.feature.wallet.impl.R private const val ORGANIZE_BUTTON_CONTENT_TYPE = "OrganizeTokensButton" @@ -14,9 +17,20 @@ private const val ORGANIZE_BUTTON_CONTENT_TYPE = "OrganizeTokensButton" * [REDACTED_AUTHOR] */ -@OptIn(ExperimentalFoundationApi::class) -internal fun LazyListScope.organizeButton(onClick: (() -> Unit)?, modifier: Modifier = Modifier) { +internal fun LazyListScope.organizeTokensButton( + isEnabled: Boolean, + onClick: () -> Unit, + modifier: Modifier = Modifier, +) { item(key = ORGANIZE_BUTTON_CONTENT_TYPE, contentType = ORGANIZE_BUTTON_CONTENT_TYPE) { - OrganizeTokensButton(onClick = onClick, modifier = modifier.animateItemPlacement()) + RoundedActionButton( + modifier = modifier, + config = ActionButtonConfig( + text = resourceReference(id = R.string.organize_tokens_title), + iconResId = R.drawable.ic_filter_24, + onClick = onClick, + enabled = isEnabled, + ), + ) } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/multicurrency/OrganizeTokensButton.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/multicurrency/OrganizeTokensButton.kt deleted file mode 100644 index 53aeef026b..0000000000 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/multicurrency/OrganizeTokensButton.kt +++ /dev/null @@ -1,29 +0,0 @@ -package com.tangem.feature.wallet.presentation.wallet.ui.components.multicurrency - -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import com.tangem.core.ui.components.buttons.actions.ActionButtonConfig -import com.tangem.core.ui.components.buttons.actions.RoundedActionButton -import com.tangem.core.ui.extensions.TextReference -import com.tangem.feature.wallet.impl.R - -/** - * Organize tokens button - * - * @param onClick callback, if null button is disabled - * @param modifier modifier - * -[REDACTED_AUTHOR] - */ -@Composable -internal fun OrganizeTokensButton(onClick: (() -> Unit)?, modifier: Modifier = Modifier) { - RoundedActionButton( - config = ActionButtonConfig( - text = TextReference.Res(id = R.string.organize_tokens_title), - iconResId = R.drawable.ic_filter_24, - onClick = onClick ?: {}, - enabled = onClick != null, - ), - modifier = modifier, - ) -} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/CryptoCurrencyStatusToTokenItemConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/CryptoCurrencyStatusToTokenItemConverter.kt index a2e85d00a2..1e073b5ffd 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/CryptoCurrencyStatusToTokenItemConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/CryptoCurrencyStatusToTokenItemConverter.kt @@ -38,7 +38,7 @@ internal class CryptoCurrencyStatusToTokenItemConverter( return TokenItemState.Content( id = currency.id.value, name = currency.name, - icon = iconStateConverter.convert(currency), + icon = iconStateConverter.convert(value = this), amount = getFormattedAmount(), hasPending = value.hasCurrentNetworkTransactions, tokenOptions = if (isWalletContentHidden) { @@ -70,7 +70,7 @@ internal class CryptoCurrencyStatusToTokenItemConverter( private fun CryptoCurrencyStatus.mapToUnreachableTokenItemState() = TokenItemState.Unreachable( id = currency.id.value, name = currency.name, - icon = iconStateConverter.convert(currency), + icon = iconStateConverter.convert(value = this), ) private fun CryptoCurrencyStatus.getPriceChangeConfig(): PriceChangeConfig { diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/TokenListErrorConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/TokenListErrorConverter.kt index 82709f83a7..100ea1b8bf 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/TokenListErrorConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/TokenListErrorConverter.kt @@ -19,7 +19,7 @@ internal class TokenListErrorConverter( state.copy( tokensListState = WalletTokensListState.Content( items = persistentListOf(), - onOrganizeTokensClick = null, + organizeTokensButton = WalletTokensListState.OrganizeTokensButtonState.Hidden, ), ) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/TokenListToContentItemsConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/TokenListToContentItemsConverter.kt index ca50986a37..cb11331df1 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/TokenListToContentItemsConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/TokenListToContentItemsConverter.kt @@ -7,8 +7,8 @@ import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.model.NetworkGroup import com.tangem.domain.tokens.model.TokenList import com.tangem.feature.wallet.presentation.wallet.state.components.WalletTokensListState +import com.tangem.feature.wallet.presentation.wallet.state.components.WalletTokensListState.OrganizeTokensButtonState import com.tangem.feature.wallet.presentation.wallet.state.components.WalletTokensListState.TokensListItemState -import com.tangem.feature.wallet.presentation.wallet.utils.LoadingItemsProvider.getLoadingMultiCurrencyTokens import com.tangem.feature.wallet.presentation.wallet.viewmodels.WalletClickIntents import com.tangem.utils.converter.Converter import kotlinx.collections.immutable.PersistentList @@ -28,26 +28,15 @@ internal class TokenListToContentItemsConverter( ) override fun convert(value: TokenList): WalletTokensListState { - val isEmptyList = when (value) { - is TokenList.GroupedByNetwork -> value.groups.isEmpty() - is TokenList.NotInitialized -> false - is TokenList.Ungrouped -> value.currencies.isEmpty() - } - - return if (isEmptyList) { - WalletTokensListState.Empty - } else { - WalletTokensListState.Content( - items = when (value) { - is TokenList.GroupedByNetwork -> value.mapToMultiCurrencyItems() - is TokenList.Ungrouped -> value.mapToMultiCurrencyItems() - is TokenList.NotInitialized -> getLoadingMultiCurrencyTokens() - }, - onOrganizeTokensClick = if (value.totalFiatBalance is TokenList.FiatBalance.Loaded) { - clickIntents::onOrganizeTokensClick - } else { - null - }, + return when (value) { + is TokenList.NotInitialized -> WalletTokensListState.Loading() + is TokenList.GroupedByNetwork -> WalletTokensListState.Content( + items = value.mapToMultiCurrencyItems(), + organizeTokensButton = value.mapToOrganizeTokensButtonState(), + ) + is TokenList.Ungrouped -> WalletTokensListState.Content( + items = value.mapToMultiCurrencyItems(), + organizeTokensButton = value.mapToOrganizeTokensButtonState(), ) } } @@ -64,6 +53,20 @@ internal class TokenListToContentItemsConverter( } } + private fun TokenList.GroupedByNetwork.mapToOrganizeTokensButtonState(): OrganizeTokensButtonState { + return getOrganizeTokensButtonState( + isLoading = totalFiatBalance is TokenList.FiatBalance.Loading, + currenciesSize = groups.flatMap(NetworkGroup::currencies).size, + ) + } + + private fun TokenList.Ungrouped.mapToOrganizeTokensButtonState(): OrganizeTokensButtonState { + return getOrganizeTokensButtonState( + isLoading = totalFiatBalance is TokenList.FiatBalance.Loading, + currenciesSize = currencies.size, + ) + } + private fun MutableList.addGroup(group: NetworkGroup): List { this.add(TokensListItemState.NetworkGroupTitle(TextReference.Str(group.network.name))) @@ -81,4 +84,15 @@ internal class TokenListToContentItemsConverter( return this } + + private fun getOrganizeTokensButtonState(isLoading: Boolean, currenciesSize: Int): OrganizeTokensButtonState { + return if (currenciesSize > 1) { + OrganizeTokensButtonState.Visible( + isEnabled = !isLoading, + onClick = clickIntents::onOrganizeTokensClick, + ) + } else { + OrganizeTokensButtonState.Hidden + } + } } \ No newline at end of file From d5d4f2dac87aae3f0920c66209cf1a694f64d4c6 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 11 Sep 2023 11:17:23 +0300 Subject: [PATCH 021/242] Updated on 2026-08-14 --- app/src/main/java/com/tangem/tap/features/home/HomeFragment.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/tangem/tap/features/home/HomeFragment.kt b/app/src/main/java/com/tangem/tap/features/home/HomeFragment.kt index ceb3ead5f5..2127b6e8c6 100644 --- a/app/src/main/java/com/tangem/tap/features/home/HomeFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/home/HomeFragment.kt @@ -17,11 +17,11 @@ import com.tangem.core.analytics.Analytics import com.tangem.core.navigation.AppScreen import com.tangem.core.navigation.NavigationAction import com.tangem.core.ui.res.TangemTheme +import com.tangem.domain.tokens.TokensAction import com.tangem.tap.common.analytics.events.IntroductionProcess import com.tangem.tap.features.home.compose.StoriesScreen import com.tangem.tap.features.home.redux.HomeAction import com.tangem.tap.features.home.redux.HomeState -import com.tangem.tap.features.tokens.legacy.redux.TokensAction import com.tangem.tap.store import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.launch From 0e3412711aa371c55038f9dfb672bc0cfc8f5283 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 7 Sep 2023 17:07:17 +0300 Subject: [PATCH 022/242] Updated on 2026-08-14 --- .../tokendetails/TokenDetailsPreviewData.kt | 7 ++ .../tokendetails/state/TokenDetailsState.kt | 2 + .../TokenDetailsPullToRefreshConfig.kt | 3 + .../TokenDetailsRefreshStateConverter.kt | 19 +++++ .../TokenDetailsSkeletonStateConverter.kt | 7 ++ .../state/factory/TokenDetailsStateFactory.kt | 14 ++++ .../tokendetails/ui/TokenDetailsScreen.kt | 71 ++++++++++++------- .../viewmodels/TokenDetailsClickIntents.kt | 2 + .../viewmodels/TokenDetailsViewModel.kt | 28 ++++++-- 9 files changed, 121 insertions(+), 32 deletions(-) create mode 100644 features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/components/TokenDetailsPullToRefreshConfig.kt create mode 100644 features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsRefreshStateConverter.kt diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/TokenDetailsPreviewData.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/TokenDetailsPreviewData.kt index 360e77ad20..7f79abcb99 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/TokenDetailsPreviewData.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/TokenDetailsPreviewData.kt @@ -10,6 +10,7 @@ import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDeta import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsTopAppBarConfig import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenInfoBlockState import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.TokenDetailsActionButton +import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.TokenDetailsPullToRefreshConfig import com.tangem.features.tokendetails.impl.R import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.flow.MutableStateFlow @@ -71,6 +72,11 @@ internal object TokenDetailsPreviewData { private val marketPriceLoading = MarketPriceBlockState.Loading(currencyName = "USDT") + private val pullToRefreshConfig = TokenDetailsPullToRefreshConfig( + isRefreshing = false, + onRefresh = {}, + ) + val tokenDetailsState = TokenDetailsState( topAppBarConfig = tokenDetailsTopAppBarConfig, tokenInfoBlockState = tokenInfoBlockState, @@ -83,5 +89,6 @@ internal object TokenDetailsPreviewData { ), dialogConfig = null, pendingTxs = persistentListOf(), + pullToRefreshConfig = pullToRefreshConfig, ) } \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsState.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsState.kt index 7aece5e270..3c40416911 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsState.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsState.kt @@ -4,6 +4,7 @@ import com.tangem.core.ui.components.marketprice.MarketPriceBlockState import com.tangem.core.ui.components.transactions.state.TransactionState import com.tangem.core.ui.components.transactions.state.TxHistoryState import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.TokenDetailsDialogConfig +import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.TokenDetailsPullToRefreshConfig import kotlinx.collections.immutable.PersistentList internal data class TokenDetailsState( @@ -14,4 +15,5 @@ internal data class TokenDetailsState( val pendingTxs: PersistentList, val txHistoryState: TxHistoryState, val dialogConfig: TokenDetailsDialogConfig?, + val pullToRefreshConfig: TokenDetailsPullToRefreshConfig, ) \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/components/TokenDetailsPullToRefreshConfig.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/components/TokenDetailsPullToRefreshConfig.kt new file mode 100644 index 0000000000..5015995638 --- /dev/null +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/components/TokenDetailsPullToRefreshConfig.kt @@ -0,0 +1,3 @@ +package com.tangem.feature.tokendetails.presentation.tokendetails.state.components + +data class TokenDetailsPullToRefreshConfig(val isRefreshing: Boolean, val onRefresh: () -> Unit) \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsRefreshStateConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsRefreshStateConverter.kt new file mode 100644 index 0000000000..580f193c3f --- /dev/null +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsRefreshStateConverter.kt @@ -0,0 +1,19 @@ +package com.tangem.feature.tokendetails.presentation.tokendetails.state.factory + +import com.tangem.common.Provider +import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState +import com.tangem.utils.converter.Converter + +internal class TokenDetailsRefreshStateConverter( + private val currentStateProvider: Provider, +) : Converter { + + override fun convert(value: Boolean): TokenDetailsState { + val state = currentStateProvider() + return state.createPullToRefresh(value) + } + + private fun TokenDetailsState.createPullToRefresh(value: Boolean): TokenDetailsState { + return copy(pullToRefreshConfig = pullToRefreshConfig.copy(isRefreshing = value)) + } +} \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSkeletonStateConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSkeletonStateConverter.kt index 9d666add03..8d4bd2f378 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSkeletonStateConverter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSkeletonStateConverter.kt @@ -8,6 +8,7 @@ import com.tangem.core.ui.res.TangemTheme import com.tangem.domain.tokens.models.CryptoCurrency import com.tangem.feature.tokendetails.presentation.tokendetails.state.* import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.TokenDetailsActionButton +import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.TokenDetailsPullToRefreshConfig import com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.TokenDetailsSkeletonStateConverter.SkeletonModel import com.tangem.feature.tokendetails.presentation.tokendetails.viewmodels.TokenDetailsClickIntents import com.tangem.features.tokendetails.impl.R @@ -49,6 +50,7 @@ internal class TokenDetailsSkeletonStateConverter( ), ), dialogConfig = null, + pullToRefreshConfig = createPullToRefresh(), ) } @@ -72,5 +74,10 @@ internal class TokenDetailsSkeletonStateConverter( ) } + private fun createPullToRefresh(): TokenDetailsPullToRefreshConfig = TokenDetailsPullToRefreshConfig( + isRefreshing = false, + onRefresh = clickIntents::onRefreshSwipe, + ) + data class SkeletonModel(val cryptoCurrency: CryptoCurrency) } \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt index eb3ee645d1..0ad3b96933 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt @@ -59,6 +59,12 @@ internal class TokenDetailsStateFactory( ) } + private val refreshStateConverter by lazy { + TokenDetailsRefreshStateConverter( + currentStateProvider = currentStateProvider, + ) + } + fun getInitialState(cryptoCurrency: CryptoCurrency): TokenDetailsState { return skeletonStateConverter.convert( TokenDetailsSkeletonStateConverter.SkeletonModel(cryptoCurrency = cryptoCurrency), @@ -117,4 +123,12 @@ internal class TokenDetailsStateFactory( ), ) } + + fun getRefreshingState(): TokenDetailsState { + return refreshStateConverter.convert(true) + } + + fun getRefreshedState(): TokenDetailsState { + return refreshStateConverter.convert(false) + } } \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/TokenDetailsScreen.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/TokenDetailsScreen.kt index 9dbb266f47..5c33684a65 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/TokenDetailsScreen.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/TokenDetailsScreen.kt @@ -1,11 +1,12 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.ui import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.material.ExperimentalMaterialApi +import androidx.compose.material.pullrefresh.PullRefreshIndicator +import androidx.compose.material.pullrefresh.pullRefresh +import androidx.compose.material.pullrefresh.rememberPullRefreshState import androidx.compose.material3.Scaffold import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment @@ -29,12 +30,18 @@ import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.T import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.TokenInfoBlock import kotlinx.collections.immutable.PersistentList +@OptIn(ExperimentalMaterialApi::class) @Composable internal fun TokenDetailsScreen(state: TokenDetailsState) { Scaffold( topBar = { TokenDetailsTopAppBar(config = state.topAppBarConfig) }, containerColor = TangemTheme.colors.background.secondary, ) { scaffoldPaddings -> + val pullRefreshState = rememberPullRefreshState( + refreshing = state.pullToRefreshConfig.isRefreshing, + onRefresh = state.pullToRefreshConfig.onRefresh, + ) + val txHistoryItems = if (state.txHistoryState is TxHistoryState.Content) { state.txHistoryState.contentItems.collectAsLazyPagingItems() } else { @@ -45,34 +52,46 @@ internal fun TokenDetailsScreen(state: TokenDetailsState) { val itemModifier = Modifier .padding(top = betweenItemsPadding) .padding(horizontal = horizontalPadding) - LazyColumn( + + Box( modifier = Modifier - .padding(paddingValues = scaffoldPaddings) - .fillMaxSize(), + .padding(scaffoldPaddings) + .pullRefresh(pullRefreshState), ) { - item { - TokenInfoBlock( - modifier = Modifier - .padding(top = TangemTheme.dimens.spacing4) - .padding(horizontal = horizontalPadding), - state = state.tokenInfoBlockState, - ) - } - item { TokenDetailsBalanceBlock(modifier = itemModifier, state = state.tokenBalanceBlockState) } - item( - key = MarketPriceBlockState::class.java, - contentType = MarketPriceBlockState::class.java, - content = { MarketPriceBlock(modifier = itemModifier, state = state.marketPriceBlockState) }, - ) - if (state.txHistoryState is TxHistoryState.NotSupported && state.pendingTxs.isNotEmpty()) { + LazyColumn( + modifier = Modifier + .fillMaxSize(), + ) { item { - PendingTxsBlock( - pendingTxs = state.pendingTxs, - modifier = itemModifier, + TokenInfoBlock( + modifier = Modifier + .padding(top = TangemTheme.dimens.spacing4) + .padding(horizontal = horizontalPadding), + state = state.tokenInfoBlockState, ) } + item { TokenDetailsBalanceBlock(modifier = itemModifier, state = state.tokenBalanceBlockState) } + item( + key = MarketPriceBlockState::class.java, + contentType = MarketPriceBlockState::class.java, + content = { MarketPriceBlock(modifier = itemModifier, state = state.marketPriceBlockState) }, + ) + if (state.txHistoryState is TxHistoryState.NotSupported && state.pendingTxs.isNotEmpty()) { + item { + PendingTxsBlock( + pendingTxs = state.pendingTxs, + modifier = itemModifier, + ) + } + } + txHistoryItems(state = state.txHistoryState, txHistoryItems = txHistoryItems) } - txHistoryItems(state = state.txHistoryState, txHistoryItems = txHistoryItems) + + PullRefreshIndicator( + refreshing = state.pullToRefreshConfig.isRefreshing, + state = pullRefreshState, + modifier = Modifier.align(Alignment.TopCenter), + ) } TokenDetailsDialogs(state = state) diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsClickIntents.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsClickIntents.kt index b0917c5d2a..0e1eab9fc1 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsClickIntents.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsClickIntents.kt @@ -19,4 +19,6 @@ interface TokenDetailsClickIntents : TxHistoryClickIntents { fun onHideClick() fun onHideConfirmed() + + fun onRefreshSwipe() } \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt index 27b52fe003..0af0c850bd 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt @@ -10,10 +10,7 @@ import com.tangem.common.Provider import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.redux.ReduxStateHolder -import com.tangem.domain.tokens.GetCryptoCurrencyActionsUseCase -import com.tangem.domain.tokens.GetCurrencyStatusUpdatesUseCase -import com.tangem.domain.tokens.GetNetworkCoinStatusUseCase -import com.tangem.domain.tokens.RemoveCurrencyUseCase +import com.tangem.domain.tokens.* import com.tangem.domain.tokens.legacy.TradeCryptoAction import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.models.CryptoCurrency @@ -44,6 +41,7 @@ internal class TokenDetailsViewModel @Inject constructor( private val getSelectedWalletUseCase: GetSelectedWalletUseCase, private val getCurrencyStatusUpdatesUseCase: GetCurrencyStatusUpdatesUseCase, private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, + private val fetchCurrencyStatusUseCase: FetchCurrencyStatusUseCase, private val txHistoryItemsCountUseCase: GetTxHistoryItemsCountUseCase, private val txHistoryItemsUseCase: GetTxHistoryItemsUseCase, private val getExploreUrlUseCase: GetExploreUrlUseCase, @@ -60,6 +58,7 @@ internal class TokenDetailsViewModel @Inject constructor( var router by Delegates.notNull() private val marketPriceJobHolder = JobHolder() + private val refreshStateJobHolder = JobHolder() private var cryptoCurrencyStatus: CryptoCurrencyStatus? = null private var wallet by Delegates.notNull() @@ -116,14 +115,16 @@ internal class TokenDetailsViewModel @Inject constructor( .saveIn(marketPriceJobHolder) } - private fun updateTxHistory() { + private fun updateTxHistory(refresh: Boolean = false) { viewModelScope.launch(dispatchers.io) { val txHistoryItemsCountEither = txHistoryItemsCountUseCase( networkId = cryptoCurrency.network.id, derivationPath = cryptoCurrency.derivationPath, ) - uiState = stateFactory.getLoadingTxHistoryState(itemsCountEither = txHistoryItemsCountEither) + if (!refresh) { + uiState = stateFactory.getLoadingTxHistoryState(itemsCountEither = txHistoryItemsCountEither) + } txHistoryItemsCountEither.onRight { uiState = stateFactory.getLoadedTxHistoryState( @@ -258,4 +259,19 @@ internal class TokenDetailsViewModel @Inject constructor( ) } } + + override fun onRefreshSwipe() { + uiState = stateFactory.getRefreshingState() + + viewModelScope.launch(dispatchers.io) { + fetchCurrencyStatusUseCase.invoke( + userWalletId = wallet.walletId, + id = cryptoCurrency.id, + refresh = true, + ) + updateTxHistory(refresh = true) + + uiState = stateFactory.getRefreshedState() + }.saveIn(refreshStateJobHolder) + } } \ No newline at end of file From 0f1183910b29b18d788ecad2ffe3e4a7b899db6a Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 11 Sep 2023 10:49:51 +0300 Subject: [PATCH 023/242] Updated on 2026-08-14 --- .../state/factory/TokenDetailsRefreshStateConverter.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsRefreshStateConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsRefreshStateConverter.kt index 580f193c3f..1b2f4c7a6e 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsRefreshStateConverter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsRefreshStateConverter.kt @@ -13,7 +13,7 @@ internal class TokenDetailsRefreshStateConverter( return state.createPullToRefresh(value) } - private fun TokenDetailsState.createPullToRefresh(value: Boolean): TokenDetailsState { + private fun TokenDetailsState.createPullToRefresh(isRefreshing: Boolean): TokenDetailsState { return copy(pullToRefreshConfig = pullToRefreshConfig.copy(isRefreshing = value)) } } \ No newline at end of file From f314f7d7435d90e2c2c80f777869c941f1f054eb Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 11 Sep 2023 10:50:30 +0300 Subject: [PATCH 024/242] Updated on 2026-08-14 --- .../state/factory/TokenDetailsRefreshStateConverter.kt | 2 +- .../presentation/tokendetails/ui/TokenDetailsScreen.kt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsRefreshStateConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsRefreshStateConverter.kt index 1b2f4c7a6e..23c045fe3f 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsRefreshStateConverter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsRefreshStateConverter.kt @@ -14,6 +14,6 @@ internal class TokenDetailsRefreshStateConverter( } private fun TokenDetailsState.createPullToRefresh(isRefreshing: Boolean): TokenDetailsState { - return copy(pullToRefreshConfig = pullToRefreshConfig.copy(isRefreshing = value)) + return copy(pullToRefreshConfig = pullToRefreshConfig.copy(isRefreshing = isRefreshing)) } } \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/TokenDetailsScreen.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/TokenDetailsScreen.kt index 5c33684a65..b47ae3c260 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/TokenDetailsScreen.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/TokenDetailsScreen.kt @@ -88,9 +88,9 @@ internal fun TokenDetailsScreen(state: TokenDetailsState) { } PullRefreshIndicator( + modifier = Modifier.align(Alignment.TopCenter), refreshing = state.pullToRefreshConfig.isRefreshing, state = pullRefreshState, - modifier = Modifier.align(Alignment.TopCenter), ) } From c483e6f2a2f7b16329b5e90cb4057149fa9cf9a8 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 8 Sep 2023 12:37:16 +0400 Subject: [PATCH 025/242] Updated on 2026-08-14 --- .../products/twins/ui/TwinsCardsFragment.kt | 3 ++ .../wallet/ui/OnboardingWalletFragment.kt | 2 + app/src/main/res/color/selector_edit_text.xml | 8 +-- .../res/drawable/ic_activation_success.xml | 24 --------- app/src/main/res/drawable/ic_dot.xml | 2 +- app/src/main/res/drawable/ic_selected_dot.xml | 2 +- .../res/drawable/img_onboarding_success.xml | 14 ++++++ .../res/drawable/shape_refresh_button.xml | 4 +- .../res/drawable/shape_success_circle.xml | 8 +++ .../layout_onboarding_container_bottom.xml | 3 ++ .../layout/dialog_onboarding_address_info.xml | 25 +++++++--- .../res/layout/fragment_onboarding_main.xml | 2 + .../res/layout/fragment_onboarding_wallet.xml | 50 +++++++++++++++---- .../res/layout/item_backup_info_adapter.xml | 11 ++-- .../res/layout/layout_backup_access_code.xml | 3 +- .../layout/layout_backup_access_code_info.xml | 12 +++-- .../layout_backup_access_code_submit.xml | 13 +++-- .../layout_onboarding_buttons_add_cards.xml | 3 +- .../layout_onboarding_buttons_common.xml | 3 ++ .../layout_onboarding_container_bottom.xml | 10 +++- .../layout_onboarding_container_top.xml | 37 +++++++++++--- .../main/res/layout/layout_pseudo_toolbar.xml | 3 +- .../layout/lp_onboarding_create_wallet.xml | 34 +++++++++++-- .../main/res/layout/lp_onboarding_done.xml | 25 ++++++++-- .../layout/lp_onboarding_done_activation.xml | 35 +++++++++++-- .../lp_onboarding_done_activation_twins.xml | 35 +++++++++++-- .../res/layout/lp_onboarding_topup_wallet.xml | 37 +++++++++++--- .../lp_onboarding_topup_wallet_twins.xml | 37 +++++++++++--- .../main/res/layout/view_bg_twins_welcome.xml | 6 +-- .../res/layout/view_onboarding_progress.xml | 4 +- .../view_onboarding_refresh_balance.xml | 18 ++----- .../res/layout/view_onboarding_tv_balance.xml | 4 +- app/src/main/res/values-night/colors.xml | 7 ++- app/src/main/res/values/colors.xml | 8 ++- app/src/main/res/values/styles.xml | 4 +- 35 files changed, 371 insertions(+), 125 deletions(-) delete mode 100644 app/src/main/res/drawable/ic_activation_success.xml create mode 100644 app/src/main/res/drawable/img_onboarding_success.xml create mode 100644 app/src/main/res/drawable/shape_success_circle.xml diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/ui/TwinsCardsFragment.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/ui/TwinsCardsFragment.kt index ddcdc4119b..7d12941132 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/ui/TwinsCardsFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/ui/TwinsCardsFragment.kt @@ -375,6 +375,9 @@ class TwinsCardsFragment : BaseOnboardingFragment() { mainBinding.onboardingTopContainer.imvCardBackground.setBackgroundDrawable( requireContext().getDrawableCompat(R.drawable.shape_rectangle_rounded_8), ) + mainBinding.onboardingTopContainer.imvCardBackground.backgroundTintList = + requireContext().resources.getColorStateList(R.color.onboarding_card_background, null) + updateConstraints(state.currentStep, R.layout.lp_onboarding_topup_wallet_twins) } diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/OnboardingWalletFragment.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/OnboardingWalletFragment.kt index ff40ab5ad6..2ee6338d5e 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/OnboardingWalletFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/OnboardingWalletFragment.kt @@ -22,6 +22,7 @@ import com.tangem.common.CardIdFormatter import com.tangem.common.CompletionResult import com.tangem.common.core.CardIdDisplayFormat import com.tangem.core.analytics.Analytics +import com.tangem.core.ui.extensions.setStatusBarColor import com.tangem.domain.common.util.cardTypesResolver import com.tangem.feature.onboarding.data.model.CreateWalletResponse import com.tangem.feature.onboarding.presentation.wallet2.analytics.SeedPhraseSource @@ -158,6 +159,7 @@ class OnboardingWalletFragment : oldState.onboardingWalletState == newState.onboardingWalletState }.select { it.onboardingWalletState } } + setStatusBarColor(R.color.background_primary) } override fun onStop() { diff --git a/app/src/main/res/color/selector_edit_text.xml b/app/src/main/res/color/selector_edit_text.xml index b6c0697011..192514f802 100644 --- a/app/src/main/res/color/selector_edit_text.xml +++ b/app/src/main/res/color/selector_edit_text.xml @@ -1,7 +1,7 @@ - - - - + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_activation_success.xml b/app/src/main/res/drawable/ic_activation_success.xml deleted file mode 100644 index 7957c82493..0000000000 --- a/app/src/main/res/drawable/ic_activation_success.xml +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - diff --git a/app/src/main/res/drawable/ic_dot.xml b/app/src/main/res/drawable/ic_dot.xml index 321894f58a..bef54d1532 100644 --- a/app/src/main/res/drawable/ic_dot.xml +++ b/app/src/main/res/drawable/ic_dot.xml @@ -7,7 +7,7 @@ android:thickness="4.5dp" android:useLevel="false"> diff --git a/app/src/main/res/drawable/ic_selected_dot.xml b/app/src/main/res/drawable/ic_selected_dot.xml index e847725fc2..f747ab54ba 100644 --- a/app/src/main/res/drawable/ic_selected_dot.xml +++ b/app/src/main/res/drawable/ic_selected_dot.xml @@ -6,7 +6,7 @@ android:shape="ring" android:thickness="4.5dp" android:useLevel="false"> - + \ No newline at end of file diff --git a/app/src/main/res/drawable/img_onboarding_success.xml b/app/src/main/res/drawable/img_onboarding_success.xml new file mode 100644 index 0000000000..84574803ea --- /dev/null +++ b/app/src/main/res/drawable/img_onboarding_success.xml @@ -0,0 +1,14 @@ + + + + diff --git a/app/src/main/res/drawable/shape_refresh_button.xml b/app/src/main/res/drawable/shape_refresh_button.xml index ac9508438d..cade7f63f3 100644 --- a/app/src/main/res/drawable/shape_refresh_button.xml +++ b/app/src/main/res/drawable/shape_refresh_button.xml @@ -2,11 +2,11 @@ - + + android:color="@color/button_secondary" /> + + + + \ No newline at end of file diff --git a/app/src/main/res/layout-h680dp/layout_onboarding_container_bottom.xml b/app/src/main/res/layout-h680dp/layout_onboarding_container_bottom.xml index ee7f2b2cb0..131299d4d5 100644 --- a/app/src/main/res/layout-h680dp/layout_onboarding_container_bottom.xml +++ b/app/src/main/res/layout-h680dp/layout_onboarding_container_bottom.xml @@ -93,6 +93,9 @@ + android:layout_height="wrap_content" + android:background="@color/background_primary"> @@ -52,13 +54,14 @@ android:layout_marginStart="32dp" android:layout_marginEnd="8dp" android:background="@drawable/shape_rectangle_rounded_100" - android:backgroundTint="@color/lightGray0" + android:layout_marginTop="30dp" android:paddingStart="16dp" android:paddingEnd="16dp" app:layout_constraintEnd_toStartOf="@+id/btn_fl_share" app:layout_constraintHorizontal_chainStyle="packed" app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toTopOf="@+id/guideline3"> + android:backgroundTint="@color/button_secondary" + app:layout_constraintTop_toBottomOf="@id/tv_receive_message"> @@ -98,13 +103,14 @@ android:layout_height="40dp" android:layout_marginEnd="32dp" android:background="@drawable/shape_rectangle_rounded_100" - android:backgroundTint="@color/lightGray0" + android:layout_marginTop="30dp" android:paddingStart="16dp" android:paddingEnd="16dp" + android:backgroundTint="@color/background_primary" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintHorizontal_bias="0.5" app:layout_constraintStart_toEndOf="@+id/btn_fl_copy_address" - app:layout_constraintTop_toTopOf="@+id/guideline3"> + app:layout_constraintTop_toBottomOf="@+id/tv_receive_message"> + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_onboarding_main.xml b/app/src/main/res/layout/fragment_onboarding_main.xml index bc1cafb403..b184df2cd2 100644 --- a/app/src/main/res/layout/fragment_onboarding_main.xml +++ b/app/src/main/res/layout/fragment_onboarding_main.xml @@ -22,6 +22,8 @@ diff --git a/app/src/main/res/layout/fragment_onboarding_wallet.xml b/app/src/main/res/layout/fragment_onboarding_wallet.xml index 0080b81937..217d2db70e 100644 --- a/app/src/main/res/layout/fragment_onboarding_wallet.xml +++ b/app/src/main/res/layout/fragment_onboarding_wallet.xml @@ -5,7 +5,7 @@ android:id="@+id/coordinator_details_confirm" android:layout_width="match_parent" android:layout_height="match_parent" - android:background="@color/background_secondary" + android:background="@color/background_primary" android:clipChildren="false" android:clipToPadding="false" android:orientation="vertical"> @@ -21,8 +21,10 @@ @@ -59,7 +61,7 @@ android:layout_height="236dp" android:adjustViewBounds="true" android:background="@drawable/shape_circle" - android:backgroundTint="@color/lightGray0" + android:backgroundTint="@color/background_primary" android:elevation="0dp" android:scaleType="fitCenter" app:layout_constraintBottom_toBottomOf="@id/fl_cards_container" @@ -113,17 +115,35 @@ - + app:layout_constraintTop_toTopOf="parent"> + + + + + + + android:layout_marginStart="@dimen/dimen16" + android:layout_marginEnd="@dimen/dimen16" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" /> + android:layout_marginStart="@dimen/dimen16" + android:layout_marginEnd="@dimen/dimen16" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" /> diff --git a/app/src/main/res/layout/item_backup_info_adapter.xml b/app/src/main/res/layout/item_backup_info_adapter.xml index ecbfdbdb1b..469434318b 100644 --- a/app/src/main/res/layout/item_backup_info_adapter.xml +++ b/app/src/main/res/layout/item_backup_info_adapter.xml @@ -2,7 +2,8 @@ + xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:tools="http://schemas.android.com/tools"> + android:layout_height="match_parent" + android:background="@color/background_primary"> + app:layout_constraintTop_toTopOf="parent" + app:tint="@color/icon_informative" /> @@ -65,6 +67,7 @@ android:layout_marginStart="46dp" android:layout_marginTop="8dp" android:src="@drawable/ic_feature_2" + app:tint="@color/icon_primary_1" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/tv_feature_2_title" /> @@ -98,6 +101,7 @@ android:layout_marginStart="46dp" android:layout_marginTop="8dp" android:src="@drawable/ic_feature_3" + app:tint="@color/icon_primary_1" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/tv_feature_3_title" /> @@ -117,10 +121,10 @@ @@ -28,12 +29,17 @@ android:layout_marginEnd="16dp" android:layout_marginBottom="40dp" android:hint="@string/onboarding_wallet_info_title_third" + android:textColorHint="@color/text_primary_1" android:theme="@style/EditTextThemeOverlay" app:boxStrokeColor="@color/selector_edit_text" app:boxStrokeWidth="1dp" app:endIconDrawable="@drawable/selector_password_toggle" + app:boxStrokeErrorColor="@color/icon_warning" app:endIconMode="password_toggle" app:hintTextColor="@color/accent" + app:endIconTint="@color/icon_primary_1" + app:errorIconTint="@color/icon_warning" + app:errorTextColor="@color/icon_warning" app:layout_constraintTop_toBottomOf="@id/tv_access_code_enter_description"> @@ -48,10 +55,10 @@ @@ -75,7 +78,9 @@ - + app:layout_constraintTop_toBottomOf="@+id/imv_card_background"> + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/layout_pseudo_toolbar.xml b/app/src/main/res/layout/layout_pseudo_toolbar.xml index bb2a5ce723..0f9b853f36 100644 --- a/app/src/main/res/layout/layout_pseudo_toolbar.xml +++ b/app/src/main/res/layout/layout_pseudo_toolbar.xml @@ -4,7 +4,7 @@ android:id="@+id/pseudo_toolbar" android:layout_width="match_parent" android:layout_height="?attr/actionBarSize" - android:background="@color/background_secondary" + android:background="@color/background_action" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent"> @@ -18,6 +18,7 @@ android:clickable="true" android:focusable="true" android:padding="16dp" + android:tint="@color/icon_primary_1" android:src="@drawable/ic_close_24" /> diff --git a/app/src/main/res/layout/lp_onboarding_create_wallet.xml b/app/src/main/res/layout/lp_onboarding_create_wallet.xml index 68e87f3e61..85bf7d1c41 100644 --- a/app/src/main/res/layout/lp_onboarding_create_wallet.xml +++ b/app/src/main/res/layout/lp_onboarding_create_wallet.xml @@ -104,15 +104,39 @@ app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/imv_card_background" /> - + app:layout_constraintTop_toBottomOf="@+id/imv_card_background"> + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/lp_onboarding_done.xml b/app/src/main/res/layout/lp_onboarding_done.xml index 8f7be0c671..a5db6527a8 100644 --- a/app/src/main/res/layout/lp_onboarding_done.xml +++ b/app/src/main/res/layout/lp_onboarding_done.xml @@ -105,14 +105,33 @@ app:layout_constraintStart_toStartOf="@+id/imv_card_background" app:layout_constraintTop_toBottomOf="@+id/imv_card_background" /> - + app:layout_constraintTop_toBottomOf="@+id/pb_state"> + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/lp_onboarding_done_activation.xml b/app/src/main/res/layout/lp_onboarding_done_activation.xml index 02512421c5..b1517de7c9 100644 --- a/app/src/main/res/layout/lp_onboarding_done_activation.xml +++ b/app/src/main/res/layout/lp_onboarding_done_activation.xml @@ -104,14 +104,39 @@ app:layout_constraintStart_toStartOf="@+id/imv_card_background" app:layout_constraintTop_toBottomOf="@+id/imv_card_background" /> - + app:layout_constraintTop_toBottomOf="@+id/imv_card_background"> + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/lp_onboarding_done_activation_twins.xml b/app/src/main/res/layout/lp_onboarding_done_activation_twins.xml index c465795c75..31a41e9321 100644 --- a/app/src/main/res/layout/lp_onboarding_done_activation_twins.xml +++ b/app/src/main/res/layout/lp_onboarding_done_activation_twins.xml @@ -103,14 +103,39 @@ app:layout_constraintStart_toStartOf="@+id/imv_card_background" app:layout_constraintTop_toBottomOf="@+id/imv_card_background" /> - + app:layout_constraintTop_toBottomOf="@+id/imv_card_background"> + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/lp_onboarding_topup_wallet.xml b/app/src/main/res/layout/lp_onboarding_topup_wallet.xml index e4e5587025..6d4f2d8ee5 100644 --- a/app/src/main/res/layout/lp_onboarding_topup_wallet.xml +++ b/app/src/main/res/layout/lp_onboarding_topup_wallet.xml @@ -30,7 +30,7 @@ android:layout_marginTop="@dimen/onboarding_square_background_margin_top" android:layout_marginEnd="32dp" android:background="@drawable/shape_rectangle_rounded_8" - android:backgroundTint="@color/lightGray0" + android:backgroundTint="@color/onboarding_card_background" android:elevation="0dp" android:scaleType="fitCenter" app:layout_constraintEnd_toEndOf="parent" @@ -102,15 +102,40 @@ app:layout_constraintStart_toStartOf="@+id/imv_card_background" app:layout_constraintTop_toBottomOf="@+id/imv_card_background" /> - + app:layout_constraintTop_toBottomOf="@+id/imv_card_background"> + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/lp_onboarding_topup_wallet_twins.xml b/app/src/main/res/layout/lp_onboarding_topup_wallet_twins.xml index 66351f4062..2a0c9ed3ba 100644 --- a/app/src/main/res/layout/lp_onboarding_topup_wallet_twins.xml +++ b/app/src/main/res/layout/lp_onboarding_topup_wallet_twins.xml @@ -30,7 +30,7 @@ android:layout_marginTop="@dimen/onboarding_square_background_margin_top" android:layout_marginEnd="32dp" android:background="@drawable/shape_rectangle_rounded_8" - android:backgroundTint="@color/lightGray0" + android:backgroundTint="@color/onboarding_card_background" android:elevation="0dp" android:scaleType="fitCenter" app:layout_constraintEnd_toEndOf="parent" @@ -102,15 +102,40 @@ app:layout_constraintStart_toStartOf="@+id/imv_card_background" app:layout_constraintTop_toBottomOf="@+id/imv_card_background" /> - + app:layout_constraintTop_toBottomOf="@+id/imv_card_background"> + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/view_bg_twins_welcome.xml b/app/src/main/res/layout/view_bg_twins_welcome.xml index acfee09420..f9a713eab6 100644 --- a/app/src/main/res/layout/view_bg_twins_welcome.xml +++ b/app/src/main/res/layout/view_bg_twins_welcome.xml @@ -11,7 +11,7 @@ android:alpha="0.4" android:scaleType="fitXY" android:src="@drawable/shape_circle" - android:tint="#DEDEE0" + android:tint="@color/onboarding_twin_wave_1" android:transitionName="bg_circle_large" android:translationX="-272dp" android:translationY="-200dp" @@ -25,7 +25,7 @@ android:alpha="0.4" android:scaleType="fitXY" android:src="@drawable/shape_circle" - android:tint="#DCDCDC" + android:tint="@color/onboarding_twin_wave_2" android:transitionName="bg_circle_medium" android:translationX="-254dp" android:translationY="-269dp" @@ -39,7 +39,7 @@ android:alpha="0.4" android:scaleType="fitXY" android:src="@drawable/shape_circle" - android:tint="#D9D9D9" + android:tint="@color/onboarding_twin_wave_3" android:transitionName="bg_circle_min" android:translationX="-394dp" android:translationY="-411dp" diff --git a/app/src/main/res/layout/view_onboarding_progress.xml b/app/src/main/res/layout/view_onboarding_progress.xml index 1d840e7958..696a282eda 100644 --- a/app/src/main/res/layout/view_onboarding_progress.xml +++ b/app/src/main/res/layout/view_onboarding_progress.xml @@ -7,6 +7,6 @@ android:layout_height="wrap_content" android:layout_marginStart="16dp" android:layout_marginEnd="16dp" - android:progressBackgroundTint="#32000000" - android:progressTint="@color/background_action" /> + android:progressBackgroundTint="@color/icon_informative" + android:progressTint="@color/icon_primary_1" /> diff --git a/app/src/main/res/layout/view_onboarding_refresh_balance.xml b/app/src/main/res/layout/view_onboarding_refresh_balance.xml index 88daba2ce2..1bf18a57ac 100644 --- a/app/src/main/res/layout/view_onboarding_refresh_balance.xml +++ b/app/src/main/res/layout/view_onboarding_refresh_balance.xml @@ -11,6 +11,7 @@ android:id="@+id/imv_bg_circle" android:layout_width="60dp" android:layout_height="60dp" + android:layout_gravity="center" android:src="@drawable/shape_refresh_button" /> @@ -39,23 +41,9 @@ android:layout_height="24dp" android:layout_gravity="center" android:indeterminate="true" - app:indicatorColor="@android:color/black" + app:indicatorColor="@color/icon_primary_1" app:indicatorSize="23dp" app:trackThickness="1.8dp" /> - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/view_onboarding_tv_balance.xml b/app/src/main/res/layout/view_onboarding_tv_balance.xml index 84aceb28fa..f421e4d992 100644 --- a/app/src/main/res/layout/view_onboarding_tv_balance.xml +++ b/app/src/main/res/layout/view_onboarding_tv_balance.xml @@ -14,7 +14,7 @@ android:letterSpacing="0.036" android:text="@string/onboarding_balance_title" android:textAllCaps="true" - android:textColor="#ABABAB" + android:textColor="@color/text_secondary" android:textSize="14sp" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" @@ -31,6 +31,7 @@ android:maxLines="1" android:textSize="28sp" android:textStyle="bold" + android:textColor="@color/text_primary_1" app:layout_constraintEnd_toStartOf="@+id/tv_balance_currency" app:layout_constraintHorizontal_bias="0.5" app:layout_constraintHorizontal_chainStyle="packed" @@ -45,6 +46,7 @@ android:letterSpacing="0.036" android:textSize="28sp" android:textStyle="bold" + android:textColor="@color/text_primary_1" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintHorizontal_bias="0.5" app:layout_constraintStart_toEndOf="@+id/tv_balance_value" diff --git a/app/src/main/res/values-night/colors.xml b/app/src/main/res/values-night/colors.xml index 75ebc64343..5e6d3cdd75 100644 --- a/app/src/main/res/values-night/colors.xml +++ b/app/src/main/res/values-night/colors.xml @@ -2,7 +2,7 @@ - #C9C9C9 + #303030 #1E1E1E #000000 @@ -20,6 +20,11 @@ + #3B3B3B + #3E3E3E + #404040 + #444444 + #1ACE80 #FFB71B #3B3B3B diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index 733c9d3d07..f163351439 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -36,13 +36,19 @@ @color/colorSecondary #E0E6FA + #F3F3F3 + #DDDEE0 + #DCDCDC + #D9D9D9 + + #1C1C1E #FFB71B #CA0F03 #007AFF - #000000 + #FFFFFF #FFFFFF #F5F5F5 diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index 52253e723d..540e4ebe30 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -6,7 +6,7 @@ @color/menu_accent_color @color/menu_accent_color @color/text_primary_1 - @color/menu_accent_color + @color/icon_primary_1 @color/text_secondary @color/accent @@ -32,7 +32,7 @@ From b35a60098e8554e1208c0a6ef6e09271eb503965 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 11 Sep 2023 21:04:30 +0300 Subject: [PATCH 026/242] Updated on 2026-08-14 --- .../tap/features/details/redux/DetailsMiddleware.kt | 8 +++++--- .../com/tangem/domain/apptheme/model/AppThemeMode.kt | 2 +- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsMiddleware.kt b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsMiddleware.kt index 47497bd0b1..6a72a5811a 100644 --- a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsMiddleware.kt @@ -47,7 +47,7 @@ import timber.log.Timber class DetailsMiddleware { private val eraseWalletMiddleware = EraseWalletMiddleware() private val manageSecurityMiddleware = ManageSecurityMiddleware() - private val managePrivacyMiddleware = ManagePrivacyMiddleware() + private val appSettingsMiddleware = AppSettingsMiddleware() private val accessCodeRecoveryMiddleware = AccessCodeRecoveryMiddleware() val detailsMiddleware: Middleware = { _, stateProvider -> { next -> @@ -67,7 +67,7 @@ class DetailsMiddleware { when (action) { is DetailsAction.ResetToFactory -> eraseWalletMiddleware.handle(action) is DetailsAction.ManageSecurity -> manageSecurityMiddleware.handle(action, state) - is DetailsAction.AppSettings -> managePrivacyMiddleware.handle(state, action) + is DetailsAction.AppSettings -> appSettingsMiddleware.handle(state, action) is DetailsAction.ReCreateTwinsWallet -> { store.dispatch(TwinCardsAction.SetMode(CreateTwinWalletMode.RecreateWallet)) store.dispatch(NavigationAction.NavigateTo(AppScreen.OnboardingTwins)) @@ -207,7 +207,7 @@ class DetailsMiddleware { } } - class ManagePrivacyMiddleware { + class AppSettingsMiddleware { fun handle(state: DetailsState, action: DetailsAction.AppSettings) { when (action) { is DetailsAction.AppSettings.SwitchPrivacySetting -> { @@ -261,6 +261,8 @@ class DetailsMiddleware { scope.launch { repository.changeAppThemeMode(appThemeMode) + + store.dispatchWithMain(GlobalAction.ChangeAppThemeMode(appThemeMode)) } } diff --git a/domain/app-theme/models/src/main/kotlin/com/tangem/domain/apptheme/model/AppThemeMode.kt b/domain/app-theme/models/src/main/kotlin/com/tangem/domain/apptheme/model/AppThemeMode.kt index db744ebb4c..dd24287e13 100644 --- a/domain/app-theme/models/src/main/kotlin/com/tangem/domain/apptheme/model/AppThemeMode.kt +++ b/domain/app-theme/models/src/main/kotlin/com/tangem/domain/apptheme/model/AppThemeMode.kt @@ -25,7 +25,7 @@ enum class AppThemeMode { /** * The default [AppThemeMode]. */ - val DEFAULT: AppThemeMode = FORCE_LIGHT + val DEFAULT: AppThemeMode = FOLLOW_SYSTEM /** * List of available [AppThemeMode]s. From bb7bef8ab7fc476d8558f14c9586b7cbbf76f50e Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 11 Sep 2023 11:21:12 +0300 Subject: [PATCH 027/242] Updated on 2026-08-14 --- .../tap/di/domain/AppCurrencyDomainModule.kt | 16 +++++++++-- .../DefaultAppCurrencyRepository.kt | 2 ++ .../GetAvailableCurrenciesUseCase.kt | 27 +++++++++++++++++++ .../appcurrency/SelectAppCurrencyUseCase.kt | 14 ++++++++++ .../error/AvailableCurrenciesError.kt | 8 ++++++ 5 files changed, 65 insertions(+), 2 deletions(-) create mode 100644 domain/app-currency/src/main/kotlin/com/tangem/domain/appcurrency/GetAvailableCurrenciesUseCase.kt create mode 100644 domain/app-currency/src/main/kotlin/com/tangem/domain/appcurrency/SelectAppCurrencyUseCase.kt create mode 100644 domain/app-currency/src/main/kotlin/com/tangem/domain/appcurrency/error/AvailableCurrenciesError.kt diff --git a/app/src/main/java/com/tangem/tap/di/domain/AppCurrencyDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/AppCurrencyDomainModule.kt index c69bca18d1..ab7bc24782 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/AppCurrencyDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/AppCurrencyDomainModule.kt @@ -1,22 +1,34 @@ package com.tangem.tap.di.domain +import com.tangem.domain.appcurrency.GetAvailableCurrenciesUseCase import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase +import com.tangem.domain.appcurrency.SelectAppCurrencyUseCase import com.tangem.domain.appcurrency.repository.AppCurrencyRepository import dagger.Module import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.android.components.ViewModelComponent -import dagger.hilt.android.scopes.ViewModelScoped @Module @InstallIn(ViewModelComponent::class) internal object AppCurrencyDomainModule { @Provides - @ViewModelScoped fun provideGetSelectedAppCurrencyUseCase( appCurrencyRepository: AppCurrencyRepository, ): GetSelectedAppCurrencyUseCase { return GetSelectedAppCurrencyUseCase(appCurrencyRepository) } + + @Provides + fun provideSelectAppCurrencyUseCase(appCurrencyRepository: AppCurrencyRepository): SelectAppCurrencyUseCase { + return SelectAppCurrencyUseCase(appCurrencyRepository) + } + + @Provides + fun provideGetAvailableCurrenciesUseCase( + appCurrencyRepository: AppCurrencyRepository, + ): GetAvailableCurrenciesUseCase { + return GetAvailableCurrenciesUseCase(appCurrencyRepository) + } } \ No newline at end of file diff --git a/data/app-currency/src/main/kotlin/com/tangem/data/appcurrency/DefaultAppCurrencyRepository.kt b/data/app-currency/src/main/kotlin/com/tangem/data/appcurrency/DefaultAppCurrencyRepository.kt index c2fe02a2b5..a20bd2309b 100644 --- a/data/app-currency/src/main/kotlin/com/tangem/data/appcurrency/DefaultAppCurrencyRepository.kt +++ b/data/app-currency/src/main/kotlin/com/tangem/data/appcurrency/DefaultAppCurrencyRepository.kt @@ -88,6 +88,8 @@ internal class DefaultAppCurrencyRepository( Timber.e(e, "Unable to fetch available currencies") availableAppCurrenciesStore.store(getDefaultCurrenciesResponse()) + + throw e } } diff --git a/domain/app-currency/src/main/kotlin/com/tangem/domain/appcurrency/GetAvailableCurrenciesUseCase.kt b/domain/app-currency/src/main/kotlin/com/tangem/domain/appcurrency/GetAvailableCurrenciesUseCase.kt new file mode 100644 index 0000000000..90670a27da --- /dev/null +++ b/domain/app-currency/src/main/kotlin/com/tangem/domain/appcurrency/GetAvailableCurrenciesUseCase.kt @@ -0,0 +1,27 @@ +package com.tangem.domain.appcurrency + +import arrow.core.Either +import arrow.core.NonEmptyList +import arrow.core.raise.catch +import arrow.core.raise.either +import arrow.core.raise.ensureNotNull +import arrow.core.toNonEmptyListOrNull +import com.tangem.domain.appcurrency.error.AvailableCurrenciesError +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.appcurrency.repository.AppCurrencyRepository + +// TODO: Add tests +class GetAvailableCurrenciesUseCase( + private val appCurrencyRepository: AppCurrencyRepository, +) { + + suspend operator fun invoke(): Either> = either { + val currencies = catch({ appCurrencyRepository.getAvailableAppCurrencies() }) { + raise(AvailableCurrenciesError.DataError(it)) + } + + ensureNotNull(currencies.toNonEmptyListOrNull()) { + AvailableCurrenciesError.CurrenciesIsEmpty + } + } +} \ No newline at end of file diff --git a/domain/app-currency/src/main/kotlin/com/tangem/domain/appcurrency/SelectAppCurrencyUseCase.kt b/domain/app-currency/src/main/kotlin/com/tangem/domain/appcurrency/SelectAppCurrencyUseCase.kt new file mode 100644 index 0000000000..25d7c9c739 --- /dev/null +++ b/domain/app-currency/src/main/kotlin/com/tangem/domain/appcurrency/SelectAppCurrencyUseCase.kt @@ -0,0 +1,14 @@ +package com.tangem.domain.appcurrency + +import arrow.core.Either +import com.tangem.domain.appcurrency.repository.AppCurrencyRepository + +// TODO: Add tests +class SelectAppCurrencyUseCase( + private val appCurrencyRepository: AppCurrencyRepository, +) { + + suspend operator fun invoke(currencyCode: String): Either { + return Either.catch { appCurrencyRepository.changeAppCurrency(currencyCode) } + } +} \ No newline at end of file diff --git a/domain/app-currency/src/main/kotlin/com/tangem/domain/appcurrency/error/AvailableCurrenciesError.kt b/domain/app-currency/src/main/kotlin/com/tangem/domain/appcurrency/error/AvailableCurrenciesError.kt new file mode 100644 index 0000000000..a80e0bc920 --- /dev/null +++ b/domain/app-currency/src/main/kotlin/com/tangem/domain/appcurrency/error/AvailableCurrenciesError.kt @@ -0,0 +1,8 @@ +package com.tangem.domain.appcurrency.error + +sealed class AvailableCurrenciesError { + + object CurrenciesIsEmpty : AvailableCurrenciesError() + + data class DataError(val cause: Throwable) : AvailableCurrenciesError() +} \ No newline at end of file From c4ec5774a39a1489dce4f936f861c3cc12e39a6b Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 8 Sep 2023 16:30:14 +0800 Subject: [PATCH 028/242] Updated on 2026-08-14 --- .../components/notifications/Notification.kt | 339 +++++++++++------- .../notifications/NotificationConfig.kt | 42 +++ .../notifications/NotificationState.kt | 72 ---- .../state/components/WalletNotification.kt | 51 ++- .../components/common/WalletNotifications.kt | 6 +- 5 files changed, 284 insertions(+), 226 deletions(-) create mode 100644 core/ui/src/main/java/com/tangem/core/ui/components/notifications/NotificationConfig.kt delete mode 100644 core/ui/src/main/java/com/tangem/core/ui/components/notifications/NotificationState.kt diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/notifications/Notification.kt b/core/ui/src/main/java/com/tangem/core/ui/components/notifications/Notification.kt index 76b7894881..b465ae8d17 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/notifications/Notification.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/notifications/Notification.kt @@ -1,112 +1,119 @@ package com.tangem.core.ui.components.notifications import androidx.annotation.DrawableRes +import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.ExperimentalAnimationApi import androidx.compose.foundation.Image -import androidx.compose.foundation.background +import androidx.compose.foundation.LocalIndication import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.* -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.Icon -import androidx.compose.material3.Text +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.* import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberUpdatedState import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.painterResource +import androidx.compose.ui.semantics.Role import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider import com.tangem.core.ui.R -import com.tangem.core.ui.components.SpacerH2 +import com.tangem.core.ui.components.PrimaryButton +import com.tangem.core.ui.components.PrimaryButtonIconEnd +import com.tangem.core.ui.components.SecondaryButton import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resolveReference -import com.tangem.core.ui.res.TangemColorPalette import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.components.notifications.NotificationConfig.ButtonsState as NotificationButtonsState /** * Notification component from Design system. * Use this for Notification with title, subtitle, clickable or not. * - * @param state component state + * @param config component config * @param modifier modifier + * @param iconTint icon tint * * @see Figma component */ @Composable -fun Notification(state: NotificationState, modifier: Modifier = Modifier) { - Box( - modifier = modifier - .clip(shape = RoundedCornerShape(size = TangemTheme.dimens.radius18)) - .background( - color = TangemTheme.colors.button.secondary, - shape = RoundedCornerShape(size = TangemTheme.dimens.radius18), - ) - .clickable( - enabled = when (state) { - is NotificationState.Clickable -> true - is NotificationState.Simple, is NotificationState.Closable -> false - }, - onClick = if (state is NotificationState.Clickable) { - state.onClick - } else { - {} - }, - ), - ) { - Box( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = TangemTheme.dimens.spacing12, vertical = TangemTheme.dimens.spacing8), +fun Notification(config: NotificationConfig, modifier: Modifier = Modifier, iconTint: Color? = null) { + BaseContainer(buttonsState = config.buttonsState, modifier = modifier) { + Column( + modifier = Modifier.padding(all = TangemTheme.dimens.spacing12), + verticalArrangement = Arrangement.spacedBy(space = TangemTheme.dimens.spacing12), ) { - NotificationIcon( - iconResId = state.iconResId, - iconTint = state.tint, - modifier = Modifier - .size(size = TangemTheme.dimens.size20) - .align(alignment = Alignment.CenterStart), + MainContent( + iconResId = config.iconResId, + iconTint = iconTint, + title = config.title, + subtitle = config.subtitle, ) - NotificationInfoBlock( - title = state.title.resolveReference(), - subtitle = state.subtitle?.resolveReference(), - modifier = Modifier.align(alignment = Alignment.CenterStart), - ) - - if (state is NotificationState.Closable) { - Icon( - modifier = Modifier - .size(size = TangemTheme.dimens.size20) - .align(alignment = Alignment.TopEnd), - painter = painterResource(id = R.drawable.ic_close_24), - contentDescription = null, - tint = TangemTheme.colors.icon.informative, - ) - } - - if (state is NotificationState.Clickable) { - Icon( - modifier = Modifier - .size(size = TangemTheme.dimens.size20) - .align(alignment = Alignment.CenterEnd), - painter = painterResource(id = R.drawable.ic_chevron_right_24), - contentDescription = null, - tint = TangemTheme.colors.icon.informative, - ) - } + Buttons(state = config.buttonsState) } + + CloseableIconButton( + onClick = config.onCloseClick, + modifier = Modifier.align(alignment = Alignment.TopEnd), + ) } } @Composable -private fun NotificationIcon(@DrawableRes iconResId: Int, iconTint: Color?, modifier: Modifier = Modifier) { - if (iconTint != null) { +private fun BaseContainer( + buttonsState: NotificationConfig.ButtonsState?, + modifier: Modifier = Modifier, + content: @Composable BoxScope.() -> Unit, +) { + val containerColor by rememberUpdatedState( + newValue = if (buttonsState != null) { + TangemTheme.colors.background.primary + } else { + TangemTheme.colors.button.disabled + }, + ) + + Surface( + modifier = modifier + .defaultMinSize(minHeight = TangemTheme.dimens.size62) + .wrapContentWidth(), + shape = TangemTheme.shapes.roundedCornersXMedium, + color = containerColor, + ) { + Box(content = content) + } +} + +@Composable +private fun MainContent(iconResId: Int, iconTint: Color?, title: TextReference, subtitle: TextReference) { + Row(horizontalArrangement = Arrangement.spacedBy(space = TangemTheme.dimens.spacing10)) { + Icon( + iconResId = iconResId, + tint = iconTint, + modifier = Modifier.align(alignment = Alignment.CenterVertically), + ) + + TextsBlock(title = title, subtitle = subtitle) + } +} + +@Composable +private fun Icon(@DrawableRes iconResId: Int, tint: Color?, modifier: Modifier = Modifier) { + if (tint != null) { Icon( painter = painterResource(id = iconResId), contentDescription = null, modifier = modifier, - tint = iconTint, + tint = tint, ) } else { Image( @@ -118,20 +125,107 @@ private fun NotificationIcon(@DrawableRes iconResId: Int, iconTint: Color?, modi } @Composable -private fun NotificationInfoBlock(title: String, subtitle: String?, modifier: Modifier = Modifier) { - Column(modifier = modifier.padding(horizontal = TangemTheme.dimens.spacing30)) { +private fun TextsBlock(title: TextReference, subtitle: TextReference) { + Column(verticalArrangement = Arrangement.spacedBy(space = TangemTheme.dimens.spacing2)) { Text( - text = title, + text = title.resolveReference(), color = TangemTheme.colors.text.primary1, style = TangemTheme.typography.body2, ) - if (!subtitle.isNullOrEmpty()) { - SpacerH2() - Text( - text = subtitle, - color = TangemTheme.colors.text.tertiary, - style = TangemTheme.typography.caption, + Text( + text = subtitle.resolveReference(), + color = TangemTheme.colors.text.tertiary, + style = TangemTheme.typography.caption, + ) + } +} + +@OptIn(ExperimentalAnimationApi::class) +@Composable +private fun Buttons(state: NotificationButtonsState?) { + AnimatedContent(targetState = state, label = "Update the buttons content") { animatedState -> + when (animatedState) { + is NotificationButtonsState.SecondaryButtonConfig -> SingleSecondaryButton(config = animatedState) + is NotificationButtonsState.PrimaryButtonConfig -> SinglePrimaryButton(config = animatedState) + is NotificationButtonsState.PairButtonsConfig -> PairButtons(config = animatedState) + null -> Unit + } + } +} + +@Composable +private fun SingleSecondaryButton(config: NotificationButtonsState.SecondaryButtonConfig) { + SecondaryButton( + text = config.text.resolveReference(), + onClick = config.onClick, + modifier = Modifier.fillMaxWidth(), + ) +} + +@Composable +private fun SinglePrimaryButton(config: NotificationButtonsState.PrimaryButtonConfig) { + if (config.iconResId != null) { + PrimaryButtonIconEnd( + text = config.text.resolveReference(), + iconResId = config.iconResId, + onClick = config.onClick, + modifier = Modifier.fillMaxWidth(), + ) + } else { + PrimaryButton( + text = config.text.resolveReference(), + onClick = config.onClick, + modifier = Modifier.fillMaxWidth(), + ) + } +} + +@Composable +private fun PairButtons(config: NotificationButtonsState.PairButtonsConfig) { + Row(horizontalArrangement = Arrangement.spacedBy(space = TangemTheme.dimens.spacing8)) { + SecondaryButton( + text = config.secondaryText.resolveReference(), + onClick = config.onSecondaryClick, + modifier = Modifier.weight(weight = 1f), + ) + + PrimaryButton( + text = config.primaryText.resolveReference(), + onClick = config.onPrimaryClick, + modifier = Modifier.weight(weight = 1f), + ) + } +} + +@Composable +private fun CloseableIconButton(onClick: (() -> Unit)?, modifier: Modifier = Modifier) { + AnimatedVisibility(visible = onClick != null, modifier = modifier) { + onClick ?: return@AnimatedVisibility + + /* + * Implement a custom ripple because the design layout doesn't match the Material Design. + * Material Icon has a size 24x24 and Material IconButton has a size 48x48, + * but icon from Figma has a size 16x16. + */ + Box( + modifier = Modifier + .size(size = TangemTheme.dimens.size40) + .clip(shape = CircleShape) + .clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = LocalIndication.current, + role = Role.Button, + onClick = onClick, + ), + ) { + Icon( + painter = painterResource(id = R.drawable.ic_close_24), + contentDescription = null, + modifier = Modifier + .size(size = TangemTheme.dimens.size16) + .align(alignment = Alignment.Center), + tint = TangemTheme.colors.icon.inactive, ) } } @@ -139,72 +233,77 @@ private fun NotificationInfoBlock(title: String, subtitle: String?, modifier: Mo @Preview @Composable -private fun Preview_WarningNotification_Light( - @PreviewParameter(NotificationStateProvider::class) - state: NotificationState, +private fun Preview_Notification_Light( + @PreviewParameter(NotificationConfigProvider::class) + config: NotificationConfig, ) { TangemTheme(isDark = false) { - Notification(state) + Notification(config) } } @Preview @Composable -private fun Preview_WarningNotification_Dark( - @PreviewParameter(NotificationStateProvider::class) - state: NotificationState, +private fun Preview_Notification_Dark( + @PreviewParameter(NotificationConfigProvider::class) config: NotificationConfig, ) { TangemTheme(isDark = true) { - Notification(state) + Notification(config) } } -private class NotificationStateProvider : CollectionPreviewParameterProvider( +private class NotificationConfigProvider : CollectionPreviewParameterProvider( collection = listOf( - NotificationState.Simple( - title = TextReference.Str(value = "Your wallet hasn’t been backed up"), + NotificationConfig( + title = TextReference.Str(value = "Development card"), subtitle = TextReference.Str( - value = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt " + - "ut labore et...", + value = "The card you scanned is a development card.\nDon’t accept it as a payment.", ), + iconResId = R.drawable.ic_alert_circle_24, + ), + NotificationConfig( + title = TextReference.Str(value = "Some networks are unreachable"), + subtitle = TextReference.Str(value = "Check your network connection"), iconResId = R.drawable.img_attention_20, ), - NotificationState.Simple( - title = TextReference.Str("Your wallet hasn’t been backed up"), - subtitle = null, - iconResId = R.drawable.ic_alert_circle_24, - tint = TangemColorPalette.Amaranth, - ), - NotificationState.Clickable( + NotificationConfig( title = TextReference.Str(value = "Your wallet hasn’t been backed up"), - subtitle = TextReference.Str( - value = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt " + - "ut labore et...", - ), + subtitle = TextReference.Str(value = "To protect your assets, we advise you to carry out this procedure"), iconResId = R.drawable.img_attention_20, - onClick = {}, - ), - NotificationState.Clickable( - title = TextReference.Str(value = "Your wallet hasn’t been backed up"), - subtitle = null, - iconResId = R.drawable.ic_alert_circle_24, - tint = TangemColorPalette.Amaranth, - onClick = {}, - ), - NotificationState.Closable( - title = TextReference.Str(value = "Your wallet hasn’t been backed up"), - subtitle = TextReference.Str( - value = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt " + - "ut labore et...", + buttonsState = NotificationButtonsState.SecondaryButtonConfig( + text = TextReference.Str(value = "Start backup process"), + onClick = {}, ), - iconResId = R.drawable.img_attention_20, - onCloseClick = {}, ), - NotificationState.Closable( - title = TextReference.Str(value = "Your wallet hasn’t been backed up"), - subtitle = null, + NotificationConfig( + title = TextReference.Str(value = "Some addresses are missing"), + subtitle = TextReference.Str(value = "Generate addresses for 2 new networks using your card"), iconResId = R.drawable.ic_alert_circle_24, - tint = TangemColorPalette.Amaranth, + buttonsState = NotificationButtonsState.PrimaryButtonConfig( + text = TextReference.Str(value = "Generate addresses"), + iconResId = R.drawable.ic_tangem_24, + onClick = {}, + ), + ), + NotificationConfig( + title = TextReference.Str(value = "Rate the app"), + subtitle = TextReference.Str(value = "How do you like Tangem?"), + iconResId = R.drawable.img_attention_20, + buttonsState = NotificationButtonsState.PairButtonsConfig( + primaryText = TextReference.Str(value = "Love it!"), + onPrimaryClick = {}, + secondaryText = TextReference.Str(value = "Can be better"), + onSecondaryClick = {}, + ), + ), + NotificationConfig( + title = TextReference.Str(value = "Note top up"), + subtitle = TextReference.Str(value = "To activate card top up it with at least 1 XLM"), + iconResId = R.drawable.ic_alert_circle_24, + buttonsState = NotificationButtonsState.SecondaryButtonConfig( + text = TextReference.Str(value = "Top up card"), + onClick = {}, + ), onCloseClick = {}, ), ), diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/notifications/NotificationConfig.kt b/core/ui/src/main/java/com/tangem/core/ui/components/notifications/NotificationConfig.kt new file mode 100644 index 0000000000..d0d5600139 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/notifications/NotificationConfig.kt @@ -0,0 +1,42 @@ +package com.tangem.core.ui.components.notifications + +import androidx.annotation.DrawableRes +import com.tangem.core.ui.extensions.TextReference + +/** + * Notification component state + * + * @property title title + * @property subtitle subtitle + * @property iconResId icon resource id + * @property buttonsState buttons state + * @property onCloseClick lambda be invoked when close button is clicked + * +[REDACTED_AUTHOR] + */ +data class NotificationConfig( + val title: TextReference, + val subtitle: TextReference, + @DrawableRes val iconResId: Int, + val buttonsState: ButtonsState? = null, + val onCloseClick: (() -> Unit)? = null, +) { + + sealed class ButtonsState { + + data class PrimaryButtonConfig( + val text: TextReference, + @DrawableRes val iconResId: Int? = null, + val onClick: () -> Unit, + ) : ButtonsState() + + data class SecondaryButtonConfig(val text: TextReference, val onClick: () -> Unit) : ButtonsState() + + data class PairButtonsConfig( + val primaryText: TextReference, + val onPrimaryClick: () -> Unit, + val secondaryText: TextReference, + val onSecondaryClick: () -> Unit, + ) : ButtonsState() + } +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/notifications/NotificationState.kt b/core/ui/src/main/java/com/tangem/core/ui/components/notifications/NotificationState.kt deleted file mode 100644 index f787e62a66..0000000000 --- a/core/ui/src/main/java/com/tangem/core/ui/components/notifications/NotificationState.kt +++ /dev/null @@ -1,72 +0,0 @@ -package com.tangem.core.ui.components.notifications - -import androidx.annotation.DrawableRes -import androidx.compose.ui.graphics.Color -import com.tangem.core.ui.extensions.TextReference - -/** - * Notification component state - * - * @property title title - * @property subtitle subtitle - * @property iconResId icon resource id - * @property tint icon tint - * -[REDACTED_AUTHOR] - */ -sealed class NotificationState( - open val title: TextReference, - open val subtitle: TextReference? = null, - @DrawableRes open val iconResId: Int, - open val tint: Color? = null, -) { - - /** - * Simple notification state. Non clickable. - * - * @property title title - * @property subtitle subtitle - * @property iconResId icon resource id - * @property tint icon tint - */ - data class Simple( - override val title: TextReference, - override val subtitle: TextReference? = null, - @DrawableRes override val iconResId: Int, - override val tint: Color? = null, - ) : NotificationState(title, subtitle, iconResId, tint) - - /** - * Clickable notification state - * - * @property title title - * @property subtitle subtitle - * @property iconResId icon resource id - * @property tint icon tint - * @property onClick lambda be invoked when notification component is clicked - */ - data class Clickable( - override val title: TextReference, - override val subtitle: TextReference? = null, - @DrawableRes override val iconResId: Int, - override val tint: Color? = null, - val onClick: () -> Unit, - ) : NotificationState(title, subtitle, iconResId, tint) - - /** - * Closable notification state - * - * @property title title - * @property subtitle subtitle - * @property iconResId icon resource id - * @property tint icon tint - * @property onCloseClick lambda be invoked when close button is clicked - */ - data class Closable( - override val title: TextReference, - override val subtitle: TextReference? = null, - @DrawableRes override val iconResId: Int, - override val tint: Color? = null, - val onCloseClick: (() -> Unit)? = null, - ) : NotificationState(title, subtitle, iconResId, tint) -} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/components/WalletNotification.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/components/WalletNotification.kt index 60abf781ae..e5e9221005 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/components/WalletNotification.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/components/WalletNotification.kt @@ -1,22 +1,21 @@ package com.tangem.feature.wallet.presentation.wallet.state.components import androidx.compose.runtime.Immutable -import com.tangem.core.ui.components.notifications.NotificationState +import com.tangem.core.ui.components.notifications.NotificationConfig import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.WrappedList -import com.tangem.core.ui.res.TangemColorPalette import com.tangem.feature.wallet.impl.R /** * Wallet notification component state * - * @property state state + * @property config state * [REDACTED_AUTHOR] */ // TODO: Finalize notification strings [REDACTED_JIRA] @Immutable -sealed class WalletNotification(open val state: NotificationState) { +sealed class WalletNotification(open val config: NotificationConfig) { /** Clickable notification */ sealed interface Clickable { @@ -27,41 +26,37 @@ sealed class WalletNotification(open val state: NotificationState) { /** "Development card" notification */ object DevCard : WalletNotification( - state = NotificationState.Simple( + config = NotificationConfig( title = TextReference.Res(id = R.string.common_warning), subtitle = TextReference.Res(id = R.string.alert_developer_card), iconResId = R.drawable.ic_alert_circle_24, - tint = TangemColorPalette.Amaranth, ), ) /** "Test card" notification */ object TestCard : WalletNotification( - state = NotificationState.Simple( + config = NotificationConfig( title = TextReference.Res(id = R.string.common_warning), subtitle = TextReference.Res(id = R.string.warning_testnet_card_message), iconResId = R.drawable.ic_alert_circle_24, - tint = TangemColorPalette.Amaranth, ), ) /** "Demo card" notification */ object DemoCard : WalletNotification( - state = NotificationState.Simple( + config = NotificationConfig( title = TextReference.Res(id = R.string.common_warning), subtitle = TextReference.Res(id = R.string.alert_demo_message), iconResId = R.drawable.ic_alert_circle_24, - tint = TangemColorPalette.Amaranth, ), ) /** "Card verification failed" notification */ object CardVerificationFailed : WalletNotification( - state = NotificationState.Simple( + config = NotificationConfig( title = TextReference.Res(id = R.string.warning_failed_to_verify_card_title), subtitle = TextReference.Res(id = R.string.warning_failed_to_verify_card_message), iconResId = R.drawable.ic_alert_circle_24, - tint = TangemColorPalette.Amaranth, ), ) @@ -71,14 +66,13 @@ sealed class WalletNotification(open val state: NotificationState) { * @param count number of remaining signatures */ class RemainingSignaturesLeft(count: Int) : WalletNotification( - state = NotificationState.Simple( + config = NotificationConfig( title = TextReference.Res(id = R.string.common_warning), subtitle = TextReference.Res( id = R.string.warning_low_signatures_format, formatArgs = WrappedList(data = listOf(count)), ), iconResId = R.drawable.ic_alert_circle_24, - tint = TangemColorPalette.Amaranth, ), ) @@ -88,11 +82,10 @@ sealed class WalletNotification(open val state: NotificationState) { * @property onClick lambda be invoked when notification's close button is clicked */ data class WarningAlreadySignedHashes(override val onClick: () -> Unit) : Clickable, WalletNotification( - state = NotificationState.Closable( + config = NotificationConfig( title = TextReference.Res(id = R.string.common_warning), subtitle = TextReference.Res(id = R.string.alert_card_signed_transactions), iconResId = R.drawable.img_attention_20, - tint = null, onCloseClick = onClick, ), ) @@ -103,15 +96,13 @@ sealed class WalletNotification(open val state: NotificationState) { * @property onClick lambda be invoked when notification is clicked */ data class CriticalWarningAlreadySignedHashes(override val onClick: () -> Unit) : Clickable, WalletNotification( - state = NotificationState.Clickable( + config = NotificationConfig( title = TextReference.Res( id = R.string.warning_important_security_info, formatArgs = WrappedList(listOf("\u26A0")), ), subtitle = TextReference.Res(id = R.string.warning_signed_tx_previously), iconResId = R.drawable.img_attention_20, - onClick = onClick, - tint = null, ), ) @@ -121,20 +112,19 @@ sealed class WalletNotification(open val state: NotificationState) { * @property onClick lambda be invoked when notification is clicked */ data class BackupCard(override val onClick: () -> Unit) : Clickable, WalletNotification( - state = NotificationState.Clickable( + config = NotificationConfig( title = TextReference.Str(value = "Backup your card"), + subtitle = TextReference.Str(value = ""), iconResId = R.drawable.img_attention_20, - onClick = onClick, - tint = null, ), ) /** "Unreachable networks" notification */ object UnreachableNetworks : WalletNotification( - state = NotificationState.Simple( + config = NotificationConfig( title = TextReference.Str(value = "Some networks are unreachable"), + subtitle = TextReference.Str(value = ""), iconResId = R.drawable.img_attention_20, - tint = null, ), ) @@ -144,11 +134,10 @@ sealed class WalletNotification(open val state: NotificationState) { * @property onClick lambda be invoked when notification is clicked */ data class LikeTangemApp(override val onClick: () -> Unit) : Clickable, WalletNotification( - state = NotificationState.Clickable( + config = NotificationConfig( title = TextReference.Str(value = "Like Tangem App?"), + subtitle = TextReference.Str(value = ""), iconResId = R.drawable.ic_star_24, - onClick = onClick, - tint = TangemColorPalette.Tangerine, ), ) @@ -158,10 +147,10 @@ sealed class WalletNotification(open val state: NotificationState) { * @property onClick lambda be invoked when notification is clicked */ data class ScanCard(override val onClick: () -> Unit) : Clickable, WalletNotification( - state = NotificationState.Clickable( + config = NotificationConfig( title = TextReference.Str(value = "Scan your card to continue"), + subtitle = TextReference.Str(value = ""), iconResId = R.drawable.ic_tangem_24, - onClick = onClick, ), ) @@ -171,10 +160,10 @@ sealed class WalletNotification(open val state: NotificationState) { * @property onClick lambda be invoked when notification is clicked */ data class UnlockWallets(override val onClick: () -> Unit) : Clickable, WalletNotification( - state = NotificationState.Clickable( + config = NotificationConfig( title = TextReference.Str(value = "Unlock needed"), + subtitle = TextReference.Str(value = ""), iconResId = R.drawable.ic_locked_24, - onClick = onClick, ), ) } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletNotifications.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletNotifications.kt index c6ef7f89ef..730832731b 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletNotifications.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletNotifications.kt @@ -20,8 +20,8 @@ import kotlinx.collections.immutable.ImmutableList internal fun LazyListScope.notifications(configs: ImmutableList, modifier: Modifier = Modifier) { items( items = configs, - key = { it.state.title.hashCode() }, - contentType = { it.state::class.java }, - itemContent = { Notification(state = it.state, modifier = modifier.animateItemPlacement()) }, + key = { it.config.title.hashCode() }, + contentType = { it.config::class.java }, + itemContent = { Notification(config = it.config, modifier = modifier.animateItemPlacement()) }, ) } \ No newline at end of file From c62c6b032b91954c60d8a1f5978664a3e501095f Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 11 Sep 2023 12:53:42 +0300 Subject: [PATCH 029/242] Updated on 2026-08-14 --- .../com/tangem/core/ui/event/EventEffect.kt | 9 +++-- .../com/tangem/core/ui/event/StateEvent.kt | 38 ++++++++++--------- .../presentation/common/WalletPreviewData.kt | 4 +- .../OrganizeTokensStateHolder.kt | 12 +++--- .../model/OrganizeTokensState.kt | 2 +- 5 files changed, 35 insertions(+), 30 deletions(-) diff --git a/core/ui/src/main/java/com/tangem/core/ui/event/EventEffect.kt b/core/ui/src/main/java/com/tangem/core/ui/event/EventEffect.kt index 49abda9299..e6f3ad0d84 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/event/EventEffect.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/event/EventEffect.kt @@ -13,11 +13,12 @@ import androidx.compose.runtime.NonRestartableComposable */ @Composable @NonRestartableComposable -fun EventEffect(event: StateEvent, onTrigger: suspend () -> Unit) { +@Suppress("UnnecessaryEventHandlerParameter") +fun EventEffect(event: StateEvent, onTrigger: suspend (data: A) -> Unit) { LaunchedEffect(event) { - if (event is StateEvent.Triggered) { - onTrigger() - event.consume() + if (event is StateEvent.Triggered) { + onTrigger(event.data) + event.onConsume() } } } \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/event/StateEvent.kt b/core/ui/src/main/java/com/tangem/core/ui/event/StateEvent.kt index d0f495b944..1860808b1d 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/event/StateEvent.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/event/StateEvent.kt @@ -9,32 +9,34 @@ import androidx.compose.runtime.Immutable * re-triggered on recompositions or state changes. */ @Immutable -sealed class StateEvent { - - /** Defines the action to be executed when the event is consumed. */ - protected abstract val onConsume: () -> Unit +sealed class StateEvent { /** * Represents an already consumed state event. * Events of this type will not trigger any further actions. */ - object Consumed : StateEvent() { - override val onConsume: () -> Unit = {} + class Consumed : StateEvent() { + + override fun equals(other: Any?): Boolean { + if (this === other) return true + return other is Consumed<*> + } + + override fun hashCode(): Int { + return javaClass.hashCode() + } } /** * Represents a state event that has been triggered but not yet consumed. * + * @property data The data provided by the event. * @property onConsume The action to be executed when the event is consumed. */ - data class Triggered(override val onConsume: () -> Unit) : StateEvent() - - /** - * Consumes the event, triggering any associated action. - */ - fun consume() { - onConsume() - } + data class Triggered( + val data: A, + internal val onConsume: () -> Unit, + ) : StateEvent() } /** @@ -43,9 +45,11 @@ sealed class StateEvent { * @param onConsume The action to be executed when the event is consumed. * @return A triggered state event. */ -fun triggered(onConsume: () -> Unit): StateEvent.Triggered = StateEvent.Triggered(onConsume) +fun triggeredEvent(data: A, onConsume: () -> Unit): StateEvent = StateEvent.Triggered(data, onConsume) /** - * Represents a statically defined [StateEvent.Consumed] event. + * Creates a [StateEvent.Consumed] instance. + * + * @return A consumed state event. */ -val consumed: StateEvent.Consumed = StateEvent.Consumed \ No newline at end of file +fun consumedEvent(): StateEvent = StateEvent.Consumed() \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/WalletPreviewData.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/WalletPreviewData.kt index 7562581a91..8fd27445cb 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/WalletPreviewData.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/WalletPreviewData.kt @@ -6,7 +6,7 @@ import com.tangem.core.ui.components.marketprice.MarketPriceBlockState import com.tangem.core.ui.components.marketprice.PriceChangeConfig import com.tangem.core.ui.components.transactions.state.TransactionState import com.tangem.core.ui.components.transactions.state.TxHistoryState -import com.tangem.core.ui.event.consumed +import com.tangem.core.ui.event.consumedEvent import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.res.TangemColorPalette @@ -276,7 +276,7 @@ internal object WalletPreviewData { onApplyClick = {}, onCancelClick = {}, ), - scrollListToTop = consumed, + scrollListToTop = consumedEvent(), ) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensStateHolder.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensStateHolder.kt index 4552ef0050..a00a26f468 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensStateHolder.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensStateHolder.kt @@ -1,8 +1,8 @@ package com.tangem.feature.wallet.presentation.organizetokens import com.tangem.common.Provider -import com.tangem.core.ui.event.consumed -import com.tangem.core.ui.event.triggered +import com.tangem.core.ui.event.consumedEvent +import com.tangem.core.ui.event.triggeredEvent import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.tokens.error.TokenListError import com.tangem.domain.tokens.error.TokenListSortingError @@ -66,7 +66,7 @@ internal class OrganizeTokensStateHolder( fun updateStateAfterTokenListSorting(tokenList: TokenList) { updateState { tokenListConverter.convert(tokenList).copy( - scrollListToTop = triggered(::consumeScrollListToTopEvent), + scrollListToTop = triggeredEvent(Unit, ::consumeScrollListToTopEvent), ) } } @@ -114,15 +114,15 @@ internal class OrganizeTokensStateHolder( onItemDragEnd = dragAndDropIntents::onItemDraggingEnd, canDragItemOver = dragAndDropIntents::canDragItemOver, ), - scrollListToTop = consumed, + scrollListToTop = consumedEvent(), ) } - private fun updateState(block: OrganizeTokensState.() -> OrganizeTokensState) { + private inline fun updateState(block: OrganizeTokensState.() -> OrganizeTokensState) { stateFlowInternal.update(block) } private fun consumeScrollListToTopEvent() { - updateState { copy(scrollListToTop = consumed) } + updateState { copy(scrollListToTop = consumedEvent()) } } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/model/OrganizeTokensState.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/model/OrganizeTokensState.kt index a1ae0ff965..a269938058 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/model/OrganizeTokensState.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/model/OrganizeTokensState.kt @@ -11,7 +11,7 @@ internal data class OrganizeTokensState( val header: HeaderConfig, val actions: ActionsConfig, val dndConfig: DragAndDropConfig, - val scrollListToTop: StateEvent, + val scrollListToTop: StateEvent, ) { data class HeaderConfig( From cad4da6f19a698aaa4451109dcabab48a0a7ab62 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 12 Sep 2023 11:55:19 +0300 Subject: [PATCH 030/242] Updated on 2026-08-14 --- app/build.gradle.kts | 1 + .../AppCurrencySelectorFragment.kt | 30 ++ .../appcurrency/AppCurrencySelectorScreen.kt | 363 ++++++++++++++++++ .../appcurrency/AppCurrencySelectorState.kt | 48 +++ 4 files changed, 442 insertions(+) create mode 100644 app/src/main/java/com/tangem/tap/features/details/ui/appcurrency/AppCurrencySelectorFragment.kt create mode 100644 app/src/main/java/com/tangem/tap/features/details/ui/appcurrency/AppCurrencySelectorScreen.kt create mode 100644 app/src/main/java/com/tangem/tap/features/details/ui/appcurrency/AppCurrencySelectorState.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 570f9e96e3..02a97fd3e3 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -95,6 +95,7 @@ dependencies { /** Compose libraries */ implementation(deps.compose.constraintLayout) implementation(deps.compose.material) + implementation(deps.compose.material3) implementation(deps.compose.animation) implementation(deps.compose.coil) implementation(deps.compose.constraintLayout) diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/appcurrency/AppCurrencySelectorFragment.kt b/app/src/main/java/com/tangem/tap/features/details/ui/appcurrency/AppCurrencySelectorFragment.kt new file mode 100644 index 0000000000..72550be701 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/details/ui/appcurrency/AppCurrencySelectorFragment.kt @@ -0,0 +1,30 @@ +package com.tangem.tap.features.details.ui.appcurrency + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.tangem.core.ui.components.SystemBarsEffect +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.screen.ComposeFragment +import com.tangem.core.ui.theme.AppThemeModeHolder +import dagger.hilt.android.AndroidEntryPoint +import javax.inject.Inject + +@AndroidEntryPoint +internal class AppCurrencySelectorFragment : ComposeFragment() { + + @Inject + override lateinit var appThemeModeHolder: AppThemeModeHolder + + @Composable + override fun ScreenContent(modifier: Modifier) { + val systemBarsColor = TangemTheme.colors.background.secondary + SystemBarsEffect { + setSystemBarsColor(systemBarsColor) + } + + AppCurrencySelectorScreen( + modifier = modifier, + state = AppCurrencySelectorState.Loading({}), // TODO: Will be updated in next MR + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/appcurrency/AppCurrencySelectorScreen.kt b/app/src/main/java/com/tangem/tap/features/details/ui/appcurrency/AppCurrencySelectorScreen.kt new file mode 100644 index 0000000000..a38cac5c0a --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/details/ui/appcurrency/AppCurrencySelectorScreen.kt @@ -0,0 +1,363 @@ +package com.tangem.tap.features.details.ui.appcurrency + +import androidx.compose.foundation.LocalIndication +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider +import com.tangem.core.ui.components.CircleShimmer +import com.tangem.core.ui.components.RectangleShimmer +import com.tangem.core.ui.components.SpacerW +import com.tangem.core.ui.event.EventEffect +import com.tangem.core.ui.event.consumedEvent +import com.tangem.core.ui.res.TangemTheme +import com.tangem.tap.features.details.ui.appcurrency.AppCurrencySelectorState.Currency +import com.tangem.wallet.R +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.toPersistentList + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +internal fun AppCurrencySelectorScreen(state: AppCurrencySelectorState, modifier: Modifier = Modifier) { + val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior() + val listState = rememberLazyListState() + + Scaffold( + modifier = modifier.nestedScroll(scrollBehavior.nestedScrollConnection), + containerColor = TangemTheme.colors.background.secondary, + topBar = { + TopBar( + modifier = Modifier.fillMaxWidth(), + scrollBehavior = scrollBehavior, + state = state, + ) + }, + content = { paddingValues -> + val contentModifier = Modifier + .padding(paddingValues) + .fillMaxSize() + + when (state) { + is AppCurrencySelectorState.Loading -> LoadingList( + modifier = contentModifier, + ) + is AppCurrencySelectorState.Content -> CurrenciesList( + modifier = contentModifier, + listState = listState, + currencies = state.items, + selectedId = state.selectedId.orEmpty(), + onCurrencyClick = state.onCurrencyClick, + ) + } + }, + ) + + if (state is AppCurrencySelectorState.Content) { + EventEffect(event = state.scrollToSelected) { + listState.scrollToItem(it) + } + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun TopBar( + scrollBehavior: TopAppBarScrollBehavior, + state: AppCurrencySelectorState, + modifier: Modifier = Modifier, +) { + TopAppBar( + modifier = modifier, + scrollBehavior = scrollBehavior, + colors = TopAppBarColors, + navigationIcon = { + IconButton( + modifier = Modifier + .padding(start = TangemTheme.dimens.spacing8) + .size(TangemTheme.dimens.size32), + onClick = state.onBackClick, + ) { + Icon( + modifier = Modifier.size(TangemTheme.dimens.size24), + painter = painterResource(id = R.drawable.ic_back_24), + contentDescription = null, + ) + } + }, + title = { + when (state) { + is AppCurrencySelectorState.Search -> SearchBar( + modifier = Modifier.fillMaxWidth(), + onInputChange = state.onSearchInputChange, + ) + is AppCurrencySelectorState.Loading, + is AppCurrencySelectorState.Default, + -> Text( + text = stringResource(id = R.string.details_row_title_currency), + style = TangemTheme.typography.subtitle1, + ) + } + }, + actions = { + when (state) { + is AppCurrencySelectorState.Content -> { + IconButton( + modifier = Modifier.size(TangemTheme.dimens.size32), + onClick = state.onTopBarActionClick, + ) { + val iconResId = when (state) { + is AppCurrencySelectorState.Default -> R.drawable.ic_search_24 + is AppCurrencySelectorState.Search -> R.drawable.ic_close_24 + } + val iconTint = when (state) { + is AppCurrencySelectorState.Default -> TangemTheme.colors.icon.primary1 + is AppCurrencySelectorState.Search -> TangemTheme.colors.icon.informative + } + + Icon( + modifier = Modifier.size(TangemTheme.dimens.size24), + painter = painterResource(id = iconResId), + tint = iconTint, + contentDescription = null, + ) + } + } + is AppCurrencySelectorState.Loading -> Unit + } + SpacerW(width = TangemTheme.dimens.spacing8) + }, + ) +} + +@Composable +private fun SearchBar(onInputChange: (String) -> Unit, modifier: Modifier = Modifier) { + val focusRequester = remember { FocusRequester() } + var input by remember { mutableStateOf(value = "") } + + TextField( + modifier = modifier + .focusRequester(focusRequester), + value = input, + onValueChange = { input = it }, + singleLine = true, + textStyle = TangemTheme.typography.subtitle2, + placeholder = { + Text(text = stringResource(id = R.string.common_search)) + }, + colors = SearchBarColors, + ) + + LaunchedEffect(key1 = input) { + onInputChange(input) + } + + LaunchedEffect(Unit) { + focusRequester.requestFocus() + } +} + +@Composable +private fun LoadingList(modifier: Modifier = Modifier) { + Column(modifier = modifier) { + repeat(times = 10) { + Row( + modifier = Modifier + .padding( + start = TangemTheme.dimens.spacing16, + end = TangemTheme.dimens.spacing24, + ) + .height(TangemTheme.dimens.size56) + .fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy( + space = TangemTheme.dimens.spacing16, + alignment = Alignment.Start, + ), + ) { + CircleShimmer(modifier = Modifier.size(TangemTheme.dimens.size24)) + RectangleShimmer( + modifier = Modifier + .height(TangemTheme.dimens.size24) + .fillMaxWidth(), + ) + } + } + } +} + +@Composable +private fun CurrenciesList( + listState: LazyListState, + currencies: ImmutableList, + selectedId: String, + onCurrencyClick: (Currency) -> Unit, + modifier: Modifier = Modifier, +) { + LazyColumn( + modifier = modifier, + state = listState, + ) { + items( + items = currencies, + key = Currency::id, + ) { currency -> + val onClick = remember(key1 = currency) { + { onCurrencyClick(currency) } + } + + CurrencyItem( + modifier = Modifier.fillMaxWidth(), + name = currency.name, + isSelected = currency.id == selectedId, + onClick = onClick, + ) + } + } +} + +@Composable +private fun CurrencyItem(name: String, isSelected: Boolean, onClick: () -> Unit, modifier: Modifier = Modifier) { + val interactionSource = remember { MutableInteractionSource() } + + Row( + modifier = modifier + .clickable( + interactionSource = interactionSource, + indication = LocalIndication.current, + onClick = onClick, + ) + .padding( + start = TangemTheme.dimens.spacing16, + end = TangemTheme.dimens.spacing24, + ) + .heightIn(min = TangemTheme.dimens.size56), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy( + space = TangemTheme.dimens.spacing16, + alignment = Alignment.Start, + ), + ) { + RadioButton( + modifier = Modifier.size(TangemTheme.dimens.size24), + selected = isSelected, + onClick = onClick, + interactionSource = interactionSource, + colors = RadioButtonColors, + ) + Text( + text = name, + style = TangemTheme.typography.body1, + color = TangemTheme.colors.text.primary1, + ) + } +} + +@OptIn(ExperimentalMaterial3Api::class) +private val TopAppBarColors: TopAppBarColors + @Composable + get() = TopAppBarDefaults.topAppBarColors( + containerColor = TangemTheme.colors.background.secondary, + // Currently (08.09.23) it's not working when scrolling programmatically + scrolledContainerColor = TangemTheme.colors.background.secondary, + navigationIconContentColor = TangemTheme.colors.icon.primary1, + titleContentColor = TangemTheme.colors.text.primary1, + actionIconContentColor = TangemTheme.colors.icon.primary1, + ) + +private val SearchBarColors: TextFieldColors + @Composable + get() = TextFieldDefaults.colors( + unfocusedContainerColor = TangemTheme.colors.background.secondary, + focusedContainerColor = TangemTheme.colors.background.secondary, + focusedTextColor = TangemTheme.colors.text.primary1, + unfocusedTextColor = TangemTheme.colors.text.secondary, + focusedPlaceholderColor = TangemTheme.colors.text.disabled, + unfocusedPlaceholderColor = TangemTheme.colors.text.disabled, + focusedIndicatorColor = TangemTheme.colors.background.secondary, + unfocusedIndicatorColor = TangemTheme.colors.background.secondary, + cursorColor = TangemTheme.colors.icon.primary1, + ) + +private val RadioButtonColors: RadioButtonColors + @Composable + get() = RadioButtonDefaults.colors( + selectedColor = TangemTheme.colors.icon.accent, + unselectedColor = TangemTheme.colors.icon.secondary, + ) + +// region Preview +@Preview(showBackground = true, widthDp = 360, heightDp = 720) +@Composable +private fun AppCurrencySelectorScreenPreview_Light( + @PreviewParameter(AppCurrencySelectorStateProvider::class) param: AppCurrencySelectorState, +) { + TangemTheme(isDark = false) { + AppCurrencySelectorScreen(param) + } +} + +@Preview(showBackground = true, widthDp = 360, heightDp = 720) +@Composable +private fun AppCurrencySelectorScreenPreview_Dark( + @PreviewParameter(AppCurrencySelectorStateProvider::class) param: AppCurrencySelectorState, +) { + TangemTheme(isDark = true) { + AppCurrencySelectorScreen(param) + } +} + +private class AppCurrencySelectorStateProvider : CollectionPreviewParameterProvider( + collection = buildList { + val items = listOf( + "US Dollar (USD) – $", + "Inited Arab Emirates Dirham (AED) – DH", + "Argentine Peso (ARS) – $", + "Australian Dollar (AUD) – A$", + "Bangladeshi Taka (BDT) – ৳", + "Bahraini Dinar (BHD) – BD", + "Bermudian Dollar (BMD) – $", + "Brazil Real (BRL) – R$", + "Canadian Dollar (CAD) – CA$", + "Swiss Franc (CHF) – Fr", + "Chilean Peso (CLP) – CLP$", + "Chinese Yan (CNY)", + ) + .mapIndexed { index, s -> Currency(index.toString(), s) } + .toPersistentList() + + AppCurrencySelectorState.Loading(onBackClick = {}).let(::add) + AppCurrencySelectorState.Default( + selectedId = "0", + items = items, + scrollToSelected = consumedEvent(), + onCurrencyClick = {}, + onBackClick = {}, + onTopBarActionClick = {}, + ).let(::add) + AppCurrencySelectorState.Search( + selectedId = "0", + items = items, + scrollToSelected = consumedEvent(), + onCurrencyClick = {}, + onBackClick = {}, + onSearchInputChange = {}, + onTopBarActionClick = {}, + ).let(::add) + }, +) +// endregion Preview \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/appcurrency/AppCurrencySelectorState.kt b/app/src/main/java/com/tangem/tap/features/details/ui/appcurrency/AppCurrencySelectorState.kt new file mode 100644 index 0000000000..f50d52cc3e --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/details/ui/appcurrency/AppCurrencySelectorState.kt @@ -0,0 +1,48 @@ +package com.tangem.tap.features.details.ui.appcurrency + +import androidx.compose.runtime.Immutable +import com.tangem.core.ui.event.StateEvent +import kotlinx.collections.immutable.PersistentList + +@Immutable +internal sealed class AppCurrencySelectorState { + + abstract val onBackClick: () -> Unit + + data class Loading( + override val onBackClick: () -> Unit, + ) : AppCurrencySelectorState() + + @Immutable + sealed class Content : AppCurrencySelectorState() { + abstract val selectedId: String + abstract val scrollToSelected: StateEvent + abstract val items: PersistentList + abstract val onCurrencyClick: (Currency) -> Unit + abstract val onTopBarActionClick: () -> Unit + } + + data class Default( + override val selectedId: String, + override val items: PersistentList, + override val onCurrencyClick: (Currency) -> Unit, + override val onBackClick: () -> Unit, + override val onTopBarActionClick: () -> Unit, + override val scrollToSelected: StateEvent, + ) : Content() + + data class Search( + override val selectedId: String, + override val items: PersistentList, + override val scrollToSelected: StateEvent, + override val onCurrencyClick: (Currency) -> Unit, + override val onBackClick: () -> Unit, + override val onTopBarActionClick: () -> Unit, + val onSearchInputChange: (String) -> Unit, + ) : Content() + + data class Currency( + val id: String, + val name: String, + ) +} \ No newline at end of file From 2c259cb036fa17c27a01f0eec069fbc1b850876c Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 11 Sep 2023 16:33:28 +0300 Subject: [PATCH 031/242] Updated on 2026-08-14 --- .../com/tangem/datasource/di/NetworkModule.kt | 5 +- .../src/main/res/drawable/ic_currency_24.xml | 2 +- .../drawable/ic_exchange_horizontal_24.xml | 9 +++ .../presentation/common/WalletPreviewData.kt | 2 +- .../wallet/state/TokenActionButtonConfig.kt | 3 +- .../state/factory/TokenActionsProvider.kt | 76 ++++++++++++------- .../state/factory/WalletStateFactory.kt | 5 +- .../ui/components/TokenActionsBottomSheet.kt | 3 +- .../wallet/viewmodels/WalletViewModel.kt | 11 ++- 9 files changed, 79 insertions(+), 37 deletions(-) create mode 100644 core/ui/src/main/res/drawable/ic_exchange_horizontal_24.xml diff --git a/core/datasource/src/main/java/com/tangem/datasource/di/NetworkModule.kt b/core/datasource/src/main/java/com/tangem/datasource/di/NetworkModule.kt index 10abba599f..e68a69e06e 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/di/NetworkModule.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/di/NetworkModule.kt @@ -8,7 +8,6 @@ import com.tangem.datasource.utils.RequestHeader.* import com.tangem.datasource.utils.addHeaders import com.tangem.datasource.utils.allowLogging import com.tangem.lib.auth.AuthProvider -import com.tangem.lib.auth.BuildConfig import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -28,7 +27,7 @@ class NetworkModule { fun provideTangemTechApi(@NetworkMoshi moshi: Moshi): TangemTechApi { return Retrofit.Builder() .addConverterFactory(MoshiConverterFactory.create(moshi)) - .baseUrl(if (BuildConfig.DEBUG) DEV_TANGEM_TECH_BASE_URL else PROD_TANGEM_TECH_BASE_URL) + .baseUrl(PROD_TANGEM_TECH_BASE_URL) .client( OkHttpClient.Builder() .addHeaders( @@ -76,7 +75,7 @@ class NetworkModule { private fun createBasePromotionRetrofit(okHttpClient: OkHttpClient, moshi: Moshi): PromotionApi { return Retrofit.Builder() .addConverterFactory(MoshiConverterFactory.create(moshi)) - .baseUrl(if (BuildConfig.DEBUG) DEV_TANGEM_TECH_BASE_URL else PROD_TANGEM_TECH_BASE_URL) + .baseUrl(PROD_TANGEM_TECH_BASE_URL) .client(okHttpClient) .build() .create(PromotionApi::class.java) diff --git a/core/ui/src/main/res/drawable/ic_currency_24.xml b/core/ui/src/main/res/drawable/ic_currency_24.xml index bee87c2b68..1b8eb1b684 100644 --- a/core/ui/src/main/res/drawable/ic_currency_24.xml +++ b/core/ui/src/main/res/drawable/ic_currency_24.xml @@ -5,5 +5,5 @@ android:viewportHeight="24"> + android:fillColor="#000000" /> diff --git a/core/ui/src/main/res/drawable/ic_exchange_horizontal_24.xml b/core/ui/src/main/res/drawable/ic_exchange_horizontal_24.xml new file mode 100644 index 0000000000..ab4c32c060 --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_exchange_horizontal_24.xml @@ -0,0 +1,9 @@ + + + diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/WalletPreviewData.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/WalletPreviewData.kt index 8fd27445cb..307db0c04c 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/WalletPreviewData.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/WalletPreviewData.kt @@ -304,7 +304,7 @@ internal object WalletPreviewData { onDismissRequest = {}, actions = listOf( TokenActionButtonConfig( - text = "Send", + text = TextReference.Str("Send"), iconResId = R.drawable.ic_share_24, onClick = {}, ), diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/TokenActionButtonConfig.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/TokenActionButtonConfig.kt index 934bfd6230..2c95c35f55 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/TokenActionButtonConfig.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/TokenActionButtonConfig.kt @@ -1,6 +1,7 @@ package com.tangem.feature.wallet.presentation.wallet.state import androidx.annotation.DrawableRes +import com.tangem.core.ui.extensions.TextReference /** * Action button config @@ -11,7 +12,7 @@ import androidx.annotation.DrawableRes * @property enabled enabled */ data class TokenActionButtonConfig( - val text: String, + val text: TextReference, @DrawableRes val iconResId: Int, val onClick: () -> Unit, val enabled: Boolean = true, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/TokenActionsProvider.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/TokenActionsProvider.kt index 37c3309f8b..8b8c5f5086 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/TokenActionsProvider.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/TokenActionsProvider.kt @@ -1,6 +1,8 @@ package com.tangem.feature.wallet.presentation.wallet.state.factory -import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.domain.tokens.model.TokenActionsState import com.tangem.feature.wallet.impl.R import com.tangem.feature.wallet.presentation.common.state.TokenItemState import com.tangem.feature.wallet.presentation.wallet.state.TokenActionButtonConfig @@ -14,35 +16,57 @@ import kotlinx.collections.immutable.toImmutableList * @property clickIntents screen click intents * */ +@Suppress("UnusedPrivateMember") // will be used in next PRs internal class TokenActionsProvider(private val clickIntents: WalletClickIntents) { - fun provideActions(cryptoCurrencyStatus: CryptoCurrencyStatus): ImmutableList { - // TODO: [REDACTED_JIRA] - return mockTokenActionButtonConfig(cryptoCurrencyStatus).toImmutableList() + fun provideActions(tokenActions: TokenActionsState): ImmutableList { + return convertTokenActionsState(tokenActions) } - private fun mockTokenActionButtonConfig(cryptoCurrencyStatus: CryptoCurrencyStatus): List { - return listOf( - TokenActionButtonConfig( - text = "Send", - iconResId = R.drawable.ic_plus_24, - onClick = { clickIntents.onMultiCurrencySendClick(cryptoCurrencyStatus) }, - ), - TokenActionButtonConfig( - text = "Buy", - iconResId = R.drawable.ic_plus_24, - onClick = {}, - ), - TokenActionButtonConfig( - text = "Sell", - iconResId = R.drawable.ic_plus_24, - onClick = {}, - ), - TokenActionButtonConfig( - text = "Swap", - iconResId = R.drawable.ic_plus_24, - onClick = {}, - ), + private fun convertTokenActionsState( + tokenActionsState: TokenActionsState, + ): ImmutableList { + return tokenActionsState.states + .map(::tokenActionStateMapper) + .toImmutableList() + } + + private fun tokenActionStateMapper(actionsState: TokenActionsState.ActionState): TokenActionButtonConfig { + val title: TextReference + val icon: Int + val action: () -> Unit + when (actionsState) { + is TokenActionsState.ActionState.Buy -> { + title = resourceReference(R.string.common_buy) + icon = R.drawable.ic_plus_24 + action = { } + } + is TokenActionsState.ActionState.Receive -> { + title = resourceReference(R.string.common_receive) + icon = R.drawable.ic_arrow_down_24 + action = { } + } + is TokenActionsState.ActionState.Sell -> { + title = resourceReference(R.string.common_sell) + icon = R.drawable.ic_currency_24 + action = { } + } + is TokenActionsState.ActionState.Send -> { + title = resourceReference(R.string.common_send) + icon = R.drawable.ic_arrow_up_24 + action = { } + } + is TokenActionsState.ActionState.Swap -> { + title = resourceReference(R.string.common_swap) + icon = R.drawable.ic_exchange_horizontal_24 + action = { } + } + } + return TokenActionButtonConfig( + text = title, + iconResId = icon, + onClick = action, + enabled = actionsState.enabled, ) } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletStateFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletStateFactory.kt index 4a282a682c..00e8cbc09a 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletStateFactory.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletStateFactory.kt @@ -199,16 +199,15 @@ internal class WalletStateFactory( bottomSheetConfig = state.bottomSheetConfig?.copy(isShow = false), ) is WalletSingleCurrencyState.Locked -> state.copy(isBottomSheetShow = false) - else -> state } } - fun getStateWithTokenActionBottomSheet(currencyStatus: CryptoCurrencyStatus): WalletState { + fun getStateWithTokenActionBottomSheet(tokenActions: TokenActionsState): WalletState { return when (val state = currentStateProvider() as WalletState.ContentState) { is WalletMultiCurrencyState.Content -> state.copy( tokenActionsBottomSheet = ActionsBottomSheetConfig( isShow = true, - actions = tokenActionsProvider.provideActions(currencyStatus), + actions = tokenActionsProvider.provideActions(tokenActions), onDismissRequest = clickIntents::onDismissActionsBottomSheet, ), ) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/TokenActionsBottomSheet.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/TokenActionsBottomSheet.kt index 31ea416730..7037bcdd39 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/TokenActionsBottomSheet.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/TokenActionsBottomSheet.kt @@ -12,6 +12,7 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider import com.tangem.core.ui.components.SimpleSettingsRow +import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.res.TangemTheme import com.tangem.feature.wallet.presentation.common.WalletPreviewData import com.tangem.feature.wallet.presentation.wallet.state.ActionsBottomSheetConfig @@ -38,7 +39,7 @@ private fun ActionsBottomSheetContent(actions: ImmutableList SimpleSettingsRow( - title = action.text, + title = action.text.resolveReference(), icon = action.iconResId, enabled = action.enabled, onItemsClick = action.onClick, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt index 537366f56b..6efc2b0461 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt @@ -496,7 +496,16 @@ internal class WalletViewModel @Inject constructor( } override fun onTokenItemLongClick(cryptoCurrencyStatus: CryptoCurrencyStatus) { - uiState = stateFactory.getStateWithTokenActionBottomSheet(cryptoCurrencyStatus) + val state = uiState as? WalletState.ContentState ?: return + val userWallet = getWallet(state.walletsListConfig.selectedWalletIndex) + viewModelScope.launch(dispatchers.io) { + getCryptoCurrencyActionsUseCase + .invoke(userWallet.walletId, cryptoCurrencyStatus.currency) + .take(count = 1) + .collectLatest { + uiState = stateFactory.getStateWithTokenActionBottomSheet(it) + } + } } override fun onRenameClick(userWalletId: UserWalletId, name: String) { From 4b5adcd08ae4537a7c826ceaf62be66d72d52d27 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 13 Sep 2023 13:26:01 +0300 Subject: [PATCH 032/242] Updated on 2026-08-14 --- .../features/details/redux/DetailsAction.kt | 9 + .../details/redux/DetailsMiddleware.kt | 131 ++++++-- .../features/details/redux/DetailsReducer.kt | 14 + .../features/details/redux/DetailsState.kt | 3 + .../ui/common/DetailsComposeElements.kt | 47 +-- .../details/ui/details/DetailsScreen.kt | 285 +++++++++++------- .../details/ui/details/DetailsScreenState.kt | 123 ++++++-- .../details/ui/details/DetailsViewModel.kt | 185 +++++++----- 8 files changed, 539 insertions(+), 258 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsAction.kt b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsAction.kt index 1e719df7c7..0b4facea41 100644 --- a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsAction.kt +++ b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsAction.kt @@ -1,5 +1,6 @@ package com.tangem.tap.features.details.redux +import com.tangem.core.ui.extensions.TextReference import com.tangem.domain.apptheme.model.AppThemeMode import com.tangem.domain.common.CardTypesResolver import com.tangem.domain.models.scan.CardDTO @@ -27,6 +28,14 @@ sealed class DetailsAction : Action { data class PrepareCardSettingsData(val card: CardDTO, val cardTypesResolver: CardTypesResolver) : DetailsAction() object ResetCardSettingsData : DetailsAction() + object ScanAndSaveUserWallet : DetailsAction() { + + object Success : DetailsAction() + + data class Error(val error: TextReference?) : DetailsAction() + } + + object DismissError : DetailsAction() sealed class AccessCodeRecovery : DetailsAction() { object Open : AccessCodeRecovery() diff --git a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsMiddleware.kt b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsMiddleware.kt index 6a72a5811a..aa3264275d 100644 --- a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsMiddleware.kt @@ -1,6 +1,7 @@ package com.tangem.tap.features.details.redux import com.tangem.common.CompletionResult +import com.tangem.common.core.TangemError import com.tangem.common.core.TangemSdkError import com.tangem.common.doOnFailure import com.tangem.common.doOnSuccess @@ -9,6 +10,9 @@ import com.tangem.common.flatMap import com.tangem.core.analytics.Analytics import com.tangem.core.navigation.AppScreen import com.tangem.core.navigation.NavigationAction +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.stringReference import com.tangem.domain.apptheme.model.AppThemeMode import com.tangem.domain.common.TapWorkarounds.isTangemTwins import com.tangem.domain.common.util.cardTypesResolver @@ -19,6 +23,7 @@ import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.wallets.legacy.isLockedSync import com.tangem.tap.* import com.tangem.tap.common.analytics.events.AnalyticsParam +import com.tangem.tap.common.analytics.events.Basic import com.tangem.tap.common.analytics.events.Settings import com.tangem.tap.common.extensions.dispatchDialogShow import com.tangem.tap.common.extensions.dispatchOnMain @@ -73,37 +78,8 @@ class DetailsMiddleware { store.dispatch(NavigationAction.NavigateTo(AppScreen.OnboardingTwins)) } is DetailsAction.AccessCodeRecovery -> accessCodeRecoveryMiddleware.handle(state, action) - DetailsAction.ScanCard -> { - scope.launch { - store.state.daggerGraphState.get(DaggerGraphState::scanCardProcessor) - .scan(allowsRequestAccessCodeFromRepository = true) - .doOnSuccess { scanResponse -> - // if we use biometric, scanResponse in GlobalState is null, and crashes NPE on twin cards - store.dispatch(GlobalAction.SaveScanResponse(scanResponse)) - val currentUserWalletId = state.scanResponse - ?.let { UserWalletIdBuilder.scanResponse(it).build() } - val scannedUserWalletId = UserWalletIdBuilder.scanResponse(scanResponse) - .build() - val isSameWallet = currentUserWalletId == scannedUserWalletId - - if (isSameWallet) { - store.dispatchOnMain( - DetailsAction.PrepareCardSettingsData( - scanResponse.card, - scanResponse.cardTypesResolver, - ), - ) - } else { - store.dispatchDialogShow( - AppDialog.SimpleOkDialogRes( - headerId = R.string.common_warning, - messageId = R.string.error_wrong_wallet_tapped, - ), - ) - } - } - } - } + is DetailsAction.ScanCard -> scanCard(state) + is DetailsAction.ScanAndSaveUserWallet -> scanAndSaveUserWallet() } } @@ -460,4 +436,97 @@ class DetailsMiddleware { } } } + + private fun scanCard(state: DetailsState) = scope.launch { + store.state.daggerGraphState.get(DaggerGraphState::scanCardProcessor) + .scan(allowsRequestAccessCodeFromRepository = true) + .doOnSuccess { scanResponse -> + // if we use biometric, scanResponse in GlobalState is null, and crashes NPE on twin cards + store.dispatch(GlobalAction.SaveScanResponse(scanResponse)) + val currentUserWalletId = state.scanResponse + ?.let { UserWalletIdBuilder.scanResponse(it).build() } + val scannedUserWalletId = UserWalletIdBuilder.scanResponse(scanResponse) + .build() + val isSameWallet = currentUserWalletId == scannedUserWalletId + + if (isSameWallet) { + store.dispatchOnMain( + DetailsAction.PrepareCardSettingsData( + scanResponse.card, + scanResponse.cardTypesResolver, + ), + ) + } else { + store.dispatchDialogShow( + AppDialog.SimpleOkDialogRes( + headerId = R.string.common_warning, + messageId = R.string.error_wrong_wallet_tapped, + ), + ) + } + } + } + + private fun scanAndSaveUserWallet() = scope.launch(Dispatchers.IO) { + val cardSdkConfigRepository = store.state.daggerGraphState.get(DaggerGraphState::cardSdkConfigRepository) + + val prevUseBiometricsForAccessCode = cardSdkConfigRepository.isBiometricsRequestPolicy() + + // Update access code policy for access code saving when a card was scanned + cardSdkConfigRepository.setAccessCodeRequestPolicy( + isBiometricsRequestPolicy = preferencesStorage.shouldSaveAccessCodes, + ) + + store.state.daggerGraphState.get(DaggerGraphState::scanCardProcessor).scan( + analyticsEvent = Basic.CardWasScanned(AnalyticsParam.ScannedFrom.MyWallets), + onWalletNotCreated = { + // No need to rollback policy, continue with the policy set before the card scan + store.dispatchWithMain(DetailsAction.ScanAndSaveUserWallet.Success) + store.dispatchWithMain(NavigationAction.PopBackTo(AppScreen.Wallet)) + }, + disclaimerWillShow = { + store.dispatchOnMain(NavigationAction.PopBackTo()) + }, + onSuccess = { scanResponse -> + saveUserWalletAndPopBackToWalletScreen(scanResponse) + .doOnFailure { error -> + // Rollback policy if card saving was failed + cardSdkConfigRepository.setAccessCodeRequestPolicy(prevUseBiometricsForAccessCode) + Timber.e(error, "Unable to save user wallet") + + store.dispatchWithMain(DetailsAction.ScanAndSaveUserWallet.Error(error.toTextReference())) + } + }, + onFailure = { error -> + // Rollback policy if card scanning was failed + cardSdkConfigRepository.setAccessCodeRequestPolicy(prevUseBiometricsForAccessCode) + Timber.e(error, "Unable to scan card") + store.dispatchWithMain(DetailsAction.ScanAndSaveUserWallet.Error(error.toTextReference())) + }, + ) + } + + private suspend fun saveUserWalletAndPopBackToWalletScreen(scanResponse: ScanResponse): CompletionResult { + val userWallet = UserWalletBuilder(scanResponse).build() + ?: return CompletionResult.Failure(TangemSdkError.WalletIsNotCreated()) + + return userWalletsListManager.save(userWallet) + .doOnSuccess { + store.dispatchWithMain(DetailsAction.ScanAndSaveUserWallet.Success) + store.dispatchWithMain(NavigationAction.PopBackTo(AppScreen.Wallet)) + + val walletFeatureToggles = store.state.daggerGraphState + .get(DaggerGraphState::walletFeatureToggles) + + if (!walletFeatureToggles.isRedesignedScreenEnabled) { + store.onUserWalletSelected(userWallet) + } + } + } + + private fun TangemError.toTextReference(): TextReference? { + if (silent) return null + + return messageResId?.let(::resourceReference) ?: stringReference(customMessage) + } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsReducer.kt b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsReducer.kt index 1b70773cc1..b2754c775e 100644 --- a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsReducer.kt +++ b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsReducer.kt @@ -15,6 +15,7 @@ object DetailsReducer { fun reduce(action: Action, state: AppState): DetailsState = internalReduce(action, state) } +@Suppress("CyclomaticComplexMethod") private fun internalReduce(action: Action, state: AppState): DetailsState { if (action !is DetailsAction) return state.detailsState val detailsState = state.detailsState @@ -45,6 +46,19 @@ private fun internalReduce(action: Action, state: AppState): DetailsState { ), ) is DetailsAction.AccessCodeRecovery -> handleAccessCodeRecoveryAction(action, detailsState) + is DetailsAction.ScanAndSaveUserWallet -> detailsState.copy( + isScanningInProgress = true, + ) + is DetailsAction.ScanAndSaveUserWallet.Error -> detailsState.copy( + isScanningInProgress = false, + error = action.error, + ) + is DetailsAction.ScanAndSaveUserWallet.Success -> detailsState.copy( + isScanningInProgress = false, + ) + is DetailsAction.DismissError -> detailsState.copy( + error = null, + ) else -> detailsState } } diff --git a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsState.kt b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsState.kt index 14e533ed54..f37f6fed94 100644 --- a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsState.kt +++ b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsState.kt @@ -1,5 +1,6 @@ package com.tangem.tap.features.details.redux +import com.tangem.core.ui.extensions.TextReference import com.tangem.domain.apptheme.model.AppThemeMode import com.tangem.domain.models.scan.CardDTO import com.tangem.domain.models.scan.ScanResponse @@ -13,6 +14,8 @@ data class DetailsState( val cardSettingsState: CardSettingsState? = null, val privacyPolicyUrl: String? = null, val createBackupAllowed: Boolean = false, + val isScanningInProgress: Boolean = false, + val error: TextReference? = null, val appSettingsState: AppSettingsState = AppSettingsState(), ) : StateType diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/common/DetailsComposeElements.kt b/app/src/main/java/com/tangem/tap/features/details/ui/common/DetailsComposeElements.kt index 653c54b272..bdba522a2b 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/common/DetailsComposeElements.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/common/DetailsComposeElements.kt @@ -6,6 +6,7 @@ import androidx.compose.foundation.layout.* import androidx.compose.foundation.selection.selectable import androidx.compose.material.* import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.colorResource @@ -22,9 +23,11 @@ internal fun SettingsScreensScaffold( onBackClick: () -> Unit, content: @Composable () -> Unit, modifier: Modifier = Modifier, - fab: @Composable (() -> Unit)? = null, @StringRes titleRes: Int? = null, + snackbarHostState: SnackbarHostState = remember { SnackbarHostState() }, + fab: @Composable () -> Unit = {}, ) { + val state = rememberScaffoldState(snackbarHostState = snackbarHostState) val backgroundColor = TangemTheme.colors.background.secondary BackHandler(onBack = onBackClick) @@ -33,6 +36,7 @@ internal fun SettingsScreensScaffold( } Scaffold( + scaffoldState = state, topBar = { EmptyTopBarWithNavigation( onBackClick = onBackClick, @@ -41,27 +45,28 @@ internal fun SettingsScreensScaffold( }, modifier = modifier.systemBarsPadding(), backgroundColor = backgroundColor, - floatingActionButton = { fab?.invoke() }, - ) { paddings -> - Column( - modifier = Modifier - .padding(paddings) - .fillMaxSize(), - ) { - if (titleRes != null) { - Text( - text = stringResource(id = titleRes), - modifier = Modifier - .padding(horizontal = TangemTheme.dimens.spacing20) - .padding(bottom = TangemTheme.dimens.spacing36), - style = TangemTheme.typography.h1, - color = TangemTheme.colors.text.primary1, - ) - } + floatingActionButton = fab, + content = { paddings -> + Column( + modifier = Modifier + .padding(paddings) + .fillMaxSize(), + ) { + if (titleRes != null) { + Text( + text = stringResource(id = titleRes), + modifier = Modifier + .padding(horizontal = TangemTheme.dimens.spacing20) + .padding(bottom = TangemTheme.dimens.spacing36), + style = TangemTheme.typography.h1, + color = TangemTheme.colors.text.primary1, + ) + } - content() - } - } + content() + } + }, + ) } @Composable diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsScreen.kt b/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsScreen.kt index 2326b3db65..1d074d274a 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsScreen.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsScreen.kt @@ -6,36 +6,44 @@ import androidx.compose.foundation.lazy.LazyRow import androidx.compose.foundation.lazy.items import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll -import androidx.compose.material.Icon -import androidx.compose.material.SnackbarHost -import androidx.compose.material.SnackbarHostState -import androidx.compose.material.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.SideEffect -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.material.* +import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider import com.tangem.core.ui.components.SpacerH import com.tangem.core.ui.components.SpacerHMax +import com.tangem.core.ui.event.EventEffect +import com.tangem.core.ui.event.StateEvent +import com.tangem.core.ui.event.consumedEvent +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.res.TangemColorPalette import com.tangem.core.ui.res.TangemTheme import com.tangem.tap.features.details.ui.common.ScreenTitle import com.tangem.tap.features.details.ui.common.SettingsScreensScaffold import com.tangem.wallet.R -import kotlinx.coroutines.launch +import kotlinx.collections.immutable.toImmutableList @Composable internal fun DetailsScreen(state: DetailsScreenState, onBackClick: () -> Unit, modifier: Modifier = Modifier) { + val snackbarHostState = remember { SnackbarHostState() } + SettingsScreensScaffold( modifier = modifier, + snackbarHostState = snackbarHostState, content = { Content(state = state) }, onBackClick = onBackClick, ) + + ShowSnackbarIfNeeded( + snackbarHostState = snackbarHostState, + messageEvent = state.showSnackbar, + ) } @Composable @@ -50,102 +58,124 @@ private fun Content(state: DetailsScreenState, modifier: Modifier = Modifier) { SpacerH(height = TangemTheme.dimens.spacing36) SettingsItems( items = state.elements, - onItemsClick = state.onItemsClick, ) SpacerHMax() TangemSocialAccounts( links = state.tangemLinks, onSocialNetworkClick = state.onSocialNetworkClick, ) - SpacerH(height = TangemTheme.dimens.spacing16) + SpacerH(height = TangemTheme.dimens.spacing12) TangemAppVersion( appNameRes = state.appNameRes, version = state.tangemVersion, ) - SpacerH(height = TangemTheme.dimens.spacing24) + SpacerH(height = TangemTheme.dimens.spacing16) } - ShowSnackbarIfNeeded(state.showErrorSnackbar.value) } } @Composable -private fun SettingsItems(items: List, onItemsClick: (SettingsElement) -> Unit) { +private fun SettingsItems(items: List) { items.forEach { item -> - val onItemClick = remember(item) { - { onItemsClick(item) } - } - - if (item == SettingsElement.WalletConnect) { - WalletConnectDetailsItem(onItemClick) + if (item.isLarge) { + LargeDetailsItem(item) } else { - DetailsItem( - item = item, - onItemClick = onItemClick, - ) + DetailsItem(item) } } } @Composable -private fun WalletConnectDetailsItem(onItemClick: () -> Unit) { +private fun LargeDetailsItem(item: SettingsItem) { Row( modifier = Modifier - .defaultMinSize(minHeight = 84.dp) - .fillMaxWidth() - .clickable(onClick = onItemClick), - horizontalArrangement = Arrangement.Start, + .clickable(onClick = item.onClick) + .padding(horizontal = TangemTheme.dimens.spacing20) + .heightIn(min = TangemTheme.dimens.size84) + .fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing20), verticalAlignment = Alignment.CenterVertically, ) { - Icon( - painter = painterResource(id = R.drawable.ic_walletconnect), - contentDescription = stringResource(id = R.string.wallet_connect_title), - modifier = Modifier.padding(start = 20.dp, end = 20.dp), - tint = TangemColorPalette.Azure, - ) + if (item.showProgress) { + CircularProgressIndicator( + modifier = Modifier.size(TangemTheme.dimens.size24), + color = TangemTheme.colors.icon.informative, + ) + } else { + Icon( + modifier = Modifier.size(TangemTheme.dimens.size24), + painter = painterResource(id = item.iconResId), + contentDescription = item.title.resolveReference(), + tint = TangemColorPalette.Azure, + ) + } Column( - modifier = Modifier.defaultMinSize(minHeight = 56.dp), + modifier = Modifier.heightIn(min = TangemTheme.dimens.size56), horizontalAlignment = Alignment.Start, - verticalArrangement = Arrangement.Center, + verticalArrangement = Arrangement.spacedBy( + space = TangemTheme.dimens.spacing4, + alignment = Alignment.CenterVertically, + ), ) { Text( - text = stringResource(id = R.string.wallet_connect_title), - modifier = Modifier.padding(end = 20.dp, bottom = 4.dp), + text = item.title.resolveReference(), style = TangemTheme.typography.h3, color = TangemTheme.colors.text.primary1, ) - Text( - text = stringResource(id = R.string.wallet_connect_subtitle), - modifier = Modifier.padding(end = 20.dp, bottom = 4.dp), - style = TangemTheme.typography.body1, - color = TangemTheme.colors.text.secondary, - ) + + if (item.subtitle != null) { + Text( + text = item.subtitle.resolveReference(), + style = TangemTheme.typography.body1, + color = TangemTheme.colors.text.secondary, + ) + } } } } @Composable -private fun DetailsItem(item: SettingsElement, onItemClick: () -> Unit) { +private fun DetailsItem(item: SettingsItem) { Row( modifier = Modifier - .height(56.dp) - .fillMaxWidth() - .clickable(onClick = onItemClick), - horizontalArrangement = Arrangement.Start, + .clickable(enabled = !item.showProgress, onClick = item.onClick) + .padding(horizontal = TangemTheme.dimens.spacing20) + .heightIn(min = TangemTheme.dimens.size56) + .fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing20), verticalAlignment = Alignment.CenterVertically, ) { - Icon( - painter = painterResource(id = item.iconRes), - contentDescription = stringResource(id = item.titleRes), - modifier = Modifier.padding(start = 20.dp, end = 20.dp), - tint = TangemTheme.colors.icon.secondary, - ) - Column(modifier = Modifier.padding(end = 20.dp)) { + if (item.showProgress) { + CircularProgressIndicator( + modifier = Modifier.size(TangemTheme.dimens.size24), + color = TangemTheme.colors.icon.informative, + ) + } else { + Icon( + modifier = Modifier.size(TangemTheme.dimens.size24), + painter = painterResource(id = item.iconResId), + contentDescription = item.title.resolveReference(), + tint = TangemTheme.colors.icon.secondary, + ) + } + + Column( + horizontalAlignment = Alignment.Start, + verticalArrangement = Arrangement.SpaceAround, + ) { Text( - text = stringResource(id = item.titleRes), - modifier = Modifier, + text = item.title.resolveReference(), style = TangemTheme.typography.subtitle1, color = TangemTheme.colors.text.primary1, ) + + if (item.subtitle != null) { + Text( + text = item.subtitle.resolveReference(), + style = TangemTheme.typography.body2, + color = TangemTheme.colors.text.secondary, + ) + } } } } @@ -153,50 +183,45 @@ private fun DetailsItem(item: SettingsElement, onItemClick: () -> Unit) { @Composable private fun TangemSocialAccounts(links: List, onSocialNetworkClick: (SocialNetworkLink) -> Unit) { LazyRow( - modifier = Modifier.padding(start = 8.dp, end = 8.dp), verticalAlignment = Alignment.CenterVertically, + contentPadding = PaddingValues(horizontal = TangemTheme.dimens.spacing8), ) { items(links) { - Icon( - painter = painterResource(id = it.network.iconRes), - contentDescription = "", + val onClick = remember(it) { + { onSocialNetworkClick(it) } + } + + IconButton( modifier = Modifier - .padding(8.dp) - .clickable { onSocialNetworkClick(it) }, - tint = TangemTheme.colors.icon.informative, - ) + .padding(horizontal = TangemTheme.dimens.spacing4) + .size(TangemTheme.dimens.size32), + onClick = onClick, + ) { + Icon( + modifier = Modifier.size(TangemTheme.dimens.size24), + painter = painterResource(id = it.network.iconRes), + tint = TangemTheme.colors.icon.informative, + contentDescription = null, + ) + } } } } @Composable -private fun BoxScope.ShowSnackbarIfNeeded(snackbarErrorState: EventError) { - val snackbarHostState = remember { SnackbarHostState() } - val coroutineScope = rememberCoroutineScope() - SnackbarHost( - modifier = Modifier - .align(Alignment.BottomCenter) - .padding(vertical = TangemTheme.dimens.spacing16) - .fillMaxWidth(), - hostState = snackbarHostState, - ) - val errorTitle = when (snackbarErrorState) { - is EventError.DemoReferralNotAvailable -> stringResource(id = R.string.alert_demo_feature_disabled) - EventError.Empty -> "" - } - if (snackbarErrorState != EventError.Empty) { - SideEffect { - coroutineScope.launch { - snackbarHostState.showSnackbar(errorTitle) - } - when (snackbarErrorState) { - is EventError.DemoReferralNotAvailable -> snackbarErrorState.onErrorShow.invoke() - else -> { - /*no-op*/ - } - } +private fun ShowSnackbarIfNeeded(snackbarHostState: SnackbarHostState, messageEvent: StateEvent) { + var message: TextReference? by remember { mutableStateOf(value = null) } + val resolvedMessage by rememberUpdatedState(newValue = message?.resolveReference()) + + LaunchedEffect(resolvedMessage) { + resolvedMessage?.let { + snackbarHostState.showSnackbar(it) } } + + EventEffect(messageEvent) { + message = it + } } @Composable @@ -210,33 +235,59 @@ private fun TangemAppVersion(appNameRes: Int, version: String, modifier: Modifie } // region Preview +@Preview(showBackground = true, widthDp = 360, heightDp = 900) @Composable -private fun DetailsScreenContentSample() { - DetailsScreen( - state = DetailsScreenState( - elements = SettingsElement.values().toList(), +private fun DetailsScreenPreview_Light( + @PreviewParameter(DetailsScreenStateProvider::class) param: DetailsScreenState, +) { + TangemTheme(isDark = false) { + DetailsScreen(param, onBackClick = {}) + } +} + +@Preview(showBackground = true, widthDp = 360, heightDp = 900) +@Composable +private fun DetailsScreenPreview_Dark(@PreviewParameter(DetailsScreenStateProvider::class) param: DetailsScreenState) { + TangemTheme(isDark = true) { + DetailsScreen(param, onBackClick = {}) + } +} + +private class DetailsScreenStateProvider : CollectionPreviewParameterProvider( + collection = buildList { + DetailsScreenState( + elements = buildList { + SettingsItem.WalletConnect({}).let(::add) + SettingsItem.AddWallet(showProgress = false, {}).let(::add) + SettingsItem.LinkMoreCards({}).let(::add) + SettingsItem.CardSettings({}).let(::add) + SettingsItem.AppSettings({}).let(::add) + SettingsItem.Chat({}).let(::add) + SettingsItem.SendFeedback({}).let(::add) + SettingsItem.ReferralProgram({}).let(::add) + SettingsItem.TermsOfService({}).let(::add) + }.toImmutableList(), tangemLinks = TangemSocialAccounts.accountsEn, tangemVersion = "Tangem 2.14.12 (343)", - onItemsClick = {}, + showSnackbar = consumedEvent(), onSocialNetworkClick = {}, - ), - onBackClick = {}, - ) -} + ).let(::add) -@Preview(showBackground = true, widthDp = 360) -@Composable -private fun DetailsScreenContentPreview_Light() { - TangemTheme(isDark = false) { - DetailsScreenContentSample() - } -} - -@Preview(showBackground = true, widthDp = 360) -@Composable -private fun DetailsScreenContentPreview_Dark() { - TangemTheme(isDark = true) { - DetailsScreenContentSample() - } -} + DetailsScreenState( + elements = buildList { + SettingsItem.WalletConnect({}).let(::add) + SettingsItem.AddWallet(showProgress = true, {}).let(::add) + SettingsItem.CardSettings({}).let(::add) + SettingsItem.AppSettings({}).let(::add) + SettingsItem.Chat({}).let(::add) + SettingsItem.SendFeedback({}).let(::add) + SettingsItem.TermsOfService({}).let(::add) + }.toImmutableList(), + tangemLinks = TangemSocialAccounts.accountsRu, + tangemVersion = "Tangem 2.14.12 (343)", + showSnackbar = consumedEvent(), + onSocialNetworkClick = {}, + ).let(::add) + }, +) // endregion Preview \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsScreenState.kt b/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsScreenState.kt index f6ea3f1f19..a9d511210e 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsScreenState.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsScreenState.kt @@ -1,37 +1,118 @@ package com.tangem.tap.features.details.ui.details +import androidx.annotation.DrawableRes import androidx.compose.runtime.Immutable -import androidx.compose.runtime.MutableState -import androidx.compose.runtime.mutableStateOf +import com.tangem.core.ui.event.StateEvent +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.stringReference import com.tangem.wallet.R +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf @Immutable internal data class DetailsScreenState( - val elements: List, - val tangemLinks: List, + val elements: ImmutableList, + val tangemLinks: ImmutableList, val tangemVersion: String, - val onItemsClick: (SettingsElement) -> Unit, + val showSnackbar: StateEvent, val onSocialNetworkClick: (SocialNetworkLink) -> Unit, - val showErrorSnackbar: MutableState = mutableStateOf(EventError.Empty), ) { val appNameRes: Int = R.string.tangem_app_name } @Immutable -internal enum class SettingsElement( - val iconRes: Int, - val titleRes: Int, +internal sealed class SettingsItem( + @DrawableRes val iconResId: Int, + val title: TextReference, + val subtitle: TextReference? = null, + val isLarge: Boolean = false, ) { - WalletConnect(R.drawable.ic_walletconnect, R.string.wallet_connect_title), - LinkMoreCards(R.drawable.ic_more_cards, R.string.details_row_title_create_backup), - ReferralProgram(R.drawable.ic_add_friends, R.string.details_referral_title), - CardSettings(R.drawable.ic_card_settings, R.string.card_settings_title), - AppSettings(R.drawable.ic_settings, R.string.app_settings_title), - Chat(R.drawable.ic_chat, R.string.details_chat), - SendFeedback(R.drawable.ic_comment, R.string.details_row_title_send_feedback), - TermsOfService(R.drawable.ic_text, R.string.disclaimer_title), // General Terms of Service of the App, - PrivacyPolicy(R.drawable.ic_lock_24, R.string.details_row_privacy_policy), - TesterMenu(R.drawable.ic_alert_24, R.string.tester_menu), + + abstract val onClick: () -> Unit + + open val showProgress: Boolean = false + + data class WalletConnect( + override val onClick: () -> Unit, + ) : SettingsItem( + iconResId = R.drawable.ic_walletconnect, + title = resourceReference(R.string.wallet_connect_title), + subtitle = resourceReference(R.string.wallet_connect_subtitle), + isLarge = true, + ) + + data class AddWallet( + override val showProgress: Boolean, + override val onClick: () -> Unit, + ) : SettingsItem( + iconResId = R.drawable.ic_plus_24, + title = stringReference(value = "Add new wallet"), + ) + + data class ScanWallet( + override val showProgress: Boolean, + override val onClick: () -> Unit, + ) : SettingsItem( + iconResId = R.drawable.ic_plus_24, + title = stringReference(value = "Scan new wallet"), + ) + + data class LinkMoreCards( + override val onClick: () -> Unit, + ) : SettingsItem( + iconResId = R.drawable.ic_more_cards, + title = resourceReference(R.string.details_row_title_create_backup), + ) + + data class CardSettings( + override val onClick: () -> Unit, + ) : SettingsItem( + iconResId = R.drawable.ic_card_settings, + title = resourceReference(R.string.card_settings_title), + ) + + data class AppSettings( + override val onClick: () -> Unit, + ) : SettingsItem( + iconResId = R.drawable.ic_settings, + title = resourceReference(R.string.app_settings_title), + ) + + data class Chat( + override val onClick: () -> Unit, + ) : SettingsItem( + iconResId = R.drawable.ic_chat, + title = resourceReference(R.string.details_chat), + ) + + data class SendFeedback( + override val onClick: () -> Unit, + ) : SettingsItem( + iconResId = R.drawable.ic_comment, + title = resourceReference(R.string.details_row_title_send_feedback), + ) + + data class ReferralProgram( + override val onClick: () -> Unit, + ) : SettingsItem( + iconResId = R.drawable.ic_add_friends, + title = resourceReference(R.string.details_referral_title), + ) + + data class TermsOfService( + override val onClick: () -> Unit, + ) : SettingsItem( + iconResId = R.drawable.ic_text, + title = resourceReference(R.string.disclaimer_title), + ) + + data class TesterMenu( + override val onClick: () -> Unit, + ) : SettingsItem( + iconResId = R.drawable.ic_alert_24, + title = resourceReference(R.string.tester_menu), + ) } @Immutable @@ -57,7 +138,7 @@ sealed class SocialNetwork(val id: String, val iconRes: Int) { } internal object TangemSocialAccounts { - val accountsEn: List = listOf( + val accountsEn: ImmutableList = persistentListOf( SocialNetworkLink(SocialNetwork.Telegram, "https://t.me/tangem_chat"), SocialNetworkLink(SocialNetwork.Twitter, "https://twitter.com/tangem"), SocialNetworkLink(SocialNetwork.Facebook, "https://m.facebook.com/TangemCards/"), @@ -67,7 +148,7 @@ internal object TangemSocialAccounts { SocialNetworkLink(SocialNetwork.LinkedIn, "https://www.linkedin.com/company/tangem"), SocialNetworkLink(SocialNetwork.Discord, "https://discord.gg/7AqTVyqdGS"), ) - val accountsRu: List = listOf( + val accountsRu: ImmutableList = persistentListOf( SocialNetworkLink(SocialNetwork.Telegram, "https://t.me/tangem_chat_ru"), SocialNetworkLink(SocialNetwork.Twitter, "https://twitter.com/tangem"), SocialNetworkLink(SocialNetwork.Facebook, "https://m.facebook.com/TangemCards/"), diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsViewModel.kt b/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsViewModel.kt index 7877dff881..29ab390def 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsViewModel.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsViewModel.kt @@ -5,6 +5,10 @@ import androidx.compose.runtime.mutableStateOf import com.tangem.core.analytics.Analytics import com.tangem.core.navigation.AppScreen import com.tangem.core.navigation.NavigationAction +import com.tangem.core.ui.event.StateEvent +import com.tangem.core.ui.event.consumedEvent +import com.tangem.core.ui.event.triggeredEvent +import com.tangem.core.ui.extensions.TextReference import com.tangem.domain.common.util.cardTypesResolver import com.tangem.tap.common.analytics.events.Settings import com.tangem.tap.common.extensions.dispatchWithMain @@ -21,6 +25,9 @@ import com.tangem.tap.features.wallet.redux.WalletAction import com.tangem.tap.scope import com.tangem.tap.userWalletsListManager import com.tangem.wallet.BuildConfig +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.flowOn @@ -42,82 +49,64 @@ internal class DetailsViewModel(private val store: Store) { elements = createSettingsItems(state), tangemLinks = getSocialLinks(), tangemVersion = getTangemAppVersion(), - onItemsClick = { handleClickingSettingsItem(it) }, - onSocialNetworkClick = { handleSocialNetworkClick(it) }, + showSnackbar = triggerErrorSnackbarIfNeeded(state.error), + onSocialNetworkClick = ::handleSocialNetworkClick, ) } - @Suppress("ComplexMethod") - private fun createSettingsItems(state: DetailsState): List { - val scanResponse = state.scanResponse ?: return emptyList() + private fun createSettingsItems(state: DetailsState): ImmutableList { + val scanResponse = state.scanResponse ?: return persistentListOf() val cardTypesResolver = scanResponse.cardTypesResolver - return SettingsElement.values().mapNotNull { - when (it) { - SettingsElement.WalletConnect -> if (cardTypesResolver.isMultiwalletAllowed()) it else null - SettingsElement.SendFeedback -> it - SettingsElement.LinkMoreCards -> if (state.createBackupAllowed) it else null - SettingsElement.PrivacyPolicy -> if (state.privacyPolicyUrl != null) it else null - SettingsElement.AppSettings -> if (state.appSettingsState.isBiometricsAvailable) it else null - SettingsElement.ReferralProgram -> if (cardTypesResolver.isTangemWallet()) it else null - SettingsElement.TesterMenu -> if (BuildConfig.TESTER_MENU_ENABLED) it else null - else -> it - } - } + return buildList { + SettingsItem.WalletConnect(::navigateToWalletConnect) + .takeIf { cardTypesResolver.isMultiwalletAllowed() } + ?.let(::add) + + SettingsItem.AddWallet(showProgress = state.isScanningInProgress, ::scanAndSaveUserWallet) + .takeIf { state.appSettingsState.saveWallets } + ?.let(::add) + + SettingsItem.ScanWallet(showProgress = state.isScanningInProgress, ::scanAndSaveUserWallet) + .takeUnless { state.appSettingsState.saveWallets } + ?.let(::add) + + SettingsItem.LinkMoreCards(::linkMoreCards) + .takeIf { state.createBackupAllowed } + ?.let(::add) + + SettingsItem.CardSettings(::navigateToCardSettings) + .let(::add) + + SettingsItem.AppSettings(::navigateToAppSettings) + .let(::add) + + SettingsItem.Chat(::navigateToChat) + .let(::add) + + SettingsItem.SendFeedback(::sendFeedback) + .let(::add) + + SettingsItem.ReferralProgram(::navigateToReferralProgram) + .takeIf { cardTypesResolver.isTangemWallet() } + ?.let(::add) + + SettingsItem.TermsOfService(::navigateToToS) + .let(::add) + + SettingsItem.TesterMenu(::navigateToTesterMenu) + .takeIf { BuildConfig.TESTER_MENU_ENABLED } + ?.let(::add) + }.toImmutableList() } - private fun handleSocialNetworkClick(link: SocialNetworkLink) { - Analytics.send(Settings.ButtonSocialNetwork(link.network)) - store.dispatch(NavigationAction.OpenUrl(link.url)) - } - - private fun handleClickingSettingsItem(item: SettingsElement) { - when (item) { - SettingsElement.WalletConnect -> { - Analytics.send(Settings.ButtonWalletConnect()) - store.dispatch(NavigationAction.NavigateTo(AppScreen.WalletConnectSessions)) - } - SettingsElement.Chat -> { - Analytics.send(Settings.ButtonChat()) - store.dispatch(GlobalAction.OpenChat(SupportInfo())) - } - SettingsElement.SendFeedback -> { - Analytics.send(Settings.ButtonSendFeedback()) - store.dispatch(GlobalAction.SendEmail(FeedbackEmail())) - } - SettingsElement.CardSettings -> { - Analytics.send(Settings.ButtonCardSettings()) - store.dispatch(NavigationAction.NavigateTo(AppScreen.CardSettings)) - } - SettingsElement.AppSettings -> { - Analytics.send(Settings.ButtonAppSettings()) - store.dispatch(NavigationAction.NavigateTo(AppScreen.AppSettings)) - } - SettingsElement.LinkMoreCards -> { - Analytics.send(Settings.ButtonCreateBackup()) - store.dispatch(WalletAction.MultiWallet.BackupWallet) - } - SettingsElement.TermsOfService -> { - store.dispatch(DisclaimerAction.Show(AppScreen.Details)) - } - SettingsElement.PrivacyPolicy -> { - // TODO: To be available later - } - SettingsElement.ReferralProgram -> { - store.dispatch(NavigationAction.NavigateTo(AppScreen.ReferralProgram)) - } - SettingsElement.TesterMenu -> { - store.state.daggerGraphState.testerRouter?.startTesterScreen() - } - } - } - - private fun getSocialLinks(): List { - val locale = LocaleRegionProvider().getRegion() - return if (locale.lowercase() == RUSSIA_COUNTRY_CODE) { - TangemSocialAccounts.accountsRu + private fun triggerErrorSnackbarIfNeeded(text: TextReference?): StateEvent { + return if (text == null) { + consumedEvent() } else { - TangemSocialAccounts.accountsEn + triggeredEvent(text) { + store.dispatch(DetailsAction.DismissError) + } } } @@ -127,6 +116,66 @@ internal class DetailsViewModel(private val store: Store) { return "$versionName ($versionCode)" } + private fun navigateToTesterMenu() { + store.state.daggerGraphState.testerRouter?.startTesterScreen() + } + + private fun navigateToToS() { + store.dispatch(DisclaimerAction.Show(AppScreen.Details)) + } + + private fun navigateToReferralProgram() { + store.dispatch(NavigationAction.NavigateTo(AppScreen.ReferralProgram)) + } + + private fun sendFeedback() { + Analytics.send(Settings.ButtonSendFeedback()) + store.dispatch(GlobalAction.SendEmail(FeedbackEmail())) + } + + private fun navigateToChat() { + Analytics.send(Settings.ButtonChat()) + store.dispatch(GlobalAction.OpenChat(SupportInfo())) + } + + private fun navigateToAppSettings() { + Analytics.send(Settings.ButtonAppSettings()) + store.dispatch(NavigationAction.NavigateTo(AppScreen.AppSettings)) + } + + private fun navigateToCardSettings() { + Analytics.send(Settings.ButtonCardSettings()) + store.dispatch(NavigationAction.NavigateTo(AppScreen.CardSettings)) + } + + private fun linkMoreCards() { + Analytics.send(Settings.ButtonCreateBackup()) + store.dispatch(WalletAction.MultiWallet.BackupWallet) + } + + private fun scanAndSaveUserWallet() { + store.dispatch(DetailsAction.ScanAndSaveUserWallet) + } + + private fun navigateToWalletConnect() { + Analytics.send(Settings.ButtonWalletConnect()) + store.dispatch(NavigationAction.NavigateTo(AppScreen.WalletConnectSessions)) + } + + private fun handleSocialNetworkClick(link: SocialNetworkLink) { + Analytics.send(Settings.ButtonSocialNetwork(link.network)) + store.dispatch(NavigationAction.OpenUrl(link.url)) + } + + private fun getSocialLinks(): ImmutableList { + val locale = LocaleRegionProvider().getRegion() + return if (locale.lowercase() == RUSSIA_COUNTRY_CODE) { + TangemSocialAccounts.accountsRu + } else { + TangemSocialAccounts.accountsEn + } + } + private fun bootstrapScreenState() { userWalletsListManager.selectedUserWallet .distinctUntilChanged() From 5ed381c26378ec95b8e37dabe776352f76c58a31 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 13 Sep 2023 14:19:25 +0300 Subject: [PATCH 033/242] Updated on 2026-08-14 --- .../viewmodels/TokensListMigration.kt | 11 +-- .../tokens/legacy/redux/TokensMiddleware.kt | 4 +- .../converters/CryptoCurrencyConverter.kt | 6 +- .../middlewares/TradeCryptoMiddleware.kt | 13 +-- .../java/com/tangem/utils/extensions/List.kt | 5 +- .../java/com/tangem/utils/extensions/Set.kt | 39 +++++++++ .../repository/DefaultCurrenciesRepository.kt | 27 +++--- .../repository/DefaultNetworksRepository.kt | 86 +++++++++---------- .../utils/CardCryptoCurrenciesFactory.kt | 61 +++++++++++++ .../tokens/utils/CardCurrenciesFactory.kt | 48 ----------- .../tokens/utils/CryptoCurrencyFactory.kt | 23 +++-- .../data/tokens/utils/NetworkConverter.kt | 22 ----- .../data/tokens/utils/NetworkOperations.kt | 31 ++++++- .../data/tokens/utils/NetworkStatusFactory.kt | 4 +- ....kt => ResponseCryptoCurrenciesFactory.kt} | 58 +++++++++---- .../data/tokens/utils/TokensOperations.kt | 45 +++++----- .../tokens/utils/UserTokensResponseFactory.kt | 2 +- .../repository/DefaultTxHistoryRepository.kt | 14 +-- .../DefaultWalletManagersFacade.kt | 74 +++++++++------- .../walletmanager/WalletManagersFacade.kt | 34 ++++---- .../domain/tokens/models/CryptoCurrency.kt | 72 ++++++++++++---- .../tangem/domain/tokens/models/Network.kt | 35 ++++++++ .../tokens/FetchCurrencyStatusUseCase.kt | 6 +- .../domain/tokens/FetchTokenListUseCase.kt | 6 +- .../domain/tokens/RemoveCurrencyUseCase.kt | 14 ++- .../tokens/model/CryptoCurrencyStatus.kt | 47 +++++----- .../domain/tokens/model/NetworkStatus.kt | 4 +- .../CurrenciesStatusesOperations.kt | 34 ++++---- .../operations/CurrencyStatusOperations.kt | 2 +- .../TokenListFiatBalanceOperations.kt | 1 + .../tokens/repository/NetworksRepository.kt | 17 +--- ...PrimaryCurrencyStatusUpdatesUseCaseTest.kt | 2 +- .../domain/tokens/GetTokenListUseCaseTest.kt | 2 +- .../tangem/domain/tokens/mock/MockNetworks.kt | 11 +-- .../tangem/domain/tokens/mock/MockTokens.kt | 73 +++++++++++----- .../domain/tokens/mock/MockTokensStates.kt | 4 +- .../repository/MockNetworksRepository.kt | 9 +- .../repository/TxHistoryRepository.kt | 10 +-- .../usecase/GetTxHistoryItemsCountUseCase.kt | 4 +- .../usecase/GetTxHistoryItemsUseCase.kt | 7 +- .../wallets/usecase/GetExploreUrlUseCase.kt | 9 +- .../TokenDetailsLoadedBalanceConverter.kt | 2 + .../viewmodels/TokenDetailsViewModel.kt | 8 +- .../presentation/common/WalletPreviewData.kt | 6 +- .../organizetokens/model/DraggableItem.kt | 6 +- .../utils/common/IdsOperations.kt | 2 +- .../CryptoCurrencyToDraggableItemConverter.kt | 2 +- .../NetworkGroupToDraggableItemsConverter.kt | 2 +- .../utils/dnd/DraggableGroupsOperations.kt | 2 +- .../state/components/WalletTokensListState.kt | 15 +++- ...letSingleCurrencyLoadedBalanceConverter.kt | 2 + .../multicurrency/MultiCurrencyContent.kt | 7 +- .../multicurrency/MultiCurrencyContentItem.kt | 2 +- ...ryptoCurrencyStatusToTokenItemConverter.kt | 1 + .../utils/TokenListToContentItemsConverter.kt | 9 +- .../wallet/viewmodels/WalletViewModel.kt | 44 ++++------ 56 files changed, 631 insertions(+), 455 deletions(-) create mode 100644 core/utils/src/main/java/com/tangem/utils/extensions/Set.kt create mode 100644 data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/CardCryptoCurrenciesFactory.kt delete mode 100644 data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/CardCurrenciesFactory.kt delete mode 100644 data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/NetworkConverter.kt rename data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/{ResponseCurrenciesFactory.kt => ResponseCryptoCurrenciesFactory.kt} (55%) diff --git a/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/viewmodels/TokensListMigration.kt b/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/viewmodels/TokensListMigration.kt index 8ce3e83f02..8340df9959 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/viewmodels/TokensListMigration.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/viewmodels/TokensListMigration.kt @@ -54,7 +54,6 @@ internal class TokensListMigration( } is Either.Right -> { currentUserWallet = selectedWalletEither.value - val derivationStyle = currentUserWallet.scanResponse.derivationStyleProvider.getDerivationStyle() when (val currenciesEither = getCurrenciesUseCase(userWalletId = selectedWalletEither.value.walletId)) { is Either.Left -> { @@ -65,7 +64,7 @@ internal class TokensListMigration( TokensListCryptoCurrencies( coins = currenciesEither.value .filterIsInstance() - .filterNot { it.isCustomCurrency(derivationStyle) } + .filterNot { it.isCustom } .also { currentNewCoins = it } .map { Blockchain.fromId(it.network.id.value) }, tokens = currenciesEither.value @@ -91,12 +90,6 @@ internal class TokensListMigration( } } - private fun CryptoCurrency.Coin.isCustomCurrency(derivationStyle: DerivationStyle?): Boolean { - if (derivationPath == null || derivationStyle == null) return false - - return derivationPath != Blockchain.fromId(network.id.value).derivationPath(derivationStyle)?.rawPath - } - private fun getLegacyCryptoCurrencies(): TokensListCryptoCurrencies { val wallets = store.state.walletState.walletsDataFromStores val derivationStyle = store.state.globalState.scanResponse?.derivationStyleProvider?.getDerivationStyle() @@ -157,12 +150,14 @@ internal class TokensListMigration( cryptoCurrencyFactory.createToken( sdkToken = it.token, blockchain = it.blockchain, + extraDerivationPath = null, derivationStyleProvider = currentUserWallet.scanResponse.derivationStyleProvider, ) }, changedCoins = changedBlockchainList.mapNotNull { cryptoCurrencyFactory.createCoin( blockchain = it, + extraDerivationPath = null, derivationStyleProvider = currentUserWallet.scanResponse.derivationStyleProvider, ) }, diff --git a/app/src/main/java/com/tangem/tap/features/tokens/legacy/redux/TokensMiddleware.kt b/app/src/main/java/com/tangem/tap/features/tokens/legacy/redux/TokensMiddleware.kt index 012bdf1618..aeecc533ac 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/legacy/redux/TokensMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/legacy/redux/TokensMiddleware.kt @@ -298,7 +298,7 @@ object TokensMiddleware { val customTokensCandidates = currencyList .filter { Blockchain.fromId(it.network.id.value).getSupportedCurves().contains(curve) } - .mapNotNull(CryptoCurrency::derivationPath) + .mapNotNull { it.network.derivationPath.value } .map(::DerivationPath) val bothCandidates = (manageTokensCandidates + customTokensCandidates).distinct().toMutableList() @@ -306,7 +306,7 @@ object TokensMiddleware { currencyList.find { it is CryptoCurrency.Coin && Blockchain.fromId(it.network.id.value) == Blockchain.Cardano } ?.let { currency -> - currency.derivationPath?.let { + currency.network.derivationPath.value?.let { bothCandidates.add(CardanoUtils.extendedDerivationPath(DerivationPath(it))) } } diff --git a/app/src/main/java/com/tangem/tap/features/wallet/converters/CryptoCurrencyConverter.kt b/app/src/main/java/com/tangem/tap/features/wallet/converters/CryptoCurrencyConverter.kt index 832de0bda1..8506d82036 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/converters/CryptoCurrencyConverter.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/converters/CryptoCurrencyConverter.kt @@ -18,6 +18,7 @@ internal class CryptoCurrencyConverter : TwoWayConverter requireNotNull( cryptoCurrencyFactory.createCoin( blockchain = value.blockchain, + extraDerivationPath = value.derivationPath, derivationStyleProvider = requireNotNull( store.state.globalState .userWalletsListManager @@ -31,6 +32,7 @@ internal class CryptoCurrencyConverter : TwoWayConverter Currency.Blockchain( blockchain = blockchain, - derivationPath = value.derivationPath, + derivationPath = value.network.derivationPath.value, ) is CryptoCurrency.Token -> Currency.Token( token = Token( @@ -60,7 +62,7 @@ internal class CryptoCurrencyConverter : TwoWayConverter MutableList.replaceBy(item: T, predicate: (T) -> Boolean): Boo /** * Adds the specified element to the list or replaces an existing element. - * The predicate defines the condition to replace the existing element. + * + * !!!This function is not thread-safe!!! * * @param item The element to be added or replace the existing one. * @param predicate The condition to replace an existing element. diff --git a/core/utils/src/main/java/com/tangem/utils/extensions/Set.kt b/core/utils/src/main/java/com/tangem/utils/extensions/Set.kt new file mode 100644 index 0000000000..f5c0cf1eb7 --- /dev/null +++ b/core/utils/src/main/java/com/tangem/utils/extensions/Set.kt @@ -0,0 +1,39 @@ +package com.tangem.utils.extensions + +/** + * Replaces an element in the set with the provided item based on the predicate. + * + * !!!This function is not thread-safe!!! + * + * @param item The element to replace the existing one. + * @param predicate The condition to replace an existing element. + * @return [Boolean] indicating whether an element was replaced. + */ +inline fun MutableSet.replaceBy(item: T, predicate: (T) -> Boolean): Boolean { + val foundItem = firstOrNull(predicate) ?: return false + + remove(foundItem) + add(item) + + return true +} + +/** + * Adds the specified element to the set or replaces an existing element. + * + * !!!This function is not thread-safe!!! + * + * @param item The element to be added or replace the existing one. + * @param predicate The condition to replace an existing element. + * @return The modified [Set] after adding or replacing the element. + */ +inline fun Set.addOrReplace(item: T, predicate: (T) -> Boolean): Set { + val mutableList = this.toMutableSet() + val isReplaced = mutableList.replaceBy(item, predicate) + + if (!isReplaced) { + mutableList.add(item) + } + + return mutableList +} \ No newline at end of file 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 0e83c656d4..5dda8b7e59 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 @@ -8,6 +8,7 @@ import com.tangem.datasource.api.tangemTech.models.UserTokensResponse import com.tangem.datasource.local.token.UserMarketCoinsStore import com.tangem.datasource.local.token.UserTokensStore import com.tangem.datasource.local.userwallet.UserWalletsStore +import com.tangem.domain.common.extensions.toCoinId import com.tangem.domain.common.extensions.toNetworkId import com.tangem.domain.common.util.derivationStyleProvider import com.tangem.domain.core.error.DataError @@ -35,8 +36,8 @@ internal class DefaultCurrenciesRepository( ) : CurrenciesRepository { private val demoConfig = DemoConfig() - private val responseCurrenciesFactory = ResponseCurrenciesFactory(demoConfig) - private val cardCurrenciesFactory = CardCurrenciesFactory(demoConfig) + private val responseCurrenciesFactory = ResponseCryptoCurrenciesFactory(demoConfig) + private val cardCurrenciesFactory = CardCryptoCurrenciesFactory(demoConfig) private val userTokensResponseFactory = UserTokensResponseFactory() override suspend fun saveTokens( @@ -90,6 +91,7 @@ internal class DefaultCurrenciesRepository( .mapNotNull { CryptoCurrencyFactory().createCoin( blockchain = getBlockchain(networkId = it.network.id), + extraDerivationPath = it.network.derivationPath.value, derivationStyleProvider = getUserWallet(userWalletId).scanResponse.derivationStyleProvider, ) } @@ -99,7 +101,7 @@ internal class DefaultCurrenciesRepository( return any { val blockchain = getBlockchain(networkId = token.network.id) - it.id == getCoinId(blockchain).rawCurrencyId + it.id == blockchain.toCoinId() } } @@ -173,10 +175,7 @@ internal class DefaultCurrenciesRepository( "Unable to find tokens response for user wallet with provided ID: $userWalletId" } - return responseCurrenciesFactory.createCurrencies( - response = storedTokens, - card = userWallet.scanResponse.card, - ) + return responseCurrenciesFactory.createCurrencies(storedTokens, userWallet.scanResponse) } override suspend fun getMultiCurrencyWalletCurrency( @@ -190,7 +189,7 @@ internal class DefaultCurrenciesRepository( "Unable to find tokens response for user wallet with provided ID: $userWalletId" } - responseCurrenciesFactory.createCurrency(id, response, userWallet.scanResponse.card) + responseCurrenciesFactory.createCurrency(id, response, userWallet.scanResponse) } override suspend fun getNetworkCoin(userWalletId: UserWalletId, networkId: Network.ID): CryptoCurrency.Coin { @@ -206,10 +205,7 @@ internal class DefaultCurrenciesRepository( val storedCoin = storedTokens.tokens.find { it.networkId == Blockchain.fromId(networkId.value).toNetworkId() } ?: error("Coin in this network $networkId not found") - val coin = responseCurrenciesFactory.createCurrency( - responseToken = storedCoin, - card = userWallet.scanResponse.card, - ) + val coin = responseCurrenciesFactory.createCurrency(storedCoin, userWallet.scanResponse) return coin as? CryptoCurrency.Coin ?: error("Unable to create currency") } @@ -242,7 +238,7 @@ internal class DefaultCurrenciesRepository( return userTokensStore.get(userWallet.walletId).map { storedTokens -> responseCurrenciesFactory.createCurrencies( response = storedTokens, - card = userWallet.scanResponse.card, + scanResponse = userWallet.scanResponse, ) } } @@ -288,10 +284,7 @@ internal class DefaultCurrenciesRepository( if (NOT_FOUND_HTTP_CODE in errorMessage) { val response = userTokensStore.getSyncOrNull(userWallet.walletId) ?: userTokensResponseFactory.createUserTokensResponse( - currencies = cardCurrenciesFactory.createDefaultCoinsForMultiCurrencyCard( - card = userWallet.scanResponse.card, - derivationStyleProvider = userWallet.scanResponse.derivationStyleProvider, - ), + currencies = cardCurrenciesFactory.createDefaultCoinsForMultiCurrencyCard(userWallet.scanResponse), isGroupedByNetwork = false, isSortedByBalance = false, ) diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultNetworksRepository.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultNetworksRepository.kt index b8861a9533..667bf5c512 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultNetworksRepository.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultNetworksRepository.kt @@ -1,10 +1,9 @@ package com.tangem.data.tokens.repository import com.tangem.data.common.cache.CacheRegistry -import com.tangem.data.tokens.utils.CardCurrenciesFactory -import com.tangem.data.tokens.utils.NetworkConverter +import com.tangem.data.tokens.utils.CardCryptoCurrenciesFactory import com.tangem.data.tokens.utils.NetworkStatusFactory -import com.tangem.data.tokens.utils.ResponseCurrenciesFactory +import com.tangem.data.tokens.utils.ResponseCryptoCurrenciesFactory import com.tangem.datasource.local.token.UserTokensStore import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.domain.demo.DemoConfig @@ -13,7 +12,6 @@ import com.tangem.domain.tokens.models.CryptoCurrency import com.tangem.domain.tokens.models.Network import com.tangem.domain.tokens.repository.NetworksRepository import com.tangem.domain.walletmanager.WalletManagersFacade -import com.tangem.domain.walletmanager.model.UpdateWalletManagerResult import com.tangem.domain.wallets.models.UserWalletId import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.extensions.addOrReplace @@ -29,25 +27,18 @@ internal class DefaultNetworksRepository( ) : NetworksRepository { private val demoConfig by lazy { DemoConfig() } - private val networkConverter by lazy { NetworkConverter() } - private val cardCurrenciesFactory by lazy { CardCurrenciesFactory(demoConfig) } - private val responseCurrenciesFactory by lazy { ResponseCurrenciesFactory(demoConfig) } + private val cardCurrenciesFactory by lazy { CardCryptoCurrenciesFactory(demoConfig) } + private val responseCurrenciesFactory by lazy { ResponseCryptoCurrenciesFactory(demoConfig) } private val networkStatusFactory by lazy { NetworkStatusFactory() } - private val networksStatuses: MutableStateFlow> = MutableStateFlow(emptyList()) - - override fun getNetworks(networksIds: Set): Set { - return networkConverter.convertSet(networksIds) - } + private val networksStatuses: MutableStateFlow> = MutableStateFlow(hashSetOf()) override fun getNetworkStatusesUpdates( userWalletId: UserWalletId, - networks: Set, + networks: Set, ): Flow> = channelFlow { launch(dispatchers.io) { - networksStatuses.collect { - send(it.toSet()) - } + networksStatuses.collect(::send) } launch(dispatchers.io) { @@ -57,7 +48,7 @@ internal class DefaultNetworksRepository( override suspend fun getNetworkStatusesSync( userWalletId: UserWalletId, - networks: Set, + networks: Set, refresh: Boolean, ): Set = withContext(dispatchers.io) { fetchNetworksStatusesIfCacheExpired(userWalletId, networks, refresh) @@ -66,14 +57,14 @@ internal class DefaultNetworksRepository( private suspend fun fetchNetworksStatusesIfCacheExpired( userWalletId: UserWalletId, - networks: Set, + networks: Set, refresh: Boolean, ) { coroutineScope { networks - .map { networkId -> + .map { network -> async { - fetchNetworkStatusIfCacheExpired(userWalletId, networkId, refresh) + fetchNetworkStatusIfCacheExpired(userWalletId, network, refresh) } } .awaitAll() @@ -82,67 +73,70 @@ internal class DefaultNetworksRepository( private suspend fun fetchNetworkStatusIfCacheExpired( userWalletId: UserWalletId, - networkId: Network.ID, + network: Network, refresh: Boolean, ) { cacheRegistry.invokeOnExpire( - key = getNetworksStatusesCacheKey(userWalletId, networkId), + key = getNetworksStatusesCacheKey(userWalletId, network), skipCache = refresh, - block = { fetchNetworkStatus(userWalletId, networkId) }, + block = { fetchNetworkStatus(userWalletId, network) }, ) } - private suspend fun fetchNetworkStatus(userWalletId: UserWalletId, networkId: Network.ID) { - val currencies = getCurrencies(userWalletId) - .asSequence() - .filter { it.network.id == networkId } + private suspend fun fetchNetworkStatus(userWalletId: UserWalletId, network: Network) { + val currencies = getCurrencies(userWalletId, network) val result = walletManagersFacade.update( userWalletId = userWalletId, - networkId = networkId, + network = network, extraTokens = currencies.filterIsInstance().toSet(), ) - // Invalidate cache key if wallet manager update failed - when (result) { - is UpdateWalletManagerResult.Verified, - is UpdateWalletManagerResult.NoAccount, - -> Unit - is UpdateWalletManagerResult.Unreachable, - is UpdateWalletManagerResult.MissedDerivation, - -> cacheRegistry.invalidate(getNetworksStatusesCacheKey(userWalletId, networkId)) - } - val networkStatus = networkStatusFactory.createNetworkStatus( - networkId = networkId, + network = network, result = result, currencies = currencies.toSet(), ) networksStatuses.update { statuses -> - statuses.addOrReplace(networkStatus) { it.networkId == networkStatus.networkId } + statuses.addOrReplace(networkStatus) { it.network == networkStatus.network } } + + invalidateCacheKeyIfNeeded(userWalletId, networkStatus) } - private suspend fun getCurrencies(userWalletId: UserWalletId): List { + private suspend fun getCurrencies(userWalletId: UserWalletId, network: Network): Sequence { val userWallet = requireNotNull(userWalletsStore.getSyncOrNull(userWalletId)) { "Unable to find user wallet with provided ID: $userWalletId" } - return if (userWallet.isMultiCurrency) { + val currencies = if (userWallet.isMultiCurrency) { val response = requireNotNull(userTokensStore.getSyncOrNull(userWalletId)) { "Unable to find tokens response for user wallet with provided ID: $userWalletId" } - responseCurrenciesFactory.createCurrencies(response, userWallet.scanResponse.card) + responseCurrenciesFactory.createCurrencies(response, userWallet.scanResponse).asSequence() } else { val currency = cardCurrenciesFactory.createPrimaryCurrencyForSingleCurrencyCard(userWallet.scanResponse) - listOf(currency) + sequenceOf(currency) + } + + return currencies.filter { it.network == network } + } + + private suspend fun invalidateCacheKeyIfNeeded(userWalletId: UserWalletId, networkStatus: NetworkStatus) { + when (networkStatus.value) { + is NetworkStatus.Verified, + is NetworkStatus.NoAccount, + -> Unit + is NetworkStatus.Unreachable, + is NetworkStatus.MissedDerivation, + -> cacheRegistry.invalidate(getNetworksStatusesCacheKey(userWalletId, networkStatus.network)) } } - private fun getNetworksStatusesCacheKey(userWalletId: UserWalletId, nerworkId: Network.ID): String { - return "network_status_${userWalletId}_${nerworkId.value}" + private fun getNetworksStatusesCacheKey(userWalletId: UserWalletId, network: Network): String { + return "network_status_${userWalletId}_${network.id}_${network.derivationPath.value}" } } \ No newline at end of file diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/CardCryptoCurrenciesFactory.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/CardCryptoCurrenciesFactory.kt new file mode 100644 index 0000000000..aa96c7a9b5 --- /dev/null +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/CardCryptoCurrenciesFactory.kt @@ -0,0 +1,61 @@ +package com.tangem.data.tokens.utils + +import com.tangem.blockchain.common.Blockchain +import com.tangem.domain.common.TapWorkarounds.isTestCard +import com.tangem.domain.common.util.cardTypesResolver +import com.tangem.domain.common.util.derivationStyleProvider +import com.tangem.domain.demo.DemoConfig +import com.tangem.domain.models.scan.ScanResponse +import com.tangem.domain.tokens.models.CryptoCurrency + +internal class CardCryptoCurrenciesFactory(private val demoConfig: DemoConfig) { + + private val cryptoCurrencyFactory = CryptoCurrencyFactory() + + fun createDefaultCoinsForMultiCurrencyCard(scanResponse: ScanResponse): List { + val cardDerivationStyleProvider = scanResponse.derivationStyleProvider + val card = scanResponse.card + + var blockchains = if (demoConfig.isDemoCardId(card.cardId)) { + demoConfig.demoBlockchains + } else { + listOf(Blockchain.Bitcoin, Blockchain.Ethereum) + } + + if (card.isTestCard) { + blockchains = blockchains.mapNotNull { it.getTestnetVersion() } + } + + return blockchains.mapNotNull { + cryptoCurrencyFactory.createCoin( + blockchain = it, + extraDerivationPath = null, + derivationStyleProvider = cardDerivationStyleProvider, + ) + } + } + + fun createPrimaryCurrencyForSingleCurrencyCard(scanResponse: ScanResponse): CryptoCurrency { + val cardDerivationStyleProvider = scanResponse.derivationStyleProvider + val resolver = scanResponse.cardTypesResolver + val blockchain = resolver.getBlockchain() + + val coin = cryptoCurrencyFactory.createCoin( + blockchain = blockchain, + extraDerivationPath = null, + derivationStyleProvider = cardDerivationStyleProvider, + ) + requireNotNull(coin) { "Coin for the single currency card cannot be null" } + + val primaryToken = resolver.getPrimaryToken()?.let { token -> + cryptoCurrencyFactory.createToken( + sdkToken = token, + blockchain = blockchain, + extraDerivationPath = null, + derivationStyleProvider = cardDerivationStyleProvider, + ) + } + + return primaryToken ?: coin + } +} \ No newline at end of file diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/CardCurrenciesFactory.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/CardCurrenciesFactory.kt deleted file mode 100644 index 269c65e9f7..0000000000 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/CardCurrenciesFactory.kt +++ /dev/null @@ -1,48 +0,0 @@ -package com.tangem.data.tokens.utils - -import com.tangem.blockchain.common.Blockchain -import com.tangem.domain.common.DerivationStyleProvider -import com.tangem.domain.common.TapWorkarounds.isTestCard -import com.tangem.domain.common.util.cardTypesResolver -import com.tangem.domain.common.util.derivationStyleProvider -import com.tangem.domain.demo.DemoConfig -import com.tangem.domain.models.scan.CardDTO -import com.tangem.domain.models.scan.ScanResponse -import com.tangem.domain.tokens.models.CryptoCurrency - -internal class CardCurrenciesFactory(private val demoConfig: DemoConfig) { - - private val cryptoCurrencyFactory by lazy { CryptoCurrencyFactory() } - - fun createDefaultCoinsForMultiCurrencyCard( - card: CardDTO, - derivationStyleProvider: DerivationStyleProvider, - ): List { - var blockchains = if (demoConfig.isDemoCardId(card.cardId)) { - demoConfig.demoBlockchains - } else { - listOf(Blockchain.Bitcoin, Blockchain.Ethereum) - } - - if (card.isTestCard) { - blockchains = blockchains.mapNotNull { it.getTestnetVersion() } - } - - return blockchains.mapNotNull { cryptoCurrencyFactory.createCoin(it, derivationStyleProvider) } - } - - fun createPrimaryCurrencyForSingleCurrencyCard(scanResponse: ScanResponse): CryptoCurrency { - val derivationStyleProvider = scanResponse.derivationStyleProvider - val resolver = scanResponse.cardTypesResolver - val blockchain = resolver.getBlockchain() - - val coin = requireNotNull(cryptoCurrencyFactory.createCoin(blockchain, derivationStyleProvider)) { - "Coin for the single currency card cannot be null" - } - val primaryToken = resolver.getPrimaryToken()?.let { token -> - cryptoCurrencyFactory.createToken(token, blockchain, derivationStyleProvider) - } - - return primaryToken ?: coin - } -} \ No newline at end of file diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/CryptoCurrencyFactory.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/CryptoCurrencyFactory.kt index e6fff3176f..e12c65ff37 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/CryptoCurrencyFactory.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/CryptoCurrencyFactory.kt @@ -2,6 +2,7 @@ package com.tangem.data.tokens.utils import com.tangem.blockchain.common.Blockchain import com.tangem.domain.common.DerivationStyleProvider +import com.tangem.domain.common.extensions.toCoinId import com.tangem.domain.tokens.models.CryptoCurrency import timber.log.Timber import com.tangem.blockchain.common.Token as SdkToken @@ -12,6 +13,7 @@ class CryptoCurrencyFactory { fun createToken( sdkToken: SdkToken, blockchain: Blockchain, + extraDerivationPath: String?, derivationStyleProvider: DerivationStyleProvider, ): CryptoCurrency.Token? { if (blockchain == Blockchain.Unknown) { @@ -19,35 +21,40 @@ class CryptoCurrencyFactory { return null } - val id = getTokenId(blockchain, sdkToken) + val network = getNetwork(blockchain, extraDerivationPath, derivationStyleProvider) ?: return null + val id = getTokenId(network, sdkToken) return CryptoCurrency.Token( id = id, - network = getNetwork(blockchain) ?: return null, + network = network, name = sdkToken.name, symbol = sdkToken.symbol, iconUrl = getTokenIconUrl(blockchain, sdkToken), decimals = sdkToken.decimals, - isCustom = isCustomToken(id), + isCustom = isCustomToken(id, network), contractAddress = sdkToken.contractAddress, - derivationPath = getDerivationPath(blockchain, derivationStyleProvider), ) } - fun createCoin(blockchain: Blockchain, derivationStyleProvider: DerivationStyleProvider): CryptoCurrency.Coin? { + fun createCoin( + blockchain: Blockchain, + extraDerivationPath: String?, + derivationStyleProvider: DerivationStyleProvider, + ): CryptoCurrency.Coin? { if (blockchain == Blockchain.Unknown) { Timber.e("Unable to map the SDK token to the domain token with Unknown blockchain") return null } + val network = getNetwork(blockchain, extraDerivationPath, derivationStyleProvider) ?: return null return CryptoCurrency.Coin( - id = getCoinId(blockchain), - network = getNetwork(blockchain) ?: return null, + id = getCoinId(network, blockchain.toCoinId()), + network = network, name = blockchain.fullName, symbol = blockchain.currency, iconUrl = getCoinIconUrl(blockchain), decimals = blockchain.decimals(), - derivationPath = getDerivationPath(blockchain, derivationStyleProvider), + isCustom = isCustomCoin(network), ) } } \ No newline at end of file diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/NetworkConverter.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/NetworkConverter.kt deleted file mode 100644 index 6c57906a38..0000000000 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/NetworkConverter.kt +++ /dev/null @@ -1,22 +0,0 @@ -package com.tangem.data.tokens.utils - -import com.tangem.blockchain.common.Blockchain -import com.tangem.domain.tokens.models.Network -import com.tangem.utils.converter.Converter - -internal class NetworkConverter : Converter { - - override fun convert(value: Network.ID): Network? { - val blockchain = Blockchain.fromId(value.value) - - return getNetwork(blockchain) - } - - override fun convertList(input: Collection): List { - return input.mapNotNull(::convert) - } - - override fun convertSet(input: Collection): Set { - return input.mapNotNullTo(hashSetOf(), ::convert) - } -} \ No newline at end of file diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/NetworkOperations.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/NetworkOperations.kt index 3a536a46ed..e70b7a4060 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/NetworkOperations.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/NetworkOperations.kt @@ -1,10 +1,19 @@ package com.tangem.data.tokens.utils import com.tangem.blockchain.common.Blockchain +import com.tangem.domain.common.DerivationStyleProvider import com.tangem.domain.tokens.models.Network import timber.log.Timber -internal fun getNetwork(blockchain: Blockchain): Network? { +internal fun getBlockchain(networkId: Network.ID): Blockchain { + return Blockchain.fromId(networkId.value) +} + +internal fun getNetwork( + blockchain: Blockchain, + extraDerivationPath: String?, + derivationStyleProvider: DerivationStyleProvider, +): Network? { if (blockchain == Blockchain.Unknown) { Timber.e("Unable to convert Unknown blockchain to the domain network model") return null @@ -14,10 +23,26 @@ internal fun getNetwork(blockchain: Blockchain): Network? { id = Network.ID(blockchain.id), name = blockchain.fullName, isTestnet = blockchain.isTestnet(), + derivationPath = getDerivationPath(blockchain, extraDerivationPath, derivationStyleProvider), standardType = getNetworkStandardType(blockchain), ) } +private fun getDerivationPath( + blockchain: Blockchain, + extraDerivationPath: String?, + derivationStyleProvider: DerivationStyleProvider, +): Network.DerivationPath { + val cardDerivationPath = getCardDerivationPath(blockchain, derivationStyleProvider) + + return when { + cardDerivationPath.isNullOrBlank() -> Network.DerivationPath.None + extraDerivationPath == cardDerivationPath -> Network.DerivationPath.Card(extraDerivationPath) + !extraDerivationPath.isNullOrBlank() -> Network.DerivationPath.Custom(extraDerivationPath) + else -> Network.DerivationPath.None + } +} + private fun getNetworkStandardType(blockchain: Blockchain): Network.StandardType { return when (blockchain) { Blockchain.Ethereum, Blockchain.EthereumTestnet -> Network.StandardType.ERC20 @@ -26,4 +51,8 @@ private fun getNetworkStandardType(blockchain: Blockchain): Network.StandardType Blockchain.Tron, Blockchain.TronTestnet -> Network.StandardType.TRC20 else -> Network.StandardType.Unspecified(blockchain.name) } +} + +private fun getCardDerivationPath(blockchain: Blockchain, derivationStyleProvider: DerivationStyleProvider): String? { + return blockchain.derivationPath(derivationStyleProvider.getDerivationStyle())?.rawPath } \ No newline at end of file diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/NetworkStatusFactory.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/NetworkStatusFactory.kt index faed2bda5c..76f28d82a3 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/NetworkStatusFactory.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/NetworkStatusFactory.kt @@ -14,12 +14,12 @@ import java.math.BigDecimal internal class NetworkStatusFactory { fun createNetworkStatus( - networkId: Network.ID, + network: Network, result: UpdateWalletManagerResult, currencies: Set, ): NetworkStatus { return NetworkStatus( - networkId = networkId, + network = network, value = when (result) { is UpdateWalletManagerResult.MissedDerivation -> NetworkStatus.MissedDerivation is UpdateWalletManagerResult.Unreachable -> NetworkStatus.Unreachable diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/ResponseCurrenciesFactory.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/ResponseCryptoCurrenciesFactory.kt similarity index 55% rename from data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/ResponseCurrenciesFactory.kt rename to data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/ResponseCryptoCurrenciesFactory.kt index 514ef69b10..3a7208bcde 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/ResponseCurrenciesFactory.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/ResponseCryptoCurrenciesFactory.kt @@ -3,47 +3,57 @@ package com.tangem.data.tokens.utils import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.Token import com.tangem.datasource.api.tangemTech.models.UserTokensResponse +import com.tangem.domain.common.DerivationStyleProvider import com.tangem.domain.common.extensions.fromNetworkId +import com.tangem.domain.common.extensions.toCoinId +import com.tangem.domain.common.util.derivationStyleProvider import com.tangem.domain.demo.DemoConfig -import com.tangem.domain.models.scan.CardDTO +import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.tokens.models.CryptoCurrency import timber.log.Timber import com.tangem.blockchain.common.Token as SdkToken -internal class ResponseCurrenciesFactory(private val demoConfig: DemoConfig) { +internal class ResponseCryptoCurrenciesFactory(private val demoConfig: DemoConfig) { - fun createCurrency(currencyId: CryptoCurrency.ID, response: UserTokensResponse, card: CardDTO): CryptoCurrency { + fun createCurrency( + currencyId: CryptoCurrency.ID, + response: UserTokensResponse, + scanResponse: ScanResponse, + ): CryptoCurrency { val responseTokenId = currencyId.rawCurrencyId val token = requireNotNull(response.tokens.firstOrNull { it.id == responseTokenId }) { "Unable find a token with provided ID: $responseTokenId" } - return requireNotNull(createCurrency(token, card)) { + return requireNotNull(createCurrency(token, scanResponse)) { "Unable to create a currency with provided ID: $currencyId" } } - fun createCurrencies(response: UserTokensResponse, card: CardDTO): List { - return response.tokens.mapNotNull { createCurrency(it, card) } + fun createCurrencies(response: UserTokensResponse, scanResponse: ScanResponse): List { + return response.tokens.mapNotNull { createCurrency(it, scanResponse) } } - fun createCurrency(responseToken: UserTokensResponse.Token, card: CardDTO): CryptoCurrency? { + fun createCurrency(responseToken: UserTokensResponse.Token, scanResponse: ScanResponse): CryptoCurrency? { var blockchain = Blockchain.fromNetworkId(responseToken.networkId) if (blockchain == null || blockchain == Blockchain.Unknown) { Timber.e("Unable to find a blockchain with the network ID: ${responseToken.networkId}") return null } + val cardDerivationStyleProvider = scanResponse.derivationStyleProvider + val card = scanResponse.card + if (demoConfig.isDemoCardId(card.cardId)) { blockchain = blockchain.getTestnetVersion() ?: blockchain } val sdkToken = createSdkToken(responseToken) return if (sdkToken == null) { - createCoin(blockchain, responseToken) + createCoin(blockchain, responseToken, cardDerivationStyleProvider) } else { - createToken(blockchain, sdkToken, responseToken.derivationPath) + createToken(blockchain, sdkToken, responseToken.derivationPath, cardDerivationStyleProvider) } } @@ -59,31 +69,43 @@ internal class ResponseCurrenciesFactory(private val demoConfig: DemoConfig) { } } - private fun createCoin(blockchain: Blockchain, responseToken: UserTokensResponse.Token): CryptoCurrency.Coin? { + private fun createCoin( + blockchain: Blockchain, + responseToken: UserTokensResponse.Token, + derivationStyleProvider: DerivationStyleProvider, + ): CryptoCurrency.Coin? { + val network = getNetwork(blockchain, responseToken.derivationPath, derivationStyleProvider) ?: return null + return CryptoCurrency.Coin( - id = getCoinId(blockchain), - network = getNetwork(blockchain) ?: return null, + id = getCoinId(network, blockchain.toCoinId()), + network = network, name = responseToken.name, symbol = responseToken.symbol, decimals = responseToken.decimals, - derivationPath = responseToken.derivationPath, iconUrl = getCoinIconUrl(blockchain), + isCustom = isCustomCoin(network), ) } - private fun createToken(blockchain: Blockchain, sdkToken: Token, derivationPath: String?): CryptoCurrency.Token? { - val id = getTokenId(blockchain, sdkToken) + private fun createToken( + blockchain: Blockchain, + sdkToken: Token, + responseDerivationPath: String?, + derivationStyleProvider: DerivationStyleProvider, + ): CryptoCurrency.Token? { + val network = getNetwork(blockchain, responseDerivationPath, derivationStyleProvider) + ?: return null + val id = getTokenId(network, sdkToken) return CryptoCurrency.Token( id = id, - network = getNetwork(blockchain) ?: return null, + network = network, name = sdkToken.name, symbol = sdkToken.symbol, decimals = sdkToken.decimals, - derivationPath = derivationPath, iconUrl = getTokenIconUrl(blockchain, sdkToken), contractAddress = sdkToken.contractAddress, - isCustom = isCustomToken(id), + isCustom = isCustomToken(id, network), ) } } \ No newline at end of file diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/TokensOperations.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/TokensOperations.kt index a95d6efa08..5f5e666c48 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/TokensOperations.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/TokensOperations.kt @@ -2,14 +2,13 @@ package com.tangem.data.tokens.utils import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.IconsUtil -import com.tangem.domain.common.DerivationStyleProvider import com.tangem.domain.common.extensions.toCoinId import com.tangem.domain.common.extensions.toNetworkId import com.tangem.domain.tokens.models.CryptoCurrency.ID import com.tangem.domain.tokens.models.Network import com.tangem.blockchain.common.Token as SdkToken +import com.tangem.domain.tokens.models.CryptoCurrency.ID.Body as CurrencyIdBody import com.tangem.domain.tokens.models.CryptoCurrency.ID.Prefix.COIN_PREFIX as COIN_ID_PREFIX -import com.tangem.domain.tokens.models.CryptoCurrency.ID.Prefix.CUSTOM_TOKEN_PREFIX as CUSTOM_TOKEN_ID_PREFIX import com.tangem.domain.tokens.models.CryptoCurrency.ID.Prefix.TOKEN_PREFIX as TOKEN_ID_PREFIX import com.tangem.domain.tokens.models.CryptoCurrency.ID.Suffix.ContractAddress as CustomCurrencyIdSuffix import com.tangem.domain.tokens.models.CryptoCurrency.ID.Suffix.RawID as CurrencyIdSuffix @@ -18,24 +17,27 @@ private const val DEFAULT_TOKENS_ICONS_HOST = "https://s3.eu-central-1.amazonaws private const val TOKEN_ICON_SIZE = "large" private const val TOKEN_ICON_EXT = "png" -internal fun isCustomToken(tokenId: ID): Boolean { - return tokenId.rawCurrencyId == null +internal fun isCustomToken(tokenId: ID, network: Network): Boolean { + return network.derivationPath is Network.DerivationPath.Custom || tokenId.rawCurrencyId == null } -internal fun getDerivationPath(blockchain: Blockchain, derivationStyleProvider: DerivationStyleProvider): String? { - return blockchain.derivationPath(derivationStyleProvider.getDerivationStyle())?.rawPath +internal fun isCustomCoin(network: Network): Boolean { + return network.derivationPath is Network.DerivationPath.Custom } -internal fun getBlockchain(networkId: Network.ID): Blockchain { - return Blockchain.fromId(networkId.value) +internal fun getCoinId(network: Network, coinId: String): ID { + return ID(COIN_ID_PREFIX, getCurrencyIdBody(network), CurrencyIdSuffix(rawId = coinId)) } -internal fun getCoinId(blockchain: Blockchain): ID { - return getTokenOrCoinId(blockchain, token = null) -} +internal fun getTokenId(network: Network, sdkToken: SdkToken): ID { + val sdkTokenId = sdkToken.id + val suffix = if (sdkTokenId == null) { + CustomCurrencyIdSuffix(contractAddress = sdkToken.contractAddress) + } else { + CurrencyIdSuffix(rawId = sdkTokenId) + } -internal fun getTokenId(blockchain: Blockchain, token: SdkToken): ID { - return getTokenOrCoinId(blockchain, token) + return ID(TOKEN_ID_PREFIX, getCurrencyIdBody(network), suffix) } internal fun getTokenIconUrl(blockchain: Blockchain, token: SdkToken): String? { @@ -58,15 +60,16 @@ internal fun getCoinIconUrl(blockchain: Blockchain): String? { return coinId?.let(::getTokenIconUrlFromDefaultHost) } -private fun getTokenOrCoinId(blockchain: Blockchain, token: SdkToken?): ID { - val sdkTokenId = token?.id - val (prefix, suffix) = when { - token == null -> COIN_ID_PREFIX to CurrencyIdSuffix(rawId = blockchain.toCoinId()) - sdkTokenId == null -> CUSTOM_TOKEN_ID_PREFIX to CustomCurrencyIdSuffix(contractAddress = token.contractAddress) - else -> TOKEN_ID_PREFIX to CurrencyIdSuffix(rawId = sdkTokenId) +private fun getCurrencyIdBody(network: Network): CurrencyIdBody { + return when (val path = network.derivationPath) { + is Network.DerivationPath.Custom -> CurrencyIdBody.NetworkIdWithDerivationPath( + rawId = network.id.value, + derivationPath = path.value, + ) + is Network.DerivationPath.Card, + is Network.DerivationPath.None, + -> CurrencyIdBody.NetworkId(network.id.value) } - - return ID(prefix, Network.ID(blockchain.id), suffix) } private fun getTokenIconUrlFromDefaultHost(tokenId: String): String { diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/UserTokensResponseFactory.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/UserTokensResponseFactory.kt index c6cbc7f858..c8cb48b178 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/UserTokensResponseFactory.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/UserTokensResponseFactory.kt @@ -32,7 +32,7 @@ internal class UserTokensResponseFactory { return UserTokensResponse.Token( id = currency.id.rawCurrencyId, networkId = blockchain.toNetworkId(), - derivationPath = currency.derivationPath, + derivationPath = currency.network.derivationPath.value, name = currency.name, symbol = currency.symbol, decimals = currency.decimals, diff --git a/data/txhistory/src/main/kotlin/com/tangem/data/txhistory/repository/DefaultTxHistoryRepository.kt b/data/txhistory/src/main/kotlin/com/tangem/data/txhistory/repository/DefaultTxHistoryRepository.kt index a042400894..7aa4add858 100644 --- a/data/txhistory/src/main/kotlin/com/tangem/data/txhistory/repository/DefaultTxHistoryRepository.kt +++ b/data/txhistory/src/main/kotlin/com/tangem/data/txhistory/repository/DefaultTxHistoryRepository.kt @@ -19,12 +19,11 @@ class DefaultTxHistoryRepository( private val userWalletsStore: UserWalletsStore, ) : TxHistoryRepository { - override suspend fun getTxHistoryItemsCount(networkId: Network.ID, derivationPath: String?): Int { + override suspend fun getTxHistoryItemsCount(network: Network): Int { val userWallet = getUserWallet() val state = walletManagersFacade.getTxHistoryState( userWalletId = userWallet.walletId, - networkId = networkId, - rawDerivationPath = derivationPath, + network = network, ) return when (state) { is TxHistoryState.Failed.FetchError -> throw TxHistoryStateError.DataError(state.exception) @@ -34,11 +33,7 @@ class DefaultTxHistoryRepository( } } - override fun getTxHistoryItems( - networkId: Network.ID, - derivationPath: String?, - pageSize: Int, - ): Flow> { + override fun getTxHistoryItems(network: Network, pageSize: Int): Flow> { val userWallet = getUserWallet() return Pager( config = PagingConfig( @@ -49,8 +44,7 @@ class DefaultTxHistoryRepository( loadPage = { page: Int, pageSize: Int -> walletManagersFacade.getTxHistoryItems( userWalletId = userWallet.walletId, - networkId = networkId, - rawDerivationPath = derivationPath, + network = network, page = page, pageSize = pageSize, ) diff --git a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/DefaultWalletManagersFacade.kt b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/DefaultWalletManagersFacade.kt index deb238337d..dd1cf7a25b 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/DefaultWalletManagersFacade.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/DefaultWalletManagersFacade.kt @@ -8,7 +8,6 @@ import com.tangem.crypto.hdWallet.DerivationPath import com.tangem.datasource.config.ConfigManager import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.datasource.local.walletmanager.WalletManagersStore -import com.tangem.domain.common.util.derivationStyleProvider import com.tangem.domain.common.util.hasDerivation import com.tangem.domain.demo.DemoConfig import com.tangem.domain.tokens.models.CryptoCurrency @@ -39,42 +38,46 @@ class DefaultWalletManagersFacade( override suspend fun update( userWalletId: UserWalletId, - networkId: Network.ID, + network: Network, extraTokens: Set, ): UpdateWalletManagerResult { val userWallet = getUserWallet(userWalletId) - val blockchain = Blockchain.fromId(networkId.value) + val blockchain = Blockchain.fromId(network.id.value) + val derivationPath = network.derivationPath.value - return getAndUpdateWalletManager(userWallet, blockchain, extraTokens) + return getAndUpdateWalletManager(userWallet, blockchain, derivationPath, extraTokens) } - override suspend fun getExploreUrl(userWalletId: UserWalletId, networkId: Network.ID): String { + override suspend fun getExploreUrl(userWalletId: UserWalletId, network: Network): String { val userWallet = getUserWallet(userWalletId) + val blockchain = Blockchain.fromId(network.id.value) - val blockchain = Blockchain.fromId(networkId.value) - - return getOrCreateWalletManager( + val walletManager = getOrCreateWalletManager( userWallet = userWallet, blockchain = blockchain, - derivationPath = blockchain - .derivationPath(userWallet.scanResponse.derivationStyleProvider.getDerivationStyle()), + derivationPath = network.derivationPath.value, ) - ?.wallet - ?.getExploreUrl() - .orEmpty() - } - override suspend fun getTxHistoryState( - userWalletId: UserWalletId, - networkId: Network.ID, - rawDerivationPath: String?, - ): TxHistoryState { - val userWallet = getUserWallet(userWalletId) - val blockchain = Blockchain.fromId(networkId.value) - val derivationPath = rawDerivationPath?.let(::DerivationPath) - val walletManager = requireNotNull(getOrCreateWalletManager(userWallet, blockchain, derivationPath)) { + requireNotNull(walletManager) { "Unable to get a wallet manager for blockchain: $blockchain" } + + return walletManager.wallet.getExploreUrl() + } + + override suspend fun getTxHistoryState(userWalletId: UserWalletId, network: Network): TxHistoryState { + val userWallet = getUserWallet(userWalletId) + val blockchain = Blockchain.fromId(network.id.value) + val walletManager = getOrCreateWalletManager( + userWallet = userWallet, + blockchain = blockchain, + derivationPath = network.derivationPath.value, + ) + + requireNotNull(walletManager) { + "Unable to get a wallet manager for blockchain: $blockchain" + } + return walletManager .getTransactionHistoryState(walletManager.wallet.address) .let(txHistoryStateConverter::convert) @@ -82,17 +85,22 @@ class DefaultWalletManagersFacade( override suspend fun getTxHistoryItems( userWalletId: UserWalletId, - networkId: Network.ID, - rawDerivationPath: String?, + network: Network, page: Int, pageSize: Int, ): PaginationWrapper { val userWallet = getUserWallet(userWalletId) - val blockchain = Blockchain.fromId(networkId.value) - val derivationPath = rawDerivationPath?.let(::DerivationPath) - val walletManager = requireNotNull(getOrCreateWalletManager(userWallet, blockchain, derivationPath)) { + val blockchain = Blockchain.fromId(network.id.value) + val walletManager = getOrCreateWalletManager( + userWallet = userWallet, + blockchain = blockchain, + derivationPath = network.derivationPath.value, + ) + + requireNotNull(walletManager) { "Unable to get a wallet manager for blockchain: $blockchain" } + val itemsResult = walletManager.getTransactionsHistory( address = walletManager.wallet.address, page = page, @@ -118,12 +126,12 @@ class DefaultWalletManagersFacade( private suspend fun getAndUpdateWalletManager( userWallet: UserWallet, blockchain: Blockchain, + derivationPath: String?, extraTokens: Set, ): UpdateWalletManagerResult { val scanResponse = userWallet.scanResponse - val derivationPath = blockchain.derivationPath(scanResponse.derivationStyleProvider.getDerivationStyle()) - if (derivationPath != null && !scanResponse.hasDerivation(blockchain, derivationPath.rawPath)) { + if (derivationPath != null && !scanResponse.hasDerivation(blockchain, derivationPath)) { Timber.e("Derivation missed for: $blockchain") return UpdateWalletManagerResult.MissedDerivation } @@ -171,21 +179,21 @@ class DefaultWalletManagersFacade( override suspend fun getOrCreateWalletManager( userWallet: UserWallet, blockchain: Blockchain, - derivationPath: DerivationPath?, + derivationPath: String?, ): WalletManager? { val userWalletId = userWallet.walletId var walletManager = walletManagersStore.getSyncOrNull( userWalletId = userWalletId, blockchain = blockchain, - derivationPath = derivationPath?.rawPath, + derivationPath = derivationPath, ) if (walletManager == null) { walletManager = walletManagerFactory.createWalletManager( scanResponse = userWallet.scanResponse, blockchain = blockchain, - derivationPath = derivationPath, + derivationPath = derivationPath?.let { DerivationPath(rawPath = it) }, ) ?: return null walletManagersStore.store(userWalletId, walletManager) diff --git a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/WalletManagersFacade.kt b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/WalletManagersFacade.kt index 4c296b01e4..bcb4409033 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/WalletManagersFacade.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/WalletManagersFacade.kt @@ -2,7 +2,6 @@ package com.tangem.domain.walletmanager import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.WalletManager -import com.tangem.crypto.hdWallet.DerivationPath import com.tangem.domain.tokens.models.CryptoCurrency import com.tangem.domain.tokens.models.Network import com.tangem.domain.txhistory.models.PaginationWrapper @@ -22,52 +21,53 @@ interface WalletManagersFacade { * Updates the wallet manager associated with a user's wallet and network. * * @param userWalletId The ID of the user's wallet. - * @param networkId The network ID. + * @param network The network. * @param extraTokens Additional tokens. * @return The result of updating the wallet manager. */ suspend fun update( userWalletId: UserWalletId, - networkId: Network.ID, + network: Network, extraTokens: Set, ): UpdateWalletManagerResult - suspend fun getExploreUrl(userWalletId: UserWalletId, networkId: Network.ID): String + /** + * Returns network explorer URL of the wallet manager associated with a user's wallet and network. + * + * @param userWalletId The ID of the user's wallet. + * @param network The network. + * + * @return The network explorer URL, maybe empty if the wallet manager was not found. + * */ + suspend fun getExploreUrl(userWalletId: UserWalletId, network: Network): String /** * Returns transactions count * * @param userWalletId The ID of the user's wallet. - * @param networkId The network ID. - * @param rawDerivationPath Derivation path in raw form. - + * @param network The network. */ - suspend fun getTxHistoryState( - userWalletId: UserWalletId, - networkId: Network.ID, - rawDerivationPath: String?, - ): TxHistoryState + suspend fun getTxHistoryState(userWalletId: UserWalletId, network: Network): TxHistoryState /** * Returns transaction history items wrapped to pagination * * @param userWalletId The ID of the user's wallet. - * @param networkId The network ID. - * @param rawDerivationPath Derivation path in raw form. + * @param network The network. * @param page Pagination page. * @param pageSize Pagination size. */ suspend fun getTxHistoryItems( userWalletId: UserWalletId, - networkId: Network.ID, - rawDerivationPath: String?, + network: Network, page: Int, pageSize: Int, ): PaginationWrapper + // TODO: Remove after refactoring suspend fun getOrCreateWalletManager( userWallet: UserWallet, blockchain: Blockchain, - derivationPath: DerivationPath?, + derivationPath: String?, ): WalletManager? } \ No newline at end of file diff --git a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/models/CryptoCurrency.kt b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/models/CryptoCurrency.kt index 562625c73d..94d8104980 100644 --- a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/models/CryptoCurrency.kt +++ b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/models/CryptoCurrency.kt @@ -11,8 +11,7 @@ import java.io.Serializable * @property symbol Symbol of the cryptocurrency. * @property decimals Number of decimal places used by the cryptocurrency. * @property iconUrl Optional URL of the cryptocurrency icon. `null` if not found. - * @property derivationPath Optional path used for key derivation. `null` if the wallet does not support the - * [HD Wallet](https://coinsutra.com/hd-wallets-deterministic-wallet/) feature. + * @property isCustom Indicates whether the currency is a custom user-added currency or not. */ // FIXME: Remove serialization [REDACTED_JIRA] sealed class CryptoCurrency : Serializable { @@ -23,7 +22,7 @@ sealed class CryptoCurrency : Serializable { abstract val symbol: String abstract val decimals: Int abstract val iconUrl: String? - abstract val derivationPath: String? + abstract val isCustom: Boolean /** * Represents a native coin in the blockchain network. @@ -35,7 +34,7 @@ sealed class CryptoCurrency : Serializable { override val symbol: String, override val decimals: Int, override val iconUrl: String?, - override val derivationPath: String?, + override val isCustom: Boolean, ) : CryptoCurrency() { init { @@ -47,7 +46,6 @@ sealed class CryptoCurrency : Serializable { * Represents a token in the blockchain network, typically a non-native asset. * * @property contractAddress Address of the contract managing the token. - * @property isCustom Indicates whether the token is a custom user-added token or not. */ data class Token( override val id: ID, @@ -56,9 +54,8 @@ sealed class CryptoCurrency : Serializable { override val symbol: String, override val decimals: Int, override val iconUrl: String?, - override val derivationPath: String?, + override val isCustom: Boolean, val contractAddress: String, - val isCustom: Boolean, ) : CryptoCurrency() { init { @@ -80,34 +77,70 @@ sealed class CryptoCurrency : Serializable { // FIXME: Remove serialization [REDACTED_JIRA] data class ID( private val prefix: Prefix, - private val networkId: Network.ID, + private val body: Body, private val suffix: Suffix, ) : Serializable { val value: String = buildString { append(prefix.value) - append(networkId.value) - append(DELIMITER) + append(PREFIX_DELIMITER) + append(body.value) + append(SUFFIX_DELIMITER) append(suffix.value) } + /** Represents a raw cryptocurrency ID. If it is a custom token, the value will be `null`. */ val rawCurrencyId: String? = (suffix as? Suffix.RawID)?.rawId - val rawNetworkId: String = networkId.value + /** Represents a raw cryptocurrency's network ID. */ + val rawNetworkId: String = when (body) { + is Body.NetworkId -> body.rawId + is Body.NetworkIdWithDerivationPath -> body.rawId + } /** * Represents the different types of prefixes that can be associated with a cryptocurrency ID. + * * These prefixes can help in quickly categorizing the type of cryptocurrency. */ enum class Prefix(val value: String) { /** Prefix for standard coins. */ - COIN_PREFIX(value = "coin_"), + COIN_PREFIX(value = "coin"), /** Prefix for standard tokens. */ - TOKEN_PREFIX(value = "token_"), + TOKEN_PREFIX(value = "token"), + } - /** Prefix for custom tokens. */ - CUSTOM_TOKEN_PREFIX(value = "custom_"), + /** + * Represents the body part of the cryptocurrency ID. + * + * The body can be either a raw network ID or a raw network ID with a network derivation path. + */ + sealed class Body { + + /** The value of the body. */ + abstract val value: String + + /** Represents a raw network ID. */ + data class NetworkId(val rawId: String) : Body() { + override val value: String = rawId + } + + /** + * Represents a raw network ID with a network derivation path. + * + * Should be used for a cryptocurrencies with custom derivation path. + * */ + data class NetworkIdWithDerivationPath( + val rawId: String, + val derivationPath: String, + ) : Body() { + override val value: String = buildString { + append(rawId) + append(DERIVATION_PATH_DELIMITER) + append(derivationPath.hashCode()) + } + } } /** @@ -132,8 +165,14 @@ sealed class CryptoCurrency : Serializable { } } + override fun toString(): String { + return "ID(value='$value')" + } + private companion object { - const val DELIMITER = '#' + const val PREFIX_DELIMITER = '_' + const val SUFFIX_DELIMITER = '#' + const val DERIVATION_PATH_DELIMITER = 'd' } } @@ -142,6 +181,5 @@ sealed class CryptoCurrency : Serializable { require(symbol.isNotBlank()) { "Crypto currency symbol must not be blank" } require(iconUrl?.isNotBlank() ?: true) { "Crypto currency icon URL must not be blank" } require(decimals >= 0) { "Crypto currency decimal must not be less then zero, but it is: $decimals" } - require(derivationPath?.isNotBlank() ?: true) { "Crypto currency derivation path must not be blank" } } } \ No newline at end of file diff --git a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/models/Network.kt b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/models/Network.kt index aaa1cb39d8..9c1091ac3e 100644 --- a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/models/Network.kt +++ b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/models/Network.kt @@ -11,6 +11,7 @@ import java.io.Serializable * * @property id The unique identifier of the network. * @property name The human-readable name of the network, such as "Ethereum" or "Bitcoin". + * @property derivationPath The path used to derive keys for this network. * @property isTestnet Indicates whether the network is a test network or a main network. * @property standardType The type of blockchain standard the network adheres to. */ @@ -18,6 +19,7 @@ import java.io.Serializable data class Network( val id: ID, val name: String, + val derivationPath: DerivationPath, val isTestnet: Boolean, val standardType: StandardType, ) : Serializable { @@ -39,6 +41,39 @@ data class Network( } } + /** + * Represents a path used to derive cryptographic keys for a blockchain network. + * + * This class represents such paths in a generic manner, allowing for predefined card-based paths, + * custom paths, or even no derivation path at all. + */ + sealed class DerivationPath { + + /** The actual derivation path value, if any. */ + abstract val value: String? + + /** + * Represents a predefined card-based derivation path. + * + * @property value The derivation path string. + */ + data class Card(override val value: String) : DerivationPath() + + /** + * Represents a custom derivation path specified by the user. + * + * @property value The derivation path string. + */ + data class Custom(override val value: String) : DerivationPath() + + /** + * Represents a lack of derivation path. + */ + object None : DerivationPath() { + override val value: String? = null + } + } + /** * Represents the type of blockchain standard that a network adheres to. * diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/FetchCurrencyStatusUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/FetchCurrencyStatusUseCase.kt index b10e3633c9..91f7e713a4 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/FetchCurrencyStatusUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/FetchCurrencyStatusUseCase.kt @@ -75,7 +75,7 @@ class FetchCurrencyStatusUseCase( refresh: Boolean, ) = coroutineScope { val fetchStatus = async { - fetchNetworkStatus(userWalletId, currency.network.id, refresh) + fetchNetworkStatus(userWalletId, currency.network, refresh) } val fetchQuote = async { fetchQuote(currency.id, refresh) @@ -101,11 +101,11 @@ class FetchCurrencyStatusUseCase( private suspend fun Raise.fetchNetworkStatus( userWalletId: UserWalletId, - networkId: Network.ID, + network: Network, refresh: Boolean, ) { catch( - block = { networksRepository.getNetworkStatusesSync(userWalletId, setOf(networkId), refresh) }, + block = { networksRepository.getNetworkStatusesSync(userWalletId, setOf(network), refresh) }, ) { raise(CurrencyStatusError.DataError(it)) } diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/FetchTokenListUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/FetchTokenListUseCase.kt index 7e379288b9..9dc971195a 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/FetchTokenListUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/FetchTokenListUseCase.kt @@ -48,7 +48,7 @@ class FetchTokenListUseCase( val fetchStatuses = async { fetchNetworksStatuses( userWalletId, - currencies.mapTo(hashSetOf()) { it.network.id }, + currencies.mapTo(hashSetOf()) { it.network }, refresh, ) } @@ -81,11 +81,11 @@ class FetchTokenListUseCase( private suspend fun Raise.fetchNetworksStatuses( userWalletId: UserWalletId, - networksIds: Set, + networks: Set, refresh: Boolean, ) { catch( - block = { networksRepository.getNetworkStatusesSync(userWalletId, networksIds, refresh) }, + block = { networksRepository.getNetworkStatusesSync(userWalletId, networks, refresh) }, ) { raise(TokenListError.DataError(it)) } diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/RemoveCurrencyUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/RemoveCurrencyUseCase.kt index dba5642239..e2a908df3b 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/RemoveCurrencyUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/RemoveCurrencyUseCase.kt @@ -27,10 +27,16 @@ class RemoveCurrencyUseCase( } suspend fun hasLinkedTokens(userWalletId: UserWalletId, currency: CryptoCurrency): Boolean { - val walletCurrencies = currenciesRepository - .getMultiCurrencyWalletCurrenciesSync(userWalletId = userWalletId, refresh = false) + return when (currency) { + is CryptoCurrency.Coin -> { + val walletCurrencies = currenciesRepository.getMultiCurrencyWalletCurrenciesSync( + userWalletId = userWalletId, + refresh = false, + ) - return currency is CryptoCurrency.Coin && - walletCurrencies.any { it != currency && it.network.id == currency.network.id } + walletCurrencies.any { it is CryptoCurrency.Token && it.network == currency.network } + } + is CryptoCurrency.Token -> false + } } } \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/CryptoCurrencyStatus.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/CryptoCurrencyStatus.kt index 5f4f6aa6d0..603ffb73f7 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/CryptoCurrencyStatus.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/CryptoCurrencyStatus.kt @@ -7,7 +7,7 @@ import java.math.BigDecimal /** * Represents the status of a cryptocurrency asset within a network. * - * This class encapsulates the details of a specific cryptocurrency, either a coin or token, + * This class encapsulates the details of a specific cryptocurrency, either a coin or cryptocurrency, * along with its current status within the blockchain network. The status can include various states * like Loading, Unreachable, Loaded, etc. * @@ -20,22 +20,22 @@ data class CryptoCurrencyStatus( ) { /** - * Represents the various states a token can have, encapsulating different information based on the state. + * Represents the various states a cryptocurrency can have, encapsulating different information based on the state. * * @property isError Indicates whether this status represents an error status. */ sealed class Status(val isError: Boolean) { - /** The amount of the token. */ + /** The amount of the cryptocurrency. */ open val amount: BigDecimal? = null - /** The fiat equivalent of the token's amount. */ + /** The fiat equivalent of the cryptocurrency's amount. */ open val fiatAmount: BigDecimal? = null - /** The exchange rate used for converting the token amount to fiat. */ + /** The exchange rate used for converting the cryptocurrency amount to fiat. */ open val fiatRate: BigDecimal? = null - /** The change in price of the token. */ + /** The change in price of the cryptocurrency. */ open val priceChange: BigDecimal? = null /** Indicates if there are any transactions in progress related to the cryptocurrency network. */ @@ -48,25 +48,28 @@ data class CryptoCurrencyStatus( open val networkAddress: NetworkAddress? = null } - /** Represents the Loading state of a token, typically while fetching its details. */ + /** Represents the Loading state of a cryptocurrency, typically while fetching its details. */ object Loading : Status(isError = false) - /** Represents a state where the token is not reachable. */ + /** Represents a state where the cryptocurrency is not reachable. */ object Unreachable : Status(isError = true) - /** Represents a state where the token's derivation is missed. */ + /** Represents a state where the cryptocurrency's network amount not found. */ + object NoAmount : Status(isError = true) + + /** Represents a state where the cryptocurrency's derivation is missed. */ object MissedDerivation : Status(isError = true) - /** Represents a state where there is no account associated with the token. */ + /** Represents a state where there is no account associated with the cryptocurrency. */ object NoAccount : Status(isError = false) /** - * Represents a Loaded state of a token with complete information. + * Represents a Loaded state of a cryptocurrency with complete information. * - * @property amount The amount of the token. - * @property fiatAmount The fiat equivalent of the token's amount. - * @property fiatRate The exchange rate used for converting the token amount to fiat. - * @property priceChange The change in price of the token. + * @property amount The amount of the cryptocurrency. + * @property fiatAmount The fiat equivalent of the cryptocurrency's amount. + * @property fiatRate The exchange rate used for converting the cryptocurrency amount to fiat. + * @property priceChange The change in price of the cryptocurrency. * @property hasCurrentNetworkTransactions Indicates if there are any transactions in progress related to the * cryptocurrency network. * @property pendingTransactions The current cryptocurrency transactions. @@ -82,12 +85,12 @@ data class CryptoCurrencyStatus( ) : Status(isError = false) /** - * Represents a Custom state of a token, typically used for user-defined tokens. + * Represents a Custom state of a cryptocurrency, typically used for user-defined tokens. * - * @property amount The amount of the token. - * @property fiatAmount The fiat equivalent of the token's amount (optional). - * @property fiatRate The exchange rate used for converting the token amount to fiat (optional). - * @property priceChange The change in price of the token (optional). + * @property amount The amount of the cryptocurrency. + * @property fiatAmount The fiat equivalent of the cryptocurrency's amount (optional). + * @property fiatRate The exchange rate used for converting the cryptocurrency amount to fiat (optional). + * @property priceChange The change in price of the cryptocurrency (optional). * @property hasCurrentNetworkTransactions Indicates if there are any transactions in progress related to the * cryptocurrency network. * @property pendingTransactions The current cryptocurrency transactions. @@ -103,9 +106,9 @@ data class CryptoCurrencyStatus( ) : Status(isError = false) /** - * Represents a state where the token is available, but there is no current quote available for it. + * Represents a state where the cryptocurrency is available, but there is no current quote available for it. * - * @property amount The amount of the token. + * @property amount The amount of the cryptocurrency. * @property hasCurrentNetworkTransactions Indicates if there are any transactions in progress related to the * cryptocurrency network. * @property pendingTransactions The current cryptocurrency transactions. diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/NetworkStatus.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/NetworkStatus.kt index 87be054645..ef1c54b04f 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/NetworkStatus.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/NetworkStatus.kt @@ -8,11 +8,11 @@ import java.math.BigDecimal /** * Represents the status of a specific blockchain network. * - * @property networkId The unique identifier of the network for which the status is provided. + * @property network The network for which the status is provided. * @property value The specific status value, represented as a sealed class to encapsulate the various possible states of the network. */ data class NetworkStatus( - val networkId: Network.ID, + val network: Network, val value: Status, ) { diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesOperations.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesOperations.kt index e72a1769bb..07a72dbee4 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesOperations.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesOperations.kt @@ -58,11 +58,11 @@ internal class CurrenciesStatusesOperations( emit(maybeLoadingCurrenciesStatuses) - val (networksIds, currenciesIds) = getIds(nonEmptyCurrencies) + val (networks, currenciesIds) = getIds(nonEmptyCurrencies) val currenciesFlow = combine( getQuotes(currenciesIds), - getNetworksStatuses(networksIds), + getNetworksStatuses(networks), ) { maybeQuotes, maybeNetworksStatuses -> createCurrenciesStatuses(nonEmptyCurrencies, maybeQuotes, maybeNetworksStatuses) } @@ -99,7 +99,7 @@ internal class CurrenciesStatusesOperations( } private fun getCurrencyStatusFlow(currency: CryptoCurrency): Flow> { - val (networksIds, currenciesIds) = getIds(nonEmptyListOf(currency)) + val (networks, currenciesIds) = getIds(nonEmptyListOf(currency)) val quoteFlow = getQuotes(currenciesIds) .map { maybeQuotes -> @@ -108,15 +108,15 @@ internal class CurrenciesStatusesOperations( } } - val statusFlow = getNetworksStatuses(networksIds) + val statusFlow = getNetworksStatuses(networks) .map { maybeStatuses -> maybeStatuses.map { statuses -> - statuses.singleOrNull { it.networkId == currency.network.id } + statuses.singleOrNull { it.network == currency.network } } } return combine(quoteFlow, statusFlow) { maybeQuote, maybeNetworkStatus -> - createStatus(currency, maybeQuote, maybeNetworkStatus) + createCurrencyStatus(currency, maybeQuote, maybeNetworkStatus) } } @@ -135,13 +135,13 @@ internal class CurrenciesStatusesOperations( currencies.map { currency -> val quote = quotes?.firstOrNull { it.rawCurrencyId == currency.id.rawCurrencyId } - val networkStatus = networksStatuses?.firstOrNull { it.networkId == currency.network.id } + val networkStatus = networksStatuses?.firstOrNull { it.network == currency.network } - createStatus(currency, quote, networkStatus, ignoreQuote = quotesRetrievingFailed) + createCurrencyStatus(currency, quote, networkStatus, ignoreQuote = quotesRetrievingFailed) } } - private fun createStatus( + private fun createCurrencyStatus( currency: CryptoCurrency, maybeQuote: Either, maybeNetworkStatus: Either, @@ -154,10 +154,10 @@ internal class CurrenciesStatusesOperations( null } - createStatus(currency, quote, networkStatus, ignoreQuote = quoteRetrievingFailed) + createCurrencyStatus(currency, quote, networkStatus, ignoreQuote = quoteRetrievingFailed) } - private fun createStatus( + private fun createCurrencyStatus( currency: CryptoCurrency, quote: Quote?, networkStatus: NetworkStatus?, @@ -206,7 +206,7 @@ internal class CurrenciesStatusesOperations( .onEmpty { emit(Error.EmptyQuotes.left()) } } - private fun getNetworksStatuses(networks: NonEmptySet): Flow>> { + private fun getNetworksStatuses(networks: NonEmptySet): Flow>> { return networksRepository.getNetworkStatusesUpdates(userWalletId, networks) .map, Either>> { it.right() } .catch { emit(Error.DataError(it).left()) } @@ -215,17 +215,17 @@ internal class CurrenciesStatusesOperations( private fun getIds( currencies: NonEmptyList, - ): Pair, NonEmptySet> { + ): Pair, NonEmptySet> { val currencyIdToNetworkId = currencies.associate { currency -> - currency.id to currency.network.id + currency.id to currency.network } val currenciesIds = currencyIdToNetworkId.keys.toNonEmptySetOrNull() - val networksIds = currencyIdToNetworkId.values.toNonEmptySetOrNull() + val networks = currencyIdToNetworkId.values.toNonEmptySetOrNull() requireNotNull(currenciesIds) { "Currencies IDs cannot be empty" } - requireNotNull(networksIds) { "Networks IDs cannot be empty" } + requireNotNull(networks) { "Networks IDs cannot be empty" } - return networksIds to currenciesIds + return networks to currenciesIds } sealed class Error { diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrencyStatusOperations.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrencyStatusOperations.kt index 2082010b8d..4e043eaa52 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrencyStatusOperations.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrencyStatusOperations.kt @@ -26,7 +26,7 @@ internal class CurrencyStatusOperations( } private fun createStatus(status: NetworkStatus.Verified): CryptoCurrencyStatus.Status { - val amount = status.amounts[currency.id] ?: return CryptoCurrencyStatus.Unreachable + val amount = status.amounts[currency.id] ?: return CryptoCurrencyStatus.NoAmount val hasCurrentNetworkTransactions = status.pendingTransactions.isNotEmpty() val currentTransactions = status.pendingTransactions.getOrElse(currency.id, ::emptySet) diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TokenListFiatBalanceOperations.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TokenListFiatBalanceOperations.kt index ec63a9fa0c..326fceb11f 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TokenListFiatBalanceOperations.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TokenListFiatBalanceOperations.kt @@ -22,6 +22,7 @@ internal class TokenListFiatBalanceOperations( } is CryptoCurrencyStatus.MissedDerivation, is CryptoCurrencyStatus.Unreachable, + is CryptoCurrencyStatus.NoAmount, -> { fiatBalance = TokenList.FiatBalance.Failed break diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/NetworksRepository.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/NetworksRepository.kt index b019c7f3f5..a69acb7bec 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/NetworksRepository.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/NetworksRepository.kt @@ -9,25 +9,16 @@ import kotlinx.coroutines.flow.Flow * Repository for everything related to the blockchain networks * */ interface NetworksRepository { - - /** - * Retrieves the details of the specified blockchain networks, identified by their unique IDs. - * - * @param networksIds The unique identifiers of the networks to be retrieved. - * @return A set of [Network] objects corresponding to the specified network IDs. - */ - fun getNetworks(networksIds: Set): Set - /** * Retrieves updates of network statuses of specified blockchain networks for a specific user wallet. * * Loads remote network statuses if they have expired. * * @param userWalletId The unique identifier of the user wallet. - * @param networks A set of network IDs which statuses are to be retrieved. + * @param networks A set of network which statuses are to be retrieved. * @return A [Flow] emitting a set of [NetworkStatus] objects corresponding to the specified networks. */ - fun getNetworkStatusesUpdates(userWalletId: UserWalletId, networks: Set): Flow> + fun getNetworkStatusesUpdates(userWalletId: UserWalletId, networks: Set): Flow> /** * Retrieves network statuses of specified blockchain networks for a specific user wallet. @@ -35,13 +26,13 @@ interface NetworksRepository { * Loads remote network statuses if they have expired or if [refresh] is `true`. * * @param userWalletId The unique identifier of the user wallet. - * @param networks A set of network IDs which statuses are to be retrieved. + * @param networks A set of network which statuses are to be retrieved. * @param refresh A boolean flag indicating whether the data should be refreshed. * @return A [Flow] emitting a set of [NetworkStatus] objects corresponding to the specified networks. */ suspend fun getNetworkStatusesSync( userWalletId: UserWalletId, - networks: Set, + networks: Set, refresh: Boolean, ): Set } \ No newline at end of file diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/GetPrimaryCurrencyStatusUpdatesUseCaseTest.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/GetPrimaryCurrencyStatusUpdatesUseCaseTest.kt index 41b1dda67b..a1f9e62bf9 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/GetPrimaryCurrencyStatusUpdatesUseCaseTest.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/GetPrimaryCurrencyStatusUpdatesUseCaseTest.kt @@ -164,6 +164,6 @@ internal class GetPrimaryCurrencyStatusUpdatesUseCaseTest { isSortedByBalance = flowOf(), ), quotesRepository = MockQuotesRepository(quotes), - networksRepository = MockNetworksRepository(MockNetworks.networks.right(), statuses), + networksRepository = MockNetworksRepository(statuses), ) } \ No newline at end of file diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/GetTokenListUseCaseTest.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/GetTokenListUseCaseTest.kt index b3a5e61a2e..0affccbec6 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/GetTokenListUseCaseTest.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/GetTokenListUseCaseTest.kt @@ -314,6 +314,6 @@ internal class GetTokenListUseCaseTest { isSortedByBalance = isSortedByBalance, ), quotesRepository = MockQuotesRepository(quotes), - networksRepository = MockNetworksRepository(MockNetworks.networks.right(), statuses), + networksRepository = MockNetworksRepository(statuses), ) } \ No newline at end of file diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockNetworks.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockNetworks.kt index 831d0c308b..f29e8b3a74 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockNetworks.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockNetworks.kt @@ -17,6 +17,7 @@ internal object MockNetworks { name = "Network One", isTestnet = false, standardType = Network.StandardType.ERC20, + derivationPath = Network.DerivationPath.None, ) val network2 = Network( @@ -24,6 +25,7 @@ internal object MockNetworks { name = "Network Two", isTestnet = false, standardType = Network.StandardType.ERC20, + derivationPath = Network.DerivationPath.None, ) val network3 = Network( @@ -31,22 +33,21 @@ internal object MockNetworks { name = "Network Three", isTestnet = false, standardType = Network.StandardType.ERC20, + derivationPath = Network.DerivationPath.None, ) - val networks = nonEmptySetOf(network1, network2, network3) - val networkStatus1 = NetworkStatus( - networkId = network1.id, + network = network1, value = NetworkStatus.Unreachable, ) val networkStatus2 = NetworkStatus( - networkId = network2.id, + network = network2, value = NetworkStatus.MissedDerivation, ) val networkStatus3 = NetworkStatus( - networkId = network3.id, + network = network3, value = NetworkStatus.NoAccount( amountToCreateAccount = amountToCreateAccount, address = NetworkAddress.Single(defaultAddress = "mock"), diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockTokens.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockTokens.kt index fda72a1b06..7a045b5662 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockTokens.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockTokens.kt @@ -7,17 +7,25 @@ internal object MockTokens { val token1 get() = CryptoCurrency.Coin( - id = ID(ID.Prefix.COIN_PREFIX, MockNetworks.network1.id, ID.Suffix.RawID("token1")), + id = ID( + ID.Prefix.COIN_PREFIX, + ID.Body.NetworkId(MockNetworks.network1.id.value), + ID.Suffix.RawID("token1"), + ), network = MockNetworks.network1, name = "Token 1", symbol = "T1", decimals = 8, iconUrl = null, - derivationPath = null, + isCustom = false, ) val token2 get() = CryptoCurrency.Token( - id = ID(ID.Prefix.TOKEN_PREFIX, MockNetworks.network1.id, ID.Suffix.RawID("token2")), + id = ID( + ID.Prefix.TOKEN_PREFIX, + ID.Body.NetworkId(MockNetworks.network1.id.value), + ID.Suffix.RawID("token2"), + ), network = MockNetworks.network1, name = "Token 2", symbol = "T2", @@ -25,11 +33,14 @@ internal object MockTokens { decimals = 8, iconUrl = null, contractAddress = "address", - derivationPath = null, ) val token3 get() = CryptoCurrency.Token( - id = ID(ID.Prefix.TOKEN_PREFIX, MockNetworks.network1.id, ID.Suffix.RawID("token3")), + id = ID( + ID.Prefix.TOKEN_PREFIX, + ID.Body.NetworkId(MockNetworks.network1.id.value), + ID.Suffix.RawID("token3"), + ), network = MockNetworks.network1, name = "Token 3", symbol = "T3", @@ -37,21 +48,28 @@ internal object MockTokens { decimals = 8, iconUrl = null, contractAddress = "address", - derivationPath = null, ) val token4 get() = CryptoCurrency.Coin( - id = ID(ID.Prefix.COIN_PREFIX, MockNetworks.network2.id, ID.Suffix.RawID("token4")), + id = ID( + ID.Prefix.COIN_PREFIX, + ID.Body.NetworkId(MockNetworks.network2.id.value), + ID.Suffix.RawID("token4"), + ), network = MockNetworks.network2, name = "Token 4", symbol = "T4", decimals = 8, iconUrl = null, - derivationPath = null, + isCustom = false, ) val token5 get() = CryptoCurrency.Token( - id = ID(ID.Prefix.TOKEN_PREFIX, MockNetworks.network2.id, ID.Suffix.RawID("token5")), + id = ID( + ID.Prefix.TOKEN_PREFIX, + ID.Body.NetworkId(MockNetworks.network2.id.value), + ID.Suffix.RawID("token5"), + ), network = MockNetworks.network2, name = "Token 5", symbol = "T5", @@ -59,11 +77,14 @@ internal object MockTokens { decimals = 8, iconUrl = null, contractAddress = "address", - derivationPath = null, ) val token6 get() = CryptoCurrency.Token( - id = ID(ID.Prefix.TOKEN_PREFIX, MockNetworks.network2.id, ID.Suffix.RawID("token6")), + id = ID( + ID.Prefix.TOKEN_PREFIX, + ID.Body.NetworkId(MockNetworks.network2.id.value), + ID.Suffix.RawID("token6"), + ), network = MockNetworks.network2, name = "Token 6", symbol = "T6", @@ -71,21 +92,28 @@ internal object MockTokens { decimals = 8, iconUrl = null, contractAddress = "address", - derivationPath = null, ) val token7 get() = CryptoCurrency.Coin( - id = ID(ID.Prefix.COIN_PREFIX, MockNetworks.network3.id, ID.Suffix.RawID("token7")), + id = ID( + ID.Prefix.COIN_PREFIX, + ID.Body.NetworkId(MockNetworks.network3.id.value), + ID.Suffix.RawID("token7"), + ), network = MockNetworks.network3, name = "Token 7", symbol = "T7", decimals = 8, iconUrl = null, - derivationPath = null, + isCustom = false, ) val token8 get() = CryptoCurrency.Token( - id = ID(ID.Prefix.TOKEN_PREFIX, MockNetworks.network3.id, ID.Suffix.RawID("token8")), + id = ID( + ID.Prefix.TOKEN_PREFIX, + ID.Body.NetworkId(MockNetworks.network3.id.value), + ID.Suffix.RawID("token8"), + ), network = MockNetworks.network3, name = "Token 8", symbol = "T8", @@ -93,11 +121,14 @@ internal object MockTokens { decimals = 8, iconUrl = null, contractAddress = "address", - derivationPath = null, ) val token9 get() = CryptoCurrency.Token( - id = ID(ID.Prefix.TOKEN_PREFIX, MockNetworks.network3.id, ID.Suffix.RawID("token9")), + id = ID( + ID.Prefix.TOKEN_PREFIX, + ID.Body.NetworkId(MockNetworks.network3.id.value), + ID.Suffix.RawID("token9"), + ), network = MockNetworks.network3, name = "Token 9", symbol = "T9", @@ -105,11 +136,14 @@ internal object MockTokens { decimals = 8, iconUrl = null, contractAddress = "address", - derivationPath = null, ) val token10 get() = CryptoCurrency.Token( - id = ID(ID.Prefix.TOKEN_PREFIX, MockNetworks.network3.id, ID.Suffix.RawID("token10")), + id = ID( + ID.Prefix.TOKEN_PREFIX, + ID.Body.NetworkId(MockNetworks.network3.id.value), + ID.Suffix.RawID("token10"), + ), network = MockNetworks.network3, name = "Token 10", symbol = "T10", @@ -117,7 +151,6 @@ internal object MockTokens { decimals = 8, iconUrl = null, contractAddress = "address", - derivationPath = null, ) val tokens = listOf(token1, token2, token3, token4, token5, token6, token7, token8, token9, token10) diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockTokensStates.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockTokensStates.kt index d01254b1e7..4dd52cf2a3 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockTokensStates.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockTokensStates.kt @@ -72,7 +72,7 @@ internal object MockTokensStates { val loadedTokensStates = failedTokenStates.map { status -> val networkStatus = MockNetworks.verifiedNetworksStatuses - .first { it.networkId == status.currency.network.id } + .first { it.network == status.currency.network } val amount = (networkStatus.value as NetworkStatus.Verified).amounts[status.currency.id]!! val quote = MockQuotes.quotes.first { it.rawCurrencyId == status.currency.id.rawCurrencyId } val fiatAmount = amount * quote.fiatRate @@ -98,7 +98,7 @@ internal object MockTokensStates { hasCurrentNetworkTransactions = false, networkAddress = requireNotNull( value = MockNetworks.verifiedNetworksStatuses - .first { it.networkId == status.currency.network.id } + .first { it.network == status.currency.network } .value as? NetworkStatus.Verified, ).address, ), diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockNetworksRepository.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockNetworksRepository.kt index 0209a5e953..6d5026250c 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockNetworksRepository.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockNetworksRepository.kt @@ -11,24 +11,19 @@ import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.map internal class MockNetworksRepository( - private val networks: Either>, private val statuses: Flow>>, ) : NetworksRepository { - override fun getNetworks(networksIds: Set): Set { - return networks.getOrElse { throw it } - } - override fun getNetworkStatusesUpdates( userWalletId: UserWalletId, - networks: Set, + networks: Set, ): Flow> { return statuses.map { it.getOrElse { e -> throw e } } } override suspend fun getNetworkStatusesSync( userWalletId: UserWalletId, - networks: Set, + networks: Set, refresh: Boolean, ): Set { return getNetworkStatusesUpdates(userWalletId, networks).first() diff --git a/domain/txhistory/src/main/kotlin/com/tangem/domain/txhistory/repository/TxHistoryRepository.kt b/domain/txhistory/src/main/kotlin/com/tangem/domain/txhistory/repository/TxHistoryRepository.kt index 8ff4c05cff..b912b2111e 100644 --- a/domain/txhistory/src/main/kotlin/com/tangem/domain/txhistory/repository/TxHistoryRepository.kt +++ b/domain/txhistory/src/main/kotlin/com/tangem/domain/txhistory/repository/TxHistoryRepository.kt @@ -2,20 +2,16 @@ package com.tangem.domain.txhistory.repository import androidx.paging.PagingData import com.tangem.domain.tokens.models.Network +import com.tangem.domain.txhistory.models.TxHistoryItem import com.tangem.domain.txhistory.models.TxHistoryListError import com.tangem.domain.txhistory.models.TxHistoryStateError -import com.tangem.domain.txhistory.models.TxHistoryItem import kotlinx.coroutines.flow.Flow interface TxHistoryRepository { @Throws(TxHistoryStateError::class) - suspend fun getTxHistoryItemsCount(networkId: Network.ID, derivationPath: String?): Int + suspend fun getTxHistoryItemsCount(network: Network): Int @Throws(TxHistoryListError::class) - fun getTxHistoryItems( - networkId: Network.ID, - derivationPath: String?, - pageSize: Int, - ): Flow> + fun getTxHistoryItems(network: Network, pageSize: Int): Flow> } \ No newline at end of file diff --git a/domain/txhistory/src/main/kotlin/com/tangem/domain/txhistory/usecase/GetTxHistoryItemsCountUseCase.kt b/domain/txhistory/src/main/kotlin/com/tangem/domain/txhistory/usecase/GetTxHistoryItemsCountUseCase.kt index 172c8c2b3f..df9a8013cc 100644 --- a/domain/txhistory/src/main/kotlin/com/tangem/domain/txhistory/usecase/GetTxHistoryItemsCountUseCase.kt +++ b/domain/txhistory/src/main/kotlin/com/tangem/domain/txhistory/usecase/GetTxHistoryItemsCountUseCase.kt @@ -9,10 +9,10 @@ import com.tangem.domain.txhistory.repository.TxHistoryRepository class GetTxHistoryItemsCountUseCase(private val repository: TxHistoryRepository) { - suspend operator fun invoke(networkId: Network.ID, derivationPath: String?): Either { + suspend operator fun invoke(network: Network): Either { return either { catch( - block = { repository.getTxHistoryItemsCount(networkId, derivationPath) }, + block = { repository.getTxHistoryItemsCount(network) }, catch = { throwable -> raise( when (throwable) { diff --git a/domain/txhistory/src/main/kotlin/com/tangem/domain/txhistory/usecase/GetTxHistoryItemsUseCase.kt b/domain/txhistory/src/main/kotlin/com/tangem/domain/txhistory/usecase/GetTxHistoryItemsUseCase.kt index 38fbf4b87e..77ef74e98a 100644 --- a/domain/txhistory/src/main/kotlin/com/tangem/domain/txhistory/usecase/GetTxHistoryItemsUseCase.kt +++ b/domain/txhistory/src/main/kotlin/com/tangem/domain/txhistory/usecase/GetTxHistoryItemsUseCase.kt @@ -4,8 +4,8 @@ import androidx.paging.PagingData import arrow.core.Either import arrow.core.raise.either import com.tangem.domain.tokens.models.Network -import com.tangem.domain.txhistory.models.TxHistoryListError import com.tangem.domain.txhistory.models.TxHistoryItem +import com.tangem.domain.txhistory.models.TxHistoryListError import com.tangem.domain.txhistory.repository.TxHistoryRepository import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.catch @@ -15,13 +15,12 @@ private const val DEFAULT_PAGE_SIZE = 20 class GetTxHistoryItemsUseCase(private val repository: TxHistoryRepository) { operator fun invoke( - networkId: Network.ID, - derivationPath: String?, + network: Network, pageSize: Int = DEFAULT_PAGE_SIZE, ): Either>> { return either { repository - .getTxHistoryItems(networkId = networkId, derivationPath = derivationPath, pageSize = pageSize) + .getTxHistoryItems(network = network, pageSize = pageSize) .catch { raise(TxHistoryListError.DataError(it)) } } } diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetExploreUrlUseCase.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetExploreUrlUseCase.kt index 564192c4f8..f255563e7f 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetExploreUrlUseCase.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetExploreUrlUseCase.kt @@ -1,12 +1,17 @@ package com.tangem.domain.wallets.usecase +import arrow.core.raise.catch import com.tangem.domain.tokens.models.Network import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.models.UserWalletId +// TODO: Add tests class GetExploreUrlUseCase(private val walletsManagersFacade: WalletManagersFacade) { - suspend operator fun invoke(userWalletId: UserWalletId, networkId: Network.ID): String { - return walletsManagersFacade.getExploreUrl(userWalletId, networkId) + // FIXME: Handle error + suspend operator fun invoke(userWalletId: UserWalletId, network: Network): String { + return catch({ walletsManagersFacade.getExploreUrl(userWalletId, network) }) { + "" + } } } \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsLoadedBalanceConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsLoadedBalanceConverter.kt index 1d09bb6fae..2199b485c3 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsLoadedBalanceConverter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsLoadedBalanceConverter.kt @@ -65,6 +65,7 @@ internal class TokenDetailsLoadedBalanceConverter( is CryptoCurrencyStatus.MissedDerivation, is CryptoCurrencyStatus.NoAccount, is CryptoCurrencyStatus.Custom, + is CryptoCurrencyStatus.NoAmount, // TODO: [REDACTED_JIRA] is CryptoCurrencyStatus.Unreachable, -> { @@ -89,6 +90,7 @@ internal class TokenDetailsLoadedBalanceConverter( is CryptoCurrencyStatus.Custom, is CryptoCurrencyStatus.MissedDerivation, is CryptoCurrencyStatus.NoAccount, + is CryptoCurrencyStatus.NoAmount, is CryptoCurrencyStatus.Unreachable, -> MarketPriceBlockState.Error(currencyName) } diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt index 0af0c850bd..599c54b7c1 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt @@ -118,8 +118,7 @@ internal class TokenDetailsViewModel @Inject constructor( private fun updateTxHistory(refresh: Boolean = false) { viewModelScope.launch(dispatchers.io) { val txHistoryItemsCountEither = txHistoryItemsCountUseCase( - networkId = cryptoCurrency.network.id, - derivationPath = cryptoCurrency.derivationPath, + network = cryptoCurrency.network, ) if (!refresh) { @@ -129,8 +128,7 @@ internal class TokenDetailsViewModel @Inject constructor( txHistoryItemsCountEither.onRight { uiState = stateFactory.getLoadedTxHistoryState( txHistoryEither = txHistoryItemsUseCase( - networkId = cryptoCurrency.network.id, - derivationPath = cryptoCurrency.derivationPath, + network = cryptoCurrency.network, ).map { it.cachedIn(viewModelScope) }, @@ -254,7 +252,7 @@ internal class TokenDetailsViewModel @Inject constructor( router.openUrl( url = getExploreUrlUseCase( userWalletId = wallet.walletId, - networkId = cryptoCurrency.network.id, + network = cryptoCurrency.network, ), ) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/WalletPreviewData.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/WalletPreviewData.kt index 307db0c04c..91cc615b96 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/WalletPreviewData.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/WalletPreviewData.kt @@ -206,7 +206,7 @@ internal object WalletPreviewData { val networkNumber = index + 1 val group = DraggableItem.GroupHeader( - id = "group_$networkNumber", + id = networkNumber, networkName = "$networkNumber", roundingMode = when (index) { 0 -> DraggableItem.RoundingMode.Top() @@ -328,7 +328,7 @@ internal object WalletPreviewData { walletsListConfig = walletListConfig, tokensListState = WalletTokensListState.Content( persistentListOf( - TokensListItemState.NetworkGroupTitle(TextReference.Str("Bitcoin")), + TokensListItemState.NetworkGroupTitle(id = 0, stringReference("Bitcoin")), TokensListItemState.Token( tokenItemVisibleState.copy( id = "token_1", @@ -357,7 +357,7 @@ internal object WalletPreviewData { amount = "1,89340821 ETH", ), ), - TokensListItemState.NetworkGroupTitle(TextReference.Str("Ethereum")), + TokensListItemState.NetworkGroupTitle(id = 1, stringReference("Ethereum")), TokensListItemState.Token( tokenItemVisibleState.copy( id = "token_5", diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/model/DraggableItem.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/model/DraggableItem.kt index 3424a965b6..f7c411a377 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/model/DraggableItem.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/model/DraggableItem.kt @@ -13,7 +13,7 @@ import com.tangem.feature.wallet.presentation.organizetokens.model.DraggableItem * */ @Immutable internal sealed class DraggableItem { - abstract val id: String + abstract val id: Any abstract val roundingMode: RoundingMode abstract val showShadow: Boolean @@ -26,7 +26,7 @@ internal sealed class DraggableItem { * @property showShadow if true then item should be elevated * */ data class GroupHeader( - override val id: String, + override val id: Int, val networkName: String, override val roundingMode: RoundingMode = RoundingMode.None, override val showShadow: Boolean = false, @@ -43,7 +43,7 @@ internal sealed class DraggableItem { * */ data class Token( val tokenItemState: TokenItemState.Draggable, - val groupId: String, + val groupId: Int, override val showShadow: Boolean = false, override val roundingMode: RoundingMode = RoundingMode.None, ) : DraggableItem() { diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/common/IdsOperations.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/common/IdsOperations.kt index e28e32c3a8..5cf4e81e8e 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/common/IdsOperations.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/common/IdsOperations.kt @@ -5,4 +5,4 @@ import com.tangem.domain.tokens.models.Network internal fun getTokenItemId(currencyId: CryptoCurrency.ID): String = currencyId.value -internal fun getGroupHeaderId(networkId: Network.ID): String = networkId.value \ No newline at end of file +internal fun getGroupHeaderId(network: Network): Int = network.hashCode() \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/items/CryptoCurrencyToDraggableItemConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/items/CryptoCurrencyToDraggableItemConverter.kt index 8dad69b926..a6c36995b0 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/items/CryptoCurrencyToDraggableItemConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/items/CryptoCurrencyToDraggableItemConverter.kt @@ -36,7 +36,7 @@ internal class CryptoCurrencyToDraggableItemConverter( ): DraggableItem.Token { return DraggableItem.Token( tokenItemState = createTokenItemState(currencyStatus, appCurrency), - groupId = getGroupHeaderId(currencyStatus.currency.network.id), + groupId = getGroupHeaderId(currencyStatus.currency.network), ) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/items/NetworkGroupToDraggableItemsConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/items/NetworkGroupToDraggableItemsConverter.kt index e57076e83f..0205df476a 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/items/NetworkGroupToDraggableItemsConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/items/NetworkGroupToDraggableItemsConverter.kt @@ -31,7 +31,7 @@ internal class NetworkGroupToDraggableItemsConverter( } private fun createGroupHeader(group: NetworkGroup) = DraggableItem.GroupHeader( - id = getGroupHeaderId(group.network.id), + id = getGroupHeaderId(group.network), networkName = group.network.name, ) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/dnd/DraggableGroupsOperations.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/dnd/DraggableGroupsOperations.kt index 3018d416a5..3891fdea66 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/dnd/DraggableGroupsOperations.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/dnd/DraggableGroupsOperations.kt @@ -7,7 +7,7 @@ import com.tangem.feature.wallet.presentation.organizetokens.utils.common.uniteI internal class DraggableGroupsOperations { - private var groupIdToTokens: Map>? = null + private var groupIdToTokens: Map>? = null fun collapseGroup(items: List, movingGroup: DraggableItem.GroupHeader): List { if (!groupIdToTokens.isNullOrEmpty()) return items diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/components/WalletTokensListState.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/components/WalletTokensListState.kt index 0cb0188b10..b81f513cc0 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/components/WalletTokensListState.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/components/WalletTokensListState.kt @@ -53,7 +53,7 @@ internal sealed class WalletTokensListState { /** Locked content state */ object Locked : ContentState( items = persistentListOf( - TokensListItemState.NetworkGroupTitle(value = TextReference.Res(id = R.string.main_tokens)), + TokensListItemState.NetworkGroupTitle(id = 42, name = TextReference.Res(id = R.string.main_tokens)), TokensListItemState.Token(state = TokenItemState.Locked(id = LOCKED_TOKEN_ID)), ), organizeTokensButton = OrganizeTokensButtonState.Hidden, @@ -84,19 +84,26 @@ internal sealed class WalletTokensListState { @Immutable sealed interface TokensListItemState { + val id: Any + /** * Network group title item * - * @property value network name + * @property name network name */ - data class NetworkGroupTitle(val value: TextReference) : TokensListItemState + data class NetworkGroupTitle( + override val id: Int, + val name: TextReference, + ) : TokensListItemState /** * Token item * * @property state token item state */ - data class Token(val state: TokenItemState) : TokensListItemState + data class Token(val state: TokenItemState) : TokensListItemState { + override val id: String = state.id + } } private companion object { diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletSingleCurrencyLoadedBalanceConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletSingleCurrencyLoadedBalanceConverter.kt index b67b7cedb8..6028b4aff8 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletSingleCurrencyLoadedBalanceConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletSingleCurrencyLoadedBalanceConverter.kt @@ -71,6 +71,7 @@ internal class WalletSingleCurrencyLoadedBalanceConverter( is CryptoCurrencyStatus.MissedDerivation, is CryptoCurrencyStatus.NoAccount, is CryptoCurrencyStatus.Unreachable, + is CryptoCurrencyStatus.NoAmount, -> MarketPriceBlockState.Error(currencyName) } } @@ -112,6 +113,7 @@ internal class WalletSingleCurrencyLoadedBalanceConverter( is CryptoCurrencyStatus.NoAccount, is CryptoCurrencyStatus.Custom, is CryptoCurrencyStatus.Unreachable, + is CryptoCurrencyStatus.NoAmount, -> { WalletCardState.Error( id = selectedWallet.id, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/multicurrency/MultiCurrencyContent.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/multicurrency/MultiCurrencyContent.kt index 350cb95ecf..1330515bf4 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/multicurrency/MultiCurrencyContent.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/multicurrency/MultiCurrencyContent.kt @@ -44,12 +44,7 @@ private fun LazyListScope.contentItems( ) { itemsIndexed( items = items, - key = { _, item -> - when (item) { - is WalletTokensListState.TokensListItemState.NetworkGroupTitle -> item.value.hashCode() - is WalletTokensListState.TokensListItemState.Token -> item.state.id - } - }, + key = { _, item -> item.id }, contentType = { _, item -> item::class.java }, itemContent = { index, item -> MultiCurrencyContentItem( diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/multicurrency/MultiCurrencyContentItem.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/multicurrency/MultiCurrencyContentItem.kt index a9ac43d4ab..882548c609 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/multicurrency/MultiCurrencyContentItem.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/multicurrency/MultiCurrencyContentItem.kt @@ -19,7 +19,7 @@ import com.tangem.feature.wallet.presentation.wallet.state.components.WalletToke internal fun MultiCurrencyContentItem(state: WalletTokensListState.TokensListItemState, modifier: Modifier = Modifier) { when (state) { is WalletTokensListState.TokensListItemState.NetworkGroupTitle -> { - NetworkGroupItem(networkName = state.value.resolveReference(), modifier = modifier) + NetworkGroupItem(networkName = state.name.resolveReference(), modifier = modifier) } is WalletTokensListState.TokensListItemState.Token -> { TokenItem(state = state.state, modifier = modifier) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/CryptoCurrencyStatusToTokenItemConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/CryptoCurrencyStatusToTokenItemConverter.kt index 1e073b5ffd..ebd6ad5e7e 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/CryptoCurrencyStatusToTokenItemConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/CryptoCurrencyStatusToTokenItemConverter.kt @@ -30,6 +30,7 @@ internal class CryptoCurrencyStatusToTokenItemConverter( is CryptoCurrencyStatus.MissedDerivation, is CryptoCurrencyStatus.NoAccount, is CryptoCurrencyStatus.Unreachable, + is CryptoCurrencyStatus.NoAmount, -> value.mapToUnreachableTokenItemState() } } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/TokenListToContentItemsConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/TokenListToContentItemsConverter.kt index cb11331df1..8fb50ac24e 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/TokenListToContentItemsConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/TokenListToContentItemsConverter.kt @@ -1,7 +1,7 @@ package com.tangem.feature.wallet.presentation.wallet.utils import com.tangem.common.Provider -import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.stringReference import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.model.NetworkGroup @@ -68,7 +68,12 @@ internal class TokenListToContentItemsConverter( } private fun MutableList.addGroup(group: NetworkGroup): List { - this.add(TokensListItemState.NetworkGroupTitle(TextReference.Str(group.network.name))) + val groupTitle = TokensListItemState.NetworkGroupTitle( + id = group.network.hashCode(), + name = stringReference(group.network.name), + ) + + this.add(groupTitle) group.currencies.forEach { token -> this.addToken(token) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt index 6efc2b0461..1c8e0dd7f9 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt @@ -3,8 +3,6 @@ package com.tangem.feature.wallet.presentation.wallet.viewmodels import androidx.lifecycle.* import androidx.paging.cachedIn import arrow.core.getOrElse -import com.tangem.blockchain.common.Blockchain -import com.tangem.blockchain.common.derivation.DerivationStyle import com.tangem.common.Provider import com.tangem.common.doOnFailure import com.tangem.common.doOnSuccess @@ -15,7 +13,6 @@ import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.card.* import com.tangem.domain.common.CardTypesResolver import com.tangem.domain.common.util.cardTypesResolver -import com.tangem.domain.common.util.derivationStyleProvider import com.tangem.domain.demo.IsDemoCardUseCase import com.tangem.domain.redux.ReduxStateHolder import com.tangem.domain.settings.CanUseBiometryUseCase @@ -461,14 +458,18 @@ internal class WalletViewModel @Inject constructor( val wallet = getWallet( index = requireNotNull(uiState as? WalletState.ContentState).walletsListConfig.selectedWalletIndex, ) - router.openTxHistoryWebsite( - url = getExploreUrlUseCase( - userWalletId = wallet.walletId, - networkId = Network.ID( - value = wallet.scanResponse.cardTypesResolver.getBlockchain().id, + val currencyStatus = getPrimaryCurrencyStatusUpdatesUseCase(wallet.walletId) + .firstOrNull() + ?.getOrNull() + + if (currencyStatus != null) { + router.openTxHistoryWebsite( + url = getExploreUrlUseCase( + userWalletId = wallet.walletId, + network = currencyStatus.currency.network, ), - ), - ) + ) + } } } @@ -582,31 +583,20 @@ internal class WalletViewModel @Inject constructor( private fun getSingleCurrencyContent(index: Int) { val wallet = getWallet(index) - val blockchain = getCardTypeResolver(index).getBlockchain() - updateTxHistory( - blockchain = blockchain, - derivationStyle = wallet.scanResponse.derivationStyleProvider.getDerivationStyle(), - ) - updateMarketPrice(userWalletId = wallet.walletId) + updatePrimaryCurrencyStatus(userWalletId = wallet.walletId) updateNotifications(index) } - private fun updateTxHistory(blockchain: Blockchain, derivationStyle: DerivationStyle?) { + private fun updateTxHistory(network: Network) { viewModelScope.launch(dispatchers.io) { - val derivationPath = blockchain.derivationPath(style = derivationStyle)?.rawPath - - val txHistoryItemsCountEither = txHistoryItemsCountUseCase( - networkId = Network.ID(blockchain.id), - derivationPath = derivationPath, - ) + val txHistoryItemsCountEither = txHistoryItemsCountUseCase(network) uiState = stateFactory.getLoadingTxHistoryState(itemsCountEither = txHistoryItemsCountEither) txHistoryItemsCountEither.onRight { uiState = stateFactory.getLoadedTxHistoryState( txHistoryEither = txHistoryItemsUseCase( - networkId = Network.ID(blockchain.id), - derivationPath = derivationPath, + network, ).map { it.cachedIn(viewModelScope) }, @@ -615,8 +605,7 @@ internal class WalletViewModel @Inject constructor( } } - // It also update wallet balance - private fun updateMarketPrice(userWalletId: UserWalletId) { + private fun updatePrimaryCurrencyStatus(userWalletId: UserWalletId) { getPrimaryCurrencyStatusUpdatesUseCase(userWalletId = userWalletId) .distinctUntilChanged() .onEach { maybeCryptoCurrencyStatus -> @@ -625,6 +614,7 @@ internal class WalletViewModel @Inject constructor( maybeCryptoCurrencyStatus.onRight { status -> singleWalletCryptoCurrencyStatus = status updateButtons(userWalletId = userWalletId, currency = status.currency) + updateTxHistory(status.currency.network) } } .flowOn(dispatchers.io) From 9260676c4b69baafdc4711c9d4130c876f12dd90 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 12 Sep 2023 16:01:10 +0400 Subject: [PATCH 034/242] Updated on 2026-08-14 --- .../features/details/redux/DetailsAction.kt | 2 ++ .../details/redux/DetailsMiddleware.kt | 15 +++++++++++--- .../ui/appsettings/AppSettingsFragment.kt | 5 +++-- .../ui/appsettings/AppSettingsViewModel.kt | 19 ++++++++++++++---- .../data/card/sdk/DefaultCardSdkProvider.kt | 20 ++++++------------- 5 files changed, 38 insertions(+), 23 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsAction.kt b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsAction.kt index 0b4facea41..f65bb76b14 100644 --- a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsAction.kt +++ b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsAction.kt @@ -1,6 +1,7 @@ package com.tangem.tap.features.details.redux import com.tangem.core.ui.extensions.TextReference +import androidx.lifecycle.LifecycleCoroutineScope import com.tangem.domain.apptheme.model.AppThemeMode import com.tangem.domain.common.CardTypesResolver import com.tangem.domain.models.scan.CardDTO @@ -72,6 +73,7 @@ sealed class DetailsAction : Action { data class CheckBiometricsStatus( val awaitStatusChange: Boolean, + val lifecycleCoroutineScope: LifecycleCoroutineScope, ) : AppSettings() object EnrollBiometrics : AppSettings() diff --git a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsMiddleware.kt b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsMiddleware.kt index aa3264275d..6c15790af7 100644 --- a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsMiddleware.kt @@ -1,5 +1,6 @@ package com.tangem.tap.features.details.redux +import androidx.lifecycle.LifecycleCoroutineScope import com.tangem.common.CompletionResult import com.tangem.common.core.TangemError import com.tangem.common.core.TangemSdkError @@ -193,7 +194,11 @@ class DetailsMiddleware { } } is DetailsAction.AppSettings.CheckBiometricsStatus -> { - checkBiometricsStatus(action.awaitStatusChange, state) + checkBiometricsStatus( + awaitStatusChange = action.awaitStatusChange, + state = state, + lifecycleScope = action.lifecycleCoroutineScope, + ) } is DetailsAction.AppSettings.EnrollBiometrics -> { enrollBiometrics() @@ -212,8 +217,12 @@ class DetailsMiddleware { * @param awaitStatusChange If true then start a new coroutine and check the biometric status every 100 * milliseconds until it changes * */ - private fun checkBiometricsStatus(awaitStatusChange: Boolean, state: DetailsState) { - scope.launch { + private fun checkBiometricsStatus( + awaitStatusChange: Boolean, + state: DetailsState, + lifecycleScope: LifecycleCoroutineScope, + ) { + lifecycleScope.launch { if (awaitStatusChange) { while (state.appSettingsState.needEnrollBiometrics == tangemSdkManager.needEnrollBiometrics) { delay(timeMillis = 100) diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsFragment.kt b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsFragment.kt index ef5d881910..b1d8e9b55c 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsFragment.kt @@ -3,6 +3,7 @@ package com.tangem.tap.features.details.ui.appsettings import android.os.Bundle import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import androidx.lifecycle.lifecycleScope import androidx.transition.TransitionInflater import com.tangem.core.navigation.NavigationAction import com.tangem.core.ui.screen.ComposeFragment @@ -38,7 +39,7 @@ internal class AppSettingsFragment : ComposeFragment(), StoreSubscriber) { ) } - fun checkBiometricsStatus() { - store.dispatch(DetailsAction.AppSettings.CheckBiometricsStatus(awaitStatusChange = false)) + fun checkBiometricsStatus(lifecycleScope: LifecycleCoroutineScope) { + store.dispatch( + DetailsAction.AppSettings.CheckBiometricsStatus( + awaitStatusChange = false, + lifecycleCoroutineScope = lifecycleScope, + ), + ) } - fun refreshBiometricsStatus() { - store.dispatch(DetailsAction.AppSettings.CheckBiometricsStatus(awaitStatusChange = true)) + fun refreshBiometricsStatus(lifecycleScope: LifecycleCoroutineScope) { + store.dispatch( + DetailsAction.AppSettings.CheckBiometricsStatus( + awaitStatusChange = true, + lifecycleCoroutineScope = lifecycleScope, + ), + ) } private fun buildItems(state: AppSettingsState): ImmutableList { diff --git a/data/card/src/main/java/com/tangem/data/card/sdk/DefaultCardSdkProvider.kt b/data/card/src/main/java/com/tangem/data/card/sdk/DefaultCardSdkProvider.kt index b536029bcb..c2c2a2dd19 100644 --- a/data/card/src/main/java/com/tangem/data/card/sdk/DefaultCardSdkProvider.kt +++ b/data/card/src/main/java/com/tangem/data/card/sdk/DefaultCardSdkProvider.kt @@ -20,26 +20,18 @@ import javax.inject.Singleton internal class DefaultCardSdkProvider @Inject constructor() : CardSdkProvider, CardSdkLifecycleObserver { override val sdk: TangemSdk - get() = requireNotNull(value = _sdk) { "Impossible to get the TangemSdk when activity is destroyed" } + get() = requireNotNull(value = _sdk?.get()) { "Impossible to get the TangemSdk when activity is destroyed" } - private var _sdk: TangemSdk? = null - - /** Weak reference of context that uses to create [TangemSdk] */ - private var contextRef: WeakReference = WeakReference(null) + private var _sdk: WeakReference? = null override fun onCreate(context: Context) { - contextRef = WeakReference(context) - _sdk = TangemSdk.initWithBiometrics(activity = context as FragmentActivity, config = config) + _sdk = WeakReference(TangemSdk.initWithBiometrics(activity = context as FragmentActivity, config = config)) } override fun onDestroy(context: Context) { - /* - - - */ - if (contextRef.get() == context) { - _sdk = null - } + // Commented out to prevent crash on getting sdk when it's null. + // FIXME: We still should find the real cause and fix it properly. + // _sdk = null } private companion object { From 42e2ed630921060472d582eccff0643985cdba11 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 13 Sep 2023 12:49:44 +0300 Subject: [PATCH 035/242] Updated on 2026-08-14 --- .../com/tangem/core/ui/components/Shimmers.kt | 79 +++++++++++++++---- .../com/tangem/core/ui/res/TangemTheme.kt | 5 +- 2 files changed, 67 insertions(+), 17 deletions(-) diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/Shimmers.kt b/core/ui/src/main/java/com/tangem/core/ui/components/Shimmers.kt index 640f986b3e..311fe1aae8 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/Shimmers.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/Shimmers.kt @@ -1,15 +1,25 @@ package com.tangem.core.ui.components +import androidx.compose.animation.core.LinearEasing +import androidx.compose.animation.core.RepeatMode +import androidx.compose.animation.core.infiniteRepeatable +import androidx.compose.animation.core.tween import androidx.compose.foundation.background import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.runtime.Composable +import androidx.compose.runtime.ReadOnlyComposable import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.BlendMode +import androidx.compose.ui.graphics.Color import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.Dp +import com.tangem.core.ui.res.LocalIsInDarkTheme +import com.tangem.core.ui.res.TangemColorPalette import com.tangem.core.ui.res.TangemTheme -import com.valentinilk.shimmer.shimmer +import com.valentinilk.shimmer.* /** * Rectangle shimmer item with rounded shape from DS @@ -18,11 +28,8 @@ import com.valentinilk.shimmer.shimmer fun RectangleShimmer(modifier: Modifier = Modifier, radius: Dp = TangemTheme.dimens.radius6) { Box( modifier = modifier - .shimmer() - .background( - color = TangemTheme.colors.button.secondary, - shape = RoundedCornerShape(size = radius), - ), + .clip(RoundedCornerShape(size = radius)) + .shimmer(TangemShimmer), ) } @@ -34,27 +41,67 @@ fun RectangleShimmer(modifier: Modifier = Modifier, radius: Dp = TangemTheme.dim fun CircleShimmer(modifier: Modifier = Modifier) { Box( modifier = modifier - .shimmer() - .background( - color = TangemTheme.colors.button.secondary, - shape = CircleShape, - ), + .clip(CircleShape) + .shimmer(TangemShimmer), ) } +private val TangemShimmer: Shimmer + @Composable + get() = rememberShimmer( + shimmerBounds = ShimmerBounds.View, + theme = defaultShimmerTheme.copy( + animationSpec = infiniteRepeatable( + animation = tween( + durationMillis = 800, + easing = LinearEasing, + delayMillis = 800, + ), + repeatMode = RepeatMode.Restart, + ), + shaderColors = TangemShimmerColors, + blendMode = BlendMode.Src, + shaderColorStops = null, + ), + ) + +private val TangemShimmerColors: List + @Composable + @ReadOnlyComposable + get() { + val isInDarkTheme = LocalIsInDarkTheme.current + + return buildList { + if (isInDarkTheme) { + TangemColorPalette.Dark3.let(::add) + TangemColorPalette.Dark4.let(::add) + TangemColorPalette.Dark6.let(::add) + TangemColorPalette.Dark4.let(::add) + TangemColorPalette.Dark3.let(::add) + } else { + TangemColorPalette.Light2.let(::add) + TangemColorPalette.Light1.let(::add) + TangemColorPalette.White.let(::add) + TangemColorPalette.Light1.let(::add) + TangemColorPalette.Light2.let(::add) + } + } + } + // region preview @Composable private fun ShimmersPreview() { Column( - modifier = Modifier.fillMaxWidth(), + modifier = Modifier + .fillMaxWidth() + .background(TangemTheme.colors.background.primary), verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing18), ) { RectangleShimmer( - modifier = Modifier.size( - width = TangemTheme.dimens.size72, - height = TangemTheme.dimens.size12, - ), + modifier = Modifier + .fillMaxWidth() + .height(TangemTheme.dimens.size24), ) CircleShimmer(modifier = Modifier.size(size = TangemTheme.dimens.size42)) } diff --git a/core/ui/src/main/java/com/tangem/core/ui/res/TangemTheme.kt b/core/ui/src/main/java/com/tangem/core/ui/res/TangemTheme.kt index efe05116e7..f72c4b456f 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/res/TangemTheme.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/res/TangemTheme.kt @@ -36,6 +36,7 @@ fun TangemTheme( LocalTangemTypography provides typography, LocalTangemDimens provides dimens, LocalTangemShapes provides shapes, + LocalIsInDarkTheme provides isDark, ) { ProvideTextStyle( value = TangemTheme.typography.body1, @@ -197,4 +198,6 @@ private val LocalTangemDimens = staticCompositionLocalOf { private val LocalTangemShapes = staticCompositionLocalOf { error("No TangemShapes provided") -} \ No newline at end of file +} + +val LocalIsInDarkTheme = staticCompositionLocalOf { false } \ No newline at end of file From e8176995221f71d088ae5f76c7608e47fa3c6aab Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 11 Sep 2023 16:57:06 +0400 Subject: [PATCH 036/242] Updated on 2026-08-14 --- .../ui/common/DetailsComposeElements.kt | 9 ++--- .../details/ui/common/TangemSwitch.kt | 21 +++------- .../details/ui/resetcard/ResetCardScreen.kt | 25 ++++++++---- .../ui/walletconnect/WalletConnectScreen.kt | 17 ++++----- .../disclaimer/ui/DisclaimerFragment.kt | 1 + .../main/res/drawable/ic_walletconnect.xml | 4 +- .../res/drawable/ill_reset_background.xml | 24 ------------ app/src/main/res/drawable/img_alert.xml | 19 ++++++++++ .../main/res/layout/fragment_disclaimer.xml | 4 +- app/src/main/res/values/colors.xml | 3 +- app/src/main/res/values/styles.xml | 2 +- .../com/tangem/core/ui/res/IconColorType.kt | 4 +- .../tangem/core/ui/res/TangemColorPalette.kt | 8 ++-- .../com/tangem/core/ui/res/TangemColors.kt | 12 +++--- .../com/tangem/core/ui/res/TangemTheme.kt | 8 ++++ .../referral/presentation/build.gradle.kts | 1 + .../feature/referral/ReferralFragment.kt | 38 ++++++++++--------- 17 files changed, 103 insertions(+), 97 deletions(-) delete mode 100644 app/src/main/res/drawable/ill_reset_background.xml create mode 100644 app/src/main/res/drawable/img_alert.xml diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/common/DetailsComposeElements.kt b/app/src/main/java/com/tangem/tap/features/details/ui/common/DetailsComposeElements.kt index bdba522a2b..35b36c0f00 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/common/DetailsComposeElements.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/common/DetailsComposeElements.kt @@ -9,7 +9,6 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.res.colorResource import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp @@ -134,8 +133,8 @@ internal fun DetailsRadioButtonElement(title: String, subtitle: String, selected onClick = null, modifier = Modifier.padding(end = 20.dp), colors = RadioButtonDefaults.colors( - unselectedColor = colorResource(id = R.color.icon_secondary), - selectedColor = colorResource(id = R.color.icon_accent), + unselectedColor = TangemTheme.colors.icon.secondary, + selectedColor = TangemTheme.colors.icon.accent, ), ) @@ -143,13 +142,13 @@ internal fun DetailsRadioButtonElement(title: String, subtitle: String, selected Text( text = title, style = TangemTheme.typography.subtitle1, - color = colorResource(id = R.color.text_primary_1), + color = TangemTheme.colors.text.primary1, ) Spacer(modifier = Modifier.size(4.dp)) Text( text = subtitle, style = TangemTheme.typography.body2, - color = colorResource(id = R.color.text_secondary), + color = TangemTheme.colors.text.secondary, ) } } diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/common/TangemSwitch.kt b/app/src/main/java/com/tangem/tap/features/details/ui/common/TangemSwitch.kt index c6c8e82cf4..9839280049 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/common/TangemSwitch.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/common/TangemSwitch.kt @@ -1,22 +1,12 @@ package com.tangem.tap.features.details.ui.common import androidx.compose.animation.animateColor -import androidx.compose.animation.core.FastOutLinearInEasing -import androidx.compose.animation.core.LinearOutSlowInEasing -import androidx.compose.animation.core.animateDp -import androidx.compose.animation.core.tween -import androidx.compose.animation.core.updateTransition +import androidx.compose.animation.core.* import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.indication import androidx.compose.foundation.interaction.MutableInteractionSource -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.BoxWithConstraints -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.offset -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.ripple.rememberRipple import androidx.compose.runtime.Composable @@ -25,17 +15,16 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.res.colorResource import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp -import com.tangem.wallet.R +import com.tangem.core.ui.res.TangemTheme @Suppress("MagicNumber") @Composable fun TangemSwitch( onCheckedChange: (Boolean) -> Unit, - checkedColor: Color = colorResource(id = R.color.control_checked), - uncheckedColor: Color = colorResource(id = R.color.icon_informative), + checkedColor: Color = TangemTheme.colors.icon.accent, + uncheckedColor: Color = TangemTheme.colors.icon.informative, size: Dp = 48.dp, checked: Boolean = false, enabled: Boolean = true, diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardScreen.kt b/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardScreen.kt index eb656819fd..d7b85a7202 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardScreen.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardScreen.kt @@ -1,12 +1,17 @@ package com.tangem.tap.features.details.ui.resetcard -import androidx.compose.foundation.* +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll import androidx.compose.material.Icon import androidx.compose.material.IconToggleButton import androidx.compose.material.Text import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview @@ -37,15 +42,19 @@ private fun ResetCardView(state: ResetCardScreenState) { .verticalScroll(rememberScrollState()), verticalArrangement = Arrangement.SpaceBetween, ) { - Box { - Image( - painter = painterResource(id = R.drawable.ill_reset_background), - contentDescription = null, - modifier = Modifier.offset(y = (-82).dp), + ScreenTitle(titleRes = R.string.card_settings_reset_card_to_factory) + Box( + modifier = Modifier + .weight(1f) + .padding(horizontal = 21.dp), + contentAlignment = Alignment.CenterStart, + ) { + Icon( + painter = painterResource(id = R.drawable.img_alert), + contentDescription = "", + tint = Color.Unspecified, ) - ScreenTitle(titleRes = R.string.card_settings_reset_card_to_factory) } - Spacer(modifier = Modifier.weight(1f)) Column( modifier = Modifier.offset(y = (-32).dp), verticalArrangement = Arrangement.Bottom, diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/WalletConnectScreen.kt b/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/WalletConnectScreen.kt index 409b63fb8b..79e8b38b23 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/WalletConnectScreen.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/WalletConnectScreen.kt @@ -12,7 +12,6 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.colorResource import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview @@ -61,8 +60,8 @@ internal fun WalletConnectScreen( private fun AddSessionFab(onAddSession: () -> Unit, modifier: Modifier = Modifier) { FloatingActionButton( onClick = onAddSession, - backgroundColor = colorResource(id = R.color.button_primary), - contentColor = colorResource(id = R.color.icon_primary_2), + backgroundColor = TangemTheme.colors.button.primary, + contentColor = TangemTheme.colors.icon.primary2, shape = RoundedCornerShape(16.dp), modifier = modifier, ) { @@ -78,7 +77,7 @@ private fun EmptyScreen(state: WalletConnectScreenState) { if (state.isLoading) { LinearProgressIndicator( modifier = Modifier.fillMaxWidth(), - color = colorResource(id = R.color.icon_accent), + color = TangemTheme.colors.icon.accent, ) } Column( @@ -91,7 +90,7 @@ private fun EmptyScreen(state: WalletConnectScreenState) { Image( painter = painterResource(id = R.drawable.ic_walletconnect), contentDescription = "", - colorFilter = ColorFilter.tint(colorResource(id = R.color.icon_inactive)), + colorFilter = ColorFilter.tint(TangemTheme.colors.icon.inactive), contentScale = ContentScale.FillWidth, modifier = Modifier.width(width = 100.dp), ) @@ -99,7 +98,7 @@ private fun EmptyScreen(state: WalletConnectScreenState) { Text( text = stringResource(id = R.string.wallet_connect_subtitle), style = TangemTheme.typography.body2, - color = colorResource(id = R.color.text_tertiary), + color = TangemTheme.colors.text.tertiary, ) } } @@ -111,7 +110,7 @@ private fun WalletConnectSessions(state: WalletConnectScreenState) { modifier = Modifier .fillMaxWidth() .height(2.dp), - color = colorResource(id = R.color.icon_accent), + color = TangemTheme.colors.icon.accent, ) } else { Spacer(modifier = Modifier.height(2.dp)) @@ -132,7 +131,7 @@ private fun WalletConnectSessions(state: WalletConnectScreenState) { Text( text = session.description, style = TangemTheme.typography.subtitle1, - color = colorResource(id = R.color.text_primary_1), + color = TangemTheme.colors.text.primary1, modifier = Modifier.weight(1f), ) IconButton( @@ -144,7 +143,7 @@ private fun WalletConnectSessions(state: WalletConnectScreenState) { Icon( painter = painterResource(id = R.drawable.ic_cross_rounded_24), contentDescription = "", - tint = colorResource(id = R.color.icon_warning), + tint = TangemTheme.colors.icon.warning, ) } } diff --git a/app/src/main/java/com/tangem/tap/features/disclaimer/ui/DisclaimerFragment.kt b/app/src/main/java/com/tangem/tap/features/disclaimer/ui/DisclaimerFragment.kt index 21d6804bb5..f208ac437a 100644 --- a/app/src/main/java/com/tangem/tap/features/disclaimer/ui/DisclaimerFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/disclaimer/ui/DisclaimerFragment.kt @@ -122,6 +122,7 @@ class DisclaimerFragment : BaseFragment(R.layout.fragment_disclaimer), StoreSubs webView.loadLocalTermsOfServices() } else -> { + webView.setBackgroundColor(resources.getColor(R.color.transparent, null)) webView.loadUrl(disclaimer.getUri().toString()) } } diff --git a/app/src/main/res/drawable/ic_walletconnect.xml b/app/src/main/res/drawable/ic_walletconnect.xml index 925a858a8a..d4b4c99942 100644 --- a/app/src/main/res/drawable/ic_walletconnect.xml +++ b/app/src/main/res/drawable/ic_walletconnect.xml @@ -1,5 +1,7 @@ - + diff --git a/app/src/main/res/drawable/ill_reset_background.xml b/app/src/main/res/drawable/ill_reset_background.xml deleted file mode 100644 index 8ce3137945..0000000000 --- a/app/src/main/res/drawable/ill_reset_background.xml +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/drawable/img_alert.xml b/app/src/main/res/drawable/img_alert.xml new file mode 100644 index 0000000000..42b087d91c --- /dev/null +++ b/app/src/main/res/drawable/img_alert.xml @@ -0,0 +1,19 @@ + + + + + + + diff --git a/app/src/main/res/layout/fragment_disclaimer.xml b/app/src/main/res/layout/fragment_disclaimer.xml index 136a258b23..1c237b7938 100644 --- a/app/src/main/res/layout/fragment_disclaimer.xml +++ b/app/src/main/res/layout/fragment_disclaimer.xml @@ -21,6 +21,8 @@ android:layout_width="match_parent" android:layout_height="?attr/actionBarSize" app:navigationIcon="@drawable/ic_baseline_arrow_back_24" + app:navigationIconTint="@color/icon_primary_1" + app:titleTextColor="@color/text_primary_1" app:title="@string/disclaimer_title" /> @@ -29,7 +31,7 @@ android:id="@+id/cl_details_confirm" android:layout_width="match_parent" android:layout_height="match_parent" - android:background="@color/white" + android:background="@color/background_secondary" app:layout_behavior="@string/appbar_scrolling_view_behavior"> #919191 #000000 - - + #00000000 \ No newline at end of file diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index 540e4ebe30..34b6f8789f 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -1,6 +1,6 @@ - + + + + + + + + diff --git a/features/onboarding/src/main/java/com/tangem/feature/onboarding/api/OnboardingSeedPhrase.kt b/features/onboarding/src/main/java/com/tangem/feature/onboarding/api/OnboardingSeedPhrase.kt index 2f6b1d4a0c..75e0acda9f 100644 --- a/features/onboarding/src/main/java/com/tangem/feature/onboarding/api/OnboardingSeedPhrase.kt +++ b/features/onboarding/src/main/java/com/tangem/feature/onboarding/api/OnboardingSeedPhrase.kt @@ -1,6 +1,7 @@ package com.tangem.feature.onboarding.api import androidx.activity.compose.BackHandler +import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.* import androidx.compose.material.LinearProgressIndicator import androidx.compose.runtime.Composable @@ -18,7 +19,7 @@ class OnboardingSeedPhrase : OnboardingSeedPhraseApi { @Composable override fun ScreenContent(uiState: OnboardingSeedPhraseState, subScreen: SeedPhraseScreen, progress: Float) { BackHandler(onBack = uiState.onBackClick) - TangemTheme { + TangemTheme(isDark = isSystemInDarkTheme()) { Column { ProgressIndicator(progress) Content(subScreen, uiState) diff --git a/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/AboutSeedPhraseScreen.kt b/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/AboutSeedPhraseScreen.kt index fc583bccc7..51e56a32da 100644 --- a/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/AboutSeedPhraseScreen.kt +++ b/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/AboutSeedPhraseScreen.kt @@ -6,6 +6,7 @@ import androidx.compose.foundation.border import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.Icon import androidx.compose.material.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment @@ -135,13 +136,15 @@ private fun ReadMoreBlock(state: AboutState) { vertical = TangemTheme.dimens.size8, ), ) { - Image( + Icon( painter = painterResource(id = R.drawable.ic_arrow_top_right_24), contentDescription = null, + tint = TangemTheme.colors.icon.primary1, ) SpacerW8() Text( text = stringResource(id = R.string.onboarding_seed_button_read_more), + color = TangemTheme.colors.text.primary1, ) } } diff --git a/features/referral/presentation/src/main/java/com/tangem/feature/referral/ReferralFragment.kt b/features/referral/presentation/src/main/java/com/tangem/feature/referral/ReferralFragment.kt index f949764f94..e45014f407 100644 --- a/features/referral/presentation/src/main/java/com/tangem/feature/referral/ReferralFragment.kt +++ b/features/referral/presentation/src/main/java/com/tangem/feature/referral/ReferralFragment.kt @@ -1,5 +1,6 @@ package com.tangem.feature.referral +import android.os.Bundle import androidx.compose.foundation.layout.systemBarsPadding import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier @@ -23,9 +24,13 @@ class ReferralFragment : ComposeFragment() { private val viewModel by viewModels() + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + viewModel.setRouter(ReferralRouter(fragmentManager = WeakReference(parentFragmentManager))) + } + @Composable override fun ScreenContent(modifier: Modifier) { - viewModel.setRouter(ReferralRouter(fragmentManager = WeakReference(parentFragmentManager))) viewModel.onScreenOpened() val backgroundColor = TangemTheme.colors.background.secondary diff --git a/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/ReferralScreen.kt b/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/ReferralScreen.kt index 6dcec255cf..fd50ed3486 100644 --- a/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/ReferralScreen.kt +++ b/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/ReferralScreen.kt @@ -12,8 +12,6 @@ import androidx.compose.material.* import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.draw.shadow import androidx.compose.ui.layout.onSizeChanged import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalDensity @@ -26,6 +24,7 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.withStyle import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import com.tangem.core.ui.components.RectangleShimmer import com.tangem.core.ui.components.SpacerH16 import com.tangem.core.ui.components.SpacerH24 import com.tangem.core.ui.components.SpacerH32 @@ -38,7 +37,6 @@ import com.tangem.feature.referral.models.DemoModeException import com.tangem.feature.referral.models.ReferralStateHolder import com.tangem.feature.referral.models.ReferralStateHolder.* import com.tangem.feature.referral.presentation.R -import com.valentinilk.shimmer.shimmer import kotlinx.coroutines.launch /** @@ -338,24 +336,18 @@ private fun ShimmerInfo() { Column( modifier = Modifier .fillMaxWidth() - .padding(top = TangemTheme.dimens.spacing4) - .shimmer(), + .padding(top = TangemTheme.dimens.spacing4), verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing10), ) { - Box( + RectangleShimmer( modifier = Modifier - .clip(RoundedCornerShape(TangemTheme.dimens.radius6)) .width(TangemTheme.dimens.size102) - .height(TangemTheme.dimens.size16) - .background(TangemColorPalette.White), - // .background(Color(0xFFF8F8F8)), + .height(TangemTheme.dimens.size16), ) - Box( + RectangleShimmer( modifier = Modifier - .clip(RoundedCornerShape(TangemTheme.dimens.radius6)) .width(TangemTheme.dimens.size40) - .height(TangemTheme.dimens.size12) - .background(TangemColorPalette.White), + .height(TangemTheme.dimens.size12), ) } } @@ -422,10 +414,9 @@ private fun BoxScope.CopySnackbarHost(isCopyButtonPressed: MutableState Box( modifier = Modifier .onSizeChanged { snackbarSize = it.width } - .background(TangemColorPalette.Black, RoundedCornerShape(size = TangemTheme.dimens.radius8)) - .shadow( - TangemTheme.dimens.elevation3, - RoundedCornerShape(size = TangemTheme.dimens.radius8), + .background( + color = TangemTheme.colors.icon.primary1, + shape = RoundedCornerShape(size = TangemTheme.dimens.radius8), ) .padding( horizontal = TangemTheme.dimens.spacing16, diff --git a/features/swap/presentation/build.gradle.kts b/features/swap/presentation/build.gradle.kts index cd060cd2e8..63f2d6414e 100644 --- a/features/swap/presentation/build.gradle.kts +++ b/features/swap/presentation/build.gradle.kts @@ -32,6 +32,7 @@ dependencies { implementation(deps.compose.ui.tooling) implementation(deps.compose.coil) implementation(deps.compose.constraintLayout) + implementation(deps.compose.accompanist.systemUiController) /** Api */ implementation(projects.features.swap.api) diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/presentation/SwapFragment.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/presentation/SwapFragment.kt index 07ab075e98..5130290638 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/presentation/SwapFragment.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/presentation/SwapFragment.kt @@ -1,14 +1,14 @@ package com.tangem.feature.swap.presentation import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup import androidx.compose.animation.Crossfade import androidx.compose.runtime.Composable -import androidx.compose.ui.platform.ComposeView -import androidx.fragment.app.Fragment +import androidx.compose.ui.Modifier import androidx.fragment.app.viewModels +import com.tangem.core.ui.components.SystemBarsEffect +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.screen.ComposeFragment +import com.tangem.core.ui.theme.AppThemeModeHolder import com.tangem.feature.swap.router.CustomTabsManager import com.tangem.feature.swap.router.SwapNavScreen import com.tangem.feature.swap.router.SwapRouter @@ -18,31 +18,35 @@ import com.tangem.feature.swap.ui.SwapSuccessScreen import com.tangem.feature.swap.viewmodels.SwapViewModel import dagger.hilt.android.AndroidEntryPoint import java.lang.ref.WeakReference +import javax.inject.Inject @AndroidEntryPoint -class SwapFragment : Fragment() { +class SwapFragment : ComposeFragment() { + + @Inject + override lateinit var appThemeModeHolder: AppThemeModeHolder private val viewModel by viewModels() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) lifecycle.addObserver(viewModel) - } - - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { viewModel.setRouter( SwapRouter( fragmentManager = WeakReference(parentFragmentManager), customTabsManager = CustomTabsManager(WeakReference(context)), ), ) + } + + @Composable + override fun ScreenContent(modifier: Modifier) { viewModel.onScreenOpened() - return ComposeView(inflater.context).apply { - setContent { - ScreenContent(viewModel = viewModel) - } - } + val backgroundColor = TangemTheme.colors.background.secondary + SystemBarsEffect { setSystemBarsColor(backgroundColor) } + + ScreenContent(viewModel = viewModel) } @Suppress("TopLevelComposableFunctions") diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreen.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreen.kt index 9c43985c64..a5f8b658ec 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreen.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreen.kt @@ -5,11 +5,7 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.systemBarsPadding import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.ExperimentalMaterialApi -import androidx.compose.material.ModalBottomSheetLayout -import androidx.compose.material.ModalBottomSheetState -import androidx.compose.material.ModalBottomSheetValue -import androidx.compose.material.rememberModalBottomSheetState +import androidx.compose.material.* import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.rememberCoroutineScope @@ -38,51 +34,49 @@ internal fun SwapScreen(stateHolder: SwapStateHolder) { }, ) - TangemTheme { - ModalBottomSheetLayout( - modifier = Modifier.systemBarsPadding(), - sheetContent = { - if (stateHolder.permissionState is SwapPermissionState.ReadyForRequest) { - SwapPermissionBottomSheetContent( - data = stateHolder.permissionState, - onCancel = { - hideBottomSheet(coroutineScope, stateHolder, bottomSheetState) - }, - ) - } else { - // Required "else" block to prevent compose crash - // always close BS if its empty, cause user should not see this - LaunchedEffect(Unit) { - coroutineScope.launch { bottomSheetState.hide() } - } - Box(modifier = Modifier.fillMaxSize()) - } - }, - sheetState = bottomSheetState, - sheetShape = RoundedCornerShape( - topStart = TangemTheme.dimens.radius16, - topEnd = TangemTheme.dimens.radius16, - ), - sheetElevation = TangemTheme.dimens.elevation24, - content = { - SwapScreenContent( - state = stateHolder, - onPermissionWarningClick = { - val isBottomSheetReady = !bottomSheetState.isVisible && - stateHolder.permissionState is SwapPermissionState.ReadyForRequest - coroutineScope.launch { - if (isBottomSheetReady) { - bottomSheetState.show() - stateHolder.onShowPermissionBottomSheet.invoke() - } else { - bottomSheetState.hide() - } - } + ModalBottomSheetLayout( + modifier = Modifier.systemBarsPadding(), + sheetContent = { + if (stateHolder.permissionState is SwapPermissionState.ReadyForRequest) { + SwapPermissionBottomSheetContent( + data = stateHolder.permissionState, + onCancel = { + hideBottomSheet(coroutineScope, stateHolder, bottomSheetState) }, ) - }, - ) - } + } else { + // Required "else" block to prevent compose crash + // always close BS if its empty, cause user should not see this + LaunchedEffect(Unit) { + coroutineScope.launch { bottomSheetState.hide() } + } + Box(modifier = Modifier.fillMaxSize()) + } + }, + sheetState = bottomSheetState, + sheetShape = RoundedCornerShape( + topStart = TangemTheme.dimens.radius16, + topEnd = TangemTheme.dimens.radius16, + ), + sheetElevation = TangemTheme.dimens.elevation24, + content = { + SwapScreenContent( + state = stateHolder, + onPermissionWarningClick = { + val isBottomSheetReady = !bottomSheetState.isVisible && + stateHolder.permissionState is SwapPermissionState.ReadyForRequest + coroutineScope.launch { + if (isBottomSheetReady) { + bottomSheetState.show() + stateHolder.onShowPermissionBottomSheet.invoke() + } else { + bottomSheetState.hide() + } + } + }, + ) + }, + ) } @OptIn(ExperimentalMaterialApi::class) diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt index c18c6a8889..52deac586f 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt @@ -293,10 +293,11 @@ private fun SwapWarnings(warnings: List) { warning.tokenCurrency, ), icon = { - Image( + Icon( painter = painterResource(id = com.tangem.core.ui.R.drawable.ic_locked_24), contentDescription = null, modifier = Modifier.size(TangemTheme.dimens.size20), + tint = TangemTheme.colors.icon.primary1, ) }, ) diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapSelectTokenScreen.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapSelectTokenScreen.kt index ad9b5d8d2c..eb086a8df4 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapSelectTokenScreen.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapSelectTokenScreen.kt @@ -8,7 +8,6 @@ import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.itemsIndexed -import androidx.compose.foundation.shape.CircleShape import androidx.compose.material.Divider import androidx.compose.material.Scaffold import androidx.compose.material.Text @@ -24,6 +23,7 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import coil.compose.SubcomposeAsyncImage import coil.request.ImageRequest +import com.tangem.core.ui.components.CircleShimmer import com.tangem.core.ui.components.CurrencyPlaceholderIcon import com.tangem.core.ui.components.SpacerW2 import com.tangem.core.ui.components.appbar.ExpandableSearchView @@ -34,7 +34,6 @@ import com.tangem.feature.swap.models.SwapSelectTokenStateHolder import com.tangem.feature.swap.models.TokenBalanceData import com.tangem.feature.swap.models.TokenToSelect import com.tangem.feature.swap.presentation.R -import com.valentinilk.shimmer.shimmer @Composable fun SwapSelectTokenScreen( @@ -42,28 +41,26 @@ fun SwapSelectTokenScreen( onSearchFocusChange: (Boolean) -> Unit, onBack: () -> Unit, ) { - TangemTheme { - Scaffold( - modifier = Modifier - .systemBarsPadding() - .background(color = TangemTheme.colors.background.secondary), - content = { padding -> - ListOfTokens(state = state, Modifier.padding(padding)) - }, - topBar = { - ExpandableSearchView( - title = stringResource(R.string.swapping_token_list_title), - onBackClick = onBack, - placeholderSearchText = stringResource(id = R.string.common_search_tokens), - onSearchChange = state.onSearchEntered, - onSearchDisplayClose = { state.onSearchEntered("") }, - onFocusChange = onSearchFocusChange, - subtitle = state.network.name, - icon = painterResource(id = getActiveIconRes(state.network.blockchainId)), - ) - }, - ) - } + Scaffold( + modifier = Modifier + .systemBarsPadding() + .background(color = TangemTheme.colors.background.secondary), + content = { padding -> + ListOfTokens(state = state, Modifier.padding(padding)) + }, + topBar = { + ExpandableSearchView( + title = stringResource(R.string.swapping_token_list_title), + onBackClick = onBack, + placeholderSearchText = stringResource(id = R.string.common_search_tokens), + onSearchChange = state.onSearchEntered, + onSearchDisplayClose = { state.onSearchEntered("") }, + onFocusChange = onSearchFocusChange, + subtitle = state.network.name, + icon = painterResource(id = getActiveIconRes(state.network.blockchainId)), + ) + }, + ) } @OptIn(ExperimentalFoundationApi::class) @@ -213,7 +210,7 @@ private fun TokenIcon(token: TokenToSelect, @DrawableRes iconPlaceholder: Int?) .crossfade(true) .build(), contentDescription = token.id, - loading = { TokenImageShimmer(modifier = iconModifier) }, + loading = { CircleShimmer(modifier = iconModifier) }, error = { CurrencyPlaceholderIcon(modifier = iconModifier, id = token.id) }, alpha = if (!token.available) 0.7f else 1f, colorFilter = colorFilter, @@ -221,20 +218,6 @@ private fun TokenIcon(token: TokenToSelect, @DrawableRes iconPlaceholder: Int?) } } -@Composable -private fun TokenImageShimmer(modifier: Modifier = Modifier) { - Box(modifier = modifier.shimmer()) { - Box( - modifier = Modifier - .matchParentSize() - .background( - color = TangemTheme.colors.button.secondary, - shape = CircleShape, - ), - ) - } -} - private val token = TokenToSelect( id = "", name = "USDC", diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapSuccessScreen.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapSuccessScreen.kt index 2d6f3e52b4..a05fbdb29b 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapSuccessScreen.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapSuccessScreen.kt @@ -18,34 +18,32 @@ import com.tangem.feature.swap.presentation.R @Composable fun SwapSuccessScreen(state: SwapSuccessStateHolder, onBack: () -> Unit) { - TangemTheme { - Scaffold( - modifier = Modifier.systemBarsPadding(), - content = { padding -> - ResultScreenContent( - resultMessage = makeSuccessMessage( - fromTokenAmount = state.fromTokenAmount, - toTokenAmount = state.toTokenAmount, - ), - resultColor = TangemTheme.colors.icon.attention, - onButtonClick = onBack, - icon = R.drawable.ic_clock_24, - secondaryButtonIcon = R.drawable.ic_arrow_top_right_24, - onSecondaryButtonClick = state.onSecondaryButtonClick, - secondaryButtonText = R.string.swapping_success_view_explorer_button_title, - title = R.string.swapping_success_view_title, - modifier = Modifier.padding(padding), - ) - }, - topBar = { - AppBarWithBackButton( - text = stringResource(R.string.common_swap), - onBackClick = onBack, - iconRes = R.drawable.ic_close_24, - ) - }, - ) - } + Scaffold( + modifier = Modifier.systemBarsPadding(), + content = { padding -> + ResultScreenContent( + resultMessage = makeSuccessMessage( + fromTokenAmount = state.fromTokenAmount, + toTokenAmount = state.toTokenAmount, + ), + resultColor = TangemTheme.colors.icon.attention, + onButtonClick = onBack, + icon = R.drawable.ic_clock_24, + secondaryButtonIcon = R.drawable.ic_arrow_top_right_24, + onSecondaryButtonClick = state.onSecondaryButtonClick, + secondaryButtonText = R.string.swapping_success_view_explorer_button_title, + title = R.string.swapping_success_view_title, + modifier = Modifier.padding(padding), + ) + }, + topBar = { + AppBarWithBackButton( + text = stringResource(R.string.common_swap), + onBackClick = onBack, + iconRes = R.drawable.ic_close_24, + ) + }, + ) } @Composable diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/TransactionCard.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/TransactionCard.kt index 761a8f9448..be8d01441a 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/TransactionCard.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/TransactionCard.kt @@ -5,18 +5,7 @@ import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.defaultMinSize -import androidx.compose.foundation.layout.fillMaxHeight -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.Card @@ -43,16 +32,10 @@ import androidx.compose.ui.unit.sp import coil.compose.SubcomposeAsyncImage import coil.request.ImageRequest import com.tangem.core.ui.R -import com.tangem.core.ui.components.FontSizeRange -import com.tangem.core.ui.components.ResizableText -import com.tangem.core.ui.components.SpacerH4 -import com.tangem.core.ui.components.SpacerH8 -import com.tangem.core.ui.components.SpacerW16 -import com.tangem.core.ui.components.SpacerW4 +import com.tangem.core.ui.components.* import com.tangem.core.ui.res.TangemTheme import com.tangem.feature.swap.models.SwapWarning import com.tangem.feature.swap.models.TransactionCardType -import com.valentinilk.shimmer.shimmer @Suppress("LongParameterList") @Composable @@ -156,14 +139,11 @@ private fun Header(type: TransactionCardType, balance: String, modifier: Modifie .padding(top = TangemTheme.dimens.spacing2), ) } else { - Box( + RectangleShimmer( modifier = Modifier - .size(width = TangemTheme.dimens.size80, height = TangemTheme.dimens.size12) - .shimmer() - .background( - color = TangemTheme.colors.button.secondary, - shape = RoundedCornerShape(TangemTheme.dimens.radius3), - ), + .width(TangemTheme.dimens.size80) + .height(TangemTheme.dimens.size12), + radius = TangemTheme.dimens.radius3, ) } } @@ -206,15 +186,11 @@ private fun Content( modifier = sumTextModifier, ) } else { - Box( + RectangleShimmer( modifier = Modifier .padding(vertical = TangemTheme.dimens.spacing4) - .size(width = TangemTheme.dimens.size102, height = TangemTheme.dimens.size24) - .shimmer() - .background( - color = TangemTheme.colors.button.secondary, - shape = RoundedCornerShape(TangemTheme.dimens.radius6), - ), + .width(TangemTheme.dimens.size102) + .height(TangemTheme.dimens.size24), ) } } @@ -260,15 +236,12 @@ private fun Content( ) } } else { - Box( + RectangleShimmer( modifier = Modifier .padding(vertical = TangemTheme.dimens.spacing4) - .size(width = TangemTheme.dimens.size40, height = TangemTheme.dimens.size12) - .shimmer() - .background( - color = TangemTheme.colors.button.secondary, - shape = RoundedCornerShape(TangemTheme.dimens.radius3), - ), + .width(TangemTheme.dimens.size40) + .height(TangemTheme.dimens.size12), + radius = TangemTheme.dimens.radius3, ) } } @@ -310,8 +283,7 @@ fun Token( .data(data) .crossfade(true) .build(), - loading = { TokenImageShimmer(modifier = tokenImageModifier) }, - // error = { CurrencyPlaceholderIcon(modifier = tokenImageModifier, tokenCurrency) }, + loading = { CircleShimmer(modifier = tokenImageModifier) }, contentDescription = tokenCurrency, ) @@ -378,20 +350,6 @@ private fun makePriceImpactBalanceWarning(value: String, priceImpactPercents: In } } -@Composable -private fun TokenImageShimmer(modifier: Modifier = Modifier) { - Box(modifier = modifier.shimmer()) { - Box( - modifier = Modifier - .matchParentSize() - .background( - color = TangemTheme.colors.button.secondary, - shape = CircleShape, - ), - ) - } -} - // region preview @Preview(widthDp = 328, heightDp = 116, showBackground = true) From e76414cc06b47d19b860ed21086d292ad5e6699c Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 27 Sep 2023 13:03:12 +0500 Subject: [PATCH 098/242] Updated on 2026-08-14 --- .../ui/components/transactions/Transaction.kt | 55 +++++++++----- .../transactions/state/TransactionState.kt | 43 ++++++----- ...ilsTxHistoryToTransactionStateConverter.kt | 72 ++++++++++--------- ...tailsTxHistoryTransactionStateConverter.kt | 72 ++++++++++--------- .../presentation/common/WalletPreviewData.kt | 6 +- ...alletTxHistoryTransactionStateConverter.kt | 72 ++++++++++--------- 6 files changed, 171 insertions(+), 149 deletions(-) diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/transactions/Transaction.kt b/core/ui/src/main/java/com/tangem/core/ui/components/transactions/Transaction.kt index a98279f80f..66ab911a07 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/transactions/Transaction.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/transactions/Transaction.kt @@ -23,7 +23,8 @@ import com.tangem.core.ui.R import com.tangem.core.ui.components.CircleShimmer import com.tangem.core.ui.components.RectangleShimmer import com.tangem.core.ui.components.transactions.state.TransactionState -import com.tangem.core.ui.components.transactions.state.TransactionState.Content.* +import com.tangem.core.ui.components.transactions.state.TransactionState.Content.Status +import com.tangem.core.ui.components.transactions.state.TransactionState.Content.Direction import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.res.TangemTheme @@ -131,8 +132,12 @@ private fun Icon(state: TransactionState, modifier: Modifier = Modifier) { Icon( painter = painterResource( id = when (state) { - is TransactionState.Send -> R.drawable.ic_arrow_up_24 - is TransactionState.Receive -> R.drawable.ic_arrow_down_24 + is TransactionState.Transfer -> { + when (state.direction) { + Direction.OUTGOING -> R.drawable.ic_arrow_up_24 + Direction.INCOMING -> R.drawable.ic_arrow_down_24 + } + } is TransactionState.Approve -> R.drawable.ic_doc_24 is TransactionState.Swap -> R.drawable.ic_exchange_vertical_24 is TransactionState.Custom -> R.drawable.ic_exchange_vertical_24 @@ -175,8 +180,7 @@ private fun Title(state: TransactionState, modifier: Modifier = Modifier) { Text( text = when (state) { is TransactionState.Approve -> stringResource(R.string.common_approval) - is TransactionState.Receive -> stringResource(R.string.common_transfer) - is TransactionState.Send -> stringResource(R.string.common_transfer) + is TransactionState.Transfer -> stringResource(R.string.common_transfer) is TransactionState.Swap -> stringResource(R.string.common_swap) is TransactionState.Custom -> state.title.resolveReference() }, @@ -212,11 +216,18 @@ private fun Subtitle(state: TransactionState, modifier: Modifier = Modifier) { is TransactionState.Content -> { Text( text = when (state) { - is TransactionState.Send -> stringResource( - id = R.string.transaction_history_transaction_to_address, - state.address.resolveReference(), - ) - is TransactionState.Receive, + is TransactionState.Transfer -> { + when (state.direction) { + Direction.OUTGOING -> stringResource( + id = R.string.transaction_history_transaction_to_address, + state.address.resolveReference(), + ) + Direction.INCOMING -> stringResource( + id = R.string.transaction_history_transaction_from_address, + state.address.resolveReference(), + ) + } + } is TransactionState.Approve, -> stringResource( id = R.string.transaction_history_transaction_from_address, @@ -227,10 +238,9 @@ private fun Subtitle(state: TransactionState, modifier: Modifier = Modifier) { state.address.resolveReference(), ) is TransactionState.Custom -> stringResource( - id = if (state.isIncoming) { - R.string.transaction_history_transaction_from_address - } else { - R.string.transaction_history_transaction_to_address + id = when (state.direction) { + Direction.OUTGOING -> R.string.transaction_history_transaction_to_address + Direction.INCOMING -> R.string.transaction_history_transaction_from_address }, state.subtitle.resolveReference(), ) @@ -262,7 +272,10 @@ private fun Amount(state: TransactionState, modifier: Modifier = Modifier) { text = state.amount, modifier = modifier, textAlign = TextAlign.End, - color = TangemTheme.colors.text.primary1, + color = when (state.direction) { + Direction.INCOMING -> TangemTheme.colors.text.accent + Direction.OUTGOING -> TangemTheme.colors.text.primary1 + }, style = TangemTheme.typography.body2, ) } @@ -336,19 +349,21 @@ private fun Preview_TransactionItem_DarkTheme( private class TransactionItemStateProvider : CollectionPreviewParameterProvider( collection = listOf( - TransactionState.Send( + TransactionState.Transfer( txHash = UUID.randomUUID().toString(), address = TextReference.Str("33BddS...ga2B"), amount = "-0.500913 BTC", timestamp = "8:41", status = Status.Confirmed, + direction = Direction.OUTGOING, ), - TransactionState.Receive( + TransactionState.Transfer( txHash = UUID.randomUUID().toString(), address = TextReference.Str("33BddS...ga2B"), amount = "+0.500913 BTC", timestamp = "8:41", status = Status.Unconfirmed, + direction = Direction.INCOMING, ), TransactionState.Approve( txHash = UUID.randomUUID().toString(), @@ -356,6 +371,7 @@ private class TransactionItemStateProvider : CollectionPreviewParameterProvider< amount = "+0.500913 BTC", timestamp = "8:41", status = Status.Failed, + direction = Direction.OUTGOING, ), TransactionState.Swap( txHash = UUID.randomUUID().toString(), @@ -363,6 +379,7 @@ private class TransactionItemStateProvider : CollectionPreviewParameterProvider< amount = "+0.500913 BTC", timestamp = "8:41", status = Status.Unconfirmed, + direction = Direction.INCOMING, ), TransactionState.Custom( txHash = UUID.randomUUID().toString(), @@ -370,9 +387,9 @@ private class TransactionItemStateProvider : CollectionPreviewParameterProvider< amount = "+0.500913 BTC", timestamp = "8:41", status = Status.Confirmed, + direction = Direction.INCOMING, title = TextReference.Str("Submit"), subtitle = TextReference.Str("33BddS...ga2B"), - isIncoming = true, ), TransactionState.Custom( txHash = UUID.randomUUID().toString(), @@ -380,9 +397,9 @@ private class TransactionItemStateProvider : CollectionPreviewParameterProvider< amount = "+0.500913 BTC", timestamp = "8:41", status = Status.Confirmed, + direction = Direction.OUTGOING, title = TextReference.Str("Submit"), subtitle = TextReference.Str("33BddS...ga2B"), - isIncoming = false, ), TransactionState.Loading(txHash = UUID.randomUUID().toString()), ), diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/transactions/state/TransactionState.kt b/core/ui/src/main/java/com/tangem/core/ui/components/transactions/state/TransactionState.kt index 5335144a70..ad3853ceea 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/transactions/state/TransactionState.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/transactions/state/TransactionState.kt @@ -19,6 +19,7 @@ sealed interface TransactionState { * @property amount amount * @property timestamp timestamp * @property status transaction status + * @property direction transaction direction */ sealed class Content : TransactionState { @@ -26,6 +27,7 @@ sealed interface TransactionState { abstract val amount: String abstract val timestamp: String abstract val status: Status + abstract val direction: Direction fun copySealed( txHash: String = this.txHash, @@ -33,13 +35,13 @@ sealed interface TransactionState { amount: String = this.amount, timestamp: String = this.timestamp, status: Status = this.status, + direction: Direction = this.direction, ): Content { return when (this) { - is Approve -> copy(txHash, address, amount, timestamp, status) - is Receive -> copy(txHash, address, amount, timestamp, status) - is Send -> copy(txHash, address, amount, timestamp, status) - is Swap -> copy(txHash, address, amount, timestamp, status) - is Custom -> copy(txHash, address, amount, timestamp, status) + is Approve -> copy(txHash, address, amount, timestamp, status, direction) + is Transfer -> copy(txHash, address, amount, timestamp, status, direction) + is Swap -> copy(txHash, address, amount, timestamp, status, direction) + is Custom -> copy(txHash, address, amount, timestamp, status, direction) } } @@ -48,6 +50,11 @@ sealed interface TransactionState { object Confirmed : Status() object Unconfirmed : Status() } + + enum class Direction { + INCOMING, + OUTGOING, + } } /** @@ -57,29 +64,15 @@ sealed interface TransactionState { * @property address address * @property amount amount * @property timestamp timestamp + * @property direction transaction direction */ - data class Send( - override val txHash: String, - override val address: TextReference, - override val amount: String, - override val timestamp: String, - override val status: Status, - ) : Content() - - /** - * Completed receiving transaction state - * - * @property txHash transaction hash - * @property address address - * @property amount amount - * @property timestamp timestamp - */ - data class Receive( + data class Transfer( override val txHash: String, override val address: TextReference, override val amount: String, override val timestamp: String, override val status: Status, + override val direction: Direction, ) : Content() /** @@ -89,6 +82,7 @@ sealed interface TransactionState { * @property address address * @property amount amount * @property timestamp timestamp + * @property direction transaction direction */ data class Approve( override val txHash: String, @@ -96,6 +90,7 @@ sealed interface TransactionState { override val amount: String, override val timestamp: String, override val status: Status, + override val direction: Direction, ) : Content() /** @@ -105,6 +100,7 @@ sealed interface TransactionState { * @property address address * @property amount amount * @property timestamp timestamp + * @property direction transaction direction */ data class Swap( override val txHash: String, @@ -112,6 +108,7 @@ sealed interface TransactionState { override val amount: String, override val timestamp: String, override val status: Status, + override val direction: Direction, ) : Content() data class Custom( @@ -120,9 +117,9 @@ sealed interface TransactionState { override val amount: String, override val timestamp: String, override val status: Status, + override val direction: Direction, val title: TextReference, val subtitle: TextReference, - val isIncoming: Boolean, ) : Content() /** diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/txhistory/TokenDetailsTxHistoryToTransactionStateConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/txhistory/TokenDetailsTxHistoryToTransactionStateConverter.kt index 74df541b04..2b8eaca2ad 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/txhistory/TokenDetailsTxHistoryToTransactionStateConverter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/txhistory/TokenDetailsTxHistoryToTransactionStateConverter.kt @@ -10,7 +10,6 @@ import com.tangem.utils.toBriefAddressFormat import com.tangem.utils.toFormattedCurrencyString import org.joda.time.DateTime import org.joda.time.DateTimeZone -import java.math.BigDecimal internal class TokenDetailsTxHistoryToTransactionStateConverter( private val symbol: String, @@ -30,108 +29,98 @@ internal class TokenDetailsTxHistoryToTransactionStateConverter( TxHistoryItem.TransactionType.Deposit -> TransactionState.Custom( txHash = item.txHash, address = item.direction.extractAddress(), - amount = item.amount.toCryptoCurrencyFormat(), + amount = item.extractFormattedCryptoBalance(), timestamp = item.timestampInMillis.toTimeFormat(), status = item.status.tiUiStatus(), + direction = item.direction.toUiDirection(), title = TextReference.Str("Deposit"), subtitle = item.direction.extractAddress(), - isIncoming = item.direction is TxHistoryItem.TransactionDirection.Incoming, ) TxHistoryItem.TransactionType.Submit -> TransactionState.Custom( txHash = item.txHash, address = item.direction.extractAddress(), - amount = item.amount.toCryptoCurrencyFormat(), + amount = item.extractFormattedCryptoBalance(), timestamp = item.timestampInMillis.toTimeFormat(), status = item.status.tiUiStatus(), + direction = item.direction.toUiDirection(), title = TextReference.Str("Submit"), subtitle = item.direction.extractAddress(), - isIncoming = item.direction is TxHistoryItem.TransactionDirection.Incoming, ) TxHistoryItem.TransactionType.Supply -> TransactionState.Custom( txHash = item.txHash, address = item.direction.extractAddress(), - amount = item.amount.toCryptoCurrencyFormat(), + amount = item.extractFormattedCryptoBalance(), timestamp = item.timestampInMillis.toTimeFormat(), status = item.status.tiUiStatus(), + direction = item.direction.toUiDirection(), title = TextReference.Str("Supply"), subtitle = item.direction.extractAddress(), - isIncoming = item.direction is TxHistoryItem.TransactionDirection.Incoming, ) TxHistoryItem.TransactionType.Unoswap -> TransactionState.Custom( txHash = item.txHash, address = item.direction.extractAddress(), - amount = item.amount.toCryptoCurrencyFormat(), + amount = item.extractFormattedCryptoBalance(), timestamp = item.timestampInMillis.toTimeFormat(), status = item.status.tiUiStatus(), + direction = item.direction.toUiDirection(), title = TextReference.Str("Unoswap"), subtitle = item.direction.extractAddress(), - isIncoming = item.direction is TxHistoryItem.TransactionDirection.Incoming, ) TxHistoryItem.TransactionType.Withdraw -> TransactionState.Custom( txHash = item.txHash, address = item.direction.extractAddress(), - amount = item.amount.toCryptoCurrencyFormat(), + amount = item.extractFormattedCryptoBalance(), timestamp = item.timestampInMillis.toTimeFormat(), status = item.status.tiUiStatus(), + direction = item.direction.toUiDirection(), title = TextReference.Str("Withdraw"), subtitle = item.direction.extractAddress(), - isIncoming = item.direction is TxHistoryItem.TransactionDirection.Incoming, ) is TxHistoryItem.TransactionType.Custom -> TransactionState.Custom( txHash = item.txHash, address = item.direction.extractAddress(), - amount = item.amount.toCryptoCurrencyFormat(), + amount = item.extractFormattedCryptoBalance(), timestamp = item.timestampInMillis.toTimeFormat(), status = item.status.tiUiStatus(), + direction = item.direction.toUiDirection(), title = TextReference.Str(type.id), subtitle = item.direction.extractAddress(), - isIncoming = item.direction is TxHistoryItem.TransactionDirection.Incoming, ) } } private fun mapTransfer(item: TxHistoryItem): TransactionState { - return when (item.direction) { - is TxHistoryItem.TransactionDirection.Incoming -> TransactionState.Receive( - txHash = item.txHash, - address = item.direction.extractAddress(), - amount = item.amount.toCryptoCurrencyFormat(), - timestamp = item.timestampInMillis.toTimeFormat(), - status = item.status.tiUiStatus(), - ) - is TxHistoryItem.TransactionDirection.Outgoing -> TransactionState.Send( - txHash = item.txHash, - address = item.direction.extractAddress(), - amount = item.amount.toCryptoCurrencyFormat(), - timestamp = item.timestampInMillis.toTimeFormat(), - status = item.status.tiUiStatus(), - ) - } + return TransactionState.Transfer( + txHash = item.txHash, + address = item.direction.extractAddress(), + amount = item.extractFormattedCryptoBalance(), + timestamp = item.timestampInMillis.toTimeFormat(), + status = item.status.tiUiStatus(), + direction = item.direction.toUiDirection(), + ) } private fun mapApprove(item: TxHistoryItem): TransactionState { return TransactionState.Approve( txHash = item.txHash, address = item.direction.extractAddress(), - amount = item.amount.toCryptoCurrencyFormat(), + amount = item.extractFormattedCryptoBalance(), timestamp = item.timestampInMillis.toTimeFormat(), status = item.status.tiUiStatus(), + direction = item.direction.toUiDirection(), ) } private fun mapSwap(item: TxHistoryItem): TransactionState { return TransactionState.Swap( txHash = item.txHash, address = item.direction.extractAddress(), - amount = item.amount.toCryptoCurrencyFormat(), + amount = item.extractFormattedCryptoBalance(), timestamp = item.timestampInMillis.toTimeFormat(), status = item.status.tiUiStatus(), + direction = item.direction.toUiDirection(), ) } - private fun BigDecimal.toCryptoCurrencyFormat(): String { - return toFormattedCurrencyString(currency = symbol, decimals = decimals) - } - private fun TxHistoryItem.TransactionDirection.extractAddress(): TextReference = when (val addr = address) { TxHistoryItem.Address.Multiple -> TextReference.Res(R.string.transaction_history_multiple_addresses) is TxHistoryItem.Address.Single -> TextReference.Str(addr.rawAddress.toBriefAddressFormat()) @@ -146,4 +135,17 @@ internal class TokenDetailsTxHistoryToTransactionStateConverter( private fun Long.toTimeFormat(): String { return DateTimeFormatters.formatTime(time = DateTime(this, DateTimeZone.getDefault())) } + + private fun TxHistoryItem.TransactionDirection.toUiDirection() = when (this) { + is TxHistoryItem.TransactionDirection.Incoming -> TransactionState.Content.Direction.INCOMING + is TxHistoryItem.TransactionDirection.Outgoing -> TransactionState.Content.Direction.OUTGOING + } + + private fun TxHistoryItem.extractFormattedCryptoBalance(): String { + val prefix = when (direction) { + is TxHistoryItem.TransactionDirection.Incoming -> "+" + is TxHistoryItem.TransactionDirection.Outgoing -> "-" + } + return prefix + amount.toFormattedCurrencyString(currency = symbol, decimals = decimals) + } } \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/txhistory/TokenDetailsTxHistoryTransactionStateConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/txhistory/TokenDetailsTxHistoryTransactionStateConverter.kt index d5e78350e5..be32a72923 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/txhistory/TokenDetailsTxHistoryTransactionStateConverter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/txhistory/TokenDetailsTxHistoryTransactionStateConverter.kt @@ -7,7 +7,6 @@ import com.tangem.features.tokendetails.impl.R import com.tangem.utils.converter.Converter import com.tangem.utils.toBriefAddressFormat import com.tangem.utils.toFormattedCurrencyString -import java.math.BigDecimal internal class TokenDetailsTxHistoryTransactionStateConverter( private val symbol: String, @@ -27,101 +26,95 @@ internal class TokenDetailsTxHistoryTransactionStateConverter( TxHistoryItem.TransactionType.Deposit -> TransactionState.Custom( txHash = item.txHash, address = item.direction.extractAddress(), - amount = item.amount.toCryptoCurrencyFormat(), + amount = item.extractFormattedCryptoBalance(), timestamp = item.getRawTimestamp(), status = item.status.tiUiStatus(), + direction = item.direction.toUiDirection(), title = TextReference.Str("Deposit"), subtitle = item.direction.extractAddress(), - isIncoming = item.direction is TxHistoryItem.TransactionDirection.Incoming, ) TxHistoryItem.TransactionType.Submit -> TransactionState.Custom( txHash = item.txHash, address = item.direction.extractAddress(), - amount = item.amount.toCryptoCurrencyFormat(), + amount = item.extractFormattedCryptoBalance(), timestamp = item.getRawTimestamp(), status = item.status.tiUiStatus(), + direction = item.direction.toUiDirection(), title = TextReference.Str("Submit"), subtitle = item.direction.extractAddress(), - isIncoming = item.direction is TxHistoryItem.TransactionDirection.Incoming, ) TxHistoryItem.TransactionType.Supply -> TransactionState.Custom( txHash = item.txHash, address = item.direction.extractAddress(), - amount = item.amount.toCryptoCurrencyFormat(), + amount = item.extractFormattedCryptoBalance(), timestamp = item.getRawTimestamp(), status = item.status.tiUiStatus(), + direction = item.direction.toUiDirection(), title = TextReference.Str("Supply"), subtitle = item.direction.extractAddress(), - isIncoming = item.direction is TxHistoryItem.TransactionDirection.Incoming, ) TxHistoryItem.TransactionType.Unoswap -> TransactionState.Custom( txHash = item.txHash, address = item.direction.extractAddress(), - amount = item.amount.toCryptoCurrencyFormat(), + amount = item.extractFormattedCryptoBalance(), timestamp = item.getRawTimestamp(), status = item.status.tiUiStatus(), + direction = item.direction.toUiDirection(), title = TextReference.Str("Unoswap"), subtitle = item.direction.extractAddress(), - isIncoming = item.direction is TxHistoryItem.TransactionDirection.Incoming, ) TxHistoryItem.TransactionType.Withdraw -> TransactionState.Custom( txHash = item.txHash, address = item.direction.extractAddress(), - amount = item.amount.toCryptoCurrencyFormat(), + amount = item.extractFormattedCryptoBalance(), timestamp = item.getRawTimestamp(), status = item.status.tiUiStatus(), + direction = item.direction.toUiDirection(), title = TextReference.Str("Withdraw"), subtitle = item.direction.extractAddress(), - isIncoming = item.direction is TxHistoryItem.TransactionDirection.Incoming, ) is TxHistoryItem.TransactionType.Custom -> TransactionState.Custom( txHash = item.txHash, address = item.direction.extractAddress(), - amount = item.amount.toCryptoCurrencyFormat(), + amount = item.extractFormattedCryptoBalance(), timestamp = item.getRawTimestamp(), status = item.status.tiUiStatus(), + direction = item.direction.toUiDirection(), title = TextReference.Str(type.id), subtitle = item.direction.extractAddress(), - isIncoming = item.direction is TxHistoryItem.TransactionDirection.Incoming, ) } } private fun mapTransfer(item: TxHistoryItem): TransactionState { - return when (item.direction) { - is TxHistoryItem.TransactionDirection.Incoming -> TransactionState.Receive( - txHash = item.txHash, - address = item.direction.extractAddress(), - amount = item.amount.toCryptoCurrencyFormat(), - timestamp = item.getRawTimestamp(), - status = item.status.tiUiStatus(), - ) - is TxHistoryItem.TransactionDirection.Outgoing -> TransactionState.Send( - txHash = item.txHash, - address = item.direction.extractAddress(), - amount = item.amount.toCryptoCurrencyFormat(), - timestamp = item.getRawTimestamp(), - status = item.status.tiUiStatus(), - ) - } + return TransactionState.Transfer( + txHash = item.txHash, + address = item.direction.extractAddress(), + amount = item.extractFormattedCryptoBalance(), + timestamp = item.getRawTimestamp(), + status = item.status.tiUiStatus(), + direction = item.direction.toUiDirection(), + ) } private fun mapApprove(item: TxHistoryItem): TransactionState { return TransactionState.Approve( txHash = item.txHash, address = item.direction.extractAddress(), - amount = item.amount.toCryptoCurrencyFormat(), + amount = item.extractFormattedCryptoBalance(), timestamp = item.getRawTimestamp(), status = item.status.tiUiStatus(), + direction = item.direction.toUiDirection(), ) } private fun mapSwap(item: TxHistoryItem): TransactionState { return TransactionState.Swap( txHash = item.txHash, address = item.direction.extractAddress(), - amount = item.amount.toCryptoCurrencyFormat(), + amount = item.extractFormattedCryptoBalance(), timestamp = item.getRawTimestamp(), status = item.status.tiUiStatus(), + direction = item.direction.toUiDirection(), ) } @@ -133,10 +126,6 @@ internal class TokenDetailsTxHistoryTransactionStateConverter( */ private fun TxHistoryItem.getRawTimestamp() = this.timestampInMillis.toString() - private fun BigDecimal.toCryptoCurrencyFormat(): String { - return toFormattedCurrencyString(currency = symbol, decimals = decimals) - } - private fun TxHistoryItem.TransactionDirection.extractAddress(): TextReference = when (val addr = address) { TxHistoryItem.Address.Multiple -> TextReference.Res(R.string.transaction_history_multiple_addresses) is TxHistoryItem.Address.Single -> TextReference.Str(addr.rawAddress.toBriefAddressFormat()) @@ -147,4 +136,17 @@ internal class TokenDetailsTxHistoryTransactionStateConverter( TxHistoryItem.TransactionStatus.Failed -> TransactionState.Content.Status.Failed TxHistoryItem.TransactionStatus.Unconfirmed -> TransactionState.Content.Status.Unconfirmed } + + private fun TxHistoryItem.TransactionDirection.toUiDirection() = when (this) { + is TxHistoryItem.TransactionDirection.Incoming -> TransactionState.Content.Direction.INCOMING + is TxHistoryItem.TransactionDirection.Outgoing -> TransactionState.Content.Direction.OUTGOING + } + + private fun TxHistoryItem.extractFormattedCryptoBalance(): String { + val prefix = when (direction) { + is TxHistoryItem.TransactionDirection.Incoming -> "+" + is TxHistoryItem.TransactionDirection.Outgoing -> "-" + } + return prefix + amount.toFormattedCurrencyString(currency = symbol, decimals = decimals) + } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/WalletPreviewData.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/WalletPreviewData.kt index c782a3c61a..1c6883653b 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/WalletPreviewData.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/WalletPreviewData.kt @@ -429,22 +429,24 @@ internal object WalletPreviewData { listOf( TxHistoryState.TxHistoryItemState.GroupTitle("Today"), TxHistoryState.TxHistoryItemState.Transaction( - TransactionState.Send( + TransactionState.Transfer( txHash = UUID.randomUUID().toString(), address = TextReference.Str("33BddS...ga2B"), amount = "-0.500913 BTC", timestamp = "8:41", status = TransactionState.Content.Status.Unconfirmed, + direction = TransactionState.Content.Direction.OUTGOING, ), ), TxHistoryState.TxHistoryItemState.GroupTitle("Yesterday"), TxHistoryState.TxHistoryItemState.Transaction( - TransactionState.Send( + TransactionState.Transfer( txHash = UUID.randomUUID().toString(), address = TextReference.Str("33BddS...ga2B"), amount = "-0.500913 BTC", timestamp = "8:41", status = TransactionState.Content.Status.Confirmed, + direction = TransactionState.Content.Direction.OUTGOING, ), ), ), diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/txhistory/WalletTxHistoryTransactionStateConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/txhistory/WalletTxHistoryTransactionStateConverter.kt index ca23784729..44dedce7b4 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/txhistory/WalletTxHistoryTransactionStateConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/txhistory/WalletTxHistoryTransactionStateConverter.kt @@ -7,7 +7,6 @@ import com.tangem.feature.wallet.impl.R import com.tangem.utils.converter.Converter import com.tangem.utils.toBriefAddressFormat import com.tangem.utils.toFormattedCurrencyString -import java.math.BigDecimal class WalletTxHistoryTransactionStateConverter( private val symbol: String, @@ -27,101 +26,95 @@ class WalletTxHistoryTransactionStateConverter( TxHistoryItem.TransactionType.Deposit -> TransactionState.Custom( txHash = item.txHash, address = item.direction.extractAddress(), - amount = item.amount.toCryptoCurrencyFormat(), + amount = item.extractFormattedCryptoBalance(), timestamp = item.getRawTimestamp(), status = item.status.tiUiStatus(), + direction = item.direction.toUiDirection(), title = TextReference.Str("Deposit"), subtitle = item.direction.extractAddress(), - isIncoming = item.direction is TxHistoryItem.TransactionDirection.Incoming, ) TxHistoryItem.TransactionType.Submit -> TransactionState.Custom( txHash = item.txHash, address = item.direction.extractAddress(), - amount = item.amount.toCryptoCurrencyFormat(), + amount = item.extractFormattedCryptoBalance(), timestamp = item.getRawTimestamp(), status = item.status.tiUiStatus(), + direction = item.direction.toUiDirection(), title = TextReference.Str("Submit"), subtitle = item.direction.extractAddress(), - isIncoming = item.direction is TxHistoryItem.TransactionDirection.Incoming, ) TxHistoryItem.TransactionType.Supply -> TransactionState.Custom( txHash = item.txHash, address = item.direction.extractAddress(), - amount = item.amount.toCryptoCurrencyFormat(), + amount = item.extractFormattedCryptoBalance(), timestamp = item.getRawTimestamp(), status = item.status.tiUiStatus(), + direction = item.direction.toUiDirection(), title = TextReference.Str("Supply"), subtitle = item.direction.extractAddress(), - isIncoming = item.direction is TxHistoryItem.TransactionDirection.Incoming, ) TxHistoryItem.TransactionType.Unoswap -> TransactionState.Custom( txHash = item.txHash, address = item.direction.extractAddress(), - amount = item.amount.toCryptoCurrencyFormat(), + amount = item.extractFormattedCryptoBalance(), timestamp = item.getRawTimestamp(), status = item.status.tiUiStatus(), + direction = item.direction.toUiDirection(), title = TextReference.Str("Unoswap"), subtitle = item.direction.extractAddress(), - isIncoming = item.direction is TxHistoryItem.TransactionDirection.Incoming, ) TxHistoryItem.TransactionType.Withdraw -> TransactionState.Custom( txHash = item.txHash, address = item.direction.extractAddress(), - amount = item.amount.toCryptoCurrencyFormat(), + amount = item.extractFormattedCryptoBalance(), timestamp = item.getRawTimestamp(), status = item.status.tiUiStatus(), + direction = item.direction.toUiDirection(), title = TextReference.Str("Withdraw"), subtitle = item.direction.extractAddress(), - isIncoming = item.direction is TxHistoryItem.TransactionDirection.Incoming, ) is TxHistoryItem.TransactionType.Custom -> TransactionState.Custom( txHash = item.txHash, address = item.direction.extractAddress(), - amount = item.amount.toCryptoCurrencyFormat(), + amount = item.extractFormattedCryptoBalance(), timestamp = item.getRawTimestamp(), status = item.status.tiUiStatus(), + direction = item.direction.toUiDirection(), title = TextReference.Str(type.id), subtitle = item.direction.extractAddress(), - isIncoming = item.direction is TxHistoryItem.TransactionDirection.Incoming, ) } } private fun mapTransfer(item: TxHistoryItem): TransactionState { - return when (item.direction) { - is TxHistoryItem.TransactionDirection.Incoming -> TransactionState.Receive( - txHash = item.txHash, - address = item.direction.extractAddress(), - amount = item.amount.toCryptoCurrencyFormat(), - timestamp = item.getRawTimestamp(), - status = item.status.tiUiStatus(), - ) - is TxHistoryItem.TransactionDirection.Outgoing -> TransactionState.Send( - txHash = item.txHash, - address = item.direction.extractAddress(), - amount = item.amount.toCryptoCurrencyFormat(), - timestamp = item.getRawTimestamp(), - status = item.status.tiUiStatus(), - ) - } + return TransactionState.Transfer( + txHash = item.txHash, + address = item.direction.extractAddress(), + amount = item.extractFormattedCryptoBalance(), + timestamp = item.getRawTimestamp(), + status = item.status.tiUiStatus(), + direction = item.direction.toUiDirection(), + ) } private fun mapApprove(item: TxHistoryItem): TransactionState { return TransactionState.Approve( txHash = item.txHash, address = item.direction.extractAddress(), - amount = item.amount.toCryptoCurrencyFormat(), + amount = item.extractFormattedCryptoBalance(), timestamp = item.getRawTimestamp(), status = item.status.tiUiStatus(), + direction = item.direction.toUiDirection(), ) } private fun mapSwap(item: TxHistoryItem): TransactionState { return TransactionState.Swap( txHash = item.txHash, address = item.direction.extractAddress(), - amount = item.amount.toCryptoCurrencyFormat(), + amount = item.extractFormattedCryptoBalance(), timestamp = item.getRawTimestamp(), status = item.status.tiUiStatus(), + direction = item.direction.toUiDirection(), ) } @@ -133,10 +126,6 @@ class WalletTxHistoryTransactionStateConverter( */ private fun TxHistoryItem.getRawTimestamp() = this.timestampInMillis.toString() - private fun BigDecimal.toCryptoCurrencyFormat(): String { - return toFormattedCurrencyString(currency = symbol, decimals = decimals) - } - private fun TxHistoryItem.TransactionDirection.extractAddress(): TextReference = when (val addr = address) { TxHistoryItem.Address.Multiple -> TextReference.Res(R.string.transaction_history_multiple_addresses) is TxHistoryItem.Address.Single -> TextReference.Str(addr.rawAddress.toBriefAddressFormat()) @@ -147,4 +136,17 @@ class WalletTxHistoryTransactionStateConverter( TxHistoryItem.TransactionStatus.Failed -> TransactionState.Content.Status.Failed TxHistoryItem.TransactionStatus.Unconfirmed -> TransactionState.Content.Status.Unconfirmed } + + private fun TxHistoryItem.TransactionDirection.toUiDirection() = when (this) { + is TxHistoryItem.TransactionDirection.Incoming -> TransactionState.Content.Direction.INCOMING + is TxHistoryItem.TransactionDirection.Outgoing -> TransactionState.Content.Direction.OUTGOING + } + + private fun TxHistoryItem.extractFormattedCryptoBalance(): String { + val prefix = when (direction) { + is TxHistoryItem.TransactionDirection.Incoming -> "+" + is TxHistoryItem.TransactionDirection.Outgoing -> "-" + } + return prefix + amount.toFormattedCurrencyString(currency = symbol, decimals = decimals) + } } \ No newline at end of file From f3f29813e0ec5597c8f7e5026bcd43838a36ace8 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 27 Sep 2023 11:16:22 +0300 Subject: [PATCH 099/242] Updated on 2026-08-14 --- .../handlers/BuyCurrencyIntentHandler.kt | 4 +- .../tokenreceive/TokenReceiveBottomSheet.kt | 2 + .../TokenReceiveBottomSheetConfig.kt | 2 + domain/tokens/models/build.gradle.kts | 1 + .../analytics/TokenReceiveAnalyticsEvent.kt | 19 ++++++ features/tokendetails/impl/build.gradle.kts | 2 + .../analytics/TokenScreenEvent.kt | 63 +++++++++++++++++++ .../state/factory/TokenDetailsStateFactory.kt | 9 ++- .../viewmodels/TokenDetailsViewModel.kt | 27 +++++++- .../wallet/viewmodels/WalletViewModel.kt | 7 +++ 10 files changed, 132 insertions(+), 4 deletions(-) create mode 100644 domain/tokens/models/src/main/java/com/tangem/domain/tokens/models/analytics/TokenReceiveAnalyticsEvent.kt create mode 100644 features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/analytics/TokenScreenEvent.kt diff --git a/app/src/main/java/com/tangem/tap/features/intentHandler/handlers/BuyCurrencyIntentHandler.kt b/app/src/main/java/com/tangem/tap/features/intentHandler/handlers/BuyCurrencyIntentHandler.kt index 39dda7cd24..83c294bfb3 100644 --- a/app/src/main/java/com/tangem/tap/features/intentHandler/handlers/BuyCurrencyIntentHandler.kt +++ b/app/src/main/java/com/tangem/tap/features/intentHandler/handlers/BuyCurrencyIntentHandler.kt @@ -3,8 +3,8 @@ package com.tangem.tap.features.intentHandler.handlers import android.content.Intent import android.net.Uri import com.tangem.core.analytics.Analytics +import com.tangem.feature.tokendetails.presentation.tokendetails.analytics.TokenScreenEvent import com.tangem.tap.common.analytics.events.AnalyticsParam -import com.tangem.tap.common.analytics.events.Token import com.tangem.tap.features.intentHandler.IntentHandler import com.tangem.tap.network.exchangeServices.ExchangeUrlBuilder import com.tangem.tap.store @@ -21,7 +21,7 @@ class BuyCurrencyIntentHandler : IntentHandler { val successUri = Uri.parse(ExchangeUrlBuilder.SUCCESS_URL) return if (data.host == successUri.host && data.authority == successUri.authority) { val currencyType = AnalyticsParam.CurrencyType.Currency(currency) - Analytics.send(Token.Bought(currencyType)) + Analytics.send(TokenScreenEvent.Bought(currencyType.value)) true } else { false diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/tokenreceive/TokenReceiveBottomSheet.kt b/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/tokenreceive/TokenReceiveBottomSheet.kt index beadf33d24..ee8b8d3581 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/tokenreceive/TokenReceiveBottomSheet.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/tokenreceive/TokenReceiveBottomSheet.kt @@ -76,6 +76,7 @@ private fun TokenReceiveBottomSheetContent(content: TokenReceiveBottomSheetConfi text = stringResource(id = R.string.common_copy), iconResId = R.drawable.ic_copy_24, onClick = { + content.onCopyClick.invoke() hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress) clipboardManager.setText(AnnotatedString(selectedAddress.value)) }, @@ -85,6 +86,7 @@ private fun TokenReceiveBottomSheetContent(content: TokenReceiveBottomSheetConfi text = stringResource(id = R.string.common_share), iconResId = R.drawable.ic_share_24, onClick = { + content.onShareClick.invoke() hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress) context.shareText(selectedAddress.value) }, diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/tokenreceive/TokenReceiveBottomSheetConfig.kt b/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/tokenreceive/TokenReceiveBottomSheetConfig.kt index b1b84dc016..c2531a839d 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/tokenreceive/TokenReceiveBottomSheetConfig.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/tokenreceive/TokenReceiveBottomSheetConfig.kt @@ -7,4 +7,6 @@ class TokenReceiveBottomSheetConfig( val symbol: String, val network: String, val addresses: List, + val onCopyClick: () -> Unit, + val onShareClick: () -> Unit, ) : TangemBottomSheetConfigContent \ No newline at end of file diff --git a/domain/tokens/models/build.gradle.kts b/domain/tokens/models/build.gradle.kts index 0e4d43a1fc..e9075cefa0 100644 --- a/domain/tokens/models/build.gradle.kts +++ b/domain/tokens/models/build.gradle.kts @@ -11,4 +11,5 @@ android { dependencies { implementation(projects.domain.txhistory.models) + implementation(projects.core.analytics.models) } \ No newline at end of file diff --git a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/models/analytics/TokenReceiveAnalyticsEvent.kt b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/models/analytics/TokenReceiveAnalyticsEvent.kt new file mode 100644 index 0000000000..ae658aee6d --- /dev/null +++ b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/models/analytics/TokenReceiveAnalyticsEvent.kt @@ -0,0 +1,19 @@ +package com.tangem.domain.tokens.models.analytics + +import com.tangem.core.analytics.models.AnalyticsEvent + +sealed class TokenReceiveAnalyticsEvent( + event: String, + params: Map = mapOf(), +) : AnalyticsEvent("Token / Receive", event, params, null) { + + class ButtonCopyAddress(token: String) : TokenReceiveAnalyticsEvent( + event = "Button - Copy Address", + params = mapOf("Token" to token), + ) + + class ButtonShareAddress(token: String) : TokenReceiveAnalyticsEvent( + event = "Button - Share Address", + params = mapOf("Token" to token), + ) +} \ No newline at end of file diff --git a/features/tokendetails/impl/build.gradle.kts b/features/tokendetails/impl/build.gradle.kts index 704e1f8bc9..97561c5bb1 100644 --- a/features/tokendetails/impl/build.gradle.kts +++ b/features/tokendetails/impl/build.gradle.kts @@ -47,6 +47,8 @@ dependencies { implementation(projects.core.navigation) implementation(projects.core.ui) implementation(projects.core.utils) + implementation(projects.core.analytics) + implementation(projects.core.analytics.models) /** Domain modules */ implementation(projects.domain.appCurrency) diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/analytics/TokenScreenEvent.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/analytics/TokenScreenEvent.kt new file mode 100644 index 0000000000..758184763f --- /dev/null +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/analytics/TokenScreenEvent.kt @@ -0,0 +1,63 @@ +package com.tangem.feature.tokendetails.presentation.tokendetails.analytics + +import com.tangem.core.analytics.models.AnalyticsEvent + +/** +[REDACTED_AUTHOR] + */ +sealed class TokenScreenEvent( + event: String, + params: Map = mapOf(), + error: Throwable? = null, +) : AnalyticsEvent("Token", event, params, error) { + + class Refreshed(token: String) : TokenScreenEvent( + event = "Refreshed", + params = mapOf("Token" to token), + ) + + class ButtonRemoveToken(token: String) : TokenScreenEvent( + "Button - Remove Token", + params = mapOf("Token" to token), + ) + + class ButtonExplore(token: String) : TokenScreenEvent( + event = "Button - Explore", + params = mapOf("Token" to token), + ) + + class ButtonReload(token: String) : TokenScreenEvent( + event = "Button - Reload", + params = mapOf("Token" to token), + ) + + class ButtonBuy(token: String) : TokenScreenEvent( + event = "Button - Buy", + params = mapOf("Token" to token), + ) + + class ButtonSell(token: String) : TokenScreenEvent( + event = "Button - Sell", + params = mapOf("Token" to token), + ) + + class ButtonExchange(token: String) : TokenScreenEvent( + event = "Button - Exchange", + params = mapOf("Token" to token), + ) + + class ButtonSend(token: String) : TokenScreenEvent( + event = "Button - Send", + params = mapOf("Token" to token), + ) + + class ButtonReceive(token: String) : TokenScreenEvent( + event = "Button - Receive", + params = mapOf("Token" to token), + ) + + class Bought(token: String) : TokenScreenEvent( + event = "Token Bought", + params = mapOf("Token" to token), + ) +} \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt index 606c36974f..4a54ef00de 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt @@ -144,7 +144,12 @@ internal class TokenDetailsStateFactory( return refreshStateConverter.convert(false) } - fun getStateWithReceiveBottomSheet(currency: CryptoCurrency, addresses: List
): TokenDetailsState { + fun getStateWithReceiveBottomSheet( + currency: CryptoCurrency, + addresses: List
, + sendCopyAnalyticsEvent: () -> Unit, + sendShareAnalyticsEvent: () -> Unit, + ): TokenDetailsState { return currentStateProvider().copy( bottomSheetConfig = TangemBottomSheetConfig( isShow = true, @@ -159,6 +164,8 @@ internal class TokenDetailsStateFactory( type = AddressModel.Type.valueOf(it.type.name), ) }, + onCopyClick = sendCopyAnalyticsEvent, + onShareClick = sendShareAnalyticsEvent, ), ), ) diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt index e848594478..4487529033 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt @@ -8,6 +8,7 @@ import androidx.paging.cachedIn import arrow.core.getOrElse import com.tangem.blockchain.common.address.AddressType import com.tangem.common.Provider +import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.balancehiding.IsBalanceHiddenUseCase @@ -17,6 +18,7 @@ import com.tangem.domain.tokens.* import com.tangem.domain.tokens.legacy.TradeCryptoAction import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.tokens.models.analytics.TokenReceiveAnalyticsEvent import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsCountUseCase import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsUseCase import com.tangem.domain.walletmanager.WalletManagersFacade @@ -25,6 +27,7 @@ import com.tangem.domain.wallets.models.UserWalletId import com.tangem.domain.wallets.usecase.GetExploreUrlUseCase import com.tangem.domain.wallets.usecase.GetSelectedWalletUseCase import com.tangem.feature.tokendetails.presentation.router.InnerTokenDetailsRouter +import com.tangem.feature.tokendetails.presentation.tokendetails.analytics.TokenScreenEvent import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState import com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.TokenDetailsStateFactory import com.tangem.features.tokendetails.navigation.TokenDetailsArguments @@ -39,7 +42,7 @@ import timber.log.Timber import javax.inject.Inject import kotlin.properties.Delegates -@Suppress("LongParameterList") +@Suppress("LongParameterList", "LargeClass") @HiltViewModel internal class TokenDetailsViewModel @Inject constructor( private val dispatchers: CoroutineDispatcherProvider, @@ -59,6 +62,7 @@ internal class TokenDetailsViewModel @Inject constructor( private val getCryptoCurrencyUseCase: GetCryptoCurrencyUseCase, private val walletManagersFacade: WalletManagersFacade, private val reduxStateHolder: ReduxStateHolder, + private val analyticsEventsHandler: AnalyticsEventHandler, savedStateHandle: SavedStateHandle, ) : ViewModel(), DefaultLifecycleObserver, TokenDetailsClickIntents { @@ -208,6 +212,7 @@ internal class TokenDetailsViewModel @Inject constructor( } override fun onBuyClick() { + analyticsEventsHandler.send(TokenScreenEvent.ButtonBuy(cryptoCurrency.symbol)) val status = cryptoCurrencyStatus ?: return reduxStateHolder.dispatch( @@ -220,10 +225,13 @@ internal class TokenDetailsViewModel @Inject constructor( } override fun onReloadClick() { + analyticsEventsHandler.send(TokenScreenEvent.ButtonReload(cryptoCurrency.symbol)) updateTxHistory() } override fun onSendClick() { + analyticsEventsHandler.send(TokenScreenEvent.ButtonSend(cryptoCurrency.symbol)) + val cryptoCurrencyStatus = cryptoCurrencyStatus ?: return when (cryptoCurrencyStatus.currency) { @@ -261,6 +269,8 @@ internal class TokenDetailsViewModel @Inject constructor( } override fun onReceiveClick() { + analyticsEventsHandler.send(TokenScreenEvent.ButtonReceive(cryptoCurrency.symbol)) + viewModelScope.launch(dispatchers.io) { val addresses = walletManagersFacade.getAddress( userWalletId = wallet.walletId, @@ -270,11 +280,19 @@ internal class TokenDetailsViewModel @Inject constructor( uiState = stateFactory.getStateWithReceiveBottomSheet( currency = cryptoCurrency, addresses = addresses, + sendCopyAnalyticsEvent = { + analyticsEventsHandler.send(TokenReceiveAnalyticsEvent.ButtonCopyAddress(cryptoCurrency.symbol)) + }, + sendShareAnalyticsEvent = { + analyticsEventsHandler.send(TokenReceiveAnalyticsEvent.ButtonShareAddress(cryptoCurrency.symbol)) + }, ) } } override fun onSellClick() { + analyticsEventsHandler.send(TokenScreenEvent.ButtonSell(cryptoCurrency.symbol)) + val status = cryptoCurrencyStatus ?: return reduxStateHolder.dispatch( TradeCryptoAction.New.Sell( @@ -285,6 +303,8 @@ internal class TokenDetailsViewModel @Inject constructor( } override fun onSwapClick() { + analyticsEventsHandler.send(TokenScreenEvent.ButtonExchange(cryptoCurrency.symbol)) + reduxStateHolder.dispatch(TradeCryptoAction.New.Swap(cryptoCurrency)) } @@ -293,6 +313,8 @@ internal class TokenDetailsViewModel @Inject constructor( } override fun onHideClick() { + analyticsEventsHandler.send(TokenScreenEvent.ButtonRemoveToken(cryptoCurrency.symbol)) + viewModelScope.launch { val hasLinkedTokens = removeCurrencyUseCase.hasLinkedTokens(wallet.walletId, cryptoCurrency) uiState = if (hasLinkedTokens) { @@ -312,6 +334,7 @@ internal class TokenDetailsViewModel @Inject constructor( } override fun onExploreClick() { + analyticsEventsHandler.send(TokenScreenEvent.ButtonExplore(cryptoCurrency.symbol)) viewModelScope.launch(dispatchers.io) { val addresses = walletManagersFacade.getAddress( userWalletId = wallet.walletId, @@ -345,6 +368,8 @@ internal class TokenDetailsViewModel @Inject constructor( } override fun onRefreshSwipe() { + analyticsEventsHandler.send(TokenScreenEvent.Refreshed(cryptoCurrency.symbol)) + uiState = stateFactory.getRefreshingState() viewModelScope.launch(dispatchers.io) { diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt index a76a16ffe8..015c3e99a5 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt @@ -43,6 +43,7 @@ import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.model.NetworkGroup import com.tangem.domain.tokens.model.TokenList +import com.tangem.domain.tokens.models.analytics.TokenReceiveAnalyticsEvent import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsCountUseCase import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsUseCase import com.tangem.domain.userwallets.UserWalletBuilder @@ -639,6 +640,12 @@ internal class WalletViewModel @Inject constructor( type = AddressModel.Type.valueOf(it.type.name), ) }, + onCopyClick = { + analyticsEventsHandler.send(TokenReceiveAnalyticsEvent.ButtonCopyAddress(currency.symbol)) + }, + onShareClick = { + analyticsEventsHandler.send(TokenReceiveAnalyticsEvent.ButtonShareAddress(currency.symbol)) + }, ), ) } From b74e2b916833028f58f7b9363256381e857f3fc6 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 27 Sep 2023 14:44:19 +0300 Subject: [PATCH 100/242] Updated on 2026-08-14 --- .../tap/domain/tokens/UserTokensRepository.kt | 4 +- .../wallet/data/WalletRepositoryImpl.kt | 3 +- .../api/common/response/ApiResponse.kt | 40 ++++++++++ .../common/response/ApiResponseCallAdapter.kt | 16 ++++ .../response/ApiResponseCallAdapterFactory.kt | 29 +++++++ .../response/ApiResponseCallDelegate.kt | 46 +++++++++++ .../api/common/response/ApiResponseError.kt | 80 +++++++++++++++++++ .../api/common/response/ApiResponseExt.kt | 6 ++ .../api/common/response/ResponseExt.kt | 32 ++++++++ .../api/tangemTech/TangemTechApi.kt | 7 +- .../api/tangemTech/TangemTechService.kt | 2 + .../com/tangem/datasource/di/NetworkModule.kt | 2 + .../datasource/local/cache/CacheKeysStore.kt | 2 + .../local/datastore/FileDataStore.kt | 7 ++ .../local/datastore/RuntimeDataStore.kt | 28 ++++--- .../datastore/SharedPreferencesDataStore.kt | 10 +++ .../local/datastore/core/DataStore.kt | 2 + .../core/StringKeyDataStoreDecorator.kt | 4 + .../DefaultAppCurrencyRepository.kt | 20 +++-- data/common/build.gradle.kts | 1 + .../data/common/api/ApiResponseRaise.kt | 47 +++++++++++ .../tangem/data/common/cache/CacheRegistry.kt | 9 +++ .../data/common/cache/DefaultCacheRegistry.kt | 9 ++- .../repository/DefaultCurrenciesRepository.kt | 48 ++++------- .../repository/DefaultQuotesRepository.kt | 23 +++--- 25 files changed, 405 insertions(+), 72 deletions(-) create mode 100644 core/datasource/src/main/java/com/tangem/datasource/api/common/response/ApiResponse.kt create mode 100644 core/datasource/src/main/java/com/tangem/datasource/api/common/response/ApiResponseCallAdapter.kt create mode 100644 core/datasource/src/main/java/com/tangem/datasource/api/common/response/ApiResponseCallAdapterFactory.kt create mode 100644 core/datasource/src/main/java/com/tangem/datasource/api/common/response/ApiResponseCallDelegate.kt create mode 100644 core/datasource/src/main/java/com/tangem/datasource/api/common/response/ApiResponseError.kt create mode 100644 core/datasource/src/main/java/com/tangem/datasource/api/common/response/ApiResponseExt.kt create mode 100644 core/datasource/src/main/java/com/tangem/datasource/api/common/response/ResponseExt.kt create mode 100644 data/common/src/main/kotlin/com/tangem/data/common/api/ApiResponseRaise.kt diff --git a/app/src/main/java/com/tangem/tap/domain/tokens/UserTokensRepository.kt b/app/src/main/java/com/tangem/tap/domain/tokens/UserTokensRepository.kt index a2baa9126b..c3cfe566f3 100644 --- a/app/src/main/java/com/tangem/tap/domain/tokens/UserTokensRepository.kt +++ b/app/src/main/java/com/tangem/tap/domain/tokens/UserTokensRepository.kt @@ -2,6 +2,7 @@ package com.tangem.tap.domain.tokens import com.tangem.blockchain.common.derivation.DerivationStyle import com.tangem.common.core.TangemSdkError +import com.tangem.datasource.api.common.response.getOrThrow import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.api.tangemTech.TangemTechService import com.tangem.datasource.api.tangemTech.models.UserTokensResponse @@ -106,7 +107,8 @@ class UserTokensRepository( return runCatching { tangemTechApi.getUserTokens(userWalletId) } .fold( onSuccess = { response -> - response.tokens + response.getOrThrow() + .tokens .mapNotNull(Currency.Companion::fromTokenResponse) .also { storageService.saveUserTokens(userWalletId, it.toUserTokensResponse()) } .distinct() diff --git a/app/src/main/java/com/tangem/tap/features/wallet/data/WalletRepositoryImpl.kt b/app/src/main/java/com/tangem/tap/features/wallet/data/WalletRepositoryImpl.kt index e610e03389..e0b6ce6972 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/data/WalletRepositoryImpl.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/data/WalletRepositoryImpl.kt @@ -1,5 +1,6 @@ package com.tangem.tap.features.wallet.data +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.tap.features.wallet.domain.WalletRepository @@ -18,6 +19,6 @@ class WalletRepositoryImpl( ) : WalletRepository { override suspend fun getCurrencyList(): CurrenciesResponse = withContext(dispatchers.io) { - tangemTechApi.getCurrencyList() + tangemTechApi.getCurrencyList().getOrThrow() } } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/response/ApiResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/response/ApiResponse.kt new file mode 100644 index 0000000000..0348dba0e3 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/response/ApiResponse.kt @@ -0,0 +1,40 @@ +package com.tangem.datasource.api.common.response + +/** + * Represents the possible responses from an API request. + * + * @param T The type of the data that is expected in a successful response. + */ +sealed class ApiResponse { + + /** + * Represents a successful response from the API. + * + * @property data The data returned by the API. + */ + data class Success(val data: T) : ApiResponse() + + /** + * Represents an error response or failure from the API. + * + * @property cause The cause of the error. + */ + data class Error(val cause: ApiResponseError) : ApiResponse() +} + +/** + * Wraps data in a [ApiResponse.Success] instance. + * + * @param data The data to wrap. + * @return A [ApiResponse.Success] instance containing the provided data. + */ +internal fun apiSuccess(data: T): ApiResponse = ApiResponse.Success(data) + +/** + * Wraps an [ApiResponseError] in a [ApiResponse.Error] instance. + * + * @param cause The error to wrap. + * @return A [ApiResponse.Error] instance containing the provided error. + */ +@Suppress("UNCHECKED_CAST") +internal fun apiError(cause: ApiResponseError): ApiResponse = ApiResponse.Error(cause) as ApiResponse \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/response/ApiResponseCallAdapter.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/response/ApiResponseCallAdapter.kt new file mode 100644 index 0000000000..885b52ca28 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/response/ApiResponseCallAdapter.kt @@ -0,0 +1,16 @@ +package com.tangem.datasource.api.common.response + +import retrofit2.Call +import retrofit2.CallAdapter +import java.lang.reflect.Type + +internal class ApiResponseCallAdapter( + private val resultType: Type, +) : CallAdapter>> { + + override fun responseType(): Type = resultType + + override fun adapt(call: Call): Call> { + return ApiResponseCallDelegate(call) + } +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/response/ApiResponseCallAdapterFactory.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/response/ApiResponseCallAdapterFactory.kt new file mode 100644 index 0000000000..11eca2e0d6 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/response/ApiResponseCallAdapterFactory.kt @@ -0,0 +1,29 @@ +package com.tangem.datasource.api.common.response + +import retrofit2.Call +import retrofit2.CallAdapter +import retrofit2.Retrofit +import java.lang.reflect.ParameterizedType +import java.lang.reflect.Type + +internal class ApiResponseCallAdapterFactory private constructor() : CallAdapter.Factory() { + + override fun get(returnType: Type, annotations: Array, retrofit: Retrofit): CallAdapter<*, *>? { + if (getRawType(returnType) != Call::class.java) { + return null + } + + val callType = getParameterUpperBound(0, returnType as ParameterizedType) + if (getRawType(callType) != ApiResponse::class.java) { + return null + } + + val resultType = getParameterUpperBound(0, callType as ParameterizedType) + return ApiResponseCallAdapter(resultType) + } + + companion object { + + fun create() = ApiResponseCallAdapterFactory() + } +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/response/ApiResponseCallDelegate.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/response/ApiResponseCallDelegate.kt new file mode 100644 index 0000000000..b69ef4e355 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/response/ApiResponseCallDelegate.kt @@ -0,0 +1,46 @@ +package com.tangem.datasource.api.common.response + +import okhttp3.Request +import okio.Timeout +import retrofit2.Call +import retrofit2.Callback +import retrofit2.Response + +internal class ApiResponseCallDelegate( + private val wrappedCall: Call, +) : Call> { + + override fun enqueue(callback: Callback>) { + wrappedCall.enqueue(ApiResponseCallback(callback)) + } + + override fun execute(): Response> = throw NotImplementedError() + override fun clone(): Call> = ApiResponseCallDelegate(wrappedCall.clone()) + override fun request(): Request = wrappedCall.request() + override fun timeout(): Timeout = wrappedCall.timeout() + override fun isExecuted(): Boolean = wrappedCall.isExecuted + override fun isCanceled(): Boolean = wrappedCall.isCanceled + override fun cancel() { wrappedCall.cancel() } + + private inner class ApiResponseCallback( + private val responseCallback: Callback>, + ) : Callback { + + override fun onResponse(call: Call, response: Response) { + val safeResponse = response.toSafeApiResponse() + + responseCallback.onResponse(this@ApiResponseCallDelegate, Response.success(safeResponse)) + } + + override fun onFailure(call: Call, t: Throwable) { + val e = if (t.isNetworkException()) { + ApiResponseError.NetworkException + } else { + ApiResponseError.UnknownException(t) + } + val safeResponse = apiError(e) + + responseCallback.onResponse(this@ApiResponseCallDelegate, Response.success(safeResponse)) + } + } +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/response/ApiResponseError.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/response/ApiResponseError.kt new file mode 100644 index 0000000000..5e88155058 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/response/ApiResponseError.kt @@ -0,0 +1,80 @@ +package com.tangem.datasource.api.common.response + +/** + * Represents the possible errors that can occur during an API request. + */ +sealed class ApiResponseError : Exception() { + + /** + * Represents an HTTP exception, which typically occurs when the server responds + * with a non-2xx HTTP status code. + * + * @property code The HTTP status code. + * @property message A human-readable message describing the error. + */ + data class HttpException(val code: Code, override val message: String?) : ApiResponseError() { + + // region Error Codes + enum class Code(val code: Int) { + // 4xx Server Errors + BAD_REQUEST(code = 400), + UNAUTHORIZED(code = 401), + PAYMENT_REQUIRED(code = 402), + FORBIDDEN(code = 403), + NOT_FOUND(code = 404), + METHOD_NOT_ALLOWED(code = 405), + NOT_ACCEPTABLE(code = 406), + PROXY_AUTHENTICATION_REQUIRED(code = 407), + REQUEST_TIMEOUT(code = 408), + CONFLICT(code = 409), + GONE(code = 410), + LENGTH_REQUIRED(code = 411), + PRECONDITION_FAILED(code = 412), + PAYLOAD_TOO_LARGE(code = 413), + URI_TOO_LONG(code = 414), + UNSUPPORTED_MEDIA_TYPE(code = 415), + RANGE_NOT_SATISFIABLE(code = 416), + EXPECTATION_FAILED(code = 417), + IM_A_TEAPOT(code = 418), // Not an error, but an April Fools' joke from RFC 2324 + UNPROCESSABLE_ENTITY(code = 422), + LOCKED(code = 423), + FAILED_DEPENDENCY(code = 424), + TOO_EARLY(code = 425), + UPGRADE_REQUIRED(code = 426), + PRECONDITION_REQUIRED(code = 428), + TOO_MANY_REQUESTS(code = 429), + REQUEST_HEADER_FIELDS_TOO_LARGE(code = 431), + UNAVAILABLE_FOR_LEGAL_REASONS(code = 451), + // 5xx Server Errors + INTERNAL_SERVER_ERROR(code = 500), + NOT_IMPLEMENTED(code = 501), + BAD_GATEWAY(code = 502), + SERVICE_UNAVAILABLE(code = 503), + GATEWAY_TIMEOUT(code = 504), + HTTP_VERSION_NOT_SUPPORTED(code = 505), + VARIANT_ALSO_NEGOTIATES(code = 506), + INSUFFICIENT_STORAGE(code = 507), + LOOP_DETECTED(code = 508), + NOT_EXTENDED(code = 510), + NETWORK_AUTHENTICATION_REQUIRED(code = 511), + ; + + override fun toString(): String = "$code - $name" + + companion object { + val values = values() + } + } + // endregion Error Codes + } + + /** Represents a network error, typically when there's no connectivity. */ + object NetworkException : ApiResponseError() + + /** + * Represents an unexpected exception that doesn't fall into one of the other categories. + * + * @property cause The exception that caused this error. + */ + data class UnknownException(override val cause: Throwable) : ApiResponseError() +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/response/ApiResponseExt.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/response/ApiResponseExt.kt new file mode 100644 index 0000000000..256c45afe2 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/response/ApiResponseExt.kt @@ -0,0 +1,6 @@ +package com.tangem.datasource.api.common.response + +fun ApiResponse.getOrThrow(): T = when (this) { + is ApiResponse.Error -> throw cause + is ApiResponse.Success -> data +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/response/ResponseExt.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/response/ResponseExt.kt new file mode 100644 index 0000000000..0ddb0ffa36 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/response/ResponseExt.kt @@ -0,0 +1,32 @@ +package com.tangem.datasource.api.common.response + +import retrofit2.Response +import java.net.ConnectException +import java.net.UnknownHostException +import javax.net.ssl.SSLHandshakeException + +internal fun Response.toSafeApiResponse(): ApiResponse { + val body = body() + + return if (isSuccessful && body != null) { + apiSuccess(body) + } else { + val code = ApiResponseError.HttpException.Code.values + .firstOrNull { it.code == code() } + val e = if (code == null) { + ApiResponseError.UnknownException(IllegalArgumentException("Unknown error status code: ${code()}")) + } else { + ApiResponseError.HttpException(code, message()) + } + + apiError(e) + } +} + +internal fun Throwable.isNetworkException(): Boolean = when (this) { + is ConnectException, + is UnknownHostException, + is SSLHandshakeException, + -> true + else -> false +} \ No newline at end of file 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 0a609246ac..be16a55885 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 @@ -1,5 +1,6 @@ package com.tangem.datasource.api.tangemTech +import com.tangem.datasource.api.common.response.ApiResponse import com.tangem.datasource.api.tangemTech.models.* import retrofit2.http.* @@ -25,13 +26,13 @@ interface TangemTechApi { suspend fun getRates(@Query("currencyId") currencyId: String, @Query("coinIds") coinIds: String): RatesResponse @GET("currencies") - suspend fun getCurrencyList(): CurrenciesResponse + suspend fun getCurrencyList(): ApiResponse @GET("geo") suspend fun getUserCountryCode(): GeoResponse @GET("user-tokens/{user-id}") - suspend fun getUserTokens(@Path(value = "user-id") userId: String): UserTokensResponse + suspend fun getUserTokens(@Path(value = "user-id") userId: String): ApiResponse @PUT("user-tokens/{user-id}") suspend fun saveUserTokens(@Path(value = "user-id") userId: String, @Body userTokens: UserTokensResponse) @@ -66,5 +67,5 @@ interface TangemTechApi { @Query("currencyId") currencyId: String, @Query("coinIds") coinIds: String, @Query("fields") fields: String = "price,priceChange24h,lastUpdatedAt", - ): QuotesResponse + ): ApiResponse } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/TangemTechService.kt b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/TangemTechService.kt index 50f11aa961..cf51d43c58 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/TangemTechService.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/TangemTechService.kt @@ -1,6 +1,7 @@ package com.tangem.datasource.api.tangemTech import com.tangem.datasource.api.common.MoshiConverter +import com.tangem.datasource.api.common.response.ApiResponseCallAdapterFactory import com.tangem.datasource.utils.RequestHeader import com.tangem.datasource.utils.RequestHeader.AuthenticationHeader import com.tangem.datasource.utils.RequestHeader.CacheControlHeader @@ -29,6 +30,7 @@ object TangemTechService { val headers = mutableListOf(CacheControlHeader).apply { header?.let(::add) } return Retrofit.Builder() .addConverterFactory(MoshiConverter.networkMoshiConverter) + .addCallAdapterFactory(ApiResponseCallAdapterFactory.create()) .baseUrl(TANGEM_TECH_BASE_URL) .client( OkHttpClient.Builder() diff --git a/core/datasource/src/main/java/com/tangem/datasource/di/NetworkModule.kt b/core/datasource/src/main/java/com/tangem/datasource/di/NetworkModule.kt index e68a69e06e..3a9580d2d0 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/di/NetworkModule.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/di/NetworkModule.kt @@ -1,6 +1,7 @@ package com.tangem.datasource.di import com.squareup.moshi.Moshi +import com.tangem.datasource.api.common.response.ApiResponseCallAdapterFactory import com.tangem.datasource.api.paymentology.PaymentologyApi import com.tangem.datasource.api.promotion.PromotionApi import com.tangem.datasource.api.tangemTech.TangemTechApi @@ -27,6 +28,7 @@ class NetworkModule { fun provideTangemTechApi(@NetworkMoshi moshi: Moshi): TangemTechApi { return Retrofit.Builder() .addConverterFactory(MoshiConverterFactory.create(moshi)) + .addCallAdapterFactory(ApiResponseCallAdapterFactory.create()) .baseUrl(PROD_TANGEM_TECH_BASE_URL) .client( OkHttpClient.Builder() diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/cache/CacheKeysStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/cache/CacheKeysStore.kt index 962c09661a..20e96ef798 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/cache/CacheKeysStore.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/cache/CacheKeysStore.kt @@ -10,5 +10,7 @@ interface CacheKeysStore { suspend fun remove(key: String) + suspend fun remove(keys: Collection) + suspend fun clear() } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/datastore/FileDataStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/datastore/FileDataStore.kt index 44038e9880..768a0e4c6a 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/datastore/FileDataStore.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/datastore/FileDataStore.kt @@ -65,6 +65,13 @@ internal class FileDataStore( writeTrigger.trigger() } + override suspend fun remove(keys: Collection) { + val e = NotImplementedError("`remove(keys)` function not implemented for `FileDataStore`") + Timber.e(e) + + throw e + } + override suspend fun clear() { val e = NotImplementedError("`clear()` function not implemented for `FileDataStore`") Timber.e(e) diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/datastore/RuntimeDataStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/datastore/RuntimeDataStore.kt index 1a295689db..77aa987353 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/datastore/RuntimeDataStore.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/datastore/RuntimeDataStore.kt @@ -12,11 +12,9 @@ internal class RuntimeDataStore : StringKeyDataStore { } override fun get(key: String): Flow { - return store - .map { value -> - value?.get(key) - } - .filterNotNull() + return store.mapNotNull { value -> + value?.get(key) + } } override fun getAll(): Flow> { @@ -42,20 +40,24 @@ internal class RuntimeDataStore : StringKeyDataStore { } override suspend fun store(values: Map) { - updateValue { value -> - values.forEach { (key, item) -> - value[key] = item - } + updateValue { storedValue -> + storedValue.putAll(values) - value + storedValue } } override suspend fun remove(key: String) { - updateValue { value -> - value.remove(key) + updateValue { storedValue -> + storedValue.remove(key) - value + storedValue + } + } + + override suspend fun remove(keys: Collection) { + updateValue { value -> + HashMap(value.filterKeys { it !in keys }) } } diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/datastore/SharedPreferencesDataStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/datastore/SharedPreferencesDataStore.kt index 3924bc0104..bf987439b8 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/datastore/SharedPreferencesDataStore.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/datastore/SharedPreferencesDataStore.kt @@ -62,6 +62,16 @@ internal abstract class SharedPreferencesDataStore( writeTrigger.trigger() } + override suspend fun remove(keys: Collection) { + sharedPreferences.edit { + keys.forEach { key -> + remove(key) + } + } + + writeTrigger.trigger() + } + override suspend fun clear() { sharedPreferences.edit { clear() } writeTrigger.trigger() diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/datastore/core/DataStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/datastore/core/DataStore.kt index edc3a4bbd8..828b0cb7b5 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/datastore/core/DataStore.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/datastore/core/DataStore.kt @@ -18,5 +18,7 @@ internal interface DataStore { suspend fun remove(key: Key) + suspend fun remove(keys: Collection) + suspend fun clear() } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/datastore/core/StringKeyDataStoreDecorator.kt b/core/datasource/src/main/java/com/tangem/datasource/local/datastore/core/StringKeyDataStoreDecorator.kt index 2e79bda3fd..d897ab9e7a 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/datastore/core/StringKeyDataStoreDecorator.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/datastore/core/StringKeyDataStoreDecorator.kt @@ -38,6 +38,10 @@ internal abstract class StringKeyDataStoreDecorator( wrappedDataStore.remove(provideStringKey(key)) } + override suspend fun remove(keys: Collection) { + wrappedDataStore.remove(keys.map(::provideStringKey)) + } + override suspend fun clear() { wrappedDataStore.clear() } diff --git a/data/app-currency/src/main/kotlin/com/tangem/data/appcurrency/DefaultAppCurrencyRepository.kt b/data/app-currency/src/main/kotlin/com/tangem/data/appcurrency/DefaultAppCurrencyRepository.kt index a20bd2309b..91abb1d2a3 100644 --- a/data/app-currency/src/main/kotlin/com/tangem/data/appcurrency/DefaultAppCurrencyRepository.kt +++ b/data/app-currency/src/main/kotlin/com/tangem/data/appcurrency/DefaultAppCurrencyRepository.kt @@ -1,6 +1,7 @@ package com.tangem.data.appcurrency import com.tangem.data.appcurrency.utils.AppCurrencyConverter +import com.tangem.data.common.api.safeApiCall import com.tangem.data.common.cache.CacheRegistry import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.api.tangemTech.models.CurrenciesResponse @@ -15,7 +16,6 @@ import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.joda.time.Duration -import timber.log.Timber internal class DefaultAppCurrencyRepository( private val tangemTechApi: TangemTechApi, @@ -80,17 +80,15 @@ internal class DefaultAppCurrencyRepository( } private suspend fun fetchAvailableCurrencies() { - try { - val response = tangemTechApi.getCurrencyList() + val response = safeApiCall( + call = { tangemTechApi.getCurrencyList().bind() }, + onError = { + cacheRegistry.invalidate(AVAILABLE_CURRENCIES_CACHE_KEY) + getDefaultCurrenciesResponse() + }, + ) - availableAppCurrenciesStore.store(response) - } catch (e: Throwable) { - Timber.e(e, "Unable to fetch available currencies") - - availableAppCurrenciesStore.store(getDefaultCurrenciesResponse()) - - throw e - } + availableAppCurrenciesStore.store(response) } private fun getDefaultCurrenciesResponse(): CurrenciesResponse = CurrenciesResponse( diff --git a/data/common/build.gradle.kts b/data/common/build.gradle.kts index eee47186e3..f1a67e77a3 100644 --- a/data/common/build.gradle.kts +++ b/data/common/build.gradle.kts @@ -15,6 +15,7 @@ dependencies { implementation(deps.kotlin.coroutines) implementation(deps.jodatime) implementation(deps.timber) + implementation(deps.arrow.core) implementation(deps.hilt.android) kapt(deps.hilt.kapt) diff --git a/data/common/src/main/kotlin/com/tangem/data/common/api/ApiResponseRaise.kt b/data/common/src/main/kotlin/com/tangem/data/common/api/ApiResponseRaise.kt new file mode 100644 index 0000000000..2d29e30093 --- /dev/null +++ b/data/common/src/main/kotlin/com/tangem/data/common/api/ApiResponseRaise.kt @@ -0,0 +1,47 @@ +package com.tangem.data.common.api + +import arrow.core.raise.Raise +import arrow.core.raise.recover +import com.tangem.datasource.api.common.response.ApiResponse +import com.tangem.datasource.api.common.response.ApiResponseError +import timber.log.Timber + +/** + * A wrapper around the [Raise] interface specific for [ApiResponseError]. It provides utility functions to + * operate on [ApiResponse] instances. + * + * @property raise A [Raise] instance for raising [ApiResponseError]. + */ +@JvmInline +value class ApiResponseRaise( + private val raise: Raise, +) : Raise by raise { + + /** + * Binds the given [ApiResponse] to its underlying value or raises an error. + * + * @return The underlying data of the response if it's successful. + */ + fun ApiResponse.bind(): T = when (this) { + is ApiResponse.Success -> data + is ApiResponse.Error -> raise.raise(cause) + } +} + +/** + * Attempts to execute an API call safely, providing error handling. + * + * @param call The API call block to execute. + * @param onError A function to handle errors and return a fallback value of type [T]. + * + * @return The result of the API call or the fallback value provided by [onError] if an error occurs. + */ +inline fun safeApiCall(call: ApiResponseRaise.() -> T, onError: (ApiResponseError) -> T): T { + return recover( + block = { call(ApiResponseRaise(raise = this)) }, + recover = { + Timber.w(it, "Unable to perform safe API call") + onError(it) + }, + ) +} \ No newline at end of file diff --git a/data/common/src/main/kotlin/com/tangem/data/common/cache/CacheRegistry.kt b/data/common/src/main/kotlin/com/tangem/data/common/cache/CacheRegistry.kt index fb698486ad..1486cab448 100644 --- a/data/common/src/main/kotlin/com/tangem/data/common/cache/CacheRegistry.kt +++ b/data/common/src/main/kotlin/com/tangem/data/common/cache/CacheRegistry.kt @@ -24,6 +24,15 @@ interface CacheRegistry { */ suspend fun invalidate(key: String) + /** + * Invalidates cache keys in registry. + * + * If the key doesn't exist, or it's already invalidated, this method doesn't have any effect. + * + * @param keys cache keys. + */ + suspend fun invalidate(keys: Collection) + /** * Invalidates all cache keys in the registry. * diff --git a/data/common/src/main/kotlin/com/tangem/data/common/cache/DefaultCacheRegistry.kt b/data/common/src/main/kotlin/com/tangem/data/common/cache/DefaultCacheRegistry.kt index 6c06ce7b88..57f6e0e76f 100644 --- a/data/common/src/main/kotlin/com/tangem/data/common/cache/DefaultCacheRegistry.kt +++ b/data/common/src/main/kotlin/com/tangem/data/common/cache/DefaultCacheRegistry.kt @@ -23,6 +23,11 @@ internal class DefaultCacheRegistry( cacheKeysStore.remove(key) } + override suspend fun invalidate(keys: Collection) { + Timber.d("Invalidate cache keys: $keys") + cacheKeysStore.remove(keys) + } + override suspend fun invalidateAll() { Timber.d("Invalidate all cache keys") cacheKeysStore.clear() @@ -32,14 +37,14 @@ internal class DefaultCacheRegistry( key: String, skipCache: Boolean, expireIn: Duration, - action: suspend () -> Unit, + block: suspend () -> Unit, ) { val isExpired = isExpired(key) || skipCache if (!isExpired) return try { Timber.d("Invoke the action associated with the cache key: $key") - action() + block() } catch (e: Throwable) { Timber.w(e, "The action related to the cache key has failed: $key") throw e 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 0967a4a262..cbe836e1cd 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 @@ -1,8 +1,10 @@ package com.tangem.data.tokens.repository import com.tangem.blockchain.common.Blockchain +import com.tangem.data.common.api.safeApiCall import com.tangem.data.common.cache.CacheRegistry import com.tangem.data.tokens.utils.* +import com.tangem.datasource.api.common.response.ApiResponseError import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.api.tangemTech.models.UserTokensResponse import com.tangem.datasource.local.token.UserMarketCoinsStore @@ -21,10 +23,7 @@ import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -import retrofit2.HttpException import timber.log.Timber -import java.net.ConnectException -import java.net.UnknownHostException internal class DefaultCurrenciesRepository( private val tangemTechApi: TangemTechApi, @@ -259,14 +258,14 @@ internal class DefaultCurrenciesRepository( private suspend fun fetchTokens(userWallet: UserWallet) { val userWalletId = userWallet.walletId - val response = try { - with(tangemTechApi.getUserTokens(userWalletId.stringValue)) { - // The response may contain repeated tokens - copy(tokens = tokens.distinct()) - } - } catch (e: Throwable) { - handleFetchTokensError(userWallet, e) - } + val response = safeApiCall( + call = { + tangemTechApi.getUserTokens(userWalletId.stringValue).bind().let { + it.copy(tokens = it.tokens.distinct()) + } + }, + onError = { handleFetchTokensError(userWallet, it) }, + ) userTokensStore.store(userWallet.walletId, response) fetchUserMarketCoinsByIds(userWalletId, response) @@ -288,7 +287,7 @@ internal class DefaultCurrenciesRepository( } } - private suspend fun handleFetchTokensError(userWallet: UserWallet, throwable: Throwable): UserTokensResponse { + private suspend fun handleFetchTokensError(userWallet: UserWallet, e: ApiResponseError): UserTokensResponse { val userWalletId = userWallet.walletId val response = userTokensStore.getSyncOrNull(userWalletId) ?: userTokensResponseFactory.createUserTokensResponse( @@ -297,27 +296,12 @@ internal class DefaultCurrenciesRepository( isSortedByBalance = false, ) - when (throwable) { - is ConnectException, - is UnknownHostException, - -> { - Timber.e("Unable to fetch currencies due to lack of internet connection") - } - is HttpException -> { - if (throwable.code() == NOT_FOUND_HTTP_CODE) { - Timber.w( - throwable, - "Requested currencies could not be found in the remote store for: $userWalletId", - ) + if (e is ApiResponseError.HttpException && e.code == ApiResponseError.HttpException.Code.NOT_FOUND) { + Timber.w(e, "Requested currencies could not be found in the remote store for: $userWalletId") - tangemTechApi.saveUserTokens(userWalletId.stringValue, response) - } else { - Timber.e(throwable, "Unable to fetch currencies for: $userWalletId") - } - } - else -> { - throw throwable - } + tangemTechApi.saveUserTokens(userWalletId.stringValue, response) + } else { + cacheRegistry.invalidate(getTokensCacheKey(userWalletId)) } return response diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultQuotesRepository.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultQuotesRepository.kt index f0fc985d25..e63ea16071 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultQuotesRepository.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultQuotesRepository.kt @@ -1,5 +1,6 @@ package com.tangem.data.tokens.repository +import com.tangem.data.common.api.safeApiCall import com.tangem.data.common.cache.CacheRegistry import com.tangem.data.tokens.utils.QuotesConverter import com.tangem.datasource.api.tangemTech.TangemTechApi @@ -12,7 +13,6 @@ import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -import timber.log.Timber internal class DefaultQuotesRepository( private val tangemTechApi: TangemTechApi, @@ -72,15 +72,20 @@ internal class DefaultQuotesRepository( } private suspend fun fetchQuotes(rawCurrenciesIds: Set, appCurrencyId: String) { - val response = try { - val coinIds = rawCurrenciesIds.joinToString(separator = ",") - tangemTechApi.getQuotes(appCurrencyId, coinIds) - } catch (e: Throwable) { - Timber.e(e, "Unable to fetch quotes for: $rawCurrenciesIds") - throw e - } + val response = safeApiCall( + call = { + val coinIds = rawCurrenciesIds.joinToString(separator = ",") + tangemTechApi.getQuotes(appCurrencyId, coinIds).bind() + }, + onError = { + cacheRegistry.invalidate(rawCurrenciesIds.map(::getQuoteCacheKey)) + null + }, + ) - quotesStore.store(response) + if (response != null) { + quotesStore.store(response) + } } private suspend fun filterExpiredCurrenciesIds( From 3ef300ee56e0c6fd5cd59b8d4f1d6f39b6eefea5 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 26 Sep 2023 14:56:05 +0500 Subject: [PATCH 101/242] Updated on 2026-08-14 --- .../transactions/TransactionList.kt | 42 ++++++++----------- .../repository/DefaultTxHistoryRepository.kt | 1 + .../usecase/GetTxHistoryItemsUseCase.kt | 2 +- 3 files changed, 19 insertions(+), 26 deletions(-) diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/transactions/TransactionList.kt b/core/ui/src/main/java/com/tangem/core/ui/components/transactions/TransactionList.kt index f28a081419..323cb0e77b 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/transactions/TransactionList.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/transactions/TransactionList.kt @@ -4,7 +4,6 @@ import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyListScope -import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.ui.Modifier import androidx.paging.compose.LazyPagingItems import androidx.paging.compose.itemContentType @@ -57,36 +56,29 @@ private fun LazyListScope.contentItems( txHistoryItems: LazyPagingItems, modifier: Modifier = Modifier, ) { - txHistoryItems.itemKey { item -> - when (item) { - is TxHistoryState.TxHistoryItemState.GroupTitle -> item.title - is TxHistoryState.TxHistoryItemState.Title -> item.onExploreClick.hashCode() - is TxHistoryState.TxHistoryItemState.Transaction -> item.state.txHash - } - } - - txHistoryItems.itemContentType { it::class.java } - - itemsIndexed( - items = txHistoryItems.itemSnapshotList.items, - key = { _, item -> + items( + count = txHistoryItems.itemCount, + key = txHistoryItems.itemKey { item -> when (item) { is TxHistoryState.TxHistoryItemState.GroupTitle -> item.title is TxHistoryState.TxHistoryItemState.Title -> item.onExploreClick.hashCode() is TxHistoryState.TxHistoryItemState.Transaction -> item.state.txHash } }, - ) { index, item -> - TxHistoryListItem( - state = item, - modifier = modifier - .animateItemPlacement() - .roundedShapeItemDecoration( - currentIndex = index, - lastIndex = txHistoryItems.itemSnapshotList.lastIndex, - ), - ) - } + contentType = txHistoryItems.itemContentType { it::class.java }, + itemContent = { index -> + val item = txHistoryItems[index]!! + TxHistoryListItem( + state = item, + modifier = modifier + .animateItemPlacement() + .roundedShapeItemDecoration( + currentIndex = index, + lastIndex = txHistoryItems.itemSnapshotList.lastIndex, + ), + ) + }, + ) } @OptIn(ExperimentalFoundationApi::class) diff --git a/data/txhistory/src/main/kotlin/com/tangem/data/txhistory/repository/DefaultTxHistoryRepository.kt b/data/txhistory/src/main/kotlin/com/tangem/data/txhistory/repository/DefaultTxHistoryRepository.kt index 718cfe7037..2f9fa852c0 100644 --- a/data/txhistory/src/main/kotlin/com/tangem/data/txhistory/repository/DefaultTxHistoryRepository.kt +++ b/data/txhistory/src/main/kotlin/com/tangem/data/txhistory/repository/DefaultTxHistoryRepository.kt @@ -39,6 +39,7 @@ class DefaultTxHistoryRepository( return Pager( config = PagingConfig( pageSize = pageSize, + initialLoadSize = pageSize, ), pagingSourceFactory = { TxHistoryPagingSource( diff --git a/domain/txhistory/src/main/kotlin/com/tangem/domain/txhistory/usecase/GetTxHistoryItemsUseCase.kt b/domain/txhistory/src/main/kotlin/com/tangem/domain/txhistory/usecase/GetTxHistoryItemsUseCase.kt index 6c9c0f682f..39865a24f5 100644 --- a/domain/txhistory/src/main/kotlin/com/tangem/domain/txhistory/usecase/GetTxHistoryItemsUseCase.kt +++ b/domain/txhistory/src/main/kotlin/com/tangem/domain/txhistory/usecase/GetTxHistoryItemsUseCase.kt @@ -10,7 +10,7 @@ import com.tangem.domain.txhistory.repository.TxHistoryRepository import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.catch -private const val DEFAULT_PAGE_SIZE = 20 +private const val DEFAULT_PAGE_SIZE = 50 class GetTxHistoryItemsUseCase(private val repository: TxHistoryRepository) { From 640e6433ff774f943f4c9bcc286222b57faefedb Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 26 Sep 2023 15:27:06 +0300 Subject: [PATCH 102/242] Updated on 2026-08-14 --- .../tangem/tap/proxy/DerivationManagerImpl.kt | 137 ++++++++++++++++-- .../tangem/tap/proxy/UserWalletManagerImpl.kt | 3 +- .../com/tangem/tap/proxy/di/ProxyModule.kt | 7 +- features/referral/domain/build.gradle.kts | 18 ++- .../referral/domain/ReferralInteractorImpl.kt | 10 +- .../domain/di/ReferralDomainModule.kt | 3 + .../tangem/lib/crypto/DerivationManager.kt | 5 + 7 files changed, 165 insertions(+), 18 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/proxy/DerivationManagerImpl.kt b/app/src/main/java/com/tangem/tap/proxy/DerivationManagerImpl.kt index d03a7956e9..df4bd27c47 100644 --- a/app/src/main/java/com/tangem/tap/proxy/DerivationManagerImpl.kt +++ b/app/src/main/java/com/tangem/tap/proxy/DerivationManagerImpl.kt @@ -10,12 +10,17 @@ import com.tangem.common.core.TangemSdkError import com.tangem.common.extensions.ByteArrayKey import com.tangem.common.extensions.toMapKey import com.tangem.crypto.hdWallet.DerivationPath +import com.tangem.data.tokens.utils.CryptoCurrencyFactory import com.tangem.domain.common.BlockchainNetwork +import com.tangem.domain.common.DerivationStyleProvider import com.tangem.domain.common.configs.CardConfig import com.tangem.domain.common.extensions.fromNetworkId import com.tangem.domain.common.util.derivationStyleProvider import com.tangem.domain.common.util.hasDerivation import com.tangem.domain.models.scan.ScanResponse +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.tokens.repository.CurrenciesRepository +import com.tangem.domain.wallets.models.UserWalletId import com.tangem.lib.crypto.DerivationManager import com.tangem.lib.crypto.models.Currency import com.tangem.lib.crypto.models.Currency.NonNativeToken @@ -28,6 +33,7 @@ import com.tangem.tap.common.redux.global.GlobalAction import com.tangem.tap.domain.TapError import com.tangem.tap.features.tokens.legacy.redux.TokensMiddleware import com.tangem.tap.scope +import com.tangem.tap.userWalletsListManager import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlin.coroutines.suspendCoroutine @@ -35,21 +41,14 @@ import com.tangem.tap.features.wallet.models.Currency as WalletModelCurrency class DerivationManagerImpl( private val appStateHolder: AppStateHolder, + private val currenciesRepository: CurrenciesRepository, ) : DerivationManager { override suspend fun deriveMissingBlockchains(currency: Currency) = suspendCoroutine { continuation -> val blockchain = Blockchain.fromNetworkId(currency.networkId) val card = appStateHolder.getActualCard() if (blockchain != null && card != null) { - val appToken = if (currency is NonNativeToken) { - Token( - symbol = currency.symbol, - contractAddress = currency.contractAddress, - decimals = currency.decimalCount, - ) - } else { - null - } + val appToken = getAppToken(currency) val scanResponse = appStateHolder.scanResponse if (scanResponse != null) { val blockchainNetwork = BlockchainNetwork(blockchain, scanResponse.derivationStyleProvider) @@ -70,6 +69,64 @@ class DerivationManagerImpl( } } + override suspend fun deriveAndAddTokens(currency: Currency) = suspendCoroutine { continuation -> + val selectedUserWallet = requireNotNull( + userWalletsListManager.selectedUserWalletSync, + ) { "selectedUserWallet shouldn't be null" } + val scanResponse = selectedUserWallet.scanResponse + val blockchain = requireNotNull( + Blockchain.fromNetworkId(currency.networkId), + ) { "unsupported blockchain" } + val derivationStyleProvider = scanResponse.derivationStyleProvider + val derivationPath = requireNotNull( + blockchain.derivationPath(derivationStyleProvider.getDerivationStyle())?.rawPath, + ) { "derivationPath shouldn't be null" } + val hasDerivation = scanResponse.hasDerivation( + blockchain, + derivationPath, + ) + if (hasDerivation) { + scope.launch { + addToken( + userWalletId = selectedUserWallet.walletId, + blockchain = blockchain, + currency = currency, + derivationPath = derivationPath, + derivationStyleProvider = derivationStyleProvider, + ) + } + } else { + val blockchainNetwork = BlockchainNetwork(blockchain, scanResponse.derivationStyleProvider) + val appCurrency = WalletModelCurrency.fromBlockchainNetwork( + blockchainNetwork, + getAppToken(currency), + ) + deriveMissingBlockchains( + scanResponse = scanResponse, + currencyList = listOf(appCurrency), + onSuccess = { updatedScanResponse -> + scope.launch { + userWalletsListManager.update( + userWalletId = selectedUserWallet.walletId, + update = { it.copy(scanResponse = updatedScanResponse) }, + ) + addToken( + userWalletId = selectedUserWallet.walletId, + blockchain = blockchain, + currency = currency, + derivationPath = derivationPath, + derivationStyleProvider = derivationStyleProvider, + ) + continuation.resumeWith(Result.success(derivationPath)) + } + }, + onFailure = { + continuation.resumeWith(Result.failure(it)) + }, + ) + } + } + override fun getDerivationPathForBlockchain(networkId: String): String? { val scanResponse = appStateHolder.scanResponse val blockchain = Blockchain.fromNetworkId(networkId) @@ -91,6 +148,57 @@ class DerivationManagerImpl( return false } + private suspend fun addToken( + userWalletId: UserWalletId, + blockchain: Blockchain, + currency: Currency, + derivationPath: String, + derivationStyleProvider: DerivationStyleProvider, + ) { + currenciesRepository.addCurrencies( + userWalletId, + listOf( + convertCurrency( + blockchain = blockchain, + currency = currency, + derivationPath = derivationPath, + derivationStyleProvider = derivationStyleProvider, + ), + ), + ) + } + + private fun convertCurrency( + blockchain: Blockchain, + currency: Currency, + derivationPath: String, + derivationStyleProvider: DerivationStyleProvider, + ): CryptoCurrency { + val cryptoCurrencyFactory = CryptoCurrencyFactory() + return when (currency) { + is Currency.NativeToken -> { + cryptoCurrencyFactory.createCoin( + blockchain = blockchain, + extraDerivationPath = derivationPath, + derivationStyleProvider = derivationStyleProvider, + ) + } + is NonNativeToken -> { + val sdkToken = Token( + symbol = currency.symbol, + contractAddress = currency.contractAddress, + decimals = currency.decimalCount, + ) + cryptoCurrencyFactory.createToken( + sdkToken = sdkToken, + blockchain = blockchain, + extraDerivationPath = derivationPath, + derivationStyleProvider = derivationStyleProvider, + ) + } + } as CryptoCurrency + } + private fun deriveMissingBlockchains( scanResponse: ScanResponse, currencyList: List, @@ -204,6 +312,17 @@ class DerivationManagerImpl( return TokensMiddleware.DerivationData(derivations = mapKeyOfWalletPublicKey to toDerive) } + private fun getAppToken(currency: Currency): Token? { + return if (currency is NonNativeToken) { + Token( + symbol = currency.symbol, + contractAddress = currency.contractAddress, + decimals = currency.decimalCount, + ) + } else { + null + } + } /** * Simple error handler * for now specifically handle only UserCancelled diff --git a/app/src/main/java/com/tangem/tap/proxy/UserWalletManagerImpl.kt b/app/src/main/java/com/tangem/tap/proxy/UserWalletManagerImpl.kt index ae87ca5aec..6165cbd367 100644 --- a/app/src/main/java/com/tangem/tap/proxy/UserWalletManagerImpl.kt +++ b/app/src/main/java/com/tangem/tap/proxy/UserWalletManagerImpl.kt @@ -94,8 +94,7 @@ class UserWalletManagerImpl( UserWalletIdBuilder.card(it) .build() ?.stringValue - } - ?: "" + } ?: "" } override suspend fun isTokenAdded(currency: Currency, derivationPath: String?): Boolean { diff --git a/app/src/main/java/com/tangem/tap/proxy/di/ProxyModule.kt b/app/src/main/java/com/tangem/tap/proxy/di/ProxyModule.kt index c4b935a3d5..8d2154c795 100644 --- a/app/src/main/java/com/tangem/tap/proxy/di/ProxyModule.kt +++ b/app/src/main/java/com/tangem/tap/proxy/di/ProxyModule.kt @@ -6,6 +6,7 @@ import com.tangem.core.featuretoggle.manager.FeatureTogglesManager import com.tangem.domain.card.repository.CardSdkConfigRepository import com.tangem.domain.common.CardTypesResolver import com.tangem.domain.common.util.cardTypesResolver +import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.feature.learn2earn.domain.api.Learn2earnDependencyProvider import com.tangem.features.wallet.featuretoggles.WalletFeatureToggles @@ -66,9 +67,13 @@ class ProxyModule { @Provides @Singleton - fun provideDerivationManager(appStateHolder: AppStateHolder): DerivationManager { + fun provideDerivationManager( + appStateHolder: AppStateHolder, + currenciesRepository: CurrenciesRepository, + ): DerivationManager { return DerivationManagerImpl( appStateHolder = appStateHolder, + currenciesRepository = currenciesRepository, ) } diff --git a/features/referral/domain/build.gradle.kts b/features/referral/domain/build.gradle.kts index 6f335c1221..036c5995f8 100644 --- a/features/referral/domain/build.gradle.kts +++ b/features/referral/domain/build.gradle.kts @@ -1,16 +1,26 @@ plugins { - alias(deps.plugins.kotlin.jvm) + alias(deps.plugins.android.library) + alias(deps.plugins.kotlin.android) alias(deps.plugins.kotlin.kapt) + alias(deps.plugins.kotlin.serialization) id("configuration") } +android { + namespace = "com.tangem.domain.referral" +} dependencies { /** Libs */ - implementation(project(":core:utils")) - implementation(project(":libs:crypto")) + implementation(projects.core.utils) - /** Time */ + /** Core modules */ + implementation(projects.libs.crypto) + + /** Feature Apis */ + implementation(projects.features.wallet.api) + + /** Dependencies */ implementation(deps.jodatime) /** DI */ diff --git a/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/ReferralInteractorImpl.kt b/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/ReferralInteractorImpl.kt index a62fbcca35..5beacce884 100644 --- a/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/ReferralInteractorImpl.kt +++ b/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/ReferralInteractorImpl.kt @@ -3,6 +3,7 @@ package com.tangem.feature.referral.domain import com.tangem.feature.referral.domain.converter.TokensConverter import com.tangem.feature.referral.domain.models.ReferralData import com.tangem.feature.referral.domain.models.TokenData +import com.tangem.features.wallet.featuretoggles.WalletFeatureToggles import com.tangem.lib.crypto.DerivationManager import com.tangem.lib.crypto.UserWalletManager import com.tangem.lib.crypto.models.Currency @@ -12,6 +13,7 @@ internal class ReferralInteractorImpl( private val derivationManager: DerivationManager, private val userWalletManager: UserWalletManager, private val tokensConverter: TokensConverter, + private val walletFeatureToggles: WalletFeatureToggles, ) : ReferralInteractor { private val tokensForReferral = mutableListOf() @@ -30,7 +32,11 @@ internal class ReferralInteractorImpl( override suspend fun startReferral(): ReferralData { if (tokensForReferral.isNotEmpty()) { val currency = tokensConverter.convert(tokensForReferral.first()) - val derivationPath = deriveOrAddTokens(currency) + val derivationPath = if (walletFeatureToggles.isRedesignedScreenEnabled) { + derivationManager.deriveAndAddTokens(currency) + } else { + deriveAndAddTokens(currency) + } val publicAddress = userWalletManager.getWalletAddress(currency.networkId, derivationPath) return repository.startReferral( walletId = userWalletManager.getWalletId(), @@ -43,7 +49,7 @@ internal class ReferralInteractorImpl( } } - private suspend fun deriveOrAddTokens(currency: Currency): String { + private suspend fun deriveAndAddTokens(currency: Currency): String { val derivationPath = derivationManager.getDerivationPathForBlockchain(currency.networkId) if (derivationPath.isNullOrEmpty()) error("derivationPath shouldn't be empty") if (!derivationManager.hasDerivation(currency.networkId, derivationPath)) { diff --git a/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/di/ReferralDomainModule.kt b/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/di/ReferralDomainModule.kt index f991068cd5..8e6b9fbe88 100644 --- a/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/di/ReferralDomainModule.kt +++ b/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/di/ReferralDomainModule.kt @@ -4,6 +4,7 @@ import com.tangem.feature.referral.domain.ReferralInteractor import com.tangem.feature.referral.domain.ReferralInteractorImpl import com.tangem.feature.referral.domain.ReferralRepository import com.tangem.feature.referral.domain.converter.TokensConverter +import com.tangem.features.wallet.featuretoggles.WalletFeatureToggles import com.tangem.lib.crypto.DerivationManager import com.tangem.lib.crypto.UserWalletManager import dagger.Module @@ -23,12 +24,14 @@ class ReferralDomainModule { derivationManager: DerivationManager, userWalletManager: UserWalletManager, tokensConverter: TokensConverter, + walletFeatureToggles: WalletFeatureToggles, ): ReferralInteractor { return ReferralInteractorImpl( repository = referralRepository, derivationManager = derivationManager, userWalletManager = userWalletManager, tokensConverter = tokensConverter, + walletFeatureToggles = walletFeatureToggles, ) } } \ No newline at end of file diff --git a/libs/crypto/src/main/java/com/tangem/lib/crypto/DerivationManager.kt b/libs/crypto/src/main/java/com/tangem/lib/crypto/DerivationManager.kt index 7dd3d51970..bf9803cbb2 100644 --- a/libs/crypto/src/main/java/com/tangem/lib/crypto/DerivationManager.kt +++ b/libs/crypto/src/main/java/com/tangem/lib/crypto/DerivationManager.kt @@ -22,4 +22,9 @@ interface DerivationManager { * Checks that given [networkId] has derivations for [derivationPath] */ fun hasDerivation(networkId: String, derivationPath: String): Boolean + + /** + * Makes derivation for [Currency] if it is missing and adds token to wallet + */ + suspend fun deriveAndAddTokens(currency: Currency): String } \ No newline at end of file From c1561a366cff6983c34ed69495f7f811db51946c Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 27 Sep 2023 17:39:23 +0800 Subject: [PATCH 103/242] Updated on 2026-08-14 --- .../presentation/common/WalletPreviewData.kt | 3 +- .../common/component/TokenItem.kt | 185 +++++++++++------ .../component/token/NonFiatContentBlock.kt | 78 +++++++ .../component/token/TokenCryptoAmount.kt | 60 ++++++ .../component/token/TokenCryptoInfoBlock.kt | 118 ----------- .../common/component/token/TokenFiatAmount.kt | 54 +++++ .../component/token/TokenFiatInfoBlock.kt | 195 ------------------ .../component/token/TokenPriceChange.kt | 102 +++++++++ .../common/component/token/TokenTitle.kt | 80 +++++++ 9 files changed, 498 insertions(+), 377 deletions(-) create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/token/NonFiatContentBlock.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/token/TokenCryptoAmount.kt delete mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/token/TokenCryptoInfoBlock.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/token/TokenFiatAmount.kt delete mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/token/TokenFiatInfoBlock.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/token/TokenPriceChange.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/token/TokenTitle.kt diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/WalletPreviewData.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/WalletPreviewData.kt index 1c6883653b..566eb3e04d 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/WalletPreviewData.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/WalletPreviewData.kt @@ -153,7 +153,7 @@ internal object WalletPreviewData { icon = tokenIconState, name = "Polygon", amount = "5,412 MATIC", - hasPending = true, + hasPending = false, tokenOptions = TokenOptionsState( config = PriceChangeConfig( valueInPercent = "2%", @@ -161,7 +161,6 @@ internal object WalletPreviewData { ), fiatAmount = "321 $", isBalanceHidden = false, - ), onItemClick = {}, onItemLongClick = {}, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/TokenItem.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/TokenItem.kt index a306e8bad2..0125f47284 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/TokenItem.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/TokenItem.kt @@ -1,40 +1,40 @@ package com.tangem.feature.wallet.presentation.common.component -import androidx.compose.foundation.ExperimentalFoundationApi -import androidx.compose.foundation.background -import androidx.compose.foundation.combinedClickable -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.defaultMinSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.runtime.Composable -import androidx.compose.runtime.Stable +import androidx.compose.foundation.* +import androidx.compose.foundation.layout.* +import androidx.compose.runtime.* import androidx.compose.ui.Modifier import androidx.compose.ui.composed +import androidx.compose.ui.layout.onSizeChanged +import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider -import androidx.constraintlayout.compose.ConstrainedLayoutReference -import androidx.constraintlayout.compose.ConstraintLayout -import androidx.constraintlayout.compose.ConstraintLayoutScope -import androidx.constraintlayout.compose.Dimension +import androidx.constraintlayout.compose.* import com.tangem.core.ui.extensions.rememberHapticFeedback import com.tangem.core.ui.res.TangemTheme import com.tangem.feature.wallet.presentation.common.WalletPreviewData -import com.tangem.feature.wallet.presentation.common.component.token.TokenCryptoInfoBlock -import com.tangem.feature.wallet.presentation.common.component.token.TokenFiatInfoBlock +import com.tangem.feature.wallet.presentation.common.component.token.* import com.tangem.feature.wallet.presentation.common.component.token.icon.TokenIcon import com.tangem.feature.wallet.presentation.common.state.TokenItemState import org.burnoutcrew.reorderable.ReorderableLazyListState +@Suppress("LongMethod") @Composable internal fun TokenItem( state: TokenItemState, modifier: Modifier = Modifier, reorderableTokenListState: ReorderableLazyListState? = null, ) { - BaseContainer(modifier = modifier.tokenClickable(state)) { - val (iconRef, cryptoInfoRef, fiatInfoRef) = createRefs() + var rootWidth by remember { mutableStateOf(Int.MIN_VALUE) } + + @Suppress("DestructuringDeclarationWithTooManyEntries") + BaseContainer( + modifier = modifier + .tokenClickable(state) + .onSizeChanged { rootWidth = it.width }, + ) { + val (iconRef, titleRef, cryptoAmountRef, fiatAmountRef, priceChangeRef, nonFiatContentRef) = createRefs() TokenIcon( state = state, @@ -44,22 +44,98 @@ internal fun TokenItem( }, ) - TokenCryptoInfoBlock( + val density = LocalDensity.current + val titleRequiredMinWidth by remember(rootWidth) { + derivedStateOf { with(density) { rootWidth.toDp().times(other = 0.22f) } } + } + + TokenTitle( state = state, modifier = Modifier .padding(horizontal = TangemTheme.dimens.spacing8) - .constrainAs(cryptoInfoRef) { - centerVerticallyTo(parent) + .constrainAs(titleRef) { start.linkTo(iconRef.end) - end.linkTo(fiatInfoRef.start) - width = Dimension.fillToConstraints + top.linkTo(parent.top) + + width = Dimension.fillToConstraints.atLeast(dp = titleRequiredMinWidth) + + when (state) { + is TokenItemState.Content -> end.linkTo(fiatAmountRef.start) + is TokenItemState.Draggable -> end.linkTo(nonFiatContentRef.start) + is TokenItemState.Unreachable, + is TokenItemState.NoAddress, + -> { + end.linkTo(nonFiatContentRef.start) + bottom.linkTo(parent.bottom) + } + else -> Unit + } }, ) - TokenFiatInfoBlock( + TokenFiatAmount( + state = state, + modifier = Modifier.constrainAs(fiatAmountRef) { + top.linkTo(parent.top) + end.linkTo(parent.end) + + width = Dimension.fillToConstraints.atMostWrapContent + + if (state is TokenItemState.Content) { + start.linkTo(titleRef.end) + } + }, + ) + + val marginBetweenRows = TangemTheme.dimens.spacing2 + TokenCryptoAmount( + state = state, + modifier = Modifier + .padding(horizontal = TangemTheme.dimens.spacing8) + .constrainAs(cryptoAmountRef) { + start.linkTo(iconRef.end) + top.linkTo(titleRef.bottom, marginBetweenRows) + bottom.linkTo(parent.bottom) + + when (state) { + is TokenItemState.Content -> { + end.linkTo(priceChangeRef.start) + width = Dimension.fillToConstraints.atMostWrapContent + } + is TokenItemState.Draggable -> { + end.linkTo(nonFiatContentRef.start) + width = Dimension.fillToConstraints + } + else -> Unit + } + }, + ) + + val priceChangeRequiredMinWidth by remember(rootWidth) { + derivedStateOf { with(density) { rootWidth.toDp().times(other = 0.16f) } } + } + TokenPriceChange( + state = state, + modifier = Modifier.constrainAs(priceChangeRef) { + top.linkTo(fiatAmountRef.bottom, marginBetweenRows) + end.linkTo(anchor = parent.end) + bottom.linkTo(parent.bottom) + + if (state is TokenItemState.ContentState) { + start.linkTo(cryptoAmountRef.end) + width = Dimension.fillToConstraints + .atLeast(priceChangeRequiredMinWidth) + } + }, + ) + + NonFiatContentBlock( state = state, - modifier = Modifier.constrainAsOptionsItem(scope = this, ref = fiatInfoRef), reorderableTokenListState = reorderableTokenListState, + modifier = Modifier.constrainAs(nonFiatContentRef) { + centerVerticallyTo(parent) + end.linkTo(parent.end) + }, ) } } @@ -77,57 +153,26 @@ private inline fun BaseContainer( ConstraintLayout( modifier = Modifier .fillMaxWidth() - .padding( - horizontal = TangemTheme.dimens.spacing14, - vertical = TangemTheme.dimens.spacing14, - ), + .padding(all = TangemTheme.dimens.spacing14), content = content, ) } } -@Stable -private fun Modifier.constrainAsOptionsItem(scope: ConstraintLayoutScope, ref: ConstrainedLayoutReference): Modifier { - return with(scope) { - this@constrainAsOptionsItem.constrainAs(ref) { - centerVerticallyTo(parent) - end.linkTo(parent.end) - } - } -} - @OptIn(ExperimentalFoundationApi::class) private fun Modifier.tokenClickable(state: TokenItemState): Modifier = composed { when (state) { is TokenItemState.Content -> { - val onLongClick = rememberHapticFeedback( - state = state, - onAction = state.onItemLongClick, - ) - this.combinedClickable( - onClick = state.onItemClick, - onLongClick = onLongClick, - ) + val onLongClick = rememberHapticFeedback(state = state, onAction = state.onItemLongClick) + combinedClickable(onClick = state.onItemClick, onLongClick = onLongClick) } is TokenItemState.Unreachable -> { - val onLongClick = rememberHapticFeedback( - state = state, - onAction = state.onItemLongClick, - ) - this.combinedClickable( - onClick = state.onItemClick, - onLongClick = onLongClick, - ) + val onLongClick = rememberHapticFeedback(state = state, onAction = state.onItemLongClick) + combinedClickable(onClick = state.onItemClick, onLongClick = onLongClick) } is TokenItemState.NoAddress -> { - val onLongClick = rememberHapticFeedback( - state = state, - onAction = state.onItemLongClick, - ) - this.combinedClickable( - onClick = {}, - onLongClick = onLongClick, - ) + val onLongClick = rememberHapticFeedback(state = state, onAction = state.onItemLongClick) + combinedClickable(onClick = {}, onLongClick = onLongClick) } is TokenItemState.Draggable, is TokenItemState.Loading, @@ -155,6 +200,22 @@ private fun Preview_Tokens_DarkTheme(@PreviewParameter(TokenConfigProvider::clas private class TokenConfigProvider : CollectionPreviewParameterProvider( collection = listOf( + WalletPreviewData.tokenItemVisibleState.copy(amount = "5,41221467146712416241274127841274174213421 MATIC"), + WalletPreviewData.tokenItemVisibleState.copy( + tokenOptions = WalletPreviewData.tokenItemVisibleState.tokenOptions.copy( + config = WalletPreviewData.tokenItemVisibleState.tokenOptions.config.copy( + valueInPercent = "31231231231231231231223123123123212312312312.00%", + ), + ), + ), + WalletPreviewData.tokenItemVisibleState.copy( + amount = "5,41221467146712416241274127841274174213421 MATIC", + tokenOptions = WalletPreviewData.tokenItemVisibleState.tokenOptions.copy( + config = WalletPreviewData.tokenItemVisibleState.tokenOptions.config.copy( + valueInPercent = "31231231231231231231223123123123212312312312.00%", + ), + ), + ), WalletPreviewData.tokenItemVisibleState, WalletPreviewData.tokenItemUnreachableState, WalletPreviewData.tokenItemNoAddressState, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/token/NonFiatContentBlock.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/token/NonFiatContentBlock.kt new file mode 100644 index 0000000000..cb5c920555 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/token/NonFiatContentBlock.kt @@ -0,0 +1,78 @@ +package com.tangem.feature.wallet.presentation.common.component.token + +import androidx.annotation.StringRes +import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.ExperimentalAnimationApi +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.size +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextOverflow +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemTypography +import com.tangem.feature.wallet.impl.R +import com.tangem.feature.wallet.presentation.common.state.TokenItemState +import org.burnoutcrew.reorderable.ReorderableLazyListState +import org.burnoutcrew.reorderable.detectReorder + +@OptIn(ExperimentalAnimationApi::class) +@Composable +internal fun NonFiatContentBlock( + state: TokenItemState, + reorderableTokenListState: ReorderableLazyListState?, + modifier: Modifier = Modifier, +) { + AnimatedContent( + targetState = state, + label = "Update non content fiat block", + modifier = modifier, + ) { animatedState -> + when (animatedState) { + is TokenItemState.Draggable -> DraggableImage(reorderableTokenListState = reorderableTokenListState) + is TokenItemState.Unreachable -> NonFiatContentText(text = R.string.common_unreachable) + is TokenItemState.NoAddress -> NonFiatContentText(text = R.string.common_no_address) + is TokenItemState.Content, + is TokenItemState.Loading, + is TokenItemState.Locked, + -> Unit + } + } +} + +@Composable +private fun DraggableImage(reorderableTokenListState: ReorderableLazyListState?) { + Box( + modifier = Modifier + .size(size = TangemTheme.dimens.size32) + .then( + other = if (reorderableTokenListState != null) { + Modifier.detectReorder(reorderableTokenListState) + } else { + Modifier + }, + ), + contentAlignment = Alignment.Center, + ) { + Icon( + painter = painterResource(id = R.drawable.ic_drag_24), + tint = TangemTheme.colors.icon.informative, + contentDescription = null, + ) + } +} + +@Composable +private fun NonFiatContentText(@StringRes text: Int) { + Text( + text = stringResource(id = text), + color = TangemTheme.colors.text.tertiary, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + style = TangemTypography.body2, + ) +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/token/TokenCryptoAmount.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/token/TokenCryptoAmount.kt new file mode 100644 index 0000000000..700dfdc58c --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/token/TokenCryptoAmount.kt @@ -0,0 +1,60 @@ +package com.tangem.feature.wallet.presentation.common.component.token + +import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.ExperimentalAnimationApi +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.composed +import androidx.compose.ui.text.style.TextOverflow +import com.tangem.common.Strings +import com.tangem.core.ui.components.RectangleShimmer +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemTypography +import com.tangem.feature.wallet.presentation.common.state.TokenItemState + +@OptIn(ExperimentalAnimationApi::class) +@Composable +internal fun TokenCryptoAmount(state: TokenItemState, modifier: Modifier = Modifier) { + AnimatedContent(targetState = state, label = "Update crypto amount", modifier = modifier) { animatedState -> + when (animatedState) { + is TokenItemState.Content -> { + CryptoAmountText( + amount = if (animatedState.tokenOptions.isBalanceHidden) Strings.STARS else animatedState.amount, + ) + } + is TokenItemState.Draggable -> { + CryptoAmountText(amount = animatedState.info.resolveReference()) + } + is TokenItemState.Loading -> { + RectangleShimmer(modifier = Modifier.placeholderSize(), radius = TangemTheme.dimens.radius4) + } + is TokenItemState.Locked -> { + LockedRectangle(modifier = Modifier.placeholderSize()) + } + is TokenItemState.Unreachable, + is TokenItemState.NoAddress, + -> Unit + } + } +} + +@Composable +private fun CryptoAmountText(amount: String) { + Text( + text = amount, + color = TangemTheme.colors.text.tertiary, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + style = TangemTypography.body2, + ) +} + +private fun Modifier.placeholderSize(): Modifier = composed { + return@composed this + .padding(vertical = TangemTheme.dimens.spacing4) + .size(width = TangemTheme.dimens.size52, height = TangemTheme.dimens.size12) +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/token/TokenCryptoInfoBlock.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/token/TokenCryptoInfoBlock.kt deleted file mode 100644 index 4521b33872..0000000000 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/token/TokenCryptoInfoBlock.kt +++ /dev/null @@ -1,118 +0,0 @@ -package com.tangem.feature.wallet.presentation.common.component.token - -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.foundation.Image -import androidx.compose.foundation.layout.* -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.composed -import androidx.compose.ui.res.painterResource -import com.tangem.common.Strings.STARS -import com.tangem.core.ui.components.RectangleShimmer -import com.tangem.core.ui.extensions.resolveReference -import com.tangem.core.ui.res.TangemTheme -import com.tangem.core.ui.res.TangemTypography -import com.tangem.feature.wallet.impl.R -import com.tangem.feature.wallet.presentation.common.state.TokenItemState - -@Composable -internal fun TokenCryptoInfoBlock(state: TokenItemState, modifier: Modifier = Modifier) { - when (state) { - is TokenItemState.ContentState -> ContentBlock(state = state, modifier = modifier) - is TokenItemState.Loading -> LoadingBlock(modifier = modifier) - is TokenItemState.Locked -> LockedBlock(modifier = modifier) - } -} - -@Composable -private fun ContentBlock(state: TokenItemState.ContentState, modifier: Modifier = Modifier) { - Column( - modifier = modifier, - verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing2), - ) { - CurrencyNameText( - name = state.name, - hasPending = (state as? TokenItemState.Content)?.hasPending == true, - ) - - AmountText( - amount = when (state) { - is TokenItemState.Content -> if (state.tokenOptions.isBalanceHidden) STARS else state.amount - is TokenItemState.Draggable -> state.info.resolveReference() - is TokenItemState.Unreachable, - is TokenItemState.NoAddress, - -> null - }, - ) - } -} - -@Composable -private fun CurrencyNameText(name: String, hasPending: Boolean) { - Row(horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8)) { - Text( - text = name, - style = TangemTypography.subtitle2, - color = TangemTheme.colors.text.primary1, - ) - - PendingTransactionImage(hasPending = hasPending, modifier = Modifier.align(Alignment.CenterVertically)) - } -} - -@Composable -private fun PendingTransactionImage(hasPending: Boolean, modifier: Modifier = Modifier) { - AnimatedVisibility(visible = hasPending, modifier = modifier) { - Image( - painter = painterResource(id = R.drawable.img_loader_15), - contentDescription = null, - ) - } -} - -@Composable -private fun AmountText(amount: String?) { - AnimatedVisibility(visible = !amount.isNullOrBlank()) { - if (amount == null) return@AnimatedVisibility - Text( - text = amount, - style = TangemTypography.body2, - color = TangemTheme.colors.text.tertiary, - ) - } -} - -@Composable -private fun LoadingBlock(modifier: Modifier = Modifier) { - NonContentContainer(modifier = modifier) { - RectangleShimmer(modifier = Modifier.nameSize(), radius = TangemTheme.dimens.radius4) - RectangleShimmer(modifier = Modifier.amountSize(), radius = TangemTheme.dimens.radius4) - } -} - -@Composable -private fun LockedBlock(modifier: Modifier = Modifier) { - NonContentContainer(modifier = modifier) { - LockedRectangle(modifier = Modifier.nameSize()) - LockedRectangle(modifier = Modifier.amountSize()) - } -} - -@Composable -private fun NonContentContainer(modifier: Modifier = Modifier, content: @Composable ColumnScope.() -> Unit) { - Column( - modifier = modifier, - verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing10), - content = content, - ) -} - -private fun Modifier.nameSize(): Modifier = composed { - return@composed size(width = TangemTheme.dimens.size72, height = TangemTheme.dimens.size12) -} - -private fun Modifier.amountSize(): Modifier = composed { - return@composed size(width = TangemTheme.dimens.size50, height = TangemTheme.dimens.size12) -} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/token/TokenFiatAmount.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/token/TokenFiatAmount.kt new file mode 100644 index 0000000000..f4ed36c1c8 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/token/TokenFiatAmount.kt @@ -0,0 +1,54 @@ +package com.tangem.feature.wallet.presentation.common.component.token + +import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.ExperimentalAnimationApi +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.composed +import androidx.compose.ui.text.style.TextOverflow +import com.tangem.common.Strings +import com.tangem.core.ui.components.RectangleShimmer +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemTypography +import com.tangem.feature.wallet.presentation.common.state.TokenItemState + +@OptIn(ExperimentalAnimationApi::class) +@Composable +internal fun TokenFiatAmount(state: TokenItemState, modifier: Modifier = Modifier) { + AnimatedContent(targetState = state, label = "Update fiat amount", modifier = modifier) { animatedState -> + when (animatedState) { + is TokenItemState.Content -> { + Text( + text = if (animatedState.tokenOptions.isBalanceHidden) { + Strings.STARS + } else { + animatedState.tokenOptions.fiatAmount + }, + color = TangemTheme.colors.text.primary1, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + style = TangemTypography.body2, + ) + } + is TokenItemState.Loading -> { + RectangleShimmer(modifier = Modifier.placeholderSize(), radius = TangemTheme.dimens.radius4) + } + is TokenItemState.Locked -> { + LockedRectangle(modifier = Modifier.placeholderSize()) + } + is TokenItemState.Unreachable, + is TokenItemState.Draggable, + is TokenItemState.NoAddress, + -> Unit + } + } +} + +private fun Modifier.placeholderSize(): Modifier = composed { + return@composed this + .padding(vertical = TangemTheme.dimens.spacing4) + .size(width = TangemTheme.dimens.size40, height = TangemTheme.dimens.size12) +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/token/TokenFiatInfoBlock.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/token/TokenFiatInfoBlock.kt deleted file mode 100644 index 74a07725c8..0000000000 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/token/TokenFiatInfoBlock.kt +++ /dev/null @@ -1,195 +0,0 @@ -package com.tangem.feature.wallet.presentation.common.component.token - -import androidx.compose.animation.AnimatedContent -import androidx.compose.animation.ExperimentalAnimationApi -import androidx.compose.foundation.layout.* -import androidx.compose.material3.Icon -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.composed -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource -import com.tangem.common.Strings.STARS -import com.tangem.core.ui.components.RectangleShimmer -import com.tangem.core.ui.components.SpacerW4 -import com.tangem.core.ui.components.marketprice.PriceChangeConfig -import com.tangem.core.ui.res.TangemTheme -import com.tangem.core.ui.res.TangemTypography -import com.tangem.feature.wallet.impl.R -import com.tangem.feature.wallet.presentation.common.state.TokenItemState -import com.tangem.feature.wallet.presentation.common.state.TokenItemState.TokenOptionsState -import org.burnoutcrew.reorderable.ReorderableLazyListState -import org.burnoutcrew.reorderable.detectReorder - -@Composable -internal fun TokenFiatInfoBlock( - state: TokenItemState, - modifier: Modifier = Modifier, - reorderableTokenListState: ReorderableLazyListState? = null, -) { - when (state) { - is TokenItemState.Content -> ContentBlock(state = state.tokenOptions, modifier = modifier) - is TokenItemState.Draggable -> { - DraggableBlock( - modifier = modifier, - reorderableTokenListState = reorderableTokenListState, - ) - } - is TokenItemState.Unreachable -> UnreachableBlock(modifier = modifier) - is TokenItemState.NoAddress -> NoAddressBlock(modifier = modifier) - is TokenItemState.Loading -> LoadingBlock(modifier = modifier) - is TokenItemState.Locked -> LockedBlock(modifier = modifier) - } -} - -@OptIn(ExperimentalAnimationApi::class) -@Composable -private fun ContentBlock(state: TokenOptionsState, modifier: Modifier = Modifier) { - Column( - modifier = modifier.requiredWidth(IntrinsicSize.Max), - verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing2), - ) { - AnimatedContent( - targetState = state, - label = "Update the fiat percentage block", - modifier = Modifier.align(Alignment.End), - ) { - Text( - text = if (it.isBalanceHidden) { - STARS - } else { - it.fiatAmount - }, - style = TangemTypography.body2, - color = TangemTheme.colors.text.primary1, - ) - } - - PriceChangeBlock(config = state.config) - } -} - -@Composable -private fun PriceChangeBlock(config: PriceChangeConfig) { - Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.End) { - PriceChangeIcon(type = config.type, modifier = Modifier.align(Alignment.CenterVertically)) - SpacerW4() - PriceChangeText(config = config, modifier = Modifier.align(Alignment.CenterVertically)) - } -} - -@OptIn(ExperimentalAnimationApi::class) -@Composable -private fun PriceChangeIcon(type: PriceChangeConfig.Type, modifier: Modifier = Modifier) { - AnimatedContent( - targetState = type, - label = "Update the price change's arrow", - modifier = modifier, - ) { - Icon( - painter = painterResource( - id = when (it) { - PriceChangeConfig.Type.UP -> R.drawable.ic_arrow_up_8 - PriceChangeConfig.Type.DOWN -> R.drawable.ic_arrow_down_8 - }, - ), - tint = when (it) { - PriceChangeConfig.Type.UP -> TangemTheme.colors.icon.accent - PriceChangeConfig.Type.DOWN -> TangemTheme.colors.icon.warning - }, - contentDescription = null, - ) - } -} - -@OptIn(ExperimentalAnimationApi::class) -@Composable -private fun PriceChangeText(config: PriceChangeConfig, modifier: Modifier = Modifier) { - AnimatedContent( - targetState = config.type, - label = "Update the price change's arrow", - modifier = modifier, - ) { - Text( - text = config.valueInPercent, - style = TangemTypography.body2, - color = when (it) { - PriceChangeConfig.Type.UP -> TangemTheme.colors.text.accent - PriceChangeConfig.Type.DOWN -> TangemTheme.colors.text.warning - }, - ) - } -} - -@Composable -private fun DraggableBlock(reorderableTokenListState: ReorderableLazyListState?, modifier: Modifier = Modifier) { - Box( - modifier = modifier - .size(TangemTheme.dimens.size32) - .then( - other = if (reorderableTokenListState != null) { - Modifier.detectReorder(reorderableTokenListState) - } else { - Modifier - }, - ), - contentAlignment = Alignment.Center, - ) { - Icon( - painter = painterResource(id = R.drawable.ic_drag_24), - tint = TangemTheme.colors.icon.informative, - contentDescription = null, - ) - } -} - -@Composable -private fun UnreachableBlock(modifier: Modifier = Modifier) { - Text( - modifier = modifier, - text = stringResource(id = R.string.common_unreachable), - style = TangemTypography.body2, - color = TangemTheme.colors.text.tertiary, - ) -} - -@Composable -private fun NoAddressBlock(modifier: Modifier = Modifier) { - Text( - modifier = modifier, - text = stringResource(id = R.string.common_no_address), - style = TangemTypography.body2, - color = TangemTheme.colors.text.tertiary, - ) -} - -@Composable -private fun LoadingBlock(modifier: Modifier = Modifier) { - NonContentContainer(modifier = modifier) { - RectangleShimmer(modifier = Modifier.viewSize(), radius = TangemTheme.dimens.radius4) - RectangleShimmer(modifier = Modifier.viewSize(), radius = TangemTheme.dimens.radius4) - } -} - -@Composable -private fun LockedBlock(modifier: Modifier = Modifier) { - NonContentContainer(modifier = modifier) { - LockedRectangle(modifier = Modifier.viewSize()) - LockedRectangle(modifier = Modifier.viewSize()) - } -} - -@Composable -private fun NonContentContainer(modifier: Modifier = Modifier, content: @Composable ColumnScope.() -> Unit) { - Column( - modifier = modifier, - verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing10), - content = content, - ) -} - -private fun Modifier.viewSize(): Modifier = composed { - return@composed size(width = TangemTheme.dimens.size40, height = TangemTheme.dimens.size12) -} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/token/TokenPriceChange.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/token/TokenPriceChange.kt new file mode 100644 index 0000000000..45d8194128 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/token/TokenPriceChange.kt @@ -0,0 +1,102 @@ +package com.tangem.feature.wallet.presentation.common.component.token + +import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.ExperimentalAnimationApi +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.composed +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.style.TextOverflow +import com.tangem.core.ui.components.RectangleShimmer +import com.tangem.core.ui.components.SpacerW4 +import com.tangem.core.ui.components.marketprice.PriceChangeConfig +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemTypography +import com.tangem.feature.wallet.impl.R +import com.tangem.feature.wallet.presentation.common.state.TokenItemState + +@OptIn(ExperimentalAnimationApi::class) +@Composable +internal fun TokenPriceChange(state: TokenItemState, modifier: Modifier = Modifier) { + AnimatedContent(targetState = state, label = "Update the price change", modifier = modifier) { animatedState -> + when (animatedState) { + is TokenItemState.Content -> { + PriceChangeBlock(config = animatedState.tokenOptions.config) + } + is TokenItemState.Loading -> { + RectangleShimmer(modifier = Modifier.placeholderSize(), radius = TangemTheme.dimens.radius4) + } + is TokenItemState.Locked -> { + LockedRectangle(modifier = Modifier.placeholderSize()) + } + is TokenItemState.Unreachable, + is TokenItemState.Draggable, + is TokenItemState.NoAddress, + -> Unit + } + } +} + +@Composable +private fun PriceChangeBlock(config: PriceChangeConfig) { + Row(horizontalArrangement = Arrangement.End) { + PriceChangeIcon( + type = config.type, + modifier = Modifier.align(Alignment.CenterVertically), + ) + SpacerW4() + PriceChangeText(config = config, modifier = Modifier.align(Alignment.CenterVertically)) + } +} + +@OptIn(ExperimentalAnimationApi::class) +@Composable +private fun PriceChangeIcon(type: PriceChangeConfig.Type, modifier: Modifier = Modifier) { + AnimatedContent( + targetState = type, + label = "Update the price change's arrow", + modifier = modifier, + ) { animatedType -> + Icon( + painter = painterResource( + id = when (animatedType) { + PriceChangeConfig.Type.UP -> R.drawable.ic_arrow_up_8 + PriceChangeConfig.Type.DOWN -> R.drawable.ic_arrow_down_8 + }, + ), + tint = when (animatedType) { + PriceChangeConfig.Type.UP -> TangemTheme.colors.icon.accent + PriceChangeConfig.Type.DOWN -> TangemTheme.colors.icon.warning + }, + contentDescription = null, + ) + } +} + +@Composable +private fun PriceChangeText(config: PriceChangeConfig, modifier: Modifier = Modifier) { + Text( + text = config.valueInPercent, + modifier = modifier, + color = when (config.type) { + PriceChangeConfig.Type.UP -> TangemTheme.colors.text.accent + PriceChangeConfig.Type.DOWN -> TangemTheme.colors.text.warning + }, + overflow = TextOverflow.Ellipsis, + maxLines = 1, + style = TangemTypography.body2, + ) +} + +private fun Modifier.placeholderSize(): Modifier = composed { + return@composed this + .padding(vertical = TangemTheme.dimens.spacing4) + .size(width = TangemTheme.dimens.size40, height = TangemTheme.dimens.size12) +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/token/TokenTitle.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/token/TokenTitle.kt new file mode 100644 index 0000000000..37106e707c --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/token/TokenTitle.kt @@ -0,0 +1,80 @@ +package com.tangem.feature.wallet.presentation.common.component.token + +import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.ExperimentalAnimationApi +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.* +import androidx.compose.material3.Text +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.composed +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.style.TextOverflow +import com.tangem.core.ui.components.RectangleShimmer +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemTypography +import com.tangem.feature.wallet.impl.R +import com.tangem.feature.wallet.presentation.common.state.TokenItemState + +@OptIn(ExperimentalAnimationApi::class) +@Composable +internal fun TokenTitle(state: TokenItemState, modifier: Modifier = Modifier) { + AnimatedContent(targetState = state, label = "Update title", modifier = modifier) { animatedState -> + when (animatedState) { + is TokenItemState.ContentState -> { + ContentTitle( + name = animatedState.name, + hasPending = (animatedState as? TokenItemState.Content)?.hasPending == true, + ) + } + is TokenItemState.Loading -> { + RectangleShimmer(modifier = Modifier.placeholderSize(), radius = TangemTheme.dimens.radius4) + } + is TokenItemState.Locked -> { + LockedRectangle(modifier = Modifier.placeholderSize()) + } + } + } +} + +@Composable +private fun ContentTitle(name: String, hasPending: Boolean) { + Row(horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing6)) { + CurrencyNameText(name = name, modifier = Modifier.weight(weight = 1f, fill = false)) + + PendingTransactionImage( + hasPending = hasPending, + modifier = Modifier.align(alignment = Alignment.CenterVertically), + ) + } +} + +@Composable +private fun CurrencyNameText(name: String, modifier: Modifier = Modifier) { + Text( + text = name, + modifier = modifier, + color = TangemTheme.colors.text.primary1, + overflow = TextOverflow.Ellipsis, + maxLines = 1, + style = TangemTypography.subtitle2, + ) +} + +@Composable +private fun PendingTransactionImage(hasPending: Boolean, modifier: Modifier = Modifier) { + AnimatedVisibility(visible = hasPending, modifier = modifier) { + Image( + painter = painterResource(id = R.drawable.img_loader_15), + contentDescription = null, + ) + } +} + +private fun Modifier.placeholderSize(): Modifier = composed { + return@composed this + .padding(vertical = TangemTheme.dimens.spacing4) + .size(width = TangemTheme.dimens.size70, height = TangemTheme.dimens.size12) +} \ No newline at end of file From c6fc0d49bab9697f2b9926874368ce9dbcd5c088 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 27 Sep 2023 17:08:13 +0300 Subject: [PATCH 104/242] Updated on 2026-08-14 --- .../tangem/datasource/di/QuotesStoreModule.kt | 14 +--- .../local/datastore/FileDataStore.kt | 8 +++ .../local/datastore/RuntimeDataStore.kt | 4 ++ .../datastore/SharedPreferencesDataStore.kt | 8 +++ .../local/datastore/core/DataStore.kt | 71 +++++++++++++++++++ .../core/KeylessDataStoreDecorator.kt | 2 +- .../core/StringKeyDataStoreDecorator.kt | 4 ++ .../local/quote/DefaultQuotesStore.kt | 21 ++++-- .../CurrenciesStatusesOperations.kt | 10 +-- ...PrimaryCurrencyStatusUpdatesUseCaseTest.kt | 26 ++++--- 10 files changed, 135 insertions(+), 33 deletions(-) diff --git a/core/datasource/src/main/java/com/tangem/datasource/di/QuotesStoreModule.kt b/core/datasource/src/main/java/com/tangem/datasource/di/QuotesStoreModule.kt index 91de405a4b..ff605b0138 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/di/QuotesStoreModule.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/di/QuotesStoreModule.kt @@ -1,15 +1,11 @@ package com.tangem.datasource.di -import android.content.Context -import com.squareup.moshi.Moshi -import com.tangem.datasource.local.datastore.JsonSharedPreferencesDataStore +import com.tangem.datasource.local.datastore.RuntimeDataStore import com.tangem.datasource.local.quote.DefaultQuotesStore import com.tangem.datasource.local.quote.QuotesStore -import com.tangem.datasource.local.quote.model.StoredQuote import dagger.Module import dagger.Provides import dagger.hilt.InstallIn -import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.components.SingletonComponent import javax.inject.Singleton @@ -19,13 +15,9 @@ internal object QuotesStoreModule { @Provides @Singleton - fun provideQuotesStore(@ApplicationContext context: Context, @NetworkMoshi moshi: Moshi): QuotesStore { + fun provideQuotesStore(): QuotesStore { return DefaultQuotesStore( - dataStore = JsonSharedPreferencesDataStore( - preferencesName = "quotes", - context = context, - adapter = moshi.adapter(StoredQuote::class.java), - ), + dataStore = RuntimeDataStore(), ) } } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/datastore/FileDataStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/datastore/FileDataStore.kt index 768a0e4c6a..e9b2317f35 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/datastore/FileDataStore.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/datastore/FileDataStore.kt @@ -17,6 +17,14 @@ internal class FileDataStore( ) : StringKeyDataStore { private val writeTrigger = Trigger() + override suspend fun isEmpty(): Boolean { + val e = NotImplementedError("`isEmpty()` function not implemented for `FileDataStore`") + Timber.e(e) + + throw e + } + + override suspend fun contains(key: String): Boolean = getSyncOrNull(key) != null override fun get(key: String): Flow { return writeTrigger diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/datastore/RuntimeDataStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/datastore/RuntimeDataStore.kt index 77aa987353..db39d064d3 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/datastore/RuntimeDataStore.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/datastore/RuntimeDataStore.kt @@ -11,6 +11,10 @@ internal class RuntimeDataStore : StringKeyDataStore { store.tryEmit(value = null) } + override suspend fun isEmpty(): Boolean = store.firstOrNull().isNullOrEmpty() + + override suspend fun contains(key: String): Boolean = getSyncOrNull(key) != null + override fun get(key: String): Flow { return store.mapNotNull { value -> value?.get(key) diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/datastore/SharedPreferencesDataStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/datastore/SharedPreferencesDataStore.kt index bf987439b8..417b4e9ac4 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/datastore/SharedPreferencesDataStore.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/datastore/SharedPreferencesDataStore.kt @@ -26,6 +26,14 @@ internal abstract class SharedPreferencesDataStore( abstract fun storeByKey(key: String, value: Value) + override suspend fun isEmpty(): Boolean { + return sharedPreferences.all.isEmpty() + } + + override suspend fun contains(key: String): Boolean { + return sharedPreferences.contains(key) + } + override fun get(key: String): Flow { return writeTrigger .mapNotNull { getInternal(key) } diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/datastore/core/DataStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/datastore/core/DataStore.kt index 828b0cb7b5..4746617f3b 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/datastore/core/DataStore.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/datastore/core/DataStore.kt @@ -2,23 +2,94 @@ package com.tangem.datasource.local.datastore.core import kotlinx.coroutines.flow.Flow +/** + * Represents a generic key-value data store. + * + * @param Key The type of the keys used to identify values in the store. + * @param Value The type of the values stored. + */ internal interface DataStore { + /** + * Checks if the data store is empty. + * + * @return `true` if the data store has no entries, otherwise `false`. + */ + suspend fun isEmpty(): Boolean + + /** + * Checks if the data store contains an entry with the specified key. + * + * @param key The key to check for presence in the store. + * @return `true` if the key is present, otherwise `false`. + */ + suspend fun contains(key: Key): Boolean + + /** + * Retrieves a value updates associated with the given key, as a flow. + * + * @param key The key to look up in the store. + * @return A flow emitting the value associated with the given key. + */ fun get(key: Key): Flow + /** + * Retrieves all values updates from the data store, as a flow. + * + * @return A flow emitting a list of all values in the store. + */ fun getAll(): Flow> + /** + * Retrieves a value associated with the given key synchronously. + * + * If the key does not exist, this method returns `null`. + * + * @param key The key to look up in the store. + * @return The value associated with the key, or `null` if not present. + */ suspend fun getSyncOrNull(key: Key): Value? + /** + * Retrieves all values from the data store synchronously. + * + * If the store is empty, this method returns `null`. + * + * @return A list of all values in the store, or `null` if empty. + */ suspend fun getAllSyncOrNull(): List? + /** + * Stores a value in the data store associated with the given key. + * + * @param key The key to associate with the value. + * @param value The value to store. + */ suspend fun store(key: Key, value: Value) + /** + * Stores multiple values in the data store with their associated keys. + * + * @param values A map of keys to values to store. + */ suspend fun store(values: Map) + /** + * Removes a value associated with the given key from the data store. + * + * @param key The key of the value to remove. + */ suspend fun remove(key: Key) + /** + * Removes values associated with the given keys from the data store. + * + * @param keys Keys of values to remove. + */ suspend fun remove(keys: Collection) + /** + * Clears all entries from the data store. + */ suspend fun clear() } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/datastore/core/KeylessDataStoreDecorator.kt b/core/datasource/src/main/java/com/tangem/datasource/local/datastore/core/KeylessDataStoreDecorator.kt index fd8f6b333c..421be2b698 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/datastore/core/KeylessDataStoreDecorator.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/datastore/core/KeylessDataStoreDecorator.kt @@ -13,7 +13,7 @@ internal abstract class KeylessDataStoreDecorator( open suspend fun store(item: Value) = store(key, item) - open suspend fun isEmpty() = getSyncOrNull() == null + override suspend fun isEmpty(): Boolean = getSyncOrNull() == null private companion object { const val DEFAULT_STRING_KEY = "key" diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/datastore/core/StringKeyDataStoreDecorator.kt b/core/datasource/src/main/java/com/tangem/datasource/local/datastore/core/StringKeyDataStoreDecorator.kt index d897ab9e7a..73c2afb6d4 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/datastore/core/StringKeyDataStoreDecorator.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/datastore/core/StringKeyDataStoreDecorator.kt @@ -8,6 +8,10 @@ internal abstract class StringKeyDataStoreDecorator( abstract fun provideStringKey(key: Key): String + override suspend fun isEmpty(): Boolean = wrappedDataStore.isEmpty() + + override suspend fun contains(key: Key): Boolean = wrappedDataStore.contains(provideStringKey(key)) + override fun get(key: Key): Flow { return wrappedDataStore.get(provideStringKey(key)) } diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/quote/DefaultQuotesStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/quote/DefaultQuotesStore.kt index 4d547208a2..667cb0f4b1 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/quote/DefaultQuotesStore.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/quote/DefaultQuotesStore.kt @@ -5,6 +5,7 @@ import com.tangem.datasource.local.datastore.core.StringKeyDataStore import com.tangem.datasource.local.quote.model.StoredQuote import com.tangem.domain.tokens.model.CryptoCurrency import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.channelFlow import kotlinx.coroutines.flow.combine internal class DefaultQuotesStore( @@ -12,16 +13,24 @@ internal class DefaultQuotesStore( ) : QuotesStore { override fun get(currenciesIds: Set): Flow> { - val flows = currenciesIds.mapNotNull { currencyId -> - dataStore.get(currencyId.rawCurrencyId ?: return@mapNotNull null) - } + return channelFlow { + val flows = currenciesIds.mapNotNull { currencyId -> + currencyId.rawCurrencyId?.let(dataStore::get) + } - return combine(flows) { quotes -> quotes.toSet() } + if (dataStore.isEmpty() || flows.isEmpty()) { + send(emptySet()) + } + + combine(flows) { quotes -> quotes.toSet() }.collect(::send) + } } override suspend fun store(response: QuotesResponse) { - response.quotes.forEach { (rawCurrencyId, quote) -> - dataStore.store(rawCurrencyId, StoredQuote(rawCurrencyId, quote)) + val quotes = response.quotes.mapValues { (id, quote) -> + StoredQuote(id, quote) } + + dataStore.store(quotes) } } \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesOperations.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesOperations.kt index 445b076284..fc4ec95f89 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesOperations.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesOperations.kt @@ -99,15 +99,17 @@ internal class CurrenciesStatusesOperations( val quoteFlow = getQuotes(currenciesIds) .map { maybeQuotes -> - maybeQuotes.map { quotes -> - quotes.singleOrNull { it.rawCurrencyId == currency.id.rawCurrencyId } + maybeQuotes.flatMap { quotes -> + quotes.singleOrNull { it.rawCurrencyId == currency.id.rawCurrencyId }?.right() + ?: Error.EmptyQuotes.left() } } val statusFlow = getNetworksStatuses(networks) .map { maybeStatuses -> - maybeStatuses.map { statuses -> - statuses.singleOrNull { it.network == currency.network } + maybeStatuses.flatMap { statuses -> + statuses.singleOrNull { it.network == currency.network }?.right() + ?: Error.EmptyNetworksStatuses.left() } } diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/GetPrimaryCurrencyStatusUpdatesUseCaseTest.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/GetPrimaryCurrencyStatusUpdatesUseCaseTest.kt index 010976fe3a..4abddd57fa 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/GetPrimaryCurrencyStatusUpdatesUseCaseTest.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/GetPrimaryCurrencyStatusUpdatesUseCaseTest.kt @@ -9,10 +9,7 @@ import com.tangem.domain.tokens.mock.MockNetworks import com.tangem.domain.tokens.mock.MockQuotes import com.tangem.domain.tokens.mock.MockTokens import com.tangem.domain.tokens.mock.MockTokensStates -import com.tangem.domain.tokens.model.CryptoCurrency -import com.tangem.domain.tokens.model.CryptoCurrencyStatus -import com.tangem.domain.tokens.model.NetworkStatus -import com.tangem.domain.tokens.model.Quote +import com.tangem.domain.tokens.model.* import com.tangem.domain.tokens.repository.MockCurrenciesRepository import com.tangem.domain.tokens.repository.MockNetworksRepository import com.tangem.domain.tokens.repository.MockQuotesRepository @@ -24,6 +21,7 @@ import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.runTest import org.junit.Test +import java.math.BigDecimal internal class GetPrimaryCurrencyStatusUpdatesUseCaseTest { @@ -113,9 +111,17 @@ internal class GetPrimaryCurrencyStatusUpdatesUseCaseTest { } @Test - fun `when quotes are empty and statuses are verified then loading token should be received`() = runTest { - val expectedResult = MockTokensStates.tokenState1 - .copy(value = CryptoCurrencyStatus.Loading) + fun `when quotes are empty and statuses are verified then token without quote should be received`() = runTest { + val expectedResult = with(MockTokensStates.tokenState1) { + copy( + value = CryptoCurrencyStatus.NoQuote( + amount = BigDecimal.TEN, + hasCurrentNetworkTransactions = false, + pendingTransactions = emptySet(), + networkAddress = NetworkAddress.Single(defaultAddress = "mock"), + ), + ) + } .right() val useCase = getUseCase( @@ -131,10 +137,8 @@ internal class GetPrimaryCurrencyStatusUpdatesUseCaseTest { } @Test - fun `when quotes are loaded and statuses are empty then loading token should be received`() = runTest { - val expectedResult = MockTokensStates.tokenState1 - .copy(value = CryptoCurrencyStatus.Loading) - .right() + fun `when quotes are loaded and statuses are empty then error be received`() = runTest { + val expectedResult = CurrencyStatusError.UnableToCreateCurrency.left() val useCase = getUseCase( statuses = flowOf(emptySet().right()), From 31b1b2182b2abdec32b018ff089cab0d613bc25f Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 27 Sep 2023 22:27:46 +0800 Subject: [PATCH 105/242] Updated on 2026-08-14 --- .../marketprice/MarketPriceBlock.kt | 22 +++---- .../marketprice/MarketPriceBlockState.kt | 2 +- .../marketprice/PriceChangeConfig.kt | 9 --- .../marketprice/PriceChangeState.kt | 13 ++++ .../TokenDetailsLoadedBalanceConverter.kt | 15 ++--- .../presentation/common/WalletPreviewData.kt | 16 ++--- .../common/component/TokenItem.kt | 8 ++- .../component/token/TokenPriceChange.kt | 64 +++++++++++-------- .../common/state/TokenItemState.kt | 8 ++- ...letSingleCurrencyLoadedBalanceConverter.kt | 15 ++--- ...ryptoCurrencyStatusToTokenItemConverter.kt | 34 +++++----- 11 files changed, 110 insertions(+), 96 deletions(-) delete mode 100644 core/ui/src/main/java/com/tangem/core/ui/components/marketprice/PriceChangeConfig.kt create mode 100644 core/ui/src/main/java/com/tangem/core/ui/components/marketprice/PriceChangeState.kt diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/marketprice/MarketPriceBlock.kt b/core/ui/src/main/java/com/tangem/core/ui/components/marketprice/MarketPriceBlock.kt index 61ae23802d..585426d477 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/marketprice/MarketPriceBlock.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/marketprice/MarketPriceBlock.kt @@ -130,7 +130,7 @@ private fun Price(price: String, modifier: Modifier = Modifier) { @OptIn(ExperimentalAnimationApi::class) @Composable -private fun PriceChangeInPercent(config: PriceChangeConfig) { +private fun PriceChangeInPercent(config: PriceChangeState.Content) { AnimatedContent(targetState = config.type, label = "Update price change") { type -> Row( verticalAlignment = Alignment.CenterVertically, @@ -139,13 +139,13 @@ private fun PriceChangeInPercent(config: PriceChangeConfig) { Icon( painter = painterResource( id = when (type) { - PriceChangeConfig.Type.UP -> R.drawable.ic_arrow_up_8 - PriceChangeConfig.Type.DOWN -> R.drawable.ic_arrow_down_8 + PriceChangeType.UP -> R.drawable.ic_arrow_up_8 + PriceChangeType.DOWN -> R.drawable.ic_arrow_down_8 }, ), tint = when (type) { - PriceChangeConfig.Type.UP -> TangemTheme.colors.icon.accent - PriceChangeConfig.Type.DOWN -> TangemTheme.colors.icon.warning + PriceChangeType.UP -> TangemTheme.colors.icon.accent + PriceChangeType.DOWN -> TangemTheme.colors.icon.warning }, contentDescription = null, ) @@ -153,8 +153,8 @@ private fun PriceChangeInPercent(config: PriceChangeConfig) { Text( text = config.valueInPercent, color = when (type) { - PriceChangeConfig.Type.UP -> TangemTheme.colors.text.accent - PriceChangeConfig.Type.DOWN -> TangemTheme.colors.text.warning + PriceChangeType.UP -> TangemTheme.colors.text.accent + PriceChangeType.DOWN -> TangemTheme.colors.text.warning }, style = TangemTheme.typography.body2, ) @@ -215,17 +215,17 @@ private class WalletMarketPriceBlockStateProvider : CollectionPreviewParameterPr MarketPriceBlockState.Content( currencyName = "BTC", price = "98900 $", - priceChangeConfig = PriceChangeConfig( + priceChangeConfig = PriceChangeState.Content( valueInPercent = "5.16%", - type = PriceChangeConfig.Type.DOWN, + type = PriceChangeType.DOWN, ), ), MarketPriceBlockState.Content( currencyName = "BTC", price = "98900 $", - priceChangeConfig = PriceChangeConfig( + priceChangeConfig = PriceChangeState.Content( valueInPercent = "10.89%", - type = PriceChangeConfig.Type.UP, + type = PriceChangeType.UP, ), ), MarketPriceBlockState.Loading(currencyName = "BTC"), diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/marketprice/MarketPriceBlockState.kt b/core/ui/src/main/java/com/tangem/core/ui/components/marketprice/MarketPriceBlockState.kt index 30b652ea68..e1c7a1e21a 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/marketprice/MarketPriceBlockState.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/marketprice/MarketPriceBlockState.kt @@ -14,6 +14,6 @@ sealed interface MarketPriceBlockState { data class Content( override val currencyName: String, val price: String, - val priceChangeConfig: PriceChangeConfig, + val priceChangeConfig: PriceChangeState.Content, ) : MarketPriceBlockState } \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/marketprice/PriceChangeConfig.kt b/core/ui/src/main/java/com/tangem/core/ui/components/marketprice/PriceChangeConfig.kt deleted file mode 100644 index 60af2367e5..0000000000 --- a/core/ui/src/main/java/com/tangem/core/ui/components/marketprice/PriceChangeConfig.kt +++ /dev/null @@ -1,9 +0,0 @@ -package com.tangem.core.ui.components.marketprice - -data class PriceChangeConfig(val valueInPercent: String, val type: Type) { - - /** Price changing type */ - enum class Type { - UP, DOWN - } -} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/marketprice/PriceChangeState.kt b/core/ui/src/main/java/com/tangem/core/ui/components/marketprice/PriceChangeState.kt new file mode 100644 index 0000000000..5130e77203 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/marketprice/PriceChangeState.kt @@ -0,0 +1,13 @@ +package com.tangem.core.ui.components.marketprice + +sealed class PriceChangeState { + + data class Content(val valueInPercent: String, val type: PriceChangeType) : PriceChangeState() + + object Unknown : PriceChangeState() +} + +/** Price changing type */ +enum class PriceChangeType { + UP, DOWN +} \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsLoadedBalanceConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsLoadedBalanceConverter.kt index 44401e8414..fa6aaee52a 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsLoadedBalanceConverter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsLoadedBalanceConverter.kt @@ -3,7 +3,8 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.state.factory import arrow.core.Either import com.tangem.common.Provider import com.tangem.core.ui.components.marketprice.MarketPriceBlockState -import com.tangem.core.ui.components.marketprice.PriceChangeConfig +import com.tangem.core.ui.components.marketprice.PriceChangeState +import com.tangem.core.ui.components.marketprice.PriceChangeType import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.tokens.error.CurrencyStatusError @@ -88,7 +89,7 @@ internal class TokenDetailsLoadedBalanceConverter( -> MarketPriceBlockState.Content( currencyName = currencyName, price = formatPrice(status, appCurrencyProvider()), - priceChangeConfig = PriceChangeConfig( + priceChangeConfig = PriceChangeState.Content( valueInPercent = formatPriceChange(status), type = getPriceChangeType(status), ), @@ -103,14 +104,10 @@ internal class TokenDetailsLoadedBalanceConverter( } } - private fun getPriceChangeType(status: CryptoCurrencyStatus.Status): PriceChangeConfig.Type { - val priceChange = status.priceChange ?: return PriceChangeConfig.Type.DOWN + private fun getPriceChangeType(status: CryptoCurrencyStatus.Status): PriceChangeType { + val priceChange = status.priceChange ?: return PriceChangeType.DOWN - return if (priceChange > BigDecimal.ZERO) { - PriceChangeConfig.Type.UP - } else { - PriceChangeConfig.Type.DOWN - } + return if (priceChange > BigDecimal.ZERO) PriceChangeType.UP else PriceChangeType.DOWN } private fun formatPriceChange(status: CryptoCurrencyStatus.Status): String { diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/WalletPreviewData.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/WalletPreviewData.kt index 566eb3e04d..5ae45f4206 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/WalletPreviewData.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/WalletPreviewData.kt @@ -4,7 +4,8 @@ import androidx.paging.PagingData import com.tangem.core.ui.R import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.components.marketprice.MarketPriceBlockState -import com.tangem.core.ui.components.marketprice.PriceChangeConfig +import com.tangem.core.ui.components.marketprice.PriceChangeState +import com.tangem.core.ui.components.marketprice.PriceChangeType import com.tangem.core.ui.components.transactions.state.TransactionState import com.tangem.core.ui.components.transactions.state.TxHistoryState import com.tangem.core.ui.event.consumedEvent @@ -129,10 +130,7 @@ internal object WalletPreviewData { hasPending = true, tokenOptions = TokenOptionsState( fiatAmount = "321 $", - config = PriceChangeConfig( - valueInPercent = "2%", - type = PriceChangeConfig.Type.UP, - ), + priceChangeState = PriceChangeState.Unknown, isBalanceHidden = false, ), onItemClick = {}, @@ -155,9 +153,9 @@ internal object WalletPreviewData { amount = "5,412 MATIC", hasPending = false, tokenOptions = TokenOptionsState( - config = PriceChangeConfig( + priceChangeState = PriceChangeState.Content( valueInPercent = "2%", - type = PriceChangeConfig.Type.UP, + type = PriceChangeType.UP, ), fiatAmount = "321 $", isBalanceHidden = false, @@ -417,9 +415,9 @@ internal object WalletPreviewData { marketPriceBlockState = MarketPriceBlockState.Content( currencyName = "BTC", price = "98900.12$", - priceChangeConfig = PriceChangeConfig( + priceChangeConfig = PriceChangeState.Content( valueInPercent = "5.16%", - type = PriceChangeConfig.Type.UP, + type = PriceChangeType.UP, ), ), txHistoryState = TxHistoryState.Content( diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/TokenItem.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/TokenItem.kt index 0125f47284..7e0fd572df 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/TokenItem.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/TokenItem.kt @@ -11,6 +11,8 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider import androidx.constraintlayout.compose.* +import com.tangem.core.ui.components.marketprice.PriceChangeState +import com.tangem.core.ui.components.marketprice.PriceChangeType import com.tangem.core.ui.extensions.rememberHapticFeedback import com.tangem.core.ui.res.TangemTheme import com.tangem.feature.wallet.presentation.common.WalletPreviewData @@ -203,16 +205,18 @@ private class TokenConfigProvider : CollectionPreviewParameterProvider when (animatedState) { is TokenItemState.Content -> { - PriceChangeBlock(config = animatedState.tokenOptions.config) + PriceChangeBlock(state = animatedState.tokenOptions.priceChangeState) } is TokenItemState.Loading -> { RectangleShimmer(modifier = Modifier.placeholderSize(), radius = TangemTheme.dimens.radius4) @@ -45,54 +43,70 @@ internal fun TokenPriceChange(state: TokenItemState, modifier: Modifier = Modifi } @Composable -private fun PriceChangeBlock(config: PriceChangeConfig) { +private fun PriceChangeBlock(state: PriceChangeState) { Row(horizontalArrangement = Arrangement.End) { PriceChangeIcon( - type = config.type, + type = (state as? PriceChangeState.Content)?.type, modifier = Modifier.align(Alignment.CenterVertically), ) SpacerW4() - PriceChangeText(config = config, modifier = Modifier.align(Alignment.CenterVertically)) + PriceChangeText(state = state, modifier = Modifier.align(Alignment.CenterVertically)) } } @OptIn(ExperimentalAnimationApi::class) @Composable -private fun PriceChangeIcon(type: PriceChangeConfig.Type, modifier: Modifier = Modifier) { +private fun PriceChangeIcon(type: PriceChangeType?, modifier: Modifier = Modifier) { AnimatedContent( targetState = type, label = "Update the price change's arrow", modifier = modifier, ) { animatedType -> + animatedType ?: return@AnimatedContent + Icon( painter = painterResource( id = when (animatedType) { - PriceChangeConfig.Type.UP -> R.drawable.ic_arrow_up_8 - PriceChangeConfig.Type.DOWN -> R.drawable.ic_arrow_down_8 + PriceChangeType.UP -> R.drawable.ic_arrow_up_8 + PriceChangeType.DOWN -> R.drawable.ic_arrow_down_8 }, ), tint = when (animatedType) { - PriceChangeConfig.Type.UP -> TangemTheme.colors.icon.accent - PriceChangeConfig.Type.DOWN -> TangemTheme.colors.icon.warning + PriceChangeType.UP -> TangemTheme.colors.icon.accent + PriceChangeType.DOWN -> TangemTheme.colors.icon.warning }, contentDescription = null, ) } } +@OptIn(ExperimentalAnimationApi::class) @Composable -private fun PriceChangeText(config: PriceChangeConfig, modifier: Modifier = Modifier) { - Text( - text = config.valueInPercent, +private fun PriceChangeText(state: PriceChangeState, modifier: Modifier = Modifier) { + AnimatedContent( + targetState = state, + label = "Update the price change's text", modifier = modifier, - color = when (config.type) { - PriceChangeConfig.Type.UP -> TangemTheme.colors.text.accent - PriceChangeConfig.Type.DOWN -> TangemTheme.colors.text.warning - }, - overflow = TextOverflow.Ellipsis, - maxLines = 1, - style = TangemTypography.body2, - ) + ) { animatedState -> + Text( + text = when (animatedState) { + is PriceChangeState.Content -> animatedState.valueInPercent + is PriceChangeState.Unknown -> TokenItemState.UNKNOWN_AMOUNT_SIGN + }, + color = when (animatedState) { + is PriceChangeState.Content -> { + when (animatedState.type) { + PriceChangeType.UP -> TangemTheme.colors.text.accent + PriceChangeType.DOWN -> TangemTheme.colors.text.warning + } + } + PriceChangeState.Unknown -> TangemTheme.colors.text.primary1 + }, + overflow = TextOverflow.Ellipsis, + maxLines = 1, + style = TangemTypography.body2, + ) + } } private fun Modifier.placeholderSize(): Modifier = composed { diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/state/TokenItemState.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/state/TokenItemState.kt index 06db91f953..ca960f320f 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/state/TokenItemState.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/state/TokenItemState.kt @@ -3,7 +3,7 @@ package com.tangem.feature.wallet.presentation.common.state import androidx.annotation.DrawableRes import androidx.compose.runtime.Immutable import androidx.compose.ui.graphics.Color -import com.tangem.core.ui.components.marketprice.PriceChangeConfig +import com.tangem.core.ui.components.marketprice.PriceChangeState import com.tangem.core.ui.extensions.TextReference /** Token item state */ @@ -166,8 +166,12 @@ internal sealed interface TokenItemState { /** Token options state */ @Immutable data class TokenOptionsState( - val config: PriceChangeConfig, + val priceChangeState: PriceChangeState, val fiatAmount: String, val isBalanceHidden: Boolean, ) + + companion object { + const val UNKNOWN_AMOUNT_SIGN = "—" + } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletSingleCurrencyLoadedBalanceConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletSingleCurrencyLoadedBalanceConverter.kt index d0d570cacb..99e0575bb5 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletSingleCurrencyLoadedBalanceConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletSingleCurrencyLoadedBalanceConverter.kt @@ -3,7 +3,8 @@ package com.tangem.feature.wallet.presentation.wallet.state.factory import arrow.core.Either import com.tangem.common.Provider import com.tangem.core.ui.components.marketprice.MarketPriceBlockState -import com.tangem.core.ui.components.marketprice.PriceChangeConfig +import com.tangem.core.ui.components.marketprice.PriceChangeState +import com.tangem.core.ui.components.marketprice.PriceChangeType import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.tokens.error.CurrencyStatusError @@ -59,7 +60,7 @@ internal class WalletSingleCurrencyLoadedBalanceConverter( -> MarketPriceBlockState.Content( currencyName = currencyName, price = formatPrice(status, appCurrencyProvider()), - priceChangeConfig = PriceChangeConfig( + priceChangeConfig = PriceChangeState.Content( valueInPercent = formatPriceChange(status), type = getPriceChangeType(status), ), @@ -128,14 +129,10 @@ internal class WalletSingleCurrencyLoadedBalanceConverter( ) } - private fun getPriceChangeType(status: CryptoCurrencyStatus.Status): PriceChangeConfig.Type { - val priceChange = status.priceChange ?: return PriceChangeConfig.Type.DOWN + private fun getPriceChangeType(status: CryptoCurrencyStatus.Status): PriceChangeType { + val priceChange = status.priceChange ?: return PriceChangeType.DOWN - return if (priceChange > BigDecimal.ZERO) { - PriceChangeConfig.Type.UP - } else { - PriceChangeConfig.Type.DOWN - } + return if (priceChange > BigDecimal.ZERO) PriceChangeType.UP else PriceChangeType.DOWN } private fun formatPriceChange(status: CryptoCurrencyStatus.Status): String { diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/CryptoCurrencyStatusToTokenItemConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/CryptoCurrencyStatusToTokenItemConverter.kt index 08bb0e8704..f434ec4117 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/CryptoCurrencyStatusToTokenItemConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/CryptoCurrencyStatusToTokenItemConverter.kt @@ -1,7 +1,8 @@ package com.tangem.feature.wallet.presentation.wallet.utils import com.tangem.common.Provider -import com.tangem.core.ui.components.marketprice.PriceChangeConfig +import com.tangem.core.ui.components.marketprice.PriceChangeState +import com.tangem.core.ui.components.marketprice.PriceChangeType import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus @@ -44,7 +45,7 @@ internal class CryptoCurrencyStatusToTokenItemConverter( hasPending = value.hasCurrentNetworkTransactions, tokenOptions = TokenItemState.TokenOptionsState( fiatAmount = getFormattedFiatAmount(), - config = getPriceChangeConfig(), + priceChangeState = getPriceChangeConfig(), isBalanceHidden = isBalanceHiddenProvider(), ), onItemClick = { clickIntents.onTokenItemClick(currency) }, @@ -53,13 +54,13 @@ internal class CryptoCurrencyStatusToTokenItemConverter( } private fun CryptoCurrencyStatus.getFormattedAmount(): String { - val amount = value.amount ?: return UNKNOWN_AMOUNT_SIGN + val amount = value.amount ?: return TokenItemState.UNKNOWN_AMOUNT_SIGN return BigDecimalFormatter.formatCryptoAmount(amount, currency.symbol, currency.decimals) } private fun CryptoCurrencyStatus.getFormattedFiatAmount(): String { - val fiatAmount = value.fiatAmount ?: return UNKNOWN_AMOUNT_SIGN + val fiatAmount = value.fiatAmount ?: return TokenItemState.UNKNOWN_AMOUNT_SIGN val appCurrency = appCurrencyProvider() return BigDecimalFormatter.formatFiatAmount(fiatAmount, appCurrency.code, appCurrency.symbol) @@ -80,25 +81,20 @@ internal class CryptoCurrencyStatusToTokenItemConverter( onItemLongClick = { clickIntents.onTokenItemLongClick(cryptoCurrencyStatus = this) }, ) - private fun CryptoCurrencyStatus.getPriceChangeConfig(): PriceChangeConfig { + private fun CryptoCurrencyStatus.getPriceChangeConfig(): PriceChangeState { val priceChange = value.priceChange - ?: return PriceChangeConfig(UNKNOWN_AMOUNT_SIGN, PriceChangeConfig.Type.DOWN) - return PriceChangeConfig( - valueInPercent = BigDecimalFormatter.formatPercent(priceChange, useAbsoluteValue = true), - type = priceChange.getPriceChangeType(), - ) - } - - private fun BigDecimal?.getPriceChangeType(): PriceChangeConfig.Type { - return when { - this == null -> PriceChangeConfig.Type.DOWN - this < BigDecimal.ZERO -> PriceChangeConfig.Type.DOWN - else -> PriceChangeConfig.Type.UP + return if (priceChange != null) { + PriceChangeState.Content( + valueInPercent = BigDecimalFormatter.formatPercent(percent = priceChange, useAbsoluteValue = true), + type = priceChange.getPriceChangeType(), + ) + } else { + PriceChangeState.Unknown } } - private companion object { - const val UNKNOWN_AMOUNT_SIGN = "—" + private fun BigDecimal.getPriceChangeType(): PriceChangeType { + return if (this > BigDecimal.ZERO) PriceChangeType.UP else PriceChangeType.DOWN } } \ No newline at end of file From a79b38c2a4c9c96b3ffbef48b86a8e6625c47669 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 25 Sep 2023 14:17:26 +0300 Subject: [PATCH 106/242] Updated on 2026-08-14 --- .../com/tangem/tap/domain/TangemSdkManager.kt | 16 ++-- .../di/UserWalletsListManagerProvider.kt | 13 +++- .../BiometricUserWalletsListManager.kt | 4 +- .../repository/UserWalletsKeysRepository.kt | 6 +- .../UserWalletsKeysStoreDecorator.kt | 31 ++++++++ .../BiometricUserWalletsKeysRepository.kt | 73 +++++++------------ gradle/dependencies.toml | 2 +- 7 files changed, 85 insertions(+), 60 deletions(-) create mode 100644 app/src/main/java/com/tangem/tap/domain/userWalletList/repository/UserWalletsKeysStoreDecorator.kt diff --git a/app/src/main/java/com/tangem/tap/domain/TangemSdkManager.kt b/app/src/main/java/com/tangem/tap/domain/TangemSdkManager.kt index bbf617b817..54b6313c9c 100644 --- a/app/src/main/java/com/tangem/tap/domain/TangemSdkManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/TangemSdkManager.kt @@ -5,10 +5,11 @@ import androidx.annotation.StringRes import com.tangem.Message import com.tangem.TangemSdk import com.tangem.common.* -import com.tangem.common.biometric.BiometricManager +import com.tangem.common.authentication.KeystoreManager import com.tangem.common.card.FirmwareVersion import com.tangem.common.core.* import com.tangem.common.extensions.ByteArrayKey +import com.tangem.common.services.secure.SecureStorage import com.tangem.common.usersCode.UserCodeRepository import com.tangem.core.analytics.Analytics import com.tangem.crypto.bip39.DefaultMnemonic @@ -47,19 +48,22 @@ class TangemSdkManager( private val userCodeRepository by lazy { UserCodeRepository( - biometricManager = tangemSdk.biometricManager, + keystoreManager = tangemSdk.keystoreManager, secureStorage = tangemSdk.secureStorage, ) } val canUseBiometry: Boolean - get() = tangemSdk.biometricManager.canAuthenticate || needEnrollBiometrics + get() = tangemSdk.authenticationManager.canAuthenticate || needEnrollBiometrics val needEnrollBiometrics: Boolean - get() = tangemSdk.biometricManager.canEnrollBiometrics + get() = tangemSdk.authenticationManager.canEnrollBiometrics - val biometricManager: BiometricManager - get() = tangemSdk.biometricManager + val keystoreManager: KeystoreManager + get() = tangemSdk.keystoreManager + + val secureStorage: SecureStorage + get() = tangemSdk.secureStorage val userCodeRequestPolicy: UserCodeRequestPolicy get() = tangemSdk.config.userCodeRequestPolicy diff --git a/app/src/main/java/com/tangem/tap/domain/userWalletList/di/UserWalletsListManagerProvider.kt b/app/src/main/java/com/tangem/tap/domain/userWalletList/di/UserWalletsListManagerProvider.kt index 5f02cd1258..2657240db8 100644 --- a/app/src/main/java/com/tangem/tap/domain/userWalletList/di/UserWalletsListManagerProvider.kt +++ b/app/src/main/java/com/tangem/tap/domain/userWalletList/di/UserWalletsListManagerProvider.kt @@ -3,6 +3,7 @@ package com.tangem.tap.domain.userWalletList.di import android.content.Context import com.squareup.moshi.Moshi import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory +import com.tangem.common.authentication.AuthenticatedStorage import com.tangem.common.json.TangemSdkAdapter import com.tangem.common.services.secure.SecureStorage import com.tangem.domain.wallets.legacy.UserWalletsListManager @@ -11,6 +12,7 @@ import com.tangem.sdk.storage.createEncryptedSharedPreferences import com.tangem.tap.domain.TangemSdkManager import com.tangem.tap.domain.userWalletList.implementation.BiometricUserWalletsListManager import com.tangem.tap.domain.userWalletList.implementation.RuntimeUserWalletsListManager +import com.tangem.tap.domain.userWalletList.repository.UserWalletsKeysStoreDecorator import com.tangem.tap.domain.userWalletList.repository.implementation.BiometricUserWalletsKeysRepository import com.tangem.tap.domain.userWalletList.repository.implementation.DefaultSelectedUserWalletRepository import com.tangem.tap.domain.userWalletList.repository.implementation.DefaultUserWalletsPublicInformationRepository @@ -43,10 +45,19 @@ fun UserWalletsListManager.Companion.provideBiometricImplementation( ), ) + val authenticatedStorage = AuthenticatedStorage( + secureStorage = UserWalletsKeysStoreDecorator( + featureStorage = secureStorage, + cardSdkStorage = tangemSdkManager.secureStorage, + ), + keystoreManager = tangemSdkManager.keystoreManager, + ) + val keysRepository = BiometricUserWalletsKeysRepository( moshi = moshi, secureStorage = secureStorage, - biometricManager = tangemSdkManager.biometricManager, + authenticatedStorage = authenticatedStorage, + ) val publicInformationRepository = DefaultUserWalletsPublicInformationRepository( moshi = moshi, diff --git a/app/src/main/java/com/tangem/tap/domain/userWalletList/implementation/BiometricUserWalletsListManager.kt b/app/src/main/java/com/tangem/tap/domain/userWalletList/implementation/BiometricUserWalletsListManager.kt index 76a94f34bd..59819e3d7d 100644 --- a/app/src/main/java/com/tangem/tap/domain/userWalletList/implementation/BiometricUserWalletsListManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/userWalletList/implementation/BiometricUserWalletsListManager.kt @@ -141,7 +141,7 @@ internal class BiometricUserWalletsListManager( return sensitiveInformationRepository.delete(idsToRemove) .flatMap { publicInformationRepository.delete(idsToRemove) } - .flatMap { keysRepository.delete(idsToRemove) } + .map { keysRepository.delete(idsToRemove) } .map { state.update { prevState -> val newUserWallets = prevState.userWallets.filter { it.walletId !in idsToRemove } @@ -158,7 +158,7 @@ internal class BiometricUserWalletsListManager( override suspend fun clear(): CompletionResult { return sensitiveInformationRepository.clear() .flatMap { publicInformationRepository.clear() } - .flatMap { keysRepository.clear() } + .map { keysRepository.clear() } .map { selectedUserWalletRepository.set(null) lock() diff --git a/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/UserWalletsKeysRepository.kt b/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/UserWalletsKeysRepository.kt index 08c509a16c..170296fbe1 100644 --- a/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/UserWalletsKeysRepository.kt +++ b/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/UserWalletsKeysRepository.kt @@ -23,15 +23,13 @@ internal interface UserWalletsKeysRepository { /** * Delete encryption keys for user wallets. Biometric authentication not required * @param userWalletsIds List of [UserWalletId] whose encryption keys will be deleted - * @return [CompletionResult] of operation * */ - suspend fun delete(userWalletsIds: List): CompletionResult + suspend fun delete(userWalletsIds: List) /** * Clear all encryption keys for user wallets. Biometric authentication not required - * @return [CompletionResult] of operation * */ - suspend fun clear(): CompletionResult + suspend fun clear() /** * Determine if the user has saved user wallets diff --git a/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/UserWalletsKeysStoreDecorator.kt b/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/UserWalletsKeysStoreDecorator.kt new file mode 100644 index 0000000000..662da27e35 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/UserWalletsKeysStoreDecorator.kt @@ -0,0 +1,31 @@ +package com.tangem.tap.domain.userWalletList.repository + +import com.tangem.common.services.secure.SecureStorage + +/** + * A decorator for [SecureStorage] that facilitates data migration between two storages. + * + * @property featureStorage The primary storage, which will eventually contain all user data. + * @property cardSdkStorage The SDK's storage where user data might have been previously stored. + */ +internal class UserWalletsKeysStoreDecorator( + private val featureStorage: SecureStorage, + private val cardSdkStorage: SecureStorage, +) : SecureStorage by featureStorage { + + override fun delete(account: String) { + featureStorage.delete(account) + cardSdkStorage.delete(account) + } + + override fun get(account: String): ByteArray? { + var data = featureStorage.get(account) + + if (data == null) { + data = cardSdkStorage.get(account) ?: return null + featureStorage.store(data, account) + } + + return data + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/implementation/BiometricUserWalletsKeysRepository.kt b/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/implementation/BiometricUserWalletsKeysRepository.kt index 0d4dd5d1c5..bb40308916 100644 --- a/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/implementation/BiometricUserWalletsKeysRepository.kt +++ b/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/implementation/BiometricUserWalletsKeysRepository.kt @@ -4,8 +4,7 @@ import com.squareup.moshi.JsonAdapter import com.squareup.moshi.Moshi import com.squareup.moshi.Types import com.tangem.common.* -import com.tangem.common.biometric.BiometricManager -import com.tangem.common.biometric.BiometricStorage +import com.tangem.common.authentication.AuthenticatedStorage import com.tangem.common.core.TangemSdkError import com.tangem.common.services.secure.SecureStorage import com.tangem.domain.wallets.legacy.UserWalletsListError @@ -18,13 +17,10 @@ import kotlinx.coroutines.withContext internal class BiometricUserWalletsKeysRepository( moshi: Moshi, - biometricManager: BiometricManager, + private val authenticatedStorage: AuthenticatedStorage, private val secureStorage: SecureStorage, ) : UserWalletsKeysRepository { - private val biometricStorage = BiometricStorage( - biometricManager = biometricManager, - secureStorage = secureStorage, - ) + private val encryptionKeyAdapter: JsonAdapter = moshi.adapter( UserWalletEncryptionKey::class.java, ) @@ -37,13 +33,13 @@ internal class BiometricUserWalletsKeysRepository( getAllInternal() .mapFailure { error -> when (error) { - is TangemSdkError.BiometricsAuthenticationLockout -> + is TangemSdkError.AuthenticationLockout -> UserWalletsListError.BiometricsAuthenticationLockout(isPermanent = false) - is TangemSdkError.BiometricsAuthenticationPermanentLockout -> + is TangemSdkError.AuthenticationPermanentLockout -> UserWalletsListError.BiometricsAuthenticationLockout(isPermanent = true) - is TangemSdkError.BiometricCryptographyKeyInvalidated -> + is TangemSdkError.KeystoreInvalidated -> UserWalletsListError.EncryptionKeyInvalidated - is TangemSdkError.BiometricsUnavailable -> + is TangemSdkError.AuthenticationUnavailable -> UserWalletsListError.BiometricsAuthenticationDisabled else -> error } @@ -57,26 +53,24 @@ internal class BiometricUserWalletsKeysRepository( } } - override suspend fun delete(userWalletsIds: List): CompletionResult { + override suspend fun delete(userWalletsIds: List) { return withContext(Dispatchers.IO) { - userWalletsIds.map { userWalletId -> + userWalletsIds.forEach { userWalletId -> deleteEncryptionKey(userWalletId) } - .fold() - .map { deleteUserWalletsIds(userWalletsIds) } + + deleteUserWalletsIds(userWalletsIds) } } - override suspend fun clear(): CompletionResult { + override suspend fun clear() { return withContext(Dispatchers.IO) { getUserWalletsIds() - .map { userWalletId -> + .forEach { userWalletId -> deleteEncryptionKey(userWalletId) } - .fold() - .map { - clearUserWalletsIds() - } + + clearUserWalletsIds() } } @@ -89,35 +83,20 @@ internal class BiometricUserWalletsKeysRepository( private suspend fun getAllInternal(): CompletionResult> { return getUserWalletsIds() .map { userWalletId -> - // It is possible to request multiple user wallet keys from biometric storage because - // the biometric cryptography key has an expiration time. - // If this operation runs more than that expiration time, then the user will have to re-authorize - // to receive all user wallets encryption keys getEncryptionKey(userWalletId) - .flatMapOnFailure { error -> - when (error) { - is TangemSdkError.InvalidBiometricCryptographyKey, - is TangemSdkError.BiometricCryptographyOperationFailed, - -> { - // These errors can be skipped as the user has the option to re-save their wallets - // in case they occur - CompletionResult.Success(data = null) - } - else -> CompletionResult.Failure(error) - } - } .doOnFailure { error -> when (error) { - is TangemSdkError.UserCanceledBiometricsAuthentication -> { + is TangemSdkError.UserCanceledAuthentication -> { // If the user cancels biometric authentication, then cancel operation with error return CompletionResult.Failure(error) } - is TangemSdkError.BiometricCryptographyKeyInvalidated -> { + is TangemSdkError.KeystoreInvalidated -> { // If the biometric cryptography key was invalidated, // then delete all user wallets encryption keys and cancel operation with error getUserWalletsIds().forEach { userWalletId -> deleteEncryptionKey(userWalletId) } + return CompletionResult.Failure(error) } } @@ -129,20 +108,22 @@ internal class BiometricUserWalletsKeysRepository( } private suspend fun getEncryptionKey(userWalletId: UserWalletId): CompletionResult { - return biometricStorage.get(StorageKey.UserWalletEncryptionKey(userWalletId).name) + return catching { authenticatedStorage.get(StorageKey.UserWalletEncryptionKey(userWalletId).name) } .map { it.decodeToKey() } } private suspend fun storeEncryptionKey(encryptionKey: UserWalletEncryptionKey): CompletionResult { - return biometricStorage.store( - key = StorageKey.UserWalletEncryptionKey(encryptionKey.walletId).name, - data = encryptionKey.encode(), - ) + return catching { + authenticatedStorage.store( + key = StorageKey.UserWalletEncryptionKey(encryptionKey.walletId).name, + data = encryptionKey.encode(), + ) + } .map { storeUserWalletId(encryptionKey.walletId) } } - private suspend fun deleteEncryptionKey(userWalletId: UserWalletId): CompletionResult { - return biometricStorage.delete(StorageKey.UserWalletEncryptionKey(userWalletId).name) + private fun deleteEncryptionKey(userWalletId: UserWalletId) { + return authenticatedStorage.delete(StorageKey.UserWalletEncryptionKey(userWalletId).name) } private suspend fun getUserWalletsIds(): List { diff --git a/gradle/dependencies.toml b/gradle/dependencies.toml index e9679b30b3..89c974eda0 100644 --- a/gradle/dependencies.toml +++ b/gradle/dependencies.toml @@ -84,7 +84,7 @@ okHttp-prettyLogging = "3.1.0" # region Tangem tangemBlockchainSdk = "develop-351" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds -tangemCardSdk = "develop-297" +tangemCardSdk = "develop-300" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds # endregion Tangem From 4935692735519ebf78d64b977c1259f7fe9ad690 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 27 Sep 2023 22:58:39 +0800 Subject: [PATCH 107/242] Updated on 2026-08-14 --- .../impl/di/CustomTokenInteractorModule.kt | 3 + .../domain/DefaultCustomTokenInteractor.kt | 59 +++++++++++++++++++ 2 files changed, 62 insertions(+) diff --git a/app/src/main/java/com/tangem/tap/features/customtoken/impl/di/CustomTokenInteractorModule.kt b/app/src/main/java/com/tangem/tap/features/customtoken/impl/di/CustomTokenInteractorModule.kt index 4ae3926d84..4d8d3d66dc 100644 --- a/app/src/main/java/com/tangem/tap/features/customtoken/impl/di/CustomTokenInteractorModule.kt +++ b/app/src/main/java/com/tangem/tap/features/customtoken/impl/di/CustomTokenInteractorModule.kt @@ -1,6 +1,7 @@ package com.tangem.tap.features.customtoken.impl.di import com.tangem.datasource.api.tangemTech.TangemTechApi +import com.tangem.domain.wallets.usecase.GetSelectedWalletUseCase import com.tangem.tap.features.customtoken.impl.data.DefaultCustomTokenRepository import com.tangem.tap.features.customtoken.impl.domain.CustomTokenInteractor import com.tangem.tap.features.customtoken.impl.domain.DefaultCustomTokenInteractor @@ -24,6 +25,7 @@ internal object CustomTokenInteractorModule { fun provideCustomTokenInteractor( tangemTechApi: TangemTechApi, appCoroutineDispatcherProvider: AppCoroutineDispatcherProvider, + getSelectedWalletUseCase: GetSelectedWalletUseCase, reduxStateHolder: AppStateHolder, ): CustomTokenInteractor { return DefaultCustomTokenInteractor( @@ -32,6 +34,7 @@ internal object CustomTokenInteractorModule { dispatchers = appCoroutineDispatcherProvider, reduxStateHolder = reduxStateHolder, ), + getSelectedWalletUseCase = getSelectedWalletUseCase, reduxStateHolder = reduxStateHolder, ) } diff --git a/app/src/main/java/com/tangem/tap/features/customtoken/impl/domain/DefaultCustomTokenInteractor.kt b/app/src/main/java/com/tangem/tap/features/customtoken/impl/domain/DefaultCustomTokenInteractor.kt index f104a8ca09..a392516294 100644 --- a/app/src/main/java/com/tangem/tap/features/customtoken/impl/domain/DefaultCustomTokenInteractor.kt +++ b/app/src/main/java/com/tangem/tap/features/customtoken/impl/domain/DefaultCustomTokenInteractor.kt @@ -9,12 +9,17 @@ import com.tangem.common.extensions.guard import com.tangem.common.extensions.toMapKey import com.tangem.common.flatMap import com.tangem.crypto.hdWallet.DerivationPath +import com.tangem.data.tokens.utils.CryptoCurrencyFactory import com.tangem.domain.common.configs.CardConfig import com.tangem.domain.common.extensions.toNetworkId import com.tangem.domain.common.util.derivationStyleProvider import com.tangem.domain.common.util.hasDerivation import com.tangem.domain.features.addCustomToken.CustomCurrency import com.tangem.domain.models.scan.ScanResponse +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.domain.wallets.usecase.GetSelectedWalletUseCase import com.tangem.operations.derivation.ExtendedPublicKeysMap import com.tangem.tap.* import com.tangem.tap.common.extensions.dispatchDebugErrorNotification @@ -25,7 +30,9 @@ import com.tangem.tap.features.customtoken.impl.domain.models.FoundToken import com.tangem.tap.features.tokens.legacy.redux.TokensMiddleware import com.tangem.tap.features.wallet.models.Currency import com.tangem.tap.proxy.AppStateHolder +import com.tangem.tap.proxy.redux.DaggerGraphState import kotlinx.coroutines.delay +import kotlinx.coroutines.launch import timber.log.Timber /** @@ -38,6 +45,7 @@ import timber.log.Timber */ class DefaultCustomTokenInteractor( private val featureRepository: CustomTokenRepository, + private val getSelectedWalletUseCase: GetSelectedWalletUseCase, private val reduxStateHolder: AppStateHolder, ) : CustomTokenInteractor { @@ -154,6 +162,40 @@ class DefaultCustomTokenInteractor( } private suspend fun submitAdd(scanResponse: ScanResponse, currency: Currency) { + val walletFeatureToggles = store.state.daggerGraphState.get(DaggerGraphState::walletFeatureToggles) + + if (walletFeatureToggles.isRedesignedScreenEnabled) { + val cryptoCurrencyFactory = CryptoCurrencyFactory() + + submitNewAdd( + userWalletId = getSelectedWalletUseCase().fold(ifLeft = { return }, ifRight = UserWallet::walletId), + updatedScanResponse = scanResponse, + currencyList = listOfNotNull( + when (currency) { + is Currency.Blockchain -> { + cryptoCurrencyFactory.createCoin( + blockchain = currency.blockchain, + extraDerivationPath = null, + derivationStyleProvider = scanResponse.derivationStyleProvider, + ) + } + is Currency.Token -> { + cryptoCurrencyFactory.createToken( + sdkToken = currency.token, + blockchain = currency.blockchain, + extraDerivationPath = null, + derivationStyleProvider = scanResponse.derivationStyleProvider, + ) + } + }, + ), + ) + } else { + submitLegacyAdd(scanResponse = scanResponse, currency = currency) + } + } + + private suspend fun submitLegacyAdd(scanResponse: ScanResponse, currency: Currency) { val selectedUserWallet = userWalletsListManager.selectedUserWalletSync.guard { Timber.e("Unable to add currencies, no user wallet selected") return @@ -170,4 +212,21 @@ class DefaultCustomTokenInteractor( ) } } + + private fun submitNewAdd( + userWalletId: UserWalletId, + updatedScanResponse: ScanResponse, + currencyList: List, + ) { + val currenciesRepository = store.state.daggerGraphState.get(DaggerGraphState::currenciesRepository) + + scope.launch { + userWalletsListManager.update( + userWalletId = userWalletId, + update = { it.copy(scanResponse = updatedScanResponse) }, + ) + + currenciesRepository.addCurrencies(userWalletId = userWalletId, currencies = currencyList) + } + } } \ No newline at end of file From 3fe6b7008a2c23f3a55d33e9a1bcc5fcaa496fa1 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 27 Sep 2023 18:07:20 +0300 Subject: [PATCH 108/242] Updated on 2026-08-14 --- .../domain/tokens/GetCryptoCurrencyActionsUseCase.kt | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyActionsUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyActionsUseCase.kt index 6727184224..cd63eb702a 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyActionsUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyActionsUseCase.kt @@ -70,7 +70,12 @@ class GetCryptoCurrencyActionsUseCase( } // send - activeList.add(TokenActionsState.ActionState.Send(true)) + if (cryptoCurrencyStatus.value.amount?.signum() == 0) { + disabledList.add(TokenActionsState.ActionState.Send(false)) + } else { + activeList.add(TokenActionsState.ActionState.Send(true)) + } + // receive activeList.add(TokenActionsState.ActionState.Receive(true)) From 1d151c11a16f70636abe26ab95ecd543476c5bd7 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 27 Sep 2023 12:15:54 +0300 Subject: [PATCH 109/242] Updated on 2026-08-14 --- .../organizetokens/OrganizeTokensStateHolder.kt | 13 +++---------- .../organizetokens/OrganizeTokensViewModel.kt | 10 ++++------ .../organizetokens/utils/dnd/DragAndDropAdapter.kt | 10 +++++----- .../presentation/router/DefaultWalletRouter.kt | 8 ++++---- 4 files changed, 16 insertions(+), 25 deletions(-) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensStateHolder.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensStateHolder.kt index a00a26f468..25598c9983 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensStateHolder.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensStateHolder.kt @@ -16,15 +16,14 @@ import com.tangem.feature.wallet.presentation.organizetokens.utils.converter.err import com.tangem.feature.wallet.presentation.organizetokens.utils.converter.items.CryptoCurrencyToDraggableItemConverter import com.tangem.feature.wallet.presentation.organizetokens.utils.converter.items.NetworkGroupToDraggableItemsConverter import com.tangem.feature.wallet.presentation.organizetokens.utils.converter.items.TokenListToListStateConverter -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.flow.* +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.update internal class OrganizeTokensStateHolder( private val intents: OrganizeTokensIntents, private val dragAndDropIntents: DragAndDropIntents, private val appCurrencyProvider: Provider, - private val onSubscription: () -> Unit, - stateFlowScope: CoroutineScope, ) { private val stateFlowInternal: MutableStateFlow = MutableStateFlow(getInitialState()) @@ -52,12 +51,6 @@ internal class OrganizeTokensStateHolder( } val stateFlow: StateFlow = stateFlowInternal - .onSubscription { onSubscription() } - .stateIn( - scope = stateFlowScope, - started = SharingStarted.WhileSubscribed(), - initialValue = getInitialState(), - ) fun updateStateWithTokenList(tokenList: TokenList) { updateState { tokenListConverter.convert(tokenList) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensViewModel.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensViewModel.kt index a972f62260..9fa199b6a1 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensViewModel.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensViewModel.kt @@ -50,14 +50,9 @@ internal class OrganizeTokensViewModel @Inject constructor( ) private val stateHolder = OrganizeTokensStateHolder( - stateFlowScope = viewModelScope, intents = this, dragAndDropIntents = dragAndDropAdapter, appCurrencyProvider = Provider(selectedAppCurrencyFlow::value), - onSubscription = { - bootstrapTokenList() - bootstrapDragAndDropUpdates() - }, ) private val userWalletId: UserWalletId by lazy { @@ -72,6 +67,9 @@ internal class OrganizeTokensViewModel @Inject constructor( override fun onCreate(owner: LifecycleOwner) { analyticsEventsHandler.send(PortfolioOrganizeTokensAnalyticsEvent.ScreenOpened) + + bootstrapTokenList() + bootstrapDragAndDropUpdates() } override fun onBackClick() { @@ -166,7 +164,7 @@ internal class OrganizeTokensViewModel @Inject constructor( } private fun bootstrapDragAndDropUpdates() { - dragAndDropAdapter.stateFlow + dragAndDropAdapter.dragAndDropUpdates .distinctUntilChanged() .onEach { stateHolder.updateStateWithManualSorting(it) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/dnd/DragAndDropAdapter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/dnd/DragAndDropAdapter.kt index 45dcb02cee..7cdc7d9d4c 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/dnd/DragAndDropAdapter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/dnd/DragAndDropAdapter.kt @@ -9,8 +9,8 @@ import com.tangem.feature.wallet.presentation.organizetokens.utils.common.uniteI import com.tangem.feature.wallet.presentation.organizetokens.utils.common.updateItems import kotlinx.collections.immutable.mutate import kotlinx.coroutines.channels.BufferOverflow -import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.SharedFlow import org.burnoutcrew.reorderable.ItemPosition internal class DragAndDropAdapter( @@ -22,15 +22,15 @@ internal class DragAndDropAdapter( private val currentListState: OrganizeTokensListState get() = listStateProvider.invoke() - private val listStateFlowInternal: MutableSharedFlow = MutableSharedFlow( + private val dragAndDropUpdatesInternal: MutableSharedFlow = MutableSharedFlow( replay = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST, ) private var currentDraggingItem: DraggableItem? = null - val stateFlow: Flow - get() = listStateFlowInternal + val dragAndDropUpdates: SharedFlow + get() = dragAndDropUpdatesInternal override fun canDragItemOver(dragOver: ItemPosition, dragging: ItemPosition): Boolean { val items = when (val listState = currentListState) { @@ -97,7 +97,7 @@ internal class DragAndDropAdapter( private fun updateListState(block: OrganizeTokensListState.() -> List) { val updatedState = currentListState.updateItems { block(currentListState) } - listStateFlowInternal.tryEmit(updatedState) + dragAndDropUpdatesInternal.tryEmit(updatedState) } private fun findItemsToMove( diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/DefaultWalletRouter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/DefaultWalletRouter.kt index 2e5be36d4a..dbe5b06c33 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/DefaultWalletRouter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/DefaultWalletRouter.kt @@ -58,10 +58,10 @@ internal class DefaultWalletRouter(private val reduxNavController: ReduxNavContr WalletRoute.OrganizeTokens.route, arguments = listOf(navArgument(WalletRoute.userWalletIdKey) { type = NavType.StringType }), ) { - val viewModel: OrganizeTokensViewModel = hiltViewModel() - .apply { - router = this@DefaultWalletRouter - } + val viewModel = hiltViewModel().apply { + router = this@DefaultWalletRouter + } + LocalLifecycleOwner.current.lifecycle.addObserver(viewModel) val uiState by viewModel.uiState.collectAsStateWithLifecycle() From bc3534c2771bcd84cc8a5ea7404ec16d2a2d0221 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 27 Sep 2023 18:06:27 +0300 Subject: [PATCH 110/242] Updated on 2026-08-14 --- .../data/settings/DefaultAppRatingRepository.kt | 3 ++- .../state/components/TokenDetailsNotification.kt | 6 +++++- .../factory/TokenDetailsNotificationConverter.kt | 11 +++++++++-- .../state/factory/TokenDetailsStateFactory.kt | 4 +++- .../tokendetails/ui/TokenDetailsScreen.kt | 2 +- 5 files changed, 20 insertions(+), 6 deletions(-) diff --git a/data/settings/src/main/java/com/tangem/data/settings/DefaultAppRatingRepository.kt b/data/settings/src/main/java/com/tangem/data/settings/DefaultAppRatingRepository.kt index 395542253b..e7bc353372 100644 --- a/data/settings/src/main/java/com/tangem/data/settings/DefaultAppRatingRepository.kt +++ b/data/settings/src/main/java/com/tangem/data/settings/DefaultAppRatingRepository.kt @@ -67,8 +67,9 @@ internal class DefaultAppRatingRepository( if (!isInteracting) { val diff = Calendar.getInstance().timeInMillis - fundsFoundDate val diffInDays = diff / DAY_IN_MILLIS + val isFundsFound = fundsFoundDate != FUNDS_FOUND_DATE_UNDEFINED - appLaunchCount >= ratingShowingCount && diffInDays >= FIRST_SHOWING_COUNT + appLaunchCount >= ratingShowingCount && diffInDays >= FIRST_SHOWING_COUNT && isFundsFound } else { appLaunchCount >= ratingShowingCount } diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/components/TokenDetailsNotification.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/components/TokenDetailsNotification.kt index 0b461acf11..070bc8d6dd 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/components/TokenDetailsNotification.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/components/TokenDetailsNotification.kt @@ -10,11 +10,15 @@ import com.tangem.features.tokendetails.impl.R // TODO: Finalize notification strings [REDACTED_JIRA] @Immutable -sealed class TokenDetailsNotification(open val config: NotificationConfig) { +sealed class TokenDetailsNotification( + open val isVisible: Boolean = true, + open val config: NotificationConfig, +) { data class RentInfo( private val rentInfo: CryptoCurrencyWarning.Rent, private val onCloseClick: () -> Unit, + override val isVisible: Boolean = true, ) : TokenDetailsNotification( config = NotificationConfig( title = TextReference.Res(R.string.send_network_fee_title), diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsNotificationConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsNotificationConverter.kt index 6dffee2a74..a379b3788f 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsNotificationConverter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsNotificationConverter.kt @@ -1,5 +1,6 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.state.factory +import com.tangem.common.extensions.cast import com.tangem.domain.tokens.model.warnings.CryptoCurrencyWarning import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.TokenDetailsNotification @@ -23,9 +24,15 @@ internal class TokenDetailsNotificationConverter( return newNotifications.toImmutableList() } - fun removeRentInfo(currentState: TokenDetailsState): ImmutableList { + fun getStateRentInfoVisibility( + currentState: TokenDetailsState, + isVisible: Boolean, + ): ImmutableList { val newNotifications = currentState.notifications.toMutableList() - newNotifications.removeBy { it is TokenDetailsNotification.RentInfo } + val oldNotification = newNotifications.find { it is TokenDetailsNotification.RentInfo } + oldNotification?.let { + newNotifications.add(it.cast().copy(isVisible = isVisible)) + } return newNotifications.toImmutableList() } diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt index 4a54ef00de..fde99ca794 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt @@ -141,7 +141,9 @@ internal class TokenDetailsStateFactory( } fun getRefreshedState(): TokenDetailsState { + val state = currentStateProvider() return refreshStateConverter.convert(false) + .copy(notifications = notificationConverter.getStateRentInfoVisibility(state, true)) } fun getStateWithReceiveBottomSheet( @@ -223,6 +225,6 @@ internal class TokenDetailsStateFactory( fun getStateWithRemovedRentNotification(): TokenDetailsState { val state = currentStateProvider() - return state.copy(notifications = notificationConverter.removeRentInfo(state)) + return state.copy(notifications = notificationConverter.getStateRentInfoVisibility(state, false)) } } \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/TokenDetailsScreen.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/TokenDetailsScreen.kt index 7a7d535d15..908501c1d0 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/TokenDetailsScreen.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/TokenDetailsScreen.kt @@ -81,7 +81,7 @@ internal fun TokenDetailsScreen(state: TokenDetailsState) { } item { TokenDetailsBalanceBlock(modifier = itemModifier, state = state.tokenBalanceBlockState) } items( - items = state.notifications, + items = state.notifications.filter { it.isVisible }, key = { it.config::class.java }, contentType = { it.config::class.java }, itemContent = { Notification(config = it.config, modifier = itemModifier.animateItemPlacement()) }, From 6a15cfd2e42cc8b93dc74775cddbd7f2f266486e Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 28 Sep 2023 12:25:33 +0500 Subject: [PATCH 111/242] Updated on 2026-08-14 --- .../transactions/TransactionList.kt | 21 ++++---- .../tokens/model/CryptoCurrencyStatus.kt | 23 +++++--- .../operations/CurrencyStatusOperations.kt | 21 ++++++-- .../domain/tokens/mock/MockTokensStates.kt | 54 +++++++++++++++---- .../TokenDetailsLoadedBalanceConverter.kt | 15 +++--- 5 files changed, 96 insertions(+), 38 deletions(-) diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/transactions/TransactionList.kt b/core/ui/src/main/java/com/tangem/core/ui/components/transactions/TransactionList.kt index 323cb0e77b..e2dec6eb86 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/transactions/TransactionList.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/transactions/TransactionList.kt @@ -67,16 +67,17 @@ private fun LazyListScope.contentItems( }, contentType = txHistoryItems.itemContentType { it::class.java }, itemContent = { index -> - val item = txHistoryItems[index]!! - TxHistoryListItem( - state = item, - modifier = modifier - .animateItemPlacement() - .roundedShapeItemDecoration( - currentIndex = index, - lastIndex = txHistoryItems.itemSnapshotList.lastIndex, - ), - ) + txHistoryItems[index]?.let { item -> + TxHistoryListItem( + state = item, + modifier = modifier + .animateItemPlacement() + .roundedShapeItemDecoration( + currentIndex = index, + lastIndex = txHistoryItems.itemSnapshotList.lastIndex, + ), + ) + } }, ) } diff --git a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/CryptoCurrencyStatus.kt b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/CryptoCurrencyStatus.kt index c09cdcd3cc..b4187361d6 100644 --- a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/CryptoCurrencyStatus.kt +++ b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/CryptoCurrencyStatus.kt @@ -51,25 +51,36 @@ data class CryptoCurrencyStatus( object Loading : Status(isError = false) /** Represents a state where the cryptocurrency is not reachable. */ - object Unreachable : Status(isError = true) + data class Unreachable( + override val priceChange: BigDecimal?, + override val fiatRate: BigDecimal?, + ) : Status(isError = true) /** Represents a state where the cryptocurrency's network amount not found. */ - object NoAmount : Status(isError = true) + data class NoAmount( + override val priceChange: BigDecimal?, + override val fiatRate: BigDecimal?, + ) : Status(isError = true) /** Represents a state where the cryptocurrency's derivation is missed. */ - object MissedDerivation : Status(isError = true) + data class MissedDerivation( + override val priceChange: BigDecimal?, + override val fiatRate: BigDecimal?, + ) : Status(isError = true) /** * Represents a state where there is no account associated with the cryptocurrency * * @property errorMessage error message */ - data class NoAccount(val errorMessage: String) : Status(isError = false) { + data class NoAccount( + val errorMessage: String, + override val priceChange: BigDecimal?, + override val fiatRate: BigDecimal?, + ) : Status(isError = false) { override val amount: BigDecimal = BigDecimal.ZERO override val fiatAmount: BigDecimal = BigDecimal.ZERO - override val fiatRate: BigDecimal = BigDecimal.ZERO - override val priceChange: BigDecimal = BigDecimal.ZERO } /** diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrencyStatusOperations.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrencyStatusOperations.kt index b3f38c96b4..804c8a58bc 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrencyStatusOperations.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrencyStatusOperations.kt @@ -18,15 +18,28 @@ internal class CurrencyStatusOperations( private fun createStatus(): CryptoCurrencyStatus.Status { return when (val status = networkStatus?.value) { null -> CryptoCurrencyStatus.Loading - is NetworkStatus.MissedDerivation -> CryptoCurrencyStatus.MissedDerivation - is NetworkStatus.Unreachable -> CryptoCurrencyStatus.Unreachable - is NetworkStatus.NoAccount -> CryptoCurrencyStatus.NoAccount(errorMessage = status.errorMessage) + is NetworkStatus.MissedDerivation -> createMissedDerivationStatus() + is NetworkStatus.Unreachable -> createUnreachableStatus() + is NetworkStatus.NoAccount -> createNoAccountStatus(status.errorMessage) is NetworkStatus.Verified -> createStatus(status) } } + private fun createMissedDerivationStatus(): CryptoCurrencyStatus.MissedDerivation = + CryptoCurrencyStatus.MissedDerivation(priceChange = quote?.priceChange, fiatRate = quote?.fiatRate) + + private fun createUnreachableStatus(): CryptoCurrencyStatus.Unreachable = + CryptoCurrencyStatus.Unreachable(priceChange = quote?.priceChange, fiatRate = quote?.fiatRate) + + private fun createNoAccountStatus(message: String): CryptoCurrencyStatus.NoAccount = CryptoCurrencyStatus.NoAccount( + errorMessage = message, + priceChange = quote?.priceChange, + fiatRate = quote?.fiatRate, + ) + private fun createStatus(status: NetworkStatus.Verified): CryptoCurrencyStatus.Status { - val amount = status.amounts[currency.id] ?: return CryptoCurrencyStatus.NoAmount + val amount = status.amounts[currency.id] + ?: return CryptoCurrencyStatus.NoAmount(priceChange = quote?.priceChange, fiatRate = quote?.fiatRate) val hasCurrentNetworkTransactions = status.pendingTransactions.isNotEmpty() val currentTransactions = status.pendingTransactions.getOrElse(currency.id, ::emptySet) diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockTokensStates.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockTokensStates.kt index 2ffefd3f10..69e1b66b5c 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockTokensStates.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockTokensStates.kt @@ -9,52 +9,86 @@ internal object MockTokensStates { val tokenState1 = CryptoCurrencyStatus( currency = MockTokens.token1, - value = CryptoCurrencyStatus.Unreachable, + value = CryptoCurrencyStatus.Unreachable( + priceChange = MockQuotes.quote1.priceChange, + fiatRate = MockQuotes.quote1.fiatRate, + ), ) val tokenState2 = CryptoCurrencyStatus( currency = MockTokens.token2, - value = CryptoCurrencyStatus.Unreachable, + value = CryptoCurrencyStatus.Unreachable( + priceChange = MockQuotes.quote2.priceChange, + fiatRate = MockQuotes.quote2.fiatRate, + ), ) val tokenState3 = CryptoCurrencyStatus( currency = MockTokens.token3, - value = CryptoCurrencyStatus.Unreachable, + value = CryptoCurrencyStatus.Unreachable( + priceChange = MockQuotes.quote3.priceChange, + fiatRate = MockQuotes.quote3.fiatRate, + ), ) val tokenState4 = CryptoCurrencyStatus( currency = MockTokens.token4, - value = CryptoCurrencyStatus.MissedDerivation, + value = CryptoCurrencyStatus.MissedDerivation( + priceChange = MockQuotes.quote4.priceChange, + fiatRate = MockQuotes.quote4.fiatRate, + ), ) val tokenState5 = CryptoCurrencyStatus( currency = MockTokens.token5, - value = CryptoCurrencyStatus.MissedDerivation, + value = CryptoCurrencyStatus.MissedDerivation( + priceChange = MockQuotes.quote5.priceChange, + fiatRate = MockQuotes.quote5.fiatRate, + ), ) val tokenState6 = CryptoCurrencyStatus( currency = MockTokens.token6, - value = CryptoCurrencyStatus.MissedDerivation, + value = CryptoCurrencyStatus.MissedDerivation( + priceChange = MockQuotes.quote6.priceChange, + fiatRate = MockQuotes.quote6.fiatRate, + ), ) val tokenState7 = CryptoCurrencyStatus( currency = MockTokens.token7, - value = CryptoCurrencyStatus.NoAccount(errorMessage = ""), + value = CryptoCurrencyStatus.NoAccount( + priceChange = MockQuotes.quote7.priceChange, + fiatRate = MockQuotes.quote7.fiatRate, + errorMessage = "", + ), ) val tokenState8 = CryptoCurrencyStatus( currency = MockTokens.token8, - value = CryptoCurrencyStatus.NoAccount(errorMessage = ""), + value = CryptoCurrencyStatus.NoAccount( + priceChange = MockQuotes.quote8.priceChange, + fiatRate = MockQuotes.quote8.fiatRate, + errorMessage = "", + ), ) val tokenState9 = CryptoCurrencyStatus( currency = MockTokens.token9, - value = CryptoCurrencyStatus.NoAccount(errorMessage = ""), + value = CryptoCurrencyStatus.NoAccount( + priceChange = MockQuotes.quote9.priceChange, + fiatRate = MockQuotes.quote9.fiatRate, + errorMessage = "", + ), ) val tokenState10 = CryptoCurrencyStatus( currency = MockTokens.token10, - value = CryptoCurrencyStatus.NoAccount(errorMessage = ""), + value = CryptoCurrencyStatus.NoAccount( + priceChange = MockQuotes.quote10.priceChange, + fiatRate = MockQuotes.quote10.fiatRate, + errorMessage = "", + ), ) val failedTokenStates = nonEmptyListOf( diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsLoadedBalanceConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsLoadedBalanceConverter.kt index fa6aaee52a..cc8b4d17d0 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsLoadedBalanceConverter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsLoadedBalanceConverter.kt @@ -84,8 +84,14 @@ internal class TokenDetailsLoadedBalanceConverter( private fun getMarketPriceState(status: CryptoCurrencyStatus.Status, currencyName: String): MarketPriceBlockState { return when (status) { - is CryptoCurrencyStatus.NoQuote, + is CryptoCurrencyStatus.Loading -> MarketPriceBlockState.Loading(currencyName) + is CryptoCurrencyStatus.NoQuote -> MarketPriceBlockState.Error(currencyName) is CryptoCurrencyStatus.Loaded, + is CryptoCurrencyStatus.Custom, + is CryptoCurrencyStatus.MissedDerivation, + is CryptoCurrencyStatus.NoAccount, + is CryptoCurrencyStatus.NoAmount, + is CryptoCurrencyStatus.Unreachable, -> MarketPriceBlockState.Content( currencyName = currencyName, price = formatPrice(status, appCurrencyProvider()), @@ -94,13 +100,6 @@ internal class TokenDetailsLoadedBalanceConverter( type = getPriceChangeType(status), ), ) - is CryptoCurrencyStatus.Loading -> MarketPriceBlockState.Loading(currencyName) - is CryptoCurrencyStatus.Custom, - is CryptoCurrencyStatus.MissedDerivation, - is CryptoCurrencyStatus.NoAccount, - is CryptoCurrencyStatus.NoAmount, - is CryptoCurrencyStatus.Unreachable, - -> MarketPriceBlockState.Error(currencyName) } } From 294c7139ee00370193e787acbf9bac28296bb066 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 28 Sep 2023 15:43:18 +0300 Subject: [PATCH 112/242] Updated on 2026-08-14 --- .../main/java/com/tangem/common/Strings.kt | 2 +- core/ui/build.gradle.kts | 3 ++ .../ui/components/transactions/Transaction.kt | 17 ++++++---- .../transactions/TransactionList.kt | 4 +++ .../transactions/TxHistoryContentItem.kt | 12 +++++-- .../tokendetails/TokenDetailsPreviewData.kt | 2 +- .../state/TokenDetailsBalanceBlockState.kt | 1 - .../tokendetails/state/TokenDetailsState.kt | 1 + .../TokenDetailsLoadedBalanceConverter.kt | 2 -- .../TokenDetailsSkeletonStateConverter.kt | 1 + .../state/factory/TokenDetailsStateFactory.kt | 15 +++------ .../TokenDetailsLoadedTxHistoryConverter.kt | 24 ++++++-------- .../TokenDetailsTxHistoryItemFlowConverter.kt | 5 ++- ...ilsTxHistoryToTransactionStateConverter.kt | 20 +++++------ ...tailsTxHistoryTransactionStateConverter.kt | 20 +++++------ .../tokendetails/ui/TokenDetailsScreen.kt | 23 ++++++++++--- .../ui/components/TokenDetailsBalanceBlock.kt | 33 +++++++++++++------ .../viewmodels/TokenDetailsViewModel.kt | 15 ++++----- .../presentation/common/WalletPreviewData.kt | 2 ++ .../wallet/state/WalletMultiCurrencyState.kt | 2 ++ .../wallet/state/WalletSingleCurrencyState.kt | 2 ++ .../presentation/wallet/state/WalletState.kt | 8 +++++ .../state/factory/WalletLockedConverter.kt | 2 ++ .../factory/WalletSkeletonStateConverter.kt | 2 ++ .../state/factory/WalletStateFactory.kt | 1 + .../factory/WalletsUnlockStateConverter.kt | 2 ++ .../WalletLoadedTxHistoryConverter.kt | 2 ++ .../WalletTxHistoryItemFlowConverter.kt | 7 +++- ...alletTxHistoryTransactionStateConverter.kt | 25 ++++++++------ .../presentation/wallet/ui/WalletScreen.kt | 7 +++- .../ui/components/common/WalletContent.kt | 3 +- 31 files changed, 169 insertions(+), 96 deletions(-) diff --git a/common/src/main/java/com/tangem/common/Strings.kt b/common/src/main/java/com/tangem/common/Strings.kt index 402f3daa07..943b0de614 100644 --- a/common/src/main/java/com/tangem/common/Strings.kt +++ b/common/src/main/java/com/tangem/common/Strings.kt @@ -2,5 +2,5 @@ package com.tangem.common object Strings { - const val STARS = "***" + const val STARS = "\u2217\u2217\u2217" } \ No newline at end of file diff --git a/core/ui/build.gradle.kts b/core/ui/build.gradle.kts index 7814c986a3..4f39028ae4 100644 --- a/core/ui/build.gradle.kts +++ b/core/ui/build.gradle.kts @@ -5,6 +5,9 @@ plugins { } dependencies { + /** Project - Common */ + implementation(projects.common) + /** Project - Domain */ implementation(projects.domain.tokens.models) implementation(projects.domain.appTheme.models) diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/transactions/Transaction.kt b/core/ui/src/main/java/com/tangem/core/ui/components/transactions/Transaction.kt index 66ab911a07..283bbf7b89 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/transactions/Transaction.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/transactions/Transaction.kt @@ -19,6 +19,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider import androidx.constraintlayout.compose.ConstraintLayout import androidx.constraintlayout.compose.Dimension +import com.tangem.common.Strings import com.tangem.core.ui.R import com.tangem.core.ui.components.CircleShimmer import com.tangem.core.ui.components.RectangleShimmer @@ -33,8 +34,9 @@ import java.util.UUID /** * Transaction component * - * @param state state - * @param modifier modifier + * @param state state + * @param isBalanceHidden is balance hidden + * @param modifier modifier * * @see Figma Component @@ -42,7 +44,7 @@ import java.util.UUID [REDACTED_AUTHOR] */ @Composable -fun Transaction(state: TransactionState, modifier: Modifier = Modifier) { +fun Transaction(state: TransactionState, isBalanceHidden: Boolean, modifier: Modifier = Modifier) { Surface( modifier = modifier .background(TangemTheme.colors.background.primary) @@ -89,6 +91,7 @@ fun Transaction(state: TransactionState, modifier: Modifier = Modifier) { Amount( state = state, + isBalanceHidden = isBalanceHidden, modifier = Modifier.constrainAs(amountItem) { start.linkTo(titleItem.end) top.linkTo(titleItem.top) @@ -265,11 +268,11 @@ private fun Subtitle(state: TransactionState, modifier: Modifier = Modifier) { } @Composable -private fun Amount(state: TransactionState, modifier: Modifier = Modifier) { +private fun Amount(state: TransactionState, isBalanceHidden: Boolean, modifier: Modifier = Modifier) { when (state) { is TransactionState.Content -> { Text( - text = state.amount, + text = if (isBalanceHidden) Strings.STARS else state.amount, modifier = modifier, textAlign = TextAlign.End, color = when (state.direction) { @@ -333,7 +336,7 @@ private fun Preview_TransactionItem_LightTheme( @PreviewParameter(TransactionItemStateProvider::class) state: TransactionState, ) { TangemTheme(isDark = false) { - Transaction(state) + Transaction(state = state, isBalanceHidden = false) } } @@ -343,7 +346,7 @@ private fun Preview_TransactionItem_DarkTheme( @PreviewParameter(TransactionItemStateProvider::class) state: TransactionState, ) { TangemTheme(isDark = true) { - Transaction(state) + Transaction(state = state, isBalanceHidden = false) } } diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/transactions/TransactionList.kt b/core/ui/src/main/java/com/tangem/core/ui/components/transactions/TransactionList.kt index e2dec6eb86..3314333a5e 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/transactions/TransactionList.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/transactions/TransactionList.kt @@ -24,12 +24,14 @@ import com.tangem.core.ui.res.TangemTheme fun LazyListScope.txHistoryItems( state: TxHistoryState, txHistoryItems: LazyPagingItems?, + isBalanceHidden: Boolean, modifier: Modifier = Modifier, ) { when (state) { is TxHistoryState.Content -> { contentItems( txHistoryItems = requireNotNull(txHistoryItems), + isBalanceHidden = isBalanceHidden, modifier = modifier, ) } @@ -54,6 +56,7 @@ fun LazyListScope.txHistoryItems( @OptIn(ExperimentalFoundationApi::class) private fun LazyListScope.contentItems( txHistoryItems: LazyPagingItems, + isBalanceHidden: Boolean, modifier: Modifier = Modifier, ) { items( @@ -70,6 +73,7 @@ private fun LazyListScope.contentItems( txHistoryItems[index]?.let { item -> TxHistoryListItem( state = item, + isBalanceHidden = isBalanceHidden, modifier = modifier .animateItemPlacement() .roundedShapeItemDecoration( diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/transactions/TxHistoryContentItem.kt b/core/ui/src/main/java/com/tangem/core/ui/components/transactions/TxHistoryContentItem.kt index 6f918f3cad..b532c2d533 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/transactions/TxHistoryContentItem.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/transactions/TxHistoryContentItem.kt @@ -5,7 +5,11 @@ import androidx.compose.ui.Modifier import com.tangem.core.ui.components.transactions.state.TxHistoryState @Composable -internal fun TxHistoryListItem(state: TxHistoryState.TxHistoryItemState, modifier: Modifier = Modifier) { +internal fun TxHistoryListItem( + state: TxHistoryState.TxHistoryItemState, + isBalanceHidden: Boolean, + modifier: Modifier = Modifier, +) { when (state) { is TxHistoryState.TxHistoryItemState.GroupTitle -> { TxHistoryGroupTitle(config = state, modifier = modifier) @@ -14,7 +18,11 @@ internal fun TxHistoryListItem(state: TxHistoryState.TxHistoryItemState, modifie TxHistoryTitle(config = state, modifier = modifier) } is TxHistoryState.TxHistoryItemState.Transaction -> { - Transaction(state = state.state, modifier = modifier) + Transaction( + state = state.state, + isBalanceHidden = isBalanceHidden, + modifier = modifier, + ) } } } \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/TokenDetailsPreviewData.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/TokenDetailsPreviewData.kt index 2fb3b9b125..d7771e9eb6 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/TokenDetailsPreviewData.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/TokenDetailsPreviewData.kt @@ -67,7 +67,6 @@ internal object TokenDetailsPreviewData { actionButtons = actionButtons, fiatBalance = "123,00$", cryptoBalance = "866,96 USDT", - isBalanceHidden = false, ) val balanceError = TokenDetailsBalanceBlockState.Error(actionButtons = actionButtons) @@ -93,5 +92,6 @@ internal object TokenDetailsPreviewData { pendingTxs = persistentListOf(), pullToRefreshConfig = pullToRefreshConfig, bottomSheetConfig = null, + isBalanceHidden = false, ) } \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsBalanceBlockState.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsBalanceBlockState.kt index 9bcf4d84c3..2f066dae01 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsBalanceBlockState.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsBalanceBlockState.kt @@ -15,7 +15,6 @@ internal sealed class TokenDetailsBalanceBlockState { override val actionButtons: ImmutableList, val fiatBalance: String, val cryptoBalance: String, - val isBalanceHidden: Boolean, ) : TokenDetailsBalanceBlockState() data class Error( diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsState.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsState.kt index cdf8590f46..936440ff36 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsState.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsState.kt @@ -21,4 +21,5 @@ internal data class TokenDetailsState( val dialogConfig: TokenDetailsDialogConfig?, val pullToRefreshConfig: TokenDetailsPullToRefreshConfig, val bottomSheetConfig: TangemBottomSheetConfig?, + val isBalanceHidden: Boolean, ) \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsLoadedBalanceConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsLoadedBalanceConverter.kt index cc8b4d17d0..9bb7136de7 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsLoadedBalanceConverter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsLoadedBalanceConverter.kt @@ -21,7 +21,6 @@ import java.math.BigDecimal internal class TokenDetailsLoadedBalanceConverter( private val currentStateProvider: Provider, private val appCurrencyProvider: Provider, - private val isBalanceHiddenProvider: Provider, private val symbol: String, private val decimals: Int, ) : Converter, TokenDetailsState> { @@ -65,7 +64,6 @@ internal class TokenDetailsLoadedBalanceConverter( actionButtons = currentState.actionButtons, fiatBalance = formatFiatAmount(status.value, appCurrencyProvider()), cryptoBalance = formatCryptoAmount(status), - isBalanceHidden = isBalanceHiddenProvider(), ) } is CryptoCurrencyStatus.Loading -> { diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSkeletonStateConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSkeletonStateConverter.kt index 3cd81399df..d149791b9e 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSkeletonStateConverter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSkeletonStateConverter.kt @@ -49,6 +49,7 @@ internal class TokenDetailsSkeletonStateConverter( dialogConfig = null, pullToRefreshConfig = createPullToRefresh(), bottomSheetConfig = null, + isBalanceHidden = true, ) } diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt index fde99ca794..52ef44ac9c 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt @@ -17,7 +17,6 @@ import com.tangem.domain.tokens.model.warnings.CryptoCurrencyWarning import com.tangem.domain.txhistory.models.TxHistoryItem import com.tangem.domain.txhistory.models.TxHistoryListError import com.tangem.domain.txhistory.models.TxHistoryStateError -import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsBalanceBlockState import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.TokenDetailsDialogConfig import com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.txhistory.TokenDetailsLoadedTxHistoryConverter @@ -29,7 +28,6 @@ import kotlinx.coroutines.flow.Flow internal class TokenDetailsStateFactory( private val currentStateProvider: Provider, private val appCurrencyProvider: Provider, - private val isBalanceHiddenProvider: Provider, private val clickIntents: TokenDetailsClickIntents, currencySymbolProvider: Provider, currencyDecimalsProvider: Provider, @@ -47,7 +45,6 @@ internal class TokenDetailsStateFactory( TokenDetailsLoadedBalanceConverter( currentStateProvider = currentStateProvider, appCurrencyProvider = appCurrencyProvider, - isBalanceHiddenProvider = isBalanceHiddenProvider, symbol = currencySymbolProvider(), decimals = currencyDecimalsProvider(), ) @@ -100,7 +97,9 @@ internal class TokenDetailsStateFactory( fun getLoadedTxHistoryState( txHistoryEither: Either>>, ): TokenDetailsState { - return loadedTxHistoryConverter.convert(txHistoryEither) + return currentStateProvider().copy( + txHistoryState = loadedTxHistoryConverter.convert(txHistoryEither), + ) } fun getStateWithClosedDialog(): TokenDetailsState { @@ -203,14 +202,8 @@ internal class TokenDetailsStateFactory( fun getStateWithUpdatedHidden(isBalanceHidden: Boolean): TokenDetailsState { val currentState = currentStateProvider() - val possibleTokenBalanceBlockState = currentState.tokenBalanceBlockState as? - TokenDetailsBalanceBlockState.Content - possibleTokenBalanceBlockState?.let { - return currentState.copy( - tokenBalanceBlockState = possibleTokenBalanceBlockState.copy(isBalanceHidden = isBalanceHidden), - ) - } ?: return currentState + return currentState.copy(isBalanceHidden = isBalanceHidden) } fun getStateWithNotifications(warnings: Set): TokenDetailsState { diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/txhistory/TokenDetailsLoadedTxHistoryConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/txhistory/TokenDetailsLoadedTxHistoryConverter.kt index 4c5fbc9fa2..b5675ea9f0 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/txhistory/TokenDetailsLoadedTxHistoryConverter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/txhistory/TokenDetailsLoadedTxHistoryConverter.kt @@ -16,7 +16,7 @@ internal class TokenDetailsLoadedTxHistoryConverter( private val clickIntents: TokenDetailsClickIntents, symbol: String, decimals: Int, -) : Converter>>, TokenDetailsState> { +) : Converter>>, TxHistoryState> { private val txHistoryItemFlowConverter by lazy { TokenDetailsTxHistoryItemFlowConverter( @@ -27,23 +27,19 @@ internal class TokenDetailsLoadedTxHistoryConverter( ) } - override fun convert(value: Either>>): TokenDetailsState { + override fun convert(value: Either>>): TxHistoryState { return value.fold(ifLeft = ::convertError, ifRight = ::convert) } - private fun convertError(error: TxHistoryListError): TokenDetailsState { - return currentStateProvider().copy( - txHistoryState = when (error) { - is TxHistoryListError.DataError -> { - TxHistoryState.Error(onReloadClick = clickIntents::onReloadClick) - } - }, - ) + private fun convertError(error: TxHistoryListError): TxHistoryState { + return when (error) { + is TxHistoryListError.DataError -> { + TxHistoryState.Error(onReloadClick = clickIntents::onReloadClick) + } + } } - private fun convert(items: Flow>): TokenDetailsState { - return currentStateProvider().copy( - txHistoryState = txHistoryItemFlowConverter.convert(value = items), - ) + private fun convert(items: Flow>): TxHistoryState { + return txHistoryItemFlowConverter.convert(value = items) } } \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/txhistory/TokenDetailsTxHistoryItemFlowConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/txhistory/TokenDetailsTxHistoryItemFlowConverter.kt index 11d8ee4aaf..b4e618aeb3 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/txhistory/TokenDetailsTxHistoryItemFlowConverter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/txhistory/TokenDetailsTxHistoryItemFlowConverter.kt @@ -30,7 +30,10 @@ internal class TokenDetailsTxHistoryItemFlowConverter( ) : Converter>, TxHistoryState> { private val txHistoryItemConverter by lazy { - TokenDetailsTxHistoryTransactionStateConverter(symbol = symbol, decimals = decimals) + TokenDetailsTxHistoryTransactionStateConverter( + symbol = symbol, + decimals = decimals, + ) } override fun convert(value: Flow>): TxHistoryState { diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/txhistory/TokenDetailsTxHistoryToTransactionStateConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/txhistory/TokenDetailsTxHistoryToTransactionStateConverter.kt index 2b8eaca2ad..5c11efff45 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/txhistory/TokenDetailsTxHistoryToTransactionStateConverter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/txhistory/TokenDetailsTxHistoryToTransactionStateConverter.kt @@ -29,7 +29,7 @@ internal class TokenDetailsTxHistoryToTransactionStateConverter( TxHistoryItem.TransactionType.Deposit -> TransactionState.Custom( txHash = item.txHash, address = item.direction.extractAddress(), - amount = item.extractFormattedCryptoBalance(), + amount = item.getAmount(), timestamp = item.timestampInMillis.toTimeFormat(), status = item.status.tiUiStatus(), direction = item.direction.toUiDirection(), @@ -39,7 +39,7 @@ internal class TokenDetailsTxHistoryToTransactionStateConverter( TxHistoryItem.TransactionType.Submit -> TransactionState.Custom( txHash = item.txHash, address = item.direction.extractAddress(), - amount = item.extractFormattedCryptoBalance(), + amount = item.getAmount(), timestamp = item.timestampInMillis.toTimeFormat(), status = item.status.tiUiStatus(), direction = item.direction.toUiDirection(), @@ -49,7 +49,7 @@ internal class TokenDetailsTxHistoryToTransactionStateConverter( TxHistoryItem.TransactionType.Supply -> TransactionState.Custom( txHash = item.txHash, address = item.direction.extractAddress(), - amount = item.extractFormattedCryptoBalance(), + amount = item.getAmount(), timestamp = item.timestampInMillis.toTimeFormat(), status = item.status.tiUiStatus(), direction = item.direction.toUiDirection(), @@ -59,7 +59,7 @@ internal class TokenDetailsTxHistoryToTransactionStateConverter( TxHistoryItem.TransactionType.Unoswap -> TransactionState.Custom( txHash = item.txHash, address = item.direction.extractAddress(), - amount = item.extractFormattedCryptoBalance(), + amount = item.getAmount(), timestamp = item.timestampInMillis.toTimeFormat(), status = item.status.tiUiStatus(), direction = item.direction.toUiDirection(), @@ -69,7 +69,7 @@ internal class TokenDetailsTxHistoryToTransactionStateConverter( TxHistoryItem.TransactionType.Withdraw -> TransactionState.Custom( txHash = item.txHash, address = item.direction.extractAddress(), - amount = item.extractFormattedCryptoBalance(), + amount = item.getAmount(), timestamp = item.timestampInMillis.toTimeFormat(), status = item.status.tiUiStatus(), direction = item.direction.toUiDirection(), @@ -79,7 +79,7 @@ internal class TokenDetailsTxHistoryToTransactionStateConverter( is TxHistoryItem.TransactionType.Custom -> TransactionState.Custom( txHash = item.txHash, address = item.direction.extractAddress(), - amount = item.extractFormattedCryptoBalance(), + amount = item.getAmount(), timestamp = item.timestampInMillis.toTimeFormat(), status = item.status.tiUiStatus(), direction = item.direction.toUiDirection(), @@ -93,7 +93,7 @@ internal class TokenDetailsTxHistoryToTransactionStateConverter( return TransactionState.Transfer( txHash = item.txHash, address = item.direction.extractAddress(), - amount = item.extractFormattedCryptoBalance(), + amount = item.getAmount(), timestamp = item.timestampInMillis.toTimeFormat(), status = item.status.tiUiStatus(), direction = item.direction.toUiDirection(), @@ -104,7 +104,7 @@ internal class TokenDetailsTxHistoryToTransactionStateConverter( return TransactionState.Approve( txHash = item.txHash, address = item.direction.extractAddress(), - amount = item.extractFormattedCryptoBalance(), + amount = item.getAmount(), timestamp = item.timestampInMillis.toTimeFormat(), status = item.status.tiUiStatus(), direction = item.direction.toUiDirection(), @@ -114,7 +114,7 @@ internal class TokenDetailsTxHistoryToTransactionStateConverter( return TransactionState.Swap( txHash = item.txHash, address = item.direction.extractAddress(), - amount = item.extractFormattedCryptoBalance(), + amount = item.getAmount(), timestamp = item.timestampInMillis.toTimeFormat(), status = item.status.tiUiStatus(), direction = item.direction.toUiDirection(), @@ -141,7 +141,7 @@ internal class TokenDetailsTxHistoryToTransactionStateConverter( is TxHistoryItem.TransactionDirection.Outgoing -> TransactionState.Content.Direction.OUTGOING } - private fun TxHistoryItem.extractFormattedCryptoBalance(): String { + private fun TxHistoryItem.getAmount(): String { val prefix = when (direction) { is TxHistoryItem.TransactionDirection.Incoming -> "+" is TxHistoryItem.TransactionDirection.Outgoing -> "-" diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/txhistory/TokenDetailsTxHistoryTransactionStateConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/txhistory/TokenDetailsTxHistoryTransactionStateConverter.kt index be32a72923..f4174cafae 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/txhistory/TokenDetailsTxHistoryTransactionStateConverter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/txhistory/TokenDetailsTxHistoryTransactionStateConverter.kt @@ -26,7 +26,7 @@ internal class TokenDetailsTxHistoryTransactionStateConverter( TxHistoryItem.TransactionType.Deposit -> TransactionState.Custom( txHash = item.txHash, address = item.direction.extractAddress(), - amount = item.extractFormattedCryptoBalance(), + amount = item.getAmount(), timestamp = item.getRawTimestamp(), status = item.status.tiUiStatus(), direction = item.direction.toUiDirection(), @@ -36,7 +36,7 @@ internal class TokenDetailsTxHistoryTransactionStateConverter( TxHistoryItem.TransactionType.Submit -> TransactionState.Custom( txHash = item.txHash, address = item.direction.extractAddress(), - amount = item.extractFormattedCryptoBalance(), + amount = item.getAmount(), timestamp = item.getRawTimestamp(), status = item.status.tiUiStatus(), direction = item.direction.toUiDirection(), @@ -46,7 +46,7 @@ internal class TokenDetailsTxHistoryTransactionStateConverter( TxHistoryItem.TransactionType.Supply -> TransactionState.Custom( txHash = item.txHash, address = item.direction.extractAddress(), - amount = item.extractFormattedCryptoBalance(), + amount = item.getAmount(), timestamp = item.getRawTimestamp(), status = item.status.tiUiStatus(), direction = item.direction.toUiDirection(), @@ -56,7 +56,7 @@ internal class TokenDetailsTxHistoryTransactionStateConverter( TxHistoryItem.TransactionType.Unoswap -> TransactionState.Custom( txHash = item.txHash, address = item.direction.extractAddress(), - amount = item.extractFormattedCryptoBalance(), + amount = item.getAmount(), timestamp = item.getRawTimestamp(), status = item.status.tiUiStatus(), direction = item.direction.toUiDirection(), @@ -66,7 +66,7 @@ internal class TokenDetailsTxHistoryTransactionStateConverter( TxHistoryItem.TransactionType.Withdraw -> TransactionState.Custom( txHash = item.txHash, address = item.direction.extractAddress(), - amount = item.extractFormattedCryptoBalance(), + amount = item.getAmount(), timestamp = item.getRawTimestamp(), status = item.status.tiUiStatus(), direction = item.direction.toUiDirection(), @@ -76,7 +76,7 @@ internal class TokenDetailsTxHistoryTransactionStateConverter( is TxHistoryItem.TransactionType.Custom -> TransactionState.Custom( txHash = item.txHash, address = item.direction.extractAddress(), - amount = item.extractFormattedCryptoBalance(), + amount = item.getAmount(), timestamp = item.getRawTimestamp(), status = item.status.tiUiStatus(), direction = item.direction.toUiDirection(), @@ -90,7 +90,7 @@ internal class TokenDetailsTxHistoryTransactionStateConverter( return TransactionState.Transfer( txHash = item.txHash, address = item.direction.extractAddress(), - amount = item.extractFormattedCryptoBalance(), + amount = item.getAmount(), timestamp = item.getRawTimestamp(), status = item.status.tiUiStatus(), direction = item.direction.toUiDirection(), @@ -101,7 +101,7 @@ internal class TokenDetailsTxHistoryTransactionStateConverter( return TransactionState.Approve( txHash = item.txHash, address = item.direction.extractAddress(), - amount = item.extractFormattedCryptoBalance(), + amount = item.getAmount(), timestamp = item.getRawTimestamp(), status = item.status.tiUiStatus(), direction = item.direction.toUiDirection(), @@ -111,7 +111,7 @@ internal class TokenDetailsTxHistoryTransactionStateConverter( return TransactionState.Swap( txHash = item.txHash, address = item.direction.extractAddress(), - amount = item.extractFormattedCryptoBalance(), + amount = item.getAmount(), timestamp = item.getRawTimestamp(), status = item.status.tiUiStatus(), direction = item.direction.toUiDirection(), @@ -142,7 +142,7 @@ internal class TokenDetailsTxHistoryTransactionStateConverter( is TxHistoryItem.TransactionDirection.Outgoing -> TransactionState.Content.Direction.OUTGOING } - private fun TxHistoryItem.extractFormattedCryptoBalance(): String { + private fun TxHistoryItem.getAmount(): String { val prefix = when (direction) { is TxHistoryItem.TransactionDirection.Incoming -> "+" is TxHistoryItem.TransactionDirection.Outgoing -> "-" diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/TokenDetailsScreen.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/TokenDetailsScreen.kt index 908501c1d0..5ff3e0fff7 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/TokenDetailsScreen.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/TokenDetailsScreen.kt @@ -79,7 +79,13 @@ internal fun TokenDetailsScreen(state: TokenDetailsState) { state = state.tokenInfoBlockState, ) } - item { TokenDetailsBalanceBlock(modifier = itemModifier, state = state.tokenBalanceBlockState) } + item { + TokenDetailsBalanceBlock( + modifier = itemModifier, + isBalanceHidden = state.isBalanceHidden, + state = state.tokenBalanceBlockState, + ) + } items( items = state.notifications.filter { it.isVisible }, key = { it.config::class.java }, @@ -95,11 +101,16 @@ internal fun TokenDetailsScreen(state: TokenDetailsState) { item { PendingTxsBlock( pendingTxs = state.pendingTxs, + isBalanceHidden = state.isBalanceHidden, modifier = itemModifier, ) } } - txHistoryItems(state = state.txHistoryState, txHistoryItems = txHistoryItems) + txHistoryItems( + state = state.txHistoryState, + isBalanceHidden = state.isBalanceHidden, + txHistoryItems = txHistoryItems, + ) } PullRefreshIndicator( @@ -127,7 +138,11 @@ internal fun TokenDetailsScreen(state: TokenDetailsState) { } @Composable -private fun PendingTxsBlock(pendingTxs: PersistentList, modifier: Modifier = Modifier) { +private fun PendingTxsBlock( + pendingTxs: PersistentList, + isBalanceHidden: Boolean, + modifier: Modifier = Modifier, +) { Column( modifier = modifier .clip(shape = TangemTheme.shapes.roundedCornersXMedium) @@ -135,7 +150,7 @@ private fun PendingTxsBlock(pendingTxs: PersistentList, modifi verticalArrangement = Arrangement.spacedBy(space = TangemTheme.dimens.spacing8), horizontalAlignment = Alignment.Start, ) { - pendingTxs.fastForEach { Transaction(state = it) } + pendingTxs.fastForEach { Transaction(state = it, isBalanceHidden = isBalanceHidden) } } } diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenDetailsBalanceBlock.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenDetailsBalanceBlock.kt index 2a3cf30680..0fc3606d9b 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenDetailsBalanceBlock.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenDetailsBalanceBlock.kt @@ -9,6 +9,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider +import com.tangem.common.Strings.STARS import com.tangem.core.ui.components.RectangleShimmer import com.tangem.core.ui.components.buttons.HorizontalActionChips import com.tangem.core.ui.res.TangemTheme @@ -20,7 +21,11 @@ import com.tangem.features.tokendetails.impl.R import kotlinx.collections.immutable.toImmutableList @Composable -internal fun TokenDetailsBalanceBlock(state: TokenDetailsBalanceBlockState, modifier: Modifier = Modifier) { +internal fun TokenDetailsBalanceBlock( + state: TokenDetailsBalanceBlockState, + isBalanceHidden: Boolean, + modifier: Modifier = Modifier, +) { Surface( modifier = modifier.fillMaxWidth(), shape = TangemTheme.shapes.roundedCornersXMedium, @@ -41,12 +46,14 @@ internal fun TokenDetailsBalanceBlock(state: TokenDetailsBalanceBlockState, modi ) FiatBalance( state = state, + isBalanceHidden = isBalanceHidden, modifier = Modifier .padding(top = TangemTheme.dimens.spacing4) .padding(horizontal = TangemTheme.dimens.spacing12), ) CryptoBalance( state = state, + isBalanceHidden = isBalanceHidden, modifier = Modifier .padding(top = TangemTheme.dimens.spacing4) .padding(horizontal = TangemTheme.dimens.spacing12), @@ -62,7 +69,11 @@ internal fun TokenDetailsBalanceBlock(state: TokenDetailsBalanceBlockState, modi } @Composable -private fun FiatBalance(state: TokenDetailsBalanceBlockState, modifier: Modifier = Modifier) { +private fun FiatBalance( + state: TokenDetailsBalanceBlockState, + isBalanceHidden: Boolean, + modifier: Modifier = Modifier, +) { when (state) { is TokenDetailsBalanceBlockState.Loading -> RectangleShimmer( modifier = modifier.size( @@ -72,7 +83,7 @@ private fun FiatBalance(state: TokenDetailsBalanceBlockState, modifier: Modifier ) is TokenDetailsBalanceBlockState.Content -> Text( modifier = modifier, - text = if (state.isBalanceHidden) DOTS else state.fiatBalance, + text = if (isBalanceHidden) STARS else state.fiatBalance, style = TangemTheme.typography.h2, color = TangemTheme.colors.text.primary1, ) @@ -86,7 +97,11 @@ private fun FiatBalance(state: TokenDetailsBalanceBlockState, modifier: Modifier } @Composable -private fun CryptoBalance(state: TokenDetailsBalanceBlockState, modifier: Modifier = Modifier) { +private fun CryptoBalance( + state: TokenDetailsBalanceBlockState, + isBalanceHidden: Boolean, + modifier: Modifier = Modifier, +) { when (state) { is TokenDetailsBalanceBlockState.Loading -> RectangleShimmer( modifier = modifier.size( @@ -96,7 +111,7 @@ private fun CryptoBalance(state: TokenDetailsBalanceBlockState, modifier: Modifi ) is TokenDetailsBalanceBlockState.Content -> Text( modifier = modifier, - text = if (state.isBalanceHidden) DOTS else state.cryptoBalance, + text = if (isBalanceHidden) STARS else state.cryptoBalance, style = TangemTheme.typography.caption, color = TangemTheme.colors.text.tertiary, ) @@ -115,7 +130,7 @@ private fun Preview_TokenDetailsBalanceBlock_LightTheme( @PreviewParameter(TokenDetailsBalanceBlockStateProvider::class) state: TokenDetailsBalanceBlockState, ) { TangemTheme(isDark = false) { - TokenDetailsBalanceBlock(state) + TokenDetailsBalanceBlock(state = state, isBalanceHidden = false) } } @@ -125,7 +140,7 @@ private fun Preview_TokenDetailsBalanceBlock_DarkTheme( @PreviewParameter(TokenDetailsBalanceBlockStateProvider::class) state: TokenDetailsBalanceBlockState, ) { TangemTheme(isDark = true) { - TokenDetailsBalanceBlock(state) + TokenDetailsBalanceBlock(state = state, isBalanceHidden = false) } } @@ -135,6 +150,4 @@ private class TokenDetailsBalanceBlockStateProvider : CollectionPreviewParameter TokenDetailsPreviewData.balanceContent, TokenDetailsPreviewData.balanceError, ), -) - -const val DOTS = "***" \ No newline at end of file +) \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt index 4487529033..2e1ffc3c93 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt @@ -78,12 +78,10 @@ internal class TokenDetailsViewModel @Inject constructor( private var cryptoCurrency by Delegates.notNull() private val selectedAppCurrencyFlow: StateFlow = createSelectedAppCurrencyFlow() - private var isBalanceHidden = true private val stateFactory = TokenDetailsStateFactory( currentStateProvider = Provider { uiState }, appCurrencyProvider = Provider(selectedAppCurrencyFlow::value), - isBalanceHiddenProvider = Provider { isBalanceHidden }, clickIntents = this, currencySymbolProvider = Provider { cryptoCurrency.symbol }, currencyDecimalsProvider = Provider { cryptoCurrency.decimals }, @@ -125,8 +123,9 @@ internal class TokenDetailsViewModel @Inject constructor( isBalanceHiddenUseCase() .flowWithLifecycle(owner.lifecycle) .onEach { hidden -> - isBalanceHidden = hidden - uiState = stateFactory.getStateWithUpdatedHidden(isBalanceHidden = hidden) + uiState = stateFactory.getStateWithUpdatedHidden( + isBalanceHidden = hidden, + ) } .launchIn(viewModelScope) @@ -186,11 +185,9 @@ internal class TokenDetailsViewModel @Inject constructor( } txHistoryItemsCountEither.onRight { - uiState = stateFactory.getLoadedTxHistoryState( - txHistoryEither = txHistoryItemsUseCase(currency = cryptoCurrency).map { - it.cachedIn(viewModelScope) - }, - ) + val either = txHistoryItemsUseCase(currency = cryptoCurrency) + .map { it.cachedIn(viewModelScope) } + uiState = stateFactory.getLoadedTxHistoryState(txHistoryEither = either) } } } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/WalletPreviewData.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/WalletPreviewData.kt index 5ae45f4206..f3786031b5 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/WalletPreviewData.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/WalletPreviewData.kt @@ -397,6 +397,7 @@ internal object WalletPreviewData { tokenActionsBottomSheet = actionsBottomSheet, onManageTokensClick = {}, event = consumedEvent(), + isBalanceHidden = false, ) } @@ -451,6 +452,7 @@ internal object WalletPreviewData { ), ), event = consumedEvent(), + isBalanceHidden = false, ) } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletMultiCurrencyState.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletMultiCurrencyState.kt index d91449abb4..0a71550228 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletMultiCurrencyState.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletMultiCurrencyState.kt @@ -26,6 +26,7 @@ internal sealed class WalletMultiCurrencyState : WalletState.ContentState() { override val bottomSheetConfig: TangemBottomSheetConfig?, override val tokensListState: WalletTokensListState, override val event: StateEvent = consumedEvent(), + override val isBalanceHidden: Boolean, val tokenActionsBottomSheet: ActionsBottomSheetConfig?, val onManageTokensClick: () -> Unit, ) : WalletMultiCurrencyState() @@ -41,6 +42,7 @@ internal sealed class WalletMultiCurrencyState : WalletState.ContentState() { override val isBottomSheetShow: Boolean = false, override val onBottomSheetDismiss: () -> Unit = {}, override val event: StateEvent = consumedEvent(), + override val isBalanceHidden: Boolean, ) : WalletMultiCurrencyState(), WalletLockedState { override val notifications = persistentListOf( diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletSingleCurrencyState.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletSingleCurrencyState.kt index 6a5467d025..4bb761d008 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletSingleCurrencyState.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletSingleCurrencyState.kt @@ -38,6 +38,7 @@ internal sealed class WalletSingleCurrencyState : WalletState.ContentState() { override val buttons: PersistentList, override val txHistoryState: TxHistoryState, override val event: StateEvent = consumedEvent(), + override val isBalanceHidden: Boolean, val marketPriceBlockState: MarketPriceBlockState, ) : WalletSingleCurrencyState() @@ -53,6 +54,7 @@ internal sealed class WalletSingleCurrencyState : WalletState.ContentState() { override val isBottomSheetShow: Boolean = false, override val onBottomSheetDismiss: () -> Unit = {}, override val event: StateEvent = consumedEvent(), + override val isBalanceHidden: Boolean, val onExploreClick: () -> Unit, ) : WalletSingleCurrencyState(), WalletLockedState { diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletState.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletState.kt index 9872db9572..a3e9904881 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletState.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletState.kt @@ -39,6 +39,9 @@ internal sealed class WalletState { /** State event */ abstract val event: StateEvent + /** Whether balance should be hidden */ + abstract val isBalanceHidden: Boolean + /** * Util function that allow to make a copy * @@ -50,6 +53,7 @@ internal sealed class WalletState { walletsListConfig: WalletsListConfig = this.walletsListConfig, pullToRefreshConfig: WalletPullToRefreshConfig = this.pullToRefreshConfig, event: StateEvent = this.event, + isBalanceHidden: Boolean = this.isBalanceHidden, ): ContentState { return when (this) { is WalletMultiCurrencyState.Content -> { @@ -57,6 +61,7 @@ internal sealed class WalletState { walletsListConfig = walletsListConfig, pullToRefreshConfig = pullToRefreshConfig, event = event, + isBalanceHidden = isBalanceHidden, ) } is WalletMultiCurrencyState.Locked -> { @@ -64,6 +69,7 @@ internal sealed class WalletState { walletsListConfig = walletsListConfig, pullToRefreshConfig = pullToRefreshConfig, event = event, + isBalanceHidden = isBalanceHidden, ) } is WalletSingleCurrencyState.Content -> { @@ -71,6 +77,7 @@ internal sealed class WalletState { walletsListConfig = walletsListConfig, pullToRefreshConfig = pullToRefreshConfig, event = event, + isBalanceHidden = isBalanceHidden, ) } is WalletSingleCurrencyState.Locked -> { @@ -78,6 +85,7 @@ internal sealed class WalletState { walletsListConfig = walletsListConfig, pullToRefreshConfig = pullToRefreshConfig, event = event, + isBalanceHidden = isBalanceHidden, ) } } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletLockedConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletLockedConverter.kt index 51cb49325d..8007a4cf4d 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletLockedConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletLockedConverter.kt @@ -37,6 +37,7 @@ internal class WalletLockedConverter( onUnlockWalletsNotificationClick = clickIntents::onUnlockWalletNotificationClick, onUnlockClick = clickIntents::onUnlockWalletClick, onScanClick = clickIntents::onScanToUnlockWalletClick, + isBalanceHidden = isBalanceHidden, ) } @@ -51,6 +52,7 @@ internal class WalletLockedConverter( onUnlockClick = clickIntents::onUnlockWalletClick, onScanClick = clickIntents::onScanToUnlockWalletClick, onExploreClick = clickIntents::onExploreClick, + isBalanceHidden = isBalanceHidden, ) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletSkeletonStateConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletSkeletonStateConverter.kt index 55520ad855..ffc4fb691f 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletSkeletonStateConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletSkeletonStateConverter.kt @@ -55,6 +55,7 @@ internal class WalletSkeletonStateConverter( bottomSheetConfig = null, tokenActionsBottomSheet = null, onManageTokensClick = clickIntents::onManageTokensClick, + isBalanceHidden = true, ) } @@ -73,6 +74,7 @@ internal class WalletSkeletonStateConverter( value = TxHistoryState.getDefaultLoadingTransactions(clickIntents::onExploreClick), ), ), + isBalanceHidden = true, ) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletStateFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletStateFactory.kt index f4b1ade111..92783093a4 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletStateFactory.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletStateFactory.kt @@ -88,6 +88,7 @@ internal class WalletStateFactory( private val loadedTxHistoryConverter by lazy { WalletLoadedTxHistoryConverter( currentStateProvider = currentStateProvider, + isBalanceHiddenProvider = isBalanceHiddenProvider, currentCardTypeResolverProvider = currentCardTypeResolverProvider, clickIntents = clickIntents, ) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletsUnlockStateConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletsUnlockStateConverter.kt index 39f3eb7cc9..ed986d7f0e 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletsUnlockStateConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletsUnlockStateConverter.kt @@ -54,6 +54,7 @@ internal class WalletsUnlockStateConverter( bottomSheetConfig = null, tokenActionsBottomSheet = null, onManageTokensClick = clickIntents::onManageTokensClick, + isBalanceHidden = isBalanceHidden, ) } @@ -74,6 +75,7 @@ internal class WalletsUnlockStateConverter( value = TxHistoryState.getDefaultLoadingTransactions(clickIntents::onExploreClick), ), ), + isBalanceHidden = isBalanceHidden, ) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/txhistory/WalletLoadedTxHistoryConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/txhistory/WalletLoadedTxHistoryConverter.kt index 202fcc3ae5..7d2636f3a2 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/txhistory/WalletLoadedTxHistoryConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/txhistory/WalletLoadedTxHistoryConverter.kt @@ -27,6 +27,7 @@ internal class WalletLoadedTxHistoryConverter( private val currentStateProvider: Provider, private val currentCardTypeResolverProvider: Provider, private val clickIntents: WalletClickIntents, + private val isBalanceHiddenProvider: Provider, ) : Converter>>, WalletState> { private val walletTxHistoryItemFlowConverter by lazy { @@ -34,6 +35,7 @@ internal class WalletLoadedTxHistoryConverter( currentStateProvider = currentStateProvider, blockchain = currentCardTypeResolverProvider().getBlockchain(), clickIntents = clickIntents, + isBalanceHiddenProvider = isBalanceHiddenProvider, ) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/txhistory/WalletTxHistoryItemFlowConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/txhistory/WalletTxHistoryItemFlowConverter.kt index bac145142e..2789256422 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/txhistory/WalletTxHistoryItemFlowConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/txhistory/WalletTxHistoryItemFlowConverter.kt @@ -35,12 +35,17 @@ import org.joda.time.DateTimeZone */ internal class WalletTxHistoryItemFlowConverter( private val currentStateProvider: Provider, + private val isBalanceHiddenProvider: Provider, private val blockchain: Blockchain, private val clickIntents: WalletClickIntents, ) : Converter>, TxHistoryState?> { private val txHistoryItemConverter by lazy { - WalletTxHistoryTransactionStateConverter(symbol = blockchain.currency, decimals = blockchain.decimals()) + WalletTxHistoryTransactionStateConverter( + symbol = blockchain.currency, + decimals = blockchain.decimals(), + isBalanceHiddenProvider = isBalanceHiddenProvider, + ) } override fun convert(value: Flow>): TxHistoryState? { diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/txhistory/WalletTxHistoryTransactionStateConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/txhistory/WalletTxHistoryTransactionStateConverter.kt index 44dedce7b4..c4af843d2b 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/txhistory/WalletTxHistoryTransactionStateConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/txhistory/WalletTxHistoryTransactionStateConverter.kt @@ -1,5 +1,7 @@ package com.tangem.feature.wallet.presentation.wallet.state.factory.txhistory +import com.tangem.common.Provider +import com.tangem.common.Strings import com.tangem.core.ui.components.transactions.state.TransactionState import com.tangem.core.ui.extensions.TextReference import com.tangem.domain.txhistory.models.TxHistoryItem @@ -11,6 +13,7 @@ import com.tangem.utils.toFormattedCurrencyString class WalletTxHistoryTransactionStateConverter( private val symbol: String, private val decimals: Int, + private val isBalanceHiddenProvider: Provider, ) : Converter { override fun convert(value: TxHistoryItem): TransactionState { @@ -26,7 +29,7 @@ class WalletTxHistoryTransactionStateConverter( TxHistoryItem.TransactionType.Deposit -> TransactionState.Custom( txHash = item.txHash, address = item.direction.extractAddress(), - amount = item.extractFormattedCryptoBalance(), + amount = item.getAmount(), timestamp = item.getRawTimestamp(), status = item.status.tiUiStatus(), direction = item.direction.toUiDirection(), @@ -36,7 +39,7 @@ class WalletTxHistoryTransactionStateConverter( TxHistoryItem.TransactionType.Submit -> TransactionState.Custom( txHash = item.txHash, address = item.direction.extractAddress(), - amount = item.extractFormattedCryptoBalance(), + amount = item.getAmount(), timestamp = item.getRawTimestamp(), status = item.status.tiUiStatus(), direction = item.direction.toUiDirection(), @@ -46,7 +49,7 @@ class WalletTxHistoryTransactionStateConverter( TxHistoryItem.TransactionType.Supply -> TransactionState.Custom( txHash = item.txHash, address = item.direction.extractAddress(), - amount = item.extractFormattedCryptoBalance(), + amount = item.getAmount(), timestamp = item.getRawTimestamp(), status = item.status.tiUiStatus(), direction = item.direction.toUiDirection(), @@ -56,7 +59,7 @@ class WalletTxHistoryTransactionStateConverter( TxHistoryItem.TransactionType.Unoswap -> TransactionState.Custom( txHash = item.txHash, address = item.direction.extractAddress(), - amount = item.extractFormattedCryptoBalance(), + amount = item.getAmount(), timestamp = item.getRawTimestamp(), status = item.status.tiUiStatus(), direction = item.direction.toUiDirection(), @@ -66,7 +69,7 @@ class WalletTxHistoryTransactionStateConverter( TxHistoryItem.TransactionType.Withdraw -> TransactionState.Custom( txHash = item.txHash, address = item.direction.extractAddress(), - amount = item.extractFormattedCryptoBalance(), + amount = item.getAmount(), timestamp = item.getRawTimestamp(), status = item.status.tiUiStatus(), direction = item.direction.toUiDirection(), @@ -76,7 +79,7 @@ class WalletTxHistoryTransactionStateConverter( is TxHistoryItem.TransactionType.Custom -> TransactionState.Custom( txHash = item.txHash, address = item.direction.extractAddress(), - amount = item.extractFormattedCryptoBalance(), + amount = item.getAmount(), timestamp = item.getRawTimestamp(), status = item.status.tiUiStatus(), direction = item.direction.toUiDirection(), @@ -90,7 +93,7 @@ class WalletTxHistoryTransactionStateConverter( return TransactionState.Transfer( txHash = item.txHash, address = item.direction.extractAddress(), - amount = item.extractFormattedCryptoBalance(), + amount = item.getAmount(), timestamp = item.getRawTimestamp(), status = item.status.tiUiStatus(), direction = item.direction.toUiDirection(), @@ -101,7 +104,7 @@ class WalletTxHistoryTransactionStateConverter( return TransactionState.Approve( txHash = item.txHash, address = item.direction.extractAddress(), - amount = item.extractFormattedCryptoBalance(), + amount = item.getAmount(), timestamp = item.getRawTimestamp(), status = item.status.tiUiStatus(), direction = item.direction.toUiDirection(), @@ -111,7 +114,7 @@ class WalletTxHistoryTransactionStateConverter( return TransactionState.Swap( txHash = item.txHash, address = item.direction.extractAddress(), - amount = item.extractFormattedCryptoBalance(), + amount = item.getAmount(), timestamp = item.getRawTimestamp(), status = item.status.tiUiStatus(), direction = item.direction.toUiDirection(), @@ -142,7 +145,9 @@ class WalletTxHistoryTransactionStateConverter( is TxHistoryItem.TransactionDirection.Outgoing -> TransactionState.Content.Direction.OUTGOING } - private fun TxHistoryItem.extractFormattedCryptoBalance(): String { + private fun TxHistoryItem.getAmount(): String { + if (isBalanceHiddenProvider()) return Strings.STARS + val prefix = when (direction) { is TxHistoryItem.TransactionDirection.Incoming -> "+" is TxHistoryItem.TransactionDirection.Outgoing -> "-" diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt index bacaea655a..8d61d90510 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt @@ -142,7 +142,12 @@ private fun WalletContent( marketPriceBlock(state = state.marketPriceBlockState, modifier = itemModifier) } - contentItems(state = state, txHistoryItems = txHistoryItems, modifier = movableItemModifier) + contentItems( + state = state, + txHistoryItems = txHistoryItems, + isBalanceHidden = state.isBalanceHidden, + modifier = movableItemModifier, + ) if (state is WalletMultiCurrencyState) { val contentTokenListState = state.tokensListState as? WalletTokensListState.ContentState diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletContent.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletContent.kt index 211ad0e75f..cfdc6e5955 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletContent.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletContent.kt @@ -22,10 +22,11 @@ import com.tangem.feature.wallet.presentation.wallet.ui.components.multicurrency internal fun LazyListScope.contentItems( state: WalletState.ContentState, txHistoryItems: LazyPagingItems?, + isBalanceHidden: Boolean, modifier: Modifier = Modifier, ) { when (state) { is WalletMultiCurrencyState -> tokensListItems(state.tokensListState, modifier) - is WalletSingleCurrencyState -> txHistoryItems(state.txHistoryState, txHistoryItems, modifier) + is WalletSingleCurrencyState -> txHistoryItems(state.txHistoryState, txHistoryItems, isBalanceHidden, modifier) } } \ No newline at end of file From fd29bcdc08bb47eac49d8e37940d136af9c9b3fd Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 29 Sep 2023 12:23:14 +0500 Subject: [PATCH 113/242] Updated on 2026-08-14 --- .../redux/middlewares/MultiWalletMiddleware.kt | 1 + .../components/marketprice/MarketPriceBlock.kt | 10 +++++----- .../marketprice/MarketPriceBlockState.kt | 8 ++++---- .../navigation/TokenDetailsArguments.kt | 1 + .../tokendetails/TokenDetailsPreviewData.kt | 2 +- .../TokenDetailsLoadedBalanceConverter.kt | 17 ++++++++++------- .../TokenDetailsSkeletonStateConverter.kt | 2 +- .../presentation/common/WalletPreviewData.kt | 2 +- .../presentation/router/DefaultWalletRouter.kt | 1 + ...alletSingleCurrencyLoadedBalanceConverter.kt | 4 ++-- .../factory/WalletSkeletonStateConverter.kt | 2 +- .../factory/WalletsUnlockStateConverter.kt | 2 +- 12 files changed, 29 insertions(+), 23 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/MultiWalletMiddleware.kt b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/MultiWalletMiddleware.kt index 1e42d1a91f..9b4d3fe816 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/MultiWalletMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/MultiWalletMiddleware.kt @@ -139,6 +139,7 @@ class MultiWalletMiddleware { return TokenDetailsArguments( currencyId = cryptoCurrency.id, currencyName = cryptoCurrency.name, + currencySymbol = cryptoCurrency.symbol, iconUrl = cryptoCurrency.iconUrl, coinType = when (cryptoCurrency) { is CryptoCurrency.Coin -> TokenDetailsArguments.CoinType.Native diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/marketprice/MarketPriceBlock.kt b/core/ui/src/main/java/com/tangem/core/ui/components/marketprice/MarketPriceBlock.kt index 585426d477..40cb551701 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/marketprice/MarketPriceBlock.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/marketprice/MarketPriceBlock.kt @@ -51,7 +51,7 @@ fun MarketPriceBlock(state: MarketPriceBlockState, modifier: Modifier = Modifier verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing6), horizontalAlignment = Alignment.Start, ) { - Title(currencyName = state.currencyName) + Title(currencyName = state.currencySymbol) Content(state = state, rootWidth = rootWidth) } @@ -213,7 +213,7 @@ private fun Preview_MarketPriceBlock_Dark( private class WalletMarketPriceBlockStateProvider : CollectionPreviewParameterProvider( collection = listOf( MarketPriceBlockState.Content( - currencyName = "BTC", + currencySymbol = "BTC", price = "98900 $", priceChangeConfig = PriceChangeState.Content( valueInPercent = "5.16%", @@ -221,14 +221,14 @@ private class WalletMarketPriceBlockStateProvider : CollectionPreviewParameterPr ), ), MarketPriceBlockState.Content( - currencyName = "BTC", + currencySymbol = "BTC", price = "98900 $", priceChangeConfig = PriceChangeState.Content( valueInPercent = "10.89%", type = PriceChangeType.UP, ), ), - MarketPriceBlockState.Loading(currencyName = "BTC"), - MarketPriceBlockState.Error(currencyName = "BTC"), + MarketPriceBlockState.Loading(currencySymbol = "BTC"), + MarketPriceBlockState.Error(currencySymbol = "BTC"), ), ) \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/marketprice/MarketPriceBlockState.kt b/core/ui/src/main/java/com/tangem/core/ui/components/marketprice/MarketPriceBlockState.kt index e1c7a1e21a..566ce6fef6 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/marketprice/MarketPriceBlockState.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/marketprice/MarketPriceBlockState.kt @@ -5,14 +5,14 @@ import androidx.compose.runtime.Immutable @Immutable sealed interface MarketPriceBlockState { - val currencyName: String + val currencySymbol: String - data class Error(override val currencyName: String) : MarketPriceBlockState + data class Error(override val currencySymbol: String) : MarketPriceBlockState - data class Loading(override val currencyName: String) : MarketPriceBlockState + data class Loading(override val currencySymbol: String) : MarketPriceBlockState data class Content( - override val currencyName: String, + override val currencySymbol: String, val price: String, val priceChangeConfig: PriceChangeState.Content, ) : MarketPriceBlockState diff --git a/features/tokendetails/api/src/main/kotlin/com/tangem/features/tokendetails/navigation/TokenDetailsArguments.kt b/features/tokendetails/api/src/main/kotlin/com/tangem/features/tokendetails/navigation/TokenDetailsArguments.kt index 30c0ecc912..03a33f3af3 100644 --- a/features/tokendetails/api/src/main/kotlin/com/tangem/features/tokendetails/navigation/TokenDetailsArguments.kt +++ b/features/tokendetails/api/src/main/kotlin/com/tangem/features/tokendetails/navigation/TokenDetailsArguments.kt @@ -9,6 +9,7 @@ import kotlinx.parcelize.Parcelize data class TokenDetailsArguments( val currencyId: CryptoCurrency.ID, val currencyName: String, + val currencySymbol: String, val iconUrl: String?, val coinType: CoinType, ) : Parcelable { diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/TokenDetailsPreviewData.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/TokenDetailsPreviewData.kt index d7771e9eb6..47fb487c28 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/TokenDetailsPreviewData.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/TokenDetailsPreviewData.kt @@ -70,7 +70,7 @@ internal object TokenDetailsPreviewData { ) val balanceError = TokenDetailsBalanceBlockState.Error(actionButtons = actionButtons) - private val marketPriceLoading = MarketPriceBlockState.Loading(currencyName = "USDT") + private val marketPriceLoading = MarketPriceBlockState.Loading(currencySymbol = "USDT") private val pullToRefreshConfig = TokenDetailsPullToRefreshConfig( isRefreshing = false, diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsLoadedBalanceConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsLoadedBalanceConverter.kt index 9bb7136de7..9240092123 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsLoadedBalanceConverter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsLoadedBalanceConverter.kt @@ -37,17 +37,17 @@ internal class TokenDetailsLoadedBalanceConverter( val state = currentStateProvider() return state.copy( tokenBalanceBlockState = TokenDetailsBalanceBlockState.Error(state.tokenBalanceBlockState.actionButtons), - marketPriceBlockState = MarketPriceBlockState.Error(state.marketPriceBlockState.currencyName), + marketPriceBlockState = MarketPriceBlockState.Error(state.marketPriceBlockState.currencySymbol), notifications = persistentListOf(TokenDetailsNotification.NetworksUnreachable), ) } private fun convert(status: CryptoCurrencyStatus): TokenDetailsState { val state = currentStateProvider() - val currencyName = state.marketPriceBlockState.currencyName + val currencyName = state.marketPriceBlockState.currencySymbol return state.copy( tokenBalanceBlockState = getBalanceState(state.tokenBalanceBlockState, status), - marketPriceBlockState = getMarketPriceState(status = status.value, currencyName = currencyName), + marketPriceBlockState = getMarketPriceState(status = status.value, currencySymbol = currencyName), pendingTxs = status.value.pendingTransactions.map(txHistoryItemConverter::convert).toPersistentList(), ) } @@ -80,10 +80,13 @@ internal class TokenDetailsLoadedBalanceConverter( } } - private fun getMarketPriceState(status: CryptoCurrencyStatus.Status, currencyName: String): MarketPriceBlockState { + private fun getMarketPriceState( + status: CryptoCurrencyStatus.Status, + currencySymbol: String, + ): MarketPriceBlockState { return when (status) { - is CryptoCurrencyStatus.Loading -> MarketPriceBlockState.Loading(currencyName) - is CryptoCurrencyStatus.NoQuote -> MarketPriceBlockState.Error(currencyName) + is CryptoCurrencyStatus.Loading -> MarketPriceBlockState.Loading(currencySymbol) + is CryptoCurrencyStatus.NoQuote -> MarketPriceBlockState.Error(currencySymbol) is CryptoCurrencyStatus.Loaded, is CryptoCurrencyStatus.Custom, is CryptoCurrencyStatus.MissedDerivation, @@ -91,7 +94,7 @@ internal class TokenDetailsLoadedBalanceConverter( is CryptoCurrencyStatus.NoAmount, is CryptoCurrencyStatus.Unreachable, -> MarketPriceBlockState.Content( - currencyName = currencyName, + currencySymbol = currencySymbol, price = formatPrice(status, appCurrencyProvider()), priceChangeConfig = PriceChangeState.Content( valueInPercent = formatPriceChange(status), diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSkeletonStateConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSkeletonStateConverter.kt index d149791b9e..9392fa607c 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSkeletonStateConverter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSkeletonStateConverter.kt @@ -38,7 +38,7 @@ internal class TokenDetailsSkeletonStateConverter( }, ), tokenBalanceBlockState = TokenDetailsBalanceBlockState.Loading(actionButtons = createButtons()), - marketPriceBlockState = MarketPriceBlockState.Loading(value.currencyName), + marketPriceBlockState = MarketPriceBlockState.Loading(value.currencySymbol), notifications = persistentListOf(), pendingTxs = persistentListOf(), txHistoryState = TxHistoryState.Content( diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/WalletPreviewData.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/WalletPreviewData.kt index f3786031b5..2e38ce1e26 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/WalletPreviewData.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/WalletPreviewData.kt @@ -414,7 +414,7 @@ internal object WalletPreviewData { buttons = manageButtons, bottomSheetConfig = bottomSheet, marketPriceBlockState = MarketPriceBlockState.Content( - currencyName = "BTC", + currencySymbol = "BTC", price = "98900.12$", priceChangeConfig = PriceChangeState.Content( valueInPercent = "5.16%", diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/DefaultWalletRouter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/DefaultWalletRouter.kt index dbe5b06c33..f134aeade2 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/DefaultWalletRouter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/DefaultWalletRouter.kt @@ -117,6 +117,7 @@ internal class DefaultWalletRouter(private val reduxNavController: ReduxNavContr TokenDetailsRouter.TOKEN_DETAILS_ARGS to TokenDetailsArguments( currencyId = currency.id, currencyName = currency.name, + currencySymbol = currency.symbol, iconUrl = currency.iconUrl, coinType = when (currency) { is CryptoCurrency.Coin -> TokenDetailsArguments.CoinType.Native diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletSingleCurrencyLoadedBalanceConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletSingleCurrencyLoadedBalanceConverter.kt index 99e0575bb5..ff8df226a1 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletSingleCurrencyLoadedBalanceConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletSingleCurrencyLoadedBalanceConverter.kt @@ -38,7 +38,7 @@ internal class WalletSingleCurrencyLoadedBalanceConverter( private fun convertContent(status: CryptoCurrencyStatus): WalletState { return when (val state = currentStateProvider()) { is WalletSingleCurrencyState.Content -> { - val currencyName = state.marketPriceBlockState.currencyName + val currencyName = state.marketPriceBlockState.currencySymbol state.copy( walletsListConfig = getUpdatedSelectedWallet(status = status.value, state = state), @@ -58,7 +58,7 @@ internal class WalletSingleCurrencyLoadedBalanceConverter( is CryptoCurrencyStatus.NoQuote, is CryptoCurrencyStatus.Loaded, -> MarketPriceBlockState.Content( - currencyName = currencyName, + currencySymbol = currencyName, price = formatPrice(status, appCurrencyProvider()), priceChangeConfig = PriceChangeState.Content( valueInPercent = formatPriceChange(status), diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletSkeletonStateConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletSkeletonStateConverter.kt index ffc4fb691f..51a78e138e 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletSkeletonStateConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletSkeletonStateConverter.kt @@ -68,7 +68,7 @@ internal class WalletSkeletonStateConverter( notifications = persistentListOf(), bottomSheetConfig = null, buttons = createButtons(), - marketPriceBlockState = MarketPriceBlockState.Loading(currencyName = currencyName), + marketPriceBlockState = MarketPriceBlockState.Loading(currencySymbol = currencyName), txHistoryState = TxHistoryState.Content( contentItems = MutableStateFlow( value = TxHistoryState.getDefaultLoadingTransactions(clickIntents::onExploreClick), diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletsUnlockStateConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletsUnlockStateConverter.kt index ed986d7f0e..b1488f51a6 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletsUnlockStateConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletsUnlockStateConverter.kt @@ -68,7 +68,7 @@ internal class WalletsUnlockStateConverter( bottomSheetConfig = null, buttons = buttons, marketPriceBlockState = MarketPriceBlockState.Loading( - currencyName = action.selectedWallet.getPrimaryCurrencyName(), + currencySymbol = action.selectedWallet.getPrimaryCurrencyName(), ), txHistoryState = TxHistoryState.Content( contentItems = MutableStateFlow( From 187218cf037839ec4b58fa6f0627882bd0f22dd7 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 29 Sep 2023 12:40:50 +0500 Subject: [PATCH 114/242] Updated on 2026-08-14 --- .../redux/middlewares/MultiWalletMiddleware.kt | 1 + .../tokendetails/navigation/TokenDetailsArguments.kt | 2 ++ .../tokendetails/TokenDetailsPreviewData.kt | 1 + .../tokendetails/state/TokenDetailsState.kt | 1 + .../factory/TokenDetailsSkeletonStateConverter.kt | 4 +++- .../tokendetails/ui/TokenDetailsScreen.kt | 12 +++++++----- .../presentation/router/DefaultWalletRouter.kt | 1 + 7 files changed, 16 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/MultiWalletMiddleware.kt b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/MultiWalletMiddleware.kt index 9b4d3fe816..6a76d4ef38 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/MultiWalletMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/MultiWalletMiddleware.kt @@ -144,6 +144,7 @@ class MultiWalletMiddleware { coinType = when (cryptoCurrency) { is CryptoCurrency.Coin -> TokenDetailsArguments.CoinType.Native is CryptoCurrency.Token -> TokenDetailsArguments.CoinType.Token( + isCustom = cryptoCurrency.isCustom, standardName = cryptoCurrency.network.standardType.name, networkName = cryptoCurrency.network.name, networkIcon = cryptoCurrency.networkIconResId, diff --git a/features/tokendetails/api/src/main/kotlin/com/tangem/features/tokendetails/navigation/TokenDetailsArguments.kt b/features/tokendetails/api/src/main/kotlin/com/tangem/features/tokendetails/navigation/TokenDetailsArguments.kt index 03a33f3af3..5238d98973 100644 --- a/features/tokendetails/api/src/main/kotlin/com/tangem/features/tokendetails/navigation/TokenDetailsArguments.kt +++ b/features/tokendetails/api/src/main/kotlin/com/tangem/features/tokendetails/navigation/TokenDetailsArguments.kt @@ -19,10 +19,12 @@ data class TokenDetailsArguments( object Native : CoinType() /** + * @param isCustom - Indicates whether the currency is a custom user-added currency or not. * @param standardName - token standard. Samples: ERC20, BEP20, BEP2, TRC20 and etc. * @param networkName - token's blockchain name. Ethereum, Tron and etc. */ data class Token( + val isCustom: Boolean, val standardName: String, val networkName: String, @DrawableRes val networkIcon: Int, diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/TokenDetailsPreviewData.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/TokenDetailsPreviewData.kt index 47fb487c28..664c3fd0e1 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/TokenDetailsPreviewData.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/TokenDetailsPreviewData.kt @@ -93,5 +93,6 @@ internal object TokenDetailsPreviewData { pullToRefreshConfig = pullToRefreshConfig, bottomSheetConfig = null, isBalanceHidden = false, + isCustomToken = false, ) } \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsState.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsState.kt index 936440ff36..a86052874e 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsState.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsState.kt @@ -22,4 +22,5 @@ internal data class TokenDetailsState( val pullToRefreshConfig: TokenDetailsPullToRefreshConfig, val bottomSheetConfig: TangemBottomSheetConfig?, val isBalanceHidden: Boolean, + val isCustomToken: Boolean, ) \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSkeletonStateConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSkeletonStateConverter.kt index 9392fa607c..e828c9a2be 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSkeletonStateConverter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSkeletonStateConverter.kt @@ -20,6 +20,7 @@ internal class TokenDetailsSkeletonStateConverter( ) : Converter { override fun convert(value: TokenDetailsArguments): TokenDetailsState { + val coinType = value.coinType return TokenDetailsState( topAppBarConfig = TokenDetailsTopAppBarConfig( onBackClick = clickIntents::onBackClick, @@ -28,7 +29,7 @@ internal class TokenDetailsSkeletonStateConverter( tokenInfoBlockState = TokenInfoBlockState( name = value.currencyName, iconUrl = value.iconUrl, - currency = when (val coinType = value.coinType) { + currency = when (coinType) { TokenDetailsArguments.CoinType.Native -> TokenInfoBlockState.Currency.Native is TokenDetailsArguments.CoinType.Token -> TokenInfoBlockState.Currency.Token( standardName = coinType.networkName, @@ -50,6 +51,7 @@ internal class TokenDetailsSkeletonStateConverter( pullToRefreshConfig = createPullToRefresh(), bottomSheetConfig = null, isBalanceHidden = true, + isCustomToken = coinType is TokenDetailsArguments.CoinType.Token && coinType.isCustom, ) } diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/TokenDetailsScreen.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/TokenDetailsScreen.kt index 5ff3e0fff7..d1683f22c8 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/TokenDetailsScreen.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/TokenDetailsScreen.kt @@ -92,11 +92,13 @@ internal fun TokenDetailsScreen(state: TokenDetailsState) { contentType = { it.config::class.java }, itemContent = { Notification(config = it.config, modifier = itemModifier.animateItemPlacement()) }, ) - item( - key = MarketPriceBlockState::class.java, - contentType = MarketPriceBlockState::class.java, - content = { MarketPriceBlock(modifier = itemModifier, state = state.marketPriceBlockState) }, - ) + if (!state.isCustomToken) { + item( + key = MarketPriceBlockState::class.java, + contentType = MarketPriceBlockState::class.java, + content = { MarketPriceBlock(modifier = itemModifier, state = state.marketPriceBlockState) }, + ) + } if (state.txHistoryState is TxHistoryState.NotSupported && state.pendingTxs.isNotEmpty()) { item { PendingTxsBlock( diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/DefaultWalletRouter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/DefaultWalletRouter.kt index f134aeade2..2c80ed2af1 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/DefaultWalletRouter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/DefaultWalletRouter.kt @@ -122,6 +122,7 @@ internal class DefaultWalletRouter(private val reduxNavController: ReduxNavContr coinType = when (currency) { is CryptoCurrency.Coin -> TokenDetailsArguments.CoinType.Native is CryptoCurrency.Token -> TokenDetailsArguments.CoinType.Token( + isCustom = currency.isCustom, standardName = currency.network.standardType.name, networkName = currency.network.name, networkIcon = currency.networkIconResId, From c1302681a49813139332c7434c897ef37f8d3219 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 29 Sep 2023 12:51:19 +0500 Subject: [PATCH 115/242] Updated on 2026-08-14 --- .../repository/DefaultCurrenciesRepository.kt | 17 +++++++++++------ .../utils/ResponseCryptoCurrenciesFactory.kt | 14 +++++++++++--- .../viewmodels/TokenDetailsViewModel.kt | 2 +- 3 files changed, 23 insertions(+), 10 deletions(-) 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 cbe836e1cd..fa5f16dbf1 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 @@ -10,6 +10,7 @@ import com.tangem.datasource.api.tangemTech.models.UserTokensResponse import com.tangem.datasource.local.token.UserMarketCoinsStore import com.tangem.datasource.local.token.UserTokensStore import com.tangem.datasource.local.userwallet.UserWalletsStore +import com.tangem.domain.common.extensions.toCoinId import com.tangem.domain.common.extensions.toNetworkId import com.tangem.domain.common.util.derivationStyleProvider import com.tangem.domain.core.error.DataError @@ -205,9 +206,17 @@ internal class DefaultCurrenciesRepository( val storedTokens = requireNotNull(userTokensStore.getSyncOrNull(userWallet.walletId)) { "Unable to find tokens response for user wallet with provided ID: $userWalletId" } + val blockchain = Blockchain.fromId(networkId.value) + val derivationPath = blockchain + .derivationPath(userWallet.scanResponse.derivationStyleProvider.getDerivationStyle()) + ?.rawPath - val storedCoin = storedTokens.tokens.find { it.networkId == Blockchain.fromId(networkId.value).toNetworkId() } - ?: error("Coin in this network $networkId not found") + val storedCoin = storedTokens.tokens + .find { + it.networkId == blockchain.toNetworkId() && + it.id == blockchain.toCoinId() && + it.derivationPath == derivationPath + } ?: error("Coin in this network $networkId not found") val coin = responseCurrenciesFactory.createCurrency(storedCoin, userWallet.scanResponse) @@ -341,8 +350,4 @@ internal class DefaultCurrenciesRepository( } private fun getTokensCacheKey(userWalletId: UserWalletId): String = "tokens_cache_key_${userWalletId.stringValue}" - - private companion object { - const val NOT_FOUND_HTTP_CODE = 404 - } } \ No newline at end of file diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/ResponseCryptoCurrenciesFactory.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/ResponseCryptoCurrenciesFactory.kt index d0f16e8762..27afcb1c84 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/ResponseCryptoCurrenciesFactory.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/ResponseCryptoCurrenciesFactory.kt @@ -6,6 +6,7 @@ import com.tangem.datasource.api.tangemTech.models.UserTokensResponse import com.tangem.domain.common.DerivationStyleProvider import com.tangem.domain.common.extensions.fromNetworkId import com.tangem.domain.common.extensions.toCoinId +import com.tangem.domain.common.extensions.toNetworkId import com.tangem.domain.common.util.derivationStyleProvider import com.tangem.domain.demo.DemoConfig import com.tangem.domain.models.scan.ScanResponse @@ -21,10 +22,17 @@ internal class ResponseCryptoCurrenciesFactory(private val demoConfig: DemoConfi scanResponse: ScanResponse, ): CryptoCurrency { val responseTokenId = currencyId.rawCurrencyId + val blockchain = Blockchain.fromId(currencyId.rawNetworkId) + val networkId = blockchain.toNetworkId() + val derivationPath = blockchain + .derivationPath(scanResponse.derivationStyleProvider.getDerivationStyle()) + ?.rawPath - val token = requireNotNull(response.tokens.firstOrNull { it.id == responseTokenId }) { - "Unable find a token with provided ID: $responseTokenId" - } + val token = requireNotNull( + value = response.tokens + .find { it.id == responseTokenId && it.networkId == networkId && it.derivationPath == derivationPath }, + lazyMessage = { "Unable find a token with provided TokenID($responseTokenId) and NetworkID($networkId)" }, + ) return requireNotNull(createCurrency(token, scanResponse)) { "Unable to create a currency with provided ID: $currencyId" diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt index 2e1ffc3c93..a8f37d0bf3 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt @@ -102,7 +102,7 @@ internal class TokenDetailsViewModel @Inject constructor( ifRight = { wallet = it }, ) viewModelScope.launch { - getCryptoCurrencyUseCase.invoke(userWalletId = wallet.walletId, id = screenArgument.currencyId) + getCryptoCurrencyUseCase(userWalletId = wallet.walletId, id = screenArgument.currencyId) .fold( ifLeft = { error("Can not get cryptoCurrency with given ID: screenArgument.currencyId. $it") }, ifRight = { From e1f80e7e41f25d3cc63af962f51d3ea8fc4dfb5c Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 29 Sep 2023 14:54:15 +0800 Subject: [PATCH 116/242] Updated on 2026-08-14 --- .../com/tangem/core/ui/res/TangemDimens.kt | 1 + .../core/ui/utils/BigDecimalFormatter.kt | 12 +- .../presentation/common/WalletPreviewData.kt | 85 +++++---- .../common/component/TokenItem.kt | 54 +++--- .../component/token/TokenCryptoAmount.kt | 54 +++--- .../common/component/token/TokenFiatAmount.kt | 57 +++---- .../component/token/TokenPriceChange.kt | 83 ++++----- .../common/component/token/TokenTitle.kt | 49 +++--- .../component/token/icon/ContentIcon.kt | 3 + .../common/component/token/icon/TokenIcon.kt | 20 ++- .../common/state/TokenItemState.kt | 161 ++++++++++++------ .../TokenItemHiddenStateConverter.kt | 17 +- .../CryptoCurrencyToDraggableItemConverter.kt | 13 +- ...ryptoCurrencyStatusToTokenItemConverter.kt | 40 +++-- .../wallet/utils/HiddenStateConverter.kt | 4 +- 15 files changed, 356 insertions(+), 297 deletions(-) diff --git a/core/ui/src/main/java/com/tangem/core/ui/res/TangemDimens.kt b/core/ui/src/main/java/com/tangem/core/ui/res/TangemDimens.kt index 3f80bcbc71..bea3f669ff 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/res/TangemDimens.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/res/TangemDimens.kt @@ -87,6 +87,7 @@ data class TangemDimens internal constructor( val spacing0: Dp = 0.dp, val spacing0_5: Dp = 0.5.dp, val spacing2: Dp = 2.dp, + val spacing3: Dp = 3.dp, val spacing4: Dp = 4.dp, val spacing6: Dp = 6.dp, val spacing8: Dp = 8.dp, diff --git a/core/ui/src/main/java/com/tangem/core/ui/utils/BigDecimalFormatter.kt b/core/ui/src/main/java/com/tangem/core/ui/utils/BigDecimalFormatter.kt index db128105fb..2350350cd0 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/utils/BigDecimalFormatter.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/utils/BigDecimalFormatter.kt @@ -40,10 +40,16 @@ object BigDecimalFormatter { .replace(formatterCurrency.getSymbol(locale), fiatCurrencySymbol) } - fun formatPercent(percent: BigDecimal, useAbsoluteValue: Boolean, locale: Locale = Locale.getDefault()): String { + fun formatPercent( + percent: BigDecimal, + useAbsoluteValue: Boolean, + locale: Locale = Locale.getDefault(), + maxFractionDigits: Int = 2, + minFractionDigits: Int = 2, + ): String { val formatter = NumberFormat.getPercentInstance(locale).apply { - maximumFractionDigits = 2 - minimumFractionDigits = 2 + maximumFractionDigits = maxFractionDigits + minimumFractionDigits = minFractionDigits roundingMode = RoundingMode.HALF_UP } val value = if (useAbsoluteValue) percent.abs() else percent diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/WalletPreviewData.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/WalletPreviewData.kt index 2e38ce1e26..0671449433 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/WalletPreviewData.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/WalletPreviewData.kt @@ -14,7 +14,6 @@ import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.res.TangemColorPalette import com.tangem.domain.wallets.models.UserWalletId import com.tangem.feature.wallet.presentation.common.state.TokenItemState -import com.tangem.feature.wallet.presentation.common.state.TokenItemState.TokenOptionsState import com.tangem.feature.wallet.presentation.organizetokens.model.DraggableItem import com.tangem.feature.wallet.presentation.organizetokens.model.OrganizeTokensListState import com.tangem.feature.wallet.presentation.organizetokens.model.OrganizeTokensState @@ -124,15 +123,12 @@ internal object WalletPreviewData { val tokenItemVisibleState by lazy { TokenItemState.Content( id = UUID.randomUUID().toString(), - icon = coinIconState, - name = "Polygon", - amount = "5,412 MATIC", - hasPending = true, - tokenOptions = TokenOptionsState( - fiatAmount = "321 $", - priceChangeState = PriceChangeState.Unknown, - isBalanceHidden = false, - ), + iconState = coinIconState, + titleState = TokenItemState.TitleState.Content(text = "Polygon", hasPending = true), + fiatAmountState = TokenItemState.FiatAmountState.Content(text = "321 $"), + cryptoAmountState = TokenItemState.CryptoAmountState.Content(text = "5,412 MATIC"), + priceChangeState = TokenItemState.PriceChangeState.Unknown, + isBalanceHidden = false, onItemClick = {}, onItemLongClick = {}, ) @@ -140,26 +136,23 @@ internal object WalletPreviewData { val testnetTokenItemVisibleState by lazy { tokenItemVisibleState.copy( - name = "Polygon testnet", - icon = tokenIconState.copy(isGrayscale = true), + titleState = TokenItemState.TitleState.Content(text = "Polygon testnet"), + iconState = tokenIconState.copy(isGrayscale = true), ) } val tokenItemHiddenState by lazy { TokenItemState.Content( id = UUID.randomUUID().toString(), - icon = tokenIconState, - name = "Polygon", - amount = "5,412 MATIC", - hasPending = false, - tokenOptions = TokenOptionsState( - priceChangeState = PriceChangeState.Content( - valueInPercent = "2%", - type = PriceChangeType.UP, - ), - fiatAmount = "321 $", - isBalanceHidden = false, + iconState = tokenIconState, + titleState = TokenItemState.TitleState.Content(text = "Polygon"), + fiatAmountState = TokenItemState.FiatAmountState.Content(text = "321 $"), + cryptoAmountState = TokenItemState.CryptoAmountState.Content(text = "5,412 MATIC"), + priceChangeState = TokenItemState.PriceChangeState.Content( + valueInPercent = "2.0%", + type = PriceChangeType.UP, ), + isBalanceHidden = false, onItemClick = {}, onItemLongClick = {}, ) @@ -168,17 +161,17 @@ internal object WalletPreviewData { val tokenItemDragState by lazy { TokenItemState.Draggable( id = UUID.randomUUID().toString(), - icon = tokenIconState, - name = "Polygon", - info = stringReference(value = "3 172,14 $"), + iconState = tokenIconState, + titleState = TokenItemState.TitleState.Content(text = "Polygon"), + cryptoAmountState = TokenItemState.CryptoAmountState.Content(text = "3 172,14 $"), ) } val tokenItemUnreachableState by lazy { TokenItemState.Unreachable( id = UUID.randomUUID().toString(), - icon = tokenIconState, - name = "Polygon", + iconState = tokenIconState, + titleState = TokenItemState.TitleState.Content(text = "Polygon"), onItemClick = {}, onItemLongClick = {}, ) @@ -187,16 +180,16 @@ internal object WalletPreviewData { val tokenItemNoAddressState by lazy { TokenItemState.NoAddress( id = UUID.randomUUID().toString(), - icon = tokenIconState, - name = "Polygon", + iconState = tokenIconState, + titleState = TokenItemState.TitleState.Content(text = "Polygon"), onItemLongClick = {}, ) } val customTokenItemVisibleState by lazy { tokenItemVisibleState.copy( - name = "Polygon custom", - icon = customTokenIconState.copy( + titleState = TokenItemState.TitleState.Content(text = "Polygon"), + iconState = customTokenIconState.copy( tint = TangemColorPalette.White, background = TangemColorPalette.Black, ), @@ -205,8 +198,8 @@ internal object WalletPreviewData { val customTestnetTokenItemVisibleState by lazy { tokenItemVisibleState.copy( - name = "Polygon custom testnet", - icon = customTokenIconState.copy(isGrayscale = true), + titleState = TokenItemState.TitleState.Content(text = "Polygon"), + iconState = customTokenIconState.copy(isGrayscale = true), ) } @@ -238,7 +231,9 @@ internal object WalletPreviewData { DraggableItem.Token( tokenItemState = tokenItemDragState.copy( id = "${group.id}_token_$tokenNumber", - name = "Token $tokenNumber from $networkNumber network", + titleState = TokenItemState.TitleState.Content( + text = "Token $tokenNumber from $networkNumber network", + ), ), groupId = group.id, roundingMode = when { @@ -348,37 +343,37 @@ internal object WalletPreviewData { TokensListItemState.Token( tokenItemVisibleState.copy( id = "token_1", - name = "Ethereum", - amount = "1,89340821 ETH", + titleState = TokenItemState.TitleState.Content(text = "Ethereum"), + cryptoAmountState = TokenItemState.CryptoAmountState.Content("1,89340821 ETH"), ), ), TokensListItemState.Token( tokenItemVisibleState.copy( id = "token_2", - name = "Ethereum", - amount = "1,89340821 ETH", + titleState = TokenItemState.TitleState.Content(text = "Ethereum"), + cryptoAmountState = TokenItemState.CryptoAmountState.Content("1,89340821 ETH"), ), ), TokensListItemState.Token( tokenItemVisibleState.copy( id = "token_3", - name = "Ethereum", - amount = "1,89340821 ETH", + titleState = TokenItemState.TitleState.Content(text = "Ethereum"), + cryptoAmountState = TokenItemState.CryptoAmountState.Content("1,89340821 ETH"), ), ), TokensListItemState.Token( tokenItemVisibleState.copy( id = "token_4", - name = "Ethereum", - amount = "1,89340821 ETH", + titleState = TokenItemState.TitleState.Content(text = "Ethereum"), + cryptoAmountState = TokenItemState.CryptoAmountState.Content("1,89340821 ETH"), ), ), TokensListItemState.NetworkGroupTitle(id = 1, stringReference("Ethereum")), TokensListItemState.Token( tokenItemVisibleState.copy( id = "token_5", - name = "Ethereum", - amount = "1,89340821 ETH", + titleState = TokenItemState.TitleState.Content(text = "Ethereum"), + cryptoAmountState = TokenItemState.CryptoAmountState.Content("1,89340821 ETH"), ), ), ), diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/TokenItem.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/TokenItem.kt index 7e0fd572df..4c1514a7f0 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/TokenItem.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/TokenItem.kt @@ -11,7 +11,6 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider import androidx.constraintlayout.compose.* -import com.tangem.core.ui.components.marketprice.PriceChangeState import com.tangem.core.ui.components.marketprice.PriceChangeType import com.tangem.core.ui.extensions.rememberHapticFeedback import com.tangem.core.ui.res.TangemTheme @@ -38,8 +37,10 @@ internal fun TokenItem( ) { val (iconRef, titleRef, cryptoAmountRef, fiatAmountRef, priceChangeRef, nonFiatContentRef) = createRefs() + val isBalanceHidden = (state as? TokenItemState.Content)?.isBalanceHidden ?: false + TokenIcon( - state = state, + state = state.iconState, modifier = Modifier.constrainAs(iconRef) { centerVerticallyTo(parent) start.linkTo(parent.start) @@ -52,7 +53,7 @@ internal fun TokenItem( } TokenTitle( - state = state, + state = state.titleState, modifier = Modifier .padding(horizontal = TangemTheme.dimens.spacing8) .constrainAs(titleRef) { @@ -76,7 +77,8 @@ internal fun TokenItem( ) TokenFiatAmount( - state = state, + state = state.fiatAmountState, + isBalanceHidden = isBalanceHidden, modifier = Modifier.constrainAs(fiatAmountRef) { top.linkTo(parent.top) end.linkTo(parent.end) @@ -91,7 +93,8 @@ internal fun TokenItem( val marginBetweenRows = TangemTheme.dimens.spacing2 TokenCryptoAmount( - state = state, + state = state.cryptoAmountState, + isBalanceHidden = isBalanceHidden, modifier = Modifier .padding(horizontal = TangemTheme.dimens.spacing8) .constrainAs(cryptoAmountRef) { @@ -117,16 +120,21 @@ internal fun TokenItem( derivedStateOf { with(density) { rootWidth.toDp().times(other = 0.16f) } } } TokenPriceChange( - state = state, + state = state.priceChangeState, modifier = Modifier.constrainAs(priceChangeRef) { top.linkTo(fiatAmountRef.bottom, marginBetweenRows) end.linkTo(anchor = parent.end) bottom.linkTo(parent.bottom) - if (state is TokenItemState.ContentState) { - start.linkTo(cryptoAmountRef.end) - width = Dimension.fillToConstraints - .atLeast(priceChangeRequiredMinWidth) + when (state.priceChangeState) { + is TokenItemState.PriceChangeState.Content, + is TokenItemState.PriceChangeState.Unknown, + -> { + start.linkTo(cryptoAmountRef.end) + width = Dimension.fillToConstraints + .atLeast(priceChangeRequiredMinWidth) + } + else -> Unit } }, ) @@ -202,22 +210,24 @@ private fun Preview_Tokens_DarkTheme(@PreviewParameter(TokenConfigProvider::clas private class TokenConfigProvider : CollectionPreviewParameterProvider( collection = listOf( - WalletPreviewData.tokenItemVisibleState.copy(amount = "5,41221467146712416241274127841274174213421 MATIC"), WalletPreviewData.tokenItemVisibleState.copy( - tokenOptions = WalletPreviewData.tokenItemVisibleState.tokenOptions.copy( - priceChangeState = PriceChangeState.Content( - valueInPercent = "31231231231231231231223123123123212312312312.00%", - type = PriceChangeType.UP, - ), + cryptoAmountState = TokenItemState.CryptoAmountState.Content( + text = "5,41221467146712416241274127841274174213421 MATIC", ), ), WalletPreviewData.tokenItemVisibleState.copy( - amount = "5,41221467146712416241274127841274174213421 MATIC", - tokenOptions = WalletPreviewData.tokenItemVisibleState.tokenOptions.copy( - priceChangeState = PriceChangeState.Content( - valueInPercent = "31231231231231231231223123123123212312312312.00%", - type = PriceChangeType.UP, - ), + priceChangeState = TokenItemState.PriceChangeState.Content( + valueInPercent = "31231231231231231231223123123123212312312312.0%", + type = PriceChangeType.UP, + ), + ), + WalletPreviewData.tokenItemVisibleState.copy( + cryptoAmountState = TokenItemState.CryptoAmountState.Content( + text = "5,41221467146712416241274127841274174213421 MATIC", + ), + priceChangeState = TokenItemState.PriceChangeState.Content( + valueInPercent = "31231231231231231231223123123123212312312312.0%", + type = PriceChangeType.UP, ), ), WalletPreviewData.tokenItemVisibleState, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/token/TokenCryptoAmount.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/token/TokenCryptoAmount.kt index 700dfdc58c..74d1cf836f 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/token/TokenCryptoAmount.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/token/TokenCryptoAmount.kt @@ -1,51 +1,51 @@ package com.tangem.feature.wallet.presentation.common.component.token -import androidx.compose.animation.AnimatedContent -import androidx.compose.animation.ExperimentalAnimationApi import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.composed +import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow import com.tangem.common.Strings import com.tangem.core.ui.components.RectangleShimmer -import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemTypography -import com.tangem.feature.wallet.presentation.common.state.TokenItemState +import com.tangem.feature.wallet.impl.R +import com.tangem.feature.wallet.presentation.common.state.TokenItemState.CryptoAmountState as TokenCryptoAmountState -@OptIn(ExperimentalAnimationApi::class) @Composable -internal fun TokenCryptoAmount(state: TokenItemState, modifier: Modifier = Modifier) { - AnimatedContent(targetState = state, label = "Update crypto amount", modifier = modifier) { animatedState -> - when (animatedState) { - is TokenItemState.Content -> { - CryptoAmountText( - amount = if (animatedState.tokenOptions.isBalanceHidden) Strings.STARS else animatedState.amount, - ) - } - is TokenItemState.Draggable -> { - CryptoAmountText(amount = animatedState.info.resolveReference()) - } - is TokenItemState.Loading -> { - RectangleShimmer(modifier = Modifier.placeholderSize(), radius = TangemTheme.dimens.radius4) - } - is TokenItemState.Locked -> { - LockedRectangle(modifier = Modifier.placeholderSize()) - } - is TokenItemState.Unreachable, - is TokenItemState.NoAddress, - -> Unit +internal fun TokenCryptoAmount( + state: TokenCryptoAmountState?, + isBalanceHidden: Boolean, + modifier: Modifier = Modifier, +) { + when (state) { + is TokenCryptoAmountState.Content -> { + CryptoAmountText( + amount = if (isBalanceHidden) Strings.STARS else state.text, + modifier = modifier, + ) } + is TokenCryptoAmountState.Unreachable -> { + CryptoAmountText(amount = stringResource(id = R.string.common_unreachable), modifier = modifier) + } + is TokenCryptoAmountState.Loading -> { + RectangleShimmer(modifier = modifier.placeholderSize(), radius = TangemTheme.dimens.radius4) + } + is TokenCryptoAmountState.Locked -> { + LockedRectangle(modifier = modifier.placeholderSize()) + } + null -> Unit } } @Composable -private fun CryptoAmountText(amount: String) { +private fun CryptoAmountText(amount: String, modifier: Modifier = Modifier) { Text( text = amount, + modifier = modifier, color = TangemTheme.colors.text.tertiary, maxLines = 1, overflow = TextOverflow.Ellipsis, @@ -55,6 +55,6 @@ private fun CryptoAmountText(amount: String) { private fun Modifier.placeholderSize(): Modifier = composed { return@composed this - .padding(vertical = TangemTheme.dimens.spacing4) + .padding(vertical = TangemTheme.dimens.spacing3) .size(width = TangemTheme.dimens.size52, height = TangemTheme.dimens.size12) } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/token/TokenFiatAmount.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/token/TokenFiatAmount.kt index f4ed36c1c8..6aff7c7e1c 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/token/TokenFiatAmount.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/token/TokenFiatAmount.kt @@ -1,7 +1,5 @@ package com.tangem.feature.wallet.presentation.common.component.token -import androidx.compose.animation.AnimatedContent -import androidx.compose.animation.ExperimentalAnimationApi import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.material3.Text @@ -13,40 +11,39 @@ import com.tangem.common.Strings import com.tangem.core.ui.components.RectangleShimmer import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemTypography -import com.tangem.feature.wallet.presentation.common.state.TokenItemState +import com.tangem.feature.wallet.presentation.common.state.TokenItemState.FiatAmountState as TokenFiatAmountState -@OptIn(ExperimentalAnimationApi::class) @Composable -internal fun TokenFiatAmount(state: TokenItemState, modifier: Modifier = Modifier) { - AnimatedContent(targetState = state, label = "Update fiat amount", modifier = modifier) { animatedState -> - when (animatedState) { - is TokenItemState.Content -> { - Text( - text = if (animatedState.tokenOptions.isBalanceHidden) { - Strings.STARS - } else { - animatedState.tokenOptions.fiatAmount - }, - color = TangemTheme.colors.text.primary1, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - style = TangemTypography.body2, - ) - } - is TokenItemState.Loading -> { - RectangleShimmer(modifier = Modifier.placeholderSize(), radius = TangemTheme.dimens.radius4) - } - is TokenItemState.Locked -> { - LockedRectangle(modifier = Modifier.placeholderSize()) - } - is TokenItemState.Unreachable, - is TokenItemState.Draggable, - is TokenItemState.NoAddress, - -> Unit +internal fun TokenFiatAmount(state: TokenFiatAmountState?, isBalanceHidden: Boolean, modifier: Modifier = Modifier) { + when (state) { + is TokenFiatAmountState.Content -> { + FiatAmountText( + text = if (isBalanceHidden) Strings.STARS else state.text, + modifier, + ) } + is TokenFiatAmountState.Loading -> { + RectangleShimmer(modifier = modifier.placeholderSize(), radius = TangemTheme.dimens.radius4) + } + is TokenFiatAmountState.Locked -> { + LockedRectangle(modifier = modifier.placeholderSize()) + } + null -> Unit } } +@Composable +private fun FiatAmountText(text: String, modifier: Modifier = Modifier) { + Text( + text = text, + modifier, + color = TangemTheme.colors.text.primary1, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + style = TangemTypography.body2, + ) +} + private fun Modifier.placeholderSize(): Modifier = composed { return@composed this .padding(vertical = TangemTheme.dimens.spacing4) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/token/TokenPriceChange.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/token/TokenPriceChange.kt index 1981bb5b17..002357512e 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/token/TokenPriceChange.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/token/TokenPriceChange.kt @@ -13,55 +13,49 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.style.TextOverflow import com.tangem.core.ui.components.RectangleShimmer import com.tangem.core.ui.components.SpacerW4 -import com.tangem.core.ui.components.marketprice.PriceChangeState import com.tangem.core.ui.components.marketprice.PriceChangeType import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemTypography import com.tangem.feature.wallet.impl.R import com.tangem.feature.wallet.presentation.common.state.TokenItemState +import com.tangem.feature.wallet.presentation.common.state.TokenItemState.PriceChangeState as TokenPriceChangeState -@OptIn(ExperimentalAnimationApi::class) @Composable -internal fun TokenPriceChange(state: TokenItemState, modifier: Modifier = Modifier) { - AnimatedContent(targetState = state, label = "Update the price change", modifier = modifier) { animatedState -> - when (animatedState) { - is TokenItemState.Content -> { - PriceChangeBlock(state = animatedState.tokenOptions.priceChangeState) - } - is TokenItemState.Loading -> { - RectangleShimmer(modifier = Modifier.placeholderSize(), radius = TangemTheme.dimens.radius4) - } - is TokenItemState.Locked -> { - LockedRectangle(modifier = Modifier.placeholderSize()) - } - is TokenItemState.Unreachable, - is TokenItemState.Draggable, - is TokenItemState.NoAddress, - -> Unit +internal fun TokenPriceChange(state: TokenPriceChangeState?, modifier: Modifier = Modifier) { + when (state) { + is TokenPriceChangeState.Content -> { + PriceChangeBlock(modifier = modifier, type = state.type, text = state.valueInPercent) } + is TokenPriceChangeState.Unknown -> { + PriceChangeBlock(modifier = modifier) + } + is TokenPriceChangeState.Loading -> { + RectangleShimmer(modifier = modifier.placeholderSize(), radius = TangemTheme.dimens.radius4) + } + is TokenPriceChangeState.Locked -> { + LockedRectangle(modifier = modifier.placeholderSize()) + } + null -> Unit } } @Composable -private fun PriceChangeBlock(state: PriceChangeState) { - Row(horizontalArrangement = Arrangement.End) { - PriceChangeIcon( - type = (state as? PriceChangeState.Content)?.type, - modifier = Modifier.align(Alignment.CenterVertically), - ) +private fun PriceChangeBlock(modifier: Modifier = Modifier, type: PriceChangeType? = null, text: String? = null) { + Row( + modifier = modifier, + horizontalArrangement = Arrangement.End, + verticalAlignment = Alignment.CenterVertically, + ) { + PriceChangeIcon(type = type) SpacerW4() - PriceChangeText(state = state, modifier = Modifier.align(Alignment.CenterVertically)) + PriceChangeText(type = type, text = text) } } @OptIn(ExperimentalAnimationApi::class) @Composable -private fun PriceChangeIcon(type: PriceChangeType?, modifier: Modifier = Modifier) { - AnimatedContent( - targetState = type, - label = "Update the price change's arrow", - modifier = modifier, - ) { animatedType -> +private fun PriceChangeIcon(type: PriceChangeType?) { + AnimatedContent(targetState = type, label = "Update the price change's arrow") { animatedType -> animatedType ?: return@AnimatedContent Icon( @@ -82,25 +76,14 @@ private fun PriceChangeIcon(type: PriceChangeType?, modifier: Modifier = Modifie @OptIn(ExperimentalAnimationApi::class) @Composable -private fun PriceChangeText(state: PriceChangeState, modifier: Modifier = Modifier) { - AnimatedContent( - targetState = state, - label = "Update the price change's text", - modifier = modifier, - ) { animatedState -> +private fun PriceChangeText(type: PriceChangeType?, text: String?) { + AnimatedContent(targetState = text, label = "Update the price change's text") { animatedText -> Text( - text = when (animatedState) { - is PriceChangeState.Content -> animatedState.valueInPercent - is PriceChangeState.Unknown -> TokenItemState.UNKNOWN_AMOUNT_SIGN - }, - color = when (animatedState) { - is PriceChangeState.Content -> { - when (animatedState.type) { - PriceChangeType.UP -> TangemTheme.colors.text.accent - PriceChangeType.DOWN -> TangemTheme.colors.text.warning - } - } - PriceChangeState.Unknown -> TangemTheme.colors.text.primary1 + text = animatedText ?: TokenItemState.UNKNOWN_AMOUNT_SIGN, + color = when (type) { + PriceChangeType.UP -> TangemTheme.colors.text.accent + PriceChangeType.DOWN -> TangemTheme.colors.text.warning + null -> TangemTheme.colors.text.primary1 }, overflow = TextOverflow.Ellipsis, maxLines = 1, @@ -111,6 +94,6 @@ private fun PriceChangeText(state: PriceChangeState, modifier: Modifier = Modifi private fun Modifier.placeholderSize(): Modifier = composed { return@composed this - .padding(vertical = TangemTheme.dimens.spacing4) + .padding(vertical = TangemTheme.dimens.spacing3) .size(width = TangemTheme.dimens.size40, height = TangemTheme.dimens.size12) } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/token/TokenTitle.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/token/TokenTitle.kt index 37106e707c..50cea114b0 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/token/TokenTitle.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/token/TokenTitle.kt @@ -1,12 +1,13 @@ package com.tangem.feature.wallet.presentation.common.component.token -import androidx.compose.animation.AnimatedContent import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.ExperimentalAnimationApi import androidx.compose.foundation.Image -import androidx.compose.foundation.layout.* +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.material3.Text -import androidx.compose.runtime.* +import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.composed @@ -16,32 +17,34 @@ import com.tangem.core.ui.components.RectangleShimmer import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemTypography import com.tangem.feature.wallet.impl.R -import com.tangem.feature.wallet.presentation.common.state.TokenItemState +import com.tangem.feature.wallet.presentation.common.state.TokenItemState.TitleState as TokenTitleState -@OptIn(ExperimentalAnimationApi::class) @Composable -internal fun TokenTitle(state: TokenItemState, modifier: Modifier = Modifier) { - AnimatedContent(targetState = state, label = "Update title", modifier = modifier) { animatedState -> - when (animatedState) { - is TokenItemState.ContentState -> { - ContentTitle( - name = animatedState.name, - hasPending = (animatedState as? TokenItemState.Content)?.hasPending == true, - ) - } - is TokenItemState.Loading -> { - RectangleShimmer(modifier = Modifier.placeholderSize(), radius = TangemTheme.dimens.radius4) - } - is TokenItemState.Locked -> { - LockedRectangle(modifier = Modifier.placeholderSize()) - } +internal fun TokenTitle(state: TokenTitleState?, modifier: Modifier = Modifier) { + when (state) { + is TokenTitleState.Content -> { + ContentTitle(name = state.text, hasPending = state.hasPending, modifier = modifier) } + is TokenTitleState.Loading -> { + RectangleShimmer(modifier = modifier.placeholderSize(), radius = TangemTheme.dimens.radius4) + } + is TokenTitleState.Locked -> { + LockedRectangle(modifier = modifier.placeholderSize()) + } + null -> Unit } } @Composable -private fun ContentTitle(name: String, hasPending: Boolean) { - Row(horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing6)) { +private fun ContentTitle(name: String, hasPending: Boolean, modifier: Modifier = Modifier) { + Row( + modifier = modifier, + horizontalArrangement = Arrangement.spacedBy(space = TangemTheme.dimens.spacing6), + ) { + /* + * If currency name has a long width, then it will completely displace the image. + * So we need to use [weight] to avoid displacement. + */ CurrencyNameText(name = name, modifier = Modifier.weight(weight = 1f, fill = false)) PendingTransactionImage( diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/token/icon/ContentIcon.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/token/icon/ContentIcon.kt index 49e3b3f183..9f29cbd72b 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/token/icon/ContentIcon.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/token/icon/ContentIcon.kt @@ -53,6 +53,9 @@ internal fun ContentIcon( background = icon.background, alpha = alpha, ) + TokenItemState.IconState.Loading, + TokenItemState.IconState.Locked, + -> Unit } } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/token/icon/TokenIcon.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/token/icon/TokenIcon.kt index be02ad2229..9a5cd1bee0 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/token/icon/TokenIcon.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/token/icon/TokenIcon.kt @@ -14,26 +14,28 @@ import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.graphics.ColorMatrix import com.tangem.core.ui.components.CircleShimmer import com.tangem.core.ui.res.TangemTheme -import com.tangem.feature.wallet.presentation.common.state.TokenItemState +import com.tangem.feature.wallet.presentation.common.state.TokenItemState.IconState as TokenIconState private const val GRAY_SCALE_SATURATION = 0f private const val GRAY_SCALE_ALPHA = 0.4f private const val NORMAL_ALPHA = 1f @Composable -internal fun TokenIcon(state: TokenItemState, modifier: Modifier = Modifier) { +internal fun TokenIcon(state: TokenIconState, modifier: Modifier = Modifier) { BaseContainer(modifier = modifier) { val iconModifier = Modifier .align(Alignment.Center) .size(TangemTheme.dimens.size36) when (state) { - is TokenItemState.Loading -> LoadingIcon(modifier = iconModifier) - is TokenItemState.Locked -> LockedIcon(modifier = iconModifier) - is TokenItemState.ContentState -> ContentIconContainer( - modifier = iconModifier, - icon = state.icon, - ) + is TokenIconState.Loading -> LoadingIcon(modifier = iconModifier) + is TokenIconState.Locked -> LockedIcon(modifier = iconModifier) + is TokenIconState.CoinIcon, + is TokenIconState.CustomTokenIcon, + is TokenIconState.TokenIcon, + -> { + ContentIconContainer(modifier = iconModifier, icon = state) + } } } } @@ -58,7 +60,7 @@ private fun LockedIcon(modifier: Modifier = Modifier) { } @Composable -private fun BoxScope.ContentIconContainer(icon: TokenItemState.IconState, modifier: Modifier = Modifier) { +private fun BoxScope.ContentIconContainer(icon: TokenIconState, modifier: Modifier = Modifier) { val networkBadgeOffset = TangemTheme.dimens.spacing4 val (alpha, colorFilter) = remember(icon.isGrayscale) { if (icon.isGrayscale) { diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/state/TokenItemState.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/state/TokenItemState.kt index ca960f320f..d61fbbbba4 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/state/TokenItemState.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/state/TokenItemState.kt @@ -3,99 +3,119 @@ package com.tangem.feature.wallet.presentation.common.state import androidx.annotation.DrawableRes import androidx.compose.runtime.Immutable import androidx.compose.ui.graphics.Color -import com.tangem.core.ui.components.marketprice.PriceChangeState -import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.components.marketprice.PriceChangeType /** Token item state */ @Immutable -internal sealed interface TokenItemState { +internal sealed class TokenItemState { - /** Unique id */ - val id: String + abstract val id: String + + abstract val iconState: IconState + + abstract val titleState: TitleState + + abstract val fiatAmountState: FiatAmountState? + + abstract val cryptoAmountState: CryptoAmountState? + + abstract val priceChangeState: PriceChangeState? /** Loading token state */ - data class Loading(override val id: String) : TokenItemState + data class Loading(override val id: String) : TokenItemState() { + override val iconState: IconState = IconState.Loading + override val titleState: TitleState = TitleState.Loading + override val fiatAmountState: FiatAmountState = FiatAmountState.Loading + override val cryptoAmountState: CryptoAmountState = CryptoAmountState.Loading + override val priceChangeState: PriceChangeState = PriceChangeState.Loading + } /** Locked token state */ - data class Locked(override val id: String) : TokenItemState - - /** Content state */ - @Immutable - sealed class ContentState : TokenItemState { - - abstract val icon: IconState - abstract val name: String + data class Locked(override val id: String) : TokenItemState() { + override val iconState: IconState = IconState.Locked + override val titleState: TitleState = TitleState.Locked + override val fiatAmountState: FiatAmountState = FiatAmountState.Locked + override val cryptoAmountState: CryptoAmountState = CryptoAmountState.Locked + override val priceChangeState: PriceChangeState = PriceChangeState.Locked } /** * Content token state * * @property id unique id - * @property icon token icon state - * @property name token name - * @property amount amount of token - * @property hasPending pending tx in blockchain - * @property tokenOptions state for token options + * @property iconState token icon state + * @property titleState token name * @property onItemClick callback which will be called when an item is clicked * @property onItemLongClick callback which will be called when an item is long clicked */ data class Content( override val id: String, - override val icon: IconState, - override val name: String, - val amount: String, - val hasPending: Boolean, - val tokenOptions: TokenOptionsState, + override val iconState: IconState, + override val titleState: TitleState, + override val fiatAmountState: FiatAmountState?, + override val cryptoAmountState: CryptoAmountState.Content, + override val priceChangeState: PriceChangeState?, + val isBalanceHidden: Boolean, val onItemClick: () -> Unit, val onItemLongClick: () -> Unit, - ) : ContentState() + ) : TokenItemState() /** * Draggable token state * * @property id unique id - * @property icon token icon state - * @property name token name - * @property info token info (e.g. fiat balance or status) + * @property iconState token icon state + * @property titleState token name */ data class Draggable( override val id: String, - override val icon: IconState, - override val name: String, - val info: TextReference, - ) : ContentState() + override val iconState: IconState, + override val titleState: TitleState, + override val cryptoAmountState: CryptoAmountState, + ) : TokenItemState() { + override val fiatAmountState: FiatAmountState? = null + override val priceChangeState: PriceChangeState? = null + } /** * Unreachable token state * * @property id token id - * @property icon token icon state - * @property name token name + * @property iconState token icon state + * @property titleState token name * @property onItemClick callback which will be called when an item is clicked * @property onItemLongClick callback which will be called when an item is long clicked */ data class Unreachable( override val id: String, - override val icon: IconState, - override val name: String, + override val iconState: IconState, + override val titleState: TitleState, val onItemClick: () -> Unit, val onItemLongClick: () -> Unit, - ) : ContentState() + ) : TokenItemState() { + override val fiatAmountState: FiatAmountState? = null + override val cryptoAmountState: CryptoAmountState? = null + override val priceChangeState: PriceChangeState? = null + } /** * No derivation address state * * @property id token id - * @property icon token icon state - * @property name token name + * @property iconState token icon state + * @property titleState token name * @property onItemLongClick callback which will be called when an item is long clicked */ data class NoAddress( override val id: String, - override val icon: IconState, - override val name: String, + override val iconState: IconState, + override val titleState: TitleState, val onItemLongClick: () -> Unit, - ) : ContentState() + ) : TokenItemState() { + override val fiatAmountState: FiatAmountState? = null + override val cryptoAmountState: CryptoAmountState? = null + override val priceChangeState: PriceChangeState? = null + } /** * Represents the various states an icon can be in. @@ -161,15 +181,60 @@ internal sealed interface TokenItemState { override val isCustom: Boolean = false } + + object Loading : IconState() { + override val isGrayscale: Boolean = false + override val isCustom: Boolean = false + override val networkBadgeIconResId: Int? = null + } + + object Locked : IconState() { + override val isGrayscale: Boolean = false + override val isCustom: Boolean = false + override val networkBadgeIconResId: Int? = null + } } - /** Token options state */ @Immutable - data class TokenOptionsState( - val priceChangeState: PriceChangeState, - val fiatAmount: String, - val isBalanceHidden: Boolean, - ) + sealed class TitleState { + + data class Content(val text: String, val hasPending: Boolean = false) : TitleState() + + object Loading : TitleState() + + object Locked : TitleState() + } + + @Immutable + sealed class FiatAmountState { + data class Content(val text: String) : FiatAmountState() + + object Loading : FiatAmountState() + + object Locked : FiatAmountState() + } + + @Immutable + sealed class CryptoAmountState { + data class Content(val text: String) : CryptoAmountState() + + object Unreachable : CryptoAmountState() + + object Loading : CryptoAmountState() + + object Locked : CryptoAmountState() + } + + sealed class PriceChangeState { + + data class Content(val valueInPercent: String, val type: PriceChangeType) : PriceChangeState() + + object Unknown : PriceChangeState() + + object Loading : PriceChangeState() + + object Locked : PriceChangeState() + } companion object { const val UNKNOWN_AMOUNT_SIGN = "—" diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/TokenItemHiddenStateConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/TokenItemHiddenStateConverter.kt index 91e8124c43..5a98aa841d 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/TokenItemHiddenStateConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/TokenItemHiddenStateConverter.kt @@ -1,21 +1,12 @@ package com.tangem.feature.wallet.presentation.organizetokens.utils.converter -import com.tangem.feature.wallet.presentation.common.state.TokenItemState - internal class TokenItemHiddenStateConverter { - fun updateHiddenState( - optionsState: TokenItemState.TokenOptionsState, - isBalanceHidden: Boolean, - ): TokenItemState.TokenOptionsState { + fun updateHiddenState(wasBalanceHidden: Boolean, isBalanceHidden: Boolean): Boolean { return when { - !optionsState.isBalanceHidden && isBalanceHidden -> { - optionsState.copy(isBalanceHidden = true) - } - optionsState.isBalanceHidden && !isBalanceHidden -> { - optionsState.copy(isBalanceHidden = false) - } - else -> optionsState + !wasBalanceHidden && isBalanceHidden -> true + wasBalanceHidden && !isBalanceHidden -> false + else -> wasBalanceHidden } } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/items/CryptoCurrencyToDraggableItemConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/items/CryptoCurrencyToDraggableItemConverter.kt index a6c36995b0..aa518deba2 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/items/CryptoCurrencyToDraggableItemConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/items/CryptoCurrencyToDraggableItemConverter.kt @@ -1,12 +1,9 @@ package com.tangem.feature.wallet.presentation.organizetokens.utils.converter.items import com.tangem.common.Provider -import com.tangem.core.ui.extensions.resourceReference -import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus -import com.tangem.feature.wallet.impl.R import com.tangem.feature.wallet.presentation.common.state.TokenItemState import com.tangem.feature.wallet.presentation.common.utils.CryptoCurrencyToIconStateConverter import com.tangem.feature.wallet.presentation.organizetokens.model.DraggableItem @@ -48,12 +45,12 @@ internal class CryptoCurrencyToDraggableItemConverter( return TokenItemState.Draggable( id = getTokenItemId(currency.id), - icon = iconStateConverter.convert(currencyStatus), - name = currency.name, - info = if (currencyStatus.value.isError) { - resourceReference(id = R.string.common_unreachable) + iconState = iconStateConverter.convert(currencyStatus), + titleState = TokenItemState.TitleState.Content(text = currency.name), + cryptoAmountState = if (currencyStatus.value.isError) { + TokenItemState.CryptoAmountState.Unreachable } else { - stringReference(getFormattedFiatAmount(currencyStatus, appCurrency)) + TokenItemState.CryptoAmountState.Content(text = getFormattedFiatAmount(currencyStatus, appCurrency)) }, ) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/CryptoCurrencyStatusToTokenItemConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/CryptoCurrencyStatusToTokenItemConverter.kt index f434ec4117..89ce3ee6b1 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/CryptoCurrencyStatusToTokenItemConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/CryptoCurrencyStatusToTokenItemConverter.kt @@ -1,7 +1,6 @@ package com.tangem.feature.wallet.presentation.wallet.utils import com.tangem.common.Provider -import com.tangem.core.ui.components.marketprice.PriceChangeState import com.tangem.core.ui.components.marketprice.PriceChangeType import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.domain.appcurrency.model.AppCurrency @@ -39,15 +38,17 @@ internal class CryptoCurrencyStatusToTokenItemConverter( private fun CryptoCurrencyStatus.mapToTokenItemState(): TokenItemState.Content { return TokenItemState.Content( id = currency.id.value, - name = currency.name, - icon = iconStateConverter.convert(value = this), - amount = getFormattedAmount(), - hasPending = value.hasCurrentNetworkTransactions, - tokenOptions = TokenItemState.TokenOptionsState( - fiatAmount = getFormattedFiatAmount(), - priceChangeState = getPriceChangeConfig(), - isBalanceHidden = isBalanceHiddenProvider(), + iconState = iconStateConverter.convert(value = this), + titleState = TokenItemState.TitleState.Content( + text = currency.name, + hasPending = value.hasCurrentNetworkTransactions, ), + fiatAmountState = TokenItemState.FiatAmountState.Content( + text = getFormattedFiatAmount(), + ), + cryptoAmountState = TokenItemState.CryptoAmountState.Content(text = getFormattedAmount()), + priceChangeState = getPriceChangeConfig(), + isBalanceHidden = isBalanceHiddenProvider(), onItemClick = { clickIntents.onTokenItemClick(currency) }, onItemLongClick = { clickIntents.onTokenItemLongClick(cryptoCurrencyStatus = this) }, ) @@ -68,29 +69,34 @@ internal class CryptoCurrencyStatusToTokenItemConverter( private fun CryptoCurrencyStatus.mapToUnreachableTokenItemState() = TokenItemState.Unreachable( id = currency.id.value, - name = currency.name, - icon = iconStateConverter.convert(value = this), + iconState = iconStateConverter.convert(value = this), + titleState = TokenItemState.TitleState.Content(text = currency.name), onItemClick = { clickIntents.onTokenItemClick(currency) }, onItemLongClick = { clickIntents.onTokenItemLongClick(cryptoCurrencyStatus = this) }, ) private fun CryptoCurrencyStatus.mapToNoAddressTokenItemState() = TokenItemState.NoAddress( id = currency.id.value, - name = currency.name, - icon = iconStateConverter.convert(this), + iconState = iconStateConverter.convert(this), + titleState = TokenItemState.TitleState.Content(text = currency.name), onItemLongClick = { clickIntents.onTokenItemLongClick(cryptoCurrencyStatus = this) }, ) - private fun CryptoCurrencyStatus.getPriceChangeConfig(): PriceChangeState { + private fun CryptoCurrencyStatus.getPriceChangeConfig(): TokenItemState.PriceChangeState { val priceChange = value.priceChange return if (priceChange != null) { - PriceChangeState.Content( - valueInPercent = BigDecimalFormatter.formatPercent(percent = priceChange, useAbsoluteValue = true), + TokenItemState.PriceChangeState.Content( + valueInPercent = BigDecimalFormatter.formatPercent( + percent = priceChange, + useAbsoluteValue = true, + maxFractionDigits = 1, + minFractionDigits = 1, + ), type = priceChange.getPriceChangeType(), ) } else { - PriceChangeState.Unknown + TokenItemState.PriceChangeState.Unknown } } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/HiddenStateConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/HiddenStateConverter.kt index a5f5ea9808..ae1528ddaf 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/HiddenStateConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/HiddenStateConverter.kt @@ -28,8 +28,8 @@ internal class HiddenStateConverter( if (tokenListItemState.state is TokenItemState.Content) { tokenListItemState.copy( state = tokenListItemState.state.copy( - tokenOptions = tokenItemHiddenStateConverter.updateHiddenState( - optionsState = tokenListItemState.state.tokenOptions, + isBalanceHidden = tokenItemHiddenStateConverter.updateHiddenState( + wasBalanceHidden = tokenListItemState.state.isBalanceHidden, isBalanceHidden = value, ), ), From 324f365771a854829bf4778e8cd0f61c842033a3 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 2 Oct 2023 13:21:24 +0500 Subject: [PATCH 117/242] Updated on 2026-08-14 --- .../middlewares/MultiWalletMiddleware.kt | 25 +-- .../domain/tokens/model/CryptoCurrency.kt | 3 +- .../com/tangem/domain/tokens/model/Network.kt | 25 +-- .../navigation/TokenDetailsArguments.kt | 33 ---- .../navigation/TokenDetailsRouter.kt | 2 +- .../tokendetails/TokenDetailsPreviewData.kt | 20 ++- .../tokendetails/state/TokenInfoBlockState.kt | 30 +++- .../factory/TokenDetailsIconStateConverter.kt | 47 ++++++ .../TokenDetailsSkeletonStateConverter.kt | 30 ++-- .../state/factory/TokenDetailsStateFactory.kt | 15 +- .../tokendetails/ui/components/TokenIcon.kt | 146 ++++++++++++++++++ .../ui/components/TokenInfoBlock.kt | 30 ++-- .../viewmodels/TokenDetailsViewModel.kt | 28 +--- .../router/DefaultWalletRouter.kt | 20 +-- 14 files changed, 310 insertions(+), 144 deletions(-) delete mode 100644 features/tokendetails/api/src/main/kotlin/com/tangem/features/tokendetails/navigation/TokenDetailsArguments.kt create mode 100644 features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsIconStateConverter.kt create mode 100644 features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenIcon.kt diff --git a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/MultiWalletMiddleware.kt b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/MultiWalletMiddleware.kt index 6a76d4ef38..eb198a07f9 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/MultiWalletMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/MultiWalletMiddleware.kt @@ -7,10 +7,7 @@ import com.tangem.common.flatMap import com.tangem.core.analytics.Analytics import com.tangem.core.navigation.AppScreen import com.tangem.core.navigation.NavigationAction -import com.tangem.core.ui.extensions.networkIconResId -import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.wallets.models.UserWallet -import com.tangem.features.tokendetails.navigation.TokenDetailsArguments import com.tangem.features.tokendetails.navigation.TokenDetailsRouter import com.tangem.tap.common.analytics.events.AnalyticsParam import com.tangem.tap.common.analytics.events.Token.ButtonRemoveToken @@ -20,7 +17,6 @@ import com.tangem.tap.common.extensions.dispatchErrorNotification import com.tangem.tap.common.extensions.dispatchWithMain import com.tangem.tap.common.redux.global.GlobalAction import com.tangem.tap.domain.TapError -import com.tangem.tap.features.wallet.models.Currency import com.tangem.tap.features.wallet.converters.CryptoCurrencyConverter import com.tangem.tap.features.wallet.redux.WalletAction import com.tangem.tap.features.wallet.redux.WalletState @@ -44,7 +40,7 @@ class MultiWalletMiddleware { is WalletAction.MultiWallet.SelectWallet -> { if (action.currency != null) { val bundle = bundleOf( - TokenDetailsRouter.TOKEN_DETAILS_ARGS to createTokenDetailsArgument(action.currency), + TokenDetailsRouter.CRYPTO_CURRENCY_KEY to cryptoCurrencyConverter.convert(action.currency), ) store.dispatch(NavigationAction.NavigateTo(screen = AppScreen.WalletDetails, bundle = bundle)) } @@ -133,23 +129,4 @@ class MultiWalletMiddleware { store.state.globalState.tapWalletManager.loadData(updatedUserWallet, refresh = true) } } - - private fun createTokenDetailsArgument(currency: Currency): TokenDetailsArguments { - val cryptoCurrency = cryptoCurrencyConverter.convert(currency) - return TokenDetailsArguments( - currencyId = cryptoCurrency.id, - currencyName = cryptoCurrency.name, - currencySymbol = cryptoCurrency.symbol, - iconUrl = cryptoCurrency.iconUrl, - coinType = when (cryptoCurrency) { - is CryptoCurrency.Coin -> TokenDetailsArguments.CoinType.Native - is CryptoCurrency.Token -> TokenDetailsArguments.CoinType.Token( - isCustom = cryptoCurrency.isCustom, - standardName = cryptoCurrency.network.standardType.name, - networkName = cryptoCurrency.network.name, - networkIcon = cryptoCurrency.networkIconResId, - ) - }, - ) - } } \ No newline at end of file diff --git a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/CryptoCurrency.kt b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/CryptoCurrency.kt index 161c9cb1cc..265f94094c 100644 --- a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/CryptoCurrency.kt +++ b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/CryptoCurrency.kt @@ -14,7 +14,8 @@ import kotlinx.parcelize.Parcelize * @property iconUrl Optional URL of the cryptocurrency icon. `null` if not found. * @property isCustom Indicates whether the currency is a custom user-added currency or not. */ -sealed class CryptoCurrency { +@Parcelize +sealed class CryptoCurrency : Parcelable { abstract val id: ID abstract val network: Network diff --git a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/Network.kt b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/Network.kt index 03bfac5ee6..2bc545ba77 100644 --- a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/Network.kt +++ b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/Network.kt @@ -1,5 +1,8 @@ package com.tangem.domain.tokens.model +import android.os.Parcelable +import kotlinx.parcelize.Parcelize + /** * Represents a blockchain network, identified by a unique ID, a human-readable name, and its standard type. * @@ -13,13 +16,14 @@ package com.tangem.domain.tokens.model * @property isTestnet Indicates whether the network is a test network or a main network. * @property standardType The type of blockchain standard the network adheres to. */ +@Parcelize data class Network( val id: ID, val name: String, val derivationPath: DerivationPath, val isTestnet: Boolean, val standardType: StandardType, -) { +) : Parcelable { init { require(name.isNotBlank()) { "Network name must not be blank" } @@ -31,7 +35,8 @@ data class Network( * @property value The string representation of the network ID. */ @JvmInline - value class ID(val value: String) { + @Parcelize + value class ID(val value: String) : Parcelable { init { require(value.isNotBlank()) { "Network ID must not be blank" } @@ -44,7 +49,8 @@ data class Network( * This class represents such paths in a generic manner, allowing for predefined card-based paths, * custom paths, or even no derivation path at all. */ - sealed class DerivationPath { + @Parcelize + sealed class DerivationPath : Parcelable { /** The actual derivation path value, if any. */ abstract val value: String? @@ -67,7 +73,7 @@ data class Network( * Represents a lack of derivation path. */ object None : DerivationPath() { - override val value: String? = null + override val value: String? get() = null } } @@ -80,27 +86,28 @@ data class Network( * * @property name The human-readable name of the standard type. */ - sealed class StandardType { + @Parcelize + sealed class StandardType : Parcelable { abstract val name: String /** Represents the ERC20 token standard, common on the Ethereum network. */ object ERC20 : StandardType() { - override val name: String = "ERC20" + override val name: String get() = "ERC20" } /** Represents the TRC20 token standard, common on the TRON network. */ object TRC20 : StandardType() { - override val name: String = "TRC20" + override val name: String get() = "TRC20" } /** Represents the BEP20 token standard, common on the Binance Smart Chain network. */ object BEP20 : StandardType() { - override val name: String = "BEP20" + override val name: String get() = "BEP20" } /** Represents the BEP2 token standard, common on the Binance Chain network. */ object BEP2 : StandardType() { - override val name: String = "BEP2" + override val name: String get() = "BEP2" } /** Represents a network that does not adhere to a predefined standard type. */ diff --git a/features/tokendetails/api/src/main/kotlin/com/tangem/features/tokendetails/navigation/TokenDetailsArguments.kt b/features/tokendetails/api/src/main/kotlin/com/tangem/features/tokendetails/navigation/TokenDetailsArguments.kt deleted file mode 100644 index 5238d98973..0000000000 --- a/features/tokendetails/api/src/main/kotlin/com/tangem/features/tokendetails/navigation/TokenDetailsArguments.kt +++ /dev/null @@ -1,33 +0,0 @@ -package com.tangem.features.tokendetails.navigation - -import android.os.Parcelable -import androidx.annotation.DrawableRes -import com.tangem.domain.tokens.model.CryptoCurrency -import kotlinx.parcelize.Parcelize - -@Parcelize -data class TokenDetailsArguments( - val currencyId: CryptoCurrency.ID, - val currencyName: String, - val currencySymbol: String, - val iconUrl: String?, - val coinType: CoinType, -) : Parcelable { - - @Parcelize - sealed class CoinType : Parcelable { - object Native : CoinType() - - /** - * @param isCustom - Indicates whether the currency is a custom user-added currency or not. - * @param standardName - token standard. Samples: ERC20, BEP20, BEP2, TRC20 and etc. - * @param networkName - token's blockchain name. Ethereum, Tron and etc. - */ - data class Token( - val isCustom: Boolean, - val standardName: String, - val networkName: String, - @DrawableRes val networkIcon: Int, - ) : CoinType() - } -} \ No newline at end of file diff --git a/features/tokendetails/api/src/main/kotlin/com/tangem/features/tokendetails/navigation/TokenDetailsRouter.kt b/features/tokendetails/api/src/main/kotlin/com/tangem/features/tokendetails/navigation/TokenDetailsRouter.kt index b0130e64cf..da5bc7571f 100644 --- a/features/tokendetails/api/src/main/kotlin/com/tangem/features/tokendetails/navigation/TokenDetailsRouter.kt +++ b/features/tokendetails/api/src/main/kotlin/com/tangem/features/tokendetails/navigation/TokenDetailsRouter.kt @@ -7,6 +7,6 @@ interface TokenDetailsRouter { fun getEntryFragment(): Fragment companion object { - const val TOKEN_DETAILS_ARGS = "token_details_args" + const val CRYPTO_CURRENCY_KEY = "token_details_crypto_currency" } } \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/TokenDetailsPreviewData.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/TokenDetailsPreviewData.kt index 664c3fd0e1..6db94b8e34 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/TokenDetailsPreviewData.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/TokenDetailsPreviewData.kt @@ -1,5 +1,6 @@ package com.tangem.feature.tokendetails.presentation.tokendetails +import androidx.compose.ui.graphics.Color import com.tangem.core.ui.components.marketprice.MarketPriceBlockState import com.tangem.core.ui.components.transactions.state.TxHistoryState import com.tangem.core.ui.extensions.TextReference @@ -32,12 +33,21 @@ internal object TokenDetailsPreviewData { val tokenInfoBlockStateWithLongNameInMainCurrency = TokenInfoBlockState( name = "Stellar (XLM) with long name test", - iconUrl = "https://s3.eu-central-1.amazonaws.com/tangem.api/coins/large/stellar.png", + iconState = TokenInfoBlockState.IconState.CoinIcon( + url = "https://s3.eu-central-1.amazonaws.com/tangem.api/coins/large/stellar.png", + fallbackResId = R.drawable.img_stellar_22, + isGrayscale = false, + ), currency = TokenInfoBlockState.Currency.Native, ) val tokenInfoBlockStateWithLongName = TokenInfoBlockState( name = "Tether (USDT) with long name test", - iconUrl = "https://s3.eu-central-1.amazonaws.com/tangem.api/coins/large/stellar.png", + iconState = TokenInfoBlockState.IconState.TokenIcon( + url = "https://s3.eu-central-1.amazonaws.com/tangem.api/coins/large/stellar.png", + fallbackTint = Color.Cyan, + fallbackBackground = Color.Blue, + isGrayscale = false, + ), currency = TokenInfoBlockState.Currency.Token( standardName = "ERC20", networkIcon = R.drawable.img_eth_22, @@ -47,7 +57,11 @@ internal object TokenDetailsPreviewData { val tokenInfoBlockState = TokenInfoBlockState( name = "Tether USDT", - iconUrl = "https://s3.eu-central-1.amazonaws.com/tangem.api/coins/large/tether.png", + iconState = TokenInfoBlockState.IconState.CustomTokenIcon( + tint = Color.Green, + background = Color.Magenta, + isGrayscale = true, + ), currency = TokenInfoBlockState.Currency.Token( standardName = "ERC20", networkIcon = R.drawable.img_eth_22, diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenInfoBlockState.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenInfoBlockState.kt index 10bdfe2821..7c1e1eda2b 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenInfoBlockState.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenInfoBlockState.kt @@ -1,12 +1,15 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.state import androidx.annotation.DrawableRes +import androidx.compose.runtime.Immutable +import androidx.compose.ui.graphics.Color internal data class TokenInfoBlockState( val name: String, - val iconUrl: String?, + val iconState: IconState, val currency: Currency, ) { + @Immutable sealed class Currency { object Native : Currency() @@ -21,4 +24,29 @@ internal data class TokenInfoBlockState( @DrawableRes val networkIcon: Int, ) : Currency() } + + @Immutable + sealed class IconState { + + abstract val isGrayscale: Boolean + + data class CoinIcon( + val url: String?, + @DrawableRes val fallbackResId: Int, + override val isGrayscale: Boolean, + ) : IconState() + + data class TokenIcon( + val url: String?, + val fallbackTint: Color, + val fallbackBackground: Color, + override val isGrayscale: Boolean, + ) : IconState() + + data class CustomTokenIcon( + val tint: Color, + val background: Color, + override val isGrayscale: Boolean, + ) : IconState() + } } \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsIconStateConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsIconStateConverter.kt new file mode 100644 index 0000000000..ebd12ff9b0 --- /dev/null +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsIconStateConverter.kt @@ -0,0 +1,47 @@ +package com.tangem.feature.tokendetails.presentation.tokendetails.state.factory + +import com.tangem.core.ui.extensions.getTintForTokenIcon +import com.tangem.core.ui.extensions.networkIconResId +import com.tangem.core.ui.extensions.tryGetBackgroundForTokenIcon +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenInfoBlockState +import com.tangem.utils.converter.Converter + +internal class TokenDetailsIconStateConverter : Converter { + + override fun convert(value: CryptoCurrency): TokenInfoBlockState.IconState { + return when (value) { + is CryptoCurrency.Coin -> getIconStateForCoin(value) + is CryptoCurrency.Token -> getIconStateForToken(value) + } + } + + private fun getIconStateForCoin(coin: CryptoCurrency.Coin): TokenInfoBlockState.IconState.CoinIcon { + return TokenInfoBlockState.IconState.CoinIcon( + url = coin.iconUrl, + fallbackResId = coin.networkIconResId, + isGrayscale = coin.network.isTestnet, + ) + } + + private fun getIconStateForToken(token: CryptoCurrency.Token): TokenInfoBlockState.IconState { + val isGrayscale = token.network.isTestnet + val background = token.tryGetBackgroundForTokenIcon(isGrayscale) + val tint = getTintForTokenIcon(background) + + return if (token.isCustom) { + TokenInfoBlockState.IconState.CustomTokenIcon( + tint = tint, + background = background, + isGrayscale = isGrayscale, + ) + } else { + TokenInfoBlockState.IconState.TokenIcon( + url = token.iconUrl, + isGrayscale = isGrayscale, + fallbackTint = tint, + fallbackBackground = background, + ) + } + } +} \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSkeletonStateConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSkeletonStateConverter.kt index e828c9a2be..6fa5307dde 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSkeletonStateConverter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSkeletonStateConverter.kt @@ -3,13 +3,14 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.state.factory import com.tangem.core.ui.components.marketprice.MarketPriceBlockState import com.tangem.core.ui.components.transactions.state.TxHistoryState import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.networkIconResId import com.tangem.core.ui.res.TangemTheme +import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.feature.tokendetails.presentation.tokendetails.state.* import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.TokenDetailsActionButton import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.TokenDetailsPullToRefreshConfig import com.tangem.feature.tokendetails.presentation.tokendetails.viewmodels.TokenDetailsClickIntents import com.tangem.features.tokendetails.impl.R -import com.tangem.features.tokendetails.navigation.TokenDetailsArguments import com.tangem.utils.converter.Converter import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf @@ -17,29 +18,30 @@ import kotlinx.coroutines.flow.MutableStateFlow internal class TokenDetailsSkeletonStateConverter( private val clickIntents: TokenDetailsClickIntents, -) : Converter { +) : Converter { - override fun convert(value: TokenDetailsArguments): TokenDetailsState { - val coinType = value.coinType + private val iconStateConverter by lazy { TokenDetailsIconStateConverter() } + + override fun convert(value: CryptoCurrency): TokenDetailsState { return TokenDetailsState( topAppBarConfig = TokenDetailsTopAppBarConfig( onBackClick = clickIntents::onBackClick, tokenDetailsAppBarMenuConfig = createMenu(), ), tokenInfoBlockState = TokenInfoBlockState( - name = value.currencyName, - iconUrl = value.iconUrl, - currency = when (coinType) { - TokenDetailsArguments.CoinType.Native -> TokenInfoBlockState.Currency.Native - is TokenDetailsArguments.CoinType.Token -> TokenInfoBlockState.Currency.Token( - standardName = coinType.networkName, - networkName = coinType.networkName, - networkIcon = coinType.networkIcon, + name = value.name, + iconState = iconStateConverter.convert(value), + currency = when (value) { + is CryptoCurrency.Coin -> TokenInfoBlockState.Currency.Native + is CryptoCurrency.Token -> TokenInfoBlockState.Currency.Token( + standardName = value.network.standardType.name, + networkName = value.network.name, + networkIcon = value.networkIconResId, ) }, ), tokenBalanceBlockState = TokenDetailsBalanceBlockState.Loading(actionButtons = createButtons()), - marketPriceBlockState = MarketPriceBlockState.Loading(value.currencySymbol), + marketPriceBlockState = MarketPriceBlockState.Loading(value.symbol), notifications = persistentListOf(), pendingTxs = persistentListOf(), txHistoryState = TxHistoryState.Content( @@ -51,7 +53,7 @@ internal class TokenDetailsSkeletonStateConverter( pullToRefreshConfig = createPullToRefresh(), bottomSheetConfig = null, isBalanceHidden = true, - isCustomToken = coinType is TokenDetailsArguments.CoinType.Token && coinType.isCustom, + isCustomToken = value.isCustom, ) } diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt index 52ef44ac9c..bf1c5a2ad0 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt @@ -22,15 +22,14 @@ import com.tangem.feature.tokendetails.presentation.tokendetails.state.component import com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.txhistory.TokenDetailsLoadedTxHistoryConverter import com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.txhistory.TokenDetailsLoadingTxHistoryConverter import com.tangem.feature.tokendetails.presentation.tokendetails.viewmodels.TokenDetailsClickIntents -import com.tangem.features.tokendetails.navigation.TokenDetailsArguments import kotlinx.coroutines.flow.Flow internal class TokenDetailsStateFactory( private val currentStateProvider: Provider, private val appCurrencyProvider: Provider, private val clickIntents: TokenDetailsClickIntents, - currencySymbolProvider: Provider, - currencyDecimalsProvider: Provider, + symbol: String, + decimals: Int, ) { private val skeletonStateConverter by lazy { @@ -45,8 +44,8 @@ internal class TokenDetailsStateFactory( TokenDetailsLoadedBalanceConverter( currentStateProvider = currentStateProvider, appCurrencyProvider = appCurrencyProvider, - symbol = currencySymbolProvider(), - decimals = currencyDecimalsProvider(), + symbol = symbol, + decimals = decimals, ) } @@ -65,8 +64,8 @@ internal class TokenDetailsStateFactory( TokenDetailsLoadedTxHistoryConverter( currentStateProvider = currentStateProvider, clickIntents = clickIntents, - symbol = currencySymbolProvider(), - decimals = currencyDecimalsProvider(), + symbol = symbol, + decimals = decimals, ) } @@ -76,7 +75,7 @@ internal class TokenDetailsStateFactory( ) } - fun getInitialState(screenArgument: TokenDetailsArguments): TokenDetailsState { + fun getInitialState(screenArgument: CryptoCurrency): TokenDetailsState { return skeletonStateConverter.convert(value = screenArgument) } diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenIcon.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenIcon.kt new file mode 100644 index 0000000000..a210927b5b --- /dev/null +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenIcon.kt @@ -0,0 +1,146 @@ +package com.tangem.feature.tokendetails.presentation.tokendetails.ui.components + +import androidx.annotation.DrawableRes +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.Icon +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import coil.compose.SubcomposeAsyncImage +import coil.request.ImageRequest +import com.tangem.core.ui.components.CircleShimmer +import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenInfoBlockState +import com.tangem.features.tokendetails.impl.R + +@Composable +internal fun CurrencyIcon( + icon: TokenInfoBlockState.IconState, + alpha: Float, + colorFilter: ColorFilter?, + modifier: Modifier = Modifier, +) { + when (icon) { + is TokenInfoBlockState.IconState.CoinIcon -> CoinIcon( + modifier = modifier, + url = icon.url, + fallbackResId = icon.fallbackResId, + alpha = alpha, + colorFilter = colorFilter, + ) + is TokenInfoBlockState.IconState.TokenIcon -> TokenIcon( + modifier = modifier, + url = icon.url, + alpha = alpha, + colorFilter = colorFilter, + errorIcon = { + CustomTokenIcon( + modifier = modifier, + tint = icon.fallbackTint, + background = icon.fallbackBackground, + alpha = alpha, + ) + }, + ) + is TokenInfoBlockState.IconState.CustomTokenIcon -> CustomTokenIcon( + modifier = modifier, + tint = icon.tint, + background = icon.background, + alpha = alpha, + ) + } +} + +@Composable +private fun CoinIcon( + url: String?, + @DrawableRes fallbackResId: Int, + alpha: Float, + colorFilter: ColorFilter?, + modifier: Modifier = Modifier, +) { + val iconData: Any = if (url.isNullOrBlank()) fallbackResId else url + + DefaultCurrencyIcon( + modifier = modifier, + iconData = iconData, + errorIcon = { + Image( + painter = painterResource(id = fallbackResId), + alpha = alpha, + colorFilter = colorFilter, + contentDescription = null, + ) + }, + alpha = alpha, + colorFilter = colorFilter, + ) +} + +@Composable +private fun TokenIcon( + url: String?, + alpha: Float, + colorFilter: ColorFilter?, + errorIcon: @Composable () -> Unit, + modifier: Modifier = Modifier, +) { + if (url == null) { + errorIcon() + } else { + DefaultCurrencyIcon( + modifier = modifier, + iconData = url, + errorIcon = errorIcon, + alpha = alpha, + colorFilter = colorFilter, + ) + } +} + +@Composable +private fun CustomTokenIcon(tint: Color, background: Color, alpha: Float, modifier: Modifier = Modifier) { + Box( + modifier = modifier + .background( + color = background.copy(alpha = alpha), + shape = CircleShape, + ), + contentAlignment = Alignment.Center, + ) { + Icon( + modifier = Modifier.matchParentSize(), + painter = painterResource(id = R.drawable.ic_custom_token_44), + tint = tint.copy(alpha = alpha), + contentDescription = null, + ) + } +} + +@Composable +private inline fun DefaultCurrencyIcon( + iconData: Any, + alpha: Float, + colorFilter: ColorFilter?, + crossinline errorIcon: @Composable () -> Unit, + modifier: Modifier = Modifier, +) { + SubcomposeAsyncImage( + modifier = modifier, + model = ImageRequest.Builder(context = LocalContext.current) + .data(iconData) + .crossfade(enable = true) + .build(), + loading = { CircleShimmer() }, + error = { errorIcon() }, + alpha = alpha, + colorFilter = colorFilter, + contentDescription = null, + ) +} \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenInfoBlock.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenInfoBlock.kt index a4dd5c2b9e..4d149f38c8 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenInfoBlock.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenInfoBlock.kt @@ -1,6 +1,5 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.ui.components -import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.layout.* import androidx.compose.material.Text @@ -10,19 +9,23 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.platform.LocalInspectionMode +import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.graphics.ColorMatrix import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider -import coil.compose.rememberAsyncImagePainter import com.tangem.core.ui.res.TangemTheme import com.tangem.feature.tokendetails.presentation.tokendetails.TokenDetailsPreviewData import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenInfoBlockState import com.tangem.features.tokendetails.impl.R +private const val GRAY_SCALE_SATURATION = 0f +private const val GRAY_SCALE_ALPHA = 0.4f +private const val NORMAL_ALPHA = 1f + @Composable internal fun TokenInfoBlock(state: TokenInfoBlockState, modifier: Modifier = Modifier) { Row(modifier = modifier.fillMaxWidth()) { @@ -37,16 +40,18 @@ internal fun TokenInfoBlock(state: TokenInfoBlockState, modifier: Modifier = Mod NetworkInfoText(state.currency) } - val tokenIconPainter = when (LocalInspectionMode.current) { - // show drawable res in preview - true -> painterResource(id = R.drawable.img_stellar_22) - false -> rememberAsyncImagePainter(model = state.iconUrl) + val (alpha, colorFilter) = remember(state.iconState.isGrayscale) { + if (state.iconState.isGrayscale) { + GRAY_SCALE_ALPHA to GrayscaleColorFilter + } else { + NORMAL_ALPHA to null + } } - - Image( + CurrencyIcon( modifier = Modifier.size(TangemTheme.dimens.size48), - painter = tokenIconPainter, - contentDescription = null, + icon = state.iconState, + alpha = alpha, + colorFilter = colorFilter, ) } } @@ -110,6 +115,9 @@ private fun extractNetwork(tokenCurrency: TokenInfoBlockState.Currency.Token): E private data class ExtractedTokenNetworkText(val normalText: String, val boldText: String) +private val GrayscaleColorFilter: ColorFilter + get() = ColorFilter.colorMatrix(ColorMatrix().apply { setToSaturation(GRAY_SCALE_SATURATION) }) + @Preview @Composable private fun Preview_TokenInfoBlock_LightTheme( diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt index a8f37d0bf3..2b0a8ff76e 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt @@ -30,7 +30,6 @@ import com.tangem.feature.tokendetails.presentation.router.InnerTokenDetailsRout import com.tangem.feature.tokendetails.presentation.tokendetails.analytics.TokenScreenEvent import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState import com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.TokenDetailsStateFactory -import com.tangem.features.tokendetails.navigation.TokenDetailsArguments import com.tangem.features.tokendetails.navigation.TokenDetailsRouter import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.JobHolder @@ -59,15 +58,14 @@ internal class TokenDetailsViewModel @Inject constructor( private val isBalanceHiddenUseCase: IsBalanceHiddenUseCase, private val listenToFlipsUseCase: ListenToFlipsUseCase, private val getCurrencyWarningsUseCase: GetCurrencyWarningsUseCase, - private val getCryptoCurrencyUseCase: GetCryptoCurrencyUseCase, private val walletManagersFacade: WalletManagersFacade, private val reduxStateHolder: ReduxStateHolder, private val analyticsEventsHandler: AnalyticsEventHandler, savedStateHandle: SavedStateHandle, ) : ViewModel(), DefaultLifecycleObserver, TokenDetailsClickIntents { - private val screenArgument: TokenDetailsArguments = savedStateHandle[TokenDetailsRouter.TOKEN_DETAILS_ARGS] - ?: error("This screen can't open without TokenDetailsArgument") + private val cryptoCurrency: CryptoCurrency = savedStateHandle[TokenDetailsRouter.CRYPTO_CURRENCY_KEY] + ?: error("This screen can't open without CryptoCurrency") var router by Delegates.notNull() @@ -75,7 +73,6 @@ internal class TokenDetailsViewModel @Inject constructor( private val refreshStateJobHolder = JobHolder() private var cryptoCurrencyStatus: CryptoCurrencyStatus? = null private var wallet by Delegates.notNull() - private var cryptoCurrency by Delegates.notNull() private val selectedAppCurrencyFlow: StateFlow = createSelectedAppCurrencyFlow() @@ -83,34 +80,25 @@ internal class TokenDetailsViewModel @Inject constructor( currentStateProvider = Provider { uiState }, appCurrencyProvider = Provider(selectedAppCurrencyFlow::value), clickIntents = this, - currencySymbolProvider = Provider { cryptoCurrency.symbol }, - currencyDecimalsProvider = Provider { cryptoCurrency.decimals }, + symbol = cryptoCurrency.symbol, + decimals = cryptoCurrency.decimals, ) - var uiState: TokenDetailsState by mutableStateOf(stateFactory.getInitialState(screenArgument)) + var uiState: TokenDetailsState by mutableStateOf(stateFactory.getInitialState(cryptoCurrency)) private set override fun onCreate(owner: LifecycleOwner) { - initRequiredFields() + getWallet() + updateContent(selectedWallet = wallet) handleBalanceHiding(owner) } - private fun initRequiredFields() { + private fun getWallet() { getSelectedWalletUseCase() .fold( ifLeft = { error("Can not get selected wallet $it") }, ifRight = { wallet = it }, ) - viewModelScope.launch { - getCryptoCurrencyUseCase(userWalletId = wallet.walletId, id = screenArgument.currencyId) - .fold( - ifLeft = { error("Can not get cryptoCurrency with given ID: screenArgument.currencyId. $it") }, - ifRight = { - cryptoCurrency = it - updateContent(selectedWallet = wallet) - }, - ) - } } private fun updateContent(selectedWallet: UserWallet) { diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/DefaultWalletRouter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/DefaultWalletRouter.kt index 2c80ed2af1..2c7205d680 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/DefaultWalletRouter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/DefaultWalletRouter.kt @@ -19,7 +19,6 @@ import androidx.navigation.navArgument import com.tangem.core.navigation.AppScreen import com.tangem.core.navigation.NavigationAction import com.tangem.core.navigation.ReduxNavController -import com.tangem.core.ui.extensions.networkIconResId import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.wallets.models.UserWalletId import com.tangem.feature.wallet.presentation.WalletFragment @@ -27,7 +26,6 @@ import com.tangem.feature.wallet.presentation.organizetokens.OrganizeTokensScree import com.tangem.feature.wallet.presentation.organizetokens.OrganizeTokensViewModel import com.tangem.feature.wallet.presentation.wallet.ui.WalletScreen import com.tangem.feature.wallet.presentation.wallet.viewmodels.WalletViewModel -import com.tangem.features.tokendetails.navigation.TokenDetailsArguments import com.tangem.features.tokendetails.navigation.TokenDetailsRouter import kotlin.properties.Delegates @@ -113,23 +111,7 @@ internal class DefaultWalletRouter(private val reduxNavController: ReduxNavContr reduxNavController.navigate( action = NavigationAction.NavigateTo( screen = AppScreen.WalletDetails, - bundle = bundleOf( - TokenDetailsRouter.TOKEN_DETAILS_ARGS to TokenDetailsArguments( - currencyId = currency.id, - currencyName = currency.name, - currencySymbol = currency.symbol, - iconUrl = currency.iconUrl, - coinType = when (currency) { - is CryptoCurrency.Coin -> TokenDetailsArguments.CoinType.Native - is CryptoCurrency.Token -> TokenDetailsArguments.CoinType.Token( - isCustom = currency.isCustom, - standardName = currency.network.standardType.name, - networkName = currency.network.name, - networkIcon = currency.networkIconResId, - ) - }, - ), - ), + bundle = bundleOf(TokenDetailsRouter.CRYPTO_CURRENCY_KEY to currency), ), ) } From b91efa673064e536013ae68770c76474753c7d15 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 29 Sep 2023 18:17:28 +0300 Subject: [PATCH 118/242] Updated on 2026-08-14 --- .../repository/DefaultNetworksRepository.kt | 3 ++- .../tokens/GetCurrencyStatusUpdatesUseCase.kt | 6 ++++-- .../CurrenciesStatusesOperations.kt | 21 +++++++++++++------ .../tokens/repository/NetworksRepository.kt | 7 ++++++- .../repository/MockNetworksRepository.kt | 1 + .../components/TokenDetailsNotification.kt | 2 -- .../TokenDetailsNotificationConverter.kt | 8 ------- .../state/factory/TokenDetailsStateFactory.kt | 5 ----- .../viewmodels/TokenDetailsClickIntents.kt | 2 -- .../viewmodels/TokenDetailsViewModel.kt | 5 +---- 10 files changed, 29 insertions(+), 31 deletions(-) diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultNetworksRepository.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultNetworksRepository.kt index b8c62585a9..6f5615c1b6 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultNetworksRepository.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultNetworksRepository.kt @@ -40,6 +40,7 @@ internal class DefaultNetworksRepository( override fun getNetworkStatusesUpdates( userWalletId: UserWalletId, networks: Set, + refresh: Boolean, ): Flow> = channelFlow { launch(dispatchers.io) { networksStatusesStore.get(userWalletId) @@ -47,7 +48,7 @@ internal class DefaultNetworksRepository( } withContext(dispatchers.io) { - fetchNetworksStatusesIfCacheExpired(userWalletId, networks, refresh = false) + fetchNetworksStatusesIfCacheExpired(userWalletId, networks, refresh) } }.cancellable() diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCurrencyStatusUpdatesUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCurrencyStatusUpdatesUseCase.kt index d018b74e85..f3cf282b1d 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCurrencyStatusUpdatesUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCurrencyStatusUpdatesUseCase.kt @@ -37,15 +37,17 @@ class GetCurrencyStatusUpdatesUseCase( operator fun invoke( userWalletId: UserWalletId, currencyId: CryptoCurrency.ID, + refresh: Boolean, ): Flow> { return flow { - emitAll(getCurrency(userWalletId, currencyId)) + emitAll(getCurrency(userWalletId, currencyId, refresh)) }.flowOn(dispatchers.io) } private suspend fun getCurrency( userWalletId: UserWalletId, currencyId: CryptoCurrency.ID, + refresh: Boolean, ): Flow> { val operations = CurrenciesStatusesOperations( currenciesRepository = currenciesRepository, @@ -54,7 +56,7 @@ class GetCurrencyStatusUpdatesUseCase( userWalletId = userWalletId, ) - return operations.getCurrencyStatusFlow(currencyId).map { maybeCurrency -> + return operations.getCurrencyStatusFlow(currencyId, refresh).map { maybeCurrency -> maybeCurrency.mapLeft(CurrenciesStatusesOperations.Error::mapToCurrencyError) } } diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesOperations.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesOperations.kt index fc4ec95f89..3a2327b7cd 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesOperations.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesOperations.kt @@ -67,13 +67,16 @@ internal class CurrenciesStatusesOperations( } } - suspend fun getCurrencyStatusFlow(currencyId: CryptoCurrency.ID): Flow> { + suspend fun getCurrencyStatusFlow( + currencyId: CryptoCurrency.ID, + refresh: Boolean = false, + ): Flow> { val currency = recover( block = { getMultiCurrencyWalletCurrency(currencyId) }, recover = { return flowOf(it.left()) }, ) - return getCurrencyStatusFlow(currency) + return getCurrencyStatusFlow(currency, refresh) } suspend fun getNetworkCoinFlow(networkId: Network.ID): Flow> { @@ -94,7 +97,10 @@ internal class CurrenciesStatusesOperations( return getCurrencyStatusFlow(currency) } - private fun getCurrencyStatusFlow(currency: CryptoCurrency): Flow> { + private fun getCurrencyStatusFlow( + currency: CryptoCurrency, + refresh: Boolean = false, + ): Flow> { val (networks, currenciesIds) = getIds(nonEmptyListOf(currency)) val quoteFlow = getQuotes(currenciesIds) @@ -105,7 +111,7 @@ internal class CurrenciesStatusesOperations( } } - val statusFlow = getNetworksStatuses(networks) + val statusFlow = getNetworksStatuses(networks, refresh) .map { maybeStatuses -> maybeStatuses.flatMap { statuses -> statuses.singleOrNull { it.network == currency.network }?.right() @@ -204,8 +210,11 @@ internal class CurrenciesStatusesOperations( .onEmpty { emit(Error.EmptyQuotes.left()) } } - private fun getNetworksStatuses(networks: NonEmptySet): Flow>> { - return networksRepository.getNetworkStatusesUpdates(userWalletId, networks) + private fun getNetworksStatuses( + networks: NonEmptySet, + refresh: Boolean = false, + ): Flow>> { + return networksRepository.getNetworkStatusesUpdates(userWalletId, networks, refresh) .map, Either>> { it.right() } .catch { emit(Error.DataError(it).left()) } .onEmpty { emit(Error.EmptyNetworksStatuses.left()) } diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/NetworksRepository.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/NetworksRepository.kt index 30baab37f3..da67708111 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/NetworksRepository.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/NetworksRepository.kt @@ -16,9 +16,14 @@ interface NetworksRepository { * * @param userWalletId The unique identifier of the user wallet. * @param networks A set of network which statuses are to be retrieved. + * @param refresh A boolean flag indicating whether the data should be refreshed from remote. * @return A [Flow] emitting a set of [NetworkStatus] objects corresponding to the specified networks. */ - fun getNetworkStatusesUpdates(userWalletId: UserWalletId, networks: Set): Flow> + fun getNetworkStatusesUpdates( + userWalletId: UserWalletId, + networks: Set, + refresh: Boolean = false, + ): Flow> /** * Retrieves network statuses of specified blockchain networks for a specific user wallet. diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockNetworksRepository.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockNetworksRepository.kt index ffefd750e0..363ad6202a 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockNetworksRepository.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockNetworksRepository.kt @@ -17,6 +17,7 @@ internal class MockNetworksRepository( override fun getNetworkStatusesUpdates( userWalletId: UserWalletId, networks: Set, + refresh: Boolean, ): Flow> { return statuses.map { it.getOrElse { e -> throw e } } } diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/components/TokenDetailsNotification.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/components/TokenDetailsNotification.kt index 070bc8d6dd..5d7693bd08 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/components/TokenDetailsNotification.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/components/TokenDetailsNotification.kt @@ -33,7 +33,6 @@ sealed class TokenDetailsNotification( data class ExistentialDeposit( private val existentialInfo: CryptoCurrencyWarning.ExistentialDeposit, - private val onCloseClick: () -> Unit, ) : TokenDetailsNotification( config = NotificationConfig( title = TextReference.Str("Existential Deposit"), @@ -42,7 +41,6 @@ sealed class TokenDetailsNotification( formatArgs = wrappedList(existentialInfo.currencyName, existentialInfo.edStringValueWithSymbol), ), iconResId = R.drawable.img_attention_20, - onCloseClick = onCloseClick, ), ) diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsNotificationConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsNotificationConverter.kt index a379b3788f..25f691722d 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsNotificationConverter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsNotificationConverter.kt @@ -6,7 +6,6 @@ import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDeta import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.TokenDetailsNotification import com.tangem.feature.tokendetails.presentation.tokendetails.viewmodels.TokenDetailsClickIntents import com.tangem.utils.converter.Converter -import com.tangem.utils.extensions.removeBy import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList @@ -18,12 +17,6 @@ internal class TokenDetailsNotificationConverter( return value.map(::mapToNotification).toImmutableList() } - fun removeExistentialDeposit(currentState: TokenDetailsState): ImmutableList { - val newNotifications = currentState.notifications.toMutableList() - newNotifications.removeBy { it is TokenDetailsNotification.ExistentialDeposit } - return newNotifications.toImmutableList() - } - fun getStateRentInfoVisibility( currentState: TokenDetailsState, isVisible: Boolean, @@ -44,7 +37,6 @@ internal class TokenDetailsNotificationConverter( ) is CryptoCurrencyWarning.ExistentialDeposit -> TokenDetailsNotification.ExistentialDeposit( existentialInfo = warning, - onCloseClick = clickIntents::onCloseExistentialDepositNotification, ) is CryptoCurrencyWarning.Rent -> TokenDetailsNotification.RentInfo( rentInfo = warning, diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt index bf1c5a2ad0..40f0b4da65 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt @@ -210,11 +210,6 @@ internal class TokenDetailsStateFactory( return state.copy(notifications = notificationConverter.convert(warnings)) } - fun getStateWithRemovedExistentialNotification(): TokenDetailsState { - val state = currentStateProvider() - return state.copy(notifications = notificationConverter.removeExistentialDeposit(state)) - } - fun getStateWithRemovedRentNotification(): TokenDetailsState { val state = currentStateProvider() return state.copy(notifications = notificationConverter.getStateRentInfoVisibility(state, false)) diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsClickIntents.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsClickIntents.kt index 9881457874..1adc6b021d 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsClickIntents.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsClickIntents.kt @@ -29,6 +29,4 @@ interface TokenDetailsClickIntents { fun onDismissBottomSheet() fun onCloseRentInfoNotification() - - fun onCloseExistentialDepositNotification() } \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt index 2b0a8ff76e..f37726978c 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt @@ -148,6 +148,7 @@ internal class TokenDetailsViewModel @Inject constructor( getCurrencyStatusUpdatesUseCase( userWalletId = selectedWallet.walletId, currencyId = cryptoCurrency.id, + refresh = true, ) .distinctUntilChanged() .onEach { either -> @@ -373,10 +374,6 @@ internal class TokenDetailsViewModel @Inject constructor( uiState = stateFactory.getStateWithClosedBottomSheet() } - override fun onCloseExistentialDepositNotification() { - uiState = stateFactory.getStateWithRemovedExistentialNotification() - } - override fun onCloseRentInfoNotification() { uiState = stateFactory.getStateWithRemovedRentNotification() } From d36d3f542757283a3e0580103af2528c907644e1 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 2 Oct 2023 12:38:55 +0300 Subject: [PATCH 119/242] Updated on 2026-08-14 --- .../tangem/tap/di/domain/CardDomainModule.kt | 8 +++ .../redux/OnboardingWalletMiddleware.kt | 17 ++++++ .../wallets/usecase/IsNeedToBackupUseCase.kt | 27 +++++++++ .../presentation/common/WalletPreviewData.kt | 2 + .../state/components/WalletCardState.kt | 4 ++ ...letSingleCurrencyLoadedBalanceConverter.kt | 2 + .../state/factory/WalletStateFactory.kt | 9 +++ .../factory/WalletUpdateCardCountConverter.kt | 56 +++++++++++++++++++ .../utils/FiatBalanceToWalletCardConverter.kt | 3 + .../WalletHiddenBalanceStateConverter.kt | 2 + .../WalletNotificationsListFactory.kt | 19 +++++-- .../wallet/viewmodels/WalletViewModel.kt | 6 ++ .../viewmodels/WalletsUpdateActionResolver.kt | 36 ++++++++---- 13 files changed, 173 insertions(+), 18 deletions(-) create mode 100644 domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/IsNeedToBackupUseCase.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletUpdateCardCountConverter.kt diff --git a/app/src/main/java/com/tangem/tap/di/domain/CardDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/CardDomainModule.kt index 4f41cea873..918b41ff04 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/CardDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/CardDomainModule.kt @@ -5,6 +5,8 @@ import com.tangem.domain.card.repository.CardRepository import com.tangem.domain.card.repository.CardSdkConfigRepository import com.tangem.domain.demo.DemoConfig import com.tangem.domain.demo.IsDemoCardUseCase +import com.tangem.domain.wallets.legacy.WalletsStateHolder +import com.tangem.domain.wallets.usecase.IsNeedToBackupUseCase import com.tangem.tap.domain.TangemSdkManager import com.tangem.tap.domain.card.DefaultDerivePublicKeysUseCase import dagger.Module @@ -62,4 +64,10 @@ internal object CardDomainModule { fun provideDerivePublicKeysUseCase(tangemSdkManager: TangemSdkManager): DerivePublicKeysUseCase { return DefaultDerivePublicKeysUseCase(tangemSdkManager = tangemSdkManager) } + + @Provides + @ViewModelScoped + fun provideIsNeedToBackupUseCase(walletStateHolder: WalletsStateHolder): IsNeedToBackupUseCase { + return IsNeedToBackupUseCase(walletStateHolder) + } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletMiddleware.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletMiddleware.kt index 2ab9ded42f..e686d2f730 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletMiddleware.kt @@ -450,6 +450,23 @@ private fun handleBackupAction(appState: () -> AppState?, action: BackupAction) Analytics.send(Onboarding.Backup.Finished(backupState.backupCardsNumber)) } + userWalletsListManager.selectedUserWalletSync?.walletId?.let { + scope.launch { + userWalletsListManager.update( + userWalletId = it, + update = { wallet -> + wallet.copy( + scanResponse = updateScanResponseAfterBackup( + scanResponse = wallet.scanResponse, + backupState = backupState, + ), + ) + }, + ) + store.dispatchOnMain(GlobalAction.UpdateUserWalletsListManager(userWalletsListManager)) + } + } + val notActivatedCardIds = gatherCardIds(backupState, card) .mapNotNull { if (cardActivationIsFinished(it)) null else it } diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/IsNeedToBackupUseCase.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/IsNeedToBackupUseCase.kt new file mode 100644 index 0000000000..24fdc5a232 --- /dev/null +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/IsNeedToBackupUseCase.kt @@ -0,0 +1,27 @@ +package com.tangem.domain.wallets.usecase + +import com.tangem.domain.models.scan.CardDTO +import com.tangem.domain.wallets.legacy.WalletsStateHolder +import com.tangem.domain.wallets.models.UserWalletId +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map + +/** + * Use case that checks if wallet need backup cards + */ +class IsNeedToBackupUseCase(private val walletsStateHolder: WalletsStateHolder) { + + operator fun invoke(id: UserWalletId): Flow { + val userWalletsListManager = requireNotNull(walletsStateHolder.userWalletsListManager) + + return userWalletsListManager.userWallets + .map { wallets -> + val wallet = wallets.firstOrNull { it.walletId == id } + if (wallet == null) { + false + } else { + wallet.scanResponse.card.backupStatus is CardDTO.BackupStatus.NoBackup + } + } + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/WalletPreviewData.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/WalletPreviewData.kt index 0671449433..59e36af357 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/WalletPreviewData.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/WalletPreviewData.kt @@ -43,6 +43,7 @@ internal object WalletPreviewData { imageResId = R.drawable.ill_businessman_3d, onRenameClick = { _, _ -> }, onDeleteClick = {}, + cardCount = 1, ) } @@ -65,6 +66,7 @@ internal object WalletPreviewData { onDeleteClick = {}, balance = "8923,05 $", additionalInfo = TextReference.Str("3 cards • Seed phrase"), + cardCount = 1, ) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/components/WalletCardState.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/components/WalletCardState.kt index c9830c2a7b..c968de0013 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/components/WalletCardState.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/components/WalletCardState.kt @@ -34,6 +34,7 @@ internal sealed interface WalletCardState { * @property onRenameClick lambda be invoked when Rename button is clicked * @property onDeleteClick lambda be invoked when Delete button is clicked * @property additionalInfo wallet additional info + * @property cardCount number of cards in the wallet * @property balance wallet balance */ data class Content( @@ -43,6 +44,7 @@ internal sealed interface WalletCardState { override val onRenameClick: (UserWalletId, String) -> Unit, override val onDeleteClick: (UserWalletId) -> Unit, val additionalInfo: TextReference, + val cardCount: Int?, val balance: String, ) : WalletCardState @@ -55,6 +57,7 @@ internal sealed interface WalletCardState { * @property onRenameClick lambda be invoked when Rename button is clicked * @property onDeleteClick lambda be invoked when Delete button is clicked * @property additionalInfo wallet additional info + * @property cardCount number of cards in the wallet * @property balance wallet balance */ data class HiddenContent( @@ -65,6 +68,7 @@ internal sealed interface WalletCardState { override val onDeleteClick: (UserWalletId) -> Unit, val additionalInfo: TextReference, val balance: String, + val cardCount: Int?, ) : WalletCardState /** diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletSingleCurrencyLoadedBalanceConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletSingleCurrencyLoadedBalanceConverter.kt index ff8df226a1..ccfbe015bc 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletSingleCurrencyLoadedBalanceConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletSingleCurrencyLoadedBalanceConverter.kt @@ -11,6 +11,7 @@ import com.tangem.domain.tokens.error.CurrencyStatusError import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.wallets.models.UserWallet import com.tangem.feature.wallet.presentation.wallet.domain.WalletAdditionalInfoFactory +import com.tangem.feature.wallet.presentation.wallet.domain.getCardsCount import com.tangem.feature.wallet.presentation.wallet.state.WalletMultiCurrencyState import com.tangem.feature.wallet.presentation.wallet.state.WalletSingleCurrencyState import com.tangem.feature.wallet.presentation.wallet.state.WalletState @@ -96,6 +97,7 @@ internal class WalletSingleCurrencyLoadedBalanceConverter( onRenameClick = selectedWallet.onRenameClick, onDeleteClick = selectedWallet.onDeleteClick, balance = formatFiatAmount(status = status, appCurrency = appCurrencyProvider()), + cardCount = currentWalletProvider().getCardsCount(), ) } is CryptoCurrencyStatus.Loading -> { diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletStateFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletStateFactory.kt index 92783093a4..a46a50cc2c 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletStateFactory.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletStateFactory.kt @@ -61,6 +61,13 @@ internal class WalletStateFactory( private val hiddenStateConverter by lazy { HiddenStateConverter(currentStateProvider) } + private val walletUpdateCardCountConverter by lazy { + WalletUpdateCardCountConverter( + currentStateProvider, + currentWalletProvider, + ) + } + private val tokenListErrorConverter by lazy { TokenListErrorConverter(currentStateProvider) } @@ -134,6 +141,8 @@ internal class WalletStateFactory( fun getStateWithUpdatedWalletName(name: String): WalletState = walletRenameStateConverter.convert(value = name) + fun getStateWithUpdatedWalletCardCount(): WalletState = walletUpdateCardCountConverter.convert(Unit) + fun getUnlockedState(action: WalletsUpdateActionResolver.Action.UnlockWallet): WalletState { return walletsUnlockStateConverter.convert(value = action) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletUpdateCardCountConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletUpdateCardCountConverter.kt new file mode 100644 index 0000000000..805f3168b4 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletUpdateCardCountConverter.kt @@ -0,0 +1,56 @@ +package com.tangem.feature.wallet.presentation.wallet.state.factory + +import com.tangem.common.Provider +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.feature.wallet.presentation.wallet.domain.WalletAdditionalInfoFactory +import com.tangem.feature.wallet.presentation.wallet.domain.getCardsCount +import com.tangem.feature.wallet.presentation.wallet.state.WalletState +import com.tangem.feature.wallet.presentation.wallet.state.components.WalletCardState +import com.tangem.feature.wallet.presentation.wallet.state.components.WalletsListConfig +import com.tangem.utils.converter.Converter +import kotlinx.collections.immutable.toImmutableList + +internal class WalletUpdateCardCountConverter( + private val currentStateProvider: Provider, + private val currentWalletProvider: Provider, +) : Converter { + + override fun convert(value: Unit): WalletState { + return when (val state = currentStateProvider()) { + is WalletState.ContentState -> { + state.copySealed( + walletsListConfig = state.walletsListConfig.refreshCardCount(), + ) + } + is WalletState.Initial -> state + } + } + + private fun WalletsListConfig.refreshCardCount(): WalletsListConfig { + return copy( + wallets = wallets + .mapIndexed { index, walletCard -> + if (index == selectedWalletIndex) { + when (walletCard) { + is WalletCardState.Content -> walletCard.copy( + additionalInfo = WalletAdditionalInfoFactory.resolve( + wallet = currentWalletProvider(), + ), + cardCount = currentWalletProvider().getCardsCount(), + ) + is WalletCardState.HiddenContent -> walletCard.copy( + additionalInfo = WalletAdditionalInfoFactory.resolve( + wallet = currentWalletProvider(), + ), + cardCount = currentWalletProvider().getCardsCount(), + ) + else -> walletCard + } + } else { + walletCard + } + } + .toImmutableList(), + ) + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/FiatBalanceToWalletCardConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/FiatBalanceToWalletCardConverter.kt index 89e3ff8be5..bca1082c42 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/FiatBalanceToWalletCardConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/FiatBalanceToWalletCardConverter.kt @@ -6,6 +6,7 @@ import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.tokens.model.TokenList.FiatBalance import com.tangem.domain.wallets.models.UserWallet import com.tangem.feature.wallet.presentation.wallet.domain.WalletAdditionalInfoFactory +import com.tangem.feature.wallet.presentation.wallet.domain.getCardsCount import com.tangem.feature.wallet.presentation.wallet.state.components.WalletCardState import com.tangem.utils.converter.Converter @@ -54,6 +55,7 @@ internal class FiatBalanceToWalletCardConverter( fiatCurrencySymbol = appCurrency.symbol, ), additionalInfo = WalletAdditionalInfoFactory.resolve(wallet = currentWalletProvider()), + cardCount = currentWalletProvider().getCardsCount(), ) } else { WalletCardState.Content( @@ -68,6 +70,7 @@ internal class FiatBalanceToWalletCardConverter( fiatCurrencyCode = appCurrency.code, fiatCurrencySymbol = appCurrency.symbol, ), + cardCount = currentWalletProvider().getCardsCount(), ) } } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/WalletHiddenBalanceStateConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/WalletHiddenBalanceStateConverter.kt index 7f29843cea..eb85c8f42e 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/WalletHiddenBalanceStateConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/WalletHiddenBalanceStateConverter.kt @@ -25,6 +25,7 @@ internal class WalletHiddenBalanceStateConverter { onRenameClick = content.onRenameClick, onDeleteClick = content.onDeleteClick, balance = content.balance, + cardCount = content.cardCount, ) } @@ -37,6 +38,7 @@ internal class WalletHiddenBalanceStateConverter { onRenameClick = hiddenContent.onRenameClick, onDeleteClick = hiddenContent.onDeleteClick, balance = hiddenContent.balance, + cardCount = hiddenContent.cardCount, ) } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletNotificationsListFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletNotificationsListFactory.kt index 928b02cc55..8c0fa79e8d 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletNotificationsListFactory.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletNotificationsListFactory.kt @@ -6,6 +6,8 @@ import com.tangem.domain.demo.IsDemoCardUseCase import com.tangem.domain.settings.IsReadyToShowRateAppUseCase import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.domain.wallets.usecase.IsNeedToBackupUseCase import com.tangem.feature.wallet.presentation.wallet.state.components.WalletNotification import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList @@ -16,9 +18,10 @@ import kotlin.collections.count /** * Wallet notifications list factory * - * @property isDemoCardUseCase use case that check if card is demo - * @property isReadyToShowRateAppUseCase use case that check if card is user already rate app - * @property wasCardScannedUseCase use case that check if card was scanned + * @property isDemoCardUseCase use case that checks if card is demo + * @property isReadyToShowRateAppUseCase use case that checks if card is user already rate app + * @property wasCardScannedUseCase use case that checks if card was scanned + * @property isNeedToBackupUseCase use case that checks if wallet need backup cards * @property clickIntents screen click intents * [REDACTED_AUTHOR] @@ -27,17 +30,20 @@ internal class WalletNotificationsListFactory( private val isDemoCardUseCase: IsDemoCardUseCase, private val isReadyToShowRateAppUseCase: IsReadyToShowRateAppUseCase, private val wasCardScannedUseCase: WasCardScannedUseCase, + private val isNeedToBackupUseCase: IsNeedToBackupUseCase, private val clickIntents: WalletClickIntents, ) { fun create( + selectedWalletId: UserWalletId, cardTypesResolver: CardTypesResolver, cryptoCurrencyList: List, ): Flow> { return combine( flow = wasCardScannedUseCase(cardTypesResolver.getCardId()), flow2 = isReadyToShowRateAppUseCase(), - ) { wasCardScanned, isReadyToShowRating -> + flow3 = isNeedToBackupUseCase(selectedWalletId), + ) { wasCardScanned, isReadyToShowRating, isNeedToBackup -> buildList { addCriticalNotifications(cardTypesResolver) @@ -45,7 +51,7 @@ internal class WalletNotificationsListFactory( addRateTheAppNotification(isReadyToShowRating) - addWarningNotifications(cardTypesResolver, cryptoCurrencyList, wasCardScanned) + addWarningNotifications(cardTypesResolver, cryptoCurrencyList, wasCardScanned, isNeedToBackup) }.toImmutableList() } } @@ -116,12 +122,13 @@ internal class WalletNotificationsListFactory( cardTypesResolver: CardTypesResolver, cryptoCurrencyList: List, wasCardScanned: Boolean, + isNeedToBackup: Boolean, ) { addIf( element = WalletNotification.Warning.MissingBackup( onStartBackupClick = clickIntents::onBackupCardClick, ), - condition = !cardTypesResolver.isBackupForbidden() && !cardTypesResolver.hasBackup(), + condition = isNeedToBackup, ) val isDemo = isDemoCardUseCase(cardId = cardTypesResolver.getCardId()) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt index 015c3e99a5..f0c38778bf 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt @@ -122,6 +122,7 @@ internal class WalletViewModel @Inject constructor( wasCardScannedUseCase: WasCardScannedUseCase, isReadyToShowRateAppUseCase: IsReadyToShowRateAppUseCase, isDemoCardUseCase: IsDemoCardUseCase, + isNeedToBackupUseCase: IsNeedToBackupUseCase, // endregion Parameters ) : ViewModel(), DefaultLifecycleObserver, WalletClickIntents { @@ -135,6 +136,7 @@ internal class WalletViewModel @Inject constructor( wasCardScannedUseCase = wasCardScannedUseCase, isReadyToShowRateAppUseCase = isReadyToShowRateAppUseCase, isDemoCardUseCase = isDemoCardUseCase, + isNeedToBackupUseCase = isNeedToBackupUseCase, clickIntents = this, ) @@ -227,6 +229,9 @@ internal class WalletViewModel @Inject constructor( is WalletsUpdateActionResolver.Action.AddWallet -> { scrollAndUpdateState(action.selectedWalletIndex) } + is WalletsUpdateActionResolver.Action.UpdateWalletCardCount -> { + uiState = stateFactory.getStateWithUpdatedWalletCardCount() + } is WalletsUpdateActionResolver.Action.Unknown -> Unit } } @@ -983,6 +988,7 @@ internal class WalletViewModel @Inject constructor( private fun updateNotifications(index: Int, tokenList: TokenList? = null) { notificationsListFactory.create( + selectedWalletId = getWallet(index).walletId, cardTypesResolver = getCardTypeResolver(index = index), cryptoCurrencyList = if (tokenList != null) { when (tokenList) { diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletsUpdateActionResolver.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletsUpdateActionResolver.kt index 101f025d4e..bc8aa4f61a 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletsUpdateActionResolver.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletsUpdateActionResolver.kt @@ -4,6 +4,7 @@ import com.tangem.common.Provider import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWalletId import com.tangem.domain.wallets.usecase.GetSelectedWalletUseCase +import com.tangem.feature.wallet.presentation.wallet.domain.getCardsCount import com.tangem.feature.wallet.presentation.wallet.state.WalletLockedState import com.tangem.feature.wallet.presentation.wallet.state.WalletState import com.tangem.feature.wallet.presentation.wallet.state.components.WalletCardState @@ -110,20 +111,29 @@ internal class WalletsUpdateActionResolver( selectedWallet: UserWallet, ): Action { val selectedWalletName = selectedWallet.name + val previousWalletState = state.getPrevSelectedWallet() + return when { + previousWalletState.title != selectedWalletName -> { + Action.UpdateWalletName(selectedWalletName) + } - if (state.getPrevSelectedWallet().title != selectedWalletName) { - return Action.UpdateWalletName(selectedWalletName) + state is WalletLockedState && !selectedWallet.isLocked -> { + Action.UnlockWallet( + selectedWalletIndex = wallets.indexOfWallet(id = selectedWallet.walletId), + selectedWallet = selectedWallet, + unlockedWallets = wallets.filterNot(UserWallet::isLocked), + ) + } + + previousWalletState is WalletCardState.Content && + previousWalletState.cardCount != selectedWallet.getCardsCount() || + previousWalletState is WalletCardState.HiddenContent && + previousWalletState.cardCount != selectedWallet.getCardsCount() -> { + Action.UpdateWalletCardCount + } + + else -> Action.Unknown } - - if (state is WalletLockedState && !selectedWallet.isLocked) { - return Action.UnlockWallet( - selectedWalletIndex = wallets.indexOfWallet(id = selectedWallet.walletId), - selectedWallet = selectedWallet, - unlockedWallets = wallets.filterNot(UserWallet::isLocked), - ) - } - - return Action.Unknown } private fun WalletState.ContentState.getPrevSelectedWallet(): WalletCardState { @@ -163,6 +173,8 @@ internal class WalletsUpdateActionResolver( data class AddWallet(val selectedWalletIndex: Int) : Action() + object UpdateWalletCardCount : Action() + object Unknown : Action() } } \ No newline at end of file From 56590d9345093a49cab1d23a3a6a504e2df1b740 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 2 Oct 2023 12:40:55 +0300 Subject: [PATCH 120/242] Updated on 2026-08-14 --- .../state/components/TokenDetailsNotification.kt | 2 -- .../factory/TokenDetailsNotificationConverter.kt | 12 +++--------- .../state/factory/TokenDetailsStateFactory.kt | 4 +--- .../tokendetails/ui/TokenDetailsScreen.kt | 2 +- .../tokendetails/viewmodels/TokenDetailsViewModel.kt | 2 +- 5 files changed, 6 insertions(+), 16 deletions(-) diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/components/TokenDetailsNotification.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/components/TokenDetailsNotification.kt index 5d7693bd08..13b31508f4 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/components/TokenDetailsNotification.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/components/TokenDetailsNotification.kt @@ -11,14 +11,12 @@ import com.tangem.features.tokendetails.impl.R // TODO: Finalize notification strings [REDACTED_JIRA] @Immutable sealed class TokenDetailsNotification( - open val isVisible: Boolean = true, open val config: NotificationConfig, ) { data class RentInfo( private val rentInfo: CryptoCurrencyWarning.Rent, private val onCloseClick: () -> Unit, - override val isVisible: Boolean = true, ) : TokenDetailsNotification( config = NotificationConfig( title = TextReference.Res(R.string.send_network_fee_title), diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsNotificationConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsNotificationConverter.kt index 25f691722d..945fd6107e 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsNotificationConverter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsNotificationConverter.kt @@ -1,11 +1,11 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.state.factory -import com.tangem.common.extensions.cast import com.tangem.domain.tokens.model.warnings.CryptoCurrencyWarning import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.TokenDetailsNotification import com.tangem.feature.tokendetails.presentation.tokendetails.viewmodels.TokenDetailsClickIntents import com.tangem.utils.converter.Converter +import com.tangem.utils.extensions.removeBy import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList @@ -17,15 +17,9 @@ internal class TokenDetailsNotificationConverter( return value.map(::mapToNotification).toImmutableList() } - fun getStateRentInfoVisibility( - currentState: TokenDetailsState, - isVisible: Boolean, - ): ImmutableList { + fun removeRentInfo(currentState: TokenDetailsState): ImmutableList { val newNotifications = currentState.notifications.toMutableList() - val oldNotification = newNotifications.find { it is TokenDetailsNotification.RentInfo } - oldNotification?.let { - newNotifications.add(it.cast().copy(isVisible = isVisible)) - } + newNotifications.removeBy { it is TokenDetailsNotification.RentInfo } return newNotifications.toImmutableList() } diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt index 40f0b4da65..053b87757c 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt @@ -139,9 +139,7 @@ internal class TokenDetailsStateFactory( } fun getRefreshedState(): TokenDetailsState { - val state = currentStateProvider() return refreshStateConverter.convert(false) - .copy(notifications = notificationConverter.getStateRentInfoVisibility(state, true)) } fun getStateWithReceiveBottomSheet( @@ -212,6 +210,6 @@ internal class TokenDetailsStateFactory( fun getStateWithRemovedRentNotification(): TokenDetailsState { val state = currentStateProvider() - return state.copy(notifications = notificationConverter.getStateRentInfoVisibility(state, false)) + return state.copy(notifications = notificationConverter.removeRentInfo(state)) } } \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/TokenDetailsScreen.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/TokenDetailsScreen.kt index d1683f22c8..288f8e1464 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/TokenDetailsScreen.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/TokenDetailsScreen.kt @@ -87,7 +87,7 @@ internal fun TokenDetailsScreen(state: TokenDetailsState) { ) } items( - items = state.notifications.filter { it.isVisible }, + items = state.notifications, key = { it.config::class.java }, contentType = { it.config::class.java }, itemContent = { Notification(config = it.config, modifier = itemModifier.animateItemPlacement()) }, diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt index f37726978c..b28ca2dff7 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt @@ -365,7 +365,7 @@ internal class TokenDetailsViewModel @Inject constructor( refresh = true, ) updateTxHistory(refresh = true) - + updateWarnings(wallet) uiState = stateFactory.getRefreshedState() }.saveIn(refreshStateJobHolder) } From f6c6c5faed6e7c8b154351846944cfcbc1c3879f Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 2 Oct 2023 12:45:20 +0300 Subject: [PATCH 121/242] Updated on 2026-08-14 --- .../repository/DefaultNetworksRepository.kt | 3 +-- .../tokens/GetCurrencyStatusUpdatesUseCase.kt | 6 ++---- .../CurrenciesStatusesOperations.kt | 21 ++++++------------- .../tokens/repository/NetworksRepository.kt | 7 +------ .../repository/MockNetworksRepository.kt | 1 - .../viewmodels/TokenDetailsViewModel.kt | 18 +++++++++++++++- 6 files changed, 27 insertions(+), 29 deletions(-) diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultNetworksRepository.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultNetworksRepository.kt index 6f5615c1b6..e1d3826183 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultNetworksRepository.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultNetworksRepository.kt @@ -40,7 +40,6 @@ internal class DefaultNetworksRepository( override fun getNetworkStatusesUpdates( userWalletId: UserWalletId, networks: Set, - refresh: Boolean, ): Flow> = channelFlow { launch(dispatchers.io) { networksStatusesStore.get(userWalletId) @@ -48,7 +47,7 @@ internal class DefaultNetworksRepository( } withContext(dispatchers.io) { - fetchNetworksStatusesIfCacheExpired(userWalletId, networks, refresh) + fetchNetworksStatusesIfCacheExpired(userWalletId, networks, false) } }.cancellable() diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCurrencyStatusUpdatesUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCurrencyStatusUpdatesUseCase.kt index f3cf282b1d..d018b74e85 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCurrencyStatusUpdatesUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCurrencyStatusUpdatesUseCase.kt @@ -37,17 +37,15 @@ class GetCurrencyStatusUpdatesUseCase( operator fun invoke( userWalletId: UserWalletId, currencyId: CryptoCurrency.ID, - refresh: Boolean, ): Flow> { return flow { - emitAll(getCurrency(userWalletId, currencyId, refresh)) + emitAll(getCurrency(userWalletId, currencyId)) }.flowOn(dispatchers.io) } private suspend fun getCurrency( userWalletId: UserWalletId, currencyId: CryptoCurrency.ID, - refresh: Boolean, ): Flow> { val operations = CurrenciesStatusesOperations( currenciesRepository = currenciesRepository, @@ -56,7 +54,7 @@ class GetCurrencyStatusUpdatesUseCase( userWalletId = userWalletId, ) - return operations.getCurrencyStatusFlow(currencyId, refresh).map { maybeCurrency -> + return operations.getCurrencyStatusFlow(currencyId).map { maybeCurrency -> maybeCurrency.mapLeft(CurrenciesStatusesOperations.Error::mapToCurrencyError) } } diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesOperations.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesOperations.kt index 3a2327b7cd..fc4ec95f89 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesOperations.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesOperations.kt @@ -67,16 +67,13 @@ internal class CurrenciesStatusesOperations( } } - suspend fun getCurrencyStatusFlow( - currencyId: CryptoCurrency.ID, - refresh: Boolean = false, - ): Flow> { + suspend fun getCurrencyStatusFlow(currencyId: CryptoCurrency.ID): Flow> { val currency = recover( block = { getMultiCurrencyWalletCurrency(currencyId) }, recover = { return flowOf(it.left()) }, ) - return getCurrencyStatusFlow(currency, refresh) + return getCurrencyStatusFlow(currency) } suspend fun getNetworkCoinFlow(networkId: Network.ID): Flow> { @@ -97,10 +94,7 @@ internal class CurrenciesStatusesOperations( return getCurrencyStatusFlow(currency) } - private fun getCurrencyStatusFlow( - currency: CryptoCurrency, - refresh: Boolean = false, - ): Flow> { + private fun getCurrencyStatusFlow(currency: CryptoCurrency): Flow> { val (networks, currenciesIds) = getIds(nonEmptyListOf(currency)) val quoteFlow = getQuotes(currenciesIds) @@ -111,7 +105,7 @@ internal class CurrenciesStatusesOperations( } } - val statusFlow = getNetworksStatuses(networks, refresh) + val statusFlow = getNetworksStatuses(networks) .map { maybeStatuses -> maybeStatuses.flatMap { statuses -> statuses.singleOrNull { it.network == currency.network }?.right() @@ -210,11 +204,8 @@ internal class CurrenciesStatusesOperations( .onEmpty { emit(Error.EmptyQuotes.left()) } } - private fun getNetworksStatuses( - networks: NonEmptySet, - refresh: Boolean = false, - ): Flow>> { - return networksRepository.getNetworkStatusesUpdates(userWalletId, networks, refresh) + private fun getNetworksStatuses(networks: NonEmptySet): Flow>> { + return networksRepository.getNetworkStatusesUpdates(userWalletId, networks) .map, Either>> { it.right() } .catch { emit(Error.DataError(it).left()) } .onEmpty { emit(Error.EmptyNetworksStatuses.left()) } diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/NetworksRepository.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/NetworksRepository.kt index da67708111..30baab37f3 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/NetworksRepository.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/NetworksRepository.kt @@ -16,14 +16,9 @@ interface NetworksRepository { * * @param userWalletId The unique identifier of the user wallet. * @param networks A set of network which statuses are to be retrieved. - * @param refresh A boolean flag indicating whether the data should be refreshed from remote. * @return A [Flow] emitting a set of [NetworkStatus] objects corresponding to the specified networks. */ - fun getNetworkStatusesUpdates( - userWalletId: UserWalletId, - networks: Set, - refresh: Boolean = false, - ): Flow> + fun getNetworkStatusesUpdates(userWalletId: UserWalletId, networks: Set): Flow> /** * Retrieves network statuses of specified blockchain networks for a specific user wallet. diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockNetworksRepository.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockNetworksRepository.kt index 363ad6202a..ffefd750e0 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockNetworksRepository.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockNetworksRepository.kt @@ -17,7 +17,6 @@ internal class MockNetworksRepository( override fun getNetworkStatusesUpdates( userWalletId: UserWalletId, networks: Set, - refresh: Boolean, ): Flow> { return statuses.map { it.getOrElse { e -> throw e } } } diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt index b28ca2dff7..ab5d7f11d0 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt @@ -35,6 +35,7 @@ import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.JobHolder import com.tangem.utils.coroutines.saveIn import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch import timber.log.Timber @@ -71,6 +72,7 @@ internal class TokenDetailsViewModel @Inject constructor( private val marketPriceJobHolder = JobHolder() private val refreshStateJobHolder = JobHolder() + private val networkStatusAutoUpdateStateJobHolder = JobHolder() private var cryptoCurrencyStatus: CryptoCurrencyStatus? = null private var wallet by Delegates.notNull() @@ -148,7 +150,6 @@ internal class TokenDetailsViewModel @Inject constructor( getCurrencyStatusUpdatesUseCase( userWalletId = selectedWallet.walletId, currencyId = cryptoCurrency.id, - refresh = true, ) .distinctUntilChanged() .onEach { either -> @@ -161,6 +162,17 @@ internal class TokenDetailsViewModel @Inject constructor( .flowOn(dispatchers.io) .launchIn(viewModelScope) .saveIn(marketPriceJobHolder) + + viewModelScope.launch(dispatchers.io) { + // Wait for blockchain updates pending transactions. + // Immediate update doesn't receive any changes. + delay(NETWORK_STATUS_AUTO_UPDATE_DELAY) + fetchCurrencyStatusUseCase.invoke( + userWalletId = wallet.walletId, + id = cryptoCurrency.id, + refresh = true, + ) + }.saveIn(networkStatusAutoUpdateStateJobHolder) } private fun updateTxHistory(refresh: Boolean = false) { @@ -377,4 +389,8 @@ internal class TokenDetailsViewModel @Inject constructor( override fun onCloseRentInfoNotification() { uiState = stateFactory.getStateWithRemovedRentNotification() } + + companion object { + private const val NETWORK_STATUS_AUTO_UPDATE_DELAY = 1000L + } } \ No newline at end of file From 52f7454657e5e720b3b82d7da0de40760984ef40 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 21 Sep 2023 17:17:13 +0300 Subject: [PATCH 122/242] Updated on 2026-08-14 --- .../middlewares/TradeCryptoMiddleware.kt | 6 +- .../tap/proxy/TransactionManagerImpl.kt | 2 +- .../tangem/tap/proxy/UserWalletManagerImpl.kt | 2 +- .../DefaultWalletManagersFacade.kt | 29 +++-- .../walletmanager/WalletManagersFacade.kt | 3 +- .../tangem/feature/swap/SwapRepositoryImpl.kt | 101 +++++++++++++----- .../swap/converters/ApproveConverter.kt | 16 --- .../tangem/feature/swap/di/SwapDataModule.kt | 3 + .../feature/swap/domain/SwapInteractorImpl.kt | 74 +++++++++---- .../feature/swap/domain/SwapRepository.kt | 43 ++++---- .../domain/models/cache/SwapDataHolder.kt | 21 ---- .../swap/domain/models/domain/ApproveModel.kt | 14 --- .../swap/domain/models/ui/SwapState.kt | 3 +- gradle/dependencies.toml | 2 +- 14 files changed, 174 insertions(+), 145 deletions(-) delete mode 100644 features/swap/data/src/main/java/com/tangem/feature/swap/converters/ApproveConverter.kt delete mode 100644 features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/cache/SwapDataHolder.kt delete mode 100644 features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/ApproveModel.kt diff --git a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/TradeCryptoMiddleware.kt b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/TradeCryptoMiddleware.kt index 2324133b75..36ab93bff5 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/TradeCryptoMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/TradeCryptoMiddleware.kt @@ -155,7 +155,7 @@ class TradeCryptoMiddleware { val walletManager = store.state.daggerGraphState .get(DaggerGraphState::walletManagersFacade) .getOrCreateWalletManager( - userWallet = action.userWallet, + userWalletId = action.userWallet.walletId, blockchain = blockchain, derivationPath = currency.network.derivationPath.value, ) @@ -338,7 +338,7 @@ class TradeCryptoMiddleware { val walletManager = store.state.daggerGraphState .get(DaggerGraphState::walletManagersFacade) .getOrCreateWalletManager( - userWallet = action.userWallet, + userWalletId = action.userWallet.walletId, blockchain = blockchain, derivationPath = currency.network.derivationPath.value, ) @@ -379,7 +379,7 @@ class TradeCryptoMiddleware { val walletManager = store.state.daggerGraphState .get(DaggerGraphState::walletManagersFacade) .getOrCreateWalletManager( - userWallet = action.userWallet, + userWalletId = action.userWallet.walletId, blockchain = blockchain, derivationPath = currency.network.derivationPath.value, ) diff --git a/app/src/main/java/com/tangem/tap/proxy/TransactionManagerImpl.kt b/app/src/main/java/com/tangem/tap/proxy/TransactionManagerImpl.kt index 3df732e168..cfce94b52b 100644 --- a/app/src/main/java/com/tangem/tap/proxy/TransactionManagerImpl.kt +++ b/app/src/main/java/com/tangem/tap/proxy/TransactionManagerImpl.kt @@ -419,7 +419,7 @@ class TransactionManagerImpl( appStateHolder.userWalletsListManager?.selectedUserWalletSync, ) { "userWallet or userWalletsListManager is null" } walletManagersFacade.getOrCreateWalletManager( - selectedUserWallet, + selectedUserWallet.walletId, blockchain, derivationPath, ) diff --git a/app/src/main/java/com/tangem/tap/proxy/UserWalletManagerImpl.kt b/app/src/main/java/com/tangem/tap/proxy/UserWalletManagerImpl.kt index 6165cbd367..3640c46412 100644 --- a/app/src/main/java/com/tangem/tap/proxy/UserWalletManagerImpl.kt +++ b/app/src/main/java/com/tangem/tap/proxy/UserWalletManagerImpl.kt @@ -230,7 +230,7 @@ class UserWalletManagerImpl( appStateHolder.userWalletsListManager?.selectedUserWalletSync, ) { "userWallet or userWalletsListManager is null" } walletManagersFacade.getOrCreateWalletManager( - selectedUserWallet, + selectedUserWallet.walletId, blockchain, derivationPath, ) diff --git a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/DefaultWalletManagersFacade.kt b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/DefaultWalletManagersFacade.kt index 31d72a5476..7d0e1e2593 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/DefaultWalletManagersFacade.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/DefaultWalletManagersFacade.kt @@ -61,11 +61,10 @@ class DefaultWalletManagersFacade( network: Network, addressType: AddressType, ): String { - val userWallet = getUserWallet(userWalletId) val blockchain = Blockchain.fromId(network.id.value) val walletManager = getOrCreateWalletManager( - userWallet = userWallet, + userWalletId = userWalletId, blockchain = blockchain, derivationPath = network.derivationPath.value, ) @@ -79,10 +78,9 @@ class DefaultWalletManagersFacade( } override suspend fun getTxHistoryState(userWalletId: UserWalletId, network: Network): TxHistoryState { - val userWallet = getUserWallet(userWalletId) val blockchain = Blockchain.fromId(network.id.value) val walletManager = getOrCreateWalletManager( - userWallet = userWallet, + userWalletId = userWalletId, blockchain = blockchain, derivationPath = network.derivationPath.value, ) @@ -102,10 +100,9 @@ class DefaultWalletManagersFacade( page: Int, pageSize: Int, ): PaginationWrapper { - val userWallet = getUserWallet(userWalletId) val blockchain = Blockchain.fromId(currency.network.id.value) val walletManager = getOrCreateWalletManager( - userWallet = userWallet, + userWalletId = userWalletId, blockchain = blockchain, derivationPath = currency.network.derivationPath.value, ) @@ -154,7 +151,11 @@ class DefaultWalletManagersFacade( return UpdateWalletManagerResult.MissedDerivation } - val walletManager = getOrCreateWalletManager(userWallet, blockchain, derivationPath) + val walletManager = getOrCreateWalletManager( + userWalletId = userWallet.walletId, + blockchain = blockchain, + derivationPath = derivationPath, + ) if (walletManager == null || blockchain == Blockchain.Unknown) { Timber.w("Unable to get a wallet manager for blockchain: $blockchain") return UpdateWalletManagerResult.Unreachable @@ -195,12 +196,11 @@ class DefaultWalletManagersFacade( } override suspend fun getOrCreateWalletManager( - userWallet: UserWallet, + userWalletId: UserWalletId, blockchain: Blockchain, derivationPath: String?, ): WalletManager? { - val userWalletId = userWallet.walletId - + val userWallet = getUserWallet(userWalletId) var walletManager = walletManagersStore.getSyncOrNull( userWalletId = userWalletId, blockchain = blockchain, @@ -221,11 +221,10 @@ class DefaultWalletManagersFacade( } override suspend fun getAddress(userWalletId: UserWalletId, network: Network): List
{ - val userWallet = getUserWallet(userWalletId) val blockchain = Blockchain.fromId(network.id.value) return getOrCreateWalletManager( - userWallet = userWallet, + userWalletId = userWalletId, blockchain = blockchain, derivationPath = network.derivationPath.value, ) @@ -236,10 +235,9 @@ class DefaultWalletManagersFacade( } override suspend fun getRentInfo(userWalletId: UserWalletId, network: Network): CryptoCurrencyWarning.Rent? { - val userWallet = getUserWallet(userWalletId) val blockchain = Blockchain.fromId(network.id.value) val manager = getOrCreateWalletManager( - userWallet = userWallet, + userWalletId = userWalletId, blockchain = blockchain, derivationPath = network.derivationPath.value, ) @@ -267,10 +265,9 @@ class DefaultWalletManagersFacade( } override suspend fun getExistentialDeposit(userWalletId: UserWalletId, network: Network): BigDecimal? { - val userWallet = getUserWallet(userWalletId) val blockchain = Blockchain.fromId(network.id.value) val manager = getOrCreateWalletManager( - userWallet = userWallet, + userWalletId = userWalletId, blockchain = blockchain, derivationPath = network.derivationPath.value, ) diff --git a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/WalletManagersFacade.kt b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/WalletManagersFacade.kt index 9ec947d51e..5be94faf27 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/WalletManagersFacade.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/WalletManagersFacade.kt @@ -12,7 +12,6 @@ import com.tangem.domain.txhistory.models.PaginationWrapper import com.tangem.domain.txhistory.models.TxHistoryItem import com.tangem.domain.txhistory.models.TxHistoryState import com.tangem.domain.walletmanager.model.UpdateWalletManagerResult -import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWalletId import java.math.BigDecimal @@ -72,7 +71,7 @@ interface WalletManagersFacade { // TODO: Remove after refactoring suspend fun getOrCreateWalletManager( - userWallet: UserWallet, + userWalletId: UserWalletId, blockchain: Blockchain, derivationPath: String?, ): WalletManager? diff --git a/features/swap/data/src/main/java/com/tangem/feature/swap/SwapRepositoryImpl.kt b/features/swap/data/src/main/java/com/tangem/feature/swap/SwapRepositoryImpl.kt index 897c742670..e5394f3013 100644 --- a/features/swap/data/src/main/java/com/tangem/feature/swap/SwapRepositoryImpl.kt +++ b/features/swap/data/src/main/java/com/tangem/feature/swap/SwapRepositoryImpl.kt @@ -1,6 +1,10 @@ package com.tangem.feature.swap +import com.tangem.blockchain.common.Amount +import com.tangem.blockchain.common.Approver import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchain.common.Token +import com.tangem.blockchain.extensions.Result import com.tangem.data.tokens.utils.CryptoCurrencyFactory import com.tangem.datasource.api.oneinch.OneInchApi import com.tangem.datasource.api.oneinch.OneInchApiFactory @@ -12,20 +16,21 @@ import com.tangem.domain.common.extensions.fromNetworkId import com.tangem.domain.common.util.derivationStyleProvider import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.Network +import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.models.UserWallet -import com.tangem.feature.swap.converters.ApproveConverter +import com.tangem.domain.wallets.models.UserWalletId import com.tangem.feature.swap.converters.QuotesConverter import com.tangem.feature.swap.converters.SwapConverter import com.tangem.feature.swap.converters.TokensConverter import com.tangem.feature.swap.domain.SwapRepository import com.tangem.feature.swap.domain.models.data.AggregatedSwapDataModel -import com.tangem.feature.swap.domain.models.domain.ApproveModel import com.tangem.feature.swap.domain.models.domain.Currency import com.tangem.feature.swap.domain.models.domain.QuoteModel import com.tangem.feature.swap.domain.models.domain.SwapDataModel import com.tangem.feature.swap.domain.models.mapErrors import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.withContext +import java.math.BigDecimal import javax.inject.Inject import com.tangem.blockchain.common.Token as SdkToken @@ -35,12 +40,12 @@ internal class SwapRepositoryImpl @Inject constructor( private val oneInchErrorsHandler: OneInchErrorsHandler, private val coroutineDispatcher: CoroutineDispatcherProvider, private val configManager: ConfigManager, + private val walletManagersFacade: WalletManagersFacade, ) : SwapRepository { private val tokensConverter = TokensConverter() private val quotesConverter = QuotesConverter() private val swapConverter = SwapConverter() - private val approveConverter = ApproveConverter() override suspend fun getRates(currencyId: String, tokenIds: List): Map { // workaround cause backend do not return arbitrum and optimism rates @@ -104,29 +109,6 @@ internal class SwapRepositoryImpl @Inject constructor( } } - override suspend fun dataToApprove(networkId: String, tokenAddress: String, amount: String?): ApproveModel { - return withContext(coroutineDispatcher.io) { - approveConverter.convert(getOneInchApi(networkId).approveTransaction(tokenAddress, amount)) - } - } - - override suspend fun checkTokensSpendAllowance( - networkId: String, - tokenAddress: String, - walletAddress: String, - ): AggregatedSwapDataModel { - return withContext(coroutineDispatcher.io) { - try { - val response = oneInchErrorsHandler.handleOneInchResponse( - getOneInchApi(networkId).approveAllowance(tokenAddress, walletAddress), - ) - AggregatedSwapDataModel(response.allowance) - } catch (ex: OneIncResponseException) { - AggregatedSwapDataModel(null, mapErrors(ex.data.description)) - } - } - } - override suspend fun prepareSwapTransaction( networkId: String, fromTokenAddress: String, @@ -191,6 +173,73 @@ internal class SwapRepositoryImpl @Inject constructor( } as CryptoCurrency } + override suspend fun getAllowance( + userWalletId: UserWalletId, + networkId: String, + derivationPath: String?, + tokenDecimalCount: Int, + tokenAddress: String, + ): BigDecimal { + val blockchain = requireNotNull(Blockchain.fromNetworkId(networkId)) { "blockchain not found" } + val walletManager = walletManagersFacade.getOrCreateWalletManager( + userWalletId = userWalletId, + blockchain = blockchain, + derivationPath = derivationPath, + ) + val spenderAddress = addressForTrust(networkId) + + val result = (walletManager as? Approver)?.getAllowance( + spenderAddress, + Token( + symbol = blockchain.currency, + contractAddress = tokenAddress, + decimals = tokenDecimalCount, + ), + ) ?: error("Cannot cast to Approver") + + return when (result) { + is Result.Success -> result.data + is Result.Failure -> error(result.error) + } + } + + override suspend fun getApproveData( + userWalletId: UserWalletId, + networkId: String, + derivationPath: String?, + currency: Currency, + amount: BigDecimal?, + ): String { + val blockchain = + requireNotNull(Blockchain.fromNetworkId(networkId)) { "blockchain not found" } + val walletManager = walletManagersFacade.getOrCreateWalletManager( + userWalletId = userWalletId, + blockchain = blockchain, + derivationPath = derivationPath, + ) + val spenderAddress = addressForTrust(networkId) + + return (walletManager as? Approver)?.getApproveData( + spenderAddress, + amount?.let { convertToAmount(it, currency, blockchain) }, + ) ?: error("Cannot cast to Approver") + } + + private fun convertToAmount(amount: BigDecimal, currency: Currency, blockchain: Blockchain): Amount { + return when (currency) { + is Currency.NativeToken -> { + Amount(value = amount, blockchain = blockchain) + } + is Currency.NonNativeToken -> { + Amount( + currencySymbol = currency.symbol, + value = amount, + decimals = currency.decimalCount, + ) + } + } + } + private fun getOneInchApi(networkId: String): OneInchApi { return oneInchApiFactory.getApi(networkId) } diff --git a/features/swap/data/src/main/java/com/tangem/feature/swap/converters/ApproveConverter.kt b/features/swap/data/src/main/java/com/tangem/feature/swap/converters/ApproveConverter.kt deleted file mode 100644 index b281f8df49..0000000000 --- a/features/swap/data/src/main/java/com/tangem/feature/swap/converters/ApproveConverter.kt +++ /dev/null @@ -1,16 +0,0 @@ -package com.tangem.feature.swap.converters - -import com.tangem.datasource.api.oneinch.models.ApproveCalldataResponse -import com.tangem.feature.swap.domain.models.domain.ApproveModel -import com.tangem.utils.converter.Converter - -class ApproveConverter : Converter { - - override fun convert(value: ApproveCalldataResponse): ApproveModel { - return ApproveModel( - data = value.data, - gasPrice = value.gasPrice, - toAddress = value.toAddress, - ) - } -} \ No newline at end of file diff --git a/features/swap/data/src/main/java/com/tangem/feature/swap/di/SwapDataModule.kt b/features/swap/data/src/main/java/com/tangem/feature/swap/di/SwapDataModule.kt index bdb28d8007..81931ca111 100644 --- a/features/swap/data/src/main/java/com/tangem/feature/swap/di/SwapDataModule.kt +++ b/features/swap/data/src/main/java/com/tangem/feature/swap/di/SwapDataModule.kt @@ -4,6 +4,7 @@ import com.tangem.datasource.api.oneinch.OneInchApiFactory import com.tangem.datasource.api.oneinch.OneInchErrorsHandler import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.config.ConfigManager +import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.feature.swap.SwapRepositoryImpl import com.tangem.feature.swap.domain.SwapRepository import com.tangem.utils.coroutines.CoroutineDispatcherProvider @@ -25,6 +26,7 @@ class SwapDataModule { oneInchErrorsHandler: OneInchErrorsHandler, coroutineDispatcher: CoroutineDispatcherProvider, configManager: ConfigManager, + walletManagerFacade: WalletManagersFacade, ): SwapRepository { return SwapRepositoryImpl( tangemTechApi = tangemTechApi, @@ -32,6 +34,7 @@ class SwapDataModule { oneInchErrorsHandler = oneInchErrorsHandler, coroutineDispatcher = coroutineDispatcher, configManager = configManager, + walletManagersFacade = walletManagerFacade, ) } } \ No newline at end of file diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt index 8eb7545ecb..10e542bd74 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt @@ -6,7 +6,6 @@ import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.usecase.GetSelectedWalletUseCase import com.tangem.feature.swap.domain.cache.SwapDataCache import com.tangem.feature.swap.domain.converters.SwapCurrencyConverter -import com.tangem.feature.swap.domain.models.DataError import com.tangem.feature.swap.domain.models.SwapAmount import com.tangem.feature.swap.domain.models.domain.* import com.tangem.feature.swap.domain.models.domain.Currency @@ -118,16 +117,20 @@ internal class SwapInteractorImpl @Inject constructor( override suspend fun givePermissionToSwap(networkId: String, permissionOptions: PermissionOptions): TxState { val dataToSign = if (permissionOptions.approveType == SwapApproveType.UNLIMITED) { - repository.dataToApprove(networkId, getTokenAddress(permissionOptions.fromToken)).data + getApproveData( + networkId = networkId, + derivationPath = derivationPath, + fromToken = permissionOptions.fromToken, + ) } else { - permissionOptions.approveData.approveModel.data + permissionOptions.approveData.approveData } val result = transactionManager.sendApproveTransaction( txData = ApproveTxData( networkId = networkId, feeAmount = permissionOptions.txFee.feeValue, gasLimit = permissionOptions.txFee.gasLimit, - destinationAddress = permissionOptions.approveData.approveModel.toAddress, + destinationAddress = getTokenAddress(permissionOptions.fromToken), dataToSign = dataToSign, ), derivationPath = derivationPath, @@ -164,7 +167,7 @@ internal class SwapInteractorImpl @Inject constructor( val amount = SwapAmount(amountDecimal, getTokenDecimals(fromToken)) val fromTokenAddress = getTokenAddress(fromToken) val toTokenAddress = getTokenAddress(toToken) - val isAllowedToSpend = isAllowedToSpend(networkId, fromTokenAddress, amount) + val isAllowedToSpend = isAllowedToSpend(networkId, fromToken, amount) if (isAllowedToSpend && allowPermissionsHandler.isAddressAllowanceInProgress(fromTokenAddress)) { allowPermissionsHandler.removeAddressFromProgress(fromTokenAddress) transactionManager.updateWalletManager(networkId, derivationPath) @@ -340,14 +343,23 @@ internal class SwapInteractorImpl @Inject constructor( } } - private suspend fun isAllowedToSpend(networkId: String, fromTokenAddress: String, amount: SwapAmount): Boolean { - val allowance = repository.checkTokensSpendAllowance( - networkId = networkId, - tokenAddress = fromTokenAddress, - walletAddress = userWalletManager.getWalletAddress(networkId, derivationPath), + private suspend fun isAllowedToSpend(networkId: String, fromToken: Currency, amount: SwapAmount): Boolean { + return getSelectedWalletUseCase().fold( + ifRight = { userWallet -> + val allowance = repository.getAllowance( + userWallet.walletId, + networkId, + derivationPath, + getTokenDecimals(fromToken), + getTokenAddress(fromToken), + ) + allowance >= amount.value + }, + ifLeft = { + Timber.e("Swap Error on isAllowedToSpend") + false + }, ) - val allowanceAmount = allowance.dataModel?.toBigDecimalOrNull() ?: BigDecimal.ZERO - return allowance.error == DataError.NoError && allowanceAmount >= amount.value.movePointRight(amount.decimals) } private fun createEmptyAmountState(networkId: String, fromToken: Currency, toToken: Currency): SwapState { @@ -558,18 +570,19 @@ internal class SwapInteractorImpl @Inject constructor( ) } // setting up amount for approve with given amount for swap [SwapApproveType.Limited] - val transactionData = repository.dataToApprove( + val transactionData = getApproveData( networkId = networkId, - tokenAddress = getTokenAddress(fromToken), - amount = swapAmount.toStringWithRightOffset(), + derivationPath = derivationPath, + fromToken = fromToken, + swapAmount = swapAmount, ) val feeData = transactionManager.getFee( networkId = networkId, amountToSend = BigDecimal.ZERO, currencyToSend = userWalletManager.getNativeTokenForNetwork(networkId), - destinationAddress = transactionData.toAddress, + destinationAddress = getTokenAddress(fromToken), increaseBy = INCREASE_GAS_LIMIT_BY, - data = transactionData.data, + data = transactionData, derivationPath = derivationPath, ) val feeState = proxyFeesToFeeState(networkId, feeData) @@ -584,10 +597,10 @@ internal class SwapInteractorImpl @Inject constructor( currency = fromToken.symbol, amount = INFINITY_SYMBOL, walletAddress = getWalletAddress(networkId), - spenderAddress = transactionData.toAddress, + spenderAddress = getTokenAddress(fromToken), requestApproveData = RequestApproveStateData( fee = feeState, - approveModel = transactionData, + approveData = transactionData, ), ), preparedSwapConfigState = quotesLoadedState.preparedSwapConfigState.copy( @@ -718,6 +731,29 @@ internal class SwapInteractorImpl @Inject constructor( return (BigDecimal.ONE - toTokenFiatValue.divide(fromTokenFiatValue, 2, RoundingMode.HALF_UP)).toFloat() } + private suspend fun getApproveData( + networkId: String, + derivationPath: String?, + fromToken: Currency, + swapAmount: SwapAmount? = null, + ): String { + return getSelectedWalletUseCase().fold( + ifRight = { userWallet -> + repository.getApproveData( + userWalletId = userWallet.walletId, + networkId = networkId, + derivationPath = derivationPath, + currency = fromToken, + amount = swapAmount?.value, + ) + }, + ifLeft = { + Timber.e("Swap Error on getApproveData") + error("Swap Error on getApproveData") + }, + ) + } + companion object { private const val DEFAULT_SLIPPAGE = 2 private const val ZERO_BALANCE = "0" diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapRepository.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapRepository.kt index 449df97cdb..b303325c8c 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapRepository.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapRepository.kt @@ -3,11 +3,12 @@ package com.tangem.feature.swap.domain import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.Network import com.tangem.domain.wallets.models.UserWallet +import com.tangem.domain.wallets.models.UserWalletId import com.tangem.feature.swap.domain.models.data.AggregatedSwapDataModel -import com.tangem.feature.swap.domain.models.domain.ApproveModel import com.tangem.feature.swap.domain.models.domain.Currency import com.tangem.feature.swap.domain.models.domain.QuoteModel import com.tangem.feature.swap.domain.models.domain.SwapDataModel +import java.math.BigDecimal interface SwapRepository { @@ -29,28 +30,6 @@ interface SwapRepository { */ suspend fun addressForTrust(networkId: String): String - /** - * Generate "data" for calling contract in order to allow 1inch spend funds - * - * @param tokenAddress token you want to exchange - * @param amount number of tokens is allowed. By default infinite - */ - suspend fun dataToApprove(networkId: String, tokenAddress: String, amount: String? = null): ApproveModel - - /** - * Get the number of tokens that the 1inch router is allowed to spend - * - * @param tokenAddress Token address you want to exchange - * @param walletAddress address for which you want to check - * - * @return amount of tokens allowed to spend - */ - suspend fun checkTokensSpendAllowance( - networkId: String, - tokenAddress: String, - walletAddress: String, - ): AggregatedSwapDataModel - @Suppress("LongParameterList") suspend fun prepareSwapTransaction( networkId: String, @@ -68,4 +47,22 @@ interface SwapRepository { fun getTangemFee(): Double suspend fun getCryptoCurrency(userWallet: UserWallet, currency: Currency, network: Network): CryptoCurrency? + + @Throws(IllegalStateException::class) + suspend fun getAllowance( + userWalletId: UserWalletId, + networkId: String, + derivationPath: String?, + tokenDecimalCount: Int, + tokenAddress: String, + ): BigDecimal + + @Throws(IllegalStateException::class) + suspend fun getApproveData( + userWalletId: UserWalletId, + networkId: String, + derivationPath: String?, + currency: Currency, + amount: BigDecimal?, + ): String } \ No newline at end of file diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/cache/SwapDataHolder.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/cache/SwapDataHolder.kt deleted file mode 100644 index 1271078626..0000000000 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/cache/SwapDataHolder.kt +++ /dev/null @@ -1,21 +0,0 @@ -package com.tangem.feature.swap.domain.models.cache - -import com.tangem.feature.swap.domain.models.domain.ApproveModel -import com.tangem.feature.swap.domain.models.domain.Currency -import com.tangem.feature.swap.domain.models.domain.QuoteModel -import com.tangem.feature.swap.domain.models.SwapAmount -import com.tangem.feature.swap.domain.models.domain.SwapDataModel - -data class SwapDataHolder( - val quoteModel: QuoteModel? = null, - val swapModel: SwapDataModel? = null, - val approveTxModel: ApproveModel? = null, - val amountToSwap: SwapAmount? = null, - val networkId: String? = null, - val exchangeCurrencies: ExchangeCurrencies? = null, -) - -data class ExchangeCurrencies( - val fromCurrency: Currency? = null, - val toCurrency: Currency? = null, -) \ No newline at end of file diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/ApproveModel.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/ApproveModel.kt deleted file mode 100644 index c21b52c0fa..0000000000 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/ApproveModel.kt +++ /dev/null @@ -1,14 +0,0 @@ -package com.tangem.feature.swap.domain.models.domain - -/** - * Approve model - * - * @property data The encoded data to call the approve method on the swapped token contract - * @property gasPrice Gas price for fast transaction processing - * @property toAddress Token address that will be allowed to exchange through 1inch router - */ -data class ApproveModel( - val data: String, - val gasPrice: String, - val toAddress: String, -) \ No newline at end of file diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ui/SwapState.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ui/SwapState.kt index d2b760740e..d212c8d259 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ui/SwapState.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ui/SwapState.kt @@ -2,7 +2,6 @@ package com.tangem.feature.swap.domain.models.ui import com.tangem.feature.swap.domain.models.DataError import com.tangem.feature.swap.domain.models.SwapAmount -import com.tangem.feature.swap.domain.models.domain.ApproveModel import com.tangem.feature.swap.domain.models.domain.PreparedSwapConfigState import com.tangem.feature.swap.domain.models.domain.SwapDataModel import java.math.BigDecimal @@ -59,7 +58,7 @@ data class TokenSwapInfo( data class RequestApproveStateData( val fee: TxFeeState, - val approveModel: ApproveModel, + val approveData: String, ) data class SwapStateData( diff --git a/gradle/dependencies.toml b/gradle/dependencies.toml index 89c974eda0..3ecbc0bcfb 100644 --- a/gradle/dependencies.toml +++ b/gradle/dependencies.toml @@ -82,7 +82,7 @@ okHttp-prettyLogging = "3.1.0" # endregion Other libraries # region Tangem -tangemBlockchainSdk = "develop-351" +tangemBlockchainSdk = "develop-354" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds tangemCardSdk = "develop-300" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds From 799a423f9fcb21bfef130f745d4b28f97cebb6c9 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 2 Oct 2023 15:39:32 +0400 Subject: [PATCH 123/242] Updated on 2026-08-14 --- .../wallet/presentation/common/WalletPreviewData.kt | 2 +- .../presentation/common/component/TokenItem.kt | 13 ++++++++++--- .../presentation/common/state/TokenItemState.kt | 2 +- 3 files changed, 12 insertions(+), 5 deletions(-) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/WalletPreviewData.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/WalletPreviewData.kt index 59e36af357..ba6f2ecca0 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/WalletPreviewData.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/WalletPreviewData.kt @@ -97,7 +97,7 @@ internal object WalletPreviewData { ) } - private val coinIconState + val coinIconState get() = TokenItemState.IconState.CoinIcon( url = null, fallbackResId = R.drawable.img_polygon_22, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/TokenItem.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/TokenItem.kt index 4c1514a7f0..41a7b590c4 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/TokenItem.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/TokenItem.kt @@ -1,7 +1,12 @@ package com.tangem.feature.wallet.presentation.common.component -import androidx.compose.foundation.* -import androidx.compose.foundation.layout.* +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.background +import androidx.compose.foundation.combinedClickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.defaultMinSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding import androidx.compose.runtime.* import androidx.compose.ui.Modifier import androidx.compose.ui.composed @@ -230,7 +235,9 @@ private class TokenConfigProvider : CollectionPreviewParameterProvider Date: Tue, 3 Oct 2023 10:23:30 +0300 Subject: [PATCH 124/242] Updated on 2026-08-14 --- core/res/src/main/res/values-ru/strings.xml | 4 +- .../organizetokens/OrganizeTokensScreen.kt | 2 +- .../OrganizeTokensStateHolder.kt | 11 ++- .../organizetokens/OrganizeTokensViewModel.kt | 25 ++++-- .../converter/InProgressStateConverter.kt | 6 ++ .../utils/dnd/DragAndDropAdapter.kt | 77 +++++++++++++------ 6 files changed, 85 insertions(+), 40 deletions(-) diff --git a/core/res/src/main/res/values-ru/strings.xml b/core/res/src/main/res/values-ru/strings.xml index ae7cbf8d96..7804c88500 100644 --- a/core/res/src/main/res/values-ru/strings.xml +++ b/core/res/src/main/res/values-ru/strings.xml @@ -308,10 +308,10 @@ Восстановление кода доступа Идентичные карты Код доступа - Группировка + Группы По балансу Сортировка токенов - Разгруппировать + Список %1$s %2$s адрес в сети %3$s %1$s (%2$s) в сети %3$s Участвовать diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensScreen.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensScreen.kt index b47d1fd500..63b4037673 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensScreen.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensScreen.kt @@ -244,7 +244,7 @@ private fun TopBar( iconResId = R.drawable.ic_sort_24, enabled = config.isEnabled, onClick = config.onSortClick, - dimContent = !config.isSortedByBalance, + dimContent = config.isSortedByBalance, ), modifier = Modifier.weight(1f), color = TangemTheme.colors.background.primary, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensStateHolder.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensStateHolder.kt index 25598c9983..b8d2dd73a2 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensStateHolder.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensStateHolder.kt @@ -73,12 +73,11 @@ internal class OrganizeTokensStateHolder( } fun updateStateWithManualSorting(itemsState: OrganizeTokensListState) { - updateState { - copy( - header = header.copy(isSortedByBalance = false), - itemsState = itemsState, - ) - } + updateState { copy(itemsState = itemsState) } + } + + fun disableSortingByBalance() { + updateState { copy(header = header.copy(isSortedByBalance = false)) } } fun updateStateWithError(error: TokenListError) { diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensViewModel.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensViewModel.kt index 9fa199b6a1..5a1af707d2 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensViewModel.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensViewModel.kt @@ -77,11 +77,12 @@ internal class OrganizeTokensViewModel @Inject constructor( } override fun onSortClick() { + val list = tokenList ?: return + if (list.sortedBy == TokenList.SortType.BALANCE) return + analyticsEventsHandler.send(PortfolioOrganizeTokensAnalyticsEvent.ByBalance) viewModelScope.launch(dispatchers.default) { - val list = tokenList ?: return@launch - toggleTokenListSortingUseCase(list).fold( ifLeft = stateHolder::updateStateWithError, ifRight = { @@ -93,11 +94,11 @@ internal class OrganizeTokensViewModel @Inject constructor( } override fun onGroupClick() { + val list = tokenList ?: return + analyticsEventsHandler.send(PortfolioOrganizeTokensAnalyticsEvent.Group) viewModelScope.launch(dispatchers.default) { - val list = tokenList ?: return@launch - toggleTokenListGroupingUseCase(list).fold( ifLeft = stateHolder::updateStateWithError, ifRight = { @@ -166,13 +167,23 @@ internal class OrganizeTokensViewModel @Inject constructor( private fun bootstrapDragAndDropUpdates() { dragAndDropAdapter.dragAndDropUpdates .distinctUntilChanged() - .onEach { - stateHolder.updateStateWithManualSorting(it) - tokenList = tokenList?.disableSortingByBalance() + .onEach { (type, updatedListState) -> + disableSortingByBalanceIfListChanged(type) + + stateHolder.updateStateWithManualSorting(updatedListState) } .launchIn(viewModelScope) } + private fun disableSortingByBalanceIfListChanged(dragOperationType: DragAndDropAdapter.DragOperation.Type) { + if (dragOperationType !is DragAndDropAdapter.DragOperation.Type.End) return + + if (uiState.value.header.isSortedByBalance && dragOperationType.isItemsOrderChanged) { + tokenList = tokenList?.disableSortingByBalance() + stateHolder.disableSortingByBalance() + } + } + private fun createSelectedAppCurrencyFlow(): StateFlow { return getSelectedAppCurrencyUseCase() .map { maybeAppCurrency -> diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/InProgressStateConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/InProgressStateConverter.kt index 4c26e09a29..936385e8ea 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/InProgressStateConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/InProgressStateConverter.kt @@ -10,6 +10,9 @@ internal class InProgressStateConverter : TwoWayConverter = MutableSharedFlow( - replay = 1, - onBufferOverflow = BufferOverflow.DROP_OLDEST, - ) + private val dragAndDropUpdatesInternal: MutableStateFlow = MutableStateFlow(value = null) - private var currentDraggingItem: DraggableItem? = null + private var draggingItem: DraggableItem? = null + private var draggingListState: OrganizeTokensListState? = null - val dragAndDropUpdates: SharedFlow - get() = dragAndDropUpdatesInternal + val dragAndDropUpdates: Flow + get() = dragAndDropUpdatesInternal.filterNotNull() override fun canDragItemOver(dragOver: ItemPosition, dragging: ItemPosition): Boolean { - val items = when (val listState = currentListState) { + val items = when (val listState = externalListState) { is OrganizeTokensListState.GroupedByNetwork -> listState.items is OrganizeTokensListState.Empty, is OrganizeTokensListState.Ungrouped, @@ -58,10 +56,10 @@ internal class DragAndDropAdapter( } override fun onItemDraggingStart(item: DraggableItem) { - if (currentDraggingItem != null) return - currentDraggingItem = item + if (draggingItem != null) return + draggingItem = item - updateListState { + updateListState(DragOperation.Type.Start) { when (item) { is DraggableItem.Placeholder -> items is DraggableItem.GroupHeader -> draggableGroupsOperations.collapseGroup(items, item) @@ -72,12 +70,14 @@ internal class DragAndDropAdapter( } } } + + draggingListState = externalListState } override fun onItemDraggingEnd() { - val draggingItem = currentDraggingItem ?: return + val draggingItem = draggingItem ?: return - updateListState { + updateListState(DragOperation.Type.End(isItemsOrderChanged = checkIsItemsOrderChanged())) { when (draggingItem) { is DraggableItem.GroupHeader -> draggableGroupsOperations.expandGroups(items) is DraggableItem.Token -> items.uniteItems() @@ -85,19 +85,21 @@ internal class DragAndDropAdapter( } } - currentDraggingItem = null + this.draggingItem = null } - override fun onItemDragged(from: ItemPosition, to: ItemPosition) = updateListState { - items.mutate { - it.add(to.index, it.removeAt(from.index)) + override fun onItemDragged(from: ItemPosition, to: ItemPosition) { + updateListState(DragOperation.Type.Dragged) { + items.mutate { + it.add(to.index, it.removeAt(from.index)) + } } } - private fun updateListState(block: OrganizeTokensListState.() -> List) { - val updatedState = currentListState.updateItems { block(currentListState) } + private fun updateListState(type: DragOperation.Type, block: OrganizeTokensListState.() -> List) { + val updatedState = externalListState.updateItems { block(externalListState) } - dragAndDropUpdatesInternal.tryEmit(updatedState) + dragAndDropUpdatesInternal.value = DragOperation(type, updatedState) } private fun findItemsToMove( @@ -145,4 +147,31 @@ internal class DragAndDropAdapter( is DraggableItem.Placeholder -> false } } + + private fun checkIsItemsOrderChanged(): Boolean { + fun OrganizeTokensListState?.getItemsIds(): List? = this?.items?.mapNotNull { item -> + if (item is DraggableItem.Placeholder) { + null + } else { + item.id + } + } + + return externalListState.getItemsIds() != draggingListState.getItemsIds() + } + + data class DragOperation( + val type: Type, + val listState: OrganizeTokensListState, + ) { + + sealed class Type { + + object Start : Type() + + object Dragged : Type() + + data class End(val isItemsOrderChanged: Boolean) : Type() + } + } } \ No newline at end of file From 94a3d2bb2dc321b5b8bbe78760a6ffe50164a7be Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 2 Oct 2023 22:50:15 +0800 Subject: [PATCH 125/242] Updated on 2026-08-14 --- .../wallet/viewmodels/WalletViewModel.kt | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt index f0c38778bf..d7176b704d 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt @@ -323,7 +323,7 @@ internal class WalletViewModel @Inject constructor( .ifEmpty { return } viewModelScope.launch(dispatchers.io) { - derivePublicKeysUseCase(cardId = null, derivations = derivations) + derivePublicKeysUseCase(cardId = scanResponse.card.cardId, derivations = derivations) .onRight { val newDerivedKeys = it.entries val oldDerivedKeys = scanResponse.derivedKeys @@ -488,12 +488,23 @@ internal class WalletViewModel @Inject constructor( // Reset the job to avoid a redundant state updating onWalletChangeJobHolder.update(null) + /* + * When wallet is changed it's necessary to stop the last jobs. + * If jobs aren't stopped and wallet is changed then it will update state for the prev wallet. + */ + tokensJobHolder.update(job = null) + marketPriceJobHolder.update(job = null) + buttonsJobHolder.update(job = null) + notificationsJobHolder.update(job = null) + refreshContentJobHolder.update(job = null) + viewModelScope.launch(dispatchers.main) { + val userWalletId = state.walletsListConfig.wallets[index].id withContext(dispatchers.io) { - selectWalletUseCase(userWalletId = state.walletsListConfig.wallets[index].id) + selectWalletUseCase(userWalletId = userWalletId) } - val cacheState = WalletStateCache.getState(userWalletId = state.walletsListConfig.wallets[index].id) + val cacheState = WalletStateCache.getState(userWalletId = userWalletId) if (cacheState != null && cacheState !is WalletLockedState) { uiState = cacheState.copySealed( walletsListConfig = state.walletsListConfig.copy( From a285866cab4ef01300549dd205eb1c4892896364 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 29 Sep 2023 18:44:50 +0800 Subject: [PATCH 126/242] Updated on 2026-08-14 --- .../common/component/token/LockedRectangle.kt | 2 +- .../common/component/token/icon/TokenIcon.kt | 2 +- .../components/WalletBottomSheetConfig.kt | 4 - .../state/components/WalletTokensListState.kt | 7 +- .../ui/components/common/WalletBottomSheet.kt | 24 ++---- .../multicurrency/MultiCurrencyContent.kt | 11 +-- .../wallet/ui/utils/ChangeWalletAnimator.kt | 83 +++++++++++++++++++ .../ui/utils/WalletContentItemAnimator.kt | 76 ----------------- 8 files changed, 98 insertions(+), 111 deletions(-) create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/utils/ChangeWalletAnimator.kt delete mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/utils/WalletContentItemAnimator.kt diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/token/LockedRectangle.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/token/LockedRectangle.kt index cf31ecffca..238d2ebb0d 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/token/LockedRectangle.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/token/LockedRectangle.kt @@ -11,7 +11,7 @@ import com.tangem.core.ui.res.TangemTheme internal fun LockedRectangle(modifier: Modifier = Modifier) { Box( modifier = modifier.background( - color = TangemTheme.colors.background.secondary, + color = TangemTheme.colors.field.primary, shape = RoundedCornerShape(TangemTheme.dimens.radius4), ), ) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/token/icon/TokenIcon.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/token/icon/TokenIcon.kt index 9a5cd1bee0..fce5631f81 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/token/icon/TokenIcon.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/token/icon/TokenIcon.kt @@ -52,7 +52,7 @@ private fun LockedIcon(modifier: Modifier = Modifier) { modifier = Modifier .matchParentSize() .background( - color = TangemTheme.colors.background.secondary, + color = TangemTheme.colors.field.primary, shape = CircleShape, ), ) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/components/WalletBottomSheetConfig.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/components/WalletBottomSheetConfig.kt index 44d45cf615..271da5128b 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/components/WalletBottomSheetConfig.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/components/WalletBottomSheetConfig.kt @@ -1,12 +1,10 @@ package com.tangem.feature.wallet.presentation.wallet.state.components import androidx.annotation.DrawableRes -import androidx.compose.ui.graphics.Color import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.wrappedList -import com.tangem.core.ui.res.TangemColorPalette import com.tangem.feature.wallet.impl.R /** @@ -18,7 +16,6 @@ sealed class WalletBottomSheetConfig( open val title: TextReference, open val subtitle: TextReference, @DrawableRes open val iconResId: Int, - open val tint: Color? = null, val primaryButtonConfig: ButtonConfig, val secondaryButtonConfig: ButtonConfig, ) : TangemBottomSheetConfigContent { @@ -38,7 +35,6 @@ sealed class WalletBottomSheetConfig( ), ), iconResId = R.drawable.ic_locked_24, - tint = TangemColorPalette.Black, primaryButtonConfig = ButtonConfig( text = resourceReference(id = R.string.user_wallet_list_unlock_all), onClick = onUnlockClick, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/components/WalletTokensListState.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/components/WalletTokensListState.kt index b81f513cc0..fe750ba585 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/components/WalletTokensListState.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/components/WalletTokensListState.kt @@ -33,10 +33,7 @@ internal sealed class WalletTokensListState { * @property items content items */ data class Loading( - override val items: ImmutableList = persistentListOf( - TokensListItemState.Token(state = TokenItemState.Loading(id = FIRST_LOADING_TOKEN_ID)), - TokensListItemState.Token(state = TokenItemState.Loading(id = SECOND_LOADING_TOKEN_ID)), - ), + override val items: ImmutableList = persistentListOf(), ) : ContentState(items = items, organizeTokensButton = OrganizeTokensButtonState.Hidden) /** @@ -107,8 +104,6 @@ internal sealed class WalletTokensListState { } private companion object { - const val FIRST_LOADING_TOKEN_ID = "Loading#1" - const val SECOND_LOADING_TOKEN_ID = "Loading#2" const val LOCKED_TOKEN_ID = "Locked#1" } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletBottomSheet.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletBottomSheet.kt index 8794acf686..a5e778dce6 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletBottomSheet.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletBottomSheet.kt @@ -1,6 +1,5 @@ package com.tangem.feature.wallet.presentation.wallet.ui.components.common -import androidx.compose.foundation.Image import androidx.compose.foundation.layout.* import androidx.compose.material3.* import androidx.compose.runtime.Composable @@ -47,21 +46,14 @@ private fun BottomSheetContent(config: WalletBottomSheetConfig) { verticalArrangement = Arrangement.spacedBy(space = TangemTheme.dimens.spacing40), horizontalAlignment = Alignment.CenterHorizontally, ) { - val iconTint = config.tint - if (iconTint != null) { - Icon( - painter = painterResource(id = config.iconResId), - contentDescription = null, - modifier = Modifier.size(size = TangemTheme.dimens.size48), - tint = iconTint, - ) - } else { - Image( - painter = painterResource(id = config.iconResId), - contentDescription = null, - modifier = Modifier.size(size = TangemTheme.dimens.size48), - ) - } + Icon( + painter = painterResource(id = config.iconResId), + contentDescription = null, + modifier = Modifier.size(size = TangemTheme.dimens.size48), + tint = when (config) { + is WalletBottomSheetConfig.UnlockWallets -> TangemTheme.colors.icon.primary1 + }, + ) Text( text = config.title.resolveReference(), diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/multicurrency/MultiCurrencyContent.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/multicurrency/MultiCurrencyContent.kt index 1330515bf4..9bdaf10b43 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/multicurrency/MultiCurrencyContent.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/multicurrency/MultiCurrencyContent.kt @@ -37,7 +37,6 @@ internal fun LazyListScope.tokensListItems(state: WalletTokensListState, modifie } } -@OptIn(ExperimentalFoundationApi::class) private fun LazyListScope.contentItems( items: ImmutableList, modifier: Modifier = Modifier, @@ -49,12 +48,10 @@ private fun LazyListScope.contentItems( itemContent = { index, item -> MultiCurrencyContentItem( state = item, - modifier = modifier - .animateItemPlacement() - .roundedShapeItemDecoration( - currentIndex = index, - lastIndex = items.lastIndex, - ), + modifier = modifier.roundedShapeItemDecoration( + currentIndex = index, + lastIndex = items.lastIndex, + ), ) }, ) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/utils/ChangeWalletAnimator.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/utils/ChangeWalletAnimator.kt new file mode 100644 index 0000000000..d211e051f1 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/utils/ChangeWalletAnimator.kt @@ -0,0 +1,83 @@ +package com.tangem.feature.wallet.presentation.wallet.ui.utils + +import androidx.compose.animation.core.animateDpAsState +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.foundation.layout.absoluteOffset +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.runtime.* +import androidx.compose.ui.Modifier +import androidx.compose.ui.composed +import androidx.compose.ui.draw.alpha +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.coerceIn +import androidx.compose.ui.unit.dp +import kotlin.math.sin + +private val minOffset = 0.dp +private val maxOffset = 40.dp + +private const val MIN_ALPHA = 0f +private const val MAX_ALPHA = 1f + +private const val PI = Math.PI.toFloat() +private const val HALF_PI = PI / 2 +private const val DOUBLE_PI = 2 * PI + +private const val Y_AXIS_OFFSET = 0.5f +private const val AMPLITUDE_MULTIPLIER = 0.5f + +/** + * Modifier extension for setting [absoluteOffset] and [alpha] animations of wallet change + * + * @param lazyListState lazy list state + * +[REDACTED_AUTHOR] + */ +internal fun Modifier.changeWalletAnimator(lazyListState: LazyListState) = composed { + val offset by remember(lazyListState) { derivedStateOf { lazyListState.firstVisibleItemScrollOffset } } + + val size by remember(lazyListState) { + derivedStateOf { lazyListState.layoutInfo.visibleItemsInfo.firstOrNull()?.size ?: 1 } + } + + val contentOffsetY by rememberOffsetY(offset = offset, size = size) + val contentAlpha by rememberAlpha(offset = offset, size = size) + + val animatedOffsetY by animateDpAsState(targetValue = contentOffsetY, label = "offsetY") + val animatedContentAlpha by animateFloatAsState(targetValue = contentAlpha, label = "alpha") + + this + .absoluteOffset(y = animatedOffsetY) + .alpha(alpha = animatedContentAlpha) +} + +/** + * y = ANIMATION_MAX_OFFSET_IN_DP.dp * sin(π / size * offset) + * + * @see Graphic + */ +@Composable +private fun rememberOffsetY(offset: Int, size: Int): State { + return remember(offset) { + derivedStateOf { + val y = maxOffset * sin(x = PI / size * offset) + y.coerceIn(minimumValue = minOffset, maximumValue = maxOffset) + } + } +} + +/** + * y = 0.5 * sin(2 * pi / size * offset + pi / 2) + 0.5 + * + * @see Graphic + */ +@Composable +private fun rememberAlpha(offset: Int, size: Int): State { + return remember(offset) { + derivedStateOf { + val y = AMPLITUDE_MULTIPLIER * sin(x = DOUBLE_PI / size * offset + HALF_PI) + Y_AXIS_OFFSET + y.coerceIn(MIN_ALPHA, MAX_ALPHA) + } + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/utils/WalletContentItemAnimator.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/utils/WalletContentItemAnimator.kt deleted file mode 100644 index 61a190a01f..0000000000 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/utils/WalletContentItemAnimator.kt +++ /dev/null @@ -1,76 +0,0 @@ -package com.tangem.feature.wallet.presentation.wallet.ui.utils - -import androidx.compose.animation.core.animateDpAsState -import androidx.compose.animation.core.animateFloatAsState -import androidx.compose.foundation.layout.absoluteOffset -import androidx.compose.foundation.lazy.LazyListState -import androidx.compose.runtime.derivedStateOf -import androidx.compose.runtime.getValue -import androidx.compose.runtime.remember -import androidx.compose.ui.Modifier -import androidx.compose.ui.composed -import androidx.compose.ui.draw.alpha -import androidx.compose.ui.unit.dp - -private const val ANIMATION_MAX_OFFSET_IN_DP = 80 -private const val ANIMATION_MAX_ALPHA = 1f - -/** - * Modifier extension for setting [absoluteOffset] and [alpha] animations of wallet change - * - * @param lazyListState lazy list state - * -[REDACTED_AUTHOR] - */ -internal fun Modifier.changeWalletAnimator(lazyListState: LazyListState) = composed { - val walletItemOffset by remember(lazyListState) { derivedStateOf { lazyListState.firstVisibleItemScrollOffset } } - - val walletItemSize by remember(lazyListState) { - derivedStateOf { lazyListState.layoutInfo.visibleItemsInfo.firstOrNull()?.size ?: 1 } - } - - val walletHalfItemSize by remember { - derivedStateOf { walletItemSize / 2 } - } - - val contentOffsetY by remember { - derivedStateOf { - /* - * Until [walletItemOffset] is less than [walletHalfItemSize], - * then content offset animation has positive value (downward movement). - * Otherwise, it has negative value (upward movement). - */ - val position = if (walletItemOffset < walletHalfItemSize) { - walletItemOffset - } else { - walletItemSize - walletItemOffset - } - - ANIMATION_MAX_OFFSET_IN_DP.dp / walletItemSize * position - } - } - - val contentAlpha by remember { - derivedStateOf { - /* - * Until [walletItemOffset] is less than [walletHalfItemSize], - * then content alpha animation has negative value (fade out). - * Otherwise, it has positive value (fade in). - */ - val position = if (walletItemOffset < walletHalfItemSize) { - walletHalfItemSize - walletItemOffset - } else { - walletItemOffset - walletHalfItemSize - } - - ANIMATION_MAX_ALPHA / walletHalfItemSize * position - } - } - - val animatedOffsetY by animateDpAsState(targetValue = contentOffsetY) - val animatedContentAlpha by animateFloatAsState(targetValue = contentAlpha) - - this - .absoluteOffset(y = animatedOffsetY) - .alpha(alpha = animatedContentAlpha) -} \ No newline at end of file From 79e262a069ee84e7a0fcb402103a9c9c0c179009 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 2 Oct 2023 16:24:48 +0400 Subject: [PATCH 127/242] Updated on 2026-08-14 --- .../presentation/common/WalletPreviewData.kt | 3 ++- .../presentation/common/component/TokenItem.kt | 2 +- .../common/component/token/icon/TokenIcon.kt | 2 +- .../common/state/TokenItemState.kt | 18 +++++++++--------- .../CryptoCurrencyToIconStateConverter.kt | 5 +++-- 5 files changed, 16 insertions(+), 14 deletions(-) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/WalletPreviewData.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/WalletPreviewData.kt index ba6f2ecca0..a0cfeb80f9 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/WalletPreviewData.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/WalletPreviewData.kt @@ -102,7 +102,7 @@ internal object WalletPreviewData { url = null, fallbackResId = R.drawable.img_polygon_22, isGrayscale = false, - isCustom = false, + showCustomBadge = false, ) private val tokenIconState @@ -112,6 +112,7 @@ internal object WalletPreviewData { fallbackTint = TangemColorPalette.Black, fallbackBackground = TangemColorPalette.Meadow, isGrayscale = false, + showCustomBadge = false, ) private val customTokenIconState diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/TokenItem.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/TokenItem.kt index 41a7b590c4..010ee08900 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/TokenItem.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/TokenItem.kt @@ -236,7 +236,7 @@ private class TokenConfigProvider : CollectionPreviewParameterProvider Date: Wed, 4 Oct 2023 14:35:13 +0500 Subject: [PATCH 128/242] Updated on 2026-08-14 --- .../tap/features/send/redux/reducers/AmountReducer.kt | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/tangem/tap/features/send/redux/reducers/AmountReducer.kt b/app/src/main/java/com/tangem/tap/features/send/redux/reducers/AmountReducer.kt index fd1fe275e7..1f3b82b267 100644 --- a/app/src/main/java/com/tangem/tap/features/send/redux/reducers/AmountReducer.kt +++ b/app/src/main/java/com/tangem/tap/features/send/redux/reducers/AmountReducer.kt @@ -82,7 +82,11 @@ class AmountReducer : SendInternalReducer { is AmountAction.SetAmountError -> state.copy(error = action.error) is AmountAction.SetDecimalSeparator -> state.copy(decimalSeparator = action.separator) is AmountAction.HideBalance -> { - val rescaledBalance = sendState.convertExtractCryptoToFiat(state.balanceCrypto, true) + val rescaledBalance = if (state.mainCurrency.type == MainCurrencyType.CRYPTO) { + state.balanceCrypto + } else { + sendState.convertExtractCryptoToFiat(state.balanceCrypto, true) + } state.copy( hideBalance = action.hide, From 2d1f7a0cd2abd7190e258f119b432a9e2ee5e27e Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 2 Oct 2023 19:26:30 +0300 Subject: [PATCH 129/242] Updated on 2026-08-14 --- app/build.gradle.kts | 1 + .../java/com/tangem/tap/di/ActivityModule.kt | 10 ++++ .../main/java/com/tangem/tap/di/Qualifiers.kt | 8 +++ .../tap/di/domain/TokensDomainModule.kt | 10 ++++ .../send/redux/middlewares/SendMiddleware.kt | 17 +++++- .../send/redux/reducers/SendScreenReducer.kt | 1 + .../features/send/redux/states/SendState.kt | 1 + .../tap/features/send/ui/SendFragment.kt | 1 + .../tap/features/send/ui/SendViewModel.kt | 38 +++++++++++- .../stateSubscribers/SendStateSubscriber.kt | 13 +++- .../middlewares/TradeCryptoMiddleware.kt | 8 ++- .../UpdateDelayedNetworkStatusUseCase.kt | 59 +++++++++++++++++++ features/send/api/.gitignore | 1 + features/send/api/build.gradle.kts | 17 ++++++ .../features/send/navigation/SendRouter.kt | 12 ++++ features/tokendetails/impl/build.gradle.kts | 1 + features/wallet/impl/build.gradle.kts | 1 + settings.gradle.kts | 2 + 18 files changed, 193 insertions(+), 8 deletions(-) create mode 100644 app/src/main/java/com/tangem/tap/di/Qualifiers.kt create mode 100644 domain/tokens/src/main/kotlin/com/tangem/domain/tokens/UpdateDelayedNetworkStatusUseCase.kt create mode 100644 features/send/api/.gitignore create mode 100644 features/send/api/build.gradle.kts create mode 100644 features/send/api/src/main/kotlin/com/tangem/features/send/navigation/SendRouter.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index e371e05751..23d6214b10 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -80,6 +80,7 @@ dependencies { implementation(projects.features.wallet.impl) implementation(projects.features.tokendetails.api) implementation(projects.features.tokendetails.impl) + implementation(projects.features.send.api) /** AndroidX libraries */ implementation(deps.androidx.core.ktx) diff --git a/app/src/main/java/com/tangem/tap/di/ActivityModule.kt b/app/src/main/java/com/tangem/tap/di/ActivityModule.kt index 67e752053c..2ff65f28a2 100644 --- a/app/src/main/java/com/tangem/tap/di/ActivityModule.kt +++ b/app/src/main/java/com/tangem/tap/di/ActivityModule.kt @@ -13,6 +13,9 @@ import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.components.SingletonComponent +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob import javax.inject.Singleton @Module @@ -47,4 +50,11 @@ internal object ActivityModule { fun provideDefaultRampManager(appStateHolder: AppStateHolder): RampStateManager { return DefaultRampManager(appStateHolder.exchangeService) } + + @Provides + @Singleton + @DelayedWork + fun provideActivityDelayedWorkCoroutineScope(): CoroutineScope { + return CoroutineScope(SupervisorJob() + Dispatchers.IO) + } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/di/Qualifiers.kt b/app/src/main/java/com/tangem/tap/di/Qualifiers.kt new file mode 100644 index 0000000000..df7fab7779 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/di/Qualifiers.kt @@ -0,0 +1,8 @@ +@file:Suppress("Filename") +package com.tangem.tap.di + +import javax.inject.Qualifier + +@Qualifier +@Retention(AnnotationRetention.BINARY) +annotation class DelayedWork \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt index b2f6fa50eb..9bbf1229fa 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt @@ -173,4 +173,14 @@ internal object TokensDomainModule { currenciesRepository = currenciesRepository, ) } + + @Provides + @ViewModelScoped + fun provideUpdateDelayedCurrencyStatusUseCase( + networksRepository: NetworksRepository, + ): UpdateDelayedNetworkStatusUseCase { + return UpdateDelayedNetworkStatusUseCase( + networksRepository = networksRepository, + ) + } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/send/redux/middlewares/SendMiddleware.kt b/app/src/main/java/com/tangem/tap/features/send/redux/middlewares/SendMiddleware.kt index eeb7c0cf20..4e0490a4f9 100644 --- a/app/src/main/java/com/tangem/tap/features/send/redux/middlewares/SendMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/send/redux/middlewares/SendMiddleware.kt @@ -270,9 +270,7 @@ private fun sendTransaction( dispatch(NavigationAction.PopBackTo()) } scope.launch(Dispatchers.IO) { - updateWallet(walletManager) - delay(timeMillis = 11000) // more than 10000 to avoid throttling - updateWallet(walletManager) + updateAfterTransaction(walletManager) } } is SimpleResult.Failure -> { @@ -414,6 +412,19 @@ private fun updateWarnings(dispatch: (Action) -> Unit) { dispatch(SendAction.Warnings.Set(warnings)) } +private suspend fun updateAfterTransaction(walletManager: WalletManager) { + val walletFeatureToggles = store.state.daggerGraphState.get(DaggerGraphState::walletFeatureToggles) + if (!walletFeatureToggles.isRedesignedScreenEnabled) { + updateWalletsLegacy(walletManager) + } +} + +private suspend fun updateWalletsLegacy(walletManager: WalletManager) { + updateWallet(walletManager) + delay(timeMillis = 11000) // more than 10000 to avoid throttling + updateWallet(walletManager) +} + private suspend fun updateWallet(walletManager: WalletManager) { val selectedUserWallet = userWalletsListManager.selectedUserWalletSync.guard { Timber.e("Unable to update wallet, no user wallet selected") diff --git a/app/src/main/java/com/tangem/tap/features/send/redux/reducers/SendScreenReducer.kt b/app/src/main/java/com/tangem/tap/features/send/redux/reducers/SendScreenReducer.kt index 8a9cab2329..f2d05ab0c2 100644 --- a/app/src/main/java/com/tangem/tap/features/send/redux/reducers/SendScreenReducer.kt +++ b/app/src/main/java/com/tangem/tap/features/send/redux/reducers/SendScreenReducer.kt @@ -53,6 +53,7 @@ private class SendReducer : SendInternalReducer { is SendAction.Dialog.Hide -> sendState.copy(dialog = null) is SendAction.Warnings.Set -> sendState.copy(sendWarningsList = action.warningList) is SendAction.SendSpecificTransaction -> handleSendSpecificTransactionAction(action, sendState) + is SendAction.SendSuccess -> sendState.copy(isSuccessSend = true) else -> return sendState } diff --git a/app/src/main/java/com/tangem/tap/features/send/redux/states/SendState.kt b/app/src/main/java/com/tangem/tap/features/send/redux/states/SendState.kt index fa9aa1d60e..34553ec931 100644 --- a/app/src/main/java/com/tangem/tap/features/send/redux/states/SendState.kt +++ b/app/src/main/java/com/tangem/tap/features/send/redux/states/SendState.kt @@ -43,6 +43,7 @@ data class SendState( val sendButtonState: IndeterminateProgressButton = IndeterminateProgressButton(ButtonState.DISABLED), val dialog: StateDialog? = null, val externalTransactionData: ExternalTransactionData? = null, + val isSuccessSend: Boolean = false, ) : SendScreenState { override val stateId: StateId = StateId.SEND_SCREEN diff --git a/app/src/main/java/com/tangem/tap/features/send/ui/SendFragment.kt b/app/src/main/java/com/tangem/tap/features/send/ui/SendFragment.kt index 5da7c2fe6a..e115eff66d 100644 --- a/app/src/main/java/com/tangem/tap/features/send/ui/SendFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/send/ui/SendFragment.kt @@ -76,6 +76,7 @@ class SendFragment : BaseStoreFragment(R.layout.fragment_send) { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) lifecycle.addObserver(viewModel) + sendSubscriber.initViewModel(viewModel) Analytics.send(Token.Send.ScreenOpened()) } diff --git a/app/src/main/java/com/tangem/tap/features/send/ui/SendViewModel.kt b/app/src/main/java/com/tangem/tap/features/send/ui/SendViewModel.kt index 1ac28bda46..245d0f9a89 100644 --- a/app/src/main/java/com/tangem/tap/features/send/ui/SendViewModel.kt +++ b/app/src/main/java/com/tangem/tap/features/send/ui/SendViewModel.kt @@ -3,23 +3,39 @@ package com.tangem.tap.features.send.ui import androidx.lifecycle.* import com.tangem.domain.balancehiding.IsBalanceHiddenUseCase import com.tangem.domain.balancehiding.ListenToFlipsUseCase +import com.tangem.domain.tokens.UpdateDelayedNetworkStatusUseCase +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.wallets.usecase.GetSelectedWalletUseCase +import com.tangem.features.send.navigation.SendRouter +import com.tangem.tap.di.DelayedWork import com.tangem.tap.features.send.redux.AmountAction import com.tangem.tap.proxy.AppStateHolder import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.flow.* +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +import timber.log.Timber import javax.inject.Inject +@Suppress("LongParameterList") @HiltViewModel internal class SendViewModel @Inject constructor( private val dispatchers: CoroutineDispatcherProvider, private val appStateHolder: AppStateHolder, private val isBalanceHiddenUseCase: IsBalanceHiddenUseCase, private val listenToFlipsUseCase: ListenToFlipsUseCase, + private val updateDelayedCurrencyStatusUseCase: UpdateDelayedNetworkStatusUseCase, + private val getSelectedWalletUseCase: GetSelectedWalletUseCase, + @DelayedWork private val coroutineScope: CoroutineScope, + savedStateHandle: SavedStateHandle, ) : ViewModel(), DefaultLifecycleObserver { + private val cryptoCurrency: CryptoCurrency? = savedStateHandle[SendRouter.CRYPTO_CURRENCY_KEY] + override fun onCreate(owner: LifecycleOwner) { isBalanceHiddenUseCase() .flowWithLifecycle(owner.lifecycle) @@ -36,4 +52,24 @@ internal class SendViewModel @Inject constructor( .collect() } } + + fun updateCurrencyDelayed() { + if (cryptoCurrency != null) { + coroutineScope.launch { + getSelectedWalletUseCase() + .fold( + ifLeft = { Timber.e(it.toString()) }, + ifRight = { wallet -> + updateDelayedCurrencyStatusUseCase(wallet.walletId, cryptoCurrency.network, true) + }, + ) + } + } else { + Timber.w("$TAG: cryptoCurrency is null, legacy flow") + } + } + + companion object { + private const val TAG = "SendViewModel" + } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/send/ui/stateSubscribers/SendStateSubscriber.kt b/app/src/main/java/com/tangem/tap/features/send/ui/stateSubscribers/SendStateSubscriber.kt index 1e44546acc..d71adc7cc8 100644 --- a/app/src/main/java/com/tangem/tap/features/send/ui/stateSubscribers/SendStateSubscriber.kt +++ b/app/src/main/java/com/tangem/tap/features/send/ui/stateSubscribers/SendStateSubscriber.kt @@ -18,6 +18,7 @@ import com.tangem.tap.features.send.redux.SendAction import com.tangem.tap.features.send.redux.states.* import com.tangem.tap.features.send.ui.FeeUiHelper import com.tangem.tap.features.send.ui.SendFragment +import com.tangem.tap.features.send.ui.SendViewModel import com.tangem.tap.features.send.ui.dialogs.* import com.tangem.tap.features.wallet.redux.ProgressState import com.tangem.tap.features.wallet.redux.utils.ROUGH_SIGN @@ -29,13 +30,23 @@ import com.tangem.wallet.R [REDACTED_AUTHOR] */ @Suppress("LargeClass") -class SendStateSubscriber(fragment: BaseStoreFragment) : FragmentStateSubscriber(fragment) { +internal class SendStateSubscriber( + fragment: BaseStoreFragment, +) : FragmentStateSubscriber(fragment) { private var dialog: Dialog? = null + private var sendViewModel: SendViewModel? = null + fun initViewModel(viewModel: SendViewModel) { + sendViewModel = viewModel + } override fun updateWithNewState(fg: BaseStoreFragment, state: SendState) { fg.view ?: return if (fg !is SendFragment) return + if (state.isSuccessSend) { + sendViewModel?.updateCurrencyDelayed() + return + } val lastChangedStates = state.lastChangedStates.toList() state.lastChangedStates.clear() diff --git a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/TradeCryptoMiddleware.kt b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/TradeCryptoMiddleware.kt index 36ab93bff5..666f1ac84e 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/TradeCryptoMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/TradeCryptoMiddleware.kt @@ -15,6 +15,7 @@ import com.tangem.domain.tokens.legacy.TradeCryptoAction import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.Network import com.tangem.feature.swap.presentation.SwapFragment +import com.tangem.features.send.navigation.SendRouter import com.tangem.tap.common.analytics.events.AnalyticsParam import com.tangem.tap.common.analytics.events.Token import com.tangem.tap.common.apptheme.MutableAppThemeModeHolder @@ -365,8 +366,8 @@ class TradeCryptoMiddleware { ) } } - - store.dispatchOnMain(NavigationAction.NavigateTo(AppScreen.Send)) + val bundle = bundleOf(SendRouter.CRYPTO_CURRENCY_KEY to currency) + store.dispatchOnMain(NavigationAction.NavigateTo(screen = AppScreen.Send, bundle = bundle)) } } @@ -415,7 +416,8 @@ class TradeCryptoMiddleware { is CryptoCurrency.Token -> error("Action.tokenStatus.currency is Token") } - store.dispatchOnMain(NavigationAction.NavigateTo(AppScreen.Send)) + val bundle = bundleOf(SendRouter.CRYPTO_CURRENCY_KEY to currency) + store.dispatchOnMain(NavigationAction.NavigateTo(screen = AppScreen.Send, bundle = bundle)) } } } \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/UpdateDelayedNetworkStatusUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/UpdateDelayedNetworkStatusUseCase.kt new file mode 100644 index 0000000000..30ff71c68f --- /dev/null +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/UpdateDelayedNetworkStatusUseCase.kt @@ -0,0 +1,59 @@ +package com.tangem.domain.tokens + +import arrow.core.Either +import arrow.core.raise.Raise +import arrow.core.raise.catch +import arrow.core.raise.either +import com.tangem.domain.tokens.error.CurrencyStatusError +import com.tangem.domain.tokens.model.Network +import com.tangem.domain.tokens.repository.NetworksRepository +import com.tangem.domain.wallets.models.UserWalletId +import kotlinx.coroutines.delay + +/** + * Use case responsible for fetching currency status information, including network status + * and quotes for a given cryptocurrency. It provides methods to fetch currency status either + * by providing a specific currency ID or fetching the status of the primary currency. + * + * @param networksRepository The repository for retrieving network-related data. + */ + +class UpdateDelayedNetworkStatusUseCase( + private val networksRepository: NetworksRepository, +) { + + /** + * Fetches the status of a specific cryptocurrency for a given user wallet. + * + * @param userWalletId The ID of the user's wallet. + * @param network Network of the cryptocurrency. + * @param refresh Indicates whether to force a refresh of the status data. + * @return An [Either] representing success (Right) or an error (Left) in fetching the status. + */ + suspend operator fun invoke( + userWalletId: UserWalletId, + network: Network, + refresh: Boolean = false, + ): Either { + delay(DELAY_MILLIS) + return either { + fetchNetworkStatus(userWalletId, network, refresh) + } + } + + private suspend fun Raise.fetchNetworkStatus( + userWalletId: UserWalletId, + network: Network, + refresh: Boolean, + ) { + catch( + block = { networksRepository.getNetworkStatusesSync(userWalletId, setOf(network), refresh) }, + ) { + raise(CurrencyStatusError.DataError(it)) + } + } + + companion object { + private const val DELAY_MILLIS = 11000L + } +} \ No newline at end of file diff --git a/features/send/api/.gitignore b/features/send/api/.gitignore new file mode 100644 index 0000000000..796b96d1c4 --- /dev/null +++ b/features/send/api/.gitignore @@ -0,0 +1 @@ +/build diff --git a/features/send/api/build.gradle.kts b/features/send/api/build.gradle.kts new file mode 100644 index 0000000000..6b742b5542 --- /dev/null +++ b/features/send/api/build.gradle.kts @@ -0,0 +1,17 @@ +plugins { + alias(deps.plugins.android.library) + alias(deps.plugins.kotlin.android) + id("kotlin-parcelize") + id("configuration") +} + +android { + namespace = "com.tangem.features.send.api" +} + +dependencies { + implementation(projects.domain.tokens.models) + + /** AndroidX */ + implementation(deps.androidx.fragment.ktx) +} \ No newline at end of file diff --git a/features/send/api/src/main/kotlin/com/tangem/features/send/navigation/SendRouter.kt b/features/send/api/src/main/kotlin/com/tangem/features/send/navigation/SendRouter.kt new file mode 100644 index 0000000000..0d0676b23b --- /dev/null +++ b/features/send/api/src/main/kotlin/com/tangem/features/send/navigation/SendRouter.kt @@ -0,0 +1,12 @@ +package com.tangem.features.send.navigation + +import androidx.fragment.app.Fragment + +interface SendRouter { + + fun getEntryFragment(): Fragment + + companion object { + const val CRYPTO_CURRENCY_KEY = "send_crypto_currency" + } +} \ No newline at end of file diff --git a/features/tokendetails/impl/build.gradle.kts b/features/tokendetails/impl/build.gradle.kts index 97561c5bb1..bc68543a4d 100644 --- a/features/tokendetails/impl/build.gradle.kts +++ b/features/tokendetails/impl/build.gradle.kts @@ -66,4 +66,5 @@ dependencies { /** Feature Apis */ implementation(projects.features.tokendetails.api) + implementation(projects.features.send.api) } \ No newline at end of file diff --git a/features/wallet/impl/build.gradle.kts b/features/wallet/impl/build.gradle.kts index 6edd0059b2..b48335e242 100644 --- a/features/wallet/impl/build.gradle.kts +++ b/features/wallet/impl/build.gradle.kts @@ -69,4 +69,5 @@ dependencies { /** Feature Apis */ implementation(projects.features.wallet.api) implementation(projects.features.tokendetails.api) + implementation(projects.features.send.api) } \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index 00b22db5d1..174e29f0e4 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -91,6 +91,8 @@ include(":features:tokendetails:impl") include(":features:learn2earn:api") include(":features:learn2earn:impl") + +include(":features:send:api") // endregion Feature modules // region Domain modules From bae36dfbf5ba910c08984361d6d39bde70898b2e Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 3 Oct 2023 16:05:37 +0800 Subject: [PATCH 130/242] Updated on 2026-08-14 --- .../wallet/ui/components/common/WalletCard.kt | 48 ++++++++------ .../wallet/viewmodels/WalletViewModel.kt | 65 ++++++++----------- 2 files changed, 54 insertions(+), 59 deletions(-) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletCard.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletCard.kt index d1c6cfeb73..4cd547a875 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletCard.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletCard.kt @@ -44,7 +44,6 @@ import com.tangem.core.ui.components.ResizableText import com.tangem.core.ui.components.wallets.RenameWalletDialogContent import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resolveReference -import com.tangem.core.ui.res.TangemColorPalette import com.tangem.core.ui.res.TangemDimens import com.tangem.core.ui.res.TangemTheme import com.tangem.feature.wallet.impl.R @@ -66,6 +65,7 @@ internal fun WalletCard(state: WalletCardState, modifier: Modifier = Modifier) { name = state.title, onDeleteClick = { state.onDeleteClick(state.id) }, onRenameClick = { state.onRenameClick(state.id, it) }, + isLockedState = state is WalletCardState.LockedContent, modifier = modifier, ) { val (title, balance, additionalText, image) = createRefs() @@ -118,6 +118,7 @@ private fun CardContainer( name: String, onDeleteClick: () -> Unit, onRenameClick: (String) -> Unit, + isLockedState: Boolean, modifier: Modifier = Modifier, content: @Composable (ConstraintLayoutScope.() -> Unit), ) { @@ -132,24 +133,31 @@ private fun CardContainer( Surface( modifier = modifier .defaultMinSize(minHeight = TangemTheme.dimens.size108) - .onSizeChanged { itemHeight = with(density) { it.height.toDp() } } - .clip(shape = TangemTheme.shapes.roundedCornersXMedium) - .indication(interactionSource = interactionSource, indication = LocalIndication.current) - .pointerInput(true) { - detectTapGestures( - onLongPress = { - haptic.performHapticFeedback(HapticFeedbackType.LongPress) - isMenuVisible = true - pressOffset = DpOffset(x = it.x.toDp(), y = it.y.toDp()) - }, - onPress = { - val press = PressInteraction.Press(it) - interactionSource.emit(press) - tryAwaitRelease() - interactionSource.emit(PressInteraction.Release(press)) - }, - ) - }, + .then( + if (isLockedState) { + Modifier + } else { + Modifier + .onSizeChanged { itemHeight = with(density) { it.height.toDp() } } + .clip(shape = TangemTheme.shapes.roundedCornersXMedium) + .indication(interactionSource = interactionSource, indication = LocalIndication.current) + .pointerInput(true) { + detectTapGestures( + onLongPress = { + haptic.performHapticFeedback(HapticFeedbackType.LongPress) + isMenuVisible = true + pressOffset = DpOffset(x = it.x.toDp(), y = it.y.toDp()) + }, + onPress = { + val press = PressInteraction.Press(it) + interactionSource.emit(press) + tryAwaitRelease() + interactionSource.emit(PressInteraction.Release(press)) + }, + ) + } + }, + ), shape = TangemTheme.shapes.roundedCornersXMedium, color = TangemTheme.colors.background.primary, ) { @@ -229,7 +237,7 @@ private fun MenuItem(@StringRes textResId: Int, imageVector: ImageVector, onClic onClick = onClick, colors = MenuDefaults.itemColors( textColor = TangemTheme.colors.text.primary1, - trailingIconColor = TangemColorPalette.Dark6, + trailingIconColor = TangemTheme.colors.icon.primary1, ), ) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt index d7176b704d..8d6d27d709 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt @@ -8,7 +8,6 @@ import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.address.AddressType import com.tangem.common.Provider import com.tangem.common.card.EllipticCurve -import com.tangem.common.doOnFailure import com.tangem.common.doOnSuccess import com.tangem.common.extensions.ByteArrayKey import com.tangem.common.extensions.isZero @@ -27,7 +26,10 @@ import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.balancehiding.IsBalanceHiddenUseCase import com.tangem.domain.balancehiding.ListenToFlipsUseCase -import com.tangem.domain.card.* +import com.tangem.domain.card.DerivePublicKeysUseCase +import com.tangem.domain.card.ScanCardProcessor +import com.tangem.domain.card.SetCardWasScannedUseCase +import com.tangem.domain.card.WasCardScannedUseCase import com.tangem.domain.common.CardTypesResolver import com.tangem.domain.common.configs.CardConfig import com.tangem.domain.common.util.cardTypesResolver @@ -88,9 +90,6 @@ internal class WalletViewModel @Inject constructor( private val selectWalletUseCase: SelectWalletUseCase, private val updateWalletUseCase: UpdateWalletUseCase, private val deleteWalletUseCase: DeleteWalletUseCase, - private val getBiometricsStatusUseCase: GetBiometricsStatusUseCase, - private val setAccessCodeRequestPolicyUseCase: SetAccessCodeRequestPolicyUseCase, - private val getAccessCodeSavingStatusUseCase: GetAccessCodeSavingStatusUseCase, private val getTokenListUseCase: GetTokenListUseCase, private val fetchTokenListUseCase: FetchTokenListUseCase, private val getPrimaryCurrencyStatusUpdatesUseCase: GetPrimaryCurrencyStatusUpdatesUseCase, @@ -384,42 +383,19 @@ internal class WalletViewModel @Inject constructor( class DerivationData(val derivations: Pair>) override fun onScanToUnlockWalletClick() { - scanToUpdateSelectedWallet() - } - - private fun scanToUpdateSelectedWallet(onSuccessSave: suspend (UserWallet) -> Unit = {}) { val state = uiState as? WalletState.ContentState ?: return - - val prevRequestPolicyStatus = getBiometricsStatusUseCase() - - // Update access the code policy according access code saving status - setAccessCodeRequestPolicyUseCase(isBiometricsRequestPolicy = getAccessCodeSavingStatusUseCase()) + val lockedWallet = getWallet(index = state.walletsListConfig.selectedWalletIndex) viewModelScope.launch(dispatchers.io) { - scanCardProcessor.scan( - cardId = getWallet(state.walletsListConfig.selectedWalletIndex).cardId, - allowsRequestAccessCodeFromRepository = true, - ) + scanCardProcessor.scan() .doOnSuccess { // If card's public key is null then user wallet will be null - val userWallet = UserWalletBuilder(scanResponse = it).build() + val unlockedWallet = UserWalletBuilder(scanResponse = it).build() - if (userWallet != null) { - saveWalletUseCase(userWallet = userWallet, canOverride = true) - .onLeft { - // Rollback policy if card saving was failed - setAccessCodeRequestPolicyUseCase(prevRequestPolicyStatus) - } - .onRight { onSuccessSave(userWallet) } - } else { - // Rollback policy if card saving was failed - setAccessCodeRequestPolicyUseCase(prevRequestPolicyStatus) + if (lockedWallet.walletId == unlockedWallet?.walletId) { + saveWalletUseCase(userWallet = unlockedWallet, canOverride = true) } } - .doOnFailure { - // Rollback policy if card scanning was failed - setAccessCodeRequestPolicyUseCase(prevRequestPolicyStatus) - } } } @@ -499,12 +475,14 @@ internal class WalletViewModel @Inject constructor( refreshContentJobHolder.update(job = null) viewModelScope.launch(dispatchers.main) { - val userWalletId = state.walletsListConfig.wallets[index].id + val userWallet = state.walletsListConfig.wallets[index] withContext(dispatchers.io) { - selectWalletUseCase(userWalletId = userWalletId) + if (userWallet !is WalletCardState.LockedContent) { + selectWalletUseCase(userWalletId = userWallet.id) + } } - val cacheState = WalletStateCache.getState(userWalletId = userWalletId) + val cacheState = WalletStateCache.getState(userWalletId = userWallet.id) if (cacheState != null && cacheState !is WalletLockedState) { uiState = cacheState.copySealed( walletsListConfig = state.walletsListConfig.copy( @@ -783,11 +761,20 @@ internal class WalletViewModel @Inject constructor( override fun onDeleteClick(userWalletId: UserWalletId) { val state = uiState as? WalletState.ContentState ?: return - viewModelScope.launch(dispatchers.io) { - val either = deleteWalletUseCase(userWalletId) + deleteWalletUseCase(userWalletId) - if (state.walletsListConfig.wallets.size <= 1 && either.isRight()) onBackClick() + popBackIfAllWalletsIsLocked(wallets = state.walletsListConfig.wallets) + } + } + + private fun popBackIfAllWalletsIsLocked(wallets: List) { + val unlockedWallet = wallets.count { it !is WalletCardState.LockedContent } + + if (unlockedWallet == 1) { + router.popBackStack( + screen = if (wallets.size > 1) AppScreen.Welcome else AppScreen.Home, + ) } } From 21d70dce7cc301c2f42bb456aedaf2a10aa5f96f Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 4 Oct 2023 13:32:04 +0300 Subject: [PATCH 131/242] Updated on 2026-08-14 --- .../wallet/state/WalletAlertState.kt | 3 --- .../presentation/wallet/state/WalletEvent.kt | 2 -- .../presentation/wallet/ui/WalletAlert.kt | 20 ----------------- .../wallet/ui/WalletEventEffect.kt | 5 ----- .../wallet/viewmodels/WalletClickIntents.kt | 2 -- .../WalletNotificationsListFactory.kt | 22 ------------------- .../wallet/viewmodels/WalletViewModel.kt | 18 --------------- 7 files changed, 72 deletions(-) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletAlertState.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletAlertState.kt index 7e6692060a..be52fb2220 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletAlertState.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletAlertState.kt @@ -5,9 +5,6 @@ import com.tangem.core.ui.extensions.TextReference @Immutable internal sealed class WalletAlertState { - - data class WalletAlreadySignedHashes(val onUnderstandClick: () -> Unit) : WalletAlertState() - data class DefaultAlert( val title: TextReference, val message: TextReference, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletEvent.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletEvent.kt index 99a30916b8..6102887cd0 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletEvent.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletEvent.kt @@ -20,7 +20,5 @@ internal sealed class WalletEvent { data class CopyAddress(val address: String) : WalletEvent() - data class ShowWalletAlreadySignedHashesMessage(val onUnderstandClick: () -> Unit) : WalletEvent() - data class RateApp(val onDismissClick: () -> Unit) : WalletEvent() } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletAlert.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletAlert.kt index 74582e01c7..77374812d7 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletAlert.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletAlert.kt @@ -11,32 +11,12 @@ import com.tangem.feature.wallet.presentation.wallet.state.WalletAlertState @Composable internal fun WalletAlert(config: WalletAlertState, onDismiss: () -> Unit) { when (config) { - is WalletAlertState.WalletAlreadySignedHashes -> { - WalletAlreadySignedHashesAlert(config = config, onDismiss = onDismiss) - } is WalletAlertState.DefaultAlert -> { DefaultAlert(config = config, onDismiss = onDismiss) } } } -@Composable -private fun WalletAlreadySignedHashesAlert(config: WalletAlertState.WalletAlreadySignedHashes, onDismiss: () -> Unit) { - BasicDialog( - message = stringResource(id = R.string.alert_signed_hashes_message), - confirmButton = DialogButton( - title = stringResource(id = R.string.common_understand), - onClick = { - config.onUnderstandClick() - onDismiss() - }, - ), - onDismissDialog = onDismiss, - title = stringResource(id = R.string.warning_important_security_info, "\u26A0"), - dismissButton = DialogButton(title = stringResource(id = R.string.common_cancel), onClick = onDismiss), - ) -} - @Composable private fun DefaultAlert(config: WalletAlertState.DefaultAlert, onDismiss: () -> Unit) { val confirmButton: DialogButton diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletEventEffect.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletEventEffect.kt index 7eebc553dd..7924d2a655 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletEventEffect.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletEventEffect.kt @@ -47,11 +47,6 @@ internal fun WalletEventEffect( is WalletEvent.CopyAddress -> { clipboardManager.setText(AnnotatedString(value.address)) } - is WalletEvent.ShowWalletAlreadySignedHashesMessage -> { - onAlertConfigSet( - WalletAlertState.WalletAlreadySignedHashes(onUnderstandClick = value.onUnderstandClick), - ) - } is WalletEvent.ShowAlert -> { onAlertConfigSet( WalletAlertState.DefaultAlert( diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletClickIntents.kt index d7c3ac8444..4e9831d070 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletClickIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletClickIntents.kt @@ -17,8 +17,6 @@ internal interface WalletClickIntents { fun onBackupCardClick() - fun onMultiWalletSignedHashesNotificationClick() - fun onLikeAppClick() fun onDislikeAppClick() diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletNotificationsListFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletNotificationsListFactory.kt index 8c0fa79e8d..40dbab847b 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletNotificationsListFactory.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletNotificationsListFactory.kt @@ -137,28 +137,6 @@ internal class WalletNotificationsListFactory( element = WalletNotification.Warning.SomeNetworksUnreachable, condition = cryptoCurrencyList.hasUnreachableNetworks(), ) - - if (cardTypesResolver.isBackupForbidden()) { - addIf( - element = WalletNotification.Warning.NumberOfSignedHashesIncorrect, - condition = checkSignedHashes( - cardTypesResolver = cardTypesResolver, - isDemo = isDemo, - wasCardScanned, - ), - ) - } else { - addIf( - element = WalletNotification.Warning.MultiWalletSignedHashesIncorrect( - onClick = clickIntents::onMultiWalletSignedHashesNotificationClick, - ), - condition = checkSignedHashes( - cardTypesResolver = cardTypesResolver, - isDemo = isDemo, - wasCardScanned, - ), - ) - } } else { addIf( element = WalletNotification.Warning.NetworksUnreachable, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt index 8d6d27d709..35fd1201e5 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt @@ -406,24 +406,6 @@ internal class WalletViewModel @Inject constructor( router.openOnboardingScreen() } - override fun onMultiWalletSignedHashesNotificationClick() { - val state = uiState as? WalletState.ContentState ?: return - - uiState = stateFactory.getStateAndTriggerEvent( - state = uiState, - event = WalletEvent.ShowWalletAlreadySignedHashesMessage( - onUnderstandClick = { - viewModelScope.launch(dispatchers.main) { - setCardWasScannedUseCase( - cardId = getWallet(index = state.walletsListConfig.selectedWalletIndex).cardId, - ) - } - }, - ), - setUiState = { uiState = it }, - ) - } - override fun onLikeAppClick() { analyticsEventsHandler.send(WalletScreenAnalyticsEvent.NoticeRateAppButton(AnalyticsParam.RateApp.Liked)) uiState = stateFactory.getStateAndTriggerEvent( From 26c2805f430cc1ffdd351e99badb0f6637076f53 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 4 Oct 2023 13:39:06 +0300 Subject: [PATCH 132/242] Updated on 2026-08-14 --- .../tangem/data/card/DefaultCardRepository.kt | 10 +++------- .../state/components/WalletNotification.kt | 11 ++++------- .../wallet/viewmodels/WalletClickIntents.kt | 2 ++ .../WalletNotificationsListFactory.kt | 17 +++++++++++++---- .../wallet/viewmodels/WalletViewModel.kt | 9 +++++++++ 5 files changed, 31 insertions(+), 18 deletions(-) diff --git a/data/card/src/main/java/com/tangem/data/card/DefaultCardRepository.kt b/data/card/src/main/java/com/tangem/data/card/DefaultCardRepository.kt index 4c2cb66af4..6f4a1fb553 100644 --- a/data/card/src/main/java/com/tangem/data/card/DefaultCardRepository.kt +++ b/data/card/src/main/java/com/tangem/data/card/DefaultCardRepository.kt @@ -4,6 +4,7 @@ import com.tangem.datasource.local.card.UsedCardInfo import com.tangem.datasource.local.card.UsedCardsStore import com.tangem.domain.card.repository.CardRepository import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import com.tangem.utils.extensions.addOrReplace import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.channelFlow import kotlinx.coroutines.launch @@ -42,12 +43,7 @@ internal class DefaultCardRepository( } private fun List.updateCard(cardId: String): List { - return map { cardInfo -> - if (cardInfo.cardId == cardId) { - cardInfo.copy(isScanned = true) - } else { - cardInfo - } - } + val card = find { it.cardId == cardId } ?: UsedCardInfo(cardId = cardId, isScanned = true) + return addOrReplace(item = card.copy(isScanned = true), predicate = { it.cardId == cardId }) } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/components/WalletNotification.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/components/WalletNotification.kt index a3828feae4..3e8335d4bd 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/components/WalletNotification.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/components/WalletNotification.kt @@ -57,6 +57,7 @@ sealed class WalletNotification(val config: NotificationConfig) { subtitle: TextReference, buttonsState: NotificationConfig.ButtonsState? = null, onClick: (() -> Unit)? = null, + onCloseClick: (() -> Unit)? = null, ) : WalletNotification( config = NotificationConfig( title = title, @@ -64,6 +65,7 @@ sealed class WalletNotification(val config: NotificationConfig) { iconResId = R.drawable.img_attention_20, buttonsState = buttonsState, onClick = onClick, + onCloseClick = onCloseClick, ), ) { @@ -91,15 +93,10 @@ sealed class WalletNotification(val config: NotificationConfig) { subtitle = stringReference(value = errorMessage), ) - object NumberOfSignedHashesIncorrect : Warning( + data class NumberOfSignedHashesIncorrect(val onCloseClick: () -> Unit) : Warning( title = resourceReference(id = R.string.common_warning), subtitle = resourceReference(id = R.string.alert_card_signed_transactions), - ) - - data class MultiWalletSignedHashesIncorrect(val onClick: () -> Unit) : Warning( - title = resourceReference(id = R.string.common_warning), - subtitle = resourceReference(id = R.string.warning_signed_tx_previously), - onClick = onClick, + onCloseClick = onCloseClick, ) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletClickIntents.kt index 4e9831d070..63cfe9c54c 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletClickIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletClickIntents.kt @@ -17,6 +17,8 @@ internal interface WalletClickIntents { fun onBackupCardClick() + fun onSignedHashesNotificationCloseClick() + fun onLikeAppClick() fun onDislikeAppClick() diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletNotificationsListFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletNotificationsListFactory.kt index 40dbab847b..859b8ccf58 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletNotificationsListFactory.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletNotificationsListFactory.kt @@ -149,8 +149,14 @@ internal class WalletNotificationsListFactory( } addIf( - element = WalletNotification.Warning.NumberOfSignedHashesIncorrect, - condition = checkSignedHashes(cardTypesResolver, isDemo, wasCardScanned), + element = WalletNotification.Warning.NumberOfSignedHashesIncorrect( + onCloseClick = clickIntents::onSignedHashesNotificationCloseClick, + ), + condition = checkSignedHashes( + cardTypesResolver = cardTypesResolver, + isDemo = isDemo, + wasCardScanned = wasCardScanned, + ), ) } } @@ -171,13 +177,16 @@ internal class WalletNotificationsListFactory( ?.errorMessage } + /** + * Warning is being shown for single wallet cards only + */ private fun checkSignedHashes( cardTypesResolver: CardTypesResolver, isDemo: Boolean, wasCardScanned: Boolean, ): Boolean { - return cardTypesResolver.isReleaseFirmwareType() && cardTypesResolver.hasWalletSignedHashes() && !isDemo && - !wasCardScanned + return cardTypesResolver.isReleaseFirmwareType() && !cardTypesResolver.isTangemTwins() && + cardTypesResolver.hasWalletSignedHashes() && !isDemo && !wasCardScanned } private companion object { diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt index 35fd1201e5..3944531b1c 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt @@ -406,6 +406,15 @@ internal class WalletViewModel @Inject constructor( router.openOnboardingScreen() } + override fun onSignedHashesNotificationCloseClick() { + val state = uiState as? WalletState.ContentState ?: return + viewModelScope.launch(dispatchers.main) { + setCardWasScannedUseCase( + cardId = getWallet(index = state.walletsListConfig.selectedWalletIndex).cardId, + ) + } + } + override fun onLikeAppClick() { analyticsEventsHandler.send(WalletScreenAnalyticsEvent.NoticeRateAppButton(AnalyticsParam.RateApp.Liked)) uiState = stateFactory.getStateAndTriggerEvent( From 68503f3a51a00b9be7b77ac75d7d4d1f54261563 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 4 Oct 2023 15:11:52 +0300 Subject: [PATCH 133/242] Updated on 2026-08-14 --- .../customtoken/impl/domain/DefaultCustomTokenInteractor.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/features/customtoken/impl/domain/DefaultCustomTokenInteractor.kt b/app/src/main/java/com/tangem/tap/features/customtoken/impl/domain/DefaultCustomTokenInteractor.kt index a392516294..a74e4b8a66 100644 --- a/app/src/main/java/com/tangem/tap/features/customtoken/impl/domain/DefaultCustomTokenInteractor.kt +++ b/app/src/main/java/com/tangem/tap/features/customtoken/impl/domain/DefaultCustomTokenInteractor.kt @@ -175,7 +175,7 @@ class DefaultCustomTokenInteractor( is Currency.Blockchain -> { cryptoCurrencyFactory.createCoin( blockchain = currency.blockchain, - extraDerivationPath = null, + extraDerivationPath = currency.derivationPath, derivationStyleProvider = scanResponse.derivationStyleProvider, ) } @@ -183,7 +183,7 @@ class DefaultCustomTokenInteractor( cryptoCurrencyFactory.createToken( sdkToken = currency.token, blockchain = currency.blockchain, - extraDerivationPath = null, + extraDerivationPath = currency.derivationPath, derivationStyleProvider = scanResponse.derivationStyleProvider, ) } From 55fcdc6e8306320853bffce0d7d6680cb3834bdc Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 4 Oct 2023 22:37:13 +0300 Subject: [PATCH 134/242] Updated on 2026-08-14 --- .../ui/appsettings/AppSettingsViewModel.kt | 2 ++ .../data/common/cache/DefaultCacheRegistry.kt | 30 +++++++++++-------- .../repository/DefaultNetworksRepository.kt | 4 ++- .../repository/DefaultQuotesRepository.kt | 8 +++-- .../wallet/viewmodels/WalletViewModel.kt | 2 +- 5 files changed, 29 insertions(+), 17 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsViewModel.kt b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsViewModel.kt index aaaae0a8d1..4876447e4c 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsViewModel.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsViewModel.kt @@ -196,6 +196,8 @@ internal class AppSettingsViewModel( appCurrencyRepository .getSelectedAppCurrency() .onEach { + if (it.code == store.state.globalState.appCurrency.code) return@onEach + val fiatCurrency = with(it) { FiatCurrency(code, name, symbol) } store.dispatchWithMain(DetailsAction.AppSettings.ChangeAppCurrency(fiatCurrency)) } diff --git a/data/common/src/main/kotlin/com/tangem/data/common/cache/DefaultCacheRegistry.kt b/data/common/src/main/kotlin/com/tangem/data/common/cache/DefaultCacheRegistry.kt index 57f6e0e76f..6d6f19cd0c 100644 --- a/data/common/src/main/kotlin/com/tangem/data/common/cache/DefaultCacheRegistry.kt +++ b/data/common/src/main/kotlin/com/tangem/data/common/cache/DefaultCacheRegistry.kt @@ -2,6 +2,8 @@ package com.tangem.data.common.cache import com.tangem.datasource.local.cache.CacheKeysStore import com.tangem.datasource.local.cache.model.CacheKey +import kotlinx.coroutines.NonCancellable +import kotlinx.coroutines.withContext import org.joda.time.Duration import org.joda.time.LocalDateTime import timber.log.Timber @@ -20,17 +22,17 @@ internal class DefaultCacheRegistry( override suspend fun invalidate(key: String) { Timber.d("Invalidate the cache key: $key") - cacheKeysStore.remove(key) + withContext(NonCancellable) { cacheKeysStore.remove(key) } } override suspend fun invalidate(keys: Collection) { Timber.d("Invalidate cache keys: $keys") - cacheKeysStore.remove(keys) + withContext(NonCancellable) { cacheKeysStore.remove(keys) } } override suspend fun invalidateAll() { Timber.d("Invalidate all cache keys") - cacheKeysStore.clear() + withContext(NonCancellable) { cacheKeysStore.clear() } } override suspend fun invokeOnExpire( @@ -44,18 +46,22 @@ internal class DefaultCacheRegistry( try { Timber.d("Invoke the action associated with the cache key: $key") + + cacheKeysStore.store( + key = CacheKey( + id = key, + updatedAt = LocalDateTime.now(), + expiresIn = expireIn, + ), + ) + block() } catch (e: Throwable) { - Timber.w(e, "The action related to the cache key has failed: $key") + Timber.e(e, "The action related to the cache key has failed: $key") + + invalidate(key) + throw e } - - cacheKeysStore.store( - key = CacheKey( - id = key, - updatedAt = LocalDateTime.now(), - expiresIn = expireIn, - ), - ) } } \ No newline at end of file diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultNetworksRepository.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultNetworksRepository.kt index e1d3826183..7a8a4e726f 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultNetworksRepository.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultNetworksRepository.kt @@ -97,7 +97,9 @@ internal class DefaultNetworksRepository( extraTokens = currencies.filterIsInstance().toSet(), ) - invalidateCacheKeyIfNeeded(userWalletId, network, result) + withContext(NonCancellable) { + invalidateCacheKeyIfNeeded(userWalletId, network, result) + } val networkStatus = networkStatusFactory.createNetworkStatus( network = network, diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultQuotesRepository.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultQuotesRepository.kt index e63ea16071..e557875dc8 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultQuotesRepository.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultQuotesRepository.kt @@ -35,9 +35,11 @@ internal class DefaultQuotesRepository( } withContext(dispatchers.io) { - selectedAppCurrencyStore.get().collectLatest { appCurrency -> - fetchExpiredQuotes(currenciesIds, appCurrency.id, refresh = false) - } + selectedAppCurrencyStore.get() + .distinctUntilChanged() + .collectLatest { appCurrency -> + fetchExpiredQuotes(currenciesIds, appCurrency.id, refresh = false) + } } }.cancellable() diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt index 3944531b1c..09b9aff20b 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt @@ -298,7 +298,7 @@ internal class WalletViewModel @Inject constructor( update = { it.copy(scanResponse = scannedCardResponse) }, ) .onRight { - fetchTokenListUseCase(userWalletId = it.walletId, refresh = true) + fetchTokenListUseCase(userWalletId = it.walletId) } } } From ac295a300c94d47f0020fd5496ce7c793c21e585 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 3 Oct 2023 14:41:22 +0400 Subject: [PATCH 135/242] Updated on 2026-08-14 --- .../tangem/tap/common/CustomTabsManager.kt | 6 ++ .../drawable-night/ic_arbitrum_no_color.xml | 29 ------ .../drawable-night/ic_avalanche_no_color.xml | 17 ---- .../ic_bitcoin_cash_no_color.xml | 13 --- .../drawable-night/ic_bitcoin_no_color.xml | 16 ---- .../res/drawable-night/ic_bsc_no_color.xml | 17 ---- .../drawable-night/ic_cardano_no_color.xml | 16 ---- .../res/drawable-night/ic_chia_no_color.xml | 17 ---- .../res/drawable-night/ic_cosmos_no_color.xml | 43 --------- .../res/drawable-night/ic_cronos_no_color.xml | 25 ----- .../res/drawable-night/ic_dash_no_color.xml | 19 ---- .../drawable-night/ic_dogecoin_no_color.xml | 17 ---- .../res/drawable-night/ic_eth_no_color.xml | 31 ------ .../ic_ethereumfair_no_color.xml | 40 -------- .../ic_ethereumpow_no_color.xml | 31 ------ .../res/drawable-night/ic_fantom_no_color.xml | 17 ---- .../res/drawable-night/ic_gnosis_no_color.xml | 25 ----- .../res/drawable-night/ic_kaspa_no_color.xml | 16 ---- .../res/drawable-night/ic_kava_no_color.xml | 17 ---- .../res/drawable-night/ic_kusama_no_color.xml | 16 ---- .../drawable-night/ic_litecoin_no_color.xml | 16 ---- .../drawable-night/ic_octaspace_no_color.xml | 61 ------------ .../drawable-night/ic_optimism_no_color.xml | 17 ---- .../drawable-night/ic_polkadot_no_color.xml | 17 ---- .../drawable-night/ic_polygon_no_color.xml | 16 ---- .../drawable-night/ic_ravencoin_no_color.xml | 22 ----- .../res/drawable-night/ic_rsk_no_color.xml | 16 ---- .../res/drawable-night/ic_solana_no_color.xml | 13 --- .../drawable-night/ic_stellar_no_color.xml | 16 ---- .../res/drawable-night/ic_telos_no_color.xml | 17 ---- .../res/drawable-night/ic_terra2_no_color.xml | 25 ----- .../res/drawable-night/ic_terra_no_color.xml | 22 ----- .../res/drawable-night/ic_tezos_no_color.xml | 16 ---- .../res/drawable-night/ic_ton_no_color.xml | 17 ---- .../res/drawable-night/ic_tron_no_color.xml | 16 ---- .../res/drawable-night/ic_xrp_no_color.xml | 16 ---- .../res/drawable/card_placeholder_black.xml | 88 ------------------ .../res/drawable/ic_arbitrum_no_color.xml | 1 - .../res/drawable/ic_avalanche_no_color.xml | 15 ++- .../main/res/drawable/ic_azero_no_color.xml | 3 - .../res/drawable/ic_bitcoin_cash_no_color.xml | 1 - .../main/res/drawable/ic_bitcoin_no_color.xml | 1 - app/src/main/res/drawable/ic_bsc_no_color.xml | 1 - .../main/res/drawable/ic_cardano_no_color.xml | 1 - .../main/res/drawable/ic_chia_no_color.xml | 3 - .../main/res/drawable/ic_cosmos_no_color.xml | 3 - .../main/res/drawable/ic_cronos_no_color.xml | 3 - .../main/res/drawable/ic_dash_no_color.xml | 3 - .../res/drawable/ic_dogecoin_no_color.xml | 1 - app/src/main/res/drawable/ic_eth_no_color.xml | 1 - .../res/drawable/ic_ethereumfair_no_color.xml | 3 - .../res/drawable/ic_ethereumpow_no_color.xml | 3 - .../main/res/drawable/ic_fantom_no_color.xml | 1 - .../main/res/drawable/ic_gnosis_no_color.xml | 3 - .../main/res/drawable/ic_kaspa_no_color.xml | 3 - .../main/res/drawable/ic_kava_no_color.xml | 3 - .../main/res/drawable/ic_kusama_no_color.xml | 1 - .../res/drawable/ic_litecoin_no_color.xml | 17 ++-- .../res/drawable/ic_octaspace_no_color.xml | 3 - .../res/drawable/ic_optimism_no_color.xml | 3 - .../res/drawable/ic_polkadot_no_color.xml | 1 - .../main/res/drawable/ic_polygon_no_color.xml | 1 - .../res/drawable/ic_ravencoin_no_color.xml | 3 - app/src/main/res/drawable/ic_rsk_no_color.xml | 1 - .../main/res/drawable/ic_solana_no_color.xml | 1 - .../main/res/drawable/ic_stellar_no_color.xml | 1 - .../main/res/drawable/ic_telos_no_color.xml | 3 - .../main/res/drawable/ic_terra2_no_color.xml | 3 - .../main/res/drawable/ic_terra_no_color.xml | 3 - .../main/res/drawable/ic_tezos_no_color.xml | 1 - app/src/main/res/drawable/ic_ton_no_color.xml | 3 - .../main/res/drawable/ic_tron_no_color.xml | 1 - app/src/main/res/drawable/ic_xrp_no_color.xml | 14 ++- .../main/res/layout/layout_pseudo_toolbar.xml | 2 +- app/src/main/res/values-night/colors.xml | 1 + app/src/main/res/values/colors.xml | 2 + .../tangem/core/ui/components/TextFields.kt | 28 +----- .../card_placeholder_black.webp | Bin 0 -> 3248 bytes .../res/drawable/card_placeholder_black.webp | Bin 0 -> 3574 bytes .../res/drawable/card_placeholder_black.xml | 88 ------------------ .../com/tangem/feature/swap/ui/SwapScreen.kt | 1 + .../drawable-night/ill_one_inch_powered.webp | Bin 0 -> 3978 bytes 82 files changed, 44 insertions(+), 1030 deletions(-) delete mode 100644 app/src/main/res/drawable-night/ic_arbitrum_no_color.xml delete mode 100644 app/src/main/res/drawable-night/ic_avalanche_no_color.xml delete mode 100644 app/src/main/res/drawable-night/ic_bitcoin_cash_no_color.xml delete mode 100644 app/src/main/res/drawable-night/ic_bitcoin_no_color.xml delete mode 100644 app/src/main/res/drawable-night/ic_bsc_no_color.xml delete mode 100644 app/src/main/res/drawable-night/ic_cardano_no_color.xml delete mode 100644 app/src/main/res/drawable-night/ic_chia_no_color.xml delete mode 100644 app/src/main/res/drawable-night/ic_cosmos_no_color.xml delete mode 100644 app/src/main/res/drawable-night/ic_cronos_no_color.xml delete mode 100644 app/src/main/res/drawable-night/ic_dash_no_color.xml delete mode 100644 app/src/main/res/drawable-night/ic_dogecoin_no_color.xml delete mode 100644 app/src/main/res/drawable-night/ic_eth_no_color.xml delete mode 100644 app/src/main/res/drawable-night/ic_ethereumfair_no_color.xml delete mode 100644 app/src/main/res/drawable-night/ic_ethereumpow_no_color.xml delete mode 100644 app/src/main/res/drawable-night/ic_fantom_no_color.xml delete mode 100644 app/src/main/res/drawable-night/ic_gnosis_no_color.xml delete mode 100644 app/src/main/res/drawable-night/ic_kaspa_no_color.xml delete mode 100644 app/src/main/res/drawable-night/ic_kava_no_color.xml delete mode 100644 app/src/main/res/drawable-night/ic_kusama_no_color.xml delete mode 100644 app/src/main/res/drawable-night/ic_litecoin_no_color.xml delete mode 100644 app/src/main/res/drawable-night/ic_octaspace_no_color.xml delete mode 100644 app/src/main/res/drawable-night/ic_optimism_no_color.xml delete mode 100644 app/src/main/res/drawable-night/ic_polkadot_no_color.xml delete mode 100644 app/src/main/res/drawable-night/ic_polygon_no_color.xml delete mode 100644 app/src/main/res/drawable-night/ic_ravencoin_no_color.xml delete mode 100644 app/src/main/res/drawable-night/ic_rsk_no_color.xml delete mode 100644 app/src/main/res/drawable-night/ic_solana_no_color.xml delete mode 100644 app/src/main/res/drawable-night/ic_stellar_no_color.xml delete mode 100644 app/src/main/res/drawable-night/ic_telos_no_color.xml delete mode 100644 app/src/main/res/drawable-night/ic_terra2_no_color.xml delete mode 100644 app/src/main/res/drawable-night/ic_terra_no_color.xml delete mode 100644 app/src/main/res/drawable-night/ic_tezos_no_color.xml delete mode 100644 app/src/main/res/drawable-night/ic_ton_no_color.xml delete mode 100644 app/src/main/res/drawable-night/ic_tron_no_color.xml delete mode 100644 app/src/main/res/drawable-night/ic_xrp_no_color.xml delete mode 100644 app/src/main/res/drawable/card_placeholder_black.xml create mode 100644 core/ui/src/main/res/drawable-night/card_placeholder_black.webp create mode 100644 core/ui/src/main/res/drawable/card_placeholder_black.webp delete mode 100644 core/ui/src/main/res/drawable/card_placeholder_black.xml create mode 100644 features/swap/presentation/src/main/res/drawable-night/ill_one_inch_powered.webp diff --git a/app/src/main/java/com/tangem/tap/common/CustomTabsManager.kt b/app/src/main/java/com/tangem/tap/common/CustomTabsManager.kt index b66ed0a3ad..a913befc98 100644 --- a/app/src/main/java/com/tangem/tap/common/CustomTabsManager.kt +++ b/app/src/main/java/com/tangem/tap/common/CustomTabsManager.kt @@ -4,6 +4,9 @@ import android.content.Context import android.net.Uri import androidx.browser.customtabs.CustomTabColorSchemeParams import androidx.browser.customtabs.CustomTabsIntent +import androidx.browser.customtabs.CustomTabsIntent.COLOR_SCHEME_DARK +import androidx.browser.customtabs.CustomTabsIntent.COLOR_SCHEME_LIGHT +import com.tangem.tap.common.apptheme.MutableAppThemeModeHolder import com.tangem.tap.common.extensions.getColorCompat import com.tangem.wallet.R @@ -15,6 +18,9 @@ class CustomTabsManager { .setNavigationBarColor(context.getColorCompat(R.color.toolbarColor)) .build(), ) + .setColorScheme( + if (MutableAppThemeModeHolder.isDarkThemeActive) COLOR_SCHEME_DARK else COLOR_SCHEME_LIGHT, + ) .build() customTabsIntent.launchUrl(context, Uri.parse(url)) } diff --git a/app/src/main/res/drawable-night/ic_arbitrum_no_color.xml b/app/src/main/res/drawable-night/ic_arbitrum_no_color.xml deleted file mode 100644 index d27c75195b..0000000000 --- a/app/src/main/res/drawable-night/ic_arbitrum_no_color.xml +++ /dev/null @@ -1,29 +0,0 @@ - - - - - - - - - - - diff --git a/app/src/main/res/drawable-night/ic_avalanche_no_color.xml b/app/src/main/res/drawable-night/ic_avalanche_no_color.xml deleted file mode 100644 index 84bb62ad1e..0000000000 --- a/app/src/main/res/drawable-night/ic_avalanche_no_color.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - - diff --git a/app/src/main/res/drawable-night/ic_bitcoin_cash_no_color.xml b/app/src/main/res/drawable-night/ic_bitcoin_cash_no_color.xml deleted file mode 100644 index 501c006728..0000000000 --- a/app/src/main/res/drawable-night/ic_bitcoin_cash_no_color.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - diff --git a/app/src/main/res/drawable-night/ic_bitcoin_no_color.xml b/app/src/main/res/drawable-night/ic_bitcoin_no_color.xml deleted file mode 100644 index 84b7fa673d..0000000000 --- a/app/src/main/res/drawable-night/ic_bitcoin_no_color.xml +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - - diff --git a/app/src/main/res/drawable-night/ic_bsc_no_color.xml b/app/src/main/res/drawable-night/ic_bsc_no_color.xml deleted file mode 100644 index 7a6e69d0d0..0000000000 --- a/app/src/main/res/drawable-night/ic_bsc_no_color.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - - diff --git a/app/src/main/res/drawable-night/ic_cardano_no_color.xml b/app/src/main/res/drawable-night/ic_cardano_no_color.xml deleted file mode 100644 index 688149e946..0000000000 --- a/app/src/main/res/drawable-night/ic_cardano_no_color.xml +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - - diff --git a/app/src/main/res/drawable-night/ic_chia_no_color.xml b/app/src/main/res/drawable-night/ic_chia_no_color.xml deleted file mode 100644 index a8beb8b71e..0000000000 --- a/app/src/main/res/drawable-night/ic_chia_no_color.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - - diff --git a/app/src/main/res/drawable-night/ic_cosmos_no_color.xml b/app/src/main/res/drawable-night/ic_cosmos_no_color.xml deleted file mode 100644 index 42e2426955..0000000000 --- a/app/src/main/res/drawable-night/ic_cosmos_no_color.xml +++ /dev/null @@ -1,43 +0,0 @@ - - - - - - - - - - - diff --git a/app/src/main/res/drawable-night/ic_cronos_no_color.xml b/app/src/main/res/drawable-night/ic_cronos_no_color.xml deleted file mode 100644 index 95c7b20aa0..0000000000 --- a/app/src/main/res/drawable-night/ic_cronos_no_color.xml +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - - - - - diff --git a/app/src/main/res/drawable-night/ic_dash_no_color.xml b/app/src/main/res/drawable-night/ic_dash_no_color.xml deleted file mode 100644 index cbe04a6a95..0000000000 --- a/app/src/main/res/drawable-night/ic_dash_no_color.xml +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - - diff --git a/app/src/main/res/drawable-night/ic_dogecoin_no_color.xml b/app/src/main/res/drawable-night/ic_dogecoin_no_color.xml deleted file mode 100644 index 0c20ff8559..0000000000 --- a/app/src/main/res/drawable-night/ic_dogecoin_no_color.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - - diff --git a/app/src/main/res/drawable-night/ic_eth_no_color.xml b/app/src/main/res/drawable-night/ic_eth_no_color.xml deleted file mode 100644 index ff2ec1be37..0000000000 --- a/app/src/main/res/drawable-night/ic_eth_no_color.xml +++ /dev/null @@ -1,31 +0,0 @@ - - - - - - - - - - - - diff --git a/app/src/main/res/drawable-night/ic_ethereumfair_no_color.xml b/app/src/main/res/drawable-night/ic_ethereumfair_no_color.xml deleted file mode 100644 index 97dbf4686f..0000000000 --- a/app/src/main/res/drawable-night/ic_ethereumfair_no_color.xml +++ /dev/null @@ -1,40 +0,0 @@ - - - - - - - - - - - - - - - diff --git a/app/src/main/res/drawable-night/ic_ethereumpow_no_color.xml b/app/src/main/res/drawable-night/ic_ethereumpow_no_color.xml deleted file mode 100644 index 15e4b3efd0..0000000000 --- a/app/src/main/res/drawable-night/ic_ethereumpow_no_color.xml +++ /dev/null @@ -1,31 +0,0 @@ - - - - - - - - - - - - - - diff --git a/app/src/main/res/drawable-night/ic_fantom_no_color.xml b/app/src/main/res/drawable-night/ic_fantom_no_color.xml deleted file mode 100644 index bbb7044103..0000000000 --- a/app/src/main/res/drawable-night/ic_fantom_no_color.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - - diff --git a/app/src/main/res/drawable-night/ic_gnosis_no_color.xml b/app/src/main/res/drawable-night/ic_gnosis_no_color.xml deleted file mode 100644 index fb21b7ec5b..0000000000 --- a/app/src/main/res/drawable-night/ic_gnosis_no_color.xml +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - - - - - diff --git a/app/src/main/res/drawable-night/ic_kaspa_no_color.xml b/app/src/main/res/drawable-night/ic_kaspa_no_color.xml deleted file mode 100644 index d245591015..0000000000 --- a/app/src/main/res/drawable-night/ic_kaspa_no_color.xml +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - - diff --git a/app/src/main/res/drawable-night/ic_kava_no_color.xml b/app/src/main/res/drawable-night/ic_kava_no_color.xml deleted file mode 100644 index 52132dd8db..0000000000 --- a/app/src/main/res/drawable-night/ic_kava_no_color.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - - diff --git a/app/src/main/res/drawable-night/ic_kusama_no_color.xml b/app/src/main/res/drawable-night/ic_kusama_no_color.xml deleted file mode 100644 index d2601e7684..0000000000 --- a/app/src/main/res/drawable-night/ic_kusama_no_color.xml +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - - diff --git a/app/src/main/res/drawable-night/ic_litecoin_no_color.xml b/app/src/main/res/drawable-night/ic_litecoin_no_color.xml deleted file mode 100644 index da1b438b7e..0000000000 --- a/app/src/main/res/drawable-night/ic_litecoin_no_color.xml +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - - diff --git a/app/src/main/res/drawable-night/ic_octaspace_no_color.xml b/app/src/main/res/drawable-night/ic_octaspace_no_color.xml deleted file mode 100644 index 3483ed7be3..0000000000 --- a/app/src/main/res/drawable-night/ic_octaspace_no_color.xml +++ /dev/null @@ -1,61 +0,0 @@ - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/drawable-night/ic_optimism_no_color.xml b/app/src/main/res/drawable-night/ic_optimism_no_color.xml deleted file mode 100644 index 9a28dabb2f..0000000000 --- a/app/src/main/res/drawable-night/ic_optimism_no_color.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - - diff --git a/app/src/main/res/drawable-night/ic_polkadot_no_color.xml b/app/src/main/res/drawable-night/ic_polkadot_no_color.xml deleted file mode 100644 index cc9d9d2179..0000000000 --- a/app/src/main/res/drawable-night/ic_polkadot_no_color.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - - diff --git a/app/src/main/res/drawable-night/ic_polygon_no_color.xml b/app/src/main/res/drawable-night/ic_polygon_no_color.xml deleted file mode 100644 index bfd632c93a..0000000000 --- a/app/src/main/res/drawable-night/ic_polygon_no_color.xml +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - - diff --git a/app/src/main/res/drawable-night/ic_ravencoin_no_color.xml b/app/src/main/res/drawable-night/ic_ravencoin_no_color.xml deleted file mode 100644 index 428b851f0e..0000000000 --- a/app/src/main/res/drawable-night/ic_ravencoin_no_color.xml +++ /dev/null @@ -1,22 +0,0 @@ - - - - - - - - - diff --git a/app/src/main/res/drawable-night/ic_rsk_no_color.xml b/app/src/main/res/drawable-night/ic_rsk_no_color.xml deleted file mode 100644 index 8a1fc2a678..0000000000 --- a/app/src/main/res/drawable-night/ic_rsk_no_color.xml +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - - diff --git a/app/src/main/res/drawable-night/ic_solana_no_color.xml b/app/src/main/res/drawable-night/ic_solana_no_color.xml deleted file mode 100644 index ac1bb7c565..0000000000 --- a/app/src/main/res/drawable-night/ic_solana_no_color.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - diff --git a/app/src/main/res/drawable-night/ic_stellar_no_color.xml b/app/src/main/res/drawable-night/ic_stellar_no_color.xml deleted file mode 100644 index e87b7c5061..0000000000 --- a/app/src/main/res/drawable-night/ic_stellar_no_color.xml +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - - diff --git a/app/src/main/res/drawable-night/ic_telos_no_color.xml b/app/src/main/res/drawable-night/ic_telos_no_color.xml deleted file mode 100644 index b853488d28..0000000000 --- a/app/src/main/res/drawable-night/ic_telos_no_color.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - - diff --git a/app/src/main/res/drawable-night/ic_terra2_no_color.xml b/app/src/main/res/drawable-night/ic_terra2_no_color.xml deleted file mode 100644 index 2ac3b44b03..0000000000 --- a/app/src/main/res/drawable-night/ic_terra2_no_color.xml +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - - - - - diff --git a/app/src/main/res/drawable-night/ic_terra_no_color.xml b/app/src/main/res/drawable-night/ic_terra_no_color.xml deleted file mode 100644 index 26d8fe8e4b..0000000000 --- a/app/src/main/res/drawable-night/ic_terra_no_color.xml +++ /dev/null @@ -1,22 +0,0 @@ - - - - - - - - - diff --git a/app/src/main/res/drawable-night/ic_tezos_no_color.xml b/app/src/main/res/drawable-night/ic_tezos_no_color.xml deleted file mode 100644 index 8976ad0a0e..0000000000 --- a/app/src/main/res/drawable-night/ic_tezos_no_color.xml +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - - diff --git a/app/src/main/res/drawable-night/ic_ton_no_color.xml b/app/src/main/res/drawable-night/ic_ton_no_color.xml deleted file mode 100644 index 098b3a6883..0000000000 --- a/app/src/main/res/drawable-night/ic_ton_no_color.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - - diff --git a/app/src/main/res/drawable-night/ic_tron_no_color.xml b/app/src/main/res/drawable-night/ic_tron_no_color.xml deleted file mode 100644 index 581cf60d6a..0000000000 --- a/app/src/main/res/drawable-night/ic_tron_no_color.xml +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - - diff --git a/app/src/main/res/drawable-night/ic_xrp_no_color.xml b/app/src/main/res/drawable-night/ic_xrp_no_color.xml deleted file mode 100644 index 9c4d2e9b43..0000000000 --- a/app/src/main/res/drawable-night/ic_xrp_no_color.xml +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - - diff --git a/app/src/main/res/drawable/card_placeholder_black.xml b/app/src/main/res/drawable/card_placeholder_black.xml deleted file mode 100644 index 83fab8e0f7..0000000000 --- a/app/src/main/res/drawable/card_placeholder_black.xml +++ /dev/null @@ -1,88 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/drawable/ic_arbitrum_no_color.xml b/app/src/main/res/drawable/ic_arbitrum_no_color.xml index 02164f7121..e7c20e844b 100644 --- a/app/src/main/res/drawable/ic_arbitrum_no_color.xml +++ b/app/src/main/res/drawable/ic_arbitrum_no_color.xml @@ -3,7 +3,6 @@ android:width="22dp" xmlns:android="http://schemas.android.com/apk/res/android"> - diff --git a/app/src/main/res/drawable/ic_avalanche_no_color.xml b/app/src/main/res/drawable/ic_avalanche_no_color.xml index 72317338ab..032533db9d 100644 --- a/app/src/main/res/drawable/ic_avalanche_no_color.xml +++ b/app/src/main/res/drawable/ic_avalanche_no_color.xml @@ -1,6 +1,11 @@ - - - + + diff --git a/app/src/main/res/drawable/ic_azero_no_color.xml b/app/src/main/res/drawable/ic_azero_no_color.xml index da712895b3..612a316f3f 100644 --- a/app/src/main/res/drawable/ic_azero_no_color.xml +++ b/app/src/main/res/drawable/ic_azero_no_color.xml @@ -3,9 +3,6 @@ android:height="22dp" android:viewportWidth="22" android:viewportHeight="22"> - diff --git a/app/src/main/res/drawable/ic_bitcoin_cash_no_color.xml b/app/src/main/res/drawable/ic_bitcoin_cash_no_color.xml index 8ca8b29ddb..f9a1276588 100644 --- a/app/src/main/res/drawable/ic_bitcoin_cash_no_color.xml +++ b/app/src/main/res/drawable/ic_bitcoin_cash_no_color.xml @@ -1,6 +1,5 @@ - diff --git a/app/src/main/res/drawable/ic_bitcoin_no_color.xml b/app/src/main/res/drawable/ic_bitcoin_no_color.xml index 9b8aef2452..b18c3c9cce 100644 --- a/app/src/main/res/drawable/ic_bitcoin_no_color.xml +++ b/app/src/main/res/drawable/ic_bitcoin_no_color.xml @@ -1,6 +1,5 @@ - diff --git a/app/src/main/res/drawable/ic_bsc_no_color.xml b/app/src/main/res/drawable/ic_bsc_no_color.xml index 1fb72febdf..a7062f73c1 100644 --- a/app/src/main/res/drawable/ic_bsc_no_color.xml +++ b/app/src/main/res/drawable/ic_bsc_no_color.xml @@ -1,7 +1,6 @@ - diff --git a/app/src/main/res/drawable/ic_cardano_no_color.xml b/app/src/main/res/drawable/ic_cardano_no_color.xml index ea706040d3..46a9df7709 100644 --- a/app/src/main/res/drawable/ic_cardano_no_color.xml +++ b/app/src/main/res/drawable/ic_cardano_no_color.xml @@ -1,6 +1,5 @@ - diff --git a/app/src/main/res/drawable/ic_chia_no_color.xml b/app/src/main/res/drawable/ic_chia_no_color.xml index 9119f89479..4ed5d9574b 100644 --- a/app/src/main/res/drawable/ic_chia_no_color.xml +++ b/app/src/main/res/drawable/ic_chia_no_color.xml @@ -3,9 +3,6 @@ android:height="22dp" android:viewportWidth="22" android:viewportHeight="22"> - - - diff --git a/app/src/main/res/drawable/ic_dash_no_color.xml b/app/src/main/res/drawable/ic_dash_no_color.xml index be475ecf51..0e7c3f29b6 100644 --- a/app/src/main/res/drawable/ic_dash_no_color.xml +++ b/app/src/main/res/drawable/ic_dash_no_color.xml @@ -6,9 +6,6 @@ - diff --git a/app/src/main/res/drawable/ic_dogecoin_no_color.xml b/app/src/main/res/drawable/ic_dogecoin_no_color.xml index 8ca48b696e..9d52af3530 100644 --- a/app/src/main/res/drawable/ic_dogecoin_no_color.xml +++ b/app/src/main/res/drawable/ic_dogecoin_no_color.xml @@ -1,6 +1,5 @@ - diff --git a/app/src/main/res/drawable/ic_eth_no_color.xml b/app/src/main/res/drawable/ic_eth_no_color.xml index 95066b1560..41071ba559 100644 --- a/app/src/main/res/drawable/ic_eth_no_color.xml +++ b/app/src/main/res/drawable/ic_eth_no_color.xml @@ -1,7 +1,6 @@ - diff --git a/app/src/main/res/drawable/ic_ethereumfair_no_color.xml b/app/src/main/res/drawable/ic_ethereumfair_no_color.xml index dd68efa83e..1379276e0d 100644 --- a/app/src/main/res/drawable/ic_ethereumfair_no_color.xml +++ b/app/src/main/res/drawable/ic_ethereumfair_no_color.xml @@ -6,9 +6,6 @@ android:viewportHeight="22"> - diff --git a/app/src/main/res/drawable/ic_ethereumpow_no_color.xml b/app/src/main/res/drawable/ic_ethereumpow_no_color.xml index aa369fd0ab..40a0651a32 100644 --- a/app/src/main/res/drawable/ic_ethereumpow_no_color.xml +++ b/app/src/main/res/drawable/ic_ethereumpow_no_color.xml @@ -6,9 +6,6 @@ android:viewportHeight="22"> - - diff --git a/app/src/main/res/drawable/ic_gnosis_no_color.xml b/app/src/main/res/drawable/ic_gnosis_no_color.xml index a8d4069093..271d24efd6 100644 --- a/app/src/main/res/drawable/ic_gnosis_no_color.xml +++ b/app/src/main/res/drawable/ic_gnosis_no_color.xml @@ -3,9 +3,6 @@ android:height="22dp" android:viewportWidth="22" android:viewportHeight="22"> - diff --git a/app/src/main/res/drawable/ic_kaspa_no_color.xml b/app/src/main/res/drawable/ic_kaspa_no_color.xml index cae1a10de8..55b68fd581 100644 --- a/app/src/main/res/drawable/ic_kaspa_no_color.xml +++ b/app/src/main/res/drawable/ic_kaspa_no_color.xml @@ -3,9 +3,6 @@ android:height="22dp" android:viewportWidth="22" android:viewportHeight="22"> - diff --git a/app/src/main/res/drawable/ic_kava_no_color.xml b/app/src/main/res/drawable/ic_kava_no_color.xml index d48b77cede..49e4647c33 100644 --- a/app/src/main/res/drawable/ic_kava_no_color.xml +++ b/app/src/main/res/drawable/ic_kava_no_color.xml @@ -3,9 +3,6 @@ android:height="22dp" android:viewportWidth="22" android:viewportHeight="22"> - - diff --git a/app/src/main/res/drawable/ic_litecoin_no_color.xml b/app/src/main/res/drawable/ic_litecoin_no_color.xml index 5758f1ab6e..5b3acc91e8 100644 --- a/app/src/main/res/drawable/ic_litecoin_no_color.xml +++ b/app/src/main/res/drawable/ic_litecoin_no_color.xml @@ -1,9 +1,10 @@ - - - - - - + + diff --git a/app/src/main/res/drawable/ic_octaspace_no_color.xml b/app/src/main/res/drawable/ic_octaspace_no_color.xml index 36751027a3..c61e79358f 100644 --- a/app/src/main/res/drawable/ic_octaspace_no_color.xml +++ b/app/src/main/res/drawable/ic_octaspace_no_color.xml @@ -4,9 +4,6 @@ android:autoMirrored="true" android:viewportWidth="22" android:viewportHeight="22"> - diff --git a/app/src/main/res/drawable/ic_optimism_no_color.xml b/app/src/main/res/drawable/ic_optimism_no_color.xml index 3a9d21620b..7091ff4c7d 100644 --- a/app/src/main/res/drawable/ic_optimism_no_color.xml +++ b/app/src/main/res/drawable/ic_optimism_no_color.xml @@ -5,9 +5,6 @@ android:viewportHeight="22"> - - diff --git a/app/src/main/res/drawable/ic_polygon_no_color.xml b/app/src/main/res/drawable/ic_polygon_no_color.xml index 665486d682..eafe18126e 100644 --- a/app/src/main/res/drawable/ic_polygon_no_color.xml +++ b/app/src/main/res/drawable/ic_polygon_no_color.xml @@ -1,6 +1,5 @@ - diff --git a/app/src/main/res/drawable/ic_ravencoin_no_color.xml b/app/src/main/res/drawable/ic_ravencoin_no_color.xml index 5bc49c57cf..7599b5c971 100644 --- a/app/src/main/res/drawable/ic_ravencoin_no_color.xml +++ b/app/src/main/res/drawable/ic_ravencoin_no_color.xml @@ -3,9 +3,6 @@ android:height="22dp" android:viewportWidth="22" android:viewportHeight="22"> - - diff --git a/app/src/main/res/drawable/ic_solana_no_color.xml b/app/src/main/res/drawable/ic_solana_no_color.xml index a49a56f643..31e6195232 100644 --- a/app/src/main/res/drawable/ic_solana_no_color.xml +++ b/app/src/main/res/drawable/ic_solana_no_color.xml @@ -1,6 +1,5 @@ - diff --git a/app/src/main/res/drawable/ic_stellar_no_color.xml b/app/src/main/res/drawable/ic_stellar_no_color.xml index 27a497f3fb..61e3683e6f 100644 --- a/app/src/main/res/drawable/ic_stellar_no_color.xml +++ b/app/src/main/res/drawable/ic_stellar_no_color.xml @@ -3,7 +3,6 @@ android:width="16dp" xmlns:android="http://schemas.android.com/apk/res/android"> - diff --git a/app/src/main/res/drawable/ic_telos_no_color.xml b/app/src/main/res/drawable/ic_telos_no_color.xml index 01aca1d2b2..bdd073cd40 100644 --- a/app/src/main/res/drawable/ic_telos_no_color.xml +++ b/app/src/main/res/drawable/ic_telos_no_color.xml @@ -3,9 +3,6 @@ android:height="22dp" android:viewportWidth="22" android:viewportHeight="22"> - - diff --git a/app/src/main/res/drawable/ic_terra_no_color.xml b/app/src/main/res/drawable/ic_terra_no_color.xml index 0ba5bf40f7..f25adb981e 100644 --- a/app/src/main/res/drawable/ic_terra_no_color.xml +++ b/app/src/main/res/drawable/ic_terra_no_color.xml @@ -3,9 +3,6 @@ android:height="22dp" android:viewportWidth="22" android:viewportHeight="22"> - diff --git a/app/src/main/res/drawable/ic_tezos_no_color.xml b/app/src/main/res/drawable/ic_tezos_no_color.xml index c023c94fa3..a929bead46 100644 --- a/app/src/main/res/drawable/ic_tezos_no_color.xml +++ b/app/src/main/res/drawable/ic_tezos_no_color.xml @@ -1,6 +1,5 @@ - diff --git a/app/src/main/res/drawable/ic_ton_no_color.xml b/app/src/main/res/drawable/ic_ton_no_color.xml index c3d5d856db..e9ecd65434 100644 --- a/app/src/main/res/drawable/ic_ton_no_color.xml +++ b/app/src/main/res/drawable/ic_ton_no_color.xml @@ -3,9 +3,6 @@ android:height="22dp" android:viewportWidth="22" android:viewportHeight="22"> - - diff --git a/app/src/main/res/drawable/ic_xrp_no_color.xml b/app/src/main/res/drawable/ic_xrp_no_color.xml index 67185b115a..47a43000ca 100644 --- a/app/src/main/res/drawable/ic_xrp_no_color.xml +++ b/app/src/main/res/drawable/ic_xrp_no_color.xml @@ -1,6 +1,10 @@ - - - + + diff --git a/app/src/main/res/layout/layout_pseudo_toolbar.xml b/app/src/main/res/layout/layout_pseudo_toolbar.xml index 0f9b853f36..361f8cbb40 100644 --- a/app/src/main/res/layout/layout_pseudo_toolbar.xml +++ b/app/src/main/res/layout/layout_pseudo_toolbar.xml @@ -4,7 +4,7 @@ android:id="@+id/pseudo_toolbar" android:layout_width="match_parent" android:layout_height="?attr/actionBarSize" - android:background="@color/background_action" + android:background="@color/background_primary" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent"> diff --git a/app/src/main/res/values-night/colors.xml b/app/src/main/res/values-night/colors.xml index 1acebccb4b..1b1466a874 100644 --- a/app/src/main/res/values-night/colors.xml +++ b/app/src/main/res/values-night/colors.xml @@ -50,4 +50,5 @@ #919191 + #333333 \ No newline at end of file diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index cf46dfba11..5d49d88d30 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -80,4 +80,6 @@ #000000 #00000000 + + #1B1D1C \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/TextFields.kt b/core/ui/src/main/java/com/tangem/core/ui/components/TextFields.kt index 2575fee4ad..6f09757064 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/TextFields.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/TextFields.kt @@ -9,29 +9,11 @@ import androidx.compose.foundation.background import androidx.compose.foundation.interaction.InteractionSource import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.collectIsFocusedAsState -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.IntrinsicSize -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.heightIn -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.* import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.material.Divider -import androidx.compose.material.Icon -import androidx.compose.material.IconButton -import androidx.compose.material.OutlinedTextField -import androidx.compose.material.Text -import androidx.compose.material.TextFieldColors -import androidx.compose.runtime.Composable -import androidx.compose.runtime.Immutable -import androidx.compose.runtime.Stable -import androidx.compose.runtime.State -import androidx.compose.runtime.getValue -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberUpdatedState +import androidx.compose.material.* +import androidx.compose.runtime.* import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Shape @@ -210,8 +192,8 @@ object TangemTextFieldsDefault { cursorColor = TangemTheme.colors.icon.primary1, errorCursorColor = TangemTheme.colors.icon.warning, focusedIndicatorColor = TangemTheme.colors.icon.primary1, - unfocusedIndicatorColor = TangemTheme.colors.stroke.primary, - disabledIndicatorColor = TangemTheme.colors.stroke.primary, + unfocusedIndicatorColor = TangemTheme.colors.stroke.secondary, + disabledIndicatorColor = TangemTheme.colors.stroke.secondary, errorIndicatorColor = TangemTheme.colors.icon.warning, leadingIconColor = TangemTheme.colors.icon.informative, disabledLeadingIconColor = Color.Transparent, diff --git a/core/ui/src/main/res/drawable-night/card_placeholder_black.webp b/core/ui/src/main/res/drawable-night/card_placeholder_black.webp new file mode 100644 index 0000000000000000000000000000000000000000..376f1200410487bf51c8529e2ef0e302f316bb31 GIT binary patch literal 3248 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 Zy-M*Nzb)jPNatxhOgGvAZ literal 0 HcmV?d00001 diff --git a/core/ui/src/main/res/drawable/card_placeholder_black.webp b/core/ui/src/main/res/drawable/card_placeholder_black.webp new file mode 100644 index 0000000000000000000000000000000000000000..b7077165a5636145500e8433779011f76a277c89 GIT binary patch literal 3574 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 N4&Uz3D!k4L_!mh`Am{)9 literal 0 HcmV?d00001 diff --git a/core/ui/src/main/res/drawable/card_placeholder_black.xml b/core/ui/src/main/res/drawable/card_placeholder_black.xml deleted file mode 100644 index 83fab8e0f7..0000000000 --- a/core/ui/src/main/res/drawable/card_placeholder_black.xml +++ /dev/null @@ -1,88 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreen.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreen.kt index a5f8b658ec..bfd13ad8c8 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreen.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreen.kt @@ -59,6 +59,7 @@ internal fun SwapScreen(stateHolder: SwapStateHolder) { topEnd = TangemTheme.dimens.radius16, ), sheetElevation = TangemTheme.dimens.elevation24, + scrimColor = TangemTheme.colors.background.secondary.copy(alpha = 0.4f), content = { SwapScreenContent( state = stateHolder, diff --git a/features/swap/presentation/src/main/res/drawable-night/ill_one_inch_powered.webp b/features/swap/presentation/src/main/res/drawable-night/ill_one_inch_powered.webp new file mode 100644 index 0000000000000000000000000000000000000000..329d1f977e835b036a1bcd0ff2aa4df2bc8311e1 GIT binary patch literal 3978 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 literal 0 HcmV?d00001 From 8c3eb4b60401027523008d9bbe653c06b071fb6e Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 4 Oct 2023 20:53:22 +0300 Subject: [PATCH 136/242] Updated on 2026-08-14 --- .../tap/features/send/ui/SendViewModel.kt | 27 ++++++++++++++++++- .../UpdateDelayedNetworkStatusUseCase.kt | 7 ++--- .../viewmodels/TokenDetailsViewModel.kt | 17 ------------ 3 files changed, 28 insertions(+), 23 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/features/send/ui/SendViewModel.kt b/app/src/main/java/com/tangem/tap/features/send/ui/SendViewModel.kt index 245d0f9a89..06cb71c643 100644 --- a/app/src/main/java/com/tangem/tap/features/send/ui/SendViewModel.kt +++ b/app/src/main/java/com/tangem/tap/features/send/ui/SendViewModel.kt @@ -5,6 +5,8 @@ import com.tangem.domain.balancehiding.IsBalanceHiddenUseCase import com.tangem.domain.balancehiding.ListenToFlipsUseCase import com.tangem.domain.tokens.UpdateDelayedNetworkStatusUseCase import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.tokens.model.Network +import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.usecase.GetSelectedWalletUseCase import com.tangem.features.send.navigation.SendRouter import com.tangem.tap.di.DelayedWork @@ -60,7 +62,10 @@ internal class SendViewModel @Inject constructor( .fold( ifLeft = { Timber.e(it.toString()) }, ifRight = { wallet -> - updateDelayedCurrencyStatusUseCase(wallet.walletId, cryptoCurrency.network, true) + // we should update network to find pending tx after 1 sec + updateForPendingTx(wallet, cryptoCurrency.network) + // we should update network for new balance + updateForBalance(wallet, cryptoCurrency.network) }, ) } @@ -69,7 +74,27 @@ internal class SendViewModel @Inject constructor( } } + private suspend fun updateForPendingTx(userWallet: UserWallet, network: Network) { + updateDelayedCurrencyStatusUseCase( + userWalletId = userWallet.walletId, + network = network, + delayMillis = UPDATE_PENDING_TX_DELAY_MILLIS, + refresh = true, + ) + } + + private suspend fun updateForBalance(userWallet: UserWallet, network: Network) { + updateDelayedCurrencyStatusUseCase( + userWalletId = userWallet.walletId, + network = network, + delayMillis = UPDATE_BALANCE_DELAY_MILLIS, + refresh = true, + ) + } + companion object { + private const val UPDATE_BALANCE_DELAY_MILLIS = 11000L + private const val UPDATE_PENDING_TX_DELAY_MILLIS = 1000L private const val TAG = "SendViewModel" } } \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/UpdateDelayedNetworkStatusUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/UpdateDelayedNetworkStatusUseCase.kt index 30ff71c68f..1991a33bf8 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/UpdateDelayedNetworkStatusUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/UpdateDelayedNetworkStatusUseCase.kt @@ -33,9 +33,10 @@ class UpdateDelayedNetworkStatusUseCase( suspend operator fun invoke( userWalletId: UserWalletId, network: Network, + delayMillis: Long, refresh: Boolean = false, ): Either { - delay(DELAY_MILLIS) + delay(delayMillis) return either { fetchNetworkStatus(userWalletId, network, refresh) } @@ -52,8 +53,4 @@ class UpdateDelayedNetworkStatusUseCase( raise(CurrencyStatusError.DataError(it)) } } - - companion object { - private const val DELAY_MILLIS = 11000L - } } \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt index ab5d7f11d0..c2c671b82d 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt @@ -35,7 +35,6 @@ import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.JobHolder import com.tangem.utils.coroutines.saveIn import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.delay import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch import timber.log.Timber @@ -72,7 +71,6 @@ internal class TokenDetailsViewModel @Inject constructor( private val marketPriceJobHolder = JobHolder() private val refreshStateJobHolder = JobHolder() - private val networkStatusAutoUpdateStateJobHolder = JobHolder() private var cryptoCurrencyStatus: CryptoCurrencyStatus? = null private var wallet by Delegates.notNull() @@ -162,17 +160,6 @@ internal class TokenDetailsViewModel @Inject constructor( .flowOn(dispatchers.io) .launchIn(viewModelScope) .saveIn(marketPriceJobHolder) - - viewModelScope.launch(dispatchers.io) { - // Wait for blockchain updates pending transactions. - // Immediate update doesn't receive any changes. - delay(NETWORK_STATUS_AUTO_UPDATE_DELAY) - fetchCurrencyStatusUseCase.invoke( - userWalletId = wallet.walletId, - id = cryptoCurrency.id, - refresh = true, - ) - }.saveIn(networkStatusAutoUpdateStateJobHolder) } private fun updateTxHistory(refresh: Boolean = false) { @@ -389,8 +376,4 @@ internal class TokenDetailsViewModel @Inject constructor( override fun onCloseRentInfoNotification() { uiState = stateFactory.getStateWithRemovedRentNotification() } - - companion object { - private const val NETWORK_STATUS_AUTO_UPDATE_DELAY = 1000L - } } \ No newline at end of file From ddd9ce1c7f9ad5e311b5a4c57eeae1fcb840bf06 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 5 Oct 2023 11:09:59 +0300 Subject: [PATCH 137/242] Updated on 2026-08-14 --- .../impl/presentation/ui/TokensListScreen.kt | 5 +- .../viewmodels/TokensListViewModel.kt | 60 +++++++++++-------- 2 files changed, 36 insertions(+), 29 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/ui/TokensListScreen.kt b/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/ui/TokensListScreen.kt index 38e5072d84..67d8066531 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/ui/TokensListScreen.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/ui/TokensListScreen.kt @@ -33,7 +33,6 @@ import androidx.compose.ui.unit.dp import androidx.paging.PagingData import androidx.paging.compose.* import com.tangem.core.ui.components.PrimaryButton -import com.tangem.core.ui.res.TangemColorPalette import com.tangem.core.ui.res.TangemTheme import com.tangem.tap.features.tokens.impl.presentation.states.TokenItemState import com.tangem.tap.features.tokens.impl.presentation.states.TokensListStateHolder @@ -147,7 +146,7 @@ private fun DifferentAddressesWarning() { modifier = Modifier .padding(TangemTheme.dimens.spacing16) .background( - color = TangemColorPalette.Light1, + color = TangemTheme.colors.button.disabled, shape = RoundedCornerShape(TangemTheme.dimens.radius10), ), contentAlignment = Alignment.Center, @@ -159,7 +158,7 @@ private fun DifferentAddressesWarning() { horizontal = TangemTheme.dimens.spacing16, vertical = TangemTheme.dimens.spacing8, ), - color = TangemColorPalette.Dark1, + color = TangemTheme.colors.text.tertiary, textAlign = TextAlign.Start, style = TangemTheme.typography.body2.copy( letterSpacing = TextUnit(value = 0.5f, type = TextUnitType.Sp), diff --git a/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/viewmodels/TokensListViewModel.kt b/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/viewmodels/TokensListViewModel.kt index 9dc9ccfbf1..41b7f45861 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/viewmodels/TokensListViewModel.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/viewmodels/TokensListViewModel.kt @@ -35,7 +35,6 @@ import com.tangem.tap.features.tokens.impl.presentation.states.NetworkItemState import com.tangem.tap.features.tokens.impl.presentation.states.TokenItemState import com.tangem.tap.features.tokens.impl.presentation.states.TokensListStateHolder import com.tangem.tap.features.tokens.impl.presentation.states.TokensListToolbarState -import com.tangem.tap.proxy.AppStateHolder import com.tangem.tap.store import com.tangem.utils.coroutines.AppCoroutineDispatcherProvider import com.tangem.utils.coroutines.Debouncer @@ -57,7 +56,7 @@ import com.tangem.blockchain.common.Token as BlockchainToken * @property interactor feature interactor * @property router feature router * @property dispatchers coroutine dispatchers provider - * @property reduxStateHolder redux state holder + * @property getSelectedWalletUseCase use case that returns selected wallet * @param analyticsEventHandler analytics event handler * [REDACTED_AUTHOR] @@ -68,10 +67,9 @@ internal class TokensListViewModel @Inject constructor( private val interactor: TokensListInteractor, private val router: TokensListRouter, private val dispatchers: AppCoroutineDispatcherProvider, - private val reduxStateHolder: AppStateHolder, + private val getSelectedWalletUseCase: GetSelectedWalletUseCase, analyticsEventHandler: AnalyticsEventHandler, getCurrenciesUseCase: GetCryptoCurrenciesUseCase, - getSelectedWalletUseCase: GetSelectedWalletUseCase, walletFeatureToggles: WalletFeatureToggles, ) : ViewModel(), DefaultLifecycleObserver { @@ -150,7 +148,10 @@ internal class TokensListViewModel @Inject constructor( } private fun isDifferentAddressesBlockVisible(): Boolean { - return reduxStateHolder.scanResponse?.card?.useOldStyleDerivation == true + return getSelectedWalletUseCase().fold( + ifLeft = { false }, + ifRight = { it.scanResponse.card.useOldStyleDerivation }, + ) } private fun getInitialTokensList(searchText: String = ""): Flow> { @@ -406,32 +407,39 @@ internal class TokensListViewModel @Inject constructor( } private fun isUnsupportedToken(blockchain: Blockchain): SupportTokensState? { - val scanResponse = reduxStateHolder.scanResponse - val cardTypesResolver = scanResponse?.cardTypesResolver ?: return null - val supportedTokens = scanResponse.card.supportedTokens(cardTypesResolver) + return getSelectedWalletUseCase().fold( + ifLeft = { null }, + ifRight = { + val cardTypesResolver = it.scanResponse.cardTypesResolver + val supportedTokens = it.scanResponse.card.supportedTokens(cardTypesResolver) - // refactor this later by moving all this logic in card config - if (blockchain == Blockchain.Solana && !supportedTokens.contains(Blockchain.Solana)) { - return SupportTokensState.SolanaNetworkUnsupported - } - val canHandleToken = scanResponse.card.canHandleToken( - supportedTokens = supportedTokens, - blockchain = blockchain, - cardTypesResolver = cardTypesResolver, + // refactor this later by moving all this logic in card config + if (blockchain == Blockchain.Solana && !supportedTokens.contains(Blockchain.Solana)) { + return SupportTokensState.SolanaNetworkUnsupported + } + val canHandleToken = it.scanResponse.card.canHandleToken( + supportedTokens = supportedTokens, + blockchain = blockchain, + cardTypesResolver = cardTypesResolver, + ) + if (!canHandleToken) { + return SupportTokensState.UnsupportedCurve + } + return SupportTokensState.SupportedToken + }, ) - if (!canHandleToken) { - return SupportTokensState.UnsupportedCurve - } - return SupportTokensState.SupportedToken } private fun isUnsupportedBlockchain(blockchain: Blockchain): Boolean { - val scanResponse = reduxStateHolder.scanResponse - val canHandleToken = scanResponse?.card?.canHandleBlockchain( - blockchain = blockchain, - cardTypesResolver = scanResponse.cardTypesResolver, - ) ?: false - return !canHandleToken + return getSelectedWalletUseCase().fold( + ifLeft = { false }, + ifRight = { + !it.scanResponse.card.canHandleBlockchain( + blockchain = blockchain, + cardTypesResolver = it.scanResponse.cardTypesResolver, + ) + }, + ) } private companion object { From 477bff1de0f967e20446a21434e9d30c7fee299e Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 4 Oct 2023 15:43:19 +0300 Subject: [PATCH 138/242] Updated on 2026-08-14 --- .../wallet/viewmodels/WalletViewModel.kt | 47 +++---------------- 1 file changed, 7 insertions(+), 40 deletions(-) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt index 09b9aff20b..3dcfdd7311 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt @@ -17,7 +17,6 @@ import com.tangem.core.analytics.models.AnalyticsParam import com.tangem.core.navigation.AppScreen import com.tangem.core.ui.components.bottomsheets.tokenreceive.AddressModel import com.tangem.core.ui.components.bottomsheets.tokenreceive.TokenReceiveBottomSheetConfig -import com.tangem.core.ui.components.marketprice.MarketPriceBlockState import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.WrappedList import com.tangem.core.ui.extensions.resourceReference @@ -54,13 +53,11 @@ import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWalletId import com.tangem.domain.wallets.usecase.* import com.tangem.feature.wallet.impl.R -import com.tangem.feature.wallet.presentation.common.state.TokenItemState import com.tangem.feature.wallet.presentation.router.InnerWalletRouter import com.tangem.feature.wallet.presentation.wallet.analytics.PortfolioEvent import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent import com.tangem.feature.wallet.presentation.wallet.state.* import com.tangem.feature.wallet.presentation.wallet.state.components.WalletCardState -import com.tangem.feature.wallet.presentation.wallet.state.components.WalletTokensListState import com.tangem.feature.wallet.presentation.wallet.state.factory.WalletStateFactory import com.tangem.operations.derivation.ExtendedPublicKeysMap import com.tangem.utils.coroutines.CoroutineDispatcherProvider @@ -246,14 +243,12 @@ internal class WalletViewModel @Inject constructor( if (cacheState != null) { uiState = stateFactory.getStateWithoutDeletedWallet(cacheState, action) - if (cacheState.isLoadingOrEmptyState()) { - uiState = stateFactory.getStateAndTriggerEvent( - state = uiState, - event = WalletEvent.ChangeWallet(action.selectedWalletIndex), - setUiState = { uiState = it }, - ) - getContentItemsUpdates(action.selectedWalletIndex) - } + uiState = stateFactory.getStateAndTriggerEvent( + state = uiState, + event = WalletEvent.ChangeWallet(action.selectedWalletIndex), + setUiState = { uiState = it }, + ) + getContentItemsUpdates(action.selectedWalletIndex) } else { /* It's impossible case because user can delete only visible state, but we support this case */ scrollAndUpdateState(selectedWalletIndex = action.selectedWalletIndex) @@ -495,9 +490,7 @@ internal class WalletViewModel @Inject constructor( pullToRefreshConfig = cacheState.pullToRefreshConfig.copy(isRefreshing = false), ) - if (cacheState.isLoadingOrEmptyState()) { - getContentItemsUpdates(index) - } + getContentItemsUpdates(index) } else { initializeAndLoadState(selectedWalletIndex = index) } @@ -1035,32 +1028,6 @@ internal class WalletViewModel @Inject constructor( ) } - private fun WalletState.isLoadingOrEmptyState(): Boolean { - // Check the base components - if (this is WalletState.ContentState && - walletsListConfig.wallets[walletsListConfig.selectedWalletIndex] is WalletCardState.Loading - ) { - return true - } - - // Check the special components - return when (this) { - is WalletMultiCurrencyState -> { - val isTokensEmpty = tokensListState is WalletTokensListState.Empty - val hasLoadingTokens = tokensListState is WalletTokensListState.ContentState && - (tokensListState as WalletTokensListState.ContentState).items - .filterIsInstance() - .any { it.state is TokenItemState.Loading } - - isTokensEmpty || tokensListState is WalletTokensListState.Loading || hasLoadingTokens - } - is WalletSingleCurrencyState -> { - this is WalletSingleCurrencyState.Content && marketPriceBlockState is MarketPriceBlockState.Loading - } - is WalletState.Initial -> false - } - } - private fun getWallet(index: Int): UserWallet { return requireNotNull( value = wallets.getOrNull(index), From 56956a0c2ba2cd340b594c56cb96fff22ba5941d Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 4 Oct 2023 17:43:35 +0300 Subject: [PATCH 139/242] Updated on 2026-08-14 --- .../wallet/presentation/wallet/state/WalletEvent.kt | 2 +- .../wallet/presentation/wallet/ui/WalletEventEffect.kt | 1 + .../presentation/wallet/viewmodels/WalletViewModel.kt | 10 ++++------ 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletEvent.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletEvent.kt index 6102887cd0..16cfffd301 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletEvent.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletEvent.kt @@ -18,7 +18,7 @@ internal sealed class WalletEvent { val onActionClick: (() -> Unit)?, ) : WalletEvent() - data class CopyAddress(val address: String) : WalletEvent() + data class CopyAddress(val address: String, val toast: TextReference) : WalletEvent() data class RateApp(val onDismissClick: () -> Unit) : WalletEvent() } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletEventEffect.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletEventEffect.kt index 7924d2a655..efdc905452 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletEventEffect.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletEventEffect.kt @@ -46,6 +46,7 @@ internal fun WalletEventEffect( } is WalletEvent.CopyAddress -> { clipboardManager.setText(AnnotatedString(value.address)) + Toast.makeText(context, value.toast.resolveReference(resources), Toast.LENGTH_SHORT).show() } is WalletEvent.ShowAlert -> { onAlertConfigSet( diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt index 3dcfdd7311..850c1aeef3 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt @@ -643,12 +643,10 @@ internal class WalletViewModel @Inject constructor( defaultAddress?.value?.let { address -> uiState = stateFactory.getStateAndTriggerEvent( state = uiState, - event = WalletEvent.CopyAddress(address), - setUiState = { uiState = it }, - ) - uiState = stateFactory.getStateAndTriggerEvent( - state = uiState, - event = WalletEvent.ShowToast(resourceReference(R.string.wallet_notification_address_copied)), + event = WalletEvent.CopyAddress( + address = address, + toast = resourceReference(R.string.wallet_notification_address_copied), + ), setUiState = { uiState = it }, ) } From b73d93928bc08b712dca3bf6af4806e3b351f563 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 6 Oct 2023 10:45:51 +0300 Subject: [PATCH 140/242] Updated on 2026-08-14 --- .../com/tangem/tap/GlobalSettingsState.kt | 12 -- .../main/java/com/tangem/tap/MainActivity.kt | 190 +++++++++--------- .../main/java/com/tangem/tap/MainViewModel.kt | 35 ---- .../java/com/tangem/tap/TapApplication.kt | 4 + .../tap/di/domain/AppThemeDomainModule.kt | 4 +- .../com/tangem/tap/domain/TangemSdkManager.kt | 2 +- .../tangem/tap/features/home/HomeFragment.kt | 2 +- .../tap/features/home/redux/HomeAction.kt | 4 +- .../tap/features/home/redux/HomeMiddleware.kt | 54 +++-- .../features/intentHandler/IntentHandler.kt | 3 +- .../handlers/BackgroundScanIntentHandler.kt | 18 +- .../handlers/BuyCurrencyIntentHandler.kt | 2 +- .../handlers/SellCurrencyIntentHandler.kt | 6 +- .../WalletConnectLinkIntentHandler.kt | 6 +- .../redux/OnboardingWalletMiddleware.kt | 2 +- .../features/welcome/redux/WelcomeAction.kt | 13 +- .../welcome/redux/WelcomeMiddleware.kt | 137 ++++++++----- .../features/welcome/redux/WelcomeReducer.kt | 4 +- .../features/welcome/redux/WelcomeState.kt | 2 + .../features/welcome/ui/WelcomeFragment.kt | 15 +- .../features/welcome/ui/WelcomeViewModel.kt | 39 +++- gradle/dependencies.toml | 2 +- 22 files changed, 292 insertions(+), 264 deletions(-) delete mode 100644 app/src/main/java/com/tangem/tap/GlobalSettingsState.kt delete mode 100644 app/src/main/java/com/tangem/tap/MainViewModel.kt diff --git a/app/src/main/java/com/tangem/tap/GlobalSettingsState.kt b/app/src/main/java/com/tangem/tap/GlobalSettingsState.kt deleted file mode 100644 index 19cd307271..0000000000 --- a/app/src/main/java/com/tangem/tap/GlobalSettingsState.kt +++ /dev/null @@ -1,12 +0,0 @@ -package com.tangem.tap - -import com.tangem.domain.apptheme.model.AppThemeMode - -internal sealed class GlobalSettingsState { - - object Loading : GlobalSettingsState() - - data class Content( - val appThemeMode: AppThemeMode, - ) : GlobalSettingsState() -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/MainActivity.kt b/app/src/main/java/com/tangem/tap/MainActivity.kt index 01bdd66eb8..9f1970f662 100644 --- a/app/src/main/java/com/tangem/tap/MainActivity.kt +++ b/app/src/main/java/com/tangem/tap/MainActivity.kt @@ -1,18 +1,20 @@ package com.tangem.tap -import android.app.Application +import android.annotation.SuppressLint import android.content.Intent import android.content.pm.ActivityInfo import android.content.res.Configuration import android.os.Bundle import android.view.View -import androidx.activity.viewModels import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatDelegate import androidx.appcompat.app.AppCompatDelegate.setDefaultNightMode +import androidx.core.os.bundleOf import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen import androidx.core.view.WindowCompat +import androidx.lifecycle.flowWithLifecycle import androidx.lifecycle.lifecycleScope +import arrow.core.getOrElse import by.kirich1409.viewbindingdelegate.viewBinding import com.google.android.material.snackbar.Snackbar import com.tangem.core.navigation.AppScreen @@ -32,7 +34,6 @@ import com.tangem.tap.common.DialogManager import com.tangem.tap.common.OnActivityResultCallback import com.tangem.tap.common.SnackbarHandler import com.tangem.tap.common.apptheme.MutableAppThemeModeHolder -import com.tangem.tap.common.extensions.dispatchOnMain import com.tangem.tap.common.redux.NotificationsHandler import com.tangem.tap.common.redux.global.GlobalAction import com.tangem.tap.common.shop.googlepay.GooglePayService @@ -49,19 +50,15 @@ import com.tangem.tap.features.intentHandler.handlers.SellCurrencyIntentHandler import com.tangem.tap.features.intentHandler.handlers.WalletConnectLinkIntentHandler import com.tangem.tap.features.onboarding.products.wallet.redux.BackupAction import com.tangem.tap.features.shop.redux.ShopAction -import com.tangem.tap.features.welcome.redux.WelcomeAction +import com.tangem.tap.features.welcome.ui.WelcomeFragment import com.tangem.tap.proxy.AppStateHolder import com.tangem.tap.proxy.redux.DaggerGraphAction import com.tangem.utils.coroutines.FeatureCoroutineExceptionHandler import com.tangem.wallet.R import com.tangem.wallet.databinding.ActivityMainBinding import dagger.hilt.android.AndroidEntryPoint -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.Job -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.launch +import kotlinx.coroutines.* +import kotlinx.coroutines.flow.* import java.lang.ref.WeakReference import javax.inject.Inject import kotlin.coroutines.CoroutineContext @@ -117,8 +114,7 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac @Inject lateinit var walletConnectInteractor: WalletConnectInteractor - private val viewModel: MainViewModel by viewModels() - private var isInitializing: Boolean = true + private lateinit var appThemeModeFlow: SharedFlow // TODO: fixme: inject through DI private val intentProcessor: IntentProcessor = IntentProcessor() @@ -130,25 +126,24 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac private val onActivityResultCallbacks = mutableListOf() override fun onCreate(savedInstanceState: Bundle?) { - val splashScreen = installSplashScreen() - - if (!isDarkThemeFeatureEnabled(application)) { - setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO) - } + installSplashScreen() + installAppTheme() // We need to call it before onCreate to prevent unnecessary activity recreation super.onCreate(savedInstanceState) - cardSdkLifecycleObserver.onCreate(context = this) - - bootstrapMainStateUpdates(application) - - splashScreen.setKeepOnScreenCondition { isInitializing } + installActivityDependencies() + observeAppThemeModeUpdates() setContentView(R.layout.activity_main) - systemActions() + initContent() + checkGooglePayAvailability() + } + + private fun installActivityDependencies() { store.dispatch(NavigationAction.ActivityCreated(WeakReference(this))) + cardSdkLifecycleObserver.onCreate(context = this) tangemSdkManager = injectedTangemSdkManager appStateHolder.tangemSdkManager = tangemSdkManager backupService = BackupService.init(cardSdkConfigRepository.sdk, this) @@ -157,11 +152,6 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac initUserWalletsListManager() initIntentHandlers() - store.dispatch( - ShopAction.CheckIfGooglePayAvailable( - GooglePayService(createPaymentsClient(this), this), - ), - ) store.dispatch( DaggerGraphAction.SetActivityDependencies( testerRouter = testerRouter, @@ -174,9 +164,57 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac ) } - private fun isDarkThemeFeatureEnabled(application: Application): Boolean { - val featureToggle = (application as TapApplication).darkThemeFeatureToggle - return featureToggle.isDarkThemeEnabled + private fun installAppTheme() { + appThemeModeFlow = createAppThemeModeFlow() + val mode = runBlocking { appThemeModeFlow.filterNotNull().first() } + + updateAppTheme(mode) + } + + private fun observeAppThemeModeUpdates() { + appThemeModeFlow + .filterNotNull() + .flowWithLifecycle(lifecycle) + .onEach(::updateAppTheme) + .launchIn(lifecycleScope) + } + + @SuppressLint("SourceLockedOrientationActivity") + private fun initContent() { + WindowCompat.setDecorFitsSystemWindows(window, false) + + supportFragmentManager.registerFragmentLifecycleCallbacks( + NavBarInsetsFragmentLifecycleCallback(), + true, + ) + + requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT + } + + private fun checkGooglePayAvailability() { + store.dispatch( + ShopAction.CheckIfGooglePayAvailable( + GooglePayService(createPaymentsClient(this), this), + ), + ) + } + + private fun createAppThemeModeFlow(): SharedFlow { + val tapApplication = application as TapApplication + val featureToggle = tapApplication.darkThemeFeatureToggle + + return if (featureToggle.isDarkThemeEnabled) { + tapApplication.getAppThemeModeUseCase() + .map { maybeMode -> + maybeMode.getOrElse { AppThemeMode.DEFAULT } + } + .shareIn( + scope = lifecycleScope + Dispatchers.IO, + started = SharingStarted.WhileSubscribed(stopTimeoutMillis = 5_000), + ) + } else { + MutableStateFlow(AppThemeMode.FORCE_LIGHT) + } } override fun onStart() { @@ -189,7 +227,7 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac // TODO: RESEARCH! NotificationsHandler is created in onResume and destroyed in onStop notificationsHandler = NotificationsHandler(binding.fragmentContainer) - navigateToInitialScreenIfNeededOnResume(intent) + navigateToInitialScreenIfNeeded(intent) } override fun onStop() { @@ -207,12 +245,7 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac private fun initIntentHandlers() { val hasSavedWalletsProvider = { store.state.globalState.userWalletsListManager?.hasUserWallets == true } - intentProcessor.addHandler( - BackgroundScanIntentHandler( - hasSavedWalletsProvider, - lifecycleScope, - ), - ) + intentProcessor.addHandler(BackgroundScanIntentHandler(hasSavedWalletsProvider, lifecycleScope)) intentProcessor.addHandler(WalletConnectLinkIntentHandler()) intentProcessor.addHandler(BuyCurrencyIntentHandler()) intentProcessor.addHandler(SellCurrencyIntentHandler()) @@ -230,38 +263,21 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac store.dispatch(GlobalAction.UpdateUserWalletsListManager(manager)) } - private fun bootstrapMainStateUpdates(application: Application) { - viewModel.state - .onEach { state -> - isInitializing = state is GlobalSettingsState.Loading + private fun updateAppTheme(appThemeMode: AppThemeMode) { + MutableAppThemeModeHolder.value = appThemeMode + MutableAppThemeModeHolder.isDarkThemeActive = isDarkTheme() - when (state) { - is GlobalSettingsState.Content -> { - if (isDarkThemeFeatureEnabled(application)) { - MutableAppThemeModeHolder.value = state.appThemeMode - MutableAppThemeModeHolder.isDarkThemeActive = isDarkTheme() + val mode = when (appThemeMode) { + AppThemeMode.FORCE_DARK -> AppCompatDelegate.MODE_NIGHT_YES + AppThemeMode.FORCE_LIGHT -> AppCompatDelegate.MODE_NIGHT_NO + AppThemeMode.FOLLOW_SYSTEM -> AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM + } - val mode = when (state.appThemeMode) { - AppThemeMode.FORCE_DARK -> AppCompatDelegate.MODE_NIGHT_YES - AppThemeMode.FORCE_LIGHT -> AppCompatDelegate.MODE_NIGHT_NO - AppThemeMode.FOLLOW_SYSTEM -> AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM - } - setDefaultNightMode(mode) - } else { - MutableAppThemeModeHolder.value = AppThemeMode.FORCE_LIGHT - } - } - is GlobalSettingsState.Loading -> Unit - } - } - .launchIn(lifecycleScope) + setDefaultNightMode(mode) } private fun isDarkTheme(): Boolean { - return when ( - resources.configuration.uiMode and - Configuration.UI_MODE_NIGHT_MASK - ) { + return when (resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK) { Configuration.UI_MODE_NIGHT_YES -> true Configuration.UI_MODE_NIGHT_NO -> false Configuration.UI_MODE_NIGHT_UNDEFINED -> false @@ -269,17 +285,6 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac } } - private fun systemActions() { - WindowCompat.setDecorFitsSystemWindows(window, false) - - supportFragmentManager.registerFragmentLifecycleCallbacks( - NavBarInsetsFragmentLifecycleCallback(), - true, - ) - - requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT - } - override fun onNewIntent(intent: Intent?) { super.onNewIntent(intent) @@ -341,43 +346,40 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac lockUserWalletsTimer?.restart() } - private fun navigateToInitialScreenIfNeededOnResume(intentWhichStartedActivity: Intent?) { + private fun navigateToInitialScreenIfNeeded(intentWhichStartedActivity: Intent?) { val backStackIsEmpty = supportFragmentManager.backStackEntryCount == 0 val isNotScannedBefore = store.state.globalState.scanResponse == null val isOnboardingServiceNotActive = store.state.globalState.onboardingState.onboardingStarted val isShopNotOpened = store.state.shopState.total != null + when { !backStackIsEmpty && isNotScannedBefore && isOnboardingServiceNotActive && isShopNotOpened -> { - navigateToInitialScreenOnResume(intentWhichStartedActivity) + navigateToInitialScreen(intentWhichStartedActivity) } backStackIsEmpty -> { - navigateToInitialScreenOnResume(intentWhichStartedActivity) + navigateToInitialScreen(intentWhichStartedActivity) } else -> Unit } } - private fun navigateToInitialScreenOnResume(intentWhichStartedActivity: Intent?) { + private fun navigateToInitialScreen(intentWhichStartedActivity: Intent?) { if (store.state.globalState.userWalletsListManager?.hasUserWallets == true) { - store.dispatchOnMain(NavigationAction.NavigateTo(AppScreen.Welcome)) - store.dispatchOnMain(WelcomeAction.SetInitialIntent(intentWhichStartedActivity)) - lifecycleScope.launch { - val handler = BackgroundScanIntentHandler( - hasSavedUserWalletsProvider = { true }, - lifecycleCoroutineScope = lifecycleScope, - ) - val isBackgroundScanHandled = handler.handleIntent(intentWhichStartedActivity) - val hasNotIncompletedBackup = !backupService.hasIncompletedBackup - if (!isBackgroundScanHandled && hasNotIncompletedBackup) { - store.dispatchOnMain(WelcomeAction.ProceedWithBiometrics) - } - } + store.dispatch( + NavigationAction.NavigateTo( + screen = AppScreen.Welcome, + bundle = intentWhichStartedActivity?.let { + bundleOf(WelcomeFragment.INITIAL_INTENT_KEY to it) + }, + ), + ) } else { - store.dispatchOnMain(NavigationAction.NavigateTo(AppScreen.Home)) + store.dispatch(NavigationAction.NavigateTo(AppScreen.Home)) lifecycleScope.launch { intentProcessor.handleIntent(intentWhichStartedActivity) } } + store.dispatch(BackupAction.CheckForUnfinishedBackup) } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/MainViewModel.kt b/app/src/main/java/com/tangem/tap/MainViewModel.kt deleted file mode 100644 index 2cb276000c..0000000000 --- a/app/src/main/java/com/tangem/tap/MainViewModel.kt +++ /dev/null @@ -1,35 +0,0 @@ -package com.tangem.tap - -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import arrow.core.getOrElse -import com.tangem.domain.apptheme.GetAppThemeModeUseCase -import com.tangem.domain.apptheme.model.AppThemeMode -import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.flow.SharingStarted -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.stateIn -import javax.inject.Inject - -@HiltViewModel -internal class MainViewModel @Inject constructor( - private val getAppThemeModeUseCase: GetAppThemeModeUseCase, -) : ViewModel() { - - val state: StateFlow = createMainStateFlow() - - private fun createMainStateFlow(): StateFlow { - return getAppThemeModeUseCase() - .map { maybeMode -> - val mode = maybeMode.getOrElse { AppThemeMode.DEFAULT } - - GlobalSettingsState.Content(appThemeMode = mode) - } - .stateIn( - scope = viewModelScope, - started = SharingStarted.WhileSubscribed(stopTimeoutMillis = 5_000), - initialValue = GlobalSettingsState.Loading, - ) - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/TapApplication.kt b/app/src/main/java/com/tangem/tap/TapApplication.kt index 341ee0112d..6992e82d7e 100644 --- a/app/src/main/java/com/tangem/tap/TapApplication.kt +++ b/app/src/main/java/com/tangem/tap/TapApplication.kt @@ -24,6 +24,7 @@ import com.tangem.datasource.config.models.Config import com.tangem.datasource.connection.NetworkConnectionManager import com.tangem.datasource.local.token.UserTokensStore import com.tangem.domain.appcurrency.repository.AppCurrencyRepository +import com.tangem.domain.apptheme.GetAppThemeModeUseCase import com.tangem.domain.apptheme.repository.AppThemeModeRepository import com.tangem.domain.balancehiding.repositories.BalanceHidingRepository import com.tangem.domain.card.ScanCardProcessor @@ -200,6 +201,9 @@ internal class TapApplication : Application(), ImageLoaderFactory { @Inject lateinit var appRatingRepository: AppRatingRepository + + @Inject + lateinit var getAppThemeModeUseCase: GetAppThemeModeUseCase // endregion Injected override fun onCreate() { diff --git a/app/src/main/java/com/tangem/tap/di/domain/AppThemeDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/AppThemeDomainModule.kt index b4378101b0..4b6fc45a17 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/AppThemeDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/AppThemeDomainModule.kt @@ -6,10 +6,10 @@ import com.tangem.domain.apptheme.repository.AppThemeModeRepository import dagger.Module import dagger.Provides import dagger.hilt.InstallIn -import dagger.hilt.android.components.ViewModelComponent +import dagger.hilt.components.SingletonComponent @Module -@InstallIn(ViewModelComponent::class) +@InstallIn(SingletonComponent::class) internal object AppThemeDomainModule { @Provides diff --git a/app/src/main/java/com/tangem/tap/domain/TangemSdkManager.kt b/app/src/main/java/com/tangem/tap/domain/TangemSdkManager.kt index 54b6313c9c..7b5691b79d 100644 --- a/app/src/main/java/com/tangem/tap/domain/TangemSdkManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/TangemSdkManager.kt @@ -57,7 +57,7 @@ class TangemSdkManager( get() = tangemSdk.authenticationManager.canAuthenticate || needEnrollBiometrics val needEnrollBiometrics: Boolean - get() = tangemSdk.authenticationManager.canEnrollBiometrics + get() = tangemSdk.authenticationManager.needEnrollBiometrics val keystoreManager: KeystoreManager get() = tangemSdk.keystoreManager diff --git a/app/src/main/java/com/tangem/tap/features/home/HomeFragment.kt b/app/src/main/java/com/tangem/tap/features/home/HomeFragment.kt index 2127b6e8c6..12a6e857cd 100644 --- a/app/src/main/java/com/tangem/tap/features/home/HomeFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/home/HomeFragment.kt @@ -95,7 +95,7 @@ class HomeFragment : Fragment(), StoreSubscriber { Analytics.send(IntroductionProcess.ButtonScanCard()) lifecycleScope.launch { store.dispatch( - HomeAction.ReadCard(lifecycleCoroutineScope = lifecycleScope), + HomeAction.ReadCard(scope = this), ) } }, diff --git a/app/src/main/java/com/tangem/tap/features/home/redux/HomeAction.kt b/app/src/main/java/com/tangem/tap/features/home/redux/HomeAction.kt index 033171c4f4..c15ba54403 100644 --- a/app/src/main/java/com/tangem/tap/features/home/redux/HomeAction.kt +++ b/app/src/main/java/com/tangem/tap/features/home/redux/HomeAction.kt @@ -1,10 +1,10 @@ package com.tangem.tap.features.home.redux -import androidx.lifecycle.LifecycleCoroutineScope import com.tangem.core.analytics.models.AnalyticsEvent import com.tangem.tap.common.analytics.events.AnalyticsParam import com.tangem.tap.common.analytics.events.Basic import com.tangem.tap.common.entities.IndeterminateProgressButton +import kotlinx.coroutines.CoroutineScope import org.rekotlin.Action sealed class HomeAction : Action { @@ -23,7 +23,7 @@ sealed class HomeAction : Action { */ data class ReadCard( val analyticsEvent: AnalyticsEvent? = Basic.CardWasScanned(AnalyticsParam.ScannedFrom.Introduction), - val lifecycleCoroutineScope: LifecycleCoroutineScope, + val scope: CoroutineScope, ) : HomeAction() data class ScanInProgress(val scanInProgress: Boolean) : HomeAction() diff --git a/app/src/main/java/com/tangem/tap/features/home/redux/HomeMiddleware.kt b/app/src/main/java/com/tangem/tap/features/home/redux/HomeMiddleware.kt index 48f2f37be5..e13c8cff2e 100644 --- a/app/src/main/java/com/tangem/tap/features/home/redux/HomeMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/home/redux/HomeMiddleware.kt @@ -1,6 +1,5 @@ package com.tangem.tap.features.home.redux -import androidx.lifecycle.LifecycleCoroutineScope import com.tangem.common.doOnFailure import com.tangem.common.doOnResult import com.tangem.common.doOnSuccess @@ -63,7 +62,9 @@ private fun handleHomeAction(action: Action) { store.dispatch(GlobalAction.FetchUserCountry) } is HomeAction.ReadCard -> { - readCard(action.analyticsEvent, action.lifecycleCoroutineScope) + action.scope.launch { + readCard(action.analyticsEvent) + } } is HomeAction.GoToShop -> { Analytics.send(Shop.ScreenOpened()) @@ -78,34 +79,31 @@ private fun handleHomeAction(action: Action) { } } -private fun readCard(analyticsEvent: AnalyticsEvent?, lifecycleCoroutineScope: LifecycleCoroutineScope) { - lifecycleCoroutineScope.launch { - delay(timeMillis = 200) - store.state.daggerGraphState.get(DaggerGraphState::cardSdkConfigRepository).setAccessCodeRequestPolicy( - isBiometricsRequestPolicy = preferencesStorage.shouldSaveAccessCodes, - ) +private suspend fun readCard(analyticsEvent: AnalyticsEvent?) { + store.state.daggerGraphState.get(DaggerGraphState::cardSdkConfigRepository).setAccessCodeRequestPolicy( + isBiometricsRequestPolicy = preferencesStorage.shouldSaveAccessCodes, + ) - store.state.daggerGraphState.get(DaggerGraphState::scanCardProcessor).scan( - analyticsEvent = analyticsEvent, - onProgressStateChange = { showProgress -> - if (showProgress) { - changeButtonState(ButtonState.PROGRESS) - } else { - changeButtonState(ButtonState.ENABLED) - } - }, - onScanStateChange = { scanInProgress -> - store.dispatch(HomeAction.ScanInProgress(scanInProgress)) - }, - onFailure = { - Timber.e(it, "Unable to scan card") + store.state.daggerGraphState.get(DaggerGraphState::scanCardProcessor).scan( + analyticsEvent = analyticsEvent, + onProgressStateChange = { showProgress -> + if (showProgress) { + changeButtonState(ButtonState.PROGRESS) + } else { changeButtonState(ButtonState.ENABLED) - }, - onSuccess = { scanResponse -> - proceedWithScanResponse(scanResponse) - }, - ) - } + } + }, + onScanStateChange = { scanInProgress -> + store.dispatch(HomeAction.ScanInProgress(scanInProgress)) + }, + onFailure = { + Timber.e(it, "Unable to scan card") + changeButtonState(ButtonState.ENABLED) + }, + onSuccess = { scanResponse -> + proceedWithScanResponse(scanResponse) + }, + ) } private fun proceedWithScanResponse(scanResponse: ScanResponse) = scope.launch { diff --git a/app/src/main/java/com/tangem/tap/features/intentHandler/IntentHandler.kt b/app/src/main/java/com/tangem/tap/features/intentHandler/IntentHandler.kt index ba008d4e5a..7458f759ba 100644 --- a/app/src/main/java/com/tangem/tap/features/intentHandler/IntentHandler.kt +++ b/app/src/main/java/com/tangem/tap/features/intentHandler/IntentHandler.kt @@ -6,5 +6,6 @@ import android.content.Intent [REDACTED_AUTHOR] */ interface IntentHandler { - suspend fun handleIntent(intent: Intent?): Boolean + + fun handleIntent(intent: Intent?): Boolean } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/intentHandler/handlers/BackgroundScanIntentHandler.kt b/app/src/main/java/com/tangem/tap/features/intentHandler/handlers/BackgroundScanIntentHandler.kt index 09a682cdb5..cd46a39fad 100644 --- a/app/src/main/java/com/tangem/tap/features/intentHandler/handlers/BackgroundScanIntentHandler.kt +++ b/app/src/main/java/com/tangem/tap/features/intentHandler/handlers/BackgroundScanIntentHandler.kt @@ -4,21 +4,19 @@ import android.content.Intent import android.nfc.NfcAdapter import android.nfc.Tag import android.os.Build -import androidx.lifecycle.LifecycleCoroutineScope -import com.tangem.tap.common.extensions.dispatchWithMain +import com.tangem.tap.common.extensions.dispatchOnMain import com.tangem.tap.features.home.redux.HomeAction import com.tangem.tap.features.intentHandler.IntentHandler import com.tangem.tap.features.welcome.redux.WelcomeAction import com.tangem.tap.store -import kotlinx.coroutines.delay -import kotlinx.coroutines.launch +import kotlinx.coroutines.CoroutineScope /** [REDACTED_AUTHOR] */ class BackgroundScanIntentHandler( private val hasSavedUserWalletsProvider: () -> Boolean, - private val lifecycleCoroutineScope: LifecycleCoroutineScope, + private val scope: CoroutineScope, ) : IntentHandler { private val nfcActions = arrayOf( @@ -27,7 +25,7 @@ class BackgroundScanIntentHandler( NfcAdapter.ACTION_TAG_DISCOVERED, ) - override suspend fun handleIntent(intent: Intent?): Boolean { + override fun handleIntent(intent: Intent?): Boolean { if (intent == null || intent.action !in nfcActions) return false val tag: Tag? = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { @@ -40,13 +38,9 @@ class BackgroundScanIntentHandler( intent.action = null if (hasSavedUserWalletsProvider.invoke()) { - // TODO: Remove delay after [REDACTED_JIRA] - lifecycleCoroutineScope.launch { - delay(timeMillis = 200) - store.dispatchWithMain(WelcomeAction.ProceedWithCard(lifecycleCoroutineScope)) - } + store.dispatchOnMain(WelcomeAction.ProceedWithCard) } else { - store.dispatchWithMain(HomeAction.ReadCard(lifecycleCoroutineScope = lifecycleCoroutineScope)) + store.dispatchOnMain(HomeAction.ReadCard(scope = scope)) } return true diff --git a/app/src/main/java/com/tangem/tap/features/intentHandler/handlers/BuyCurrencyIntentHandler.kt b/app/src/main/java/com/tangem/tap/features/intentHandler/handlers/BuyCurrencyIntentHandler.kt index 83c294bfb3..04c138ce0e 100644 --- a/app/src/main/java/com/tangem/tap/features/intentHandler/handlers/BuyCurrencyIntentHandler.kt +++ b/app/src/main/java/com/tangem/tap/features/intentHandler/handlers/BuyCurrencyIntentHandler.kt @@ -14,7 +14,7 @@ import com.tangem.tap.store */ class BuyCurrencyIntentHandler : IntentHandler { - override suspend fun handleIntent(intent: Intent?): Boolean { + override fun handleIntent(intent: Intent?): Boolean { val data = intent?.data ?: return false val currency = store.state.walletState.selectedCurrency ?: return false diff --git a/app/src/main/java/com/tangem/tap/features/intentHandler/handlers/SellCurrencyIntentHandler.kt b/app/src/main/java/com/tangem/tap/features/intentHandler/handlers/SellCurrencyIntentHandler.kt index 2879f9ef13..da1c6cc68a 100644 --- a/app/src/main/java/com/tangem/tap/features/intentHandler/handlers/SellCurrencyIntentHandler.kt +++ b/app/src/main/java/com/tangem/tap/features/intentHandler/handlers/SellCurrencyIntentHandler.kt @@ -2,7 +2,7 @@ package com.tangem.tap.features.intentHandler.handlers import android.content.Intent import com.tangem.domain.tokens.legacy.TradeCryptoAction -import com.tangem.tap.common.extensions.dispatchWithMain +import com.tangem.tap.common.extensions.dispatchOnMain import com.tangem.tap.features.intentHandler.IntentHandler import com.tangem.tap.store import timber.log.Timber @@ -12,7 +12,7 @@ import timber.log.Timber */ class SellCurrencyIntentHandler : IntentHandler { - override suspend fun handleIntent(intent: Intent?): Boolean { + override fun handleIntent(intent: Intent?): Boolean { return try { val intentData = intent?.data ?: return false val transactionID = intentData.getQueryParameter(TRANSACTION_ID_PARAM) ?: return false @@ -21,7 +21,7 @@ class SellCurrencyIntentHandler : IntentHandler { val destinationAddress = intentData.getQueryParameter(DEPOSIT_WALLET_ADDRESS_PARAM) ?: return false Timber.d("MoonPay Sell: $amount $currency to $destinationAddress") - store.dispatchWithMain( + store.dispatchOnMain( TradeCryptoAction.SendCrypto( currencyId = currency, amount = amount, diff --git a/app/src/main/java/com/tangem/tap/features/intentHandler/handlers/WalletConnectLinkIntentHandler.kt b/app/src/main/java/com/tangem/tap/features/intentHandler/handlers/WalletConnectLinkIntentHandler.kt index 95d2e3abe7..23f8ec2235 100644 --- a/app/src/main/java/com/tangem/tap/features/intentHandler/handlers/WalletConnectLinkIntentHandler.kt +++ b/app/src/main/java/com/tangem/tap/features/intentHandler/handlers/WalletConnectLinkIntentHandler.kt @@ -1,7 +1,7 @@ package com.tangem.tap.features.intentHandler.handlers import android.content.Intent -import com.tangem.tap.common.extensions.dispatchWithMain +import com.tangem.tap.common.extensions.dispatchOnMain import com.tangem.tap.common.extensions.removePrefixOrNull import com.tangem.tap.domain.walletconnect.WalletConnectManager import com.tangem.tap.features.details.redux.walletconnect.WalletConnectAction @@ -15,7 +15,7 @@ import java.net.URLDecoder */ class WalletConnectLinkIntentHandler : IntentHandler { - override suspend fun handleIntent(intent: Intent?): Boolean { + override fun handleIntent(intent: Intent?): Boolean { val intentData = intent?.data ?: return false val scheme = intent.scheme ?: return false @@ -34,7 +34,7 @@ class WalletConnectLinkIntentHandler : IntentHandler { Timber.e(e) return false } - store.dispatchWithMain(WalletConnectAction.HandleDeepLink(decodedWcUri)) + store.dispatchOnMain(WalletConnectAction.HandleDeepLink(decodedWcUri)) true } } diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletMiddleware.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletMiddleware.kt index e686d2f730..b46578b48b 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletMiddleware.kt @@ -152,7 +152,7 @@ private fun handleWalletAction(action: Action) { if (scanResponse == null) { store.dispatch(NavigationAction.PopBackTo()) - store.dispatch(HomeAction.ReadCard(lifecycleCoroutineScope = action.lifecycleCoroutineScope)) + store.dispatch(HomeAction.ReadCard(scope = action.lifecycleCoroutineScope)) } else { val backupState = store.state.onboardingWalletState.backupState val updatedScanResponse = updateScanResponseAfterBackup(scanResponse, backupState) diff --git a/app/src/main/java/com/tangem/tap/features/welcome/redux/WelcomeAction.kt b/app/src/main/java/com/tangem/tap/features/welcome/redux/WelcomeAction.kt index 338743a569..97138375c6 100644 --- a/app/src/main/java/com/tangem/tap/features/welcome/redux/WelcomeAction.kt +++ b/app/src/main/java/com/tangem/tap/features/welcome/redux/WelcomeAction.kt @@ -1,22 +1,27 @@ package com.tangem.tap.features.welcome.redux import android.content.Intent -import androidx.lifecycle.LifecycleCoroutineScope import com.tangem.common.core.TangemError +import kotlinx.coroutines.CoroutineScope import org.rekotlin.Action internal sealed interface WelcomeAction : Action { - object ProceedWithBiometrics : WelcomeAction { + + data class SetCoroutineScope(val scope: CoroutineScope) : WelcomeAction + + object ClearCoroutineScope : WelcomeAction + + data class ProceedWithBiometrics(val afterUnlockIntent: Intent? = null) : WelcomeAction { object Success : WelcomeAction data class Error(val error: TangemError) : WelcomeAction } - data class ProceedWithCard(val lifecycleCoroutineScope: LifecycleCoroutineScope) : WelcomeAction { + object ProceedWithCard : WelcomeAction { object Success : WelcomeAction data class Error(val error: TangemError) : WelcomeAction } - data class SetInitialIntent(val intent: Intent?) : WelcomeAction + data class ProceedWithIntent(val intent: Intent) : WelcomeAction object CloseError : WelcomeAction diff --git a/app/src/main/java/com/tangem/tap/features/welcome/redux/WelcomeMiddleware.kt b/app/src/main/java/com/tangem/tap/features/welcome/redux/WelcomeMiddleware.kt index 1ab1cee43d..164ef24c14 100644 --- a/app/src/main/java/com/tangem/tap/features/welcome/redux/WelcomeMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/welcome/redux/WelcomeMiddleware.kt @@ -1,6 +1,6 @@ package com.tangem.tap.features.welcome.redux -import androidx.lifecycle.LifecycleCoroutineScope +import android.content.Intent import com.tangem.common.core.TangemSdkError import com.tangem.common.doOnFailure import com.tangem.common.doOnResult @@ -18,9 +18,11 @@ import com.tangem.tap.common.extensions.dispatchOnMain import com.tangem.tap.common.extensions.dispatchWithMain import com.tangem.tap.common.extensions.onUserWalletSelected import com.tangem.tap.common.redux.AppState +import com.tangem.tap.features.intentHandler.handlers.BackgroundScanIntentHandler import com.tangem.tap.features.intentHandler.handlers.WalletConnectLinkIntentHandler import com.tangem.tap.features.signin.redux.SignInAction import com.tangem.tap.proxy.redux.DaggerGraphState +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch import org.rekotlin.Middleware import timber.log.Timber @@ -39,15 +41,94 @@ internal class WelcomeMiddleware { } private fun handleAction(action: WelcomeAction, state: WelcomeState) { - when (action) { - is WelcomeAction.ProceedWithBiometrics -> proceedWithBiometrics(state) - is WelcomeAction.ProceedWithCard -> proceedWithCard(state, action.lifecycleCoroutineScope) - is WelcomeAction.ClearUserWallets -> disableUserWalletsSaving() - else -> Unit + state.scope?.launch { + when (action) { + is WelcomeAction.ProceedWithIntent -> proceedWithIntent(action.intent, scope = this) + is WelcomeAction.ProceedWithBiometrics -> proceedWithBiometrics( + afterUnlockIntent = action.afterUnlockIntent ?: state.intent, + ) + is WelcomeAction.ProceedWithCard -> proceedWithCard(afterScanIntent = state.intent) + is WelcomeAction.ClearUserWallets -> disableUserWalletsSaving() + else -> Unit + } } } - private fun disableUserWalletsSaving() = scope.launch { + private suspend fun proceedWithIntent(initialIntent: Intent, scope: CoroutineScope) { + Timber.d( + """ + Proceeding with intent + |- Intent: $initialIntent + """.trimIndent(), + ) + + val handler = BackgroundScanIntentHandler( + scope = scope, + hasSavedUserWalletsProvider = { true }, + ) + val isBackgroundScanHandled = handler.handleIntent(initialIntent) + val hasUncompletedBackup = backupService.hasIncompletedBackup + + if (!isBackgroundScanHandled && !hasUncompletedBackup) { + store.dispatchWithMain(WelcomeAction.ProceedWithBiometrics(initialIntent)) + } + } + + private suspend fun proceedWithBiometrics(afterUnlockIntent: Intent?) { + Timber.d( + """ + Proceeding with biometry + |- Intent: $afterUnlockIntent + """.trimIndent(), + ) + + userWalletsListManager.unlockIfLockable() + .doOnFailure { error -> + Timber.e(error, "Unable to unlock user wallets with biometrics") + store.dispatchWithMain(WelcomeAction.ProceedWithBiometrics.Error(error)) + } + .doOnSuccess { selectedUserWallet -> + store.dispatchWithMain(SignInAction.SetSignInType(Basic.SignedIn.SignInType.Biometric)) + store.dispatchWithMain(NavigationAction.NavigateTo(AppScreen.Wallet)) + store.dispatchWithMain(WelcomeAction.ProceedWithBiometrics.Success) + store.onUserWalletSelected(userWallet = selectedUserWallet) + + afterUnlockIntent?.let { + WalletConnectLinkIntentHandler().handleIntent(it) + } + } + } + + private suspend fun proceedWithCard(afterScanIntent: Intent?) { + Timber.d( + """ + Proceeding with card + |- Intent: $afterScanIntent + """.trimIndent(), + ) + + scanCardInternal { scanResponse -> + val userWallet = UserWalletBuilder(scanResponse).build() ?: return@scanCardInternal + + userWalletsListManager.save(userWallet, canOverride = true) + .doOnFailure { error -> + Timber.e(error, "Unable to save user wallet") + store.dispatchWithMain(WelcomeAction.ProceedWithCard.Error(error)) + } + .doOnSuccess { + store.dispatchWithMain(SignInAction.SetSignInType(Basic.SignedIn.SignInType.Card)) + store.dispatchWithMain(NavigationAction.NavigateTo(AppScreen.Wallet)) + store.dispatchWithMain(WelcomeAction.ProceedWithCard.Success) + store.onUserWalletSelected(userWallet = userWallet) + + afterScanIntent?.let { + WalletConnectLinkIntentHandler().handleIntent(it) + } + } + } + } + + private suspend fun disableUserWalletsSaving() { userWalletsListManager.clear() .flatMap { walletStoresManager.clear() } .flatMap { tangemSdkManager.clearSavedUserCodes() } @@ -60,48 +141,6 @@ internal class WelcomeMiddleware { } } - private fun proceedWithBiometrics(state: WelcomeState) = scope.launch { - userWalletsListManager.unlockIfLockable() - .doOnFailure { error -> - Timber.e(error, "Unable to unlock user wallets with biometrics") - store.dispatchOnMain(WelcomeAction.ProceedWithBiometrics.Error(error)) - } - .doOnSuccess { selectedUserWallet -> - store.dispatchOnMain(SignInAction.SetSignInType(Basic.SignedIn.SignInType.Biometric)) - store.dispatchOnMain(NavigationAction.NavigateTo(AppScreen.Wallet)) - store.dispatchOnMain(WelcomeAction.ProceedWithBiometrics.Success) - store.onUserWalletSelected(userWallet = selectedUserWallet) - - state.intent?.let { - WalletConnectLinkIntentHandler().handleIntent(it) - } - } - } - - private fun proceedWithCard(state: WelcomeState, lifecycleCoroutineScope: LifecycleCoroutineScope) { - lifecycleCoroutineScope.launch { - scanCardInternal { scanResponse -> - val userWallet = UserWalletBuilder(scanResponse).build() ?: return@scanCardInternal - - userWalletsListManager.save(userWallet, canOverride = true) - .doOnFailure { error -> - Timber.e(error, "Unable to save user wallet") - store.dispatchOnMain(WelcomeAction.ProceedWithCard.Error(error)) - } - .doOnSuccess { - store.dispatchOnMain(SignInAction.SetSignInType(Basic.SignedIn.SignInType.Card)) - store.dispatchOnMain(NavigationAction.NavigateTo(AppScreen.Wallet)) - store.dispatchOnMain(WelcomeAction.ProceedWithCard.Success) - store.onUserWalletSelected(userWallet = userWallet) - - state.intent?.let { - WalletConnectLinkIntentHandler().handleIntent(it) - } - } - } - } - } - private suspend inline fun scanCardInternal(crossinline onCardScanned: suspend (ScanResponse) -> Unit) { store.state.daggerGraphState.get(DaggerGraphState::cardSdkConfigRepository).setAccessCodeRequestPolicy( isBiometricsRequestPolicy = preferencesStorage.shouldSaveAccessCodes, diff --git a/app/src/main/java/com/tangem/tap/features/welcome/redux/WelcomeReducer.kt b/app/src/main/java/com/tangem/tap/features/welcome/redux/WelcomeReducer.kt index 11bfb0ea4a..85db06dc63 100644 --- a/app/src/main/java/com/tangem/tap/features/welcome/redux/WelcomeReducer.kt +++ b/app/src/main/java/com/tangem/tap/features/welcome/redux/WelcomeReducer.kt @@ -14,7 +14,9 @@ internal object WelcomeReducer { private fun internalReduce(action: WelcomeAction, state: WelcomeState): WelcomeState { return when (action) { - is WelcomeAction.SetInitialIntent -> state.copy(intent = action.intent) + is WelcomeAction.SetCoroutineScope -> state.copy(scope = action.scope) + is WelcomeAction.ClearCoroutineScope -> state.copy(scope = null) + is WelcomeAction.ProceedWithIntent -> state.copy(intent = action.intent) is WelcomeAction.ProceedWithBiometrics -> state.copy(isUnlockWithBiometricsInProgress = true) is WelcomeAction.ProceedWithCard -> state.copy(isUnlockWithCardInProgress = true) is WelcomeAction.ProceedWithBiometrics.Error -> state.copy( diff --git a/app/src/main/java/com/tangem/tap/features/welcome/redux/WelcomeState.kt b/app/src/main/java/com/tangem/tap/features/welcome/redux/WelcomeState.kt index a8a55d3984..951fa1e673 100644 --- a/app/src/main/java/com/tangem/tap/features/welcome/redux/WelcomeState.kt +++ b/app/src/main/java/com/tangem/tap/features/welcome/redux/WelcomeState.kt @@ -2,9 +2,11 @@ package com.tangem.tap.features.welcome.redux import android.content.Intent import com.tangem.common.core.TangemError +import kotlinx.coroutines.CoroutineScope import org.rekotlin.StateType data class WelcomeState( + val scope: CoroutineScope? = null, val isUnlockWithBiometricsInProgress: Boolean = false, val isUnlockWithCardInProgress: Boolean = false, val intent: Intent? = null, diff --git a/app/src/main/java/com/tangem/tap/features/welcome/ui/WelcomeFragment.kt b/app/src/main/java/com/tangem/tap/features/welcome/ui/WelcomeFragment.kt index f3d9cd27f1..c3197818c0 100644 --- a/app/src/main/java/com/tangem/tap/features/welcome/ui/WelcomeFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/welcome/ui/WelcomeFragment.kt @@ -11,9 +11,9 @@ import androidx.compose.material.SnackbarHostState import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.fragment.app.viewModels +import androidx.compose.ui.platform.LocalLifecycleOwner +import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle -import androidx.lifecycle.lifecycleScope import com.tangem.core.analytics.Analytics import com.tangem.core.ui.components.SystemBarsEffect import com.tangem.core.ui.res.TangemTheme @@ -33,8 +33,6 @@ internal class WelcomeFragment : ComposeFragment() { @Inject override lateinit var appThemeModeHolder: AppThemeModeHolder - private val viewModel by viewModels() - override fun onStart() { super.onStart() Analytics.eraseContext() @@ -43,6 +41,9 @@ internal class WelcomeFragment : ComposeFragment() { @Composable override fun ScreenContent(modifier: Modifier) { + val viewModel = hiltViewModel() + LocalLifecycleOwner.current.lifecycle.addObserver(viewModel) + val state by viewModel.state.collectAsStateWithLifecycle() val snackbarHostState = remember { SnackbarHostState() } val errorMessage by rememberUpdatedState(newValue = state.error?.resolveReference()) @@ -66,7 +67,7 @@ internal class WelcomeFragment : ComposeFragment() { showUnlockProgress = state.showUnlockWithBiometricsProgress, showScanCardProgress = state.showUnlockWithCardProgress, onUnlockClick = viewModel::unlockWallets, - onScanCardClick = { viewModel.scanCard(lifecycleCoroutineScope = lifecycleScope) }, + onScanCardClick = viewModel::scanCard, ) SnackbarHost( @@ -87,4 +88,8 @@ internal class WelcomeFragment : ComposeFragment() { } } } + + internal companion object { + const val INITIAL_INTENT_KEY = "intent" + } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/welcome/ui/WelcomeViewModel.kt b/app/src/main/java/com/tangem/tap/features/welcome/ui/WelcomeViewModel.kt index 4f7587289d..fbd107685e 100644 --- a/app/src/main/java/com/tangem/tap/features/welcome/ui/WelcomeViewModel.kt +++ b/app/src/main/java/com/tangem/tap/features/welcome/ui/WelcomeViewModel.kt @@ -1,7 +1,7 @@ package com.tangem.tap.features.welcome.ui -import androidx.lifecycle.LifecycleCoroutineScope -import androidx.lifecycle.ViewModel +import android.content.Intent +import androidx.lifecycle.* import com.tangem.common.core.TangemError import com.tangem.core.analytics.Analytics import com.tangem.domain.wallets.legacy.UserWalletsListError @@ -12,28 +12,50 @@ import com.tangem.tap.features.welcome.redux.WelcomeAction import com.tangem.tap.features.welcome.redux.WelcomeState import com.tangem.tap.features.welcome.ui.model.WarningModel import com.tangem.tap.store +import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.update import org.rekotlin.StoreSubscriber +import javax.inject.Inject + +@HiltViewModel +internal class WelcomeViewModel @Inject constructor( + savedStateHandle: SavedStateHandle, +) : ViewModel(), + StoreSubscriber, + DefaultLifecycleObserver { + + private val initialIntent: Intent? = savedStateHandle[WelcomeFragment.INITIAL_INTENT_KEY] -internal class WelcomeViewModel : ViewModel(), StoreSubscriber { private val stateInternal = MutableStateFlow(WelcomeScreenState()) val state: StateFlow = stateInternal init { + store.dispatch(WelcomeAction.SetCoroutineScope(viewModelScope)) + subscribeToStoreChanges() initGlobalState() } - fun unlockWallets() { - Analytics.send(SignIn.ButtonBiometricSignIn()) - store.dispatch(WelcomeAction.ProceedWithBiometrics) + override fun onCreate(owner: LifecycleOwner) { + val welcomeAction = if (initialIntent != null) { + WelcomeAction.ProceedWithIntent(initialIntent) + } else { + WelcomeAction.ProceedWithBiometrics() + } + + store.dispatch(welcomeAction) } - fun scanCard(lifecycleCoroutineScope: LifecycleCoroutineScope) { + fun unlockWallets() { + Analytics.send(SignIn.ButtonBiometricSignIn()) + store.dispatch(WelcomeAction.ProceedWithBiometrics()) + } + + fun scanCard() { Analytics.send(SignIn.ButtonCardSignIn()) - store.dispatch(WelcomeAction.ProceedWithCard(lifecycleCoroutineScope)) + store.dispatch(WelcomeAction.ProceedWithCard) } fun closeError() { @@ -60,6 +82,7 @@ internal class WelcomeViewModel : ViewModel(), StoreSubscriber { } override fun onCleared() { + store.dispatch(WelcomeAction.ClearCoroutineScope) store.unsubscribe(this) } diff --git a/gradle/dependencies.toml b/gradle/dependencies.toml index 3ecbc0bcfb..f0b0c1d42f 100644 --- a/gradle/dependencies.toml +++ b/gradle/dependencies.toml @@ -84,7 +84,7 @@ okHttp-prettyLogging = "3.1.0" # region Tangem tangemBlockchainSdk = "develop-354" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds -tangemCardSdk = "develop-300" +tangemCardSdk = "develop-302" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds # endregion Tangem From eee82855221fa632bfe3df6749040dff28fef974 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 6 Oct 2023 15:11:14 +0500 Subject: [PATCH 141/242] Updated on 2026-08-14 --- .../state/factory/TokenDetailsSkeletonStateConverter.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSkeletonStateConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSkeletonStateConverter.kt index 6fa5307dde..75ce9259fb 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSkeletonStateConverter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSkeletonStateConverter.kt @@ -53,7 +53,7 @@ internal class TokenDetailsSkeletonStateConverter( pullToRefreshConfig = createPullToRefresh(), bottomSheetConfig = null, isBalanceHidden = true, - isCustomToken = value.isCustom, + isCustomToken = value is CryptoCurrency.Token && value.isCustom, ) } From caa4d036dcb5ea82d10213ba8fd07d3f2f4391d6 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 6 Oct 2023 13:31:22 +0300 Subject: [PATCH 142/242] Updated on 2026-08-14 --- .../presentation/wallet/state/components/WalletCardState.kt | 3 ++- .../presentation/wallet/ui/components/common/WalletCard.kt | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/components/WalletCardState.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/components/WalletCardState.kt index c968de0013..8edccf6a0f 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/components/WalletCardState.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/components/WalletCardState.kt @@ -2,6 +2,7 @@ package com.tangem.feature.wallet.presentation.wallet.state.components import androidx.annotation.DrawableRes import androidx.compose.runtime.Immutable +import com.tangem.common.Strings import com.tangem.core.ui.extensions.TextReference import com.tangem.domain.wallets.models.UserWalletId @@ -135,7 +136,7 @@ internal sealed interface WalletCardState { } companion object { - val HIDDEN_BALANCE_TEXT by lazy { TextReference.Str(value = "•••") } + val HIDDEN_BALANCE_TEXT by lazy { TextReference.Str(value = Strings.STARS) } val EMPTY_BALANCE_TEXT by lazy { TextReference.Str(value = "—") } } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletCard.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletCard.kt index 4cd547a875..d7dad0e9e2 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletCard.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletCard.kt @@ -317,9 +317,9 @@ private fun AdditionalInfo(state: WalletCardState, modifier: Modifier = Modifier ) { animatedState -> when (animatedState) { is WalletCardState.Content -> AdditionalInfoText(text = animatedState.additionalInfo) + is WalletCardState.HiddenContent -> AdditionalInfoText(text = animatedState.additionalInfo) is WalletCardState.LockedContent -> AdditionalInfoText(text = animatedState.additionalInfo) is WalletCardState.Error -> AdditionalInfoText(text = WalletCardState.EMPTY_BALANCE_TEXT) - is WalletCardState.HiddenContent -> AdditionalInfoText(text = WalletCardState.HIDDEN_BALANCE_TEXT) is WalletCardState.Loading -> { RectangleShimmer(modifier = Modifier.nonContentAdditionalInfoSize(dimens = TangemTheme.dimens)) } From 7a2847ed17a7254f73e3d3fa07a0f6e7eb2894cd Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 6 Oct 2023 15:53:02 +0300 Subject: [PATCH 143/242] Updated on 2026-08-14 --- ...letSingleCurrencyLoadedBalanceConverter.kt | 43 +++++++++++++------ .../state/factory/WalletStateFactory.kt | 1 + .../wallet/utils/HiddenStateConverter.kt | 2 + 3 files changed, 33 insertions(+), 13 deletions(-) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletSingleCurrencyLoadedBalanceConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletSingleCurrencyLoadedBalanceConverter.kt index ccfbe015bc..6f24e4d02b 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletSingleCurrencyLoadedBalanceConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletSingleCurrencyLoadedBalanceConverter.kt @@ -26,6 +26,7 @@ internal class WalletSingleCurrencyLoadedBalanceConverter( private val currentStateProvider: Provider, private val appCurrencyProvider: Provider, private val currentWalletProvider: Provider, + private val isBalanceHiddenProvider: Provider, private val currencyStatusErrorConverter: CurrencyStatusErrorConverter, ) : Converter, WalletState> { @@ -86,19 +87,35 @@ internal class WalletSingleCurrencyLoadedBalanceConverter( is CryptoCurrencyStatus.NoQuote, is CryptoCurrencyStatus.Loaded, -> { - WalletCardState.Content( - id = selectedWallet.id, - title = selectedWallet.title, - additionalInfo = WalletAdditionalInfoFactory.resolve( - wallet = currentWalletProvider(), - currencyAmount = status.amount, - ), - imageResId = selectedWallet.imageResId, - onRenameClick = selectedWallet.onRenameClick, - onDeleteClick = selectedWallet.onDeleteClick, - balance = formatFiatAmount(status = status, appCurrency = appCurrencyProvider()), - cardCount = currentWalletProvider().getCardsCount(), - ) + if (isBalanceHiddenProvider()) { + WalletCardState.HiddenContent( + id = selectedWallet.id, + title = selectedWallet.title, + additionalInfo = WalletAdditionalInfoFactory.resolve( + wallet = currentWalletProvider(), + currencyAmount = status.amount, + ), + imageResId = selectedWallet.imageResId, + onRenameClick = selectedWallet.onRenameClick, + onDeleteClick = selectedWallet.onDeleteClick, + balance = formatFiatAmount(status = status, appCurrency = appCurrencyProvider()), + cardCount = currentWalletProvider().getCardsCount(), + ) + } else { + WalletCardState.Content( + id = selectedWallet.id, + title = selectedWallet.title, + additionalInfo = WalletAdditionalInfoFactory.resolve( + wallet = currentWalletProvider(), + currencyAmount = status.amount, + ), + imageResId = selectedWallet.imageResId, + onRenameClick = selectedWallet.onRenameClick, + onDeleteClick = selectedWallet.onDeleteClick, + balance = formatFiatAmount(status = status, appCurrency = appCurrencyProvider()), + cardCount = currentWalletProvider().getCardsCount(), + ) + } } is CryptoCurrencyStatus.Loading -> { WalletCardState.Loading( diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletStateFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletStateFactory.kt index a46a50cc2c..4354bc8992 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletStateFactory.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletStateFactory.kt @@ -106,6 +106,7 @@ internal class WalletStateFactory( currentStateProvider = currentStateProvider, appCurrencyProvider = appCurrencyProvider, currentWalletProvider = currentWalletProvider, + isBalanceHiddenProvider = isBalanceHiddenProvider, currencyStatusErrorConverter = currencyStatusErrorConverter, ) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/HiddenStateConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/HiddenStateConverter.kt index ae1528ddaf..837ac11b52 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/HiddenStateConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/HiddenStateConverter.kt @@ -51,6 +51,7 @@ internal class HiddenStateConverter( }.toImmutableList(), ), tokensListState = updatedTokensList, + isBalanceHidden = value ) } @@ -61,6 +62,7 @@ internal class HiddenStateConverter( walletHiddenBalanceStateConverter.updateHiddenState(it, value) }.toImmutableList(), ), + isBalanceHidden = value ) } From 81ad4db39282734784fc0a55296667af70eac4ce Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 2 Oct 2023 18:10:53 +0800 Subject: [PATCH 144/242] Updated on 2026-08-14 --- .../core/ui/components/ResizableText.kt | 10 +- .../domain/common/TangemCardTypesResolver.kt | 2 +- .../presentation/common/WalletPreviewData.kt | 16 +- .../state/components/WalletCardState.kt | 3 +- .../wallet/ui/components/common/WalletCard.kt | 151 +++++++++++------- 5 files changed, 109 insertions(+), 73 deletions(-) diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/ResizableText.kt b/core/ui/src/main/java/com/tangem/core/ui/components/ResizableText.kt index d4d1abb476..8801cb7983 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/ResizableText.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/ResizableText.kt @@ -20,6 +20,8 @@ fun ResizableText( fontSizeRange: FontSizeRange, modifier: Modifier = Modifier, color: Color = Color.Unspecified, + overflow: TextOverflow = TextOverflow.Clip, + maxLines: Int = Int.MAX_VALUE, style: TextStyle = LocalTextStyle.current, ) { val fontSizeValue = remember { mutableStateOf(fontSizeRange.max.value) } @@ -33,12 +35,13 @@ fun ResizableText( } Text( - modifier = modifier.drawWithContent { if (readyToDraw.value) drawContent() }, text = text, + modifier = modifier.drawWithContent { if (readyToDraw.value) drawContent() }, color = color, - softWrap = false, - style = style, fontSize = fontSizeValue.value.sp, + overflow = overflow, + softWrap = false, + maxLines = maxLines, onTextLayout = { if (it.hasVisualOverflow) { val nextFontSizeValue = fontSizeValue.value - fontSizeRange.step.value @@ -52,6 +55,7 @@ fun ResizableText( readyToDraw.value = true } }, + style = style, ) } 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 6437bc105d..65550e62f4 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 @@ -26,7 +26,7 @@ internal class TangemCardTypesResolver( } override fun isWhiteWallet(): Boolean { - return walletData == null && card.firmwareVersion >= FirmwareVersion.HDWalletAvailable + return walletData == null && card.firmwareVersion <= FirmwareVersion.HDWalletAvailable } override fun isWallet2(): Boolean { diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/WalletPreviewData.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/WalletPreviewData.kt index a0cfeb80f9..22e3dba93f 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/WalletPreviewData.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/WalletPreviewData.kt @@ -1,7 +1,6 @@ package com.tangem.feature.wallet.presentation.common import androidx.paging.PagingData -import com.tangem.core.ui.R import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.components.marketprice.MarketPriceBlockState import com.tangem.core.ui.components.marketprice.PriceChangeState @@ -13,6 +12,7 @@ import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.res.TangemColorPalette import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.feature.wallet.impl.R import com.tangem.feature.wallet.presentation.common.state.TokenItemState import com.tangem.feature.wallet.presentation.organizetokens.model.DraggableItem import com.tangem.feature.wallet.presentation.organizetokens.model.OrganizeTokensListState @@ -37,10 +37,10 @@ internal object WalletPreviewData { val walletCardContentState by lazy { WalletCardState.Content( id = UserWalletId(stringValue = "123"), - title = "Wallet 1", - balance = "8923,05 $", - additionalInfo = TextReference.Str("3 cards • Seed phrase"), - imageResId = R.drawable.ill_businessman_3d, + title = "Wallet1Wallet1Wallet1Wallet1Wallet1Wallet1Wallet1Wallet1", + balance = "8923,05312312312312312312331231231233432423423424234 $", + additionalInfo = TextReference.Str("3 cards • Seed phrase3 cards • Seed phraseцфвцфвфцвцфввцфвцф"), + imageResId = R.drawable.ill_wallet2_cards3_120_106, onRenameClick = { _, _ -> }, onDeleteClick = {}, cardCount = 1, @@ -51,7 +51,7 @@ internal object WalletPreviewData { WalletCardState.Loading( id = UserWalletId("321"), title = "Wallet 1", - imageResId = R.drawable.ill_businessman_3d, + imageResId = R.drawable.ill_wallet2_cards3_120_106, onRenameClick = { _, _ -> }, onDeleteClick = {}, ) @@ -61,7 +61,7 @@ internal object WalletPreviewData { WalletCardState.HiddenContent( id = UserWalletId("42"), title = "Wallet 1", - imageResId = R.drawable.ill_businessman_3d, + imageResId = R.drawable.ill_wallet2_cards3_120_106, onRenameClick = { _, _ -> }, onDeleteClick = {}, balance = "8923,05 $", @@ -74,7 +74,7 @@ internal object WalletPreviewData { WalletCardState.Error( id = UserWalletId("24"), title = "Wallet 1", - imageResId = R.drawable.ill_businessman_3d, + imageResId = R.drawable.ill_wallet2_cards3_120_106, onRenameClick = { _, _ -> }, onDeleteClick = {}, ) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/components/WalletCardState.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/components/WalletCardState.kt index c968de0013..8edccf6a0f 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/components/WalletCardState.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/components/WalletCardState.kt @@ -2,6 +2,7 @@ package com.tangem.feature.wallet.presentation.wallet.state.components import androidx.annotation.DrawableRes import androidx.compose.runtime.Immutable +import com.tangem.common.Strings import com.tangem.core.ui.extensions.TextReference import com.tangem.domain.wallets.models.UserWalletId @@ -135,7 +136,7 @@ internal sealed interface WalletCardState { } companion object { - val HIDDEN_BALANCE_TEXT by lazy { TextReference.Str(value = "•••") } + val HIDDEN_BALANCE_TEXT by lazy { TextReference.Str(value = Strings.STARS) } val EMPTY_BALANCE_TEXT by lazy { TextReference.Str(value = "—") } } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletCard.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletCard.kt index 4cd547a875..ffd3f192de 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletCard.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletCard.kt @@ -2,9 +2,8 @@ package com.tangem.feature.wallet.presentation.wallet.ui.components.common import androidx.annotation.DrawableRes import androidx.annotation.StringRes -import androidx.compose.animation.AnimatedContent -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.ExperimentalAnimationApi +import androidx.compose.animation.* +import androidx.compose.animation.core.tween import androidx.compose.foundation.* import androidx.compose.foundation.gestures.detectTapGestures import androidx.compose.foundation.interaction.MutableInteractionSource @@ -28,16 +27,12 @@ import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider -import androidx.compose.ui.unit.Dp -import androidx.compose.ui.unit.DpOffset -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import androidx.constraintlayout.compose.ConstraintLayout -import androidx.constraintlayout.compose.ConstraintLayoutScope -import androidx.constraintlayout.compose.Dimension +import androidx.compose.ui.unit.* +import androidx.constraintlayout.compose.* import com.tangem.core.ui.components.FontSizeRange import com.tangem.core.ui.components.RectangleShimmer import com.tangem.core.ui.components.ResizableText @@ -50,6 +45,8 @@ import com.tangem.feature.wallet.impl.R import com.tangem.feature.wallet.presentation.common.WalletPreviewData import com.tangem.feature.wallet.presentation.wallet.state.components.WalletCardState +private const val HALF_OF_ITEM_WIDTH = 0.5 + /** * Wallet card * @@ -67,47 +64,65 @@ internal fun WalletCard(state: WalletCardState, modifier: Modifier = Modifier) { onRenameClick = { state.onRenameClick(state.id, it) }, isLockedState = state is WalletCardState.LockedContent, modifier = modifier, - ) { - val (title, balance, additionalText, image) = createRefs() + ) { itemSize -> + val (titleRef, balanceRef, additionalTextRef, imageRef) = createRefs() val contentVerticalMargin = TangemTheme.dimens.spacing12 - Title( - state = state, - modifier = Modifier.constrainAs(title) { + TitleText( + text = state.title, + modifier = Modifier.constrainAs(titleRef) { start.linkTo(parent.start) top.linkTo(anchor = parent.top, margin = contentVerticalMargin) - end.linkTo(image.start) + end.linkTo(imageRef.start) width = Dimension.fillToConstraints }, ) - val betweenContentMargin = TangemTheme.dimens.spacing8 + var balanceWidth by remember { mutableStateOf(value = Int.MIN_VALUE) } Balance( state = state, - modifier = Modifier.constrainAs(balance) { - start.linkTo(parent.start) - top.linkTo(anchor = title.bottom, margin = betweenContentMargin) - bottom.linkTo(anchor = additionalText.top, margin = betweenContentMargin) - }, + modifier = Modifier + .onSizeChanged { balanceWidth = it.width } + .padding(vertical = TangemTheme.dimens.spacing8) + .constrainAs(balanceRef) { + start.linkTo(parent.start) + top.linkTo(anchor = titleRef.bottom) + bottom.linkTo(anchor = additionalTextRef.top) + }, ) AdditionalInfo( - state = state, - modifier = Modifier.constrainAs(additionalText) { + text = resolveAdditionalTextByState(state), + modifier = Modifier.constrainAs(additionalTextRef) { start.linkTo(parent.start) + top.linkTo(balanceRef.bottom) bottom.linkTo(anchor = parent.bottom, margin = contentVerticalMargin) + + when (state) { + is WalletCardState.Content, + is WalletCardState.Error, + is WalletCardState.HiddenContent, + -> { + end.linkTo(imageRef.start) + width = Dimension.fillToConstraints + } + else -> Unit + } }, ) - val imageWidth = TangemTheme.dimens.size120 + // If balance has a large width then image must be hidden + val hasSpaceForImage by remember(key1 = balanceWidth, key2 = itemSize.width) { + mutableStateOf(value = balanceWidth < itemSize.width * HALF_OF_ITEM_WIDTH) + } + Image( id = state.imageResId, - modifier = Modifier.constrainAs(image) { - centerVerticallyTo(parent) - top.linkTo(parent.top) + isVisible = hasSpaceForImage, + modifier = Modifier.constrainAs(imageRef) { end.linkTo(parent.end) + bottom.linkTo(parent.bottom) height = Dimension.fillToConstraints - width = Dimension.value(imageWidth) }, ) } @@ -120,11 +135,11 @@ private fun CardContainer( onRenameClick: (String) -> Unit, isLockedState: Boolean, modifier: Modifier = Modifier, - content: @Composable (ConstraintLayoutScope.() -> Unit), + content: @Composable (ConstraintLayoutScope.(IntSize) -> Unit), ) { var isMenuVisible by rememberSaveable { mutableStateOf(value = false) } var pressOffset by remember { mutableStateOf(value = DpOffset.Zero) } - var itemHeight by remember { mutableStateOf(value = 0.dp) } + var itemSize by remember { mutableStateOf(value = IntSize.Zero) } val density = LocalDensity.current val interactionSource = remember { MutableInteractionSource() } @@ -138,7 +153,7 @@ private fun CardContainer( Modifier } else { Modifier - .onSizeChanged { itemHeight = with(density) { it.height.toDp() } } + .onSizeChanged { itemSize = it } .clip(shape = TangemTheme.shapes.roundedCornersXMedium) .indication(interactionSource = interactionSource, indication = LocalIndication.current) .pointerInput(true) { @@ -166,12 +181,15 @@ private fun CardContainer( .fillMaxWidth() .padding(horizontal = TangemTheme.dimens.spacing14), ) { - content() + content(itemSize) } } var isRenameWalletDialogVisible by rememberSaveable { mutableStateOf(value = false) } + val itemHeight by remember(itemSize.height) { + mutableStateOf(value = with(density) { itemSize.height.toDp() }) + } ManageWalletContextMenu( isMenuVisible = isMenuVisible, pressOffset = pressOffset, @@ -243,22 +261,14 @@ private fun MenuItem(@StringRes textResId: Int, imageVector: ImageVector, onClic } @Composable -private fun Title(state: WalletCardState, modifier: Modifier = Modifier) { - Row( - modifier = modifier, - horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing4), - ) { - TitleText(title = state.title) - } -} - -@Composable -private fun TitleText(title: String) { +private fun TitleText(text: String, modifier: Modifier = Modifier) { Text( - text = title, + text = text, + modifier = modifier, color = TangemTheme.colors.text.tertiary, - style = TangemTheme.typography.button, maxLines = 1, + overflow = TextOverflow.Ellipsis, + style = TangemTheme.typography.button, ) } @@ -269,6 +279,10 @@ private fun Balance(state: WalletCardState, modifier: Modifier = Modifier) { targetState = state, label = "Update the balance", modifier = modifier, + transitionSpec = { + fadeIn(animationSpec = tween(durationMillis = 220, delayMillis = 90)) with + fadeOut(animationSpec = tween(durationMillis = 90)) + }, ) { walletCardState -> when (walletCardState) { is WalletCardState.Content -> { @@ -277,6 +291,8 @@ private fun Balance(state: WalletCardState, modifier: Modifier = Modifier) { fontSizeRange = FontSizeRange(min = 16.sp, max = TangemTheme.typography.h2.fontSize), modifier = Modifier.defaultMinSize(minHeight = TangemTheme.dimens.size32), color = TangemTheme.colors.text.primary1, + overflow = TextOverflow.Ellipsis, + maxLines = 1, style = TangemTheme.typography.h2, ) } @@ -309,29 +325,41 @@ private fun Modifier.nonContentBalanceSize(dimens: TangemDimens): Modifier { @OptIn(ExperimentalAnimationApi::class) @Composable -private fun AdditionalInfo(state: WalletCardState, modifier: Modifier = Modifier) { +private fun AdditionalInfo(text: TextReference?, modifier: Modifier = Modifier) { AnimatedContent( - targetState = state, + targetState = text, label = "Update the additional text", modifier = modifier, - ) { animatedState -> - when (animatedState) { - is WalletCardState.Content -> AdditionalInfoText(text = animatedState.additionalInfo) - is WalletCardState.LockedContent -> AdditionalInfoText(text = animatedState.additionalInfo) - is WalletCardState.Error -> AdditionalInfoText(text = WalletCardState.EMPTY_BALANCE_TEXT) - is WalletCardState.HiddenContent -> AdditionalInfoText(text = WalletCardState.HIDDEN_BALANCE_TEXT) - is WalletCardState.Loading -> { - RectangleShimmer(modifier = Modifier.nonContentAdditionalInfoSize(dimens = TangemTheme.dimens)) - } + transitionSpec = { + fadeIn(animationSpec = tween(durationMillis = 220, delayMillis = 90)) with + fadeOut(animationSpec = tween(durationMillis = 90)) + }, + ) { animatedText -> + if (animatedText != null) { + AdditionalInfoText(text = animatedText) + } else { + RectangleShimmer(modifier = Modifier.nonContentAdditionalInfoSize(dimens = TangemTheme.dimens)) } } } +private fun resolveAdditionalTextByState(state: WalletCardState): TextReference? { + return when (state) { + is WalletCardState.Content -> state.additionalInfo + is WalletCardState.LockedContent -> state.additionalInfo + is WalletCardState.Error -> WalletCardState.EMPTY_BALANCE_TEXT + is WalletCardState.HiddenContent -> WalletCardState.HIDDEN_BALANCE_TEXT + is WalletCardState.Loading -> null + } +} + @Composable private fun AdditionalInfoText(text: TextReference) { Text( text = text.resolveReference(), color = TangemTheme.colors.text.tertiary, + maxLines = 1, + overflow = TextOverflow.Ellipsis, style = TangemTheme.typography.caption, ) } @@ -351,11 +379,14 @@ private fun LockedContent(modifier: Modifier = Modifier) { } @Composable -private fun Image(@DrawableRes id: Int?, modifier: Modifier = Modifier) { - AnimatedVisibility(visible = id != null, modifier = modifier) { +private fun Image(@DrawableRes id: Int?, isVisible: Boolean, modifier: Modifier = Modifier) { + AnimatedVisibility(visible = id != null && isVisible, modifier = modifier) { + val imageRes = id ?: return@AnimatedVisibility + Image( - painter = painterResource(id = requireNotNull(id)), + painter = painterResource(id = imageRes), contentDescription = null, + modifier = Modifier.width(width = TangemTheme.dimens.size120), contentScale = ContentScale.FillWidth, ) } From 5bbbd9e945cd2cbfc1f445c1252330b987a64a7c Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 5 Oct 2023 17:41:54 +0300 Subject: [PATCH 145/242] Updated on 2026-08-14 --- .../viewmodels/AddCustomTokenViewModel.kt | 116 +++++++++++------- .../impl/data/DefaultTokensListRepository.kt | 39 +++--- .../impl/data/TangemApiTokensPagingSource.kt | 19 +-- .../impl/di/TokensListInteractorModule.kt | 6 +- .../impl/di/TokensListRepositoryModule.kt | 6 +- 5 files changed, 109 insertions(+), 77 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/viewmodels/AddCustomTokenViewModel.kt b/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/viewmodels/AddCustomTokenViewModel.kt index dffe1709dd..660be89b17 100644 --- a/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/viewmodels/AddCustomTokenViewModel.kt +++ b/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/viewmodels/AddCustomTokenViewModel.kt @@ -22,6 +22,7 @@ import com.tangem.domain.common.extensions.* import com.tangem.domain.common.util.cardTypesResolver import com.tangem.domain.common.util.derivationStyleProvider import com.tangem.domain.features.addCustomToken.CustomCurrency +import com.tangem.domain.wallets.usecase.GetSelectedWalletUseCase import com.tangem.tap.domain.model.WalletDataModel import com.tangem.tap.features.customtoken.impl.domain.CustomTokenInteractor import com.tangem.tap.features.customtoken.impl.domain.models.FoundToken @@ -35,7 +36,6 @@ import com.tangem.tap.features.customtoken.impl.presentation.validators.ContactA import com.tangem.tap.features.customtoken.impl.presentation.validators.ContractAddressValidatorResult import com.tangem.tap.features.details.ui.cardsettings.TextReference import com.tangem.tap.features.wallet.models.Currency -import com.tangem.tap.proxy.AppStateHolder import com.tangem.tap.store import com.tangem.utils.coroutines.AppCoroutineDispatcherProvider import com.tangem.utils.coroutines.runCatching @@ -51,11 +51,11 @@ import javax.inject.Inject /** * ViewModel for add custom token screen * - * @param analyticsEventHandler analytics event handler - * @param featureRouter feature router - * @property featureInteractor feature interactor - * @property dispatchers coroutine dispatchers provider - * @property reduxStateHolder redux state holder + * @param analyticsEventHandler analytics event handler + * @param featureRouter feature router + * @property featureInteractor feature interactor + * @property getSelectedWalletUseCase use case that returns selected wallet + * @property dispatchers coroutine dispatchers provider * [REDACTED_AUTHOR] */ @@ -66,7 +66,7 @@ internal class AddCustomTokenViewModel @Inject constructor( featureRouter: CustomTokenRouter, private val featureInteractor: CustomTokenInteractor, private val dispatchers: AppCoroutineDispatcherProvider, - private val reduxStateHolder: AppStateHolder, + private val getSelectedWalletUseCase: GetSelectedWalletUseCase, ) : ViewModel(), DefaultLifecycleObserver { private val analyticsSender = AddCustomTokenAnalyticsSender(analyticsEventHandler) @@ -208,7 +208,10 @@ internal class AddCustomTokenViewModel @Inject constructor( private fun getNetworkSelectorItems(): List { val defaultNetwork = createNetworkSelectorItem(blockchain = Blockchain.Unknown) - val scanResponse = reduxStateHolder.scanResponse + val scanResponse = getSelectedWalletUseCase().fold( + ifLeft = { null }, + ifRight = { it.scanResponse }, + ) val derivationStyle = scanResponse?.derivationStyleProvider?.getDerivationStyle() return listOf(defaultNetwork) + Blockchain.values() .filter { blockchain -> @@ -262,17 +265,20 @@ internal class AddCustomTokenViewModel @Inject constructor( } private fun createDerivationPathsSelectorField(): AddCustomTokenSelectorField.DerivationPath? { - val scanResponse = reduxStateHolder.scanResponse - if (scanResponse?.card?.settings?.isHDWalletAllowed == false) return null + return getSelectedWalletUseCase().fold( + ifLeft = { null }, + ifRight = { + if (!it.scanResponse.card.settings.isHDWalletAllowed) return null - val selectorItems = - getDerivationPathsSelectorItems(scanResponse?.derivationStyleProvider) - return AddCustomTokenSelectorField.DerivationPath( - label = TextReference.Res(R.string.custom_token_derivation_path_input_title), - selectedItem = requireNotNull(selectorItems.firstOrNull()), - items = selectorItems, - onMenuItemClick = actionsHandler::onDerivationPathSelectorItemClick, - isEnabled = true, + val selectorItems = getDerivationPathsSelectorItems(it.scanResponse.derivationStyleProvider) + AddCustomTokenSelectorField.DerivationPath( + label = TextReference.Res(R.string.custom_token_derivation_path_input_title), + selectedItem = requireNotNull(selectorItems.firstOrNull()), + items = selectorItems, + onMenuItemClick = actionsHandler::onDerivationPathSelectorItemClick, + isEnabled = true, + ) + }, ) } @@ -315,14 +321,19 @@ internal class AddCustomTokenViewModel @Inject constructor( } private fun createDerivationPathInputField(): AddCustomTokenInputField.DerivationPath? { - if (reduxStateHolder.scanResponse?.card?.settings?.isHDWalletAllowed == false) return null + return getSelectedWalletUseCase().fold( + ifLeft = { null }, + ifRight = { + if (!it.scanResponse.card.settings.isHDWalletAllowed) return null - return AddCustomTokenInputField.DerivationPath( - value = "", - onValueChange = actionsHandler::onDerivationPathValueChange, - keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done), - label = TextReference.Res(R.string.custom_token_custom_derivation), - placeholder = TextReference.Str(value = DERIVATION_PATH_PLACEHOLDER), + AddCustomTokenInputField.DerivationPath( + value = "", + onValueChange = actionsHandler::onDerivationPathValueChange, + keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done), + label = TextReference.Res(R.string.custom_token_custom_derivation), + placeholder = TextReference.Str(value = DERIVATION_PATH_PLACEHOLDER), + ) + }, ) } } @@ -440,11 +451,15 @@ internal class AddCustomTokenViewModel @Inject constructor( val isSupportedToken = if (!isNetworkSelected()) { true } else { - val scanResponse = reduxStateHolder.scanResponse - scanResponse?.card?.canHandleToken( - blockchain = networkSelectorValue, - cardTypesResolver = scanResponse.cardTypesResolver, - ) ?: false + getSelectedWalletUseCase().fold( + ifLeft = { false }, + ifRight = { + it.scanResponse.card.canHandleToken( + blockchain = networkSelectorValue, + cardTypesResolver = it.scanResponse.cardTypesResolver, + ) + }, + ) } return buildSet { @@ -478,13 +493,16 @@ internal class AddCustomTokenViewModel @Inject constructor( address = uiState.form.contractAddressInputField.value, blockchain = networkSelectorValue, ) - val scanResponse = reduxStateHolder.scanResponse - val isSupportedToken = scanResponse?.card - ?.canHandleToken( - blockchain = networkSelectorValue, - cardTypesResolver = scanResponse.cardTypesResolver, - ) - ?: false + + val isSupportedToken = getSelectedWalletUseCase().fold( + ifLeft = { false }, + ifRight = { + it.scanResponse.card.canHandleToken( + blockchain = networkSelectorValue, + cardTypesResolver = it.scanResponse.cardTypesResolver, + ) + }, + ) uiState.copySealed( floatingButton = uiState.floatingButton.copy( @@ -641,8 +659,12 @@ internal class AddCustomTokenViewModel @Inject constructor( private fun getDerivationPathForBlockchain(blockchain: Blockchain?): DerivationPath? { if (blockchain == null) return null - val derivationStyle = reduxStateHolder.scanResponse?.derivationStyleProvider?.getDerivationStyle() - ?: DerivationStyle.V1 + val derivationStyle = getSelectedWalletUseCase().fold( + ifLeft = { null }, + ifRight = { + it.scanResponse.derivationStyleProvider.getDerivationStyle() + }, + ) val derivationNetwork = if (blockchain == Blockchain.Unknown) { uiState.form.networkSelectorField.selectedItem.blockchain @@ -653,12 +675,15 @@ internal class AddCustomTokenViewModel @Inject constructor( } private fun isUnsupportedBlockchain(blockchain: Blockchain): Boolean { - val scanResponse = reduxStateHolder.scanResponse - val canHandleToken = scanResponse?.card?.canHandleBlockchain( - blockchain = blockchain, - cardTypesResolver = scanResponse.cardTypesResolver, - ) ?: false - return !canHandleToken + return getSelectedWalletUseCase().fold( + ifLeft = { false }, + ifRight = { + !it.scanResponse.card.canHandleBlockchain( + blockchain = blockchain, + cardTypesResolver = it.scanResponse.cardTypesResolver, + ) + }, + ) } private inner class ActionsHandler(private val featureRouter: CustomTokenRouter) { @@ -827,7 +852,6 @@ internal class AddCustomTokenViewModel @Inject constructor( } fun onResetButtonClick() { - val scanResponse = reduxStateHolder.scanResponse with(uiState.form) { uiState = uiState.copySealed( form = uiState.form.copy( diff --git a/app/src/main/java/com/tangem/tap/features/tokens/impl/data/DefaultTokensListRepository.kt b/app/src/main/java/com/tangem/tap/features/tokens/impl/data/DefaultTokensListRepository.kt index 7ff7b78228..7f6ecf5aec 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/impl/data/DefaultTokensListRepository.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/impl/data/DefaultTokensListRepository.kt @@ -6,26 +6,26 @@ import androidx.paging.PagingData import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.local.testnet.TestnetTokensStorage import com.tangem.domain.common.TapWorkarounds.isTestCard +import com.tangem.domain.wallets.usecase.GetSelectedWalletUseCase import com.tangem.tap.features.tokens.impl.domain.TokensListRepository import com.tangem.tap.features.tokens.impl.domain.models.Token -import com.tangem.tap.proxy.AppStateHolder import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.flow.Flow /** * Default repository implementation of tokens list feature * - * @property tangemTechApi Tangem Tech API - * @property dispatchers coroutine dispatchers provider - * @property reduxStateHolder redux state holder - * @property testnetTokensStorage storage for getting testnet tokens data + * @property tangemTechApi Tangem Tech API + * @property dispatchers coroutine dispatchers provider + * @property getSelectedWalletUseCase use case that returns selected wallet + * @property testnetTokensStorage storage for getting testnet tokens data * [REDACTED_AUTHOR] */ internal class DefaultTokensListRepository( private val tangemTechApi: TangemTechApi, private val dispatchers: CoroutineDispatcherProvider, - private val reduxStateHolder: AppStateHolder, + private val getSelectedWalletUseCase: GetSelectedWalletUseCase, private val testnetTokensStorage: TestnetTokensStorage, ) : TokensListRepository { @@ -37,16 +37,23 @@ internal class DefaultTokensListRepository( enablePlaceholders = false, ), pagingSourceFactory = { - if (reduxStateHolder.scanResponse?.card?.isTestCard == true) { - TestnetTokensPagingSource(testnetTokensStorage, searchText) - } else { - TangemApiTokensPagingSource( - api = tangemTechApi, - dispatchers = dispatchers, - reduxStateHolder = reduxStateHolder, - searchText = searchText, - ) - } + val defaultSource = TangemApiTokensPagingSource( + api = tangemTechApi, + dispatchers = dispatchers, + getSelectedWalletUseCase = getSelectedWalletUseCase, + searchText = searchText, + ) + + getSelectedWalletUseCase().fold( + ifLeft = { defaultSource }, + ifRight = { + if (it.scanResponse.card.isTestCard) { + TestnetTokensPagingSource(testnetTokensStorage, searchText) + } else { + defaultSource + } + }, + ) }, ).flow } diff --git a/app/src/main/java/com/tangem/tap/features/tokens/impl/data/TangemApiTokensPagingSource.kt b/app/src/main/java/com/tangem/tap/features/tokens/impl/data/TangemApiTokensPagingSource.kt index fb04538ee2..e36aaa66d5 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/impl/data/TangemApiTokensPagingSource.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/impl/data/TangemApiTokensPagingSource.kt @@ -7,24 +7,24 @@ import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.domain.common.extensions.supportedBlockchains import com.tangem.domain.common.extensions.toNetworkId import com.tangem.domain.common.util.cardTypesResolver +import com.tangem.domain.wallets.usecase.GetSelectedWalletUseCase import com.tangem.tap.features.tokens.impl.data.converters.CoinsResponseConverter import com.tangem.tap.features.tokens.impl.domain.models.Token -import com.tangem.tap.proxy.AppStateHolder import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.runCatching /** * Paging source that get tokens by Tangem Tech API * - * @property api Tangem Tech API - * @property dispatchers coroutine dispatchers provider - * @property reduxStateHolder redux state holder - * @property searchText search text + * @property api Tangem Tech API + * @property dispatchers coroutine dispatchers provider + * @property getSelectedWalletUseCase use case that returns selected wallet + * @property searchText search text */ internal class TangemApiTokensPagingSource( private val api: TangemTechApi, private val dispatchers: CoroutineDispatcherProvider, - private val reduxStateHolder: AppStateHolder, + private val getSelectedWalletUseCase: GetSelectedWalletUseCase, private val searchText: String?, ) : PagingSource() { @@ -39,9 +39,10 @@ internal class TangemApiTokensPagingSource( val page = params.key ?: 0 return runCatching(dispatchers.io) { - val scanResponse = reduxStateHolder.scanResponse - val supportedBlockchains = scanResponse?.card?.supportedBlockchains(scanResponse.cardTypesResolver) - ?: Blockchain.values().toList() + val supportedBlockchains = getSelectedWalletUseCase().fold( + ifLeft = { Blockchain.values().toList() }, + ifRight = { it.scanResponse.card.supportedBlockchains(it.scanResponse.cardTypesResolver) }, + ) api.getCoins( networkIds = supportedBlockchains.joinToString(separator = ",", transform = Blockchain::toNetworkId), diff --git a/app/src/main/java/com/tangem/tap/features/tokens/impl/di/TokensListInteractorModule.kt b/app/src/main/java/com/tangem/tap/features/tokens/impl/di/TokensListInteractorModule.kt index 85fd22ab71..f588d3fc76 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/impl/di/TokensListInteractorModule.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/impl/di/TokensListInteractorModule.kt @@ -2,10 +2,10 @@ package com.tangem.tap.features.tokens.impl.di import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.local.testnet.TestnetTokensStorage +import com.tangem.domain.wallets.usecase.GetSelectedWalletUseCase import com.tangem.tap.features.tokens.impl.data.DefaultTokensListRepository import com.tangem.tap.features.tokens.impl.domain.DefaultTokensListInteractor import com.tangem.tap.features.tokens.impl.domain.TokensListInteractor -import com.tangem.tap.proxy.AppStateHolder import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.Module import dagger.Provides @@ -25,14 +25,14 @@ internal object TokensListInteractorModule { fun provideTokensListInteractor( tangemTechApi: TangemTechApi, dispatchers: CoroutineDispatcherProvider, - reduxStateHolder: AppStateHolder, + getSelectedWalletUseCase: GetSelectedWalletUseCase, testnetTokensStorage: TestnetTokensStorage, ): TokensListInteractor { return DefaultTokensListInteractor( repository = DefaultTokensListRepository( tangemTechApi = tangemTechApi, dispatchers = dispatchers, - reduxStateHolder = reduxStateHolder, + getSelectedWalletUseCase = getSelectedWalletUseCase, testnetTokensStorage = testnetTokensStorage, ), ) diff --git a/app/src/main/java/com/tangem/tap/features/tokens/impl/di/TokensListRepositoryModule.kt b/app/src/main/java/com/tangem/tap/features/tokens/impl/di/TokensListRepositoryModule.kt index dbba8fe0dd..88443e8601 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/impl/di/TokensListRepositoryModule.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/impl/di/TokensListRepositoryModule.kt @@ -2,9 +2,9 @@ package com.tangem.tap.features.tokens.impl.di import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.local.testnet.TestnetTokensStorage +import com.tangem.domain.wallets.usecase.GetSelectedWalletUseCase import com.tangem.tap.features.tokens.impl.data.DefaultTokensListRepository import com.tangem.tap.features.tokens.impl.domain.TokensListRepository -import com.tangem.tap.proxy.AppStateHolder import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.Module import dagger.Provides @@ -24,13 +24,13 @@ internal object TokensListRepositoryModule { fun providesTokensListRepository( tangemTechApi: TangemTechApi, dispatchers: CoroutineDispatcherProvider, - reduxStateHolder: AppStateHolder, + getSelectedWalletUseCase: GetSelectedWalletUseCase, testnetTokensStorage: TestnetTokensStorage, ): TokensListRepository { return DefaultTokensListRepository( tangemTechApi = tangemTechApi, dispatchers = dispatchers, - reduxStateHolder = reduxStateHolder, + getSelectedWalletUseCase = getSelectedWalletUseCase, testnetTokensStorage = testnetTokensStorage, ) } From f74f4e708338c524122b88639de75b1470129f30 Mon Sep 17 00:00:00 2001 From: Tangem Date: Sat, 7 Oct 2023 04:58:06 +0300 Subject: [PATCH 146/242] Updated on 2026-08-14 --- .../main/java/com/tangem/tap/MainActivity.kt | 16 ------ .../java/com/tangem/tap/TapApplication.kt | 14 ++++++ .../di/UserWalletsListManagerProvider.kt | 16 +++--- .../repository/DelegatedKeystoreManager.kt | 18 +++++++ .../UserWalletsKeysStoreDecorator.kt | 9 ++-- .../features/details/redux/DetailsAction.kt | 3 +- .../details/redux/DetailsMiddleware.kt | 50 ++++++++----------- .../ui/appsettings/AppSettingsFragment.kt | 16 ++---- .../ui/appsettings/AppSettingsViewModel.kt | 16 +----- .../saveWallet/redux/SaveWalletMiddleware.kt | 5 +- .../usecase/GetTxHistoryItemsCountUseCase.kt | 1 + .../usecase/GetTxHistoryItemsUseCase.kt | 1 + 12 files changed, 76 insertions(+), 89 deletions(-) create mode 100644 app/src/main/java/com/tangem/tap/domain/userWalletList/repository/DelegatedKeystoreManager.kt diff --git a/app/src/main/java/com/tangem/tap/MainActivity.kt b/app/src/main/java/com/tangem/tap/MainActivity.kt index 9f1970f662..2b66b30ca7 100644 --- a/app/src/main/java/com/tangem/tap/MainActivity.kt +++ b/app/src/main/java/com/tangem/tap/MainActivity.kt @@ -35,13 +35,10 @@ import com.tangem.tap.common.OnActivityResultCallback import com.tangem.tap.common.SnackbarHandler import com.tangem.tap.common.apptheme.MutableAppThemeModeHolder import com.tangem.tap.common.redux.NotificationsHandler -import com.tangem.tap.common.redux.global.GlobalAction import com.tangem.tap.common.shop.googlepay.GooglePayService import com.tangem.tap.common.shop.googlepay.GooglePayService.Companion.LOAD_PAYMENT_DATA_REQUEST_CODE import com.tangem.tap.common.shop.googlepay.GooglePayUtil.createPaymentsClient import com.tangem.tap.domain.TangemSdkManager -import com.tangem.tap.domain.userWalletList.di.provideBiometricImplementation -import com.tangem.tap.domain.userWalletList.di.provideRuntimeImplementation import com.tangem.tap.domain.walletconnect2.domain.WalletConnectInteractor import com.tangem.tap.features.intentHandler.IntentProcessor import com.tangem.tap.features.intentHandler.handlers.BackgroundScanIntentHandler @@ -149,7 +146,6 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac backupService = BackupService.init(cardSdkConfigRepository.sdk, this) lockUserWalletsTimer = LockUserWalletsTimer(owner = this) - initUserWalletsListManager() initIntentHandlers() store.dispatch( @@ -251,18 +247,6 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac intentProcessor.addHandler(SellCurrencyIntentHandler()) } - private fun initUserWalletsListManager() { - val manager = if (preferencesStorage.shouldSaveUserWallets) { - UserWalletsListManager.provideBiometricImplementation( - context = applicationContext, - tangemSdkManager = tangemSdkManager, - ) - } else { - UserWalletsListManager.provideRuntimeImplementation() - } - store.dispatch(GlobalAction.UpdateUserWalletsListManager(manager)) - } - private fun updateAppTheme(appThemeMode: AppThemeMode) { MutableAppThemeModeHolder.value = appThemeMode MutableAppThemeModeHolder.isDarkThemeActive = isDarkTheme() diff --git a/app/src/main/java/com/tangem/tap/TapApplication.kt b/app/src/main/java/com/tangem/tap/TapApplication.kt index 6992e82d7e..75211c56c5 100644 --- a/app/src/main/java/com/tangem/tap/TapApplication.kt +++ b/app/src/main/java/com/tangem/tap/TapApplication.kt @@ -32,6 +32,7 @@ import com.tangem.domain.common.LogConfig import com.tangem.domain.settings.repositories.AppRatingRepository import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.walletmanager.WalletManagersFacade +import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.wallets.legacy.WalletManagersRepository import com.tangem.features.tokendetails.featuretoggles.TokenDetailsFeatureToggles import com.tangem.features.wallet.featuretoggles.WalletFeatureToggles @@ -58,6 +59,8 @@ import com.tangem.tap.domain.tokens.UserTokensRepository import com.tangem.tap.domain.tokens.UserTokensStorageService import com.tangem.tap.domain.totalBalance.TotalFiatBalanceCalculator import com.tangem.tap.domain.totalBalance.di.provideDefaultImplementation +import com.tangem.tap.domain.userWalletList.di.provideBiometricImplementation +import com.tangem.tap.domain.userWalletList.di.provideRuntimeImplementation import com.tangem.tap.domain.walletCurrencies.WalletCurrenciesManager import com.tangem.tap.domain.walletCurrencies.di.provideDefaultImplementation import com.tangem.tap.domain.walletStores.WalletStoresManager @@ -254,6 +257,7 @@ internal class TapApplication : Application(), ImageLoaderFactory { val configLoader = FeaturesLocalLoader(assetReader, MoshiConverter.sdkMoshi, BuildConfig.ENVIRONMENT) initConfigManager(configLoader, ::initWithConfigDependency) initWarningMessagesManager() + initUserWalletsListManager() loadNativeLibraries() @@ -391,4 +395,14 @@ internal class TapApplication : Application(), ImageLoaderFactory { private fun initWarningMessagesManager() { store.dispatch(GlobalAction.SetWarningManager(WarningMessagesManager())) } + + private fun initUserWalletsListManager() { + val manager = if (preferencesStorage.shouldSaveUserWallets) { + UserWalletsListManager.provideBiometricImplementation(applicationContext) + } else { + UserWalletsListManager.provideRuntimeImplementation() + } + + store.dispatch(GlobalAction.UpdateUserWalletsListManager(manager)) + } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/userWalletList/di/UserWalletsListManagerProvider.kt b/app/src/main/java/com/tangem/tap/domain/userWalletList/di/UserWalletsListManagerProvider.kt index 2657240db8..719491a036 100644 --- a/app/src/main/java/com/tangem/tap/domain/userWalletList/di/UserWalletsListManagerProvider.kt +++ b/app/src/main/java/com/tangem/tap/domain/userWalletList/di/UserWalletsListManagerProvider.kt @@ -3,27 +3,28 @@ package com.tangem.tap.domain.userWalletList.di import android.content.Context import com.squareup.moshi.Moshi import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory +import com.tangem.common.Provider import com.tangem.common.authentication.AuthenticatedStorage import com.tangem.common.json.TangemSdkAdapter import com.tangem.common.services.secure.SecureStorage import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.sdk.storage.AndroidSecureStorage import com.tangem.sdk.storage.createEncryptedSharedPreferences -import com.tangem.tap.domain.TangemSdkManager import com.tangem.tap.domain.userWalletList.implementation.BiometricUserWalletsListManager import com.tangem.tap.domain.userWalletList.implementation.RuntimeUserWalletsListManager +import com.tangem.tap.domain.userWalletList.repository.DelegatedKeystoreManager import com.tangem.tap.domain.userWalletList.repository.UserWalletsKeysStoreDecorator import com.tangem.tap.domain.userWalletList.repository.implementation.BiometricUserWalletsKeysRepository import com.tangem.tap.domain.userWalletList.repository.implementation.DefaultSelectedUserWalletRepository import com.tangem.tap.domain.userWalletList.repository.implementation.DefaultUserWalletsPublicInformationRepository import com.tangem.tap.domain.userWalletList.repository.implementation.DefaultUserWalletsSensitiveInformationRepository import com.tangem.tap.domain.userWalletList.utils.json.* +import com.tangem.tap.tangemSdkManager private const val USER_WALLETS_STORAGE_NAME = "user_wallets_storage" fun UserWalletsListManager.Companion.provideBiometricImplementation( - context: Context, - tangemSdkManager: TangemSdkManager, + applicationContext: Context, ): UserWalletsListManager { val moshi = Moshi.Builder() .add(WalletDerivedKeysMapAdapter()) @@ -40,7 +41,7 @@ fun UserWalletsListManager.Companion.provideBiometricImplementation( val secureStorage = AndroidSecureStorage( preferences = SecureStorage.createEncryptedSharedPreferences( - context = context, + context = applicationContext, storageName = USER_WALLETS_STORAGE_NAME, ), ) @@ -48,16 +49,17 @@ fun UserWalletsListManager.Companion.provideBiometricImplementation( val authenticatedStorage = AuthenticatedStorage( secureStorage = UserWalletsKeysStoreDecorator( featureStorage = secureStorage, - cardSdkStorage = tangemSdkManager.secureStorage, + cardSdkStorageProvider = Provider { tangemSdkManager.secureStorage }, + ), + keystoreManager = DelegatedKeystoreManager( + keystoreManagerProvider = Provider { tangemSdkManager.keystoreManager }, ), - keystoreManager = tangemSdkManager.keystoreManager, ) val keysRepository = BiometricUserWalletsKeysRepository( moshi = moshi, secureStorage = secureStorage, authenticatedStorage = authenticatedStorage, - ) val publicInformationRepository = DefaultUserWalletsPublicInformationRepository( moshi = moshi, diff --git a/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/DelegatedKeystoreManager.kt b/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/DelegatedKeystoreManager.kt new file mode 100644 index 0000000000..64e0f9a1ce --- /dev/null +++ b/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/DelegatedKeystoreManager.kt @@ -0,0 +1,18 @@ +package com.tangem.tap.domain.userWalletList.repository + +import com.tangem.common.Provider +import com.tangem.common.authentication.KeystoreManager +import javax.crypto.SecretKey + +internal class DelegatedKeystoreManager( + private val keystoreManagerProvider: Provider, +) : KeystoreManager { + + override suspend fun authenticateAndGetKey(keyAlias: String): SecretKey? { + return keystoreManagerProvider().authenticateAndGetKey(keyAlias) + } + + override suspend fun storeKey(keyAlias: String, key: SecretKey) { + keystoreManagerProvider().storeKey(keyAlias, key) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/UserWalletsKeysStoreDecorator.kt b/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/UserWalletsKeysStoreDecorator.kt index 662da27e35..49e87a395a 100644 --- a/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/UserWalletsKeysStoreDecorator.kt +++ b/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/UserWalletsKeysStoreDecorator.kt @@ -1,28 +1,29 @@ package com.tangem.tap.domain.userWalletList.repository +import com.tangem.common.Provider import com.tangem.common.services.secure.SecureStorage /** * A decorator for [SecureStorage] that facilitates data migration between two storages. * * @property featureStorage The primary storage, which will eventually contain all user data. - * @property cardSdkStorage The SDK's storage where user data might have been previously stored. + * @property cardSdkStorageProvider The SDK's storage where user data might have been previously stored. */ internal class UserWalletsKeysStoreDecorator( private val featureStorage: SecureStorage, - private val cardSdkStorage: SecureStorage, + private val cardSdkStorageProvider: Provider, ) : SecureStorage by featureStorage { override fun delete(account: String) { featureStorage.delete(account) - cardSdkStorage.delete(account) + cardSdkStorageProvider().delete(account) } override fun get(account: String): ByteArray? { var data = featureStorage.get(account) if (data == null) { - data = cardSdkStorage.get(account) ?: return null + data = cardSdkStorageProvider().get(account) ?: return null featureStorage.store(data, account) } diff --git a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsAction.kt b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsAction.kt index 9dd0c59cef..0190c0aa97 100644 --- a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsAction.kt +++ b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsAction.kt @@ -74,8 +74,7 @@ sealed class DetailsAction : Action { } data class CheckBiometricsStatus( - val awaitStatusChange: Boolean, - val lifecycleCoroutineScope: LifecycleCoroutineScope, + val lifecycleScope: LifecycleCoroutineScope, ) : AppSettings() object EnrollBiometrics : AppSettings() diff --git a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsMiddleware.kt b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsMiddleware.kt index 1c13b7ed80..bdf4867874 100644 --- a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsMiddleware.kt @@ -40,7 +40,8 @@ import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsAction import com.tangem.tap.features.wallet.redux.WalletAction import com.tangem.tap.features.walletSelector.redux.WalletSelectorAction import com.tangem.tap.proxy.redux.DaggerGraphState -import com.tangem.tap.tangemSdkManager +import com.tangem.utils.coroutines.JobHolder +import com.tangem.utils.coroutines.saveIn import com.tangem.wallet.R import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay @@ -209,6 +210,9 @@ class DetailsMiddleware { } class AppSettingsMiddleware { + + private val checkBiometricsStatusJobHolder = JobHolder() + fun handle(state: DetailsState, action: DetailsAction.AppSettings) { when (action) { is DetailsAction.AppSettings.SwitchPrivacySetting -> { @@ -218,11 +222,7 @@ class DetailsMiddleware { } } is DetailsAction.AppSettings.CheckBiometricsStatus -> { - checkBiometricsStatus( - awaitStatusChange = action.awaitStatusChange, - state = state, - lifecycleScope = action.lifecycleCoroutineScope, - ) + observeBiometricsStatusChanges(state, action.lifecycleScope) } is DetailsAction.AppSettings.EnrollBiometrics -> { enrollBiometrics() @@ -245,27 +245,20 @@ class DetailsMiddleware { } } - /** - * @param awaitStatusChange If true then start a new coroutine and check the biometric status every 100 - * milliseconds until it changes - * */ - private fun checkBiometricsStatus( - awaitStatusChange: Boolean, - state: DetailsState, - lifecycleScope: LifecycleCoroutineScope, - ) { - lifecycleScope.launch { - if (awaitStatusChange) { - while (state.appSettingsState.needEnrollBiometrics == tangemSdkManager.needEnrollBiometrics) { - delay(timeMillis = 100) + private fun observeBiometricsStatusChanges(state: DetailsState, lifecycleScope: LifecycleCoroutineScope) { + lifecycleScope.launch(Dispatchers.IO) { + do { + val needEnrollBiometrics = runCatching(tangemSdkManager::needEnrollBiometrics).getOrNull() + + if (needEnrollBiometrics != null && + needEnrollBiometrics != state.appSettingsState.needEnrollBiometrics + ) { + store.dispatchWithMain(DetailsAction.AppSettings.BiometricsStatusChanged(needEnrollBiometrics)) } - } - store.dispatchWithMain( - DetailsAction.AppSettings.BiometricsStatusChanged( - needEnrollBiometrics = tangemSdkManager.needEnrollBiometrics, - ), - ) - } + + delay(timeMillis = 500) + } while (true) + }.saveIn(checkBiometricsStatusJobHolder) } private fun enrollBiometrics() { @@ -454,10 +447,7 @@ class DetailsMiddleware { return null } - return UserWalletsListManager.provideBiometricImplementation( - context = context, - tangemSdkManager = tangemSdkManager, - ) + return UserWalletsListManager.provideBiometricImplementation(context) } } diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsFragment.kt b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsFragment.kt index 974c0937d9..122e9437d1 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsFragment.kt @@ -1,6 +1,5 @@ package com.tangem.tap.features.details.ui.appsettings -import android.os.Bundle import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.lifecycle.lifecycleScope @@ -32,11 +31,6 @@ internal class AppSettingsFragment : ComposeFragment(), StoreSubscriber @@ -58,11 +57,6 @@ internal class AppSettingsFragment : ComposeFragment(), StoreSubscriber { diff --git a/app/src/main/java/com/tangem/tap/features/saveWallet/redux/SaveWalletMiddleware.kt b/app/src/main/java/com/tangem/tap/features/saveWallet/redux/SaveWalletMiddleware.kt index 5305d35dfb..29e8ad6a02 100644 --- a/app/src/main/java/com/tangem/tap/features/saveWallet/redux/SaveWalletMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/saveWallet/redux/SaveWalletMiddleware.kt @@ -140,10 +140,7 @@ internal class SaveWalletMiddleware { store.dispatchWithMain(SaveWalletAction.Save.Error(TangemSdkError.ExceptionError(error))) return } - val manager = UserWalletsListManager.provideBiometricImplementation( - context = context, - tangemSdkManager = tangemSdkManager, - ) + val manager = UserWalletsListManager.provideBiometricImplementation(context) store.dispatchWithMain(GlobalAction.UpdateUserWalletsListManager(manager)) } diff --git a/domain/txhistory/src/main/kotlin/com/tangem/domain/txhistory/usecase/GetTxHistoryItemsCountUseCase.kt b/domain/txhistory/src/main/kotlin/com/tangem/domain/txhistory/usecase/GetTxHistoryItemsCountUseCase.kt index de458c7a51..6ca59cb013 100644 --- a/domain/txhistory/src/main/kotlin/com/tangem/domain/txhistory/usecase/GetTxHistoryItemsCountUseCase.kt +++ b/domain/txhistory/src/main/kotlin/com/tangem/domain/txhistory/usecase/GetTxHistoryItemsCountUseCase.kt @@ -9,6 +9,7 @@ import com.tangem.domain.txhistory.repository.TxHistoryRepository class GetTxHistoryItemsCountUseCase(private val repository: TxHistoryRepository) { + // FIXME: Provide UserWalletId suspend operator fun invoke(network: Network): Either { return either { catch( diff --git a/domain/txhistory/src/main/kotlin/com/tangem/domain/txhistory/usecase/GetTxHistoryItemsUseCase.kt b/domain/txhistory/src/main/kotlin/com/tangem/domain/txhistory/usecase/GetTxHistoryItemsUseCase.kt index 39865a24f5..0555f17187 100644 --- a/domain/txhistory/src/main/kotlin/com/tangem/domain/txhistory/usecase/GetTxHistoryItemsUseCase.kt +++ b/domain/txhistory/src/main/kotlin/com/tangem/domain/txhistory/usecase/GetTxHistoryItemsUseCase.kt @@ -14,6 +14,7 @@ private const val DEFAULT_PAGE_SIZE = 50 class GetTxHistoryItemsUseCase(private val repository: TxHistoryRepository) { + // FIXME: Provide UserWalletId operator fun invoke( currency: CryptoCurrency, pageSize: Int = DEFAULT_PAGE_SIZE, From a51012484fdaf3c0d95faf220592d53a2f67f880 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 9 Oct 2023 12:47:32 +0500 Subject: [PATCH 147/242] Updated on 2026-08-14 --- .../state/factory/TokenDetailsStateFactory.kt | 12 ++++++ .../TokenDetailsLoadingTxHistoryConverter.kt | 33 +++++++++------ .../TokenDetailsTxHistoryItemFlowConverter.kt | 13 +++--- .../viewmodels/TokenDetailsViewModel.kt | 41 ++++++++++++++----- 4 files changed, 69 insertions(+), 30 deletions(-) diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt index 053b87757c..e4904101e8 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt @@ -8,6 +8,7 @@ import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.components.bottomsheets.chooseaddress.ChooseAddressBottomSheetConfig import com.tangem.core.ui.components.bottomsheets.tokenreceive.AddressModel import com.tangem.core.ui.components.bottomsheets.tokenreceive.TokenReceiveBottomSheetConfig +import com.tangem.core.ui.components.transactions.state.TxHistoryState import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.tokens.error.CurrencyStatusError import com.tangem.domain.tokens.model.CryptoCurrency @@ -23,6 +24,7 @@ import com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.t import com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.txhistory.TokenDetailsLoadingTxHistoryConverter import com.tangem.feature.tokendetails.presentation.tokendetails.viewmodels.TokenDetailsClickIntents import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow internal class TokenDetailsStateFactory( private val currentStateProvider: Provider, @@ -89,6 +91,16 @@ internal class TokenDetailsStateFactory( return tokenDetailsButtonsConverter.convert(actions) } + fun getLoadingTxHistoryState(): TokenDetailsState { + return currentStateProvider().copy( + txHistoryState = TxHistoryState.Content( + contentItems = MutableStateFlow( + value = TxHistoryState.getDefaultLoadingTransactions(clickIntents::onExploreClick), + ), + ), + ) + } + fun getLoadingTxHistoryState(itemsCountEither: Either): TokenDetailsState { return loadingTransactionsStateConverter.convert(value = itemsCountEither) } diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/txhistory/TokenDetailsLoadingTxHistoryConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/txhistory/TokenDetailsLoadingTxHistoryConverter.kt index 599d55f418..591a42c218 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/txhistory/TokenDetailsLoadingTxHistoryConverter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/txhistory/TokenDetailsLoadingTxHistoryConverter.kt @@ -9,6 +9,7 @@ import com.tangem.domain.txhistory.models.TxHistoryStateError import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState import com.tangem.feature.tokendetails.presentation.tokendetails.viewmodels.TokenDetailsClickIntents import com.tangem.utils.converter.Converter +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.update internal class TokenDetailsLoadingTxHistoryConverter( @@ -34,22 +35,28 @@ internal class TokenDetailsLoadingTxHistoryConverter( private fun convert(value: Int): TokenDetailsState { val state = currentStateProvider() - val txHistoryContent = state.txHistoryState as TxHistoryState.Content - txHistoryContent.contentItems.update { - PagingData.from( - data = listOf(TxHistoryState.TxHistoryItemState.Title(onExploreClick = clickIntents::onExploreClick)) + - MutableList( - size = value, - init = { - TxHistoryState.TxHistoryItemState.Transaction( - state = TransactionState.Loading(it.toString()), - ) - }, - ), + return if (state.txHistoryState is TxHistoryState.Content) { + state.txHistoryState.contentItems.update { + PagingData.from(data = createLoadingItems(value)) + } + state + } else { + val txHistoryContent = TxHistoryState.Content( + contentItems = MutableStateFlow( + value = PagingData.from(data = createLoadingItems(value)), + ), ) + state.copy(txHistoryState = txHistoryContent) } + } - return state + private fun createLoadingItems(size: Int): List { + return buildList { + add(TxHistoryState.TxHistoryItemState.Title(onExploreClick = clickIntents::onExploreClick)) + (1..size).forEach { + add(TxHistoryState.TxHistoryItemState.Transaction(state = TransactionState.Loading(it.toString()))) + } + } } } \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/txhistory/TokenDetailsTxHistoryItemFlowConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/txhistory/TokenDetailsTxHistoryItemFlowConverter.kt index b4e618aeb3..0f700b9373 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/txhistory/TokenDetailsTxHistoryItemFlowConverter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/txhistory/TokenDetailsTxHistoryItemFlowConverter.kt @@ -15,10 +15,7 @@ import com.tangem.utils.extensions.isToday import com.tangem.utils.extensions.isYesterday import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.flow.update +import kotlinx.coroutines.flow.* import org.joda.time.DateTime import org.joda.time.DateTimeZone @@ -37,8 +34,12 @@ internal class TokenDetailsTxHistoryItemFlowConverter( } override fun convert(value: Flow>): TxHistoryState { - val txHistoryContent = currentStateProvider().txHistoryState as TxHistoryState.Content - + val state = currentStateProvider() + val txHistoryContent = if (state.txHistoryState is TxHistoryState.Content) { + state.txHistoryState + } else { + TxHistoryState.Content(contentItems = MutableStateFlow(PagingData.empty())) + } // FIXME: TxHistoryRepository should send loading transactions // [REDACTED_JIRA] value diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt index c2c671b82d..fcb13d0d4c 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt @@ -9,6 +9,7 @@ import arrow.core.getOrElse import com.tangem.blockchain.common.address.AddressType import com.tangem.common.Provider import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.core.ui.components.transactions.state.TxHistoryState import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.balancehiding.IsBalanceHiddenUseCase @@ -35,6 +36,8 @@ import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.JobHolder import com.tangem.utils.coroutines.saveIn import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch import timber.log.Timber @@ -103,7 +106,7 @@ internal class TokenDetailsViewModel @Inject constructor( private fun updateContent(selectedWallet: UserWallet) { updateMarketPrice(selectedWallet = selectedWallet) - updateTxHistory() + updateTxHistory(refresh = false, showItemsLoading = true) updateWarnings(selectedWallet = selectedWallet) } @@ -162,13 +165,19 @@ internal class TokenDetailsViewModel @Inject constructor( .saveIn(marketPriceJobHolder) } - private fun updateTxHistory(refresh: Boolean = false) { + /** + * @param refresh - invalidate cache and get data from remote + * @param showItemsLoading - show loading items placeholder. + */ + @Suppress("UnusedPrivateMember") // will be removed after implement caching + private fun updateTxHistory(refresh: Boolean, showItemsLoading: Boolean) { viewModelScope.launch(dispatchers.io) { val txHistoryItemsCountEither = txHistoryItemsCountUseCase( network = cryptoCurrency.network, ) - if (!refresh) { + // if countEither is left, handling error state run inside getLoadingTxHistoryState + if (showItemsLoading || txHistoryItemsCountEither.isLeft()) { uiState = stateFactory.getLoadingTxHistoryState(itemsCountEither = txHistoryItemsCountEither) } @@ -211,7 +220,8 @@ internal class TokenDetailsViewModel @Inject constructor( override fun onReloadClick() { analyticsEventsHandler.send(TokenScreenEvent.ButtonReload(cryptoCurrency.symbol)) - updateTxHistory() + uiState = stateFactory.getLoadingTxHistoryState() + updateTxHistory(refresh = true, showItemsLoading = true) } override fun onSendClick() { @@ -358,13 +368,22 @@ internal class TokenDetailsViewModel @Inject constructor( uiState = stateFactory.getRefreshingState() viewModelScope.launch(dispatchers.io) { - fetchCurrencyStatusUseCase.invoke( - userWalletId = wallet.walletId, - id = cryptoCurrency.id, - refresh = true, - ) - updateTxHistory(refresh = true) - updateWarnings(wallet) + listOf( + async { + fetchCurrencyStatusUseCase.invoke( + userWalletId = wallet.walletId, + id = cryptoCurrency.id, + refresh = true, + ) + }, + async { + updateTxHistory( + refresh = true, + showItemsLoading = uiState.txHistoryState !is TxHistoryState.Content, + ) + }, + async { updateWarnings(wallet) }, + ).awaitAll() uiState = stateFactory.getRefreshedState() }.saveIn(refreshStateJobHolder) } From 32ced9df85d29e6832168fd8584d8cd61c03e1b6 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 9 Oct 2023 13:28:33 +0500 Subject: [PATCH 148/242] Updated on 2026-08-14 --- .../repository/DefaultCurrenciesRepository.kt | 18 +++++----- .../utils/ResponseCryptoCurrenciesFactory.kt | 7 ++-- .../tokens/FetchCurrencyStatusUseCase.kt | 7 ++-- .../domain/tokens/GetCryptoCurrencyUseCase.kt | 8 +++-- .../tokens/GetCurrencyStatusUpdatesUseCase.kt | 8 +++-- .../tokens/GetCurrencyWarningsUseCase.kt | 7 ++-- .../tokens/GetNetworkCoinStatusUseCase.kt | 5 ++- .../CurrenciesStatusesOperations.kt | 34 ++++++++++++++----- .../tokens/repository/CurrenciesRepository.kt | 14 ++++++-- .../repository/MockCurrenciesRepository.kt | 7 +++- .../viewmodels/TokenDetailsViewModel.kt | 4 +++ .../wallet/viewmodels/WalletViewModel.kt | 1 + 12 files changed, 87 insertions(+), 33 deletions(-) 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 fa5f16dbf1..661dd32011 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 @@ -186,6 +186,7 @@ internal class DefaultCurrenciesRepository( override suspend fun getMultiCurrencyWalletCurrency( userWalletId: UserWalletId, id: CryptoCurrency.ID, + derivationPath: Network.DerivationPath, ): CryptoCurrency = withContext(dispatchers.io) { val userWallet = getUserWallet(userWalletId) ensureIsCorrectUserWallet(userWallet, isMultiCurrencyWalletExpected = true) @@ -194,10 +195,14 @@ internal class DefaultCurrenciesRepository( "Unable to find tokens response for user wallet with provided ID: $userWalletId" } - responseCurrenciesFactory.createCurrency(id, response, userWallet.scanResponse) + responseCurrenciesFactory.createCurrency(id, response, userWallet.scanResponse, derivationPath.value) } - override suspend fun getNetworkCoin(userWalletId: UserWalletId, networkId: Network.ID): CryptoCurrency.Coin { + override suspend fun getNetworkCoin( + userWalletId: UserWalletId, + networkId: Network.ID, + derivationPath: Network.DerivationPath, + ): CryptoCurrency.Coin { val userWallet = getUserWallet(userWalletId) ensureIsCorrectUserWallet(userWallet = userWallet, isMultiCurrencyWalletExpected = true) @@ -207,15 +212,12 @@ internal class DefaultCurrenciesRepository( "Unable to find tokens response for user wallet with provided ID: $userWalletId" } val blockchain = Blockchain.fromId(networkId.value) - val derivationPath = blockchain - .derivationPath(userWallet.scanResponse.derivationStyleProvider.getDerivationStyle()) - ?.rawPath + val blockchainNetworkId = blockchain.toNetworkId() + val coinId = blockchain.toCoinId() val storedCoin = storedTokens.tokens .find { - it.networkId == blockchain.toNetworkId() && - it.id == blockchain.toCoinId() && - it.derivationPath == derivationPath + it.networkId == blockchainNetworkId && it.id == coinId && it.derivationPath == derivationPath.value } ?: error("Coin in this network $networkId not found") val coin = responseCurrenciesFactory.createCurrency(storedCoin, userWallet.scanResponse) diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/ResponseCryptoCurrenciesFactory.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/ResponseCryptoCurrenciesFactory.kt index 27afcb1c84..3274fb2221 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/ResponseCryptoCurrenciesFactory.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/ResponseCryptoCurrenciesFactory.kt @@ -20,13 +20,10 @@ internal class ResponseCryptoCurrenciesFactory(private val demoConfig: DemoConfi currencyId: CryptoCurrency.ID, response: UserTokensResponse, scanResponse: ScanResponse, + derivationPath: String?, ): CryptoCurrency { val responseTokenId = currencyId.rawCurrencyId - val blockchain = Blockchain.fromId(currencyId.rawNetworkId) - val networkId = blockchain.toNetworkId() - val derivationPath = blockchain - .derivationPath(scanResponse.derivationStyleProvider.getDerivationStyle()) - ?.rawPath + val networkId = Blockchain.fromId(currencyId.rawNetworkId).toNetworkId() val token = requireNotNull( value = response.tokens diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/FetchCurrencyStatusUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/FetchCurrencyStatusUseCase.kt index eeeea7c9ec..7b5c4de6fb 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/FetchCurrencyStatusUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/FetchCurrencyStatusUseCase.kt @@ -36,16 +36,18 @@ class FetchCurrencyStatusUseCase( * * @param userWalletId The ID of the user's wallet. * @param id The ID of the cryptocurrency. + * @param derivationPath currency derivation path. * @param refresh Indicates whether to force a refresh of the status data. * @return An [Either] representing success (Right) or an error (Left) in fetching the status. */ suspend operator fun invoke( userWalletId: UserWalletId, id: CryptoCurrency.ID, + derivationPath: Network.DerivationPath, refresh: Boolean = false, ): Either { return either { - val currency = getCurrency(userWalletId, id) + val currency = getCurrency(userWalletId, id, derivationPath) fetchCurrencyStatus(userWalletId, currency, refresh) } @@ -87,8 +89,9 @@ class FetchCurrencyStatusUseCase( private suspend fun Raise.getCurrency( userWalletId: UserWalletId, id: CryptoCurrency.ID, + derivationPath: Network.DerivationPath, ): CryptoCurrency { - return catch({ currenciesRepository.getMultiCurrencyWalletCurrency(userWalletId, id) }) { + return catch({ currenciesRepository.getMultiCurrencyWalletCurrency(userWalletId, id, derivationPath) }) { raise(CurrencyStatusError.DataError(it)) } } diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyUseCase.kt index 194ffc0416..ae9b7f6326 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyUseCase.kt @@ -6,6 +6,7 @@ import arrow.core.raise.catch import arrow.core.raise.either import com.tangem.domain.tokens.error.CurrencyStatusError import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.tokens.model.Network import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.wallets.models.UserWalletId @@ -18,13 +19,15 @@ class GetCryptoCurrencyUseCase( * * @param userWalletId The ID of the user's wallet. * @param id The ID of the cryptocurrency. + * @param derivationPath currency derivation path. * @return An [Either] representing success (Right) or an error (Left) in fetching the status. */ suspend operator fun invoke( userWalletId: UserWalletId, id: CryptoCurrency.ID, + derivationPath: Network.DerivationPath, ): Either { - return either { getCurrency(userWalletId, id) } + return either { getCurrency(userWalletId, id, derivationPath) } } /** @@ -40,9 +43,10 @@ class GetCryptoCurrencyUseCase( private suspend fun Raise.getCurrency( userWalletId: UserWalletId, id: CryptoCurrency.ID, + derivationPath: Network.DerivationPath, ): CryptoCurrency { return catch( - block = { currenciesRepository.getMultiCurrencyWalletCurrency(userWalletId, id) }, + block = { currenciesRepository.getMultiCurrencyWalletCurrency(userWalletId, id, derivationPath) }, catch = { raise(CurrencyStatusError.DataError(it)) }, ) } diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCurrencyStatusUpdatesUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCurrencyStatusUpdatesUseCase.kt index d018b74e85..f575854cc6 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCurrencyStatusUpdatesUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCurrencyStatusUpdatesUseCase.kt @@ -5,6 +5,7 @@ import com.tangem.domain.tokens.error.CurrencyStatusError import com.tangem.domain.tokens.error.mapper.mapToCurrencyError import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.tokens.model.Network import com.tangem.domain.tokens.operations.CurrenciesStatusesOperations import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.tokens.repository.NetworksRepository @@ -32,20 +33,23 @@ class GetCurrencyStatusUpdatesUseCase( * * @param userWalletId The unique identifier of the user's wallet. * @param currencyId The unique identifier of the cryptocurrency. + * @param derivationPath currency derivation path. * @return A [Flow] emitting either a [CurrencyStatusError] or a [CryptoCurrencyStatus], indicating the result of the fetch operation. */ operator fun invoke( userWalletId: UserWalletId, currencyId: CryptoCurrency.ID, + derivationPath: Network.DerivationPath, ): Flow> { return flow { - emitAll(getCurrency(userWalletId, currencyId)) + emitAll(getCurrency(userWalletId, currencyId, derivationPath)) }.flowOn(dispatchers.io) } private suspend fun getCurrency( userWalletId: UserWalletId, currencyId: CryptoCurrency.ID, + derivationPath: Network.DerivationPath, ): Flow> { val operations = CurrenciesStatusesOperations( currenciesRepository = currenciesRepository, @@ -54,7 +58,7 @@ class GetCurrencyStatusUpdatesUseCase( userWalletId = userWalletId, ) - return operations.getCurrencyStatusFlow(currencyId).map { maybeCurrency -> + return operations.getCurrencyStatusFlow(currencyId, derivationPath).map { maybeCurrency -> maybeCurrency.mapLeft(CurrenciesStatusesOperations.Error::mapToCurrencyError) } } diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCurrencyWarningsUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCurrencyWarningsUseCase.kt index d11e7b8f93..994eda5490 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCurrencyWarningsUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCurrencyWarningsUseCase.kt @@ -24,12 +24,14 @@ class GetCurrencyWarningsUseCase( suspend operator fun invoke( userWalletId: UserWalletId, currency: CryptoCurrency, + derivationPath: Network.DerivationPath, ): Flow> { return combine( getFeeWarningFlow( userWalletId = userWalletId, networkId = currency.network.id, currencyId = currency.id, + derivationPath = derivationPath, ), flowOf(walletManagersFacade.getRentInfo(userWalletId, currency.network)), flowOf(walletManagersFacade.getExistentialDeposit(userWalletId, currency.network)), @@ -51,6 +53,7 @@ class GetCurrencyWarningsUseCase( userWalletId: UserWalletId, networkId: Network.ID, currencyId: CryptoCurrency.ID, + derivationPath: Network.DerivationPath, ): Flow { val operations = CurrenciesStatusesOperations( currenciesRepository = currenciesRepository, @@ -60,8 +63,8 @@ class GetCurrencyWarningsUseCase( ) return combine( - operations.getCurrencyStatusFlow(currencyId).map { it.getOrNull() }, - operations.getNetworkCoinFlow(networkId).map { it.getOrNull() }, + operations.getCurrencyStatusFlow(currencyId, derivationPath).map { it.getOrNull() }, + operations.getNetworkCoinFlow(networkId, derivationPath).map { it.getOrNull() }, ) { tokenStatus, coinStatus -> when { tokenStatus != null && coinStatus != null -> { diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetNetworkCoinStatusUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetNetworkCoinStatusUseCase.kt index 999d6e15af..44ca1e09cc 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetNetworkCoinStatusUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetNetworkCoinStatusUseCase.kt @@ -23,12 +23,14 @@ class GetNetworkCoinStatusUseCase( operator fun invoke( userWalletId: UserWalletId, networkId: Network.ID, + derivationPath: Network.DerivationPath, ): Flow> { return flow { emitAll( flow = getCurrency( userWalletId = userWalletId, networkId = networkId, + derivationPath = derivationPath, ), ) } @@ -38,6 +40,7 @@ class GetNetworkCoinStatusUseCase( private suspend fun getCurrency( userWalletId: UserWalletId, networkId: Network.ID, + derivationPath: Network.DerivationPath, ): Flow> { val operations = CurrenciesStatusesOperations( currenciesRepository = currenciesRepository, @@ -46,7 +49,7 @@ class GetNetworkCoinStatusUseCase( userWalletId = userWalletId, ) - return operations.getNetworkCoinFlow(networkId).map { maybeCurrency -> + return operations.getNetworkCoinFlow(networkId, derivationPath).map { maybeCurrency -> maybeCurrency.mapLeft(CurrenciesStatusesOperations.Error::mapToCurrencyError) } } diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesOperations.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesOperations.kt index fc4ec95f89..4271ce732e 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesOperations.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesOperations.kt @@ -67,18 +67,24 @@ internal class CurrenciesStatusesOperations( } } - suspend fun getCurrencyStatusFlow(currencyId: CryptoCurrency.ID): Flow> { + suspend fun getCurrencyStatusFlow( + currencyId: CryptoCurrency.ID, + derivationPath: Network.DerivationPath, + ): Flow> { val currency = recover( - block = { getMultiCurrencyWalletCurrency(currencyId) }, + block = { getMultiCurrencyWalletCurrency(currencyId, derivationPath) }, recover = { return flowOf(it.left()) }, ) return getCurrencyStatusFlow(currency) } - suspend fun getNetworkCoinFlow(networkId: Network.ID): Flow> { + suspend fun getNetworkCoinFlow( + networkId: Network.ID, + derivationPath: Network.DerivationPath, + ): Flow> { val currency = recover( - block = { getNetworkCoin(networkId) }, + block = { getNetworkCoin(networkId, derivationPath) }, recover = { return flowOf(it.left()) }, ) @@ -178,14 +184,26 @@ internal class CurrenciesStatusesOperations( .onEmpty { emit(Error.EmptyCurrencies.left()) } } - private suspend fun Raise.getMultiCurrencyWalletCurrency(currencyId: CryptoCurrency.ID): CryptoCurrency { - return Either.catch { currenciesRepository.getMultiCurrencyWalletCurrency(userWalletId, currencyId) } + private suspend fun Raise.getMultiCurrencyWalletCurrency( + currencyId: CryptoCurrency.ID, + derivationPath: Network.DerivationPath, + ): CryptoCurrency { + return Either.catch { + currenciesRepository.getMultiCurrencyWalletCurrency( + userWalletId, + currencyId, + derivationPath, + ) + } .mapLeft { Error.DataError(it) } .bind() } - private suspend fun Raise.getNetworkCoin(networkId: Network.ID): CryptoCurrency { - return Either.catch { currenciesRepository.getNetworkCoin(userWalletId, networkId) } + private suspend fun Raise.getNetworkCoin( + networkId: Network.ID, + derivationPath: Network.DerivationPath, + ): CryptoCurrency { + return Either.catch { currenciesRepository.getNetworkCoin(userWalletId, networkId, derivationPath) } .mapLeft { Error.DataError(it) } .bind() } diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/CurrenciesRepository.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/CurrenciesRepository.kt index 5554ee3990..2973c698f6 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/CurrenciesRepository.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/CurrenciesRepository.kt @@ -98,19 +98,29 @@ interface CurrenciesRepository { * * @param userWalletId The unique identifier of the user wallet. * @param id The unique identifier of the cryptocurrency to be retrieved. + * @param derivationPath currency derivation path. * @return The cryptocurrency associated with the user wallet and ID. * @throws com.tangem.domain.core.error.DataError.UserWalletError.WrongUserWallet If single-currency user wallet * ID provided. */ - suspend fun getMultiCurrencyWalletCurrency(userWalletId: UserWalletId, id: CryptoCurrency.ID): CryptoCurrency + suspend fun getMultiCurrencyWalletCurrency( + userWalletId: UserWalletId, + id: CryptoCurrency.ID, + derivationPath: Network.DerivationPath, + ): CryptoCurrency /** * Get the coin for a specific network. * * @param userWalletId The unique identifier of the user wallet. * @param networkId The unique identifier of the network. + * @param derivationPath currency derivation path. */ - suspend fun getNetworkCoin(userWalletId: UserWalletId, networkId: Network.ID): CryptoCurrency.Coin + suspend fun getNetworkCoin( + userWalletId: UserWalletId, + networkId: Network.ID, + derivationPath: Network.DerivationPath, + ): CryptoCurrency.Coin /** * Determines whether the tokens within a specific multi-currency user wallet are grouped. diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockCurrenciesRepository.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockCurrenciesRepository.kt index 8cdec88feb..c50ee469db 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockCurrenciesRepository.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockCurrenciesRepository.kt @@ -67,6 +67,7 @@ internal class MockCurrenciesRepository( override suspend fun getMultiCurrencyWalletCurrency( userWalletId: UserWalletId, id: CryptoCurrency.ID, + derivationPath: Network.DerivationPath, ): CryptoCurrency { val token = token.getOrElse { e -> throw e } @@ -75,7 +76,11 @@ internal class MockCurrenciesRepository( return token } - override suspend fun getNetworkCoin(userWalletId: UserWalletId, networkId: Network.ID): CryptoCurrency.Coin { + override suspend fun getNetworkCoin( + userWalletId: UserWalletId, + networkId: Network.ID, + derivationPath: Network.DerivationPath, + ): CryptoCurrency.Coin { TODO("Not yet implemented") } diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt index fcb13d0d4c..bbf6300015 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt @@ -140,6 +140,7 @@ internal class TokenDetailsViewModel @Inject constructor( getCurrencyWarningsUseCase.invoke( userWalletId = selectedWallet.walletId, currency = cryptoCurrency, + derivationPath = cryptoCurrency.network.derivationPath, ) .distinctUntilChanged() .onEach { uiState = stateFactory.getStateWithNotifications(it) } @@ -151,6 +152,7 @@ internal class TokenDetailsViewModel @Inject constructor( getCurrencyStatusUpdatesUseCase( userWalletId = selectedWallet.walletId, currencyId = cryptoCurrency.id, + derivationPath = cryptoCurrency.network.derivationPath, ) .distinctUntilChanged() .onEach { either -> @@ -247,6 +249,7 @@ internal class TokenDetailsViewModel @Inject constructor( getNetworkCoinStatusUseCase( userWalletId = wallet.walletId, networkId = status.currency.network.id, + derivationPath = status.currency.network.derivationPath, ) .take(count = 1) .collectLatest { @@ -373,6 +376,7 @@ internal class TokenDetailsViewModel @Inject constructor( fetchCurrencyStatusUseCase.invoke( userWalletId = wallet.walletId, id = cryptoCurrency.id, + derivationPath = cryptoCurrency.network.derivationPath, refresh = true, ) }, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt index 850c1aeef3..0705092df8 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt @@ -579,6 +579,7 @@ internal class WalletViewModel @Inject constructor( getNetworkCoinStatusUseCase( userWalletId = userWallet.walletId, networkId = cryptoCurrencyStatus.currency.network.id, + derivationPath = cryptoCurrencyStatus.currency.network.derivationPath, ) .take(count = 1) .collectLatest { From de38208fe9fc49f664e05b86557e08d5a897c300 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 9 Oct 2023 10:39:30 +0300 Subject: [PATCH 149/242] Updated on 2026-08-14 --- .../tangem/data/tokens/repository/DefaultCurrenciesRepository.kt | 1 + 1 file changed, 1 insertion(+) 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 661dd32011..1e2a48616d 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 @@ -107,6 +107,7 @@ internal class DefaultCurrenciesRepository( derivationStyleProvider = getUserWallet(userWalletId).scanResponse.derivationStyleProvider, ) } + .distinct() } override suspend fun removeCurrency(userWalletId: UserWalletId, currency: CryptoCurrency) = From 45229675f2fad1ee98e8b73644e128d19862d45d Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 9 Oct 2023 10:43:48 +0300 Subject: [PATCH 150/242] Updated on 2026-08-14 --- .../repository/DefaultCurrenciesRepository.kt | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) 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 1e2a48616d..aa3d99f8a1 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 @@ -280,7 +280,7 @@ internal class DefaultCurrenciesRepository( ) userTokensStore.store(userWallet.walletId, response) - fetchUserMarketCoinsByIds(userWalletId, response) + fetchExchangeableUserMarketCoinsByIds(userWalletId, response) } private suspend fun storeAndPushTokens(userWalletId: UserWalletId, response: UserTokensResponse) { @@ -288,10 +288,15 @@ internal class DefaultCurrenciesRepository( tangemTechApi.saveUserTokens(userWalletId.stringValue, response) } - private suspend fun fetchUserMarketCoinsByIds(userWalletId: UserWalletId, userTokens: UserTokensResponse) { + private suspend fun fetchExchangeableUserMarketCoinsByIds( + userWalletId: UserWalletId, + userTokens: UserTokensResponse, + ) { try { - val networkIds = userTokens.tokens.joinToString(separator = ",") { it.networkId } - val response = tangemTechApi.getCoins(networkIds = networkIds) + val networkIds = userTokens.tokens + .distinctBy { it.networkId } + .joinToString(separator = ",") { it.networkId } + val response = tangemTechApi.getCoins(networkIds = networkIds, exchangeable = true) userMarketCoinsStore.store(userWalletId, response) } catch (e: Throwable) { From 2a97712a720fb13551e33844b0603b1b0d38f76a Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 9 Oct 2023 11:49:43 +0800 Subject: [PATCH 151/242] Updated on 2026-08-14 --- .../java/com/tangem/tap/TapApplication.kt | 41 +++++++++++-------- .../common/feedback/AdditionalFeedbackInfo.kt | 31 +++++++++++++- .../DefaultWalletManagersStore.kt | 5 +++ .../walletmanager/WalletManagersStore.kt | 3 ++ .../DefaultWalletManagersFacade.kt | 7 +++- .../walletmanager/WalletManagersFacade.kt | 5 ++- 6 files changed, 72 insertions(+), 20 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/TapApplication.kt b/app/src/main/java/com/tangem/tap/TapApplication.kt index 75211c56c5..919b23292e 100644 --- a/app/src/main/java/com/tangem/tap/TapApplication.kt +++ b/app/src/main/java/com/tangem/tap/TapApplication.kt @@ -255,9 +255,18 @@ internal class TapApplication : Application(), ImageLoaderFactory { walletConnectRepository = WalletConnectRepository(this) val configLoader = FeaturesLocalLoader(assetReader, MoshiConverter.sdkMoshi, BuildConfig.ENVIRONMENT) + initUserWalletsListManager() + + // TODO: Try to performance and user experience. + // [REDACTED_JIRA] + runBlocking { + featureTogglesManager.init() + appRatingRepository.initialize() + // learn2earnInteractor.init() + } + initConfigManager(configLoader, ::initWithConfigDependency) initWarningMessagesManager() - initUserWalletsListManager() loadNativeLibraries() @@ -283,14 +292,6 @@ internal class TapApplication : Application(), ImageLoaderFactory { appStateHolder.userTokensRepository = userTokensRepository appStateHolder.walletStoresManager = walletStoresManager - // TODO: Try to performance and user experience. - // [REDACTED_JIRA] - runBlocking { - featureTogglesManager.init() - appRatingRepository.initialize() - // learn2earnInteractor.init() - } - initTopUpController() walletConnect2Repository.init(projectId = configManager.config.walletConnectProjectId) } @@ -354,14 +355,20 @@ internal class TapApplication : Application(), ImageLoaderFactory { foregroundActivityObserver: ForegroundActivityObserver, store: Store, ) { - fun initAdditionalFeedbackInfo(context: Context): AdditionalFeedbackInfo = AdditionalFeedbackInfo().apply { - appVersion = try { - // TODO don't use deprecated method - val pInfo = context.packageManager.getPackageInfo(context.packageName, 0) - pInfo.versionName - } catch (e: PackageManager.NameNotFoundException) { - e.printStackTrace() - "x.y.z" + fun initAdditionalFeedbackInfo(context: Context): AdditionalFeedbackInfo { + return AdditionalFeedbackInfo( + userWalletsListManager = userWalletsListManager, + walletManagersFacade = walletManagersFacade, + walletFeatureToggles = walletFeatureToggles, + ).apply { + appVersion = try { + // TODO don't use deprecated method + val pInfo = context.packageManager.getPackageInfo(context.packageName, 0) + pInfo.versionName + } catch (e: PackageManager.NameNotFoundException) { + e.printStackTrace() + "x.y.z" + } } } diff --git a/app/src/main/java/com/tangem/tap/common/feedback/AdditionalFeedbackInfo.kt b/app/src/main/java/com/tangem/tap/common/feedback/AdditionalFeedbackInfo.kt index 947691b26f..a3407ad4d2 100644 --- a/app/src/main/java/com/tangem/tap/common/feedback/AdditionalFeedbackInfo.kt +++ b/app/src/main/java/com/tangem/tap/common/feedback/AdditionalFeedbackInfo.kt @@ -6,9 +6,36 @@ import com.tangem.blockchain.common.address.Address import com.tangem.domain.models.scan.CardDTO import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.userwallets.UserWalletIdBuilder +import com.tangem.domain.walletmanager.WalletManagersFacade +import com.tangem.domain.wallets.legacy.UserWalletsListManager +import com.tangem.features.wallet.featuretoggles.WalletFeatureToggles import com.tangem.tap.common.extensions.stripZeroPlainString +import com.tangem.tap.scope +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach + +class AdditionalFeedbackInfo( + userWalletsListManager: UserWalletsListManager, + walletManagersFacade: WalletManagersFacade, + walletFeatureToggles: WalletFeatureToggles, +) { + + init { + if (walletFeatureToggles.isRedesignedScreenEnabled) { + userWalletsListManager.selectedUserWallet + .distinctUntilChanged() + .onEach { userWallet -> + setCardInfo(data = userWallet.scanResponse) + + walletManagersFacade.getAll(userWalletId = userWallet.walletId) + .onEach(::setWalletsInfo) + .launchIn(scope) + } + .launchIn(scope) + } + } -class AdditionalFeedbackInfo { class EmailWalletInfo( var blockchain: Blockchain = Blockchain.Unknown, var derivationPath: String = "", @@ -46,6 +73,7 @@ class AdditionalFeedbackInfo { private val Address.name: String get() = type.javaClass.simpleName + @Deprecated("Don't use it directly") fun setCardInfo(data: ScanResponse) { cardId = data.card.cardId cardBlockchain = data.walletData?.blockchain ?: "" @@ -55,6 +83,7 @@ class AdditionalFeedbackInfo { userWalletId = UserWalletIdBuilder.scanResponse(data).build()?.stringValue ?: "" } + @Deprecated("Don't use it directly") fun setWalletsInfo(walletManagers: List) { walletsInfo.clear() tokens.clear() diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/walletmanager/DefaultWalletManagersStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/walletmanager/DefaultWalletManagersStore.kt index 36bc7723c2..1222edafb4 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/walletmanager/DefaultWalletManagersStore.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/walletmanager/DefaultWalletManagersStore.kt @@ -6,6 +6,7 @@ import com.tangem.datasource.local.datastore.core.StringKeyDataStore import com.tangem.datasource.local.datastore.core.StringKeyDataStoreDecorator import com.tangem.domain.wallets.models.UserWalletId import com.tangem.utils.extensions.addOrReplace +import kotlinx.coroutines.flow.Flow internal class DefaultWalletManagersStore( dataStore: StringKeyDataStore>, @@ -15,6 +16,10 @@ internal class DefaultWalletManagersStore( return key.stringValue } + override fun getAll(userWalletId: UserWalletId): Flow> { + return get(key = userWalletId) + } + override suspend fun getSyncOrNull( userWalletId: UserWalletId, blockchain: Blockchain, diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/walletmanager/WalletManagersStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/walletmanager/WalletManagersStore.kt index c025cf6911..8506eb5803 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/walletmanager/WalletManagersStore.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/walletmanager/WalletManagersStore.kt @@ -3,9 +3,12 @@ package com.tangem.datasource.local.walletmanager import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.WalletManager import com.tangem.domain.wallets.models.UserWalletId +import kotlinx.coroutines.flow.Flow interface WalletManagersStore { + fun getAll(userWalletId: UserWalletId): Flow> + suspend fun getSyncOrNull( userWalletId: UserWalletId, blockchain: Blockchain, diff --git a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/DefaultWalletManagersFacade.kt b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/DefaultWalletManagersFacade.kt index 7d0e1e2593..9851ae5bc8 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/DefaultWalletManagersFacade.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/DefaultWalletManagersFacade.kt @@ -7,8 +7,8 @@ import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.BlockchainSdkError import com.tangem.blockchain.common.WalletManager import com.tangem.blockchain.common.address.Address -import com.tangem.blockchain.common.txhistory.TransactionHistoryRequest import com.tangem.blockchain.common.address.AddressType +import com.tangem.blockchain.common.txhistory.TransactionHistoryRequest import com.tangem.blockchain.extensions.Result import com.tangem.crypto.hdWallet.DerivationPath import com.tangem.datasource.config.ConfigManager @@ -26,6 +26,7 @@ import com.tangem.domain.walletmanager.model.UpdateWalletManagerResult import com.tangem.domain.walletmanager.utils.* import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWalletId +import kotlinx.coroutines.flow.Flow import timber.log.Timber import java.math.BigDecimal @@ -275,6 +276,10 @@ class DefaultWalletManagersFacade( return if (manager is ExistentialDepositProvider) manager.getExistentialDeposit() else null } + override fun getAll(userWalletId: UserWalletId): Flow> { + return walletManagersStore.getAll(userWalletId) + } + private fun updateWalletManagerTokensIfNeeded(walletManager: WalletManager, tokens: Set) { if (tokens.isEmpty()) return diff --git a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/WalletManagersFacade.kt b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/WalletManagersFacade.kt index 5be94faf27..94ce68d1d3 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/WalletManagersFacade.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/WalletManagersFacade.kt @@ -4,15 +4,16 @@ import com.tangem.blockchain.blockchains.solana.RentProvider import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.WalletManager import com.tangem.blockchain.common.address.Address +import com.tangem.blockchain.common.address.AddressType import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.Network import com.tangem.domain.tokens.model.warnings.CryptoCurrencyWarning -import com.tangem.blockchain.common.address.AddressType import com.tangem.domain.txhistory.models.PaginationWrapper import com.tangem.domain.txhistory.models.TxHistoryItem import com.tangem.domain.txhistory.models.TxHistoryState import com.tangem.domain.walletmanager.model.UpdateWalletManagerResult import com.tangem.domain.wallets.models.UserWalletId +import kotlinx.coroutines.flow.Flow import java.math.BigDecimal // TODO: Move to its own module @@ -97,4 +98,6 @@ interface WalletManagersFacade { * deactivated and any remaining funds will be destroyed. */ suspend fun getExistentialDeposit(userWalletId: UserWalletId, network: Network): BigDecimal? + + fun getAll(userWalletId: UserWalletId): Flow> } \ No newline at end of file From 59080b682de05b83953ebb565fc00c1e2bc6edb5 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 6 Oct 2023 15:00:57 +0800 Subject: [PATCH 152/242] Updated on 2026-08-14 --- .../common/component/TokenItem.kt | 463 ++++++++++++------ .../common/component/token/TokenTitle.kt | 1 + .../common/state/TokenItemState.kt | 2 +- 3 files changed, 313 insertions(+), 153 deletions(-) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/TokenItem.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/TokenItem.kt index 010ee08900..0d23420655 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/TokenItem.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/TokenItem.kt @@ -3,19 +3,16 @@ package com.tangem.feature.wallet.presentation.common.component import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background import androidx.compose.foundation.combinedClickable -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.defaultMinSize -import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding -import androidx.compose.runtime.* +import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.composed -import androidx.compose.ui.layout.onSizeChanged +import androidx.compose.ui.layout.* import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider -import androidx.constraintlayout.compose.* +import androidx.compose.ui.unit.Constraints import com.tangem.core.ui.components.marketprice.PriceChangeType import com.tangem.core.ui.extensions.rememberHapticFeedback import com.tangem.core.ui.res.TangemTheme @@ -24,152 +21,64 @@ import com.tangem.feature.wallet.presentation.common.component.token.* import com.tangem.feature.wallet.presentation.common.component.token.icon.TokenIcon import com.tangem.feature.wallet.presentation.common.state.TokenItemState import org.burnoutcrew.reorderable.ReorderableLazyListState +import kotlin.math.max + +private const val TITLE_MIN_WIDTH_COEFFICIENT = 0.22 +private const val PRICE_CHANGE_MIN_WIDTH_COEFFICIENT = 0.16 + +private enum class LayoutId { + ICON, TITLE, FIAT_AMOUNT, CRYPTO_AMOUNT, PRICE_CHANGE, NON_FIAT_CONTENT +} -@Suppress("LongMethod") @Composable internal fun TokenItem( state: TokenItemState, modifier: Modifier = Modifier, reorderableTokenListState: ReorderableLazyListState? = null, ) { - var rootWidth by remember { mutableStateOf(Int.MIN_VALUE) } - - @Suppress("DestructuringDeclarationWithTooManyEntries") - BaseContainer( + CustomContainer( + state = state, modifier = modifier - .tokenClickable(state) - .onSizeChanged { rootWidth = it.width }, + .tokenClickable(state = state) + .background(color = TangemTheme.colors.background.primary), ) { - val (iconRef, titleRef, cryptoAmountRef, fiatAmountRef, priceChangeRef, nonFiatContentRef) = createRefs() - val isBalanceHidden = (state as? TokenItemState.Content)?.isBalanceHidden ?: false - TokenIcon( - state = state.iconState, - modifier = Modifier.constrainAs(iconRef) { - centerVerticallyTo(parent) - start.linkTo(parent.start) - }, - ) - - val density = LocalDensity.current - val titleRequiredMinWidth by remember(rootWidth) { - derivedStateOf { with(density) { rootWidth.toDp().times(other = 0.22f) } } - } + TokenIcon(state = state.iconState, modifier = Modifier.layoutId(layoutId = LayoutId.ICON)) TokenTitle( state = state.titleState, modifier = Modifier + .layoutId(layoutId = LayoutId.TITLE) .padding(horizontal = TangemTheme.dimens.spacing8) - .constrainAs(titleRef) { - start.linkTo(iconRef.end) - top.linkTo(parent.top) - - width = Dimension.fillToConstraints.atLeast(dp = titleRequiredMinWidth) - - when (state) { - is TokenItemState.Content -> end.linkTo(fiatAmountRef.start) - is TokenItemState.Draggable -> end.linkTo(nonFiatContentRef.start) - is TokenItemState.Unreachable, - is TokenItemState.NoAddress, - -> { - end.linkTo(nonFiatContentRef.start) - bottom.linkTo(parent.bottom) - } - else -> Unit - } - }, + .padding(bottom = TangemTheme.dimens.spacing2), ) TokenFiatAmount( state = state.fiatAmountState, isBalanceHidden = isBalanceHidden, - modifier = Modifier.constrainAs(fiatAmountRef) { - top.linkTo(parent.top) - end.linkTo(parent.end) - - width = Dimension.fillToConstraints.atMostWrapContent - - if (state is TokenItemState.Content) { - start.linkTo(titleRef.end) - } - }, + modifier = Modifier + .layoutId(layoutId = LayoutId.FIAT_AMOUNT) + .padding(bottom = TangemTheme.dimens.spacing2), ) - val marginBetweenRows = TangemTheme.dimens.spacing2 TokenCryptoAmount( state = state.cryptoAmountState, isBalanceHidden = isBalanceHidden, modifier = Modifier - .padding(horizontal = TangemTheme.dimens.spacing8) - .constrainAs(cryptoAmountRef) { - start.linkTo(iconRef.end) - top.linkTo(titleRef.bottom, marginBetweenRows) - bottom.linkTo(parent.bottom) - - when (state) { - is TokenItemState.Content -> { - end.linkTo(priceChangeRef.start) - width = Dimension.fillToConstraints.atMostWrapContent - } - is TokenItemState.Draggable -> { - end.linkTo(nonFiatContentRef.start) - width = Dimension.fillToConstraints - } - else -> Unit - } - }, + .layoutId(layoutId = LayoutId.CRYPTO_AMOUNT) + .padding(horizontal = TangemTheme.dimens.spacing8), ) - val priceChangeRequiredMinWidth by remember(rootWidth) { - derivedStateOf { with(density) { rootWidth.toDp().times(other = 0.16f) } } - } TokenPriceChange( state = state.priceChangeState, - modifier = Modifier.constrainAs(priceChangeRef) { - top.linkTo(fiatAmountRef.bottom, marginBetweenRows) - end.linkTo(anchor = parent.end) - bottom.linkTo(parent.bottom) - - when (state.priceChangeState) { - is TokenItemState.PriceChangeState.Content, - is TokenItemState.PriceChangeState.Unknown, - -> { - start.linkTo(cryptoAmountRef.end) - width = Dimension.fillToConstraints - .atLeast(priceChangeRequiredMinWidth) - } - else -> Unit - } - }, + modifier = Modifier.layoutId(layoutId = LayoutId.PRICE_CHANGE), ) NonFiatContentBlock( state = state, reorderableTokenListState = reorderableTokenListState, - modifier = Modifier.constrainAs(nonFiatContentRef) { - centerVerticallyTo(parent) - end.linkTo(parent.end) - }, - ) - } -} - -@Composable -private inline fun BaseContainer( - modifier: Modifier = Modifier, - crossinline content: @Composable ConstraintLayoutScope.() -> Unit, -) { - Box( - modifier = modifier - .defaultMinSize(minHeight = TangemTheme.dimens.size68) - .background(color = TangemTheme.colors.background.primary), - ) { - ConstraintLayout( - modifier = Modifier - .fillMaxWidth() - .padding(all = TangemTheme.dimens.spacing14), - content = content, + modifier = Modifier.layoutId(layoutId = LayoutId.NON_FIAT_CONTENT), ) } } @@ -196,47 +105,299 @@ private fun Modifier.tokenClickable(state: TokenItemState): Modifier = composed } } -// region preview -@Preview +/** + * IMPORTANT! All margins that used between children setup like as children paddings. + */ +@Suppress("LongMethod") @Composable -private fun Preview_Tokens_LightTheme(@PreviewParameter(TokenConfigProvider::class) state: TokenItemState) { +private fun CustomContainer(state: TokenItemState, modifier: Modifier = Modifier, content: @Composable () -> Unit) { + val density = LocalDensity.current + val dimens = TangemTheme.dimens + + Layout(content = content, modifier = modifier) { measurables, constraints -> + + val layoutWidth = constraints.maxWidth + val layoutPadding = with(density) { dimens.size14.roundToPx() } + val layoutWidthWithPaddings = layoutWidth - 2 * layoutPadding + + val titleMinWidth = (layoutWidth * TITLE_MIN_WIDTH_COEFFICIENT).toInt() + val priceChangeMinWidth = (layoutWidth * PRICE_CHANGE_MIN_WIDTH_COEFFICIENT).toInt() + + val icon = measurables.measure(layoutId = LayoutId.ICON, constraints = constraints) + + /* + * Title width take the whole REMAINING space. + * If FiatAmount took the whole free space, then Title will has min width. + */ + val title: Placeable + + // FiatAmount width must take the whole free space but is not greater the Title min size + var fiatAmount: Placeable? = null + + // CryptoAmount width must take the whole free space but is not greater the PriceChange min size + var cryptoAmount: Placeable? = null + + /* + * PriceChange width take the whole REMAINING space. + * If CryptoAmount took the whole free space, then PriceChange will has min width. + */ + val priceChange: Placeable? + + val nonFiatContent = measurables.measure(layoutId = LayoutId.NON_FIAT_CONTENT, constraints = constraints) + + var firstRowRemainingFreeSpace: Int? = null + var secondRowRemainingFreeSpace: Int? = null + + when (state) { + is TokenItemState.Content, + is TokenItemState.Loading, + is TokenItemState.Locked, + -> { + fiatAmount = measurables.measureFiatAmount( + state = state, + maxWidth = layoutWidthWithPaddings - icon.width - titleMinWidth, + defaultConstraints = constraints, + ) + + cryptoAmount = measurables.measureCryptoAmount( + state = state, + maxWidth = layoutWidthWithPaddings - icon.width - priceChangeMinWidth, + defaultConstraints = constraints, + ) + + firstRowRemainingFreeSpace = layoutWidthWithPaddings - icon.width - fiatAmount.width + secondRowRemainingFreeSpace = layoutWidthWithPaddings - icon.width - cryptoAmount.width + } + is TokenItemState.Draggable -> { + cryptoAmount = measurables.measureCryptoAmount( + state = state, + maxWidth = layoutWidthWithPaddings - icon.width - nonFiatContent.width, + defaultConstraints = constraints, + ) + + firstRowRemainingFreeSpace = layoutWidthWithPaddings - icon.width - nonFiatContent.width + } + is TokenItemState.NoAddress, + is TokenItemState.Unreachable, + -> { + firstRowRemainingFreeSpace = layoutWidthWithPaddings - icon.width - nonFiatContent.width + } + } + + title = measurables.measureTitle( + state = state, + minWidth = titleMinWidth, + remainingFreeSpace = firstRowRemainingFreeSpace, + defaultConstraints = constraints, + ) + + priceChange = secondRowRemainingFreeSpace?.let { + measurables.measurePriceChange( + state = state, + minWidth = priceChangeMinWidth, + remainingFreeSpace = secondRowRemainingFreeSpace, + defaultConstraints = constraints, + ) + } + + val layoutHeight = calculateLayoutHeight( + state = state, + minLayoutHeight = with(density) { dimens.size68.roundToPx() }, + layoutPadding = layoutPadding, + betweenRowsPadding = with(density) { dimens.size2.roundToPx() }, + title = title, + fiatAmount = fiatAmount, + cryptoAmount = cryptoAmount, + priceChange = priceChange, + ) + + layout(width = constraints.maxWidth, height = layoutHeight) { + icon.placeRelative(x = layoutPadding, y = (layoutHeight - icon.height).div(other = 2)) + + title.placeRelative( + x = layoutPadding + icon.width, + y = when (state) { + is TokenItemState.NoAddress, + is TokenItemState.Unreachable, + -> (layoutHeight - title.height).div(other = 2) + else -> layoutPadding + }, + ) + + cryptoAmount?.placeRelative( + x = layoutPadding + icon.width, + y = layoutHeight - cryptoAmount.height - layoutPadding, + ) + + fiatAmount?.placeRelative(x = layoutWidth - fiatAmount.width - layoutPadding, y = layoutPadding) + + priceChange?.placeRelative( + x = layoutWidth - priceChange.width - layoutPadding, + y = layoutHeight - priceChange.height - layoutPadding, + ) + + nonFiatContent.placeRelative( + x = layoutWidth - nonFiatContent.width - layoutPadding, + y = (layoutHeight - nonFiatContent.height).div(other = 2), + ) + } + } +} + +private fun List.measureFiatAmount( + state: TokenItemState, + maxWidth: Int, + defaultConstraints: Constraints, +): Placeable { + return measure( + layoutId = LayoutId.FIAT_AMOUNT, + constraints = when (state) { + is TokenItemState.Content, + is TokenItemState.Draggable, + -> createConstrainsSafely(maxWidth = maxWidth) + else -> defaultConstraints + }, + ) +} + +private fun List.measureCryptoAmount( + state: TokenItemState, + maxWidth: Int, + defaultConstraints: Constraints, +): Placeable { + return measure( + layoutId = LayoutId.CRYPTO_AMOUNT, + constraints = when (state) { + is TokenItemState.Content, + is TokenItemState.Draggable, + -> createConstrainsSafely(maxWidth = maxWidth) + else -> defaultConstraints + }, + ) +} + +private fun List.measureTitle( + state: TokenItemState, + minWidth: Int, + remainingFreeSpace: Int, + defaultConstraints: Constraints, +): Placeable { + return measure( + layoutId = LayoutId.TITLE, + constraints = when (state) { + is TokenItemState.Content, + is TokenItemState.Draggable, + is TokenItemState.NoAddress, + is TokenItemState.Unreachable, + -> createDynamicConstrains(minWidth = minWidth, remainingFreeSpace = remainingFreeSpace) + else -> defaultConstraints + }, + ) +} + +private fun List.measurePriceChange( + state: TokenItemState, + minWidth: Int, + remainingFreeSpace: Int, + defaultConstraints: Constraints, +): Placeable { + return measure( + layoutId = LayoutId.PRICE_CHANGE, + constraints = when (state) { + is TokenItemState.Content, + -> createDynamicConstrains(minWidth = minWidth, remainingFreeSpace = remainingFreeSpace) + else -> defaultConstraints + }, + ) +} + +private fun List.measure(layoutId: LayoutId, constraints: Constraints): Placeable { + return requireNotNull( + value = firstOrNull { it.layoutId == layoutId }, + lazyMessage = { "Measurables[$layoutId] is null" }, + ).measure(constraints) +} + +private fun createDynamicConstrains(minWidth: Int, remainingFreeSpace: Int): Constraints { + return createConstrainsSafely( + minWidth = minWidth, + maxWidth = max(a = minWidth, b = remainingFreeSpace), + ) +} + +private fun createConstrainsSafely( + minWidth: Int = 0, + maxWidth: Int = Constraints.Infinity, + minHeight: Int = 0, + maxHeight: Int = Constraints.Infinity, +): Constraints { + return Constraints( + minWidth = minWidth.makeNotLessZero(), + maxWidth = maxWidth.makeNotLessZero(), + minHeight = minHeight.makeNotLessZero(), + maxHeight = maxHeight.makeNotLessZero(), + ) +} + +private fun Int.makeNotLessZero(): Int = max(a = 0, b = this) + +@Suppress("LongParameterList") +private fun calculateLayoutHeight( + state: TokenItemState, + minLayoutHeight: Int, + layoutPadding: Int, + betweenRowsPadding: Int, + title: Placeable, + fiatAmount: Placeable?, + cryptoAmount: Placeable?, + priceChange: Placeable?, +): Int { + val firstColumnHeight: Int + val secondColumnHeight: Int + + when (state) { + is TokenItemState.Content, + is TokenItemState.Loading, + is TokenItemState.Locked, + -> { + firstColumnHeight = 2 * layoutPadding + title.height + betweenRowsPadding + (cryptoAmount?.height ?: 0) + secondColumnHeight = 2 * layoutPadding + (fiatAmount?.height ?: 0) + betweenRowsPadding + + (priceChange?.height ?: 0) + } + is TokenItemState.Draggable, + is TokenItemState.NoAddress, + is TokenItemState.Unreachable, + -> { + firstColumnHeight = minLayoutHeight + secondColumnHeight = minLayoutHeight + } + } + + return max(firstColumnHeight, secondColumnHeight).coerceAtLeast(minLayoutHeight) +} + +@Preview(widthDp = 360) +@Composable +private fun Preview_CustomTokenItem_InLight(@PreviewParameter(TokenItemStateProvider::class) state: TokenItemState) { TangemTheme(isDark = false) { - TokenItem(state) + TokenItem(state = state) } } -@Preview -@Composable -private fun Preview_Tokens_DarkTheme(@PreviewParameter(TokenConfigProvider::class) state: TokenItemState) { - TangemTheme(isDark = true) { - TokenItem(state) - } -} - -private class TokenConfigProvider : CollectionPreviewParameterProvider( +private class TokenItemStateProvider : CollectionPreviewParameterProvider( collection = listOf( - WalletPreviewData.tokenItemVisibleState.copy( - cryptoAmountState = TokenItemState.CryptoAmountState.Content( - text = "5,41221467146712416241274127841274174213421 MATIC", - ), - ), - WalletPreviewData.tokenItemVisibleState.copy( - priceChangeState = TokenItemState.PriceChangeState.Content( - valueInPercent = "31231231231231231231223123123123212312312312.0%", - type = PriceChangeType.UP, - ), - ), - WalletPreviewData.tokenItemVisibleState.copy( - cryptoAmountState = TokenItemState.CryptoAmountState.Content( - text = "5,41221467146712416241274127841274174213421 MATIC", - ), - priceChangeState = TokenItemState.PriceChangeState.Content( - valueInPercent = "31231231231231231231223123123123212312312312.0%", - type = PriceChangeType.UP, - ), - ), WalletPreviewData.tokenItemVisibleState.copy( iconState = WalletPreviewData.coinIconState.copy(showCustomBadge = true), + titleState = TokenItemState.TitleState.Content( + text = "PolygonPolygonPolygonPolygonPolygonPolygon", + hasPending = true, + ), + fiatAmountState = TokenItemState.FiatAmountState.Content(text = "3213123123321312312312312312 $"), + cryptoAmountState = TokenItemState.CryptoAmountState.Content(text = "5,4123123213123123123123123123 MATIC"), + priceChangeState = TokenItemState.PriceChangeState.Content( + valueInPercent = "2365723643724723423742342374623642374723472342342.0%", + type = PriceChangeType.UP, + ), ), WalletPreviewData.tokenItemUnreachableState, WalletPreviewData.tokenItemNoAddressState, @@ -247,6 +408,4 @@ private class TokenConfigProvider : CollectionPreviewParameterProvider Date: Fri, 6 Oct 2023 18:16:45 +0300 Subject: [PATCH 153/242] Updated on 2026-08-14 --- .../wallet/viewmodels/WalletViewModel.kt | 44 +++++++++---------- 1 file changed, 20 insertions(+), 24 deletions(-) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt index 0705092df8..e860f3d518 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt @@ -914,8 +914,26 @@ internal class WalletViewModel @Inject constructor( private fun getSingleCurrencyContent(index: Int) { val wallet = getWallet(index) - updatePrimaryCurrencyStatus(userWalletId = wallet.walletId) - updateNotifications(index) + getPrimaryCurrencyStatusUpdatesUseCase(userWalletId = wallet.walletId) + .distinctUntilChanged() + .onEach { maybeCryptoCurrencyStatus -> + uiState = stateFactory.getSingleCurrencyLoadedBalanceState(maybeCryptoCurrencyStatus) + + maybeCryptoCurrencyStatus.onRight { status -> + singleWalletCryptoCurrencyStatus = status + + if (status.value.amount?.isZero() == false) { + setWalletWithFundsFoundUseCase() + } + + updateNotifications(index) + updateButtons(userWalletId = wallet.walletId, currencyStatus = status) + updateTxHistory(status.currency) + } + } + .flowOn(dispatchers.io) + .launchIn(viewModelScope) + .saveIn(marketPriceJobHolder) } private fun updateTxHistory(currency: CryptoCurrency) { @@ -936,28 +954,6 @@ internal class WalletViewModel @Inject constructor( } } - private fun updatePrimaryCurrencyStatus(userWalletId: UserWalletId) { - getPrimaryCurrencyStatusUpdatesUseCase(userWalletId = userWalletId) - .distinctUntilChanged() - .onEach { maybeCryptoCurrencyStatus -> - uiState = stateFactory.getSingleCurrencyLoadedBalanceState(maybeCryptoCurrencyStatus) - - maybeCryptoCurrencyStatus.onRight { status -> - singleWalletCryptoCurrencyStatus = status - - if (status.value.amount?.isZero() == false) { - setWalletWithFundsFoundUseCase() - } - - updateButtons(userWalletId = userWalletId, currencyStatus = status) - updateTxHistory(status.currency) - } - } - .flowOn(dispatchers.io) - .launchIn(viewModelScope) - .saveIn(marketPriceJobHolder) - } - private fun updateButtons(userWalletId: UserWalletId, currencyStatus: CryptoCurrencyStatus) { getCryptoCurrencyActionsUseCase(userWalletId = userWalletId, cryptoCurrencyStatus = currencyStatus) .distinctUntilChanged() From 05d2286ed7051d44bf3bd49114f68429fbac7d23 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 9 Oct 2023 16:51:32 +0300 Subject: [PATCH 154/242] Updated on 2026-08-14 --- .../presentation/common/WalletPreviewData.kt | 1 + .../common/component/TokenItem.kt | 4 ++- .../common/state/TokenItemState.kt | 1 + .../OrganizeTokensStateHolder.kt | 14 ++++++--- .../organizetokens/OrganizeTokensViewModel.kt | 22 +++++++++++++ .../model/OrganizeTokensListState.kt | 8 +++++ .../error/TokenListHiddenStateConverter.kt | 31 +++++++++++++++++++ .../CryptoCurrencyToDraggableItemConverter.kt | 12 +++++-- .../wallet/ui/components/common/WalletCard.kt | 3 +- 9 files changed, 87 insertions(+), 9 deletions(-) create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/error/TokenListHiddenStateConverter.kt diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/WalletPreviewData.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/WalletPreviewData.kt index 22e3dba93f..9456466cb9 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/WalletPreviewData.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/WalletPreviewData.kt @@ -167,6 +167,7 @@ internal object WalletPreviewData { iconState = tokenIconState, titleState = TokenItemState.TitleState.Content(text = "Polygon"), cryptoAmountState = TokenItemState.CryptoAmountState.Content(text = "3 172,14 $"), + isBalanceHidden = false, ) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/TokenItem.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/TokenItem.kt index 0d23420655..930a9d838b 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/TokenItem.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/TokenItem.kt @@ -42,7 +42,9 @@ internal fun TokenItem( .tokenClickable(state = state) .background(color = TangemTheme.colors.background.primary), ) { - val isBalanceHidden = (state as? TokenItemState.Content)?.isBalanceHidden ?: false + val isBalanceHidden = (state as? TokenItemState.Content)?.isBalanceHidden + ?: (state as? TokenItemState.Draggable)?.isBalanceHidden + ?: false TokenIcon(state = state.iconState, modifier = Modifier.layoutId(layoutId = LayoutId.ICON)) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/state/TokenItemState.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/state/TokenItemState.kt index f9226a1bbe..b5d6b52d47 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/state/TokenItemState.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/state/TokenItemState.kt @@ -72,6 +72,7 @@ internal sealed class TokenItemState { override val iconState: IconState, override val titleState: TitleState, override val cryptoAmountState: CryptoAmountState, + val isBalanceHidden: Boolean, ) : TokenItemState() { override val fiatAmountState: FiatAmountState? = null override val priceChangeState: PriceChangeState? = null diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensStateHolder.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensStateHolder.kt index b8d2dd73a2..9bb4d7874c 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensStateHolder.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensStateHolder.kt @@ -11,6 +11,7 @@ import com.tangem.feature.wallet.presentation.organizetokens.model.OrganizeToken import com.tangem.feature.wallet.presentation.organizetokens.model.OrganizeTokensState import com.tangem.feature.wallet.presentation.organizetokens.utils.converter.InProgressStateConverter import com.tangem.feature.wallet.presentation.organizetokens.utils.converter.TokenListToStateConverter +import com.tangem.feature.wallet.presentation.organizetokens.utils.converter.error.TokenListHiddenStateConverter import com.tangem.feature.wallet.presentation.organizetokens.utils.converter.error.TokenListErrorConverter import com.tangem.feature.wallet.presentation.organizetokens.utils.converter.error.TokenListSortingErrorConverter import com.tangem.feature.wallet.presentation.organizetokens.utils.converter.items.CryptoCurrencyToDraggableItemConverter @@ -24,12 +25,14 @@ internal class OrganizeTokensStateHolder( private val intents: OrganizeTokensIntents, private val dragAndDropIntents: DragAndDropIntents, private val appCurrencyProvider: Provider, + private val isBalanceHiddenProvider: Provider, + private val listStateProvider: Provider, ) { private val stateFlowInternal: MutableStateFlow = MutableStateFlow(getInitialState()) private val tokenListConverter by lazy { - val tokensConverter = CryptoCurrencyToDraggableItemConverter(appCurrencyProvider) + val tokensConverter = CryptoCurrencyToDraggableItemConverter(appCurrencyProvider, isBalanceHiddenProvider) val itemsConverter = TokenListToListStateConverter( tokensConverter = tokensConverter, groupsConverter = NetworkGroupToDraggableItemsConverter(tokensConverter), @@ -38,9 +41,8 @@ internal class OrganizeTokensStateHolder( TokenListToStateConverter(Provider(stateFlowInternal::value), itemsConverter) } - private val inProgressStateConverter by lazy { - InProgressStateConverter() - } + private val inProgressStateConverter by lazy { InProgressStateConverter() } + private val tokenListHiddenStateConverter by lazy { TokenListHiddenStateConverter(listStateProvider) } private val tokenListErrorConverter by lazy { TokenListErrorConverter(Provider(stateFlowInternal::value), inProgressStateConverter) @@ -80,6 +82,10 @@ internal class OrganizeTokensStateHolder( updateState { copy(header = header.copy(isSortedByBalance = false)) } } + fun updateHiddenState(isBalanceHidden: Boolean) { + updateState { copy(itemsState = tokenListHiddenStateConverter.convert(isBalanceHidden)) } + } + fun updateStateWithError(error: TokenListError) { updateState { tokenListErrorConverter.convert(error) } } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensViewModel.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensViewModel.kt index 5a1af707d2..d71ac77914 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensViewModel.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensViewModel.kt @@ -7,6 +7,8 @@ import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.analytics.models.AnalyticsParam import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.balancehiding.IsBalanceHiddenUseCase +import com.tangem.domain.balancehiding.ListenToFlipsUseCase import com.tangem.domain.tokens.ApplyTokenListSortingUseCase import com.tangem.domain.tokens.GetTokenListUseCase import com.tangem.domain.tokens.ToggleTokenListGroupingUseCase @@ -36,6 +38,8 @@ internal class OrganizeTokensViewModel @Inject constructor( private val toggleTokenListSortingUseCase: ToggleTokenListSortingUseCase, private val applyTokenListSortingUseCase: ApplyTokenListSortingUseCase, private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, + private val isBalanceHiddenUseCase: IsBalanceHiddenUseCase, + private val listenToFlipsUseCase: ListenToFlipsUseCase, private val analyticsEventsHandler: AnalyticsEventHandler, private val dispatchers: CoroutineDispatcherProvider, savedStateHandle: SavedStateHandle, @@ -45,6 +49,8 @@ internal class OrganizeTokensViewModel @Inject constructor( private val selectedAppCurrencyFlow = createSelectedAppCurrencyFlow() + private var isBalanceHidden = true + private val dragAndDropAdapter = DragAndDropAdapter( listStateProvider = Provider { uiState.value.itemsState }, ) @@ -53,6 +59,8 @@ internal class OrganizeTokensViewModel @Inject constructor( intents = this, dragAndDropIntents = dragAndDropAdapter, appCurrencyProvider = Provider(selectedAppCurrencyFlow::value), + isBalanceHiddenProvider = Provider { isBalanceHidden }, + listStateProvider = Provider { uiState.value.itemsState }, ) private val userWalletId: UserWalletId by lazy { @@ -68,6 +76,20 @@ internal class OrganizeTokensViewModel @Inject constructor( override fun onCreate(owner: LifecycleOwner) { analyticsEventsHandler.send(PortfolioOrganizeTokensAnalyticsEvent.ScreenOpened) + isBalanceHiddenUseCase() + .flowWithLifecycle(owner.lifecycle) + .onEach { hidden -> + isBalanceHidden = hidden + stateHolder.updateHiddenState(isBalanceHidden) + } + .launchIn(viewModelScope) + + viewModelScope.launch { + listenToFlipsUseCase() + .flowWithLifecycle(owner.lifecycle) + .collect() + } + bootstrapTokenList() bootstrapDragAndDropUpdates() } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/model/OrganizeTokensListState.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/model/OrganizeTokensListState.kt index 5d16b5035c..2bc9fe9dd9 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/model/OrganizeTokensListState.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/model/OrganizeTokensListState.kt @@ -19,4 +19,12 @@ internal sealed class OrganizeTokensListState { object Empty : OrganizeTokensListState() { override val items: PersistentList = persistentListOf() } + + fun copySealed(items: PersistentList = this.items): OrganizeTokensListState { + return when (this) { + is GroupedByNetwork -> copy(items = items) + is Ungrouped -> copy(items = items) + is Empty -> Empty + } + } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/error/TokenListHiddenStateConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/error/TokenListHiddenStateConverter.kt new file mode 100644 index 0000000000..96e5cf4538 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/error/TokenListHiddenStateConverter.kt @@ -0,0 +1,31 @@ +package com.tangem.feature.wallet.presentation.organizetokens.utils.converter.error + +import com.tangem.common.Provider +import com.tangem.feature.wallet.presentation.organizetokens.model.DraggableItem +import com.tangem.feature.wallet.presentation.organizetokens.model.OrganizeTokensListState +import com.tangem.utils.converter.Converter +import kotlinx.collections.immutable.toPersistentList + +internal class TokenListHiddenStateConverter( + private val currentStateProvider: Provider, +) : Converter { + + override fun convert(input: Boolean): OrganizeTokensListState { + val currentState = currentStateProvider() + val isBalanceHidden = input + + return currentState.copySealed( + currentState.items.map { draggableItem -> + if (draggableItem is DraggableItem.Token) { + draggableItem.copy( + tokenItemState = draggableItem.tokenItemState.copy( + isBalanceHidden = isBalanceHidden, + ), + ) + } else { + draggableItem + } + }.toPersistentList(), + ) + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/items/CryptoCurrencyToDraggableItemConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/items/CryptoCurrencyToDraggableItemConverter.kt index aa518deba2..f18e43de7a 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/items/CryptoCurrencyToDraggableItemConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/items/CryptoCurrencyToDraggableItemConverter.kt @@ -13,26 +13,29 @@ import com.tangem.utils.converter.Converter internal class CryptoCurrencyToDraggableItemConverter( private val appCurrencyProvider: Provider, + private val isBalanceHiddenProvider: Provider, ) : Converter { private val iconStateConverter = CryptoCurrencyToIconStateConverter() override fun convert(value: CryptoCurrencyStatus): DraggableItem.Token { - return createDraggableToken(value, appCurrencyProvider()) + return createDraggableToken(value, appCurrencyProvider(), isBalanceHiddenProvider()) } override fun convertList(input: Collection): List { val appCurrency = appCurrencyProvider() + val isBalanceHidden = isBalanceHiddenProvider() - return input.map { createDraggableToken(it, appCurrency) } + return input.map { createDraggableToken(it, appCurrency, isBalanceHidden) } } private fun createDraggableToken( currencyStatus: CryptoCurrencyStatus, appCurrency: AppCurrency, + isBalanceHidden: Boolean, ): DraggableItem.Token { return DraggableItem.Token( - tokenItemState = createTokenItemState(currencyStatus, appCurrency), + tokenItemState = createTokenItemState(currencyStatus, appCurrency, isBalanceHidden), groupId = getGroupHeaderId(currencyStatus.currency.network), ) } @@ -40,6 +43,7 @@ internal class CryptoCurrencyToDraggableItemConverter( private fun createTokenItemState( currencyStatus: CryptoCurrencyStatus, appCurrency: AppCurrency, + isBalanceHidden: Boolean, ): TokenItemState.Draggable { val currency = currencyStatus.currency @@ -52,6 +56,8 @@ internal class CryptoCurrencyToDraggableItemConverter( } else { TokenItemState.CryptoAmountState.Content(text = getFormattedFiatAmount(currencyStatus, appCurrency)) }, + isBalanceHidden = isBalanceHidden, + ) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletCard.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletCard.kt index ffd3f192de..9af63cffb8 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletCard.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletCard.kt @@ -33,6 +33,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider import androidx.compose.ui.unit.* import androidx.constraintlayout.compose.* +import com.tangem.common.Strings import com.tangem.core.ui.components.FontSizeRange import com.tangem.core.ui.components.RectangleShimmer import com.tangem.core.ui.components.ResizableText @@ -296,7 +297,7 @@ private fun Balance(state: WalletCardState, modifier: Modifier = Modifier) { style = TangemTheme.typography.h2, ) } - is WalletCardState.HiddenContent -> NonContentBalanceText(text = WalletCardState.HIDDEN_BALANCE_TEXT) + is WalletCardState.HiddenContent -> NonContentBalanceText(TextReference.Str(Strings.STARS)) is WalletCardState.Error -> NonContentBalanceText(text = WalletCardState.EMPTY_BALANCE_TEXT) is WalletCardState.Loading -> { RectangleShimmer(modifier = Modifier.nonContentBalanceSize(TangemTheme.dimens)) From a38bc0322d4ceaeef24a3e62500ccdb4904bda49 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 3 Oct 2023 16:36:42 +0300 Subject: [PATCH 155/242] Updated on 2026-08-14 --- .../com/tangem/tap/domain/TapWalletManager.kt | 42 ++++--- .../walletconnect/WalletConnectMiddleware.kt | 111 +++++++++++------- .../DefaultWalletManagersStore.kt | 4 + .../walletmanager/WalletManagersStore.kt | 2 + .../domain/common/extensions/Blockchain.kt | 2 - .../DefaultWalletManagersFacade.kt | 4 + .../walletmanager/WalletManagersFacade.kt | 2 + 7 files changed, 107 insertions(+), 60 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/domain/TapWalletManager.kt b/app/src/main/java/com/tangem/tap/domain/TapWalletManager.kt index c10b88280a..67f1253c9e 100644 --- a/app/src/main/java/com/tangem/tap/domain/TapWalletManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/TapWalletManager.kt @@ -92,11 +92,7 @@ class TapWalletManager( store.dispatchWalletAction(action = WalletAction.Warnings.CheckIfNeeded) } setupWalletConnectV2(userWallet) - - val walletFeatureToggles = store.state.daggerGraphState.get(DaggerGraphState::walletFeatureToggles) - if (!walletFeatureToggles.isRedesignedScreenEnabled) { - loadData(userWallet = userWallet, refresh = refresh) - } + loadData(userWallet = userWallet, refresh = refresh) } private fun Store.dispatchWalletAction(action: WalletAction) { @@ -161,21 +157,31 @@ class TapWalletManager( } } - private fun getAccountsForWc(wcInteractor: WalletConnectInteractor): List { - return store.state.walletState.walletManagers - .mapNotNull { - val wallet = it.wallet - val chainId = wcInteractor.blockchainHelper.networkIdToChainIdOrNull( - wallet.blockchain.toNetworkId(), + private suspend fun getAccountsForWc(wcInteractor: WalletConnectInteractor): List { + val walletManagerToggles = store.state.daggerGraphState + .get(DaggerGraphState::walletFeatureToggles) + val walletManagers = if (walletManagerToggles.isRedesignedScreenEnabled) { + val walletManagerFacade = store.state.daggerGraphState + .get(DaggerGraphState::walletManagersFacade) + val userWallet = userWalletsListManager.selectedUserWalletSync ?: return emptyList() + walletManagerFacade.getStoredWalletManagers(userWallet.walletId) + } else { + store.state.walletState.walletManagers + } + + return walletManagers.mapNotNull { + val wallet = it.wallet + val chainId = wcInteractor.blockchainHelper.networkIdToChainIdOrNull( + wallet.blockchain.toNetworkId(), + ) + chainId?.let { + Account( + chainId, + wallet.address, + wallet.publicKey.derivationPath?.rawPath, ) - chainId?.let { - Account( - chainId, - wallet.address, - wallet.publicKey.derivationPath?.rawPath, - ) - } } + } } fun updateConfigManager(data: ScanResponse) { diff --git a/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectMiddleware.kt b/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectMiddleware.kt index a436d55d8e..a9f177a893 100644 --- a/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectMiddleware.kt @@ -32,6 +32,7 @@ import com.tangem.tap.features.wallet.redux.WalletState import com.tangem.tap.proxy.redux.DaggerGraphState import com.tangem.tap.scope import com.tangem.tap.store +import com.tangem.tap.userWalletsListManager import kotlinx.coroutines.launch import org.rekotlin.Action import org.rekotlin.Middleware @@ -246,47 +247,50 @@ class WalletConnectMiddleware { store.dispatchOnMain(GlobalAction.ShowDialog(WalletConnectDialog.UnsupportedNetwork())) return } - val walletManager = getWalletManager( - wallet = action.session.wallet, - blockchain = blockchain, - walletState = store.state.walletState, - ).guard { - store.dispatchOnMain( - GlobalAction.ShowDialog( - WalletConnectDialog.AddNetwork(blockchain.fullName), - ), + scope.launch { + val walletManager = getWalletManager( + wallet = action.session.wallet, + blockchain = blockchain, + walletState = store.state.walletState, + ).guard { + store.dispatchOnMain( + GlobalAction.ShowDialog( + WalletConnectDialog.AddNetwork(blockchain.fullName), + ), + ) + return@launch + } + val updatedWallet = action.session.wallet.copy( + walletPublicKey = walletManager.wallet.publicKey.seedKey, + derivedPublicKey = walletManager.wallet.publicKey.derivedKey, + derivationPath = walletManager.wallet.publicKey.derivationPath, + blockchain = action.blockchain, ) - return + val updatedSession = action.session.copy(wallet = updatedWallet) + store.dispatchOnMain(WalletConnectAction.UpdateBlockchain(updatedSession)) } - val updatedWallet = action.session.wallet.copy( - walletPublicKey = walletManager.wallet.publicKey.seedKey, - derivedPublicKey = walletManager.wallet.publicKey.derivedKey, - derivationPath = walletManager.wallet.publicKey.derivationPath, - blockchain = action.blockchain, - ) - val updatedSession = action.session.copy(wallet = updatedWallet) - store.dispatchOnMain(WalletConnectAction.UpdateBlockchain(updatedSession)) } is WalletConnectAction.UpdateBlockchain -> { walletConnectManager.updateBlockchain(action.updatedSession) } is WalletConnectAction.ApproveProposal -> { - val accounts = store.state.walletState.walletManagers - .mapNotNull { - val wallet = it.wallet - val chainId = walletConnectInteractor.blockchainHelper.networkIdToChainIdOrNull( - wallet.blockchain.toNetworkId(), - ) - chainId?.let { - Account( - chainId, - wallet.address, - wallet.publicKey.derivationPath?.rawPath, + scope.launch { + val accounts = getWalletManagers() + .mapNotNull { + val wallet = it.wallet + val chainId = walletConnectInteractor.blockchainHelper.networkIdToChainIdOrNull( + wallet.blockchain.toNetworkId(), ) + chainId?.let { + Account( + chainId, + wallet.address, + wallet.publicKey.derivationPath?.rawPath, + ) + } } - } - - walletConnectInteractor.approveSessionProposal(accounts) + walletConnectInteractor.approveSessionProposal(accounts) + } } is WalletConnectAction.RejectProposal -> { walletConnectInteractor.rejectSessionProposal() @@ -345,6 +349,19 @@ class WalletConnectMiddleware { } } + private suspend fun getWalletManagers(): List { + val walletManagerToggles = store.state.daggerGraphState + .get(DaggerGraphState::walletFeatureToggles) + return if (walletManagerToggles.isRedesignedScreenEnabled) { + val walletManagerFacade = store.state.daggerGraphState + .get(DaggerGraphState::walletManagersFacade) + val userWallet = userWalletsListManager.selectedUserWalletSync ?: return emptyList() + walletManagerFacade.getStoredWalletManagers(userWallet.walletId) + } else { + store.state.walletState.walletManagers + } + } + private fun scanCard(scanResponse: ScanResponse, session: WalletConnectSession, chainId: Int?) { val blockchain = WalletConnectNetworkUtils.parseBlockchain( chainId = chainId, @@ -430,7 +447,7 @@ class WalletConnectMiddleware { ) } - private fun getWalletManager( + private suspend fun getWalletManager( wallet: WalletForSession, blockchain: Blockchain, walletState: WalletState, @@ -440,15 +457,29 @@ class WalletConnectMiddleware { } else { blockchain } + val userWallet = userWalletsListManager.selectedUserWalletSync ?: return null val derivation = blockchainToMake.derivationPath( - style = store.state.globalState.scanResponse?.derivationStyleProvider?.getDerivationStyle(), + style = userWallet.scanResponse.derivationStyleProvider.getDerivationStyle(), )?.rawPath - val blockchainNetwork = BlockchainNetwork( - blockchain = blockchainToMake, - derivationPath = derivation, - tokens = emptyList(), - ) - return walletState.getWalletManager(blockchainNetwork) + val walletFeatureToggles = store.state.daggerGraphState + .get(DaggerGraphState::walletFeatureToggles) + + return if (walletFeatureToggles.isRedesignedScreenEnabled) { + val walletManagerFacade = store.state.daggerGraphState + .get(DaggerGraphState::walletManagersFacade) + walletManagerFacade.getOrCreateWalletManager( + userWalletId = userWallet.walletId, + blockchain = blockchainToMake, + derivationPath = derivation, + ) + } else { + val blockchainNetwork = BlockchainNetwork( + blockchain = blockchainToMake, + derivationPath = derivation, + tokens = emptyList(), + ) + walletState.getWalletManager(blockchainNetwork) + } } private fun isWalletConnectUri(uri: String): Boolean { diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/walletmanager/DefaultWalletManagersStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/walletmanager/DefaultWalletManagersStore.kt index 1222edafb4..f99db26624 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/walletmanager/DefaultWalletManagersStore.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/walletmanager/DefaultWalletManagersStore.kt @@ -33,6 +33,10 @@ internal class DefaultWalletManagersStore( } } + override suspend fun getAllSync(userWalletId: UserWalletId): List { + return getSyncOrNull(userWalletId) ?: emptyList() + } + override suspend fun store(userWalletId: UserWalletId, walletManager: WalletManager) { val walletManagers = getSyncOrNull(userWalletId) diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/walletmanager/WalletManagersStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/walletmanager/WalletManagersStore.kt index 8506eb5803..a820b1d7c9 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/walletmanager/WalletManagersStore.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/walletmanager/WalletManagersStore.kt @@ -15,6 +15,8 @@ interface WalletManagersStore { derivationPath: String?, ): WalletManager? + suspend fun getAllSync(userWalletId: UserWalletId): List + suspend fun store(userWalletId: UserWalletId, walletManager: WalletManager) suspend fun clear() diff --git a/domain/legacy/src/main/java/com/tangem/domain/common/extensions/Blockchain.kt b/domain/legacy/src/main/java/com/tangem/domain/common/extensions/Blockchain.kt index 0970e85d77..a37ce82bcb 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/common/extensions/Blockchain.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/common/extensions/Blockchain.kt @@ -212,6 +212,4 @@ private const val NODL_AMOUNT_TO_CREATE_ACCOUNT = 1.5 private val excludedBlockchains = listOf( Blockchain.Unknown, Blockchain.Ducatus, - Blockchain.Telos, // disable in 4.9 - Blockchain.TelosTestnet, // disable in 4.9 ) \ No newline at end of file diff --git a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/DefaultWalletManagersFacade.kt b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/DefaultWalletManagersFacade.kt index 9851ae5bc8..01a025f741 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/DefaultWalletManagersFacade.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/DefaultWalletManagersFacade.kt @@ -221,6 +221,10 @@ class DefaultWalletManagersFacade( return walletManager } + override suspend fun getStoredWalletManagers(userWalletId: UserWalletId): List { + return walletManagersStore.getAllSync(userWalletId) + } + override suspend fun getAddress(userWalletId: UserWalletId, network: Network): List
{ val blockchain = Blockchain.fromId(network.id.value) diff --git a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/WalletManagersFacade.kt b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/WalletManagersFacade.kt index 94ce68d1d3..2a30480141 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/WalletManagersFacade.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/WalletManagersFacade.kt @@ -77,6 +77,8 @@ interface WalletManagersFacade { derivationPath: String?, ): WalletManager? + suspend fun getStoredWalletManagers(userWalletId: UserWalletId): List + /** * Returns ordered list of addresses for selected wallet for given currency * From bf31324d3eedb33603a2d46c17a9ef4fb23d18ae Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 5 Oct 2023 16:35:37 +0300 Subject: [PATCH 156/242] Updated on 2026-08-14 --- .../com/tangem/tap/domain/TapWalletManager.kt | 8 ++- .../walletconnect/WalletConnectMiddleware.kt | 42 ++++++++++++ .../walletconnect/WalletConnectActions.kt | 12 ++++ .../walletmanager/WalletManagersFacade.kt | 1 + .../wallet/viewmodels/WalletViewModel.kt | 67 ++++++++++++++++++- 5 files changed, 125 insertions(+), 5 deletions(-) create mode 100644 domain/legacy/src/main/java/com/tangem/domain/walletconnect/WalletConnectActions.kt diff --git a/app/src/main/java/com/tangem/tap/domain/TapWalletManager.kt b/app/src/main/java/com/tangem/tap/domain/TapWalletManager.kt index 67f1253c9e..729e96fe27 100644 --- a/app/src/main/java/com/tangem/tap/domain/TapWalletManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/TapWalletManager.kt @@ -91,8 +91,12 @@ class TapWalletManager( store.dispatch(GlobalAction.SetIfCardVerifiedOnline(!attestationFailed)) store.dispatchWalletAction(action = WalletAction.Warnings.CheckIfNeeded) } - setupWalletConnectV2(userWallet) - loadData(userWallet = userWallet, refresh = refresh) + + val walletFeatureToggles = store.state.daggerGraphState.get(DaggerGraphState::walletFeatureToggles) + if (!walletFeatureToggles.isRedesignedScreenEnabled) { + setupWalletConnectV2(userWallet) + loadData(userWallet = userWallet, refresh = refresh) + } } private fun Store.dispatchWalletAction(action: WalletAction) { diff --git a/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectMiddleware.kt b/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectMiddleware.kt index a9f177a893..4ed140a246 100644 --- a/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectMiddleware.kt @@ -14,6 +14,8 @@ import com.tangem.domain.common.extensions.withMainContext import com.tangem.domain.common.util.cardTypesResolver import com.tangem.domain.common.util.derivationStyleProvider import com.tangem.domain.models.scan.ScanResponse +import com.tangem.domain.walletconnect.WalletConnectActions +import com.tangem.domain.wallets.models.UserWallet import com.tangem.tap.common.extensions.dispatchOnMain import com.tangem.tap.common.redux.AppState import com.tangem.tap.common.redux.global.GlobalAction @@ -60,6 +62,28 @@ class WalletConnectMiddleware { if (DemoHelper.tryHandle(state, action)) return when (action) { + is WalletConnectActions.New.Initialize -> { + val userWallet = action.userWallet + val cardId = if (userWallet.scanResponse.card.backupStatus?.isActive != true) { + userWallet.cardId + } else { // if wallet has backup, any card from wallet can be used to sign + null + } + scope.launch { + val wcInteractor = store.state.daggerGraphState.walletConnectInteractor ?: return@launch + wcInteractor.startListening( + userWalletId = userWallet.walletId.stringValue, + cardId = cardId, + ) + } + } + is WalletConnectActions.New.SetupUserChains -> { + scope.launch { + val userWallet = action.userWallet + val wcInteractor = store.state.daggerGraphState.walletConnectInteractor ?: return@launch + wcInteractor.setUserChains(getAccountsForWc(wcInteractor, userWallet)) + } + } is WalletConnectAction.ResetState -> walletConnectManager = WalletConnectManager() is WalletConnectAction.RestoreSessions -> { walletConnectManager.restoreSessions(action.scanResponse) @@ -485,4 +509,22 @@ class WalletConnectMiddleware { private fun isWalletConnectUri(uri: String): Boolean { return WalletConnectManager.isCorrectWcUri(uri) || walletConnectInteractor.isWalletConnectUri(uri) } + + private suspend fun getAccountsForWc(wcInteractor: WalletConnectInteractor, userWallet: UserWallet): List { + val walletManagerFacade = store.state.daggerGraphState + .get(DaggerGraphState::walletManagersFacade) + return walletManagerFacade.getStoredWalletManagers(userWallet.walletId).mapNotNull { + val wallet = it.wallet + val chainId = wcInteractor.blockchainHelper.networkIdToChainIdOrNull( + wallet.blockchain.toNetworkId(), + ) + chainId?.let { + Account( + chainId, + wallet.address, + wallet.publicKey.derivationPath?.rawPath, + ) + } + } + } } \ No newline at end of file diff --git a/domain/legacy/src/main/java/com/tangem/domain/walletconnect/WalletConnectActions.kt b/domain/legacy/src/main/java/com/tangem/domain/walletconnect/WalletConnectActions.kt new file mode 100644 index 0000000000..afb7d26d9f --- /dev/null +++ b/domain/legacy/src/main/java/com/tangem/domain/walletconnect/WalletConnectActions.kt @@ -0,0 +1,12 @@ +package com.tangem.domain.walletconnect + +import com.tangem.domain.wallets.models.UserWallet +import org.rekotlin.Action + +sealed class WalletConnectActions : Action { + sealed class New { + data class Initialize(val userWallet: UserWallet) : WalletConnectActions() + + data class SetupUserChains(val userWallet: UserWallet) : WalletConnectActions() + } +} \ No newline at end of file diff --git a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/WalletManagersFacade.kt b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/WalletManagersFacade.kt index 2a30480141..6df4bf3f84 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/WalletManagersFacade.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/WalletManagersFacade.kt @@ -77,6 +77,7 @@ interface WalletManagersFacade { derivationPath: String?, ): WalletManager? + @Deprecated("Will be removed in future") suspend fun getStoredWalletManagers(userWalletId: UserWalletId): List /** diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt index e860f3d518..9cb22da162 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt @@ -2,6 +2,7 @@ package com.tangem.feature.wallet.presentation.wallet.viewmodels import androidx.lifecycle.* import androidx.paging.cachedIn +import arrow.core.Either import arrow.core.getOrElse import com.tangem.blockchain.blockchains.cardano.CardanoUtils import com.tangem.blockchain.common.Blockchain @@ -39,6 +40,7 @@ import com.tangem.domain.redux.LegacyAction import com.tangem.domain.redux.ReduxStateHolder import com.tangem.domain.settings.* import com.tangem.domain.tokens.* +import com.tangem.domain.tokens.error.TokenListError import com.tangem.domain.tokens.legacy.TradeCryptoAction import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus @@ -48,6 +50,7 @@ import com.tangem.domain.tokens.models.analytics.TokenReceiveAnalyticsEvent import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsCountUseCase import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsUseCase import com.tangem.domain.userwallets.UserWalletBuilder +import com.tangem.domain.walletconnect.WalletConnectActions import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWalletId @@ -158,6 +161,7 @@ internal class WalletViewModel @Inject constructor( private var singleWalletCryptoCurrencyStatus: CryptoCurrencyStatus? = null private val tokensJobHolder = JobHolder() + private val updateWcJobHolder = JobHolder() private val marketPriceJobHolder = JobHolder() private val buttonsJobHolder = JobHolder() private val notificationsJobHolder = JobHolder() @@ -455,6 +459,7 @@ internal class WalletViewModel @Inject constructor( * If jobs aren't stopped and wallet is changed then it will update state for the prev wallet. */ tokensJobHolder.update(job = null) + updateWcJobHolder.update(job = null) marketPriceJobHolder.update(job = null) buttonsJobHolder.update(job = null) notificationsJobHolder.update(job = null) @@ -851,6 +856,7 @@ internal class WalletViewModel @Inject constructor( * If jobs aren't stopped and wallet is changed then it will update state for the prev wallet. */ tokensJobHolder.update(job = null) + updateWcJobHolder.update(job = null) marketPriceJobHolder.update(job = null) buttonsJobHolder.update(job = null) notificationsJobHolder.update(job = null) @@ -862,17 +868,22 @@ internal class WalletViewModel @Inject constructor( wallet.isLocked -> { uiState = stateFactory.getLockedState() } - wallet.isMultiCurrency -> getMultiCurrencyContent(index) + wallet.isMultiCurrency -> getMultiCurrencyContent(wallet, index) !wallet.isMultiCurrency -> getSingleCurrencyContent(index) } } - private fun getMultiCurrencyContent(walletIndex: Int) { + private fun getMultiCurrencyContent(wallet: UserWallet, walletIndex: Int) { val state = requireNotNull(uiState as? WalletMultiCurrencyState) { "Impossible to get a token list updates if state isn't WalletMultiCurrencyState" } - getTokenListUseCase(userWalletId = state.walletsListConfig.wallets[walletIndex].id) + val tokenListFlow = getTokenListUseCase(userWalletId = state.walletsListConfig.wallets[walletIndex].id) + .shareIn(viewModelScope, SharingStarted.WhileSubscribed()) + + initAndSetupWc(tokenListFlow, wallet) + + tokenListFlow .distinctUntilChanged() .onEach { maybeTokenList -> uiState = stateFactory.getStateByTokensList(maybeTokenList) @@ -905,6 +916,44 @@ internal class WalletViewModel @Inject constructor( } } + private fun initAndSetupWc(tokenListFlow: SharedFlow>, wallet: UserWallet) { + initWalletConnectForWallet(wallet) + tokenListFlow + .filter(::filterLoadedTokenList) + .take(1) + .onEach { + it.onRight { + setupWalletConnectOnWallet(wallet) + } + } + .flowOn(dispatchers.io) + .launchIn(viewModelScope) + .saveIn(updateWcJobHolder) + } + + private fun List.isAllCurrenciesLoaded(): Boolean { + return !this.any { it.value is CryptoCurrencyStatus.Loading } + } + + private fun filterLoadedTokenList(either: Either): Boolean { + return either.fold( + ifRight = { list -> + when (list) { + is TokenList.Ungrouped -> { + list.currencies.isAllCurrenciesLoaded() + } + is TokenList.GroupedByNetwork -> { + list.groups.flatMap { group -> group.currencies }.isAllCurrenciesLoaded() + } + else -> { + false + } + } + }, + ifLeft = { false }, + ) + } + private fun List.hasNonZeroWallets(): Boolean { return any { val amount = it.value.amount ?: return@any false @@ -1023,6 +1072,18 @@ internal class WalletViewModel @Inject constructor( ) } + private fun initWalletConnectForWallet(userWallet: UserWallet) { + reduxStateHolder.dispatch( + WalletConnectActions.New.Initialize(userWallet = userWallet), + ) + } + + private fun setupWalletConnectOnWallet(userWallet: UserWallet) { + reduxStateHolder.dispatch( + WalletConnectActions.New.SetupUserChains(userWallet = userWallet), + ) + } + private fun getWallet(index: Int): UserWallet { return requireNotNull( value = wallets.getOrNull(index), From 3eb3ea260fae3352c8f43b320ffb96b6fcce1d24 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 9 Oct 2023 20:41:33 +0300 Subject: [PATCH 157/242] Updated on 2026-08-14 --- .../presentation/common/WalletPreviewData.kt | 18 +------ .../common/component/TokenItem.kt | 7 +-- .../common/state/TokenItemState.kt | 2 - .../organizetokens/OrganizeTokensScreen.kt | 5 ++ .../OrganizeTokensStateHolder.kt | 9 ++-- .../organizetokens/OrganizeTokensViewModel.kt | 2 - .../model/OrganizeTokensState.kt | 1 + .../TokenItemHiddenStateConverter.kt | 12 ----- .../error/TokenListHiddenStateConverter.kt | 31 ------------ .../CryptoCurrencyToDraggableItemConverter.kt | 12 ++--- .../state/components/WalletCardState.kt | 24 --------- .../WalletLoadedTokensListConverter.kt | 2 - ...letSingleCurrencyLoadedBalanceConverter.kt | 43 +++++----------- .../state/factory/WalletStateFactory.kt | 2 - .../factory/WalletUpdateCardCountConverter.kt | 6 --- .../presentation/wallet/ui/WalletScreen.kt | 6 ++- .../wallet/ui/components/WalletsList.kt | 15 ++++-- .../wallet/ui/components/common/WalletCard.kt | 19 ++++--- .../ui/components/common/WalletContent.kt | 2 +- .../multicurrency/MultiCurrencyContent.kt | 14 +++++- .../multicurrency/MultiCurrencyContentItem.kt | 8 ++- ...ryptoCurrencyStatusToTokenItemConverter.kt | 2 - .../utils/FiatBalanceToWalletCardConverter.kt | 46 ++++++----------- .../wallet/utils/HiddenStateConverter.kt | 50 +------------------ .../utils/TokenListToContentItemsConverter.kt | 2 - .../utils/TokenListToWalletStateConverter.kt | 3 -- .../WalletHiddenBalanceStateConverter.kt | 44 ---------------- .../wallet/viewmodels/WalletStateCache.kt | 6 +++ .../wallet/viewmodels/WalletViewModel.kt | 1 + .../viewmodels/WalletsUpdateActionResolver.kt | 2 - 30 files changed, 96 insertions(+), 300 deletions(-) delete mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/TokenItemHiddenStateConverter.kt delete mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/error/TokenListHiddenStateConverter.kt delete mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/WalletHiddenBalanceStateConverter.kt diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/WalletPreviewData.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/WalletPreviewData.kt index 9456466cb9..841863194b 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/WalletPreviewData.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/WalletPreviewData.kt @@ -57,19 +57,6 @@ internal object WalletPreviewData { ) } - val walletCardHiddenContentState by lazy { - WalletCardState.HiddenContent( - id = UserWalletId("42"), - title = "Wallet 1", - imageResId = R.drawable.ill_wallet2_cards3_120_106, - onRenameClick = { _, _ -> }, - onDeleteClick = {}, - balance = "8923,05 $", - additionalInfo = TextReference.Str("3 cards • Seed phrase"), - cardCount = 1, - ) - } - val walletCardErrorState by lazy { WalletCardState.Error( id = UserWalletId("24"), @@ -84,7 +71,6 @@ internal object WalletPreviewData { mapOf( UserWalletId(stringValue = "123") to walletCardContentState, UserWalletId(stringValue = "321") to walletCardLoadingState, - UserWalletId(stringValue = "42") to walletCardHiddenContentState, UserWalletId(stringValue = "24") to walletCardErrorState, ) } @@ -131,7 +117,6 @@ internal object WalletPreviewData { fiatAmountState = TokenItemState.FiatAmountState.Content(text = "321 $"), cryptoAmountState = TokenItemState.CryptoAmountState.Content(text = "5,412 MATIC"), priceChangeState = TokenItemState.PriceChangeState.Unknown, - isBalanceHidden = false, onItemClick = {}, onItemLongClick = {}, ) @@ -155,7 +140,6 @@ internal object WalletPreviewData { valueInPercent = "2.0%", type = PriceChangeType.UP, ), - isBalanceHidden = false, onItemClick = {}, onItemLongClick = {}, ) @@ -167,7 +151,6 @@ internal object WalletPreviewData { iconState = tokenIconState, titleState = TokenItemState.TitleState.Content(text = "Polygon"), cryptoAmountState = TokenItemState.CryptoAmountState.Content(text = "3 172,14 $"), - isBalanceHidden = false, ) } @@ -292,6 +275,7 @@ internal object WalletPreviewData { onCancelClick = {}, ), scrollListToTop = consumedEvent(), + isBalanceHidden = true ) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/TokenItem.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/TokenItem.kt index 930a9d838b..ea5364d1a4 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/TokenItem.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/TokenItem.kt @@ -35,6 +35,7 @@ internal fun TokenItem( state: TokenItemState, modifier: Modifier = Modifier, reorderableTokenListState: ReorderableLazyListState? = null, + isBalanceHidden: Boolean ) { CustomContainer( state = state, @@ -42,10 +43,6 @@ internal fun TokenItem( .tokenClickable(state = state) .background(color = TangemTheme.colors.background.primary), ) { - val isBalanceHidden = (state as? TokenItemState.Content)?.isBalanceHidden - ?: (state as? TokenItemState.Draggable)?.isBalanceHidden - ?: false - TokenIcon(state = state.iconState, modifier = Modifier.layoutId(layoutId = LayoutId.ICON)) TokenTitle( @@ -382,7 +379,7 @@ private fun calculateLayoutHeight( @Composable private fun Preview_CustomTokenItem_InLight(@PreviewParameter(TokenItemStateProvider::class) state: TokenItemState) { TangemTheme(isDark = false) { - TokenItem(state = state) + TokenItem(state = state, isBalanceHidden = false) } } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/state/TokenItemState.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/state/TokenItemState.kt index b5d6b52d47..939c833e5d 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/state/TokenItemState.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/state/TokenItemState.kt @@ -55,7 +55,6 @@ internal sealed class TokenItemState { override val fiatAmountState: FiatAmountState, override val cryptoAmountState: CryptoAmountState.Content, override val priceChangeState: PriceChangeState?, - val isBalanceHidden: Boolean, val onItemClick: () -> Unit, val onItemLongClick: () -> Unit, ) : TokenItemState() @@ -72,7 +71,6 @@ internal sealed class TokenItemState { override val iconState: IconState, override val titleState: TitleState, override val cryptoAmountState: CryptoAmountState, - val isBalanceHidden: Boolean, ) : TokenItemState() { override val fiatAmountState: FiatAmountState? = null override val priceChangeState: PriceChangeState? = null diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensScreen.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensScreen.kt index 63b4037673..6a0c471a92 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensScreen.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensScreen.kt @@ -63,6 +63,7 @@ internal fun OrganizeTokensScreen(state: OrganizeTokensState, modifier: Modifier listState = tokensListState, state = state.itemsState, dndConfig = state.dndConfig, + isBalanceHidden = state.isBalanceHidden, ) }, floatingActionButtonPosition = FabPosition.Center, @@ -83,6 +84,7 @@ private fun TokenList( state: OrganizeTokensListState, dndConfig: OrganizeTokensState.DragAndDropConfig, modifier: Modifier = Modifier, + isBalanceHidden: Boolean, ) { Box(modifier = modifier) { val onDragEnd: (Int, Int) -> Unit = remember { @@ -125,6 +127,7 @@ private fun TokenList( item = item, reorderableState = reorderableListState, onDragStart = onDragStart, + isBalanceHidden = isBalanceHidden, ) } } @@ -139,6 +142,7 @@ private fun LazyItemScope.DraggableItem( item: DraggableItem, reorderableState: ReorderableLazyListState, onDragStart: () -> Unit, + isBalanceHidden: Boolean, ) { var isDragging by remember { mutableStateOf(value = false) @@ -163,6 +167,7 @@ private fun LazyItemScope.DraggableItem( modifier = itemModifier, state = item.tokenItemState, reorderableTokenListState = reorderableState, + isBalanceHidden = isBalanceHidden ) // Should be presented in the list but remain invisible is DraggableItem.Placeholder -> Box(modifier = Modifier.fillMaxWidth()) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensStateHolder.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensStateHolder.kt index 9bb4d7874c..18a2625bab 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensStateHolder.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensStateHolder.kt @@ -11,7 +11,6 @@ import com.tangem.feature.wallet.presentation.organizetokens.model.OrganizeToken import com.tangem.feature.wallet.presentation.organizetokens.model.OrganizeTokensState import com.tangem.feature.wallet.presentation.organizetokens.utils.converter.InProgressStateConverter import com.tangem.feature.wallet.presentation.organizetokens.utils.converter.TokenListToStateConverter -import com.tangem.feature.wallet.presentation.organizetokens.utils.converter.error.TokenListHiddenStateConverter import com.tangem.feature.wallet.presentation.organizetokens.utils.converter.error.TokenListErrorConverter import com.tangem.feature.wallet.presentation.organizetokens.utils.converter.error.TokenListSortingErrorConverter import com.tangem.feature.wallet.presentation.organizetokens.utils.converter.items.CryptoCurrencyToDraggableItemConverter @@ -25,14 +24,12 @@ internal class OrganizeTokensStateHolder( private val intents: OrganizeTokensIntents, private val dragAndDropIntents: DragAndDropIntents, private val appCurrencyProvider: Provider, - private val isBalanceHiddenProvider: Provider, - private val listStateProvider: Provider, ) { private val stateFlowInternal: MutableStateFlow = MutableStateFlow(getInitialState()) private val tokenListConverter by lazy { - val tokensConverter = CryptoCurrencyToDraggableItemConverter(appCurrencyProvider, isBalanceHiddenProvider) + val tokensConverter = CryptoCurrencyToDraggableItemConverter(appCurrencyProvider) val itemsConverter = TokenListToListStateConverter( tokensConverter = tokensConverter, groupsConverter = NetworkGroupToDraggableItemsConverter(tokensConverter), @@ -42,7 +39,6 @@ internal class OrganizeTokensStateHolder( } private val inProgressStateConverter by lazy { InProgressStateConverter() } - private val tokenListHiddenStateConverter by lazy { TokenListHiddenStateConverter(listStateProvider) } private val tokenListErrorConverter by lazy { TokenListErrorConverter(Provider(stateFlowInternal::value), inProgressStateConverter) @@ -83,7 +79,7 @@ internal class OrganizeTokensStateHolder( } fun updateHiddenState(isBalanceHidden: Boolean) { - updateState { copy(itemsState = tokenListHiddenStateConverter.convert(isBalanceHidden)) } + updateState { copy(isBalanceHidden = isBalanceHidden) } } fun updateStateWithError(error: TokenListError) { @@ -113,6 +109,7 @@ internal class OrganizeTokensStateHolder( canDragItemOver = dragAndDropIntents::canDragItemOver, ), scrollListToTop = consumedEvent(), + isBalanceHidden = true ) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensViewModel.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensViewModel.kt index d71ac77914..a522081e21 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensViewModel.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensViewModel.kt @@ -59,8 +59,6 @@ internal class OrganizeTokensViewModel @Inject constructor( intents = this, dragAndDropIntents = dragAndDropAdapter, appCurrencyProvider = Provider(selectedAppCurrencyFlow::value), - isBalanceHiddenProvider = Provider { isBalanceHidden }, - listStateProvider = Provider { uiState.value.itemsState }, ) private val userWalletId: UserWalletId by lazy { diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/model/OrganizeTokensState.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/model/OrganizeTokensState.kt index a269938058..d290f81bb7 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/model/OrganizeTokensState.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/model/OrganizeTokensState.kt @@ -12,6 +12,7 @@ internal data class OrganizeTokensState( val actions: ActionsConfig, val dndConfig: DragAndDropConfig, val scrollListToTop: StateEvent, + val isBalanceHidden: Boolean, ) { data class HeaderConfig( diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/TokenItemHiddenStateConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/TokenItemHiddenStateConverter.kt deleted file mode 100644 index 5a98aa841d..0000000000 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/TokenItemHiddenStateConverter.kt +++ /dev/null @@ -1,12 +0,0 @@ -package com.tangem.feature.wallet.presentation.organizetokens.utils.converter - -internal class TokenItemHiddenStateConverter { - - fun updateHiddenState(wasBalanceHidden: Boolean, isBalanceHidden: Boolean): Boolean { - return when { - !wasBalanceHidden && isBalanceHidden -> true - wasBalanceHidden && !isBalanceHidden -> false - else -> wasBalanceHidden - } - } -} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/error/TokenListHiddenStateConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/error/TokenListHiddenStateConverter.kt deleted file mode 100644 index 96e5cf4538..0000000000 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/error/TokenListHiddenStateConverter.kt +++ /dev/null @@ -1,31 +0,0 @@ -package com.tangem.feature.wallet.presentation.organizetokens.utils.converter.error - -import com.tangem.common.Provider -import com.tangem.feature.wallet.presentation.organizetokens.model.DraggableItem -import com.tangem.feature.wallet.presentation.organizetokens.model.OrganizeTokensListState -import com.tangem.utils.converter.Converter -import kotlinx.collections.immutable.toPersistentList - -internal class TokenListHiddenStateConverter( - private val currentStateProvider: Provider, -) : Converter { - - override fun convert(input: Boolean): OrganizeTokensListState { - val currentState = currentStateProvider() - val isBalanceHidden = input - - return currentState.copySealed( - currentState.items.map { draggableItem -> - if (draggableItem is DraggableItem.Token) { - draggableItem.copy( - tokenItemState = draggableItem.tokenItemState.copy( - isBalanceHidden = isBalanceHidden, - ), - ) - } else { - draggableItem - } - }.toPersistentList(), - ) - } -} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/items/CryptoCurrencyToDraggableItemConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/items/CryptoCurrencyToDraggableItemConverter.kt index f18e43de7a..aa518deba2 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/items/CryptoCurrencyToDraggableItemConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/items/CryptoCurrencyToDraggableItemConverter.kt @@ -13,29 +13,26 @@ import com.tangem.utils.converter.Converter internal class CryptoCurrencyToDraggableItemConverter( private val appCurrencyProvider: Provider, - private val isBalanceHiddenProvider: Provider, ) : Converter { private val iconStateConverter = CryptoCurrencyToIconStateConverter() override fun convert(value: CryptoCurrencyStatus): DraggableItem.Token { - return createDraggableToken(value, appCurrencyProvider(), isBalanceHiddenProvider()) + return createDraggableToken(value, appCurrencyProvider()) } override fun convertList(input: Collection): List { val appCurrency = appCurrencyProvider() - val isBalanceHidden = isBalanceHiddenProvider() - return input.map { createDraggableToken(it, appCurrency, isBalanceHidden) } + return input.map { createDraggableToken(it, appCurrency) } } private fun createDraggableToken( currencyStatus: CryptoCurrencyStatus, appCurrency: AppCurrency, - isBalanceHidden: Boolean, ): DraggableItem.Token { return DraggableItem.Token( - tokenItemState = createTokenItemState(currencyStatus, appCurrency, isBalanceHidden), + tokenItemState = createTokenItemState(currencyStatus, appCurrency), groupId = getGroupHeaderId(currencyStatus.currency.network), ) } @@ -43,7 +40,6 @@ internal class CryptoCurrencyToDraggableItemConverter( private fun createTokenItemState( currencyStatus: CryptoCurrencyStatus, appCurrency: AppCurrency, - isBalanceHidden: Boolean, ): TokenItemState.Draggable { val currency = currencyStatus.currency @@ -56,8 +52,6 @@ internal class CryptoCurrencyToDraggableItemConverter( } else { TokenItemState.CryptoAmountState.Content(text = getFormattedFiatAmount(currencyStatus, appCurrency)) }, - isBalanceHidden = isBalanceHidden, - ) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/components/WalletCardState.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/components/WalletCardState.kt index 8edccf6a0f..4a68a0d8fd 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/components/WalletCardState.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/components/WalletCardState.kt @@ -49,29 +49,6 @@ internal sealed interface WalletCardState { val balance: String, ) : WalletCardState - /** - * Wallet card hidden content state - * - * @property id wallet id - * @property title wallet name - * @property imageResId wallet image resource id - * @property onRenameClick lambda be invoked when Rename button is clicked - * @property onDeleteClick lambda be invoked when Delete button is clicked - * @property additionalInfo wallet additional info - * @property cardCount number of cards in the wallet - * @property balance wallet balance - */ - data class HiddenContent( - override val id: UserWalletId, - override val title: String, - override val imageResId: Int?, - override val onRenameClick: (UserWalletId, String) -> Unit, - override val onDeleteClick: (UserWalletId) -> Unit, - val additionalInfo: TextReference, - val balance: String, - val cardCount: Int?, - ) : WalletCardState - /** * Wallet card locked state * @@ -129,7 +106,6 @@ internal sealed interface WalletCardState { return when (this) { is Content -> copy(title = title) is Error -> copy(title = title) - is HiddenContent -> copy(title = title) is Loading -> copy(title = title) is LockedContent -> copy(title = title) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletLoadedTokensListConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletLoadedTokensListConverter.kt index 1f6d2ceff0..3cd576ca7d 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletLoadedTokensListConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletLoadedTokensListConverter.kt @@ -30,7 +30,6 @@ internal class WalletLoadedTokensListConverter( private val tokenListErrorConverter: TokenListErrorConverter, appCurrencyProvider: Provider, currentWalletProvider: Provider, - isBalanceHiddenProvider: Provider, clickIntents: WalletClickIntents, ) : Converter, WalletState> { @@ -38,7 +37,6 @@ internal class WalletLoadedTokensListConverter( currentStateProvider = currentStateProvider, currentWalletProvider = currentWalletProvider, appCurrencyProvider = appCurrencyProvider, - isBalanceHiddenProvider = isBalanceHiddenProvider, clickIntents = clickIntents, ) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletSingleCurrencyLoadedBalanceConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletSingleCurrencyLoadedBalanceConverter.kt index 6f24e4d02b..ccfbe015bc 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletSingleCurrencyLoadedBalanceConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletSingleCurrencyLoadedBalanceConverter.kt @@ -26,7 +26,6 @@ internal class WalletSingleCurrencyLoadedBalanceConverter( private val currentStateProvider: Provider, private val appCurrencyProvider: Provider, private val currentWalletProvider: Provider, - private val isBalanceHiddenProvider: Provider, private val currencyStatusErrorConverter: CurrencyStatusErrorConverter, ) : Converter, WalletState> { @@ -87,35 +86,19 @@ internal class WalletSingleCurrencyLoadedBalanceConverter( is CryptoCurrencyStatus.NoQuote, is CryptoCurrencyStatus.Loaded, -> { - if (isBalanceHiddenProvider()) { - WalletCardState.HiddenContent( - id = selectedWallet.id, - title = selectedWallet.title, - additionalInfo = WalletAdditionalInfoFactory.resolve( - wallet = currentWalletProvider(), - currencyAmount = status.amount, - ), - imageResId = selectedWallet.imageResId, - onRenameClick = selectedWallet.onRenameClick, - onDeleteClick = selectedWallet.onDeleteClick, - balance = formatFiatAmount(status = status, appCurrency = appCurrencyProvider()), - cardCount = currentWalletProvider().getCardsCount(), - ) - } else { - WalletCardState.Content( - id = selectedWallet.id, - title = selectedWallet.title, - additionalInfo = WalletAdditionalInfoFactory.resolve( - wallet = currentWalletProvider(), - currencyAmount = status.amount, - ), - imageResId = selectedWallet.imageResId, - onRenameClick = selectedWallet.onRenameClick, - onDeleteClick = selectedWallet.onDeleteClick, - balance = formatFiatAmount(status = status, appCurrency = appCurrencyProvider()), - cardCount = currentWalletProvider().getCardsCount(), - ) - } + WalletCardState.Content( + id = selectedWallet.id, + title = selectedWallet.title, + additionalInfo = WalletAdditionalInfoFactory.resolve( + wallet = currentWalletProvider(), + currencyAmount = status.amount, + ), + imageResId = selectedWallet.imageResId, + onRenameClick = selectedWallet.onRenameClick, + onDeleteClick = selectedWallet.onDeleteClick, + balance = formatFiatAmount(status = status, appCurrency = appCurrencyProvider()), + cardCount = currentWalletProvider().getCardsCount(), + ) } is CryptoCurrencyStatus.Loading -> { WalletCardState.Loading( diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletStateFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletStateFactory.kt index 4354bc8992..d904986cb5 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletStateFactory.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletStateFactory.kt @@ -80,7 +80,6 @@ internal class WalletStateFactory( tokenListErrorConverter = tokenListErrorConverter, appCurrencyProvider = appCurrencyProvider, currentWalletProvider = currentWalletProvider, - isBalanceHiddenProvider = isBalanceHiddenProvider, clickIntents = clickIntents, ) } @@ -106,7 +105,6 @@ internal class WalletStateFactory( currentStateProvider = currentStateProvider, appCurrencyProvider = appCurrencyProvider, currentWalletProvider = currentWalletProvider, - isBalanceHiddenProvider = isBalanceHiddenProvider, currencyStatusErrorConverter = currencyStatusErrorConverter, ) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletUpdateCardCountConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletUpdateCardCountConverter.kt index 805f3168b4..59a45b0f28 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletUpdateCardCountConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletUpdateCardCountConverter.kt @@ -38,12 +38,6 @@ internal class WalletUpdateCardCountConverter( ), cardCount = currentWalletProvider().getCardsCount(), ) - is WalletCardState.HiddenContent -> walletCard.copy( - additionalInfo = WalletAdditionalInfoFactory.resolve( - wallet = currentWalletProvider(), - ), - cardCount = currentWalletProvider().getCardsCount(), - ) else -> walletCard } } else { diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt index 8d61d90510..fe7215f347 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt @@ -126,7 +126,11 @@ private fun WalletContent( horizontalAlignment = Alignment.CenterHorizontally, ) { item { - WalletsList(config = state.walletsListConfig, lazyListState = walletsListState) + WalletsList( + config = state.walletsListConfig, + lazyListState = walletsListState, + isBalanceHidden = state.isBalanceHidden, + ) } if (state is WalletSingleCurrencyState) { diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/WalletsList.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/WalletsList.kt index 9da275d0cb..431197b952 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/WalletsList.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/WalletsList.kt @@ -38,7 +38,7 @@ private const val SHORT_SNAP_ELEMENT_COUNT = 50 */ @OptIn(ExperimentalFoundationApi::class) @Composable -internal fun WalletsList(config: WalletsListConfig, lazyListState: LazyListState) { +internal fun WalletsList(config: WalletsListConfig, lazyListState: LazyListState, isBalanceHidden: Boolean) { val horizontalCardPadding = TangemTheme.dimens.spacing16 val screenWidth = LocalConfiguration.current.screenWidthDp.dp val itemWidth by remember(screenWidth) { derivedStateOf { screenWidth - horizontalCardPadding * 2 } } @@ -60,6 +60,7 @@ internal fun WalletsList(config: WalletsListConfig, lazyListState: LazyListState modifier = Modifier .animateItemPlacement() .width(itemWidth), + isBalanceHidden = isBalanceHidden ) } } @@ -98,7 +99,11 @@ private fun rememberWalletsFlingBehaviour(lazyListState: LazyListState, itemWidt @Composable private fun Preview_WalletsList_LightTheme() { TangemTheme(isDark = false) { - WalletsList(config = WalletPreviewData.walletListConfig, lazyListState = rememberLazyListState()) + WalletsList( + config = WalletPreviewData.walletListConfig, + lazyListState = rememberLazyListState(), + isBalanceHidden = false + ) } } @@ -106,6 +111,10 @@ private fun Preview_WalletsList_LightTheme() { @Composable private fun Preview_WalletsList_DarkTheme() { TangemTheme(isDark = true) { - WalletsList(config = WalletPreviewData.walletListConfig, lazyListState = rememberLazyListState()) + WalletsList( + config = WalletPreviewData.walletListConfig, + lazyListState = rememberLazyListState(), + isBalanceHidden = false + ) } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletCard.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletCard.kt index 9af63cffb8..def0b40b40 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletCard.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletCard.kt @@ -57,7 +57,7 @@ private const val HALF_OF_ITEM_WIDTH = 0.5 [REDACTED_AUTHOR] */ @Composable -internal fun WalletCard(state: WalletCardState, modifier: Modifier = Modifier) { +internal fun WalletCard(state: WalletCardState, modifier: Modifier = Modifier, isBalanceHidden: Boolean) { @Suppress("DestructuringDeclarationWithTooManyEntries") CardContainer( name = state.title, @@ -90,6 +90,7 @@ internal fun WalletCard(state: WalletCardState, modifier: Modifier = Modifier) { top.linkTo(anchor = titleRef.bottom) bottom.linkTo(anchor = additionalTextRef.top) }, + isBalanceHidden = isBalanceHidden ) AdditionalInfo( @@ -102,7 +103,6 @@ internal fun WalletCard(state: WalletCardState, modifier: Modifier = Modifier) { when (state) { is WalletCardState.Content, is WalletCardState.Error, - is WalletCardState.HiddenContent, -> { end.linkTo(imageRef.start) width = Dimension.fillToConstraints @@ -275,7 +275,7 @@ private fun TitleText(text: String, modifier: Modifier = Modifier) { @OptIn(ExperimentalAnimationApi::class) @Composable -private fun Balance(state: WalletCardState, modifier: Modifier = Modifier) { +private fun Balance(state: WalletCardState, modifier: Modifier = Modifier, isBalanceHidden: Boolean) { AnimatedContent( targetState = state, label = "Update the balance", @@ -288,7 +288,7 @@ private fun Balance(state: WalletCardState, modifier: Modifier = Modifier) { when (walletCardState) { is WalletCardState.Content -> { ResizableText( - text = walletCardState.balance, + text = if (isBalanceHidden) Strings.STARS else walletCardState.balance, fontSizeRange = FontSizeRange(min = 16.sp, max = TangemTheme.typography.h2.fontSize), modifier = Modifier.defaultMinSize(minHeight = TangemTheme.dimens.size32), color = TangemTheme.colors.text.primary1, @@ -297,8 +297,9 @@ private fun Balance(state: WalletCardState, modifier: Modifier = Modifier) { style = TangemTheme.typography.h2, ) } - is WalletCardState.HiddenContent -> NonContentBalanceText(TextReference.Str(Strings.STARS)) - is WalletCardState.Error -> NonContentBalanceText(text = WalletCardState.EMPTY_BALANCE_TEXT) + is WalletCardState.Error -> NonContentBalanceText( + text = if (isBalanceHidden) WalletCardState.HIDDEN_BALANCE_TEXT else WalletCardState.EMPTY_BALANCE_TEXT + ) is WalletCardState.Loading -> { RectangleShimmer(modifier = Modifier.nonContentBalanceSize(TangemTheme.dimens)) } @@ -349,7 +350,6 @@ private fun resolveAdditionalTextByState(state: WalletCardState): TextReference? is WalletCardState.Content -> state.additionalInfo is WalletCardState.LockedContent -> state.additionalInfo is WalletCardState.Error -> WalletCardState.EMPTY_BALANCE_TEXT - is WalletCardState.HiddenContent -> WalletCardState.HIDDEN_BALANCE_TEXT is WalletCardState.Loading -> null } } @@ -402,7 +402,7 @@ private fun Preview_WalletCard_LightTheme( state: WalletCardState, ) { TangemTheme(isDark = false) { - WalletCard(state = state) + WalletCard(state = state, isBalanceHidden = false) } } @@ -410,7 +410,7 @@ private fun Preview_WalletCard_LightTheme( @Composable private fun Preview_WalletCard_DarkTheme(@PreviewParameter(WalletCardStateProvider::class) state: WalletCardState) { TangemTheme(isDark = true) { - WalletCard(state) + WalletCard(state = state, isBalanceHidden = false) } } @@ -418,7 +418,6 @@ private class WalletCardStateProvider : CollectionPreviewParameterProvider tokensListItems(state.tokensListState, modifier) + is WalletMultiCurrencyState -> tokensListItems(state.tokensListState, modifier, isBalanceHidden) is WalletSingleCurrencyState -> txHistoryItems(state.txHistoryState, txHistoryItems, isBalanceHidden, modifier) } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/multicurrency/MultiCurrencyContent.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/multicurrency/MultiCurrencyContent.kt index 9bdaf10b43..fe9967104d 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/multicurrency/MultiCurrencyContent.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/multicurrency/MultiCurrencyContent.kt @@ -30,9 +30,17 @@ private const val NON_CONTENT_TOKENS_LIST_KEY = "NON_CONTENT_TOKENS_LIST" * [REDACTED_AUTHOR] */ -internal fun LazyListScope.tokensListItems(state: WalletTokensListState, modifier: Modifier = Modifier) { +internal fun LazyListScope.tokensListItems( + state: WalletTokensListState, + modifier: Modifier = Modifier, + isBalanceHidden: Boolean, +) { when (state) { - is WalletTokensListState.ContentState -> contentItems(items = state.items, modifier = modifier) + is WalletTokensListState.ContentState -> contentItems( + items = state.items, + modifier = modifier, + isBalanceHidden = isBalanceHidden + ) WalletTokensListState.Empty -> nonContentItem(modifier = modifier) } } @@ -40,6 +48,7 @@ internal fun LazyListScope.tokensListItems(state: WalletTokensListState, modifie private fun LazyListScope.contentItems( items: ImmutableList, modifier: Modifier = Modifier, + isBalanceHidden: Boolean, ) { itemsIndexed( items = items, @@ -52,6 +61,7 @@ private fun LazyListScope.contentItems( currentIndex = index, lastIndex = items.lastIndex, ), + isBalanceHidden = isBalanceHidden ) }, ) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/multicurrency/MultiCurrencyContentItem.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/multicurrency/MultiCurrencyContentItem.kt index 882548c609..e923d38147 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/multicurrency/MultiCurrencyContentItem.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/multicurrency/MultiCurrencyContentItem.kt @@ -16,13 +16,17 @@ import com.tangem.feature.wallet.presentation.wallet.state.components.WalletToke [REDACTED_AUTHOR] */ @Composable -internal fun MultiCurrencyContentItem(state: WalletTokensListState.TokensListItemState, modifier: Modifier = Modifier) { +internal fun MultiCurrencyContentItem( + state: WalletTokensListState.TokensListItemState, + modifier: Modifier = Modifier, + isBalanceHidden: Boolean, +) { when (state) { is WalletTokensListState.TokensListItemState.NetworkGroupTitle -> { NetworkGroupItem(networkName = state.name.resolveReference(), modifier = modifier) } is WalletTokensListState.TokensListItemState.Token -> { - TokenItem(state = state.state, modifier = modifier) + TokenItem(state = state.state, modifier = modifier, isBalanceHidden = isBalanceHidden) } } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/CryptoCurrencyStatusToTokenItemConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/CryptoCurrencyStatusToTokenItemConverter.kt index 89ce3ee6b1..e5b68effb8 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/CryptoCurrencyStatusToTokenItemConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/CryptoCurrencyStatusToTokenItemConverter.kt @@ -13,7 +13,6 @@ import java.math.BigDecimal internal class CryptoCurrencyStatusToTokenItemConverter( private val appCurrencyProvider: Provider, - private val isBalanceHiddenProvider: Provider, private val clickIntents: WalletClickIntents, ) : Converter { @@ -48,7 +47,6 @@ internal class CryptoCurrencyStatusToTokenItemConverter( ), cryptoAmountState = TokenItemState.CryptoAmountState.Content(text = getFormattedAmount()), priceChangeState = getPriceChangeConfig(), - isBalanceHidden = isBalanceHiddenProvider(), onItemClick = { clickIntents.onTokenItemClick(currency) }, onItemLongClick = { clickIntents.onTokenItemLongClick(cryptoCurrencyStatus = this) }, ) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/FiatBalanceToWalletCardConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/FiatBalanceToWalletCardConverter.kt index bca1082c42..85baf34ab3 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/FiatBalanceToWalletCardConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/FiatBalanceToWalletCardConverter.kt @@ -14,7 +14,6 @@ internal class FiatBalanceToWalletCardConverter( private val currentState: WalletCardState, private val appCurrencyProvider: Provider, private val currentWalletProvider: Provider, - private val isBalanceHiddenProvider: Provider, ) : Converter { override fun convert(value: FiatBalance): WalletCardState { @@ -42,36 +41,19 @@ internal class FiatBalanceToWalletCardConverter( private fun FiatBalance.Loaded.convertToWalletCardState(): WalletCardState { val appCurrency = appCurrencyProvider() - return if (isBalanceHiddenProvider()) { - WalletCardState.HiddenContent( - id = currentState.id, - title = currentState.title, - imageResId = currentState.imageResId, - onRenameClick = currentState.onRenameClick, - onDeleteClick = currentState.onDeleteClick, - balance = formatFiatAmount( - fiatAmount = this.amount, - fiatCurrencyCode = appCurrency.code, - fiatCurrencySymbol = appCurrency.symbol, - ), - additionalInfo = WalletAdditionalInfoFactory.resolve(wallet = currentWalletProvider()), - cardCount = currentWalletProvider().getCardsCount(), - ) - } else { - WalletCardState.Content( - id = currentState.id, - title = currentState.title, - additionalInfo = WalletAdditionalInfoFactory.resolve(wallet = currentWalletProvider()), - imageResId = currentState.imageResId, - onRenameClick = currentState.onRenameClick, - onDeleteClick = currentState.onDeleteClick, - balance = formatFiatAmount( - fiatAmount = this.amount, - fiatCurrencyCode = appCurrency.code, - fiatCurrencySymbol = appCurrency.symbol, - ), - cardCount = currentWalletProvider().getCardsCount(), - ) - } + return WalletCardState.Content( + id = currentState.id, + title = currentState.title, + additionalInfo = WalletAdditionalInfoFactory.resolve(wallet = currentWalletProvider()), + imageResId = currentState.imageResId, + onRenameClick = currentState.onRenameClick, + onDeleteClick = currentState.onDeleteClick, + balance = formatFiatAmount( + fiatAmount = this.amount, + fiatCurrencyCode = appCurrency.code, + fiatCurrencySymbol = appCurrency.symbol, + ), + cardCount = currentWalletProvider().getCardsCount(), + ) } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/HiddenStateConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/HiddenStateConverter.kt index 837ac11b52..67e39880dd 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/HiddenStateConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/HiddenStateConverter.kt @@ -2,68 +2,22 @@ package com.tangem.feature.wallet.presentation.wallet.utils import com.tangem.common.Converter import com.tangem.common.Provider -import com.tangem.feature.wallet.presentation.common.state.TokenItemState -import com.tangem.feature.wallet.presentation.organizetokens.utils.converter.TokenItemHiddenStateConverter import com.tangem.feature.wallet.presentation.wallet.state.WalletMultiCurrencyState import com.tangem.feature.wallet.presentation.wallet.state.WalletSingleCurrencyState import com.tangem.feature.wallet.presentation.wallet.state.WalletState -import com.tangem.feature.wallet.presentation.wallet.state.components.WalletTokensListState -import kotlinx.collections.immutable.toImmutableList internal class HiddenStateConverter( private val currentStateProvider: Provider, ) : Converter { - private val walletHiddenBalanceStateConverter by lazy { WalletHiddenBalanceStateConverter() } - - private val tokenItemHiddenStateConverter by lazy { TokenItemHiddenStateConverter() } - override fun convert(value: Boolean): WalletState { return when (val state = currentStateProvider() as? WalletState.ContentState) { is WalletMultiCurrencyState.Content -> { - val updatedTokensList = (state.tokensListState as? WalletTokensListState.Content)?.let { content -> - content.copy( - items = content.items.map { tokenListItemState -> - if (tokenListItemState is WalletTokensListState.TokensListItemState.Token) { - if (tokenListItemState.state is TokenItemState.Content) { - tokenListItemState.copy( - state = tokenListItemState.state.copy( - isBalanceHidden = tokenItemHiddenStateConverter.updateHiddenState( - wasBalanceHidden = tokenListItemState.state.isBalanceHidden, - isBalanceHidden = value, - ), - ), - ) - } else { - tokenListItemState - } - } else { - tokenListItemState - } - }.toImmutableList(), - ) - } ?: state.tokensListState - - state.copy( - walletsListConfig = state.walletsListConfig.copy( - wallets = state.walletsListConfig.wallets.map { - walletHiddenBalanceStateConverter.updateHiddenState(it, value) - }.toImmutableList(), - ), - tokensListState = updatedTokensList, - isBalanceHidden = value - ) + state.copy(isBalanceHidden = value) } is WalletSingleCurrencyState.Content -> { - state.copy( - walletsListConfig = state.walletsListConfig.copy( - wallets = state.walletsListConfig.wallets.map { - walletHiddenBalanceStateConverter.updateHiddenState(it, value) - }.toImmutableList(), - ), - isBalanceHidden = value - ) + state.copy(isBalanceHidden = value) } else -> currentStateProvider() diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/TokenListToContentItemsConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/TokenListToContentItemsConverter.kt index 3ed9fb061b..45442e30c8 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/TokenListToContentItemsConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/TokenListToContentItemsConverter.kt @@ -17,12 +17,10 @@ import kotlinx.collections.immutable.persistentListOf internal class TokenListToContentItemsConverter( appCurrencyProvider: Provider, - isBalanceHiddenProvider: Provider, private val clickIntents: WalletClickIntents, ) : Converter { private val tokenStatusConverter = CryptoCurrencyStatusToTokenItemConverter( - isBalanceHiddenProvider = isBalanceHiddenProvider, appCurrencyProvider = appCurrencyProvider, clickIntents = clickIntents, ) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/TokenListToWalletStateConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/TokenListToWalletStateConverter.kt index eabeaf9611..896f278745 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/TokenListToWalletStateConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/TokenListToWalletStateConverter.kt @@ -17,12 +17,10 @@ internal class TokenListToWalletStateConverter( private val currentStateProvider: Provider, private val currentWalletProvider: Provider, private val appCurrencyProvider: Provider, - private val isBalanceHiddenProvider: Provider, clickIntents: WalletClickIntents, ) : Converter { private val tokenListToContentConverter = TokenListToContentItemsConverter( - isBalanceHiddenProvider = isBalanceHiddenProvider, appCurrencyProvider = appCurrencyProvider, clickIntents = clickIntents, ) @@ -50,7 +48,6 @@ internal class TokenListToWalletStateConverter( currentState = selectedWalletCard, currentWalletProvider = currentWalletProvider, appCurrencyProvider = appCurrencyProvider, - isBalanceHiddenProvider = isBalanceHiddenProvider, ) return walletsListConfig.copy( diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/WalletHiddenBalanceStateConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/WalletHiddenBalanceStateConverter.kt deleted file mode 100644 index eb85c8f42e..0000000000 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/WalletHiddenBalanceStateConverter.kt +++ /dev/null @@ -1,44 +0,0 @@ -package com.tangem.feature.wallet.presentation.wallet.utils - -import com.tangem.feature.wallet.presentation.wallet.state.components.WalletCardState - -internal class WalletHiddenBalanceStateConverter { - - fun updateHiddenState(walletCardState: WalletCardState, hiddenBalance: Boolean): WalletCardState { - return when { - walletCardState is WalletCardState.Content && hiddenBalance -> { - contentToHidden(walletCardState) - } - walletCardState is WalletCardState.HiddenContent && !hiddenBalance -> { - hiddenToContent(walletCardState) - } - else -> walletCardState - } - } - - private fun contentToHidden(content: WalletCardState.Content): WalletCardState.HiddenContent { - return WalletCardState.HiddenContent( - id = content.id, - title = content.title, - additionalInfo = content.additionalInfo, - imageResId = content.imageResId, - onRenameClick = content.onRenameClick, - onDeleteClick = content.onDeleteClick, - balance = content.balance, - cardCount = content.cardCount, - ) - } - - private fun hiddenToContent(hiddenContent: WalletCardState.HiddenContent): WalletCardState.Content { - return WalletCardState.Content( - id = hiddenContent.id, - title = hiddenContent.title, - additionalInfo = hiddenContent.additionalInfo, - imageResId = hiddenContent.imageResId, - onRenameClick = hiddenContent.onRenameClick, - onDeleteClick = hiddenContent.onDeleteClick, - balance = hiddenContent.balance, - cardCount = hiddenContent.cardCount, - ) - } -} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletStateCache.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletStateCache.kt index f508bacc5a..489b6dd666 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletStateCache.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletStateCache.kt @@ -19,4 +19,10 @@ internal object WalletStateCache { fun update(userWalletId: UserWalletId, state: WalletState.ContentState) { states[userWalletId] = state } + + fun updateAll(func: (WalletState.ContentState.() -> WalletState.ContentState)) { + states.keys.forEach { + states[it] = func(states[it]!!) + } + } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt index e860f3d518..e6cf464d0f 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt @@ -191,6 +191,7 @@ internal class WalletViewModel @Inject constructor( .flowWithLifecycle(owner.lifecycle) .onEach { hidden -> isBalanceHidden = hidden + WalletStateCache.updateAll { copySealed(isBalanceHidden = hidden) } uiState = stateFactory.getHiddenBalanceState(isBalanceHidden = hidden) } .launchIn(viewModelScope) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletsUpdateActionResolver.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletsUpdateActionResolver.kt index bc8aa4f61a..d540d4e4d0 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletsUpdateActionResolver.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletsUpdateActionResolver.kt @@ -126,8 +126,6 @@ internal class WalletsUpdateActionResolver( } previousWalletState is WalletCardState.Content && - previousWalletState.cardCount != selectedWallet.getCardsCount() || - previousWalletState is WalletCardState.HiddenContent && previousWalletState.cardCount != selectedWallet.getCardsCount() -> { Action.UpdateWalletCardCount } From 97890ac7501722963bdf32845d0bd5ca4130adfd Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 9 Oct 2023 22:31:12 +0300 Subject: [PATCH 158/242] Updated on 2026-08-14 --- .../presentation/wallet/state/factory/WalletStateFactory.kt | 2 -- .../state/factory/txhistory/WalletLoadedTxHistoryConverter.kt | 2 -- .../factory/txhistory/WalletTxHistoryItemFlowConverter.kt | 2 -- .../txhistory/WalletTxHistoryTransactionStateConverter.kt | 3 --- .../wallet/presentation/wallet/viewmodels/WalletViewModel.kt | 3 --- 5 files changed, 12 deletions(-) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletStateFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletStateFactory.kt index d904986cb5..1c13a73e30 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletStateFactory.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletStateFactory.kt @@ -45,7 +45,6 @@ internal class WalletStateFactory( private val currentCardTypeResolverProvider: Provider, private val currentWalletProvider: Provider, private val appCurrencyProvider: Provider, - private val isBalanceHiddenProvider: Provider, private val clickIntents: WalletClickIntents, ) { @@ -94,7 +93,6 @@ internal class WalletStateFactory( private val loadedTxHistoryConverter by lazy { WalletLoadedTxHistoryConverter( currentStateProvider = currentStateProvider, - isBalanceHiddenProvider = isBalanceHiddenProvider, currentCardTypeResolverProvider = currentCardTypeResolverProvider, clickIntents = clickIntents, ) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/txhistory/WalletLoadedTxHistoryConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/txhistory/WalletLoadedTxHistoryConverter.kt index 7d2636f3a2..202fcc3ae5 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/txhistory/WalletLoadedTxHistoryConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/txhistory/WalletLoadedTxHistoryConverter.kt @@ -27,7 +27,6 @@ internal class WalletLoadedTxHistoryConverter( private val currentStateProvider: Provider, private val currentCardTypeResolverProvider: Provider, private val clickIntents: WalletClickIntents, - private val isBalanceHiddenProvider: Provider, ) : Converter>>, WalletState> { private val walletTxHistoryItemFlowConverter by lazy { @@ -35,7 +34,6 @@ internal class WalletLoadedTxHistoryConverter( currentStateProvider = currentStateProvider, blockchain = currentCardTypeResolverProvider().getBlockchain(), clickIntents = clickIntents, - isBalanceHiddenProvider = isBalanceHiddenProvider, ) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/txhistory/WalletTxHistoryItemFlowConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/txhistory/WalletTxHistoryItemFlowConverter.kt index 2789256422..0c5b962d71 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/txhistory/WalletTxHistoryItemFlowConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/txhistory/WalletTxHistoryItemFlowConverter.kt @@ -35,7 +35,6 @@ import org.joda.time.DateTimeZone */ internal class WalletTxHistoryItemFlowConverter( private val currentStateProvider: Provider, - private val isBalanceHiddenProvider: Provider, private val blockchain: Blockchain, private val clickIntents: WalletClickIntents, ) : Converter>, TxHistoryState?> { @@ -44,7 +43,6 @@ internal class WalletTxHistoryItemFlowConverter( WalletTxHistoryTransactionStateConverter( symbol = blockchain.currency, decimals = blockchain.decimals(), - isBalanceHiddenProvider = isBalanceHiddenProvider, ) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/txhistory/WalletTxHistoryTransactionStateConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/txhistory/WalletTxHistoryTransactionStateConverter.kt index c4af843d2b..5c953517fe 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/txhistory/WalletTxHistoryTransactionStateConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/txhistory/WalletTxHistoryTransactionStateConverter.kt @@ -13,7 +13,6 @@ import com.tangem.utils.toFormattedCurrencyString class WalletTxHistoryTransactionStateConverter( private val symbol: String, private val decimals: Int, - private val isBalanceHiddenProvider: Provider, ) : Converter { override fun convert(value: TxHistoryItem): TransactionState { @@ -146,8 +145,6 @@ class WalletTxHistoryTransactionStateConverter( } private fun TxHistoryItem.getAmount(): String { - if (isBalanceHiddenProvider()) return Strings.STARS - val prefix = when (direction) { is TxHistoryItem.TransactionDirection.Incoming -> "+" is TxHistoryItem.TransactionDirection.Outgoing -> "-" diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt index e6cf464d0f..720cad8f50 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt @@ -126,7 +126,6 @@ internal class WalletViewModel @Inject constructor( var router: InnerWalletRouter by Delegates.notNull() private val selectedAppCurrencyFlow: StateFlow = createSelectedAppCurrencyFlow() - private var isBalanceHidden = true private val notificationsListFactory = WalletNotificationsListFactory( wasCardScannedUseCase = wasCardScannedUseCase, @@ -147,7 +146,6 @@ internal class WalletViewModel @Inject constructor( wallets[requireNotNull(uiState as? WalletState.ContentState).walletsListConfig.selectedWalletIndex] }, appCurrencyProvider = Provider(selectedAppCurrencyFlow::value), - isBalanceHiddenProvider = Provider { isBalanceHidden }, clickIntents = this, ) @@ -190,7 +188,6 @@ internal class WalletViewModel @Inject constructor( isBalanceHiddenUseCase() .flowWithLifecycle(owner.lifecycle) .onEach { hidden -> - isBalanceHidden = hidden WalletStateCache.updateAll { copySealed(isBalanceHidden = hidden) } uiState = stateFactory.getHiddenBalanceState(isBalanceHidden = hidden) } From e53a4bf8994b81ca52bd2a52b76ffdd82f12fcc7 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 9 Oct 2023 23:12:38 +0300 Subject: [PATCH 159/242] Updated on 2026-08-14 --- .../tokendetails/ui/components/TokenDetailsBalanceBlock.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenDetailsBalanceBlock.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenDetailsBalanceBlock.kt index 0fc3606d9b..19577b16b4 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenDetailsBalanceBlock.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenDetailsBalanceBlock.kt @@ -89,7 +89,7 @@ private fun FiatBalance( ) is TokenDetailsBalanceBlockState.Error -> Text( modifier = modifier, - text = BigDecimalFormatter.EMPTY_BALANCE_SIGN, + text = if (isBalanceHidden) STARS else BigDecimalFormatter.EMPTY_BALANCE_SIGN, style = TangemTheme.typography.h2, color = TangemTheme.colors.text.primary1, ) @@ -117,7 +117,7 @@ private fun CryptoBalance( ) is TokenDetailsBalanceBlockState.Error -> Text( modifier = modifier, - text = BigDecimalFormatter.EMPTY_BALANCE_SIGN, + text = if (isBalanceHidden) STARS else BigDecimalFormatter.EMPTY_BALANCE_SIGN, style = TangemTheme.typography.caption, color = TangemTheme.colors.text.tertiary, ) From 1f0ed3a95e059d16d60d9eedbec4299121f15449 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 10 Oct 2023 00:26:08 +0300 Subject: [PATCH 160/242] Updated on 2026-08-14 --- .../presentation/common/WalletPreviewData.kt | 2 +- .../common/component/TokenItem.kt | 2 +- .../organizetokens/OrganizeTokensScreen.kt | 4 +-- .../OrganizeTokensStateHolder.kt | 2 +- ...alletTxHistoryTransactionStateConverter.kt | 2 -- .../presentation/wallet/ui/WalletScreen.kt | 29 +++++++++++-------- .../wallet/ui/components/WalletsList.kt | 6 ++-- .../wallet/ui/components/common/WalletCard.kt | 8 ++--- .../multicurrency/MultiCurrencyContent.kt | 4 +-- .../multicurrency/MultiCurrencyContentItem.kt | 4 +-- 10 files changed, 33 insertions(+), 30 deletions(-) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/WalletPreviewData.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/WalletPreviewData.kt index 841863194b..e493ed29f4 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/WalletPreviewData.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/WalletPreviewData.kt @@ -275,7 +275,7 @@ internal object WalletPreviewData { onCancelClick = {}, ), scrollListToTop = consumedEvent(), - isBalanceHidden = true + isBalanceHidden = true, ) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/TokenItem.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/TokenItem.kt index ea5364d1a4..9e0ea7e0cd 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/TokenItem.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/TokenItem.kt @@ -33,9 +33,9 @@ private enum class LayoutId { @Composable internal fun TokenItem( state: TokenItemState, + isBalanceHidden: Boolean, modifier: Modifier = Modifier, reorderableTokenListState: ReorderableLazyListState? = null, - isBalanceHidden: Boolean ) { CustomContainer( state = state, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensScreen.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensScreen.kt index 6a0c471a92..3b1ec1dd46 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensScreen.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensScreen.kt @@ -83,8 +83,8 @@ private fun TokenList( listState: LazyListState, state: OrganizeTokensListState, dndConfig: OrganizeTokensState.DragAndDropConfig, - modifier: Modifier = Modifier, isBalanceHidden: Boolean, + modifier: Modifier = Modifier, ) { Box(modifier = modifier) { val onDragEnd: (Int, Int) -> Unit = remember { @@ -167,7 +167,7 @@ private fun LazyItemScope.DraggableItem( modifier = itemModifier, state = item.tokenItemState, reorderableTokenListState = reorderableState, - isBalanceHidden = isBalanceHidden + isBalanceHidden = isBalanceHidden, ) // Should be presented in the list but remain invisible is DraggableItem.Placeholder -> Box(modifier = Modifier.fillMaxWidth()) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensStateHolder.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensStateHolder.kt index 18a2625bab..e435b21b5e 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensStateHolder.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensStateHolder.kt @@ -109,7 +109,7 @@ internal class OrganizeTokensStateHolder( canDragItemOver = dragAndDropIntents::canDragItemOver, ), scrollListToTop = consumedEvent(), - isBalanceHidden = true + isBalanceHidden = true, ) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/txhistory/WalletTxHistoryTransactionStateConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/txhistory/WalletTxHistoryTransactionStateConverter.kt index 5c953517fe..ece331befc 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/txhistory/WalletTxHistoryTransactionStateConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/txhistory/WalletTxHistoryTransactionStateConverter.kt @@ -1,7 +1,5 @@ package com.tangem.feature.wallet.presentation.wallet.state.factory.txhistory -import com.tangem.common.Provider -import com.tangem.common.Strings import com.tangem.core.ui.components.transactions.state.TransactionState import com.tangem.core.ui.extensions.TextReference import com.tangem.domain.txhistory.models.TxHistoryItem diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt index fe7215f347..76dc0bac3d 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt @@ -3,6 +3,7 @@ package com.tangem.feature.wallet.presentation.wallet.ui import androidx.activity.compose.BackHandler import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material.ExperimentalMaterialApi @@ -153,18 +154,7 @@ private fun WalletContent( modifier = movableItemModifier, ) - if (state is WalletMultiCurrencyState) { - val contentTokenListState = state.tokensListState as? WalletTokensListState.ContentState - val organizeTokensButton = contentTokenListState?.organizeTokensButton - - if (organizeTokensButton is OrganizeTokensButtonState.Visible) { - organizeTokensButton( - modifier = itemModifier, - isEnabled = organizeTokensButton.isEnabled, - onClick = organizeTokensButton.onClick, - ) - } - } + organizeTokens(state = state, itemModifier = itemModifier) } } } @@ -179,6 +169,21 @@ private fun WalletContent( ) } +internal fun LazyListScope.organizeTokens(state: WalletState.ContentState, itemModifier: Modifier) { + if (state is WalletMultiCurrencyState) { + val contentTokenListState = state.tokensListState as? WalletTokensListState.ContentState + val organizeTokensButton = contentTokenListState?.organizeTokensButton + + if (organizeTokensButton is OrganizeTokensButtonState.Visible) { + organizeTokensButton( + modifier = itemModifier, + isEnabled = organizeTokensButton.isEnabled, + onClick = organizeTokensButton.onClick, + ) + } + } +} + @OptIn(ExperimentalMaterialApi::class) @Composable private fun UpdatableContainer( diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/WalletsList.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/WalletsList.kt index 431197b952..7caf718821 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/WalletsList.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/WalletsList.kt @@ -57,10 +57,10 @@ internal fun WalletsList(config: WalletsListConfig, lazyListState: LazyListState ) { state -> WalletCard( state = state, + isBalanceHidden = isBalanceHidden, modifier = Modifier .animateItemPlacement() .width(itemWidth), - isBalanceHidden = isBalanceHidden ) } } @@ -102,7 +102,7 @@ private fun Preview_WalletsList_LightTheme() { WalletsList( config = WalletPreviewData.walletListConfig, lazyListState = rememberLazyListState(), - isBalanceHidden = false + isBalanceHidden = false, ) } } @@ -114,7 +114,7 @@ private fun Preview_WalletsList_DarkTheme() { WalletsList( config = WalletPreviewData.walletListConfig, lazyListState = rememberLazyListState(), - isBalanceHidden = false + isBalanceHidden = false, ) } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletCard.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletCard.kt index def0b40b40..910308e35f 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletCard.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletCard.kt @@ -57,7 +57,7 @@ private const val HALF_OF_ITEM_WIDTH = 0.5 [REDACTED_AUTHOR] */ @Composable -internal fun WalletCard(state: WalletCardState, modifier: Modifier = Modifier, isBalanceHidden: Boolean) { +internal fun WalletCard(state: WalletCardState, isBalanceHidden: Boolean, modifier: Modifier = Modifier) { @Suppress("DestructuringDeclarationWithTooManyEntries") CardContainer( name = state.title, @@ -82,6 +82,7 @@ internal fun WalletCard(state: WalletCardState, modifier: Modifier = Modifier, i var balanceWidth by remember { mutableStateOf(value = Int.MIN_VALUE) } Balance( state = state, + isBalanceHidden = isBalanceHidden, modifier = Modifier .onSizeChanged { balanceWidth = it.width } .padding(vertical = TangemTheme.dimens.spacing8) @@ -90,7 +91,6 @@ internal fun WalletCard(state: WalletCardState, modifier: Modifier = Modifier, i top.linkTo(anchor = titleRef.bottom) bottom.linkTo(anchor = additionalTextRef.top) }, - isBalanceHidden = isBalanceHidden ) AdditionalInfo( @@ -275,7 +275,7 @@ private fun TitleText(text: String, modifier: Modifier = Modifier) { @OptIn(ExperimentalAnimationApi::class) @Composable -private fun Balance(state: WalletCardState, modifier: Modifier = Modifier, isBalanceHidden: Boolean) { +private fun Balance(state: WalletCardState, isBalanceHidden: Boolean, modifier: Modifier = Modifier) { AnimatedContent( targetState = state, label = "Update the balance", @@ -298,7 +298,7 @@ private fun Balance(state: WalletCardState, modifier: Modifier = Modifier, isBal ) } is WalletCardState.Error -> NonContentBalanceText( - text = if (isBalanceHidden) WalletCardState.HIDDEN_BALANCE_TEXT else WalletCardState.EMPTY_BALANCE_TEXT + text = if (isBalanceHidden) WalletCardState.HIDDEN_BALANCE_TEXT else WalletCardState.EMPTY_BALANCE_TEXT, ) is WalletCardState.Loading -> { RectangleShimmer(modifier = Modifier.nonContentBalanceSize(TangemTheme.dimens)) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/multicurrency/MultiCurrencyContent.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/multicurrency/MultiCurrencyContent.kt index fe9967104d..955e19c87d 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/multicurrency/MultiCurrencyContent.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/multicurrency/MultiCurrencyContent.kt @@ -38,8 +38,8 @@ internal fun LazyListScope.tokensListItems( when (state) { is WalletTokensListState.ContentState -> contentItems( items = state.items, + isBalanceHidden = isBalanceHidden, modifier = modifier, - isBalanceHidden = isBalanceHidden ) WalletTokensListState.Empty -> nonContentItem(modifier = modifier) } @@ -57,11 +57,11 @@ private fun LazyListScope.contentItems( itemContent = { index, item -> MultiCurrencyContentItem( state = item, + isBalanceHidden = isBalanceHidden, modifier = modifier.roundedShapeItemDecoration( currentIndex = index, lastIndex = items.lastIndex, ), - isBalanceHidden = isBalanceHidden ) }, ) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/multicurrency/MultiCurrencyContentItem.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/multicurrency/MultiCurrencyContentItem.kt index e923d38147..9678a22d13 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/multicurrency/MultiCurrencyContentItem.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/multicurrency/MultiCurrencyContentItem.kt @@ -18,15 +18,15 @@ import com.tangem.feature.wallet.presentation.wallet.state.components.WalletToke @Composable internal fun MultiCurrencyContentItem( state: WalletTokensListState.TokensListItemState, - modifier: Modifier = Modifier, isBalanceHidden: Boolean, + modifier: Modifier = Modifier, ) { when (state) { is WalletTokensListState.TokensListItemState.NetworkGroupTitle -> { NetworkGroupItem(networkName = state.name.resolveReference(), modifier = modifier) } is WalletTokensListState.TokensListItemState.Token -> { - TokenItem(state = state.state, modifier = modifier, isBalanceHidden = isBalanceHidden) + TokenItem(state = state.state, isBalanceHidden = isBalanceHidden, modifier = modifier) } } } \ No newline at end of file From 519f5332f1d1549cb25d6c1ba901f5e43f4819a8 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 10 Oct 2023 08:58:37 +0300 Subject: [PATCH 161/242] Updated on 2026-08-14 --- .../tap/di/domain/WalletsDomainModule.kt | 6 + .../middlewares/MultiWalletMiddleware.kt | 4 + core/datasource/build.gradle.kts | 1 + .../di/TxHistoryItemsStoreModule.kt | 21 +++ .../txhistory/DefaultTxHistoryItemsStore.kt | 42 ++++++ .../local/txhistory/TxHistoryItemsStore.kt | 22 +++ data/txhistory/build.gradle.kts | 5 +- .../data/txhistory/di/TxHistoryDataModule.kt | 10 +- .../repository/DefaultTxHistoryRepository.kt | 45 +++--- .../paging/TxHistoryPagingSource.kt | 88 ++++++++++-- domain/txhistory/build.gradle.kts | 14 +- .../repository/TxHistoryRepository.kt | 10 +- .../usecase/GetTxHistoryItemsCountUseCase.kt | 7 +- .../usecase/GetTxHistoryItemsUseCase.kt | 6 +- .../wallets/models/GetSelectedWalletError.kt | 8 -- .../wallets/models/GetUserWalletError.kt | 8 ++ .../usecase/GetSelectedWalletUseCase.kt | 12 +- .../wallets/usecase/GetUserWalletUseCase.kt | 30 ++++ .../navigation/TokenDetailsRouter.kt | 1 + .../viewmodels/TokenDetailsViewModel.kt | 130 +++++++++--------- .../router/DefaultWalletRouter.kt | 7 +- .../presentation/router/InnerWalletRouter.kt | 2 +- .../wallet/viewmodels/WalletViewModel.kt | 32 ++++- 23 files changed, 379 insertions(+), 132 deletions(-) create mode 100644 core/datasource/src/main/java/com/tangem/datasource/di/TxHistoryItemsStoreModule.kt create mode 100644 core/datasource/src/main/java/com/tangem/datasource/local/txhistory/DefaultTxHistoryItemsStore.kt create mode 100644 core/datasource/src/main/java/com/tangem/datasource/local/txhistory/TxHistoryItemsStore.kt delete mode 100644 domain/wallets/src/main/java/com/tangem/domain/wallets/models/GetSelectedWalletError.kt create mode 100644 domain/wallets/src/main/java/com/tangem/domain/wallets/models/GetUserWalletError.kt create mode 100644 domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetUserWalletUseCase.kt diff --git a/app/src/main/java/com/tangem/tap/di/domain/WalletsDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/WalletsDomainModule.kt index 8e3d763c56..2d12ae7b0b 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/WalletsDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/WalletsDomainModule.kt @@ -20,6 +20,12 @@ internal object WalletsDomainModule { return GetWalletsUseCase(walletsStateHolder = walletsStateHolder) } + @Provides + @ViewModelScoped + fun providesGetUserWalletUseCase(walletsStateHolder: WalletsStateHolder): GetUserWalletUseCase { + return GetUserWalletUseCase(walletsStateHolder = walletsStateHolder) + } + @Provides @ViewModelScoped fun providesGetSelectedWalletUseCase(walletsStateHolder: WalletsStateHolder): GetSelectedWalletUseCase { diff --git a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/MultiWalletMiddleware.kt b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/MultiWalletMiddleware.kt index eb198a07f9..74aaf39091 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/MultiWalletMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/MultiWalletMiddleware.kt @@ -39,9 +39,13 @@ class MultiWalletMiddleware { when (action) { is WalletAction.MultiWallet.SelectWallet -> { if (action.currency != null) { + val userWalletId = userWalletsListManager.selectedUserWalletSync?.walletId + val bundle = bundleOf( + TokenDetailsRouter.USER_WALLET_ID_KEY to userWalletId?.stringValue, TokenDetailsRouter.CRYPTO_CURRENCY_KEY to cryptoCurrencyConverter.convert(action.currency), ) + store.dispatch(NavigationAction.NavigateTo(screen = AppScreen.WalletDetails, bundle = bundle)) } } diff --git a/core/datasource/build.gradle.kts b/core/datasource/build.gradle.kts index 79b6816470..6d7e66c87e 100644 --- a/core/datasource/build.gradle.kts +++ b/core/datasource/build.gradle.kts @@ -15,6 +15,7 @@ dependencies { implementation(projects.domain.tokens.models) implementation(projects.domain.wallets.models) implementation(projects.domain.balanceHiding.models) + implementation(projects.domain.txhistory.models) /** Tangem libraries */ implementation(deps.tangem.blockchain) diff --git a/core/datasource/src/main/java/com/tangem/datasource/di/TxHistoryItemsStoreModule.kt b/core/datasource/src/main/java/com/tangem/datasource/di/TxHistoryItemsStoreModule.kt new file mode 100644 index 0000000000..9a936f57f0 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/di/TxHistoryItemsStoreModule.kt @@ -0,0 +1,21 @@ +package com.tangem.datasource.di + +import com.tangem.datasource.local.datastore.RuntimeDataStore +import com.tangem.datasource.local.txhistory.DefaultTxHistoryItemsStore +import com.tangem.datasource.local.txhistory.TxHistoryItemsStore +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent + +@Module +@InstallIn(SingletonComponent::class) +internal object TxHistoryItemsStoreModule { + + @Provides + fun provideTxHistoryItemsStore(): TxHistoryItemsStore { + return DefaultTxHistoryItemsStore( + dataStore = RuntimeDataStore(), + ) + } +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/txhistory/DefaultTxHistoryItemsStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/txhistory/DefaultTxHistoryItemsStore.kt new file mode 100644 index 0000000000..7cbeb3f317 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/local/txhistory/DefaultTxHistoryItemsStore.kt @@ -0,0 +1,42 @@ +package com.tangem.datasource.local.txhistory + +import com.tangem.datasource.local.datastore.core.StringKeyDataStore +import com.tangem.datasource.local.datastore.core.StringKeyDataStoreDecorator +import com.tangem.domain.txhistory.models.PaginationWrapper +import com.tangem.domain.txhistory.models.TxHistoryItem +import com.tangem.utils.extensions.addOrReplace + +internal class DefaultTxHistoryItemsStore( + dataStore: StringKeyDataStore>>, +) : TxHistoryItemsStore, + StringKeyDataStoreDecorator>>(dataStore) { + + override fun provideStringKey(key: TxHistoryItemsStore.Key): String = key.toString() + + override suspend fun getNextPageSyncOrNull(key: TxHistoryItemsStore.Key): Int? { + val storedValue = getSyncOrNull(key) ?: return null + val lastWrappedItems = storedValue.maxBy(PaginationWrapper<*>::page) + val lastPage = lastWrappedItems.page + + return if (lastPage <= lastWrappedItems.totalPages) { + lastPage + } else { + null + } + } + + override suspend fun getSyncOrNull(key: TxHistoryItemsStore.Key, page: Int): PaginationWrapper? { + val storedValue = getSyncOrNull(key) + + return storedValue?.firstOrNull { it.page == page } + } + + override suspend fun store(key: TxHistoryItemsStore.Key, value: PaginationWrapper) { + val oldValue = getSyncOrNull(key).orEmpty() + val newValue = oldValue.addOrReplace(value) { + it.page == value.page + } + + store(key, newValue) + } +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/txhistory/TxHistoryItemsStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/txhistory/TxHistoryItemsStore.kt new file mode 100644 index 0000000000..e2f288e235 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/local/txhistory/TxHistoryItemsStore.kt @@ -0,0 +1,22 @@ +package com.tangem.datasource.local.txhistory + +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.txhistory.models.PaginationWrapper +import com.tangem.domain.txhistory.models.TxHistoryItem +import com.tangem.domain.wallets.models.UserWalletId + +interface TxHistoryItemsStore { + + suspend fun getNextPageSyncOrNull(key: Key): Int? + + suspend fun getSyncOrNull(key: Key, page: Int): PaginationWrapper? + + suspend fun remove(key: Key) + + suspend fun store(key: Key, value: PaginationWrapper) + + data class Key( + val userWalletId: UserWalletId, + val currency: CryptoCurrency, + ) +} \ No newline at end of file diff --git a/data/txhistory/build.gradle.kts b/data/txhistory/build.gradle.kts index 7ffda888cc..d53b060232 100644 --- a/data/txhistory/build.gradle.kts +++ b/data/txhistory/build.gradle.kts @@ -10,6 +10,8 @@ android { } dependencies { + implementation(projects.data.common) + implementation(projects.core.utils) implementation(projects.core.datasource) implementation(projects.domain.legacy) @@ -20,7 +22,8 @@ dependencies { implementation(deps.kotlin.coroutines) implementation(deps.androidx.paging.runtime) - implementation(deps.arrow.core) + implementation(deps.timber) + implementation(deps.jodatime) implementation(deps.hilt.core) kapt(deps.hilt.kapt) diff --git a/data/txhistory/src/main/kotlin/com/tangem/data/txhistory/di/TxHistoryDataModule.kt b/data/txhistory/src/main/kotlin/com/tangem/data/txhistory/di/TxHistoryDataModule.kt index da8b08603c..27f3b8e7bb 100644 --- a/data/txhistory/src/main/kotlin/com/tangem/data/txhistory/di/TxHistoryDataModule.kt +++ b/data/txhistory/src/main/kotlin/com/tangem/data/txhistory/di/TxHistoryDataModule.kt @@ -1,6 +1,8 @@ package com.tangem.data.txhistory.di +import com.tangem.data.common.cache.CacheRegistry import com.tangem.data.txhistory.repository.DefaultTxHistoryRepository +import com.tangem.datasource.local.txhistory.TxHistoryItemsStore import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.domain.txhistory.repository.TxHistoryRepository import com.tangem.domain.walletmanager.WalletManagersFacade @@ -17,10 +19,14 @@ internal object TxHistoryDataModule { @Provides @Singleton fun provideTxHistoryRepository( + cacheRegistry: CacheRegistry, walletManagersFacade: WalletManagersFacade, userWalletsStore: UserWalletsStore, + txHistoryItemsStore: TxHistoryItemsStore, ): TxHistoryRepository = DefaultTxHistoryRepository( - walletManagersFacade = walletManagersFacade, - userWalletsStore = userWalletsStore, + cacheRegistry, + walletManagersFacade, + userWalletsStore, + txHistoryItemsStore, ) } \ No newline at end of file diff --git a/data/txhistory/src/main/kotlin/com/tangem/data/txhistory/repository/DefaultTxHistoryRepository.kt b/data/txhistory/src/main/kotlin/com/tangem/data/txhistory/repository/DefaultTxHistoryRepository.kt index 2f9fa852c0..bfa1cc57d1 100644 --- a/data/txhistory/src/main/kotlin/com/tangem/data/txhistory/repository/DefaultTxHistoryRepository.kt +++ b/data/txhistory/src/main/kotlin/com/tangem/data/txhistory/repository/DefaultTxHistoryRepository.kt @@ -3,7 +3,9 @@ package com.tangem.data.txhistory.repository import androidx.paging.Pager import androidx.paging.PagingConfig import androidx.paging.PagingData +import com.tangem.data.common.cache.CacheRegistry import com.tangem.data.txhistory.repository.paging.TxHistoryPagingSource +import com.tangem.datasource.local.txhistory.TxHistoryItemsStore import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.Network @@ -13,50 +15,57 @@ import com.tangem.domain.txhistory.models.TxHistoryStateError import com.tangem.domain.txhistory.repository.TxHistoryRepository import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.models.UserWallet +import com.tangem.domain.wallets.models.UserWalletId import kotlinx.coroutines.flow.Flow class DefaultTxHistoryRepository( + private val cacheRegistry: CacheRegistry, private val walletManagersFacade: WalletManagersFacade, private val userWalletsStore: UserWalletsStore, + private val txHistoryItemsStore: TxHistoryItemsStore, ) : TxHistoryRepository { - override suspend fun getTxHistoryItemsCount(network: Network): Int { - val userWallet = getUserWallet() + override suspend fun getTxHistoryItemsCount(userWalletId: UserWalletId, network: Network): Int { + val userWallet = getUserWallet(userWalletId) val state = walletManagersFacade.getTxHistoryState( userWalletId = userWallet.walletId, network = network, ) return when (state) { is TxHistoryState.Failed.FetchError -> throw TxHistoryStateError.DataError(state.exception) - TxHistoryState.NotImplemented -> throw TxHistoryStateError.TxHistoryNotImplemented - TxHistoryState.Success.Empty -> throw TxHistoryStateError.EmptyTxHistories + is TxHistoryState.NotImplemented -> throw TxHistoryStateError.TxHistoryNotImplemented + is TxHistoryState.Success.Empty -> throw TxHistoryStateError.EmptyTxHistories is TxHistoryState.Success.HasTransactions -> state.txCount } } - override fun getTxHistoryItems(currency: CryptoCurrency, pageSize: Int): Flow> { - val userWallet = getUserWallet() - return Pager( + override fun getTxHistoryItems( + userWalletId: UserWalletId, + currency: CryptoCurrency, + pageSize: Int, + refresh: Boolean, + ): Flow> { + val pager = Pager( config = PagingConfig( pageSize = pageSize, initialLoadSize = pageSize, ), pagingSourceFactory = { TxHistoryPagingSource( - loadPage = { page: Int, pageSize: Int -> - walletManagersFacade.getTxHistoryItems( - userWalletId = userWallet.walletId, - currency = currency, - page = page, - pageSize = pageSize, - ) - }, + sourceParams = TxHistoryPagingSource.Params(userWalletId, currency, pageSize, refresh), + txHistoryItemsStore = txHistoryItemsStore, + walletManagersFacade = walletManagersFacade, + cacheRegistry = cacheRegistry, ) }, - ).flow + ) + + return pager.flow } - private fun getUserWallet(): UserWallet = requireNotNull(userWalletsStore.selectedUserWalletOrNull) { - "Selected wallet must not be null" + private suspend fun getUserWallet(userWalletId: UserWalletId): UserWallet { + return requireNotNull(userWalletsStore.getSyncOrNull(userWalletId)) { + "Unable to find user wallet with provided ID: $userWalletId" + } } } \ No newline at end of file diff --git a/data/txhistory/src/main/kotlin/com/tangem/data/txhistory/repository/paging/TxHistoryPagingSource.kt b/data/txhistory/src/main/kotlin/com/tangem/data/txhistory/repository/paging/TxHistoryPagingSource.kt index a3ebb06194..f195b173f7 100644 --- a/data/txhistory/src/main/kotlin/com/tangem/data/txhistory/repository/paging/TxHistoryPagingSource.kt +++ b/data/txhistory/src/main/kotlin/com/tangem/data/txhistory/repository/paging/TxHistoryPagingSource.kt @@ -2,34 +2,96 @@ package com.tangem.data.txhistory.repository.paging import androidx.paging.PagingSource import androidx.paging.PagingState +import com.tangem.data.common.cache.CacheRegistry +import com.tangem.datasource.local.txhistory.TxHistoryItemsStore +import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.txhistory.models.PaginationWrapper import com.tangem.domain.txhistory.models.TxHistoryItem - -private const val INITIAL_PAGE = 1 +import com.tangem.domain.walletmanager.WalletManagersFacade +import com.tangem.domain.wallets.models.UserWalletId +import timber.log.Timber internal class TxHistoryPagingSource( - private val loadPage: suspend (page: Int, pageSize: Int) -> PaginationWrapper, + private val sourceParams: Params, + private val txHistoryItemsStore: TxHistoryItemsStore, + private val walletManagersFacade: WalletManagersFacade, + private val cacheRegistry: CacheRegistry, ) : PagingSource() { + private val storeKey = TxHistoryItemsStore.Key(sourceParams.userWalletId, sourceParams.currency) + override fun getRefreshKey(state: PagingState): Int? { return state.anchorPosition?.let { anchorPosition -> - state.closestPageToPosition(anchorPosition)?.prevKey?.plus(other = 1) - ?: state.closestPageToPosition(anchorPosition)?.nextKey?.minus(other = 1) + val anchorPage = state.closestPageToPosition(anchorPosition) + anchorPage?.prevKey?.inc() ?: anchorPage?.nextKey?.dec() } } override suspend fun load(params: LoadParams): LoadResult { - val currentPage = params.key ?: INITIAL_PAGE - return try { - val result = loadPage(currentPage, params.loadSize) + val pageToLoad = params.key ?: INITIAL_PAGE - LoadResult.Page( - data = result.items, - prevKey = if (currentPage > INITIAL_PAGE) currentPage.minus(1) else null, - nextKey = if (result.page < result.totalPages) currentPage.plus(1) else null, + return try { + val wrappedItems = loadItems( + pageToLoad = pageToLoad, + pageSize = sourceParams.pageSize, + refresh = sourceParams.refresh && params is LoadParams.Refresh, ) - } catch (e: Exception) { + + val items = wrappedItems.items + val prevPage = when { + items.isEmpty() -> null + pageToLoad > INITIAL_PAGE -> pageToLoad.dec() + else -> null + } + val nextPage = when { + items.isEmpty() -> INITIAL_PAGE + pageToLoad < wrappedItems.totalPages -> pageToLoad.inc() + else -> null + } + + LoadResult.Page(items, prevKey = prevPage, nextKey = nextPage) + } catch (e: Throwable) { + Timber.e(e, "Unable to load the transaction history for the requested page: $pageToLoad") + LoadResult.Error(e) } } + + private suspend fun loadItems(pageToLoad: Int, pageSize: Int, refresh: Boolean): PaginationWrapper { + cacheRegistry.invokeOnExpire( + key = getTxHistoryPageKey(pageToLoad), + skipCache = refresh, + block = { fetch(pageToLoad, pageSize) }, + ) + + return requireNotNull(txHistoryItemsStore.getSyncOrNull(storeKey, pageToLoad)) { + "The transaction history page #$pageToLoad could not be retrieved" + } + } + + private suspend fun fetch(pageToLoad: Int, pageSize: Int) { + val wrappedItems = walletManagersFacade.getTxHistoryItems( + userWalletId = sourceParams.userWalletId, + currency = sourceParams.currency, + page = pageToLoad, + pageSize = pageSize, + ) + + txHistoryItemsStore.store(storeKey, wrappedItems) + } + + private fun getTxHistoryPageKey(page: Int): String { + return "tx_history_page_${sourceParams.currency}_${sourceParams.userWalletId}_$page" + } + + data class Params( + val userWalletId: UserWalletId, + val currency: CryptoCurrency, + val pageSize: Int, + val refresh: Boolean, + ) + + private companion object { + private const val INITIAL_PAGE = 1 + } } \ No newline at end of file diff --git a/domain/txhistory/build.gradle.kts b/domain/txhistory/build.gradle.kts index 2ba0547f59..892e55e639 100644 --- a/domain/txhistory/build.gradle.kts +++ b/domain/txhistory/build.gradle.kts @@ -9,11 +9,15 @@ android { } dependencies { - implementation(deps.arrow.core) - implementation(deps.kotlin.coroutines) - implementation(deps.androidx.paging.runtime) - - implementation(projects.core.utils) + /** Project - Domain */ + implementation(projects.domain.core) implementation(projects.domain.tokens.models) implementation(projects.domain.txhistory.models) + implementation(projects.domain.wallets.models) + + /** Project - Other */ + implementation(projects.core.utils) + + /** Android - Other */ + implementation(deps.androidx.paging.runtime) } \ No newline at end of file diff --git a/domain/txhistory/src/main/kotlin/com/tangem/domain/txhistory/repository/TxHistoryRepository.kt b/domain/txhistory/src/main/kotlin/com/tangem/domain/txhistory/repository/TxHistoryRepository.kt index 0c778f81ae..a71fe087c1 100644 --- a/domain/txhistory/src/main/kotlin/com/tangem/domain/txhistory/repository/TxHistoryRepository.kt +++ b/domain/txhistory/src/main/kotlin/com/tangem/domain/txhistory/repository/TxHistoryRepository.kt @@ -6,13 +6,19 @@ import com.tangem.domain.tokens.model.Network import com.tangem.domain.txhistory.models.TxHistoryItem import com.tangem.domain.txhistory.models.TxHistoryListError import com.tangem.domain.txhistory.models.TxHistoryStateError +import com.tangem.domain.wallets.models.UserWalletId import kotlinx.coroutines.flow.Flow interface TxHistoryRepository { @Throws(TxHistoryStateError::class) - suspend fun getTxHistoryItemsCount(network: Network): Int + suspend fun getTxHistoryItemsCount(userWalletId: UserWalletId, network: Network): Int @Throws(TxHistoryListError::class) - fun getTxHistoryItems(currency: CryptoCurrency, pageSize: Int): Flow> + fun getTxHistoryItems( + userWalletId: UserWalletId, + currency: CryptoCurrency, + pageSize: Int, + refresh: Boolean, + ): Flow> } \ No newline at end of file diff --git a/domain/txhistory/src/main/kotlin/com/tangem/domain/txhistory/usecase/GetTxHistoryItemsCountUseCase.kt b/domain/txhistory/src/main/kotlin/com/tangem/domain/txhistory/usecase/GetTxHistoryItemsCountUseCase.kt index 6ca59cb013..ab14187a02 100644 --- a/domain/txhistory/src/main/kotlin/com/tangem/domain/txhistory/usecase/GetTxHistoryItemsCountUseCase.kt +++ b/domain/txhistory/src/main/kotlin/com/tangem/domain/txhistory/usecase/GetTxHistoryItemsCountUseCase.kt @@ -6,14 +6,15 @@ import arrow.core.raise.either import com.tangem.domain.tokens.model.Network import com.tangem.domain.txhistory.models.TxHistoryStateError import com.tangem.domain.txhistory.repository.TxHistoryRepository +import com.tangem.domain.wallets.models.UserWalletId +// TODO: Add tests class GetTxHistoryItemsCountUseCase(private val repository: TxHistoryRepository) { - // FIXME: Provide UserWalletId - suspend operator fun invoke(network: Network): Either { + suspend operator fun invoke(userWalletId: UserWalletId, network: Network): Either { return either { catch( - block = { repository.getTxHistoryItemsCount(network) }, + block = { repository.getTxHistoryItemsCount(userWalletId, network) }, catch = { throwable -> raise( when (throwable) { diff --git a/domain/txhistory/src/main/kotlin/com/tangem/domain/txhistory/usecase/GetTxHistoryItemsUseCase.kt b/domain/txhistory/src/main/kotlin/com/tangem/domain/txhistory/usecase/GetTxHistoryItemsUseCase.kt index 0555f17187..e5570e8077 100644 --- a/domain/txhistory/src/main/kotlin/com/tangem/domain/txhistory/usecase/GetTxHistoryItemsUseCase.kt +++ b/domain/txhistory/src/main/kotlin/com/tangem/domain/txhistory/usecase/GetTxHistoryItemsUseCase.kt @@ -7,21 +7,25 @@ import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.txhistory.models.TxHistoryItem import com.tangem.domain.txhistory.models.TxHistoryListError import com.tangem.domain.txhistory.repository.TxHistoryRepository +import com.tangem.domain.wallets.models.UserWalletId import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.catch private const val DEFAULT_PAGE_SIZE = 50 +// TODO: Add tests class GetTxHistoryItemsUseCase(private val repository: TxHistoryRepository) { // FIXME: Provide UserWalletId operator fun invoke( + userWalletId: UserWalletId, currency: CryptoCurrency, pageSize: Int = DEFAULT_PAGE_SIZE, + refresh: Boolean = false, ): Either>> { return either { repository - .getTxHistoryItems(currency = currency, pageSize = pageSize) + .getTxHistoryItems(userWalletId, currency, pageSize, refresh) .catch { raise(TxHistoryListError.DataError(it)) } } } diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/models/GetSelectedWalletError.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/models/GetSelectedWalletError.kt deleted file mode 100644 index ad0867fa6e..0000000000 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/models/GetSelectedWalletError.kt +++ /dev/null @@ -1,8 +0,0 @@ -package com.tangem.domain.wallets.models - -sealed interface GetSelectedWalletError { - - object DataError : GetSelectedWalletError - - object NoUserWalletSelected : GetSelectedWalletError -} \ No newline at end of file diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/models/GetUserWalletError.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/models/GetUserWalletError.kt new file mode 100644 index 0000000000..50d1bea262 --- /dev/null +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/models/GetUserWalletError.kt @@ -0,0 +1,8 @@ +package com.tangem.domain.wallets.models + +sealed class GetUserWalletError { + + data class DataError(val cause: Throwable) : GetUserWalletError() + + object UserWalletNotFound : GetUserWalletError() +} \ No newline at end of file diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetSelectedWalletUseCase.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetSelectedWalletUseCase.kt index b56de58375..a29aad4ea4 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetSelectedWalletUseCase.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetSelectedWalletUseCase.kt @@ -4,7 +4,7 @@ import arrow.core.Either import arrow.core.raise.either import arrow.core.raise.ensureNotNull import com.tangem.domain.wallets.legacy.WalletsStateHolder -import com.tangem.domain.wallets.models.GetSelectedWalletError +import com.tangem.domain.wallets.models.GetUserWalletError import com.tangem.domain.wallets.models.UserWallet /** @@ -17,16 +17,20 @@ import com.tangem.domain.wallets.models.UserWallet */ class GetSelectedWalletUseCase(private val walletsStateHolder: WalletsStateHolder) { - operator fun invoke(): Either { + operator fun invoke(): Either { return either { val userWalletsListManager = ensureNotNull( value = walletsStateHolder.userWalletsListManager, - raise = { GetSelectedWalletError.DataError }, + raise = { + val error = IllegalStateException("User wallets list manager not initialized") + + GetUserWalletError.DataError(error) + }, ) ensureNotNull( value = userWalletsListManager.selectedUserWalletSync, - raise = { GetSelectedWalletError.NoUserWalletSelected }, + raise = { GetUserWalletError.UserWalletNotFound }, ) } } diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetUserWalletUseCase.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetUserWalletUseCase.kt new file mode 100644 index 0000000000..9d1fe85f5e --- /dev/null +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetUserWalletUseCase.kt @@ -0,0 +1,30 @@ +package com.tangem.domain.wallets.usecase + +import arrow.core.Either +import arrow.core.raise.either +import arrow.core.raise.ensureNotNull +import com.tangem.domain.wallets.legacy.WalletsStateHolder +import com.tangem.domain.wallets.models.GetUserWalletError +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.domain.wallets.models.UserWalletId +import kotlinx.coroutines.flow.firstOrNull + +class GetUserWalletUseCase(private val walletsStateHolder: WalletsStateHolder) { + + suspend operator fun invoke(userWalletId: UserWalletId): Either = either { + val userWalletsListManager = ensureNotNull( + value = walletsStateHolder.userWalletsListManager, + raise = { + val error = IllegalStateException("User wallets list manager not initialized") + + GetUserWalletError.DataError(error) + }, + ) + + val userWallets = userWalletsListManager.userWallets.firstOrNull().orEmpty() + + ensureNotNull(userWallets.firstOrNull { it.walletId == userWalletId }) { + raise(GetUserWalletError.UserWalletNotFound) + } + } +} \ No newline at end of file diff --git a/features/tokendetails/api/src/main/kotlin/com/tangem/features/tokendetails/navigation/TokenDetailsRouter.kt b/features/tokendetails/api/src/main/kotlin/com/tangem/features/tokendetails/navigation/TokenDetailsRouter.kt index da5bc7571f..a0f630c125 100644 --- a/features/tokendetails/api/src/main/kotlin/com/tangem/features/tokendetails/navigation/TokenDetailsRouter.kt +++ b/features/tokendetails/api/src/main/kotlin/com/tangem/features/tokendetails/navigation/TokenDetailsRouter.kt @@ -7,6 +7,7 @@ interface TokenDetailsRouter { fun getEntryFragment(): Fragment companion object { + const val USER_WALLET_ID_KEY = "token_details_user_wallet_id" const val CRYPTO_CURRENCY_KEY = "token_details_crypto_currency" } } \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt index bbf6300015..1de3829916 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt @@ -23,10 +23,9 @@ import com.tangem.domain.tokens.models.analytics.TokenReceiveAnalyticsEvent import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsCountUseCase import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsUseCase import com.tangem.domain.walletmanager.WalletManagersFacade -import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWalletId import com.tangem.domain.wallets.usecase.GetExploreUrlUseCase -import com.tangem.domain.wallets.usecase.GetSelectedWalletUseCase +import com.tangem.domain.wallets.usecase.GetUserWalletUseCase import com.tangem.feature.tokendetails.presentation.router.InnerTokenDetailsRouter import com.tangem.feature.tokendetails.presentation.tokendetails.analytics.TokenScreenEvent import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState @@ -48,7 +47,7 @@ import kotlin.properties.Delegates @HiltViewModel internal class TokenDetailsViewModel @Inject constructor( private val dispatchers: CoroutineDispatcherProvider, - private val getSelectedWalletUseCase: GetSelectedWalletUseCase, + private val getUserWalletUseCase: GetUserWalletUseCase, private val getCurrencyStatusUpdatesUseCase: GetCurrencyStatusUpdatesUseCase, private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, private val fetchCurrencyStatusUseCase: FetchCurrencyStatusUseCase, @@ -67,15 +66,18 @@ internal class TokenDetailsViewModel @Inject constructor( savedStateHandle: SavedStateHandle, ) : ViewModel(), DefaultLifecycleObserver, TokenDetailsClickIntents { + private val userWalletId: UserWalletId = savedStateHandle.get(TokenDetailsRouter.USER_WALLET_ID_KEY) + ?.let { stringValue -> UserWalletId(stringValue) } + ?: error("This screen can't open without `UserWalletId`") + private val cryptoCurrency: CryptoCurrency = savedStateHandle[TokenDetailsRouter.CRYPTO_CURRENCY_KEY] - ?: error("This screen can't open without CryptoCurrency") + ?: error("This screen can't open without `CryptoCurrency`") var router by Delegates.notNull() private val marketPriceJobHolder = JobHolder() private val refreshStateJobHolder = JobHolder() private var cryptoCurrencyStatus: CryptoCurrencyStatus? = null - private var wallet by Delegates.notNull() private val selectedAppCurrencyFlow: StateFlow = createSelectedAppCurrencyFlow() @@ -91,23 +93,14 @@ internal class TokenDetailsViewModel @Inject constructor( private set override fun onCreate(owner: LifecycleOwner) { - getWallet() - updateContent(selectedWallet = wallet) + updateContent() handleBalanceHiding(owner) } - private fun getWallet() { - getSelectedWalletUseCase() - .fold( - ifLeft = { error("Can not get selected wallet $it") }, - ifRight = { wallet = it }, - ) - } - - private fun updateContent(selectedWallet: UserWallet) { - updateMarketPrice(selectedWallet = selectedWallet) + private fun updateContent() { + updateMarketPrice() updateTxHistory(refresh = false, showItemsLoading = true) - updateWarnings(selectedWallet = selectedWallet) + updateWarnings() } private fun handleBalanceHiding(owner: LifecycleOwner) { @@ -135,10 +128,10 @@ internal class TokenDetailsViewModel @Inject constructor( .launchIn(viewModelScope) } - private fun updateWarnings(selectedWallet: UserWallet) { + private fun updateWarnings() { viewModelScope.launch(dispatchers.io) { getCurrencyWarningsUseCase.invoke( - userWalletId = selectedWallet.walletId, + userWalletId = userWalletId, currency = cryptoCurrency, derivationPath = cryptoCurrency.network.derivationPath, ) @@ -148,9 +141,9 @@ internal class TokenDetailsViewModel @Inject constructor( } } - private fun updateMarketPrice(selectedWallet: UserWallet) { + private fun updateMarketPrice() { getCurrencyStatusUpdatesUseCase( - userWalletId = selectedWallet.walletId, + userWalletId = userWalletId, currencyId = cryptoCurrency.id, derivationPath = cryptoCurrency.network.derivationPath, ) @@ -159,7 +152,7 @@ internal class TokenDetailsViewModel @Inject constructor( uiState = stateFactory.getCurrencyLoadedBalanceState(either) either.onRight { status -> cryptoCurrencyStatus = status - updateButtons(userWalletId = selectedWallet.walletId, currencyStatus = status) + updateButtons(userWalletId = userWalletId, currencyStatus = status) } } .flowOn(dispatchers.io) @@ -175,6 +168,7 @@ internal class TokenDetailsViewModel @Inject constructor( private fun updateTxHistory(refresh: Boolean, showItemsLoading: Boolean) { viewModelScope.launch(dispatchers.io) { val txHistoryItemsCountEither = txHistoryItemsCountUseCase( + userWalletId = userWalletId, network = cryptoCurrency.network, ) @@ -184,9 +178,13 @@ internal class TokenDetailsViewModel @Inject constructor( } txHistoryItemsCountEither.onRight { - val either = txHistoryItemsUseCase(currency = cryptoCurrency) - .map { it.cachedIn(viewModelScope) } - uiState = stateFactory.getLoadedTxHistoryState(txHistoryEither = either) + val maybeTxHistory = txHistoryItemsUseCase( + userWalletId = userWalletId, + currency = cryptoCurrency, + refresh = refresh, + ).map { it.cachedIn(viewModelScope) } + + uiState = stateFactory.getLoadedTxHistoryState(maybeTxHistory) } } } @@ -211,13 +209,15 @@ internal class TokenDetailsViewModel @Inject constructor( analyticsEventsHandler.send(TokenScreenEvent.ButtonBuy(cryptoCurrency.symbol)) val status = cryptoCurrencyStatus ?: return - reduxStateHolder.dispatch( - TradeCryptoAction.New.Buy( - userWallet = wallet, - cryptoCurrencyStatus = status, - appCurrencyCode = selectedAppCurrencyFlow.value.code, - ), - ) + viewModelScope.launch(dispatchers.io) { + reduxStateHolder.dispatch( + TradeCryptoAction.New.Buy( + userWallet = getUserWalletUseCase(userWalletId).getOrElse { return@launch }, + cryptoCurrencyStatus = status, + appCurrencyCode = selectedAppCurrencyFlow.value.code, + ), + ) + } } override fun onReloadClick() { @@ -231,38 +231,38 @@ internal class TokenDetailsViewModel @Inject constructor( val cryptoCurrencyStatus = cryptoCurrencyStatus ?: return - when (cryptoCurrencyStatus.currency) { - is CryptoCurrency.Coin -> { - reduxStateHolder.dispatch( - action = TradeCryptoAction.New.SendCoin( - userWallet = wallet, - coinStatus = cryptoCurrencyStatus, - ), - ) + viewModelScope.launch(dispatchers.io) { + when (cryptoCurrencyStatus.currency) { + is CryptoCurrency.Coin -> { + reduxStateHolder.dispatch( + action = TradeCryptoAction.New.SendCoin( + userWallet = getUserWalletUseCase(userWalletId).getOrElse { return@launch }, + coinStatus = cryptoCurrencyStatus, + ), + ) + } + is CryptoCurrency.Token -> sendToken(status = cryptoCurrencyStatus) } - is CryptoCurrency.Token -> sendToken(status = cryptoCurrencyStatus) } } private fun sendToken(status: CryptoCurrencyStatus) { viewModelScope.launch(dispatchers.io) { - getNetworkCoinStatusUseCase( - userWalletId = wallet.walletId, + val maybeCoinStatus = getNetworkCoinStatusUseCase( + userWalletId = userWalletId, networkId = status.currency.network.id, derivationPath = status.currency.network.derivationPath, - ) - .take(count = 1) - .collectLatest { - it.onRight { coinStatus -> - reduxStateHolder.dispatch( - action = TradeCryptoAction.New.SendToken( - userWallet = wallet, - tokenStatus = status, - coinFiatRate = coinStatus.value.fiatRate, - ), - ) - } - } + ).firstOrNull() + + maybeCoinStatus?.onRight { coinStatus -> + reduxStateHolder.dispatch( + action = TradeCryptoAction.New.SendToken( + userWallet = getUserWalletUseCase(userWalletId).getOrElse { return@launch }, + tokenStatus = status, + coinFiatRate = coinStatus.value.fiatRate, + ), + ) + } } } @@ -271,7 +271,7 @@ internal class TokenDetailsViewModel @Inject constructor( viewModelScope.launch(dispatchers.io) { val addresses = walletManagersFacade.getAddress( - userWalletId = wallet.walletId, + userWalletId = userWalletId, network = cryptoCurrency.network, ) @@ -314,7 +314,7 @@ internal class TokenDetailsViewModel @Inject constructor( analyticsEventsHandler.send(TokenScreenEvent.ButtonRemoveToken(cryptoCurrency.symbol)) viewModelScope.launch { - val hasLinkedTokens = removeCurrencyUseCase.hasLinkedTokens(wallet.walletId, cryptoCurrency) + val hasLinkedTokens = removeCurrencyUseCase.hasLinkedTokens(userWalletId, cryptoCurrency) uiState = if (hasLinkedTokens) { stateFactory.getStateWithLinkedTokensDialog(cryptoCurrency) } else { @@ -325,7 +325,7 @@ internal class TokenDetailsViewModel @Inject constructor( override fun onHideConfirmed() { viewModelScope.launch { - removeCurrencyUseCase.invoke(wallet.walletId, cryptoCurrency) + removeCurrencyUseCase.invoke(userWalletId, cryptoCurrency) .onLeft { Timber.e(it) } .onRight { router.popBackStack() } } @@ -335,7 +335,7 @@ internal class TokenDetailsViewModel @Inject constructor( analyticsEventsHandler.send(TokenScreenEvent.ButtonExplore(cryptoCurrency.symbol)) viewModelScope.launch(dispatchers.io) { val addresses = walletManagersFacade.getAddress( - userWalletId = wallet.walletId, + userWalletId = userWalletId, network = cryptoCurrency.network, ) @@ -357,7 +357,7 @@ internal class TokenDetailsViewModel @Inject constructor( viewModelScope.launch { router.openUrl( url = getExploreUrlUseCase( - userWalletId = wallet.walletId, + userWalletId = userWalletId, network = cryptoCurrency.network, addressType = addressType, ), @@ -373,8 +373,8 @@ internal class TokenDetailsViewModel @Inject constructor( viewModelScope.launch(dispatchers.io) { listOf( async { - fetchCurrencyStatusUseCase.invoke( - userWalletId = wallet.walletId, + fetchCurrencyStatusUseCase( + userWalletId = userWalletId, id = cryptoCurrency.id, derivationPath = cryptoCurrency.network.derivationPath, refresh = true, @@ -386,7 +386,7 @@ internal class TokenDetailsViewModel @Inject constructor( showItemsLoading = uiState.txHistoryState !is TxHistoryState.Content, ) }, - async { updateWarnings(wallet) }, + async { updateWarnings() }, ).awaitAll() uiState = stateFactory.getRefreshedState() }.saveIn(refreshStateJobHolder) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/DefaultWalletRouter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/DefaultWalletRouter.kt index 2c7205d680..c481e57703 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/DefaultWalletRouter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/DefaultWalletRouter.kt @@ -107,11 +107,14 @@ internal class DefaultWalletRouter(private val reduxNavController: ReduxNavContr reduxNavController.navigate(action = NavigationAction.OpenUrl(url)) } - override fun openTokenDetails(currency: CryptoCurrency) { + override fun openTokenDetails(userWalletId: UserWalletId, currency: CryptoCurrency) { reduxNavController.navigate( action = NavigationAction.NavigateTo( screen = AppScreen.WalletDetails, - bundle = bundleOf(TokenDetailsRouter.CRYPTO_CURRENCY_KEY to currency), + bundle = bundleOf( + TokenDetailsRouter.USER_WALLET_ID_KEY to userWalletId.stringValue, + TokenDetailsRouter.CRYPTO_CURRENCY_KEY to currency, + ), ), ) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/InnerWalletRouter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/InnerWalletRouter.kt index e48e601399..07a2e8a80b 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/InnerWalletRouter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/InnerWalletRouter.kt @@ -44,7 +44,7 @@ internal interface InnerWalletRouter : WalletRouter { fun openTxHistoryWebsite(url: String) /** Open token details screen */ - fun openTokenDetails(currency: CryptoCurrency) + fun openTokenDetails(userWalletId: UserWalletId, currency: CryptoCurrency) /** Open stories screen */ fun openStoriesScreen() diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt index 9cb22da162..5b6e0fd44e 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt @@ -726,7 +726,7 @@ internal class WalletViewModel @Inject constructor( override fun onTokenItemClick(currency: CryptoCurrency) { analyticsEventsHandler.send(PortfolioEvent.TokenTapped) - router.openTokenDetails(currency = currency) + router.openTokenDetails(getSelectedWallet().walletId, currency) } override fun onTokenItemLongClick(cryptoCurrencyStatus: CryptoCurrencyStatus) { @@ -963,7 +963,7 @@ internal class WalletViewModel @Inject constructor( private fun getSingleCurrencyContent(index: Int) { val wallet = getWallet(index) - getPrimaryCurrencyStatusUpdatesUseCase(userWalletId = wallet.walletId) + getPrimaryCurrencyStatusUpdatesUseCase(wallet.walletId) .distinctUntilChanged() .onEach { maybeCryptoCurrencyStatus -> uiState = stateFactory.getSingleCurrencyLoadedBalanceState(maybeCryptoCurrencyStatus) @@ -976,8 +976,8 @@ internal class WalletViewModel @Inject constructor( } updateNotifications(index) - updateButtons(userWalletId = wallet.walletId, currencyStatus = status) - updateTxHistory(status.currency) + updateButtons(wallet.walletId, status) + updateTxHistory(wallet.walletId, status.currency, refresh = false) } } .flowOn(dispatchers.io) @@ -985,9 +985,12 @@ internal class WalletViewModel @Inject constructor( .saveIn(marketPriceJobHolder) } - private fun updateTxHistory(currency: CryptoCurrency) { + private fun updateTxHistory(userWalletId: UserWalletId, currency: CryptoCurrency, refresh: Boolean) { viewModelScope.launch(dispatchers.io) { - val txHistoryItemsCountEither = txHistoryItemsCountUseCase(currency.network) + val txHistoryItemsCountEither = txHistoryItemsCountUseCase( + userWalletId = userWalletId, + network = currency.network, + ) uiState = stateFactory.getLoadingTxHistoryState( itemsCountEither = txHistoryItemsCountEither, @@ -995,7 +998,11 @@ internal class WalletViewModel @Inject constructor( txHistoryItemsCountEither.onRight { uiState = stateFactory.getLoadedTxHistoryState( - txHistoryEither = txHistoryItemsUseCase(currency = currency).map { + txHistoryEither = txHistoryItemsUseCase( + userWalletId = userWalletId, + currency = currency, + refresh = refresh, + ).map { it.cachedIn(viewModelScope) }, ) @@ -1057,6 +1064,10 @@ internal class WalletViewModel @Inject constructor( uiState = stateFactory.getRefreshedState() uiState = result.fold(stateFactory::getStateByCurrencyStatusError) { uiState } + + singleWalletCryptoCurrencyStatus?.let { + updateTxHistory(wallet.walletId, it.currency, refresh = true) + } }.saveIn(refreshContentJobHolder) } @@ -1091,5 +1102,12 @@ internal class WalletViewModel @Inject constructor( ) } + private fun getSelectedWallet(): UserWallet { + val state = uiState as? WalletState.ContentState + ?: error("Unable to get selected user wallet") + + return getWallet(state.walletsListConfig.selectedWalletIndex) + } + private fun getCardTypeResolver(index: Int): CardTypesResolver = getWallet(index).scanResponse.cardTypesResolver } \ No newline at end of file From 8962f8d333186237a169c8a4652c7464052c4cd9 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 10 Oct 2023 12:07:39 +0500 Subject: [PATCH 162/242] Updated on 2026-08-14 --- .../ui/components/AddCustomTokenForm.kt | 4 +- .../details/ui/details/DetailsScreen.kt | 2 +- .../ui/components/SaveWalletScreenContent.kt | 2 +- .../wallet/ui/view/TotalBalanceCard.kt | 2 +- .../ui/components/WalletItem.kt | 4 +- .../tangem/core/ui/components/TextFields.kt | 5 +- .../ui/components/appbar/AppBarWithSearch.kt | 2 +- .../tokenreceive/TokenReceiveBottomSheet.kt | 2 +- .../components/notifications/Notification.kt | 2 +- .../ui/components/transactions/Transaction.kt | 4 +- .../com/tangem/core/ui/res/TangemTheme.kt | 9 ++-- .../tangem/core/ui/res/TangemTypography.kt | 50 ++++++++++++++----- .../presentation/ui/GetBonusView.kt | 5 +- .../ui/Learn2earnStoriesScreen.kt | 3 +- .../wallet2/ui/ImportSeedPhraseScreen.kt | 2 +- .../feature/referral/ui/AgreementText.kt | 2 +- .../feature/swap/ui/SwapSelectTokenScreen.kt | 6 +-- .../TokenDetailsLoadedBalanceConverter.kt | 26 +++++----- .../ui/components/TokenDetailsBalanceBlock.kt | 4 +- .../ui/components/TokenInfoBlock.kt | 8 +-- .../component/token/NonFiatContentBlock.kt | 3 +- .../component/token/TokenCryptoAmount.kt | 3 +- .../common/component/token/TokenFiatAmount.kt | 3 +- .../component/token/TokenPriceChange.kt | 3 +- .../common/component/token/TokenTitle.kt | 3 +- .../wallet/ui/components/common/WalletCard.kt | 2 +- .../multicurrency/MultiCurrencyContent.kt | 2 +- 27 files changed, 87 insertions(+), 76 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/ui/components/AddCustomTokenForm.kt b/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/ui/components/AddCustomTokenForm.kt index 0090ef8223..fb6c260728 100644 --- a/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/ui/components/AddCustomTokenForm.kt +++ b/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/ui/components/AddCustomTokenForm.kt @@ -94,7 +94,7 @@ private fun TextField(model: AddCustomTokenInputField, isError: Boolean) { label = { Text( text = model.label.resolveReference(), - style = TangemTheme.typography.caption, + style = TangemTheme.typography.caption2, color = TangemTextFieldsDefault.defaultTextFieldColors.labelColor( enabled = isEnabled, error = isError, @@ -178,7 +178,7 @@ private fun SelectorField(model: AddCustomTokenSelectorField) { text = subtitle, color = TangemTheme.colors.text.secondary, maxLines = 1, - style = TangemTheme.typography.caption, + style = TangemTheme.typography.caption2, ) } } diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsScreen.kt b/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsScreen.kt index 1d074d274a..8a65d0e7bd 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsScreen.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsScreen.kt @@ -229,7 +229,7 @@ private fun TangemAppVersion(appNameRes: Int, version: String, modifier: Modifie Text( modifier = modifier.padding(horizontal = TangemTheme.dimens.spacing16), text = "${stringResource(id = appNameRes)} $version", - style = TangemTheme.typography.caption, + style = TangemTheme.typography.caption2, color = TangemTheme.colors.text.tertiary, ) } diff --git a/app/src/main/java/com/tangem/tap/features/saveWallet/ui/components/SaveWalletScreenContent.kt b/app/src/main/java/com/tangem/tap/features/saveWallet/ui/components/SaveWalletScreenContent.kt index a20a0edf59..a8bbc46f02 100644 --- a/app/src/main/java/com/tangem/tap/features/saveWallet/ui/components/SaveWalletScreenContent.kt +++ b/app/src/main/java/com/tangem/tap/features/saveWallet/ui/components/SaveWalletScreenContent.kt @@ -144,7 +144,7 @@ private fun Footer(showProgress: Boolean, onSaveWalletClick: () -> Unit) { Text( modifier = Modifier.fillMaxWidth(fraction = .7f), text = stringResource(R.string.save_user_wallet_agreement_notice), - style = TangemTheme.typography.caption, + style = TangemTheme.typography.caption2, color = TangemTheme.colors.text.tertiary, textAlign = TextAlign.Center, ) diff --git a/app/src/main/java/com/tangem/tap/features/wallet/ui/view/TotalBalanceCard.kt b/app/src/main/java/com/tangem/tap/features/wallet/ui/view/TotalBalanceCard.kt index fdfadc43e1..d39bf43e05 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/ui/view/TotalBalanceCard.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/ui/view/TotalBalanceCard.kt @@ -134,7 +134,7 @@ private fun TotalBalanceCardContent(state: TotalBalanceCardState, modifier: Modi Text( modifier = Modifier.fillMaxWidth(), text = stringResource(id = R.string.main_processing_full_amount), - style = TangemTheme.typography.caption, + style = TangemTheme.typography.caption2, color = TangemTheme.colors.text.attention, ) } diff --git a/app/src/main/java/com/tangem/tap/features/walletSelector/ui/components/WalletItem.kt b/app/src/main/java/com/tangem/tap/features/walletSelector/ui/components/WalletItem.kt index 62ef2c0468..0224ee04fc 100644 --- a/app/src/main/java/com/tangem/tap/features/walletSelector/ui/components/WalletItem.kt +++ b/app/src/main/java/com/tangem/tap/features/walletSelector/ui/components/WalletItem.kt @@ -151,7 +151,7 @@ private fun RowScope.WalletInfo(wallet: UserWalletItem, isSelected: Boolean) { }, color = TangemTheme.colors.text.tertiary, maxLines = 1, - style = TangemTheme.typography.caption, + style = TangemTheme.typography.caption2, ) } } @@ -255,7 +255,7 @@ private fun LoadedTokensInfo( count = tokensCount, tokensCount, ), - style = TangemTheme.typography.caption, + style = TangemTheme.typography.caption2, color = TangemTheme.colors.text.tertiary, textAlign = TextAlign.End, ) diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/TextFields.kt b/core/ui/src/main/java/com/tangem/core/ui/components/TextFields.kt index 6f09757064..e954cbd32d 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/TextFields.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/TextFields.kt @@ -25,7 +25,6 @@ import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import com.tangem.core.ui.R import com.tangem.core.ui.res.TangemTheme -import com.tangem.core.ui.res.TangemTypography /** * [Show in Figma](https://www.figma.com/file/14ISV23YB1yVW1uNVwqrKv/Android?node-id=213%3A218&t=TmfD6UBHPg9uYfev-4) @@ -103,7 +102,7 @@ private fun TangemTextField( if (!label.isNullOrEmpty()) { Text( text = label, - style = TangemTheme.typography.caption, + style = TangemTheme.typography.caption2, color = colors.labelColor( enabled = enabled, error = isError, @@ -161,7 +160,7 @@ private fun TangemTextField( .fillMaxWidth() .padding(horizontal = 16.dp), text = caption, - style = TangemTypography.body1, + style = TangemTheme.typography.body1, color = colors.captionColor(enabled = enabled, isError = isError).value, ) } diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/appbar/AppBarWithSearch.kt b/core/ui/src/main/java/com/tangem/core/ui/components/appbar/AppBarWithSearch.kt index 55b0a08fe3..b88991e655 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/appbar/AppBarWithSearch.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/appbar/AppBarWithSearch.kt @@ -169,7 +169,7 @@ private fun SubtitleView(subtitle: String, icon: Painter?) { text = subtitle, color = TangemTheme.colors.text.secondary, maxLines = 1, - style = TangemTheme.typography.caption, + style = TangemTheme.typography.caption2, ) } } diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/tokenreceive/TokenReceiveBottomSheet.kt b/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/tokenreceive/TokenReceiveBottomSheet.kt index ee8b8d3581..ce07dc5ba7 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/tokenreceive/TokenReceiveBottomSheet.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/tokenreceive/TokenReceiveBottomSheet.kt @@ -63,7 +63,7 @@ private fun TokenReceiveBottomSheetContent(content: TokenReceiveBottomSheetConfi text = stringResource(R.string.receive_bottom_sheet_warning_message_full, content.name), color = TangemTheme.colors.text.secondary, textAlign = TextAlign.Center, - style = TangemTheme.typography.caption, + style = TangemTheme.typography.caption2, ) Row( horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing16), diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/notifications/Notification.kt b/core/ui/src/main/java/com/tangem/core/ui/components/notifications/Notification.kt index 6e569c56bb..56b449ec3a 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/notifications/Notification.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/notifications/Notification.kt @@ -157,7 +157,7 @@ private fun TextsBlock(title: TextReference, subtitle: TextReference) { Text( text = subtitle.resolveReference(), color = TangemTheme.colors.text.tertiary, - style = TangemTheme.typography.caption, + style = TangemTheme.typography.caption2, ) } } diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/transactions/Transaction.kt b/core/ui/src/main/java/com/tangem/core/ui/components/transactions/Transaction.kt index 283bbf7b89..4cbbb22026 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/transactions/Transaction.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/transactions/Transaction.kt @@ -251,7 +251,7 @@ private fun Subtitle(state: TransactionState, modifier: Modifier = Modifier) { modifier = modifier, textAlign = TextAlign.Start, color = TangemTheme.colors.text.tertiary, - style = TangemTheme.typography.caption, + style = TangemTheme.typography.caption2, ) } is TransactionState.Loading -> { @@ -304,7 +304,7 @@ private fun Timestamp(state: TransactionState, modifier: Modifier = Modifier) { modifier = modifier, textAlign = TextAlign.End, color = TangemTheme.colors.text.tertiary, - style = TangemTheme.typography.caption, + style = TangemTheme.typography.caption2, ) } is TransactionState.Loading -> { diff --git a/core/ui/src/main/java/com/tangem/core/ui/res/TangemTheme.kt b/core/ui/src/main/java/com/tangem/core/ui/res/TangemTheme.kt index 293956cef6..a4f6721f5c 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/res/TangemTheme.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/res/TangemTheme.kt @@ -3,7 +3,6 @@ package com.tangem.core.ui.res import androidx.compose.material.Colors import androidx.compose.material.MaterialTheme import androidx.compose.material.ProvideTextStyle -import androidx.compose.material.Typography import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.ReadOnlyComposable @@ -17,7 +16,7 @@ internal const val IS_SYSTEM_IN_DARK_THEME: Boolean = false @Composable fun TangemTheme( isDark: Boolean = false, - typography: Typography = TangemTheme.typography, + typography: TangemTypography = TangemTheme.typography, dimens: TangemDimens = TangemTheme.dimens, content: @Composable () -> Unit, ) { @@ -26,10 +25,8 @@ fun TangemTheme( .also { it.update(themeColors) } val shapes = remember { TangemShapes(dimens) } - MaterialTheme( colors = materialThemeColors(colors = themeColors, isDark = isDark), - typography = typography, ) { CompositionLocalProvider( LocalTangemColors provides rememberedColors, @@ -52,7 +49,7 @@ object TangemTheme { @ReadOnlyComposable get() = LocalTangemColors.current - val typography: Typography + val typography: TangemTypography @Composable @ReadOnlyComposable get() = LocalTangemTypography.current @@ -197,7 +194,7 @@ private val LocalTangemColors = staticCompositionLocalOf { } private val LocalTangemTypography = staticCompositionLocalOf { - TangemTypography + TangemTypography() } private val LocalTangemDimens = staticCompositionLocalOf { diff --git a/core/ui/src/main/java/com/tangem/core/ui/res/TangemTypography.kt b/core/ui/src/main/java/com/tangem/core/ui/res/TangemTypography.kt index bdcd413dbd..0187255098 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/res/TangemTypography.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/res/TangemTypography.kt @@ -1,6 +1,6 @@ package com.tangem.core.ui.res -import androidx.compose.material.Typography +import androidx.compose.runtime.Immutable import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.Font import androidx.compose.ui.text.font.FontFamily @@ -15,63 +15,87 @@ private val RobotoFamily = FontFamily( Font(R.font.roboto_medium, FontWeight.Medium), ) -val TangemTypography = Typography( - defaultFontFamily = RobotoFamily, - h1 = TextStyle( +@Immutable +data class TangemTypography internal constructor( + val head: TextStyle = TextStyle( + fontFamily = RobotoFamily, + fontSize = 34.sp, + fontWeight = FontWeight.SemiBold, + letterSpacing = TextUnit(value = 0f, type = TextUnitType.Sp), + lineHeight = TextUnit(value = 44f, type = TextUnitType.Sp), + ), + val h1: TextStyle = TextStyle( + fontFamily = RobotoFamily, fontSize = 34.sp, fontWeight = FontWeight.Normal, letterSpacing = TextUnit(value = 0f, type = TextUnitType.Sp), lineHeight = TextUnit(value = 44f, type = TextUnitType.Sp), ), - h2 = TextStyle( + val h2: TextStyle = TextStyle( + fontFamily = RobotoFamily, fontSize = 24.sp, fontWeight = FontWeight.Medium, letterSpacing = TextUnit(value = 0.18f, type = TextUnitType.Sp), lineHeight = TextUnit(value = 32f, type = TextUnitType.Sp), ), - h3 = TextStyle( + val h3: TextStyle = TextStyle( + fontFamily = RobotoFamily, fontSize = 20.sp, fontWeight = FontWeight.Medium, letterSpacing = TextUnit(value = 0.15f, type = TextUnitType.Sp), lineHeight = TextUnit(value = 24f, type = TextUnitType.Sp), ), - subtitle1 = TextStyle( + val subtitle1: TextStyle = TextStyle( + fontFamily = RobotoFamily, fontSize = 16.sp, fontWeight = FontWeight.Medium, letterSpacing = TextUnit(value = 0.15f, type = TextUnitType.Sp), lineHeight = TextUnit(value = 24f, type = TextUnitType.Sp), ), - subtitle2 = TextStyle( + val subtitle2: TextStyle = TextStyle( + fontFamily = RobotoFamily, fontSize = 14.sp, fontWeight = FontWeight.Medium, letterSpacing = TextUnit(value = 0.5f, type = TextUnitType.Sp), lineHeight = TextUnit(value = 24f, type = TextUnitType.Sp), ), - body1 = TextStyle( + val body1: TextStyle = TextStyle( + fontFamily = RobotoFamily, fontSize = 16.sp, fontWeight = FontWeight.Normal, letterSpacing = TextUnit(value = 0.5f, type = TextUnitType.Sp), lineHeight = TextUnit(value = 24f, type = TextUnitType.Sp), ), - body2 = TextStyle( + val body2: TextStyle = TextStyle( + fontFamily = RobotoFamily, fontSize = 14.sp, fontWeight = FontWeight.Normal, letterSpacing = TextUnit(value = 0.25f, type = TextUnitType.Sp), lineHeight = TextUnit(value = 20f, type = TextUnitType.Sp), ), - button = TextStyle( + val button: TextStyle = TextStyle( + fontFamily = RobotoFamily, fontSize = 14.sp, fontWeight = FontWeight.Medium, letterSpacing = TextUnit(value = 0.1f, type = TextUnitType.Sp), lineHeight = TextUnit(value = 16f, type = TextUnitType.Sp), ), - caption = TextStyle( + val caption1: TextStyle = TextStyle( + fontFamily = RobotoFamily, + fontSize = 12.sp, + fontWeight = FontWeight.Medium, + letterSpacing = TextUnit(value = 0.4f, type = TextUnitType.Sp), + lineHeight = TextUnit(value = 16f, type = TextUnitType.Sp), + ), + val caption2: TextStyle = TextStyle( + fontFamily = RobotoFamily, fontSize = 12.sp, fontWeight = FontWeight.Normal, letterSpacing = TextUnit(value = 0.4f, type = TextUnitType.Sp), lineHeight = TextUnit(value = 16f, type = TextUnitType.Sp), ), - overline = TextStyle( + val overline: TextStyle = TextStyle( + fontFamily = RobotoFamily, fontSize = 10.sp, fontWeight = FontWeight.Medium, letterSpacing = TextUnit(value = 1.5f, type = TextUnitType.Sp), diff --git a/features/learn2earn/impl/src/main/java/com/tangem/feature/learn2earn/presentation/ui/GetBonusView.kt b/features/learn2earn/impl/src/main/java/com/tangem/feature/learn2earn/presentation/ui/GetBonusView.kt index a36d44f760..792105fb40 100644 --- a/features/learn2earn/impl/src/main/java/com/tangem/feature/learn2earn/presentation/ui/GetBonusView.kt +++ b/features/learn2earn/impl/src/main/java/com/tangem/feature/learn2earn/presentation/ui/GetBonusView.kt @@ -22,7 +22,6 @@ import com.tangem.core.ui.components.SpacerH4 import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.res.TangemColorPalette import com.tangem.core.ui.res.TangemTheme -import com.tangem.core.ui.res.TangemTypography import com.tangem.feature.learn2earn.impl.R import com.tangem.feature.learn2earn.presentation.ui.component.GradientCircle import com.tangem.feature.learn2earn.presentation.ui.state.MainScreenState @@ -70,13 +69,13 @@ internal fun GetBonusView(state: MainScreenState, modifier: Modifier = Modifier) ) { Text( text = state.description.title.resolveReference(), - style = TangemTypography.body1, + style = TangemTheme.typography.body1, color = TangemTheme.colors.text.primary2, ) SpacerH4() Text( text = state.description.subtitle.resolveReference(), - style = TangemTypography.caption, + style = TangemTheme.typography.caption2, color = TangemTheme.colors.text.tertiary, ) } diff --git a/features/learn2earn/impl/src/main/java/com/tangem/feature/learn2earn/presentation/ui/Learn2earnStoriesScreen.kt b/features/learn2earn/impl/src/main/java/com/tangem/feature/learn2earn/presentation/ui/Learn2earnStoriesScreen.kt index b91ffaf2f5..4d498fd377 100644 --- a/features/learn2earn/impl/src/main/java/com/tangem/feature/learn2earn/presentation/ui/Learn2earnStoriesScreen.kt +++ b/features/learn2earn/impl/src/main/java/com/tangem/feature/learn2earn/presentation/ui/Learn2earnStoriesScreen.kt @@ -17,7 +17,6 @@ import com.tangem.core.ui.components.SecondaryButton import com.tangem.core.ui.components.SpacerH16 import com.tangem.core.ui.components.SpacerH32 import com.tangem.core.ui.res.TangemTheme -import com.tangem.core.ui.res.TangemTypography import com.tangem.feature.learn2earn.impl.R import com.tangem.feature.learn2earn.presentation.ui.component.GradientCircle @@ -106,7 +105,7 @@ private fun StoryDescription(headerText: String, bodyText: String, modifier: Mod Text( text = bodyText, textAlign = TextAlign.Center, - style = TangemTypography.subtitle1, + style = TangemTheme.typography.subtitle1, color = TangemTheme.colors.text.tertiary, ) } diff --git a/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/ImportSeedPhraseScreen.kt b/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/ImportSeedPhraseScreen.kt index 5f9b14ae11..10248b767a 100644 --- a/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/ImportSeedPhraseScreen.kt +++ b/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/ImportSeedPhraseScreen.kt @@ -112,7 +112,7 @@ private fun PhraseBlock(state: ImportSeedPhraseState, modifier: Modifier = Modif Text( modifier = Modifier.fillMaxSize(), text = message, - style = TangemTheme.typography.caption.copy( + style = TangemTheme.typography.caption2.copy( color = TangemTheme.colors.text.warning, ), ) diff --git a/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/AgreementText.kt b/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/AgreementText.kt index c143b3b4e5..d29a6c996b 100644 --- a/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/AgreementText.kt +++ b/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/AgreementText.kt @@ -26,7 +26,7 @@ internal fun AgreementText(@StringRes firstPartResId: Int, onClick: () -> Unit) modifier = Modifier .fillMaxWidth() .padding(horizontal = TangemTheme.dimens.spacing54), - style = TangemTheme.typography.caption.copy(textAlign = TextAlign.Center), + style = TangemTheme.typography.caption2.copy(textAlign = TextAlign.Center), maxLines = 2, onClick = { val clickableSpanStyle = requireNotNull(agreementText.spanStyles.getOrNull(1)) diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapSelectTokenScreen.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapSelectTokenScreen.kt index eb086a8df4..23647ff1d8 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapSelectTokenScreen.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapSelectTokenScreen.kt @@ -150,7 +150,7 @@ private fun TokenItem(token: TokenToSelect, network: Network, onTokenClick: () - SpacerW2() Text( text = token.symbol, - style = TangemTheme.typography.caption, + style = TangemTheme.typography.caption2, color = TangemTheme.colors.text.tertiary, ) } @@ -160,7 +160,7 @@ private fun TokenItem(token: TokenToSelect, network: Network, onTokenClick: () - if (!token.available) { Text( text = stringResource(id = R.string.swapping_token_not_available), - style = TangemTheme.typography.caption, + style = TangemTheme.typography.caption2, color = TangemTheme.colors.text.tertiary, ) } else if (token.addedTokenBalanceData != null) { @@ -177,7 +177,7 @@ private fun TokenItem(token: TokenToSelect, network: Network, onTokenClick: () - SpacerW2() Text( text = token.addedTokenBalanceData.amount.orEmpty(), - style = TangemTheme.typography.caption, + style = TangemTheme.typography.caption2, color = TangemTheme.colors.text.tertiary, ) } diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsLoadedBalanceConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsLoadedBalanceConverter.kt index 9240092123..cf82bb5725 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsLoadedBalanceConverter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsLoadedBalanceConverter.kt @@ -59,24 +59,22 @@ internal class TokenDetailsLoadedBalanceConverter( return when (status.value) { is CryptoCurrencyStatus.NoQuote, is CryptoCurrencyStatus.Loaded, - -> { - TokenDetailsBalanceBlockState.Content( - actionButtons = currentState.actionButtons, - fiatBalance = formatFiatAmount(status.value, appCurrencyProvider()), - cryptoBalance = formatCryptoAmount(status), - ) - } - is CryptoCurrencyStatus.Loading -> { - TokenDetailsBalanceBlockState.Loading(currentState.actionButtons) - } + -> TokenDetailsBalanceBlockState.Content( + actionButtons = currentState.actionButtons, + fiatBalance = formatFiatAmount(status.value, appCurrencyProvider()), + cryptoBalance = formatCryptoAmount(status), + ) + is CryptoCurrencyStatus.NoAccount -> TokenDetailsBalanceBlockState.Content( + actionButtons = currentState.actionButtons, + fiatBalance = formatFiatAmount(status.value, appCurrencyProvider()), + cryptoBalance = formatCryptoAmount(status), + ) + is CryptoCurrencyStatus.Loading -> TokenDetailsBalanceBlockState.Loading(currentState.actionButtons) is CryptoCurrencyStatus.MissedDerivation, - is CryptoCurrencyStatus.NoAccount, is CryptoCurrencyStatus.Custom, is CryptoCurrencyStatus.NoAmount, is CryptoCurrencyStatus.Unreachable, - -> { - TokenDetailsBalanceBlockState.Error(currentState.actionButtons) - } + -> TokenDetailsBalanceBlockState.Error(currentState.actionButtons) } } diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenDetailsBalanceBlock.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenDetailsBalanceBlock.kt index 0fc3606d9b..2df27ff04c 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenDetailsBalanceBlock.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenDetailsBalanceBlock.kt @@ -112,13 +112,13 @@ private fun CryptoBalance( is TokenDetailsBalanceBlockState.Content -> Text( modifier = modifier, text = if (isBalanceHidden) STARS else state.cryptoBalance, - style = TangemTheme.typography.caption, + style = TangemTheme.typography.caption2, color = TangemTheme.colors.text.tertiary, ) is TokenDetailsBalanceBlockState.Error -> Text( modifier = modifier, text = BigDecimalFormatter.EMPTY_BALANCE_SIGN, - style = TangemTheme.typography.caption, + style = TangemTheme.typography.caption2, color = TangemTheme.colors.text.tertiary, ) } diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenInfoBlock.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenInfoBlock.kt index 4d149f38c8..7ccdf694b5 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenInfoBlock.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenInfoBlock.kt @@ -34,7 +34,7 @@ internal fun TokenInfoBlock(state: TokenInfoBlockState, modifier: Modifier = Mod ) { Text( text = state.name, - style = TangemTheme.typography.h1, + style = TangemTheme.typography.head, color = TangemTheme.colors.text.primary1, ) NetworkInfoText(state.currency) @@ -63,7 +63,7 @@ private fun NetworkInfoText(currency: TokenInfoBlockState.Currency) { Text( text = stringResource(id = R.string.common_main_network), color = TangemTheme.colors.text.tertiary, - style = TangemTheme.typography.caption, + style = TangemTheme.typography.caption2, ) } is TokenInfoBlockState.Currency.Token -> { @@ -74,7 +74,7 @@ private fun NetworkInfoText(currency: TokenInfoBlockState.Currency) { val state = extractNetwork(tokenCurrency = currency) Text( text = state.normalText, - style = TangemTheme.typography.caption, + style = TangemTheme.typography.caption2, color = TangemTheme.colors.text.tertiary, ) Icon( @@ -85,7 +85,7 @@ private fun NetworkInfoText(currency: TokenInfoBlockState.Currency) { ) Text( text = state.boldText, - style = TangemTheme.typography.caption.copy(fontWeight = FontWeight.Medium), + style = TangemTheme.typography.caption2.copy(fontWeight = FontWeight.Medium), color = TangemTheme.colors.text.primary1, ) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/token/NonFiatContentBlock.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/token/NonFiatContentBlock.kt index cb5c920555..478991676d 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/token/NonFiatContentBlock.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/token/NonFiatContentBlock.kt @@ -14,7 +14,6 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow import com.tangem.core.ui.res.TangemTheme -import com.tangem.core.ui.res.TangemTypography import com.tangem.feature.wallet.impl.R import com.tangem.feature.wallet.presentation.common.state.TokenItemState import org.burnoutcrew.reorderable.ReorderableLazyListState @@ -73,6 +72,6 @@ private fun NonFiatContentText(@StringRes text: Int) { color = TangemTheme.colors.text.tertiary, maxLines = 1, overflow = TextOverflow.Ellipsis, - style = TangemTypography.body2, + style = TangemTheme.typography.body2, ) } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/token/TokenCryptoAmount.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/token/TokenCryptoAmount.kt index 74d1cf836f..426d6d153a 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/token/TokenCryptoAmount.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/token/TokenCryptoAmount.kt @@ -11,7 +11,6 @@ import androidx.compose.ui.text.style.TextOverflow import com.tangem.common.Strings import com.tangem.core.ui.components.RectangleShimmer import com.tangem.core.ui.res.TangemTheme -import com.tangem.core.ui.res.TangemTypography import com.tangem.feature.wallet.impl.R import com.tangem.feature.wallet.presentation.common.state.TokenItemState.CryptoAmountState as TokenCryptoAmountState @@ -49,7 +48,7 @@ private fun CryptoAmountText(amount: String, modifier: Modifier = Modifier) { color = TangemTheme.colors.text.tertiary, maxLines = 1, overflow = TextOverflow.Ellipsis, - style = TangemTypography.body2, + style = TangemTheme.typography.body2, ) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/token/TokenFiatAmount.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/token/TokenFiatAmount.kt index 6aff7c7e1c..f223ecdcfe 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/token/TokenFiatAmount.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/token/TokenFiatAmount.kt @@ -10,7 +10,6 @@ import androidx.compose.ui.text.style.TextOverflow import com.tangem.common.Strings import com.tangem.core.ui.components.RectangleShimmer import com.tangem.core.ui.res.TangemTheme -import com.tangem.core.ui.res.TangemTypography import com.tangem.feature.wallet.presentation.common.state.TokenItemState.FiatAmountState as TokenFiatAmountState @Composable @@ -40,7 +39,7 @@ private fun FiatAmountText(text: String, modifier: Modifier = Modifier) { color = TangemTheme.colors.text.primary1, maxLines = 1, overflow = TextOverflow.Ellipsis, - style = TangemTypography.body2, + style = TangemTheme.typography.body2, ) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/token/TokenPriceChange.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/token/TokenPriceChange.kt index 002357512e..d40de53a30 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/token/TokenPriceChange.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/token/TokenPriceChange.kt @@ -15,7 +15,6 @@ import com.tangem.core.ui.components.RectangleShimmer import com.tangem.core.ui.components.SpacerW4 import com.tangem.core.ui.components.marketprice.PriceChangeType import com.tangem.core.ui.res.TangemTheme -import com.tangem.core.ui.res.TangemTypography import com.tangem.feature.wallet.impl.R import com.tangem.feature.wallet.presentation.common.state.TokenItemState import com.tangem.feature.wallet.presentation.common.state.TokenItemState.PriceChangeState as TokenPriceChangeState @@ -87,7 +86,7 @@ private fun PriceChangeText(type: PriceChangeType?, text: String?) { }, overflow = TextOverflow.Ellipsis, maxLines = 1, - style = TangemTypography.body2, + style = TangemTheme.typography.body2, ) } } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/token/TokenTitle.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/token/TokenTitle.kt index 85091af4f8..e5b3050a53 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/token/TokenTitle.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/token/TokenTitle.kt @@ -15,7 +15,6 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.style.TextOverflow import com.tangem.core.ui.components.RectangleShimmer import com.tangem.core.ui.res.TangemTheme -import com.tangem.core.ui.res.TangemTypography import com.tangem.feature.wallet.impl.R import com.tangem.feature.wallet.presentation.common.state.TokenItemState.TitleState as TokenTitleState @@ -63,7 +62,7 @@ private fun CurrencyNameText(name: String, modifier: Modifier = Modifier) { color = TangemTheme.colors.text.primary1, overflow = TextOverflow.Ellipsis, maxLines = 1, - style = TangemTypography.subtitle2, + style = TangemTheme.typography.subtitle2, ) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletCard.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletCard.kt index 9af63cffb8..f0348827c3 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletCard.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletCard.kt @@ -361,7 +361,7 @@ private fun AdditionalInfoText(text: TextReference) { color = TangemTheme.colors.text.tertiary, maxLines = 1, overflow = TextOverflow.Ellipsis, - style = TangemTheme.typography.caption, + style = TangemTheme.typography.caption2, ) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/multicurrency/MultiCurrencyContent.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/multicurrency/MultiCurrencyContent.kt index 9bdaf10b43..287f84f8d9 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/multicurrency/MultiCurrencyContent.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/multicurrency/MultiCurrencyContent.kt @@ -82,7 +82,7 @@ private fun LazyListScope.nonContentItem(modifier: Modifier = Modifier) { modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing48), color = TangemTheme.colors.text.tertiary, textAlign = TextAlign.Center, - style = TangemTheme.typography.caption, + style = TangemTheme.typography.caption2, ) } } From 965a6f8e4610c463994159553cd2ed11c1896ed6 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 9 Oct 2023 16:04:17 +0300 Subject: [PATCH 163/242] Updated on 2026-08-14 --- .../kotlin/com/tangem/data/tokens/utils/TokensOperations.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/TokensOperations.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/TokensOperations.kt index d4549a6243..be82d11c84 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/TokensOperations.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/TokensOperations.kt @@ -65,8 +65,8 @@ internal fun getCoinIconUrl(blockchain: Blockchain): String? { internal fun List.hasCoinForToken(token: CryptoCurrency.Token): Boolean { return any { val blockchain = getBlockchain(networkId = token.network.id) - - it.id == blockchain.toCoinId() + val tokenDerivation = token.network.derivationPath.value + it.id == blockchain.toCoinId() && it.derivationPath == tokenDerivation } } From a78432b9f619743ab1f8ef647f12def7e6701aa4 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 10 Oct 2023 16:20:40 +0800 Subject: [PATCH 164/242] Updated on 2026-08-14 --- .../presentation/common/WalletPreviewData.kt | 8 ++++++- .../common/state/TokenItemState.kt | 8 ++++--- .../state/components/WalletCardState.kt | 15 ++++++++----- .../factory/WalletSkeletonStateConverter.kt | 1 + .../wallet/ui/components/common/WalletCard.kt | 12 +---------- ...ryptoCurrencyStatusToTokenItemConverter.kt | 13 +++++++++--- .../utils/FiatBalanceToWalletCardConverter.kt | 2 +- .../wallet/utils/LoadingItemsProvider.kt | 21 ------------------- 8 files changed, 35 insertions(+), 45 deletions(-) delete mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/LoadingItemsProvider.kt diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/WalletPreviewData.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/WalletPreviewData.kt index 9456466cb9..c22554ba8d 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/WalletPreviewData.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/WalletPreviewData.kt @@ -207,7 +207,13 @@ internal object WalletPreviewData { ) } - val loadingTokenItemState by lazy { TokenItemState.Loading(id = "Loading#1") } + val loadingTokenItemState by lazy { + TokenItemState.Loading( + id = "Loading#1", + iconState = customTokenIconState.copy(isGrayscale = true), + titleState = TokenItemState.TitleState.Content(text = "Polygon"), + ) + } private const val networksSize = 10 private const val tokensSize = 3 diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/state/TokenItemState.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/state/TokenItemState.kt index b5d6b52d47..2002ca9fc9 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/state/TokenItemState.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/state/TokenItemState.kt @@ -22,9 +22,11 @@ internal sealed class TokenItemState { abstract val priceChangeState: PriceChangeState? /** Loading token state */ - data class Loading(override val id: String) : TokenItemState() { - override val iconState: IconState = IconState.Loading - override val titleState: TitleState = TitleState.Loading + data class Loading( + override val id: String, + override val iconState: IconState, + override val titleState: TitleState.Content, + ) : TokenItemState() { override val fiatAmountState: FiatAmountState = FiatAmountState.Loading override val cryptoAmountState: CryptoAmountState = CryptoAmountState.Loading override val priceChangeState: PriceChangeState = PriceChangeState.Loading diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/components/WalletCardState.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/components/WalletCardState.kt index 8edccf6a0f..b1d0bb3853 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/components/WalletCardState.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/components/WalletCardState.kt @@ -16,6 +16,8 @@ internal sealed interface WalletCardState { /** Title */ val title: String + val additionalInfo: TextReference? + /** Wallet image resource id */ @get:DrawableRes val imageResId: Int? @@ -41,10 +43,10 @@ internal sealed interface WalletCardState { data class Content( override val id: UserWalletId, override val title: String, + override val additionalInfo: TextReference, override val imageResId: Int?, override val onRenameClick: (UserWalletId, String) -> Unit, override val onDeleteClick: (UserWalletId) -> Unit, - val additionalInfo: TextReference, val cardCount: Int?, val balance: String, ) : WalletCardState @@ -64,10 +66,10 @@ internal sealed interface WalletCardState { data class HiddenContent( override val id: UserWalletId, override val title: String, + override val additionalInfo: TextReference, override val imageResId: Int?, override val onRenameClick: (UserWalletId, String) -> Unit, override val onDeleteClick: (UserWalletId) -> Unit, - val additionalInfo: TextReference, val balance: String, val cardCount: Int?, ) : WalletCardState @@ -77,18 +79,18 @@ internal sealed interface WalletCardState { * * @property id wallet id * @property title wallet name + * @property additionalInfo wallet additional info * @property imageResId wallet image resource id * @property onRenameClick lambda be invoked when Rename button is clicked * @property onDeleteClick lambda be invoked when Delete button is clicked - * @property additionalInfo wallet additional info */ data class LockedContent( override val id: UserWalletId, override val title: String, + override val additionalInfo: TextReference, override val imageResId: Int?, override val onRenameClick: (UserWalletId, String) -> Unit, override val onDeleteClick: (UserWalletId) -> Unit, - val additionalInfo: TextReference, ) : WalletCardState /** @@ -106,7 +108,9 @@ internal sealed interface WalletCardState { override val imageResId: Int?, override val onRenameClick: (UserWalletId, String) -> Unit, override val onDeleteClick: (UserWalletId) -> Unit, - ) : WalletCardState + ) : WalletCardState { + override val additionalInfo: TextReference = EMPTY_BALANCE_TEXT + } /** * Wallet card loading state @@ -120,6 +124,7 @@ internal sealed interface WalletCardState { data class Loading( override val id: UserWalletId, override val title: String, + override val additionalInfo: TextReference? = null, override val imageResId: Int?, override val onRenameClick: (UserWalletId, String) -> Unit, override val onDeleteClick: (UserWalletId) -> Unit, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletSkeletonStateConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletSkeletonStateConverter.kt index 51a78e138e..df44339b4d 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletSkeletonStateConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletSkeletonStateConverter.kt @@ -126,6 +126,7 @@ internal class WalletSkeletonStateConverter( return WalletCardState.Loading( id = walletId, title = name, + additionalInfo = if (isMultiCurrency) WalletAdditionalInfoFactory.resolve(wallet = this) else null, imageResId = createImageResId(), onRenameClick = clickIntents::onRenameClick, onDeleteClick = clickIntents::onDeleteClick, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletCard.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletCard.kt index f0348827c3..fb0002479c 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletCard.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletCard.kt @@ -93,7 +93,7 @@ internal fun WalletCard(state: WalletCardState, modifier: Modifier = Modifier) { ) AdditionalInfo( - text = resolveAdditionalTextByState(state), + text = state.additionalInfo, modifier = Modifier.constrainAs(additionalTextRef) { start.linkTo(parent.start) top.linkTo(balanceRef.bottom) @@ -344,16 +344,6 @@ private fun AdditionalInfo(text: TextReference?, modifier: Modifier = Modifier) } } -private fun resolveAdditionalTextByState(state: WalletCardState): TextReference? { - return when (state) { - is WalletCardState.Content -> state.additionalInfo - is WalletCardState.LockedContent -> state.additionalInfo - is WalletCardState.Error -> WalletCardState.EMPTY_BALANCE_TEXT - is WalletCardState.HiddenContent -> WalletCardState.HIDDEN_BALANCE_TEXT - is WalletCardState.Loading -> null - } -} - @Composable private fun AdditionalInfoText(text: TextReference) { Text( diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/CryptoCurrencyStatusToTokenItemConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/CryptoCurrencyStatusToTokenItemConverter.kt index 89ce3ee6b1..c23c0cefa7 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/CryptoCurrencyStatusToTokenItemConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/CryptoCurrencyStatusToTokenItemConverter.kt @@ -17,24 +17,31 @@ internal class CryptoCurrencyStatusToTokenItemConverter( private val clickIntents: WalletClickIntents, ) : Converter { - private val iconStateConverter = CryptoCurrencyToIconStateConverter() + private val iconStateConverter by lazy(::CryptoCurrencyToIconStateConverter) override fun convert(value: CryptoCurrencyStatus): TokenItemState { return when (value.value) { - is CryptoCurrencyStatus.Loading -> TokenItemState.Loading(id = value.currency.id.value) + is CryptoCurrencyStatus.Loading -> value.mapToLoadingState() is CryptoCurrencyStatus.Loaded, is CryptoCurrencyStatus.Custom, is CryptoCurrencyStatus.NoQuote, is CryptoCurrencyStatus.NoAccount, -> value.mapToTokenItemState() is CryptoCurrencyStatus.MissedDerivation -> value.mapToNoAddressTokenItemState() - // TODO: Add other token item states, currently not designed is CryptoCurrencyStatus.Unreachable, is CryptoCurrencyStatus.NoAmount, -> value.mapToUnreachableTokenItemState() } } + private fun CryptoCurrencyStatus.mapToLoadingState(): TokenItemState.Loading { + return TokenItemState.Loading( + id = currency.id.value, + iconState = iconStateConverter.convert(value = this), + titleState = TokenItemState.TitleState.Content(text = currency.name), + ) + } + private fun CryptoCurrencyStatus.mapToTokenItemState(): TokenItemState.Content { return TokenItemState.Content( id = currency.id.value, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/FiatBalanceToWalletCardConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/FiatBalanceToWalletCardConverter.kt index bca1082c42..6be00960a7 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/FiatBalanceToWalletCardConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/FiatBalanceToWalletCardConverter.kt @@ -26,7 +26,7 @@ internal class FiatBalanceToWalletCardConverter( } private fun WalletCardState.toLoadingWalletCardState(): WalletCardState { - return WalletCardState.Loading(id, title, imageResId, onRenameClick, onDeleteClick) + return WalletCardState.Loading(id, title, additionalInfo, imageResId, onRenameClick, onDeleteClick) } private fun WalletCardState.toErrorWalletCardState(): WalletCardState { diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/LoadingItemsProvider.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/LoadingItemsProvider.kt deleted file mode 100644 index c7e545dec3..0000000000 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/LoadingItemsProvider.kt +++ /dev/null @@ -1,21 +0,0 @@ -package com.tangem.feature.wallet.presentation.wallet.utils - -import com.tangem.feature.wallet.presentation.common.state.TokenItemState -import com.tangem.feature.wallet.presentation.wallet.state.components.WalletTokensListState -import kotlinx.collections.immutable.ImmutableList -import kotlinx.collections.immutable.toImmutableList - -internal object LoadingItemsProvider { - - fun getLoadingMultiCurrencyTokens(): ImmutableList { - val items = mutableListOf() - repeat(times = 5) { - items.add( - WalletTokensListState.TokensListItemState.Token( - state = TokenItemState.Loading(id = "Loading#$it"), - ), - ) - } - return items.toImmutableList() - } -} \ No newline at end of file From 33cf25ac4c25f8df43ee113cf716f77e5da75883 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 10 Oct 2023 11:23:03 +0300 Subject: [PATCH 165/242] Updated on 2026-08-14 --- .../presentation/wallet/viewmodels/WalletStateCache.kt | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletStateCache.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletStateCache.kt index 489b6dd666..0c81b8e3ee 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletStateCache.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletStateCache.kt @@ -20,9 +20,10 @@ internal object WalletStateCache { states[userWalletId] = state } - fun updateAll(func: (WalletState.ContentState.() -> WalletState.ContentState)) { + /** Update all content states */ + fun updateAll(block: (WalletState.ContentState.() -> WalletState.ContentState)) { states.keys.forEach { - states[it] = func(states[it]!!) + states[it] = block(requireNotNull(states[it])) } } } \ No newline at end of file From 7d6e48a613cba21a034315218117e4ae7d314cf4 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 9 Oct 2023 18:44:27 +0300 Subject: [PATCH 166/242] Updated on 2026-08-14 --- .../presentation/organizetokens/OrganizeTokensScreen.kt | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensScreen.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensScreen.kt index 63b4037673..6053795ce3 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensScreen.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensScreen.kt @@ -19,6 +19,8 @@ import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.RectangleShape import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.hapticfeedback.HapticFeedbackType +import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter @@ -84,6 +86,7 @@ private fun TokenList( dndConfig: OrganizeTokensState.DragAndDropConfig, modifier: Modifier = Modifier, ) { + val hapticFeedback = LocalHapticFeedback.current Box(modifier = modifier) { val onDragEnd: (Int, Int) -> Unit = remember { { _, _ -> @@ -117,7 +120,10 @@ private fun TokenList( ) { index, item -> val onDragStart = remember(item) { - { dndConfig.onItemDragStart(item) } + { + dndConfig.onItemDragStart(item) + hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress) + } } DraggableItem( From 648236f1870a9d247f85bea8c1173bc9dc848c75 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 9 Oct 2023 17:05:59 +0300 Subject: [PATCH 167/242] Updated on 2026-08-14 --- .../tokenreceive/TokenReceiveBottomSheet.kt | 13 ++++++++----- .../presentation/router/DefaultWalletRouter.kt | 2 +- gradle/dependencies.toml | 16 ++++++++-------- .../plugin/configuration/model/AppConfig.kt | 4 ++-- 4 files changed, 19 insertions(+), 16 deletions(-) diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/tokenreceive/TokenReceiveBottomSheet.kt b/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/tokenreceive/TokenReceiveBottomSheet.kt index ce07dc5ba7..417b9fcb35 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/tokenreceive/TokenReceiveBottomSheet.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/tokenreceive/TokenReceiveBottomSheet.kt @@ -100,15 +100,18 @@ private fun TokenReceiveBottomSheetContent(content: TokenReceiveBottomSheetConfi @Composable private fun QrCodeContent(content: TokenReceiveBottomSheetConfig, onAddressChange: (AddressModel) -> Unit) { val qrCodes = rememberQrPainters(content.addresses.map(AddressModel::value)) - val pagerState = rememberPagerState() - val pageCount = content.addresses.count() + val pagerState = rememberPagerState( + initialPage = 0, + initialPageOffsetFraction = 0f, + ) { + content.addresses.count() + } LaunchedEffect(key1 = pagerState.currentPage) { onAddressChange.invoke(content.addresses[pagerState.currentPage]) } HorizontalPager( - pageCount = pageCount, state = pagerState, ) { currentPage -> Column( @@ -142,7 +145,7 @@ private fun QrCodeContent(content: TokenReceiveBottomSheetConfig, onAddressChang } } - if (pageCount > 1) { + if (pagerState.pageCount > 1) { val indicatorState = rememberLazyListState() val selectedColor = TangemTheme.colors.icon.primary1 val unselectedColor = TangemTheme.colors.icon.informative @@ -153,7 +156,7 @@ private fun QrCodeContent(content: TokenReceiveBottomSheetConfig, onAddressChang horizontalArrangement = Arrangement.Center, verticalAlignment = Alignment.CenterVertically, ) { - repeat(pageCount) { iteration -> + repeat(pagerState.pageCount) { iteration -> item(key = iteration) { val color = if (pagerState.currentPage == iteration) { selectedColor diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/DefaultWalletRouter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/DefaultWalletRouter.kt index c481e57703..6ad00bcf52 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/DefaultWalletRouter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/DefaultWalletRouter.kt @@ -78,7 +78,7 @@ internal class DefaultWalletRouter(private val reduxNavController: ReduxNavContr * next element is wallet screen entry. * If backstack contains only NavGraph entry and wallet screen entry then we close the wallet fragment. */ - if (navController.backQueue.size == BACKSTACK_ENTRY_COUNT_TO_CLOSE_WALLET_SCREEN) { + if (navController.currentBackStack.value.size == BACKSTACK_ENTRY_COUNT_TO_CLOSE_WALLET_SCREEN) { if (screen != null) { reduxNavController.navigate(action = NavigationAction.PopBackTo(screen)) } else { diff --git a/gradle/dependencies.toml b/gradle/dependencies.toml index f0b0c1d42f..a75f75bdfa 100644 --- a/gradle/dependencies.toml +++ b/gradle/dependencies.toml @@ -10,7 +10,7 @@ kotlin = "1.8.21" # endregion Classpath # region AndroidX -androidxActivityCompose = "1.5.0" +androidxActivityCompose = "1.8.0" androidxAppCompat = "1.5.1" androidxBrowser = "1.4.0" androidxConstraintLayout = "2.1.4" @@ -24,16 +24,16 @@ androidx-palette = "1.0.0" # region Compose compose-compiler = "1.4.7" -compose-runtime = "1.4.3" -compose-foundation = "1.4.3" -compose-material = "1.4.3" -compose-material3 = "1.1.0" +compose-runtime = "1.5.3" +compose-foundation = "1.5.3" +compose-material = "1.5.3" +compose-material3 = "1.1.2" compose-constraint = "1.0.1" -compose-navigation = "2.5.3" +compose-navigation = "2.7.4" compose-accompanist = "0.30.1" -compose-paging = "3.2.0" +compose-paging = "3.2.1" compose-reorderable = "0.9.6" -compoese-lifecycle-runtime = "2.6.1" +compoese-lifecycle-runtime = "2.6.2" # endregion Compose # region Other libraries diff --git a/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/model/AppConfig.kt b/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/model/AppConfig.kt index 39b4f01925..c8bfbff0f1 100644 --- a/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/model/AppConfig.kt +++ b/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/model/AppConfig.kt @@ -5,6 +5,6 @@ internal object AppConfig { const val versionCode = 1 const val versionName = "1.0.0-SNAPSHOT" const val minSdkVersion = 23 - const val targetSdkVersion = 33 - const val compileSdkVersion = 33 + const val targetSdkVersion = 34 + const val compileSdkVersion = 34 } \ No newline at end of file From 7ce0d44c0aaec3f63d63b98ee37e8ff741d76c93 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 9 Oct 2023 17:59:06 +0300 Subject: [PATCH 168/242] Updated on 2026-08-14 --- .../tangem/core/ui/components/buttons/HorizontalActionChips.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/buttons/HorizontalActionChips.kt b/core/ui/src/main/java/com/tangem/core/ui/components/buttons/HorizontalActionChips.kt index b34aa9c970..3a5bbafce5 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/buttons/HorizontalActionChips.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/buttons/HorizontalActionChips.kt @@ -31,9 +31,9 @@ fun HorizontalActionChips( verticalAlignment = Alignment.CenterVertically, contentPadding = contentPadding, ) { + // do not use key cause when change items order, list is scrolled items( items = buttons, - key = { config -> config.text.hashCode() }, itemContent = { ActionButton(config = it) }, ) } From c2edc435d0dcb124ea1395f4b76e3a83949bfb58 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 9 Oct 2023 13:28:43 +0300 Subject: [PATCH 169/242] Updated on 2026-08-14 --- .../wallet/ui/OnboardingWalletFragment.kt | 5 ++++- .../onboarding/di/OnboardingRouterModule.kt | 20 +++++++++++++++++++ .../navigation/DefaultOnboardingRouter.kt | 3 +++ .../onboarding/navigation/OnboardingRouter.kt | 12 +++++++++++ features/wallet/impl/build.gradle.kts | 3 +++ .../router/DefaultWalletRouter.kt | 8 +++++++- 6 files changed, 49 insertions(+), 2 deletions(-) create mode 100644 features/onboarding/src/main/java/com/tangem/feature/onboarding/di/OnboardingRouterModule.kt create mode 100644 features/onboarding/src/main/java/com/tangem/feature/onboarding/navigation/DefaultOnboardingRouter.kt create mode 100644 features/onboarding/src/main/java/com/tangem/feature/onboarding/navigation/OnboardingRouter.kt diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/OnboardingWalletFragment.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/OnboardingWalletFragment.kt index be6b6cd967..2eb4abdc18 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/OnboardingWalletFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/OnboardingWalletFragment.kt @@ -24,6 +24,7 @@ import com.tangem.core.analytics.Analytics import com.tangem.core.ui.extensions.setStatusBarColor import com.tangem.domain.common.util.cardTypesResolver import com.tangem.feature.onboarding.data.model.CreateWalletResponse +import com.tangem.feature.onboarding.navigation.OnboardingRouter import com.tangem.feature.onboarding.presentation.wallet2.analytics.SeedPhraseSource import com.tangem.feature.onboarding.presentation.wallet2.viewmodel.SeedPhraseMediator import com.tangem.feature.onboarding.presentation.wallet2.viewmodel.SeedPhraseRouter @@ -62,6 +63,8 @@ class OnboardingWalletFragment : internal val bindingSeedPhrase: LayoutOnboardingSeedPhraseBinding by lazy { binding.onboardingSeedPhraseContainer } + private val canSkipBackup by lazy { arguments?.getBoolean(OnboardingRouter.CAN_SKIP_BACKUP) ?: true } + private val seedPhraseStateHandler: OnboardingSeedPhraseStateHandler = OnboardingSeedPhraseStateHandler() private val seedPhraseViewModel by viewModels() @@ -255,7 +258,7 @@ class OnboardingWalletFragment : btnWalletAlternativeAction.text = getText(R.string.onboarding_button_skip_backup) btnWalletAlternativeAction.setOnClickListener { store.dispatch(BackupAction.SkipBackup) } - btnWalletAlternativeAction.show(state.canSkipBackup) + btnWalletAlternativeAction.show(state.canSkipBackup && canSkipBackup) } animator.showBackupIntro(state) } diff --git a/features/onboarding/src/main/java/com/tangem/feature/onboarding/di/OnboardingRouterModule.kt b/features/onboarding/src/main/java/com/tangem/feature/onboarding/di/OnboardingRouterModule.kt new file mode 100644 index 0000000000..acc54e7b9b --- /dev/null +++ b/features/onboarding/src/main/java/com/tangem/feature/onboarding/di/OnboardingRouterModule.kt @@ -0,0 +1,20 @@ +package com.tangem.feature.onboarding.di + +import com.tangem.feature.onboarding.navigation.DefaultOnboardingRouter +import com.tangem.feature.onboarding.navigation.OnboardingRouter +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.components.ActivityComponent +import dagger.hilt.android.scopes.ActivityScoped + +@Module +@InstallIn(ActivityComponent::class) +internal object OnboardingRouterModule { + + @Provides + @ActivityScoped + fun provideOnboardingRouter(): OnboardingRouter { + return DefaultOnboardingRouter() + } +} \ No newline at end of file diff --git a/features/onboarding/src/main/java/com/tangem/feature/onboarding/navigation/DefaultOnboardingRouter.kt b/features/onboarding/src/main/java/com/tangem/feature/onboarding/navigation/DefaultOnboardingRouter.kt new file mode 100644 index 0000000000..0d34ac7cd3 --- /dev/null +++ b/features/onboarding/src/main/java/com/tangem/feature/onboarding/navigation/DefaultOnboardingRouter.kt @@ -0,0 +1,3 @@ +package com.tangem.feature.onboarding.navigation + +class DefaultOnboardingRouter : OnboardingRouter \ No newline at end of file diff --git a/features/onboarding/src/main/java/com/tangem/feature/onboarding/navigation/OnboardingRouter.kt b/features/onboarding/src/main/java/com/tangem/feature/onboarding/navigation/OnboardingRouter.kt new file mode 100644 index 0000000000..5704da9b7f --- /dev/null +++ b/features/onboarding/src/main/java/com/tangem/feature/onboarding/navigation/OnboardingRouter.kt @@ -0,0 +1,12 @@ +package com.tangem.feature.onboarding.navigation + +/** + * Onboarding router + */ +// TODO: Move to onboarding api module [REDACTED_JIRA] +interface OnboardingRouter { + + companion object { + const val CAN_SKIP_BACKUP = "onboarding_wallet_can_skip_backup" + } +} \ No newline at end of file diff --git a/features/wallet/impl/build.gradle.kts b/features/wallet/impl/build.gradle.kts index b48335e242..d95c4421f7 100644 --- a/features/wallet/impl/build.gradle.kts +++ b/features/wallet/impl/build.gradle.kts @@ -66,6 +66,9 @@ dependencies { implementation(projects.domain.appCurrency.models) implementation(projects.domain.balanceHiding) + //TODO: Create api/impl modules for onboarding [REDACTED_JIRA] + implementation(projects.features.onboarding) + /** Feature Apis */ implementation(projects.features.wallet.api) implementation(projects.features.tokendetails.api) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/DefaultWalletRouter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/DefaultWalletRouter.kt index 6ad00bcf52..198df54ab0 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/DefaultWalletRouter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/DefaultWalletRouter.kt @@ -21,6 +21,7 @@ import com.tangem.core.navigation.NavigationAction import com.tangem.core.navigation.ReduxNavController import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.feature.onboarding.navigation.OnboardingRouter import com.tangem.feature.wallet.presentation.WalletFragment import com.tangem.feature.wallet.presentation.organizetokens.OrganizeTokensScreen import com.tangem.feature.wallet.presentation.organizetokens.OrganizeTokensViewModel @@ -100,7 +101,12 @@ internal class DefaultWalletRouter(private val reduxNavController: ReduxNavContr } override fun openOnboardingScreen() { - reduxNavController.navigate(action = NavigationAction.NavigateTo(AppScreen.OnboardingWallet)) + reduxNavController.navigate( + action = NavigationAction.NavigateTo( + screen = AppScreen.OnboardingWallet, + bundle = bundleOf(OnboardingRouter.CAN_SKIP_BACKUP to false), + ), + ) } override fun openTxHistoryWebsite(url: String) { From 04195a9f123c538ad4e788e94041a5d655569576 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 9 Oct 2023 18:54:39 +0300 Subject: [PATCH 170/242] Updated on 2026-08-14 --- .../feature/wallet/presentation/WalletFragment.kt | 2 +- .../wallet/presentation/router/DefaultWalletRouter.kt | 11 +++++------ .../wallet/presentation/router/InnerWalletRouter.kt | 5 ++--- 3 files changed, 8 insertions(+), 10 deletions(-) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/WalletFragment.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/WalletFragment.kt index 91a98afe61..e8c0881cfe 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/WalletFragment.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/WalletFragment.kt @@ -38,7 +38,7 @@ internal class WalletFragment : ComposeFragment() { setSystemBarsColor(systemBarsColor) } - _walletRouter.Initialize(fragmentManager = requireActivity().supportFragmentManager) + _walletRouter.Initialize(onFinish = requireActivity()::finish) } companion object { diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/DefaultWalletRouter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/DefaultWalletRouter.kt index 198df54ab0..08206005d7 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/DefaultWalletRouter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/DefaultWalletRouter.kt @@ -7,7 +7,6 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.core.os.bundleOf import androidx.fragment.app.Fragment -import androidx.fragment.app.FragmentManager import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation.NavHostController @@ -34,13 +33,13 @@ import kotlin.properties.Delegates internal class DefaultWalletRouter(private val reduxNavController: ReduxNavController) : InnerWalletRouter { private var navController: NavHostController by Delegates.notNull() - private var fragmentManager: FragmentManager by Delegates.notNull() + private var onFinish: () -> Unit = {} override fun getEntryFragment(): Fragment = WalletFragment.create() @Composable - override fun Initialize(fragmentManager: FragmentManager) { - this.fragmentManager = fragmentManager + override fun Initialize(onFinish: () -> Unit) { + this.onFinish = onFinish NavHost( navController = rememberNavController().apply { navController = this }, @@ -80,10 +79,10 @@ internal class DefaultWalletRouter(private val reduxNavController: ReduxNavContr * If backstack contains only NavGraph entry and wallet screen entry then we close the wallet fragment. */ if (navController.currentBackStack.value.size == BACKSTACK_ENTRY_COUNT_TO_CLOSE_WALLET_SCREEN) { - if (screen != null) { + if (screen == AppScreen.Home) { reduxNavController.navigate(action = NavigationAction.PopBackTo(screen)) } else { - fragmentManager.popBackStack() + onFinish.invoke() } } else { navController.popBackStack() diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/InnerWalletRouter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/InnerWalletRouter.kt index 07a2e8a80b..8234b7a917 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/InnerWalletRouter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/InnerWalletRouter.kt @@ -2,7 +2,6 @@ package com.tangem.feature.wallet.presentation.router import androidx.compose.runtime.Composable import androidx.compose.runtime.Stable -import androidx.fragment.app.FragmentManager import com.tangem.core.navigation.AppScreen import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.wallets.models.UserWalletId @@ -22,11 +21,11 @@ internal interface InnerWalletRouter : WalletRouter { /** * Initialize router * - * @param fragmentManager fragment manager + * @param onFinish finish activity callback */ @Suppress("TopLevelComposableFunctions") @Composable - fun Initialize(fragmentManager: FragmentManager) + fun Initialize(onFinish: () -> Unit) /** Pop back stack */ fun popBackStack(screen: AppScreen? = null) From fc0da8e978e8d1beb10a40d426958327bc7694f7 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 9 Oct 2023 15:48:05 +0300 Subject: [PATCH 171/242] Updated on 2026-08-14 --- .../viewmodels/AddCustomTokenViewModel.kt | 69 ++++++++++++++++++- 1 file changed, 68 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/viewmodels/AddCustomTokenViewModel.kt b/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/viewmodels/AddCustomTokenViewModel.kt index 660be89b17..16d07a40ae 100644 --- a/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/viewmodels/AddCustomTokenViewModel.kt +++ b/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/viewmodels/AddCustomTokenViewModel.kt @@ -22,7 +22,10 @@ import com.tangem.domain.common.extensions.* import com.tangem.domain.common.util.cardTypesResolver import com.tangem.domain.common.util.derivationStyleProvider import com.tangem.domain.features.addCustomToken.CustomCurrency +import com.tangem.domain.tokens.GetCryptoCurrenciesUseCase +import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.wallets.usecase.GetSelectedWalletUseCase +import com.tangem.features.wallet.featuretoggles.WalletFeatureToggles import com.tangem.tap.domain.model.WalletDataModel import com.tangem.tap.features.customtoken.impl.domain.CustomTokenInteractor import com.tangem.tap.features.customtoken.impl.domain.models.FoundToken @@ -59,14 +62,16 @@ import javax.inject.Inject * [REDACTED_AUTHOR] */ -@Suppress("LargeClass") +@Suppress("LargeClass", "LongParameterList") @HiltViewModel internal class AddCustomTokenViewModel @Inject constructor( analyticsEventHandler: AnalyticsEventHandler, featureRouter: CustomTokenRouter, + getCurrenciesUseCase: GetCryptoCurrenciesUseCase, private val featureInteractor: CustomTokenInteractor, private val dispatchers: AppCoroutineDispatcherProvider, private val getSelectedWalletUseCase: GetSelectedWalletUseCase, + private val walletFeatureToggles: WalletFeatureToggles, ) : ViewModel(), DefaultLifecycleObserver { private val analyticsSender = AddCustomTokenAnalyticsSender(analyticsEventHandler) @@ -74,12 +79,30 @@ internal class AddCustomTokenViewModel @Inject constructor( private val testActionsHandler = TestActionsHandler() private val formStateBuilder = FormStateBuilder() + private var currentCryptoCurrencies: List = emptyList() + /** Screen state */ var uiState by mutableStateOf(getInitialUiState()) private set private var foundToken: FoundToken? = null + init { + if (walletFeatureToggles.isRedesignedScreenEnabled) { + viewModelScope.launch(dispatchers.main) { + currentCryptoCurrencies = getSelectedWalletUseCase().fold( + ifLeft = { emptyList() }, + ifRight = { selectedWallet -> + getCurrenciesUseCase(selectedWallet.walletId).fold( + ifLeft = { emptyList() }, + ifRight = { it }, + ) + }, + ) + } + } + } + override fun onCreate(owner: LifecycleOwner) { analyticsSender.sendWhenScreenOpened() } @@ -564,6 +587,33 @@ internal class AddCustomTokenViewModel @Inject constructor( } private fun isTokenAlreadyAdded(): Boolean { + return if (walletFeatureToggles.isRedesignedScreenEnabled) { + isTokenAlreadyAddedNew() + } else { + isTokenAlreadyAddedOld() + } + } + + private fun isTokenAlreadyAddedNew(): Boolean { + return currentCryptoCurrencies + .filterIsInstance() + .any { token -> + val contractAddress = uiState.form.contractAddressInputField.value + val networkSelectorValue = uiState.form.networkSelectorField.selectedItem.blockchain + val networkId = Blockchain.fromNetworkId(networkSelectorValue.toNetworkId())?.id + + val savedTokenId = if (token.isCustom) null else token.id.value + + val sameId = foundToken?.id == savedTokenId + val sameAddress = contractAddress == token.contractAddress + val sameBlockchain = networkId == token.network.id.value + val isSameDerivationPath = getDerivationPath()?.rawPath == token.network.derivationPath.value + + sameId && sameAddress && sameBlockchain && isSameDerivationPath + } + } + + private fun isTokenAlreadyAddedOld(): Boolean { return store.state.walletState.walletsStores .map { walletStore -> walletStore.walletsData.map(WalletDataModel::currency) } .flatten() @@ -581,6 +631,23 @@ internal class AddCustomTokenViewModel @Inject constructor( } private fun isBlockchainAlreadyAdded(): Boolean { + return if (walletFeatureToggles.isRedesignedScreenEnabled) { + isBlockchainAlreadyAddedNew() + } else { + isBlockchainAlreadyAddedOld() + } + } + + private fun isBlockchainAlreadyAddedNew(): Boolean { + return currentCryptoCurrencies + .filterIsInstance() + .any { coin -> + coin.network.id.value == uiState.form.networkSelectorField.selectedItem.blockchain.id && + coin.network.derivationPath.value == getDerivationPath()?.rawPath + } + } + + private fun isBlockchainAlreadyAddedOld(): Boolean { return store.state.walletState.walletsStores .map { walletStore -> walletStore.walletsData.map(WalletDataModel::currency) } .flatten() From afcde269ce124dbdb4beb0b319608abb62d28588 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 10 Oct 2023 14:50:19 +0800 Subject: [PATCH 172/242] Updated on 2026-08-14 --- .../java/com/tangem/tap/TapApplication.kt | 74 ++++++++++--------- .../common/feedback/AdditionalFeedbackInfo.kt | 29 +------- .../common/redux/global/GlobalMiddleware.kt | 23 +++++- .../tap/di/domain/WalletsDomainModule.kt | 8 ++ .../com/tangem/tap/domain/TapWalletManager.kt | 3 +- .../features/details/redux/DetailsAction.kt | 1 + .../details/redux/DetailsMiddleware.kt | 9 ++- .../features/details/redux/DetailsReducer.kt | 2 +- .../details/ui/details/DetailsFragment.kt | 6 +- .../details/ui/details/DetailsViewModel.kt | 10 ++- .../features/onboarding/OnboardingHelper.kt | 62 ++++++++-------- .../twins/redux/TwinCardsMiddleware.kt | 13 +++- .../saveWallet/redux/SaveWalletMiddleware.kt | 5 +- .../tap/proxy/redux/DaggerGraphState.kt | 2 + .../tangem/datasource/di/WalletsDataModule.kt | 26 +++++++ .../userwallet/ShouldSaveUserWalletStore.kt | 24 ++++++ .../preferences/PreferencesDataSource.kt | 7 -- data/wallets/build.gradle.kts | 1 + .../data/wallets/DefaultWalletsRepository.kt | 21 +++++- .../data/wallets/di/WalletsDataModule.kt | 6 +- .../wallets/repository/WalletsRepository.kt | 10 ++- .../ShouldSaveUserWalletsSyncUseCase.kt | 8 ++ .../usecase/ShouldSaveUserWalletsUseCase.kt | 3 +- .../wallet/viewmodels/WalletViewModel.kt | 21 ++++-- 24 files changed, 243 insertions(+), 131 deletions(-) create mode 100644 core/datasource/src/main/java/com/tangem/datasource/di/WalletsDataModule.kt create mode 100644 core/datasource/src/main/java/com/tangem/datasource/local/userwallet/ShouldSaveUserWalletStore.kt create mode 100644 domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/ShouldSaveUserWalletsSyncUseCase.kt diff --git a/app/src/main/java/com/tangem/tap/TapApplication.kt b/app/src/main/java/com/tangem/tap/TapApplication.kt index 919b23292e..599ad22aa3 100644 --- a/app/src/main/java/com/tangem/tap/TapApplication.kt +++ b/app/src/main/java/com/tangem/tap/TapApplication.kt @@ -34,6 +34,7 @@ import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.wallets.legacy.WalletManagersRepository +import com.tangem.domain.wallets.repository.WalletsRepository import com.tangem.features.tokendetails.featuretoggles.TokenDetailsFeatureToggles import com.tangem.features.wallet.featuretoggles.WalletFeatureToggles import com.tangem.tap.common.analytics.AnalyticsFactory @@ -44,6 +45,7 @@ import com.tangem.tap.common.analytics.handlers.appsFlyer.AppsFlyerAnalyticsHand import com.tangem.tap.common.analytics.handlers.firebase.FirebaseAnalyticsHandler import com.tangem.tap.common.analytics.topup.TopUpController import com.tangem.tap.common.chat.ChatManager +import com.tangem.tap.common.extensions.dispatchOnMain import com.tangem.tap.common.feedback.AdditionalFeedbackInfo import com.tangem.tap.common.feedback.FeedbackManager import com.tangem.tap.common.images.createCoilImageLoader @@ -207,34 +209,15 @@ internal class TapApplication : Application(), ImageLoaderFactory { @Inject lateinit var getAppThemeModeUseCase: GetAppThemeModeUseCase + + @Inject + lateinit var walletsRepository: WalletsRepository // endregion Injected override fun onCreate() { super.onCreate() - store = Store( - reducer = { action, state -> appReducer(action, state, appStateHolder) }, - middleware = AppState.getMiddleware(), - state = AppState( - daggerGraphState = DaggerGraphState( - assetReader = assetReader, - networkConnectionManager = networkConnectionManager, - customTokenFeatureToggles = customTokenFeatureToggles, - walletFeatureToggles = walletFeatureToggles, - walletConnectRepository = walletConnect2Repository, - walletConnectSessionsRepository = walletConnectSessionsRepository, - tokenDetailsFeatureToggles = tokenDetailsFeatureToggles, - scanCardProcessor = scanCardProcessor, - appCurrencyRepository = appCurrencyRepository, - walletManagersFacade = walletManagersFacade, - appStateHolder = appStateHolder, - currenciesRepository = currenciesRepository, - appThemeModeRepository = appThemeModeRepository, - balanceHidingRepository = balanceHidingRepository, - detailsFeatureToggles = detailsFeatureToggles, - ), - ), - ) + store = createReduxStore() if (BuildConfig.DEBUG) { Logger.addLogAdapter(AndroidLogAdapter(TimberFormatStrategy())) @@ -254,17 +237,17 @@ internal class TapApplication : Application(), ImageLoaderFactory { preferencesStorage = preferencesDataSource walletConnectRepository = WalletConnectRepository(this) - val configLoader = FeaturesLocalLoader(assetReader, MoshiConverter.sdkMoshi, BuildConfig.ENVIRONMENT) - initUserWalletsListManager() - // TODO: Try to performance and user experience. // [REDACTED_JIRA] runBlocking { + initUserWalletsListManager() featureTogglesManager.init() appRatingRepository.initialize() + walletsRepository.initialize() // learn2earnInteractor.init() } + val configLoader = FeaturesLocalLoader(assetReader, MoshiConverter.sdkMoshi, BuildConfig.ENVIRONMENT) initConfigManager(configLoader, ::initWithConfigDependency) initWarningMessagesManager() @@ -296,6 +279,33 @@ internal class TapApplication : Application(), ImageLoaderFactory { walletConnect2Repository.init(projectId = configManager.config.walletConnectProjectId) } + private fun createReduxStore(): Store { + return Store( + reducer = { action, state -> appReducer(action, state, appStateHolder) }, + middleware = AppState.getMiddleware(), + state = AppState( + daggerGraphState = DaggerGraphState( + assetReader = assetReader, + networkConnectionManager = networkConnectionManager, + customTokenFeatureToggles = customTokenFeatureToggles, + walletFeatureToggles = walletFeatureToggles, + walletConnectRepository = walletConnect2Repository, + walletConnectSessionsRepository = walletConnectSessionsRepository, + tokenDetailsFeatureToggles = tokenDetailsFeatureToggles, + scanCardProcessor = scanCardProcessor, + appCurrencyRepository = appCurrencyRepository, + walletManagersFacade = walletManagersFacade, + appStateHolder = appStateHolder, + currenciesRepository = currenciesRepository, + appThemeModeRepository = appThemeModeRepository, + balanceHidingRepository = balanceHidingRepository, + detailsFeatureToggles = detailsFeatureToggles, + walletsRepository = walletsRepository, + ), + ), + ) + } + private fun initTopUpController() { val topUpController = TopUpController( scanResponseProvider = { @@ -356,11 +366,7 @@ internal class TapApplication : Application(), ImageLoaderFactory { store: Store, ) { fun initAdditionalFeedbackInfo(context: Context): AdditionalFeedbackInfo { - return AdditionalFeedbackInfo( - userWalletsListManager = userWalletsListManager, - walletManagersFacade = walletManagersFacade, - walletFeatureToggles = walletFeatureToggles, - ).apply { + return AdditionalFeedbackInfo().apply { appVersion = try { // TODO don't use deprecated method val pInfo = context.packageManager.getPackageInfo(context.packageName, 0) @@ -403,13 +409,13 @@ internal class TapApplication : Application(), ImageLoaderFactory { store.dispatch(GlobalAction.SetWarningManager(WarningMessagesManager())) } - private fun initUserWalletsListManager() { - val manager = if (preferencesStorage.shouldSaveUserWallets) { + private suspend fun initUserWalletsListManager() { + val manager = if (walletsRepository.shouldSaveUserWalletsSync()) { UserWalletsListManager.provideBiometricImplementation(applicationContext) } else { UserWalletsListManager.provideRuntimeImplementation() } - store.dispatch(GlobalAction.UpdateUserWalletsListManager(manager)) + store.dispatchOnMain(GlobalAction.UpdateUserWalletsListManager(manager)) } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/feedback/AdditionalFeedbackInfo.kt b/app/src/main/java/com/tangem/tap/common/feedback/AdditionalFeedbackInfo.kt index a3407ad4d2..e1d67df755 100644 --- a/app/src/main/java/com/tangem/tap/common/feedback/AdditionalFeedbackInfo.kt +++ b/app/src/main/java/com/tangem/tap/common/feedback/AdditionalFeedbackInfo.kt @@ -6,35 +6,10 @@ import com.tangem.blockchain.common.address.Address import com.tangem.domain.models.scan.CardDTO import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.userwallets.UserWalletIdBuilder -import com.tangem.domain.walletmanager.WalletManagersFacade -import com.tangem.domain.wallets.legacy.UserWalletsListManager -import com.tangem.features.wallet.featuretoggles.WalletFeatureToggles import com.tangem.tap.common.extensions.stripZeroPlainString -import com.tangem.tap.scope -import kotlinx.coroutines.flow.distinctUntilChanged -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.* -class AdditionalFeedbackInfo( - userWalletsListManager: UserWalletsListManager, - walletManagersFacade: WalletManagersFacade, - walletFeatureToggles: WalletFeatureToggles, -) { - - init { - if (walletFeatureToggles.isRedesignedScreenEnabled) { - userWalletsListManager.selectedUserWallet - .distinctUntilChanged() - .onEach { userWallet -> - setCardInfo(data = userWallet.scanResponse) - - walletManagersFacade.getAll(userWalletId = userWallet.walletId) - .onEach(::setWalletsInfo) - .launchIn(scope) - } - .launchIn(scope) - } - } +class AdditionalFeedbackInfo { class EmailWalletInfo( var blockchain: Blockchain = Blockchain.Unknown, diff --git a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalMiddleware.kt b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalMiddleware.kt index e7421cf777..9f6d1d71c0 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalMiddleware.kt @@ -29,7 +29,8 @@ import com.tangem.tap.proxy.redux.DaggerGraphState import com.tangem.tap.scope import com.tangem.tap.store import com.tangem.tap.walletCurrenciesManager -import kotlinx.coroutines.flow.firstOrNull +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch import org.rekotlin.Action import org.rekotlin.DispatchFunction @@ -174,6 +175,26 @@ private fun handleAction(action: Action, appState: () -> AppState?, dispatch: Di is GlobalAction.SetTopUpController -> { walletCurrenciesManager.addListener(action.topUpController) } + is GlobalAction.UpdateUserWalletsListManager -> { + /* + * If UserWalletsListManager's implementation is changed, + * then all selectedUserWallet's observers is became irrelevant + */ + action.manager.selectedUserWallet + .distinctUntilChanged() + .onEach { userWallet -> + store.state.globalState.feedbackManager?.infoHolder?.let { infoHolder -> + infoHolder.setCardInfo(data = userWallet.scanResponse) + + store.state.daggerGraphState.get(DaggerGraphState::walletManagersFacade) + .getAll(userWalletId = userWallet.walletId) + .onEach(infoHolder::setWalletsInfo) + .launchIn(scope) + } + } + .flowOn(Dispatchers.IO) + .launchIn(scope) + } } } diff --git a/app/src/main/java/com/tangem/tap/di/domain/WalletsDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/WalletsDomainModule.kt index 2d12ae7b0b..e491a32ba9 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/WalletsDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/WalletsDomainModule.kt @@ -68,6 +68,14 @@ internal object WalletsDomainModule { return DeleteWalletUseCase(walletsStateHolder = walletsStateHolder) } + @Provides + @ViewModelScoped + fun providesShouldSaveUserWalletsSyncUseCase( + walletsRepository: WalletsRepository, + ): ShouldSaveUserWalletsSyncUseCase { + return ShouldSaveUserWalletsSyncUseCase(walletsRepository = walletsRepository) + } + @Provides @ViewModelScoped fun providesShouldSaveUserWalletsUseCase(walletsRepository: WalletsRepository): ShouldSaveUserWalletsUseCase { diff --git a/app/src/main/java/com/tangem/tap/domain/TapWalletManager.kt b/app/src/main/java/com/tangem/tap/domain/TapWalletManager.kt index 729e96fe27..9edc4ee417 100644 --- a/app/src/main/java/com/tangem/tap/domain/TapWalletManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/TapWalletManager.kt @@ -81,7 +81,8 @@ class TapWalletManager( store.dispatchWalletAction(action = WalletAction.UserWalletChanged(userWallet)) store.dispatchWalletAction( action = WalletAction.UpdateCanSaveUserWallets( - canSaveUserWallets = preferencesStorage.shouldSaveUserWallets, + canSaveUserWallets = store.state.daggerGraphState.get(DaggerGraphState::walletsRepository) + .shouldSaveUserWalletsSync(), ), ) store.dispatch(TwinCardsAction.IfTwinsPrepareState(scanResponse)) diff --git a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsAction.kt b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsAction.kt index 0190c0aa97..338e8f255c 100644 --- a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsAction.kt +++ b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsAction.kt @@ -14,6 +14,7 @@ sealed class DetailsAction : Action { data class PrepareScreen( val scanResponse: ScanResponse, val darkThemeSwitchEnabled: Boolean, + val shouldSaveUserWallets: Boolean, ) : DetailsAction() object ReCreateTwinsWallet : DetailsAction() diff --git a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsMiddleware.kt b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsMiddleware.kt index bdf4867874..3dac94f996 100644 --- a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsMiddleware.kt @@ -291,7 +291,8 @@ class DetailsMiddleware { private fun toggleSaveWallets(state: DetailsState, enable: Boolean) = scope.launch { // Nothing to change - if (preferencesStorage.shouldSaveUserWallets == enable) { + val walletsRepository = store.state.daggerGraphState.get(DaggerGraphState::walletsRepository) + if (walletsRepository.shouldSaveUserWalletsSync() == enable) { store.dispatchWithMain(DetailsAction.AppSettings.SwitchPrivacySetting.Success) return@launch } @@ -379,7 +380,8 @@ class DetailsMiddleware { Analytics.send(Settings.AppSettings.SaveWalletSwitcherChanged(AnalyticsParam.OnOffState.On)) preferencesStorage.shouldShowSaveUserWalletScreen = false - preferencesStorage.shouldSaveUserWallets = true + store.state.daggerGraphState.get(DaggerGraphState::walletsRepository) + .saveShouldSaveUserWallets(item = true) store.dispatchWithMain(WalletAction.UpdateCanSaveUserWallets(canSaveUserWallets = true)) } @@ -395,7 +397,8 @@ class DetailsMiddleware { Analytics.send(Settings.AppSettings.SaveWalletSwitcherChanged(AnalyticsParam.OnOffState.Off)) deleteSavedAccessCodes() updateUserWalletsListManager(enableUserWalletsSaving = false) - preferencesStorage.shouldSaveUserWallets = false + store.state.daggerGraphState.get(DaggerGraphState::walletsRepository) + .saveShouldSaveUserWallets(item = false) store.dispatchWithMain(WalletAction.UpdateCanSaveUserWallets(canSaveUserWallets = true)) store.dispatchWithMain(NavigationAction.PopBackTo(AppScreen.Home)) diff --git a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsReducer.kt b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsReducer.kt index a3dc4cfe98..ff94b5cc7a 100644 --- a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsReducer.kt +++ b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsReducer.kt @@ -72,7 +72,7 @@ private fun handlePrepareScreen(action: DetailsAction.PrepareScreen): DetailsSta createBackupAllowed = action.scanResponse.card.backupStatus == CardDTO.BackupStatus.NoBackup, appSettingsState = AppSettingsState( isBiometricsAvailable = tangemSdkManager.canUseBiometry, - saveWallets = preferencesStorage.shouldSaveUserWallets, + saveWallets = action.shouldSaveUserWallets, saveAccessCodes = preferencesStorage.shouldSaveAccessCodes, selectedFiatCurrency = store.state.globalState.appCurrency, selectedThemeMode = runBlocking { diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsFragment.kt b/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsFragment.kt index 3d819cd6d0..d3d215551c 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsFragment.kt @@ -7,6 +7,7 @@ import com.tangem.core.analytics.Analytics import com.tangem.core.navigation.NavigationAction import com.tangem.core.ui.screen.ComposeFragment import com.tangem.core.ui.theme.AppThemeModeHolder +import com.tangem.domain.wallets.repository.WalletsRepository import com.tangem.tap.common.analytics.events.Settings import com.tangem.tap.features.details.DarkThemeFeatureToggle import com.tangem.tap.features.details.redux.DetailsState @@ -24,11 +25,14 @@ internal class DetailsFragment : ComposeFragment(), StoreSubscriber, private val darkThemeFeatureToggle: DarkThemeFeatureToggle, -) { // TODO: change to Android ViewModel + private val walletsRepository: WalletsRepository, +) { var detailsScreenState: MutableState = mutableStateOf(updateState(store.state.detailsState)) private set @@ -186,8 +189,9 @@ internal class DetailsViewModel( .onEach { selectedUserWallet -> store.dispatchWithMain( DetailsAction.PrepareScreen( - selectedUserWallet.scanResponse, - darkThemeFeatureToggle.isDarkThemeEnabled, + scanResponse = selectedUserWallet.scanResponse, + darkThemeSwitchEnabled = darkThemeFeatureToggle.isDarkThemeEnabled, + shouldSaveUserWallets = walletsRepository.shouldSaveUserWalletsSync(), ), ) } diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingHelper.kt b/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingHelper.kt index baf5ed2fcf..29c6d0fe98 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingHelper.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingHelper.kt @@ -17,6 +17,7 @@ import com.tangem.tap.common.extensions.onUserWalletSelected import com.tangem.tap.common.extensions.removeContext import com.tangem.tap.common.extensions.setContext import com.tangem.tap.features.saveWallet.redux.SaveWalletAction +import com.tangem.tap.proxy.redux.DaggerGraphState import kotlinx.coroutines.delay import kotlinx.coroutines.launch import timber.log.Timber @@ -70,40 +71,39 @@ object OnboardingHelper { backupCardsIds: List? = null, ) { Analytics.setContext(scanResponse) - when { - // When should save user wallets, then save card without navigate to save wallet screen - preferencesStorage.shouldSaveUserWallets -> scope.launch { - proceedWithScanResponse(scanResponse, backupCardsIds) + scope.launch { + when { + // When should save user wallets, then save card without navigate to save wallet screen + store.state.daggerGraphState.get(DaggerGraphState::walletsRepository).shouldSaveUserWalletsSync() -> { + proceedWithScanResponse(scanResponse, backupCardsIds) - store.dispatchOnMain( - SaveWalletAction.ProvideBackupInfo( - scanResponse = scanResponse, - accessCode = accessCode, - backupCardsIds = backupCardsIds?.toSet(), - ), - ) - store.dispatchOnMain(SaveWalletAction.Save) - } - // When should not save user wallets but device has biometry and save wallet screen has not been shown, - // then open save wallet screen - tangemSdkManager.canUseBiometry && - preferencesStorage.shouldShowSaveUserWalletScreen -> scope.launch { - proceedWithScanResponse(scanResponse, backupCardsIds) + store.dispatchOnMain( + SaveWalletAction.ProvideBackupInfo( + scanResponse = scanResponse, + accessCode = accessCode, + backupCardsIds = backupCardsIds?.toSet(), + ), + ) + store.dispatchOnMain(SaveWalletAction.Save) + } + // When should not save user wallets but device has biometry and save wallet screen has not been shown, + // then open save wallet screen + tangemSdkManager.canUseBiometry && preferencesStorage.shouldShowSaveUserWalletScreen -> { + proceedWithScanResponse(scanResponse, backupCardsIds) - delay(timeMillis = 1_200) + delay(timeMillis = 1_200) - store.dispatchOnMain( - SaveWalletAction.ProvideBackupInfo( - scanResponse = scanResponse, - accessCode = accessCode, - backupCardsIds = backupCardsIds?.toSet(), - ), - ) - store.dispatchOnMain(NavigationAction.NavigateTo(AppScreen.SaveWallet)) - } - // If device has no biometry and save wallet screen has been shown, then go through old scenario - else -> scope.launch { - proceedWithScanResponse(scanResponse, backupCardsIds) + store.dispatchOnMain( + SaveWalletAction.ProvideBackupInfo( + scanResponse = scanResponse, + accessCode = accessCode, + backupCardsIds = backupCardsIds?.toSet(), + ), + ) + store.dispatchOnMain(NavigationAction.NavigateTo(AppScreen.SaveWallet)) + } + // If device has no biometry and save wallet screen has been shown, then go through old scenario + else -> proceedWithScanResponse(scanResponse, backupCardsIds) } } diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/redux/TwinCardsMiddleware.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/redux/TwinCardsMiddleware.kt index f68fa7e806..ee8ba338c9 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/redux/TwinCardsMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/redux/TwinCardsMiddleware.kt @@ -29,6 +29,7 @@ import com.tangem.tap.features.wallet.models.Currency import com.tangem.tap.features.wallet.redux.ProgressState import com.tangem.tap.features.wallet.redux.WalletAction import com.tangem.tap.features.wallet.redux.models.WalletDialog +import com.tangem.tap.proxy.redux.DaggerGraphState import kotlinx.coroutines.delay import kotlinx.coroutines.launch import org.rekotlin.Action @@ -292,10 +293,14 @@ private fun handle(action: Action, dispatch: DispatchFunction) { OnboardingHelper.trySaveWalletAndNavigateToWalletScreen(scanResponse) } CreateTwinWalletMode.RecreateWallet -> { - if (preferencesStorage.shouldSaveUserWallets) { - OnboardingHelper.trySaveWalletAndNavigateToWalletScreen(scanResponse) - } else { - store.dispatchOnMain(NavigationAction.PopBackTo(AppScreen.Home)) + scope.launch { + val walletsRepository = store.state.daggerGraphState.get(DaggerGraphState::walletsRepository) + + if (walletsRepository.shouldSaveUserWalletsSync()) { + OnboardingHelper.trySaveWalletAndNavigateToWalletScreen(scanResponse) + } else { + store.dispatchOnMain(NavigationAction.PopBackTo(AppScreen.Home)) + } } } } diff --git a/app/src/main/java/com/tangem/tap/features/saveWallet/redux/SaveWalletMiddleware.kt b/app/src/main/java/com/tangem/tap/features/saveWallet/redux/SaveWalletMiddleware.kt index 29e8ad6a02..f68fa4579e 100644 --- a/app/src/main/java/com/tangem/tap/features/saveWallet/redux/SaveWalletMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/saveWallet/redux/SaveWalletMiddleware.kt @@ -107,10 +107,11 @@ internal class SaveWalletMiddleware { store.dispatchWithMain(SaveWalletAction.Save.Error(error)) } .doOnSuccess { - preferencesStorage.shouldSaveUserWallets = true + store.state.daggerGraphState.get(DaggerGraphState::walletsRepository) + .saveShouldSaveUserWallets(item = true) + // Enable saving access codes only if this is the first time user save the wallet if (isFirstSavedWallet) { - preferencesStorage.shouldSaveAccessCodes = true store.state.daggerGraphState.get(DaggerGraphState::cardSdkConfigRepository) .setAccessCodeRequestPolicy( isBiometricsRequestPolicy = userWallet.hasAccessCode, diff --git a/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphState.kt b/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphState.kt index cd68dae6e3..441afa259c 100644 --- a/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphState.kt +++ b/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphState.kt @@ -10,6 +10,7 @@ import com.tangem.domain.card.ScanCardUseCase import com.tangem.domain.card.repository.CardSdkConfigRepository import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.walletmanager.WalletManagersFacade +import com.tangem.domain.wallets.repository.WalletsRepository import com.tangem.features.tester.api.TesterRouter import com.tangem.features.tokendetails.featuretoggles.TokenDetailsFeatureToggles import com.tangem.features.tokendetails.navigation.TokenDetailsRouter @@ -44,6 +45,7 @@ data class DaggerGraphState( val appThemeModeRepository: AppThemeModeRepository? = null, val balanceHidingRepository: BalanceHidingRepository? = null, val detailsFeatureToggles: DetailsFeatureToggles? = null, + val walletsRepository: WalletsRepository? = null, // FIXME: It is used only for TokensList screen. Remove after refactoring of TokensList val currenciesRepository: CurrenciesRepository? = null, diff --git a/core/datasource/src/main/java/com/tangem/datasource/di/WalletsDataModule.kt b/core/datasource/src/main/java/com/tangem/datasource/di/WalletsDataModule.kt new file mode 100644 index 0000000000..61cdcacad3 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/di/WalletsDataModule.kt @@ -0,0 +1,26 @@ +package com.tangem.datasource.di + +import android.content.Context +import com.tangem.datasource.local.datastore.BooleanSharedPreferencesDataStore +import com.tangem.datasource.local.settings.* +import com.tangem.datasource.local.userwallet.DefaultShouldSaveUserWalletStore +import com.tangem.datasource.local.userwallet.ShouldSaveUserWalletStore +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.qualifiers.ApplicationContext +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal object WalletsDataModule { + + @Provides + @Singleton + fun provideShouldSaveUserWalletsStore(@ApplicationContext context: Context): ShouldSaveUserWalletStore { + return DefaultShouldSaveUserWalletStore( + store = BooleanSharedPreferencesDataStore(preferencesName = "tapPrefs", context = context), + ) + } +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/userwallet/ShouldSaveUserWalletStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/userwallet/ShouldSaveUserWalletStore.kt new file mode 100644 index 0000000000..cbc20ba792 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/local/userwallet/ShouldSaveUserWalletStore.kt @@ -0,0 +1,24 @@ +package com.tangem.datasource.local.userwallet + +import com.tangem.datasource.local.datastore.BooleanSharedPreferencesDataStore +import com.tangem.datasource.local.datastore.core.KeylessDataStoreDecorator +import kotlinx.coroutines.flow.Flow + +/** +[REDACTED_AUTHOR] + */ +interface ShouldSaveUserWalletStore { + + fun get(): Flow + + suspend fun getSyncOrNull(): Boolean? + + suspend fun store(item: Boolean) +} + +internal class DefaultShouldSaveUserWalletStore( + store: BooleanSharedPreferencesDataStore, +) : ShouldSaveUserWalletStore, KeylessDataStoreDecorator( + wrappedDataStore = store, + key = "saveUserWallets", +) \ No newline at end of file diff --git a/data/source/preferences/src/main/kotlin/com/tangem/data/source/preferences/PreferencesDataSource.kt b/data/source/preferences/src/main/kotlin/com/tangem/data/source/preferences/PreferencesDataSource.kt index ec8b43876b..218863ac9a 100644 --- a/data/source/preferences/src/main/kotlin/com/tangem/data/source/preferences/PreferencesDataSource.kt +++ b/data/source/preferences/src/main/kotlin/com/tangem/data/source/preferences/PreferencesDataSource.kt @@ -56,12 +56,6 @@ class PreferencesDataSource @Inject internal constructor(applicationContext: Con putBoolean(SAVE_WALLET_DIALOG_SHOWN_KEY, value) } - var shouldSaveUserWallets: Boolean - get() = preferences.getBoolean(SAVE_USER_WALLETS_KEY, false) - set(value) = preferences.edit { - putBoolean(SAVE_USER_WALLETS_KEY, value) - } - var shouldSaveAccessCodes: Boolean get() = preferences.getBoolean(SAVE_ACCESS_CODES_KEY, false) set(value) = preferences.edit { @@ -102,7 +96,6 @@ class PreferencesDataSource @Inject internal constructor(applicationContext: Con private const val ZENDESK_FIRST_LAUNCH_KEY = "chatFirstLaunchKey" private const val SPRINKLR_FIRST_LAUNCH_KEY = "sprinklrFirstLaunch" private const val SAVE_WALLET_DIALOG_SHOWN_KEY = "saveUserWalletShown" - private const val SAVE_USER_WALLETS_KEY = "saveUserWallets" private const val SAVE_ACCESS_CODES_KEY = "saveAccessCodes" private const val APPLICATION_STOPPED_KEY = "applicationStopped" private const val OPEN_WELCOME_ON_RESUME_KEY = "openWelcomeOnResume" diff --git a/data/wallets/build.gradle.kts b/data/wallets/build.gradle.kts index 7bc383e45b..f39f9b3c1e 100644 --- a/data/wallets/build.gradle.kts +++ b/data/wallets/build.gradle.kts @@ -11,6 +11,7 @@ android { } dependencies { + implementation(projects.core.datasource) implementation(projects.core.utils) implementation(projects.data.source.preferences) implementation(projects.domain.wallets) diff --git a/data/wallets/src/main/java/com/tangem/data/wallets/DefaultWalletsRepository.kt b/data/wallets/src/main/java/com/tangem/data/wallets/DefaultWalletsRepository.kt index 848735ca16..af326a1a45 100644 --- a/data/wallets/src/main/java/com/tangem/data/wallets/DefaultWalletsRepository.kt +++ b/data/wallets/src/main/java/com/tangem/data/wallets/DefaultWalletsRepository.kt @@ -1,16 +1,29 @@ package com.tangem.data.wallets -import com.tangem.data.source.preferences.PreferencesDataSource +import com.tangem.datasource.local.userwallet.ShouldSaveUserWalletStore import com.tangem.domain.wallets.repository.WalletsRepository import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.withContext internal class DefaultWalletsRepository( - private val preferencesDataSource: PreferencesDataSource, + private val shouldSaveUserWalletStore: ShouldSaveUserWalletStore, private val dispatchers: CoroutineDispatcherProvider, ) : WalletsRepository { - override suspend fun shouldSaveUserWallets(): Boolean { - return withContext(dispatchers.io) { preferencesDataSource.shouldSaveUserWallets } + override suspend fun initialize() { + withContext(dispatchers.io) { + shouldSaveUserWalletStore.getSyncOrNull() ?: shouldSaveUserWalletStore.store(item = false) + } + } + + override suspend fun shouldSaveUserWalletsSync(): Boolean { + return withContext(dispatchers.io) { shouldSaveUserWalletStore.getSyncOrNull() ?: false } + } + + override fun shouldSaveUserWallets(): Flow = shouldSaveUserWalletStore.get() + + override suspend fun saveShouldSaveUserWallets(item: Boolean) { + withContext(dispatchers.io) { shouldSaveUserWalletStore.store(item = item) } } } \ No newline at end of file diff --git a/data/wallets/src/main/java/com/tangem/data/wallets/di/WalletsDataModule.kt b/data/wallets/src/main/java/com/tangem/data/wallets/di/WalletsDataModule.kt index 3f418202cb..80ff2c2567 100644 --- a/data/wallets/src/main/java/com/tangem/data/wallets/di/WalletsDataModule.kt +++ b/data/wallets/src/main/java/com/tangem/data/wallets/di/WalletsDataModule.kt @@ -1,7 +1,7 @@ package com.tangem.data.wallets.di -import com.tangem.data.source.preferences.PreferencesDataSource import com.tangem.data.wallets.DefaultWalletsRepository +import com.tangem.datasource.local.userwallet.ShouldSaveUserWalletStore import com.tangem.domain.wallets.repository.WalletsRepository import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.Module @@ -17,11 +17,11 @@ object WalletsDataModule { @Provides @Singleton fun providesWalletsRepository( - preferencesDataSource: PreferencesDataSource, + shouldSaveUserWalletStore: ShouldSaveUserWalletStore, coroutineDispatcherProvider: CoroutineDispatcherProvider, ): WalletsRepository { return DefaultWalletsRepository( - preferencesDataSource = preferencesDataSource, + shouldSaveUserWalletStore = shouldSaveUserWalletStore, dispatchers = coroutineDispatcherProvider, ) } diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/repository/WalletsRepository.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/repository/WalletsRepository.kt index 7d5d65f233..6190b6d2e0 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/repository/WalletsRepository.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/repository/WalletsRepository.kt @@ -1,6 +1,14 @@ package com.tangem.domain.wallets.repository +import kotlinx.coroutines.flow.Flow + interface WalletsRepository { - suspend fun shouldSaveUserWallets(): Boolean + suspend fun initialize() + + suspend fun shouldSaveUserWalletsSync(): Boolean + + fun shouldSaveUserWallets(): Flow + + suspend fun saveShouldSaveUserWallets(item: Boolean) } \ No newline at end of file diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/ShouldSaveUserWalletsSyncUseCase.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/ShouldSaveUserWalletsSyncUseCase.kt new file mode 100644 index 0000000000..16efc0eadb --- /dev/null +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/ShouldSaveUserWalletsSyncUseCase.kt @@ -0,0 +1,8 @@ +package com.tangem.domain.wallets.usecase + +import com.tangem.domain.wallets.repository.WalletsRepository + +class ShouldSaveUserWalletsSyncUseCase(private val walletsRepository: WalletsRepository) { + + suspend operator fun invoke(): Boolean = walletsRepository.shouldSaveUserWalletsSync() +} \ No newline at end of file diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/ShouldSaveUserWalletsUseCase.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/ShouldSaveUserWalletsUseCase.kt index b21f04a003..fc62813050 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/ShouldSaveUserWalletsUseCase.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/ShouldSaveUserWalletsUseCase.kt @@ -1,8 +1,9 @@ package com.tangem.domain.wallets.usecase import com.tangem.domain.wallets.repository.WalletsRepository +import kotlinx.coroutines.flow.Flow class ShouldSaveUserWalletsUseCase(private val walletsRepository: WalletsRepository) { - suspend operator fun invoke(): Boolean = walletsRepository.shouldSaveUserWallets() + operator fun invoke(): Flow = walletsRepository.shouldSaveUserWallets() } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt index c06afd4d6b..e690d4303a 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt @@ -106,6 +106,7 @@ internal class WalletViewModel @Inject constructor( private val shouldShowSaveWalletScreenUseCase: ShouldShowSaveWalletScreenUseCase, private val canUseBiometryUseCase: CanUseBiometryUseCase, private val shouldSaveUserWalletsUseCase: ShouldSaveUserWalletsUseCase, + private val shouldSaveUserWalletsSyncUseCase: ShouldSaveUserWalletsSyncUseCase, private val isBalanceHiddenUseCase: IsBalanceHiddenUseCase, private val listenToFlipsUseCase: ListenToFlipsUseCase, private val removeCurrencyUseCase: RemoveCurrencyUseCase, @@ -182,12 +183,18 @@ internal class WalletViewModel @Inject constructor( } } - getWalletsUseCase() - .flowWithLifecycle(owner.lifecycle) - .distinctUntilChanged() - .onEach(::updateWallets) - .flowOn(dispatchers.io) - .launchIn(viewModelScope) + viewModelScope.launch(dispatchers.io) { + shouldSaveUserWalletsUseCase() + .flowWithLifecycle(owner.lifecycle) + .collectLatest { + getWalletsUseCase() + .flowWithLifecycle(owner.lifecycle) + .distinctUntilChanged() + .onEach(::updateWallets) + .flowOn(dispatchers.io) + .launchIn(viewModelScope) + } + } isBalanceHiddenUseCase() .flowWithLifecycle(owner.lifecycle) @@ -274,7 +281,7 @@ internal class WalletViewModel @Inject constructor( override fun onBackClick() { viewModelScope.launch(dispatchers.main) { - router.popBackStack(screen = if (shouldSaveUserWalletsUseCase()) AppScreen.Welcome else AppScreen.Home) + router.popBackStack(screen = if (shouldSaveUserWalletsSyncUseCase()) AppScreen.Welcome else AppScreen.Home) } } From 13ae7be53f7fe02cef98f514f13a6a427e2380ae Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 10 Oct 2023 13:59:53 +0300 Subject: [PATCH 173/242] Updated on 2026-08-14 --- .../details/ui/appcurrency/converter/CurrencyConverter.kt | 2 +- .../tangem/data/appcurrency/DefaultAppCurrencyRepository.kt | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/appcurrency/converter/CurrencyConverter.kt b/app/src/main/java/com/tangem/tap/features/details/ui/appcurrency/converter/CurrencyConverter.kt index 9e6df0ed22..445f24d3c8 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/appcurrency/converter/CurrencyConverter.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/appcurrency/converter/CurrencyConverter.kt @@ -7,7 +7,7 @@ import com.tangem.utils.converter.Converter internal class CurrencyConverter : Converter { override fun convert(value: AppCurrency): AppCurrencySelectorState.Currency { - val fullCurrencyName = with(value) { "$name ($code) - $symbol" } + val fullCurrencyName = with(value) { "$name ($code) — $symbol" } return AppCurrencySelectorState.Currency(value.code, fullCurrencyName) } diff --git a/data/app-currency/src/main/kotlin/com/tangem/data/appcurrency/DefaultAppCurrencyRepository.kt b/data/app-currency/src/main/kotlin/com/tangem/data/appcurrency/DefaultAppCurrencyRepository.kt index 91abb1d2a3..520be9c24d 100644 --- a/data/app-currency/src/main/kotlin/com/tangem/data/appcurrency/DefaultAppCurrencyRepository.kt +++ b/data/app-currency/src/main/kotlin/com/tangem/data/appcurrency/DefaultAppCurrencyRepository.kt @@ -34,7 +34,7 @@ internal class DefaultAppCurrencyRepository( .collect(::send) } - launch(dispatchers.io) { + withContext(dispatchers.io) { if (selectedAppCurrencyStore.isEmpty()) { fetchDefaultAppCurrency() } @@ -47,6 +47,7 @@ internal class DefaultAppCurrencyRepository( val currencies = availableAppCurrenciesStore.getAllSyncOrNull() ?.map(appCurrencyConverter::convert) + ?.sortedBy(AppCurrency::name) requireNotNull(currencies) { "No available currencies stored" From 75234e34df370db89f322c28f34b0dfdb07a60ba Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 10 Oct 2023 15:17:32 +0300 Subject: [PATCH 174/242] Updated on 2026-08-14 --- .../tangem/domain/common/extensions/Blockchain.kt | 12 +++++++++--- gradle/dependencies.toml | 2 +- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/domain/legacy/src/main/java/com/tangem/domain/common/extensions/Blockchain.kt b/domain/legacy/src/main/java/com/tangem/domain/common/extensions/Blockchain.kt index a37ce82bcb..925137aab5 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/common/extensions/Blockchain.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/common/extensions/Blockchain.kt @@ -4,7 +4,7 @@ import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.Token import java.math.BigDecimal -@Suppress("ComplexMethod") +@Suppress("ComplexMethod", "LongMethod") fun Blockchain.Companion.fromNetworkId(networkId: String): Blockchain? { return when (networkId) { "arbitrum-one" -> Blockchain.Arbitrum @@ -70,11 +70,13 @@ fun Blockchain.Companion.fromNetworkId(networkId: String): Blockchain? { "octaspace/test" -> Blockchain.OctaSpaceTestnet "chia" -> Blockchain.Chia "chia/test" -> Blockchain.ChiaTestnet + "near" -> Blockchain.Near + "near/test" -> Blockchain.NearTestnet else -> null } } -@Suppress("ComplexMethod") +@Suppress("ComplexMethod", "LongMethod") fun Blockchain.toNetworkId(): String { return when (this) { Blockchain.Unknown -> "unknown" @@ -141,10 +143,12 @@ fun Blockchain.toNetworkId(): String { Blockchain.OctaSpaceTestnet -> "octaspace/test" Blockchain.Chia -> "chia" Blockchain.ChiaTestnet -> "chia/test" + Blockchain.Near -> "near" + Blockchain.NearTestnet -> "near/test" } } -@Suppress("ComplexMethod") +@Suppress("ComplexMethod", "LongMethod") fun Blockchain.toCoinId(): String { return when (this) { Blockchain.Binance, Blockchain.BinanceTestnet, Blockchain.BSC, Blockchain.BSCTestnet -> "binancecoin" @@ -187,6 +191,8 @@ fun Blockchain.toCoinId(): String { Blockchain.OctaSpace, Blockchain.OctaSpaceTestnet -> "octaspace" Blockchain.Chia -> "chia" Blockchain.ChiaTestnet -> "chia/test" + Blockchain.Near -> "near" + Blockchain.NearTestnet -> "near/test" } } diff --git a/gradle/dependencies.toml b/gradle/dependencies.toml index a75f75bdfa..e580c27332 100644 --- a/gradle/dependencies.toml +++ b/gradle/dependencies.toml @@ -82,7 +82,7 @@ okHttp-prettyLogging = "3.1.0" # endregion Other libraries # region Tangem -tangemBlockchainSdk = "develop-354" +tangemBlockchainSdk = "develop-355" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds tangemCardSdk = "develop-302" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds From 9091a8acacbc487102d8341c8213f33daba0ad4f Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 10 Oct 2023 18:07:39 +0800 Subject: [PATCH 175/242] Updated on 2026-08-14 --- .../saveWallet/redux/SaveWalletMiddleware.kt | 1 + .../wallet/state/WalletAlertState.kt | 22 ++++++-- .../presentation/wallet/state/WalletEvent.kt | 6 +-- .../factory/WalletSkeletonStateConverter.kt | 4 +- .../presentation/wallet/ui/WalletAlert.kt | 27 +++++----- .../wallet/ui/WalletEventEffect.kt | 10 +--- .../presentation/wallet/ui/WalletScreen.kt | 2 +- .../wallet/viewmodels/WalletClickIntents.kt | 4 +- .../wallet/viewmodels/WalletViewModel.kt | 50 ++++++++++++------- 9 files changed, 75 insertions(+), 51 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/features/saveWallet/redux/SaveWalletMiddleware.kt b/app/src/main/java/com/tangem/tap/features/saveWallet/redux/SaveWalletMiddleware.kt index f68fa4579e..1a1bdd7dd9 100644 --- a/app/src/main/java/com/tangem/tap/features/saveWallet/redux/SaveWalletMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/saveWallet/redux/SaveWalletMiddleware.kt @@ -112,6 +112,7 @@ internal class SaveWalletMiddleware { // Enable saving access codes only if this is the first time user save the wallet if (isFirstSavedWallet) { + preferencesStorage.shouldSaveAccessCodes = true store.state.daggerGraphState.get(DaggerGraphState::cardSdkConfigRepository) .setAccessCodeRequestPolicy( isBiometricsRequestPolicy = userWallet.hasAccessCode, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletAlertState.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletAlertState.kt index be52fb2220..97d4baa78c 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletAlertState.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletAlertState.kt @@ -2,12 +2,28 @@ package com.tangem.feature.wallet.presentation.wallet.state import androidx.compose.runtime.Immutable import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.feature.wallet.impl.R @Immutable internal sealed class WalletAlertState { + + abstract val title: TextReference? + abstract val message: TextReference + open val confirmButtonText: TextReference = resourceReference(id = R.string.common_ok) + open val isWarningConfirmButton: Boolean = false + abstract val onConfirmClick: (() -> Unit)? + data class DefaultAlert( - val title: TextReference, - val message: TextReference, - val onActionClick: (() -> Unit)?, + override val title: TextReference, + override val message: TextReference, + override val onConfirmClick: (() -> Unit)?, ) : WalletAlertState() + + data class RemoveWalletAlert(override val onConfirmClick: (() -> Unit)?) : WalletAlertState() { + override val title: TextReference? = null + override val message: TextReference = resourceReference(id = R.string.user_wallet_list_delete_prompt) + override val confirmButtonText: TextReference = resourceReference(id = R.string.common_delete) + override val isWarningConfirmButton: Boolean = true + } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletEvent.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletEvent.kt index 16cfffd301..9c619d9a9c 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletEvent.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletEvent.kt @@ -12,11 +12,7 @@ internal sealed class WalletEvent { data class ShowToast(val text: TextReference) : WalletEvent() - data class ShowAlert( - val title: TextReference, - val message: TextReference, - val onActionClick: (() -> Unit)?, - ) : WalletEvent() + data class ShowAlert(val state: WalletAlertState) : WalletEvent() data class CopyAddress(val address: String, val toast: TextReference) : WalletEvent() diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletSkeletonStateConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletSkeletonStateConverter.kt index df44339b4d..4b26019576 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletSkeletonStateConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletSkeletonStateConverter.kt @@ -118,7 +118,7 @@ internal class WalletSkeletonStateConverter( additionalInfo = WalletAdditionalInfoFactory.resolve(wallet = this), imageResId = createImageResId(), onRenameClick = clickIntents::onRenameClick, - onDeleteClick = clickIntents::onDeleteClick, + onDeleteClick = clickIntents::onDeleteBeforeConfirmationClick, ) } @@ -129,7 +129,7 @@ internal class WalletSkeletonStateConverter( additionalInfo = if (isMultiCurrency) WalletAdditionalInfoFactory.resolve(wallet = this) else null, imageResId = createImageResId(), onRenameClick = clickIntents::onRenameClick, - onDeleteClick = clickIntents::onDeleteClick, + onDeleteClick = clickIntents::onDeleteBeforeConfirmationClick, ) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletAlert.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletAlert.kt index 77374812d7..1edcb46df5 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletAlert.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletAlert.kt @@ -9,23 +9,22 @@ import com.tangem.feature.wallet.impl.R import com.tangem.feature.wallet.presentation.wallet.state.WalletAlertState @Composable -internal fun WalletAlert(config: WalletAlertState, onDismiss: () -> Unit) { - when (config) { - is WalletAlertState.DefaultAlert -> { - DefaultAlert(config = config, onDismiss = onDismiss) - } - } +internal fun WalletAlert(state: WalletAlertState, onDismiss: () -> Unit) { + DefaultAlert(state = state, onDismiss = onDismiss) } @Composable -private fun DefaultAlert(config: WalletAlertState.DefaultAlert, onDismiss: () -> Unit) { +private fun DefaultAlert(state: WalletAlertState, onDismiss: () -> Unit) { val confirmButton: DialogButton val dismissButton: DialogButton? - if (config.onActionClick != null) { + + val onActionClick = state.onConfirmClick + if (onActionClick != null) { confirmButton = DialogButton( - title = stringResource(id = R.string.common_ok), + title = state.confirmButtonText.resolveReference(), + warning = state.isWarningConfirmButton, onClick = { - config.onActionClick.invoke() + onActionClick() onDismiss() }, ) @@ -35,16 +34,18 @@ private fun DefaultAlert(config: WalletAlertState.DefaultAlert, onDismiss: () -> ) } else { confirmButton = DialogButton( - title = stringResource(id = R.string.common_ok), + title = state.confirmButtonText.resolveReference(), + warning = state.isWarningConfirmButton, onClick = onDismiss, ) dismissButton = null } + BasicDialog( - message = config.message.resolveReference(), + message = state.message.resolveReference(), confirmButton = confirmButton, onDismissDialog = onDismiss, - title = config.title.resolveReference(), + title = state.title?.resolveReference(), dismissButton = dismissButton, ) } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletEventEffect.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletEventEffect.kt index efdc905452..ea7437eeb8 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletEventEffect.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletEventEffect.kt @@ -48,15 +48,7 @@ internal fun WalletEventEffect( clipboardManager.setText(AnnotatedString(value.address)) Toast.makeText(context, value.toast.resolveReference(resources), Toast.LENGTH_SHORT).show() } - is WalletEvent.ShowAlert -> { - onAlertConfigSet( - WalletAlertState.DefaultAlert( - title = value.title, - message = value.message, - onActionClick = value.onActionClick, - ), - ) - } + is WalletEvent.ShowAlert -> onAlertConfigSet(value.state) is WalletEvent.RateApp -> { val reviewManager = ReviewManagerFactory.create(context) val requestTask = reviewManager.requestReviewFlow() diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt index 76dc0bac3d..449909bf02 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt @@ -82,7 +82,7 @@ internal fun WalletScreen(state: WalletState) { ) alertConfig?.let { - WalletAlert(config = it, onDismiss = { alertConfig = null }) + WalletAlert(state = it, onDismiss = { alertConfig = null }) } } is WalletState.Initial -> Unit diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletClickIntents.kt index 63cfe9c54c..06fc109344 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletClickIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletClickIntents.kt @@ -45,7 +45,9 @@ internal interface WalletClickIntents { fun onRenameClick(userWalletId: UserWalletId, name: String) - fun onDeleteClick(userWalletId: UserWalletId) + fun onDeleteBeforeConfirmationClick(userWalletId: UserWalletId) + + fun onDeleteAfterConfirmationClick(userWalletId: UserWalletId) fun onSingleCurrencySendClick(cryptoCurrencyStatus: CryptoCurrencyStatus? = null) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt index e690d4303a..7f14b96010 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt @@ -752,7 +752,19 @@ internal class WalletViewModel @Inject constructor( } } - override fun onDeleteClick(userWalletId: UserWalletId) { + override fun onDeleteBeforeConfirmationClick(userWalletId: UserWalletId) { + uiState = stateFactory.getStateAndTriggerEvent( + state = uiState, + event = WalletEvent.ShowAlert( + state = WalletAlertState.RemoveWalletAlert( + onConfirmClick = { onDeleteAfterConfirmationClick(userWalletId) }, + ), + ), + setUiState = { uiState = it }, + ) + } + + override fun onDeleteAfterConfirmationClick(userWalletId: UserWalletId) { val state = uiState as? WalletState.ContentState ?: return viewModelScope.launch(dispatchers.io) { deleteWalletUseCase(userWalletId) @@ -820,29 +832,33 @@ internal class WalletViewModel @Inject constructor( val currency = cryptoCurrencyStatus.currency return if (currency is CryptoCurrency.Coin && !isCryptoCurrencyCoinCouldHide(userWalletId, currency)) { WalletEvent.ShowAlert( - title = resourceReference( - id = R.string.token_details_unable_hide_alert_title, - formatArgs = WrappedList(listOf(cryptoCurrencyStatus.currency.name)), - ), - message = resourceReference( - id = R.string.token_details_unable_hide_alert_message, - formatArgs = WrappedList( - listOf( - cryptoCurrencyStatus.currency.name, - cryptoCurrencyStatus.currency.network.name, + state = WalletAlertState.DefaultAlert( + title = resourceReference( + id = R.string.token_details_unable_hide_alert_title, + formatArgs = WrappedList(listOf(cryptoCurrencyStatus.currency.name)), + ), + message = resourceReference( + id = R.string.token_details_unable_hide_alert_message, + formatArgs = WrappedList( + listOf( + cryptoCurrencyStatus.currency.name, + cryptoCurrencyStatus.currency.network.name, + ), ), ), + onConfirmClick = null, ), - onActionClick = null, ) } else { WalletEvent.ShowAlert( - title = resourceReference( - id = R.string.token_details_hide_alert_title, - formatArgs = WrappedList(listOf(cryptoCurrencyStatus.currency.name)), + state = WalletAlertState.DefaultAlert( + title = resourceReference( + id = R.string.token_details_hide_alert_title, + formatArgs = WrappedList(listOf(cryptoCurrencyStatus.currency.name)), + ), + message = resourceReference(R.string.token_details_hide_alert_message), + onConfirmClick = { onPerformHideToken(cryptoCurrencyStatus) }, ), - message = resourceReference(R.string.token_details_hide_alert_message), - onActionClick = { onPerformHideToken(cryptoCurrencyStatus) }, ) } } From 36fabffe3a11aa67281a0f0ce88248b414864209 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 10 Oct 2023 15:30:51 +0300 Subject: [PATCH 176/242] Updated on 2026-08-14 --- .../data/tokens/repository/DefaultCurrenciesRepository.kt | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) 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 aa3d99f8a1..910b4f7b61 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 @@ -285,7 +285,11 @@ internal class DefaultCurrenciesRepository( private suspend fun storeAndPushTokens(userWalletId: UserWalletId, response: UserTokensResponse) { userTokensStore.store(userWalletId, response) - tangemTechApi.saveUserTokens(userWalletId.stringValue, response) + try { + tangemTechApi.saveUserTokens(userWalletId.stringValue, response) + } catch (e: Throwable) { + Timber.e("Unable to save user tokens for: ${userWalletId.stringValue}") + } } private suspend fun fetchExchangeableUserMarketCoinsByIds( From c8da4e40e15712bf8bef2a2da1b399449164ba89 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 10 Oct 2023 15:40:23 +0300 Subject: [PATCH 177/242] Updated on 2026-08-14 --- core/utils/src/main/java/com/tangem/utils/Extensions.kt | 6 ++++++ .../tangem/domain/tokens/GetCryptoCurrencyActionsUseCase.kt | 3 ++- .../wallet/state/factory/TokenActionsProvider.kt | 4 ++-- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/core/utils/src/main/java/com/tangem/utils/Extensions.kt b/core/utils/src/main/java/com/tangem/utils/Extensions.kt index de72e53d54..efeb15cdb9 100644 --- a/core/utils/src/main/java/com/tangem/utils/Extensions.kt +++ b/core/utils/src/main/java/com/tangem/utils/Extensions.kt @@ -1,9 +1,15 @@ package com.tangem.utils +import java.math.BigDecimal + inline fun > safeValueOf(type: String, default: T): T { return try { java.lang.Enum.valueOf(T::class.java, type) } catch (e: IllegalArgumentException) { default } +} + +fun BigDecimal?.isNullOrZero(): Boolean { + return this == null || this.compareTo(BigDecimal.ZERO) == 0 } \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyActionsUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyActionsUseCase.kt index cd63eb702a..c9452c4411 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyActionsUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyActionsUseCase.kt @@ -7,6 +7,7 @@ import com.tangem.domain.tokens.model.TokenActionsState import com.tangem.domain.tokens.repository.MarketCryptoCurrencyRepository import com.tangem.domain.wallets.models.UserWalletId import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import com.tangem.utils.isNullOrZero import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flowOn @@ -70,7 +71,7 @@ class GetCryptoCurrencyActionsUseCase( } // send - if (cryptoCurrencyStatus.value.amount?.signum() == 0) { + if (cryptoCurrencyStatus.value.amount.isNullOrZero()) { disabledList.add(TokenActionsState.ActionState.Send(false)) } else { activeList.add(TokenActionsState.ActionState.Send(true)) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/TokenActionsProvider.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/TokenActionsProvider.kt index 7f86a56e66..9c671a6549 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/TokenActionsProvider.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/TokenActionsProvider.kt @@ -1,6 +1,5 @@ package com.tangem.feature.wallet.presentation.wallet.state.factory -import com.tangem.common.extensions.isZero import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resourceReference import com.tangem.domain.tokens.model.CryptoCurrencyStatus @@ -9,6 +8,7 @@ import com.tangem.feature.wallet.impl.R import com.tangem.feature.wallet.presentation.common.state.TokenItemState import com.tangem.feature.wallet.presentation.wallet.state.TokenActionButtonConfig import com.tangem.feature.wallet.presentation.wallet.viewmodels.WalletClickIntents +import com.tangem.utils.isNullOrZero import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList @@ -34,7 +34,7 @@ internal class TokenActionsProvider(private val clickIntents: WalletClickIntents cryptoCurrencyStatus: CryptoCurrencyStatus, ): TokenActionButtonConfig? { if (actionsState is TokenActionsState.ActionState.Send && - cryptoCurrencyStatus.value.amount?.isZero() == true + cryptoCurrencyStatus.value.amount.isNullOrZero() ) { return null } From 66427b61534098b889ce78d265372949c419329b Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 10 Oct 2023 21:12:36 +0300 Subject: [PATCH 178/242] Updated on 2026-08-14 --- .../presentation/common/WalletPreviewData.kt | 5 ++- .../domain/WalletAdditionalInfoFactory.kt | 34 +++++++++++++------ .../state/components/WalletAdditionalInfo.kt | 10 ++++++ .../state/components/WalletCardState.kt | 14 +++++--- .../wallet/ui/components/common/WalletCard.kt | 6 +++- 5 files changed, 51 insertions(+), 18 deletions(-) create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/components/WalletAdditionalInfo.kt diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/WalletPreviewData.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/WalletPreviewData.kt index 42613794b0..9f6b97b22b 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/WalletPreviewData.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/WalletPreviewData.kt @@ -39,7 +39,10 @@ internal object WalletPreviewData { id = UserWalletId(stringValue = "123"), title = "Wallet1Wallet1Wallet1Wallet1Wallet1Wallet1Wallet1Wallet1", balance = "8923,05312312312312312312331231231233432423423424234 $", - additionalInfo = TextReference.Str("3 cards • Seed phrase3 cards • Seed phraseцфвцфвфцвцфввцфвцф"), + additionalInfo = WalletAdditionalInfo( + hideable = false, + content = TextReference.Str("3 cards • Seed phrase3 cards • Seed phrasephrasephrasephrase"), + ), imageResId = R.drawable.ill_wallet2_cards3_120_106, onRenameClick = { _, _ -> }, onDeleteClick = {}, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/WalletAdditionalInfoFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/WalletAdditionalInfoFactory.kt index 9124311a34..40ba99fbde 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/WalletAdditionalInfoFactory.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/WalletAdditionalInfoFactory.kt @@ -8,6 +8,7 @@ import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.domain.common.util.cardTypesResolver import com.tangem.domain.wallets.models.UserWallet import com.tangem.feature.wallet.impl.R +import com.tangem.feature.wallet.presentation.wallet.state.components.WalletAdditionalInfo import java.math.BigDecimal /** @@ -25,7 +26,7 @@ internal object WalletAdditionalInfoFactory { * @param wallet current wallet * @param currencyAmount amount of currency */ - fun resolve(wallet: UserWallet, currencyAmount: BigDecimal? = null): TextReference { + fun resolve(wallet: UserWallet, currencyAmount: BigDecimal? = null): WalletAdditionalInfo { return if (wallet.isMultiCurrency) { wallet.resolveMultiCurrencyInfo() } else { @@ -33,9 +34,14 @@ internal object WalletAdditionalInfoFactory { } } - private fun UserWallet.resolveMultiCurrencyInfo(): TextReference { + private fun UserWallet.resolveMultiCurrencyInfo(): WalletAdditionalInfo { return if (isLocked) { - getBackupInfoWithDivider(backupCardsCount = getCardsCount()) + TextReference.Res(R.string.common_locked) + WalletAdditionalInfo( + hideable = false, + content = getBackupInfoWithDivider( + backupCardsCount = getCardsCount(), + ) + TextReference.Res(R.string.common_locked), + ) } else { val cardTypeResolver = scanResponse.cardTypesResolver if (cardTypeResolver.isWallet2()) { @@ -46,10 +52,14 @@ internal object WalletAdditionalInfoFactory { } } - private fun UserWallet.resolveWallet2Info(): TextReference { + private fun UserWallet.resolveWallet2Info(): WalletAdditionalInfo { return if (isImported) { - getBackupInfoWithDivider(backupCardsCount = getCardsCount()) + - TextReference.Res(id = R.string.common_seed_phrase) + WalletAdditionalInfo( + hideable = false, + content = getBackupInfoWithDivider(backupCardsCount = getCardsCount()) + TextReference.Res( + id = R.string.common_seed_phrase, + ), + ) } else { getBackupInfo(backupCardsCount = getCardsCount()) } @@ -63,12 +73,14 @@ internal object WalletAdditionalInfoFactory { } } - private fun getBackupInfo(backupCardsCount: Int?): TextReference { - return if (backupCardsCount != null) { + private fun getBackupInfo(backupCardsCount: Int?): WalletAdditionalInfo { + val content = if (backupCardsCount != null) { getBackupInfoTextReference(count = backupCardsCount) } else { TextReference.EMPTY } + + return WalletAdditionalInfo(hideable = false, content = content) } private fun getBackupInfoTextReference(count: Int): TextReference { @@ -79,9 +91,9 @@ internal object WalletAdditionalInfoFactory { ) } - private fun UserWallet.resolveSingleCurrencyInfo(currencyAmount: BigDecimal?): TextReference { + private fun UserWallet.resolveSingleCurrencyInfo(currencyAmount: BigDecimal?): WalletAdditionalInfo { return if (isLocked) { - TextReference.Res(R.string.common_locked) + WalletAdditionalInfo(hideable = false, content = TextReference.Res(R.string.common_locked)) } else { val blockchain = scanResponse.cardTypesResolver.getBlockchain() val amount = currencyAmount?.let { @@ -92,7 +104,7 @@ internal object WalletAdditionalInfoFactory { ) } - TextReference.Str(value = amount.orEmpty()) + WalletAdditionalInfo(hideable = true, content = TextReference.Str(value = amount.orEmpty())) } } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/components/WalletAdditionalInfo.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/components/WalletAdditionalInfo.kt new file mode 100644 index 0000000000..612f2111e5 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/components/WalletAdditionalInfo.kt @@ -0,0 +1,10 @@ +package com.tangem.feature.wallet.presentation.wallet.state.components + +import androidx.compose.runtime.Immutable +import com.tangem.core.ui.extensions.TextReference + +@Immutable +data class WalletAdditionalInfo( + val hideable: Boolean, + val content: TextReference, +) \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/components/WalletCardState.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/components/WalletCardState.kt index 13ce9569e8..5e74bd00af 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/components/WalletCardState.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/components/WalletCardState.kt @@ -16,7 +16,8 @@ internal sealed interface WalletCardState { /** Title */ val title: String - val additionalInfo: TextReference? + /** Wallet additional info */ + val additionalInfo: WalletAdditionalInfo? /** Wallet image resource id */ @get:DrawableRes @@ -43,7 +44,7 @@ internal sealed interface WalletCardState { data class Content( override val id: UserWalletId, override val title: String, - override val additionalInfo: TextReference, + override val additionalInfo: WalletAdditionalInfo, override val imageResId: Int?, override val onRenameClick: (UserWalletId, String) -> Unit, override val onDeleteClick: (UserWalletId) -> Unit, @@ -64,7 +65,7 @@ internal sealed interface WalletCardState { data class LockedContent( override val id: UserWalletId, override val title: String, - override val additionalInfo: TextReference, + override val additionalInfo: WalletAdditionalInfo, override val imageResId: Int?, override val onRenameClick: (UserWalletId, String) -> Unit, override val onDeleteClick: (UserWalletId) -> Unit, @@ -86,7 +87,10 @@ internal sealed interface WalletCardState { override val onRenameClick: (UserWalletId, String) -> Unit, override val onDeleteClick: (UserWalletId) -> Unit, ) : WalletCardState { - override val additionalInfo: TextReference = EMPTY_BALANCE_TEXT + override val additionalInfo: WalletAdditionalInfo = WalletAdditionalInfo( + hideable = true, + content = EMPTY_BALANCE_TEXT, + ) } /** @@ -101,7 +105,7 @@ internal sealed interface WalletCardState { data class Loading( override val id: UserWalletId, override val title: String, - override val additionalInfo: TextReference? = null, + override val additionalInfo: WalletAdditionalInfo? = null, override val imageResId: Int?, override val onRenameClick: (UserWalletId, String) -> Unit, override val onDeleteClick: (UserWalletId) -> Unit, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletCard.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletCard.kt index be70f2f0bd..4f7ed253be 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletCard.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletCard.kt @@ -94,7 +94,11 @@ internal fun WalletCard(state: WalletCardState, isBalanceHidden: Boolean, modifi ) AdditionalInfo( - text = state.additionalInfo, + text = if (state.additionalInfo?.hideable == true && isBalanceHidden) { + WalletCardState.HIDDEN_BALANCE_TEXT + } else { + state.additionalInfo?.content + }, modifier = Modifier.constrainAs(additionalTextRef) { start.linkTo(parent.start) top.linkTo(balanceRef.bottom) From 7fb168b533e5d05e56f43ededdda74e23167c5c8 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 11 Oct 2023 14:27:17 +0300 Subject: [PATCH 179/242] Updated on 2026-08-14 --- .../wallet/state/factory/WalletSkeletonStateConverter.kt | 5 +++-- .../wallet/state/factory/WalletStateFactory.kt | 9 ++++++++- .../presentation/wallet/viewmodels/WalletViewModel.kt | 4 ++++ 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletSkeletonStateConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletSkeletonStateConverter.kt index 4b26019576..dbdd8d5ea9 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletSkeletonStateConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletSkeletonStateConverter.kt @@ -31,6 +31,7 @@ import kotlinx.coroutines.flow.MutableStateFlow */ internal class WalletSkeletonStateConverter( private val currentStateProvider: Provider, + private val isBalanceHiddenProvider: Provider, private val clickIntents: WalletClickIntents, ) : Converter { @@ -55,7 +56,7 @@ internal class WalletSkeletonStateConverter( bottomSheetConfig = null, tokenActionsBottomSheet = null, onManageTokensClick = clickIntents::onManageTokensClick, - isBalanceHidden = true, + isBalanceHidden = isBalanceHiddenProvider(), ) } @@ -74,7 +75,7 @@ internal class WalletSkeletonStateConverter( value = TxHistoryState.getDefaultLoadingTransactions(clickIntents::onExploreClick), ), ), - isBalanceHidden = true, + isBalanceHidden = isBalanceHiddenProvider(), ) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletStateFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletStateFactory.kt index 1c13a73e30..d8ea0d3ff9 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletStateFactory.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletStateFactory.kt @@ -45,12 +45,19 @@ internal class WalletStateFactory( private val currentCardTypeResolverProvider: Provider, private val currentWalletProvider: Provider, private val appCurrencyProvider: Provider, + private val isBalanceHiddenProvider: Provider, private val clickIntents: WalletClickIntents, ) { private val tokenActionsProvider by lazy { TokenActionsProvider(clickIntents) } - private val skeletonConverter by lazy { WalletSkeletonStateConverter(currentStateProvider, clickIntents) } + private val skeletonConverter by lazy { + WalletSkeletonStateConverter( + currentStateProvider = currentStateProvider, + isBalanceHiddenProvider = isBalanceHiddenProvider, + clickIntents = clickIntents, + ) + } private val walletsUnlockStateConverter by lazy { WalletsUnlockStateConverter(currentStateProvider, clickIntents) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt index 7f14b96010..20d3f08f1a 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt @@ -139,6 +139,8 @@ internal class WalletViewModel @Inject constructor( clickIntents = this, ) + private var isBalanceHidden = true + private val stateFactory = WalletStateFactory( currentStateProvider = Provider { uiState }, currentCardTypeResolverProvider = Provider { @@ -150,6 +152,7 @@ internal class WalletViewModel @Inject constructor( wallets[requireNotNull(uiState as? WalletState.ContentState).walletsListConfig.selectedWalletIndex] }, appCurrencyProvider = Provider(selectedAppCurrencyFlow::value), + isBalanceHiddenProvider = Provider { isBalanceHidden }, clickIntents = this, ) @@ -199,6 +202,7 @@ internal class WalletViewModel @Inject constructor( isBalanceHiddenUseCase() .flowWithLifecycle(owner.lifecycle) .onEach { hidden -> + isBalanceHidden = hidden WalletStateCache.updateAll { copySealed(isBalanceHidden = hidden) } uiState = stateFactory.getHiddenBalanceState(isBalanceHidden = hidden) } From 7df5f9edc24264cba6e44366052958351816f76d Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 11 Oct 2023 17:29:26 +0500 Subject: [PATCH 180/242] Updated on 2026-08-14 --- .../tap/di/domain/TxHistoryDomainModule.kt | 9 +++++ .../ui/components/transactions/Transaction.kt | 14 +++++++- .../transactions/state/TransactionState.kt | 6 ++++ data/txhistory/build.gradle.kts | 1 + .../repository/DefaultTxHistoryRepository.kt | 6 ++++ .../repository/TxHistoryRepository.kt | 2 ++ .../GetExplorerTransactionUrlUseCase.kt | 12 +++++++ .../TokenDetailsLoadedBalanceConverter.kt | 4 ++- .../state/factory/TokenDetailsStateFactory.kt | 8 ++--- .../TokenDetailsTxHistoryItemFlowConverter.kt | 1 + ...ilsTxHistoryToTransactionStateConverter.kt | 12 +++++++ ...tailsTxHistoryTransactionStateConverter.kt | 12 +++++++ .../viewmodels/TokenDetailsClickIntents.kt | 6 ++++ .../viewmodels/TokenDetailsViewModel.kt | 33 +++++++++++++------ .../presentation/common/WalletPreviewData.kt | 2 ++ .../router/DefaultWalletRouter.kt | 2 +- .../presentation/router/InnerWalletRouter.kt | 2 +- .../WalletTxHistoryItemFlowConverter.kt | 1 + ...alletTxHistoryTransactionStateConverter.kt | 14 +++++++- .../wallet/viewmodels/WalletClickIntents.kt | 2 ++ .../wallet/viewmodels/WalletViewModel.kt | 24 +++++++++++++- gradle/dependencies.toml | 2 +- 22 files changed, 153 insertions(+), 22 deletions(-) create mode 100644 domain/txhistory/src/main/kotlin/com/tangem/domain/txhistory/usecase/GetExplorerTransactionUrlUseCase.kt diff --git a/app/src/main/java/com/tangem/tap/di/domain/TxHistoryDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/TxHistoryDomainModule.kt index 3ab36cd231..7ba9e05cff 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/TxHistoryDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/TxHistoryDomainModule.kt @@ -2,6 +2,7 @@ package com.tangem.tap.di.domain import com.tangem.domain.tokens.* import com.tangem.domain.txhistory.repository.TxHistoryRepository +import com.tangem.domain.txhistory.usecase.GetExplorerTransactionUrlUseCase import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsCountUseCase import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsUseCase import dagger.Module @@ -25,4 +26,12 @@ internal object TxHistoryDomainModule { fun provideGetTxHistoryItemsUseCase(txHistoryRepository: TxHistoryRepository): GetTxHistoryItemsUseCase { return GetTxHistoryItemsUseCase(repository = txHistoryRepository) } + + @Provides + @ViewModelScoped + fun providesGetExplorerTransactionUrlUseCase( + txHistoryRepository: TxHistoryRepository, + ): GetExplorerTransactionUrlUseCase { + return GetExplorerTransactionUrlUseCase(repository = txHistoryRepository) + } } \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/transactions/Transaction.kt b/core/ui/src/main/java/com/tangem/core/ui/components/transactions/Transaction.kt index 4cbbb22026..676da451cf 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/transactions/Transaction.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/transactions/Transaction.kt @@ -2,6 +2,7 @@ package com.tangem.core.ui.components.transactions import androidx.compose.foundation.Image import androidx.compose.foundation.background +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape @@ -24,8 +25,8 @@ import com.tangem.core.ui.R import com.tangem.core.ui.components.CircleShimmer import com.tangem.core.ui.components.RectangleShimmer import com.tangem.core.ui.components.transactions.state.TransactionState -import com.tangem.core.ui.components.transactions.state.TransactionState.Content.Status import com.tangem.core.ui.components.transactions.state.TransactionState.Content.Direction +import com.tangem.core.ui.components.transactions.state.TransactionState.Content.Status import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.res.TangemTheme @@ -49,10 +50,15 @@ fun Transaction(state: TransactionState, isBalanceHidden: Boolean, modifier: Mod modifier = modifier .background(TangemTheme.colors.background.primary) .defaultMinSize(minHeight = TangemTheme.dimens.size56) + .clickable( + enabled = state is TransactionState.Content, + onClick = (state as? TransactionState.Content)?.onClick ?: {}, + ) .padding(horizontal = TangemTheme.dimens.spacing12, vertical = TangemTheme.dimens.spacing10), color = TangemTheme.colors.background.primary, ) { @Suppress("DestructuringDeclarationWithTooManyEntries") + // FIXME: split this composable to small composables(loading/content/error) to properly handle onClick ConstraintLayout(modifier = Modifier.fillMaxWidth()) { val (iconItem, titleItem, subtitleItem, amountItem, timestampItem) = createRefs() @@ -359,6 +365,7 @@ private class TransactionItemStateProvider : CollectionPreviewParameterProvider< timestamp = "8:41", status = Status.Confirmed, direction = Direction.OUTGOING, + onClick = {}, ), TransactionState.Transfer( txHash = UUID.randomUUID().toString(), @@ -367,6 +374,7 @@ private class TransactionItemStateProvider : CollectionPreviewParameterProvider< timestamp = "8:41", status = Status.Unconfirmed, direction = Direction.INCOMING, + onClick = {}, ), TransactionState.Approve( txHash = UUID.randomUUID().toString(), @@ -375,6 +383,7 @@ private class TransactionItemStateProvider : CollectionPreviewParameterProvider< timestamp = "8:41", status = Status.Failed, direction = Direction.OUTGOING, + onClick = {}, ), TransactionState.Swap( txHash = UUID.randomUUID().toString(), @@ -383,6 +392,7 @@ private class TransactionItemStateProvider : CollectionPreviewParameterProvider< timestamp = "8:41", status = Status.Unconfirmed, direction = Direction.INCOMING, + onClick = {}, ), TransactionState.Custom( txHash = UUID.randomUUID().toString(), @@ -393,6 +403,7 @@ private class TransactionItemStateProvider : CollectionPreviewParameterProvider< direction = Direction.INCOMING, title = TextReference.Str("Submit"), subtitle = TextReference.Str("33BddS...ga2B"), + onClick = {}, ), TransactionState.Custom( txHash = UUID.randomUUID().toString(), @@ -403,6 +414,7 @@ private class TransactionItemStateProvider : CollectionPreviewParameterProvider< direction = Direction.OUTGOING, title = TextReference.Str("Submit"), subtitle = TextReference.Str("33BddS...ga2B"), + onClick = {}, ), TransactionState.Loading(txHash = UUID.randomUUID().toString()), ), diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/transactions/state/TransactionState.kt b/core/ui/src/main/java/com/tangem/core/ui/components/transactions/state/TransactionState.kt index ad3853ceea..c513c83fba 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/transactions/state/TransactionState.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/transactions/state/TransactionState.kt @@ -20,6 +20,7 @@ sealed interface TransactionState { * @property timestamp timestamp * @property status transaction status * @property direction transaction direction + * @property onClick Lambda be invoked when manage button is clicked */ sealed class Content : TransactionState { @@ -28,6 +29,7 @@ sealed interface TransactionState { abstract val timestamp: String abstract val status: Status abstract val direction: Direction + abstract val onClick: () -> Unit fun copySealed( txHash: String = this.txHash, @@ -73,6 +75,7 @@ sealed interface TransactionState { override val timestamp: String, override val status: Status, override val direction: Direction, + override val onClick: () -> Unit, ) : Content() /** @@ -91,6 +94,7 @@ sealed interface TransactionState { override val timestamp: String, override val status: Status, override val direction: Direction, + override val onClick: () -> Unit, ) : Content() /** @@ -109,6 +113,7 @@ sealed interface TransactionState { override val timestamp: String, override val status: Status, override val direction: Direction, + override val onClick: () -> Unit, ) : Content() data class Custom( @@ -118,6 +123,7 @@ sealed interface TransactionState { override val timestamp: String, override val status: Status, override val direction: Direction, + override val onClick: () -> Unit, val title: TextReference, val subtitle: TextReference, ) : Content() diff --git a/data/txhistory/build.gradle.kts b/data/txhistory/build.gradle.kts index d53b060232..2b5b2f8165 100644 --- a/data/txhistory/build.gradle.kts +++ b/data/txhistory/build.gradle.kts @@ -24,6 +24,7 @@ dependencies { implementation(deps.androidx.paging.runtime) implementation(deps.timber) implementation(deps.jodatime) + implementation(deps.tangem.blockchain) implementation(deps.hilt.core) kapt(deps.hilt.kapt) diff --git a/data/txhistory/src/main/kotlin/com/tangem/data/txhistory/repository/DefaultTxHistoryRepository.kt b/data/txhistory/src/main/kotlin/com/tangem/data/txhistory/repository/DefaultTxHistoryRepository.kt index bfa1cc57d1..0e2cd53343 100644 --- a/data/txhistory/src/main/kotlin/com/tangem/data/txhistory/repository/DefaultTxHistoryRepository.kt +++ b/data/txhistory/src/main/kotlin/com/tangem/data/txhistory/repository/DefaultTxHistoryRepository.kt @@ -3,6 +3,7 @@ package com.tangem.data.txhistory.repository import androidx.paging.Pager import androidx.paging.PagingConfig import androidx.paging.PagingData +import com.tangem.blockchain.common.Blockchain import com.tangem.data.common.cache.CacheRegistry import com.tangem.data.txhistory.repository.paging.TxHistoryPagingSource import com.tangem.datasource.local.txhistory.TxHistoryItemsStore @@ -63,6 +64,11 @@ class DefaultTxHistoryRepository( return pager.flow } + override fun getTxExploreUrl(txHash: String, networkId: Network.ID): String { + val blockchain = Blockchain.fromId(networkId.value) + return blockchain.getExploreTxUrl(txHash) + } + private suspend fun getUserWallet(userWalletId: UserWalletId): UserWallet { return requireNotNull(userWalletsStore.getSyncOrNull(userWalletId)) { "Unable to find user wallet with provided ID: $userWalletId" diff --git a/domain/txhistory/src/main/kotlin/com/tangem/domain/txhistory/repository/TxHistoryRepository.kt b/domain/txhistory/src/main/kotlin/com/tangem/domain/txhistory/repository/TxHistoryRepository.kt index a71fe087c1..6ddb7efd6f 100644 --- a/domain/txhistory/src/main/kotlin/com/tangem/domain/txhistory/repository/TxHistoryRepository.kt +++ b/domain/txhistory/src/main/kotlin/com/tangem/domain/txhistory/repository/TxHistoryRepository.kt @@ -21,4 +21,6 @@ interface TxHistoryRepository { pageSize: Int, refresh: Boolean, ): Flow> + + fun getTxExploreUrl(txHash: String, networkId: Network.ID): String } \ No newline at end of file diff --git a/domain/txhistory/src/main/kotlin/com/tangem/domain/txhistory/usecase/GetExplorerTransactionUrlUseCase.kt b/domain/txhistory/src/main/kotlin/com/tangem/domain/txhistory/usecase/GetExplorerTransactionUrlUseCase.kt new file mode 100644 index 0000000000..d5628aa838 --- /dev/null +++ b/domain/txhistory/src/main/kotlin/com/tangem/domain/txhistory/usecase/GetExplorerTransactionUrlUseCase.kt @@ -0,0 +1,12 @@ +package com.tangem.domain.txhistory.usecase + +import com.tangem.domain.tokens.model.Network +import com.tangem.domain.txhistory.repository.TxHistoryRepository + +class GetExplorerTransactionUrlUseCase( + private val repository: TxHistoryRepository, +) { + operator fun invoke(txHash: String, networkId: Network.ID): String { + return repository.getTxExploreUrl(txHash, networkId) + } +} \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsLoadedBalanceConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsLoadedBalanceConverter.kt index cf82bb5725..e29e728e6b 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsLoadedBalanceConverter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsLoadedBalanceConverter.kt @@ -13,6 +13,7 @@ import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDeta import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.TokenDetailsNotification import com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.txhistory.TokenDetailsTxHistoryToTransactionStateConverter +import com.tangem.feature.tokendetails.presentation.tokendetails.viewmodels.TokenDetailsClickIntents import com.tangem.utils.converter.Converter import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toPersistentList @@ -23,10 +24,11 @@ internal class TokenDetailsLoadedBalanceConverter( private val appCurrencyProvider: Provider, private val symbol: String, private val decimals: Int, + private val clickIntents: TokenDetailsClickIntents, ) : Converter, TokenDetailsState> { private val txHistoryItemConverter by lazy { - TokenDetailsTxHistoryToTransactionStateConverter(symbol, decimals) + TokenDetailsTxHistoryToTransactionStateConverter(symbol, decimals, clickIntents) } override fun convert(value: Either): TokenDetailsState { diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt index e4904101e8..dd73a40465 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt @@ -48,6 +48,7 @@ internal class TokenDetailsStateFactory( appCurrencyProvider = appCurrencyProvider, symbol = symbol, decimals = decimals, + clickIntents = clickIntents, ) } @@ -181,10 +182,7 @@ internal class TokenDetailsStateFactory( ) } - fun getStateWithChooseAddressBottomSheet( - addresses: List
, - onAddressTypeClick: (AddressModel) -> Unit, - ): TokenDetailsState { + fun getStateWithChooseAddressBottomSheet(addresses: List
): TokenDetailsState { return currentStateProvider().copy( bottomSheetConfig = TangemBottomSheetConfig( isShow = true, @@ -196,7 +194,7 @@ internal class TokenDetailsStateFactory( type = AddressModel.Type.valueOf(it.type.name), ) }, - onClick = onAddressTypeClick, + onClick = clickIntents::onAddressTypeSelected, ), ), ) diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/txhistory/TokenDetailsTxHistoryItemFlowConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/txhistory/TokenDetailsTxHistoryItemFlowConverter.kt index 0f700b9373..c0aead9837 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/txhistory/TokenDetailsTxHistoryItemFlowConverter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/txhistory/TokenDetailsTxHistoryItemFlowConverter.kt @@ -30,6 +30,7 @@ internal class TokenDetailsTxHistoryItemFlowConverter( TokenDetailsTxHistoryTransactionStateConverter( symbol = symbol, decimals = decimals, + clickIntents = clickIntents, ) } diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/txhistory/TokenDetailsTxHistoryToTransactionStateConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/txhistory/TokenDetailsTxHistoryToTransactionStateConverter.kt index 5c11efff45..9425cf38e5 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/txhistory/TokenDetailsTxHistoryToTransactionStateConverter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/txhistory/TokenDetailsTxHistoryToTransactionStateConverter.kt @@ -4,6 +4,7 @@ import com.tangem.core.ui.components.transactions.state.TransactionState import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.utils.DateTimeFormatters import com.tangem.domain.txhistory.models.TxHistoryItem +import com.tangem.feature.tokendetails.presentation.tokendetails.viewmodels.TokenDetailsClickIntents import com.tangem.features.tokendetails.impl.R import com.tangem.utils.converter.Converter import com.tangem.utils.toBriefAddressFormat @@ -14,6 +15,7 @@ import org.joda.time.DateTimeZone internal class TokenDetailsTxHistoryToTransactionStateConverter( private val symbol: String, private val decimals: Int, + private val clickIntents: TokenDetailsClickIntents, ) : Converter { override fun convert(value: TxHistoryItem): TransactionState { @@ -21,6 +23,7 @@ internal class TokenDetailsTxHistoryToTransactionStateConverter( } // TODO: Finalize transaction types [REDACTED_JIRA] + @Suppress("LongMethod") private fun createTransactionStateItem(item: TxHistoryItem): TransactionState { return when (val type = item.type) { TxHistoryItem.TransactionType.Transfer -> mapTransfer(item) @@ -35,6 +38,7 @@ internal class TokenDetailsTxHistoryToTransactionStateConverter( direction = item.direction.toUiDirection(), title = TextReference.Str("Deposit"), subtitle = item.direction.extractAddress(), + onClick = { clickIntents.onTransactionClick(item.txHash) }, ) TxHistoryItem.TransactionType.Submit -> TransactionState.Custom( txHash = item.txHash, @@ -45,6 +49,7 @@ internal class TokenDetailsTxHistoryToTransactionStateConverter( direction = item.direction.toUiDirection(), title = TextReference.Str("Submit"), subtitle = item.direction.extractAddress(), + onClick = { clickIntents.onTransactionClick(item.txHash) }, ) TxHistoryItem.TransactionType.Supply -> TransactionState.Custom( txHash = item.txHash, @@ -55,6 +60,7 @@ internal class TokenDetailsTxHistoryToTransactionStateConverter( direction = item.direction.toUiDirection(), title = TextReference.Str("Supply"), subtitle = item.direction.extractAddress(), + onClick = { clickIntents.onTransactionClick(item.txHash) }, ) TxHistoryItem.TransactionType.Unoswap -> TransactionState.Custom( txHash = item.txHash, @@ -65,6 +71,7 @@ internal class TokenDetailsTxHistoryToTransactionStateConverter( direction = item.direction.toUiDirection(), title = TextReference.Str("Unoswap"), subtitle = item.direction.extractAddress(), + onClick = { clickIntents.onTransactionClick(item.txHash) }, ) TxHistoryItem.TransactionType.Withdraw -> TransactionState.Custom( txHash = item.txHash, @@ -75,6 +82,7 @@ internal class TokenDetailsTxHistoryToTransactionStateConverter( direction = item.direction.toUiDirection(), title = TextReference.Str("Withdraw"), subtitle = item.direction.extractAddress(), + onClick = { clickIntents.onTransactionClick(item.txHash) }, ) is TxHistoryItem.TransactionType.Custom -> TransactionState.Custom( txHash = item.txHash, @@ -85,6 +93,7 @@ internal class TokenDetailsTxHistoryToTransactionStateConverter( direction = item.direction.toUiDirection(), title = TextReference.Str(type.id), subtitle = item.direction.extractAddress(), + onClick = { clickIntents.onTransactionClick(item.txHash) }, ) } } @@ -97,6 +106,7 @@ internal class TokenDetailsTxHistoryToTransactionStateConverter( timestamp = item.timestampInMillis.toTimeFormat(), status = item.status.tiUiStatus(), direction = item.direction.toUiDirection(), + onClick = { clickIntents.onTransactionClick(item.txHash) }, ) } @@ -108,6 +118,7 @@ internal class TokenDetailsTxHistoryToTransactionStateConverter( timestamp = item.timestampInMillis.toTimeFormat(), status = item.status.tiUiStatus(), direction = item.direction.toUiDirection(), + onClick = { clickIntents.onTransactionClick(item.txHash) }, ) } private fun mapSwap(item: TxHistoryItem): TransactionState { @@ -118,6 +129,7 @@ internal class TokenDetailsTxHistoryToTransactionStateConverter( timestamp = item.timestampInMillis.toTimeFormat(), status = item.status.tiUiStatus(), direction = item.direction.toUiDirection(), + onClick = { clickIntents.onTransactionClick(item.txHash) }, ) } diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/txhistory/TokenDetailsTxHistoryTransactionStateConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/txhistory/TokenDetailsTxHistoryTransactionStateConverter.kt index f4174cafae..6690c6d52f 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/txhistory/TokenDetailsTxHistoryTransactionStateConverter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/txhistory/TokenDetailsTxHistoryTransactionStateConverter.kt @@ -3,6 +3,7 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.state.factory. import com.tangem.core.ui.components.transactions.state.TransactionState import com.tangem.core.ui.extensions.TextReference import com.tangem.domain.txhistory.models.TxHistoryItem +import com.tangem.feature.tokendetails.presentation.tokendetails.viewmodels.TokenDetailsClickIntents import com.tangem.features.tokendetails.impl.R import com.tangem.utils.converter.Converter import com.tangem.utils.toBriefAddressFormat @@ -11,6 +12,7 @@ import com.tangem.utils.toFormattedCurrencyString internal class TokenDetailsTxHistoryTransactionStateConverter( private val symbol: String, private val decimals: Int, + private val clickIntents: TokenDetailsClickIntents, ) : Converter { override fun convert(value: TxHistoryItem): TransactionState { @@ -18,6 +20,7 @@ internal class TokenDetailsTxHistoryTransactionStateConverter( } // TODO: Finalize transaction types [REDACTED_JIRA] + @Suppress("LongMethod") private fun createTransactionStateItem(item: TxHistoryItem): TransactionState { return when (val type = item.type) { TxHistoryItem.TransactionType.Transfer -> mapTransfer(item) @@ -32,6 +35,7 @@ internal class TokenDetailsTxHistoryTransactionStateConverter( direction = item.direction.toUiDirection(), title = TextReference.Str("Deposit"), subtitle = item.direction.extractAddress(), + onClick = { clickIntents.onTransactionClick(item.txHash) }, ) TxHistoryItem.TransactionType.Submit -> TransactionState.Custom( txHash = item.txHash, @@ -42,6 +46,7 @@ internal class TokenDetailsTxHistoryTransactionStateConverter( direction = item.direction.toUiDirection(), title = TextReference.Str("Submit"), subtitle = item.direction.extractAddress(), + onClick = { clickIntents.onTransactionClick(item.txHash) }, ) TxHistoryItem.TransactionType.Supply -> TransactionState.Custom( txHash = item.txHash, @@ -52,6 +57,7 @@ internal class TokenDetailsTxHistoryTransactionStateConverter( direction = item.direction.toUiDirection(), title = TextReference.Str("Supply"), subtitle = item.direction.extractAddress(), + onClick = { clickIntents.onTransactionClick(item.txHash) }, ) TxHistoryItem.TransactionType.Unoswap -> TransactionState.Custom( txHash = item.txHash, @@ -62,6 +68,7 @@ internal class TokenDetailsTxHistoryTransactionStateConverter( direction = item.direction.toUiDirection(), title = TextReference.Str("Unoswap"), subtitle = item.direction.extractAddress(), + onClick = { clickIntents.onTransactionClick(item.txHash) }, ) TxHistoryItem.TransactionType.Withdraw -> TransactionState.Custom( txHash = item.txHash, @@ -72,6 +79,7 @@ internal class TokenDetailsTxHistoryTransactionStateConverter( direction = item.direction.toUiDirection(), title = TextReference.Str("Withdraw"), subtitle = item.direction.extractAddress(), + onClick = { clickIntents.onTransactionClick(item.txHash) }, ) is TxHistoryItem.TransactionType.Custom -> TransactionState.Custom( txHash = item.txHash, @@ -82,6 +90,7 @@ internal class TokenDetailsTxHistoryTransactionStateConverter( direction = item.direction.toUiDirection(), title = TextReference.Str(type.id), subtitle = item.direction.extractAddress(), + onClick = { clickIntents.onTransactionClick(item.txHash) }, ) } } @@ -94,6 +103,7 @@ internal class TokenDetailsTxHistoryTransactionStateConverter( timestamp = item.getRawTimestamp(), status = item.status.tiUiStatus(), direction = item.direction.toUiDirection(), + onClick = { clickIntents.onTransactionClick(item.txHash) }, ) } @@ -105,6 +115,7 @@ internal class TokenDetailsTxHistoryTransactionStateConverter( timestamp = item.getRawTimestamp(), status = item.status.tiUiStatus(), direction = item.direction.toUiDirection(), + onClick = { clickIntents.onTransactionClick(item.txHash) }, ) } private fun mapSwap(item: TxHistoryItem): TransactionState { @@ -115,6 +126,7 @@ internal class TokenDetailsTxHistoryTransactionStateConverter( timestamp = item.getRawTimestamp(), status = item.status.tiUiStatus(), direction = item.direction.toUiDirection(), + onClick = { clickIntents.onTransactionClick(item.txHash) }, ) } diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsClickIntents.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsClickIntents.kt index 1adc6b021d..be5c08eab8 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsClickIntents.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsClickIntents.kt @@ -1,5 +1,7 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.viewmodels +import com.tangem.core.ui.components.bottomsheets.tokenreceive.AddressModel + interface TokenDetailsClickIntents { fun onBackClick() @@ -26,6 +28,10 @@ interface TokenDetailsClickIntents { fun onExploreClick() + fun onTransactionClick(txHash: String) + + fun onAddressTypeSelected(addressModel: AddressModel) + fun onDismissBottomSheet() fun onCloseRentInfoNotification() diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt index 1de3829916..4d5a4ad91a 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt @@ -9,6 +9,7 @@ import arrow.core.getOrElse import com.tangem.blockchain.common.address.AddressType import com.tangem.common.Provider import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.core.ui.components.bottomsheets.tokenreceive.AddressModel import com.tangem.core.ui.components.transactions.state.TxHistoryState import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.model.AppCurrency @@ -25,6 +26,7 @@ import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsUseCase import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.models.UserWalletId import com.tangem.domain.wallets.usecase.GetExploreUrlUseCase +import com.tangem.domain.txhistory.usecase.GetExplorerTransactionUrlUseCase import com.tangem.domain.wallets.usecase.GetUserWalletUseCase import com.tangem.feature.tokendetails.presentation.router.InnerTokenDetailsRouter import com.tangem.feature.tokendetails.presentation.tokendetails.analytics.TokenScreenEvent @@ -60,6 +62,7 @@ internal class TokenDetailsViewModel @Inject constructor( private val isBalanceHiddenUseCase: IsBalanceHiddenUseCase, private val listenToFlipsUseCase: ListenToFlipsUseCase, private val getCurrencyWarningsUseCase: GetCurrencyWarningsUseCase, + private val getExplorerTransactionUrlUseCase: GetExplorerTransactionUrlUseCase, private val walletManagersFacade: WalletManagersFacade, private val reduxStateHolder: ReduxStateHolder, private val analyticsEventsHandler: AnalyticsEventHandler, @@ -340,31 +343,41 @@ internal class TokenDetailsViewModel @Inject constructor( ) if (addresses.size == 1) { - openUrl(AddressType.Default) - } else { - uiState = stateFactory.getStateWithChooseAddressBottomSheet( - addresses = addresses, - onAddressTypeClick = { - openUrl(AddressType.valueOf(it.type.name)) - uiState = stateFactory.getStateWithClosedBottomSheet() - }, + router.openUrl( + url = getExploreUrlUseCase( + userWalletId = userWalletId, + network = cryptoCurrency.network, + addressType = AddressType.Default, + ), ) + } else { + uiState = stateFactory.getStateWithChooseAddressBottomSheet(addresses = addresses) } } } - private fun openUrl(addressType: AddressType) { + override fun onAddressTypeSelected(addressModel: AddressModel) { viewModelScope.launch { router.openUrl( url = getExploreUrlUseCase( userWalletId = userWalletId, network = cryptoCurrency.network, - addressType = addressType, + addressType = AddressType.valueOf(addressModel.type.name), ), ) + uiState = stateFactory.getStateWithClosedBottomSheet() } } + override fun onTransactionClick(txHash: String) { + router.openUrl( + url = getExplorerTransactionUrlUseCase( + txHash = txHash, + networkId = cryptoCurrency.network.id, + ), + ) + } + override fun onRefreshSwipe() { analyticsEventsHandler.send(TokenScreenEvent.Refreshed(cryptoCurrency.symbol)) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/WalletPreviewData.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/WalletPreviewData.kt index 9f6b97b22b..2cc53a625c 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/WalletPreviewData.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/WalletPreviewData.kt @@ -426,6 +426,7 @@ internal object WalletPreviewData { timestamp = "8:41", status = TransactionState.Content.Status.Unconfirmed, direction = TransactionState.Content.Direction.OUTGOING, + onClick = {}, ), ), TxHistoryState.TxHistoryItemState.GroupTitle("Yesterday"), @@ -437,6 +438,7 @@ internal object WalletPreviewData { timestamp = "8:41", status = TransactionState.Content.Status.Confirmed, direction = TransactionState.Content.Direction.OUTGOING, + onClick = {}, ), ), ), diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/DefaultWalletRouter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/DefaultWalletRouter.kt index 08206005d7..cf0a846de9 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/DefaultWalletRouter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/DefaultWalletRouter.kt @@ -108,7 +108,7 @@ internal class DefaultWalletRouter(private val reduxNavController: ReduxNavContr ) } - override fun openTxHistoryWebsite(url: String) { + override fun openUrl(url: String) { reduxNavController.navigate(action = NavigationAction.OpenUrl(url)) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/InnerWalletRouter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/InnerWalletRouter.kt index 8234b7a917..5354359c05 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/InnerWalletRouter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/InnerWalletRouter.kt @@ -40,7 +40,7 @@ internal interface InnerWalletRouter : WalletRouter { fun openOnboardingScreen() /** Open transaction history website by [url] */ - fun openTxHistoryWebsite(url: String) + fun openUrl(url: String) /** Open token details screen */ fun openTokenDetails(userWalletId: UserWalletId, currency: CryptoCurrency) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/txhistory/WalletTxHistoryItemFlowConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/txhistory/WalletTxHistoryItemFlowConverter.kt index 0c5b962d71..245cfa94e8 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/txhistory/WalletTxHistoryItemFlowConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/txhistory/WalletTxHistoryItemFlowConverter.kt @@ -43,6 +43,7 @@ internal class WalletTxHistoryItemFlowConverter( WalletTxHistoryTransactionStateConverter( symbol = blockchain.currency, decimals = blockchain.decimals(), + clickIntents = clickIntents, ) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/txhistory/WalletTxHistoryTransactionStateConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/txhistory/WalletTxHistoryTransactionStateConverter.kt index ece331befc..b43fbb3029 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/txhistory/WalletTxHistoryTransactionStateConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/txhistory/WalletTxHistoryTransactionStateConverter.kt @@ -4,13 +4,15 @@ import com.tangem.core.ui.components.transactions.state.TransactionState import com.tangem.core.ui.extensions.TextReference import com.tangem.domain.txhistory.models.TxHistoryItem import com.tangem.feature.wallet.impl.R +import com.tangem.feature.wallet.presentation.wallet.viewmodels.WalletClickIntents import com.tangem.utils.converter.Converter import com.tangem.utils.toBriefAddressFormat import com.tangem.utils.toFormattedCurrencyString -class WalletTxHistoryTransactionStateConverter( +internal class WalletTxHistoryTransactionStateConverter( private val symbol: String, private val decimals: Int, + private val clickIntents: WalletClickIntents, ) : Converter { override fun convert(value: TxHistoryItem): TransactionState { @@ -18,6 +20,7 @@ class WalletTxHistoryTransactionStateConverter( } // TODO: Finalize transaction types [REDACTED_JIRA] + @Suppress("LongMethod") private fun createTransactionStateItem(item: TxHistoryItem): TransactionState { return when (val type = item.type) { TxHistoryItem.TransactionType.Transfer -> mapTransfer(item) @@ -32,6 +35,7 @@ class WalletTxHistoryTransactionStateConverter( direction = item.direction.toUiDirection(), title = TextReference.Str("Deposit"), subtitle = item.direction.extractAddress(), + onClick = { clickIntents.onTransactionClick(item.txHash) }, ) TxHistoryItem.TransactionType.Submit -> TransactionState.Custom( txHash = item.txHash, @@ -42,6 +46,7 @@ class WalletTxHistoryTransactionStateConverter( direction = item.direction.toUiDirection(), title = TextReference.Str("Submit"), subtitle = item.direction.extractAddress(), + onClick = { clickIntents.onTransactionClick(item.txHash) }, ) TxHistoryItem.TransactionType.Supply -> TransactionState.Custom( txHash = item.txHash, @@ -52,6 +57,7 @@ class WalletTxHistoryTransactionStateConverter( direction = item.direction.toUiDirection(), title = TextReference.Str("Supply"), subtitle = item.direction.extractAddress(), + onClick = { clickIntents.onTransactionClick(item.txHash) }, ) TxHistoryItem.TransactionType.Unoswap -> TransactionState.Custom( txHash = item.txHash, @@ -62,6 +68,7 @@ class WalletTxHistoryTransactionStateConverter( direction = item.direction.toUiDirection(), title = TextReference.Str("Unoswap"), subtitle = item.direction.extractAddress(), + onClick = { clickIntents.onTransactionClick(item.txHash) }, ) TxHistoryItem.TransactionType.Withdraw -> TransactionState.Custom( txHash = item.txHash, @@ -72,6 +79,7 @@ class WalletTxHistoryTransactionStateConverter( direction = item.direction.toUiDirection(), title = TextReference.Str("Withdraw"), subtitle = item.direction.extractAddress(), + onClick = { clickIntents.onTransactionClick(item.txHash) }, ) is TxHistoryItem.TransactionType.Custom -> TransactionState.Custom( txHash = item.txHash, @@ -82,6 +90,7 @@ class WalletTxHistoryTransactionStateConverter( direction = item.direction.toUiDirection(), title = TextReference.Str(type.id), subtitle = item.direction.extractAddress(), + onClick = { clickIntents.onTransactionClick(item.txHash) }, ) } } @@ -94,6 +103,7 @@ class WalletTxHistoryTransactionStateConverter( timestamp = item.getRawTimestamp(), status = item.status.tiUiStatus(), direction = item.direction.toUiDirection(), + onClick = { clickIntents.onTransactionClick(item.txHash) }, ) } @@ -105,6 +115,7 @@ class WalletTxHistoryTransactionStateConverter( timestamp = item.getRawTimestamp(), status = item.status.tiUiStatus(), direction = item.direction.toUiDirection(), + onClick = { clickIntents.onTransactionClick(item.txHash) }, ) } private fun mapSwap(item: TxHistoryItem): TransactionState { @@ -115,6 +126,7 @@ class WalletTxHistoryTransactionStateConverter( timestamp = item.getRawTimestamp(), status = item.status.tiUiStatus(), direction = item.direction.toUiDirection(), + onClick = { clickIntents.onTransactionClick(item.txHash) }, ) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletClickIntents.kt index 06fc109344..193377047c 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletClickIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletClickIntents.kt @@ -72,4 +72,6 @@ internal interface WalletClickIntents { fun onReloadClick() fun onExploreClick() + + fun onTransactionClick(txHash: String) } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt index 20d3f08f1a..8c97e1e814 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt @@ -47,6 +47,7 @@ import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.model.NetworkGroup import com.tangem.domain.tokens.model.TokenList import com.tangem.domain.tokens.models.analytics.TokenReceiveAnalyticsEvent +import com.tangem.domain.txhistory.usecase.GetExplorerTransactionUrlUseCase import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsCountUseCase import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsUseCase import com.tangem.domain.userwallets.UserWalletBuilder @@ -119,6 +120,7 @@ internal class WalletViewModel @Inject constructor( private val remindToRateAppLaterUseCase: RemindToRateAppLaterUseCase, private val neverToSuggestRateAppUseCase: NeverToSuggestRateAppUseCase, private val setWalletWithFundsFoundUseCase: SetWalletWithFundsFoundUseCase, + private val getExplorerTransactionUrlUseCase: GetExplorerTransactionUrlUseCase, wasCardScannedUseCase: WasCardScannedUseCase, isReadyToShowRateAppUseCase: IsReadyToShowRateAppUseCase, isDemoCardUseCase: IsDemoCardUseCase, @@ -702,7 +704,7 @@ internal class WalletViewModel @Inject constructor( ?.getOrNull() if (currencyStatus != null) { - router.openTxHistoryWebsite( + router.openUrl( url = getExploreUrlUseCase( userWalletId = wallet.walletId, network = currencyStatus.currency.network, @@ -829,6 +831,26 @@ internal class WalletViewModel @Inject constructor( } } + override fun onTransactionClick(txHash: String) { + viewModelScope.launch(dispatchers.io) { + val wallet = getWallet( + index = requireNotNull(uiState as? WalletState.ContentState).walletsListConfig.selectedWalletIndex, + ) + val currencyStatus = getPrimaryCurrencyStatusUpdatesUseCase(wallet.walletId) + .firstOrNull() + ?.getOrNull() + + if (currencyStatus != null) { + router.openUrl( + url = getExplorerTransactionUrlUseCase( + txHash = txHash, + networkId = currencyStatus.currency.network.id, + ), + ) + } + } + } + private suspend fun getHideTokeAlert( userWalletId: UserWalletId, cryptoCurrencyStatus: CryptoCurrencyStatus, diff --git a/gradle/dependencies.toml b/gradle/dependencies.toml index e580c27332..5f97e0c940 100644 --- a/gradle/dependencies.toml +++ b/gradle/dependencies.toml @@ -82,7 +82,7 @@ okHttp-prettyLogging = "3.1.0" # endregion Other libraries # region Tangem -tangemBlockchainSdk = "develop-355" +tangemBlockchainSdk = "develop-356" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds tangemCardSdk = "develop-302" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds From 8027be74aa57cabd6e33343d1afbae4e9ffdbd98 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 11 Oct 2023 19:13:33 +0800 Subject: [PATCH 181/242] Updated on 2026-08-14 --- app/src/main/java/com/tangem/tap/TapApplication.kt | 5 ++--- .../tokens/operations/CurrenciesStatusesOperations.kt | 9 ++++++--- .../tokens/GetPrimaryCurrencyStatusUpdatesUseCaseTest.kt | 2 +- .../com/tangem/domain/tokens/GetTokenListUseCaseTest.kt | 2 +- 4 files changed, 10 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/TapApplication.kt b/app/src/main/java/com/tangem/tap/TapApplication.kt index 599ad22aa3..8d56db3a2b 100644 --- a/app/src/main/java/com/tangem/tap/TapApplication.kt +++ b/app/src/main/java/com/tangem/tap/TapApplication.kt @@ -45,7 +45,6 @@ import com.tangem.tap.common.analytics.handlers.appsFlyer.AppsFlyerAnalyticsHand import com.tangem.tap.common.analytics.handlers.firebase.FirebaseAnalyticsHandler import com.tangem.tap.common.analytics.topup.TopUpController import com.tangem.tap.common.chat.ChatManager -import com.tangem.tap.common.extensions.dispatchOnMain import com.tangem.tap.common.feedback.AdditionalFeedbackInfo import com.tangem.tap.common.feedback.FeedbackManager import com.tangem.tap.common.images.createCoilImageLoader @@ -240,10 +239,10 @@ internal class TapApplication : Application(), ImageLoaderFactory { // TODO: Try to performance and user experience. // [REDACTED_JIRA] runBlocking { + walletsRepository.initialize() initUserWalletsListManager() featureTogglesManager.init() appRatingRepository.initialize() - walletsRepository.initialize() // learn2earnInteractor.init() } @@ -416,6 +415,6 @@ internal class TapApplication : Application(), ImageLoaderFactory { UserWalletsListManager.provideRuntimeImplementation() } - store.dispatchOnMain(GlobalAction.UpdateUserWalletsListManager(manager)) + store.dispatch(GlobalAction.UpdateUserWalletsListManager(manager)) } } \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesOperations.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesOperations.kt index 4271ce732e..7037cb1458 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesOperations.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesOperations.kt @@ -217,9 +217,12 @@ internal class CurrenciesStatusesOperations( private fun getQuotes(tokensIds: NonEmptySet): Flow>> { return quotesRepository.getQuotesUpdates(tokensIds) - .map, Either>> { it.right() } - .catch { emit(Error.DataError(it).left()) } - .onEmpty { emit(Error.EmptyQuotes.left()) } + .map, Either>> { quotes -> + if (quotes.isEmpty()) Error.EmptyQuotes.left() else quotes.right() + } + .catch { + emit(Error.DataError(it).left()) + } } private fun getNetworksStatuses(networks: NonEmptySet): Flow>> { diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/GetPrimaryCurrencyStatusUpdatesUseCaseTest.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/GetPrimaryCurrencyStatusUpdatesUseCaseTest.kt index 4abddd57fa..6cdc76877c 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/GetPrimaryCurrencyStatusUpdatesUseCaseTest.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/GetPrimaryCurrencyStatusUpdatesUseCaseTest.kt @@ -101,7 +101,7 @@ internal class GetPrimaryCurrencyStatusUpdatesUseCaseTest { fun `when quotes flow is empty then no quote status should be received`() = runTest { val expectedResult = MockTokensStates.noQuotesTokensStatuses.first().right() - val useCase = getUseCase(quotes = flowOf()) + val useCase = getUseCase(quotes = flowOf(emptySet().right())) // When val result = useCase(userWalletId).first() diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/GetTokenListUseCaseTest.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/GetTokenListUseCaseTest.kt index 7736efaf5c..7702463cf6 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/GetTokenListUseCaseTest.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/GetTokenListUseCaseTest.kt @@ -269,7 +269,7 @@ internal class GetTokenListUseCaseTest { val useCase = getUseCase( statuses = flowOf(MockNetworks.verifiedNetworksStatuses.right()), - quotes = flowOf(), + quotes = flowOf(emptySet().right()), ) // When From a730af0f1f5e10815bfd67ae647987fbaef5cfa9 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 10 Oct 2023 22:14:03 +0300 Subject: [PATCH 182/242] Updated on 2026-08-14 --- .../tap/di/domain/TokensDomainModule.kt | 8 +++ .../send/redux/middlewares/SendMiddleware.kt | 57 ++++++++++--------- .../tap/features/send/ui/SendViewModel.kt | 10 +--- .../repository/DefaultNetworksRepository.kt | 57 +++++++++++++++++++ .../DefaultWalletManagersFacade.kt | 34 +++++++++++ .../walletmanager/WalletManagersFacade.kt | 9 +++ .../tokens/FetchPendingTransactionsUseCase.kt | 22 +++++++ .../tokens/repository/NetworksRepository.kt | 8 +++ .../repository/MockNetworksRepository.kt | 4 ++ 9 files changed, 174 insertions(+), 35 deletions(-) create mode 100644 domain/tokens/src/main/kotlin/com/tangem/domain/tokens/FetchPendingTransactionsUseCase.kt diff --git a/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt index 9bbf1229fa..330be53972 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt @@ -28,6 +28,14 @@ internal object TokensDomainModule { return FetchTokenListUseCase(currenciesRepository, networksRepository, quotesRepository) } + @Provides + @ViewModelScoped + fun provideFetchPendingTransactionsUseCase( + networksRepository: NetworksRepository, + ): FetchPendingTransactionsUseCase { + return FetchPendingTransactionsUseCase(networksRepository) + } + @Provides @ViewModelScoped fun provideGetTokenListUseCase( diff --git a/app/src/main/java/com/tangem/tap/features/send/redux/middlewares/SendMiddleware.kt b/app/src/main/java/com/tangem/tap/features/send/redux/middlewares/SendMiddleware.kt index 4e0490a4f9..1af3704e50 100644 --- a/app/src/main/java/com/tangem/tap/features/send/redux/middlewares/SendMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/send/redux/middlewares/SendMiddleware.kt @@ -12,7 +12,6 @@ import com.tangem.blockchain.common.transaction.Fee import com.tangem.blockchain.extensions.SimpleResult import com.tangem.common.core.TangemSdkError import com.tangem.common.extensions.guard -import com.tangem.common.services.Result import com.tangem.core.analytics.Analytics import com.tangem.core.navigation.NavigationAction import com.tangem.domain.common.TapWorkarounds.isStart2Coin @@ -174,33 +173,35 @@ private fun sendTransaction( transactionExtras.tonMemoState?.memo?.let { txData = txData.copy(extras = TonTransactionExtras(it)) } scope.launch { - val updateWalletResult = walletManager.safeUpdate() - if (updateWalletResult is Result.Failure) { - withMainContext { - when (val error = updateWalletResult.error) { - is TapError -> store.dispatchErrorNotification(error) - is BlockchainSdkError -> { - updateFeedbackManagerInfo( - walletManager = walletManager, - amountToSend = amountToSend, - feeAmount = fee.amount, - destinationAddress = destinationAddress, - ) - dispatch(SendAction.Dialog.SendTransactionFails.BlockchainSdkError(error = error)) - } - else -> { - val tapError = if (error.message == null) { - TapError.UnknownError - } else { - TapError.CustomError(error.message!!) - } - store.dispatchErrorNotification(tapError) - } - } - dispatch(SendAction.ChangeSendButtonState(ButtonState.ENABLED)) - } - return@launch - } + // TODO: Risky commented this part, unknown logic, need to test if removed + // TODO: [REDACTED_JIRA] + // val updateWalletResult = walletManager.safeUpdate() + // if (updateWalletResult is Result.Failure) { + // withMainContext { + // when (val error = updateWalletResult.error) { + // is TapError -> store.dispatchErrorNotification(error) + // is BlockchainSdkError -> { + // updateFeedbackManagerInfo( + // walletManager = walletManager, + // amountToSend = amountToSend, + // feeAmount = fee.amount, + // destinationAddress = destinationAddress, + // ) + // dispatch(SendAction.Dialog.SendTransactionFails.BlockchainSdkError(error = error)) + // } + // else -> { + // val tapError = if (error.message == null) { + // TapError.UnknownError + // } else { + // TapError.CustomError(error.message!!) + // } + // store.dispatchErrorNotification(tapError) + // } + // } + // dispatch(SendAction.ChangeSendButtonState(ButtonState.ENABLED)) + // } + // return@launch + // } val tangemSdk = store.state.daggerGraphState.get(DaggerGraphState::cardSdkConfigRepository).sdk val linkedTerminalState = tangemSdk.config.linkedTerminal diff --git a/app/src/main/java/com/tangem/tap/features/send/ui/SendViewModel.kt b/app/src/main/java/com/tangem/tap/features/send/ui/SendViewModel.kt index 06cb71c643..999140cff9 100644 --- a/app/src/main/java/com/tangem/tap/features/send/ui/SendViewModel.kt +++ b/app/src/main/java/com/tangem/tap/features/send/ui/SendViewModel.kt @@ -3,6 +3,7 @@ package com.tangem.tap.features.send.ui import androidx.lifecycle.* import com.tangem.domain.balancehiding.IsBalanceHiddenUseCase import com.tangem.domain.balancehiding.ListenToFlipsUseCase +import com.tangem.domain.tokens.FetchPendingTransactionsUseCase import com.tangem.domain.tokens.UpdateDelayedNetworkStatusUseCase import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.Network @@ -32,6 +33,7 @@ internal class SendViewModel @Inject constructor( private val listenToFlipsUseCase: ListenToFlipsUseCase, private val updateDelayedCurrencyStatusUseCase: UpdateDelayedNetworkStatusUseCase, private val getSelectedWalletUseCase: GetSelectedWalletUseCase, + private val fetchPendingTransactionsUseCase: FetchPendingTransactionsUseCase, @DelayedWork private val coroutineScope: CoroutineScope, savedStateHandle: SavedStateHandle, ) : ViewModel(), DefaultLifecycleObserver { @@ -75,12 +77,7 @@ internal class SendViewModel @Inject constructor( } private suspend fun updateForPendingTx(userWallet: UserWallet, network: Network) { - updateDelayedCurrencyStatusUseCase( - userWalletId = userWallet.walletId, - network = network, - delayMillis = UPDATE_PENDING_TX_DELAY_MILLIS, - refresh = true, - ) + fetchPendingTransactionsUseCase(userWallet.walletId, setOf(network)) } private suspend fun updateForBalance(userWallet: UserWallet, network: Network) { @@ -94,7 +91,6 @@ internal class SendViewModel @Inject constructor( companion object { private const val UPDATE_BALANCE_DELAY_MILLIS = 11000L - private const val UPDATE_PENDING_TX_DELAY_MILLIS = 1000L private const val TAG = "SendViewModel" } } \ No newline at end of file diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultNetworksRepository.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultNetworksRepository.kt index 7a8a4e726f..673b9fdf4c 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultNetworksRepository.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultNetworksRepository.kt @@ -51,6 +51,12 @@ internal class DefaultNetworksRepository( } }.cancellable() + override suspend fun fetchNetworkPendingTransactions(userWalletId: UserWalletId, networks: Set) { + withContext(dispatchers.io) { + fetchNetworksPendingTransactionsIfCacheExpired(userWalletId, networks, false) + } + } + override suspend fun getNetworkStatusesSync( userWalletId: UserWalletId, networks: Set, @@ -76,6 +82,22 @@ internal class DefaultNetworksRepository( } } + private suspend fun fetchNetworksPendingTransactionsIfCacheExpired( + userWalletId: UserWalletId, + networks: Set, + refresh: Boolean, + ) { + coroutineScope { + networks + .map { network -> + async { + fetchNetworkPendingTransactionsIfCacheExpired(userWalletId, network, refresh) + } + } + .awaitAll() + } + } + private suspend fun fetchNetworkStatusIfCacheExpired( userWalletId: UserWalletId, network: Network, @@ -88,6 +110,20 @@ internal class DefaultNetworksRepository( ) } + private suspend fun fetchNetworkPendingTransactionsIfCacheExpired( + userWalletId: UserWalletId, + network: Network, + refresh: Boolean, + ) { + val key = getNetworksStatusesCacheKey(userWalletId, network) + cacheRegistry.invalidate(key) + cacheRegistry.invokeOnExpire( + key = key, + skipCache = refresh, + block = { fetchNetworkPendingTransactions(userWalletId, network) }, + ) + } + private suspend fun fetchNetworkStatus(userWalletId: UserWalletId, network: Network) { val currencies = getCurrencies(userWalletId, network) @@ -110,6 +146,27 @@ internal class DefaultNetworksRepository( networksStatusesStore.store(userWalletId, networkStatus) } + private suspend fun fetchNetworkPendingTransactions(userWalletId: UserWalletId, network: Network) { + val currencies = getCurrencies(userWalletId, network) + + val result = walletManagersFacade.updatePendingTransactions( + userWalletId = userWalletId, + network = network, + ) + + withContext(NonCancellable) { + invalidateCacheKeyIfNeeded(userWalletId, network, result) + } + + val networkStatus = networkStatusFactory.createNetworkStatus( + network = network, + result = result, + currencies = currencies.toSet(), + ) + + networksStatusesStore.store(userWalletId, networkStatus) + } + private suspend fun getCurrencies(userWalletId: UserWalletId, network: Network): Sequence { val userWallet = requireNotNull(userWalletsStore.getSyncOrNull(userWalletId)) { "Unable to find user wallet with provided ID: $userWalletId" diff --git a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/DefaultWalletManagersFacade.kt b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/DefaultWalletManagersFacade.kt index 01a025f741..1f4caa5690 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/DefaultWalletManagersFacade.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/DefaultWalletManagersFacade.kt @@ -57,6 +57,28 @@ class DefaultWalletManagersFacade( return getAndUpdateWalletManager(userWallet, blockchain, derivationPath, extraTokens) } + override suspend fun updatePendingTransactions( + userWalletId: UserWalletId, + network: Network, + ): UpdateWalletManagerResult { + val userWallet = getUserWallet(userWalletId) + val blockchain = Blockchain.fromId(network.id.value) + val derivationPath = network.derivationPath.value + + if (derivationPath != null && !userWallet.scanResponse.hasDerivation(blockchain, derivationPath)) { + Timber.w("Derivation missed for: $blockchain") + return UpdateWalletManagerResult.MissedDerivation + } + + val walletManager = getOrCreateWalletManager(userWalletId, blockchain, derivationPath) + if (walletManager == null || blockchain == Blockchain.Unknown) { + Timber.w("Unable to get a wallet manager for blockchain: $blockchain") + return UpdateWalletManagerResult.Unreachable + } + + return getLastWalletManagerResult(walletManager) + } + override suspend fun getExploreUrl( userWalletId: UserWalletId, network: Network, @@ -196,6 +218,18 @@ class DefaultWalletManagersFacade( } } + private fun getLastWalletManagerResult(walletManager: WalletManager): UpdateWalletManagerResult { + return try { + resultFactory.getResult(walletManager) + } catch (e: BlockchainSdkError.AccountNotFound) { + resultFactory.getNoAccountResult(walletManager = walletManager, customMessage = e.customMessage) + } catch (e: Throwable) { + Timber.w(e, "Unable to update a wallet manager for: ${walletManager.wallet.blockchain}") + + UpdateWalletManagerResult.Unreachable + } + } + override suspend fun getOrCreateWalletManager( userWalletId: UserWalletId, blockchain: Blockchain, diff --git a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/WalletManagersFacade.kt b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/WalletManagersFacade.kt index 6df4bf3f84..19e400b067 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/WalletManagersFacade.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/WalletManagersFacade.kt @@ -36,6 +36,15 @@ interface WalletManagersFacade { extraTokens: Set, ): UpdateWalletManagerResult + /** + * Returns [UpdateWalletManagerResult] with last pending transactions + * + * @param userWalletId The ID of the user's wallet. + * @param network The network. + * @return The result of updating the wallet manager. + */ + suspend fun updatePendingTransactions(userWalletId: UserWalletId, network: Network): UpdateWalletManagerResult + /** * Returns network explorer URL of the wallet manager associated with a user's wallet and network. * diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/FetchPendingTransactionsUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/FetchPendingTransactionsUseCase.kt new file mode 100644 index 0000000000..aeda66d1f9 --- /dev/null +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/FetchPendingTransactionsUseCase.kt @@ -0,0 +1,22 @@ +package com.tangem.domain.tokens + +import com.tangem.domain.tokens.model.Network +import com.tangem.domain.tokens.repository.NetworksRepository +import com.tangem.domain.wallets.models.UserWalletId +import kotlinx.coroutines.coroutineScope + +/** + * Use case responsible for fetching current pending transactions + * + * @param networksRepository The repository for retrieving network-related data. + */ +class FetchPendingTransactionsUseCase( + private val networksRepository: NetworksRepository, +) { + + suspend operator fun invoke(userWalletId: UserWalletId, networks: Set) { + coroutineScope { + networksRepository.fetchNetworkPendingTransactions(userWalletId, networks) + } + } +} \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/NetworksRepository.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/NetworksRepository.kt index 30baab37f3..8e1b1103ff 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/NetworksRepository.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/NetworksRepository.kt @@ -20,6 +20,14 @@ interface NetworksRepository { */ fun getNetworkStatusesUpdates(userWalletId: UserWalletId, networks: Set): Flow> + /** + * Fetches pending transactions for given network + * + * @param userWalletId The unique identifier of the user wallet. + * @param networks A set of network which statuses are to be retrieved. + */ + suspend fun fetchNetworkPendingTransactions(userWalletId: UserWalletId, networks: Set) + /** * Retrieves network statuses of specified blockchain networks for a specific user wallet. * diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockNetworksRepository.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockNetworksRepository.kt index ffefd750e0..f345dbedeb 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockNetworksRepository.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockNetworksRepository.kt @@ -21,6 +21,10 @@ internal class MockNetworksRepository( return statuses.map { it.getOrElse { e -> throw e } } } + override suspend fun fetchNetworkPendingTransactions(userWalletId: UserWalletId, networks: Set) { + // no-op + } + override suspend fun getNetworkStatusesSync( userWalletId: UserWalletId, networks: Set, From 98915e2507f6ece3575df6eaf8920eaf5781cf2e Mon Sep 17 00:00:00 2001 From: Tangem Date: Sun, 8 Oct 2023 21:10:03 +0300 Subject: [PATCH 183/242] Updated on 2026-08-14 --- .../tap/di/domain/TokensDomainModule.kt | 11 +++ .../repository/DefaultCurrenciesRepository.kt | 24 ++++++ .../utils/CardCryptoCurrenciesFactory.kt | 24 ++++++ .../domain/tokens/GetCardTokensListUseCase.kt | 70 +++++++++++++++ .../tokens/GetCurrencyStatusUpdatesUseCase.kt | 19 ++++- .../tokens/GetCurrencyWarningsUseCase.kt | 17 +++- .../tokens/GetNetworkCoinStatusUseCase.kt | 11 ++- .../CurrenciesStatusesOperations.kt | 85 +++++++++++++++++++ .../tokens/operations/TokenListOperations.kt | 26 ++++++ .../tokens/repository/CurrenciesRepository.kt | 25 ++++++ .../repository/MockCurrenciesRepository.kt | 11 +++ .../viewmodels/TokenDetailsViewModel.kt | 43 ++++++---- .../wallet/state/WalletMultiCurrencyState.kt | 1 + .../state/factory/TokenListWithWallet.kt | 9 ++ .../WalletLoadedTokensListConverter.kt | 4 +- .../factory/WalletSkeletonStateConverter.kt | 4 +- .../state/factory/WalletStateFactory.kt | 5 +- .../presentation/wallet/ui/WalletScreen.kt | 2 +- .../utils/TokenListToContentItemsConverter.kt | 35 +++++--- .../utils/TokenListToWalletStateConverter.kt | 11 ++- .../wallet/viewmodels/WalletViewModel.kt | 56 +++++++++++- 21 files changed, 448 insertions(+), 45 deletions(-) create mode 100644 domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCardTokensListUseCase.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/TokenListWithWallet.kt diff --git a/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt index 330be53972..ba41f1dda4 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt @@ -47,6 +47,17 @@ internal object TokensDomainModule { return GetTokenListUseCase(currenciesRepository, quotesRepository, networksRepository, dispatchers) } + @Provides + @ViewModelScoped + fun provideGetCardTokensListUseCase( + currenciesRepository: CurrenciesRepository, + quotesRepository: QuotesRepository, + networksRepository: NetworksRepository, + dispatchers: CoroutineDispatcherProvider, + ): GetCardTokensListUseCase { + return GetCardTokensListUseCase(currenciesRepository, quotesRepository, networksRepository, dispatchers) + } + @Provides @ViewModelScoped fun provideRemoveCurrencyUseCase( 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 910b4f7b61..01f5379e8f 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 @@ -26,6 +26,7 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import timber.log.Timber +@Suppress("LargeClass") internal class DefaultCurrenciesRepository( private val tangemTechApi: TangemTechApi, private val userTokensStore: UserTokensStore, @@ -152,6 +153,29 @@ internal class DefaultCurrenciesRepository( } } + override suspend fun getSingleCurrencyWalletWithCardCurrencies(userWalletId: UserWalletId): List { + return withContext(dispatchers.io) { + val userWallet = getUserWallet(userWalletId) + ensureIsCorrectUserWallet(userWallet, isMultiCurrencyWalletExpected = false) + + cardCurrenciesFactory.createCurrenciesForSingleCurrencyCardWithToken(userWallet.scanResponse) + } + } + + override suspend fun getSingleCurrencyWalletWithCardCurrency( + userWalletId: UserWalletId, + id: CryptoCurrency.ID, + ): CryptoCurrency { + return withContext(dispatchers.io) { + val userWallet = getUserWallet(userWalletId) + ensureIsCorrectUserWallet(userWallet, isMultiCurrencyWalletExpected = false) + + val currency = cardCurrenciesFactory.createCurrenciesForSingleCurrencyCardWithToken(userWallet.scanResponse) + .find { it.id == id } + requireNotNull(currency) { "Unable to find currency with provided ID: $id" } + } + } + override fun getMultiCurrencyWalletCurrenciesUpdates(userWalletId: UserWalletId): Flow> { return channelFlow { val userWallet = getUserWallet(userWalletId) diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/CardCryptoCurrenciesFactory.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/CardCryptoCurrenciesFactory.kt index 11e573b876..a2993452d9 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/CardCryptoCurrenciesFactory.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/CardCryptoCurrenciesFactory.kt @@ -58,4 +58,28 @@ internal class CardCryptoCurrenciesFactory(private val demoConfig: DemoConfig) { return primaryToken ?: coin } + + fun createCurrenciesForSingleCurrencyCardWithToken(scanResponse: ScanResponse): List { + val cardDerivationStyleProvider = scanResponse.derivationStyleProvider + val resolver = scanResponse.cardTypesResolver + val blockchain = resolver.getBlockchain() + + val coin = cryptoCurrencyFactory.createCoin( + blockchain = blockchain, + extraDerivationPath = null, + derivationStyleProvider = cardDerivationStyleProvider, + ) + requireNotNull(coin) { "Coin for the single currency card cannot be null" } + + val primaryToken = resolver.getPrimaryToken()?.let { token -> + cryptoCurrencyFactory.createToken( + sdkToken = token, + blockchain = blockchain, + extraDerivationPath = null, + derivationStyleProvider = cardDerivationStyleProvider, + ) + } + + return listOfNotNull(coin, primaryToken) + } } \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCardTokensListUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCardTokensListUseCase.kt new file mode 100644 index 0000000000..efe0126403 --- /dev/null +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCardTokensListUseCase.kt @@ -0,0 +1,70 @@ +package com.tangem.domain.tokens + +import arrow.core.Either +import arrow.core.left +import com.tangem.domain.tokens.error.TokenListError +import com.tangem.domain.tokens.error.mapper.mapToTokenListError +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.tokens.model.TokenList +import com.tangem.domain.tokens.operations.CurrenciesStatusesOperations +import com.tangem.domain.tokens.operations.TokenListOperations +import com.tangem.domain.tokens.repository.CurrenciesRepository +import com.tangem.domain.tokens.repository.NetworksRepository +import com.tangem.domain.tokens.repository.QuotesRepository +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.* + +class GetCardTokensListUseCase( + internal val currenciesRepository: CurrenciesRepository, + internal val quotesRepository: QuotesRepository, + internal val networksRepository: NetworksRepository, + internal val dispatchers: CoroutineDispatcherProvider, +) { + + @OptIn(ExperimentalCoroutinesApi::class) + operator fun invoke(userWalletId: UserWalletId): Flow> { + return getTokensStatuses(userWalletId).transformLatest { maybeTokens -> + maybeTokens.fold( + ifLeft = { error -> + emit(error.left()) + }, + ifRight = { tokens -> + emitAll(createTokenList(userWalletId, tokens)) + }, + ) + } + } + + private fun getTokensStatuses( + userWalletId: UserWalletId, + ): Flow>> { + val operations = CurrenciesStatusesOperations( + userWalletId = userWalletId, + currenciesRepository = currenciesRepository, + quotesRepository = quotesRepository, + networksRepository = networksRepository, + ) + + return operations.getCardCurrenciesStatusesFlow() + .map { maybeCurrenciesStatuses -> + maybeCurrenciesStatuses.mapLeft(CurrenciesStatusesOperations.Error::mapToTokenListError) + } + } + + private fun createTokenList( + userWalletId: UserWalletId, + tokens: List, + ): Flow> { + val operations = TokenListOperations( + userWalletId = userWalletId, + tokens = tokens, + currenciesRepository = currenciesRepository, + ) + + return operations.getTokenListForSingleCurrencyFlow().map { maybeTokenList -> + maybeTokenList.mapLeft(TokenListOperations.Error::mapToTokenListError) + } + } +} \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCurrencyStatusUpdatesUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCurrencyStatusUpdatesUseCase.kt index f575854cc6..97f340256f 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCurrencyStatusUpdatesUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCurrencyStatusUpdatesUseCase.kt @@ -34,15 +34,24 @@ class GetCurrencyStatusUpdatesUseCase( * @param userWalletId The unique identifier of the user's wallet. * @param currencyId The unique identifier of the cryptocurrency. * @param derivationPath currency derivation path. + * @param isSingleWalletWithTokens Indicates whether the user wallet contains only one token on card (old cards) * @return A [Flow] emitting either a [CurrencyStatusError] or a [CryptoCurrencyStatus], indicating the result of the fetch operation. */ operator fun invoke( userWalletId: UserWalletId, currencyId: CryptoCurrency.ID, derivationPath: Network.DerivationPath, + isSingleWalletWithTokens: Boolean, ): Flow> { return flow { - emitAll(getCurrency(userWalletId, currencyId, derivationPath)) + emitAll( + getCurrency( + userWalletId, + currencyId, + derivationPath, + isSingleWalletWithTokens, + ), + ) }.flowOn(dispatchers.io) } @@ -50,6 +59,7 @@ class GetCurrencyStatusUpdatesUseCase( userWalletId: UserWalletId, currencyId: CryptoCurrency.ID, derivationPath: Network.DerivationPath, + isSingleWalletWithTokens: Boolean, ): Flow> { val operations = CurrenciesStatusesOperations( currenciesRepository = currenciesRepository, @@ -58,7 +68,12 @@ class GetCurrencyStatusUpdatesUseCase( userWalletId = userWalletId, ) - return operations.getCurrencyStatusFlow(currencyId, derivationPath).map { maybeCurrency -> + val currencyFlow = if (isSingleWalletWithTokens) { + operations.getCurrencyStatusSingleWalletWithTokensFlow(currencyId) + } else { + operations.getCurrencyStatusFlow(currencyId, derivationPath) + } + return currencyFlow.map { maybeCurrency -> maybeCurrency.mapLeft(CurrenciesStatusesOperations.Error::mapToCurrencyError) } } diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCurrencyWarningsUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCurrencyWarningsUseCase.kt index 994eda5490..b79ffa3f49 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCurrencyWarningsUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCurrencyWarningsUseCase.kt @@ -25,6 +25,7 @@ class GetCurrencyWarningsUseCase( userWalletId: UserWalletId, currency: CryptoCurrency, derivationPath: Network.DerivationPath, + isSingleWalletWithTokens: Boolean, ): Flow> { return combine( getFeeWarningFlow( @@ -32,6 +33,7 @@ class GetCurrencyWarningsUseCase( networkId = currency.network.id, currencyId = currency.id, derivationPath = derivationPath, + isSingleWalletWithTokens = isSingleWalletWithTokens, ), flowOf(walletManagersFacade.getRentInfo(userWalletId, currency.network)), flowOf(walletManagersFacade.getExistentialDeposit(userWalletId, currency.network)), @@ -54,6 +56,7 @@ class GetCurrencyWarningsUseCase( networkId: Network.ID, currencyId: CryptoCurrency.ID, derivationPath: Network.DerivationPath, + isSingleWalletWithTokens: Boolean, ): Flow { val operations = CurrenciesStatusesOperations( currenciesRepository = currenciesRepository, @@ -62,9 +65,19 @@ class GetCurrencyWarningsUseCase( userWalletId = userWalletId, ) + val currencyFlow = if (isSingleWalletWithTokens) { + operations.getCurrencyStatusSingleWalletWithTokensFlow(currencyId) + } else { + operations.getCurrencyStatusFlow(currencyId, derivationPath) + } + val networkFlow = if (isSingleWalletWithTokens) { + operations.getNetworkCoinForSingleWalletWithTokenFlow(networkId) + } else { + operations.getNetworkCoinFlow(networkId, derivationPath) + } return combine( - operations.getCurrencyStatusFlow(currencyId, derivationPath).map { it.getOrNull() }, - operations.getNetworkCoinFlow(networkId, derivationPath).map { it.getOrNull() }, + currencyFlow.map { it.getOrNull() }, + networkFlow.map { it.getOrNull() }, ) { tokenStatus, coinStatus -> when { tokenStatus != null && coinStatus != null -> { diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetNetworkCoinStatusUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetNetworkCoinStatusUseCase.kt index 44ca1e09cc..f33b882c8e 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetNetworkCoinStatusUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetNetworkCoinStatusUseCase.kt @@ -24,6 +24,7 @@ class GetNetworkCoinStatusUseCase( userWalletId: UserWalletId, networkId: Network.ID, derivationPath: Network.DerivationPath, + isSingleWalletWithTokens: Boolean, ): Flow> { return flow { emitAll( @@ -31,6 +32,7 @@ class GetNetworkCoinStatusUseCase( userWalletId = userWalletId, networkId = networkId, derivationPath = derivationPath, + isSingleWalletWithTokens = isSingleWalletWithTokens, ), ) } @@ -41,6 +43,7 @@ class GetNetworkCoinStatusUseCase( userWalletId: UserWalletId, networkId: Network.ID, derivationPath: Network.DerivationPath, + isSingleWalletWithTokens: Boolean, ): Flow> { val operations = CurrenciesStatusesOperations( currenciesRepository = currenciesRepository, @@ -48,8 +51,12 @@ class GetNetworkCoinStatusUseCase( networksRepository = networksRepository, userWalletId = userWalletId, ) - - return operations.getNetworkCoinFlow(networkId, derivationPath).map { maybeCurrency -> + val networkFlow = if (isSingleWalletWithTokens) { + operations.getNetworkCoinForSingleWalletWithTokenFlow(networkId) + } else { + operations.getNetworkCoinFlow(networkId, derivationPath) + } + return networkFlow.map { maybeCurrency -> maybeCurrency.mapLeft(CurrenciesStatusesOperations.Error::mapToCurrencyError) } } diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesOperations.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesOperations.kt index 7037cb1458..6af9729e8b 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesOperations.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesOperations.kt @@ -67,6 +67,44 @@ internal class CurrenciesStatusesOperations( } } + fun getCardCurrenciesStatusesFlow(): Flow>> { + return flow { + val nonEmptyCurrencies = recover( + block = { getCurrenciesFromCard(userWalletId) }, + recover = { + emit(it.left()) + return@flow + }, + ).toNonEmptyListOrNull() + + if (nonEmptyCurrencies == null) { + val emptyCurrenciesStatuses = emptyList() + + emit(emptyCurrenciesStatuses.right()) + return@flow + } + + val maybeLoadingCurrenciesStatuses = createCurrenciesStatuses( + currencies = nonEmptyCurrencies, + maybeNetworkStatuses = null, + maybeQuotes = null, + ) + + emit(maybeLoadingCurrenciesStatuses) + + val (networks, currenciesIds) = getIds(nonEmptyCurrencies) + + val currenciesFlow = combine( + getQuotes(currenciesIds), + getNetworksStatuses(networks), + ) { maybeQuotes, maybeNetworksStatuses -> + createCurrenciesStatuses(nonEmptyCurrencies, maybeQuotes, maybeNetworksStatuses) + } + + emitAll(currenciesFlow) + } + } + suspend fun getCurrencyStatusFlow( currencyId: CryptoCurrency.ID, derivationPath: Network.DerivationPath, @@ -79,6 +117,17 @@ internal class CurrenciesStatusesOperations( return getCurrencyStatusFlow(currency) } + suspend fun getCurrencyStatusSingleWalletWithTokensFlow( + currencyId: CryptoCurrency.ID, + ): Flow> { + val currency = recover( + block = { getSingleCurrencyWalletWithCardTokensCurrency(currencyId) }, + recover = { return flowOf(it.left()) }, + ) + + return getCurrencyStatusFlow(currency) + } + suspend fun getNetworkCoinFlow( networkId: Network.ID, derivationPath: Network.DerivationPath, @@ -91,6 +140,18 @@ internal class CurrenciesStatusesOperations( return getCurrencyStatusFlow(currency) } + suspend fun getNetworkCoinForSingleWalletWithTokenFlow( + networkId: Network.ID, + ): Flow,> { + val currency = recover( + block = { getNetworkCoinForSingleWalletWithToken(networkId) }, + recover = { return flowOf(it.left()) }, + ) + + return getCurrencyStatusFlow(currency) + } + suspend fun getPrimaryCurrencyStatusFlow(): Flow> { val currency = recover( block = { getPrimaryCurrency() }, @@ -199,6 +260,14 @@ internal class CurrenciesStatusesOperations( .bind() } + private suspend fun Raise.getSingleCurrencyWalletWithCardTokensCurrency( + currencyId: CryptoCurrency.ID, + ): CryptoCurrency { + return Either.catch { currenciesRepository.getSingleCurrencyWalletWithCardCurrency(userWalletId, currencyId) } + .mapLeft { Error.DataError(it) } + .bind() + } + private suspend fun Raise.getNetworkCoin( networkId: Network.ID, derivationPath: Network.DerivationPath, @@ -208,6 +277,16 @@ internal class CurrenciesStatusesOperations( .bind() } + private suspend fun Raise.getNetworkCoinForSingleWalletWithToken(networkId: Network.ID): CryptoCurrency { + return Either.catch { + currenciesRepository.getSingleCurrencyWalletWithCardCurrencies(userWalletId) + .find { it.network.id == networkId && it is CryptoCurrency.Coin } + ?: raise(Error.DataError(IllegalStateException("Coin with network $networkId not found for this card"))) + } + .mapLeft { Error.DataError(it) } + .bind() + } + private suspend fun Raise.getPrimaryCurrency(): CryptoCurrency { return catch( block = { currenciesRepository.getSingleCurrencyWalletPrimaryCurrency(userWalletId) }, @@ -215,6 +294,12 @@ internal class CurrenciesStatusesOperations( ) } + private suspend fun Raise.getCurrenciesFromCard(userWalletId: UserWalletId): List { + return catch({ currenciesRepository.getSingleCurrencyWalletWithCardCurrencies(userWalletId) }) { + raise(Error.DataError(it)) + } + } + private fun getQuotes(tokensIds: NonEmptySet): Flow>> { return quotesRepository.getQuotesUpdates(tokensIds) .map, Either>> { quotes -> diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TokenListOperations.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TokenListOperations.kt index 1a03353ee5..1c77b372ce 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TokenListOperations.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TokenListOperations.kt @@ -39,6 +39,16 @@ internal class TokenListOperations( } } + fun getTokenListForSingleCurrencyFlow(): Flow> { + return flow { + emit( + either { + createTokenList() + }, + ) + } + } + private fun Raise.createTokenList(isGrouped: Boolean, isSortedByBalance: Boolean): TokenList { val nonEmptyCurrencies = tokens.toNonEmptyListOrNull() ?: return TokenList.Empty @@ -55,6 +65,22 @@ internal class TokenListOperations( ) } + private fun Raise.createTokenList(): TokenList { + val nonEmptyCurrencies = tokens.toNonEmptyListOrNull() + ?: return TokenList.Empty + + val isAnyTokenLoading = nonEmptyCurrencies.any { it.value is CryptoCurrencyStatus.Loading } + val fiatBalanceOperations = TokenListFiatBalanceOperations(nonEmptyCurrencies, isAnyTokenLoading) + + return createTokenList( + currencies = nonEmptyCurrencies, + fiatBalance = fiatBalanceOperations.calculateFiatBalance(), + isAnyTokenLoading = isAnyTokenLoading, + isGrouped = false, + isSortedByBalance = false, + ) + } + private fun Raise.createTokenList( currencies: NonEmptyList, fiatBalance: TokenList.FiatBalance, diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/CurrenciesRepository.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/CurrenciesRepository.kt index 2973c698f6..de719ff8c0 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/CurrenciesRepository.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/CurrenciesRepository.kt @@ -68,6 +68,31 @@ interface CurrenciesRepository { */ suspend fun getSingleCurrencyWalletPrimaryCurrency(userWalletId: UserWalletId): CryptoCurrency + /** + * Retrieves the cryptocurrencies for a specific single-currency user wallet with tokens on the card. + * + * @param userWalletId The unique identifier of the user wallet. + * @return The primary cryptocurrency associated with the user wallet. + * @throws com.tangem.domain.core.error.DataError.UserWalletError.WrongUserWallet If multi-currency user wallet + * ID provided. + */ + suspend fun getSingleCurrencyWalletWithCardCurrencies(userWalletId: UserWalletId): List + + /** + * Retrieves the cryptocurrency for a specific single-currency user old wallet + * that stores token on card + * + * @param userWalletId The unique identifier of the user wallet. + * @param id The unique identifier of the cryptocurrency to be retrieved. + * @return The cryptocurrency associated with the user wallet and ID. + * @throws com.tangem.domain.core.error.DataError.UserWalletError.WrongUserWallet If single-currency user wallet + * ID provided. + */ + suspend fun getSingleCurrencyWalletWithCardCurrency( + userWalletId: UserWalletId, + id: CryptoCurrency.ID, + ): CryptoCurrency + /** * Retrieves updates of the list of cryptocurrencies within a multi-currency wallet. * diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockCurrenciesRepository.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockCurrenciesRepository.kt index c50ee469db..c3ae45ed54 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockCurrenciesRepository.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockCurrenciesRepository.kt @@ -60,6 +60,17 @@ internal class MockCurrenciesRepository( return token.getOrElse { e -> throw e } } + override suspend fun getSingleCurrencyWalletWithCardCurrencies(userWalletId: UserWalletId): List { + return tokens.first().getOrElse { e -> throw e } + } + + override suspend fun getSingleCurrencyWalletWithCardCurrency( + userWalletId: UserWalletId, + id: CryptoCurrency.ID, + ): CryptoCurrency { + return token.getOrElse { e -> throw e } + } + override fun getMultiCurrencyWalletCurrenciesUpdates(userWalletId: UserWalletId): Flow> { return tokens.map { it.getOrElse { e -> throw e } } } diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt index 4d5a4ad91a..493e8fa532 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt @@ -24,6 +24,7 @@ import com.tangem.domain.tokens.models.analytics.TokenReceiveAnalyticsEvent import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsCountUseCase import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsUseCase import com.tangem.domain.walletmanager.WalletManagersFacade +import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWalletId import com.tangem.domain.wallets.usecase.GetExploreUrlUseCase import com.tangem.domain.txhistory.usecase.GetExplorerTransactionUrlUseCase @@ -133,10 +134,12 @@ internal class TokenDetailsViewModel @Inject constructor( private fun updateWarnings() { viewModelScope.launch(dispatchers.io) { + val wallet = getUserWalletUseCase(userWalletId).getOrElse { return@launch } getCurrencyWarningsUseCase.invoke( userWalletId = userWalletId, currency = cryptoCurrency, derivationPath = cryptoCurrency.network.derivationPath, + isSingleWalletWithTokens = isSingleWalletWithTokens(wallet), ) .distinctUntilChanged() .onEach { uiState = stateFactory.getStateWithNotifications(it) } @@ -145,22 +148,30 @@ internal class TokenDetailsViewModel @Inject constructor( } private fun updateMarketPrice() { - getCurrencyStatusUpdatesUseCase( - userWalletId = userWalletId, - currencyId = cryptoCurrency.id, - derivationPath = cryptoCurrency.network.derivationPath, - ) - .distinctUntilChanged() - .onEach { either -> - uiState = stateFactory.getCurrencyLoadedBalanceState(either) - either.onRight { status -> - cryptoCurrencyStatus = status - updateButtons(userWalletId = userWalletId, currencyStatus = status) + viewModelScope.launch(dispatchers.io) { + val wallet = getUserWalletUseCase(userWalletId).getOrElse { return@launch } + getCurrencyStatusUpdatesUseCase( + userWalletId = userWalletId, + currencyId = cryptoCurrency.id, + derivationPath = cryptoCurrency.network.derivationPath, + isSingleWalletWithTokens = isSingleWalletWithTokens(wallet), + ) + .distinctUntilChanged() + .onEach { either -> + uiState = stateFactory.getCurrencyLoadedBalanceState(either) + either.onRight { status -> + cryptoCurrencyStatus = status + updateButtons(userWalletId = userWalletId, currencyStatus = status) + } } - } - .flowOn(dispatchers.io) - .launchIn(viewModelScope) - .saveIn(marketPriceJobHolder) + .flowOn(dispatchers.io) + .launchIn(viewModelScope) + .saveIn(marketPriceJobHolder) + } + } + + private fun isSingleWalletWithTokens(userWallet: UserWallet): Boolean { + return userWallet.scanResponse.walletData?.token != null && !userWallet.isMultiCurrency } /** @@ -251,10 +262,12 @@ internal class TokenDetailsViewModel @Inject constructor( private fun sendToken(status: CryptoCurrencyStatus) { viewModelScope.launch(dispatchers.io) { + val wallet = getUserWalletUseCase(userWalletId).getOrElse { return@launch } val maybeCoinStatus = getNetworkCoinStatusUseCase( userWalletId = userWalletId, networkId = status.currency.network.id, derivationPath = status.currency.network.derivationPath, + isSingleWalletWithTokens = isSingleWalletWithTokens(wallet), ).firstOrNull() maybeCoinStatus?.onRight { coinStatus -> diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletMultiCurrencyState.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletMultiCurrencyState.kt index 0a71550228..1e8fcbcc9e 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletMultiCurrencyState.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletMultiCurrencyState.kt @@ -27,6 +27,7 @@ internal sealed class WalletMultiCurrencyState : WalletState.ContentState() { override val tokensListState: WalletTokensListState, override val event: StateEvent = consumedEvent(), override val isBalanceHidden: Boolean, + val isManageTokensAvailable: Boolean = true, val tokenActionsBottomSheet: ActionsBottomSheetConfig?, val onManageTokensClick: () -> Unit, ) : WalletMultiCurrencyState() diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/TokenListWithWallet.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/TokenListWithWallet.kt new file mode 100644 index 0000000000..bd9060c2ce --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/TokenListWithWallet.kt @@ -0,0 +1,9 @@ +package com.tangem.feature.wallet.presentation.wallet.state.factory + +import com.tangem.domain.tokens.model.TokenList +import com.tangem.domain.wallets.models.UserWallet + +data class TokenListWithWallet( + val tokenList: TokenList, + val wallet: UserWallet, +) \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletLoadedTokensListConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletLoadedTokensListConverter.kt index 3cd576ca7d..f2d12adf65 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletLoadedTokensListConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletLoadedTokensListConverter.kt @@ -31,7 +31,7 @@ internal class WalletLoadedTokensListConverter( appCurrencyProvider: Provider, currentWalletProvider: Provider, clickIntents: WalletClickIntents, -) : Converter, WalletState> { +) : Converter, WalletState> { private val tokenListStateConverter = TokenListToWalletStateConverter( currentStateProvider = currentStateProvider, @@ -40,7 +40,7 @@ internal class WalletLoadedTokensListConverter( clickIntents = clickIntents, ) - override fun convert(value: Either): WalletState { + override fun convert(value: Either): WalletState { return value.fold( ifLeft = tokenListErrorConverter::convert, ifRight = tokenListStateConverter::convert, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletSkeletonStateConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletSkeletonStateConverter.kt index dbdd8d5ea9..ead7b287fc 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletSkeletonStateConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletSkeletonStateConverter.kt @@ -38,7 +38,9 @@ internal class WalletSkeletonStateConverter( override fun convert(value: SkeletonModel): WalletState.ContentState { val selectedWallet = value.wallets[value.selectedWalletIndex] - return if (selectedWallet.isMultiCurrency) { + val isSingleWalletWithToken = !selectedWallet.isMultiCurrency && + selectedWallet.scanResponse.walletData?.token != null + return if (selectedWallet.isMultiCurrency || isSingleWalletWithToken) { createMultiCurrencyState(value = value) } else { createSingleCurrencyState(value = value, currencyName = selectedWallet.getPrimaryCurrencyName()) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletStateFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletStateFactory.kt index d8ea0d3ff9..961c4078dd 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletStateFactory.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletStateFactory.kt @@ -13,7 +13,6 @@ import com.tangem.domain.tokens.error.CurrencyStatusError import com.tangem.domain.tokens.error.TokenListError import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.model.TokenActionsState -import com.tangem.domain.tokens.model.TokenList import com.tangem.domain.txhistory.models.TxHistoryItem import com.tangem.domain.txhistory.models.TxHistoryListError import com.tangem.domain.txhistory.models.TxHistoryStateError @@ -160,8 +159,8 @@ internal class WalletStateFactory( ) } - fun getStateByTokensList(maybeTokenList: Either): WalletState { - return loadedTokensListConverter.convert(maybeTokenList) + fun getStateByTokensList(maybeTokenListWithWallet: Either): WalletState { + return loadedTokensListConverter.convert(maybeTokenListWithWallet) } fun getStateByTokenListError(error: TokenListError): WalletState { diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt index 449909bf02..7cef25b9ea 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt @@ -217,7 +217,7 @@ private fun BaseScaffold( topBar = { WalletTopBar(config = state.topBarConfig) }, snackbarHost = { SnackbarHost(hostState = snackbarHostState) }, floatingActionButton = { - if (state is WalletMultiCurrencyState.Content) { + if (state is WalletMultiCurrencyState.Content && state.isManageTokensAvailable) { ManageTokensButton(onManageTokensClick = state.onManageTokensClick) } }, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/TokenListToContentItemsConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/TokenListToContentItemsConverter.kt index 45442e30c8..13626e2988 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/TokenListToContentItemsConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/TokenListToContentItemsConverter.kt @@ -9,6 +9,7 @@ import com.tangem.domain.tokens.model.TokenList import com.tangem.feature.wallet.presentation.wallet.state.components.WalletTokensListState import com.tangem.feature.wallet.presentation.wallet.state.components.WalletTokensListState.OrganizeTokensButtonState import com.tangem.feature.wallet.presentation.wallet.state.components.WalletTokensListState.TokensListItemState +import com.tangem.feature.wallet.presentation.wallet.state.factory.TokenListWithWallet import com.tangem.feature.wallet.presentation.wallet.viewmodels.WalletClickIntents import com.tangem.utils.converter.Converter import kotlinx.collections.immutable.PersistentList @@ -18,23 +19,25 @@ import kotlinx.collections.immutable.persistentListOf internal class TokenListToContentItemsConverter( appCurrencyProvider: Provider, private val clickIntents: WalletClickIntents, -) : Converter { +) : Converter { private val tokenStatusConverter = CryptoCurrencyStatusToTokenItemConverter( appCurrencyProvider = appCurrencyProvider, clickIntents = clickIntents, ) - override fun convert(value: TokenList): WalletTokensListState { - return when (value) { + override fun convert(value: TokenListWithWallet): WalletTokensListState { + val isSingleCurrencyWalletWithToken = !value.wallet.isMultiCurrency && + value.wallet.scanResponse.walletData?.token != null + return when (val tokenList = value.tokenList) { is TokenList.Empty -> WalletTokensListState.Empty is TokenList.GroupedByNetwork -> WalletTokensListState.Content( - items = value.mapToMultiCurrencyItems(), - organizeTokensButton = value.mapToOrganizeTokensButtonState(), + items = tokenList.mapToMultiCurrencyItems(), + organizeTokensButton = tokenList.mapToOrganizeTokensButtonState(isSingleCurrencyWalletWithToken), ) is TokenList.Ungrouped -> WalletTokensListState.Content( - items = value.mapToMultiCurrencyItems(), - organizeTokensButton = value.mapToOrganizeTokensButtonState(), + items = tokenList.mapToMultiCurrencyItems(), + organizeTokensButton = tokenList.mapToOrganizeTokensButtonState(isSingleCurrencyWalletWithToken), ) } } @@ -51,17 +54,23 @@ internal class TokenListToContentItemsConverter( } } - private fun TokenList.GroupedByNetwork.mapToOrganizeTokensButtonState(): OrganizeTokensButtonState { + private fun TokenList.GroupedByNetwork.mapToOrganizeTokensButtonState( + isSingleCurrencyWithTokenWallet: Boolean, + ): OrganizeTokensButtonState { return getOrganizeTokensButtonState( isLoading = totalFiatBalance is TokenList.FiatBalance.Loading, currenciesSize = groups.flatMap(NetworkGroup::currencies).size, + isSingleCurrencyWithTokenWallet = isSingleCurrencyWithTokenWallet, ) } - private fun TokenList.Ungrouped.mapToOrganizeTokensButtonState(): OrganizeTokensButtonState { + private fun TokenList.Ungrouped.mapToOrganizeTokensButtonState( + isSingleCurrencyWithTokenWallet: Boolean, + ): OrganizeTokensButtonState { return getOrganizeTokensButtonState( isLoading = totalFiatBalance is TokenList.FiatBalance.Loading, currenciesSize = currencies.size, + isSingleCurrencyWithTokenWallet = isSingleCurrencyWithTokenWallet, ) } @@ -88,8 +97,12 @@ internal class TokenListToContentItemsConverter( return this } - private fun getOrganizeTokensButtonState(isLoading: Boolean, currenciesSize: Int): OrganizeTokensButtonState { - return if (currenciesSize > 1) { + private fun getOrganizeTokensButtonState( + isLoading: Boolean, + currenciesSize: Int, + isSingleCurrencyWithTokenWallet: Boolean, + ): OrganizeTokensButtonState { + return if (currenciesSize > 1 && !isSingleCurrencyWithTokenWallet) { OrganizeTokensButtonState.Visible( isEnabled = !isLoading, onClick = clickIntents::onOrganizeTokensClick, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/TokenListToWalletStateConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/TokenListToWalletStateConverter.kt index 896f278745..2228f63598 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/TokenListToWalletStateConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/TokenListToWalletStateConverter.kt @@ -8,6 +8,7 @@ import com.tangem.feature.wallet.presentation.wallet.state.WalletMultiCurrencySt import com.tangem.feature.wallet.presentation.wallet.state.WalletSingleCurrencyState import com.tangem.feature.wallet.presentation.wallet.state.WalletState import com.tangem.feature.wallet.presentation.wallet.state.components.WalletsListConfig +import com.tangem.feature.wallet.presentation.wallet.state.factory.TokenListWithWallet import com.tangem.feature.wallet.presentation.wallet.viewmodels.WalletClickIntents import com.tangem.utils.converter.Converter import kotlinx.collections.immutable.toPersistentList @@ -18,19 +19,23 @@ internal class TokenListToWalletStateConverter( private val currentWalletProvider: Provider, private val appCurrencyProvider: Provider, clickIntents: WalletClickIntents, -) : Converter { +) : Converter { private val tokenListToContentConverter = TokenListToContentItemsConverter( appCurrencyProvider = appCurrencyProvider, clickIntents = clickIntents, ) - override fun convert(value: TokenList): WalletState { + override fun convert(value: TokenListWithWallet): WalletState { + val tokenList = value.tokenList + val isSingleCurrencyWalletWithToken = !value.wallet.isMultiCurrency && + value.wallet.scanResponse.walletData?.token != null return when (val state = currentStateProvider()) { is WalletMultiCurrencyState.Content -> { state.copy( - walletsListConfig = state.updateSelectedWallet(fiatBalance = value.totalFiatBalance), + walletsListConfig = state.updateSelectedWallet(fiatBalance = tokenList.totalFiatBalance), tokensListState = tokenListToContentConverter.convert(value = value), + isManageTokensAvailable = !isSingleCurrencyWalletWithToken, ) } is WalletMultiCurrencyState.Locked, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt index 8c97e1e814..b27abb5602 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt @@ -62,6 +62,7 @@ import com.tangem.feature.wallet.presentation.wallet.analytics.PortfolioEvent import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent import com.tangem.feature.wallet.presentation.wallet.state.* import com.tangem.feature.wallet.presentation.wallet.state.components.WalletCardState +import com.tangem.feature.wallet.presentation.wallet.state.factory.TokenListWithWallet import com.tangem.feature.wallet.presentation.wallet.state.factory.WalletStateFactory import com.tangem.operations.derivation.ExtendedPublicKeysMap import com.tangem.utils.coroutines.CoroutineDispatcherProvider @@ -92,6 +93,7 @@ internal class WalletViewModel @Inject constructor( private val updateWalletUseCase: UpdateWalletUseCase, private val deleteWalletUseCase: DeleteWalletUseCase, private val getTokenListUseCase: GetTokenListUseCase, + private val getCardTokensListUseCase: GetCardTokensListUseCase, private val fetchTokenListUseCase: FetchTokenListUseCase, private val getPrimaryCurrencyStatusUpdatesUseCase: GetPrimaryCurrencyStatusUpdatesUseCase, private val fetchCurrencyStatusUseCase: FetchCurrencyStatusUseCase, @@ -592,10 +594,14 @@ internal class WalletViewModel @Inject constructor( viewModelScope.launch(dispatchers.io) { val userWallet = getWallet(index = state.walletsListConfig.selectedWalletIndex) + val isSingleWalletWithTokens = !userWallet.isMultiCurrency && + userWallet.scanResponse.walletData?.token != null + getNetworkCoinStatusUseCase( userWalletId = userWallet.walletId, networkId = cryptoCurrencyStatus.currency.network.id, derivationPath = cryptoCurrencyStatus.currency.network.derivationPath, + isSingleWalletWithTokens = isSingleWalletWithTokens, ) .take(count = 1) .collectLatest { @@ -916,6 +922,9 @@ internal class WalletViewModel @Inject constructor( uiState = stateFactory.getLockedState() } wallet.isMultiCurrency -> getMultiCurrencyContent(wallet, index) + isSingleWalletWithTokens(wallet) -> { + getSingleCurrencyWithTokenContent(index) + } !wallet.isMultiCurrency -> getSingleCurrencyContent(index) } } @@ -933,7 +942,7 @@ internal class WalletViewModel @Inject constructor( tokenListFlow .distinctUntilChanged() .onEach { maybeTokenList -> - uiState = stateFactory.getStateByTokensList(maybeTokenList) + uiState = stateFactory.getStateByTokensList(maybeTokenList.getTokenListWithWallet(wallet)) maybeTokenList.onRight { checkMultiWalletWithFunds(it) } @@ -978,6 +987,10 @@ internal class WalletViewModel @Inject constructor( .saveIn(updateWcJobHolder) } + private fun isSingleWalletWithTokens(userWallet: UserWallet): Boolean { + return userWallet.scanResponse.walletData?.token != null && !userWallet.isMultiCurrency + } + private fun List.isAllCurrenciesLoaded(): Boolean { return !this.any { it.value is CryptoCurrencyStatus.Loading } } @@ -1008,6 +1021,14 @@ internal class WalletViewModel @Inject constructor( } } + private fun Either.getTokenListWithWallet( + userWallet: UserWallet, + ): Either { + return this.map { + TokenListWithWallet(it, userWallet) + } + } + private fun getSingleCurrencyContent(index: Int) { val wallet = getWallet(index) getPrimaryCurrencyStatusUpdatesUseCase(wallet.walletId) @@ -1032,6 +1053,30 @@ internal class WalletViewModel @Inject constructor( .saveIn(marketPriceJobHolder) } + private fun getSingleCurrencyWithTokenContent(walletIndex: Int) { + val state = requireNotNull(uiState as? WalletMultiCurrencyState) { + "Impossible to get a token list updates if state isn't WalletMultiCurrencyState" + } + + val wallet = getWallet(walletIndex) + + getCardTokensListUseCase(userWalletId = state.walletsListConfig.wallets[walletIndex].id) + .distinctUntilChanged() + .onEach { maybeTokenList -> + uiState = stateFactory.getStateByTokensList(maybeTokenList.getTokenListWithWallet(wallet)) + + maybeTokenList.onRight { checkMultiWalletWithFunds(it) } + + updateNotifications( + index = walletIndex, + tokenList = maybeTokenList.fold(ifLeft = { null }, ifRight = { it }), + ) + } + .flowOn(dispatchers.io) + .launchIn(viewModelScope) + .saveIn(tokensJobHolder) + } + private fun updateTxHistory(userWalletId: UserWalletId, currency: CryptoCurrency, refresh: Boolean) { viewModelScope.launch(dispatchers.io) { val txHistoryItemsCountEither = txHistoryItemsCountUseCase( @@ -1095,10 +1140,15 @@ internal class WalletViewModel @Inject constructor( val wallet = getWallet(walletIndex) viewModelScope.launch(dispatchers.io) { - val result = fetchTokenListUseCase(wallet.walletId, refresh = true) + if (isSingleWalletWithTokens(wallet)) { + // TODO add refresh for nodl cards ([REDACTED_JIRA]) + delay(timeMillis = 1000) + } else { + val result = fetchTokenListUseCase(wallet.walletId, refresh = true) + uiState = result.fold(stateFactory::getStateByTokenListError) { uiState } + } uiState = stateFactory.getRefreshedState() - uiState = result.fold(stateFactory::getStateByTokenListError) { uiState } }.saveIn(refreshContentJobHolder) } From 1cdc5b5ab9ea614fdafdeed9504e0cbce6751bc5 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 11 Oct 2023 18:27:56 +0500 Subject: [PATCH 184/242] Updated on 2026-08-14 --- .../viewmodels/TokenDetailsViewModel.kt | 1 - .../WalletLoadingTxHistoryConverter.kt | 35 +++++++++++-------- .../WalletTxHistoryItemFlowConverter.kt | 8 ++--- .../wallet/viewmodels/WalletViewModel.kt | 7 ++++ 4 files changed, 31 insertions(+), 20 deletions(-) diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt index 493e8fa532..6c131e438b 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt @@ -178,7 +178,6 @@ internal class TokenDetailsViewModel @Inject constructor( * @param refresh - invalidate cache and get data from remote * @param showItemsLoading - show loading items placeholder. */ - @Suppress("UnusedPrivateMember") // will be removed after implement caching private fun updateTxHistory(refresh: Boolean, showItemsLoading: Boolean) { viewModelScope.launch(dispatchers.io) { val txHistoryItemsCountEither = txHistoryItemsCountUseCase( diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/txhistory/WalletLoadingTxHistoryConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/txhistory/WalletLoadingTxHistoryConverter.kt index 65e92212f3..f0db791f6f 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/txhistory/WalletLoadingTxHistoryConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/txhistory/WalletLoadingTxHistoryConverter.kt @@ -10,6 +10,7 @@ import com.tangem.domain.txhistory.models.TxHistoryStateError import com.tangem.feature.wallet.presentation.wallet.state.WalletSingleCurrencyState import com.tangem.feature.wallet.presentation.wallet.state.WalletState import com.tangem.feature.wallet.presentation.wallet.viewmodels.WalletClickIntents +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.update /** @@ -52,23 +53,29 @@ internal class WalletLoadingTxHistoryConverter( private fun convertRight(value: Int): WalletState { val state = currentStateProvider() - val txHistoryContent = (state as? WalletSingleCurrencyState.Content)?.txHistoryState as? Content - - txHistoryContent?.contentItems?.update { - PagingData.from( - data = listOf(TxHistoryItemState.Title(onExploreClick = clickIntents::onExploreClick)) + - MutableList( - size = value, - init = { - TxHistoryItemState.Transaction( - state = TransactionState.Loading(it.toString()), - ) - }, - ), + val singleCurrencyContentState = state as? WalletSingleCurrencyState.Content ?: return state + return if (singleCurrencyContentState.txHistoryState is Content) { + singleCurrencyContentState.txHistoryState.contentItems.update { + PagingData.from(data = createLoadingItems(value)) + } + state + } else { + val txHistoryContent = Content( + contentItems = MutableStateFlow( + value = PagingData.from(data = createLoadingItems(value)), + ), ) + state.copy(txHistoryState = txHistoryContent) } + } - return state + private fun createLoadingItems(size: Int): List { + return buildList { + add(TxHistoryItemState.Title(onExploreClick = clickIntents::onExploreClick)) + (1..size).forEach { + add(TxHistoryItemState.Transaction(state = TransactionState.Loading(it.toString()))) + } + } } data class WalletLoadingTxHistoryModel(val historyLoadingState: Either) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/txhistory/WalletTxHistoryItemFlowConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/txhistory/WalletTxHistoryItemFlowConverter.kt index 245cfa94e8..e8e5b65758 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/txhistory/WalletTxHistoryItemFlowConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/txhistory/WalletTxHistoryItemFlowConverter.kt @@ -17,10 +17,7 @@ import com.tangem.utils.extensions.isToday import com.tangem.utils.extensions.isYesterday import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.flow.update +import kotlinx.coroutines.flow.* import org.joda.time.DateTime import org.joda.time.DateTimeZone @@ -49,7 +46,8 @@ internal class WalletTxHistoryItemFlowConverter( override fun convert(value: Flow>): TxHistoryState? { val state = currentStateProvider() as? WalletSingleCurrencyState ?: return null - val txHistoryContent = state.txHistoryState as? TxHistoryState.Content ?: return state.txHistoryState + val txHistoryContent = state.txHistoryState as? TxHistoryState.Content + ?: TxHistoryState.Content(contentItems = MutableStateFlow(PagingData.empty())) // FIXME: TxHistoryRepository should send loading transactions // [REDACTED_JIRA] diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt index b27abb5602..f0b0798819 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt @@ -4,6 +4,7 @@ import androidx.lifecycle.* import androidx.paging.cachedIn import arrow.core.Either import arrow.core.getOrElse +import arrow.core.right import com.tangem.blockchain.blockchains.cardano.CardanoUtils import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.address.AddressType @@ -18,6 +19,7 @@ import com.tangem.core.analytics.models.AnalyticsParam import com.tangem.core.navigation.AppScreen import com.tangem.core.ui.components.bottomsheets.tokenreceive.AddressModel import com.tangem.core.ui.components.bottomsheets.tokenreceive.TokenReceiveBottomSheetConfig +import com.tangem.core.ui.components.transactions.state.TxHistoryState import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.WrappedList import com.tangem.core.ui.extensions.resourceReference @@ -1163,6 +1165,11 @@ internal class WalletViewModel @Inject constructor( uiState = result.fold(stateFactory::getStateByCurrencyStatusError) { uiState } singleWalletCryptoCurrencyStatus?.let { + val singleCurrencyState = uiState as WalletSingleCurrencyState + if (singleCurrencyState.txHistoryState !is TxHistoryState.Content) { + // show loading indicator while refreshing in non content state + uiState = stateFactory.getLoadingTxHistoryState(1.right()) + } updateTxHistory(wallet.walletId, it.currency, refresh = true) } }.saveIn(refreshContentJobHolder) From 7141c0e6ee4171433b8d668eccbd6ff4aea6eaf8 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 11 Oct 2023 21:55:11 +0800 Subject: [PATCH 185/242] Updated on 2026-08-14 --- core/res/src/main/res/values-ru/strings.xml | 7 +++ .../src/main/res/values-zh-rTW/strings.xml | 2 + core/res/src/main/res/values/strings.xml | 9 ++-- .../tangem/domain/common/CardTypesResolver.kt | 2 + .../domain/common/TangemCardTypesResolver.kt | 4 ++ .../wallet/domain/WalletImageResolver.kt | 49 +++++++++++++----- .../state/components/WalletNotification.kt | 2 +- .../res/drawable/ill_shiba_card2_120_106.webp | Bin 0 -> 8754 bytes .../res/drawable/ill_shiba_card3_120_106.webp | Bin 0 -> 10080 bytes .../main/res/drawable/ill_twin_120_106.webp | Bin 93756 -> 0 bytes .../main/res/drawable/ill_twins_120_106.webp | Bin 0 -> 5742 bytes .../drawable/ill_wallet1_cards1_120_106.webp | Bin 0 -> 13368 bytes .../drawable/ill_wallet1_cards2_120_106.webp | Bin 0 -> 14832 bytes .../drawable/ill_wallet1_cards3_120_106.webp | Bin 0 -> 15948 bytes .../main/res/drawable/ill_wallet_120_106.webp | Bin 110596 -> 0 bytes ...webp => ill_wallet_old_white_120_106.webp} | Bin 16 files changed, 59 insertions(+), 16 deletions(-) create mode 100644 features/wallet/impl/src/main/res/drawable/ill_shiba_card2_120_106.webp create mode 100644 features/wallet/impl/src/main/res/drawable/ill_shiba_card3_120_106.webp delete mode 100644 features/wallet/impl/src/main/res/drawable/ill_twin_120_106.webp create mode 100644 features/wallet/impl/src/main/res/drawable/ill_twins_120_106.webp create mode 100644 features/wallet/impl/src/main/res/drawable/ill_wallet1_cards1_120_106.webp create mode 100644 features/wallet/impl/src/main/res/drawable/ill_wallet1_cards2_120_106.webp create mode 100644 features/wallet/impl/src/main/res/drawable/ill_wallet1_cards3_120_106.webp delete mode 100644 features/wallet/impl/src/main/res/drawable/ill_wallet_120_106.webp rename features/wallet/impl/src/main/res/drawable/{ill_old_wallet_120_106.webp => ill_wallet_old_white_120_106.webp} (100%) diff --git a/core/res/src/main/res/values-ru/strings.xml b/core/res/src/main/res/values-ru/strings.xml index 7804c88500..fbfba50e2f 100644 --- a/core/res/src/main/res/values-ru/strings.xml +++ b/core/res/src/main/res/values-ru/strings.xml @@ -219,6 +219,7 @@ Некоторые адреса отсутствуют Добавить Изменить + Чтобы отправить транзакцию %1$s, необходимо пополнить баланс %2$s (%3$s) для оплаты комиссии сети Невозможно покрыть %1$s комиссию Вам необходимо установить единый код доступа для защиты всех ваших карт Защита @@ -351,6 +352,8 @@ Пожалуйста, удерживайте карту до завершения операции Сбросить карту + Я понимаю, что после выполнения этого действия у меня больше не будет доступа к текущему кошельку + Я понимаю, что не смогу этой картой восстановить пароль на остальных картах, если я его забуду Сброс к заводским настройкам приведет к полному удалению кошелька с выбранной карты. Вы не сможете восстановить текущий кошелек или использовать данную карту для восстановления кода доступа. Сброс к заводским настройкам приведет к полному удалению кошелька с выбранной карты. Вы не сможете восстановить текущий кошелек. У вас есть карта банка другой страны, а также вид на жительство или регистрация вне РФ? @@ -530,6 +533,7 @@ Сеть %s не найдена. Пожалуйста, добавьте её и попробуйте заново. Нет открытых сессий WalletConnect Упс. Нет сессий. + Не удалось создать пару WalletConnect: %1$s Вставить из буфера обмена Сообщение для %1$s:\n%2$s Запрос на открытие сессии для\n%1$s\n\nСЕТЬ: %2$s\n\nURL: %3$s @@ -572,7 +576,10 @@ Как вам Tangem? Один вопрос Эта карта подписывала транзакции в прошлом + В настоящее время сеть недоступна. Пожалуйста, повторите попытку позже. + Некоторые сети в настоящее время недоступны. Пожалуйста, повторите попытку позже. Это тестовая карта. Не принимайте её в качестве оплаты. Эта карта должна использоваться только в целях тестирования и разработки. + Некоторые сети недоступны Отказаться Вы не закончили резервное копирование. Хотите продолжить? Да, возобновить diff --git a/core/res/src/main/res/values-zh-rTW/strings.xml b/core/res/src/main/res/values-zh-rTW/strings.xml index 01a2c72b3e..0b5c40a739 100644 --- a/core/res/src/main/res/values-zh-rTW/strings.xml +++ b/core/res/src/main/res/values-zh-rTW/strings.xml @@ -277,6 +277,7 @@ 請持有卡片直至操作完成 重置卡片 + 我了解執行此操作後,我將無法再訪問當前錢包 恢復原廠設置將從所選卡中完全刪除錢包。您將無法恢復當前錢包或使用卡恢復訪問密碼 恢復原廠設置將從所選卡中完全刪除錢包並將其從應用程序中刪除。您將無法恢復當前錢包 目前不接受俄羅斯銀行卡 @@ -456,6 +457,7 @@ 帳號尚未被創造 不支持此卡 您的 Tangem 卡是為與不同的應用程序一起工作而設計的。請查看卡片上的名稱和說明,並安裝正確的應用程序 + %s市場價格 地址已復製到剪貼板 無網路 接收中 diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index 784650d7a8..f6e5c92913 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -160,13 +160,13 @@ Passcode Before executing any command entailing a change of the card state, you will have to enter the passcode. Referral program + Flip your device screen down to quickly hide and show balances Privacy policy %s hashes Card ID Link More Cards App Currency Flip-to-Hide Balances - Flip your device screen down to quickly hide and show balances Issuer Send Feedback Signed @@ -242,6 +242,7 @@ The selected token is currently unavailable for actions within the crypto wallet. But worry not, you can express your interest by upvoting it. Upvote Choose wallet + To make a %1$s transaction you need to deposit some %2$s (%3$s) to cover the network fee Unable to cover %1$s fee You have to set up a single access code to protect all your wallets Protect @@ -371,7 +372,7 @@ Please hold the card until the operation complete Reset the Card - I realize that I\'ll lose access to my funds on this card after this action. + I understand that after performing this action, I will no longer have access to the current wallet I realize that I can\'t use this card to recover my access code on the other cards. Factory Reset will completely delete the wallet from the selected card. You will not be able to restore the current wallet or use the card to recover the access code. Factory Reset will completely delete the wallet from the selected card and remove it from the app. You will not be able to restore the current wallet. @@ -475,6 +476,7 @@ %d token %d tokens + Choose address Hide You are about to hide this token from the main screen. You can add it back anytime through the manage tokens page. Hide %s @@ -550,6 +552,7 @@ %s network not found. Please, add it first and try again. No opened WalletConnect sessions Ooops. No Sessions. + Failed to pairing WalletConnect session: %1$s Paste from clipboard Message for %1$s:\n%2$s Request to start a session for\n%1$s\n\nNETWORK: %2$s\n\nURL: %3$s @@ -592,6 +595,7 @@ There are only %s signatures available on this card. You must withdraw all of your funds. How do you like Tangem? One question + Rate the app This card has signed transactions in the past Network currently is unreachable. Please try again later. Some networks currently are unreachable. Please try again later. @@ -609,5 +613,4 @@ Scan card Use %s or scan a card to access the app Welcome back! - Choose address diff --git a/domain/legacy/src/main/java/com/tangem/domain/common/CardTypesResolver.kt b/domain/legacy/src/main/java/com/tangem/domain/common/CardTypesResolver.kt index c8dd146a5e..55153e8edc 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/common/CardTypesResolver.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/common/CardTypesResolver.kt @@ -9,6 +9,8 @@ interface CardTypesResolver { fun isTangemWallet(): Boolean + fun isShibaWallet(): Boolean + fun isWhiteWallet(): Boolean fun isWallet2(): Boolean 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 65550e62f4..2a4baa2814 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 @@ -25,6 +25,10 @@ internal class TangemCardTypesResolver( card.firmwareVersion >= FirmwareVersion.MultiWalletAvailable } + override fun isShibaWallet(): Boolean { + return card.firmwareVersion.compareTo(FirmwareVersion.KeysImportAvailable) == 0 + } + override fun isWhiteWallet(): Boolean { return walletData == null && card.firmwareVersion <= FirmwareVersion.HDWalletAvailable } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/WalletImageResolver.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/WalletImageResolver.kt index c262b71ea3..b9c019e84d 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/WalletImageResolver.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/WalletImageResolver.kt @@ -13,8 +13,9 @@ import com.tangem.feature.wallet.impl.R */ internal object WalletImageResolver { - private const val DOUBLE_WALLET_SET_BACKUP_COUNT = 2 - private const val TRIPLE_WALLET_SET_BACKUP_COUNT = 3 + private const val WALLET_WITHOUT_BACKUP_COUNT = 1 + private const val WALLET_WITH_ONE_BACKUP_COUNT = 2 + private const val WALLET_WITH_TWO_BACKUPS_COUNT = 3 /** Get a specified wallet [userWallet] image */ @DrawableRes @@ -22,9 +23,10 @@ internal object WalletImageResolver { val cardTypesResolver = userWallet.scanResponse.cardTypesResolver return when { cardTypesResolver.isWallet2() -> userWallet.resolveWallet2() - cardTypesResolver.isTangemWallet() -> R.drawable.ill_wallet_120_106 - cardTypesResolver.isWhiteWallet() -> R.drawable.ill_old_wallet_120_106 - cardTypesResolver.isTangemTwins() -> R.drawable.ill_twin_120_106 + cardTypesResolver.isShibaWallet() -> userWallet.resolveShibaWallet() + cardTypesResolver.isTangemWallet() -> userWallet.resolveWallet1() + cardTypesResolver.isWhiteWallet() -> R.drawable.ill_wallet_old_white_120_106 + cardTypesResolver.isTangemTwins() -> R.drawable.ill_twins_120_106 cardTypesResolver.isStart2Coin() -> R.drawable.ill_start2coin_120_106 cardTypesResolver.isTangemNote() -> resolveNote(blockchain = cardTypesResolver.getBlockchain()) cardTypesResolver.isDevKit() -> R.drawable.ill_dev_120_106 @@ -33,19 +35,42 @@ internal object WalletImageResolver { } private fun UserWallet.resolveWallet2(): Int? { - val count = getCardsCount() - - return if (count != null) { + return resolveWalletWithBackups { count -> when (count) { - DOUBLE_WALLET_SET_BACKUP_COUNT -> R.drawable.ill_wallet2_cards2_120_106 - TRIPLE_WALLET_SET_BACKUP_COUNT -> R.drawable.ill_wallet2_cards3_120_106 + WALLET_WITH_ONE_BACKUP_COUNT -> R.drawable.ill_wallet2_cards2_120_106 + WALLET_WITH_TWO_BACKUPS_COUNT -> R.drawable.ill_wallet2_cards3_120_106 else -> null } - } else { - null } } + private fun UserWallet.resolveShibaWallet(): Int? { + return resolveWalletWithBackups { count -> + when (count) { + WALLET_WITH_ONE_BACKUP_COUNT -> R.drawable.ill_shiba_card2_120_106 + WALLET_WITH_TWO_BACKUPS_COUNT -> R.drawable.ill_shiba_card3_120_106 + else -> null + } + } + } + + private fun UserWallet.resolveWallet1(): Int? { + return resolveWalletWithBackups { count -> + when (count) { + WALLET_WITHOUT_BACKUP_COUNT -> R.drawable.ill_wallet1_cards1_120_106 + WALLET_WITH_ONE_BACKUP_COUNT -> R.drawable.ill_wallet1_cards2_120_106 + WALLET_WITH_TWO_BACKUPS_COUNT -> R.drawable.ill_wallet1_cards3_120_106 + else -> null + } + } + } + + private fun UserWallet.resolveWalletWithBackups(resolve: (Int) -> Int?): Int? { + val count = getCardsCount() + + return if (count != null) resolve(count) else null + } + private fun resolveNote(blockchain: Blockchain): Int? { return when (blockchain) { Blockchain.Bitcoin -> R.drawable.ill_note_btc_120_106 diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/components/WalletNotification.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/components/WalletNotification.kt index 3e8335d4bd..5a2120d0e3 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/components/WalletNotification.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/components/WalletNotification.kt @@ -137,7 +137,7 @@ sealed class WalletNotification(val config: NotificationConfig) { val onCloseClick: () -> Unit, ) : WalletNotification( config = NotificationConfig( - title = resourceReference(id = R.string.warning_rate_app_title), + title = resourceReference(id = R.string.warning_rate_app_title_new), subtitle = resourceReference(id = R.string.warning_rate_app_message), iconResId = R.drawable.ic_star_24, buttonsState = NotificationConfig.ButtonsState.PairButtonsConfig( diff --git a/features/wallet/impl/src/main/res/drawable/ill_shiba_card2_120_106.webp b/features/wallet/impl/src/main/res/drawable/ill_shiba_card2_120_106.webp new file mode 100644 index 0000000000000000000000000000000000000000..4bb9688be5eb0d7eb468042c7e7a2b447b1b711e GIT binary patch literal 8754 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 Cuy->6 literal 0 HcmV?d00001 diff --git a/features/wallet/impl/src/main/res/drawable/ill_twin_120_106.webp b/features/wallet/impl/src/main/res/drawable/ill_twin_120_106.webp deleted file mode 100644 index c70a8526def5d02b55f9517e456a90662b83ce9e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 93756 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 Z#ioOquU%%qO7p;Q_0CFJgp&^#;D52hT^s-a diff --git a/features/wallet/impl/src/main/res/drawable/ill_twins_120_106.webp b/features/wallet/impl/src/main/res/drawable/ill_twins_120_106.webp new file mode 100644 index 0000000000000000000000000000000000000000..58e2196ccf23db9f8540b3a7e5f1b2ab3167c68a GIT binary patch literal 5742 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 W0UK+K&*(=!DBnx)`olLe;C}#o2=f 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 literal 0 HcmV?d00001 diff --git a/features/wallet/impl/src/main/res/drawable/ill_wallet1_cards3_120_106.webp b/features/wallet/impl/src/main/res/drawable/ill_wallet1_cards3_120_106.webp new file mode 100644 index 0000000000000000000000000000000000000000..ae1f21e142949f6db434833dce1b7d44dbb1a4d6 GIT binary patch literal 15948 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 literal 0 HcmV?d00001 diff --git a/features/wallet/impl/src/main/res/drawable/ill_wallet_120_106.webp b/features/wallet/impl/src/main/res/drawable/ill_wallet_120_106.webp deleted file mode 100644 index 890ad12eae5755529266cb45dfd2928205b99293..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 110596 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�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%0Ae@zK>2r3jD?mBA3ndq#uVQBSanw zJ=o>bz1tjkkXc{GcUE(@Cj&vp9SK52=+7TKEgLPj<>n-XC;3jE%xlu4ZYmzb-E>?MqP$?t}Ts>~KFye%PRTCq2^31ZK1hV27`8J*Zb$vp>P9_=&}UKGw0xk z0@j|6>m_*u|MV`AY%Vc;HMn>jl95b^IFjdh#s`Z$s;A~+f)$W~m1I(TXGTwq%;?V) zlN!rfX1Ky2xHGjljX_=N7qJ&=rn)*HxRyXA>SS}TXT2-`(*~c!gj^T|h)Xr+a9o>0 zS6blsH`hx5@K!vO$l=)^5-H?7eGJ(0wvX zQw^TFu+-rSmLkAKEYYfzlQR^bToiT9>QM)jo_bTWodM!9xk=(mMP*)!E9q&vo0gB2 zq+ZTvYOxDt6%kheC!A62PdX!cqjdTyi9taU(GKrNevB1ZIfyscGjjWN|H1#Azx7_? ziS&3&3MDI2`4x$d%9rtQ&2&eyeeAf0Bn%nr#X{bzt9}fXqng~pK)=gEq8$hijvO5J z?P1^pveC4HzN8n#UTB}<1`1APUQdB54-iwpSlYOVC)hmGmgm%81KLi*RO${liUj9T z18!TF7>2Dm7DB9!1xX77+_^z>3;u!am9=RcgqxHd570!ofyh;4O1m~RC6`k(BYI(4 zV+0QszJJxbHGKI8S`wTHpo=4T^!u}sS)!yP)3s^KrIU0j6VoY69SA}`i?cl&lR_YLpe6w!_Iltr$)~%AAH)Fb z65#zx$!xKd>9}IXh+fMu%ruqh>bAHrQwXu$#||ty)8&Nv)e-qSP{&Hosv3ywjwyg( ziM~7vMLjWvtq5?%SU}ucX!mTvdxo;}s=lpuB4E~EP0kY+yTfmM0PoNfwR2?Q#E^7E z{*21_bCwAvfYgLMrm^iDWDqFlj}Ex`gEWiSA_fdcn44_*c4LRM+Adib)mN#B&CAc= zYv(dBhLB8-X?L55F^~J1*MaH~cuIyCjcQUbXfw5Saz{$?ll0qLrY3r(&Iyp?dERIC zIZC)Tv0SIqB}fSD{PKC&Y@d%Q33!>en#XS^9{I0~w9@Fd_?-4;e;Js8H*_>e* z&TNY9@Qj4EIC&m{mK(m_4rSbLMxgJ97fe>j2>fyoBaB3vJK|C3=)Nbd7u}g&4l2S8 z7Zo%`5$7O!OFgD?TL7h4Odb6X6s#<6qR?h>|a&2j6-x5fCBHt z)1VKcfGOb#cmEl(F|N&>kVlZBLuNY?#w6W((@@IfE$w(@6_^HV=fxB+m>FB=*N8O# z0M*#J?A{KYDGWs(ed0R=hUruxdMQ;3P=m}#Z-T8wC6O*=gSUjUn%rd(vt0aJhbIG$ zgTb)1h1w1uPbS~=xU%fxXhTG4yx(7sb7~8~44ny)`%Ud)P@Q`bJ=u)~M-!KQbL>Jt z^PrX_G&_?6%9ou3*C;_A_`rH8Z$wth!t697S-4zx0Qh5&mf3u=na+!oZmq>9KjW?e z2QJNhQ{;>!AgL*H!3+qqJf#Vn}m`o`Iew1Qyf*<+nyCPEIBn z=9x{7%HkYd@sGcw&@;eQOB}iQH0SdgejVB&EkBLmWKvSW>NUQ8^>1a+>Iqon5n)p% zl=w=x$TCE0g=G#?(_Z;(hmO|j;vtqLXgj2mZpU?7?TwNo_qD8Hs`P3QSp|`Z+%Qpr zjcFxBLbU7oV8vQ`cM9&%M5yK8to1y@aYIpU5WepHR zB^CavYEyvcM2?CgW^H8Wsxc!v#bG$;L;xTmFkm4JQB?^O%=7ziVLFn_`VR*82s<@s zk}D^fzynrlBtaS1=xxu1+03$Jg`DZ9%K<%D>R$gkT&KCY9#Z%1=?(z@6C6z{42HoR zW)cC1%kF)n#!-Y!%uqE&&Ln`4x0wRj>g*3ya^nVDs%*{3Bn&T@hA=2?WHtW?cWpBP z8~pq{3~@9P2hEd9rp5QJ=~9Yg|8&RhfPH@Z#YzAOa8uR1Pd8`jBKD3Pc%Rqr3{j@lo;tGuHi z8P$|TA)YF>j3A;5nu$7NWYfFdWFlrLZ8`-7kN`w%5>N61HdO0P4(tlPPbWfYY2~>{ zyR98CcBtGmkNIf0H5MIYj2t`?2KS8Y7`@$UlHe}GSd<}CCq^q63Zc(yc-vJm3VMND zg`e2bC?JQTqfEzrq#y?Z1a-N~E>s?dSB{#}&|1dENF@d!0B9Ve*<3dI2ElXxpcM`9 zoY$jAcIKpU0Bj0N1~{Wru#v^eg+yBdUb;4TAeiC1(oXG=B<|!s&`qhFwl1;XtoILn zgh~LCq(j$smv%>%tID>v*dW_6FeuN)1veINJY8m5Eg=Xgnp#s?EgGo>NSI(zBu_;= zvuBkPfr(azcm&%d6ReG!L*evNQi{#%AJsGXCRfviC|`tmMv-`MZQO5CKTCO(Ics-1 zys4K>Wss@0RRPA1BfJ*{6NjZ00>1*G_$ZOGH1imSso=mR_iIzbkRr)@V*^o zRrZ)9;_Bpt=Tc9!*2wI~eq=$cjQ1?D*R|A@R~j$=d)@3s zY7Jw`E0|dx4|J$oDqs)u$Kn~hUwnB4vM(mWi` zPqTWxLB5uLqnFP5ns{cjEIh_gnYzbLcS?jKB;+W9mJ- z&m}6lKOR}{us2%GjkDXT_Av{y%^dWaxRJ%Wy5G(`a$4Z+ zRU>cyod)dAEbYN92N6hd1WU)OX?A^AV@Q(Kg@xz#QPm_m{G8K33OM>-C{~GI=S!0$ zoF8LN2-F5~SdW>UF%BkBfan;xM$V{C#XCfnJ4rei%#O9c)LNftIfaEj5X8X}2L9c< zG^^z)gga)`9@)r3R}g`a140hb3IbDRm<;-enGHPuay;Y?b8UuM{1zZ&YT!waH29u} zbRM+XsJ<7T`@TULScR}b8u1-b62apFo(uCwOH;#9jigU#YHc<-r=#ftTML1-SV2!% zcdbiwL7ULfun8?Ei6c`37?nx9HKOAdyh!>57!9z{a>n_pS==vjqr95r z=_L>PucUK+N%d;X9k);!@gRkCTgt5Au`>^yyrSH>$e5f!T+&mBxJ+)r``(yEs<=Bm zEO}g$5j`s@F$1kTyo6^Aoq9V}LlyHF<@&l+`h&Z}iJM7H3W5gd-eEG_By%~hljA{>v2JHg+^>F@s)n+@Dt(c7 zae96R&&@K?iSpP=QN;KF!*O<-W0u=c$>E#c@6h($6FQ2W%=l@ED4EEFrim+>m70J& zZ?)xW^O-juVvoK@9F?LZhfVbH?%8%9VgbS+A+LYQvlfSq#Yu`7b~8b|S>uB0iBr@89aP*c!uCxm>1z%m zDS&#lld= zs+#!ycnBl5 zyKvt{=CKDADfma9iIgi(^9q$}%qre`c29qIa}hUDWDCZ&5TpwrY(kKrRNw)L0SsVL zrLmiHJ@@_gOXS-pv=7X{A;lyhuc_q5aU-T0@r_z(c~9&;uh)6#YgU)CZwW#M z5v$2t9xmZu94@YMcdw!wBhqY?5&Iwlfy zL>&n`qhxO8G*VI?ZY0WE+n3u1E7L;lv+U`ag0$h3va8^t@er6Iy%uYA1V(NSpdpmD z%wW)&c{}MSnI%UEeRzU>yUCzoR?X?ay|Umo_ViS`X&zyIMfVRvP9a>#V+JmWQjyhR zbahkU_9q2M_nMU%1xS|NNk^E%N*Gm}m?*INxWs?_V<;8#Y* zExF_~whl}IJj~W=3O4c%0Wnx8<35_a)jsB>_aVbafE+$d(IA0F0wB&b9}+o8k?=XGa%d0M9&`th z{qMxb#v)$_cA`pOT8mApixgbIhR0guhJr!50PO zFho?Ls)@UzKYJBS3N{EMk(%bv4T};{<{!Wd&I7~~;~y{T@1(oyrPjIxDNnE8C+Bmu zdq{q4EyfBt95pPPm)m{V!N|7X9ivn)#8QSM+^OWGDKJYRPN>GMt~GnfmE2UJ&CVq= z(E=BYkj7O?wYUVnStrCCtd=G&FI(GJAHB>pNwSyPk@hGK9dR5GcPM|39BUlONv?=1 zyKyw}1Mln*F~A8GlH-rw^CNFMKZ;E?9*doTVVOE9)Y^QL#FO-EjGr+$AVH|z(c<8+ zP|g1XyJqjQ&8L*@$DZLc8N+4ziuQSW*mfxL;&<21FeLCGrJA!@ilW-)<&rw$oCi8c z3uj=NVrvK?)6CD~O|i~pPN4!yq3tid-jN}e_Td0b`o-b(AuMv07=Z|kh-D$NgwY<` zE>GB)N&nUiq=<+sr-2ELpSMl(R+m`UQs|=Ch>1&yP>>YC!y2BVLYLr>1Irke@yu}h zc_`e)B}mfCr`2P|AN>j}mPj<`UzS9(;~oou6v1T(FftNuxur&zDH2~iD`)oE#Ak+n zyA9e^v(6e_-v!cl)t}~3@rkj-Op;6!zdJx!Hj7=eaw(Pml3VIwm1(xy#2EA3>$szx|*8J^pbO8&q&^?Sg-V7#Bf%Xy)a#so2iHvZ2q|2 zn5oo6!-DF2CvNu>TY+#gneKA+xIM{AB_6&6F*VC56Rnl&WX@K*`ALUZt3Y@*$_|~z z3H*bi&m1+yK6C8C$vBGuA`t2VT#^*m{*D;pYq$!U@_}+ znycLINQuVb;xbd1{FJHj>^_IHT#HV_FFKp+%^*&pI{?Hl!fE>UJ%l4cPA44sT9QTU zNsP-u!z4ACbDG;;5)T$?;IbT`iKM4l=@Qt)trW%Q^0C24SpbP|5?~KNC^2I=bb5n9 zL!sm5BSbxq_+$%xR}pJeV~ON5=|4kzY9NxAoKCK)Ox}&is^bmrnP=Pkk~(A-Kn7U> zvH|CGzuynBPusOj)(-HTd~e~{7+^HfVuR&sNo{8AJa!`e7H6dEzJI!mv1QLPk!qqg zUK)1@R64qG#6^)4rT%2S6TC6#>gP~>Y1yVgQ&*^;J5rz#1`LKdJej7t=oRyq)abI_ zp&Y`N11cys=XcXS(6fy<5KPOsMS{QCE&zohXPal9T@%IIS znTH&!fUD~57^qc)NDLU9$?ptA#%UlG+>(%j1Jg%=(RBr(#j-c0KvX(K5`xyN+Yyyk zf{yl8TxlVO%yN4&;i~`5msHC+od+tfNyFKu+%cZ5PV}dmcMZ%NGLaM2=Aye21k1Ld zRIh`I^t1WQU@z&1x6VKEIN#vm2e! z4C#oT_E_1G?vU=dv)!4?Za3c9e2i;EQvIBNa!9^oFa!Ia4mfKsR{LnzoJPajm;D`M z?GtO>K{AeKB)a>OLjsh*a;TLFlBOpS*AzvC0h$4N?9q?xJ zu>Sed@+R>?5(7M?l}6QoD2JU=bUH;y5iTs-LIiT6^yK#T;uLN)u=NxO00L<(hRSF< zQ#CjS^}F|mn4_vBj4MA7qOsU1AKOHK3BJ4Ij&+B1@y`24zcKWXY34H7iBEelgAfuK zx{aC(JR5m83X5w&nsY=fRxndCna}Kf`eHu36ytA`x+p-9c8uHHyzc_;Bo;-fHTa+C z)nZrd)$L81C%Y>?WIxgVVw?9h8$)T(#h`dFj!v%0RATII4*Ljg2md~cI0}|G1ga)+ zfyMuX9g*#Dh#Oj5GIK_3ntV<*=aRqrDa4ODXrR9-lo!H&(Rl}O02Dx`Kt;EBdARi+ zATav$!EW*13=pd((|*66Zf1Uu$m(vHcO^V0q!S34qflBw=EZjqMD6i?hCyl;?S2)fj6uS5e{WzG-o?DwlXyO@*iMx>K8SXt9%JTy`Ho zQdC`X;G&Du61^Q{BE%{LwD2|dAg4AqHB&V(fSc@6 zC2clyxr~8r0Q)grFg}7NpSs(ewH|vVVtk6Q{}rW zj1xJQVoevg1MeW+ov|bKBUXRTm67hkfHCZlaHZ0Sx9|c#BkWI=Fe=*=<^iKFT+tSq zrfF`-w-m&M9qEX=sBXtvV`K5OV=3qImn<7!Ybjf)ZrO-LjfkuVx7y){dau1(tQ=Bb zWC1e8fsqnF2RO)Jr34rhNN6FxFl=T*f1!;pe#FK;$YDl&D>o2LFVn@BNGxn^67iKJr0o69h~*6 zKOp2Hcc&zbU@sWBMsBuTe;I}R@kSaYo@#lC?ZaY|W=0?hrk4@}W_qir)hMg%w#^^W)HtNOCrtIVqVz$IBj>^mv%NHoJC@yyL(D3911TA3IiUBnsdg&fh> zyB+S`@`-cVqm=mtpR-Lj6dVRUb$t3m{5mKn5)Dy3&IL_8g8m_`sHoC{+}GSlBMJ7PA-WL5^U zCNuD~hCt|X`1)cxza-{lujf2)EU32ldQ>O`;{FvyKM3fGE}19@8QBrTaqDFJZAYJYt+DgW0`AAA=S{MRRsr`71yaDuCZdE7&{ zH0u_;rQagK$-2aGjhl|eM3k7f9&!vy_Z(l47(yQOI+Db0+KN6J)RYBFgiIH}q6at{ z{xC2w3gyrbgh`6)w~Q+D?^(sO=yTVa8;T1ayUB3KOFM-#9b$|pUlqJJju8{OHCsl@>}Zp(7$a$bHkBC{j; zO%%I|?(mN4S8(Hi2xjy=>NfV5d!;d|m|qc%Sn9Xo!N)LDlB)@HyMO zIlQ^U@sN_C@gcF2ML(0CB9LIijVdAO(UWsj86Sgj)wUCgR9rvX3NkC=RorY5nYm-) z11cQ#;3jH*EF?wpUaGKgOY4 zN|_+aM4y2o{NWehi#PlrqV{KQX2yMQ@~2>j$U<3Rr9o}kAyuW5XDuUnF8_DM9ABr|k;6N?Mk%ZJ8F{P%NzE_>e+nnBc+G@9Dyx2A`nDb{R zS&@)(-%FIZ?M$33#V1*)#39C+vBYQxG>$F;qY2FJKnv1q%UpMf1pLF_7U{zAtvyv^ zEfz6WH})+oqJrmr=itHAS=u);uIprL!5`6w&cF}fy^8w2?Psql_WZG<8Nu{_$XU@3 zCURT+3tW*NLJ@vuUXT3DZ2R+jgU#+8k_S-EdKs_anLq4h#$>(MEiIFgpUAS;vuA_1p9FU3O^2CIfi>E~ zkM`}HZIQ54HH9P=UO^?BC~t;MEad_Lz_5}??GC{_9%Cae5PYAE`pubmb8)kN&J6`t z!Gbs>kdc{TMfvJLPID>-ay3U-AvWQvfFLPsC7Ojh{lVEuB#NP=s0c)=Z5ci=s)LMg zrzx9KlIgCtQ2w-ld|(+X6Jl8wHn_SZxidD2hEvPRDyghotESRegOEy)prA|K7tO|B z|3czQi5-H|HAxK>m8x3K)uVLg0L+0yIU~R&q#@+T%z%hz)<5(8=?`8t8Y+G)=lN{% z@d5vy)1n{kg>lacz*M7PGwn8sB1W~4V~pv-sB!TLN)f5R5zZ80 zQ}c6*`bb<6fE+_wfYC(^i+dzt_%1c+Dz`ZkeV0_&P;mAA_O9;WS!<9p5c~LvY#FsQR7lbu=6IT8 zM*^o|L{D-=@1`h=KSR>L0~BbiB2R3bXITTCmnN&Z^t%rdlLd0lBTbzzr_ z5@_Hw?1?5_qW_FhcPdci85aki4LfA}K+i3MWdaHMd2R2P}ln5HnC?;a{AwQg!Y z!1jS&8|JFsH}rfL$-Mf<`2ih#|IlVXUgzS|<0Jg19}Iu`8SGj2`tmnppM*WQOVa3T`ZHqWYSRN3p68~_Yr zWn11m!ygu#rSUKh zUu_P31c@>(Kym2wcH>>N%OkKyxvs`VqpYv;IFL=k9bC6Frf1!=)cvcRmy^Dq!a;@f z>@A!_9cOwGQ*GkK$%jybHNf|o$3jgS&9XMm&j&e{nD&jUwD@ zyB#2=YSI=JQNP>oBiq;h|9v)nqaZQJ2s&GAv^2Anpq&{p%o92hbz-ua01ys_NTbc_ z!=NM}QA=JrLc*}VQ zhd~GlcS`A#%R4{H!G|B)fEYQ3*t<3@F#rw9-g0oslzg@a3&e5fP zH63!c3i$03fFMu`hmRwUaIEG7<-wrE!`lF1q44_3FgxOR??q95beJlip_m^p4$9;R z1s^$y{W!T)M_0DVQpx0Bh=%HjGa^(UXS$IOgO-p!CDIOws$!6qz;7xSZ!u;WS4xU0 zSJuYvtDfo!87WUYh*x2CsfAFASmqX!DUB6X2a%fQ7T3fes+7t617#grX9nG2Sj(k& zCiKy@g&Luk_;#aM`=hZe0cuzjGPTt+8w|v_wo|5CZK_R&m(|Vxf)!D>5W$E5C<84S z=o(5IHy+JHrGREyT~qvKQQGV=`NM25mFv2lmL0~OR0e^F;oDRW)RbQ5lut0{W+rZ; zeeQ_>RugeS2t%kPshnZWGV2Yy$jj7~GsBQU$EcmBkJPJvXC>_jgq|#IZr~7V@`#W; zRHCkmjD+z{fnY}#;EdI4vI{h#6mbY~(jg47<)Nk!CT(F<>UyF|6NIcbE{S7?o`}Ex zoSW~t+0$Xp&V$~5i6Td?ROo86pI2-H73~b=RBjX^O`0&2<0kDG%W}lkC8ojS-9JL4mXaXFVN;Og zDy5c!e@if}1lW~K&bpQrHHQ(-GR|LeQ^lSpV1l6MM`GHU}FNLz&b>!9rG zh}!(1>ON@*5d=tMYIE92GUrot>Zea@4RE?2&QraqYH0Ggud#b4X~}vhnz$v-oMMAbkYq62%%t0Q9w=m89eWFtho7HW)oh?WV+l@ih3 zzk~k@a3(6rU!|aY@_PGt7xx2dMTVihNaVW=aSZjj{cq2!0N_Z^?1r2O5Qw0z^KVZXHx{xTNWy(*xenDwcFYKA z6$_bOJ^m2L|H9Wp+)>>D;D3=cs?OY)xM1Oohz?mx%@M1I^V#eWnUheiM|AOWX^vpN zyk*oW0=#pay%bW14Kj69nNQACzR#JBT>TVtK$zepUOb%7!EKDEoZ%Mx`xFPWo)sECz>q+8Q|he{g<~K(G}HQ^|RV{(wxuy znUKY3`Q%@|N3ZHH4BN>oAY{XGuU*)~Ijrmx6XdqFq{Fq(GHO=1UC=ewb}m#-x3Rl9jkOOpbanqCnWfLzoyg*LAe^*`K&aenkuS6XgdT8C)`ZhLH z*(u8CU@~E`GM`VVkDDB*x3|yy=7Rw=mW}lRi|x*9Wdp__2wl6iftL2W z7;B_#YGlWGcZDV2O8k4Uj~bH--bz6q&j)(n)Dht~eQBzE%Cy0TFrWKy zD2i01TEf(Y!%&0RF}Z=4+57%5?ZxgxcL09<`+?F}-)Xi*I3j8p(v0J3`Jh^Do8uz@ zjC;#pjM^Dj{`Y4&?cm-O5r=SUq@o)0UX5q7h~qAh{}8^~JsH=KsYD61h}^}7$fDQD z_4ZnG@J7DucO)^O1$SCyNbyHaw`P<_$pc^jZ1VHoLz&{B*sCi|Qe|PW?e_AYd99G6 zfk?CNwdY`C=wl|j`=mE28;y2dbeI~If;ZrvXYQ>xD`RlR`7zQ|7O?FG0^xx2XQl2jYEX!tYTMWu|UAGdED4NVW2e)vNjc_BX^WqQG%_vA2J*csN3y*1{H(2 z3XG~4{(;=Y(^O_gyb!6PMoz^rYI~h5^cU1^? zO%od$IIgm;=n{Ubv(WntEtw{#SP9A^ax4ML2eBl9VZi8Mq+1~|u}C{WfHVQ?3*|io zU7{=<<9HZ=;g;)}LCSNsY!C=}0-@2iah~^@KIr!&u?Q8oW{00^G}l*JdFvE=6V>na zW8QxA{(i(9Oc4tY@6(&SWY_)17B%sjUhuJ9dP{nox*+qXgJ$E09hx+IX1F;WGQaZg zgL6zCPFI-~^>WqQSg8}+je}lbU%AsyrJxx+ELPv`QnQYG?xOJjIX7jaYLdQMl=JWC zQa3s+vdZ<_861#*B&^L@Dqv7>1rL=wZ+b|-t71k93bvcP7NPQ!MK}5H)qIq)hnLpa zoYMec#3DlNQa9HWK?p6Sb5M#x-hMKUGjOYWS6;Ic9s2u9{)*;<{FL)kkqoh5;i-T|($8xkpsOATdaQ#aM$pP&s7Vjb~Zg zeHM4Sn!yoT0qOKv01DOnx8w0sj1f_m?EBr-|IShS=x5gUpNl>vCCxmCK2m5qW-zA8 zu5Y@uXDBXevThXiCT#dNR$kEQ&U9y0vD0(8-t=#=`3`ACMs-DUNM81espwFh&BKV6 z+{pjdtyAKki1idc#nZUOJ@7eKG4#)o&#|`N2tTUqcAeal?sBDd6UU56Y&$qy4Tr%p zq1c6y@G-aj2W*PU#F|WIEyLnU1R&QUr1-Lf+$lFfYPoYE)8IR$-UJdoNlxTBJD>S^ z#`?;q&R>pDthhDy_=$DT=RAUx_lm3n=pSLKEMbe+C!v^fRnEoZ1dA8k7_GlH`EBom2G4AB%4vi!g8@RzvMvEy zhjGS|g-=p;nB>W63smgU%@uwM-03hUH|}d)0yxH+2#N^?D(LJX9u!9l%R!3l$H?-4 zqbTj9vO;|7Ud}HLj+xa~jipc_gzs01?@B`5;{ND;b$W=uMByB`4T+E=84U!-e6Hf< zx;i_Jg{P8{Qc&=UlkRbACr-+qk)8}ZyBS>ru3c`2(x;us(hf5nIsy+_TvufTg2i%zBa)d4mCvh#1-}J za3z`P3R%sr-Y~0BH6LQ>u-+s_Ck>9@${mQP6$SOAW2;ZKbS?}m**92SGVeDr64vF& zS>~BbIN70dWi2aO{x(0Jz8;b=YAcMKvxXs@&rT!B#GPJ^tE{DbKbxny_T=#UR299M zKd~aRC|QXOQTA!Ld?!5_`Oy(JcT0m6Na+>zfX-k5MX@l z7^R6?LmQ(-FLsF6T01A#r$0+QSi>yZW~ks-*Vh?l3bEIl(u8^Sos?_DbnA4L3glsf zbU*3a%dmg9dyrXBI~ffShwyVh?c1)w?-=N8YgI!-(pp8M7Z@LuC|Jc;55XT4t@ z)?pco!^Y5%+k;ZTq^bGWMu|1z!Q0XyE<9tOBun)&CUn<~(jm&Qs|J4UH$JgRI+BCF z)t9j#3ow>M;P90x-IXh8qqav0>X`@;yWbgMV=kIN#pFk{2#hx7>MA|M2XUl9#Ky-3 z;s7xYiH#>f?43q4=zkYWLUT&JX7Xs#7I6u0u|AMR6anuJ!oU6lDN%!_(w@n{+hqj;wgbS5C$biZU^99=lQJCUMP3i)PV&@x2fh9o`ZX!rkG%BDv z!4ZsY0&p&1I0Rdwr8e&G!(hPpj53Ncmt~dC`ZaxkTFV;Ap}agetA@8B!Vu%ST{~T5 z>TzVT7069eo=B;vh*G{RvCRZsMY7x`dr?tgQUHTaGr@|e3p?$R z3^p>f?!Yz!Fpgd}-F1d}evLSl01_Swjc%!C6Rz2Q-;MXKtm)i2yPsby8@b&&WmL_D z&x-H*OoG!Q>eInq5Gm)yC_u2X zr6&()FLN3T#TqbaFKOYN0S*xNvfau_t9!4!l2tc9bVpK|iFvA#9Em{1ZpR#u18;4N zS)>Dv4o2=b>bOJYnA;mmPDuax2tN#ln_J9FI zmPzL(BZkaox+BsxlGSr`P;?d`Hm(7OydQ(=xBCa$pbqtu?P&Ta2?-|>A(A3UgJ&`z zO0=kUEzPjso9p~X5U_hqJdSvc@c*1hhkMwR3?OOXZk%yl(Yt5ilQ@Y2hTbDibeFiU4gC5SLr+LLA5!_E`$8*`V6TW4dAJy%wRKQ=8pA{W)A&dO)}9LM=O*Ed%4 z@Na5-|3>pN)leOIF z4^zwSKRi4nu#^Za(_j+*@A5M1h|&X%rI~%TgVXz61vJ0}xvK0`8n4zdiM<@s>H_(D zmXY7XYKu5!I}3=cVs&7jo`%y5QMs*}_J-Or$ZaQ7(-YDhrYO|Cehe}5rr$Xws5{sQ zI(^ZRkD&lC$Ek&)FJk(jJ44Dkx?n~F9cvnO?$Ee$MT?A^S7)XnA0cQ7$_mYXx3$C+ zDbL6KN<}N$lb?V(TsFp$wd-uiTqb;dB#(MY1yIA;3*YFgpJ4~=K7lNy95|a4>LYtH zT|Uy4{_dvgS~=?}Tlk$O9`0JqB2>R?sABX|T>W?MxT*In02A4g90QXVgt=uLk4U)> znm=bZg@XBa{+Jm{`8KufKt(oZ(Z(l>YztG!h!WKY1@EL{ZmC)c1=@~)-qxL{( z=2S~ktf{P~(VBnJ$WtOQU<|H)`pp|)Npq+LMl^%UzMqquRYtL6=lKX<$Iz4p*vx>j z5%hXHE|7aCOYczdGoj3d0U0urUit zRSz6OO1oZXprx=yVlcyyqvZ%pz*2xxJMB<1 zDfYUnWVQwBarHc6Sh#LXa@ZAa`Plz{zHdK#9`z*N`RaP#_1R2s?1_E`BH@8BcqP5` z@K=(qtc#(`3*UE%^n?QHPU6K&1m_2vp$UKDe|t%9S>JA1W`38P@DSEH*|cmi4l%Ej zqpH}VQy6ZRP?Ib%=pyN_bZnCpL&)xLM~w1-K>*z#lMQjvx6I~E$P^vvgM&`AS9|dc z-PfUYtl`(@S=kDEOB(vl5Ba ziUt|cfy<6|O-|>Pi35hM%R-ABqtD1DO9_m9KRUc7IfnySjr-zy86wpL5RVTswBR zB2-a8XqNCkn*|NoLjdZA*iwk*;O z(-NIEXdK;Ynx2qWvF4uGBIU7t>0*f@rZOMgxW&0TN~_I@pB;vcM78#mc0B z7#=7iO9|L|!m?U>!1EmR^d@9lbUi)R{oh}B%rQ`#MgPx_iI3#Z5|iu`mSJ2)AupoX zev5wCOz!6DHq^k8sYRt_9AX&Fp%|D{8R152E1)-cyI49*c@msOz~|IfQSm>~c01Yi zL9$d~wmoc9RX&48h=`mYP!$WHyyg}?l z(9mYhHMnrh^Lau|%POftwAu+Bnm~ki2%#y5rhQo;T%QJhKbNMiE~Nk5)A8DWEf>C4M{y-Bme^q$d4rdxn4?O8G~ zL_NCiWfp+zEH~7 zw;+PhfWVQ>zh_6%O)Sop;_>;61VrX)oOUgqq$q-_aa%HwWsVr8wW;3Z6I*?vD2Ahi z?WtMxH$V7!^~=OE`4=?u8J;l=G`x{jKr)czputKMV2=dgk`|7M2p~G-!pyCP+qLtKG zuy6>IWzDe!J1f}LE;T*FH8;&V;e3W}i7DV&agv3iZ|`m^`(A1wPKeWBF-_c~uow&* zQIOG-J~S(^BPMa^79rz*y5Z{^&4_HW_>z#D@neyX#2og_YY?Lxwkq4PG;2FO%A8M? zv@0~ooa3?T?&n7r5ud54dF+Q2ub8y2rX;8H??67=&LiJN5Mf5#PeHO?y77KUm_iR8 zQ7bO~K6K3xnmJ*8gcdG3 zRmlja&`da-Od88e(I3c747F4op)Y_mDphi5zRPgZn(Gc?ERd8OM&TtbAId;dGuX?v zPlQ~SFdyKGQL3Undy0Chq9P@`9dCjE`1!{2 z@-WRxYR`~pb~fb;I2ZRp#oEtvvUp-{kcULNIlt(+}w)HD&->n8*&I=~20ZLO$Jrkny=021jgo z5Qx-Gpp4ulu_($6?VIvWeBG>TJN@*{br(R%vpLMxNBx;&NJ~Z-7y_t(sBizETf}tw zWayAJqw4fXk}G;{vM26$w&CXeJbjP@h7=w$z8bi8!B&~VT}83ieBYOAcGm^jBI`|V z_(k24=Opyf1GqkbbLbAjzyAFx&Bj{`4+}NAK+O6SrMFU`uEk#m5YxWCch{{^3*~?# z2$WfQ=Gc`JH8nxfdyZedFJ4G`sb8CDw}Of%CNsAJp{9QsDYboNAH230_@dQ-n3k3a z6gk-CXkk4kj`pr5b$Zv?;^xbego~vpL-8;u@?wmsl0|8~qRi2z3~BR? zfBxNBat%UL^L9p7nHs8P8>%q2#)4@`Rqm6yCsWN4f1IvmOrZktsxCWRyOvYXEP5ULYLmT8$^U6CMS0@yCzFsU~tj)TfAJvTWJL$|o@SV}?bMpj~=2}D7n3`7|G zubUhq{%VJx_Z1UP@F|s!r^ZL7!i**iVxqY(yGI)vHfyZy9Y-Yp&Xz-3C8iu1NN292 z88N_(5x;Ya!i0w?$Vp>lg@x}PqUtIkD)D^HJ$mj#Qku&?+m^BQI&Pj^UyUa;P4sv2g3UWQBU}=xo#Ys;3$@p`+Co4I9hfluJ*p2BPdm@w_nXs;L zJ7ifP(XQyTj+25(s6h0IKwX`p5&)B2! zUh1UNbAdDrYUC3}rm&OC?B^aXUzCz@X3s6eP~G2JG!r`3<mOECfj)(%u~89bN{w zTY@;%5@MO`{Wo>{FhRtK3(9SyX2*z6$B0!jhmRCC;{hNo0YpqJLIM_i5LMe5exJGI zZ<4D`XJTb=YxB{rmfKCHR~R!vYRw4`t{X+^0v<4cg*Rw*?oM5pq>nGScfs`JGdtw$eWduO!3z6 z3A*0(YBF)fTkgx#Vs@9a7S@1J_M9Yo!}S_{?ne9Q!uW-lA+XJ1990~4R?^*j;||j` zWEjiw3kVo;s*-0;Dqbjv(1uw-S844s1aRV|AztmnU8~=N-^)fCsi92{Bb~v>@A;WW zLUKF=AVG%VoglG0!hRkBIk Date: Wed, 11 Oct 2023 10:57:11 +0400 Subject: [PATCH 186/242] Updated on 2026-08-14 --- app/build.gradle.kts | 1 - .../impl/presentation/ui/BriefNetworksList.kt | 15 ++- .../tokens/impl/presentation/ui/TokenItem.kt | 39 ++---- .../drawable/bg_half_transparent_overlay.xml | 6 +- app/src/main/res/values-night/colors.xml | 4 + app/src/main/res/values/colors.xml | 6 +- core/ui/build.gradle.kts | 1 + .../utils/ImageBackgroundContrastChecker.kt | 44 +++++++ .../feature/swap/ui/SwapSelectTokenScreen.kt | 57 ++++++-- .../tangem/feature/swap/ui/TransactionCard.kt | 123 ++++++++++++------ .../tokendetails/ui/components/TokenIcon.kt | 33 ++++- .../component/token/icon/ContentIcon.kt | 33 ++++- 12 files changed, 269 insertions(+), 93 deletions(-) create mode 100644 core/ui/src/main/java/com/tangem/core/ui/utils/ImageBackgroundContrastChecker.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 23d6214b10..e3e96fb28d 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -95,7 +95,6 @@ dependencies { implementation(deps.lifecycle.common.java8) implementation(deps.lifecycle.viewModel.ktx) implementation(deps.lifecycle.compose) - implementation(deps.androidx.palette) /** Compose libraries */ implementation(deps.compose.constraintLayout) diff --git a/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/ui/BriefNetworksList.kt b/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/ui/BriefNetworksList.kt index 4d6738f03d..ea2c77ed2a 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/ui/BriefNetworksList.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/ui/BriefNetworksList.kt @@ -3,19 +3,20 @@ package com.tangem.tap.features.tokens.impl.presentation.ui import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut -import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.Icon import androidx.compose.material.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.key import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.painterResource import com.tangem.core.ui.res.TangemTheme import com.tangem.tap.features.tokens.impl.presentation.states.NetworkItemState @@ -69,11 +70,21 @@ internal fun BriefNetworksList( */ @Composable internal fun BriefNetworkItem(model: NetworkItemState, modifier: Modifier = Modifier) { + val isAdded = model is NetworkItemState.ManageContent && model.isAdded.value Box(modifier = modifier.size(size = TangemTheme.dimens.size20)) { - Image( + if (!isAdded) { + Box( + modifier = Modifier + .size(TangemTheme.dimens.size20) + .clip(CircleShape) + .background(TangemTheme.colors.control.unchecked), + ) + } + Icon( painter = painterResource(id = model.iconResId.value), contentDescription = null, modifier = Modifier.size(size = TangemTheme.dimens.size20), + tint = if (isAdded) Color.Unspecified else TangemTheme.colors.text.tertiary, ) if (model.isMainNetwork) { diff --git a/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/ui/TokenItem.kt b/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/ui/TokenItem.kt index 42577a5372..862b0c874f 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/ui/TokenItem.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/ui/TokenItem.kt @@ -1,6 +1,5 @@ package com.tangem.tap.features.tokens.impl.presentation.ui -import android.graphics.drawable.Drawable import androidx.compose.animation.* import androidx.compose.foundation.background import androidx.compose.foundation.isSystemInDarkTheme @@ -20,16 +19,15 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.constraintlayout.compose.ConstraintLayout import androidx.constraintlayout.compose.Dimension -import androidx.core.graphics.ColorUtils -import androidx.core.graphics.drawable.toBitmap -import androidx.palette.graphics.Palette import coil.compose.SubcomposeAsyncImage import coil.request.ImageRequest import com.tangem.core.ui.components.CurrencyPlaceholderIcon import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.utils.ImageBackgroundContrastChecker import com.tangem.tap.common.compose.extensions.toPx import com.tangem.tap.features.tokens.impl.presentation.states.TokenItemState import com.tangem.wallet.R +import kotlinx.coroutines.launch /** [REDACTED_AUTHOR] @@ -130,6 +128,7 @@ private fun Icon(name: String, iconUrl: String, onContrastCalculate: (Color) -> val iconModifier = modifier.size(size = TangemTheme.dimens.size46) val screenBackgroundColor = TangemTheme.colors.background.primary.toArgb() val isDarkTheme = isSystemInDarkTheme() + val coroutineScope = rememberCoroutineScope() SubcomposeAsyncImage( modifier = iconModifier, @@ -141,11 +140,13 @@ private fun Icon(name: String, iconUrl: String, onContrastCalculate: (Color) -> .listener( onSuccess = { _, result -> if (isDarkTheme) { - setContrastBackgroundIfNeeded( - drawable = result.drawable, - screenBackgroundColor = screenBackgroundColor, - onContrastCalculate = onContrastCalculate, - ) + coroutineScope.launch { + val color = ImageBackgroundContrastChecker( + drawable = result.drawable, + backgroundColor = screenBackgroundColor, + ).getContrastColorIfNeeded(isDarkTheme) + onContrastCalculate(color) + } } }, ) @@ -156,26 +157,6 @@ private fun Icon(name: String, iconUrl: String, onContrastCalculate: (Color) -> ) } -private fun setContrastBackgroundIfNeeded( - drawable: Drawable, - screenBackgroundColor: Int, - onContrastCalculate: (Color) -> Unit, -) { - Palette.Builder(drawable.toBitmap()).generate { palette -> - val color = palette?.getDominantColor(screenBackgroundColor) ?: screenBackgroundColor - val contrast = ColorUtils.calculateContrast(color, screenBackgroundColor) - val colorToSet = if (contrast > LOW_CONTRAST_RATIO) { - Color.Transparent - } else { - Color.White - } - onContrastCalculate(colorToSet) - } -} - -// https://www.w3.org/TR/2008/REC-WCAG20-20081211/#contrast-ratiodef -private const val LOW_CONTRAST_RATIO = 1.5f - @Composable private fun Title(title: String, modifier: Modifier = Modifier) { Text( diff --git a/app/src/main/res/drawable/bg_half_transparent_overlay.xml b/app/src/main/res/drawable/bg_half_transparent_overlay.xml index 4b5937d793..1e819d64aa 100644 --- a/app/src/main/res/drawable/bg_half_transparent_overlay.xml +++ b/app/src/main/res/drawable/bg_half_transparent_overlay.xml @@ -2,8 +2,8 @@ \ No newline at end of file diff --git a/app/src/main/res/values-night/colors.xml b/app/src/main/res/values-night/colors.xml index 1b1466a874..29535c674f 100644 --- a/app/src/main/res/values-night/colors.xml +++ b/app/src/main/res/values-night/colors.xml @@ -51,4 +51,8 @@ #333333 + + #000000 + #0F0000 + #19000000 \ No newline at end of file diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index 5d49d88d30..7d26f9c9d0 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -80,6 +80,10 @@ #000000 #00000000 - + d #1B1D1C + + #FFFFFF + #F0FFFF + #19FFFFFF \ No newline at end of file diff --git a/core/ui/build.gradle.kts b/core/ui/build.gradle.kts index 4f39028ae4..3a09e91308 100644 --- a/core/ui/build.gradle.kts +++ b/core/ui/build.gradle.kts @@ -18,6 +18,7 @@ dependencies { /** AndroidX libraries */ implementation(deps.androidx.fragment.ktx) implementation(deps.androidx.paging.runtime) + implementation(deps.androidx.palette) /** Compose */ implementation(deps.compose.constraintLayout) diff --git a/core/ui/src/main/java/com/tangem/core/ui/utils/ImageBackgroundContrastChecker.kt b/core/ui/src/main/java/com/tangem/core/ui/utils/ImageBackgroundContrastChecker.kt new file mode 100644 index 0000000000..5fab6c4856 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/utils/ImageBackgroundContrastChecker.kt @@ -0,0 +1,44 @@ +package com.tangem.core.ui.utils + +import android.graphics.Bitmap +import android.graphics.drawable.Drawable +import androidx.compose.ui.graphics.Color +import androidx.core.graphics.ColorUtils +import androidx.core.graphics.drawable.toBitmap +import androidx.palette.graphics.Palette +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext + +class ImageBackgroundContrastChecker( + private val image: Bitmap, + private val backgroundColor: Int, +) { + constructor(drawable: Drawable, backgroundColor: Int) : this( + image = drawable.toBitmap(), + backgroundColor = backgroundColor, + ) + + suspend fun getContrastColorIfNeeded(isDarkTheme: Boolean): Color { + return if (isLowContrast()) { + if (isDarkTheme) Color.White else Color.Black + } else { + Color.Transparent + } + } + + suspend fun isLowContrast(): Boolean { + val palette = generatePaletteAsync(bitmap = image) + val color = palette.getDominantColor(backgroundColor) + val contrast = ColorUtils.calculateContrast(color, backgroundColor) + return contrast <= LOW_CONTRAST_RATIO + } + + private suspend fun generatePaletteAsync(bitmap: Bitmap): Palette = withContext(Dispatchers.Default) { + return@withContext Palette.Builder(bitmap).generate() + } + + private companion object { + // https://www.w3.org/TR/2008/REC-WCAG20-20081211/#contrast-ratiodef + private const val LOW_CONTRAST_RATIO = 1.5f + } +} \ No newline at end of file diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapSelectTokenScreen.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapSelectTokenScreen.kt index 23647ff1d8..ea3a63e8dd 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapSelectTokenScreen.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapSelectTokenScreen.kt @@ -5,17 +5,20 @@ import androidx.annotation.StringRes import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background import androidx.compose.foundation.clickable +import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.material.Divider import androidx.compose.material.Scaffold import androidx.compose.material.Text -import androidx.compose.runtime.Composable +import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.graphics.ColorMatrix +import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource @@ -29,11 +32,13 @@ import com.tangem.core.ui.components.SpacerW2 import com.tangem.core.ui.components.appbar.ExpandableSearchView import com.tangem.core.ui.extensions.getActiveIconRes import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.utils.ImageBackgroundContrastChecker import com.tangem.feature.swap.models.Network import com.tangem.feature.swap.models.SwapSelectTokenStateHolder import com.tangem.feature.swap.models.TokenBalanceData import com.tangem.feature.swap.models.TokenToSelect import com.tangem.feature.swap.presentation.R +import kotlinx.coroutines.launch @Composable fun SwapSelectTokenScreen( @@ -66,9 +71,10 @@ fun SwapSelectTokenScreen( @OptIn(ExperimentalFoundationApi::class) @Composable private fun ListOfTokens(state: SwapSelectTokenStateHolder, modifier: Modifier = Modifier) { + val screenBackgroundColor = TangemTheme.colors.background.secondary LazyColumn( modifier = modifier - .background(color = TangemTheme.colors.background.secondary) + .background(color = screenBackgroundColor) .fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally, ) { @@ -77,7 +83,15 @@ private fun ListOfTokens(state: SwapSelectTokenStateHolder, modifier: Modifier = } itemsIndexed(items = state.addedTokens) { index, item -> - TokenItem(token = item, network = state.network, onTokenClick = { state.onTokenSelected(item.id) }) + TokenItem( + token = item, + network = state.network, + screenBackgroundColor = screenBackgroundColor, + onTokenClick = { + state + .onTokenSelected(item.id) + }, + ) if (index != state.addedTokens.lastIndex) { Divider( @@ -92,7 +106,12 @@ private fun ListOfTokens(state: SwapSelectTokenStateHolder, modifier: Modifier = } itemsIndexed(items = state.otherTokens) { index, item -> - TokenItem(token = item, network = state.network, onTokenClick = { state.onTokenSelected(item.id) }) + TokenItem( + token = item, + network = state.network, + screenBackgroundColor = screenBackgroundColor, + onTokenClick = { state.onTokenSelected(item.id) }, + ) if (index != state.otherTokens.lastIndex) { Divider( color = TangemTheme.colors.stroke.primary, @@ -121,7 +140,7 @@ private fun Header(@StringRes title: Int) { @Suppress("LongMethod") @Composable -private fun TokenItem(token: TokenToSelect, network: Network, onTokenClick: () -> Unit) { +private fun TokenItem(token: TokenToSelect, network: Network, screenBackgroundColor: Color, onTokenClick: () -> Unit) { Row( modifier = Modifier .fillMaxWidth() @@ -134,6 +153,7 @@ private fun TokenItem(token: TokenToSelect, network: Network, onTokenClick: () - ) { TokenIcon( token = token, + screenBackgroundColor = screenBackgroundColor, iconPlaceholder = if (token.isNative) getActiveIconRes(network.blockchainId) else null, ) @@ -187,13 +207,21 @@ private fun TokenItem(token: TokenToSelect, network: Network, onTokenClick: () - @Suppress("MagicNumber") @Composable -private fun TokenIcon(token: TokenToSelect, @DrawableRes iconPlaceholder: Int?) { +private fun TokenIcon(token: TokenToSelect, screenBackgroundColor: Color, @DrawableRes iconPlaceholder: Int?) { + var iconBackgroundColor by remember { mutableStateOf(Color.Transparent) } + val isDarkTheme = isSystemInDarkTheme() + val coroutineScope = rememberCoroutineScope() + val data = token.iconUrl.ifEmpty { iconPlaceholder } Box( modifier = Modifier - .padding(end = TangemTheme.dimens.spacing12), + .padding(end = TangemTheme.dimens.spacing12) + .background( + color = iconBackgroundColor, + shape = TangemTheme.shapes.roundedCorners8, + ), ) { val iconModifier = Modifier .size(TangemTheme.dimens.size40) @@ -208,7 +236,20 @@ private fun TokenIcon(token: TokenToSelect, @DrawableRes iconPlaceholder: Int?) model = ImageRequest.Builder(LocalContext.current) .data(data) .crossfade(true) - .build(), + .allowHardware(false) + .listener( + onSuccess = { _, result -> + if (isDarkTheme) { + coroutineScope.launch { + val color = ImageBackgroundContrastChecker( + drawable = result.drawable, + backgroundColor = screenBackgroundColor.toArgb(), + ).getContrastColorIfNeeded(isDarkTheme) + iconBackgroundColor = color + } + } + }, + ).build(), contentDescription = token.id, loading = { CircleShimmer(modifier = iconModifier) }, error = { CurrencyPlaceholderIcon(modifier = iconModifier, id = token.id) }, diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/TransactionCard.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/TransactionCard.kt index be8d01441a..bd57a551b1 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/TransactionCard.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/TransactionCard.kt @@ -5,6 +5,7 @@ import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape @@ -13,11 +14,11 @@ import androidx.compose.material.Icon import androidx.compose.material.MaterialTheme import androidx.compose.material.Text import androidx.compose.material.ripple.rememberRipple -import androidx.compose.runtime.Composable -import androidx.compose.runtime.remember +import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource @@ -27,15 +28,16 @@ import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import coil.compose.SubcomposeAsyncImage import coil.request.ImageRequest import com.tangem.core.ui.R import com.tangem.core.ui.components.* import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.utils.ImageBackgroundContrastChecker import com.tangem.feature.swap.models.SwapWarning import com.tangem.feature.swap.models.TransactionCardType +import kotlinx.coroutines.launch @Suppress("LongParameterList") @Composable @@ -265,47 +267,13 @@ fun Token( verticalArrangement = Arrangement.Bottom, horizontalAlignment = Alignment.End, ) { - Box( - modifier = Modifier - .padding(end = TangemTheme.dimens.spacing16) - .size(TangemTheme.dimens.size42), - ) { - val tokenImageModifier = Modifier - .align(Alignment.BottomStart) - .size(TangemTheme.dimens.size36) - - val data = tokenIconUrl.ifEmpty { - iconPlaceholder - } - SubcomposeAsyncImage( - modifier = tokenImageModifier, - model = ImageRequest.Builder(LocalContext.current) - .data(data) - .crossfade(true) - .build(), - loading = { CircleShimmer(modifier = tokenImageModifier) }, - contentDescription = tokenCurrency, - ) - - if (networkIconRes != null) { - Box( - modifier = Modifier - .align(Alignment.TopEnd) - .size(TangemTheme.dimens.size18) - .background(color = Color.White, shape = CircleShape), - contentAlignment = Alignment.Center, - ) { - Image( - modifier = Modifier.padding(all = 0.5.dp), - painter = painterResource(id = networkIconRes), - contentDescription = null, - ) - } - } - } - + TokenIcon( + tokenIconUrl = tokenIconUrl, + tokenCurrency = tokenCurrency, + iconPlaceholder = iconPlaceholder, + networkIconRes = networkIconRes, + ) SpacerH4() - Text( text = tokenCurrency, color = TangemTheme.colors.text.primary1, @@ -317,6 +285,75 @@ fun Token( } } +@Composable +private fun TokenIcon( + tokenIconUrl: String, + tokenCurrency: String, + @DrawableRes iconPlaceholder: Int? = null, + @DrawableRes networkIconRes: Int? = null, +) { + val itemBackgroundColor = TangemTheme.colors.background.primary.toArgb() + var iconBackgroundColor by remember { mutableStateOf(Color.Transparent) } + val isDarkTheme = isSystemInDarkTheme() + val coroutineScope = rememberCoroutineScope() + + Box( + modifier = Modifier + .padding(end = TangemTheme.dimens.spacing16) + .size(TangemTheme.dimens.size42), + ) { + val tokenImageModifier = Modifier + .align(Alignment.BottomStart) + .size(TangemTheme.dimens.size36) + .background( + color = iconBackgroundColor, + shape = TangemTheme.shapes.roundedCorners8, + ) + + val data = tokenIconUrl.ifEmpty { + iconPlaceholder + } + SubcomposeAsyncImage( + modifier = tokenImageModifier, + model = ImageRequest.Builder(LocalContext.current) + .data(data) + .crossfade(true) + .allowHardware(false) + .listener( + onSuccess = { _, result -> + if (isDarkTheme) { + coroutineScope.launch { + val color = ImageBackgroundContrastChecker( + drawable = result.drawable, + backgroundColor = itemBackgroundColor, + ).getContrastColorIfNeeded(isDarkTheme) + iconBackgroundColor = color + } + } + }, + ).build(), + loading = { CircleShimmer(modifier = tokenImageModifier) }, + contentDescription = tokenCurrency, + ) + + if (networkIconRes != null) { + Box( + modifier = Modifier + .align(Alignment.TopEnd) + .size(TangemTheme.dimens.size18) + .background(color = TangemTheme.colors.background.primary, shape = CircleShape), + contentAlignment = Alignment.Center, + ) { + Image( + modifier = Modifier.padding(all = TangemTheme.dimens.spacing0_5), + painter = painterResource(id = networkIconRes), + contentDescription = null, + ) + } + } + } +} + @Composable fun ChangeTokenSelector() { Box( diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenIcon.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenIcon.kt index a210927b5b..2a11731a45 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenIcon.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenIcon.kt @@ -3,21 +3,26 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.ui.components import androidx.annotation.DrawableRes import androidx.compose.foundation.Image import androidx.compose.foundation.background +import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.Box import androidx.compose.foundation.shape.CircleShape import androidx.compose.material3.Icon -import androidx.compose.runtime.Composable +import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import coil.compose.SubcomposeAsyncImage import coil.request.ImageRequest import com.tangem.core.ui.components.CircleShimmer +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.utils.ImageBackgroundContrastChecker import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenInfoBlockState import com.tangem.features.tokendetails.impl.R +import kotlinx.coroutines.launch @Composable internal fun CurrencyIcon( @@ -131,12 +136,34 @@ private inline fun DefaultCurrencyIcon( crossinline errorIcon: @Composable () -> Unit, modifier: Modifier = Modifier, ) { + val itemBackgroundColor = TangemTheme.colors.background.primary.toArgb() + var iconBackgroundColor by remember { mutableStateOf(Color.Transparent) } + val isDarkTheme = isSystemInDarkTheme() + val coroutineScope = rememberCoroutineScope() + SubcomposeAsyncImage( - modifier = modifier, + modifier = modifier + .background( + color = iconBackgroundColor, + shape = TangemTheme.shapes.roundedCorners8, + ), model = ImageRequest.Builder(context = LocalContext.current) .data(iconData) .crossfade(enable = true) - .build(), + .allowHardware(false) + .listener( + onSuccess = { _, result -> + if (isDarkTheme) { + coroutineScope.launch { + val color = ImageBackgroundContrastChecker( + drawable = result.drawable, + backgroundColor = itemBackgroundColor, + ).getContrastColorIfNeeded(isDarkTheme) + iconBackgroundColor = color + } + } + }, + ).build(), loading = { CircleShimmer() }, error = { errorIcon() }, alpha = alpha, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/token/icon/ContentIcon.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/token/icon/ContentIcon.kt index 9f29cbd72b..dd1be11f5a 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/token/icon/ContentIcon.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/token/icon/ContentIcon.kt @@ -3,20 +3,25 @@ package com.tangem.feature.wallet.presentation.common.component.token.icon import androidx.annotation.DrawableRes import androidx.compose.foundation.Image import androidx.compose.foundation.background +import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.Box import androidx.compose.foundation.shape.CircleShape import androidx.compose.material3.Icon -import androidx.compose.runtime.Composable +import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import coil.compose.SubcomposeAsyncImage import coil.request.ImageRequest +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.utils.ImageBackgroundContrastChecker import com.tangem.feature.wallet.impl.R import com.tangem.feature.wallet.presentation.common.state.TokenItemState +import kotlinx.coroutines.launch @Composable internal fun ContentIcon( @@ -133,12 +138,34 @@ private inline fun DefaultCurrencyIcon( crossinline errorIcon: @Composable () -> Unit, modifier: Modifier = Modifier, ) { + var iconBackgroundColor by remember { mutableStateOf(Color.Transparent) } + val itemBackgroundColor = TangemTheme.colors.background.primary.toArgb() + val isDarkTheme = isSystemInDarkTheme() + val coroutineScope = rememberCoroutineScope() + SubcomposeAsyncImage( - modifier = modifier, + modifier = modifier + .background( + color = iconBackgroundColor, + shape = TangemTheme.shapes.roundedCorners8, + ), model = ImageRequest.Builder(context = LocalContext.current) .data(iconData) .crossfade(enable = true) - .build(), + .allowHardware(false) + .listener( + onSuccess = { _, result -> + if (isDarkTheme) { + coroutineScope.launch { + val color = ImageBackgroundContrastChecker( + drawable = result.drawable, + backgroundColor = itemBackgroundColor, + ).getContrastColorIfNeeded(isDarkTheme) + iconBackgroundColor = color + } + } + }, + ).build(), loading = { LoadingIcon() }, error = { errorIcon() }, alpha = alpha, From 43d52e0b60cecd365c0123c425b152ed5789f96e Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 11 Oct 2023 17:35:02 +0300 Subject: [PATCH 187/242] Updated on 2026-08-14 --- core/res/src/main/res/values-ru/strings.xml | 3 +++ core/res/src/main/res/values/strings.xml | 3 +++ .../domain/tokens/GetCurrencyWarningsUseCase.kt | 14 ++++++++++++-- .../state/components/TokenDetailsNotification.kt | 7 ++++--- .../viewmodels/TokenDetailsViewModel.kt | 15 ++++++++------- 5 files changed, 30 insertions(+), 12 deletions(-) diff --git a/core/res/src/main/res/values-ru/strings.xml b/core/res/src/main/res/values-ru/strings.xml index fbfba50e2f..2250396755 100644 --- a/core/res/src/main/res/values-ru/strings.xml +++ b/core/res/src/main/res/values-ru/strings.xml @@ -569,10 +569,13 @@ Понятно! Очень круто! Сеть %1$s использует концепцию экзистенциального депозита. Если баланс вашего счета опустится ниже %2$s, он будет деактивирован, а все оставшиеся средства будут уничтожены. + Для работы с сетью необходим депозит. Эта карта может быть производственным образцом или подделкой Проверка подлинности не удалась Важная информация о безопасности %s На этой карте осталось только %s подписей. Вы должны вывести все свои средства. + В данный момент сеть недоступна. Пожалуйста, попробуйте позже. + Сеть недоступна. Как вам Tangem? Один вопрос Эта карта подписывала транзакции в прошлом diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index f6e5c92913..8b245bc9b9 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -589,10 +589,13 @@ Ok, Got it! Really cool! %1$s network has a concept of Existential Deposit. If your account drops below %2$s it will be deactivated and any remaining funds will be destroyed. + Network requires Existential Deposit This card might be a production sample or counterfeit Authenticity check failed Important security information %s There are only %s signatures available on this card. You must withdraw all of your funds. + The network is currently unreachable. Please try again later. + Network is unreachable How do you like Tangem? One question Rate the app diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCurrencyWarningsUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCurrencyWarningsUseCase.kt index b79ffa3f49..227161d34f 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCurrencyWarningsUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCurrencyWarningsUseCase.kt @@ -1,6 +1,7 @@ package com.tangem.domain.tokens import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.model.Network import com.tangem.domain.tokens.model.warnings.CryptoCurrencyWarning import com.tangem.domain.tokens.operations.CurrenciesStatusesOperations @@ -23,10 +24,11 @@ class GetCurrencyWarningsUseCase( suspend operator fun invoke( userWalletId: UserWalletId, - currency: CryptoCurrency, + currencyStatus: CryptoCurrencyStatus, derivationPath: Network.DerivationPath, isSingleWalletWithTokens: Boolean, ): Flow> { + val currency = currencyStatus.currency return combine( getFeeWarningFlow( userWalletId = userWalletId, @@ -37,7 +39,8 @@ class GetCurrencyWarningsUseCase( ), flowOf(walletManagersFacade.getRentInfo(userWalletId, currency.network)), flowOf(walletManagersFacade.getExistentialDeposit(userWalletId, currency.network)), - ) { maybeFeeWarning, maybeRentWarning, maybeEdWarning -> + flowOf(getNetworkUnavailableWarning(currencyStatus)), + ) { maybeFeeWarning, maybeRentWarning, maybeEdWarning, maybeNetworkUnavailable -> setOfNotNull( maybeRentWarning, maybeEdWarning?.let { @@ -47,6 +50,7 @@ class GetCurrencyWarningsUseCase( ) }, maybeFeeWarning, + maybeNetworkUnavailable, ) }.flowOn(dispatchers.io) } @@ -96,6 +100,12 @@ class GetCurrencyWarningsUseCase( } } + private fun getNetworkUnavailableWarning(currencyStatus: CryptoCurrencyStatus): CryptoCurrencyWarning? { + return (currencyStatus.value as? CryptoCurrencyStatus.Unreachable)?.let { + CryptoCurrencyWarning.SomeNetworksUnreachable + } + } + private fun BigDecimal?.isZero(): Boolean { return this?.signum() == 0 } diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/components/TokenDetailsNotification.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/components/TokenDetailsNotification.kt index 13b31508f4..99d77114ba 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/components/TokenDetailsNotification.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/components/TokenDetailsNotification.kt @@ -4,6 +4,7 @@ import androidx.compose.runtime.Immutable import com.tangem.core.ui.components.notifications.NotificationConfig import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.networkIconResId +import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.wrappedList import com.tangem.domain.tokens.model.warnings.CryptoCurrencyWarning import com.tangem.features.tokendetails.impl.R @@ -33,7 +34,7 @@ sealed class TokenDetailsNotification( private val existentialInfo: CryptoCurrencyWarning.ExistentialDeposit, ) : TokenDetailsNotification( config = NotificationConfig( - title = TextReference.Str("Existential Deposit"), + title = resourceReference(R.string.warning_existential_deposit_title), subtitle = TextReference.Res( id = R.string.warning_existential_deposit_message, formatArgs = wrappedList(existentialInfo.currencyName, existentialInfo.edStringValueWithSymbol), @@ -71,8 +72,8 @@ sealed class TokenDetailsNotification( object NetworksUnreachable : TokenDetailsNotification( config = NotificationConfig( - title = TextReference.Str("Some networks are unreachable"), - subtitle = TextReference.Str("The problem is on the crypto-network side. It will be fixed soon."), + title = resourceReference(R.string.warning_network_unreachable_title), + subtitle = resourceReference(R.string.warning_network_unreachable_message), iconResId = R.drawable.img_attention_20, ), ) diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt index 6c131e438b..e11faf6b7d 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt @@ -21,13 +21,13 @@ import com.tangem.domain.tokens.legacy.TradeCryptoAction import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.models.analytics.TokenReceiveAnalyticsEvent +import com.tangem.domain.txhistory.usecase.GetExplorerTransactionUrlUseCase import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsCountUseCase import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsUseCase import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWalletId import com.tangem.domain.wallets.usecase.GetExploreUrlUseCase -import com.tangem.domain.txhistory.usecase.GetExplorerTransactionUrlUseCase import com.tangem.domain.wallets.usecase.GetUserWalletUseCase import com.tangem.feature.tokendetails.presentation.router.InnerTokenDetailsRouter import com.tangem.feature.tokendetails.presentation.tokendetails.analytics.TokenScreenEvent @@ -81,6 +81,7 @@ internal class TokenDetailsViewModel @Inject constructor( private val marketPriceJobHolder = JobHolder() private val refreshStateJobHolder = JobHolder() + private val warningsJobHolder = JobHolder() private var cryptoCurrencyStatus: CryptoCurrencyStatus? = null private val selectedAppCurrencyFlow: StateFlow = createSelectedAppCurrencyFlow() @@ -102,9 +103,8 @@ internal class TokenDetailsViewModel @Inject constructor( } private fun updateContent() { - updateMarketPrice() + subscribeOnCurrencyStatusUpdates() updateTxHistory(refresh = false, showItemsLoading = true) - updateWarnings() } private fun handleBalanceHiding(owner: LifecycleOwner) { @@ -132,22 +132,23 @@ internal class TokenDetailsViewModel @Inject constructor( .launchIn(viewModelScope) } - private fun updateWarnings() { + private fun updateWarnings(cryptoCurrencyStatus: CryptoCurrencyStatus) { viewModelScope.launch(dispatchers.io) { val wallet = getUserWalletUseCase(userWalletId).getOrElse { return@launch } getCurrencyWarningsUseCase.invoke( userWalletId = userWalletId, - currency = cryptoCurrency, + currencyStatus = cryptoCurrencyStatus, derivationPath = cryptoCurrency.network.derivationPath, isSingleWalletWithTokens = isSingleWalletWithTokens(wallet), ) .distinctUntilChanged() .onEach { uiState = stateFactory.getStateWithNotifications(it) } .launchIn(viewModelScope) + .saveIn(warningsJobHolder) } } - private fun updateMarketPrice() { + private fun subscribeOnCurrencyStatusUpdates() { viewModelScope.launch(dispatchers.io) { val wallet = getUserWalletUseCase(userWalletId).getOrElse { return@launch } getCurrencyStatusUpdatesUseCase( @@ -162,6 +163,7 @@ internal class TokenDetailsViewModel @Inject constructor( either.onRight { status -> cryptoCurrencyStatus = status updateButtons(userWalletId = userWalletId, currencyStatus = status) + updateWarnings(status) } } .flowOn(dispatchers.io) @@ -411,7 +413,6 @@ internal class TokenDetailsViewModel @Inject constructor( showItemsLoading = uiState.txHistoryState !is TxHistoryState.Content, ) }, - async { updateWarnings() }, ).awaitAll() uiState = stateFactory.getRefreshedState() }.saveIn(refreshStateJobHolder) From f5666f3bd83fc5b2bf54b089110bd3cbbc978b8f Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 11 Oct 2023 20:52:19 +0500 Subject: [PATCH 188/242] Updated on 2026-08-14 --- .../com/tangem/tap/common/extensions/WalletManager.kt | 9 --------- gradle/dependencies.toml | 2 +- 2 files changed, 1 insertion(+), 10 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/common/extensions/WalletManager.kt b/app/src/main/java/com/tangem/tap/common/extensions/WalletManager.kt index bf3d0d5c18..096f5c96bb 100644 --- a/app/src/main/java/com/tangem/tap/common/extensions/WalletManager.kt +++ b/app/src/main/java/com/tangem/tap/common/extensions/WalletManager.kt @@ -1,6 +1,5 @@ package com.tangem.tap.common.extensions -import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.BlockchainSdkError import com.tangem.blockchain.common.Wallet import com.tangem.blockchain.common.WalletManager @@ -81,12 +80,4 @@ fun WalletManager?.getAddressData(): WalletDataModel.AddressData? { val addressDataList = wallet.createAddressesData() return if (addressDataList.isEmpty()) null else addressDataList[0] -} - -fun WalletManager.Companion.stub(): T { - val wallet = Wallet(Blockchain.Unknown, setOf(), Wallet.PublicKey(byteArrayOf(), null), setOf()) - return object : WalletManager(wallet) { - override val currentHost: String = "" - override suspend fun update() {} - } as T } \ No newline at end of file diff --git a/gradle/dependencies.toml b/gradle/dependencies.toml index 5f97e0c940..b63f0cc158 100644 --- a/gradle/dependencies.toml +++ b/gradle/dependencies.toml @@ -82,7 +82,7 @@ okHttp-prettyLogging = "3.1.0" # endregion Other libraries # region Tangem -tangemBlockchainSdk = "develop-356" +tangemBlockchainSdk = "develop-358" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds tangemCardSdk = "develop-302" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds From 40869beea94a837c81093ad3f8597bb9805a1807 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 12 Oct 2023 15:53:38 +0800 Subject: [PATCH 189/242] Updated on 2026-08-14 --- .../presentation/tokendetails/ui/TokenDetailsScreen.kt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/TokenDetailsScreen.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/TokenDetailsScreen.kt index 288f8e1464..e2ecb00e2e 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/TokenDetailsScreen.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/TokenDetailsScreen.kt @@ -1,5 +1,6 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.ui +import androidx.activity.compose.BackHandler import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background import androidx.compose.foundation.layout.* @@ -42,6 +43,8 @@ import kotlinx.collections.immutable.PersistentList @OptIn(ExperimentalMaterialApi::class, ExperimentalFoundationApi::class) @Composable internal fun TokenDetailsScreen(state: TokenDetailsState) { + BackHandler(onBack = state.topAppBarConfig.onBackClick) + Scaffold( topBar = { TokenDetailsTopAppBar(config = state.topAppBarConfig) }, containerColor = TangemTheme.colors.background.secondary, From e156c413694ddd5b8736afd5b375ae34a8a791dc Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 12 Oct 2023 12:23:07 +0300 Subject: [PATCH 190/242] Updated on 2026-08-14 --- gradle/dependencies.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle/dependencies.toml b/gradle/dependencies.toml index b63f0cc158..4c1ae2ae45 100644 --- a/gradle/dependencies.toml +++ b/gradle/dependencies.toml @@ -82,9 +82,9 @@ okHttp-prettyLogging = "3.1.0" # endregion Other libraries # region Tangem -tangemBlockchainSdk = "develop-358" +tangemBlockchainSdk = "release-app_5.0-359" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds -tangemCardSdk = "develop-302" +tangemCardSdk = "release-app_5.0-303" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds # endregion Tangem From 2176c243e0cb52b4fbcc6838c6d832b0715a50a8 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 12 Oct 2023 18:09:04 +0800 Subject: [PATCH 191/242] Updated on 2026-08-14 --- .../tap/common/analytics/events/AnalyticsParam.kt | 9 +++++---- .../tangem/tap/common/analytics/events/Onboarding.kt | 7 ++++++- .../paramsInterceptor/CardContextInterceptor.kt | 1 + .../tangem/tap/domain/tasks/product/ScanProductTask.kt | 4 +++- .../tangem/tap/features/onboarding/OnboardingHelper.kt | 2 +- .../wallet/redux/OnboardingWalletMiddleware.kt | 7 ++++++- .../tangem/data/card/DefaultCardSdkConfigRepository.kt | 1 + .../com/tangem/domain/common/extensions/Blockchain.kt | 5 ++--- .../com/tangem/domain/userwallets/UserWalletBuilder.kt | 1 + .../tangem/domain/userwallets/UserWalletIdBuilder.kt | 1 + .../com/tangem/domain/models/scan/ScanResponse.kt | 2 +- .../wallet2/viewmodel/SeedPhraseViewModel.kt | 10 +++++----- 12 files changed, 33 insertions(+), 17 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/common/analytics/events/AnalyticsParam.kt b/app/src/main/java/com/tangem/tap/common/analytics/events/AnalyticsParam.kt index d1dd0f06bb..e7e51bdd4c 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/events/AnalyticsParam.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/events/AnalyticsParam.kt @@ -133,9 +133,9 @@ sealed class AnalyticsParam { } sealed class WalletCreationType(val value: String) { - object PrivateKey : WalletCreationType("Private key") - object NewSeed : WalletCreationType("New seed") - object SeedImport : WalletCreationType("Seed import") + object PrivateKey : WalletCreationType(value = "Private Key") + object NewSeed : WalletCreationType(value = "New Seed") + object SeedImport : WalletCreationType(value = "Seed Import") } companion object Key { @@ -152,7 +152,8 @@ sealed class AnalyticsParam { const val ERROR_DESCRIPTION = "Error Description" const val ERROR_CODE = "Error Code" const val ERROR_KEY = "Error Key" - const val CREATION_TYPE = "Creation type" + const val CREATION_TYPE = "Creation Type" + const val SEED_PHRASE_LENGTH = "Seed Phrase Length" const val DAPP_NAME = "DApp Name" const val DAPP_URL = "DApp Url" const val METHOD_NAME = "Method Name" diff --git a/app/src/main/java/com/tangem/tap/common/analytics/events/Onboarding.kt b/app/src/main/java/com/tangem/tap/common/analytics/events/Onboarding.kt index a9432ad404..9088608413 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/events/Onboarding.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/events/Onboarding.kt @@ -23,9 +23,14 @@ sealed class Onboarding( class ButtonCreateWallet : CreateWallet("Button - Create Wallet") class WalletCreatedSuccessfully( creationType: AnalyticsParam.WalletCreationType = AnalyticsParam.WalletCreationType.PrivateKey, + seedPhraseLength: Int? = null, ) : CreateWallet( event = "Wallet Created Successfully", - params = mapOf(AnalyticsParam.CREATION_TYPE to creationType.value), + params = buildMap { + put(AnalyticsParam.CREATION_TYPE, creationType.value) + + if (seedPhraseLength != null) put(AnalyticsParam.SEED_PHRASE_LENGTH, seedPhraseLength.toString()) + }, ) } diff --git a/app/src/main/java/com/tangem/tap/common/analytics/paramsInterceptor/CardContextInterceptor.kt b/app/src/main/java/com/tangem/tap/common/analytics/paramsInterceptor/CardContextInterceptor.kt index 0db51098a3..6db15ac882 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/paramsInterceptor/CardContextInterceptor.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/paramsInterceptor/CardContextInterceptor.kt @@ -45,6 +45,7 @@ class CardContextInterceptor( ProductType.Note -> "Note" ProductType.Twins -> "Twin" ProductType.Wallet -> "Wallet" + ProductType.Wallet2 -> "Wallet 2.0" ProductType.Start2Coin -> "Start2Coin" else -> if (DemoHelper.isDemoCard(scanResponse)) { if (DemoHelper.isTestDemoCard(scanResponse)) { 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 30b5814a11..e6be2ff4c2 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 @@ -209,7 +209,9 @@ private class ScanWalletProcessor( session: CardSession, callback: (result: CompletionResult) -> Unit, ) { - val productType = ProductType.Wallet + val isWallet2 = card.settings.isKeysImportAllowed || card.firmwareVersion >= FirmwareVersion.KeysImportAvailable + + val productType = if (isWallet2) ProductType.Wallet2 else ProductType.Wallet val config = CardConfig.createConfig(card) scope.launch { val scanResponse = ScanResponse( diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingHelper.kt b/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingHelper.kt index 29c6d0fe98..6369010887 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingHelper.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingHelper.kt @@ -53,7 +53,7 @@ object OnboardingHelper { fun whereToNavigate(scanResponse: ScanResponse): AppScreen { return when (scanResponse.productType) { ProductType.Note -> AppScreen.OnboardingNote - ProductType.Wallet -> if (scanResponse.card.settings.isBackupAllowed) { + ProductType.Wallet, ProductType.Wallet2 -> if (scanResponse.card.settings.isBackupAllowed) { AppScreen.OnboardingWallet } else { AppScreen.OnboardingOther diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletMiddleware.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletMiddleware.kt index b46578b48b..b825a98b28 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletMiddleware.kt @@ -237,7 +237,12 @@ private fun handleWallet2Action(action: OnboardingWallet2Action) { SeedPhraseSource.IMPORTED -> AnalyticsParam.WalletCreationType.SeedImport SeedPhraseSource.GENERATED -> AnalyticsParam.WalletCreationType.NewSeed } - Analytics.send(Onboarding.CreateWallet.WalletCreatedSuccessfully(creationType)) + Analytics.send( + event = Onboarding.CreateWallet.WalletCreatedSuccessfully( + creationType = creationType, + seedPhraseLength = action.mnemonicComponents.size, + ), + ) val response = CreateWalletResponse( card = result.data.card, derivedKeys = result.data.derivedKeys, diff --git a/data/card/src/main/java/com/tangem/data/card/DefaultCardSdkConfigRepository.kt b/data/card/src/main/java/com/tangem/data/card/DefaultCardSdkConfigRepository.kt index 321a48b0bf..11dc95e2b1 100644 --- a/data/card/src/main/java/com/tangem/data/card/DefaultCardSdkConfigRepository.kt +++ b/data/card/src/main/java/com/tangem/data/card/DefaultCardSdkConfigRepository.kt @@ -50,6 +50,7 @@ internal class DefaultCardSdkConfigRepository( ProductType.Note, ProductType.Wallet, ProductType.Start2Coin, + ProductType.Wallet2, -> CardIdDisplayFormat.Full } } diff --git a/domain/legacy/src/main/java/com/tangem/domain/common/extensions/Blockchain.kt b/domain/legacy/src/main/java/com/tangem/domain/common/extensions/Blockchain.kt index 925137aab5..8c4877ad37 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/common/extensions/Blockchain.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/common/extensions/Blockchain.kt @@ -179,7 +179,6 @@ fun Blockchain.toCoinId(): String { Blockchain.Dash -> "dash" Blockchain.Kaspa -> "kaspa" Blockchain.TON, Blockchain.TONTestnet -> "the-open-network" - Blockchain.Unknown -> "unknown" Blockchain.Kava, Blockchain.KavaTestnet -> "kava" Blockchain.Ravencoin, Blockchain.RavencoinTestnet -> "ravencoin" Blockchain.Cosmos, Blockchain.CosmosTestnet -> "cosmos" @@ -189,10 +188,10 @@ fun Blockchain.toCoinId(): String { Blockchain.Telos, Blockchain.TelosTestnet -> "telos" Blockchain.AlephZero, Blockchain.AlephZeroTestnet -> "aleph-zero" Blockchain.OctaSpace, Blockchain.OctaSpaceTestnet -> "octaspace" - Blockchain.Chia -> "chia" - Blockchain.ChiaTestnet -> "chia/test" + Blockchain.Chia, Blockchain.ChiaTestnet -> "chia" Blockchain.Near -> "near" Blockchain.NearTestnet -> "near/test" + Blockchain.Unknown -> "unknown" } } diff --git a/domain/legacy/src/main/java/com/tangem/domain/userwallets/UserWalletBuilder.kt b/domain/legacy/src/main/java/com/tangem/domain/userwallets/UserWalletBuilder.kt index 4916db578c..29890a9abd 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/userwallets/UserWalletBuilder.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/userwallets/UserWalletBuilder.kt @@ -25,6 +25,7 @@ class UserWalletBuilder( cardTypesResolver.isStart2Coin() -> "Start2Coin" else -> "Wallet" } + ProductType.Wallet2 -> "Wallet" } /** diff --git a/domain/legacy/src/main/java/com/tangem/domain/userwallets/UserWalletIdBuilder.kt b/domain/legacy/src/main/java/com/tangem/domain/userwallets/UserWalletIdBuilder.kt index cad0957152..40ac39de3c 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/userwallets/UserWalletIdBuilder.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/userwallets/UserWalletIdBuilder.kt @@ -60,6 +60,7 @@ class UserWalletIdBuilder private constructor( ProductType.Twins -> scanResponse.secondTwinPublicKey?.hexToBytes() ProductType.Note, ProductType.Wallet, + ProductType.Wallet2, ProductType.Start2Coin, -> null }, diff --git a/domain/models/src/main/kotlin/com/tangem/domain/models/scan/ScanResponse.kt b/domain/models/src/main/kotlin/com/tangem/domain/models/scan/ScanResponse.kt index c6f1d9f75c..74a552e520 100644 --- a/domain/models/src/main/kotlin/com/tangem/domain/models/scan/ScanResponse.kt +++ b/domain/models/src/main/kotlin/com/tangem/domain/models/scan/ScanResponse.kt @@ -21,5 +21,5 @@ data class ScanResponse( typealias KeyWalletPublicKey = ByteArrayKey enum class ProductType { - Note, Twins, Wallet, Start2Coin + Note, Twins, Wallet, Start2Coin, Wallet2 } \ No newline at end of file diff --git a/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/viewmodel/SeedPhraseViewModel.kt b/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/viewmodel/SeedPhraseViewModel.kt index 7cdaf16171..719abb7761 100644 --- a/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/viewmodel/SeedPhraseViewModel.kt +++ b/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/viewmodel/SeedPhraseViewModel.kt @@ -156,6 +156,7 @@ class SeedPhraseViewModel @Inject constructor( ), suggestedPhraseClick = ::buttonSuggestedPhraseClick, buttonCreateWalletClick = { + analyticsEventHandler.send(SeedPhraseEvents.ButtonImport) buttonImportWalletClick(importedMnemonicComponents, SeedPhraseSource.IMPORTED) }, ), @@ -165,7 +166,7 @@ class SeedPhraseViewModel @Inject constructor( // region CheckSeedPhrase private fun onTextFieldChanged(field: SeedPhraseField, textFieldValue: TextFieldValue) { - viewModelScope.launchSingle { + launchSingle { updateUi { uiBuilder.checkSeedPhrase.updateTextField(uiState, field, textFieldValue) } val fieldState = field.getState(uiState) @@ -278,7 +279,6 @@ class SeedPhraseViewModel @Inject constructor( } private fun buttonImportWalletClick(mnemonicComponents: List?, seedPhraseSource: SeedPhraseSource) { - analyticsEventHandler.send(SeedPhraseEvents.ButtonImport) mnemonicComponents ?: return viewModelScope.launch(dispatchers.io) { @@ -325,7 +325,7 @@ class SeedPhraseViewModel @Inject constructor( private fun buttonGenerateSeedPhraseClick() { analyticsEventHandler.send(SeedPhraseEvents.ButtonGenerateSeedPhrase) - viewModelScope.launchSingle { + launchSingle { updateUi { uiBuilder.generateMnemonicComponents(uiState) } delay(DELAY_GENERATE_SEED_PHRASE) interactor.generateMnemonic() @@ -380,7 +380,7 @@ class SeedPhraseViewModel @Inject constructor( } private fun buttonSuggestedPhraseClick(suggestionIndex: Int) { - viewModelScope.launchSingle { + launchSingle { val textFieldValue = uiState.importSeedPhraseState.tvSeedPhrase.textFieldValue val word = uiState.importSeedPhraseState.suggestionsList[suggestionIndex] val cursorPosition = textFieldValue.selection.end @@ -420,7 +420,7 @@ class SeedPhraseViewModel @Inject constructor( SeedPhraseField.Eleventh -> uiState.checkSeedPhraseState.tvEleventhPhrase } - private fun CoroutineScope.launchSingle(block: suspend CoroutineScope.() -> Unit): Job { + private fun launchSingle(block: suspend CoroutineScope.() -> Unit): Job { return viewModelScope.launch(dispatchers.single, block = block) } From 2ad2edd6c823885067dcec10fe9bf80d8a0f1e90 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 12 Oct 2023 21:49:34 +0800 Subject: [PATCH 192/242] Updated on 2026-08-14 --- .../tap/di/domain/TokensDomainModule.kt | 10 +++ .../tokens/FetchCardTokenListUseCase.kt | 72 ++++++++++++++++ .../wallet/viewmodels/WalletViewModel.kt | 82 +++++++++---------- 3 files changed, 123 insertions(+), 41 deletions(-) create mode 100644 domain/tokens/src/main/kotlin/com/tangem/domain/tokens/FetchCardTokenListUseCase.kt diff --git a/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt index ba41f1dda4..026bf203bb 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt @@ -122,6 +122,16 @@ internal object TokensDomainModule { return FetchCurrencyStatusUseCase(currenciesRepository, networksRepository, quotesRepository) } + @Provides + @ViewModelScoped + fun provideFetchCardTokenListUseCase( + currenciesRepository: CurrenciesRepository, + quotesRepository: QuotesRepository, + networksRepository: NetworksRepository, + ): FetchCardTokenListUseCase { + return FetchCardTokenListUseCase(currenciesRepository, networksRepository, quotesRepository) + } + @Provides @ViewModelScoped fun provideGetCryptoCurrencyUseCase(currenciesRepository: CurrenciesRepository): GetCryptoCurrencyUseCase { diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/FetchCardTokenListUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/FetchCardTokenListUseCase.kt new file mode 100644 index 0000000000..7448714472 --- /dev/null +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/FetchCardTokenListUseCase.kt @@ -0,0 +1,72 @@ +package com.tangem.domain.tokens + +import arrow.core.Either +import arrow.core.raise.Raise +import arrow.core.raise.catch +import arrow.core.raise.either +import com.tangem.domain.tokens.error.TokenListError +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.tokens.model.Network +import com.tangem.domain.tokens.repository.CurrenciesRepository +import com.tangem.domain.tokens.repository.NetworksRepository +import com.tangem.domain.tokens.repository.QuotesRepository +import com.tangem.domain.wallets.models.UserWalletId +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.coroutineScope + +class FetchCardTokenListUseCase( + private val currenciesRepository: CurrenciesRepository, + private val networksRepository: NetworksRepository, + private val quotesRepository: QuotesRepository, +) { + + suspend operator fun invoke(userWalletId: UserWalletId, refresh: Boolean = false): Either { + return either { + val currencies = fetchCurrencies(userWalletId = userWalletId) + + coroutineScope { + val fetchStatuses = async { + fetchNetworksStatuses( + userWalletId = userWalletId, + networks = currencies.mapTo(destination = hashSetOf(), transform = CryptoCurrency::network), + refresh = refresh, + ) + } + val fetchQuotes = async { + fetchQuotes( + currenciesIds = currencies.mapTo(destination = hashSetOf(), transform = CryptoCurrency::id), + refresh = refresh, + ) + } + + awaitAll(fetchStatuses, fetchQuotes) + } + } + } + + private suspend fun Raise.fetchCurrencies(userWalletId: UserWalletId): List { + return catch( + block = { currenciesRepository.getSingleCurrencyWalletWithCardCurrencies(userWalletId = userWalletId) }, + catch = { raise(TokenListError.DataError(it)) }, + ) + } + + private suspend fun Raise.fetchNetworksStatuses( + userWalletId: UserWalletId, + networks: Set, + refresh: Boolean, + ) { + catch( + block = { networksRepository.getNetworkStatusesSync(userWalletId, networks, refresh) }, + catch = { raise(TokenListError.DataError(it)) }, + ) + } + + private suspend fun fetchQuotes(currenciesIds: Set, refresh: Boolean) { + catch( + block = { quotesRepository.getQuotesSync(currenciesIds, refresh) }, + catch = { /* Ignore error */ }, + ) + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt index f0b0798819..23054b94df 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt @@ -97,6 +97,7 @@ internal class WalletViewModel @Inject constructor( private val getTokenListUseCase: GetTokenListUseCase, private val getCardTokensListUseCase: GetCardTokensListUseCase, private val fetchTokenListUseCase: FetchTokenListUseCase, + private val fetchCardTokenListUseCase: FetchCardTokenListUseCase, private val getPrimaryCurrencyStatusUpdatesUseCase: GetPrimaryCurrencyStatusUpdatesUseCase, private val fetchCurrencyStatusUseCase: FetchCurrencyStatusUseCase, private val getNetworkCoinStatusUseCase: GetNetworkCoinStatusUseCase, @@ -544,6 +545,24 @@ internal class WalletViewModel @Inject constructor( } } + private fun refreshMultiCurrencyContent(walletIndex: Int) { + uiState = stateFactory.getRefreshingState() + + viewModelScope.launch(dispatchers.main) { + val wallet = getWallet(walletIndex) + + val maybeFetchResult = if (isSingleWalletWithTokens(wallet)) { + fetchCardTokenListUseCase(userWalletId = wallet.walletId, refresh = true) + } else { + fetchTokenListUseCase(userWalletId = wallet.walletId, refresh = true) + } + + maybeFetchResult.onLeft { uiState = stateFactory.getStateByTokenListError(it) } + + uiState = stateFactory.getRefreshedState() + }.saveIn(refreshContentJobHolder) + } + override fun onOrganizeTokensClick() { analyticsEventsHandler.send(PortfolioEvent.OrganizeTokens) @@ -702,6 +721,27 @@ internal class WalletViewModel @Inject constructor( refreshSingleCurrencyContent(selectedWalletIndex) } + private fun refreshSingleCurrencyContent(walletIndex: Int) { + uiState = stateFactory.getRefreshingState() + val wallet = getWallet(walletIndex) + + viewModelScope.launch(dispatchers.main) { + val result = fetchCurrencyStatusUseCase(wallet.walletId, refresh = true) + + uiState = stateFactory.getRefreshedState() + uiState = result.fold(stateFactory::getStateByCurrencyStatusError) { uiState } + + singleWalletCryptoCurrencyStatus?.let { + val singleCurrencyState = uiState as WalletSingleCurrencyState + if (singleCurrencyState.txHistoryState !is TxHistoryState.Content) { + // show loading indicator while refreshing in non content state + uiState = stateFactory.getLoadingTxHistoryState(1.right()) + } + updateTxHistory(wallet.walletId, it.currency, refresh = true) + } + }.saveIn(refreshContentJobHolder) + } + override fun onExploreClick() { viewModelScope.launch(dispatchers.io) { val wallet = getWallet( @@ -924,9 +964,7 @@ internal class WalletViewModel @Inject constructor( uiState = stateFactory.getLockedState() } wallet.isMultiCurrency -> getMultiCurrencyContent(wallet, index) - isSingleWalletWithTokens(wallet) -> { - getSingleCurrencyWithTokenContent(index) - } + isSingleWalletWithTokens(wallet) -> getSingleCurrencyWithTokenContent(index) !wallet.isMultiCurrency -> getSingleCurrencyContent(index) } } @@ -1137,44 +1175,6 @@ internal class WalletViewModel @Inject constructor( .saveIn(notificationsJobHolder) } - private fun refreshMultiCurrencyContent(walletIndex: Int) { - uiState = stateFactory.getRefreshingState() - val wallet = getWallet(walletIndex) - - viewModelScope.launch(dispatchers.io) { - if (isSingleWalletWithTokens(wallet)) { - // TODO add refresh for nodl cards ([REDACTED_JIRA]) - delay(timeMillis = 1000) - } else { - val result = fetchTokenListUseCase(wallet.walletId, refresh = true) - uiState = result.fold(stateFactory::getStateByTokenListError) { uiState } - } - - uiState = stateFactory.getRefreshedState() - }.saveIn(refreshContentJobHolder) - } - - private fun refreshSingleCurrencyContent(walletIndex: Int) { - uiState = stateFactory.getRefreshingState() - val wallet = getWallet(walletIndex) - - viewModelScope.launch(dispatchers.io) { - val result = fetchCurrencyStatusUseCase(wallet.walletId, refresh = true) - - uiState = stateFactory.getRefreshedState() - uiState = result.fold(stateFactory::getStateByCurrencyStatusError) { uiState } - - singleWalletCryptoCurrencyStatus?.let { - val singleCurrencyState = uiState as WalletSingleCurrencyState - if (singleCurrencyState.txHistoryState !is TxHistoryState.Content) { - // show loading indicator while refreshing in non content state - uiState = stateFactory.getLoadingTxHistoryState(1.right()) - } - updateTxHistory(wallet.walletId, it.currency, refresh = true) - } - }.saveIn(refreshContentJobHolder) - } - private fun createSelectedAppCurrencyFlow(): StateFlow { return getSelectedAppCurrencyUseCase() .map { maybeAppCurrency -> From dd11f56a38db1cc9dd2a793fe7003a499f1001ca Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 13 Oct 2023 14:26:43 +0500 Subject: [PATCH 193/242] Updated on 2026-08-14 --- core/res/src/main/res/values-ru/strings.xml | 21 ++++--- core/res/src/main/res/values/strings.xml | 18 +++--- .../components/TokenDetailsNotification.kt | 56 ++++++++++--------- .../tokendetails/ui/TokenDetailsScreen.kt | 12 +++- .../state/components/WalletNotification.kt | 10 ++-- 5 files changed, 71 insertions(+), 46 deletions(-) diff --git a/core/res/src/main/res/values-ru/strings.xml b/core/res/src/main/res/values-ru/strings.xml index 2250396755..4917da1390 100644 --- a/core/res/src/main/res/values-ru/strings.xml +++ b/core/res/src/main/res/values-ru/strings.xml @@ -568,21 +568,26 @@ Узнать больше Понятно! Очень круто! - Сеть %1$s использует концепцию экзистенциального депозита. Если баланс вашего счета опустится ниже %2$s, он будет деактивирован, а все оставшиеся средства будут уничтожены. - Для работы с сетью необходим депозит. + Cеть %1$s использует концепцию экзистенциального депозита. Если баланс вашего счета будет ниже %2$s, то он будет деактивирован, а средства на счете уничтожены. + Для работы с сетью необходим депозит Эта карта может быть производственным образцом или подделкой Проверка подлинности не удалась Важная информация о безопасности %s На этой карте осталось только %s подписей. Вы должны вывести все свои средства. В данный момент сеть недоступна. Пожалуйста, попробуйте позже. - Сеть недоступна. - Как вам Tangem? - Один вопрос + Сеть недоступна + Пополните ваш кошелек + Ваш отзыв мотивирует нас сделать кошелек Tangem еще лучше + Нравится Tangem? + Необходима плата за аренду сети + %1$s - это токен в сети %2$s. Для совершения транзакции %3$s, вам необходимо внести депозит в размере %4$s (%5$s), чтобы покрыть комиссию сети. + Недостаточно %1$s для оплаты комиссии сети + Отправка средств станет доступной после завершения транзакции %s Эта карта подписывала транзакции в прошлом - В настоящее время сеть недоступна. Пожалуйста, повторите попытку позже. - Некоторые сети в настоящее время недоступны. Пожалуйста, повторите попытку позже. + Сеть Solana взимает арендную плату в размере %1$s каждые 2 дня. Аккаунты, которые не могут позволить себе арендную плату, удаляются из сети. Пополните свой счет более чем на %2$s, чтобы не платить арендную плату. + Некоторые сети в настоящее время недоступны. Пожалуйста, повторите попытку позже. + Некоторые сети недоступны Это тестовая карта. Не принимайте её в качестве оплаты. Эта карта должна использоваться только в целях тестирования и разработки. - Некоторые сети недоступны Отказаться Вы не закончили резервное копирование. Хотите продолжить? Да, возобновить diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index 8b245bc9b9..b04bede936 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -588,7 +588,7 @@ Love it! Ok, Got it! Really cool! - %1$s network has a concept of Existential Deposit. If your account drops below %2$s it will be deactivated and any remaining funds will be destroyed. + %1$s network requires an Existential Deposit. If your account drops below %2$s, it will be deactivated, and any remaining funds will be destroyed. Network requires Existential Deposit This card might be a production sample or counterfeit Authenticity check failed @@ -596,15 +596,19 @@ There are only %s signatures available on this card. You must withdraw all of your funds. The network is currently unreachable. Please try again later. Network is unreachable - How do you like Tangem? - One question - Rate the app + Top up your wallet + Your review keeps us motivated to make Tangem Wallet even better + Enjoying Tangem? + Network rent fee required + %1$s is a token in the %2$s network. To make a %3$s transaction, you must deposit some %4$s (%5$s) to cover the network fee. + Insufficient %1$s to cover network fee + Sending funds will be available once the %s transaction is complete This card has signed transactions in the past - Network currently is unreachable. Please try again later. - Some networks currently are unreachable. Please try again later. + Solana network charges a rent of %1$s every 2 days. Accounts that can\'t afford the rent are purged from the network. Deposit your account with more than %2$s to use it for free. + Some networks currently are unreachable. Please try again later. + Some networks are unreachable This is a Testnet card. Don\'t accept it as a payment. This card must only be used for testing and development purposes. Note top up - Some networks are unreachable Discard You have an interrupted backup. Do you want to resume? Yes, resume diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/components/TokenDetailsNotification.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/components/TokenDetailsNotification.kt index 99d77114ba..fd43bdcd6e 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/components/TokenDetailsNotification.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/components/TokenDetailsNotification.kt @@ -11,49 +11,52 @@ import com.tangem.features.tokendetails.impl.R // TODO: Finalize notification strings [REDACTED_JIRA] @Immutable -sealed class TokenDetailsNotification( - open val config: NotificationConfig, -) { +sealed class TokenDetailsNotification { + + abstract val config: NotificationConfig + + sealed class Informational : TokenDetailsNotification() + sealed class Warning : TokenDetailsNotification() data class RentInfo( private val rentInfo: CryptoCurrencyWarning.Rent, private val onCloseClick: () -> Unit, - ) : TokenDetailsNotification( - config = NotificationConfig( - title = TextReference.Res(R.string.send_network_fee_title), + ) : Warning() { + override val config = NotificationConfig( + title = TextReference.Res(R.string.warning_rent_fee_title), subtitle = TextReference.Res( - id = R.string.solana_rent_warning, + id = R.string.warning_solana_rent_fee_message, formatArgs = wrappedList(rentInfo.rent, rentInfo.exemptionAmount), ), iconResId = R.drawable.img_attention_20, onCloseClick = onCloseClick, - ), - ) + ) + } data class ExistentialDeposit( private val existentialInfo: CryptoCurrencyWarning.ExistentialDeposit, - ) : TokenDetailsNotification( - config = NotificationConfig( + ) : Informational() { + override val config = NotificationConfig( title = resourceReference(R.string.warning_existential_deposit_title), subtitle = TextReference.Res( id = R.string.warning_existential_deposit_message, formatArgs = wrappedList(existentialInfo.currencyName, existentialInfo.edStringValueWithSymbol), ), - iconResId = R.drawable.img_attention_20, - ), - ) + iconResId = R.drawable.ic_alert_circle_24, + ) + } data class NetworkFee( private val feeInfo: CryptoCurrencyWarning.BalanceNotEnoughForFee, private val onBuyClick: () -> Unit, - ) : TokenDetailsNotification( - config = NotificationConfig( + ) : Warning() { + override val config = NotificationConfig( title = TextReference.Res( - id = R.string.notification_title_not_enough_funds, + id = R.string.warning_send_blocked_funds_for_fee_title, formatArgs = wrappedList(feeInfo.blockchainFullName), ), subtitle = TextReference.Res( - id = R.string.token_details_send_blocked_fee_format, + id = R.string.warning_send_blocked_funds_for_fee_message, formatArgs = wrappedList( feeInfo.currency.name, feeInfo.blockchainFullName, @@ -64,17 +67,20 @@ sealed class TokenDetailsNotification( ), iconResId = feeInfo.currency.networkIconResId, buttonsState = NotificationConfig.ButtonsState.SecondaryButtonConfig( - text = TextReference.Res(R.string.common_buy), + text = resourceReference( + id = R.string.common_buy_currency, + formatArgs = wrappedList(feeInfo.blockchainSymbol), + ), onClick = onBuyClick, ), - ), - ) + ) + } - object NetworksUnreachable : TokenDetailsNotification( - config = NotificationConfig( + object NetworksUnreachable : Warning() { + override val config = NotificationConfig( title = resourceReference(R.string.warning_network_unreachable_title), subtitle = resourceReference(R.string.warning_network_unreachable_message), iconResId = R.drawable.img_attention_20, - ), - ) + ) + } } \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/TokenDetailsScreen.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/TokenDetailsScreen.kt index e2ecb00e2e..67f09871a1 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/TokenDetailsScreen.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/TokenDetailsScreen.kt @@ -32,6 +32,7 @@ import com.tangem.core.ui.components.transactions.txHistoryItems import com.tangem.core.ui.res.TangemTheme import com.tangem.feature.tokendetails.presentation.tokendetails.TokenDetailsPreviewData import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState +import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.TokenDetailsNotification import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.TokenDetailsBalanceBlock import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.TokenDetailsDialogs import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.TokenDetailsTopAppBar @@ -93,7 +94,16 @@ internal fun TokenDetailsScreen(state: TokenDetailsState) { items = state.notifications, key = { it.config::class.java }, contentType = { it.config::class.java }, - itemContent = { Notification(config = it.config, modifier = itemModifier.animateItemPlacement()) }, + itemContent = { + Notification( + modifier = itemModifier.animateItemPlacement(), + config = it.config, + iconTint = when (it) { + is TokenDetailsNotification.Warning -> null + is TokenDetailsNotification.Informational -> TangemTheme.colors.icon.accent + }, + ) + }, ) if (!state.isCustomToken) { item( diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/components/WalletNotification.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/components/WalletNotification.kt index 5a2120d0e3..6c3fd2d2d8 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/components/WalletNotification.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/components/WalletNotification.kt @@ -79,13 +79,13 @@ sealed class WalletNotification(val config: NotificationConfig) { ) object NetworksUnreachable : Warning( - title = resourceReference(id = R.string.wallet_balance_blockchain_unreachable), - subtitle = resourceReference(id = R.string.warning_subtitle_network_unreachable), + title = resourceReference(id = R.string.warning_network_unreachable_title), + subtitle = resourceReference(id = R.string.warning_network_unreachable_message), ) object SomeNetworksUnreachable : Warning( - title = resourceReference(id = R.string.warning_title_some_networks_unreachable), - subtitle = resourceReference(id = R.string.warning_subtitle_some_networks_unreachable), + title = resourceReference(id = R.string.warning_some_networks_unreachable_title), + subtitle = resourceReference(id = R.string.warning_some_networks_unreachable_message), ) data class TopUpNote(val errorMessage: String) : Warning( @@ -137,7 +137,7 @@ sealed class WalletNotification(val config: NotificationConfig) { val onCloseClick: () -> Unit, ) : WalletNotification( config = NotificationConfig( - title = resourceReference(id = R.string.warning_rate_app_title_new), + title = resourceReference(id = R.string.warning_rate_app_title), subtitle = resourceReference(id = R.string.warning_rate_app_message), iconResId = R.drawable.ic_star_24, buttonsState = NotificationConfig.ButtonsState.PairButtonsConfig( From 62b7fbda75aee5b22ac4f27bb6fbaa29e7b4b6f2 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 13 Oct 2023 16:36:15 +0800 Subject: [PATCH 194/242] Updated on 2026-08-14 --- .../wallet/ui/OnboardingWalletFragment.kt | 8 +- .../wallet/viewmodels/WalletViewModel.kt | 80 +++++++++---------- 2 files changed, 43 insertions(+), 45 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/OnboardingWalletFragment.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/OnboardingWalletFragment.kt index 2eb4abdc18..aa714f2c28 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/OnboardingWalletFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/OnboardingWalletFragment.kt @@ -191,7 +191,7 @@ class OnboardingWalletFragment : } } - internal fun loadImageIntoImageView(uri: Uri?, view: ImageView) { + private fun loadImageIntoImageView(uri: Uri?, view: ImageView) { view.load(uri) { placeholder(R.drawable.card_placeholder_black) error(R.drawable.card_placeholder_black) @@ -412,7 +412,7 @@ class OnboardingWalletFragment : animator.showWriteBackupCard(state, cardNumber) } - internal fun showSuccess() = with(binding) { + private fun showSuccess() = with(binding) { toolbar.title = getString(R.string.onboarding_done_header) tvHeader.text = getText(R.string.onboarding_done_header) @@ -427,9 +427,7 @@ class OnboardingWalletFragment : layoutButtonsCommon.btnWalletAlternativeAction.hide() layoutButtonsCommon.btnWalletMainAction.setOnClickListener { showConfetti(false) - lifecycleScope.launch { - store.dispatch(OnboardingWalletAction.FinishOnboarding(lifecycleCoroutineScope = lifecycleScope)) - } + store.dispatch(OnboardingWalletAction.FinishOnboarding(lifecycleCoroutineScope = lifecycleScope)) } animator.showSuccess { diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt index 23054b94df..a182b01ae7 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt @@ -1012,21 +1012,31 @@ internal class WalletViewModel @Inject constructor( } } - private fun initAndSetupWc(tokenListFlow: SharedFlow>, wallet: UserWallet) { - initWalletConnectForWallet(wallet) - tokenListFlow - .filter(::filterLoadedTokenList) - .take(1) - .onEach { - it.onRight { - setupWalletConnectOnWallet(wallet) - } + private fun initAndSetupWc(tokenListFlow: MaybeTokenListFlow, wallet: UserWallet) { + viewModelScope + .launch(dispatchers.main) { + initWalletConnectForWallet(wallet) + + tokenListFlow + .filterLoadedTokenList() + .take(count = 1) + .collect { setupWalletConnectOnWallet(wallet) } } - .flowOn(dispatchers.io) - .launchIn(viewModelScope) .saveIn(updateWcJobHolder) } + private fun initWalletConnectForWallet(userWallet: UserWallet) { + reduxStateHolder.dispatch( + action = WalletConnectActions.New.Initialize(userWallet = userWallet), + ) + } + + private fun setupWalletConnectOnWallet(userWallet: UserWallet) { + reduxStateHolder.dispatch( + action = WalletConnectActions.New.SetupUserChains(userWallet = userWallet), + ) + } + private fun isSingleWalletWithTokens(userWallet: UserWallet): Boolean { return userWallet.scanResponse.walletData?.token != null && !userWallet.isMultiCurrency } @@ -1035,23 +1045,23 @@ internal class WalletViewModel @Inject constructor( return !this.any { it.value is CryptoCurrencyStatus.Loading } } - private fun filterLoadedTokenList(either: Either): Boolean { - return either.fold( - ifRight = { list -> - when (list) { - is TokenList.Ungrouped -> { - list.currencies.isAllCurrenciesLoaded() + private fun MaybeTokenListFlow.filterLoadedTokenList(): MaybeTokenListFlow { + return filter { either -> + either.fold( + ifRight = { list -> + when (list) { + is TokenList.Ungrouped -> { + list.currencies.isAllCurrenciesLoaded() + } + is TokenList.GroupedByNetwork -> { + list.groups.flatMap(NetworkGroup::currencies).isAllCurrenciesLoaded() + } + else -> false } - is TokenList.GroupedByNetwork -> { - list.groups.flatMap { group -> group.currencies }.isAllCurrenciesLoaded() - } - else -> { - false - } - } - }, - ifLeft = { false }, - ) + }, + ifLeft = { false }, + ) + } } private fun List.hasNonZeroWallets(): Boolean { @@ -1187,18 +1197,6 @@ internal class WalletViewModel @Inject constructor( ) } - private fun initWalletConnectForWallet(userWallet: UserWallet) { - reduxStateHolder.dispatch( - WalletConnectActions.New.Initialize(userWallet = userWallet), - ) - } - - private fun setupWalletConnectOnWallet(userWallet: UserWallet) { - reduxStateHolder.dispatch( - WalletConnectActions.New.SetupUserChains(userWallet = userWallet), - ) - } - private fun getWallet(index: Int): UserWallet { return requireNotNull( value = wallets.getOrNull(index), @@ -1214,4 +1212,6 @@ internal class WalletViewModel @Inject constructor( } private fun getCardTypeResolver(index: Int): CardTypesResolver = getWallet(index).scanResponse.cardTypesResolver -} \ No newline at end of file +} + +typealias MaybeTokenListFlow = Flow> \ No newline at end of file From c795074253a26e48b572b6f96a83ee2be3f74cec Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 12 Oct 2023 16:34:59 +0300 Subject: [PATCH 195/242] Updated on 2026-08-14 --- .../chooseaddress/ChooseAddressBottomSheet.kt | 38 +++++-------------- 1 file changed, 9 insertions(+), 29 deletions(-) diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/chooseaddress/ChooseAddressBottomSheet.kt b/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/chooseaddress/ChooseAddressBottomSheet.kt index adcdb176c1..5b41ed7fe6 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/chooseaddress/ChooseAddressBottomSheet.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/chooseaddress/ChooseAddressBottomSheet.kt @@ -1,17 +1,11 @@ package com.tangem.core.ui.components.bottomsheets.chooseaddress -import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.background import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.material3.Text -import androidx.compose.runtime.* -import androidx.compose.ui.Alignment -import com.tangem.core.ui.R +import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.style.TextAlign -import com.tangem.core.ui.components.SecondaryButton +import com.tangem.core.ui.R +import com.tangem.core.ui.components.SimpleSettingsRow import com.tangem.core.ui.components.bottomsheets.TangemBottomSheet import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.res.TangemTheme @@ -30,27 +24,13 @@ fun ChooseAddressBottomSheet(config: TangemBottomSheetConfig) { @Composable private fun ChooseAddressBottomSheetContent(content: ChooseAddressBottomSheetConfig) { Column( - modifier = Modifier - .fillMaxWidth() - .padding( - start = TangemTheme.dimens.spacing24, - top = TangemTheme.dimens.spacing24, - end = TangemTheme.dimens.spacing24, - bottom = TangemTheme.dimens.spacing16, - ), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.spacedBy(space = TangemTheme.dimens.spacing24), + modifier = Modifier.background(TangemTheme.colors.background.primary), ) { - Text( - text = stringResource(id = R.string.token_details_choose_address), - color = TangemTheme.colors.text.secondary, - textAlign = TextAlign.Center, - style = TangemTheme.typography.body2, - ) content.addressModels.forEach { addressModel -> - SecondaryButton( - text = addressModel.type.name, - onClick = { content.onClick(addressModel) }, + SimpleSettingsRow( + title = addressModel.type.name, + icon = R.drawable.ic_arrow_top_right_24, + onItemsClick = { content.onClick(addressModel) }, ) } } From e7da35375ba30145e9e925c1c1c4d13405cb57df Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 12 Oct 2023 16:01:51 +0300 Subject: [PATCH 196/242] Updated on 2026-08-14 --- .../repository/DefaultNetworksRepository.kt | 24 +++---------------- 1 file changed, 3 insertions(+), 21 deletions(-) diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultNetworksRepository.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultNetworksRepository.kt index 673b9fdf4c..bd536ed456 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultNetworksRepository.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultNetworksRepository.kt @@ -53,7 +53,7 @@ internal class DefaultNetworksRepository( override suspend fun fetchNetworkPendingTransactions(userWalletId: UserWalletId, networks: Set) { withContext(dispatchers.io) { - fetchNetworksPendingTransactionsIfCacheExpired(userWalletId, networks, false) + fetchNetworksPendingTransactions(userWalletId, networks) } } @@ -82,16 +82,12 @@ internal class DefaultNetworksRepository( } } - private suspend fun fetchNetworksPendingTransactionsIfCacheExpired( - userWalletId: UserWalletId, - networks: Set, - refresh: Boolean, - ) { + private suspend fun fetchNetworksPendingTransactions(userWalletId: UserWalletId, networks: Set) { coroutineScope { networks .map { network -> async { - fetchNetworkPendingTransactionsIfCacheExpired(userWalletId, network, refresh) + fetchNetworkPendingTransactions(userWalletId, network) } } .awaitAll() @@ -110,20 +106,6 @@ internal class DefaultNetworksRepository( ) } - private suspend fun fetchNetworkPendingTransactionsIfCacheExpired( - userWalletId: UserWalletId, - network: Network, - refresh: Boolean, - ) { - val key = getNetworksStatusesCacheKey(userWalletId, network) - cacheRegistry.invalidate(key) - cacheRegistry.invokeOnExpire( - key = key, - skipCache = refresh, - block = { fetchNetworkPendingTransactions(userWalletId, network) }, - ) - } - private suspend fun fetchNetworkStatus(userWalletId: UserWalletId, network: Network) { val currencies = getCurrencies(userWalletId, network) From 3f3a114dadd8403b7fb147e937311d7ccff3e03c Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 13 Oct 2023 17:33:05 +0800 Subject: [PATCH 197/242] Updated on 2026-08-14 --- .../wallet/ui/components/common/WalletCard.kt | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletCard.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletCard.kt index 4f7ed253be..ec7c87f061 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletCard.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletCard.kt @@ -79,7 +79,7 @@ internal fun WalletCard(state: WalletCardState, isBalanceHidden: Boolean, modifi }, ) - var balanceWidth by remember { mutableStateOf(value = Int.MIN_VALUE) } + var balanceWidth by remember { mutableIntStateOf(value = Int.MIN_VALUE) } Balance( state = state, isBalanceHidden = isBalanceHidden, @@ -153,12 +153,12 @@ private fun CardContainer( Surface( modifier = modifier .defaultMinSize(minHeight = TangemTheme.dimens.size108) + .onSizeChanged { itemSize = it } .then( if (isLockedState) { Modifier } else { Modifier - .onSizeChanged { itemSize = it } .clip(shape = TangemTheme.shapes.roundedCornersXMedium) .indication(interactionSource = interactionSource, indication = LocalIndication.current) .pointerInput(true) { @@ -277,7 +277,6 @@ private fun TitleText(text: String, modifier: Modifier = Modifier) { ) } -@OptIn(ExperimentalAnimationApi::class) @Composable private fun Balance(state: WalletCardState, isBalanceHidden: Boolean, modifier: Modifier = Modifier) { AnimatedContent( @@ -285,7 +284,7 @@ private fun Balance(state: WalletCardState, isBalanceHidden: Boolean, modifier: label = "Update the balance", modifier = modifier, transitionSpec = { - fadeIn(animationSpec = tween(durationMillis = 220, delayMillis = 90)) with + fadeIn(animationSpec = tween(durationMillis = 220, delayMillis = 90)) togetherWith fadeOut(animationSpec = tween(durationMillis = 90)) }, ) { walletCardState -> @@ -329,7 +328,6 @@ private fun Modifier.nonContentBalanceSize(dimens: TangemDimens): Modifier { .size(width = dimens.size102, height = dimens.size24) } -@OptIn(ExperimentalAnimationApi::class) @Composable private fun AdditionalInfo(text: TextReference?, modifier: Modifier = Modifier) { AnimatedContent( @@ -337,7 +335,7 @@ private fun AdditionalInfo(text: TextReference?, modifier: Modifier = Modifier) label = "Update the additional text", modifier = modifier, transitionSpec = { - fadeIn(animationSpec = tween(durationMillis = 220, delayMillis = 90)) with + fadeIn(animationSpec = tween(durationMillis = 220, delayMillis = 90)) togetherWith fadeOut(animationSpec = tween(durationMillis = 90)) }, ) { animatedText -> From 355acaea56ea7468d55edaa6ee51c917a868f08f Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 13 Oct 2023 13:51:55 +0300 Subject: [PATCH 198/242] Updated on 2026-08-14 --- .../main/res/values-ru/strings-blockchain.xml | 2 +- .../main/res/values/strings-blockchain.xml | 2 +- core/res/src/main/res/values/strings.xml | 1 - .../tokens/model/CryptoCurrencyStatus.kt | 4 ++-- .../model/warnings/CryptoCurrencyWarning.kt | 5 ++++ .../tokens/GetCurrencyWarningsUseCase.kt | 13 +++++++++- .../operations/CurrencyStatusOperations.kt | 13 +++++----- .../domain/tokens/mock/MockTokenLists.kt | 2 +- .../domain/tokens/mock/MockTokensStates.kt | 8 +++---- .../components/TokenDetailsNotification.kt | 14 +++++++++-- .../TokenDetailsNotificationConverter.kt | 5 ++++ .../state/components/WalletNotification.kt | 21 +++++++++++----- .../components/common/WalletNotifications.kt | 1 + .../WalletNotificationsListFactory.kt | 24 +++++++++++-------- 14 files changed, 80 insertions(+), 35 deletions(-) diff --git a/core/res/src/main/res/values-ru/strings-blockchain.xml b/core/res/src/main/res/values-ru/strings-blockchain.xml index fcec1f9e0c..0744b44f6b 100644 --- a/core/res/src/main/res/values-ru/strings-blockchain.xml +++ b/core/res/src/main/res/values-ru/strings-blockchain.xml @@ -7,7 +7,7 @@ Недостаточно средств для совершения транзакции. Пожалуйста, пополните свой аккаунт. Произошла ошибка. Код: %s. Из-за ограничений Kaspa в одну транзакцию может поместиться только %1$d UTXO. Это означает, что вы можете отправить только %2$s или меньше. Вам нужно уменьшить сумму. - Пополните счет на %1$s+ %2$s, чтобы создать аккаунт + Чтобы использовать сеть %1$s, вы должны оплатить резерв аккаунта (%2$s %3$s), который блокируется и не используется в вашем балансе. Аккаунт получателя не активирован. Отправьте %s или более для активации аккаунта. Минимальная сумма: %s Сдача слишком мала diff --git a/core/res/src/main/res/values/strings-blockchain.xml b/core/res/src/main/res/values/strings-blockchain.xml index 7e938d5420..bb28ad284c 100644 --- a/core/res/src/main/res/values/strings-blockchain.xml +++ b/core/res/src/main/res/values/strings-blockchain.xml @@ -7,7 +7,7 @@ Not enough funds for the transaction. Please top up your account. An error occurred. Code: %s. Due to Kaspa limitations only %1$d UTXOs can fit in a single transaction. This means you can only send %2$s or less. You need to reduce the amount. - Load %1$s+ %2$s to create account + To use the %1$s network, you must pay the account reserve (%2$s %3$s), which locks up and hides that amount indefinitely. Destination account is not active. Send %s or more to activate the account. Minimum amount is %s Change is too small diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index b04bede936..9e2b6673a4 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -608,7 +608,6 @@ Some networks currently are unreachable. Please try again later. Some networks are unreachable This is a Testnet card. Don\'t accept it as a payment. This card must only be used for testing and development purposes. - Note top up Discard You have an interrupted backup. Do you want to resume? Yes, resume diff --git a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/CryptoCurrencyStatus.kt b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/CryptoCurrencyStatus.kt index b4187361d6..8f49493cc5 100644 --- a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/CryptoCurrencyStatus.kt +++ b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/CryptoCurrencyStatus.kt @@ -71,10 +71,10 @@ data class CryptoCurrencyStatus( /** * Represents a state where there is no account associated with the cryptocurrency * - * @property errorMessage error message + * @property amountToCreateAccount base reserve amount for account creation */ data class NoAccount( - val errorMessage: String, + val amountToCreateAccount: BigDecimal, override val priceChange: BigDecimal?, override val fiatRate: BigDecimal?, ) : Status(isError = false) { diff --git a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/warnings/CryptoCurrencyWarning.kt b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/warnings/CryptoCurrencyWarning.kt index 89663cf0fa..6c8ec13eb1 100644 --- a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/warnings/CryptoCurrencyWarning.kt +++ b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/warnings/CryptoCurrencyWarning.kt @@ -18,6 +18,11 @@ sealed class CryptoCurrencyWarning { object SomeNetworksUnreachable : CryptoCurrencyWarning() + data class SomeNetworksNoAccount( + val amountToCreateAccount: BigDecimal, + val amountCurrency: CryptoCurrency, + ) : CryptoCurrencyWarning() + /** * Represents wallet blockchain rent * @param rent Amount that will be charged in overtime if the blockchain does not have an amount greater than diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCurrencyWarningsUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCurrencyWarningsUseCase.kt index 227161d34f..b35a730d3a 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCurrencyWarningsUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCurrencyWarningsUseCase.kt @@ -40,7 +40,8 @@ class GetCurrencyWarningsUseCase( flowOf(walletManagersFacade.getRentInfo(userWalletId, currency.network)), flowOf(walletManagersFacade.getExistentialDeposit(userWalletId, currency.network)), flowOf(getNetworkUnavailableWarning(currencyStatus)), - ) { maybeFeeWarning, maybeRentWarning, maybeEdWarning, maybeNetworkUnavailable -> + flowOf(getNetworkNoAccountWarning(currencyStatus)), + ) { maybeFeeWarning, maybeRentWarning, maybeEdWarning, maybeNetworkUnavailable, maybeNetworkNoAccount -> setOfNotNull( maybeRentWarning, maybeEdWarning?.let { @@ -51,6 +52,7 @@ class GetCurrencyWarningsUseCase( }, maybeFeeWarning, maybeNetworkUnavailable, + maybeNetworkNoAccount, ) }.flowOn(dispatchers.io) } @@ -106,6 +108,15 @@ class GetCurrencyWarningsUseCase( } } + private fun getNetworkNoAccountWarning(currencyStatus: CryptoCurrencyStatus): CryptoCurrencyWarning? { + return (currencyStatus.value as? CryptoCurrencyStatus.NoAccount)?.let { + CryptoCurrencyWarning.SomeNetworksNoAccount( + amountToCreateAccount = it.amountToCreateAccount, + amountCurrency = currencyStatus.currency, + ) + } + } + private fun BigDecimal?.isZero(): Boolean { return this?.signum() == 0 } diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrencyStatusOperations.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrencyStatusOperations.kt index 804c8a58bc..89f506ab39 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrencyStatusOperations.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrencyStatusOperations.kt @@ -20,7 +20,7 @@ internal class CurrencyStatusOperations( null -> CryptoCurrencyStatus.Loading is NetworkStatus.MissedDerivation -> createMissedDerivationStatus() is NetworkStatus.Unreachable -> createUnreachableStatus() - is NetworkStatus.NoAccount -> createNoAccountStatus(status.errorMessage) + is NetworkStatus.NoAccount -> createNoAccountStatus(status.amountToCreateAccount) is NetworkStatus.Verified -> createStatus(status) } } @@ -31,11 +31,12 @@ internal class CurrencyStatusOperations( private fun createUnreachableStatus(): CryptoCurrencyStatus.Unreachable = CryptoCurrencyStatus.Unreachable(priceChange = quote?.priceChange, fiatRate = quote?.fiatRate) - private fun createNoAccountStatus(message: String): CryptoCurrencyStatus.NoAccount = CryptoCurrencyStatus.NoAccount( - errorMessage = message, - priceChange = quote?.priceChange, - fiatRate = quote?.fiatRate, - ) + private fun createNoAccountStatus(amount: BigDecimal): CryptoCurrencyStatus.NoAccount = + CryptoCurrencyStatus.NoAccount( + amountToCreateAccount = amount, + priceChange = quote?.priceChange, + fiatRate = quote?.fiatRate, + ) private fun createStatus(status: NetworkStatus.Verified): CryptoCurrencyStatus.Status { val amount = status.amounts[currency.id] diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockTokenLists.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockTokenLists.kt index fb41b5aa56..0cbc4a5d5c 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockTokenLists.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockTokenLists.kt @@ -48,7 +48,7 @@ internal object MockTokenLists { val loadingUngroupedTokenList = with(failedUngroupedTokenList) { copy( - currencies = currencies.map { it.copy(value = CryptoCurrencyStatus.Loading) }, + currencies = currencies.map { it.copy(value = CryptoCurrencyStatus.Loading) }.toNonEmptyListOrNull() ?: emptyList(), totalFiatBalance = TokenList.FiatBalance.Loading, ) } diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockTokensStates.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockTokensStates.kt index 69e1b66b5c..d1410765aa 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockTokensStates.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockTokensStates.kt @@ -60,7 +60,7 @@ internal object MockTokensStates { value = CryptoCurrencyStatus.NoAccount( priceChange = MockQuotes.quote7.priceChange, fiatRate = MockQuotes.quote7.fiatRate, - errorMessage = "", + amountToCreateAccount = MockNetworks.amountToCreateAccount, ), ) @@ -69,7 +69,7 @@ internal object MockTokensStates { value = CryptoCurrencyStatus.NoAccount( priceChange = MockQuotes.quote8.priceChange, fiatRate = MockQuotes.quote8.fiatRate, - errorMessage = "", + amountToCreateAccount = MockNetworks.amountToCreateAccount, ), ) @@ -78,7 +78,7 @@ internal object MockTokensStates { value = CryptoCurrencyStatus.NoAccount( priceChange = MockQuotes.quote9.priceChange, fiatRate = MockQuotes.quote9.fiatRate, - errorMessage = "", + amountToCreateAccount = MockNetworks.amountToCreateAccount, ), ) @@ -87,7 +87,7 @@ internal object MockTokensStates { value = CryptoCurrencyStatus.NoAccount( priceChange = MockQuotes.quote10.priceChange, fiatRate = MockQuotes.quote10.fiatRate, - errorMessage = "", + amountToCreateAccount = MockNetworks.amountToCreateAccount, ), ) diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/components/TokenDetailsNotification.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/components/TokenDetailsNotification.kt index fd43bdcd6e..6a6488777c 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/components/TokenDetailsNotification.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/components/TokenDetailsNotification.kt @@ -9,9 +9,8 @@ import com.tangem.core.ui.extensions.wrappedList import com.tangem.domain.tokens.model.warnings.CryptoCurrencyWarning import com.tangem.features.tokendetails.impl.R -// TODO: Finalize notification strings [REDACTED_JIRA] @Immutable -sealed class TokenDetailsNotification { +internal sealed class TokenDetailsNotification { abstract val config: NotificationConfig @@ -83,4 +82,15 @@ sealed class TokenDetailsNotification { iconResId = R.drawable.img_attention_20, ) } + + class NetworksNoAccount(val network: String, val symbol: String, val amount: String) : Informational() { + override val config = NotificationConfig( + title = resourceReference(R.string.warning_no_account_title), + subtitle = resourceReference( + R.string.no_account_generic, + wrappedList(network, amount, symbol), + ), + iconResId = R.drawable.ic_alert_circle_24, + ) + } } \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsNotificationConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsNotificationConverter.kt index 945fd6107e..6a20547a9f 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsNotificationConverter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsNotificationConverter.kt @@ -37,6 +37,11 @@ internal class TokenDetailsNotificationConverter( onCloseClick = clickIntents::onCloseRentInfoNotification, ) CryptoCurrencyWarning.SomeNetworksUnreachable -> TokenDetailsNotification.NetworksUnreachable + is CryptoCurrencyWarning.SomeNetworksNoAccount -> TokenDetailsNotification.NetworksNoAccount( + network = warning.amountCurrency.name, + amount = warning.amountToCreateAccount.toString(), + symbol = warning.amountCurrency.symbol, + ) } } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/components/WalletNotification.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/components/WalletNotification.kt index 6c3fd2d2d8..4f62061929 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/components/WalletNotification.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/components/WalletNotification.kt @@ -2,7 +2,10 @@ package com.tangem.feature.wallet.presentation.wallet.state.components import androidx.compose.runtime.Immutable import com.tangem.core.ui.components.notifications.NotificationConfig -import com.tangem.core.ui.extensions.* +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.pluralReference +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.wrappedList import com.tangem.feature.wallet.impl.R /** @@ -88,11 +91,6 @@ sealed class WalletNotification(val config: NotificationConfig) { subtitle = resourceReference(id = R.string.warning_some_networks_unreachable_message), ) - data class TopUpNote(val errorMessage: String) : Warning( - title = resourceReference(id = R.string.warning_title_note_top_up), - subtitle = stringReference(value = errorMessage), - ) - data class NumberOfSignedHashesIncorrect(val onCloseClick: () -> Unit) : Warning( title = resourceReference(id = R.string.common_warning), subtitle = resourceReference(id = R.string.alert_card_signed_transactions), @@ -117,6 +115,17 @@ sealed class WalletNotification(val config: NotificationConfig) { ), ) + data class NoAccount(val network: String, val symbol: String, val amount: String) : WalletNotification( + config = NotificationConfig( + title = resourceReference(id = R.string.warning_no_account_title), + subtitle = resourceReference( + id = R.string.no_account_generic, + wrappedList(network, amount, symbol), + ), + iconResId = R.drawable.ic_alert_circle_24, + ), + ) + data class UnlockWallets(val onClick: () -> Unit) : WalletNotification( config = NotificationConfig( title = resourceReference(id = R.string.common_unlock_needed), diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletNotifications.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletNotifications.kt index 22c20dcfc7..1ef6f61148 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletNotifications.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletNotifications.kt @@ -32,6 +32,7 @@ internal fun LazyListScope.notifications(configs: ImmutableList TangemTheme.colors.icon.accent is WalletNotification.RateApp -> TangemTheme.colors.icon.attention is WalletNotification.UnlockWallets -> TangemTheme.colors.icon.primary1 + is WalletNotification.NoAccount -> TangemTheme.colors.icon.accent is WalletNotification.Warning -> null }, ) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletNotificationsListFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletNotificationsListFactory.kt index 859b8ccf58..4b160e908b 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletNotificationsListFactory.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletNotificationsListFactory.kt @@ -143,10 +143,7 @@ internal class WalletNotificationsListFactory( condition = cryptoCurrencyList.hasUnreachableNetworks(), ) - val errorMessage = cryptoCurrencyList.geNoAccountStatusMessage() - if (errorMessage != null) { - add(element = WalletNotification.Warning.TopUpNote(errorMessage = errorMessage)) - } + addNoAccountWarning(cryptoCurrencyList) addIf( element = WalletNotification.Warning.NumberOfSignedHashesIncorrect( @@ -169,12 +166,19 @@ internal class WalletNotificationsListFactory( return any { it.value is CryptoCurrencyStatus.Unreachable } } - private fun List.geNoAccountStatusMessage(): String? { - return this - .map(CryptoCurrencyStatus::value) - .filterIsInstance() - .firstOrNull() - ?.errorMessage + private fun MutableList.addNoAccountWarning(cryptoCurrencyList: List) { + val noAccountNetwork = cryptoCurrencyList.firstOrNull { it.value is CryptoCurrencyStatus.NoAccount } + if (noAccountNetwork != null) { + val amountToCreateAccount = (noAccountNetwork.value as? CryptoCurrencyStatus.NoAccount) + ?.amountToCreateAccount.toString() + add( + element = WalletNotification.NoAccount( + network = noAccountNetwork.currency.name, + amount = amountToCreateAccount, + symbol = noAccountNetwork.currency.symbol, + ), + ) + } } /** From 5f95ba532e65333579a5e7f9ddc3c9421be99fa3 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 13 Oct 2023 22:51:18 +0800 Subject: [PATCH 199/242] Updated on 2026-08-14 --- .../BiometricUserWalletsListManager.kt | 46 +++++++++++-------- .../wallets/legacy/UserWalletsListManager.kt | 7 +++ .../wallets/usecase/UnlockWalletsUseCase.kt | 5 +- .../wallet/viewmodels/WalletViewModel.kt | 8 +++- 4 files changed, 43 insertions(+), 23 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/domain/userWalletList/implementation/BiometricUserWalletsListManager.kt b/app/src/main/java/com/tangem/tap/domain/userWalletList/implementation/BiometricUserWalletsListManager.kt index 59819e3d7d..f4c32feb54 100644 --- a/app/src/main/java/com/tangem/tap/domain/userWalletList/implementation/BiometricUserWalletsListManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/userWalletList/implementation/BiometricUserWalletsListManager.kt @@ -57,21 +57,11 @@ internal class BiometricUserWalletsListManager( get() = state.value.userWallets.size override suspend fun unlock(): CompletionResult { - return unlockWithBiometryInternal() - .mapFailure { error -> - if (error is UserWalletsListError) { - error - } else { - UserWalletsListError.UnableToUnlockUserWallets(cause = error) - } - } - .map { - selectedUserWalletSync.guard { - throw UserWalletsListError.UnableToUnlockUserWallets( - cause = IllegalStateException("No user wallet selected"), - ) - } - } + return unlockWithBiometryInternal().mapUnlockResult() + } + + override suspend fun unlockAndSelect(selectedWalletId: UserWalletId): CompletionResult { + return unlockWithBiometryInternal(selectedWalletId = selectedWalletId).mapUnlockResult() } override fun lock() { @@ -198,7 +188,7 @@ internal class BiometricUserWalletsListManager( } } - private suspend fun unlockWithBiometryInternal(): CompletionResult { + private suspend fun unlockWithBiometryInternal(selectedWalletId: UserWalletId? = null): CompletionResult { return keysRepository.getAll() .map { keys -> state.update { prevState -> @@ -207,7 +197,7 @@ internal class BiometricUserWalletsListManager( ) } } - .flatMap { loadModels() } + .flatMap { loadModels(selectedWalletId = selectedWalletId) } .map { state.update { prevState -> val hasLockedUserWallets = prevState.userWallets.any { it.isLocked } @@ -216,6 +206,24 @@ internal class BiometricUserWalletsListManager( } } + private fun CompletionResult.mapUnlockResult(): CompletionResult { + return this + .mapFailure { error -> + if (error is UserWalletsListError) { + error + } else { + UserWalletsListError.UnableToUnlockUserWallets(cause = error) + } + } + .map { + selectedUserWalletSync.guard { + throw UserWalletsListError.UnableToUnlockUserWallets( + cause = IllegalStateException("No user wallet selected"), + ) + } + } + } + private suspend fun saveEncryptionKeyIfNotNull(userWallet: UserWallet): CompletionResult { val encryptionKey = userWallet.scanResponse.card.encryptionKey ?.let { UserWalletEncryptionKey(userWallet.walletId, it) } @@ -237,7 +245,7 @@ internal class BiometricUserWalletsListManager( } } - private suspend fun loadModels(): CompletionResult { + private suspend fun loadModels(selectedWalletId: UserWalletId? = null): CompletionResult { return getSavedUserWallets() .map { userWallets -> if (userWallets.isNotEmpty()) { @@ -247,7 +255,7 @@ internal class BiometricUserWalletsListManager( prevState.copy( userWallets = wallets, selectedUserWalletId = findOrSetSelectedUserWalletId( - prevSelectedWalletId = prevState.selectedUserWalletId, + prevSelectedWalletId = selectedWalletId ?: prevState.selectedUserWalletId, userWallets = wallets, ), ) diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/legacy/UserWalletsListManager.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/legacy/UserWalletsListManager.kt index ff4e805ad1..db6a353f7b 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/legacy/UserWalletsListManager.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/legacy/UserWalletsListManager.kt @@ -110,6 +110,13 @@ interface UserWalletsListManager { */ suspend fun unlock(): CompletionResult + /** + * Unlock all [UserWallet]s and select passed [UserWalletId] + * + * @param selectedWalletId [UserWalletId] of [UserWallet] which must be selected + */ + suspend fun unlockAndSelect(selectedWalletId: UserWalletId): CompletionResult + /** Remove [UserWallet]s from [userWallets] and set [isLocked] as true */ fun lock() } diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/UnlockWalletsUseCase.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/UnlockWalletsUseCase.kt index 3459902720..2a713a0ac8 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/UnlockWalletsUseCase.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/UnlockWalletsUseCase.kt @@ -8,6 +8,7 @@ import com.tangem.common.doOnSuccess import com.tangem.domain.wallets.legacy.WalletsStateHolder import com.tangem.domain.wallets.legacy.asLockable import com.tangem.domain.wallets.models.UnlockWalletError +import com.tangem.domain.wallets.models.UserWalletId /** * Unlock wallets use case @@ -18,11 +19,11 @@ import com.tangem.domain.wallets.models.UnlockWalletError */ class UnlockWalletsUseCase(private val walletsStateHolder: WalletsStateHolder) { - suspend operator fun invoke(): Either { + suspend operator fun invoke(selectedWalletId: UserWalletId): Either { val userWalletsListManager = walletsStateHolder.userWalletsListManager?.asLockable() ?: return UnlockWalletError.CommonError.left() - userWalletsListManager.unlock() + userWalletsListManager.unlockAndSelect(selectedWalletId = selectedWalletId) .doOnSuccess { return Unit.right() } .doOnFailure { return UnlockWalletError.CommonError.left() } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt index a182b01ae7..34ffd5f578 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt @@ -763,10 +763,14 @@ internal class WalletViewModel @Inject constructor( } override fun onUnlockWalletClick() { + val state = uiState as? WalletState.ContentState ?: return + analyticsEventsHandler.send(WalletScreenAnalyticsEvent.NoticeWalletLocked) - viewModelScope.launch(dispatchers.io) { - unlockWalletsUseCase() + viewModelScope.launch(dispatchers.main) { + unlockWalletsUseCase( + selectedWalletId = state.walletsListConfig.wallets[state.walletsListConfig.selectedWalletIndex].id, + ) } } From 4942718cc938a7008d9a3d5c9ccf59083a0094ee Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 13 Oct 2023 19:08:56 +0800 Subject: [PATCH 200/242] Updated on 2026-08-14 --- .../common/redux/global/GlobalMiddleware.kt | 9 +- .../warningMessage/WarningMessagesManager.kt | 20 +-- .../impl/presentation/ui/TokensListScreen.kt | 2 +- .../redux/middlewares/WarningsMiddleware.kt | 5 +- .../tap/features/wallet/ui/BalanceWidget.kt | 17 ++- .../wallet/ui/WalletDetailsFragment.kt | 11 +- .../wallet/ui/WalletWarningConverter.kt | 68 ++++----- .../wallet/ui/adapters/WalletAdapter.kt | 7 +- .../ui/adapters/WarningMessagesAdapter.kt | 31 ++-- .../ui/dialogs/SignedHashesWarningDialog.kt | 6 +- app/src/main/res/layout/layout_balance.xml | 69 +++++---- .../layout/layout_single_wallet_balance.xml | 139 +++++++++--------- .../layout/layout_wallet_backup_warning.xml | 88 +++++------ .../res/layout/layout_warning_card_action.xml | 23 +-- core/res/src/main/res/values-de/strings.xml | 2 - core/res/src/main/res/values-fr/strings.xml | 2 - core/res/src/main/res/values-it/strings.xml | 2 - core/res/src/main/res/values-ru/strings.xml | 57 ++++--- .../src/main/res/values-zh-rTW/strings.xml | 22 +-- core/res/src/main/res/values/strings.xml | 50 +++---- .../components/WalletBottomSheetConfig.kt | 2 +- .../state/components/WalletNotification.kt | 34 ++--- 22 files changed, 322 insertions(+), 344 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalMiddleware.kt b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalMiddleware.kt index 9f6d1d71c0..e3c134ee2d 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalMiddleware.kt @@ -14,7 +14,6 @@ import com.tangem.tap.common.extensions.dispatchOnMain import com.tangem.tap.common.extensions.dispatchWithMain import com.tangem.tap.common.redux.AppDialog import com.tangem.tap.common.redux.AppState -import com.tangem.tap.domain.configurable.warningMessage.WarningMessagesManager import com.tangem.tap.features.send.redux.SendAction import com.tangem.tap.features.wallet.redux.WalletAction import com.tangem.tap.network.exchangeServices.BuyExchangeService @@ -84,10 +83,10 @@ private fun handleAction(action: Action, appState: () -> AppState?, dispatch: Di is GlobalAction.HideWarningMessage -> { store.state.globalState.warningManager?.let { if (it.hideWarning(action.warning)) { - if (WarningMessagesManager.isAlreadySignedHashesWarning(action.warning)) { - // TODO: No appropriate warningMessage identification. Make it better later - store.dispatch(WalletAction.Warnings.CheckHashesCount.SaveCardId) - } + // if (WarningMessagesManager.isAlreadySignedHashesWarning()) { + // // TODO: No appropriate warningMessage identification. Make it better later + // store.dispatch(WalletAction.Warnings.CheckHashesCount.SaveCardId) + // } store.dispatch(WalletAction.Warnings.Update) store.dispatch(SendAction.Warnings.Update) diff --git a/app/src/main/java/com/tangem/tap/domain/configurable/warningMessage/WarningMessagesManager.kt b/app/src/main/java/com/tangem/tap/domain/configurable/warningMessage/WarningMessagesManager.kt index 183ba76207..f1345d6989 100644 --- a/app/src/main/java/com/tangem/tap/domain/configurable/warningMessage/WarningMessagesManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/configurable/warningMessage/WarningMessagesManager.kt @@ -8,6 +8,8 @@ import java.util.concurrent.CopyOnWriteArrayList /** [REDACTED_AUTHOR] */ +// TODO: Delete with WalletFeatureToggles +@Deprecated(message = "Used only in old wallet screen") class WarningMessagesManager { private val warningsList = CopyOnWriteArrayList() @@ -73,7 +75,7 @@ class WarningMessagesManager { location = listOf(WarningMessage.Location.MainScreen), blockchains = null, titleResId = R.string.common_warning, - messageResId = R.string.alert_developer_card, + // messageResId = R.string.alert_developer_card, origin = WarningMessage.Origin.Local, ) @@ -85,7 +87,7 @@ class WarningMessagesManager { location = listOf(WarningMessage.Location.MainScreen), blockchains = null, titleResId = R.string.common_warning, - messageResId = R.string.alert_card_signed_transactions, + // messageResId = R.string.alert_card_signed_transactions, origin = WarningMessage.Origin.Local, ) @@ -96,8 +98,8 @@ class WarningMessagesManager { priority = WarningMessage.Priority.Info, location = listOf(WarningMessage.Location.MainScreen), blockchains = null, - titleResId = R.string.warning_important_security_info, - messageResId = R.string.warning_signed_tx_previously, + // titleResId = R.string.warning_important_security_info, + // messageResId = R.string.warning_signed_tx_previously, origin = WarningMessage.Origin.Local, buttonTextId = R.string.warning_button_learn_more, titleFormatArg = "\u26A0", @@ -147,7 +149,7 @@ class WarningMessagesManager { location = listOf(WarningMessage.Location.MainScreen), blockchains = null, titleResId = R.string.common_warning, - messageResId = R.string.alert_demo_message, + // messageResId = R.string.alert_demo_message, origin = WarningMessage.Origin.Local, ) @@ -160,14 +162,14 @@ class WarningMessagesManager { location = listOf(WarningMessage.Location.MainScreen), blockchains = null, titleResId = R.string.common_warning, - messageResId = R.string.warning_low_signatures_format, + // messageResId = R.string.warning_low_signatures_format, origin = WarningMessage.Origin.Local, messageFormatArg = remainingSignatures.toString(), ) } - fun isAlreadySignedHashesWarning(warning: WarningMessage): Boolean { - return warning.messageResId == R.string.alert_card_signed_transactions - } + // fun isAlreadySignedHashesWarning(warning: WarningMessage): Boolean { + // return warning.messageResId == R.string.alert_card_signed_transactions + // } } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/ui/TokensListScreen.kt b/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/ui/TokensListScreen.kt index 67d8066531..cf7f9c448b 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/ui/TokensListScreen.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/ui/TokensListScreen.kt @@ -151,7 +151,7 @@ private fun DifferentAddressesWarning() { ), contentAlignment = Alignment.Center, ) { - val text = stringResource(id = R.string.alert_manage_tokens_addresses_message) + val text = stringResource(id = R.string.warning_manage_tokens_legacy_derivation_message) Text( text = text, modifier = Modifier.padding( diff --git a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/WarningsMiddleware.kt b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/WarningsMiddleware.kt index e908e27b26..39e905255e 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/WarningsMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/WarningsMiddleware.kt @@ -19,11 +19,12 @@ import com.tangem.tap.preferencesStorage import com.tangem.tap.proxy.redux.DaggerGraphState import com.tangem.tap.scope import com.tangem.tap.store -import com.tangem.wallet.R import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +// TODO: Delete with WalletFeatureToggles +@Deprecated(message = "Used only in old wallet screen") class WarningsMiddleware { fun handle(action: WalletAction.Warnings, globalState: GlobalState?) { when (action) { @@ -49,7 +50,7 @@ class WarningsMiddleware { if (action.remainingSignatures != null && action.remainingSignatures <= WarningMessagesManager.REMAINING_SIGNATURES_WARNING ) { - store.state.globalState.warningManager?.removeWarnings(R.string.warning_low_signatures_format) + // store.state.globalState.warningManager?.removeWarnings(R.string.warning_low_signatures_format) addWarningMessage( warning = WarningMessagesManager.remainingSignaturesNotEnough(action.remainingSignatures), autoUpdate = true, diff --git a/app/src/main/java/com/tangem/tap/features/wallet/ui/BalanceWidget.kt b/app/src/main/java/com/tangem/tap/features/wallet/ui/BalanceWidget.kt index 3cab6df274..eb7dfd7126 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/ui/BalanceWidget.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/ui/BalanceWidget.kt @@ -10,6 +10,8 @@ import com.tangem.tap.store import com.tangem.wallet.R import com.tangem.wallet.databinding.CardBalanceBinding +// TODO: Delete with WalletFeatureToggles +@Deprecated(message = "Used only in old wallet screen") class BalanceWidget( private val binding: CardBalanceBinding, private val fragment: WalletFragment, @@ -46,12 +48,11 @@ class BalanceWidget( val statusView = if (blockchainWalletData.status is WalletDataModel.VerifiedOnline) { R.id.tv_status_verified } else { - tvStatusError.text = - fragment.getText(R.string.wallet_balance_tx_in_progress) + // tvStatusError.text = fragment.getText(R.string.wallet_balance_tx_in_progress) R.id.group_error } showStatus(statusView) - tvStatusErrorMessage.hide() + // tvStatusErrorMessage.hide() if (tokenWalletData != null) { showBalanceWithToken(blockchainWalletData, true) @@ -70,12 +71,12 @@ class BalanceWidget( tvCurrency.text = currency tvAmount.text = "" - tvStatusErrorMessage.text = blockchainWalletData.status.errorMessage - tvStatusError.text = - fragment.getString(R.string.wallet_balance_blockchain_unreachable) + // tvStatusErrorMessage.text = blockchainWalletData.status.errorMessage + // TODO: Delete with WalletFeatureToggles + // tvStatusError.text = fragment.getString(R.string.wallet_balance_blockchain_unreachable) showStatus(R.id.group_error) - tvStatusErrorMessage.show(!blockchainWalletData.status.errorMessage.isNullOrBlank()) + // tvStatusErrorMessage.show(!blockchainWalletData.status.errorMessage.isNullOrBlank()) } is WalletDataModel.NoAccount -> with(binding.lBalanceError) { binding.lBalance.root.hide() @@ -93,7 +94,7 @@ class BalanceWidget( } private fun showStatus(@IdRes viewRes: Int) = with(binding.lBalance) { - groupError.show(viewRes == R.id.group_error) + // groupError.show(viewRes == R.id.group_error) tvStatusLoading.show(viewRes == R.id.tv_status_loading) tvStatusVerified.show(viewRes == R.id.tv_status_verified) } diff --git a/app/src/main/java/com/tangem/tap/features/wallet/ui/WalletDetailsFragment.kt b/app/src/main/java/com/tangem/tap/features/wallet/ui/WalletDetailsFragment.kt index 9099193e8b..de08710457 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/ui/WalletDetailsFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/ui/WalletDetailsFragment.kt @@ -62,7 +62,9 @@ import javax.inject.Inject /** * Wallet details fragment - use only for MultiWallet */ +// TODO: Delete with WalletFeatureToggles @Suppress("LargeClass", "MagicNumber") +@Deprecated(message = "Used only in old wallet screen") @AndroidEntryPoint class WalletDetailsFragment : Fragment(R.layout.fragment_wallet_details), SafeStoreSubscriber { @@ -436,10 +438,11 @@ class WalletDetailsFragment : Fragment(R.layout.fragment_wallet_details), SafeSt lBalance.root.show() lBalance.groupBalance.hide() lBalance.tvError.show() - lBalance.tvError.setWarningStatus( - R.string.wallet_balance_blockchain_unreachable, - status.errorMessage, - ) + // TODO: Delete with WalletFeatureToggles + // lBalance.tvError.setWarningStatus( + // R.string.wallet_balance_blockchain_unreachable, + // status.errorMessage, + // ) } is WalletDataModel.NoAccount -> { lBalance.root.hide() diff --git a/app/src/main/java/com/tangem/tap/features/wallet/ui/WalletWarningConverter.kt b/app/src/main/java/com/tangem/tap/features/wallet/ui/WalletWarningConverter.kt index afbf3bff5a..5f98692fca 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/ui/WalletWarningConverter.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/ui/WalletWarningConverter.kt @@ -9,43 +9,45 @@ import com.tangem.wallet.R /** [REDACTED_AUTHOR] */ +// TODO: Delete with WalletFeatureToggles +@Deprecated(message = "Used only in old wallet screen") class WalletWarningConverter( private val context: Context, ) : ModuleMessageConverter { override fun convert(message: WalletWarning): WalletWarningDescription { - val warningMessage = when (message) { - is WalletWarning.ExistentialDeposit -> { - context.getString( - R.string.warning_existential_deposit_message, - message.currencyName, - message.edStringValueWithSymbol, - ) - } - is WalletWarning.BalanceNotEnoughForFee -> { - context.getString( - R.string.token_details_send_blocked_fee_format, - message.currencyName, - message.blockchainFullName, - message.currencyName, - message.blockchainFullName, - message.blockchainSymbol, - ) - } - is WalletWarning.TransactionInProgress -> { - context.getString( - R.string.token_details_send_blocked_tx_format, - message.currencyName, - ) - } - is WalletWarning.Rent -> { - context.getString( - R.string.solana_rent_warning, - message.walletRent.rent, - message.walletRent.exemptionAmount, - ) - } - } - return WalletWarningDescription(context.getString(R.string.common_warning), warningMessage) + // val warningMessage = when (message) { + // is WalletWarning.ExistentialDeposit -> { + // context.getString( + // R.string.warning_existential_deposit_message, + // message.currencyName, + // message.edStringValueWithSymbol, + // ) + // } + // is WalletWarning.BalanceNotEnoughForFee -> { + // context.getString( + // R.string.token_details_send_blocked_fee_format, + // message.currencyName, + // message.blockchainFullName, + // message.currencyName, + // message.blockchainFullName, + // message.blockchainSymbol, + // ) + // } + // is WalletWarning.TransactionInProgress -> { + // context.getString( + // R.string.token_details_send_blocked_tx_format, + // message.currencyName, + // ) + // } + // is WalletWarning.Rent -> { + // context.getString( + // R.string.solana_rent_warning, + // message.walletRent.rent, + // message.walletRent.exemptionAmount, + // ) + // } + // } + return WalletWarningDescription(context.getString(R.string.common_warning), "") } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/wallet/ui/adapters/WalletAdapter.kt b/app/src/main/java/com/tangem/tap/features/wallet/ui/adapters/WalletAdapter.kt index 3f511f7b65..0b1e671dab 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/ui/adapters/WalletAdapter.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/ui/adapters/WalletAdapter.kt @@ -22,6 +22,8 @@ import com.tangem.tap.store import com.tangem.wallet.R import com.tangem.wallet.databinding.ItemCurrencyWalletBinding +// TODO: Delete with WalletFeatureToggles +@Deprecated(message = "Used only in old wallet screen") class WalletAdapter : ListAdapter(DiffUtilCallback) { override fun getItemId(position: Int): Long { @@ -59,7 +61,8 @@ class WalletAdapter : ListAdapter { - root.getString(R.string.wallet_balance_blockchain_unreachable) + // TODO: Delete with WalletFeatureToggles + // root.getString(R.string.wallet_balance_blockchain_unreachable) } is WalletDataModel.MissedDerivation -> { root.getString(R.string.wallet_balance_missing_derivation) @@ -86,7 +89,7 @@ class WalletAdapter : ListAdapter(DiffUtilCallback) { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): WarningMessageVH { @@ -94,11 +91,11 @@ class WarningMessageVH(val binding: LayoutWarningCardActionBinding) : RecyclerVi val buttonAction = when (warning.titleResId) { - R.string.warning_important_security_info -> { - View.OnClickListener { - store.dispatch(WalletAction.DialogAction.SignedHashesMultiWalletDialog) - } - } + // R.string.warning_important_security_info -> { + // View.OnClickListener { + // store.dispatch(WalletAction.DialogAction.SignedHashesMultiWalletDialog) + // } + // } else -> { View.OnClickListener { store.dispatch(GlobalAction.HideWarningMessage(warning)) @@ -120,12 +117,12 @@ class WarningMessageVH(val binding: LayoutWarningCardActionBinding) : RecyclerVi store.dispatch(GlobalAction.HideWarningMessage(warning)) store.dispatch(WalletAction.Warnings.AppRating.RemindLater) } - binding.btnCanBeBetter.setOnClickListener { - Analytics.send(MainScreen.NoticeRateAppButton(AnalyticsParam.RateApp.Disliked)) - store.dispatch(WalletAction.Warnings.AppRating.SetNeverToShow) - store.dispatch(GlobalAction.HideWarningMessage(warning)) - store.dispatch(GlobalAction.SendEmail(RateCanBeBetterEmail())) - } + // binding.btnCanBeBetter.setOnClickListener { + // Analytics.send(MainScreen.NoticeRateAppButton(AnalyticsParam.RateApp.Disliked)) + // store.dispatch(WalletAction.Warnings.AppRating.SetNeverToShow) + // store.dispatch(GlobalAction.HideWarningMessage(warning)) + // store.dispatch(GlobalAction.SendEmail(RateCanBeBetterEmail())) + // } binding.btnReallyCool.setOnClickListener { val activity = binding.root.context.getActivity() ?: return@setOnClickListener diff --git a/app/src/main/java/com/tangem/tap/features/wallet/ui/dialogs/SignedHashesWarningDialog.kt b/app/src/main/java/com/tangem/tap/features/wallet/ui/dialogs/SignedHashesWarningDialog.kt index f4de1c7672..8ebbffeb54 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/ui/dialogs/SignedHashesWarningDialog.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/ui/dialogs/SignedHashesWarningDialog.kt @@ -9,11 +9,13 @@ import com.tangem.tap.features.wallet.redux.WalletAction import com.tangem.tap.store import com.tangem.wallet.R +// TODO: Delete with WalletFeatureToggles +@Deprecated(message = "Used only in old wallet screen") object SignedHashesWarningDialog { fun create(context: Context): AlertDialog { return MaterialAlertDialogBuilder(context, R.style.CustomMaterialDialog).apply { - setTitle(context.getString(R.string.warning_important_security_info, "\u26A0")) - setMessage(R.string.alert_signed_hashes_message) + // setTitle(context.getString(R.string.warning_important_security_info, "\u26A0")) + // setMessage(R.string.alert_signed_hashes_message) setPositiveButton(R.string.common_understand) { _, _ -> store.dispatch(WalletAction.Warnings.CheckHashesCount.SaveCardId) store.dispatch( diff --git a/app/src/main/res/layout/layout_balance.xml b/app/src/main/res/layout/layout_balance.xml index 6addd2be86..4141bd7302 100644 --- a/app/src/main/res/layout/layout_balance.xml +++ b/app/src/main/res/layout/layout_balance.xml @@ -38,41 +38,40 @@ app:layout_constraintTop_toBottomOf="@id/tv_currency" tools:visibility="visible" /> - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + android:paddingBottom="4dp" + android:src="@drawable/img_warning_triangle_24" + android:scaleType="center" + android:background="@drawable/shape_ellipse" + android:backgroundTint="@color/buttonGray" + android:contentDescription="@null" + app:layout_constraintTop_toTopOf="parent" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintStart_toStartOf="parent" /> - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/layout_warning_card_action.xml b/app/src/main/res/layout/layout_warning_card_action.xml index e85e77d3e5..bbb58fab01 100644 --- a/app/src/main/res/layout/layout_warning_card_action.xml +++ b/app/src/main/res/layout/layout_warning_card_action.xml @@ -45,17 +45,18 @@ app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toBottomOf="@+id/warning_content_container" /> -