From fefe37a890d676d905c8506cbc4f1bf6eac00191 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 31 Aug 2023 16:58:05 +0300 Subject: [PATCH 01/69] 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 02/69] 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 03/69] 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 04/69] 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 05/69] 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 06/69] 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 07/69] 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 08/69] 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 09/69] 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 10/69] 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 11/69] 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 12/69] 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 13/69] 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 14/69] 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 15/69] 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 16/69] 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 17/69] 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 18/69] 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 19/69] 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 20/69] 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 21/69] 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 22/69] 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 23/69] 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 24/69] 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 25/69] 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 26/69] 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 27/69] 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 28/69] 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 29/69] 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 30/69] 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 31/69] 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 32/69] 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 33/69] 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 34/69] 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 35/69] 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 36/69] 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 @@ -