Updated on 2026-08-14

This commit is contained in:
Tangem 2026-02-10 11:49:44 +03:00
commit 6519ad5568
341 changed files with 6153 additions and 6250 deletions

View file

@ -3,15 +3,12 @@ package com.tangem.tap.features.details.redux
import com.tangem.common.CompletionResult
import com.tangem.common.doOnFailure
import com.tangem.common.doOnSuccess
import com.tangem.common.routing.AppRoute
import com.tangem.core.analytics.Analytics
import com.tangem.domain.apptheme.model.AppThemeMode
import com.tangem.domain.common.wallets.UserWalletsListRepository.LockMethod
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.tap.common.analytics.events.AnalyticsParam
import com.tangem.tap.common.analytics.events.Settings
import com.tangem.tap.common.extensions.dispatchNavigationAction
import com.tangem.tap.common.extensions.dispatchWithMain
import com.tangem.tap.common.extensions.inject
import com.tangem.tap.common.redux.AppState
@ -43,7 +40,7 @@ class DetailsMiddleware {
if (!DemoHelper.tryHandle(stateProvider)) {
val detailsState = stateProvider()?.detailsState
if (detailsState != null) {
handleAction(detailsState, action)
handleAction(action)
}
}
next(action)
@ -51,9 +48,9 @@ class DetailsMiddleware {
}
}
private fun handleAction(state: DetailsState, action: Action) {
private fun handleAction(action: Action) {
when (action) {
is DetailsAction.AppSettings -> appSettingsMiddleware.handle(state, action)
is DetailsAction.AppSettings -> appSettingsMiddleware.handle(action)
}
}
@ -61,12 +58,10 @@ class DetailsMiddleware {
private val checkBiometricsStatusJobHolder = JobHolder()
fun handle(state: DetailsState, action: DetailsAction.AppSettings) {
fun handle(action: DetailsAction.AppSettings) {
when (action) {
is DetailsAction.AppSettings.SwitchPrivacySetting -> {
when (action.setting) {
AppSetting.SaveWallets -> toggleSaveWallets(state, enable = action.enable)
AppSetting.SaveAccessCode -> toggleSaveAccessCodes(state, enable = action.enable)
AppSetting.RequireAccessCode -> toggleRequireAccessCode(enable = action.enable)
AppSetting.BiometricAuthentication -> toggleBiometricsAuthentication(enable = action.enable)
}
@ -221,118 +216,6 @@ class DetailsMiddleware {
}
}
private fun toggleSaveWallets(state: DetailsState, enable: Boolean) = scope.launch {
// Nothing to change
val walletsRepository = store.inject(DaggerGraphState::walletsRepository)
if (walletsRepository.shouldSaveUserWalletsSync() == enable) {
store.dispatchWithMain(DetailsAction.AppSettings.SwitchPrivacySetting.Success)
return@launch
}
toggleSaveWallets(state.scanResponse, enable)
.doOnFailure {
store.dispatchWithMain(
DetailsAction.AppSettings.SwitchPrivacySetting.Failure(
prevState = !enable,
setting = AppSetting.SaveWallets,
),
)
}
.doOnSuccess {
store.dispatchWithMain(DetailsAction.AppSettings.SwitchPrivacySetting.Success)
}
}
private suspend fun toggleSaveWallets(scanResponse: ScanResponse?, enable: Boolean): CompletionResult<Unit> {
return if (enable) {
saveCurrentWallet(scanResponse, enableAccessCodesSaving = false)
} else {
deleteSavedWalletsAndAccessCodes()
}
}
private fun toggleSaveAccessCodes(state: DetailsState, enable: Boolean) = scope.launch {
val shouldSaveAccessCodes = store.inject(DaggerGraphState::settingsRepository).shouldSaveAccessCodes()
// Nothing to change
if (shouldSaveAccessCodes == enable) {
store.dispatchWithMain(DetailsAction.AppSettings.SwitchPrivacySetting.Success)
return@launch
}
toggleSaveAccessCodes(state.scanResponse, state.appSettingsState.saveWallets, enable)
.doOnFailure {
store.dispatchWithMain(
DetailsAction.AppSettings.SwitchPrivacySetting.Failure(
prevState = !enable,
setting = AppSetting.SaveAccessCode,
),
)
}
.doOnSuccess {
store.dispatchWithMain(DetailsAction.AppSettings.SwitchPrivacySetting.Success)
}
}
private suspend fun toggleSaveAccessCodes(
scanResponse: ScanResponse?,
isWalletsSavingEnabled: Boolean,
enable: Boolean,
): CompletionResult<Unit> {
return if (enable) {
if (!isWalletsSavingEnabled) {
saveCurrentWallet(scanResponse, enableAccessCodesSaving = true)
} else {
saveAccessCodes(scanResponse)
}
} else {
deleteSavedAccessCodes()
}
}
private suspend fun saveCurrentWallet(
scanResponse: ScanResponse?,
enableAccessCodesSaving: Boolean,
): CompletionResult<Unit> {
store.inject(DaggerGraphState::walletsRepository).saveShouldSaveUserWallets(item = true)
return if (enableAccessCodesSaving) {
saveAccessCodes(scanResponse)
} else {
CompletionResult.Success(Unit)
}
.doOnSuccess {
Analytics.send(Settings.AppSettings.SaveWalletSwitcherChanged(AnalyticsParam.OnOffState.On))
}
.doOnFailure { error ->
Timber.e(error, "Unable to save user wallet")
}
}
private suspend fun deleteSavedWalletsAndAccessCodes(): CompletionResult<Unit> {
Analytics.send(Settings.AppSettings.SaveWalletSwitcherChanged(AnalyticsParam.OnOffState.Off))
deleteSavedAccessCodes()
store.inject(DaggerGraphState::walletsRepository).saveShouldSaveUserWallets(item = false)
store.dispatchNavigationAction { replaceAll(AppRoute.Home()) }
return CompletionResult.Success(Unit)
}
private suspend fun saveAccessCodes(scanResponse: ScanResponse?): CompletionResult<Unit> {
Analytics.send(Settings.AppSettings.SaveAccessCodeSwitcherChanged(AnalyticsParam.OnOffState.On))
store.inject(DaggerGraphState::settingsRepository).setShouldSaveAccessCodes(value = true)
store.inject(DaggerGraphState::cardSdkConfigRepository).setAccessCodeRequestPolicy(
isBiometricsRequestPolicy = scanResponse?.card?.isAccessCodeSet == true,
)
return CompletionResult.Success(Unit)
}
private suspend fun deleteSavedAccessCodes(): CompletionResult<Unit> {
return tangemSdkManager.clearSavedUserCodes()
.doOnSuccess {

View file

@ -38,15 +38,6 @@ private fun handlePrivacyAction(action: DetailsAction.AppSettings, state: Detail
return when (action) {
is DetailsAction.AppSettings.SwitchPrivacySetting -> state.copy(
appSettingsState = when (action.setting) {
AppSetting.SaveWallets -> state.appSettingsState.copy(
isInProgress = true,
saveWallets = action.enable,
)
AppSetting.SaveAccessCode -> state.appSettingsState.copy(
isInProgress = true,
saveWallets = true, // User can't enable access codes saving without wallets saving
saveAccessCodes = action.enable,
)
AppSetting.RequireAccessCode -> state.appSettingsState.copy(
isInProgress = true,
requireAccessCode = action.enable,
@ -64,14 +55,6 @@ private fun handlePrivacyAction(action: DetailsAction.AppSettings, state: Detail
)
is DetailsAction.AppSettings.SwitchPrivacySetting.Failure -> state.copy(
appSettingsState = when (action.setting) {
AppSetting.SaveWallets -> state.appSettingsState.copy(
isInProgress = false,
saveWallets = action.prevState,
)
AppSetting.SaveAccessCode -> state.appSettingsState.copy(
isInProgress = false,
saveAccessCodes = action.prevState,
)
AppSetting.RequireAccessCode -> state.appSettingsState.copy(
isInProgress = false,
requireAccessCode = action.prevState,
@ -105,9 +88,6 @@ private fun handlePrivacyAction(action: DetailsAction.AppSettings, state: Detail
// state should be copied to avoid concurrent modifications from different sources
is DetailsAction.AppSettings.Prepare -> state.copy(
appSettingsState = state.appSettingsState.copy(
saveWallets = action.state.saveWallets,
saveAccessCodes = action.state.saveAccessCodes,
isBiometricsAvailable = action.state.isBiometricsAvailable,
isHidingEnabled = action.state.isHidingEnabled,
selectedAppCurrency = action.state.selectedAppCurrency,
selectedThemeMode = action.state.selectedThemeMode,

View file

@ -13,12 +13,6 @@ data class DetailsState(
@Suppress("BooleanPropertyNaming")
data class AppSettingsState(
@Deprecated("Delete after hot wallet release")
val saveWallets: Boolean = false,
@Deprecated("Delete after hot wallet release")
val saveAccessCodes: Boolean = false,
@Deprecated("Delete after hot wallet release")
val isBiometricsAvailable: Boolean = false,
val requireAccessCode: Boolean = false,
val useBiometricAuthentication: Boolean = false,
val needEnrollBiometrics: Boolean = false,
@ -32,5 +26,5 @@ data class AppSettingsState(
enum class SecurityOption { LongTap, PassCode, AccessCode }
enum class AppSetting {
SaveWallets, SaveAccessCode, RequireAccessCode, BiometricAuthentication,
RequireAccessCode, BiometricAuthentication,
}

View file

@ -9,26 +9,6 @@ import kotlinx.collections.immutable.toImmutableList
internal class AppSettingsDialogsFactory {
fun createDeleteSavedWalletsAlert(onDelete: () -> Unit, onDismiss: () -> Unit): Dialog.Alert {
return Dialog.Alert(
title = resourceReference(R.string.common_attention),
description = resourceReference(R.string.app_settings_off_saved_wallet_alert_message),
confirmText = resourceReference(R.string.common_delete),
onConfirm = onDelete,
onDismiss = onDismiss,
)
}
fun createDeleteSavedAccessCodesAlert(onDelete: () -> Unit, onDismiss: () -> Unit): Dialog.Alert {
return Dialog.Alert(
title = resourceReference(R.string.common_attention),
description = resourceReference(R.string.app_settings_off_saved_access_code_alert_message),
confirmText = resourceReference(R.string.common_delete),
onConfirm = onDelete,
onDismiss = onDismiss,
)
}
fun createThemeModeSelectorDialog(
selectedModeIndex: Int,
onSelect: (AppThemeMode) -> Unit,

View file

@ -19,21 +19,6 @@ internal class AppSettingsItemsFactory {
)
}
fun createSaveWalletsSwitch(
isChecked: Boolean,
isEnabled: Boolean,
onCheckedChange: (Boolean) -> Unit,
): Item.Switch {
return Item.Switch(
id = ID_SAVE_WALLETS_SWITCH,
title = resourceReference(R.string.app_settings_saved_wallet),
description = resourceReference(R.string.app_settings_saved_wallet_footer),
isEnabled = isEnabled,
isChecked = isChecked,
onCheckedChange = onCheckedChange,
)
}
fun createUseBiometricsSwitch(
isChecked: Boolean,
isEnabled: Boolean,
@ -127,7 +112,6 @@ internal class AppSettingsItemsFactory {
companion object {
const val ID_ENROLL_BIOMETRICS_CARD = "enroll_biometrics_card"
const val ID_SAVE_WALLETS_SWITCH = "save_wallets_switch"
const val ID_SAVE_ACCESS_CODES_SWITCH = "save_access_codes_switch"
const val ID_FLIP_TO_HIDE_BALANCE_SWITCH = "flip_to_hide_balance_switch"
const val ID_SELECT_APP_CURRENCY_BUTTON = "select_app_currency_button"

View file

@ -97,7 +97,6 @@ private class AppSettingsScreenStateProvider : CollectionPreviewParameterProvide
val items = persistentListOf(
itemsFactory.createEnrollBiometricsCard {},
itemsFactory.createSelectAppCurrencyButton(currentAppCurrencyName = "US Dollar") {},
itemsFactory.createSaveWalletsSwitch(isChecked = true, isEnabled = true, { _ -> }),
itemsFactory.createSaveAccessCodeSwitch(isChecked = false, isEnabled = true) { _ -> },
itemsFactory.createFlipToHideBalanceSwitch(isChecked = false, isEnabled = true) { _ -> },
itemsFactory.createSelectThemeModeButton(AppThemeMode.DEFAULT, {}),

View file

@ -43,12 +43,12 @@ private fun AlertDialogPreview(@PreviewParameter(AlertDialogProvider::class) dia
}
}
private class AlertDialogProvider : CollectionPreviewParameterProvider<Dialog.Alert>(
private class AlertDialogProvider : CollectionPreviewParameterProvider<Dialog>(
collection = buildList {
val dialogsFactory = AppSettingsDialogsFactory()
add(dialogsFactory.createDeleteSavedAccessCodesAlert({}, {}))
add(dialogsFactory.createDeleteSavedWalletsAlert({}, {}))
add(dialogsFactory.createThemeModeSelectorDialog(selectedModeIndex = 0, onSelect = {}, onDismiss = {}))
add(dialogsFactory.createDisableBiometricAuthenticationAlert(onDisable = {}, onDismiss = {}))
},
)
// endregion Preview

View file

@ -14,10 +14,7 @@ import com.tangem.domain.apptheme.model.AppThemeMode
import com.tangem.domain.apptheme.repository.AppThemeModeRepository
import com.tangem.domain.balancehiding.repositories.BalanceHidingRepository
import com.tangem.domain.common.wallets.UserWalletsListRepository
import com.tangem.domain.settings.CanUseBiometryUseCase
import com.tangem.domain.settings.repositories.SettingsRepository
import com.tangem.domain.wallets.repository.WalletsRepository
import com.tangem.features.hotwallet.HotWalletFeatureToggles
import com.tangem.tap.common.analytics.events.AnalyticsParam
import com.tangem.tap.common.analytics.events.Settings
import com.tangem.tap.common.extensions.dispatchNavigationAction
@ -52,14 +49,11 @@ internal class AppSettingsModel @Inject constructor(
override val dispatchers: CoroutineDispatcherProvider,
private val appCurrencyRepository: AppCurrencyRepository,
private val walletsRepository: WalletsRepository,
private val canUseBiometryUseCase: CanUseBiometryUseCase,
private val userWalletsListRepository: UserWalletsListRepository,
private val balanceHidingRepository: BalanceHidingRepository,
private val analyticsEventHandler: AnalyticsEventHandler,
private val appThemeModeRepository: AppThemeModeRepository,
private val settingsRepository: SettingsRepository,
private val appSettingsItemsAnalyticsSender: AppSettingsItemsAnalyticsSender,
private val hotWalletFeatureToggles: HotWalletFeatureToggles,
private val uiMessageSender: UiMessageSender,
) : Model(), StoreSubscriber<DetailsState> {
@ -120,47 +114,25 @@ internal class AppSettingsModel @Inject constructor(
),
)
if (hotWalletFeatureToggles.isHotWalletEnabled) {
val canUseBiometrics =
!state.needEnrollBiometrics && !state.isInProgress && state.hasSecuredWallets
val canUseBiometrics =
!state.needEnrollBiometrics && !state.isInProgress && state.hasSecuredWallets
add(
itemsFactory.createUseBiometricsSwitch(
isChecked = state.useBiometricAuthentication,
isEnabled = canUseBiometrics,
onCheckedChange = ::onBiometricAuthenticationToggled,
onDisabledClick = ::onBiometricAuthenticationDisabledClicked,
),
)
add(
itemsFactory.createUseBiometricsSwitch(
isChecked = state.useBiometricAuthentication,
isEnabled = canUseBiometrics,
onCheckedChange = ::onBiometricAuthenticationToggled,
onDisabledClick = ::onBiometricAuthenticationDisabledClicked,
),
)
add(
itemsFactory.createRequireAccessCodeSwitch(
isChecked = state.requireAccessCode || !state.useBiometricAuthentication,
isEnabled = canUseBiometrics && state.useBiometricAuthentication,
onCheckedChange = ::onRequireAccessCodeToggled,
),
)
} else {
if (state.isBiometricsAvailable) {
val canUseBiometrics = !state.needEnrollBiometrics && !state.isInProgress
add(
itemsFactory.createSaveWalletsSwitch(
isChecked = state.saveWallets,
isEnabled = canUseBiometrics,
onCheckedChange = ::onSaveWalletsToggled,
),
)
add(
itemsFactory.createSaveAccessCodeSwitch(
isChecked = state.saveAccessCodes,
isEnabled = canUseBiometrics,
onCheckedChange = ::onSaveAccessCodesToggled,
),
)
}
}
add(
itemsFactory.createRequireAccessCodeSwitch(
isChecked = state.requireAccessCode || !state.useBiometricAuthentication,
isEnabled = canUseBiometrics && state.useBiometricAuthentication,
onCheckedChange = ::onRequireAccessCodeToggled,
),
)
add(
itemsFactory.createFlipToHideBalanceSwitch(
@ -261,42 +233,6 @@ internal class AppSettingsModel @Inject constructor(
}
}
private fun onSaveWalletsToggled(isChecked: Boolean) {
if (isChecked) {
onSettingsToggled(AppSetting.SaveWallets, enable = true)
} else {
updateContentState {
copy(
dialog = dialogsFactory.createDeleteSavedWalletsAlert(
onDelete = {
onSettingsToggled(AppSetting.SaveWallets, enable = false)
dismissDialog()
},
onDismiss = ::dismissDialog,
),
)
}
}
}
private fun onSaveAccessCodesToggled(isChecked: Boolean) {
if (isChecked) {
onSettingsToggled(AppSetting.SaveAccessCode, enable = true)
} else {
updateContentState {
copy(
dialog = dialogsFactory.createDeleteSavedAccessCodesAlert(
onDelete = {
onSettingsToggled(AppSetting.SaveAccessCode, enable = false)
dismissDialog()
},
onDismiss = ::dismissDialog,
),
)
}
}
}
private fun onSettingsToggled(setting: AppSetting, enable: Boolean) {
store.dispatch(DetailsAction.AppSettings.SwitchPrivacySetting(enable = enable, setting = setting))
}
@ -326,9 +262,6 @@ internal class AppSettingsModel @Inject constructor(
private fun bootstrapBiometricsUpdates() = modelScope.launch {
val state = AppSettingsState(
saveWallets = walletsRepository.shouldSaveUserWalletsSync(),
saveAccessCodes = settingsRepository.shouldSaveAccessCodes(),
isBiometricsAvailable = canUseBiometryUseCase(),
useBiometricAuthentication = walletsRepository.useBiometricAuthentication(),
requireAccessCode = walletsRepository.requireAccessCode(),
isHidingEnabled = balanceHidingRepository.getBalanceHidingSettings().isHidingEnabledInSettings,

View file

@ -14,12 +14,9 @@ import com.tangem.domain.card.ResetCardUseCase
import com.tangem.domain.card.ResetCardUserCodeParams
import com.tangem.domain.card.common.util.cardTypesResolver
import com.tangem.domain.models.wallet.requireColdWallet
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.domain.wallets.legacy.asLockable
import com.tangem.domain.wallets.usecase.DeleteWalletUseCase
import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
import com.tangem.features.hotwallet.HotWalletFeatureToggles
import com.tangem.tap.common.analytics.events.Settings
import com.tangem.tap.common.extensions.dispatchNavigationAction
import com.tangem.tap.common.extensions.onUserWalletSelected
@ -52,10 +49,8 @@ internal class ResetCardModel @Inject constructor(
private val resetCardUseCase: ResetCardUseCase,
private val deleteSavedAccessCodesUseCase: DeleteSavedAccessCodesUseCase,
private val deleteWalletUseCase: DeleteWalletUseCase,
private val userWalletsListManager: UserWalletsListManager,
private val analyticsEventHandler: AnalyticsEventHandler,
private val cardSettingsInteractor: CardSettingsInteractor,
private val hotWalletFeatureToggles: HotWalletFeatureToggles,
) : Model() {
private val params = paramsContainer.require<ResetCardComponent.Params>()
@ -277,16 +272,7 @@ internal class ResetCardModel @Inject constructor(
if (newSelectedWallet != null) {
store.dispatchNavigationAction { popTo<AppRoute.Wallet>() }
} else {
if (hotWalletFeatureToggles.isHotWalletEnabled) {
store.dispatchNavigationAction { replaceAll(AppRoute.Home()) }
} else {
val isLocked = runCatching { userWalletsListManager.asLockable()?.isLocked }.isSuccess
if (isLocked && userWalletsListManager.hasUserWallets) {
store.dispatchNavigationAction { popTo<AppRoute.Welcome>() }
} else {
store.dispatchNavigationAction { replaceAll(AppRoute.Home()) }
}
}
store.dispatchNavigationAction { replaceAll(AppRoute.Home()) }
}
}

View file

@ -1,36 +0,0 @@
package com.tangem.tap.features.welcome.component
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.tap.features.welcome.model.WelcomeModel
import com.tangem.tap.features.welcome.ui.components.WelcomeScreen
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
internal class DefaultWelcomeComponent @AssistedInject constructor(
@Assisted context: AppComponentContext,
@Assisted params: WelcomeComponent.Params,
) : WelcomeComponent, AppComponentContext by context {
private val model: WelcomeModel = getOrCreateModel(params)
@Composable
override fun Content(modifier: Modifier) {
val state by model.state.collectAsStateWithLifecycle()
WelcomeScreen(
modifier = modifier,
state = state,
)
}
@AssistedFactory
interface Factory : WelcomeComponent.Factory {
override fun create(context: AppComponentContext, params: WelcomeComponent.Params): DefaultWelcomeComponent
}
}

View file

@ -1,14 +0,0 @@
package com.tangem.tap.features.welcome.component
import com.tangem.common.routing.entity.InitScreenLaunchMode
import com.tangem.core.decompose.factory.ComponentFactory
import com.tangem.core.ui.decompose.ComposableContentComponent
interface WelcomeComponent : ComposableContentComponent {
data class Params(
val launchMode: InitScreenLaunchMode,
)
interface Factory : ComponentFactory<Params, WelcomeComponent>
}

View file

@ -1,20 +0,0 @@
package com.tangem.tap.features.welcome.component.impl
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.tangem.tap.features.welcome.component.WelcomeComponent
import com.tangem.tap.features.welcome.ui.WelcomeScreenState
import com.tangem.tap.features.welcome.ui.components.WelcomeScreen
internal class PreviewWelcomeComponent(
private val initialState: WelcomeScreenState = WelcomeScreenState(),
) : WelcomeComponent {
@Composable
override fun Content(modifier: Modifier) {
WelcomeScreen(
modifier = modifier,
state = initialState,
)
}
}

View file

@ -1,18 +0,0 @@
package com.tangem.tap.features.welcome.di
import com.tangem.tap.features.welcome.component.DefaultWelcomeComponent
import com.tangem.tap.features.welcome.component.WelcomeComponent
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 bindWelcomeComponentFactory(factory: DefaultWelcomeComponent.Factory): WelcomeComponent.Factory
}

View file

@ -1,20 +0,0 @@
package com.tangem.tap.features.welcome.di
import com.tangem.core.decompose.di.ModelComponent
import com.tangem.core.decompose.model.Model
import com.tangem.tap.features.welcome.model.WelcomeModel
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.multibindings.ClassKey
import dagger.multibindings.IntoMap
@Module
@InstallIn(ModelComponent::class)
internal interface ModelModule {
@Binds
@IntoMap
@ClassKey(WelcomeModel::class)
fun bindWelcomeModel(model: WelcomeModel): Model
}

View file

@ -1,140 +0,0 @@
package com.tangem.tap.features.welcome.model
import com.tangem.common.core.TangemError
import com.tangem.common.routing.entity.InitScreenLaunchMode
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.core.navigation.finisher.AppFinisher
import com.tangem.domain.wallets.legacy.UserWalletsListError
import com.tangem.tap.common.analytics.events.SignIn
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.core.ui.extensions.TextReference
import com.tangem.tap.features.welcome.component.WelcomeComponent
import com.tangem.tap.features.welcome.redux.WelcomeAction
import com.tangem.tap.features.welcome.redux.WelcomeState
import com.tangem.tap.features.welcome.ui.WelcomeScreenState
import com.tangem.tap.features.welcome.ui.model.WarningModel
import com.tangem.tap.store
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.update
import org.rekotlin.StoreSubscriber
import javax.inject.Inject
// FIXME: Remove redux: [REDACTED_JIRA]
@ModelScoped
internal class WelcomeModel @Inject constructor(
override val dispatchers: CoroutineDispatcherProvider,
private val appFinisher: AppFinisher,
private val analyticsEventsHandler: AnalyticsEventHandler,
paramsContainer: ParamsContainer,
) : Model(), StoreSubscriber<WelcomeState> {
private val params: WelcomeComponent.Params = paramsContainer.require()
private val initialState: WelcomeScreenState = WelcomeScreenState(
onPopBack = appFinisher::finish,
onUnlockClick = this::unlockWallets,
onScanCardClick = this::scanCard,
onCloseError = this::closeError,
)
val state: MutableStateFlow<WelcomeScreenState> = MutableStateFlow(initialState)
init {
subscribeToStoreChanges()
initGlobalState()
val welcomeAction = when (params.launchMode) {
is InitScreenLaunchMode.WithCardScan -> WelcomeAction.ProceedWithCard
is InitScreenLaunchMode.Standard -> WelcomeAction.ProceedWithBiometrics
}
store.dispatch(welcomeAction)
}
private fun unlockWallets() {
analyticsEventsHandler.send(SignIn.ButtonBiometricSignIn())
store.dispatch(WelcomeAction.ProceedWithBiometrics)
}
private fun scanCard() {
analyticsEventsHandler.send(SignIn.ButtonCardSignIn())
store.dispatch(WelcomeAction.ProceedWithCard)
}
private fun closeError() {
store.dispatch(WelcomeAction.CloseError)
}
override fun newState(state: WelcomeState) {
val warning = createWarningIfNeeded(state.error)
this.state.update { prevState ->
prevState.copy(
isUnlockWithBiometricsProgressVisible = state.isUnlockWithBiometricsInProgress,
isUnlockWithCardProgressVisible = state.isUnlockWithCardInProgress,
warning = warning,
error = state.error
?.takeIf { !it.silent && warning == null }
?.let { error ->
val messageResId = error.messageResId
if (messageResId != null) {
TextReference.Res(messageResId)
} else {
TextReference.Str(error.customMessage)
}
},
)
}
}
override fun onDestroy() {
store.unsubscribe(subscriber = this)
super.onDestroy()
}
private fun createWarningIfNeeded(error: TangemError?): WarningModel? {
return when (error) {
is UserWalletsListError.BiometricsAuthenticationLockout -> WarningModel.BiometricsLockoutWarning(
isPermanent = error.isPermanent,
onDismiss = this::dismissWarning,
)
is UserWalletsListError.AllKeysInvalidated,
is UserWalletsListError.NoUserWalletSelected,
-> WarningModel.KeyInvalidatedWarning(
onDismiss = this::dismissWarning,
)
is UserWalletsListError.BiometricsAuthenticationDisabled -> WarningModel.BiometricsDisabledWarning(
onDismiss = this::clearUserWallets,
)
else -> null
}
}
private fun dismissWarning() {
state.update { prevState ->
prevState.copy(
warning = null,
)
}
closeError()
}
private fun clearUserWallets() {
store.dispatch(WelcomeAction.ClearUserWallets)
}
private fun subscribeToStoreChanges() {
store.subscribe(this) { appState ->
appState.skip { old, new -> old.welcomeState == new.welcomeState }
.select { it.welcomeState }
}
}
private fun initGlobalState() {
store.dispatch(GlobalAction.RestoreAppCurrency)
}
}

View file

@ -1,22 +0,0 @@
package com.tangem.tap.features.welcome.redux
import com.tangem.common.core.TangemError
import org.rekotlin.Action
internal sealed interface WelcomeAction : Action {
data object ProceedWithBiometrics : WelcomeAction {
object Success : WelcomeAction
data class Error(val error: TangemError) : WelcomeAction
}
object ProceedWithCard : WelcomeAction {
object Success : WelcomeAction
data class Error(val error: TangemError) : WelcomeAction
data class ChangeProgress(val isProgress: Boolean) : WelcomeAction
}
object CloseError : WelcomeAction
object ClearUserWallets : WelcomeAction
}

View file

@ -1,164 +0,0 @@
package com.tangem.tap.features.welcome.redux
import com.tangem.common.core.TangemSdkError
import com.tangem.common.doOnFailure
import com.tangem.common.doOnResult
import com.tangem.common.doOnSuccess
import com.tangem.common.flatMap
import com.tangem.common.routing.AppRoute
import com.tangem.common.routing.utils.popTo
import com.tangem.core.analytics.Analytics
import com.tangem.core.analytics.models.AnalyticsParam
import com.tangem.core.analytics.models.Basic
import com.tangem.domain.card.analytics.ParamCardCurrencyConverter
import com.tangem.domain.card.common.util.cardTypesResolver
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.wallets.legacy.UserWalletsListManager.Lockable.UnlockType
import com.tangem.domain.wallets.legacy.unlockIfLockable
import com.tangem.tap.*
import com.tangem.tap.common.extensions.*
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.proxy.redux.DaggerGraphState
import kotlinx.coroutines.launch
import org.rekotlin.Middleware
import timber.log.Timber
internal class WelcomeMiddleware {
val middleware: Middleware<AppState> = { _, appStateProvider ->
{ next ->
{ action ->
val appState = appStateProvider()
if (action is WelcomeAction && appState != null) {
handleAction(action)
}
next(action)
}
}
}
private fun handleAction(action: WelcomeAction) {
mainScope.launch {
when (action) {
is WelcomeAction.ProceedWithBiometrics -> proceedWithBiometrics()
is WelcomeAction.ProceedWithCard -> proceedWithCard()
is WelcomeAction.ClearUserWallets -> disableUserWalletsSaving()
else -> Unit
}
}
}
private suspend fun proceedWithBiometrics() {
val userWalletsListManager = store.inject(DaggerGraphState::generalUserWalletsListManager)
userWalletsListManager.unlockIfLockable(type = UnlockType.ANY)
.doOnFailure { error ->
Timber.e(error, "Unable to unlock user wallets with biometrics")
store.dispatchWithMain(WelcomeAction.ProceedWithBiometrics.Error(error))
}
.doOnSuccess { selectedUserWallet ->
sendSignedInAnalyticsEvent(
userWallet = selectedUserWallet,
signInType = Basic.SignedInLegacy.SignInType.Biometric,
)
store.dispatchNavigationAction { replaceAll(AppRoute.Wallet) }
store.dispatchWithMain(WelcomeAction.ProceedWithBiometrics.Success)
store.onUserWalletSelected(userWallet = selectedUserWallet)
}
}
private suspend fun proceedWithCard() {
scanCardInternal { scanResponse ->
val userWalletBuilder = store.inject(DaggerGraphState::coldUserWalletBuilderFactory).create(scanResponse)
val userWallet = userWalletBuilder.build() ?: return@scanCardInternal
val userWalletsListManager = store.inject(DaggerGraphState::generalUserWalletsListManager)
userWalletsListManager.save(userWallet, canOverride = true)
.doOnFailure { error ->
Timber.e(error, "Unable to save user wallet")
store.dispatchWithMain(WelcomeAction.ProceedWithCard.Error(error))
}
.doOnSuccess {
sendSignedInAnalyticsEvent(userWallet, signInType = Basic.SignedInLegacy.SignInType.Card)
store.dispatchNavigationAction { replaceAll(AppRoute.Wallet) }
store.dispatchWithMain(WelcomeAction.ProceedWithCard.Success)
store.onUserWalletSelected(userWallet = userWallet)
}
}
}
private fun sendSignedInAnalyticsEvent(userWallet: UserWallet, signInType: Basic.SignedInLegacy.SignInType) {
if (userWallet !is UserWallet.Cold) {
return
}
val scanResponse = userWallet.scanResponse
val currency = ParamCardCurrencyConverter().convert(
value = scanResponse.cardTypesResolver,
)
val trackingContextProxy = store.inject(DaggerGraphState::trackingContextProxy)
trackingContextProxy.addContext(scanResponse)
if (currency != null) {
val userWalletsListManager = store.inject(DaggerGraphState::generalUserWalletsListManager)
Analytics.send(
event = Basic.SignedInLegacy(
currency = currency,
batch = scanResponse.card.batchId,
signInType = signInType,
walletsCount = userWalletsListManager.walletsCount.toString(),
isImported = userWallet.isImported,
hasBackup = scanResponse.card.backupStatus?.isActive,
),
)
}
}
private suspend fun disableUserWalletsSaving() {
val userWalletsListManager = store.inject(DaggerGraphState::generalUserWalletsListManager)
userWalletsListManager.clear()
.flatMap { tangemSdkManager.clearSavedUserCodes() }
.doOnFailure { e ->
Timber.e(e, "Unable to clear user wallets")
}
.doOnResult {
store.dispatchWithMain(WelcomeAction.CloseError)
store.dispatchNavigationAction { popTo<AppRoute.Home>() }
}
}
private suspend inline fun scanCardInternal(crossinline onCardScanned: suspend (ScanResponse) -> Unit) {
val shouldSaveAccessCodes = store.inject(DaggerGraphState::settingsRepository).shouldSaveAccessCodes()
store.inject(DaggerGraphState::cardSdkConfigRepository).setAccessCodeRequestPolicy(
isBiometricsRequestPolicy = shouldSaveAccessCodes,
)
store.inject(DaggerGraphState::scanCardProcessor).scan(
analyticsSource = AnalyticsParam.ScreensSources.SignIn,
onSuccess = { scanResponse ->
scope.launch { onCardScanned(scanResponse) }
},
onFailure = { error ->
when (error) {
is TangemSdkError.ExceptionError -> {
store.dispatchOnMain(WelcomeAction.ProceedWithCard.Success)
}
else -> {
store.dispatchOnMain(WelcomeAction.ProceedWithCard.Error(error))
}
}
},
onProgressStateChange = {
store.dispatchWithMain(WelcomeAction.ProceedWithCard.ChangeProgress(it))
},
onWalletNotCreated = {
store.dispatchOnMain(WelcomeAction.ProceedWithCard.Success)
},
)
}
}

View file

@ -1,36 +0,0 @@
package com.tangem.tap.features.welcome.redux
import com.tangem.tap.common.redux.AppState
import org.rekotlin.Action
internal object WelcomeReducer {
fun reduce(action: Action, state: AppState): WelcomeState {
return if (action is WelcomeAction) {
internalReduce(action, state.welcomeState)
} else {
state.welcomeState
}
}
private fun internalReduce(action: WelcomeAction, state: WelcomeState): WelcomeState {
return when (action) {
is WelcomeAction.ProceedWithBiometrics -> state.copy(isUnlockWithBiometricsInProgress = true)
is WelcomeAction.ProceedWithCard -> state.copy(isUnlockWithCardInProgress = true)
is WelcomeAction.ProceedWithBiometrics.Error -> state.copy(
error = action.error,
isUnlockWithBiometricsInProgress = false,
)
is WelcomeAction.ProceedWithCard.Error -> state.copy(
error = action.error,
isUnlockWithCardInProgress = false,
)
is WelcomeAction.ProceedWithCard.ChangeProgress -> state.copy(
isUnlockWithCardInProgress = action.isProgress,
)
is WelcomeAction.ProceedWithBiometrics.Success -> state.copy(isUnlockWithBiometricsInProgress = false)
is WelcomeAction.ProceedWithCard.Success -> state.copy(isUnlockWithCardInProgress = false)
is WelcomeAction.CloseError -> state.copy(error = null)
else -> state
}
}
}

View file

@ -1,10 +0,0 @@
package com.tangem.tap.features.welcome.redux
import com.tangem.common.core.TangemError
import org.rekotlin.StateType
data class WelcomeState(
val isUnlockWithBiometricsInProgress: Boolean = false,
val isUnlockWithCardInProgress: Boolean = false,
val error: TangemError? = null,
) : StateType

View file

@ -1,15 +0,0 @@
package com.tangem.tap.features.welcome.ui
import com.tangem.core.ui.extensions.TextReference
import com.tangem.tap.features.welcome.ui.model.WarningModel
internal data class WelcomeScreenState(
val onPopBack: () -> Unit = {},
val isUnlockWithBiometricsProgressVisible: Boolean = false,
val isUnlockWithCardProgressVisible: Boolean = false,
val warning: WarningModel? = null,
val error: TextReference? = null,
val onUnlockClick: () -> Unit = {},
val onScanCardClick: () -> Unit = {},
val onCloseError: () -> Unit = {},
)

View file

@ -1,136 +0,0 @@
package com.tangem.tap.features.welcome.ui.components
import android.content.res.Configuration
import androidx.compose.foundation.layout.Column
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import com.tangem.core.ui.components.BasicDialog
import com.tangem.core.ui.components.DialogButtonUM
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.tap.features.welcome.ui.model.WarningModel
import com.tangem.wallet.R
@Composable
internal fun WarningDialog(warning: WarningModel?) {
when (warning) {
null -> Unit
is WarningModel.BiometricsLockoutWarning -> {
BasicDialog(
title = stringResourceSafe(id = R.string.biometric_lockout_warning_title),
message = stringResourceSafe(
id = if (warning.isPermanent) {
R.string.biometric_lockout_permanent_warning_description
} else {
R.string.biometric_lockout_warning_description
},
),
onDismissDialog = warning.onDismiss,
confirmButton = DialogButtonUM(
title = stringResourceSafe(id = R.string.common_ok),
onClick = warning.onDismiss,
),
)
}
is WarningModel.KeyInvalidatedWarning -> {
BasicDialog(
title = stringResourceSafe(id = R.string.common_attention),
message = stringResourceSafe(id = R.string.key_invalidated_warning_description),
onDismissDialog = warning.onDismiss,
confirmButton = DialogButtonUM(
title = stringResourceSafe(id = R.string.common_ok),
onClick = warning.onDismiss,
),
)
}
is WarningModel.BiometricsDisabledWarning -> {
BasicDialog(
title = stringResourceSafe(id = R.string.common_warning),
message = stringResourceSafe(id = R.string.biometric_unavailable_warning),
onDismissDialog = warning.onDismiss,
isDismissable = false,
confirmButton = DialogButtonUM(
title = stringResourceSafe(id = R.string.common_ok),
onClick = warning.onDismiss,
),
)
}
}
}
// region Preview
@Composable
private fun BiometricsLockoutDialogSample(modifier: Modifier = Modifier) {
Column(modifier = modifier) {
WarningDialog(
warning = WarningModel.BiometricsLockoutWarning(
isPermanent = false,
onDismiss = {},
),
)
}
}
@Preview(showBackground = true, widthDp = 360)
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun BiometricsLockoutDialogPreview() {
TangemThemePreview {
BiometricsLockoutDialogSample()
}
}
@Composable
private fun BiometricsLockoutDialog_Permanent_Sample(modifier: Modifier = Modifier) {
Column(modifier = modifier) {
WarningDialog(
warning = WarningModel.BiometricsLockoutWarning(
isPermanent = true,
onDismiss = {},
),
)
}
}
@Preview(showBackground = true, widthDp = 360)
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun BiometricsLockoutDialog_Permanent_Preview() {
TangemThemePreview {
BiometricsLockoutDialog_Permanent_Sample()
}
}
@Composable
private fun KeyInvalidatedWarningSample(modifier: Modifier = Modifier) {
Column(modifier = modifier) {
WarningDialog(warning = WarningModel.KeyInvalidatedWarning(onDismiss = {}))
}
}
@Preview(showBackground = true, widthDp = 360)
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun KeyInvalidatedWarningPreview() {
TangemThemePreview {
KeyInvalidatedWarningSample()
}
}
@Composable
private fun BiometricDisabledWarningSample(modifier: Modifier = Modifier) {
Column(modifier = modifier) {
WarningDialog(warning = WarningModel.BiometricsDisabledWarning(onDismiss = {}))
}
}
@Preview(showBackground = true, widthDp = 360)
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun BiometricDisabledWarningPreview() {
TangemThemePreview {
BiometricDisabledWarningSample()
}
}
// endregion Preview

View file

@ -1,101 +0,0 @@
package com.tangem.tap.features.welcome.ui.components
import android.content.res.Configuration
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.systemBarsPadding
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.tap.features.welcome.component.WelcomeComponent
import com.tangem.tap.features.welcome.component.impl.PreviewWelcomeComponent
import com.tangem.tap.features.welcome.ui.WelcomeScreenState
import com.tangem.tap.features.welcome.ui.model.WarningModel
@Composable
internal fun WelcomeScreen(state: WelcomeScreenState, modifier: Modifier = Modifier) {
val snackbarHostState = remember { SnackbarHostState() }
val errorMessage by rememberUpdatedState(newValue = state.error?.resolveReference())
val warning by rememberUpdatedState(newValue = state.warning)
BackHandler(onBack = state.onPopBack)
Box(
modifier = modifier
.background(TangemTheme.colors.background.primary)
.systemBarsPadding(),
) {
WelcomeScreenContent(
showUnlockProgress = state.isUnlockWithBiometricsProgressVisible,
showScanCardProgress = state.isUnlockWithCardProgressVisible,
onUnlockClick = state.onUnlockClick,
onScanCardClick = state.onScanCardClick,
)
SnackbarHost(
modifier = Modifier
.align(Alignment.BottomCenter)
.padding(vertical = 16.dp)
.fillMaxWidth(),
hostState = snackbarHostState,
)
}
WarningDialog(warning)
LaunchedEffect(errorMessage, state.onCloseError) {
errorMessage?.let { message ->
snackbarHostState.showSnackbar(message)
state.onCloseError()
}
}
}
// region Preview
@Composable
@Preview(showBackground = true, widthDp = 360)
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
private fun Preview_WelcomeScreen(
@PreviewParameter(WelcomeComponentPreviewProvider::class) component: WelcomeComponent,
) {
TangemThemePreview {
component.Content(Modifier)
}
}
private class WelcomeComponentPreviewProvider : PreviewParameterProvider<WelcomeComponent> {
override val values: Sequence<WelcomeComponent>
get() = sequenceOf(
PreviewWelcomeComponent(),
PreviewWelcomeComponent(
initialState = WelcomeScreenState(
isUnlockWithBiometricsProgressVisible = true,
isUnlockWithCardProgressVisible = true,
),
),
PreviewWelcomeComponent(
initialState = WelcomeScreenState(
error = TextReference.Str(value = "Error"),
),
),
PreviewWelcomeComponent(
initialState = WelcomeScreenState(
warning = WarningModel.KeyInvalidatedWarning(onDismiss = {}),
),
),
)
}
// endregion Preview

View file

@ -1,113 +0,0 @@
package com.tangem.tap.features.welcome.ui.components
import android.content.res.Configuration
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import com.tangem.core.ui.components.*
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.wallet.R
@Suppress("LongMethod")
@Composable
internal fun WelcomeScreenContent(
showUnlockProgress: Boolean,
showScanCardProgress: Boolean,
onUnlockClick: () -> Unit,
onScanCardClick: () -> Unit,
modifier: Modifier = Modifier,
) {
Column(
modifier = modifier,
horizontalAlignment = Alignment.CenterHorizontally,
) {
SpacerHMax()
Icon(
modifier = Modifier.size(TangemTheme.dimens.size96),
painter = painterResource(id = R.drawable.img_tangem_logo_96),
tint = TangemTheme.colors.icon.primary1,
contentDescription = null,
)
SpacerH32()
Text(
modifier = Modifier
.padding(horizontal = TangemTheme.dimens.spacing16)
.fillMaxWidth(),
text = stringResourceSafe(R.string.welcome_unlock_title),
style = TangemTheme.typography.h2,
color = TangemTheme.colors.text.primary1,
textAlign = TextAlign.Center,
)
SpacerH12()
Text(
modifier = Modifier
.padding(horizontal = TangemTheme.dimens.spacing44)
.fillMaxWidth(),
text = stringResourceSafe(
id = R.string.welcome_unlock_description,
stringResourceSafe(id = R.string.common_biometric_authentication),
),
style = TangemTheme.typography.body1,
color = TangemTheme.colors.text.secondary,
textAlign = TextAlign.Center,
)
SpacerHMax()
SecondaryButton(
modifier = Modifier
.padding(horizontal = TangemTheme.dimens.spacing16)
.fillMaxWidth(),
text = stringResourceSafe(
id = R.string.welcome_unlock,
stringResourceSafe(id = R.string.common_biometrics),
),
showProgress = showUnlockProgress,
onClick = onUnlockClick,
)
SpacerH12()
PrimaryButtonIconEnd(
modifier = Modifier
.padding(horizontal = TangemTheme.dimens.spacing16)
.fillMaxWidth(),
text = stringResourceSafe(R.string.welcome_unlock_card),
showProgress = showScanCardProgress,
iconResId = R.drawable.ic_tangem_24,
onClick = onScanCardClick,
)
SpacerH16()
}
}
// region Preview
@Composable
private fun WelcomeScreenContentSample(modifier: Modifier = Modifier) {
Box(
modifier = modifier
.background(TangemTheme.colors.background.primary),
) {
WelcomeScreenContent(
showUnlockProgress = false,
showScanCardProgress = false,
onUnlockClick = { /* no-op */ },
onScanCardClick = { /* no-op */ },
)
}
}
@Preview(showBackground = true, widthDp = 360)
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun WelcomeScreenContentPreview() {
TangemThemePreview {
WelcomeScreenContentSample()
}
}
// endregion Preview

View file

@ -1,16 +0,0 @@
package com.tangem.tap.features.welcome.ui.model
internal sealed interface WarningModel {
data class BiometricsLockoutWarning(
val isPermanent: Boolean,
val onDismiss: () -> Unit,
) : WarningModel
data class KeyInvalidatedWarning(
val onDismiss: () -> Unit,
) : WarningModel
data class BiometricsDisabledWarning(
val onDismiss: () -> Unit,
) : WarningModel
}