Updated on 2026-08-14

This commit is contained in:
Tangem 2023-09-15 13:50:13 +03:00
parent 1d75f13d4e
commit b2f12e90aa
22 changed files with 386 additions and 5 deletions

View file

@ -62,6 +62,7 @@ import com.tangem.tap.domain.walletStores.repository.di.provideDefaultImplementa
import com.tangem.tap.domain.walletconnect.WalletConnectRepository
import com.tangem.tap.domain.walletconnect2.domain.WalletConnectSessionsRepository
import com.tangem.tap.features.customtoken.api.featuretoggles.CustomTokenFeatureToggles
import com.tangem.tap.features.details.featuretoggles.DetailsFeatureToggles
import com.tangem.tap.proxy.AppStateHolder
import com.tangem.tap.proxy.redux.DaggerGraphState
import com.tangem.wallet.BuildConfig
@ -176,6 +177,9 @@ class TapApplication : Application(), ImageLoaderFactory {
@Inject
lateinit var appThemeModeRepository: AppThemeModeRepository
@Inject
lateinit var detailsFeatureToggles: DetailsFeatureToggles
override fun onCreate() {
super.onCreate()
@ -199,6 +203,7 @@ class TapApplication : Application(), ImageLoaderFactory {
appStateHolder = appStateHolder,
currenciesRepository = currenciesRepository,
appThemeModeRepository = appThemeModeRepository,
detailsFeatureToggles = detailsFeatureToggles,
),
),
)

View file

@ -7,6 +7,7 @@ import com.tangem.core.navigation.FragmentShareTransition
import com.tangem.feature.referral.ReferralFragment
import com.tangem.feature.swap.presentation.SwapFragment
import com.tangem.tap.features.customtoken.impl.presentation.AddCustomTokenFragment
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.cardsettings.CardSettingsFragment
import com.tangem.tap.features.details.ui.cardsettings.coderecovery.AccessCodeRecoveryFragment
@ -174,5 +175,6 @@ private fun fragmentFactory(screen: AppScreen): Fragment {
AppScreen.Welcome -> WelcomeFragment()
AppScreen.SaveWallet -> SaveWalletBottomSheetFragment()
AppScreen.WalletSelector -> WalletSelectorBottomSheetFragment()
AppScreen.AppCurrencySelector -> AppCurrencySelectorFragment()
}
}

View file

