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.stories.impl)
implementation(projects.features.txhistory.api) implementation(projects.features.txhistory.api)
implementation(projects.features.txhistory.impl) implementation(projects.features.txhistory.impl)
implementation(projects.features.askBiometry.api) implementation(projects.features.biometry.api)
implementation(projects.features.askBiometry.impl) implementation(projects.features.biometry.impl)
implementation(projects.features.nft.api) implementation(projects.features.nft.api)
implementation(projects.features.nft.impl) 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 { android {
namespace = "com.tangem.features.askbiometry" namespace = "com.tangem.features.biometry"
} }
dependencies { 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.decompose.factory.ComponentFactory
import com.tangem.core.ui.decompose.ComposableBottomSheetComponent 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 { android {
namespace = "com.tangem.features.askbiometry.impl" namespace = "com.tangem.features.biometry.impl"
} }
dependencies { dependencies {
api(projects.features.askBiometry.api) api(projects.features.biometry.api)
/** Core modules */ /** Core modules */
implementation(projects.core.ui) 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.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.arkivanov.essenty.instancekeeper.getOrCreateSimple
import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.model.getOrCreateModel import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheet import com.tangem.core.ui.components.bottomsheets.TangemBottomSheet
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
import com.tangem.features.askbiometry.AskBiometryComponent import com.tangem.features.biometry.AskBiometryComponent
import com.tangem.features.askbiometry.impl.model.AskBiometryModel import com.tangem.features.biometry.impl.model.AskBiometryModel
import com.tangem.features.askbiometry.impl.ui.AskBiometry import com.tangem.features.biometry.impl.ui.AskBiometry
import dagger.assisted.Assisted import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject import dagger.assisted.AssistedInject
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
internal class DefaultAskBiometryComponent @AssistedInject constructor( internal class DefaultAskBiometryComponent @AssistedInject constructor(
@Assisted appComponentContext: AppComponentContext, @Assisted appComponentContext: AppComponentContext,
@ -23,15 +28,23 @@ internal class DefaultAskBiometryComponent @AssistedInject constructor(
) : AskBiometryComponent, AppComponentContext by appComponentContext { ) : AskBiometryComponent, AppComponentContext by appComponentContext {
private val model: AskBiometryModel = getOrCreateModel(params) 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() override fun dismiss() = model.dismiss()
@Composable @Composable
override fun BottomSheet() { override fun BottomSheet() {
val state by model.uiState.collectAsStateWithLifecycle() val state by model.uiState.collectAsStateWithLifecycle()
val bsConfig = remember(this) { val bsShown by bsShown.collectAsStateWithLifecycle()
val bsConfig = remember(this, bsShown) {
TangemBottomSheetConfig( TangemBottomSheetConfig(
isShown = true, isShown = bsShown,
onDismissRequest = ::dismiss, onDismissRequest = ::dismiss,
content = TangemBottomSheetConfigContent.Empty, content = TangemBottomSheetConfigContent.Empty,
) )
@ -48,7 +61,7 @@ internal class DefaultAskBiometryComponent @AssistedInject constructor(
val state by model.uiState.collectAsStateWithLifecycle() val state by model.uiState.collectAsStateWithLifecycle()
AskBiometry( AskBiometry(
modifier = modifier, modifier = modifier.navigationBarsPadding(),
state = state, 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.core.configtoggle.feature.FeatureTogglesManager
import com.tangem.features.askbiometry.AskBiometryFeatureToggles import com.tangem.features.biometry.BiometryFeatureToggles
import javax.inject.Inject import javax.inject.Inject
class DefaultAskBiometryFeatureToggles @Inject constructor( internal class DefaultBiometryFeatureToggles @Inject constructor(
private val featureTogglesManager: FeatureTogglesManager, private val featureTogglesManager: FeatureTogglesManager,
) : AskBiometryFeatureToggles { ) : BiometryFeatureToggles {
override val isEnabled: Boolean override val isAskForBiometryEnabled: Boolean
get() = featureTogglesManager.isFeatureEnabled("ASK_BIOMETRY_REFACTORING_ENABLED") 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.di.ModelComponent
import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.Model
import com.tangem.features.askbiometry.AskBiometryComponent import com.tangem.features.biometry.AskBiometryComponent
import com.tangem.features.askbiometry.AskBiometryFeatureToggles import com.tangem.features.biometry.BiometryFeatureToggles
import com.tangem.features.askbiometry.impl.DefaultAskBiometryComponent import com.tangem.features.biometry.impl.DefaultAskBiometryComponent
import com.tangem.features.askbiometry.impl.DefaultAskBiometryFeatureToggles import com.tangem.features.biometry.impl.DefaultBiometryFeatureToggles
import com.tangem.features.askbiometry.impl.model.AskBiometryModel import com.tangem.features.biometry.impl.model.AskBiometryModel
import dagger.Binds import dagger.Binds
import dagger.Module import dagger.Module
import dagger.hilt.InstallIn import dagger.hilt.InstallIn
@ -19,7 +19,7 @@ import dagger.multibindings.IntoMap
internal interface FeatureModule { internal interface FeatureModule {
@Binds @Binds
fun bindFeatureToggles(impl: DefaultAskBiometryFeatureToggles): AskBiometryFeatureToggles fun bindFeatureToggles(impl: DefaultBiometryFeatureToggles): BiometryFeatureToggles
@Binds @Binds
fun bindComponentFactory(impl: DefaultAskBiometryComponent.Factory): AskBiometryComponent.Factory 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 androidx.compose.runtime.Stable
import com.tangem.core.decompose.di.ModelScoped 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.legacy.UserWalletsListManager
import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.repository.WalletsRepository import com.tangem.domain.wallets.repository.WalletsRepository
import com.tangem.features.askbiometry.AskBiometryComponent import com.tangem.features.biometry.AskBiometryComponent
import com.tangem.features.askbiometry.impl.ui.state.AskBiometryUM import com.tangem.features.biometry.impl.ui.state.AskBiometryUM
import com.tangem.sdk.api.TangemSdkManager import com.tangem.sdk.api.TangemSdkManager
import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
@ -52,9 +54,11 @@ internal class AskBiometryModel @Inject constructor(
bottomSheetVariant = params.bottomSheetVariant, bottomSheetVariant = params.bottomSheetVariant,
onAllowClick = ::onAllowClick, onAllowClick = ::onAllowClick,
onDontAllowClick = ::dontAllow, onDontAllowClick = ::dontAllow,
onDismiss = ::dismiss,
), ),
) )
val uiState = _uiState.asStateFlow() val uiState = _uiState.asStateFlow()
val dismissBSFlow = MutableSharedFlow<Unit>()
init { init {
modelScope.launch { modelScope.launch {
@ -72,7 +76,11 @@ internal class AskBiometryModel @Inject constructor(
} }
fun dismiss() { fun dismiss() {
dontAllow() modelScope.launch {
dismissBSFlow.emit(Unit)
delay(timeMillis = 500)
dontAllow()
}
} }
private fun allowToUseBiometrics() { private fun allowToUseBiometrics() {
@ -107,6 +115,11 @@ internal class AskBiometryModel @Inject constructor(
isBiometricsRequestPolicy = userWallet.hasAccessCode, isBiometricsRequestPolicy = userWallet.hasAccessCode,
) )
if (_uiState.value.bottomSheetVariant) {
dismissBSFlow.emit(Unit)
delay(timeMillis = 500)
}
params.modelCallbacks.onAllowed() 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.layout.*
import androidx.compose.foundation.rememberScrollState 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.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import com.tangem.core.ui.components.* 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.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.features.askbiometry.impl.ui.state.AskBiometryUM import com.tangem.features.biometry.impl.ui.state.AskBiometryUM
import com.tangem.features.askbiometry.impl.R import com.tangem.features.biometry.R
@Composable @Composable
internal fun AskBiometry(state: AskBiometryUM, modifier: Modifier = Modifier) { internal fun AskBiometry(state: AskBiometryUM, modifier: Modifier = Modifier) {
@ -31,15 +30,16 @@ internal fun AskBiometry(state: AskBiometryUM, modifier: Modifier = Modifier) {
modifier = Modifier.weight(1f), modifier = Modifier.weight(1f),
) { ) {
if (state.bottomSheetVariant) { if (state.bottomSheetVariant) {
Header(onCloseClick = state.onDontAllowClick) Header(onCloseClick = state.onDismiss)
} }
SpacerH32() SpacerH32()
SpacerHHalf()
Column( Column(
modifier = Modifier.verticalScroll(rememberScrollState()), modifier = Modifier.verticalScroll(rememberScrollState()),
) { ) {
Title(modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing22)) Title(modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing56))
SpacerH32() SpacerH32()
Description( Description(
modifier = Modifier modifier = Modifier
@ -47,6 +47,8 @@ internal fun AskBiometry(state: AskBiometryUM, modifier: Modifier = Modifier) {
.fillMaxWidth(), .fillMaxWidth(),
) )
} }
SpacerHHalf()
} }
Footer(state = state) Footer(state = state)
SpacerH16() SpacerH16()
@ -54,29 +56,23 @@ internal fun AskBiometry(state: AskBiometryUM, modifier: Modifier = Modifier) {
} }
@Composable @Composable
private fun Header(onCloseClick: () -> Unit) { private fun Header(onCloseClick: () -> Unit, modifier: Modifier = Modifier) {
Column( Row(
modifier = Modifier.fillMaxWidth(), modifier = modifier.fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8), horizontalArrangement = Arrangement.End,
) { ) {
Hand() IconButton(
Row( modifier = Modifier.size(TangemTheme.dimens.size32),
modifier = Modifier.fillMaxWidth(), onClick = onCloseClick,
horizontalArrangement = Arrangement.End,
) { ) {
IconButton( Icon(
modifier = Modifier.size(TangemTheme.dimens.size32), modifier = Modifier.size(TangemTheme.dimens.size24),
onClick = onCloseClick, painter = painterResource(id = R.drawable.ic_close_24),
) { tint = TangemTheme.colors.icon.secondary,
Icon( contentDescription = stringResourceSafe(id = R.string.common_cancel),
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()
} }
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 import com.tangem.core.ui.extensions.TextReference
@ -8,4 +8,5 @@ internal data class AskBiometryUM(
val error: TextReference? = null, val error: TextReference? = null,
val onAllowClick: () -> Unit = {}, val onAllowClick: () -> Unit = {},
val onDontAllowClick: () -> Unit = {}, val onDontAllowClick: () -> Unit = {},
val onDismiss: () -> Unit = {},
) )

View file

@ -15,6 +15,7 @@ dependencies {
/** Api */ /** Api */
implementation(projects.features.onboardingV2.api) implementation(projects.features.onboardingV2.api)
implementation(projects.features.manageTokens.api) implementation(projects.features.manageTokens.api)
implementation(projects.features.biometry.api)
/** Core modules */ /** Core modules */
implementation(projects.core.configToggles) 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.context.childByContext
import com.tangem.core.decompose.model.getOrCreateModel import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.core.decompose.navigation.inner.InnerNavigationHolder 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.OnboardingEntryComponent
import com.tangem.features.onboarding.v2.entry.impl.model.OnboardingEntryModel 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.OnboardingChildFactory
import com.tangem.features.onboarding.v2.entry.impl.routing.OnboardingRoute 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.entry.impl.ui.OnboardingEntry
import com.tangem.features.onboarding.v2.impl.R
import com.tangem.features.onboarding.v2.stepper.api.OnboardingStepperComponent import com.tangem.features.onboarding.v2.stepper.api.OnboardingStepperComponent
import com.tangem.utils.coroutines.JobHolder import com.tangem.utils.coroutines.JobHolder
import com.tangem.utils.coroutines.saveIn import com.tangem.utils.coroutines.saveIn
@ -110,9 +112,19 @@ internal class DefaultOnboardingEntryComponent @AssistedInject constructor(
} else { } else {
stepperComponent.state.update { stepperComponent.state.update {
it.copy( it.copy(
currentStep = if (stack.active.configuration is OnboardingRoute.ManageTokens) 7 else 8, currentStep = when (stack.active.configuration) {
steps = 8, is OnboardingRoute.AskBiometry -> 7
title = model.titleProvider.currentTitle.value, 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, 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 package com.tangem.features.onboarding.v2.entry.impl.model
import com.arkivanov.decompose.router.stack.StackNavigation 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.common.routing.AppRoute
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer 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.models.scan.ScanResponse
import com.tangem.domain.settings.repositories.SettingsRepository import com.tangem.domain.settings.repositories.SettingsRepository
import com.tangem.domain.wallets.models.UserWallet 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.TitleProvider
import com.tangem.features.onboarding.v2.entry.OnboardingEntryComponent 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.entry.impl.routing.OnboardingRoute
import com.tangem.features.onboarding.v2.multiwallet.api.OnboardingMultiWalletComponent import com.tangem.features.onboarding.v2.multiwallet.api.OnboardingMultiWalletComponent
import com.tangem.sdk.api.TangemSdkManager import com.tangem.sdk.api.TangemSdkManager
@ -26,6 +30,7 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import javax.inject.Inject import javax.inject.Inject
@Suppress("LongParameterList")
@ModelScoped @ModelScoped
internal class OnboardingEntryModel @Inject constructor( internal class OnboardingEntryModel @Inject constructor(
paramsContainer: ParamsContainer, paramsContainer: ParamsContainer,
@ -33,6 +38,8 @@ internal class OnboardingEntryModel @Inject constructor(
private val router: Router, private val router: Router,
private val tangemSdkManager: TangemSdkManager, private val tangemSdkManager: TangemSdkManager,
private val settingsRepository: SettingsRepository, private val settingsRepository: SettingsRepository,
private val askBiometryFeatureToggles: BiometryFeatureToggles,
private val analyticsEventHandler: AnalyticsEventHandler,
) : Model() { ) : Model() {
private val params = paramsContainer.require<OnboardingEntryComponent.Params>() private val params = paramsContainer.require<OnboardingEntryComponent.Params>()
@ -78,7 +85,7 @@ internal class OnboardingEntryModel @Inject constructor(
ProductType.Note -> OnboardingRoute.Note( ProductType.Note -> OnboardingRoute.Note(
scanResponse = scanResponse, scanResponse = scanResponse,
titleProvider = titleProvider, titleProvider = titleProvider,
onDone = ::navigateToWalletScreen, onDone = ::navigateToFinalScreenFlow,
) )
else -> error("Unsupported") else -> error("Unsupported")
} }
@ -86,34 +93,58 @@ internal class OnboardingEntryModel @Inject constructor(
private fun onMultiWalletOnboardingDone(userWallet: UserWallet) { private fun onMultiWalletOnboardingDone(userWallet: UserWallet) {
if (userWallet.scanResponse.cardTypesResolver.isMultiwalletAllowed()) { if (userWallet.scanResponse.cardTypesResolver.isMultiwalletAllowed()) {
stackNavigation.navigate { stackNavigation.replaceAll(OnboardingRoute.ManageTokens(userWallet))
listOf(OnboardingRoute.ManageTokens(userWallet))
}
} else { } else {
stackNavigation.navigate { navigateToFinalScreenFlow()
listOf(OnboardingRoute.Done(onDone = ::navigateToWalletScreen))
}
} }
} }
fun onManageTokensDone() { fun onManageTokensDone() {
stackNavigation.navigate { navigateToFinalScreenFlow()
listOf(OnboardingRoute.Done(onDone = ::navigateToWalletScreen))
}
} }
private fun onVisaOnboardingDone() { private fun onVisaOnboardingDone() {
stackNavigation.navigate { navigateToFinalScreenFlow()
listOf(OnboardingRoute.Done(onDone = ::navigateToWalletScreen)) }
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() { private fun navigateToWalletScreen() {
modelScope.launch(NonCancellable) { if (askBiometryFeatureToggles.isAskForBiometryEnabled) {
router.replaceAll(AppRoute.Wallet) router.replaceAll(AppRoute.Wallet)
if (tangemSdkManager.checkCanUseBiometry() && settingsRepository.shouldShowSaveUserWalletScreen()) { } else {
delay(timeMillis = 1_800) modelScope.launch(NonCancellable) {
router.push(AppRoute.SaveWallet) 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 package com.tangem.features.onboarding.v2.entry.impl.routing
import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.features.biometry.AskBiometryComponent
import com.tangem.features.managetokens.component.OnboardingManageTokensComponent import com.tangem.features.managetokens.component.OnboardingManageTokensComponent
import com.tangem.features.onboarding.v2.done.api.OnboardingDoneComponent import com.tangem.features.onboarding.v2.done.api.OnboardingDoneComponent
import com.tangem.features.onboarding.v2.multiwallet.api.OnboardingMultiWalletComponent 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 onboardingManageTokensComponentFactory: OnboardingManageTokensComponent.Factory,
private val onboardingDoneComponentFactory: OnboardingDoneComponent.Factory, private val onboardingDoneComponentFactory: OnboardingDoneComponent.Factory,
private val onBoardingVisaComponentFactory: OnboardingVisaComponent.Factory, private val onBoardingVisaComponentFactory: OnboardingVisaComponent.Factory,
private val askBiometryComponentFactory: AskBiometryComponent.Factory,
private val onboardingNoteComponentFactory: OnboardingNoteComponent.Factory, private val onboardingNoteComponentFactory: OnboardingNoteComponent.Factory,
) { ) {
@ -57,7 +59,14 @@ internal class OnboardingChildFactory @Inject constructor(
onDone = route.onDone, 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.core.decompose.navigation.Route
import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.wallets.models.UserWallet 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.TitleProvider
import com.tangem.features.onboarding.v2.multiwallet.api.OnboardingMultiWalletComponent import com.tangem.features.onboarding.v2.multiwallet.api.OnboardingMultiWalletComponent
@ -34,6 +35,10 @@ sealed class OnboardingRoute : Route {
val userWallet: UserWallet, val userWallet: UserWallet,
) : OnboardingRoute() ) : OnboardingRoute()
data class AskBiometry(
val modelCallbacks: AskBiometryComponent.ModelCallbacks,
) : OnboardingRoute()
data class Done( data class Done(
val onDone: () -> Unit, val onDone: () -> Unit,
) : OnboardingRoute() ) : 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.decompose.ComposableContentComponent
import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemTheme
import com.tangem.features.onboarding.v2.entry.impl.routing.OnboardingRoute import com.tangem.features.onboarding.v2.entry.impl.routing.OnboardingRoute
import com.tangem.features.onboarding.v2.multiwallet.api.OnboardingMultiWalletComponent
@Composable @Composable
internal inline fun OnboardingEntry( internal inline fun OnboardingEntry(
@ -31,34 +30,7 @@ internal inline fun OnboardingEntry(
stack = childStack, stack = childStack,
animation = stackAnimation(slide()), animation = stackAnimation(slide()),
) { ) {
when (it.configuration) { (it.instance as? ComposableContentComponent)?.Content(modifier = modifier)
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 -> {}
}
} }
} }
} }

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -107,6 +107,7 @@ dependencies {
implementation(projects.features.tokendetails.api) implementation(projects.features.tokendetails.api)
implementation(projects.features.wallet.api) implementation(projects.features.wallet.api)
implementation(projects.features.walletSettings.api) implementation(projects.features.walletSettings.api)
implementation(projects.features.biometry.api)
/** Common modules */ /** Common modules */
implementation(projects.common) 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.child
import com.tangem.core.decompose.context.childByContext import com.tangem.core.decompose.context.childByContext
import com.tangem.core.decompose.model.getOrCreateModel 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.ComposableContentComponent
import com.tangem.core.ui.decompose.ComposableDialogComponent
import com.tangem.core.ui.utils.findActivity import com.tangem.core.ui.utils.findActivity
import com.tangem.feature.wallet.child.wallet.model.WalletModel import com.tangem.feature.wallet.child.wallet.model.WalletModel
import com.tangem.feature.wallet.navigation.WalletRoute import com.tangem.feature.wallet.navigation.WalletRoute
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletDialogConfig import com.tangem.feature.wallet.presentation.wallet.state.model.WalletDialogConfig
import com.tangem.feature.wallet.presentation.wallet.ui.WalletScreen import com.tangem.feature.wallet.presentation.wallet.ui.WalletScreen
import com.tangem.feature.walletsettings.component.RenameWalletComponent import com.tangem.feature.walletsettings.component.RenameWalletComponent
import com.tangem.features.biometry.AskBiometryComponent
import com.tangem.features.markets.entry.MarketsEntryComponent import com.tangem.features.markets.entry.MarketsEntryComponent
import dagger.assisted.Assisted import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory import dagger.assisted.AssistedFactory
@ -31,6 +34,7 @@ internal class WalletComponent @AssistedInject constructor(
@Assisted navigate: (WalletRoute) -> Unit, @Assisted navigate: (WalletRoute) -> Unit,
private val renameWalletComponentFactory: RenameWalletComponent.Factory, private val renameWalletComponentFactory: RenameWalletComponent.Factory,
private val marketsEntryComponentFactory: MarketsEntryComponent.Factory, private val marketsEntryComponentFactory: MarketsEntryComponent.Factory,
private val askBiometryComponentFactory: AskBiometryComponent.Factory,
) : ComposableContentComponent, AppComponentContext by appComponentContext { ) : ComposableContentComponent, AppComponentContext by appComponentContext {
private val model: WalletModel = getOrCreateModel() 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, marketsEntryComponent = marketsEntryComponent,
) )
dialog.child?.instance?.Dialog() when (val dialog = dialog.child?.instance) {
is ComposableDialogComponent -> dialog.Dialog()
is ComposableBottomSheetComponent -> dialog.BottomSheet()
else -> {}
}
} }
@AssistedFactory @AssistedFactory

View file

@ -2,7 +2,11 @@ package com.tangem.feature.wallet.child.wallet.model
import androidx.compose.runtime.Stable import androidx.compose.runtime.Stable
import arrow.core.getOrElse 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.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.di.ModelScoped
import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.Model
import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase 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.loaders.WalletScreenContentLoader
import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController 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.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
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletEvent.DemonstrateWalletsScrollPreview.Direction 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.model.WalletScreenState
import com.tangem.feature.wallet.presentation.wallet.state.transformers.* 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.state.utils.WalletEventSender
import com.tangem.feature.wallet.presentation.wallet.utils.ScreenLifecycleProvider 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.PUSH_PERMISSION
import com.tangem.features.pushnotifications.api.utils.getPushPermissionOrNull import com.tangem.features.pushnotifications.api.utils.getPushPermissionOrNull
import com.tangem.features.wallet.featuretoggles.WalletFeatureToggles import com.tangem.features.wallet.featuretoggles.WalletFeatureToggles
@ -67,11 +74,13 @@ internal class WalletModel @Inject constructor(
private val tokenListStore: MultiWalletTokenListStore, private val tokenListStore: MultiWalletTokenListStore,
private val onrampStatusFactory: OnrampStatusFactory, private val onrampStatusFactory: OnrampStatusFactory,
private val walletFeatureToggles: WalletFeatureToggles, private val walletFeatureToggles: WalletFeatureToggles,
private val biometryFeatureToggles: BiometryFeatureToggles,
private val analyticsEventsHandler: AnalyticsEventHandler,
val screenLifecycleProvider: ScreenLifecycleProvider, val screenLifecycleProvider: ScreenLifecycleProvider,
val innerWalletRouter: InnerWalletRouter, val innerWalletRouter: InnerWalletRouter,
analyticsEventsHandler: AnalyticsEventHandler,
) : Model() { ) : Model() {
val askBiometryModelCallbacks = AskBiometryModelCallbacks()
val uiState: StateFlow<WalletScreenState> = stateHolder.uiState val uiState: StateFlow<WalletScreenState> = stateHolder.uiState
private val walletsUpdateJobHolder = JobHolder() private val walletsUpdateJobHolder = JobHolder()
@ -116,7 +125,15 @@ internal class WalletModel @Inject constructor(
modelScope.launch(dispatchers.main) { modelScope.launch(dispatchers.main) {
withContext(dispatchers.io) { delay(timeMillis = 1_800) } 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 { private companion object {
const val REFRESH_WALLET_BACKGROUND_TIMER_MILLIS = 10000L const val REFRESH_WALLET_BACKGROUND_TIMER_MILLIS = 10000L
const val EXPRESS_STATUS_UPDATE_DELAY = 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.domain.wallets.models.UserWalletId
import com.tangem.feature.wallet.navigation.WalletRoute import com.tangem.feature.wallet.navigation.WalletRoute
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletDialogConfig import com.tangem.feature.wallet.presentation.wallet.state.model.WalletDialogConfig
import com.tangem.features.biometry.BiometryFeatureToggles
import com.tangem.features.onboarding.v2.OnboardingV2FeatureToggles import com.tangem.features.onboarding.v2.OnboardingV2FeatureToggles
import kotlinx.coroutines.channels.BufferOverflow import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableSharedFlow
@ -25,6 +26,7 @@ internal class DefaultWalletRouter @Inject constructor(
private val urlOpener: UrlOpener, private val urlOpener: UrlOpener,
private val reduxStateHolder: ReduxStateHolder, private val reduxStateHolder: ReduxStateHolder,
private val onboardingV2FeatureToggles: OnboardingV2FeatureToggles, private val onboardingV2FeatureToggles: OnboardingV2FeatureToggles,
private val biometryFeatureToggles: BiometryFeatureToggles,
) : InnerWalletRouter { ) : InnerWalletRouter {
override val dialogNavigation: SlotNavigation<WalletDialogConfig> = SlotNavigation() override val dialogNavigation: SlotNavigation<WalletDialogConfig> = SlotNavigation()
@ -98,7 +100,9 @@ internal class DefaultWalletRouter @Inject constructor(
} }
override fun openSaveUserWalletScreen() { override fun openSaveUserWalletScreen() {
router.push(AppRoute.SaveWallet) if (biometryFeatureToggles.isAskForBiometryEnabled.not()) {
router.push(AppRoute.SaveWallet)
}
} }
override fun isWalletLastScreen(): Boolean { override fun isWalletLastScreen(): Boolean {

View file

@ -13,4 +13,7 @@ internal sealed interface WalletDialogConfig {
@Serializable @Serializable
data class RenameWallet(val userWalletId: UserWalletId, val currentName: String) : WalletDialogConfig 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:api")
include(":features:txhistory:impl") include(":features:txhistory:impl")
include(":features:ask-biometry:api") include(":features:biometry:api")
include(":features:ask-biometry:impl") include(":features:biometry:impl")
include(":features:nft:api") include(":features:nft:api")
include(":features:nft:impl") include(":features:nft:impl")