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 index d079febfc3..051fef6074 100644 --- 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 @@ -19,9 +19,11 @@ import com.tangem.feature.walletsettings.ui.RenameWalletDialog import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject +import kotlinx.coroutines.NonCancellable import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import timber.log.Timber internal class DefaultRenameWalletComponent @AssistedInject constructor( @@ -76,25 +78,32 @@ internal class DefaultRenameWalletComponent @AssistedInject constructor( } } - private fun renameWallet(userWalletId: UserWalletId) = componentScope.launch { - val newName = stateFlow.value.walletNameValue - val maybeError = renameWalletUseCase(userWalletId, newName.text).leftOrNull() + private fun renameWallet(userWalletId: UserWalletId) { + componentScope.launch { + withContext(NonCancellable) { + val newName = stateFlow.value.walletNameValue - if (maybeError != null) { - Timber.e("Unable to rename wallet: $maybeError") - - val message = when (maybeError) { - is UpdateWalletError.DataError -> resourceReference(id = R.string.common_unknown_error) - is UpdateWalletError.NameAlreadyExists -> resourceReference( - id = R.string.user_wallet_list_rename_popup_error_already_exists, - formatArgs = wrappedList(newName.text), - ) + renameWalletUseCase(userWalletId, newName.text) + .onLeft { + Timber.e("Unable to rename wallet: $it") + showRenameWalletError(error = it, updatedName = newName.text) + } } - - messageSender.send(message = SnackbarMessage(message)) - } else { - dismiss() } + + dismiss() + } + + private fun showRenameWalletError(error: UpdateWalletError, updatedName: String) { + val message = when (error) { + is UpdateWalletError.DataError -> resourceReference(id = R.string.common_unknown_error) + is UpdateWalletError.NameAlreadyExists -> resourceReference( + id = R.string.user_wallet_list_rename_popup_error_already_exists, + formatArgs = wrappedList(updatedName), + ) + } + + messageSender.send(message = SnackbarMessage(message)) } @AssistedFactory diff --git a/features/wallet/impl/build.gradle.kts b/features/wallet/impl/build.gradle.kts index 3749a2f4a1..3d27a12451 100644 --- a/features/wallet/impl/build.gradle.kts +++ b/features/wallet/impl/build.gradle.kts @@ -3,6 +3,7 @@ plugins { alias(deps.plugins.kotlin.android) alias(deps.plugins.kotlin.kapt) alias(deps.plugins.hilt.android) + alias(deps.plugins.kotlin.serialization) id("configuration") } @@ -33,6 +34,7 @@ dependencies { /** Other libraries */ implementation(deps.arrow.core) + implementation(deps.decompose.ext.compose) implementation(deps.googlePlay.review) implementation(deps.jodatime) implementation(deps.kotlin.immutable.collections) @@ -95,17 +97,18 @@ dependencies { implementation(projects.features.onboarding) /** Feature Apis */ - implementation(projects.features.wallet.api) - implementation(projects.features.tokendetails.api) - implementation(projects.features.send.api) - implementation(projects.features.tester.api) - implementation(projects.features.manageTokens.api) implementation(projects.features.details.api) - implementation(projects.features.pushNotifications.api) + implementation(projects.features.manageTokens.api) implementation(projects.features.markets.api) - implementation(projects.features.onramp.api) implementation(projects.features.onboardingV2.api) + implementation(projects.features.onramp.api) + implementation(projects.features.pushNotifications.api) + implementation(projects.features.send.api) implementation(projects.features.swap.api) + implementation(projects.features.tester.api) + implementation(projects.features.tokendetails.api) + implementation(projects.features.wallet.api) + implementation(projects.features.walletSettings.api) /** Common modules */ implementation(projects.common) 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 5124c8df29..4d110cae5d 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,29 +1,18 @@ package com.tangem.feature.wallet.di -import com.tangem.common.routing.AppRouter -import com.tangem.core.navigation.url.UrlOpener -import com.tangem.domain.redux.ReduxStateHolder import com.tangem.feature.wallet.presentation.router.DefaultWalletRouter -import com.tangem.features.onboarding.v2.OnboardingV2FeatureToggles import com.tangem.features.wallet.navigation.WalletRouter +import dagger.Binds import dagger.Module -import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.android.components.ActivityComponent import dagger.hilt.android.scopes.ActivityScoped @Module @InstallIn(ActivityComponent::class) -internal object WalletRouterModule { +internal interface WalletRouterModule { - @Provides + @Binds @ActivityScoped - fun provideWalletRouter( - appRouter: AppRouter, - urlOpener: UrlOpener, - reduxStateHolder: ReduxStateHolder, - onboardingV2FeatureToggles: OnboardingV2FeatureToggles, - ): WalletRouter { - return DefaultWalletRouter(appRouter, urlOpener, reduxStateHolder, onboardingV2FeatureToggles) - } + fun bindsWalletRouter(defaultWalletRouter: DefaultWalletRouter): WalletRouter } \ 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 11842f32eb..d8a119f840 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 @@ -14,7 +14,6 @@ import com.tangem.core.decompose.ui.UiMessageSender import com.tangem.core.ui.UiDependencies import com.tangem.core.ui.screen.ComposeFragment import com.tangem.feature.wallet.presentation.router.InnerWalletRouter -import com.tangem.features.markets.entry.MarketsEntryComponent import com.tangem.features.wallet.navigation.WalletRouter import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.hilt.android.AndroidEntryPoint @@ -35,9 +34,6 @@ internal class WalletFragment : ComposeFragment() { @Inject internal lateinit var walletRouter: WalletRouter - @Inject - internal lateinit var marketsEntryComponentFactory: MarketsEntryComponent.Factory - @Inject internal lateinit var coroutineDispatcherProvider: CoroutineDispatcherProvider @@ -51,8 +47,6 @@ internal class WalletFragment : ComposeFragment() { @GlobalUiMessageSender internal lateinit var messageSender: UiMessageSender - private lateinit var marketsEntryComponent: MarketsEntryComponent - private val _walletRouter: InnerWalletRouter get() = requireNotNull(walletRouter as? InnerWalletRouter) { "_walletRouter should be instance of InnerWalletRouter" @@ -61,15 +55,15 @@ internal class WalletFragment : ComposeFragment() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - val appContext = DefaultAppComponentContext( - componentContext = defaultComponentContext(requireActivity().onBackPressedDispatcher), - messageSender = messageSender, - dispatchers = coroutineDispatcherProvider, - hiltComponentBuilder = componentBuilder, - replaceRouter = appRouter.asRouter(), + _walletRouter.initializeResources( + appComponentContext = DefaultAppComponentContext( + componentContext = defaultComponentContext(requireActivity().onBackPressedDispatcher), + messageSender = messageSender, + dispatchers = coroutineDispatcherProvider, + hiltComponentBuilder = componentBuilder, + replaceRouter = appRouter.asRouter(), + ), ) - - marketsEntryComponent = marketsEntryComponentFactory.create(appContext) } @Composable @@ -80,7 +74,6 @@ internal class WalletFragment : ComposeFragment() { requireActivity().finish() } }, - marketsEntryComponent = marketsEntryComponent, ) } 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 6780b00e47..384435d2a8 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 @@ -3,9 +3,9 @@ package com.tangem.feature.wallet.presentation.router import android.annotation.SuppressLint import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue -import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.fragment.app.Fragment import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.LocalLifecycleOwner import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation.NavHostController import androidx.navigation.NavType @@ -13,10 +13,20 @@ import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController import androidx.navigation.navArgument +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.SlotNavigation +import com.arkivanov.decompose.router.slot.childSlot +import com.arkivanov.decompose.router.slot.dismiss +import com.arkivanov.decompose.value.Value import com.tangem.common.routing.AppRoute import com.tangem.common.routing.AppRoute.ManageTokens.Source import com.tangem.common.routing.AppRouter +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.decompose.context.childByContext import com.tangem.core.navigation.url.UrlOpener +import com.tangem.core.ui.decompose.ComposableDialogComponent import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.redux.ReduxStateHolder import com.tangem.domain.redux.StateDialog @@ -25,27 +35,53 @@ import com.tangem.domain.wallets.models.UserWalletId import com.tangem.feature.wallet.presentation.WalletFragment import com.tangem.feature.wallet.presentation.organizetokens.OrganizeTokensScreen import com.tangem.feature.wallet.presentation.organizetokens.OrganizeTokensViewModel +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletDialogConfig import com.tangem.feature.wallet.presentation.wallet.ui.WalletScreen import com.tangem.feature.wallet.presentation.wallet.viewmodels.WalletViewModel +import com.tangem.feature.walletsettings.component.RenameWalletComponent import com.tangem.features.markets.entry.MarketsEntryComponent import com.tangem.features.onboarding.v2.OnboardingV2FeatureToggles +import javax.inject.Inject import kotlin.properties.Delegates /** Default implementation of wallet feature router */ -internal class DefaultWalletRouter( +internal class DefaultWalletRouter @Inject constructor( private val router: AppRouter, private val urlOpener: UrlOpener, private val reduxStateHolder: ReduxStateHolder, private val onboardingV2FeatureToggles: OnboardingV2FeatureToggles, + private val marketsEntryComponentFactory: MarketsEntryComponent.Factory, + private val renameWalletComponentFactory: RenameWalletComponent.Factory, ) : InnerWalletRouter { private var navController: NavHostController by Delegates.notNull() private var onFinish: () -> Unit = {} + private lateinit var marketsEntryComponent: MarketsEntryComponent + private lateinit var dialog: Value> + + override val dialogNavigation: SlotNavigation = SlotNavigation() + + override fun initializeResources(appComponentContext: AppComponentContext) { + marketsEntryComponent = marketsEntryComponentFactory.create(appComponentContext) + dialog = appComponentContext.childSlot( + source = dialogNavigation, + serializer = WalletDialogConfig.serializer(), + handleBackButton = true, + childFactory = { dialogConfig, componentContext -> + dialogChild( + appContext = appComponentContext, + dialogConfig = dialogConfig, + componentContext = componentContext, + ) + }, + ) + } + override fun getEntryFragment(): Fragment = WalletFragment.create() @Composable - override fun Initialize(onFinish: () -> Unit, marketsEntryComponent: MarketsEntryComponent) { + override fun Initialize(onFinish: () -> Unit) { this.onFinish = onFinish NavHost( @@ -58,10 +94,14 @@ internal class DefaultWalletRouter( subscribeToLifecycle(LocalLifecycleOwner.current) } + val dialog by dialog.subscribeAsState() + WalletScreen( state = viewModel.uiState.collectAsStateWithLifecycle().value, marketsEntryComponent = marketsEntryComponent, ) + + dialog.child?.instance?.Dialog() } composable( @@ -176,6 +216,23 @@ internal class DefaultWalletRouter( reduxStateHolder.dispatchDialogShow(StateDialog.ScanFailsDialog(StateDialog.ScanFailsSource.MAIN, onTryAgain)) } + private fun dialogChild( + appContext: AppComponentContext, + dialogConfig: WalletDialogConfig, + componentContext: ComponentContext, + ): ComposableDialogComponent = when (dialogConfig) { + is WalletDialogConfig.RenameWallet -> { + renameWalletComponentFactory.create( + context = appContext.childByContext(componentContext), + params = RenameWalletComponent.Params( + userWalletId = dialogConfig.userWalletId, + currentName = dialogConfig.currentName, + onDismiss = dialogNavigation::dismiss, + ), + ) + } + } + 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 7441c80063..f20dd1c443 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 @@ -2,10 +2,12 @@ package com.tangem.feature.wallet.presentation.router import androidx.compose.runtime.Composable import androidx.compose.runtime.Stable +import com.arkivanov.decompose.router.slot.SlotNavigation +import com.tangem.core.decompose.context.AppComponentContext import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.wallets.models.UserWalletId -import com.tangem.features.markets.entry.MarketsEntryComponent +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletDialogConfig import com.tangem.features.wallet.navigation.WalletRouter /** @@ -19,13 +21,17 @@ import com.tangem.features.wallet.navigation.WalletRouter @Stable internal interface InnerWalletRouter : WalletRouter { + val dialogNavigation: SlotNavigation + + fun initializeResources(appComponentContext: AppComponentContext) + /** * Initialize router * * @param onFinish finish activity callback */ @Composable - fun Initialize(onFinish: () -> Unit, marketsEntryComponent: MarketsEntryComponent) + fun Initialize(onFinish: () -> Unit) /** Pop back stack */ fun popBackStack() diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletAlertState.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletAlertState.kt index 7d8fe9b661..3e34289d7f 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletAlertState.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletAlertState.kt @@ -38,16 +38,6 @@ internal sealed interface WalletAlertState { override val onConfirmClick: (() -> Unit)?, ) : Basic() - // TODO: reuse RenameWalletComponent [REDACTED_JIRA] - data class RenameWalletAlert( - override val text: String, - override val onConfirmClick: (String) -> Unit, - override val errorTextProvider: (String) -> TextReference?, - ) : TextInput() { - override val title: TextReference = resourceReference(id = R.string.user_wallet_list_rename_popup_title) - override val label: TextReference = resourceReference(id = R.string.user_wallet_list_rename_popup_placeholder) - } - data class RemoveWalletAlert(override val onConfirmClick: (() -> Unit)?) : Basic() { override val title: TextReference? = null override val message: TextReference = resourceReference(id = R.string.user_wallet_list_delete_prompt) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletDialogConfig.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletDialogConfig.kt new file mode 100644 index 0000000000..71dc159435 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletDialogConfig.kt @@ -0,0 +1,16 @@ +package com.tangem.feature.wallet.presentation.wallet.state.model + +import com.tangem.domain.wallets.models.UserWalletId +import kotlinx.serialization.Serializable + +/** + * Wallet dialog config. Used to show Decompose dialogs + * +[REDACTED_AUTHOR] + */ +@Serializable +internal sealed interface WalletDialogConfig { + + @Serializable + data class RenameWallet(val userWalletId: UserWalletId, val currentName: String) : WalletDialogConfig +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletCardClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletCardClickIntents.kt index 066fe95381..622afd1151 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletCardClickIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletCardClickIntents.kt @@ -1,21 +1,22 @@ package com.tangem.feature.wallet.presentation.wallet.viewmodels.intents import arrow.core.getOrElse +import com.arkivanov.decompose.router.slot.activate import com.tangem.common.routing.AppRoute import com.tangem.common.routing.AppRouter import com.tangem.core.analytics.api.AnalyticsEventHandler -import com.tangem.core.ui.extensions.resourceReference -import com.tangem.core.ui.extensions.wrappedList import com.tangem.domain.card.DeleteSavedAccessCodesUseCase import com.tangem.domain.redux.ReduxStateHolder import com.tangem.domain.wallets.models.UserWalletId -import com.tangem.domain.wallets.usecase.* -import com.tangem.feature.wallet.impl.R +import com.tangem.domain.wallets.usecase.DeleteWalletUseCase +import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase +import com.tangem.domain.wallets.usecase.GetUserWalletUseCase import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent.MainScreen import com.tangem.feature.wallet.presentation.wallet.domain.MultiWalletTokenListStore import com.tangem.feature.wallet.presentation.wallet.loaders.WalletScreenContentLoader import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController import com.tangem.feature.wallet.presentation.wallet.state.model.WalletAlertState +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletDialogConfig import com.tangem.feature.wallet.presentation.wallet.state.model.WalletEvent import com.tangem.feature.wallet.presentation.wallet.state.utils.WalletEventSender import com.tangem.utils.coroutines.CoroutineDispatcherProvider @@ -27,8 +28,6 @@ internal interface WalletCardClickIntents { fun onRenameBeforeConfirmationClick(userWalletId: UserWalletId) - fun onRenameAfterConfirmationClick(userWalletId: UserWalletId, name: String) - fun onDeleteBeforeConfirmationClick(userWalletId: UserWalletId) fun onDeleteAfterConfirmationClick(userWalletId: UserWalletId) @@ -41,8 +40,6 @@ internal class WalletCardClickIntentsImplementor @Inject constructor( private val tokenListStore: MultiWalletTokenListStore, private val walletEventSender: WalletEventSender, private val walletScreenContentLoader: WalletScreenContentLoader, - private val renameWalletUseCase: RenameWalletUseCase, - private val getWalletNamesUseCase: GetWalletNamesUseCase, private val getUserWalletUseCase: GetUserWalletUseCase, private val getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase, private val deleteWalletUseCase: DeleteWalletUseCase, @@ -56,34 +53,12 @@ internal class WalletCardClickIntentsImplementor @Inject constructor( override fun onRenameBeforeConfirmationClick(userWalletId: UserWalletId) { analyticsEventHandler.send(MainScreen.EditWalletTapped) - viewModelScope.launch(dispatchers.main) { - val walletNames = getWalletNamesUseCase() - val currentWalletName = stateHolder.getSelectedWallet().walletCardState.title - walletEventSender.send( - event = WalletEvent.ShowAlert( - state = WalletAlertState.RenameWalletAlert( - text = currentWalletName, - onConfirmClick = { onRenameAfterConfirmationClick(userWalletId, it) }, - errorTextProvider = { enteredName -> - if (walletNames.contains(enteredName) && enteredName != currentWalletName) { - resourceReference( - R.string.user_wallet_list_rename_popup_error_already_exists, - wrappedList(enteredName), - ) - } else { - null - } - }, - ), - ), - ) - } - } - - override fun onRenameAfterConfirmationClick(userWalletId: UserWalletId, name: String) { - viewModelScope.launch(dispatchers.main) { - renameWalletUseCase(userWalletId = userWalletId, name) - } + router.dialogNavigation.activate( + configuration = WalletDialogConfig.RenameWallet( + userWalletId = userWalletId, + currentName = stateHolder.getSelectedWallet().walletCardState.title, + ), + ) } override fun onDeleteBeforeConfirmationClick(userWalletId: UserWalletId) {