diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 5e5e70656a..570f9e96e3 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -35,6 +35,8 @@ dependencies { implementation(projects.domain.txhistory) implementation(projects.domain.appCurrency) implementation(projects.domain.appCurrency.models) + implementation(projects.domain.appTheme) + implementation(projects.domain.appTheme.models) implementation(project(":common")) implementation(project(":core:analytics")) @@ -48,13 +50,15 @@ dependencies { implementation(project(":libs:crypto")) implementation(project(":libs:auth")) - implementation(project(":data:source:preferences")) + implementation(projects.data.appCurrency) + implementation(projects.data.appTheme) implementation(projects.data.card) implementation(projects.data.common) implementation(projects.data.settings) + implementation(projects.data.source.preferences) implementation(projects.data.tokens) implementation(projects.data.txhistory) - implementation(projects.data.appCurrency) + implementation(projects.data.wallets) /** Features */ implementation(project(":features:onboarding")) @@ -86,6 +90,7 @@ dependencies { implementation(deps.lifecycle.runtime.ktx) implementation(deps.lifecycle.common.java8) implementation(deps.lifecycle.viewModel.ktx) + implementation(deps.lifecycle.compose) /** Compose libraries */ implementation(deps.compose.constraintLayout) diff --git a/app/src/main/java/com/tangem/tap/GlobalSettingsState.kt b/app/src/main/java/com/tangem/tap/GlobalSettingsState.kt new file mode 100644 index 0000000000..19cd307271 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/GlobalSettingsState.kt @@ -0,0 +1,12 @@ +package com.tangem.tap + +import com.tangem.domain.apptheme.model.AppThemeMode + +internal sealed class GlobalSettingsState { + + object Loading : GlobalSettingsState() + + data class Content( + val appThemeMode: AppThemeMode, + ) : GlobalSettingsState() +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/MainActivity.kt b/app/src/main/java/com/tangem/tap/MainActivity.kt index f0a79eafeb..52e9ff1262 100644 --- a/app/src/main/java/com/tangem/tap/MainActivity.kt +++ b/app/src/main/java/com/tangem/tap/MainActivity.kt @@ -4,10 +4,10 @@ import android.content.Intent import android.content.pm.ActivityInfo import android.os.Bundle import android.view.View +import androidx.activity.viewModels import androidx.appcompat.app.AppCompatActivity import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen import androidx.core.view.WindowCompat -import androidx.core.view.WindowInsetsControllerCompat import androidx.lifecycle.lifecycleScope import by.kirich1409.viewbindingdelegate.viewBinding import com.google.android.material.snackbar.Snackbar @@ -26,6 +26,7 @@ import com.tangem.tap.common.ActivityResultCallbackHolder import com.tangem.tap.common.DialogManager import com.tangem.tap.common.OnActivityResultCallback import com.tangem.tap.common.SnackbarHandler +import com.tangem.tap.common.apptheme.MutableAppThemeModeHolder import com.tangem.tap.common.extensions.dispatchOnMain import com.tangem.tap.common.redux.NotificationsHandler import com.tangem.tap.common.redux.global.GlobalAction @@ -53,6 +54,8 @@ import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import java.lang.ref.WeakReference import javax.inject.Inject @@ -109,6 +112,9 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac @Inject lateinit var walletConnectInteractor: WalletConnectInteractor + private val viewModel: MainViewModel by viewModels() + private var isInitializing: Boolean = true + // TODO: fixme: inject through DI private val intentProcessor: IntentProcessor = IntentProcessor() @@ -119,11 +125,17 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac private val onActivityResultCallbacks = mutableListOf() override fun onCreate(savedInstanceState: Bundle?) { - installSplashScreen() + val splashScreen = installSplashScreen() super.onCreate(savedInstanceState) + + bootstrapMainStateUpdates() + + splashScreen.setKeepOnScreenCondition { isInitializing } + setContentView(R.layout.activity_main) systemActions() + store.dispatch(NavigationAction.ActivityCreated(WeakReference(this))) cardSdkLifecycleObserver.onCreate(context = this) @@ -204,13 +216,24 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac store.dispatch(GlobalAction.UpdateUserWalletsListManager(manager)) } + private fun bootstrapMainStateUpdates() { + viewModel.state + .onEach { state -> + isInitializing = state is GlobalSettingsState.Loading + + when (state) { + is GlobalSettingsState.Content -> { + MutableAppThemeModeHolder.value = state.appThemeMode + } + is GlobalSettingsState.Loading -> Unit + } + } + .launchIn(lifecycleScope) + } + private fun systemActions() { WindowCompat.setDecorFitsSystemWindows(window, false) - val windowInsetsController = WindowInsetsControllerCompat(window, binding.root) - windowInsetsController.isAppearanceLightStatusBars = true - windowInsetsController.isAppearanceLightNavigationBars = true - supportFragmentManager.registerFragmentLifecycleCallbacks( NavBarInsetsFragmentLifecycleCallback(), true, diff --git a/app/src/main/java/com/tangem/tap/MainViewModel.kt b/app/src/main/java/com/tangem/tap/MainViewModel.kt new file mode 100644 index 0000000000..2cb276000c --- /dev/null +++ b/app/src/main/java/com/tangem/tap/MainViewModel.kt @@ -0,0 +1,35 @@ +package com.tangem.tap + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import arrow.core.getOrElse +import com.tangem.domain.apptheme.GetAppThemeModeUseCase +import com.tangem.domain.apptheme.model.AppThemeMode +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn +import javax.inject.Inject + +@HiltViewModel +internal class MainViewModel @Inject constructor( + private val getAppThemeModeUseCase: GetAppThemeModeUseCase, +) : ViewModel() { + + val state: StateFlow = createMainStateFlow() + + private fun createMainStateFlow(): StateFlow { + return getAppThemeModeUseCase() + .map { maybeMode -> + val mode = maybeMode.getOrElse { AppThemeMode.DEFAULT } + + GlobalSettingsState.Content(appThemeMode = mode) + } + .stateIn( + scope = viewModelScope, + started = SharingStarted.WhileSubscribed(stopTimeoutMillis = 5_000), + initialValue = GlobalSettingsState.Loading, + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/TapApplication.kt b/app/src/main/java/com/tangem/tap/TapApplication.kt index fb12b1008f..cfd38560ca 100644 --- a/app/src/main/java/com/tangem/tap/TapApplication.kt +++ b/app/src/main/java/com/tangem/tap/TapApplication.kt @@ -26,6 +26,7 @@ import com.tangem.domain.DomainLayer import com.tangem.domain.appcurrency.repository.AppCurrencyRepository import com.tangem.domain.card.ScanCardProcessor import com.tangem.domain.common.LogConfig +import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.legacy.WalletManagersRepository import com.tangem.features.tokendetails.featuretoggles.TokenDetailsFeatureToggles import com.tangem.features.wallet.featuretoggles.WalletFeatureToggles @@ -41,6 +42,7 @@ import com.tangem.tap.common.feedback.AdditionalFeedbackInfo import com.tangem.tap.common.feedback.FeedbackManager import com.tangem.tap.common.images.createCoilImageLoader import com.tangem.tap.common.log.TangemLogCollector +import com.tangem.tap.common.log.TimberFormatStrategy import com.tangem.tap.common.redux.AppState import com.tangem.tap.common.redux.appReducer import com.tangem.tap.common.redux.global.GlobalAction @@ -164,6 +166,9 @@ class TapApplication : Application(), ImageLoaderFactory { @Inject lateinit var appCurrencyRepository: AppCurrencyRepository + @Inject + lateinit var walletManagersFacade: WalletManagersFacade + override fun onCreate() { super.onCreate() @@ -183,12 +188,13 @@ class TapApplication : Application(), ImageLoaderFactory { tokenDetailsFeatureToggles = tokenDetailsFeatureToggles, scanCardProcessor = scanCardProcessor, appCurrencyRepository = appCurrencyRepository, + walletManagersFacade = walletManagersFacade, ), ), ) if (BuildConfig.DEBUG) { - Logger.addLogAdapter(AndroidLogAdapter()) + Logger.addLogAdapter(AndroidLogAdapter(TimberFormatStrategy())) Timber.plant( object : Timber.DebugTree() { override fun log(priority: Int, tag: String?, message: String, t: Throwable?) { diff --git a/app/src/main/java/com/tangem/tap/common/apptheme/MutableAppThemeModeHolder.kt b/app/src/main/java/com/tangem/tap/common/apptheme/MutableAppThemeModeHolder.kt new file mode 100644 index 0000000000..c1b0f942c5 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/apptheme/MutableAppThemeModeHolder.kt @@ -0,0 +1,17 @@ +package com.tangem.tap.common.apptheme + +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.mutableStateOf +import com.tangem.core.ui.theme.AppThemeModeHolder +import com.tangem.domain.apptheme.model.AppThemeMode + +internal object MutableAppThemeModeHolder : AppThemeModeHolder { + + override val appThemeMode: MutableState = mutableStateOf(AppThemeMode.DEFAULT) + + var value: AppThemeMode + set(value) { + appThemeMode.value = value + } + get() = appThemeMode.value +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/extensions/WalletManager.kt b/app/src/main/java/com/tangem/tap/common/extensions/WalletManager.kt index 6665cab485..1533313216 100644 --- a/app/src/main/java/com/tangem/tap/common/extensions/WalletManager.kt +++ b/app/src/main/java/com/tangem/tap/common/extensions/WalletManager.kt @@ -82,7 +82,7 @@ fun WalletManager?.getAddressData(): WalletDataModel.AddressData? { } fun WalletManager.Companion.stub(): T { - val wallet = Wallet(Blockchain.Unknown, setOf(), Wallet.PublicKey(byteArrayOf(), null, null), setOf()) + val wallet = Wallet(Blockchain.Unknown, setOf(), Wallet.PublicKey(byteArrayOf(), null), setOf()) return object : WalletManager(wallet) { override val currentHost: String = "" override suspend fun update() {} diff --git a/app/src/main/java/com/tangem/tap/common/log/TimberFormatStrategy.kt b/app/src/main/java/com/tangem/tap/common/log/TimberFormatStrategy.kt new file mode 100644 index 0000000000..0a43f525d4 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/log/TimberFormatStrategy.kt @@ -0,0 +1,63 @@ +package com.tangem.tap.common.log + +import com.orhanobut.logger.FormatStrategy +import com.orhanobut.logger.LogStrategy +import com.orhanobut.logger.LogcatLogStrategy + +class TimberFormatStrategy : FormatStrategy { + + private val logStrategy: LogStrategy = LogcatLogStrategy() + + override fun log(priority: Int, tag: String?, message: String) { + logTopBorder(priority, tag) + val bytes = message.toByteArray() + val length = bytes.size + if (length <= CHUNK_SIZE) { + logContent(priority, tag, message) + logBottomBorder(priority, tag) + return + } + var i = 0 + while (i < length) { + val count = (length - i).coerceAtMost(CHUNK_SIZE) + // create a new String with system's default charset (which is UTF-8 for Android) + logContent(priority, tag, String(bytes, i, count)) + i += CHUNK_SIZE + } + logBottomBorder(priority, tag) + } + + private fun logTopBorder(logType: Int, tag: String?) { + logChunk(logType, tag, TOP_BORDER) + } + + private fun logBottomBorder(logType: Int, tag: String?) { + logChunk(logType, tag, BOTTOM_BORDER) + } + + private fun logContent(logType: Int, tag: String?, chunk: String) { + chunk.split(System.lineSeparator()).forEach { line -> + logChunk(logType, tag, "$HORIZONTAL_LINE $line") + } + } + + private fun logChunk(priority: Int, tag: String?, chunk: String) { + logStrategy.log(priority, tag, chunk) + } + + private companion object { + /** + * Android's max limit for a log entry is ~4076 bytes, + * so 4000 bytes is used as chunk size since default charset + * is UTF-8 + */ + private const val CHUNK_SIZE = 4000 + + const val TOP_LEFT_CORNER = "┌" + const val BOTTOM_LEFT_CORNER = "└" + const val HORIZONTAL_LINE = "│" + const val DOUBLE_DIVIDER = "────────────────────────────────────────────────────────" + const val TOP_BORDER = TOP_LEFT_CORNER + DOUBLE_DIVIDER + DOUBLE_DIVIDER + const val BOTTOM_BORDER = BOTTOM_LEFT_CORNER + DOUBLE_DIVIDER + DOUBLE_DIVIDER + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/di/AppStateHolderModule.kt b/app/src/main/java/com/tangem/tap/di/AppStateHolderModule.kt index 05cc610f95..e1bf363dd7 100644 --- a/app/src/main/java/com/tangem/tap/di/AppStateHolderModule.kt +++ b/app/src/main/java/com/tangem/tap/di/AppStateHolderModule.kt @@ -1,6 +1,7 @@ package com.tangem.tap.di -import com.tangem.core.navigation.NavigationStateHolder +import com.tangem.core.navigation.ReduxNavController +import com.tangem.domain.redux.ReduxStateHolder import com.tangem.domain.wallets.legacy.WalletsStateHolder import com.tangem.tap.proxy.AppStateHolder import dagger.Binds @@ -19,5 +20,9 @@ internal interface AppStateHolderModule { @Binds @Singleton - fun bindsNavigationStateHolder(appStateHolder: AppStateHolder): NavigationStateHolder + fun bindsNavigationStateHolder(appStateHolder: AppStateHolder): ReduxNavController + + @Binds + @Singleton + fun bindsReduxStateHolder(appStateHolder: AppStateHolder): ReduxStateHolder } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/di/ThemeModule.kt b/app/src/main/java/com/tangem/tap/di/ThemeModule.kt new file mode 100644 index 0000000000..af2732b2d9 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/di/ThemeModule.kt @@ -0,0 +1,18 @@ +package com.tangem.tap.di + +import com.tangem.core.ui.theme.AppThemeModeHolder +import com.tangem.tap.common.apptheme.MutableAppThemeModeHolder +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.components.ActivityComponent + +@Module +@InstallIn(ActivityComponent::class) +internal object ThemeModule { + + @Provides + fun provideAppThemeModeHolder(): AppThemeModeHolder { + return MutableAppThemeModeHolder + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/di/domain/AppThemeDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/AppThemeDomainModule.kt new file mode 100644 index 0000000000..b4378101b0 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/di/domain/AppThemeDomainModule.kt @@ -0,0 +1,24 @@ +package com.tangem.tap.di.domain + +import com.tangem.domain.apptheme.ChangeAppThemeModeUseCase +import com.tangem.domain.apptheme.GetAppThemeModeUseCase +import com.tangem.domain.apptheme.repository.AppThemeModeRepository +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.components.ViewModelComponent + +@Module +@InstallIn(ViewModelComponent::class) +internal object AppThemeDomainModule { + + @Provides + fun provideGetAppThemeModeUpdatesUseCase(appThemeModeRepository: AppThemeModeRepository): GetAppThemeModeUseCase { + return GetAppThemeModeUseCase(appThemeModeRepository) + } + + @Provides + fun provideChangeAppThemeModeUseCase(appThemeModeRepository: AppThemeModeRepository): ChangeAppThemeModeUseCase { + return ChangeAppThemeModeUseCase(appThemeModeRepository) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/di/domain/SettingsDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/SettingsDomainModule.kt index 4897db1d24..eaf0da329c 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/SettingsDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/SettingsDomainModule.kt @@ -1,7 +1,11 @@ package com.tangem.tap.di.domain +import com.tangem.domain.settings.CanUseBiometryUseCase import com.tangem.domain.settings.IsUserAlreadyRateAppUseCase +import com.tangem.domain.settings.ShouldShowSaveWalletScreenUseCase import com.tangem.domain.settings.repositories.SettingsRepository +import com.tangem.tap.domain.TangemSdkManager +import com.tangem.tap.domain.settings.DefaultLegacySettingsRepository import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -17,4 +21,20 @@ internal object SettingsDomainModule { fun providesGetWalletsUseCase(settingsRepository: SettingsRepository): IsUserAlreadyRateAppUseCase { return IsUserAlreadyRateAppUseCase(settingsRepository = settingsRepository) } + + @Provides + @ViewModelScoped + fun providesShouldShowSaveWalletScreenUseCase( + settingsRepository: SettingsRepository, + ): ShouldShowSaveWalletScreenUseCase { + return ShouldShowSaveWalletScreenUseCase(settingsRepository = settingsRepository) + } + + @Provides + @ViewModelScoped + fun providesCanUseBiometryUseCase(tangemSdkManager: TangemSdkManager): CanUseBiometryUseCase { + return CanUseBiometryUseCase( + legacySettingsRepository = DefaultLegacySettingsRepository(tangemSdkManager = tangemSdkManager), + ) + } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt index d5ab303555..1a77f50c7b 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt @@ -15,6 +15,16 @@ import dagger.hilt.android.scopes.ViewModelScoped @InstallIn(ViewModelComponent::class) internal object TokensDomainModule { + @Provides + @ViewModelScoped + fun provideFetchTokenListUseCase( + currenciesRepository: CurrenciesRepository, + quotesRepository: QuotesRepository, + networksRepository: NetworksRepository, + ): FetchTokenListUseCase { + return FetchTokenListUseCase(currenciesRepository, networksRepository, quotesRepository) + } + @Provides @ViewModelScoped fun provideGetTokenListUseCase( @@ -26,6 +36,15 @@ internal object TokensDomainModule { return GetTokenListUseCase(currenciesRepository, quotesRepository, networksRepository, dispatchers) } + @Provides + @ViewModelScoped + fun provideRemoveCurrencyUseCase( + currenciesRepository: CurrenciesRepository, + dispatchers: CoroutineDispatcherProvider, + ): RemoveCurrencyUseCase { + return RemoveCurrencyUseCase(currenciesRepository, dispatchers) + } + @Provides @ViewModelScoped fun provideGetCurrencyUseCase( @@ -33,8 +52,8 @@ internal object TokensDomainModule { quotesRepository: QuotesRepository, networksRepository: NetworksRepository, dispatchers: CoroutineDispatcherProvider, - ): GetCurrencyUseCase { - return GetCurrencyUseCase(currenciesRepository, quotesRepository, networksRepository, dispatchers) + ): GetCurrencyStatusUpdatesUseCase { + return GetCurrencyStatusUpdatesUseCase(currenciesRepository, quotesRepository, networksRepository, dispatchers) } @Provides @@ -44,17 +63,31 @@ internal object TokensDomainModule { quotesRepository: QuotesRepository, networksRepository: NetworksRepository, dispatchers: CoroutineDispatcherProvider, - ): GetPrimaryCurrencyUseCase { - return GetPrimaryCurrencyUseCase(currenciesRepository, quotesRepository, networksRepository, dispatchers) + ): GetPrimaryCurrencyStatusUpdatesUseCase { + return GetPrimaryCurrencyStatusUpdatesUseCase( + currenciesRepository, + quotesRepository, + networksRepository, + dispatchers, + ) + } + + @Provides + @ViewModelScoped + fun provideFetchCurrencyStatusUseCase( + currenciesRepository: CurrenciesRepository, + quotesRepository: QuotesRepository, + networksRepository: NetworksRepository, + ): FetchCurrencyStatusUseCase { + return FetchCurrencyStatusUseCase(currenciesRepository, networksRepository, quotesRepository) } @Provides @ViewModelScoped fun provideToggleTokenListGroupingUseCase( - networksRepository: NetworksRepository, dispatchers: CoroutineDispatcherProvider, ): ToggleTokenListGroupingUseCase { - return ToggleTokenListGroupingUseCase(networksRepository, dispatchers) + return ToggleTokenListGroupingUseCase(dispatchers) } @Provides @@ -71,4 +104,12 @@ internal object TokensDomainModule { ): ApplyTokenListSortingUseCase { return ApplyTokenListSortingUseCase(currenciesRepository, dispatchers) } + + @Provides + @ViewModelScoped + fun provideGetCryptoCurrencyActionsUseCase( + dispatchers: CoroutineDispatcherProvider, + ): GetCryptoCurrencyActionsUseCase { + return GetCryptoCurrencyActionsUseCase(dispatchers) + } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/di/domain/WalletsDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/WalletsDomainModule.kt index 92f1340058..8e3d763c56 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/WalletsDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/WalletsDomainModule.kt @@ -2,6 +2,7 @@ package com.tangem.tap.di.domain import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.legacy.WalletsStateHolder +import com.tangem.domain.wallets.repository.WalletsRepository import com.tangem.domain.wallets.usecase.* import dagger.Module import dagger.Provides @@ -48,4 +49,22 @@ internal object WalletsDomainModule { fun providesSelectWalletUseCase(walletsStateHolder: WalletsStateHolder): SelectWalletUseCase { return SelectWalletUseCase(walletsStateHolder = walletsStateHolder) } + + @Provides + @ViewModelScoped + fun providesUpdateWalletUseCase(walletsStateHolder: WalletsStateHolder): UpdateWalletUseCase { + return UpdateWalletUseCase(walletsStateHolder = walletsStateHolder) + } + + @Provides + @ViewModelScoped + fun providesDeleteWalletUseCase(walletsStateHolder: WalletsStateHolder): DeleteWalletUseCase { + return DeleteWalletUseCase(walletsStateHolder = walletsStateHolder) + } + + @Provides + @ViewModelScoped + fun providesShouldSaveUserWalletsUseCase(walletsRepository: WalletsRepository): ShouldSaveUserWalletsUseCase { + return ShouldSaveUserWalletsUseCase(walletsRepository = walletsRepository) + } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/settings/DefaultLegacySettingsRepository.kt b/app/src/main/java/com/tangem/tap/domain/settings/DefaultLegacySettingsRepository.kt new file mode 100644 index 0000000000..681d4ffabd --- /dev/null +++ b/app/src/main/java/com/tangem/tap/domain/settings/DefaultLegacySettingsRepository.kt @@ -0,0 +1,11 @@ +package com.tangem.tap.domain.settings + +import com.tangem.domain.settings.repositories.LegacySettingsRepository +import com.tangem.tap.domain.TangemSdkManager + +internal class DefaultLegacySettingsRepository( + private val tangemSdkManager: TangemSdkManager, +) : LegacySettingsRepository { + + override fun canUseBiometry(): Boolean = tangemSdkManager.canUseBiometry +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/tasks/product/ScanProductTask.kt b/app/src/main/java/com/tangem/tap/domain/tasks/product/ScanProductTask.kt index e17d4e2ebb..31a7a1ade4 100644 --- a/app/src/main/java/com/tangem/tap/domain/tasks/product/ScanProductTask.kt +++ b/app/src/main/java/com/tangem/tap/domain/tasks/product/ScanProductTask.kt @@ -1,5 +1,6 @@ package com.tangem.tap.domain.tasks.product +import com.tangem.blockchain.blockchains.cardano.CardanoUtils import com.tangem.blockchain.common.Blockchain import com.tangem.common.CompletionResult import com.tangem.common.card.Card @@ -224,7 +225,8 @@ private class ScanWalletProcessor( walletData = session.environment.walletData, primaryCard = primaryCard, ) - val derivations = collectDerivations(card, config, scanResponse.derivationStyleProvider) + val derivations = + collectDerivations(card, config, scanResponse.derivationStyleProvider) if (derivations.isEmpty() || !card.settings.isHDWalletAllowed) { callback(CompletionResult.Success(scanResponse)) return@launch @@ -252,60 +254,101 @@ private class ScanWalletProcessor( derivationStyleProvider.getDerivationStyle(), ) .toMutableList() - .ifEmpty { - mutableListOf( - BlockchainNetwork( - blockchain = Blockchain.Bitcoin, - derivationStyleProvider = derivationStyleProvider, - ), - BlockchainNetwork( - blockchain = Blockchain.Ethereum, - derivationStyleProvider = derivationStyleProvider, - ), - ) - } + .ifEmpty { getDefaultBlockchains(derivationStyleProvider) } if (card.settings.isHDWalletAllowed) { - blockchainsToDerive.addAll( - listOf( - BlockchainNetwork( - blockchain = Blockchain.Ethereum, - derivationStyleProvider = derivationStyleProvider, - ), - BlockchainNetwork( - blockchain = Blockchain.EthereumTestnet, - derivationStyleProvider = derivationStyleProvider, - ), - ), - ) + blockchainsToDerive += getEthereumBlockchains(derivationStyleProvider) } - if (additionalBlockchainsToDerive != null) { - blockchainsToDerive.addAll( - additionalBlockchainsToDerive.map { - BlockchainNetwork( - blockchain = it, - derivationStyleProvider = derivationStyleProvider, - ) - }, - ) + + additionalBlockchainsToDerive?.let { + blockchainsToDerive += getAdditionalBlockchainToDerive(derivationStyleProvider, it) } + + // we should generate second key for cardano + // because cardano address generation for wallet2 requires keys from 2 derivations + // https://developers.cardano.org/docs/get-started/cardano-serialization-lib/generating-keys/ + val secondCardanoNetwork = blockchainsToDerive + .find { it.blockchain == Blockchain.Cardano } + ?.let { getCardanoSecondNetwork(it) } + secondCardanoNetwork?.let { blockchainsToDerive.add(it) } + + // pay attention to this if (!card.useOldStyleDerivation) { - blockchainsToDerive.removeAll( - listOf( - Blockchain.BSC, Blockchain.BSCTestnet, - Blockchain.Polygon, Blockchain.PolygonTestnet, - Blockchain.RSK, - Blockchain.Fantom, Blockchain.FantomTestnet, - Blockchain.Avalanche, Blockchain.AvalancheTestnet, - ).map { - BlockchainNetwork( - blockchain = it, - derivationStyleProvider = derivationStyleProvider, - ) - }, + removeUnnecessaryBlockchains(blockchainsToDerive, derivationStyleProvider) + } + + return blockchainsToDerive.distinct() + } + + private fun getDefaultBlockchains( + derivationStyleProvider: DerivationStyleProvider, + ): MutableList { + return mutableListOf( + BlockchainNetwork( + blockchain = Blockchain.Bitcoin, + derivationStyleProvider = derivationStyleProvider, + ), + BlockchainNetwork( + blockchain = Blockchain.Ethereum, + derivationStyleProvider = derivationStyleProvider, + ), + ) + } + + private fun getEthereumBlockchains(derivationStyleProvider: DerivationStyleProvider): List { + return listOf( + BlockchainNetwork( + blockchain = Blockchain.Ethereum, + derivationStyleProvider = derivationStyleProvider, + ), + BlockchainNetwork( + blockchain = Blockchain.EthereumTestnet, + derivationStyleProvider = derivationStyleProvider, + ), + ) + } + + private fun getAdditionalBlockchainToDerive( + derivationStyleProvider: DerivationStyleProvider, + collection: Collection, + ): List { + return collection.map { + BlockchainNetwork( + blockchain = it, + derivationStyleProvider = derivationStyleProvider, ) } - return blockchainsToDerive.distinct() + } + + private fun getCardanoSecondNetwork(cardanoBlockchainNetwork: BlockchainNetwork): BlockchainNetwork? { + val cardanoStandardDerivation = cardanoBlockchainNetwork.derivationPath?.let { DerivationPath(it) } + ?: return null + val cardanoPatchedDerivation = CardanoUtils.extendedDerivationPath(cardanoStandardDerivation) + return BlockchainNetwork( + blockchain = Blockchain.Cardano, + derivationPath = cardanoPatchedDerivation.rawPath, + tokens = emptyList(), + ) + } + + private fun removeUnnecessaryBlockchains( + blockchainsToDerive: MutableList, + derivationStyleProvider: DerivationStyleProvider, + ) { + blockchainsToDerive.removeAll( + listOf( + Blockchain.BSC, Blockchain.BSCTestnet, + Blockchain.Polygon, Blockchain.PolygonTestnet, + Blockchain.RSK, + Blockchain.Fantom, Blockchain.FantomTestnet, + Blockchain.Avalanche, Blockchain.AvalancheTestnet, + ).map { + BlockchainNetwork( + blockchain = it, + derivationStyleProvider = derivationStyleProvider, + ) + }, + ) } private suspend fun collectDerivations( diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect2/data/WalletConnectRepositoryImpl.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect2/data/WalletConnectRepositoryImpl.kt index da78bbdf84..ab1355fae2 100644 --- a/app/src/main/java/com/tangem/tap/domain/walletconnect2/data/WalletConnectRepositoryImpl.kt +++ b/app/src/main/java/com/tangem/tap/domain/walletconnect2/data/WalletConnectRepositoryImpl.kt @@ -251,7 +251,7 @@ class WalletConnectRepositoryImpl @Inject constructor( }.groupBy { pair -> pair.first } .mapValues { entry -> entry.value.map { pair -> pair.second }.toSet() } - val preparedNamespaces = sessionProposal.requiredNamespaces + val preparedRequiredNamespaces = sessionProposal.requiredNamespaces .map { requiredNamespace -> val accountsRequired = requiredNamespace.value.chains ?.mapNotNull { chain -> userChains[chain] } @@ -272,7 +272,9 @@ class WalletConnectRepositoryImpl @Inject constructor( val sessionApproval = Wallet.Params.SessionApprove( proposerPublicKey = sessionProposal.proposerPublicKey, - namespaces = preparedNamespaces, + namespaces = preparedRequiredNamespaces.ifEmpty { + sessionProposal.createPreparedOptionalNamespaces(userChains) + }, ) Timber.d("Session approval is prepared for sending: $sessionApproval") @@ -301,6 +303,25 @@ class WalletConnectRepositoryImpl @Inject constructor( ) } + private fun Wallet.Model.SessionProposal.createPreparedOptionalNamespaces( + userChains: Map>, + ): Map { + return optionalNamespaces + .map { optionalNamespace -> + val accountsOptional = optionalNamespace.value.chains + ?.mapNotNull { chain -> userChains[chain] } + ?.flatten() ?: emptyList() + + val methods = optionalNamespace.value.methods + optionalNamespace.key to Wallet.Model.Namespace.Session( + accounts = accountsOptional.distinct(), + methods = methods, + events = optionalNamespace.value.events, + ) + } + .toMap() + } + override fun sendRequest(requestData: RequestData, result: String) { val session = currentSessions.find { it.topic == requestData.topic } // Add Ethereum Chain method is processed without user input, skip logging it diff --git a/app/src/main/java/com/tangem/tap/features/customtoken/impl/domain/DefaultCustomTokenInteractor.kt b/app/src/main/java/com/tangem/tap/features/customtoken/impl/domain/DefaultCustomTokenInteractor.kt index 1f38373d86..fe1bb63d2a 100644 --- a/app/src/main/java/com/tangem/tap/features/customtoken/impl/domain/DefaultCustomTokenInteractor.kt +++ b/app/src/main/java/com/tangem/tap/features/customtoken/impl/domain/DefaultCustomTokenInteractor.kt @@ -1,5 +1,6 @@ package com.tangem.tap.features.customtoken.impl.domain +import com.tangem.blockchain.blockchains.cardano.CardanoUtils import com.tangem.blockchain.common.Blockchain import com.tangem.common.CompletionResult import com.tangem.common.card.EllipticCurve @@ -123,9 +124,15 @@ class DefaultCustomTokenInteractor( it.blockchain.getSupportedCurves().contains(curve) }.mapNotNull { it.derivationPath }.map { DerivationPath(it) } - val bothCandidates = (manageTokensCandidates + customTokensCandidates).distinct() + val bothCandidates = (manageTokensCandidates + customTokensCandidates).distinct().toMutableList() if (bothCandidates.isEmpty()) return null + currencyList.find { it is Currency.Blockchain && it.blockchain == Blockchain.Cardano }?.let { currency -> + currency.derivationPath?.let { + bothCandidates.add(CardanoUtils.extendedDerivationPath(DerivationPath(it))) + } + } + val mapKeyOfWalletPublicKey = wallet.publicKey.toMapKey() val alreadyDerivedKeys: ExtendedPublicKeysMap = scanResponse.derivedKeys[mapKeyOfWalletPublicKey] ?: ExtendedPublicKeysMap(emptyMap()) diff --git a/app/src/main/java/com/tangem/tap/features/demo/DemoHelper.kt b/app/src/main/java/com/tangem/tap/features/demo/DemoHelper.kt index fcf584cf31..1de357e9df 100644 --- a/app/src/main/java/com/tangem/tap/features/demo/DemoHelper.kt +++ b/app/src/main/java/com/tangem/tap/features/demo/DemoHelper.kt @@ -2,6 +2,7 @@ package com.tangem.tap.features.demo import com.tangem.domain.demo.DemoConfig import com.tangem.domain.models.scan.ScanResponse +import com.tangem.domain.tokens.legacy.TradeCryptoAction import com.tangem.tap.common.extensions.dispatchNotification import com.tangem.tap.common.redux.AppState import com.tangem.tap.features.details.redux.DetailsAction @@ -21,8 +22,8 @@ object DemoHelper { private val disabledActionFeatures = listOf( WalletConnectAction.StartWalletConnect::class.java, - WalletAction.TradeCryptoAction.Buy::class.java, - WalletAction.TradeCryptoAction.Sell::class.java, + TradeCryptoAction.Buy::class.java, + TradeCryptoAction.Sell::class.java, BackupAction.StartBackup::class.java, WalletAction.ExploreAddress::class.java, DetailsAction.ResetToFactory.Start::class.java, diff --git a/app/src/main/java/com/tangem/tap/features/disclaimer/ui/DisclaimerFragment.kt b/app/src/main/java/com/tangem/tap/features/disclaimer/ui/DisclaimerFragment.kt index ab1ec2715d..930f33afe6 100644 --- a/app/src/main/java/com/tangem/tap/features/disclaimer/ui/DisclaimerFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/disclaimer/ui/DisclaimerFragment.kt @@ -7,7 +7,7 @@ import android.webkit.WebView import androidx.transition.TransitionInflater import by.kirich1409.viewbindingdelegate.viewBinding import com.tangem.core.navigation.AppScreen -import com.tangem.core.ui.fragments.setStatusBarColor +import com.tangem.core.ui.extensions.setStatusBarColor import com.tangem.tap.common.extensions.beginDelayedTransition import com.tangem.tap.common.extensions.hide import com.tangem.tap.common.extensions.show diff --git a/app/src/main/java/com/tangem/tap/features/intentHandler/handlers/SellCurrencyIntentHandler.kt b/app/src/main/java/com/tangem/tap/features/intentHandler/handlers/SellCurrencyIntentHandler.kt index 9d28a45db6..2879f9ef13 100644 --- a/app/src/main/java/com/tangem/tap/features/intentHandler/handlers/SellCurrencyIntentHandler.kt +++ b/app/src/main/java/com/tangem/tap/features/intentHandler/handlers/SellCurrencyIntentHandler.kt @@ -1,9 +1,9 @@ package com.tangem.tap.features.intentHandler.handlers import android.content.Intent +import com.tangem.domain.tokens.legacy.TradeCryptoAction import com.tangem.tap.common.extensions.dispatchWithMain import com.tangem.tap.features.intentHandler.IntentHandler -import com.tangem.tap.features.wallet.redux.WalletAction import com.tangem.tap.store import timber.log.Timber @@ -22,7 +22,7 @@ class SellCurrencyIntentHandler : IntentHandler { Timber.d("MoonPay Sell: $amount $currency to $destinationAddress") store.dispatchWithMain( - WalletAction.TradeCryptoAction.SendCrypto( + TradeCryptoAction.SendCrypto( currencyId = currency, amount = amount, destinationAddress = destinationAddress, diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/ui/TwinsCardsFragment.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/ui/TwinsCardsFragment.kt index de763ef778..a4b12b434c 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/ui/TwinsCardsFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/ui/TwinsCardsFragment.kt @@ -16,7 +16,7 @@ import com.tangem.blockchain.common.Blockchain import com.tangem.common.extensions.VoidCallback import com.tangem.core.analytics.Analytics import com.tangem.core.navigation.ShareElement -import com.tangem.core.ui.fragments.setStatusBarColor +import com.tangem.core.ui.extensions.setStatusBarColor import com.tangem.datasource.asset.AssetReader import com.tangem.domain.common.TwinCardNumber import com.tangem.domain.models.scan.ScanResponse diff --git a/app/src/main/java/com/tangem/tap/features/saveWallet/ui/SaveWalletBottomSheetFragment.kt b/app/src/main/java/com/tangem/tap/features/saveWallet/ui/SaveWalletBottomSheetFragment.kt index 0a6a076bff..fe5792ad29 100644 --- a/app/src/main/java/com/tangem/tap/features/saveWallet/ui/SaveWalletBottomSheetFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/saveWallet/ui/SaveWalletBottomSheetFragment.kt @@ -6,24 +6,27 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.material.SnackbarHost import androidx.compose.material.SnackbarHostState -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.State -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberUpdatedState +import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.fragment.app.viewModels -import com.tangem.core.ui.fragments.ComposeBottomSheetFragment +import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.screen.ComposeBottomSheetFragment +import com.tangem.core.ui.theme.AppThemeModeHolder import com.tangem.tap.features.details.ui.cardsettings.resolveReference import com.tangem.tap.features.saveWallet.ui.components.EnrollBiometricsDialogContent import com.tangem.tap.features.saveWallet.ui.components.SaveWalletScreenContent import com.tangem.tap.features.saveWallet.ui.models.EnrollBiometricsDialog +import dagger.hilt.android.AndroidEntryPoint +import javax.inject.Inject + +@AndroidEntryPoint +internal class SaveWalletBottomSheetFragment : ComposeBottomSheetFragment() { + + @Inject + override lateinit var appThemeModeHolder: AppThemeModeHolder -internal class SaveWalletBottomSheetFragment : ComposeBottomSheetFragment() { override val expandedHeightFraction: Float = .98f private val viewModel by viewModels() @@ -34,12 +37,8 @@ internal class SaveWalletBottomSheetFragment : ComposeBottomSheetFragment { - return viewModel.state.collectAsState() - } - - @Composable - override fun ScreenContent(state: SaveWalletScreenState, modifier: Modifier) { + override fun ScreenContent(modifier: Modifier) { + val state by viewModel.state.collectAsStateWithLifecycle() val snackbarHostState = remember { SnackbarHostState() } val errorMessage by rememberUpdatedState(newValue = state.error?.resolveReference()) val enrollBiometricsDialog by rememberUpdatedState(newValue = state.enrollBiometricsDialog) diff --git a/app/src/main/java/com/tangem/tap/features/send/redux/middlewares/SendMiddleware.kt b/app/src/main/java/com/tangem/tap/features/send/redux/middlewares/SendMiddleware.kt index 4e4d53efc4..eeb7c0cf20 100644 --- a/app/src/main/java/com/tangem/tap/features/send/redux/middlewares/SendMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/send/redux/middlewares/SendMiddleware.kt @@ -19,6 +19,7 @@ import com.tangem.domain.common.TapWorkarounds.isStart2Coin import com.tangem.domain.common.extensions.minimalAmount import com.tangem.domain.common.extensions.withMainContext import com.tangem.domain.models.scan.CardDTO +import com.tangem.domain.tokens.legacy.TradeCryptoAction import com.tangem.tap.* import com.tangem.tap.common.analytics.events.AnalyticsParam import com.tangem.tap.common.analytics.events.Basic @@ -38,7 +39,6 @@ import com.tangem.tap.features.send.redux.* import com.tangem.tap.features.send.redux.FeeAction.RequestFee import com.tangem.tap.features.send.redux.states.* import com.tangem.tap.features.wallet.models.Currency -import com.tangem.tap.features.wallet.redux.WalletAction import com.tangem.tap.proxy.redux.DaggerGraphState import com.tangem.wallet.R import kotlinx.coroutines.Dispatchers @@ -254,7 +254,7 @@ private fun sendTransaction( ), ) Analytics.sendSelectedCurrencyEvent(mainCurrencyType) - dispatch(WalletAction.TradeCryptoAction.FinishSelling(externalTransactionData.transactionId)) + dispatch(TradeCryptoAction.FinishSelling(externalTransactionData.transactionId)) } else { Analytics.send( Basic.TransactionSent( diff --git a/app/src/main/java/com/tangem/tap/features/send/ui/SendFragment.kt b/app/src/main/java/com/tangem/tap/features/send/ui/SendFragment.kt index 748ecacb4b..0e963d3ac5 100644 --- a/app/src/main/java/com/tangem/tap/features/send/ui/SendFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/send/ui/SendFragment.kt @@ -18,6 +18,7 @@ import com.google.android.material.textfield.TextInputEditText import com.tangem.Message import com.tangem.core.analytics.Analytics import com.tangem.core.navigation.NavigationAction +import com.tangem.domain.tokens.legacy.TradeCryptoAction import com.tangem.sdk.extensions.hideSoftKeyboard import com.tangem.tap.common.KeyboardObserver import com.tangem.tap.common.analytics.events.Token @@ -39,7 +40,6 @@ import com.tangem.tap.features.send.redux.FeeActionUi.* import com.tangem.tap.features.send.redux.states.FeeType import com.tangem.tap.features.send.redux.states.MainCurrencyType import com.tangem.tap.features.send.ui.stateSubscribers.SendStateSubscriber -import com.tangem.tap.features.wallet.redux.WalletAction import com.tangem.tap.features.wallet.ui.adapters.WarningMessagesAdapter import com.tangem.tap.mainScope import com.tangem.tap.store @@ -337,9 +337,7 @@ class SendFragment : BaseStoreFragment(R.layout.fragment_send) { if (externalTransactionData == null) { store.dispatch(NavigationAction.PopBackTo()) } else { - store.dispatch( - WalletAction.TradeCryptoAction.FinishSelling(externalTransactionData.transactionId), - ) + store.dispatch(TradeCryptoAction.FinishSelling(externalTransactionData.transactionId)) } } diff --git a/app/src/main/java/com/tangem/tap/features/sprinklr/ui/SprinklrActivity.kt b/app/src/main/java/com/tangem/tap/features/sprinklr/ui/SprinklrActivity.kt index b567dce03b..7ddbf49c68 100644 --- a/app/src/main/java/com/tangem/tap/features/sprinklr/ui/SprinklrActivity.kt +++ b/app/src/main/java/com/tangem/tap/features/sprinklr/ui/SprinklrActivity.kt @@ -6,14 +6,24 @@ import androidx.activity.viewModels import androidx.compose.foundation.layout.imePadding import androidx.compose.foundation.layout.systemBarsPadding import androidx.compose.runtime.Composable -import androidx.compose.runtime.State -import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier +import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.tangem.core.analytics.Analytics -import com.tangem.core.ui.fragments.ComposeActivity +import com.tangem.core.ui.components.SystemBarsEffect +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.screen.ComposeActivity +import com.tangem.core.ui.theme.AppThemeModeHolder import com.tangem.tap.common.analytics.events.Chat +import dagger.hilt.android.AndroidEntryPoint +import javax.inject.Inject + +@AndroidEntryPoint +internal class SprinklrActivity : ComposeActivity() { + + @Inject + override lateinit var appThemeModeHolder: AppThemeModeHolder -internal class SprinklrActivity : ComposeActivity() { private val viewModel by viewModels() override fun onCreate(savedInstanceState: Bundle?) { @@ -25,12 +35,14 @@ internal class SprinklrActivity : ComposeActivity() { } @Composable - override fun provideState(): State { - return viewModel.state.collectAsState() - } + override fun ScreenContent(modifier: Modifier) { + val state by viewModel.state.collectAsStateWithLifecycle() + + val systemBarsColor = TangemTheme.colors.background.primary + SystemBarsEffect { + setSystemBarsColor(systemBarsColor) + } - @Composable - override fun ScreenContent(state: SprinklrScreenState, modifier: Modifier) { BackHandler(onBack = state.onNavigateBack) SprinklrScreenContent( modifier = modifier diff --git a/app/src/main/java/com/tangem/tap/features/tokens/impl/domain/DefaultTokensListInteractor.kt b/app/src/main/java/com/tangem/tap/features/tokens/impl/domain/DefaultTokensListInteractor.kt index c61c1a2f7e..9610727483 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/impl/domain/DefaultTokensListInteractor.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/impl/domain/DefaultTokensListInteractor.kt @@ -1,6 +1,7 @@ package com.tangem.tap.features.tokens.impl.domain import androidx.paging.PagingData +import com.tangem.blockchain.blockchains.cardano.CardanoUtils import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.derivation.DerivationStyle import com.tangem.common.CompletionResult @@ -190,9 +191,15 @@ internal class DefaultTokensListInteractor( .mapNotNull(Currency::derivationPath) .map(::DerivationPath) - val bothCandidates = (manageTokensCandidates + customTokensCandidates).distinct() + val bothCandidates = (manageTokensCandidates + customTokensCandidates).distinct().toMutableList() if (bothCandidates.isEmpty()) return null + currencyList.find { it is Currency.Blockchain && it.blockchain == Blockchain.Cardano }?.let { currency -> + currency.derivationPath?.let { + bothCandidates.add(CardanoUtils.extendedDerivationPath(DerivationPath(it))) + } + } + val mapKeyOfWalletPublicKey = wallet.publicKey.toMapKey() val alreadyDerivedKeys = scanResponse.derivedKeys[mapKeyOfWalletPublicKey] ?: ExtendedPublicKeysMap(emptyMap()) val alreadyDerivedPaths = alreadyDerivedKeys.keys.toList() diff --git a/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/ui/TokensListScreen.kt b/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/ui/TokensListScreen.kt index 174563c6be..ea6ab16083 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/ui/TokensListScreen.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/ui/TokensListScreen.kt @@ -7,17 +7,14 @@ import androidx.compose.animation.fadeOut import androidx.compose.foundation.background import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.CircularProgressIndicator import androidx.compose.material.FabPosition import androidx.compose.material.Scaffold import androidx.compose.material.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue +import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier @@ -32,9 +29,7 @@ import androidx.compose.ui.unit.TextUnit import androidx.compose.ui.unit.TextUnitType import androidx.compose.ui.unit.dp import androidx.paging.PagingData -import androidx.paging.compose.LazyPagingItems -import androidx.paging.compose.collectAsLazyPagingItems -import androidx.paging.compose.items +import androidx.paging.compose.* import com.tangem.core.ui.components.PrimaryButton import com.tangem.core.ui.res.TangemColorPalette import com.tangem.core.ui.res.TangemTheme @@ -134,8 +129,11 @@ private fun TokensListContent( item { DifferentAddressesWarning() } } - items(items = tokens, key = TokenItemState::composedId) { - it?.let { TokenItem(model = it) } + tokens.itemKey(TokenItemState::composedId) + tokens.itemContentType(TokenItemState::composedId) + + items(items = tokens.itemSnapshotList.items, key = TokenItemState::composedId) { + TokenItem(model = it) } } } diff --git a/app/src/main/java/com/tangem/tap/features/tokens/legacy/redux/TokensMiddleware.kt b/app/src/main/java/com/tangem/tap/features/tokens/legacy/redux/TokensMiddleware.kt index 67bbf74043..1a5eb1fd45 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/legacy/redux/TokensMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/legacy/redux/TokensMiddleware.kt @@ -1,5 +1,6 @@ package com.tangem.tap.features.tokens.legacy.redux +import com.tangem.blockchain.blockchains.cardano.CardanoUtils import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.derivation.DerivationStyle import com.tangem.common.CompletionResult @@ -179,9 +180,15 @@ object TokensMiddleware { it.blockchain.getSupportedCurves().contains(curve) }.mapNotNull { it.derivationPath }.map { DerivationPath(it) } - val bothCandidates = (manageTokensCandidates + customTokensCandidates).distinct() + val bothCandidates = (manageTokensCandidates + customTokensCandidates).distinct().toMutableList() if (bothCandidates.isEmpty()) return null + currencyList.find { it is Currency.Blockchain && it.blockchain == Blockchain.Cardano }?.let { currency -> + currency.derivationPath?.let { + bothCandidates.add(CardanoUtils.extendedDerivationPath(DerivationPath(it))) + } + } + val mapKeyOfWalletPublicKey = wallet.publicKey.toMapKey() val alreadyDerivedKeys: ExtendedPublicKeysMap = scanResponse.derivedKeys[mapKeyOfWalletPublicKey] ?: ExtendedPublicKeysMap(emptyMap()) diff --git a/app/src/main/java/com/tangem/tap/features/wallet/converters/CryptoCurrencyConverter.kt b/app/src/main/java/com/tangem/tap/features/wallet/converters/CryptoCurrencyConverter.kt index 1cf1515b01..8b22dd95e6 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/converters/CryptoCurrencyConverter.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/converters/CryptoCurrencyConverter.kt @@ -7,7 +7,7 @@ import com.tangem.tap.features.wallet.models.Currency import com.tangem.tap.store import com.tangem.utils.converter.Converter -class CryptoCurrencyConverter : Converter { +internal class CryptoCurrencyConverter : Converter { private val cryptoCurrencyFactory by lazy { CryptoCurrencyFactory() } diff --git a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/TradeCryptoMiddleware.kt b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/TradeCryptoMiddleware.kt index 65be537ac8..2528753b4e 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/TradeCryptoMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/TradeCryptoMiddleware.kt @@ -4,12 +4,16 @@ import androidx.core.os.bundleOf import com.google.firebase.crashlytics.FirebaseCrashlytics import com.tangem.blockchain.blockchains.ethereum.EthereumWalletManager import com.tangem.blockchain.common.AmountType +import com.tangem.blockchain.common.Blockchain import com.tangem.common.extensions.guard import com.tangem.core.analytics.Analytics import com.tangem.core.navigation.AppScreen import com.tangem.core.navigation.NavigationAction import com.tangem.domain.common.extensions.toCoinId import com.tangem.domain.common.extensions.toNetworkId +import com.tangem.domain.common.util.derivationStyleProvider +import com.tangem.domain.tokens.legacy.TradeCryptoAction +import com.tangem.domain.tokens.models.CryptoCurrency import com.tangem.feature.swap.presentation.SwapFragment import com.tangem.tap.common.analytics.events.AnalyticsParam import com.tangem.tap.common.analytics.events.Token @@ -27,6 +31,7 @@ import com.tangem.tap.features.wallet.redux.WalletAction import com.tangem.tap.features.wallet.redux.WalletState import com.tangem.tap.network.exchangeServices.CurrencyExchangeManager import com.tangem.tap.network.exchangeServices.buyErc20TestnetTokens +import com.tangem.tap.proxy.redux.DaggerGraphState import com.tangem.tap.scope import com.tangem.tap.store import kotlinx.coroutines.launch @@ -35,19 +40,27 @@ import kotlinx.serialization.json.Json import com.tangem.feature.swap.domain.models.domain.Currency as SwapCurrency class TradeCryptoMiddleware { - fun handle(state: () -> AppState?, action: WalletAction.TradeCryptoAction) { + fun handle(state: () -> AppState?, action: TradeCryptoAction) { if (DemoHelper.tryHandle(state, action)) return when (action) { - is WalletAction.TradeCryptoAction.Buy -> proceedBuyAction(state, action) - is WalletAction.TradeCryptoAction.Sell -> proceedSellAction() - is WalletAction.TradeCryptoAction.SendCrypto -> preconfigureAndOpenSendScreen(action) - is WalletAction.TradeCryptoAction.FinishSelling -> openReceiptUrl(action.transactionId) - is WalletAction.TradeCryptoAction.Swap -> openSwap() + is TradeCryptoAction.Buy -> proceedBuyAction(state, action) + is TradeCryptoAction.Sell -> proceedSellAction() + is TradeCryptoAction.SendCrypto -> preconfigureAndOpenSendScreen(action) + is TradeCryptoAction.FinishSelling -> openReceiptUrl(action.transactionId) + is TradeCryptoAction.Swap -> { + openSwap(currency = store.state.walletState.selectedWalletData?.currency?.toSwapCurrency()) + } + is TradeCryptoAction.New.Buy -> proceedNewBuyAction(state, action) + TradeCryptoAction.New.Send -> store.dispatch(WalletAction.Send()) + is TradeCryptoAction.New.Sell -> proceedNewSellAction(action) + is TradeCryptoAction.New.Swap -> { + openSwap(currency = action.cryptoCurrency.toSwapCurrency()) + } } } - private fun proceedBuyAction(state: () -> AppState?, action: WalletAction.TradeCryptoAction.Buy) { + private fun proceedBuyAction(state: () -> AppState?, action: TradeCryptoAction.Buy) { val selectedWalletData = store.state.walletState.selectedWalletData ?: return val currency = chooseAppropriateCurrency(store.state.walletState) ?: return @@ -75,7 +88,7 @@ class TradeCryptoMiddleware { buyErc20TestnetTokens( card = card, walletManager = walletManager, - token = currency.token, + destinationAddress = currency.token.contractAddress, ) } return @@ -93,6 +106,56 @@ class TradeCryptoMiddleware { } } + private fun proceedNewBuyAction(state: () -> AppState?, action: TradeCryptoAction.New.Buy) { + val networkAddress = action.cryptoCurrencyStatus.value.networkAddress?.defaultAddress ?: return + + if (action.checkUserLocation && state()?.globalState?.userCountryCode == RUSSIA_COUNTRY_CODE) { + store.dispatchOnMain(WalletAction.DialogAction.RussianCardholdersWarningDialog()) + return + } + + val status = action.cryptoCurrencyStatus + val currency = status.currency + val blockchain = Blockchain.fromId(currency.network.id.value) + if (currency is CryptoCurrency.Token && currency.network.isTestnet) { + scope.launch { + val walletManager = store.state.daggerGraphState + .get(DaggerGraphState::walletManagersFacade) + .getOrCreateWalletManager( + userWallet = action.userWallet, + blockchain = blockchain, + derivationPath = blockchain.derivationPath( + style = action.userWallet.scanResponse.derivationStyleProvider.getDerivationStyle(), + ), + ) + + if (walletManager !is EthereumWalletManager) { + store.dispatchDebugErrorNotification("Testnet tokens available only for the Ethereum") + return@launch + } + + buyErc20TestnetTokens( + card = action.userWallet.scanResponse.card, + walletManager = walletManager, + destinationAddress = currency.contractAddress, + ) + } + return + } + + val exchangeManager = store.state.globalState.exchangeManager + exchangeManager.getUrl( + action = CurrencyExchangeManager.Action.Buy, + blockchain = blockchain, + cryptoCurrencyName = currency.symbol, + fiatCurrencyName = action.appCurrencyCode, + walletAddress = networkAddress, + )?.let { + store.dispatchOpenUrl(it) + Analytics.send(Token.Topup.ScreenOpened()) + } + } + private fun proceedSellAction() { val selectedWalletData = store.state.walletState.selectedWalletData ?: return val currency = chooseAppropriateCurrency(store.state.walletState) ?: return @@ -115,6 +178,22 @@ class TradeCryptoMiddleware { } } + private fun proceedNewSellAction(action: TradeCryptoAction.New.Sell) { + val networkAddress = action.cryptoCurrencyStatus.value.networkAddress?.defaultAddress ?: return + val currency = action.cryptoCurrencyStatus.currency + + store.state.globalState.exchangeManager.getUrl( + action = CurrencyExchangeManager.Action.Sell, + blockchain = Blockchain.fromId(currency.network.id.value), + cryptoCurrencyName = currency.symbol, + fiatCurrencyName = action.appCurrencyCode, + walletAddress = networkAddress, + )?.let { + store.dispatchOpenUrl(it) + Analytics.send(Token.Withdraw.ScreenOpened()) + } + } + private fun chooseAppropriateCurrency(walletState: WalletState): Currency? { return if (walletState.primaryTokenData == null) { walletState.selectedWalletData?.currency @@ -126,7 +205,7 @@ class TradeCryptoMiddleware { } } - private fun preconfigureAndOpenSendScreen(action: WalletAction.TradeCryptoAction.SendCrypto) { + private fun preconfigureAndOpenSendScreen(action: TradeCryptoAction.SendCrypto) { val selectedWalletData = store.state.walletState.selectedWalletData ?: return Analytics.send(Token.ButtonSend(AnalyticsParam.CurrencyType.Currency(selectedWalletData.currency))) @@ -160,16 +239,44 @@ class TradeCryptoMiddleware { )?.let { store.dispatchOpenUrl(it) } } - private fun openSwap() { - val currency = store.state.walletState.selectedWalletData?.currency?.toSwapCurrency() - val bundle = - bundleOf( - SwapFragment.CURRENCY_BUNDLE_KEY to Json.encodeToString(currency), - SwapFragment.DERIVATION_PATH to store.state.walletState.selectedWalletData?.currency?.derivationPath, - ) + private fun openSwap(currency: SwapCurrency?) { + val bundle = bundleOf( + SwapFragment.CURRENCY_BUNDLE_KEY to Json.encodeToString(currency), + SwapFragment.DERIVATION_PATH to store.state.walletState.selectedWalletData?.currency?.derivationPath, + ) + store.dispatchOnMain(NavigationAction.NavigateTo(screen = AppScreen.Swap, bundle = bundle)) } + private fun CryptoCurrency.toSwapCurrency(): SwapCurrency { + val blockchain = Blockchain.fromId(network.id.value) + + return when (this) { + is CryptoCurrency.Coin -> { + SwapCurrency.NativeToken( + id = blockchain.toCoinId(), + name = name, + symbol = symbol, + networkId = blockchain.toNetworkId(), + // no need to set logoUrl for blockchain cause + // error when form url with coinId, coinId of eth and arbitrum the same + logoUrl = "", + ) + } + is CryptoCurrency.Token -> { + SwapCurrency.NonNativeToken( + id = id.value, + name = name, + symbol = symbol, + networkId = blockchain.toNetworkId(), + logoUrl = getIconUrl(id.value), + contractAddress = contractAddress, + decimalCount = decimals, + ) + } + } + } + private fun Currency.toSwapCurrency(): SwapCurrency { return when (this) { is Currency.Blockchain -> { diff --git a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/WalletMiddleware.kt b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/WalletMiddleware.kt index 2072690c1c..889dab19e8 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/WalletMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/WalletMiddleware.kt @@ -13,6 +13,7 @@ import com.tangem.core.analytics.Analytics import com.tangem.core.navigation.AppScreen import com.tangem.core.navigation.NavigationAction import com.tangem.datasource.connection.NetworkConnectionManager +import com.tangem.domain.tokens.legacy.TradeCryptoAction import com.tangem.domain.userwallets.GetCardImageUseCase import com.tangem.domain.wallets.legacy.lockIfLockable import com.tangem.tap.* @@ -88,7 +89,7 @@ class WalletMiddleware { val walletState = store.state.walletState when (action) { - is WalletAction.TradeCryptoAction -> tradeCryptoMiddleware.handle(state, action) + is TradeCryptoAction -> tradeCryptoMiddleware.handle(state, action) is WalletAction.Warnings -> warningsMiddleware.handle(action, globalState) is WalletAction.MultiWallet -> multiWalletMiddleware.handle(action, walletState) is WalletAction.AppCurrencyAction -> appCurrencyMiddleware.handle(action) diff --git a/app/src/main/java/com/tangem/tap/features/wallet/redux/reducers/WalletReducer.kt b/app/src/main/java/com/tangem/tap/features/wallet/redux/reducers/WalletReducer.kt index e91e0a1194..3fb8a17540 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/redux/reducers/WalletReducer.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/redux/reducers/WalletReducer.kt @@ -68,7 +68,6 @@ private fun internalReduce(action: Action, state: AppState, appStateHolder: AppS error = null, ) } - is WalletAction.TradeCryptoAction -> return newState is WalletAction.AppCurrencyAction -> { newState = appCurrencyReducer.reduce(action, newState) } diff --git a/app/src/main/java/com/tangem/tap/features/wallet/ui/WalletDetailsFragment.kt b/app/src/main/java/com/tangem/tap/features/wallet/ui/WalletDetailsFragment.kt index 64105fe887..fe767bafda 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/ui/WalletDetailsFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/ui/WalletDetailsFragment.kt @@ -21,6 +21,7 @@ import com.tangem.core.analytics.Analytics import com.tangem.core.navigation.NavigationAction import com.tangem.domain.common.extensions.withMainContext import com.tangem.domain.common.util.derivationStyleProvider +import com.tangem.domain.tokens.legacy.TradeCryptoAction import com.tangem.feature.swap.api.SwapFeatureToggleManager import com.tangem.feature.swap.domain.SwapInteractor import com.tangem.sdk.extensions.dpToPx @@ -276,9 +277,9 @@ class WalletDetailsFragment : Fragment(R.layout.fragment_wallet_details), SafeSt ) { val exchangeManager = store.state.globalState.exchangeManager binding.rowButtons.apply { - onBuyClick = { store.dispatch(WalletAction.TradeCryptoAction.Buy()) } - onSellClick = { store.dispatch(WalletAction.TradeCryptoAction.Sell) } - onSwapClick = { store.dispatch(WalletAction.TradeCryptoAction.Swap) } + onBuyClick = { store.dispatch(TradeCryptoAction.Buy()) } + onSellClick = { store.dispatch(TradeCryptoAction.Sell) } + onSwapClick = { store.dispatch(TradeCryptoAction.Swap) } onTradeClick = { store.dispatch( WalletAction.DialogAction.ChooseTradeActionDialog( diff --git a/app/src/main/java/com/tangem/tap/features/wallet/ui/WalletFragment.kt b/app/src/main/java/com/tangem/tap/features/wallet/ui/WalletFragment.kt index e96fa22bb3..d049f046a3 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/ui/WalletFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/ui/WalletFragment.kt @@ -23,7 +23,7 @@ import com.badoo.mvicore.modelWatcher import com.tangem.core.analytics.Analytics import com.tangem.core.navigation.AppScreen import com.tangem.core.navigation.NavigationAction -import com.tangem.core.ui.fragments.setStatusBarColor +import com.tangem.core.ui.extensions.setStatusBarColor import com.tangem.core.ui.utils.OneTouchClickListener import com.tangem.datasource.connection.NetworkConnectionManager import com.tangem.feature.swap.api.SwapFeatureToggleManager diff --git a/app/src/main/java/com/tangem/tap/features/wallet/ui/dialogs/ChooseTradeActionBottomSheetDialog.kt b/app/src/main/java/com/tangem/tap/features/wallet/ui/dialogs/ChooseTradeActionBottomSheetDialog.kt index 1495219c54..25ce55e44c 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/ui/dialogs/ChooseTradeActionBottomSheetDialog.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/ui/dialogs/ChooseTradeActionBottomSheetDialog.kt @@ -4,6 +4,7 @@ import android.content.Context import android.os.Bundle import android.view.LayoutInflater import com.google.android.material.bottomsheet.BottomSheetDialog +import com.tangem.domain.tokens.legacy.TradeCryptoAction import com.tangem.tap.common.extensions.show import com.tangem.tap.features.wallet.redux.WalletAction import com.tangem.tap.features.wallet.redux.models.WalletDialog @@ -39,15 +40,15 @@ class ChooseTradeActionBottomSheetDialog( dialogBtnBuy.setOnClickListener { dismiss() - store.dispatch(WalletAction.TradeCryptoAction.Buy()) + store.dispatch(TradeCryptoAction.Buy()) } dialogBtnSell.setOnClickListener { dismiss() - store.dispatch(WalletAction.TradeCryptoAction.Sell) + store.dispatch(TradeCryptoAction.Sell) } dialogBtnSwap.setOnClickListener { dismiss() - store.dispatch(WalletAction.TradeCryptoAction.Swap) + store.dispatch(TradeCryptoAction.Swap) } } } diff --git a/app/src/main/java/com/tangem/tap/features/wallet/ui/dialogs/RussianCardholdersWarningBottomSheetDialog.kt b/app/src/main/java/com/tangem/tap/features/wallet/ui/dialogs/RussianCardholdersWarningBottomSheetDialog.kt index b7ec4ee4a1..185c5e366a 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/ui/dialogs/RussianCardholdersWarningBottomSheetDialog.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/ui/dialogs/RussianCardholdersWarningBottomSheetDialog.kt @@ -6,10 +6,10 @@ import android.view.LayoutInflater import com.google.android.material.bottomsheet.BottomSheetDialog import com.tangem.core.analytics.Analytics import com.tangem.core.navigation.NavigationAction +import com.tangem.domain.tokens.legacy.TradeCryptoAction import com.tangem.tap.common.analytics.events.Token import com.tangem.tap.common.extensions.dispatchDialogHide import com.tangem.tap.common.extensions.dispatchOpenUrl -import com.tangem.tap.features.wallet.redux.WalletAction import com.tangem.tap.features.wallet.redux.models.WalletDialog import com.tangem.tap.store import com.tangem.wallet.databinding.DialogRussiansCardholdersWarningBinding @@ -40,7 +40,7 @@ class RussianCardholdersWarningBottomSheetDialog( if (dialogData != null) { store.dispatchOpenUrl(dialogData.topUpUrl) } else { - store.dispatch(WalletAction.TradeCryptoAction.Buy(checkUserLocation = false)) + store.dispatch(TradeCryptoAction.Buy(checkUserLocation = false)) } dismiss() } diff --git a/app/src/main/java/com/tangem/tap/features/wallet/ui/wallet/SingleWalletView.kt b/app/src/main/java/com/tangem/tap/features/wallet/ui/wallet/SingleWalletView.kt index 23a4645bb2..7ce1a4d49e 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/ui/wallet/SingleWalletView.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/ui/wallet/SingleWalletView.kt @@ -4,6 +4,7 @@ import android.view.View import android.view.ViewGroup import androidx.recyclerview.widget.LinearLayoutManager import com.tangem.core.analytics.Analytics +import com.tangem.domain.tokens.legacy.TradeCryptoAction import com.tangem.tap.common.analytics.events.Token import com.tangem.tap.common.extensions.* import com.tangem.tap.domain.model.WalletDataModel @@ -151,9 +152,9 @@ class SingleWalletView : WalletView() { val exchangeManager = store.state.globalState.exchangeManager binding?.rowButtons?.apply { - onBuyClick = { store.dispatch(WalletAction.TradeCryptoAction.Buy()) } - onSellClick = { store.dispatch(WalletAction.TradeCryptoAction.Sell) } - onSwapClick = { store.dispatch(WalletAction.TradeCryptoAction.Swap) } + onBuyClick = { store.dispatch(TradeCryptoAction.Buy()) } + onSellClick = { store.dispatch(TradeCryptoAction.Sell) } + onSwapClick = { store.dispatch(TradeCryptoAction.Swap) } onTradeClick = { store.dispatch( WalletAction.DialogAction.ChooseTradeActionDialog( diff --git a/app/src/main/java/com/tangem/tap/features/walletSelector/ui/WalletSelectorBottomSheetFragment.kt b/app/src/main/java/com/tangem/tap/features/walletSelector/ui/WalletSelectorBottomSheetFragment.kt index ecb5291d26..2e80036cd4 100644 --- a/app/src/main/java/com/tangem/tap/features/walletSelector/ui/WalletSelectorBottomSheetFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/walletSelector/ui/WalletSelectorBottomSheetFragment.kt @@ -7,35 +7,32 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.material.SnackbarHost import androidx.compose.material.SnackbarHostState -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.State -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberUpdatedState +import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.platform.rememberNestedScrollInteropConnection import androidx.fragment.app.viewModels +import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.tangem.core.analytics.Analytics -import com.tangem.core.ui.fragments.ComposeBottomSheetFragment +import com.tangem.core.ui.components.wallets.RenameWalletDialogContent import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.screen.ComposeBottomSheetFragment +import com.tangem.core.ui.theme.AppThemeModeHolder import com.tangem.tap.common.analytics.events.MyWallets import com.tangem.tap.features.details.ui.cardsettings.resolveReference -import com.tangem.tap.features.walletSelector.ui.components.BiometricsDisabledWarningContent -import com.tangem.tap.features.walletSelector.ui.components.BiometricsLockoutWarningContent -import com.tangem.tap.features.walletSelector.ui.components.KeyInvalidatedWarningContent -import com.tangem.tap.features.walletSelector.ui.components.RemoveWalletDialogContent -import com.tangem.tap.features.walletSelector.ui.components.RenameWalletDialogContent -import com.tangem.tap.features.walletSelector.ui.components.WalletSelectorScreenContent +import com.tangem.tap.features.walletSelector.ui.components.* import com.tangem.tap.features.walletSelector.ui.model.DialogModel import com.tangem.tap.features.walletSelector.ui.model.WarningModel import dagger.hilt.android.AndroidEntryPoint +import javax.inject.Inject @AndroidEntryPoint -internal class WalletSelectorBottomSheetFragment : ComposeBottomSheetFragment() { +internal class WalletSelectorBottomSheetFragment : ComposeBottomSheetFragment() { + + @Inject + override lateinit var appThemeModeHolder: AppThemeModeHolder + private val viewModel by viewModels() override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { @@ -45,10 +42,8 @@ internal class WalletSelectorBottomSheetFragment : ComposeBottomSheetFragment = viewModel.state.collectAsState() - - @Composable - override fun ScreenContent(state: WalletSelectorScreenState, modifier: Modifier) { + override fun ScreenContent(modifier: Modifier) { + val state by viewModel.state.collectAsStateWithLifecycle() val snackbarHostState = remember { SnackbarHostState() } val errorMessage by rememberUpdatedState(newValue = state.error?.resolveReference()) val dialog by rememberUpdatedState(newValue = state.dialog) @@ -90,7 +85,13 @@ internal class WalletSelectorBottomSheetFragment : ComposeBottomSheetFragment RemoveWalletDialogContent(dialog) - is DialogModel.RenameWalletDialog -> RenameWalletDialogContent(dialog) + is DialogModel.RenameWalletDialog -> { + RenameWalletDialogContent( + name = dialog.currentName, + onConfirm = dialog.onConfirm, + onDismiss = dialog.onDismiss, + ) + } is WarningModel.BiometricsLockoutWarning -> BiometricsLockoutWarningContent(dialog) is WarningModel.KeyInvalidatedWarning -> KeyInvalidatedWarningContent(dialog) is WarningModel.BiometricsDisabledWarning -> BiometricsDisabledWarningContent(dialog) diff --git a/app/src/main/java/com/tangem/tap/features/walletSelector/ui/components/RenameWalletDialogContent.kt b/app/src/main/java/com/tangem/tap/features/walletSelector/ui/components/RenameWalletDialogContent.kt deleted file mode 100644 index 7bc58f301a..0000000000 --- a/app/src/main/java/com/tangem/tap/features/walletSelector/ui/components/RenameWalletDialogContent.kt +++ /dev/null @@ -1,69 +0,0 @@ -package com.tangem.tap.features.walletSelector.ui.components - -import androidx.compose.foundation.layout.Column -import androidx.compose.runtime.* -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.input.TextFieldValue -import androidx.compose.ui.tooling.preview.Preview -import com.tangem.core.ui.components.AdditionalTextInputDialogParams -import com.tangem.core.ui.components.DialogButton -import com.tangem.core.ui.components.TextInputDialog -import com.tangem.core.ui.res.TangemTheme -import com.tangem.tap.features.walletSelector.ui.model.DialogModel -import com.tangem.wallet.R - -@Composable -internal fun RenameWalletDialogContent(dialog: DialogModel.RenameWalletDialog) { - var value by remember { - mutableStateOf(TextFieldValue(text = dialog.currentName)) - } - - TextInputDialog( - fieldValue = value, - confirmButton = DialogButton( - title = stringResource(id = R.string.common_ok), - enabled = value.text.isNotEmpty() && value.text != dialog.currentName, - onClick = { dialog.onConfirm(value.text) }, - ), - onDismissDialog = dialog.onDismiss, - onValueChange = { newValue -> - value = newValue - }, - title = stringResource(R.string.user_wallet_list_rename_popup_title), - dismissButton = DialogButton( - title = stringResource(id = R.string.common_cancel), - onClick = dialog.onDismiss, - ), - textFieldParams = AdditionalTextInputDialogParams( - label = stringResource(R.string.user_wallet_list_rename_popup_placeholder), - ), - ) -} - -// region Preview -@Composable -private fun RenameWalletDialogContentSample(modifier: Modifier = Modifier) { - Column( - modifier = modifier, - ) { - RenameWalletDialogContent(dialog = DialogModel.RenameWalletDialog("", {}, {})) - } -} - -@Preview(showBackground = true, widthDp = 360) -@Composable -private fun RenameWalletDialogContentPreview_Light() { - TangemTheme { - RenameWalletDialogContentSample() - } -} - -@Preview(showBackground = true, widthDp = 360) -@Composable -private fun RenameWalletDialogContentPreview_Dark() { - TangemTheme(isDark = true) { - RenameWalletDialogContentSample() - } -} -// endregion Preview \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/welcome/ui/WelcomeFragment.kt b/app/src/main/java/com/tangem/tap/features/welcome/ui/WelcomeFragment.kt index 0713e33482..f3d9cd27f1 100644 --- a/app/src/main/java/com/tangem/tap/features/welcome/ui/WelcomeFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/welcome/ui/WelcomeFragment.kt @@ -1,6 +1,7 @@ package com.tangem.tap.features.welcome.ui import androidx.activity.compose.BackHandler +import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding @@ -10,21 +11,27 @@ import androidx.compose.material.SnackbarHostState import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.res.colorResource import androidx.fragment.app.viewModels +import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.lifecycleScope import com.tangem.core.analytics.Analytics import com.tangem.core.ui.components.SystemBarsEffect -import com.tangem.core.ui.fragments.ComposeFragment import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.screen.ComposeFragment +import com.tangem.core.ui.theme.AppThemeModeHolder import com.tangem.tap.common.analytics.events.SignIn import com.tangem.tap.common.extensions.eraseContext import com.tangem.tap.features.details.ui.cardsettings.resolveReference import com.tangem.tap.features.welcome.ui.components.WarningDialog import com.tangem.tap.features.welcome.ui.components.WelcomeScreenContent -import com.tangem.wallet.R +import dagger.hilt.android.AndroidEntryPoint +import javax.inject.Inject -internal class WelcomeFragment : ComposeFragment() { +@AndroidEntryPoint +internal class WelcomeFragment : ComposeFragment() { + + @Inject + override lateinit var appThemeModeHolder: AppThemeModeHolder private val viewModel by viewModels() @@ -35,19 +42,15 @@ internal class WelcomeFragment : ComposeFragment() { } @Composable - override fun provideState(): State { - return viewModel.state.collectAsState() - } - - @Composable - override fun ScreenContent(state: WelcomeScreenState, modifier: Modifier) { + override fun ScreenContent(modifier: Modifier) { + val state by viewModel.state.collectAsStateWithLifecycle() val snackbarHostState = remember { SnackbarHostState() } val errorMessage by rememberUpdatedState(newValue = state.error?.resolveReference()) val warning by rememberUpdatedState(newValue = state.warning) - val backgroundColor = colorResource(id = R.color.background_primary) + val backgroundColor = TangemTheme.colors.background.primary SystemBarsEffect { - setSystemBarsColor(color = backgroundColor) + setSystemBarsColor(backgroundColor) } BackHandler { @@ -56,7 +59,8 @@ internal class WelcomeFragment : ComposeFragment() { Box( modifier = modifier - .systemBarsPadding(), + .systemBarsPadding() + .background(backgroundColor), ) { WelcomeScreenContent( showUnlockProgress = state.showUnlockWithBiometricsProgress, diff --git a/app/src/main/java/com/tangem/tap/network/exchangeServices/CurrencyExchangeManager.kt b/app/src/main/java/com/tangem/tap/network/exchangeServices/CurrencyExchangeManager.kt index d3294b1ed2..ad03f2c04a 100644 --- a/app/src/main/java/com/tangem/tap/network/exchangeServices/CurrencyExchangeManager.kt +++ b/app/src/main/java/com/tangem/tap/network/exchangeServices/CurrencyExchangeManager.kt @@ -5,7 +5,6 @@ import com.tangem.blockchain.blockchains.ethereum.EthereumWalletManager import com.tangem.blockchain.common.Amount import com.tangem.blockchain.common.AmountType import com.tangem.blockchain.common.Blockchain -import com.tangem.blockchain.common.Token import com.tangem.blockchain.common.transaction.TransactionFee import com.tangem.blockchain.extensions.Result import com.tangem.domain.models.scan.CardDTO @@ -87,17 +86,12 @@ class CurrencyExchangeManager( } } -suspend fun buyErc20TestnetTokens(card: CardDTO, walletManager: EthereumWalletManager, token: Token) { +suspend fun buyErc20TestnetTokens(card: CardDTO, walletManager: EthereumWalletManager, destinationAddress: String) { walletManager.safeUpdate() val amountToSend = Amount(walletManager.wallet.blockchain) - val destinationAddress = token.contractAddress - - val feeResult = walletManager.getFee( - amountToSend, - destinationAddress, - ) as? Result.Success ?: return + val feeResult = walletManager.getFee(amountToSend, destinationAddress) as? Result.Success ?: return val fee = when (val feeForTx = feeResult.data) { is TransactionFee.Choosable -> feeForTx.minimum is TransactionFee.Single -> feeForTx.normal @@ -106,8 +100,6 @@ suspend fun buyErc20TestnetTokens(card: CardDTO, walletManager: EthereumWalletMa val coinValue = walletManager.wallet.amounts[AmountType.Coin]?.value ?: BigDecimal.ZERO if (coinValue < fee.amount.value) return - val transaction = walletManager.createTransaction(amountToSend, fee, destinationAddress) - val signer = TangemSigner( card = card, tangemSdk = store.state.daggerGraphState.get(DaggerGraphState::cardSdkConfigRepository).sdk, @@ -121,5 +113,13 @@ suspend fun buyErc20TestnetTokens(card: CardDTO, walletManager: EthereumWalletMa ), ) } - walletManager.send(transaction, signer) + + walletManager.send( + transactionData = walletManager.createTransaction( + amount = amountToSend, + fee = fee, + destination = destinationAddress, + ), + signer = signer, + ) } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/proxy/AppStateHolder.kt b/app/src/main/java/com/tangem/tap/proxy/AppStateHolder.kt index 8ea80aec87..cdcbc7fd9a 100644 --- a/app/src/main/java/com/tangem/tap/proxy/AppStateHolder.kt +++ b/app/src/main/java/com/tangem/tap/proxy/AppStateHolder.kt @@ -1,9 +1,11 @@ package com.tangem.tap.proxy +import com.tangem.core.navigation.AppScreen import com.tangem.core.navigation.NavigationAction -import com.tangem.core.navigation.NavigationStateHolder +import com.tangem.core.navigation.ReduxNavController import com.tangem.domain.models.scan.CardDTO import com.tangem.domain.models.scan.ScanResponse +import com.tangem.domain.redux.ReduxStateHolder import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.wallets.legacy.WalletsStateHolder import com.tangem.tap.common.entities.FiatCurrency @@ -14,6 +16,7 @@ import com.tangem.tap.domain.walletStores.WalletStoresManager import com.tangem.tap.features.wallet.redux.WalletState import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow +import org.rekotlin.Action import org.rekotlin.Store import javax.inject.Inject @@ -21,7 +24,7 @@ import javax.inject.Inject * Holds objects from old modules, that missing in DI graph. * Object sets manually to use in new modules and [AppStateHolder] proxies its to DI. */ -class AppStateHolder @Inject constructor() : WalletsStateHolder, NavigationStateHolder { +class AppStateHolder @Inject constructor() : WalletsStateHolder, ReduxNavController, ReduxStateHolder { override var userWalletsListManager: UserWalletsListManager? = null set(value) { @@ -50,4 +53,10 @@ class AppStateHolder @Inject constructor() : WalletsStateHolder, NavigationState override fun navigate(action: NavigationAction) { mainStore?.dispatch(action) } + + override fun getBackStack(): List = mainStore?.state?.navigationState?.backStack.orEmpty() + + override fun dispatch(action: Action) { + mainStore?.dispatch(action) + } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/proxy/DerivationManagerImpl.kt b/app/src/main/java/com/tangem/tap/proxy/DerivationManagerImpl.kt index 894467225e..174dd3762d 100644 --- a/app/src/main/java/com/tangem/tap/proxy/DerivationManagerImpl.kt +++ b/app/src/main/java/com/tangem/tap/proxy/DerivationManagerImpl.kt @@ -1,5 +1,6 @@ package com.tangem.tap.proxy +import com.tangem.blockchain.blockchains.cardano.CardanoUtils import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.Token import com.tangem.common.CompletionResult @@ -29,6 +30,7 @@ import com.tangem.tap.scope import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlin.coroutines.suspendCoroutine +import com.tangem.tap.features.wallet.models.Currency as WalletModelCurrency class DerivationManagerImpl( private val appStateHolder: AppStateHolder, @@ -50,7 +52,7 @@ class DerivationManagerImpl( val scanResponse = appStateHolder.scanResponse if (scanResponse != null) { val blockchainNetwork = BlockchainNetwork(blockchain, scanResponse.derivationStyleProvider) - val appCurrency = com.tangem.tap.features.wallet.models.Currency.fromBlockchainNetwork( + val appCurrency = WalletModelCurrency.fromBlockchainNetwork( blockchainNetwork, appToken, ) @@ -90,7 +92,7 @@ class DerivationManagerImpl( private fun deriveMissingBlockchains( scanResponse: ScanResponse, - currencyList: List, + currencyList: List, onSuccess: (ScanResponse) -> Unit, onFailure: (Exception) -> Unit, ) { @@ -157,7 +159,7 @@ class DerivationManagerImpl( private fun getDerivations( curve: EllipticCurve, scanResponse: ScanResponse, - currencyList: List, + currencyList: List, ): DerivationData? { val wallet = scanResponse.card.wallets.firstOrNull { it.curve == curve } ?: return null @@ -171,7 +173,7 @@ class DerivationManagerImpl( it.blockchain.getSupportedCurves().contains(curve) }.mapNotNull { it.derivationPath }.map { DerivationPath(it) } - val bothCandidates = (manageTokensCandidates + customTokensCandidates).distinct() + val bothCandidates = (manageTokensCandidates + customTokensCandidates).distinct().toMutableList() if (bothCandidates.isEmpty()) return null val mapKeyOfWalletPublicKey = wallet.publicKey.toMapKey() @@ -182,6 +184,13 @@ class DerivationManagerImpl( val toDerive = bothCandidates.filterNot { alreadyDerivedPaths.contains(it) } if (toDerive.isEmpty()) return null + currencyList.find { it is WalletModelCurrency.Blockchain && it.blockchain == Blockchain.Cardano } + ?.let { currency -> + currency.derivationPath?.let { + bothCandidates.add(CardanoUtils.extendedDerivationPath(DerivationPath(it))) + } + } + return DerivationData( derivations = mapKeyOfWalletPublicKey to toDerive, ) diff --git a/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphState.kt b/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphState.kt index b9be4b9f93..c677f70300 100644 --- a/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphState.kt +++ b/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphState.kt @@ -6,6 +6,7 @@ import com.tangem.domain.appcurrency.repository.AppCurrencyRepository import com.tangem.domain.card.ScanCardProcessor import com.tangem.domain.card.ScanCardUseCase import com.tangem.domain.card.repository.CardSdkConfigRepository +import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.features.tester.api.TesterRouter import com.tangem.features.tokendetails.featuretoggles.TokenDetailsFeatureToggles import com.tangem.features.tokendetails.navigation.TokenDetailsRouter @@ -33,6 +34,7 @@ data class DaggerGraphState( val scanCardProcessor: ScanCardProcessor? = null, val cardSdkConfigRepository: CardSdkConfigRepository? = null, val appCurrencyRepository: AppCurrencyRepository? = null, + val walletManagersFacade: WalletManagersFacade? = null, ) : StateType { inline fun get(getDependency: DaggerGraphState.() -> T?): T { diff --git a/app/src/main/res/values-v29/styles.xml b/app/src/main/res/values-v29/styles.xml index 7032bb8f84..6d423390d2 100644 --- a/app/src/main/res/values-v29/styles.xml +++ b/app/src/main/res/values-v29/styles.xml @@ -1,6 +1,7 @@ + - \ No newline at end of file + diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/Retrofit.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/Retrofit.kt index 3d8b6f3930..830cc43c53 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/common/Retrofit.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/Retrofit.kt @@ -37,5 +37,8 @@ fun createNetworkLoggingInterceptor(): Interceptor { return LoggingInterceptor.Builder() .setLevel(Level.BODY) .log(Log.VERBOSE) + .tag(NETWORK_LOGS_TAG) .build() -} \ No newline at end of file +} + +private const val NETWORK_LOGS_TAG = "NetworkLogs" \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/appcurrency/MockSelectedAppCurrencyStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/appcurrency/MockSelectedAppCurrencyStore.kt deleted file mode 100644 index 58f48aacc8..0000000000 --- a/core/datasource/src/main/java/com/tangem/datasource/local/appcurrency/MockSelectedAppCurrencyStore.kt +++ /dev/null @@ -1,26 +0,0 @@ -package com.tangem.datasource.local.appcurrency - -import com.tangem.datasource.api.tangemTech.models.CurrenciesResponse -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.flowOf - -// TODO: Will be implemented in [REDACTED_TASK_KEY] task -internal class MockSelectedAppCurrencyStore : SelectedAppCurrencyStore { - - override fun get(): Flow { - return flowOf( - CurrenciesResponse.Currency( - id = "usd", - code = "USD", - name = "US Dollar", - unit = "$", - type = "fiat", - rateBTC = "", - ), - ) - } - - override suspend fun store(item: CurrenciesResponse.Currency) { - /* no-op */ - } -} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/appcurrency/SelectedAppCurrencyStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/appcurrency/SelectedAppCurrencyStore.kt index e1fbad8329..e732ce8415 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/appcurrency/SelectedAppCurrencyStore.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/appcurrency/SelectedAppCurrencyStore.kt @@ -7,5 +7,9 @@ interface SelectedAppCurrencyStore { fun get(): Flow + suspend fun getSyncOrNull(): CurrenciesResponse.Currency? + suspend fun store(item: CurrenciesResponse.Currency) + + suspend fun isEmpty(): Boolean } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/appcurrency/implementation/DefaultSelectedAppCurrencyStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/appcurrency/implementation/DefaultSelectedAppCurrencyStore.kt index c79bf3045b..b6d8f61ade 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/appcurrency/implementation/DefaultSelectedAppCurrencyStore.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/appcurrency/implementation/DefaultSelectedAppCurrencyStore.kt @@ -7,4 +7,8 @@ import com.tangem.datasource.local.datastore.core.StringKeyDataStore internal class DefaultSelectedAppCurrencyStore( dataStore: StringKeyDataStore, -) : SelectedAppCurrencyStore, KeylessDataStoreDecorator(dataStore) \ No newline at end of file +) : SelectedAppCurrencyStore, KeylessDataStoreDecorator(dataStore) { + override suspend fun isEmpty(): Boolean { + return getSyncOrNull() == null + } +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/datastore/core/KeylessDataStoreDecorator.kt b/core/datasource/src/main/java/com/tangem/datasource/local/datastore/core/KeylessDataStoreDecorator.kt index 3d567967cf..5e74e2f386 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/datastore/core/KeylessDataStoreDecorator.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/datastore/core/KeylessDataStoreDecorator.kt @@ -10,15 +10,15 @@ internal abstract class KeylessDataStoreDecorator( return STRING_KEY } - fun get(): Flow { + open fun get(): Flow { return get(Unit) } - suspend fun getSyncOrNull(): Value? { + open suspend fun getSyncOrNull(): Value? { return getSyncOrNull(Unit) } - suspend fun store(item: Value) { + open suspend fun store(item: Value) { store(Unit, item) } diff --git a/core/navigation/src/main/java/com/tangem/core/navigation/NavigationStateHolder.kt b/core/navigation/src/main/java/com/tangem/core/navigation/ReduxNavController.kt similarity index 53% rename from core/navigation/src/main/java/com/tangem/core/navigation/NavigationStateHolder.kt rename to core/navigation/src/main/java/com/tangem/core/navigation/ReduxNavController.kt index 7504b75656..ff8e8ced9e 100644 --- a/core/navigation/src/main/java/com/tangem/core/navigation/NavigationStateHolder.kt +++ b/core/navigation/src/main/java/com/tangem/core/navigation/ReduxNavController.kt @@ -1,12 +1,14 @@ package com.tangem.core.navigation /** - * Navigation state holder + * Navigation controller that based on redux actions * [REDACTED_AUTHOR] */ -interface NavigationStateHolder { +interface ReduxNavController { /** Navigate by [action] */ fun navigate(action: NavigationAction) + + fun getBackStack(): List } \ No newline at end of file diff --git a/core/res/src/main/res/values-ru/strings.xml b/core/res/src/main/res/values-ru/strings.xml index 2831c8d27d..2eb19248a6 100644 --- a/core/res/src/main/res/values-ru/strings.xml +++ b/core/res/src/main/res/values-ru/strings.xml @@ -95,6 +95,7 @@ Получить Отклонить Перезагрузить + Переименовать Сбросить Сохранить изменения Искать @@ -116,6 +117,7 @@ Транзакции Перевод Я понял + Необходима разблокировка Недоступно Да Адрес контракта скопирован! @@ -457,6 +459,7 @@ контракт: %s У вас еще нет транзакций Не удалось загрузить историю транзакций.\nНажмите на кнопку перезагрузки, чтобы обновить информацию. + Несколько адресов История транзакций в настоящее время не поддерживается для этого блокчейна. Но не волнуйтесь, мы работаем над этим! А пока вы можете проверить ее в обозревателе. от: %s на: %s @@ -472,6 +475,8 @@ Tangem Twin Это действие необратимо. У вас не будет доступа к старому кошельку. Приложите twin-карту с номером %s и не убирайте до окончания операции + Используйте %s или отсканируйте карту, чтобы получить доступ к своему кошельку + Используйте %s или отсканируйте карту Добавить новый кошелек Вы уверены, что хотите удалить этот кошелек? %d выбрано diff --git a/core/res/src/main/res/values-zh-rTW/strings.xml b/core/res/src/main/res/values-zh-rTW/strings.xml index ad55e197d1..294f3413b7 100644 --- a/core/res/src/main/res/values-zh-rTW/strings.xml +++ b/core/res/src/main/res/values-zh-rTW/strings.xml @@ -79,6 +79,7 @@ OK 主卡片 拒絕 + 重新命名 重置 保存設置 搜索 diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index 0542ac7dc4..186b92d9ea 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -94,6 +94,7 @@ Receive Reject Reload + Rename Reset Save changes Search @@ -115,6 +116,7 @@ Transactions Transfer I understand + Unlock needed Unreachable Yes Contract address copied! @@ -448,6 +450,7 @@ contract: %s You don\'t have any transactions yet Failed to load transaction history.\nClick on reload button to update the information. + Multiple addresses Transaction history is currently not supported for this blockchain. But don\'t worry, we\'re working on it! In the meantime you can check it in the explorer. from: %s to: %s @@ -463,6 +466,8 @@ Tangem Twin This action is irreversible. You will not have access to the old wallet. Tap the twin card with number %s and do not remove until the end of the operation + Use %s or scan a card to have an access to your wallet + Use %s or scan a card Add new wallet Are you sure you want to delete this wallet? %d selected diff --git a/core/ui/build.gradle.kts b/core/ui/build.gradle.kts index 5cabb03840..20d4303b93 100644 --- a/core/ui/build.gradle.kts +++ b/core/ui/build.gradle.kts @@ -5,6 +5,13 @@ plugins { } dependencies { + /** Project - Domain */ + implementation(projects.domain.tokens.models) + implementation(projects.domain.appTheme.models) + + /** Project - Core */ + implementation(projects.core.res) + /** AndroidX libraries */ implementation(deps.androidx.fragment.ktx) implementation(deps.androidx.paging.runtime) @@ -22,6 +29,4 @@ dependencies { implementation(deps.material) implementation(deps.compose.shimmer) implementation(deps.kotlin.immutable.collections) - - implementation(project(":core:res")) } \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/SettingsRow.kt b/core/ui/src/main/java/com/tangem/core/ui/components/SettingsRow.kt new file mode 100644 index 0000000000..823a93b8e3 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/SettingsRow.kt @@ -0,0 +1,74 @@ +package com.tangem.core.ui.components + +import androidx.annotation.DrawableRes +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.animateColorAsState +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.material.Icon +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +import com.tangem.core.ui.res.TangemTheme + +/** + * [Show in Figma](https://www.figma.com/file/14ISV23YB1yVW1uNVwqrKv/Android?type=design&node-id=281-248&mode=design&t=bXqehWPHyATKcZEW-4) + * */ +@Composable +fun SimpleSettingsRow( + title: String, + @DrawableRes icon: Int, + onItemsClick: () -> Unit, + modifier: Modifier = Modifier, + enabled: Boolean = true, + subtitle: String? = null, +) { + Row( + modifier = modifier + .height(TangemTheme.dimens.size56) + .fillMaxWidth() + .clickable( + onClick = { + if (enabled) { + onItemsClick() + } + }, + ), + horizontalArrangement = Arrangement.Start, + verticalAlignment = Alignment.CenterVertically, + ) { + val textColor: Color by animateColorAsState( + targetValue = if (enabled) { + TangemTheme.colors.text.primary1 + } else { + TangemTheme.colors.text.secondary + }, + ) + Icon( + painter = painterResource(id = icon), + contentDescription = null, + modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing20), + tint = textColor, + ) + Column(modifier = Modifier.padding(end = TangemTheme.dimens.spacing20)) { + Text( + text = title, + style = TangemTheme.typography.subtitle1, + color = textColor, + ) + AnimatedVisibility( + visible = !subtitle.isNullOrEmpty(), + ) { + Text( + text = subtitle ?: "", + style = TangemTheme.typography.body2, + color = TangemTheme.colors.text.secondary, + ) + } + } + } +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/Shimmers.kt b/core/ui/src/main/java/com/tangem/core/ui/components/Shimmers.kt index e793e21eaf..640f986b3e 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/Shimmers.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/Shimmers.kt @@ -7,6 +7,7 @@ import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.Dp import com.tangem.core.ui.res.TangemTheme import com.valentinilk.shimmer.shimmer @@ -14,13 +15,13 @@ import com.valentinilk.shimmer.shimmer * Rectangle shimmer item with rounded shape from DS */ @Composable -fun RectangleShimmer(modifier: Modifier = Modifier) { +fun RectangleShimmer(modifier: Modifier = Modifier, radius: Dp = TangemTheme.dimens.radius6) { Box( modifier = modifier .shimmer() .background( color = TangemTheme.colors.button.secondary, - shape = RoundedCornerShape(TangemTheme.dimens.radius6), + shape = RoundedCornerShape(size = radius), ), ) } @@ -31,16 +32,14 @@ fun RectangleShimmer(modifier: Modifier = Modifier) { */ @Composable fun CircleShimmer(modifier: Modifier = Modifier) { - Box(modifier = modifier.shimmer()) { - Box( - modifier = Modifier - .matchParentSize() - .background( - color = TangemTheme.colors.button.secondary, - shape = CircleShape, - ), - ) - } + Box( + modifier = modifier + .shimmer() + .background( + color = TangemTheme.colors.button.secondary, + shape = CircleShape, + ), + ) } // region preview diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/buttons/HorizontalActionChips.kt b/core/ui/src/main/java/com/tangem/core/ui/components/buttons/HorizontalActionChips.kt index 6b20a931f8..b34aa9c970 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/buttons/HorizontalActionChips.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/buttons/HorizontalActionChips.kt @@ -33,7 +33,7 @@ fun HorizontalActionChips( ) { items( items = buttons, - key = { config -> "${config.text.hashCode()} ${config.iconResId}" }, + key = { config -> config.text.hashCode() }, itemContent = { ActionButton(config = it) }, ) } diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/buttons/actions/Actions.kt b/core/ui/src/main/java/com/tangem/core/ui/components/buttons/actions/Actions.kt index 65422a2f7b..2167ad385c 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/buttons/actions/Actions.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/buttons/actions/Actions.kt @@ -1,5 +1,6 @@ package com.tangem.core.ui.components.buttons.actions +import androidx.compose.animation.animateColorAsState import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* @@ -7,6 +8,7 @@ import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.Icon import androidx.compose.material.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip @@ -78,43 +80,52 @@ private fun Button( modifier: Modifier = Modifier, color: Color = TangemTheme.colors.button.secondary, ) { + val backgroundColor by animateColorAsState( + targetValue = if (config.enabled) color else TangemTheme.colors.button.disabled, + label = "Update background color", + ) + Row( modifier = modifier .heightIn(min = TangemTheme.dimens.size36) .clip(shape) - .background( - color = if (config.enabled) color else TangemTheme.colors.button.disabled, - shape = shape, - ) + .background(color = backgroundColor, shape = shape) .clickable(enabled = config.enabled, onClick = config.onClick) - .padding( - start = TangemTheme.dimens.spacing16, - end = TangemTheme.dimens.spacing24, - ) + .padding(start = TangemTheme.dimens.spacing16, end = TangemTheme.dimens.spacing24) .padding(vertical = TangemTheme.dimens.spacing8), horizontalArrangement = Arrangement.Center, verticalAlignment = Alignment.CenterVertically, ) { - Icon( - painter = painterResource(id = config.iconResId), - contentDescription = null, - modifier = Modifier.size(size = TangemTheme.dimens.size20), - tint = when { + val iconTint by animateColorAsState( + targetValue = when { !config.enabled -> TangemTheme.colors.icon.informative config.dimContent -> TangemTheme.colors.icon.secondary else -> TangemTheme.colors.icon.primary1 }, + label = "Update tint color", + ) + + Icon( + painter = painterResource(id = config.iconResId), + contentDescription = null, + modifier = Modifier.size(size = TangemTheme.dimens.size20), + tint = iconTint, ) SpacerW8() - Text( - text = config.text.resolveReference(), - color = when { + val textColor by animateColorAsState( + targetValue = when { !config.enabled -> TangemTheme.colors.text.disabled config.dimContent -> TangemTheme.colors.text.secondary else -> TangemTheme.colors.text.primary1 }, + label = "Update text color", + ) + + Text( + text = config.text.resolveReference(), + color = textColor, overflow = TextOverflow.Ellipsis, maxLines = 1, style = TangemTheme.typography.button, diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/marketprice/MarketPriceBlock.kt b/core/ui/src/main/java/com/tangem/core/ui/components/marketprice/MarketPriceBlock.kt index 4192706627..f498b4caf7 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/marketprice/MarketPriceBlock.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/marketprice/MarketPriceBlock.kt @@ -167,7 +167,7 @@ private fun LoadingContent() { RectangleShimmer( modifier = Modifier.size( width = TangemTheme.dimens.size158, - height = TangemTheme.dimens.size20, + height = TangemTheme.dimens.size18, ), ) diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/transactions/Transaction.kt b/core/ui/src/main/java/com/tangem/core/ui/components/transactions/Transaction.kt index 9256ad0ccc..b3622b4b1b 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/transactions/Transaction.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/transactions/Transaction.kt @@ -4,6 +4,7 @@ import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Icon import androidx.compose.material3.Surface import androidx.compose.material3.Text @@ -22,6 +23,8 @@ import com.tangem.core.ui.R import com.tangem.core.ui.components.CircleShimmer import com.tangem.core.ui.components.RectangleShimmer import com.tangem.core.ui.components.transactions.state.TransactionState +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.res.TangemTheme import java.util.UUID @@ -158,6 +161,18 @@ private fun Icon(state: TransactionState, modifier: Modifier = Modifier) { is TransactionState.Loading -> { CircleShimmer(modifier = modifier.size(TangemTheme.dimens.size40)) } + is TransactionState.Locked -> { + Box(modifier = modifier.size(TangemTheme.dimens.size40)) { + Box( + modifier = Modifier + .matchParentSize() + .background( + color = TangemTheme.colors.button.secondary, + shape = CircleShape, + ), + ) + } + } } } @@ -206,6 +221,11 @@ private fun Title(state: TransactionState, modifier: Modifier = Modifier) { modifier = modifier.size(width = TangemTheme.dimens.size70, height = TangemTheme.dimens.size12), ) } + is TransactionState.Locked -> { + LockedContent( + modifier = modifier.size(width = TangemTheme.dimens.size70, height = TangemTheme.dimens.size12), + ) + } } } @@ -219,7 +239,7 @@ private fun Subtitle(state: TransactionState, modifier: Modifier = Modifier) { is TransactionState.Send, -> stringResource( id = R.string.transaction_history_transaction_to_address, - state.address, + state.address.resolveReference(), ) is TransactionState.Receiving, is TransactionState.Receive, @@ -227,13 +247,13 @@ private fun Subtitle(state: TransactionState, modifier: Modifier = Modifier) { is TransactionState.Approved, -> stringResource( id = R.string.transaction_history_transaction_from_address, - state.address, + state.address.resolveReference(), ) is TransactionState.Swapping, is TransactionState.Swapped, -> stringResource( id = R.string.transaction_history_contract_address, - state.address, + state.address.resolveReference(), ) }, modifier = modifier, @@ -247,6 +267,11 @@ private fun Subtitle(state: TransactionState, modifier: Modifier = Modifier) { modifier = modifier.size(width = TangemTheme.dimens.size52, height = TangemTheme.dimens.size12), ) } + is TransactionState.Locked -> { + LockedContent( + modifier = modifier.size(width = TangemTheme.dimens.size52, height = TangemTheme.dimens.size12), + ) + } } } @@ -267,6 +292,11 @@ private fun Amount(state: TransactionState, modifier: Modifier = Modifier) { modifier = modifier.size(width = TangemTheme.dimens.size40, height = TangemTheme.dimens.size12), ) } + is TransactionState.Locked -> { + LockedContent( + modifier = modifier.size(width = TangemTheme.dimens.size40, height = TangemTheme.dimens.size12), + ) + } } } @@ -287,9 +317,24 @@ private fun Timestamp(state: TransactionState, modifier: Modifier = Modifier) { modifier = modifier.size(width = TangemTheme.dimens.size40, height = TangemTheme.dimens.size12), ) } + is TransactionState.Locked -> { + LockedContent( + modifier = modifier.size(width = TangemTheme.dimens.size40, height = TangemTheme.dimens.size12), + ) + } } } +@Composable +private fun LockedContent(modifier: Modifier = Modifier) { + Box( + modifier = modifier.background( + color = TangemTheme.colors.field.primary, + shape = RoundedCornerShape(TangemTheme.dimens.radius6), + ), + ) +} + @Preview @Composable private fun Preview_TransactionItem_LightTheme( @@ -314,49 +359,49 @@ private class TransactionItemStateProvider : CollectionPreviewParameterProvider< collection = listOf( TransactionState.Sending( txHash = UUID.randomUUID().toString(), - address = "33BddS...ga2B", + address = TextReference.Str("33BddS...ga2B"), amount = "-0.500913 BTC", timestamp = "8:41", ), TransactionState.Receiving( txHash = UUID.randomUUID().toString(), - address = "33BddS...ga2B", + address = TextReference.Str("33BddS...ga2B"), amount = "+0.500913 BTC", timestamp = "8:41", ), TransactionState.Approving( txHash = UUID.randomUUID().toString(), - address = "33BddS...ga2B", + address = TextReference.Str("33BddS...ga2B"), amount = "+0.500913 BTC", timestamp = "8:41", ), TransactionState.Swapping( txHash = UUID.randomUUID().toString(), - address = "33BddS...ga2B", + address = TextReference.Str("33BddS...ga2B"), amount = "+0.500913 BTC", timestamp = "8:41", ), TransactionState.Send( txHash = UUID.randomUUID().toString(), - address = "33BddS...ga2B", + address = TextReference.Str("33BddS...ga2B"), amount = "-0.500913 BTC", timestamp = "8:41", ), TransactionState.Receive( txHash = UUID.randomUUID().toString(), - address = "33BddS...ga2B", + address = TextReference.Str("33BddS...ga2B"), amount = "+0.500913 BTC", timestamp = "8:41", ), TransactionState.Approved( txHash = UUID.randomUUID().toString(), - address = "33BddS...ga2B", + address = TextReference.Str("33BddS...ga2B"), amount = "+0.500913 BTC", timestamp = "8:41", ), TransactionState.Swapped( txHash = UUID.randomUUID().toString(), - address = "33BddS...ga2B", + address = TextReference.Str("33BddS...ga2B"), amount = "+0.500913 BTC", timestamp = "8:41", ), diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/transactions/TransactionList.kt b/core/ui/src/main/java/com/tangem/core/ui/components/transactions/TransactionList.kt index 6be91c66ae..93a6d16058 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/transactions/TransactionList.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/transactions/TransactionList.kt @@ -4,9 +4,11 @@ import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyListScope +import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.ui.Modifier import androidx.paging.compose.LazyPagingItems -import androidx.paging.compose.itemsIndexed +import androidx.paging.compose.itemContentType +import androidx.paging.compose.itemKey import com.tangem.core.ui.components.transactions.empty.EmptyTransactionBlock import com.tangem.core.ui.components.transactions.empty.EmptyTransactionsBlockState import com.tangem.core.ui.components.transactions.state.TxHistoryState @@ -26,7 +28,7 @@ fun LazyListScope.txHistoryItems( modifier: Modifier = Modifier, ) { when (state) { - is TxHistoryState.ContentState -> { + is TxHistoryState.Content -> { contentItems( txHistoryItems = requireNotNull(txHistoryItems), modifier = modifier, @@ -58,8 +60,18 @@ private fun LazyListScope.contentItems( txHistoryItems: LazyPagingItems, modifier: Modifier = Modifier, ) { + txHistoryItems.itemKey { item -> + when (item) { + is TxHistoryState.TxHistoryItemState.GroupTitle -> item.title + is TxHistoryState.TxHistoryItemState.Title -> item.onExploreClick.hashCode() + is TxHistoryState.TxHistoryItemState.Transaction -> item.state.txHash + } + } + + txHistoryItems.itemContentType { it::class.java } + itemsIndexed( - items = txHistoryItems, + items = txHistoryItems.itemSnapshotList.items, key = { _, item -> when (item) { is TxHistoryState.TxHistoryItemState.GroupTitle -> item.title @@ -68,8 +80,6 @@ private fun LazyListScope.contentItems( } }, ) { index, item -> - if (item == null) return@itemsIndexed - TxHistoryListItem( state = item, modifier = modifier diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/transactions/intents/TxHistoryClickIntents.kt b/core/ui/src/main/java/com/tangem/core/ui/components/transactions/intents/TxHistoryClickIntents.kt new file mode 100644 index 0000000000..4abce062be --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/transactions/intents/TxHistoryClickIntents.kt @@ -0,0 +1,10 @@ +package com.tangem.core.ui.components.transactions.intents + +interface TxHistoryClickIntents { + + fun onBuyClick() + + fun onReloadClick() + + fun onExploreClick() +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/transactions/state/TransactionState.kt b/core/ui/src/main/java/com/tangem/core/ui/components/transactions/state/TransactionState.kt index 8c5f73c545..8178e83e61 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/transactions/state/TransactionState.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/transactions/state/TransactionState.kt @@ -1,5 +1,7 @@ package com.tangem.core.ui.components.transactions.state +import com.tangem.core.ui.extensions.TextReference + /** * Transaction component state * @@ -20,14 +22,14 @@ sealed interface TransactionState { */ sealed class Content( override val txHash: String, - open val address: String, + open val address: TextReference, open val amount: String, open val timestamp: String, ) : TransactionState { fun copySealed( txHash: String = this.txHash, - address: String = this.address, + address: TextReference = this.address, amount: String = this.amount, timestamp: String = this.timestamp, ): Content { @@ -54,7 +56,7 @@ sealed interface TransactionState { */ sealed class ProcessedTransactionContent( override val txHash: String, - override val address: String, + override val address: TextReference, override val amount: String, override val timestamp: String, ) : Content(txHash, address, amount, timestamp) @@ -69,7 +71,7 @@ sealed interface TransactionState { */ sealed class CompletedTransactionContent( override val txHash: String, - override val address: String, + override val address: TextReference, override val amount: String, override val timestamp: String, ) : Content(txHash, address, amount, timestamp) @@ -84,7 +86,7 @@ sealed interface TransactionState { */ data class Sending( override val txHash: String, - override val address: String, + override val address: TextReference, override val amount: String, override val timestamp: String, ) : ProcessedTransactionContent(txHash, address, amount, timestamp) @@ -99,7 +101,7 @@ sealed interface TransactionState { */ data class Receiving( override val txHash: String, - override val address: String, + override val address: TextReference, override val amount: String, override val timestamp: String, ) : ProcessedTransactionContent(txHash, address, amount, timestamp) @@ -114,7 +116,7 @@ sealed interface TransactionState { */ data class Approving( override val txHash: String, - override val address: String, + override val address: TextReference, override val amount: String, override val timestamp: String, ) : ProcessedTransactionContent(txHash, address, amount, timestamp) @@ -129,7 +131,7 @@ sealed interface TransactionState { */ data class Swapping( override val txHash: String, - override val address: String, + override val address: TextReference, override val amount: String, override val timestamp: String, ) : ProcessedTransactionContent(txHash, address, amount, timestamp) @@ -144,7 +146,7 @@ sealed interface TransactionState { */ data class Send( override val txHash: String, - override val address: String, + override val address: TextReference, override val amount: String, override val timestamp: String, ) : CompletedTransactionContent(txHash, address, amount, timestamp) @@ -159,7 +161,7 @@ sealed interface TransactionState { */ data class Receive( override val txHash: String, - override val address: String, + override val address: TextReference, override val amount: String, override val timestamp: String, ) : CompletedTransactionContent(txHash, address, amount, timestamp) @@ -174,7 +176,7 @@ sealed interface TransactionState { */ data class Approved( override val txHash: String, - override val address: String, + override val address: TextReference, override val amount: String, override val timestamp: String, ) : CompletedTransactionContent(txHash, address, amount, timestamp) @@ -189,7 +191,7 @@ sealed interface TransactionState { */ data class Swapped( override val txHash: String, - override val address: String, + override val address: TextReference, override val amount: String, override val timestamp: String, ) : CompletedTransactionContent(txHash, address, amount, timestamp) @@ -200,4 +202,11 @@ sealed interface TransactionState { * @property txHash transaction hash */ data class Loading(override val txHash: String) : TransactionState + + /** + * Locked state + * + * @property txHash transaction hash + */ + data class Locked(override val txHash: String) : TransactionState } \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/transactions/state/TxHistoryState.kt b/core/ui/src/main/java/com/tangem/core/ui/components/transactions/state/TxHistoryState.kt index 85c907187a..a6035f48dc 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/transactions/state/TxHistoryState.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/transactions/state/TxHistoryState.kt @@ -1,77 +1,17 @@ package com.tangem.core.ui.components.transactions.state import androidx.paging.PagingData -import com.tangem.core.ui.components.wallet.WalletLockedContentState -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.MutableStateFlow -/** - * Wallet transaction history state - */ +/** Wallet transaction history state */ sealed interface TxHistoryState { /** * Wallet transaction history state with content * - * @property items content items + * @property contentItems content items */ - sealed class ContentState(open val items: Flow>) : TxHistoryState - - /** - * Loading state - * - * @property onExploreClick lambda be invoke when explore button was clicked - */ - data class Loading(val onExploreClick: () -> Unit) : ContentState( - items = flowOf( - PagingData.from( - listOf( - TxHistoryItemState.Title(onExploreClick = onExploreClick), - TxHistoryItemState.Transaction(state = TransactionState.Loading(txHash = LOADING_TX_HASH)), - ), - ), - ), - ) - - /** - * Wallet transaction history state with loading transactions - * - * @property itemsCount count of loading transactions - */ - data class ContentWithLoadingItems(val itemsCount: Int) : ContentState( - items = flowOf( - value = PagingData.from( - data = buildList(capacity = itemsCount) { - add(TxHistoryItemState.Transaction(state = TransactionState.Loading(txHash = LOADING_TX_HASH))) - }, - ), - ), - ) - - /** - * Wallet transaction history state with content - * - * @property items content items - */ - data class Content(override val items: Flow>) : ContentState(items) - - /** - * Locked state - * - * @property onExploreClick lambda be invoke when explore button was clicked - */ - data class Locked(val onExploreClick: () -> Unit) : - ContentState( - items = flowOf( - PagingData.from( - listOf( - TxHistoryItemState.Title(onExploreClick = onExploreClick), - TxHistoryItemState.Transaction(state = TransactionState.Loading(txHash = LOADING_TX_HASH)), - ), - ), - ), - ), - WalletLockedContentState + data class Content(val contentItems: MutableStateFlow>) : TxHistoryState /** * Empty state @@ -119,7 +59,18 @@ sealed interface TxHistoryState { data class Transaction(val state: TransactionState) : TxHistoryItemState } - private companion object { - const val LOADING_TX_HASH = "LOADING_TX_HASH" + companion object { + private const val LOADING_TX_HASH = "LOADING_TX_HASH" + + fun getDefaultLoadingTransactions(onExploreClick: () -> Unit): PagingData { + return PagingData.from( + data = listOf( + TxHistoryItemState.Title(onExploreClick = onExploreClick), + TxHistoryItemState.Transaction( + state = TransactionState.Loading(txHash = LOADING_TX_HASH), + ), + ), + ) + } } } \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/wallet/WalletLockedContentState.kt b/core/ui/src/main/java/com/tangem/core/ui/components/wallet/WalletLockedContentState.kt deleted file mode 100644 index e1c2049da6..0000000000 --- a/core/ui/src/main/java/com/tangem/core/ui/components/wallet/WalletLockedContentState.kt +++ /dev/null @@ -1,7 +0,0 @@ -package com.tangem.core.ui.components.wallet - -/** - * Wallet locked content state. - * It allows to divide the locked content of multi-currency and single-currency wallets. - */ -interface WalletLockedContentState \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/wallets/RenameWalletDialogContent.kt b/core/ui/src/main/java/com/tangem/core/ui/components/wallets/RenameWalletDialogContent.kt new file mode 100644 index 0000000000..eabd0fbeb8 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/wallets/RenameWalletDialogContent.kt @@ -0,0 +1,59 @@ +package com.tangem.core.ui.components.wallets + +import androidx.compose.runtime.* +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.input.TextFieldValue +import androidx.compose.ui.tooling.preview.Preview +import com.tangem.core.ui.R +import com.tangem.core.ui.components.AdditionalTextInputDialogParams +import com.tangem.core.ui.components.DialogButton +import com.tangem.core.ui.components.TextInputDialog +import com.tangem.core.ui.res.TangemTheme + +/** + * Rename a wallet dialog + * + * @param name wallet name + * @param onConfirm lambda be invoked when Confirm button is clicked + * @param onDismiss lambda be invoked when dialog is dismissed + */ +@Composable +fun RenameWalletDialogContent(name: String, onConfirm: (newName: String) -> Unit, onDismiss: () -> Unit) { + var value by remember { mutableStateOf(TextFieldValue(text = name)) } + + TextInputDialog( + fieldValue = value, + confirmButton = DialogButton( + title = stringResource(id = R.string.common_ok), + enabled = value.text.isNotEmpty() && value.text != name, + onClick = { onConfirm(value.text) }, + ), + onDismissDialog = onDismiss, + onValueChange = { value = it }, + title = stringResource(R.string.user_wallet_list_rename_popup_title), + dismissButton = DialogButton(title = stringResource(id = R.string.common_cancel), onClick = onDismiss), + textFieldParams = AdditionalTextInputDialogParams( + label = stringResource(R.string.user_wallet_list_rename_popup_placeholder), + ), + ) +} + +// region Preview + +@Preview(showBackground = true, widthDp = 360) +@Composable +private fun RenameWalletDialogContentPreview_Light() { + TangemTheme(isDark = false) { + RenameWalletDialogContent(name = "", onConfirm = {}, onDismiss = {}) + } +} + +@Preview(showBackground = true, widthDp = 360) +@Composable +private fun RenameWalletDialogContentPreview_Dark() { + TangemTheme(isDark = true) { + RenameWalletDialogContent(name = "", onConfirm = {}, onDismiss = {}) + } +} + +// endregion Preview \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/event/EventEffect.kt b/core/ui/src/main/java/com/tangem/core/ui/event/EventEffect.kt new file mode 100644 index 0000000000..49abda9299 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/event/EventEffect.kt @@ -0,0 +1,23 @@ +package com.tangem.core.ui.event + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.NonRestartableComposable + +/** + * A Composable function that reacts to a given [StateEvent], executing the provided action only once when the event + * is triggered. + * + * @param event The [StateEvent] to listen to. + * @param onTrigger The action to execute when the event is triggered. + */ +@Composable +@NonRestartableComposable +fun EventEffect(event: StateEvent, onTrigger: suspend () -> Unit) { + LaunchedEffect(event) { + if (event is StateEvent.Triggered) { + onTrigger() + event.consume() + } + } +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/event/StateEvent.kt b/core/ui/src/main/java/com/tangem/core/ui/event/StateEvent.kt new file mode 100644 index 0000000000..d0f495b944 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/event/StateEvent.kt @@ -0,0 +1,51 @@ +package com.tangem.core.ui.event + +import androidx.compose.runtime.Immutable + +/** + * Represents compose state event, which can be consumed or triggered. + * + * This is especially useful for handling one-off UI events like showing snack bars or navigation which should not be + * re-triggered on recompositions or state changes. + */ +@Immutable +sealed class StateEvent { + + /** Defines the action to be executed when the event is consumed. */ + protected abstract val onConsume: () -> Unit + + /** + * Represents an already consumed state event. + * Events of this type will not trigger any further actions. + */ + object Consumed : StateEvent() { + override val onConsume: () -> Unit = {} + } + + /** + * Represents a state event that has been triggered but not yet consumed. + * + * @property onConsume The action to be executed when the event is consumed. + */ + data class Triggered(override val onConsume: () -> Unit) : StateEvent() + + /** + * Consumes the event, triggering any associated action. + */ + fun consume() { + onConsume() + } +} + +/** + * Creates a [StateEvent.Triggered] instance. + * + * @param onConsume The action to be executed when the event is consumed. + * @return A triggered state event. + */ +fun triggered(onConsume: () -> Unit): StateEvent.Triggered = StateEvent.Triggered(onConsume) + +/** + * Represents a statically defined [StateEvent.Consumed] event. + */ +val consumed: StateEvent.Consumed = StateEvent.Consumed \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/extensions/CryptoCurrency.kt b/core/ui/src/main/java/com/tangem/core/ui/extensions/CryptoCurrency.kt new file mode 100644 index 0000000000..7c32f55014 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/extensions/CryptoCurrency.kt @@ -0,0 +1,45 @@ +package com.tangem.core.ui.extensions + +import androidx.annotation.DrawableRes +import com.tangem.core.ui.R +import com.tangem.domain.tokens.models.CryptoCurrency + +/** + * Retrieves the resource ID for the network badge of a [CryptoCurrency]. + * + * This property provides a way to fetch the appropriate drawable resource ID + * for the network badge of a given cryptocurrency. For coins, this will typically + * return null as they do not have network badges, while tokens will fetch the icon + * based on their associated network ID. + * + * @return Drawable resource ID for the network badge or null if the cryptocurrency is a coin. + */ +@get:DrawableRes +val CryptoCurrency.networkBadgeIconResId: Int? + get() = when (this) { + is CryptoCurrency.Coin -> null + is CryptoCurrency.Token -> getActiveIconRes(network.id.value) + } + +/** + * Retrieves the resource ID for the icon of a [CryptoCurrency]. + * + * This property provides a way to fetch the appropriate drawable resource ID + * for the icon of a given cryptocurrency. + * + * @return Drawable resource ID for the cryptocurrency icon. + */ +@get:DrawableRes +val CryptoCurrency.iconResId: Int + get() = when (this) { + is CryptoCurrency.Coin -> { + val rawCoinId = id.rawCurrencyId + + if (rawCoinId != null) { + getActiveIconResByCoinId(rawCoinId, network.id.value) + } else { + R.drawable.ic_alert_24 + } + } + is CryptoCurrency.Token -> R.drawable.ic_alert_24 + } \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/fragments/FragmentExt.kt b/core/ui/src/main/java/com/tangem/core/ui/extensions/Fragment.kt similarity index 92% rename from core/ui/src/main/java/com/tangem/core/ui/fragments/FragmentExt.kt rename to core/ui/src/main/java/com/tangem/core/ui/extensions/Fragment.kt index 0de62569bb..3a0c083a41 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/fragments/FragmentExt.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/extensions/Fragment.kt @@ -1,4 +1,4 @@ -package com.tangem.core.ui.fragments +package com.tangem.core.ui.extensions import android.view.WindowManager import androidx.annotation.ColorRes diff --git a/core/ui/src/main/java/com/tangem/core/ui/extensions/TextReference.kt b/core/ui/src/main/java/com/tangem/core/ui/extensions/TextReference.kt index 92df3daeb9..bc8107d5f5 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/extensions/TextReference.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/extensions/TextReference.kt @@ -20,12 +20,19 @@ sealed interface TextReference { * Text resource id * * @property id resource id - * @property formatArgs arguments - * - * Impossible to use [kotlinx.collections.immutable.ImmutableList] because [Any] is unstable. + * @property formatArgs arguments. Impossible to use [kotlinx.collections.immutable.ImmutableList] because [Any] is + * unstable. */ data class Res(@StringRes val id: Int, val formatArgs: WrappedList = WrappedList(emptyList())) : TextReference + /** + * Plural resource id + * + * @property id resource id + * @property count count + * @property formatArgs arguments. Impossible to use [kotlinx.collections.immutable.ImmutableList] because [Any] is + * unstable. + */ data class PluralRes(@PluralsRes val id: Int, val count: Int, val formatArgs: WrappedList) : TextReference /** @@ -34,9 +41,16 @@ sealed interface TextReference { * @property value value */ data class Str(val value: String) : TextReference + + /** + * Combined reference. It concatenates all [refs]. + * + * @see [TextReference.plus] method + */ + data class Combined(val refs: WrappedList) : TextReference } -/** Get text */ +/** Resolve [TextReference] as [String] */ @Composable @ReadOnlyComposable fun TextReference.resolveReference(): String { @@ -44,5 +58,23 @@ fun TextReference.resolveReference(): String { is TextReference.Res -> stringResource(id, *formatArgs.toTypedArray()) is TextReference.PluralRes -> pluralStringResource(id, count, *formatArgs.toTypedArray()) is TextReference.Str -> value + is TextReference.Combined -> { + buildString { + refs.forEach { + append(it.resolveReference()) + } + } + } + } +} + +/** Concatenate [this] reference with [ref] */ +operator fun TextReference.plus(ref: TextReference): TextReference { + return when (this) { + is TextReference.Combined -> copy(refs = (refs.data + ref).toWrappedList()) + is TextReference.PluralRes, + is TextReference.Res, + is TextReference.Str, + -> TextReference.Combined(refs = wrappedList(this, ref)) } } \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/extensions/WrappedList.kt b/core/ui/src/main/java/com/tangem/core/ui/extensions/WrappedList.kt index 6f529c9574..47be5e543d 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/extensions/WrappedList.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/extensions/WrappedList.kt @@ -7,4 +7,8 @@ import androidx.compose.runtime.Immutable */ @JvmInline @Immutable -value class WrappedList(val data: List) : List by data \ No newline at end of file +value class WrappedList(val data: List) : List by data + +fun List.toWrappedList(): WrappedList = WrappedList(data = this) + +fun wrappedList(vararg elements: T): WrappedList = WrappedList(data = listOf(*elements)) \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/fragments/ComposeActivity.kt b/core/ui/src/main/java/com/tangem/core/ui/fragments/ComposeActivity.kt deleted file mode 100644 index b14a8f3c9d..0000000000 --- a/core/ui/src/main/java/com/tangem/core/ui/fragments/ComposeActivity.kt +++ /dev/null @@ -1,11 +0,0 @@ -package com.tangem.core.ui.fragments - -import android.os.Bundle -import androidx.appcompat.app.AppCompatActivity - -abstract class ComposeActivity : AppCompatActivity(), ComposeScreen { - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setContentView(createComposeView(context = this)) - } -} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/fragments/ComposeFragment.kt b/core/ui/src/main/java/com/tangem/core/ui/fragments/ComposeFragment.kt deleted file mode 100644 index cde4de5275..0000000000 --- a/core/ui/src/main/java/com/tangem/core/ui/fragments/ComposeFragment.kt +++ /dev/null @@ -1,13 +0,0 @@ -package com.tangem.core.ui.fragments - -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.fragment.app.Fragment - -abstract class ComposeFragment : Fragment(), ComposeScreen { - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { - return createComposeView(inflater.context) - } -} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/fragments/ComposeScreen.kt b/core/ui/src/main/java/com/tangem/core/ui/fragments/ComposeScreen.kt deleted file mode 100644 index 987f29cd34..0000000000 --- a/core/ui/src/main/java/com/tangem/core/ui/fragments/ComposeScreen.kt +++ /dev/null @@ -1,44 +0,0 @@ -package com.tangem.core.ui.fragments - -import android.content.Context -import android.view.View -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.runtime.Composable -import androidx.compose.runtime.State -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.ComposeView -import com.tangem.core.ui.components.SystemBarsEffect -import com.tangem.core.ui.res.TangemTheme - -internal interface ComposeScreen { - fun createComposeView(context: Context): View { - return ComposeView(context).apply { - setContent { - TangemTheme { - val backgroundColor = TangemTheme.colors.background.primary - SystemBarsEffect { - setSystemBarsColor( - color = backgroundColor, - ) - } - - ScreenContent( - state = provideState().value, - modifier = Modifier - .fillMaxSize() - .background(color = backgroundColor), - ) - } - } - } - } - - @Suppress("TopLevelComposableFunctions") - @Composable - fun provideState(): State - - @Suppress("TopLevelComposableFunctions") - @Composable - fun ScreenContent(state: ScreenState, modifier: Modifier) -} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/res/TangemDimens.kt b/core/ui/src/main/java/com/tangem/core/ui/res/TangemDimens.kt index 1c60364fa2..b4fd692f7e 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/res/TangemDimens.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/res/TangemDimens.kt @@ -46,6 +46,7 @@ data class TangemDimens internal constructor( val size10: Dp = 10.dp, val size11: Dp = 11.dp, val size12: Dp = 12.dp, + val size14: Dp = 14.dp, val size16: Dp = 16.dp, val size18: Dp = 18.dp, val size20: Dp = 20.dp, diff --git a/core/ui/src/main/java/com/tangem/core/ui/screen/ComposeActivity.kt b/core/ui/src/main/java/com/tangem/core/ui/screen/ComposeActivity.kt new file mode 100644 index 0000000000..fe4f115dcd --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/screen/ComposeActivity.kt @@ -0,0 +1,17 @@ +package com.tangem.core.ui.screen + +import android.os.Bundle +import androidx.activity.ComponentActivity + +/** + * An abstract base class for activities that use Compose for UI rendering. + * Extends [ComponentActivity] and implements [ComposeScreen] interface. + */ +abstract class ComposeActivity : ComponentActivity(), ComposeScreen { + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + setContentView(createComposeView(context = this)) + } +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/fragments/ComposeBottomSheetFragment.kt b/core/ui/src/main/java/com/tangem/core/ui/screen/ComposeBottomSheetFragment.kt similarity index 55% rename from core/ui/src/main/java/com/tangem/core/ui/fragments/ComposeBottomSheetFragment.kt rename to core/ui/src/main/java/com/tangem/core/ui/screen/ComposeBottomSheetFragment.kt index 7e50223a02..c361732038 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/fragments/ComposeBottomSheetFragment.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/screen/ComposeBottomSheetFragment.kt @@ -1,7 +1,6 @@ -package com.tangem.core.ui.fragments +package com.tangem.core.ui.screen import android.app.Dialog -import android.content.Context import android.os.Bundle import android.view.LayoutInflater import android.view.View @@ -10,20 +9,46 @@ import androidx.annotation.FloatRange import androidx.compose.foundation.background import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.runtime.Composable +import androidx.compose.runtime.ReadOnlyComposable import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.ComposeView import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.bottomsheet.BottomSheetDialog import com.google.android.material.bottomsheet.BottomSheetDialogFragment import com.tangem.core.ui.R import com.tangem.core.ui.res.TangemTheme -abstract class ComposeBottomSheetFragment : BottomSheetDialogFragment(), ComposeScreen { +/** + * An abstract base class for bottom sheet dialogs that use Compose for UI rendering. + * Extends [BottomSheetDialogFragment] and implements [ComposeScreen] interface. + */ +abstract class ComposeBottomSheetFragment : BottomSheetDialogFragment(), ComposeScreen { + + /** + * The initial state of the bottom sheet. Default is [BottomSheetBehavior.STATE_EXPANDED]. + */ open val initialBottomSheetState = BottomSheetBehavior.STATE_EXPANDED + /** + * The fraction of the screen height that the bottom sheet should take when expanded. + * Default is `null`, indicating that the height will be determined by the content. + */ @FloatRange(from = 0.0, to = 1.0) open val expandedHeightFraction: Float? = null + override val screenModifier: Modifier + @Composable + @ReadOnlyComposable + get() = Modifier + .fillMaxWidth() + .let { + if (expandedHeightFraction != null) it.fillMaxHeight(expandedHeightFraction!!) else it + } + .background( + color = TangemTheme.colors.background.plain, + shape = TangemTheme.shapes.bottomSheet, + ) + override fun getTheme(): Int = R.style.AppTheme_TransparentBottomSheetDialog override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { @@ -40,25 +65,4 @@ abstract class ComposeBottomSheetFragment : BottomSheetDialogFragme return dialog } - - override fun createComposeView(context: Context): View { - return ComposeView(context).apply { - setContent { - TangemTheme { - ScreenContent( - state = provideState().value, - modifier = Modifier - .fillMaxWidth() - .let { - if (expandedHeightFraction != null) it.fillMaxHeight(expandedHeightFraction!!) else it - } - .background( - color = TangemTheme.colors.background.plain, - shape = TangemTheme.shapes.bottomSheet, - ), - ) - } - } - } - } } \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/screen/ComposeFragment.kt b/core/ui/src/main/java/com/tangem/core/ui/screen/ComposeFragment.kt new file mode 100644 index 0000000000..8167263503 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/screen/ComposeFragment.kt @@ -0,0 +1,37 @@ +package com.tangem.core.ui.screen + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.Fragment +import androidx.transition.TransitionInflater +import com.tangem.core.ui.R + +/** + * An abstract base class for fragments that use Compose for UI rendering. + * Extends [Fragment] and implements [ComposeScreen] interface. + */ +abstract class ComposeFragment : Fragment(), ComposeScreen { + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { + val isTransitionsInflated = TransitionInflater.from(requireContext()).inflateTransitions() + + return createComposeView(inflater.context).also { + it.isTransitionGroup = isTransitionsInflated + } + } + + /** + * Inflates transitions for the fragment. Override this method to customize + * enter and exit transitions for the fragment. + * + * @return `true` if transitions were inflated; `false` otherwise. + */ + protected open fun TransitionInflater.inflateTransitions(): Boolean { + enterTransition = inflateTransition(R.transition.slide_right) + exitTransition = inflateTransition(R.transition.fade) + + return true + } +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/screen/ComposeScreen.kt b/core/ui/src/main/java/com/tangem/core/ui/screen/ComposeScreen.kt new file mode 100644 index 0000000000..f2eade80ca --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/screen/ComposeScreen.kt @@ -0,0 +1,79 @@ +package com.tangem.core.ui.screen + +import android.content.Context +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.Composable +import androidx.compose.runtime.ReadOnlyComposable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.ComposeView +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.theme.AppThemeModeHolder +import com.tangem.domain.apptheme.model.AppThemeMode + +/** + * Interface representing a Compose screen with common theming and content composition properties. + * + * This interface defines properties and functions that allow a Compose screen to manage its theme, + * background color, and screen modifier. It also provides a composable function to define the content + * of the screen. + */ +internal interface ComposeScreen { + + /** + * The holder for managing the current application theme mode. + */ + val appThemeModeHolder: AppThemeModeHolder + + /** + * The screen modifier. + */ + val screenModifier: Modifier + @Composable + @ReadOnlyComposable + get() = Modifier.fillMaxSize() + + /** + * Composable function to define the content of the screen. + * + * @param modifier The modifier to apply to the screen content. + */ + @Suppress("TopLevelComposableFunctions") + @Composable + fun ScreenContent(modifier: Modifier) +} + +/** + * Creates a [ComposeView] with the defined content for the Compose screen. + * + * @param context The context. + * @return A [ComposeView] instance with the defined screen content. + */ +internal fun ComposeScreen.createComposeView(context: Context): ComposeView { + return ComposeView(context).apply { + setContent { + val appThemeMode by appThemeModeHolder.appThemeMode + + TangemTheme(isDark = shouldUseDarkTheme(appThemeMode)) { + ScreenContent(modifier = screenModifier) + } + } + } +} + +/** + * Determines whether the dark theme should be used based on the given [AppThemeMode]. + * + * @param appThemeMode The application theme mode. + * @return `true` if the dark theme should be used, `false` otherwise. + */ +@Composable +@ReadOnlyComposable +private fun shouldUseDarkTheme(appThemeMode: AppThemeMode): Boolean { + return when (appThemeMode) { + AppThemeMode.FORCE_DARK -> true + AppThemeMode.FORCE_LIGHT -> false + AppThemeMode.FOLLOW_SYSTEM -> isSystemInDarkTheme() + } +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/theme/AppThemeModeHolder.kt b/core/ui/src/main/java/com/tangem/core/ui/theme/AppThemeModeHolder.kt new file mode 100644 index 0000000000..a1b25707de --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/theme/AppThemeModeHolder.kt @@ -0,0 +1,17 @@ +package com.tangem.core.ui.theme + +import androidx.compose.runtime.Stable +import androidx.compose.runtime.State +import com.tangem.domain.apptheme.model.AppThemeMode + +/** + * Representing a holder for the application theme mode. + */ +@Stable +interface AppThemeModeHolder { + + /** + * A [State] representing the current application theme mode. + */ + val appThemeMode: State +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/utils/BigDecimalFormatter.kt b/core/ui/src/main/java/com/tangem/core/ui/utils/BigDecimalFormatter.kt index 2a78274eb5..db128105fb 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/utils/BigDecimalFormatter.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/utils/BigDecimalFormatter.kt @@ -12,22 +12,14 @@ object BigDecimalFormatter { private const val TEMP_CURRENCY_CODE = "USD" - fun formatCryptoAmount( - cryptoAmount: BigDecimal, - cryptoCurrency: String, - decimals: Int, - locale: Locale = Locale.getDefault(), - ): String { - val formatterCurrency = getCurrency(cryptoCurrency) - val formatter = NumberFormat.getCurrencyInstance(locale).apply { - currency = formatterCurrency + fun formatCryptoAmount(cryptoAmount: BigDecimal, cryptoCurrency: String, decimals: Int): String { + val formatter = NumberFormat.getNumberInstance().apply { maximumFractionDigits = decimals.coerceAtMost(maximumValue = 8) minimumFractionDigits = 2 roundingMode = RoundingMode.DOWN } - return formatter.format(cryptoAmount) - .replace(formatterCurrency.getSymbol(locale), cryptoCurrency) + return formatter.format(cryptoAmount) + "\u2009$cryptoCurrency" } fun formatFiatAmount( diff --git a/features/wallet/impl/src/main/res/drawable/ic_currency_24.xml b/core/ui/src/main/res/drawable/ic_currency_24.xml similarity index 100% rename from features/wallet/impl/src/main/res/drawable/ic_currency_24.xml rename to core/ui/src/main/res/drawable/ic_currency_24.xml diff --git a/app/src/main/res/drawable-ldpi/ic_qcx.webp b/core/ui/src/main/res/drawable/ic_qcx.webp similarity index 100% rename from app/src/main/res/drawable-ldpi/ic_qcx.webp rename to core/ui/src/main/res/drawable/ic_qcx.webp diff --git a/app/src/main/res/drawable/ic_voyr.webp b/core/ui/src/main/res/drawable/ic_voyr.webp similarity index 100% rename from app/src/main/res/drawable/ic_voyr.webp rename to core/ui/src/main/res/drawable/ic_voyr.webp diff --git a/data/app-currency/src/main/kotlin/com/tangem/data/appcurrency/DefaultAppCurrencyRepository.kt b/data/app-currency/src/main/kotlin/com/tangem/data/appcurrency/DefaultAppCurrencyRepository.kt index 8a780ba151..c2fe02a2b5 100644 --- a/data/app-currency/src/main/kotlin/com/tangem/data/appcurrency/DefaultAppCurrencyRepository.kt +++ b/data/app-currency/src/main/kotlin/com/tangem/data/appcurrency/DefaultAppCurrencyRepository.kt @@ -10,9 +10,9 @@ import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.appcurrency.repository.AppCurrencyRepository import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.channelFlow import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.onEmpty +import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.joda.time.Duration import timber.log.Timber @@ -27,11 +27,18 @@ internal class DefaultAppCurrencyRepository( private val appCurrencyConverter = AppCurrencyConverter() - override fun getSelectedAppCurrency(): Flow { - return selectedAppCurrencyStore.get() - .onEmpty { fetchDefaultAppCurrency() } - .map(appCurrencyConverter::convert) - .flowOn(dispatchers.io) + override fun getSelectedAppCurrency(): Flow = channelFlow { + launch(dispatchers.io) { + selectedAppCurrencyStore.get() + .map(appCurrencyConverter::convert) + .collect(::send) + } + + launch(dispatchers.io) { + if (selectedAppCurrencyStore.isEmpty()) { + fetchDefaultAppCurrency() + } + } } override suspend fun getAvailableAppCurrencies(): List { diff --git a/data/app-theme/.gitignore b/data/app-theme/.gitignore new file mode 100644 index 0000000000..796b96d1c4 --- /dev/null +++ b/data/app-theme/.gitignore @@ -0,0 +1 @@ +/build diff --git a/data/app-theme/build.gradle.kts b/data/app-theme/build.gradle.kts new file mode 100644 index 0000000000..b0f1f17a6a --- /dev/null +++ b/data/app-theme/build.gradle.kts @@ -0,0 +1,34 @@ +plugins { + alias(deps.plugins.android.library) + alias(deps.plugins.kotlin.android) + alias(deps.plugins.kotlin.kapt) + id("configuration") +} + +android { + namespace = "com.tangem.data.apptheme" +} + +dependencies { + + /** Project - Domain */ + implementation(projects.domain.core) + implementation(projects.domain.appTheme) + implementation(projects.domain.appTheme.models) + + /** Project - Data */ + implementation(projects.core.datasource) + implementation(projects.data.common) + + /** Project - Utils */ + implementation(projects.core.utils) + + /** DI */ + implementation(deps.hilt.core) + kapt(deps.hilt.kapt) + + /** Other */ + implementation(deps.kotlin.coroutines) + implementation(deps.timber) + implementation(deps.jodatime) +} \ No newline at end of file diff --git a/data/app-theme/src/main/kotlin/com/tangem/data/apptheme/MockAppThemeModeRepository.kt b/data/app-theme/src/main/kotlin/com/tangem/data/apptheme/MockAppThemeModeRepository.kt new file mode 100644 index 0000000000..a4a885aab2 --- /dev/null +++ b/data/app-theme/src/main/kotlin/com/tangem/data/apptheme/MockAppThemeModeRepository.kt @@ -0,0 +1,19 @@ +package com.tangem.data.apptheme + +import com.tangem.domain.apptheme.model.AppThemeMode +import com.tangem.domain.apptheme.repository.AppThemeModeRepository +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow + +internal class MockAppThemeModeRepository : AppThemeModeRepository { + + private val appThemeModeFlow = MutableStateFlow(AppThemeMode.DEFAULT) + + override fun getAppThemeMode(): Flow { + return appThemeModeFlow + } + + override suspend fun changeAppThemeMode(mode: AppThemeMode) { + appThemeModeFlow.value = mode + } +} \ No newline at end of file diff --git a/data/app-theme/src/main/kotlin/com/tangem/data/apptheme/di/AppThemeModeDataModule.kt b/data/app-theme/src/main/kotlin/com/tangem/data/apptheme/di/AppThemeModeDataModule.kt new file mode 100644 index 0000000000..6847fad0b8 --- /dev/null +++ b/data/app-theme/src/main/kotlin/com/tangem/data/apptheme/di/AppThemeModeDataModule.kt @@ -0,0 +1,20 @@ +package com.tangem.data.apptheme.di + +import com.tangem.data.apptheme.MockAppThemeModeRepository +import com.tangem.domain.apptheme.repository.AppThemeModeRepository +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal object AppThemeModeDataModule { + + @Provides + @Singleton + fun provideAppThemeModeRepository(): AppThemeModeRepository { + return MockAppThemeModeRepository() + } +} \ No newline at end of file diff --git a/data/settings/src/main/java/com/tangem/data/settings/DefaultSettingsRepository.kt b/data/settings/src/main/java/com/tangem/data/settings/DefaultSettingsRepository.kt index c1ee6fc57e..c4d0074263 100644 --- a/data/settings/src/main/java/com/tangem/data/settings/DefaultSettingsRepository.kt +++ b/data/settings/src/main/java/com/tangem/data/settings/DefaultSettingsRepository.kt @@ -15,4 +15,8 @@ internal class DefaultSettingsRepository( preferencesDataSource.appRatingLaunchObserver.isReadyToShow() } } + + override suspend fun shouldShowSaveUserWalletScreen(): Boolean { + return withContext(dispatchers.io) { preferencesDataSource.shouldShowSaveUserWalletScreen } + } } \ No newline at end of file diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt index 36abfd1d54..0df288c0c0 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt @@ -8,8 +8,8 @@ import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.api.tangemTech.models.UserTokensResponse import com.tangem.datasource.local.token.UserTokensStore import com.tangem.datasource.local.userwallet.UserWalletsStore -import com.tangem.domain.core.error.DataError import com.tangem.domain.common.util.derivationStyleProvider +import com.tangem.domain.core.error.DataError import com.tangem.domain.demo.DemoConfig import com.tangem.domain.tokens.models.CryptoCurrency import com.tangem.domain.tokens.repository.CurrenciesRepository @@ -53,6 +53,22 @@ internal class DefaultCurrenciesRepository( storeAndPushTokens(userWalletId, response) } + override suspend fun removeCurrency(userWalletId: UserWalletId, currency: CryptoCurrency) = + withContext(dispatchers.io) { + val savedCurrencies = requireNotNull( + value = userTokensStore.getSyncOrNull(userWalletId), + lazyMessage = { "Saved tokens empty. Can not perform remove currency action" }, + ) + + val token = userTokensResponseFactory.createResponseToken(currency) + storeAndPushTokens( + userWalletId = userWalletId, + response = savedCurrencies.copy( + tokens = savedCurrencies.tokens.filter { it != token }, + ), + ) + } + override suspend fun getSingleCurrencyWalletPrimaryCurrency(userWalletId: UserWalletId): CryptoCurrency { return withContext(dispatchers.io) { val userWallet = getUserWallet(userWalletId) @@ -62,20 +78,38 @@ internal class DefaultCurrenciesRepository( } } - override fun getMultiCurrencyWalletCurrencies( + override fun getMultiCurrencyWalletCurrenciesUpdates(userWalletId: UserWalletId): Flow> { + return channelFlow { + val userWallet = getUserWallet(userWalletId) + ensureIsCorrectUserWallet(userWallet, isMultiCurrencyWalletExpected = true) + + launch(dispatchers.io) { + getMultiCurrencyWalletCurrencies(userWallet).collect(::send) + } + + launch(dispatchers.io) { + fetchTokensIfCacheExpired(userWallet, refresh = false) + } + } + } + + override suspend fun getMultiCurrencyWalletCurrenciesSync( userWalletId: UserWalletId, refresh: Boolean, - ): Flow> = channelFlow { + ): List { val userWallet = getUserWallet(userWalletId) ensureIsCorrectUserWallet(userWallet, isMultiCurrencyWalletExpected = true) - launch(dispatchers.io) { - getMultiCurrencyWalletCurrencies(userWallet).collect(::send) + fetchTokensIfCacheExpired(userWallet, refresh) + + val storedTokens = requireNotNull(userTokensStore.getSyncOrNull(userWallet.walletId)) { + "Unable to find tokens response for user wallet with provided ID: $userWalletId" } - launch(dispatchers.io) { - fetchTokensIfCacheExpired(userWallet, refresh) - } + return responseCurrenciesFactory.createCurrencies( + response = storedTokens, + card = userWallet.scanResponse.card, + ) } override suspend fun getMultiCurrencyWalletCurrency( @@ -164,7 +198,7 @@ internal class DefaultCurrenciesRepository( tangemTechApi.saveUserTokens(userWallet.walletId.stringValue, response) } else { - throw error + Timber.e(error, "Unable to fetch currencies for: ${userWallet.walletId}") } } diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultNetworksRepository.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultNetworksRepository.kt index e9e235c8d1..5ac3840c7f 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultNetworksRepository.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultNetworksRepository.kt @@ -16,14 +16,8 @@ import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.models.UserWalletId import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.extensions.addOrReplace -import kotlinx.coroutines.async -import kotlinx.coroutines.awaitAll -import kotlinx.coroutines.coroutineScope -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.channelFlow -import kotlinx.coroutines.flow.update -import kotlinx.coroutines.launch +import kotlinx.coroutines.* +import kotlinx.coroutines.flow.* internal class DefaultNetworksRepository( private val walletManagersFacade: WalletManagersFacade, @@ -45,10 +39,9 @@ internal class DefaultNetworksRepository( return networkConverter.convertSet(networksIds) } - override fun getNetworkStatuses( + override fun getNetworkStatusesUpdates( userWalletId: UserWalletId, networks: Set, - refresh: Boolean, ): Flow> = channelFlow { launch(dispatchers.io) { networksStatuses.collect { @@ -57,10 +50,19 @@ internal class DefaultNetworksRepository( } launch(dispatchers.io) { - fetchNetworksStatusesIfCacheExpired(userWalletId, networks, refresh) + fetchNetworksStatusesIfCacheExpired(userWalletId, networks, refresh = false) } } + override suspend fun getNetworkStatusesSync( + userWalletId: UserWalletId, + networks: Set, + refresh: Boolean, + ): Set = withContext(dispatchers.io) { + fetchNetworksStatusesIfCacheExpired(userWalletId, networks, refresh) + networksStatuses.first().toSet() + } + private suspend fun fetchNetworksStatusesIfCacheExpired( userWalletId: UserWalletId, networks: Set, @@ -88,7 +90,7 @@ internal class DefaultNetworksRepository( private suspend fun fetchNetworkStatus(userWalletId: UserWalletId, networkId: Network.ID) { val currencies = getCurrencies(userWalletId) .asSequence() - .filter { it.networkId == networkId } + .filter { it.network.id == networkId } val result = walletManagersFacade.update( userWalletId = userWalletId, diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultQuotesRepository.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultQuotesRepository.kt index 25fb880a50..a288240656 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultQuotesRepository.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultQuotesRepository.kt @@ -9,11 +9,9 @@ import com.tangem.domain.tokens.models.CryptoCurrency import com.tangem.domain.tokens.models.Quote import com.tangem.domain.tokens.repository.QuotesRepository import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.channelFlow -import kotlinx.coroutines.flow.collectLatest -import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import timber.log.Timber internal class DefaultQuotesRepository( @@ -28,7 +26,7 @@ internal class DefaultQuotesRepository( private var quotesFetchedForAppCurrency: String? = null - override fun getQuotes(currenciesIds: Set, refresh: Boolean): Flow> { + override fun getQuotesUpdates(currenciesIds: Set): Flow> { return channelFlow { launch(dispatchers.io) { quotesStore.get(currenciesIds) @@ -38,12 +36,26 @@ internal class DefaultQuotesRepository( launch(dispatchers.io) { selectedAppCurrencyStore.get().collectLatest { appCurrency -> - fetchExpiredQuotes(currenciesIds, appCurrency.id, refresh) + fetchExpiredQuotes(currenciesIds, appCurrency.id, refresh = false) } } } } + override suspend fun getQuotesSync(currenciesIds: Set, refresh: Boolean): Set { + return withContext(dispatchers.io) { + val selectedAppCurrency = requireNotNull(selectedAppCurrencyStore.getSyncOrNull()) { + "Unable to get selected application currency to update quotes" + } + + fetchExpiredQuotes(currenciesIds, selectedAppCurrency.id, refresh) + + val quotes = quotesStore.get(currenciesIds).first() + + quotesConverter.convertSet(quotes) + } + } + private suspend fun fetchExpiredQuotes( currenciesIds: Set, appCurrencyId: String, diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/CryptoCurrencyFactory.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/CryptoCurrencyFactory.kt index f4e0148b34..e6fff3176f 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/CryptoCurrencyFactory.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/CryptoCurrencyFactory.kt @@ -1,11 +1,12 @@ package com.tangem.data.tokens.utils import com.tangem.blockchain.common.Blockchain -import com.tangem.blockchain.common.Token as SdkToken import com.tangem.domain.common.DerivationStyleProvider import com.tangem.domain.tokens.models.CryptoCurrency import timber.log.Timber +import com.tangem.blockchain.common.Token as SdkToken +// FIXME: Make internal class CryptoCurrencyFactory { fun createToken( @@ -19,9 +20,10 @@ class CryptoCurrencyFactory { } val id = getTokenId(blockchain, sdkToken) + return CryptoCurrency.Token( id = id, - networkId = getNetworkId(blockchain), + network = getNetwork(blockchain) ?: return null, name = sdkToken.name, symbol = sdkToken.symbol, iconUrl = getTokenIconUrl(blockchain, sdkToken), @@ -29,8 +31,6 @@ class CryptoCurrencyFactory { isCustom = isCustomToken(id), contractAddress = sdkToken.contractAddress, derivationPath = getDerivationPath(blockchain, derivationStyleProvider), - blockchainName = blockchain.fullName, - standardType = getTokenStandardType(blockchain, sdkToken), ) } @@ -42,7 +42,7 @@ class CryptoCurrencyFactory { return CryptoCurrency.Coin( id = getCoinId(blockchain), - networkId = getNetworkId(blockchain), + network = getNetwork(blockchain) ?: return null, name = blockchain.fullName, symbol = blockchain.currency, iconUrl = getCoinIconUrl(blockchain), diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/NetworkConverter.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/NetworkConverter.kt index 23e27fe466..6c57906a38 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/NetworkConverter.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/NetworkConverter.kt @@ -3,22 +3,13 @@ package com.tangem.data.tokens.utils import com.tangem.blockchain.common.Blockchain import com.tangem.domain.tokens.models.Network import com.tangem.utils.converter.Converter -import timber.log.Timber internal class NetworkConverter : Converter { override fun convert(value: Network.ID): Network? { val blockchain = Blockchain.fromId(value.value) - if (blockchain == Blockchain.Unknown) { - Timber.e("Unable to convert Unknown blockchain to the domain network model") - return null - } - - return Network( - id = value, - name = blockchain.fullName, - ) + return getNetwork(blockchain) } override fun convertList(input: Collection): List { diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/NetworkOperations.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/NetworkOperations.kt new file mode 100644 index 0000000000..3a536a46ed --- /dev/null +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/NetworkOperations.kt @@ -0,0 +1,29 @@ +package com.tangem.data.tokens.utils + +import com.tangem.blockchain.common.Blockchain +import com.tangem.domain.tokens.models.Network +import timber.log.Timber + +internal fun getNetwork(blockchain: Blockchain): Network? { + if (blockchain == Blockchain.Unknown) { + Timber.e("Unable to convert Unknown blockchain to the domain network model") + return null + } + + return Network( + id = Network.ID(blockchain.id), + name = blockchain.fullName, + isTestnet = blockchain.isTestnet(), + standardType = getNetworkStandardType(blockchain), + ) +} + +private fun getNetworkStandardType(blockchain: Blockchain): Network.StandardType { + return when (blockchain) { + Blockchain.Ethereum, Blockchain.EthereumTestnet -> Network.StandardType.ERC20 + Blockchain.BSC, Blockchain.BSCTestnet -> Network.StandardType.BEP20 + Blockchain.Binance, Blockchain.BinanceTestnet -> Network.StandardType.BEP2 + Blockchain.Tron, Blockchain.TronTestnet -> Network.StandardType.TRC20 + else -> Network.StandardType.Unspecified(blockchain.name) + } +} \ No newline at end of file diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/NetworkStatusFactory.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/NetworkStatusFactory.kt index 6feee38172..0127ed7330 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/NetworkStatusFactory.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/NetworkStatusFactory.kt @@ -1,10 +1,14 @@ package com.tangem.data.tokens.utils +import com.tangem.domain.tokens.model.NetworkAddress import com.tangem.domain.tokens.model.NetworkStatus +import com.tangem.domain.tokens.model.PendingTransaction import com.tangem.domain.tokens.models.CryptoCurrency import com.tangem.domain.tokens.models.Network import com.tangem.domain.walletmanager.model.CryptoCurrencyAmount +import com.tangem.domain.walletmanager.model.CryptoCurrencyTransaction import com.tangem.domain.walletmanager.model.UpdateWalletManagerResult +import timber.log.Timber import java.math.BigDecimal internal class NetworkStatusFactory { @@ -19,10 +23,18 @@ internal class NetworkStatusFactory { value = when (result) { is UpdateWalletManagerResult.MissedDerivation -> NetworkStatus.MissedDerivation is UpdateWalletManagerResult.Unreachable -> NetworkStatus.Unreachable - is UpdateWalletManagerResult.NoAccount -> NetworkStatus.NoAccount(result.amountToCreateAccount) + is UpdateWalletManagerResult.NoAccount -> NetworkStatus.NoAccount( + address = getNetworkAddress(result.defaultAddress, result.addresses), + amountToCreateAccount = result.amountToCreateAccount, + ) is UpdateWalletManagerResult.Verified -> NetworkStatus.Verified( - amounts = formatAmounts(result.tokensAmounts, currencies), - hasTransactionsInProgress = result.hasTransactionsInProgress, + address = getNetworkAddress(result.defaultAddress, result.addresses), + amounts = formatAmounts(result.currenciesAmounts, currencies), + pendingTransactions = formatTransactions( + networksAddresses = result.addresses, + transactions = result.currentTransactions, + currencies = currencies, + ), ) }, ) @@ -39,13 +51,91 @@ internal class NetworkStatusFactory { is CryptoCurrencyAmount.Coin -> currencies.singleOrNull { it is CryptoCurrency.Coin } is CryptoCurrencyAmount.Token -> currencies.firstOrNull { it is CryptoCurrency.Token && - it.id.rawCurrencyId == amount.id && + it.id.rawCurrencyId == amount.tokenId && it.contractAddress == amount.tokenContractAddress } } - currency?.id?.let { it to amount.value } + if (currency == null) { + Timber.e("Unable to find cryptocurrency for amount: $amount") + null + } else { + currency.id to amount.value + } } .toMap() } + + private fun formatTransactions( + networksAddresses: Set, + transactions: Set, + currencies: Set, + ): Map> { + if (transactions.isEmpty()) return emptyMap() + + return currencies + .asSequence() + .map { currency -> + val currencyTransactions = when (currency) { + is CryptoCurrency.Coin -> transactions.filterTo(hashSetOf()) { transaction -> + transaction is CryptoCurrencyTransaction.Coin + } + is CryptoCurrency.Token -> transactions.filterTo(hashSetOf()) { transaction -> + transaction is CryptoCurrencyTransaction.Token && + transaction.tokenId == currency.id.rawCurrencyId && + transaction.tokenContractAddress == currency.contractAddress + } + } + + currency.id to createCurrentTransactions(networksAddresses, currencyTransactions) + } + .toMap() + } + + private fun createCurrentTransactions( + networksAddresses: Set, + transactions: Set, + ): Set { + return transactions.mapNotNullTo(hashSetOf()) { createCurrentTransaction(networksAddresses, it) } + } + + private fun createCurrentTransaction( + networksAddresses: Set, + transaction: CryptoCurrencyTransaction, + ): PendingTransaction? { + val direction = when { + transaction.toAddress in networksAddresses -> PendingTransaction.Direction.Incoming( + fromAddress = transaction.fromAddress, + ) + transaction.fromAddress in networksAddresses -> PendingTransaction.Direction.Outgoing( + toAddress = transaction.toAddress, + ) + else -> { + Timber.e( + """ + Unable to find transaction direction + |- To address: ${transaction.toAddress} + |- From address: ${transaction.fromAddress} + |- Network addresses: $networksAddresses + """.trimIndent(), + ) + + return null + } + } + + return PendingTransaction( + amount = transaction.amount, + direction = direction, + sentAt = transaction.sentAt, + ) + } + + private fun getNetworkAddress(defaultAddress: String, availableAddresses: Set): NetworkAddress { + return if (availableAddresses.size != 1) { + NetworkAddress.Selectable(defaultAddress, availableAddresses) + } else { + NetworkAddress.Single(defaultAddress) + } + } } \ No newline at end of file diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/ResponseCurrenciesFactory.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/ResponseCurrenciesFactory.kt index b79f32f515..f3e7832865 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/ResponseCurrenciesFactory.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/ResponseCurrenciesFactory.kt @@ -59,10 +59,10 @@ internal class ResponseCurrenciesFactory(private val demoConfig: DemoConfig) { } } - private fun createCoin(blockchain: Blockchain, responseToken: UserTokensResponse.Token): CryptoCurrency.Coin { + private fun createCoin(blockchain: Blockchain, responseToken: UserTokensResponse.Token): CryptoCurrency.Coin? { return CryptoCurrency.Coin( id = getCoinId(blockchain), - networkId = getNetworkId(blockchain), + network = getNetwork(blockchain) ?: return null, name = responseToken.name, symbol = responseToken.symbol, decimals = responseToken.decimals, @@ -71,12 +71,12 @@ internal class ResponseCurrenciesFactory(private val demoConfig: DemoConfig) { ) } - private fun createToken(blockchain: Blockchain, sdkToken: Token, derivationPath: String?): CryptoCurrency.Token { + private fun createToken(blockchain: Blockchain, sdkToken: Token, derivationPath: String?): CryptoCurrency.Token? { val id = getTokenId(blockchain, sdkToken) return CryptoCurrency.Token( id = id, - networkId = getNetworkId(blockchain), + network = getNetwork(blockchain) ?: return null, name = sdkToken.name, symbol = sdkToken.symbol, decimals = sdkToken.decimals, @@ -84,8 +84,6 @@ internal class ResponseCurrenciesFactory(private val demoConfig: DemoConfig) { iconUrl = getTokenIconUrl(blockchain, sdkToken), contractAddress = sdkToken.contractAddress, isCustom = isCustomToken(id), - blockchainName = blockchain.fullName, - standardType = getTokenStandardType(blockchain, sdkToken), ) } } \ No newline at end of file diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/TokensOperations.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/TokensOperations.kt index 2298a28fb2..a95d6efa08 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/TokensOperations.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/TokensOperations.kt @@ -5,7 +5,6 @@ import com.tangem.blockchain.common.IconsUtil import com.tangem.domain.common.DerivationStyleProvider import com.tangem.domain.common.extensions.toCoinId import com.tangem.domain.common.extensions.toNetworkId -import com.tangem.domain.tokens.models.CryptoCurrency import com.tangem.domain.tokens.models.CryptoCurrency.ID import com.tangem.domain.tokens.models.Network import com.tangem.blockchain.common.Token as SdkToken @@ -31,12 +30,6 @@ internal fun getBlockchain(networkId: Network.ID): Blockchain { return Blockchain.fromId(networkId.value) } -internal fun getNetworkId(blockchain: Blockchain): Network.ID { - val value = blockchain.id - - return Network.ID(value) -} - internal fun getCoinId(blockchain: Blockchain): ID { return getTokenOrCoinId(blockchain, token = null) } @@ -45,16 +38,6 @@ internal fun getTokenId(blockchain: Blockchain, token: SdkToken): ID { return getTokenOrCoinId(blockchain, token) } -internal fun getTokenStandardType(blockchain: Blockchain, token: SdkToken): CryptoCurrency.StandardType { - return when (blockchain) { - Blockchain.Ethereum, Blockchain.EthereumTestnet -> CryptoCurrency.StandardType.ERC20 - Blockchain.BSC, Blockchain.BSCTestnet -> CryptoCurrency.StandardType.BEP20 - Blockchain.Binance, Blockchain.BinanceTestnet -> CryptoCurrency.StandardType.BEP2 - Blockchain.Tron, Blockchain.TronTestnet -> CryptoCurrency.StandardType.TRC20 - else -> CryptoCurrency.StandardType.Unspecified(token.name) - } -} - internal fun getTokenIconUrl(blockchain: Blockchain, token: SdkToken): String? { val tokenId = token.id @@ -83,7 +66,7 @@ private fun getTokenOrCoinId(blockchain: Blockchain, token: SdkToken?): ID { else -> TOKEN_ID_PREFIX to CurrencyIdSuffix(rawId = sdkTokenId) } - return ID(prefix, getNetworkId(blockchain), suffix) + return ID(prefix, Network.ID(blockchain.id), suffix) } private fun getTokenIconUrlFromDefaultHost(tokenId: String): String { diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/UserTokensResponseFactory.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/UserTokensResponseFactory.kt index d112ad4476..c6cbc7f858 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/UserTokensResponseFactory.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/UserTokensResponseFactory.kt @@ -26,8 +26,8 @@ internal class UserTokensResponseFactory { ) } - private fun createResponseToken(currency: CryptoCurrency): UserTokensResponse.Token { - val blockchain = getBlockchain(currency.networkId) + fun createResponseToken(currency: CryptoCurrency): UserTokensResponse.Token { + val blockchain = getBlockchain(currency.network.id) return UserTokensResponse.Token( id = currency.id.rawCurrencyId, diff --git a/data/wallets/.gitignore b/data/wallets/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/data/wallets/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/data/wallets/build.gradle.kts b/data/wallets/build.gradle.kts new file mode 100644 index 0000000000..7bc383e45b --- /dev/null +++ b/data/wallets/build.gradle.kts @@ -0,0 +1,21 @@ +plugins { + alias(deps.plugins.android.library) + alias(deps.plugins.kotlin.android) + alias(deps.plugins.kotlin.kapt) + alias(deps.plugins.hilt.android) + id("configuration") +} + +android { + namespace = "com.tangem.data.wallet" +} + +dependencies { + implementation(projects.core.utils) + implementation(projects.data.source.preferences) + implementation(projects.domain.wallets) + + /** DI */ + implementation(deps.hilt.android) + kapt(deps.hilt.kapt) +} \ No newline at end of file diff --git a/data/wallets/src/main/java/com/tangem/data/wallets/DefaultWalletsRepository.kt b/data/wallets/src/main/java/com/tangem/data/wallets/DefaultWalletsRepository.kt new file mode 100644 index 0000000000..848735ca16 --- /dev/null +++ b/data/wallets/src/main/java/com/tangem/data/wallets/DefaultWalletsRepository.kt @@ -0,0 +1,16 @@ +package com.tangem.data.wallets + +import com.tangem.data.source.preferences.PreferencesDataSource +import com.tangem.domain.wallets.repository.WalletsRepository +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.withContext + +internal class DefaultWalletsRepository( + private val preferencesDataSource: PreferencesDataSource, + private val dispatchers: CoroutineDispatcherProvider, +) : WalletsRepository { + + override suspend fun shouldSaveUserWallets(): Boolean { + return withContext(dispatchers.io) { preferencesDataSource.shouldSaveUserWallets } + } +} \ No newline at end of file diff --git a/data/wallets/src/main/java/com/tangem/data/wallets/di/WalletsDataModule.kt b/data/wallets/src/main/java/com/tangem/data/wallets/di/WalletsDataModule.kt new file mode 100644 index 0000000000..3f418202cb --- /dev/null +++ b/data/wallets/src/main/java/com/tangem/data/wallets/di/WalletsDataModule.kt @@ -0,0 +1,28 @@ +package com.tangem.data.wallets.di + +import com.tangem.data.source.preferences.PreferencesDataSource +import com.tangem.data.wallets.DefaultWalletsRepository +import com.tangem.domain.wallets.repository.WalletsRepository +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +object WalletsDataModule { + + @Provides + @Singleton + fun providesWalletsRepository( + preferencesDataSource: PreferencesDataSource, + coroutineDispatcherProvider: CoroutineDispatcherProvider, + ): WalletsRepository { + return DefaultWalletsRepository( + preferencesDataSource = preferencesDataSource, + dispatchers = coroutineDispatcherProvider, + ) + } +} \ No newline at end of file diff --git a/domain/app-theme/.gitignore b/domain/app-theme/.gitignore new file mode 100644 index 0000000000..796b96d1c4 --- /dev/null +++ b/domain/app-theme/.gitignore @@ -0,0 +1 @@ +/build diff --git a/domain/app-theme/build.gradle.kts b/domain/app-theme/build.gradle.kts new file mode 100644 index 0000000000..79f941dbbb --- /dev/null +++ b/domain/app-theme/build.gradle.kts @@ -0,0 +1,12 @@ +plugins { + alias(deps.plugins.kotlin.jvm) + id("configuration") +} + +dependencies { + + /** Project - Domain */ + implementation(projects.core.utils) + implementation(projects.domain.core) + implementation(projects.domain.appTheme.models) +} \ No newline at end of file diff --git a/domain/app-theme/models/.gitignore b/domain/app-theme/models/.gitignore new file mode 100644 index 0000000000..796b96d1c4 --- /dev/null +++ b/domain/app-theme/models/.gitignore @@ -0,0 +1 @@ +/build diff --git a/domain/app-theme/models/build.gradle.kts b/domain/app-theme/models/build.gradle.kts new file mode 100644 index 0000000000..7ff7fb7522 --- /dev/null +++ b/domain/app-theme/models/build.gradle.kts @@ -0,0 +1,4 @@ +plugins { + alias(deps.plugins.kotlin.jvm) + id("configuration") +} \ No newline at end of file diff --git a/domain/app-theme/models/src/main/kotlin/com/tangem/domain/apptheme/model/AppThemeMode.kt b/domain/app-theme/models/src/main/kotlin/com/tangem/domain/apptheme/model/AppThemeMode.kt new file mode 100644 index 0000000000..f6ff6269f7 --- /dev/null +++ b/domain/app-theme/models/src/main/kotlin/com/tangem/domain/apptheme/model/AppThemeMode.kt @@ -0,0 +1,30 @@ +package com.tangem.domain.apptheme.model + +/** + * Enumerates the possible modes for the application's theme. + */ +enum class AppThemeMode { + /** + * Forces the dark theme mode regardless of system settings. + */ + FORCE_DARK, + + /** + * Forces the light theme mode regardless of system settings. + */ + FORCE_LIGHT, + + /** + * Follows the system-wide theme mode. + */ + FOLLOW_SYSTEM, + + ; + + companion object { + /** + * The default [AppThemeMode]. + */ + val DEFAULT: AppThemeMode = FORCE_LIGHT + } +} \ No newline at end of file diff --git a/domain/app-theme/src/main/kotlin/com/tangem/domain/apptheme/ChangeAppThemeModeUseCase.kt b/domain/app-theme/src/main/kotlin/com/tangem/domain/apptheme/ChangeAppThemeModeUseCase.kt new file mode 100644 index 0000000000..718f75a1a3 --- /dev/null +++ b/domain/app-theme/src/main/kotlin/com/tangem/domain/apptheme/ChangeAppThemeModeUseCase.kt @@ -0,0 +1,31 @@ +package com.tangem.domain.apptheme + +import arrow.core.Either +import arrow.core.raise.catch +import arrow.core.raise.either +import com.tangem.domain.apptheme.error.AppThemeModeError +import com.tangem.domain.apptheme.model.AppThemeMode +import com.tangem.domain.apptheme.repository.AppThemeModeRepository + +/** + * Use case responsible for changing the application theme mode. + * + * @property appThemeModeRepository The repository providing access to theme mode settings. + */ +class ChangeAppThemeModeUseCase( + private val appThemeModeRepository: AppThemeModeRepository, +) { + + /** + * Changes the application theme mode. + * + * @param mode The new [AppThemeMode] to set. + * @return An [Either] instance. The right side contains a [Unit] value indicating success, + * and the left side contains any [AppThemeModeError] that occurred during the process. + */ + suspend operator fun invoke(mode: AppThemeMode): Either = either { + catch({ appThemeModeRepository.changeAppThemeMode(mode) }) { + raise(AppThemeModeError.DataError(it)) + } + } +} \ No newline at end of file diff --git a/domain/app-theme/src/main/kotlin/com/tangem/domain/apptheme/GetAppThemeModeUseCase.kt b/domain/app-theme/src/main/kotlin/com/tangem/domain/apptheme/GetAppThemeModeUseCase.kt new file mode 100644 index 0000000000..171209c0ba --- /dev/null +++ b/domain/app-theme/src/main/kotlin/com/tangem/domain/apptheme/GetAppThemeModeUseCase.kt @@ -0,0 +1,33 @@ +package com.tangem.domain.apptheme + +import arrow.core.Either +import arrow.core.left +import arrow.core.right +import com.tangem.domain.apptheme.error.AppThemeModeError +import com.tangem.domain.apptheme.model.AppThemeMode +import com.tangem.domain.apptheme.repository.AppThemeModeRepository +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.map + +/** + * Use case responsible for retrieving the current application theme mode. + * + * @property appThemeModeRepository The repository providing access to theme mode settings. + */ +class GetAppThemeModeUseCase( + private val appThemeModeRepository: AppThemeModeRepository, +) { + + /** + * Invokes the use case to retrieve the current application theme mode. + * + * @return A [Flow] emitting an [Either] instance. The right side contains the retrieved + * [AppThemeMode], and the left side contains any [AppThemeModeError] that occurred during the process. + */ + operator fun invoke(): Flow> { + return appThemeModeRepository.getAppThemeMode() + .map> { it.right() } + .catch { emit(AppThemeModeError.DataError(it).left()) } + } +} \ No newline at end of file diff --git a/domain/app-theme/src/main/kotlin/com/tangem/domain/apptheme/error/AppThemeModeError.kt b/domain/app-theme/src/main/kotlin/com/tangem/domain/apptheme/error/AppThemeModeError.kt new file mode 100644 index 0000000000..2ef8cfebb3 --- /dev/null +++ b/domain/app-theme/src/main/kotlin/com/tangem/domain/apptheme/error/AppThemeModeError.kt @@ -0,0 +1,6 @@ +package com.tangem.domain.apptheme.error + +sealed class AppThemeModeError { + + data class DataError(val cause: Throwable) : AppThemeModeError() +} \ No newline at end of file diff --git a/domain/app-theme/src/main/kotlin/com/tangem/domain/apptheme/repository/AppThemeModeRepository.kt b/domain/app-theme/src/main/kotlin/com/tangem/domain/apptheme/repository/AppThemeModeRepository.kt new file mode 100644 index 0000000000..3d2e2d5935 --- /dev/null +++ b/domain/app-theme/src/main/kotlin/com/tangem/domain/apptheme/repository/AppThemeModeRepository.kt @@ -0,0 +1,24 @@ +package com.tangem.domain.apptheme.repository + +import com.tangem.domain.apptheme.model.AppThemeMode +import kotlinx.coroutines.flow.Flow + +/** + * Represents a repository for managing the application's theme mode settings. + */ +interface AppThemeModeRepository { + + /** + * Retrieves the current application theme mode as a flow. + * + * @return A [Flow] emitting the current [AppThemeMode]. + */ + fun getAppThemeMode(): Flow + + /** + * Changes the application's theme mode to the specified [mode]. + * + * @param mode The new [AppThemeMode] to be set. + */ + suspend fun changeAppThemeMode(mode: AppThemeMode) +} \ No newline at end of file diff --git a/domain/legacy/build.gradle.kts b/domain/legacy/build.gradle.kts index 9ba0f990f3..69f443892c 100644 --- a/domain/legacy/build.gradle.kts +++ b/domain/legacy/build.gradle.kts @@ -30,6 +30,7 @@ dependencies { implementation(deps.moshi.kotlin) implementation(deps.timber) implementation(deps.kotlin.coroutines) + implementation(deps.jodatime) /** Testing libraries */ testImplementation(deps.test.junit) diff --git a/domain/legacy/src/main/java/com/tangem/domain/common/configs/Wallet2CardConfig.kt b/domain/legacy/src/main/java/com/tangem/domain/common/configs/Wallet2CardConfig.kt index cfa8cec1f6..0e02f053df 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/common/configs/Wallet2CardConfig.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/common/configs/Wallet2CardConfig.kt @@ -16,6 +16,7 @@ object Wallet2CardConfig : CardConfig { /** * Logic to determine primary curve for blockchain in TangemWallet 2.0 + * Order is important here */ override fun primaryCurve(blockchain: Blockchain): EllipticCurve? { // order is important, new curve is preferred for wallet 2 @@ -29,6 +30,10 @@ object Wallet2CardConfig : CardConfig { blockchain.getSupportedCurves().contains(EllipticCurve.Bls12381G2Aug) -> { EllipticCurve.Bls12381G2Aug } + // only for support cardano on Wallet2 + blockchain.getSupportedCurves().contains(EllipticCurve.Ed25519) -> { + EllipticCurve.Ed25519 + } else -> { Timber.e("Unsupported blockchain, curve not found") null diff --git a/domain/legacy/src/main/java/com/tangem/domain/common/extensions/CardSdk.kt b/domain/legacy/src/main/java/com/tangem/domain/common/extensions/CardSdk.kt index 66d0a0f12d..aea826fa7c 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/common/extensions/CardSdk.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/common/extensions/CardSdk.kt @@ -24,13 +24,6 @@ fun CardDTO.supportedBlockchains(cardTypesResolver: CardTypesResolver): List { val derivedKeys = scanResponse.derivedKeys[wallet.publicKey.toMapKey()] - val derivationPath = when (derivationParams) { - is DerivationParams.Default -> blockchain.derivationPath(derivationParams.style) - is DerivationParams.Custom -> derivationParams.path - } - val derivedKey = derivedKeys?.get(derivationPath) - ?: return null + val derivationPath = derivationParams.getPath(blockchain) + + val publicKey = makePublicKey( + seedKey = wallet.publicKey, + blockchain = blockchain, + derivationPath = derivationPath ?: return null, + derivedWalletKeys = derivedKeys ?: return null, + isWallet2 = scanResponse.cardTypesResolver.isWallet2(), + ) ?: return null createWalletManager( blockchain = environmentBlockchain, - seedKey = wallet.publicKey, - derivedKey = derivedKey, - derivation = derivationParams, + publicKey = publicKey, curve = wallet.curve, ) } @@ -70,6 +74,41 @@ fun WalletManagerFactory.makeWalletManagerForApp( } } +private fun makePublicKey( + seedKey: ByteArray, + blockchain: Blockchain, + derivationPath: DerivationPath, + derivedWalletKeys: Map, + isWallet2: Boolean, +): Wallet.PublicKey? { + val derivedKey = derivedWalletKeys[derivationPath] ?: return null + + val derivationKey = Wallet.HDKey( + path = derivationPath, + extendedPublicKey = derivedKey, + ) + + // we should generate second key for cardano + // because cardano address generation for wallet2 requires keys from 2 derivations + // https://developers.cardano.org/docs/get-started/cardano-serialization-lib/generating-keys/ + if (blockchain == Blockchain.Cardano && isWallet2) { + val extendedDerivationPath = CardanoUtils.extendedDerivationPath(derivationPath) + val secondDerivedKey = derivedWalletKeys[extendedDerivationPath] ?: error("No derivation found") + + val secondDerivationKey = Wallet.HDKey(secondDerivedKey, extendedDerivationPath) + + return Wallet.PublicKey( + seedKey = seedKey, + derivationType = Wallet.PublicKey.DerivationType.Double(derivationKey, secondDerivationKey), + ) + } + + return Wallet.PublicKey( + seedKey = seedKey, + derivationType = Wallet.PublicKey.DerivationType.Plain(derivationKey), + ) +} + private fun getDerivationParams(card: CardDTO): DerivationParams? { return if (!card.settings.isHDWalletAllowed) { null diff --git a/domain/legacy/src/main/java/com/tangem/domain/redux/ReduxStateHolder.kt b/domain/legacy/src/main/java/com/tangem/domain/redux/ReduxStateHolder.kt new file mode 100644 index 0000000000..4d43b6da00 --- /dev/null +++ b/domain/legacy/src/main/java/com/tangem/domain/redux/ReduxStateHolder.kt @@ -0,0 +1,8 @@ +package com.tangem.domain.redux + +import org.rekotlin.Action + +interface ReduxStateHolder { + + fun dispatch(action: Action) +} \ No newline at end of file diff --git a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/DefaultWalletManagersFacade.kt b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/DefaultWalletManagersFacade.kt index e119f09792..deb238337d 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 @@ -168,7 +168,7 @@ class DefaultWalletManagersFacade( } } - private suspend fun getOrCreateWalletManager( + override suspend fun getOrCreateWalletManager( userWallet: UserWallet, blockchain: Blockchain, derivationPath: DerivationPath?, 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 c923ac2c76..4c296b01e4 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/WalletManagersFacade.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/WalletManagersFacade.kt @@ -1,11 +1,15 @@ package com.tangem.domain.walletmanager +import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchain.common.WalletManager +import com.tangem.crypto.hdWallet.DerivationPath import com.tangem.domain.tokens.models.CryptoCurrency import com.tangem.domain.tokens.models.Network import com.tangem.domain.txhistory.models.PaginationWrapper import com.tangem.domain.txhistory.models.TxHistoryItem import com.tangem.domain.txhistory.models.TxHistoryState import com.tangem.domain.walletmanager.model.UpdateWalletManagerResult +import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWalletId // TODO: Move to its own module @@ -60,4 +64,10 @@ interface WalletManagersFacade { page: Int, pageSize: Int, ): PaginationWrapper + + suspend fun getOrCreateWalletManager( + userWallet: UserWallet, + blockchain: Blockchain, + derivationPath: DerivationPath?, + ): WalletManager? } \ No newline at end of file diff --git a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/model/CryptoCurrencyAmount.kt b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/model/CryptoCurrencyAmount.kt index ae72f37ec4..6ae9f6ae43 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/model/CryptoCurrencyAmount.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/model/CryptoCurrencyAmount.kt @@ -9,7 +9,7 @@ sealed class CryptoCurrencyAmount { data class Coin(override val value: BigDecimal) : CryptoCurrencyAmount() data class Token( - val id: String?, + val tokenId: String?, val tokenContractAddress: String, override val value: BigDecimal, ) : CryptoCurrencyAmount() diff --git a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/model/CryptoCurrencyTransaction.kt b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/model/CryptoCurrencyTransaction.kt new file mode 100644 index 0000000000..c0b69b941f --- /dev/null +++ b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/model/CryptoCurrencyTransaction.kt @@ -0,0 +1,28 @@ +package com.tangem.domain.walletmanager.model + +import org.joda.time.DateTime +import java.math.BigDecimal + +sealed class CryptoCurrencyTransaction { + + abstract val amount: BigDecimal + abstract val fromAddress: String? + abstract val toAddress: String? + abstract val sentAt: DateTime + + data class Coin( + override val amount: BigDecimal, + override val fromAddress: String?, + override val toAddress: String?, + override val sentAt: DateTime, + ) : CryptoCurrencyTransaction() + + data class Token( + val tokenId: String?, + val tokenContractAddress: String, + override val amount: BigDecimal, + override val fromAddress: String?, + override val toAddress: String?, + override val sentAt: DateTime, + ) : CryptoCurrencyTransaction() +} \ No newline at end of file diff --git a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/model/UpdateWalletManagerResult.kt b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/model/UpdateWalletManagerResult.kt index a516c03d0a..4ad842a7dd 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/model/UpdateWalletManagerResult.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/model/UpdateWalletManagerResult.kt @@ -9,9 +9,15 @@ sealed class UpdateWalletManagerResult { object Unreachable : UpdateWalletManagerResult() data class Verified( - val tokensAmounts: Set, - val hasTransactionsInProgress: Boolean, // TODO: May be add recent transactions + val defaultAddress: String, + val addresses: Set, + val currenciesAmounts: Set, + val currentTransactions: Set, ) : UpdateWalletManagerResult() - data class NoAccount(val amountToCreateAccount: BigDecimal) : UpdateWalletManagerResult() + data class NoAccount( + val defaultAddress: String, + val addresses: Set, + val amountToCreateAccount: BigDecimal, + ) : UpdateWalletManagerResult() } \ No newline at end of file diff --git a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/utils/SdkTransactionHistoryItemConverter.kt b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/utils/SdkTransactionHistoryItemConverter.kt index 8df6e63c54..df57ff2349 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/utils/SdkTransactionHistoryItemConverter.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/utils/SdkTransactionHistoryItemConverter.kt @@ -11,12 +11,7 @@ internal class SdkTransactionHistoryItemConverter : Converter - TxHistoryItem.TransactionDirection.Incoming(direction.from) - is SdkTransactionHistoryItem.TransactionDirection.Outgoing -> - TxHistoryItem.TransactionDirection.Outgoing(direction.to) - }, + direction = value.direction.toDomain(), status = when (value.status) { TransactionStatus.Confirmed -> TxHistoryItem.TxStatus.Confirmed TransactionStatus.Unconfirmed -> TxHistoryItem.TxStatus.Unconfirmed @@ -26,4 +21,16 @@ internal class SdkTransactionHistoryItemConverter : Converter + TxHistoryItem.TransactionDirection.Incoming(address.toDomain()) + is TransactionHistoryItem.TransactionDirection.Outgoing -> + TxHistoryItem.TransactionDirection.Outgoing(address.toDomain()) + } + + private fun TransactionHistoryItem.Address.toDomain(): TxHistoryItem.Address = when (this) { + TransactionHistoryItem.Address.Multiple -> TxHistoryItem.Address.Multiple + is TransactionHistoryItem.Address.Single -> TxHistoryItem.Address.Single(rawAddress = rawAddress) + } } \ No newline at end of file diff --git a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/utils/UpdateWalletManagerResultFactory.kt b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/utils/UpdateWalletManagerResultFactory.kt index df5737de85..4d644e4b7c 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/utils/UpdateWalletManagerResultFactory.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/utils/UpdateWalletManagerResultFactory.kt @@ -1,31 +1,39 @@ package com.tangem.domain.walletmanager.utils import com.tangem.blockchain.common.* +import com.tangem.blockchain.common.address.Address import com.tangem.domain.common.extensions.amountToCreateAccount import com.tangem.domain.walletmanager.model.CryptoCurrencyAmount +import com.tangem.domain.walletmanager.model.CryptoCurrencyTransaction import com.tangem.domain.walletmanager.model.UpdateWalletManagerResult +import org.joda.time.DateTime +import org.joda.time.DateTimeZone +import org.joda.time.Instant import timber.log.Timber import java.math.BigDecimal +import java.util.Calendar internal class UpdateWalletManagerResultFactory { fun getResult(walletManager: WalletManager): UpdateWalletManagerResult.Verified { - val hasNotConfirmedTransactions = walletManager.wallet - .recentTransactions - .any { it.status != TransactionStatus.Confirmed } - - val amounts = walletManager.wallet.amounts + val wallet = walletManager.wallet return UpdateWalletManagerResult.Verified( - tokensAmounts = getTokensAmounts(amounts.values.toSet()), - hasTransactionsInProgress = hasNotConfirmedTransactions, + defaultAddress = wallet.address, + addresses = getAvailableAddresses(wallet.addresses), + currenciesAmounts = getTokensAmounts(wallet.amounts.values.toSet()), + currentTransactions = getCurrentTransactions(wallet.recentTransactions.toSet()), ) } fun getDemoResult(walletManager: WalletManager, demoAmount: Amount): UpdateWalletManagerResult.Verified { + val wallet = walletManager.wallet + return UpdateWalletManagerResult.Verified( - tokensAmounts = getDemoTokensAmounts(demoAmount, walletManager.cardTokens), - hasTransactionsInProgress = false, + defaultAddress = wallet.address, + addresses = getAvailableAddresses(wallet.addresses), + currenciesAmounts = getDemoTokensAmounts(demoAmount, walletManager.cardTokens), + currentTransactions = getCurrentTransactions(wallet.recentTransactions.toSet()), ) } @@ -40,13 +48,17 @@ internal class UpdateWalletManagerResultFactory { "Unable to get required amount to create account for: $blockchain" } - return UpdateWalletManagerResult.NoAccount(amountToCreateAccount) + return UpdateWalletManagerResult.NoAccount( + defaultAddress = wallet.address, + addresses = getAvailableAddresses(wallet.addresses), + amountToCreateAccount = amountToCreateAccount, + ) } private fun getTokensAmounts(amounts: Set): Set { val mutableAmounts = hashSetOf() - return amounts.mapNotNullTo(mutableAmounts, ::getTokenAmount) + return amounts.mapNotNullTo(mutableAmounts, ::createCurrencyAmount) } private fun getDemoTokensAmounts(demoAmount: Amount, tokens: Set): Set { @@ -58,27 +70,94 @@ internal class UpdateWalletManagerResultFactory { } } - private fun getTokenAmount(amount: Amount): CryptoCurrencyAmount? { + private fun getCurrentTransactions(recentTransactions: Set): Set { + val unconfirmedTransactions = recentTransactions.filter { + it.status == TransactionStatus.Unconfirmed + } + + return unconfirmedTransactions.mapNotNullTo(hashSetOf(), ::createCurrencyTransaction) + } + + private fun createCurrencyAmount(amount: Amount): CryptoCurrencyAmount? { return when (val type = amount.type) { is AmountType.Token -> CryptoCurrencyAmount.Token( - id = type.token.id, + tokenId = type.token.id, tokenContractAddress = type.token.contractAddress, - value = getAmountValue(amount) ?: return null, + value = getCurrencyAmountValue(amount) ?: return null, ) is AmountType.Coin -> CryptoCurrencyAmount.Coin( - value = getAmountValue(amount) ?: return null, + value = getCurrencyAmountValue(amount) ?: return null, ) is AmountType.Reserve -> null } } - private fun getAmountValue(amount: Amount): BigDecimal? { + private fun createCurrencyTransaction(data: TransactionData): CryptoCurrencyTransaction? { + val fromAddress = takeAddressIfNotUnknown(data.sourceAddress) + val toAddress = takeAddressIfNotUnknown(data.destinationAddress) + val amount = getTransactionAmountValue(data.amount) ?: return null + val sentAt = getTransactionSentTime(data.date) ?: return null + + return when (val type = data.amount.type) { + is AmountType.Coin -> CryptoCurrencyTransaction.Coin( + amount = amount, + fromAddress = fromAddress, + toAddress = toAddress, + sentAt = sentAt, + ) + is AmountType.Token -> CryptoCurrencyTransaction.Token( + tokenId = type.token.id, + tokenContractAddress = type.token.contractAddress, + amount = amount, + fromAddress = fromAddress, + toAddress = toAddress, + sentAt = sentAt, + ) + is AmountType.Reserve -> null + } + } + + private fun getAvailableAddresses(addresses: Set
): Set { + return addresses.mapTo(hashSetOf()) { it.value } + } + + private fun getCurrencyAmountValue(amount: Amount): BigDecimal? { val value = amount.value if (value == null) { - Timber.e("Amount not found for currency: ${amount.currencySymbol}") + Timber.e("Currency amount must not be null: ${amount.currencySymbol}") } return value } + + private fun getTransactionAmountValue(amount: Amount): BigDecimal? { + val value = amount.value + + if (value == null) { + Timber.e("Transaction amount must not be null: ${amount.currencySymbol}") + } + + return value + } + + private fun getTransactionSentTime(date: Calendar?): DateTime? { + if (date == null) { + Timber.e("Transaction date must not be null") + return null + } + + val instant = Instant.ofEpochMilli(date.timeInMillis) + val timeZone = DateTimeZone.forTimeZone(date.timeZone) + + return instant.toDateTime(timeZone) + } + + private fun takeAddressIfNotUnknown(address: String): String? { + return address.takeIf { it.isNotBlank() && it != UNKNOWN_TRANSACTION_ADDRESS } + } + + private companion object { + const val UNKNOWN_TRANSACTION_ADDRESS = "unknown" + } } \ No newline at end of file diff --git a/domain/settings/src/main/java/com/tangem/domain/settings/CanUseBiometryUseCase.kt b/domain/settings/src/main/java/com/tangem/domain/settings/CanUseBiometryUseCase.kt new file mode 100644 index 0000000000..390c70dcc4 --- /dev/null +++ b/domain/settings/src/main/java/com/tangem/domain/settings/CanUseBiometryUseCase.kt @@ -0,0 +1,8 @@ +package com.tangem.domain.settings + +import com.tangem.domain.settings.repositories.LegacySettingsRepository + +class CanUseBiometryUseCase(private val legacySettingsRepository: LegacySettingsRepository) { + + operator fun invoke(): Boolean = legacySettingsRepository.canUseBiometry() +} \ No newline at end of file diff --git a/domain/settings/src/main/java/com/tangem/domain/settings/ShouldShowSaveWalletScreenUseCase.kt b/domain/settings/src/main/java/com/tangem/domain/settings/ShouldShowSaveWalletScreenUseCase.kt new file mode 100644 index 0000000000..872f62c157 --- /dev/null +++ b/domain/settings/src/main/java/com/tangem/domain/settings/ShouldShowSaveWalletScreenUseCase.kt @@ -0,0 +1,8 @@ +package com.tangem.domain.settings + +import com.tangem.domain.settings.repositories.SettingsRepository + +class ShouldShowSaveWalletScreenUseCase(private val settingsRepository: SettingsRepository) { + + suspend operator fun invoke(): Boolean = settingsRepository.shouldShowSaveUserWalletScreen() +} \ No newline at end of file diff --git a/domain/settings/src/main/java/com/tangem/domain/settings/repositories/LegacySettingsRepository.kt b/domain/settings/src/main/java/com/tangem/domain/settings/repositories/LegacySettingsRepository.kt new file mode 100644 index 0000000000..9e1dc7cf3a --- /dev/null +++ b/domain/settings/src/main/java/com/tangem/domain/settings/repositories/LegacySettingsRepository.kt @@ -0,0 +1,6 @@ +package com.tangem.domain.settings.repositories + +interface LegacySettingsRepository { + + fun canUseBiometry(): Boolean +} \ No newline at end of file diff --git a/domain/settings/src/main/java/com/tangem/domain/settings/repositories/SettingsRepository.kt b/domain/settings/src/main/java/com/tangem/domain/settings/repositories/SettingsRepository.kt index c33c9d4cf7..4b6810e2d4 100644 --- a/domain/settings/src/main/java/com/tangem/domain/settings/repositories/SettingsRepository.kt +++ b/domain/settings/src/main/java/com/tangem/domain/settings/repositories/SettingsRepository.kt @@ -3,4 +3,6 @@ package com.tangem.domain.settings.repositories interface SettingsRepository { suspend fun isUserAlreadyRateApp(): Boolean + + suspend fun shouldShowSaveUserWalletScreen(): Boolean } \ No newline at end of file diff --git a/domain/tokens/build.gradle.kts b/domain/tokens/build.gradle.kts index e2b240e5bc..6fae380259 100644 --- a/domain/tokens/build.gradle.kts +++ b/domain/tokens/build.gradle.kts @@ -7,6 +7,7 @@ dependencies { /** Project - Domain */ implementation(projects.domain.core) + implementation(projects.domain.models) implementation(projects.domain.tokens.models) implementation(projects.domain.wallets.models) implementation(projects.domain.appCurrency.models) @@ -14,6 +15,10 @@ dependencies { /** Project - Other */ implementation(projects.core.utils) + /** Utils */ + implementation(deps.jodatime) + implementation(deps.reKotlin) + /** Tests */ testImplementation(deps.test.junit) testImplementation(deps.test.coroutine) diff --git a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/models/CryptoCurrency.kt b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/models/CryptoCurrency.kt index 8422e6035a..c4346df385 100644 --- a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/models/CryptoCurrency.kt +++ b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/models/CryptoCurrency.kt @@ -6,7 +6,7 @@ import java.io.Serializable * Represents a generic cryptocurrency. * * @property id Unique identifier for the cryptocurrency. - * @property networkId Identifier for the network to which the cryptocurrency belongs. + * @property network The network to which the cryptocurrency belongs. * @property name Human-readable name of the cryptocurrency. * @property symbol Symbol of the cryptocurrency. * @property decimals Number of decimal places used by the cryptocurrency. @@ -14,11 +14,11 @@ import java.io.Serializable * @property derivationPath Optional path used for key derivation. `null` if the wallet does not support the * [HD Wallet](https://coinsutra.com/hd-wallets-deterministic-wallet/) feature. */ -// TODO: [REDACTED_JIRA] delete serializable +// FIXME: Remove serialization [REDACTED_JIRA] sealed class CryptoCurrency : Serializable { abstract val id: ID - abstract val networkId: Network.ID + abstract val network: Network abstract val name: String abstract val symbol: String abstract val decimals: Int @@ -30,7 +30,7 @@ sealed class CryptoCurrency : Serializable { */ data class Coin( override val id: ID, - override val networkId: Network.ID, + override val network: Network, override val name: String, override val symbol: String, override val decimals: Int, @@ -51,7 +51,7 @@ sealed class CryptoCurrency : Serializable { */ data class Token( override val id: ID, - override val networkId: Network.ID, + override val network: Network, override val name: String, override val symbol: String, override val decimals: Int, @@ -59,8 +59,6 @@ sealed class CryptoCurrency : Serializable { override val derivationPath: String?, val contractAddress: String, val isCustom: Boolean, - val blockchainName: String, // TODO: Move this field to proper entity - val standardType: StandardType, // TODO: Move this field to proper entity ) : CryptoCurrency() { init { @@ -79,6 +77,7 @@ sealed class CryptoCurrency : Serializable { * @property rawCurrencyId Represents not unique currency ID from the blockchain network. `null` if * its ID of the custom token. */ + // FIXME: Remove serialization [REDACTED_JIRA] data class ID( private val prefix: Prefix, private val networkId: Network.ID, @@ -114,6 +113,7 @@ sealed class CryptoCurrency : Serializable { * * The suffix can either be a raw ID or a contract address. */ + // FIXME: Remove serialization [REDACTED_JIRA] sealed class Suffix : Serializable { /** The value of the suffix, which could be either a raw ID or a contract address. */ @@ -135,26 +135,6 @@ sealed class CryptoCurrency : Serializable { } } - sealed class StandardType : Serializable { - abstract val name: String - - object ERC20 : StandardType() { - override val name: String = "ERC20" - } - object TRC20 : StandardType() { - override val name: String = "TRC20" - } - object BEP20 : StandardType() { - override val name: String = "BEP20" - } - object BEP2 : StandardType() { - override val name: String = "BEP2" - } - class Unspecified(val tokenName: String) : StandardType() { - override val name: String = tokenName - } - } - protected fun checkProperties() { require(name.isNotBlank()) { "Crypto currency name must not be blank" } require(symbol.isNotBlank()) { "Crypto currency symbol must not be blank" } diff --git a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/models/Network.kt b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/models/Network.kt index 48ba69226f..aaa1cb39d8 100644 --- a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/models/Network.kt +++ b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/models/Network.kt @@ -1,23 +1,35 @@ package com.tangem.domain.tokens.models +import java.io.Serializable + /** - * Represents a blockchain network, identified by a unique ID and a human-readable name. + * Represents a blockchain network, identified by a unique ID, a human-readable name, and its standard type. * - * @property id The unique identifier of the network, encapsulated as an inline value class. + * This class encapsulates the primary details of a blockchain network, such as its ID, name, + * whether it operates as a test network, and the type of blockchain standard it conforms to + * (e.g., ERC20, BEP20). + * + * @property id The unique identifier of the network. * @property name The human-readable name of the network, such as "Ethereum" or "Bitcoin". - * - * @throws IllegalArgumentException If the name or ID is blank. + * @property isTestnet Indicates whether the network is a test network or a main network. + * @property standardType The type of blockchain standard the network adheres to. */ -data class Network(val id: ID, val name: String) { +// FIXME: Remove serialization [REDACTED_JIRA] +data class Network( + val id: ID, + val name: String, + val isTestnet: Boolean, + val standardType: StandardType, +) : Serializable { init { require(name.isNotBlank()) { "Network name must not be blank" } } /** - * Represents a unique identifier for a network. + * Represents a unique identifier for a blockchain network. * - * @property value The string value of the network ID. + * @property value The string representation of the network ID. */ @JvmInline value class ID(val value: String) { @@ -26,4 +38,41 @@ data class Network(val id: ID, val name: String) { require(value.isNotBlank()) { "Network ID must not be blank" } } } + + /** + * Represents the type of blockchain standard that a network adheres to. + * + * Blockchain networks often follow certain standards that dictate how tokens operate on them. + * These standards can define functionalities such as how transactions are processed, + * how tokens are minted or burned, and more. + * + * @property name The human-readable name of the standard type. + */ + // FIXME: Remove serialization [REDACTED_JIRA] + sealed class StandardType : Serializable { + abstract val name: String + + /** Represents the ERC20 token standard, common on the Ethereum network. */ + object ERC20 : StandardType() { + override val name: String = "ERC20" + } + + /** Represents the TRC20 token standard, common on the TRON network. */ + object TRC20 : StandardType() { + override val name: String = "TRC20" + } + + /** Represents the BEP20 token standard, common on the Binance Smart Chain network. */ + object BEP20 : StandardType() { + override val name: String = "BEP20" + } + + /** Represents the BEP2 token standard, common on the Binance Chain network. */ + object BEP2 : StandardType() { + override val name: String = "BEP2" + } + + /** Represents a network that does not adhere to a predefined standard type. */ + data class Unspecified(override val name: String) : StandardType() + } } \ No newline at end of file diff --git a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/models/remove/RemoveCurrencyError.kt b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/models/remove/RemoveCurrencyError.kt new file mode 100644 index 0000000000..906f30f1fd --- /dev/null +++ b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/models/remove/RemoveCurrencyError.kt @@ -0,0 +1,8 @@ +package com.tangem.domain.tokens.models.remove + +import com.tangem.domain.tokens.models.CryptoCurrency + +sealed class RemoveCurrencyError : Throwable() { + data class HasLinkedTokens(val currency: CryptoCurrency) : RemoveCurrencyError() + data class DataError(override val cause: Throwable) : RemoveCurrencyError() +} \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/ApplyTokenListSortingUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/ApplyTokenListSortingUseCase.kt index e57b268e68..39e2ea82db 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/ApplyTokenListSortingUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/ApplyTokenListSortingUseCase.kt @@ -68,7 +68,7 @@ class ApplyTokenListSortingUseCase( private suspend fun Raise.getCurrencies(userWalletId: UserWalletId): List { val tokens = catch( block = { - currenciesRepository.getMultiCurrencyWalletCurrencies(userWalletId, refresh = false).firstOrNull() + currenciesRepository.getMultiCurrencyWalletCurrenciesUpdates(userWalletId).firstOrNull() }, catch = { raise(TokenListSortingError.DataError(it)) }, ) 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 new file mode 100644 index 0000000000..b10e3633c9 --- /dev/null +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/FetchCurrencyStatusUseCase.kt @@ -0,0 +1,121 @@ +package com.tangem.domain.tokens + +import arrow.core.Either +import arrow.core.raise.Raise +import arrow.core.raise.catch +import arrow.core.raise.either +import com.tangem.domain.tokens.error.CurrencyStatusError +import com.tangem.domain.tokens.models.CryptoCurrency +import com.tangem.domain.tokens.models.Network +import com.tangem.domain.tokens.repository.CurrenciesRepository +import com.tangem.domain.tokens.repository.NetworksRepository +import com.tangem.domain.tokens.repository.QuotesRepository +import com.tangem.domain.wallets.models.UserWalletId +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.coroutineScope + +/** + * Use case responsible for fetching currency status information, including network status + * and quotes for a given cryptocurrency. It provides methods to fetch currency status either + * by providing a specific currency ID or fetching the status of the primary currency. + * + * @param currenciesRepository The repository for retrieving currency-related data. + * @param networksRepository The repository for retrieving network-related data. + * @param quotesRepository The repository for retrieving cryptocurrency quotes. + */ +// TODO: Add tests +class FetchCurrencyStatusUseCase( + private val currenciesRepository: CurrenciesRepository, + private val networksRepository: NetworksRepository, + private val quotesRepository: QuotesRepository, +) { + + /** + * Fetches the status of a specific cryptocurrency for a given user wallet. + * + * @param userWalletId The ID of the user's wallet. + * @param id The ID of the cryptocurrency. + * @param refresh Indicates whether to force a refresh of the status data. + * @return An [Either] representing success (Right) or an error (Left) in fetching the status. + */ + suspend operator fun invoke( + userWalletId: UserWalletId, + id: CryptoCurrency.ID, + refresh: Boolean = false, + ): Either { + return either { + val currency = getCurrency(userWalletId, id) + + fetchCurrencyStatus(userWalletId, currency, refresh) + } + } + + /** + * Fetches the status of the primary cryptocurrency for a given user wallet. + * + * @param userWalletId The ID of the user's wallet. + * @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, + refresh: Boolean = false, + ): Either { + return either { + val currency = getPrimaryCurrency(userWalletId) + + fetchCurrencyStatus(userWalletId, currency, refresh) + } + } + + private suspend fun Raise.fetchCurrencyStatus( + userWalletId: UserWalletId, + currency: CryptoCurrency, + refresh: Boolean, + ) = coroutineScope { + val fetchStatus = async { + fetchNetworkStatus(userWalletId, currency.network.id, refresh) + } + val fetchQuote = async { + fetchQuote(currency.id, refresh) + } + + awaitAll(fetchStatus, fetchQuote) + } + + private suspend fun Raise.getCurrency( + userWalletId: UserWalletId, + id: CryptoCurrency.ID, + ): CryptoCurrency { + return catch({ currenciesRepository.getMultiCurrencyWalletCurrency(userWalletId, id) }) { + raise(CurrencyStatusError.DataError(it)) + } + } + + private suspend fun Raise.getPrimaryCurrency(userWalletId: UserWalletId): CryptoCurrency { + return catch({ currenciesRepository.getSingleCurrencyWalletPrimaryCurrency(userWalletId) }) { + raise(CurrencyStatusError.DataError(it)) + } + } + + private suspend fun Raise.fetchNetworkStatus( + userWalletId: UserWalletId, + networkId: Network.ID, + refresh: Boolean, + ) { + catch( + block = { networksRepository.getNetworkStatusesSync(userWalletId, setOf(networkId), refresh) }, + ) { + raise(CurrencyStatusError.DataError(it)) + } + } + + private suspend fun Raise.fetchQuote(currencyId: CryptoCurrency.ID, refresh: Boolean) { + catch( + block = { quotesRepository.getQuotesSync(setOf(currencyId), refresh) }, + ) { + raise(CurrencyStatusError.DataError(it)) + } + } +} \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/FetchTokenListUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/FetchTokenListUseCase.kt new file mode 100644 index 0000000000..7e379288b9 --- /dev/null +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/FetchTokenListUseCase.kt @@ -0,0 +1,101 @@ +package com.tangem.domain.tokens + +import arrow.core.Either +import arrow.core.raise.Raise +import arrow.core.raise.catch +import arrow.core.raise.either +import arrow.core.raise.ensureNotNull +import arrow.core.toNonEmptyListOrNull +import com.tangem.domain.tokens.error.TokenListError +import com.tangem.domain.tokens.models.CryptoCurrency +import com.tangem.domain.tokens.models.Network +import com.tangem.domain.tokens.repository.CurrenciesRepository +import com.tangem.domain.tokens.repository.NetworksRepository +import com.tangem.domain.tokens.repository.QuotesRepository +import com.tangem.domain.wallets.models.UserWalletId +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.coroutineScope + +/** + * Use case responsible for fetching token list information, including currency data, + * network statuses, and quotes for tokens associated with a user's wallet. + * + * @param currenciesRepository The repository for retrieving currency-related data. + * @param networksRepository The repository for retrieving network-related data. + * @param quotesRepository The repository for retrieving cryptocurrency quotes. + */ +// TODO: Add tests +class FetchTokenListUseCase( + private val currenciesRepository: CurrenciesRepository, + private val networksRepository: NetworksRepository, + private val quotesRepository: QuotesRepository, +) { + + /** + * Fetches the token list information for a user's wallet, including currency data, + * network statuses, and quotes for associated tokens. + * + * @param userWalletId The ID of the user's wallet. + * @param refresh Indicates whether to force a refresh of the token list data. + * @return An [Either] representing success (Right) or an error (Left) in fetching the token list. + */ + suspend operator fun invoke(userWalletId: UserWalletId, refresh: Boolean = false): Either { + return either { + val currencies = fetchCurrencies(userWalletId, refresh) + + coroutineScope { + val fetchStatuses = async { + fetchNetworksStatuses( + userWalletId, + currencies.mapTo(hashSetOf()) { it.network.id }, + refresh, + ) + } + val fetchQuotes = async { + fetchQuotes( + currencies.mapTo(hashSetOf()) { it.id }, + refresh, + ) + } + + awaitAll(fetchStatuses, fetchQuotes) + } + } + } + + private suspend fun Raise.fetchCurrencies( + userWalletId: UserWalletId, + refresh: Boolean, + ): List { + val currencies = catch( + block = { currenciesRepository.getMultiCurrencyWalletCurrenciesSync(userWalletId, refresh) }, + ) { + raise(TokenListError.DataError(it)) + } + + return ensureNotNull(currencies.toNonEmptyListOrNull()) { + TokenListError.EmptyTokens + } + } + + private suspend fun Raise.fetchNetworksStatuses( + userWalletId: UserWalletId, + networksIds: Set, + refresh: Boolean, + ) { + catch( + block = { networksRepository.getNetworkStatusesSync(userWalletId, networksIds, refresh) }, + ) { + raise(TokenListError.DataError(it)) + } + } + + private suspend fun Raise.fetchQuotes(currenciesIds: Set, refresh: Boolean) { + catch( + block = { quotesRepository.getQuotesSync(currenciesIds, refresh) }, + ) { + raise(TokenListError.DataError(it)) + } + } +} \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyActionsUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyActionsUseCase.kt new file mode 100644 index 0000000000..d705aacf94 --- /dev/null +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyActionsUseCase.kt @@ -0,0 +1,34 @@ +package com.tangem.domain.tokens + +import com.tangem.domain.tokens.model.TokenActionsState +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.flowOn + +class GetCryptoCurrencyActionsUseCase( + private val dispatchers: CoroutineDispatcherProvider, +) { + + operator fun invoke(userWalletId: UserWalletId, tokenId: String): Flow { + return flow { + emit(getMockState(userWalletId, tokenId)) + }.flowOn(dispatchers.io) + } + + // TODO replace by real data + private fun getMockState(userWalletId: UserWalletId, tokenId: String): TokenActionsState { + return TokenActionsState( + walletId = userWalletId, + tokenId = tokenId, + states = listOf( + TokenActionsState.ActionState.Buy(true), + TokenActionsState.ActionState.Send(true), + TokenActionsState.ActionState.Receive(true), + TokenActionsState.ActionState.Sell(true), + TokenActionsState.ActionState.Swap(true), + ), + ) + } +} \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCurrencyUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCurrencyStatusUpdatesUseCase.kt similarity index 74% rename from domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCurrencyUseCase.kt rename to domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCurrencyStatusUpdatesUseCase.kt index 650abfd651..068b7b1572 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCurrencyUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCurrencyStatusUpdatesUseCase.kt @@ -1,7 +1,7 @@ package com.tangem.domain.tokens import arrow.core.Either -import com.tangem.domain.tokens.error.CurrencyError +import com.tangem.domain.tokens.error.CurrencyStatusError import com.tangem.domain.tokens.error.mapper.mapToCurrencyError import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.models.CryptoCurrency @@ -14,14 +14,13 @@ import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.flow.* /** - * Use case for fetching the status of a specific cryptocurrency associated with a user wallet. + * Use case for fetching the status of a cryptocurrency associated with a user wallet. * * @property currenciesRepository Repository for managing and fetching cryptocurrencies. * @property quotesRepository Repository for managing and fetching cryptocurrency quotes. * @property networksRepository Repository for managing and fetching information related to blockchain networks. - * @property dispatchers Provides coroutine dispatchers. */ -class GetCurrencyUseCase( +class GetCurrencyStatusUpdatesUseCase( private val currenciesRepository: CurrenciesRepository, private val quotesRepository: QuotesRepository, private val networksRepository: NetworksRepository, @@ -33,30 +32,26 @@ class GetCurrencyUseCase( * * @param userWalletId The unique identifier of the user's wallet. * @param currencyId The unique identifier of the cryptocurrency. - * @param refresh A boolean flag indicating whether the data should be refreshed. - * @return A [Flow] emitting either a [CurrencyError] or a [CryptoCurrencyStatus], indicating the result of the fetch operation. + * @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, - refresh: Boolean = false, - ): Flow> { + ): Flow> { return flow { - emitAll(getCurrency(userWalletId, currencyId, refresh)) + emitAll(getCurrency(userWalletId, currencyId)) }.flowOn(dispatchers.io) } private suspend fun getCurrency( userWalletId: UserWalletId, currencyId: CryptoCurrency.ID, - refresh: Boolean, - ): Flow> { + ): Flow> { val operations = CurrenciesStatusesOperations( currenciesRepository = currenciesRepository, quotesRepository = quotesRepository, networksRepository = networksRepository, userWalletId = userWalletId, - refresh = refresh, ) return operations.getCurrencyStatusFlow(currencyId).map { maybeCurrency -> diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetPrimaryCurrencyUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetPrimaryCurrencyStatusUpdatesUseCase.kt similarity index 79% rename from domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetPrimaryCurrencyUseCase.kt rename to domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetPrimaryCurrencyStatusUpdatesUseCase.kt index 58a8bba978..7ae1e238aa 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetPrimaryCurrencyUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetPrimaryCurrencyStatusUpdatesUseCase.kt @@ -1,7 +1,7 @@ package com.tangem.domain.tokens import arrow.core.Either -import com.tangem.domain.tokens.error.CurrencyError +import com.tangem.domain.tokens.error.CurrencyStatusError import com.tangem.domain.tokens.error.mapper.mapToCurrencyError import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.operations.CurrenciesStatusesOperations @@ -20,7 +20,7 @@ import kotlinx.coroutines.flow.* * @property networksRepository Repository for managing and fetching information related to blockchain networks. * @property dispatchers Provides coroutine dispatchers. */ -class GetPrimaryCurrencyUseCase( +class GetPrimaryCurrencyStatusUpdatesUseCase( private val currenciesRepository: CurrenciesRepository, private val quotesRepository: QuotesRepository, private val networksRepository: NetworksRepository, @@ -32,27 +32,22 @@ class GetPrimaryCurrencyUseCase( * * @param userWalletId The unique identifier of the user's wallet. * @param refresh A boolean flag indicating whether the data should be refreshed. - * @return A [Flow] emitting either a [CurrencyError] or a [CryptoCurrencyStatus], indicating the result of the fetch operation. + * @return A [Flow] emitting either a [CurrencyStatusError] or a [CryptoCurrencyStatus], indicating the result of the fetch operation. */ - operator fun invoke( - userWalletId: UserWalletId, - refresh: Boolean = false, - ): Flow> { + operator fun invoke(userWalletId: UserWalletId): Flow> { return flow { - emitAll(getPrimaryCurrency(userWalletId, refresh)) + emitAll(getPrimaryCurrency(userWalletId)) }.flowOn(dispatchers.io) } private suspend fun getPrimaryCurrency( userWalletId: UserWalletId, - refresh: Boolean, - ): Flow> { + ): Flow> { val operations = CurrenciesStatusesOperations( currenciesRepository = currenciesRepository, quotesRepository = quotesRepository, networksRepository = networksRepository, userWalletId = userWalletId, - refresh = refresh, ) return operations.getPrimaryCurrencyStatusFlow().map { maybeCurrency -> diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetTokenListUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetTokenListUseCase.kt index 59125a081c..99c107632a 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetTokenListUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetTokenListUseCase.kt @@ -14,7 +14,10 @@ import com.tangem.domain.tokens.repository.QuotesRepository import com.tangem.domain.wallets.models.UserWalletId import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.flow.* +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flatMapMerge +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.map class GetTokenListUseCase( internal val currenciesRepository: CurrenciesRepository, @@ -24,8 +27,8 @@ class GetTokenListUseCase( ) { @OptIn(ExperimentalCoroutinesApi::class) - operator fun invoke(userWalletId: UserWalletId, refresh: Boolean = false): Flow> { - return getTokensStatuses(userWalletId, refresh).flatMapMerge { maybeTokens -> + operator fun invoke(userWalletId: UserWalletId): Flow> { + return getTokensStatuses(userWalletId).flatMapMerge { maybeTokens -> maybeTokens.fold( ifLeft = { error -> flowOf(error.left()) @@ -39,11 +42,9 @@ class GetTokenListUseCase( private fun getTokensStatuses( userWalletId: UserWalletId, - refresh: Boolean, ): Flow>> { val operations = CurrenciesStatusesOperations( userWalletId = userWalletId, - refresh = refresh, useCase = this@GetTokenListUseCase, ) diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/RemoveCurrencyUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/RemoveCurrencyUseCase.kt new file mode 100644 index 0000000000..71dc767ed2 --- /dev/null +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/RemoveCurrencyUseCase.kt @@ -0,0 +1,40 @@ +package com.tangem.domain.tokens + +import arrow.core.Either +import arrow.core.raise.catch +import arrow.core.raise.either +import arrow.core.raise.ensure +import com.tangem.domain.tokens.models.CryptoCurrency +import com.tangem.domain.tokens.models.remove.RemoveCurrencyError +import com.tangem.domain.tokens.repository.CurrenciesRepository +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.utils.coroutines.CoroutineDispatcherProvider + +class RemoveCurrencyUseCase( + internal val currenciesRepository: CurrenciesRepository, + internal val dispatchers: CoroutineDispatcherProvider, +) { + + suspend operator fun invoke( + userWalletId: UserWalletId, + currency: CryptoCurrency, + ): Either { + return either { + ensure( + condition = !currency.hasLinkedTokens(userWalletId), + raise = { RemoveCurrencyError.HasLinkedTokens(currency) }, + ) + catch( + block = { currenciesRepository.removeCurrency(userWalletId, currency) }, + catch = { raise(RemoveCurrencyError.DataError(it)) }, + ) + } + } + + private suspend fun CryptoCurrency.hasLinkedTokens(userWalletId: UserWalletId): Boolean { + val walletCurrencies = currenciesRepository + .getMultiCurrencyWalletCurrenciesSync(userWalletId = userWalletId, refresh = false) + + return this is CryptoCurrency.Coin && walletCurrencies.any { it != this && it.network.id == this.network.id } + } +} \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/ToggleTokenListGroupingUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/ToggleTokenListGroupingUseCase.kt index 07f86f5069..6328868257 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/ToggleTokenListGroupingUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/ToggleTokenListGroupingUseCase.kt @@ -1,18 +1,18 @@ package com.tangem.domain.tokens import arrow.core.Either -import arrow.core.raise.* +import arrow.core.raise.Raise +import arrow.core.raise.either +import arrow.core.raise.ensure +import arrow.core.raise.withError import com.tangem.domain.tokens.error.TokenListSortingError import com.tangem.domain.tokens.error.mapper.mapToTokenListSortingError import com.tangem.domain.tokens.model.TokenList -import com.tangem.domain.tokens.models.Network import com.tangem.domain.tokens.operations.TokenListSortingOperations -import com.tangem.domain.tokens.repository.NetworksRepository import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.withContext class ToggleTokenListGroupingUseCase( - private val networksRepository: NetworksRepository, private val dispatchers: CoroutineDispatcherProvider, ) { @@ -34,14 +34,10 @@ class ToggleTokenListGroupingUseCase( private fun Raise.groupTokens(tokenList: TokenList.Ungrouped): TokenList.GroupedByNetwork { val sortingOperations = TokenListSortingOperations(tokenList) - val tokens = withError(TokenListSortingOperations.Error::mapToTokenListSortingError) { - sortingOperations.getTokens().bind() - } - val networks = getNetworks(tokens.map { it.currency.networkId }.toSet()) return TokenList.GroupedByNetwork( groups = withError(TokenListSortingOperations.Error::mapToTokenListSortingError) { - sortingOperations.getGroupedTokens(networks).bind() + sortingOperations.getGroupedTokens().bind() }, totalFiatBalance = tokenList.totalFiatBalance, sortedBy = sortingOperations.getSortType(), @@ -61,11 +57,4 @@ class ToggleTokenListGroupingUseCase( sortedBy = sortingOperations.getSortType(), ) } - - private fun Raise.getNetworks(networksIds: Set): Set { - return catch( - block = { networksRepository.getNetworks(networksIds) }, - catch = { raise(TokenListSortingError.DataError(it)) }, - ) - } } \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/ToggleTokenListSortingUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/ToggleTokenListSortingUseCase.kt index 483b822928..d1ae3d9b74 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/ToggleTokenListSortingUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/ToggleTokenListSortingUseCase.kt @@ -36,11 +36,10 @@ class ToggleTokenListSortingUseCase( tokenList: TokenList.GroupedByNetwork, ): TokenList.GroupedByNetwork { val operations = getSortingOperations(tokenList) - val networks = tokenList.groups.map { it.network }.toSet() return tokenList.copy( groups = withError(TokenListSortingOperations.Error::mapToTokenListSortingError) { - operations.getGroupedTokens(networks).bind() + operations.getGroupedTokens().bind() }, sortedBy = operations.getSortType(), ) diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/error/CurrencyError.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/error/CurrencyError.kt deleted file mode 100644 index 5d1ab7e2d8..0000000000 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/error/CurrencyError.kt +++ /dev/null @@ -1,8 +0,0 @@ -package com.tangem.domain.tokens.error - -sealed class CurrencyError { - - object UnableToCreateCurrency : CurrencyError() - - data class DataError(val cause: Throwable) : CurrencyError() -} \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/error/CurrencyStatusError.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/error/CurrencyStatusError.kt new file mode 100644 index 0000000000..c1f5dfaac8 --- /dev/null +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/error/CurrencyStatusError.kt @@ -0,0 +1,8 @@ +package com.tangem.domain.tokens.error + +sealed class CurrencyStatusError { + + object UnableToCreateCurrency : CurrencyStatusError() + + data class DataError(val cause: Throwable) : CurrencyStatusError() +} \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/error/mapper/GetWalletTokenErrorMappers.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/error/mapper/CurrencyStatusErrorMappers.kt similarity index 75% rename from domain/tokens/src/main/kotlin/com/tangem/domain/tokens/error/mapper/GetWalletTokenErrorMappers.kt rename to domain/tokens/src/main/kotlin/com/tangem/domain/tokens/error/mapper/CurrencyStatusErrorMappers.kt index 51fc33c463..6b5e9429d8 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/error/mapper/GetWalletTokenErrorMappers.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/error/mapper/CurrencyStatusErrorMappers.kt @@ -1,15 +1,15 @@ package com.tangem.domain.tokens.error.mapper -import com.tangem.domain.tokens.error.CurrencyError +import com.tangem.domain.tokens.error.CurrencyStatusError import com.tangem.domain.tokens.operations.CurrenciesStatusesOperations -internal fun CurrenciesStatusesOperations.Error.mapToCurrencyError(): CurrencyError { +internal fun CurrenciesStatusesOperations.Error.mapToCurrencyError(): CurrencyStatusError { return when (this) { - is CurrenciesStatusesOperations.Error.DataError -> CurrencyError.DataError(this.cause) + is CurrenciesStatusesOperations.Error.DataError -> CurrencyStatusError.DataError(this.cause) is CurrenciesStatusesOperations.Error.EmptyNetworksStatuses, is CurrenciesStatusesOperations.Error.EmptyQuotes, is CurrenciesStatusesOperations.Error.EmptyCurrencies, is CurrenciesStatusesOperations.Error.UnableToCreateCurrencyStatus, - -> CurrencyError.UnableToCreateCurrency + -> CurrencyStatusError.UnableToCreateCurrency } } \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/error/mapper/TokenListErrorMappers.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/error/mapper/TokenListErrorMappers.kt index 4796f50063..27b4e3956f 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/error/mapper/TokenListErrorMappers.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/error/mapper/TokenListErrorMappers.kt @@ -20,7 +20,5 @@ internal fun TokenListOperations.Error.mapToTokenListError(): TokenListError { is TokenListOperations.Error.DataError -> TokenListError.DataError(this.cause) is TokenListOperations.Error.UnableToSortTokenList -> TokenListError.UnableToSortTokenList(this.unsortedTokenList) - is TokenListOperations.Error.UnableToGroupTokenList -> - TokenListError.UnableToSortTokenList(this.ungroupedTokenList) } } \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/error/mapper/TokenListSortingErrorMappers.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/error/mapper/TokenListSortingErrorMappers.kt index 2b492ad7ee..eefa5d1417 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/error/mapper/TokenListSortingErrorMappers.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/error/mapper/TokenListSortingErrorMappers.kt @@ -6,7 +6,6 @@ import com.tangem.domain.tokens.operations.TokenListSortingOperations internal fun TokenListSortingOperations.Error.mapToTokenListSortingError(): TokenListSortingError { return when (this) { is TokenListSortingOperations.Error.EmptyTokens -> TokenListSortingError.TokenListIsEmpty - is TokenListSortingOperations.Error.EmptyNetworks, is TokenListSortingOperations.Error.NetworkNotFound, -> TokenListSortingError.UnableToSortTokenList } diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/legacy/TradeCryptoAction.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/legacy/TradeCryptoAction.kt new file mode 100644 index 0000000000..3ef7c99d04 --- /dev/null +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/legacy/TradeCryptoAction.kt @@ -0,0 +1,43 @@ +package com.tangem.domain.tokens.legacy + +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.tokens.models.CryptoCurrency +import com.tangem.domain.wallets.models.UserWallet +import org.rekotlin.Action + +sealed class TradeCryptoAction : Action { + + data class Buy(val checkUserLocation: Boolean = true) : TradeCryptoAction() + + object Sell : TradeCryptoAction() + + data class SendCrypto( + val currencyId: String, + val amount: String, + val destinationAddress: String, + val transactionId: String, + ) : TradeCryptoAction() + + data class FinishSelling(val transactionId: String) : TradeCryptoAction() + + object Swap : TradeCryptoAction() + + sealed class New : TradeCryptoAction() { + + data class Buy( + val userWallet: UserWallet, + val cryptoCurrencyStatus: CryptoCurrencyStatus, + val appCurrencyCode: String, + val checkUserLocation: Boolean = true, + ) : New() + + data class Sell( + val cryptoCurrencyStatus: CryptoCurrencyStatus, + val appCurrencyCode: String, + ) : New() + + object Send : New() + + data class Swap(val cryptoCurrency: CryptoCurrency) : New() + } +} \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/CryptoCurrencyStatus.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/CryptoCurrencyStatus.kt index 1f2db6777f..b71f6496b5 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/CryptoCurrencyStatus.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/CryptoCurrencyStatus.kt @@ -35,8 +35,14 @@ data class CryptoCurrencyStatus( /** The change in price of the token. */ open val priceChange: BigDecimal? = null - /** Indicates if there are any transactions in progress related to the token. */ - open val hasTransactionsInProgress: Boolean = false + /** Indicates if there are any transactions in progress related to the cryptocurrency network. */ + open val hasCurrentNetworkTransactions: Boolean = false + + /** The pending cryptocurrency transactions. */ + open val pendingTransactions: Set = emptySet() + + /** The network address */ + open val networkAddress: NetworkAddress? = null } /** Represents the Loading state of a token, typically while fetching its details. */ @@ -58,15 +64,18 @@ data class CryptoCurrencyStatus( * @property fiatAmount The fiat equivalent of the token's amount. * @property fiatRate The exchange rate used for converting the token amount to fiat. * @property priceChange The change in price of the token. - * @property hasTransactionsInProgress Indicates if there are any transactions in progress related to the token - * network. + * @property hasCurrentNetworkTransactions Indicates if there are any transactions in progress related to the + * cryptocurrency network. + * @property pendingTransactions The current cryptocurrency transactions. */ data class Loaded( override val amount: BigDecimal, override val fiatAmount: BigDecimal, override val fiatRate: BigDecimal, override val priceChange: BigDecimal, - override val hasTransactionsInProgress: Boolean, + override val hasCurrentNetworkTransactions: Boolean, + override val pendingTransactions: Set, + override val networkAddress: NetworkAddress?, ) : Status() /** @@ -76,25 +85,32 @@ data class CryptoCurrencyStatus( * @property fiatAmount The fiat equivalent of the token's amount (optional). * @property fiatRate The exchange rate used for converting the token amount to fiat (optional). * @property priceChange The change in price of the token (optional). - * @property hasTransactionsInProgress Indicates if there are any transactions in progress related to the token - * network. + * @property hasCurrentNetworkTransactions Indicates if there are any transactions in progress related to the + * cryptocurrency network. + * @property pendingTransactions The current cryptocurrency transactions. */ data class Custom( override val amount: BigDecimal, override val fiatAmount: BigDecimal?, override val fiatRate: BigDecimal?, override val priceChange: BigDecimal?, - override val hasTransactionsInProgress: Boolean, + override val hasCurrentNetworkTransactions: Boolean, + override val pendingTransactions: Set, + override val networkAddress: NetworkAddress?, ) : Status() /** * Represents a state where the token is available, but there is no current quote available for it. * * @property amount The amount of the token. - * @property hasTransactionsInProgress Indicates if there are any transactions in progress related to the token. + * @property hasCurrentNetworkTransactions Indicates if there are any transactions in progress related to the + * cryptocurrency network. + * @property pendingTransactions The current cryptocurrency transactions. */ data class NoQuote( override val amount: BigDecimal, - override val hasTransactionsInProgress: Boolean, + override val hasCurrentNetworkTransactions: Boolean, + override val pendingTransactions: Set, + override val networkAddress: NetworkAddress?, ) : Status() } \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/NetworkAddress.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/NetworkAddress.kt new file mode 100644 index 0000000000..542d48485e --- /dev/null +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/NetworkAddress.kt @@ -0,0 +1,43 @@ +package com.tangem.domain.tokens.model + +/** + * Represents a network address configuration. + */ +sealed class NetworkAddress { + + /** The default or currently selected network address. */ + abstract val defaultAddress: String + + /** + * Represents a single static network address. + * + * @property defaultAddress The static network address. + */ + data class Single(override val defaultAddress: String) : NetworkAddress() { + + init { + checkDefaultAddress() + } + } + + /** + * Represents a network configuration where an address can be chosen from a set of available addresses. + * + * @property defaultAddress The currently selected or default network address. + * @property availableAddresses The set of available network addresses to choose from. + */ + data class Selectable( + override val defaultAddress: String, + val availableAddresses: Set, + ) : NetworkAddress() { + + init { + checkDefaultAddress() + require(availableAddresses.isNotEmpty()) { "Available network addresses must not be empty" } + } + } + + protected fun checkDefaultAddress() { + require(defaultAddress.isNotBlank()) { "Selected network address must not be blank" } + } +} \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/NetworkStatus.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/NetworkStatus.kt index cfb815d9af..e9d5ec7405 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/NetworkStatus.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/NetworkStatus.kt @@ -33,20 +33,28 @@ data class NetworkStatus( object MissedDerivation : Status() /** - * Represents the verified state of the network, including the amounts associated with different cryptocurrencies and whether there are transactions in progress. + * Represents the verified state of the network, including the amounts associated with different cryptocurrencies + * and whether there are transactions in progress. * + * @property address Network addresses. * @property amounts A map containing the amounts associated with different cryptocurrencies within the network. - * @property hasTransactionsInProgress A boolean indicating whether there are transactions in progress within the network. + * @property pendingTransactions A map containing pending transactions associated with different cryptocurrencies + * within the network. */ data class Verified( + val address: NetworkAddress, val amounts: Map, - val hasTransactionsInProgress: Boolean, + val pendingTransactions: Map>, ) : Status() /** * Represents the state where there is no account, and an amount is required to create one. * + * @property address Network addresses. * @property amountToCreateAccount The amount required to create an account within the network. */ - data class NoAccount(val amountToCreateAccount: BigDecimal) : Status() + data class NoAccount( + val address: NetworkAddress, + val amountToCreateAccount: BigDecimal, + ) : Status() } \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/PendingTransaction.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/PendingTransaction.kt new file mode 100644 index 0000000000..2c4084b81a --- /dev/null +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/PendingTransaction.kt @@ -0,0 +1,40 @@ +package com.tangem.domain.tokens.model + +import org.joda.time.DateTime +import java.math.BigDecimal + +/** + * Represents a cryptocurrency transaction that is currently in progress. + * + * @property amount The monetary amount involved in the transaction. + * @property direction The direction of the transaction, indicating if it's an incoming or outgoing transaction. + * @property sentAt The timestamp when the transaction was executed. + */ +data class PendingTransaction( + val amount: BigDecimal, + val direction: Direction, + val sentAt: DateTime, +) { + + /** + * Represents the direction of the transaction. + */ + sealed class Direction { + + /** + * Represents an incoming transaction. + * + * @property fromAddress The source address from which the assets are being received. May be `null` if + * transaction received from unknown address. + */ + data class Incoming(val fromAddress: String?) : Direction() + + /** + * Represents an outgoing transaction. + * + * @property toAddress The destination address to which the assets are being sent. May be `null` if transaction + * sent to unknown address. + */ + data class Outgoing(val toAddress: String?) : Direction() + } +} \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/TokenActionsState.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/TokenActionsState.kt new file mode 100644 index 0000000000..b2d6e6e71b --- /dev/null +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/TokenActionsState.kt @@ -0,0 +1,25 @@ +package com.tangem.domain.tokens.model + +import com.tangem.domain.wallets.models.UserWalletId + +data class TokenActionsState( + val walletId: UserWalletId, + val tokenId: String, + val states: List, +) { + + sealed class ActionState { + + abstract val enabled: Boolean + + data class Buy(override val enabled: Boolean) : ActionState() + + data class Sell(override val enabled: Boolean) : ActionState() + + data class Receive(override val enabled: Boolean) : ActionState() + + data class Swap(override val enabled: Boolean) : ActionState() + + data class Send(override val enabled: Boolean) : ActionState() + } +} \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesOperations.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesOperations.kt index 463205a41c..cb8885e6b5 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 @@ -20,19 +20,16 @@ internal class CurrenciesStatusesOperations( private val quotesRepository: QuotesRepository, private val networksRepository: NetworksRepository, private val userWalletId: UserWalletId, - private val refresh: Boolean, ) { constructor( userWalletId: UserWalletId, - refresh: Boolean, useCase: GetTokenListUseCase, ) : this( currenciesRepository = useCase.currenciesRepository, quotesRepository = useCase.quotesRepository, networksRepository = useCase.networksRepository, userWalletId = userWalletId, - refresh = refresh, ) @OptIn(ExperimentalCoroutinesApi::class) @@ -51,16 +48,16 @@ internal class CurrenciesStatusesOperations( emit(emptyCurrenciesStatuses.right()) return@transformLatest - } else if (!refresh) { - val maybeLoadingCurrenciesStatuses = createCurrenciesStatuses( - currencies = nonEmptyCurrencies, - maybeNetworkStatuses = null, - maybeQuotes = null, - ) - - emit(maybeLoadingCurrenciesStatuses) } + val maybeLoadingCurrenciesStatuses = createCurrenciesStatuses( + currencies = nonEmptyCurrencies, + maybeNetworkStatuses = null, + maybeQuotes = null, + ) + + emit(maybeLoadingCurrenciesStatuses) + val (networksIds, currenciesIds) = getIds(nonEmptyCurrencies) val currenciesFlow = combine( @@ -105,7 +102,7 @@ internal class CurrenciesStatusesOperations( val statusFlow = getNetworksStatuses(networksIds) .map { maybeStatuses -> maybeStatuses.map { statuses -> - statuses.singleOrNull { it.networkId == currency.networkId } + statuses.singleOrNull { it.networkId == currency.network.id } } } @@ -129,7 +126,7 @@ internal class CurrenciesStatusesOperations( currencies.map { currency -> val quote = quotes?.firstOrNull { it.rawCurrencyId == currency.id.rawCurrencyId } - val networkStatus = networksStatuses?.firstOrNull { it.networkId == currency.networkId } + val networkStatus = networksStatuses?.firstOrNull { it.networkId == currency.network.id } createStatus(currency, quote, networkStatus, ignoreQuote = quotesRetrievingFailed) } @@ -168,7 +165,7 @@ internal class CurrenciesStatusesOperations( } private fun getMultiCurrencyWalletCurrencies(): Flow>> { - return currenciesRepository.getMultiCurrencyWalletCurrencies(userWalletId, refresh) + return currenciesRepository.getMultiCurrencyWalletCurrenciesUpdates(userWalletId) .map, Either>> { it.right() } .catch { emit(Error.DataError(it).left()) } .onEmpty { emit(Error.EmptyCurrencies.left()) } @@ -188,14 +185,14 @@ internal class CurrenciesStatusesOperations( } private fun getQuotes(tokensIds: NonEmptySet): Flow>> { - return quotesRepository.getQuotes(tokensIds, refresh) + return quotesRepository.getQuotesUpdates(tokensIds) .map, Either>> { it.right() } .catch { emit(Error.DataError(it).left()) } .onEmpty { emit(Error.EmptyQuotes.left()) } } private fun getNetworksStatuses(networks: NonEmptySet): Flow>> { - return networksRepository.getNetworkStatuses(userWalletId, networks, refresh) + return networksRepository.getNetworkStatusesUpdates(userWalletId, networks) .map, Either>> { it.right() } .catch { emit(Error.DataError(it).left()) } .onEmpty { emit(Error.EmptyNetworksStatuses.left()) } @@ -205,7 +202,7 @@ internal class CurrenciesStatusesOperations( currencies: NonEmptyList, ): Pair, NonEmptySet> { val currencyIdToNetworkId = currencies.associate { currency -> - currency.id to currency.networkId + currency.id to currency.network.id } val currenciesIds = currencyIdToNetworkId.keys.toNonEmptySetOrNull() val networksIds = currencyIdToNetworkId.values.toNonEmptySetOrNull() diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrencyStatusOperations.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrencyStatusOperations.kt index b37f324c1f..2082010b8d 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrencyStatusOperations.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrencyStatusOperations.kt @@ -27,18 +27,24 @@ internal class CurrencyStatusOperations( private fun createStatus(status: NetworkStatus.Verified): CryptoCurrencyStatus.Status { val amount = status.amounts[currency.id] ?: return CryptoCurrencyStatus.Unreachable + val hasCurrentNetworkTransactions = status.pendingTransactions.isNotEmpty() + val currentTransactions = status.pendingTransactions.getOrElse(currency.id, ::emptySet) return when { ignoreQuote -> CryptoCurrencyStatus.NoQuote( amount = amount, - hasTransactionsInProgress = status.hasTransactionsInProgress, + hasCurrentNetworkTransactions = hasCurrentNetworkTransactions, + pendingTransactions = currentTransactions, + networkAddress = status.address, ) currency is CryptoCurrency.Token && currency.isCustom -> CryptoCurrencyStatus.Custom( amount = amount, fiatAmount = calculateFiatAmountOrNull(amount, quote?.fiatRate), fiatRate = quote?.fiatRate, priceChange = quote?.priceChange, - hasTransactionsInProgress = status.hasTransactionsInProgress, + hasCurrentNetworkTransactions = hasCurrentNetworkTransactions, + pendingTransactions = currentTransactions, + networkAddress = status.address, ) quote == null -> CryptoCurrencyStatus.Loading else -> CryptoCurrencyStatus.Loaded( @@ -46,7 +52,9 @@ internal class CurrencyStatusOperations( fiatAmount = calculateFiatAmount(amount, quote.fiatRate), fiatRate = quote.fiatRate, priceChange = quote.priceChange, - hasTransactionsInProgress = status.hasTransactionsInProgress, + hasCurrentNetworkTransactions = hasCurrentNetworkTransactions, + pendingTransactions = currentTransactions, + networkAddress = status.address, ) } } diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TokenListOperations.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TokenListOperations.kt index 627df695af..2fee6a75c0 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TokenListOperations.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TokenListOperations.kt @@ -5,16 +5,13 @@ import arrow.core.raise.* import com.tangem.domain.tokens.GetTokenListUseCase import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.model.TokenList -import com.tangem.domain.tokens.models.Network import com.tangem.domain.tokens.repository.CurrenciesRepository -import com.tangem.domain.tokens.repository.NetworksRepository import com.tangem.domain.wallets.models.UserWalletId import kotlinx.coroutines.flow.* @Suppress("LongParameterList") internal class TokenListOperations( private val currenciesRepository: CurrenciesRepository, - private val networksRepository: NetworksRepository, private val userWalletId: UserWalletId, private val tokens: List, ) { @@ -25,7 +22,6 @@ internal class TokenListOperations( useCase: GetTokenListUseCase, ) : this( currenciesRepository = useCase.currenciesRepository, - networksRepository = useCase.networksRepository, userWalletId = userWalletId, tokens = tokens, ) @@ -70,37 +66,21 @@ internal class TokenListOperations( sortByBalance = isSortedByBalance, ) - return createTokenList(currencies, sortingOperations, fiatBalance, isGrouped) + return createTokenList(sortingOperations, fiatBalance, isGrouped) } private fun Raise.createTokenList( - tokens: NonEmptyList, sortingOperations: TokenListSortingOperations, fiatBalance: TokenList.FiatBalance, isGrouped: Boolean, ): TokenList { return if (isGrouped) { - val networks = ensureNotNull(getNetworks(tokens).toNonEmptySetOrNull()) { - Error.UnableToGroupTokenList( - ungroupedTokenList = createUngroupedTokenList(sortingOperations, fiatBalance), - ) - } - - createGroupedTokenList(sortingOperations, fiatBalance, networks) + createGroupedTokenList(sortingOperations, fiatBalance) } else { createUngroupedTokenList(sortingOperations, fiatBalance) } } - private fun Raise.getNetworks(tokensNes: NonEmptyList): Set { - val networksIds = tokensNes.map { it.currency.networkId }.toNonEmptySet() - - return catch( - block = { networksRepository.getNetworks(networksIds) }, - catch = { raise(Error.DataError(it)) }, - ) - } - private fun Raise.createUngroupedTokenList( sortingOperations: TokenListSortingOperations, fiatBalance: TokenList.FiatBalance, @@ -118,7 +98,6 @@ internal class TokenListOperations( private fun Raise.createGroupedTokenList( sortingOperations: TokenListSortingOperations, fiatBalance: TokenList.FiatBalance, - networks: NonEmptySet, ): TokenList.GroupedByNetwork = TokenList.GroupedByNetwork( sortedBy = sortingOperations.getSortType(), totalFiatBalance = fiatBalance, @@ -126,7 +105,7 @@ internal class TokenListOperations( transform = { e -> Error.fromTokenListOperations(e) { createUnsortedUngroupedTokenList(tokens, fiatBalance) } }, - block = { sortingOperations.getGroupedTokens(networks).bind() }, + block = { sortingOperations.getGroupedTokens().bind() }, ), ) @@ -159,8 +138,6 @@ internal class TokenListOperations( data class UnableToSortTokenList(val unsortedTokenList: TokenList.Ungrouped) : Error() - data class UnableToGroupTokenList(val ungroupedTokenList: TokenList.Ungrouped) : Error() - data class DataError(val cause: Throwable) : Error() internal companion object { @@ -169,7 +146,6 @@ internal class TokenListOperations( e: TokenListSortingOperations.Error, createUnsortedUngroupedTokenList: () -> TokenList.Ungrouped, ): Error = when (e) { - is TokenListSortingOperations.Error.EmptyNetworks, is TokenListSortingOperations.Error.EmptyTokens, is TokenListSortingOperations.Error.NetworkNotFound, -> UnableToSortTokenList( diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TokenListSortingOperations.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TokenListSortingOperations.kt index 4e609a140f..281d35a8ba 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TokenListSortingOperations.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TokenListSortingOperations.kt @@ -1,10 +1,12 @@ package com.tangem.domain.tokens.operations -import arrow.core.* +import arrow.core.Either +import arrow.core.NonEmptyList import arrow.core.raise.Raise import arrow.core.raise.either import arrow.core.raise.ensure import arrow.core.raise.ensureNotNull +import arrow.core.toNonEmptyListOrNull import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.model.NetworkGroup import com.tangem.domain.tokens.model.TokenList @@ -31,16 +33,13 @@ internal class TokenListSortingOperations( sortByBalance = sortByBalance, ) - fun getGroupedTokens(networks: Set): Either> = either { + fun getGroupedTokens(): Either> = either { ensure(currencies.isNotEmpty()) { Error.EmptyTokens } - val networksNes = ensureNotNull(networks.toNonEmptySetOrNull()) { - Error.EmptyNetworks - } if (sortByBalance) { - groupAndSortTokensByBalance(networksNes) + groupAndSortTokensByBalance() } else { - groupTokens(networksNes) + groupTokens() } } @@ -54,14 +53,10 @@ internal class TokenListSortingOperations( fun getSortType(): TokenList.SortType = if (sortByBalance) TokenList.SortType.BALANCE else TokenList.SortType.NONE - private fun Raise.groupTokens(networks: NonEmptySet): NonEmptyList { + private fun Raise.groupTokens(): NonEmptyList { val groupedTokens = currencies - .groupBy { it.currency.networkId } - .map { (networkId, tokens) -> - val network = ensureNotNull(networks.firstOrNull { it.id == networkId }) { - Error.NetworkNotFound(networkId) - } - + .groupBy { it.currency.network } + .map { (network, tokens) -> NetworkGroup( network = network, currencies = ensureNotNull(tokens.toNonEmptyListOrNull()) { Error.EmptyTokens }, @@ -72,8 +67,8 @@ internal class TokenListSortingOperations( return ensureNotNull(groupedTokens) { Error.EmptyTokens } } - private fun Raise.groupAndSortTokensByBalance(networks: NonEmptySet): NonEmptyList { - val groupsWithSortedTokens = groupTokens(networks) + private fun Raise.groupAndSortTokensByBalance(): NonEmptyList { + val groupsWithSortedTokens = groupTokens() .map { group -> val tokens = group.currencies as? NonEmptyList ?: error("Tokens can not be empty here") @@ -110,8 +105,6 @@ internal class TokenListSortingOperations( object EmptyTokens : Error() - object EmptyNetworks : Error() - data class NetworkNotFound(val networkId: Network.ID) : Error() } } \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/CurrenciesRepository.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/CurrenciesRepository.kt index 91a04b5af8..97cbf78ede 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/CurrenciesRepository.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/CurrenciesRepository.kt @@ -27,6 +27,16 @@ interface CurrenciesRepository { isSortedByBalance: Boolean, ) + /** + * Removes currency from a specific user wallet. + * + * @param userWalletId The unique identifier of the user wallet. + * @param currency The currency which must be removed. + * @throws com.tangem.domain.core.error.DataError.UserWalletError.WrongUserWallet If multi-currency user wallet + * ID provided. + */ + suspend fun removeCurrency(userWalletId: UserWalletId, currency: CryptoCurrency) + /** * Retrieves the primary cryptocurrency for a specific single-currency user wallet. * @@ -38,15 +48,29 @@ interface CurrenciesRepository { suspend fun getSingleCurrencyWalletPrimaryCurrency(userWalletId: UserWalletId): CryptoCurrency /** - * Retrieves the list of cryptocurrencies within a multi-currency wallet. + * Retrieves updates of the list of cryptocurrencies within a multi-currency wallet. + * + * Loads remote cryptocurrencies if they have expired. * * @param userWalletId The unique identifier of the user wallet. - * @param refresh A boolean flag indicating whether the data should be refreshed. * @return A [Flow] emitting the set of cryptocurrencies associated with the user wallet. * @throws com.tangem.domain.core.error.DataError.UserWalletError.WrongUserWallet If single-currency user wallet * ID provided. */ - fun getMultiCurrencyWalletCurrencies(userWalletId: UserWalletId, refresh: Boolean): Flow> + fun getMultiCurrencyWalletCurrenciesUpdates(userWalletId: UserWalletId): Flow> + + /** + * Retrieves the list of cryptocurrencies within a multi-currency wallet. + * + * Loads cryptocurrencies if they have expired or if [refresh] is `true`. + * + * @param userWalletId The unique identifier of the user wallet. + * @param refresh A boolean flag indicating whether the data should be refreshed. + * @return A list of [CryptoCurrency]. + * @throws com.tangem.domain.core.error.DataError.UserWalletError.WrongUserWallet If single-currency user wallet + * ID provided. + */ + suspend fun getMultiCurrencyWalletCurrenciesSync(userWalletId: UserWalletId, refresh: Boolean): List /** * Retrieves the cryptocurrency for a specific multi-currency user wallet. diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/NetworksRepository.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/NetworksRepository.kt index 6dcec4843b..b019c7f3f5 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/NetworksRepository.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/NetworksRepository.kt @@ -19,16 +19,29 @@ interface NetworksRepository { fun getNetworks(networksIds: Set): Set /** - * Retrieves the statuses of specified blockchain networks for a specific user wallet. + * Retrieves updates of network statuses of specified blockchain networks for a specific user wallet. + * + * Loads remote network statuses if they have expired. + * + * @param userWalletId The unique identifier of the user wallet. + * @param networks A set of network IDs which statuses are to be retrieved. + * @return A [Flow] emitting a set of [NetworkStatus] objects corresponding to the specified networks. + */ + fun getNetworkStatusesUpdates(userWalletId: UserWalletId, networks: Set): Flow> + + /** + * Retrieves network statuses of specified blockchain networks for a specific user wallet. + * + * Loads remote network statuses if they have expired or if [refresh] is `true`. * * @param userWalletId The unique identifier of the user wallet. * @param networks A set of network IDs which statuses are to be retrieved. * @param refresh A boolean flag indicating whether the data should be refreshed. * @return A [Flow] emitting a set of [NetworkStatus] objects corresponding to the specified networks. */ - fun getNetworkStatuses( + suspend fun getNetworkStatusesSync( userWalletId: UserWalletId, networks: Set, refresh: Boolean, - ): Flow> + ): Set } \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/QuotesRepository.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/QuotesRepository.kt index 39d6d8489d..7e82d77d52 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/QuotesRepository.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/QuotesRepository.kt @@ -10,11 +10,23 @@ import kotlinx.coroutines.flow.Flow interface QuotesRepository { /** - * Retrieves the quotes for a set of specified cryptocurrencies, identified by their unique IDs. + * Retrieves updates of quotes for a set of specified cryptocurrencies, identified by their unique IDs. + * + * Loads remote quotes if they have expired. + * + * @param currenciesIds The unique identifiers of the cryptocurrencies for which quotes are to be retrieved. + * @return A [Flow] emitting a set of quotes corresponding to the specified cryptocurrencies. + */ + fun getQuotesUpdates(currenciesIds: Set): Flow> + + /** + * Retrieves quotes for a set of specified cryptocurrencies, identified by their unique IDs. + * + * Loads remote quotes if they have expired or if [refresh] is `true`. * * @param currenciesIds The unique identifiers of the cryptocurrencies for which quotes are to be retrieved. * @param refresh A boolean flag indicating whether the data should be refreshed. * @return A [Flow] emitting a set of quotes corresponding to the specified cryptocurrencies. */ - fun getQuotes(currenciesIds: Set, refresh: Boolean): Flow> + suspend fun getQuotesSync(currenciesIds: Set, refresh: Boolean): Set } \ No newline at end of file diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/ApplyTokenListSortingUseCaseTest.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/ApplyTokenListSortingUseCaseTest.kt index d5f180d4be..c40e5fc94b 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/ApplyTokenListSortingUseCaseTest.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/ApplyTokenListSortingUseCaseTest.kt @@ -190,8 +190,16 @@ internal class ApplyTokenListSortingUseCaseTest { private fun getTokensRepository( sortTokensResult: Either = Unit.right(), + removeCurrencyResult: Either = Unit.right(), tokens: Flow>> = flowOf(MockTokens.tokens.right()), ): MockCurrenciesRepository { - return MockCurrenciesRepository(sortTokensResult, MockTokens.token1.right(), tokens, emptyFlow(), emptyFlow()) + return MockCurrenciesRepository( + sortTokensResult = sortTokensResult, + removeCurrencyResult = removeCurrencyResult, + token = MockTokens.token1.right(), + tokens = tokens, + isGrouped = emptyFlow(), + isSortedByBalance = emptyFlow(), + ) } } \ No newline at end of file diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/GetPrimaryCurrencyUseCaseTest.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/GetPrimaryCurrencyStatusUpdatesUseCaseTest.kt similarity index 89% rename from domain/tokens/src/test/kotlin/com/tangem/domain/tokens/GetPrimaryCurrencyUseCaseTest.kt rename to domain/tokens/src/test/kotlin/com/tangem/domain/tokens/GetPrimaryCurrencyStatusUpdatesUseCaseTest.kt index 3d5b25e5eb..41b1dda67b 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/GetPrimaryCurrencyUseCaseTest.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/GetPrimaryCurrencyStatusUpdatesUseCaseTest.kt @@ -4,7 +4,7 @@ import arrow.core.Either import arrow.core.left import arrow.core.right import com.tangem.domain.core.error.DataError -import com.tangem.domain.tokens.error.CurrencyError +import com.tangem.domain.tokens.error.CurrencyStatusError import com.tangem.domain.tokens.mock.MockNetworks import com.tangem.domain.tokens.mock.MockQuotes import com.tangem.domain.tokens.mock.MockTokens @@ -25,7 +25,7 @@ import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.runTest import org.junit.Test -internal class GetPrimaryCurrencyUseCaseTest { +internal class GetPrimaryCurrencyStatusUpdatesUseCaseTest { private val dispatchers = TestingCoroutineDispatcherProvider() private val userWalletId = UserWalletId(value = null) @@ -47,7 +47,7 @@ internal class GetPrimaryCurrencyUseCaseTest { @Test fun `when token getting failed then error should be received`() = runTest { // Given - val expectedResult = CurrencyError.DataError(DataError.NetworkError.NoInternetConnection).left() + val expectedResult = CurrencyStatusError.DataError(DataError.NetworkError.NoInternetConnection).left() val useCase = getUseCase(token = DataError.NetworkError.NoInternetConnection.left()) @@ -75,7 +75,7 @@ internal class GetPrimaryCurrencyUseCaseTest { @Test fun `when networks statuses getting failed then error should be received`() = runTest { // Given - val expectedResult = CurrencyError.DataError(DataError.NetworkError.NoInternetConnection).left() + val expectedResult = CurrencyStatusError.DataError(DataError.NetworkError.NoInternetConnection).left() val useCase = getUseCase(statuses = flowOf(DataError.NetworkError.NoInternetConnection.left())) @@ -88,7 +88,7 @@ internal class GetPrimaryCurrencyUseCaseTest { @Test fun `when networks statuses flow is empty then error should be received`() = runTest { - val expectedResult = CurrencyError.UnableToCreateCurrency.left() + val expectedResult = CurrencyStatusError.UnableToCreateCurrency.left() val useCase = getUseCase(statuses = flowOf()) @@ -150,12 +150,14 @@ internal class GetPrimaryCurrencyUseCaseTest { private fun getUseCase( token: Either = MockTokens.token1.right(), + removeCurrencyResult: Either = Unit.right(), quotes: Flow>> = flowOf(MockQuotes.quotes.right()), statuses: Flow>> = flowOf(MockNetworks.verifiedNetworksStatuses.right()), - ) = GetPrimaryCurrencyUseCase( + ) = GetPrimaryCurrencyStatusUpdatesUseCase( dispatchers = dispatchers, currenciesRepository = MockCurrenciesRepository( sortTokensResult = Unit.right(), + removeCurrencyResult = removeCurrencyResult, token = token, tokens = flowOf(), isGrouped = flowOf(), diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/GetTokenListUseCaseTest.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/GetTokenListUseCaseTest.kt index 8b9eebf8a8..b3a5e61a2e 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/GetTokenListUseCaseTest.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/GetTokenListUseCaseTest.kt @@ -12,7 +12,6 @@ import com.tangem.domain.tokens.mock.MockTokens import com.tangem.domain.tokens.model.NetworkStatus import com.tangem.domain.tokens.model.TokenList import com.tangem.domain.tokens.models.CryptoCurrency -import com.tangem.domain.tokens.models.Network import com.tangem.domain.tokens.models.Quote import com.tangem.domain.tokens.repository.MockCurrenciesRepository import com.tangem.domain.tokens.repository.MockNetworksRepository @@ -52,27 +51,6 @@ internal class GetTokenListUseCaseTest { assertEquals(expectedResult, result) } - @Test - fun `when list refreshed then correct token list should be returned`() = runTest { - // Given - val expectedResult = listOf( - MockTokenLists.failedUngroupedTokenList.right(), - ) - - val useCase = getUseCase( - isGrouped = flowOf(false.right()), - isSortedByBalance = flowOf(false.right()), - ) - - // When - val result = useCase(userWalletId, refresh = true) - .take(count = 1) - .toList() - - // Then - assertEquals(expectedResult, result) - } - @Test fun `when tokens getting failed then error should be received`() = runTest { // Given @@ -109,37 +87,6 @@ internal class GetTokenListUseCaseTest { assertEquals(expectedResult, result) } - @Test - fun `when networks getting failed and list is groped then error should be received`() = runTest { - // Given - val expectedResult = TokenListError.DataError(DataError.NetworkError.NoInternetConnection).left() - - val useCase = getUseCase( - networks = DataError.NetworkError.NoInternetConnection.left(), - isGrouped = flowOf(true.right()), - ) - - // When - val result = useCase(userWalletId).first() - - // Then - assertEquals(expectedResult, result) - } - - @Test - fun `when networks statuses getting failed then error should be received`() = runTest { - // Given - val expectedResult = TokenListError.DataError(DataError.NetworkError.NoInternetConnection).left() - - val useCase = getUseCase(statuses = flowOf(DataError.NetworkError.NoInternetConnection.left())) - - // When - val result = useCase(userWalletId, refresh = true).first() - - // Then - assertEquals(expectedResult, result) - } - @Test fun `when grouping type getting failed then error should be received`() = runTest { // Given @@ -212,22 +159,6 @@ internal class GetTokenListUseCaseTest { assertEquals(expectedResult, result) } - @Test - fun `when list is grouped and networks getting failed then error should be received`() = runTest { - val expectedResult = TokenListError.DataError(DataError.NetworkError.NoInternetConnection).left() - - val useCase = getUseCase( - networks = DataError.NetworkError.NoInternetConnection.left(), - isGrouped = flowOf(true.right()), - ) - - // When - val result = useCase(userWalletId).first() - - // Then - assertEquals(expectedResult, result) - } - @Test fun `when list is sorted and ungrouped then correct token list should be received`() = runTest { val expectedResult = listOf( @@ -285,27 +216,6 @@ internal class GetTokenListUseCaseTest { assertEquals(expectedResult, result) } - @Test - fun `when networks is empty and list is grouped then ungrouped list should be received`() = runTest { - val expectedResult = listOf( - TokenListError.UnableToSortTokenList(MockTokenLists.loadingUngroupedTokenList).left(), - TokenListError.UnableToSortTokenList(MockTokenLists.failedUngroupedTokenList).left(), - ) - - val useCase = getUseCase( - networks = emptySet().right(), - isGrouped = flowOf(true.right()), - ) - - // When - val result = useCase(userWalletId) - .take(count = 2) - .toList() - - // Then - assertEquals(expectedResult, result) - } - @Test fun `when tokens flow is empty then error should be received`() = runTest { val expectedResult = TokenListError.EmptyTokens.left() @@ -390,7 +300,6 @@ internal class GetTokenListUseCaseTest { private fun getUseCase( tokens: Flow>> = flowOf(MockTokens.tokens.right()), quotes: Flow>> = flowOf(MockQuotes.quotes.right()), - networks: Either> = MockNetworks.networks.right(), statuses: Flow>> = flowOf(MockNetworks.errorNetworksStatuses.right()), isGrouped: Flow> = flowOf(MockTokenLists.isGrouped.right()), isSortedByBalance: Flow> = flowOf(MockTokenLists.isSortedByBalance.right()), @@ -398,12 +307,13 @@ internal class GetTokenListUseCaseTest { dispatchers = dispatchers, currenciesRepository = MockCurrenciesRepository( sortTokensResult = Unit.right(), + removeCurrencyResult = Unit.right(), token = MockTokens.token1.right(), tokens = tokens, isGrouped = isGrouped, isSortedByBalance = isSortedByBalance, ), quotesRepository = MockQuotesRepository(quotes), - networksRepository = MockNetworksRepository(networks, statuses), + networksRepository = MockNetworksRepository(MockNetworks.networks.right(), statuses), ) } \ No newline at end of file diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/ToggleTokenListGroupingTest.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/ToggleTokenListGroupingTest.kt index 8e34224394..299b44a0d0 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/ToggleTokenListGroupingTest.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/ToggleTokenListGroupingTest.kt @@ -1,17 +1,11 @@ package com.tangem.domain.tokens -import arrow.core.Either import arrow.core.left import arrow.core.right -import com.tangem.domain.core.error.DataError import com.tangem.domain.tokens.error.TokenListSortingError -import com.tangem.domain.tokens.mock.MockNetworks import com.tangem.domain.tokens.mock.MockTokenLists -import com.tangem.domain.tokens.models.Network -import com.tangem.domain.tokens.repository.MockNetworksRepository import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider import junit.framework.TestCase.assertEquals -import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.runTest import org.junit.Test @@ -143,38 +137,7 @@ internal class ToggleTokenListGroupingTest { assertEquals(expectedResult, result) } - @Test - fun `when list is ungrouped but networks is empty then error should be received`() = runTest { - // Given - val expectedResult = TokenListSortingError.UnableToSortTokenList.left() - - val useCase = getUseCase(networks = emptySet().right()) - - // When - val result = useCase(MockTokenLists.unsortedUngroupedTokenList) - - // Then - assertEquals(expectedResult, result) - } - - @Test - fun `when list is ungrouped but networks getting failed then error should be received`() = runTest { - // Given - val error = DataError.NetworkError.NoInternetConnection - val expectedResult = TokenListSortingError.DataError(error).left() - - val useCase = getUseCase(networks = error.left()) - - // When - val result = useCase(MockTokenLists.unsortedUngroupedTokenList) - - // Then - assertEquals(expectedResult, result) - } - - private fun getUseCase(networks: Either> = MockNetworks.networks.right()) = - ToggleTokenListGroupingUseCase( - networksRepository = MockNetworksRepository(networks, statuses = flowOf()), - dispatchers = TestingCoroutineDispatcherProvider(), - ) + private fun getUseCase() = ToggleTokenListGroupingUseCase( + dispatchers = TestingCoroutineDispatcherProvider(), + ) } \ No newline at end of file diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockNetworks.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockNetworks.kt index 4bf559f4c7..831d0c308b 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockNetworks.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockNetworks.kt @@ -2,6 +2,7 @@ package com.tangem.domain.tokens.mock import arrow.core.NonEmptySet import arrow.core.nonEmptySetOf +import com.tangem.domain.tokens.model.NetworkAddress import com.tangem.domain.tokens.model.NetworkStatus import com.tangem.domain.tokens.models.Network import java.math.BigDecimal @@ -14,16 +15,22 @@ internal object MockNetworks { val network1 = Network( id = Network.ID("network1"), name = "Network One", + isTestnet = false, + standardType = Network.StandardType.ERC20, ) val network2 = Network( id = Network.ID("network2"), name = "Network Two", + isTestnet = false, + standardType = Network.StandardType.ERC20, ) val network3 = Network( id = Network.ID("network3"), name = "Network Three", + isTestnet = false, + standardType = Network.StandardType.ERC20, ) val networks = nonEmptySetOf(network1, network2, network3) @@ -42,6 +49,7 @@ internal object MockNetworks { networkId = network3.id, value = NetworkStatus.NoAccount( amountToCreateAccount = amountToCreateAccount, + address = NetworkAddress.Single(defaultAddress = "mock"), ), ) @@ -55,7 +63,8 @@ internal object MockNetworks { MockTokens.token2.id to BigDecimal.TEN, MockTokens.token3.id to BigDecimal.TEN, ), - hasTransactionsInProgress = false, + pendingTransactions = emptyMap(), + address = NetworkAddress.Single(defaultAddress = "mock"), ), ) @@ -67,7 +76,8 @@ internal object MockNetworks { MockTokens.token5.id to BigDecimal.TEN, MockTokens.token6.id to BigDecimal.TEN, ), - hasTransactionsInProgress = false, + pendingTransactions = emptyMap(), + address = NetworkAddress.Single(defaultAddress = "mock"), ), ) @@ -80,7 +90,8 @@ internal object MockNetworks { MockTokens.token9.id to BigDecimal.TEN, MockTokens.token10.id to BigDecimal.TEN, ), - hasTransactionsInProgress = false, + pendingTransactions = emptyMap(), + address = NetworkAddress.Single(defaultAddress = "mock"), ), ) diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockNetworksGroups.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockNetworksGroups.kt index ab3078de1d..d69fa589b0 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockNetworksGroups.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockNetworksGroups.kt @@ -10,21 +10,21 @@ internal object MockNetworksGroups { val networkGroup1 = NetworkGroup( network = MockNetworks.network1, currencies = MockTokensStates.failedTokenStates - .filter { it.currency.networkId == MockNetworks.network1.id } + .filter { it.currency.network.id == MockNetworks.network1.id } .toNonEmptyListOrNull()!!, ) val networkGroup2 = NetworkGroup( network = MockNetworks.network2, currencies = MockTokensStates.failedTokenStates - .filter { it.currency.networkId == MockNetworks.network2.id } + .filter { it.currency.network.id == MockNetworks.network2.id } .toNonEmptyListOrNull()!!, ) val networkGroup3 = NetworkGroup( network = MockNetworks.network3, currencies = MockTokensStates.failedTokenStates - .filter { it.currency.networkId == MockNetworks.network3.id } + .filter { it.currency.network.id == MockNetworks.network3.id } .toNonEmptyListOrNull()!!, ) @@ -33,7 +33,7 @@ internal object MockNetworksGroups { val loadedNetworksGroups = failedNetworksGroups.map { group -> group.copy( currencies = MockTokensStates.loadedTokensStates - .filter { it.currency.networkId == group.network.id } + .filter { it.currency.network.id == group.network.id } .toNonEmptyListOrNull()!!, ) } diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockTokens.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockTokens.kt index 6e3749a46f..fda72a1b06 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockTokens.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockTokens.kt @@ -8,7 +8,7 @@ internal object MockTokens { val token1 get() = CryptoCurrency.Coin( id = ID(ID.Prefix.COIN_PREFIX, MockNetworks.network1.id, ID.Suffix.RawID("token1")), - networkId = MockNetworks.network1.id, + network = MockNetworks.network1, name = "Token 1", symbol = "T1", decimals = 8, @@ -18,7 +18,7 @@ internal object MockTokens { val token2 get() = CryptoCurrency.Token( id = ID(ID.Prefix.TOKEN_PREFIX, MockNetworks.network1.id, ID.Suffix.RawID("token2")), - networkId = MockNetworks.network1.id, + network = MockNetworks.network1, name = "Token 2", symbol = "T2", isCustom = false, @@ -26,13 +26,11 @@ internal object MockTokens { iconUrl = null, contractAddress = "address", derivationPath = null, - blockchainName = "Ethereum", - standardType = CryptoCurrency.StandardType.ERC20, ) val token3 get() = CryptoCurrency.Token( id = ID(ID.Prefix.TOKEN_PREFIX, MockNetworks.network1.id, ID.Suffix.RawID("token3")), - networkId = MockNetworks.network1.id, + network = MockNetworks.network1, name = "Token 3", symbol = "T3", isCustom = false, @@ -40,13 +38,11 @@ internal object MockTokens { iconUrl = null, contractAddress = "address", derivationPath = null, - blockchainName = "Ethereum", - standardType = CryptoCurrency.StandardType.ERC20, ) val token4 get() = CryptoCurrency.Coin( id = ID(ID.Prefix.COIN_PREFIX, MockNetworks.network2.id, ID.Suffix.RawID("token4")), - networkId = MockNetworks.network2.id, + network = MockNetworks.network2, name = "Token 4", symbol = "T4", decimals = 8, @@ -56,7 +52,7 @@ internal object MockTokens { val token5 get() = CryptoCurrency.Token( id = ID(ID.Prefix.TOKEN_PREFIX, MockNetworks.network2.id, ID.Suffix.RawID("token5")), - networkId = MockNetworks.network2.id, + network = MockNetworks.network2, name = "Token 5", symbol = "T5", isCustom = false, @@ -64,13 +60,11 @@ internal object MockTokens { iconUrl = null, contractAddress = "address", derivationPath = null, - blockchainName = "Ethereum", - standardType = CryptoCurrency.StandardType.ERC20, ) val token6 get() = CryptoCurrency.Token( id = ID(ID.Prefix.TOKEN_PREFIX, MockNetworks.network2.id, ID.Suffix.RawID("token6")), - networkId = MockNetworks.network2.id, + network = MockNetworks.network2, name = "Token 6", symbol = "T6", isCustom = false, @@ -78,13 +72,11 @@ internal object MockTokens { iconUrl = null, contractAddress = "address", derivationPath = null, - blockchainName = "Ethereum", - standardType = CryptoCurrency.StandardType.ERC20, ) val token7 get() = CryptoCurrency.Coin( id = ID(ID.Prefix.COIN_PREFIX, MockNetworks.network3.id, ID.Suffix.RawID("token7")), - networkId = MockNetworks.network3.id, + network = MockNetworks.network3, name = "Token 7", symbol = "T7", decimals = 8, @@ -94,7 +86,7 @@ internal object MockTokens { val token8 get() = CryptoCurrency.Token( id = ID(ID.Prefix.TOKEN_PREFIX, MockNetworks.network3.id, ID.Suffix.RawID("token8")), - networkId = MockNetworks.network3.id, + network = MockNetworks.network3, name = "Token 8", symbol = "T8", isCustom = false, @@ -102,13 +94,11 @@ internal object MockTokens { iconUrl = null, contractAddress = "address", derivationPath = null, - blockchainName = "Ethereum", - standardType = CryptoCurrency.StandardType.ERC20, ) val token9 get() = CryptoCurrency.Token( id = ID(ID.Prefix.TOKEN_PREFIX, MockNetworks.network3.id, ID.Suffix.RawID("token9")), - networkId = MockNetworks.network3.id, + network = MockNetworks.network3, name = "Token 9", symbol = "T9", isCustom = false, @@ -116,13 +106,11 @@ internal object MockTokens { iconUrl = null, contractAddress = "address", derivationPath = null, - blockchainName = "Ethereum", - standardType = CryptoCurrency.StandardType.ERC20, ) val token10 get() = CryptoCurrency.Token( id = ID(ID.Prefix.TOKEN_PREFIX, MockNetworks.network3.id, ID.Suffix.RawID("token10")), - networkId = MockNetworks.network3.id, + network = MockNetworks.network3, name = "Token 10", symbol = "T10", isCustom = false, @@ -130,8 +118,6 @@ internal object MockTokens { iconUrl = null, contractAddress = "address", derivationPath = null, - blockchainName = "Ethereum", - standardType = CryptoCurrency.StandardType.ERC20, ) val tokens = listOf(token1, token2, token3, token4, token5, token6, token7, token8, token9, token10) diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockTokensStates.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockTokensStates.kt index ee6b0a6a1e..d01254b1e7 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockTokensStates.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockTokensStates.kt @@ -72,7 +72,7 @@ internal object MockTokensStates { val loadedTokensStates = failedTokenStates.map { status -> val networkStatus = MockNetworks.verifiedNetworksStatuses - .first { it.networkId == status.currency.networkId } + .first { it.networkId == status.currency.network.id } val amount = (networkStatus.value as NetworkStatus.Verified).amounts[status.currency.id]!! val quote = MockQuotes.quotes.first { it.rawCurrencyId == status.currency.id.rawCurrencyId } val fiatAmount = amount * quote.fiatRate @@ -83,16 +83,24 @@ internal object MockTokensStates { fiatAmount = fiatAmount, fiatRate = quote.fiatRate, priceChange = quote.priceChange, - hasTransactionsInProgress = false, + pendingTransactions = emptySet(), + hasCurrentNetworkTransactions = false, + networkAddress = requireNotNull(networkStatus.value as? NetworkStatus.Verified).address, ), ) } - val noQuotesTokensStatuses = loadedTokensStates.map { currency -> - currency.copy( + val noQuotesTokensStatuses = loadedTokensStates.map { status -> + status.copy( value = CryptoCurrencyStatus.NoQuote( - amount = currency.value.amount!!, - hasTransactionsInProgress = false, + amount = status.value.amount!!, + pendingTransactions = emptySet(), + hasCurrentNetworkTransactions = false, + networkAddress = requireNotNull( + value = MockNetworks.verifiedNetworksStatuses + .first { it.networkId == status.currency.network.id } + .value as? NetworkStatus.Verified, + ).address, ), ) } 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 7ac79d2d4c..73e096b201 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 @@ -6,10 +6,12 @@ import com.tangem.domain.core.error.DataError import com.tangem.domain.tokens.models.CryptoCurrency import com.tangem.domain.wallets.models.UserWalletId import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.map internal class MockCurrenciesRepository( private val sortTokensResult: Either, + private val removeCurrencyResult: Either, private val token: Either, private val tokens: Flow>>, private val isGrouped: Flow>, @@ -38,14 +40,22 @@ internal class MockCurrenciesRepository( isTokensSortedByBalanceAfterSortingApply = isSortedByBalance } + override suspend fun removeCurrency(userWalletId: UserWalletId, currency: CryptoCurrency) { + removeCurrencyResult.onLeft { throw it } + } + + override suspend fun getMultiCurrencyWalletCurrenciesSync( + userWalletId: UserWalletId, + refresh: Boolean, + ): List { + return tokens.first().getOrElse { e -> throw e } + } + override suspend fun getSingleCurrencyWalletPrimaryCurrency(userWalletId: UserWalletId): CryptoCurrency { return token.getOrElse { e -> throw e } } - override fun getMultiCurrencyWalletCurrencies( - userWalletId: UserWalletId, - refresh: Boolean, - ): Flow> { + override fun getMultiCurrencyWalletCurrenciesUpdates(userWalletId: UserWalletId): Flow> { return tokens.map { it.getOrElse { e -> throw e } } } diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockNetworksRepository.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockNetworksRepository.kt index 5ac5c7c8fa..0209a5e953 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockNetworksRepository.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockNetworksRepository.kt @@ -7,6 +7,7 @@ import com.tangem.domain.tokens.model.NetworkStatus import com.tangem.domain.tokens.models.Network import com.tangem.domain.wallets.models.UserWalletId import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.map internal class MockNetworksRepository( @@ -18,11 +19,18 @@ internal class MockNetworksRepository( return networks.getOrElse { throw it } } - override fun getNetworkStatuses( + override fun getNetworkStatusesUpdates( userWalletId: UserWalletId, networks: Set, - refresh: Boolean, ): Flow> { return statuses.map { it.getOrElse { e -> throw e } } } + + override suspend fun getNetworkStatusesSync( + userWalletId: UserWalletId, + networks: Set, + refresh: Boolean, + ): Set { + return getNetworkStatusesUpdates(userWalletId, networks).first() + } } \ No newline at end of file diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockQuotesRepository.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockQuotesRepository.kt index baa2ef1599..3a1470ba1a 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockQuotesRepository.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockQuotesRepository.kt @@ -6,13 +6,18 @@ import com.tangem.domain.core.error.DataError import com.tangem.domain.tokens.models.CryptoCurrency import com.tangem.domain.tokens.models.Quote import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.map internal class MockQuotesRepository( private val quotes: Flow>>, ) : QuotesRepository { - override fun getQuotes(currenciesIds: Set, refresh: Boolean): Flow> { + override fun getQuotesUpdates(currenciesIds: Set): Flow> { return quotes.map { it.getOrElse { e -> throw e } } } + + override suspend fun getQuotesSync(currenciesIds: Set, refresh: Boolean): Set { + return getQuotesUpdates(currenciesIds).first() + } } \ No newline at end of file diff --git a/domain/txhistory/models/src/main/kotlin/com/tangem/domain/txhistory/models/TxHistoryItem.kt b/domain/txhistory/models/src/main/kotlin/com/tangem/domain/txhistory/models/TxHistoryItem.kt index 447a9395ff..c17f0dccb6 100644 --- a/domain/txhistory/models/src/main/kotlin/com/tangem/domain/txhistory/models/TxHistoryItem.kt +++ b/domain/txhistory/models/src/main/kotlin/com/tangem/domain/txhistory/models/TxHistoryItem.kt @@ -11,8 +11,11 @@ data class TxHistoryItem( val amount: BigDecimal, ) { sealed interface TransactionDirection { - data class Incoming(val from: String) : TransactionDirection - data class Outgoing(val to: String) : TransactionDirection + + val address: Address + + data class Incoming(override val address: Address) : TransactionDirection + data class Outgoing(override val address: Address) : TransactionDirection } sealed interface TransactionType { @@ -20,4 +23,9 @@ data class TxHistoryItem( } enum class TxStatus { Confirmed, Unconfirmed } + + sealed class Address { + data class Single(val rawAddress: String) : Address() + object Multiple : Address() + } } \ No newline at end of file diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/models/DeleteWalletError.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/models/DeleteWalletError.kt new file mode 100644 index 0000000000..6ed1ab11b9 --- /dev/null +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/models/DeleteWalletError.kt @@ -0,0 +1,6 @@ +package com.tangem.domain.wallets.models + +sealed interface DeleteWalletError { + + object DataError : DeleteWalletError +} \ No newline at end of file diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/models/UpdateWalletError.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/models/UpdateWalletError.kt new file mode 100644 index 0000000000..48a93dd33b --- /dev/null +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/models/UpdateWalletError.kt @@ -0,0 +1,6 @@ +package com.tangem.domain.wallets.models + +sealed interface UpdateWalletError { + + object DataError : UpdateWalletError +} \ No newline at end of file diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/repository/WalletsRepository.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/repository/WalletsRepository.kt new file mode 100644 index 0000000000..7d5d65f233 --- /dev/null +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/repository/WalletsRepository.kt @@ -0,0 +1,6 @@ +package com.tangem.domain.wallets.repository + +interface WalletsRepository { + + suspend fun shouldSaveUserWallets(): Boolean +} \ No newline at end of file diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/DeleteWalletUseCase.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/DeleteWalletUseCase.kt new file mode 100644 index 0000000000..e4102d98c2 --- /dev/null +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/DeleteWalletUseCase.kt @@ -0,0 +1,31 @@ +package com.tangem.domain.wallets.usecase + +import arrow.core.Either +import arrow.core.left +import arrow.core.right +import com.tangem.common.doOnFailure +import com.tangem.common.doOnSuccess +import com.tangem.domain.wallets.legacy.WalletsStateHolder +import com.tangem.domain.wallets.models.DeleteWalletError +import com.tangem.domain.wallets.models.UserWalletId + +/** + * Use case for updating user wallet + * + * @property walletsStateHolder state holder for getting static initialized 'userWalletsListManager' + * +[REDACTED_AUTHOR] + */ +class DeleteWalletUseCase(private val walletsStateHolder: WalletsStateHolder) { + + suspend operator fun invoke(userWalletId: UserWalletId): Either { + val userWalletsListManager = walletsStateHolder.userWalletsListManager + ?: return DeleteWalletError.DataError.left() + + userWalletsListManager.delete(userWalletIds = listOf(userWalletId)) + .doOnSuccess { return Unit.right() } + .doOnFailure { return DeleteWalletError.DataError.left() } + + return Unit.right() + } +} \ No newline at end of file diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/ShouldSaveUserWalletsUseCase.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/ShouldSaveUserWalletsUseCase.kt new file mode 100644 index 0000000000..b21f04a003 --- /dev/null +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/ShouldSaveUserWalletsUseCase.kt @@ -0,0 +1,8 @@ +package com.tangem.domain.wallets.usecase + +import com.tangem.domain.wallets.repository.WalletsRepository + +class ShouldSaveUserWalletsUseCase(private val walletsRepository: WalletsRepository) { + + suspend operator fun invoke(): Boolean = walletsRepository.shouldSaveUserWallets() +} \ No newline at end of file diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/UpdateWalletUseCase.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/UpdateWalletUseCase.kt new file mode 100644 index 0000000000..1990c26dda --- /dev/null +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/UpdateWalletUseCase.kt @@ -0,0 +1,35 @@ +package com.tangem.domain.wallets.usecase + +import arrow.core.Either +import arrow.core.left +import arrow.core.right +import com.tangem.common.doOnFailure +import com.tangem.common.doOnSuccess +import com.tangem.domain.wallets.legacy.WalletsStateHolder +import com.tangem.domain.wallets.models.UpdateWalletError +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.domain.wallets.models.UserWalletId + +/** + * Use case for updating user wallet + * + * @property walletsStateHolder state holder for getting static initialized 'userWalletsListManager' + * +[REDACTED_AUTHOR] + */ +class UpdateWalletUseCase(private val walletsStateHolder: WalletsStateHolder) { + + suspend operator fun invoke( + userWalletId: UserWalletId, + update: suspend (UserWallet) -> UserWallet, + ): Either { + val userWalletsListManager = walletsStateHolder.userWalletsListManager + ?: return UpdateWalletError.DataError.left() + + userWalletsListManager.update(userWalletId, update) + .doOnSuccess { return Unit.right() } + .doOnFailure { return UpdateWalletError.DataError.left() } + + return Unit.right() + } +} \ No newline at end of file diff --git a/features/tester/impl/build.gradle.kts b/features/tester/impl/build.gradle.kts index b94d17db0e..73011f1a76 100644 --- a/features/tester/impl/build.gradle.kts +++ b/features/tester/impl/build.gradle.kts @@ -23,6 +23,10 @@ dependencies { implementation(deps.hilt.android) kapt(deps.hilt.kapt) + /** Domain modules */ + implementation(projects.domain.appTheme) + implementation(projects.domain.appTheme.models) + /** Core modules */ implementation(project(":core:featuretoggles")) implementation(project(":core:ui")) @@ -32,4 +36,8 @@ dependencies { /** Other modules */ implementation(project(":libs:crypto")) + + /** Other libraries */ + implementation(deps.arrow.core) + implementation(deps.timber) } \ No newline at end of file diff --git a/features/tester/impl/src/main/AndroidManifest.xml b/features/tester/impl/src/main/AndroidManifest.xml index 57f9021fbf..b4e4fc0cfd 100644 --- a/features/tester/impl/src/main/AndroidManifest.xml +++ b/features/tester/impl/src/main/AndroidManifest.xml @@ -5,6 +5,7 @@ - \ No newline at end of file + diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/TesterActivity.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/TesterActivity.kt index ea29eaa44e..13c5799f38 100644 --- a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/TesterActivity.kt +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/TesterActivity.kt @@ -1,15 +1,15 @@ package com.tangem.feature.tester.presentation -import android.os.Bundle -import androidx.activity.ComponentActivity -import androidx.activity.compose.setContent import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier import androidx.hilt.navigation.compose.hiltViewModel import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController import com.tangem.core.ui.components.SystemBarsEffect import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.screen.ComposeActivity +import com.tangem.core.ui.theme.AppThemeModeHolder import com.tangem.feature.tester.presentation.actions.TesterActionsScreen import com.tangem.feature.tester.presentation.actions.TesterActionsViewModel import com.tangem.feature.tester.presentation.featuretoggles.ui.FeatureTogglesScreen @@ -24,7 +24,10 @@ import javax.inject.Inject /** Activity for testers */ @AndroidEntryPoint -internal class TesterActivity : ComponentActivity() { +internal class TesterActivity : ComposeActivity() { + + @Inject + override lateinit var appThemeModeHolder: AppThemeModeHolder /** Router for inner feature navigation */ @Inject @@ -35,19 +38,14 @@ internal class TesterActivity : ComponentActivity() { "TesterRouter must be InnerTesterRouter for tester feature" } - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - - setContent { - TangemTheme { - val statusBarColor = TangemTheme.colors.background.secondary - SystemBarsEffect { - setStatusBarColor(color = statusBarColor) - } - - TesterNavHost() - } + @Composable + override fun ScreenContent(modifier: Modifier) { + val systemBarsColor = TangemTheme.colors.background.secondary + SystemBarsEffect { + setSystemBarsColor(systemBarsColor) } + + TesterNavHost() } @Suppress("TopLevelComposableFunctions") diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/actions/TesterActionsContentState.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/actions/TesterActionsContentState.kt index ea8771920d..fa65d935e4 100644 --- a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/actions/TesterActionsContentState.kt +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/actions/TesterActionsContentState.kt @@ -1,12 +1,20 @@ package com.tangem.feature.tester.presentation.actions +import com.tangem.domain.apptheme.model.AppThemeMode + internal data class TesterActionsContentState( val onBackClick: () -> Unit, - val hideAllCurrencies: HideAllCurrenciesState, + val hideAllCurrenciesConfig: HideAllCurrenciesConfig, + val toggleAppThemeConfig: ToggleAppThemeConfig, ) -internal sealed interface HideAllCurrenciesState { - data class Clickable(val onClick: () -> Unit) : HideAllCurrenciesState +internal sealed class HideAllCurrenciesConfig { + data class Clickable(val onClick: () -> Unit) : HideAllCurrenciesConfig() - object Progress : HideAllCurrenciesState -} \ No newline at end of file + object Progress : HideAllCurrenciesConfig() +} + +internal data class ToggleAppThemeConfig( + val currentAppTheme: AppThemeMode, + val onClick: () -> Unit, +) \ No newline at end of file diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/actions/TesterActionsScreen.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/actions/TesterActionsScreen.kt index 341b2879e7..f8d9df0a78 100644 --- a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/actions/TesterActionsScreen.kt +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/actions/TesterActionsScreen.kt @@ -2,11 +2,7 @@ package com.tangem.feature.tester.presentation.actions import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.runtime.Composable import androidx.compose.runtime.remember @@ -16,6 +12,7 @@ import androidx.compose.ui.tooling.preview.Preview import com.tangem.core.ui.components.PrimaryButton import com.tangem.core.ui.components.appbar.AppBarWithBackButton import com.tangem.core.ui.res.TangemTheme +import com.tangem.domain.apptheme.model.AppThemeMode import com.tangem.feature.tester.impl.R @OptIn(ExperimentalFoundationApi::class) @@ -33,23 +30,37 @@ internal fun TesterActionsScreen(state: TesterActionsContentState, modifier: Mod ) } item { - val onClick = remember(state.hideAllCurrencies) { - { (state.hideAllCurrencies as? HideAllCurrenciesState.Clickable)?.onClick?.invoke() ?: Unit } + val onClick = remember(state.hideAllCurrenciesConfig) { + { (state.hideAllCurrenciesConfig as? HideAllCurrenciesConfig.Clickable)?.onClick?.invoke() ?: Unit } } TesterActionItem( - progress = state.hideAllCurrencies is HideAllCurrenciesState.Progress, + name = stringResource(R.string.hide_all_currencies), + progress = state.hideAllCurrenciesConfig is HideAllCurrenciesConfig.Progress, onClick = onClick, ) } + item { + val config = state.toggleAppThemeConfig + + TesterActionItem( + name = stringResource(id = R.string.toggle_app_theme, config.currentAppTheme.name), + onClick = config.onClick, + ) + } } } @Composable -private fun TesterActionItem(progress: Boolean, onClick: () -> Unit, modifier: Modifier = Modifier) { +private fun TesterActionItem( + name: String, + onClick: () -> Unit, + modifier: Modifier = Modifier, + progress: Boolean = false, +) { Box(modifier = modifier.padding(all = TangemTheme.dimens.spacing16)) { PrimaryButton( modifier = Modifier.fillMaxWidth(), - text = stringResource(R.string.hide_all_currencies), + text = name, onClick = onClick, showProgress = progress, ) @@ -63,7 +74,13 @@ private fun TesterActionsScreenSample(modifier: Modifier = Modifier) { modifier = modifier .background(TangemTheme.colors.background.primary), ) { - TesterActionsScreen(state = TesterActionsContentState({}, HideAllCurrenciesState.Clickable({}))) + TesterActionsScreen( + state = TesterActionsContentState( + onBackClick = {}, + hideAllCurrenciesConfig = HideAllCurrenciesConfig.Clickable {}, + toggleAppThemeConfig = ToggleAppThemeConfig(AppThemeMode.DEFAULT) {}, + ), + ) } } diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/actions/TesterActionsViewModel.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/actions/TesterActionsViewModel.kt index f2024d34cd..b919f37132 100644 --- a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/actions/TesterActionsViewModel.kt +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/actions/TesterActionsViewModel.kt @@ -5,15 +5,25 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import arrow.core.getOrElse +import com.tangem.domain.apptheme.ChangeAppThemeModeUseCase +import com.tangem.domain.apptheme.GetAppThemeModeUseCase +import com.tangem.domain.apptheme.model.AppThemeMode import com.tangem.feature.tester.presentation.navigation.InnerTesterRouter import com.tangem.lib.crypto.UserWalletManager import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch +import timber.log.Timber import javax.inject.Inject @HiltViewModel internal class TesterActionsViewModel @Inject constructor( private val userWalletManager: UserWalletManager, + private val changeAppThemeModeUseCase: ChangeAppThemeModeUseCase, + private val getAppThemeModeUseCase: GetAppThemeModeUseCase, ) : ViewModel() { var uiState: TesterActionsContentState by mutableStateOf(initialState) @@ -21,22 +31,84 @@ internal class TesterActionsViewModel @Inject constructor( private val initialState: TesterActionsContentState get() = TesterActionsContentState( - onBackClick = { /* [REDACTED_TODO_COMMENT] */ }, - hideAllCurrencies = HideAllCurrenciesState.Clickable(this::hideAllCurrencies), + onBackClick = { /* no-op */ }, + hideAllCurrenciesConfig = HideAllCurrenciesConfig.Clickable(this::hideAllCurrencies), + toggleAppThemeConfig = ToggleAppThemeConfig(AppThemeMode.DEFAULT, this::toggleAppTheme), ) + init { + bootstrapAppThemeModeUpdates() + } + fun setupNavigation(router: InnerTesterRouter) { uiState = uiState.copy(onBackClick = router::back) } private fun hideAllCurrencies() = viewModelScope.launch { uiState = uiState.copy( - hideAllCurrencies = HideAllCurrenciesState.Progress, + hideAllCurrenciesConfig = HideAllCurrenciesConfig.Progress, ) userWalletManager.hideAllTokens() uiState = uiState.copy( - hideAllCurrencies = HideAllCurrenciesState.Clickable(this@TesterActionsViewModel::hideAllCurrencies), + hideAllCurrenciesConfig = HideAllCurrenciesConfig.Clickable(this@TesterActionsViewModel::hideAllCurrencies), ) } + + private fun toggleAppTheme() = viewModelScope.launch { + val currentAppThemeMode = uiState.toggleAppThemeConfig.currentAppTheme + val newAppThemeMode = when (currentAppThemeMode) { + AppThemeMode.FORCE_DARK -> AppThemeMode.FORCE_LIGHT + AppThemeMode.FORCE_LIGHT -> AppThemeMode.FOLLOW_SYSTEM + AppThemeMode.FOLLOW_SYSTEM -> AppThemeMode.FORCE_DARK + } + + Timber.d( + """ + Change app theme mode + |- Current theme mode: $currentAppThemeMode + |- New theme mode: $newAppThemeMode + """.trimIndent(), + ) + + changeAppThemeModeUseCase(newAppThemeMode).onLeft { error -> + Timber.e( + """ + Unable to change app theme mode + |- Error: $error + """.trimIndent(), + ) + } + } + + private fun bootstrapAppThemeModeUpdates() { + getAppThemeModeUseCase() + .distinctUntilChanged() + .onEach { maybeAppThemeMode -> + Timber.d( + """ + Current app theme mode updated + |- Previous app theme mode: ${uiState.toggleAppThemeConfig.currentAppTheme} + |- New app theme mode: $maybeAppThemeMode + """.trimIndent(), + ) + + uiState = uiState.copy( + toggleAppThemeConfig = uiState.toggleAppThemeConfig.copy( + currentAppTheme = maybeAppThemeMode.getOrElse { error -> + Timber.e( + """ + Unable to get current app theme mode, using default + |- Default theme mode: ${AppThemeMode.DEFAULT} + |- Error: $error + """.trimIndent(), + ) + + AppThemeMode.DEFAULT + }, + ), + ) + } + .launchIn(viewModelScope) + } } \ No newline at end of file diff --git a/features/tester/impl/src/main/res/values/strings.xml b/features/tester/impl/src/main/res/values/strings.xml index 22d5e89633..9a5a5846b6 100644 --- a/features/tester/impl/src/main/res/values/strings.xml +++ b/features/tester/impl/src/main/res/values/strings.xml @@ -5,4 +5,5 @@ Stand toggles Tester actions Hide all currencies + Toggle app theme - %s diff --git a/features/tokendetails/impl/build.gradle.kts b/features/tokendetails/impl/build.gradle.kts index a7c6440862..1957acae29 100644 --- a/features/tokendetails/impl/build.gradle.kts +++ b/features/tokendetails/impl/build.gradle.kts @@ -14,33 +14,50 @@ android { dependencies { /** AndroidX */ implementation(deps.androidx.activity.compose) + implementation(deps.androidx.paging.runtime) /** Compose */ - implementation(deps.compose.material) + implementation(deps.compose.accompanist.systemUiController) + implementation(deps.compose.coil) implementation(deps.compose.foundation) + implementation(deps.compose.material) implementation(deps.compose.material3) implementation(deps.compose.navigation) implementation(deps.compose.navigation.hilt) + implementation(deps.compose.paging) implementation(deps.compose.ui) implementation(deps.compose.ui.tooling) - implementation(deps.compose.accompanist.systemUiController) - implementation(deps.compose.coil) - implementation(deps.kotlin.immutable.collections) implementation(deps.arrow.core) + implementation(deps.jodatime) + implementation(deps.kotlin.immutable.collections) + implementation(deps.reKotlin) + implementation(deps.tangem.blockchain) + implementation(deps.tangem.card.core) + implementation(deps.timber) /** DI */ implementation(deps.hilt.android) kapt(deps.hilt.kapt) /** Core modules */ + implementation(projects.common) implementation(projects.core.featuretoggles) - implementation(projects.core.ui) implementation(projects.core.navigation) + implementation(projects.core.ui) + implementation(projects.core.utils) + /** Domain modules */ + implementation(projects.domain.appCurrency) + implementation(projects.domain.appCurrency.models) + implementation(projects.domain.legacy) + implementation(projects.domain.models) + implementation(projects.domain.tokens) implementation(projects.domain.tokens.models) implementation(projects.domain.txhistory) implementation(projects.domain.txhistory.models) + implementation(projects.domain.wallets) + implementation(projects.domain.wallets.models) /** Feature Apis */ implementation(projects.features.tokendetails.api) diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/di/TokenDetailsRouterModule.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/di/TokenDetailsRouterModule.kt index 3a0dc79713..b87eb5b538 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/di/TokenDetailsRouterModule.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/di/TokenDetailsRouterModule.kt @@ -1,6 +1,6 @@ package com.tangem.feature.tokendetails.di -import com.tangem.core.navigation.NavigationStateHolder +import com.tangem.core.navigation.ReduxNavController import com.tangem.feature.tokendetails.presentation.router.DefaultTokenDetailsRouter import com.tangem.features.tokendetails.navigation.TokenDetailsRouter import dagger.Module @@ -15,7 +15,7 @@ internal object TokenDetailsRouterModule { @Provides @ActivityScoped - fun provideTokenDetailsRouter(navigationStateHolder: NavigationStateHolder): TokenDetailsRouter { - return DefaultTokenDetailsRouter(navigationStateHolder) + fun provideTokenDetailsRouter(reduxNavController: ReduxNavController): TokenDetailsRouter { + return DefaultTokenDetailsRouter(reduxNavController) } } \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/TokenDetailsFragment.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/TokenDetailsFragment.kt index d9e0a1bcf7..bea2fb5a9f 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/TokenDetailsFragment.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/TokenDetailsFragment.kt @@ -1,14 +1,13 @@ package com.tangem.feature.tokendetails.presentation -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.compose.ui.platform.ComposeView -import androidx.fragment.app.Fragment +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.hilt.navigation.compose.hiltViewModel import com.tangem.core.ui.components.SystemBarsEffect import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.screen.ComposeFragment +import com.tangem.core.ui.theme.AppThemeModeHolder import com.tangem.feature.tokendetails.presentation.router.InnerTokenDetailsRouter import com.tangem.feature.tokendetails.presentation.tokendetails.ui.TokenDetailsScreen import com.tangem.feature.tokendetails.presentation.tokendetails.viewmodels.TokenDetailsViewModel @@ -17,7 +16,10 @@ import dagger.hilt.android.AndroidEntryPoint import javax.inject.Inject @AndroidEntryPoint -internal class TokenDetailsFragment : Fragment() { +internal class TokenDetailsFragment : ComposeFragment() { + + @Inject + override lateinit var appThemeModeHolder: AppThemeModeHolder @Inject lateinit var tokenDetailsRouter: TokenDetailsRouter @@ -27,20 +29,18 @@ internal class TokenDetailsFragment : Fragment() { "internalTokenDetailsRouter should be instance of InnerTokenDetailsRouter" } - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { - return ComposeView(inflater.context).apply { - setContent { - TangemTheme { - val systemBarsColor = TangemTheme.colors.background.secondary - SystemBarsEffect { - setSystemBarsColor(systemBarsColor) - } + @Composable + override fun ScreenContent(modifier: Modifier) { + val viewModel = hiltViewModel() + viewModel.router = this@TokenDetailsFragment.internalTokenDetailsRouter - val viewModel = hiltViewModel() - viewModel.router = this@TokenDetailsFragment.internalTokenDetailsRouter - TokenDetailsScreen(state = viewModel.uiState) - } - } + LocalLifecycleOwner.current.lifecycle.addObserver(viewModel) + + val systemBarsColor = TangemTheme.colors.background.secondary + SystemBarsEffect { + setSystemBarsColor(systemBarsColor) } + + TokenDetailsScreen(state = viewModel.uiState) } } \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/router/DefaultTokenDetailsRouter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/router/DefaultTokenDetailsRouter.kt index 73819028da..ef23464cb0 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/router/DefaultTokenDetailsRouter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/router/DefaultTokenDetailsRouter.kt @@ -2,16 +2,20 @@ package com.tangem.feature.tokendetails.presentation.router import androidx.fragment.app.Fragment import com.tangem.core.navigation.NavigationAction -import com.tangem.core.navigation.NavigationStateHolder +import com.tangem.core.navigation.ReduxNavController import com.tangem.feature.tokendetails.presentation.TokenDetailsFragment internal class DefaultTokenDetailsRouter( - private val navigationStateHolder: NavigationStateHolder, + private val reduxNavController: ReduxNavController, ) : InnerTokenDetailsRouter { override fun getEntryFragment(): Fragment = TokenDetailsFragment() override fun popBackStack() { - navigationStateHolder.navigate(NavigationAction.PopBackTo()) + reduxNavController.navigate(NavigationAction.PopBackTo()) + } + + override fun openUrl(url: String) { + reduxNavController.navigate(NavigationAction.OpenUrl(url = url)) } } \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/router/InnerTokenDetailsRouter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/router/InnerTokenDetailsRouter.kt index 89448deb0d..718ceb5821 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/router/InnerTokenDetailsRouter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/router/InnerTokenDetailsRouter.kt @@ -4,5 +4,9 @@ import com.tangem.features.tokendetails.navigation.TokenDetailsRouter internal interface InnerTokenDetailsRouter : TokenDetailsRouter { + /** Pop back stack */ fun popBackStack() + + /** Open website by [url] */ + fun openUrl(url: String) } \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/TokenDetailsPreviewData.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/TokenDetailsPreviewData.kt index 47cef6133f..b4af868ab8 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/TokenDetailsPreviewData.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/TokenDetailsPreviewData.kt @@ -1,16 +1,15 @@ package com.tangem.feature.tokendetails.presentation.tokendetails -import com.tangem.core.ui.components.buttons.actions.ActionButtonConfig import com.tangem.core.ui.components.marketprice.MarketPriceBlockState -import com.tangem.core.ui.components.marketprice.PriceChangeConfig -import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.components.transactions.state.TxHistoryState import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsBalanceBlockState import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsTopAppBarConfig import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenInfoBlockState +import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.TokenDetailsActionButton import com.tangem.features.tokendetails.impl.R import kotlinx.collections.immutable.persistentListOf -import kotlinx.collections.immutable.toPersistentList +import kotlinx.coroutines.flow.MutableStateFlow internal object TokenDetailsPreviewData { @@ -42,46 +41,19 @@ internal object TokenDetailsPreviewData { ) private val actionButtons = persistentListOf( - ActionButtonConfig( - text = TextReference.Str(value = "Buy"), - iconResId = R.drawable.ic_plus_24, - onClick = {}, - ), - ActionButtonConfig( - text = TextReference.Str(value = "Send"), - iconResId = R.drawable.ic_arrow_up_24, - onClick = {}, - ), - ActionButtonConfig( - text = TextReference.Str(value = "Receive"), - iconResId = R.drawable.ic_arrow_down_24, - onClick = {}, - ), - ActionButtonConfig( - text = TextReference.Str(value = "Exchange"), - iconResId = R.drawable.ic_exchange_vertical_24, - onClick = {}, - ), + TokenDetailsActionButton.Buy(enabled = true, onClick = {}), + TokenDetailsActionButton.Send(enabled = true, onClick = {}), + TokenDetailsActionButton.Receive(onClick = {}), + TokenDetailsActionButton.Swap(enabled = true, onClick = {}), ) - private val disabledActionButtons = actionButtons.map { it.copy(enabled = false) }.toPersistentList() - - val balanceLoading = TokenDetailsBalanceBlockState.Loading(actionButtons = disabledActionButtons) + val balanceLoading = TokenDetailsBalanceBlockState.Loading(actionButtons = actionButtons) val balanceContent = TokenDetailsBalanceBlockState.Content( actionButtons = actionButtons, fiatBalance = "123,00$", cryptoBalance = "866,96 USDT", ) - val balanceError = TokenDetailsBalanceBlockState.Error(actionButtons = disabledActionButtons) - - val marketPriceContent = MarketPriceBlockState.Content( - currencyName = "USDT", - price = "98900 $", - priceChangeConfig = PriceChangeConfig( - valueInPercent = "10.89%", - type = PriceChangeConfig.Type.UP, - ), - ) + val balanceError = TokenDetailsBalanceBlockState.Error(actionButtons = actionButtons) private val marketPriceLoading = MarketPriceBlockState.Loading(currencyName = "USDT") @@ -90,5 +62,10 @@ internal object TokenDetailsPreviewData { tokenInfoBlockState = tokenInfoBlockState, tokenBalanceBlockState = balanceLoading, marketPriceBlockState = marketPriceLoading, + txHistoryState = TxHistoryState.Content( + contentItems = MutableStateFlow( + value = TxHistoryState.getDefaultLoadingTransactions {}, + ), + ), ) } \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsBalanceBlockState.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsBalanceBlockState.kt index a67edd1a61..2f066dae01 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsBalanceBlockState.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsBalanceBlockState.kt @@ -1,23 +1,31 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.state -import com.tangem.core.ui.components.buttons.actions.ActionButtonConfig +import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.TokenDetailsActionButton import kotlinx.collections.immutable.ImmutableList -sealed class TokenDetailsBalanceBlockState { +internal sealed class TokenDetailsBalanceBlockState { - abstract val actionButtons: ImmutableList + abstract val actionButtons: ImmutableList data class Loading( - override val actionButtons: ImmutableList, + override val actionButtons: ImmutableList, ) : TokenDetailsBalanceBlockState() data class Content( - override val actionButtons: ImmutableList, + override val actionButtons: ImmutableList, val fiatBalance: String, val cryptoBalance: String, ) : TokenDetailsBalanceBlockState() data class Error( - override val actionButtons: ImmutableList, + override val actionButtons: ImmutableList, ) : TokenDetailsBalanceBlockState() + + fun copyActionButtons(buttons: ImmutableList): TokenDetailsBalanceBlockState { + return when (this) { + is Content -> this.copy(actionButtons = buttons) + is Error -> this.copy(actionButtons = buttons) + is Loading -> this.copy(actionButtons = buttons) + } + } } \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsState.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsState.kt index 8c7ba02c14..7006b6c4ef 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsState.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsState.kt @@ -1,10 +1,12 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.state import com.tangem.core.ui.components.marketprice.MarketPriceBlockState +import com.tangem.core.ui.components.transactions.state.TxHistoryState -data class TokenDetailsState( +internal data class TokenDetailsState( val topAppBarConfig: TokenDetailsTopAppBarConfig, val tokenInfoBlockState: TokenInfoBlockState, val tokenBalanceBlockState: TokenDetailsBalanceBlockState, val marketPriceBlockState: MarketPriceBlockState, + val txHistoryState: TxHistoryState, ) \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/components/TokenDetailsActionButton.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/components/TokenDetailsActionButton.kt new file mode 100644 index 0000000000..8af1d13a13 --- /dev/null +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/components/TokenDetailsActionButton.kt @@ -0,0 +1,87 @@ +package com.tangem.feature.tokendetails.presentation.tokendetails.state.components + +import androidx.compose.runtime.Immutable +import com.tangem.core.ui.components.buttons.actions.ActionButtonConfig +import com.tangem.core.ui.extensions.TextReference +import com.tangem.features.tokendetails.impl.R + +@Immutable +internal sealed class TokenDetailsActionButton(val config: ActionButtonConfig) { + + /** Lambda be invoked when manage button is clicked */ + abstract val onClick: () -> Unit + + /** + * Buy + * + * @property enabled button click availability + * @property onClick lambda be invoked when Buy button is clicked + */ + data class Buy(val enabled: Boolean, override val onClick: () -> Unit) : TokenDetailsActionButton( + config = ActionButtonConfig( + text = TextReference.Res(id = R.string.common_buy), + iconResId = R.drawable.ic_plus_24, + onClick = onClick, + enabled = enabled, + ), + ) + + /** + * Send + * + * @property enabled button click availability + * @property onClick lambda be invoked when Send button is clicked + */ + data class Send(val enabled: Boolean, override val onClick: () -> Unit) : TokenDetailsActionButton( + config = ActionButtonConfig( + text = TextReference.Res(id = R.string.common_send), + iconResId = R.drawable.ic_arrow_up_24, + onClick = onClick, + enabled = enabled, + ), + ) + + /** + * Receive + * + * @property onClick lambda be invoked when Receive button is clicked + */ + data class Receive(override val onClick: () -> Unit) : TokenDetailsActionButton( + config = ActionButtonConfig( + text = TextReference.Res(id = R.string.common_receive), + iconResId = R.drawable.ic_arrow_down_24, + onClick = onClick, + enabled = true, + ), + ) + + /** + * Sell + * + * @property enabled button click availability + * @property onClick lambda be invoked when Sell button is clicked + */ + data class Sell(val enabled: Boolean, override val onClick: () -> Unit) : TokenDetailsActionButton( + config = ActionButtonConfig( + text = TextReference.Res(id = R.string.common_sell), + iconResId = R.drawable.ic_currency_24, + onClick = onClick, + enabled = enabled, + ), + ) + + /** + * Swap + * + * @property enabled button click availability + * @property onClick lambda be invoked when Swap button is clicked + */ + data class Swap(val enabled: Boolean, override val onClick: () -> Unit) : TokenDetailsActionButton( + config = ActionButtonConfig( + text = TextReference.Res(id = R.string.common_swap), + iconResId = R.drawable.ic_exchange_vertical_24, + onClick = onClick, + enabled = enabled, + ), + ) +} \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsActionButtonsConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsActionButtonsConverter.kt new file mode 100644 index 0000000000..3cd4b2e4de --- /dev/null +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsActionButtonsConverter.kt @@ -0,0 +1,47 @@ +package com.tangem.feature.tokendetails.presentation.tokendetails.state.factory + +import com.tangem.common.Provider +import com.tangem.domain.tokens.model.TokenActionsState +import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState +import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.TokenDetailsActionButton +import com.tangem.feature.tokendetails.presentation.tokendetails.viewmodels.TokenDetailsClickIntents +import com.tangem.utils.converter.Converter +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.toImmutableList + +internal class TokenDetailsActionButtonsConverter( + private val currentStateProvider: Provider, + private val clickIntents: TokenDetailsClickIntents, +) : Converter, TokenDetailsState> { + + override fun convert(value: List): TokenDetailsState { + val state = currentStateProvider() + return state.copy( + tokenBalanceBlockState = state.tokenBalanceBlockState.copyActionButtons(value.mapToManageButtons()), + ) + } + + private fun List.mapToManageButtons(): ImmutableList { + return this + .map { action -> + when (action) { + is TokenActionsState.ActionState.Buy -> { + TokenDetailsActionButton.Buy(enabled = action.enabled, onClick = clickIntents::onBuyClick) + } + is TokenActionsState.ActionState.Receive -> { + TokenDetailsActionButton.Receive(onClick = clickIntents::onReceiveClick) + } + is TokenActionsState.ActionState.Sell -> { + TokenDetailsActionButton.Sell(enabled = action.enabled, onClick = clickIntents::onSellClick) + } + is TokenActionsState.ActionState.Send -> { + TokenDetailsActionButton.Send(enabled = action.enabled, onClick = clickIntents::onSendClick) + } + is TokenActionsState.ActionState.Swap -> { + TokenDetailsActionButton.Swap(enabled = action.enabled, onClick = clickIntents::onSwapClick) + } + } + } + .toImmutableList() + } +} \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsLoadedBalanceConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsLoadedBalanceConverter.kt new file mode 100644 index 0000000000..19c2d0c8b4 --- /dev/null +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsLoadedBalanceConverter.kt @@ -0,0 +1,132 @@ +package com.tangem.feature.tokendetails.presentation.tokendetails.state.factory + +import arrow.core.Either +import com.tangem.common.Provider +import com.tangem.core.ui.components.marketprice.MarketPriceBlockState +import com.tangem.core.ui.components.marketprice.PriceChangeConfig +import com.tangem.core.ui.utils.BigDecimalFormatter +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.tokens.error.CurrencyStatusError +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsBalanceBlockState +import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState +import com.tangem.utils.converter.Converter +import java.math.BigDecimal + +internal class TokenDetailsLoadedBalanceConverter( + private val currentStateProvider: Provider, + private val appCurrencyProvider: Provider, +) : Converter, TokenDetailsState> { + + override fun convert(value: Either): TokenDetailsState { + return value.fold(ifLeft = { convertError() }, ifRight = ::convert) + } + + private fun convertError(): TokenDetailsState { + // TODO: [REDACTED_JIRA] + return currentStateProvider() + } + + private fun convert(status: CryptoCurrencyStatus): TokenDetailsState { + val state = currentStateProvider() + val currencyName = state.marketPriceBlockState.currencyName + return state.copy( + tokenBalanceBlockState = getBalanceState(state.tokenBalanceBlockState, status), + marketPriceBlockState = getMarketPriceState(status = status.value, currencyName = currencyName), + ) + } + + private fun getBalanceState( + currentState: TokenDetailsBalanceBlockState, + status: CryptoCurrencyStatus, + ): TokenDetailsBalanceBlockState { + return when (status.value) { + is CryptoCurrencyStatus.NoQuote, + is CryptoCurrencyStatus.Loaded, + -> { + TokenDetailsBalanceBlockState.Content( + actionButtons = currentState.actionButtons, + fiatBalance = formatFiatAmount(status.value, appCurrencyProvider()), + cryptoBalance = formatCryptoAmount(status), + ) + } + is CryptoCurrencyStatus.Loading -> { + TokenDetailsBalanceBlockState.Loading(currentState.actionButtons) + } + is CryptoCurrencyStatus.MissedDerivation, + is CryptoCurrencyStatus.NoAccount, + is CryptoCurrencyStatus.Custom, + // TODO: [REDACTED_JIRA] + is CryptoCurrencyStatus.Unreachable, + -> { + TokenDetailsBalanceBlockState.Error(currentState.actionButtons) + } + } + } + + private fun getMarketPriceState(status: CryptoCurrencyStatus.Status, currencyName: String): MarketPriceBlockState { + return when (status) { + is CryptoCurrencyStatus.NoQuote, + is CryptoCurrencyStatus.Loaded, + -> MarketPriceBlockState.Content( + currencyName = currencyName, + price = formatPrice(status, appCurrencyProvider()), + priceChangeConfig = PriceChangeConfig( + valueInPercent = formatPriceChange(status), + type = getPriceChangeType(status), + ), + ) + is CryptoCurrencyStatus.Loading -> MarketPriceBlockState.Loading(currencyName) + is CryptoCurrencyStatus.Custom, + is CryptoCurrencyStatus.MissedDerivation, + is CryptoCurrencyStatus.NoAccount, + is CryptoCurrencyStatus.Unreachable, + -> MarketPriceBlockState.Error(currencyName) + } + } + + private fun getPriceChangeType(status: CryptoCurrencyStatus.Status): PriceChangeConfig.Type { + val priceChange = status.priceChange ?: return PriceChangeConfig.Type.DOWN + + return if (priceChange > BigDecimal.ZERO) { + PriceChangeConfig.Type.UP + } else { + PriceChangeConfig.Type.DOWN + } + } + + private fun formatPriceChange(status: CryptoCurrencyStatus.Status): String { + val priceChange = status.priceChange ?: return BigDecimalFormatter.EMPTY_BALANCE_SIGN + + return BigDecimalFormatter.formatPercent( + percent = priceChange, + useAbsoluteValue = true, + ) + } + + private fun formatPrice(status: CryptoCurrencyStatus.Status, appCurrency: AppCurrency): String { + val fiatRate = status.fiatRate ?: return BigDecimalFormatter.EMPTY_BALANCE_SIGN + + return BigDecimalFormatter.formatFiatAmount( + fiatAmount = fiatRate, + fiatCurrencyCode = appCurrency.code, + fiatCurrencySymbol = appCurrency.symbol, + ) + } + + private fun formatFiatAmount(status: CryptoCurrencyStatus.Status, appCurrency: AppCurrency): String { + val fiatAmount = status.fiatAmount ?: return BigDecimalFormatter.EMPTY_BALANCE_SIGN + + return BigDecimalFormatter.formatFiatAmount( + fiatAmount = fiatAmount, + fiatCurrencyCode = appCurrency.code, + fiatCurrencySymbol = appCurrency.symbol, + ) + } + + private fun formatCryptoAmount(status: CryptoCurrencyStatus): String { + val amount = status.value.amount ?: return BigDecimalFormatter.EMPTY_BALANCE_SIGN + + return BigDecimalFormatter.formatCryptoAmount(amount, status.currency.symbol, status.currency.decimals) + } +} \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSkeletonStateConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSkeletonStateConverter.kt new file mode 100644 index 0000000000..a02f4cc5fe --- /dev/null +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSkeletonStateConverter.kt @@ -0,0 +1,64 @@ +package com.tangem.feature.tokendetails.presentation.tokendetails.state.factory + +import com.tangem.core.ui.components.marketprice.MarketPriceBlockState +import com.tangem.core.ui.components.transactions.state.TxHistoryState +import com.tangem.core.ui.extensions.iconResId +import com.tangem.domain.tokens.models.CryptoCurrency +import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsBalanceBlockState +import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState +import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsTopAppBarConfig +import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenInfoBlockState +import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.TokenDetailsActionButton +import com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.TokenDetailsSkeletonStateConverter.SkeletonModel +import com.tangem.feature.tokendetails.presentation.tokendetails.viewmodels.TokenDetailsClickIntents +import com.tangem.utils.converter.Converter +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf +import kotlinx.coroutines.flow.MutableStateFlow + +internal class TokenDetailsSkeletonStateConverter( + private val clickIntents: TokenDetailsClickIntents, +) : Converter { + + override fun convert(value: SkeletonModel): TokenDetailsState { + return TokenDetailsState( + topAppBarConfig = TokenDetailsTopAppBarConfig( + onBackClick = clickIntents::onBackClick, + onMoreClick = clickIntents::onMoreClick, + ), + tokenInfoBlockState = TokenInfoBlockState( + name = value.cryptoCurrency.name, + iconUrl = requireNotNull(value.cryptoCurrency.iconUrl), + currency = when (val currency = value.cryptoCurrency) { + is CryptoCurrency.Coin -> TokenInfoBlockState.Currency.Native + is CryptoCurrency.Token -> TokenInfoBlockState.Currency.Token( + networkName = currency.network.standardType.name, + blockchainName = currency.network.name, + networkIcon = currency.iconResId, + ) + }, + ), + tokenBalanceBlockState = TokenDetailsBalanceBlockState.Loading( + actionButtons = createButtons(), + ), + marketPriceBlockState = MarketPriceBlockState.Loading(value.cryptoCurrency.name), + txHistoryState = TxHistoryState.Content( + contentItems = MutableStateFlow( + value = TxHistoryState.getDefaultLoadingTransactions(clickIntents::onExploreClick), + ), + ), + ) + } + + private fun createButtons(): ImmutableList { + return persistentListOf( + TokenDetailsActionButton.Buy(enabled = false, onClick = {}), + TokenDetailsActionButton.Send(enabled = false, onClick = {}), + TokenDetailsActionButton.Receive(onClick = {}), + TokenDetailsActionButton.Sell(enabled = false, onClick = {}), + TokenDetailsActionButton.Swap(enabled = false, onClick = {}), + ) + } + + data class SkeletonModel(val cryptoCurrency: CryptoCurrency) +} \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt new file mode 100644 index 0000000000..e0ad501d38 --- /dev/null +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt @@ -0,0 +1,84 @@ +package com.tangem.feature.tokendetails.presentation.tokendetails.state.factory + +import androidx.paging.PagingData +import arrow.core.Either +import com.tangem.common.Provider +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.tokens.error.CurrencyStatusError +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.tokens.model.TokenActionsState +import com.tangem.domain.tokens.models.CryptoCurrency +import com.tangem.domain.txhistory.models.TxHistoryItem +import com.tangem.domain.txhistory.models.TxHistoryListError +import com.tangem.domain.txhistory.models.TxHistoryStateError +import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState +import com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.txhistory.TokenDetailsLoadedTxHistoryConverter +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 + +internal class TokenDetailsStateFactory( + private val currentStateProvider: Provider, + private val appCurrencyProvider: Provider, + private val clickIntents: TokenDetailsClickIntents, + symbol: String, + decimals: Int, +) { + + private val skeletonStateConverter by lazy { + TokenDetailsSkeletonStateConverter(clickIntents = clickIntents) + } + + private val tokenDetailsLoadedBalanceConverter by lazy { + TokenDetailsLoadedBalanceConverter( + currentStateProvider = currentStateProvider, + appCurrencyProvider = appCurrencyProvider, + ) + } + + private val tokenDetailsButtonsConverter by lazy { + TokenDetailsActionButtonsConverter( + currentStateProvider = currentStateProvider, + clickIntents = clickIntents, + ) + } + + private val loadingTransactionsStateConverter by lazy { + TokenDetailsLoadingTxHistoryConverter(currentStateProvider = currentStateProvider, clickIntents = clickIntents) + } + + private val loadedTxHistoryConverter by lazy { + TokenDetailsLoadedTxHistoryConverter( + currentStateProvider = currentStateProvider, + clickIntents = clickIntents, + symbol = symbol, + decimals = decimals, + ) + } + + fun getInitialState(cryptoCurrency: CryptoCurrency): TokenDetailsState { + return skeletonStateConverter.convert( + TokenDetailsSkeletonStateConverter.SkeletonModel(cryptoCurrency = cryptoCurrency), + ) + } + + fun getCurrencyLoadedBalanceState( + cryptoCurrencyEither: Either, + ): TokenDetailsState { + return tokenDetailsLoadedBalanceConverter.convert(cryptoCurrencyEither) + } + + fun getManageButtonsState(actions: List): TokenDetailsState { + return tokenDetailsButtonsConverter.convert(actions) + } + + fun getLoadingTxHistoryState(itemsCountEither: Either): TokenDetailsState { + return loadingTransactionsStateConverter.convert(value = itemsCountEither) + } + + fun getLoadedTxHistoryState( + txHistoryEither: Either>>, + ): TokenDetailsState { + return loadedTxHistoryConverter.convert(txHistoryEither) + } +} \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/txhistory/TokenDetailsLoadedTxHistoryConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/txhistory/TokenDetailsLoadedTxHistoryConverter.kt new file mode 100644 index 0000000000..f9146ee1d3 --- /dev/null +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/txhistory/TokenDetailsLoadedTxHistoryConverter.kt @@ -0,0 +1,49 @@ +package com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.txhistory + +import androidx.paging.PagingData +import arrow.core.Either +import com.tangem.common.Provider +import com.tangem.core.ui.components.transactions.intents.TxHistoryClickIntents +import com.tangem.core.ui.components.transactions.state.TxHistoryState +import com.tangem.domain.txhistory.models.TxHistoryItem +import com.tangem.domain.txhistory.models.TxHistoryListError +import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState +import com.tangem.utils.converter.Converter +import kotlinx.coroutines.flow.Flow + +internal class TokenDetailsLoadedTxHistoryConverter( + private val currentStateProvider: Provider, + private val clickIntents: TxHistoryClickIntents, + symbol: String, + decimals: Int, +) : Converter>>, TokenDetailsState> { + + private val txHistoryItemFlowConverter by lazy { + TokenDetailsTxHistoryItemFlowConverter( + currentStateProvider = currentStateProvider, + symbol = symbol, + decimals = decimals, + clickIntents = clickIntents, + ) + } + + override fun convert(value: Either>>): TokenDetailsState { + return value.fold(ifLeft = ::convertError, ifRight = ::convert) + } + + private fun convertError(error: TxHistoryListError): TokenDetailsState { + return currentStateProvider().copy( + txHistoryState = when (error) { + is TxHistoryListError.DataError -> { + TxHistoryState.Error(onReloadClick = clickIntents::onReloadClick) + } + }, + ) + } + + private fun convert(items: Flow>): TokenDetailsState { + return currentStateProvider().copy( + txHistoryState = txHistoryItemFlowConverter.convert(value = items), + ) + } +} \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/txhistory/TokenDetailsLoadingTxHistoryConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/txhistory/TokenDetailsLoadingTxHistoryConverter.kt new file mode 100644 index 0000000000..9f98c1ccf3 --- /dev/null +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/txhistory/TokenDetailsLoadingTxHistoryConverter.kt @@ -0,0 +1,59 @@ +package com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.txhistory + +import androidx.paging.PagingData +import arrow.core.Either +import com.tangem.common.Provider +import com.tangem.core.ui.components.transactions.intents.TxHistoryClickIntents +import com.tangem.core.ui.components.transactions.state.TransactionState +import com.tangem.core.ui.components.transactions.state.TxHistoryState +import com.tangem.domain.txhistory.models.TxHistoryStateError +import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState +import com.tangem.utils.converter.Converter +import kotlinx.coroutines.flow.update + +internal class TokenDetailsLoadingTxHistoryConverter( + private val currentStateProvider: Provider, + private val clickIntents: TxHistoryClickIntents, +) : Converter, TokenDetailsState> { + + override fun convert(value: Either): TokenDetailsState { + return value.fold(ifLeft = ::convertError, ifRight = ::convert) + } + + private fun convertError(error: TxHistoryStateError): TokenDetailsState { + return currentStateProvider().copy( + txHistoryState = when (error) { + is TxHistoryStateError.EmptyTxHistories -> { + TxHistoryState.Empty(onBuyClick = clickIntents::onBuyClick) + } + is TxHistoryStateError.DataError -> { + TxHistoryState.Error(onReloadClick = clickIntents::onReloadClick) + } + is TxHistoryStateError.TxHistoryNotImplemented -> { + TxHistoryState.NotSupported(onExploreClick = clickIntents::onExploreClick) + } + }, + ) + } + + 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 state + } +} \ 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 new file mode 100644 index 0000000000..a8f2d95f25 --- /dev/null +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/txhistory/TokenDetailsTxHistoryItemFlowConverter.kt @@ -0,0 +1,230 @@ +package com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.txhistory + +import android.text.format.DateUtils +import androidx.paging.* +import com.tangem.common.Provider +import com.tangem.core.ui.components.transactions.intents.TxHistoryClickIntents +import com.tangem.core.ui.components.transactions.state.TransactionState +import com.tangem.core.ui.components.transactions.state.TxHistoryState +import com.tangem.core.ui.components.transactions.state.TxHistoryState.TxHistoryItemState +import com.tangem.core.ui.extensions.TextReference +import com.tangem.domain.txhistory.models.TxHistoryItem +import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState +import com.tangem.features.tokendetails.impl.R +import com.tangem.utils.converter.Converter +import com.tangem.utils.extensions.isToday +import com.tangem.utils.extensions.isYesterday +import com.tangem.utils.toBriefAddressFormat +import com.tangem.utils.toFormattedCurrencyString +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.* +import org.joda.time.DateTime +import org.joda.time.DateTimeZone +import org.joda.time.format.DateTimeFormatterBuilder +import java.math.BigDecimal +import java.util.Locale + +internal class TokenDetailsTxHistoryItemFlowConverter( + private val currentStateProvider: Provider, + private val symbol: String, + private val decimals: Int, + private val clickIntents: TxHistoryClickIntents, +) : Converter>, TxHistoryState> { + + /** Example, 2 Aug, 2023 */ + private val dateFormatter by lazy { + DateTimeFormatterBuilder() + .appendDayOfMonth(1) + .appendLiteral(' ') + .appendMonthOfYearShortText() + .appendLiteral(", ") + .appendYear(4, 4) + .toFormatter() + .withLocale(Locale.getDefault()) + } + + /** Example, 13:35 */ + private val timeFormatter by lazy { + DateTimeFormatterBuilder() + .appendHourOfDay(1) + .appendLiteral(':') + .appendMinuteOfHour(2) + .toFormatter() + .withLocale(Locale.getDefault()) + } + + override fun convert(value: Flow>): TxHistoryState { + val txHistoryContent = currentStateProvider().txHistoryState as TxHistoryState.Content + + // FIXME: TxHistoryRepository should send loading transactions + // [REDACTED_JIRA] + value + .onEach { txHistoryStatePagingData -> + txHistoryContent.contentItems.update { + txHistoryStatePagingData + .map { item -> + // [createTransactionState] returns timestamp without formatting + TxHistoryItemState.Transaction(state = createTransactionState(item)) + } + .insertHeaderItem( + terminalSeparatorType = TerminalSeparatorType.SOURCE_COMPLETE, + item = TxHistoryItemState.Title(clickIntents::onExploreClick), + ) + .insertGroupTitle() // method uses the raw timestamp + .formatTransactionsTimestamp() // method formats the timestamp + } + } + .launchIn(CoroutineScope(Dispatchers.IO)) + + return txHistoryContent + } + + private fun createTransactionState(item: TxHistoryItem): TransactionState { + return when (item.type) { + TxHistoryItem.TransactionType.Transfer -> { + when (val direction = item.direction) { + is TxHistoryItem.TransactionDirection.Incoming -> { + createIncomingTransferTransaction(item, direction) + } + is TxHistoryItem.TransactionDirection.Outgoing -> { + createOutgoingTransferTransaction(item, direction) + } + } + } + } + } + + private fun createIncomingTransferTransaction( + item: TxHistoryItem, + direction: TxHistoryItem.TransactionDirection.Incoming, + ): TransactionState { + return when (item.status) { + TxHistoryItem.TxStatus.Confirmed -> TransactionState.Receive( + txHash = item.txHash, + address = direction.extractAddress(), + amount = item.amount.toCryptoCurrencyFormat(), + timestamp = item.getRawTimestamp(), + ) + TxHistoryItem.TxStatus.Unconfirmed -> TransactionState.Receiving( + txHash = item.txHash, + address = direction.extractAddress(), + amount = item.amount.toCryptoCurrencyFormat(), + timestamp = item.getRawTimestamp(), + ) + } + } + + private fun createOutgoingTransferTransaction( + item: TxHistoryItem, + direction: TxHistoryItem.TransactionDirection.Outgoing, + ): TransactionState { + return when (item.status) { + TxHistoryItem.TxStatus.Confirmed -> TransactionState.Send( + txHash = item.txHash, + address = direction.extractAddress(), + amount = item.amount.toCryptoCurrencyFormat(), + timestamp = item.getRawTimestamp(), + ) + TxHistoryItem.TxStatus.Unconfirmed -> TransactionState.Sending( + txHash = item.txHash, + address = direction.extractAddress(), + amount = item.amount.toCryptoCurrencyFormat(), + timestamp = item.getRawTimestamp(), + ) + } + } + + private fun BigDecimal.toCryptoCurrencyFormat(): String { + return toFormattedCurrencyString(currency = symbol, decimals = decimals) + } + + private fun PagingData.insertGroupTitle(): PagingData { + return insertSeparators(terminalSeparatorType = TerminalSeparatorType.SOURCE_COMPLETE) { before, after -> + // Use raw timestamp to get date + + // If [afterDate] is the first transaction in the flow, add the group title + val afterDate = after.getTimestamp()?.toDateFormat() ?: return@insertSeparators null + if (before is TxHistoryItemState.Title) { + return@insertSeparators TxHistoryItemState.GroupTitle(afterDate) + } + + /* + * If [beforeDate] is not equals to [afterDate], then [afterDate] is first transaction in + * the new group + */ + val beforeDate = before.getTimestamp()?.toDateFormat() ?: return@insertSeparators null + return@insertSeparators if (beforeDate != afterDate) { + TxHistoryItemState.GroupTitle(afterDate) + } else { + null + } + } + } + + /** + * Map the [PagingData] to format the [TxHistoryItemState] timestamp + */ + private fun PagingData.formatTransactionsTimestamp(): PagingData { + return map { txHistoryItemState -> + if (txHistoryItemState is TxHistoryItemState.Transaction && + txHistoryItemState.state is TransactionState.Content + ) { + val txContent = txHistoryItemState.state as TransactionState.Content + txHistoryItemState.copy( + state = txContent.copySealed( + timestamp = txContent.timestamp.toTimeFormat(), + ), + ) + } else { + txHistoryItemState + } + } + } + + /** + * Get timestamp without formatting. + * It's life hack that help us to add transaction's group title to flow. + * + * @see [convert] + */ + private fun TxHistoryItem.getRawTimestamp() = this.timestampInMillis.toString() + + private fun TxHistoryItemState?.getTimestamp(): Long? { + return if (this is TxHistoryItemState.Transaction && this.state is TransactionState.Content) { + val txContent = this.state as TransactionState.Content + requireNotNull(txContent.timestamp.toLongOrNull()) { "Timestamp must be Long type" } + } else { + null + } + } + + /** + * If [this] timestamp is today or yesterday, returns relative date, + * otherwise returns formatting date by [dateFormatter] + */ + private fun Long.toDateFormat(): String { + val localDate = DateTime(this, DateTimeZone.getDefault()) + return if (localDate.isToday() || localDate.isYesterday()) { + DateUtils.getRelativeTimeSpanString( + this, + DateTime.now().millis, + DateUtils.DAY_IN_MILLIS, + DateUtils.FORMAT_ABBREV_RELATIVE, + ).toString() + } else { + dateFormatter.print(localDate) + } + } + + private fun String.toTimeFormat(): String { + return timeFormatter.print( + DateTime(this.toLong(), DateTimeZone.getDefault()), + ) + } + + private fun TxHistoryItem.TransactionDirection.extractAddress(): TextReference = when (val addr = address) { + TxHistoryItem.Address.Multiple -> TextReference.Res(R.string.transaction_history_multiple_addresses) + is TxHistoryItem.Address.Single -> TextReference.Str(addr.rawAddress.toBriefAddressFormat()) + } +} \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/TokenDetailsScreen.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/TokenDetailsScreen.kt index a8a20dfa34..a54eef8db7 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/TokenDetailsScreen.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/TokenDetailsScreen.kt @@ -1,14 +1,17 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.ui -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.material3.Scaffold import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview +import androidx.paging.compose.collectAsLazyPagingItems import com.tangem.core.ui.components.marketprice.MarketPriceBlock +import com.tangem.core.ui.components.marketprice.MarketPriceBlockState +import com.tangem.core.ui.components.transactions.state.TxHistoryState +import com.tangem.core.ui.components.transactions.txHistoryItems import com.tangem.core.ui.res.TangemTheme import com.tangem.feature.tokendetails.presentation.tokendetails.TokenDetailsPreviewData import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState @@ -22,16 +25,36 @@ internal fun TokenDetailsScreen(state: TokenDetailsState) { topBar = { TokenDetailsTopAppBar(config = state.topAppBarConfig) }, containerColor = TangemTheme.colors.background.secondary, ) { scaffoldPaddings -> - Column( + val txHistoryItems = if (state.txHistoryState is TxHistoryState.Content) { + state.txHistoryState.contentItems.collectAsLazyPagingItems() + } else { + null + } + val betweenItemsPadding = TangemTheme.dimens.spacing12 + val horizontalPadding = TangemTheme.dimens.spacing16 + val itemModifier = Modifier + .padding(top = betweenItemsPadding) + .padding(horizontal = horizontalPadding) + LazyColumn( modifier = Modifier .padding(paddingValues = scaffoldPaddings) - .padding(horizontal = TangemTheme.dimens.spacing16) .fillMaxSize(), - verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), ) { - TokenInfoBlock(state = state.tokenInfoBlockState) - TokenDetailsBalanceBlock(state = state.tokenBalanceBlockState) - MarketPriceBlock(state = state.marketPriceBlockState) + item { + TokenInfoBlock( + modifier = Modifier + .padding(top = TangemTheme.dimens.spacing4) + .padding(horizontal = horizontalPadding), + state = state.tokenInfoBlockState, + ) + } + item { TokenDetailsBalanceBlock(modifier = itemModifier, state = state.tokenBalanceBlockState) } + item( + key = MarketPriceBlockState::class.java, + contentType = MarketPriceBlockState::class.java, + content = { MarketPriceBlock(modifier = itemModifier, state = state.marketPriceBlockState) }, + ) + txHistoryItems(state = state.txHistoryState, txHistoryItems = txHistoryItems) } } } diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenDetailsBalanceBlock.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenDetailsBalanceBlock.kt index df54b41461..b1b9ac832a 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenDetailsBalanceBlock.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenDetailsBalanceBlock.kt @@ -15,7 +15,9 @@ import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.feature.tokendetails.presentation.tokendetails.TokenDetailsPreviewData import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsBalanceBlockState +import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.TokenDetailsActionButton import com.tangem.features.tokendetails.impl.R +import kotlinx.collections.immutable.toImmutableList @Composable internal fun TokenDetailsBalanceBlock(state: TokenDetailsBalanceBlockState, modifier: Modifier = Modifier) { @@ -51,7 +53,7 @@ internal fun TokenDetailsBalanceBlock(state: TokenDetailsBalanceBlockState, modi ) HorizontalActionChips( - buttons = state.actionButtons, + buttons = state.actionButtons.map(TokenDetailsActionButton::config).toImmutableList(), modifier = Modifier.padding(vertical = TangemTheme.dimens.spacing12), contentPadding = PaddingValues(horizontal = TangemTheme.dimens.spacing12), ) diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsClickIntents.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsClickIntents.kt new file mode 100644 index 0000000000..fba6130de7 --- /dev/null +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsClickIntents.kt @@ -0,0 +1,18 @@ +package com.tangem.feature.tokendetails.presentation.tokendetails.viewmodels + +import com.tangem.core.ui.components.transactions.intents.TxHistoryClickIntents + +interface TokenDetailsClickIntents : TxHistoryClickIntents { + + fun onBackClick() + + fun onMoreClick() + + fun onSendClick() + + fun onReceiveClick() + + fun onSellClick() + + fun onSwapClick() +} \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt index 5dfeaf2b82..f4defde6c4 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt @@ -3,67 +3,202 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.viewmodels import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue -import androidx.lifecycle.SavedStateHandle -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope +import androidx.lifecycle.* +import androidx.paging.cachedIn +import arrow.core.getOrElse +import com.tangem.common.Provider +import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.redux.ReduxStateHolder +import com.tangem.domain.tokens.GetCryptoCurrencyActionsUseCase +import com.tangem.domain.tokens.GetCurrencyStatusUpdatesUseCase +import com.tangem.domain.tokens.legacy.TradeCryptoAction +import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.models.CryptoCurrency +import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsCountUseCase +import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsUseCase +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.domain.wallets.usecase.GetExploreUrlUseCase +import com.tangem.domain.wallets.usecase.GetSelectedWalletUseCase import com.tangem.feature.tokendetails.presentation.router.InnerTokenDetailsRouter -import com.tangem.feature.tokendetails.presentation.tokendetails.TokenDetailsPreviewData -import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenInfoBlockState -import com.tangem.features.tokendetails.impl.R +import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState +import com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.TokenDetailsStateFactory import com.tangem.features.tokendetails.navigation.TokenDetailsRouter +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import com.tangem.utils.coroutines.JobHolder +import com.tangem.utils.coroutines.saveIn import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch import javax.inject.Inject import kotlin.properties.Delegates -private const val LOADING_DELAY = 4_000L - +@Suppress("LongParameterList") @HiltViewModel internal class TokenDetailsViewModel @Inject constructor( + private val dispatchers: CoroutineDispatcherProvider, + private val getSelectedWalletUseCase: GetSelectedWalletUseCase, + private val getCurrencyStatusUpdatesUseCase: GetCurrencyStatusUpdatesUseCase, + private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, + private val txHistoryItemsCountUseCase: GetTxHistoryItemsCountUseCase, + private val txHistoryItemsUseCase: GetTxHistoryItemsUseCase, + private val getExploreUrlUseCase: GetExploreUrlUseCase, + private val getCryptoCurrencyActionsUseCase: GetCryptoCurrencyActionsUseCase, + private val reduxStateHolder: ReduxStateHolder, savedStateHandle: SavedStateHandle, -) : ViewModel() { +) : ViewModel(), DefaultLifecycleObserver, TokenDetailsClickIntents { private val cryptoCurrency: CryptoCurrency = savedStateHandle[TokenDetailsRouter.SELECTED_CURRENCY_KEY] ?: error("no expected parameter CryptoCurrency found") var router by Delegates.notNull() - var uiState by mutableStateOf(getInitialState()) + private val marketPriceJobHolder = JobHolder() + private var cryptoCurrencyStatus: CryptoCurrencyStatus? = null + private var wallet by Delegates.notNull() + + private val selectedAppCurrencyFlow: StateFlow = createSelectedAppCurrencyFlow() + private val stateFactory = TokenDetailsStateFactory( + currentStateProvider = Provider { uiState }, + appCurrencyProvider = Provider(selectedAppCurrencyFlow::value), + clickIntents = this, + symbol = cryptoCurrency.symbol, + decimals = cryptoCurrency.decimals, + ) + var uiState: TokenDetailsState by mutableStateOf(stateFactory.getInitialState(cryptoCurrency)) private set - init { - // simulate loading state - viewModelScope.launch { - delay(LOADING_DELAY) - uiState = uiState.copy( - tokenBalanceBlockState = TokenDetailsPreviewData.balanceContent, - marketPriceBlockState = TokenDetailsPreviewData.marketPriceContent, + override fun onCreate(owner: LifecycleOwner) { + getWallet() + updateContent(selectedWallet = wallet) + } + + private fun getWallet() { + return getSelectedWalletUseCase() + .fold( + ifLeft = { error("Can not get selected wallet $it") }, + ifRight = { wallet = it }, ) + } + + private fun updateContent(selectedWallet: UserWallet) { + updateMarketPrice(selectedWallet = selectedWallet) + updateButtons(userWalletId = selectedWallet.walletId, currencyId = cryptoCurrency.id.value) + updateTxHistory() + } + + private fun updateButtons(userWalletId: UserWalletId, currencyId: String) { + getCryptoCurrencyActionsUseCase(userWalletId = userWalletId, tokenId = currencyId) + .distinctUntilChanged() + .onEach { uiState = stateFactory.getManageButtonsState(actions = it.states) } + .flowOn(dispatchers.io) + .launchIn(viewModelScope) + } + + private fun updateMarketPrice(selectedWallet: UserWallet) { + getCurrencyStatusUpdatesUseCase( + userWalletId = selectedWallet.walletId, + currencyId = cryptoCurrency.id, + ) + .distinctUntilChanged() + .onEach { either -> + uiState = stateFactory.getCurrencyLoadedBalanceState(either) + either.onRight { status -> cryptoCurrencyStatus = status } + } + .flowOn(dispatchers.io) + .launchIn(viewModelScope) + .saveIn(marketPriceJobHolder) + } + + private fun updateTxHistory() { + viewModelScope.launch(dispatchers.io) { + val txHistoryItemsCountEither = txHistoryItemsCountUseCase( + networkId = cryptoCurrency.network.id, + derivationPath = cryptoCurrency.derivationPath, + ) + + uiState = stateFactory.getLoadingTxHistoryState(itemsCountEither = txHistoryItemsCountEither) + + txHistoryItemsCountEither.onRight { + uiState = stateFactory.getLoadedTxHistoryState( + txHistoryEither = txHistoryItemsUseCase( + networkId = cryptoCurrency.network.id, + derivationPath = cryptoCurrency.derivationPath, + ).map { + it.cachedIn(viewModelScope) + }, + ) + } } } - private fun getInitialState() = TokenDetailsPreviewData.tokenDetailsState.copy( - topAppBarConfig = TokenDetailsPreviewData.tokenDetailsTopAppBarConfig.copy( - onBackClick = ::onBackClick, - ), - tokenInfoBlockState = TokenInfoBlockState( - name = cryptoCurrency.name, - iconUrl = requireNotNull(cryptoCurrency.iconUrl), - currency = when (cryptoCurrency) { - is CryptoCurrency.Coin -> TokenInfoBlockState.Currency.Native - is CryptoCurrency.Token -> TokenInfoBlockState.Currency.Token( - networkName = cryptoCurrency.standardType.name, - blockchainName = cryptoCurrency.blockchainName, - // TODO: [REDACTED_JIRA] - networkIcon = R.drawable.img_eth_22, - ) - }, - ), - ) + private fun createSelectedAppCurrencyFlow(): StateFlow { + return getSelectedAppCurrencyUseCase() + .map { maybeAppCurrency -> + maybeAppCurrency.getOrElse { AppCurrency.Default } + } + .stateIn( + scope = viewModelScope, + started = SharingStarted.Eagerly, + initialValue = AppCurrency.Default, + ) + } - private fun onBackClick() { + override fun onBackClick() { router.popBackStack() } + + override fun onMoreClick() { + TODO("Not yet implemented") + } + + override fun onBuyClick() { + val status = cryptoCurrencyStatus ?: return + + reduxStateHolder.dispatch( + TradeCryptoAction.New.Buy( + userWallet = wallet, + cryptoCurrencyStatus = status, + appCurrencyCode = selectedAppCurrencyFlow.value.code, + ), + ) + } + + override fun onReloadClick() { + updateTxHistory() + } + + override fun onSendClick() { + reduxStateHolder.dispatch(TradeCryptoAction.New.Send) + } + + override fun onReceiveClick() { + // TODO: [REDACTED_JIRA] + } + + override fun onSellClick() { + val status = cryptoCurrencyStatus ?: return + reduxStateHolder.dispatch( + TradeCryptoAction.New.Sell( + cryptoCurrencyStatus = status, + appCurrencyCode = selectedAppCurrencyFlow.value.code, + ), + ) + } + + override fun onSwapClick() { + reduxStateHolder.dispatch(TradeCryptoAction.New.Swap(cryptoCurrency)) + } + + override fun onExploreClick() { + viewModelScope.launch { + router.openUrl( + url = getExploreUrlUseCase( + userWalletId = wallet.walletId, + networkId = cryptoCurrency.network.id, + ), + ) + } + } } \ No newline at end of file diff --git a/features/wallet/impl/build.gradle.kts b/features/wallet/impl/build.gradle.kts index 48a2eca7c6..0e84af12c3 100644 --- a/features/wallet/impl/build.gradle.kts +++ b/features/wallet/impl/build.gradle.kts @@ -31,6 +31,7 @@ dependencies { implementation(deps.arrow.core) implementation(deps.jodatime) implementation(deps.kotlin.immutable.collections) + implementation(deps.reKotlin) implementation(deps.tangem.card.core) implementation(deps.tangem.blockchain) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/di/WalletRouterModule.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/di/WalletRouterModule.kt index 721bffead3..1e64fc4122 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/di/WalletRouterModule.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/di/WalletRouterModule.kt @@ -1,6 +1,6 @@ package com.tangem.feature.wallet.di -import com.tangem.core.navigation.NavigationStateHolder +import com.tangem.core.navigation.ReduxNavController import com.tangem.feature.wallet.presentation.router.DefaultWalletRouter import com.tangem.features.wallet.navigation.WalletRouter import dagger.Module @@ -15,7 +15,7 @@ internal object WalletRouterModule { @Provides @ActivityScoped - fun provideWalletRouter(navigationStateHolder: NavigationStateHolder): WalletRouter { - return DefaultWalletRouter(navigationStateHolder = navigationStateHolder) + fun provideWalletRouter(reduxNavController: ReduxNavController): WalletRouter { + return DefaultWalletRouter(reduxNavController = reduxNavController) } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/WalletFragment.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/WalletFragment.kt index f33c221c8d..91a98afe61 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/WalletFragment.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/WalletFragment.kt @@ -1,15 +1,11 @@ package com.tangem.feature.wallet.presentation -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.compose.ui.platform.ComposeView -import androidx.fragment.app.Fragment -import androidx.transition.TransitionInflater +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier import com.tangem.core.ui.components.SystemBarsEffect import com.tangem.core.ui.res.TangemTheme -import com.tangem.feature.wallet.impl.R +import com.tangem.core.ui.screen.ComposeFragment +import com.tangem.core.ui.theme.AppThemeModeHolder import com.tangem.feature.wallet.presentation.router.InnerWalletRouter import com.tangem.features.wallet.navigation.WalletRouter import dagger.hilt.android.AndroidEntryPoint @@ -21,7 +17,10 @@ import javax.inject.Inject [REDACTED_AUTHOR] */ @AndroidEntryPoint -internal class WalletFragment : Fragment() { +internal class WalletFragment : ComposeFragment() { + + @Inject + override lateinit var appThemeModeHolder: AppThemeModeHolder /** Feature router */ @Inject @@ -32,25 +31,14 @@ internal class WalletFragment : Fragment() { "_walletRouter should be instance of InnerWalletRouter" } - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { - with(TransitionInflater.from(requireContext())) { - enterTransition = inflateTransition(R.transition.slide_right) - exitTransition = inflateTransition(R.transition.fade) + @Composable + override fun ScreenContent(modifier: Modifier) { + val systemBarsColor = TangemTheme.colors.background.secondary + SystemBarsEffect { + setSystemBarsColor(systemBarsColor) } - return ComposeView(inflater.context).apply { - setContent { - TangemTheme { - val systemBarsColor = TangemTheme.colors.background.secondary - SystemBarsEffect { - setSystemBarsColor(systemBarsColor) - } - - isTransitionGroup = true - _walletRouter.Initialize(fragmentManager = requireActivity().supportFragmentManager) - } - } - } + _walletRouter.Initialize(fragmentManager = requireActivity().supportFragmentManager) } companion object { 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 d0bb5cc8e7..60f3ec11ff 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/WalletPreviewData.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/WalletPreviewData.kt @@ -6,6 +6,7 @@ import com.tangem.core.ui.components.marketprice.MarketPriceBlockState import com.tangem.core.ui.components.marketprice.PriceChangeConfig import com.tangem.core.ui.components.transactions.state.TransactionState import com.tangem.core.ui.components.transactions.state.TxHistoryState +import com.tangem.core.ui.event.consumed import com.tangem.core.ui.extensions.TextReference import com.tangem.domain.wallets.models.UserWalletId import com.tangem.feature.wallet.presentation.common.state.TokenItemState @@ -17,8 +18,9 @@ import com.tangem.feature.wallet.presentation.wallet.state.* import com.tangem.feature.wallet.presentation.wallet.state.components.* import com.tangem.feature.wallet.presentation.wallet.state.components.WalletTokensListState.TokensListItemState import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toPersistentList -import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.MutableStateFlow import java.util.UUID @Suppress("LargeClass") @@ -28,12 +30,13 @@ internal object WalletPreviewData { val walletCardContentState by lazy { WalletCardState.Content( - id = UserWalletId("123"), + id = UserWalletId(stringValue = "123"), title = "Wallet 1", balance = "8923,05 $", - additionalInfo = "3 cards • Seed enabled", + additionalInfo = TextReference.Str("3 cards • Seed phrase"), imageResId = R.drawable.ill_businessman_3d, - onClick = null, + onRenameClick = { _, _ -> }, + onDeleteClick = {}, ) } @@ -41,9 +44,9 @@ internal object WalletPreviewData { WalletCardState.Loading( id = UserWalletId("321"), title = "Wallet 1", - additionalInfo = "3 cards • Seed enabled", imageResId = R.drawable.ill_businessman_3d, - onClick = null, + onRenameClick = { _, _ -> }, + onDeleteClick = {}, ) } @@ -51,9 +54,9 @@ internal object WalletPreviewData { WalletCardState.HiddenContent( id = UserWalletId("42"), title = "Wallet 1", - additionalInfo = "3 cards • Seed enabled", imageResId = R.drawable.ill_businessman_3d, - onClick = null, + onRenameClick = { _, _ -> }, + onDeleteClick = {}, ) } @@ -61,9 +64,9 @@ internal object WalletPreviewData { WalletCardState.Error( id = UserWalletId("24"), title = "Wallet 1", - additionalInfo = "3 cards • Seed enabled", imageResId = R.drawable.ill_businessman_3d, - onClick = null, + onRenameClick = { _, _ -> }, + onDeleteClick = {}, ) } @@ -89,18 +92,27 @@ internal object WalletPreviewData { id = UUID.randomUUID().toString(), tokenIconUrl = null, tokenIconResId = R.drawable.img_polygon_22, - networkIconResId = R.drawable.img_polygon_22, + networkBadgeIconResId = R.drawable.img_polygon_22, name = "Polygon", amount = "5,412 MATIC", hasPending = true, tokenOptions = TokenOptionsState.Visible( fiatAmount = "321 $", - priceChange = PriceChangeConfig( + config = PriceChangeConfig( valueInPercent = "2%", type = PriceChangeConfig.Type.UP, ), ), - onClick = {}, + isTestnet = false, + onItemClick = {}, + onItemLongClick = {}, + ) + } + + val testnetTokenItemVisibleState by lazy { + tokenItemVisibleState.copy( + name = "Polygon testnet", + isTestnet = true, ) } @@ -109,17 +121,19 @@ internal object WalletPreviewData { id = UUID.randomUUID().toString(), tokenIconUrl = null, tokenIconResId = R.drawable.img_polygon_22, - networkIconResId = R.drawable.img_polygon_22, + networkBadgeIconResId = R.drawable.img_polygon_22, name = "Polygon", amount = "5,412 MATIC", hasPending = true, tokenOptions = TokenOptionsState.Hidden( - priceChange = PriceChangeConfig( + config = PriceChangeConfig( valueInPercent = "2%", type = PriceChangeConfig.Type.UP, ), ), - onClick = {}, + isTestnet = false, + onItemClick = {}, + onItemLongClick = {}, ) } @@ -128,8 +142,9 @@ internal object WalletPreviewData { id = UUID.randomUUID().toString(), tokenIconUrl = null, tokenIconResId = R.drawable.img_polygon_22, - networkIconResId = R.drawable.img_polygon_22, + networkBadgeIconResId = R.drawable.img_polygon_22, name = "Polygon", + isTestnet = false, fiatAmount = "3 172,14 $", ) } @@ -139,7 +154,7 @@ internal object WalletPreviewData { id = UUID.randomUUID().toString(), tokenIconUrl = null, tokenIconResId = R.drawable.img_polygon_22, - networkIconResId = R.drawable.img_polygon_22, + networkBadgeIconResId = R.drawable.img_polygon_22, name = "Polygon", ) } @@ -148,7 +163,7 @@ internal object WalletPreviewData { private const val networksSize = 10 private const val tokensSize = 3 - val draggableItems by lazy { + private val draggableItems by lazy { List(networksSize) { it } .flatMap { index -> val lastNetworkIndex = networksSize - 1 @@ -173,7 +188,7 @@ internal object WalletPreviewData { tokenItemState = tokenItemDragState.copy( id = "${group.id}_token_$tokenNumber", name = "Token $tokenNumber from $networkNumber network", - networkIconResId = R.drawable.img_eth_22.takeIf { i != 0 }, + networkBadgeIconResId = R.drawable.img_eth_22.takeIf { i != 0 }, ), groupId = group.id, roundingMode = when { @@ -197,7 +212,7 @@ internal object WalletPreviewData { .toPersistentList() } - val draggableTokens by lazy { + private val draggableTokens by lazy { draggableItems .filterIsInstance() .toMutableList() @@ -227,6 +242,7 @@ internal object WalletPreviewData { onApplyClick = {}, onCancelClick = {}, ), + scrollListToTop = consumed, ) } @@ -249,13 +265,25 @@ internal object WalletPreviewData { ) } + val actionsBottomSheet = ActionsBottomSheetConfig( + isShow = true, + onDismissRequest = {}, + actions = listOf( + TokenActionButtonConfig( + text = "Send", + iconResId = R.drawable.ic_share_24, + onClick = {}, + ), + ).toImmutableList(), + ) + private val manageButtons by lazy { persistentListOf( - WalletManageButton.Buy(onClick = {}), - WalletManageButton.Send(onClick = {}), + WalletManageButton.Buy(enabled = true, onClick = {}), + WalletManageButton.Send(enabled = true, onClick = {}), WalletManageButton.Receive(onClick = {}), - WalletManageButton.Exchange(onClick = {}), - WalletManageButton.CopyAddress(onClick = {}), + WalletManageButton.Sell(enabled = true, onClick = {}), + WalletManageButton.Swap(enabled = true, onClick = {}), ) } @@ -272,7 +300,7 @@ internal object WalletPreviewData { id = "token_1", name = "Ethereum", tokenIconResId = R.drawable.img_eth_22, - networkIconResId = null, + networkBadgeIconResId = null, amount = "1,89340821 ETH", ), ), @@ -281,7 +309,7 @@ internal object WalletPreviewData { id = "token_2", name = "Ethereum", tokenIconResId = R.drawable.img_eth_22, - networkIconResId = null, + networkBadgeIconResId = null, amount = "1,89340821 ETH", ), ), @@ -290,7 +318,7 @@ internal object WalletPreviewData { id = "token_3", name = "Ethereum", tokenIconResId = R.drawable.img_eth_22, - networkIconResId = null, + networkBadgeIconResId = null, amount = "1,89340821 ETH", ), ), @@ -299,7 +327,7 @@ internal object WalletPreviewData { id = "token_4", name = "Ethereum", tokenIconResId = R.drawable.img_eth_22, - networkIconResId = null, + networkBadgeIconResId = null, amount = "1,89340821 ETH", ), ), @@ -309,7 +337,7 @@ internal object WalletPreviewData { id = "token_5", name = "Ethereum", tokenIconResId = R.drawable.img_eth_22, - networkIconResId = null, + networkBadgeIconResId = null, amount = "1,89340821 ETH", ), ), @@ -327,6 +355,8 @@ internal object WalletPreviewData { WalletNotification.ScanCard(onClick = {}), ), bottomSheetConfig = bottomSheet, + tokenActionsBottomSheet = actionsBottomSheet, + onManageTokensClick = {}, ) } @@ -351,15 +381,14 @@ internal object WalletPreviewData { ), ), txHistoryState = TxHistoryState.Content( - flowOf( + contentItems = MutableStateFlow( PagingData.from( listOf( - TxHistoryState.TxHistoryItemState.Title(onExploreClick = {}), TxHistoryState.TxHistoryItemState.GroupTitle("Today"), TxHistoryState.TxHistoryItemState.Transaction( TransactionState.Sending( txHash = UUID.randomUUID().toString(), - address = "33BddS...ga2B", + address = TextReference.Str("33BddS...ga2B"), amount = "-0.500913 BTC", timestamp = "8:41", ), @@ -368,7 +397,7 @@ internal object WalletPreviewData { TxHistoryState.TxHistoryItemState.Transaction( TransactionState.Sending( txHash = UUID.randomUUID().toString(), - address = "33BddS...ga2B", + address = TextReference.Str("33BddS...ga2B"), amount = "-0.500913 BTC", timestamp = "8:41", ), 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 0e9751d451..6169aa9a8c 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/TokenItem.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/TokenItem.kt @@ -1,265 +1,102 @@ package com.tangem.feature.wallet.presentation.common.component -import androidx.annotation.DrawableRes -import androidx.compose.animation.AnimatedContent -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.ExperimentalAnimationApi -import androidx.compose.foundation.Image +import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background +import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.layout.* -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.material3.Icon -import androidx.compose.material3.Surface -import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.ReadOnlyComposable import androidx.compose.runtime.Stable -import androidx.compose.ui.Alignment +import androidx.compose.runtime.remember import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource +import androidx.compose.ui.hapticfeedback.HapticFeedbackType +import androidx.compose.ui.platform.LocalHapticFeedback 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.constraintlayout.compose.ConstrainedLayoutReference import androidx.constraintlayout.compose.ConstraintLayout import androidx.constraintlayout.compose.ConstraintLayoutScope import androidx.constraintlayout.compose.Dimension -import coil.compose.SubcomposeAsyncImage -import coil.request.ImageRequest import com.tangem.core.ui.components.* -import com.tangem.core.ui.components.marketprice.PriceChangeConfig import com.tangem.core.ui.res.TangemTheme -import com.tangem.core.ui.res.TangemTypography -import com.tangem.feature.wallet.impl.R import com.tangem.feature.wallet.presentation.common.WalletPreviewData +import com.tangem.feature.wallet.presentation.common.component.token.TokenCryptoInfoBlock +import com.tangem.feature.wallet.presentation.common.component.token.TokenFiatInfoBlock +import com.tangem.feature.wallet.presentation.common.component.token.TokenIcon import com.tangem.feature.wallet.presentation.common.state.TokenItemState -import com.tangem.feature.wallet.presentation.common.state.TokenItemState.TokenOptionsState import org.burnoutcrew.reorderable.ReorderableLazyListState -import org.burnoutcrew.reorderable.detectReorder - -private const val DOTS = "•••" -val TOKEN_ITEM_HEIGHT: Dp - @Composable - @ReadOnlyComposable - get() = TangemTheme.dimens.size68 +// TODO: Add custom token state: [REDACTED_JIRA] +@OptIn(ExperimentalFoundationApi::class) @Composable -internal fun TokenItem(state: TokenItemState, modifier: Modifier = Modifier) { - when (state) { - is TokenItemState.Content -> ContentTokenItem(state, modifier) - is TokenItemState.Loading -> LoadingTokenItem(modifier) - is TokenItemState.Draggable -> DraggableTokenItem(state, modifier, reorderableTokenListState = null) - is TokenItemState.Unreachable -> UnreachableTokenItem(state, modifier) - } -} - -@Composable -private fun ContentTokenItem(content: TokenItemState.Content, modifier: Modifier = Modifier) { - InternalTokenItem( - modifier = modifier, - onClick = content.onClick, - name = content.name, - tokenIconUrl = content.tokenIconUrl, - tokenIconResId = content.tokenIconResId, - networkIconResId = content.networkIconResId, - amount = if (content.tokenOptions is TokenOptionsState.Hidden) DOTS else content.amount, - hasPending = content.hasPending, - options = { ref -> - TokenOptionsBlock( - modifier = Modifier.constrainAsOptionsItem(scope = this, ref), - state = content.tokenOptions, - ) - }, - ) -} - -@Composable -internal fun DraggableTokenItem( - state: TokenItemState.Draggable, +internal fun TokenItem( + state: TokenItemState, modifier: Modifier = Modifier, reorderableTokenListState: ReorderableLazyListState? = null, ) { - InternalTokenItem( - modifier = modifier, - name = state.name, - tokenIconUrl = state.tokenIconUrl, - tokenIconResId = state.tokenIconResId, - networkIconResId = state.networkIconResId, - amount = state.fiatAmount, - hasPending = false, - options = { ref -> - Box( - modifier = Modifier - .size(TangemTheme.dimens.size32) - .constrainAsOptionsItem(scope = this, ref) - .let { - if (reorderableTokenListState != null) { - it.detectReorder(reorderableTokenListState) - } else { - it - } - }, - contentAlignment = Alignment.Center, - ) { - Icon( - painter = painterResource(id = R.drawable.ic_drag_24), - tint = TangemTheme.colors.icon.informative, - contentDescription = null, - ) - } - }, - ) -} - -@Composable -internal fun UnreachableTokenItem(state: TokenItemState.Unreachable, modifier: Modifier = Modifier) { - InternalTokenItem( - modifier = modifier, - name = state.name, - tokenIconUrl = state.tokenIconUrl, - tokenIconResId = state.tokenIconResId, - networkIconResId = state.networkIconResId, - amount = null, - hasPending = false, - options = { ref -> - Text( - modifier = Modifier.constrainAsOptionsItem(scope = this, ref), - text = stringResource(id = R.string.common_unreachable), - style = TangemTypography.body2, - color = TangemTheme.colors.text.tertiary, + val hapticFeedback = LocalHapticFeedback.current + val containerModifier: Modifier = remember(state) { + when (state) { + is TokenItemState.Content -> modifier.combinedClickable( + onClick = state.onItemClick, + onLongClick = { + hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress) + state.onItemLongClick() + }, ) - }, - ) -} + else -> modifier + } + } + BaseContainer(modifier = containerModifier) { + val (iconRef, cryptoInfoRef, fiatInfoRef) = createRefs() -@Composable -private fun LoadingTokenItem(modifier: Modifier = Modifier) { - BaseSurface(modifier) { - Row( + TokenIcon( + state = state, + modifier = Modifier.constrainAs(iconRef) { + centerVerticallyTo(parent) + start.linkTo(parent.start) + }, + ) + + TokenCryptoInfoBlock( + state = state, modifier = Modifier - .fillMaxWidth() - .padding( - horizontal = TangemTheme.dimens.spacing12, - vertical = TangemTheme.dimens.spacing4, - ), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing24), - ) { - CircleShimmer(modifier = Modifier.size(size = TangemTheme.dimens.size42)) - Row( - modifier = Modifier.fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.SpaceBetween, - ) { - Column(verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8)) { - RectangleShimmer( - modifier = Modifier.size( - width = TangemTheme.dimens.size72, - height = TangemTheme.dimens.size12, - ), - ) - RectangleShimmer( - modifier = Modifier.size( - width = TangemTheme.dimens.size50, - height = TangemTheme.dimens.size12, - ), - ) - } - Column(verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8)) { - RectangleShimmer( - modifier = Modifier.size( - width = TangemTheme.dimens.size40, - height = TangemTheme.dimens.size12, - ), - ) - RectangleShimmer( - modifier = Modifier.size( - width = TangemTheme.dimens.size40, - height = TangemTheme.dimens.size12, - ), - ) - } - } - } + .padding(horizontal = TangemTheme.dimens.spacing8) + .constrainAs(cryptoInfoRef) { + centerVerticallyTo(parent) + start.linkTo(iconRef.end) + end.linkTo(fiatInfoRef.start) + width = Dimension.fillToConstraints + }, + ) + + TokenFiatInfoBlock( + state = state, + modifier = Modifier.constrainAsOptionsItem(scope = this, ref = fiatInfoRef), + reorderableTokenListState = reorderableTokenListState, + ) } } -/** - * Block for end part of token item - * shows status is reachable, is drag, hidden or show balance - */ -@OptIn(ExperimentalAnimationApi::class) @Composable -private fun TokenOptionsBlock(state: TokenOptionsState, modifier: Modifier = Modifier) { - AnimatedContent(targetState = state, label = "Update the options", modifier = modifier) { options -> - when (options) { - is TokenOptionsState.Visible -> { - TokenFiatPercentageBlock(fiatAmount = options.fiatAmount, priceChange = options.priceChange) - } - is TokenOptionsState.Hidden -> { - TokenFiatPercentageBlock(fiatAmount = DOTS, priceChange = options.priceChange) - } - } - } -} - -@Suppress("LongParameterList") -@Composable -private fun InternalTokenItem( - name: String, - tokenIconUrl: String?, - @DrawableRes tokenIconResId: Int, - @DrawableRes networkIconResId: Int?, - amount: String?, - hasPending: Boolean, - options: @Composable ConstraintLayoutScope.(ref: ConstrainedLayoutReference) -> Unit, +private inline fun BaseContainer( modifier: Modifier = Modifier, - onClick: (() -> Unit)? = null, + crossinline content: @Composable ConstraintLayoutScope.() -> Unit, ) { - BaseSurface( - modifier = modifier, - onClick = onClick, + Box( + modifier = modifier + .defaultMinSize(minHeight = TangemTheme.dimens.size68) + .background(color = TangemTheme.colors.background.primary), ) { ConstraintLayout( modifier = Modifier - .fillMaxWidth() + .fillMaxSize() .padding( horizontal = TangemTheme.dimens.spacing14, - vertical = TangemTheme.dimens.spacing4, + vertical = TangemTheme.dimens.spacing14, ), - ) { - val (iconItem, tokenNameItem, optionsItem) = createRefs() - - TokenIcon( - modifier = Modifier.constrainAs(iconItem) { - centerVerticallyTo(parent) - start.linkTo(parent.start) - }, - tokenIconUrl = tokenIconUrl, - tokenIconResId = tokenIconResId, - networkIconRes = networkIconResId, - ) - - TokenTitleAmountBlock( - modifier = Modifier - .padding(horizontal = TangemTheme.dimens.spacing8) - .constrainAs(tokenNameItem) { - centerVerticallyTo(parent) - start.linkTo(iconItem.end) - end.linkTo(optionsItem.start) - width = Dimension.fillToConstraints - }, - title = name, - amount = amount, - hasPending = hasPending, - ) - - options(optionsItem) - } + content = content, + ) } } @@ -273,154 +110,6 @@ private fun Modifier.constrainAsOptionsItem(scope: ConstraintLayoutScope, ref: C } } -@Composable -private fun BaseSurface( - modifier: Modifier = Modifier, - onClick: (() -> Unit)? = null, - content: @Composable () -> Unit, -) { - Surface( - modifier = modifier.defaultMinSize(minHeight = TOKEN_ITEM_HEIGHT), - color = TangemTheme.colors.background.primary, - onClick = onClick ?: {}, - enabled = onClick != null, - ) { - content() - } -} - -@Composable -private fun TokenTitleAmountBlock(title: String, amount: String?, hasPending: Boolean, modifier: Modifier = Modifier) { - Column( - modifier = modifier, - verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing2), - ) { - Row(horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8)) { - Text( - text = title, - style = TangemTypography.subtitle2, - color = TangemTheme.colors.text.primary1, - ) - - AnimatedVisibility(visible = hasPending, modifier = Modifier.align(Alignment.CenterVertically)) { - Image( - painter = painterResource(id = R.drawable.img_loader_15), - contentDescription = null, - ) - } - } - - AnimatedVisibility(visible = !amount.isNullOrBlank()) { - Text( - text = requireNotNull(amount), - style = TangemTypography.body2, - color = TangemTheme.colors.text.tertiary, - ) - } - } -} - -@OptIn(ExperimentalAnimationApi::class) -@Composable -private fun TokenFiatPercentageBlock( - fiatAmount: String, - priceChange: PriceChangeConfig, - modifier: Modifier = Modifier, -) { - Column(modifier = modifier.requiredWidth(IntrinsicSize.Max)) { - Text( - modifier = Modifier.align(Alignment.End), - text = fiatAmount, - style = TangemTypography.body2, - color = TangemTheme.colors.text.primary1, - ) - SpacerH2() - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.End, - ) { - AnimatedContent( - targetState = priceChange.type, - label = "Update the price change's arrow", - modifier = Modifier.align(Alignment.CenterVertically), - ) { - Image( - painter = painterResource( - id = when (priceChange.type) { - PriceChangeConfig.Type.UP -> R.drawable.img_arrow_up_8 - PriceChangeConfig.Type.DOWN -> R.drawable.img_arrow_down_8 - }, - ), - contentDescription = null, - ) - } - - SpacerW4() - - AnimatedContent( - targetState = priceChange.type, - label = "Update the price change's arrow", - modifier = Modifier.align(Alignment.CenterVertically), - ) { - Text( - modifier = Modifier.align(Alignment.CenterVertically), - text = priceChange.valueInPercent, - style = TangemTypography.body2, - color = when (priceChange.type) { - PriceChangeConfig.Type.UP -> TangemTheme.colors.text.accent - PriceChangeConfig.Type.DOWN -> TangemTheme.colors.text.warning - }, - ) - } - } - } -} - -@Composable -private fun TokenIcon( - tokenIconUrl: String?, - modifier: Modifier = Modifier, - @DrawableRes tokenIconResId: Int? = null, - @DrawableRes networkIconRes: Int? = null, -) { - Box( - modifier = modifier - .padding(end = TangemTheme.dimens.spacing16) - .size(TangemTheme.dimens.size42), - ) { - val tokenImageModifier = Modifier - .align(Alignment.BottomStart) - .size(TangemTheme.dimens.size36) - - val data = if (tokenIconUrl.isNullOrEmpty()) tokenIconResId else tokenIconUrl - SubcomposeAsyncImage( - modifier = tokenImageModifier, - model = ImageRequest.Builder(LocalContext.current) - .data(data) - .crossfade(true) - .build(), - loading = { CircleShimmer(modifier = tokenImageModifier) }, - contentDescription = null, - ) - - AnimatedVisibility( - visible = networkIconRes != null, - modifier = Modifier - .align(Alignment.TopEnd) - .size(TangemTheme.dimens.size18) - .background(color = Color.White, shape = CircleShape), - ) { - Image( - modifier = Modifier - .padding(all = TangemTheme.dimens.spacing0_5) - .align(Alignment.Center), - painter = painterResource(id = requireNotNull(networkIconRes)), - contentDescription = null, - ) - } - } -} - // region preview @Preview @@ -446,6 +135,7 @@ private class TokenConfigProvider : CollectionPreviewParameterProvider ContentBlock(state = state, modifier = modifier) + is TokenItemState.Loading -> LoadingBlock(modifier = modifier) + is TokenItemState.Locked -> LockedBlock(modifier = modifier) + } +} + +@Composable +private fun ContentBlock(state: TokenItemState.ContentState, modifier: Modifier = Modifier) { + Column( + modifier = modifier, + verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing2), + ) { + CurrencyNameText( + name = state.name, + hasPending = (state as? TokenItemState.Content)?.hasPending == true, + ) + + AmountText( + amount = when (state) { + is TokenItemState.Content -> if (state.tokenOptions is TokenOptionsState.Hidden) DOTS else state.amount + is TokenItemState.Draggable -> state.fiatAmount + is TokenItemState.Unreachable -> null + }, + ) + } +} + +@Composable +private fun CurrencyNameText(name: String, hasPending: Boolean) { + Row(horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8)) { + Text( + text = name, + style = TangemTypography.subtitle2, + color = TangemTheme.colors.text.primary1, + ) + + PendingTransactionImage(hasPending = hasPending, modifier = Modifier.align(Alignment.CenterVertically)) + } +} + +@Composable +private fun PendingTransactionImage(hasPending: Boolean, modifier: Modifier = Modifier) { + AnimatedVisibility(visible = hasPending, modifier = modifier) { + Image( + painter = painterResource(id = R.drawable.img_loader_15), + contentDescription = null, + ) + } +} + +@Composable +private fun AmountText(amount: String?) { + AnimatedVisibility(visible = !amount.isNullOrBlank()) { + if (amount == null) return@AnimatedVisibility + Text( + text = amount, + style = TangemTypography.body2, + color = TangemTheme.colors.text.tertiary, + ) + } +} + +@Composable +private fun LoadingBlock(modifier: Modifier = Modifier) { + NonContentContainer(modifier = modifier) { + RectangleShimmer(modifier = Modifier.nameSize(), radius = TangemTheme.dimens.radius4) + RectangleShimmer(modifier = Modifier.amountSize(), radius = TangemTheme.dimens.radius4) + } +} + +@Composable +private fun LockedBlock(modifier: Modifier = Modifier) { + NonContentContainer(modifier = modifier) { + LockedRectangle(modifier = Modifier.nameSize()) + LockedRectangle(modifier = Modifier.amountSize()) + } +} + +@Composable +private fun NonContentContainer(modifier: Modifier = Modifier, content: @Composable ColumnScope.() -> Unit) { + Column( + modifier = modifier, + verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing10), + content = content, + ) +} + +private fun Modifier.nameSize(): Modifier = composed { + return@composed size(width = TangemTheme.dimens.size72, height = TangemTheme.dimens.size12) +} + +private fun Modifier.amountSize(): Modifier = composed { + return@composed size(width = TangemTheme.dimens.size50, height = TangemTheme.dimens.size12) +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/token/TokenFiatInfoBlock.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/token/TokenFiatInfoBlock.kt new file mode 100644 index 0000000000..ef5d0d5f72 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/token/TokenFiatInfoBlock.kt @@ -0,0 +1,180 @@ +package com.tangem.feature.wallet.presentation.common.component.token + +import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.ExperimentalAnimationApi +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.* +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.composed +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import com.tangem.core.ui.components.RectangleShimmer +import com.tangem.core.ui.components.SpacerW4 +import com.tangem.core.ui.components.marketprice.PriceChangeConfig +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemTypography +import com.tangem.feature.wallet.impl.R +import com.tangem.feature.wallet.presentation.common.state.TokenItemState +import com.tangem.feature.wallet.presentation.common.state.TokenItemState.Companion.DOTS +import com.tangem.feature.wallet.presentation.common.state.TokenItemState.TokenOptionsState +import org.burnoutcrew.reorderable.ReorderableLazyListState +import org.burnoutcrew.reorderable.detectReorder + +@Composable +internal fun TokenFiatInfoBlock( + state: TokenItemState, + modifier: Modifier = Modifier, + reorderableTokenListState: ReorderableLazyListState? = null, +) { + when (state) { + is TokenItemState.Content -> ContentBlock(state = state.tokenOptions, modifier = modifier) + is TokenItemState.Draggable -> { + DraggableBlock( + modifier = modifier, + reorderableTokenListState = reorderableTokenListState, + ) + } + is TokenItemState.Unreachable -> UnreachableBlock(modifier = modifier) + is TokenItemState.Loading -> LoadingBlock(modifier = modifier) + is TokenItemState.Locked -> LockedBlock(modifier = modifier) + } +} + +@OptIn(ExperimentalAnimationApi::class) +@Composable +private fun ContentBlock(state: TokenOptionsState, modifier: Modifier = Modifier) { + Column( + modifier = modifier.requiredWidth(IntrinsicSize.Max), + verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing2), + ) { + AnimatedContent( + targetState = state, + label = "Update the fiat percentage block", + modifier = Modifier.align(Alignment.End), + ) { + Text( + text = when (it) { + is TokenOptionsState.Visible -> it.fiatAmount + is TokenOptionsState.Hidden -> DOTS + }, + style = TangemTypography.body2, + color = TangemTheme.colors.text.primary1, + ) + } + + PriceChangeBlock(config = state.config) + } +} + +@Composable +private fun PriceChangeBlock(config: PriceChangeConfig) { + Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.End) { + PriceChangeIcon(type = config.type, modifier = Modifier.align(Alignment.CenterVertically)) + SpacerW4() + PriceChangeText(config = config, modifier = Modifier.align(Alignment.CenterVertically)) + } +} + +@OptIn(ExperimentalAnimationApi::class) +@Composable +private fun PriceChangeIcon(type: PriceChangeConfig.Type, modifier: Modifier = Modifier) { + AnimatedContent( + targetState = type, + label = "Update the price change's arrow", + modifier = modifier, + ) { + Image( + painter = painterResource( + id = when (it) { + PriceChangeConfig.Type.UP -> R.drawable.img_arrow_up_8 + PriceChangeConfig.Type.DOWN -> R.drawable.img_arrow_down_8 + }, + ), + contentDescription = null, + ) + } +} + +@OptIn(ExperimentalAnimationApi::class) +@Composable +private fun PriceChangeText(config: PriceChangeConfig, modifier: Modifier = Modifier) { + AnimatedContent( + targetState = config.type, + label = "Update the price change's arrow", + modifier = modifier, + ) { + Text( + text = config.valueInPercent, + style = TangemTypography.body2, + color = when (it) { + PriceChangeConfig.Type.UP -> TangemTheme.colors.text.accent + PriceChangeConfig.Type.DOWN -> TangemTheme.colors.text.warning + }, + ) + } +} + +@Composable +private fun DraggableBlock(reorderableTokenListState: ReorderableLazyListState?, modifier: Modifier = Modifier) { + Box( + modifier = modifier + .size(TangemTheme.dimens.size32) + .then( + other = if (reorderableTokenListState != null) { + Modifier.detectReorder(reorderableTokenListState) + } else { + Modifier + }, + ), + contentAlignment = Alignment.Center, + ) { + Icon( + painter = painterResource(id = R.drawable.ic_drag_24), + tint = TangemTheme.colors.icon.informative, + contentDescription = null, + ) + } +} + +@Composable +private fun UnreachableBlock(modifier: Modifier = Modifier) { + Text( + modifier = modifier, + text = stringResource(id = R.string.common_unreachable), + style = TangemTypography.body2, + color = TangemTheme.colors.text.tertiary, + ) +} + +@Composable +private fun LoadingBlock(modifier: Modifier = Modifier) { + NonContentContainer(modifier = modifier) { + RectangleShimmer(modifier = Modifier.viewSize(), radius = TangemTheme.dimens.radius4) + RectangleShimmer(modifier = Modifier.viewSize(), radius = TangemTheme.dimens.radius4) + } +} + +@Composable +private fun LockedBlock(modifier: Modifier = Modifier) { + NonContentContainer(modifier = modifier) { + LockedRectangle(modifier = Modifier.viewSize()) + LockedRectangle(modifier = Modifier.viewSize()) + } +} + +@Composable +private fun NonContentContainer(modifier: Modifier = Modifier, content: @Composable ColumnScope.() -> Unit) { + Column( + modifier = modifier, + verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing10), + content = content, + ) +} + +private fun Modifier.viewSize(): Modifier = composed { + return@composed size(width = TangemTheme.dimens.size40, height = TangemTheme.dimens.size12) +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/token/TokenIcon.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/token/TokenIcon.kt new file mode 100644 index 0000000000..6ddfe82790 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/token/TokenIcon.kt @@ -0,0 +1,152 @@ +package com.tangem.feature.wallet.presentation.common.component.token + +import androidx.annotation.DrawableRes +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxScope +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.composed +import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.graphics.ColorMatrix +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import coil.compose.SubcomposeAsyncImage +import coil.request.ImageRequest +import com.tangem.core.ui.components.CircleShimmer +import com.tangem.core.ui.res.TangemTheme +import com.tangem.feature.wallet.presentation.common.state.TokenItemState + +private const val GRAY_SCALE_SATURATION = 0f + +@Composable +internal fun TokenIcon(state: TokenItemState, modifier: Modifier = Modifier) { + when (state) { + is TokenItemState.ContentState -> ContentIcon(content = state, modifier = modifier) + is TokenItemState.Loading -> LoadingIcon(modifier = modifier) + is TokenItemState.Locked -> LockedIcon(modifier = modifier) + } +} + +@Composable +private fun ContentIcon(content: TokenItemState.ContentState, modifier: Modifier = Modifier) { + BaseContainer(modifier = modifier) { + val isTestnet = when (content) { + is TokenItemState.Content -> content.isTestnet + is TokenItemState.Draggable -> content.isTestnet + is TokenItemState.Unreachable -> false + } + + val colorFilter = remember(isTestnet) { + if (isTestnet) { + ColorFilter.colorMatrix( + colorMatrix = ColorMatrix().apply { setToSaturation(GRAY_SCALE_SATURATION) }, + ) + } else { + null + } + } + + Icon( + content = content, + colorFilter = colorFilter, + modifier = Modifier.align(Alignment.BottomStart), + ) + + NetworkBadge( + iconResId = content.networkBadgeIconResId, + colorFilter = colorFilter, + modifier = Modifier.align(Alignment.TopEnd), + ) + } +} + +@Composable +private fun Icon(content: TokenItemState.ContentState, colorFilter: ColorFilter?, modifier: Modifier = Modifier) { + val iconUrl = content.tokenIconUrl + val iconData: Any = remember(iconUrl) { + if (iconUrl.isNullOrEmpty()) content.tokenIconResId else iconUrl + } + + SubcomposeAsyncImage( + modifier = modifier.iconSize(), + model = ImageRequest.Builder(context = LocalContext.current) + .data(data = iconData) + .placeholder(drawableResId = content.tokenIconResId) + .error(drawableResId = content.tokenIconResId) + .fallback(drawableResId = content.tokenIconResId) + .crossfade(enable = true) + .build(), + colorFilter = colorFilter, + contentDescription = null, + ) +} + +@Composable +private fun BoxScope.NetworkBadge( + @DrawableRes iconResId: Int?, + colorFilter: ColorFilter?, + modifier: Modifier = Modifier, +) { + AnimatedVisibility( + visible = iconResId != null, + modifier = modifier + .size(TangemTheme.dimens.size18) + .background(color = TangemTheme.colors.background.primary, shape = CircleShape), + ) { + if (iconResId == null) return@AnimatedVisibility + + Image( + modifier = Modifier + .padding(all = TangemTheme.dimens.spacing2) + .align(Alignment.Center), + painter = painterResource(id = iconResId), + colorFilter = colorFilter, + contentDescription = null, + ) + } +} + +@Composable +private fun LoadingIcon(modifier: Modifier = Modifier) { + BaseContainer(modifier) { + CircleShimmer( + modifier = Modifier + .iconSize() + .align(alignment = Alignment.BottomStart), + ) + } +} + +@Composable +private fun LockedIcon(modifier: Modifier = Modifier) { + BaseContainer(modifier) { + Box( + modifier = Modifier + .iconSize() + .align(Alignment.BottomStart), + ) { + Box( + modifier = Modifier + .matchParentSize() + .background(color = TangemTheme.colors.background.secondary, shape = CircleShape), + ) + } + } +} + +@Composable +private inline fun BaseContainer(modifier: Modifier = Modifier, content: @Composable BoxScope.() -> Unit) { + Box(modifier = modifier.size(size = TangemTheme.dimens.size40), content = content) +} + +private fun Modifier.iconSize(): Modifier = composed { + return@composed this.size(size = TangemTheme.dimens.size36) +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/state/TokenItemState.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/state/TokenItemState.kt index bb2883ed5f..8310f134d5 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/state/TokenItemState.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/state/TokenItemState.kt @@ -14,82 +14,111 @@ internal sealed interface TokenItemState { /** Loading token state */ data class Loading(override val id: String) : TokenItemState + /** Locked token state */ + data class Locked(override val id: String) : TokenItemState + + /** Content state */ + sealed class ContentState( + override val id: String, + open val tokenIconUrl: String?, + @DrawableRes open val tokenIconResId: Int, + @DrawableRes open val networkBadgeIconResId: Int?, + open val name: String, + ) : TokenItemState + /** * Content token state * - * @property id unique id - * @property tokenIconUrl token icon url - * @property tokenIconResId token icon resource id - * @property networkIconResId network icon resource id, may be null if it is a coin - * @property name token name - * @property amount amount of token - * @property hasPending pending tx in blockchain - * @property tokenOptions state for token options + * @property id unique id + * @property tokenIconUrl token icon url + * @property tokenIconResId token icon resource id + * @property networkBadgeIconResId network badge icon resource id, may be null if it is a coin + * @property name token name + * @property amount amount of token + * @property hasPending pending tx in blockchain + * @property tokenOptions state for token options + * @property isTestnet indicates whether the token is from test network or not + * @property onItemClick callback which will be called when an item is clicked + * @property onItemLongClick callback which will be called when an item is long clicked */ data class Content( override val id: String, - val tokenIconUrl: String?, - @DrawableRes val tokenIconResId: Int, - @DrawableRes val networkIconResId: Int?, - val name: String, + override val tokenIconUrl: String?, + @DrawableRes override val tokenIconResId: Int, + @DrawableRes override val networkBadgeIconResId: Int?, + override val name: String, val amount: String, val hasPending: Boolean, val tokenOptions: TokenOptionsState, - val onClick: () -> Unit, - ) : TokenItemState + val isTestnet: Boolean, + val onItemClick: () -> Unit, + val onItemLongClick: () -> Unit, + ) : ContentState(id, tokenIconUrl, tokenIconResId, networkBadgeIconResId, name) /** * Draggable token state * - * @property id unique id - * @property tokenIconUrl token icon url - * @property tokenIconResId token icon resource id - * @property networkIconResId network icon resource id, may be null if it is a coin - * @property name token name - * @property fiatAmount fiat amount of token + * @property id unique id + * @property tokenIconUrl token icon url + * @property tokenIconResId token icon resource id + * @property networkBadgeIconResId network badge icon resource id, may be null if it is a coin + * @property name token name + * @property fiatAmount fiat amount of token + * @property isTestnet indicates whether the token is from test network or not */ data class Draggable( override val id: String, - val tokenIconUrl: String?, - @DrawableRes val tokenIconResId: Int, - @DrawableRes val networkIconResId: Int?, - val name: String, + override val tokenIconUrl: String?, + @DrawableRes override val tokenIconResId: Int, + @DrawableRes override val networkBadgeIconResId: Int?, + override val name: String, val fiatAmount: String, - ) : TokenItemState + val isTestnet: Boolean, + ) : ContentState(id, tokenIconUrl, tokenIconResId, networkBadgeIconResId, name) /** * Unreachable token state * - * @property id token id - * @property tokenIconUrl token icon url - * @property tokenIconResId token icon resource id - * @property networkIconResId network icon resource id, may be null if it is a coin - * @property name token name + * @property id token id + * @property tokenIconUrl token icon url + * @property tokenIconResId token icon resource id + * @property networkBadgeIconResId network badge icon resource id, may be null if it is a coin + * @property name token name */ data class Unreachable( override val id: String, - val tokenIconUrl: String?, - @DrawableRes val tokenIconResId: Int, - @DrawableRes val networkIconResId: Int?, - val name: String, - ) : TokenItemState + override val tokenIconUrl: String?, + @DrawableRes override val tokenIconResId: Int, + @DrawableRes override val networkBadgeIconResId: Int?, + override val name: String, + ) : ContentState(id, tokenIconUrl, tokenIconResId, networkBadgeIconResId, name) /** Token options state */ + @Immutable sealed interface TokenOptionsState { + val config: PriceChangeConfig + /** * Visible token options state * * @property fiatAmount fiat amount of token - * @property priceChange value of price changing + * @property config value of price changing */ - data class Visible(val fiatAmount: String, val priceChange: PriceChangeConfig) : TokenOptionsState + data class Visible( + override val config: PriceChangeConfig, + val fiatAmount: String, + ) : TokenOptionsState /** * Hidden token options state * - * @property priceChange value of price changing + * @property config value of price changing */ - data class Hidden(val priceChange: PriceChangeConfig) : TokenOptionsState + data class Hidden(override val config: PriceChangeConfig) : TokenOptionsState + } + + companion object { + const val DOTS = "•••" } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/Intents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/Intents.kt new file mode 100644 index 0000000000..e0f4868347 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/Intents.kt @@ -0,0 +1,28 @@ +package com.tangem.feature.wallet.presentation.organizetokens + +import com.tangem.feature.wallet.presentation.organizetokens.model.DraggableItem +import org.burnoutcrew.reorderable.ItemPosition + +internal interface OrganizeTokensIntents { + + fun onBackClick() + + fun onSortClick() + + fun onGroupClick() + + fun onApplyClick() + + fun onCancelClick() +} + +internal interface DragAndDropIntents { + + fun onItemDragged(from: ItemPosition, to: ItemPosition) + + fun canDragItemOver(dragOver: ItemPosition, dragging: ItemPosition): Boolean + + fun onItemDraggingStart(item: DraggableItem) + + fun onItemDraggingEnd() +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensIntents.kt deleted file mode 100644 index 84ce246245..0000000000 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensIntents.kt +++ /dev/null @@ -1,14 +0,0 @@ -package com.tangem.feature.wallet.presentation.organizetokens - -internal interface OrganizeTokensIntents { - - fun onBackClick() - - fun onSortClick() - - fun onGroupClick() - - fun onApplyClick() - - fun onCancelClick() -} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensScreen.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensScreen.kt index beb7709ffb..238ff49a18 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensScreen.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensScreen.kt @@ -2,6 +2,7 @@ package com.tangem.feature.wallet.presentation.organizetokens import androidx.activity.compose.BackHandler import androidx.compose.animation.core.* +import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.* @@ -26,12 +27,13 @@ import com.tangem.core.ui.components.PrimaryButton import com.tangem.core.ui.components.SecondaryButton import com.tangem.core.ui.components.buttons.actions.ActionButtonConfig import com.tangem.core.ui.components.buttons.actions.RoundedActionButton +import com.tangem.core.ui.event.EventEffect import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.res.TangemTheme import com.tangem.feature.wallet.impl.R import com.tangem.feature.wallet.presentation.common.WalletPreviewData import com.tangem.feature.wallet.presentation.common.component.DraggableNetworkGroupItem -import com.tangem.feature.wallet.presentation.common.component.DraggableTokenItem +import com.tangem.feature.wallet.presentation.common.component.TokenItem import com.tangem.feature.wallet.presentation.organizetokens.model.DraggableItem import com.tangem.feature.wallet.presentation.organizetokens.model.OrganizeTokensListState import com.tangem.feature.wallet.presentation.organizetokens.model.OrganizeTokensState @@ -64,6 +66,10 @@ internal fun OrganizeTokensScreen(state: OrganizeTokensState, modifier: Modifier }, containerColor = TangemTheme.colors.background.secondary, ) + + EventEffect(state.scrollListToTop) { + tokensListState.animateScrollToItem(index = 0) + } } @Composable @@ -74,11 +80,16 @@ private fun TokenList( modifier: Modifier = Modifier, ) { Box(modifier = modifier) { + val onDragEnd: (Int, Int) -> Unit = remember { + { _, _ -> + dndConfig.onItemDragEnd() + } + } val reorderableListState = rememberReorderableLazyListState( onMove = dndConfig.onItemDragged, listState = listState, canDragOver = dndConfig.canDragItemOver, - onDragEnd = { _, _ -> dndConfig.onItemDragEnd() }, + onDragEnd = onDragEnd, ) val items = state.items @@ -109,11 +120,6 @@ private fun TokenList( reorderableState = reorderableListState, onDragStart = onDragStart, ) - - if (item is DraggableItem.GroupPlaceholder) { - // This item should be displayed in the list but remain invisible - Box(modifier = Modifier.fillMaxWidth()) - } } } @@ -121,6 +127,7 @@ private fun TokenList( } } +@OptIn(ExperimentalFoundationApi::class) @Composable private fun LazyItemScope.DraggableItem( index: Int, @@ -129,15 +136,13 @@ private fun LazyItemScope.DraggableItem( onDragStart: () -> Unit, ) { ReorderableItem( - reorderableState = reorderableState, + defaultDraggingModifier = Modifier.animateItemPlacement( + animationSpec = tween(easing = LinearOutSlowInEasing), + ), + state = reorderableState, index = index, key = item.id, ) { isDragging -> - - if (isDragging) { - onDragStart() - } - val itemModifier = Modifier.applyShapeAndShadow(item.roundingMode, item.showShadow) when (item) { @@ -146,12 +151,19 @@ private fun LazyItemScope.DraggableItem( networkName = item.networkName, reorderableTokenListState = reorderableState, ) - is DraggableItem.Token -> DraggableTokenItem( + is DraggableItem.Token -> TokenItem( modifier = itemModifier, state = item.tokenItemState, reorderableTokenListState = reorderableState, ) - is DraggableItem.GroupPlaceholder -> Unit + // Should be presented in the list but remain invisible + is DraggableItem.GroupPlaceholder -> Box(modifier = Modifier.fillMaxWidth()) + } + + LaunchedEffect(isDragging) { + if (isDragging) { + onDragStart() + } } } } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensStateHolder.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensStateHolder.kt index 14fecec78d..ec8c8655c2 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensStateHolder.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensStateHolder.kt @@ -1,6 +1,8 @@ package com.tangem.feature.wallet.presentation.organizetokens import com.tangem.common.Provider +import com.tangem.core.ui.event.consumed +import com.tangem.core.ui.event.triggered import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.tokens.error.TokenListError import com.tangem.domain.tokens.error.TokenListSortingError @@ -19,6 +21,7 @@ import kotlinx.coroutines.flow.* internal class OrganizeTokensStateHolder( private val intents: OrganizeTokensIntents, + private val dragAndDropIntents: DragAndDropIntents, private val appCurrencyProvider: Provider, private val onSubscription: () -> Unit, stateFlowScope: CoroutineScope, @@ -60,6 +63,14 @@ internal class OrganizeTokensStateHolder( updateState { tokenListConverter.convert(tokenList) } } + fun updateStateAfterTokenListSorting(tokenList: TokenList) { + updateState { + tokenListConverter.convert(tokenList).copy( + scrollListToTop = triggered(::consumeScrollListToTopEvent), + ) + } + } + fun updateStateToDisplayProgress() { updateState { inProgressStateConverter.convert(value = this) } } @@ -68,6 +79,15 @@ internal class OrganizeTokensStateHolder( updateState { inProgressStateConverter.convertBack(value = this) } } + fun updateStateWithManualSorting(itemsState: OrganizeTokensListState) { + updateState { + copy( + header = header.copy(isSortedByBalance = false), + itemsState = itemsState, + ) + } + } + fun updateStateWithError(error: TokenListError) { updateState { tokenListErrorConverter.convert(error) } } @@ -88,17 +108,21 @@ internal class OrganizeTokensStateHolder( onApplyClick = intents::onApplyClick, onCancelClick = intents::onCancelClick, ), - // TODO: Will be added in next MR dndConfig = OrganizeTokensState.DragAndDropConfig( - onItemDragged = { _, _ -> }, - onDragStart = { }, - onItemDragEnd = { }, - canDragItemOver = { _, _ -> false }, + onItemDragged = dragAndDropIntents::onItemDragged, + onDragStart = dragAndDropIntents::onItemDraggingStart, + onItemDragEnd = dragAndDropIntents::onItemDraggingEnd, + canDragItemOver = dragAndDropIntents::canDragItemOver, ), + scrollListToTop = consumed, ) } private fun updateState(block: OrganizeTokensState.() -> OrganizeTokensState) { stateFlowInternal.update(block) } + + private fun consumeScrollListToTopEvent() { + updateState { copy(scrollListToTop = consumed) } + } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensViewModel.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensViewModel.kt index 0313bf1e7a..5c24a976b3 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 @@ -16,6 +16,8 @@ import com.tangem.domain.wallets.models.UserWalletId import com.tangem.feature.wallet.presentation.organizetokens.model.OrganizeTokensListState import com.tangem.feature.wallet.presentation.organizetokens.model.OrganizeTokensState import com.tangem.feature.wallet.presentation.organizetokens.utils.CryptoCurrenciesIdsResolver +import com.tangem.feature.wallet.presentation.organizetokens.utils.common.disableSortingByBalance +import com.tangem.feature.wallet.presentation.organizetokens.utils.dnd.DragAndDropAdapter import com.tangem.feature.wallet.presentation.router.InnerWalletRouter import com.tangem.feature.wallet.presentation.router.WalletRoute import dagger.hilt.android.lifecycle.HiltViewModel @@ -39,12 +41,19 @@ internal class OrganizeTokensViewModel @Inject constructor( private val selectedAppCurrencyFlow = createSelectedAppCurrencyFlow() + private val dragAndDropAdapter = DragAndDropAdapter( + listStateProvider = Provider { uiState.value.itemsState }, + scope = viewModelScope, + ) + private val stateHolder = OrganizeTokensStateHolder( stateFlowScope = viewModelScope, intents = this, + dragAndDropIntents = dragAndDropAdapter, appCurrencyProvider = Provider(selectedAppCurrencyFlow::value), onSubscription = { bootstrapTokenList() + bootstrapDragAndDropUpdates() }, ) @@ -69,7 +78,7 @@ internal class OrganizeTokensViewModel @Inject constructor( toggleTokenListSortingUseCase(list).fold( ifLeft = stateHolder::updateStateWithError, ifRight = { - stateHolder.updateStateWithTokenList(it) + stateHolder.updateStateAfterTokenListSorting(it) tokenList = it }, ) @@ -83,7 +92,7 @@ internal class OrganizeTokensViewModel @Inject constructor( toggleTokenListGroupingUseCase(list).fold( ifLeft = stateHolder::updateStateWithError, ifRight = { - stateHolder.updateStateWithTokenList(it) + stateHolder.updateStateAfterTokenListSorting(it) tokenList = it }, ) @@ -133,6 +142,16 @@ internal class OrganizeTokensViewModel @Inject constructor( } } + private fun bootstrapDragAndDropUpdates() { + dragAndDropAdapter.stateFlow + .distinctUntilChanged() + .onEach { + stateHolder.updateStateWithManualSorting(it) + tokenList = tokenList?.disableSortingByBalance() + } + .launchIn(viewModelScope) + } + private fun createSelectedAppCurrencyFlow(): StateFlow { return getSelectedAppCurrencyUseCase() .map { maybeAppCurrency -> diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/model/OrganizeTokensState.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/model/OrganizeTokensState.kt index 30d8e846c1..860c4c5b24 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/model/OrganizeTokensState.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/model/OrganizeTokensState.kt @@ -1,6 +1,7 @@ package com.tangem.feature.wallet.presentation.organizetokens.model import androidx.compose.runtime.Immutable +import com.tangem.core.ui.event.StateEvent import org.burnoutcrew.reorderable.ItemPosition @Immutable @@ -10,6 +11,7 @@ internal data class OrganizeTokensState( val header: HeaderConfig, val actions: ActionsConfig, val dndConfig: DragAndDropConfig, + val scrollListToTop: StateEvent, ) { data class HeaderConfig( diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/common/DraggableItemsOperations.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/common/DraggableItemsOperations.kt index 74ada001e8..fee7cb1033 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/common/DraggableItemsOperations.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/common/DraggableItemsOperations.kt @@ -1,184 +1,27 @@ package com.tangem.feature.wallet.presentation.organizetokens.utils.common import com.tangem.feature.wallet.presentation.organizetokens.model.DraggableItem -import kotlinx.collections.immutable.PersistentList -import org.burnoutcrew.reorderable.ItemPosition -internal fun List.findItemsToMove( - moveOverItemKey: Any?, - movedItemKey: Any?, -): Pair { - var moveOverItem: DraggableItem? = null - var movedItem: DraggableItem? = null - - for (item in this) { - if (item.id == moveOverItemKey) { - moveOverItem = item - } - if (item.id == movedItemKey) { - movedItem = item - } - if (moveOverItem != null && movedItem != null) { - break - } - } - - return Pair(moveOverItem, movedItem) -} - -internal fun checkCanMoveHeaderOver( - moveOverItemPosition: ItemPosition, - moveOverItem: DraggableItem, - lastItemIndex: Int, -): Boolean { - // Group item can be moved only to group divider or to ages of the items list - return when { - moveOverItemPosition.index == 0 -> true - moveOverItemPosition.index == lastItemIndex -> true - moveOverItem is DraggableItem.GroupPlaceholder -> true - else -> false - } -} - -internal fun checkCanMoveTokenOver(item: DraggableItem.Token, moveOverItem: DraggableItem): Boolean { - // Token item can be moved only in its group - return when (moveOverItem) { - is DraggableItem.GroupHeader -> false // Token item can not be moved to group item - is DraggableItem.Token -> item.groupId == moveOverItem.groupId // Token item can not be moved over its group - is DraggableItem.GroupPlaceholder -> false - } -} - -internal fun PersistentList.moveItem(fromIndex: Int, toIndex: Int): PersistentList { - val fromItem = this[fromIndex] - return this - .removeAt(fromIndex) - .add(toIndex, fromItem) -} - -internal fun List.divideItems(movingItem: DraggableItem): List { - return this.map { - it - .updateRoundingMode(DraggableItem.RoundingMode.All(showGap = true)) - .updateShadowVisibility(show = it.id == movingItem.id) - } -} - -@Suppress("UNCHECKED_CAST") // Erased type -internal fun List.uniteItems(): List { +internal fun List.uniteItems(): List { val lastItemIndex = this.lastIndex return this.mapIndexed { index, item -> val mode = when (index) { 0 -> DraggableItem.RoundingMode.Top() lastItemIndex -> DraggableItem.RoundingMode.Bottom() - else -> DraggableItem.RoundingMode.None + else -> when (item) { + is DraggableItem.GroupHeader -> DraggableItem.RoundingMode.Top(showGap = true) + is DraggableItem.Token -> if (this[index + 1] is DraggableItem.GroupPlaceholder) { + DraggableItem.RoundingMode.Bottom(showGap = true) + } else { + DraggableItem.RoundingMode.None + } + is DraggableItem.GroupPlaceholder -> DraggableItem.RoundingMode.None + } } item .updateRoundingMode(mode) .updateShadowVisibility(show = false) - } as List -} - -// TODO: Move to domain -@Volatile -private var groupIdToTokens: Map>? = null - -internal fun List.collapseGroup(group: DraggableItem.GroupHeader): List { - if (!groupIdToTokens.isNullOrEmpty()) return this - - groupIdToTokens = this - .asSequence() - .filterIsInstance() - .groupBy { it.groupId } - - return this - .filterNot { it is DraggableItem.Token && it.groupId == group.id } - .divideGroups(group) -} - -internal fun List.expandGroups(): List { - if (groupIdToTokens.isNullOrEmpty()) return this - - val currentGroups = this.filterIsInstance() - val lastGroupIndex = currentGroups.lastIndex - - return currentGroups - .flatMapIndexed { index, group -> - buildList { - add(group) - addAll(groupIdToTokens?.get(group.id).orEmpty()) - if (index != lastGroupIndex) { - add(DraggableItem.GroupPlaceholder(id = "group_divider_$index")) - } - } - } - .uniteItems() - .also { groupIdToTokens = null } -} - -/** - * Applies the correct [DraggableItem.RoundingMode] and shadow status to each item in the list, - * based on the relationship of each item to the [movingItem] and its position in the list. - * - * @param movingItem The item that is being dragged/moved. - * @return A list of [DraggableItem]s with updated rounding modes and shadow statuses. - */ -internal fun List.divideGroups(movingItem: DraggableItem): List { - val lastItemIndex = this.lastIndex - - return this.mapIndexed { index, item -> - when { - // Case when current item is the moving item - item.id == movingItem.id -> { - item - .updateRoundingMode(DraggableItem.RoundingMode.All(showGap = true)) - .updateShadowVisibility(show = true) - } - // Case when moving item is a token and current item is the group of the moving token - movingItem is DraggableItem.Token && item.id == movingItem.groupId -> { - item - .updateRoundingMode(DraggableItem.RoundingMode.All(showGap = true)) - .updateShadowVisibility(show = true) - } - // Case when both moving item and current item are tokens and belong to the same group - movingItem is DraggableItem.Token && - item is DraggableItem.Token && item.groupId == movingItem.groupId -> { - item - .updateRoundingMode(DraggableItem.RoundingMode.All(showGap = true)) - .updateShadowVisibility(show = false) - } - // Case when current item is the first item in the list - index == 0 -> { - item - .updateRoundingMode(DraggableItem.RoundingMode.Top()) - .updateShadowVisibility(show = false) - } - // Case when current item is the last item in the list - index == lastItemIndex -> { - item - .updateRoundingMode(DraggableItem.RoundingMode.Bottom()) - .updateShadowVisibility(show = false) - } - // Case when previous item is a GroupPlaceholder - this[index - 1] is DraggableItem.GroupPlaceholder -> { - item - .updateRoundingMode(DraggableItem.RoundingMode.Top(showGap = true)) - .updateShadowVisibility(show = false) - } - // Case when next item is a GroupPlaceholder - this[index + 1] is DraggableItem.GroupPlaceholder -> { - item - .updateRoundingMode(DraggableItem.RoundingMode.Bottom(showGap = true)) - .updateShadowVisibility(show = false) - } - // Default case when none of the above conditions are met - else -> { - item - .updateRoundingMode(DraggableItem.RoundingMode.None) - .updateShadowVisibility(show = false) - } - } } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/common/TokenListOperations.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/common/TokenListOperations.kt index 4f1478d2bf..4e4250f5e9 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/common/TokenListOperations.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/common/TokenListOperations.kt @@ -3,12 +3,10 @@ package com.tangem.feature.wallet.presentation.organizetokens.utils.common import com.tangem.domain.tokens.model.TokenList import com.tangem.domain.tokens.model.TokenList.SortType -internal fun TokenList.updateSorting(isSortedByBalance: Boolean): TokenList { - val sortType = if (isSortedByBalance) SortType.BALANCE else SortType.NONE - +internal fun TokenList.disableSortingByBalance(): TokenList { return when (this) { - is TokenList.GroupedByNetwork -> this.copy(sortedBy = sortType) - is TokenList.Ungrouped -> this.copy(sortedBy = sortType) + is TokenList.GroupedByNetwork -> this.copy(sortedBy = SortType.NONE) + is TokenList.Ungrouped -> this.copy(sortedBy = SortType.NONE) is TokenList.NotInitialized -> this } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/items/CryptoCurrencyToDraggableItemConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/items/CryptoCurrencyToDraggableItemConverter.kt index 5284855142..11f4747c2b 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/items/CryptoCurrencyToDraggableItemConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/items/CryptoCurrencyToDraggableItemConverter.kt @@ -1,12 +1,11 @@ package com.tangem.feature.wallet.presentation.organizetokens.utils.converter.items -import androidx.annotation.DrawableRes import com.tangem.common.Provider +import com.tangem.core.ui.extensions.iconResId +import com.tangem.core.ui.extensions.networkBadgeIconResId import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus -import com.tangem.domain.tokens.models.CryptoCurrency -import com.tangem.feature.wallet.impl.R import com.tangem.feature.wallet.presentation.common.state.TokenItemState import com.tangem.feature.wallet.presentation.organizetokens.model.DraggableItem import com.tangem.feature.wallet.presentation.organizetokens.utils.common.getGroupHeaderId @@ -17,18 +16,6 @@ internal class CryptoCurrencyToDraggableItemConverter( private val appCurrencyProvider: Provider, ) : Converter { - private val CryptoCurrency.networkIconResId: Int? - @DrawableRes get() { - // TODO: [REDACTED_JIRA] - return if (this is CryptoCurrency.Coin) null else R.drawable.img_eth_22 - } - - private val CryptoCurrency.tokenIconResId: Int - @DrawableRes get() { - // TODO: [REDACTED_JIRA] - return R.drawable.img_eth_22 - } - override fun convert(value: CryptoCurrencyStatus): DraggableItem.Token { return createDraggableToken(value, appCurrencyProvider()) } @@ -45,7 +32,7 @@ internal class CryptoCurrencyToDraggableItemConverter( ): DraggableItem.Token { return DraggableItem.Token( tokenItemState = createTokenItemState(currencyStatus, appCurrency), - groupId = getGroupHeaderId(currencyStatus.currency.networkId), + groupId = getGroupHeaderId(currencyStatus.currency.network.id), ) } @@ -58,10 +45,11 @@ internal class CryptoCurrencyToDraggableItemConverter( return TokenItemState.Draggable( id = getTokenItemId(currency.id), tokenIconUrl = currency.iconUrl, - tokenIconResId = currency.tokenIconResId, - networkIconResId = currency.networkIconResId, + tokenIconResId = currencyStatus.currency.iconResId, + networkBadgeIconResId = currencyStatus.currency.networkBadgeIconResId, name = currency.name, fiatAmount = getFormattedFiatAmount(currencyStatus, appCurrency), + isTestnet = currencyStatus.currency.network.isTestnet, ) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/items/TokenListToListStateConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/items/TokenListToListStateConverter.kt index bb1b829d47..dd82c1672b 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/items/TokenListToListStateConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/items/TokenListToListStateConverter.kt @@ -1,9 +1,11 @@ package com.tangem.feature.wallet.presentation.organizetokens.utils.converter.items import com.tangem.domain.tokens.model.TokenList +import com.tangem.feature.wallet.presentation.organizetokens.model.DraggableItem import com.tangem.feature.wallet.presentation.organizetokens.model.OrganizeTokensListState import com.tangem.feature.wallet.presentation.organizetokens.utils.common.uniteItems import com.tangem.utils.converter.Converter +import kotlinx.collections.immutable.PersistentList import kotlinx.collections.immutable.toPersistentList internal class TokenListToListStateConverter( @@ -28,11 +30,12 @@ internal class TokenListToListStateConverter( ) } + @Suppress("UNCHECKED_CAST") // Erased type private fun createListState(tokenList: TokenList.Ungrouped): OrganizeTokensListState.Ungrouped { return OrganizeTokensListState.Ungrouped( items = tokensConverter.convertList(tokenList.currencies) .uniteItems() - .toPersistentList(), + .toPersistentList() as PersistentList, ) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/dnd/DragAndDropAdapter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/dnd/DragAndDropAdapter.kt new file mode 100644 index 0000000000..c73798c77e --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/dnd/DragAndDropAdapter.kt @@ -0,0 +1,169 @@ +package com.tangem.feature.wallet.presentation.organizetokens.utils.dnd + +import com.tangem.common.Provider +import com.tangem.feature.wallet.presentation.organizetokens.DragAndDropIntents +import com.tangem.feature.wallet.presentation.organizetokens.model.DraggableItem +import com.tangem.feature.wallet.presentation.organizetokens.model.OrganizeTokensListState +import com.tangem.feature.wallet.presentation.organizetokens.utils.common.uniteItems +import com.tangem.feature.wallet.presentation.organizetokens.utils.common.updateItems +import kotlinx.collections.immutable.mutate +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.channels.BufferOverflow +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.launch +import org.burnoutcrew.reorderable.ItemPosition + +internal class DragAndDropAdapter( + private val listStateProvider: Provider, + private val scope: CoroutineScope, +) : DragAndDropIntents { + + private val draggableGroupsOperations = DraggableGroupsOperations() + + private val currentListState: OrganizeTokensListState + get() = listStateProvider.invoke() + + private val listStateFlowInternal: MutableSharedFlow = MutableSharedFlow( + replay = 1, + onBufferOverflow = BufferOverflow.DROP_OLDEST, + ) + + private var currentDraggingItem: DraggableItem? = null + + val stateFlow: Flow + get() = listStateFlowInternal + + override fun canDragItemOver(dragOver: ItemPosition, dragging: ItemPosition): Boolean { + val items = (currentListState as? OrganizeTokensListState.GroupedByNetwork) + ?.items + ?: return true // If ungrouped then item can be moved anywhere + + val (dragOverItem, draggingItem) = findItemsToMove( + items = items, + moveOverItemKey = dragOver.key, + movedItemKey = dragging.key, + ) + + if (dragOverItem == null || draggingItem == null) { + return false + } + + return when (draggingItem) { + is DraggableItem.GroupHeader -> checkCanMoveHeaderOver(dragOver, dragOverItem, items.lastIndex) + is DraggableItem.Token -> checkCanMoveTokenOver(draggingItem, dragOverItem) + is DraggableItem.GroupPlaceholder -> false + } + } + + override fun onItemDraggingStart(item: DraggableItem) { + if (currentDraggingItem != null) return + currentDraggingItem = item + + updateListState { + when (item) { + is DraggableItem.GroupPlaceholder -> items + is DraggableItem.GroupHeader -> draggableGroupsOperations.collapseGroup(items, item) + is DraggableItem.Token -> when (this) { + is OrganizeTokensListState.GroupedByNetwork -> draggableGroupsOperations.divideGroups(items, item) + is OrganizeTokensListState.Ungrouped -> divideTokens(items, item) + is OrganizeTokensListState.Empty -> items + } + } + } + } + + override fun onItemDraggingEnd() { + scope.launch(Dispatchers.IO) { + val draggingItem = currentDraggingItem ?: return@launch + + delay(FINISH_DRAGGING_DELAY_MILLIS) + + updateListState { + when (draggingItem) { + is DraggableItem.GroupHeader -> draggableGroupsOperations.expandGroups(items) + is DraggableItem.Token -> items.uniteItems() + is DraggableItem.GroupPlaceholder -> items + } + } + + currentDraggingItem = null + } + } + + override fun onItemDragged(from: ItemPosition, to: ItemPosition) = updateListState { + items.mutate { + it.add(to.index, it.removeAt(from.index)) + } + } + + private fun updateListState(block: OrganizeTokensListState.() -> List) { + val updatedState = currentListState.updateItems { block(currentListState) } + + listStateFlowInternal.tryEmit(updatedState) + } + + private fun findItemsToMove( + items: List, + moveOverItemKey: Any?, + movedItemKey: Any?, + ): Pair { + var moveOverItem: DraggableItem? = null + var movedItem: DraggableItem? = null + + for (item in items) { + if (item.id == moveOverItemKey) { + moveOverItem = item + } + if (item.id == movedItemKey) { + movedItem = item + } + if (moveOverItem != null && movedItem != null) { + break + } + } + + return Pair(moveOverItem, movedItem) + } + + private fun checkCanMoveHeaderOver( + moveOverItemPosition: ItemPosition, + moveOverItem: DraggableItem, + lastItemIndex: Int, + ): Boolean { + // Group item can be moved only to group divider or to ages of the items list + return when { + moveOverItemPosition.index == 0 -> true + moveOverItemPosition.index == lastItemIndex -> true + moveOverItem is DraggableItem.GroupPlaceholder -> true + else -> false + } + } + + private fun checkCanMoveTokenOver(item: DraggableItem.Token, moveOverItem: DraggableItem): Boolean { + // Token item can be moved only in its group + return when (moveOverItem) { + is DraggableItem.GroupHeader -> false // Token item can not be moved to group item + is DraggableItem.Token -> item.groupId == moveOverItem.groupId // Token item can not be moved over its group + is DraggableItem.GroupPlaceholder -> false + } + } + + @Suppress("UNCHECKED_CAST") // Erased type + private fun divideTokens( + items: List, + movingItem: DraggableItem.Token, + ): List { + return items.map { token -> + token + .updateRoundingMode(DraggableItem.RoundingMode.All(showGap = true)) + .updateShadowVisibility(show = token.id == movingItem.id) + } as List + } + + private companion object { + const val FINISH_DRAGGING_DELAY_MILLIS = 200L + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/dnd/DraggableGroupsOperations.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/dnd/DraggableGroupsOperations.kt new file mode 100644 index 0000000000..d28133195a --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/dnd/DraggableGroupsOperations.kt @@ -0,0 +1,106 @@ +package com.tangem.feature.wallet.presentation.organizetokens.utils.dnd + +import com.tangem.feature.wallet.presentation.organizetokens.model.DraggableItem +import com.tangem.feature.wallet.presentation.organizetokens.utils.common.getGroupPlaceholder +import com.tangem.feature.wallet.presentation.organizetokens.utils.common.uniteItems + +internal class DraggableGroupsOperations { + + private var groupIdToTokens: Map>? = null + + fun collapseGroup(items: List, movingGroup: DraggableItem.GroupHeader): List { + if (!groupIdToTokens.isNullOrEmpty()) return items + + groupIdToTokens = items + .asSequence() + .filterIsInstance() + .groupBy { it.groupId } + + val itemsWithoutGroupTokens = items.filterNot { + it is DraggableItem.Token && it.groupId == movingGroup.id + } + + return divideGroups(itemsWithoutGroupTokens, movingGroup) + } + + fun expandGroups(items: List): List { + if (groupIdToTokens.isNullOrEmpty()) return items + + val currentGroups = items.filterIsInstance() + val lastGroupIndex = currentGroups.lastIndex + + val expandedGroups = currentGroups + .flatMapIndexed { index, group -> + buildList { + add(group) + addAll(groupIdToTokens?.get(group.id).orEmpty()) + if (index != lastGroupIndex) { + add(getGroupPlaceholder(index)) + } + } + } + .uniteItems() + + groupIdToTokens = null + + return expandedGroups + } + + fun divideGroups(items: List, movingItem: DraggableItem): List { + val lastItemIndex = items.lastIndex + + return items.mapIndexed { index, item -> + when { + // Case when current item is the moving item + item.id == movingItem.id -> { + item + .updateRoundingMode(DraggableItem.RoundingMode.All(showGap = true)) + .updateShadowVisibility(show = true) + } + // Case when moving item is a token and current item is the group of the moving token + movingItem is DraggableItem.Token && item.id == movingItem.groupId -> { + item + .updateRoundingMode(DraggableItem.RoundingMode.All(showGap = true)) + .updateShadowVisibility(show = true) + } + // Case when both moving item and current item are tokens and belong to the same group + movingItem is DraggableItem.Token && + item is DraggableItem.Token && item.groupId == movingItem.groupId -> { + item + .updateRoundingMode(DraggableItem.RoundingMode.All(showGap = true)) + .updateShadowVisibility(show = false) + } + // Case when current item is the first item in the list + index == 0 -> { + item + .updateRoundingMode(DraggableItem.RoundingMode.Top()) + .updateShadowVisibility(show = false) + } + // Case when current item is the last item in the list + index == lastItemIndex -> { + item + .updateRoundingMode(DraggableItem.RoundingMode.Bottom()) + .updateShadowVisibility(show = false) + } + // Case when previous item is a GroupPlaceholder + items[index - 1] is DraggableItem.GroupPlaceholder -> { + item + .updateRoundingMode(DraggableItem.RoundingMode.Top(showGap = true)) + .updateShadowVisibility(show = false) + } + // Case when next item is a GroupPlaceholder + items[index + 1] is DraggableItem.GroupPlaceholder -> { + item + .updateRoundingMode(DraggableItem.RoundingMode.Bottom(showGap = true)) + .updateShadowVisibility(show = false) + } + // Default case when none of the above conditions are met + else -> { + item + .updateRoundingMode(DraggableItem.RoundingMode.None) + .updateShadowVisibility(show = false) + } + } + } + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/DefaultWalletRouter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/DefaultWalletRouter.kt index 84ca2ae72b..ceaeead504 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/DefaultWalletRouter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/DefaultWalletRouter.kt @@ -18,7 +18,7 @@ import androidx.navigation.compose.rememberNavController import androidx.navigation.navArgument import com.tangem.core.navigation.AppScreen import com.tangem.core.navigation.NavigationAction -import com.tangem.core.navigation.NavigationStateHolder +import com.tangem.core.navigation.ReduxNavController import com.tangem.domain.tokens.models.CryptoCurrency import com.tangem.domain.wallets.models.UserWalletId import com.tangem.feature.wallet.presentation.WalletFragment @@ -30,7 +30,7 @@ import com.tangem.features.tokendetails.navigation.TokenDetailsRouter import kotlin.properties.Delegates /** Default implementation of wallet feature router */ -internal class DefaultWalletRouter(private val navigationStateHolder: NavigationStateHolder) : InnerWalletRouter { +internal class DefaultWalletRouter(private val reduxNavController: ReduxNavController) : InnerWalletRouter { private var navController: NavHostController by Delegates.notNull() private var fragmentManager: FragmentManager by Delegates.notNull() @@ -71,7 +71,7 @@ internal class DefaultWalletRouter(private val navigationStateHolder: Navigation } } - override fun popBackStack() { + override fun popBackStack(screen: AppScreen?) { /* * It's hack that avoid issue with closing the wallet screen. * We are using NavGraph only inside feature so first backstack's element is entry of NavGraph and @@ -79,7 +79,11 @@ internal class DefaultWalletRouter(private val navigationStateHolder: Navigation * If backstack contains only NavGraph entry and wallet screen entry then we close the wallet fragment. */ if (navController.backQueue.size == BACKSTACK_ENTRY_COUNT_TO_CLOSE_WALLET_SCREEN) { - fragmentManager.popBackStack() + if (screen != null) { + reduxNavController.navigate(action = NavigationAction.PopBackTo(screen)) + } else { + fragmentManager.popBackStack() + } } else { navController.popBackStack() } @@ -92,19 +96,19 @@ internal class DefaultWalletRouter(private val navigationStateHolder: Navigation override fun openDetailsScreen() { // FIXME: Prepare details screen (e.g. dispatch action: `DetailsAction.PrepareScreen`) // [REDACTED_JIRA] - navigationStateHolder.navigate(action = NavigationAction.NavigateTo(AppScreen.Details)) + reduxNavController.navigate(action = NavigationAction.NavigateTo(AppScreen.Details)) } override fun openOnboardingScreen() { - navigationStateHolder.navigate(action = NavigationAction.NavigateTo(AppScreen.OnboardingWallet)) + reduxNavController.navigate(action = NavigationAction.NavigateTo(AppScreen.OnboardingWallet)) } override fun openTxHistoryWebsite(url: String) { - navigationStateHolder.navigate(action = NavigationAction.OpenUrl(url)) + reduxNavController.navigate(action = NavigationAction.OpenUrl(url)) } override fun openTokenDetails(currency: CryptoCurrency) { - navigationStateHolder.navigate( + reduxNavController.navigate( action = NavigationAction.NavigateTo( screen = AppScreen.WalletDetails, // TODO: [REDACTED_JIRA] @@ -113,6 +117,20 @@ internal class DefaultWalletRouter(private val navigationStateHolder: Navigation ) } + override fun openStoriesScreen() { + reduxNavController.navigate(action = NavigationAction.NavigateTo(screen = AppScreen.Home)) + } + + override fun openSaveUserWalletScreen() { + reduxNavController.navigate(action = NavigationAction.NavigateTo(AppScreen.SaveWallet)) + } + + override fun isWalletLastScreen(): Boolean = reduxNavController.getBackStack().lastOrNull() == AppScreen.Wallet + + override fun openManageTokensScreen() { + reduxNavController.navigate(action = NavigationAction.NavigateTo(AppScreen.AddTokens)) + } + private companion object { const val BACKSTACK_ENTRY_COUNT_TO_CLOSE_WALLET_SCREEN = 2 } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/InnerWalletRouter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/InnerWalletRouter.kt index 4a0d24bbaa..b5b2ff23e1 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/InnerWalletRouter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/InnerWalletRouter.kt @@ -3,6 +3,7 @@ package com.tangem.feature.wallet.presentation.router import androidx.compose.runtime.Composable import androidx.compose.runtime.Stable import androidx.fragment.app.FragmentManager +import com.tangem.core.navigation.AppScreen import com.tangem.domain.tokens.models.CryptoCurrency import com.tangem.domain.wallets.models.UserWalletId import com.tangem.features.wallet.navigation.WalletRouter @@ -28,7 +29,7 @@ internal interface InnerWalletRouter : WalletRouter { fun Initialize(fragmentManager: FragmentManager) /** Pop back stack */ - fun popBackStack() + fun popBackStack(screen: AppScreen? = null) /** Open organize tokens screen */ fun openOrganizeTokensScreen(userWalletId: UserWalletId) @@ -44,4 +45,16 @@ internal interface InnerWalletRouter : WalletRouter { /** Open token details screen */ fun openTokenDetails(currency: CryptoCurrency) + + /** Open stories screen */ + fun openStoriesScreen() + + /** Open save user wallet screen */ + fun openSaveUserWalletScreen() + + /** Is wallet last screen */ + fun isWalletLastScreen(): Boolean + + /** Open manage tokens screen */ + fun openManageTokensScreen() } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/WalletAdditionalInfoFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/WalletAdditionalInfoFactory.kt index affbdde057..4b03d163f5 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/WalletAdditionalInfoFactory.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/WalletAdditionalInfoFactory.kt @@ -1,7 +1,12 @@ package com.tangem.feature.wallet.presentation.wallet.domain +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.plus +import com.tangem.core.ui.extensions.wrappedList +import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.domain.common.CardTypesResolver -import com.tangem.utils.toFormattedCurrencyString +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.feature.wallet.impl.R import java.math.BigDecimal /** @@ -9,35 +14,66 @@ import java.math.BigDecimal * [REDACTED_AUTHOR] */ -// TODO: Finalize strings [REDACTED_JIRA] internal object WalletAdditionalInfoFactory { + private val DIVIDER_RES by lazy { TextReference.Str(value = " • ") } + /** * Get additional info * - * @param cardTypesResolver card types resolver - * @param isLocked check if wallet is locked + * @param cardTypesResolver card type resolver + * @param wallet current wallet * @param currencyAmount amount of currency */ - fun resolve(cardTypesResolver: CardTypesResolver, isLocked: Boolean, currencyAmount: BigDecimal? = null): String { + fun resolve( + cardTypesResolver: CardTypesResolver, + wallet: UserWallet, + currencyAmount: BigDecimal? = null, + ): TextReference { return if (cardTypesResolver.isMultiwalletAllowed()) { - val backupInfo = "${cardTypesResolver.getBackupCardsCount()} cards" - when { - cardTypesResolver.isWallet2() && !isLocked -> "$backupInfo • Seed phrase" - cardTypesResolver.isTangemWallet() && !isLocked -> backupInfo - isLocked -> "$backupInfo • Locked" - else -> "" - } + resolveMultiCurrencyInfo(cardTypesResolver, wallet) } else { - if (isLocked) { - "Locked" + resolveSingleCurrencyInfo(cardTypesResolver, wallet, currencyAmount) + } + } + + private fun resolveMultiCurrencyInfo(cardTypeResolver: CardTypesResolver, wallet: UserWallet): TextReference { + val backupCardsCount = wallet.cardsInWallet.size + 1 + val backupInfoRes = TextReference.PluralRes( + id = R.plurals.card_label_card_count, + count = backupCardsCount, + formatArgs = wrappedList(backupCardsCount), + ) + + return if (wallet.isLocked) { + backupInfoRes + DIVIDER_RES + TextReference.Res(R.string.common_locked) + } else { + if (cardTypeResolver.isWallet2()) { + backupInfoRes + DIVIDER_RES + TextReference.Res(id = R.string.common_seed_phrase) } else { - val blockchain = cardTypesResolver.getBlockchain() - currencyAmount?.toFormattedCurrencyString( - decimals = blockchain.decimals(), - currency = blockchain.currency, - ).orEmpty() + backupInfoRes } } } + + private fun resolveSingleCurrencyInfo( + cardTypeResolver: CardTypesResolver, + wallet: UserWallet, + currencyAmount: BigDecimal?, + ): TextReference { + return if (wallet.isLocked) { + TextReference.Res(R.string.common_locked) + } else { + val blockchain = cardTypeResolver.getBlockchain() + val amount = currencyAmount?.let { + BigDecimalFormatter.formatCryptoAmount( + cryptoAmount = it, + cryptoCurrency = blockchain.currency, + decimals = blockchain.decimals(), + ) + } + + TextReference.Str(value = amount.orEmpty()) + } + } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/ActionsBottomSheetConfig.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/ActionsBottomSheetConfig.kt new file mode 100644 index 0000000000..cb7e79cee3 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/ActionsBottomSheetConfig.kt @@ -0,0 +1,17 @@ +package com.tangem.feature.wallet.presentation.wallet.state + +import kotlinx.collections.immutable.ImmutableList + +/** + * Config for the token actions bottom sheet + * + * @property isShow flag that determine if bottom sheet is shown + * @property onDismissRequest lambda be invoked when bottom sheet is dismissed + * @property actions actions + * + */ +internal data class ActionsBottomSheetConfig( + val isShow: Boolean, + val onDismissRequest: () -> Unit, + val actions: ImmutableList, +) \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/TokenActionButtonConfig.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/TokenActionButtonConfig.kt new file mode 100644 index 0000000000..934bfd6230 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/TokenActionButtonConfig.kt @@ -0,0 +1,18 @@ +package com.tangem.feature.wallet.presentation.wallet.state + +import androidx.annotation.DrawableRes + +/** + * Action button config + * + * @property text text + * @property iconResId icon resource id + * @property onClick lambda be invoked when action component is clicked + * @property enabled enabled + */ +data class TokenActionButtonConfig( + val text: String, + @DrawableRes val iconResId: Int, + val onClick: () -> Unit, + val enabled: Boolean = true, +) \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletMultiCurrencyState.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletMultiCurrencyState.kt index 87d675cd95..d22b1b6db6 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletMultiCurrencyState.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletMultiCurrencyState.kt @@ -22,6 +22,8 @@ internal sealed class WalletMultiCurrencyState : WalletState.ContentState() { override val notifications: ImmutableList, override val bottomSheetConfig: WalletBottomSheetConfig?, override val tokensListState: WalletTokensListState, + val tokenActionsBottomSheet: ActionsBottomSheetConfig?, + val onManageTokensClick: () -> Unit, ) : WalletMultiCurrencyState() data class Locked( diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletSingleCurrencyState.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletSingleCurrencyState.kt index 8433b8d9f7..6991db467a 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletSingleCurrencyState.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletSingleCurrencyState.kt @@ -1,24 +1,26 @@ package com.tangem.feature.wallet.presentation.wallet.state +import androidx.compose.runtime.Immutable +import androidx.paging.PagingData import com.tangem.core.ui.components.marketprice.MarketPriceBlockState +import com.tangem.core.ui.components.transactions.state.TransactionState import com.tangem.core.ui.components.transactions.state.TxHistoryState import com.tangem.feature.wallet.presentation.wallet.state.components.* import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf +import kotlinx.coroutines.flow.MutableStateFlow /** * Single currency wallet content state * [REDACTED_AUTHOR] */ +@Immutable internal sealed class WalletSingleCurrencyState : WalletState.ContentState() { /** Manage buttons */ abstract val buttons: ImmutableList - /** Market price block state */ - abstract val marketPriceBlockState: MarketPriceBlockState? - /** Transactions history state */ abstract val txHistoryState: TxHistoryState @@ -30,8 +32,8 @@ internal sealed class WalletSingleCurrencyState : WalletState.ContentState() { override val notifications: ImmutableList, override val bottomSheetConfig: WalletBottomSheetConfig?, override val buttons: ImmutableList, - override val marketPriceBlockState: MarketPriceBlockState, override val txHistoryState: TxHistoryState, + val marketPriceBlockState: MarketPriceBlockState, ) : WalletSingleCurrencyState() data class Locked( @@ -61,8 +63,21 @@ internal sealed class WalletSingleCurrencyState : WalletState.ContentState() { ), ) - override val marketPriceBlockState = null + override val txHistoryState: TxHistoryState = TxHistoryState.Content( + contentItems = MutableStateFlow( + value = PagingData.from( + data = listOf( + TxHistoryState.TxHistoryItemState.Title(onExploreClick = onExploreClick), + TxHistoryState.TxHistoryItemState.Transaction( + state = TransactionState.Locked(txHash = LOCKED_TX_HASH), + ), + ), + ), + ), + ) - override val txHistoryState: TxHistoryState = TxHistoryState.Locked(onExploreClick) + private companion object { + const val LOCKED_TX_HASH = "LOCKED_TX_HASH" + } } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletState.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletState.kt index 9cc1f3fa27..c9c327f540 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletState.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletState.kt @@ -34,14 +34,26 @@ internal sealed class WalletState { /** * Util function that allow to make a copy * - * @param walletsListConfig wallets list config + * @param walletsListConfig wallets list config + * @param pullToRefreshConfig pull to refresh config */ - fun copySealed(walletsListConfig: WalletsListConfig = this.walletsListConfig): ContentState { + fun copySealed( + walletsListConfig: WalletsListConfig = this.walletsListConfig, + pullToRefreshConfig: WalletPullToRefreshConfig = this.pullToRefreshConfig, + ): ContentState { return when (this) { - is WalletMultiCurrencyState.Content -> copy(walletsListConfig = walletsListConfig) - is WalletMultiCurrencyState.Locked -> copy(walletsListConfig = walletsListConfig) - is WalletSingleCurrencyState.Content -> copy(walletsListConfig = walletsListConfig) - is WalletSingleCurrencyState.Locked -> copy(walletsListConfig = walletsListConfig) + is WalletMultiCurrencyState.Content -> { + copy(walletsListConfig = walletsListConfig, pullToRefreshConfig = pullToRefreshConfig) + } + is WalletMultiCurrencyState.Locked -> { + copy(walletsListConfig = walletsListConfig, pullToRefreshConfig = pullToRefreshConfig) + } + is WalletSingleCurrencyState.Content -> { + copy(walletsListConfig = walletsListConfig, pullToRefreshConfig = pullToRefreshConfig) + } + is WalletSingleCurrencyState.Locked -> { + copy(walletsListConfig = walletsListConfig, pullToRefreshConfig = pullToRefreshConfig) + } } } } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/components/WalletCardState.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/components/WalletCardState.kt index 2da80de62f..e25b851c21 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/components/WalletCardState.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/components/WalletCardState.kt @@ -2,6 +2,7 @@ package com.tangem.feature.wallet.presentation.wallet.state.components import androidx.annotation.DrawableRes import androidx.compose.runtime.Immutable +import com.tangem.core.ui.extensions.TextReference import com.tangem.domain.wallets.models.UserWalletId /** Wallet card state */ @@ -14,15 +15,18 @@ internal sealed interface WalletCardState { /** Title */ val title: String - /** Additional wallet information */ - val additionalInfo: String + /** Additional text */ + val additionalInfo: TextReference? /** Wallet image resource id */ @get:DrawableRes val imageResId: Int? - /** Lambda be invoked when card is clicked */ - val onClick: (() -> Unit)? + /** Lambda be invoked when Rename button is clicked */ + val onRenameClick: (UserWalletId, String) -> Unit + + /** Lambda be invoked when Delete button is clicked */ + val onDeleteClick: (UserWalletId) -> Unit /** * Wallet card content state @@ -31,35 +35,20 @@ internal sealed interface WalletCardState { * @property title wallet name * @property additionalInfo wallet additional info * @property imageResId wallet image resource id - * @property onClick lambda be invoked when wallet card is clicked + * @property onRenameClick lambda be invoked when Rename button is clicked + * @property onDeleteClick lambda be invoked when Delete button is clicked * @property balance wallet balance */ data class Content( override val id: UserWalletId, override val title: String, - override val additionalInfo: String, + override val additionalInfo: TextReference, override val imageResId: Int?, - override val onClick: (() -> Unit)? = null, + override val onRenameClick: (UserWalletId, String) -> Unit, + override val onDeleteClick: (UserWalletId) -> Unit, val balance: String, ) : WalletCardState - /** - * Wallet card loading state - * - * @property id wallet id - * @property title wallet name - * @property additionalInfo wallet additional info - * @property imageResId wallet image resource id - * @property onClick lambda be invoked when wallet card is clicked - */ - data class Loading( - override val id: UserWalletId, - override val title: String, - override val additionalInfo: String, - override val imageResId: Int?, - override val onClick: (() -> Unit)? = null, - ) : WalletCardState - /** * Wallet card hidden content state * @@ -67,14 +56,35 @@ internal sealed interface WalletCardState { * @property title wallet name * @property additionalInfo wallet additional info * @property imageResId wallet image resource id - * @property onClick lambda be invoked when wallet card is clicked + * @property onRenameClick lambda be invoked when Rename button is clicked + * @property onDeleteClick lambda be invoked when Delete button is clicked */ data class HiddenContent( override val id: UserWalletId, override val title: String, - override val additionalInfo: String, + override val additionalInfo: TextReference = HIDDEN_BALANCE_TEXT, override val imageResId: Int?, - override val onClick: (() -> Unit)?, + override val onRenameClick: (UserWalletId, String) -> Unit, + override val onDeleteClick: (UserWalletId) -> Unit, + ) : WalletCardState + + /** + * Wallet card locked state + * + * @property id wallet id + * @property title wallet name + * @property additionalInfo wallet additional info + * @property imageResId wallet image resource id + * @property onRenameClick lambda be invoked when Rename button is clicked + * @property onDeleteClick lambda be invoked when Delete button is clicked + */ + data class LockedContent( + override val id: UserWalletId, + override val title: String, + override val additionalInfo: TextReference? = null, + override val imageResId: Int?, + override val onRenameClick: (UserWalletId, String) -> Unit, + override val onDeleteClick: (UserWalletId) -> Unit, ) : WalletCardState /** @@ -84,13 +94,49 @@ internal sealed interface WalletCardState { * @property title wallet name * @property additionalInfo wallet additional info * @property imageResId wallet image resource id - * @property onClick lambda be invoked when wallet card is clicked + * @property onRenameClick lambda be invoked when Rename button is clicked + * @property onDeleteClick lambda be invoked when Delete button is clicked */ data class Error( override val id: UserWalletId, override val title: String, - override val additionalInfo: String, + override val additionalInfo: TextReference = EMPTY_BALANCE_TEXT, override val imageResId: Int?, - override val onClick: (() -> Unit)?, + override val onRenameClick: (UserWalletId, String) -> Unit, + override val onDeleteClick: (UserWalletId) -> Unit, ) : WalletCardState + + /** + * Wallet card loading state + * + * @property id wallet id + * @property title wallet name + * @property additionalInfo wallet additional info + * @property imageResId wallet image resource id + * @property onRenameClick lambda be invoked when Rename button is clicked + * @property onDeleteClick lambda be invoked when Delete button is clicked + */ + data class Loading( + override val id: UserWalletId, + override val title: String, + override val additionalInfo: TextReference? = null, + override val imageResId: Int?, + override val onRenameClick: (UserWalletId, String) -> Unit, + override val onDeleteClick: (UserWalletId) -> Unit, + ) : WalletCardState + + fun copySealed(title: String = this.title): WalletCardState { + return when (this) { + is Content -> copy(title = title) + is Error -> copy(title = title) + is HiddenContent -> copy(title = title) + is Loading -> copy(title = title) + is LockedContent -> copy(title = title) + } + } + + companion object { + val HIDDEN_BALANCE_TEXT by lazy { TextReference.Str(value = "•••") } + val EMPTY_BALANCE_TEXT by lazy { TextReference.Str(value = "—") } + } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/components/WalletManageButton.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/components/WalletManageButton.kt index 6d6055c045..18b589196a 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/components/WalletManageButton.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/components/WalletManageButton.kt @@ -1,5 +1,6 @@ package com.tangem.feature.wallet.presentation.wallet.state.components +import androidx.compose.runtime.Immutable import com.tangem.core.ui.components.buttons.actions.ActionButtonConfig import com.tangem.core.ui.extensions.TextReference import com.tangem.feature.wallet.impl.R @@ -7,94 +8,87 @@ import com.tangem.feature.wallet.impl.R /** * Wallet manage button state * - * @param config action config + * @property config action config * [REDACTED_AUTHOR] */ -sealed class WalletManageButton(val config: ActionButtonConfig) { +@Immutable +internal sealed class WalletManageButton(val config: ActionButtonConfig) { /** Lambda be invoked when manage button is clicked */ - abstract val onClick: (() -> Unit)? + abstract val onClick: () -> Unit /** * Buy * - * @param onClick lambda be invoked when manage button is clicked + * @property enabled button click availability + * @property onClick lambda be invoked when Buy button is clicked */ - data class Buy(override val onClick: (() -> Unit)? = null) : WalletManageButton( + data class Buy(val enabled: Boolean, override val onClick: () -> Unit) : WalletManageButton( config = ActionButtonConfig( text = TextReference.Res(id = R.string.common_buy), iconResId = R.drawable.ic_plus_24, - onClick = onClick ?: {}, - enabled = onClick != null, - ), - ) - - /** - * Sell - * - * @param onClick lambda be invoked when manage button is clicked - */ - data class Sell(override val onClick: (() -> Unit)? = null) : WalletManageButton( - config = ActionButtonConfig( - text = TextReference.Res(id = R.string.common_sell), - iconResId = R.drawable.ic_currency_24, - onClick = onClick ?: {}, - enabled = onClick != null, + onClick = onClick, + enabled = enabled, ), ) /** * Send * - * @param onClick lambda be invoked when manage button is clicked + * @property enabled button click availability + * @property onClick lambda be invoked when Send button is clicked */ - data class Send(override val onClick: (() -> Unit)? = null) : WalletManageButton( + data class Send(val enabled: Boolean, override val onClick: () -> Unit) : WalletManageButton( config = ActionButtonConfig( text = TextReference.Res(id = R.string.common_send), iconResId = R.drawable.ic_arrow_up_24, - onClick = onClick ?: {}, - enabled = onClick != null, + onClick = onClick, + enabled = enabled, ), ) /** * Receive * - * @param onClick lambda be invoked when manage button is clicked + * @property onClick lambda be invoked when Receive button is clicked */ data class Receive(override val onClick: () -> Unit) : WalletManageButton( config = ActionButtonConfig( text = TextReference.Res(id = R.string.common_receive), iconResId = R.drawable.ic_arrow_down_24, onClick = onClick, + enabled = true, ), ) /** - * Exchange + * Sell * - * @param onClick lambda be invoked when manage button is clicked + * @property enabled button click availability + * @property onClick lambda be invoked when Sell button is clicked */ - data class Exchange(override val onClick: (() -> Unit)? = null) : WalletManageButton( + data class Sell(val enabled: Boolean, override val onClick: () -> Unit) : WalletManageButton( config = ActionButtonConfig( - text = TextReference.Res(id = R.string.common_exchange), - iconResId = R.drawable.ic_exchange_vertical_24, - onClick = onClick ?: {}, - enabled = onClick != null, - ), - ) - - /** - * Copy address - * - * @param onClick lambda be invoked when manage button is clicked - */ - data class CopyAddress(override val onClick: () -> Unit) : WalletManageButton( - config = ActionButtonConfig( - text = TextReference.Res(id = R.string.common_copy_address), - iconResId = R.drawable.ic_copy_24, + text = TextReference.Res(id = R.string.common_sell), + iconResId = R.drawable.ic_currency_24, onClick = onClick, + enabled = enabled, + ), + ) + + /** + * Swap + * + * @property enabled button click availability + * @property onClick lambda be invoked when Swap button is clicked + */ + data class Swap(val enabled: Boolean, override val onClick: () -> Unit) : WalletManageButton( + config = ActionButtonConfig( + text = TextReference.Res(id = R.string.common_swap), + iconResId = R.drawable.ic_exchange_vertical_24, + onClick = onClick, + enabled = enabled, ), ) } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/components/WalletNotification.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/components/WalletNotification.kt index 99a59df14d..60abf781ae 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/components/WalletNotification.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/components/WalletNotification.kt @@ -1,5 +1,6 @@ package com.tangem.feature.wallet.presentation.wallet.state.components +import androidx.compose.runtime.Immutable import com.tangem.core.ui.components.notifications.NotificationState import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.WrappedList @@ -14,6 +15,7 @@ import com.tangem.feature.wallet.impl.R [REDACTED_AUTHOR] */ // TODO: Finalize notification strings [REDACTED_JIRA] +@Immutable sealed class WalletNotification(open val state: NotificationState) { /** Clickable notification */ diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/components/WalletTokensListState.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/components/WalletTokensListState.kt index 1209a2da37..8b973054f2 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/components/WalletTokensListState.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/components/WalletTokensListState.kt @@ -1,6 +1,5 @@ package com.tangem.feature.wallet.presentation.wallet.state.components -import com.tangem.core.ui.components.wallet.WalletLockedContentState import com.tangem.core.ui.extensions.TextReference import com.tangem.feature.wallet.impl.R import com.tangem.feature.wallet.presentation.common.state.TokenItemState @@ -28,14 +27,17 @@ internal sealed class WalletTokensListState { open val onOrganizeTokensClick: (() -> Unit)?, ) : WalletTokensListState() - /** Loading content state */ - object Loading : ContentState( - items = persistentListOf( + /** + * Loading content state + * + * @property items content items + */ + data class Loading( + override val items: ImmutableList = persistentListOf( TokensListItemState.Token(state = TokenItemState.Loading(id = FIRST_LOADING_TOKEN_ID)), TokensListItemState.Token(state = TokenItemState.Loading(id = SECOND_LOADING_TOKEN_ID)), ), - onOrganizeTokensClick = null, - ) + ) : ContentState(items = items, onOrganizeTokensClick = null) /** * Content state @@ -49,15 +51,13 @@ internal sealed class WalletTokensListState { ) : ContentState(items, onOrganizeTokensClick) /** Locked content state */ - object Locked : - ContentState( - items = persistentListOf( - TokensListItemState.NetworkGroupTitle(value = TextReference.Res(id = R.string.main_tokens)), - TokensListItemState.Token(state = TokenItemState.Loading(id = LOCKED_TOKEN_ID)), - ), - onOrganizeTokensClick = null, + object Locked : ContentState( + items = persistentListOf( + TokensListItemState.NetworkGroupTitle(value = TextReference.Res(id = R.string.main_tokens)), + TokensListItemState.Token(state = TokenItemState.Locked(id = LOCKED_TOKEN_ID)), ), - WalletLockedContentState + onOrganizeTokensClick = null, + ) /** Tokens list item state */ sealed interface TokensListItemState { diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/TokenActionsProvider.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/TokenActionsProvider.kt new file mode 100644 index 0000000000..f9a7dbfc4e --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/TokenActionsProvider.kt @@ -0,0 +1,52 @@ +package com.tangem.feature.wallet.presentation.wallet.state.factory + +import com.tangem.common.Provider +import com.tangem.feature.wallet.impl.R +import com.tangem.feature.wallet.presentation.common.state.TokenItemState +import com.tangem.feature.wallet.presentation.wallet.state.TokenActionButtonConfig +import com.tangem.feature.wallet.presentation.wallet.state.WalletState +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.toImmutableList + +/** + * Converter from loaded [TokenItemState.Content] to ImmutableList<[TokenActionButtonConfig]> + * + * @property currentStateProvider current ui state provider + * + */ +@Suppress("UnusedPrivateMember") +internal class TokenActionsProvider( + private val currentStateProvider: Provider, +) { + + @Suppress("UnusedPrivateMember") + fun provideActions(tokenId: String): ImmutableList { + // TODO: [REDACTED_JIRA] + return mockTokenActionButtonConfig().toImmutableList() + } + + private fun mockTokenActionButtonConfig(): List { + return listOf( + TokenActionButtonConfig( + text = "Send", + iconResId = R.drawable.ic_plus_24, + onClick = {}, + ), + TokenActionButtonConfig( + text = "Buy", + iconResId = R.drawable.ic_plus_24, + onClick = {}, + ), + TokenActionButtonConfig( + text = "Sell", + iconResId = R.drawable.ic_plus_24, + onClick = {}, + ), + TokenActionButtonConfig( + text = "Swap", + iconResId = R.drawable.ic_plus_24, + onClick = {}, + ), + ) + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletCryptoCurrencyActionsConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletCryptoCurrencyActionsConverter.kt new file mode 100644 index 0000000000..597680d5eb --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletCryptoCurrencyActionsConverter.kt @@ -0,0 +1,50 @@ +package com.tangem.feature.wallet.presentation.wallet.state.factory + +import com.tangem.common.Provider +import com.tangem.domain.tokens.model.TokenActionsState +import com.tangem.feature.wallet.presentation.wallet.state.WalletMultiCurrencyState +import com.tangem.feature.wallet.presentation.wallet.state.WalletSingleCurrencyState +import com.tangem.feature.wallet.presentation.wallet.state.WalletState +import com.tangem.feature.wallet.presentation.wallet.state.components.WalletManageButton +import com.tangem.feature.wallet.presentation.wallet.viewmodels.WalletClickIntents +import com.tangem.utils.converter.Converter +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.toImmutableList + +internal class WalletCryptoCurrencyActionsConverter( + private val currentStateProvider: Provider, + private val clickIntents: WalletClickIntents, +) : Converter, WalletState> { + + override fun convert(value: List): WalletState { + return when (val state = currentStateProvider()) { + is WalletSingleCurrencyState.Content -> state.copy(buttons = value.mapToManageButtons()) + is WalletSingleCurrencyState.Locked, + is WalletMultiCurrencyState, + is WalletState.Initial, + -> state + } + } + + private fun List.mapToManageButtons(): ImmutableList { + return this + .mapNotNull { action -> + when (action) { + is TokenActionsState.ActionState.Buy -> { + WalletManageButton.Buy(enabled = action.enabled, onClick = clickIntents::onBuyClick) + } + is TokenActionsState.ActionState.Receive -> { + WalletManageButton.Receive(onClick = clickIntents::onReceiveClick) + } + is TokenActionsState.ActionState.Sell -> { + WalletManageButton.Sell(enabled = action.enabled, onClick = clickIntents::onSellClick) + } + is TokenActionsState.ActionState.Send -> { + WalletManageButton.Send(enabled = action.enabled, onClick = clickIntents::onSendClick) + } + is TokenActionsState.ActionState.Swap -> null + } + } + .toImmutableList() + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletLoadedTokensListConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletLoadedTokensListConverter.kt index de50af78d2..a98b75e4f8 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletLoadedTokensListConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletLoadedTokensListConverter.kt @@ -6,6 +6,7 @@ import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.common.CardTypesResolver import com.tangem.domain.tokens.error.TokenListError import com.tangem.domain.tokens.model.TokenList +import com.tangem.domain.wallets.models.UserWallet import com.tangem.feature.wallet.presentation.wallet.state.WalletMultiCurrencyState import com.tangem.feature.wallet.presentation.wallet.state.WalletState import com.tangem.feature.wallet.presentation.wallet.state.factory.WalletLoadedTokensListConverter.LoadedTokensListModel @@ -19,7 +20,7 @@ import com.tangem.utils.converter.Converter * * @property currentStateProvider current ui state provider * @param cardTypeResolverProvider card type resolver - * @param isLockedWalletProvider current wallet is locked or not provider + * @param currentWalletProvider current wallet provider * @param clickIntents screen click intents * [REDACTED_AUTHOR] @@ -28,14 +29,14 @@ internal class WalletLoadedTokensListConverter( private val currentStateProvider: Provider, appCurrencyProvider: Provider, cardTypeResolverProvider: Provider, - isLockedWalletProvider: Provider, + currentWalletProvider: Provider, clickIntents: WalletClickIntents, -) : Converter { +) : Converter { private val tokenListStateConverter = TokenListToWalletStateConverter( currentStateProvider = currentStateProvider, cardTypeResolverProvider = cardTypeResolverProvider, - isLockedWalletProvider = isLockedWalletProvider, + currentWalletProvider = currentWalletProvider, appCurrencyProvider = appCurrencyProvider, isWalletContentHidden = false, // TODO: [REDACTED_JIRA] clickIntents = clickIntents, @@ -45,7 +46,7 @@ internal class WalletLoadedTokensListConverter( currentStateProvider = currentStateProvider, ) - override fun convert(value: LoadedTokensListModel): WalletMultiCurrencyState.Content { + override fun convert(value: LoadedTokensListModel): WalletState { return value.tokenListEither.fold( ifLeft = tokenListErrorStateConverter::convert, ifRight = { diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletLockedConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletLockedConverter.kt new file mode 100644 index 0000000000..5ec6613bb8 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletLockedConverter.kt @@ -0,0 +1,110 @@ +package com.tangem.feature.wallet.presentation.wallet.state.factory + +import com.tangem.common.Provider +import com.tangem.domain.common.CardTypesResolver +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.feature.wallet.presentation.wallet.domain.WalletAdditionalInfoFactory +import com.tangem.feature.wallet.presentation.wallet.state.WalletMultiCurrencyState +import com.tangem.feature.wallet.presentation.wallet.state.WalletSingleCurrencyState +import com.tangem.feature.wallet.presentation.wallet.state.WalletState +import com.tangem.feature.wallet.presentation.wallet.state.components.WalletCardState +import com.tangem.feature.wallet.presentation.wallet.state.components.WalletManageButton +import com.tangem.feature.wallet.presentation.wallet.state.components.WalletTopBarConfig +import com.tangem.feature.wallet.presentation.wallet.state.components.WalletsListConfig +import com.tangem.feature.wallet.presentation.wallet.viewmodels.WalletClickIntents +import com.tangem.utils.converter.Converter +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toImmutableList + +internal class WalletLockedConverter( + private val currentStateProvider: Provider, + private val currentCardTypeResolverProvider: Provider, + private val currentWalletProvider: Provider, + private val clickIntents: WalletClickIntents, +) : Converter { + + override fun convert(value: Unit): WalletState { + return when (val state = currentStateProvider()) { + is WalletState.ContentState -> { + val cardTypeResolver = currentCardTypeResolverProvider() + + if (cardTypeResolver.isMultiwalletAllowed()) { + state.toMultiCurrencyLockedState(cardTypeResolver) + } else { + state.toSingleCurrencyLockedState(cardTypeResolver) + } + } + is WalletState.Initial -> state + } + } + + private fun WalletState.ContentState.toMultiCurrencyLockedState( + cardTypeResolver: CardTypesResolver, + ): WalletMultiCurrencyState.Locked { + return WalletMultiCurrencyState.Locked( + onBackClick = onBackClick, + topBarConfig = createTopBarConfig(), + walletsListConfig = createWalletsListConfig(cardTypeResolver), + pullToRefreshConfig = pullToRefreshConfig, + onUnlockWalletsNotificationClick = clickIntents::onUnlockWalletNotificationClick, + onUnlockClick = clickIntents::onUnlockWalletClick, + onScanClick = clickIntents::onScanCardClick, + ) + } + + private fun WalletState.ContentState.toSingleCurrencyLockedState( + cardTypeResolver: CardTypesResolver, + ): WalletSingleCurrencyState.Locked { + return WalletSingleCurrencyState.Locked( + onBackClick = onBackClick, + topBarConfig = createTopBarConfig(), + walletsListConfig = createWalletsListConfig(cardTypeResolver), + pullToRefreshConfig = pullToRefreshConfig, + buttons = createButtons(), + onUnlockWalletsNotificationClick = clickIntents::onUnlockWalletNotificationClick, + onUnlockClick = clickIntents::onUnlockWalletClick, + onScanClick = clickIntents::onScanCardClick, + onExploreClick = clickIntents::onExploreClick, + ) + } + + private fun WalletState.ContentState.createTopBarConfig(): WalletTopBarConfig { + return topBarConfig.copy(onMoreClick = clickIntents::onUnlockWalletNotificationClick) + } + + private fun WalletState.ContentState.createWalletsListConfig( + cardTypeResolver: CardTypesResolver, + ): WalletsListConfig { + return walletsListConfig.copy( + wallets = walletsListConfig.wallets + .map { walletCardState -> + WalletCardState.LockedContent( + id = walletCardState.id, + title = walletCardState.title, + additionalInfo = if (cardTypeResolver.isMultiwalletAllowed()) { + WalletAdditionalInfoFactory.resolve( + cardTypesResolver = cardTypeResolver, + wallet = currentWalletProvider(), + ) + } else { + null + }, + imageResId = walletCardState.imageResId, + onRenameClick = walletCardState.onRenameClick, + onDeleteClick = walletCardState.onDeleteClick, + ) + } + .toImmutableList(), + ) + } + + private fun createButtons(): ImmutableList { + return persistentListOf( + WalletManageButton.Buy(enabled = false, onClick = {}), + WalletManageButton.Send(enabled = false, onClick = {}), + WalletManageButton.Receive(onClick = {}), + WalletManageButton.Sell(enabled = false, onClick = {}), + ) + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletRefreshStateConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletRefreshStateConverter.kt new file mode 100644 index 0000000000..f820ff8444 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletRefreshStateConverter.kt @@ -0,0 +1,124 @@ +package com.tangem.feature.wallet.presentation.wallet.state.factory + +import com.tangem.common.Provider +import com.tangem.core.ui.components.marketprice.MarketPriceBlockState +import com.tangem.core.ui.components.transactions.state.TxHistoryState +import com.tangem.domain.common.CardTypesResolver +import com.tangem.feature.wallet.presentation.common.state.TokenItemState +import com.tangem.feature.wallet.presentation.wallet.state.WalletMultiCurrencyState +import com.tangem.feature.wallet.presentation.wallet.state.WalletSingleCurrencyState +import com.tangem.feature.wallet.presentation.wallet.state.WalletState +import com.tangem.feature.wallet.presentation.wallet.state.components.* +import com.tangem.feature.wallet.presentation.wallet.state.components.WalletTokensListState.TokensListItemState +import com.tangem.feature.wallet.presentation.wallet.viewmodels.WalletClickIntents +import com.tangem.utils.converter.Converter +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.toImmutableList +import kotlinx.collections.immutable.toPersistentList +import kotlinx.coroutines.flow.update + +internal class WalletRefreshStateConverter( + private val currentStateProvider: Provider, + private val currentCardTypeResolverProvider: Provider, + private val clickIntents: WalletClickIntents, +) : Converter { + + override fun convert(value: Unit): WalletState { + return when (val state = currentStateProvider()) { + is WalletMultiCurrencyState.Content -> state.getRefreshState() + is WalletSingleCurrencyState.Content -> state.getRefreshState() + else -> state + } + } + + private fun WalletMultiCurrencyState.Content.getRefreshState(): WalletMultiCurrencyState.Content { + return copy( + walletsListConfig = createWalletsListConfig(), + pullToRefreshConfig = createPullToRefreshConfig(), + tokensListState = createTokenListState(), + ) + } + + private fun WalletSingleCurrencyState.Content.getRefreshState(): WalletSingleCurrencyState.Content { + return copy( + walletsListConfig = createWalletsListConfig(), + pullToRefreshConfig = createPullToRefreshConfig(), + buttons = buttons.mapToDisabledButton(), + txHistoryState = createTxHistoryState(), + marketPriceBlockState = MarketPriceBlockState.Loading(currencyName = marketPriceBlockState.currencyName), + ) + } + + private fun WalletState.ContentState.createWalletsListConfig(): WalletsListConfig { + val selectedWallet = walletsListConfig.wallets[walletsListConfig.selectedWalletIndex] + val additionalInfo = if (currentCardTypeResolverProvider().isMultiwalletAllowed()) { + selectedWallet.additionalInfo + } else { + null + } + + return walletsListConfig.copy( + wallets = walletsListConfig.wallets.toPersistentList().set( + index = walletsListConfig.selectedWalletIndex, + element = WalletCardState.Loading( + id = selectedWallet.id, + title = selectedWallet.title, + additionalInfo = additionalInfo, + imageResId = selectedWallet.imageResId, + onRenameClick = selectedWallet.onRenameClick, + onDeleteClick = selectedWallet.onDeleteClick, + ), + ), + ) + } + + private fun WalletState.ContentState.createPullToRefreshConfig(): WalletPullToRefreshConfig { + return pullToRefreshConfig.copy(isRefreshing = true) + } + + private fun WalletMultiCurrencyState.Content.createTokenListState(): WalletTokensListState { + return when (tokensListState) { + is WalletTokensListState.Content -> { + WalletTokensListState.Loading( + items = tokensListState.items + .filterIsInstance() + .mapToLoadingTokenState(), + ) + } + is WalletTokensListState.Empty -> WalletTokensListState.Loading() + is WalletTokensListState.Loading, + is WalletTokensListState.Locked, + -> tokensListState + } + } + + private fun List.mapToLoadingTokenState(): ImmutableList { + return this + .map { TokensListItemState.Token(state = TokenItemState.Loading(id = it.state.id)) } + .toImmutableList() + } + + private fun ImmutableList.mapToDisabledButton(): ImmutableList { + return this + .mapNotNull { button -> + when (button) { + is WalletManageButton.Buy -> button.copy(enabled = false) + is WalletManageButton.Send -> button.copy(enabled = false) + is WalletManageButton.Receive -> button + is WalletManageButton.Sell -> button.copy(enabled = false) + is WalletManageButton.Swap -> null + } + } + .toImmutableList() + } + + private fun WalletSingleCurrencyState.Content.createTxHistoryState(): TxHistoryState { + if (txHistoryState is TxHistoryState.Content) { + txHistoryState.contentItems.update { + TxHistoryState.getDefaultLoadingTransactions(onExploreClick = clickIntents::onExploreClick) + } + } + + return txHistoryState + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletSingleCurrencyLoadedBalanceConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletSingleCurrencyLoadedBalanceConverter.kt index c6d3e7fae9..6acdd08c82 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletSingleCurrencyLoadedBalanceConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletSingleCurrencyLoadedBalanceConverter.kt @@ -7,13 +7,15 @@ import com.tangem.core.ui.components.marketprice.PriceChangeConfig import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.common.CardTypesResolver -import com.tangem.domain.tokens.error.CurrencyError +import com.tangem.domain.tokens.error.CurrencyStatusError import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.wallets.models.UserWallet import com.tangem.feature.wallet.presentation.wallet.domain.WalletAdditionalInfoFactory import com.tangem.feature.wallet.presentation.wallet.state.WalletSingleCurrencyState import com.tangem.feature.wallet.presentation.wallet.state.WalletState import com.tangem.feature.wallet.presentation.wallet.state.components.WalletCardState import com.tangem.feature.wallet.presentation.wallet.state.components.WalletsListConfig +import com.tangem.feature.wallet.presentation.wallet.state.factory.WalletSingleCurrencyLoadedBalanceConverter.SingleCurrencyLoadedBalanceModel import com.tangem.utils.converter.Converter import kotlinx.collections.immutable.toPersistentList import java.math.BigDecimal @@ -22,21 +24,33 @@ internal class WalletSingleCurrencyLoadedBalanceConverter( private val currentStateProvider: Provider, private val cardTypeResolverProvider: Provider, private val appCurrencyProvider: Provider, -) : Converter, WalletSingleCurrencyState.Content> { + private val currentWalletProvider: Provider, +) : Converter { - override fun convert(value: Either): WalletSingleCurrencyState.Content { - return value.fold(ifLeft = { convertError() }, ifRight = ::convert) + override fun convert(value: SingleCurrencyLoadedBalanceModel): WalletSingleCurrencyState.Content { + return value.cryptoCurrencyEither.fold( + ifLeft = { convertError() }, + ifRight = { convertContent(it, value.isRefreshing) }, + ) } private fun convertError(): WalletSingleCurrencyState.Content { return requireNotNull(currentStateProvider() as? WalletSingleCurrencyState.Content) } - private fun convert(status: CryptoCurrencyStatus): WalletSingleCurrencyState.Content { + private fun convertContent( + status: CryptoCurrencyStatus, + isRefreshing: Boolean, + ): WalletSingleCurrencyState.Content { val state = requireNotNull(currentStateProvider() as? WalletSingleCurrencyState.Content) val currencyName = state.marketPriceBlockState.currencyName return state.copy( walletsListConfig = getUpdatedSelectedWallet(status = status.value, state = state), + pullToRefreshConfig = if (isRefreshing) { + state.pullToRefreshConfig.copy(isRefreshing = status.value is CryptoCurrencyStatus.Loading) + } else { + state.pullToRefreshConfig + }, marketPriceBlockState = getMarketPriceState(status = status.value, currencyName = currencyName), ) } @@ -77,21 +91,22 @@ internal class WalletSingleCurrencyLoadedBalanceConverter( title = selectedWallet.title, additionalInfo = WalletAdditionalInfoFactory.resolve( cardTypesResolver = cardTypeResolverProvider(), - isLocked = false, + wallet = currentWalletProvider(), currencyAmount = status.amount, ), imageResId = selectedWallet.imageResId, - onClick = selectedWallet.onClick, - balance = formatFiatAmount(status, appCurrencyProvider()), + onRenameClick = selectedWallet.onRenameClick, + onDeleteClick = selectedWallet.onDeleteClick, + balance = formatFiatAmount(status = status, appCurrency = appCurrencyProvider()), ) } is CryptoCurrencyStatus.Loading -> { WalletCardState.Loading( id = selectedWallet.id, title = selectedWallet.title, - additionalInfo = selectedWallet.additionalInfo, imageResId = selectedWallet.imageResId, - onClick = selectedWallet.onClick, + onRenameClick = selectedWallet.onRenameClick, + onDeleteClick = selectedWallet.onDeleteClick, ) } is CryptoCurrencyStatus.MissedDerivation, @@ -102,9 +117,9 @@ internal class WalletSingleCurrencyLoadedBalanceConverter( WalletCardState.Error( id = selectedWallet.id, title = selectedWallet.title, - additionalInfo = selectedWallet.additionalInfo, imageResId = selectedWallet.imageResId, - onClick = selectedWallet.onClick, + onRenameClick = selectedWallet.onRenameClick, + onDeleteClick = selectedWallet.onDeleteClick, ) } } @@ -153,4 +168,9 @@ internal class WalletSingleCurrencyLoadedBalanceConverter( fiatCurrencySymbol = appCurrency.symbol, ) } + + data class SingleCurrencyLoadedBalanceModel( + val cryptoCurrencyEither: Either, + val isRefreshing: Boolean, + ) } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletSkeletonStateConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletSkeletonStateConverter.kt index f40a3b577a..15e42f3283 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletSkeletonStateConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletSkeletonStateConverter.kt @@ -17,6 +17,7 @@ import com.tangem.utils.converter.Converter import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList +import kotlinx.coroutines.flow.MutableStateFlow /** * Converter from loaded list of [UserWallet] to skeleton state of screen [WalletState.ContentState] @@ -47,9 +48,11 @@ internal class WalletSkeletonStateConverter( topBarConfig = createTopBarConfig(), walletsListConfig = createWalletsListConfig(value), pullToRefreshConfig = createPullToRefreshConfig(), - tokensListState = WalletTokensListState.Loading, + tokensListState = WalletTokensListState.Loading(), notifications = persistentListOf(), bottomSheetConfig = null, + tokenActionsBottomSheet = null, + onManageTokensClick = clickIntents::onManageTokensClick, ) } @@ -61,9 +64,13 @@ internal class WalletSkeletonStateConverter( pullToRefreshConfig = createPullToRefreshConfig(), notifications = persistentListOf(), bottomSheetConfig = null, - buttons = getButtons(), + buttons = createButtons(), marketPriceBlockState = MarketPriceBlockState.Loading(currencyName = currencyName), - txHistoryState = TxHistoryState.Loading(onExploreClick = clickIntents::onExploreClick), + txHistoryState = TxHistoryState.Content( + contentItems = MutableStateFlow( + value = TxHistoryState.getDefaultLoadingTransactions(clickIntents::onExploreClick), + ), + ), ) } @@ -91,7 +98,7 @@ internal class WalletSkeletonStateConverter( // If wallet is initialized, return it, otherwise return loading state if (initializedWallet !is WalletCardState.Loading) { - initializedWallet + initializedWallet.copySealed(title = wallet.name) } else { createWalletLoadingState(wallet) } @@ -106,11 +113,14 @@ internal class WalletSkeletonStateConverter( return WalletCardState.Loading( id = wallet.walletId, title = wallet.name, - additionalInfo = WalletAdditionalInfoFactory.resolve( - cardTypesResolver = cardTypeResolver, - isLocked = wallet.isLocked, - ), + additionalInfo = if (cardTypeResolver.isMultiwalletAllowed()) { + WalletAdditionalInfoFactory.resolve(cardTypesResolver = cardTypeResolver, wallet = wallet) + } else { + null + }, imageResId = WalletImageResolver.resolve(cardTypesResolver = cardTypeResolver), + onRenameClick = clickIntents::onRenameClick, + onDeleteClick = clickIntents::onDeleteClick, ) } @@ -118,15 +128,12 @@ internal class WalletSkeletonStateConverter( return WalletPullToRefreshConfig(isRefreshing = false, onRefresh = clickIntents::onRefreshSwipe) } - // TODO: [REDACTED_JIRA] - private fun getButtons(): ImmutableList { + private fun createButtons(): ImmutableList { return persistentListOf( - WalletManageButton.Buy(), - WalletManageButton.Send(), + WalletManageButton.Buy(enabled = false, onClick = {}), + WalletManageButton.Send(enabled = false, onClick = {}), WalletManageButton.Receive(onClick = {}), - WalletManageButton.Exchange(), - WalletManageButton.Sell(), - WalletManageButton.CopyAddress(onClick = {}), + WalletManageButton.Sell(enabled = false, onClick = {}), ) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletStateFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletStateFactory.kt index 9e274774b5..512d0d0cc0 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletStateFactory.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletStateFactory.kt @@ -5,25 +5,25 @@ import arrow.core.Either import com.tangem.common.Provider import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.common.CardTypesResolver -import com.tangem.domain.tokens.error.CurrencyError +import com.tangem.domain.tokens.error.CurrencyStatusError import com.tangem.domain.tokens.error.TokenListError import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.tokens.model.TokenActionsState import com.tangem.domain.tokens.model.TokenList import com.tangem.domain.txhistory.models.TxHistoryItem import com.tangem.domain.txhistory.models.TxHistoryListError import com.tangem.domain.txhistory.models.TxHistoryStateError import com.tangem.domain.wallets.models.UserWallet +import com.tangem.feature.wallet.presentation.wallet.state.ActionsBottomSheetConfig import com.tangem.feature.wallet.presentation.wallet.state.WalletMultiCurrencyState import com.tangem.feature.wallet.presentation.wallet.state.WalletSingleCurrencyState import com.tangem.feature.wallet.presentation.wallet.state.WalletState import com.tangem.feature.wallet.presentation.wallet.state.components.WalletBottomSheetConfig -import com.tangem.feature.wallet.presentation.wallet.state.components.WalletManageButton import com.tangem.feature.wallet.presentation.wallet.state.components.WalletNotification import com.tangem.feature.wallet.presentation.wallet.state.factory.txhistory.WalletLoadedTxHistoryConverter import com.tangem.feature.wallet.presentation.wallet.state.factory.txhistory.WalletLoadingTxHistoryConverter import com.tangem.feature.wallet.presentation.wallet.viewmodels.WalletClickIntents import kotlinx.collections.immutable.ImmutableList -import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.flow.Flow /** @@ -31,24 +31,25 @@ import kotlinx.coroutines.flow.Flow * * @property currentStateProvider current ui state provider * @property currentCardTypeResolverProvider current card type resolver - * @property isLockedWalletProvider current wallet is locked or not + * @property currentWalletProvider current wallet * @property clickIntents screen click intents */ internal class WalletStateFactory( private val currentStateProvider: Provider, private val currentCardTypeResolverProvider: Provider, - private val isLockedWalletProvider: Provider, + private val currentWalletProvider: Provider, private val appCurrencyProvider: Provider, private val clickIntents: WalletClickIntents, ) { + private val tokenActionsProvider by lazy { TokenActionsProvider(currentStateProvider = currentStateProvider) } private val skeletonConverter by lazy { WalletSkeletonStateConverter(currentStateProvider, clickIntents) } private val loadedTokensListConverter by lazy { WalletLoadedTokensListConverter( currentStateProvider = currentStateProvider, cardTypeResolverProvider = currentCardTypeResolverProvider, - isLockedWalletProvider = isLockedWalletProvider, + currentWalletProvider = currentWalletProvider, appCurrencyProvider = appCurrencyProvider, clickIntents = clickIntents, ) @@ -74,6 +75,31 @@ internal class WalletStateFactory( currentStateProvider = currentStateProvider, cardTypeResolverProvider = currentCardTypeResolverProvider, appCurrencyProvider = appCurrencyProvider, + currentWalletProvider = currentWalletProvider, + ) + } + + private val lockedConverter by lazy { + WalletLockedConverter( + currentStateProvider = currentStateProvider, + currentCardTypeResolverProvider = currentCardTypeResolverProvider, + currentWalletProvider = currentWalletProvider, + clickIntents = clickIntents, + ) + } + + private val refreshStateConverter by lazy { + WalletRefreshStateConverter( + currentStateProvider = currentStateProvider, + currentCardTypeResolverProvider = currentCardTypeResolverProvider, + clickIntents = clickIntents, + ) + } + + private val cryptoCurrencyActionsConverter by lazy { + WalletCryptoCurrencyActionsConverter( + currentStateProvider = currentStateProvider, + clickIntents = clickIntents, ) } @@ -105,33 +131,31 @@ internal class WalletStateFactory( } } - fun getStateAfterContentRefreshing(): WalletState { - return currentStateProvider() - } + fun getStateAfterContentRefreshing(): WalletState = refreshStateConverter.convert(Unit) - fun getStateWithOpenBottomSheet(content: WalletBottomSheetConfig.BottomSheetContentConfig): WalletState { + fun getStateWithOpenWalletBottomSheet(content: WalletBottomSheetConfig.BottomSheetContentConfig): WalletState { return when (val state = currentStateProvider() as WalletState.ContentState) { is WalletMultiCurrencyState.Content -> state.copy( bottomSheetConfig = WalletBottomSheetConfig( isShow = true, - onDismissRequest = clickIntents::onBottomSheetDismiss, + onDismissRequest = clickIntents::onDismissBottomSheet, content = content, ), ) is WalletMultiCurrencyState.Locked -> state.copy( isBottomSheetShow = true, - onBottomSheetDismiss = clickIntents::onBottomSheetDismiss, + onBottomSheetDismiss = clickIntents::onDismissBottomSheet, ) is WalletSingleCurrencyState.Content -> state.copy( bottomSheetConfig = WalletBottomSheetConfig( isShow = true, - onDismissRequest = clickIntents::onBottomSheetDismiss, + onDismissRequest = clickIntents::onDismissBottomSheet, content = content, ), ) is WalletSingleCurrencyState.Locked -> state.copy( isBottomSheetShow = true, - onBottomSheetDismiss = clickIntents::onBottomSheetDismiss, + onBottomSheetDismiss = clickIntents::onDismissBottomSheet, ) } } @@ -149,6 +173,19 @@ internal class WalletStateFactory( } } + fun getStateWithTokenActionBottomSheet(tokenId: String): WalletState { + return when (val state = currentStateProvider() as WalletState.ContentState) { + is WalletMultiCurrencyState.Content -> state.copy( + tokenActionsBottomSheet = ActionsBottomSheetConfig( + isShow = true, + actions = tokenActionsProvider.provideActions(tokenId = tokenId), + onDismissRequest = clickIntents::onDismissActionsBottomSheet, + ), + ) + else -> state + } + } + fun getLoadingTxHistoryState(itemsCountEither: Either): WalletState { return loadingTransactionsStateConverter.convert(value = itemsCountEither) } @@ -159,53 +196,21 @@ internal class WalletStateFactory( return loadedTxHistoryConverter.convert(txHistoryEither) } - fun getLockedState(): WalletState { - val cardTypeResolver = currentCardTypeResolverProvider() - val state = requireNotNull(currentStateProvider() as? WalletState.ContentState) - return if (cardTypeResolver.isMultiwalletAllowed()) { - WalletMultiCurrencyState.Locked( - onBackClick = state.onBackClick, - topBarConfig = state.topBarConfig.copy( - onMoreClick = clickIntents::onUnlockWalletNotificationClick, - ), - walletsListConfig = state.walletsListConfig, - pullToRefreshConfig = state.pullToRefreshConfig, - onUnlockWalletsNotificationClick = clickIntents::onUnlockWalletNotificationClick, - onUnlockClick = clickIntents::onUnlockWalletClick, - onScanClick = clickIntents::onScanCardClick, - ) - } else { - WalletSingleCurrencyState.Locked( - onBackClick = state.onBackClick, - topBarConfig = state.topBarConfig.copy( - onMoreClick = clickIntents::onUnlockWalletNotificationClick, - ), - walletsListConfig = state.walletsListConfig, - pullToRefreshConfig = state.pullToRefreshConfig, - buttons = getButtons(), - onUnlockWalletsNotificationClick = clickIntents::onUnlockWalletNotificationClick, - onUnlockClick = clickIntents::onUnlockWalletClick, - onScanClick = clickIntents::onScanCardClick, - onExploreClick = clickIntents::onExploreClick, - ) - } - } + fun getLockedState(): WalletState = lockedConverter.convert(Unit) - // TODO: [REDACTED_JIRA] - private fun getButtons(): ImmutableList { - return persistentListOf( - WalletManageButton.Buy(), - WalletManageButton.Send(), - WalletManageButton.Receive(onClick = {}), - WalletManageButton.Exchange(), - WalletManageButton.Sell(), - WalletManageButton.CopyAddress(onClick = {}), + fun getSingleCurrencyLoadedBalanceState( + cryptoCurrencyEither: Either, + isRefreshing: Boolean, + ): WalletState { + return singleCurrencyLoadedBalanceConverter.convert( + value = WalletSingleCurrencyLoadedBalanceConverter.SingleCurrencyLoadedBalanceModel( + cryptoCurrencyEither = cryptoCurrencyEither, + isRefreshing = isRefreshing, + ), ) } - fun getSingleCurrencyLoadedBalanceState( - cryptoCurrencyEither: Either, - ): WalletState { - return singleCurrencyLoadedBalanceConverter.convert(cryptoCurrencyEither) + fun getSingleCurrencyManageButtonsState(actions: List): WalletState { + return cryptoCurrencyActionsConverter.convert(value = actions) } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/txhistory/WalletLoadedTxHistoryConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/txhistory/WalletLoadedTxHistoryConverter.kt index d5a8f7cdbd..e585ac71f5 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/txhistory/WalletLoadedTxHistoryConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/txhistory/WalletLoadedTxHistoryConverter.kt @@ -30,6 +30,7 @@ internal class WalletLoadedTxHistoryConverter( private val walletTxHistoryItemFlowConverter by lazy { WalletTxHistoryItemFlowConverter( + currentStateProvider = currentStateProvider, blockchain = currentCardTypeResolverProvider().getBlockchain(), clickIntents = clickIntents, ) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/txhistory/WalletLoadingTxHistoryConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/txhistory/WalletLoadingTxHistoryConverter.kt index ef8131d180..b8b5ab1a7d 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/txhistory/WalletLoadingTxHistoryConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/txhistory/WalletLoadingTxHistoryConverter.kt @@ -1,13 +1,16 @@ package com.tangem.feature.wallet.presentation.wallet.state.factory.txhistory +import androidx.paging.PagingData import arrow.core.Either import com.tangem.common.Provider -import com.tangem.core.ui.components.transactions.state.TxHistoryState +import com.tangem.core.ui.components.transactions.state.TransactionState +import com.tangem.core.ui.components.transactions.state.TxHistoryState.* import com.tangem.domain.txhistory.models.TxHistoryStateError import com.tangem.feature.wallet.presentation.wallet.state.WalletSingleCurrencyState import com.tangem.feature.wallet.presentation.wallet.state.WalletState import com.tangem.feature.wallet.presentation.wallet.viewmodels.WalletClickIntents import com.tangem.utils.converter.Converter +import kotlinx.coroutines.flow.update /** * Converter from loading tx history state to [WalletSingleCurrencyState.Content] @@ -20,31 +23,52 @@ import com.tangem.utils.converter.Converter internal class WalletLoadingTxHistoryConverter( private val currentStateProvider: Provider, private val clickIntents: WalletClickIntents, -) : Converter, WalletSingleCurrencyState.Content> { +) : Converter, WalletState> { - override fun convert(value: Either): WalletSingleCurrencyState.Content { + override fun convert(value: Either): WalletState { return value.fold(ifLeft = ::convertError, ifRight = ::convert) } - private fun convertError(error: TxHistoryStateError): WalletSingleCurrencyState.Content { - return requireNotNull(currentStateProvider() as? WalletSingleCurrencyState.Content).copy( - txHistoryState = when (error) { - is TxHistoryStateError.EmptyTxHistories -> { - TxHistoryState.Empty(onBuyClick = clickIntents::onBuyClick) - } - is TxHistoryStateError.DataError -> { - TxHistoryState.Error(onReloadClick = clickIntents::onReloadClick) - } - is TxHistoryStateError.TxHistoryNotImplemented -> { - TxHistoryState.NotSupported(onExploreClick = clickIntents::onExploreClick) - } - }, - ) + private fun convertError(error: TxHistoryStateError): WalletState { + val state = currentStateProvider() + + return if (state is WalletSingleCurrencyState.Content) { + state.copy( + txHistoryState = when (error) { + is TxHistoryStateError.EmptyTxHistories -> { + Empty(onBuyClick = clickIntents::onBuyClick) + } + is TxHistoryStateError.DataError -> { + Error(onReloadClick = clickIntents::onReloadClick) + } + is TxHistoryStateError.TxHistoryNotImplemented -> { + NotSupported(onExploreClick = clickIntents::onExploreClick) + } + }, + ) + } else { + state + } } private fun convert(value: Int): WalletSingleCurrencyState.Content { - return requireNotNull(currentStateProvider() as? WalletSingleCurrencyState.Content).copy( - txHistoryState = TxHistoryState.ContentWithLoadingItems(itemsCount = value), - ) + val state = requireNotNull(currentStateProvider() as? WalletSingleCurrencyState.Content) + val txHistoryContent = requireNotNull(state.txHistoryState as? Content) + + txHistoryContent.contentItems.update { + PagingData.from( + data = listOf(TxHistoryItemState.Title(onExploreClick = clickIntents::onExploreClick)) + + MutableList( + size = value, + init = { + TxHistoryItemState.Transaction( + state = TransactionState.Loading(it.toString()), + ) + }, + ), + ) + } + + return state } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/txhistory/WalletTxHistoryItemFlowConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/txhistory/WalletTxHistoryItemFlowConverter.kt index 1c4af33fac..5e4a05132f 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/txhistory/WalletTxHistoryItemFlowConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/txhistory/WalletTxHistoryItemFlowConverter.kt @@ -3,18 +3,27 @@ package com.tangem.feature.wallet.presentation.wallet.state.factory.txhistory import android.text.format.DateUtils import androidx.paging.* import com.tangem.blockchain.common.Blockchain +import com.tangem.common.Provider import com.tangem.core.ui.components.transactions.state.TransactionState import com.tangem.core.ui.components.transactions.state.TxHistoryState import com.tangem.core.ui.components.transactions.state.TxHistoryState.TxHistoryItemState +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.domain.txhistory.models.TxHistoryItem +import com.tangem.feature.wallet.impl.R +import com.tangem.feature.wallet.presentation.wallet.state.WalletSingleCurrencyState +import com.tangem.feature.wallet.presentation.wallet.state.WalletState import com.tangem.feature.wallet.presentation.wallet.viewmodels.WalletClickIntents import com.tangem.utils.converter.Converter import com.tangem.utils.extensions.isToday import com.tangem.utils.extensions.isYesterday import com.tangem.utils.toBriefAddressFormat -import com.tangem.utils.toFormattedCurrencyString +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.update import org.joda.time.DateTime import org.joda.time.DateTimeZone import org.joda.time.format.DateTimeFormatterBuilder @@ -24,12 +33,14 @@ import java.util.Locale /** * Convert from [Flow] of [TxHistoryItem] to [TxHistoryState] * - * @property blockchain blockchain of transactions history - * @property clickIntents screen click intents + * @property currentStateProvider current state provider + * @property blockchain blockchain of transactions history + * @property clickIntents screen click intents * [REDACTED_AUTHOR] */ internal class WalletTxHistoryItemFlowConverter( + private val currentStateProvider: Provider, private val blockchain: Blockchain, private val clickIntents: WalletClickIntents, ) : Converter>, TxHistoryState> { @@ -57,22 +68,30 @@ internal class WalletTxHistoryItemFlowConverter( } override fun convert(value: Flow>): TxHistoryState { - return TxHistoryState.Content( - items = value - .map { pagingData -> - pagingData + val state = currentStateProvider() as WalletSingleCurrencyState + val txHistoryContent = state.txHistoryState as TxHistoryState.Content + + // FIXME: TxHistoryRepository should send loading transactions + // [REDACTED_JIRA] + value + .onEach { txHistoryStatePagingData -> + txHistoryContent.contentItems.update { + txHistoryStatePagingData .map { item -> // [createTransactionState] returns timestamp without formatting TxHistoryItemState.Transaction(state = createTransactionState(item)) } .insertHeaderItem( terminalSeparatorType = TerminalSeparatorType.SOURCE_COMPLETE, - item = TxHistoryItemState.Title(onExploreClick = clickIntents::onExploreClick), + item = TxHistoryItemState.Title(clickIntents::onExploreClick), ) .insertGroupTitle() // method uses the raw timestamp .formatTransactionsTimestamp() // method formats the timestamp - }, - ) + } + } + .launchIn(CoroutineScope(Dispatchers.IO)) + + return txHistoryContent } private fun createTransactionState(item: TxHistoryItem): TransactionState { @@ -98,13 +117,13 @@ internal class WalletTxHistoryItemFlowConverter( return when (item.status) { TxHistoryItem.TxStatus.Confirmed -> TransactionState.Receive( txHash = item.txHash, - address = direction.from.toBriefAddressFormat(), + address = direction.extractAddress(), amount = item.amount.toCryptoCurrencyFormat(blockchain = blockchain), timestamp = item.getRawTimestamp(), ) TxHistoryItem.TxStatus.Unconfirmed -> TransactionState.Receiving( txHash = item.txHash, - address = direction.from.toBriefAddressFormat(), + address = direction.extractAddress(), amount = item.amount.toCryptoCurrencyFormat(blockchain = blockchain), timestamp = item.getRawTimestamp(), ) @@ -119,13 +138,13 @@ internal class WalletTxHistoryItemFlowConverter( return when (item.status) { TxHistoryItem.TxStatus.Confirmed -> TransactionState.Send( txHash = item.txHash, - address = direction.to.toBriefAddressFormat(), + address = direction.extractAddress(), amount = item.amount.toCryptoCurrencyFormat(blockchain = blockchain), timestamp = item.getRawTimestamp(), ) TxHistoryItem.TxStatus.Unconfirmed -> TransactionState.Sending( txHash = item.txHash, - address = direction.to.toBriefAddressFormat(), + address = direction.extractAddress(), amount = item.amount.toCryptoCurrencyFormat(blockchain = blockchain), timestamp = item.getRawTimestamp(), ) @@ -133,7 +152,11 @@ internal class WalletTxHistoryItemFlowConverter( } private fun BigDecimal.toCryptoCurrencyFormat(blockchain: Blockchain): String { - return toFormattedCurrencyString(currency = blockchain.currency, decimals = blockchain.decimals()) + return BigDecimalFormatter.formatCryptoAmount( + cryptoAmount = this, + cryptoCurrency = blockchain.currency, + decimals = blockchain.decimals(), + ) } private fun PagingData.insertGroupTitle(): PagingData { @@ -219,4 +242,9 @@ internal class WalletTxHistoryItemFlowConverter( DateTime(this.toLong(), DateTimeZone.getDefault()), ) } + + private fun TxHistoryItem.TransactionDirection.extractAddress(): TextReference = when (val addr = address) { + TxHistoryItem.Address.Multiple -> TextReference.Res(R.string.transaction_history_multiple_addresses) + is TxHistoryItem.Address.Single -> TextReference.Str(addr.rawAddress.toBriefAddressFormat()) + } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt index 4bc618e81d..fca76604c4 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt @@ -12,17 +12,21 @@ import androidx.compose.material3.* import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider import androidx.paging.compose.collectAsLazyPagingItems +import com.tangem.core.ui.components.PrimaryButton import com.tangem.core.ui.components.transactions.state.TxHistoryState import com.tangem.core.ui.res.TangemTheme +import com.tangem.feature.wallet.impl.R import com.tangem.feature.wallet.presentation.common.WalletPreviewData import com.tangem.feature.wallet.presentation.wallet.state.WalletMultiCurrencyState import com.tangem.feature.wallet.presentation.wallet.state.WalletSingleCurrencyState import com.tangem.feature.wallet.presentation.wallet.state.WalletState import com.tangem.feature.wallet.presentation.wallet.state.components.WalletTokensListState +import com.tangem.feature.wallet.presentation.wallet.ui.components.TokenActionsBottomSheet import com.tangem.feature.wallet.presentation.wallet.ui.components.WalletsList import com.tangem.feature.wallet.presentation.wallet.ui.components.common.* import com.tangem.feature.wallet.presentation.wallet.ui.components.multicurrency.organizeButton @@ -52,11 +56,7 @@ internal fun WalletScreen(state: WalletState) { private fun WalletContent(state: WalletState.ContentState) { val walletsListState = rememberLazyListState() - Scaffold( - topBar = { WalletTopBar(config = state.topBarConfig) }, - containerColor = TangemTheme.colors.background.secondary, - ) { scaffoldPaddings -> - + BaseScaffold(state = state) { scaffoldPaddings -> val movableItemModifier = Modifier.changeWalletAnimator(walletsListState) val pullRefreshState = rememberPullRefreshState( refreshing = state.pullToRefreshConfig.isRefreshing, @@ -69,9 +69,9 @@ private fun WalletContent(state: WalletState.ContentState) { .pullRefresh(pullRefreshState), ) { val txHistoryItems = if (state is WalletSingleCurrencyState && - state.txHistoryState is TxHistoryState.ContentState + state.txHistoryState is TxHistoryState.Content ) { - (state.txHistoryState as? TxHistoryState.ContentState)?.items?.collectAsLazyPagingItems() + (state.txHistoryState as? TxHistoryState.Content)?.contentItems?.collectAsLazyPagingItems() } else { null } @@ -84,7 +84,10 @@ private fun WalletContent(state: WalletState.ContentState) { LazyColumn( modifier = Modifier.fillMaxSize(), - contentPadding = PaddingValues(vertical = TangemTheme.dimens.spacing8), + contentPadding = PaddingValues( + top = TangemTheme.dimens.spacing8, + bottom = TangemTheme.dimens.spacing92, + ), horizontalAlignment = Alignment.CenterHorizontally, ) { item { @@ -122,12 +125,49 @@ private fun WalletContent(state: WalletState.ContentState) { } } - val bottomSheetConfig = state.bottomSheetConfig + WalletBottomSheets(state = state) + + WalletSideEffects(lazyListState = walletsListState, walletsListConfig = state.walletsListConfig) +} + +@Composable +private fun BaseScaffold(state: WalletState.ContentState, content: @Composable (PaddingValues) -> Unit) { + Scaffold( + topBar = { WalletTopBar(config = state.topBarConfig) }, + floatingActionButton = { + if (state is WalletMultiCurrencyState.Content) { + ManageTokensButton(onManageTokensClick = state.onManageTokensClick) + } + }, + floatingActionButtonPosition = FabPosition.Center, + containerColor = TangemTheme.colors.background.secondary, + content = content, + ) +} + +@Composable +private fun ManageTokensButton(onManageTokensClick: () -> Unit) { + PrimaryButton( + text = stringResource(id = R.string.main_manage_tokens), + onClick = onManageTokensClick, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = TangemTheme.dimens.spacing16), + ) +} + +@Composable +private fun WalletBottomSheets(state: WalletState) { + val bottomSheetConfig = (state as? WalletState.ContentState)?.bottomSheetConfig if (bottomSheetConfig != null && bottomSheetConfig.isShow) { WalletBottomSheet(config = bottomSheetConfig) } - WalletSideEffects(lazyListState = walletsListState, walletsListConfig = state.walletsListConfig) + (state as? WalletMultiCurrencyState.Content)?.let { multiCurrencyState -> + if (multiCurrencyState.tokenActionsBottomSheet != null && multiCurrencyState.tokenActionsBottomSheet.isShow) { + TokenActionsBottomSheet(config = multiCurrencyState.tokenActionsBottomSheet) + } + } } // region Preview diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/TokenActionsBottomSheet.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/TokenActionsBottomSheet.kt new file mode 100644 index 0000000000..a79d41bb23 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/TokenActionsBottomSheet.kt @@ -0,0 +1,76 @@ +package com.tangem.feature.wallet.presentation.wallet.ui.components + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column +import androidx.compose.material3.BottomSheetDefaults +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.ModalBottomSheet +import androidx.compose.material3.rememberModalBottomSheetState +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider +import com.tangem.core.ui.components.SimpleSettingsRow +import com.tangem.core.ui.res.TangemTheme +import com.tangem.feature.wallet.presentation.common.WalletPreviewData +import com.tangem.feature.wallet.presentation.wallet.state.ActionsBottomSheetConfig +import com.tangem.feature.wallet.presentation.wallet.state.TokenActionButtonConfig +import kotlinx.collections.immutable.ImmutableList + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +internal fun TokenActionsBottomSheet(config: ActionsBottomSheetConfig) { + ModalBottomSheet( + onDismissRequest = config.onDismissRequest, + sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true), + containerColor = TangemTheme.colors.background.primary, + dragHandle = { BottomSheetDefaults.DragHandle() }, + ) { + ActionsBottomSheetContent(config.actions) + } +} + +@Composable +private fun ActionsBottomSheetContent(actions: ImmutableList) { + Column( + modifier = Modifier.background(TangemTheme.colors.background.primary), + ) { + actions.forEach { action -> + SimpleSettingsRow( + title = action.text, + icon = action.iconResId, + enabled = action.enabled, + onItemsClick = action.onClick, + ) + } + } +} + +@Preview +@Composable +private fun ActionsBottomSheetContent_Light( + @PreviewParameter(ActionsBottomSheetContentConfigProvider::class) + config: ActionsBottomSheetConfig, +) { + TangemTheme(isDark = false) { + // Use preview of content because ModalBottomSheet isn't supported in Preview mode + ActionsBottomSheetContent(actions = config.actions) + } +} + +@Preview +@Composable +private fun ActionsBottomSheetContent_Dark( + @PreviewParameter(ActionsBottomSheetContentConfigProvider::class) + config: ActionsBottomSheetConfig, +) { + TangemTheme(isDark = false) { + // Use preview of content because ModalBottomSheet isn't supported in Preview mode + ActionsBottomSheetContent(actions = config.actions) + } +} + +private class ActionsBottomSheetContentConfigProvider : CollectionPreviewParameterProvider( + collection = listOf(WalletPreviewData.actionsBottomSheet), +) \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletCard.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletCard.kt index ccff84d714..494e36cee2 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 @@ -1,35 +1,55 @@ 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.foundation.Image +import androidx.compose.foundation.* +import androidx.compose.foundation.gestures.detectTapGestures +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.interaction.PressInteraction import androidx.compose.foundation.layout.* -import androidx.compose.material3.Icon -import androidx.compose.material3.Surface -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Delete +import androidx.compose.material.icons.outlined.Edit +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.hapticfeedback.HapticFeedbackType +import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.layout.onSizeChanged +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.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider +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 com.tangem.core.ui.components.FontSizeRange import com.tangem.core.ui.components.RectangleShimmer import com.tangem.core.ui.components.ResizableText +import com.tangem.core.ui.components.wallets.RenameWalletDialogContent +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.res.TangemColorPalette +import com.tangem.core.ui.res.TangemDimens import com.tangem.core.ui.res.TangemTheme -import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.feature.wallet.impl.R import com.tangem.feature.wallet.presentation.common.WalletPreviewData import com.tangem.feature.wallet.presentation.wallet.state.components.WalletCardState -private const val DOTS = "•••" - /** * Wallet card * @@ -40,135 +60,273 @@ private const val DOTS = "•••" */ @Composable internal fun WalletCard(state: WalletCardState, modifier: Modifier = Modifier) { + @Suppress("DestructuringDeclarationWithTooManyEntries") + CardContainer( + name = state.title, + onDeleteClick = { state.onDeleteClick(state.id) }, + onRenameClick = { state.onRenameClick(state.id, it) }, + modifier = modifier, + ) { + val (title, balance, additionalText, image) = createRefs() + + val contentVerticalMargin = TangemTheme.dimens.spacing12 + Title( + state = state, + modifier = Modifier.constrainAs(title) { + start.linkTo(parent.start) + top.linkTo(anchor = parent.top, margin = contentVerticalMargin) + end.linkTo(image.start) + width = Dimension.fillToConstraints + }, + ) + + val betweenContentMargin = TangemTheme.dimens.spacing8 + 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) + }, + ) + + AdditionalInfo( + state = state, + modifier = Modifier.constrainAs(additionalText) { + start.linkTo(parent.start) + bottom.linkTo(anchor = parent.bottom, margin = contentVerticalMargin) + }, + ) + + val imageWidth = TangemTheme.dimens.size120 + Image( + id = state.imageResId, + modifier = Modifier.constrainAs(image) { + centerVerticallyTo(parent) + top.linkTo(parent.top) + end.linkTo(parent.end) + height = Dimension.fillToConstraints + width = Dimension.value(imageWidth) + }, + ) + } +} + +@Composable +private fun CardContainer( + name: String, + onDeleteClick: () -> Unit, + onRenameClick: (String) -> Unit, + modifier: Modifier = Modifier, + content: @Composable (ConstraintLayoutScope.() -> Unit), +) { + var isMenuVisible by rememberSaveable { mutableStateOf(value = false) } + var pressOffset by remember { mutableStateOf(value = DpOffset.Zero) } + var itemHeight by remember { mutableStateOf(value = 0.dp) } + + val density = LocalDensity.current + val interactionSource = remember { MutableInteractionSource() } + val haptic = LocalHapticFeedback.current + Surface( - modifier = modifier.defaultMinSize(minHeight = TangemTheme.dimens.size108), + modifier = modifier + .defaultMinSize(minHeight = TangemTheme.dimens.size108) + .onSizeChanged { itemHeight = with(density) { it.height.toDp() } } + .clip(shape = TangemTheme.shapes.roundedCornersXMedium) + .indication(interactionSource = interactionSource, indication = LocalIndication.current) + .pointerInput(true) { + detectTapGestures( + onLongPress = { + haptic.performHapticFeedback(HapticFeedbackType.LongPress) + isMenuVisible = true + pressOffset = DpOffset(x = it.x.toDp(), y = it.y.toDp()) + }, + onPress = { + val press = PressInteraction.Press(it) + interactionSource.emit(press) + tryAwaitRelease() + interactionSource.emit(PressInteraction.Release(press)) + }, + ) + }, shape = TangemTheme.shapes.roundedCornersXMedium, color = TangemTheme.colors.background.primary, - onClick = state.onClick ?: {}, - enabled = state.onClick != null, ) { ConstraintLayout( modifier = Modifier .fillMaxWidth() .padding(horizontal = TangemTheme.dimens.spacing14), ) { - val (balanceBlock, imageItem) = createRefs() - Column( - modifier = Modifier.constrainAs(balanceBlock) { - centerVerticallyTo(parent) - start.linkTo(parent.start) - end.linkTo(imageItem.start) - width = Dimension.fillToConstraints - }, - verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8), - ) { - Title(state) - Balance(state) - AdditionalInfo(description = state.additionalInfo) - } + content() + } + } - val imageWidth = TangemTheme.dimens.size120 - WalletImage( - id = state.imageResId, - modifier = Modifier.constrainAs(imageItem) { - centerVerticallyTo(parent) - top.linkTo(parent.top) - end.linkTo(parent.end) - height = Dimension.fillToConstraints - width = Dimension.value(imageWidth) - }, + var isRenameWalletDialogVisible by rememberSaveable { mutableStateOf(value = false) } + + DropdownMenu( + expanded = isMenuVisible, + onDismissRequest = { isMenuVisible = false }, + modifier = Modifier.background(color = TangemTheme.colors.background.secondary), + offset = pressOffset.copy(y = pressOffset.y - itemHeight), + ) { + MenuItem( + textResId = R.string.common_rename, + imageVector = Icons.Outlined.Edit, + onClick = { + isMenuVisible = false + isRenameWalletDialogVisible = true + }, + ) + MenuItem(textResId = R.string.common_delete, imageVector = Icons.Outlined.Delete, onClick = onDeleteClick) + } + + if (isRenameWalletDialogVisible) { + RenameWalletDialogContent( + name = name, + onConfirm = { + onRenameClick(it) + isRenameWalletDialogVisible = false + }, + onDismiss = { isRenameWalletDialogVisible = false }, + ) + } +} + +@Composable +private fun MenuItem(@StringRes textResId: Int, imageVector: ImageVector, onClick: () -> Unit) { + DropdownMenuItem( + text = { Text(text = stringResource(id = textResId), style = TangemTheme.typography.subtitle2) }, + modifier = Modifier.background(color = TangemTheme.colors.background.secondary), + trailingIcon = { Icon(imageVector = imageVector, contentDescription = null) }, + onClick = onClick, + colors = MenuDefaults.itemColors( + textColor = TangemTheme.colors.text.primary1, + trailingIconColor = TangemColorPalette.Dark6, + ), + ) +} + +@Composable +private fun Title(state: WalletCardState, modifier: Modifier = Modifier) { + Row( + modifier = modifier, + horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing4), + ) { + TitleText(title = state.title) + + AnimatedVisibility(visible = state is WalletCardState.HiddenContent, label = "Update the hidden icon") { + Icon( + modifier = Modifier.size(size = TangemTheme.dimens.size20), + painter = painterResource(id = R.drawable.ic_eye_off_24), + contentDescription = null, + tint = TangemTheme.colors.icon.informative, ) } } } -@OptIn(ExperimentalAnimationApi::class) @Composable -private fun Title(state: WalletCardState) { - AnimatedContent(targetState = state, label = "Update the title") { - when (it) { - is WalletCardState.HiddenContent -> { - Row(horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing4)) { - Text( - text = it.title, - color = TangemTheme.colors.text.tertiary, - style = TangemTheme.typography.body2, - maxLines = 1, - ) - Icon( - modifier = Modifier.size(size = TangemTheme.dimens.size20), - painter = painterResource(id = R.drawable.ic_eye_off_24), - contentDescription = null, - tint = TangemTheme.colors.icon.informative, - ) - } - } - is WalletCardState.Content, - is WalletCardState.Error, - is WalletCardState.Loading, - -> { - Text( - text = it.title, - color = TangemTheme.colors.text.tertiary, - style = TangemTheme.typography.body2, - maxLines = 1, - ) - } - } - } +private fun TitleText(title: String) { + Text( + text = title, + color = TangemTheme.colors.text.tertiary, + style = TangemTheme.typography.body2, + maxLines = 1, + ) } @OptIn(ExperimentalAnimationApi::class) @Composable -private fun Balance(state: WalletCardState) { - AnimatedContent(targetState = state, label = "Update the balance") { - when (it) { +private fun Balance(state: WalletCardState, modifier: Modifier = Modifier) { + AnimatedContent( + targetState = state, + label = "Update the balance", + modifier = modifier, + ) { walletCardState -> + when (walletCardState) { is WalletCardState.Content -> { ResizableText( - text = it.balance, - color = TangemTheme.colors.text.primary1, - style = TangemTheme.typography.h2, + text = walletCardState.balance, fontSizeRange = FontSizeRange(min = 16.sp, max = TangemTheme.typography.h2.fontSize), modifier = Modifier.defaultMinSize(minHeight = TangemTheme.dimens.size32), + color = TangemTheme.colors.text.primary1, + style = TangemTheme.typography.h2, ) } + is WalletCardState.HiddenContent -> NonContentBalanceText(text = WalletCardState.HIDDEN_BALANCE_TEXT) + is WalletCardState.Error -> NonContentBalanceText(text = WalletCardState.EMPTY_BALANCE_TEXT) is WalletCardState.Loading -> { - RectangleShimmer( - modifier = Modifier.size( - width = TangemTheme.dimens.size102, - height = TangemTheme.dimens.size24, - ), - ) + RectangleShimmer(modifier = Modifier.nonContentBalanceSize(TangemTheme.dimens)) } - is WalletCardState.HiddenContent -> { - Text( - text = DOTS, - color = TangemTheme.colors.text.primary1, - style = TangemTheme.typography.h2, - ) - } - is WalletCardState.Error -> { - Text( - text = BigDecimalFormatter.EMPTY_BALANCE_SIGN, - color = TangemTheme.colors.text.primary1, - style = TangemTheme.typography.h2, - ) + is WalletCardState.LockedContent -> { + LockedContent(modifier = Modifier.nonContentBalanceSize(TangemTheme.dimens)) } } } } @Composable -private fun AdditionalInfo(description: String) { +private fun NonContentBalanceText(text: TextReference) { Text( - text = description, + text = text.resolveReference(), + color = TangemTheme.colors.text.primary1, + style = TangemTheme.typography.h2, + ) +} + +private fun Modifier.nonContentBalanceSize(dimens: TangemDimens): Modifier { + return size(width = dimens.size102, height = dimens.size32) +} + +@OptIn(ExperimentalAnimationApi::class) +@Composable +private fun AdditionalInfo(state: WalletCardState, modifier: Modifier = Modifier) { + AnimatedContent( + targetState = state.additionalInfo, + label = "Update the additional text", + modifier = modifier, + ) { additionalInfo -> + if (additionalInfo != null) { + AdditionalInfoText(text = additionalInfo) + } else { + when (state) { + is WalletCardState.Loading -> { + RectangleShimmer(modifier = Modifier.nonContentAdditionalInfoSize(TangemTheme.dimens)) + } + is WalletCardState.LockedContent -> { + LockedContent(modifier = Modifier.nonContentAdditionalInfoSize(TangemTheme.dimens)) + } + else -> Unit + } + } + } +} + +@Composable +private fun AdditionalInfoText(text: TextReference) { + Text( + text = text.resolveReference(), color = TangemTheme.colors.text.disabled, style = TangemTheme.typography.caption, ) } +private fun Modifier.nonContentAdditionalInfoSize(dimens: TangemDimens): Modifier { + return size(width = dimens.size84, height = dimens.size16) +} + @Composable -private fun WalletImage(@DrawableRes id: Int?, modifier: Modifier = Modifier) { +private fun LockedContent(modifier: Modifier = Modifier) { + Box( + modifier = modifier.background( + color = TangemTheme.colors.field.primary, + shape = RoundedCornerShape(TangemTheme.dimens.radius6), + ), + ) +} + +@Composable +private fun Image(@DrawableRes id: Int?, modifier: Modifier = Modifier) { AnimatedVisibility(visible = id != null, modifier = modifier) { Image( painter = painterResource(id = requireNotNull(id)), @@ -180,11 +338,14 @@ private fun WalletImage(@DrawableRes id: Int?, modifier: Modifier = Modifier) { // region Preview -@Preview(widthDp = 360, heightDp = 360) +@Preview @Composable -private fun Preview_WalletCard_LightTheme(@PreviewParameter(WalletCardStateProvider::class) state: WalletCardState) { +private fun Preview_WalletCard_LightTheme( + @PreviewParameter(WalletCardStateProvider::class) + state: WalletCardState, +) { TangemTheme(isDark = false) { - WalletCard(state = state, modifier = Modifier.fillMaxWidth()) + WalletCard(state = state) } } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/CryptoCurrencyStatusToTokenItemConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/CryptoCurrencyStatusToTokenItemConverter.kt index 4c9c49ed14..b19dcf646a 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/CryptoCurrencyStatusToTokenItemConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/CryptoCurrencyStatusToTokenItemConverter.kt @@ -1,13 +1,12 @@ package com.tangem.feature.wallet.presentation.wallet.utils -import androidx.annotation.DrawableRes import com.tangem.common.Provider import com.tangem.core.ui.components.marketprice.PriceChangeConfig +import com.tangem.core.ui.extensions.iconResId +import com.tangem.core.ui.extensions.networkBadgeIconResId import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus -import com.tangem.domain.tokens.models.CryptoCurrency -import com.tangem.feature.wallet.impl.R import com.tangem.feature.wallet.presentation.common.state.TokenItemState import com.tangem.feature.wallet.presentation.wallet.viewmodels.WalletClickIntents import com.tangem.utils.converter.Converter @@ -19,18 +18,6 @@ internal class CryptoCurrencyStatusToTokenItemConverter( private val clickIntents: WalletClickIntents, ) : Converter { - private val CryptoCurrencyStatus.networkIconResId: Int? - @DrawableRes get() { - // TODO: [REDACTED_JIRA] - return if (currency is CryptoCurrency.Coin) null else R.drawable.img_eth_22 - } - - private val CryptoCurrencyStatus.tokenIconResId: Int - @DrawableRes get() { - // TODO: [REDACTED_JIRA] - return R.drawable.img_eth_22 - } - override fun convert(value: CryptoCurrencyStatus): TokenItemState { return when (value.value) { is CryptoCurrencyStatus.Loading -> TokenItemState.Loading(id = value.currency.id.value) @@ -51,19 +38,21 @@ internal class CryptoCurrencyStatusToTokenItemConverter( id = currency.id.value, name = currency.name, tokenIconUrl = currency.iconUrl, - tokenIconResId = this.tokenIconResId, - networkIconResId = this.networkIconResId, + tokenIconResId = currency.iconResId, + networkBadgeIconResId = currency.networkBadgeIconResId, amount = getFormattedAmount(), - hasPending = value.hasTransactionsInProgress, + hasPending = value.hasCurrentNetworkTransactions, tokenOptions = if (isWalletContentHidden) { TokenItemState.TokenOptionsState.Hidden(getPriceChangeConfig()) } else { TokenItemState.TokenOptionsState.Visible( fiatAmount = getFormattedFiatAmount(), - priceChange = getPriceChangeConfig(), + config = getPriceChangeConfig(), ) }, - onClick = { clickIntents.onTokenClick(currency) }, + isTestnet = currency.network.isTestnet, + onItemClick = { clickIntents.onTokenItemClick(currency) }, + onItemLongClick = { clickIntents.onTokenItemLongClick(currency) }, ) } @@ -84,8 +73,8 @@ internal class CryptoCurrencyStatusToTokenItemConverter( id = currency.id.value, name = currency.name, tokenIconUrl = currency.iconUrl, - tokenIconResId = this.tokenIconResId, - networkIconResId = this.networkIconResId, + tokenIconResId = currency.iconResId, + networkBadgeIconResId = currency.networkBadgeIconResId, ) private fun CryptoCurrencyStatus.getPriceChangeConfig(): PriceChangeConfig { diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/FiatBalanceToWalletCardConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/FiatBalanceToWalletCardConverter.kt index 8d48f9b5ec..05340d37ed 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/FiatBalanceToWalletCardConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/FiatBalanceToWalletCardConverter.kt @@ -4,7 +4,8 @@ import com.tangem.common.Provider import com.tangem.core.ui.utils.BigDecimalFormatter.formatFiatAmount import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.common.CardTypesResolver -import com.tangem.domain.tokens.model.TokenList +import com.tangem.domain.tokens.model.TokenList.FiatBalance +import com.tangem.domain.wallets.models.UserWallet import com.tangem.feature.wallet.presentation.wallet.domain.WalletAdditionalInfoFactory import com.tangem.feature.wallet.presentation.wallet.state.components.WalletCardState import com.tangem.utils.converter.Converter @@ -13,38 +14,65 @@ internal class FiatBalanceToWalletCardConverter( private val currentState: WalletCardState, private val cardTypeResolverProvider: Provider, private val appCurrencyProvider: Provider, - private val isLockedState: Boolean, + private val currentWalletProvider: Provider, private val isWalletContentHidden: Boolean, -) : Converter { +) : Converter { - override fun convert(value: TokenList.FiatBalance): WalletCardState { - val additionalInfo = WalletAdditionalInfoFactory.resolve( - cardTypesResolver = cardTypeResolverProvider(), - isLocked = isLockedState, - ) + override fun convert(value: FiatBalance): WalletCardState { return when (value) { - is TokenList.FiatBalance.Loading -> with(currentState) { - WalletCardState.Loading(id, title, additionalInfo, imageResId, onClick) - } - is TokenList.FiatBalance.Failed -> with(currentState) { - WalletCardState.Error(id, title, additionalInfo, imageResId, onClick) - } - is TokenList.FiatBalance.Loaded -> with(currentState) { - if (isWalletContentHidden) { - WalletCardState.HiddenContent(id, title, additionalInfo, imageResId, onClick) - } else { - val appCurrency = appCurrencyProvider() + is FiatBalance.Loading -> currentState.toLoadingWalletCardState() + is FiatBalance.Failed -> currentState.toErrorWalletCardState() + is FiatBalance.Loaded -> value.convertToWalletCardState() + } + } - WalletCardState.Content( - id = id, - title = title, - additionalInfo = additionalInfo, - imageResId = imageResId, - onClick = onClick, - balance = formatFiatAmount(value.amount, appCurrency.code, appCurrency.symbol), - ) - } - } + private fun WalletCardState.toLoadingWalletCardState(): WalletCardState { + return WalletCardState.Loading(id, title, additionalInfo, imageResId, onRenameClick, onDeleteClick) + } + + private fun WalletCardState.toErrorWalletCardState(): WalletCardState { + return WalletCardState.Error( + id = id, + title = title, + imageResId = imageResId, + onDeleteClick = onDeleteClick, + onRenameClick = onRenameClick, + additionalInfo = WalletAdditionalInfoFactory.resolve( + cardTypesResolver = cardTypeResolverProvider(), + wallet = currentWalletProvider(), + ), + ) + } + + private fun FiatBalance.Loaded.convertToWalletCardState(): WalletCardState { + return if (isWalletContentHidden) { + WalletCardState.HiddenContent( + id = currentState.id, + title = currentState.title, + additionalInfo = currentState.additionalInfo ?: WalletCardState.HIDDEN_BALANCE_TEXT, + imageResId = currentState.imageResId, + onRenameClick = currentState.onRenameClick, + onDeleteClick = currentState.onDeleteClick, + ) + } else { + val appCurrency = appCurrencyProvider() + + WalletCardState.Content( + id = currentState.id, + title = currentState.title, + additionalInfo = WalletAdditionalInfoFactory.resolve( + cardTypesResolver = cardTypeResolverProvider(), + wallet = currentWalletProvider(), + ), + imageResId = currentState.imageResId, + onRenameClick = currentState.onRenameClick, + onDeleteClick = currentState.onDeleteClick, + balance = formatFiatAmount( + fiatAmount = this.amount, + fiatCurrencyCode = appCurrency.code, + fiatCurrencySymbol = appCurrency.symbol, + ), + ) } } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/TokenListErrorConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/TokenListErrorConverter.kt index ac32cdb876..82709f83a7 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/TokenListErrorConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/TokenListErrorConverter.kt @@ -3,6 +3,7 @@ package com.tangem.feature.wallet.presentation.wallet.utils import com.tangem.common.Provider import com.tangem.domain.tokens.error.TokenListError import com.tangem.feature.wallet.presentation.wallet.state.WalletMultiCurrencyState +import com.tangem.feature.wallet.presentation.wallet.state.WalletSingleCurrencyState import com.tangem.feature.wallet.presentation.wallet.state.WalletState import com.tangem.feature.wallet.presentation.wallet.state.components.WalletTokensListState import com.tangem.utils.converter.Converter @@ -10,11 +11,23 @@ import kotlinx.collections.immutable.persistentListOf internal class TokenListErrorConverter( private val currentStateProvider: Provider, -) : Converter { +) : Converter { - override fun convert(value: TokenListError): WalletMultiCurrencyState.Content { - return requireNotNull(currentStateProvider() as? WalletMultiCurrencyState.Content).copy( - tokensListState = WalletTokensListState.Content(items = persistentListOf(), onOrganizeTokensClick = null), - ) + override fun convert(value: TokenListError): WalletState { + return when (val state = currentStateProvider()) { + is WalletMultiCurrencyState.Content -> { + state.copy( + tokensListState = WalletTokensListState.Content( + items = persistentListOf(), + onOrganizeTokensClick = null, + ), + ) + } + is WalletMultiCurrencyState.Locked, + is WalletSingleCurrencyState.Content, + is WalletSingleCurrencyState.Locked, + is WalletState.Initial, + -> state + } } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/TokenListToWalletStateConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/TokenListToWalletStateConverter.kt index fb21502fe6..35aa99598f 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/TokenListToWalletStateConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/TokenListToWalletStateConverter.kt @@ -4,10 +4,9 @@ import com.tangem.common.Provider import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.common.CardTypesResolver import com.tangem.domain.tokens.model.TokenList -import com.tangem.feature.wallet.presentation.common.state.TokenItemState +import com.tangem.domain.wallets.models.UserWallet import com.tangem.feature.wallet.presentation.wallet.state.WalletMultiCurrencyState import com.tangem.feature.wallet.presentation.wallet.state.WalletState -import com.tangem.feature.wallet.presentation.wallet.state.components.WalletTokensListState import com.tangem.feature.wallet.presentation.wallet.state.components.WalletsListConfig import com.tangem.feature.wallet.presentation.wallet.utils.TokenListToWalletStateConverter.TokensListModel import com.tangem.feature.wallet.presentation.wallet.viewmodels.WalletClickIntents @@ -18,7 +17,7 @@ import kotlinx.collections.immutable.toPersistentList internal class TokenListToWalletStateConverter( private val currentStateProvider: Provider, private val cardTypeResolverProvider: Provider, - private val isLockedWalletProvider: Provider, + private val currentWalletProvider: Provider, private val appCurrencyProvider: Provider, private val isWalletContentHidden: Boolean, clickIntents: WalletClickIntents, @@ -35,7 +34,7 @@ internal class TokenListToWalletStateConverter( return state.copy( walletsListConfig = state.updateSelectedWallet(fiatBalance = value.tokenList.totalFiatBalance), pullToRefreshConfig = if (value.isRefreshing) { - state.pullToRefreshConfig.copy(isRefreshing = state.getRefreshingStatus()) + state.pullToRefreshConfig.copy(isRefreshing = getRefreshingStatus(tokenList = value.tokenList)) } else { state.pullToRefreshConfig }, @@ -48,7 +47,7 @@ internal class TokenListToWalletStateConverter( val selectedWalletCard = walletsListConfig.wallets[selectedWalletIndex] val converter = FiatBalanceToWalletCardConverter( currentState = selectedWalletCard, - isLockedState = isLockedWalletProvider(), + currentWalletProvider = currentWalletProvider, cardTypeResolverProvider = cardTypeResolverProvider, appCurrencyProvider = appCurrencyProvider, isWalletContentHidden = isWalletContentHidden, @@ -60,17 +59,8 @@ internal class TokenListToWalletStateConverter( ) } - private fun WalletState.getRefreshingStatus(): Boolean { - return if (this is WalletMultiCurrencyState.Content && - this.tokensListState is WalletTokensListState.ContentState - ) { - tokensListState.items.any { tokensListItemState -> - tokensListItemState is WalletTokensListState.TokensListItemState.Token && - tokensListItemState.state is TokenItemState.Loading - } - } else { - false - } + private fun getRefreshingStatus(tokenList: TokenList): Boolean { + return tokenList.totalFiatBalance is TokenList.FiatBalance.Loading } data class TokensListModel(val tokenList: TokenList, val isRefreshing: Boolean) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletClickIntents.kt index 2dcc2ddc06..c3ba714c76 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletClickIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletClickIntents.kt @@ -1,8 +1,11 @@ package com.tangem.feature.wallet.presentation.wallet.viewmodels +import com.tangem.core.ui.components.transactions.intents.TxHistoryClickIntents import com.tangem.domain.tokens.models.CryptoCurrency +import com.tangem.domain.wallets.models.UserWalletId -internal interface WalletClickIntents { +@Suppress("TooManyFunctions") +internal interface WalletClickIntents : TxHistoryClickIntents { fun onBackClick() @@ -28,17 +31,27 @@ internal interface WalletClickIntents { fun onOrganizeTokensClick() - fun onBuyClick() - - fun onReloadClick() - - fun onExploreClick() - fun onUnlockWalletClick() fun onUnlockWalletNotificationClick() - fun onBottomSheetDismiss() + fun onDismissBottomSheet() - fun onTokenClick(currency: CryptoCurrency) + fun onTokenItemClick(currency: CryptoCurrency) + + fun onTokenItemLongClick(currency: CryptoCurrency) + + fun onDismissActionsBottomSheet() + + fun onRenameClick(userWalletId: UserWalletId, name: String) + + fun onDeleteClick(userWalletId: UserWalletId) + + fun onSendClick() + + fun onReceiveClick() + + fun onSellClick() + + fun onManageTokensClick() } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletNotificationsListFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletNotificationsListFactory.kt index 11beecade5..fb70c489e4 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletNotificationsListFactory.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletNotificationsListFactory.kt @@ -1,15 +1,10 @@ package com.tangem.feature.wallet.presentation.wallet.viewmodels -import com.tangem.common.Provider import com.tangem.domain.common.CardTypesResolver import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.model.NetworkGroup import com.tangem.domain.tokens.model.TokenList -import com.tangem.feature.wallet.presentation.common.state.TokenItemState -import com.tangem.feature.wallet.presentation.wallet.state.WalletMultiCurrencyState -import com.tangem.feature.wallet.presentation.wallet.state.WalletState import com.tangem.feature.wallet.presentation.wallet.state.components.WalletNotification -import com.tangem.feature.wallet.presentation.wallet.state.components.WalletTokensListState import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.flow.Flow @@ -26,7 +21,6 @@ import kotlinx.coroutines.flow.flow [REDACTED_AUTHOR] */ internal class WalletNotificationsListFactory( - private val currentStateProvider: Provider, private val wasCardScannedCallback: suspend (String) -> Boolean, private val isUserAlreadyRateAppCallback: suspend () -> Boolean, private val isDemoCardCallback: (String) -> Boolean, @@ -56,7 +50,7 @@ internal class WalletNotificationsListFactory( add(element = WalletNotification.DemoCard) } - if (hasUnreachableNetworks()) { + if (hasUnreachableNetworks(tokenList)) { add(element = WalletNotification.UnreachableNetworks) } @@ -112,15 +106,22 @@ internal class WalletNotificationsListFactory( } } - private fun hasUnreachableNetworks(): Boolean { - val isUnreachableState = { item: WalletTokensListState.TokensListItemState -> - (item as? WalletTokensListState.TokensListItemState.Token)?.state is TokenItemState.Unreachable - } - - return currentStateProvider().let { state -> - state is WalletMultiCurrencyState.Content && - state.tokensListState is WalletTokensListState.ContentState && - state.tokensListState.items.any(isUnreachableState) + private fun hasUnreachableNetworks(tokenList: TokenList?): Boolean { + return when (tokenList) { + is TokenList.GroupedByNetwork -> { + tokenList.groups + .flatMap(NetworkGroup::currencies) + .map(CryptoCurrencyStatus::value) + .any { it is CryptoCurrencyStatus.Unreachable } + } + is TokenList.Ungrouped -> { + tokenList.currencies + .map(CryptoCurrencyStatus::value) + .any { it is CryptoCurrencyStatus.Unreachable } + } + is TokenList.NotInitialized, + null, + -> false } } 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 f81cd1f969..509377d260 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt @@ -8,8 +8,8 @@ import com.tangem.blockchain.common.derivation.DerivationStyle import com.tangem.common.Provider import com.tangem.common.doOnFailure import com.tangem.common.doOnSuccess +import com.tangem.core.navigation.AppScreen import com.tangem.core.ui.components.marketprice.MarketPriceBlockState -import com.tangem.core.ui.components.transactions.state.TxHistoryState import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.card.* @@ -17,9 +17,15 @@ import com.tangem.domain.common.CardTypesResolver import com.tangem.domain.common.util.cardTypesResolver import com.tangem.domain.common.util.derivationStyleProvider import com.tangem.domain.demo.IsDemoCardUseCase +import com.tangem.domain.redux.ReduxStateHolder +import com.tangem.domain.settings.CanUseBiometryUseCase import com.tangem.domain.settings.IsUserAlreadyRateAppUseCase -import com.tangem.domain.tokens.GetPrimaryCurrencyUseCase +import com.tangem.domain.settings.ShouldShowSaveWalletScreenUseCase +import com.tangem.domain.tokens.GetCryptoCurrencyActionsUseCase +import com.tangem.domain.tokens.GetPrimaryCurrencyStatusUpdatesUseCase import com.tangem.domain.tokens.GetTokenListUseCase +import com.tangem.domain.tokens.legacy.TradeCryptoAction +import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.model.TokenList import com.tangem.domain.tokens.models.CryptoCurrency import com.tangem.domain.tokens.models.Network @@ -43,6 +49,7 @@ import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.JobHolder import com.tangem.utils.coroutines.saveIn import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch import javax.inject.Inject @@ -60,11 +67,13 @@ internal class WalletViewModel @Inject constructor( private val saveWalletUseCase: SaveWalletUseCase, private val getSelectedWalletUseCase: GetSelectedWalletUseCase, private val selectWalletUseCase: SelectWalletUseCase, + private val updateWalletUseCase: UpdateWalletUseCase, + private val deleteWalletUseCase: DeleteWalletUseCase, private val getBiometricsStatusUseCase: GetBiometricsStatusUseCase, private val setAccessCodeRequestPolicyUseCase: SetAccessCodeRequestPolicyUseCase, private val getAccessCodeSavingStatusUseCase: GetAccessCodeSavingStatusUseCase, private val getTokenListUseCase: GetTokenListUseCase, - private val getPrimaryCurrencyUseCase: GetPrimaryCurrencyUseCase, + private val getPrimaryCurrencyUseCase: GetPrimaryCurrencyStatusUpdatesUseCase, private val getCardWasScannedUseCase: GetCardWasScannedUseCase, private val isUserAlreadyRateAppUseCase: IsUserAlreadyRateAppUseCase, private val isDemoCardUseCase: IsDemoCardUseCase, @@ -74,6 +83,11 @@ internal class WalletViewModel @Inject constructor( private val getExploreUrlUseCase: GetExploreUrlUseCase, private val unlockWalletsUseCase: UnlockWalletsUseCase, private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, + private val getCryptoCurrencyActionsUseCase: GetCryptoCurrencyActionsUseCase, + private val shouldShowSaveWalletScreenUseCase: ShouldShowSaveWalletScreenUseCase, + private val canUseBiometryUseCase: CanUseBiometryUseCase, + private val shouldSaveUserWalletsUseCase: ShouldSaveUserWalletsUseCase, + private val reduxStateHolder: ReduxStateHolder, private val dispatchers: CoroutineDispatcherProvider, ) : ViewModel(), DefaultLifecycleObserver, WalletClickIntents { @@ -83,7 +97,6 @@ internal class WalletViewModel @Inject constructor( private val selectedAppCurrencyFlow: StateFlow = createSelectedAppCurrencyFlow() private val notificationsListFactory = WalletNotificationsListFactory( - currentStateProvider = Provider { uiState }, wasCardScannedCallback = getCardWasScannedUseCase::invoke, isUserAlreadyRateAppCallback = isUserAlreadyRateAppUseCase::invoke, isDemoCardCallback = isDemoCardUseCase::invoke, @@ -97,8 +110,8 @@ internal class WalletViewModel @Inject constructor( index = requireNotNull(uiState as? WalletState.ContentState).walletsListConfig.selectedWalletIndex, ) }, - isLockedWalletProvider = Provider { - wallets[requireNotNull(uiState as? WalletState.ContentState).walletsListConfig.selectedWalletIndex].isLocked + currentWalletProvider = Provider { + wallets[requireNotNull(uiState as? WalletState.ContentState).walletsListConfig.selectedWalletIndex] }, appCurrencyProvider = Provider(selectedAppCurrencyFlow::value), clickIntents = this, @@ -108,12 +121,22 @@ internal class WalletViewModel @Inject constructor( var uiState: WalletState by uiStateHolder(initialState = stateFactory.getInitialState()) private var wallets: List by Delegates.notNull() + private var cryptoCurrencyStatus: CryptoCurrencyStatus? = null private val tokensJobHolder = JobHolder() private val marketPriceJobHolder = JobHolder() + private val buttonsJobHolder = JobHolder() private val notificationsJobHolder = JobHolder() override fun onCreate(owner: LifecycleOwner) { + viewModelScope.launch(dispatchers.main) { + delay(timeMillis = 1_800) + + if (router.isWalletLastScreen() && shouldShowSaveWalletScreenUseCase() && canUseBiometryUseCase()) { + router.openSaveUserWalletScreen() + } + } + getWalletsUseCase() .flowWithLifecycle(owner.lifecycle) .distinctUntilChanged() @@ -148,7 +171,7 @@ internal class WalletViewModel @Inject constructor( when { getWallet(index).isLocked -> uiState = stateFactory.getLockedState() cardTypeResolver.isMultiwalletAllowed() -> updateMultiCurrencyContent(index, isRefreshing) - !cardTypeResolver.isMultiwalletAllowed() -> updateSingleCurrencyContent(index) + !cardTypeResolver.isMultiwalletAllowed() -> updateSingleCurrencyContent(index, isRefreshing) } } @@ -175,13 +198,15 @@ internal class WalletViewModel @Inject constructor( .saveIn(tokensJobHolder) } - private fun updateSingleCurrencyContent(index: Int) { + private fun updateSingleCurrencyContent(index: Int, isRefreshing: Boolean) { val wallet = getWallet(index) + val blockchain = getCardTypeResolver(index).getBlockchain() + updateButtons(userWalletId = wallet.walletId, currencyId = blockchain.id) updateTxHistory( - blockchain = getCardTypeResolver(index).getBlockchain(), + blockchain = blockchain, derivationStyle = wallet.scanResponse.derivationStyleProvider.getDerivationStyle(), ) - updateMarketPrice(userWalletId = wallet.walletId) + updateMarketPrice(userWalletId = wallet.walletId, isRefreshing = isRefreshing) updateNotifications(index) } @@ -209,15 +234,32 @@ internal class WalletViewModel @Inject constructor( } } - private fun updateMarketPrice(userWalletId: UserWalletId) { + // It also update wallet balance + private fun updateMarketPrice(userWalletId: UserWalletId, isRefreshing: Boolean) { getPrimaryCurrencyUseCase(userWalletId = userWalletId) .distinctUntilChanged() - .onEach { uiState = stateFactory.getSingleCurrencyLoadedBalanceState(cryptoCurrencyEither = it) } + .onEach { either -> + uiState = stateFactory.getSingleCurrencyLoadedBalanceState( + cryptoCurrencyEither = either, + isRefreshing = isRefreshing, + ) + + either.onRight { status -> cryptoCurrencyStatus = status } + } .flowOn(dispatchers.io) .launchIn(viewModelScope) .saveIn(marketPriceJobHolder) } + private fun updateButtons(userWalletId: UserWalletId, currencyId: String) { + getCryptoCurrencyActionsUseCase(userWalletId = userWalletId, tokenId = currencyId) + .distinctUntilChanged() + .onEach { uiState = stateFactory.getSingleCurrencyManageButtonsState(actions = it.states) } + .flowOn(dispatchers.io) + .launchIn(viewModelScope) + .saveIn(buttonsJobHolder) + } + private fun updateNotifications(index: Int, tokenList: TokenList? = null) { notificationsListFactory.create( cardTypesResolver = getCardTypeResolver(index = index), @@ -252,7 +294,11 @@ internal class WalletViewModel @Inject constructor( private fun getCardTypeResolver(index: Int): CardTypesResolver = getWallet(index).scanResponse.cardTypesResolver - override fun onBackClick() = router.popBackStack() + override fun onBackClick() { + viewModelScope.launch(dispatchers.main) { + router.popBackStack(screen = if (shouldSaveUserWalletsUseCase()) AppScreen.Welcome else AppScreen.Home) + } + } override fun onScanCardClick() { val prevRequestPolicyStatus = getBiometricsStatusUseCase() @@ -289,7 +335,7 @@ internal class WalletViewModel @Inject constructor( override fun onBackupCardClick() = router.openOnboardingScreen() override fun onCriticalWarningAlreadySignedHashesClick() { - uiState = stateFactory.getStateWithOpenBottomSheet( + uiState = stateFactory.getStateWithOpenWalletBottomSheet( content = WalletBottomSheetConfig.BottomSheetContentConfig.CriticalWarningAlreadySignedHashes( onOkClick = {}, onCancelClick = {}, @@ -302,7 +348,7 @@ internal class WalletViewModel @Inject constructor( } override fun onLikeTangemAppClick() { - uiState = stateFactory.getStateWithOpenBottomSheet( + uiState = stateFactory.getStateWithOpenWalletBottomSheet( content = WalletBottomSheetConfig.BottomSheetContentConfig.LikeTangemApp( onRateTheAppClick = ::onRateTheAppClick, onShareClick = ::onShareClick, @@ -331,12 +377,16 @@ internal class WalletViewModel @Inject constructor( */ tokensJobHolder.update(job = null) marketPriceJobHolder.update(job = null) + buttonsJobHolder.update(job = null) notificationsJobHolder.update(job = null) val cacheState = WalletStateCache.getState(userWalletId = state.walletsListConfig.wallets[index].id) if (cacheState != null) { uiState = if (cacheState is WalletState.ContentState) { - cacheState.copySealed(walletsListConfig = state.walletsListConfig.copy(selectedWalletIndex = index)) + cacheState.copySealed( + walletsListConfig = state.walletsListConfig.copy(selectedWalletIndex = index), + pullToRefreshConfig = state.pullToRefreshConfig.copy(isRefreshing = false), + ) } else { cacheState } @@ -366,19 +416,26 @@ internal class WalletViewModel @Inject constructor( tokensListState is WalletTokensListState.Loading || hasLoadingTokens } is WalletSingleCurrencyState -> { - txHistoryState is TxHistoryState.Loading || marketPriceBlockState is MarketPriceBlockState.Loading + this is WalletSingleCurrencyState.Content && marketPriceBlockState is MarketPriceBlockState.Loading } is WalletState.Initial -> false } } override fun onRefreshSwipe() { - uiState = stateFactory.getStateAfterContentRefreshing() + if (uiState is WalletState.Initial || uiState is WalletLockedState) return - updateContentItems( - index = requireNotNull(uiState as? WalletState.ContentState).walletsListConfig.selectedWalletIndex, - isRefreshing = true, - ) + viewModelScope.launch(dispatchers.io) { + uiState = stateFactory.getStateAfterContentRefreshing() + + // TODO: [REDACTED_JIRA] + delay(timeMillis = 500) + + updateContentItems( + index = requireNotNull(uiState as? WalletState.ContentState).walletsListConfig.selectedWalletIndex, + isRefreshing = true, + ) + } } override fun onOrganizeTokensClick() { @@ -390,13 +447,47 @@ internal class WalletViewModel @Inject constructor( } override fun onBuyClick() { + val state = uiState as? WalletState.ContentState ?: return + val status = cryptoCurrencyStatus ?: return + val wallet = getWallet(index = state.walletsListConfig.selectedWalletIndex) + + reduxStateHolder.dispatch( + TradeCryptoAction.New.Buy( + userWallet = wallet, + cryptoCurrencyStatus = status, + appCurrencyCode = selectedAppCurrencyFlow.value.code, + ), + ) + } + + override fun onSendClick() { + reduxStateHolder.dispatch(TradeCryptoAction.New.Send) + } + + override fun onReceiveClick() { // TODO: [REDACTED_JIRA] } + override fun onSellClick() { + val status = cryptoCurrencyStatus ?: return + + reduxStateHolder.dispatch( + TradeCryptoAction.New.Sell( + cryptoCurrencyStatus = status, + appCurrencyCode = selectedAppCurrencyFlow.value.code, + ), + ) + } + + override fun onManageTokensClick() { + router.openManageTokensScreen() + } + override fun onReloadClick() { uiState = stateFactory.getStateAfterContentRefreshing() updateSingleCurrencyContent( index = requireNotNull(uiState as? WalletState.ContentState).walletsListConfig.selectedWalletIndex, + isRefreshing = true, ) } @@ -427,7 +518,7 @@ internal class WalletViewModel @Inject constructor( "Impossible to unlock wallet if state isn't WalletLockedState" } - uiState = stateFactory.getStateWithOpenBottomSheet( + uiState = stateFactory.getStateWithOpenWalletBottomSheet( content = when (state) { is WalletMultiCurrencyState.Locked -> state.bottomSheetConfig.content is WalletSingleCurrencyState.Locked -> state.bottomSheetConfig.content @@ -435,12 +526,29 @@ internal class WalletViewModel @Inject constructor( ) } - override fun onBottomSheetDismiss() { - uiState = stateFactory.getStateWithClosedBottomSheet() + override fun onTokenItemClick(currency: CryptoCurrency) { + router.openTokenDetails(currency = currency) } - override fun onTokenClick(currency: CryptoCurrency) { - router.openTokenDetails(currency = currency) + override fun onTokenItemLongClick(currency: CryptoCurrency) { + uiState = stateFactory.getStateWithTokenActionBottomSheet( + tokenId = currency.id.value, + ) + } + + override fun onRenameClick(userWalletId: UserWalletId, name: String) { + viewModelScope.launch(dispatchers.io) { + updateWalletUseCase(userWalletId = userWalletId, update = { it.copy(name) }) + } + } + + override fun onDeleteClick(userWalletId: UserWalletId) { + viewModelScope.launch(dispatchers.io) { + val either = deleteWalletUseCase(userWalletId) + + val state = requireNotNull(uiState as? WalletState.ContentState) + if (state.walletsListConfig.wallets.size <= 1 && either.isRight()) onBackClick() + } } private fun createSelectedAppCurrencyFlow(): StateFlow { @@ -454,4 +562,18 @@ internal class WalletViewModel @Inject constructor( initialValue = AppCurrency.Default, ) } + + override fun onDismissBottomSheet() { + uiState = stateFactory.getStateWithClosedBottomSheet() + } + + override fun onDismissActionsBottomSheet() { + (uiState as? WalletMultiCurrencyState.Content)?.let { state -> + uiState = state.copy( + tokenActionsBottomSheet = state.tokenActionsBottomSheet?.copy( + isShow = false, + ), + ) + } + } } \ No newline at end of file diff --git a/gradle/dependencies.toml b/gradle/dependencies.toml index f9144bb4cf..eacf2e16fc 100644 --- a/gradle/dependencies.toml +++ b/gradle/dependencies.toml @@ -30,7 +30,7 @@ compose-material3 = "1.1.0" compose-constraint = "1.0.1" compose-navigation = "2.5.3" compose-accompanist = "0.30.1" -compose-paging = "1.0.0-alpha18" +compose-paging = "3.2.0" compose-reorderable = "0.9.6" # endregion Compose @@ -80,9 +80,9 @@ okHttp-prettyLogging = "3.1.0" # endregion Other libraries # region Tangem -tangemBlockchainSdk = "release-app_4.10-341" +tangemBlockchainSdk = "release-app_4.11-344" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds -tangemCardSdk = "release-app_4.10-292" +tangemCardSdk = "release-app_4.11-294" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds # endregion Tangem diff --git a/settings.gradle.kts b/settings.gradle.kts index 33ccfc81d6..b2c3126321 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -110,14 +110,18 @@ include(":domain:txhistory") include(":domain:txhistory:models") include(":domain:app-currency") include(":domain:app-currency:models") +include(":domain:app-theme") +include(":domain:app-theme:models") // endregion Domain modules // region Data modules +include(":data:app-currency") +include(":data:app-theme") include(":data:common") include(":data:card") include(":data:tokens") include(":data:source:preferences") include(":data:settings") include(":data:txhistory") -include(":data:app-currency") +include(":data:wallets") // endregion Data modules \ No newline at end of file