diff --git a/app/build.gradle.kts b/app/build.gradle.kts index de202bea18..5e5e70656a 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -31,7 +31,10 @@ dependencies { implementation(project(":domain:wallets:models")) implementation(projects.domain.settings) implementation(projects.domain.tokens) + implementation(projects.domain.tokens.models) implementation(projects.domain.txhistory) + implementation(projects.domain.appCurrency) + implementation(projects.domain.appCurrency.models) implementation(project(":common")) implementation(project(":core:analytics")) @@ -51,6 +54,7 @@ dependencies { implementation(projects.data.settings) implementation(projects.data.tokens) implementation(projects.data.txhistory) + implementation(projects.data.appCurrency) /** Features */ implementation(project(":features:onboarding")) diff --git a/app/src/main/assets/tangem-app-config b/app/src/main/assets/tangem-app-config index b791bd4cf6..8c1c53b739 160000 --- a/app/src/main/assets/tangem-app-config +++ b/app/src/main/assets/tangem-app-config @@ -1 +1 @@ -Subproject commit b791bd4cf6c5eca9778f89e87cd62b72d24f5ce9 +Subproject commit 8c1c53b73950698d4acfa4d925b8c14bf6f0da63 diff --git a/app/src/main/java/com/tangem/tap/LockUserWalletsTimer.kt b/app/src/main/java/com/tangem/tap/LockUserWalletsTimer.kt index 569a62b33a..256bd62a5f 100644 --- a/app/src/main/java/com/tangem/tap/LockUserWalletsTimer.kt +++ b/app/src/main/java/com/tangem/tap/LockUserWalletsTimer.kt @@ -5,8 +5,8 @@ import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.lifecycleScope import com.tangem.core.navigation.AppScreen import com.tangem.core.navigation.NavigationAction +import com.tangem.domain.wallets.legacy.asLockable import com.tangem.tap.common.extensions.dispatchOnMain -import com.tangem.tap.domain.userWalletList.asLockable import kotlinx.coroutines.* import timber.log.Timber import kotlin.time.Duration diff --git a/app/src/main/java/com/tangem/tap/TapApplication.kt b/app/src/main/java/com/tangem/tap/TapApplication.kt index cb21e52c1d..28cd0f57de 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.DomainLayer +import com.tangem.domain.appcurrency.repository.AppCurrencyRepository import com.tangem.domain.card.ScanCardProcessor import com.tangem.domain.common.LogConfig import com.tangem.domain.wallets.legacy.WalletManagersRepository @@ -161,6 +162,9 @@ class TapApplication : Application(), ImageLoaderFactory { @Inject lateinit var blockchainExceptionHandler: BlockchainExceptionHandler + @Inject + lateinit var appCurrencyRepository: AppCurrencyRepository + override fun onCreate() { super.onCreate() @@ -179,6 +183,7 @@ class TapApplication : Application(), ImageLoaderFactory { walletConnectSessionsRepository = walletConnectSessionsRepository, tokenDetailsFeatureToggles = tokenDetailsFeatureToggles, scanCardProcessor = scanCardProcessor, + appCurrencyRepository = appCurrencyRepository, ), ), ) diff --git a/app/src/main/java/com/tangem/tap/common/extensions/Blockchain.kt b/app/src/main/java/com/tangem/tap/common/extensions/Blockchain.kt index 4d081cc2e6..88a8e16ed6 100644 --- a/app/src/main/java/com/tangem/tap/common/extensions/Blockchain.kt +++ b/app/src/main/java/com/tangem/tap/common/extensions/Blockchain.kt @@ -16,7 +16,7 @@ fun Blockchain.getGreyedOutIconRes(): Int { Blockchain.Ethereum, Blockchain.EthereumTestnet -> R.drawable.ic_eth_no_color Blockchain.EthereumClassic, Blockchain.EthereumClassicTestnet -> R.drawable.ic_eth_no_color Blockchain.RSK -> R.drawable.ic_rsk_no_color - Blockchain.Cardano, Blockchain.CardanoShelley -> R.drawable.ic_cardano_no_color + Blockchain.Cardano -> R.drawable.ic_cardano_no_color Blockchain.Tezos -> R.drawable.ic_tezos_no_color Blockchain.XRP -> R.drawable.ic_xrp_no_color Blockchain.Stellar -> R.drawable.ic_stellar_no_color @@ -46,6 +46,7 @@ fun Blockchain.getGreyedOutIconRes(): Int { Blockchain.Telos, Blockchain.TelosTestnet -> R.drawable.ic_telos_no_color Blockchain.AlephZero, Blockchain.AlephZeroTestnet -> R.drawable.ic_azero_no_color Blockchain.OctaSpace, Blockchain.OctaSpaceTestnet -> R.drawable.ic_octaspace_no_color + Blockchain.Chia, Blockchain.ChiaTestnet -> R.drawable.ic_chia_no_color else -> R.drawable.ic_tangem_logo } } 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 46835f3076..287b5d35f1 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 @@ -8,10 +8,12 @@ import com.tangem.domain.common.LogConfig import com.tangem.domain.common.extensions.withMainContext import com.tangem.domain.models.scan.CardDTO import com.tangem.domain.models.scan.ScanResponse +import com.tangem.tap.* import com.tangem.tap.common.entities.FiatCurrency import com.tangem.tap.common.extensions.dispatchDebugErrorNotification import com.tangem.tap.common.extensions.dispatchDialogShow import com.tangem.tap.common.extensions.dispatchOnMain +import com.tangem.tap.common.extensions.dispatchWithMain import com.tangem.tap.common.redux.AppDialog import com.tangem.tap.common.redux.AppState import com.tangem.tap.domain.configurable.warningMessage.WarningMessagesManager @@ -24,17 +26,13 @@ import com.tangem.tap.network.exchangeServices.ExchangeService import com.tangem.tap.network.exchangeServices.mercuryo.MercuryoEnvironment import com.tangem.tap.network.exchangeServices.mercuryo.MercuryoService import com.tangem.tap.network.exchangeServices.moonpay.MoonPayService -import com.tangem.tap.preferencesStorage -import com.tangem.tap.scope -import com.tangem.tap.store -import com.tangem.tap.tangemSdkManager -import com.tangem.tap.userTokensRepository -import com.tangem.tap.walletCurrenciesManager +import com.tangem.tap.proxy.redux.DaggerGraphState +import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.launch import org.rekotlin.Action import org.rekotlin.DispatchFunction import org.rekotlin.Middleware -import java.util.* +import java.util.Locale object GlobalMiddleware { val handler = globalMiddlewareHandler @@ -68,13 +66,11 @@ private fun handleAction(action: Action, appState: () -> AppState?, dispatch: Di } } is GlobalAction.RestoreAppCurrency -> { - store.dispatch( - GlobalAction.RestoreAppCurrency.Success( - preferencesStorage.fiatCurrenciesPrefStorage.getAppCurrency() - ?.run { FiatCurrency(code, name, symbol) } - ?: FiatCurrency.Default, - ), - ) + if (store.state.daggerGraphState.get(DaggerGraphState::walletFeatureToggles).isRedesignedScreenEnabled) { + restoreAppCurrencyNew() + } else { + restoreAppCurrencyLegacy() + } } is GlobalAction.HideWarningMessage -> { store.state.globalState.warningManager?.let { @@ -193,6 +189,28 @@ private fun handleAction(action: Action, appState: () -> AppState?, dispatch: Di } } +private fun restoreAppCurrencyLegacy() { + store.dispatch( + GlobalAction.RestoreAppCurrency.Success( + preferencesStorage.fiatCurrenciesPrefStorage.getAppCurrency() + ?.run { FiatCurrency(code, name, symbol) } + ?: FiatCurrency.Default, + ), + ) +} + +private fun restoreAppCurrencyNew() { + scope.launch { + val currency = store.state.daggerGraphState.get(DaggerGraphState::appCurrencyRepository) + .getSelectedAppCurrency() + .firstOrNull() + ?.run { FiatCurrency(code, name, symbol) } + ?: FiatCurrency.Default + + store.dispatchWithMain(GlobalAction.RestoreAppCurrency.Success(currency)) + } +} + private fun makeSellExchangeService(config: Config): ExchangeService { return MoonPayService( apiKey = config.moonPayApiKey, diff --git a/app/src/main/java/com/tangem/tap/data/RuntimeUserWalletsStore.kt b/app/src/main/java/com/tangem/tap/data/RuntimeUserWalletsStore.kt index d46dfadcad..a84ddc77c6 100644 --- a/app/src/main/java/com/tangem/tap/data/RuntimeUserWalletsStore.kt +++ b/app/src/main/java/com/tangem/tap/data/RuntimeUserWalletsStore.kt @@ -12,6 +12,9 @@ internal class RuntimeUserWalletsStore( private val walletsStateHolder: WalletsStateHolder, ) : UserWalletsStore { + override val selectedUserWalletOrNull: UserWallet? + get() = walletsStateHolder.userWalletsListManager?.selectedUserWalletSync + override suspend fun getSyncOrNull(key: UserWalletId): UserWallet? { return walletsStateHolder.userWalletsListManager ?.userWallets 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 new file mode 100644 index 0000000000..c69bca18d1 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/di/domain/AppCurrencyDomainModule.kt @@ -0,0 +1,22 @@ +package com.tangem.tap.di.domain + +import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase +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) + } +} \ 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 8fc08144df..d5ab303555 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,9 +1,9 @@ package com.tangem.tap.di.domain import com.tangem.domain.tokens.* +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.tokens.repository.TokensRepository import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.Module import dagger.Provides @@ -18,23 +18,34 @@ internal object TokensDomainModule { @Provides @ViewModelScoped fun provideGetTokenListUseCase( - tokensRepository: TokensRepository, + currenciesRepository: CurrenciesRepository, quotesRepository: QuotesRepository, networksRepository: NetworksRepository, dispatchers: CoroutineDispatcherProvider, ): GetTokenListUseCase { - return GetTokenListUseCase(tokensRepository, quotesRepository, networksRepository, dispatchers) + return GetTokenListUseCase(currenciesRepository, quotesRepository, networksRepository, dispatchers) + } + + @Provides + @ViewModelScoped + fun provideGetCurrencyUseCase( + currenciesRepository: CurrenciesRepository, + quotesRepository: QuotesRepository, + networksRepository: NetworksRepository, + dispatchers: CoroutineDispatcherProvider, + ): GetCurrencyUseCase { + return GetCurrencyUseCase(currenciesRepository, quotesRepository, networksRepository, dispatchers) } @Provides @ViewModelScoped fun provideGetPrimaryCurrencyUseCase( - tokensRepository: TokensRepository, + currenciesRepository: CurrenciesRepository, quotesRepository: QuotesRepository, networksRepository: NetworksRepository, dispatchers: CoroutineDispatcherProvider, ): GetPrimaryCurrencyUseCase { - return GetPrimaryCurrencyUseCase(tokensRepository, quotesRepository, networksRepository, dispatchers) + return GetPrimaryCurrencyUseCase(currenciesRepository, quotesRepository, networksRepository, dispatchers) } @Provides @@ -55,9 +66,9 @@ internal object TokensDomainModule { @Provides @ViewModelScoped fun provideApplyTokenListSortingUseCase( - tokensRepository: TokensRepository, + currenciesRepository: CurrenciesRepository, dispatchers: CoroutineDispatcherProvider, ): ApplyTokenListSortingUseCase { - return ApplyTokenListSortingUseCase(tokensRepository, dispatchers) + return ApplyTokenListSortingUseCase(currenciesRepository, dispatchers) } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/di/domain/WalletsDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/WalletsDomainModule.kt index 9321638b94..92f1340058 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/WalletsDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/WalletsDomainModule.kt @@ -2,9 +2,7 @@ package com.tangem.tap.di.domain import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.legacy.WalletsStateHolder -import com.tangem.domain.wallets.usecase.GetExploreUrlUseCase -import com.tangem.domain.wallets.usecase.GetWalletsUseCase -import com.tangem.domain.wallets.usecase.SaveWalletUseCase +import com.tangem.domain.wallets.usecase.* import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -21,6 +19,12 @@ internal object WalletsDomainModule { return GetWalletsUseCase(walletsStateHolder = walletsStateHolder) } + @Provides + @ViewModelScoped + fun providesGetSelectedWalletUseCase(walletsStateHolder: WalletsStateHolder): GetSelectedWalletUseCase { + return GetSelectedWalletUseCase(walletsStateHolder = walletsStateHolder) + } + @Provides @ViewModelScoped fun providesSaveWalletUseCase(walletsStateHolder: WalletsStateHolder): SaveWalletUseCase { @@ -32,4 +36,16 @@ internal object WalletsDomainModule { fun providesGetExploreUrlUseCase(walletsManagersFacade: WalletManagersFacade): GetExploreUrlUseCase { return GetExploreUrlUseCase(walletsManagersFacade = walletsManagersFacade) } + + @Provides + @ViewModelScoped + fun providesUnlockWalletUseCase(walletsStateHolder: WalletsStateHolder): UnlockWalletsUseCase { + return UnlockWalletsUseCase(walletsStateHolder = walletsStateHolder) + } + + @Provides + @ViewModelScoped + fun providesSelectWalletUseCase(walletsStateHolder: WalletsStateHolder): SelectWalletUseCase { + return SelectWalletUseCase(walletsStateHolder = walletsStateHolder) + } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/TangemSdkManager.kt b/app/src/main/java/com/tangem/tap/domain/TangemSdkManager.kt index 5bac8dc2b2..51e0adb686 100644 --- a/app/src/main/java/com/tangem/tap/domain/TangemSdkManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/TangemSdkManager.kt @@ -16,6 +16,7 @@ import com.tangem.crypto.bip39.DefaultMnemonic import com.tangem.crypto.hdWallet.DerivationPath import com.tangem.domain.card.repository.CardSdkConfigRepository import com.tangem.domain.common.util.cardTypesResolver +import com.tangem.domain.common.util.derivationStyleProvider import com.tangem.domain.models.scan.CardDTO import com.tangem.domain.models.scan.ScanResponse import com.tangem.operations.ScanTask @@ -80,7 +81,10 @@ class TangemSdkManager(private val cardSdkConfigRepository: CardSdkConfigReposit suspend fun createProductWallet(scanResponse: ScanResponse): CompletionResult { return runTaskAsync( - CreateProductWalletTask(scanResponse.cardTypesResolver), + CreateProductWalletTask( + cardTypesResolver = scanResponse.cardTypesResolver, + derivationStyleProvider = scanResponse.derivationStyleProvider, + ), scanResponse.card.cardId, Message(resources.getString(R.string.initial_message_create_wallet_body)), ) @@ -90,15 +94,20 @@ class TangemSdkManager(private val cardSdkConfigRepository: CardSdkConfigReposit scanResponse: ScanResponse, mnemonic: String, ): CompletionResult { - return when (val seedResult = DefaultMnemonic(mnemonic, tangemSdk.wordlist).generateSeed()) { - is CompletionResult.Success -> runTaskAsync( - CreateProductWalletTask(scanResponse.cardTypesResolver, seedResult.data), - scanResponse.card.cardId, - Message(resources.getString(R.string.initial_message_create_wallet_body)), - ) - - is CompletionResult.Failure -> CompletionResult.Failure(seedResult.error) + val mnemonic = try { + DefaultMnemonic(mnemonic, tangemSdk.wordlist) + } catch (e: TangemSdkError.MnemonicException) { + return CompletionResult.Failure(e) } + return runTaskAsync( + CreateProductWalletTask( + scanResponse.cardTypesResolver, + derivationStyleProvider = scanResponse.derivationStyleProvider, + mnemonic, + ), + scanResponse.card.cardId, + Message(resources.getString(R.string.initial_message_create_wallet_body)), + ) } private fun sendScanResultsToAnalytics(result: CompletionResult) { @@ -246,12 +255,13 @@ class TangemSdkManager(private val cardSdkConfigRepository: CardSdkConfigReposit } companion object { + @Deprecated("Use [DefaultCardSdkProvider] instead") val config = Config( linkedTerminal = true, allowUntrustedCards = true, filter = CardFilter( allowedCardTypes = FirmwareVersion.FirmwareType.values().toList(), - maxFirmwareVersion = FirmwareVersion(major = 6, minor = 21), + maxFirmwareVersion = FirmwareVersion(major = 6, minor = 33), ), ) } diff --git a/app/src/main/java/com/tangem/tap/domain/TapErrors.kt b/app/src/main/java/com/tangem/tap/domain/TapErrors.kt index b986f9f247..8ac459bf2b 100644 --- a/app/src/main/java/com/tangem/tap/domain/TapErrors.kt +++ b/app/src/main/java/com/tangem/tap/domain/TapErrors.kt @@ -26,10 +26,8 @@ sealed class TapError( val stateError: String, ) : TapError(R.string.common_custom_string, listOf("Unsupported state: $stateError")) - object ScanCardError : TapError(R.string.scan_card_error) object UnknownBlockchain : TapError(R.string.wallet_error_unsupported_blockchain_subtitle) object NoInternetConnection : TapError(R.string.wallet_notification_no_internet) - object BlockchainInternalError : TapError(R.string.send_error_blockchain_internal) object AmountExceedsBalance : TapError(R.string.send_validation_amount_exceeds_balance) data class AmountLowerExistentialDeposit( override val args: List, @@ -43,7 +41,7 @@ sealed class TapError( object DustChange : TapError(R.string.send_error_dust_change) sealed class WalletManager { - object CreationError : CustomError("Can't create wallet manager") + object CreationError : CustomError(customMessage = "Can't create wallet manager") class NoAccountError(amountToCreateAccount: String) : CustomError(amountToCreateAccount) class InternalError(message: String) : CustomError(message) object BlockchainIsUnreachableTryLater : TapError(R.string.wallet_balance_blockchain_unreachable_try_later) diff --git a/app/src/main/java/com/tangem/tap/domain/TapWalletManager.kt b/app/src/main/java/com/tangem/tap/domain/TapWalletManager.kt index 23ee5bb963..6d23a8b78a 100644 --- a/app/src/main/java/com/tangem/tap/domain/TapWalletManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/TapWalletManager.kt @@ -8,6 +8,7 @@ import com.tangem.common.doOnFailure import com.tangem.common.doOnSuccess import com.tangem.core.analytics.Analytics import com.tangem.datasource.config.ConfigManager +import com.tangem.domain.common.extensions.toNetworkId import com.tangem.domain.common.extensions.withMainContext import com.tangem.domain.common.util.cardTypesResolver import com.tangem.domain.models.scan.ScanResponse @@ -20,6 +21,8 @@ import com.tangem.tap.common.extensions.dispatchWithMain import com.tangem.tap.common.extensions.setContext import com.tangem.tap.common.redux.global.GlobalAction import com.tangem.tap.domain.walletStores.WalletStoresError +import com.tangem.tap.domain.walletconnect2.domain.WalletConnectInteractor +import com.tangem.tap.domain.walletconnect2.domain.models.Account import com.tangem.tap.features.details.redux.walletconnect.WalletConnectAction import com.tangem.tap.features.disclaimer.createDisclaimer import com.tangem.tap.features.disclaimer.redux.DisclaimerAction @@ -92,7 +95,8 @@ class TapWalletManager( null } scope.launch { - store.state.daggerGraphState.walletConnectInteractor?.startListening( + val wcInteractor = store.state.daggerGraphState.walletConnectInteractor ?: return@launch + wcInteractor.startListening( userWalletId = userWallet.walletId.stringValue, cardId = cardId, ) @@ -106,6 +110,9 @@ class TapWalletManager( store.dispatchOnMain(WalletAction.LoadData.Success) store.state.globalState.topUpController?.loadDataSuccess() store.dispatchWithMain(WalletAction.Warnings.CheckHashesCount.VerifyOnlineIfNeeded) + + val wcInteractor = store.state.daggerGraphState.walletConnectInteractor + wcInteractor?.setUserChains(getAccountsForWc(wcInteractor)) } .doOnFailure { error -> val errorAction = when (error) { @@ -135,6 +142,23 @@ class TapWalletManager( } } + private fun getAccountsForWc(wcInteractor: WalletConnectInteractor): List { + return store.state.walletState.walletManagers + .mapNotNull { + val wallet = it.wallet + val chainId = wcInteractor.blockchainHelper.networkIdToChainIdOrNull( + wallet.blockchain.toNetworkId(), + ) + chainId?.let { + Account( + chainId, + wallet.address, + wallet.publicKey.derivationPath?.rawPath, + ) + } + } + } + fun updateConfigManager(data: ScanResponse) { val configManager = store.state.globalState.configManager diff --git a/app/src/main/java/com/tangem/tap/domain/configurable/warningMessage/WarningMessagesManager.kt b/app/src/main/java/com/tangem/tap/domain/configurable/warningMessage/WarningMessagesManager.kt index 01bb883f69..183ba76207 100644 --- a/app/src/main/java/com/tangem/tap/domain/configurable/warningMessage/WarningMessagesManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/configurable/warningMessage/WarningMessagesManager.kt @@ -1,7 +1,7 @@ package com.tangem.tap.domain.configurable.warningMessage import com.tangem.blockchain.common.Blockchain -import com.tangem.utils.extensions.removeByReplace +import com.tangem.utils.extensions.removeBy import com.tangem.wallet.R import java.util.concurrent.CopyOnWriteArrayList @@ -44,12 +44,12 @@ class WarningMessagesManager { } fun removeWarnings(origin: WarningMessage.Origin) { - warningsList.removeByReplace { it.origin == origin } + warningsList.removeBy { it.origin == origin } sortByPriority() } fun removeWarnings(messageRes: Int) { - warningsList.removeByReplace { it.messageResId == messageRes } + warningsList.removeBy { it.messageResId == messageRes } } fun containsWarning(warning: WarningMessage) = warning in warningsList diff --git a/app/src/main/java/com/tangem/tap/domain/model/builders/WalletStoreBuilder.kt b/app/src/main/java/com/tangem/tap/domain/model/builders/WalletStoreBuilder.kt index 81532e5bb9..7bd2f77fe1 100644 --- a/app/src/main/java/com/tangem/tap/domain/model/builders/WalletStoreBuilder.kt +++ b/app/src/main/java/com/tangem/tap/domain/model/builders/WalletStoreBuilder.kt @@ -1,11 +1,15 @@ package com.tangem.tap.domain.model.builders import com.tangem.blockchain.blockchains.polkadot.ExistentialDepositProvider -import com.tangem.blockchain.common.* +import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchain.common.Token +import com.tangem.blockchain.common.Wallet +import com.tangem.blockchain.common.WalletManager +import com.tangem.blockchain.common.derivation.DerivationStyle import com.tangem.crypto.hdWallet.DerivationPath import com.tangem.domain.common.BlockchainNetwork -import com.tangem.domain.common.TapWorkarounds.derivationStyle import com.tangem.domain.common.util.cardTypesResolver +import com.tangem.domain.common.util.derivationStyleProvider import com.tangem.domain.wallets.models.UserWallet import com.tangem.tap.domain.model.WalletDataModel import com.tangem.tap.domain.model.WalletStoreModel @@ -47,7 +51,7 @@ private class BlockchainNetworkWalletStoreBuilderImpl( } override fun build(): WalletStoreModel { - val cardDerivationStyle = userWallet.scanResponse.card.derivationStyle + val cardDerivationStyle = userWallet.scanResponse.derivationStyleProvider.getDerivationStyle() val blockchainWalletData = blockchainNetwork.getBlockchainWalletData(walletManager, cardDerivationStyle) val tokensWalletsData = blockchainNetwork.getTokensWalletsData( walletManager = walletManager, diff --git a/app/src/main/java/com/tangem/tap/domain/tasks/product/CreateProductWalletTask.kt b/app/src/main/java/com/tangem/tap/domain/tasks/product/CreateProductWalletTask.kt index d23e9fde5c..78d1ce464f 100644 --- a/app/src/main/java/com/tangem/tap/domain/tasks/product/CreateProductWalletTask.kt +++ b/app/src/main/java/com/tangem/tap/domain/tasks/product/CreateProductWalletTask.kt @@ -11,10 +11,12 @@ import com.tangem.common.extensions.ByteArrayKey import com.tangem.common.extensions.guard import com.tangem.common.extensions.toMapKey import com.tangem.common.map +import com.tangem.crypto.bip39.Mnemonic import com.tangem.crypto.hdWallet.DerivationPath import com.tangem.domain.common.CardTypesResolver -import com.tangem.domain.common.TapWorkarounds.derivationStyle +import com.tangem.domain.common.DerivationStyleProvider import com.tangem.domain.common.TapWorkarounds.isTestCard +import com.tangem.domain.common.configs.CardConfig import com.tangem.domain.models.scan.CardDTO import com.tangem.domain.models.scan.KeyWalletPublicKey import com.tangem.operations.CommandResponse @@ -56,7 +58,8 @@ private data class CreateWalletResponse( class CreateProductWalletTask( private val cardTypesResolver: CardTypesResolver, - private val seed: ByteArray? = null, + private val derivationStyleProvider: DerivationStyleProvider, + private val mnemonic: Mnemonic? = null, ) : CardSessionRunnable { override val allowsRequestAccessCodeFromRepository: Boolean = false @@ -76,7 +79,7 @@ class CreateProductWalletTask( cardTypesResolver.isTangemTwins() -> throw UnsupportedOperationException("Use the TwinCardsManager to create a wallet") - else -> CreateWalletTangemWallet(seed) + else -> CreateWalletTangemWallet(mnemonic, derivationStyleProvider) } commandProcessor.proceed(cardDto, session) { when (it) { @@ -131,8 +134,12 @@ private class CreateWalletTangemNote(private val cardTypesResolver: CardTypesRes } } +/** + * Uses for multiWallet 1st and 2nd + */ private class CreateWalletTangemWallet( - private val seed: ByteArray?, + private val mnemonic: Mnemonic?, + private val derivationStyleProvider: DerivationStyleProvider, ) : ProductCommandProcessor { private var primaryCard: PrimaryCard? = null @@ -142,8 +149,9 @@ private class CreateWalletTangemWallet( session: CardSession, callback: (result: CompletionResult) -> Unit, ) { + val config = CardConfig.createConfig(card) val walletsOnCard = card.wallets.map { it.curve }.toSet() - val curves = card.supportedCurves.intersect(CURVES_FOR_WALLETS).subtract(walletsOnCard).toList() + val curves = card.supportedCurves.intersect(config.mandatoryCurves.toSet()).subtract(walletsOnCard).toList() if (curves.isEmpty()) { val createWalletResponses = card.wallets.map { wallet -> @@ -152,8 +160,7 @@ private class CreateWalletTangemWallet( proceedWithCreatedWallets(card, createWalletResponses, session, callback) return } - - CreateWalletsTask(curves, seed).run(session) { result -> + CreateWalletsTask(curves, mnemonic).run(session) { result -> when (result) { is CompletionResult.Success -> { proceedWithCreatedWallets( @@ -242,9 +249,9 @@ private class CreateWalletTangemWallet( val blockchainsForCurve = getBlockchains(response.cardId, card).filter { it.getSupportedCurves().contains(response.wallet.curve) } - val derivationPaths = blockchainsForCurve.mapNotNull { + val derivationPaths = blockchainsForCurve.mapNotNull { blockchain -> isBlockchainsForCurvesExist = true - it.derivationPath(card.derivationStyle) + blockchain.derivationPath(derivationStyleProvider.getDerivationStyle()) } if (derivationPaths.isNotEmpty()) { map[response.wallet.publicKey.toMapKey()] = derivationPaths diff --git a/app/src/main/java/com/tangem/tap/domain/tasks/product/CreateWalletsTask.kt b/app/src/main/java/com/tangem/tap/domain/tasks/product/CreateWalletsTask.kt index 23f0986e34..2c75d8da36 100644 --- a/app/src/main/java/com/tangem/tap/domain/tasks/product/CreateWalletsTask.kt +++ b/app/src/main/java/com/tangem/tap/domain/tasks/product/CreateWalletsTask.kt @@ -5,6 +5,8 @@ import com.tangem.common.card.EllipticCurve import com.tangem.common.core.CardSession import com.tangem.common.core.CardSessionRunnable import com.tangem.common.core.TangemSdkError +import com.tangem.crypto.bip39.Mnemonic +import com.tangem.crypto.hdWallet.masterkey.AnyMasterKeyFactory import com.tangem.operations.CommandResponse import com.tangem.operations.wallet.CreateWalletResponse import com.tangem.operations.wallet.CreateWalletTask @@ -18,7 +20,7 @@ class CreateWalletsResponse( class CreateWalletsTask( private val curves: List, - private val seed: ByteArray? = null, + private val mnemonic: Mnemonic? = null, ) : CardSessionRunnable { private val createdWalletsResponses = mutableListOf() @@ -38,7 +40,10 @@ class CreateWalletsTask( session: CardSession, callback: (result: CompletionResult) -> Unit, ) { - CreateWalletTask(curve, seed).run(session) { result -> + val extendedPrivateKey = mnemonic?.let { + AnyMasterKeyFactory(mnemonic = it, passphrase = "").makeMasterKey(curve) + } + CreateWalletTask(curve, extendedPrivateKey).run(session) { result -> when (result) { is CompletionResult.Success -> { createdWalletsResponses.add(result.data) diff --git a/app/src/main/java/com/tangem/tap/domain/tasks/product/ScanProductTask.kt b/app/src/main/java/com/tangem/tap/domain/tasks/product/ScanProductTask.kt index 5bef94e017..d04cb9a62e 100644 --- a/app/src/main/java/com/tangem/tap/domain/tasks/product/ScanProductTask.kt +++ b/app/src/main/java/com/tangem/tap/domain/tasks/product/ScanProductTask.kt @@ -14,13 +14,15 @@ import com.tangem.common.tlv.TlvDecoder import com.tangem.crypto.CryptoUtils import com.tangem.crypto.hdWallet.DerivationPath import com.tangem.domain.common.BlockchainNetwork +import com.tangem.domain.common.DerivationStyleProvider import com.tangem.domain.common.TapWorkarounds.isExcluded import com.tangem.domain.common.TapWorkarounds.isNotSupportedInThatRelease import com.tangem.domain.common.TapWorkarounds.isStart2Coin import com.tangem.domain.common.TapWorkarounds.isTangemTwins import com.tangem.domain.common.TapWorkarounds.useOldStyleDerivation import com.tangem.domain.common.TwinsHelper -import com.tangem.domain.common.extensions.getPrimaryCurve +import com.tangem.domain.common.configs.CardConfig +import com.tangem.domain.common.util.derivationStyleProvider import com.tangem.domain.models.scan.CardDTO import com.tangem.domain.models.scan.ProductType import com.tangem.domain.models.scan.ScanResponse @@ -51,7 +53,7 @@ class ScanProductTask( } val cardDto = CardDTO(card) - val error = getErrorIfExcludedCard(cardDto, card) + val error = getErrorIfExcludedCard(cardDto) if (error != null) { callback(CompletionResult.Failure(error)) return @@ -81,11 +83,9 @@ class ScanProductTask( } } - private fun getErrorIfExcludedCard(cardDto: CardDTO, card: Card): TangemError? { + private fun getErrorIfExcludedCard(cardDto: CardDTO): TangemError? { if (cardDto.isExcluded) return TapSdkError.CardForDifferentApp if (cardDto.isNotSupportedInThatRelease) return TapSdkError.CardNotSupportedByRelease - // todo check isImported to prevent using old app with imported wallet, remove before wallet 2.0 enabled ([REDACTED_TASK_KEY]) - if (card.wallets.any { it.isImported }) return TapSdkError.CardNotSupportedByRelease return null } } @@ -209,32 +209,24 @@ private class ScanWalletProcessor( callback: (result: CompletionResult) -> Unit, ) { val productType = ProductType.Wallet + val config = CardConfig.createConfig(card) scope.launch { - val derivations = collectDerivations(card) + val scanResponse = ScanResponse( + card = card, + productType = productType, + walletData = session.environment.walletData, + primaryCard = primaryCard, + ) + val derivations = collectDerivations(card, config, scanResponse.derivationStyleProvider) if (derivations.isEmpty() || !card.settings.isHDWalletAllowed) { - callback( - CompletionResult.Success( - ScanResponse( - card = card, - productType = productType, - walletData = session.environment.walletData, - primaryCard = primaryCard, - ), - ), - ) + callback(CompletionResult.Success(scanResponse)) return@launch } DeriveMultipleWalletPublicKeysTask(derivations).run(session) { result -> when (result) { is CompletionResult.Success -> { - val response = ScanResponse( - card = card, - productType = productType, - walletData = session.environment.walletData, - derivedKeys = result.data.entries, - primaryCard = primaryCard, - ) + val response = scanResponse.copy(derivedKeys = result.data.entries) callback(CompletionResult.Success(response)) } is CompletionResult.Failure -> callback(CompletionResult.Failure(result.error)) @@ -243,7 +235,10 @@ private class ScanWalletProcessor( } } - private suspend fun getBlockchainsToDerive(card: CardDTO): List { + private suspend fun getBlockchainsToDerive( + card: CardDTO, + derivationStyleProvider: DerivationStyleProvider, + ): List { val userTokensRepository = userTokensRepository ?: return emptyList() val blockchainsToDerive = userTokensRepository.loadBlockchainsToDerive(card) .toMutableList() @@ -251,11 +246,11 @@ private class ScanWalletProcessor( mutableListOf( BlockchainNetwork( blockchain = Blockchain.Bitcoin, - card = card, + derivationStyleProvider = derivationStyleProvider, ), BlockchainNetwork( blockchain = Blockchain.Ethereum, - card = card, + derivationStyleProvider = derivationStyleProvider, ), ) } @@ -265,11 +260,11 @@ private class ScanWalletProcessor( listOf( BlockchainNetwork( blockchain = Blockchain.Ethereum, - card = card, + derivationStyleProvider = derivationStyleProvider, ), BlockchainNetwork( blockchain = Blockchain.EthereumTestnet, - card = card, + derivationStyleProvider = derivationStyleProvider, ), ), ) @@ -279,7 +274,7 @@ private class ScanWalletProcessor( additionalBlockchainsToDerive.map { BlockchainNetwork( blockchain = it, - card = card, + derivationStyleProvider = derivationStyleProvider, ) }, ) @@ -295,7 +290,7 @@ private class ScanWalletProcessor( ).map { BlockchainNetwork( blockchain = it, - card = card, + derivationStyleProvider = derivationStyleProvider, ) }, ) @@ -303,12 +298,16 @@ private class ScanWalletProcessor( return blockchainsToDerive.distinct() } - private suspend fun collectDerivations(card: CardDTO): Map> { - val blockchains = getBlockchainsToDerive(card) + private suspend fun collectDerivations( + card: CardDTO, + config: CardConfig, + derivationStyleProvider: DerivationStyleProvider, + ): Map> { + val blockchains = getBlockchainsToDerive(card, derivationStyleProvider) val derivations = mutableMapOf>() blockchains.forEach { blockchain -> - val curve = blockchain.blockchain.getPrimaryCurve() + val curve = config.primaryCurve(blockchain.blockchain) val wallet = card.wallets.firstOrNull { it.curve == curve } ?: return@forEach if (wallet.chainCode == null) return@forEach diff --git a/app/src/main/java/com/tangem/tap/domain/tokens/CurrenciesRepository.kt b/app/src/main/java/com/tangem/tap/domain/tokens/CurrenciesRepository.kt deleted file mode 100644 index 22baf587ab..0000000000 --- a/app/src/main/java/com/tangem/tap/domain/tokens/CurrenciesRepository.kt +++ /dev/null @@ -1,27 +0,0 @@ -package com.tangem.tap.domain.tokens - -import com.tangem.blockchain.common.Blockchain -import com.tangem.common.card.FirmwareVersion -import com.tangem.domain.models.scan.CardDTO - -object CurrenciesRepository { - fun getBlockchains(cardFirmware: CardDTO.FirmwareVersion, isTestNet: Boolean = false): List { - val blockchains = if (cardFirmware < FirmwareVersion.MultiWalletAvailable) { - Blockchain.secp256k1Blockchains(isTestNet) - } else { - Blockchain.secp256k1Blockchains(isTestNet) + Blockchain.ed25519OnlyBlockchains(isTestNet) - } - return excludeUnsupportedBlockchains(blockchains) - } - - // Use this list to temporarily exclude a blockchain from the list of tokens. - private fun excludeUnsupportedBlockchains(blockchains: List): List { - return blockchains.toMutableList().apply { - removeAll( - listOf( -// Any blockchain - ), - ) - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/tokens/UserTokensRepository.kt b/app/src/main/java/com/tangem/tap/domain/tokens/UserTokensRepository.kt index c3ed94f70c..1304588781 100644 --- a/app/src/main/java/com/tangem/tap/domain/tokens/UserTokensRepository.kt +++ b/app/src/main/java/com/tangem/tap/domain/tokens/UserTokensRepository.kt @@ -1,7 +1,7 @@ package com.tangem.tap.domain.tokens import android.content.Context -import com.tangem.blockchain.common.DerivationStyle +import com.tangem.blockchain.common.derivation.DerivationStyle import com.tangem.common.core.TangemSdkError import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.api.tangemTech.TangemTechService diff --git a/app/src/main/java/com/tangem/tap/domain/twins/CreateSecondTwinWalletTask.kt b/app/src/main/java/com/tangem/tap/domain/twins/CreateSecondTwinWalletTask.kt index 628c041112..d91c57c086 100644 --- a/app/src/main/java/com/tangem/tap/domain/twins/CreateSecondTwinWalletTask.kt +++ b/app/src/main/java/com/tangem/tap/domain/twins/CreateSecondTwinWalletTask.kt @@ -34,6 +34,11 @@ class CreateSecondTwinWalletTask( return } + if (!TwinsHelper.isTwinsCompatible(firstCardId, card.cardId)) { + callback(CompletionResult.Failure(IncompatibleTwinCard)) + return + } + session.setMessage(preparingMessage) PurgeWalletCommand(publicKey).run(session) { response -> when (response) { diff --git a/app/src/main/java/com/tangem/tap/domain/twins/IncompatibleTwinCard.kt b/app/src/main/java/com/tangem/tap/domain/twins/IncompatibleTwinCard.kt new file mode 100644 index 0000000000..10afdb020a --- /dev/null +++ b/app/src/main/java/com/tangem/tap/domain/twins/IncompatibleTwinCard.kt @@ -0,0 +1,12 @@ +package com.tangem.tap.domain.twins + +import com.tangem.common.core.TangemError +import com.tangem.tap.tangemSdkManager +import com.tangem.wallet.R + +object IncompatibleTwinCard : TangemError(code = 50005) { + override var customMessage: String = tangemSdkManager.getString( + R.string.twin_error_wrong_twin, + ) + override val messageResId: Int? = null +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/userWalletList/implementation/BiometricUserWalletsListManager.kt b/app/src/main/java/com/tangem/tap/domain/userWalletList/implementation/BiometricUserWalletsListManager.kt index f28f3b06ee..76a94f34bd 100644 --- a/app/src/main/java/com/tangem/tap/domain/userWalletList/implementation/BiometricUserWalletsListManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/userWalletList/implementation/BiometricUserWalletsListManager.kt @@ -2,10 +2,10 @@ package com.tangem.tap.domain.userWalletList.implementation import com.tangem.common.* import com.tangem.common.extensions.guard +import com.tangem.domain.wallets.legacy.UserWalletsListError import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWalletId -import com.tangem.tap.domain.userWalletList.UserWalletsListError import com.tangem.tap.domain.userWalletList.model.UserWalletEncryptionKey import com.tangem.tap.domain.userWalletList.repository.SelectedUserWalletRepository import com.tangem.tap.domain.userWalletList.repository.UserWalletsKeysRepository diff --git a/app/src/main/java/com/tangem/tap/domain/userWalletList/implementation/RuntimeUserWalletsListManager.kt b/app/src/main/java/com/tangem/tap/domain/userWalletList/implementation/RuntimeUserWalletsListManager.kt index d1cd26b6ff..eedc68771b 100644 --- a/app/src/main/java/com/tangem/tap/domain/userWalletList/implementation/RuntimeUserWalletsListManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/userWalletList/implementation/RuntimeUserWalletsListManager.kt @@ -2,10 +2,10 @@ package com.tangem.tap.domain.userWalletList.implementation import com.tangem.common.CompletionResult import com.tangem.common.catching +import com.tangem.domain.wallets.legacy.UserWalletsListError import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWalletId -import com.tangem.tap.domain.userWalletList.UserWalletsListError import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.* diff --git a/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/implementation/BiometricUserWalletsKeysRepository.kt b/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/implementation/BiometricUserWalletsKeysRepository.kt index b8c9ce2bc5..0d4dd5d1c5 100644 --- a/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/implementation/BiometricUserWalletsKeysRepository.kt +++ b/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/implementation/BiometricUserWalletsKeysRepository.kt @@ -8,8 +8,8 @@ import com.tangem.common.biometric.BiometricManager import com.tangem.common.biometric.BiometricStorage import com.tangem.common.core.TangemSdkError import com.tangem.common.services.secure.SecureStorage +import com.tangem.domain.wallets.legacy.UserWalletsListError import com.tangem.domain.wallets.models.UserWalletId -import com.tangem.tap.domain.userWalletList.UserWalletsListError import com.tangem.tap.domain.userWalletList.model.UserWalletEncryptionKey import com.tangem.tap.domain.userWalletList.repository.UserWalletsKeysRepository import kotlinx.coroutines.Dispatchers diff --git a/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/implementation/DefaultUserWalletsPublicInformationRepository.kt b/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/implementation/DefaultUserWalletsPublicInformationRepository.kt index 4ab48a80a9..1f64334fd7 100644 --- a/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/implementation/DefaultUserWalletsPublicInformationRepository.kt +++ b/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/implementation/DefaultUserWalletsPublicInformationRepository.kt @@ -12,7 +12,7 @@ import com.tangem.domain.wallets.models.UserWalletId import com.tangem.tap.domain.userWalletList.model.UserWalletPublicInformation import com.tangem.tap.domain.userWalletList.repository.UserWalletsPublicInformationRepository import com.tangem.tap.domain.userWalletList.utils.publicInformation -import com.tangem.utils.extensions.plusOrReplace +import com.tangem.utils.extensions.addOrReplace import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext @@ -29,7 +29,7 @@ internal class DefaultUserWalletsPublicInformationRepository( getAll() .flatMap { savedInformation -> val infoToSave = withContext(Dispatchers.Default) { - savedInformation.plusOrReplace(userWallet.publicInformation) { + savedInformation.addOrReplace(userWallet.publicInformation) { userWallet.walletId == it.walletId } } diff --git a/app/src/main/java/com/tangem/tap/domain/walletCurrencies/implementation/DefaultWalletCurrenciesManager.kt b/app/src/main/java/com/tangem/tap/domain/walletCurrencies/implementation/DefaultWalletCurrenciesManager.kt index 07e8b8ccf6..db1f9347dc 100644 --- a/app/src/main/java/com/tangem/tap/domain/walletCurrencies/implementation/DefaultWalletCurrenciesManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/walletCurrencies/implementation/DefaultWalletCurrenciesManager.kt @@ -1,9 +1,10 @@ package com.tangem.tap.domain.walletCurrencies.implementation -import com.tangem.blockchain.common.DerivationStyle +import com.tangem.blockchain.common.derivation.DerivationStyle import com.tangem.common.* import com.tangem.domain.common.BlockchainNetwork -import com.tangem.domain.common.TapWorkarounds.derivationStyle +import com.tangem.domain.common.DerivationStyleProvider +import com.tangem.domain.common.util.derivationStyleProvider import com.tangem.domain.models.scan.CardDTO import com.tangem.domain.wallets.legacy.WalletManagersRepository import com.tangem.domain.wallets.models.UserWallet @@ -61,7 +62,9 @@ internal class DefaultWalletCurrenciesManager( } val card = userWallet.scanResponse.card - val currenciesToAddWithMissingBlockchains = currenciesToAdd.addMissingBlockchainsIfNeeded(card) + val currenciesToAddWithMissingBlockchains = currenciesToAdd.addMissingBlockchainsIfNeeded( + userWallet.scanResponse.derivationStyleProvider, + ) listeners.forEach { it.willCurrenciesAdd(userWallet, currenciesToAddWithMissingBlockchains) } updateWalletStores( @@ -167,13 +170,15 @@ internal class DefaultWalletCurrenciesManager( return networks } - private fun List.addMissingBlockchainsIfNeeded(card: CardDTO): List { + private fun List.addMissingBlockchainsIfNeeded( + derivationStyleProvider: DerivationStyleProvider, + ): List { if (this.isEmpty()) return this val currencies = this.asSequence() return currencies .groupBy { currency -> - findBlockchainCurrency(currency, currencies, card.derivationStyle) + findBlockchainCurrency(currency, currencies, derivationStyleProvider.getDerivationStyle()) } .mapValues { (blockchainCurrency, blockchainCurrencies) -> findBlockchainTokens(blockchainCurrency, blockchainCurrencies) diff --git a/app/src/main/java/com/tangem/tap/domain/walletStores/repository/implementation/DefaultWalletAmountsRepository.kt b/app/src/main/java/com/tangem/tap/domain/walletStores/repository/implementation/DefaultWalletAmountsRepository.kt index 6fa441f106..0203abef4b 100644 --- a/app/src/main/java/com/tangem/tap/domain/walletStores/repository/implementation/DefaultWalletAmountsRepository.kt +++ b/app/src/main/java/com/tangem/tap/domain/walletStores/repository/implementation/DefaultWalletAmountsRepository.kt @@ -31,7 +31,7 @@ import com.tangem.tap.features.wallet.models.getPendingTransactions import com.tangem.tap.proxy.redux.DaggerGraphState import com.tangem.tap.store import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import com.tangem.utils.extensions.plusOrReplace +import com.tangem.utils.extensions.addOrReplace import kotlinx.coroutines.* import kotlinx.coroutines.flow.firstOrNull import timber.log.Timber @@ -429,7 +429,7 @@ internal class DefaultWalletAmountsRepository( withContext(Dispatchers.Default) { WalletManagerStorage.update { prevManagers -> val newManagersForUserWallet = prevManagers[userWalletId].orEmpty() - .plusOrReplace(walletManager) { + .addOrReplace(walletManager) { it.wallet.blockchain == walletManager.wallet.blockchain } diff --git a/app/src/main/java/com/tangem/tap/domain/walletStores/repository/implementation/DefaultWalletManagersRepository.kt b/app/src/main/java/com/tangem/tap/domain/walletStores/repository/implementation/DefaultWalletManagersRepository.kt index 2fbd1e0624..006bca6820 100644 --- a/app/src/main/java/com/tangem/tap/domain/walletStores/repository/implementation/DefaultWalletManagersRepository.kt +++ b/app/src/main/java/com/tangem/tap/domain/walletStores/repository/implementation/DefaultWalletManagersRepository.kt @@ -1,6 +1,7 @@ package com.tangem.tap.domain.walletStores.repository.implementation import com.tangem.blockchain.common.* +import com.tangem.blockchain.common.derivation.DerivationStyle import com.tangem.common.CompletionResult import com.tangem.common.catching import com.tangem.common.doOnSuccess diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect2/data/WalletConnectRepositoryImpl.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect2/data/WalletConnectRepositoryImpl.kt index 02e8105ee4..da78bbdf84 100644 --- a/app/src/main/java/com/tangem/tap/domain/walletconnect2/data/WalletConnectRepositoryImpl.kt +++ b/app/src/main/java/com/tangem/tap/domain/walletconnect2/data/WalletConnectRepositoryImpl.kt @@ -100,6 +100,28 @@ class WalletConnectRepositoryImpl @Inject constructor( Timber.d("sessionProposal: $sessionProposal") this@WalletConnectRepositoryImpl.sessionProposal = sessionProposal + val missingNetworks = findMissingNetworks( + namespaces = sessionProposal.requiredNamespaces, + userNamespaces = this@WalletConnectRepositoryImpl.userNamespaces ?: emptyMap(), + ) + + if (missingNetworks.isNotEmpty()) { + Timber.w("Not added blockchains: $missingNetworks") + scope.launch { + _events.emit( + WalletConnectEvents.SessionApprovalError( + WalletConnectError.ApprovalErrorMissingNetworks(missingNetworks.toList()), + ), + ) + } + return + } + + val optionalWithoutMissingNetworks = removeMissingNetworks( + namespaces = sessionProposal.optionalNamespaces, + userNamespaces = this@WalletConnectRepositoryImpl.userNamespaces ?: emptyMap(), + ) + scope.launch { _events.emit( WalletConnectEvents.SessionProposal( @@ -108,7 +130,7 @@ class WalletConnectRepositoryImpl @Inject constructor( sessionProposal.url, sessionProposal.icons, sessionProposal.requiredNamespaces.values.flatMap { it.chains ?: emptyList() }, - sessionProposal.optionalNamespaces.values.flatMap { it.chains ?: emptyList() }, + optionalWithoutMissingNetworks.toList(), ), ) } @@ -211,6 +233,10 @@ class WalletConnectRepositoryImpl @Inject constructor( } } + override fun setUserNamespaces(userNamespaces: Map>) { + this.userNamespaces = userNamespaces + } + override fun pair(uri: String) { Web3Wallet.pair(Wallet.Params.Pair(uri)) } @@ -220,23 +246,6 @@ class WalletConnectRepositoryImpl @Inject constructor( val sessionProposal: Wallet.Model.SessionProposal = requireNotNull(this.sessionProposal) - val missingNetworks = findMissingNetworks( - namespaces = sessionProposal.requiredNamespaces, - userNamespaces = userNamespaces, - ) - - if (missingNetworks.isNotEmpty()) { - Timber.e("Not added blockchains: $missingNetworks") - scope.launch { - _events.emit( - WalletConnectEvents.SessionApprovalError( - WalletConnectError.ApprovalErrorMissingNetworks(missingNetworks.toList()), - ), - ) - } - return - } - val userChains = userNamespaces.flatMap { namespace -> namespace.value.map { it.chainId to "${it.chainId}:${it.walletAddress}" } }.groupBy { pair -> pair.first } @@ -433,4 +442,13 @@ class WalletConnectRepositoryImpl @Inject constructor( val userChains = userNamespaces.flatMap { it.value.map { account -> account.chainId } } return requiredChains.subtract(userChains.toSet()) } + + private fun removeMissingNetworks( + namespaces: Map, + userNamespaces: Map>, + ): Collection { + val wcProvidedChains = namespaces.values.flatMap { it.chains ?: emptyList() } + val userChains = userNamespaces.flatMap { it.value.map { account -> account.chainId } } + return wcProvidedChains.intersect(userChains.toSet()) + } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WalletConnectInteractor.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WalletConnectInteractor.kt index 9f5ce79361..d95640dc2b 100644 --- a/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WalletConnectInteractor.kt +++ b/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WalletConnectInteractor.kt @@ -44,6 +44,15 @@ class WalletConnectInteractor( } } + fun setUserChains(accounts: List) { + val userNamespaces: Map> = accounts + .groupBy { account -> + blockchainHelper.getNamespaceFromFullChainIdOrNull(account.chainId) + ?.let { NetworkNamespace(it) } + }.filterNotNull() + walletConnectRepository.setUserNamespaces(userNamespaces) + } + private suspend fun subscribeToEvents() { events .onEach { wcEvent -> diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WalletConnectRepository.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WalletConnectRepository.kt index 93e772b0d2..33d62fca22 100644 --- a/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WalletConnectRepository.kt +++ b/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WalletConnectRepository.kt @@ -11,6 +11,8 @@ interface WalletConnectRepository { fun init(projectId: String) + fun setUserNamespaces(userNamespaces: Map>) + fun updateSessions() fun pair(uri: String) diff --git a/app/src/main/java/com/tangem/tap/features/customtoken/impl/data/DefaultCustomTokenRepository.kt b/app/src/main/java/com/tangem/tap/features/customtoken/impl/data/DefaultCustomTokenRepository.kt index 119872604d..b949793a7d 100644 --- a/app/src/main/java/com/tangem/tap/features/customtoken/impl/data/DefaultCustomTokenRepository.kt +++ b/app/src/main/java/com/tangem/tap/features/customtoken/impl/data/DefaultCustomTokenRepository.kt @@ -4,6 +4,7 @@ import com.tangem.blockchain.common.Blockchain import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.domain.common.extensions.supportedBlockchains import com.tangem.domain.common.extensions.toNetworkId +import com.tangem.domain.common.util.cardTypesResolver import com.tangem.tap.features.customtoken.impl.data.converters.FoundTokenConverter import com.tangem.tap.features.customtoken.impl.domain.CustomTokenRepository import com.tangem.tap.features.customtoken.impl.domain.models.FoundToken @@ -27,8 +28,9 @@ class DefaultCustomTokenRepository( ) : CustomTokenRepository { override suspend fun findToken(address: String, networkId: String?): FoundToken { - val supportedTokenNetworkIds = requireNotNull(reduxStateHolder.scanResponse?.card) - .supportedBlockchains() + val scanResponse = requireNotNull(reduxStateHolder.scanResponse) + val supportedTokenNetworkIds = requireNotNull(scanResponse.card) + .supportedBlockchains(scanResponse.cardTypesResolver) .filter(Blockchain::canHandleTokens) .map(Blockchain::toNetworkId) diff --git a/app/src/main/java/com/tangem/tap/features/customtoken/impl/domain/DefaultCustomTokenInteractor.kt b/app/src/main/java/com/tangem/tap/features/customtoken/impl/domain/DefaultCustomTokenInteractor.kt index a92ac13e11..1f38373d86 100644 --- a/app/src/main/java/com/tangem/tap/features/customtoken/impl/domain/DefaultCustomTokenInteractor.kt +++ b/app/src/main/java/com/tangem/tap/features/customtoken/impl/domain/DefaultCustomTokenInteractor.kt @@ -7,8 +7,9 @@ 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.TapWorkarounds.derivationStyle +import com.tangem.domain.common.configs.CardConfig import com.tangem.domain.common.extensions.toNetworkId +import com.tangem.domain.common.util.derivationStyleProvider import com.tangem.domain.common.util.hasDerivation import com.tangem.domain.features.addCustomToken.CustomCurrency import com.tangem.domain.models.scan.ScanResponse @@ -68,10 +69,11 @@ class DefaultCustomTokenInteractor( currencyList: List, onSuccess: suspend (ScanResponse) -> Unit, ) { - val derivationDataList = listOfNotNull( - getDerivations(EllipticCurve.Secp256k1, scanResponse, currencyList), - getDerivations(EllipticCurve.Ed25519, scanResponse, currencyList), - ) + val config = CardConfig.createConfig(scanResponse.card) + val derivationDataList = currencyList.mapNotNull { + val curve = config.primaryCurve(it.blockchain) + curve?.let { getDerivations(curve, scanResponse, currencyList) } + } val derivations = derivationDataList.associate(TokensMiddleware.DerivationData::derivations) if (derivations.isEmpty()) { @@ -114,7 +116,7 @@ class DefaultCustomTokenInteractor( val manageTokensCandidates = currencyList.map { it.blockchain }.distinct().filter { it.getSupportedCurves().contains(curve) }.mapNotNull { - it.derivationPath(scanResponse.card.derivationStyle) + it.derivationPath(scanResponse.derivationStyleProvider.getDerivationStyle()) } val customTokensCandidates = currencyList.filter { diff --git a/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/ui/components/AddCustomTokenFloatingButton.kt b/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/ui/components/AddCustomTokenFloatingButton.kt index 5011ca010c..558534f369 100644 --- a/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/ui/components/AddCustomTokenFloatingButton.kt +++ b/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/ui/components/AddCustomTokenFloatingButton.kt @@ -29,7 +29,7 @@ internal fun AddCustomTokenFloatingButton(model: AddCustomTokenFloatingButton, m .imePadding() .padding(horizontal = TangemTheme.dimens.spacing16) .fillMaxWidth(), - text = stringResource(id = R.string.common_add), + text = stringResource(id = R.string.custom_token_add_token), iconResId = R.drawable.ic_plus_24, enabled = model.isEnabled, onClick = model.onClick, diff --git a/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/viewmodels/AddCustomTokenViewModel.kt b/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/viewmodels/AddCustomTokenViewModel.kt index 297bf37f54..3b5e96c9e9 100644 --- a/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/viewmodels/AddCustomTokenViewModel.kt +++ b/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/viewmodels/AddCustomTokenViewModel.kt @@ -11,14 +11,15 @@ import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.tangem.blockchain.common.Blockchain -import com.tangem.blockchain.common.DerivationStyle import com.tangem.blockchain.common.Token +import com.tangem.blockchain.common.derivation.DerivationStyle import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.crypto.hdWallet.DerivationPath import com.tangem.crypto.hdWallet.HDWalletError import com.tangem.domain.AddCustomTokenError -import com.tangem.domain.common.TapWorkarounds.derivationStyle import com.tangem.domain.common.extensions.* +import com.tangem.domain.common.util.cardTypesResolver +import com.tangem.domain.common.util.derivationStyleProvider import com.tangem.domain.features.addCustomToken.CustomCurrency import com.tangem.tap.domain.model.WalletDataModel import com.tangem.tap.features.customtoken.impl.domain.CustomTokenInteractor @@ -206,10 +207,11 @@ internal class AddCustomTokenViewModel @Inject constructor( private fun getNetworkSelectorItems(): List { val defaultNetwork = createNetworkSelectorItem(blockchain = Blockchain.Unknown) + val scanResponse = reduxStateHolder.scanResponse return listOf(defaultNetwork) + Blockchain.values() .filter { blockchain -> - reduxStateHolder.scanResponse?.card?.supportedBlockchains()?.contains(blockchain) == true && - blockchain != Blockchain.Cardano + scanResponse?.card?.supportedBlockchains(scanResponse.cardTypesResolver) + ?.contains(blockchain) == true } .sortedBy(Blockchain::fullName) .map(::createNetworkSelectorItem) @@ -273,7 +275,7 @@ internal class AddCustomTokenViewModel @Inject constructor( ), ) + Blockchain.values() .filter { blockchain -> - blockchain.isSupportedInApp() && !blockchain.isTestnet() && blockchain != Blockchain.Cardano + blockchain.isSupportedInApp() && !blockchain.isTestnet() } .sortedBy(Blockchain::fullName) .map(::createDerivationPathSelectorAdditionalItem) @@ -405,7 +407,11 @@ internal class AddCustomTokenViewModel @Inject constructor( val isSupportedToken = if (!isNetworkSelected()) { true } else { - reduxStateHolder.scanResponse?.card?.canHandleToken(networkSelectorValue) ?: false + val scanResponse = reduxStateHolder.scanResponse + scanResponse?.card?.canHandleToken( + blockchain = networkSelectorValue, + cardTypesResolver = scanResponse.cardTypesResolver, + ) ?: false } return buildSet { @@ -439,8 +445,12 @@ internal class AddCustomTokenViewModel @Inject constructor( address = uiState.form.contractAddressInputField.value, blockchain = networkSelectorValue, ) - val isSupportedToken = reduxStateHolder.scanResponse?.card - ?.canHandleToken(networkSelectorValue) + val scanResponse = reduxStateHolder.scanResponse + val isSupportedToken = scanResponse?.card + ?.canHandleToken( + blockchain = networkSelectorValue, + cardTypesResolver = scanResponse.cardTypesResolver, + ) ?: false uiState.copySealed( @@ -599,7 +609,7 @@ internal class AddCustomTokenViewModel @Inject constructor( if (blockchain == null) return null val derivationStyle = if (!isDerivationPathSelected()) { - reduxStateHolder.scanResponse?.card?.derivationStyle + reduxStateHolder.scanResponse?.derivationStyleProvider?.getDerivationStyle() } else { DerivationStyle.LEGACY } 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 478d0cb1bd..5778b35562 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 @@ -15,6 +15,7 @@ import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.userwallets.UserWalletBuilder import com.tangem.domain.userwallets.UserWalletIdBuilder 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.Settings @@ -27,7 +28,6 @@ import com.tangem.tap.common.redux.AppState import com.tangem.tap.common.redux.global.GlobalAction import com.tangem.tap.domain.userWalletList.di.provideBiometricImplementation import com.tangem.tap.domain.userWalletList.di.provideRuntimeImplementation -import com.tangem.tap.domain.userWalletList.isLockedSync import com.tangem.tap.features.demo.DemoHelper import com.tangem.tap.features.onboarding.products.twins.redux.CreateTwinWalletMode import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsAction diff --git a/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectMiddleware.kt b/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectMiddleware.kt index c2276c9065..a436d55d8e 100644 --- a/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectMiddleware.kt @@ -1,18 +1,18 @@ package com.tangem.tap.features.details.redux.walletconnect import com.tangem.blockchain.common.Blockchain -import com.tangem.blockchain.common.DerivationStyle import com.tangem.blockchain.common.WalletManager +import com.tangem.blockchain.common.derivation.DerivationStyle import com.tangem.common.extensions.guard import com.tangem.core.navigation.AppScreen import com.tangem.core.navigation.NavigationAction import com.tangem.domain.common.BlockchainNetwork -import com.tangem.domain.common.TapWorkarounds.derivationStyle +import com.tangem.domain.common.DerivationStyleProvider import com.tangem.domain.common.extensions.fromNetworkId import com.tangem.domain.common.extensions.toNetworkId import com.tangem.domain.common.extensions.withMainContext import com.tangem.domain.common.util.cardTypesResolver -import com.tangem.domain.models.scan.CardDTO +import com.tangem.domain.common.util.derivationStyleProvider import com.tangem.domain.models.scan.ScanResponse import com.tangem.tap.common.extensions.dispatchOnMain import com.tangem.tap.common.redux.AppState @@ -357,9 +357,13 @@ class WalletConnectMiddleware { handleScanResponse(scanResponse = scanResponse, session = session, blockchain = blockchain) } - private fun getAvailableBlockchains(card: CardDTO, walletState: WalletState): List { + private fun getAvailableBlockchains( + derivationStyleProvider: DerivationStyleProvider, + walletState: WalletState, + ): List { return walletState.currencies.filter { - it.isBlockchain() && !it.isCustomCurrency(card.derivationStyle) && it.blockchain.isEvm() + it.isBlockchain() && + !it.isCustomCurrency(derivationStyleProvider.getDerivationStyle()) && it.blockchain.isEvm() }.map { it.blockchain } } @@ -390,7 +394,7 @@ class WalletConnectMiddleware { walletPublicKey = wallet.publicKey.seedKey, derivedPublicKey = derivedKey, derivationPath = wallet.publicKey.derivationPath, - derivationStyle = scanResponse.card.derivationStyle, + derivationStyle = scanResponse.derivationStyleProvider.getDerivationStyle(), blockchain = wallet.blockchain, ) @@ -403,7 +407,6 @@ class WalletConnectMiddleware { } private fun handleScanResponse(scanResponse: ScanResponse, session: WalletConnectSession, blockchain: Blockchain) { - val card = scanResponse.card if (!scanResponse.cardTypesResolver.isMultiwalletAllowed()) { store.dispatchOnMain(WalletConnectAction.UnsupportedCard) return @@ -415,7 +418,11 @@ class WalletConnectMiddleware { NewWcSessionData(session = updatedSession, scanResponse = scanResponse, blockchain = blockchain), ), ) - val blockchains = if (blockchain.isEvm()) getAvailableBlockchains(card, walletState) else emptyList() + val blockchains = if (blockchain.isEvm()) { + getAvailableBlockchains(scanResponse.derivationStyleProvider, walletState) + } else { + emptyList() + } store.dispatch( GlobalAction.ShowDialog( WalletConnectDialog.ApproveWcSession(session = updatedSession, networks = blockchains), @@ -433,8 +440,9 @@ class WalletConnectMiddleware { } else { blockchain } - val derivation = blockchainToMake.derivationPath(store.state.globalState.scanResponse?.card?.derivationStyle) - ?.rawPath + val derivation = blockchainToMake.derivationPath( + style = store.state.globalState.scanResponse?.derivationStyleProvider?.getDerivationStyle(), + )?.rawPath val blockchainNetwork = BlockchainNetwork( blockchain = blockchainToMake, derivationPath = derivation, diff --git a/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectState.kt b/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectState.kt index 135f597dc1..0446b1ca82 100644 --- a/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectState.kt +++ b/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectState.kt @@ -2,9 +2,9 @@ package com.tangem.tap.features.details.redux.walletconnect import com.squareup.moshi.JsonClass import com.tangem.blockchain.common.Blockchain -import com.tangem.blockchain.common.DerivationStyle import com.tangem.blockchain.common.TransactionData import com.tangem.blockchain.common.WalletManager +import com.tangem.blockchain.common.derivation.DerivationStyle import com.tangem.crypto.hdWallet.DerivationPath import com.tangem.domain.models.scan.ScanResponse import com.tangem.tap.common.redux.StateDialog diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/dialogs/ClipboardOrScanQrDialog.kt b/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/dialogs/ClipboardOrScanQrDialog.kt index a3f94a3cd9..ce79d75c14 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/dialogs/ClipboardOrScanQrDialog.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/dialogs/ClipboardOrScanQrDialog.kt @@ -12,7 +12,7 @@ import com.tangem.wallet.R object ClipboardOrScanQrDialog { fun create(wcUri: String, context: Context): AlertDialog { return AlertDialog.Builder(context).apply { - setTitle(context.getString(R.string.wallet_connect_title)) + setTitle(context.getString(R.string.common_select_action)) setMessage(context.getText(R.string.wallet_connect_clipboard_alert)) setPositiveButton(context.getText(R.string.wallet_connect_paste_from_clipboard)) { _, _ -> store.dispatch(WalletConnectAction.OpenSession(wcUri)) diff --git a/app/src/main/java/com/tangem/tap/features/home/compose/StoriesScreen.kt b/app/src/main/java/com/tangem/tap/features/home/compose/StoriesScreen.kt index fa3e832436..ca6b84babc 100644 --- a/app/src/main/java/com/tangem/tap/features/home/compose/StoriesScreen.kt +++ b/app/src/main/java/com/tangem/tap/features/home/compose/StoriesScreen.kt @@ -188,7 +188,7 @@ fun StoriesScreen( contentDescription = null, ) Text( - text = stringResource(id = R.string.search_tokens_title), + text = stringResource(id = R.string.common_search_tokens), fontWeight = FontWeight.Medium, fontSize = 16.sp, textAlign = TextAlign.Center, diff --git a/app/src/main/java/com/tangem/tap/features/home/compose/content/Content.kt b/app/src/main/java/com/tangem/tap/features/home/compose/content/Content.kt index d119f5b863..2c430b48db 100644 --- a/app/src/main/java/com/tangem/tap/features/home/compose/content/Content.kt +++ b/app/src/main/java/com/tangem/tap/features/home/compose/content/Content.kt @@ -176,6 +176,7 @@ private fun StoriesTitleText(text: String, isDarkBackground: Boolean) { .padding(start = 40.dp, end = 40.dp), text = text, fontSize = 32.sp, + lineHeight = 38.sp, fontWeight = FontWeight.SemiBold, color = if (isDarkBackground) Color.White else Color(0xFF090E13), textAlign = TextAlign.Center, @@ -198,6 +199,7 @@ private fun StoriesSubtitleText(subtitleText: AnnotatedString) { fontWeight = FontWeight.Normal, text = subtitleText, fontSize = 20.sp, + lineHeight = 26.sp, color = color, textAlign = TextAlign.Center, ) diff --git a/app/src/main/java/com/tangem/tap/features/home/compose/views/HomeButtons.kt b/app/src/main/java/com/tangem/tap/features/home/compose/views/HomeButtons.kt index 95049be464..ed7456d300 100644 --- a/app/src/main/java/com/tangem/tap/features/home/compose/views/HomeButtons.kt +++ b/app/src/main/java/com/tangem/tap/features/home/compose/views/HomeButtons.kt @@ -1,11 +1,6 @@ package com.tangem.tap.features.home.compose.views -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.RowScope -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.heightIn -import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.* import androidx.compose.material.Button import androidx.compose.material.ButtonDefaults import androidx.compose.material.CircularProgressIndicator @@ -51,7 +46,7 @@ fun HomeButtons( }, content = { Text( - text = stringResource(id = R.string.welcome_unlock_card), + text = stringResource(id = R.string.home_button_scan), fontWeight = FontWeight.Medium, fontSize = 16.sp, textAlign = TextAlign.Center, diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingHelper.kt b/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingHelper.kt index 78f8cc873e..baf5ed2fcf 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingHelper.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingHelper.kt @@ -6,7 +6,6 @@ import com.tangem.common.extensions.guard import com.tangem.core.analytics.Analytics import com.tangem.core.navigation.AppScreen import com.tangem.core.navigation.NavigationAction -import com.tangem.domain.common.TapWorkarounds.canSkipBackup import com.tangem.domain.common.util.cardTypesResolver import com.tangem.domain.common.util.twinsIsTwinned import com.tangem.domain.models.scan.ProductType @@ -38,10 +37,7 @@ object OnboardingHelper { } } - // TODO for Shiba disabled check wallet 2, and only check canSkipBackup, enable when release wallet 2 - // ([REDACTED_TASK_KEY]) - // response.cardTypesResolver.isWallet2() -> { - !response.card.canSkipBackup -> { + response.cardTypesResolver.isWallet2() -> { val emptyWallets = response.card.wallets.isEmpty() val activationInProgress = cardInfoStorage.isActivationInProgress(cardId) val backupNotActive = response.card.backupStatus?.isActive != true diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/note/OnboardingNoteFragment.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/note/OnboardingNoteFragment.kt index cf68e35486..6cdb83e4d7 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/note/OnboardingNoteFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/note/OnboardingNoteFragment.kt @@ -151,7 +151,7 @@ class OnboardingNoteFragment : BaseOnboardingFragment() { btnAlternativeAction.isVisible = false } - tvHeader.setText(R.string.onboarding_top_up_header) + tvHeader.setText(R.string.onboarding_topup_title) if (state.balanceNonCriticalError == null) { tvBody.setText(R.string.onboarding_top_up_body) } else { diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/otherCards/redux/OnboardingOtherCardsMiddleware.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/otherCards/redux/OnboardingOtherCardsMiddleware.kt index 00a9d94af5..9a76813cab 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/otherCards/redux/OnboardingOtherCardsMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/otherCards/redux/OnboardingOtherCardsMiddleware.kt @@ -6,6 +6,7 @@ import com.tangem.core.analytics.Analytics import com.tangem.domain.common.BlockchainNetwork import com.tangem.domain.common.extensions.withMainContext import com.tangem.domain.common.util.cardTypesResolver +import com.tangem.domain.common.util.derivationStyleProvider import com.tangem.tap.* import com.tangem.tap.common.analytics.events.Onboarding import com.tangem.tap.common.postUi @@ -92,7 +93,7 @@ private fun handleOtherCardsAction(action: Action) { val blockchainNetwork = BlockchainNetwork( blockchain = primaryBlockchain, - card = updatedCard, + derivationStyleProvider = updatedResponse.derivationStyleProvider, ) .updateTokens( listOfNotNull(primaryToken), @@ -102,11 +103,11 @@ private fun handleOtherCardsAction(action: Action) { listOf( BlockchainNetwork( blockchain = Blockchain.Bitcoin, - card = updatedCard, + derivationStyleProvider = updatedResponse.derivationStyleProvider, ), BlockchainNetwork( blockchain = Blockchain.Ethereum, - card = updatedCard, + derivationStyleProvider = updatedResponse.derivationStyleProvider, ), ) } diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/redux/TwinCardsMiddleware.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/redux/TwinCardsMiddleware.kt index 023e3137c4..f68fa7e806 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/redux/TwinCardsMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/redux/TwinCardsMiddleware.kt @@ -11,6 +11,7 @@ import com.tangem.domain.common.extensions.withMainContext import com.tangem.domain.common.util.twinsIsTwinned import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.userwallets.UserWalletIdBuilder +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.Onboarding @@ -21,7 +22,6 @@ 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.twins.TwinCardsManager -import com.tangem.tap.domain.userWalletList.isLockedSync import com.tangem.tap.features.home.RUSSIA_COUNTRY_CODE import com.tangem.tap.features.onboarding.OnboardingDialog import com.tangem.tap.features.onboarding.OnboardingHelper @@ -50,7 +50,7 @@ private val twinsWalletMiddleware: Middleware = { dispatch, state -> @Suppress("LongMethod", "ComplexMethod", "MagicNumber") private fun handle(action: Action, dispatch: DispatchFunction) { - val action = action as? TwinCardsAction ?: return + if (action !is TwinCardsAction) return val globalState = store.state.globalState val onboardingManager = globalState.onboardingState.onboardingManager 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 2fa0a10909..5163bf4284 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 @@ -350,7 +350,7 @@ class TwinsCardsFragment : BaseOnboardingFragment() { btnAlternativeAction.isVisible = false } - tvHeader.setText(R.string.onboarding_top_up_header) + tvHeader.setText(R.string.onboarding_topup_title) tvBody.setText(R.string.onboarding_top_up_body) btnRefreshBalanceWidget.changeState(state.walletBalance.state) diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletMiddleware.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletMiddleware.kt index 2ce6725c37..d1a927c252 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletMiddleware.kt @@ -12,6 +12,7 @@ import com.tangem.domain.common.BlockchainNetwork import com.tangem.domain.common.TapWorkarounds.canSkipBackup import com.tangem.domain.common.extensions.withMainContext import com.tangem.domain.common.util.cardTypesResolver +import com.tangem.domain.common.util.derivationStyleProvider import com.tangem.domain.models.scan.CardDTO import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.userwallets.Artwork @@ -125,7 +126,10 @@ private fun handleWalletAction(action: Action) { } else { listOf(Blockchain.Bitcoin, Blockchain.Ethereum) }.map { blockchain -> - BlockchainNetwork(blockchain, result.data.card) + BlockchainNetwork( + blockchain = blockchain, + derivationStyleProvider = updatedResponse.derivationStyleProvider, + ) } scope.launch { diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/OnboardingSeedPhraseStateHandler.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/OnboardingSeedPhraseStateHandler.kt index 8a26aa71f0..05eaf95d55 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/OnboardingSeedPhraseStateHandler.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/OnboardingSeedPhraseStateHandler.kt @@ -3,11 +3,13 @@ package com.tangem.tap.features.onboarding.products.wallet.ui import androidx.compose.runtime.collectAsState import com.tangem.feature.onboarding.api.OnboardingSeedPhrase import com.tangem.feature.onboarding.api.OnboardingSeedPhraseApi +import com.tangem.feature.onboarding.presentation.wallet2.viewmodel.SeedPhraseScreen import com.tangem.feature.onboarding.presentation.wallet2.viewmodel.SeedPhraseViewModel import com.tangem.tap.common.extensions.hide import com.tangem.tap.common.extensions.show import com.tangem.tap.features.onboarding.products.wallet.redux.OnboardingWalletState import com.tangem.tap.features.onboarding.products.wallet.redux.OnboardingWalletStep +import com.tangem.wallet.R /** [REDACTED_AUTHOR] @@ -44,11 +46,27 @@ internal class OnboardingSeedPhraseStateHandler( walletFragment.binding.onboardingWalletContainer.hide() walletFragment.bindingSeedPhrase.onboardingSeedPhraseContainer.show() walletFragment.bindingSeedPhrase.onboardingSeedPhraseContainer.setContent { + val subScreen = viewModel.currentScreen.collectAsState().value + setMainScreenToolbarTitle(walletFragment, subScreen) + onboardingSeedPhraseApi.ScreenContent( uiState = viewModel.uiState, - subScreen = viewModel.currentScreen.collectAsState().value, + subScreen = subScreen, progress = viewModel.progress.collectAsState(0).value.toFloat() / onboardingWalletMaxProgress, ) } } + + private fun setMainScreenToolbarTitle(walletFragment: OnboardingWalletFragment, subScreen: SeedPhraseScreen) { + val titleResId = when (subScreen) { + SeedPhraseScreen.Intro, + SeedPhraseScreen.AboutSeedPhrase, + SeedPhraseScreen.YourSeedPhrase, + SeedPhraseScreen.CheckSeedPhrase, + -> R.string.onboarding_create_wallet_header + SeedPhraseScreen.ImportSeedPhrase -> R.string.onboarding_seed_intro_button_import + } + + walletFragment.binding.toolbar.title = walletFragment.getString(titleResId) + } } \ No newline at end of file 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 269449599e..5b6afc478f 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 @@ -257,7 +257,7 @@ class OnboardingWalletFragment : prepareBackupView() tvHeader.text = getText(R.string.onboarding_title_scan_origin_card) tvBody.text = getString( - R.string.onboarding_subtitle_scan_origin_card, + R.string.onboarding_subtitle_scan_primary, ) with(layoutButtonsCommon) { diff --git a/app/src/main/java/com/tangem/tap/features/saveWallet/redux/SaveWalletMiddleware.kt b/app/src/main/java/com/tangem/tap/features/saveWallet/redux/SaveWalletMiddleware.kt index 8261f5bef3..5305d35dfb 100644 --- a/app/src/main/java/com/tangem/tap/features/saveWallet/redux/SaveWalletMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/saveWallet/redux/SaveWalletMiddleware.kt @@ -11,6 +11,7 @@ import com.tangem.core.navigation.AppScreen import com.tangem.core.navigation.NavigationAction import com.tangem.domain.userwallets.UserWalletBuilder import com.tangem.domain.wallets.legacy.UserWalletsListManager +import com.tangem.domain.wallets.legacy.isLockable import com.tangem.tap.* import com.tangem.tap.common.analytics.events.AnalyticsParam import com.tangem.tap.common.analytics.events.MainScreen @@ -20,7 +21,6 @@ import com.tangem.tap.common.extensions.dispatchWithMain import com.tangem.tap.common.redux.AppState import com.tangem.tap.common.redux.global.GlobalAction import com.tangem.tap.domain.userWalletList.di.provideBiometricImplementation -import com.tangem.tap.domain.userWalletList.isLockable import com.tangem.tap.features.wallet.redux.WalletAction import com.tangem.tap.proxy.redux.DaggerGraphState import kotlinx.coroutines.launch diff --git a/app/src/main/java/com/tangem/tap/features/shop/ui/ShopFragment.kt b/app/src/main/java/com/tangem/tap/features/shop/ui/ShopFragment.kt index 58b89b9901..bfb86a0f8a 100644 --- a/app/src/main/java/com/tangem/tap/features/shop/ui/ShopFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/shop/ui/ShopFragment.kt @@ -15,7 +15,6 @@ import com.tangem.core.navigation.NavigationAction import com.tangem.tap.common.GlobalLayoutStateHandler import com.tangem.tap.common.KeyboardObserver 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.features.BaseStoreFragment import com.tangem.tap.features.shop.domain.models.ProductState @@ -153,11 +152,12 @@ internal class ShopFragment : BaseStoreFragment(R.layout.fragment_shop), StoreSu animateProductSelection(state.selectedProduct) handlePriceState(state) handlePromoCodeState(state) - if (shopifyFeatureToggleManager.isDynamicSalesProductsEnabled) { - handleNotificationBlock(state) - } else { - handleOrderingDelayBlock(isVisible = state.isOrderingDelayBlockVisible) - } + // TODO: https://tangem.slack.com/archives/C01HARKDLQ0/p1691421861756069 + // if (shopifyFeatureToggleManager.isDynamicSalesProductsEnabled) { + // handleNotificationBlock(state) + // } else { + // handleOrderingDelayBlock(isVisible = state.isOrderingDelayBlockVisible) + // } handleButtonsState(state) } @@ -198,20 +198,20 @@ internal class ShopFragment : BaseStoreFragment(R.layout.fragment_shop), StoreSu pbPromoCode.show(state.promoCodeLoading) } - private fun handleOrderingDelayBlock(isVisible: Boolean) { - if (isVisible) binding.tvSoldOutDesc.show() else binding.tvSoldOutDesc.hide() - } - - private fun handleNotificationBlock(state: ShopState) { - if (isVisible) { - binding.tvSoldOutDesc.show() - getSelectedSalesProduct(state)?.notification?.let { notification -> - binding.tvSoldOutDesc.text = notification.description - } - } else { - binding.tvSoldOutDesc.hide() - } - } + // private fun handleOrderingDelayBlock(isVisible: Boolean) { + // if (isVisible) binding.tvSoldOutDesc.show() else binding.tvSoldOutDesc.hide() + // } + // + // private fun handleNotificationBlock(state: ShopState) { + // if (isVisible) { + // binding.tvSoldOutDesc.show() + // getSelectedSalesProduct(state)?.notification?.let { notification -> + // binding.tvSoldOutDesc.text = notification.description + // } + // } else { + // binding.tvSoldOutDesc.hide() + // } + // } private fun handleButtonsState(state: ShopState) = with(binding) { btnPayGooglePay.root.show(state.isGooglePayAvailable) diff --git a/app/src/main/java/com/tangem/tap/features/tokens/impl/data/TangemApiTokensPagingSource.kt b/app/src/main/java/com/tangem/tap/features/tokens/impl/data/TangemApiTokensPagingSource.kt index 2cae031963..fb04538ee2 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/impl/data/TangemApiTokensPagingSource.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/impl/data/TangemApiTokensPagingSource.kt @@ -6,6 +6,7 @@ import com.tangem.blockchain.common.Blockchain import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.domain.common.extensions.supportedBlockchains import com.tangem.domain.common.extensions.toNetworkId +import com.tangem.domain.common.util.cardTypesResolver import com.tangem.tap.features.tokens.impl.data.converters.CoinsResponseConverter import com.tangem.tap.features.tokens.impl.domain.models.Token import com.tangem.tap.proxy.AppStateHolder @@ -38,7 +39,8 @@ internal class TangemApiTokensPagingSource( val page = params.key ?: 0 return runCatching(dispatchers.io) { - val supportedBlockchains = reduxStateHolder.scanResponse?.card?.supportedBlockchains() + val scanResponse = reduxStateHolder.scanResponse + val supportedBlockchains = scanResponse?.card?.supportedBlockchains(scanResponse.cardTypesResolver) ?: Blockchain.values().toList() api.getCoins( 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 c7743263f4..c61c1a2f7e 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 @@ -2,18 +2,19 @@ package com.tangem.tap.features.tokens.impl.domain import androidx.paging.PagingData import com.tangem.blockchain.common.Blockchain -import com.tangem.blockchain.common.DerivationStyle +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.TapWorkarounds.derivationStyle +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.DELAY_SDK_DIALOG_CLOSE +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 @@ -24,10 +25,6 @@ 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 com.tangem.tap.store -import com.tangem.tap.tangemSdkManager -import com.tangem.tap.userWalletsListManager -import com.tangem.tap.walletCurrenciesManager import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow import timber.log.Timber @@ -51,11 +48,12 @@ internal class DefaultTokensListInteractor( 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(style = scanResponse.card.derivationStyle) + .toNonCustomTokensWithBlockchains(derivationStyle = derivationStyle) val currentBlockchains = store.state.tokensState.addedWallets - .toNonCustomBlockchains(derivationStyle = scanResponse.card.derivationStyle) + .toNonCustomBlockchains(derivationStyle = derivationStyle) val blockchainsToAdd = blockchains.filterNot(currentBlockchains::contains) val blockchainsToRemove = currentBlockchains.filterNot(blockchains::contains) @@ -73,18 +71,18 @@ internal class DefaultTokensListInteractor( remove( tokens = tokensToRemove, blockchains = blockchainsToRemove, - derivationStyle = scanResponse.card.derivationStyle, + derivationStyle = scanResponse.derivationStyleProvider.getDerivationStyle(), ) add(tokens = tokensToAdd, blockchains = blockchainsToAdd, scanResponse = scanResponse) } private fun List.toNonCustomTokensWithBlockchains( - style: DerivationStyle?, + derivationStyle: DerivationStyle?, ): List { return this.map(WalletDataModel::currency) .mapNotNull { currency -> - if (currency !is Currency.Token || currency.isCustomCurrency(style)) return@mapNotNull null + if (currency !is Currency.Token || currency.isCustomCurrency(derivationStyle)) return@mapNotNull null TokenWithBlockchain(token = currency.token, blockchain = currency.blockchain) } .distinct() @@ -123,7 +121,7 @@ internal class DefaultTokensListInteractor( val currenciesToAdd = convertToCurrencies( tokens = tokens, blockchains = blockchains, - derivationStyle = scanResponse.card.derivationStyle, + derivationStyle = scanResponse.derivationStyleProvider.getDerivationStyle(), ) // TODO("[REDACTED_TASK_KEY] use DerivationManager") @@ -136,10 +134,11 @@ internal class DefaultTokensListInteractor( } private suspend fun deriveMissingBlockchains(scanResponse: ScanResponse, currencies: List) { - val derivations = listOfNotNull( - getDerivations(EllipticCurve.Secp256k1, scanResponse, currencies), - getDerivations(EllipticCurve.Ed25519, scanResponse, currencies), - ).associate(transform = TokensMiddleware.DerivationData::derivations) + 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) @@ -184,7 +183,7 @@ internal class DefaultTokensListInteractor( .map(Currency::blockchain) .distinct() .filter { it.getSupportedCurves().contains(curve) } - .mapNotNull { it.derivationPath(scanResponse.card.derivationStyle) } + .mapNotNull { it.derivationPath(scanResponse.derivationStyleProvider.getDerivationStyle()) } val customTokensCandidates = currencyList .filter { it.blockchain.getSupportedCurves().contains(curve) } diff --git a/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/ui/TokensListScreen.kt b/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/ui/TokensListScreen.kt index b35bcd93bc..174563c6be 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/ui/TokensListScreen.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/ui/TokensListScreen.kt @@ -234,7 +234,7 @@ private fun Preview_TokensListScreen_Read() { TokensListScreen( stateHolder = TokensListStateHolder.ReadContent( toolbarState = TokensListToolbarState.Title.Read( - titleResId = R.string.search_tokens_title, + titleResId = R.string.common_search_tokens, onBackButtonClick = {}, onSearchButtonClick = {}, ), diff --git a/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/ui/TokensListToolbar.kt b/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/ui/TokensListToolbar.kt index 03ae578e8a..f5db5aa1a1 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/ui/TokensListToolbar.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/ui/TokensListToolbar.kt @@ -11,12 +11,7 @@ import androidx.compose.material.Icon import androidx.compose.material.IconButton import androidx.compose.material.Text import androidx.compose.material.TopAppBar -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue +import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier @@ -186,7 +181,7 @@ private fun Preview_AddTokensToolbar_ReadAccess() { TangemTheme { TokensListToolbar( state = Title.Read( - titleResId = R.string.search_tokens_title, + titleResId = R.string.common_search_tokens, onBackButtonClick = {}, onSearchButtonClick = {}, ), 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 c9ab947187..acc0ef2830 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 @@ -16,6 +16,7 @@ import com.tangem.core.ui.extensions.getActiveIconRes import com.tangem.domain.common.TapWorkarounds.useOldStyleDerivation import com.tangem.domain.common.extensions.canHandleToken import com.tangem.domain.common.extensions.fromNetworkId +import com.tangem.domain.common.util.cardTypesResolver import com.tangem.tap.common.extensions.fullNameWithoutTestnet import com.tangem.tap.common.extensions.getGreyedOutIconRes import com.tangem.tap.common.extensions.getNetworkName @@ -102,14 +103,14 @@ internal class TokensListViewModel @Inject constructor( private fun getInitialToolbarState(): TokensListToolbarState { return if (args.isManageAccess) { TokensListToolbarState.Title.Manage( - titleResId = R.string.main_manage_tokens, + titleResId = R.string.add_tokens_title, onBackButtonClick = actionsHandler::onBackButtonClick, onSearchButtonClick = actionsHandler::onSearchButtonClick, onAddCustomTokenClick = actionsHandler::onAddCustomTokenClick, ) } else { TokensListToolbarState.Title.Read( - titleResId = R.string.search_tokens_title, + titleResId = R.string.common_search_tokens, onBackButtonClick = actionsHandler::onBackButtonClick, onSearchButtonClick = actionsHandler::onSearchButtonClick, ) @@ -349,8 +350,14 @@ internal class TokensListViewModel @Inject constructor( toggledNetwork.changeToggleState() } } else { + val scanResponse = reduxStateHolder.scanResponse val isUnsupportedToken = - !(reduxStateHolder.scanResponse?.card?.canHandleToken(token.blockchain) ?: false) + !( + scanResponse?.card?.canHandleToken( + blockchain = token.blockchain, + cardTypesResolver = scanResponse.cardTypesResolver, + ) ?: false + ) if (isUnsupportedToken) { router.openUnsupportedSoltanaNetworkAlert() 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 21f763e48a..c24f982fca 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 @@ -1,7 +1,7 @@ package com.tangem.tap.features.tokens.legacy.redux import com.tangem.blockchain.common.Blockchain -import com.tangem.blockchain.common.DerivationStyle +import com.tangem.blockchain.common.derivation.DerivationStyle import com.tangem.tap.domain.model.WalletDataModel import org.rekotlin.Action 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 c62a8053d9..67bbf74043 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 @@ -1,7 +1,7 @@ package com.tangem.tap.features.tokens.legacy.redux import com.tangem.blockchain.common.Blockchain -import com.tangem.blockchain.common.DerivationStyle +import com.tangem.blockchain.common.derivation.DerivationStyle import com.tangem.common.CompletionResult import com.tangem.common.card.EllipticCurve import com.tangem.common.extensions.ByteArrayKey @@ -13,7 +13,8 @@ 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.TapWorkarounds.derivationStyle +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 @@ -21,7 +22,7 @@ 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.DELAY_SDK_DIALOG_CLOSE +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 @@ -30,11 +31,6 @@ 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 com.tangem.tap.scope -import com.tangem.tap.store -import com.tangem.tap.tangemSdkManager -import com.tangem.tap.userWalletsListManager -import com.tangem.tap.walletCurrenciesManager import kotlinx.coroutines.delay import kotlinx.coroutines.launch import org.rekotlin.Middleware @@ -72,7 +68,7 @@ object TokensMiddleware { currencies = convertToCurrencies( blockchains = blockchainsToRemove, tokens = tokensToRemove, - derivationStyle = scanResponse.card.derivationStyle, + derivationStyle = scanResponse.derivationStyleProvider.getDerivationStyle(), ), ) @@ -87,7 +83,7 @@ object TokensMiddleware { val currencyList = convertToCurrencies( blockchains = blockchainsToAdd, tokens = tokensToAdd, - derivationStyle = scanResponse.card.derivationStyle, + derivationStyle = scanResponse.derivationStyleProvider.getDerivationStyle(), ) if (scanResponse.supportsHdWallet()) { @@ -123,10 +119,11 @@ object TokensMiddleware { currencyList: List, onSuccess: (ScanResponse) -> Unit, ) { - val derivationDataList = listOfNotNull( - getDerivations(EllipticCurve.Secp256k1, scanResponse, currencyList), - getDerivations(EllipticCurve.Ed25519, scanResponse, currencyList), - ) + val config = CardConfig.createConfig(scanResponse.card) + val derivationDataList = currencyList.mapNotNull { + val curve = config.primaryCurve(it.blockchain) + curve?.let { getDerivations(curve, scanResponse, currencyList) } + } val derivations = derivationDataList.associate { it.derivations } if (derivations.isEmpty()) { onSuccess(scanResponse) @@ -175,7 +172,7 @@ object TokensMiddleware { val manageTokensCandidates = currencyList.map { it.blockchain }.distinct().filter { it.getSupportedCurves().contains(curve) }.mapNotNull { - it.derivationPath(scanResponse.card.derivationStyle) + it.derivationPath(scanResponse.derivationStyleProvider.getDerivationStyle()) } val customTokensCandidates = currencyList.filter { 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 e2589be5b7..8e33fdc8f4 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,7 +1,7 @@ package com.tangem.tap.features.tokens.legacy.redux import com.tangem.blockchain.common.Blockchain -import com.tangem.blockchain.common.DerivationStyle +import com.tangem.blockchain.common.derivation.DerivationStyle import com.tangem.tap.common.redux.AppState import com.tangem.tap.domain.model.WalletDataModel import com.tangem.tap.features.wallet.models.Currency 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 new file mode 100644 index 0000000000..1cf1515b01 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/wallet/converters/CryptoCurrencyConverter.kt @@ -0,0 +1,43 @@ +package com.tangem.tap.features.wallet.converters + +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 + +class CryptoCurrencyConverter : Converter { + + private val cryptoCurrencyFactory by lazy { CryptoCurrencyFactory() } + + override fun convert(value: Currency): CryptoCurrency { + return when (value) { + is Currency.Blockchain -> requireNotNull( + cryptoCurrencyFactory.createCoin( + blockchain = value.blockchain, + derivationStyleProvider = requireNotNull( + store.state.globalState + .userWalletsListManager + ?.selectedUserWalletSync + ?.scanResponse + ?.derivationStyleProvider, + ), + ), + ) + is Currency.Token -> requireNotNull( + cryptoCurrencyFactory.createToken( + sdkToken = value.token, + blockchain = value.blockchain, + derivationStyleProvider = requireNotNull( + store.state.globalState + .userWalletsListManager + ?.selectedUserWalletSync + ?.scanResponse + ?.derivationStyleProvider, + ), + ), + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/wallet/models/Currency.kt b/app/src/main/java/com/tangem/tap/features/wallet/models/Currency.kt index 4b82889e22..9262a61187 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/models/Currency.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/models/Currency.kt @@ -1,6 +1,6 @@ package com.tangem.tap.features.wallet.models -import com.tangem.blockchain.common.DerivationStyle +import com.tangem.blockchain.common.derivation.DerivationStyle import com.tangem.datasource.api.tangemTech.models.UserTokensResponse import com.tangem.domain.common.BlockchainNetwork import com.tangem.domain.common.extensions.fromNetworkId diff --git a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/AppCurrencyMiddleware.kt b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/AppCurrencyMiddleware.kt index bc5e960649..402f1aaef9 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/AppCurrencyMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/AppCurrencyMiddleware.kt @@ -3,12 +3,14 @@ package com.tangem.tap.features.wallet.redux.middlewares import com.tangem.common.extensions.guard import com.tangem.core.analytics.Analytics import com.tangem.data.source.preferences.model.DataSourceCurrency -import com.tangem.data.source.preferences.model.DataSourceFiatCurrency import com.tangem.data.source.preferences.storage.FiatCurrenciesPrefStorage +import com.tangem.domain.appcurrency.repository.AppCurrencyRepository +import com.tangem.features.wallet.featuretoggles.WalletFeatureToggles import com.tangem.tap.common.analytics.events.AnalyticsParam import com.tangem.tap.common.analytics.events.MainScreen import com.tangem.tap.common.entities.FiatCurrency import com.tangem.tap.common.extensions.dispatchDialogShow +import com.tangem.tap.common.extensions.dispatchWithMain import com.tangem.tap.common.redux.global.GlobalAction import com.tangem.tap.domain.TapWalletManager import com.tangem.tap.features.details.redux.DetailsAction @@ -19,6 +21,8 @@ import com.tangem.tap.features.walletSelector.redux.WalletSelectorAction import com.tangem.tap.scope import com.tangem.tap.store import com.tangem.tap.userWalletsListManager +import com.tangem.utils.coroutines.JobHolder +import com.tangem.utils.coroutines.saveIn import kotlinx.coroutines.launch import timber.log.Timber @@ -26,8 +30,12 @@ class AppCurrencyMiddleware( private val walletRepository: WalletRepository, private val tapWalletManager: TapWalletManager, private val fiatCurrenciesPrefStorage: FiatCurrenciesPrefStorage, + private val featureToggles: WalletFeatureToggles, + private val appCurrencyRepository: AppCurrencyRepository, private val appCurrencyProvider: () -> FiatCurrency, ) { + private val showSelectorJobHolder = JobHolder() + fun handle(action: WalletAction.AppCurrencyAction) { when (action) { is WalletAction.AppCurrencyAction.ChooseAppCurrency -> showSelector() @@ -36,6 +44,52 @@ class AppCurrencyMiddleware( } private fun showSelector() { + if (featureToggles.isRedesignedScreenEnabled) { + showSelectorNew() + } else { + showSelectorLegacy() + } + } + + private fun selectCurrency(action: WalletAction.AppCurrencyAction.SelectAppCurrency) { + Analytics.send(MainScreen.MainCurrencyChanged(AnalyticsParam.CurrencyType.FiatCurrency(action.fiatCurrency))) + + scope.launch { + appCurrencyRepository.changeAppCurrency(action.fiatCurrency.code) + + store.dispatchWithMain(GlobalAction.ChangeAppCurrency(action.fiatCurrency)) + store.dispatchWithMain(DetailsAction.ChangeAppCurrency(action.fiatCurrency)) + store.dispatchWithMain(WalletSelectorAction.ChangeAppCurrency(action.fiatCurrency)) + + val selectedUserWallet = userWalletsListManager.selectedUserWalletSync.guard { + Timber.e("Unable to select currency, no user wallet selected") + return@launch + } + + tapWalletManager.loadData(selectedUserWallet, refresh = true) + } + } + + private fun showSelectorNew() { + scope.launch { + val currencies = appCurrencyRepository.getAvailableAppCurrencies() + + store.dispatchDialogShow( + WalletDialog.CurrencySelectionDialog( + currenciesList = currencies.map { appCurrency -> + FiatCurrency( + code = appCurrency.code, + name = appCurrency.name, + symbol = appCurrency.symbol, + ) + }, + currentAppCurrency = appCurrencyProvider.invoke(), + ), + ) + }.saveIn(showSelectorJobHolder) + } + + private fun showSelectorLegacy() { val storedFiatCurrencies = fiatCurrenciesPrefStorage.restore() if (storedFiatCurrencies.isNotEmpty()) { store.dispatchDialogShow( @@ -65,23 +119,6 @@ class AppCurrencyMiddleware( } } - private fun selectCurrency(action: WalletAction.AppCurrencyAction.SelectAppCurrency) { - Analytics.send(MainScreen.MainCurrencyChanged(AnalyticsParam.CurrencyType.FiatCurrency(action.fiatCurrency))) - fiatCurrenciesPrefStorage.saveAppCurrency( - with(action.fiatCurrency) { DataSourceFiatCurrency(code, name, symbol) }, - ) - store.dispatch(GlobalAction.ChangeAppCurrency(action.fiatCurrency)) - store.dispatch(DetailsAction.ChangeAppCurrency(action.fiatCurrency)) - store.dispatch(WalletSelectorAction.ChangeAppCurrency(action.fiatCurrency)) - val selectedUserWallet = userWalletsListManager.selectedUserWalletSync.guard { - Timber.e("Unable to select currency, no user wallet selected") - return - } - scope.launch { - tapWalletManager.loadData(selectedUserWallet, refresh = true) - } - } - private fun List.mapToUiModel(): List { return this.map { FiatCurrency( diff --git a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/MultiWalletMiddleware.kt b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/MultiWalletMiddleware.kt index 3f080d645a..91fa88858c 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/MultiWalletMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/MultiWalletMiddleware.kt @@ -1,5 +1,6 @@ package com.tangem.tap.features.wallet.redux.middlewares +import androidx.core.os.bundleOf import com.tangem.common.doOnSuccess import com.tangem.common.extensions.guard import com.tangem.common.flatMap @@ -7,6 +8,7 @@ import com.tangem.core.analytics.Analytics import com.tangem.core.navigation.AppScreen import com.tangem.core.navigation.NavigationAction import com.tangem.domain.wallets.models.UserWallet +import com.tangem.features.tokendetails.navigation.TokenDetailsRouter import com.tangem.tap.common.analytics.events.AnalyticsParam import com.tangem.tap.common.analytics.events.Token.ButtonRemoveToken import com.tangem.tap.common.extensions.addContext @@ -15,6 +17,7 @@ import com.tangem.tap.common.extensions.dispatchErrorNotification import com.tangem.tap.common.extensions.dispatchWithMain import com.tangem.tap.common.redux.global.GlobalAction import com.tangem.tap.domain.TapError +import com.tangem.tap.features.wallet.converters.CryptoCurrencyConverter import com.tangem.tap.features.wallet.redux.WalletAction import com.tangem.tap.features.wallet.redux.WalletState import com.tangem.tap.features.wallet.redux.models.WalletDialog @@ -28,12 +31,19 @@ import kotlinx.coroutines.launch import timber.log.Timber class MultiWalletMiddleware { + + private val cryptoCurrencyConverter by lazy { CryptoCurrencyConverter() } + @Suppress("LongMethod", "ComplexMethod") fun handle(action: WalletAction.MultiWallet, walletState: WalletState?) { when (action) { is WalletAction.MultiWallet.SelectWallet -> { if (action.currency != null) { - store.dispatch(NavigationAction.NavigateTo(AppScreen.WalletDetails)) + val bundle = bundleOf( + // TODO: [REDACTED_JIRA] + TokenDetailsRouter.SELECTED_CURRENCY_KEY to cryptoCurrencyConverter.convert(action.currency), + ) + store.dispatch(NavigationAction.NavigateTo(screen = AppScreen.WalletDetails, bundle = bundle)) } } is WalletAction.MultiWallet.TryToRemoveWallet -> { diff --git a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/WalletMiddleware.kt b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/WalletMiddleware.kt index a96e7e6197..c84ac7fa2d 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/WalletMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/WalletMiddleware.kt @@ -13,6 +13,7 @@ import com.tangem.core.navigation.AppScreen import com.tangem.core.navigation.NavigationAction import com.tangem.datasource.connection.NetworkConnectionManager import com.tangem.domain.userwallets.GetCardImageUseCase +import com.tangem.domain.wallets.legacy.lockIfLockable import com.tangem.tap.* import com.tangem.tap.common.analytics.events.AnalyticsParam import com.tangem.tap.common.analytics.events.Basic @@ -24,7 +25,6 @@ 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.domain.model.WalletStoreModel -import com.tangem.tap.domain.userWalletList.lockIfLockable import com.tangem.tap.features.demo.DemoHelper import com.tangem.tap.features.home.redux.HomeAction import com.tangem.tap.features.send.redux.PrepareSendScreen @@ -55,6 +55,8 @@ class WalletMiddleware { walletRepository = store.state.featureRepositoryProvider.walletRepository, tapWalletManager = store.state.globalState.tapWalletManager, fiatCurrenciesPrefStorage = preferencesStorage.fiatCurrenciesPrefStorage, + appCurrencyRepository = store.state.daggerGraphState.get(DaggerGraphState::appCurrencyRepository), + featureToggles = store.state.daggerGraphState.get(DaggerGraphState::walletFeatureToggles), appCurrencyProvider = { store.state.globalState.appCurrency }, ) } diff --git a/app/src/main/java/com/tangem/tap/features/wallet/ui/MultipleAddressUiHelper.kt b/app/src/main/java/com/tangem/tap/features/wallet/ui/MultipleAddressUiHelper.kt index c61cb57c3f..9531a726e3 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/ui/MultipleAddressUiHelper.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/ui/MultipleAddressUiHelper.kt @@ -12,7 +12,7 @@ object MultipleAddressUiHelper { Blockchain.BitcoinTestnet, Blockchain.Litecoin, Blockchain.BitcoinCash, - Blockchain.CardanoShelley, + Blockchain.Cardano, ) fun typeToId(type: AddressType, blockchain: Blockchain): Int { diff --git a/app/src/main/java/com/tangem/tap/features/wallet/ui/WalletDetailsFragment.kt b/app/src/main/java/com/tangem/tap/features/wallet/ui/WalletDetailsFragment.kt index 1c0c5027f2..f515410168 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/ui/WalletDetailsFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/ui/WalletDetailsFragment.kt @@ -1,11 +1,7 @@ package com.tangem.tap.features.wallet.ui import android.os.Bundle -import android.view.Menu -import android.view.MenuInflater -import android.view.MenuItem -import android.view.View -import android.view.ViewGroup +import android.view.* import android.widget.TextView import androidx.activity.OnBackPressedCallback import androidx.annotation.ColorRes @@ -23,8 +19,8 @@ import com.tangem.common.doOnResult import com.tangem.common.extensions.guard import com.tangem.core.analytics.Analytics import com.tangem.core.navigation.NavigationAction -import com.tangem.domain.common.TapWorkarounds.derivationStyle import com.tangem.domain.common.extensions.withMainContext +import com.tangem.domain.common.util.derivationStyleProvider import com.tangem.feature.swap.api.SwapFeatureToggleManager import com.tangem.feature.swap.domain.SwapInteractor import com.tangem.sdk.extensions.dpToPx @@ -32,14 +28,7 @@ import com.tangem.tap.common.SnackbarHandler import com.tangem.tap.common.TestActions import com.tangem.tap.common.analytics.events.DetailsScreen import com.tangem.tap.common.analytics.events.Token -import com.tangem.tap.common.extensions.appendIfNotNull -import com.tangem.tap.common.extensions.beginDelayedTransition -import com.tangem.tap.common.extensions.fitChipsByGroupWidth -import com.tangem.tap.common.extensions.getColor -import com.tangem.tap.common.extensions.getString -import com.tangem.tap.common.extensions.hide -import com.tangem.tap.common.extensions.show -import com.tangem.tap.common.extensions.toQrCode +import com.tangem.tap.common.extensions.* import com.tangem.tap.common.recyclerView.SpaceItemDecoration import com.tangem.tap.common.redux.AppState import com.tangem.tap.common.utils.SafeStoreSubscriber @@ -57,15 +46,7 @@ import com.tangem.tap.features.wallet.ui.adapters.PendingTransactionsAdapter import com.tangem.tap.features.wallet.ui.adapters.WalletDetailWarningMessagesAdapter import com.tangem.tap.features.wallet.ui.images.load import com.tangem.tap.features.wallet.ui.test.TestWallet -import com.tangem.tap.features.wallet.ui.utils.assembleWarnings -import com.tangem.tap.features.wallet.ui.utils.getAvailableActions -import com.tangem.tap.features.wallet.ui.utils.getFormattedCryptoAmount -import com.tangem.tap.features.wallet.ui.utils.getFormattedFiatAmount -import com.tangem.tap.features.wallet.ui.utils.isAvailableToBuy -import com.tangem.tap.features.wallet.ui.utils.isAvailableToSell -import com.tangem.tap.features.wallet.ui.utils.isAvailableToSwap -import com.tangem.tap.features.wallet.ui.utils.mainButton -import com.tangem.tap.features.wallet.ui.utils.shouldShowMultipleAddress +import com.tangem.tap.features.wallet.ui.utils.* import com.tangem.tap.store import com.tangem.tap.userWalletsListManagerSafe import com.tangem.tap.walletCurrenciesManager @@ -336,10 +317,8 @@ class WalletDetailsFragment : Fragment(R.layout.fragment_wallet_details), SafeSt private fun handleCurrencyIcon(currency: Currency) = with(binding.lWalletDetails.lBalance) { ivCurrency.load( currency = currency, - derivationStyle = store.state.globalState - .scanResponse - ?.card - ?.derivationStyle, + derivationStyle = store.state.globalState.scanResponse + ?.derivationStyleProvider?.getDerivationStyle(), ) } diff --git a/app/src/main/java/com/tangem/tap/features/wallet/ui/adapters/WalletAdapter.kt b/app/src/main/java/com/tangem/tap/features/wallet/ui/adapters/WalletAdapter.kt index e0a15c7e4b..3f511f7b65 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/ui/adapters/WalletAdapter.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/ui/adapters/WalletAdapter.kt @@ -7,7 +7,7 @@ import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.ListAdapter import androidx.recyclerview.widget.RecyclerView import com.tangem.core.analytics.Analytics -import com.tangem.domain.common.TapWorkarounds.derivationStyle +import com.tangem.domain.common.util.derivationStyleProvider import com.tangem.tap.common.analytics.events.Portfolio import com.tangem.tap.common.extensions.getString import com.tangem.tap.common.extensions.hide @@ -77,10 +77,8 @@ class WalletAdapter : ListAdapter feeForTx.minimum + is TransactionFee.Single -> feeForTx.normal + } val coinValue = walletManager.wallet.amounts[AmountType.Coin]?.value ?: BigDecimal.ZERO if (coinValue < fee.amount.value) return diff --git a/app/src/main/java/com/tangem/tap/network/exchangeServices/mercuryo/MercuryoService.kt b/app/src/main/java/com/tangem/tap/network/exchangeServices/mercuryo/MercuryoService.kt index f7725ec89a..39d09fb0dc 100644 --- a/app/src/main/java/com/tangem/tap/network/exchangeServices/mercuryo/MercuryoService.kt +++ b/app/src/main/java/com/tangem/tap/network/exchangeServices/mercuryo/MercuryoService.kt @@ -119,7 +119,7 @@ internal class MercuryoService(private val environment: MercuryoEnvironment) : E return when (currencyName) { "BNB" -> Blockchain.BSC "ETH" -> Blockchain.Ethereum - "ADA" -> Blockchain.CardanoShelley + "ADA" -> Blockchain.Cardano else -> Blockchain.values().find { it.currency.lowercase() == currencyName.lowercase() } } } diff --git a/app/src/main/java/com/tangem/tap/proxy/DerivationManagerImpl.kt b/app/src/main/java/com/tangem/tap/proxy/DerivationManagerImpl.kt index e24eae21c2..894467225e 100644 --- a/app/src/main/java/com/tangem/tap/proxy/DerivationManagerImpl.kt +++ b/app/src/main/java/com/tangem/tap/proxy/DerivationManagerImpl.kt @@ -10,8 +10,9 @@ import com.tangem.common.extensions.ByteArrayKey import com.tangem.common.extensions.toMapKey import com.tangem.crypto.hdWallet.DerivationPath import com.tangem.domain.common.BlockchainNetwork -import com.tangem.domain.common.TapWorkarounds.derivationStyle +import com.tangem.domain.common.configs.CardConfig import com.tangem.domain.common.extensions.fromNetworkId +import com.tangem.domain.common.util.derivationStyleProvider import com.tangem.domain.common.util.hasDerivation import com.tangem.domain.models.scan.ScanResponse import com.tangem.lib.crypto.DerivationManager @@ -46,13 +47,13 @@ class DerivationManagerImpl( } else { null } - val blockchainNetwork = BlockchainNetwork(blockchain, card) - val appCurrency = com.tangem.tap.features.wallet.models.Currency.fromBlockchainNetwork( - blockchainNetwork, - appToken, - ) val scanResponse = appStateHolder.scanResponse if (scanResponse != null) { + val blockchainNetwork = BlockchainNetwork(blockchain, scanResponse.derivationStyleProvider) + val appCurrency = com.tangem.tap.features.wallet.models.Currency.fromBlockchainNetwork( + blockchainNetwork, + appToken, + ) deriveMissingBlockchains( scanResponse = scanResponse, currencyList = listOf(appCurrency), @@ -70,7 +71,7 @@ class DerivationManagerImpl( val scanResponse = appStateHolder.scanResponse val blockchain = Blockchain.fromNetworkId(networkId) if (scanResponse != null && blockchain != null) { - return blockchain.derivationPath(appStateHolder.getActualCard()?.derivationStyle)?.rawPath + return blockchain.derivationPath(scanResponse.derivationStyleProvider.getDerivationStyle())?.rawPath } return null } @@ -93,10 +94,11 @@ class DerivationManagerImpl( onSuccess: (ScanResponse) -> Unit, onFailure: (Exception) -> Unit, ) { - val derivationDataList = listOfNotNull( - getDerivations(EllipticCurve.Secp256k1, scanResponse, currencyList), - getDerivations(EllipticCurve.Ed25519, scanResponse, currencyList), - ) + val config = CardConfig.createConfig(scanResponse.card) + val derivationDataList = currencyList.mapNotNull { + val curve = config.primaryCurve(it.blockchain) + curve?.let { getDerivations(curve, scanResponse, currencyList) } + } val derivations = derivationDataList.associate { it.derivations } if (derivations.isEmpty()) { onSuccess(scanResponse) @@ -162,7 +164,7 @@ class DerivationManagerImpl( val manageTokensCandidates = currencyList.map { it.blockchain }.distinct().filter { it.getSupportedCurves().contains(curve) }.mapNotNull { - it.derivationPath(scanResponse.card.derivationStyle) + it.derivationPath(scanResponse.derivationStyleProvider.getDerivationStyle()) } val customTokensCandidates = currencyList.filter { diff --git a/app/src/main/java/com/tangem/tap/proxy/TxHistoryManagerImpl.kt b/app/src/main/java/com/tangem/tap/proxy/TxHistoryManagerImpl.kt deleted file mode 100644 index f09737e428..0000000000 --- a/app/src/main/java/com/tangem/tap/proxy/TxHistoryManagerImpl.kt +++ /dev/null @@ -1,86 +0,0 @@ -package com.tangem.tap.proxy - -import com.tangem.blockchain.common.Blockchain -import com.tangem.blockchain.common.TransactionStatus -import com.tangem.blockchain.common.WalletManager -import com.tangem.blockchain.common.txhistory.TransactionHistoryItem -import com.tangem.blockchain.common.txhistory.TransactionHistoryState -import com.tangem.blockchain.extensions.Result -import com.tangem.domain.common.BlockchainNetwork -import com.tangem.domain.common.extensions.fromNetworkId -import com.tangem.lib.crypto.TxHistoryManager -import com.tangem.lib.crypto.models.ProxyAmount -import com.tangem.lib.crypto.models.txhistory.ProxyTransactionHistoryItem -import com.tangem.lib.crypto.models.txhistory.ProxyTransactionHistoryState -import com.tangem.lib.crypto.models.txhistory.ProxyTransactionStatus - -class TxHistoryManagerImpl( - private val appStateHolder: AppStateHolder, -) : TxHistoryManager { - - override suspend fun checkTxHistoryState(networkId: String, derivationPath: String?): ProxyTransactionHistoryState { - val blockchain = requireNotNull(Blockchain.fromNetworkId(networkId)) { "blockchain not found" } - val walletManager = getActualWalletManager(blockchain, derivationPath) - val state = walletManager.getTransactionHistoryState(address = walletManager.wallet.address) - return state.mapToProxy() - } - - override suspend fun getTxHistoryItems( - networkId: String, - derivationPath: String?, - page: Int, - pageSize: Int, - ): List { - val blockchain = requireNotNull(Blockchain.fromNetworkId(networkId)) { "blockchain not found" } - val walletManager = getActualWalletManager(blockchain, derivationPath) - val itemsResult = walletManager.getTransactionsHistory( - address = walletManager.wallet.address, - page = page, - pageSize = pageSize, - ) - - return when (itemsResult) { - is Result.Success -> itemsResult.data.items.map { historyItem -> historyItem.mapToProxy() } - is Result.Failure -> error(itemsResult.error.message ?: itemsResult.error.customMessage) - } - } - - private fun getActualWalletManager(blockchain: Blockchain, derivationPath: String?): WalletManager { - val blockchainNetwork = BlockchainNetwork(blockchain, derivationPath, emptyList()) - val walletManager = appStateHolder.walletState?.getWalletManager(blockchainNetwork) - return requireNotNull(walletManager) { "no wallet manager found" } - } - - private fun TransactionHistoryState.mapToProxy(): ProxyTransactionHistoryState { - return when (this) { - TransactionHistoryState.Success.Empty -> ProxyTransactionHistoryState.Success.Empty - is TransactionHistoryState.Failed.FetchError -> ProxyTransactionHistoryState.Failed.FetchError(exception) - TransactionHistoryState.NotImplemented -> ProxyTransactionHistoryState.NotImplemented - is TransactionHistoryState.Success.HasTransactions -> - ProxyTransactionHistoryState.Success.HasTransactions(txCount) - } - } - - private fun TransactionHistoryItem.mapToProxy() = ProxyTransactionHistoryItem( - txHash = txHash, - timestamp = timestamp, - direction = when (val direction = direction) { - is TransactionHistoryItem.TransactionDirection.Incoming -> - ProxyTransactionHistoryItem.TransactionDirection.Incoming(direction.from) - is TransactionHistoryItem.TransactionDirection.Outgoing -> - ProxyTransactionHistoryItem.TransactionDirection.Outgoing(direction.to) - }, - status = when (status) { - TransactionStatus.Confirmed -> ProxyTransactionStatus.Confirmed - TransactionStatus.Unconfirmed -> ProxyTransactionStatus.Unconfirmed - }, - type = when (type) { - TransactionHistoryItem.TransactionType.Transfer -> ProxyTransactionHistoryItem.TransactionType.Transfer - }, - amount = ProxyAmount( - currencySymbol = amount.currencySymbol, - value = requireNotNull(amount.value) { "Amount value must not be null" }, - decimals = amount.decimals, - ), - ) -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/proxy/di/ProxyModule.kt b/app/src/main/java/com/tangem/tap/proxy/di/ProxyModule.kt index 9bae439352..19474ce5cc 100644 --- a/app/src/main/java/com/tangem/tap/proxy/di/ProxyModule.kt +++ b/app/src/main/java/com/tangem/tap/proxy/di/ProxyModule.kt @@ -1,6 +1,6 @@ package com.tangem.tap.proxy.di -import androidx.compose.ui.text.intl.Locale +import com.tangem.common.Provider import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.domain.card.repository.CardSdkConfigRepository import com.tangem.domain.common.CardTypesResolver @@ -8,7 +8,6 @@ import com.tangem.domain.common.util.cardTypesResolver import com.tangem.feature.learn2earn.domain.api.Learn2earnDependencyProvider import com.tangem.lib.crypto.DerivationManager import com.tangem.lib.crypto.TransactionManager -import com.tangem.lib.crypto.TxHistoryManager import com.tangem.lib.crypto.UserWalletManager import com.tangem.tap.proxy.* import dagger.Module @@ -59,12 +58,6 @@ class ProxyModule { ) } - @Provides - @Singleton - fun provideTxHistoryManager(appStateHolder: AppStateHolder): TxHistoryManager { - return TxHistoryManagerImpl(appStateHolder = appStateHolder) - } - // regions FeatureConsumers @Provides @Singleton @@ -81,9 +74,7 @@ class ProxyModule { } } - override fun getLocaleProvider(): () -> String = { Locale.current.language } - - override fun getWebViewAuthCredentialsProvider(): () -> String? = { + override fun getWebViewAuthCredentialsProvider(): Provider = Provider { appStateHolder.mainStore?.state?.globalState?.configManager?.config?.tangemComAuthorization } } 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 228d3f014b..b9be4b9f93 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 @@ -2,6 +2,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.card.ScanCardProcessor import com.tangem.domain.card.ScanCardUseCase import com.tangem.domain.card.repository.CardSdkConfigRepository @@ -31,6 +32,7 @@ data class DaggerGraphState( val tokenDetailsRouter: TokenDetailsRouter? = null, val scanCardProcessor: ScanCardProcessor? = null, val cardSdkConfigRepository: CardSdkConfigRepository? = null, + val appCurrencyRepository: AppCurrencyRepository? = null, ) : StateType { inline fun get(getDependency: DaggerGraphState.() -> T?): T { diff --git a/app/src/main/res/drawable/ic_chia_no_color.xml b/app/src/main/res/drawable/ic_chia_no_color.xml new file mode 100644 index 0000000000..9119f89479 --- /dev/null +++ b/app/src/main/res/drawable/ic_chia_no_color.xml @@ -0,0 +1,13 @@ + + + + diff --git a/app/src/main/res/layout/dialog_wallet_send.xml b/app/src/main/res/layout/dialog_wallet_send.xml index 2fa54ef3d3..d204d38de3 100644 --- a/app/src/main/res/layout/dialog_wallet_send.xml +++ b/app/src/main/res/layout/dialog_wallet_send.xml @@ -10,7 +10,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:paddingBottom="16dp" - android:text="Select" + android:text="@string/wallet_choice_wallet_option_title" android:textSize="14sp" android:textStyle="bold" /> diff --git a/app/src/main/res/layout/fragment_shop.xml b/app/src/main/res/layout/fragment_shop.xml index 0a61bf5b4a..3d6e2f4020 100644 --- a/app/src/main/res/layout/fragment_shop.xml +++ b/app/src/main/res/layout/fragment_shop.xml @@ -264,16 +264,17 @@ - + + + + + + + + + + + , +) { + + data class Quote( + @Json(name = "price") + val price: BigDecimal, + @Json(name = "priceChange24h") + val priceChange: BigDecimal, + @Json(name = "lastUpdatedAt") + val lastUpdated: String, + ) +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/config/ConfigManagerImpl.kt b/core/datasource/src/main/java/com/tangem/datasource/config/ConfigManagerImpl.kt index 1557e84bf1..75b37e271f 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/config/ConfigManagerImpl.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/config/ConfigManagerImpl.kt @@ -96,6 +96,7 @@ internal class ConfigManagerImpl @Inject constructor() : ConfigManager { mainnetApiKey = configValues.tonCenterKeys.mainnet, testnetApiKey = configValues.tonCenterKeys.testnet, ), + chiaFireAcademyApiKey = configValues.chiaFireAcademyApiKey, ), appsFlyerDevKey = configValues.appsFlyer.appsFlyerDevKey, amplitudeApiKey = configValues.amplitudeApiKey, diff --git a/core/datasource/src/main/java/com/tangem/datasource/config/models/JsonModels.kt b/core/datasource/src/main/java/com/tangem/datasource/config/models/JsonModels.kt index 2e195247a6..bcad6003db 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/config/models/JsonModels.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/config/models/JsonModels.kt @@ -38,6 +38,7 @@ class ConfigValueModel( val kaspaSecondaryApiUrl: String, val walletConnectProjectId: String, val tangemComAuthorization: String?, + val chiaFireAcademyApiKey: String?, ) data class AppsFlyer( diff --git a/core/datasource/src/main/java/com/tangem/datasource/di/AppCurrencyDataModule.kt b/core/datasource/src/main/java/com/tangem/datasource/di/AppCurrencyDataModule.kt new file mode 100644 index 0000000000..b6d8832f86 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/di/AppCurrencyDataModule.kt @@ -0,0 +1,45 @@ +package com.tangem.datasource.di + +import android.content.Context +import com.squareup.moshi.Moshi +import com.tangem.datasource.api.tangemTech.models.CurrenciesResponse +import com.tangem.datasource.local.appcurrency.AvailableAppCurrenciesStore +import com.tangem.datasource.local.appcurrency.SelectedAppCurrencyStore +import com.tangem.datasource.local.appcurrency.implementation.DefaultAvailableAppCurrenciesStore +import com.tangem.datasource.local.appcurrency.implementation.DefaultSelectedAppCurrencyStore +import com.tangem.datasource.local.datastore.RuntimeDataStore +import com.tangem.datasource.local.datastore.SharedPreferencesDataStore +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.qualifiers.ApplicationContext +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal object AppCurrencyDataModule { + + @Provides + @Singleton + fun provideAvailableAppCurrenciesStore(): AvailableAppCurrenciesStore { + return DefaultAvailableAppCurrenciesStore( + dataStore = RuntimeDataStore(), + ) + } + + @Provides + @Singleton + fun provideSelectedAppCurrencyStore( + @ApplicationContext context: Context, + @NetworkMoshi moshi: Moshi, + ): SelectedAppCurrencyStore { + return DefaultSelectedAppCurrencyStore( + dataStore = SharedPreferencesDataStore( + preferencesName = "selected_app_currency", + context = context, + adapter = moshi.adapter(CurrenciesResponse.Currency::class.java), + ), + ) + } +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/di/QuotesStoreModule.kt b/core/datasource/src/main/java/com/tangem/datasource/di/QuotesStoreModule.kt new file mode 100644 index 0000000000..a62a3e4f31 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/di/QuotesStoreModule.kt @@ -0,0 +1,31 @@ +package com.tangem.datasource.di + +import android.content.Context +import com.squareup.moshi.Moshi +import com.tangem.datasource.local.datastore.SharedPreferencesDataStore +import com.tangem.datasource.local.quote.DefaultQuotesStore +import com.tangem.datasource.local.quote.QuotesStore +import com.tangem.datasource.local.quote.model.StoredQuote +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.qualifiers.ApplicationContext +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal object QuotesStoreModule { + + @Provides + @Singleton + fun provideQuotesStore(@ApplicationContext context: Context, @NetworkMoshi moshi: Moshi): QuotesStore { + return DefaultQuotesStore( + dataStore = SharedPreferencesDataStore( + preferencesName = "quotes", + context = context, + adapter = moshi.adapter(StoredQuote::class.java), + ), + ) + } +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/appcurrency/AvailableAppCurrenciesStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/appcurrency/AvailableAppCurrenciesStore.kt new file mode 100644 index 0000000000..be9b7281cf --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/local/appcurrency/AvailableAppCurrenciesStore.kt @@ -0,0 +1,12 @@ +package com.tangem.datasource.local.appcurrency + +import com.tangem.datasource.api.tangemTech.models.CurrenciesResponse + +interface AvailableAppCurrenciesStore { + + suspend fun getAllSyncOrNull(): List? + + suspend fun getSyncOrNull(key: String): CurrenciesResponse.Currency? + + suspend fun store(response: CurrenciesResponse) +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/appcurrency/MockSelectedAppCurrencyStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/appcurrency/MockSelectedAppCurrencyStore.kt new file mode 100644 index 0000000000..58f48aacc8 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/local/appcurrency/MockSelectedAppCurrencyStore.kt @@ -0,0 +1,26 @@ +package com.tangem.datasource.local.appcurrency + +import com.tangem.datasource.api.tangemTech.models.CurrenciesResponse +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flowOf + +// TODO: Will be implemented in [REDACTED_TASK_KEY] task +internal class MockSelectedAppCurrencyStore : SelectedAppCurrencyStore { + + override fun get(): Flow { + return flowOf( + CurrenciesResponse.Currency( + id = "usd", + code = "USD", + name = "US Dollar", + unit = "$", + type = "fiat", + rateBTC = "", + ), + ) + } + + override suspend fun store(item: CurrenciesResponse.Currency) { + /* no-op */ + } +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/appcurrency/SelectedAppCurrencyStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/appcurrency/SelectedAppCurrencyStore.kt new file mode 100644 index 0000000000..e1fbad8329 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/local/appcurrency/SelectedAppCurrencyStore.kt @@ -0,0 +1,11 @@ +package com.tangem.datasource.local.appcurrency + +import com.tangem.datasource.api.tangemTech.models.CurrenciesResponse +import kotlinx.coroutines.flow.Flow + +interface SelectedAppCurrencyStore { + + fun get(): Flow + + suspend fun store(item: CurrenciesResponse.Currency) +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/appcurrency/implementation/DefaultAvailableAppCurrenciesStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/appcurrency/implementation/DefaultAvailableAppCurrenciesStore.kt new file mode 100644 index 0000000000..ed9d257ec8 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/local/appcurrency/implementation/DefaultAvailableAppCurrenciesStore.kt @@ -0,0 +1,22 @@ +package com.tangem.datasource.local.appcurrency.implementation + +import com.tangem.datasource.api.tangemTech.models.CurrenciesResponse +import com.tangem.datasource.local.appcurrency.AvailableAppCurrenciesStore +import com.tangem.datasource.local.datastore.core.StringKeyDataStore +import com.tangem.datasource.local.datastore.core.StringKeyDataStoreDecorator + +internal class DefaultAvailableAppCurrenciesStore( + private val dataStore: StringKeyDataStore, +) : AvailableAppCurrenciesStore, + StringKeyDataStoreDecorator(dataStore) { + + override fun provideStringKey(key: String): String { + return key + } + + override suspend fun store(response: CurrenciesResponse) { + val currencies = response.currencies.associateBy(CurrenciesResponse.Currency::code) + + dataStore.store(currencies) + } +} \ 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 new file mode 100644 index 0000000000..c79bf3045b --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/local/appcurrency/implementation/DefaultSelectedAppCurrencyStore.kt @@ -0,0 +1,10 @@ +package com.tangem.datasource.local.appcurrency.implementation + +import com.tangem.datasource.api.tangemTech.models.CurrenciesResponse +import com.tangem.datasource.local.appcurrency.SelectedAppCurrencyStore +import com.tangem.datasource.local.datastore.core.KeylessDataStoreDecorator +import com.tangem.datasource.local.datastore.core.StringKeyDataStore + +internal class DefaultSelectedAppCurrencyStore( + dataStore: StringKeyDataStore, +) : SelectedAppCurrencyStore, KeylessDataStoreDecorator(dataStore) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/datastore/FileDataStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/datastore/FileDataStore.kt index c69c6d5c64..cc65a7ff21 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/datastore/FileDataStore.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/datastore/FileDataStore.kt @@ -3,38 +3,49 @@ package com.tangem.datasource.local.datastore import com.squareup.moshi.JsonAdapter import com.tangem.datasource.files.FileReader import com.tangem.datasource.local.datastore.core.StringKeyDataStore -import com.tangem.datasource.local.datastore.model.WriteTrigger -import kotlinx.coroutines.channels.BufferOverflow +import com.tangem.datasource.local.datastore.utils.Trigger import kotlinx.coroutines.flow.* import timber.log.Timber +@Deprecated("Use shared preferences data store instead") internal class FileDataStore( private val fileReader: FileReader, private val adapter: JsonAdapter, ) : StringKeyDataStore { - private val writeTrigger = MutableSharedFlow( - replay = 1, - onBufferOverflow = BufferOverflow.DROP_OLDEST, - ) + private val writeTrigger = Trigger() override fun get(key: String): Flow { return writeTrigger - .onEmpty { emit(WriteTrigger) } .map { getInternal(key) } .filterNotNull() + .distinctUntilChanged() + } + + override fun getAll(): Flow> { + val e = NotImplementedError("`getAll()` function not implemented for `FileDataStore`") + Timber.e(e) + + throw e } override suspend fun getSyncOrNull(key: String): Value? { return getInternal(key) } + override suspend fun getAllSyncOrNull(): List { + val e = NotImplementedError("`getAllSyncOrNull()` function not implemented for `FileDataStore`") + Timber.e(e) + + throw e + } + override suspend fun store(key: String, item: Value) { try { val json = adapter.toJson(item) fileReader.rewriteFile(json, key) - writeTrigger.tryEmit(WriteTrigger) + writeTrigger.trigger() } catch (e: Throwable) { Timber.e(e, "Unable to write file: $key") } @@ -48,10 +59,14 @@ internal class FileDataStore( override suspend fun remove(key: String) { fileReader.removeFile(key) + writeTrigger.trigger() } override suspend fun clear() { - // TODO: Implement if needed + val e = NotImplementedError("`clear()` function not implemented for `FileDataStore`") + Timber.e(e) + + throw e } private fun getInternal(fileName: String): Value? { diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/datastore/RuntimeDataStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/datastore/RuntimeDataStore.kt index d1c4eff1eb..2d4149648f 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/datastore/RuntimeDataStore.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/datastore/RuntimeDataStore.kt @@ -13,10 +13,18 @@ internal class RuntimeDataStore : StringKeyDataStore { .filterNotNull() } + override fun getAll(): Flow> { + return store.map { value -> value.values.toList() } + } + override suspend fun getSyncOrNull(key: String): Data? { return store.value[key] } + override suspend fun getAllSyncOrNull(): List { + return store.value.values.toList() + } + override suspend fun store(key: String, item: Data) { store.update { value -> value[key] = item diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/datastore/SharedPreferencesDataStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/datastore/SharedPreferencesDataStore.kt new file mode 100644 index 0000000000..cd2ed5c463 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/local/datastore/SharedPreferencesDataStore.kt @@ -0,0 +1,99 @@ +package com.tangem.datasource.local.datastore + +import android.content.Context +import android.content.Context.MODE_PRIVATE +import android.content.SharedPreferences +import androidx.core.content.edit +import com.squareup.moshi.JsonAdapter +import com.tangem.datasource.local.datastore.core.StringKeyDataStore +import com.tangem.datasource.local.datastore.utils.Trigger +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.flow.map +import timber.log.Timber + +internal class SharedPreferencesDataStore( + preferencesName: String, + private val context: Context, + private val adapter: JsonAdapter, +) : StringKeyDataStore { + + private val sharedPreferences: SharedPreferences by lazy { + context.getSharedPreferences(preferencesName, MODE_PRIVATE) + } + + private val writeTrigger = Trigger() + + override fun get(key: String): Flow { + return writeTrigger + .map { getInternal(key) } + .filterNotNull() + .distinctUntilChanged() + } + + override fun getAll(): Flow> { + return writeTrigger + .map { getAllInternal() } + .distinctUntilChanged() + } + + override suspend fun getSyncOrNull(key: String): Value? { + return getInternal(key) + } + + override suspend fun getAllSyncOrNull(): List { + return getAllInternal() + } + + override suspend fun store(key: String, item: Value) { + try { + val json = adapter.toJson(item) + + sharedPreferences.edit { putString(key, json) } + writeTrigger.trigger() + } catch (e: Throwable) { + Timber.e(e, "Unable to edit preferences: $key") + } + } + + override suspend fun store(items: Map) { + items.forEach { (key, item) -> + store(key, item) + } + } + + override suspend fun remove(key: String) { + sharedPreferences.edit { remove(key) } + writeTrigger.trigger() + } + + override suspend fun clear() { + sharedPreferences.edit { clear() } + writeTrigger.trigger() + } + + private fun getInternal(key: String): Value? { + return try { + val json = sharedPreferences.getString(key, null) ?: return null + + adapter.fromJson(json) + } catch (e: Throwable) { + Timber.e(e, "Unable to get value from preferences: $key") + null + } + } + + private fun getAllInternal(): List { + return sharedPreferences.all.mapNotNull { (key, value) -> + try { + val json = value as? String ?: return@mapNotNull null + + adapter.fromJson(json) + } catch (e: Throwable) { + Timber.e(e, "Unable to convert value from JSON: $key") + null + } + } + } +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/datastore/core/DataStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/datastore/core/DataStore.kt index 7374c56980..4ac5668e44 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/datastore/core/DataStore.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/datastore/core/DataStore.kt @@ -6,8 +6,12 @@ internal interface DataStore { fun get(key: Key): Flow + fun getAll(): Flow> + suspend fun getSyncOrNull(key: Key): Value? + suspend fun getAllSyncOrNull(): List + suspend fun store(key: Key, item: Value) suspend fun store(items: Map) 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 new file mode 100644 index 0000000000..3d567967cf --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/local/datastore/core/KeylessDataStoreDecorator.kt @@ -0,0 +1,28 @@ +package com.tangem.datasource.local.datastore.core + +import kotlinx.coroutines.flow.Flow + +internal abstract class KeylessDataStoreDecorator( + wrappedDataStore: StringKeyDataStore, +) : StringKeyDataStoreDecorator(wrappedDataStore) { + + override fun provideStringKey(key: Unit): String { + return STRING_KEY + } + + fun get(): Flow { + return get(Unit) + } + + suspend fun getSyncOrNull(): Value? { + return getSyncOrNull(Unit) + } + + suspend fun store(item: Value) { + store(Unit, item) + } + + private companion object { + const val STRING_KEY = "key" + } +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/datastore/core/StringKeyDataStoreDecorator.kt b/core/datasource/src/main/java/com/tangem/datasource/local/datastore/core/StringKeyDataStoreDecorator.kt index 92daa1a731..e6afee1d93 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/datastore/core/StringKeyDataStoreDecorator.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/datastore/core/StringKeyDataStoreDecorator.kt @@ -3,34 +3,42 @@ package com.tangem.datasource.local.datastore.core import kotlinx.coroutines.flow.Flow internal abstract class StringKeyDataStoreDecorator( - private val dataStore: StringKeyDataStore, + private val wrappedDataStore: StringKeyDataStore, ) : DataStore { abstract fun provideStringKey(key: Key): String override fun get(key: Key): Flow { - return dataStore.get(provideStringKey(key)) + return wrappedDataStore.get(provideStringKey(key)) + } + + override fun getAll(): Flow> { + return wrappedDataStore.getAll() + } + + override suspend fun getAllSyncOrNull(): List { + return wrappedDataStore.getAllSyncOrNull() } override suspend fun getSyncOrNull(key: Key): Value? { - return dataStore.getSyncOrNull(provideStringKey(key)) + return wrappedDataStore.getSyncOrNull(provideStringKey(key)) } override suspend fun store(key: Key, item: Value) { - dataStore.store(provideStringKey(key), item) + wrappedDataStore.store(provideStringKey(key), item) } override suspend fun store(items: Map) { - dataStore.store( + wrappedDataStore.store( items = items.mapKeys { (key, _) -> provideStringKey(key) }, ) } override suspend fun remove(key: Key) { - dataStore.remove(provideStringKey(key)) + wrappedDataStore.remove(provideStringKey(key)) } override suspend fun clear() { - dataStore.clear() + wrappedDataStore.clear() } } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/datastore/model/WriteTrigger.kt b/core/datasource/src/main/java/com/tangem/datasource/local/datastore/model/WriteTrigger.kt deleted file mode 100644 index aaab7f83df..0000000000 --- a/core/datasource/src/main/java/com/tangem/datasource/local/datastore/model/WriteTrigger.kt +++ /dev/null @@ -1,3 +0,0 @@ -package com.tangem.datasource.local.datastore.model - -internal typealias WriteTrigger = Unit \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/datastore/utils/Trigger.kt b/core/datasource/src/main/java/com/tangem/datasource/local/datastore/utils/Trigger.kt new file mode 100644 index 0000000000..d343129a6f --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/local/datastore/utils/Trigger.kt @@ -0,0 +1,35 @@ +package com.tangem.datasource.local.datastore.utils + +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.FlowCollector +import kotlinx.coroutines.flow.MutableStateFlow + +/** + * Represents a trigger mechanism to emit values on-demand. + * + * This class provides a mechanism to trigger emissions via the [trigger] method. + * + * @property triggerFlow The internal flow that gets toggled to trigger emissions. + */ +internal class Trigger( + private val triggerFlow: MutableStateFlow = MutableStateFlow(value = false), +) : Flow { + + /** + * Collects values emitted by this flow. + * + * Overrides the default collection mechanism to emit a [Unit] value whenever [triggerFlow] changes. + * + * @param collector The collector responsible for handling emitted values. + */ + override suspend fun collect(collector: FlowCollector): Nothing { + triggerFlow.collect { collector.emit(Unit) } + } + + /** + * Triggers an emission. + */ + fun trigger() { + triggerFlow.value = !triggerFlow.value + } +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/quote/DefaultQuotesStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/quote/DefaultQuotesStore.kt new file mode 100644 index 0000000000..78e6ffb72b --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/local/quote/DefaultQuotesStore.kt @@ -0,0 +1,27 @@ +package com.tangem.datasource.local.quote + +import com.tangem.datasource.api.tangemTech.models.QuotesResponse +import com.tangem.datasource.local.datastore.core.StringKeyDataStore +import com.tangem.datasource.local.quote.model.StoredQuote +import com.tangem.domain.tokens.models.CryptoCurrency +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.combine + +internal class DefaultQuotesStore( + private val dataStore: StringKeyDataStore, +) : QuotesStore { + + override fun get(currenciesIds: Set): Flow> { + val flows = currenciesIds.mapNotNull { currencyId -> + dataStore.get(currencyId.rawCurrencyId ?: return@mapNotNull null) + } + + return combine(flows) { quotes -> quotes.toSet() } + } + + override suspend fun store(response: QuotesResponse) { + response.quotes.forEach { (rawCurrencyId, quote) -> + dataStore.store(rawCurrencyId, StoredQuote(rawCurrencyId, quote)) + } + } +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/quote/QuotesStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/quote/QuotesStore.kt new file mode 100644 index 0000000000..f34d0039b8 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/local/quote/QuotesStore.kt @@ -0,0 +1,13 @@ +package com.tangem.datasource.local.quote + +import com.tangem.datasource.api.tangemTech.models.QuotesResponse +import com.tangem.datasource.local.quote.model.StoredQuote +import com.tangem.domain.tokens.models.CryptoCurrency +import kotlinx.coroutines.flow.Flow + +interface QuotesStore { + + fun get(currenciesIds: Set): Flow> + + suspend fun store(response: QuotesResponse) +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/quote/model/StoredQuote.kt b/core/datasource/src/main/java/com/tangem/datasource/local/quote/model/StoredQuote.kt new file mode 100644 index 0000000000..92397d1a81 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/local/quote/model/StoredQuote.kt @@ -0,0 +1,9 @@ +package com.tangem.datasource.local.quote.model + +import com.squareup.moshi.Json +import com.tangem.datasource.api.tangemTech.models.QuotesResponse + +data class StoredQuote( + @Json(name = "rawCurrencyId") val rawCurrencyId: String, + @Json(name = "quote") val quote: QuotesResponse.Quote, +) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/userwallet/UserWalletsStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/userwallet/UserWalletsStore.kt index a42253ee50..c905063cc3 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/userwallet/UserWalletsStore.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/userwallet/UserWalletsStore.kt @@ -5,5 +5,7 @@ import com.tangem.domain.wallets.models.UserWalletId interface UserWalletsStore { + val selectedUserWalletOrNull: UserWallet? + suspend fun getSyncOrNull(key: UserWalletId): UserWallet? } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/walletmanager/DefaultWalletManagersStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/walletmanager/DefaultWalletManagersStore.kt index bed4e6a55c..36bc7723c2 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/walletmanager/DefaultWalletManagersStore.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/walletmanager/DefaultWalletManagersStore.kt @@ -5,7 +5,7 @@ import com.tangem.blockchain.common.WalletManager import com.tangem.datasource.local.datastore.core.StringKeyDataStore import com.tangem.datasource.local.datastore.core.StringKeyDataStoreDecorator import com.tangem.domain.wallets.models.UserWalletId -import com.tangem.utils.extensions.plusOrReplace +import com.tangem.utils.extensions.addOrReplace internal class DefaultWalletManagersStore( dataStore: StringKeyDataStore>, @@ -32,7 +32,7 @@ internal class DefaultWalletManagersStore( val walletManagers = getSyncOrNull(userWalletId) val updatedWalletManagers = walletManagers - ?.plusOrReplace(walletManager) { + ?.addOrReplace(walletManager) { it.wallet.blockchain == walletManager.wallet.blockchain && it.wallet.publicKey == walletManager.wallet.publicKey } diff --git a/core/res/src/main/res/values-de/strings-blockchain.xml b/core/res/src/main/res/values-de/strings-blockchain.xml index c30ba56418..b6c425deeb 100644 --- a/core/res/src/main/res/values-de/strings-blockchain.xml +++ b/core/res/src/main/res/values-de/strings-blockchain.xml @@ -2,7 +2,6 @@ Erhalt der Gebühr fehlgeschlagen Laden Sie %1$s+ %2$s auf um ein Konto zu erstellen - Interner Fehler der Blockchain Minimaler Betrag ist %s Restbestand zu klein Falsche Gebühr diff --git a/core/res/src/main/res/values-de/strings.xml b/core/res/src/main/res/values-de/strings.xml index 93537cff03..0542e0607f 100644 --- a/core/res/src/main/res/values-de/strings.xml +++ b/core/res/src/main/res/values-de/strings.xml @@ -10,6 +10,7 @@ Abbrechen Entfernen Erledigt + Fehler OK Änderungen speichern Absenden @@ -27,12 +28,14 @@ Signiert Details Nutzungsbedingungen + Karte scannen Tippen Sie um den Zugangscode zu ändern Tippen Sie um den Passcode zu ändern Legen Sie die Karte zum Scannen an Tippen um zu signieren Legen Sie die Karte an Der Betrag enthält nicht einige Ihrer Mittel + Ein wallet erstellen Betrag Adresse Die Adresse stimmt mit der Adresse Ihrer Brieftasche überein diff --git a/core/res/src/main/res/values-fr/strings-blockchain.xml b/core/res/src/main/res/values-fr/strings-blockchain.xml index 7a18c99c18..42c3c19b03 100644 --- a/core/res/src/main/res/values-fr/strings-blockchain.xml +++ b/core/res/src/main/res/values-fr/strings-blockchain.xml @@ -2,7 +2,6 @@ Échec de réception des commissions Pour créer un compte, téléchargez %1$s+ %2$s - Erreur interne de la blockchain Le montant minimal est de %s Le reste est trop petit Commission non valide diff --git a/core/res/src/main/res/values-fr/strings.xml b/core/res/src/main/res/values-fr/strings.xml index 529af080aa..3dcb556630 100644 --- a/core/res/src/main/res/values-fr/strings.xml +++ b/core/res/src/main/res/values-fr/strings.xml @@ -10,6 +10,7 @@ Annuler Supprimer Exécuté + Erreur OK Sauvegarder les modifications Envoyer @@ -27,12 +28,14 @@ Signé Référénces Conditions d\'utilisation + Scannez la carte Touchez, pour modifier le code d\'accès Touchez, pour modifier le mot de passe Posez pour scanner Touchez pour signer Posez la carte Le montant n\'inclut pas certains de vos fonds + Créer un portefeuille Somme Adresse L\'adresse est la même que celle de votre portefeuille diff --git a/core/res/src/main/res/values-it/strings-blockchain.xml b/core/res/src/main/res/values-it/strings-blockchain.xml index 92560ee28a..2d50632130 100644 --- a/core/res/src/main/res/values-it/strings-blockchain.xml +++ b/core/res/src/main/res/values-it/strings-blockchain.xml @@ -2,7 +2,6 @@ Impossibile ottenere la commissione Scarica %1$s+ %2$s per creare un account - Errore interno della blockchain L\'importo minimo è di %s L\'importo residuo è molto basso Commissione non valida diff --git a/core/res/src/main/res/values-it/strings.xml b/core/res/src/main/res/values-it/strings.xml index 3c294f6376..e9d1904611 100644 --- a/core/res/src/main/res/values-it/strings.xml +++ b/core/res/src/main/res/values-it/strings.xml @@ -10,6 +10,7 @@ Annulla Rimuovere Fatto + Errore OK Mantieni le modifiche Invia @@ -27,12 +28,14 @@ Firmato Requisiti Termini del servizio + Scansiona carta Avvicina per modificare il codice di accesso Avvicina per modificare la password Avvicina per scansionare Avvicina per firmare Avvicina la carta L\'importo non include alcuni dei tuoi fondi + Crea portafoglio Importo Indirizzo L\'indirizzo corrisponde all\'indirizzo del tuo portafoglio diff --git a/core/res/src/main/res/values-ru/strings-blockchain.xml b/core/res/src/main/res/values-ru/strings-blockchain.xml index 15d04919e5..0e716ae5d1 100644 --- a/core/res/src/main/res/values-ru/strings-blockchain.xml +++ b/core/res/src/main/res/values-ru/strings-blockchain.xml @@ -6,7 +6,6 @@ Из-за ограничений Kaspa в одну транзакцию может поместиться только %1$d UTXO. Это означает, что вы можете отправить только %2$s или меньше. Вам нужно уменьшить сумму. Пополните счет на %1$s+ %2$s, чтобы создать аккаунт Аккаунт получателя не активирован. Отправьте %s или более для активации аккаунта. - Внутренняя ошибка блокчейна Минимальная сумма: %s Сдача слишком мала Неверная комиссия diff --git a/core/res/src/main/res/values-ru/strings.xml b/core/res/src/main/res/values-ru/strings.xml index 7076371f81..ab73d49ede 100644 --- a/core/res/src/main/res/values-ru/strings.xml +++ b/core/res/src/main/res/values-ru/strings.xml @@ -1,13 +1,17 @@ Добавить токен + Валюты Отправляйте только %1$s (%2$s) в сети %3$s на этот адрес. Использование другой сети может привести к утрате средств. + Спасибо за ваш отзыв + Отправлено успешно Обратиться в поддержку Отправить отзыв Эта карта ранее пополнялась и подписывала транзакции. Выведите средства как можно быстрее, если вы получили эту карту из ненадежного источника. Если это ваша карта, то не о чем беспокоиться. Эта функция недоступна в демонстрационном режиме Приложение работает в демонстрационном режиме. Средства на всех счетах ненастоящие. Карта, которую вы отсканировали, является картой разработчика. Не принимайте её в качестве оплаты. + Не удалось отправить письмо Причина: %s Не могу отправить транзакцию Внимание! Валюты на разных сетях имеют разные адреса. Убедитесь, что адрес соответствует сети, в которой вы отправляете средства. @@ -36,14 +40,22 @@ Отключите эту опцию, если не хотите, чтобы эта карта использовалась для сброса кодов доступа на другие карты этого кошелька. Обратите внимание, сброс кода также не будет доступен на этой карте. Использовать эту карту для сброса кода доступа на других картах в этом кошельке + Отключить возможность сброса кода доступа на этой карте или других картах этого кошелька Восстановление кода доступа + Сбросить + Вы уверены, что хотите это сделать? Смена кода доступа Код доступа будет изменен только на данной карте Заводские настройки Тип безопасности + Выбранный способ защиты приложения Настройки карты Tangem Bot Чат + Оценить агента + Отправить логи + Пожалуйста, выберите действие + Пожалуйста, оцените работу агента Принять Добавить Применить @@ -56,6 +68,7 @@ Вы не предоставили доступ к камере, пожалуйста, измените настройки конфиденциальности. Отмена Закрыть + Продолжить Копировать Скопировать адрес Создать @@ -65,12 +78,17 @@ Готово Включить Включено + Ошибка Обменять Посмотреть историю транзакций Обозреватель + Сгенерировать адреса + Импортировать Нравится + Заблокирован Основная сеть Нет + Нет адреса Нет данных OK Основная карта @@ -79,7 +97,9 @@ Перезагрузить Сохранить изменения Искать + Поиск токенов Секретная фраза + Выберите действие Продать Отправить Сервер недоступен, повторите попытку позднее @@ -99,6 +119,7 @@ Да Адрес контракта скопирован! Доступные сети + Добавить токен Адрес контракта Адрес контракта некорректен Путь деривации некорректен @@ -138,7 +159,6 @@ Проверьте подключение с интернетом или переключитесь на другую сеть Условия использования К сожалению, текущая версия приложения не готова к работе с этой картой, проверьте наличие обновлений - Данное приложение не предназначено для работы с этой картой или требует обновления Вы использовали карту от другого кошелька. Приложите карту, связанную с этим кошельком. Вы получаете Вы отправляете @@ -153,10 +173,13 @@ Обращение в поддержку Tangem Не могу отправить транзакцию Купить + Сканировать Чтобы изменить код доступа, приложите карту как показано выше и не убирайте до окончания операции Чтобы изменить пароль, приложите карту как показано выше и не убирайте до окончания операции Чтобы создать кошелек, приложите карту как показано выше и не убирайте до окончания операции + Чтобы сбросить настройки до заводских, приложите карту как показано выше и не убирайте до окончания операции Приложите, чтобы отсканировать + Чтобы подписать транзакцию, приложите карту как показано выше и не убирайте до окончания операции Приложите, чтобы подписать Приложите карту Внутренняя ошибка: не удается найти менеджер кошельков @@ -174,9 +197,16 @@ Баланс В сумме учтены не все монеты 1INCH токены будут зачислены на адрес вашего кошелька в сети %s в течение 48 часов - По вашему промокоду не было покупки кошелька, а значит вы не можете получить бонус. Купите кошелек Tangem, отсканируйте его в приложении и получите бонус. Чтобы получить доступ ко всем сетям, вам необходимо отсканировать карту Отсканируйте карту + Токены + + Вам надо сгенерировать адрес для %d новой сети, используя вашу карту + Вам надо сгенерировать адреса для %d новых сетей, используя вашу карту + Вам надо сгенерировать адреса для %d новых сетей, используя вашу карту + Вам надо сгенерировать адреса для %d новых сетей, используя вашу карту + + Некоторые адреса отсутствуют Вам необходимо установить единый код доступа для защиты всех ваших карт Защита Позже вы сможете установить индивидуальный код доступа для каждой карты @@ -218,7 +248,7 @@ Код доступа Подключиться Резервная копия - Читать про секретную фразу + Прочитать о секретной фразе Запишите эти 12 слов в порядке, указанном ниже, и сохраните их в надежном месте. Ваша секретная фраза Чтобы импортировать кошелек, введите секретную фразу в поле ниже @@ -236,7 +266,7 @@ Чтобы начать процесс резервного копирования, добавьте одну или две резервные карты. Вы можете добавить еще одну карту или завершить процесс резервного копирования Подготовьте резервную карту с номером %s - Подготовьте основную карту + Отсканируйте основную карту, чтобы начать процесс резервного копирования. Подготовьте основную карту с номером %s Поздравляем! Ваша платежная крипто карта теперь активирована! Ваша карта настроена и готова к использованию. @@ -254,7 +284,7 @@ Пополните кошелек более чем на %1$s %2$s, чтобы начать пользоваться картой Купить криптовалюту Показать адрес кошелька - Пополните свой кошелек + Активация кошелька Процесс связывания карт частично завершен. Вы не можете выйти из него сейчас. Если процесc создания кошелька каким-либо образом прервется, вам придется начинать сначала Вы можете сделать резервную копию своих ключей на одной или двух других пустых картах Wallet. @@ -274,12 +304,23 @@ Участвовать Не удалось загрузить информацию по реферальной программе. Пожалуйста, попробуйте позже. Не удалось загрузить информацию по реферальной программе. Код ошибки: %s. Пожалуйста, попробуйте позже. + Грядущие выплаты Ваши друзья купили + Меньше + Больше + Нет грядущих выплат + + за %d кошелек + за %d кошелька + за %d кошельков + за %d кошельков + + Получите ^^%1$s^^ на ваш адрес в сети %2$s %3$s ^^спустя 30 дней^^ за каждый кошелек, который купит ваш друг Получите на ваш адрес в сети %1$s%2$s за каждый кошелек, который купит ваш друг Вы Получит - при покупке карточки на сайте tangem.com + при покупке кошелька на сайте tangem.com %s скидку Ваш друг Персональный код скопирован! @@ -315,7 +356,6 @@ Сканировать Отсканируйте карту, чтобы изменить ее настройки. Изменения затронут только ту карту, которую вы отсканировали, и не повлияют на другие карты, привязанные к вашему кошельку. Приготовьте свою карту - Поиск валют Сумма Адрес Адрес совпадает с адресом кошелька @@ -341,7 +381,7 @@ У меня есть промо-код… Tangem Wallet Другие способы оплаты - Из-за высокого количества заказов, которые мы получаем, доставка может быть задержана на срок до 5 недель в зависимости от вашего местоположения + Сделать предзаказ Итого Сеть Solana взимает арендную плату в размере %1$s каждые 2 дня. Аккаунты, которые не могут позволить себе арендную плату, удаляются из сети. Пополните свой счет более чем на %2$s, чтобы не платить арендную плату. Держите свои криптосбережения в безопасности. Приватные ключи надежно хранятся на карте. @@ -413,12 +453,6 @@ Токен %1$s является основной валютой в сети %2$s и не может быть скрыт до тех пор, пока у вас в списке есть другие токены этой сети. Невозможно скрыть %s Нет цены - - %d токен - %d токена - %d токенов - %d токенов - контракт: %s У вас еще нет транзакций Не удалось загрузить историю транзакций.\nНажмите на кнопку перезагрузки, чтобы обновить информацию. @@ -427,6 +461,7 @@ на: %s В процессе… Вы отсканировали ту же карту. Для создания twin-кошелька вам необходимо отсканировать карту с номером %d + Вы отсканировали не ту twin-карту. Пожалуйста, попробуйте отсканировать другую Это карта, которую вы держите в руках. У парной карты номер %s.\n\nОбе карты можно использовать для вывода средств из этого кошелька. Один кошелек. Две карты. Сканировать карту #%s @@ -435,6 +470,7 @@ Подготовка карты Tangem Twin Это действие необратимо. У вас не будет доступа к старому кошельку. + Приложите twin-карту с номером %s и не убирайте до окончания операции Добавить новый кошелек Вы уверены, что хотите удалить этот кошелек? %d выбрано @@ -454,6 +490,7 @@ Транзакция подтверждается… Подтвержденный баланс Действия + Что отправить? Вы хотите купить или продать криптовалюту? Запрос на подпись сообщения.\n\n%s Dapp %1$s, запрос на\nподпись транзакции с BNB.\n\n%2$s @@ -475,7 +512,10 @@ Произошла непредвиденная ошибка. Код ошибки: %d Попробуйте, пожалуйста, позже. Если проблема будет продолжать возникать - обратитесь в службу поддержки. Сообщение было успешно подписано и отправлено в Dapp Сеть %s не найдена. Пожалуйста, добавьте её и попробуйте заново. + Нет открытых сессий WalletConnect + Упс. Нет сессий. Вставить из буфера обмена + Сообщение для %1$s:\n%2$s Запрос на открытие сессии для\n%1$s\n\nСЕТЬ: %2$s\n\nURL: %3$s Операция не может быть завершена.\n\nВы уже установили сеанс WalletConnect с этими параметрами. Сканировать новый код @@ -483,6 +523,8 @@ Сеть не поддерживается. Пожалуйста, выберите другую сеть. Выберите сеть Dapp не предоставил необходимые данные для открытия сессии WalletConnect + Не удалось найти сессию для обработки запроса + Сессии WalletConnect Подключение к Dapps WalletConnect Транзакция успешно подписана и отправлена ​​в Dapp diff --git a/core/res/src/main/res/values-zh-rTW/strings-blockchain.xml b/core/res/src/main/res/values-zh-rTW/strings-blockchain.xml index 67f050ffa1..5aa30cef8f 100644 --- a/core/res/src/main/res/values-zh-rTW/strings-blockchain.xml +++ b/core/res/src/main/res/values-zh-rTW/strings-blockchain.xml @@ -6,7 +6,6 @@ 由於 Kaspa 的限制,只有%1$d UTXO 可以放入單次交易中。這意味著您只能發送%2$s或更少數量。您需要減少數量。 加載 %1$s+ %2$s 以創建帳戶 目標帳戶未激活。發送 %s 或更多以激活帳戶 - 區塊鏈內部錯誤 最小數量是 %s 更動太小 無效費用 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 0b2d148627..748251aefa 100644 --- a/core/res/src/main/res/values-zh-rTW/strings.xml +++ b/core/res/src/main/res/values-zh-rTW/strings.xml @@ -1,13 +1,17 @@ 添加自定義代幣 + 管理代幣 僅將 %1$s (%2$s) 從 %3$s 網絡發送到此地址。使用其他代幣和網絡可能會導致資金損失 + 謝謝您的反饋 + 成功送出 請求支持 發送反饋 此卡過去已經充值並簽署過交易。如果您從不受信任的來源收到此卡,請考慮立即提取所有資金。如果是您的卡,則無需擔心 此功能不在展示模式中提供 您正在展示模式。所有資產皆不是真的 您掃描的卡是開發卡。不要用它作為付款方式 + 發送電子郵件失敗 原因:%s 無法發送交易 請注意代幣在不同的網路有不同的地址。請再次檢查正確的地址 @@ -32,14 +36,22 @@ 如果您不希望使用此卡重置此錢包中其他卡上的訪問密碼,請禁用此選項。請注意,這也會阻止您重置此卡上的訪問密碼。 允許您使用此卡重置此錢包中其他卡上的訪問密碼 + 禁用重置此卡或此錢包中其他卡上的訪問密碼的功能 恢復訪問密碼 + 重置 + 您確定要這麼做嗎? 更改訪問密碼 訪問密碼將僅在此卡上更改 回復至原廠設置 安全模式 + 選定的應用程序保護方法 卡片設置 Tangem 機器人 支援 + 請評價 + 發送logs + 請選擇一個操作 + 請評價我們的服務 接受 添加 注意 @@ -50,6 +62,7 @@ 您尚未授予相機訪問權限,請更改您的隱私設置 刪除 關閉 + 繼續 複製 複製地址 創造 @@ -59,7 +72,9 @@ 完成 允許 啟用 + 錯誤 交易 + 導入 喜歡 OK @@ -67,6 +82,8 @@ 拒絕 保存設置 搜索 + 搜尋代幣 + 選擇 銷售 發送 伺服器不可用,請稍後在試 @@ -84,6 +101,7 @@ 已複製代幣地址 支持的網路 + 添加代幣 代幣地址 無效地址 衍生路徑錯誤 @@ -135,10 +153,13 @@ Tangem反饋 無法發送交易 訂購 + 掃描卡片 要更改訪問密碼,請完全按照上圖所示連接手機和卡片 要更改密碼,請完全按照上圖所示連接手機和卡 要創建錢包,請完全按照上圖所示連接手機和卡 + 要重置為出廠設置,請完全按照上圖所示連接手機和卡片 點擊掃描 + 要簽名,請完全按照上圖所示連接手機和卡 點擊簽名 點按卡片 內部錯誤:找不到錢包管理器 @@ -150,6 +171,7 @@ 該金額不包括您的部分資金 要訪問所有的網路您需要掃描卡片 掃描卡片 + 代幣 您必須設置一個單一的訪問代碼來保護您的所有錢包 保護 您可以稍後在每張卡上設置單獨的訪問密碼 @@ -187,7 +209,7 @@ 這此情況,您必須要重新開始 您想要離開啟用程序嗎? 開始 - 您要添加的卡上已經創建了另一個錢包。你想重置它並將卡用於新錢包嗎 + 您要添加的卡上已經創建了另一個錢包。你想重置它並將卡用於新錢包嗎? PIN 碼 連接 創建備份 @@ -209,7 +231,7 @@ 要開始備份過程,最多可添加兩張備份卡。 您可以再添加一張卡或完成備份過程 準備編號為 %s 的備份卡 - 準備主卡 + 掃描主卡以啟動備份過程 準備編號為 %s 的主卡 恭喜! 您的第一張支付加密卡已啟用! 您的錢包卡已配置完畢,可以使用了 @@ -227,7 +249,7 @@ 要開始,只需為錢包充值超過 %1$s %2$s 購買加密貨幣 顯示錢包地址 - 充值你的錢包 + 啟動錢包 結對過程已部分完成。你現在不能退出 如果創建錢包的過程以任何方式中斷,您將得重新開始 您最多可以額外備份兩張空白的 Tangem冷錢包 @@ -246,7 +268,6 @@ 對於你的朋友在你的 %1$s 網絡地址%2$s上購買的每個錢包 得到 - 當在 tangem.com購買卡片 %s 折扣 你的朋友 個人促銷碼已複製! @@ -279,7 +300,6 @@ 掃描卡片 掃描卡片以更改其設置。這些更改只會影響您掃描過的卡,不會影響綁定到您錢包的其他卡。 準備好您的卡! - 搜尋代幣 數量 地址 地址與錢包地址相同 @@ -366,13 +386,11 @@ %1$s 代幣是 %2$s 網絡上的主要貨幣,只要列表中還有該網絡上的其他代幣,它就無法被隱藏。 無法隱藏 %s 無費用 - - %d 代幣 - 您還沒有任何交易 無法加載交易 進行中… 您掃描了同一張卡片。要創建雙錢包,您需要掃描編號為 %d 的卡 + 你掃描錯了胞胎卡。請嘗試另一個 這一個是你手裡拿著的,另一個是編號為 %s 的,這兩張卡都可以用來從這個錢包中提取資金 一個錢包,兩張卡片 掃描卡片 #%s @@ -381,6 +399,7 @@ 準備卡片 Tangem Twin 這個動作是不可逆的。您將無法訪問舊錢包 + 將您的 iPhone 靠近編號為 %s 的雙胞胎卡 添加新錢包 您確定要刪除此錢包? 已選擇 %d @@ -400,6 +419,7 @@ 交易進行中 檢視餘額 動作 + 選擇錢包選項 您想要購買或賣出交易貨幣? 請求籤署消息。%s Dapp %1$s,請求\n簽署 BNB 交易。\n%2$s @@ -421,7 +441,10 @@ 我們遇到了未知錯誤。錯誤代碼:%d。如果問題仍然存在-請隨時聯繫我們的支持人員 消息已成功簽名並發送至Dapp 沒有 %s 網路,請先加入後再試一次 + 沒有已連結的WalletConnect + Ooops, 沒有連接 從剪貼板貼上 + 給 %1$s 的消息:%2$s 請求開始會話\n%1$s\n\n網絡: %2$s\n\n網址:%3$s 無法完成執行,您已經使用此參數建立了 WalletConnect 連接 掃描新密碼 @@ -429,6 +452,8 @@ 不支持此網絡。請選擇其他網絡 選擇網路 Dapp 沒有提供必要的數據來建立 WalletConnect 連接 + 找不到請求的連接 + WalletConnect 連接 連結到Dapps WalletConnect 交易已成功簽署並發送至 Dapp diff --git a/core/res/src/main/res/values/strings-blockchain.xml b/core/res/src/main/res/values/strings-blockchain.xml index 4e305a3ce1..41209dd8f9 100644 --- a/core/res/src/main/res/values/strings-blockchain.xml +++ b/core/res/src/main/res/values/strings-blockchain.xml @@ -6,7 +6,6 @@ Due to Kaspa limitations only %1$d UTXOs can fit in a single transaction. This means you can only send %2$s or less. You need to reduce the amount. Load %1$s+ %2$s to create account Destination account is not active. Send %s or more to activate the account. - Blockchain internal error Minimum amount is %s Change is too small Invalid Fee diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index e207afb74d..f1471f6745 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -1,13 +1,17 @@ Add custom token + Manage tokens Send only %1$s (%2$s) from %3$s network to this address. Using other tokens and networks may result in loss of funds. + Thank you for your feedback + Sent successfully Request support Send feedback This card has been already topped up and signed transactions in the past. Consider immediate withdrawal of all funds if you have received this card from an untrusted source. If it\'s your card, there is nothing to worry about. This feature is disabled in Demo mode You are currently running in Demo mode. All funds are not real. The card you scanned is a development card. Don\'t accept it as a payment. + Failed to send the email Reason: %s Can\'t send a transaction Note that tokens on different networks have different addresses. Double check that your address matches the network when you transfer funds. @@ -34,14 +38,22 @@ Disable this option if you don\'t want this card to be used to reset access codes on other cards in this wallet. Please note that this will also prevent you from resetting the access code on this card. 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 Reset to Factory Settings Security Mode + Selected application protection method Card Settings Tangem Bot Support + Rate agent + Send logs + Please select an action + Please, rate the work of the agent Accept Add Apply @@ -54,6 +66,7 @@ You have not given access to your camera, please adjust your privacy settings Cancel Close + Continue Copy Copy address Create @@ -63,13 +76,18 @@ Done Enable Enabled + Error Exchange Explore transaction history Explorer + Generate addresses + Import Learn & Earn Like + Locked Main network No + No address No data OK Primary Card @@ -78,7 +96,9 @@ Reload Save changes Search + Search tokens Seed phrase + Select action Sell Send The server is not available, please try again later @@ -98,6 +118,7 @@ Yes Contract address copied! Available networks + Add token Contract address Contract address is invalid Derivation path is invalid @@ -137,7 +158,6 @@ Check your internet connection or switch to a different network Terms of Service Oops, the current version of the application is not ready to work with this card, please check for updates. - This application is not designed to work with this card or needs to be updated You have used a card from another wallet. Tap the card associated with this wallet You Receive You Send @@ -152,14 +172,18 @@ Tangem feedback Can\'t send a transaction Order + Scan card To change the access code tap the card as shown above and do not remove until the end of the operation To change the passcode tap the card as shown above and do not remove until the end of the operation To create the wallet tap the card as shown above and do not remove until the end of the operation + To reset to factory settings tap the card as shown above and do not remove until the end of the operation Tap to scan + To sign tap the card as shown above and do not remove until the end of the operation Tap to sign Tap the card Internal error: wallet manager not found You have updated biometrics, scan your card to enter + To begin tracking your crypto assets and transactions, add tokens. You have completed all of the lessons, and are now eligible to receive your 1INCH tokens Complete three lessons and receive %d 1INCH token to your wallet @@ -171,9 +195,14 @@ Total balance The amount does not include some of your funds 1INCH tokens will be credited to your %s wallet address within 48 hours - There was no purchase of a wallet using your promo code, which means you cannot receive a bonus. Buy Tangem wallet, scan it in the app, and get the bonus. To access all the networks you need to scan the card Scan your card + Tokens + + You need to generate address for %d new network using your card + You need to generate addresses for %d new networks using your card + + Some addresses are missing 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 +262,7 @@ To start the backup process add up to two backup cards. You can add one more card or finalize the backup process Prepare the backup card with number %s - Prepare the primary card + Scan the primary card to start the backup process. Prepare the primary card with number %s Congratulations! Your first payment crypto card has been activated! Your wallet card is configured and ready for use. @@ -251,7 +280,7 @@ To get started, simply top up the wallet with more than %1$s %2$s Buy crypto Show the wallet\'s address - Top up your wallet + Activate a wallet The twinning process is partly complete. You can\'t exit it now. If the process of creating the wallet gets interrupted in any way, you\'ll have to start over You can backup your keys up to two other blank Tangem Wallet cards. @@ -272,12 +301,21 @@ Participate Failed to load the information about the referral program. Please try again later. Failed to load the information about the referral program. Error code: %s. Please try again later. + Upcoming payments Your friends bought + Less + More + No upcoming payments + + for %d wallet + 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 card on tangem.com + when buying a wallet on tangem.com %s discount Your friend Personal code copied! @@ -311,7 +349,6 @@ Scan Card Scan the card to change its settings. The changes will impact only the card you\'ve scanned and will not affect other cards tied to your wallet. Get your card ready! - Search tokens Amount Address Address is the same as wallet address @@ -337,7 +374,7 @@ I have a promo code… Tangem Wallet Other payment methods - Due to the high volume of orders we are receiving shipping may be delayed up to 5 weeks depending on your location + Pre-order now Total Solana network charges a rent of %1$s every 2 days. Accounts that can\'t afford the rent are purged from the network. Deposit your account with more than %2$s to use it for free. Store your crypto assets secure while keeping private keys contained in your card @@ -407,10 +444,6 @@ The %1$s token is the main currency on the %2$s network and cannot be hidden as long as you have other tokens on this network in the list. Unable to hide %s No rate - - %d token - %d tokens - contract: %s You don\'t have any transactions yet Failed to load transaction history.\nClick on reload button to update the information. @@ -419,6 +452,7 @@ to: %s In progress… You\'ve scanned the same card. To create a twin wallet you need to scan the card with number %d + You\'ve scanned wrong twin card. Please try another one This one that you are holding in your hands and the other one with number %s.\n\nBoth cards can be used to extract funds from this wallet. One wallet. Two cards. Scan the card #%s @@ -427,6 +461,7 @@ Preparing card Tangem Twin This action is irreversible. You will not have access to the old wallet. + Tap the twin card with number %s and do not remove until the end of the operation Add new wallet Are you sure you want to delete this wallet? %d selected @@ -446,6 +481,7 @@ Transaction is in progress… Verified Balance Actions + Choose wallet option Do you want to buy or sell crypto? Requesting to sign a message.\n\n%s Dapp %1$s, requesting to\nsign BNB transaction.\n\n%2$s @@ -467,7 +503,10 @@ We\'ve encountered unknown error. Error code: %d. If the problem persists — feel free to contact our support The message has been successfully signed and sent to the Dapp %s network not found. Please, add it first and try again. + No opened WalletConnect sessions + Ooops. No Sessions. Paste from clipboard + Message for %1$s:\n%2$s Request to start a session for\n%1$s\n\nNETWORK: %2$s\n\nURL: %3$s The operation couldn\'t be completed.\n\nYou have already established a WalletConnect session with this parameters. Scan new code @@ -475,6 +514,8 @@ This network is not supported. Please select another network. Select network Dapp didn\'t provide essential data to establish WalletConnect session + Failed to find session for request + WalletConnect Sessions Connect to Dapps WalletConnect The transaction has been successfully signed and sent to the Dapp diff --git a/core/ui/build.gradle.kts b/core/ui/build.gradle.kts index 94bb016bdc..5cabb03840 100644 --- a/core/ui/build.gradle.kts +++ b/core/ui/build.gradle.kts @@ -7,12 +7,14 @@ plugins { dependencies { /** AndroidX libraries */ implementation(deps.androidx.fragment.ktx) + implementation(deps.androidx.paging.runtime) /** Compose */ implementation(deps.compose.constraintLayout) implementation(deps.compose.foundation) implementation(deps.compose.material) implementation(deps.compose.material3) + implementation(deps.compose.paging) implementation(deps.compose.ui.tooling) /** Other libraries */ diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/buttons/HorizontalActionChips.kt b/core/ui/src/main/java/com/tangem/core/ui/components/buttons/HorizontalActionChips.kt index c778c6c0a4..6b20a931f8 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/buttons/HorizontalActionChips.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/buttons/HorizontalActionChips.kt @@ -31,7 +31,11 @@ fun HorizontalActionChips( verticalAlignment = Alignment.CenterVertically, contentPadding = contentPadding, ) { - items(items = buttons, itemContent = { ActionButton(config = it) }) + items( + items = buttons, + key = { config -> "${config.text.hashCode()} ${config.iconResId}" }, + itemContent = { ActionButton(config = it) }, + ) } } diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/buttons/actions/ActionButtonConfig.kt b/core/ui/src/main/java/com/tangem/core/ui/components/buttons/actions/ActionButtonConfig.kt index ae19be321b..6b9347623a 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/buttons/actions/ActionButtonConfig.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/buttons/actions/ActionButtonConfig.kt @@ -10,6 +10,8 @@ import com.tangem.core.ui.extensions.TextReference * @property iconResId icon resource id * @property onClick lambda be invoked when action component is clicked * @property enabled enabled + * @property dimContent determines whether the button content will be dimmed. This property will be ignored if [enabled] + * is `false`. * [REDACTED_AUTHOR] */ @@ -18,4 +20,5 @@ data class ActionButtonConfig( @DrawableRes val iconResId: Int, val onClick: () -> Unit, val enabled: Boolean = true, + val dimContent: Boolean = false, ) \ No newline at end of file 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 af38dad59f..65422a2f7b 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 @@ -99,14 +99,22 @@ private fun Button( painter = painterResource(id = config.iconResId), contentDescription = null, modifier = Modifier.size(size = TangemTheme.dimens.size20), - tint = if (config.enabled) TangemTheme.colors.icon.primary1 else TangemTheme.colors.icon.informative, + tint = when { + !config.enabled -> TangemTheme.colors.icon.informative + config.dimContent -> TangemTheme.colors.icon.secondary + else -> TangemTheme.colors.icon.primary1 + }, ) SpacerW8() Text( text = config.text.resolveReference(), - color = if (config.enabled) TangemTheme.colors.text.primary1 else TangemTheme.colors.text.disabled, + color = when { + !config.enabled -> TangemTheme.colors.text.disabled + config.dimContent -> TangemTheme.colors.text.secondary + else -> TangemTheme.colors.text.primary1 + }, overflow = TextOverflow.Ellipsis, maxLines = 1, style = TangemTheme.typography.button, @@ -154,6 +162,13 @@ private class ActionStateProvider : CollectionPreviewParameterProviderFigma component */ @Composable fun MarketPriceBlock(state: MarketPriceBlockState, modifier: Modifier = Modifier) { var rootWidth by remember { mutableStateOf(value = 0) } + Column( modifier = modifier .background( @@ -42,81 +51,139 @@ fun MarketPriceBlock(state: MarketPriceBlockState, modifier: Modifier = Modifier verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing6), horizontalAlignment = Alignment.Start, ) { - Text( - text = stringResource(id = R.string.wallet_marketplace_block_title, state.currencyName), - color = TangemTheme.colors.text.tertiary, - style = TangemTheme.typography.subtitle2, - ) + Title(currencyName = state.currencyName) - when (state) { - is MarketPriceBlockState.Loading -> { - RectangleShimmer( - modifier = Modifier.size(width = TangemTheme.dimens.size158, height = TangemTheme.dimens.size20), - ) - } - is MarketPriceBlockState.Content -> { - Price( - config = state, + Content(state = state, rootWidth = rootWidth) + } +} + +@Composable +private fun Title(currencyName: String) { + Text( + text = stringResource(id = R.string.wallet_marketplace_block_title, currencyName), + color = TangemTheme.colors.text.tertiary, + style = TangemTheme.typography.subtitle2, + ) +} + +@OptIn(ExperimentalAnimationApi::class) +@Composable +private fun Content(state: MarketPriceBlockState, rootWidth: Int) { + AnimatedContent(targetState = state, label = "Update the content") { marketPriceBlockState -> + when (marketPriceBlockState) { + is MarketPriceBlockState.Content, + is MarketPriceBlockState.Error, + -> { + PriceContent( + state = marketPriceBlockState, priceWidthDp = with(LocalDensity.current) { rootWidth.div(other = 2).toDp() }, ) } + is MarketPriceBlockState.Loading -> LoadingContent() } } } @Composable -private fun Price(config: MarketPriceBlockState.Content, priceWidthDp: Dp) { +private fun PriceContent(state: MarketPriceBlockState, priceWidthDp: Dp) { Row( - modifier = Modifier, verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(space = TangemTheme.dimens.spacing8), ) { - Text( - text = config.price, - modifier = Modifier.widthIn(max = priceWidthDp), - color = TangemTheme.colors.text.primary1, - overflow = TextOverflow.Ellipsis, - maxLines = 1, - style = TangemTheme.typography.body2, - ) + PriceBlock(state = state, priceWidthDp = priceWidthDp) - PriceChangeInPercent(config.priceChangeConfig) + QuoteTimeStatus() + } +} - Text( - text = stringResource(id = R.string.wallet_marketprice_block_update_time), - color = TangemTheme.colors.text.tertiary, - style = TangemTheme.typography.body2, - ) +@OptIn(ExperimentalAnimationApi::class) +@Composable +private fun PriceBlock(state: MarketPriceBlockState, priceWidthDp: Dp) { + val priceModifier = Modifier.widthIn(max = priceWidthDp) + AnimatedContent(targetState = state, label = "Update the price block") { marketPriceBlockState -> + if (marketPriceBlockState is MarketPriceBlockState.Content) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(space = TangemTheme.dimens.spacing8), + ) { + Price(price = marketPriceBlockState.price, modifier = priceModifier) + + PriceChangeInPercent(marketPriceBlockState.priceChangeConfig) + } + } else { + Price(price = BigDecimalFormatter.EMPTY_BALANCE_SIGN, modifier = priceModifier) + } } } +@Composable +private fun Price(price: String, modifier: Modifier = Modifier) { + Text( + text = price, + modifier = modifier, + color = TangemTheme.colors.text.primary1, + overflow = TextOverflow.Ellipsis, + maxLines = 1, + style = TangemTheme.typography.body2, + ) +} + +@OptIn(ExperimentalAnimationApi::class) @Composable private fun PriceChangeInPercent(config: PriceChangeConfig) { + AnimatedContent(targetState = config.type, label = "Update price change") { type -> + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(space = TangemTheme.dimens.spacing4), + ) { + Image( + painter = painterResource( + id = when (type) { + PriceChangeConfig.Type.UP -> R.drawable.img_arrow_up_8 + PriceChangeConfig.Type.DOWN -> R.drawable.img_arrow_down_8 + }, + ), + contentDescription = null, + ) + + Text( + text = config.valueInPercent, + color = when (type) { + PriceChangeConfig.Type.UP -> TangemTheme.colors.text.accent + PriceChangeConfig.Type.DOWN -> TangemTheme.colors.text.warning + }, + style = TangemTheme.typography.body2, + ) + } + } +} + +@Composable +private fun LoadingContent() { Row( verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(space = TangemTheme.dimens.spacing4), + horizontalArrangement = Arrangement.spacedBy(space = TangemTheme.dimens.spacing8), ) { - Image( - painter = painterResource( - id = when (config.type) { - PriceChangeConfig.Type.UP -> R.drawable.img_arrow_up_8 - PriceChangeConfig.Type.DOWN -> R.drawable.img_arrow_down_8 - }, + RectangleShimmer( + modifier = Modifier.size( + width = TangemTheme.dimens.size158, + height = TangemTheme.dimens.size20, ), - contentDescription = null, ) - Text( - text = config.valueInPercent, - color = when (config.type) { - PriceChangeConfig.Type.UP -> TangemTheme.colors.text.accent - PriceChangeConfig.Type.DOWN -> TangemTheme.colors.text.warning - }, - style = TangemTheme.typography.body2, - ) + QuoteTimeStatus() } } +@Composable +private fun QuoteTimeStatus() { + Text( + text = stringResource(id = R.string.wallet_marketprice_block_update_time), + color = TangemTheme.colors.text.tertiary, + style = TangemTheme.typography.body2, + ) +} + @Preview @Composable private fun Preview_MarketPriceBlock_Light( @@ -143,7 +210,7 @@ private class WalletMarketPriceBlockStateProvider : CollectionPreviewParameterPr collection = listOf( MarketPriceBlockState.Content( currencyName = "BTC", - price = "98900", + price = "98900 $", priceChangeConfig = PriceChangeConfig( valueInPercent = "5.16%", type = PriceChangeConfig.Type.DOWN, @@ -151,12 +218,13 @@ private class WalletMarketPriceBlockStateProvider : CollectionPreviewParameterPr ), MarketPriceBlockState.Content( currencyName = "BTC", - price = "98900", + price = "98900 $", priceChangeConfig = PriceChangeConfig( valueInPercent = "10.89%", type = PriceChangeConfig.Type.UP, ), ), MarketPriceBlockState.Loading(currencyName = "BTC"), + MarketPriceBlockState.Error(currencyName = "BTC"), ), ) \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/marketprice/MarketPriceBlockState.kt b/core/ui/src/main/java/com/tangem/core/ui/components/marketprice/MarketPriceBlockState.kt index ae59401aa9..30b652ea68 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/marketprice/MarketPriceBlockState.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/marketprice/MarketPriceBlockState.kt @@ -7,6 +7,8 @@ sealed interface MarketPriceBlockState { val currencyName: String + data class Error(override val currencyName: String) : MarketPriceBlockState + data class Loading(override val currencyName: String) : MarketPriceBlockState data class Content( diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/transactions/Transaction.kt b/core/ui/src/main/java/com/tangem/core/ui/components/transactions/Transaction.kt index 1257a022a7..9256ad0ccc 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/transactions/Transaction.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/transactions/Transaction.kt @@ -21,7 +21,9 @@ import androidx.constraintlayout.compose.Dimension import com.tangem.core.ui.R import com.tangem.core.ui.components.CircleShimmer import com.tangem.core.ui.components.RectangleShimmer +import com.tangem.core.ui.components.transactions.state.TransactionState import com.tangem.core.ui.res.TangemTheme +import java.util.UUID /** * Transaction component @@ -311,45 +313,53 @@ private fun Preview_TransactionItem_DarkTheme( private class TransactionItemStateProvider : CollectionPreviewParameterProvider( collection = listOf( TransactionState.Sending( + txHash = UUID.randomUUID().toString(), address = "33BddS...ga2B", amount = "-0.500913 BTC", timestamp = "8:41", ), TransactionState.Receiving( + txHash = UUID.randomUUID().toString(), address = "33BddS...ga2B", amount = "+0.500913 BTC", timestamp = "8:41", ), TransactionState.Approving( + txHash = UUID.randomUUID().toString(), address = "33BddS...ga2B", amount = "+0.500913 BTC", timestamp = "8:41", ), TransactionState.Swapping( + txHash = UUID.randomUUID().toString(), address = "33BddS...ga2B", amount = "+0.500913 BTC", timestamp = "8:41", ), TransactionState.Send( + txHash = UUID.randomUUID().toString(), address = "33BddS...ga2B", amount = "-0.500913 BTC", timestamp = "8:41", ), TransactionState.Receive( + txHash = UUID.randomUUID().toString(), address = "33BddS...ga2B", amount = "+0.500913 BTC", timestamp = "8:41", ), TransactionState.Approved( + txHash = UUID.randomUUID().toString(), address = "33BddS...ga2B", amount = "+0.500913 BTC", timestamp = "8:41", ), TransactionState.Swapped( + txHash = UUID.randomUUID().toString(), address = "33BddS...ga2B", amount = "+0.500913 BTC", timestamp = "8:41", ), - TransactionState.Loading, + TransactionState.Loading(txHash = UUID.randomUUID().toString()), ), ) \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/singlecurrency/SingleCurrencyContent.kt b/core/ui/src/main/java/com/tangem/core/ui/components/transactions/TransactionList.kt similarity index 58% rename from features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/singlecurrency/SingleCurrencyContent.kt rename to core/ui/src/main/java/com/tangem/core/ui/components/transactions/TransactionList.kt index 3684c90d62..6be91c66ae 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/singlecurrency/SingleCurrencyContent.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/transactions/TransactionList.kt @@ -1,5 +1,6 @@ -package com.tangem.feature.wallet.presentation.wallet.ui.components.singlecurrency +package com.tangem.core.ui.components.transactions +import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyListScope @@ -8,42 +9,42 @@ import androidx.paging.compose.LazyPagingItems import androidx.paging.compose.itemsIndexed import com.tangem.core.ui.components.transactions.empty.EmptyTransactionBlock import com.tangem.core.ui.components.transactions.empty.EmptyTransactionsBlockState +import com.tangem.core.ui.components.transactions.state.TxHistoryState +import com.tangem.core.ui.decorations.roundedShapeItemDecoration import com.tangem.core.ui.res.TangemTheme -import com.tangem.feature.wallet.presentation.wallet.state.content.WalletTxHistoryState -import com.tangem.feature.wallet.presentation.wallet.ui.decorations.walletContentItemDecoration /** - * LazyList extension for transactions history [WalletTxHistoryState] + * LazyList extension for transactions history [TxHistoryState] * * @param state state * @param txHistoryItems transactions * @param modifier modifier */ -internal fun LazyListScope.txHistoryItems( - state: WalletTxHistoryState, - txHistoryItems: LazyPagingItems?, +fun LazyListScope.txHistoryItems( + state: TxHistoryState, + txHistoryItems: LazyPagingItems?, modifier: Modifier = Modifier, ) { when (state) { - is WalletTxHistoryState.ContentState -> { + is TxHistoryState.ContentState -> { contentItems( txHistoryItems = requireNotNull(txHistoryItems), modifier = modifier, ) } - is WalletTxHistoryState.Empty -> { + is TxHistoryState.Empty -> { nonContentItem( state = EmptyTransactionsBlockState.Empty(onClick = state.onBuyClick), modifier = modifier, ) } - is WalletTxHistoryState.Error -> { + is TxHistoryState.Error -> { nonContentItem( state = EmptyTransactionsBlockState.FailedToLoad(onClick = state.onReloadClick), modifier = modifier, ) } - is WalletTxHistoryState.NotSupported -> { + is TxHistoryState.NotSupported -> { nonContentItem( state = EmptyTransactionsBlockState.NotImplemented(onClick = state.onExploreClick), modifier = modifier, @@ -52,32 +53,42 @@ internal fun LazyListScope.txHistoryItems( } } +@OptIn(ExperimentalFoundationApi::class) private fun LazyListScope.contentItems( - txHistoryItems: LazyPagingItems, + txHistoryItems: LazyPagingItems, modifier: Modifier = Modifier, ) { itemsIndexed( items = txHistoryItems, - key = { index, _ -> index }, - itemContent = { index, item -> - if (item == null) return@itemsIndexed + key = { _, item -> + when (item) { + is TxHistoryState.TxHistoryItemState.GroupTitle -> item.title + is TxHistoryState.TxHistoryItemState.Title -> item.onExploreClick.hashCode() + is TxHistoryState.TxHistoryItemState.Transaction -> item.state.txHash + } + }, + ) { index, item -> + if (item == null) return@itemsIndexed - SingleCurrencyContentItem( - state = item, - modifier = modifier.walletContentItemDecoration( + TxHistoryListItem( + state = item, + modifier = modifier + .animateItemPlacement() + .roundedShapeItemDecoration( currentIndex = index, lastIndex = txHistoryItems.itemSnapshotList.lastIndex, ), - ) - }, - ) + ) + } } +@OptIn(ExperimentalFoundationApi::class) private fun LazyListScope.nonContentItem(state: EmptyTransactionsBlockState, modifier: Modifier = Modifier) { - item { + item(key = state::class.java, contentType = state::class.java) { EmptyTransactionBlock( state = state, modifier = modifier + .animateItemPlacement() .padding(horizontal = TangemTheme.dimens.spacing16, vertical = TangemTheme.dimens.spacing12) .fillMaxWidth(), ) diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/transactions/TxHistoryContentItem.kt b/core/ui/src/main/java/com/tangem/core/ui/components/transactions/TxHistoryContentItem.kt new file mode 100644 index 0000000000..6f918f3cad --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/transactions/TxHistoryContentItem.kt @@ -0,0 +1,20 @@ +package com.tangem.core.ui.components.transactions + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.tangem.core.ui.components.transactions.state.TxHistoryState + +@Composable +internal fun TxHistoryListItem(state: TxHistoryState.TxHistoryItemState, modifier: Modifier = Modifier) { + when (state) { + is TxHistoryState.TxHistoryItemState.GroupTitle -> { + TxHistoryGroupTitle(config = state, modifier = modifier) + } + is TxHistoryState.TxHistoryItemState.Title -> { + TxHistoryTitle(config = state, modifier = modifier) + } + is TxHistoryState.TxHistoryItemState.Transaction -> { + Transaction(state = state.state, modifier = modifier) + } + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/singlecurrency/TxHistoryGroupTitle.kt b/core/ui/src/main/java/com/tangem/core/ui/components/transactions/TxHistoryGroupTitle.kt similarity index 88% rename from features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/singlecurrency/TxHistoryGroupTitle.kt rename to core/ui/src/main/java/com/tangem/core/ui/components/transactions/TxHistoryGroupTitle.kt index 275f2fce6d..c7eaaa7978 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/singlecurrency/TxHistoryGroupTitle.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/transactions/TxHistoryGroupTitle.kt @@ -1,4 +1,4 @@ -package com.tangem.feature.wallet.presentation.wallet.ui.components.singlecurrency +package com.tangem.core.ui.components.transactions import androidx.compose.foundation.background import androidx.compose.foundation.layout.fillMaxWidth @@ -8,8 +8,8 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview +import com.tangem.core.ui.components.transactions.state.TxHistoryState.TxHistoryItemState import com.tangem.core.ui.res.TangemTheme -import com.tangem.feature.wallet.presentation.wallet.state.content.WalletTxHistoryState.TxHistoryItemState /** * Transactions block group title diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/singlecurrency/TxHistoryTitle.kt b/core/ui/src/main/java/com/tangem/core/ui/components/transactions/TxHistoryTitle.kt similarity index 80% rename from features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/singlecurrency/TxHistoryTitle.kt rename to core/ui/src/main/java/com/tangem/core/ui/components/transactions/TxHistoryTitle.kt index a71ec939db..fd59b88c81 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/singlecurrency/TxHistoryTitle.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/transactions/TxHistoryTitle.kt @@ -1,4 +1,4 @@ -package com.tangem.feature.wallet.presentation.wallet.ui.components.singlecurrency +package com.tangem.core.ui.components.transactions import androidx.compose.foundation.background import androidx.compose.foundation.clickable @@ -10,9 +10,9 @@ 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 com.tangem.core.ui.R +import com.tangem.core.ui.components.transactions.state.TxHistoryState import com.tangem.core.ui.res.TangemTheme -import com.tangem.feature.wallet.impl.R -import com.tangem.feature.wallet.presentation.wallet.state.content.WalletTxHistoryState.TxHistoryItemState /** * Transactions block title @@ -21,7 +21,7 @@ import com.tangem.feature.wallet.presentation.wallet.state.content.WalletTxHisto * @param modifier modifier */ @Composable -internal fun TxHistoryTitle(config: TxHistoryItemState.Title, modifier: Modifier = Modifier) { +internal fun TxHistoryTitle(config: TxHistoryState.TxHistoryItemState.Title, modifier: Modifier = Modifier) { Row( modifier = modifier .background(TangemTheme.colors.background.primary) @@ -59,7 +59,7 @@ internal fun TxHistoryTitle(config: TxHistoryItemState.Title, modifier: Modifier @Composable private fun Preview_TransactionsBlockTitle_Light() { TangemTheme(isDark = false) { - TxHistoryTitle(config = TxHistoryItemState.Title(onExploreClick = {})) + TxHistoryTitle(config = TxHistoryState.TxHistoryItemState.Title(onExploreClick = {})) } } @@ -67,6 +67,6 @@ private fun Preview_TransactionsBlockTitle_Light() { @Composable private fun Preview_TransactionsBlockTitle_Dark() { TangemTheme(isDark = true) { - TxHistoryTitle(config = TxHistoryItemState.Title(onExploreClick = {})) + TxHistoryTitle(config = TxHistoryState.TxHistoryItemState.Title(onExploreClick = {})) } } \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/transactions/TransactionState.kt b/core/ui/src/main/java/com/tangem/core/ui/components/transactions/state/TransactionState.kt similarity index 61% rename from core/ui/src/main/java/com/tangem/core/ui/components/transactions/TransactionState.kt rename to core/ui/src/main/java/com/tangem/core/ui/components/transactions/state/TransactionState.kt index b5b6adaaba..8c5f73c545 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/transactions/TransactionState.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/transactions/state/TransactionState.kt @@ -1,4 +1,4 @@ -package com.tangem.core.ui.components.transactions +package com.tangem.core.ui.components.transactions.state /** * Transaction component state @@ -7,33 +7,39 @@ package com.tangem.core.ui.components.transactions */ sealed interface TransactionState { + /** Transaction hash */ + val txHash: String + /** * Content state * + * @property txHash transaction hash * @property address address * @property amount amount * @property timestamp timestamp */ sealed class Content( + override val txHash: String, open val address: String, open val amount: String, open val timestamp: String, ) : TransactionState { fun copySealed( + txHash: String = this.txHash, address: String = this.address, amount: String = this.amount, timestamp: String = this.timestamp, ): Content { return when (this) { - is Approved -> copy(address, amount, timestamp) - is Receive -> copy(address, amount, timestamp) - is Send -> copy(address, amount, timestamp) - is Swapped -> copy(address, amount, timestamp) - is Approving -> copy(address, amount, timestamp) - is Receiving -> copy(address, amount, timestamp) - is Sending -> copy(address, amount, timestamp) - is Swapping -> copy(address, amount, timestamp) + is Approved -> copy(txHash, address, amount, timestamp) + is Receive -> copy(txHash, address, amount, timestamp) + is Send -> copy(txHash, address, amount, timestamp) + is Swapped -> copy(txHash, address, amount, timestamp) + is Approving -> copy(txHash, address, amount, timestamp) + is Receiving -> copy(txHash, address, amount, timestamp) + is Sending -> copy(txHash, address, amount, timestamp) + is Swapping -> copy(txHash, address, amount, timestamp) } } } @@ -41,133 +47,157 @@ sealed interface TransactionState { /** * Content state for processed transaction * + * @property txHash transaction hash * @property address address * @property amount amount * @property timestamp timestamp */ sealed class ProcessedTransactionContent( + override val txHash: String, override val address: String, override val amount: String, override val timestamp: String, - ) : Content(address, amount, timestamp) + ) : Content(txHash, address, amount, timestamp) /** * Content state for completed transaction * + * @property txHash transaction hash * @property address address * @property amount amount * @property timestamp timestamp */ sealed class CompletedTransactionContent( + override val txHash: String, override val address: String, override val amount: String, override val timestamp: String, - ) : Content(address, amount, timestamp) + ) : Content(txHash, address, amount, timestamp) /** * Processed sending transaction state * + * @property txHash transaction hash * @property address address * @property amount amount * @property timestamp timestamp */ data class Sending( + override val txHash: String, override val address: String, override val amount: String, override val timestamp: String, - ) : ProcessedTransactionContent(address, amount, timestamp) + ) : ProcessedTransactionContent(txHash, address, amount, timestamp) /** * Processed receiving transaction state * + * @property txHash transaction hash * @property address address * @property amount amount * @property timestamp timestamp */ data class Receiving( + override val txHash: String, override val address: String, override val amount: String, override val timestamp: String, - ) : ProcessedTransactionContent(address, amount, timestamp) + ) : ProcessedTransactionContent(txHash, address, amount, timestamp) /** * Processed approving transaction state * + * @property txHash transaction hash * @property address address * @property amount amount * @property timestamp timestamp */ data class Approving( + override val txHash: String, override val address: String, override val amount: String, override val timestamp: String, - ) : ProcessedTransactionContent(address, amount, timestamp) + ) : ProcessedTransactionContent(txHash, address, amount, timestamp) /** * Processed swapping transaction state * + * @property txHash transaction hash * @property address address * @property amount amount * @property timestamp timestamp */ data class Swapping( + override val txHash: String, override val address: String, override val amount: String, override val timestamp: String, - ) : ProcessedTransactionContent(address, amount, timestamp) + ) : ProcessedTransactionContent(txHash, address, amount, timestamp) /** * Completed sending transaction state * + * @property txHash transaction hash * @property address address * @property amount amount * @property timestamp timestamp */ data class Send( + override val txHash: String, override val address: String, override val amount: String, override val timestamp: String, - ) : CompletedTransactionContent(address, amount, timestamp) + ) : CompletedTransactionContent(txHash, address, amount, timestamp) /** * Completed receiving transaction state * + * @property txHash transaction hash * @property address address * @property amount amount * @property timestamp timestamp */ data class Receive( + override val txHash: String, override val address: String, override val amount: String, override val timestamp: String, - ) : CompletedTransactionContent(address, amount, timestamp) + ) : CompletedTransactionContent(txHash, address, amount, timestamp) /** * Completed approving transaction state * + * @property txHash transaction hash * @property address address * @property amount amount * @property timestamp timestamp */ data class Approved( + override val txHash: String, override val address: String, override val amount: String, override val timestamp: String, - ) : CompletedTransactionContent(address, amount, timestamp) + ) : CompletedTransactionContent(txHash, address, amount, timestamp) /** * Completed swapping transaction state * + * @property txHash transaction hash * @property address address * @property amount amount * @property timestamp timestamp */ data class Swapped( + override val txHash: String, override val address: String, override val amount: String, override val timestamp: String, - ) : CompletedTransactionContent(address, amount, timestamp) + ) : CompletedTransactionContent(txHash, address, amount, timestamp) - /** Loading state */ - object Loading : TransactionState + /** + * Loading state + * + * @property txHash transaction hash + */ + data class Loading(override val txHash: String) : TransactionState } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/content/WalletTxHistoryState.kt b/core/ui/src/main/java/com/tangem/core/ui/components/transactions/state/TxHistoryState.kt similarity index 56% rename from features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/content/WalletTxHistoryState.kt rename to core/ui/src/main/java/com/tangem/core/ui/components/transactions/state/TxHistoryState.kt index 5252fa8981..85c907187a 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/content/WalletTxHistoryState.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/transactions/state/TxHistoryState.kt @@ -1,26 +1,55 @@ -package com.tangem.feature.wallet.presentation.wallet.state.content +package com.tangem.core.ui.components.transactions.state import androidx.paging.PagingData -import com.tangem.core.ui.components.transactions.TransactionState +import com.tangem.core.ui.components.wallet.WalletLockedContentState import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flowOf /** * Wallet transaction history state - * -[REDACTED_AUTHOR] */ -internal sealed interface WalletTxHistoryState { +sealed interface TxHistoryState { /** * Wallet transaction history state with content * * @property items content items */ - sealed class ContentState(open val items: Flow>) : WalletTxHistoryState + sealed class ContentState(open val items: Flow>) : TxHistoryState /** - * Content state + * Loading state + * + * @property onExploreClick lambda be invoke when explore button was clicked + */ + data class Loading(val onExploreClick: () -> Unit) : ContentState( + items = flowOf( + PagingData.from( + listOf( + TxHistoryItemState.Title(onExploreClick = onExploreClick), + TxHistoryItemState.Transaction(state = TransactionState.Loading(txHash = LOADING_TX_HASH)), + ), + ), + ), + ) + + /** + * Wallet transaction history state with loading transactions + * + * @property itemsCount count of loading transactions + */ + data class ContentWithLoadingItems(val itemsCount: Int) : ContentState( + items = flowOf( + value = PagingData.from( + data = buildList(capacity = itemsCount) { + add(TxHistoryItemState.Transaction(state = TransactionState.Loading(txHash = LOADING_TX_HASH))) + }, + ), + ), + ) + + /** + * Wallet transaction history state with content * * @property items content items */ @@ -37,7 +66,7 @@ internal sealed interface WalletTxHistoryState { PagingData.from( listOf( TxHistoryItemState.Title(onExploreClick = onExploreClick), - TxHistoryItemState.Transaction(state = TransactionState.Loading), + TxHistoryItemState.Transaction(state = TransactionState.Loading(txHash = LOADING_TX_HASH)), ), ), ), @@ -49,21 +78,21 @@ internal sealed interface WalletTxHistoryState { * * @property onBuyClick lambda be invoke when buy button was clicked */ - data class Empty(val onBuyClick: () -> Unit) : WalletTxHistoryState + data class Empty(val onBuyClick: () -> Unit) : TxHistoryState /** * Not supported tx history state * * @property onExploreClick lambda be invoke when explore button was clicked */ - data class NotSupported(val onExploreClick: () -> Unit) : WalletTxHistoryState + data class NotSupported(val onExploreClick: () -> Unit) : TxHistoryState /** * Error state * * @property onReloadClick lambda be invoke when reload button was clicked */ - data class Error(val onReloadClick: () -> Unit) : WalletTxHistoryState + data class Error(val onReloadClick: () -> Unit) : TxHistoryState /** Transactions history item state */ sealed interface TxHistoryItemState { @@ -89,4 +118,8 @@ internal sealed interface WalletTxHistoryState { */ data class Transaction(val state: TransactionState) : TxHistoryItemState } + + private companion object { + const val LOADING_TX_HASH = "LOADING_TX_HASH" + } } \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/wallet/WalletLockedContentState.kt b/core/ui/src/main/java/com/tangem/core/ui/components/wallet/WalletLockedContentState.kt new file mode 100644 index 0000000000..e1c2049da6 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/wallet/WalletLockedContentState.kt @@ -0,0 +1,7 @@ +package com.tangem.core.ui.components.wallet + +/** + * Wallet locked content state. + * It allows to divide the locked content of multi-currency and single-currency wallets. + */ +interface WalletLockedContentState \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/decorations/WalletContentItemDecoration.kt b/core/ui/src/main/java/com/tangem/core/ui/decorations/RoundedDecorations.kt similarity index 87% rename from features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/decorations/WalletContentItemDecoration.kt rename to core/ui/src/main/java/com/tangem/core/ui/decorations/RoundedDecorations.kt index b06ad40f02..362f4d95e9 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/decorations/WalletContentItemDecoration.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/decorations/RoundedDecorations.kt @@ -1,4 +1,4 @@ -package com.tangem.feature.wallet.presentation.wallet.ui.decorations +package com.tangem.core.ui.decorations import androidx.compose.foundation.layout.padding import androidx.compose.foundation.shape.RoundedCornerShape @@ -7,10 +7,7 @@ import androidx.compose.ui.composed import androidx.compose.ui.draw.clip import com.tangem.core.ui.res.TangemTheme -/** -[REDACTED_AUTHOR] - */ -internal fun Modifier.walletContentItemDecoration(currentIndex: Int, lastIndex: Int): Modifier = composed { +fun Modifier.roundedShapeItemDecoration(currentIndex: Int, lastIndex: Int): Modifier = composed { val modifierWithHorizontalPadding = this.padding(horizontal = TangemTheme.dimens.spacing16) val isSingleItem = currentIndex == 0 && lastIndex == 0 when { diff --git a/core/ui/src/main/java/com/tangem/core/ui/extensions/BlockchainIcons.kt b/core/ui/src/main/java/com/tangem/core/ui/extensions/BlockchainIcons.kt index c5e8a5e327..9481be4e27 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/extensions/BlockchainIcons.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/extensions/BlockchainIcons.kt @@ -43,6 +43,7 @@ fun getActiveIconRes(blockchainId: String): Int { "TELOS", "TELOS/test" -> R.drawable.img_telos_22 "aleph-zero", "aleph-zero/test" -> R.drawable.img_azero_22 "octaspace", "octaspace/test" -> R.drawable.img_octaspace_22 + "chia", "chia/test" -> R.drawable.img_chia_22 else -> R.drawable.ic_alert_24 } } @@ -89,6 +90,7 @@ fun getActiveIconResByCoinId(coinId: String, networkId: String): Int { "terra-2" -> R.drawable.img_terra2_22 "telos" -> R.drawable.img_telos_22 "octaspace" -> R.drawable.img_octaspace_22 + "chia" -> R.drawable.img_chia_22 else -> R.drawable.ic_alert_24 } } \ No newline at end of file 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 ddba0a20cd..1c60364fa2 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 @@ -105,11 +105,13 @@ data class TangemDimens internal constructor( val spacing38: Dp = 38.dp, val spacing40: Dp = 40.dp, val spacing44: Dp = 44.dp, + val spacing48: Dp = 48.dp, val spacing50: Dp = 50.dp, val spacing52: Dp = 52.dp, val spacing54: Dp = 54.dp, val spacing56: Dp = 56.dp, val spacing92: Dp = 92.dp, + val spacing96: Dp = 96.dp, val spacing154: Dp = 154.dp, // endregion Spacing ) \ No newline at end of file diff --git a/core/ui/src/main/res/drawable/img_chia_22.xml b/core/ui/src/main/res/drawable/img_chia_22.xml new file mode 100644 index 0000000000..198cffa214 --- /dev/null +++ b/core/ui/src/main/res/drawable/img_chia_22.xml @@ -0,0 +1,13 @@ + + + + diff --git a/core/utils/src/main/java/com/tangem/utils/converter/Converter.kt b/core/utils/src/main/java/com/tangem/utils/converter/Converter.kt index 6af667ae08..b818478a45 100644 --- a/core/utils/src/main/java/com/tangem/utils/converter/Converter.kt +++ b/core/utils/src/main/java/com/tangem/utils/converter/Converter.kt @@ -1,8 +1,14 @@ package com.tangem.utils.converter -interface Converter { +interface Converter { + fun convert(value: I): O - fun convertList(input: List): List { - return input.map { convert(it) } + + fun convertList(input: Collection): List { + return input.map(::convert) + } + + fun convertSet(input: Collection): Set { + return input.mapTo(hashSetOf(), ::convert) } } \ No newline at end of file diff --git a/core/utils/src/main/java/com/tangem/utils/converter/TwoWayConverter.kt b/core/utils/src/main/java/com/tangem/utils/converter/TwoWayConverter.kt index 444e4048ab..50da29ecd0 100644 --- a/core/utils/src/main/java/com/tangem/utils/converter/TwoWayConverter.kt +++ b/core/utils/src/main/java/com/tangem/utils/converter/TwoWayConverter.kt @@ -1,8 +1,10 @@ package com.tangem.utils.converter -interface TwoWayConverter : Converter { +interface TwoWayConverter : Converter { + fun convertBack(value: O): I - fun convertListBack(input: List): List { + + fun convertListBack(input: Collection): List { return input.map { convertBack(it) } } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/JobHolder.kt b/core/utils/src/main/java/com/tangem/utils/coroutines/JobHolder.kt similarity index 50% rename from features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/JobHolder.kt rename to core/utils/src/main/java/com/tangem/utils/coroutines/JobHolder.kt index 94c0e30ade..3c0ca6bd8d 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/JobHolder.kt +++ b/core/utils/src/main/java/com/tangem/utils/coroutines/JobHolder.kt @@ -1,4 +1,4 @@ -package com.tangem.feature.wallet.presentation.wallet.viewmodels +package com.tangem.utils.coroutines import kotlinx.coroutines.Job @@ -7,15 +7,15 @@ import kotlinx.coroutines.Job * [REDACTED_AUTHOR] */ -internal class JobHolder { +class JobHolder { private var job: Job? = null - /** Update current job */ - fun update(job: Job) { + /** Update current [job] */ + fun update(job: Job?) { this.job?.cancel() this.job = job } } -internal fun Job.saveIn(jobHolder: JobHolder) = jobHolder.update(job = this) \ No newline at end of file +fun Job.saveIn(jobHolder: JobHolder) = jobHolder.update(job = this) \ No newline at end of file diff --git a/core/utils/src/main/java/com/tangem/utils/extensions/Collection.kt b/core/utils/src/main/java/com/tangem/utils/extensions/Collection.kt index 36644cfda9..9930c5cb77 100644 --- a/core/utils/src/main/java/com/tangem/utils/extensions/Collection.kt +++ b/core/utils/src/main/java/com/tangem/utils/extensions/Collection.kt @@ -18,95 +18,4 @@ fun Collection.isSingleItem(): Boolean = this.size == 1 */ fun Collection.copy(): Collection { return this.map { it } -} - -/** - * Adds the specified element to the collection or replaces an existing element. - * The predicate defines the condition to replace the existing element. - * - * @param item The element to be added or replace the existing one. - * @param predicate The condition to replace an existing element. - * @return The modified [List] after adding or replacing the element. - */ -inline fun Collection.plusOrReplace(item: T, predicate: (T) -> Boolean): List { - val mutableList = this as? MutableList ?: ArrayList(this) - - mutableList.addOrReplace(item, predicate) - - return mutableList -} - -/** - * Adds the specified element to the collection or replaces an existing element. - * The predicate defines the condition to replace the existing element. - * - * @param item The element to be added or replace the existing one. - * @param predicate The condition to replace an existing element. - */ -inline fun MutableCollection.addOrReplace(item: T, predicate: (T) -> Boolean) { - val isReplaced = replaceBy(item, predicate) - - if (!isReplaced) { - add(item) - } -} - -/** - * Removes an element from the collection based on the provided predicate. - * Uses iterator, avoid using it in COW collections - * - * @param predicate The condition to remove an element. - * @return [Boolean] indicating whether an element was removed. - */ -inline fun MutableCollection.removeByIterate(predicate: (T) -> Boolean): Boolean { - var removed = false - val iterator = this.iterator() - - for (e in iterator) { - if (predicate(e)) { - iterator.remove() - removed = true - - break - } - } - - return removed -} - -/** - * Removes an element from the collection based on the provided predicate. - * Uses removeAll() method and could be used for COW collections - * - * @param predicate The condition to remove an element. - * @return [Boolean] indicating whether an element was removed. - */ -fun MutableList.removeByReplace(predicate: (T) -> Boolean): Boolean { - val toRemove = this.filter(predicate) - this.removeAll(toRemove) - return toRemove.isNotEmpty() -} - -/** - * Replaces an element in the collection with the provided item based on the predicate. - * - * @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 MutableCollection.replaceBy(item: T, predicate: (T) -> Boolean): Boolean { - var replaced = false - val mutableList = this as? MutableList ?: ArrayList(this) - val iterator = mutableList.listIterator() - - for (e in iterator) { - if (predicate(e)) { - iterator.set(item) - replaced = true - - break - } - } - - return replaced } \ No newline at end of file diff --git a/core/utils/src/main/java/com/tangem/utils/extensions/List.kt b/core/utils/src/main/java/com/tangem/utils/extensions/List.kt new file mode 100644 index 0000000000..ee1c005d8c --- /dev/null +++ b/core/utils/src/main/java/com/tangem/utils/extensions/List.kt @@ -0,0 +1,51 @@ +package com.tangem.utils.extensions + +/** + * Removes an element from the collection based on the provided predicate. + * + * @param predicate The condition to remove an element. + * @return [Boolean] indicating whether an element was removed. + */ +fun MutableList.removeBy(predicate: (T) -> Boolean): Boolean { + val toRemove = this.filter(predicate) + this.removeAll(toRemove) + return toRemove.isNotEmpty() +} + +/** + * Replaces an element in the list with the provided item based on the predicate. + * + * @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 MutableList.replaceBy(item: T, predicate: (T) -> Boolean): Boolean { + val index = indexOfFirst(predicate) + + if (index == -1) { + return false + } + + this[index] = item + + return true +} + +/** + * Adds the specified element to the list or replaces an existing element. + * The predicate defines the condition to replace the existing element. + * + * @param item The element to be added or replace the existing one. + * @param predicate The condition to replace an existing element. + * @return The modified [List] after adding or replacing the element. + */ +inline fun List.addOrReplace(item: T, predicate: (T) -> Boolean): List { + val mutableList = this.toMutableList() + val isReplaced = mutableList.replaceBy(item, predicate) + + if (!isReplaced) { + mutableList.add(item) + } + + return mutableList +} \ No newline at end of file diff --git a/data/app-currency/.gitignore b/data/app-currency/.gitignore new file mode 100644 index 0000000000..796b96d1c4 --- /dev/null +++ b/data/app-currency/.gitignore @@ -0,0 +1 @@ +/build diff --git a/data/app-currency/build.gradle.kts b/data/app-currency/build.gradle.kts new file mode 100644 index 0000000000..69613e2e85 --- /dev/null +++ b/data/app-currency/build.gradle.kts @@ -0,0 +1,34 @@ +plugins { + alias(deps.plugins.android.library) + alias(deps.plugins.kotlin.android) + alias(deps.plugins.kotlin.kapt) + id("configuration") +} + +android { + namespace = "com.tangem.data.appcurrency" +} + +dependencies { + + /** Project - Domain */ + implementation(projects.domain.core) + implementation(projects.domain.appCurrency) + implementation(projects.domain.appCurrency.models) + + /** Project - Data */ + implementation(projects.core.datasource) + implementation(projects.data.common) + + /** Project - Utils */ + implementation(projects.core.utils) + + /** DI */ + implementation(deps.hilt.core) + kapt(deps.hilt.kapt) + + /** Other */ + implementation(deps.kotlin.coroutines) + implementation(deps.timber) + implementation(deps.jodatime) +} \ 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 new file mode 100644 index 0000000000..8a780ba151 --- /dev/null +++ b/data/app-currency/src/main/kotlin/com/tangem/data/appcurrency/DefaultAppCurrencyRepository.kt @@ -0,0 +1,105 @@ +package com.tangem.data.appcurrency + +import com.tangem.data.appcurrency.utils.AppCurrencyConverter +import com.tangem.data.common.cache.CacheRegistry +import com.tangem.datasource.api.tangemTech.TangemTechApi +import com.tangem.datasource.api.tangemTech.models.CurrenciesResponse +import com.tangem.datasource.local.appcurrency.AvailableAppCurrenciesStore +import com.tangem.datasource.local.appcurrency.SelectedAppCurrencyStore +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.appcurrency.repository.AppCurrencyRepository +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onEmpty +import kotlinx.coroutines.withContext +import org.joda.time.Duration +import timber.log.Timber + +internal class DefaultAppCurrencyRepository( + private val tangemTechApi: TangemTechApi, + private val availableAppCurrenciesStore: AvailableAppCurrenciesStore, + private val selectedAppCurrencyStore: SelectedAppCurrencyStore, + private val cacheRegistry: CacheRegistry, + private val dispatchers: CoroutineDispatcherProvider, +) : AppCurrencyRepository { + + private val appCurrencyConverter = AppCurrencyConverter() + + override fun getSelectedAppCurrency(): Flow { + return selectedAppCurrencyStore.get() + .onEmpty { fetchDefaultAppCurrency() } + .map(appCurrencyConverter::convert) + .flowOn(dispatchers.io) + } + + override suspend fun getAvailableAppCurrencies(): List { + return withContext(dispatchers.io) { + fetchAvailableCurrenciesIfExpired() + + val currencies = availableAppCurrenciesStore.getAllSyncOrNull() + ?.map(appCurrencyConverter::convert) + + requireNotNull(currencies) { + "No available currencies stored" + } + } + } + + override suspend fun changeAppCurrency(currencyCode: String) { + withContext(dispatchers.io) { + val currency = requireNotNull(availableAppCurrenciesStore.getSyncOrNull(currencyCode)) { + "Unable to find app currency with provided code: $currencyCode" + } + + selectedAppCurrencyStore.store(currency) + } + } + + private suspend fun fetchDefaultAppCurrency() { + fetchAvailableCurrenciesIfExpired() + + changeAppCurrency(DEFAULT_CURRENCY_CODE) + } + + private suspend fun fetchAvailableCurrenciesIfExpired() { + cacheRegistry.invokeOnExpire( + key = AVAILABLE_CURRENCIES_CACHE_KEY, + skipCache = false, + expireIn = Duration.standardMinutes(AVAILABLE_CURRENCIES_CACHE_KEY_EXPIRE_MINUTES), + block = { fetchAvailableCurrencies() }, + ) + } + + private suspend fun fetchAvailableCurrencies() { + try { + val response = tangemTechApi.getCurrencyList() + + availableAppCurrenciesStore.store(response) + } catch (e: Throwable) { + Timber.e(e, "Unable to fetch available currencies") + + availableAppCurrenciesStore.store(getDefaultCurrenciesResponse()) + } + } + + private fun getDefaultCurrenciesResponse(): CurrenciesResponse = CurrenciesResponse( + currencies = listOf( + CurrenciesResponse.Currency( + id = DEFAULT_CURRENCY_CODE.lowercase(), + code = DEFAULT_CURRENCY_CODE, + name = "US Dollar", + unit = "$", + type = "fiat", + rateBTC = "", + ), + ), + ) + + private companion object { + const val AVAILABLE_CURRENCIES_CACHE_KEY = "available_currencies" + const val AVAILABLE_CURRENCIES_CACHE_KEY_EXPIRE_MINUTES = 15L + const val DEFAULT_CURRENCY_CODE = "USD" + } +} \ No newline at end of file diff --git a/data/app-currency/src/main/kotlin/com/tangem/data/appcurrency/di/AppCurrencyDataModule.kt b/data/app-currency/src/main/kotlin/com/tangem/data/appcurrency/di/AppCurrencyDataModule.kt new file mode 100644 index 0000000000..cd80c7c52b --- /dev/null +++ b/data/app-currency/src/main/kotlin/com/tangem/data/appcurrency/di/AppCurrencyDataModule.kt @@ -0,0 +1,37 @@ +package com.tangem.data.appcurrency.di + +import com.tangem.data.appcurrency.DefaultAppCurrencyRepository +import com.tangem.data.common.cache.CacheRegistry +import com.tangem.datasource.api.tangemTech.TangemTechApi +import com.tangem.datasource.local.appcurrency.AvailableAppCurrenciesStore +import com.tangem.datasource.local.appcurrency.SelectedAppCurrencyStore +import com.tangem.domain.appcurrency.repository.AppCurrencyRepository +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +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 AppCurrencyDataModule { + + @Provides + @Singleton + fun provideAppCurrencyRepository( + tangemTechApi: TangemTechApi, + availableAppCurrenciesStore: AvailableAppCurrenciesStore, + selectedAppCurrencyStore: SelectedAppCurrencyStore, + cacheRegistry: CacheRegistry, + dispatchers: CoroutineDispatcherProvider, + ): AppCurrencyRepository { + return DefaultAppCurrencyRepository( + tangemTechApi, + availableAppCurrenciesStore, + selectedAppCurrencyStore, + cacheRegistry, + dispatchers, + ) + } +} \ No newline at end of file diff --git a/data/app-currency/src/main/kotlin/com/tangem/data/appcurrency/utils/AppCurrencyConverter.kt b/data/app-currency/src/main/kotlin/com/tangem/data/appcurrency/utils/AppCurrencyConverter.kt new file mode 100644 index 0000000000..6b8a6409e8 --- /dev/null +++ b/data/app-currency/src/main/kotlin/com/tangem/data/appcurrency/utils/AppCurrencyConverter.kt @@ -0,0 +1,16 @@ +package com.tangem.data.appcurrency.utils + +import com.tangem.datasource.api.tangemTech.models.CurrenciesResponse +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.utils.converter.Converter + +internal class AppCurrencyConverter : Converter { + + override fun convert(value: CurrenciesResponse.Currency): AppCurrency { + return AppCurrency( + code = value.code, + name = value.name, + symbol = value.unit, + ) + } +} \ No newline at end of file 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 a66fca3546..522b4d2b9b 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 @@ -38,7 +38,7 @@ internal class DefaultCardSdkProvider @Inject constructor() : CardSdkProvider, C allowUntrustedCards = true, filter = CardFilter( allowedCardTypes = FirmwareVersion.FirmwareType.values().toList(), - maxFirmwareVersion = FirmwareVersion(major = 6, minor = 21), + maxFirmwareVersion = FirmwareVersion(major = 6, minor = 33), ), ) } diff --git a/data/common/src/main/kotlin/com/tangem/data/common/locale/DefaultLocaleProvider.kt b/data/common/src/main/kotlin/com/tangem/data/common/locale/DefaultLocaleProvider.kt new file mode 100644 index 0000000000..55cdf424ec --- /dev/null +++ b/data/common/src/main/kotlin/com/tangem/data/common/locale/DefaultLocaleProvider.kt @@ -0,0 +1,28 @@ +package com.tangem.data.common.locale + +import java.util.Locale + +/** +[REDACTED_AUTHOR] + */ +internal class DefaultLocaleProvider : LocaleProvider { + + override fun getLocale(): Locale { + return Locale.getDefault() + } + + override fun getWebUriLocaleLanguage(): String { + val language = getLocale().language + return if (LOCALE_LANG_RU.equals(language, true) || LOCALE_LANG_BY.equals(language, true)) { + LOCALE_LANG_RU + } else { + LOCALE_LANG_EN + } + } + + companion object { + const val LOCALE_LANG_RU = "ru" + const val LOCALE_LANG_BY = "by" + const val LOCALE_LANG_EN = "en" + } +} \ No newline at end of file diff --git a/data/common/src/main/kotlin/com/tangem/data/common/locale/LocaleProvider.kt b/data/common/src/main/kotlin/com/tangem/data/common/locale/LocaleProvider.kt new file mode 100644 index 0000000000..5f52c75915 --- /dev/null +++ b/data/common/src/main/kotlin/com/tangem/data/common/locale/LocaleProvider.kt @@ -0,0 +1,13 @@ +package com.tangem.data.common.locale + +import java.util.Locale + +/** +[REDACTED_AUTHOR] + */ +interface LocaleProvider { + + fun getLocale(): Locale + + fun getWebUriLocaleLanguage(): String +} \ No newline at end of file diff --git a/data/common/src/main/kotlin/com/tangem/data/common/locale/di/LocaleProviderModule.kt b/data/common/src/main/kotlin/com/tangem/data/common/locale/di/LocaleProviderModule.kt new file mode 100644 index 0000000000..656c6ed529 --- /dev/null +++ b/data/common/src/main/kotlin/com/tangem/data/common/locale/di/LocaleProviderModule.kt @@ -0,0 +1,20 @@ +package com.tangem.data.common.locale.di + +import com.tangem.data.common.locale.DefaultLocaleProvider +import com.tangem.data.common.locale.LocaleProvider +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 LocaleProviderModule { + + @Provides + @Singleton + fun provideCacheRegistry(): LocaleProvider { + return DefaultLocaleProvider() + } +} \ No newline at end of file diff --git a/data/tokens/build.gradle.kts b/data/tokens/build.gradle.kts index 5b3c4c0e7a..a45be27508 100644 --- a/data/tokens/build.gradle.kts +++ b/data/tokens/build.gradle.kts @@ -13,10 +13,10 @@ dependencies { /** Project - Domain */ implementation(projects.domain.core) + implementation(projects.domain.demo) implementation(projects.domain.models) implementation(projects.domain.tokens) implementation(projects.domain.tokens.models) - implementation(projects.domain.demo) implementation(projects.domain.wallets.models) /** Project - Data */ @@ -25,7 +25,6 @@ dependencies { /** Project - Utils */ implementation(projects.core.utils) - // FIXME: For blockchain extensions, remove after refactoring implementation(projects.domain.legacy) /** Tangem SDKs */ @@ -38,7 +37,6 @@ dependencies { /** Other */ implementation(deps.kotlin.coroutines) - implementation(deps.arrow.core) implementation(deps.moshi.kotlin) implementation(deps.jodatime) implementation(deps.timber) 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 2b512574c4..c1229cde5b 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 @@ -1,15 +1,18 @@ package com.tangem.data.tokens.di import com.tangem.data.common.cache.CacheRegistry -import com.tangem.data.tokens.repository.DefaultTokensRepository -import com.tangem.data.tokens.repository.MockNetworksRepository -import com.tangem.data.tokens.repository.MockQuotesRepository +import com.tangem.data.tokens.repository.DefaultCurrenciesRepository +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.UserTokensStore import com.tangem.datasource.local.userwallet.UserWalletsStore +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.tokens.repository.TokensRepository +import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.Module import dagger.Provides @@ -23,21 +26,49 @@ internal object TokensDataModule { @Provides @Singleton - fun provideTokensRepository( + fun provideCurrenciesRepository( tangemTechApi: TangemTechApi, userTokensStore: UserTokensStore, userWalletsStore: UserWalletsStore, cacheRegistry: CacheRegistry, dispatchers: CoroutineDispatcherProvider, - ): TokensRepository { - return DefaultTokensRepository(tangemTechApi, userTokensStore, userWalletsStore, cacheRegistry, dispatchers) + ): CurrenciesRepository { + return DefaultCurrenciesRepository(tangemTechApi, userTokensStore, userWalletsStore, cacheRegistry, dispatchers) } @Provides @Singleton - fun provideQuotesRepository(): QuotesRepository = MockQuotesRepository() + fun provideQuotesRepository( + tangemTechApi: TangemTechApi, + quotesStore: QuotesStore, + selectedAppCurrencyStore: SelectedAppCurrencyStore, + cacheRegistry: CacheRegistry, + dispatchers: CoroutineDispatcherProvider, + ): QuotesRepository { + return DefaultQuotesRepository( + tangemTechApi, + quotesStore, + selectedAppCurrencyStore, + cacheRegistry, + dispatchers, + ) + } @Provides @Singleton - fun provideNetworksRepository(): NetworksRepository = MockNetworksRepository() + fun provideNetworksRepository( + walletManagersFacade: WalletManagersFacade, + userWalletsStore: UserWalletsStore, + userTokensStore: UserTokensStore, + cacheRegistry: CacheRegistry, + dispatchers: CoroutineDispatcherProvider, + ): NetworksRepository { + return DefaultNetworksRepository( + walletManagersFacade, + userWalletsStore, + userTokensStore, + cacheRegistry, + dispatchers, + ) + } } \ No newline at end of file diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/mock/MockNetworks.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/mock/MockNetworks.kt deleted file mode 100644 index 8278cce392..0000000000 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/mock/MockNetworks.kt +++ /dev/null @@ -1,58 +0,0 @@ -package com.tangem.data.tokens.mock - -import com.tangem.domain.tokens.model.NetworkStatus -import com.tangem.domain.tokens.models.Network -import java.math.BigDecimal - -@Suppress("MemberVisibilityCanBePrivate") -internal object MockNetworks { - - val network1 = Network( - id = Network.ID("network1"), - name = "Network One", - ) - - val network2 = Network( - id = Network.ID("network2"), - name = "Network Two", - ) - - val network3 = Network( - id = Network.ID("network3"), - name = "Network Three", - ) - - val networks = setOf(network1, network2, network3) - - val networkStatus1 = NetworkStatus( - networkId = network1.id, - value = NetworkStatus.Verified( - amounts = mapOf( - MockTokens.token1.id to BigDecimal("123.1234556789"), - MockTokens.token2.id to BigDecimal("42.2"), - MockTokens.token3.id to BigDecimal("1000000000.5"), - ), - hasTransactionsInProgress = false, - ), - ) - - val networkStatus2 = NetworkStatus( - networkId = network2.id, - value = NetworkStatus.MissedDerivation, - ) - - val networkStatus3 = NetworkStatus( - networkId = network3.id, - value = NetworkStatus.Verified( - amounts = mapOf( - MockTokens.token7.id to BigDecimal.ZERO, - MockTokens.token8.id to BigDecimal.TEN, - MockTokens.token9.id to BigDecimal.TEN, - MockTokens.token10.id to BigDecimal.TEN, - ), - hasTransactionsInProgress = false, - ), - ) - - val networksStatuses = setOf(networkStatus1, networkStatus2, networkStatus3) -} \ No newline at end of file diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/mock/MockQuotes.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/mock/MockQuotes.kt deleted file mode 100644 index f89c208a52..0000000000 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/mock/MockQuotes.kt +++ /dev/null @@ -1,70 +0,0 @@ -package com.tangem.data.tokens.mock - -import com.tangem.domain.tokens.model.Quote -import java.math.BigDecimal - -@Suppress("MemberVisibilityCanBePrivate") -internal object MockQuotes { - - val quote1 = Quote( - currencyId = MockTokens.token1.id, - fiatRate = BigDecimal("1.23"), - priceChange = BigDecimal("0.01"), - ) - - val quote2 = Quote( - currencyId = MockTokens.token2.id, - fiatRate = BigDecimal("2.34"), - priceChange = BigDecimal("-0.02"), - ) - - val quote3 = Quote( - currencyId = MockTokens.token3.id, - fiatRate = BigDecimal("3.45"), - priceChange = BigDecimal("0.03"), - ) - - val quote4 = Quote( - currencyId = MockTokens.token4.id, - fiatRate = BigDecimal("4.56"), - priceChange = BigDecimal("-0.04"), - ) - - val quote5 = Quote( - currencyId = MockTokens.token5.id, - fiatRate = BigDecimal("5.67"), - priceChange = BigDecimal("0.05"), - ) - - val quote6 = Quote( - currencyId = MockTokens.token6.id, - fiatRate = BigDecimal("6.78"), - priceChange = BigDecimal("-0.06"), - ) - - val quote7 = Quote( - currencyId = MockTokens.token7.id, - fiatRate = BigDecimal("7.89"), - priceChange = BigDecimal("0.07"), - ) - - val quote8 = Quote( - currencyId = MockTokens.token8.id, - fiatRate = BigDecimal("8.90"), - priceChange = BigDecimal("-0.08"), - ) - - val quote9 = Quote( - currencyId = MockTokens.token9.id, - fiatRate = BigDecimal("9.01"), - priceChange = BigDecimal("0.09"), - ) - - val quote10 = Quote( - currencyId = MockTokens.token10.id, - fiatRate = BigDecimal("10.12"), - priceChange = BigDecimal("-0.10"), - ) - - val quotes = setOf(quote1, quote2, quote3, quote4, quote5, quote6, quote7, quote8, quote9, quote10) -} \ No newline at end of file diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/mock/MockTokens.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/mock/MockTokens.kt deleted file mode 100644 index e18e4c126d..0000000000 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/mock/MockTokens.kt +++ /dev/null @@ -1,133 +0,0 @@ -package com.tangem.data.tokens.mock - -import com.tangem.domain.tokens.model.CryptoCurrency -import com.tangem.domain.wallets.models.UserWalletId - -internal object MockTokens { - - val token1 - get() = CryptoCurrency.Coin( - id = CryptoCurrency.ID("token1"), - networkId = MockNetworks.network1.id, - name = "Token 1", - symbol = "T1", - decimals = 8, - iconUrl = null, - derivationPath = null, - ) - val token2 - get() = CryptoCurrency.Token( - id = CryptoCurrency.ID("token2"), - networkId = MockNetworks.network1.id, - name = "Token 2", - symbol = "T2", - isCustom = false, - decimals = 8, - iconUrl = null, - contractAddress = "address", - derivationPath = null, - ) - val token3 - get() = CryptoCurrency.Token( - id = CryptoCurrency.ID("token3"), - networkId = MockNetworks.network1.id, - name = "Token 3", - symbol = "T3", - isCustom = false, - decimals = 8, - iconUrl = null, - contractAddress = "address", - derivationPath = null, - ) - val token4 - get() = CryptoCurrency.Coin( - id = CryptoCurrency.ID("token4"), - networkId = MockNetworks.network2.id, - name = "Token 4", - symbol = "T4", - decimals = 8, - iconUrl = null, - derivationPath = null, - ) - val token5 - get() = CryptoCurrency.Token( - id = CryptoCurrency.ID("token5"), - networkId = MockNetworks.network2.id, - name = "Token 5", - symbol = "T5", - isCustom = false, - decimals = 8, - iconUrl = null, - contractAddress = "address", - derivationPath = null, - ) - val token6 - get() = CryptoCurrency.Token( - id = CryptoCurrency.ID("token6"), - networkId = MockNetworks.network2.id, - name = "Token 6", - symbol = "T6", - isCustom = false, - decimals = 8, - iconUrl = null, - contractAddress = "address", - derivationPath = null, - ) - val token7 - get() = CryptoCurrency.Coin( - id = CryptoCurrency.ID("token7"), - networkId = MockNetworks.network3.id, - name = "Token 7", - symbol = "T7", - decimals = 8, - iconUrl = null, - derivationPath = null, - ) - val token8 - get() = CryptoCurrency.Token( - id = CryptoCurrency.ID("token8"), - networkId = MockNetworks.network3.id, - name = "Token 8", - symbol = "T8", - isCustom = false, - decimals = 8, - iconUrl = null, - contractAddress = "address", - derivationPath = null, - ) - val token9 - get() = CryptoCurrency.Token( - id = CryptoCurrency.ID("token9"), - networkId = MockNetworks.network3.id, - name = "Token 9", - symbol = "T9", - isCustom = false, - decimals = 8, - iconUrl = null, - contractAddress = "address", - derivationPath = null, - ) - val token10 - get() = CryptoCurrency.Token( - id = CryptoCurrency.ID("token10"), - networkId = MockNetworks.network3.id, - name = "Token 10", - symbol = "T10", - isCustom = false, - decimals = 8, - iconUrl = null, - contractAddress = "address", - derivationPath = null, - ) - - val tokens - get() = mapOf( - UserWalletId(stringValue = "123") to setOf( - token1, token2, token3, token4, token5, - token6, token7, token8, token9, token10, - ), - UserWalletId(stringValue = "321") to setOf(token1, token2, token3), - UserWalletId(stringValue = "42") to setOf(token7, token8, token9, token10), - UserWalletId(stringValue = "24") to setOf(token4, token5, token6), - ) -} \ No newline at end of file diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultTokensRepository.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt similarity index 52% rename from data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultTokensRepository.kt rename to data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt index 704e25fa68..36abfd1d54 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultTokensRepository.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt @@ -8,23 +8,28 @@ import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.api.tangemTech.models.UserTokensResponse import com.tangem.datasource.local.token.UserTokensStore import com.tangem.datasource.local.userwallet.UserWalletsStore +import com.tangem.domain.core.error.DataError +import com.tangem.domain.common.util.derivationStyleProvider import com.tangem.domain.demo.DemoConfig -import com.tangem.domain.tokens.model.CryptoCurrency -import com.tangem.domain.tokens.repository.TokensRepository +import com.tangem.domain.tokens.models.CryptoCurrency +import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWalletId import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import kotlinx.coroutines.flow.* +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.channelFlow +import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +import timber.log.Timber -internal class DefaultTokensRepository( +internal class DefaultCurrenciesRepository( private val tangemTechApi: TangemTechApi, private val userTokensStore: UserTokensStore, private val userWalletsStore: UserWalletsStore, private val cacheRegistry: CacheRegistry, private val dispatchers: CoroutineDispatcherProvider, -) : TokensRepository { +) : CurrenciesRepository { private val demoConfig = DemoConfig() private val responseCurrenciesFactory = ResponseCurrenciesFactory(demoConfig) @@ -33,10 +38,12 @@ internal class DefaultTokensRepository( override suspend fun saveTokens( userWalletId: UserWalletId, - currencies: Set, + currencies: List, isGroupedByNetwork: Boolean, isSortedByBalance: Boolean, ) = withContext(dispatchers.io) { + ensureIsCorrectUserWallet(userWalletId, isMultiCurrencyWalletExpected = true) + val response = userTokensResponseFactory.createUserTokensResponse( currencies = currencies, isGroupedByNetwork = isGroupedByNetwork, @@ -46,58 +53,72 @@ internal class DefaultTokensRepository( storeAndPushTokens(userWalletId, response) } - override suspend fun getPrimaryCurrency(userWalletId: UserWalletId): CryptoCurrency { - val userWallet = withContext(dispatchers.io) { - requireNotNull(userWalletsStore.getSyncOrNull(userWalletId)) { - "Unable to find a user wallet with provided ID: $userWalletId" - } - } - require(!userWallet.isMultiCurrency) { - "Single currency wallet excepted, but multi currency wallet was found: $userWalletId" - } + override suspend fun getSingleCurrencyWalletPrimaryCurrency(userWalletId: UserWalletId): CryptoCurrency { + return withContext(dispatchers.io) { + val userWallet = getUserWallet(userWalletId) + ensureIsCorrectUserWallet(userWallet, isMultiCurrencyWalletExpected = false) - return cardCurrenciesFactory.createPrimaryCurrencyForSingleCurrencyCard(userWallet.scanResponse) + cardCurrenciesFactory.createPrimaryCurrencyForSingleCurrencyCard(userWallet.scanResponse) + } } override fun getMultiCurrencyWalletCurrencies( userWalletId: UserWalletId, refresh: Boolean, - ): Flow> { + ): Flow> = channelFlow { + val userWallet = getUserWallet(userWalletId) + ensureIsCorrectUserWallet(userWallet, isMultiCurrencyWalletExpected = true) + + launch(dispatchers.io) { + getMultiCurrencyWalletCurrencies(userWallet).collect(::send) + } + + launch(dispatchers.io) { + fetchTokensIfCacheExpired(userWallet, refresh) + } + } + + override suspend fun getMultiCurrencyWalletCurrency( + userWalletId: UserWalletId, + id: CryptoCurrency.ID, + ): CryptoCurrency = withContext(dispatchers.io) { + val userWallet = getUserWallet(userWalletId) + ensureIsCorrectUserWallet(userWallet, isMultiCurrencyWalletExpected = true) + + val response = requireNotNull(userTokensStore.getSyncOrNull(userWalletId)) { + "Unable to find tokens response for user wallet with provided ID: $userWalletId" + } + + responseCurrenciesFactory.createCurrency(id, response, userWallet.scanResponse.card) + } + + override fun isTokensGrouped(userWalletId: UserWalletId): Flow { return channelFlow { - val userWallet = withContext(dispatchers.io) { - requireNotNull(userWalletsStore.getSyncOrNull(userWalletId)) { - "Unable to find a user wallet with provided ID: $userWalletId" - } - } - require(userWallet.isMultiCurrency) { - "Multi currency wallet excepted, but single currency wallet was found: $userWalletId" - } + ensureIsCorrectUserWallet(userWalletId, isMultiCurrencyWalletExpected = true) launch(dispatchers.io) { - getMultiCurrencyWalletCurrencies(userWallet).collectLatest(::send) - } - - launch(dispatchers.io) { - fetchTokensIfCacheExpired(userWallet, refresh) + userTokensStore.get(userWalletId) + .map { it.group == UserTokensResponse.GroupType.NETWORK } + .collect(::send) } } } - override fun isTokensGrouped(userWalletId: UserWalletId): Flow { - return userTokensStore.get(userWalletId) - .map { it.group == UserTokensResponse.GroupType.NETWORK } - .flowOn(dispatchers.io) - } - override fun isTokensSortedByBalance(userWalletId: UserWalletId): Flow { - return userTokensStore.get(userWalletId) - .map { it.sort == UserTokensResponse.SortType.BALANCE } - .flowOn(dispatchers.io) + return channelFlow { + ensureIsCorrectUserWallet(userWalletId, isMultiCurrencyWalletExpected = true) + + launch(dispatchers.io) { + userTokensStore.get(userWalletId) + .map { it.sort == UserTokensResponse.SortType.BALANCE } + .collect(::send) + } + } } - private fun getMultiCurrencyWalletCurrencies(userWallet: UserWallet): Flow> { + private fun getMultiCurrencyWalletCurrencies(userWallet: UserWallet): Flow> { return userTokensStore.get(userWallet.walletId).map { storedTokens -> - responseCurrenciesFactory.createTokens( + responseCurrenciesFactory.createCurrencies( response = storedTokens, card = userWallet.scanResponse.card, ) @@ -134,7 +155,8 @@ internal class DefaultTokensRepository( val response = userTokensStore.getSyncOrNull(userWallet.walletId) ?: userTokensResponseFactory.createUserTokensResponse( currencies = cardCurrenciesFactory.createDefaultCoinsForMultiCurrencyCard( - userWallet.scanResponse.card, + card = userWallet.scanResponse.card, + derivationStyleProvider = userWallet.scanResponse.derivationStyleProvider, ), isGroupedByNetwork = false, isSortedByBalance = false, @@ -146,6 +168,39 @@ internal class DefaultTokensRepository( } } + private suspend fun getUserWallet(userWalletId: UserWalletId): UserWallet { + return requireNotNull(userWalletsStore.getSyncOrNull(userWalletId)) { + "Unable to find a user wallet with provided ID: $userWalletId" + } + } + + private suspend fun ensureIsCorrectUserWallet(userWalletId: UserWalletId, isMultiCurrencyWalletExpected: Boolean) { + val userWallet = getUserWallet(userWalletId) + + ensureIsCorrectUserWallet(userWallet, isMultiCurrencyWalletExpected) + } + + private fun ensureIsCorrectUserWallet(userWallet: UserWallet, isMultiCurrencyWalletExpected: Boolean) { + val userWalletId = userWallet.walletId + + val message = when { + !userWallet.isMultiCurrency && isMultiCurrencyWalletExpected -> { + "Multi currency wallet expected, but single currency wallet was found: $userWalletId" + } + userWallet.isMultiCurrency && !isMultiCurrencyWalletExpected -> { + "Single currency wallet expected, but multi currency wallet was found: $userWalletId" + } + else -> null + } + + if (message != null) { + val error = DataError.UserWalletError.WrongUserWallet(message) + + Timber.e(error) + throw error + } + } + private fun getTokensCacheKey(userWalletId: UserWalletId): String = "tokens_cache_key_${userWalletId.stringValue}" private companion object { 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 new file mode 100644 index 0000000000..e9e235c8d1 --- /dev/null +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultNetworksRepository.kt @@ -0,0 +1,128 @@ +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.NetworkStatusFactory +import com.tangem.data.tokens.utils.ResponseCurrenciesFactory +import com.tangem.datasource.local.token.UserTokensStore +import com.tangem.datasource.local.userwallet.UserWalletsStore +import com.tangem.domain.demo.DemoConfig +import com.tangem.domain.tokens.model.NetworkStatus +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.wallets.models.UserWalletId +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import com.tangem.utils.extensions.addOrReplace +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.channelFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch + +internal class DefaultNetworksRepository( + private val walletManagersFacade: WalletManagersFacade, + private val userWalletsStore: UserWalletsStore, + private val userTokensStore: UserTokensStore, + private val cacheRegistry: CacheRegistry, + private val dispatchers: CoroutineDispatcherProvider, +) : 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 networkStatusFactory by lazy { NetworkStatusFactory() } + + private val networksStatuses: MutableStateFlow> = MutableStateFlow(emptyList()) + + override fun getNetworks(networksIds: Set): Set { + return networkConverter.convertSet(networksIds) + } + + override fun getNetworkStatuses( + userWalletId: UserWalletId, + networks: Set, + refresh: Boolean, + ): Flow> = channelFlow { + launch(dispatchers.io) { + networksStatuses.collect { + send(it.toSet()) + } + } + + launch(dispatchers.io) { + fetchNetworksStatusesIfCacheExpired(userWalletId, networks, refresh) + } + } + + private suspend fun fetchNetworksStatusesIfCacheExpired( + userWalletId: UserWalletId, + 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) + } + } + .awaitAll() + } + } + + private suspend fun fetchNetworkStatus(userWalletId: UserWalletId, networkId: Network.ID) { + val currencies = getCurrencies(userWalletId) + .asSequence() + .filter { it.networkId == networkId } + + val result = walletManagersFacade.update( + userWalletId = userWalletId, + networkId = networkId, + extraTokens = currencies.filterIsInstance().toSet(), + ) + val networkStatus = networkStatusFactory.createNetworkStatus( + networkId = networkId, + result = result, + currencies = currencies.toSet(), + ) + + networksStatuses.update { statuses -> + statuses.addOrReplace(networkStatus) { it.networkId == networkStatus.networkId } + } + } + + private suspend fun getCurrencies(userWalletId: UserWalletId): List { + val userWallet = requireNotNull(userWalletsStore.getSyncOrNull(userWalletId)) { + "Unable to find user wallet with provided ID: $userWalletId" + } + + return 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) + } else { + val currency = cardCurrenciesFactory.createPrimaryCurrencyForSingleCurrencyCard(userWallet.scanResponse) + + listOf(currency) + } + } + + private fun getNetworksStatusesCacheKey(userWalletId: UserWalletId): String = "network_status_$userWalletId" +} \ No newline at end of file diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultQuotesRepository.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultQuotesRepository.kt new file mode 100644 index 0000000000..25fb880a50 --- /dev/null +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultQuotesRepository.kt @@ -0,0 +1,97 @@ +package com.tangem.data.tokens.repository + +import com.tangem.data.common.cache.CacheRegistry +import com.tangem.data.tokens.utils.QuotesConverter +import com.tangem.datasource.api.tangemTech.TangemTechApi +import com.tangem.datasource.local.appcurrency.SelectedAppCurrencyStore +import com.tangem.datasource.local.quote.QuotesStore +import com.tangem.domain.tokens.models.CryptoCurrency +import com.tangem.domain.tokens.models.Quote +import com.tangem.domain.tokens.repository.QuotesRepository +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.channelFlow +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.launch +import timber.log.Timber + +internal class DefaultQuotesRepository( + private val tangemTechApi: TangemTechApi, + private val quotesStore: QuotesStore, + private val selectedAppCurrencyStore: SelectedAppCurrencyStore, + private val cacheRegistry: CacheRegistry, + private val dispatchers: CoroutineDispatcherProvider, +) : QuotesRepository { + + private val quotesConverter = QuotesConverter() + + private var quotesFetchedForAppCurrency: String? = null + + override fun getQuotes(currenciesIds: Set, refresh: Boolean): Flow> { + return channelFlow { + launch(dispatchers.io) { + quotesStore.get(currenciesIds) + .map(quotesConverter::convertSet) + .collect(::send) + } + + launch(dispatchers.io) { + selectedAppCurrencyStore.get().collectLatest { appCurrency -> + fetchExpiredQuotes(currenciesIds, appCurrency.id, refresh) + } + } + } + } + + private suspend fun fetchExpiredQuotes( + currenciesIds: Set, + appCurrencyId: String, + refresh: Boolean, + ) { + val expiredCurrenciesIds = filterExpiredCurrenciesIds( + currenciesIds = currenciesIds, + refresh = refresh || quotesFetchedForAppCurrency != appCurrencyId, + ) + if (expiredCurrenciesIds.isEmpty()) return + + quotesFetchedForAppCurrency = appCurrencyId + + fetchQuotes(expiredCurrenciesIds, appCurrencyId) + } + + private suspend fun fetchQuotes(rawCurrenciesIds: Set, appCurrencyId: String) { + try { + val response = tangemTechApi.getQuotes( + currencyId = appCurrencyId, + coinIds = rawCurrenciesIds.joinToString(separator = ","), + ) + + quotesStore.store(response) + } catch (e: Throwable) { + Timber.e(e, "Unable to fetch quotes for: $rawCurrenciesIds") + throw e + } + } + + private suspend fun filterExpiredCurrenciesIds( + currenciesIds: Set, + refresh: Boolean, + ): Set { + return currenciesIds.fold(hashSetOf()) { acc, currencyId -> + val rawCurrencyId = currencyId.rawCurrencyId + + if (rawCurrencyId != null && rawCurrencyId !in acc) { + cacheRegistry.invokeOnExpire( + key = getQuoteCacheKey(rawCurrencyId), + skipCache = refresh, + block = { acc.add(rawCurrencyId) }, + ) + } + + acc + } + } + + private fun getQuoteCacheKey(rawCurrencyId: String): String = "quote_$rawCurrencyId" +} \ No newline at end of file diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/MockNetworksRepository.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/MockNetworksRepository.kt deleted file mode 100644 index 3455d66764..0000000000 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/MockNetworksRepository.kt +++ /dev/null @@ -1,31 +0,0 @@ -package com.tangem.data.tokens.repository - -import com.tangem.data.tokens.mock.MockNetworks -import com.tangem.domain.tokens.model.CryptoCurrency -import com.tangem.domain.tokens.model.NetworkStatus -import com.tangem.domain.tokens.models.Network -import com.tangem.domain.tokens.repository.NetworksRepository -import com.tangem.domain.wallets.models.UserWalletId -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.flowOf - -internal class MockNetworksRepository : NetworksRepository { - - override fun getNetworks(networksIds: Set): Set { - return MockNetworks.networks - .filter { it.id in networksIds } - .toSet() - } - - override fun getNetworkStatuses( - userWalletId: UserWalletId, - networks: Map>, - refresh: Boolean, - ): Flow> { - return flowOf( - MockNetworks.networksStatuses - .filter { it.networkId in networks.keys } - .toSet(), - ) - } -} \ No newline at end of file diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/MockQuotesRepository.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/MockQuotesRepository.kt deleted file mode 100644 index 345f17e6bc..0000000000 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/MockQuotesRepository.kt +++ /dev/null @@ -1,19 +0,0 @@ -package com.tangem.data.tokens.repository - -import com.tangem.data.tokens.mock.MockQuotes -import com.tangem.domain.tokens.model.CryptoCurrency -import com.tangem.domain.tokens.model.Quote -import com.tangem.domain.tokens.repository.QuotesRepository -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.flowOf - -internal class MockQuotesRepository : QuotesRepository { - - override fun getQuotes(tokensIds: Set, refresh: Boolean): Flow> { - return flowOf( - MockQuotes.quotes - .filter { it.currencyId in tokensIds } - .toSet(), - ) - } -} \ 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 index 15fd96b0aa..269c65e9f7 100644 --- 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 @@ -1,18 +1,23 @@ 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.model.CryptoCurrency -import timber.log.Timber -import com.tangem.blockchain.common.Token as SdkToken +import com.tangem.domain.tokens.models.CryptoCurrency internal class CardCurrenciesFactory(private val demoConfig: DemoConfig) { - fun createDefaultCoinsForMultiCurrencyCard(card: CardDTO): Set { + private val cryptoCurrencyFactory by lazy { CryptoCurrencyFactory() } + + fun createDefaultCoinsForMultiCurrencyCard( + card: CardDTO, + derivationStyleProvider: DerivationStyleProvider, + ): List { var blockchains = if (demoConfig.isDemoCardId(card.cardId)) { demoConfig.demoBlockchains } else { @@ -23,57 +28,21 @@ internal class CardCurrenciesFactory(private val demoConfig: DemoConfig) { blockchains = blockchains.mapNotNull { it.getTestnetVersion() } } - return blockchains.mapNotNull { createCoin(it, card) }.toSet() + return blockchains.mapNotNull { cryptoCurrencyFactory.createCoin(it, derivationStyleProvider) } } fun createPrimaryCurrencyForSingleCurrencyCard(scanResponse: ScanResponse): CryptoCurrency { - val card = scanResponse.card + val derivationStyleProvider = scanResponse.derivationStyleProvider val resolver = scanResponse.cardTypesResolver val blockchain = resolver.getBlockchain() - val coin = requireNotNull(createCoin(blockchain, card)) { + val coin = requireNotNull(cryptoCurrencyFactory.createCoin(blockchain, derivationStyleProvider)) { "Coin for the single currency card cannot be null" } val primaryToken = resolver.getPrimaryToken()?.let { token -> - createToken(token, blockchain, card) + cryptoCurrencyFactory.createToken(token, blockchain, derivationStyleProvider) } return primaryToken ?: coin } - - private fun createToken(sdkToken: SdkToken, blockchain: Blockchain, card: CardDTO): CryptoCurrency.Token? { - if (blockchain != Blockchain.Unknown) { - Timber.e("Unable to map the SDK token to the domain token with Unknown blockchain") - return null - } - - return CryptoCurrency.Token( - id = getTokenId(blockchain, sdkToken), - networkId = getNetworkId(blockchain), - name = sdkToken.name, - symbol = sdkToken.symbol, - iconUrl = getTokenIconUrl(blockchain, sdkToken), - decimals = sdkToken.decimals, - isCustom = false, - contractAddress = sdkToken.contractAddress, - derivationPath = getDerivationPath(blockchain, card), - ) - } - - private fun createCoin(blockchain: Blockchain, card: CardDTO): CryptoCurrency.Coin? { - if (blockchain != Blockchain.Unknown) { - Timber.e("Unable to map the SDK token to the domain token with Unknown blockchain") - return null - } - - return CryptoCurrency.Coin( - id = getCoinId(blockchain), - networkId = getNetworkId(blockchain), - name = blockchain.fullName, - symbol = blockchain.currency, - iconUrl = getCoinIconUrl(blockchain), - decimals = blockchain.decimals(), - derivationPath = getDerivationPath(blockchain, card), - ) - } } \ 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 new file mode 100644 index 0000000000..f4e0148b34 --- /dev/null +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/CryptoCurrencyFactory.kt @@ -0,0 +1,53 @@ +package com.tangem.data.tokens.utils + +import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchain.common.Token as SdkToken +import com.tangem.domain.common.DerivationStyleProvider +import com.tangem.domain.tokens.models.CryptoCurrency +import timber.log.Timber + +class CryptoCurrencyFactory { + + fun createToken( + sdkToken: SdkToken, + blockchain: Blockchain, + derivationStyleProvider: DerivationStyleProvider, + ): CryptoCurrency.Token? { + if (blockchain == Blockchain.Unknown) { + Timber.e("Unable to map the SDK token to the domain token with Unknown blockchain") + return null + } + + val id = getTokenId(blockchain, sdkToken) + return CryptoCurrency.Token( + id = id, + networkId = getNetworkId(blockchain), + name = sdkToken.name, + symbol = sdkToken.symbol, + iconUrl = getTokenIconUrl(blockchain, sdkToken), + decimals = sdkToken.decimals, + isCustom = isCustomToken(id), + contractAddress = sdkToken.contractAddress, + derivationPath = getDerivationPath(blockchain, derivationStyleProvider), + blockchainName = blockchain.fullName, + standardType = getTokenStandardType(blockchain, sdkToken), + ) + } + + fun createCoin(blockchain: Blockchain, 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 + } + + return CryptoCurrency.Coin( + id = getCoinId(blockchain), + networkId = getNetworkId(blockchain), + name = blockchain.fullName, + symbol = blockchain.currency, + iconUrl = getCoinIconUrl(blockchain), + decimals = blockchain.decimals(), + derivationPath = getDerivationPath(blockchain, derivationStyleProvider), + ) + } +} \ 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 new file mode 100644 index 0000000000..23e27fe466 --- /dev/null +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/NetworkConverter.kt @@ -0,0 +1,31 @@ +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 +import timber.log.Timber + +internal class NetworkConverter : Converter { + + override fun convert(value: Network.ID): Network? { + val blockchain = Blockchain.fromId(value.value) + + if (blockchain == Blockchain.Unknown) { + Timber.e("Unable to convert Unknown blockchain to the domain network model") + return null + } + + return Network( + id = value, + name = blockchain.fullName, + ) + } + + 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/NetworkStatusFactory.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/NetworkStatusFactory.kt new file mode 100644 index 0000000000..6feee38172 --- /dev/null +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/NetworkStatusFactory.kt @@ -0,0 +1,51 @@ +package com.tangem.data.tokens.utils + +import com.tangem.domain.tokens.model.NetworkStatus +import com.tangem.domain.tokens.models.CryptoCurrency +import com.tangem.domain.tokens.models.Network +import com.tangem.domain.walletmanager.model.CryptoCurrencyAmount +import com.tangem.domain.walletmanager.model.UpdateWalletManagerResult +import java.math.BigDecimal + +internal class NetworkStatusFactory { + + fun createNetworkStatus( + networkId: Network.ID, + result: UpdateWalletManagerResult, + currencies: Set, + ): NetworkStatus { + return NetworkStatus( + networkId = networkId, + value = when (result) { + is UpdateWalletManagerResult.MissedDerivation -> NetworkStatus.MissedDerivation + is UpdateWalletManagerResult.Unreachable -> NetworkStatus.Unreachable + is UpdateWalletManagerResult.NoAccount -> NetworkStatus.NoAccount(result.amountToCreateAccount) + is UpdateWalletManagerResult.Verified -> NetworkStatus.Verified( + amounts = formatAmounts(result.tokensAmounts, currencies), + hasTransactionsInProgress = result.hasTransactionsInProgress, + ) + }, + ) + } + + private fun formatAmounts( + amounts: Set, + currencies: Set, + ): Map { + return amounts + .asSequence() + .mapNotNull { amount -> + val currency = when (amount) { + is CryptoCurrencyAmount.Coin -> currencies.singleOrNull { it is CryptoCurrency.Coin } + is CryptoCurrencyAmount.Token -> currencies.firstOrNull { + it is CryptoCurrency.Token && + it.id.rawCurrencyId == amount.id && + it.contractAddress == amount.tokenContractAddress + } + } + + currency?.id?.let { it to amount.value } + } + .toMap() + } +} \ No newline at end of file diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/QuotesConverter.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/QuotesConverter.kt new file mode 100644 index 0000000000..b771150c67 --- /dev/null +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/QuotesConverter.kt @@ -0,0 +1,18 @@ +package com.tangem.data.tokens.utils + +import com.tangem.datasource.local.quote.model.StoredQuote +import com.tangem.domain.tokens.models.Quote +import com.tangem.utils.converter.Converter + +internal class QuotesConverter : Converter { + + override fun convert(value: StoredQuote): Quote { + val (rawCurrencyId, responseQuote) = value + + return Quote( + rawCurrencyId = rawCurrencyId, + fiatRate = responseQuote.price, + priceChange = responseQuote.priceChange.movePointLeft(2), + ) + } +} \ No newline at end of file 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 0a6c78b0e5..b79f32f515 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 @@ -6,17 +6,29 @@ import com.tangem.datasource.api.tangemTech.models.UserTokensResponse import com.tangem.domain.common.extensions.fromNetworkId import com.tangem.domain.demo.DemoConfig import com.tangem.domain.models.scan.CardDTO -import com.tangem.domain.tokens.model.CryptoCurrency +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) { - fun createTokens(response: UserTokensResponse, card: CardDTO): Set { - return response.tokens.mapNotNull { createToken(it, card) }.toSet() + fun createCurrency(currencyId: CryptoCurrency.ID, response: UserTokensResponse, card: CardDTO): 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)) { + "Unable to create a currency with provided ID: $currencyId" + } } - private fun createToken(responseToken: UserTokensResponse.Token, card: CardDTO): CryptoCurrency? { + fun createCurrencies(response: UserTokensResponse, card: CardDTO): List { + return response.tokens.mapNotNull { createCurrency(it, card) } + } + + private 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}") @@ -72,6 +84,8 @@ internal class ResponseCurrenciesFactory(private val demoConfig: DemoConfig) { iconUrl = getTokenIconUrl(blockchain, sdkToken), contractAddress = sdkToken.contractAddress, isCustom = isCustomToken(id), + blockchainName = blockchain.fullName, + standardType = getTokenStandardType(blockchain, sdkToken), ) } } \ 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 2d0b915285..2298a28fb2 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,33 +2,29 @@ package com.tangem.data.tokens.utils import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.IconsUtil -import com.tangem.domain.common.TapWorkarounds.derivationStyle +import com.tangem.domain.common.DerivationStyleProvider import com.tangem.domain.common.extensions.toCoinId import com.tangem.domain.common.extensions.toNetworkId -import com.tangem.domain.models.scan.CardDTO -import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.tokens.models.CryptoCurrency +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.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 private const val DEFAULT_TOKENS_ICONS_HOST = "https://s3.eu-central-1.amazonaws.com/tangem.api/coins" private const val TOKEN_ICON_SIZE = "large" private const val TOKEN_ICON_EXT = "png" -private const val COIN_ID_PREFIX = "coin_" -private const val TOKEN_ID_PREFIX = "token_" -private const val CUSTOM_TOKEN_ID_PREFIX = "custom_token_" -private const val TOKEN_ID_DELIMITER = '#' - -internal fun isCustomToken(tokenId: CryptoCurrency.ID): Boolean { - return tokenId.value.startsWith(CUSTOM_TOKEN_ID_PREFIX) +internal fun isCustomToken(tokenId: ID): Boolean { + return tokenId.rawCurrencyId == null } -internal fun getDerivationPath(blockchain: Blockchain, card: CardDTO): String? { - return if (card.settings.isHDWalletAllowed) { - blockchain.derivationPath(card.derivationStyle)?.rawPath - } else { - null - } +internal fun getDerivationPath(blockchain: Blockchain, derivationStyleProvider: DerivationStyleProvider): String? { + return blockchain.derivationPath(derivationStyleProvider.getDerivationStyle())?.rawPath } internal fun getBlockchain(networkId: Network.ID): Blockchain { @@ -41,17 +37,22 @@ internal fun getNetworkId(blockchain: Blockchain): Network.ID { return Network.ID(value) } -internal fun getCoinId(blockchain: Blockchain): CryptoCurrency.ID { +internal fun getCoinId(blockchain: Blockchain): ID { return getTokenOrCoinId(blockchain, token = null) } -internal fun getTokenId(blockchain: Blockchain, token: SdkToken): CryptoCurrency.ID { +internal fun getTokenId(blockchain: Blockchain, token: SdkToken): ID { return getTokenOrCoinId(blockchain, token) } -internal fun getResponseTokenId(currency: CryptoCurrency): String? { - return currency.id.value.substringAfter(TOKEN_ID_DELIMITER) - .takeUnless { currency is CryptoCurrency.Token && currency.isCustom } +internal fun getTokenStandardType(blockchain: Blockchain, token: SdkToken): CryptoCurrency.StandardType { + return when (blockchain) { + Blockchain.Ethereum, Blockchain.EthereumTestnet -> CryptoCurrency.StandardType.ERC20 + Blockchain.BSC, Blockchain.BSCTestnet -> CryptoCurrency.StandardType.BEP20 + Blockchain.Binance, Blockchain.BinanceTestnet -> CryptoCurrency.StandardType.BEP2 + Blockchain.Tron, Blockchain.TronTestnet -> CryptoCurrency.StandardType.TRC20 + else -> CryptoCurrency.StandardType.Unspecified(token.name) + } } internal fun getTokenIconUrl(blockchain: Blockchain, token: SdkToken): String? { @@ -74,22 +75,15 @@ internal fun getCoinIconUrl(blockchain: Blockchain): String? { return coinId?.let(::getTokenIconUrlFromDefaultHost) } -private fun getTokenOrCoinId(blockchain: Blockchain, token: SdkToken?): CryptoCurrency.ID { +private fun getTokenOrCoinId(blockchain: Blockchain, token: SdkToken?): ID { val sdkTokenId = token?.id val (prefix, suffix) = when { - token == null -> COIN_ID_PREFIX to blockchain.toCoinId() - sdkTokenId == null -> CUSTOM_TOKEN_ID_PREFIX to token.contractAddress - else -> TOKEN_ID_PREFIX to sdkTokenId + 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) } - val value = buildString { - append(prefix) - append(blockchain.id) - append(TOKEN_ID_DELIMITER) - append(suffix.lowercase()) - } - - return CryptoCurrency.ID(value) + return ID(prefix, getNetworkId(blockchain), 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 ffb4fc9c10..d112ad4476 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 @@ -2,12 +2,12 @@ package com.tangem.data.tokens.utils import com.tangem.datasource.api.tangemTech.models.UserTokensResponse import com.tangem.domain.common.extensions.toNetworkId -import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.tokens.models.CryptoCurrency internal class UserTokensResponseFactory { fun createUserTokensResponse( - currencies: Set, + currencies: List, isGroupedByNetwork: Boolean, isSortedByBalance: Boolean, ): UserTokensResponse { @@ -30,7 +30,7 @@ internal class UserTokensResponseFactory { val blockchain = getBlockchain(currency.networkId) return UserTokensResponse.Token( - id = getResponseTokenId(currency), + id = currency.id.rawCurrencyId, networkId = blockchain.toNetworkId(), derivationPath = currency.derivationPath, name = currency.name, diff --git a/data/txhistory/build.gradle.kts b/data/txhistory/build.gradle.kts index d2aa98d0fd..7ffda888cc 100644 --- a/data/txhistory/build.gradle.kts +++ b/data/txhistory/build.gradle.kts @@ -10,7 +10,13 @@ android { } dependencies { + implementation(projects.core.utils) + implementation(projects.core.datasource) + implementation(projects.domain.legacy) + implementation(projects.domain.tokens.models) implementation(projects.domain.txhistory) + implementation(projects.domain.txhistory.models) + implementation(projects.domain.wallets.models) implementation(deps.kotlin.coroutines) implementation(deps.androidx.paging.runtime) diff --git a/data/txhistory/src/main/kotlin/com/tangem/data/txhistory/di/TxHistoryDataModule.kt b/data/txhistory/src/main/kotlin/com/tangem/data/txhistory/di/TxHistoryDataModule.kt index 6fe0d348de..da8b08603c 100644 --- a/data/txhistory/src/main/kotlin/com/tangem/data/txhistory/di/TxHistoryDataModule.kt +++ b/data/txhistory/src/main/kotlin/com/tangem/data/txhistory/di/TxHistoryDataModule.kt @@ -1,7 +1,9 @@ package com.tangem.data.txhistory.di -import com.tangem.data.txhistory.repository.MockTxHistoryRepository +import com.tangem.data.txhistory.repository.DefaultTxHistoryRepository +import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.domain.txhistory.repository.TxHistoryRepository +import com.tangem.domain.walletmanager.WalletManagersFacade import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -14,5 +16,11 @@ internal object TxHistoryDataModule { @Provides @Singleton - fun provideTxHistoryRepository(): TxHistoryRepository = MockTxHistoryRepository() + fun provideTxHistoryRepository( + walletManagersFacade: WalletManagersFacade, + userWalletsStore: UserWalletsStore, + ): TxHistoryRepository = DefaultTxHistoryRepository( + walletManagersFacade = walletManagersFacade, + userWalletsStore = userWalletsStore, + ) } \ No newline at end of file diff --git a/data/txhistory/src/main/kotlin/com/tangem/data/txhistory/mock/MockTxHistoryItems.kt b/data/txhistory/src/main/kotlin/com/tangem/data/txhistory/mock/MockTxHistoryItems.kt deleted file mode 100644 index 583fbaa5ee..0000000000 --- a/data/txhistory/src/main/kotlin/com/tangem/data/txhistory/mock/MockTxHistoryItems.kt +++ /dev/null @@ -1,70 +0,0 @@ -package com.tangem.data.txhistory.mock - -import com.tangem.domain.txhistory.model.TxHistoryItem -import java.math.BigDecimal - -internal object MockTxHistoryItems { - - private val txHistoryItem1 = TxHistoryItem( - txHash = "noster", - timestamp = System.currentTimeMillis(), - direction = TxHistoryItem.TransactionDirection.Incoming("address"), - status = TxHistoryItem.TxStatus.Confirmed, - type = TxHistoryItem.TransactionType.Transfer, - amount = BigDecimal("1000000000.5"), - ) - - private val txHistoryItem2 = TxHistoryItem( - txHash = "noster", - timestamp = 1689844346000, - direction = TxHistoryItem.TransactionDirection.Incoming("address2"), - status = TxHistoryItem.TxStatus.Unconfirmed, - type = TxHistoryItem.TransactionType.Transfer, - amount = BigDecimal("1000000000.5"), - ) - - private val txHistoryItem3 = TxHistoryItem( - txHash = "noster", - timestamp = 1689757946000, - direction = TxHistoryItem.TransactionDirection.Outgoing("address3"), - status = TxHistoryItem.TxStatus.Confirmed, - type = TxHistoryItem.TransactionType.Transfer, - amount = BigDecimal("1000000000.5"), - ) - - private val txHistoryItem4 = TxHistoryItem( - txHash = "noster", - timestamp = 1689671546000, - direction = TxHistoryItem.TransactionDirection.Incoming("address4"), - status = TxHistoryItem.TxStatus.Confirmed, - type = TxHistoryItem.TransactionType.Transfer, - amount = BigDecimal("1000000000.5"), - ) - - private val txHistoryItem5 = TxHistoryItem( - txHash = "noster", - timestamp = 1689585146000, - direction = TxHistoryItem.TransactionDirection.Outgoing("address5"), - status = TxHistoryItem.TxStatus.Unconfirmed, - type = TxHistoryItem.TransactionType.Transfer, - amount = BigDecimal("1000000000.5"), - ) - - private val txHistoryItem6 = TxHistoryItem( - txHash = "noster", - timestamp = 1689585146000, - direction = TxHistoryItem.TransactionDirection.Incoming("address6"), - status = TxHistoryItem.TxStatus.Confirmed, - type = TxHistoryItem.TransactionType.Transfer, - amount = BigDecimal("1000000000.5"), - ) - - val txHistoryItems = listOf( - txHistoryItem1, - txHistoryItem2, - txHistoryItem3, - txHistoryItem4, - txHistoryItem5, - txHistoryItem6, - ) -} \ No newline at end of file diff --git a/data/txhistory/src/main/kotlin/com/tangem/data/txhistory/repository/DefaultTxHistoryRepository.kt b/data/txhistory/src/main/kotlin/com/tangem/data/txhistory/repository/DefaultTxHistoryRepository.kt new file mode 100644 index 0000000000..a042400894 --- /dev/null +++ b/data/txhistory/src/main/kotlin/com/tangem/data/txhistory/repository/DefaultTxHistoryRepository.kt @@ -0,0 +1,66 @@ +package com.tangem.data.txhistory.repository + +import androidx.paging.Pager +import androidx.paging.PagingConfig +import androidx.paging.PagingData +import com.tangem.data.txhistory.repository.paging.TxHistoryPagingSource +import com.tangem.datasource.local.userwallet.UserWalletsStore +import com.tangem.domain.tokens.models.Network +import com.tangem.domain.txhistory.models.TxHistoryItem +import com.tangem.domain.txhistory.models.TxHistoryState +import com.tangem.domain.txhistory.models.TxHistoryStateError +import com.tangem.domain.txhistory.repository.TxHistoryRepository +import com.tangem.domain.walletmanager.WalletManagersFacade +import com.tangem.domain.wallets.models.UserWallet +import kotlinx.coroutines.flow.Flow + +class DefaultTxHistoryRepository( + private val walletManagersFacade: WalletManagersFacade, + private val userWalletsStore: UserWalletsStore, +) : TxHistoryRepository { + + override suspend fun getTxHistoryItemsCount(networkId: Network.ID, derivationPath: String?): Int { + val userWallet = getUserWallet() + val state = walletManagersFacade.getTxHistoryState( + userWalletId = userWallet.walletId, + networkId = networkId, + rawDerivationPath = derivationPath, + ) + return when (state) { + is TxHistoryState.Failed.FetchError -> throw TxHistoryStateError.DataError(state.exception) + TxHistoryState.NotImplemented -> throw TxHistoryStateError.TxHistoryNotImplemented + TxHistoryState.Success.Empty -> throw TxHistoryStateError.EmptyTxHistories + is TxHistoryState.Success.HasTransactions -> state.txCount + } + } + + override fun getTxHistoryItems( + networkId: Network.ID, + derivationPath: String?, + pageSize: Int, + ): Flow> { + val userWallet = getUserWallet() + return Pager( + config = PagingConfig( + pageSize = pageSize, + ), + pagingSourceFactory = { + TxHistoryPagingSource( + loadPage = { page: Int, pageSize: Int -> + walletManagersFacade.getTxHistoryItems( + userWalletId = userWallet.walletId, + networkId = networkId, + rawDerivationPath = derivationPath, + page = page, + pageSize = pageSize, + ) + }, + ) + }, + ).flow + } + + private fun getUserWallet(): UserWallet = requireNotNull(userWalletsStore.selectedUserWalletOrNull) { + "Selected wallet must not be null" + } +} \ No newline at end of file diff --git a/data/txhistory/src/main/kotlin/com/tangem/data/txhistory/repository/MockTxHistoryRepository.kt b/data/txhistory/src/main/kotlin/com/tangem/data/txhistory/repository/MockTxHistoryRepository.kt deleted file mode 100644 index 59829686e4..0000000000 --- a/data/txhistory/src/main/kotlin/com/tangem/data/txhistory/repository/MockTxHistoryRepository.kt +++ /dev/null @@ -1,25 +0,0 @@ -package com.tangem.data.txhistory.repository - -import androidx.paging.Pager -import androidx.paging.PagingConfig -import androidx.paging.PagingData -import com.tangem.data.txhistory.repository.paging.TxHistoryPagingSource -import com.tangem.domain.txhistory.model.TxHistoryItem -import com.tangem.domain.txhistory.repository.TxHistoryRepository -import kotlinx.coroutines.flow.Flow - -internal class MockTxHistoryRepository : TxHistoryRepository { - - override suspend fun getTxHistoryItemsCount(networkId: String, derivationPath: String): Int { - return 0 - } - - override fun getTxHistoryItems(networkId: String, pageSize: Int): Flow> { - return Pager( - config = PagingConfig( - pageSize = pageSize, - ), - pagingSourceFactory = { TxHistoryPagingSource() }, - ).flow - } -} \ No newline at end of file diff --git a/data/txhistory/src/main/kotlin/com/tangem/data/txhistory/repository/paging/TxHistoryPagingSource.kt b/data/txhistory/src/main/kotlin/com/tangem/data/txhistory/repository/paging/TxHistoryPagingSource.kt index 3eba3ed6ce..a3ebb06194 100644 --- a/data/txhistory/src/main/kotlin/com/tangem/data/txhistory/repository/paging/TxHistoryPagingSource.kt +++ b/data/txhistory/src/main/kotlin/com/tangem/data/txhistory/repository/paging/TxHistoryPagingSource.kt @@ -2,12 +2,14 @@ package com.tangem.data.txhistory.repository.paging import androidx.paging.PagingSource import androidx.paging.PagingState -import com.tangem.data.txhistory.mock.MockTxHistoryItems -import com.tangem.domain.txhistory.model.TxHistoryItem +import com.tangem.domain.txhistory.models.PaginationWrapper +import com.tangem.domain.txhistory.models.TxHistoryItem private const val INITIAL_PAGE = 1 -internal class TxHistoryPagingSource : PagingSource() { +internal class TxHistoryPagingSource( + private val loadPage: suspend (page: Int, pageSize: Int) -> PaginationWrapper, +) : PagingSource() { override fun getRefreshKey(state: PagingState): Int? { return state.anchorPosition?.let { anchorPosition -> @@ -19,20 +21,12 @@ internal class TxHistoryPagingSource : PagingSource() { override suspend fun load(params: LoadParams): LoadResult { val currentPage = params.key ?: INITIAL_PAGE return try { - // TODO: [REDACTED_JIRA] - // val result = txHistoryManager.getTxHistoryItems( - // networkId = networkId, - // derivationPath = derivationPath, - // page = currentPage, - // pageSize = params.loadSize, - // ) - val result = MockTxHistoryItems.txHistoryItems + val result = loadPage(currentPage, params.loadSize) LoadResult.Page( - data = result, + data = result.items, prevKey = if (currentPage > INITIAL_PAGE) currentPage.minus(1) else null, - // TODO: handle end of reached [REDACTED_JIRA] - nextKey = null, + nextKey = if (result.page < result.totalPages) currentPage.plus(1) else null, ) } catch (e: Exception) { LoadResult.Error(e) diff --git a/domain/app-currency/.gitignore b/domain/app-currency/.gitignore new file mode 100644 index 0000000000..796b96d1c4 --- /dev/null +++ b/domain/app-currency/.gitignore @@ -0,0 +1 @@ +/build diff --git a/domain/app-currency/build.gradle.kts b/domain/app-currency/build.gradle.kts new file mode 100644 index 0000000000..45af2fd004 --- /dev/null +++ b/domain/app-currency/build.gradle.kts @@ -0,0 +1,12 @@ +plugins { + alias(deps.plugins.kotlin.jvm) + id("configuration") +} + +dependencies { + + /** Project - Domain */ + implementation(projects.core.utils) + implementation(projects.domain.core) + implementation(projects.domain.appCurrency.models) +} \ No newline at end of file diff --git a/domain/app-currency/models/.gitignore b/domain/app-currency/models/.gitignore new file mode 100644 index 0000000000..796b96d1c4 --- /dev/null +++ b/domain/app-currency/models/.gitignore @@ -0,0 +1 @@ +/build diff --git a/domain/app-currency/models/build.gradle.kts b/domain/app-currency/models/build.gradle.kts new file mode 100644 index 0000000000..7ff7fb7522 --- /dev/null +++ b/domain/app-currency/models/build.gradle.kts @@ -0,0 +1,4 @@ +plugins { + alias(deps.plugins.kotlin.jvm) + id("configuration") +} \ No newline at end of file diff --git a/domain/app-currency/models/src/main/kotlin/com/tangem/domain/appcurrency/model/AppCurrency.kt b/domain/app-currency/models/src/main/kotlin/com/tangem/domain/appcurrency/model/AppCurrency.kt new file mode 100644 index 0000000000..aa8fcb62fc --- /dev/null +++ b/domain/app-currency/models/src/main/kotlin/com/tangem/domain/appcurrency/model/AppCurrency.kt @@ -0,0 +1,16 @@ +package com.tangem.domain.appcurrency.model + +data class AppCurrency( + val code: String, + val name: String, + val symbol: String, +) { + + companion object { + val Default = AppCurrency( + code = "USD", + name = "US Dollar", + symbol = "$", + ) + } +} \ No newline at end of file diff --git a/domain/app-currency/src/main/kotlin/com/tangem/domain/appcurrency/GetSelectedAppCurrencyUseCase.kt b/domain/app-currency/src/main/kotlin/com/tangem/domain/appcurrency/GetSelectedAppCurrencyUseCase.kt new file mode 100644 index 0000000000..4bd3a52e99 --- /dev/null +++ b/domain/app-currency/src/main/kotlin/com/tangem/domain/appcurrency/GetSelectedAppCurrencyUseCase.kt @@ -0,0 +1,24 @@ +package com.tangem.domain.appcurrency + +import arrow.core.Either +import arrow.core.left +import arrow.core.right +import com.tangem.domain.appcurrency.error.SelectedAppCurrencyError +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.appcurrency.repository.AppCurrencyRepository +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onEmpty + +class GetSelectedAppCurrencyUseCase( + private val appCurrencyRepository: AppCurrencyRepository, +) { + + operator fun invoke(): Flow> { + return appCurrencyRepository.getSelectedAppCurrency() + .map> { it.right() } + .catch { emit(SelectedAppCurrencyError.DataError(it).left()) } + .onEmpty { emit(SelectedAppCurrencyError.NoAppCurrencySelected.left()) } + } +} \ No newline at end of file diff --git a/domain/app-currency/src/main/kotlin/com/tangem/domain/appcurrency/error/SelectedAppCurrencyError.kt b/domain/app-currency/src/main/kotlin/com/tangem/domain/appcurrency/error/SelectedAppCurrencyError.kt new file mode 100644 index 0000000000..4fc83d8671 --- /dev/null +++ b/domain/app-currency/src/main/kotlin/com/tangem/domain/appcurrency/error/SelectedAppCurrencyError.kt @@ -0,0 +1,8 @@ +package com.tangem.domain.appcurrency.error + +sealed class SelectedAppCurrencyError { + + object NoAppCurrencySelected : SelectedAppCurrencyError() + + data class DataError(val cause: Throwable) : SelectedAppCurrencyError() +} \ No newline at end of file diff --git a/domain/app-currency/src/main/kotlin/com/tangem/domain/appcurrency/repository/AppCurrencyRepository.kt b/domain/app-currency/src/main/kotlin/com/tangem/domain/appcurrency/repository/AppCurrencyRepository.kt new file mode 100644 index 0000000000..3ba529f8ae --- /dev/null +++ b/domain/app-currency/src/main/kotlin/com/tangem/domain/appcurrency/repository/AppCurrencyRepository.kt @@ -0,0 +1,13 @@ +package com.tangem.domain.appcurrency.repository + +import com.tangem.domain.appcurrency.model.AppCurrency +import kotlinx.coroutines.flow.Flow + +interface AppCurrencyRepository { + + fun getSelectedAppCurrency(): Flow + + suspend fun getAvailableAppCurrencies(): List + + suspend fun changeAppCurrency(currencyCode: String) +} \ No newline at end of file diff --git a/domain/core/src/main/kotlin/com/tangem/domain/core/error/DataError.kt b/domain/core/src/main/kotlin/com/tangem/domain/core/error/DataError.kt index 11567ca43b..35c115a1e8 100644 --- a/domain/core/src/main/kotlin/com/tangem/domain/core/error/DataError.kt +++ b/domain/core/src/main/kotlin/com/tangem/domain/core/error/DataError.kt @@ -6,4 +6,9 @@ sealed class DataError : Exception() { object NoInternetConnection : NetworkError() } + + sealed class UserWalletError : DataError() { + + data class WrongUserWallet(override val message: String) : UserWalletError() + } } \ No newline at end of file diff --git a/domain/core/src/main/kotlin/com/tangem/domain/core/raise/DelegatedRaise.kt b/domain/core/src/main/kotlin/com/tangem/domain/core/raise/DelegatedRaise.kt deleted file mode 100644 index ffde660eb6..0000000000 --- a/domain/core/src/main/kotlin/com/tangem/domain/core/raise/DelegatedRaise.kt +++ /dev/null @@ -1,13 +0,0 @@ -package com.tangem.domain.core.raise - -import arrow.core.raise.Raise - -abstract class DelegatedRaise( - private val otherRaise: Raise, - private val transformError: (Error) -> OtherError, -) : Raise { - - override fun raise(r: Error): Nothing { - otherRaise.raise(transformError(r)) - } -} \ No newline at end of file diff --git a/domain/legacy/build.gradle.kts b/domain/legacy/build.gradle.kts index 99665896d3..9ba0f990f3 100644 --- a/domain/legacy/build.gradle.kts +++ b/domain/legacy/build.gradle.kts @@ -11,8 +11,8 @@ dependencies { implementation(project(":libs:auth")) implementation(projects.domain.demo) implementation(projects.domain.models) - implementation(projects.domain.tokens) implementation(projects.domain.tokens.models) + implementation(projects.domain.txhistory.models) implementation(projects.domain.wallets.models) /** Tangem libraries */ diff --git a/domain/legacy/src/main/java/com/tangem/domain/common/BlockchainNetwork.kt b/domain/legacy/src/main/java/com/tangem/domain/common/BlockchainNetwork.kt index 212bd764be..8a81005383 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/common/BlockchainNetwork.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/common/BlockchainNetwork.kt @@ -5,8 +5,6 @@ import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.Token import com.tangem.blockchain.common.WalletManager import com.tangem.common.extensions.calculateHashCode -import com.tangem.domain.common.TapWorkarounds.derivationStyle -import com.tangem.domain.models.scan.CardDTO @JsonClass(generateAdapter = true) data class BlockchainNetwork( @@ -15,13 +13,9 @@ data class BlockchainNetwork( val tokens: List, ) { - constructor(blockchain: Blockchain, card: CardDTO) : this( + constructor(blockchain: Blockchain, derivationStyleProvider: DerivationStyleProvider) : this( blockchain = blockchain, - derivationPath = if (card.settings.isHDWalletAllowed) { - blockchain.derivationPath(card.derivationStyle)?.rawPath - } else { - null - }, + derivationPath = blockchain.derivationPath(derivationStyleProvider.getDerivationStyle())?.rawPath, tokens = emptyList(), ) diff --git a/domain/legacy/src/main/java/com/tangem/domain/common/DerivationStyleProvider.kt b/domain/legacy/src/main/java/com/tangem/domain/common/DerivationStyleProvider.kt new file mode 100644 index 0000000000..570071b37a --- /dev/null +++ b/domain/legacy/src/main/java/com/tangem/domain/common/DerivationStyleProvider.kt @@ -0,0 +1,26 @@ +package com.tangem.domain.common + +import com.tangem.blockchain.common.derivation.DerivationStyle +import com.tangem.domain.models.scan.CardDTO + +interface DerivationStyleProvider { + fun getDerivationStyle(): DerivationStyle? +} + +internal class TangemDerivationStyleProvider( + private val cardTypesResolver: CardTypesResolver, + private val card: CardDTO, +) : DerivationStyleProvider { + override fun getDerivationStyle(): DerivationStyle? { + return when { + !card.settings.isHDWalletAllowed -> null + firstBatchesOfWallet1(card) -> DerivationStyle.V1 + cardTypesResolver.isWallet2() -> DerivationStyle.V3 + else -> DerivationStyle.V2 + } + } + + private fun firstBatchesOfWallet1(card: CardDTO): Boolean { + return card.batchId == "AC01" || card.batchId == "AC02" || card.batchId == "CB95" + } +} \ No newline at end of file diff --git a/domain/legacy/src/main/java/com/tangem/domain/common/TangemCardTypesResolver.kt b/domain/legacy/src/main/java/com/tangem/domain/common/TangemCardTypesResolver.kt index 0eb5a72085..c3053f87b6 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/common/TangemCardTypesResolver.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/common/TangemCardTypesResolver.kt @@ -30,8 +30,7 @@ internal class TangemCardTypesResolver( } override fun isWallet2(): Boolean { - // todo for now disabled to prevent Shiba cards using as wallet 2, enable when release wallet 2.0 ([REDACTED_TASK_KEY]) - return false // card.firmwareVersion >= FirmwareVersion.KeysImportAvailable && card.settings.isKeysImportAllowed + return card.firmwareVersion >= FirmwareVersion.Ed25519Slip0010Available && card.settings.isKeysImportAllowed } override fun isTangemTwins(): Boolean = productType == ProductType.Twins @@ -104,6 +103,9 @@ internal class TangemCardTypesResolver( "BINANCE/test" -> { Blockchain.BSCTestnet } + "CARDANO" -> { + Blockchain.Cardano + } else -> { Blockchain.fromId(blockchainName) } 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 ccd30b6eb0..bd892377ae 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.blockchain.common.DerivationStyle import com.tangem.common.card.Card import com.tangem.common.card.FirmwareVersion import com.tangem.domain.models.scan.CardDTO @@ -14,6 +13,7 @@ object TapWorkarounds { private const val START_2_COIN_ISSUER = "start2coin" private const val TEST_CARD_BATCH = "99FF" private const val TEST_CARD_ID_STARTS_WITH = "FF99" + private val backupRequiredFirmwareVersion = FirmwareVersion(major = 6, minor = 21) val CardDTO.isTangemTwins: Boolean get() = TwinsHelper.getTwinCardNumber(cardId) != null @@ -26,19 +26,11 @@ object TapWorkarounds { // for cards 6.21 and higher backup is not skippable val CardDTO.canSkipBackup: Boolean - get() = this.firmwareVersion < FirmwareVersion.KeysImportAvailable + get() = this.firmwareVersion < backupRequiredFirmwareVersion val CardDTO.useOldStyleDerivation: Boolean get() = batchId == "AC01" || batchId == "AC02" || batchId == "CB95" - val CardDTO.derivationStyle: DerivationStyle? - get() = if (!settings.isHDWalletAllowed) { - null - } else if (useOldStyleDerivation) { - DerivationStyle.LEGACY - } else { - DerivationStyle.NEW - } val CardDTO.isExcluded: Boolean get() { val excludedBatch = excludedBatches.contains(batchId) @@ -52,7 +44,7 @@ object TapWorkarounds { private val tangemNoteBatches = mapOf( "AB01" to Blockchain.Bitcoin, "AB02" to Blockchain.Ethereum, - "AB03" to Blockchain.CardanoShelley, + "AB03" to Blockchain.Cardano, "AB04" to Blockchain.Dogecoin, "AB05" to Blockchain.BSC, "AB06" to Blockchain.XRP, diff --git a/domain/legacy/src/main/java/com/tangem/domain/common/TwinsHelper.kt b/domain/legacy/src/main/java/com/tangem/domain/common/TwinsHelper.kt index f6aa02a2c5..73c31f5a28 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/common/TwinsHelper.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/common/TwinsHelper.kt @@ -4,8 +4,17 @@ import com.tangem.crypto.CryptoUtils import com.tangem.domain.models.scan.CardDTO object TwinsHelper { - private val firstCardSeries = listOf("CB61", "CB64") - private val secondCardSeries = listOf("CB62", "CB65") + + /** + * Card compatibility + * cb61 <-> cb62 + * cb64 <-> cb65 + * + */ + private const val FIRST_CARD_FIRST_SERIES = "CB61" + private const val SECOND_CARD_FIRST_SERIES = "CB62" + private const val FIRST_CARD_SECOND_SERIES = "CB64" + private const val SECOND_CARD_SECOND_SERIES = "CB65" @Suppress("MagicNumber") fun verifyTwinPublicKey(issuerData: ByteArray, cardWalletPublicKey: ByteArray?): Boolean { @@ -16,10 +25,15 @@ object TwinsHelper { return CryptoUtils.verify(cardWalletPublicKey, publicKey, signedKey) } - fun getTwinCardNumber(cardId: String): TwinCardNumber? = when { - firstCardSeries.any(cardId::startsWith) -> TwinCardNumber.First - secondCardSeries.any(cardId::startsWith) -> TwinCardNumber.Second - else -> null + fun getTwinCardNumber(cardId: String): TwinCardNumber? { + val isFirstCard = cardId.startsWith(FIRST_CARD_FIRST_SERIES) || + cardId.startsWith(FIRST_CARD_SECOND_SERIES) + if (isFirstCard) return TwinCardNumber.First + + val isSecondCard = cardId.startsWith(SECOND_CARD_FIRST_SERIES) || + cardId.startsWith(SECOND_CARD_SECOND_SERIES) + if (isSecondCard) return TwinCardNumber.Second + return null } @Suppress("MagicNumber") @@ -30,6 +44,25 @@ object TwinsHelper { val twinCardNumber = getTwinCardNumber(cardId)?.number ?: 1 return "$twinCardId #$twinCardNumber" } + + /** + * Twins compatibility + * cb61 <-> cb62 + * cb64 <-> cb65 + */ + fun isTwinsCompatible(firstCardId: String, secondCardId: String): Boolean { + if (firstCardId.startsWith(FIRST_CARD_FIRST_SERIES) && + secondCardId.startsWith(SECOND_CARD_FIRST_SERIES) + ) { + return true + } + if (firstCardId.startsWith(FIRST_CARD_SECOND_SERIES) && + secondCardId.startsWith(SECOND_CARD_SECOND_SERIES) + ) { + return true + } + return false + } } enum class TwinCardNumber(val number: Int) { diff --git a/domain/legacy/src/main/java/com/tangem/domain/common/configs/CardConfig.kt b/domain/legacy/src/main/java/com/tangem/domain/common/configs/CardConfig.kt new file mode 100644 index 0000000000..cb34af8d75 --- /dev/null +++ b/domain/legacy/src/main/java/com/tangem/domain/common/configs/CardConfig.kt @@ -0,0 +1,28 @@ +package com.tangem.domain.common.configs + +import com.tangem.blockchain.common.Blockchain +import com.tangem.common.card.EllipticCurve +import com.tangem.common.card.FirmwareVersion +import com.tangem.domain.models.scan.CardDTO + +sealed interface CardConfig { + + val mandatoryCurves: List + + fun primaryCurve(blockchain: Blockchain): EllipticCurve? + + companion object { + + fun createConfig(cardDTO: CardDTO): CardConfig { + if (cardDTO.firmwareVersion >= FirmwareVersion.Ed25519Slip0010Available) { + return Wallet2CardConfig + } + if (cardDTO.settings.isBackupAllowed && cardDTO.settings.isHDWalletAllowed && + cardDTO.firmwareVersion >= FirmwareVersion.MultiWalletAvailable + ) { + return GenericCardConfig + } + return GenericCardConfig + } + } +} \ No newline at end of file diff --git a/domain/legacy/src/main/java/com/tangem/domain/common/configs/GenericCardConfig.kt b/domain/legacy/src/main/java/com/tangem/domain/common/configs/GenericCardConfig.kt new file mode 100644 index 0000000000..3a48e2d8ab --- /dev/null +++ b/domain/legacy/src/main/java/com/tangem/domain/common/configs/GenericCardConfig.kt @@ -0,0 +1,33 @@ +package com.tangem.domain.common.configs + +import com.tangem.blockchain.common.Blockchain +import com.tangem.common.card.EllipticCurve +import timber.log.Timber + +object GenericCardConfig : CardConfig { + override val mandatoryCurves: List + get() = listOf( + EllipticCurve.Secp256k1, + EllipticCurve.Ed25519, + EllipticCurve.Bip0340, + EllipticCurve.Bls12381G2Aug, + ) + + /** + * Old logic to determine primary curve for blockchain in TangemWallet + */ + override fun primaryCurve(blockchain: Blockchain): EllipticCurve? { + return when { + blockchain.getSupportedCurves().contains(EllipticCurve.Secp256k1) -> { + EllipticCurve.Secp256k1 + } + blockchain.getSupportedCurves().contains(EllipticCurve.Ed25519) -> { + EllipticCurve.Ed25519 + } + else -> { + Timber.e("Unsupported blockchain, curve not found") + null + } + } + } +} \ No newline at end of file diff --git a/domain/legacy/src/main/java/com/tangem/domain/common/configs/Wallet2CardConfig.kt b/domain/legacy/src/main/java/com/tangem/domain/common/configs/Wallet2CardConfig.kt new file mode 100644 index 0000000000..58ecaef77c --- /dev/null +++ b/domain/legacy/src/main/java/com/tangem/domain/common/configs/Wallet2CardConfig.kt @@ -0,0 +1,37 @@ +package com.tangem.domain.common.configs + +import com.tangem.blockchain.common.Blockchain +import com.tangem.common.card.EllipticCurve +import timber.log.Timber + +object Wallet2CardConfig : CardConfig { + override val mandatoryCurves: List + get() = listOf( + EllipticCurve.Secp256k1, + EllipticCurve.Ed25519, + EllipticCurve.Bip0340, + EllipticCurve.Bls12381G2Aug, + EllipticCurve.Ed25519Slip0010, + ) + + /** + * Logic to determine primary curve for blockchain in TangemWallet 2.0 + */ + override fun primaryCurve(blockchain: Blockchain): EllipticCurve? { + return when { + blockchain.getSupportedCurves().contains(EllipticCurve.Secp256k1) -> { + EllipticCurve.Secp256k1 + } + blockchain.getSupportedCurves().contains(EllipticCurve.Ed25519Slip0010) -> { + EllipticCurve.Ed25519Slip0010 + } + blockchain.getSupportedCurves().contains(EllipticCurve.Bls12381G2Aug) -> { + EllipticCurve.Bls12381G2Aug + } + else -> { + Timber.e("Unsupported blockchain, curve not found") + null + } + } + } +} \ No newline at end of file diff --git a/domain/legacy/src/main/java/com/tangem/domain/common/extensions/Blockchain.kt b/domain/legacy/src/main/java/com/tangem/domain/common/extensions/Blockchain.kt index 98f7c584ec..9085357e7e 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/common/extensions/Blockchain.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/common/extensions/Blockchain.kt @@ -2,7 +2,6 @@ package com.tangem.domain.common.extensions import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.Token -import com.tangem.common.card.EllipticCurve import java.math.BigDecimal @Suppress("ComplexMethod") @@ -30,7 +29,7 @@ fun Blockchain.Companion.fromNetworkId(networkId: String): Blockchain? { "bitcoin/test" -> Blockchain.BitcoinTestnet "bitcoin-cash" -> Blockchain.BitcoinCash "bitcoin-cash/test" -> Blockchain.BitcoinCashTestnet - "cardano" -> Blockchain.CardanoShelley + "cardano" -> Blockchain.Cardano "dogecoin" -> Blockchain.Dogecoin "ducatus" -> Blockchain.Ducatus "litecoin" -> Blockchain.Litecoin @@ -69,6 +68,8 @@ fun Blockchain.Companion.fromNetworkId(networkId: String): Blockchain? { "aleph-zero/test" -> Blockchain.AlephZeroTestnet "octaspace" -> Blockchain.OctaSpace "octaspace/test" -> Blockchain.OctaSpaceTestnet + "chia" -> Blockchain.Chia + "chia/test" -> Blockchain.ChiaTestnet else -> null } } @@ -90,7 +91,6 @@ fun Blockchain.toNetworkId(): String { Blockchain.BitcoinCash -> "bitcoin-cash" Blockchain.BitcoinCashTestnet -> "bitcoin-cash/test" Blockchain.Cardano -> "cardano" - Blockchain.CardanoShelley -> "cardano" Blockchain.Dogecoin -> "dogecoin" Blockchain.Ducatus -> "ducatus" Blockchain.Ethereum -> "ethereum" @@ -139,6 +139,8 @@ fun Blockchain.toNetworkId(): String { Blockchain.AlephZeroTestnet -> "aleph-zero/test" Blockchain.OctaSpace -> "octaspace" Blockchain.OctaSpaceTestnet -> "octaspace/test" + Blockchain.Chia -> "chia" + Blockchain.ChiaTestnet -> "chia/test" } } @@ -151,7 +153,7 @@ fun Blockchain.toCoinId(): String { Blockchain.Ethereum, Blockchain.EthereumTestnet -> "ethereum" Blockchain.EthereumClassic, Blockchain.EthereumClassicTestnet -> "ethereum-classic" Blockchain.Stellar, Blockchain.StellarTestnet -> "stellar" - Blockchain.Cardano, Blockchain.CardanoShelley -> "cardano" + Blockchain.Cardano -> "cardano" Blockchain.Polygon, Blockchain.PolygonTestnet -> "matic-network" Blockchain.Arbitrum, Blockchain.ArbitrumTestnet -> "ethereum" Blockchain.Avalanche, Blockchain.AvalancheTestnet -> "avalanche-2" @@ -183,6 +185,8 @@ fun Blockchain.toCoinId(): String { Blockchain.Telos, Blockchain.TelosTestnet -> "telos" Blockchain.AlephZero, Blockchain.AlephZeroTestnet -> "aleph-zero" Blockchain.OctaSpace, Blockchain.OctaSpaceTestnet -> "octaspace" + Blockchain.Chia -> "chia" + Blockchain.ChiaTestnet -> "chia/test" } } @@ -202,20 +206,6 @@ fun Blockchain.minimalAmount(): BigDecimal { return 1.toBigDecimal().movePointLeft(decimals()) } -fun Blockchain.getPrimaryCurve(): EllipticCurve? { - return when { - getSupportedCurves().contains(EllipticCurve.Secp256k1) -> { - EllipticCurve.Secp256k1 - } - getSupportedCurves().contains(EllipticCurve.Ed25519) -> { - EllipticCurve.Ed25519 - } - else -> { - null - } - } -} - private const val NODL = "NODL" private const val NODL_AMOUNT_TO_CREATE_ACCOUNT = 1.5 diff --git a/domain/legacy/src/main/java/com/tangem/domain/common/extensions/CardSdk.kt b/domain/legacy/src/main/java/com/tangem/domain/common/extensions/CardSdk.kt index 77e154859f..1efeec3cf8 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/common/extensions/CardSdk.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/common/extensions/CardSdk.kt @@ -3,6 +3,7 @@ package com.tangem.domain.common.extensions import com.tangem.blockchain.common.Blockchain import com.tangem.common.card.EllipticCurve import com.tangem.common.card.FirmwareVersion +import com.tangem.domain.common.CardTypesResolver import com.tangem.domain.common.TapWorkarounds.isTestCard import com.tangem.domain.models.scan.CardDTO @@ -12,20 +13,28 @@ import com.tangem.domain.models.scan.CardDTO val FirmwareVersion.Companion.SolanaTokensAvailable get() = FirmwareVersion(4, 52) -fun CardDTO.supportedBlockchains(): List { +fun CardDTO.supportedBlockchains(cardTypesResolver: CardTypesResolver): List { val supportedBlockchains = if (firmwareVersion < FirmwareVersion.MultiWalletAvailable) { Blockchain.fromCurve(EllipticCurve.Secp256k1) } else { wallets.flatMap { Blockchain.fromCurve(it.curve) }.distinct() + }.toMutableList() + // disabled Cardano for wallet 2 for now, should be enabled after key processed + // ([REDACTED_JIRA]) + if (cardTypesResolver.isWallet2()) { + supportedBlockchains.apply { + remove(Blockchain.Cardano) + } } - return supportedBlockchains .filter { isTestCard == it.isTestnet() } .filter { it.isSupportedInApp() } } -fun CardDTO.supportedTokens(): List { - val tokensSupportedByBlockchain = supportedBlockchains().filter { it.canHandleTokens() }.toMutableList() +fun CardDTO.supportedTokens(cardTypesResolver: CardTypesResolver): List { + val tokensSupportedByBlockchain = supportedBlockchains(cardTypesResolver) + .filter { it.canHandleTokens() } + .toMutableList() val tokensSupportedByCard = when { firmwareVersion >= FirmwareVersion.SolanaTokensAvailable -> tokensSupportedByBlockchain else -> { @@ -39,6 +48,6 @@ fun CardDTO.supportedTokens(): List { return tokensSupportedByCard.filter { isTestCard == it.isTestnet() } } -fun CardDTO.canHandleToken(blockchain: Blockchain): Boolean { - return this.supportedTokens().contains(blockchain) +fun CardDTO.canHandleToken(blockchain: Blockchain, cardTypesResolver: CardTypesResolver): Boolean { + return this.supportedTokens(cardTypesResolver).contains(blockchain) } \ No newline at end of file diff --git a/domain/legacy/src/main/java/com/tangem/domain/common/extensions/WalletManagerFactory.kt b/domain/legacy/src/main/java/com/tangem/domain/common/extensions/WalletManagerFactory.kt index 86ce14a4f4..f65ca67b3e 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/common/extensions/WalletManagerFactory.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/common/extensions/WalletManagerFactory.kt @@ -1,11 +1,14 @@ package com.tangem.domain.common.extensions import com.tangem.blockchain.common.* +import com.tangem.blockchain.common.derivation.DerivationStyle import com.tangem.common.card.EllipticCurve import com.tangem.common.extensions.hexToBytes import com.tangem.common.extensions.toMapKey import com.tangem.domain.common.TapWorkarounds.isTestCard import com.tangem.domain.common.TapWorkarounds.useOldStyleDerivation +import com.tangem.domain.common.configs.CardConfig +import com.tangem.domain.common.configs.Wallet2CardConfig import com.tangem.domain.common.util.cardTypesResolver import com.tangem.domain.models.scan.CardDTO import com.tangem.domain.models.scan.ScanResponse @@ -16,11 +19,16 @@ fun WalletManagerFactory.makeWalletManagerForApp( derivationParams: DerivationParams?, ): WalletManager? { val card = scanResponse.card + val cardConfig = CardConfig.createConfig(card) if (card.isTestCard && blockchain.getTestnetVersion() == null) return null val supportedCurves = blockchain.getSupportedCurves() val wallets = card.wallets.filter { wallet -> supportedCurves.contains(wallet.curve) } - val wallet = selectWallet(wallets) ?: return null + val wallet = selectWallet( + wallets = wallets, + cardConfig = cardConfig, + blockchain = blockchain, + ) ?: return null val environmentBlockchain = if (card.isTestCard) blockchain.getTestnetVersion()!! else blockchain @@ -85,10 +93,19 @@ fun WalletManagerFactory.makePrimaryWalletManager(scanResponse: ScanResponse): W ) } -private fun selectWallet(wallets: List): CardDTO.Wallet? { - return when (wallets.size) { - 0 -> null - 1 -> wallets[0] - else -> wallets.firstOrNull { it.curve == EllipticCurve.Secp256k1 } ?: wallets[0] +private fun selectWallet( + wallets: List, + cardConfig: CardConfig, + blockchain: Blockchain, +): CardDTO.Wallet? { + return if (cardConfig is Wallet2CardConfig) { + val primaryCurve = cardConfig.primaryCurve(blockchain) + wallets.firstOrNull { it.curve == primaryCurve } + } else { + when (wallets.size) { + 0 -> null + 1 -> wallets[0] + else -> wallets.firstOrNull { it.curve == EllipticCurve.Secp256k1 } ?: wallets[0] + } } } \ No newline at end of file diff --git a/domain/legacy/src/main/java/com/tangem/domain/common/util/ScanResponseExtensions.kt b/domain/legacy/src/main/java/com/tangem/domain/common/util/ScanResponseExtensions.kt index 8438a87f14..61ec0cc72e 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/common/util/ScanResponseExtensions.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/common/util/ScanResponseExtensions.kt @@ -5,9 +5,13 @@ import com.tangem.common.card.EllipticCurve import com.tangem.common.extensions.toMapKey import com.tangem.crypto.hdWallet.DerivationPath import com.tangem.domain.common.CardTypesResolver +import com.tangem.domain.common.DerivationStyleProvider import com.tangem.domain.common.TangemCardTypesResolver +import com.tangem.domain.common.TangemDerivationStyleProvider import com.tangem.domain.common.TapWorkarounds.isTangemTwins import com.tangem.domain.common.TapWorkarounds.isTestCard +import com.tangem.domain.common.configs.CardConfig +import com.tangem.domain.common.configs.Wallet2CardConfig import com.tangem.domain.models.scan.ScanResponse val ScanResponse.cardTypesResolver: CardTypesResolver @@ -17,6 +21,12 @@ val ScanResponse.cardTypesResolver: CardTypesResolver walletData = walletData, ) +val ScanResponse.derivationStyleProvider: DerivationStyleProvider + get() = TangemDerivationStyleProvider( + cardTypesResolver, + card, + ) + fun ScanResponse.twinsIsTwinned(): Boolean = card.isTangemTwins && walletData != null && secondTwinPublicKey != null fun ScanResponse.supportsHdWallet(): Boolean = card.settings.isHDWalletAllowed fun ScanResponse.supportsBackup(): Boolean = card.settings.isBackupAllowed @@ -27,14 +37,22 @@ fun ScanResponse.hasDerivation(blockchain: Blockchain, rawDerivationPath: String private fun ScanResponse.hasDerivation(blockchain: Blockchain, derivationPath: DerivationPath): Boolean { val isTestnet = card.isTestCard || blockchain.isTestnet() - return when { - Blockchain.secp256k1Blockchains(isTestnet).contains(blockchain) -> { - hasDerivation(EllipticCurve.Secp256k1, derivationPath) + val config = CardConfig.createConfig(card) + return if (config is Wallet2CardConfig) { + // new logic for wallet2 + val primaryCurve = config.primaryCurve(blockchain) + primaryCurve?.let { hasDerivation(it, derivationPath) } ?: false + } else { + // leave logic for legacy wallets + when { + Blockchain.secp256k1Blockchains(isTestnet).contains(blockchain) -> { + hasDerivation(EllipticCurve.Secp256k1, derivationPath) + } + Blockchain.ed25519Blockchains(isTestnet).contains(blockchain) -> { + hasDerivation(EllipticCurve.Ed25519, derivationPath) + } + else -> false } - Blockchain.ed25519OnlyBlockchains(isTestnet).contains(blockchain) -> { - hasDerivation(EllipticCurve.Ed25519, derivationPath) - } - else -> false } } 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 6757215dda..78ff46be86 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 @@ -1,8 +1,8 @@ package com.tangem.domain.features.addCustomToken import com.tangem.blockchain.common.Blockchain -import com.tangem.blockchain.common.DerivationStyle 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 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 index 30085691d3..aa12f86b00 100644 --- 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 @@ -5,49 +5,19 @@ 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.PotentialScamToken -import com.tangem.domain.AddCustomTokenError.Warning.TokenAlreadyAdded -import com.tangem.domain.AddCustomTokenError.Warning.UnsupportedSolanaToken +import com.tangem.domain.AddCustomTokenError.Warning.* import com.tangem.domain.DomainDialog import com.tangem.domain.DomainWrapped -import com.tangem.domain.common.TapWorkarounds.derivationStyle 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.Field -import com.tangem.domain.common.form.Form -import com.tangem.domain.common.form.TokenContractAddressValidator -import com.tangem.domain.common.form.TokenDecimalsValidator -import com.tangem.domain.common.form.TokenNameValidator -import com.tangem.domain.common.form.TokenNetworkValidator -import com.tangem.domain.common.form.TokenSymbolValidator -import com.tangem.domain.features.addCustomToken.AddCustomTokenService -import com.tangem.domain.features.addCustomToken.CustomTokenFieldId -import com.tangem.domain.features.addCustomToken.CustomTokenFieldId.ContractAddress -import com.tangem.domain.features.addCustomToken.CustomTokenFieldId.Decimals -import com.tangem.domain.features.addCustomToken.CustomTokenFieldId.DerivationPath -import com.tangem.domain.features.addCustomToken.CustomTokenFieldId.Name -import com.tangem.domain.features.addCustomToken.CustomTokenFieldId.Network -import com.tangem.domain.features.addCustomToken.CustomTokenFieldId.Symbol -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.FieldError -import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenAction.Init -import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenAction.OnAddCustomTokenClicked -import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenAction.OnCreate -import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenAction.OnDestroy -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.AddCustomTokenAction.Screen -import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenAction.SetFoundTokenInfo -import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenAction.UpdateForm -import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenAction.Warning +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 @@ -466,7 +436,13 @@ internal class AddCustomTokenHub : BaseStoreHub("AddCustomT private fun tokenIsSupported(blockchain: Blockchain): Boolean = when (blockchain) { Blockchain.Unknown -> true - else -> globalState.scanResponse?.card?.canHandleToken(blockchain) ?: false + else -> { + val scanResponse = globalState.scanResponse + scanResponse?.card?.canHandleToken( + blockchain = blockchain, + cardTypesResolver = scanResponse.cardTypesResolver, + ) ?: false + } } @Throws @@ -578,8 +554,9 @@ private class AddCustomTokenReducer( state.copy(onTokenAddCallback = action.callback) } is OnCreate -> { - val card = requireNotNull(globalState.scanResponse?.card) - val supportedTokenNetworkIds = card.supportedBlockchains() + val scanResponse = requireNotNull(globalState.scanResponse) + val card = globalState.scanResponse.card + val supportedTokenNetworkIds = card.supportedBlockchains(scanResponse.cardTypesResolver) .filter(Blockchain::canHandleTokens) .map(Blockchain::toNetworkId) @@ -590,15 +567,22 @@ private class AddCustomTokenReducer( ) state.copy( - cardDerivationStyle = card.derivationStyle, - form = Form(AddCustomTokenState.createFormFields(card, CustomTokenType.Blockchain)), + 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 card = requireNotNull(globalState.scanResponse?.card) - state.reset(card) + val scanResponse = requireNotNull(globalState.scanResponse) + val card = scanResponse.card + state.reset(scanResponse.cardTypesResolver, card) } is UpdateForm -> { updateFormState(action.state) 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 index 3377646d25..6d1b3fd2b0 100644 --- 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 @@ -1,40 +1,19 @@ package com.tangem.domain.features.addCustomToken.redux import com.tangem.blockchain.common.Blockchain -import com.tangem.blockchain.common.DerivationStyle +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.CustomTokenValidator -import com.tangem.domain.common.form.DataField -import com.tangem.domain.common.form.FieldDataConverter -import com.tangem.domain.common.form.FieldId -import com.tangem.domain.common.form.FieldToJsonConverter -import com.tangem.domain.common.form.Form -import com.tangem.domain.common.form.StringIsEmptyValidator -import com.tangem.domain.common.form.StringIsNotEmptyValidator -import com.tangem.domain.common.form.TokenContractAddressValidator -import com.tangem.domain.common.form.TokenDecimalsValidator -import com.tangem.domain.common.form.TokenNameValidator -import com.tangem.domain.common.form.TokenNetworkValidator -import com.tangem.domain.common.form.TokenSymbolValidator -import com.tangem.domain.features.addCustomToken.AddCustomTokenService -import com.tangem.domain.features.addCustomToken.CustomCurrency -import com.tangem.domain.features.addCustomToken.CustomTokenFieldId -import com.tangem.domain.features.addCustomToken.CustomTokenFieldId.ContractAddress -import com.tangem.domain.features.addCustomToken.CustomTokenFieldId.Decimals -import com.tangem.domain.features.addCustomToken.CustomTokenFieldId.DerivationPath -import com.tangem.domain.features.addCustomToken.CustomTokenFieldId.Name -import com.tangem.domain.features.addCustomToken.CustomTokenFieldId.Network -import com.tangem.domain.features.addCustomToken.CustomTokenFieldId.Symbol -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.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 @@ -134,12 +113,12 @@ data class AddCustomTokenState( null } - fun reset(card: CardDTO): AddCustomTokenState { + fun reset(cardTypesResolver: CardTypesResolver, card: CardDTO): AddCustomTokenState { return this.copy( appSavedCurrencies = null, onTokenAddCallback = null, cardDerivationStyle = null, - form = Form(createFormFields(card, CustomTokenType.Blockchain)), + form = Form(createFormFields(cardTypesResolver, card, CustomTokenType.Blockchain)), formErrors = emptyMap(), foundToken = null, warnings = emptySet(), @@ -186,10 +165,14 @@ data class AddCustomTokenState( }.derivationPath(derivationStyleToUse) } - internal fun createFormFields(card: CardDTO, type: CustomTokenType): List> { + internal fun createFormFields( + cardTypesResolver: CardTypesResolver, + card: CardDTO, + type: CustomTokenType, + ): List> { return listOf( TokenField(ContractAddress), - TokenBlockchainField(Network, getNetworksList(card, type)), + TokenBlockchainField(Network, getNetworksList(cardTypesResolver, card, type)), TokenField(Name), TokenField(Symbol), TokenField(Decimals), @@ -201,7 +184,11 @@ data class AddCustomTokenState( * Serves to determine the networks (blockchains & tokens) that can be selected by Form.Networks. * Blockchain.Unknown - is the default selection */ - private fun getNetworksList(card: CardDTO, type: CustomTokenType): List { + private fun getNetworksList( + cardTypesResolver: CardTypesResolver, + card: CardDTO, + type: CustomTokenType, + ): List { val evmBlockchains = Blockchain.values() .filter { it.isEvm() } .filter { card.isTestCard == it.isTestnet() } @@ -216,8 +203,8 @@ data class AddCustomTokenState( ) val supportedByCard = when (type) { - CustomTokenType.Blockchain -> card.supportedBlockchains() - CustomTokenType.Token -> card.supportedTokens() + CustomTokenType.Blockchain -> card.supportedBlockchains(cardTypesResolver) + CustomTokenType.Token -> card.supportedTokens(cardTypesResolver) } val typedNetworksList = (evmBlockchains + additionalBlockchains) .filter { supportedByCard.contains(it) } 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 09bc5ac1dd..e119f09792 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 @@ -3,19 +3,21 @@ package com.tangem.domain.walletmanager import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.BlockchainSdkError import com.tangem.blockchain.common.WalletManager +import com.tangem.blockchain.extensions.Result 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.TapWorkarounds.derivationStyle +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.model.CryptoCurrency +import com.tangem.domain.tokens.models.CryptoCurrency import com.tangem.domain.tokens.models.Network +import com.tangem.domain.txhistory.models.PaginationWrapper +import com.tangem.domain.txhistory.models.TxHistoryItem +import com.tangem.domain.txhistory.models.TxHistoryState import com.tangem.domain.walletmanager.model.UpdateWalletManagerResult -import com.tangem.domain.walletmanager.utils.SdkTokenConverter -import com.tangem.domain.walletmanager.utils.UpdateWalletManagerResultFactory -import com.tangem.domain.walletmanager.utils.WalletManagerFactory +import com.tangem.domain.walletmanager.utils.* import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWalletId import timber.log.Timber @@ -32,44 +34,94 @@ class DefaultWalletManagersFacade( private val resultFactory by lazy { UpdateWalletManagerResultFactory() } private val walletManagerFactory by lazy { WalletManagerFactory(configManager) } private val sdkTokenConverter by lazy { SdkTokenConverter() } + private val txHistoryStateConverter by lazy { SdkTransactionHistoryStateConverter() } + private val txHistoryItemConverter by lazy { SdkTransactionHistoryItemConverter() } override suspend fun update( userWalletId: UserWalletId, networkId: Network.ID, extraTokens: Set, ): UpdateWalletManagerResult { - val userWallet = requireNotNull(userWalletsStore.getSyncOrNull(userWalletId)) { - "Unable to find a user wallet with provided ID: $userWalletId" - } + val userWallet = getUserWallet(userWalletId) val blockchain = Blockchain.fromId(networkId.value) return getAndUpdateWalletManager(userWallet, blockchain, extraTokens) } override suspend fun getExploreUrl(userWalletId: UserWalletId, networkId: Network.ID): String { - val userWallet = requireNotNull(userWalletsStore.getSyncOrNull(userWalletId)) { - "Unable to find a user wallet with provided ID: $userWalletId" - } + val userWallet = getUserWallet(userWalletId) val blockchain = Blockchain.fromId(networkId.value) return getOrCreateWalletManager( userWallet = userWallet, blockchain = blockchain, - derivationPath = blockchain.derivationPath(userWallet.scanResponse.card.derivationStyle), + derivationPath = blockchain + .derivationPath(userWallet.scanResponse.derivationStyleProvider.getDerivationStyle()), ) ?.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)) { + "Unable to get a wallet manager for blockchain: $blockchain" + } + return walletManager + .getTransactionHistoryState(walletManager.wallet.address) + .let(txHistoryStateConverter::convert) + } + + override suspend fun getTxHistoryItems( + userWalletId: UserWalletId, + networkId: Network.ID, + rawDerivationPath: String?, + 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)) { + "Unable to get a wallet manager for blockchain: $blockchain" + } + val itemsResult = walletManager.getTransactionsHistory( + address = walletManager.wallet.address, + page = page, + pageSize = pageSize, + ) + + return when (itemsResult) { + is Result.Success -> PaginationWrapper( + page = itemsResult.data.page, + totalPages = itemsResult.data.totalPages, + itemsOnPage = itemsResult.data.itemsOnPage, + items = txHistoryItemConverter.convertList(itemsResult.data.items), + ) + is Result.Failure -> error(itemsResult.error.message ?: itemsResult.error.customMessage) + } + } + + private suspend fun getUserWallet(userWalletId: UserWalletId) = + requireNotNull(userWalletsStore.getSyncOrNull(userWalletId)) { + "Unable to find a user wallet with provided ID: $userWalletId" + } + private suspend fun getAndUpdateWalletManager( userWallet: UserWallet, blockchain: Blockchain, extraTokens: Set, ): UpdateWalletManagerResult { val scanResponse = userWallet.scanResponse - val derivationPath = blockchain.derivationPath(scanResponse.card.derivationStyle) + val derivationPath = blockchain.derivationPath(scanResponse.derivationStyleProvider.getDerivationStyle()) if (derivationPath != null && !scanResponse.hasDerivation(blockchain, derivationPath.rawPath)) { Timber.e("Derivation missed for: $blockchain") @@ -86,7 +138,7 @@ class DefaultWalletManagersFacade( return try { if (demoConfig.isDemoCardId(userWallet.scanResponse.card.cardId)) { - updateDemoWalletManager(walletManager, extraTokens) + updateDemoWalletManager(walletManager) } else { updateWalletManager(walletManager) } @@ -95,14 +147,11 @@ class DefaultWalletManagersFacade( } } - private fun updateDemoWalletManager( - walletManager: WalletManager, - tokens: Set, - ): UpdateWalletManagerResult { + private fun updateDemoWalletManager(walletManager: WalletManager): UpdateWalletManagerResult { val amount = demoConfig.getBalance(walletManager.wallet.blockchain) walletManager.wallet.setAmount(amount) - return resultFactory.getDemoResult(amount, tokens) + return resultFactory.getDemoResult(walletManager, amount) } private suspend fun updateWalletManager(walletManager: WalletManager): UpdateWalletManagerResult { @@ -149,7 +198,7 @@ class DefaultWalletManagersFacade( if (tokens.isEmpty()) return val tokensToAdd = sdkTokenConverter - .convertList(tokens.toList()) + .convertList(tokens) .filter { it !in walletManager.cardTokens } walletManager.addTokens(tokensToAdd) 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 a2ed074675..c923ac2c76 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 @@ -1,7 +1,10 @@ package com.tangem.domain.walletmanager -import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.tokens.models.CryptoCurrency import com.tangem.domain.tokens.models.Network +import com.tangem.domain.txhistory.models.PaginationWrapper +import com.tangem.domain.txhistory.models.TxHistoryItem +import com.tangem.domain.txhistory.models.TxHistoryState import com.tangem.domain.walletmanager.model.UpdateWalletManagerResult import com.tangem.domain.wallets.models.UserWalletId @@ -26,4 +29,35 @@ interface WalletManagersFacade { ): UpdateWalletManagerResult suspend fun getExploreUrl(userWalletId: UserWalletId, networkId: Network.ID): 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. + + */ + suspend fun getTxHistoryState( + userWalletId: UserWalletId, + networkId: Network.ID, + rawDerivationPath: String?, + ): 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 page Pagination page. + * @param pageSize Pagination size. + */ + suspend fun getTxHistoryItems( + userWalletId: UserWalletId, + networkId: Network.ID, + rawDerivationPath: String?, + page: Int, + pageSize: Int, + ): PaginationWrapper } \ No newline at end of file diff --git a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/model/CryptoCurrencyAmount.kt b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/model/CryptoCurrencyAmount.kt index 192be587e5..ae72f37ec4 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/model/CryptoCurrencyAmount.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/model/CryptoCurrencyAmount.kt @@ -9,6 +9,7 @@ sealed class CryptoCurrencyAmount { data class Coin(override val value: BigDecimal) : CryptoCurrencyAmount() data class Token( + val id: String?, val tokenContractAddress: String, override val value: BigDecimal, ) : CryptoCurrencyAmount() diff --git a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/utils/SdkTokenConverter.kt b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/utils/SdkTokenConverter.kt index 8fe4bdc174..a6ceecff07 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/utils/SdkTokenConverter.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/utils/SdkTokenConverter.kt @@ -1,6 +1,6 @@ package com.tangem.domain.walletmanager.utils -import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.tokens.models.CryptoCurrency import com.tangem.utils.converter.Converter import com.tangem.blockchain.common.Token as SdkToken @@ -8,7 +8,7 @@ internal class SdkTokenConverter : Converter { override fun convert(value: CryptoCurrency.Token): SdkToken { return SdkToken( - id = value.id.value.takeUnless { value.isCustom }, + id = value.id.rawCurrencyId, name = value.name, symbol = value.symbol, contractAddress = value.contractAddress, diff --git a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/utils/SdkTransactionHistoryItemConverter.kt b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/utils/SdkTransactionHistoryItemConverter.kt new file mode 100644 index 0000000000..8df6e63c54 --- /dev/null +++ b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/utils/SdkTransactionHistoryItemConverter.kt @@ -0,0 +1,29 @@ +package com.tangem.domain.walletmanager.utils + +import com.tangem.blockchain.common.TransactionStatus +import com.tangem.blockchain.common.txhistory.TransactionHistoryItem +import com.tangem.domain.txhistory.models.TxHistoryItem +import com.tangem.utils.converter.Converter +import com.tangem.blockchain.common.txhistory.TransactionHistoryItem as SdkTransactionHistoryItem + +internal class SdkTransactionHistoryItemConverter : Converter { + + override fun convert(value: SdkTransactionHistoryItem): TxHistoryItem = TxHistoryItem( + txHash = value.txHash, + timestampInMillis = value.timestamp, + direction = when (val direction = value.direction) { + is SdkTransactionHistoryItem.TransactionDirection.Incoming -> + TxHistoryItem.TransactionDirection.Incoming(direction.from) + is SdkTransactionHistoryItem.TransactionDirection.Outgoing -> + TxHistoryItem.TransactionDirection.Outgoing(direction.to) + }, + status = when (value.status) { + TransactionStatus.Confirmed -> TxHistoryItem.TxStatus.Confirmed + TransactionStatus.Unconfirmed -> TxHistoryItem.TxStatus.Unconfirmed + }, + type = when (value.type) { + TransactionHistoryItem.TransactionType.Transfer -> TxHistoryItem.TransactionType.Transfer + }, + amount = requireNotNull(value.amount.value) { "Transaction amount value must not be null" }, + ) +} \ No newline at end of file diff --git a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/utils/SdkTransactionHistoryStateConverter.kt b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/utils/SdkTransactionHistoryStateConverter.kt new file mode 100644 index 0000000000..6750e05410 --- /dev/null +++ b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/utils/SdkTransactionHistoryStateConverter.kt @@ -0,0 +1,16 @@ +package com.tangem.domain.walletmanager.utils + +import com.tangem.blockchain.common.txhistory.TransactionHistoryState +import com.tangem.domain.txhistory.models.TxHistoryState +import com.tangem.utils.converter.Converter +import com.tangem.blockchain.common.txhistory.TransactionHistoryState as SdkTransactionHistoryState + +internal class SdkTransactionHistoryStateConverter : Converter { + + override fun convert(value: TransactionHistoryState): TxHistoryState = when (value) { + is TransactionHistoryState.Success.Empty -> TxHistoryState.Success.Empty + is TransactionHistoryState.Success.HasTransactions -> TxHistoryState.Success.HasTransactions(value.txCount) + is TransactionHistoryState.Failed.FetchError -> TxHistoryState.Failed.FetchError(value.exception) + is TransactionHistoryState.NotImplemented -> TxHistoryState.NotImplemented + } +} \ 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 bdf864f4fe..df5737de85 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 @@ -1,11 +1,7 @@ package com.tangem.domain.walletmanager.utils -import com.tangem.blockchain.common.Amount -import com.tangem.blockchain.common.AmountType -import com.tangem.blockchain.common.TransactionStatus -import com.tangem.blockchain.common.WalletManager +import com.tangem.blockchain.common.* import com.tangem.domain.common.extensions.amountToCreateAccount -import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.walletmanager.model.CryptoCurrencyAmount import com.tangem.domain.walletmanager.model.UpdateWalletManagerResult import timber.log.Timber @@ -26,9 +22,9 @@ internal class UpdateWalletManagerResultFactory { ) } - fun getDemoResult(demoAmount: Amount, tokens: Set): UpdateWalletManagerResult.Verified { + fun getDemoResult(walletManager: WalletManager, demoAmount: Amount): UpdateWalletManagerResult.Verified { return UpdateWalletManagerResult.Verified( - tokensAmounts = getDemoTokensAmounts(demoAmount, tokens), + tokensAmounts = getDemoTokensAmounts(demoAmount, walletManager.cardTokens), hasTransactionsInProgress = false, ) } @@ -53,18 +49,19 @@ internal class UpdateWalletManagerResultFactory { return amounts.mapNotNullTo(mutableAmounts, ::getTokenAmount) } - private fun getDemoTokensAmounts(demoAmount: Amount, tokens: Set): Set { + private fun getDemoTokensAmounts(demoAmount: Amount, tokens: Set): Set { val amountValue = demoAmount.value ?: BigDecimal.ZERO val demoAmounts = hashSetOf(CryptoCurrencyAmount.Coin(amountValue)) return tokens.mapTo(demoAmounts) { token -> - CryptoCurrencyAmount.Token(token.contractAddress, amountValue) + CryptoCurrencyAmount.Token(token.id, token.contractAddress, amountValue) } } private fun getTokenAmount(amount: Amount): CryptoCurrencyAmount? { return when (val type = amount.type) { is AmountType.Token -> CryptoCurrencyAmount.Token( + id = type.token.id, tokenContractAddress = type.token.contractAddress, value = getAmountValue(amount) ?: return null, ) diff --git a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/utils/WalletManagerFactory.kt b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/utils/WalletManagerFactory.kt index f588b47702..770721f3f7 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/utils/WalletManagerFactory.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/utils/WalletManagerFactory.kt @@ -2,6 +2,7 @@ package com.tangem.domain.walletmanager.utils import com.tangem.blockchain.common.* import com.tangem.blockchain.common.WalletManagerFactory +import com.tangem.blockchain.common.derivation.DerivationStyle import com.tangem.crypto.hdWallet.DerivationPath import com.tangem.datasource.config.ConfigManager import com.tangem.domain.common.TapWorkarounds.useOldStyleDerivation diff --git a/domain/legacy/src/test/java/com/tangem/domain/features/BlockchainTests.kt b/domain/legacy/src/test/java/com/tangem/domain/features/BlockchainTests.kt index 292d9e5e06..ff7740c1ba 100644 --- a/domain/legacy/src/test/java/com/tangem/domain/features/BlockchainTests.kt +++ b/domain/legacy/src/test/java/com/tangem/domain/features/BlockchainTests.kt @@ -13,7 +13,6 @@ class BlockchainTests { .toMutableList() .apply { remove(Blockchain.Unknown) - remove(Blockchain.Optimism) } .map { it to Blockchain.fromNetworkId(it.toNetworkId()) } .mapNotNull { if (it.second == null) it.first else null } diff --git a/domain/tokens/build.gradle.kts b/domain/tokens/build.gradle.kts index 4099f104eb..e2b240e5bc 100644 --- a/domain/tokens/build.gradle.kts +++ b/domain/tokens/build.gradle.kts @@ -4,11 +4,17 @@ plugins { } dependencies { + + /** Project - Domain */ implementation(projects.domain.core) implementation(projects.domain.tokens.models) implementation(projects.domain.wallets.models) + implementation(projects.domain.appCurrency.models) + + /** Project - Other */ implementation(projects.core.utils) + /** Tests */ testImplementation(deps.test.junit) testImplementation(deps.test.coroutine) } \ 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 new file mode 100644 index 0000000000..83d1d65eb0 --- /dev/null +++ b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/models/CryptoCurrency.kt @@ -0,0 +1,164 @@ +package com.tangem.domain.tokens.models + +import java.io.Serializable + +/** + * Represents a generic cryptocurrency. + * + * @property id Unique identifier for the cryptocurrency. + * @property networkId Identifier for the network to which the cryptocurrency belongs. + * @property name Human-readable name of the cryptocurrency. + * @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. + */ +sealed class CryptoCurrency : Serializable { + + abstract val id: ID + abstract val networkId: Network.ID + abstract val name: String + abstract val symbol: String + abstract val decimals: Int + abstract val iconUrl: String? + abstract val derivationPath: String? + + /** + * Represents a native coin in the blockchain network. + */ + data class Coin( + override val id: ID, + override val networkId: Network.ID, + override val name: String, + override val symbol: String, + override val decimals: Int, + override val iconUrl: String?, + override val derivationPath: String?, + ) : CryptoCurrency() { + + init { + checkProperties() + } + } + + /** + * 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, + override val networkId: Network.ID, + override val name: String, + override val symbol: String, + override val decimals: Int, + override val iconUrl: String?, + override val derivationPath: String?, + val contractAddress: String, + val isCustom: Boolean, + val blockchainName: String, // TODO: Move this field to proper entity + val standardType: StandardType, // TODO: Move this field to proper entity + ) : CryptoCurrency() { + + init { + checkProperties() + require(contractAddress.isNotBlank()) { "Token contract address must not be blank" } + } + } + + /** + * Represents a unique identifier for a cryptocurrency, constructed from various components. + * + * The ID is designed to ensure that different cryptocurrencies, whether they are standard tokens, custom tokens or + * standard coins, can be distinctly identified within a system. + * + * @property value Constructed unique identifier value, made up of prefix, network ID, and suffix. + * @property rawCurrencyId Represents not unique currency ID from the blockchain network. `null` if + * its ID of the custom token. + */ + data class ID( + private val prefix: Prefix, + private val networkId: Network.ID, + private val suffix: Suffix, + ) { + + val value: String = buildString { + append(prefix.value) + append(networkId.value) + append(DELIMITER) + append(suffix.value) + } + + val rawCurrencyId: String? = (suffix as? Suffix.RawID)?.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_"), + + /** Prefix for standard tokens. */ + TOKEN_PREFIX(value = "token_"), + + /** Prefix for custom tokens. */ + CUSTOM_TOKEN_PREFIX(value = "custom_"), + } + + /** + * Represents the suffix part of the cryptocurrency ID. + * + * The suffix can either be a raw ID or a contract address. + */ + sealed class Suffix { + + /** The value of the suffix, which could be either a raw ID or a contract address. */ + abstract val value: String + + /** Represents a raw ID suffix. */ + data class RawID(val rawId: String) : Suffix() { + override val value: String = rawId + } + + /** Represents a contract address suffix. */ + data class ContractAddress(val contractAddress: String) : Suffix() { + override val value: String = contractAddress + } + } + + private companion object { + const val DELIMITER = '#' + } + } + + sealed class StandardType { + abstract val name: String + + object ERC20 : StandardType() { + override val name: String = "ERC20" + } + object TRC20 : StandardType() { + override val name: String = "TRC20" + } + object BEP20 : StandardType() { + override val name: String = "BEP20" + } + object BEP2 : StandardType() { + override val name: String = "BEP2" + } + class Unspecified(val tokenName: String) : StandardType() { + override val name: String = tokenName + } + } + + protected fun checkProperties() { + require(name.isNotBlank()) { "Crypto currency name must not be blank" } + 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/src/main/kotlin/com/tangem/domain/tokens/model/Quote.kt b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/models/Quote.kt similarity index 67% rename from domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/Quote.kt rename to domain/tokens/models/src/main/java/com/tangem/domain/tokens/models/Quote.kt index 7247a75ed2..835e84fe4f 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/Quote.kt +++ b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/models/Quote.kt @@ -1,16 +1,16 @@ -package com.tangem.domain.tokens.model +package com.tangem.domain.tokens.models import java.math.BigDecimal /** * Represents a financial quote for a specific cryptocurrency, including its fiat exchange rate and price change. * - * @property currencyId The unique identifier of the cryptocurrency for which the quote is provided. + * @property rawCurrencyId The unique identifier of the token for which the quote is provided. * @property fiatRate The current fiat exchange rate for the cryptocurrency. * @property priceChange The price change for the cryptocurrency. */ data class Quote( - val currencyId: CryptoCurrency.ID, + val rawCurrencyId: String, val fiatRate: BigDecimal, val priceChange: BigDecimal, ) \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/ApplyTokenListSortingUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/ApplyTokenListSortingUseCase.kt index 89d9776a18..e57b268e68 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/ApplyTokenListSortingUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/ApplyTokenListSortingUseCase.kt @@ -5,24 +5,24 @@ import arrow.core.raise.Raise import arrow.core.raise.catch import arrow.core.raise.either import arrow.core.raise.ensureNotNull +import arrow.core.toNonEmptyListOrNull import arrow.core.toNonEmptySetOrNull import com.tangem.domain.tokens.error.TokenListSortingError -import com.tangem.domain.tokens.model.CryptoCurrency -import com.tangem.domain.tokens.models.Network -import com.tangem.domain.tokens.repository.TokensRepository +import com.tangem.domain.tokens.models.CryptoCurrency +import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.wallets.models.UserWalletId import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.withContext class ApplyTokenListSortingUseCase( - private val tokensRepository: TokensRepository, + private val currenciesRepository: CurrenciesRepository, private val dispatchers: CoroutineDispatcherProvider, ) { suspend operator fun invoke( userWalletId: UserWalletId, - sortedTokensIds: Set>, + sortedTokensIds: List, isGroupedByNetwork: Boolean, isSortedByBalance: Boolean, ): Either { @@ -39,51 +39,53 @@ class ApplyTokenListSortingUseCase( } private suspend fun Raise.sortTokens( - sortedTokensIds: Set>, - unsortedTokens: Set, - ): Set = withContext(dispatchers.default) { + sortedTokensIds: List, + unsortedTokens: List, + ): List = withContext(dispatchers.default) { val nonEmptySortedTokensIds = ensureNotNull(sortedTokensIds.toNonEmptySetOrNull()) { TokenListSortingError.TokenListIsEmpty } val sortedTokens = sortedMapOf() - unsortedTokens.forEach { token -> - val index = nonEmptySortedTokensIds.indexOfFirst { (networkId, tokenId) -> - networkId == token.networkId && tokenId == token.id + unsortedTokens.distinct().forEach { currency -> + val index = nonEmptySortedTokensIds.indexOfFirst { currencyId -> + currencyId == currency.id } if (index >= 0) { - sortedTokens[index] = token + sortedTokens[index] = currency } else { raise(TokenListSortingError.UnableToSortTokenList) } } - ensureNotNull(sortedTokens.values.toNonEmptySetOrNull()) { + ensureNotNull(sortedTokens.values.toNonEmptyListOrNull()) { TokenListSortingError.TokenListIsEmpty } } - private suspend fun Raise.getCurrencies(userWalletId: UserWalletId): Set { + private suspend fun Raise.getCurrencies(userWalletId: UserWalletId): List { val tokens = catch( - block = { tokensRepository.getMultiCurrencyWalletCurrencies(userWalletId, refresh = false).firstOrNull() }, + block = { + currenciesRepository.getMultiCurrencyWalletCurrencies(userWalletId, refresh = false).firstOrNull() + }, catch = { raise(TokenListSortingError.DataError(it)) }, ) - return ensureNotNull(tokens?.toNonEmptySetOrNull()) { + return ensureNotNull(tokens?.toNonEmptyListOrNull()) { TokenListSortingError.TokenListIsEmpty } } private suspend fun Raise.applySorting( userWalletId: UserWalletId, - tokens: Set, + tokens: List, isGrouped: Boolean, isSortedByBalance: Boolean, ) = withContext(dispatchers.io) { catch( - block = { tokensRepository.saveTokens(userWalletId, tokens, isGrouped, isSortedByBalance) }, + block = { currenciesRepository.saveTokens(userWalletId, tokens, isGrouped, isSortedByBalance) }, catch = { raise(TokenListSortingError.DataError(it)) }, ) } diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCurrencyUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCurrencyUseCase.kt new file mode 100644 index 0000000000..650abfd651 --- /dev/null +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCurrencyUseCase.kt @@ -0,0 +1,66 @@ +package com.tangem.domain.tokens + +import arrow.core.Either +import com.tangem.domain.tokens.error.CurrencyError +import com.tangem.domain.tokens.error.mapper.mapToCurrencyError +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.tokens.models.CryptoCurrency +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.* + +/** + * Use case for fetching the status of a specific cryptocurrency associated with a user wallet. + * + * @property currenciesRepository Repository for managing and fetching cryptocurrencies. + * @property quotesRepository Repository for managing and fetching cryptocurrency quotes. + * @property networksRepository Repository for managing and fetching information related to blockchain networks. + * @property dispatchers Provides coroutine dispatchers. + */ +class GetCurrencyUseCase( + private val currenciesRepository: CurrenciesRepository, + private val quotesRepository: QuotesRepository, + private val networksRepository: NetworksRepository, + private val dispatchers: CoroutineDispatcherProvider, +) { + + /** + * Invokes the use case. + * + * @param userWalletId The unique identifier of the user's wallet. + * @param currencyId The unique identifier of the cryptocurrency. + * @param refresh A boolean flag indicating whether the data should be refreshed. + * @return A [Flow] emitting either a [CurrencyError] or a [CryptoCurrencyStatus], indicating the result of the fetch operation. + */ + operator fun invoke( + userWalletId: UserWalletId, + currencyId: CryptoCurrency.ID, + refresh: Boolean = false, + ): Flow> { + return flow { + emitAll(getCurrency(userWalletId, currencyId, refresh)) + }.flowOn(dispatchers.io) + } + + private suspend fun getCurrency( + userWalletId: UserWalletId, + currencyId: CryptoCurrency.ID, + refresh: Boolean, + ): Flow> { + val operations = CurrenciesStatusesOperations( + currenciesRepository = currenciesRepository, + quotesRepository = quotesRepository, + networksRepository = networksRepository, + userWalletId = userWalletId, + refresh = refresh, + ) + + return operations.getCurrencyStatusFlow(currencyId).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/GetPrimaryCurrencyUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetPrimaryCurrencyUseCase.kt index bfc25b58da..58a8bba978 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetPrimaryCurrencyUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetPrimaryCurrencyUseCase.kt @@ -1,63 +1,62 @@ package com.tangem.domain.tokens import arrow.core.Either -import arrow.core.left -import arrow.core.raise.Raise -import arrow.core.raise.recover -import arrow.core.right -import com.tangem.domain.tokens.error.TokenError -import com.tangem.domain.tokens.error.mapper.mapToTokenError +import com.tangem.domain.tokens.error.CurrencyError +import com.tangem.domain.tokens.error.mapper.mapToCurrencyError import com.tangem.domain.tokens.model.CryptoCurrencyStatus 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.tokens.repository.TokensRepository import com.tangem.domain.wallets.models.UserWalletId import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.channelFlow -import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.* +/** + * Use case for fetching the status of the primary cryptocurrency associated with a user wallet. + * + * @property currenciesRepository Repository for managing and fetching cryptocurrencies. + * @property quotesRepository Repository for managing and fetching cryptocurrency quotes. + * @property networksRepository Repository for managing and fetching information related to blockchain networks. + * @property dispatchers Provides coroutine dispatchers. + */ class GetPrimaryCurrencyUseCase( - private val tokensRepository: TokensRepository, + private val currenciesRepository: CurrenciesRepository, private val quotesRepository: QuotesRepository, private val networksRepository: NetworksRepository, private val dispatchers: CoroutineDispatcherProvider, ) { + /** + * Invokes the use case. + * + * @param userWalletId The unique identifier of the user's wallet. + * @param refresh A boolean flag indicating whether the data should be refreshed. + * @return A [Flow] emitting either a [CurrencyError] or a [CryptoCurrencyStatus], indicating the result of the fetch operation. + */ operator fun invoke( userWalletId: UserWalletId, refresh: Boolean = false, - ): Flow> { - return channelFlow { - recover( - block = { - getToken(userWalletId, refresh).collectLatest { token -> - send(token.right()) - } - }, - recover = { error -> - send(error.left()) - }, - ) - } + ): Flow> { + return flow { + emitAll(getPrimaryCurrency(userWalletId, refresh)) + }.flowOn(dispatchers.io) } - private suspend fun Raise.getToken( + private suspend fun getPrimaryCurrency( userWalletId: UserWalletId, refresh: Boolean, - ): Flow { + ): Flow> { val operations = CurrenciesStatusesOperations( - tokensRepository = tokensRepository, + currenciesRepository = currenciesRepository, quotesRepository = quotesRepository, networksRepository = networksRepository, userWalletId = userWalletId, refresh = refresh, - dispatchers = dispatchers, - raise = this, - transformError = CurrenciesStatusesOperations.Error::mapToTokenError, ) - return operations.getPrimaryCurrencyStatusFlow() + return operations.getPrimaryCurrencyStatusFlow().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/GetTokenListUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetTokenListUseCase.kt index 0a142e1241..59125a081c 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetTokenListUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetTokenListUseCase.kt @@ -2,79 +2,69 @@ package com.tangem.domain.tokens import arrow.core.Either import arrow.core.left -import arrow.core.raise.Raise -import arrow.core.raise.recover -import arrow.core.right import com.tangem.domain.tokens.error.TokenListError import com.tangem.domain.tokens.error.mapper.mapToTokenListError import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.model.TokenList import com.tangem.domain.tokens.operations.CurrenciesStatusesOperations import com.tangem.domain.tokens.operations.TokenListOperations +import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.tokens.repository.NetworksRepository import com.tangem.domain.tokens.repository.QuotesRepository -import com.tangem.domain.tokens.repository.TokensRepository import com.tangem.domain.wallets.models.UserWalletId import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.channelFlow -import kotlinx.coroutines.flow.collectLatest -import kotlinx.coroutines.flow.flatMapConcat +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.* class GetTokenListUseCase( - internal val tokensRepository: TokensRepository, + internal val currenciesRepository: CurrenciesRepository, internal val quotesRepository: QuotesRepository, internal val networksRepository: NetworksRepository, internal val dispatchers: CoroutineDispatcherProvider, ) { - operator fun invoke(userWalletId: UserWalletId, refresh: Boolean = true): Flow> { - return channelFlow { - recover( - block = { - getTokenList(userWalletId, refresh).collectLatest { list -> - send(list.right()) - } + @OptIn(ExperimentalCoroutinesApi::class) + operator fun invoke(userWalletId: UserWalletId, refresh: Boolean = false): Flow> { + return getTokensStatuses(userWalletId, refresh).flatMapMerge { maybeTokens -> + maybeTokens.fold( + ifLeft = { error -> + flowOf(error.left()) }, - recover = { error -> - send(error.left()) + ifRight = { tokens -> + createTokenList(userWalletId, tokens) }, ) } } - private fun Raise.getTokenList(userWalletId: UserWalletId, refresh: Boolean): Flow { - return getTokensStatuses(userWalletId, refresh).flatMapConcat { tokens -> - createTokenList(userWalletId, tokens) - } - } - private fun Raise.getTokensStatuses( + private fun getTokensStatuses( userWalletId: UserWalletId, refresh: Boolean, - ): Flow> { + ): Flow>> { val operations = CurrenciesStatusesOperations( userWalletId = userWalletId, refresh = refresh, useCase = this@GetTokenListUseCase, - raise = this, - transformError = CurrenciesStatusesOperations.Error::mapToTokenListError, ) - return operations.getMultiCurrencyWalletStatusesFlow() + return operations.getCurrenciesStatusesFlow() + .map { maybeCurrenciesStatuses -> + maybeCurrenciesStatuses.mapLeft(CurrenciesStatusesOperations.Error::mapToTokenListError) + } } - private fun Raise.createTokenList( + private fun createTokenList( userWalletId: UserWalletId, - tokens: Set, - ): Flow { + tokens: List, + ): Flow> { val operations = TokenListOperations( userWalletId = userWalletId, tokens = tokens, useCase = this@GetTokenListUseCase, - raise = this, - transform = TokenListOperations.Error::mapToTokenListError, ) - return operations.getTokenListFlow() + return operations.getTokenListFlow().map { maybeTokenList -> + maybeTokenList.mapLeft(TokenListOperations.Error::mapToTokenListError) + } } } \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/ToggleTokenListGroupingUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/ToggleTokenListGroupingUseCase.kt index fe70f6aacd..07f86f5069 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/ToggleTokenListGroupingUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/ToggleTokenListGroupingUseCase.kt @@ -1,10 +1,7 @@ package com.tangem.domain.tokens import arrow.core.Either -import arrow.core.raise.Raise -import arrow.core.raise.catch -import arrow.core.raise.either -import arrow.core.raise.ensure +import arrow.core.raise.* import com.tangem.domain.tokens.error.TokenListSortingError import com.tangem.domain.tokens.error.mapper.mapToTokenListSortingError import com.tangem.domain.tokens.model.TokenList @@ -35,47 +32,40 @@ class ToggleTokenListGroupingUseCase( } } - private suspend fun Raise.groupTokens( - tokenList: TokenList.Ungrouped, - ): TokenList.GroupedByNetwork { - val sortingOperations = getSortingOperations(tokenList) - val tokens = sortingOperations.getTokens() + private fun Raise.groupTokens(tokenList: TokenList.Ungrouped): TokenList.GroupedByNetwork { + val sortingOperations = TokenListSortingOperations(tokenList) + val tokens = withError(TokenListSortingOperations.Error::mapToTokenListSortingError) { + sortingOperations.getTokens().bind() + } val networks = getNetworks(tokens.map { it.currency.networkId }.toSet()) return TokenList.GroupedByNetwork( - groups = sortingOperations.getGroupedTokens(networks), + groups = withError(TokenListSortingOperations.Error::mapToTokenListSortingError) { + sortingOperations.getGroupedTokens(networks).bind() + }, totalFiatBalance = tokenList.totalFiatBalance, sortedBy = sortingOperations.getSortType(), ) } - private suspend fun Raise.ungroupTokens( + private fun Raise.ungroupTokens( tokenList: TokenList.GroupedByNetwork, ): TokenList.Ungrouped { - val sortingOperations = getSortingOperations(tokenList) + val sortingOperations = TokenListSortingOperations(tokenList) return TokenList.Ungrouped( - currencies = sortingOperations.getTokens(), + currencies = withError(TokenListSortingOperations.Error::mapToTokenListSortingError) { + sortingOperations.getTokens().bind() + }, totalFiatBalance = tokenList.totalFiatBalance, sortedBy = sortingOperations.getSortType(), ) } - private fun Raise.getSortingOperations(tokenList: TokenList): TokenListSortingOperations<*> { - return TokenListSortingOperations( - tokenList = tokenList, - dispatchers = dispatchers, - raise = this, - transformError = TokenListSortingOperations.Error::mapToTokenListSortingError, + private fun Raise.getNetworks(networksIds: Set): Set { + return catch( + block = { networksRepository.getNetworks(networksIds) }, + catch = { raise(TokenListSortingError.DataError(it)) }, ) } - - private suspend fun Raise.getNetworks(networksIds: Set): Set { - return withContext(dispatchers.io) { - catch( - block = { networksRepository.getNetworks(networksIds) }, - catch = { raise(TokenListSortingError.DataError(it)) }, - ) - } - } } \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/ToggleTokenListSortingUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/ToggleTokenListSortingUseCase.kt index b91ce9521a..483b822928 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/ToggleTokenListSortingUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/ToggleTokenListSortingUseCase.kt @@ -4,6 +4,7 @@ import arrow.core.Either import arrow.core.raise.Raise import arrow.core.raise.either import arrow.core.raise.ensure +import arrow.core.raise.withError import com.tangem.domain.tokens.error.TokenListSortingError import com.tangem.domain.tokens.error.mapper.mapToTokenListSortingError import com.tangem.domain.tokens.model.TokenList @@ -31,36 +32,37 @@ class ToggleTokenListSortingUseCase( } } - private suspend fun Raise.sortGroupedTokenList( + private fun Raise.sortGroupedTokenList( tokenList: TokenList.GroupedByNetwork, ): TokenList.GroupedByNetwork { val operations = getSortingOperations(tokenList) val networks = tokenList.groups.map { it.network }.toSet() return tokenList.copy( - groups = operations.getGroupedTokens(networks), + groups = withError(TokenListSortingOperations.Error::mapToTokenListSortingError) { + operations.getGroupedTokens(networks).bind() + }, sortedBy = operations.getSortType(), ) } - private suspend fun Raise.sortUngroupedTokenList( + private fun Raise.sortUngroupedTokenList( tokenList: TokenList.Ungrouped, ): TokenList.Ungrouped { val operations = getSortingOperations(tokenList) return tokenList.copy( - currencies = operations.getTokens(), + currencies = withError(TokenListSortingOperations.Error::mapToTokenListSortingError) { + operations.getTokens().bind() + }, sortedBy = operations.getSortType(), ) } - private fun Raise.getSortingOperations(tokenList: TokenList): TokenListSortingOperations<*> { + private fun getSortingOperations(tokenList: TokenList): TokenListSortingOperations { return TokenListSortingOperations( tokenList = tokenList, sortByBalance = tokenList.sortedBy != TokenList.SortType.BALANCE, - dispatchers = dispatchers, - raise = this, - transformError = TokenListSortingOperations.Error::mapToTokenListSortingError, ) } } \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/error/CurrencyError.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/error/CurrencyError.kt new file mode 100644 index 0000000000..5d1ab7e2d8 --- /dev/null +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/error/CurrencyError.kt @@ -0,0 +1,8 @@ +package com.tangem.domain.tokens.error + +sealed class CurrencyError { + + object UnableToCreateCurrency : CurrencyError() + + data class DataError(val cause: Throwable) : CurrencyError() +} \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/error/TokenError.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/error/TokenError.kt deleted file mode 100644 index b8b3e0631b..0000000000 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/error/TokenError.kt +++ /dev/null @@ -1,8 +0,0 @@ -package com.tangem.domain.tokens.error - -sealed class TokenError { - - object UnableToCreateToken : TokenError() - - data class DataError(val cause: Throwable) : TokenError() -} \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/error/mapper/GetWalletTokenErrorMappers.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/error/mapper/GetWalletTokenErrorMappers.kt index e129b9fc6f..51fc33c463 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/error/mapper/GetWalletTokenErrorMappers.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/error/mapper/GetWalletTokenErrorMappers.kt @@ -1,15 +1,15 @@ package com.tangem.domain.tokens.error.mapper -import com.tangem.domain.tokens.error.TokenError +import com.tangem.domain.tokens.error.CurrencyError import com.tangem.domain.tokens.operations.CurrenciesStatusesOperations -internal fun CurrenciesStatusesOperations.Error.mapToTokenError(): TokenError { +internal fun CurrenciesStatusesOperations.Error.mapToCurrencyError(): CurrencyError { return when (this) { - is CurrenciesStatusesOperations.Error.DataError -> TokenError.DataError(this.cause) + is CurrenciesStatusesOperations.Error.DataError -> CurrencyError.DataError(this.cause) is CurrenciesStatusesOperations.Error.EmptyNetworksStatuses, is CurrenciesStatusesOperations.Error.EmptyQuotes, is CurrenciesStatusesOperations.Error.EmptyCurrencies, is CurrenciesStatusesOperations.Error.UnableToCreateCurrencyStatus, - -> TokenError.UnableToCreateToken + -> CurrencyError.UnableToCreateCurrency } } \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/CryptoCurrency.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/CryptoCurrency.kt deleted file mode 100644 index 5143f90497..0000000000 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/CryptoCurrency.kt +++ /dev/null @@ -1,89 +0,0 @@ -package com.tangem.domain.tokens.model - -import com.tangem.domain.tokens.models.Network - -/** - * Represents a generic cryptocurrency. - * - * @property id Unique identifier for the cryptocurrency. - * @property networkId Identifier for the network to which the cryptocurrency belongs. - * @property name Human-readable name of the cryptocurrency. - * @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. - */ -sealed class CryptoCurrency { - - abstract val id: ID - abstract val networkId: Network.ID - abstract val name: String - abstract val symbol: String - abstract val decimals: Int - abstract val iconUrl: String? - abstract val derivationPath: String? - - /** - * Represents a native coin in the blockchain network. - */ - data class Coin( - override val id: ID, - override val networkId: Network.ID, - override val name: String, - override val symbol: String, - override val decimals: Int, - override val iconUrl: String?, - override val derivationPath: String?, - ) : CryptoCurrency() { - - init { - checkProperties() - } - } - - /** - * 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, - override val networkId: Network.ID, - override val name: String, - override val symbol: String, - override val decimals: Int, - override val iconUrl: String?, - override val derivationPath: String?, - val contractAddress: String, - val isCustom: Boolean, - ) : CryptoCurrency() { - - init { - checkProperties() - require(contractAddress.isNotBlank()) { "Token contract address must not be blank" } - } - } - - /** - * Value class for uniquely identifying a cryptocurrency. - * - * @property value The unique identifier value. - */ - @JvmInline - value class ID(val value: String) { - - init { - require(value.isNotBlank()) { "Crypto currency ID must not be blank" } - } - } - - protected fun checkProperties() { - require(name.isNotBlank()) { "Crypto currency name must not be blank" } - 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/src/main/kotlin/com/tangem/domain/tokens/model/CryptoCurrencyStatus.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/CryptoCurrencyStatus.kt index a735f3ed06..1f2db6777f 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,5 +1,6 @@ package com.tangem.domain.tokens.model +import com.tangem.domain.tokens.models.CryptoCurrency import java.math.BigDecimal /** @@ -85,4 +86,15 @@ data class CryptoCurrencyStatus( override val priceChange: BigDecimal?, override val hasTransactionsInProgress: Boolean, ) : Status() + + /** + * Represents a state where the token is available, but there is no current quote available for it. + * + * @property amount The amount of the token. + * @property hasTransactionsInProgress Indicates if there are any transactions in progress related to the token. + */ + data class NoQuote( + override val amount: BigDecimal, + override val hasTransactionsInProgress: Boolean, + ) : Status() } \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/NetworkGroup.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/NetworkGroup.kt index 89264ac332..d2463c0f16 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/NetworkGroup.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/NetworkGroup.kt @@ -8,9 +8,9 @@ import com.tangem.domain.tokens.models.Network * This class encapsulates a collection of cryptocurrency statuses, all of which are part of the same blockchain network. * * @property network The blockchain network associated with the group. - * @property currencies A set of cryptocurrency statuses that belong to the network. + * @property currencies A list of cryptocurrency statuses that belong to the network. */ data class NetworkGroup( val network: Network, - val currencies: Set, + val currencies: List, ) \ 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 17bbaa392f..cfb815d9af 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 @@ -1,5 +1,6 @@ package com.tangem.domain.tokens.model +import com.tangem.domain.tokens.models.CryptoCurrency import com.tangem.domain.tokens.models.Network import java.math.BigDecimal diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/TokenList.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/TokenList.kt index 55327b82da..c447c3ac8d 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/TokenList.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/TokenList.kt @@ -18,12 +18,12 @@ sealed class TokenList { /** * Represents tokens that are grouped by their network. * - * @property groups A set of network groups containing tokens. + * @property groups A list of network groups containing tokens. * @property totalFiatBalance The total fiat balance across all groups. * @property sortedBy The criteria used for sorting the tokens within the groups. */ data class GroupedByNetwork( - val groups: Set, + val groups: List, override val totalFiatBalance: FiatBalance, override val sortedBy: SortType, ) : TokenList() @@ -31,12 +31,12 @@ sealed class TokenList { /** * Represents tokens that are not grouped by any specific criteria. * - * @property currencies A set of cryptocurrency statuses. + * @property currencies A list of cryptocurrency statuses. * @property totalFiatBalance The total fiat balance across all currencies. * @property sortedBy The criteria used for sorting the currencies. */ data class Ungrouped( - val currencies: Set, + val currencies: List, override val totalFiatBalance: FiatBalance, override val sortedBy: SortType, ) : TokenList() 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 6ce5fc8b60..463205a41c 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 @@ -1,159 +1,219 @@ package com.tangem.domain.tokens.operations import arrow.core.* -import arrow.core.raise.Raise -import arrow.core.raise.catch -import com.tangem.domain.core.raise.DelegatedRaise +import arrow.core.raise.* import com.tangem.domain.tokens.GetTokenListUseCase -import com.tangem.domain.tokens.model.* +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.tokens.model.NetworkStatus +import com.tangem.domain.tokens.models.CryptoCurrency import com.tangem.domain.tokens.models.Network +import com.tangem.domain.tokens.models.Quote +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.tokens.repository.TokensRepository import com.tangem.domain.wallets.models.UserWalletId -import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.* -import kotlinx.coroutines.withContext -@Suppress("LongParameterList") -internal class CurrenciesStatusesOperations( - private val tokensRepository: TokensRepository, +internal class CurrenciesStatusesOperations( + private val currenciesRepository: CurrenciesRepository, private val quotesRepository: QuotesRepository, private val networksRepository: NetworksRepository, private val userWalletId: UserWalletId, private val refresh: Boolean, - private val dispatchers: CoroutineDispatcherProvider, - raise: Raise, - transformError: (Error) -> E, -) : DelegatedRaise(raise, transformError) { +) { constructor( userWalletId: UserWalletId, refresh: Boolean, useCase: GetTokenListUseCase, - raise: Raise, - transformError: (Error) -> E, ) : this( - tokensRepository = useCase.tokensRepository, + currenciesRepository = useCase.currenciesRepository, quotesRepository = useCase.quotesRepository, networksRepository = useCase.networksRepository, userWalletId = userWalletId, refresh = refresh, - dispatchers = useCase.dispatchers, - raise = raise, - transformError = transformError, ) - fun getMultiCurrencyWalletStatusesFlow(): Flow> { - return getMultiCurrencyWalletCurrencies().flatMapConcat { - val tokens = it.toNonEmptySetOrNull() + @OptIn(ExperimentalCoroutinesApi::class) + fun getCurrenciesStatusesFlow(): Flow>> { + return getMultiCurrencyWalletCurrencies().transformLatest { maybeCurrencies -> + val nonEmptyCurrencies = maybeCurrencies.fold( + ifLeft = { error -> + emit(error.left()) + return@transformLatest + }, + ifRight = List::toNonEmptyListOrNull, + ) - if (tokens == null) { - flowOf(emptySet()) - } else { - val tokensIds = tokens.map { token -> token.id }.toNonEmptySet() - val groupedTokens = groupTokens(tokens) + if (nonEmptyCurrencies == null) { + val emptyCurrenciesStatuses = emptyList() - combine(getQuotes(tokensIds), getNetworksStatues(groupedTokens)) { quotes, networksStatuses -> - createTokensStatuses(tokens, quotes, networksStatuses) + emit(emptyCurrenciesStatuses.right()) + return@transformLatest + } else if (!refresh) { + val maybeLoadingCurrenciesStatuses = createCurrenciesStatuses( + currencies = nonEmptyCurrencies, + maybeNetworkStatuses = null, + maybeQuotes = null, + ) + + emit(maybeLoadingCurrenciesStatuses) + } + + val (networksIds, currenciesIds) = getIds(nonEmptyCurrencies) + + val currenciesFlow = combine( + getQuotes(currenciesIds), + getNetworksStatuses(networksIds), + ) { maybeQuotes, maybeNetworksStatuses -> + createCurrenciesStatuses(nonEmptyCurrencies, maybeQuotes, maybeNetworksStatuses) + } + + emitAll(currenciesFlow) + }.conflate() + } + + suspend fun getCurrencyStatusFlow(currencyId: CryptoCurrency.ID): Flow> { + val currency = recover( + block = { getMultiCurrencyWalletCurrency(currencyId) }, + recover = { return flowOf(it.left()) }, + ) + + return getCurrencyStatusFlow(currency) + } + + suspend fun getPrimaryCurrencyStatusFlow(): Flow> { + val currency = recover( + block = { getPrimaryCurrency() }, + recover = { return flowOf(it.left()) }, + ) + + return getCurrencyStatusFlow(currency) + } + + private fun getCurrencyStatusFlow(currency: CryptoCurrency): Flow> { + val (networksIds, currenciesIds) = getIds(nonEmptyListOf(currency)) + + val quoteFlow = getQuotes(currenciesIds) + .map { maybeQuotes -> + maybeQuotes.map { quotes -> + quotes.singleOrNull { it.rawCurrencyId == currency.id.rawCurrencyId } } } - } - } - suspend fun getPrimaryCurrencyStatusFlow(): Flow { - val token = getPrimaryCurrency() - - val quoteFlow = getQuotes(nonEmptySetOf(token.id)) - .map { quotes -> - quotes.singleOrNull { it.currencyId == token.id } + val statusFlow = getNetworksStatuses(networksIds) + .map { maybeStatuses -> + maybeStatuses.map { statuses -> + statuses.singleOrNull { it.networkId == currency.networkId } + } } - val statusFlow = getNetworksStatues(groupTokens(nonEmptySetOf(token))) - .map { statuses -> - statuses.singleOrNull { it.networkId == token.networkId } - } - - return combine(quoteFlow, statusFlow) { quote, networkStatus -> - createStatus(token, quote, networkStatus) + return combine(quoteFlow, statusFlow) { maybeQuote, maybeNetworkStatus -> + createStatus(currency, maybeQuote, maybeNetworkStatus) } } - private suspend fun createTokensStatuses( - tokens: Set, - quotes: Set, - networkStatuses: Set, - ): Set = withContext(dispatchers.default) { - tokens.mapTo(hashSetOf()) { token -> - val quote = quotes.firstOrNull { it.currencyId == token.id } - val networkStatus = networkStatuses.firstOrNull { it.networkId == token.networkId } + private fun createCurrenciesStatuses( + currencies: NonEmptyList, + maybeQuotes: Either>?, + maybeNetworkStatuses: Either>?, + ): Either> = either { + var quotesRetrievingFailed = false - createStatus(token, quote, networkStatus) + val networksStatuses = maybeNetworkStatuses?.bind()?.toNonEmptySetOrNull() + val quotes = recover({ maybeQuotes?.bind()?.toNonEmptySetOrNull() }) { + quotesRetrievingFailed = true + null + } + + currencies.map { currency -> + val quote = quotes?.firstOrNull { it.rawCurrencyId == currency.id.rawCurrencyId } + val networkStatus = networksStatuses?.firstOrNull { it.networkId == currency.networkId } + + createStatus(currency, quote, networkStatus, ignoreQuote = quotesRetrievingFailed) } } - private suspend fun createStatus( - token: CryptoCurrency, + private fun createStatus( + currency: CryptoCurrency, + maybeQuote: Either, + maybeNetworkStatus: Either, + ): Either = either { + var quoteRetrievingFailed = false + + val networkStatus = maybeNetworkStatus.bind() + val quote = recover({ maybeQuote.bind() }) { + quoteRetrievingFailed = true + null + } + + createStatus(currency, quote, networkStatus, ignoreQuote = quoteRetrievingFailed) + } + + private fun createStatus( + currency: CryptoCurrency, quote: Quote?, networkStatus: NetworkStatus?, + ignoreQuote: Boolean, ): CryptoCurrencyStatus { val currencyStatusOperations = CurrencyStatusOperations( - currency = token, + currency = currency, quote = quote, networkStatus = networkStatus, - dispatchers = dispatchers, - raise = this, - transformError = { Error.UnableToCreateCurrencyStatus }, + ignoreQuote = ignoreQuote, ) return currencyStatusOperations.createTokenStatus() } - private fun getMultiCurrencyWalletCurrencies(): Flow> { - return tokensRepository.getMultiCurrencyWalletCurrencies(userWalletId, refresh) - .catch { raise(Error.DataError(it)) } - .onEmpty { raise(Error.EmptyCurrencies) } - .flowOn(dispatchers.io) + private fun getMultiCurrencyWalletCurrencies(): Flow>> { + return currenciesRepository.getMultiCurrencyWalletCurrencies(userWalletId, refresh) + .map, Either>> { it.right() } + .catch { emit(Error.DataError(it).left()) } + .onEmpty { emit(Error.EmptyCurrencies.left()) } } - private suspend fun getPrimaryCurrency(): CryptoCurrency { - return withContext(dispatchers.io) { - catch( - block = { tokensRepository.getPrimaryCurrency(userWalletId) }, - catch = { raise(Error.DataError(it)) }, - ) - } + private suspend fun Raise.getMultiCurrencyWalletCurrency(currencyId: CryptoCurrency.ID): CryptoCurrency { + return Either.catch { currenciesRepository.getMultiCurrencyWalletCurrency(userWalletId, currencyId) } + .mapLeft { Error.DataError(it) } + .bind() } - private fun getQuotes(tokensIds: NonEmptySet): Flow> { + private suspend fun Raise.getPrimaryCurrency(): CryptoCurrency { + return catch( + block = { currenciesRepository.getSingleCurrencyWalletPrimaryCurrency(userWalletId) }, + catch = { raise(Error.DataError(it)) }, + ) + } + + private fun getQuotes(tokensIds: NonEmptySet): Flow>> { return quotesRepository.getQuotes(tokensIds, refresh) - .catch { raise(Error.DataError(it)) } - .onEmpty { raise(Error.EmptyQuotes) } - .flowOn(dispatchers.io) + .map, Either>> { it.right() } + .catch { emit(Error.DataError(it).left()) } + .onEmpty { emit(Error.EmptyQuotes.left()) } } - private fun getNetworksStatues( - groupedTokens: Map>, - ): Flow> { - return networksRepository.getNetworkStatuses(userWalletId, groupedTokens, refresh) - .catch { raise(Error.DataError(it)) } - .onEmpty { raise(Error.EmptyNetworksStatuses) } - .flowOn(dispatchers.io) + private fun getNetworksStatuses(networks: NonEmptySet): Flow>> { + return networksRepository.getNetworkStatuses(userWalletId, networks, refresh) + .map, Either>> { it.right() } + .catch { emit(Error.DataError(it).left()) } + .onEmpty { emit(Error.EmptyNetworksStatuses.left()) } } - private suspend fun groupTokens( - tokens: NonEmptySet, - ): Map> { - return withContext(dispatchers.default) { - tokens - .groupBy { it.networkId } - .mapValues { (_, tokens) -> - // Can not be empty - tokens.toNonEmptySetOrNull()!! - .map { it.id } - .toNonEmptySet() - } + private fun getIds( + currencies: NonEmptyList, + ): Pair, NonEmptySet> { + val currencyIdToNetworkId = currencies.associate { currency -> + currency.id to currency.networkId } + val currenciesIds = currencyIdToNetworkId.keys.toNonEmptySetOrNull() + val networksIds = currencyIdToNetworkId.values.toNonEmptySetOrNull() + + requireNotNull(currenciesIds) { "Currencies IDs cannot be empty" } + requireNotNull(networksIds) { "Networks IDs cannot be empty" } + + return networksIds 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 50f348f3d3..b37f324c1f 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 @@ -1,28 +1,19 @@ package com.tangem.domain.tokens.operations -import arrow.core.raise.Raise -import arrow.core.raise.ensureNotNull -import com.tangem.domain.core.raise.DelegatedRaise -import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.model.NetworkStatus -import com.tangem.domain.tokens.model.Quote -import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import kotlinx.coroutines.withContext +import com.tangem.domain.tokens.models.CryptoCurrency +import com.tangem.domain.tokens.models.Quote import java.math.BigDecimal -internal class CurrencyStatusOperations( +internal class CurrencyStatusOperations( private val currency: CryptoCurrency, private val quote: Quote?, private val networkStatus: NetworkStatus?, - private val dispatchers: CoroutineDispatcherProvider, - raise: Raise, - transformError: (Error) -> OtherError, -) : DelegatedRaise(raise, transformError) { + private val ignoreQuote: Boolean, +) { - suspend fun createTokenStatus(): CryptoCurrencyStatus = withContext(dispatchers.default) { - CryptoCurrencyStatus(currency, createStatus()) - } + fun createTokenStatus(): CryptoCurrencyStatus = CryptoCurrencyStatus(currency, createStatus()) private fun createStatus(): CryptoCurrencyStatus.Status { return when (val status = networkStatus?.value) { @@ -35,11 +26,13 @@ internal class CurrencyStatusOperations( } private fun createStatus(status: NetworkStatus.Verified): CryptoCurrencyStatus.Status { - val amount = ensureNotNull(status.amounts[currency.id]) { - Error.UnableToFindAmount(currency.id) - } + val amount = status.amounts[currency.id] ?: return CryptoCurrencyStatus.Unreachable return when { + ignoreQuote -> CryptoCurrencyStatus.NoQuote( + amount = amount, + hasTransactionsInProgress = status.hasTransactionsInProgress, + ) currency is CryptoCurrency.Token && currency.isCustom -> CryptoCurrencyStatus.Custom( amount = amount, fiatAmount = calculateFiatAmountOrNull(amount, quote?.fiatRate), @@ -67,9 +60,4 @@ internal class CurrencyStatusOperations( private fun calculateFiatAmount(amount: BigDecimal, fiatRate: BigDecimal): BigDecimal { return amount * fiatRate } - - sealed class Error { - - data class UnableToFindAmount(val currencyId: CryptoCurrency.ID) : Error() - } } \ No newline at end of file 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 dd3cb367d5..ec63a9fa0c 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 @@ -1,51 +1,50 @@ package com.tangem.domain.tokens.operations -import arrow.core.NonEmptySet +import arrow.core.NonEmptyList import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.model.TokenList -import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import kotlinx.coroutines.withContext import java.math.BigDecimal internal class TokenListFiatBalanceOperations( - private val currencies: NonEmptySet, + private val currencies: NonEmptyList, private val isAnyTokenLoading: Boolean, - private val dispatcher: CoroutineDispatcherProvider, ) { - suspend fun calculateFiatBalance(): TokenList.FiatBalance { - return withContext(dispatcher.single) { - var fiatBalance: TokenList.FiatBalance = TokenList.FiatBalance.Loading - if (isAnyTokenLoading) return@withContext fiatBalance + fun calculateFiatBalance(): TokenList.FiatBalance { + var fiatBalance: TokenList.FiatBalance = TokenList.FiatBalance.Loading + if (isAnyTokenLoading) return fiatBalance - for (token in currencies) { - when (val status = token.value) { - is CryptoCurrencyStatus.Loading -> { - fiatBalance = TokenList.FiatBalance.Loading - break - } - is CryptoCurrencyStatus.MissedDerivation, - is CryptoCurrencyStatus.Unreachable, - -> { - fiatBalance = TokenList.FiatBalance.Failed - break - } - is CryptoCurrencyStatus.NoAccount -> { - fiatBalance = recalculateBalanceForNoAccountStatus(fiatBalance) - } - is CryptoCurrencyStatus.Loaded -> { - fiatBalance = recalculateBalance(status, fiatBalance) - } - is CryptoCurrencyStatus.Custom -> { - fiatBalance = recalculateBalance(status, fiatBalance) - } + for (token in currencies) { + when (val status = token.value) { + is CryptoCurrencyStatus.Loading -> { + fiatBalance = TokenList.FiatBalance.Loading + break + } + is CryptoCurrencyStatus.MissedDerivation, + is CryptoCurrencyStatus.Unreachable, + -> { + fiatBalance = TokenList.FiatBalance.Failed + break + } + is CryptoCurrencyStatus.NoAccount -> { + fiatBalance = recalculateBalanceWithoutQuote(fiatBalance) + } + is CryptoCurrencyStatus.Loaded -> { + fiatBalance = recalculateBalance(status, fiatBalance) + } + is CryptoCurrencyStatus.Custom -> { + fiatBalance = recalculateBalance(status, fiatBalance) + } + is CryptoCurrencyStatus.NoQuote -> { + fiatBalance = recalculateBalanceWithoutQuote(fiatBalance) } } - - fiatBalance } + + return fiatBalance } - private fun recalculateBalanceForNoAccountStatus(currentBalance: TokenList.FiatBalance): TokenList.FiatBalance { + + private fun recalculateBalanceWithoutQuote(currentBalance: TokenList.FiatBalance): TokenList.FiatBalance { return with(currentBalance) { (this as? TokenList.FiatBalance.Loaded)?.copy( isAllAmountsSummarized = false, diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TokenListOperations.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TokenListOperations.kt index 37b96a781c..627df695af 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TokenListOperations.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TokenListOperations.kt @@ -1,97 +1,81 @@ package com.tangem.domain.tokens.operations -import arrow.core.NonEmptySet -import arrow.core.raise.Raise -import arrow.core.raise.catch -import arrow.core.raise.ensureNotNull -import arrow.core.toNonEmptySetOrNull -import com.tangem.domain.core.raise.DelegatedRaise +import arrow.core.* +import arrow.core.raise.* import com.tangem.domain.tokens.GetTokenListUseCase import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.model.TokenList import com.tangem.domain.tokens.models.Network +import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.tokens.repository.NetworksRepository -import com.tangem.domain.tokens.repository.TokensRepository import com.tangem.domain.wallets.models.UserWalletId -import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.flow.* -import kotlinx.coroutines.withContext @Suppress("LongParameterList") -internal class TokenListOperations( - private val tokensRepository: TokensRepository, +internal class TokenListOperations( + private val currenciesRepository: CurrenciesRepository, private val networksRepository: NetworksRepository, private val userWalletId: UserWalletId, - private val tokens: Set, - private val dispatchers: CoroutineDispatcherProvider, - raise: Raise, - transform: (Error) -> E, -) : DelegatedRaise(raise, transform) { + private val tokens: List, +) { constructor( userWalletId: UserWalletId, - tokens: Set, + tokens: List, useCase: GetTokenListUseCase, - raise: Raise, - transform: (Error) -> E, ) : this( - tokensRepository = useCase.tokensRepository, + currenciesRepository = useCase.currenciesRepository, networksRepository = useCase.networksRepository, userWalletId = userWalletId, tokens = tokens, - dispatchers = useCase.dispatchers, - raise = raise, - transform = transform, ) - fun getTokenListFlow(): Flow { - return combine(getIsGrouped(), getIsSortedByBalance()) { isGrouped, isSortedByBalance -> - createTokenList(isGrouped, isSortedByBalance) + fun getTokenListFlow(): Flow> { + return combine( + getIsGrouped(), + getIsSortedByBalance(), + ) { isGrouped, isSortedByBalance -> + either { + createTokenList(isGrouped.bind(), isSortedByBalance.bind()) + } } } - private suspend fun createTokenList(isGrouped: Boolean, isSortedByBalance: Boolean): TokenList { - return withContext(dispatchers.default) { - val tokensNes = tokens.toNonEmptySetOrNull() - ?: return@withContext TokenList.NotInitialized + private fun Raise.createTokenList(isGrouped: Boolean, isSortedByBalance: Boolean): TokenList { + val nonEmptyCurrencies = tokens.toNonEmptyListOrNull() + ?: return TokenList.NotInitialized - val isAnyTokenLoading = tokensNes.any { it.value is CryptoCurrencyStatus.Loading } - val fiatBalanceOperations = TokenListFiatBalanceOperations(tokensNes, isAnyTokenLoading, dispatchers) + val isAnyTokenLoading = nonEmptyCurrencies.any { it.value is CryptoCurrencyStatus.Loading } + val fiatBalanceOperations = TokenListFiatBalanceOperations(nonEmptyCurrencies, isAnyTokenLoading) - createTokenList( - tokens = tokensNes, - fiatBalance = fiatBalanceOperations.calculateFiatBalance(), - isAnyTokenLoading = isAnyTokenLoading, - isGrouped = isGrouped, - isSortedByBalance = isSortedByBalance, - ) - } + return createTokenList( + currencies = nonEmptyCurrencies, + fiatBalance = fiatBalanceOperations.calculateFiatBalance(), + isAnyTokenLoading = isAnyTokenLoading, + isGrouped = isGrouped, + isSortedByBalance = isSortedByBalance, + ) } - private suspend fun createTokenList( - tokens: NonEmptySet, + private fun Raise.createTokenList( + currencies: NonEmptyList, fiatBalance: TokenList.FiatBalance, isAnyTokenLoading: Boolean, isGrouped: Boolean, isSortedByBalance: Boolean, ): TokenList { val sortingOperations = TokenListSortingOperations( - currencies = tokens, + currencies = currencies, isAnyTokenLoading = isAnyTokenLoading, sortByBalance = isSortedByBalance, - dispatchers = dispatchers, - raise = this, - transformError = { e -> - Error.fromTokenListOperations(e) { createUnsortedUngroupedTokenList(tokens, fiatBalance) } - }, ) - return createTokenList(tokens, sortingOperations, fiatBalance, isGrouped) + return createTokenList(currencies, sortingOperations, fiatBalance, isGrouped) } - private suspend fun createTokenList( - tokens: NonEmptySet, - sortingOperations: TokenListSortingOperations<*>, + private fun Raise.createTokenList( + tokens: NonEmptyList, + sortingOperations: TokenListSortingOperations, fiatBalance: TokenList.FiatBalance, isGrouped: Boolean, ): TokenList { @@ -108,37 +92,46 @@ internal class TokenListOperations( } } - private suspend fun getNetworks(tokensNes: NonEmptySet): Set { - return withContext(dispatchers.io) { - val networksIds = tokensNes.map { it.currency.networkId }.toNonEmptySet() - catch( - block = { networksRepository.getNetworks(networksIds) }, - catch = { raise(Error.DataError(it)) }, - ) - } + private fun Raise.getNetworks(tokensNes: NonEmptyList): Set { + val networksIds = tokensNes.map { it.currency.networkId }.toNonEmptySet() + + return catch( + block = { networksRepository.getNetworks(networksIds) }, + catch = { raise(Error.DataError(it)) }, + ) } - private suspend fun createUngroupedTokenList( - sortingOperations: TokenListSortingOperations<*>, + private fun Raise.createUngroupedTokenList( + sortingOperations: TokenListSortingOperations, fiatBalance: TokenList.FiatBalance, ): TokenList.Ungrouped = TokenList.Ungrouped( sortedBy = sortingOperations.getSortType(), totalFiatBalance = fiatBalance, - currencies = sortingOperations.getTokens(), + currencies = withError( + transform = { e -> + Error.fromTokenListOperations(e) { createUnsortedUngroupedTokenList(tokens, fiatBalance) } + }, + block = { sortingOperations.getTokens().bind() }, + ), ) - private suspend fun createGroupedTokenList( - sortingOperations: TokenListSortingOperations<*>, + private fun Raise.createGroupedTokenList( + sortingOperations: TokenListSortingOperations, fiatBalance: TokenList.FiatBalance, networks: NonEmptySet, ): TokenList.GroupedByNetwork = TokenList.GroupedByNetwork( sortedBy = sortingOperations.getSortType(), totalFiatBalance = fiatBalance, - groups = sortingOperations.getGroupedTokens(networks), + groups = withError( + transform = { e -> + Error.fromTokenListOperations(e) { createUnsortedUngroupedTokenList(tokens, fiatBalance) } + }, + block = { sortingOperations.getGroupedTokens(networks).bind() }, + ), ) private fun createUnsortedUngroupedTokenList( - tokens: NonEmptySet, + tokens: List, fiatBalance: TokenList.FiatBalance, ): TokenList.Ungrouped { return TokenList.Ungrouped( @@ -148,18 +141,18 @@ internal class TokenListOperations( ) } - private fun getIsGrouped(): Flow { - return tokensRepository.isTokensGrouped(userWalletId) - .catch { raise(Error.DataError(it)) } - .onEmpty { emit(value = false) } - .flowOn(dispatchers.io) + private fun getIsGrouped(): Flow> { + return currenciesRepository.isTokensGrouped(userWalletId) + .map> { it.right() } + .catch { emit(Error.DataError(it).left()) } + .onEmpty { emit(value = false.right()) } } - private fun getIsSortedByBalance(): Flow { - return tokensRepository.isTokensSortedByBalance(userWalletId) - .catch { raise(Error.DataError(it)) } - .onEmpty { emit(value = false) } - .flowOn(dispatchers.io) + private fun getIsSortedByBalance(): Flow> { + return currenciesRepository.isTokensSortedByBalance(userWalletId) + .map> { it.right() } + .catch { emit(Error.DataError(it).left()) } + .onEmpty { emit(value = false.right()) } } sealed class Error { diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TokenListSortingOperations.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TokenListSortingOperations.kt index 06c3041651..4e609a140f 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TokenListSortingOperations.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TokenListSortingOperations.kt @@ -1,76 +1,60 @@ package com.tangem.domain.tokens.operations -import arrow.core.NonEmptySet +import arrow.core.* import arrow.core.raise.Raise +import arrow.core.raise.either import arrow.core.raise.ensure import arrow.core.raise.ensureNotNull -import arrow.core.toNonEmptySetOrNull -import com.tangem.domain.core.raise.DelegatedRaise import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.model.NetworkGroup import com.tangem.domain.tokens.model.TokenList import com.tangem.domain.tokens.models.Network -import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import kotlinx.coroutines.withContext import java.math.BigDecimal -internal class TokenListSortingOperations( - private val currencies: Set, +internal class TokenListSortingOperations( + private val currencies: List, private val isAnyTokenLoading: Boolean, private val sortByBalance: Boolean, - private val dispatchers: CoroutineDispatcherProvider, - raise: Raise, - transformError: (Error) -> E, -) : DelegatedRaise(raise, transformError) { +) { constructor( tokenList: TokenList, - dispatchers: CoroutineDispatcherProvider, - raise: Raise, - transformError: (Error) -> E, sortByBalance: Boolean = tokenList.sortedBy == TokenList.SortType.BALANCE, isAnyTokenLoading: Boolean = tokenList.totalFiatBalance is TokenList.FiatBalance.Loading, ) : this( currencies = when (tokenList) { - is TokenList.GroupedByNetwork -> tokenList.groups.flatMap { it.currencies }.toSet() + is TokenList.GroupedByNetwork -> tokenList.groups.flatMap { it.currencies } is TokenList.Ungrouped -> tokenList.currencies - is TokenList.NotInitialized -> emptySet() + is TokenList.NotInitialized -> emptyList() }, isAnyTokenLoading = isAnyTokenLoading, sortByBalance = sortByBalance, - dispatchers = dispatchers, - raise = raise, - transformError = transformError, ) - suspend fun getGroupedTokens(networks: Set): NonEmptySet { - return withContext(dispatchers.default) { - ensure(currencies.isNotEmpty()) { Error.EmptyTokens } - val networksNes = ensureNotNull(networks.toNonEmptySetOrNull()) { - Error.EmptyNetworks - } + fun getGroupedTokens(networks: Set): Either> = either { + ensure(currencies.isNotEmpty()) { Error.EmptyTokens } + val networksNes = ensureNotNull(networks.toNonEmptySetOrNull()) { + Error.EmptyNetworks + } - if (sortByBalance) { - groupAndSortTokensByBalance(networksNes) - } else { - groupTokens(networksNes) - } + if (sortByBalance) { + groupAndSortTokensByBalance(networksNes) + } else { + groupTokens(networksNes) } } - suspend fun getTokens(): NonEmptySet { - return withContext(dispatchers.default) { - val tokensNes = ensureNotNull(currencies.toNonEmptySetOrNull()) { - Error.EmptyTokens - } - - if (sortByBalance) sortTokensByBalance(tokensNes) else tokensNes + fun getTokens(): Either> = either { + val nonEmptyCurrencies = ensureNotNull(currencies.toNonEmptyListOrNull()) { + Error.EmptyTokens } + + if (sortByBalance) sortTokensByBalance(nonEmptyCurrencies) else nonEmptyCurrencies } - fun getSortType() = if (sortByBalance) TokenList.SortType.BALANCE else TokenList.SortType.NONE + fun getSortType(): TokenList.SortType = if (sortByBalance) TokenList.SortType.BALANCE else TokenList.SortType.NONE - private fun groupTokens(networks: NonEmptySet): NonEmptySet { + private fun Raise.groupTokens(networks: NonEmptySet): NonEmptyList { val groupedTokens = currencies .groupBy { it.currency.networkId } .map { (networkId, tokens) -> @@ -80,22 +64,21 @@ internal class TokenListSortingOperations( NetworkGroup( network = network, - currencies = ensureNotNull(tokens.toNonEmptySetOrNull()) { Error.EmptyTokens }, + currencies = ensureNotNull(tokens.toNonEmptyListOrNull()) { Error.EmptyTokens }, ) } - .toNonEmptySetOrNull() + .toNonEmptyListOrNull() return ensureNotNull(groupedTokens) { Error.EmptyTokens } } - private fun groupAndSortTokensByBalance(networks: NonEmptySet): NonEmptySet { + private fun Raise.groupAndSortTokensByBalance(networks: NonEmptySet): NonEmptyList { val groupsWithSortedTokens = groupTokens(networks) .map { group -> - val tokens = group.currencies as? NonEmptySet + val tokens = group.currencies as? NonEmptyList ?: error("Tokens can not be empty here") group.copy(currencies = sortTokensByBalance(tokens)) } - .toNonEmptySet() return if (isAnyTokenLoading) { groupsWithSortedTokens @@ -104,22 +87,22 @@ internal class TokenListSortingOperations( } } - private fun sortTokensByBalance(tokens: NonEmptySet): NonEmptySet { + private fun sortTokensByBalance(tokens: NonEmptyList): NonEmptyList { return if (isAnyTokenLoading) { tokens } else { tokens.sortedByDescending { it.value.fiatAmount ?: BigDecimal.ZERO } - .toNonEmptySetOrNull() + .toNonEmptyListOrNull() ?: error("Tokens can not be empty here") } } - private fun sortGroupsByBalance(groupsWithSortedTokens: NonEmptySet): NonEmptySet { + private fun sortGroupsByBalance(groupsWithSortedTokens: NonEmptyList): NonEmptyList { return groupsWithSortedTokens .sortedByDescending { group -> group.currencies.sumOf { it.value.fiatAmount ?: BigDecimal.ZERO } } - .toNonEmptySetOrNull() + .toNonEmptyListOrNull() ?: error("Tokens can not be empty here") } diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/TokensRepository.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/CurrenciesRepository.kt similarity index 52% rename from domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/TokensRepository.kt rename to domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/CurrenciesRepository.kt index 865c5fc4da..91a04b5af8 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/TokensRepository.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/CurrenciesRepository.kt @@ -1,26 +1,28 @@ package com.tangem.domain.tokens.repository -import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.tokens.models.CryptoCurrency import com.tangem.domain.wallets.models.UserWalletId import kotlinx.coroutines.flow.Flow /** * Repository for everything related to the tokens of user wallet * */ -interface TokensRepository { +interface CurrenciesRepository { /** - * Saves the given set of cryptocurrencies, along with the preferences for grouping and sorting, for a specific + * Saves the given list of cryptocurrencies, along with the preferences for grouping and sorting, for a specific * multi-currency user wallet. * * @param userWalletId The unique identifier of the user wallet. - * @param currencies The set of cryptocurrencies to be saved. + * @param currencies The list of cryptocurrencies to be saved. * @param isGroupedByNetwork A boolean flag indicating whether the tokens should be grouped by network. * @param isSortedByBalance A boolean flag indicating whether the tokens should be sorted by balance. + * @throws com.tangem.domain.core.error.DataError.UserWalletError.WrongUserWallet If single-currency user wallet + * ID provided. */ suspend fun saveTokens( userWalletId: UserWalletId, - currencies: Set, + currencies: List, isGroupedByNetwork: Boolean, isSortedByBalance: Boolean, ) @@ -30,23 +32,40 @@ interface TokensRepository { * * @param userWalletId The unique identifier of the user wallet. * @return The primary cryptocurrency associated with the user wallet. + * @throws com.tangem.domain.core.error.DataError.UserWalletError.WrongUserWallet If multi-currency user wallet + * ID provided. */ - suspend fun getPrimaryCurrency(userWalletId: UserWalletId): CryptoCurrency + suspend fun getSingleCurrencyWalletPrimaryCurrency(userWalletId: UserWalletId): CryptoCurrency /** - * Retrieves the set of cryptocurrencies within a multi-currency wallet. + * Retrieves the list of cryptocurrencies within a multi-currency wallet. * * @param userWalletId The unique identifier of the user wallet. * @param refresh A boolean flag indicating whether the data should be refreshed. * @return A [Flow] emitting the set of cryptocurrencies associated with the user wallet. + * @throws com.tangem.domain.core.error.DataError.UserWalletError.WrongUserWallet If single-currency user wallet + * ID provided. */ - fun getMultiCurrencyWalletCurrencies(userWalletId: UserWalletId, refresh: Boolean): Flow> + fun getMultiCurrencyWalletCurrencies(userWalletId: UserWalletId, refresh: Boolean): Flow> + + /** + * Retrieves the cryptocurrency for a specific multi-currency user wallet. + * + * @param userWalletId The unique identifier of the user wallet. + * @param id The unique identifier of the cryptocurrency to be retrieved. + * @return The cryptocurrency associated with the user wallet and ID. + * @throws com.tangem.domain.core.error.DataError.UserWalletError.WrongUserWallet If single-currency user wallet + * ID provided. + */ + suspend fun getMultiCurrencyWalletCurrency(userWalletId: UserWalletId, id: CryptoCurrency.ID): CryptoCurrency /** * Determines whether the tokens within a specific multi-currency user wallet are grouped. * * @param userWalletId The unique identifier of the user wallet. * @return A [Flow] emitting a boolean value indicating whether the tokens are grouped. + * @throws com.tangem.domain.core.error.DataError.UserWalletError.WrongUserWallet If single-currency user wallet + * ID provided. */ fun isTokensGrouped(userWalletId: UserWalletId): Flow @@ -55,6 +74,8 @@ interface TokensRepository { * * @param userWalletId The unique identifier of the user wallet. * @return A [Flow] emitting a boolean value indicating whether the tokens are sorted by balance. + * @throws com.tangem.domain.core.error.DataError.UserWalletError.WrongUserWallet If single-currency user wallet + * ID provided. */ fun isTokensSortedByBalance(userWalletId: UserWalletId): Flow } \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/NetworksRepository.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/NetworksRepository.kt index 5acf293b11..6dcec4843b 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 @@ -1,6 +1,5 @@ package com.tangem.domain.tokens.repository -import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.NetworkStatus import com.tangem.domain.tokens.models.Network import com.tangem.domain.wallets.models.UserWalletId @@ -23,13 +22,13 @@ interface NetworksRepository { * Retrieves the statuses of specified blockchain networks for a specific user wallet. * * @param userWalletId The unique identifier of the user wallet. - * @param networks A map of network IDs to sets of cryptocurrency IDs, representing the networks for which statuses are to be retrieved. + * @param networks A set of network IDs 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. */ fun getNetworkStatuses( userWalletId: UserWalletId, - networks: Map>, + networks: Set, refresh: Boolean, ): Flow> } \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/QuotesRepository.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/QuotesRepository.kt index 1c783ce342..39d6d8489d 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/QuotesRepository.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/QuotesRepository.kt @@ -1,7 +1,7 @@ package com.tangem.domain.tokens.repository -import com.tangem.domain.tokens.model.CryptoCurrency -import com.tangem.domain.tokens.model.Quote +import com.tangem.domain.tokens.models.CryptoCurrency +import com.tangem.domain.tokens.models.Quote import kotlinx.coroutines.flow.Flow /** @@ -12,9 +12,9 @@ interface QuotesRepository { /** * Retrieves the quotes for a set of specified cryptocurrencies, identified by their unique IDs. * - * @param tokensIds The unique identifiers of the cryptocurrencies for which quotes are to be retrieved. + * @param currenciesIds The unique identifiers of the cryptocurrencies for which quotes are to be retrieved. * @param refresh A boolean flag indicating whether the data should be refreshed. * @return A [Flow] emitting a set of quotes corresponding to the specified cryptocurrencies. */ - fun getQuotes(tokensIds: Set, refresh: Boolean): Flow> + fun getQuotes(currenciesIds: Set, refresh: Boolean): Flow> } \ No newline at end of file diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/ApplyTokenListSortingUseCaseTest.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/ApplyTokenListSortingUseCaseTest.kt index 0bdf923b6b..d5f180d4be 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/ApplyTokenListSortingUseCaseTest.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/ApplyTokenListSortingUseCaseTest.kt @@ -6,8 +6,8 @@ import arrow.core.right import com.tangem.domain.core.error.DataError import com.tangem.domain.tokens.error.TokenListSortingError import com.tangem.domain.tokens.mock.MockTokens -import com.tangem.domain.tokens.model.CryptoCurrency -import com.tangem.domain.tokens.repository.MockTokensRepository +import com.tangem.domain.tokens.models.CryptoCurrency +import com.tangem.domain.tokens.repository.MockCurrenciesRepository import com.tangem.domain.wallets.models.UserWalletId import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider import junit.framework.TestCase.assertEquals @@ -32,7 +32,7 @@ internal class ApplyTokenListSortingUseCaseTest { // When val result = useCase( userWalletId = userWalletId, - sortedTokensIds = emptySet(), + sortedTokensIds = emptyList(), isGroupedByNetwork = false, isSortedByBalance = false, ) @@ -54,7 +54,7 @@ internal class ApplyTokenListSortingUseCaseTest { // When val result = useCase( userWalletId = userWalletId, - sortedTokensIds = MockTokens.tokens.map { it.networkId to it.id }.toSet(), + sortedTokensIds = MockTokens.tokens.map { it.id }, isGroupedByNetwork = false, isSortedByBalance = false, ) @@ -76,7 +76,7 @@ internal class ApplyTokenListSortingUseCaseTest { // When useCase( userWalletId = userWalletId, - sortedTokensIds = expectedTokens.map { it.networkId to it.id }.toSet(), + sortedTokensIds = expectedTokens.map { it.id }, isGroupedByNetwork = expectedIsGrouped, isSortedByBalance = expectedIsSorted, ) @@ -100,7 +100,7 @@ internal class ApplyTokenListSortingUseCaseTest { // When useCase( userWalletId = userWalletId, - sortedTokensIds = expectedTokens.map { it.networkId to it.id }.toSet(), + sortedTokensIds = expectedTokens.map { it.id }, isGroupedByNetwork = expectedIsGrouped, isSortedByBalance = expectedIsSorted, ) @@ -124,7 +124,7 @@ internal class ApplyTokenListSortingUseCaseTest { // When useCase( userWalletId = userWalletId, - sortedTokensIds = expectedTokens.map { it.networkId to it.id }.toSet(), + sortedTokensIds = expectedTokens.map { it.id }, isGroupedByNetwork = expectedIsGrouped, isSortedByBalance = expectedIsSorted, ) @@ -148,7 +148,7 @@ internal class ApplyTokenListSortingUseCaseTest { // When useCase( userWalletId = userWalletId, - sortedTokensIds = expectedTokens.map { it.networkId to it.id }.toSet(), + sortedTokensIds = expectedTokens.map { it.id }, isGroupedByNetwork = expectedIsGrouped, isSortedByBalance = expectedIsSorted, ) @@ -170,7 +170,7 @@ internal class ApplyTokenListSortingUseCaseTest { // When val result = useCase( userWalletId = userWalletId, - sortedTokensIds = getSortedTokens().drop(n = 3).map { it.networkId to it.id }.toSet(), + sortedTokensIds = getSortedTokens().drop(n = 3).map { it.id }, isGroupedByNetwork = false, isSortedByBalance = false, ) @@ -181,18 +181,17 @@ internal class ApplyTokenListSortingUseCaseTest { private fun getSortedTokens() = MockTokens.tokens .sortedBy { Random.nextInt(0, MockTokens.tokens.size) } - .toSet() - private fun getUseCase(tokensRepository: MockTokensRepository = getTokensRepository()) = + private fun getUseCase(tokensRepository: MockCurrenciesRepository = getTokensRepository()) = ApplyTokenListSortingUseCase( - tokensRepository = tokensRepository, + currenciesRepository = tokensRepository, dispatchers = TestingCoroutineDispatcherProvider(), ) private fun getTokensRepository( sortTokensResult: Either = Unit.right(), - tokens: Flow>> = flowOf(MockTokens.tokens.right()), - ): MockTokensRepository { - return MockTokensRepository(sortTokensResult, MockTokens.token1.right(), tokens, emptyFlow(), emptyFlow()) + tokens: Flow>> = flowOf(MockTokens.tokens.right()), + ): MockCurrenciesRepository { + return MockCurrenciesRepository(sortTokensResult, MockTokens.token1.right(), tokens, emptyFlow(), emptyFlow()) } } \ No newline at end of file diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/GetPrimaryCurrencyUseCaseTest.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/GetPrimaryCurrencyUseCaseTest.kt index 95026c6926..3d5b25e5eb 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/GetPrimaryCurrencyUseCaseTest.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/GetPrimaryCurrencyUseCaseTest.kt @@ -4,18 +4,18 @@ import arrow.core.Either import arrow.core.left import arrow.core.right import com.tangem.domain.core.error.DataError -import com.tangem.domain.tokens.error.TokenError +import com.tangem.domain.tokens.error.CurrencyError import com.tangem.domain.tokens.mock.MockNetworks import com.tangem.domain.tokens.mock.MockQuotes import com.tangem.domain.tokens.mock.MockTokens import com.tangem.domain.tokens.mock.MockTokensStates -import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.model.NetworkStatus -import com.tangem.domain.tokens.model.Quote +import com.tangem.domain.tokens.models.CryptoCurrency +import com.tangem.domain.tokens.models.Quote +import com.tangem.domain.tokens.repository.MockCurrenciesRepository import com.tangem.domain.tokens.repository.MockNetworksRepository import com.tangem.domain.tokens.repository.MockQuotesRepository -import com.tangem.domain.tokens.repository.MockTokensRepository import com.tangem.domain.wallets.models.UserWalletId import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider import junit.framework.TestCase.assertEquals @@ -47,7 +47,7 @@ internal class GetPrimaryCurrencyUseCaseTest { @Test fun `when token getting failed then error should be received`() = runTest { // Given - val expectedResult = TokenError.DataError(DataError.NetworkError.NoInternetConnection).left() + val expectedResult = CurrencyError.DataError(DataError.NetworkError.NoInternetConnection).left() val useCase = getUseCase(token = DataError.NetworkError.NoInternetConnection.left()) @@ -59,9 +59,9 @@ internal class GetPrimaryCurrencyUseCaseTest { } @Test - fun `when quotes getting failed then error should be received`() = runTest { + fun `when quotes getting failed then currency with no quote status should be received`() = runTest { // Given - val expectedResult = TokenError.DataError(DataError.NetworkError.NoInternetConnection).left() + val expectedResult = MockTokensStates.noQuotesTokensStatuses.first().right() val useCase = getUseCase(quotes = flowOf(DataError.NetworkError.NoInternetConnection.left())) @@ -75,7 +75,7 @@ internal class GetPrimaryCurrencyUseCaseTest { @Test fun `when networks statuses getting failed then error should be received`() = runTest { // Given - val expectedResult = TokenError.DataError(DataError.NetworkError.NoInternetConnection).left() + val expectedResult = CurrencyError.DataError(DataError.NetworkError.NoInternetConnection).left() val useCase = getUseCase(statuses = flowOf(DataError.NetworkError.NoInternetConnection.left())) @@ -88,7 +88,7 @@ internal class GetPrimaryCurrencyUseCaseTest { @Test fun `when networks statuses flow is empty then error should be received`() = runTest { - val expectedResult = TokenError.UnableToCreateToken.left() + val expectedResult = CurrencyError.UnableToCreateCurrency.left() val useCase = getUseCase(statuses = flowOf()) @@ -100,8 +100,8 @@ internal class GetPrimaryCurrencyUseCaseTest { } @Test - fun `when quotes flow is empty then error should be received`() = runTest { - val expectedResult = TokenError.UnableToCreateToken.left() + fun `when quotes flow is empty then no quote status should be received`() = runTest { + val expectedResult = MockTokensStates.noQuotesTokensStatuses.first().right() val useCase = getUseCase(quotes = flowOf()) @@ -154,7 +154,7 @@ internal class GetPrimaryCurrencyUseCaseTest { statuses: Flow>> = flowOf(MockNetworks.verifiedNetworksStatuses.right()), ) = GetPrimaryCurrencyUseCase( dispatchers = dispatchers, - tokensRepository = MockTokensRepository( + currenciesRepository = MockCurrenciesRepository( sortTokensResult = Unit.right(), token = token, tokens = flowOf(), 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 204bf4af9c..8b9eebf8a8 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 @@ -9,16 +9,18 @@ import com.tangem.domain.tokens.mock.MockNetworks import com.tangem.domain.tokens.mock.MockQuotes import com.tangem.domain.tokens.mock.MockTokenLists import com.tangem.domain.tokens.mock.MockTokens -import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.NetworkStatus -import com.tangem.domain.tokens.model.Quote +import com.tangem.domain.tokens.model.TokenList +import com.tangem.domain.tokens.models.CryptoCurrency import com.tangem.domain.tokens.models.Network +import com.tangem.domain.tokens.models.Quote +import com.tangem.domain.tokens.repository.MockCurrenciesRepository import com.tangem.domain.tokens.repository.MockNetworksRepository import com.tangem.domain.tokens.repository.MockQuotesRepository -import com.tangem.domain.tokens.repository.MockTokensRepository import com.tangem.domain.wallets.models.UserWalletId import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider import junit.framework.TestCase.assertEquals +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.* import kotlinx.coroutines.test.runTest import org.junit.Test @@ -31,7 +33,10 @@ internal class GetTokenListUseCaseTest { @Test fun `when list ungrouped and unsorted then correct token list should be returned`() = runTest { // Given - val expectedResult = MockTokenLists.failedUngroupedTokenList.right() + val expectedResult = listOf( + MockTokenLists.loadingUngroupedTokenList.right(), + MockTokenLists.failedUngroupedTokenList.right(), + ) val useCase = getUseCase( isGrouped = flowOf(false.right()), @@ -39,7 +44,30 @@ internal class GetTokenListUseCaseTest { ) // When - val result = useCase(userWalletId).first() + val result = useCase(userWalletId) + .take(count = 2) + .toList() + + // Then + assertEquals(expectedResult, result) + } + + @Test + fun `when list refreshed then correct token list should be returned`() = runTest { + // Given + val expectedResult = listOf( + MockTokenLists.failedUngroupedTokenList.right(), + ) + + val useCase = getUseCase( + isGrouped = flowOf(false.right()), + isSortedByBalance = flowOf(false.right()), + ) + + // When + val result = useCase(userWalletId, refresh = true) + .take(count = 1) + .toList() // Then assertEquals(expectedResult, result) @@ -60,14 +88,22 @@ internal class GetTokenListUseCaseTest { } @Test - fun `when quotes getting failed then error should be received`() = runTest { + fun `when quotes getting failed then token list without quotes should be received`() = runTest { // Given - val expectedResult = TokenListError.DataError(DataError.NetworkError.NoInternetConnection).left() + val expectedResult = listOf( + MockTokenLists.loadingUngroupedTokenList.right(), + MockTokenLists.noQuotesUngroupedTokenList.right(), + ) - val useCase = getUseCase(quotes = flowOf(DataError.NetworkError.NoInternetConnection.left())) + val useCase = getUseCase( + quotes = flowOf(DataError.NetworkError.NoInternetConnection.left()), + statuses = flowOf(MockNetworks.verifiedNetworksStatuses.right()), + ) // When - val result = useCase(userWalletId).first() + val result = useCase(userWalletId) + .take(count = 2) + .toList() // Then assertEquals(expectedResult, result) @@ -98,7 +134,7 @@ internal class GetTokenListUseCaseTest { val useCase = getUseCase(statuses = flowOf(DataError.NetworkError.NoInternetConnection.left())) // When - val result = useCase(userWalletId).first() + val result = useCase(userWalletId, refresh = true).first() // Then assertEquals(expectedResult, result) @@ -137,6 +173,7 @@ internal class GetTokenListUseCaseTest { // Given val error = DataError.NetworkError.NoInternetConnection.left() val expectedResult = listOf( + MockTokenLists.loadingUngroupedTokenList.right(), MockTokenLists.failedUngroupedTokenList.right(), TokenListError.DataError(DataError.NetworkError.NoInternetConnection).left(), ) @@ -145,12 +182,12 @@ internal class GetTokenListUseCaseTest { tokens = flowOf( MockTokens.tokens.right(), error, - ), + ).map { delay(timeMillis = 1_000); it }, ) // When val result = useCase(userWalletId) - .take(count = 2) + .take(count = 3) .toList() // Then @@ -159,12 +196,17 @@ internal class GetTokenListUseCaseTest { @Test fun `when list grouped then correct token list should be received`() = runTest { - val expectedResult = MockTokenLists.failedGroupedTokenList.right() + val expectedResult = listOf( + MockTokenLists.loadingGroupedTokenList.right(), + MockTokenLists.failedGroupedTokenList.right(), + ) val useCase = getUseCase(isGrouped = flowOf(true.right())) // When - val result = useCase(userWalletId).first() + val result = useCase(userWalletId) + .take(count = 2) + .toList() // Then assertEquals(expectedResult, result) @@ -188,7 +230,10 @@ internal class GetTokenListUseCaseTest { @Test fun `when list is sorted and ungrouped then correct token list should be received`() = runTest { - val expectedResult = MockTokenLists.sortedUngroupedTokenList.right() + val expectedResult = listOf( + MockTokenLists.loadingUngroupedTokenList.copy(sortedBy = TokenList.SortType.BALANCE).right(), + MockTokenLists.sortedUngroupedTokenList.right(), + ) val useCase = getUseCase( statuses = flowOf(MockNetworks.verifiedNetworksStatuses.right()), @@ -197,7 +242,9 @@ internal class GetTokenListUseCaseTest { ) // When - val result = useCase(userWalletId).first() + val result = useCase(userWalletId) + .take(count = 2) + .toList() // Then assertEquals(expectedResult, result) @@ -205,7 +252,10 @@ internal class GetTokenListUseCaseTest { @Test fun `when list is sorted and grouped then correct token list should be received`() = runTest { - val expectedResult = MockTokenLists.sortedGroupedTokenList.right() + val expectedResult = listOf( + MockTokenLists.loadingGroupedTokenList.copy(sortedBy = TokenList.SortType.BALANCE).right(), + MockTokenLists.sortedGroupedTokenList.right(), + ) val useCase = getUseCase( statuses = flowOf(MockNetworks.verifiedNetworksStatuses.right()), @@ -214,7 +264,9 @@ internal class GetTokenListUseCaseTest { ) // When - val result = useCase(userWalletId).first() + val result = useCase(userWalletId) + .take(count = 2) + .toList() // Then assertEquals(expectedResult, result) @@ -224,7 +276,7 @@ internal class GetTokenListUseCaseTest { fun `when tokens is empty then not initialized token list should be received`() = runTest { val expectedResult = MockTokenLists.notInitializedTokenList.right() - val useCase = getUseCase(tokens = flowOf(emptySet().right())) + val useCase = getUseCase(tokens = flowOf(emptyList().right())) // When val result = useCase(userWalletId).first() @@ -235,7 +287,10 @@ internal class GetTokenListUseCaseTest { @Test fun `when networks is empty and list is grouped then ungrouped list should be received`() = runTest { - val expectedResult = TokenListError.UnableToSortTokenList(MockTokenLists.failedUngroupedTokenList).left() + val expectedResult = listOf( + TokenListError.UnableToSortTokenList(MockTokenLists.loadingUngroupedTokenList).left(), + TokenListError.UnableToSortTokenList(MockTokenLists.failedUngroupedTokenList).left(), + ) val useCase = getUseCase( networks = emptySet().right(), @@ -243,7 +298,9 @@ internal class GetTokenListUseCaseTest { ) // When - val result = useCase(userWalletId).first() + val result = useCase(userWalletId) + .take(count = 2) + .toList() // Then assertEquals(expectedResult, result) @@ -264,12 +321,17 @@ internal class GetTokenListUseCaseTest { @Test fun `when networks statuses flow is empty then error should be received`() = runTest { - val expectedResult = TokenListError.EmptyTokens.left() + val expectedResult = listOf( + MockTokenLists.loadingUngroupedTokenList.right(), + TokenListError.EmptyTokens.left(), + ) val useCase = getUseCase(statuses = flowOf()) // When - val result = useCase(userWalletId).first() + val result = useCase(userWalletId) + .take(count = 2) + .toList() // Then assertEquals(expectedResult, result) @@ -289,13 +351,21 @@ internal class GetTokenListUseCaseTest { } @Test - fun `when quotes flow is empty then error should be received`() = runTest { - val expectedResult = TokenListError.EmptyTokens.left() + fun `when quotes flow is empty then list without quotes should be received`() = runTest { + val expectedResult = listOf( + MockTokenLists.loadingUngroupedTokenList.right(), + MockTokenLists.noQuotesUngroupedTokenList.right(), + ) - val useCase = getUseCase(quotes = flowOf()) + val useCase = getUseCase( + statuses = flowOf(MockNetworks.verifiedNetworksStatuses.right()), + quotes = flowOf(), + ) // When - val result = useCase(userWalletId).first() + val result = useCase(userWalletId) + .take(count = 2) + .toList() // Then assertEquals(expectedResult, result) @@ -318,7 +388,7 @@ internal class GetTokenListUseCaseTest { } private fun getUseCase( - tokens: Flow>> = flowOf(MockTokens.tokens.right()), + tokens: Flow>> = flowOf(MockTokens.tokens.right()), quotes: Flow>> = flowOf(MockQuotes.quotes.right()), networks: Either> = MockNetworks.networks.right(), statuses: Flow>> = flowOf(MockNetworks.errorNetworksStatuses.right()), @@ -326,7 +396,7 @@ internal class GetTokenListUseCaseTest { isSortedByBalance: Flow> = flowOf(MockTokenLists.isSortedByBalance.right()), ) = GetTokenListUseCase( dispatchers = dispatchers, - tokensRepository = MockTokensRepository( + currenciesRepository = MockCurrenciesRepository( sortTokensResult = Unit.right(), token = MockTokens.token1.right(), tokens = tokens, diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockNetworksGroups.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockNetworksGroups.kt index ebda181faf..ab3078de1d 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockNetworksGroups.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockNetworksGroups.kt @@ -1,7 +1,7 @@ package com.tangem.domain.tokens.mock -import arrow.core.nonEmptySetOf -import arrow.core.toNonEmptySetOrNull +import arrow.core.nonEmptyListOf +import arrow.core.toNonEmptyListOrNull import com.tangem.domain.tokens.model.NetworkGroup @Suppress("MemberVisibilityCanBePrivate") @@ -11,42 +11,42 @@ internal object MockNetworksGroups { network = MockNetworks.network1, currencies = MockTokensStates.failedTokenStates .filter { it.currency.networkId == MockNetworks.network1.id } - .toNonEmptySetOrNull()!!, + .toNonEmptyListOrNull()!!, ) val networkGroup2 = NetworkGroup( network = MockNetworks.network2, currencies = MockTokensStates.failedTokenStates .filter { it.currency.networkId == MockNetworks.network2.id } - .toNonEmptySetOrNull()!!, + .toNonEmptyListOrNull()!!, ) val networkGroup3 = NetworkGroup( network = MockNetworks.network3, currencies = MockTokensStates.failedTokenStates .filter { it.currency.networkId == MockNetworks.network3.id } - .toNonEmptySetOrNull()!!, + .toNonEmptyListOrNull()!!, ) - val failedNetworksGroups = nonEmptySetOf(networkGroup1, networkGroup2, networkGroup3) + val failedNetworksGroups = nonEmptyListOf(networkGroup1, networkGroup2, networkGroup3) val loadedNetworksGroups = failedNetworksGroups.map { group -> group.copy( currencies = MockTokensStates.loadedTokensStates .filter { it.currency.networkId == group.network.id } - .toNonEmptySetOrNull()!!, + .toNonEmptyListOrNull()!!, ) - }.toNonEmptySet() + } val sortedNetworksGroups = loadedNetworksGroups.map { group -> group.copy( currencies = group.currencies .sortedByDescending { it.value.fiatAmount } - .toNonEmptySetOrNull()!!, + .toNonEmptyListOrNull()!!, ) } .sortedByDescending { group -> group.currencies.sumOf { it.value.fiatAmount!! } } - .toNonEmptySetOrNull()!! + .toNonEmptyListOrNull()!! } \ No newline at end of file diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockQuotes.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockQuotes.kt index d54da733fd..a7e128d112 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockQuotes.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockQuotes.kt @@ -1,68 +1,68 @@ package com.tangem.domain.tokens.mock import arrow.core.nonEmptySetOf -import com.tangem.domain.tokens.model.Quote +import com.tangem.domain.tokens.models.Quote import java.math.BigDecimal @Suppress("MemberVisibilityCanBePrivate") internal object MockQuotes { val quote1 = Quote( - currencyId = MockTokens.token1.id, + rawCurrencyId = MockTokens.token1.id.rawCurrencyId!!, fiatRate = BigDecimal("1.23"), priceChange = BigDecimal("0.01"), ) val quote2 = Quote( - currencyId = MockTokens.token2.id, + rawCurrencyId = MockTokens.token2.id.rawCurrencyId!!, fiatRate = BigDecimal("2.34"), priceChange = BigDecimal("-0.02"), ) val quote3 = Quote( - currencyId = MockTokens.token3.id, + rawCurrencyId = MockTokens.token3.id.rawCurrencyId!!, fiatRate = BigDecimal("3.45"), priceChange = BigDecimal("0.03"), ) val quote4 = Quote( - currencyId = MockTokens.token4.id, + rawCurrencyId = MockTokens.token4.id.rawCurrencyId!!, fiatRate = BigDecimal("4.56"), priceChange = BigDecimal("-0.04"), ) val quote5 = Quote( - currencyId = MockTokens.token5.id, + rawCurrencyId = MockTokens.token5.id.rawCurrencyId!!, fiatRate = BigDecimal("5.67"), priceChange = BigDecimal("0.05"), ) val quote6 = Quote( - currencyId = MockTokens.token6.id, + rawCurrencyId = MockTokens.token6.id.rawCurrencyId!!, fiatRate = BigDecimal("6.78"), priceChange = BigDecimal("-0.06"), ) val quote7 = Quote( - currencyId = MockTokens.token7.id, + rawCurrencyId = MockTokens.token7.id.rawCurrencyId!!, fiatRate = BigDecimal("7.89"), priceChange = BigDecimal("0.07"), ) val quote8 = Quote( - currencyId = MockTokens.token8.id, + rawCurrencyId = MockTokens.token8.id.rawCurrencyId!!, fiatRate = BigDecimal("8.90"), priceChange = BigDecimal("-0.08"), ) val quote9 = Quote( - currencyId = MockTokens.token9.id, + rawCurrencyId = MockTokens.token9.id.rawCurrencyId!!, fiatRate = BigDecimal("9.01"), priceChange = BigDecimal("0.09"), ) val quote10 = Quote( - currencyId = MockTokens.token10.id, + rawCurrencyId = MockTokens.token10.id.rawCurrencyId!!, fiatRate = BigDecimal("10.12"), priceChange = BigDecimal("-0.10"), ) diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockTokenLists.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockTokenLists.kt index 403dcb5046..cbfd65a56f 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockTokenLists.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockTokenLists.kt @@ -1,7 +1,7 @@ package com.tangem.domain.tokens.mock -import arrow.core.NonEmptySet -import arrow.core.toNonEmptySetOrNull +import arrow.core.NonEmptyList +import arrow.core.toNonEmptyListOrNull import com.tangem.domain.tokens.mock.MockNetworksGroups.failedNetworksGroups import com.tangem.domain.tokens.mock.MockNetworksGroups.loadedNetworksGroups import com.tangem.domain.tokens.mock.MockNetworksGroups.sortedNetworksGroups @@ -18,13 +18,13 @@ internal object MockTokenLists { val notInitializedTokenList = TokenList.NotInitialized val emptyGroupedTokenList = TokenList.GroupedByNetwork( - groups = emptySet(), + groups = emptyList(), totalFiatBalance = TokenList.FiatBalance.Failed, sortedBy = TokenList.SortType.NONE, ) val emptyUngroupedTokenList = TokenList.Ungrouped( - currencies = emptySet(), + currencies = emptyList(), totalFiatBalance = TokenList.FiatBalance.Failed, sortedBy = TokenList.SortType.NONE, ) @@ -41,9 +41,14 @@ internal object MockTokenLists { sortedBy = TokenList.SortType.NONE, ) + val noQuotesUngroupedTokenList = failedUngroupedTokenList.copy( + totalFiatBalance = TokenList.FiatBalance.Loaded(amount = BigDecimal.ZERO, isAllAmountsSummarized = false), + currencies = MockTokensStates.noQuotesTokensStatuses, + ) + val loadingUngroupedTokenList = with(failedUngroupedTokenList) { copy( - currencies = currencies.map { it.copy(value = CryptoCurrencyStatus.Loading) }.toNonEmptySetOrNull()!!, + currencies = currencies.map { it.copy(value = CryptoCurrencyStatus.Loading) }, totalFiatBalance = TokenList.FiatBalance.Loading, ) } @@ -54,10 +59,9 @@ internal object MockTokenLists { groups = groups.map { group -> group.copy( currencies = group.currencies - .map { it.copy(value = CryptoCurrencyStatus.Loading) } - .toNonEmptySetOrNull()!!, + .map { it.copy(value = CryptoCurrencyStatus.Loading) }, ) - }.toNonEmptySetOrNull()!!, + }.toNonEmptyListOrNull()!!, ) } @@ -84,7 +88,7 @@ internal object MockTokenLists { sortedBy = TokenList.SortType.NONE, totalFiatBalance = TokenList.FiatBalance.Loaded( amount = groups - .flatMap { it.currencies as NonEmptySet } + .flatMap { it.currencies as NonEmptyList } .sumOf { it.value.fiatAmount ?: BigDecimal.ZERO }, isAllAmountsSummarized = true, ), @@ -95,7 +99,6 @@ internal object MockTokenLists { get() { val tokens = MockTokensStates.loadedTokensStates .sortedByDescending { it.value.fiatAmount } - .toNonEmptySetOrNull()!! return unsortedUngroupedTokenList.copy( currencies = tokens, 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 b0af037c5e..6e3749a46f 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 @@ -1,12 +1,13 @@ package com.tangem.domain.tokens.mock -import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.tokens.models.CryptoCurrency +import com.tangem.domain.tokens.models.CryptoCurrency.ID internal object MockTokens { val token1 get() = CryptoCurrency.Coin( - id = CryptoCurrency.ID("token1"), + id = ID(ID.Prefix.COIN_PREFIX, MockNetworks.network1.id, ID.Suffix.RawID("token1")), networkId = MockNetworks.network1.id, name = "Token 1", symbol = "T1", @@ -16,7 +17,7 @@ internal object MockTokens { ) val token2 get() = CryptoCurrency.Token( - id = CryptoCurrency.ID("token2"), + id = ID(ID.Prefix.TOKEN_PREFIX, MockNetworks.network1.id, ID.Suffix.RawID("token2")), networkId = MockNetworks.network1.id, name = "Token 2", symbol = "T2", @@ -25,10 +26,12 @@ internal object MockTokens { iconUrl = null, contractAddress = "address", derivationPath = null, + blockchainName = "Ethereum", + standardType = CryptoCurrency.StandardType.ERC20, ) val token3 get() = CryptoCurrency.Token( - id = CryptoCurrency.ID("token3"), + id = ID(ID.Prefix.TOKEN_PREFIX, MockNetworks.network1.id, ID.Suffix.RawID("token3")), networkId = MockNetworks.network1.id, name = "Token 3", symbol = "T3", @@ -37,10 +40,12 @@ internal object MockTokens { iconUrl = null, contractAddress = "address", derivationPath = null, + blockchainName = "Ethereum", + standardType = CryptoCurrency.StandardType.ERC20, ) val token4 get() = CryptoCurrency.Coin( - id = CryptoCurrency.ID("token4"), + id = ID(ID.Prefix.COIN_PREFIX, MockNetworks.network2.id, ID.Suffix.RawID("token4")), networkId = MockNetworks.network2.id, name = "Token 4", symbol = "T4", @@ -50,7 +55,7 @@ internal object MockTokens { ) val token5 get() = CryptoCurrency.Token( - id = CryptoCurrency.ID("token5"), + id = ID(ID.Prefix.TOKEN_PREFIX, MockNetworks.network2.id, ID.Suffix.RawID("token5")), networkId = MockNetworks.network2.id, name = "Token 5", symbol = "T5", @@ -59,10 +64,12 @@ internal object MockTokens { iconUrl = null, contractAddress = "address", derivationPath = null, + blockchainName = "Ethereum", + standardType = CryptoCurrency.StandardType.ERC20, ) val token6 get() = CryptoCurrency.Token( - id = CryptoCurrency.ID("token6"), + id = ID(ID.Prefix.TOKEN_PREFIX, MockNetworks.network2.id, ID.Suffix.RawID("token6")), networkId = MockNetworks.network2.id, name = "Token 6", symbol = "T6", @@ -71,10 +78,12 @@ internal object MockTokens { iconUrl = null, contractAddress = "address", derivationPath = null, + blockchainName = "Ethereum", + standardType = CryptoCurrency.StandardType.ERC20, ) val token7 get() = CryptoCurrency.Coin( - id = CryptoCurrency.ID("token7"), + id = ID(ID.Prefix.COIN_PREFIX, MockNetworks.network3.id, ID.Suffix.RawID("token7")), networkId = MockNetworks.network3.id, name = "Token 7", symbol = "T7", @@ -84,7 +93,7 @@ internal object MockTokens { ) val token8 get() = CryptoCurrency.Token( - id = CryptoCurrency.ID("token8"), + id = ID(ID.Prefix.TOKEN_PREFIX, MockNetworks.network3.id, ID.Suffix.RawID("token8")), networkId = MockNetworks.network3.id, name = "Token 8", symbol = "T8", @@ -93,10 +102,12 @@ internal object MockTokens { iconUrl = null, contractAddress = "address", derivationPath = null, + blockchainName = "Ethereum", + standardType = CryptoCurrency.StandardType.ERC20, ) val token9 get() = CryptoCurrency.Token( - id = CryptoCurrency.ID("token9"), + id = ID(ID.Prefix.TOKEN_PREFIX, MockNetworks.network3.id, ID.Suffix.RawID("token9")), networkId = MockNetworks.network3.id, name = "Token 9", symbol = "T9", @@ -105,10 +116,12 @@ internal object MockTokens { iconUrl = null, contractAddress = "address", derivationPath = null, + blockchainName = "Ethereum", + standardType = CryptoCurrency.StandardType.ERC20, ) val token10 get() = CryptoCurrency.Token( - id = CryptoCurrency.ID("token10"), + id = ID(ID.Prefix.TOKEN_PREFIX, MockNetworks.network3.id, ID.Suffix.RawID("token10")), networkId = MockNetworks.network3.id, name = "Token 10", symbol = "T10", @@ -117,7 +130,9 @@ internal object MockTokens { iconUrl = null, contractAddress = "address", derivationPath = null, + blockchainName = "Ethereum", + standardType = CryptoCurrency.StandardType.ERC20, ) - val tokens = setOf(token1, token2, token3, token4, token5, token6, token7, token8, token9, token10) + val tokens = listOf(token1, token2, token3, token4, token5, token6, token7, token8, token9, token10) } \ No newline at end of file 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 4df0a5d525..ee6b0a6a1e 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 @@ -1,6 +1,6 @@ package com.tangem.domain.tokens.mock -import arrow.core.nonEmptySetOf +import arrow.core.nonEmptyListOf import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.model.NetworkStatus @@ -57,7 +57,7 @@ internal object MockTokensStates { value = CryptoCurrencyStatus.NoAccount, ) - val failedTokenStates = nonEmptySetOf( + val failedTokenStates = nonEmptyListOf( tokenState1, tokenState2, tokenState3, @@ -74,7 +74,7 @@ internal object MockTokensStates { val networkStatus = MockNetworks.verifiedNetworksStatuses .first { it.networkId == status.currency.networkId } val amount = (networkStatus.value as NetworkStatus.Verified).amounts[status.currency.id]!! - val quote = MockQuotes.quotes.first { it.currencyId == status.currency.id } + val quote = MockQuotes.quotes.first { it.rawCurrencyId == status.currency.id.rawCurrencyId } val fiatAmount = amount * quote.fiatRate status.copy( @@ -86,5 +86,14 @@ internal object MockTokensStates { hasTransactionsInProgress = false, ), ) - }.toNonEmptySet() + } + + val noQuotesTokensStatuses = loadedTokensStates.map { currency -> + currency.copy( + value = CryptoCurrencyStatus.NoQuote( + amount = currency.value.amount!!, + hasTransactionsInProgress = false, + ), + ) + } } \ No newline at end of file diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockTokensRepository.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockCurrenciesRepository.kt similarity index 69% rename from domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockTokensRepository.kt rename to domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockCurrenciesRepository.kt index ad5d4e0851..7ac79d2d4c 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockTokensRepository.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockCurrenciesRepository.kt @@ -3,20 +3,20 @@ package com.tangem.domain.tokens.repository import arrow.core.Either import arrow.core.getOrElse import com.tangem.domain.core.error.DataError -import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.tokens.models.CryptoCurrency import com.tangem.domain.wallets.models.UserWalletId import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map -internal class MockTokensRepository( +internal class MockCurrenciesRepository( private val sortTokensResult: Either, private val token: Either, - private val tokens: Flow>>, + private val tokens: Flow>>, private val isGrouped: Flow>, private val isSortedByBalance: Flow>, -) : TokensRepository { +) : CurrenciesRepository { - var tokensIdsAfterSortingApply: Set? = null + var tokensIdsAfterSortingApply: List? = null private set var isTokensGroupedAfterSortingApply: Boolean? = null @@ -27,7 +27,7 @@ internal class MockTokensRepository( override suspend fun saveTokens( userWalletId: UserWalletId, - currencies: Set, + currencies: List, isGroupedByNetwork: Boolean, isSortedByBalance: Boolean, ) { @@ -38,17 +38,28 @@ internal class MockTokensRepository( isTokensSortedByBalanceAfterSortingApply = isSortedByBalance } - override suspend fun getPrimaryCurrency(userWalletId: UserWalletId): CryptoCurrency { + override suspend fun getSingleCurrencyWalletPrimaryCurrency(userWalletId: UserWalletId): CryptoCurrency { return token.getOrElse { e -> throw e } } override fun getMultiCurrencyWalletCurrencies( userWalletId: UserWalletId, refresh: Boolean, - ): Flow> { + ): Flow> { return tokens.map { it.getOrElse { e -> throw e } } } + override suspend fun getMultiCurrencyWalletCurrency( + userWalletId: UserWalletId, + id: CryptoCurrency.ID, + ): CryptoCurrency { + val token = token.getOrElse { e -> throw e } + + require(token.id == id) + + return token + } + override fun isTokensGrouped(userWalletId: UserWalletId): Flow { return isGrouped.map { it.getOrElse { e -> throw e } } } 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 ced27621b7..5ac5c7c8fa 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 @@ -3,7 +3,6 @@ package com.tangem.domain.tokens.repository import arrow.core.Either import arrow.core.getOrElse import com.tangem.domain.core.error.DataError -import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.NetworkStatus import com.tangem.domain.tokens.models.Network import com.tangem.domain.wallets.models.UserWalletId @@ -21,7 +20,7 @@ internal class MockNetworksRepository( override fun getNetworkStatuses( userWalletId: UserWalletId, - networks: Map>, + networks: Set, refresh: Boolean, ): Flow> { return statuses.map { it.getOrElse { e -> throw e } } diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockQuotesRepository.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockQuotesRepository.kt index 9ec660b113..baa2ef1599 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockQuotesRepository.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockQuotesRepository.kt @@ -3,8 +3,8 @@ package com.tangem.domain.tokens.repository import arrow.core.Either import arrow.core.getOrElse import com.tangem.domain.core.error.DataError -import com.tangem.domain.tokens.model.CryptoCurrency -import com.tangem.domain.tokens.model.Quote +import com.tangem.domain.tokens.models.CryptoCurrency +import com.tangem.domain.tokens.models.Quote import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map @@ -12,7 +12,7 @@ internal class MockQuotesRepository( private val quotes: Flow>>, ) : QuotesRepository { - override fun getQuotes(tokensIds: Set, refresh: Boolean): Flow> { + override fun getQuotes(currenciesIds: Set, refresh: Boolean): Flow> { return quotes.map { it.getOrElse { e -> throw e } } } } \ No newline at end of file diff --git a/domain/txhistory/build.gradle.kts b/domain/txhistory/build.gradle.kts index 1237e8f782..2ba0547f59 100644 --- a/domain/txhistory/build.gradle.kts +++ b/domain/txhistory/build.gradle.kts @@ -14,4 +14,6 @@ dependencies { implementation(deps.androidx.paging.runtime) implementation(projects.core.utils) + implementation(projects.domain.tokens.models) + implementation(projects.domain.txhistory.models) } \ No newline at end of file diff --git a/domain/txhistory/models/.gitignore b/domain/txhistory/models/.gitignore new file mode 100644 index 0000000000..796b96d1c4 --- /dev/null +++ b/domain/txhistory/models/.gitignore @@ -0,0 +1 @@ +/build diff --git a/domain/txhistory/models/build.gradle.kts b/domain/txhistory/models/build.gradle.kts new file mode 100644 index 0000000000..7ff7fb7522 --- /dev/null +++ b/domain/txhistory/models/build.gradle.kts @@ -0,0 +1,4 @@ +plugins { + alias(deps.plugins.kotlin.jvm) + id("configuration") +} \ No newline at end of file diff --git a/domain/txhistory/models/src/main/kotlin/com/tangem/domain/txhistory/models/PaginationWrapper.kt b/domain/txhistory/models/src/main/kotlin/com/tangem/domain/txhistory/models/PaginationWrapper.kt new file mode 100644 index 0000000000..92ea34de36 --- /dev/null +++ b/domain/txhistory/models/src/main/kotlin/com/tangem/domain/txhistory/models/PaginationWrapper.kt @@ -0,0 +1,8 @@ +package com.tangem.domain.txhistory.models + +data class PaginationWrapper( + val page: Int, + val totalPages: Int, + val itemsOnPage: Int, + val items: List, +) \ No newline at end of file diff --git a/domain/txhistory/src/main/kotlin/com/tangem/domain/txhistory/model/TxHistoryItem.kt b/domain/txhistory/models/src/main/kotlin/com/tangem/domain/txhistory/models/TxHistoryItem.kt similarity index 87% rename from domain/txhistory/src/main/kotlin/com/tangem/domain/txhistory/model/TxHistoryItem.kt rename to domain/txhistory/models/src/main/kotlin/com/tangem/domain/txhistory/models/TxHistoryItem.kt index 41d5c76d8e..447a9395ff 100644 --- a/domain/txhistory/src/main/kotlin/com/tangem/domain/txhistory/model/TxHistoryItem.kt +++ b/domain/txhistory/models/src/main/kotlin/com/tangem/domain/txhistory/models/TxHistoryItem.kt @@ -1,10 +1,10 @@ -package com.tangem.domain.txhistory.model +package com.tangem.domain.txhistory.models import java.math.BigDecimal data class TxHistoryItem( val txHash: String, - val timestamp: Long, + val timestampInMillis: Long, val direction: TransactionDirection, val status: TxStatus, val type: TransactionType, diff --git a/domain/txhistory/src/main/kotlin/com/tangem/domain/txhistory/error/TxHistoryListError.kt b/domain/txhistory/models/src/main/kotlin/com/tangem/domain/txhistory/models/TxHistoryListError.kt similarity index 75% rename from domain/txhistory/src/main/kotlin/com/tangem/domain/txhistory/error/TxHistoryListError.kt rename to domain/txhistory/models/src/main/kotlin/com/tangem/domain/txhistory/models/TxHistoryListError.kt index 1538b27e0b..618980c881 100644 --- a/domain/txhistory/src/main/kotlin/com/tangem/domain/txhistory/error/TxHistoryListError.kt +++ b/domain/txhistory/models/src/main/kotlin/com/tangem/domain/txhistory/models/TxHistoryListError.kt @@ -1,4 +1,4 @@ -package com.tangem.domain.txhistory.error +package com.tangem.domain.txhistory.models sealed class TxHistoryListError : Throwable() { data class DataError(override val cause: Throwable) : TxHistoryListError() diff --git a/domain/txhistory/models/src/main/kotlin/com/tangem/domain/txhistory/models/TxHistoryState.kt b/domain/txhistory/models/src/main/kotlin/com/tangem/domain/txhistory/models/TxHistoryState.kt new file mode 100644 index 0000000000..0436102e66 --- /dev/null +++ b/domain/txhistory/models/src/main/kotlin/com/tangem/domain/txhistory/models/TxHistoryState.kt @@ -0,0 +1,15 @@ +package com.tangem.domain.txhistory.models + +sealed class TxHistoryState { + + sealed class Success : TxHistoryState() { + object Empty : Success() + data class HasTransactions(val txCount: Int) : Success() + } + + sealed class Failed : TxHistoryState() { + data class FetchError(val exception: Exception) : Failed() + } + + object NotImplemented : TxHistoryState() +} \ No newline at end of file diff --git a/domain/txhistory/src/main/kotlin/com/tangem/domain/txhistory/error/TxHistoryStateError.kt b/domain/txhistory/models/src/main/kotlin/com/tangem/domain/txhistory/models/TxHistoryStateError.kt similarity index 85% rename from domain/txhistory/src/main/kotlin/com/tangem/domain/txhistory/error/TxHistoryStateError.kt rename to domain/txhistory/models/src/main/kotlin/com/tangem/domain/txhistory/models/TxHistoryStateError.kt index 3f93a3cf12..1b5f07333c 100644 --- a/domain/txhistory/src/main/kotlin/com/tangem/domain/txhistory/error/TxHistoryStateError.kt +++ b/domain/txhistory/models/src/main/kotlin/com/tangem/domain/txhistory/models/TxHistoryStateError.kt @@ -1,4 +1,4 @@ -package com.tangem.domain.txhistory.error +package com.tangem.domain.txhistory.models sealed class TxHistoryStateError : Throwable() { 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 1ebb8ae1dd..8ff4c05cff 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 @@ -1,16 +1,21 @@ package com.tangem.domain.txhistory.repository import androidx.paging.PagingData -import com.tangem.domain.txhistory.error.TxHistoryListError -import com.tangem.domain.txhistory.error.TxHistoryStateError -import com.tangem.domain.txhistory.model.TxHistoryItem +import com.tangem.domain.tokens.models.Network +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: String, derivationPath: String): Int + suspend fun getTxHistoryItemsCount(networkId: Network.ID, derivationPath: String?): Int @Throws(TxHistoryListError::class) - fun getTxHistoryItems(networkId: String, pageSize: Int): Flow> + fun getTxHistoryItems( + networkId: Network.ID, + derivationPath: String?, + 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 1ecdc72dd6..172c8c2b3f 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 @@ -3,12 +3,13 @@ package com.tangem.domain.txhistory.usecase import arrow.core.Either import arrow.core.raise.catch import arrow.core.raise.either -import com.tangem.domain.txhistory.error.TxHistoryStateError +import com.tangem.domain.tokens.models.Network +import com.tangem.domain.txhistory.models.TxHistoryStateError import com.tangem.domain.txhistory.repository.TxHistoryRepository class GetTxHistoryItemsCountUseCase(private val repository: TxHistoryRepository) { - suspend operator fun invoke(networkId: String, derivationPath: String): Either { + suspend operator fun invoke(networkId: Network.ID, derivationPath: String?): Either { return either { catch( block = { repository.getTxHistoryItemsCount(networkId, derivationPath) }, 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 9f3fc2fbe0..38fbf4b87e 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 @@ -3,8 +3,9 @@ package com.tangem.domain.txhistory.usecase import androidx.paging.PagingData import arrow.core.Either import arrow.core.raise.either -import com.tangem.domain.txhistory.error.TxHistoryListError -import com.tangem.domain.txhistory.model.TxHistoryItem +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.repository.TxHistoryRepository import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.catch @@ -14,12 +15,13 @@ private const val DEFAULT_PAGE_SIZE = 20 class GetTxHistoryItemsUseCase(private val repository: TxHistoryRepository) { operator fun invoke( - networkId: String, + networkId: Network.ID, + derivationPath: String?, pageSize: Int = DEFAULT_PAGE_SIZE, ): Either>> { return either { repository - .getTxHistoryItems(networkId = networkId, pageSize = pageSize) + .getTxHistoryItems(networkId = networkId, derivationPath = derivationPath, pageSize = pageSize) .catch { raise(TxHistoryListError.DataError(it)) } } } diff --git a/domain/wallets/build.gradle.kts b/domain/wallets/build.gradle.kts index 3216145e52..474b10e64e 100644 --- a/domain/wallets/build.gradle.kts +++ b/domain/wallets/build.gradle.kts @@ -10,6 +10,10 @@ android { dependencies { + // region Core modules + implementation(projects.core.res) + // endregion + // region Domain modules implementation(projects.domain.legacy) implementation(projects.domain.models) diff --git a/app/src/main/java/com/tangem/tap/domain/userWalletList/UserWalletsListError.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/legacy/UserWalletsListError.kt similarity index 94% rename from app/src/main/java/com/tangem/tap/domain/userWalletList/UserWalletsListError.kt rename to domain/wallets/src/main/java/com/tangem/domain/wallets/legacy/UserWalletsListError.kt index ec9ed95b1d..0bb0ef990b 100644 --- a/app/src/main/java/com/tangem/tap/domain/userWalletList/UserWalletsListError.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/legacy/UserWalletsListError.kt @@ -1,7 +1,7 @@ -package com.tangem.tap.domain.userWalletList +package com.tangem.domain.wallets.legacy import com.tangem.common.core.TangemError -import com.tangem.wallet.R +import com.tangem.domain.wallets.R sealed class UserWalletsListError(code: Int) : TangemError(code) { diff --git a/app/src/main/java/com/tangem/tap/domain/userWalletList/UserWalletsListManagerExtensions.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/legacy/UserWalletsListManagerExtensions.kt similarity index 95% rename from app/src/main/java/com/tangem/tap/domain/userWalletList/UserWalletsListManagerExtensions.kt rename to domain/wallets/src/main/java/com/tangem/domain/wallets/legacy/UserWalletsListManagerExtensions.kt index 8086bded7c..aa65450d99 100644 --- a/app/src/main/java/com/tangem/tap/domain/userWalletList/UserWalletsListManagerExtensions.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/legacy/UserWalletsListManagerExtensions.kt @@ -1,7 +1,6 @@ -package com.tangem.tap.domain.userWalletList +package com.tangem.domain.wallets.legacy import com.tangem.common.CompletionResult -import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.wallets.models.UserWallet import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flowOf diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/models/GetSelectedWalletError.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/models/GetSelectedWalletError.kt new file mode 100644 index 0000000000..ad0867fa6e --- /dev/null +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/models/GetSelectedWalletError.kt @@ -0,0 +1,8 @@ +package com.tangem.domain.wallets.models + +sealed interface GetSelectedWalletError { + + object DataError : GetSelectedWalletError + + object NoUserWalletSelected : GetSelectedWalletError +} \ No newline at end of file diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/models/SaveWalletError.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/models/SaveWalletError.kt index 97ff31ef11..f292875ed2 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/models/SaveWalletError.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/models/SaveWalletError.kt @@ -5,6 +5,5 @@ package com.tangem.domain.wallets.models */ sealed interface SaveWalletError { - // TODO: Finalize in next PRs object CommonError : SaveWalletError } \ No newline at end of file diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/models/SelectWalletError.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/models/SelectWalletError.kt new file mode 100644 index 0000000000..a357f06ca9 --- /dev/null +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/models/SelectWalletError.kt @@ -0,0 +1,8 @@ +package com.tangem.domain.wallets.models + +sealed interface SelectWalletError { + + object DataError : SelectWalletError + + object UnableToSelectUserWallet : SelectWalletError +} \ No newline at end of file diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/models/UnlockWalletError.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/models/UnlockWalletError.kt new file mode 100644 index 0000000000..311cdc5d0f --- /dev/null +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/models/UnlockWalletError.kt @@ -0,0 +1,6 @@ +package com.tangem.domain.wallets.models + +sealed interface UnlockWalletError { + + object CommonError : UnlockWalletError +} \ 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 new file mode 100644 index 0000000000..89d6fe1405 --- /dev/null +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetSelectedWalletUseCase.kt @@ -0,0 +1,32 @@ +package com.tangem.domain.wallets.usecase + +import arrow.core.Either +import arrow.core.raise.either +import arrow.core.raise.ensureNotNull +import com.tangem.domain.wallets.legacy.WalletsStateHolder +import com.tangem.domain.wallets.models.GetSelectedWalletError +import com.tangem.domain.wallets.models.UserWallet + +/** + * Use case for getting selected wallet + * + * @property walletsStateHolder state holder for getting static initialized 'userWalletsListManager' + * +[REDACTED_AUTHOR] + */ +class GetSelectedWalletUseCase(private val walletsStateHolder: WalletsStateHolder) { + + operator fun invoke(): Either { + return either { + val userWalletsListManager = ensureNotNull( + value = walletsStateHolder.userWalletsListManager, + raise = { GetSelectedWalletError.DataError }, + ) + + ensureNotNull( + value = userWalletsListManager.selectedUserWalletSync, + raise = { GetSelectedWalletError.NoUserWalletSelected }, + ) + } + } +} \ No newline at end of file diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/SelectWalletUseCase.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/SelectWalletUseCase.kt new file mode 100644 index 0000000000..aa16ad812b --- /dev/null +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/SelectWalletUseCase.kt @@ -0,0 +1,37 @@ +package com.tangem.domain.wallets.usecase + +import arrow.core.Either +import arrow.core.left +import arrow.core.raise.either +import arrow.core.raise.ensureNotNull +import arrow.core.right +import com.tangem.common.doOnFailure +import com.tangem.common.doOnSuccess +import com.tangem.domain.wallets.legacy.WalletsStateHolder +import com.tangem.domain.wallets.models.SelectWalletError +import com.tangem.domain.wallets.models.UserWalletId + +/** + * Use case for selecting wallet + * + * @property walletsStateHolder state holder for getting static initialized 'userWalletsListManager' + * +[REDACTED_AUTHOR] + */ +class SelectWalletUseCase(private val walletsStateHolder: WalletsStateHolder) { + + suspend operator fun invoke(userWalletId: UserWalletId): Either { + return either { + val userWalletsListManager = ensureNotNull( + value = walletsStateHolder.userWalletsListManager, + raise = { SelectWalletError.DataError }, + ) + + userWalletsListManager.select(userWalletId) + .doOnSuccess { return Unit.right() } + .doOnFailure { return SelectWalletError.UnableToSelectUserWallet.left() } + + return Unit.right() + } + } +} \ No newline at end of file diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/UnlockWalletsUseCase.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/UnlockWalletsUseCase.kt new file mode 100644 index 0000000000..3459902720 --- /dev/null +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/UnlockWalletsUseCase.kt @@ -0,0 +1,31 @@ +package com.tangem.domain.wallets.usecase + +import arrow.core.Either +import arrow.core.left +import arrow.core.right +import com.tangem.common.doOnFailure +import com.tangem.common.doOnSuccess +import com.tangem.domain.wallets.legacy.WalletsStateHolder +import com.tangem.domain.wallets.legacy.asLockable +import com.tangem.domain.wallets.models.UnlockWalletError + +/** + * Unlock wallets use case + * + * @property walletsStateHolder wallets state holder + * +[REDACTED_AUTHOR] + */ +class UnlockWalletsUseCase(private val walletsStateHolder: WalletsStateHolder) { + + suspend operator fun invoke(): Either { + val userWalletsListManager = walletsStateHolder.userWalletsListManager?.asLockable() + ?: return UnlockWalletError.CommonError.left() + + userWalletsListManager.unlock() + .doOnSuccess { return Unit.right() } + .doOnFailure { return UnlockWalletError.CommonError.left() } + + return Unit.right() + } +} \ No newline at end of file diff --git a/features/learn2earn/impl/build.gradle.kts b/features/learn2earn/impl/build.gradle.kts index d85bd7dfba..b9fb7f48a6 100644 --- a/features/learn2earn/impl/build.gradle.kts +++ b/features/learn2earn/impl/build.gradle.kts @@ -19,6 +19,7 @@ dependencies { implementation(project(":core:ui")) implementation(project(":core:res")) implementation(project(":data:source:preferences")) + implementation(project(":data:common")) implementation(project(":libs:auth")) implementation(project(":libs:crypto")) diff --git a/features/learn2earn/impl/src/main/java/com/tangem/feature/learn2earn/domain/DefaultLearn2earnInteractor.kt b/features/learn2earn/impl/src/main/java/com/tangem/feature/learn2earn/domain/DefaultLearn2earnInteractor.kt index e7988b5050..59aa4de658 100644 --- a/features/learn2earn/impl/src/main/java/com/tangem/feature/learn2earn/domain/DefaultLearn2earnInteractor.kt +++ b/features/learn2earn/impl/src/main/java/com/tangem/feature/learn2earn/domain/DefaultLearn2earnInteractor.kt @@ -1,7 +1,9 @@ package com.tangem.feature.learn2earn.domain import android.net.Uri +import com.tangem.common.Provider import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.data.common.locale.LocaleProvider import com.tangem.datasource.api.promotion.models.PromotionInfoResponse import com.tangem.datasource.demo.DemoModeDatasource import com.tangem.feature.learn2earn.analytics.AnalyticsParam @@ -35,6 +37,7 @@ internal class DefaultLearn2earnInteractor( private val repository: Learn2earnRepository, private val userWalletManager: UserWalletManager, private val derivationManager: DerivationManager, + private val localeProvider: LocaleProvider, private val analytics: AnalyticsEventHandler, private val demoModeDatasource: DemoModeDatasource, private val dependencyProvider: Learn2earnDependencyProvider, @@ -57,8 +60,8 @@ internal class DefaultLearn2earnInteractor( private val webViewUriBuilder: WebViewUriBuilder by lazy { WebViewUriBuilder( authCredentialsProvider = dependencyProvider.getWebViewAuthCredentialsProvider(), - localeLanguageProvider = dependencyProvider.getLocaleProvider(), - promoCodeProvider = { promoUserData.promoCode }, + localeProvider = localeProvider, + promoCodeProvider = Provider { promoUserData.promoCode }, ) } diff --git a/features/learn2earn/impl/src/main/java/com/tangem/feature/learn2earn/domain/WebViewUriBuilder.kt b/features/learn2earn/impl/src/main/java/com/tangem/feature/learn2earn/domain/WebViewUriBuilder.kt index ac763f5a52..22b06daeb9 100644 --- a/features/learn2earn/impl/src/main/java/com/tangem/feature/learn2earn/domain/WebViewUriBuilder.kt +++ b/features/learn2earn/impl/src/main/java/com/tangem/feature/learn2earn/domain/WebViewUriBuilder.kt @@ -1,15 +1,17 @@ package com.tangem.feature.learn2earn.domain import android.net.Uri +import com.tangem.common.Provider +import com.tangem.data.common.locale.LocaleProvider import com.tangem.feature.learn2earn.impl.BuildConfig /** [REDACTED_AUTHOR] */ internal class WebViewUriBuilder( - private val authCredentialsProvider: () -> String?, - private val localeLanguageProvider: () -> String, - private val promoCodeProvider: () -> String?, + private val authCredentialsProvider: Provider, + private val localeProvider: LocaleProvider, + private val promoCodeProvider: Provider, ) { fun buildUriForNewUser(learningIsFinished: Boolean): Uri { @@ -46,20 +48,11 @@ internal class WebViewUriBuilder( } else { authority(BASE_URL) } - appendPath(getLocaleLanguage(localeLanguageProvider.invoke())) + appendPath(localeProvider.getWebUriLocaleLanguage()) appendPath(PATH_PROMOTION) appendQueryParameter(QUERY_FINISHED, learningIsFinished.toString()) } - // TODO: locale: This can be used by another feature. Move it to the appropriate location - private fun getLocaleLanguage(language: String): String { - return if (LOCALE_LANG_RU.equals(language, true) || LOCALE_LANG_BY.equals(language, true)) { - LOCALE_LANG_RU - } else { - LOCALE_LANG_EN - } - } - private companion object { const val SCHEME = "https" @@ -71,9 +64,5 @@ internal class WebViewUriBuilder( const val QUERY_FINISHED = "finished" const val DEV_BASE_URL = "devweb.tangem.com" - - const val LOCALE_LANG_RU = "ru" - const val LOCALE_LANG_BY = "by" - const val LOCALE_LANG_EN = "en" } } \ No newline at end of file diff --git a/features/learn2earn/impl/src/main/java/com/tangem/feature/learn2earn/domain/api/Learn2earnDependencyProvider.kt b/features/learn2earn/impl/src/main/java/com/tangem/feature/learn2earn/domain/api/Learn2earnDependencyProvider.kt index 60a23a88fb..946d065952 100644 --- a/features/learn2earn/impl/src/main/java/com/tangem/feature/learn2earn/domain/api/Learn2earnDependencyProvider.kt +++ b/features/learn2earn/impl/src/main/java/com/tangem/feature/learn2earn/domain/api/Learn2earnDependencyProvider.kt @@ -1,5 +1,6 @@ package com.tangem.feature.learn2earn.domain.api +import com.tangem.common.Provider import com.tangem.domain.common.CardTypesResolver import kotlinx.coroutines.flow.Flow @@ -12,7 +13,5 @@ interface Learn2earnDependencyProvider { fun getCardTypeResolverFlow(): Flow - fun getLocaleProvider(): () -> String - - fun getWebViewAuthCredentialsProvider(): () -> String? + fun getWebViewAuthCredentialsProvider(): Provider } \ No newline at end of file diff --git a/features/learn2earn/impl/src/main/java/com/tangem/feature/learn2earn/domain/di/Learn2earnDomainModule.kt b/features/learn2earn/impl/src/main/java/com/tangem/feature/learn2earn/domain/di/Learn2earnDomainModule.kt index ae96ed3668..559deecff7 100644 --- a/features/learn2earn/impl/src/main/java/com/tangem/feature/learn2earn/domain/di/Learn2earnDomainModule.kt +++ b/features/learn2earn/impl/src/main/java/com/tangem/feature/learn2earn/domain/di/Learn2earnDomainModule.kt @@ -3,6 +3,7 @@ package com.tangem.feature.learn2earn.domain.di import android.content.Context import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.featuretoggle.manager.FeatureTogglesManager +import com.tangem.data.common.locale.LocaleProvider import com.tangem.datasource.demo.DemoModeDatasource import com.tangem.feature.learn2earn.data.api.Learn2earnRepository import com.tangem.feature.learn2earn.data.toggles.DefaultLearn2earnFeatureToggleManager @@ -45,8 +46,9 @@ internal class Learn2earnDomainModule { repository: Learn2earnRepository, dependencyProvider: Learn2earnDependencyProvider, userWalletManager: UserWalletManager, - analyticsEventHandler: AnalyticsEventHandler, derivationManager: DerivationManager, + localeProvider: LocaleProvider, + analyticsEventHandler: AnalyticsEventHandler, demoModeDatasource: DemoModeDatasource, dispatchers: AppCoroutineDispatcherProvider, ): Learn2earnInteractor { @@ -55,6 +57,7 @@ internal class Learn2earnDomainModule { repository = repository, userWalletManager = userWalletManager, derivationManager = derivationManager, + localeProvider = localeProvider, analytics = analyticsEventHandler, demoModeDatasource = demoModeDatasource, dependencyProvider = dependencyProvider, diff --git a/features/learn2earn/impl/src/main/java/com/tangem/feature/learn2earn/presentation/ui/Learn2earnDialogs.kt b/features/learn2earn/impl/src/main/java/com/tangem/feature/learn2earn/presentation/ui/Learn2earnDialogs.kt index 3c855863ac..58c4ac2cd1 100644 --- a/features/learn2earn/impl/src/main/java/com/tangem/feature/learn2earn/presentation/ui/Learn2earnDialogs.kt +++ b/features/learn2earn/impl/src/main/java/com/tangem/feature/learn2earn/presentation/ui/Learn2earnDialogs.kt @@ -16,7 +16,8 @@ import com.tangem.feature.learn2earn.presentation.ui.state.MainScreenState internal fun Learn2earnDialogs(dialog: MainScreenState.Dialog?) { when (dialog) { is MainScreenState.Dialog.Claimed -> ClaimedDialog(dialog) - is MainScreenState.Dialog.PromoCodeNotRegistered -> PromoCodeNotRegisteredDialog(dialog) + // TODO: https://tangem.slack.com/archives/C01HARKDLQ0/p1691421861756069 + // is MainScreenState.Dialog.PromoCodeNotRegistered -> PromoCodeNotRegisteredDialog(dialog) is MainScreenState.Dialog.Error -> ErrorDialog(dialog) else -> Unit } @@ -41,30 +42,30 @@ private fun ClaimedDialog(dialog: MainScreenState.Dialog.Claimed) { ) } -@Composable -private fun PromoCodeNotRegisteredDialog(dialog: MainScreenState.Dialog.PromoCodeNotRegistered) { - AlertDialog( - title = { - Text(text = stringResource(id = R.string.common_error)) - }, - text = { - Text(text = stringResource(id = R.string.main_promotion_no_purchase)) - }, - dismissButton = { - TextButton( - text = stringResource(id = R.string.common_cancel), - onClick = dialog.onCancel, - ) - }, - confirmButton = { - TextButton( - text = stringResource(id = R.string.common_buy), - onClick = dialog.onOk, - ) - }, - onDismissRequest = dialog.onDismissRequest, - ) -} +// @Composable +// private fun PromoCodeNotRegisteredDialog(dialog: MainScreenState.Dialog.PromoCodeNotRegistered) { +// AlertDialog( +// title = { +// Text(text = stringResource(id = R.string.common_error)) +// }, +// text = { +// Text(text = stringResource(id = R.string.main_promotion_no_purchase)) +// }, +// dismissButton = { +// TextButton( +// text = stringResource(id = R.string.common_cancel), +// onClick = dialog.onCancel, +// ) +// }, +// confirmButton = { +// TextButton( +// text = stringResource(id = R.string.common_buy), +// onClick = dialog.onOk, +// ) +// }, +// onDismissRequest = dialog.onDismissRequest, +// ) +// } @Composable private fun ErrorDialog(dialog: MainScreenState.Dialog.Error) { diff --git a/features/onboarding/build.gradle.kts b/features/onboarding/build.gradle.kts index cdd735f59c..8318108615 100644 --- a/features/onboarding/build.gradle.kts +++ b/features/onboarding/build.gradle.kts @@ -18,6 +18,7 @@ dependencies { implementation(project(":core:utils")) implementation(project(":core:ui")) implementation(project(":core:res")) + implementation(project(":data:common")) /** Tangem libraries */ implementation(deps.tangem.card.core) diff --git a/features/onboarding/src/main/java/com/tangem/feature/onboarding/domain/DefaultSeedPhraseInteractor.kt b/features/onboarding/src/main/java/com/tangem/feature/onboarding/domain/DefaultSeedPhraseInteractor.kt index a63b4bccea..92ed7ae69c 100644 --- a/features/onboarding/src/main/java/com/tangem/feature/onboarding/domain/DefaultSeedPhraseInteractor.kt +++ b/features/onboarding/src/main/java/com/tangem/feature/onboarding/domain/DefaultSeedPhraseInteractor.kt @@ -4,6 +4,7 @@ import com.tangem.common.core.TangemSdkError import com.tangem.crypto.bip39.Mnemonic import com.tangem.crypto.bip39.MnemonicErrorResult import com.tangem.feature.onboarding.data.MnemonicRepository +import com.tangem.feature.onboarding.presentation.wallet2.model.SeedPhraseField import com.tangem.utils.extensions.isNotWhitespace import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf @@ -36,8 +37,8 @@ internal class DefaultSeedPhraseInteractor constructor( } } - override suspend fun isWordMatch(word: String): Boolean { - return repository.getWordsDictionary().contains(word) + override suspend fun isWordMatch(mnemonicComponents: List?, field: SeedPhraseField, word: String): Boolean { + return mnemonicComponents?.get(field.index) == word } override suspend fun validateMnemonicString(text: String): Result> { @@ -111,4 +112,5 @@ private fun MnemonicErrorResult.mapToError(): SeedPhraseError = when (this) { MnemonicErrorResult.NormalizationFailed -> SeedPhraseError.NormalizationFailed MnemonicErrorResult.UnsupportedLanguage -> SeedPhraseError.UnsupportedLanguage is MnemonicErrorResult.InvalidWords -> SeedPhraseError.InvalidWords(this.words) + MnemonicErrorResult.InvalidMnemonic -> SeedPhraseError.InvalidMnemonic } \ No newline at end of file diff --git a/features/onboarding/src/main/java/com/tangem/feature/onboarding/domain/OnboardingModuleError.kt b/features/onboarding/src/main/java/com/tangem/feature/onboarding/domain/OnboardingModuleError.kt index 60a22f0fec..e36bc935bd 100644 --- a/features/onboarding/src/main/java/com/tangem/feature/onboarding/domain/OnboardingModuleError.kt +++ b/features/onboarding/src/main/java/com/tangem/feature/onboarding/domain/OnboardingModuleError.kt @@ -37,4 +37,5 @@ sealed class SeedPhraseError( object NormalizationFailed : SeedPhraseError(subCode = 5) object UnsupportedLanguage : SeedPhraseError(subCode = 6) data class InvalidWords(val words: Set) : SeedPhraseError(subCode = 7) + object InvalidMnemonic : SeedPhraseError(subCode = 8) } \ No newline at end of file diff --git a/features/onboarding/src/main/java/com/tangem/feature/onboarding/domain/SeedPhraseInteractor.kt b/features/onboarding/src/main/java/com/tangem/feature/onboarding/domain/SeedPhraseInteractor.kt index 2c0f319c19..d9420a9def 100644 --- a/features/onboarding/src/main/java/com/tangem/feature/onboarding/domain/SeedPhraseInteractor.kt +++ b/features/onboarding/src/main/java/com/tangem/feature/onboarding/domain/SeedPhraseInteractor.kt @@ -1,6 +1,7 @@ package com.tangem.feature.onboarding.domain import com.tangem.crypto.bip39.Mnemonic +import com.tangem.feature.onboarding.presentation.wallet2.model.SeedPhraseField import kotlinx.collections.immutable.ImmutableList /** @@ -9,7 +10,7 @@ import kotlinx.collections.immutable.ImmutableList interface SeedPhraseInteractor { suspend fun generateMnemonic(): Result suspend fun getMnemonicComponents(): Result> - suspend fun isWordMatch(word: String): Boolean + suspend fun isWordMatch(mnemonicComponents: List?, field: SeedPhraseField, word: String): Boolean suspend fun validateMnemonicString(text: String): Result> suspend fun getSuggestions(text: String, hasSelection: Boolean, cursorPosition: Int): ImmutableList suspend fun insertSuggestionWord(text: String, suggestion: String, cursorPosition: Int): InsertSuggestionResult diff --git a/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/model/UiActions.kt b/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/model/UiActions.kt index 5a63039118..dec0e444b9 100644 --- a/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/model/UiActions.kt +++ b/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/model/UiActions.kt @@ -50,8 +50,8 @@ data class TextFieldUiAction( -enum class SeedPhraseField { - Second, - Seventh, - Eleventh, +enum class SeedPhraseField(val index: Int) { + Second(index = 1), + Seventh(index = 6), + Eleventh(index = 10), } \ No newline at end of file diff --git a/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/viewmodel/SeedPhraseRouter.kt b/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/viewmodel/SeedPhraseRouter.kt index 9067f108cd..bfe8b2da3e 100644 --- a/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/viewmodel/SeedPhraseRouter.kt +++ b/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/viewmodel/SeedPhraseRouter.kt @@ -1,9 +1,6 @@ package com.tangem.feature.onboarding.presentation.wallet2.viewmodel import android.net.Uri -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.setValue import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -21,20 +18,17 @@ class SeedPhraseRouter( val currentScreen: StateFlow get() = _currentScreen - var currentScreenState by mutableStateOf(SeedPhraseScreen.Intro) - private set - fun navigateBack() { _currentScreen.value = when (_currentScreen.value) { - SeedPhraseScreen.Intro -> { + SeedPhraseScreen.CheckSeedPhrase -> SeedPhraseScreen.YourSeedPhrase + SeedPhraseScreen.Intro, + SeedPhraseScreen.AboutSeedPhrase, + SeedPhraseScreen.YourSeedPhrase, + SeedPhraseScreen.ImportSeedPhrase, + -> { onBack.invoke() return } - - SeedPhraseScreen.AboutSeedPhrase -> SeedPhraseScreen.Intro - SeedPhraseScreen.YourSeedPhrase -> SeedPhraseScreen.AboutSeedPhrase - SeedPhraseScreen.CheckSeedPhrase -> SeedPhraseScreen.YourSeedPhrase - SeedPhraseScreen.ImportSeedPhrase -> SeedPhraseScreen.AboutSeedPhrase } } diff --git a/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/viewmodel/SeedPhraseViewModel.kt b/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/viewmodel/SeedPhraseViewModel.kt index c7d9c234ba..204ae54069 100644 --- a/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/viewmodel/SeedPhraseViewModel.kt +++ b/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/viewmodel/SeedPhraseViewModel.kt @@ -9,6 +9,7 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.tangem.common.CompletionResult import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.data.common.locale.LocaleProvider import com.tangem.feature.onboarding.data.model.CreateWalletResponse import com.tangem.feature.onboarding.domain.SeedPhraseError import com.tangem.feature.onboarding.domain.SeedPhraseInteractor @@ -35,6 +36,7 @@ import javax.inject.Inject @HiltViewModel class SeedPhraseViewModel @Inject constructor( private val interactor: SeedPhraseInteractor, + private val localeProvider: LocaleProvider, private val dispatchers: CoroutineDispatcherProvider, private val analyticsEventHandler: AnalyticsEventHandler, ) : ViewModel() { @@ -161,12 +163,15 @@ class SeedPhraseViewModel @Inject constructor( val fieldState = field.getState(uiState) if (fieldState.textFieldValue.text.isEmpty()) { - updateUi { uiBuilder.checkSeedPhrase.updateTextFieldError(uiState, field, hasError = false) } + updateUi { + val mediate = uiBuilder.checkSeedPhrase.updateTextFieldError(uiState, field, hasError = false) + uiBuilder.checkSeedPhrase.updateCreateWalletButton(mediate, enabled = false) + } return@launchSingle } createOrGetDebouncer(field.name).debounce(viewModelScope, context = dispatchers.io) { - val hasError = !interactor.isWordMatch(textFieldValue.text) + val hasError = !interactor.isWordMatch(generatedMnemonicComponents, field, textFieldValue.text) if (fieldState.isError != hasError) { updateUi { uiBuilder.checkSeedPhrase.updateTextFieldError(uiState, field, hasError) } } @@ -278,7 +283,6 @@ class SeedPhraseViewModel @Inject constructor( is CompletionResult.Success -> { isFinished = true } - is CompletionResult.Failure -> { // errors shows on the TangemSdk bottom sheet dialog } @@ -301,7 +305,14 @@ class SeedPhraseViewModel @Inject constructor( private fun buttonReadMoreAboutSeedPhraseClick() { analyticsEventHandler.send(SeedPhraseEvents.ButtonReadMore) - router.openUri(URI_ABOUT_SEED_PHRASE) + val webUri = Uri.Builder() + .scheme("https") + .authority("tangem.com") + .appendPath(localeProvider.getWebUriLocaleLanguage()) + .appendPath("blog/post/seed-phrase-a-risky-solution") + .build() + + router.openUri(webUri) } private fun buttonGenerateSeedPhraseClick() { @@ -325,7 +336,7 @@ class SeedPhraseViewModel @Inject constructor( } } - private suspend fun generateMnemonicGridList(mnemonicComponents: List): ImmutableList { + private fun generateMnemonicGridList(mnemonicComponents: List): ImmutableList { val size = mnemonicComponents.size val splitIndex = if (size.isEven()) size / 2 else size / 2 + 1 val leftColumn = mnemonicComponents.subList(0, splitIndex) @@ -416,8 +427,6 @@ class SeedPhraseViewModel @Inject constructor( // endregion Utils companion object { - private val URI_ABOUT_SEED_PHRASE = - Uri.parse("https://tangem.com/ru/blog/post/seed-phrase-a-risky-solution/") private const val MNEMONIC_DEBOUNCER = "MnemonicDebouncer" private const val MNEMONIC_DEBOUNCE_DELAY = 700L private const val DELAY_GENERATE_SEED_PHRASE = 300L diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapSelectTokenScreen.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapSelectTokenScreen.kt index b10aac1300..ad9b5d8d2c 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapSelectTokenScreen.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapSelectTokenScreen.kt @@ -5,15 +5,7 @@ import androidx.annotation.StringRes import androidx.compose.foundation.ExperimentalFoundationApi 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.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.systemBarsPadding +import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.shape.CircleShape @@ -62,7 +54,7 @@ fun SwapSelectTokenScreen( ExpandableSearchView( title = stringResource(R.string.swapping_token_list_title), onBackClick = onBack, - placeholderSearchText = stringResource(id = R.string.search_tokens_title), + placeholderSearchText = stringResource(id = R.string.common_search_tokens), onSearchChange = state.onSearchEntered, onSearchDisplayClose = { state.onSearchEntered("") }, onFocusChange = onSearchFocusChange, diff --git a/features/tokendetails/api/src/main/kotlin/com/tangem/features/tokendetails/navigation/TokenDetailsRouter.kt b/features/tokendetails/api/src/main/kotlin/com/tangem/features/tokendetails/navigation/TokenDetailsRouter.kt index a361d75438..81dbc9eb69 100644 --- a/features/tokendetails/api/src/main/kotlin/com/tangem/features/tokendetails/navigation/TokenDetailsRouter.kt +++ b/features/tokendetails/api/src/main/kotlin/com/tangem/features/tokendetails/navigation/TokenDetailsRouter.kt @@ -5,4 +5,8 @@ import androidx.fragment.app.Fragment interface TokenDetailsRouter { fun getEntryFragment(): Fragment + + companion object { + const val SELECTED_CURRENCY_KEY = "selected_currency" + } } \ No newline at end of file diff --git a/features/tokendetails/impl/build.gradle.kts b/features/tokendetails/impl/build.gradle.kts index d3af011dcd..a7c6440862 100644 --- a/features/tokendetails/impl/build.gradle.kts +++ b/features/tokendetails/impl/build.gradle.kts @@ -27,6 +27,7 @@ dependencies { implementation(deps.compose.coil) implementation(deps.kotlin.immutable.collections) + implementation(deps.arrow.core) /** DI */ implementation(deps.hilt.android) @@ -37,6 +38,10 @@ dependencies { implementation(projects.core.ui) implementation(projects.core.navigation) + implementation(projects.domain.tokens.models) + implementation(projects.domain.txhistory) + implementation(projects.domain.txhistory.models) + /** Feature Apis */ implementation(projects.features.tokendetails.api) } \ 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 4a75d46fb5..5dfeaf2b82 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 @@ -3,10 +3,15 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.viewmodels import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue +import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.tangem.domain.tokens.models.CryptoCurrency import com.tangem.feature.tokendetails.presentation.router.InnerTokenDetailsRouter import com.tangem.feature.tokendetails.presentation.tokendetails.TokenDetailsPreviewData +import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenInfoBlockState +import com.tangem.features.tokendetails.impl.R +import com.tangem.features.tokendetails.navigation.TokenDetailsRouter import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.delay import kotlinx.coroutines.launch @@ -16,9 +21,14 @@ import kotlin.properties.Delegates private const val LOADING_DELAY = 4_000L @HiltViewModel -internal class TokenDetailsViewModel @Inject constructor() : ViewModel() { +internal class TokenDetailsViewModel @Inject constructor( + savedStateHandle: SavedStateHandle, +) : ViewModel() { - var router: InnerTokenDetailsRouter by Delegates.notNull() + private val cryptoCurrency: CryptoCurrency = savedStateHandle[TokenDetailsRouter.SELECTED_CURRENCY_KEY] + ?: error("no expected parameter CryptoCurrency found") + + var router by Delegates.notNull() var uiState by mutableStateOf(getInitialState()) private set @@ -38,6 +48,19 @@ internal class TokenDetailsViewModel @Inject constructor() : ViewModel() { topAppBarConfig = TokenDetailsPreviewData.tokenDetailsTopAppBarConfig.copy( onBackClick = ::onBackClick, ), + tokenInfoBlockState = TokenInfoBlockState( + name = cryptoCurrency.name, + iconUrl = requireNotNull(cryptoCurrency.iconUrl), + currency = when (cryptoCurrency) { + is CryptoCurrency.Coin -> TokenInfoBlockState.Currency.Native + is CryptoCurrency.Token -> TokenInfoBlockState.Currency.Token( + networkName = cryptoCurrency.standardType.name, + blockchainName = cryptoCurrency.blockchainName, + // TODO: [REDACTED_JIRA] + networkIcon = R.drawable.img_eth_22, + ) + }, + ), ) private fun onBackClick() { diff --git a/features/wallet/impl/build.gradle.kts b/features/wallet/impl/build.gradle.kts index 265fb7ce41..48a2eca7c6 100644 --- a/features/wallet/impl/build.gradle.kts +++ b/features/wallet/impl/build.gradle.kts @@ -9,6 +9,7 @@ plugins { dependencies { /** AndroidX */ implementation(deps.androidx.activity.compose) + implementation(deps.lifecycle.compose) implementation(deps.material) /** Compose */ @@ -53,9 +54,13 @@ dependencies { implementation(projects.domain.tokens) implementation(projects.domain.tokens.models) implementation(projects.domain.txhistory) + implementation(projects.domain.txhistory.models) implementation(projects.domain.wallets) implementation(projects.domain.wallets.models) + implementation(projects.domain.appCurrency) + implementation(projects.domain.appCurrency.models) /** Feature Apis */ implementation(projects.features.wallet.api) + implementation(projects.features.tokendetails.api) } \ 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 722d10e9cb..d0bb5cc8e7 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/WalletPreviewData.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/WalletPreviewData.kt @@ -4,335 +4,379 @@ import androidx.paging.PagingData import com.tangem.core.ui.R import com.tangem.core.ui.components.marketprice.MarketPriceBlockState import com.tangem.core.ui.components.marketprice.PriceChangeConfig -import com.tangem.core.ui.components.transactions.TransactionState +import com.tangem.core.ui.components.transactions.state.TransactionState +import com.tangem.core.ui.components.transactions.state.TxHistoryState +import com.tangem.core.ui.extensions.TextReference 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.DraggableItem -import com.tangem.feature.wallet.presentation.organizetokens.OrganizeTokensListState -import com.tangem.feature.wallet.presentation.organizetokens.OrganizeTokensStateHolder +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.content.WalletTokensListState -import com.tangem.feature.wallet.presentation.wallet.state.content.WalletTxHistoryState +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 import kotlinx.collections.immutable.toPersistentList import kotlinx.coroutines.flow.flowOf import java.util.UUID +@Suppress("LargeClass") internal object WalletPreviewData { - val walletTopBarConfig = WalletTopBarConfig(onScanCardClick = {}, onMoreClick = {}) + val walletTopBarConfig by lazy { WalletTopBarConfig(onScanCardClick = {}, onMoreClick = {}) } - val walletCardContentState = WalletCardState.Content( - id = UserWalletId(UUID.randomUUID().toString()), - title = "Wallet 1", - balance = "8923,05 $", - additionalInfo = "3 cards • Seed enabled", - imageResId = R.drawable.ill_businessman_3d, - onClick = null, - ) + val walletCardContentState by lazy { + WalletCardState.Content( + id = UserWalletId("123"), + title = "Wallet 1", + balance = "8923,05 $", + additionalInfo = "3 cards • Seed enabled", + imageResId = R.drawable.ill_businessman_3d, + onClick = null, + ) + } - val walletCardLoadingState = WalletCardState.Loading( - id = UserWalletId(UUID.randomUUID().toString()), - title = "Wallet 1", - additionalInfo = "3 cards • Seed enabled", - imageResId = R.drawable.ill_businessman_3d, - onClick = null, - ) + val walletCardLoadingState by lazy { + WalletCardState.Loading( + id = UserWalletId("321"), + title = "Wallet 1", + additionalInfo = "3 cards • Seed enabled", + imageResId = R.drawable.ill_businessman_3d, + onClick = null, + ) + } - val walletCardHiddenContentState = WalletCardState.HiddenContent( - id = UserWalletId(UUID.randomUUID().toString()), - title = "Wallet 1", - additionalInfo = "3 cards • Seed enabled", - imageResId = R.drawable.ill_businessman_3d, - onClick = null, - ) + val walletCardHiddenContentState by lazy { + WalletCardState.HiddenContent( + id = UserWalletId("42"), + title = "Wallet 1", + additionalInfo = "3 cards • Seed enabled", + imageResId = R.drawable.ill_businessman_3d, + onClick = null, + ) + } - val walletCardErrorState = WalletCardState.Error( - id = UserWalletId(UUID.randomUUID().toString()), - title = "Wallet 1", - additionalInfo = "3 cards • Seed enabled", - imageResId = R.drawable.ill_businessman_3d, - onClick = null, - ) + val walletCardErrorState by lazy { + WalletCardState.Error( + id = UserWalletId("24"), + title = "Wallet 1", + additionalInfo = "3 cards • Seed enabled", + imageResId = R.drawable.ill_businessman_3d, + onClick = null, + ) + } - val wallets = mapOf( - UserWalletId(stringValue = "123") to walletCardContentState, - UserWalletId(stringValue = "321") to walletCardLoadingState, - UserWalletId(stringValue = "42") to walletCardHiddenContentState, - UserWalletId(stringValue = "24") to walletCardErrorState, - ) + val wallets by lazy { + mapOf( + UserWalletId(stringValue = "123") to walletCardContentState, + UserWalletId(stringValue = "321") to walletCardLoadingState, + UserWalletId(stringValue = "42") to walletCardHiddenContentState, + UserWalletId(stringValue = "24") to walletCardErrorState, + ) + } - val walletListConfig = WalletsListConfig( - selectedWalletIndex = 0, - wallets = wallets.values.toPersistentList(), - onWalletChange = {}, - ) + val walletListConfig by lazy { + WalletsListConfig( + selectedWalletIndex = 0, + wallets = wallets.values.toPersistentList(), + onWalletChange = {}, + ) + } - val tokenItemVisibleState = TokenItemState.Content( - id = UUID.randomUUID().toString(), - tokenIconUrl = null, - tokenIconResId = R.drawable.img_polygon_22, - networkIconResId = R.drawable.img_polygon_22, - name = "Polygon", - amount = "5,412 MATIC", - hasPending = true, - tokenOptions = TokenOptionsState.Visible( - fiatAmount = "321 $", - priceChange = PriceChangeConfig( - valueInPercent = "2%", - type = PriceChangeConfig.Type.UP, + val tokenItemVisibleState by lazy { + TokenItemState.Content( + id = UUID.randomUUID().toString(), + tokenIconUrl = null, + tokenIconResId = R.drawable.img_polygon_22, + networkIconResId = R.drawable.img_polygon_22, + name = "Polygon", + amount = "5,412 MATIC", + hasPending = true, + tokenOptions = TokenOptionsState.Visible( + fiatAmount = "321 $", + priceChange = PriceChangeConfig( + valueInPercent = "2%", + type = PriceChangeConfig.Type.UP, + ), ), - ), - ) + onClick = {}, + ) + } - val tokenItemHiddenState = TokenItemState.Content( - id = UUID.randomUUID().toString(), - tokenIconUrl = null, - tokenIconResId = R.drawable.img_polygon_22, - networkIconResId = R.drawable.img_polygon_22, - name = "Polygon", - amount = "5,412 MATIC", - hasPending = true, - tokenOptions = TokenOptionsState.Hidden( - priceChange = PriceChangeConfig( - valueInPercent = "2%", - type = PriceChangeConfig.Type.UP, + val tokenItemHiddenState by lazy { + TokenItemState.Content( + id = UUID.randomUUID().toString(), + tokenIconUrl = null, + tokenIconResId = R.drawable.img_polygon_22, + networkIconResId = R.drawable.img_polygon_22, + name = "Polygon", + amount = "5,412 MATIC", + hasPending = true, + tokenOptions = TokenOptionsState.Hidden( + priceChange = PriceChangeConfig( + valueInPercent = "2%", + type = PriceChangeConfig.Type.UP, + ), ), - ), - ) + onClick = {}, + ) + } - val tokenItemDragState = TokenItemState.Draggable( - id = UUID.randomUUID().toString(), - tokenIconUrl = null, - tokenIconResId = R.drawable.img_polygon_22, - networkIconResId = R.drawable.img_polygon_22, - name = "Polygon", - fiatAmount = "3 172,14 $", - ) + val tokenItemDragState by lazy { + TokenItemState.Draggable( + id = UUID.randomUUID().toString(), + tokenIconUrl = null, + tokenIconResId = R.drawable.img_polygon_22, + networkIconResId = R.drawable.img_polygon_22, + name = "Polygon", + fiatAmount = "3 172,14 $", + ) + } - val tokenItemUnreachableState = TokenItemState.Unreachable( - id = UUID.randomUUID().toString(), - tokenIconUrl = null, - tokenIconResId = R.drawable.img_polygon_22, - networkIconResId = R.drawable.img_polygon_22, - name = "Polygon", - ) + val tokenItemUnreachableState by lazy { + TokenItemState.Unreachable( + id = UUID.randomUUID().toString(), + tokenIconUrl = null, + tokenIconResId = R.drawable.img_polygon_22, + networkIconResId = R.drawable.img_polygon_22, + name = "Polygon", + ) + } - val loadingTokenItemState = TokenItemState.Loading + val loadingTokenItemState by lazy { TokenItemState.Loading(id = "Loading#1") } private const val networksSize = 10 private const val tokensSize = 3 - val draggableItems = List(networksSize) { it } - .flatMap { index -> - val lastNetworkIndex = networksSize - 1 - val lastTokenIndex = tokensSize - 1 - val networkNumber = index + 1 + val draggableItems by lazy { + List(networksSize) { it } + .flatMap { index -> + val lastNetworkIndex = networksSize - 1 + val lastTokenIndex = tokensSize - 1 + val networkNumber = index + 1 - val group = DraggableItem.GroupHeader( - id = "group_$networkNumber", - networkName = "$networkNumber", - roundingMode = when (index) { - 0 -> DraggableItem.RoundingMode.Top() - lastNetworkIndex -> DraggableItem.RoundingMode.Bottom() - else -> DraggableItem.RoundingMode.None - }, - ) - - val tokens: MutableList = mutableListOf() - repeat(times = tokensSize) { i -> - val tokenNumber = i + 1 - tokens.add( - DraggableItem.Token( - tokenItemState = tokenItemDragState.copy( - id = "${group.id}_token_$tokenNumber", - name = "Token $tokenNumber from $networkNumber network", - networkIconResId = R.drawable.img_eth_22.takeIf { i != 0 }, - ), - groupId = group.id, - roundingMode = when { - i == lastTokenIndex && index == lastNetworkIndex -> DraggableItem.RoundingMode.Bottom() - else -> DraggableItem.RoundingMode.None - }, - ), + val group = DraggableItem.GroupHeader( + id = "group_$networkNumber", + networkName = "$networkNumber", + roundingMode = when (index) { + 0 -> DraggableItem.RoundingMode.Top() + lastNetworkIndex -> DraggableItem.RoundingMode.Bottom() + else -> DraggableItem.RoundingMode.None + }, ) - } - val divider = DraggableItem.GroupPlaceholder(id = "divider_$networkNumber") + val tokens: MutableList = mutableListOf() + repeat(times = tokensSize) { i -> + val tokenNumber = i + 1 + tokens.add( + DraggableItem.Token( + tokenItemState = tokenItemDragState.copy( + id = "${group.id}_token_$tokenNumber", + name = "Token $tokenNumber from $networkNumber network", + networkIconResId = R.drawable.img_eth_22.takeIf { i != 0 }, + ), + groupId = group.id, + roundingMode = when { + i == lastTokenIndex && index == lastNetworkIndex -> DraggableItem.RoundingMode.Bottom() + else -> DraggableItem.RoundingMode.None + }, + ), + ) + } - buildList { - add(group) - addAll(tokens) - if (index != lastNetworkIndex) { - add(divider) + val divider = DraggableItem.GroupPlaceholder(id = "divider_$networkNumber") + + buildList { + add(group) + addAll(tokens) + if (index != lastNetworkIndex) { + add(divider) + } } } - } - .toPersistentList() + .toPersistentList() + } - val draggableTokens = draggableItems - .filterIsInstance() - .toMutableList() - .also { - it[0] = it[0].copy(roundingMode = DraggableItem.RoundingMode.Top()) - } - .toPersistentList() + val draggableTokens by lazy { + draggableItems + .filterIsInstance() + .toMutableList() + .also { + it[0] = it[0].copy(roundingMode = DraggableItem.RoundingMode.Top()) + } + .toPersistentList() + } - val groupedOrganizeTokensState = OrganizeTokensStateHolder( - itemsState = OrganizeTokensListState.GroupedByNetwork( - items = draggableItems, - ), - header = OrganizeTokensStateHolder.HeaderConfig( - onSortByBalanceClick = {}, - onGroupByNetworkClick = {}, - ), - dragConfig = OrganizeTokensStateHolder.DragConfig( - onItemDragged = { _, _ -> }, - onDragStart = {}, - canDragItemOver = { _, _ -> false }, - onItemDragEnd = {}, - ), - actions = OrganizeTokensStateHolder.ActionsConfig( - onApplyClick = {}, - onCancelClick = {}, - ), - ) - - val organizeTokensState = groupedOrganizeTokensState.copy( - itemsState = OrganizeTokensListState.Ungrouped( - items = draggableTokens, - ), - ) - - val bottomSheet = WalletBottomSheetConfig( - isShow = false, - onDismissRequest = {}, - content = WalletBottomSheetConfig.BottomSheetContentConfig.UnlockWallets( - onUnlockClick = {}, - onScanClick = {}, - ), - ) - - private val manageButtons = persistentListOf( - WalletManageButton.Buy(onClick = {}), - WalletManageButton.Send(onClick = {}), - WalletManageButton.Receive(onClick = {}), - WalletManageButton.Exchange(onClick = {}), - WalletManageButton.CopyAddress(onClick = {}), - ) - - val multicurrencyWalletScreenState = WalletStateHolder.MultiCurrencyContent( - onBackClick = {}, - topBarConfig = walletTopBarConfig, - walletsListConfig = walletListConfig, - tokensListState = WalletTokensListState.Content( - persistentListOf( - WalletTokensListState.TokensListItemState.NetworkGroupTitle("Bitcoin"), - WalletTokensListState.TokensListItemState.Token( - tokenItemVisibleState.copy( - id = "token_1", - name = "Ethereum", - tokenIconResId = R.drawable.img_eth_22, - networkIconResId = null, - amount = "1,89340821 ETH", - ), - ), - WalletTokensListState.TokensListItemState.Token( - tokenItemVisibleState.copy( - id = "token_2", - name = "Ethereum", - tokenIconResId = R.drawable.img_eth_22, - networkIconResId = null, - amount = "1,89340821 ETH", - ), - ), - WalletTokensListState.TokensListItemState.Token( - tokenItemVisibleState.copy( - id = "token_3", - name = "Ethereum", - tokenIconResId = R.drawable.img_eth_22, - networkIconResId = null, - amount = "1,89340821 ETH", - ), - ), - WalletTokensListState.TokensListItemState.Token( - tokenItemVisibleState.copy( - id = "token_4", - name = "Ethereum", - tokenIconResId = R.drawable.img_eth_22, - networkIconResId = null, - amount = "1,89340821 ETH", - ), - ), - WalletTokensListState.TokensListItemState.NetworkGroupTitle("Ethereum"), - WalletTokensListState.TokensListItemState.Token( - tokenItemVisibleState.copy( - id = "token_5", - name = "Ethereum", - tokenIconResId = R.drawable.img_eth_22, - networkIconResId = null, - amount = "1,89340821 ETH", - ), - ), + val groupedOrganizeTokensState by lazy { + OrganizeTokensState( + onBackClick = {}, + itemsState = OrganizeTokensListState.GroupedByNetwork( + items = draggableItems, ), - onOrganizeTokensClick = {}, - ), - pullToRefreshConfig = WalletPullToRefreshConfig( - isRefreshing = false, - onRefresh = {}, - ), - notifications = persistentListOf( - WalletNotification.UnreachableNetworks, - WalletNotification.LikeTangemApp(onClick = {}), - WalletNotification.BackupCard(onClick = {}), - WalletNotification.ScanCard(onClick = {}), - ), - bottomSheet = bottomSheet, - ) - - val singleWalletScreenState = WalletStateHolder.SingleCurrencyContent( - onBackClick = {}, - topBarConfig = walletTopBarConfig, - walletsListConfig = walletListConfig, - pullToRefreshConfig = WalletPullToRefreshConfig( - isRefreshing = false, - onRefresh = {}, - ), - notifications = persistentListOf(WalletNotification.LikeTangemApp(onClick = {})), - buttons = manageButtons.map(WalletManageButton::config).toPersistentList(), - bottomSheet = bottomSheet, - marketPriceBlockState = MarketPriceBlockState.Content( - currencyName = "BTC", - price = "98900.12$", - priceChangeConfig = PriceChangeConfig( - valueInPercent = "5.16%", - type = PriceChangeConfig.Type.UP, + header = OrganizeTokensState.HeaderConfig( + onSortClick = {}, + onGroupClick = {}, ), - ), - txHistoryState = WalletTxHistoryState.Content( - flowOf( - PagingData.from( - listOf( - WalletTxHistoryState.TxHistoryItemState.Title(onExploreClick = {}), - WalletTxHistoryState.TxHistoryItemState.GroupTitle("Today"), - WalletTxHistoryState.TxHistoryItemState.Transaction( - TransactionState.Sending( - address = "33BddS...ga2B", - amount = "-0.500913 BTC", - timestamp = "8:41", - ), + dndConfig = OrganizeTokensState.DragAndDropConfig( + onItemDragged = { _, _ -> }, + onDragStart = {}, + canDragItemOver = { _, _ -> false }, + onItemDragEnd = {}, + ), + actions = OrganizeTokensState.ActionsConfig( + onApplyClick = {}, + onCancelClick = {}, + ), + ) + } + + val organizeTokensState by lazy { + groupedOrganizeTokensState.copy( + itemsState = OrganizeTokensListState.Ungrouped( + items = draggableTokens, + ), + ) + } + + val bottomSheet by lazy { + WalletBottomSheetConfig( + isShow = false, + onDismissRequest = {}, + content = WalletBottomSheetConfig.BottomSheetContentConfig.UnlockWallets( + onUnlockClick = {}, + onScanClick = {}, + ), + ) + } + + private val manageButtons by lazy { + persistentListOf( + WalletManageButton.Buy(onClick = {}), + WalletManageButton.Send(onClick = {}), + WalletManageButton.Receive(onClick = {}), + WalletManageButton.Exchange(onClick = {}), + WalletManageButton.CopyAddress(onClick = {}), + ) + } + + val multicurrencyWalletScreenState by lazy { + WalletMultiCurrencyState.Content( + onBackClick = {}, + topBarConfig = walletTopBarConfig, + walletsListConfig = walletListConfig, + tokensListState = WalletTokensListState.Content( + persistentListOf( + TokensListItemState.NetworkGroupTitle(TextReference.Str("Bitcoin")), + TokensListItemState.Token( + tokenItemVisibleState.copy( + id = "token_1", + name = "Ethereum", + tokenIconResId = R.drawable.img_eth_22, + networkIconResId = null, + amount = "1,89340821 ETH", ), - WalletTxHistoryState.TxHistoryItemState.GroupTitle("Yesterday"), - WalletTxHistoryState.TxHistoryItemState.Transaction( - TransactionState.Sending( - address = "33BddS...ga2B", - amount = "-0.500913 BTC", - timestamp = "8:41", + ), + TokensListItemState.Token( + tokenItemVisibleState.copy( + id = "token_2", + name = "Ethereum", + tokenIconResId = R.drawable.img_eth_22, + networkIconResId = null, + amount = "1,89340821 ETH", + ), + ), + TokensListItemState.Token( + tokenItemVisibleState.copy( + id = "token_3", + name = "Ethereum", + tokenIconResId = R.drawable.img_eth_22, + networkIconResId = null, + amount = "1,89340821 ETH", + ), + ), + TokensListItemState.Token( + tokenItemVisibleState.copy( + id = "token_4", + name = "Ethereum", + tokenIconResId = R.drawable.img_eth_22, + networkIconResId = null, + amount = "1,89340821 ETH", + ), + ), + TokensListItemState.NetworkGroupTitle(TextReference.Str("Ethereum")), + TokensListItemState.Token( + tokenItemVisibleState.copy( + id = "token_5", + name = "Ethereum", + tokenIconResId = R.drawable.img_eth_22, + networkIconResId = null, + amount = "1,89340821 ETH", + ), + ), + ), + onOrganizeTokensClick = {}, + ), + pullToRefreshConfig = WalletPullToRefreshConfig( + isRefreshing = false, + onRefresh = {}, + ), + notifications = persistentListOf( + WalletNotification.UnreachableNetworks, + WalletNotification.LikeTangemApp(onClick = {}), + WalletNotification.BackupCard(onClick = {}), + WalletNotification.ScanCard(onClick = {}), + ), + bottomSheetConfig = bottomSheet, + ) + } + + val singleWalletScreenState by lazy { + WalletSingleCurrencyState.Content( + onBackClick = {}, + topBarConfig = walletTopBarConfig, + walletsListConfig = walletListConfig, + pullToRefreshConfig = WalletPullToRefreshConfig( + isRefreshing = false, + onRefresh = {}, + ), + notifications = persistentListOf(WalletNotification.LikeTangemApp(onClick = {})), + buttons = manageButtons, + bottomSheetConfig = bottomSheet, + marketPriceBlockState = MarketPriceBlockState.Content( + currencyName = "BTC", + price = "98900.12$", + priceChangeConfig = PriceChangeConfig( + valueInPercent = "5.16%", + type = PriceChangeConfig.Type.UP, + ), + ), + txHistoryState = TxHistoryState.Content( + flowOf( + PagingData.from( + listOf( + TxHistoryState.TxHistoryItemState.Title(onExploreClick = {}), + TxHistoryState.TxHistoryItemState.GroupTitle("Today"), + TxHistoryState.TxHistoryItemState.Transaction( + TransactionState.Sending( + txHash = UUID.randomUUID().toString(), + address = "33BddS...ga2B", + amount = "-0.500913 BTC", + timestamp = "8:41", + ), + ), + TxHistoryState.TxHistoryItemState.GroupTitle("Yesterday"), + TxHistoryState.TxHistoryItemState.Transaction( + TransactionState.Sending( + txHash = UUID.randomUUID().toString(), + address = "33BddS...ga2B", + amount = "-0.500913 BTC", + timestamp = "8:41", + ), ), ), ), ), ), - ), - ) + ) + } } \ No newline at end of file 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 5c4cb19fb7..0e9751d451 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/TokenItem.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/TokenItem.kt @@ -1,6 +1,9 @@ package com.tangem.feature.wallet.presentation.common.component 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.layout.* @@ -16,11 +19,11 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext 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 androidx.compose.ui.unit.dp import androidx.constraintlayout.compose.ConstrainedLayoutReference import androidx.constraintlayout.compose.ConstraintLayout import androidx.constraintlayout.compose.ConstraintLayoutScope @@ -58,6 +61,7 @@ internal fun TokenItem(state: TokenItemState, modifier: Modifier = Modifier) { private fun ContentTokenItem(content: TokenItemState.Content, modifier: Modifier = Modifier) { InternalTokenItem( modifier = modifier, + onClick = content.onClick, name = content.name, tokenIconUrl = content.tokenIconUrl, tokenIconResId = content.tokenIconResId, @@ -124,7 +128,7 @@ internal fun UnreachableTokenItem(state: TokenItemState.Unreachable, modifier: M options = { ref -> Text( modifier = Modifier.constrainAsOptionsItem(scope = this, ref), - text = "Unreachable", // TODO (conform this text) + text = stringResource(id = R.string.common_unreachable), style = TangemTypography.body2, color = TangemTheme.colors.text.tertiary, ) @@ -143,7 +147,7 @@ private fun LoadingTokenItem(modifier: Modifier = Modifier) { vertical = TangemTheme.dimens.spacing4, ), verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), + horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing24), ) { CircleShimmer(modifier = Modifier.size(size = TangemTheme.dimens.size42)) Row( @@ -188,22 +192,17 @@ private fun LoadingTokenItem(modifier: Modifier = Modifier) { * Block for end part of token item * shows status is reachable, is drag, hidden or show balance */ +@OptIn(ExperimentalAnimationApi::class) @Composable private fun TokenOptionsBlock(state: TokenOptionsState, modifier: Modifier = Modifier) { - when (state) { - is TokenOptionsState.Visible -> { - TokenFiatPercentageBlock( - modifier = modifier, - fiatAmount = state.fiatAmount, - priceChange = state.priceChange, - ) - } - is TokenOptionsState.Hidden -> { - TokenFiatPercentageBlock( - modifier = modifier, - fiatAmount = DOTS, - priceChange = state.priceChange, - ) + AnimatedContent(targetState = state, label = "Update the options", modifier = modifier) { options -> + when (options) { + is TokenOptionsState.Visible -> { + TokenFiatPercentageBlock(fiatAmount = options.fiatAmount, priceChange = options.priceChange) + } + is TokenOptionsState.Hidden -> { + TokenFiatPercentageBlock(fiatAmount = DOTS, priceChange = options.priceChange) + } } } } @@ -219,8 +218,12 @@ private fun InternalTokenItem( hasPending: Boolean, options: @Composable ConstraintLayoutScope.(ref: ConstrainedLayoutReference) -> Unit, modifier: Modifier = Modifier, + onClick: (() -> Unit)? = null, ) { - BaseSurface(modifier) { + BaseSurface( + modifier = modifier, + onClick = onClick, + ) { ConstraintLayout( modifier = Modifier .fillMaxWidth() @@ -243,7 +246,7 @@ private fun InternalTokenItem( TokenTitleAmountBlock( modifier = Modifier - .padding(horizontal = TangemTheme.dimens.spacing12) + .padding(horizontal = TangemTheme.dimens.spacing8) .constrainAs(tokenNameItem) { centerVerticallyTo(parent) start.linkTo(iconItem.end) @@ -298,17 +301,18 @@ private fun TokenTitleAmountBlock(title: String, amount: String?, hasPending: Bo style = TangemTypography.subtitle2, color = TangemTheme.colors.text.primary1, ) - if (hasPending) { + + AnimatedVisibility(visible = hasPending, modifier = Modifier.align(Alignment.CenterVertically)) { Image( - modifier = Modifier.align(Alignment.CenterVertically), painter = painterResource(id = R.drawable.img_loader_15), contentDescription = null, ) } } - if (!amount.isNullOrBlank()) { + + AnimatedVisibility(visible = !amount.isNullOrBlank()) { Text( - text = amount, + text = requireNotNull(amount), style = TangemTypography.body2, color = TangemTheme.colors.text.tertiary, ) @@ -316,6 +320,7 @@ private fun TokenTitleAmountBlock(title: String, amount: String?, hasPending: Bo } } +@OptIn(ExperimentalAnimationApi::class) @Composable private fun TokenFiatPercentageBlock( fiatAmount: String, @@ -334,30 +339,39 @@ private fun TokenFiatPercentageBlock( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.End, ) { - val iconChangeArrow: Int - val changeTextColor: Color - when (priceChange.type) { - PriceChangeConfig.Type.UP -> { - iconChangeArrow = R.drawable.img_arrow_up_8 - changeTextColor = TangemTheme.colors.text.accent - } - PriceChangeConfig.Type.DOWN -> { - iconChangeArrow = R.drawable.img_arrow_down_8 - changeTextColor = TangemTheme.colors.text.warning - } + AnimatedContent( + targetState = priceChange.type, + label = "Update the price change's arrow", + modifier = Modifier.align(Alignment.CenterVertically), + ) { + Image( + painter = painterResource( + id = when (priceChange.type) { + PriceChangeConfig.Type.UP -> R.drawable.img_arrow_up_8 + PriceChangeConfig.Type.DOWN -> R.drawable.img_arrow_down_8 + }, + ), + contentDescription = null, + ) } - Image( - modifier = Modifier.align(Alignment.CenterVertically), - painter = painterResource(id = iconChangeArrow), - contentDescription = null, - ) + SpacerW4() - Text( + + AnimatedContent( + targetState = priceChange.type, + label = "Update the price change's arrow", modifier = Modifier.align(Alignment.CenterVertically), - text = priceChange.valueInPercent, - style = TangemTypography.body2, - color = changeTextColor, - ) + ) { + Text( + modifier = Modifier.align(Alignment.CenterVertically), + text = priceChange.valueInPercent, + style = TangemTypography.body2, + color = when (priceChange.type) { + PriceChangeConfig.Type.UP -> TangemTheme.colors.text.accent + PriceChangeConfig.Type.DOWN -> TangemTheme.colors.text.warning + }, + ) + } } } } @@ -389,20 +403,20 @@ private fun TokenIcon( contentDescription = null, ) - if (networkIconRes != null) { - Box( + AnimatedVisibility( + visible = networkIconRes != null, + modifier = Modifier + .align(Alignment.TopEnd) + .size(TangemTheme.dimens.size18) + .background(color = Color.White, shape = CircleShape), + ) { + Image( modifier = Modifier - .align(Alignment.TopEnd) - .size(TangemTheme.dimens.size18) - .background(color = Color.White, shape = CircleShape), - contentAlignment = Alignment.Center, - ) { - Image( - modifier = Modifier.padding(all = 0.5.dp), - painter = painterResource(id = networkIconRes), - contentDescription = null, - ) - } + .padding(all = TangemTheme.dimens.spacing0_5) + .align(Alignment.Center), + painter = painterResource(id = requireNotNull(networkIconRes)), + contentDescription = 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 b1c8417e9e..bb2883ed5f 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 @@ -8,12 +8,16 @@ import com.tangem.core.ui.components.marketprice.PriceChangeConfig @Immutable internal sealed interface TokenItemState { + /** Unique id */ + val id: String + /** Loading token state */ - object Loading : TokenItemState + data class Loading(override val id: String) : TokenItemState /** * Content token state * + * @property id unique id * @property tokenIconUrl token icon url * @property tokenIconResId token icon resource id * @property networkIconResId network icon resource id, may be null if it is a coin @@ -23,7 +27,7 @@ internal sealed interface TokenItemState { * @property tokenOptions state for token options */ data class Content( - val id: String, + override val id: String, val tokenIconUrl: String?, @DrawableRes val tokenIconResId: Int, @DrawableRes val networkIconResId: Int?, @@ -31,18 +35,21 @@ internal sealed interface TokenItemState { val amount: String, val hasPending: Boolean, val tokenOptions: TokenOptionsState, + val onClick: () -> Unit, ) : TokenItemState /** * Draggable token state * + * @property id unique id * @property tokenIconUrl token icon url * @property tokenIconResId token icon resource id * @property networkIconResId network icon resource id, may be null if it is a coin * @property name token name + * @property fiatAmount fiat amount of token */ data class Draggable( - val id: String, + override val id: String, val tokenIconUrl: String?, @DrawableRes val tokenIconResId: Int, @DrawableRes val networkIconResId: Int?, @@ -60,7 +67,7 @@ internal sealed interface TokenItemState { * @property name token name */ data class Unreachable( - val id: String, + override val id: String, val tokenIconUrl: String?, @DrawableRes val tokenIconResId: Int, @DrawableRes val networkIconResId: Int?, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensIntents.kt new file mode 100644 index 0000000000..84ce246245 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensIntents.kt @@ -0,0 +1,14 @@ +package com.tangem.feature.wallet.presentation.organizetokens + +internal interface OrganizeTokensIntents { + + fun onBackClick() + + fun onSortClick() + + fun onGroupClick() + + fun onApplyClick() + + fun onCancelClick() +} \ 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 87f1b076b2..beb7709ffb 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,5 +1,6 @@ package com.tangem.feature.wallet.presentation.organizetokens +import androidx.activity.compose.BackHandler import androidx.compose.animation.core.* import androidx.compose.foundation.background import androidx.compose.foundation.layout.* @@ -31,10 +32,15 @@ import com.tangem.feature.wallet.impl.R import com.tangem.feature.wallet.presentation.common.WalletPreviewData import com.tangem.feature.wallet.presentation.common.component.DraggableNetworkGroupItem import com.tangem.feature.wallet.presentation.common.component.DraggableTokenItem +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.* @Composable -internal fun OrganizeTokensScreen(state: OrganizeTokensStateHolder, modifier: Modifier = Modifier) { +internal fun OrganizeTokensScreen(state: OrganizeTokensState, modifier: Modifier = Modifier) { + BackHandler(onBack = state.onBackClick) + val tokensListState = rememberLazyListState() Scaffold( @@ -44,10 +50,12 @@ internal fun OrganizeTokensScreen(state: OrganizeTokensStateHolder, modifier: Mo }, content = { paddingValues -> TokenList( - modifier = Modifier.padding(paddingValues), + modifier = Modifier + .padding(paddingValues) + .fillMaxSize(), listState = tokensListState, state = state.itemsState, - dragConfig = state.dragConfig, + dndConfig = state.dndConfig, ) }, floatingActionButtonPosition = FabPosition.Center, @@ -62,15 +70,15 @@ internal fun OrganizeTokensScreen(state: OrganizeTokensStateHolder, modifier: Mo private fun TokenList( listState: LazyListState, state: OrganizeTokensListState, - dragConfig: OrganizeTokensStateHolder.DragConfig, + dndConfig: OrganizeTokensState.DragAndDropConfig, modifier: Modifier = Modifier, ) { Box(modifier = modifier) { val reorderableListState = rememberReorderableLazyListState( - onMove = dragConfig.onItemDragged, + onMove = dndConfig.onItemDragged, listState = listState, - canDragOver = dragConfig.canDragItemOver, - onDragEnd = { _, _ -> dragConfig.onItemDragEnd() }, + canDragOver = dndConfig.canDragItemOver, + onDragEnd = { _, _ -> dndConfig.onItemDragEnd() }, ) val items = state.items @@ -78,7 +86,8 @@ private fun TokenList( modifier = Modifier .reorderable(reorderableListState) .align(Alignment.TopCenter) - .padding(horizontal = TangemTheme.dimens.spacing16), + .padding(horizontal = TangemTheme.dimens.spacing16) + .fillMaxSize(), state = reorderableListState.listState, contentPadding = PaddingValues( top = TangemTheme.dimens.spacing12, @@ -91,7 +100,7 @@ private fun TokenList( ) { index, item -> val onDragStart = remember(item) { - { dragConfig.onDragStart(item) } + { dndConfig.onDragStart(item) } } DraggableItem( @@ -166,7 +175,7 @@ private fun BottomGradient(modifier: Modifier = Modifier) { @Composable private fun TopBar( - config: OrganizeTokensStateHolder.HeaderConfig, + config: OrganizeTokensState.HeaderConfig, tokensListState: LazyListState, modifier: Modifier = Modifier, ) { @@ -211,16 +220,25 @@ private fun TopBar( config = ActionButtonConfig( text = TextReference.Res(id = R.string.organize_tokens_sort_by_balance), iconResId = R.drawable.ic_sort_24, - onClick = config.onSortByBalanceClick, + enabled = config.isEnabled, + onClick = config.onSortClick, + dimContent = !config.isSortedByBalance, ), modifier = Modifier.weight(1f), color = TangemTheme.colors.background.primary, ) RoundedActionButton( config = ActionButtonConfig( - text = TextReference.Res(id = R.string.organize_tokens_group), + text = TextReference.Res( + id = if (config.isGrouped) { + R.string.organize_tokens_ungroup + } else { + R.string.organize_tokens_group + }, + ), + enabled = config.isEnabled, iconResId = R.drawable.ic_group_24, - onClick = config.onGroupByNetworkClick, + onClick = config.onGroupClick, ), modifier = Modifier.weight(1f), color = TangemTheme.colors.background.primary, @@ -230,7 +248,7 @@ private fun TopBar( } @Composable -private fun Actions(config: OrganizeTokensStateHolder.ActionsConfig, modifier: Modifier = Modifier) { +private fun Actions(config: OrganizeTokensState.ActionsConfig, modifier: Modifier = Modifier) { Row( modifier = modifier .padding(horizontal = TangemTheme.dimens.spacing16) @@ -247,6 +265,8 @@ private fun Actions(config: OrganizeTokensStateHolder.ActionsConfig, modifier: M modifier = Modifier.weight(1f), text = stringResource(id = R.string.common_apply), onClick = config.onApplyClick, + showProgress = config.showApplyProgress, + enabled = config.canApply, ) } } @@ -308,7 +328,7 @@ private fun Modifier.applyShapeAndShadow(roundingMode: DraggableItem.RoundingMod @Preview(showBackground = true, widthDp = 360) @Composable private fun OrganizeTokensScreenPreview_Light( - @PreviewParameter(OrganizeTokensStateProvider::class) state: OrganizeTokensStateHolder, + @PreviewParameter(OrganizeTokensStateProvider::class) state: OrganizeTokensState, ) { TangemTheme { OrganizeTokensScreen(state) @@ -318,14 +338,14 @@ private fun OrganizeTokensScreenPreview_Light( @Preview(showBackground = true, widthDp = 360) @Composable private fun OrganizeTokensScreenPreview_Dark( - @PreviewParameter(OrganizeTokensStateProvider::class) state: OrganizeTokensStateHolder, + @PreviewParameter(OrganizeTokensStateProvider::class) state: OrganizeTokensState, ) { TangemTheme(isDark = true) { OrganizeTokensScreen(state) } } -private class OrganizeTokensStateProvider : CollectionPreviewParameterProvider( +private class OrganizeTokensStateProvider : CollectionPreviewParameterProvider( collection = listOf( WalletPreviewData.organizeTokensState, WalletPreviewData.groupedOrganizeTokensState, 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 4da6dcb29c..14fecec78d 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,179 +1,104 @@ package com.tangem.feature.wallet.presentation.organizetokens -import androidx.compose.runtime.Immutable -import com.tangem.feature.wallet.presentation.common.state.TokenItemState -import kotlinx.collections.immutable.PersistentList -import kotlinx.collections.immutable.toPersistentList -import org.burnoutcrew.reorderable.ItemPosition +import com.tangem.common.Provider +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.tokens.error.TokenListError +import com.tangem.domain.tokens.error.TokenListSortingError +import com.tangem.domain.tokens.model.TokenList +import com.tangem.feature.wallet.presentation.organizetokens.model.OrganizeTokensListState +import com.tangem.feature.wallet.presentation.organizetokens.model.OrganizeTokensState +import com.tangem.feature.wallet.presentation.organizetokens.utils.converter.InProgressStateConverter +import com.tangem.feature.wallet.presentation.organizetokens.utils.converter.TokenListToStateConverter +import com.tangem.feature.wallet.presentation.organizetokens.utils.converter.error.TokenListErrorConverter +import com.tangem.feature.wallet.presentation.organizetokens.utils.converter.error.TokenListSortingErrorConverter +import com.tangem.feature.wallet.presentation.organizetokens.utils.converter.items.CryptoCurrencyToDraggableItemConverter +import com.tangem.feature.wallet.presentation.organizetokens.utils.converter.items.NetworkGroupToDraggableItemsConverter +import com.tangem.feature.wallet.presentation.organizetokens.utils.converter.items.TokenListToListStateConverter +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.* -internal data class OrganizeTokensStateHolder( - val header: HeaderConfig, - val itemsState: OrganizeTokensListState, - val dragConfig: DragConfig, - val actions: ActionsConfig, +internal class OrganizeTokensStateHolder( + private val intents: OrganizeTokensIntents, + private val appCurrencyProvider: Provider, + private val onSubscription: () -> Unit, + stateFlowScope: CoroutineScope, ) { - data class HeaderConfig( - val onSortByBalanceClick: () -> Unit, - val onGroupByNetworkClick: () -> Unit, - ) + private val stateFlowInternal: MutableStateFlow = MutableStateFlow(getInitialState()) - data class ActionsConfig( - val onApplyClick: () -> Unit, - val onCancelClick: () -> Unit, - ) + private val tokenListConverter by lazy { + val tokensConverter = CryptoCurrencyToDraggableItemConverter(appCurrencyProvider) + val itemsConverter = TokenListToListStateConverter( + tokensConverter = tokensConverter, + groupsConverter = NetworkGroupToDraggableItemsConverter(tokensConverter), + ) - data class DragConfig( - val onItemDragged: (from: ItemPosition, to: ItemPosition) -> Unit, - val canDragItemOver: (dragOver: ItemPosition, dragging: ItemPosition) -> Boolean, - val onItemDragEnd: () -> Unit, - val onDragStart: (item: DraggableItem) -> Unit, - ) -} - -@Immutable -internal sealed interface OrganizeTokensListState { - val items: PersistentList - - data class GroupedByNetwork( - override val items: PersistentList, - ) : OrganizeTokensListState - - data class Ungrouped( - override val items: PersistentList, - ) : OrganizeTokensListState - - @Suppress("UNCHECKED_CAST") - fun updateItems(update: (PersistentList) -> List): OrganizeTokensListState { - val updatedItems = update(this.items).toPersistentList() - - return when (this) { - is GroupedByNetwork -> this.copy(items = updatedItems) - is Ungrouped -> this.copy(items = updatedItems as PersistentList) - } - } -} - -/** - * Helper class for the DND list items - * - * @property id ID of the item - * @property roundingMode item [RoundingMode] - * @property showShadow if true then item should be elevated - * */ -@Immutable -internal sealed interface DraggableItem { - val id: String - val roundingMode: RoundingMode - val showShadow: Boolean - - /** - * Item for network group header. - * - * @property id ID of the network group - * @property networkName network group name - * @property roundingMode item [RoundingMode] - * @property showShadow if true then item should be elevated - * */ - data class GroupHeader( - override val id: String, - val networkName: String, - override val roundingMode: RoundingMode = RoundingMode.None, - override val showShadow: Boolean = false, - ) : DraggableItem - - /** - * Item for token. - * - * @property tokenItemState state of the token item - * @property groupId ID of the network group which contains this token - * @property id ID of the token - * @property roundingMode item [RoundingMode] - * @property showShadow if true then item should be elevated - * */ - data class Token( - val tokenItemState: TokenItemState.Draggable, - val groupId: String, - override val showShadow: Boolean = false, - override val roundingMode: RoundingMode = RoundingMode.None, - ) : DraggableItem { - override val id: String = tokenItemState.id + TokenListToStateConverter(Provider(stateFlowInternal::value), itemsConverter) } - /** - * Helper item used to detect possible positions where a network group can be placed. - * Used only on [OrganizeTokensListState.GroupedByNetwork] and placed between network groups. - * - * @property id ID of the placeholder - * */ - data class GroupPlaceholder( - override val id: String, - ) : DraggableItem { - override val showShadow: Boolean = false - override val roundingMode: RoundingMode = RoundingMode.None + private val inProgressStateConverter by lazy { + InProgressStateConverter() } - /** - * Update item [RoundingMode] - * - * @param mode new [RoundingMode] - * - * @return updated [DraggableItem] - * */ - fun roundingMode(mode: RoundingMode): DraggableItem = when (this) { - is GroupPlaceholder -> this - is GroupHeader -> this.copy(roundingMode = mode) - is Token -> this.copy(roundingMode = mode) + private val tokenListErrorConverter by lazy { + TokenListErrorConverter(Provider(stateFlowInternal::value), inProgressStateConverter) } - /** - * Update item shadow visibility - * - * @param show if true then item should be elevated - * - * @return updated [DraggableItem] - * */ - fun showShadow(show: Boolean): DraggableItem = when (this) { - is GroupPlaceholder -> this - is GroupHeader -> this.copy(showShadow = show) - is Token -> this.copy(showShadow = show) + private val tokenListSortingErrorConverter by lazy { + TokenListSortingErrorConverter(Provider(stateFlowInternal::value), inProgressStateConverter) } - /** - * Rounding mode of the [DraggableItem] - * - * @property showGap if true then item should have padding on rounded side - * */ - @Immutable - sealed interface RoundingMode { - val showGap: Boolean + val stateFlow: StateFlow = stateFlowInternal + .onSubscription { onSubscription() } + .stateIn( + scope = stateFlowScope, + started = SharingStarted.WhileSubscribed(), + initialValue = getInitialState(), + ) - /** - * In this mode, item is not rounded - * */ - object None : RoundingMode { - override val showGap: Boolean = false - } + fun updateStateWithTokenList(tokenList: TokenList) { + updateState { tokenListConverter.convert(tokenList) } + } - /** - * In this mode, item should have a rounded top side - * - * @property showGap if true then item should have top padding - * */ - data class Top(override val showGap: Boolean = false) : RoundingMode + fun updateStateToDisplayProgress() { + updateState { inProgressStateConverter.convert(value = this) } + } - /** - * In this mode, item should have a rounded bottom side - * - * @property showGap if true then item should have bottom padding - * */ - data class Bottom(override val showGap: Boolean = false) : RoundingMode + fun updateStateToHideProgress() { + updateState { inProgressStateConverter.convertBack(value = this) } + } - /** - * In this mode, item should have a rounded all sides - * - * @property showGap if true then item should have top and bottom padding - * */ - data class All(override val showGap: Boolean = false) : RoundingMode + fun updateStateWithError(error: TokenListError) { + updateState { tokenListErrorConverter.convert(error) } + } + + fun updateStateWithError(error: TokenListSortingError) { + updateState { tokenListSortingErrorConverter.convert(error) } + } + + private fun getInitialState(): OrganizeTokensState { + return OrganizeTokensState( + onBackClick = intents::onBackClick, + itemsState = OrganizeTokensListState.Empty, + header = OrganizeTokensState.HeaderConfig( + onSortClick = intents::onSortClick, + onGroupClick = intents::onGroupClick, + ), + actions = OrganizeTokensState.ActionsConfig( + onApplyClick = intents::onApplyClick, + onCancelClick = intents::onCancelClick, + ), + // TODO: Will be added in next MR + dndConfig = OrganizeTokensState.DragAndDropConfig( + onItemDragged = { _, _ -> }, + onDragStart = { }, + onItemDragEnd = { }, + canDragItemOver = { _, _ -> false }, + ), + ) + } + + private fun updateState(block: OrganizeTokensState.() -> OrganizeTokensState) { + stateFlowInternal.update(block) } } \ No newline at end of file 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 52f391935b..0313bf1e7a 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 @@ -1,130 +1,147 @@ package com.tangem.feature.wallet.presentation.organizetokens -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.setValue import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import arrow.core.getOrElse +import com.tangem.common.Provider +import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.tokens.ApplyTokenListSortingUseCase +import com.tangem.domain.tokens.GetTokenListUseCase +import com.tangem.domain.tokens.ToggleTokenListGroupingUseCase +import com.tangem.domain.tokens.ToggleTokenListSortingUseCase +import com.tangem.domain.tokens.model.TokenList import com.tangem.domain.wallets.models.UserWalletId -import com.tangem.feature.wallet.presentation.common.WalletPreviewData -import com.tangem.feature.wallet.presentation.organizetokens.OrganizeTokensStateHolder.DragConfig -import com.tangem.feature.wallet.presentation.organizetokens.OrganizeTokensStateHolder.HeaderConfig -import com.tangem.feature.wallet.presentation.organizetokens.utils.* +import com.tangem.feature.wallet.presentation.organizetokens.model.OrganizeTokensListState +import com.tangem.feature.wallet.presentation.organizetokens.model.OrganizeTokensState +import com.tangem.feature.wallet.presentation.organizetokens.utils.CryptoCurrenciesIdsResolver import com.tangem.feature.wallet.presentation.router.InnerWalletRouter import com.tangem.feature.wallet.presentation.router.WalletRoute import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.collections.immutable.toPersistentList import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch -import org.burnoutcrew.reorderable.ItemPosition +import kotlinx.coroutines.withContext import javax.inject.Inject -import kotlin.properties.Delegates -// FIXME: Implemented with preview data @HiltViewModel -internal class OrganizeTokensViewModel @Inject constructor(savedStateHandle: SavedStateHandle) : ViewModel() { +internal class OrganizeTokensViewModel @Inject constructor( + private val getTokenListUseCase: GetTokenListUseCase, + private val toggleTokenListGroupingUseCase: ToggleTokenListGroupingUseCase, + private val toggleTokenListSortingUseCase: ToggleTokenListSortingUseCase, + private val applyTokenListSortingUseCase: ApplyTokenListSortingUseCase, + private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, + savedStateHandle: SavedStateHandle, +) : ViewModel(), OrganizeTokensIntents { - @Volatile - private var movingItem: DraggableItem? = null + lateinit var router: InnerWalletRouter - var router: InnerWalletRouter by Delegates.notNull() - val userWalletId: UserWalletId by lazy { + private val selectedAppCurrencyFlow = createSelectedAppCurrencyFlow() + + private val stateHolder = OrganizeTokensStateHolder( + stateFlowScope = viewModelScope, + intents = this, + appCurrencyProvider = Provider(selectedAppCurrencyFlow::value), + onSubscription = { + bootstrapTokenList() + }, + ) + + private val userWalletId: UserWalletId by lazy { val userWalletIdValue: String = checkNotNull(savedStateHandle[WalletRoute.userWalletIdKey]) UserWalletId(userWalletIdValue) } - var uiState: OrganizeTokensStateHolder by mutableStateOf(getInitialState()) - private set + private var tokenList: TokenList? = null - private fun getInitialState(): OrganizeTokensStateHolder = WalletPreviewData.organizeTokensState.copy( - itemsState = OrganizeTokensListState.Ungrouped( - items = WalletPreviewData.draggableTokens, - ), - dragConfig = DragConfig( - onItemDragged = this::moveItem, - canDragItemOver = this::checkCanMoveItemOver, - onItemDragEnd = this::endMoving, - onDragStart = this::startMoving, - ), - header = HeaderConfig( - onSortByBalanceClick = { /* no-op */ }, - onGroupByNetworkClick = this::toggleTokensByNetworkGrouping, - ), - ) + val uiState: StateFlow = stateHolder.stateFlow - private fun toggleTokensByNetworkGrouping() { - val newListState = when (val itemsState = uiState.itemsState) { - is OrganizeTokensListState.GroupedByNetwork -> OrganizeTokensListState.Ungrouped( - items = itemsState.items.filterIsInstance().toPersistentList(), - ) - is OrganizeTokensListState.Ungrouped -> OrganizeTokensListState.GroupedByNetwork( - items = WalletPreviewData.draggableItems, + override fun onBackClick() { + router.popBackStack() + } + + override fun onSortClick() { + viewModelScope.launch(Dispatchers.Default) { + val list = tokenList ?: return@launch + + toggleTokenListSortingUseCase(list).fold( + ifLeft = stateHolder::updateStateWithError, + ifRight = { + stateHolder.updateStateWithTokenList(it) + tokenList = it + }, ) } - - uiState = uiState.copy(itemsState = newListState) } - private fun checkCanMoveItemOver(moveOverItemPosition: ItemPosition, movedItemPosition: ItemPosition): Boolean { - val items = (uiState.itemsState as? OrganizeTokensListState.GroupedByNetwork) - ?.items - ?: return true // If ungrouped then item can be moved anywhere + override fun onGroupClick() { + viewModelScope.launch(Dispatchers.Default) { + val list = tokenList ?: return@launch - val (moveOverItem, movedItem) = items.findItemsToMove(moveOverItemPosition.key, movedItemPosition.key) - - if (moveOverItem == null || movedItem == null) { - return false - } - - return when (movedItem) { - is DraggableItem.GroupHeader -> checkCanMoveHeaderOver(moveOverItemPosition, moveOverItem, items.lastIndex) - is DraggableItem.Token -> checkCanMoveTokenOver(movedItem, moveOverItem) - is DraggableItem.GroupPlaceholder -> false + toggleTokenListGroupingUseCase(list).fold( + ifLeft = stateHolder::updateStateWithError, + ifRight = { + stateHolder.updateStateWithTokenList(it) + tokenList = it + }, + ) } } - private fun startMoving(movingItem: DraggableItem) = viewModelScope.launch(Dispatchers.Default) { - if (this@OrganizeTokensViewModel.movingItem != null) return@launch - this@OrganizeTokensViewModel.movingItem = movingItem + override fun onApplyClick() { + viewModelScope.launch(Dispatchers.Default) { + stateHolder.updateStateToDisplayProgress() - val updatedItemsState = uiState.itemsState.updateItems { items -> - when (movingItem) { - is DraggableItem.GroupHeader -> items.collapseGroup(movingItem) - is DraggableItem.Token -> when (uiState.itemsState) { - is OrganizeTokensListState.GroupedByNetwork -> items.divideGroups(movingItem) - is OrganizeTokensListState.Ungrouped -> items.divideItems(movingItem) - } - is DraggableItem.GroupPlaceholder -> items + val listState = uiState.value.itemsState + val resolver = CryptoCurrenciesIdsResolver() + + val result = applyTokenListSortingUseCase( + userWalletId = userWalletId, + sortedTokensIds = resolver.resolve(listState, tokenList), + isGroupedByNetwork = listState is OrganizeTokensListState.GroupedByNetwork, + isSortedByBalance = uiState.value.header.isSortedByBalance, + ) + + result.fold( + ifLeft = stateHolder::updateStateWithError, + ifRight = { + stateHolder.updateStateToHideProgress() + withContext(Dispatchers.Main) { router.popBackStack() } + }, + ) + } + } + + override fun onCancelClick() { + router.popBackStack() + } + + private fun bootstrapTokenList() { + viewModelScope.launch(Dispatchers.Default) { + val maybeTokenList = getTokenListUseCase(userWalletId) + .first { it.getOrNull()?.totalFiatBalance is TokenList.FiatBalance.Loaded } + + maybeTokenList.fold( + ifLeft = stateHolder::updateStateWithError, + ifRight = { + stateHolder.updateStateWithTokenList(it) + tokenList = it + }, + ) + } + } + + private fun createSelectedAppCurrencyFlow(): StateFlow { + return getSelectedAppCurrencyUseCase() + .map { maybeAppCurrency -> + maybeAppCurrency.getOrElse { AppCurrency.Default } } - } - - uiState = uiState.copy(itemsState = updatedItemsState) - } - - private fun endMoving() = viewModelScope.launch(Dispatchers.Default) { - if (movingItem == null) return@launch - - val updatedItemsState = uiState.itemsState.updateItems { items -> - when (movingItem) { - is DraggableItem.GroupHeader -> items.expandGroups() - is DraggableItem.Token -> items.uniteItems() - is DraggableItem.GroupPlaceholder, - null, - -> items - } - } - - uiState = uiState.copy(itemsState = updatedItemsState) - movingItem = null - } - - private fun moveItem(from: ItemPosition, to: ItemPosition) = viewModelScope.launch(Dispatchers.Default) { - uiState = uiState.copy( - itemsState = uiState.itemsState.updateItems { - it.moveItem(from.index, to.index) - }, - ) + .stateIn( + scope = viewModelScope, + started = SharingStarted.Eagerly, + initialValue = AppCurrency.Default, + ) } } \ No newline at end of file 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 new file mode 100644 index 0000000000..5d367c0436 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/model/DraggableItem.kt @@ -0,0 +1,129 @@ +package com.tangem.feature.wallet.presentation.organizetokens.model + +import androidx.compose.runtime.Immutable +import com.tangem.feature.wallet.presentation.common.state.TokenItemState +import com.tangem.feature.wallet.presentation.organizetokens.model.DraggableItem.RoundingMode + +/** + * Helper class for the DND list items + * + * @property id ID of the item + * @property roundingMode item [RoundingMode] + * @property showShadow if true then item should be elevated + * */ +@Immutable +internal sealed class DraggableItem { + abstract val id: String + abstract val roundingMode: RoundingMode + abstract val showShadow: Boolean + + /** + * Item for network group header. + * + * @property id ID of the network group + * @property networkName network group name + * @property roundingMode item [RoundingMode] + * @property showShadow if true then item should be elevated + * */ + data class GroupHeader( + override val id: String, + val networkName: String, + override val roundingMode: RoundingMode = RoundingMode.None, + override val showShadow: Boolean = false, + ) : DraggableItem() + + /** + * Item for token. + * + * @property tokenItemState state of the token item + * @property groupId ID of the network group which contains this token + * @property id ID of the token + * @property roundingMode item [RoundingMode] + * @property showShadow if true then item should be elevated + * */ + data class Token( + val tokenItemState: TokenItemState.Draggable, + val groupId: String, + override val showShadow: Boolean = false, + override val roundingMode: RoundingMode = RoundingMode.None, + ) : DraggableItem() { + override val id: String = tokenItemState.id + } + + /** + * Helper item used to detect possible positions where a network group can be placed. + * Used only on [OrganizeTokensListState.GroupedByNetwork] and placed between network groups. + * + * @property id ID of the placeholder + * */ + data class GroupPlaceholder( + override val id: String, + ) : DraggableItem() { + override val showShadow: Boolean = false + override val roundingMode: RoundingMode = RoundingMode.None + } + + /** + * Rounding mode of the [DraggableItem] + * + * @property showGap if true then item should have padding on rounded side + * */ + @Immutable + sealed class RoundingMode { + abstract val showGap: Boolean + + /** + * In this mode, item is not rounded + * */ + object None : RoundingMode() { + override val showGap: Boolean = false + } + + /** + * In this mode, item should have a rounded top side + * + * @property showGap if true then item should have top padding + * */ + data class Top(override val showGap: Boolean = false) : RoundingMode() + + /** + * In this mode, item should have a rounded bottom side + * + * @property showGap if true then item should have bottom padding + * */ + data class Bottom(override val showGap: Boolean = false) : RoundingMode() + + /** + * In this mode, item should have a rounded all sides + * + * @property showGap if true then item should have top and bottom padding + * */ + data class All(override val showGap: Boolean = false) : RoundingMode() + } + + /** + * Update item [RoundingMode] + * + * @param mode new [RoundingMode] + * + * @return updated [DraggableItem] + * */ + fun updateRoundingMode(mode: RoundingMode): DraggableItem = when (this) { + is GroupPlaceholder -> this + is GroupHeader -> this.copy(roundingMode = mode) + is Token -> this.copy(roundingMode = mode) + } + + /** + * Update item shadow visibility + * + * @param show if true then item should be elevated + * + * @return updated [DraggableItem] + * */ + fun updateShadowVisibility(show: Boolean): DraggableItem = when (this) { + is GroupPlaceholder -> this + is GroupHeader -> this.copy(showShadow = show) + is Token -> this.copy(showShadow = show) + } +} \ No newline at end of file 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 new file mode 100644 index 0000000000..3f87cab410 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/model/OrganizeTokensListState.kt @@ -0,0 +1,22 @@ +package com.tangem.feature.wallet.presentation.organizetokens.model + +import androidx.compose.runtime.Immutable +import kotlinx.collections.immutable.PersistentList +import kotlinx.collections.immutable.persistentListOf + +@Immutable +internal sealed class OrganizeTokensListState { + abstract val items: PersistentList + + data class GroupedByNetwork( + override val items: PersistentList, + ) : OrganizeTokensListState() + + data class Ungrouped( + override val items: PersistentList, + ) : OrganizeTokensListState() + + object Empty : OrganizeTokensListState() { + override val items: PersistentList = persistentListOf() + } +} \ 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 new file mode 100644 index 0000000000..30d8e846c1 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/model/OrganizeTokensState.kt @@ -0,0 +1,36 @@ +package com.tangem.feature.wallet.presentation.organizetokens.model + +import androidx.compose.runtime.Immutable +import org.burnoutcrew.reorderable.ItemPosition + +@Immutable +internal data class OrganizeTokensState( + val onBackClick: () -> Unit, + val itemsState: OrganizeTokensListState, + val header: HeaderConfig, + val actions: ActionsConfig, + val dndConfig: DragAndDropConfig, +) { + + data class HeaderConfig( + val isEnabled: Boolean = false, + val isSortedByBalance: Boolean = false, + val isGrouped: Boolean = false, + val onSortClick: () -> Unit, + val onGroupClick: () -> Unit, + ) + + data class ActionsConfig( + val canApply: Boolean = false, + val showApplyProgress: Boolean = false, + val onApplyClick: () -> Unit, + val onCancelClick: () -> Unit, + ) + + data class DragAndDropConfig( + val onItemDragged: (ItemPosition, ItemPosition) -> Unit, + val canDragItemOver: (ItemPosition, ItemPosition) -> Boolean, + val onItemDragEnd: () -> Unit, + val onDragStart: (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 new file mode 100644 index 0000000000..71690752fb --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/CryptoCurrenciesIdsResolver.kt @@ -0,0 +1,32 @@ +package com.tangem.feature.wallet.presentation.organizetokens.utils + +import com.tangem.domain.tokens.model.TokenList +import com.tangem.domain.tokens.models.CryptoCurrency +import com.tangem.feature.wallet.presentation.organizetokens.model.DraggableItem +import com.tangem.feature.wallet.presentation.organizetokens.model.OrganizeTokensListState + +internal class CryptoCurrenciesIdsResolver { + + fun resolve(listState: OrganizeTokensListState, tokenList: TokenList?): List { + val draggableTokens = when (listState) { + is OrganizeTokensListState.Empty -> return emptyList() + is OrganizeTokensListState.GroupedByNetwork -> listState.items.filterIsInstance() + is OrganizeTokensListState.Ungrouped -> listState.items + } + val currenciesStatuses = when (tokenList) { + is TokenList.GroupedByNetwork -> tokenList.groups.flatMap { it.currencies } + is TokenList.Ungrouped -> tokenList.currencies + is TokenList.NotInitialized, + null, + -> return emptyList() + } + + return draggableTokens.mapNotNull { draggableToken -> + val currencyStatus = currenciesStatuses.firstOrNull { + it.currency.id.value == draggableToken.id + } + + currencyStatus?.currency?.id + } + } +} \ No newline at end of file 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 new file mode 100644 index 0000000000..acfcac13b7 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/common/DraggableItemOperations.kt @@ -0,0 +1,7 @@ +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()}") +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/DraggableItemsOperations.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/common/DraggableItemsOperations.kt similarity index 77% rename from features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/DraggableItemsOperations.kt rename to features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/common/DraggableItemsOperations.kt index 9bad24193c..74ada001e8 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/DraggableItemsOperations.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/common/DraggableItemsOperations.kt @@ -1,6 +1,6 @@ -package com.tangem.feature.wallet.presentation.organizetokens.utils +package com.tangem.feature.wallet.presentation.organizetokens.utils.common -import com.tangem.feature.wallet.presentation.organizetokens.DraggableItem +import com.tangem.feature.wallet.presentation.organizetokens.model.DraggableItem import kotlinx.collections.immutable.PersistentList import org.burnoutcrew.reorderable.ItemPosition @@ -59,13 +59,15 @@ internal fun PersistentList.moveItem(fromIndex: Int, toIndex: Int internal fun List.divideItems(movingItem: DraggableItem): List { return this.map { it - .roundingMode(DraggableItem.RoundingMode.All(showGap = true)) - .showShadow(show = it.id == movingItem.id) + .updateRoundingMode(DraggableItem.RoundingMode.All(showGap = true)) + .updateShadowVisibility(show = it.id == movingItem.id) } } -internal fun List.uniteItems(): List { +@Suppress("UNCHECKED_CAST") // Erased type +internal fun List.uniteItems(): List { val lastItemIndex = this.lastIndex + return this.mapIndexed { index, item -> val mode = when (index) { 0 -> DraggableItem.RoundingMode.Top() @@ -74,9 +76,9 @@ internal fun List.uniteItems(): List { } item - .roundingMode(mode) - .showShadow(show = false) - } + .updateRoundingMode(mode) + .updateShadowVisibility(show = false) + } as List } // TODO: Move to domain @@ -131,51 +133,51 @@ internal fun List.divideGroups(movingItem: DraggableItem): List { item - .roundingMode(DraggableItem.RoundingMode.All(showGap = true)) - .showShadow(show = true) + .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 - .roundingMode(DraggableItem.RoundingMode.All(showGap = true)) - .showShadow(show = true) + .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 - .roundingMode(DraggableItem.RoundingMode.All(showGap = true)) - .showShadow(show = false) + .updateRoundingMode(DraggableItem.RoundingMode.All(showGap = true)) + .updateShadowVisibility(show = false) } // Case when current item is the first item in the list index == 0 -> { item - .roundingMode(DraggableItem.RoundingMode.Top()) - .showShadow(show = false) + .updateRoundingMode(DraggableItem.RoundingMode.Top()) + .updateShadowVisibility(show = false) } // Case when current item is the last item in the list index == lastItemIndex -> { item - .roundingMode(DraggableItem.RoundingMode.Bottom()) - .showShadow(show = false) + .updateRoundingMode(DraggableItem.RoundingMode.Bottom()) + .updateShadowVisibility(show = false) } // Case when previous item is a GroupPlaceholder this[index - 1] is DraggableItem.GroupPlaceholder -> { item - .roundingMode(DraggableItem.RoundingMode.Top(showGap = true)) - .showShadow(show = false) + .updateRoundingMode(DraggableItem.RoundingMode.Top(showGap = true)) + .updateShadowVisibility(show = false) } // Case when next item is a GroupPlaceholder this[index + 1] is DraggableItem.GroupPlaceholder -> { item - .roundingMode(DraggableItem.RoundingMode.Bottom(showGap = true)) - .showShadow(show = false) + .updateRoundingMode(DraggableItem.RoundingMode.Bottom(showGap = true)) + .updateShadowVisibility(show = false) } // Default case when none of the above conditions are met else -> { item - .roundingMode(DraggableItem.RoundingMode.None) - .showShadow(show = false) + .updateRoundingMode(DraggableItem.RoundingMode.None) + .updateShadowVisibility(show = false) } } } 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 new file mode 100644 index 0000000000..e28e32c3a8 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/common/IdsOperations.kt @@ -0,0 +1,8 @@ +package com.tangem.feature.wallet.presentation.organizetokens.utils.common + +import com.tangem.domain.tokens.models.CryptoCurrency +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 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 new file mode 100644 index 0000000000..4c041f01fe --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/common/OrganiseTokensListStateOperations.kt @@ -0,0 +1,19 @@ +package com.tangem.feature.wallet.presentation.organizetokens.utils.common + +import com.tangem.feature.wallet.presentation.organizetokens.model.DraggableItem +import com.tangem.feature.wallet.presentation.organizetokens.model.OrganizeTokensListState +import kotlinx.collections.immutable.PersistentList +import kotlinx.collections.immutable.toPersistentList + +@Suppress("UNCHECKED_CAST") +internal inline fun OrganizeTokensListState.updateItems( + update: (PersistentList) -> List, +): OrganizeTokensListState { + val updatedItems = update(items).toPersistentList() + + return when (this) { + is OrganizeTokensListState.GroupedByNetwork -> copy(items = updatedItems) + is OrganizeTokensListState.Ungrouped -> copy(items = updatedItems as PersistentList) + 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/common/TokenListOperations.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/common/TokenListOperations.kt new file mode 100644 index 0000000000..4f1478d2bf --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/common/TokenListOperations.kt @@ -0,0 +1,14 @@ +package com.tangem.feature.wallet.presentation.organizetokens.utils.common + +import com.tangem.domain.tokens.model.TokenList +import com.tangem.domain.tokens.model.TokenList.SortType + +internal fun TokenList.updateSorting(isSortedByBalance: Boolean): TokenList { + val sortType = if (isSortedByBalance) SortType.BALANCE else SortType.NONE + + return when (this) { + is TokenList.GroupedByNetwork -> this.copy(sortedBy = sortType) + is TokenList.Ungrouped -> this.copy(sortedBy = sortType) + is TokenList.NotInitialized -> this + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/InProgressStateConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/InProgressStateConverter.kt new file mode 100644 index 0000000000..4c26e09a29 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/InProgressStateConverter.kt @@ -0,0 +1,23 @@ +package com.tangem.feature.wallet.presentation.organizetokens.utils.converter + +import com.tangem.feature.wallet.presentation.organizetokens.model.OrganizeTokensState +import com.tangem.utils.converter.TwoWayConverter + +internal class InProgressStateConverter : TwoWayConverter { + + override fun convert(value: OrganizeTokensState): OrganizeTokensState { + return value.copy( + actions = value.actions.copy( + showApplyProgress = true, + ), + ) + } + + override fun convertBack(value: OrganizeTokensState): OrganizeTokensState { + return value.copy( + actions = value.actions.copy( + showApplyProgress = false, + ), + ) + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/TokenListToStateConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/TokenListToStateConverter.kt new file mode 100644 index 0000000000..613d5e3dfc --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/TokenListToStateConverter.kt @@ -0,0 +1,31 @@ +package com.tangem.feature.wallet.presentation.organizetokens.utils.converter + +import com.tangem.common.Provider +import com.tangem.domain.tokens.model.TokenList +import com.tangem.feature.wallet.presentation.organizetokens.model.OrganizeTokensListState +import com.tangem.feature.wallet.presentation.organizetokens.model.OrganizeTokensState +import com.tangem.feature.wallet.presentation.organizetokens.utils.converter.items.TokenListToListStateConverter +import com.tangem.utils.converter.Converter + +internal class TokenListToStateConverter( + private val currentState: Provider, + private val itemsConverter: TokenListToListStateConverter, +) : Converter { + + override fun convert(value: TokenList): OrganizeTokensState { + val state = currentState() + val itemsState = itemsConverter.convert(value) + + return state.copy( + itemsState = itemsState, + header = state.header.copy( + isEnabled = itemsState !is OrganizeTokensListState.Empty, + isSortedByBalance = value.sortedBy == TokenList.SortType.BALANCE, + isGrouped = value is TokenList.GroupedByNetwork, + ), + actions = state.actions.copy( + canApply = itemsState !is OrganizeTokensListState.Empty, + ), + ) + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/error/TokenListErrorConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/error/TokenListErrorConverter.kt new file mode 100644 index 0000000000..53f553015f --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/error/TokenListErrorConverter.kt @@ -0,0 +1,17 @@ +package com.tangem.feature.wallet.presentation.organizetokens.utils.converter.error + +import com.tangem.common.Provider +import com.tangem.domain.tokens.error.TokenListError +import com.tangem.feature.wallet.presentation.organizetokens.model.OrganizeTokensState +import com.tangem.feature.wallet.presentation.organizetokens.utils.converter.InProgressStateConverter +import com.tangem.utils.converter.Converter + +internal class TokenListErrorConverter( + private val currentState: Provider, + private val inProgressStateConverter: InProgressStateConverter, +) : Converter { + + override fun convert(value: TokenListError): OrganizeTokensState { + return inProgressStateConverter.convertBack(currentState()) + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/error/TokenListSortingErrorConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/error/TokenListSortingErrorConverter.kt new file mode 100644 index 0000000000..e738cdada5 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/error/TokenListSortingErrorConverter.kt @@ -0,0 +1,17 @@ +package com.tangem.feature.wallet.presentation.organizetokens.utils.converter.error + +import com.tangem.common.Provider +import com.tangem.domain.tokens.error.TokenListSortingError +import com.tangem.feature.wallet.presentation.organizetokens.model.OrganizeTokensState +import com.tangem.feature.wallet.presentation.organizetokens.utils.converter.InProgressStateConverter +import com.tangem.utils.converter.Converter + +internal class TokenListSortingErrorConverter( + private val currentState: Provider, + private val inProgressStateConverter: InProgressStateConverter, +) : Converter { + + override fun convert(value: TokenListSortingError): OrganizeTokensState { + return inProgressStateConverter.convertBack(currentState()) + } +} \ 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 new file mode 100644 index 0000000000..5284855142 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/items/CryptoCurrencyToDraggableItemConverter.kt @@ -0,0 +1,73 @@ +package com.tangem.feature.wallet.presentation.organizetokens.utils.converter.items + +import androidx.annotation.DrawableRes +import com.tangem.common.Provider +import com.tangem.core.ui.utils.BigDecimalFormatter +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.tokens.models.CryptoCurrency +import com.tangem.feature.wallet.impl.R +import com.tangem.feature.wallet.presentation.common.state.TokenItemState +import com.tangem.feature.wallet.presentation.organizetokens.model.DraggableItem +import com.tangem.feature.wallet.presentation.organizetokens.utils.common.getGroupHeaderId +import com.tangem.feature.wallet.presentation.organizetokens.utils.common.getTokenItemId +import com.tangem.utils.converter.Converter + +internal class CryptoCurrencyToDraggableItemConverter( + private val appCurrencyProvider: Provider, +) : Converter { + + private val CryptoCurrency.networkIconResId: Int? + @DrawableRes get() { + // TODO: [REDACTED_JIRA] + return if (this is CryptoCurrency.Coin) null else R.drawable.img_eth_22 + } + + private val CryptoCurrency.tokenIconResId: Int + @DrawableRes get() { + // TODO: [REDACTED_JIRA] + return R.drawable.img_eth_22 + } + + override fun convert(value: CryptoCurrencyStatus): DraggableItem.Token { + return createDraggableToken(value, appCurrencyProvider()) + } + + override fun convertList(input: Collection): List { + val appCurrency = appCurrencyProvider() + + return input.map { createDraggableToken(it, appCurrency) } + } + + private fun createDraggableToken( + currencyStatus: CryptoCurrencyStatus, + appCurrency: AppCurrency, + ): DraggableItem.Token { + return DraggableItem.Token( + tokenItemState = createTokenItemState(currencyStatus, appCurrency), + groupId = getGroupHeaderId(currencyStatus.currency.networkId), + ) + } + + private fun createTokenItemState( + currencyStatus: CryptoCurrencyStatus, + appCurrency: AppCurrency, + ): TokenItemState.Draggable { + val currency = currencyStatus.currency + + return TokenItemState.Draggable( + id = getTokenItemId(currency.id), + tokenIconUrl = currency.iconUrl, + tokenIconResId = currency.tokenIconResId, + networkIconResId = currency.networkIconResId, + name = currency.name, + fiatAmount = getFormattedFiatAmount(currencyStatus, appCurrency), + ) + } + + private fun getFormattedFiatAmount(currency: CryptoCurrencyStatus, appCurrency: AppCurrency): String { + val fiatAmount = currency.value.fiatAmount ?: return BigDecimalFormatter.EMPTY_BALANCE_SIGN + + return BigDecimalFormatter.formatFiatAmount(fiatAmount, appCurrency.code, appCurrency.symbol) + } +} \ No newline at end of file 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 new file mode 100644 index 0000000000..ddd09e1da9 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/items/NetworkGroupToDraggableItemsConverter.kt @@ -0,0 +1,41 @@ +package com.tangem.feature.wallet.presentation.organizetokens.utils.converter.items + +import com.tangem.domain.tokens.model.NetworkGroup +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.getGroupPlaceholder +import com.tangem.utils.converter.Converter + +internal class NetworkGroupToDraggableItemsConverter( + private val itemConverter: CryptoCurrencyToDraggableItemConverter, +) : Converter> { + + override fun convert(value: NetworkGroup): List { + return buildList { + add(createGroupHeader(value)) + addAll(createTokens(value)) + } + } + + override fun convertList(input: Collection): List> { + val lastItemIndex = input.size - 1 + + return input.mapIndexed { index, networkGroup -> + convert(networkGroup).toMutableList() + .also { mutableGroup -> + if (index != lastItemIndex) { + mutableGroup.add(getGroupPlaceholder(index)) + } + } + } + } + + private fun createGroupHeader(group: NetworkGroup) = DraggableItem.GroupHeader( + id = getGroupHeaderId(group.network.id), + networkName = group.network.name, + ) + + private fun createTokens(group: NetworkGroup): List { + return itemConverter.convertList(group.currencies.toList()) + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/items/TokenListToListStateConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/items/TokenListToListStateConverter.kt new file mode 100644 index 0000000000..bb1b829d47 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/items/TokenListToListStateConverter.kt @@ -0,0 +1,42 @@ +package com.tangem.feature.wallet.presentation.organizetokens.utils.converter.items + +import com.tangem.domain.tokens.model.TokenList +import com.tangem.feature.wallet.presentation.organizetokens.model.OrganizeTokensListState +import com.tangem.feature.wallet.presentation.organizetokens.utils.common.uniteItems +import com.tangem.utils.converter.Converter +import kotlinx.collections.immutable.toPersistentList + +internal class TokenListToListStateConverter( + private val groupsConverter: NetworkGroupToDraggableItemsConverter, + private val tokensConverter: CryptoCurrencyToDraggableItemConverter, +) : Converter { + + override fun convert(value: TokenList): OrganizeTokensListState { + return when (value) { + is TokenList.GroupedByNetwork -> createListState(value) + is TokenList.Ungrouped -> createListState(value) + is TokenList.NotInitialized -> createEmptyListState() + } + } + + private fun createListState(tokenList: TokenList.GroupedByNetwork): OrganizeTokensListState.GroupedByNetwork { + return OrganizeTokensListState.GroupedByNetwork( + items = groupsConverter.convertList(tokenList.groups) + .flatten() + .uniteItems() + .toPersistentList(), + ) + } + + private fun createListState(tokenList: TokenList.Ungrouped): OrganizeTokensListState.Ungrouped { + return OrganizeTokensListState.Ungrouped( + items = tokensConverter.convertList(tokenList.currencies) + .uniteItems() + .toPersistentList(), + ) + } + + private fun createEmptyListState(): OrganizeTokensListState.Empty { + return OrganizeTokensListState.Empty + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/DefaultWalletRouter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/DefaultWalletRouter.kt index de6989f860..84ca2ae72b 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/DefaultWalletRouter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/DefaultWalletRouter.kt @@ -1,12 +1,15 @@ package com.tangem.feature.wallet.presentation.router -import androidx.compose.foundation.layout.systemBarsPadding +import androidx.compose.foundation.layout.statusBarsPadding import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalLifecycleOwner +import androidx.core.os.bundleOf import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentManager import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation.NavHostController import androidx.navigation.NavType import androidx.navigation.compose.NavHost @@ -16,12 +19,14 @@ import androidx.navigation.navArgument import com.tangem.core.navigation.AppScreen import com.tangem.core.navigation.NavigationAction import com.tangem.core.navigation.NavigationStateHolder +import com.tangem.domain.tokens.models.CryptoCurrency import com.tangem.domain.wallets.models.UserWalletId import com.tangem.feature.wallet.presentation.WalletFragment import com.tangem.feature.wallet.presentation.organizetokens.OrganizeTokensScreen import com.tangem.feature.wallet.presentation.organizetokens.OrganizeTokensViewModel import com.tangem.feature.wallet.presentation.wallet.ui.WalletScreen import com.tangem.feature.wallet.presentation.wallet.viewmodels.WalletViewModel +import com.tangem.features.tokendetails.navigation.TokenDetailsRouter import kotlin.properties.Delegates /** Default implementation of wallet feature router */ @@ -42,7 +47,7 @@ internal class DefaultWalletRouter(private val navigationStateHolder: Navigation ) { composable(WalletRoute.Wallet.route) { val viewModel = hiltViewModel().apply { router = this@DefaultWalletRouter } - LocalLifecycleOwner.current.lifecycle.addObserver(observer = viewModel) + LocalLifecycleOwner.current.lifecycle.addObserver(viewModel) WalletScreen(state = viewModel.uiState) } @@ -56,9 +61,11 @@ internal class DefaultWalletRouter(private val navigationStateHolder: Navigation router = this@DefaultWalletRouter } + val uiState by viewModel.uiState.collectAsStateWithLifecycle() + OrganizeTokensScreen( - modifier = Modifier.systemBarsPadding(), - state = viewModel.uiState, + modifier = Modifier.statusBarsPadding(), + state = uiState, ) } } @@ -83,6 +90,8 @@ internal class DefaultWalletRouter(private val navigationStateHolder: Navigation } override fun openDetailsScreen() { + // FIXME: Prepare details screen (e.g. dispatch action: `DetailsAction.PrepareScreen`) + // [REDACTED_JIRA] navigationStateHolder.navigate(action = NavigationAction.NavigateTo(AppScreen.Details)) } @@ -94,6 +103,16 @@ internal class DefaultWalletRouter(private val navigationStateHolder: Navigation navigationStateHolder.navigate(action = NavigationAction.OpenUrl(url)) } + override fun openTokenDetails(currency: CryptoCurrency) { + navigationStateHolder.navigate( + action = NavigationAction.NavigateTo( + screen = AppScreen.WalletDetails, + // TODO: [REDACTED_JIRA] + bundle = bundleOf(TokenDetailsRouter.SELECTED_CURRENCY_KEY to currency), + ), + ) + } + private companion object { const val BACKSTACK_ENTRY_COUNT_TO_CLOSE_WALLET_SCREEN = 2 } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/InnerWalletRouter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/InnerWalletRouter.kt index 5fab6367de..4a0d24bbaa 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/InnerWalletRouter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/InnerWalletRouter.kt @@ -3,6 +3,7 @@ package com.tangem.feature.wallet.presentation.router import androidx.compose.runtime.Composable import androidx.compose.runtime.Stable import androidx.fragment.app.FragmentManager +import com.tangem.domain.tokens.models.CryptoCurrency import com.tangem.domain.wallets.models.UserWalletId import com.tangem.features.wallet.navigation.WalletRouter @@ -40,4 +41,7 @@ internal interface InnerWalletRouter : WalletRouter { /** Open transaction history website by [url] */ fun openTxHistoryWebsite(url: String) + + /** Open token details screen */ + fun openTokenDetails(currency: CryptoCurrency) } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/WalletImageResolver.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/WalletImageResolver.kt index 2dd09d7535..c54155dd33 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/WalletImageResolver.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/WalletImageResolver.kt @@ -42,7 +42,7 @@ internal object WalletImageResolver { return when (cardTypesResolver.getBlockchain()) { Blockchain.Bitcoin -> R.drawable.ill_note_btc_120_106 Blockchain.Ethereum -> R.drawable.ill_note_ethereum_120_106 - Blockchain.Binance -> R.drawable.ill_note_binance_120_106 + Blockchain.BSC -> R.drawable.ill_note_binance_120_106 Blockchain.Dogecoin -> R.drawable.ill_note_doge_120_106 Blockchain.Cardano -> R.drawable.ill_note_cardano_120_106 Blockchain.XRP -> R.drawable.ill_note_xrp_120_106 diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletLockedState.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletLockedState.kt new file mode 100644 index 0000000000..dd3bbbeee7 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletLockedState.kt @@ -0,0 +1,32 @@ +package com.tangem.feature.wallet.presentation.wallet.state + +/** + * Locked wallet state + * +[REDACTED_AUTHOR] + */ +internal sealed interface WalletLockedState { + + /** Lambda be invoked when unlock wallet notification is clicked */ + val onUnlockWalletsNotificationClick: () -> Unit + + /** Lambda be invoked when unlock wallet button is clicked */ + val onUnlockClick: () -> Unit + + /** Lambda be invoked when scan button is clicked */ + val onScanClick: () -> Unit + + /** Bottom sheet visibility */ + val isBottomSheetShow: Boolean + + /** Lambda be invoked when bottom sheet is dismissed */ + val onBottomSheetDismiss: () -> Unit + + /** Get selected wallet index */ + fun getSelectedWalletIndex(): Int { + return when (this) { + is WalletMultiCurrencyState.Locked -> walletsListConfig.selectedWalletIndex + is WalletSingleCurrencyState.Locked -> walletsListConfig.selectedWalletIndex + } + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletMultiCurrencyState.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletMultiCurrencyState.kt new file mode 100644 index 0000000000..87d675cd95 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletMultiCurrencyState.kt @@ -0,0 +1,54 @@ +package com.tangem.feature.wallet.presentation.wallet.state + +import com.tangem.feature.wallet.presentation.wallet.state.components.* +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf + +/** + * Multi currency wallet state + * +[REDACTED_AUTHOR] + */ +internal sealed class WalletMultiCurrencyState : WalletState.ContentState() { + + /** Tokens list state */ + abstract val tokensListState: WalletTokensListState + + data class Content( + override val onBackClick: () -> Unit, + override val topBarConfig: WalletTopBarConfig, + override val walletsListConfig: WalletsListConfig, + override val pullToRefreshConfig: WalletPullToRefreshConfig, + override val notifications: ImmutableList, + override val bottomSheetConfig: WalletBottomSheetConfig?, + override val tokensListState: WalletTokensListState, + ) : WalletMultiCurrencyState() + + data class Locked( + override val onBackClick: () -> Unit, + override val topBarConfig: WalletTopBarConfig, + override val walletsListConfig: WalletsListConfig, + override val pullToRefreshConfig: WalletPullToRefreshConfig, + override val onUnlockWalletsNotificationClick: () -> Unit, + override val onUnlockClick: () -> Unit, + override val onScanClick: () -> Unit, + override val isBottomSheetShow: Boolean = false, + override val onBottomSheetDismiss: () -> Unit = {}, + ) : WalletMultiCurrencyState(), WalletLockedState { + + override val notifications = persistentListOf( + WalletNotification.UnlockWallets(onUnlockWalletsNotificationClick), + ) + + override val bottomSheetConfig = WalletBottomSheetConfig( + isShow = isBottomSheetShow, + onDismissRequest = onBottomSheetDismiss, + content = WalletBottomSheetConfig.BottomSheetContentConfig.UnlockWallets( + onUnlockClick = onUnlockClick, + onScanClick = onScanClick, + ), + ) + + override val tokensListState: WalletTokensListState = WalletTokensListState.Locked + } +} \ No newline at end of file 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 new file mode 100644 index 0000000000..8433b8d9f7 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletSingleCurrencyState.kt @@ -0,0 +1,68 @@ +package com.tangem.feature.wallet.presentation.wallet.state + +import com.tangem.core.ui.components.marketprice.MarketPriceBlockState +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.persistentListOf + +/** + * Single currency wallet content state + * +[REDACTED_AUTHOR] + */ +internal sealed class WalletSingleCurrencyState : WalletState.ContentState() { + + /** Manage buttons */ + abstract val buttons: ImmutableList + + /** Market price block state */ + abstract val marketPriceBlockState: MarketPriceBlockState? + + /** Transactions history state */ + abstract val txHistoryState: TxHistoryState + + data class Content( + override val onBackClick: () -> Unit, + override val topBarConfig: WalletTopBarConfig, + override val walletsListConfig: WalletsListConfig, + override val pullToRefreshConfig: WalletPullToRefreshConfig, + override val notifications: ImmutableList, + override val bottomSheetConfig: WalletBottomSheetConfig?, + override val buttons: ImmutableList, + override val marketPriceBlockState: MarketPriceBlockState, + override val txHistoryState: TxHistoryState, + ) : WalletSingleCurrencyState() + + data class Locked( + override val onBackClick: () -> Unit, + override val topBarConfig: WalletTopBarConfig, + override val walletsListConfig: WalletsListConfig, + override val pullToRefreshConfig: WalletPullToRefreshConfig, + override val buttons: ImmutableList, + override val onUnlockWalletsNotificationClick: () -> Unit, + override val onUnlockClick: () -> Unit, + override val onScanClick: () -> Unit, + override val isBottomSheetShow: Boolean = false, + override val onBottomSheetDismiss: () -> Unit = {}, + val onExploreClick: () -> Unit, + ) : WalletSingleCurrencyState(), WalletLockedState { + + override val notifications = persistentListOf( + WalletNotification.UnlockWallets(onUnlockWalletsNotificationClick), + ) + + override val bottomSheetConfig = WalletBottomSheetConfig( + isShow = isBottomSheetShow, + onDismissRequest = onBottomSheetDismiss, + content = WalletBottomSheetConfig.BottomSheetContentConfig.UnlockWallets( + onUnlockClick = onUnlockClick, + onScanClick = onScanClick, + ), + ) + + override val marketPriceBlockState = null + + override val txHistoryState: TxHistoryState = TxHistoryState.Locked(onExploreClick) + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletState.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletState.kt new file mode 100644 index 0000000000..9cc1f3fa27 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletState.kt @@ -0,0 +1,55 @@ +package com.tangem.feature.wallet.presentation.wallet.state + +import com.tangem.feature.wallet.presentation.wallet.state.components.* +import kotlinx.collections.immutable.ImmutableList + +/** + * Wallet screen state + * +[REDACTED_AUTHOR] + */ +internal sealed class WalletState { + + /** Lambda be invoked when back button is clicked */ + abstract val onBackClick: () -> Unit + + /** Wallet screen content state */ + sealed class ContentState : WalletState() { + + /** Top bar config */ + abstract val topBarConfig: WalletTopBarConfig + + /** Wallets list config */ + abstract val walletsListConfig: WalletsListConfig + + /** Pull to refresh config */ + abstract val pullToRefreshConfig: WalletPullToRefreshConfig + + /** Notifications */ + abstract val notifications: ImmutableList + + /** Bottom sheet config */ + abstract val bottomSheetConfig: WalletBottomSheetConfig? + + /** + * Util function that allow to make a copy + * + * @param walletsListConfig wallets list config + */ + fun copySealed(walletsListConfig: WalletsListConfig = this.walletsListConfig): ContentState { + return when (this) { + is WalletMultiCurrencyState.Content -> copy(walletsListConfig = walletsListConfig) + is WalletMultiCurrencyState.Locked -> copy(walletsListConfig = walletsListConfig) + is WalletSingleCurrencyState.Content -> copy(walletsListConfig = walletsListConfig) + is WalletSingleCurrencyState.Locked -> copy(walletsListConfig = walletsListConfig) + } + } + } + + /** + * Initial state + * + * @property onBackClick lambda be invoked when back button is clicked + */ + data class Initial(override val onBackClick: () -> Unit) : WalletState() +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletStateHolder.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletStateHolder.kt deleted file mode 100644 index 216dc94148..0000000000 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletStateHolder.kt +++ /dev/null @@ -1,188 +0,0 @@ -package com.tangem.feature.wallet.presentation.wallet.state - -import com.tangem.core.ui.components.buttons.actions.ActionButtonConfig -import com.tangem.core.ui.components.marketprice.MarketPriceBlockState -import com.tangem.domain.wallets.models.UserWalletId -import com.tangem.feature.wallet.presentation.wallet.state.content.WalletLockedContentState -import com.tangem.feature.wallet.presentation.wallet.state.content.WalletTokensListState -import com.tangem.feature.wallet.presentation.wallet.state.content.WalletTxHistoryState -import kotlinx.collections.immutable.ImmutableList -import kotlinx.collections.immutable.persistentListOf - -/** - * Wallet screen state holder - * - * @property onBackClick lambda be invoked when back button is clicked - * @property topBarConfig top bar config - * @property walletsListConfig wallets list config - * @property pullToRefreshConfig pull to refresh config - * @property notifications notifications - * -[REDACTED_AUTHOR] - */ -internal sealed class WalletStateHolder( - open val onBackClick: () -> Unit, - open val topBarConfig: WalletTopBarConfig, - open val walletsListConfig: WalletsListConfig, - open val pullToRefreshConfig: WalletPullToRefreshConfig, - open val notifications: ImmutableList, - open val bottomSheet: WalletBottomSheetConfig? = null, -) { - - fun copySealed( - onBackClick: () -> Unit = this.onBackClick, - topBarConfig: WalletTopBarConfig = this.topBarConfig, - walletsListConfig: WalletsListConfig = this.walletsListConfig, - pullToRefreshConfig: WalletPullToRefreshConfig = this.pullToRefreshConfig, - notifications: ImmutableList = this.notifications, - bottomSheet: WalletBottomSheetConfig? = this.bottomSheet, - ): WalletStateHolder { - return when (this) { - is MultiCurrencyContent -> this.copy( - onBackClick = onBackClick, - topBarConfig = topBarConfig, - walletsListConfig = walletsListConfig, - pullToRefreshConfig = pullToRefreshConfig, - notifications = notifications, - bottomSheet = bottomSheet, - ) - is SingleCurrencyContent -> this.copy( - onBackClick = onBackClick, - topBarConfig = topBarConfig, - walletsListConfig = walletsListConfig, - pullToRefreshConfig = pullToRefreshConfig, - notifications = notifications, - bottomSheet = bottomSheet, - ) - is UnlockWalletContent -> this.copy( - onBackClick = onBackClick, - topBarConfig = topBarConfig, - walletsListConfig = walletsListConfig, - pullToRefreshConfig = pullToRefreshConfig, - ) - is Loading -> copy(onBackClick = onBackClick) - } - } - - /** - * Multi currency wallet content state - * - * @property onBackClick lambda be invoked when back button is clicked - * @property topBarConfig top bar config - * @property walletsListConfig wallets list config - * @property pullToRefreshConfig pull to refresh config - * @property tokensListState token list state - * @property notifications notifications - */ - data class MultiCurrencyContent( - override val onBackClick: () -> Unit, - override val topBarConfig: WalletTopBarConfig, - override val walletsListConfig: WalletsListConfig, - override val pullToRefreshConfig: WalletPullToRefreshConfig, - override val notifications: ImmutableList, - override val bottomSheet: WalletBottomSheetConfig? = null, - val tokensListState: WalletTokensListState, - ) : WalletStateHolder( - onBackClick = onBackClick, - topBarConfig = topBarConfig, - walletsListConfig = walletsListConfig, - pullToRefreshConfig = pullToRefreshConfig, - notifications = notifications, - bottomSheet = bottomSheet, - ) - - /** - * Single currency wallet content state - * - * @property onBackClick lambda be invoked when back button is clicked - * @property topBarConfig top bar config - * @property walletsListConfig wallets list config - * @property pullToRefreshConfig pull to refresh config - * @property notifications notifications - * @property buttons manage buttons - * @property marketPriceBlockState market price block state - * @property txHistoryState transactions history state - */ - data class SingleCurrencyContent( - override val onBackClick: () -> Unit, - override val topBarConfig: WalletTopBarConfig, - override val walletsListConfig: WalletsListConfig, - override val pullToRefreshConfig: WalletPullToRefreshConfig, - override val notifications: ImmutableList, - override val bottomSheet: WalletBottomSheetConfig? = null, - val buttons: ImmutableList, - val marketPriceBlockState: MarketPriceBlockState, - val txHistoryState: WalletTxHistoryState, - ) : WalletStateHolder( - onBackClick = onBackClick, - topBarConfig = topBarConfig, - walletsListConfig = walletsListConfig, - pullToRefreshConfig = pullToRefreshConfig, - notifications = notifications, - bottomSheet = bottomSheet, - ) - - /** - * Unlock wallet content state - * - * @property onBackClick lambda be invoked when back button is clicked - * @property topBarConfig top bar config - * @property walletsListConfig wallets list config - * @property pullToRefreshConfig pull to refresh config - * @property lockedContentState locked content state - * @property onUnlockWalletsNotificationClick lambda be invoked when unlock wallets notification is clicked - * @property onBottomSheetDismissRequest lambda be invoked when bottom sheet is dismissed - * @property onUnlockClick lambda be invoked when unlock button is clicked - * @property onScanClick lambda be invoked when scan card button is clicked - */ - data class UnlockWalletContent( - override val onBackClick: () -> Unit, - override val topBarConfig: WalletTopBarConfig, - override val walletsListConfig: WalletsListConfig, - override val pullToRefreshConfig: WalletPullToRefreshConfig, - val lockedContentState: WalletLockedContentState, - val onUnlockWalletsNotificationClick: () -> Unit, - val onBottomSheetDismissRequest: () -> Unit, - val onUnlockClick: () -> Unit, - val onScanClick: () -> Unit, - ) : WalletStateHolder( - onBackClick = onBackClick, - topBarConfig = topBarConfig, - walletsListConfig = walletsListConfig, - pullToRefreshConfig = pullToRefreshConfig, - notifications = persistentListOf(WalletNotification.UnlockWallets(onUnlockWalletsNotificationClick)), - bottomSheet = WalletBottomSheetConfig( - isShow = false, - onDismissRequest = onBottomSheetDismissRequest, - content = WalletBottomSheetConfig.BottomSheetContentConfig.UnlockWallets( - onUnlockClick = onUnlockClick, - onScanClick = onScanClick, - ), - ), - ) - - /** - * Loading state - * - * @property onBackClick lambda be invoked when back button is clicked - */ - data class Loading(override val onBackClick: () -> Unit) : WalletStateHolder( - onBackClick = onBackClick, - topBarConfig = WalletTopBarConfig(onScanCardClick = {}, onMoreClick = {}), - walletsListConfig = WalletsListConfig( - selectedWalletIndex = 0, - wallets = persistentListOf( - WalletCardState.Loading( - id = UserWalletId(stringValue = ""), - title = "", - additionalInfo = "", - imageResId = null, - ), - ), - onWalletChange = {}, - ), - pullToRefreshConfig = WalletPullToRefreshConfig(isRefreshing = false, onRefresh = {}), - notifications = persistentListOf(), - bottomSheet = null, - ) -} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletBottomSheetConfig.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/components/WalletBottomSheetConfig.kt similarity index 98% rename from features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletBottomSheetConfig.kt rename to features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/components/WalletBottomSheetConfig.kt index 5a8f12209b..eef9a95ef0 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletBottomSheetConfig.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/components/WalletBottomSheetConfig.kt @@ -1,4 +1,4 @@ -package com.tangem.feature.wallet.presentation.wallet.state +package com.tangem.feature.wallet.presentation.wallet.state.components import androidx.annotation.DrawableRes import androidx.compose.ui.graphics.Color diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletCardState.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/components/WalletCardState.kt similarity index 97% rename from features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletCardState.kt rename to features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/components/WalletCardState.kt index b1f88360b1..2da80de62f 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletCardState.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/components/WalletCardState.kt @@ -1,4 +1,4 @@ -package com.tangem.feature.wallet.presentation.wallet.state +package com.tangem.feature.wallet.presentation.wallet.state.components import androidx.annotation.DrawableRes import androidx.compose.runtime.Immutable diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletManageButton.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/components/WalletManageButton.kt similarity index 58% rename from features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletManageButton.kt rename to features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/components/WalletManageButton.kt index cb07819add..6d6055c045 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletManageButton.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/components/WalletManageButton.kt @@ -1,4 +1,4 @@ -package com.tangem.feature.wallet.presentation.wallet.state +package com.tangem.feature.wallet.presentation.wallet.state.components import com.tangem.core.ui.components.buttons.actions.ActionButtonConfig import com.tangem.core.ui.extensions.TextReference @@ -13,16 +13,34 @@ import com.tangem.feature.wallet.impl.R */ sealed class WalletManageButton(val config: ActionButtonConfig) { + /** Lambda be invoked when manage button is clicked */ + abstract val onClick: (() -> Unit)? + /** * Buy * * @param onClick lambda be invoked when manage button is clicked */ - data class Buy(val onClick: () -> Unit) : WalletManageButton( + data class Buy(override val onClick: (() -> Unit)? = null) : WalletManageButton( config = ActionButtonConfig( text = TextReference.Res(id = R.string.common_buy), iconResId = R.drawable.ic_plus_24, - onClick = onClick, + onClick = onClick ?: {}, + enabled = onClick != null, + ), + ) + + /** + * Sell + * + * @param onClick lambda be invoked when manage button is clicked + */ + data class Sell(override val onClick: (() -> Unit)? = null) : WalletManageButton( + config = ActionButtonConfig( + text = TextReference.Res(id = R.string.common_sell), + iconResId = R.drawable.ic_currency_24, + onClick = onClick ?: {}, + enabled = onClick != null, ), ) @@ -31,11 +49,12 @@ sealed class WalletManageButton(val config: ActionButtonConfig) { * * @param onClick lambda be invoked when manage button is clicked */ - data class Send(val onClick: () -> Unit) : WalletManageButton( + data class Send(override val onClick: (() -> Unit)? = null) : WalletManageButton( config = ActionButtonConfig( text = TextReference.Res(id = R.string.common_send), iconResId = R.drawable.ic_arrow_up_24, - onClick = onClick, + onClick = onClick ?: {}, + enabled = onClick != null, ), ) @@ -44,7 +63,7 @@ sealed class WalletManageButton(val config: ActionButtonConfig) { * * @param onClick lambda be invoked when manage button is clicked */ - data class Receive(val onClick: () -> Unit) : WalletManageButton( + data class Receive(override val onClick: () -> Unit) : WalletManageButton( config = ActionButtonConfig( text = TextReference.Res(id = R.string.common_receive), iconResId = R.drawable.ic_arrow_down_24, @@ -57,11 +76,12 @@ sealed class WalletManageButton(val config: ActionButtonConfig) { * * @param onClick lambda be invoked when manage button is clicked */ - data class Exchange(val onClick: () -> Unit) : WalletManageButton( + data class Exchange(override val onClick: (() -> Unit)? = null) : WalletManageButton( config = ActionButtonConfig( text = TextReference.Res(id = R.string.common_exchange), iconResId = R.drawable.ic_exchange_vertical_24, - onClick = onClick, + onClick = onClick ?: {}, + enabled = onClick != null, ), ) @@ -70,7 +90,7 @@ sealed class WalletManageButton(val config: ActionButtonConfig) { * * @param onClick lambda be invoked when manage button is clicked */ - data class CopyAddress(val onClick: () -> Unit) : WalletManageButton( + data class CopyAddress(override val onClick: () -> Unit) : WalletManageButton( config = ActionButtonConfig( text = TextReference.Res(id = R.string.common_copy_address), iconResId = R.drawable.ic_copy_24, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletNotification.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/components/WalletNotification.kt similarity index 98% rename from features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletNotification.kt rename to features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/components/WalletNotification.kt index 842f92ccfe..99a59df14d 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletNotification.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/components/WalletNotification.kt @@ -1,4 +1,4 @@ -package com.tangem.feature.wallet.presentation.wallet.state +package com.tangem.feature.wallet.presentation.wallet.state.components import com.tangem.core.ui.components.notifications.NotificationState import com.tangem.core.ui.extensions.TextReference diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletPullToRefreshConfig.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/components/WalletPullToRefreshConfig.kt similarity index 78% rename from features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletPullToRefreshConfig.kt rename to features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/components/WalletPullToRefreshConfig.kt index 8aafa384f0..6714af85cc 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletPullToRefreshConfig.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/components/WalletPullToRefreshConfig.kt @@ -1,4 +1,4 @@ -package com.tangem.feature.wallet.presentation.wallet.state +package com.tangem.feature.wallet.presentation.wallet.state.components /** * Wallet screen top bar config 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 new file mode 100644 index 0000000000..1209a2da37 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/components/WalletTokensListState.kt @@ -0,0 +1,85 @@ +package com.tangem.feature.wallet.presentation.wallet.state.components + +import com.tangem.core.ui.components.wallet.WalletLockedContentState +import com.tangem.core.ui.extensions.TextReference +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 + +/** + * Wallet tokens list state + * +[REDACTED_AUTHOR] + */ +internal sealed class WalletTokensListState { + + /** Empty token list state */ + object Empty : 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)?, + ) : WalletTokensListState() + + /** Loading content state */ + object Loading : ContentState( + items = persistentListOf( + TokensListItemState.Token(state = TokenItemState.Loading(id = FIRST_LOADING_TOKEN_ID)), + TokensListItemState.Token(state = TokenItemState.Loading(id = SECOND_LOADING_TOKEN_ID)), + ), + onOrganizeTokensClick = null, + ) + + /** + * Content state + * + * @property items content items + * @property onOrganizeTokensClick lambda be invoked when organize tokens button is clicked + */ + data class Content( + override val items: ImmutableList, + override val onOrganizeTokensClick: (() -> Unit)?, + ) : ContentState(items, onOrganizeTokensClick) + + /** Locked content state */ + object Locked : + ContentState( + items = persistentListOf( + TokensListItemState.NetworkGroupTitle(value = TextReference.Res(id = R.string.main_tokens)), + TokensListItemState.Token(state = TokenItemState.Loading(id = LOCKED_TOKEN_ID)), + ), + onOrganizeTokensClick = null, + ), + WalletLockedContentState + + /** Tokens list item state */ + sealed interface TokensListItemState { + + /** + * Network group title item + * + * @property value network name + */ + data class NetworkGroupTitle(val value: TextReference) : TokensListItemState + + /** + * Token item + * + * @property state token item state + */ + data class Token(val state: TokenItemState) : TokensListItemState + } + + private companion object { + const val FIRST_LOADING_TOKEN_ID = "Loading#1" + const val SECOND_LOADING_TOKEN_ID = "Loading#2" + const val LOCKED_TOKEN_ID = "Locked#1" + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletTopBarConfig.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/components/WalletTopBarConfig.kt similarity index 80% rename from features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletTopBarConfig.kt rename to features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/components/WalletTopBarConfig.kt index f1838ea250..52e984f940 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletTopBarConfig.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/components/WalletTopBarConfig.kt @@ -1,4 +1,4 @@ -package com.tangem.feature.wallet.presentation.wallet.state +package com.tangem.feature.wallet.presentation.wallet.state.components /** * Wallet screen top bar config diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletsListConfig.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/components/WalletsListConfig.kt similarity index 86% rename from features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletsListConfig.kt rename to features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/components/WalletsListConfig.kt index 0349eb57bd..20eac2dec5 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletsListConfig.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/components/WalletsListConfig.kt @@ -1,4 +1,4 @@ -package com.tangem.feature.wallet.presentation.wallet.state +package com.tangem.feature.wallet.presentation.wallet.state.components import kotlinx.collections.immutable.ImmutableList diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/content/WalletLockedContentState.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/content/WalletLockedContentState.kt deleted file mode 100644 index 9867aed606..0000000000 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/content/WalletLockedContentState.kt +++ /dev/null @@ -1,9 +0,0 @@ -package com.tangem.feature.wallet.presentation.wallet.state.content - -/** - * Wallet locked content state. - * It allows to divide the locked content of multi-currency and single-currency wallets. - * -[REDACTED_AUTHOR] - */ -internal sealed interface WalletLockedContentState \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/content/WalletTokensListState.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/content/WalletTokensListState.kt deleted file mode 100644 index 4f7e51d4d3..0000000000 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/content/WalletTokensListState.kt +++ /dev/null @@ -1,60 +0,0 @@ -package com.tangem.feature.wallet.presentation.wallet.state.content - -import com.tangem.feature.wallet.presentation.common.state.TokenItemState -import kotlinx.collections.immutable.ImmutableList -import kotlinx.collections.immutable.persistentListOf - -/** - * Wallet tokens list state - * - * @property items content items - * @property onOrganizeTokensClick lambda be invoked when organize tokens button is clicked - * -[REDACTED_AUTHOR] - */ -// TODO: Finalize strings [REDACTED_JIRA] -internal sealed class WalletTokensListState( - open val items: ImmutableList, - open val onOrganizeTokensClick: (() -> Unit)?, -) { - - /** - * Content state - * - * @property items content items - * @property onOrganizeTokensClick lambda be invoked when organize tokens button is clicked - */ - data class Content( - override val items: ImmutableList, - override val onOrganizeTokensClick: (() -> Unit)?, - ) : WalletTokensListState(items, onOrganizeTokensClick) - - /** Locked content state */ - object Locked : - WalletTokensListState( - items = persistentListOf( - TokensListItemState.NetworkGroupTitle(networkName = "Tokens"), - TokensListItemState.Token(state = TokenItemState.Loading), - ), - onOrganizeTokensClick = null, - ), - WalletLockedContentState - - /** Tokens list item state */ - sealed interface TokensListItemState { - - /** - * Network group title item - * - * @property networkName network name - */ - data class NetworkGroupTitle(val networkName: String) : TokensListItemState - - /** - * Token item - * - * @property state token item state - */ - data class Token(val state: TokenItemState) : TokensListItemState - } -} \ 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 293d66e1bc..de50af78d2 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 @@ -2,45 +2,50 @@ package com.tangem.feature.wallet.presentation.wallet.state.factory import arrow.core.Either import com.tangem.common.Provider +import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.common.CardTypesResolver import com.tangem.domain.tokens.error.TokenListError import com.tangem.domain.tokens.model.TokenList -import com.tangem.feature.wallet.presentation.wallet.state.WalletStateHolder +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.TokenListErrorToWalletStateConverter +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 import com.tangem.utils.converter.Converter /** - * Converter from loaded [TokenListError] or [TokenList] to [WalletStateHolder] + * Converter from loaded [TokenListError] or [TokenList] to [WalletMultiCurrencyState] * * @property currentStateProvider current ui state provider * @param cardTypeResolverProvider card type resolver + * @param isLockedWalletProvider current wallet is locked or not provider * @param clickIntents screen click intents * [REDACTED_AUTHOR] */ internal class WalletLoadedTokensListConverter( - private val currentStateProvider: Provider, + private val currentStateProvider: Provider, + appCurrencyProvider: Provider, cardTypeResolverProvider: Provider, + isLockedWalletProvider: Provider, clickIntents: WalletClickIntents, -) : Converter { +) : Converter { private val tokenListStateConverter = TokenListToWalletStateConverter( currentStateProvider = currentStateProvider, cardTypeResolverProvider = cardTypeResolverProvider, + isLockedWalletProvider = isLockedWalletProvider, + appCurrencyProvider = appCurrencyProvider, isWalletContentHidden = false, // TODO: [REDACTED_JIRA] - fiatCurrencyCode = "USD", // TODO: [REDACTED_JIRA] - fiatCurrencySymbol = "$", // TODO: [REDACTED_JIRA] clickIntents = clickIntents, ) - private val tokenListErrorStateConverter = TokenListErrorToWalletStateConverter( + private val tokenListErrorStateConverter = TokenListErrorConverter( currentStateProvider = currentStateProvider, ) - override fun convert(value: LoadedTokensListModel): WalletStateHolder { + override fun convert(value: LoadedTokensListModel): WalletMultiCurrencyState.Content { return value.tokenListEither.fold( ifLeft = tokenListErrorStateConverter::convert, ifRight = { 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 new file mode 100644 index 0000000000..c6d3e7fae9 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletSingleCurrencyLoadedBalanceConverter.kt @@ -0,0 +1,156 @@ +package com.tangem.feature.wallet.presentation.wallet.state.factory + +import arrow.core.Either +import com.tangem.common.Provider +import com.tangem.core.ui.components.marketprice.MarketPriceBlockState +import com.tangem.core.ui.components.marketprice.PriceChangeConfig +import com.tangem.core.ui.utils.BigDecimalFormatter +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.common.CardTypesResolver +import com.tangem.domain.tokens.error.CurrencyError +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.feature.wallet.presentation.wallet.domain.WalletAdditionalInfoFactory +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.WalletsListConfig +import com.tangem.utils.converter.Converter +import kotlinx.collections.immutable.toPersistentList +import java.math.BigDecimal + +internal class WalletSingleCurrencyLoadedBalanceConverter( + private val currentStateProvider: Provider, + private val cardTypeResolverProvider: Provider, + private val appCurrencyProvider: Provider, +) : Converter, WalletSingleCurrencyState.Content> { + + override fun convert(value: Either): WalletSingleCurrencyState.Content { + return value.fold(ifLeft = { convertError() }, ifRight = ::convert) + } + + private fun convertError(): WalletSingleCurrencyState.Content { + return requireNotNull(currentStateProvider() as? WalletSingleCurrencyState.Content) + } + + private fun convert(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 getMarketPriceState(status: CryptoCurrencyStatus.Status, currencyName: String): MarketPriceBlockState { + return when (status) { + is CryptoCurrencyStatus.NoQuote, + is CryptoCurrencyStatus.Loaded, + -> MarketPriceBlockState.Content( + currencyName = currencyName, + price = formatPrice(status, appCurrencyProvider()), + priceChangeConfig = PriceChangeConfig( + valueInPercent = formatPriceChange(status), + type = getPriceChangeType(status), + ), + ) + is CryptoCurrencyStatus.Loading -> MarketPriceBlockState.Loading(currencyName) + is CryptoCurrencyStatus.Custom, + is CryptoCurrencyStatus.MissedDerivation, + is CryptoCurrencyStatus.NoAccount, + is CryptoCurrencyStatus.Unreachable, + -> MarketPriceBlockState.Error(currencyName) + } + } + + private fun getUpdatedSelectedWallet( + status: CryptoCurrencyStatus.Status, + state: WalletSingleCurrencyState, + ): WalletsListConfig { + val selectedWallet = state.walletsListConfig.wallets[state.walletsListConfig.selectedWalletIndex] + + val updatedWallet = when (status) { + is CryptoCurrencyStatus.NoQuote, + is CryptoCurrencyStatus.Loaded, + -> { + WalletCardState.Content( + id = selectedWallet.id, + title = selectedWallet.title, + additionalInfo = WalletAdditionalInfoFactory.resolve( + cardTypesResolver = cardTypeResolverProvider(), + isLocked = false, + currencyAmount = status.amount, + ), + imageResId = selectedWallet.imageResId, + onClick = selectedWallet.onClick, + balance = formatFiatAmount(status, appCurrencyProvider()), + ) + } + is CryptoCurrencyStatus.Loading -> { + WalletCardState.Loading( + id = selectedWallet.id, + title = selectedWallet.title, + additionalInfo = selectedWallet.additionalInfo, + imageResId = selectedWallet.imageResId, + onClick = selectedWallet.onClick, + ) + } + is CryptoCurrencyStatus.MissedDerivation, + is CryptoCurrencyStatus.NoAccount, + is CryptoCurrencyStatus.Custom, + is CryptoCurrencyStatus.Unreachable, + -> { + WalletCardState.Error( + id = selectedWallet.id, + title = selectedWallet.title, + additionalInfo = selectedWallet.additionalInfo, + imageResId = selectedWallet.imageResId, + onClick = selectedWallet.onClick, + ) + } + } + + return state.walletsListConfig.copy( + wallets = state.walletsListConfig.wallets.toPersistentList() + .set(index = state.walletsListConfig.selectedWalletIndex, element = updatedWallet), + ) + } + + private fun getPriceChangeType(status: CryptoCurrencyStatus.Status): PriceChangeConfig.Type { + val priceChange = status.priceChange ?: return PriceChangeConfig.Type.DOWN + + return if (priceChange > BigDecimal.ZERO) { + PriceChangeConfig.Type.UP + } else { + PriceChangeConfig.Type.DOWN + } + } + + private fun formatPriceChange(status: CryptoCurrencyStatus.Status): String { + val priceChange = status.priceChange ?: return BigDecimalFormatter.EMPTY_BALANCE_SIGN + + return BigDecimalFormatter.formatPercent( + percent = priceChange, + useAbsoluteValue = true, + ) + } + + private fun formatPrice(status: CryptoCurrencyStatus.Status, appCurrency: AppCurrency): String { + val fiatRate = status.fiatRate ?: return BigDecimalFormatter.EMPTY_BALANCE_SIGN + + return BigDecimalFormatter.formatFiatAmount( + fiatAmount = fiatRate, + fiatCurrencyCode = appCurrency.code, + fiatCurrencySymbol = appCurrency.symbol, + ) + } + + private fun formatFiatAmount(status: CryptoCurrencyStatus.Status, appCurrency: AppCurrency): String { + val fiatAmount = status.fiatAmount ?: return BigDecimalFormatter.EMPTY_BALANCE_SIGN + + return BigDecimalFormatter.formatFiatAmount( + fiatAmount = fiatAmount, + fiatCurrencyCode = appCurrency.code, + fiatCurrencySymbol = appCurrency.symbol, + ) + } +} \ 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 72419fce8b..f40a3b577a 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,76 +1,69 @@ package com.tangem.feature.wallet.presentation.wallet.state.factory -import androidx.paging.PagingData +import com.tangem.common.Provider import com.tangem.core.ui.components.marketprice.MarketPriceBlockState -import com.tangem.domain.common.CardTypesResolver +import com.tangem.core.ui.components.transactions.state.TxHistoryState import com.tangem.domain.common.util.cardTypesResolver import com.tangem.domain.wallets.models.UserWallet -import com.tangem.feature.wallet.presentation.common.WalletPreviewData 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.* -import com.tangem.feature.wallet.presentation.wallet.state.content.WalletTokensListState -import com.tangem.feature.wallet.presentation.wallet.state.content.WalletTxHistoryState +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.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.persistentListOf import kotlinx.collections.immutable.toImmutableList -import kotlinx.coroutines.flow.flow /** - * Converter from loaded list of [UserWallet] to skeleton state of screen [WalletStateHolder] + * Converter from loaded list of [UserWallet] to skeleton state of screen [WalletState.ContentState] * - * @property clickIntents screen click intents + * @property currentStateProvider current ui state provider + * @property clickIntents screen click intents * [REDACTED_AUTHOR] */ internal class WalletSkeletonStateConverter( + private val currentStateProvider: Provider, private val clickIntents: WalletClickIntents, -) : Converter, WalletStateHolder> { +) : Converter { - override fun convert(value: List): WalletStateHolder { - val cardTypeResolver = requireNotNull(value.firstOrNull()).scanResponse.cardTypesResolver + override fun convert(value: SkeletonModel): WalletState.ContentState { + val cardTypeResolver = value.wallets[value.selectedWalletIndex].scanResponse.cardTypesResolver return if (cardTypeResolver.isMultiwalletAllowed()) { - createMultiCurrencyState(value) + createMultiCurrencyState(value = value) } else { - createSingleCurrencyState(value, cardTypeResolver) + createSingleCurrencyState(value = value, currencyName = cardTypeResolver.getBlockchain().currency) } } - private fun createMultiCurrencyState(wallets: List): WalletStateHolder.MultiCurrencyContent { - return WalletStateHolder.MultiCurrencyContent( + private fun createMultiCurrencyState(value: SkeletonModel): WalletMultiCurrencyState { + return WalletMultiCurrencyState.Content( onBackClick = clickIntents::onBackClick, topBarConfig = createTopBarConfig(), - walletsListConfig = createWalletsListConfig(wallets), + walletsListConfig = createWalletsListConfig(value), pullToRefreshConfig = createPullToRefreshConfig(), - tokensListState = WalletTokensListState.Content( - items = persistentListOf(), - onOrganizeTokensClick = clickIntents::onOrganizeTokensClick, - ), + tokensListState = WalletTokensListState.Loading, notifications = persistentListOf(), - bottomSheet = null, + bottomSheetConfig = null, ) } - private fun createSingleCurrencyState( - wallets: List, - cardTypeResolver: CardTypesResolver, - ): WalletStateHolder.SingleCurrencyContent { - return WalletStateHolder.SingleCurrencyContent( + private fun createSingleCurrencyState(value: SkeletonModel, currencyName: String): WalletSingleCurrencyState { + return WalletSingleCurrencyState.Content( onBackClick = clickIntents::onBackClick, topBarConfig = createTopBarConfig(), - walletsListConfig = createWalletsListConfig(wallets), + walletsListConfig = createWalletsListConfig(value), pullToRefreshConfig = createPullToRefreshConfig(), notifications = persistentListOf(), - bottomSheet = null, - buttons = WalletPreviewData.singleWalletScreenState.buttons, // TODO: create buttons - marketPriceBlockState = MarketPriceBlockState.Loading( - currencyName = cardTypeResolver.getBlockchain().currency, - ), - txHistoryState = WalletTxHistoryState.Content( - items = flow { PagingData.empty() }, - ), + bottomSheetConfig = null, + buttons = getButtons(), + marketPriceBlockState = MarketPriceBlockState.Loading(currencyName = currencyName), + txHistoryState = TxHistoryState.Loading(onExploreClick = clickIntents::onExploreClick), ) } @@ -81,26 +74,61 @@ internal class WalletSkeletonStateConverter( ) } - private fun createWalletsListConfig(wallets: List): WalletsListConfig { + private fun createWalletsListConfig(value: SkeletonModel): WalletsListConfig { return WalletsListConfig( - selectedWalletIndex = 0, - wallets = wallets.map { wallet -> - val cardTypeResolver = wallet.scanResponse.cardTypesResolver - WalletCardState.Loading( - id = wallet.walletId, - title = wallet.name, - additionalInfo = WalletAdditionalInfoFactory.resolve( - cardTypesResolver = cardTypeResolver, - isLocked = wallet.isLocked, - ), - imageResId = WalletImageResolver.resolve(cardTypesResolver = cardTypeResolver), - ) - }.toImmutableList(), + selectedWalletIndex = value.selectedWalletIndex, + wallets = value.wallets.map(::createWalletState).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 + } else { + createWalletLoadingState(wallet) + } + } else { + createWalletLoadingState(wallet) + } + } + + private fun createWalletLoadingState(wallet: UserWallet): WalletCardState { + val cardTypeResolver = wallet.scanResponse.cardTypesResolver + + return WalletCardState.Loading( + id = wallet.walletId, + title = wallet.name, + additionalInfo = WalletAdditionalInfoFactory.resolve( + cardTypesResolver = cardTypeResolver, + isLocked = wallet.isLocked, + ), + imageResId = WalletImageResolver.resolve(cardTypesResolver = cardTypeResolver), + ) + } + private fun createPullToRefreshConfig(): WalletPullToRefreshConfig { return WalletPullToRefreshConfig(isRefreshing = false, onRefresh = clickIntents::onRefreshSwipe) } + + // TODO: [REDACTED_JIRA] + private fun getButtons(): ImmutableList { + return persistentListOf( + WalletManageButton.Buy(), + WalletManageButton.Send(), + WalletManageButton.Receive(onClick = {}), + WalletManageButton.Exchange(), + WalletManageButton.Sell(), + WalletManageButton.CopyAddress(onClick = {}), + ) + } + + data class SkeletonModel(val wallets: List, val selectedWalletIndex: Int) } \ 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 453c182e49..9e274774b5 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 @@ -3,42 +3,53 @@ package com.tangem.feature.wallet.presentation.wallet.state.factory import androidx.paging.PagingData import arrow.core.Either import com.tangem.common.Provider +import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.common.CardTypesResolver +import com.tangem.domain.tokens.error.CurrencyError import com.tangem.domain.tokens.error.TokenListError +import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.model.TokenList -import com.tangem.domain.txhistory.error.TxHistoryListError -import com.tangem.domain.txhistory.error.TxHistoryStateError -import com.tangem.domain.txhistory.model.TxHistoryItem +import com.tangem.domain.txhistory.models.TxHistoryItem +import com.tangem.domain.txhistory.models.TxHistoryListError +import com.tangem.domain.txhistory.models.TxHistoryStateError import com.tangem.domain.wallets.models.UserWallet -import com.tangem.feature.wallet.presentation.wallet.state.WalletBottomSheetConfig -import com.tangem.feature.wallet.presentation.wallet.state.WalletNotification -import com.tangem.feature.wallet.presentation.wallet.state.WalletStateHolder -import com.tangem.feature.wallet.presentation.wallet.state.factory.WalletLoadedTokensListConverter.LoadedTokensListModel +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.WalletBottomSheetConfig +import com.tangem.feature.wallet.presentation.wallet.state.components.WalletManageButton +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.viewmodels.WalletClickIntents import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.flow.Flow /** - * Main factory for creating [WalletStateHolder] + * Main factory for creating [WalletState] * * @property currentStateProvider current ui state provider - * @param currentCardTypeResolverProvider current card type resolver + * @property currentCardTypeResolverProvider current card type resolver + * @property isLockedWalletProvider current wallet is locked or not * @property clickIntents screen click intents */ internal class WalletStateFactory( - private val currentStateProvider: Provider, - currentCardTypeResolverProvider: Provider, + private val currentStateProvider: Provider, + private val currentCardTypeResolverProvider: Provider, + private val isLockedWalletProvider: Provider, + private val appCurrencyProvider: Provider, private val clickIntents: WalletClickIntents, ) { - private val skeletonConverter by lazy { WalletSkeletonStateConverter(clickIntents = clickIntents) } + private val skeletonConverter by lazy { WalletSkeletonStateConverter(currentStateProvider, clickIntents) } private val loadedTokensListConverter by lazy { WalletLoadedTokensListConverter( currentStateProvider = currentStateProvider, cardTypeResolverProvider = currentCardTypeResolverProvider, + isLockedWalletProvider = isLockedWalletProvider, + appCurrencyProvider = appCurrencyProvider, clickIntents = clickIntents, ) } @@ -46,7 +57,6 @@ internal class WalletStateFactory( private val loadingTransactionsStateConverter by lazy { WalletLoadingTxHistoryConverter( currentStateProvider = currentStateProvider, - currentCardTypeResolverProvider = currentCardTypeResolverProvider, clickIntents = clickIntents, ) } @@ -59,54 +69,143 @@ internal class WalletStateFactory( ) } - fun getInitialState(): WalletStateHolder = WalletStateHolder.Loading(onBackClick = clickIntents::onBackClick) - - fun getSkeletonState(wallets: List): WalletStateHolder = skeletonConverter.convert(wallets) - - fun getStateByTokensList( - tokenListEither: Either, - isRefreshing: Boolean, - ): WalletStateHolder { - return loadedTokensListConverter.convert( - value = LoadedTokensListModel(tokenListEither = tokenListEither, isRefreshing = isRefreshing), + private val singleCurrencyLoadedBalanceConverter by lazy { + WalletSingleCurrencyLoadedBalanceConverter( + currentStateProvider = currentStateProvider, + cardTypeResolverProvider = currentCardTypeResolverProvider, + appCurrencyProvider = appCurrencyProvider, ) } - fun getStateByNotifications(notifications: ImmutableList): WalletStateHolder { - return currentStateProvider().copySealed(notifications = notifications) + fun getInitialState(): WalletState = WalletState.Initial(onBackClick = clickIntents::onBackClick) + + fun getSkeletonState(wallets: List, selectedWalletIndex: Int): WalletState { + return skeletonConverter.convert( + value = WalletSkeletonStateConverter.SkeletonModel( + wallets = wallets, + selectedWalletIndex = selectedWalletIndex, + ), + ) } - fun getStateAfterWalletChanging(index: Int): WalletStateHolder { - return currentStateProvider().let { stateHolder -> - stateHolder.copySealed(walletsListConfig = stateHolder.walletsListConfig.copy(selectedWalletIndex = index)) + fun getStateByTokensList(tokenListEither: Either, isRefreshing: Boolean): WalletState { + return loadedTokensListConverter.convert( + value = WalletLoadedTokensListConverter.LoadedTokensListModel( + tokenListEither = tokenListEither, + isRefreshing = isRefreshing, + ), + ) + } + + fun getStateByNotifications(notifications: ImmutableList): WalletState { + return when (val state = currentStateProvider()) { + is WalletMultiCurrencyState.Content -> state.copy(notifications = notifications) + is WalletSingleCurrencyState.Content -> state.copy(notifications = notifications) + else -> state } } - fun getStateAfterContentRefreshing(): WalletStateHolder { - return currentStateProvider().let { state -> - state.copySealed(pullToRefreshConfig = state.pullToRefreshConfig.copy(isRefreshing = true)) - } + fun getStateAfterContentRefreshing(): WalletState { + return currentStateProvider() } - fun getStateWithOpenBottomSheet(content: WalletBottomSheetConfig.BottomSheetContentConfig): WalletStateHolder { - return currentStateProvider().let { state -> - state.copySealed( - bottomSheet = WalletBottomSheetConfig( + fun getStateWithOpenBottomSheet(content: WalletBottomSheetConfig.BottomSheetContentConfig): WalletState { + return when (val state = currentStateProvider() as WalletState.ContentState) { + is WalletMultiCurrencyState.Content -> state.copy( + bottomSheetConfig = WalletBottomSheetConfig( isShow = true, - onDismissRequest = { state.copySealed(bottomSheet = state.bottomSheet?.copy(isShow = false)) }, + onDismissRequest = clickIntents::onBottomSheetDismiss, content = content, ), ) + is WalletMultiCurrencyState.Locked -> state.copy( + isBottomSheetShow = true, + onBottomSheetDismiss = clickIntents::onBottomSheetDismiss, + ) + is WalletSingleCurrencyState.Content -> state.copy( + bottomSheetConfig = WalletBottomSheetConfig( + isShow = true, + onDismissRequest = clickIntents::onBottomSheetDismiss, + content = content, + ), + ) + is WalletSingleCurrencyState.Locked -> state.copy( + isBottomSheetShow = true, + onBottomSheetDismiss = clickIntents::onBottomSheetDismiss, + ) } } - fun getLoadingTxHistoryState(itemsCountEither: Either): WalletStateHolder { + fun getStateWithClosedBottomSheet(): WalletState { + return when (val state = currentStateProvider() as WalletState.ContentState) { + is WalletMultiCurrencyState.Content -> state.copy( + bottomSheetConfig = state.bottomSheetConfig?.copy(isShow = false), + ) + is WalletMultiCurrencyState.Locked -> state.copy(isBottomSheetShow = false) + is WalletSingleCurrencyState.Content -> state.copy( + bottomSheetConfig = state.bottomSheetConfig?.copy(isShow = false), + ) + is WalletSingleCurrencyState.Locked -> state.copy(isBottomSheetShow = false) + } + } + + fun getLoadingTxHistoryState(itemsCountEither: Either): WalletState { return loadingTransactionsStateConverter.convert(value = itemsCountEither) } fun getLoadedTxHistoryState( txHistoryEither: Either>>, - ): WalletStateHolder { + ): WalletState { return loadedTxHistoryConverter.convert(txHistoryEither) } + + fun getLockedState(): WalletState { + val cardTypeResolver = currentCardTypeResolverProvider() + val state = requireNotNull(currentStateProvider() as? WalletState.ContentState) + return if (cardTypeResolver.isMultiwalletAllowed()) { + WalletMultiCurrencyState.Locked( + onBackClick = state.onBackClick, + topBarConfig = state.topBarConfig.copy( + onMoreClick = clickIntents::onUnlockWalletNotificationClick, + ), + walletsListConfig = state.walletsListConfig, + pullToRefreshConfig = state.pullToRefreshConfig, + onUnlockWalletsNotificationClick = clickIntents::onUnlockWalletNotificationClick, + onUnlockClick = clickIntents::onUnlockWalletClick, + onScanClick = clickIntents::onScanCardClick, + ) + } else { + WalletSingleCurrencyState.Locked( + onBackClick = state.onBackClick, + topBarConfig = state.topBarConfig.copy( + onMoreClick = clickIntents::onUnlockWalletNotificationClick, + ), + walletsListConfig = state.walletsListConfig, + pullToRefreshConfig = state.pullToRefreshConfig, + buttons = getButtons(), + onUnlockWalletsNotificationClick = clickIntents::onUnlockWalletNotificationClick, + onUnlockClick = clickIntents::onUnlockWalletClick, + onScanClick = clickIntents::onScanCardClick, + onExploreClick = clickIntents::onExploreClick, + ) + } + } + + // TODO: [REDACTED_JIRA] + private fun getButtons(): ImmutableList { + return persistentListOf( + WalletManageButton.Buy(), + WalletManageButton.Send(), + WalletManageButton.Receive(onClick = {}), + WalletManageButton.Exchange(), + WalletManageButton.Sell(), + WalletManageButton.CopyAddress(onClick = {}), + ) + } + + fun getSingleCurrencyLoadedBalanceState( + cryptoCurrencyEither: Either, + ): WalletState { + return singleCurrencyLoadedBalanceConverter.convert(cryptoCurrencyEither) + } } \ 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 5ed89bf2e9..d5a8f7cdbd 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 @@ -3,23 +3,18 @@ package com.tangem.feature.wallet.presentation.wallet.state.factory.txhistory import androidx.paging.PagingData import arrow.core.Either import com.tangem.common.Provider -import com.tangem.core.ui.components.buttons.actions.ActionButtonConfig -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.domain.txhistory.error.TxHistoryListError -import com.tangem.domain.txhistory.model.TxHistoryItem -import com.tangem.feature.wallet.presentation.wallet.state.WalletManageButton -import com.tangem.feature.wallet.presentation.wallet.state.WalletStateHolder -import com.tangem.feature.wallet.presentation.wallet.state.content.WalletTxHistoryState +import com.tangem.domain.txhistory.models.TxHistoryItem +import com.tangem.domain.txhistory.models.TxHistoryListError +import com.tangem.feature.wallet.presentation.wallet.state.WalletSingleCurrencyState +import com.tangem.feature.wallet.presentation.wallet.state.WalletState import com.tangem.feature.wallet.presentation.wallet.viewmodels.WalletClickIntents import com.tangem.utils.converter.Converter -import kotlinx.collections.immutable.ImmutableList -import kotlinx.collections.immutable.persistentListOf -import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.flow.Flow /** - * Converter from loaded tx history to [WalletTxHistoryState] + * Converter from loaded tx history to [TxHistoryState] * * @property currentStateProvider current state provider * @property currentCardTypeResolverProvider current card type resolver provider @@ -28,10 +23,10 @@ import kotlinx.coroutines.flow.Flow [REDACTED_AUTHOR] */ internal class WalletLoadedTxHistoryConverter( - private val currentStateProvider: Provider, + private val currentStateProvider: Provider, private val currentCardTypeResolverProvider: Provider, private val clickIntents: WalletClickIntents, -) : Converter>>, WalletStateHolder> { +) : Converter>>, WalletState> { private val walletTxHistoryItemFlowConverter by lazy { WalletTxHistoryItemFlowConverter( @@ -40,56 +35,23 @@ internal class WalletLoadedTxHistoryConverter( ) } - override fun convert(value: Either>>): WalletStateHolder { + override fun convert(value: Either>>): WalletState { return value.fold(ifLeft = ::convertError, ifRight = ::convert) } - private fun convertError(error: TxHistoryListError): WalletStateHolder { - return currentStateProvider().copySingleCurrencyContent( + private fun convertError(error: TxHistoryListError): WalletState { + return requireNotNull(currentStateProvider() as? WalletSingleCurrencyState.Content).copy( txHistoryState = when (error) { is TxHistoryListError.DataError -> { - WalletTxHistoryState.Error(onReloadClick = clickIntents::onReloadClick) + TxHistoryState.Error(onReloadClick = clickIntents::onReloadClick) } }, ) } - private fun convert(items: Flow>): WalletStateHolder { - return currentStateProvider().copySingleCurrencyContent( + private fun convert(items: Flow>): WalletState { + return requireNotNull(currentStateProvider() as? WalletSingleCurrencyState.Content).copy( txHistoryState = walletTxHistoryItemFlowConverter.convert(value = items), ) } - - private fun WalletStateHolder.copySingleCurrencyContent( - txHistoryState: WalletTxHistoryState, - ): WalletStateHolder.SingleCurrencyContent { - return WalletStateHolder.SingleCurrencyContent( - onBackClick = onBackClick, - topBarConfig = topBarConfig, - walletsListConfig = walletsListConfig, - pullToRefreshConfig = pullToRefreshConfig, - notifications = notifications, - bottomSheet = bottomSheet, - buttons = getButtons(), - marketPriceBlockState = getLoadingMarketPriceBlockState(), - txHistoryState = txHistoryState, - ) - } - - // TODO: [REDACTED_JIRA] - private fun getButtons(): ImmutableList { - return persistentListOf( - WalletManageButton.Buy(onClick = {}), - WalletManageButton.Send(onClick = {}), - WalletManageButton.Receive(onClick = {}), - WalletManageButton.Exchange(onClick = {}), - WalletManageButton.CopyAddress(onClick = {}), - ) - .map(WalletManageButton::config) - .toImmutableList() - } - - private fun getLoadingMarketPriceBlockState(): MarketPriceBlockState { - return MarketPriceBlockState.Loading(currencyName = currentCardTypeResolverProvider().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/WalletLoadingTxHistoryConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/txhistory/WalletLoadingTxHistoryConverter.kt index da2936afcc..ef8131d180 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 @@ -1,102 +1,50 @@ package com.tangem.feature.wallet.presentation.wallet.state.factory.txhistory -import androidx.paging.PagingData import arrow.core.Either import com.tangem.common.Provider -import com.tangem.core.ui.components.buttons.actions.ActionButtonConfig -import com.tangem.core.ui.components.marketprice.MarketPriceBlockState -import com.tangem.core.ui.components.transactions.TransactionState -import com.tangem.domain.common.CardTypesResolver -import com.tangem.domain.txhistory.error.TxHistoryStateError -import com.tangem.feature.wallet.presentation.wallet.state.WalletManageButton -import com.tangem.feature.wallet.presentation.wallet.state.WalletStateHolder -import com.tangem.feature.wallet.presentation.wallet.state.content.WalletTxHistoryState +import com.tangem.core.ui.components.transactions.state.TxHistoryState +import com.tangem.domain.txhistory.models.TxHistoryStateError +import com.tangem.feature.wallet.presentation.wallet.state.WalletSingleCurrencyState +import com.tangem.feature.wallet.presentation.wallet.state.WalletState import com.tangem.feature.wallet.presentation.wallet.viewmodels.WalletClickIntents import com.tangem.utils.converter.Converter -import kotlinx.collections.immutable.ImmutableList -import kotlinx.collections.immutable.persistentListOf -import kotlinx.collections.immutable.toImmutableList -import kotlinx.coroutines.flow.flowOf /** - * Converter from loading tx history to [WalletTxHistoryState] + * Converter from loading tx history state to [WalletSingleCurrencyState.Content] * * @property currentStateProvider current state provider - * @property currentCardTypeResolverProvider current card type resolver provider * @property clickIntents screen click intents * [REDACTED_AUTHOR] */ internal class WalletLoadingTxHistoryConverter( - private val currentStateProvider: Provider, - private val currentCardTypeResolverProvider: Provider, + private val currentStateProvider: Provider, private val clickIntents: WalletClickIntents, -) : Converter, WalletStateHolder> { +) : Converter, WalletSingleCurrencyState.Content> { - override fun convert(value: Either): WalletStateHolder { + override fun convert(value: Either): WalletSingleCurrencyState.Content { return value.fold(ifLeft = ::convertError, ifRight = ::convert) } - private fun convert(value: Int): WalletStateHolder { - return currentStateProvider().copySingleCurrencyContent( - txHistoryState = WalletTxHistoryState.Content( - items = flowOf( - value = PagingData.from( - data = buildList(capacity = value) { - add(WalletTxHistoryState.TxHistoryItemState.Transaction(state = TransactionState.Loading)) - }, - ), - ), - ), - ) - } - - private fun convertError(error: TxHistoryStateError): WalletStateHolder { - return currentStateProvider().copySingleCurrencyContent( + private fun convertError(error: TxHistoryStateError): WalletSingleCurrencyState.Content { + return requireNotNull(currentStateProvider() as? WalletSingleCurrencyState.Content).copy( txHistoryState = when (error) { is TxHistoryStateError.EmptyTxHistories -> { - WalletTxHistoryState.Empty(onBuyClick = clickIntents::onBuyClick) + TxHistoryState.Empty(onBuyClick = clickIntents::onBuyClick) } is TxHistoryStateError.DataError -> { - WalletTxHistoryState.Error(onReloadClick = clickIntents::onReloadClick) + TxHistoryState.Error(onReloadClick = clickIntents::onReloadClick) } is TxHistoryStateError.TxHistoryNotImplemented -> { - WalletTxHistoryState.NotSupported(onExploreClick = clickIntents::onExploreClick) + TxHistoryState.NotSupported(onExploreClick = clickIntents::onExploreClick) } }, ) } - private fun WalletStateHolder.copySingleCurrencyContent( - txHistoryState: WalletTxHistoryState, - ): WalletStateHolder.SingleCurrencyContent { - return WalletStateHolder.SingleCurrencyContent( - onBackClick = onBackClick, - topBarConfig = topBarConfig, - walletsListConfig = walletsListConfig, - pullToRefreshConfig = pullToRefreshConfig, - notifications = notifications, - bottomSheet = bottomSheet, - buttons = getButtons(), - marketPriceBlockState = getLoadingMarketPriceBlockState(), - txHistoryState = txHistoryState, + private fun convert(value: Int): WalletSingleCurrencyState.Content { + return requireNotNull(currentStateProvider() as? WalletSingleCurrencyState.Content).copy( + txHistoryState = TxHistoryState.ContentWithLoadingItems(itemsCount = value), ) } - - // TODO: [REDACTED_JIRA] - private fun getButtons(): ImmutableList { - return persistentListOf( - WalletManageButton.Buy(onClick = {}), - WalletManageButton.Send(onClick = {}), - WalletManageButton.Receive(onClick = {}), - WalletManageButton.Exchange(onClick = {}), - WalletManageButton.CopyAddress(onClick = {}), - ) - .map(WalletManageButton::config) - .toImmutableList() - } - - private fun getLoadingMarketPriceBlockState(): MarketPriceBlockState { - return MarketPriceBlockState.Loading(currencyName = currentCardTypeResolverProvider().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/WalletTxHistoryItemFlowConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/txhistory/WalletTxHistoryItemFlowConverter.kt index 2fe01c3e0e..1c4af33fac 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/txhistory/WalletTxHistoryItemFlowConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/txhistory/WalletTxHistoryItemFlowConverter.kt @@ -3,10 +3,10 @@ package com.tangem.feature.wallet.presentation.wallet.state.factory.txhistory import android.text.format.DateUtils import androidx.paging.* import com.tangem.blockchain.common.Blockchain -import com.tangem.core.ui.components.transactions.TransactionState -import com.tangem.domain.txhistory.model.TxHistoryItem -import com.tangem.feature.wallet.presentation.wallet.state.content.WalletTxHistoryState -import com.tangem.feature.wallet.presentation.wallet.state.content.WalletTxHistoryState.TxHistoryItemState +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.state.TxHistoryState.TxHistoryItemState +import com.tangem.domain.txhistory.models.TxHistoryItem import com.tangem.feature.wallet.presentation.wallet.viewmodels.WalletClickIntents import com.tangem.utils.converter.Converter import com.tangem.utils.extensions.isToday @@ -22,7 +22,7 @@ import java.math.BigDecimal import java.util.Locale /** - * Convert from [Flow] of [TxHistoryItem] to [WalletTxHistoryState] + * Convert from [Flow] of [TxHistoryItem] to [TxHistoryState] * * @property blockchain blockchain of transactions history * @property clickIntents screen click intents @@ -32,7 +32,7 @@ import java.util.Locale internal class WalletTxHistoryItemFlowConverter( private val blockchain: Blockchain, private val clickIntents: WalletClickIntents, -) : Converter>, WalletTxHistoryState> { +) : Converter>, TxHistoryState> { /** Example, 2 Aug, 2023 */ private val dateFormatter by lazy { @@ -56,8 +56,8 @@ internal class WalletTxHistoryItemFlowConverter( .withLocale(Locale.getDefault()) } - override fun convert(value: Flow>): WalletTxHistoryState { - return WalletTxHistoryState.Content( + override fun convert(value: Flow>): TxHistoryState { + return TxHistoryState.Content( items = value .map { pagingData -> pagingData @@ -97,11 +97,13 @@ internal class WalletTxHistoryItemFlowConverter( ): TransactionState { return when (item.status) { TxHistoryItem.TxStatus.Confirmed -> TransactionState.Receive( + txHash = item.txHash, address = direction.from.toBriefAddressFormat(), amount = item.amount.toCryptoCurrencyFormat(blockchain = blockchain), timestamp = item.getRawTimestamp(), ) TxHistoryItem.TxStatus.Unconfirmed -> TransactionState.Receiving( + txHash = item.txHash, address = direction.from.toBriefAddressFormat(), amount = item.amount.toCryptoCurrencyFormat(blockchain = blockchain), timestamp = item.getRawTimestamp(), @@ -116,11 +118,13 @@ internal class WalletTxHistoryItemFlowConverter( ): TransactionState { return when (item.status) { TxHistoryItem.TxStatus.Confirmed -> TransactionState.Send( + txHash = item.txHash, address = direction.to.toBriefAddressFormat(), amount = item.amount.toCryptoCurrencyFormat(blockchain = blockchain), timestamp = item.getRawTimestamp(), ) TxHistoryItem.TxStatus.Unconfirmed -> TransactionState.Sending( + txHash = item.txHash, address = direction.to.toBriefAddressFormat(), amount = item.amount.toCryptoCurrencyFormat(blockchain = blockchain), timestamp = item.getRawTimestamp(), @@ -163,9 +167,10 @@ internal class WalletTxHistoryItemFlowConverter( if (txHistoryItemState is TxHistoryItemState.Transaction && txHistoryItemState.state is TransactionState.Content ) { + val txContent = txHistoryItemState.state as TransactionState.Content txHistoryItemState.copy( - state = txHistoryItemState.state.copySealed( - timestamp = txHistoryItemState.state.timestamp.toTimeFormat(), + state = txContent.copySealed( + timestamp = txContent.timestamp.toTimeFormat(), ), ) } else { @@ -180,11 +185,12 @@ internal class WalletTxHistoryItemFlowConverter( * * @see [convert] */ - private fun TxHistoryItem.getRawTimestamp() = this.timestamp.toString() + private fun TxHistoryItem.getRawTimestamp() = this.timestampInMillis.toString() private fun TxHistoryItemState?.getTimestamp(): Long? { return if (this is TxHistoryItemState.Transaction && this.state is TransactionState.Content) { - requireNotNull(this.state.timestamp.toLongOrNull()) { "Timestamp must be Long type" } + val txContent = this.state as TransactionState.Content + requireNotNull(txContent.timestamp.toLongOrNull()) { "Timestamp must be Long type" } } else { null } 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 05f45bd9e4..4bc618e81d 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 @@ -6,8 +6,6 @@ import androidx.compose.foundation.* import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.* import androidx.compose.material.ExperimentalMaterialApi -import androidx.compose.material.pullrefresh.PullRefreshIndicator -import androidx.compose.material.pullrefresh.PullRefreshState import androidx.compose.material.pullrefresh.pullRefresh import androidx.compose.material.pullrefresh.rememberPullRefreshState import androidx.compose.material3.* @@ -17,22 +15,19 @@ 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 androidx.paging.compose.LazyPagingItems import androidx.paging.compose.collectAsLazyPagingItems -import com.tangem.core.ui.components.buttons.HorizontalActionChips -import com.tangem.core.ui.components.marketprice.MarketPriceBlock -import com.tangem.core.ui.components.notifications.Notification +import com.tangem.core.ui.components.transactions.state.TxHistoryState import com.tangem.core.ui.res.TangemTheme import com.tangem.feature.wallet.presentation.common.WalletPreviewData -import com.tangem.feature.wallet.presentation.wallet.state.WalletStateHolder -import com.tangem.feature.wallet.presentation.wallet.state.content.WalletTxHistoryState -import com.tangem.feature.wallet.presentation.wallet.ui.components.WalletBottomSheet -import com.tangem.feature.wallet.presentation.wallet.ui.components.WalletTopBar +import com.tangem.feature.wallet.presentation.wallet.state.WalletMultiCurrencyState +import com.tangem.feature.wallet.presentation.wallet.state.WalletSingleCurrencyState +import com.tangem.feature.wallet.presentation.wallet.state.WalletState +import com.tangem.feature.wallet.presentation.wallet.state.components.WalletTokensListState import com.tangem.feature.wallet.presentation.wallet.ui.components.WalletsList -import com.tangem.feature.wallet.presentation.wallet.ui.components.multicurrency.OrganizeTokensButton -import com.tangem.feature.wallet.presentation.wallet.ui.components.multicurrency.tokensListItems -import com.tangem.feature.wallet.presentation.wallet.ui.components.singlecurrency.txHistoryItems -import com.tangem.feature.wallet.presentation.wallet.ui.utils.ScrollOffsetCollector +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.singlecurrency.controlButtons +import com.tangem.feature.wallet.presentation.wallet.ui.components.singlecurrency.marketPriceBlock import com.tangem.feature.wallet.presentation.wallet.ui.utils.changeWalletAnimator /** @@ -42,11 +37,19 @@ import com.tangem.feature.wallet.presentation.wallet.ui.utils.changeWalletAnimat * [REDACTED_AUTHOR] */ -@OptIn(ExperimentalMaterialApi::class) -@Suppress("LongMethod") @Composable -internal fun WalletScreen(state: WalletStateHolder) { +internal fun WalletScreen(state: WalletState) { BackHandler(onBack = state.onBackClick) + + when (state) { + is WalletState.ContentState -> WalletContent(state = state) + is WalletState.Initial -> Unit + } +} + +@OptIn(ExperimentalMaterialApi::class) +@Composable +private fun WalletContent(state: WalletState.ContentState) { val walletsListState = rememberLazyListState() Scaffold( @@ -54,7 +57,7 @@ internal fun WalletScreen(state: WalletStateHolder) { containerColor = TangemTheme.colors.background.secondary, ) { scaffoldPaddings -> - val changeableItemModifier = Modifier.changeWalletAnimator(walletsListState) + val movableItemModifier = Modifier.changeWalletAnimator(walletsListState) val pullRefreshState = rememberPullRefreshState( refreshing = state.pullToRefreshConfig.isRefreshing, onRefresh = state.pullToRefreshConfig.onRefresh, @@ -65,76 +68,53 @@ internal fun WalletScreen(state: WalletStateHolder) { .padding(paddingValues = scaffoldPaddings) .pullRefresh(pullRefreshState), ) { - val txHistoryItems = if (state is WalletStateHolder.SingleCurrencyContent) { - if (state.txHistoryState is WalletTxHistoryState.ContentState) { - state.txHistoryState.items.collectAsLazyPagingItems() - } else { - null - } + val txHistoryItems = if (state is WalletSingleCurrencyState && + state.txHistoryState is TxHistoryState.ContentState + ) { + (state.txHistoryState as? TxHistoryState.ContentState)?.items?.collectAsLazyPagingItems() } else { null } + val betweenItemsPadding = TangemTheme.dimens.spacing14 + val horizontalPadding = TangemTheme.dimens.spacing16 + val itemModifier = movableItemModifier + .padding(top = betweenItemsPadding) + .padding(horizontal = horizontalPadding) + LazyColumn( modifier = Modifier.fillMaxSize(), contentPadding = PaddingValues(vertical = TangemTheme.dimens.spacing8), horizontalAlignment = Alignment.CenterHorizontally, ) { item { - WalletsList( - config = state.walletsListConfig, - lazyListState = walletsListState, + WalletsList(config = state.walletsListConfig, lazyListState = walletsListState) + } + + if (state is WalletSingleCurrencyState) { + controlButtons( + configs = state.buttons, + modifier = movableItemModifier.padding(top = betweenItemsPadding), ) } - if (state is WalletStateHolder.SingleCurrencyContent) { - item { - HorizontalActionChips( - buttons = state.buttons, - modifier = changeableItemModifier.padding(top = TangemTheme.dimens.spacing14), - contentPadding = PaddingValues(horizontal = TangemTheme.dimens.spacing16), - ) - } + notifications(configs = state.notifications, modifier = itemModifier) + + if (state is WalletSingleCurrencyState.Content) { + marketPriceBlock(state = state.marketPriceBlockState, modifier = itemModifier) } - items( - items = state.notifications, - itemContent = { item -> - Notification( - state = item.state, - modifier = changeableItemModifier - .padding(top = TangemTheme.dimens.spacing14) - .padding(horizontal = TangemTheme.dimens.spacing16), - ) - }, - ) + contentItems(state = state, txHistoryItems = txHistoryItems, modifier = movableItemModifier) - if (state is WalletStateHolder.SingleCurrencyContent) { - item { - MarketPriceBlock( - state = state.marketPriceBlockState, - modifier = changeableItemModifier - .padding(top = TangemTheme.dimens.spacing14) - .padding(horizontal = TangemTheme.dimens.spacing16), - ) - } - } - - contentItems(state = state, txHistoryItems = txHistoryItems, modifier = changeableItemModifier) - - if (state is WalletStateHolder.MultiCurrencyContent) { - item { - OrganizeTokensButton( - onClick = state.tokensListState.onOrganizeTokensClick, - modifier = changeableItemModifier - .padding(top = TangemTheme.dimens.spacing14) - .padding(horizontal = TangemTheme.dimens.spacing16), - ) + if (state is WalletMultiCurrencyState) { + val tokensListState = state.tokensListState + if (tokensListState is WalletTokensListState.ContentState) { + organizeButton(onClick = tokensListState.onOrganizeTokensClick, modifier = itemModifier) } } } - PullToRefreshIndicator( + WalletPullToRefreshIndicator( isRefreshing = state.pullToRefreshConfig.isRefreshing, state = pullRefreshState, modifier = Modifier.align(Alignment.TopCenter), @@ -142,54 +122,18 @@ internal fun WalletScreen(state: WalletStateHolder) { } } - state.bottomSheet?.let { bottomSheetConfig -> - if (bottomSheetConfig.isShow) WalletBottomSheet(config = bottomSheetConfig) + val bottomSheetConfig = state.bottomSheetConfig + if (bottomSheetConfig != null && bottomSheetConfig.isShow) { + WalletBottomSheet(config = bottomSheetConfig) } - LaunchedEffect(key1 = walletsListState, key2 = state.walletsListConfig.onWalletChange) { - snapshotFlow { walletsListState.layoutInfo.visibleItemsInfo } - .collect(collector = ScrollOffsetCollector(callback = state.walletsListConfig.onWalletChange)) - } -} - -private fun LazyListScope.contentItems( - state: WalletStateHolder, - txHistoryItems: LazyPagingItems?, - modifier: Modifier = Modifier, -) { - when (state) { - is WalletStateHolder.MultiCurrencyContent -> { - tokensListItems(state = state.tokensListState, modifier = modifier) - } - is WalletStateHolder.SingleCurrencyContent -> { - txHistoryItems( - state = state.txHistoryState, - txHistoryItems = txHistoryItems, - modifier = modifier, - ) - } - is WalletStateHolder.Loading, - is WalletStateHolder.UnlockWalletContent, - -> Unit - } -} - -@OptIn(ExperimentalMaterialApi::class) -@Composable -private fun PullToRefreshIndicator(isRefreshing: Boolean, state: PullRefreshState, modifier: Modifier = Modifier) { - PullRefreshIndicator( - refreshing = isRefreshing, - state = state, - modifier = modifier, - ) + WalletSideEffects(lazyListState = walletsListState, walletsListConfig = state.walletsListConfig) } // region Preview @Preview @Composable -private fun WalletScreenPreview_Light( - @PreviewParameter(WalletScreenParameterProvider::class) state: WalletStateHolder, -) { +private fun WalletScreenPreview_Light(@PreviewParameter(WalletScreenParameterProvider::class) state: WalletState) { TangemTheme { WalletScreen(state) } @@ -197,15 +141,13 @@ private fun WalletScreenPreview_Light( @Preview @Composable -private fun WalletScreenPreview_Dark( - @PreviewParameter(WalletScreenParameterProvider::class) state: WalletStateHolder, -) { +private fun WalletScreenPreview_Dark(@PreviewParameter(WalletScreenParameterProvider::class) state: WalletState) { TangemTheme(isDark = true) { WalletScreen(state) } } -private class WalletScreenParameterProvider : CollectionPreviewParameterProvider( +private class WalletScreenParameterProvider : CollectionPreviewParameterProvider( collection = listOf( WalletPreviewData.multicurrencyWalletScreenState, WalletPreviewData.singleWalletScreenState, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/WalletsList.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/WalletsList.kt index 34dc515850..09acf941f4 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/WalletsList.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/WalletsList.kt @@ -11,59 +11,67 @@ import androidx.compose.foundation.lazy.LazyRow import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.runtime.Composable +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.tangem.core.ui.res.TangemTheme import com.tangem.feature.wallet.presentation.common.WalletPreviewData -import com.tangem.feature.wallet.presentation.wallet.state.WalletsListConfig +import com.tangem.feature.wallet.presentation.wallet.state.components.WalletsListConfig +import com.tangem.feature.wallet.presentation.wallet.ui.components.common.WalletCard /** * Wallets list component * - * @param config config - * @param modifier modifier + * @param config config + * @param lazyListState main content container list state * [REDACTED_AUTHOR] */ @OptIn(ExperimentalFoundationApi::class) @Composable -internal fun WalletsList(config: WalletsListConfig, lazyListState: LazyListState, modifier: Modifier = Modifier) { +internal fun WalletsList(config: WalletsListConfig, lazyListState: LazyListState) { val horizontalCardPadding = TangemTheme.dimens.spacing16 - val itemWidth = LocalConfiguration.current.screenWidthDp.dp - horizontalCardPadding * 2 + val screenWidth = LocalConfiguration.current.screenWidthDp.dp + val itemWidth by remember(screenWidth) { derivedStateOf { screenWidth - horizontalCardPadding * 2 } } LazyRow( - modifier = modifier.background(color = TangemTheme.colors.background.secondary), + modifier = Modifier.background(color = TangemTheme.colors.background.secondary), state = lazyListState, contentPadding = PaddingValues(horizontal = TangemTheme.dimens.spacing16), horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8), flingBehavior = rememberSnapFlingBehavior(lazyListState = lazyListState), ) { - items(items = config.wallets, key = { it.id.stringValue }) { state -> - WalletCard(state = state, modifier = Modifier.width(itemWidth)) + items( + items = config.wallets, + key = { it.id.stringValue }, + contentType = { it::class.java }, + ) { state -> + WalletCard( + state = state, + modifier = Modifier + .animateItemPlacement() + .width(itemWidth), + ) } } } @Preview @Composable -private fun Preview_WalletHeader_LightTheme() { +private fun Preview_WalletsList_LightTheme() { TangemTheme(isDark = false) { - WalletsList( - config = WalletPreviewData.walletListConfig, - lazyListState = rememberLazyListState(), - ) + WalletsList(config = WalletPreviewData.walletListConfig, lazyListState = rememberLazyListState()) } } @Preview @Composable -private fun Preview_WalletHeader_DarkTheme() { +private fun Preview_WalletsList_DarkTheme() { TangemTheme(isDark = true) { - WalletsList( - config = WalletPreviewData.walletListConfig, - lazyListState = rememberLazyListState(), - ) + WalletsList(config = WalletPreviewData.walletListConfig, lazyListState = rememberLazyListState()) } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/WalletBottomSheet.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletBottomSheet.kt similarity index 98% rename from features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/WalletBottomSheet.kt rename to features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletBottomSheet.kt index fb7a83e1bf..bf78b633da 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/WalletBottomSheet.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletBottomSheet.kt @@ -1,4 +1,4 @@ -package com.tangem.feature.wallet.presentation.wallet.ui.components +package com.tangem.feature.wallet.presentation.wallet.ui.components.common import androidx.compose.foundation.Image import androidx.compose.foundation.layout.* @@ -18,7 +18,7 @@ import com.tangem.core.ui.components.SecondaryButtonIconStart 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.WalletBottomSheetConfig +import com.tangem.feature.wallet.presentation.wallet.state.components.WalletBottomSheetConfig /** * Wallet bottom sheet with detail notification information diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/WalletCard.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletCard.kt similarity index 51% rename from features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/WalletCard.kt rename to features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletCard.kt index afc27705b9..ccff84d714 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/WalletCard.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletCard.kt @@ -1,6 +1,9 @@ -package com.tangem.feature.wallet.presentation.wallet.ui.components +package com.tangem.feature.wallet.presentation.wallet.ui.components.common 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.layout.* import androidx.compose.material3.Icon @@ -20,9 +23,10 @@ import com.tangem.core.ui.components.FontSizeRange import com.tangem.core.ui.components.RectangleShimmer import com.tangem.core.ui.components.ResizableText import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.feature.wallet.impl.R import com.tangem.feature.wallet.presentation.common.WalletPreviewData -import com.tangem.feature.wallet.presentation.wallet.state.WalletCardState +import com.tangem.feature.wallet.presentation.wallet.state.components.WalletCardState private const val DOTS = "•••" @@ -31,6 +35,8 @@ private const val DOTS = "•••" * * @param state state * @param modifier modifier + * +[REDACTED_AUTHOR] */ @Composable internal fun WalletCard(state: WalletCardState, modifier: Modifier = Modifier) { @@ -62,85 +68,92 @@ internal fun WalletCard(state: WalletCardState, modifier: Modifier = Modifier) { } val imageWidth = TangemTheme.dimens.size120 - state.imageResId?.let { - WalletImage( - id = it, - modifier = Modifier.constrainAs(imageItem) { - centerVerticallyTo(parent) - top.linkTo(parent.top) - end.linkTo(parent.end) - height = Dimension.fillToConstraints - width = Dimension.value(imageWidth) - }, - ) - } + WalletImage( + id = state.imageResId, + modifier = Modifier.constrainAs(imageItem) { + centerVerticallyTo(parent) + top.linkTo(parent.top) + end.linkTo(parent.end) + height = Dimension.fillToConstraints + width = Dimension.value(imageWidth) + }, + ) } } } +@OptIn(ExperimentalAnimationApi::class) @Composable private fun Title(state: WalletCardState) { - when (state) { - is WalletCardState.HiddenContent -> { - Row(horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing4)) { + AnimatedContent(targetState = state, label = "Update the title") { + when (it) { + is WalletCardState.HiddenContent -> { + Row(horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing4)) { + Text( + text = it.title, + color = TangemTheme.colors.text.tertiary, + style = TangemTheme.typography.body2, + maxLines = 1, + ) + Icon( + modifier = Modifier.size(size = TangemTheme.dimens.size20), + painter = painterResource(id = R.drawable.ic_eye_off_24), + contentDescription = null, + tint = TangemTheme.colors.icon.informative, + ) + } + } + is WalletCardState.Content, + is WalletCardState.Error, + is WalletCardState.Loading, + -> { Text( - text = state.title, + text = it.title, color = TangemTheme.colors.text.tertiary, style = TangemTheme.typography.body2, maxLines = 1, ) - Icon( - modifier = Modifier.size(size = TangemTheme.dimens.size20), - painter = painterResource(id = R.drawable.ic_eye_off_24), - contentDescription = null, - tint = TangemTheme.colors.icon.informative, - ) } } - else -> { - Text( - text = state.title, - color = TangemTheme.colors.text.tertiary, - style = TangemTheme.typography.body2, - maxLines = 1, - ) - } } } +@OptIn(ExperimentalAnimationApi::class) @Composable private fun Balance(state: WalletCardState) { - when (state) { - is WalletCardState.Content -> { - ResizableText( - text = state.balance, - color = TangemTheme.colors.text.primary1, - style = TangemTheme.typography.h2, - fontSizeRange = FontSizeRange(min = 16.sp, max = TangemTheme.typography.h2.fontSize), - modifier = Modifier.defaultMinSize(minHeight = TangemTheme.dimens.size32), - ) - } - is WalletCardState.Loading -> { - RectangleShimmer( - modifier = Modifier.size( - width = TangemTheme.dimens.size102, - height = TangemTheme.dimens.size24, - ), - ) - } - is WalletCardState.HiddenContent -> { - Text( - text = DOTS, - color = TangemTheme.colors.text.primary1, - style = TangemTheme.typography.h2, - ) - } - is WalletCardState.Error -> { - Text( - text = "—", - color = TangemTheme.colors.text.primary1, - style = TangemTheme.typography.h2, - ) + AnimatedContent(targetState = state, label = "Update the balance") { + when (it) { + is WalletCardState.Content -> { + ResizableText( + text = it.balance, + color = TangemTheme.colors.text.primary1, + style = TangemTheme.typography.h2, + fontSizeRange = FontSizeRange(min = 16.sp, max = TangemTheme.typography.h2.fontSize), + modifier = Modifier.defaultMinSize(minHeight = TangemTheme.dimens.size32), + ) + } + is WalletCardState.Loading -> { + RectangleShimmer( + modifier = Modifier.size( + width = TangemTheme.dimens.size102, + height = TangemTheme.dimens.size24, + ), + ) + } + is WalletCardState.HiddenContent -> { + Text( + text = DOTS, + color = TangemTheme.colors.text.primary1, + style = TangemTheme.typography.h2, + ) + } + is WalletCardState.Error -> { + Text( + text = BigDecimalFormatter.EMPTY_BALANCE_SIGN, + color = TangemTheme.colors.text.primary1, + style = TangemTheme.typography.h2, + ) + } } } } @@ -155,22 +168,23 @@ private fun AdditionalInfo(description: String) { } @Composable -private fun WalletImage(@DrawableRes id: Int, modifier: Modifier = Modifier) { - Image( - painter = painterResource(id), - contentDescription = null, - modifier = modifier, - contentScale = ContentScale.FillWidth, - ) +private fun WalletImage(@DrawableRes id: Int?, modifier: Modifier = Modifier) { + AnimatedVisibility(visible = id != null, modifier = modifier) { + Image( + painter = painterResource(id = requireNotNull(id)), + contentDescription = null, + contentScale = ContentScale.FillWidth, + ) + } } // region Preview -@Preview +@Preview(widthDp = 360, heightDp = 360) @Composable private fun Preview_WalletCard_LightTheme(@PreviewParameter(WalletCardStateProvider::class) state: WalletCardState) { TangemTheme(isDark = false) { - WalletCard(state) + WalletCard(state = state, modifier = Modifier.fillMaxWidth()) } } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletContent.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletContent.kt new file mode 100644 index 0000000000..211ad0e75f --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletContent.kt @@ -0,0 +1,31 @@ +package com.tangem.feature.wallet.presentation.wallet.ui.components.common + +import androidx.compose.foundation.lazy.LazyListScope +import androidx.compose.ui.Modifier +import androidx.paging.compose.LazyPagingItems +import com.tangem.core.ui.components.transactions.state.TxHistoryState +import com.tangem.core.ui.components.transactions.txHistoryItems +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.ui.components.multicurrency.tokensListItems + +/** + * Wallet content + * + * @param state wallet state + * @param txHistoryItems transaction history items + * @param modifier modifier + * +[REDACTED_AUTHOR] + */ +internal fun LazyListScope.contentItems( + state: WalletState.ContentState, + txHistoryItems: LazyPagingItems?, + modifier: Modifier = Modifier, +) { + when (state) { + is WalletMultiCurrencyState -> tokensListItems(state.tokensListState, modifier) + is WalletSingleCurrencyState -> txHistoryItems(state.txHistoryState, txHistoryItems, modifier) + } +} \ 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 new file mode 100644 index 0000000000..c6ef7f89ef --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletNotifications.kt @@ -0,0 +1,27 @@ +package com.tangem.feature.wallet.presentation.wallet.ui.components.common + +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.lazy.LazyListScope +import androidx.compose.foundation.lazy.items +import androidx.compose.ui.Modifier +import com.tangem.core.ui.components.notifications.Notification +import com.tangem.feature.wallet.presentation.wallet.state.components.WalletNotification +import kotlinx.collections.immutable.ImmutableList + +/** + * Wallet notifications + * + * @param configs list of notifications + * @param modifier modifier + * +[REDACTED_AUTHOR] + */ +@OptIn(ExperimentalFoundationApi::class) +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()) }, + ) +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletPullToRefreshIndicator.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletPullToRefreshIndicator.kt new file mode 100644 index 0000000000..5c26f03441 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletPullToRefreshIndicator.kt @@ -0,0 +1,26 @@ +package com.tangem.feature.wallet.presentation.wallet.ui.components.common + +import androidx.compose.material.ExperimentalMaterialApi +import androidx.compose.material.pullrefresh.PullRefreshIndicator +import androidx.compose.material.pullrefresh.PullRefreshState +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier + +/** + * "Pull to refresh" indicator + * + * @param isRefreshing indicator is currently refreshing or not + * @param state indicator state + * @param modifier modifier + * +[REDACTED_AUTHOR] + */ +@OptIn(ExperimentalMaterialApi::class) +@Composable +internal fun WalletPullToRefreshIndicator( + isRefreshing: Boolean, + state: PullRefreshState, + modifier: Modifier = Modifier, +) { + PullRefreshIndicator(refreshing = isRefreshing, state = state, modifier = modifier) +} \ No newline at end of file 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 new file mode 100644 index 0000000000..0d415eaa74 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletSideEffects.kt @@ -0,0 +1,36 @@ +package com.tangem.feature.wallet.presentation.wallet.ui.components.common + +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.snapshotFlow +import com.tangem.feature.wallet.presentation.wallet.state.components.WalletsListConfig +import com.tangem.feature.wallet.presentation.wallet.ui.utils.ScrollOffsetCollector + +/** + * Wallet screen side effects + * + * @param lazyListState lazy list state + * @param walletsListConfig wallets list config + * +[REDACTED_AUTHOR] + */ +@Composable +internal fun WalletSideEffects(lazyListState: LazyListState, walletsListConfig: WalletsListConfig) { + LaunchedEffect(key1 = walletsListConfig.selectedWalletIndex) { + lazyListState.scrollToItem(walletsListConfig.selectedWalletIndex) + } + + val dragInteraction = lazyListState.interactionSource.interactions.collectAsState(initial = null) + LaunchedEffect(key1 = lazyListState, key2 = walletsListConfig.onWalletChange) { + snapshotFlow { lazyListState.layoutInfo.visibleItemsInfo } + .collect( + collector = ScrollOffsetCollector( + lazyListState = lazyListState, + dragInteraction = dragInteraction, + callback = walletsListConfig.onWalletChange, + ), + ) + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/WalletTopBar.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletTopBar.kt similarity index 94% rename from features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/WalletTopBar.kt rename to features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletTopBar.kt index 54db11db4d..5b809bf2e7 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/WalletTopBar.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletTopBar.kt @@ -1,4 +1,4 @@ -package com.tangem.feature.wallet.presentation.wallet.ui.components +package com.tangem.feature.wallet.presentation.wallet.ui.components.common import androidx.compose.material3.* import androidx.compose.runtime.Composable @@ -7,7 +7,7 @@ import androidx.compose.ui.tooling.preview.Preview import com.tangem.core.ui.res.TangemTheme import com.tangem.feature.wallet.impl.R import com.tangem.feature.wallet.presentation.common.WalletPreviewData -import com.tangem.feature.wallet.presentation.wallet.state.WalletTopBarConfig +import com.tangem.feature.wallet.presentation.wallet.state.components.WalletTopBarConfig /** * Wallet screen top bar 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 c8fc7782f3..350cb95ecf 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 @@ -1,10 +1,26 @@ package com.tangem.feature.wallet.presentation.wallet.ui.components.multicurrency +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import com.tangem.feature.wallet.presentation.wallet.state.content.WalletTokensListState -import com.tangem.feature.wallet.presentation.wallet.ui.decorations.walletContentItemDecoration +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import com.tangem.core.ui.decorations.roundedShapeItemDecoration +import com.tangem.core.ui.res.TangemTheme +import com.tangem.feature.wallet.impl.R +import com.tangem.feature.wallet.presentation.wallet.state.components.WalletTokensListState +import kotlinx.collections.immutable.ImmutableList + +private const val NON_CONTENT_TOKENS_LIST_KEY = "NON_CONTENT_TOKENS_LIST" /** * LazyList extension for [WalletTokensListState] @@ -15,17 +31,67 @@ import com.tangem.feature.wallet.presentation.wallet.ui.decorations.walletConten [REDACTED_AUTHOR] */ internal fun LazyListScope.tokensListItems(state: WalletTokensListState, modifier: Modifier = Modifier) { + when (state) { + is WalletTokensListState.ContentState -> contentItems(items = state.items, modifier = modifier) + WalletTokensListState.Empty -> nonContentItem(modifier = modifier) + } +} + +@OptIn(ExperimentalFoundationApi::class) +private fun LazyListScope.contentItems( + items: ImmutableList, + modifier: Modifier = Modifier, +) { itemsIndexed( - items = state.items, - key = { index, _ -> index }, + items = items, + key = { _, item -> + when (item) { + is WalletTokensListState.TokensListItemState.NetworkGroupTitle -> item.value.hashCode() + is WalletTokensListState.TokensListItemState.Token -> item.state.id + } + }, + contentType = { _, item -> item::class.java }, itemContent = { index, item -> MultiCurrencyContentItem( state = item, - modifier = modifier.walletContentItemDecoration( - currentIndex = index, - lastIndex = state.items.lastIndex, - ), + modifier = modifier + .animateItemPlacement() + .roundedShapeItemDecoration( + currentIndex = index, + lastIndex = items.lastIndex, + ), ) }, ) +} + +@OptIn(ExperimentalFoundationApi::class) +private fun LazyListScope.nonContentItem(modifier: Modifier = Modifier) { + item( + key = NON_CONTENT_TOKENS_LIST_KEY, + contentType = NON_CONTENT_TOKENS_LIST_KEY, + ) { + Column( + modifier = modifier + .animateItemPlacement() + .padding(top = TangemTheme.dimens.spacing96), + verticalArrangement = Arrangement.spacedBy(space = TangemTheme.dimens.spacing16), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Icon( + painter = painterResource(id = R.drawable.ic_empty_64), + contentDescription = null, + modifier = Modifier.size(size = TangemTheme.dimens.size64), + tint = TangemTheme.colors.icon.inactive, + ) + + Text( + text = stringResource(id = R.string.main_empty_tokens_list_message), + modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing48), + color = TangemTheme.colors.text.tertiary, + textAlign = TextAlign.Center, + style = TangemTheme.typography.caption, + ) + } + } } \ No newline at end of file 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 a387c5f956..a9ac43d4ab 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 @@ -2,9 +2,10 @@ package com.tangem.feature.wallet.presentation.wallet.ui.components.multicurrenc import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import com.tangem.core.ui.extensions.resolveReference import com.tangem.feature.wallet.presentation.common.component.NetworkGroupItem import com.tangem.feature.wallet.presentation.common.component.TokenItem -import com.tangem.feature.wallet.presentation.wallet.state.content.WalletTokensListState +import com.tangem.feature.wallet.presentation.wallet.state.components.WalletTokensListState /** * Multi-currency content item @@ -18,7 +19,7 @@ import com.tangem.feature.wallet.presentation.wallet.state.content.WalletTokensL internal fun MultiCurrencyContentItem(state: WalletTokensListState.TokensListItemState, modifier: Modifier = Modifier) { when (state) { is WalletTokensListState.TokensListItemState.NetworkGroupTitle -> { - NetworkGroupItem(networkName = state.networkName, modifier = modifier) + NetworkGroupItem(networkName = state.value.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/ui/components/multicurrency/MultiCurrencyOrganizeButton.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/multicurrency/MultiCurrencyOrganizeButton.kt new file mode 100644 index 0000000000..210bfd423c --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/multicurrency/MultiCurrencyOrganizeButton.kt @@ -0,0 +1,22 @@ +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 + +private const val ORGANIZE_BUTTON_CONTENT_TYPE = "OrganizeTokensButton" + +/** + * Organize tokens button + * + * @param onClick callback is invoked when button is clicked + * @param modifier modifier + * +[REDACTED_AUTHOR] + */ +@OptIn(ExperimentalFoundationApi::class) +internal fun LazyListScope.organizeButton(onClick: (() -> Unit)?, modifier: Modifier = Modifier) { + item(key = ORGANIZE_BUTTON_CONTENT_TYPE, contentType = ORGANIZE_BUTTON_CONTENT_TYPE) { + OrganizeTokensButton(onClick = onClick, modifier = modifier.animateItemPlacement()) + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/singlecurrency/SingleCurrencyContentItem.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/singlecurrency/SingleCurrencyContentItem.kt deleted file mode 100644 index 58489e3a3d..0000000000 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/singlecurrency/SingleCurrencyContentItem.kt +++ /dev/null @@ -1,29 +0,0 @@ -package com.tangem.feature.wallet.presentation.wallet.ui.components.singlecurrency - -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import com.tangem.core.ui.components.transactions.Transaction -import com.tangem.feature.wallet.presentation.wallet.state.content.WalletTxHistoryState - -/** - * Single currency content item - * - * @param state state - * @param modifier modifier - * -[REDACTED_AUTHOR] - */ -@Composable -internal fun SingleCurrencyContentItem(state: WalletTxHistoryState.TxHistoryItemState, modifier: Modifier = Modifier) { - when (state) { - is WalletTxHistoryState.TxHistoryItemState.GroupTitle -> { - TxHistoryGroupTitle(config = state, modifier = modifier) - } - is WalletTxHistoryState.TxHistoryItemState.Title -> { - TxHistoryTitle(config = state, modifier = modifier) - } - is WalletTxHistoryState.TxHistoryItemState.Transaction -> { - Transaction(state = state.state, modifier = modifier) - } - } -} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/singlecurrency/SingleCurrencyControlButtons.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/singlecurrency/SingleCurrencyControlButtons.kt new file mode 100644 index 0000000000..4843bd9dc4 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/singlecurrency/SingleCurrencyControlButtons.kt @@ -0,0 +1,32 @@ +package com.tangem.feature.wallet.presentation.wallet.ui.components.singlecurrency + +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.lazy.LazyListScope +import androidx.compose.ui.Modifier +import com.tangem.core.ui.components.buttons.HorizontalActionChips +import com.tangem.core.ui.res.TangemTheme +import com.tangem.feature.wallet.presentation.wallet.state.components.WalletManageButton +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.toImmutableList + +private const val CONTROL_BUTTONS_CONTENT_TYPE = "ControlButtons" + +/** + * Single currency control buttons. Like, "Buy", "Sell", etc + * + * @param configs list of buttons + * @param modifier modifier + * +[REDACTED_AUTHOR] + */ +@OptIn(ExperimentalFoundationApi::class) +internal fun LazyListScope.controlButtons(configs: ImmutableList, modifier: Modifier = Modifier) { + item(key = CONTROL_BUTTONS_CONTENT_TYPE, contentType = CONTROL_BUTTONS_CONTENT_TYPE) { + HorizontalActionChips( + buttons = configs.map(WalletManageButton::config).toImmutableList(), + modifier = modifier.animateItemPlacement(), + contentPadding = PaddingValues(horizontal = TangemTheme.dimens.spacing16), + ) + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/singlecurrency/SingleCurrencyMarketPriceBlock.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/singlecurrency/SingleCurrencyMarketPriceBlock.kt new file mode 100644 index 0000000000..12d9b505b7 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/singlecurrency/SingleCurrencyMarketPriceBlock.kt @@ -0,0 +1,25 @@ +package com.tangem.feature.wallet.presentation.wallet.ui.components.singlecurrency + +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.lazy.LazyListScope +import androidx.compose.ui.Modifier +import com.tangem.core.ui.components.marketprice.MarketPriceBlock +import com.tangem.core.ui.components.marketprice.MarketPriceBlockState + +/** + * Single currency market price block + * + * @param state component state + * @param modifier modifier + * +[REDACTED_AUTHOR] + */ +@OptIn(ExperimentalFoundationApi::class) +internal fun LazyListScope.marketPriceBlock(state: MarketPriceBlockState, modifier: Modifier = Modifier) { + item( + key = MarketPriceBlockState::class.java, + contentType = MarketPriceBlockState::class.java, + ) { + MarketPriceBlock(state = state, modifier = modifier.animateItemPlacement()) + } +} \ No newline at end of file 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 41cbf50ec1..1cfc37798f 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,6 +1,9 @@ package com.tangem.feature.wallet.presentation.wallet.ui.utils +import androidx.compose.foundation.interaction.Interaction import androidx.compose.foundation.lazy.LazyListItemInfo +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.runtime.State import kotlinx.coroutines.flow.FlowCollector import kotlin.math.abs @@ -9,17 +12,22 @@ import kotlin.math.abs * If first visible item offset is greater than half item size, then [callback] be invoked. * If last visible item offset is greater than half item size, then [callback] be invoked. * - * @property callback lambda be invoked when current scroll items is changed + * @property lazyListState lazy list state + * @property dragInteraction current drag interaction + * @property callback lambda be invoked when current scroll items is changed * [REDACTED_AUTHOR] */ internal class ScrollOffsetCollector( + private val lazyListState: LazyListState, + private val dragInteraction: State, private val callback: (Int) -> Unit, ) : FlowCollector> { 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 val firstItem = value.firstOrNull() ?: return val lastItem = value.lastOrNull() ?: return 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 75084dad74..4c9c49ed14 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/CryptoCurrencyStatusToTokenItemConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/CryptoCurrencyStatusToTokenItemConverter.kt @@ -1,25 +1,28 @@ package com.tangem.feature.wallet.presentation.wallet.utils import androidx.annotation.DrawableRes +import com.tangem.common.Provider import com.tangem.core.ui.components.marketprice.PriceChangeConfig import com.tangem.core.ui.utils.BigDecimalFormatter -import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.tokens.models.CryptoCurrency import com.tangem.feature.wallet.impl.R import com.tangem.feature.wallet.presentation.common.state.TokenItemState +import com.tangem.feature.wallet.presentation.wallet.viewmodels.WalletClickIntents import com.tangem.utils.converter.Converter import java.math.BigDecimal internal class CryptoCurrencyStatusToTokenItemConverter( + private val appCurrencyProvider: Provider, private val isWalletContentHidden: Boolean, - private val fiatCurrencyCode: String, - private val fiatCurrencySymbol: String, + private val clickIntents: WalletClickIntents, ) : Converter { private val CryptoCurrencyStatus.networkIconResId: Int? @DrawableRes get() { // TODO: [REDACTED_JIRA] - return if (currency is CryptoCurrency.Token) null else R.drawable.img_eth_22 + return if (currency is CryptoCurrency.Coin) null else R.drawable.img_eth_22 } private val CryptoCurrencyStatus.tokenIconResId: Int @@ -30,9 +33,10 @@ internal class CryptoCurrencyStatusToTokenItemConverter( override fun convert(value: CryptoCurrencyStatus): TokenItemState { return when (value.value) { - is CryptoCurrencyStatus.Loading -> TokenItemState.Loading + is CryptoCurrencyStatus.Loading -> TokenItemState.Loading(id = value.currency.id.value) is CryptoCurrencyStatus.Loaded, is CryptoCurrencyStatus.Custom, + is CryptoCurrencyStatus.NoQuote, -> value.mapToTokenItemState() // TODO: Add other token item states, currently not designed is CryptoCurrencyStatus.MissedDerivation, @@ -59,6 +63,7 @@ internal class CryptoCurrencyStatusToTokenItemConverter( priceChange = getPriceChangeConfig(), ) }, + onClick = { clickIntents.onTokenClick(currency) }, ) } @@ -70,8 +75,9 @@ internal class CryptoCurrencyStatusToTokenItemConverter( private fun CryptoCurrencyStatus.getFormattedFiatAmount(): String { val fiatAmount = value.fiatAmount ?: return UNKNOWN_AMOUNT_SIGN + val appCurrency = appCurrencyProvider() - return BigDecimalFormatter.formatFiatAmount(fiatAmount, fiatCurrencyCode, fiatCurrencySymbol) + return BigDecimalFormatter.formatFiatAmount(fiatAmount, appCurrency.code, appCurrency.symbol) } private fun CryptoCurrencyStatus.mapToUnreachableTokenItemState() = TokenItemState.Unreachable( diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/FiatBalanceToWalletCardConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/FiatBalanceToWalletCardConverter.kt index b197bf76e8..8d48f9b5ec 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/FiatBalanceToWalletCardConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/FiatBalanceToWalletCardConverter.kt @@ -2,19 +2,19 @@ package com.tangem.feature.wallet.presentation.wallet.utils import com.tangem.common.Provider import com.tangem.core.ui.utils.BigDecimalFormatter.formatFiatAmount +import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.common.CardTypesResolver import com.tangem.domain.tokens.model.TokenList import com.tangem.feature.wallet.presentation.wallet.domain.WalletAdditionalInfoFactory -import com.tangem.feature.wallet.presentation.wallet.state.WalletCardState +import com.tangem.feature.wallet.presentation.wallet.state.components.WalletCardState import com.tangem.utils.converter.Converter internal class FiatBalanceToWalletCardConverter( private val currentState: WalletCardState, private val cardTypeResolverProvider: Provider, + private val appCurrencyProvider: Provider, private val isLockedState: Boolean, private val isWalletContentHidden: Boolean, - private val fiatCurrencyCode: String, - private val fiatCurrencySymbol: String, ) : Converter { override fun convert(value: TokenList.FiatBalance): WalletCardState { @@ -33,13 +33,15 @@ internal class FiatBalanceToWalletCardConverter( if (isWalletContentHidden) { WalletCardState.HiddenContent(id, title, additionalInfo, imageResId, onClick) } else { + val appCurrency = appCurrencyProvider() + WalletCardState.Content( id = id, title = title, additionalInfo = additionalInfo, imageResId = imageResId, onClick = onClick, - balance = formatFiatAmount(value.amount, fiatCurrencyCode, fiatCurrencySymbol), + balance = formatFiatAmount(value.amount, appCurrency.code, appCurrency.symbol), ) } } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/LoadingItemsProvider.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/LoadingItemsProvider.kt index 067db48037..c7e545dec3 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/LoadingItemsProvider.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/LoadingItemsProvider.kt @@ -1,15 +1,21 @@ package com.tangem.feature.wallet.presentation.wallet.utils import com.tangem.feature.wallet.presentation.common.state.TokenItemState -import com.tangem.feature.wallet.presentation.wallet.state.content.WalletTokensListState +import com.tangem.feature.wallet.presentation.wallet.state.components.WalletTokensListState import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList internal object LoadingItemsProvider { fun getLoadingMultiCurrencyTokens(): ImmutableList { - return buildList(capacity = 5) { - add(WalletTokensListState.TokensListItemState.Token(state = TokenItemState.Loading)) - }.toImmutableList() + val items = mutableListOf() + repeat(times = 5) { + items.add( + WalletTokensListState.TokensListItemState.Token( + state = TokenItemState.Loading(id = "Loading#$it"), + ), + ) + } + return items.toImmutableList() } } \ No newline at end of file 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 new file mode 100644 index 0000000000..ac32cdb876 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/TokenListErrorConverter.kt @@ -0,0 +1,20 @@ +package com.tangem.feature.wallet.presentation.wallet.utils + +import com.tangem.common.Provider +import com.tangem.domain.tokens.error.TokenListError +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.WalletTokensListState +import com.tangem.utils.converter.Converter +import kotlinx.collections.immutable.persistentListOf + +internal class TokenListErrorConverter( + private val currentStateProvider: Provider, +) : Converter { + + override fun convert(value: TokenListError): WalletMultiCurrencyState.Content { + return requireNotNull(currentStateProvider() as? WalletMultiCurrencyState.Content).copy( + tokensListState = WalletTokensListState.Content(items = persistentListOf(), onOrganizeTokensClick = null), + ) + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/TokenListErrorToWalletStateConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/TokenListErrorToWalletStateConverter.kt deleted file mode 100644 index b93109b6f6..0000000000 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/TokenListErrorToWalletStateConverter.kt +++ /dev/null @@ -1,27 +0,0 @@ -package com.tangem.feature.wallet.presentation.wallet.utils - -import com.tangem.common.Provider -import com.tangem.domain.tokens.error.TokenListError -import com.tangem.feature.wallet.presentation.wallet.state.WalletStateHolder -import com.tangem.feature.wallet.presentation.wallet.state.content.WalletTokensListState -import com.tangem.utils.converter.Converter -import kotlinx.collections.immutable.persistentListOf - -internal class TokenListErrorToWalletStateConverter( - private val currentStateProvider: Provider, -) : Converter { - - // TODO: [REDACTED_JIRA] - override fun convert(value: TokenListError): WalletStateHolder { - val state = currentStateProvider() - return WalletStateHolder.MultiCurrencyContent( - onBackClick = state.onBackClick, - topBarConfig = state.topBarConfig, - walletsListConfig = state.walletsListConfig, - pullToRefreshConfig = state.pullToRefreshConfig, - notifications = state.notifications, - bottomSheet = state.bottomSheet, - tokensListState = WalletTokensListState.Content(items = persistentListOf(), onOrganizeTokensClick = null), - ) - } -} \ No newline at end of file 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 114cd724f9..ca50986a37 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,10 +1,13 @@ package com.tangem.feature.wallet.presentation.wallet.utils +import com.tangem.common.Provider +import com.tangem.core.ui.extensions.TextReference +import com.tangem.domain.appcurrency.model.AppCurrency 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.content.WalletTokensListState -import com.tangem.feature.wallet.presentation.wallet.state.content.WalletTokensListState.TokensListItemState +import com.tangem.feature.wallet.presentation.wallet.state.components.WalletTokensListState +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 @@ -13,31 +16,40 @@ import kotlinx.collections.immutable.mutate import kotlinx.collections.immutable.persistentListOf internal class TokenListToContentItemsConverter( + appCurrencyProvider: Provider, isWalletContentHidden: Boolean, - fiatCurrencyCode: String, - fiatCurrencySymbol: String, private val clickIntents: WalletClickIntents, ) : Converter { private val tokenStatusConverter = CryptoCurrencyStatusToTokenItemConverter( isWalletContentHidden = isWalletContentHidden, - fiatCurrencyCode = fiatCurrencyCode, - fiatCurrencySymbol = fiatCurrencySymbol, + appCurrencyProvider = appCurrencyProvider, + clickIntents = clickIntents, ) override fun convert(value: TokenList): WalletTokensListState { - return 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 - }, - ) + 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 + }, + ) + } } private fun TokenList.GroupedByNetwork.mapToMultiCurrencyItems(): PersistentList { @@ -53,7 +65,7 @@ internal class TokenListToContentItemsConverter( } private fun MutableList.addGroup(group: NetworkGroup): List { - this.add(TokensListItemState.NetworkGroupTitle(group.network.name)) + this.add(TokensListItemState.NetworkGroupTitle(TextReference.Str(group.network.name))) group.currencies.forEach { token -> this.addToken(token) 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 06ec3d5b8a..fb21502fe6 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 @@ -1,81 +1,69 @@ package com.tangem.feature.wallet.presentation.wallet.utils import com.tangem.common.Provider +import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.common.CardTypesResolver import com.tangem.domain.tokens.model.TokenList import com.tangem.feature.wallet.presentation.common.state.TokenItemState -import com.tangem.feature.wallet.presentation.wallet.state.WalletStateHolder -import com.tangem.feature.wallet.presentation.wallet.state.WalletStateHolder.MultiCurrencyContent -import com.tangem.feature.wallet.presentation.wallet.state.WalletsListConfig -import com.tangem.feature.wallet.presentation.wallet.state.content.WalletTokensListState +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.WalletTokensListState +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 +@Suppress("LongParameterList") internal class TokenListToWalletStateConverter( - private val currentStateProvider: Provider, + private val currentStateProvider: Provider, private val cardTypeResolverProvider: Provider, + private val isLockedWalletProvider: Provider, + private val appCurrencyProvider: Provider, private val isWalletContentHidden: Boolean, - private val fiatCurrencyCode: String, - private val fiatCurrencySymbol: String, clickIntents: WalletClickIntents, -) : Converter { +) : Converter { private val tokenListToContentConverter = TokenListToContentItemsConverter( isWalletContentHidden = isWalletContentHidden, - fiatCurrencyCode = fiatCurrencyCode, - fiatCurrencySymbol = fiatCurrencySymbol, + appCurrencyProvider = appCurrencyProvider, clickIntents = clickIntents, ) - override fun convert(value: TokensListModel): WalletStateHolder { - val state = currentStateProvider() - return state - .updateWithTokenList(tokenList = value.tokenList) - .copySealed( - walletsListConfig = state.updateSelectedWallet(value.tokenList.totalFiatBalance), - pullToRefreshConfig = if (value.isRefreshing) { - state.pullToRefreshConfig.copy(isRefreshing = state.getRefreshingStatus()) - } else { - state.pullToRefreshConfig - }, - ) - } - - private fun WalletStateHolder.updateWithTokenList(tokenList: TokenList): MultiCurrencyContent { - return MultiCurrencyContent( - onBackClick = onBackClick, - topBarConfig = topBarConfig, - walletsListConfig = walletsListConfig, - pullToRefreshConfig = pullToRefreshConfig, - notifications = notifications, - bottomSheet = bottomSheet, - tokensListState = tokenListToContentConverter.convert(value = tokenList), + override fun convert(value: TokensListModel): 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 = state.getRefreshingStatus()) + } else { + state.pullToRefreshConfig + }, + tokensListState = tokenListToContentConverter.convert(value = value.tokenList), ) } - private fun WalletStateHolder.updateSelectedWallet(fiatBalance: TokenList.FiatBalance): WalletsListConfig { + private fun WalletMultiCurrencyState.updateSelectedWallet(fiatBalance: TokenList.FiatBalance): WalletsListConfig { val selectedWalletIndex = walletsListConfig.selectedWalletIndex val selectedWalletCard = walletsListConfig.wallets[selectedWalletIndex] val converter = FiatBalanceToWalletCardConverter( currentState = selectedWalletCard, - isLockedState = this is WalletStateHolder.UnlockWalletContent, + isLockedState = isLockedWalletProvider(), cardTypeResolverProvider = cardTypeResolverProvider, + appCurrencyProvider = appCurrencyProvider, isWalletContentHidden = isWalletContentHidden, - fiatCurrencyCode = fiatCurrencyCode, - fiatCurrencySymbol = fiatCurrencySymbol, ) return walletsListConfig.copy( - wallets = walletsListConfig.wallets - .toPersistentList() + wallets = walletsListConfig.wallets.toPersistentList() .set(index = selectedWalletIndex, element = converter.convert(fiatBalance)), ) } - private fun WalletStateHolder.getRefreshingStatus(): Boolean { - return if (this is MultiCurrencyContent) { + private fun WalletState.getRefreshingStatus(): Boolean { + return if (this is WalletMultiCurrencyState.Content && + this.tokensListState is WalletTokensListState.ContentState + ) { tokensListState.items.any { tokensListItemState -> tokensListItemState is WalletTokensListState.TokensListItemState.Token && tokensListItemState.state is TokenItemState.Loading 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 503b697419..2dcc2ddc06 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,5 +1,7 @@ package com.tangem.feature.wallet.presentation.wallet.viewmodels +import com.tangem.domain.tokens.models.CryptoCurrency + internal interface WalletClickIntents { fun onBackClick() @@ -31,4 +33,12 @@ internal interface WalletClickIntents { fun onReloadClick() fun onExploreClick() + + fun onUnlockWalletClick() + + fun onUnlockWalletNotificationClick() + + fun onBottomSheetDismiss() + + fun onTokenClick(currency: CryptoCurrency) } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletNotificationsListFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletNotificationsListFactory.kt index aace48a09d..11beecade5 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletNotificationsListFactory.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletNotificationsListFactory.kt @@ -6,9 +6,10 @@ 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.common.state.TokenItemState -import com.tangem.feature.wallet.presentation.wallet.state.WalletNotification -import com.tangem.feature.wallet.presentation.wallet.state.WalletStateHolder -import com.tangem.feature.wallet.presentation.wallet.state.content.WalletTokensListState +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.WalletNotification +import com.tangem.feature.wallet.presentation.wallet.state.components.WalletTokensListState import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.flow.Flow @@ -25,7 +26,7 @@ import kotlinx.coroutines.flow.flow [REDACTED_AUTHOR] */ internal class WalletNotificationsListFactory( - private val currentStateProvider: Provider, + private val currentStateProvider: Provider, private val wasCardScannedCallback: suspend (String) -> Boolean, private val isUserAlreadyRateAppCallback: suspend () -> Boolean, private val isDemoCardCallback: (String) -> Boolean, @@ -117,7 +118,9 @@ internal class WalletNotificationsListFactory( } return currentStateProvider().let { state -> - state is WalletStateHolder.MultiCurrencyContent && state.tokensListState.items.any(isUnreachableState) + state is WalletMultiCurrencyState.Content && + state.tokensListState is WalletTokensListState.ContentState && + state.tokensListState.items.any(isUnreachableState) } } 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 new file mode 100644 index 0000000000..c5d4e10424 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletStateCache.kt @@ -0,0 +1,22 @@ +package com.tangem.feature.wallet.presentation.wallet.viewmodels + +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.feature.wallet.presentation.wallet.state.WalletState + +/** + * Wallet state cache. It allows to switch the wallets without additional loading like a PagerView. + * +[REDACTED_AUTHOR] + */ +internal object WalletStateCache { + + private val states = mutableMapOf() + + /** Get state by [userWalletId] */ + fun getState(userWalletId: UserWalletId): WalletState? = states[userWalletId] + + /** Add or update [state] by [userWalletId] */ + fun update(userWalletId: UserWalletId, state: WalletState) { + 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/WalletStateHolder.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletStateHolder.kt new file mode 100644 index 0000000000..59c1f39f13 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletStateHolder.kt @@ -0,0 +1,40 @@ +package com.tangem.feature.wallet.presentation.wallet.viewmodels + +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import com.tangem.feature.wallet.presentation.wallet.state.WalletState + +/** + * Wallet state holder + * + * @param initialState initial ui state + * +[REDACTED_AUTHOR] + */ +internal class WalletStateHolder(initialState: WalletState) { + + /** Screen state */ + var uiState: WalletState by mutableStateOf(initialState) + private set + + /** Set screen [state] */ + fun setState(state: WalletState) { + when (state) { + is WalletState.ContentState -> { + cache(state = state) + + uiState = state + } + is WalletState.Initial -> Unit + } + } + + /** Cache [state] [WalletState.ContentState] to [WalletStateCache] */ + private fun cache(state: WalletState.ContentState) { + val selectedWalletIndex = state.walletsListConfig.selectedWalletIndex + val selectedWalletId = state.walletsListConfig.wallets[selectedWalletIndex].id + + WalletStateCache.update(userWalletId = selectedWalletId, state = state) + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletStateHolderDelegate.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletStateHolderDelegate.kt new file mode 100644 index 0000000000..25c3d12698 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletStateHolderDelegate.kt @@ -0,0 +1,20 @@ +package com.tangem.feature.wallet.presentation.wallet.viewmodels + +import com.tangem.feature.wallet.presentation.wallet.state.WalletState +import kotlin.properties.ReadWriteProperty +import kotlin.reflect.KProperty + +internal class WalletStateHolderDelegate( + private val uiStateHolder: WalletStateHolder, +) : ReadWriteProperty { + + override fun getValue(thisRef: Any?, property: KProperty<*>): WalletState = uiStateHolder.uiState + + override fun setValue(thisRef: Any?, property: KProperty<*>, value: WalletState) { + uiStateHolder.setState(value) + } +} + +internal fun uiStateHolder(initialState: WalletState): ReadWriteProperty { + return WalletStateHolderDelegate(uiStateHolder = WalletStateHolder(initialState = initialState)) +} \ 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 4c1275a2bd..f81cd1f969 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 @@ -1,33 +1,47 @@ package com.tangem.feature.wallet.presentation.wallet.viewmodels -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.setValue import androidx.lifecycle.* import androidx.paging.cachedIn -import com.tangem.blockchain.common.DerivationStyle +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 +import com.tangem.core.ui.components.marketprice.MarketPriceBlockState +import com.tangem.core.ui.components.transactions.state.TxHistoryState +import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase +import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.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.settings.IsUserAlreadyRateAppUseCase +import com.tangem.domain.tokens.GetPrimaryCurrencyUseCase import com.tangem.domain.tokens.GetTokenListUseCase import com.tangem.domain.tokens.model.TokenList +import com.tangem.domain.tokens.models.CryptoCurrency import com.tangem.domain.tokens.models.Network import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsCountUseCase import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsUseCase import com.tangem.domain.userwallets.UserWalletBuilder import com.tangem.domain.wallets.models.UserWallet -import com.tangem.domain.wallets.usecase.GetExploreUrlUseCase -import com.tangem.domain.wallets.usecase.GetWalletsUseCase -import com.tangem.domain.wallets.usecase.SaveWalletUseCase +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.domain.wallets.usecase.* +import com.tangem.feature.wallet.presentation.common.state.TokenItemState import com.tangem.feature.wallet.presentation.router.InnerWalletRouter -import com.tangem.feature.wallet.presentation.wallet.state.* +import com.tangem.feature.wallet.presentation.wallet.state.WalletLockedState +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.WalletBottomSheetConfig +import com.tangem.feature.wallet.presentation.wallet.state.components.WalletCardState +import com.tangem.feature.wallet.presentation.wallet.state.components.WalletTokensListState import com.tangem.feature.wallet.presentation.wallet.state.factory.WalletStateFactory import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import com.tangem.utils.coroutines.JobHolder +import com.tangem.utils.coroutines.saveIn import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch @@ -39,15 +53,18 @@ import kotlin.properties.Delegates * [REDACTED_AUTHOR] */ -@Suppress("LongParameterList") +@Suppress("LargeClass", "LongParameterList", "TooManyFunctions") @HiltViewModel internal class WalletViewModel @Inject constructor( private val getWalletsUseCase: GetWalletsUseCase, private val saveWalletUseCase: SaveWalletUseCase, + private val getSelectedWalletUseCase: GetSelectedWalletUseCase, + private val selectWalletUseCase: SelectWalletUseCase, private val getBiometricsStatusUseCase: GetBiometricsStatusUseCase, private val setAccessCodeRequestPolicyUseCase: SetAccessCodeRequestPolicyUseCase, private val getAccessCodeSavingStatusUseCase: GetAccessCodeSavingStatusUseCase, private val getTokenListUseCase: GetTokenListUseCase, + private val getPrimaryCurrencyUseCase: GetPrimaryCurrencyUseCase, private val getCardWasScannedUseCase: GetCardWasScannedUseCase, private val isUserAlreadyRateAppUseCase: IsUserAlreadyRateAppUseCase, private val isDemoCardUseCase: IsDemoCardUseCase, @@ -55,12 +72,16 @@ internal class WalletViewModel @Inject constructor( private val txHistoryItemsCountUseCase: GetTxHistoryItemsCountUseCase, private val txHistoryItemsUseCase: GetTxHistoryItemsUseCase, private val getExploreUrlUseCase: GetExploreUrlUseCase, + private val unlockWalletsUseCase: UnlockWalletsUseCase, + private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, private val dispatchers: CoroutineDispatcherProvider, ) : ViewModel(), DefaultLifecycleObserver, WalletClickIntents { /** Feature router */ var router: InnerWalletRouter by Delegates.notNull() + private val selectedAppCurrencyFlow: StateFlow = createSelectedAppCurrencyFlow() + private val notificationsListFactory = WalletNotificationsListFactory( currentStateProvider = Provider { uiState }, wasCardScannedCallback = getCardWasScannedUseCase::invoke, @@ -72,47 +93,71 @@ internal class WalletViewModel @Inject constructor( private val stateFactory = WalletStateFactory( currentStateProvider = Provider { uiState }, currentCardTypeResolverProvider = Provider { - getCardTypeResolver(index = uiState.walletsListConfig.selectedWalletIndex) + getCardTypeResolver( + index = requireNotNull(uiState as? WalletState.ContentState).walletsListConfig.selectedWalletIndex, + ) }, + isLockedWalletProvider = Provider { + wallets[requireNotNull(uiState as? WalletState.ContentState).walletsListConfig.selectedWalletIndex].isLocked + }, + appCurrencyProvider = Provider(selectedAppCurrencyFlow::value), clickIntents = this, ) /** Screen state */ - var uiState: WalletStateHolder by mutableStateOf(stateFactory.getInitialState()) - private set + var uiState: WalletState by uiStateHolder(initialState = stateFactory.getInitialState()) private var wallets: List by Delegates.notNull() private val tokensJobHolder = JobHolder() + private val marketPriceJobHolder = JobHolder() private val notificationsJobHolder = JobHolder() override fun onCreate(owner: LifecycleOwner) { getWalletsUseCase() .flowWithLifecycle(owner.lifecycle) .distinctUntilChanged() - .onEach { wallets -> - if (wallets.isEmpty()) return@onEach - this.wallets = wallets - - uiState = stateFactory.getSkeletonState(wallets = wallets) - - updateContentItems(index = 0) - } + .onEach(::updateWallets) .flowOn(dispatchers.io) .launchIn(viewModelScope) } + private fun updateWallets(sourceList: List) { + if (sourceList.isEmpty()) return + + wallets = sourceList + + val currentState = uiState + 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 } + } + + uiState = stateFactory.getSkeletonState(wallets = sourceList, selectedWalletIndex = selectedWalletIndex) + + updateContentItems(index = selectedWalletIndex) + } + private fun updateContentItems(index: Int, isRefreshing: Boolean = false) { val cardTypeResolver = getCardTypeResolver(index) - if (cardTypeResolver.isMultiwalletAllowed()) { - updateByTokensList(index, isRefreshing) - } else { - updateByTxHistory(index) + when { + getWallet(index).isLocked -> uiState = stateFactory.getLockedState() + cardTypeResolver.isMultiwalletAllowed() -> updateMultiCurrencyContent(index, isRefreshing) + !cardTypeResolver.isMultiwalletAllowed() -> updateSingleCurrencyContent(index) } } - private fun updateByTokensList(index: Int, isRefreshing: Boolean) { - getTokenListUseCase(userWalletId = uiState.walletsListConfig.wallets[index].id) + 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( @@ -120,33 +165,57 @@ internal class WalletViewModel @Inject constructor( isRefreshing = isRefreshing, ) - tokenListEither.onRight { updateNotifications(index = index, tokenList = it) } + updateNotifications( + index = index, + tokenList = tokenListEither.fold(ifLeft = { null }, ifRight = { it }), + ) } .flowOn(dispatchers.io) .launchIn(viewModelScope) .saveIn(tokensJobHolder) } - private fun updateByTxHistory(index: Int) { + private fun updateSingleCurrencyContent(index: Int) { + val wallet = getWallet(index) + updateTxHistory( + blockchain = getCardTypeResolver(index).getBlockchain(), + derivationStyle = wallet.scanResponse.derivationStyleProvider.getDerivationStyle(), + ) + updateMarketPrice(userWalletId = wallet.walletId) + updateNotifications(index) + } + + private fun updateTxHistory(blockchain: Blockchain, derivationStyle: DerivationStyle?) { viewModelScope.launch(dispatchers.io) { - val blockchain = getWallet(index).scanResponse.cardTypesResolver.getBlockchain() + val derivationPath = blockchain.derivationPath(style = derivationStyle)?.rawPath val txHistoryItemsCountEither = txHistoryItemsCountUseCase( - networkId = blockchain.id, - derivationPath = requireNotNull(blockchain.derivationPath(style = DerivationStyle.LEGACY)).rawPath, + networkId = Network.ID(blockchain.id), + derivationPath = derivationPath, ) uiState = stateFactory.getLoadingTxHistoryState(itemsCountEither = txHistoryItemsCountEither) - txHistoryItemsCountEither.onRight { updateTxHistory(networkId = blockchain.id) } - updateNotifications(index) + txHistoryItemsCountEither.onRight { + uiState = stateFactory.getLoadedTxHistoryState( + txHistoryEither = txHistoryItemsUseCase( + networkId = Network.ID(blockchain.id), + derivationPath = derivationPath, + ).map { + it.cachedIn(viewModelScope) + }, + ) + } } } - private fun updateTxHistory(networkId: String) { - uiState = stateFactory.getLoadedTxHistoryState( - txHistoryEither = txHistoryItemsUseCase(networkId = networkId).map { it.cachedIn(viewModelScope) }, - ) + private fun updateMarketPrice(userWalletId: UserWalletId) { + getPrimaryCurrencyUseCase(userWalletId = userWalletId) + .distinctUntilChanged() + .onEach { uiState = stateFactory.getSingleCurrencyLoadedBalanceState(cryptoCurrencyEither = it) } + .flowOn(dispatchers.io) + .launchIn(viewModelScope) + .saveIn(marketPriceJobHolder) } private fun updateNotifications(index: Int, tokenList: TokenList? = null) { @@ -158,7 +227,20 @@ internal class WalletViewModel @Inject constructor( .onEach { uiState = stateFactory.getStateByNotifications(notifications = it) } .flowOn(dispatchers.io) .launchIn(viewModelScope) - .saveIn(jobHolder = notificationsJobHolder) + .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 { @@ -237,21 +319,72 @@ internal class WalletViewModel @Inject constructor( } override fun onWalletChange(index: Int) { - if (uiState.walletsListConfig.selectedWalletIndex == index) return + val state = requireNotNull(uiState as? WalletState.ContentState) { + "Impossible to change wallet if state isn't WalletState.ContentState" + } - uiState = stateFactory.getStateAfterWalletChanging(index = index) + if (state.walletsListConfig.selectedWalletIndex == index) return - updateContentItems(index = index) + /* + * 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) + notificationsJobHolder.update(job = null) + + 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)) + } else { + cacheState + } + + if (cacheState.isLoadingState()) updateContentItems(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 -> { + txHistoryState is TxHistoryState.Loading || marketPriceBlockState is MarketPriceBlockState.Loading + } + is WalletState.Initial -> false + } } override fun onRefreshSwipe() { uiState = stateFactory.getStateAfterContentRefreshing() - updateContentItems(index = uiState.walletsListConfig.selectedWalletIndex, isRefreshing = true) + + updateContentItems( + index = requireNotNull(uiState as? WalletState.ContentState).walletsListConfig.selectedWalletIndex, + isRefreshing = true, + ) } override fun onOrganizeTokensClick() { - val index = uiState.walletsListConfig.selectedWalletIndex - val walletId = uiState.walletsListConfig.wallets[index].id + val state = requireNotNull(uiState as? WalletState.ContentState) + val index = state.walletsListConfig.selectedWalletIndex + val walletId = state.walletsListConfig.wallets[index].id router.openOrganizeTokensScreen(walletId) } @@ -262,12 +395,16 @@ internal class WalletViewModel @Inject constructor( override fun onReloadClick() { uiState = stateFactory.getStateAfterContentRefreshing() - updateByTxHistory(index = uiState.walletsListConfig.selectedWalletIndex) + updateSingleCurrencyContent( + index = requireNotNull(uiState as? WalletState.ContentState).walletsListConfig.selectedWalletIndex, + ) } override fun onExploreClick() { viewModelScope.launch(dispatchers.io) { - val wallet = getWallet(uiState.walletsListConfig.selectedWalletIndex) + val wallet = getWallet( + index = requireNotNull(uiState as? WalletState.ContentState).walletsListConfig.selectedWalletIndex, + ) router.openTxHistoryWebsite( url = getExploreUrlUseCase( userWalletId = wallet.walletId, @@ -278,4 +415,43 @@ internal class WalletViewModel @Inject constructor( ) } } + + override fun onUnlockWalletClick() { + viewModelScope.launch(dispatchers.io) { + unlockWalletsUseCase() + } + } + + override fun onUnlockWalletNotificationClick() { + val state = requireNotNull(uiState as? WalletLockedState) { + "Impossible to unlock wallet if state isn't WalletLockedState" + } + + uiState = stateFactory.getStateWithOpenBottomSheet( + content = when (state) { + is WalletMultiCurrencyState.Locked -> state.bottomSheetConfig.content + is WalletSingleCurrencyState.Locked -> state.bottomSheetConfig.content + }, + ) + } + + override fun onBottomSheetDismiss() { + uiState = stateFactory.getStateWithClosedBottomSheet() + } + + override fun onTokenClick(currency: CryptoCurrency) { + router.openTokenDetails(currency = currency) + } + + private fun createSelectedAppCurrencyFlow(): StateFlow { + return getSelectedAppCurrencyUseCase() + .map { maybeAppCurrency -> + maybeAppCurrency.getOrElse { AppCurrency.Default } + } + .stateIn( + scope = viewModelScope, + started = SharingStarted.Eagerly, + initialValue = AppCurrency.Default, + ) + } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/res/drawable/ic_currency_24.xml b/features/wallet/impl/src/main/res/drawable/ic_currency_24.xml new file mode 100644 index 0000000000..bee87c2b68 --- /dev/null +++ b/features/wallet/impl/src/main/res/drawable/ic_currency_24.xml @@ -0,0 +1,9 @@ + + + diff --git a/features/wallet/impl/src/main/res/drawable/ic_empty_64.xml b/features/wallet/impl/src/main/res/drawable/ic_empty_64.xml new file mode 100644 index 0000000000..090f906247 --- /dev/null +++ b/features/wallet/impl/src/main/res/drawable/ic_empty_64.xml @@ -0,0 +1,13 @@ + + + + + + diff --git a/gradle.properties b/gradle.properties index 6f02666fb4..4131a81f9d 100644 --- a/gradle.properties +++ b/gradle.properties @@ -6,7 +6,7 @@ # http://www.gradle.org/docs/current/userguide/build_environment.html # Specifies the JVM arguments used for the daemon process. # The setting is particularly useful for tweaking memory settings. -org.gradle.jvmargs = -Xmx4096m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 +org.gradle.jvmargs = -Xmx4096m -XX:MaxMetaspaceSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 # When configured, Gradle will run in incubating parallel mode. # This option should only be used with decoupled projects. More details, visit # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects diff --git a/gradle/dependencies.toml b/gradle/dependencies.toml index 8f9dc6f917..0025b49241 100644 --- a/gradle/dependencies.toml +++ b/gradle/dependencies.toml @@ -40,7 +40,7 @@ appsflyer = "6.5.1" armadillo = "0.9.0" coil = "2.1.0" compose-shimmer = "1.0.3" -coroutine = "1.5.2" +coroutine = "1.7.2" desugarJdkLibs = "1.1.5" firebase = "26.0.0" googleMaterialComponent = "1.6.1" @@ -80,9 +80,9 @@ okHttp-prettyLogging = "3.1.0" # endregion Other libraries # region Tangem -tangemBlockchainSdk = "release-app_4.10-310" +tangemBlockchainSdk = "develop-318" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds -tangemCardSdk = "release-app_4.10-281" +tangemCardSdk = "develop-289" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds # endregion Tangem @@ -132,6 +132,7 @@ androidx-paging-runtime = { module = "androidx.paging:paging-runtime", version.r lifecycle-common-java8 = { module = "androidx.lifecycle:lifecycle-common-java8", version.ref = "androidxLifecycle" } lifecycle-runtime-ktx = { module = "androidx.lifecycle:lifecycle-runtime-ktx", version.ref = "androidxLifecycle" } lifecycle-viewModel-ktx = { module = "androidx.lifecycle:lifecycle-viewmodel-ktx", version.ref = "androidxLifecycle" } +lifecycle-compose = { module = "androidx.lifecycle:lifecycle-runtime-compose", version.ref = "androidxLifecycle" } # region AndroidX # region Compose diff --git a/libs/crypto/src/main/java/com/tangem/lib/crypto/TxHistoryManager.kt b/libs/crypto/src/main/java/com/tangem/lib/crypto/TxHistoryManager.kt deleted file mode 100644 index de50d8b4ee..0000000000 --- a/libs/crypto/src/main/java/com/tangem/lib/crypto/TxHistoryManager.kt +++ /dev/null @@ -1,18 +0,0 @@ -package com.tangem.lib.crypto - -import com.tangem.lib.crypto.models.txhistory.ProxyTransactionHistoryItem -import com.tangem.lib.crypto.models.txhistory.ProxyTransactionHistoryState - -interface TxHistoryManager { - - @Throws(IllegalStateException::class) - suspend fun checkTxHistoryState(networkId: String, derivationPath: String?): ProxyTransactionHistoryState - - @Throws(IllegalStateException::class) - suspend fun getTxHistoryItems( - networkId: String, - derivationPath: String?, - page: Int, - pageSize: Int, - ): List -} \ No newline at end of file diff --git a/libs/crypto/src/main/java/com/tangem/lib/crypto/models/txhistory/ProxyTransactionHistoryItem.kt b/libs/crypto/src/main/java/com/tangem/lib/crypto/models/txhistory/ProxyTransactionHistoryItem.kt deleted file mode 100644 index 387a7518e6..0000000000 --- a/libs/crypto/src/main/java/com/tangem/lib/crypto/models/txhistory/ProxyTransactionHistoryItem.kt +++ /dev/null @@ -1,21 +0,0 @@ -package com.tangem.lib.crypto.models.txhistory - -import com.tangem.lib.crypto.models.ProxyAmount - -data class ProxyTransactionHistoryItem( - val txHash: String, - val timestamp: Long, - val direction: TransactionDirection, - val status: ProxyTransactionStatus, - val type: TransactionType, - val amount: ProxyAmount, -) { - sealed interface TransactionDirection { - data class Incoming(val from: String) : TransactionDirection - data class Outgoing(val to: String) : TransactionDirection - } - - sealed interface TransactionType { - object Transfer : TransactionType - } -} \ No newline at end of file diff --git a/libs/crypto/src/main/java/com/tangem/lib/crypto/models/txhistory/ProxyTransactionHistoryState.kt b/libs/crypto/src/main/java/com/tangem/lib/crypto/models/txhistory/ProxyTransactionHistoryState.kt deleted file mode 100644 index ddeeba4d3f..0000000000 --- a/libs/crypto/src/main/java/com/tangem/lib/crypto/models/txhistory/ProxyTransactionHistoryState.kt +++ /dev/null @@ -1,15 +0,0 @@ -package com.tangem.lib.crypto.models.txhistory - -sealed class ProxyTransactionHistoryState { - - sealed class Success : ProxyTransactionHistoryState() { - object Empty : Success() - data class HasTransactions(val txCount: Int) : Success() - } - - sealed class Failed : ProxyTransactionHistoryState() { - data class FetchError(val exception: Exception) : Failed() - } - - object NotImplemented : ProxyTransactionHistoryState() -} \ No newline at end of file diff --git a/libs/crypto/src/main/java/com/tangem/lib/crypto/models/txhistory/ProxyTransactionStatus.kt b/libs/crypto/src/main/java/com/tangem/lib/crypto/models/txhistory/ProxyTransactionStatus.kt deleted file mode 100644 index 2e5ffd5aaf..0000000000 --- a/libs/crypto/src/main/java/com/tangem/lib/crypto/models/txhistory/ProxyTransactionStatus.kt +++ /dev/null @@ -1,3 +0,0 @@ -package com.tangem.lib.crypto.models.txhistory - -enum class ProxyTransactionStatus { Confirmed, Unconfirmed } \ No newline at end of file diff --git a/plugins/configuration/build.gradle.kts b/plugins/configuration/build.gradle.kts index c2ab7a0a54..a8bc7da8a0 100644 --- a/plugins/configuration/build.gradle.kts +++ b/plugins/configuration/build.gradle.kts @@ -9,8 +9,8 @@ repositories { } configure { - sourceCompatibility = JavaVersion.VERSION_11 - targetCompatibility = JavaVersion.VERSION_11 + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 } dependencies { diff --git a/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/DetektConfigurations.kt b/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/DetektConfigurations.kt index 7f3919e6d8..44c658db99 100644 --- a/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/DetektConfigurations.kt +++ b/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/DetektConfigurations.kt @@ -47,6 +47,6 @@ private fun Project.configureDetektTask() { } } - jvmTarget = "11" + jvmTarget = "17" } } \ No newline at end of file diff --git a/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/KotlinConfigurations.kt b/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/KotlinConfigurations.kt index 9d10cad98d..d0e39d6777 100644 --- a/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/KotlinConfigurations.kt +++ b/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/KotlinConfigurations.kt @@ -7,7 +7,7 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile internal fun Project.configureKotlinCompilerOptions() { project.tasks.withType { kotlinOptions { - jvmTarget = "11" + jvmTarget = "17" allWarningsAsErrors = false // this is required to produce a unique META-INF/*.kotlin_module files moduleName = project.path.removePrefix(":").replace(':', '-') diff --git a/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/extension/BaseExtensionConfigurations.kt b/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/extension/BaseExtensionConfigurations.kt index c05a661a44..3504fee112 100644 --- a/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/extension/BaseExtensionConfigurations.kt +++ b/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/extension/BaseExtensionConfigurations.kt @@ -12,20 +12,22 @@ internal fun BaseExtension.configureCompileSdk() { internal fun BaseExtension.configureCompilerOptions() { compileOptions { - sourceCompatibility = JavaVersion.VERSION_11 - targetCompatibility = JavaVersion.VERSION_11 + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 } } internal fun BaseExtension.configureCompose(project: Project) { val useCompose = with(project.path) { contains(":ui") || - contains(":onboarding") || // TODO: divide on api/impl after migrating all onboarding to module - contains(":presentation") || - contains(":app") || // TODO: [REDACTED_JIRA] - contains(":impl") + contains(Regex(pattern = ":onboarding\$")) || // TODO: divide on api/impl after migrating all onboarding to module + contains(Regex(pattern = ":presentation\$")) || + contains(Regex(pattern = ":app\$")) || // TODO: [REDACTED_JIRA] + contains(Regex(pattern = ":impl\$")) } + buildFeatures.compose = useCompose + if (useCompose) { composeOptions { kotlinCompilerExtensionVersion = project.findVersion(alias = "compose-compiler").requiredVersion diff --git a/settings.gradle.kts b/settings.gradle.kts index 7378b7b744..33ccfc81d6 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -107,6 +107,9 @@ include(":domain:tokens:models") include(":domain:wallets") include(":domain:wallets:models") include(":domain:txhistory") +include(":domain:txhistory:models") +include(":domain:app-currency") +include(":domain:app-currency:models") // endregion Domain modules // region Data modules @@ -116,4 +119,5 @@ include(":data:tokens") include(":data:source:preferences") include(":data:settings") include(":data:txhistory") +include(":data:app-currency") // endregion Data modules \ No newline at end of file diff --git a/tangem-android-tools b/tangem-android-tools index 03186c35c1..0e4934995b 160000 --- a/tangem-android-tools +++ b/tangem-android-tools @@ -1 +1 @@ -Subproject commit 03186c35c13d8693d9a4c7dcb6e760fa58984e67 +Subproject commit 0e4934995b8e7c69862ef80362e86bd166bd1288