From 8e16f4e332ead556f952998f8d297d6e0e3a70c9 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 4 May 2026 13:37:43 +0100 Subject: [PATCH 1/3] Updated on 2026-08-14 --- .../src/main/res/values-pt-rBR/strings.xml | 13 + core/res/src/main/res/values-ru/strings.xml | 3 + core/res/src/main/res/values/strings.xml | 4 + .../analytics/WalletSettingsAnalyticEvents.kt | 28 +- features/hot-wallet/impl/build.gradle.kts | 11 + .../component/DefaultWalletBackupComponent.kt | 16 + ...letBackupUM.kt => WalletBackupContract.kt} | 14 +- .../walletbackup/model/WalletBackupModel.kt | 39 ++- .../walletbackup/ui/WalletBackupContent.kt | 61 ++-- .../ui/component/GoogleDriveFakeDoorDialog.kt | 17 ++ .../model/WalletBackupModelTest.kt | 276 ++++++++++++++++++ 11 files changed, 429 insertions(+), 53 deletions(-) rename features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletbackup/entity/{WalletBackupUM.kt => WalletBackupContract.kt} (61%) create mode 100644 features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletbackup/ui/component/GoogleDriveFakeDoorDialog.kt create mode 100644 features/hot-wallet/impl/src/test/kotlin/com/tangem/features/hotwallet/walletbackup/model/WalletBackupModelTest.kt diff --git a/core/res/src/main/res/values-pt-rBR/strings.xml b/core/res/src/main/res/values-pt-rBR/strings.xml index 45dbb43d03..4cd264c962 100644 --- a/core/res/src/main/res/values-pt-rBR/strings.xml +++ b/core/res/src/main/res/values-pt-rBR/strings.xml @@ -517,6 +517,7 @@ Foram encontrados fundos em endereços adicionais. Ative os Endereços Dinâmicos para acessá-los. Fundos encontrados em endereços adicionais Endereço dinâmico + O gerenciamento de endereços dinâmicos estará disponível assim que as transações pendentes estiverem na rede. %@ está completo Melhores oportunidades Limpar filtro A lista está temporariamente vazia, pois está sendo atualizada. Volte daqui a pouco. @@ -595,6 +596,7 @@ Disponível em %s Indisponível para este par Permissão necessária + É necessária permissão. Recomendado Comprado %s Comprando %s @@ -641,6 +643,7 @@ A função Aprovar é necessária para conceder permissão a outro endereço para usar uma quantidade específica de seus tokens. Por definição, os contratos inteligentes não podem acessar seus tokens a menos que você aprove. Ao \"desbloquear\" seus tokens, você autoriza o contrato inteligente StakeKit a usá-los. Os mineradores da rede recebem uma taxa de gás (paga por você) para registrar essa ação no blockchain. Você pode fazer staking de seus tokens após conceder a aprovação. Para continuar, você precisa permitir que o contrato inteligente da Polygon use seus dados. %s Para continuar, conceda %1s permissão de contratos inteligentes para usar seu %2s + As corretoras descentralizadas exigem permissão para interagir com sua carteira. %1s Conceder permissão Ilimitado Os endereços são gerados diretamente na sua carteira de hardware Tangem — prontos para usar e totalmente protegidos. @@ -1115,6 +1118,14 @@ Esta transação já foi processada. Nenhuma ação adicional é necessária. Obtendo as melhores taxas... Instantâneo + A verificação é gratuita e geralmente leva de 1 a 2 minutos. + A Tangem não terá acesso às suas informações de identidade; você compartilha os dados diretamente com o provedor regulamentado. + A verificação desbloqueia o acesso total a transações futuras com este fornecedor. + Escolha outro método + Para cumprir os requisitos regulamentares locais %@ Requer verificação de identidade. + Verificação de identidade exigida pelo provedor de pagamento + Verificar + O que é importante Ao utilizar a funcionalidade de acesso prioritário, você concorda com os termos do provedor. %1$s e %2$s O serviço é fornecido por um provedor externo. A Tangem não se responsabiliza por ele. O valor da compra não deve ser superior a %s @@ -1181,6 +1192,8 @@ Cartão de crédito ou conta bancária Compartilhe seu endereço ou código QR. Entre seus portfólios + Outro + Recarga rápida Não é necessário memorando %1$s (%2$s) sobre %3$s rede %1$s sobre %2$s rede diff --git a/core/res/src/main/res/values-ru/strings.xml b/core/res/src/main/res/values-ru/strings.xml index 0a6290a4fb..b8e2e9e0e4 100644 --- a/core/res/src/main/res/values-ru/strings.xml +++ b/core/res/src/main/res/values-ru/strings.xml @@ -293,11 +293,13 @@ Удалить Отключить Отключено + Выключение Отключить Готово Изменить Включить Включено + Включение Ошибка Комиссия сети Обменять @@ -347,6 +349,7 @@ %dмин назад месяц + Еще Комиссия сети Сумма отправки будет уменьшена на %1$s (%2$s) для покрытия выбранного уровня комиссии diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index 9e3feaa6e5..403712fc0c 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -596,6 +596,7 @@ Available from %s Unavailable for this pair Permission Required + Permission needed Recommended Bought %s Buying %s @@ -643,6 +644,7 @@ The Approve function is needed to grant permission to another address to use a specific amount of your tokens. By design, smart contracts can\'t access your tokens unless you approve. By \"unlocking\" your tokens, you authorize the StakeKit smart contract to use them. The network\'s miners receive a gas fee (paid by you) to record this action on the blockchain. You can stake your token after giving approval. To continue you need to allow Polygon smart contract to use your %s To continue, grant %1s smart contracts permission to use your %2s + Decentralized exchanges require permission to interact with your wallet. %1s Give Permission Unlimited Addresses are generated directly on your Tangem hardware wallet — ready to use and fully protected. @@ -670,6 +672,8 @@ Keeps your crypto safe and offline. Slim as a credit card, safer than a bank vault. If you do, you\'ll need to start over. Recover existing wallet via Google Drive backup + We\'re working on Google Drive backup to make wallet recovery even easier. + Google Drive backup is coming soon Google Drive backup Create a secure wallet and transfer your funds for extra protection. Create new wallet diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/analytics/WalletSettingsAnalyticEvents.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/analytics/WalletSettingsAnalyticEvents.kt index 850782d36a..b8c7e1c1f4 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/analytics/WalletSettingsAnalyticEvents.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/analytics/WalletSettingsAnalyticEvents.kt @@ -64,14 +64,18 @@ sealed class WalletSettingsAnalyticEvents( event = "Button - Recovery phrase", ) + class ButtonGoogleDriveBackup : WalletSettingsAnalyticEvents( + event = "Button - Cloud Backup", + ) + data class NoticeBackupFirst( val source: String, val action: Action, ) : WalletSettingsAnalyticEvents( event = "Notice - Backup First", params = mapOf( - AnalyticsParam.Key.SOURCE to source, - AnalyticsParam.Key.ACTION to action.value, + AnalyticsParam.SOURCE to source, + ACTION to action.value, ), ) { enum class Action(val value: String) { @@ -111,8 +115,8 @@ sealed class WalletSettingsAnalyticEvents( ) : WalletSettingsAnalyticEvents( event = "Recovery Phrase Screen Info", params = mapOf( - AnalyticsParam.Key.SOURCE to source, - AnalyticsParam.Key.ACTION to action, + AnalyticsParam.SOURCE to source, + ACTION to action, ), ) @@ -122,8 +126,8 @@ sealed class WalletSettingsAnalyticEvents( ) : WalletSettingsAnalyticEvents( event = "Recovery Phrase Screen", params = mapOf( - AnalyticsParam.Key.SOURCE to source, - AnalyticsParam.Key.ACTION to action, + AnalyticsParam.SOURCE to source, + ACTION to action, ), ) @@ -133,8 +137,8 @@ sealed class WalletSettingsAnalyticEvents( ) : WalletSettingsAnalyticEvents( event = "Recovery Phrase Check", params = mapOf( - AnalyticsParam.Key.SOURCE to source, - AnalyticsParam.Key.ACTION to action, + AnalyticsParam.SOURCE to source, + ACTION to action, ), ) @@ -144,8 +148,8 @@ sealed class WalletSettingsAnalyticEvents( ) : WalletSettingsAnalyticEvents( event = "Backup Complete Screen", params = mapOf( - AnalyticsParam.Key.SOURCE to source, - AnalyticsParam.Key.ACTION to action, + AnalyticsParam.SOURCE to source, + ACTION to action, ), ) @@ -153,14 +157,14 @@ sealed class WalletSettingsAnalyticEvents( val source: String, ) : WalletSettingsAnalyticEvents( event = "Access Code Screen Opened", - params = mapOf(AnalyticsParam.Key.SOURCE to source), + params = mapOf(AnalyticsParam.SOURCE to source), ) data class ReEnterAccessCodeScreen( val source: String, ) : WalletSettingsAnalyticEvents( event = "Re-enter Access Code Screen", - params = mapOf(AnalyticsParam.Key.SOURCE to source), + params = mapOf(AnalyticsParam.SOURCE to source), ) class ButtonStartUpgrade : WalletSettingsAnalyticEvents( diff --git a/features/hot-wallet/impl/build.gradle.kts b/features/hot-wallet/impl/build.gradle.kts index 9d2eadef6e..5fb36dc68f 100644 --- a/features/hot-wallet/impl/build.gradle.kts +++ b/features/hot-wallet/impl/build.gradle.kts @@ -11,6 +11,10 @@ android { namespace = "com.tangem.features.hotwallet.impl" } +tasks.withType().configureEach { + useJUnitPlatform() +} + dependencies { /** Api */ implementation(projects.features.hotWallet.api) @@ -78,4 +82,11 @@ dependencies { /** DI */ implementation(deps.hilt.android) kapt(deps.hilt.kapt) + + /** Test */ + testImplementation(deps.test.junit5) + testRuntimeOnly(deps.test.junit5.engine) + testImplementation(deps.test.mockk) + testImplementation(deps.test.truth) + testImplementation(deps.test.coroutine) } \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletbackup/component/DefaultWalletBackupComponent.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletbackup/component/DefaultWalletBackupComponent.kt index 5c9921bdf6..c86ca0daee 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletbackup/component/DefaultWalletBackupComponent.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletbackup/component/DefaultWalletBackupComponent.kt @@ -7,6 +7,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.decompose.model.getOrCreateModel import com.tangem.features.hotwallet.WalletBackupComponent +import com.tangem.features.hotwallet.walletbackup.entity.Action import com.tangem.features.hotwallet.walletbackup.model.WalletBackupModel import com.tangem.features.hotwallet.walletbackup.ui.WalletBackupContent import dagger.assisted.Assisted @@ -26,6 +27,21 @@ internal class DefaultWalletBackupComponent @AssistedInject constructor( WalletBackupContent( state = state, modifier = modifier, + onBackClick = { + model.onAction(Action.OnBack) + }, + onHardwareWalletClick = { + model.onAction(Action.HardwareWallet) + }, + onRecoveryPhraseClick = { + model.onAction(Action.RecoveryPhrase) + }, + onGoogleDriveClick = { + model.onAction(Action.GoogleDriveBackup(isDialogShown = true)) + }, + onGoogleDriveFakeDoorDialogDismiss = { + model.onAction(Action.GoogleDriveBackup(isDialogShown = false)) + }, ) } diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletbackup/entity/WalletBackupUM.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletbackup/entity/WalletBackupContract.kt similarity index 61% rename from features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletbackup/entity/WalletBackupUM.kt rename to features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletbackup/entity/WalletBackupContract.kt index a1219ed46a..248035f3f2 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletbackup/entity/WalletBackupUM.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletbackup/entity/WalletBackupContract.kt @@ -3,19 +3,23 @@ package com.tangem.features.hotwallet.walletbackup.entity import com.tangem.core.ui.components.label.entity.LabelUM internal data class WalletBackupUM( - val onBackClick: () -> Unit, val hardwareWalletOption: LabelUM?, val recoveryPhraseOption: LabelUM?, val googleDriveOption: LabelUM?, val googleDriveStatus: BackupStatus, - val onRecoveryPhraseClick: () -> Unit, - val onGoogleDriveClick: () -> Unit, - val onHardwareWalletClick: () -> Unit, - val backedUp: Boolean, + val isGoogleDriveDialogShown: Boolean, + val isBackedUp: Boolean, ) internal sealed class BackupStatus { object Done : BackupStatus() object ComingSoon : BackupStatus() object NoBackup : BackupStatus() +} + +internal sealed interface Action { + data object RecoveryPhrase : Action + data object HardwareWallet : Action + data class GoogleDriveBackup(val isDialogShown: Boolean) : Action + data object OnBack : Action } \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletbackup/model/WalletBackupModel.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletbackup/model/WalletBackupModel.kt index 32986a6549..c1fda203d6 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletbackup/model/WalletBackupModel.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletbackup/model/WalletBackupModel.kt @@ -16,6 +16,7 @@ import com.tangem.domain.wallets.analytics.WalletSettingsAnalyticEvents import com.tangem.domain.wallets.usecase.GetUserWalletUseCase import com.tangem.domain.wallets.usecase.UnlockHotWalletContextualUseCase import com.tangem.features.hotwallet.WalletBackupComponent +import com.tangem.features.hotwallet.walletbackup.entity.Action import com.tangem.features.hotwallet.walletbackup.entity.BackupStatus import com.tangem.features.hotwallet.walletbackup.entity.WalletBackupUM import com.tangem.utils.coroutines.CoroutineDispatcherProvider @@ -44,7 +45,6 @@ internal class WalletBackupModel @Inject constructor( val uiState: StateFlow field = MutableStateFlow( WalletBackupUM( - onBackClick = { router.pop() }, hardwareWalletOption = LabelUM( text = resourceReference(R.string.common_recommended), style = LabelStyle.ACCENT, @@ -58,10 +58,8 @@ internal class WalletBackupModel @Inject constructor( style = LabelStyle.REGULAR, ), googleDriveStatus = BackupStatus.ComingSoon, - onRecoveryPhraseClick = ::onRecoveryPhraseClick, - onGoogleDriveClick = { }, - onHardwareWalletClick = ::onHardwareWalletClick, - backedUp = false, + isGoogleDriveDialogShown = false, + isBackedUp = false, ), ) @@ -93,6 +91,19 @@ internal class WalletBackupModel @Inject constructor( super.onDestroy() } + fun onAction(action: Action) { + when (action) { + Action.RecoveryPhrase -> onRecoveryPhraseClick() + Action.HardwareWallet -> onHardwareWalletClick() + is Action.GoogleDriveBackup -> if (action.isDialogShown) { + onGoogleDriveBackupClick() + } else { + dismissGoogleDriveFakeDoorDialog() + } + Action.OnBack -> router.pop() + } + } + private fun updateBackupStatuses(userWallet: UserWallet) { uiState.update { currentState -> if (userWallet is UserWallet.Hot) { @@ -116,15 +127,16 @@ internal class WalletBackupModel @Inject constructor( ) }, googleDriveOption = LabelUM( - text = resourceReference(R.string.common_coming_soon), - style = LabelStyle.REGULAR, + text = resourceReference(R.string.hw_backup_no_backup), + style = LabelStyle.WARNING, ), - backedUp = userWallet.backedUp, + isBackedUp = userWallet.backedUp, + googleDriveStatus = BackupStatus.NoBackup, ) private fun onRecoveryPhraseClick() { analyticsEventHandler.send(WalletSettingsAnalyticEvents.ButtonRecoveryPhrase()) - if (uiState.value.backedUp) { + if (uiState.value.isBackedUp) { getUserWalletUseCase.invoke(params.userWalletId) .fold( ifLeft = { @@ -150,6 +162,15 @@ internal class WalletBackupModel @Inject constructor( } } + private fun onGoogleDriveBackupClick() { + analyticsEventHandler.send(WalletSettingsAnalyticEvents.ButtonGoogleDriveBackup()) + uiState.update { state -> state.copy(isGoogleDriveDialogShown = true) } + } + + private fun dismissGoogleDriveFakeDoorDialog() { + uiState.update { state -> state.copy(isGoogleDriveDialogShown = false) } + } + private fun showSeedPhrase(hotWallet: UserWallet.Hot) { modelScope.launch { unlockHotWalletContextualUseCase.invoke(hotWallet.hotWalletId) diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletbackup/ui/WalletBackupContent.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletbackup/ui/WalletBackupContent.kt index dc2787f9c6..f55fa8cef5 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletbackup/ui/WalletBackupContent.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletbackup/ui/WalletBackupContent.kt @@ -25,10 +25,19 @@ import com.tangem.core.ui.res.TangemThemePreview import com.tangem.features.hotwallet.common.ui.OptionBlock import com.tangem.features.hotwallet.walletbackup.entity.BackupStatus import com.tangem.features.hotwallet.walletbackup.entity.WalletBackupUM +import com.tangem.features.hotwallet.walletbackup.ui.component.GoogleDriveFakeDoorDialog -@Suppress("LongMethod") +@Suppress("LongMethod", "LongParameterList") @Composable -internal fun WalletBackupContent(state: WalletBackupUM, modifier: Modifier = Modifier) { +internal fun WalletBackupContent( + state: WalletBackupUM, + modifier: Modifier = Modifier, + onBackClick: () -> Unit, + onHardwareWalletClick: () -> Unit, + onRecoveryPhraseClick: () -> Unit, + onGoogleDriveClick: () -> Unit, + onGoogleDriveFakeDoorDialogDismiss: () -> Unit, +) { Column( modifier = modifier .background(TangemTheme.colors.background.secondary) @@ -37,7 +46,7 @@ internal fun WalletBackupContent(state: WalletBackupUM, modifier: Modifier = Mod ) { AppBarWithBackButton( text = stringResourceSafe(R.string.common_backup), - onBackClick = state.onBackClick, + onBackClick = onBackClick, ) Column( @@ -58,7 +67,7 @@ internal fun WalletBackupContent(state: WalletBackupUM, modifier: Modifier = Mod title = stringResourceSafe(R.string.hw_backup_upgrade_title), description = stringResourceSafe(R.string.hw_backup_upgrade_description), badge = { Label(state.hardwareWalletOption) }, - onClick = state.onHardwareWalletClick, + onClick = onHardwareWalletClick, enabled = true, backgroundColor = TangemTheme.colors.background.primary, ) @@ -82,7 +91,7 @@ internal fun WalletBackupContent(state: WalletBackupUM, modifier: Modifier = Mod badge = { state.recoveryPhraseOption?.let { Label(it) } }, - onClick = state.onRecoveryPhraseClick, + onClick = onRecoveryPhraseClick, enabled = true, backgroundColor = TangemTheme.colors.background.primary, ) @@ -94,13 +103,16 @@ internal fun WalletBackupContent(state: WalletBackupUM, modifier: Modifier = Mod badge = { state.googleDriveOption?.let { Label(it) } }, - onClick = state.onGoogleDriveClick, + onClick = onGoogleDriveClick, enabled = state.googleDriveStatus != BackupStatus.ComingSoon, backgroundColor = TangemTheme.colors.background.primary, ) Spacer(modifier = Modifier.size(16.dp)) } } + if (state.isGoogleDriveDialogShown) { + GoogleDriveFakeDoorDialog(onDismiss = onGoogleDriveFakeDoorDialogDismiss) + } } @Preview(showBackground = true, widthDp = 360) @@ -108,7 +120,14 @@ internal fun WalletBackupContent(state: WalletBackupUM, modifier: Modifier = Mod @Composable private fun WalletBackupContentPreview(@PreviewParameter(WalletBackupUMProvider::class) state: WalletBackupUM) { TangemThemePreview { - WalletBackupContent(state) + WalletBackupContent( + state = state, + onBackClick = {}, + onHardwareWalletClick = {}, + onRecoveryPhraseClick = {}, + onGoogleDriveClick = {}, + onGoogleDriveFakeDoorDialogDismiss = {}, + ) } } @@ -128,11 +147,8 @@ private class WalletBackupUMProvider : CollectionPreviewParameterProvider Unit) { + BasicDialog( + title = stringResourceSafe(id = R.string.hw_backup_google_drive_dialog_title), + message = stringResourceSafe(id = R.string.hw_backup_google_drive_dialog_message), + confirmButton = DialogButtonUM(onClick = onDismiss), + onDismissDialog = onDismiss, + ) +} \ No newline at end of file diff --git a/features/hot-wallet/impl/src/test/kotlin/com/tangem/features/hotwallet/walletbackup/model/WalletBackupModelTest.kt b/features/hot-wallet/impl/src/test/kotlin/com/tangem/features/hotwallet/walletbackup/model/WalletBackupModelTest.kt new file mode 100644 index 0000000000..2eafeaf0a8 --- /dev/null +++ b/features/hot-wallet/impl/src/test/kotlin/com/tangem/features/hotwallet/walletbackup/model/WalletBackupModelTest.kt @@ -0,0 +1,276 @@ +package com.tangem.features.hotwallet.walletbackup.model + +import arrow.core.left +import arrow.core.right +import com.tangem.common.routing.AppRoute +import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.core.analytics.utils.TrackingContextProxy +import com.tangem.core.decompose.model.ParamsContainer +import com.tangem.core.decompose.navigation.Router +import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.wallets.analytics.WalletSettingsAnalyticEvents +import com.tangem.domain.wallets.models.GetUserWalletError +import com.tangem.domain.wallets.usecase.GetUserWalletUseCase +import com.tangem.domain.wallets.usecase.UnlockHotWalletContextualUseCase +import com.tangem.features.hotwallet.WalletBackupComponent +import com.tangem.features.hotwallet.walletbackup.entity.Action +import com.tangem.features.hotwallet.walletbackup.entity.BackupStatus +import com.tangem.hot.sdk.model.HotWalletId +import com.tangem.hot.sdk.model.UnlockHotWallet +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.flowOf +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 + +@OptIn(ExperimentalCoroutinesApi::class) +internal class WalletBackupModelTest { + + private val getUserWalletUseCase: GetUserWalletUseCase = mockk() + private val unlockHotWalletContextualUseCase: UnlockHotWalletContextualUseCase = mockk() + private val router: Router = mockk(relaxUnitFun = true) + private val trackingContextProxy: TrackingContextProxy = mockk(relaxUnitFun = true) + private val analyticsEventHandler: AnalyticsEventHandler = mockk(relaxUnitFun = true) + private val paramsContainer: ParamsContainer = mockk() + + private val walletId = UserWalletId("011") + private val hotWalletId: HotWalletId = mockk() + private val params = WalletBackupComponent.Params( + userWalletId = walletId, + isColdWalletOptionShown = true, + ) + private val hotWalletNotBackedUp: UserWallet.Hot = mockk { + every { walletId } returns this@WalletBackupModelTest.walletId + every { hotWalletId } returns this@WalletBackupModelTest.hotWalletId + every { backedUp } returns false + } + private val hotWalletBackedUp: UserWallet.Hot = mockk { + every { walletId } returns this@WalletBackupModelTest.walletId + every { hotWalletId } returns this@WalletBackupModelTest.hotWalletId + every { backedUp } returns true + } + private val coldWallet: UserWallet.Cold = mockk { + every { walletId } returns this@WalletBackupModelTest.walletId + } + + @BeforeEach + fun setUp() { + every { paramsContainer.require() } returns params + every { getUserWalletUseCase.invokeFlow(walletId) } returns flowOf(hotWalletNotBackedUp.right()) + } + + @Test + fun `GIVEN hot wallet WHEN model is created THEN context added AND BackupScreenOpened sent AND state updated`() = + runTest { + every { getUserWalletUseCase.invokeFlow(walletId) } returns flowOf(hotWalletNotBackedUp.right()) + + val model = createModel(this) + advanceUntilIdle() + + verify { trackingContextProxy.addHotWalletContext() } + verify { + analyticsEventHandler.send(WalletSettingsAnalyticEvents.BackupScreenOpened(isBackedUp = false)) + } + val state = model.uiState.value + Assertions.assertEquals(false, state.isBackedUp) + Assertions.assertEquals(BackupStatus.NoBackup, state.googleDriveStatus) + } + + @Test + fun `GIVEN cold wallet WHEN model is created THEN context added AND BackupScreenOpened not sent AND state untouched`() = + runTest { + every { getUserWalletUseCase.invokeFlow(walletId) } returns flowOf(coldWallet.right()) + + val model = createModel(this) + advanceUntilIdle() + + verify { trackingContextProxy.addHotWalletContext() } + verify(exactly = 0) { + analyticsEventHandler.send(match { true }) + } + val state = model.uiState.value + Assertions.assertEquals(false, state.isBackedUp) + Assertions.assertEquals(BackupStatus.ComingSoon, state.googleDriveStatus) + } + + @Test + fun `GIVEN error WHEN model is created THEN context added AND BackupScreenOpened not sent`() = runTest { + every { getUserWalletUseCase.invokeFlow(walletId) } returns flowOf(GetUserWalletError.UserWalletNotFound.left()) + + createModel(this) + advanceUntilIdle() + + verify { trackingContextProxy.addHotWalletContext() } + verify(exactly = 0) { + analyticsEventHandler.send(match { true }) + } + } + + @Test + fun `WHEN onDestroy THEN trackingContextProxy removeContext is called`() = runTest { + val model = createModel(this) + advanceUntilIdle() + + model.onDestroy() + + verify { trackingContextProxy.removeContext() } + } + + @Test + fun `GIVEN backed up hot wallet AND unlock success WHEN RecoveryPhrase action THEN ViewPhrase pushed`() = runTest { + every { getUserWalletUseCase.invokeFlow(walletId) } returns flowOf(hotWalletBackedUp.right()) + every { getUserWalletUseCase.invoke(walletId) } returns hotWalletBackedUp.right() + coEvery { unlockHotWalletContextualUseCase.invoke(hotWalletId) } returns mockk().right() + + val model = createModel(this) + advanceUntilIdle() + + model.onAction(Action.RecoveryPhrase) + advanceUntilIdle() + + verify { analyticsEventHandler.send(match { true }) } + coVerify { unlockHotWalletContextualUseCase.invoke(hotWalletId) } + verify { router.push(route = AppRoute.ViewPhrase(userWalletId = walletId), onComplete = any()) } + } + + @Test + fun `GIVEN backed up hot wallet AND unlock failure WHEN RecoveryPhrase action THEN ViewPhrase not pushed`() = + runTest { + every { getUserWalletUseCase.invokeFlow(walletId) } returns flowOf(hotWalletBackedUp.right()) + every { getUserWalletUseCase.invoke(walletId) } returns hotWalletBackedUp.right() + coEvery { unlockHotWalletContextualUseCase.invoke(hotWalletId) } returns Throwable("error").left() + + val model = createModel(this) + advanceUntilIdle() + + model.onAction(Action.RecoveryPhrase) + advanceUntilIdle() + + verify { analyticsEventHandler.send(match { true }) } + coVerify { unlockHotWalletContextualUseCase.invoke(hotWalletId) } + verify(exactly = 0) { + router.push(route = AppRoute.ViewPhrase(userWalletId = walletId), onComplete = any()) + } + } + + @Test + fun `GIVEN backed up cold wallet WHEN RecoveryPhrase action THEN no navigation AND no unlock`() = runTest { + every { getUserWalletUseCase.invokeFlow(walletId) } returns flowOf(hotWalletBackedUp.right()) + every { getUserWalletUseCase.invoke(walletId) } returns coldWallet.right() + + val model = createModel(this) + advanceUntilIdle() + + model.onAction(Action.RecoveryPhrase) + advanceUntilIdle() + + verify { analyticsEventHandler.send(match { true }) } + coVerify(exactly = 0) { unlockHotWalletContextualUseCase.invoke(any()) } + verify(exactly = 0) { router.push(route = any(), onComplete = any()) } + } + + @Test + fun `GIVEN not backed up wallet WHEN RecoveryPhrase action THEN WalletActivation pushed`() = runTest { + val model = createModel(this) + advanceUntilIdle() + + model.onAction(Action.RecoveryPhrase) + advanceUntilIdle() + + verify { analyticsEventHandler.send(match { true }) } + verify(exactly = 0) { getUserWalletUseCase.invoke(walletId) } + coVerify(exactly = 0) { unlockHotWalletContextualUseCase.invoke(any()) } + verify { + router.push( + route = AppRoute.WalletActivation(userWalletId = walletId, isBackupExists = false), + onComplete = any(), + ) + } + } + + @Test + fun `WHEN HardwareWallet action THEN ButtonHardwareUpdate sent AND WalletHardwareBackup pushed`() = runTest { + val model = createModel(this) + advanceUntilIdle() + + model.onAction(Action.HardwareWallet) + + verify { analyticsEventHandler.send(match { true }) } + verify { + router.push( + route = AppRoute.WalletHardwareBackup(userWalletId = walletId), + onComplete = any(), + ) + } + } + + @Test + fun `WHEN GoogleDriveBackup with isDialogShown true THEN dialog shown AND analytics sent`() = runTest { + val model = createModel(this) + advanceUntilIdle() + + model.onAction(Action.GoogleDriveBackup(isDialogShown = true)) + + verify { analyticsEventHandler.send(match { true }) } + Assertions.assertTrue(model.uiState.value.isGoogleDriveDialogShown) + } + + @Test + fun `WHEN GoogleDriveBackup with isDialogShown false THEN dialog hidden AND no analytics sent`() = runTest { + val model = createModel(this) + advanceUntilIdle() + model.onAction(Action.GoogleDriveBackup(isDialogShown = true)) + advanceUntilIdle() + + model.onAction(Action.GoogleDriveBackup(isDialogShown = false)) + + verify(exactly = 1) { + analyticsEventHandler.send(match { true }) + } + Assertions.assertFalse(model.uiState.value.isGoogleDriveDialogShown) + } + + @Test + fun `WHEN OnBack action THEN router pop is called`() = runTest { + val model = createModel(this) + advanceUntilIdle() + + model.onAction(Action.OnBack) + + verify { router.pop(onComplete = any()) } + } + + private fun createModel(testScope: TestScope): WalletBackupModel { + return WalletBackupModel( + paramsContainer = paramsContainer, + dispatchers = testScope.createTestingCoroutineDispatcherProvider(), + getUserWalletUseCase = getUserWalletUseCase, + unlockHotWalletContextualUseCase = unlockHotWalletContextualUseCase, + router = router, + trackingContextProxy = trackingContextProxy, + analyticsEventHandler = analyticsEventHandler, + ) + } + + private fun TestScope.createTestingCoroutineDispatcherProvider(): TestingCoroutineDispatcherProvider { + val testDispatcher = StandardTestDispatcher(testScheduler) + return TestingCoroutineDispatcherProvider( + main = testDispatcher, + mainImmediate = testDispatcher, + io = testDispatcher, + default = testDispatcher, + single = testDispatcher, + ) + } +} \ No newline at end of file From c29afa81644a65a4938de32f882a9a60dbbdd65c Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 4 May 2026 17:08:11 +0100 Subject: [PATCH 2/3] Updated on 2026-08-14 --- .../component/DefaultWalletBackupComponent.kt | 16 ------ .../entity/WalletBackupContract.kt | 11 ++-- .../walletbackup/model/WalletBackupModel.kt | 27 ++++------ .../walletbackup/ui/WalletBackupContent.kt | 53 +++++++++++-------- .../model/WalletBackupModelTest.kt | 25 ++++----- 5 files changed, 55 insertions(+), 77 deletions(-) diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletbackup/component/DefaultWalletBackupComponent.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletbackup/component/DefaultWalletBackupComponent.kt index c86ca0daee..5c9921bdf6 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletbackup/component/DefaultWalletBackupComponent.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletbackup/component/DefaultWalletBackupComponent.kt @@ -7,7 +7,6 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.decompose.model.getOrCreateModel import com.tangem.features.hotwallet.WalletBackupComponent -import com.tangem.features.hotwallet.walletbackup.entity.Action import com.tangem.features.hotwallet.walletbackup.model.WalletBackupModel import com.tangem.features.hotwallet.walletbackup.ui.WalletBackupContent import dagger.assisted.Assisted @@ -27,21 +26,6 @@ internal class DefaultWalletBackupComponent @AssistedInject constructor( WalletBackupContent( state = state, modifier = modifier, - onBackClick = { - model.onAction(Action.OnBack) - }, - onHardwareWalletClick = { - model.onAction(Action.HardwareWallet) - }, - onRecoveryPhraseClick = { - model.onAction(Action.RecoveryPhrase) - }, - onGoogleDriveClick = { - model.onAction(Action.GoogleDriveBackup(isDialogShown = true)) - }, - onGoogleDriveFakeDoorDialogDismiss = { - model.onAction(Action.GoogleDriveBackup(isDialogShown = false)) - }, ) } diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletbackup/entity/WalletBackupContract.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletbackup/entity/WalletBackupContract.kt index 248035f3f2..296dcb123e 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletbackup/entity/WalletBackupContract.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletbackup/entity/WalletBackupContract.kt @@ -3,10 +3,14 @@ package com.tangem.features.hotwallet.walletbackup.entity import com.tangem.core.ui.components.label.entity.LabelUM internal data class WalletBackupUM( + val onBackClick: () -> Unit, val hardwareWalletOption: LabelUM?, val recoveryPhraseOption: LabelUM?, val googleDriveOption: LabelUM?, val googleDriveStatus: BackupStatus, + val onRecoveryPhraseClick: () -> Unit, + val onGoogleDriveAction: (Boolean) -> Unit, // boolean is for show and hide dialog + val onHardwareWalletClick: () -> Unit, val isGoogleDriveDialogShown: Boolean, val isBackedUp: Boolean, ) @@ -15,11 +19,4 @@ internal sealed class BackupStatus { object Done : BackupStatus() object ComingSoon : BackupStatus() object NoBackup : BackupStatus() -} - -internal sealed interface Action { - data object RecoveryPhrase : Action - data object HardwareWallet : Action - data class GoogleDriveBackup(val isDialogShown: Boolean) : Action - data object OnBack : Action } \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletbackup/model/WalletBackupModel.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletbackup/model/WalletBackupModel.kt index c1fda203d6..ce87a3f28c 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletbackup/model/WalletBackupModel.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletbackup/model/WalletBackupModel.kt @@ -16,13 +16,12 @@ import com.tangem.domain.wallets.analytics.WalletSettingsAnalyticEvents import com.tangem.domain.wallets.usecase.GetUserWalletUseCase import com.tangem.domain.wallets.usecase.UnlockHotWalletContextualUseCase import com.tangem.features.hotwallet.WalletBackupComponent -import com.tangem.features.hotwallet.walletbackup.entity.Action import com.tangem.features.hotwallet.walletbackup.entity.BackupStatus import com.tangem.features.hotwallet.walletbackup.entity.WalletBackupUM import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import com.tangem.utils.logging.TangemLogger import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch -import com.tangem.utils.logging.TangemLogger import java.util.concurrent.atomic.AtomicBoolean import javax.inject.Inject @@ -45,6 +44,7 @@ internal class WalletBackupModel @Inject constructor( val uiState: StateFlow field = MutableStateFlow( WalletBackupUM( + onBackClick = { router.pop() }, hardwareWalletOption = LabelUM( text = resourceReference(R.string.common_recommended), style = LabelStyle.ACCENT, @@ -58,7 +58,16 @@ internal class WalletBackupModel @Inject constructor( style = LabelStyle.REGULAR, ), googleDriveStatus = BackupStatus.ComingSoon, + onRecoveryPhraseClick = ::onRecoveryPhraseClick, + onGoogleDriveAction = { shouldShowDialog -> + if (shouldShowDialog) { + onGoogleDriveBackupClick() + } else { + dismissGoogleDriveFakeDoorDialog() + } + }, isGoogleDriveDialogShown = false, + onHardwareWalletClick = ::onHardwareWalletClick, isBackedUp = false, ), ) @@ -90,20 +99,6 @@ internal class WalletBackupModel @Inject constructor( trackingContextProxy.removeContext() super.onDestroy() } - - fun onAction(action: Action) { - when (action) { - Action.RecoveryPhrase -> onRecoveryPhraseClick() - Action.HardwareWallet -> onHardwareWalletClick() - is Action.GoogleDriveBackup -> if (action.isDialogShown) { - onGoogleDriveBackupClick() - } else { - dismissGoogleDriveFakeDoorDialog() - } - Action.OnBack -> router.pop() - } - } - private fun updateBackupStatuses(userWallet: UserWallet) { uiState.update { currentState -> if (userWallet is UserWallet.Hot) { diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletbackup/ui/WalletBackupContent.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletbackup/ui/WalletBackupContent.kt index f55fa8cef5..878f1295e8 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletbackup/ui/WalletBackupContent.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletbackup/ui/WalletBackupContent.kt @@ -27,17 +27,9 @@ import com.tangem.features.hotwallet.walletbackup.entity.BackupStatus import com.tangem.features.hotwallet.walletbackup.entity.WalletBackupUM import com.tangem.features.hotwallet.walletbackup.ui.component.GoogleDriveFakeDoorDialog -@Suppress("LongMethod", "LongParameterList") +@Suppress("LongMethod") @Composable -internal fun WalletBackupContent( - state: WalletBackupUM, - modifier: Modifier = Modifier, - onBackClick: () -> Unit, - onHardwareWalletClick: () -> Unit, - onRecoveryPhraseClick: () -> Unit, - onGoogleDriveClick: () -> Unit, - onGoogleDriveFakeDoorDialogDismiss: () -> Unit, -) { +internal fun WalletBackupContent(state: WalletBackupUM, modifier: Modifier = Modifier) { Column( modifier = modifier .background(TangemTheme.colors.background.secondary) @@ -46,7 +38,7 @@ internal fun WalletBackupContent( ) { AppBarWithBackButton( text = stringResourceSafe(R.string.common_backup), - onBackClick = onBackClick, + onBackClick = state.onBackClick, ) Column( @@ -67,7 +59,7 @@ internal fun WalletBackupContent( title = stringResourceSafe(R.string.hw_backup_upgrade_title), description = stringResourceSafe(R.string.hw_backup_upgrade_description), badge = { Label(state.hardwareWalletOption) }, - onClick = onHardwareWalletClick, + onClick = state.onHardwareWalletClick, enabled = true, backgroundColor = TangemTheme.colors.background.primary, ) @@ -91,7 +83,7 @@ internal fun WalletBackupContent( badge = { state.recoveryPhraseOption?.let { Label(it) } }, - onClick = onRecoveryPhraseClick, + onClick = state.onRecoveryPhraseClick, enabled = true, backgroundColor = TangemTheme.colors.background.primary, ) @@ -103,7 +95,9 @@ internal fun WalletBackupContent( badge = { state.googleDriveOption?.let { Label(it) } }, - onClick = onGoogleDriveClick, + onClick = { + state.onGoogleDriveAction(true) + }, enabled = state.googleDriveStatus != BackupStatus.ComingSoon, backgroundColor = TangemTheme.colors.background.primary, ) @@ -111,7 +105,11 @@ internal fun WalletBackupContent( } } if (state.isGoogleDriveDialogShown) { - GoogleDriveFakeDoorDialog(onDismiss = onGoogleDriveFakeDoorDialogDismiss) + GoogleDriveFakeDoorDialog( + onDismiss = { + state.onGoogleDriveAction(false) + }, + ) } } @@ -120,14 +118,7 @@ internal fun WalletBackupContent( @Composable private fun WalletBackupContentPreview(@PreviewParameter(WalletBackupUMProvider::class) state: WalletBackupUM) { TangemThemePreview { - WalletBackupContent( - state = state, - onBackClick = {}, - onHardwareWalletClick = {}, - onRecoveryPhraseClick = {}, - onGoogleDriveClick = {}, - onGoogleDriveFakeDoorDialogDismiss = {}, - ) + WalletBackupContent(state) } } @@ -147,7 +138,11 @@ private class WalletBackupUMProvider : CollectionPreviewParameterProvider { true }) } @@ -154,7 +149,7 @@ internal class WalletBackupModelTest { val model = createModel(this) advanceUntilIdle() - model.onAction(Action.RecoveryPhrase) + model.uiState.value.onRecoveryPhraseClick() advanceUntilIdle() verify { analyticsEventHandler.send(match { true }) } @@ -172,7 +167,7 @@ internal class WalletBackupModelTest { val model = createModel(this) advanceUntilIdle() - model.onAction(Action.RecoveryPhrase) + model.uiState.value.onRecoveryPhraseClick() advanceUntilIdle() verify { analyticsEventHandler.send(match { true }) } @@ -185,7 +180,7 @@ internal class WalletBackupModelTest { val model = createModel(this) advanceUntilIdle() - model.onAction(Action.RecoveryPhrase) + model.uiState.value.onRecoveryPhraseClick() advanceUntilIdle() verify { analyticsEventHandler.send(match { true }) } @@ -204,7 +199,7 @@ internal class WalletBackupModelTest { val model = createModel(this) advanceUntilIdle() - model.onAction(Action.HardwareWallet) + model.uiState.value.onHardwareWalletClick() verify { analyticsEventHandler.send(match { true }) } verify { @@ -220,7 +215,7 @@ internal class WalletBackupModelTest { val model = createModel(this) advanceUntilIdle() - model.onAction(Action.GoogleDriveBackup(isDialogShown = true)) + model.uiState.value.onGoogleDriveAction(true) verify { analyticsEventHandler.send(match { true }) } Assertions.assertTrue(model.uiState.value.isGoogleDriveDialogShown) @@ -230,10 +225,10 @@ internal class WalletBackupModelTest { fun `WHEN GoogleDriveBackup with isDialogShown false THEN dialog hidden AND no analytics sent`() = runTest { val model = createModel(this) advanceUntilIdle() - model.onAction(Action.GoogleDriveBackup(isDialogShown = true)) + model.uiState.value.onGoogleDriveAction(true) advanceUntilIdle() - model.onAction(Action.GoogleDriveBackup(isDialogShown = false)) + model.uiState.value.onGoogleDriveAction(false) verify(exactly = 1) { analyticsEventHandler.send(match { true }) @@ -246,7 +241,7 @@ internal class WalletBackupModelTest { val model = createModel(this) advanceUntilIdle() - model.onAction(Action.OnBack) + model.uiState.value.onBackClick() verify { router.pop(onComplete = any()) } } From b08b5df3b9254a214889556f031454cfd4c311e2 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 4 May 2026 17:48:20 +0100 Subject: [PATCH 3/3] Updated on 2026-08-14 --- .../entity/WalletBackupContract.kt | 3 +- .../walletbackup/model/WalletBackupModel.kt | 29 +++++----- .../walletbackup/ui/WalletBackupContent.kt | 24 ++------ .../ui/component/GoogleDriveFakeDoorDialog.kt | 17 ------ .../model/WalletBackupModelTest.kt | 55 +++++++++++-------- 5 files changed, 53 insertions(+), 75 deletions(-) delete mode 100644 features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletbackup/ui/component/GoogleDriveFakeDoorDialog.kt diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletbackup/entity/WalletBackupContract.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletbackup/entity/WalletBackupContract.kt index 296dcb123e..a7a4115f71 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletbackup/entity/WalletBackupContract.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletbackup/entity/WalletBackupContract.kt @@ -9,9 +9,8 @@ internal data class WalletBackupUM( val googleDriveOption: LabelUM?, val googleDriveStatus: BackupStatus, val onRecoveryPhraseClick: () -> Unit, - val onGoogleDriveAction: (Boolean) -> Unit, // boolean is for show and hide dialog + val onGoogleDriveClick: () -> Unit, val onHardwareWalletClick: () -> Unit, - val isGoogleDriveDialogShown: Boolean, val isBackedUp: Boolean, ) diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletbackup/model/WalletBackupModel.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletbackup/model/WalletBackupModel.kt index ce87a3f28c..6eb1b82ac3 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletbackup/model/WalletBackupModel.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletbackup/model/WalletBackupModel.kt @@ -7,10 +7,13 @@ 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.decompose.navigation.Router +import com.tangem.core.decompose.ui.UiMessageSender import com.tangem.core.ui.R import com.tangem.core.ui.components.label.entity.LabelStyle import com.tangem.core.ui.components.label.entity.LabelUM import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.message.DialogMessage +import com.tangem.core.ui.message.EventMessageAction import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.wallets.analytics.WalletSettingsAnalyticEvents import com.tangem.domain.wallets.usecase.GetUserWalletUseCase @@ -35,6 +38,7 @@ internal class WalletBackupModel @Inject constructor( private val router: Router, private val trackingContextProxy: TrackingContextProxy, private val analyticsEventHandler: AnalyticsEventHandler, + private val uiMessageSender: UiMessageSender, ) : Model() { private val params: WalletBackupComponent.Params = paramsContainer.require() @@ -59,14 +63,7 @@ internal class WalletBackupModel @Inject constructor( ), googleDriveStatus = BackupStatus.ComingSoon, onRecoveryPhraseClick = ::onRecoveryPhraseClick, - onGoogleDriveAction = { shouldShowDialog -> - if (shouldShowDialog) { - onGoogleDriveBackupClick() - } else { - dismissGoogleDriveFakeDoorDialog() - } - }, - isGoogleDriveDialogShown = false, + onGoogleDriveClick = ::onGoogleDriveBackupClick, onHardwareWalletClick = ::onHardwareWalletClick, isBackedUp = false, ), @@ -99,6 +96,7 @@ internal class WalletBackupModel @Inject constructor( trackingContextProxy.removeContext() super.onDestroy() } + private fun updateBackupStatuses(userWallet: UserWallet) { uiState.update { currentState -> if (userWallet is UserWallet.Hot) { @@ -159,11 +157,16 @@ internal class WalletBackupModel @Inject constructor( private fun onGoogleDriveBackupClick() { analyticsEventHandler.send(WalletSettingsAnalyticEvents.ButtonGoogleDriveBackup()) - uiState.update { state -> state.copy(isGoogleDriveDialogShown = true) } - } - - private fun dismissGoogleDriveFakeDoorDialog() { - uiState.update { state -> state.copy(isGoogleDriveDialogShown = false) } + uiMessageSender.send( + DialogMessage( + title = resourceReference(id = R.string.hw_backup_google_drive_dialog_title), + message = resourceReference(id = R.string.hw_backup_google_drive_dialog_message), + firstAction = EventMessageAction( + title = resourceReference(id = R.string.common_ok), + onClick = {}, + ), + ), + ) } private fun showSeedPhrase(hotWallet: UserWallet.Hot) { diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletbackup/ui/WalletBackupContent.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletbackup/ui/WalletBackupContent.kt index 878f1295e8..59fb6abcdd 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletbackup/ui/WalletBackupContent.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletbackup/ui/WalletBackupContent.kt @@ -25,7 +25,6 @@ import com.tangem.core.ui.res.TangemThemePreview import com.tangem.features.hotwallet.common.ui.OptionBlock import com.tangem.features.hotwallet.walletbackup.entity.BackupStatus import com.tangem.features.hotwallet.walletbackup.entity.WalletBackupUM -import com.tangem.features.hotwallet.walletbackup.ui.component.GoogleDriveFakeDoorDialog @Suppress("LongMethod") @Composable @@ -95,22 +94,13 @@ internal fun WalletBackupContent(state: WalletBackupUM, modifier: Modifier = Mod badge = { state.googleDriveOption?.let { Label(it) } }, - onClick = { - state.onGoogleDriveAction(true) - }, + onClick = state.onGoogleDriveClick, enabled = state.googleDriveStatus != BackupStatus.ComingSoon, backgroundColor = TangemTheme.colors.background.primary, ) Spacer(modifier = Modifier.size(16.dp)) } } - if (state.isGoogleDriveDialogShown) { - GoogleDriveFakeDoorDialog( - onDismiss = { - state.onGoogleDriveAction(false) - }, - ) - } } @Preview(showBackground = true, widthDp = 360) @@ -140,8 +130,7 @@ private class WalletBackupUMProvider : CollectionPreviewParameterProvider Unit) { - BasicDialog( - title = stringResourceSafe(id = R.string.hw_backup_google_drive_dialog_title), - message = stringResourceSafe(id = R.string.hw_backup_google_drive_dialog_message), - confirmButton = DialogButtonUM(onClick = onDismiss), - onDismissDialog = onDismiss, - ) -} \ No newline at end of file diff --git a/features/hot-wallet/impl/src/test/kotlin/com/tangem/features/hotwallet/walletbackup/model/WalletBackupModelTest.kt b/features/hot-wallet/impl/src/test/kotlin/com/tangem/features/hotwallet/walletbackup/model/WalletBackupModelTest.kt index e357e98530..ddf4d1d425 100644 --- a/features/hot-wallet/impl/src/test/kotlin/com/tangem/features/hotwallet/walletbackup/model/WalletBackupModelTest.kt +++ b/features/hot-wallet/impl/src/test/kotlin/com/tangem/features/hotwallet/walletbackup/model/WalletBackupModelTest.kt @@ -7,6 +7,10 @@ import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.analytics.utils.TrackingContextProxy 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.R +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.message.DialogMessage import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.wallets.analytics.WalletSettingsAnalyticEvents @@ -37,6 +41,7 @@ internal class WalletBackupModelTest { private val router: Router = mockk(relaxUnitFun = true) private val trackingContextProxy: TrackingContextProxy = mockk(relaxUnitFun = true) private val analyticsEventHandler: AnalyticsEventHandler = mockk(relaxUnitFun = true) + private val uiMessageSender: UiMessageSender = mockk(relaxUnitFun = true) private val paramsContainer: ParamsContainer = mockk() private val walletId = UserWalletId("011") @@ -123,7 +128,7 @@ internal class WalletBackupModelTest { } @Test - fun `GIVEN backed up hot wallet AND unlock success WHEN RecoveryPhrase action THEN ViewPhrase pushed`() = runTest { + fun `GIVEN backed up hot wallet AND unlock success WHEN onRecoveryPhraseClick THEN ViewPhrase pushed`() = runTest { every { getUserWalletUseCase.invokeFlow(walletId) } returns flowOf(hotWalletBackedUp.right()) every { getUserWalletUseCase.invoke(walletId) } returns hotWalletBackedUp.right() coEvery { unlockHotWalletContextualUseCase.invoke(hotWalletId) } returns mockk().right() @@ -140,7 +145,7 @@ internal class WalletBackupModelTest { } @Test - fun `GIVEN backed up hot wallet AND unlock failure WHEN RecoveryPhrase action THEN ViewPhrase not pushed`() = + fun `GIVEN backed up hot wallet AND unlock failure WHEN onRecoveryPhraseClick THEN ViewPhrase not pushed`() = runTest { every { getUserWalletUseCase.invokeFlow(walletId) } returns flowOf(hotWalletBackedUp.right()) every { getUserWalletUseCase.invoke(walletId) } returns hotWalletBackedUp.right() @@ -160,7 +165,7 @@ internal class WalletBackupModelTest { } @Test - fun `GIVEN backed up cold wallet WHEN RecoveryPhrase action THEN no navigation AND no unlock`() = runTest { + fun `GIVEN backed up cold wallet WHEN onRecoveryPhraseClick THEN no navigation AND no unlock`() = runTest { every { getUserWalletUseCase.invokeFlow(walletId) } returns flowOf(hotWalletBackedUp.right()) every { getUserWalletUseCase.invoke(walletId) } returns coldWallet.right() @@ -176,7 +181,7 @@ internal class WalletBackupModelTest { } @Test - fun `GIVEN not backed up wallet WHEN RecoveryPhrase action THEN WalletActivation pushed`() = runTest { + fun `GIVEN not backed up wallet WHEN onRecoveryPhraseClick THEN WalletActivation pushed`() = runTest { val model = createModel(this) advanceUntilIdle() @@ -195,7 +200,7 @@ internal class WalletBackupModelTest { } @Test - fun `WHEN HardwareWallet action THEN ButtonHardwareUpdate sent AND WalletHardwareBackup pushed`() = runTest { + fun `WHEN onHardwareWalletClick THEN ButtonHardwareUpdate sent AND WalletHardwareBackup pushed`() = runTest { val model = createModel(this) advanceUntilIdle() @@ -211,33 +216,34 @@ internal class WalletBackupModelTest { } @Test - fun `WHEN GoogleDriveBackup with isDialogShown true THEN dialog shown AND analytics sent`() = runTest { + fun `WHEN onGoogleDriveClick THEN DialogMessage sent AND analytics sent`() = runTest { val model = createModel(this) advanceUntilIdle() - model.uiState.value.onGoogleDriveAction(true) + model.uiState.value.onGoogleDriveClick() - verify { analyticsEventHandler.send(match { true }) } - Assertions.assertTrue(model.uiState.value.isGoogleDriveDialogShown) - } - - @Test - fun `WHEN GoogleDriveBackup with isDialogShown false THEN dialog hidden AND no analytics sent`() = runTest { - val model = createModel(this) - advanceUntilIdle() - model.uiState.value.onGoogleDriveAction(true) - advanceUntilIdle() - - model.uiState.value.onGoogleDriveAction(false) - - verify(exactly = 1) { - analyticsEventHandler.send(match { true }) + verify { + analyticsEventHandler.send( + event = match { true } + ) + } + verify { + uiMessageSender.send( + match { + val isTitleCorrect = it.title == resourceReference( + id = R.string.hw_backup_google_drive_dialog_title + ) + val isMessageCorrect = it.message == resourceReference( + id = R.string.hw_backup_google_drive_dialog_message + ) + isTitleCorrect && isMessageCorrect + } + ) } - Assertions.assertFalse(model.uiState.value.isGoogleDriveDialogShown) } @Test - fun `WHEN OnBack action THEN router pop is called`() = runTest { + fun `WHEN onBackClick THEN router pop is called`() = runTest { val model = createModel(this) advanceUntilIdle() @@ -255,6 +261,7 @@ internal class WalletBackupModelTest { router = router, trackingContextProxy = trackingContextProxy, analyticsEventHandler = analyticsEventHandler, + uiMessageSender = uiMessageSender, ) }