Updated on 2026-08-14

This commit is contained in:
Tangem 2025-03-13 11:34:30 +03:00
parent de3bdb3cb0
commit 56164d0b8b
36 changed files with 255 additions and 124 deletions

View file

@ -188,8 +188,8 @@ dependencies {
implementation(projects.features.stories.impl)
implementation(projects.features.txhistory.api)
implementation(projects.features.txhistory.impl)
implementation(projects.features.askBiometry.api)
implementation(projects.features.askBiometry.impl)
implementation(projects.features.biometry.api)
implementation(projects.features.biometry.impl)
implementation(projects.features.nft.api)
implementation(projects.features.nft.impl)

View file

@ -1,5 +0,0 @@
package com.tangem.features.askbiometry
interface AskBiometryFeatureToggles {
val isEnabled: Boolean
}

View file

@ -5,7 +5,7 @@ plugins {
}
android {
namespace = "com.tangem.features.askbiometry"
namespace = "com.tangem.features.biometry"
}
dependencies {

View file

@ -1,4 +1,4 @@
package com.tangem.features.askbiometry
package com.tangem.features.biometry
import com.tangem.core.decompose.factory.ComponentFactory
import com.tangem.core.ui.decompose.ComposableBottomSheetComponent

View file

@ -0,0 +1,5 @@
package com.tangem.features.biometry
interface BiometryFeatureToggles {
val isAskForBiometryEnabled: Boolean
}

View file

@ -8,11 +8,11 @@ plugins {
}
android {
namespace = "com.tangem.features.askbiometry.impl"
namespace = "com.tangem.features.biometry.impl"
}
dependencies {
api(projects.features.askBiometry.api)
api(projects.features.biometry.api)
/** Core modules */
implementation(projects.core.ui)

View file

@ -1,21 +1,26 @@
package com.tangem.features.askbiometry.impl
package com.tangem.features.biometry.impl
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.arkivanov.essenty.instancekeeper.getOrCreateSimple
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheet
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
import com.tangem.features.askbiometry.AskBiometryComponent
import com.tangem.features.askbiometry.impl.model.AskBiometryModel
import com.tangem.features.askbiometry.impl.ui.AskBiometry
import com.tangem.features.biometry.AskBiometryComponent
import com.tangem.features.biometry.impl.model.AskBiometryModel
import com.tangem.features.biometry.impl.ui.AskBiometry
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
internal class DefaultAskBiometryComponent @AssistedInject constructor(
@Assisted appComponentContext: AppComponentContext,
@ -23,15 +28,23 @@ internal class DefaultAskBiometryComponent @AssistedInject constructor(
) : AskBiometryComponent, AppComponentContext by appComponentContext {
private val model: AskBiometryModel = getOrCreateModel(params)
private val bsShown = instanceKeeper.getOrCreateSimple { MutableStateFlow(true) }
init {
model.dismissBSFlow
.onEach { bsShown.value = false }
.launchIn(componentScope)
}
override fun dismiss() = model.dismiss()
@Composable
override fun BottomSheet() {
val state by model.uiState.collectAsStateWithLifecycle()
val bsConfig = remember(this) {
val bsShown by bsShown.collectAsStateWithLifecycle()
val bsConfig = remember(this, bsShown) {
TangemBottomSheetConfig(
isShown = true,
isShown = bsShown,
onDismissRequest = ::dismiss,
content = TangemBottomSheetConfigContent.Empty,
)
@ -48,7 +61,7 @@ internal class DefaultAskBiometryComponent @AssistedInject constructor(
val state by model.uiState.collectAsStateWithLifecycle()
AskBiometry(
modifier = modifier,
modifier = modifier.navigationBarsPadding(),
state = state,
)
}

View file

@ -1,12 +1,12 @@
package com.tangem.features.askbiometry.impl
package com.tangem.features.biometry.impl
import com.tangem.core.configtoggle.feature.FeatureTogglesManager
import com.tangem.features.askbiometry.AskBiometryFeatureToggles
import com.tangem.features.biometry.BiometryFeatureToggles
import javax.inject.Inject
class DefaultAskBiometryFeatureToggles @Inject constructor(
internal class DefaultBiometryFeatureToggles @Inject constructor(
private val featureTogglesManager: FeatureTogglesManager,
) : AskBiometryFeatureToggles {
override val isEnabled: Boolean
) : BiometryFeatureToggles {
override val isAskForBiometryEnabled: Boolean
get() = featureTogglesManager.isFeatureEnabled("ASK_BIOMETRY_REFACTORING_ENABLED")
}

View file

@ -1,12 +1,12 @@
package com.tangem.features.askbiometry.impl.di
package com.tangem.features.biometry.impl.di
import com.tangem.core.decompose.di.ModelComponent
import com.tangem.core.decompose.model.Model
import com.tangem.features.askbiometry.AskBiometryComponent
import com.tangem.features.askbiometry.AskBiometryFeatureToggles
import com.tangem.features.askbiometry.impl.DefaultAskBiometryComponent
import com.tangem.features.askbiometry.impl.DefaultAskBiometryFeatureToggles
import com.tangem.features.askbiometry.impl.model.AskBiometryModel
import com.tangem.features.biometry.AskBiometryComponent
import com.tangem.features.biometry.BiometryFeatureToggles
import com.tangem.features.biometry.impl.DefaultAskBiometryComponent
import com.tangem.features.biometry.impl.DefaultBiometryFeatureToggles
import com.tangem.features.biometry.impl.model.AskBiometryModel
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
@ -19,7 +19,7 @@ import dagger.multibindings.IntoMap
internal interface FeatureModule {
@Binds
fun bindFeatureToggles(impl: DefaultAskBiometryFeatureToggles): AskBiometryFeatureToggles
fun bindFeatureToggles(impl: DefaultBiometryFeatureToggles): BiometryFeatureToggles
@Binds
fun bindComponentFactory(impl: DefaultAskBiometryComponent.Factory): AskBiometryComponent.Factory

View file

@ -1,4 +1,4 @@
package com.tangem.features.askbiometry.impl.model
package com.tangem.features.biometry.impl.model
import androidx.compose.runtime.Stable
import com.tangem.core.decompose.di.ModelScoped
@ -18,10 +18,12 @@ import com.tangem.domain.settings.repositories.SettingsRepository
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.repository.WalletsRepository
import com.tangem.features.askbiometry.AskBiometryComponent
import com.tangem.features.askbiometry.impl.ui.state.AskBiometryUM
import com.tangem.features.biometry.AskBiometryComponent
import com.tangem.features.biometry.impl.ui.state.AskBiometryUM
import com.tangem.sdk.api.TangemSdkManager
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
@ -52,9 +54,11 @@ internal class AskBiometryModel @Inject constructor(
bottomSheetVariant = params.bottomSheetVariant,
onAllowClick = ::onAllowClick,
onDontAllowClick = ::dontAllow,
onDismiss = ::dismiss,
),
)
val uiState = _uiState.asStateFlow()
val dismissBSFlow = MutableSharedFlow<Unit>()
init {
modelScope.launch {
@ -72,7 +76,11 @@ internal class AskBiometryModel @Inject constructor(
}
fun dismiss() {
dontAllow()
modelScope.launch {
dismissBSFlow.emit(Unit)
delay(timeMillis = 500)
dontAllow()
}
}
private fun allowToUseBiometrics() {
@ -107,6 +115,11 @@ internal class AskBiometryModel @Inject constructor(
isBiometricsRequestPolicy = userWallet.hasAccessCode,
)
if (_uiState.value.bottomSheetVariant) {
dismissBSFlow.emit(Unit)
delay(timeMillis = 500)
}
params.modelCallbacks.onAllowed()
}

View file

@ -1,4 +1,4 @@
package com.tangem.features.askbiometry.impl.ui
package com.tangem.features.biometry.impl.ui
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
@ -14,12 +14,11 @@ 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.components.atoms.Hand
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.features.askbiometry.impl.ui.state.AskBiometryUM
import com.tangem.features.askbiometry.impl.R
import com.tangem.features.biometry.impl.ui.state.AskBiometryUM
import com.tangem.features.biometry.R
@Composable
internal fun AskBiometry(state: AskBiometryUM, modifier: Modifier = Modifier) {
@ -31,15 +30,16 @@ internal fun AskBiometry(state: AskBiometryUM, modifier: Modifier = Modifier) {
modifier = Modifier.weight(1f),
) {
if (state.bottomSheetVariant) {
Header(onCloseClick = state.onDontAllowClick)
Header(onCloseClick = state.onDismiss)
}
SpacerH32()
SpacerHHalf()
Column(
modifier = Modifier.verticalScroll(rememberScrollState()),
) {
Title(modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing22))
Title(modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing56))
SpacerH32()
Description(
modifier = Modifier
@ -47,6 +47,8 @@ internal fun AskBiometry(state: AskBiometryUM, modifier: Modifier = Modifier) {
.fillMaxWidth(),
)
}
SpacerHHalf()
}
Footer(state = state)
SpacerH16()
@ -54,29 +56,23 @@ internal fun AskBiometry(state: AskBiometryUM, modifier: Modifier = Modifier) {
}
@Composable
private fun Header(onCloseClick: () -> Unit) {
Column(
modifier = Modifier.fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8),
private fun Header(onCloseClick: () -> Unit, modifier: Modifier = Modifier) {
Row(
modifier = modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.End,
) {
Hand()
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.End,
IconButton(
modifier = Modifier.size(TangemTheme.dimens.size32),
onClick = onCloseClick,
) {
IconButton(
modifier = Modifier.size(TangemTheme.dimens.size32),
onClick = onCloseClick,
) {
Icon(
modifier = Modifier.size(TangemTheme.dimens.size24),
painter = painterResource(id = R.drawable.ic_close_24),
tint = TangemTheme.colors.icon.secondary,
contentDescription = stringResourceSafe(id = R.string.common_cancel),
)
}
SpacerW8()
Icon(
modifier = Modifier.size(TangemTheme.dimens.size24),
painter = painterResource(id = R.drawable.ic_close_24),
tint = TangemTheme.colors.icon.secondary,
contentDescription = stringResourceSafe(id = R.string.common_cancel),
)
}
SpacerW8()
}
}

View file

@ -1,4 +1,4 @@
package com.tangem.features.askbiometry.impl.ui.state
package com.tangem.features.biometry.impl.ui.state
import com.tangem.core.ui.extensions.TextReference
@ -8,4 +8,5 @@ internal data class AskBiometryUM(
val error: TextReference? = null,
val onAllowClick: () -> Unit = {},
val onDontAllowClick: () -> Unit = {},
val onDismiss: () -> Unit = {},
)

View file

@ -15,6 +15,7 @@ dependencies {
/** Api */
implementation(projects.features.onboardingV2.api)
implementation(projects.features.manageTokens.api)
implementation(projects.features.biometry.api)
/** Core modules */
implementation(projects.core.configToggles)

View file

@ -14,11 +14,13 @@ import com.tangem.core.decompose.context.child
import com.tangem.core.decompose.context.childByContext
import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.core.decompose.navigation.inner.InnerNavigationHolder
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.features.onboarding.v2.entry.OnboardingEntryComponent
import com.tangem.features.onboarding.v2.entry.impl.model.OnboardingEntryModel
import com.tangem.features.onboarding.v2.entry.impl.routing.OnboardingChildFactory
import com.tangem.features.onboarding.v2.entry.impl.routing.OnboardingRoute
import com.tangem.features.onboarding.v2.entry.impl.ui.OnboardingEntry
import com.tangem.features.onboarding.v2.impl.R
import com.tangem.features.onboarding.v2.stepper.api.OnboardingStepperComponent
import com.tangem.utils.coroutines.JobHolder
import com.tangem.utils.coroutines.saveIn
@ -110,9 +112,19 @@ internal class DefaultOnboardingEntryComponent @AssistedInject constructor(
} else {
stepperComponent.state.update {
it.copy(
currentStep = if (stack.active.configuration is OnboardingRoute.ManageTokens) 7 else 8,
steps = 8,
title = model.titleProvider.currentTitle.value,
currentStep = when (stack.active.configuration) {
is OnboardingRoute.AskBiometry -> 7
is OnboardingRoute.ManageTokens -> 8
is OnboardingRoute.Done -> 9
else -> error("Unsupported route")
},
steps = 9,
title = when (stack.active.configuration) {
is OnboardingRoute.AskBiometry -> resourceReference(R.string.onboarding_navbar_save_wallet)
is OnboardingRoute.ManageTokens -> resourceReference(R.string.main_manage_tokens)
is OnboardingRoute.Done -> resourceReference(R.string.onboarding_done_header)
else -> error("Unsupported route")
},
showProgress = true,
)
}

View file

@ -0,0 +1,22 @@
package com.tangem.features.onboarding.v2.entry.impl.analytics
import com.tangem.core.analytics.models.AnalyticsEvent
internal sealed class OnboardingEntryEvent(
category: String,
event: String,
params: Map<String, String> = mapOf(),
) : AnalyticsEvent(category, event, params) {
data class Biometric(
val state: State,
) : OnboardingEntryEvent(
category = "Onboarding / Biometric",
event = "Biometry",
params = mapOf("State" to state.name),
) {
enum class State {
On, Off
}
}
}

View file

@ -1,8 +1,9 @@
package com.tangem.features.onboarding.v2.entry.impl.model
import com.arkivanov.decompose.router.stack.StackNavigation
import com.arkivanov.decompose.router.stack.navigate
import com.arkivanov.decompose.router.stack.replaceAll
import com.tangem.common.routing.AppRoute
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
@ -14,8 +15,11 @@ import com.tangem.domain.models.scan.ProductType
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.settings.repositories.SettingsRepository
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.features.biometry.AskBiometryComponent
import com.tangem.features.biometry.BiometryFeatureToggles
import com.tangem.features.onboarding.v2.TitleProvider
import com.tangem.features.onboarding.v2.entry.OnboardingEntryComponent
import com.tangem.features.onboarding.v2.entry.impl.analytics.OnboardingEntryEvent
import com.tangem.features.onboarding.v2.entry.impl.routing.OnboardingRoute
import com.tangem.features.onboarding.v2.multiwallet.api.OnboardingMultiWalletComponent
import com.tangem.sdk.api.TangemSdkManager
@ -26,6 +30,7 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.launch
import javax.inject.Inject
@Suppress("LongParameterList")
@ModelScoped
internal class OnboardingEntryModel @Inject constructor(
paramsContainer: ParamsContainer,
@ -33,6 +38,8 @@ internal class OnboardingEntryModel @Inject constructor(
private val router: Router,
private val tangemSdkManager: TangemSdkManager,
private val settingsRepository: SettingsRepository,
private val askBiometryFeatureToggles: BiometryFeatureToggles,
private val analyticsEventHandler: AnalyticsEventHandler,
) : Model() {
private val params = paramsContainer.require<OnboardingEntryComponent.Params>()
@ -78,7 +85,7 @@ internal class OnboardingEntryModel @Inject constructor(
ProductType.Note -> OnboardingRoute.Note(
scanResponse = scanResponse,
titleProvider = titleProvider,
onDone = ::navigateToWalletScreen,
onDone = ::navigateToFinalScreenFlow,
)
else -> error("Unsupported")
}
@ -86,34 +93,58 @@ internal class OnboardingEntryModel @Inject constructor(
private fun onMultiWalletOnboardingDone(userWallet: UserWallet) {
if (userWallet.scanResponse.cardTypesResolver.isMultiwalletAllowed()) {
stackNavigation.navigate {
listOf(OnboardingRoute.ManageTokens(userWallet))
}
stackNavigation.replaceAll(OnboardingRoute.ManageTokens(userWallet))
} else {
stackNavigation.navigate {
listOf(OnboardingRoute.Done(onDone = ::navigateToWalletScreen))
}
navigateToFinalScreenFlow()
}
}
fun onManageTokensDone() {
stackNavigation.navigate {
listOf(OnboardingRoute.Done(onDone = ::navigateToWalletScreen))
}
navigateToFinalScreenFlow()
}
private fun onVisaOnboardingDone() {
stackNavigation.navigate {
listOf(OnboardingRoute.Done(onDone = ::navigateToWalletScreen))
navigateToFinalScreenFlow()
}
private fun navigateToFinalScreenFlow() {
if (askBiometryFeatureToggles.isAskForBiometryEnabled) {
modelScope.launch {
if (tangemSdkManager.checkCanUseBiometry() && settingsRepository.shouldShowSaveUserWalletScreen()) {
stackNavigation.replaceAll(
OnboardingRoute.AskBiometry(modelCallbacks = AskBiometryModelCallbacks()),
)
} else {
stackNavigation.replaceAll(OnboardingRoute.Done(onDone = ::navigateToWalletScreen))
}
}
} else {
stackNavigation.replaceAll(OnboardingRoute.Done(onDone = ::navigateToWalletScreen))
}
}
inner class AskBiometryModelCallbacks : AskBiometryComponent.ModelCallbacks {
override fun onAllowed() {
analyticsEventHandler.send(OnboardingEntryEvent.Biometric(OnboardingEntryEvent.Biometric.State.On))
stackNavigation.replaceAll(OnboardingRoute.Done(onDone = ::navigateToWalletScreen))
}
override fun onDenied() {
analyticsEventHandler.send(OnboardingEntryEvent.Biometric(OnboardingEntryEvent.Biometric.State.Off))
stackNavigation.replaceAll(OnboardingRoute.Done(onDone = ::navigateToWalletScreen))
}
}
private fun navigateToWalletScreen() {
modelScope.launch(NonCancellable) {
if (askBiometryFeatureToggles.isAskForBiometryEnabled) {
router.replaceAll(AppRoute.Wallet)
if (tangemSdkManager.checkCanUseBiometry() && settingsRepository.shouldShowSaveUserWalletScreen()) {
delay(timeMillis = 1_800)
router.push(AppRoute.SaveWallet)
} else {
modelScope.launch(NonCancellable) {
router.replaceAll(AppRoute.Wallet)
if (tangemSdkManager.checkCanUseBiometry() && settingsRepository.shouldShowSaveUserWalletScreen()) {
delay(timeMillis = 1_800)
router.push(AppRoute.SaveWallet)
}
}
}
}

View file

@ -1,6 +1,7 @@
package com.tangem.features.onboarding.v2.entry.impl.routing
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.features.biometry.AskBiometryComponent
import com.tangem.features.managetokens.component.OnboardingManageTokensComponent
import com.tangem.features.onboarding.v2.done.api.OnboardingDoneComponent
import com.tangem.features.onboarding.v2.multiwallet.api.OnboardingMultiWalletComponent
@ -13,6 +14,7 @@ internal class OnboardingChildFactory @Inject constructor(
private val onboardingManageTokensComponentFactory: OnboardingManageTokensComponent.Factory,
private val onboardingDoneComponentFactory: OnboardingDoneComponent.Factory,
private val onBoardingVisaComponentFactory: OnboardingVisaComponent.Factory,
private val askBiometryComponentFactory: AskBiometryComponent.Factory,
private val onboardingNoteComponentFactory: OnboardingNoteComponent.Factory,
) {
@ -57,7 +59,14 @@ internal class OnboardingChildFactory @Inject constructor(
onDone = route.onDone,
),
)
else -> Unit
is OnboardingRoute.AskBiometry -> askBiometryComponentFactory.create(
context = childContext,
params = AskBiometryComponent.Params(
bottomSheetVariant = false,
modelCallbacks = route.modelCallbacks,
),
)
is OnboardingRoute.None -> Unit
}
}
}

View file

@ -3,6 +3,7 @@ package com.tangem.features.onboarding.v2.entry.impl.routing
import com.tangem.core.decompose.navigation.Route
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.features.biometry.AskBiometryComponent
import com.tangem.features.onboarding.v2.TitleProvider
import com.tangem.features.onboarding.v2.multiwallet.api.OnboardingMultiWalletComponent
@ -34,6 +35,10 @@ sealed class OnboardingRoute : Route {
val userWallet: UserWallet,
) : OnboardingRoute()
data class AskBiometry(
val modelCallbacks: AskBiometryComponent.ModelCallbacks,
) : OnboardingRoute()
data class Done(
val onDone: () -> Unit,
) : OnboardingRoute()

View file

@ -11,7 +11,6 @@ import com.arkivanov.decompose.router.stack.ChildStack
import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.core.ui.res.TangemTheme
import com.tangem.features.onboarding.v2.entry.impl.routing.OnboardingRoute
import com.tangem.features.onboarding.v2.multiwallet.api.OnboardingMultiWalletComponent
@Composable
internal inline fun OnboardingEntry(
@ -31,34 +30,7 @@ internal inline fun OnboardingEntry(
stack = childStack,
animation = stackAnimation(slide()),
) {
when (it.configuration) {
is OnboardingRoute.MultiWallet -> {
(it.instance as OnboardingMultiWalletComponent).Content(
modifier = modifier,
)
}
is OnboardingRoute.ManageTokens -> {
(it.instance as ComposableContentComponent).Content(
modifier = modifier,
)
}
is OnboardingRoute.Done -> {
(it.instance as ComposableContentComponent).Content(
modifier = modifier,
)
}
is OnboardingRoute.Visa -> {
(it.instance as ComposableContentComponent).Content(
modifier = modifier,
)
}
is OnboardingRoute.Note -> {
(it.instance as ComposableContentComponent).Content(
modifier = modifier,
)
}
is OnboardingRoute.None -> {}
}
(it.instance as? ComposableContentComponent)?.Content(modifier = modifier)
}
}
}

View file

@ -70,7 +70,7 @@ internal class DefaultOnboardingMultiWalletComponent @AssistedInject constructor
private val innerNavigationStateFlow = instanceKeeper.getOrCreateSimple(key = "innerNavigationState") {
MutableStateFlow(
MultiWalletInnerNavigationState(1, 5),
MultiWalletInnerNavigationState(1, 6),
)
}

View file

@ -28,7 +28,7 @@ class MultiWalletBackupComponent(
params.innerNavigation.update {
it.copy(
stackSize = 5,
stackMaxSize = 8,
stackMaxSize = 9,
)
}

View file

@ -27,7 +27,7 @@ class Wallet1ChooseOptionComponent(
params.innerNavigation.update {
it.copy(
stackSize = 3,
stackMaxSize = 8,
stackMaxSize = 9,
)
}

View file

@ -30,7 +30,7 @@ internal class MultiWalletCreateWalletComponent(
params.innerNavigation.update {
it.copy(
stackSize = 2,
stackMaxSize = 8,
stackMaxSize = 9,
)
}

View file

@ -32,7 +32,7 @@ internal class MultiWalletFinalizeComponent(
params.innerNavigation.update {
it.copy(
stackSize = 7,
stackMaxSize = 8,
stackMaxSize = 9,
)
}
params.parentParams.titleProvider.changeTitle(

View file

@ -25,7 +25,7 @@ internal class Wallet1ScanPrimaryComponent(
params.innerNavigation.update {
it.copy(
stackSize = 4,
stackMaxSize = 8,
stackMaxSize = 9,
)
}

View file

@ -39,7 +39,7 @@ internal class MultiWalletSeedPhraseComponent(
params.innerNavigation.update { st ->
st.copy(
stackSize = 3 + it.order,
stackMaxSize = 10,
stackMaxSize = 11,
)
}

View file

@ -41,6 +41,7 @@ internal class SetConfirmationStateInitTransformer(
private val isImplicitExit: Boolean
get() = pendingAction == null && pendingActions?.isEmpty() == true || isTronStakedBalance
@Suppress("CyclomaticComplexMethod")
override fun transform(prevState: StakingUiState): StakingUiState {
val actionType = when {
isEnter -> StakingActionCommonType.Enter(

View file

@ -54,6 +54,7 @@ internal class AddStakingNotificationsTransformer(
isSubtractAvailable = isSubtractAvailable,
)
@Suppress("LongMethod")
override fun transform(prevState: StakingUiState): StakingUiState {
val cryptoCurrencyStatus = cryptoCurrencyStatusProvider()
val balance = cryptoCurrencyStatus.value.amount.orZero()

View file

@ -107,6 +107,7 @@ dependencies {
implementation(projects.features.tokendetails.api)
implementation(projects.features.wallet.api)
implementation(projects.features.walletSettings.api)
implementation(projects.features.biometry.api)
/** Common modules */
implementation(projects.common)

View file

@ -13,13 +13,16 @@ import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.context.child
import com.tangem.core.decompose.context.childByContext
import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.core.ui.decompose.ComposableBottomSheetComponent
import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.core.ui.decompose.ComposableDialogComponent
import com.tangem.core.ui.utils.findActivity
import com.tangem.feature.wallet.child.wallet.model.WalletModel
import com.tangem.feature.wallet.navigation.WalletRoute
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletDialogConfig
import com.tangem.feature.wallet.presentation.wallet.ui.WalletScreen
import com.tangem.feature.walletsettings.component.RenameWalletComponent
import com.tangem.features.biometry.AskBiometryComponent
import com.tangem.features.markets.entry.MarketsEntryComponent
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
@ -31,6 +34,7 @@ internal class WalletComponent @AssistedInject constructor(
@Assisted navigate: (WalletRoute) -> Unit,
private val renameWalletComponentFactory: RenameWalletComponent.Factory,
private val marketsEntryComponentFactory: MarketsEntryComponent.Factory,
private val askBiometryComponentFactory: AskBiometryComponent.Factory,
) : ComposableContentComponent, AppComponentContext by appComponentContext {
private val model: WalletModel = getOrCreateModel()
@ -56,6 +60,15 @@ internal class WalletComponent @AssistedInject constructor(
),
)
}
is WalletDialogConfig.AskForBiometry -> {
askBiometryComponentFactory.create(
context = childByContext(componentContext),
params = AskBiometryComponent.Params(
bottomSheetVariant = true,
modelCallbacks = model.askBiometryModelCallbacks,
),
)
}
}
},
)
@ -74,7 +87,11 @@ internal class WalletComponent @AssistedInject constructor(
marketsEntryComponent = marketsEntryComponent,
)
dialog.child?.instance?.Dialog()
when (val dialog = dialog.child?.instance) {
is ComposableDialogComponent -> dialog.Dialog()
is ComposableBottomSheetComponent -> dialog.BottomSheet()
else -> {}
}
}
@AssistedFactory

View file

@ -2,7 +2,11 @@ package com.tangem.feature.wallet.child.wallet.model
import androidx.compose.runtime.Stable
import arrow.core.getOrElse
import com.arkivanov.decompose.router.slot.activate
import com.arkivanov.decompose.router.slot.dismiss
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.analytics.models.AnalyticsParam
import com.tangem.core.analytics.models.event.MainScreenAnalyticsEvent
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.Model
import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase
@ -23,12 +27,15 @@ import com.tangem.feature.wallet.presentation.wallet.domain.WalletNameMigrationU
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.PushNotificationsBottomSheetConfig
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.model.WalletEvent.DemonstrateWalletsScrollPreview.Direction
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletScreenState
import com.tangem.feature.wallet.presentation.wallet.state.transformers.*
import com.tangem.feature.wallet.presentation.wallet.state.utils.WalletEventSender
import com.tangem.feature.wallet.presentation.wallet.utils.ScreenLifecycleProvider
import com.tangem.features.biometry.AskBiometryComponent
import com.tangem.features.biometry.BiometryFeatureToggles
import com.tangem.features.pushnotifications.api.utils.PUSH_PERMISSION
import com.tangem.features.pushnotifications.api.utils.getPushPermissionOrNull
import com.tangem.features.wallet.featuretoggles.WalletFeatureToggles
@ -67,11 +74,13 @@ internal class WalletModel @Inject constructor(
private val tokenListStore: MultiWalletTokenListStore,
private val onrampStatusFactory: OnrampStatusFactory,
private val walletFeatureToggles: WalletFeatureToggles,
private val biometryFeatureToggles: BiometryFeatureToggles,
private val analyticsEventsHandler: AnalyticsEventHandler,
val screenLifecycleProvider: ScreenLifecycleProvider,
val innerWalletRouter: InnerWalletRouter,
analyticsEventsHandler: AnalyticsEventHandler,
) : Model() {
val askBiometryModelCallbacks = AskBiometryModelCallbacks()
val uiState: StateFlow<WalletScreenState> = stateHolder.uiState
private val walletsUpdateJobHolder = JobHolder()
@ -116,7 +125,15 @@ internal class WalletModel @Inject constructor(
modelScope.launch(dispatchers.main) {
withContext(dispatchers.io) { delay(timeMillis = 1_800) }
if (isShowSaveWalletScreenEnabled()) innerWalletRouter.openSaveUserWalletScreen()
if (isShowSaveWalletScreenEnabled()) {
if (biometryFeatureToggles.isAskForBiometryEnabled) {
innerWalletRouter.dialogNavigation.activate(
configuration = WalletDialogConfig.AskForBiometry,
)
} else {
innerWalletRouter.openSaveUserWalletScreen()
}
}
}
}
@ -446,6 +463,18 @@ internal class WalletModel @Inject constructor(
)
}
inner class AskBiometryModelCallbacks : AskBiometryComponent.ModelCallbacks {
override fun onAllowed() {
analyticsEventsHandler.send(MainScreenAnalyticsEvent.EnableBiometrics(AnalyticsParam.OnOffState.On))
innerWalletRouter.dialogNavigation.dismiss()
}
override fun onDenied() {
analyticsEventsHandler.send(MainScreenAnalyticsEvent.EnableBiometrics(AnalyticsParam.OnOffState.Off))
innerWalletRouter.dialogNavigation.dismiss()
}
}
private companion object {
const val REFRESH_WALLET_BACKGROUND_TIMER_MILLIS = 10000L
const val EXPRESS_STATUS_UPDATE_DELAY = 10000L

View file

@ -13,6 +13,7 @@ import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.feature.wallet.navigation.WalletRoute
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletDialogConfig
import com.tangem.features.biometry.BiometryFeatureToggles
import com.tangem.features.onboarding.v2.OnboardingV2FeatureToggles
import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.flow.MutableSharedFlow
@ -25,6 +26,7 @@ internal class DefaultWalletRouter @Inject constructor(
private val urlOpener: UrlOpener,
private val reduxStateHolder: ReduxStateHolder,
private val onboardingV2FeatureToggles: OnboardingV2FeatureToggles,
private val biometryFeatureToggles: BiometryFeatureToggles,
) : InnerWalletRouter {
override val dialogNavigation: SlotNavigation<WalletDialogConfig> = SlotNavigation()
@ -98,7 +100,9 @@ internal class DefaultWalletRouter @Inject constructor(
}
override fun openSaveUserWalletScreen() {
router.push(AppRoute.SaveWallet)
if (biometryFeatureToggles.isAskForBiometryEnabled.not()) {
router.push(AppRoute.SaveWallet)
}
}
override fun isWalletLastScreen(): Boolean {

View file

@ -13,4 +13,7 @@ internal sealed interface WalletDialogConfig {
@Serializable
data class RenameWallet(val userWalletId: UserWalletId, val currentName: String) : WalletDialogConfig
@Serializable
data object AskForBiometry : WalletDialogConfig
}

View file

@ -228,8 +228,8 @@ include(":features:stories:impl")
include(":features:txhistory:api")
include(":features:txhistory:impl")
include(":features:ask-biometry:api")
include(":features:ask-biometry:impl")
include(":features:biometry:api")
include(":features:biometry:impl")
include(":features:nft:api")
include(":features:nft:impl")