Updated on 2026-08-14

This commit is contained in:
Tangem 2026-04-28 14:35:25 +01:00
commit b32a1b4be0
24 changed files with 238 additions and 83 deletions

View file

@ -1,9 +1,9 @@
package com.tangem.features.onboarding.v2
import com.tangem.core.ui.extensions.TextReference
import com.tangem.features.onboarding.v2.title.OnboardingTitle
import kotlinx.coroutines.flow.StateFlow
interface TitleProvider {
val currentTitle: StateFlow<TextReference>
fun changeTitle(text: TextReference)
val currentTitle: StateFlow<OnboardingTitle>
fun changeTitle(title: OnboardingTitle)
}

View file

@ -1,5 +1,6 @@
package com.tangem.features.onboarding.v2.addresssync
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
@ -23,15 +24,18 @@ import com.tangem.features.onboarding.v2.addresssync.model.AddressSyncState
import com.tangem.features.onboarding.v2.addresssync.navigation.AddressSyncStep
import com.tangem.features.onboarding.v2.addresssync.ui.AddressSyncButtonScreen
import com.tangem.features.onboarding.v2.addresssync.ui.AddressSyncContent
import com.tangem.features.onboarding.v2.addresssync.ui.loading.AddressSyncLoading
import com.tangem.features.onboarding.v2.multiwallet.impl.child.MultiWalletChildParams
import com.tangem.features.pushnotifications.api.PushNotificationsComponent
import com.tangem.features.pushnotifications.api.PushNotificationsModelCallbacks
import com.tangem.features.pushnotifications.api.PushNotificationsParams
@OptIn(DelicateDecomposeApi::class)
@Suppress("LongParameterList")
internal class DefaultAddressSyncComponent(
appComponentContext: AppComponentContext,
params: MultiWalletChildParams,
private val onBack: () -> Unit,
private val addressSyncParams: AddressSyncComponent.Params,
private val askBiometryComponentFactory: AskBiometryComponent.Factory,
private val pushNotificationsComponentFactory: PushNotificationsComponent.Factory,
@ -44,7 +48,7 @@ internal class DefaultAddressSyncComponent(
key = "innerStack",
source = model.stackNavigation,
serializer = null,
initialConfiguration = AddressSyncStep.ASK_BIOMETRY,
initialConfiguration = AddressSyncStep.LOADING,
handleBackButton = true,
childFactory = { configuration, factoryContext ->
createChild(
@ -54,8 +58,29 @@ internal class DefaultAddressSyncComponent(
},
)
init {
model.initConfiguration()
}
@Composable
override fun Content(modifier: Modifier) {
BackHandler(enabled = true) {
onBack()
}
val state by model.state.collectAsStateWithLifecycle()
LaunchedEffect(state) {
when (state) {
AddressSyncState.Exit -> handleExit()
is AddressSyncState.Success -> {
val shouldExit = (state as AddressSyncState.Success).shouldExit
if (shouldExit) handleExit()
}
AddressSyncState.Loading -> Unit
}
}
AddressSyncContent(
modifier = modifier,
childContent = {
@ -68,14 +93,23 @@ internal class DefaultAddressSyncComponent(
)
}
private fun handleExit() {
if (addressSyncParams.isWalletStarted) {
router.popTo(AppRoute.Wallet)
} else {
router.replaceAll(AppRoute.Wallet)
}
}
private fun createChild(step: AddressSyncStep, childContext: AppComponentContext): ComposableContentComponent {
return when (step) {
AddressSyncStep.LOADING -> ComposableContentComponent { AddressSyncLoading() }
AddressSyncStep.ASK_BIOMETRY -> createAskBiometryComponent(childContext)
AddressSyncStep.ASK_NOTIFICATIONS -> createPushNotificationComponent(childContext)
AddressSyncStep.ADDRESS_SYNC -> ComposableContentComponent {
val state by model.state.collectAsStateWithLifecycle()
when (state) {
AddressSyncState.Loading -> Unit // todo shimmers will be implemented during [REDACTED_TASK_KEY]
AddressSyncState.Loading -> AddressSyncLoading()
is AddressSyncState.Success -> AddressSyncButtonScreen(
state = state as AddressSyncState.Success,
modifier = Modifier.fillMaxSize(),
@ -83,13 +117,7 @@ internal class DefaultAddressSyncComponent(
model.onIntent(AddressSyncIntent.Sync)
},
)
AddressSyncState.Exit -> LaunchedEffect(Unit) {
if (addressSyncParams.isWalletStarted) {
router.popTo(AppRoute.Wallet)
} else {
router.replaceAll(AppRoute.Wallet)
}
}
AddressSyncState.Exit -> AddressSyncLoading()
}
}
}

View file

@ -13,6 +13,7 @@ internal sealed class AddressSyncState {
data class Success(
val currencies: List<CryptoCurrency>,
val isButtonLoading: Boolean = false,
val shouldExit: Boolean = false,
) : AddressSyncState() {
val currenciesCount: Int = currencies.size
}

View file

@ -21,6 +21,7 @@ import com.tangem.features.onboarding.v2.addresssync.navigation.AddressSyncStep
import com.tangem.features.onboarding.v2.multiwallet.api.OnboardingMultiWalletComponent
import com.tangem.features.onboarding.v2.multiwallet.impl.MultiWalletInnerNavigationState
import com.tangem.features.onboarding.v2.multiwallet.impl.child.MultiWalletChildParams
import com.tangem.features.onboarding.v2.title.OnboardingTitle
import com.tangem.features.pushnotifications.api.utils.PUSH_PERMISSION
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.logging.TangemLogger
@ -59,9 +60,6 @@ internal class AddressSyncModel @Inject constructor(
stackSize = AddressSyncStep.ASK_BIOMETRY.pageNumber,
stackMaxSize = ADDRESS_SYNC_MAX_STEPS,
)
modelScope.launch {
trySkippingScreen(AddressSyncStep.ASK_BIOMETRY)
}
fetchWalletCrypto()
}
@ -76,24 +74,31 @@ internal class AddressSyncModel @Inject constructor(
stackNavigation.replaceCurrent(configuration = next.step)
updateStepperPage(next)
updateTitle(next)
modelScope.launch { trySkippingScreen(next.step) }
modelScope.launch { tryToSkipNotificationScreen(next.step) }
}
private suspend fun trySkippingScreen(step: AddressSyncStep) {
when (step) {
AddressSyncStep.ASK_BIOMETRY -> {
val shouldShowAskBiometry = canUseBiometryUseCase.strict() && shouldShowAskBiometryUseCase()
if (shouldShowAskBiometry.not()) {
nextScreen(AddressSyncIntent.Next(step = AddressSyncStep.ASK_NOTIFICATIONS))
}
fun initConfiguration() {
modelScope.launch {
val shouldShowAskBiometry = canUseBiometryUseCase.strict() && shouldShowAskBiometryUseCase()
val shouldShowAskNotification = shouldAskPermissionUseCase(PUSH_PERMISSION)
val step = when {
!shouldShowAskBiometry && !shouldShowAskNotification -> AddressSyncStep.ADDRESS_SYNC
!shouldShowAskBiometry -> AddressSyncStep.ASK_NOTIFICATIONS
else -> AddressSyncStep.ASK_BIOMETRY
}
nextScreen(AddressSyncIntent.Next(step))
}
}
private suspend fun tryToSkipNotificationScreen(step: AddressSyncStep) {
when (step) {
AddressSyncStep.ASK_NOTIFICATIONS -> {
val shouldShowAskNotification = shouldAskPermissionUseCase(PUSH_PERMISSION)
if (shouldShowAskNotification.not()) {
nextScreen(AddressSyncIntent.Next(step = AddressSyncStep.ADDRESS_SYNC))
}
}
AddressSyncStep.ADDRESS_SYNC -> Unit
AddressSyncStep.LOADING, AddressSyncStep.ASK_BIOMETRY, AddressSyncStep.ADDRESS_SYNC -> Unit
}
}
@ -106,9 +111,14 @@ internal class AddressSyncModel @Inject constructor(
}
private fun updateTitle(next: AddressSyncIntent.Next) {
params.parentParams.titleProvider.changeTitle(
text = resourceReference(next.step.stringId),
)
next.step.stringId?.let { stringId ->
params.parentParams.titleProvider.changeTitle(
title = OnboardingTitle(
text = resourceReference(stringId),
shouldForceTitle = true,
),
)
}
}
private fun fetchWalletCrypto() {
@ -164,7 +174,10 @@ internal class AddressSyncModel @Inject constructor(
launch { fetchNetworks(cryptoCurrencies) },
launch { fetchStaking(cryptoCurrencies) },
).joinAll()
state.value = AddressSyncState.Exit
state.update { addressSyncState ->
addressSyncState as AddressSyncState.Success
addressSyncState.copy(shouldExit = true)
}
},
)
}

View file

@ -3,7 +3,8 @@ package com.tangem.features.onboarding.v2.addresssync.navigation
import androidx.annotation.StringRes
import com.tangem.features.onboarding.v2.impl.R
internal enum class AddressSyncStep(val pageNumber: Int, @StringRes val stringId: Int) {
internal enum class AddressSyncStep(val pageNumber: Int, @StringRes val stringId: Int?) {
LOADING(pageNumber = 0, stringId = null),
ASK_BIOMETRY(pageNumber = 1, stringId = R.string.onboarding_navbar_title_biometrics),
ASK_NOTIFICATIONS(pageNumber = 2, stringId = R.string.onboarding_title_notifications),
ADDRESS_SYNC(pageNumber = 3, stringId = R.string.onboarding_navbar_title_last_step),

View file

@ -0,0 +1,39 @@
package com.tangem.features.onboarding.v2.addresssync.ui.loading
import android.content.res.Configuration
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
@Composable
internal fun AddressSyncLoading(modifier: Modifier = Modifier) {
Box(
modifier = modifier.fillMaxSize(),
contentAlignment = Alignment.Center,
) {
CircularProgressIndicator(
modifier = Modifier
.size(TangemTheme.dimens.size30)
.padding(4.dp),
color = TangemTheme.colors.icon.accent,
)
}
}
@Preview(showBackground = true, widthDp = 360)
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun AddressSyncLoadingPreview() {
TangemThemePreview {
AddressSyncLoading()
}
}

View file

@ -5,7 +5,8 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import com.arkivanov.decompose.extensions.compose.subscribeAsState
import com.arkivanov.decompose.router.stack.*
import com.arkivanov.decompose.router.stack.ChildStack
import com.arkivanov.decompose.router.stack.childStack
import com.arkivanov.decompose.value.ObserveLifecycleMode
import com.arkivanov.decompose.value.Value
import com.arkivanov.decompose.value.subscribe
@ -22,6 +23,7 @@ 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.features.onboarding.v2.title.OnboardingTitle
import com.tangem.utils.coroutines.JobHolder
import com.tangem.utils.coroutines.saveIn
import dagger.assisted.Assisted
@ -111,8 +113,14 @@ internal class DefaultOnboardingEntryComponent @AssistedInject constructor(
}
}.saveIn(innerNavigationLinkJobHolder)
} else {
stepperComponent.state.update {
it.copy(
val titleText = when (stack.active.configuration) {
is OnboardingRoute.ManageTokens -> resourceReference(R.string.main_manage_tokens)
is OnboardingRoute.AskBiometry -> resourceReference(R.string.onboarding_navbar_save_wallet)
is OnboardingRoute.Done -> resourceReference(R.string.onboarding_done_header)
else -> error("Unsupported route")
}
stepperComponent.state.update { stepperState ->
stepperState.copy(
currentStep = when (stack.active.configuration) {
is OnboardingRoute.ManageTokens -> 7
is OnboardingRoute.AskBiometry -> 8
@ -120,12 +128,7 @@ internal class DefaultOnboardingEntryComponent @AssistedInject constructor(
else -> error("Unsupported route")
},
steps = 9,
title = when (stack.active.configuration) {
is OnboardingRoute.ManageTokens -> resourceReference(R.string.main_manage_tokens)
is OnboardingRoute.AskBiometry -> resourceReference(R.string.onboarding_navbar_save_wallet)
is OnboardingRoute.Done -> resourceReference(R.string.onboarding_done_header)
else -> error("Unsupported route")
},
title = OnboardingTitle(titleText),
showProgress = true,
)
}

View file

@ -9,7 +9,6 @@ import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.core.decompose.navigation.Router
import com.tangem.core.decompose.ui.UiMessageSender
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.domain.card.common.util.cardTypesResolver
import com.tangem.domain.common.wallets.UserWalletsListRepository
@ -26,6 +25,7 @@ import com.tangem.features.onboarding.v2.entry.OnboardingEntryComponent.Mode
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.features.onboarding.v2.title.OnboardingTitle
import com.tangem.features.onboarding.v2.twin.api.OnboardingTwinComponent
import com.tangem.features.onboarding.v2.visa.impl.child.welcome.model.analytics.OnboardingVisaAnalyticsEvent
import com.tangem.sdk.api.TangemSdkManager
@ -51,9 +51,14 @@ internal class OnboardingEntryModel @Inject constructor(
val stackNavigation = StackNavigation<OnboardingRoute>()
val titleProvider = object : TitleProvider {
override val currentTitle = MutableStateFlow(stringReference(""))
override fun changeTitle(text: TextReference) {
currentTitle.value = text
override val currentTitle = MutableStateFlow(
OnboardingTitle(
text = stringReference(""),
),
)
override fun changeTitle(title: OnboardingTitle) {
currentTitle.value = title
}
}

View file

@ -201,6 +201,7 @@ internal class DefaultOnboardingMultiWalletComponent @AssistedInject constructor
addressSyncParams = AddressSyncComponent.Params(
isWalletStarted = mode.isWalletStarted,
),
onBack = { model.onBack() },
askBiometryComponentFactory = askBiometryComponentFactory,
pushNotificationsComponentFactory = pushNotificationsComponentFactory,
)

View file

@ -12,6 +12,7 @@ import com.tangem.features.onboarding.v2.multiwallet.impl.child.MultiWalletChild
import com.tangem.features.onboarding.v2.multiwallet.impl.child.MultiWalletChildParams
import com.tangem.features.onboarding.v2.multiwallet.impl.child.backup.model.MultiWalletBackupModel
import com.tangem.features.onboarding.v2.multiwallet.impl.child.backup.ui.MultiWalletBackup
import com.tangem.features.onboarding.v2.title.OnboardingTitle
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
@ -33,7 +34,9 @@ class MultiWalletBackupComponent(
}
params.parentParams.titleProvider.changeTitle(
text = resourceReference(R.string.onboarding_navbar_title_creating_backup),
title = OnboardingTitle(
text = resourceReference(R.string.onboarding_navbar_title_creating_backup),
),
)
componentScope.launch {

View file

@ -12,6 +12,7 @@ import com.tangem.features.onboarding.v2.multiwallet.impl.child.MultiWalletChild
import com.tangem.features.onboarding.v2.multiwallet.impl.child.chooseoption.model.Wallet1ChooseOptionModel
import com.tangem.features.onboarding.v2.multiwallet.impl.child.chooseoption.ui.Wallet1ChooseOption
import com.tangem.features.onboarding.v2.multiwallet.impl.model.OnboardingMultiWalletState
import com.tangem.features.onboarding.v2.title.OnboardingTitle
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
@ -32,7 +33,9 @@ class Wallet1ChooseOptionComponent(
}
params.parentParams.titleProvider.changeTitle(
text = resourceReference(R.string.onboarding_getting_started),
title = OnboardingTitle(
text = resourceReference(R.string.onboarding_getting_started),
),
)
componentScope.launch {

View file

@ -14,6 +14,7 @@ import com.tangem.features.onboarding.v2.multiwallet.impl.child.MultiWalletChild
import com.tangem.features.onboarding.v2.multiwallet.impl.child.createwallet.model.MultiWalletCreateWalletModel
import com.tangem.features.onboarding.v2.multiwallet.impl.child.createwallet.ui.MultiWalletCreateWallet
import com.tangem.features.onboarding.v2.multiwallet.impl.model.OnboardingMultiWalletState
import com.tangem.features.onboarding.v2.title.OnboardingTitle
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
@ -35,7 +36,9 @@ internal class MultiWalletCreateWalletComponent(
}
params.parentParams.titleProvider.changeTitle(
text = resourceReference(R.string.onboarding_create_wallet_header),
title = OnboardingTitle(
text = resourceReference(R.string.onboarding_create_wallet_header),
),
)
}

View file

@ -14,6 +14,7 @@ import com.tangem.features.onboarding.v2.multiwallet.impl.child.MultiWalletChild
import com.tangem.features.onboarding.v2.multiwallet.impl.child.MultiWalletChildParams
import com.tangem.features.onboarding.v2.multiwallet.impl.child.finalize.model.MultiWalletFinalizeModel
import com.tangem.features.onboarding.v2.multiwallet.impl.child.finalize.ui.MultiWalletFinalize
import com.tangem.features.onboarding.v2.title.OnboardingTitle
import com.tangem.features.onboarding.v2.util.ResetCardsComponent
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.launchIn
@ -49,7 +50,9 @@ internal class MultiWalletFinalizeComponent(
)
}
params.parentParams.titleProvider.changeTitle(
resourceReference(R.string.onboarding_button_finalize_backup),
title = OnboardingTitle(
text = resourceReference(R.string.onboarding_button_finalize_backup),
),
)
componentScope.launch {

View file

@ -10,6 +10,7 @@ import com.tangem.features.onboarding.v2.impl.R
import com.tangem.features.onboarding.v2.multiwallet.impl.child.MultiWalletChildParams
import com.tangem.features.onboarding.v2.multiwallet.impl.child.scanprimary.model.MultiWalletScanPrimaryModel
import com.tangem.features.onboarding.v2.multiwallet.impl.child.scanprimary.ui.MultiWalletScanPrimary
import com.tangem.features.onboarding.v2.title.OnboardingTitle
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
@ -30,7 +31,9 @@ internal class MultiWalletScanPrimaryComponent(
}
params.parentParams.titleProvider.changeTitle(
text = resourceReference(R.string.onboarding_navbar_title_creating_backup),
title = OnboardingTitle(
text = resourceReference(R.string.onboarding_navbar_title_creating_backup),
),
)
componentScope.launch { model.onDone.collect { onDone() } }

View file

@ -16,6 +16,7 @@ import com.tangem.features.onboarding.v2.multiwallet.impl.child.seedphrase.model
import com.tangem.features.onboarding.v2.multiwallet.impl.child.seedphrase.ui.MultiWalletSeedPhrase
import com.tangem.features.onboarding.v2.multiwallet.impl.child.seedphrase.ui.state.MultiWalletSeedPhraseUM
import com.tangem.features.onboarding.v2.multiwallet.impl.model.OnboardingMultiWalletState
import com.tangem.features.onboarding.v2.title.OnboardingTitle
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
@ -33,17 +34,17 @@ internal class MultiWalletSeedPhraseComponent(
init {
componentScope.launch {
model.uiState.collect {
model.uiState.collect { state ->
// change stepper state based on the stack of the current step
@Suppress("MagicNumber")
params.innerNavigation.update { st ->
st.copy(
stackSize = 3 + it.order,
stackSize = 3 + state.order,
stackMaxSize = 11,
)
}
val title = when (it) {
val title = when (state) {
is MultiWalletSeedPhraseUM.Import -> R.string.onboarding_seed_intro_button_import
is MultiWalletSeedPhraseUM.GenerateSeedPhrase,
is MultiWalletSeedPhraseUM.GeneratedWordsCheck,
@ -51,7 +52,11 @@ internal class MultiWalletSeedPhraseComponent(
-> R.string.onboarding_create_wallet_header
}
params.parentParams.titleProvider.changeTitle(text = resourceReference(title))
params.parentParams.titleProvider.changeTitle(
title = OnboardingTitle(
text = resourceReference(title),
),
)
}
}

View file

@ -14,6 +14,7 @@ import com.tangem.features.onboarding.v2.multiwallet.impl.child.MultiWalletChild
import com.tangem.features.onboarding.v2.multiwallet.impl.child.upgradewallet.model.MultiWalletUpgradeWalletModel
import com.tangem.features.onboarding.v2.multiwallet.impl.child.upgradewallet.ui.MultiWalletUpgradeWallet
import com.tangem.features.onboarding.v2.multiwallet.impl.model.OnboardingMultiWalletState
import com.tangem.features.onboarding.v2.title.OnboardingTitle
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
@ -35,7 +36,9 @@ internal class MultiWalletUpgradeWalletComponent(
}
params.parentParams.titleProvider.changeTitle(
text = resourceReference(R.string.common_tangem),
OnboardingTitle(
text = resourceReference(R.string.common_tangem),
),
)
}

View file

@ -18,6 +18,7 @@ import com.tangem.features.onboarding.v2.multiwallet.api.OnboardingMultiWalletCo
import com.tangem.features.onboarding.v2.multiwallet.impl.child.MultiWalletChildParams
import com.tangem.features.onboarding.v2.multiwallet.impl.model.OnboardingMultiWalletState.FinalizeStage
import com.tangem.features.onboarding.v2.multiwallet.impl.ui.state.OnboardingMultiWalletUM
import com.tangem.features.onboarding.v2.title.OnboardingTitle
import com.tangem.operations.attestation.ArtworkSize
import com.tangem.operations.backup.BackupService
import com.tangem.sdk.api.BackupServiceHolder
@ -72,7 +73,11 @@ internal class OnboardingMultiWalletModel @Inject constructor(
modelScope.launch {
onboardingRepository.clearUnfinishedFinalizeOnboarding()
if (params.mode is OnboardingMultiWalletComponent.Mode.AddressSync) {
router.replaceAll(AppRoute.Wallet)
if (params.mode.isWalletStarted) {
router.popTo(AppRoute.Wallet)
} else {
router.replaceAll(AppRoute.Wallet)
}
} else {
router.pop()
}
@ -177,7 +182,9 @@ internal class OnboardingMultiWalletModel @Inject constructor(
private fun initScreenTitle() {
val title = screenTitleByStep(getInitialStep())
params.titleProvider.changeTitle(title)
params.titleProvider.changeTitle(
title = OnboardingTitle(text = title),
)
}
private fun loadCardArtwork() {

View file

@ -27,6 +27,7 @@ import com.tangem.features.onboarding.v2.note.impl.model.OnboardingNoteCommonSta
import com.tangem.features.onboarding.v2.note.impl.model.OnboardingNoteModel
import com.tangem.features.onboarding.v2.note.impl.route.ONBOARDING_NOTE_STEPS_COUNT
import com.tangem.features.onboarding.v2.note.impl.route.OnboardingNoteRoute
import com.tangem.features.onboarding.v2.title.OnboardingTitle
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
@ -82,7 +83,11 @@ internal class DefaultOnboardingNoteComponent @AssistedInject constructor(
// sets title and stepper value
childStack.subscribe(lifecycle) { stack ->
val currentRoute = stack.active.configuration
params.titleProvider.changeTitle(TextReference.Res(R.string.onboarding_title))
params.titleProvider.changeTitle(
title = OnboardingTitle(
TextReference.Res(R.string.onboarding_title),
),
)
model.updateStepForNewRoute(currentRoute)
}
}

View file

@ -3,8 +3,8 @@ package com.tangem.features.onboarding.v2.stepper.api
import androidx.annotation.IntRange
import com.tangem.core.decompose.factory.ComponentFactory
import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.core.ui.extensions.TextReference
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.features.onboarding.v2.title.OnboardingTitle
import kotlinx.coroutines.flow.MutableStateFlow
internal interface OnboardingStepperComponent : ComposableContentComponent {
@ -12,7 +12,7 @@ internal interface OnboardingStepperComponent : ComposableContentComponent {
data class StepperState(
@IntRange(from = 0) val currentStep: Int,
@IntRange(from = 0) val steps: Int,
val title: TextReference,
val title: OnboardingTitle,
val showProgress: Boolean,
)

View file

@ -23,6 +23,7 @@ import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.features.onboarding.v2.impl.R
import com.tangem.features.onboarding.v2.stepper.api.OnboardingStepperComponent
import com.tangem.features.onboarding.v2.title.OnboardingTitle
@Composable
internal fun OnboardingStepper(
@ -51,13 +52,12 @@ internal fun OnboardingStepper(
startButton = TopAppBarButtonUM.Back(onBackClick),
endButton = TopAppBarButtonUM.Icon(iconRes = R.drawable.ic_chat_24, onClicked = onSupportButtonClick)
.takeIf { state.steps != state.currentStep },
title = if (state.steps == state.currentStep) {
title = if (state.steps == state.currentStep && !state.title.shouldForceTitle) {
resourceReference(R.string.common_done)
} else {
state.title
state.title.text
},
containerColor = TangemTheme.colors.background.primary,
modifier = modifier,
)
TangemLinearProgressIndicator(
@ -89,7 +89,7 @@ private fun OnboardingStepper_Preview() {
state = OnboardingStepperComponent.StepperState(
currentStep = 2,
steps = 3,
title = resourceReference(R.string.common_done),
title = OnboardingTitle(text = resourceReference(R.string.common_done)),
showProgress = true,
),
onBackClick = {},

View file

@ -0,0 +1,8 @@
package com.tangem.features.onboarding.v2.title
import com.tangem.core.ui.extensions.TextReference
data class OnboardingTitle(
val text: TextReference,
val shouldForceTitle: Boolean = false,
)

View file

@ -12,6 +12,7 @@ import com.tangem.core.decompose.navigation.inner.InnerNavigationHolder
import com.tangem.core.decompose.navigation.inner.InnerNavigationState
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.features.onboarding.v2.impl.R
import com.tangem.features.onboarding.v2.title.OnboardingTitle
import com.tangem.features.onboarding.v2.twin.api.OnboardingTwinComponent
import com.tangem.features.onboarding.v2.twin.impl.model.OnboardingTwinModel
import com.tangem.features.onboarding.v2.twin.impl.ui.OnboardingTwin
@ -28,7 +29,11 @@ internal class DefaultOnboardingTwinComponent @AssistedInject constructor(
private val model: OnboardingTwinModel = getOrCreateModel(params)
init {
params.titleProvider.changeTitle(resourceReference(R.string.twins_recreate_toolbar))
params.titleProvider.changeTitle(
title = OnboardingTitle(
text = resourceReference(R.string.twins_recreate_toolbar),
),
)
}
override val innerNavigation: InnerNavigation = object : InnerNavigation {

View file

@ -17,6 +17,7 @@ import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.core.decompose.navigation.inner.InnerNavigation
import com.tangem.core.decompose.navigation.inner.InnerNavigationState
import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.features.onboarding.v2.title.OnboardingTitle
import com.tangem.features.onboarding.v2.visa.api.OnboardingVisaComponent
import com.tangem.features.onboarding.v2.visa.impl.child.accesscode.OnboardingVisaAccessCodeComponent
import com.tangem.features.onboarding.v2.visa.impl.child.approve.OnboardingVisaApproveComponent
@ -87,7 +88,11 @@ internal class DefaultOnboardingVisaComponent @AssistedInject constructor(
// sets title and stepper value
childStack.subscribe(lifecycle) { stack ->
val currentRoute = stack.active.configuration
params.titleProvider.changeTitle(currentRoute.screenTitle())
params.titleProvider.changeTitle(
title = OnboardingTitle(
text = currentRoute.screenTitle(),
),
)
model.updateStepForNewRoute(currentRoute)
}
}

View file

@ -21,6 +21,7 @@ import com.tangem.features.onboarding.v2.addresssync.navigation.AddressSyncStep
import com.tangem.features.onboarding.v2.multiwallet.api.OnboardingMultiWalletComponent
import com.tangem.features.onboarding.v2.multiwallet.impl.MultiWalletInnerNavigationState
import com.tangem.features.onboarding.v2.multiwallet.impl.child.MultiWalletChildParams
import com.tangem.features.onboarding.v2.title.OnboardingTitle
import com.tangem.features.pushnotifications.api.utils.PUSH_PERMISSION
import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider
import io.mockk.*
@ -86,18 +87,19 @@ internal class AddressSyncModelTest {
val state = testInnerNavigation.value
Assertions.assertEquals(AddressSyncStep.ASK_BIOMETRY.pageNumber, state.stackSize)
Assertions.assertEquals(AddressSyncStep.entries.size, state.stackMaxSize)
Assertions.assertEquals(3, state.stackMaxSize)
}
@Test
fun `GIVEN biometry allowed AND should show biometry WHEN Next ASK_BIOMETRY THEN ASK_BIOMETRY on top`() = runTest {
fun `GIVEN biometry allowed AND should show biometry WHEN initConfiguration THEN ASK_BIOMETRY on top`() = runTest {
coEvery { canUseBiometryUseCase.strict() } returns true
coEvery { shouldShowAskBiometryUseCase() } returns true
coEvery { shouldAskPermissionUseCase(PUSH_PERMISSION) } returns false
val model = createModel(this)
val stack = model.stackNavigation.trackStack()
model.onIntent(AddressSyncIntent.Next(step = AddressSyncStep.ASK_BIOMETRY))
model.initConfiguration()
advanceUntilIdle()
Assertions.assertEquals(listOf(AddressSyncStep.ASK_BIOMETRY), stack)
@ -105,16 +107,16 @@ internal class AddressSyncModelTest {
}
@Test
fun `GIVEN biometry skipped AND notifications required WHEN Next ASK_BIOMETRY THEN ASK_NOTIFICATIONS on top`() =
fun `GIVEN biometry not needed AND notifications required WHEN initConfiguration THEN ASK_NOTIFICATIONS on top`() =
runTest {
coEvery { canUseBiometryUseCase.strict() } returns true
coEvery { canUseBiometryUseCase.strict() } returns false
coEvery { shouldShowAskBiometryUseCase() } returns false
coEvery { shouldAskPermissionUseCase(PUSH_PERMISSION) } returns true
val model = createModel(this)
val stack = model.stackNavigation.trackStack()
model.onIntent(AddressSyncIntent.Next(step = AddressSyncStep.ASK_BIOMETRY))
model.initConfiguration()
advanceUntilIdle()
Assertions.assertEquals(listOf(AddressSyncStep.ASK_NOTIFICATIONS), stack)
@ -122,20 +124,21 @@ internal class AddressSyncModelTest {
}
@Test
fun `GIVEN biometry skipped AND notifications skipped WHEN Next ASK_BIOMETRY THEN ADDRESS_SYNC on top`() = runTest {
coEvery { canUseBiometryUseCase.strict() } returns true
coEvery { shouldShowAskBiometryUseCase() } returns false
coEvery { shouldAskPermissionUseCase(PUSH_PERMISSION) } returns false
fun `GIVEN biometry not needed AND notifications skipped WHEN initConfiguration THEN ADDRESS_SYNC on top`() =
runTest {
coEvery { canUseBiometryUseCase.strict() } returns false
coEvery { shouldShowAskBiometryUseCase() } returns false
coEvery { shouldAskPermissionUseCase(PUSH_PERMISSION) } returns false
val model = createModel(this)
val stack = model.stackNavigation.trackStack()
val model = createModel(this)
val stack = model.stackNavigation.trackStack()
model.onIntent(AddressSyncIntent.Next(step = AddressSyncStep.ASK_BIOMETRY))
advanceUntilIdle()
model.initConfiguration()
advanceUntilIdle()
Assertions.assertEquals(listOf(AddressSyncStep.ADDRESS_SYNC), stack)
assertStepperAndTitleFor(AddressSyncStep.ADDRESS_SYNC)
}
Assertions.assertEquals(listOf(AddressSyncStep.ADDRESS_SYNC), stack)
assertStepperAndTitleFor(AddressSyncStep.ADDRESS_SYNC)
}
@Test
fun `GIVEN notifications required WHEN Next ASK_NOTIFICATIONS THEN ASK_NOTIFICATIONS on top`() = runTest {
@ -244,12 +247,13 @@ internal class AddressSyncModelTest {
}
@Test
fun `GIVEN success state WHEN Sync THEN state becomes Exit`() = runTest {
fun `GIVEN success state WHEN Sync THEN state becomes Success with shouldExit`() = runTest {
val currencies = listOf<CryptoCurrency>(
mockk { every { network } returns mockk() },
mockk { every { network } returns mockk() },
mockk { every { network } returns mockk() },
)
val expected = AddressSyncState.Success(currencies, isButtonLoading = true, shouldExit = true)
every { multiAccountListSupplier() } returns flowOf(
listOf(
AccountList.empty(
@ -274,7 +278,7 @@ internal class AddressSyncModelTest {
coVerify { derivePublicKeysUseCase(walletId, currencies) }
coVerify { multiNetworkStatusFetcher.invoke(any()) }
coVerify { multiStakingBalanceFetcher(any()) }
Assertions.assertEquals(AddressSyncState.Exit, model.state.value)
Assertions.assertEquals(expected, model.state.value)
}
@Test
@ -307,7 +311,14 @@ internal class AddressSyncModelTest {
private fun assertStepperAndTitleFor(step: AddressSyncStep) {
Assertions.assertEquals(step.pageNumber, testInnerNavigation.value.stackSize)
verify { titleProvider.changeTitle(resourceReference(step.stringId)) }
verify {
titleProvider.changeTitle(
title = OnboardingTitle(
text = resourceReference(step.stringId!!),
shouldForceTitle = true,
),
)
}
}
private fun StackNavigation<AddressSyncStep>.trackStack(): List<AddressSyncStep> {