From e1ef1c4b8a4c666e85c371bbcf9b06ce32ea68e8 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 17 Jun 2024 13:40:04 +0400 Subject: [PATCH] Updated on 2026-08-14 --- .../tangem/tap/routing/utils/ChildFactory.kt | 9 ++ .../com/tangem/common/routing/AppRoute.kt | 5 + domain/wallets/build.gradle.kts | 6 +- .../wallets/usecase/GetUserWalletUseCase.kt | 14 +++ .../details/utils/UserWalletsFetcher.kt | 7 +- .../impl/DefaultRenameWalletComponent.kt | 92 +++++++++++++++ .../impl/DefaultWalletSettingsComponent.kt | 74 ++++++++++++ .../walletsettings/di/ComponentModule.kt | 26 +++++ .../feature/walletsettings/di/ModelModule.kt | 20 ++++ .../model/WalletSettingsModel.kt | 107 ++++++++++++++++++ 10 files changed, 352 insertions(+), 8 deletions(-) create mode 100644 features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/component/impl/DefaultRenameWalletComponent.kt create mode 100644 features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/component/impl/DefaultWalletSettingsComponent.kt create mode 100644 features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/di/ComponentModule.kt create mode 100644 features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/di/ModelModule.kt create mode 100644 features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/model/WalletSettingsModel.kt diff --git a/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt b/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt index dc76ad9db0..48df79739b 100644 --- a/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt +++ b/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt @@ -5,6 +5,7 @@ import com.tangem.core.decompose.context.AppComponentContext import com.tangem.feature.qrscanning.QrScanningRouter import com.tangem.feature.referral.ReferralFragment import com.tangem.feature.swap.presentation.SwapFragment +import com.tangem.feature.walletsettings.component.WalletSettingsComponent import com.tangem.features.details.DetailsFeatureToggles import com.tangem.features.details.component.DetailsComponent import com.tangem.features.disclaimer.api.DisclaimerRouter @@ -44,6 +45,7 @@ import javax.inject.Inject @Suppress("LongParameterList") internal class ChildFactory @Inject constructor( private val detailsComponentFactory: DetailsComponent.Factory, + private val walletSettingsComponentFactory: WalletSettingsComponent.Factory, private val sendRouter: SendRouter, private val tokenDetailsRouter: TokenDetailsRouter, private val walletRouter: WalletRouter, @@ -160,6 +162,13 @@ internal class ChildFactory @Inject constructor( is AppRoute.PushNotification -> { route.asFragmentChild(Provider { pushNotificationRouter.entryFragment() }) } + is AppRoute.WalletSettings -> { + route.asComponentChild( + contextProvider = contextProvider(route, contextFactory), + params = WalletSettingsComponent.Params(route.userWalletId), + componentFactory = walletSettingsComponentFactory, + ) + } } } diff --git a/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt b/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt index 2b6831a967..8bd5b8a2c1 100644 --- a/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt +++ b/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt @@ -200,4 +200,9 @@ sealed class AppRoute(val path: String) : Route { @Serializable data object PushNotification : AppRoute(path = "/push_notification") + + @Serializable + data class WalletSettings( + val userWalletId: UserWalletId, + ) : AppRoute(path = "/wallet_settings/${userWalletId.stringValue}") } \ No newline at end of file diff --git a/domain/wallets/build.gradle.kts b/domain/wallets/build.gradle.kts index bd175a01fa..9972fdc9c7 100644 --- a/domain/wallets/build.gradle.kts +++ b/domain/wallets/build.gradle.kts @@ -16,6 +16,7 @@ dependencies { // endregion // region Domain modules + api(projects.domain.core) implementation(projects.domain.legacy) implementation(projects.libs.blockchainSdk) implementation(projects.domain.models) @@ -28,9 +29,4 @@ dependencies { implementation(deps.tangem.blockchain) // android-library implementation(deps.tangem.card.core) // endregion - - // region Other libraries - implementation(deps.arrow.core) - implementation(deps.kotlin.coroutines) - // endregion } \ No newline at end of file diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetUserWalletUseCase.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetUserWalletUseCase.kt index 9323fd5423..a8b14fe269 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetUserWalletUseCase.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetUserWalletUseCase.kt @@ -1,12 +1,17 @@ package com.tangem.domain.wallets.usecase import arrow.core.Either +import arrow.core.left import arrow.core.raise.either import arrow.core.raise.ensureNotNull +import arrow.core.right +import com.tangem.domain.core.utils.EitherFlow import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.wallets.models.GetUserWalletError import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWalletId +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.transformLatest class GetUserWalletUseCase(private val userWalletsListManager: UserWalletsListManager) { @@ -17,4 +22,13 @@ class GetUserWalletUseCase(private val userWalletsListManager: UserWalletsListMa raise(GetUserWalletError.UserWalletNotFound) } } + + @OptIn(ExperimentalCoroutinesApi::class) + fun invokeFlow(userWalletId: UserWalletId): EitherFlow { + return userWalletsListManager.userWallets.transformLatest { userWallets -> + userWallets.firstOrNull { it.walletId == userWalletId } + ?.let { emit(it.right()) } + ?: emit(GetUserWalletError.UserWalletNotFound.left()) + } + } } \ No newline at end of file diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/UserWalletsFetcher.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/UserWalletsFetcher.kt index 73b54461fb..e606030df5 100644 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/UserWalletsFetcher.kt +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/UserWalletsFetcher.kt @@ -1,10 +1,11 @@ package com.tangem.features.details.utils import arrow.core.Either +import com.tangem.common.routing.AppRoute import com.tangem.core.decompose.di.ComponentScoped +import com.tangem.core.decompose.navigation.Router import com.tangem.core.decompose.ui.UiMessageSender import com.tangem.core.ui.extensions.resourceReference -import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.message.SnackbarMessage import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.error.SelectedAppCurrencyError @@ -30,6 +31,7 @@ internal class UserWalletsFetcher @Inject constructor( private val getWalletsUseCase: GetWalletsUseCase, private val getWalletTotalBalanceUseCase: GetWalletTotalBalanceUseCase, private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, + private val router: Router, private val messageSender: UiMessageSender, ) { @@ -82,8 +84,7 @@ internal class UserWalletsFetcher @Inject constructor( } private fun navigateToWalletSettings(userWalletId: UserWalletId) { - val message = stringReference("Wallet settings have not yet been implemented: $userWalletId") - messageSender.send(SnackbarMessage(message)) + router.push(AppRoute.WalletSettings(userWalletId)) } sealed class Error { diff --git a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/component/impl/DefaultRenameWalletComponent.kt b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/component/impl/DefaultRenameWalletComponent.kt new file mode 100644 index 0000000000..a4c28c649c --- /dev/null +++ b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/component/impl/DefaultRenameWalletComponent.kt @@ -0,0 +1,92 @@ +package com.tangem.feature.walletsettings.component.impl + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.text.input.TextFieldValue +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.wrappedList +import com.tangem.core.ui.message.SnackbarMessage +import com.tangem.domain.wallets.models.UpdateWalletError +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.domain.wallets.usecase.RenameWalletUseCase +import com.tangem.feature.walletsettings.component.RenameWalletComponent +import com.tangem.feature.walletsettings.entity.RenameWalletUM +import com.tangem.feature.walletsettings.impl.R +import com.tangem.feature.walletsettings.ui.RenameWalletDialog +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch + +internal class DefaultRenameWalletComponent @AssistedInject constructor( + @Assisted context: AppComponentContext, + @Assisted params: RenameWalletComponent.Params, + private val renameWalletUseCase: RenameWalletUseCase, +) : RenameWalletComponent, AppComponentContext by context { + + private val userWalletId = params.userWalletId + private val currentWalletName = params.currentName + + override val doOnDismiss: () -> Unit = params.onDismiss + + private val stateFlow: MutableStateFlow = MutableStateFlow( + value = RenameWalletUM( + walletNameValue = TextFieldValue(text = params.currentName), + isNameCorrect = false, + updateValue = ::updateValue, + onConfirm = { renameWallet(params.userWalletId) }, + ), + ) + + @Composable + override fun Dialog() { + val model by stateFlow.collectAsStateWithLifecycle() + + RenameWalletDialog( + model = model, + onDismiss = doOnDismiss, + ) + } + + private fun updateValue(value: TextFieldValue) { + stateFlow.update { + it.copy( + walletNameValue = value, + isNameCorrect = value.text.isNotBlank() && value.text != currentWalletName, + ) + } + } + + private fun renameWallet(userWalletId: UserWalletId) = componentScope.launch { + val newName = stateFlow.value.walletNameValue + val maybeError = renameWalletUseCase(userWalletId, newName.text).leftOrNull() + + if (maybeError != null) { + val message = when (maybeError) { + UpdateWalletError.DataError -> resourceReference( + id = R.string.common_unknown_error, + ) + UpdateWalletError.NameAlreadyExists -> resourceReference( + id = R.string.user_wallet_list_rename_popup_error_already_exists, + formatArgs = wrappedList(newName), + ) + } + + messageSender.send(message = SnackbarMessage(message)) + } + + doOnDismiss() + } + + @AssistedFactory + interface Factory : RenameWalletComponent.Factory { + override fun create( + context: AppComponentContext, + params: RenameWalletComponent.Params, + ): DefaultRenameWalletComponent + } +} \ No newline at end of file diff --git a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/component/impl/DefaultWalletSettingsComponent.kt b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/component/impl/DefaultWalletSettingsComponent.kt new file mode 100644 index 0000000000..39ed1d65a5 --- /dev/null +++ b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/component/impl/DefaultWalletSettingsComponent.kt @@ -0,0 +1,74 @@ +package com.tangem.feature.walletsettings.component.impl + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.arkivanov.decompose.ComponentContext +import com.arkivanov.decompose.extensions.compose.jetpack.subscribeAsState +import com.arkivanov.decompose.router.slot.childSlot +import com.arkivanov.decompose.router.slot.dismiss +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.decompose.context.childByContext +import com.tangem.core.decompose.model.getOrCreateModel +import com.tangem.core.ui.decompose.ComposableDialogComponent +import com.tangem.feature.walletsettings.component.RenameWalletComponent +import com.tangem.feature.walletsettings.component.WalletSettingsComponent +import com.tangem.feature.walletsettings.entity.DialogConfig +import com.tangem.feature.walletsettings.model.WalletSettingsModel +import com.tangem.feature.walletsettings.ui.WalletSettingsScreen +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject + +internal class DefaultWalletSettingsComponent @AssistedInject constructor( + @Assisted context: AppComponentContext, + @Assisted params: WalletSettingsComponent.Params, + private val renameWalletComponentFactory: RenameWalletComponent.Factory, +) : WalletSettingsComponent, AppComponentContext by context { + + private val model: WalletSettingsModel = getOrCreateModel(params) + + private val dialog = childSlot( + source = model.dialogNavigation, + serializer = DialogConfig.serializer(), + handleBackButton = true, + childFactory = ::dialogChild, + ) + + @Composable + override fun Content(modifier: Modifier) { + val state by model.state.collectAsStateWithLifecycle() + val dialog by dialog.subscribeAsState() + + WalletSettingsScreen( + modifier = modifier, + state = state, + dialog = { dialog.child?.instance?.Dialog() }, + ) + } + + private fun dialogChild( + dialogConfig: DialogConfig, + componentContext: ComponentContext, + ): ComposableDialogComponent = when (dialogConfig) { + is DialogConfig.RenameWallet -> { + renameWalletComponentFactory.create( + context = childByContext(componentContext), + params = RenameWalletComponent.Params( + userWalletId = dialogConfig.userWalletId, + currentName = dialogConfig.currentName, + onDismiss = model.dialogNavigation::dismiss, + ), + ) + } + } + + @AssistedFactory + interface Factory : WalletSettingsComponent.Factory { + override fun create( + context: AppComponentContext, + params: WalletSettingsComponent.Params, + ): DefaultWalletSettingsComponent + } +} \ No newline at end of file diff --git a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/di/ComponentModule.kt b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/di/ComponentModule.kt new file mode 100644 index 0000000000..90ff4e1255 --- /dev/null +++ b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/di/ComponentModule.kt @@ -0,0 +1,26 @@ +package com.tangem.feature.walletsettings.di + +import com.tangem.feature.walletsettings.component.RenameWalletComponent +import com.tangem.feature.walletsettings.component.WalletSettingsComponent +import com.tangem.feature.walletsettings.component.impl.DefaultRenameWalletComponent +import com.tangem.feature.walletsettings.component.impl.DefaultWalletSettingsComponent +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal interface ComponentModule { + + @Binds + @Singleton + fun bindWalletSettingsComponentFactory( + factory: DefaultWalletSettingsComponent.Factory, + ): WalletSettingsComponent.Factory + + @Binds + @Singleton + fun bindRenameWalletComponentFactory(factory: DefaultRenameWalletComponent.Factory): RenameWalletComponent.Factory +} \ No newline at end of file diff --git a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/di/ModelModule.kt b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/di/ModelModule.kt new file mode 100644 index 0000000000..f7c93024a5 --- /dev/null +++ b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/di/ModelModule.kt @@ -0,0 +1,20 @@ +package com.tangem.feature.walletsettings.di + +import com.tangem.core.decompose.di.DecomposeComponent +import com.tangem.core.decompose.model.Model +import com.tangem.feature.walletsettings.model.WalletSettingsModel +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.multibindings.ClassKey +import dagger.multibindings.IntoMap + +@Module +@InstallIn(DecomposeComponent::class) +internal interface ModelModule { + + @Binds + @IntoMap + @ClassKey(WalletSettingsModel::class) + fun provideWalletSettingsModel(model: WalletSettingsModel): Model +} \ No newline at end of file diff --git a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/model/WalletSettingsModel.kt b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/model/WalletSettingsModel.kt new file mode 100644 index 0000000000..7926ce7f87 --- /dev/null +++ b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/model/WalletSettingsModel.kt @@ -0,0 +1,107 @@ +package com.tangem.feature.walletsettings.model + +import arrow.core.getOrElse +import com.arkivanov.decompose.router.slot.SlotNavigation +import com.arkivanov.decompose.router.slot.activate +import com.tangem.common.routing.AppRoute +import com.tangem.core.decompose.di.ComponentScoped +import com.tangem.core.decompose.model.Model +import com.tangem.core.decompose.model.ParamsContainer +import com.tangem.core.decompose.navigation.Router +import com.tangem.core.decompose.ui.UiMessageSender +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.message.SnackbarMessage +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.domain.wallets.usecase.DeleteWalletUseCase +import com.tangem.domain.wallets.usecase.GetUserWalletUseCase +import com.tangem.feature.walletsettings.component.WalletSettingsComponent +import com.tangem.feature.walletsettings.entity.DialogConfig +import com.tangem.feature.walletsettings.entity.WalletSettingsItemUM +import com.tangem.feature.walletsettings.entity.WalletSettingsUM +import com.tangem.feature.walletsettings.impl.R +import com.tangem.feature.walletsettings.utils.ItemsBuilder +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.collections.immutable.PersistentList +import kotlinx.collections.immutable.persistentListOf +import kotlinx.coroutines.flow.* +import kotlinx.coroutines.launch +import javax.inject.Inject + +@Suppress("LongParameterList") +@ComponentScoped +internal class WalletSettingsModel @Inject constructor( + private val router: Router, + private val messageSender: UiMessageSender, + private val getWalletUseCase: GetUserWalletUseCase, + private val deleteWalletUseCase: DeleteWalletUseCase, + private val itemsBuilder: ItemsBuilder, + private val paramsContainer: ParamsContainer, + override val dispatchers: CoroutineDispatcherProvider, +) : Model() { + + val params: WalletSettingsComponent.Params = paramsContainer.require() + val dialogNavigation = SlotNavigation() + + val state: MutableStateFlow = MutableStateFlow( + value = WalletSettingsUM( + popBack = router::pop, + items = persistentListOf(), + ), + ) + + init { + getWalletUseCase.invokeFlow(params.userWalletId) + .distinctUntilChanged() + .onEach { maybeWallet -> + val wallet = maybeWallet.getOrElse { error -> + error( + """ + Failed to get user wallet + |- User wallet ID: $params + |- Cause: $error + """.trimIndent(), + ) + } + + state.update { value -> + value.copy(items = buildItems(wallet, dialogNavigation)) + } + } + .launchIn(modelScope) + } + + private fun buildItems( + userWallet: UserWallet, + dialogNavigation: SlotNavigation, + ): PersistentList = itemsBuilder.buildItems( + walletName = userWallet.name, + renameWallet = { openRenameWalletDialog(userWallet, dialogNavigation) }, + forgetWallet = { forgetWallet(userWallet.walletId) }, + ) + + private fun openRenameWalletDialog(userWallet: UserWallet, dialogNavigation: SlotNavigation) { + val config = DialogConfig.RenameWallet( + userWalletId = userWallet.walletId, + currentName = userWallet.name, + ) + + dialogNavigation.activate(config) + } + + private fun forgetWallet(userWalletId: UserWalletId) = modelScope.launch { + val hasUserWallets = deleteWalletUseCase(userWalletId).getOrElse { + messageSender.send( + message = SnackbarMessage(resourceReference(R.string.common_unknown_error)), + ) + + return@launch + } + + if (hasUserWallets) { + router.pop() + } else { + router.replaceAll(AppRoute.Home) + } + } +} \ No newline at end of file