@ -66,7 +66,13 @@ private fun handleAction(action: Action, appState: () -> AppState?, dispatch: Di
}
}
is GlobalAction.RestoreAppCurrency -> {
if (store.state.daggerGraphState.get(DaggerGraphState::walletFeatureToggles).isRedesignedScreenEnabled) {
val daggerGraphState = store.state.daggerGraphState
val walletFeatureToggles = daggerGraphState.get(DaggerGraphState::walletFeatureToggles)
val detailsFeatureToggles = daggerGraphState.get(DaggerGraphState::detailsFeatureToggles)
if (walletFeatureToggles.isRedesignedScreenEnabled ||
detailsFeatureToggles.isRedesignedAppCurrencySelectorEnabled
) {
restoreAppCurrencyNew()
} else {
restoreAppCurrencyLegacy()

View file

@ -0,0 +1,11 @@
package com.tangem.tap.features.details.featuretoggles
import com.tangem.core.featuretoggle.manager.FeatureTogglesManager
internal class DefaultDetailsFeatureToggles(
private val featureTogglesManager: FeatureTogglesManager,
) : DetailsFeatureToggles {
override val isRedesignedAppCurrencySelectorEnabled: Boolean
get() = featureTogglesManager.isFeatureEnabled(name = "REDESIGNED_APP_CURRENCY_SELECTOR_ENABLED")
}

View file

@ -0,0 +1,6 @@
package com.tangem.tap.features.details.featuretoggles
interface DetailsFeatureToggles {
val isRedesignedAppCurrencySelectorEnabled: Boolean
}

View file

@ -0,0 +1,17 @@
package com.tangem.tap.features.details.featuretoggles
import com.tangem.core.featuretoggle.manager.FeatureTogglesManager
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
@Module
@InstallIn(SingletonComponent::class)
internal object DetailsFeatureTogglesModule {
@Provides
fun provideDetailsFeatureToggles(featureTogglesManager: FeatureTogglesManager): DetailsFeatureToggles {
return DefaultDetailsFeatureToggles(featureTogglesManager)
}
}

View file

@ -84,6 +84,10 @@ sealed class DetailsAction : Action {
data class ChangeAppThemeMode(
val appThemeMode: AppThemeMode,
) : AppSettings()
data class ChangeAppCurrency(
val fiatCurrency: FiatCurrency,
) : AppSettings()
}
data class ChangeAppCurrency(val fiatCurrency: FiatCurrency) : DetailsAction()

View file

@ -39,6 +39,7 @@ import com.tangem.tap.features.demo.DemoHelper
import com.tangem.tap.features.onboarding.products.twins.redux.CreateTwinWalletMode
import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsAction
import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.features.walletSelector.redux.WalletSelectorAction
import com.tangem.tap.proxy.redux.DaggerGraphState
import com.tangem.tap.tangemSdkManager
import com.tangem.wallet.R
@ -206,6 +207,11 @@ class DetailsMiddleware {
is DetailsAction.AppSettings.ChangeAppThemeMode -> {
changeAppThemeMode(action.appThemeMode)
}
is DetailsAction.AppSettings.ChangeAppCurrency -> {
store.dispatch(GlobalAction.ChangeAppCurrency(action.fiatCurrency))
store.dispatch(DetailsAction.ChangeAppCurrency(action.fiatCurrency))
store.dispatch(WalletSelectorAction.ChangeAppCurrency(action.fiatCurrency))
}
is DetailsAction.AppSettings.SwitchPrivacySetting.Success,
is DetailsAction.AppSettings.SwitchPrivacySetting.Failure,
is DetailsAction.AppSettings.BiometricsStatusChanged,

View file

@ -216,6 +216,11 @@ private fun handlePrivacyAction(action: DetailsAction.AppSettings, state: Detail
selectedThemeMode = action.appThemeMode,
),
)
is DetailsAction.AppSettings.ChangeAppCurrency -> state.copy(
appSettingsState = state.appSettingsState.copy(
selectedFiatCurrency = action.fiatCurrency,
),
)
is DetailsAction.AppSettings.EnrollBiometrics,
is DetailsAction.AppSettings.CheckBiometricsStatus,
-> state

View file

@ -1,7 +1,10 @@
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.components.SystemBarsEffect
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.screen.ComposeFragment
@ -15,6 +18,8 @@ internal class AppCurrencySelectorFragment : ComposeFragment() {
@Inject
override lateinit var appThemeModeHolder: AppThemeModeHolder
private val viewModel: AppCurrencySelectorViewModel by viewModels()
@Composable
override fun ScreenContent(modifier: Modifier) {
val systemBarsColor = TangemTheme.colors.background.secondary
@ -22,9 +27,10 @@ internal class AppCurrencySelectorFragment : ComposeFragment() {
setSystemBarsColor(systemBarsColor)
}
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
AppCurrencySelectorScreen(
modifier = modifier,
state = AppCurrencySelectorState.Loading({}), // TODO: Will be updated in next MR
state = uiState,
)
}
}

View file

@ -0,0 +1,14 @@
package com.tangem.tap.features.details.ui.appcurrency
internal interface AppCurrencySelectorIntents {
fun onBackClick()
fun onSearchClick()
fun onSearchInputChange(input: String)
fun onCurrencyClick(currency: AppCurrencySelectorState.Currency)
fun onDismissSearchClick()
}

View file

@ -20,6 +20,29 @@ internal sealed class AppCurrencySelectorState {
abstract val items: PersistentList<Currency>
abstract val onCurrencyClick: (Currency) -> Unit
abstract val onTopBarActionClick: () -> Unit
fun copySealed(
selectedId: String? = this.selectedId,
items: PersistentList<Currency> = this.items,
scrollToSelected: StateEvent<Int> = this.scrollToSelected,
onCurrencyClick: (Currency) -> Unit = this.onCurrencyClick,
onTopBarActionClick: () -> Unit = this.onTopBarActionClick,
): AppCurrencySelectorState = when (this) {
is Default -> this.copy(
selectedId = selectedId.orEmpty(),
items = items,
scrollToSelected = scrollToSelected,
onCurrencyClick = onCurrencyClick,
onTopBarActionClick = onTopBarActionClick,
)
is Search -> this.copy(
selectedId = selectedId.orEmpty(),
items = items,
scrollToSelected = scrollToSelected,
onCurrencyClick = onCurrencyClick,
onTopBarActionClick = onTopBarActionClick,
)
}
}
data class Default(

View file

@ -0,0 +1,125 @@
package com.tangem.tap.features.details.ui.appcurrency
import com.tangem.core.ui.event.consumedEvent
import com.tangem.core.ui.event.triggeredEvent
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.tap.features.details.ui.appcurrency.converter.CurrencyConverter
import kotlinx.collections.immutable.PersistentList
import kotlinx.collections.immutable.mutate
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toPersistentList
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.*
internal class AppCurrencySelectorStateHolder(
private val intents: AppCurrencySelectorIntents,
private val onSubscription: () -> Unit,
stateFlowScope: CoroutineScope,
) {
private var availableCurrencies: PersistentList<AppCurrencySelectorState.Currency> = persistentListOf()
private val stateFlowInternal: MutableStateFlow<AppCurrencySelectorState> = MutableStateFlow(getInitialState())
private val currencyConverter by lazy(mode = LazyThreadSafetyMode.NONE) { CurrencyConverter() }
val stateFlow: StateFlow<AppCurrencySelectorState> = stateFlowInternal
.onSubscription { onSubscription() }
.stateIn(
scope = stateFlowScope,
started = SharingStarted.WhileSubscribed(),
initialValue = getInitialState(),
)
fun updateStateWithAvailableCurrencies(currencies: List<AppCurrency>) {
availableCurrencies = currencyConverter.convertList(currencies).toPersistentList()
updateState {
when (this) {
is AppCurrencySelectorState.Loading -> getContentState(availableCurrencies)
is AppCurrencySelectorState.Content -> copySealed(items = availableCurrencies)
}
}
}
fun updateStateWithSelectedCurrency(selectedCurrency: AppCurrency, selectedCurrencyIndex: Int) {
val selectedCurrencyId = selectedCurrency.code
updateState {
when (this) {
is AppCurrencySelectorState.Loading -> this
is AppCurrencySelectorState.Content -> copySealed(
selectedId = selectedCurrencyId,
scrollToSelected = triggeredEvent(selectedCurrencyIndex, ::consumeScrollToSelectedEvent),
)
}
}
}
fun updateStateWithSearch(input: String = "") {
val filteredItems = availableCurrencies.mutate { list ->
list.removeAll { input.lowercase() !in it.name.lowercase() }
}
updateState {
when (this) {
is AppCurrencySelectorState.Loading -> this
is AppCurrencySelectorState.Search -> copy(items = filteredItems)
is AppCurrencySelectorState.Default -> getSearchState(filteredItems, selectedId)
}
}
}
fun updateStateWithoutSearch() {
updateState {
when (this) {
is AppCurrencySelectorState.Search -> getContentState(availableCurrencies, selectedId)
is AppCurrencySelectorState.Default,
is AppCurrencySelectorState.Loading,
-> this
}
}
}
private fun getInitialState() = AppCurrencySelectorState.Loading(
onBackClick = intents::onBackClick,
)
private fun getContentState(
items: PersistentList<AppCurrencySelectorState.Currency>,
selectedCurrencyId: String = "",
) = AppCurrencySelectorState.Default(
selectedId = selectedCurrencyId,
items = items,
onBackClick = intents::onBackClick,
onCurrencyClick = intents::onCurrencyClick,
onTopBarActionClick = intents::onSearchClick,
scrollToSelected = consumedEvent(),
)
private fun getSearchState(
filteredItems: PersistentList<AppCurrencySelectorState.Currency>,
selectedCurrencyId: String,
) = AppCurrencySelectorState.Search(
selectedId = selectedCurrencyId,
items = filteredItems,
scrollToSelected = consumedEvent(),
onBackClick = intents::onBackClick,
onCurrencyClick = intents::onCurrencyClick,
onSearchInputChange = intents::onSearchInputChange,
onTopBarActionClick = intents::onDismissSearchClick,
)
private inline fun updateState(block: AppCurrencySelectorState.() -> AppCurrencySelectorState) {
stateFlowInternal.update(block)
}
private fun consumeScrollToSelectedEvent() {
updateState {
when (this) {
is AppCurrencySelectorState.Loading -> this
is AppCurrencySelectorState.Content -> copySealed(scrollToSelected = consumedEvent())
}
}
}
}

View file

@ -0,0 +1,74 @@
package com.tangem.tap.features.details.ui.appcurrency
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.tangem.core.navigation.ReduxNavController
import com.tangem.domain.appcurrency.GetAvailableCurrenciesUseCase
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
import com.tangem.domain.appcurrency.SelectAppCurrencyUseCase
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(
private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
private val getAvailableCurrenciesUseCase: GetAvailableCurrenciesUseCase,
private val selectAppCurrencyUseCase: SelectAppCurrencyUseCase,
private val reduxNavController: ReduxNavController,
private val dispatchers: CoroutineDispatcherProvider,
) : ViewModel(), AppCurrencySelectorIntents {
private val stateController = AppCurrencySelectorStateHolder(
intents = this,
onSubscription = { fetchCurrencies() },
stateFlowScope = viewModelScope,
)
val uiState: StateFlow<AppCurrencySelectorState> = stateController.stateFlow
override fun onBackClick() {
reduxNavController.popBackStack()
}
override fun onSearchClick() {
stateController.updateStateWithSearch()
}
override fun onSearchInputChange(input: String) {
stateController.updateStateWithSearch(input)
}
override fun onCurrencyClick(currency: AppCurrencySelectorState.Currency) {
viewModelScope.launch(dispatchers.io) {
selectAppCurrencyUseCase(currency.id)
.onRight { reduxNavController.popBackStack() }
}
}
override fun onDismissSearchClick() {
stateController.updateStateWithoutSearch()
}
private fun fetchCurrencies() {
viewModelScope.launch(dispatchers.io) {
val availableCurrencies = getAvailableCurrenciesUseCase()
.onRight(stateController::updateStateWithAvailableCurrencies)
.getOrNull()
getSelectedAppCurrencyUseCase().collectLatest { maybeSelectedCurrency ->
maybeSelectedCurrency
.onRight { selectedCurrency ->
val selectedCurrencyIndex = availableCurrencies?.indexOfFirst { it == selectedCurrency }
if (selectedCurrencyIndex != null && selectedCurrencyIndex != -1) {
stateController.updateStateWithSelectedCurrency(selectedCurrency, selectedCurrencyIndex)
}
}
}
}
}
}

View file

@ -0,0 +1,14 @@
package com.tangem.tap.features.details.ui.appcurrency.converter
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.tap.features.details.ui.appcurrency.AppCurrencySelectorState
import com.tangem.utils.converter.Converter
internal class CurrencyConverter : Converter<AppCurrency, AppCurrencySelectorState.Currency> {
override fun convert(value: AppCurrency): AppCurrencySelectorState.Currency {
val fullCurrencyName = with(value) { "$name ($code) - $symbol" }
return AppCurrencySelectorState.Currency(value.code, fullCurrencyName)
}
}

View file

@ -8,6 +8,8 @@ import androidx.transition.TransitionInflater
import com.tangem.core.navigation.NavigationAction
import com.tangem.core.ui.screen.ComposeFragment
import com.tangem.core.ui.theme.AppThemeModeHolder
import com.tangem.domain.appcurrency.repository.AppCurrencyRepository
import com.tangem.tap.features.details.featuretoggles.DetailsFeatureToggles
import com.tangem.tap.features.details.redux.DetailsAction
import com.tangem.tap.features.details.redux.DetailsState
import com.tangem.tap.store
@ -22,7 +24,15 @@ internal class AppSettingsFragment : ComposeFragment(), StoreSubscriber<DetailsS
@Inject
override lateinit var appThemeModeHolder: AppThemeModeHolder
private val viewModel = AppSettingsViewModel(store)
@Inject
lateinit var detailsFeatureToggles: DetailsFeatureToggles
@Inject
lateinit var appCurrencyRepository: AppCurrencyRepository
private val viewModel by lazy(mode = LazyThreadSafetyMode.NONE) {
AppSettingsViewModel(store, detailsFeatureToggles, appCurrencyRepository)
}
@Composable
override fun ScreenContent(modifier: Modifier) {

View file

@ -4,26 +4,49 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.LifecycleCoroutineScope
import com.tangem.core.navigation.AppScreen
import com.tangem.core.navigation.NavigationAction
import com.tangem.domain.appcurrency.repository.AppCurrencyRepository
import com.tangem.domain.apptheme.model.AppThemeMode
import com.tangem.tap.common.entities.FiatCurrency
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.extensions.dispatchWithMain
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.features.details.featuretoggles.DetailsFeatureToggles
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.wallet.redux.WalletAction
import com.tangem.tap.scope
import com.tangem.utils.coroutines.JobHolder
import com.tangem.utils.coroutines.saveIn
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import org.rekotlin.Store
internal class AppSettingsViewModel(private val store: Store<AppState>) {
internal class AppSettingsViewModel(
private val store: Store<AppState>,
private val detailsFeatureToggles: DetailsFeatureToggles,
private val appCurrencyRepository: AppCurrencyRepository,
) {
private val itemsFactory = AppSettingsItemsFactory()
private val dialogsFactory = AppSettingsDialogsFactory()
private val appCurrencyUpdatesJobHolder = JobHolder()
var uiState: AppSettingsScreenState by mutableStateOf(AppSettingsScreenState.Loading)
private set
init {
if (detailsFeatureToggles.isRedesignedAppCurrencySelectorEnabled) {
bootstrapAppCurrencyUpdates()
}
}
fun updateState(state: DetailsState) {
uiState = AppSettingsScreenState.Content(
items = buildItems(state.appSettingsState),
@ -89,7 +112,13 @@ internal class AppSettingsViewModel(private val store: Store<AppState>) {
}
private fun showAppCurrencySelector() {
store.dispatchOnMain(WalletAction.AppCurrencyAction.ChooseAppCurrency)
val action = if (detailsFeatureToggles.isRedesignedAppCurrencySelectorEnabled) {
NavigationAction.NavigateTo(AppScreen.AppCurrencySelector)
} else {
WalletAction.AppCurrencyAction.ChooseAppCurrency
}
store.dispatchOnMain(action)
}
private fun showThemeModeSelector(selectedMode: AppThemeMode) {
@ -151,6 +180,17 @@ internal class AppSettingsViewModel(private val store: Store<AppState>) {
updateContentState { copy(dialog = null) }
}
private fun bootstrapAppCurrencyUpdates() {
appCurrencyRepository
.getSelectedAppCurrency()
.onEach {
val fiatCurrency = with(it) { FiatCurrency(code, name, symbol) }
store.dispatchWithMain(DetailsAction.AppSettings.ChangeAppCurrency(fiatCurrency))
}
.launchIn(scope)
.saveIn(appCurrencyUpdatesJobHolder)
}
private fun updateContentState(block: AppSettingsScreenState.Content.() -> AppSettingsScreenState.Content) {
uiState = when (val state = uiState) {
is AppSettingsScreenState.Content -> block(state)

View file

@ -58,6 +58,10 @@ class AppStateHolder @Inject constructor() : WalletsStateHolder, ReduxNavControl
override fun getBackStack(): List<AppScreen> = mainStore?.state?.navigationState?.backStack.orEmpty()
override fun popBackStack(screen: AppScreen?) {
mainStore?.dispatch(NavigationAction.PopBackTo(screen))
}
override fun dispatch(action: Action) {
mainStore?.dispatch(action)
}

View file

@ -18,6 +18,7 @@ import com.tangem.tap.domain.walletconnect2.domain.WalletConnectInteractor
import com.tangem.tap.domain.walletconnect2.domain.WalletConnectRepository
import com.tangem.tap.domain.walletconnect2.domain.WalletConnectSessionsRepository
import com.tangem.tap.features.customtoken.api.featuretoggles.CustomTokenFeatureToggles
import com.tangem.tap.features.details.featuretoggles.DetailsFeatureToggles
import com.tangem.tap.proxy.AppStateHolder
import org.rekotlin.StateType
@ -40,6 +41,7 @@ data class DaggerGraphState(
val walletManagersFacade: WalletManagersFacade? = null,
val appStateHolder: AppStateHolder? = null,
val appThemeModeRepository: AppThemeModeRepository? = null,
val detailsFeatureToggles: DetailsFeatureToggles? = null,
// FIXME: It is used only for TokensList screen. Remove after refactoring of TokensList
val currenciesRepository: CurrenciesRepository? = null,

View file

@ -22,5 +22,9 @@
{
"name": "SHOPIFY_DYNAMIC_ENABLED",
"version": "undefined"
},
{
"name": "REDESIGNED_APP_CURRENCY_SELECTOR_ENABLED",
"version": "undefined"
}
]

View file

@ -35,4 +35,5 @@ enum class AppScreen(val isDialogFragment: Boolean = false) {
Welcome,
SaveWallet(isDialogFragment = true),
WalletSelector(isDialogFragment = true),
AppCurrencySelector,
}

View file

@ -10,5 +10,7 @@ interface ReduxNavController {
/** Navigate by [action] */
fun navigate(action: NavigationAction)
fun popBackStack(screen: AppScreen? = null)
fun getBackStack(): List<AppScreen>
}