Updated on 2026-08-14

This commit is contained in:
Tangem 2025-02-07 18:14:09 +04:00
parent 978ad99625
commit 46bf08cb8b
9 changed files with 142 additions and 104 deletions

View file

@ -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

View file

@ -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)

View file

@ -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
}

View file

@ -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,
)
}

View file

@ -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<ChildSlot<WalletDialogConfig, ComposableDialogComponent>>
override val dialogNavigation: SlotNavigation<WalletDialogConfig> = 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
}

View file

@ -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<WalletDialogConfig>
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()

View file

@ -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)

View file

@ -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
}

View file

@ -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) {