From 0e169a5418b619d72aea40906d2ddb1b3b97fb6b Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 23 Apr 2026 10:45:35 +0100 Subject: [PATCH 1/2] Updated on 2026-08-14 --- .../tangem/tap/routing/utils/ChildFactory.kt | 2 +- .../com/tangem/common/routing/AppRoute.kt | 2 +- core/ui/src/main/res/drawable/ic_sync_56.xml | 21 ++++ .../features/biometry/impl/ui/AskBiometry.kt | 20 ++-- .../CreateWalletStartModel.kt | 2 +- .../CreateWalletStartModelTest.kt | 4 +- .../v2/entry/OnboardingEntryComponent.kt | 2 +- features/onboarding-v2/impl/build.gradle.kts | 1 + .../DefaultAddressSyncComponent.kt | 20 +++- .../addresssync/model/AddressSyncContract.kt | 14 +++ .../v2/addresssync/model/AddressSyncIntent.kt | 8 -- .../v2/addresssync/model/AddressSyncModel.kt | 52 +++++++-- .../addresssync/navigation/AddressSyncStep.kt | 2 +- .../addresssync/ui/AddressSyncButtonScreen.kt | 105 ++++++++++++++++++ .../entry/impl/model/OnboardingEntryModel.kt | 2 +- .../api/OnboardingMultiWalletComponent.kt | 2 +- .../model/MultiWalletFinalizeModel.kt | 2 +- .../impl/model/OnboardingMultiWalletModel.kt | 4 +- .../addresssync/model/AddressSyncModelTest.kt | 64 +++++++++++ 19 files changed, 292 insertions(+), 37 deletions(-) create mode 100644 core/ui/src/main/res/drawable/ic_sync_56.xml create mode 100644 features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/addresssync/model/AddressSyncContract.kt delete mode 100644 features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/addresssync/model/AddressSyncIntent.kt create mode 100644 features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/addresssync/ui/AddressSyncButtonScreen.kt diff --git a/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt b/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt index 743496098a..0725d8d517 100644 --- a/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt +++ b/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt @@ -261,7 +261,7 @@ internal class ChildFactory @Inject constructor( is AppRoute.Onboarding.Mode.UpgradeHotWallet -> OnboardingEntryComponent.Mode.UpgradeHotWallet(mode.userWalletId) is AppRoute.Onboarding.Mode.AddressSync -> - OnboardingEntryComponent.Mode.AddressSync + OnboardingEntryComponent.Mode.AddressSync(mode.userWalletId) }, ), componentFactory = onboardingEntryComponentFactory, diff --git a/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt b/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt index 165823622e..b70510260e 100644 --- a/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt +++ b/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt @@ -330,7 +330,7 @@ sealed class AppRoute(val path: String) : Route { data object RecreateWalletTwin : Mode() // reset twins data object ContinueFinalize : Mode() // continue finalize process (unfinished backup dialog) data class UpgradeHotWallet(val userWalletId: UserWalletId) : Mode() // upgrade hot wallet - data object AddressSync : Mode() + data class AddressSync(val userWalletId: UserWalletId) : Mode() } } diff --git a/core/ui/src/main/res/drawable/ic_sync_56.xml b/core/ui/src/main/res/drawable/ic_sync_56.xml new file mode 100644 index 0000000000..112dd64452 --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_sync_56.xml @@ -0,0 +1,21 @@ + + + + + + + + diff --git a/features/biometry/impl/src/main/kotlin/com/tangem/features/biometry/impl/ui/AskBiometry.kt b/features/biometry/impl/src/main/kotlin/com/tangem/features/biometry/impl/ui/AskBiometry.kt index a3726bf381..447e3a1238 100644 --- a/features/biometry/impl/src/main/kotlin/com/tangem/features/biometry/impl/ui/AskBiometry.kt +++ b/features/biometry/impl/src/main/kotlin/com/tangem/features/biometry/impl/ui/AskBiometry.kt @@ -126,6 +126,16 @@ private fun Footer(state: AskBiometryUM, modifier: Modifier = Modifier) { .fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally, ) { + Text( + modifier = Modifier.fillMaxWidth(fraction = .7f), + text = stringResourceSafe(R.string.save_user_wallet_agreement_notice), + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.tertiary, + textAlign = TextAlign.Center, + ) + + SpacerH16() + PrimaryButton( modifier = Modifier.fillMaxWidth(), showProgress = state.shouldShowProgress, @@ -142,16 +152,6 @@ private fun Footer(state: AskBiometryUM, modifier: Modifier = Modifier) { onClick = state.onDontAllowClick, ) } - - SpacerH16() - - Text( - modifier = Modifier.fillMaxWidth(fraction = .7f), - text = stringResourceSafe(R.string.save_user_wallet_agreement_notice), - style = TangemTheme.typography.caption2, - color = TangemTheme.colors.text.tertiary, - textAlign = TextAlign.Center, - ) } } diff --git a/features/create-wallet-start/impl/src/main/kotlin/com/tangem/features/createwalletstart/CreateWalletStartModel.kt b/features/create-wallet-start/impl/src/main/kotlin/com/tangem/features/createwalletstart/CreateWalletStartModel.kt index 8c15de536f..91f69a736d 100644 --- a/features/create-wallet-start/impl/src/main/kotlin/com/tangem/features/createwalletstart/CreateWalletStartModel.kt +++ b/features/create-wallet-start/impl/src/main/kotlin/com/tangem/features/createwalletstart/CreateWalletStartModel.kt @@ -239,7 +239,7 @@ internal class CreateWalletStartModel @Inject constructor( val route = if (onboardingV2FeatureToggles.isAddressSyncEnabled) { AppRoute.Onboarding( scanResponse = scanResponse, - mode = AppRoute.Onboarding.Mode.AddressSync, + mode = AppRoute.Onboarding.Mode.AddressSync(userWalletId = userWallet.walletId), ) } else { AppRoute.Wallet diff --git a/features/create-wallet-start/impl/src/test/kotlin/com/tangem/features/createwalletstart/CreateWalletStartModelTest.kt b/features/create-wallet-start/impl/src/test/kotlin/com/tangem/features/createwalletstart/CreateWalletStartModelTest.kt index bd7640bef5..54b19b9b4f 100644 --- a/features/create-wallet-start/impl/src/test/kotlin/com/tangem/features/createwalletstart/CreateWalletStartModelTest.kt +++ b/features/create-wallet-start/impl/src/test/kotlin/com/tangem/features/createwalletstart/CreateWalletStartModelTest.kt @@ -536,7 +536,9 @@ internal class CreateWalletStartModelTest { routes = arrayOf( AppRoute.Onboarding( scanResponse = testScanResponse, - mode = AppRoute.Onboarding.Mode.AddressSync, + mode = AppRoute.Onboarding.Mode.AddressSync( + userWalletId = testUserWalletId, + ), ) ), onComplete = any() diff --git a/features/onboarding-v2/api/src/main/kotlin/com/tangem/features/onboarding/v2/entry/OnboardingEntryComponent.kt b/features/onboarding-v2/api/src/main/kotlin/com/tangem/features/onboarding/v2/entry/OnboardingEntryComponent.kt index d4776bf833..a8314aff42 100644 --- a/features/onboarding-v2/api/src/main/kotlin/com/tangem/features/onboarding/v2/entry/OnboardingEntryComponent.kt +++ b/features/onboarding-v2/api/src/main/kotlin/com/tangem/features/onboarding/v2/entry/OnboardingEntryComponent.kt @@ -19,7 +19,7 @@ interface OnboardingEntryComponent : ComposableContentComponent { data object RecreateWalletTwin : Mode() data object ContinueFinalize : Mode() data class UpgradeHotWallet(val userWalletId: UserWalletId) : Mode() - data object AddressSync : Mode() + data class AddressSync(val userWalletId: UserWalletId) : Mode() } interface Factory : ComponentFactory diff --git a/features/onboarding-v2/impl/build.gradle.kts b/features/onboarding-v2/impl/build.gradle.kts index f4022573af..a2fadc93e1 100644 --- a/features/onboarding-v2/impl/build.gradle.kts +++ b/features/onboarding-v2/impl/build.gradle.kts @@ -42,6 +42,7 @@ dependencies { implementation(projects.common) /** Domain */ + implementation(projects.domain.account) implementation(projects.domain.models) implementation(projects.domain.feedback) implementation(projects.domain.feedback.models) diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/addresssync/DefaultAddressSyncComponent.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/addresssync/DefaultAddressSyncComponent.kt index f5999ae461..4717f961c2 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/addresssync/DefaultAddressSyncComponent.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/addresssync/DefaultAddressSyncComponent.kt @@ -1,7 +1,10 @@ package com.tangem.features.onboarding.v2.addresssync +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier +import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.arkivanov.decompose.DelicateDecomposeApi import com.arkivanov.decompose.extensions.compose.stack.Children import com.arkivanov.decompose.router.stack.ChildStack @@ -15,7 +18,9 @@ import com.tangem.core.ui.decompose.ComposableContentComponent import com.tangem.features.biometry.AskBiometryComponent import com.tangem.features.onboarding.v2.addresssync.model.AddressSyncIntent import com.tangem.features.onboarding.v2.addresssync.model.AddressSyncModel +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.multiwallet.impl.child.MultiWalletChildParams import com.tangem.features.pushnotifications.api.PushNotificationsComponent @@ -65,7 +70,20 @@ internal class DefaultAddressSyncComponent( return when (step) { AddressSyncStep.ASK_BIOMETRY -> createAskBiometryComponent(childContext) AddressSyncStep.ASK_NOTIFICATIONS -> createPushNotificationComponent(childContext) - AddressSyncStep.ADDRESS_SYNC -> TODO("Will be implemented during [REDACTED_TASK_KEY]") + AddressSyncStep.ADDRESS_SYNC -> ComposableContentComponent { + val state by model.state.collectAsStateWithLifecycle() + when (state) { + AddressSyncState.Loading -> Unit // todo shimmers will be implemented during [REDACTED_TASK_KEY] + is AddressSyncState.Success -> AddressSyncButtonScreen( + state = state as AddressSyncState.Success, + modifier = Modifier.fillMaxSize(), + onSyncClick = { + model.onIntent(AddressSyncIntent.Sync) + }, + ) + AddressSyncState.NoTokens -> router.replaceAll(AppRoute.Wallet) + } + } } } diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/addresssync/model/AddressSyncContract.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/addresssync/model/AddressSyncContract.kt new file mode 100644 index 0000000000..f283b138e2 --- /dev/null +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/addresssync/model/AddressSyncContract.kt @@ -0,0 +1,14 @@ +package com.tangem.features.onboarding.v2.addresssync.model + +import com.tangem.features.onboarding.v2.addresssync.navigation.AddressSyncStep + +internal sealed interface AddressSyncIntent { + data class Next(val step: AddressSyncStep) : AddressSyncIntent + data object Sync : AddressSyncIntent +} + +internal sealed class AddressSyncState { + data object Loading : AddressSyncState() + data class Success(val currenciesCount: Int) : AddressSyncState() + data object NoTokens : AddressSyncState() +} \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/addresssync/model/AddressSyncIntent.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/addresssync/model/AddressSyncIntent.kt deleted file mode 100644 index 0776ea3cc7..0000000000 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/addresssync/model/AddressSyncIntent.kt +++ /dev/null @@ -1,8 +0,0 @@ -package com.tangem.features.onboarding.v2.addresssync.model - -import com.tangem.features.onboarding.v2.addresssync.navigation.AddressSyncStep - -sealed interface AddressSyncIntent { - data class Next(val step: AddressSyncStep) : AddressSyncIntent - data object Back : AddressSyncIntent -} \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/addresssync/model/AddressSyncModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/addresssync/model/AddressSyncModel.kt index a73fa83fae..eb902893a3 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/addresssync/model/AddressSyncModel.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/addresssync/model/AddressSyncModel.kt @@ -2,35 +2,45 @@ package com.tangem.features.onboarding.v2.addresssync.model import com.arkivanov.decompose.DelicateDecomposeApi import com.arkivanov.decompose.router.stack.StackNavigation -import com.arkivanov.decompose.router.stack.pop import com.arkivanov.decompose.router.stack.replaceCurrent import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer import com.tangem.core.ui.extensions.resourceReference +import com.tangem.domain.account.supplier.MultiAccountListSupplier import com.tangem.domain.settings.CanUseBiometryUseCase import com.tangem.domain.settings.ShouldAskPermissionUseCase import com.tangem.domain.settings.ShouldShowAskBiometryUseCase +import com.tangem.domain.tokens.MultiWalletAccountListFetcher 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.child.MultiWalletChildParams import com.tangem.features.pushnotifications.api.utils.PUSH_PERMISSION import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import kotlinx.coroutines.flow.update +import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch import javax.inject.Inject @OptIn(DelicateDecomposeApi::class) +@Suppress("LongParameterList") @ModelScoped internal class AddressSyncModel @Inject constructor( override val dispatchers: CoroutineDispatcherProvider, private val shouldShowAskBiometryUseCase: ShouldShowAskBiometryUseCase, private val canUseBiometryUseCase: CanUseBiometryUseCase, private val shouldAskPermissionUseCase: ShouldAskPermissionUseCase, + private val multiWalletAccountListFetcher: MultiWalletAccountListFetcher, + private val multiAccountListSupplier: MultiAccountListSupplier, paramsContainer: ParamsContainer, ) : Model() { private val params = paramsContainer.require() + private val walletId = (params.parentParams.mode as OnboardingMultiWalletComponent.Mode.AddressSync).userWalletId val stackNavigation = StackNavigation() + val state: StateFlow + field = MutableStateFlow( + value = AddressSyncState.Loading, + ) init { params.innerNavigation.update { innerNavigationState -> @@ -42,12 +52,13 @@ internal class AddressSyncModel @Inject constructor( modelScope.launch { trySkippingScreen(AddressSyncStep.ASK_BIOMETRY) } + fetchWalletCrypto() } fun onIntent(intent: AddressSyncIntent) { when (intent) { is AddressSyncIntent.Next -> nextScreen(intent) - AddressSyncIntent.Back -> goBack() + AddressSyncIntent.Sync -> startSyncing() } } @@ -76,10 +87,6 @@ internal class AddressSyncModel @Inject constructor( } } - private fun goBack() { - stackNavigation.pop() - } - private fun updateStepperPage(next: AddressSyncIntent.Next) { params.innerNavigation.update { innerNavigationState -> innerNavigationState.copy( @@ -94,6 +101,37 @@ internal class AddressSyncModel @Inject constructor( ) } + private fun fetchWalletCrypto() { + modelScope.launch { + multiWalletAccountListFetcher.invoke( + params = MultiWalletAccountListFetcher.Params(userWalletId = walletId), + ) + handleAddressSyncStep() + } + } + + private suspend fun handleAddressSyncStep() { + multiAccountListSupplier() + .map { accountLists -> + accountLists + .first { it.userWalletId == walletId } + .flattenCurrencies() + } + .onEach { currencies -> + val updatedState = if (currencies.isEmpty()) { + AddressSyncState.NoTokens + } else { + AddressSyncState.Success(currenciesCount = currencies.size) + } + state.value = updatedState + } + .collect() + } + + private fun startSyncing() { + TODO("Will be implemented during [REDACTED_TASK_KEY]") + } + private companion object { const val ADDRESS_SYNC_MAX_STEPS = 3 } diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/addresssync/navigation/AddressSyncStep.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/addresssync/navigation/AddressSyncStep.kt index c03a4fc662..f9d48107ca 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/addresssync/navigation/AddressSyncStep.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/addresssync/navigation/AddressSyncStep.kt @@ -3,7 +3,7 @@ package com.tangem.features.onboarding.v2.addresssync.navigation import androidx.annotation.StringRes import com.tangem.features.onboarding.v2.impl.R -enum class AddressSyncStep(val pageNumber: Int, @StringRes val stringId: Int) { +internal enum class AddressSyncStep(val pageNumber: Int, @StringRes val stringId: Int) { 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), diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/addresssync/ui/AddressSyncButtonScreen.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/addresssync/ui/AddressSyncButtonScreen.kt new file mode 100644 index 0000000000..94e2d60121 --- /dev/null +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/addresssync/ui/AddressSyncButtonScreen.kt @@ -0,0 +1,105 @@ +package com.tangem.features.onboarding.v2.addresssync.ui + +import android.content.res.Configuration +import androidx.compose.foundation.layout.* +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import com.tangem.core.ui.components.PrimaryButtonIconEnd +import com.tangem.core.ui.components.SpacerHHalf +import com.tangem.core.ui.components.SpacerHMax +import com.tangem.core.ui.extensions.pluralStringResourceSafe +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.onboarding.v2.addresssync.model.AddressSyncState +import com.tangem.features.onboarding.v2.impl.R + +@Composable +internal fun AddressSyncButtonScreen( + state: AddressSyncState.Success, + onSyncClick: () -> Unit, + modifier: Modifier = Modifier, +) { + Column( + modifier = modifier + .fillMaxSize() + .padding(top = TangemTheme.dimens.spacing56), + ) { + SpacerHHalf() + Icon( + painter = painterResource(id = R.drawable.ic_sync_56), + contentDescription = null, + tint = TangemTheme.colors.icon.accent, + modifier = Modifier + .align(Alignment.CenterHorizontally) + .size(TangemTheme.dimens.size56), + ) + Text( + text = stringResourceSafe(R.string.onboarding_address_sync_title), + style = TangemTheme.typography.h2, + color = TangemTheme.colors.text.primary1, + textAlign = TextAlign.Center, + modifier = Modifier + .align(Alignment.CenterHorizontally) + .padding( + start = TangemTheme.dimens.spacing34, + end = TangemTheme.dimens.spacing34, + top = TangemTheme.dimens.spacing28, + ), + ) + AddressSyncDescription(state.currenciesCount) + SpacerHMax() + PrimaryButtonIconEnd( + text = stringResourceSafe(R.string.common_generate_addresses), + onClick = onSyncClick, + iconResId = R.drawable.ic_tangem_24, + modifier = Modifier + .fillMaxWidth() + .padding( + start = TangemTheme.dimens.spacing16, + end = TangemTheme.dimens.spacing16, + top = TangemTheme.dimens.spacing154, + bottom = TangemTheme.dimens.spacing16, + ), + ) + } +} + +@Composable +private fun ColumnScope.AddressSyncDescription(currenciesCount: Int) { + Text( + text = pluralStringResourceSafe( + id = R.plurals.onboarding_address_sync_description, + count = currenciesCount, + currenciesCount, + ), + style = TangemTheme.typography.body2, + color = TangemTheme.colors.text.secondary, + textAlign = TextAlign.Center, + modifier = Modifier + .align(Alignment.CenterHorizontally) + .padding( + start = TangemTheme.dimens.spacing34, + end = TangemTheme.dimens.spacing34, + top = TangemTheme.dimens.spacing28, + ), + ) +} + +@Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun AddressSyncButtonScreenPreview() { + TangemThemePreview { + AddressSyncButtonScreen( + state = AddressSyncState.Success(currenciesCount = 2), + onSyncClick = {}, + ) + } +} \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/entry/impl/model/OnboardingEntryModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/entry/impl/model/OnboardingEntryModel.kt index 5747977a6d..aab619c0e9 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/entry/impl/model/OnboardingEntryModel.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/entry/impl/model/OnboardingEntryModel.kt @@ -82,7 +82,7 @@ internal class OnboardingEntryModel @Inject constructor( is Mode.UpgradeHotWallet -> OnboardingMultiWalletComponent.Mode.UpgradeHotWallet( userWalletId = mode.userWalletId, ) - is Mode.AddressSync -> OnboardingMultiWalletComponent.Mode.AddressSync + is Mode.AddressSync -> OnboardingMultiWalletComponent.Mode.AddressSync(mode.userWalletId) else -> error("Incorrect onboarding type") } diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/api/OnboardingMultiWalletComponent.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/api/OnboardingMultiWalletComponent.kt index 9e4e023699..ed4280ad63 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/api/OnboardingMultiWalletComponent.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/api/OnboardingMultiWalletComponent.kt @@ -24,7 +24,7 @@ interface OnboardingMultiWalletComponent : ComposableContentComponent, InnerNavi data object AddBackup : Mode() data object ContinueFinalize : Mode() data class UpgradeHotWallet(val userWalletId: UserWalletId) : Mode() - data object AddressSync : Mode() + data class AddressSync(val userWalletId: UserWalletId) : Mode() } interface Factory : ComponentFactory diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/finalize/model/MultiWalletFinalizeModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/finalize/model/MultiWalletFinalizeModel.kt index 9dc1d725b0..b6431a21af 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/finalize/model/MultiWalletFinalizeModel.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/finalize/model/MultiWalletFinalizeModel.kt @@ -265,7 +265,7 @@ internal class MultiWalletFinalizeModel @Inject constructor( val userWallet = when (params.parentParams.mode) { OnboardingMultiWalletComponent.Mode.Onboarding, OnboardingMultiWalletComponent.Mode.ContinueFinalize, - OnboardingMultiWalletComponent.Mode.AddressSync, + is OnboardingMultiWalletComponent.Mode.AddressSync, -> { saveWalletUseCase.invoke( userWallet = userWalletCreated.copy( diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/model/OnboardingMultiWalletModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/model/OnboardingMultiWalletModel.kt index 050a14f74e..0d3c27e10a 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/model/OnboardingMultiWalletModel.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/model/OnboardingMultiWalletModel.kt @@ -71,7 +71,7 @@ internal class OnboardingMultiWalletModel @Inject constructor( onConfirm = { modelScope.launch { onboardingRepository.clearUnfinishedFinalizeOnboarding() - if (params.mode == OnboardingMultiWalletComponent.Mode.AddressSync) { + if (params.mode is OnboardingMultiWalletComponent.Mode.AddressSync) { router.replaceAll(AppRoute.Wallet) } else { router.pop() @@ -128,7 +128,7 @@ internal class OnboardingMultiWalletModel @Inject constructor( params.mode == OnboardingMultiWalletComponent.Mode.ContinueFinalize -> { OnboardingMultiWalletState.Step.Finalize } - params.mode == OnboardingMultiWalletComponent.Mode.AddressSync -> { + params.mode is OnboardingMultiWalletComponent.Mode.AddressSync -> { OnboardingMultiWalletState.Step.AddressSync } // Add backup button diff --git a/features/onboarding-v2/impl/src/test/kotlin/com/tangem/features/onboarding/v2/addresssync/model/AddressSyncModelTest.kt b/features/onboarding-v2/impl/src/test/kotlin/com/tangem/features/onboarding/v2/addresssync/model/AddressSyncModelTest.kt index f784c38c74..8282ed6639 100644 --- a/features/onboarding-v2/impl/src/test/kotlin/com/tangem/features/onboarding/v2/addresssync/model/AddressSyncModelTest.kt +++ b/features/onboarding-v2/impl/src/test/kotlin/com/tangem/features/onboarding/v2/addresssync/model/AddressSyncModelTest.kt @@ -1,23 +1,32 @@ package com.tangem.features.onboarding.v2.addresssync.model +import arrow.core.Either import com.arkivanov.decompose.router.stack.StackNavigation import com.tangem.core.decompose.model.ParamsContainer import com.tangem.core.ui.extensions.resourceReference +import com.tangem.domain.account.models.AccountList +import com.tangem.domain.account.supplier.MultiAccountListSupplier +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.settings.CanUseBiometryUseCase import com.tangem.domain.settings.ShouldAskPermissionUseCase import com.tangem.domain.settings.ShouldShowAskBiometryUseCase +import com.tangem.domain.tokens.MultiWalletAccountListFetcher import com.tangem.features.onboarding.v2.TitleProvider 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.pushnotifications.api.utils.PUSH_PERMISSION import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider import io.mockk.coEvery +import io.mockk.coVerify import io.mockk.every import io.mockk.mockk import io.mockk.verify import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.StandardTestDispatcher import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.advanceUntilIdle @@ -31,6 +40,8 @@ internal class AddressSyncModelTest { private val shouldShowAskBiometryUseCase: ShouldShowAskBiometryUseCase = mockk() private val canUseBiometryUseCase: CanUseBiometryUseCase = mockk() private val shouldAskPermissionUseCase: ShouldAskPermissionUseCase = mockk() + private val multiWalletAccountListFetcher: MultiWalletAccountListFetcher = mockk() + private val multiAccountListSupplier: MultiAccountListSupplier = mockk() private val paramsContainer: ParamsContainer = mockk() private val testInnerNavigation = MutableStateFlow( value = MultiWalletInnerNavigationState( @@ -39,10 +50,12 @@ internal class AddressSyncModelTest { ) ) private val titleProvider: TitleProvider = mockk(relaxUnitFun = true) + private val walletId = UserWalletId("011") private val params: MultiWalletChildParams = mockk { every { innerNavigation } returns testInnerNavigation every { parentParams } returns mockk { every { titleProvider } returns this@AddressSyncModelTest.titleProvider + every { mode } returns OnboardingMultiWalletComponent.Mode.AddressSync(walletId) } } @@ -51,6 +64,10 @@ internal class AddressSyncModelTest { coEvery { canUseBiometryUseCase.strict() } returns false coEvery { shouldShowAskBiometryUseCase() } returns false coEvery { shouldAskPermissionUseCase(PUSH_PERMISSION) } returns false + coEvery { multiWalletAccountListFetcher.invoke(any()) } returns Either.Right(Unit) + every { multiAccountListSupplier() } returns flowOf( + listOf(AccountList.empty(userWalletId = walletId)), + ) every { paramsContainer.require() } returns params } @@ -139,6 +156,51 @@ internal class AddressSyncModelTest { assertStepperAndTitleFor(AddressSyncStep.ADDRESS_SYNC) } + @Test + fun `GIVEN multiAccountListSupplier emits no currencies WHEN model is created THEN state is NoTokens`() = runTest { + every { multiAccountListSupplier() } returns flowOf( + listOf( + AccountList.empty( + userWalletId = walletId, + cryptoCurrencies = emptyList(), + ), + ), + ) + + val model = createModel(this) + advanceUntilIdle() + + coVerify { + multiWalletAccountListFetcher.invoke( + params = MultiWalletAccountListFetcher.Params(userWalletId = walletId) + ) + } + assert(model.state.value == AddressSyncState.NoTokens) + } + + @Test + fun `GIVEN multiAccountListSupplier emits currencies WHEN model is created THEN state is Success`() = runTest { + val currencies = listOf(mockk(), mockk(), mockk()) + every { multiAccountListSupplier() } returns flowOf( + listOf( + AccountList.empty( + userWalletId = walletId, + cryptoCurrencies = currencies, + ), + ), + ) + + val model = createModel(this) + advanceUntilIdle() + + coVerify { + multiWalletAccountListFetcher.invoke( + params = MultiWalletAccountListFetcher.Params(userWalletId = walletId) + ) + } + assert(model.state.value == AddressSyncState.Success(currenciesCount = currencies.size)) + } + private fun assertStepperAndTitleFor(step: AddressSyncStep) { assert(testInnerNavigation.value.stackSize == step.pageNumber) verify { titleProvider.changeTitle(resourceReference(step.stringId)) } @@ -160,6 +222,8 @@ internal class AddressSyncModelTest { shouldShowAskBiometryUseCase = shouldShowAskBiometryUseCase, canUseBiometryUseCase = canUseBiometryUseCase, shouldAskPermissionUseCase = shouldAskPermissionUseCase, + multiWalletAccountListFetcher = multiWalletAccountListFetcher, + multiAccountListSupplier = multiAccountListSupplier, paramsContainer = paramsContainer, ) } From 92f5623ba65396c918ef6bd47d98fdc5e10c7859 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 23 Apr 2026 12:33:43 +0100 Subject: [PATCH 2/2] Updated on 2026-08-14 --- .../DefaultAddressSyncComponent.kt | 5 +- .../v2/addresssync/model/AddressSyncModel.kt | 10 +++- .../addresssync/model/AddressSyncModelTest.kt | 60 ++++++++++++++----- 3 files changed, 57 insertions(+), 18 deletions(-) diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/addresssync/DefaultAddressSyncComponent.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/addresssync/DefaultAddressSyncComponent.kt index 4717f961c2..ab4f310b25 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/addresssync/DefaultAddressSyncComponent.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/addresssync/DefaultAddressSyncComponent.kt @@ -2,6 +2,7 @@ package com.tangem.features.onboarding.v2.addresssync import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.lifecycle.compose.collectAsStateWithLifecycle @@ -81,7 +82,9 @@ internal class DefaultAddressSyncComponent( model.onIntent(AddressSyncIntent.Sync) }, ) - AddressSyncState.NoTokens -> router.replaceAll(AppRoute.Wallet) + AddressSyncState.NoTokens -> LaunchedEffect(Unit) { + router.replaceAll(AppRoute.Wallet) + } } } } diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/addresssync/model/AddressSyncModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/addresssync/model/AddressSyncModel.kt index eb902893a3..d140c874f7 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/addresssync/model/AddressSyncModel.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/addresssync/model/AddressSyncModel.kt @@ -103,10 +103,16 @@ internal class AddressSyncModel @Inject constructor( private fun fetchWalletCrypto() { modelScope.launch { - multiWalletAccountListFetcher.invoke( + multiWalletAccountListFetcher( params = MultiWalletAccountListFetcher.Params(userWalletId = walletId), + ).fold( + ifLeft = { + state.value = AddressSyncState.NoTokens + }, + ifRight = { + handleAddressSyncStep() + }, ) - handleAddressSyncStep() } } diff --git a/features/onboarding-v2/impl/src/test/kotlin/com/tangem/features/onboarding/v2/addresssync/model/AddressSyncModelTest.kt b/features/onboarding-v2/impl/src/test/kotlin/com/tangem/features/onboarding/v2/addresssync/model/AddressSyncModelTest.kt index 8282ed6639..6cd4a728ba 100644 --- a/features/onboarding-v2/impl/src/test/kotlin/com/tangem/features/onboarding/v2/addresssync/model/AddressSyncModelTest.kt +++ b/features/onboarding-v2/impl/src/test/kotlin/com/tangem/features/onboarding/v2/addresssync/model/AddressSyncModelTest.kt @@ -19,11 +19,7 @@ import com.tangem.features.onboarding.v2.multiwallet.impl.MultiWalletInnerNaviga import com.tangem.features.onboarding.v2.multiwallet.impl.child.MultiWalletChildParams import com.tangem.features.pushnotifications.api.utils.PUSH_PERMISSION import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider -import io.mockk.coEvery -import io.mockk.coVerify -import io.mockk.every -import io.mockk.mockk -import io.mockk.verify +import io.mockk.* import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.flowOf @@ -31,6 +27,7 @@ import kotlinx.coroutines.test.StandardTestDispatcher import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.advanceUntilIdle import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test @@ -76,8 +73,8 @@ internal class AddressSyncModelTest { createModel(this) val state = testInnerNavigation.value - assert(state.stackSize == AddressSyncStep.ASK_BIOMETRY.pageNumber) - assert(state.stackMaxSize == AddressSyncStep.entries.size) + Assertions.assertEquals(AddressSyncStep.ASK_BIOMETRY.pageNumber, state.stackSize) + Assertions.assertEquals(AddressSyncStep.entries.size, state.stackMaxSize) } @Test @@ -91,7 +88,7 @@ internal class AddressSyncModelTest { model.onIntent(AddressSyncIntent.Next(step = AddressSyncStep.ASK_BIOMETRY)) advanceUntilIdle() - assert(stack == listOf(AddressSyncStep.ASK_BIOMETRY)) + Assertions.assertEquals(listOf(AddressSyncStep.ASK_BIOMETRY), stack) assertStepperAndTitleFor(AddressSyncStep.ASK_BIOMETRY) } @@ -108,7 +105,7 @@ internal class AddressSyncModelTest { model.onIntent(AddressSyncIntent.Next(step = AddressSyncStep.ASK_BIOMETRY)) advanceUntilIdle() - assert(stack == listOf(AddressSyncStep.ASK_NOTIFICATIONS)) + Assertions.assertEquals(listOf(AddressSyncStep.ASK_NOTIFICATIONS), stack) assertStepperAndTitleFor(AddressSyncStep.ASK_NOTIFICATIONS) } @@ -124,7 +121,7 @@ internal class AddressSyncModelTest { model.onIntent(AddressSyncIntent.Next(step = AddressSyncStep.ASK_BIOMETRY)) advanceUntilIdle() - assert(stack == listOf(AddressSyncStep.ADDRESS_SYNC)) + Assertions.assertEquals(listOf(AddressSyncStep.ADDRESS_SYNC), stack) assertStepperAndTitleFor(AddressSyncStep.ADDRESS_SYNC) } @@ -138,7 +135,7 @@ internal class AddressSyncModelTest { model.onIntent(AddressSyncIntent.Next(step = AddressSyncStep.ASK_NOTIFICATIONS)) advanceUntilIdle() - assert(stack == listOf(AddressSyncStep.ASK_NOTIFICATIONS)) + Assertions.assertEquals(listOf(AddressSyncStep.ASK_NOTIFICATIONS), stack) assertStepperAndTitleFor(AddressSyncStep.ASK_NOTIFICATIONS) } @@ -152,7 +149,7 @@ internal class AddressSyncModelTest { model.onIntent(AddressSyncIntent.Next(step = AddressSyncStep.ASK_NOTIFICATIONS)) advanceUntilIdle() - assert(stack == listOf(AddressSyncStep.ADDRESS_SYNC)) + Assertions.assertEquals(listOf(AddressSyncStep.ADDRESS_SYNC), stack) assertStepperAndTitleFor(AddressSyncStep.ADDRESS_SYNC) } @@ -175,7 +172,7 @@ internal class AddressSyncModelTest { params = MultiWalletAccountListFetcher.Params(userWalletId = walletId) ) } - assert(model.state.value == AddressSyncState.NoTokens) + Assertions.assertEquals(AddressSyncState.NoTokens, model.state.value) } @Test @@ -198,11 +195,44 @@ internal class AddressSyncModelTest { params = MultiWalletAccountListFetcher.Params(userWalletId = walletId) ) } - assert(model.state.value == AddressSyncState.Success(currenciesCount = currencies.size)) + Assertions.assertEquals( + AddressSyncState.Success(currenciesCount = currencies.size), + model.state.value, + ) + } + + @Test + fun `WHEN multiWalletAccountListFetcher emits error WHEN model is created THEN get NoToken state`() = runTest { + val currencies = listOf(mockk(), mockk(), mockk()) + coEvery { multiWalletAccountListFetcher.invoke(any()) } returns Either.Left( + value = IllegalStateException("Test") + ) + + every { multiAccountListSupplier() } returns flowOf( + listOf( + AccountList.empty( + userWalletId = walletId, + cryptoCurrencies = currencies, + ), + ), + ) + + val model = createModel(this) + advanceUntilIdle() + + coVerify { + multiWalletAccountListFetcher.invoke( + params = MultiWalletAccountListFetcher.Params(userWalletId = walletId) + ) + } + Assertions.assertEquals( + AddressSyncState.NoTokens, + model.state.value, + ) } private fun assertStepperAndTitleFor(step: AddressSyncStep) { - assert(testInnerNavigation.value.stackSize == step.pageNumber) + Assertions.assertEquals(step.pageNumber, testInnerNavigation.value.stackSize) verify { titleProvider.changeTitle(resourceReference(step.stringId)) } }