diff --git a/app/src/main/java/com/tangem/tap/MainActivity.kt b/app/src/main/java/com/tangem/tap/MainActivity.kt index 9f1970f662..2b66b30ca7 100644 --- a/app/src/main/java/com/tangem/tap/MainActivity.kt +++ b/app/src/main/java/com/tangem/tap/MainActivity.kt @@ -35,13 +35,10 @@ import com.tangem.tap.common.OnActivityResultCallback import com.tangem.tap.common.SnackbarHandler import com.tangem.tap.common.apptheme.MutableAppThemeModeHolder import com.tangem.tap.common.redux.NotificationsHandler -import com.tangem.tap.common.redux.global.GlobalAction import com.tangem.tap.common.shop.googlepay.GooglePayService import com.tangem.tap.common.shop.googlepay.GooglePayService.Companion.LOAD_PAYMENT_DATA_REQUEST_CODE import com.tangem.tap.common.shop.googlepay.GooglePayUtil.createPaymentsClient import com.tangem.tap.domain.TangemSdkManager -import com.tangem.tap.domain.userWalletList.di.provideBiometricImplementation -import com.tangem.tap.domain.userWalletList.di.provideRuntimeImplementation import com.tangem.tap.domain.walletconnect2.domain.WalletConnectInteractor import com.tangem.tap.features.intentHandler.IntentProcessor import com.tangem.tap.features.intentHandler.handlers.BackgroundScanIntentHandler @@ -149,7 +146,6 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac backupService = BackupService.init(cardSdkConfigRepository.sdk, this) lockUserWalletsTimer = LockUserWalletsTimer(owner = this) - initUserWalletsListManager() initIntentHandlers() store.dispatch( @@ -251,18 +247,6 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac intentProcessor.addHandler(SellCurrencyIntentHandler()) } - private fun initUserWalletsListManager() { - val manager = if (preferencesStorage.shouldSaveUserWallets) { - UserWalletsListManager.provideBiometricImplementation( - context = applicationContext, - tangemSdkManager = tangemSdkManager, - ) - } else { - UserWalletsListManager.provideRuntimeImplementation() - } - store.dispatch(GlobalAction.UpdateUserWalletsListManager(manager)) - } - private fun updateAppTheme(appThemeMode: AppThemeMode) { MutableAppThemeModeHolder.value = appThemeMode MutableAppThemeModeHolder.isDarkThemeActive = isDarkTheme() diff --git a/app/src/main/java/com/tangem/tap/TapApplication.kt b/app/src/main/java/com/tangem/tap/TapApplication.kt index 6992e82d7e..919b23292e 100644 --- a/app/src/main/java/com/tangem/tap/TapApplication.kt +++ b/app/src/main/java/com/tangem/tap/TapApplication.kt @@ -32,6 +32,7 @@ import com.tangem.domain.common.LogConfig import com.tangem.domain.settings.repositories.AppRatingRepository import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.walletmanager.WalletManagersFacade +import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.wallets.legacy.WalletManagersRepository import com.tangem.features.tokendetails.featuretoggles.TokenDetailsFeatureToggles import com.tangem.features.wallet.featuretoggles.WalletFeatureToggles @@ -58,6 +59,8 @@ import com.tangem.tap.domain.tokens.UserTokensRepository import com.tangem.tap.domain.tokens.UserTokensStorageService import com.tangem.tap.domain.totalBalance.TotalFiatBalanceCalculator import com.tangem.tap.domain.totalBalance.di.provideDefaultImplementation +import com.tangem.tap.domain.userWalletList.di.provideBiometricImplementation +import com.tangem.tap.domain.userWalletList.di.provideRuntimeImplementation import com.tangem.tap.domain.walletCurrencies.WalletCurrenciesManager import com.tangem.tap.domain.walletCurrencies.di.provideDefaultImplementation import com.tangem.tap.domain.walletStores.WalletStoresManager @@ -252,6 +255,16 @@ internal class TapApplication : Application(), ImageLoaderFactory { walletConnectRepository = WalletConnectRepository(this) val configLoader = FeaturesLocalLoader(assetReader, MoshiConverter.sdkMoshi, BuildConfig.ENVIRONMENT) + initUserWalletsListManager() + + // TODO: Try to performance and user experience. + // [REDACTED_JIRA] + runBlocking { + featureTogglesManager.init() + appRatingRepository.initialize() + // learn2earnInteractor.init() + } + initConfigManager(configLoader, ::initWithConfigDependency) initWarningMessagesManager() @@ -279,14 +292,6 @@ internal class TapApplication : Application(), ImageLoaderFactory { appStateHolder.userTokensRepository = userTokensRepository appStateHolder.walletStoresManager = walletStoresManager - // TODO: Try to performance and user experience. - // [REDACTED_JIRA] - runBlocking { - featureTogglesManager.init() - appRatingRepository.initialize() - // learn2earnInteractor.init() - } - initTopUpController() walletConnect2Repository.init(projectId = configManager.config.walletConnectProjectId) } @@ -350,14 +355,20 @@ internal class TapApplication : Application(), ImageLoaderFactory { foregroundActivityObserver: ForegroundActivityObserver, store: Store, ) { - fun initAdditionalFeedbackInfo(context: Context): AdditionalFeedbackInfo = AdditionalFeedbackInfo().apply { - appVersion = try { - // TODO don't use deprecated method - val pInfo = context.packageManager.getPackageInfo(context.packageName, 0) - pInfo.versionName - } catch (e: PackageManager.NameNotFoundException) { - e.printStackTrace() - "x.y.z" + fun initAdditionalFeedbackInfo(context: Context): AdditionalFeedbackInfo { + return AdditionalFeedbackInfo( + userWalletsListManager = userWalletsListManager, + walletManagersFacade = walletManagersFacade, + walletFeatureToggles = walletFeatureToggles, + ).apply { + appVersion = try { + // TODO don't use deprecated method + val pInfo = context.packageManager.getPackageInfo(context.packageName, 0) + pInfo.versionName + } catch (e: PackageManager.NameNotFoundException) { + e.printStackTrace() + "x.y.z" + } } } @@ -391,4 +402,14 @@ internal class TapApplication : Application(), ImageLoaderFactory { private fun initWarningMessagesManager() { store.dispatch(GlobalAction.SetWarningManager(WarningMessagesManager())) } + + private fun initUserWalletsListManager() { + val manager = if (preferencesStorage.shouldSaveUserWallets) { + UserWalletsListManager.provideBiometricImplementation(applicationContext) + } else { + UserWalletsListManager.provideRuntimeImplementation() + } + + store.dispatch(GlobalAction.UpdateUserWalletsListManager(manager)) + } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/feedback/AdditionalFeedbackInfo.kt b/app/src/main/java/com/tangem/tap/common/feedback/AdditionalFeedbackInfo.kt index 947691b26f..a3407ad4d2 100644 --- a/app/src/main/java/com/tangem/tap/common/feedback/AdditionalFeedbackInfo.kt +++ b/app/src/main/java/com/tangem/tap/common/feedback/AdditionalFeedbackInfo.kt @@ -6,9 +6,36 @@ import com.tangem.blockchain.common.address.Address import com.tangem.domain.models.scan.CardDTO import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.userwallets.UserWalletIdBuilder +import com.tangem.domain.walletmanager.WalletManagersFacade +import com.tangem.domain.wallets.legacy.UserWalletsListManager +import com.tangem.features.wallet.featuretoggles.WalletFeatureToggles import com.tangem.tap.common.extensions.stripZeroPlainString +import com.tangem.tap.scope +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach + +class AdditionalFeedbackInfo( + userWalletsListManager: UserWalletsListManager, + walletManagersFacade: WalletManagersFacade, + walletFeatureToggles: WalletFeatureToggles, +) { + + init { + if (walletFeatureToggles.isRedesignedScreenEnabled) { + userWalletsListManager.selectedUserWallet + .distinctUntilChanged() + .onEach { userWallet -> + setCardInfo(data = userWallet.scanResponse) + + walletManagersFacade.getAll(userWalletId = userWallet.walletId) + .onEach(::setWalletsInfo) + .launchIn(scope) + } + .launchIn(scope) + } + } -class AdditionalFeedbackInfo { class EmailWalletInfo( var blockchain: Blockchain = Blockchain.Unknown, var derivationPath: String = "", @@ -46,6 +73,7 @@ class AdditionalFeedbackInfo { private val Address.name: String get() = type.javaClass.simpleName + @Deprecated("Don't use it directly") fun setCardInfo(data: ScanResponse) { cardId = data.card.cardId cardBlockchain = data.walletData?.blockchain ?: "" @@ -55,6 +83,7 @@ class AdditionalFeedbackInfo { userWalletId = UserWalletIdBuilder.scanResponse(data).build()?.stringValue ?: "" } + @Deprecated("Don't use it directly") fun setWalletsInfo(walletManagers: List) { walletsInfo.clear() tokens.clear() diff --git a/app/src/main/java/com/tangem/tap/domain/userWalletList/di/UserWalletsListManagerProvider.kt b/app/src/main/java/com/tangem/tap/domain/userWalletList/di/UserWalletsListManagerProvider.kt index 2657240db8..719491a036 100644 --- a/app/src/main/java/com/tangem/tap/domain/userWalletList/di/UserWalletsListManagerProvider.kt +++ b/app/src/main/java/com/tangem/tap/domain/userWalletList/di/UserWalletsListManagerProvider.kt @@ -3,27 +3,28 @@ package com.tangem.tap.domain.userWalletList.di import android.content.Context import com.squareup.moshi.Moshi import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory +import com.tangem.common.Provider import com.tangem.common.authentication.AuthenticatedStorage import com.tangem.common.json.TangemSdkAdapter import com.tangem.common.services.secure.SecureStorage import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.sdk.storage.AndroidSecureStorage import com.tangem.sdk.storage.createEncryptedSharedPreferences -import com.tangem.tap.domain.TangemSdkManager import com.tangem.tap.domain.userWalletList.implementation.BiometricUserWalletsListManager import com.tangem.tap.domain.userWalletList.implementation.RuntimeUserWalletsListManager +import com.tangem.tap.domain.userWalletList.repository.DelegatedKeystoreManager import com.tangem.tap.domain.userWalletList.repository.UserWalletsKeysStoreDecorator import com.tangem.tap.domain.userWalletList.repository.implementation.BiometricUserWalletsKeysRepository import com.tangem.tap.domain.userWalletList.repository.implementation.DefaultSelectedUserWalletRepository import com.tangem.tap.domain.userWalletList.repository.implementation.DefaultUserWalletsPublicInformationRepository import com.tangem.tap.domain.userWalletList.repository.implementation.DefaultUserWalletsSensitiveInformationRepository import com.tangem.tap.domain.userWalletList.utils.json.* +import com.tangem.tap.tangemSdkManager private const val USER_WALLETS_STORAGE_NAME = "user_wallets_storage" fun UserWalletsListManager.Companion.provideBiometricImplementation( - context: Context, - tangemSdkManager: TangemSdkManager, + applicationContext: Context, ): UserWalletsListManager { val moshi = Moshi.Builder() .add(WalletDerivedKeysMapAdapter()) @@ -40,7 +41,7 @@ fun UserWalletsListManager.Companion.provideBiometricImplementation( val secureStorage = AndroidSecureStorage( preferences = SecureStorage.createEncryptedSharedPreferences( - context = context, + context = applicationContext, storageName = USER_WALLETS_STORAGE_NAME, ), ) @@ -48,16 +49,17 @@ fun UserWalletsListManager.Companion.provideBiometricImplementation( val authenticatedStorage = AuthenticatedStorage( secureStorage = UserWalletsKeysStoreDecorator( featureStorage = secureStorage, - cardSdkStorage = tangemSdkManager.secureStorage, + cardSdkStorageProvider = Provider { tangemSdkManager.secureStorage }, + ), + keystoreManager = DelegatedKeystoreManager( + keystoreManagerProvider = Provider { tangemSdkManager.keystoreManager }, ), - keystoreManager = tangemSdkManager.keystoreManager, ) val keysRepository = BiometricUserWalletsKeysRepository( moshi = moshi, secureStorage = secureStorage, authenticatedStorage = authenticatedStorage, - ) val publicInformationRepository = DefaultUserWalletsPublicInformationRepository( moshi = moshi, diff --git a/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/DelegatedKeystoreManager.kt b/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/DelegatedKeystoreManager.kt new file mode 100644 index 0000000000..64e0f9a1ce --- /dev/null +++ b/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/DelegatedKeystoreManager.kt @@ -0,0 +1,18 @@ +package com.tangem.tap.domain.userWalletList.repository + +import com.tangem.common.Provider +import com.tangem.common.authentication.KeystoreManager +import javax.crypto.SecretKey + +internal class DelegatedKeystoreManager( + private val keystoreManagerProvider: Provider, +) : KeystoreManager { + + override suspend fun authenticateAndGetKey(keyAlias: String): SecretKey? { + return keystoreManagerProvider().authenticateAndGetKey(keyAlias) + } + + override suspend fun storeKey(keyAlias: String, key: SecretKey) { + keystoreManagerProvider().storeKey(keyAlias, key) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/UserWalletsKeysStoreDecorator.kt b/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/UserWalletsKeysStoreDecorator.kt index 662da27e35..49e87a395a 100644 --- a/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/UserWalletsKeysStoreDecorator.kt +++ b/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/UserWalletsKeysStoreDecorator.kt @@ -1,28 +1,29 @@ package com.tangem.tap.domain.userWalletList.repository +import com.tangem.common.Provider import com.tangem.common.services.secure.SecureStorage /** * A decorator for [SecureStorage] that facilitates data migration between two storages. * * @property featureStorage The primary storage, which will eventually contain all user data. - * @property cardSdkStorage The SDK's storage where user data might have been previously stored. + * @property cardSdkStorageProvider The SDK's storage where user data might have been previously stored. */ internal class UserWalletsKeysStoreDecorator( private val featureStorage: SecureStorage, - private val cardSdkStorage: SecureStorage, + private val cardSdkStorageProvider: Provider, ) : SecureStorage by featureStorage { override fun delete(account: String) { featureStorage.delete(account) - cardSdkStorage.delete(account) + cardSdkStorageProvider().delete(account) } override fun get(account: String): ByteArray? { var data = featureStorage.get(account) if (data == null) { - data = cardSdkStorage.get(account) ?: return null + data = cardSdkStorageProvider().get(account) ?: return null featureStorage.store(data, account) } diff --git a/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/viewmodels/AddCustomTokenViewModel.kt b/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/viewmodels/AddCustomTokenViewModel.kt index dffe1709dd..660be89b17 100644 --- a/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/viewmodels/AddCustomTokenViewModel.kt +++ b/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/viewmodels/AddCustomTokenViewModel.kt @@ -22,6 +22,7 @@ import com.tangem.domain.common.extensions.* import com.tangem.domain.common.util.cardTypesResolver import com.tangem.domain.common.util.derivationStyleProvider import com.tangem.domain.features.addCustomToken.CustomCurrency +import com.tangem.domain.wallets.usecase.GetSelectedWalletUseCase import com.tangem.tap.domain.model.WalletDataModel import com.tangem.tap.features.customtoken.impl.domain.CustomTokenInteractor import com.tangem.tap.features.customtoken.impl.domain.models.FoundToken @@ -35,7 +36,6 @@ import com.tangem.tap.features.customtoken.impl.presentation.validators.ContactA import com.tangem.tap.features.customtoken.impl.presentation.validators.ContractAddressValidatorResult import com.tangem.tap.features.details.ui.cardsettings.TextReference import com.tangem.tap.features.wallet.models.Currency -import com.tangem.tap.proxy.AppStateHolder import com.tangem.tap.store import com.tangem.utils.coroutines.AppCoroutineDispatcherProvider import com.tangem.utils.coroutines.runCatching @@ -51,11 +51,11 @@ import javax.inject.Inject /** * ViewModel for add custom token screen * - * @param analyticsEventHandler analytics event handler - * @param featureRouter feature router - * @property featureInteractor feature interactor - * @property dispatchers coroutine dispatchers provider - * @property reduxStateHolder redux state holder + * @param analyticsEventHandler analytics event handler + * @param featureRouter feature router + * @property featureInteractor feature interactor + * @property getSelectedWalletUseCase use case that returns selected wallet + * @property dispatchers coroutine dispatchers provider * [REDACTED_AUTHOR] */ @@ -66,7 +66,7 @@ internal class AddCustomTokenViewModel @Inject constructor( featureRouter: CustomTokenRouter, private val featureInteractor: CustomTokenInteractor, private val dispatchers: AppCoroutineDispatcherProvider, - private val reduxStateHolder: AppStateHolder, + private val getSelectedWalletUseCase: GetSelectedWalletUseCase, ) : ViewModel(), DefaultLifecycleObserver { private val analyticsSender = AddCustomTokenAnalyticsSender(analyticsEventHandler) @@ -208,7 +208,10 @@ internal class AddCustomTokenViewModel @Inject constructor( private fun getNetworkSelectorItems(): List { val defaultNetwork = createNetworkSelectorItem(blockchain = Blockchain.Unknown) - val scanResponse = reduxStateHolder.scanResponse + val scanResponse = getSelectedWalletUseCase().fold( + ifLeft = { null }, + ifRight = { it.scanResponse }, + ) val derivationStyle = scanResponse?.derivationStyleProvider?.getDerivationStyle() return listOf(defaultNetwork) + Blockchain.values() .filter { blockchain -> @@ -262,17 +265,20 @@ internal class AddCustomTokenViewModel @Inject constructor( } private fun createDerivationPathsSelectorField(): AddCustomTokenSelectorField.DerivationPath? { - val scanResponse = reduxStateHolder.scanResponse - if (scanResponse?.card?.settings?.isHDWalletAllowed == false) return null + return getSelectedWalletUseCase().fold( + ifLeft = { null }, + ifRight = { + if (!it.scanResponse.card.settings.isHDWalletAllowed) return null - val selectorItems = - getDerivationPathsSelectorItems(scanResponse?.derivationStyleProvider) - return AddCustomTokenSelectorField.DerivationPath( - label = TextReference.Res(R.string.custom_token_derivation_path_input_title), - selectedItem = requireNotNull(selectorItems.firstOrNull()), - items = selectorItems, - onMenuItemClick = actionsHandler::onDerivationPathSelectorItemClick, - isEnabled = true, + val selectorItems = getDerivationPathsSelectorItems(it.scanResponse.derivationStyleProvider) + AddCustomTokenSelectorField.DerivationPath( + label = TextReference.Res(R.string.custom_token_derivation_path_input_title), + selectedItem = requireNotNull(selectorItems.firstOrNull()), + items = selectorItems, + onMenuItemClick = actionsHandler::onDerivationPathSelectorItemClick, + isEnabled = true, + ) + }, ) } @@ -315,14 +321,19 @@ internal class AddCustomTokenViewModel @Inject constructor( } private fun createDerivationPathInputField(): AddCustomTokenInputField.DerivationPath? { - if (reduxStateHolder.scanResponse?.card?.settings?.isHDWalletAllowed == false) return null + return getSelectedWalletUseCase().fold( + ifLeft = { null }, + ifRight = { + if (!it.scanResponse.card.settings.isHDWalletAllowed) return null - return AddCustomTokenInputField.DerivationPath( - value = "", - onValueChange = actionsHandler::onDerivationPathValueChange, - keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done), - label = TextReference.Res(R.string.custom_token_custom_derivation), - placeholder = TextReference.Str(value = DERIVATION_PATH_PLACEHOLDER), + AddCustomTokenInputField.DerivationPath( + value = "", + onValueChange = actionsHandler::onDerivationPathValueChange, + keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done), + label = TextReference.Res(R.string.custom_token_custom_derivation), + placeholder = TextReference.Str(value = DERIVATION_PATH_PLACEHOLDER), + ) + }, ) } } @@ -440,11 +451,15 @@ internal class AddCustomTokenViewModel @Inject constructor( val isSupportedToken = if (!isNetworkSelected()) { true } else { - val scanResponse = reduxStateHolder.scanResponse - scanResponse?.card?.canHandleToken( - blockchain = networkSelectorValue, - cardTypesResolver = scanResponse.cardTypesResolver, - ) ?: false + getSelectedWalletUseCase().fold( + ifLeft = { false }, + ifRight = { + it.scanResponse.card.canHandleToken( + blockchain = networkSelectorValue, + cardTypesResolver = it.scanResponse.cardTypesResolver, + ) + }, + ) } return buildSet { @@ -478,13 +493,16 @@ internal class AddCustomTokenViewModel @Inject constructor( address = uiState.form.contractAddressInputField.value, blockchain = networkSelectorValue, ) - val scanResponse = reduxStateHolder.scanResponse - val isSupportedToken = scanResponse?.card - ?.canHandleToken( - blockchain = networkSelectorValue, - cardTypesResolver = scanResponse.cardTypesResolver, - ) - ?: false + + val isSupportedToken = getSelectedWalletUseCase().fold( + ifLeft = { false }, + ifRight = { + it.scanResponse.card.canHandleToken( + blockchain = networkSelectorValue, + cardTypesResolver = it.scanResponse.cardTypesResolver, + ) + }, + ) uiState.copySealed( floatingButton = uiState.floatingButton.copy( @@ -641,8 +659,12 @@ internal class AddCustomTokenViewModel @Inject constructor( private fun getDerivationPathForBlockchain(blockchain: Blockchain?): DerivationPath? { if (blockchain == null) return null - val derivationStyle = reduxStateHolder.scanResponse?.derivationStyleProvider?.getDerivationStyle() - ?: DerivationStyle.V1 + val derivationStyle = getSelectedWalletUseCase().fold( + ifLeft = { null }, + ifRight = { + it.scanResponse.derivationStyleProvider.getDerivationStyle() + }, + ) val derivationNetwork = if (blockchain == Blockchain.Unknown) { uiState.form.networkSelectorField.selectedItem.blockchain @@ -653,12 +675,15 @@ internal class AddCustomTokenViewModel @Inject constructor( } private fun isUnsupportedBlockchain(blockchain: Blockchain): Boolean { - val scanResponse = reduxStateHolder.scanResponse - val canHandleToken = scanResponse?.card?.canHandleBlockchain( - blockchain = blockchain, - cardTypesResolver = scanResponse.cardTypesResolver, - ) ?: false - return !canHandleToken + return getSelectedWalletUseCase().fold( + ifLeft = { false }, + ifRight = { + !it.scanResponse.card.canHandleBlockchain( + blockchain = blockchain, + cardTypesResolver = it.scanResponse.cardTypesResolver, + ) + }, + ) } private inner class ActionsHandler(private val featureRouter: CustomTokenRouter) { @@ -827,7 +852,6 @@ internal class AddCustomTokenViewModel @Inject constructor( } fun onResetButtonClick() { - val scanResponse = reduxStateHolder.scanResponse with(uiState.form) { uiState = uiState.copySealed( form = uiState.form.copy( diff --git a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsAction.kt b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsAction.kt index 9dd0c59cef..0190c0aa97 100644 --- a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsAction.kt +++ b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsAction.kt @@ -74,8 +74,7 @@ sealed class DetailsAction : Action { } data class CheckBiometricsStatus( - val awaitStatusChange: Boolean, - val lifecycleCoroutineScope: LifecycleCoroutineScope, + val lifecycleScope: LifecycleCoroutineScope, ) : AppSettings() object EnrollBiometrics : AppSettings() diff --git a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsMiddleware.kt b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsMiddleware.kt index 1c13b7ed80..bdf4867874 100644 --- a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsMiddleware.kt @@ -40,7 +40,8 @@ import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsAction import com.tangem.tap.features.wallet.redux.WalletAction import com.tangem.tap.features.walletSelector.redux.WalletSelectorAction import com.tangem.tap.proxy.redux.DaggerGraphState -import com.tangem.tap.tangemSdkManager +import com.tangem.utils.coroutines.JobHolder +import com.tangem.utils.coroutines.saveIn import com.tangem.wallet.R import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay @@ -209,6 +210,9 @@ class DetailsMiddleware { } class AppSettingsMiddleware { + + private val checkBiometricsStatusJobHolder = JobHolder() + fun handle(state: DetailsState, action: DetailsAction.AppSettings) { when (action) { is DetailsAction.AppSettings.SwitchPrivacySetting -> { @@ -218,11 +222,7 @@ class DetailsMiddleware { } } is DetailsAction.AppSettings.CheckBiometricsStatus -> { - checkBiometricsStatus( - awaitStatusChange = action.awaitStatusChange, - state = state, - lifecycleScope = action.lifecycleCoroutineScope, - ) + observeBiometricsStatusChanges(state, action.lifecycleScope) } is DetailsAction.AppSettings.EnrollBiometrics -> { enrollBiometrics() @@ -245,27 +245,20 @@ class DetailsMiddleware { } } - /** - * @param awaitStatusChange If true then start a new coroutine and check the biometric status every 100 - * milliseconds until it changes - * */ - private fun checkBiometricsStatus( - awaitStatusChange: Boolean, - state: DetailsState, - lifecycleScope: LifecycleCoroutineScope, - ) { - lifecycleScope.launch { - if (awaitStatusChange) { - while (state.appSettingsState.needEnrollBiometrics == tangemSdkManager.needEnrollBiometrics) { - delay(timeMillis = 100) + private fun observeBiometricsStatusChanges(state: DetailsState, lifecycleScope: LifecycleCoroutineScope) { + lifecycleScope.launch(Dispatchers.IO) { + do { + val needEnrollBiometrics = runCatching(tangemSdkManager::needEnrollBiometrics).getOrNull() + + if (needEnrollBiometrics != null && + needEnrollBiometrics != state.appSettingsState.needEnrollBiometrics + ) { + store.dispatchWithMain(DetailsAction.AppSettings.BiometricsStatusChanged(needEnrollBiometrics)) } - } - store.dispatchWithMain( - DetailsAction.AppSettings.BiometricsStatusChanged( - needEnrollBiometrics = tangemSdkManager.needEnrollBiometrics, - ), - ) - } + + delay(timeMillis = 500) + } while (true) + }.saveIn(checkBiometricsStatusJobHolder) } private fun enrollBiometrics() { @@ -454,10 +447,7 @@ class DetailsMiddleware { return null } - return UserWalletsListManager.provideBiometricImplementation( - context = context, - tangemSdkManager = tangemSdkManager, - ) + return UserWalletsListManager.provideBiometricImplementation(context) } } diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsFragment.kt b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsFragment.kt index 974c0937d9..122e9437d1 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsFragment.kt @@ -1,6 +1,5 @@ package com.tangem.tap.features.details.ui.appsettings -import android.os.Bundle import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.lifecycle.lifecycleScope @@ -32,11 +31,6 @@ internal class AppSettingsFragment : ComposeFragment(), StoreSubscriber @@ -58,11 +57,6 @@ internal class AppSettingsFragment : ComposeFragment(), StoreSubscriber { diff --git a/app/src/main/java/com/tangem/tap/features/saveWallet/redux/SaveWalletMiddleware.kt b/app/src/main/java/com/tangem/tap/features/saveWallet/redux/SaveWalletMiddleware.kt index 5305d35dfb..29e8ad6a02 100644 --- a/app/src/main/java/com/tangem/tap/features/saveWallet/redux/SaveWalletMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/saveWallet/redux/SaveWalletMiddleware.kt @@ -140,10 +140,7 @@ internal class SaveWalletMiddleware { store.dispatchWithMain(SaveWalletAction.Save.Error(TangemSdkError.ExceptionError(error))) return } - val manager = UserWalletsListManager.provideBiometricImplementation( - context = context, - tangemSdkManager = tangemSdkManager, - ) + val manager = UserWalletsListManager.provideBiometricImplementation(context) store.dispatchWithMain(GlobalAction.UpdateUserWalletsListManager(manager)) } diff --git a/app/src/main/java/com/tangem/tap/features/tokens/impl/data/DefaultTokensListRepository.kt b/app/src/main/java/com/tangem/tap/features/tokens/impl/data/DefaultTokensListRepository.kt index 7ff7b78228..7f6ecf5aec 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/impl/data/DefaultTokensListRepository.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/impl/data/DefaultTokensListRepository.kt @@ -6,26 +6,26 @@ import androidx.paging.PagingData import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.local.testnet.TestnetTokensStorage import com.tangem.domain.common.TapWorkarounds.isTestCard +import com.tangem.domain.wallets.usecase.GetSelectedWalletUseCase import com.tangem.tap.features.tokens.impl.domain.TokensListRepository import com.tangem.tap.features.tokens.impl.domain.models.Token -import com.tangem.tap.proxy.AppStateHolder import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.flow.Flow /** * Default repository implementation of tokens list feature * - * @property tangemTechApi Tangem Tech API - * @property dispatchers coroutine dispatchers provider - * @property reduxStateHolder redux state holder - * @property testnetTokensStorage storage for getting testnet tokens data + * @property tangemTechApi Tangem Tech API + * @property dispatchers coroutine dispatchers provider + * @property getSelectedWalletUseCase use case that returns selected wallet + * @property testnetTokensStorage storage for getting testnet tokens data * [REDACTED_AUTHOR] */ internal class DefaultTokensListRepository( private val tangemTechApi: TangemTechApi, private val dispatchers: CoroutineDispatcherProvider, - private val reduxStateHolder: AppStateHolder, + private val getSelectedWalletUseCase: GetSelectedWalletUseCase, private val testnetTokensStorage: TestnetTokensStorage, ) : TokensListRepository { @@ -37,16 +37,23 @@ internal class DefaultTokensListRepository( enablePlaceholders = false, ), pagingSourceFactory = { - if (reduxStateHolder.scanResponse?.card?.isTestCard == true) { - TestnetTokensPagingSource(testnetTokensStorage, searchText) - } else { - TangemApiTokensPagingSource( - api = tangemTechApi, - dispatchers = dispatchers, - reduxStateHolder = reduxStateHolder, - searchText = searchText, - ) - } + val defaultSource = TangemApiTokensPagingSource( + api = tangemTechApi, + dispatchers = dispatchers, + getSelectedWalletUseCase = getSelectedWalletUseCase, + searchText = searchText, + ) + + getSelectedWalletUseCase().fold( + ifLeft = { defaultSource }, + ifRight = { + if (it.scanResponse.card.isTestCard) { + TestnetTokensPagingSource(testnetTokensStorage, searchText) + } else { + defaultSource + } + }, + ) }, ).flow } diff --git a/app/src/main/java/com/tangem/tap/features/tokens/impl/data/TangemApiTokensPagingSource.kt b/app/src/main/java/com/tangem/tap/features/tokens/impl/data/TangemApiTokensPagingSource.kt index fb04538ee2..e36aaa66d5 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/impl/data/TangemApiTokensPagingSource.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/impl/data/TangemApiTokensPagingSource.kt @@ -7,24 +7,24 @@ import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.domain.common.extensions.supportedBlockchains import com.tangem.domain.common.extensions.toNetworkId import com.tangem.domain.common.util.cardTypesResolver +import com.tangem.domain.wallets.usecase.GetSelectedWalletUseCase import com.tangem.tap.features.tokens.impl.data.converters.CoinsResponseConverter import com.tangem.tap.features.tokens.impl.domain.models.Token -import com.tangem.tap.proxy.AppStateHolder import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.runCatching /** * Paging source that get tokens by Tangem Tech API * - * @property api Tangem Tech API - * @property dispatchers coroutine dispatchers provider - * @property reduxStateHolder redux state holder - * @property searchText search text + * @property api Tangem Tech API + * @property dispatchers coroutine dispatchers provider + * @property getSelectedWalletUseCase use case that returns selected wallet + * @property searchText search text */ internal class TangemApiTokensPagingSource( private val api: TangemTechApi, private val dispatchers: CoroutineDispatcherProvider, - private val reduxStateHolder: AppStateHolder, + private val getSelectedWalletUseCase: GetSelectedWalletUseCase, private val searchText: String?, ) : PagingSource() { @@ -39,9 +39,10 @@ internal class TangemApiTokensPagingSource( val page = params.key ?: 0 return runCatching(dispatchers.io) { - val scanResponse = reduxStateHolder.scanResponse - val supportedBlockchains = scanResponse?.card?.supportedBlockchains(scanResponse.cardTypesResolver) - ?: Blockchain.values().toList() + val supportedBlockchains = getSelectedWalletUseCase().fold( + ifLeft = { Blockchain.values().toList() }, + ifRight = { it.scanResponse.card.supportedBlockchains(it.scanResponse.cardTypesResolver) }, + ) api.getCoins( networkIds = supportedBlockchains.joinToString(separator = ",", transform = Blockchain::toNetworkId), diff --git a/app/src/main/java/com/tangem/tap/features/tokens/impl/di/TokensListInteractorModule.kt b/app/src/main/java/com/tangem/tap/features/tokens/impl/di/TokensListInteractorModule.kt index 85fd22ab71..f588d3fc76 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/impl/di/TokensListInteractorModule.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/impl/di/TokensListInteractorModule.kt @@ -2,10 +2,10 @@ package com.tangem.tap.features.tokens.impl.di import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.local.testnet.TestnetTokensStorage +import com.tangem.domain.wallets.usecase.GetSelectedWalletUseCase import com.tangem.tap.features.tokens.impl.data.DefaultTokensListRepository import com.tangem.tap.features.tokens.impl.domain.DefaultTokensListInteractor import com.tangem.tap.features.tokens.impl.domain.TokensListInteractor -import com.tangem.tap.proxy.AppStateHolder import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.Module import dagger.Provides @@ -25,14 +25,14 @@ internal object TokensListInteractorModule { fun provideTokensListInteractor( tangemTechApi: TangemTechApi, dispatchers: CoroutineDispatcherProvider, - reduxStateHolder: AppStateHolder, + getSelectedWalletUseCase: GetSelectedWalletUseCase, testnetTokensStorage: TestnetTokensStorage, ): TokensListInteractor { return DefaultTokensListInteractor( repository = DefaultTokensListRepository( tangemTechApi = tangemTechApi, dispatchers = dispatchers, - reduxStateHolder = reduxStateHolder, + getSelectedWalletUseCase = getSelectedWalletUseCase, testnetTokensStorage = testnetTokensStorage, ), ) diff --git a/app/src/main/java/com/tangem/tap/features/tokens/impl/di/TokensListRepositoryModule.kt b/app/src/main/java/com/tangem/tap/features/tokens/impl/di/TokensListRepositoryModule.kt index dbba8fe0dd..88443e8601 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/impl/di/TokensListRepositoryModule.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/impl/di/TokensListRepositoryModule.kt @@ -2,9 +2,9 @@ package com.tangem.tap.features.tokens.impl.di import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.local.testnet.TestnetTokensStorage +import com.tangem.domain.wallets.usecase.GetSelectedWalletUseCase import com.tangem.tap.features.tokens.impl.data.DefaultTokensListRepository import com.tangem.tap.features.tokens.impl.domain.TokensListRepository -import com.tangem.tap.proxy.AppStateHolder import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.Module import dagger.Provides @@ -24,13 +24,13 @@ internal object TokensListRepositoryModule { fun providesTokensListRepository( tangemTechApi: TangemTechApi, dispatchers: CoroutineDispatcherProvider, - reduxStateHolder: AppStateHolder, + getSelectedWalletUseCase: GetSelectedWalletUseCase, testnetTokensStorage: TestnetTokensStorage, ): TokensListRepository { return DefaultTokensListRepository( tangemTechApi = tangemTechApi, dispatchers = dispatchers, - reduxStateHolder = reduxStateHolder, + getSelectedWalletUseCase = getSelectedWalletUseCase, testnetTokensStorage = testnetTokensStorage, ) } diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/walletmanager/DefaultWalletManagersStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/walletmanager/DefaultWalletManagersStore.kt index 36bc7723c2..1222edafb4 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/walletmanager/DefaultWalletManagersStore.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/walletmanager/DefaultWalletManagersStore.kt @@ -6,6 +6,7 @@ import com.tangem.datasource.local.datastore.core.StringKeyDataStore import com.tangem.datasource.local.datastore.core.StringKeyDataStoreDecorator import com.tangem.domain.wallets.models.UserWalletId import com.tangem.utils.extensions.addOrReplace +import kotlinx.coroutines.flow.Flow internal class DefaultWalletManagersStore( dataStore: StringKeyDataStore>, @@ -15,6 +16,10 @@ internal class DefaultWalletManagersStore( return key.stringValue } + override fun getAll(userWalletId: UserWalletId): Flow> { + return get(key = userWalletId) + } + override suspend fun getSyncOrNull( userWalletId: UserWalletId, blockchain: Blockchain, diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/walletmanager/WalletManagersStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/walletmanager/WalletManagersStore.kt index c025cf6911..8506eb5803 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/walletmanager/WalletManagersStore.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/walletmanager/WalletManagersStore.kt @@ -3,9 +3,12 @@ package com.tangem.datasource.local.walletmanager import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.WalletManager import com.tangem.domain.wallets.models.UserWalletId +import kotlinx.coroutines.flow.Flow interface WalletManagersStore { + fun getAll(userWalletId: UserWalletId): Flow> + suspend fun getSyncOrNull( userWalletId: UserWalletId, blockchain: Blockchain, diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/ResizableText.kt b/core/ui/src/main/java/com/tangem/core/ui/components/ResizableText.kt index d4d1abb476..8801cb7983 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/ResizableText.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/ResizableText.kt @@ -20,6 +20,8 @@ fun ResizableText( fontSizeRange: FontSizeRange, modifier: Modifier = Modifier, color: Color = Color.Unspecified, + overflow: TextOverflow = TextOverflow.Clip, + maxLines: Int = Int.MAX_VALUE, style: TextStyle = LocalTextStyle.current, ) { val fontSizeValue = remember { mutableStateOf(fontSizeRange.max.value) } @@ -33,12 +35,13 @@ fun ResizableText( } Text( - modifier = modifier.drawWithContent { if (readyToDraw.value) drawContent() }, text = text, + modifier = modifier.drawWithContent { if (readyToDraw.value) drawContent() }, color = color, - softWrap = false, - style = style, fontSize = fontSizeValue.value.sp, + overflow = overflow, + softWrap = false, + maxLines = maxLines, onTextLayout = { if (it.hasVisualOverflow) { val nextFontSizeValue = fontSizeValue.value - fontSizeRange.step.value @@ -52,6 +55,7 @@ fun ResizableText( readyToDraw.value = true } }, + style = style, ) } diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt index fa5f16dbf1..aa3d99f8a1 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt @@ -107,6 +107,7 @@ internal class DefaultCurrenciesRepository( derivationStyleProvider = getUserWallet(userWalletId).scanResponse.derivationStyleProvider, ) } + .distinct() } override suspend fun removeCurrency(userWalletId: UserWalletId, currency: CryptoCurrency) = @@ -186,6 +187,7 @@ internal class DefaultCurrenciesRepository( override suspend fun getMultiCurrencyWalletCurrency( userWalletId: UserWalletId, id: CryptoCurrency.ID, + derivationPath: Network.DerivationPath, ): CryptoCurrency = withContext(dispatchers.io) { val userWallet = getUserWallet(userWalletId) ensureIsCorrectUserWallet(userWallet, isMultiCurrencyWalletExpected = true) @@ -194,10 +196,14 @@ internal class DefaultCurrenciesRepository( "Unable to find tokens response for user wallet with provided ID: $userWalletId" } - responseCurrenciesFactory.createCurrency(id, response, userWallet.scanResponse) + responseCurrenciesFactory.createCurrency(id, response, userWallet.scanResponse, derivationPath.value) } - override suspend fun getNetworkCoin(userWalletId: UserWalletId, networkId: Network.ID): CryptoCurrency.Coin { + override suspend fun getNetworkCoin( + userWalletId: UserWalletId, + networkId: Network.ID, + derivationPath: Network.DerivationPath, + ): CryptoCurrency.Coin { val userWallet = getUserWallet(userWalletId) ensureIsCorrectUserWallet(userWallet = userWallet, isMultiCurrencyWalletExpected = true) @@ -207,15 +213,12 @@ internal class DefaultCurrenciesRepository( "Unable to find tokens response for user wallet with provided ID: $userWalletId" } val blockchain = Blockchain.fromId(networkId.value) - val derivationPath = blockchain - .derivationPath(userWallet.scanResponse.derivationStyleProvider.getDerivationStyle()) - ?.rawPath + val blockchainNetworkId = blockchain.toNetworkId() + val coinId = blockchain.toCoinId() val storedCoin = storedTokens.tokens .find { - it.networkId == blockchain.toNetworkId() && - it.id == blockchain.toCoinId() && - it.derivationPath == derivationPath + it.networkId == blockchainNetworkId && it.id == coinId && it.derivationPath == derivationPath.value } ?: error("Coin in this network $networkId not found") val coin = responseCurrenciesFactory.createCurrency(storedCoin, userWallet.scanResponse) @@ -277,7 +280,7 @@ internal class DefaultCurrenciesRepository( ) userTokensStore.store(userWallet.walletId, response) - fetchUserMarketCoinsByIds(userWalletId, response) + fetchExchangeableUserMarketCoinsByIds(userWalletId, response) } private suspend fun storeAndPushTokens(userWalletId: UserWalletId, response: UserTokensResponse) { @@ -285,10 +288,15 @@ internal class DefaultCurrenciesRepository( tangemTechApi.saveUserTokens(userWalletId.stringValue, response) } - private suspend fun fetchUserMarketCoinsByIds(userWalletId: UserWalletId, userTokens: UserTokensResponse) { + private suspend fun fetchExchangeableUserMarketCoinsByIds( + userWalletId: UserWalletId, + userTokens: UserTokensResponse, + ) { try { - val networkIds = userTokens.tokens.joinToString(separator = ",") { it.networkId } - val response = tangemTechApi.getCoins(networkIds = networkIds) + val networkIds = userTokens.tokens + .distinctBy { it.networkId } + .joinToString(separator = ",") { it.networkId } + val response = tangemTechApi.getCoins(networkIds = networkIds, exchangeable = true) userMarketCoinsStore.store(userWalletId, response) } catch (e: Throwable) { diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/ResponseCryptoCurrenciesFactory.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/ResponseCryptoCurrenciesFactory.kt index 27afcb1c84..3274fb2221 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/ResponseCryptoCurrenciesFactory.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/ResponseCryptoCurrenciesFactory.kt @@ -20,13 +20,10 @@ internal class ResponseCryptoCurrenciesFactory(private val demoConfig: DemoConfi currencyId: CryptoCurrency.ID, response: UserTokensResponse, scanResponse: ScanResponse, + derivationPath: String?, ): CryptoCurrency { val responseTokenId = currencyId.rawCurrencyId - val blockchain = Blockchain.fromId(currencyId.rawNetworkId) - val networkId = blockchain.toNetworkId() - val derivationPath = blockchain - .derivationPath(scanResponse.derivationStyleProvider.getDerivationStyle()) - ?.rawPath + val networkId = Blockchain.fromId(currencyId.rawNetworkId).toNetworkId() val token = requireNotNull( value = response.tokens diff --git a/domain/legacy/src/main/java/com/tangem/domain/common/TangemCardTypesResolver.kt b/domain/legacy/src/main/java/com/tangem/domain/common/TangemCardTypesResolver.kt index 6437bc105d..65550e62f4 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/common/TangemCardTypesResolver.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/common/TangemCardTypesResolver.kt @@ -26,7 +26,7 @@ internal class TangemCardTypesResolver( } override fun isWhiteWallet(): Boolean { - return walletData == null && card.firmwareVersion >= FirmwareVersion.HDWalletAvailable + return walletData == null && card.firmwareVersion <= FirmwareVersion.HDWalletAvailable } override fun isWallet2(): Boolean { diff --git a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/DefaultWalletManagersFacade.kt b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/DefaultWalletManagersFacade.kt index 7d0e1e2593..9851ae5bc8 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/DefaultWalletManagersFacade.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/DefaultWalletManagersFacade.kt @@ -7,8 +7,8 @@ import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.BlockchainSdkError import com.tangem.blockchain.common.WalletManager import com.tangem.blockchain.common.address.Address -import com.tangem.blockchain.common.txhistory.TransactionHistoryRequest import com.tangem.blockchain.common.address.AddressType +import com.tangem.blockchain.common.txhistory.TransactionHistoryRequest import com.tangem.blockchain.extensions.Result import com.tangem.crypto.hdWallet.DerivationPath import com.tangem.datasource.config.ConfigManager @@ -26,6 +26,7 @@ import com.tangem.domain.walletmanager.model.UpdateWalletManagerResult import com.tangem.domain.walletmanager.utils.* import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWalletId +import kotlinx.coroutines.flow.Flow import timber.log.Timber import java.math.BigDecimal @@ -275,6 +276,10 @@ class DefaultWalletManagersFacade( return if (manager is ExistentialDepositProvider) manager.getExistentialDeposit() else null } + override fun getAll(userWalletId: UserWalletId): Flow> { + return walletManagersStore.getAll(userWalletId) + } + private fun updateWalletManagerTokensIfNeeded(walletManager: WalletManager, tokens: Set) { if (tokens.isEmpty()) return diff --git a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/WalletManagersFacade.kt b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/WalletManagersFacade.kt index 5be94faf27..94ce68d1d3 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/WalletManagersFacade.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/WalletManagersFacade.kt @@ -4,15 +4,16 @@ import com.tangem.blockchain.blockchains.solana.RentProvider import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.WalletManager import com.tangem.blockchain.common.address.Address +import com.tangem.blockchain.common.address.AddressType import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.Network import com.tangem.domain.tokens.model.warnings.CryptoCurrencyWarning -import com.tangem.blockchain.common.address.AddressType import com.tangem.domain.txhistory.models.PaginationWrapper import com.tangem.domain.txhistory.models.TxHistoryItem import com.tangem.domain.txhistory.models.TxHistoryState import com.tangem.domain.walletmanager.model.UpdateWalletManagerResult import com.tangem.domain.wallets.models.UserWalletId +import kotlinx.coroutines.flow.Flow import java.math.BigDecimal // TODO: Move to its own module @@ -97,4 +98,6 @@ interface WalletManagersFacade { * deactivated and any remaining funds will be destroyed. */ suspend fun getExistentialDeposit(userWalletId: UserWalletId, network: Network): BigDecimal? + + fun getAll(userWalletId: UserWalletId): Flow> } \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/FetchCurrencyStatusUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/FetchCurrencyStatusUseCase.kt index eeeea7c9ec..7b5c4de6fb 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/FetchCurrencyStatusUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/FetchCurrencyStatusUseCase.kt @@ -36,16 +36,18 @@ class FetchCurrencyStatusUseCase( * * @param userWalletId The ID of the user's wallet. * @param id The ID of the cryptocurrency. + * @param derivationPath currency derivation path. * @param refresh Indicates whether to force a refresh of the status data. * @return An [Either] representing success (Right) or an error (Left) in fetching the status. */ suspend operator fun invoke( userWalletId: UserWalletId, id: CryptoCurrency.ID, + derivationPath: Network.DerivationPath, refresh: Boolean = false, ): Either { return either { - val currency = getCurrency(userWalletId, id) + val currency = getCurrency(userWalletId, id, derivationPath) fetchCurrencyStatus(userWalletId, currency, refresh) } @@ -87,8 +89,9 @@ class FetchCurrencyStatusUseCase( private suspend fun Raise.getCurrency( userWalletId: UserWalletId, id: CryptoCurrency.ID, + derivationPath: Network.DerivationPath, ): CryptoCurrency { - return catch({ currenciesRepository.getMultiCurrencyWalletCurrency(userWalletId, id) }) { + return catch({ currenciesRepository.getMultiCurrencyWalletCurrency(userWalletId, id, derivationPath) }) { raise(CurrencyStatusError.DataError(it)) } } diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyUseCase.kt index 194ffc0416..ae9b7f6326 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyUseCase.kt @@ -6,6 +6,7 @@ import arrow.core.raise.catch import arrow.core.raise.either import com.tangem.domain.tokens.error.CurrencyStatusError import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.tokens.model.Network import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.wallets.models.UserWalletId @@ -18,13 +19,15 @@ class GetCryptoCurrencyUseCase( * * @param userWalletId The ID of the user's wallet. * @param id The ID of the cryptocurrency. + * @param derivationPath currency derivation path. * @return An [Either] representing success (Right) or an error (Left) in fetching the status. */ suspend operator fun invoke( userWalletId: UserWalletId, id: CryptoCurrency.ID, + derivationPath: Network.DerivationPath, ): Either { - return either { getCurrency(userWalletId, id) } + return either { getCurrency(userWalletId, id, derivationPath) } } /** @@ -40,9 +43,10 @@ class GetCryptoCurrencyUseCase( private suspend fun Raise.getCurrency( userWalletId: UserWalletId, id: CryptoCurrency.ID, + derivationPath: Network.DerivationPath, ): CryptoCurrency { return catch( - block = { currenciesRepository.getMultiCurrencyWalletCurrency(userWalletId, id) }, + block = { currenciesRepository.getMultiCurrencyWalletCurrency(userWalletId, id, derivationPath) }, catch = { raise(CurrencyStatusError.DataError(it)) }, ) } diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCurrencyStatusUpdatesUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCurrencyStatusUpdatesUseCase.kt index d018b74e85..f575854cc6 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCurrencyStatusUpdatesUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCurrencyStatusUpdatesUseCase.kt @@ -5,6 +5,7 @@ import com.tangem.domain.tokens.error.CurrencyStatusError import com.tangem.domain.tokens.error.mapper.mapToCurrencyError import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.tokens.model.Network import com.tangem.domain.tokens.operations.CurrenciesStatusesOperations import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.tokens.repository.NetworksRepository @@ -32,20 +33,23 @@ class GetCurrencyStatusUpdatesUseCase( * * @param userWalletId The unique identifier of the user's wallet. * @param currencyId The unique identifier of the cryptocurrency. + * @param derivationPath currency derivation path. * @return A [Flow] emitting either a [CurrencyStatusError] or a [CryptoCurrencyStatus], indicating the result of the fetch operation. */ operator fun invoke( userWalletId: UserWalletId, currencyId: CryptoCurrency.ID, + derivationPath: Network.DerivationPath, ): Flow> { return flow { - emitAll(getCurrency(userWalletId, currencyId)) + emitAll(getCurrency(userWalletId, currencyId, derivationPath)) }.flowOn(dispatchers.io) } private suspend fun getCurrency( userWalletId: UserWalletId, currencyId: CryptoCurrency.ID, + derivationPath: Network.DerivationPath, ): Flow> { val operations = CurrenciesStatusesOperations( currenciesRepository = currenciesRepository, @@ -54,7 +58,7 @@ class GetCurrencyStatusUpdatesUseCase( userWalletId = userWalletId, ) - return operations.getCurrencyStatusFlow(currencyId).map { maybeCurrency -> + return operations.getCurrencyStatusFlow(currencyId, derivationPath).map { maybeCurrency -> maybeCurrency.mapLeft(CurrenciesStatusesOperations.Error::mapToCurrencyError) } } diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCurrencyWarningsUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCurrencyWarningsUseCase.kt index d11e7b8f93..994eda5490 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCurrencyWarningsUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCurrencyWarningsUseCase.kt @@ -24,12 +24,14 @@ class GetCurrencyWarningsUseCase( suspend operator fun invoke( userWalletId: UserWalletId, currency: CryptoCurrency, + derivationPath: Network.DerivationPath, ): Flow> { return combine( getFeeWarningFlow( userWalletId = userWalletId, networkId = currency.network.id, currencyId = currency.id, + derivationPath = derivationPath, ), flowOf(walletManagersFacade.getRentInfo(userWalletId, currency.network)), flowOf(walletManagersFacade.getExistentialDeposit(userWalletId, currency.network)), @@ -51,6 +53,7 @@ class GetCurrencyWarningsUseCase( userWalletId: UserWalletId, networkId: Network.ID, currencyId: CryptoCurrency.ID, + derivationPath: Network.DerivationPath, ): Flow { val operations = CurrenciesStatusesOperations( currenciesRepository = currenciesRepository, @@ -60,8 +63,8 @@ class GetCurrencyWarningsUseCase( ) return combine( - operations.getCurrencyStatusFlow(currencyId).map { it.getOrNull() }, - operations.getNetworkCoinFlow(networkId).map { it.getOrNull() }, + operations.getCurrencyStatusFlow(currencyId, derivationPath).map { it.getOrNull() }, + operations.getNetworkCoinFlow(networkId, derivationPath).map { it.getOrNull() }, ) { tokenStatus, coinStatus -> when { tokenStatus != null && coinStatus != null -> { diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetNetworkCoinStatusUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetNetworkCoinStatusUseCase.kt index 999d6e15af..44ca1e09cc 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetNetworkCoinStatusUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetNetworkCoinStatusUseCase.kt @@ -23,12 +23,14 @@ class GetNetworkCoinStatusUseCase( operator fun invoke( userWalletId: UserWalletId, networkId: Network.ID, + derivationPath: Network.DerivationPath, ): Flow> { return flow { emitAll( flow = getCurrency( userWalletId = userWalletId, networkId = networkId, + derivationPath = derivationPath, ), ) } @@ -38,6 +40,7 @@ class GetNetworkCoinStatusUseCase( private suspend fun getCurrency( userWalletId: UserWalletId, networkId: Network.ID, + derivationPath: Network.DerivationPath, ): Flow> { val operations = CurrenciesStatusesOperations( currenciesRepository = currenciesRepository, @@ -46,7 +49,7 @@ class GetNetworkCoinStatusUseCase( userWalletId = userWalletId, ) - return operations.getNetworkCoinFlow(networkId).map { maybeCurrency -> + return operations.getNetworkCoinFlow(networkId, derivationPath).map { maybeCurrency -> maybeCurrency.mapLeft(CurrenciesStatusesOperations.Error::mapToCurrencyError) } } diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesOperations.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesOperations.kt index fc4ec95f89..4271ce732e 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesOperations.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesOperations.kt @@ -67,18 +67,24 @@ internal class CurrenciesStatusesOperations( } } - suspend fun getCurrencyStatusFlow(currencyId: CryptoCurrency.ID): Flow> { + suspend fun getCurrencyStatusFlow( + currencyId: CryptoCurrency.ID, + derivationPath: Network.DerivationPath, + ): Flow> { val currency = recover( - block = { getMultiCurrencyWalletCurrency(currencyId) }, + block = { getMultiCurrencyWalletCurrency(currencyId, derivationPath) }, recover = { return flowOf(it.left()) }, ) return getCurrencyStatusFlow(currency) } - suspend fun getNetworkCoinFlow(networkId: Network.ID): Flow> { + suspend fun getNetworkCoinFlow( + networkId: Network.ID, + derivationPath: Network.DerivationPath, + ): Flow> { val currency = recover( - block = { getNetworkCoin(networkId) }, + block = { getNetworkCoin(networkId, derivationPath) }, recover = { return flowOf(it.left()) }, ) @@ -178,14 +184,26 @@ internal class CurrenciesStatusesOperations( .onEmpty { emit(Error.EmptyCurrencies.left()) } } - private suspend fun Raise.getMultiCurrencyWalletCurrency(currencyId: CryptoCurrency.ID): CryptoCurrency { - return Either.catch { currenciesRepository.getMultiCurrencyWalletCurrency(userWalletId, currencyId) } + private suspend fun Raise.getMultiCurrencyWalletCurrency( + currencyId: CryptoCurrency.ID, + derivationPath: Network.DerivationPath, + ): CryptoCurrency { + return Either.catch { + currenciesRepository.getMultiCurrencyWalletCurrency( + userWalletId, + currencyId, + derivationPath, + ) + } .mapLeft { Error.DataError(it) } .bind() } - private suspend fun Raise.getNetworkCoin(networkId: Network.ID): CryptoCurrency { - return Either.catch { currenciesRepository.getNetworkCoin(userWalletId, networkId) } + private suspend fun Raise.getNetworkCoin( + networkId: Network.ID, + derivationPath: Network.DerivationPath, + ): CryptoCurrency { + return Either.catch { currenciesRepository.getNetworkCoin(userWalletId, networkId, derivationPath) } .mapLeft { Error.DataError(it) } .bind() } diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/CurrenciesRepository.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/CurrenciesRepository.kt index 5554ee3990..2973c698f6 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/CurrenciesRepository.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/CurrenciesRepository.kt @@ -98,19 +98,29 @@ interface CurrenciesRepository { * * @param userWalletId The unique identifier of the user wallet. * @param id The unique identifier of the cryptocurrency to be retrieved. + * @param derivationPath currency derivation path. * @return The cryptocurrency associated with the user wallet and ID. * @throws com.tangem.domain.core.error.DataError.UserWalletError.WrongUserWallet If single-currency user wallet * ID provided. */ - suspend fun getMultiCurrencyWalletCurrency(userWalletId: UserWalletId, id: CryptoCurrency.ID): CryptoCurrency + suspend fun getMultiCurrencyWalletCurrency( + userWalletId: UserWalletId, + id: CryptoCurrency.ID, + derivationPath: Network.DerivationPath, + ): CryptoCurrency /** * Get the coin for a specific network. * * @param userWalletId The unique identifier of the user wallet. * @param networkId The unique identifier of the network. + * @param derivationPath currency derivation path. */ - suspend fun getNetworkCoin(userWalletId: UserWalletId, networkId: Network.ID): CryptoCurrency.Coin + suspend fun getNetworkCoin( + userWalletId: UserWalletId, + networkId: Network.ID, + derivationPath: Network.DerivationPath, + ): CryptoCurrency.Coin /** * Determines whether the tokens within a specific multi-currency user wallet are grouped. diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockCurrenciesRepository.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockCurrenciesRepository.kt index 8cdec88feb..c50ee469db 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockCurrenciesRepository.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockCurrenciesRepository.kt @@ -67,6 +67,7 @@ internal class MockCurrenciesRepository( override suspend fun getMultiCurrencyWalletCurrency( userWalletId: UserWalletId, id: CryptoCurrency.ID, + derivationPath: Network.DerivationPath, ): CryptoCurrency { val token = token.getOrElse { e -> throw e } @@ -75,7 +76,11 @@ internal class MockCurrenciesRepository( return token } - override suspend fun getNetworkCoin(userWalletId: UserWalletId, networkId: Network.ID): CryptoCurrency.Coin { + override suspend fun getNetworkCoin( + userWalletId: UserWalletId, + networkId: Network.ID, + derivationPath: Network.DerivationPath, + ): CryptoCurrency.Coin { TODO("Not yet implemented") } diff --git a/domain/txhistory/src/main/kotlin/com/tangem/domain/txhistory/usecase/GetTxHistoryItemsCountUseCase.kt b/domain/txhistory/src/main/kotlin/com/tangem/domain/txhistory/usecase/GetTxHistoryItemsCountUseCase.kt index de458c7a51..6ca59cb013 100644 --- a/domain/txhistory/src/main/kotlin/com/tangem/domain/txhistory/usecase/GetTxHistoryItemsCountUseCase.kt +++ b/domain/txhistory/src/main/kotlin/com/tangem/domain/txhistory/usecase/GetTxHistoryItemsCountUseCase.kt @@ -9,6 +9,7 @@ import com.tangem.domain.txhistory.repository.TxHistoryRepository class GetTxHistoryItemsCountUseCase(private val repository: TxHistoryRepository) { + // FIXME: Provide UserWalletId suspend operator fun invoke(network: Network): Either { return either { catch( diff --git a/domain/txhistory/src/main/kotlin/com/tangem/domain/txhistory/usecase/GetTxHistoryItemsUseCase.kt b/domain/txhistory/src/main/kotlin/com/tangem/domain/txhistory/usecase/GetTxHistoryItemsUseCase.kt index 39865a24f5..0555f17187 100644 --- a/domain/txhistory/src/main/kotlin/com/tangem/domain/txhistory/usecase/GetTxHistoryItemsUseCase.kt +++ b/domain/txhistory/src/main/kotlin/com/tangem/domain/txhistory/usecase/GetTxHistoryItemsUseCase.kt @@ -14,6 +14,7 @@ private const val DEFAULT_PAGE_SIZE = 50 class GetTxHistoryItemsUseCase(private val repository: TxHistoryRepository) { + // FIXME: Provide UserWalletId operator fun invoke( currency: CryptoCurrency, pageSize: Int = DEFAULT_PAGE_SIZE, diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSkeletonStateConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSkeletonStateConverter.kt index 6fa5307dde..75ce9259fb 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSkeletonStateConverter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSkeletonStateConverter.kt @@ -53,7 +53,7 @@ internal class TokenDetailsSkeletonStateConverter( pullToRefreshConfig = createPullToRefresh(), bottomSheetConfig = null, isBalanceHidden = true, - isCustomToken = value.isCustom, + isCustomToken = value is CryptoCurrency.Token && value.isCustom, ) } diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt index 053b87757c..e4904101e8 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt @@ -8,6 +8,7 @@ import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.components.bottomsheets.chooseaddress.ChooseAddressBottomSheetConfig import com.tangem.core.ui.components.bottomsheets.tokenreceive.AddressModel import com.tangem.core.ui.components.bottomsheets.tokenreceive.TokenReceiveBottomSheetConfig +import com.tangem.core.ui.components.transactions.state.TxHistoryState import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.tokens.error.CurrencyStatusError import com.tangem.domain.tokens.model.CryptoCurrency @@ -23,6 +24,7 @@ import com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.t import com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.txhistory.TokenDetailsLoadingTxHistoryConverter import com.tangem.feature.tokendetails.presentation.tokendetails.viewmodels.TokenDetailsClickIntents import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow internal class TokenDetailsStateFactory( private val currentStateProvider: Provider, @@ -89,6 +91,16 @@ internal class TokenDetailsStateFactory( return tokenDetailsButtonsConverter.convert(actions) } + fun getLoadingTxHistoryState(): TokenDetailsState { + return currentStateProvider().copy( + txHistoryState = TxHistoryState.Content( + contentItems = MutableStateFlow( + value = TxHistoryState.getDefaultLoadingTransactions(clickIntents::onExploreClick), + ), + ), + ) + } + fun getLoadingTxHistoryState(itemsCountEither: Either): TokenDetailsState { return loadingTransactionsStateConverter.convert(value = itemsCountEither) } diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/txhistory/TokenDetailsLoadingTxHistoryConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/txhistory/TokenDetailsLoadingTxHistoryConverter.kt index 599d55f418..591a42c218 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/txhistory/TokenDetailsLoadingTxHistoryConverter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/txhistory/TokenDetailsLoadingTxHistoryConverter.kt @@ -9,6 +9,7 @@ import com.tangem.domain.txhistory.models.TxHistoryStateError import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState import com.tangem.feature.tokendetails.presentation.tokendetails.viewmodels.TokenDetailsClickIntents import com.tangem.utils.converter.Converter +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.update internal class TokenDetailsLoadingTxHistoryConverter( @@ -34,22 +35,28 @@ internal class TokenDetailsLoadingTxHistoryConverter( private fun convert(value: Int): TokenDetailsState { val state = currentStateProvider() - val txHistoryContent = state.txHistoryState as TxHistoryState.Content - txHistoryContent.contentItems.update { - PagingData.from( - data = listOf(TxHistoryState.TxHistoryItemState.Title(onExploreClick = clickIntents::onExploreClick)) + - MutableList( - size = value, - init = { - TxHistoryState.TxHistoryItemState.Transaction( - state = TransactionState.Loading(it.toString()), - ) - }, - ), + return if (state.txHistoryState is TxHistoryState.Content) { + state.txHistoryState.contentItems.update { + PagingData.from(data = createLoadingItems(value)) + } + state + } else { + val txHistoryContent = TxHistoryState.Content( + contentItems = MutableStateFlow( + value = PagingData.from(data = createLoadingItems(value)), + ), ) + state.copy(txHistoryState = txHistoryContent) } + } - return state + private fun createLoadingItems(size: Int): List { + return buildList { + add(TxHistoryState.TxHistoryItemState.Title(onExploreClick = clickIntents::onExploreClick)) + (1..size).forEach { + add(TxHistoryState.TxHistoryItemState.Transaction(state = TransactionState.Loading(it.toString()))) + } + } } } \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/txhistory/TokenDetailsTxHistoryItemFlowConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/txhistory/TokenDetailsTxHistoryItemFlowConverter.kt index b4e618aeb3..0f700b9373 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/txhistory/TokenDetailsTxHistoryItemFlowConverter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/txhistory/TokenDetailsTxHistoryItemFlowConverter.kt @@ -15,10 +15,7 @@ import com.tangem.utils.extensions.isToday import com.tangem.utils.extensions.isYesterday import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.flow.update +import kotlinx.coroutines.flow.* import org.joda.time.DateTime import org.joda.time.DateTimeZone @@ -37,8 +34,12 @@ internal class TokenDetailsTxHistoryItemFlowConverter( } override fun convert(value: Flow>): TxHistoryState { - val txHistoryContent = currentStateProvider().txHistoryState as TxHistoryState.Content - + val state = currentStateProvider() + val txHistoryContent = if (state.txHistoryState is TxHistoryState.Content) { + state.txHistoryState + } else { + TxHistoryState.Content(contentItems = MutableStateFlow(PagingData.empty())) + } // FIXME: TxHistoryRepository should send loading transactions // [REDACTED_JIRA] value diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt index c2c671b82d..bbf6300015 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt @@ -9,6 +9,7 @@ import arrow.core.getOrElse import com.tangem.blockchain.common.address.AddressType import com.tangem.common.Provider import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.core.ui.components.transactions.state.TxHistoryState import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.balancehiding.IsBalanceHiddenUseCase @@ -35,6 +36,8 @@ import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.JobHolder import com.tangem.utils.coroutines.saveIn import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch import timber.log.Timber @@ -103,7 +106,7 @@ internal class TokenDetailsViewModel @Inject constructor( private fun updateContent(selectedWallet: UserWallet) { updateMarketPrice(selectedWallet = selectedWallet) - updateTxHistory() + updateTxHistory(refresh = false, showItemsLoading = true) updateWarnings(selectedWallet = selectedWallet) } @@ -137,6 +140,7 @@ internal class TokenDetailsViewModel @Inject constructor( getCurrencyWarningsUseCase.invoke( userWalletId = selectedWallet.walletId, currency = cryptoCurrency, + derivationPath = cryptoCurrency.network.derivationPath, ) .distinctUntilChanged() .onEach { uiState = stateFactory.getStateWithNotifications(it) } @@ -148,6 +152,7 @@ internal class TokenDetailsViewModel @Inject constructor( getCurrencyStatusUpdatesUseCase( userWalletId = selectedWallet.walletId, currencyId = cryptoCurrency.id, + derivationPath = cryptoCurrency.network.derivationPath, ) .distinctUntilChanged() .onEach { either -> @@ -162,13 +167,19 @@ internal class TokenDetailsViewModel @Inject constructor( .saveIn(marketPriceJobHolder) } - private fun updateTxHistory(refresh: Boolean = false) { + /** + * @param refresh - invalidate cache and get data from remote + * @param showItemsLoading - show loading items placeholder. + */ + @Suppress("UnusedPrivateMember") // will be removed after implement caching + private fun updateTxHistory(refresh: Boolean, showItemsLoading: Boolean) { viewModelScope.launch(dispatchers.io) { val txHistoryItemsCountEither = txHistoryItemsCountUseCase( network = cryptoCurrency.network, ) - if (!refresh) { + // if countEither is left, handling error state run inside getLoadingTxHistoryState + if (showItemsLoading || txHistoryItemsCountEither.isLeft()) { uiState = stateFactory.getLoadingTxHistoryState(itemsCountEither = txHistoryItemsCountEither) } @@ -211,7 +222,8 @@ internal class TokenDetailsViewModel @Inject constructor( override fun onReloadClick() { analyticsEventsHandler.send(TokenScreenEvent.ButtonReload(cryptoCurrency.symbol)) - updateTxHistory() + uiState = stateFactory.getLoadingTxHistoryState() + updateTxHistory(refresh = true, showItemsLoading = true) } override fun onSendClick() { @@ -237,6 +249,7 @@ internal class TokenDetailsViewModel @Inject constructor( getNetworkCoinStatusUseCase( userWalletId = wallet.walletId, networkId = status.currency.network.id, + derivationPath = status.currency.network.derivationPath, ) .take(count = 1) .collectLatest { @@ -358,13 +371,23 @@ internal class TokenDetailsViewModel @Inject constructor( uiState = stateFactory.getRefreshingState() viewModelScope.launch(dispatchers.io) { - fetchCurrencyStatusUseCase.invoke( - userWalletId = wallet.walletId, - id = cryptoCurrency.id, - refresh = true, - ) - updateTxHistory(refresh = true) - updateWarnings(wallet) + listOf( + async { + fetchCurrencyStatusUseCase.invoke( + userWalletId = wallet.walletId, + id = cryptoCurrency.id, + derivationPath = cryptoCurrency.network.derivationPath, + refresh = true, + ) + }, + async { + updateTxHistory( + refresh = true, + showItemsLoading = uiState.txHistoryState !is TxHistoryState.Content, + ) + }, + async { updateWarnings(wallet) }, + ).awaitAll() uiState = stateFactory.getRefreshedState() }.saveIn(refreshStateJobHolder) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/WalletPreviewData.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/WalletPreviewData.kt index a0cfeb80f9..9456466cb9 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/WalletPreviewData.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/WalletPreviewData.kt @@ -1,7 +1,6 @@ package com.tangem.feature.wallet.presentation.common import androidx.paging.PagingData -import com.tangem.core.ui.R import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.components.marketprice.MarketPriceBlockState import com.tangem.core.ui.components.marketprice.PriceChangeState @@ -13,6 +12,7 @@ import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.res.TangemColorPalette import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.feature.wallet.impl.R import com.tangem.feature.wallet.presentation.common.state.TokenItemState import com.tangem.feature.wallet.presentation.organizetokens.model.DraggableItem import com.tangem.feature.wallet.presentation.organizetokens.model.OrganizeTokensListState @@ -37,10 +37,10 @@ internal object WalletPreviewData { val walletCardContentState by lazy { WalletCardState.Content( id = UserWalletId(stringValue = "123"), - title = "Wallet 1", - balance = "8923,05 $", - additionalInfo = TextReference.Str("3 cards • Seed phrase"), - imageResId = R.drawable.ill_businessman_3d, + title = "Wallet1Wallet1Wallet1Wallet1Wallet1Wallet1Wallet1Wallet1", + balance = "8923,05312312312312312312331231231233432423423424234 $", + additionalInfo = TextReference.Str("3 cards • Seed phrase3 cards • Seed phraseцфвцфвфцвцфввцфвцф"), + imageResId = R.drawable.ill_wallet2_cards3_120_106, onRenameClick = { _, _ -> }, onDeleteClick = {}, cardCount = 1, @@ -51,7 +51,7 @@ internal object WalletPreviewData { WalletCardState.Loading( id = UserWalletId("321"), title = "Wallet 1", - imageResId = R.drawable.ill_businessman_3d, + imageResId = R.drawable.ill_wallet2_cards3_120_106, onRenameClick = { _, _ -> }, onDeleteClick = {}, ) @@ -61,7 +61,7 @@ internal object WalletPreviewData { WalletCardState.HiddenContent( id = UserWalletId("42"), title = "Wallet 1", - imageResId = R.drawable.ill_businessman_3d, + imageResId = R.drawable.ill_wallet2_cards3_120_106, onRenameClick = { _, _ -> }, onDeleteClick = {}, balance = "8923,05 $", @@ -74,7 +74,7 @@ internal object WalletPreviewData { WalletCardState.Error( id = UserWalletId("24"), title = "Wallet 1", - imageResId = R.drawable.ill_businessman_3d, + imageResId = R.drawable.ill_wallet2_cards3_120_106, onRenameClick = { _, _ -> }, onDeleteClick = {}, ) @@ -167,6 +167,7 @@ internal object WalletPreviewData { iconState = tokenIconState, titleState = TokenItemState.TitleState.Content(text = "Polygon"), cryptoAmountState = TokenItemState.CryptoAmountState.Content(text = "3 172,14 $"), + isBalanceHidden = false, ) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/TokenItem.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/TokenItem.kt index 010ee08900..930a9d838b 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/TokenItem.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/TokenItem.kt @@ -3,19 +3,16 @@ package com.tangem.feature.wallet.presentation.common.component import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background import androidx.compose.foundation.combinedClickable -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.defaultMinSize -import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding -import androidx.compose.runtime.* +import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.composed -import androidx.compose.ui.layout.onSizeChanged +import androidx.compose.ui.layout.* import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider -import androidx.constraintlayout.compose.* +import androidx.compose.ui.unit.Constraints import com.tangem.core.ui.components.marketprice.PriceChangeType import com.tangem.core.ui.extensions.rememberHapticFeedback import com.tangem.core.ui.res.TangemTheme @@ -24,152 +21,66 @@ import com.tangem.feature.wallet.presentation.common.component.token.* import com.tangem.feature.wallet.presentation.common.component.token.icon.TokenIcon import com.tangem.feature.wallet.presentation.common.state.TokenItemState import org.burnoutcrew.reorderable.ReorderableLazyListState +import kotlin.math.max + +private const val TITLE_MIN_WIDTH_COEFFICIENT = 0.22 +private const val PRICE_CHANGE_MIN_WIDTH_COEFFICIENT = 0.16 + +private enum class LayoutId { + ICON, TITLE, FIAT_AMOUNT, CRYPTO_AMOUNT, PRICE_CHANGE, NON_FIAT_CONTENT +} -@Suppress("LongMethod") @Composable internal fun TokenItem( state: TokenItemState, modifier: Modifier = Modifier, reorderableTokenListState: ReorderableLazyListState? = null, ) { - var rootWidth by remember { mutableStateOf(Int.MIN_VALUE) } - - @Suppress("DestructuringDeclarationWithTooManyEntries") - BaseContainer( + CustomContainer( + state = state, modifier = modifier - .tokenClickable(state) - .onSizeChanged { rootWidth = it.width }, + .tokenClickable(state = state) + .background(color = TangemTheme.colors.background.primary), ) { - val (iconRef, titleRef, cryptoAmountRef, fiatAmountRef, priceChangeRef, nonFiatContentRef) = createRefs() + val isBalanceHidden = (state as? TokenItemState.Content)?.isBalanceHidden + ?: (state as? TokenItemState.Draggable)?.isBalanceHidden + ?: false - val isBalanceHidden = (state as? TokenItemState.Content)?.isBalanceHidden ?: false - - TokenIcon( - state = state.iconState, - modifier = Modifier.constrainAs(iconRef) { - centerVerticallyTo(parent) - start.linkTo(parent.start) - }, - ) - - val density = LocalDensity.current - val titleRequiredMinWidth by remember(rootWidth) { - derivedStateOf { with(density) { rootWidth.toDp().times(other = 0.22f) } } - } + TokenIcon(state = state.iconState, modifier = Modifier.layoutId(layoutId = LayoutId.ICON)) TokenTitle( state = state.titleState, modifier = Modifier + .layoutId(layoutId = LayoutId.TITLE) .padding(horizontal = TangemTheme.dimens.spacing8) - .constrainAs(titleRef) { - start.linkTo(iconRef.end) - top.linkTo(parent.top) - - width = Dimension.fillToConstraints.atLeast(dp = titleRequiredMinWidth) - - when (state) { - is TokenItemState.Content -> end.linkTo(fiatAmountRef.start) - is TokenItemState.Draggable -> end.linkTo(nonFiatContentRef.start) - is TokenItemState.Unreachable, - is TokenItemState.NoAddress, - -> { - end.linkTo(nonFiatContentRef.start) - bottom.linkTo(parent.bottom) - } - else -> Unit - } - }, + .padding(bottom = TangemTheme.dimens.spacing2), ) TokenFiatAmount( state = state.fiatAmountState, isBalanceHidden = isBalanceHidden, - modifier = Modifier.constrainAs(fiatAmountRef) { - top.linkTo(parent.top) - end.linkTo(parent.end) - - width = Dimension.fillToConstraints.atMostWrapContent - - if (state is TokenItemState.Content) { - start.linkTo(titleRef.end) - } - }, + modifier = Modifier + .layoutId(layoutId = LayoutId.FIAT_AMOUNT) + .padding(bottom = TangemTheme.dimens.spacing2), ) - val marginBetweenRows = TangemTheme.dimens.spacing2 TokenCryptoAmount( state = state.cryptoAmountState, isBalanceHidden = isBalanceHidden, modifier = Modifier - .padding(horizontal = TangemTheme.dimens.spacing8) - .constrainAs(cryptoAmountRef) { - start.linkTo(iconRef.end) - top.linkTo(titleRef.bottom, marginBetweenRows) - bottom.linkTo(parent.bottom) - - when (state) { - is TokenItemState.Content -> { - end.linkTo(priceChangeRef.start) - width = Dimension.fillToConstraints.atMostWrapContent - } - is TokenItemState.Draggable -> { - end.linkTo(nonFiatContentRef.start) - width = Dimension.fillToConstraints - } - else -> Unit - } - }, + .layoutId(layoutId = LayoutId.CRYPTO_AMOUNT) + .padding(horizontal = TangemTheme.dimens.spacing8), ) - val priceChangeRequiredMinWidth by remember(rootWidth) { - derivedStateOf { with(density) { rootWidth.toDp().times(other = 0.16f) } } - } TokenPriceChange( state = state.priceChangeState, - modifier = Modifier.constrainAs(priceChangeRef) { - top.linkTo(fiatAmountRef.bottom, marginBetweenRows) - end.linkTo(anchor = parent.end) - bottom.linkTo(parent.bottom) - - when (state.priceChangeState) { - is TokenItemState.PriceChangeState.Content, - is TokenItemState.PriceChangeState.Unknown, - -> { - start.linkTo(cryptoAmountRef.end) - width = Dimension.fillToConstraints - .atLeast(priceChangeRequiredMinWidth) - } - else -> Unit - } - }, + modifier = Modifier.layoutId(layoutId = LayoutId.PRICE_CHANGE), ) NonFiatContentBlock( state = state, reorderableTokenListState = reorderableTokenListState, - modifier = Modifier.constrainAs(nonFiatContentRef) { - centerVerticallyTo(parent) - end.linkTo(parent.end) - }, - ) - } -} - -@Composable -private inline fun BaseContainer( - modifier: Modifier = Modifier, - crossinline content: @Composable ConstraintLayoutScope.() -> Unit, -) { - Box( - modifier = modifier - .defaultMinSize(minHeight = TangemTheme.dimens.size68) - .background(color = TangemTheme.colors.background.primary), - ) { - ConstraintLayout( - modifier = Modifier - .fillMaxWidth() - .padding(all = TangemTheme.dimens.spacing14), - content = content, + modifier = Modifier.layoutId(layoutId = LayoutId.NON_FIAT_CONTENT), ) } } @@ -196,47 +107,299 @@ private fun Modifier.tokenClickable(state: TokenItemState): Modifier = composed } } -// region preview -@Preview +/** + * IMPORTANT! All margins that used between children setup like as children paddings. + */ +@Suppress("LongMethod") @Composable -private fun Preview_Tokens_LightTheme(@PreviewParameter(TokenConfigProvider::class) state: TokenItemState) { +private fun CustomContainer(state: TokenItemState, modifier: Modifier = Modifier, content: @Composable () -> Unit) { + val density = LocalDensity.current + val dimens = TangemTheme.dimens + + Layout(content = content, modifier = modifier) { measurables, constraints -> + + val layoutWidth = constraints.maxWidth + val layoutPadding = with(density) { dimens.size14.roundToPx() } + val layoutWidthWithPaddings = layoutWidth - 2 * layoutPadding + + val titleMinWidth = (layoutWidth * TITLE_MIN_WIDTH_COEFFICIENT).toInt() + val priceChangeMinWidth = (layoutWidth * PRICE_CHANGE_MIN_WIDTH_COEFFICIENT).toInt() + + val icon = measurables.measure(layoutId = LayoutId.ICON, constraints = constraints) + + /* + * Title width take the whole REMAINING space. + * If FiatAmount took the whole free space, then Title will has min width. + */ + val title: Placeable + + // FiatAmount width must take the whole free space but is not greater the Title min size + var fiatAmount: Placeable? = null + + // CryptoAmount width must take the whole free space but is not greater the PriceChange min size + var cryptoAmount: Placeable? = null + + /* + * PriceChange width take the whole REMAINING space. + * If CryptoAmount took the whole free space, then PriceChange will has min width. + */ + val priceChange: Placeable? + + val nonFiatContent = measurables.measure(layoutId = LayoutId.NON_FIAT_CONTENT, constraints = constraints) + + var firstRowRemainingFreeSpace: Int? = null + var secondRowRemainingFreeSpace: Int? = null + + when (state) { + is TokenItemState.Content, + is TokenItemState.Loading, + is TokenItemState.Locked, + -> { + fiatAmount = measurables.measureFiatAmount( + state = state, + maxWidth = layoutWidthWithPaddings - icon.width - titleMinWidth, + defaultConstraints = constraints, + ) + + cryptoAmount = measurables.measureCryptoAmount( + state = state, + maxWidth = layoutWidthWithPaddings - icon.width - priceChangeMinWidth, + defaultConstraints = constraints, + ) + + firstRowRemainingFreeSpace = layoutWidthWithPaddings - icon.width - fiatAmount.width + secondRowRemainingFreeSpace = layoutWidthWithPaddings - icon.width - cryptoAmount.width + } + is TokenItemState.Draggable -> { + cryptoAmount = measurables.measureCryptoAmount( + state = state, + maxWidth = layoutWidthWithPaddings - icon.width - nonFiatContent.width, + defaultConstraints = constraints, + ) + + firstRowRemainingFreeSpace = layoutWidthWithPaddings - icon.width - nonFiatContent.width + } + is TokenItemState.NoAddress, + is TokenItemState.Unreachable, + -> { + firstRowRemainingFreeSpace = layoutWidthWithPaddings - icon.width - nonFiatContent.width + } + } + + title = measurables.measureTitle( + state = state, + minWidth = titleMinWidth, + remainingFreeSpace = firstRowRemainingFreeSpace, + defaultConstraints = constraints, + ) + + priceChange = secondRowRemainingFreeSpace?.let { + measurables.measurePriceChange( + state = state, + minWidth = priceChangeMinWidth, + remainingFreeSpace = secondRowRemainingFreeSpace, + defaultConstraints = constraints, + ) + } + + val layoutHeight = calculateLayoutHeight( + state = state, + minLayoutHeight = with(density) { dimens.size68.roundToPx() }, + layoutPadding = layoutPadding, + betweenRowsPadding = with(density) { dimens.size2.roundToPx() }, + title = title, + fiatAmount = fiatAmount, + cryptoAmount = cryptoAmount, + priceChange = priceChange, + ) + + layout(width = constraints.maxWidth, height = layoutHeight) { + icon.placeRelative(x = layoutPadding, y = (layoutHeight - icon.height).div(other = 2)) + + title.placeRelative( + x = layoutPadding + icon.width, + y = when (state) { + is TokenItemState.NoAddress, + is TokenItemState.Unreachable, + -> (layoutHeight - title.height).div(other = 2) + else -> layoutPadding + }, + ) + + cryptoAmount?.placeRelative( + x = layoutPadding + icon.width, + y = layoutHeight - cryptoAmount.height - layoutPadding, + ) + + fiatAmount?.placeRelative(x = layoutWidth - fiatAmount.width - layoutPadding, y = layoutPadding) + + priceChange?.placeRelative( + x = layoutWidth - priceChange.width - layoutPadding, + y = layoutHeight - priceChange.height - layoutPadding, + ) + + nonFiatContent.placeRelative( + x = layoutWidth - nonFiatContent.width - layoutPadding, + y = (layoutHeight - nonFiatContent.height).div(other = 2), + ) + } + } +} + +private fun List.measureFiatAmount( + state: TokenItemState, + maxWidth: Int, + defaultConstraints: Constraints, +): Placeable { + return measure( + layoutId = LayoutId.FIAT_AMOUNT, + constraints = when (state) { + is TokenItemState.Content, + is TokenItemState.Draggable, + -> createConstrainsSafely(maxWidth = maxWidth) + else -> defaultConstraints + }, + ) +} + +private fun List.measureCryptoAmount( + state: TokenItemState, + maxWidth: Int, + defaultConstraints: Constraints, +): Placeable { + return measure( + layoutId = LayoutId.CRYPTO_AMOUNT, + constraints = when (state) { + is TokenItemState.Content, + is TokenItemState.Draggable, + -> createConstrainsSafely(maxWidth = maxWidth) + else -> defaultConstraints + }, + ) +} + +private fun List.measureTitle( + state: TokenItemState, + minWidth: Int, + remainingFreeSpace: Int, + defaultConstraints: Constraints, +): Placeable { + return measure( + layoutId = LayoutId.TITLE, + constraints = when (state) { + is TokenItemState.Content, + is TokenItemState.Draggable, + is TokenItemState.NoAddress, + is TokenItemState.Unreachable, + -> createDynamicConstrains(minWidth = minWidth, remainingFreeSpace = remainingFreeSpace) + else -> defaultConstraints + }, + ) +} + +private fun List.measurePriceChange( + state: TokenItemState, + minWidth: Int, + remainingFreeSpace: Int, + defaultConstraints: Constraints, +): Placeable { + return measure( + layoutId = LayoutId.PRICE_CHANGE, + constraints = when (state) { + is TokenItemState.Content, + -> createDynamicConstrains(minWidth = minWidth, remainingFreeSpace = remainingFreeSpace) + else -> defaultConstraints + }, + ) +} + +private fun List.measure(layoutId: LayoutId, constraints: Constraints): Placeable { + return requireNotNull( + value = firstOrNull { it.layoutId == layoutId }, + lazyMessage = { "Measurables[$layoutId] is null" }, + ).measure(constraints) +} + +private fun createDynamicConstrains(minWidth: Int, remainingFreeSpace: Int): Constraints { + return createConstrainsSafely( + minWidth = minWidth, + maxWidth = max(a = minWidth, b = remainingFreeSpace), + ) +} + +private fun createConstrainsSafely( + minWidth: Int = 0, + maxWidth: Int = Constraints.Infinity, + minHeight: Int = 0, + maxHeight: Int = Constraints.Infinity, +): Constraints { + return Constraints( + minWidth = minWidth.makeNotLessZero(), + maxWidth = maxWidth.makeNotLessZero(), + minHeight = minHeight.makeNotLessZero(), + maxHeight = maxHeight.makeNotLessZero(), + ) +} + +private fun Int.makeNotLessZero(): Int = max(a = 0, b = this) + +@Suppress("LongParameterList") +private fun calculateLayoutHeight( + state: TokenItemState, + minLayoutHeight: Int, + layoutPadding: Int, + betweenRowsPadding: Int, + title: Placeable, + fiatAmount: Placeable?, + cryptoAmount: Placeable?, + priceChange: Placeable?, +): Int { + val firstColumnHeight: Int + val secondColumnHeight: Int + + when (state) { + is TokenItemState.Content, + is TokenItemState.Loading, + is TokenItemState.Locked, + -> { + firstColumnHeight = 2 * layoutPadding + title.height + betweenRowsPadding + (cryptoAmount?.height ?: 0) + secondColumnHeight = 2 * layoutPadding + (fiatAmount?.height ?: 0) + betweenRowsPadding + + (priceChange?.height ?: 0) + } + is TokenItemState.Draggable, + is TokenItemState.NoAddress, + is TokenItemState.Unreachable, + -> { + firstColumnHeight = minLayoutHeight + secondColumnHeight = minLayoutHeight + } + } + + return max(firstColumnHeight, secondColumnHeight).coerceAtLeast(minLayoutHeight) +} + +@Preview(widthDp = 360) +@Composable +private fun Preview_CustomTokenItem_InLight(@PreviewParameter(TokenItemStateProvider::class) state: TokenItemState) { TangemTheme(isDark = false) { - TokenItem(state) + TokenItem(state = state) } } -@Preview -@Composable -private fun Preview_Tokens_DarkTheme(@PreviewParameter(TokenConfigProvider::class) state: TokenItemState) { - TangemTheme(isDark = true) { - TokenItem(state) - } -} - -private class TokenConfigProvider : CollectionPreviewParameterProvider( +private class TokenItemStateProvider : CollectionPreviewParameterProvider( collection = listOf( - WalletPreviewData.tokenItemVisibleState.copy( - cryptoAmountState = TokenItemState.CryptoAmountState.Content( - text = "5,41221467146712416241274127841274174213421 MATIC", - ), - ), - WalletPreviewData.tokenItemVisibleState.copy( - priceChangeState = TokenItemState.PriceChangeState.Content( - valueInPercent = "31231231231231231231223123123123212312312312.0%", - type = PriceChangeType.UP, - ), - ), - WalletPreviewData.tokenItemVisibleState.copy( - cryptoAmountState = TokenItemState.CryptoAmountState.Content( - text = "5,41221467146712416241274127841274174213421 MATIC", - ), - priceChangeState = TokenItemState.PriceChangeState.Content( - valueInPercent = "31231231231231231231223123123123212312312312.0%", - type = PriceChangeType.UP, - ), - ), WalletPreviewData.tokenItemVisibleState.copy( iconState = WalletPreviewData.coinIconState.copy(showCustomBadge = true), + titleState = TokenItemState.TitleState.Content( + text = "PolygonPolygonPolygonPolygonPolygonPolygon", + hasPending = true, + ), + fiatAmountState = TokenItemState.FiatAmountState.Content(text = "3213123123321312312312312312 $"), + cryptoAmountState = TokenItemState.CryptoAmountState.Content(text = "5,4123123213123123123123123123 MATIC"), + priceChangeState = TokenItemState.PriceChangeState.Content( + valueInPercent = "2365723643724723423742342374623642374723472342342.0%", + type = PriceChangeType.UP, + ), ), WalletPreviewData.tokenItemUnreachableState, WalletPreviewData.tokenItemNoAddressState, @@ -247,6 +410,4 @@ private class TokenConfigProvider : CollectionPreviewParameterProvider, + private val isBalanceHiddenProvider: Provider, + private val listStateProvider: Provider, ) { private val stateFlowInternal: MutableStateFlow = MutableStateFlow(getInitialState()) private val tokenListConverter by lazy { - val tokensConverter = CryptoCurrencyToDraggableItemConverter(appCurrencyProvider) + val tokensConverter = CryptoCurrencyToDraggableItemConverter(appCurrencyProvider, isBalanceHiddenProvider) val itemsConverter = TokenListToListStateConverter( tokensConverter = tokensConverter, groupsConverter = NetworkGroupToDraggableItemsConverter(tokensConverter), @@ -38,9 +41,8 @@ internal class OrganizeTokensStateHolder( TokenListToStateConverter(Provider(stateFlowInternal::value), itemsConverter) } - private val inProgressStateConverter by lazy { - InProgressStateConverter() - } + private val inProgressStateConverter by lazy { InProgressStateConverter() } + private val tokenListHiddenStateConverter by lazy { TokenListHiddenStateConverter(listStateProvider) } private val tokenListErrorConverter by lazy { TokenListErrorConverter(Provider(stateFlowInternal::value), inProgressStateConverter) @@ -80,6 +82,10 @@ internal class OrganizeTokensStateHolder( updateState { copy(header = header.copy(isSortedByBalance = false)) } } + fun updateHiddenState(isBalanceHidden: Boolean) { + updateState { copy(itemsState = tokenListHiddenStateConverter.convert(isBalanceHidden)) } + } + fun updateStateWithError(error: TokenListError) { updateState { tokenListErrorConverter.convert(error) } } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensViewModel.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensViewModel.kt index 5a1af707d2..d71ac77914 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensViewModel.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensViewModel.kt @@ -7,6 +7,8 @@ import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.analytics.models.AnalyticsParam import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.balancehiding.IsBalanceHiddenUseCase +import com.tangem.domain.balancehiding.ListenToFlipsUseCase import com.tangem.domain.tokens.ApplyTokenListSortingUseCase import com.tangem.domain.tokens.GetTokenListUseCase import com.tangem.domain.tokens.ToggleTokenListGroupingUseCase @@ -36,6 +38,8 @@ internal class OrganizeTokensViewModel @Inject constructor( private val toggleTokenListSortingUseCase: ToggleTokenListSortingUseCase, private val applyTokenListSortingUseCase: ApplyTokenListSortingUseCase, private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, + private val isBalanceHiddenUseCase: IsBalanceHiddenUseCase, + private val listenToFlipsUseCase: ListenToFlipsUseCase, private val analyticsEventsHandler: AnalyticsEventHandler, private val dispatchers: CoroutineDispatcherProvider, savedStateHandle: SavedStateHandle, @@ -45,6 +49,8 @@ internal class OrganizeTokensViewModel @Inject constructor( private val selectedAppCurrencyFlow = createSelectedAppCurrencyFlow() + private var isBalanceHidden = true + private val dragAndDropAdapter = DragAndDropAdapter( listStateProvider = Provider { uiState.value.itemsState }, ) @@ -53,6 +59,8 @@ internal class OrganizeTokensViewModel @Inject constructor( intents = this, dragAndDropIntents = dragAndDropAdapter, appCurrencyProvider = Provider(selectedAppCurrencyFlow::value), + isBalanceHiddenProvider = Provider { isBalanceHidden }, + listStateProvider = Provider { uiState.value.itemsState }, ) private val userWalletId: UserWalletId by lazy { @@ -68,6 +76,20 @@ internal class OrganizeTokensViewModel @Inject constructor( override fun onCreate(owner: LifecycleOwner) { analyticsEventsHandler.send(PortfolioOrganizeTokensAnalyticsEvent.ScreenOpened) + isBalanceHiddenUseCase() + .flowWithLifecycle(owner.lifecycle) + .onEach { hidden -> + isBalanceHidden = hidden + stateHolder.updateHiddenState(isBalanceHidden) + } + .launchIn(viewModelScope) + + viewModelScope.launch { + listenToFlipsUseCase() + .flowWithLifecycle(owner.lifecycle) + .collect() + } + bootstrapTokenList() bootstrapDragAndDropUpdates() } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/model/OrganizeTokensListState.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/model/OrganizeTokensListState.kt index 5d16b5035c..2bc9fe9dd9 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/model/OrganizeTokensListState.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/model/OrganizeTokensListState.kt @@ -19,4 +19,12 @@ internal sealed class OrganizeTokensListState { object Empty : OrganizeTokensListState() { override val items: PersistentList = persistentListOf() } + + fun copySealed(items: PersistentList = this.items): OrganizeTokensListState { + return when (this) { + is GroupedByNetwork -> copy(items = items) + is Ungrouped -> copy(items = items) + is Empty -> Empty + } + } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/error/TokenListHiddenStateConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/error/TokenListHiddenStateConverter.kt new file mode 100644 index 0000000000..96e5cf4538 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/error/TokenListHiddenStateConverter.kt @@ -0,0 +1,31 @@ +package com.tangem.feature.wallet.presentation.organizetokens.utils.converter.error + +import com.tangem.common.Provider +import com.tangem.feature.wallet.presentation.organizetokens.model.DraggableItem +import com.tangem.feature.wallet.presentation.organizetokens.model.OrganizeTokensListState +import com.tangem.utils.converter.Converter +import kotlinx.collections.immutable.toPersistentList + +internal class TokenListHiddenStateConverter( + private val currentStateProvider: Provider, +) : Converter { + + override fun convert(input: Boolean): OrganizeTokensListState { + val currentState = currentStateProvider() + val isBalanceHidden = input + + return currentState.copySealed( + currentState.items.map { draggableItem -> + if (draggableItem is DraggableItem.Token) { + draggableItem.copy( + tokenItemState = draggableItem.tokenItemState.copy( + isBalanceHidden = isBalanceHidden, + ), + ) + } else { + draggableItem + } + }.toPersistentList(), + ) + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/items/CryptoCurrencyToDraggableItemConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/items/CryptoCurrencyToDraggableItemConverter.kt index aa518deba2..f18e43de7a 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/items/CryptoCurrencyToDraggableItemConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/items/CryptoCurrencyToDraggableItemConverter.kt @@ -13,26 +13,29 @@ import com.tangem.utils.converter.Converter internal class CryptoCurrencyToDraggableItemConverter( private val appCurrencyProvider: Provider, + private val isBalanceHiddenProvider: Provider, ) : Converter { private val iconStateConverter = CryptoCurrencyToIconStateConverter() override fun convert(value: CryptoCurrencyStatus): DraggableItem.Token { - return createDraggableToken(value, appCurrencyProvider()) + return createDraggableToken(value, appCurrencyProvider(), isBalanceHiddenProvider()) } override fun convertList(input: Collection): List { val appCurrency = appCurrencyProvider() + val isBalanceHidden = isBalanceHiddenProvider() - return input.map { createDraggableToken(it, appCurrency) } + return input.map { createDraggableToken(it, appCurrency, isBalanceHidden) } } private fun createDraggableToken( currencyStatus: CryptoCurrencyStatus, appCurrency: AppCurrency, + isBalanceHidden: Boolean, ): DraggableItem.Token { return DraggableItem.Token( - tokenItemState = createTokenItemState(currencyStatus, appCurrency), + tokenItemState = createTokenItemState(currencyStatus, appCurrency, isBalanceHidden), groupId = getGroupHeaderId(currencyStatus.currency.network), ) } @@ -40,6 +43,7 @@ internal class CryptoCurrencyToDraggableItemConverter( private fun createTokenItemState( currencyStatus: CryptoCurrencyStatus, appCurrency: AppCurrency, + isBalanceHidden: Boolean, ): TokenItemState.Draggable { val currency = currencyStatus.currency @@ -52,6 +56,8 @@ internal class CryptoCurrencyToDraggableItemConverter( } else { TokenItemState.CryptoAmountState.Content(text = getFormattedFiatAmount(currencyStatus, appCurrency)) }, + isBalanceHidden = isBalanceHidden, + ) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletCard.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletCard.kt index d7dad0e9e2..9af63cffb8 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletCard.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletCard.kt @@ -2,9 +2,8 @@ package com.tangem.feature.wallet.presentation.wallet.ui.components.common import androidx.annotation.DrawableRes import androidx.annotation.StringRes -import androidx.compose.animation.AnimatedContent -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.ExperimentalAnimationApi +import androidx.compose.animation.* +import androidx.compose.animation.core.tween import androidx.compose.foundation.* import androidx.compose.foundation.gestures.detectTapGestures import androidx.compose.foundation.interaction.MutableInteractionSource @@ -28,16 +27,13 @@ import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider -import androidx.compose.ui.unit.Dp -import androidx.compose.ui.unit.DpOffset -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import androidx.constraintlayout.compose.ConstraintLayout -import androidx.constraintlayout.compose.ConstraintLayoutScope -import androidx.constraintlayout.compose.Dimension +import androidx.compose.ui.unit.* +import androidx.constraintlayout.compose.* +import com.tangem.common.Strings import com.tangem.core.ui.components.FontSizeRange import com.tangem.core.ui.components.RectangleShimmer import com.tangem.core.ui.components.ResizableText @@ -50,6 +46,8 @@ import com.tangem.feature.wallet.impl.R import com.tangem.feature.wallet.presentation.common.WalletPreviewData import com.tangem.feature.wallet.presentation.wallet.state.components.WalletCardState +private const val HALF_OF_ITEM_WIDTH = 0.5 + /** * Wallet card * @@ -67,47 +65,65 @@ internal fun WalletCard(state: WalletCardState, modifier: Modifier = Modifier) { onRenameClick = { state.onRenameClick(state.id, it) }, isLockedState = state is WalletCardState.LockedContent, modifier = modifier, - ) { - val (title, balance, additionalText, image) = createRefs() + ) { itemSize -> + val (titleRef, balanceRef, additionalTextRef, imageRef) = createRefs() val contentVerticalMargin = TangemTheme.dimens.spacing12 - Title( - state = state, - modifier = Modifier.constrainAs(title) { + TitleText( + text = state.title, + modifier = Modifier.constrainAs(titleRef) { start.linkTo(parent.start) top.linkTo(anchor = parent.top, margin = contentVerticalMargin) - end.linkTo(image.start) + end.linkTo(imageRef.start) width = Dimension.fillToConstraints }, ) - val betweenContentMargin = TangemTheme.dimens.spacing8 + var balanceWidth by remember { mutableStateOf(value = Int.MIN_VALUE) } Balance( state = state, - modifier = Modifier.constrainAs(balance) { - start.linkTo(parent.start) - top.linkTo(anchor = title.bottom, margin = betweenContentMargin) - bottom.linkTo(anchor = additionalText.top, margin = betweenContentMargin) - }, + modifier = Modifier + .onSizeChanged { balanceWidth = it.width } + .padding(vertical = TangemTheme.dimens.spacing8) + .constrainAs(balanceRef) { + start.linkTo(parent.start) + top.linkTo(anchor = titleRef.bottom) + bottom.linkTo(anchor = additionalTextRef.top) + }, ) AdditionalInfo( - state = state, - modifier = Modifier.constrainAs(additionalText) { + text = resolveAdditionalTextByState(state), + modifier = Modifier.constrainAs(additionalTextRef) { start.linkTo(parent.start) + top.linkTo(balanceRef.bottom) bottom.linkTo(anchor = parent.bottom, margin = contentVerticalMargin) + + when (state) { + is WalletCardState.Content, + is WalletCardState.Error, + is WalletCardState.HiddenContent, + -> { + end.linkTo(imageRef.start) + width = Dimension.fillToConstraints + } + else -> Unit + } }, ) - val imageWidth = TangemTheme.dimens.size120 + // If balance has a large width then image must be hidden + val hasSpaceForImage by remember(key1 = balanceWidth, key2 = itemSize.width) { + mutableStateOf(value = balanceWidth < itemSize.width * HALF_OF_ITEM_WIDTH) + } + Image( id = state.imageResId, - modifier = Modifier.constrainAs(image) { - centerVerticallyTo(parent) - top.linkTo(parent.top) + isVisible = hasSpaceForImage, + modifier = Modifier.constrainAs(imageRef) { end.linkTo(parent.end) + bottom.linkTo(parent.bottom) height = Dimension.fillToConstraints - width = Dimension.value(imageWidth) }, ) } @@ -120,11 +136,11 @@ private fun CardContainer( onRenameClick: (String) -> Unit, isLockedState: Boolean, modifier: Modifier = Modifier, - content: @Composable (ConstraintLayoutScope.() -> Unit), + content: @Composable (ConstraintLayoutScope.(IntSize) -> Unit), ) { var isMenuVisible by rememberSaveable { mutableStateOf(value = false) } var pressOffset by remember { mutableStateOf(value = DpOffset.Zero) } - var itemHeight by remember { mutableStateOf(value = 0.dp) } + var itemSize by remember { mutableStateOf(value = IntSize.Zero) } val density = LocalDensity.current val interactionSource = remember { MutableInteractionSource() } @@ -138,7 +154,7 @@ private fun CardContainer( Modifier } else { Modifier - .onSizeChanged { itemHeight = with(density) { it.height.toDp() } } + .onSizeChanged { itemSize = it } .clip(shape = TangemTheme.shapes.roundedCornersXMedium) .indication(interactionSource = interactionSource, indication = LocalIndication.current) .pointerInput(true) { @@ -166,12 +182,15 @@ private fun CardContainer( .fillMaxWidth() .padding(horizontal = TangemTheme.dimens.spacing14), ) { - content() + content(itemSize) } } var isRenameWalletDialogVisible by rememberSaveable { mutableStateOf(value = false) } + val itemHeight by remember(itemSize.height) { + mutableStateOf(value = with(density) { itemSize.height.toDp() }) + } ManageWalletContextMenu( isMenuVisible = isMenuVisible, pressOffset = pressOffset, @@ -243,22 +262,14 @@ private fun MenuItem(@StringRes textResId: Int, imageVector: ImageVector, onClic } @Composable -private fun Title(state: WalletCardState, modifier: Modifier = Modifier) { - Row( - modifier = modifier, - horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing4), - ) { - TitleText(title = state.title) - } -} - -@Composable -private fun TitleText(title: String) { +private fun TitleText(text: String, modifier: Modifier = Modifier) { Text( - text = title, + text = text, + modifier = modifier, color = TangemTheme.colors.text.tertiary, - style = TangemTheme.typography.button, maxLines = 1, + overflow = TextOverflow.Ellipsis, + style = TangemTheme.typography.button, ) } @@ -269,6 +280,10 @@ private fun Balance(state: WalletCardState, modifier: Modifier = Modifier) { targetState = state, label = "Update the balance", modifier = modifier, + transitionSpec = { + fadeIn(animationSpec = tween(durationMillis = 220, delayMillis = 90)) with + fadeOut(animationSpec = tween(durationMillis = 90)) + }, ) { walletCardState -> when (walletCardState) { is WalletCardState.Content -> { @@ -277,10 +292,12 @@ private fun Balance(state: WalletCardState, modifier: Modifier = Modifier) { fontSizeRange = FontSizeRange(min = 16.sp, max = TangemTheme.typography.h2.fontSize), modifier = Modifier.defaultMinSize(minHeight = TangemTheme.dimens.size32), color = TangemTheme.colors.text.primary1, + overflow = TextOverflow.Ellipsis, + maxLines = 1, style = TangemTheme.typography.h2, ) } - is WalletCardState.HiddenContent -> NonContentBalanceText(text = WalletCardState.HIDDEN_BALANCE_TEXT) + is WalletCardState.HiddenContent -> NonContentBalanceText(TextReference.Str(Strings.STARS)) is WalletCardState.Error -> NonContentBalanceText(text = WalletCardState.EMPTY_BALANCE_TEXT) is WalletCardState.Loading -> { RectangleShimmer(modifier = Modifier.nonContentBalanceSize(TangemTheme.dimens)) @@ -309,29 +326,41 @@ private fun Modifier.nonContentBalanceSize(dimens: TangemDimens): Modifier { @OptIn(ExperimentalAnimationApi::class) @Composable -private fun AdditionalInfo(state: WalletCardState, modifier: Modifier = Modifier) { +private fun AdditionalInfo(text: TextReference?, modifier: Modifier = Modifier) { AnimatedContent( - targetState = state, + targetState = text, label = "Update the additional text", modifier = modifier, - ) { animatedState -> - when (animatedState) { - is WalletCardState.Content -> AdditionalInfoText(text = animatedState.additionalInfo) - is WalletCardState.HiddenContent -> AdditionalInfoText(text = animatedState.additionalInfo) - is WalletCardState.LockedContent -> AdditionalInfoText(text = animatedState.additionalInfo) - is WalletCardState.Error -> AdditionalInfoText(text = WalletCardState.EMPTY_BALANCE_TEXT) - is WalletCardState.Loading -> { - RectangleShimmer(modifier = Modifier.nonContentAdditionalInfoSize(dimens = TangemTheme.dimens)) - } + transitionSpec = { + fadeIn(animationSpec = tween(durationMillis = 220, delayMillis = 90)) with + fadeOut(animationSpec = tween(durationMillis = 90)) + }, + ) { animatedText -> + if (animatedText != null) { + AdditionalInfoText(text = animatedText) + } else { + RectangleShimmer(modifier = Modifier.nonContentAdditionalInfoSize(dimens = TangemTheme.dimens)) } } } +private fun resolveAdditionalTextByState(state: WalletCardState): TextReference? { + return when (state) { + is WalletCardState.Content -> state.additionalInfo + is WalletCardState.LockedContent -> state.additionalInfo + is WalletCardState.Error -> WalletCardState.EMPTY_BALANCE_TEXT + is WalletCardState.HiddenContent -> WalletCardState.HIDDEN_BALANCE_TEXT + is WalletCardState.Loading -> null + } +} + @Composable private fun AdditionalInfoText(text: TextReference) { Text( text = text.resolveReference(), color = TangemTheme.colors.text.tertiary, + maxLines = 1, + overflow = TextOverflow.Ellipsis, style = TangemTheme.typography.caption, ) } @@ -351,11 +380,14 @@ private fun LockedContent(modifier: Modifier = Modifier) { } @Composable -private fun Image(@DrawableRes id: Int?, modifier: Modifier = Modifier) { - AnimatedVisibility(visible = id != null, modifier = modifier) { +private fun Image(@DrawableRes id: Int?, isVisible: Boolean, modifier: Modifier = Modifier) { + AnimatedVisibility(visible = id != null && isVisible, modifier = modifier) { + val imageRes = id ?: return@AnimatedVisibility + Image( - painter = painterResource(id = requireNotNull(id)), + painter = painterResource(id = imageRes), contentDescription = null, + modifier = Modifier.width(width = TangemTheme.dimens.size120), contentScale = ContentScale.FillWidth, ) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt index 850c1aeef3..e860f3d518 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt @@ -579,6 +579,7 @@ internal class WalletViewModel @Inject constructor( getNetworkCoinStatusUseCase( userWalletId = userWallet.walletId, networkId = cryptoCurrencyStatus.currency.network.id, + derivationPath = cryptoCurrencyStatus.currency.network.derivationPath, ) .take(count = 1) .collectLatest { @@ -913,8 +914,26 @@ internal class WalletViewModel @Inject constructor( private fun getSingleCurrencyContent(index: Int) { val wallet = getWallet(index) - updatePrimaryCurrencyStatus(userWalletId = wallet.walletId) - updateNotifications(index) + getPrimaryCurrencyStatusUpdatesUseCase(userWalletId = wallet.walletId) + .distinctUntilChanged() + .onEach { maybeCryptoCurrencyStatus -> + uiState = stateFactory.getSingleCurrencyLoadedBalanceState(maybeCryptoCurrencyStatus) + + maybeCryptoCurrencyStatus.onRight { status -> + singleWalletCryptoCurrencyStatus = status + + if (status.value.amount?.isZero() == false) { + setWalletWithFundsFoundUseCase() + } + + updateNotifications(index) + updateButtons(userWalletId = wallet.walletId, currencyStatus = status) + updateTxHistory(status.currency) + } + } + .flowOn(dispatchers.io) + .launchIn(viewModelScope) + .saveIn(marketPriceJobHolder) } private fun updateTxHistory(currency: CryptoCurrency) { @@ -935,28 +954,6 @@ internal class WalletViewModel @Inject constructor( } } - private fun updatePrimaryCurrencyStatus(userWalletId: UserWalletId) { - getPrimaryCurrencyStatusUpdatesUseCase(userWalletId = userWalletId) - .distinctUntilChanged() - .onEach { maybeCryptoCurrencyStatus -> - uiState = stateFactory.getSingleCurrencyLoadedBalanceState(maybeCryptoCurrencyStatus) - - maybeCryptoCurrencyStatus.onRight { status -> - singleWalletCryptoCurrencyStatus = status - - if (status.value.amount?.isZero() == false) { - setWalletWithFundsFoundUseCase() - } - - updateButtons(userWalletId = userWalletId, currencyStatus = status) - updateTxHistory(status.currency) - } - } - .flowOn(dispatchers.io) - .launchIn(viewModelScope) - .saveIn(marketPriceJobHolder) - } - private fun updateButtons(userWalletId: UserWalletId, currencyStatus: CryptoCurrencyStatus) { getCryptoCurrencyActionsUseCase(userWalletId = userWalletId, cryptoCurrencyStatus = currencyStatus) .distinctUntilChanged()