Updated on 2026-08-14

This commit is contained in:
Tangem 2025-02-24 14:50:44 +03:00
parent b2d47a56a2
commit dd60ff7542
15 changed files with 225 additions and 112 deletions

View file

@ -1,9 +1,9 @@
package com.tangem.tap.features.details.redux
import androidx.lifecycle.LifecycleCoroutineScope
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.apptheme.model.AppThemeMode
import com.tangem.domain.models.scan.ScanResponse
import kotlinx.coroutines.CoroutineScope
import org.rekotlin.Action
sealed class DetailsAction : Action {
@ -27,7 +27,7 @@ sealed class DetailsAction : Action {
}
data class CheckBiometricsStatus(
val lifecycleScope: LifecycleCoroutineScope,
val coroutineScope: CoroutineScope,
) : AppSettings()
data object EnrollBiometrics : AppSettings()

View file

@ -1,6 +1,5 @@
package com.tangem.tap.features.details.redux
import androidx.lifecycle.LifecycleCoroutineScope
import com.tangem.common.CompletionResult
import com.tangem.common.doOnFailure
import com.tangem.common.doOnSuccess
@ -22,6 +21,7 @@ import com.tangem.tap.store
import com.tangem.tap.tangemSdkManager
import com.tangem.utils.coroutines.JobHolder
import com.tangem.utils.coroutines.saveIn
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flow
@ -67,7 +67,7 @@ class DetailsMiddleware {
}
}
is DetailsAction.AppSettings.CheckBiometricsStatus -> {
observeBiometricsStatusChanges(action.lifecycleScope)
observeBiometricsStatusChanges(action.coroutineScope)
}
is DetailsAction.AppSettings.EnrollBiometrics -> {
enrollBiometrics()
@ -90,7 +90,7 @@ class DetailsMiddleware {
}
}
private fun observeBiometricsStatusChanges(lifecycleScope: LifecycleCoroutineScope) {
private fun observeBiometricsStatusChanges(scope: CoroutineScope) {
val needEnrollBiometricsFlow = flow {
do {
val needEnrollBiometrics = runCatching(tangemSdkManager::needEnrollBiometrics).getOrNull()
@ -108,7 +108,7 @@ class DetailsMiddleware {
.onEach { needEnrollBiometrics ->
store.dispatchWithMain(DetailsAction.AppSettings.BiometricsStatusChanged(needEnrollBiometrics))
}
.launchIn(lifecycleScope)
.launchIn(scope)
.saveIn(checkBiometricsStatusJobHolder)
}

View file

@ -1,31 +0,0 @@
package com.tangem.tap.features.details.ui.appcurrency
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.fragment.app.viewModels
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.tangem.core.ui.UiDependencies
import com.tangem.core.ui.components.NavigationBar3ButtonsScrim
import com.tangem.core.ui.screen.ComposeFragment
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
@AndroidEntryPoint
internal class AppCurrencySelectorFragment : ComposeFragment() {
@Inject
override lateinit var uiDependencies: UiDependencies
private val viewModel: AppCurrencySelectorViewModel by viewModels()
@Composable
override fun ScreenContent(modifier: Modifier) {
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
NavigationBar3ButtonsScrim()
AppCurrencySelectorScreen(
modifier = modifier,
state = uiState,
)
}
}

View file

@ -0,0 +1,38 @@
package com.tangem.tap.features.details.ui.appcurrency
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.core.ui.components.NavigationBar3ButtonsScrim
import com.tangem.tap.features.details.ui.appcurrency.api.AppCurrencySelectorComponent
import com.tangem.tap.features.details.ui.appcurrency.model.AppCurrencySelectorModel
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
@Suppress("UnusedPrivateMember")
internal class DefaultAppCurrencySelectorComponent @AssistedInject constructor(
@Assisted appComponentContext: AppComponentContext,
@Assisted params: Unit,
) : AppCurrencySelectorComponent, AppComponentContext by appComponentContext {
private val model: AppCurrencySelectorModel = getOrCreateModel()
@Composable
override fun Content(modifier: Modifier) {
val uiState by model.uiState.collectAsStateWithLifecycle()
NavigationBar3ButtonsScrim()
AppCurrencySelectorScreen(
modifier = modifier,
state = uiState,
)
}
@AssistedFactory
interface Factory : AppCurrencySelectorComponent.Factory {
override fun create(context: AppComponentContext, params: Unit): DefaultAppCurrencySelectorComponent
}
}

View file

@ -0,0 +1,9 @@
package com.tangem.tap.features.details.ui.appcurrency.api
import com.tangem.core.decompose.factory.ComponentFactory
import com.tangem.core.ui.decompose.ComposableContentComponent
interface AppCurrencySelectorComponent : ComposableContentComponent {
interface Factory : ComponentFactory<Unit, AppCurrencySelectorComponent>
}

View file

@ -0,0 +1,25 @@
package com.tangem.tap.features.details.ui.appcurrency.di
import com.tangem.core.decompose.model.Model
import com.tangem.tap.features.details.ui.appcurrency.DefaultAppCurrencySelectorComponent
import com.tangem.tap.features.details.ui.appcurrency.api.AppCurrencySelectorComponent
import com.tangem.tap.features.details.ui.appcurrency.model.AppCurrencySelectorModel
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import dagger.multibindings.ClassKey
import dagger.multibindings.IntoMap
@Module
@InstallIn(SingletonComponent::class)
internal interface AppCurrencySelectorFeatureModule {
@Binds
fun bindFactory(impl: DefaultAppCurrencySelectorComponent.Factory): AppCurrencySelectorComponent.Factory
@Binds
@IntoMap
@ClassKey(AppCurrencySelectorModel::class)
fun bindModel(model: AppCurrencySelectorModel): Model
}

View file

@ -1,35 +1,38 @@
package com.tangem.tap.features.details.ui.appcurrency
package com.tangem.tap.features.details.ui.appcurrency.model
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.compose.runtime.Stable
import com.tangem.common.routing.AppRouter
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.decompose.di.ComponentScoped
import com.tangem.core.decompose.model.Model
import com.tangem.domain.appcurrency.GetAvailableCurrenciesUseCase
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
import com.tangem.domain.appcurrency.SelectAppCurrencyUseCase
import com.tangem.tap.common.analytics.events.Settings
import com.tangem.tap.features.details.ui.appcurrency.AppCurrencySelectorIntents
import com.tangem.tap.features.details.ui.appcurrency.AppCurrencySelectorState
import com.tangem.tap.features.details.ui.appcurrency.AppCurrencySelectorStateHolder
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import javax.inject.Inject
@HiltViewModel
internal class AppCurrencySelectorViewModel @Inject constructor(
@Stable
@ComponentScoped
internal class AppCurrencySelectorModel @Inject constructor(
override val dispatchers: CoroutineDispatcherProvider,
private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
private val getAvailableCurrenciesUseCase: GetAvailableCurrenciesUseCase,
private val selectAppCurrencyUseCase: SelectAppCurrencyUseCase,
private val router: AppRouter,
private val dispatchers: CoroutineDispatcherProvider,
private val analyticsEventHandler: AnalyticsEventHandler,
) : ViewModel(), AppCurrencySelectorIntents {
) : Model(), AppCurrencySelectorIntents {
private val stateController = AppCurrencySelectorStateHolder(
intents = this,
onSubscription = { fetchCurrencies() },
stateFlowScope = viewModelScope,
stateFlowScope = modelScope,
)
val uiState: StateFlow<AppCurrencySelectorState> = stateController.stateFlow
@ -47,7 +50,7 @@ internal class AppCurrencySelectorViewModel @Inject constructor(
}
override fun onCurrencyClick(currency: AppCurrencySelectorState.Currency) {
viewModelScope.launch(dispatchers.io) {
modelScope.launch(dispatchers.io) {
selectAppCurrencyUseCase(currency.id)
.onRight {
analyticsEventHandler.send(
@ -63,7 +66,7 @@ internal class AppCurrencySelectorViewModel @Inject constructor(
}
private fun fetchCurrencies() {
viewModelScope.launch(dispatchers.io) {
modelScope.launch(dispatchers.io) {
val availableCurrencies = getAvailableCurrenciesUseCase()
.onRight(stateController::updateStateWithAvailableCurrencies)
.getOrNull()

View file

@ -1,42 +0,0 @@
package com.tangem.tap.features.details.ui.appsettings
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.tangem.common.routing.AppRouter
import com.tangem.core.ui.UiDependencies
import com.tangem.core.ui.screen.ComposeFragment
import com.tangem.domain.appcurrency.repository.AppCurrencyRepository
import com.tangem.tap.common.extensions.dispatchNavigationAction
import com.tangem.tap.store
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
@AndroidEntryPoint
internal class AppSettingsFragment : ComposeFragment() {
@Inject
override lateinit var uiDependencies: UiDependencies
@Inject
lateinit var appCurrencyRepository: AppCurrencyRepository
@Composable
override fun ScreenContent(modifier: Modifier) {
val viewModel = hiltViewModel<AppSettingsViewModel>().apply {
LocalLifecycleOwner.current.lifecycle.addObserver(observer = this)
}
val state by viewModel.uiState.collectAsStateWithLifecycle()
AppSettingsScreen(
modifier = modifier,
state = state,
onBackClick = {
store.dispatchNavigationAction(AppRouter::pop)
},
)
}
}

View file

@ -0,0 +1,49 @@
package com.tangem.tap.features.details.ui.appsettings
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.arkivanov.essenty.lifecycle.doOnResume
import com.tangem.common.routing.AppRouter
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.tap.common.extensions.dispatchNavigationAction
import com.tangem.tap.features.details.ui.appsettings.api.AppSettingsComponent
import com.tangem.tap.features.details.ui.appsettings.model.AppSettingsModel
import com.tangem.tap.store
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
@Suppress("UnusedPrivateMember")
internal class DefaultAppSettingsComponent @AssistedInject constructor(
@Assisted appComponentContext: AppComponentContext,
@Assisted params: Unit,
) : AppSettingsComponent, AppComponentContext by appComponentContext {
private val model: AppSettingsModel = getOrCreateModel()
init {
doOnResume { model.onResume() }
}
@Composable
override fun Content(modifier: Modifier) {
val state by model.uiState.collectAsStateWithLifecycle()
AppSettingsScreen(
modifier = modifier,
state = state,
onBackClick = {
store.dispatchNavigationAction(AppRouter::pop)
},
)
}
@AssistedFactory
interface Factory : AppSettingsComponent.Factory {
override fun create(context: AppComponentContext, params: Unit): DefaultAppSettingsComponent
}
}

View file

@ -2,13 +2,13 @@ package com.tangem.tap.features.details.ui.appsettings.analytics
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.analytics.models.AnalyticsEvent
import com.tangem.core.decompose.di.ComponentScoped
import com.tangem.tap.common.analytics.events.Settings
import com.tangem.tap.features.details.ui.appsettings.AppSettingsItemsFactory
import com.tangem.tap.features.details.ui.appsettings.AppSettingsScreenState
import dagger.hilt.android.scopes.ViewModelScoped
import javax.inject.Inject
@ViewModelScoped
@ComponentScoped
internal class AppSettingsItemsAnalyticsSender @Inject constructor(
private val analyticsHandler: AnalyticsEventHandler,
) {

View file

@ -0,0 +1,9 @@
package com.tangem.tap.features.details.ui.appsettings.api
import com.tangem.core.decompose.factory.ComponentFactory
import com.tangem.core.ui.decompose.ComposableContentComponent
interface AppSettingsComponent : ComposableContentComponent {
interface Factory : ComponentFactory<Unit, AppSettingsComponent>
}

View file

@ -0,0 +1,25 @@
package com.tangem.tap.features.details.ui.appsettings.di
import com.tangem.core.decompose.model.Model
import com.tangem.tap.features.details.ui.appsettings.DefaultAppSettingsComponent
import com.tangem.tap.features.details.ui.appsettings.api.AppSettingsComponent
import com.tangem.tap.features.details.ui.appsettings.model.AppSettingsModel
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import dagger.multibindings.ClassKey
import dagger.multibindings.IntoMap
@Module
@InstallIn(SingletonComponent::class)
internal interface AppSettingsFeatureModule {
@Binds
fun bindFactory(impl: DefaultAppSettingsComponent.Factory): AppSettingsComponent.Factory
@Binds
@IntoMap
@ClassKey(AppSettingsModel::class)
fun bindModel(model: AppSettingsModel): Model
}

View file

@ -1,8 +1,10 @@
package com.tangem.tap.features.details.ui.appsettings
package com.tangem.tap.features.details.ui.appsettings.model
import androidx.lifecycle.*
import androidx.compose.runtime.Stable
import com.tangem.common.routing.AppRoute
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.decompose.di.ComponentScoped
import com.tangem.core.decompose.model.Model
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.appcurrency.repository.AppCurrencyRepository
import com.tangem.domain.apptheme.model.AppThemeMode
@ -20,12 +22,15 @@ import com.tangem.tap.features.details.redux.AppSetting
import com.tangem.tap.features.details.redux.AppSettingsState
import com.tangem.tap.features.details.redux.DetailsAction
import com.tangem.tap.features.details.redux.DetailsState
import com.tangem.tap.features.details.ui.appsettings.AppSettingsDialogsFactory
import com.tangem.tap.features.details.ui.appsettings.AppSettingsItemsFactory
import com.tangem.tap.features.details.ui.appsettings.AppSettingsScreenState
import com.tangem.tap.features.details.ui.appsettings.analytics.AppSettingsItemsAnalyticsSender
import com.tangem.tap.scope
import com.tangem.tap.store
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.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.flow.*
@ -34,8 +39,10 @@ import org.rekotlin.StoreSubscriber
import javax.inject.Inject
@Suppress("LongParameterList")
@HiltViewModel
internal class AppSettingsViewModel @Inject constructor(
@Stable
@ComponentScoped
internal class AppSettingsModel @Inject constructor(
override val dispatchers: CoroutineDispatcherProvider,
private val appCurrencyRepository: AppCurrencyRepository,
private val walletsRepository: WalletsRepository,
private val canUseBiometryUseCase: CanUseBiometryUseCase,
@ -44,9 +51,7 @@ internal class AppSettingsViewModel @Inject constructor(
private val appThemeModeRepository: AppThemeModeRepository,
private val settingsRepository: SettingsRepository,
private val appSettingsItemsAnalyticsSender: AppSettingsItemsAnalyticsSender,
) : ViewModel(),
StoreSubscriber<DetailsState>,
DefaultLifecycleObserver {
) : Model(), StoreSubscriber<DetailsState> {
private val itemsFactory = AppSettingsItemsFactory()
private val dialogsFactory = AppSettingsDialogsFactory()
@ -82,11 +87,12 @@ internal class AppSettingsViewModel @Inject constructor(
}
}
override fun onResume(owner: LifecycleOwner) {
store.dispatch(DetailsAction.AppSettings.CheckBiometricsStatus(owner.lifecycleScope))
fun onResume() {
store.dispatch(DetailsAction.AppSettings.CheckBiometricsStatus(modelScope))
}
override fun onCleared() {
override fun onDestroy() {
super.onDestroy()
store.unsubscribe(subscriber = this)
}
@ -225,7 +231,7 @@ internal class AppSettingsViewModel @Inject constructor(
.saveIn(appCurrencyUpdatesJobHolder)
}
private fun bootstrapBiometricsUpdates() = viewModelScope.launch {
private fun bootstrapBiometricsUpdates() = modelScope.launch {
val state = AppSettingsState(
saveWallets = walletsRepository.shouldSaveUserWalletsSync(),
saveAccessCodes = settingsRepository.shouldSaveAccessCodes(),

View file

@ -49,7 +49,7 @@ internal class DefaultWalletConnectComponent @AssistedInject constructor(
}
override fun newState(state: WalletConnectState) {
screenState?.value = model.updateState(state)
screenState.value = model.updateState(state)
}
@Composable

View file

@ -20,8 +20,8 @@ import com.tangem.features.staking.api.StakingComponent
import com.tangem.features.tester.api.TesterRouter
import com.tangem.features.tokendetails.TokenDetailsComponent
import com.tangem.features.wallet.navigation.WalletRouter
import com.tangem.tap.features.details.ui.appcurrency.AppCurrencySelectorFragment
import com.tangem.tap.features.details.ui.appsettings.AppSettingsFragment
import com.tangem.tap.features.details.ui.appcurrency.api.AppCurrencySelectorComponent
import com.tangem.tap.features.details.ui.appsettings.api.AppSettingsComponent
import com.tangem.tap.features.details.ui.cardsettings.api.CardSettingsComponent
import com.tangem.tap.features.details.ui.cardsettings.coderecovery.api.AccessCodeRecoveryComponent
import com.tangem.tap.features.details.ui.resetcard.ResetCardFragment
@ -67,6 +67,8 @@ internal class ChildFactory @Inject constructor(
private val qrScanningComponentFactory: QrScanningComponent.Factory,
private val accessCodeRecoveryComponentFactory: AccessCodeRecoveryComponent.Factory,
private val cardSettingsComponentFactory: CardSettingsComponent.Factory,
private val appCurrencySelectorComponentFactory: AppCurrencySelectorComponent.Factory,
private val appSettingsComponentFactory: AppSettingsComponent.Factory,
private val walletRouter: WalletRouter,
private val testerRouter: TesterRouter,
private val pushNotificationRouter: PushNotificationsRouter,
@ -303,9 +305,21 @@ internal class ChildFactory @Inject constructor(
componentFactory = cardSettingsComponentFactory,
)
}
is AppRoute.AppCurrencySelector,
is AppRoute.AppCurrencySelector -> {
createComponentChild(
contextProvider = contextProvider(route, contextFactory),
params = Unit,
componentFactory = appCurrencySelectorComponentFactory,
)
}
is AppRoute.AppSettings -> {
createComponentChild(
contextProvider = contextProvider(route, contextFactory),
params = Unit,
componentFactory = appSettingsComponentFactory,
)
}
is AppRoute.SaveWallet,
is AppRoute.AppSettings,
is AppRoute.DetailsSecurity,
is AppRoute.OnboardingNote,
is AppRoute.OnboardingOther,
@ -337,7 +351,11 @@ internal class ChildFactory @Inject constructor(
)
}
is AppRoute.AppCurrencySelector -> {
route.asFragmentChild(Provider { AppCurrencySelectorFragment() })
route.asComponentChild(
contextProvider = contextProvider(route, contextFactory),
params = Unit,
componentFactory = appCurrencySelectorComponentFactory,
)
}
is AppRoute.SaveWallet -> {
route.asFragmentChild(Provider { SaveWalletBottomSheetFragment() })
@ -357,7 +375,11 @@ internal class ChildFactory @Inject constructor(
)
}
is AppRoute.AppSettings -> {
route.asFragmentChild(Provider { AppSettingsFragment() })
route.asComponentChild(
contextProvider = contextProvider(route, contextFactory),
params = Unit,
componentFactory = appSettingsComponentFactory,
)
}
is AppRoute.CardSettings -> {
route.asComponentChild(