diff --git a/app/src/main/java/com/tangem/tap/routing/startup/AppStartupGateComponent.kt b/app/src/main/java/com/tangem/tap/routing/startup/AppStartupGateComponent.kt index cd2911409e..716fc5f9da 100644 --- a/app/src/main/java/com/tangem/tap/routing/startup/AppStartupGateComponent.kt +++ b/app/src/main/java/com/tangem/tap/routing/startup/AppStartupGateComponent.kt @@ -125,8 +125,9 @@ internal class AppStartupGateComponent @AssistedInject constructor( AppUpdateState.ForceUpdate -> ForceUpdateComponent.Mode.Force AppUpdateState.Brick -> ForceUpdateComponent.Mode.Brick AppUpdateState.OsTooOld -> ForceUpdateComponent.Mode.OsTooOld - AppUpdateState.OptionalUpdate -> ForceUpdateComponent.Mode.Optional - AppUpdateState.NoUpdate -> null + AppUpdateState.OptionalUpdate, + AppUpdateState.NoUpdate, + -> null } private sealed interface GateConfig { diff --git a/domain/app-update/src/main/kotlin/com/tangem/domain/appupdate/usecase/GetAppUpdateStateUseCase.kt b/domain/app-update/src/main/kotlin/com/tangem/domain/appupdate/usecase/GetAppUpdateStateUseCase.kt index 1a41a3e8a0..ad85e96d3d 100644 --- a/domain/app-update/src/main/kotlin/com/tangem/domain/appupdate/usecase/GetAppUpdateStateUseCase.kt +++ b/domain/app-update/src/main/kotlin/com/tangem/domain/appupdate/usecase/GetAppUpdateStateUseCase.kt @@ -8,6 +8,8 @@ import com.tangem.domain.appupdate.repository.AppUpdateRepository import com.tangem.utils.coroutines.runSuspendCatching import com.tangem.utils.info.AppInfoProvider import com.tangem.utils.logging.TangemLogger +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow class GetAppUpdateStateUseCase( private val repository: AppUpdateRepository, @@ -17,11 +19,21 @@ class GetAppUpdateStateUseCase( /** * Instant decision computed from the cached thresholds and the current app/OS version. No network. - * Records the optional update as shown when it decides to show it (24h throttle). Never throws — - * any failure resolves to [AppUpdateState.NoUpdate] so the initial navigation is never blocked. + * Records the optional update as shown as it decides to show it (24h throttle) — used by the startup gate. + * Never throws — any failure resolves to [AppUpdateState.NoUpdate] so the initial navigation is never blocked. */ - suspend fun getCached(): AppUpdateState = runSuspendCatching { - resolve(freshCachedInfoOrNull(), recordOptionalShown = true) + suspend fun getCached(): AppUpdateState = getCachedState(recordOptionalShown = true) + + /** + * Cache-only update state as a cold [Flow], with the optional-update throttle disabled so a + * non-dismissible banner stays visible while the optional update is relevant. No network, no side effects. + */ + fun getBannerStateFlow(): Flow = flow { + emit(getCachedState(recordOptionalShown = false)) + } + + private suspend fun getCachedState(recordOptionalShown: Boolean): AppUpdateState = runSuspendCatching { + resolve(freshCachedInfoOrNull(), recordOptionalShown = recordOptionalShown) }.getOrElse { error -> TangemLogger.e("Unable to resolve cached app update state", error) AppUpdateState.NoUpdate @@ -65,7 +77,7 @@ class GetAppUpdateStateUseCase( val minSupportedVersion = info.minSupportedVersion?.let(AppVersion::parseOrNull) if (minSupportedVersion != null && - appVersion <= minSupportedVersion && + appVersion < minSupportedVersion && isEscapable(latestVersion, minSupportedVersion) ) { return blockingStateFor(info.minSupportedOSVersion, deviceOsVersion, AppUpdateState.OsTooOld) diff --git a/domain/app-update/src/test/kotlin/com/tangem/domain/appupdate/GetAppUpdateStateUseCaseTest.kt b/domain/app-update/src/test/kotlin/com/tangem/domain/appupdate/GetAppUpdateStateUseCaseTest.kt index dd415faf26..59477145b0 100644 --- a/domain/app-update/src/test/kotlin/com/tangem/domain/appupdate/GetAppUpdateStateUseCaseTest.kt +++ b/domain/app-update/src/test/kotlin/com/tangem/domain/appupdate/GetAppUpdateStateUseCaseTest.kt @@ -15,6 +15,7 @@ import io.mockk.coVerify import io.mockk.every import io.mockk.just import io.mockk.mockk +import kotlinx.coroutines.flow.first import kotlinx.coroutines.test.runTest import org.junit.jupiter.api.Test @@ -78,9 +79,9 @@ internal class GetAppUpdateStateUseCaseTest { } @Test - fun `GIVEN app at min supported and OS ok WHEN getCached THEN ForceUpdate`() = runTest { + fun `GIVEN app below min supported and OS ok WHEN getCached THEN ForceUpdate`() = runTest { givenCached( - appVersion = "5.0", + appVersion = "4.9", osVersion = "14", info = info(minSupportedVersion = "5.0", minSupportedOSVersion = "10", latestVersion = "5.1"), ) @@ -89,9 +90,20 @@ internal class GetAppUpdateStateUseCaseTest { } @Test - fun `GIVEN app at min supported and OS too old WHEN getCached THEN OsTooOld`() = runTest { + fun `GIVEN app exactly at min supported WHEN getCached THEN not force and optional`() = runTest { givenCached( appVersion = "5.0", + osVersion = "14", + info = info(minSupportedVersion = "5.0", minSupportedOSVersion = "10", latestVersion = "5.1"), + ) + + assertThat(useCase.getCached()).isEqualTo(AppUpdateState.OptionalUpdate) + } + + @Test + fun `GIVEN app below min supported and OS too old WHEN getCached THEN OsTooOld`() = runTest { + givenCached( + appVersion = "4.9", osVersion = "9", info = info(minSupportedVersion = "5.0", minSupportedOSVersion = "10", latestVersion = "5.1"), ) @@ -165,6 +177,16 @@ internal class GetAppUpdateStateUseCaseTest { assertThat(useCase.getCached()).isEqualTo(AppUpdateState.OptionalUpdate) } + @Test + fun `GIVEN optional shown recently WHEN getBannerStateFlow THEN OptionalUpdate without throttle`() = runTest { + givenCached(appVersion = "5.0", info = info(latestVersion = "5.37")) + coEvery { repository.getOptionalUpdateShown() } returns + OptionalUpdateShown(version = "5.37", shownAtMillis = NOW) + + assertThat(useCase.getBannerStateFlow().first()).isEqualTo(AppUpdateState.OptionalUpdate) + coVerify(exactly = 0) { repository.setOptionalUpdateShown(any()) } + } + @Test fun `GIVEN all thresholds null WHEN getCached THEN NoUpdate`() = runTest { givenCached(info = info()) diff --git a/domain/feedback/models/src/main/kotlin/com/tangem/domain/feedback/models/FeedbackEmailType.kt b/domain/feedback/models/src/main/kotlin/com/tangem/domain/feedback/models/FeedbackEmailType.kt index 6f331c5b08..88a3632202 100644 --- a/domain/feedback/models/src/main/kotlin/com/tangem/domain/feedback/models/FeedbackEmailType.kt +++ b/domain/feedback/models/src/main/kotlin/com/tangem/domain/feedback/models/FeedbackEmailType.kt @@ -63,6 +63,10 @@ sealed interface FeedbackEmailType { override val walletMetaInfo: WalletMetaInfo? = null } + data object AppUpdateProblem : FeedbackEmailType { + override val walletMetaInfo: WalletMetaInfo? = null + } + sealed class Visa : FeedbackEmailType { abstract val customerId: String diff --git a/domain/feedback/src/main/java/com/tangem/domain/feedback/SendFeedbackEmailUseCase.kt b/domain/feedback/src/main/java/com/tangem/domain/feedback/SendFeedbackEmailUseCase.kt index 2dd2ee624f..b9cad341b9 100644 --- a/domain/feedback/src/main/java/com/tangem/domain/feedback/SendFeedbackEmailUseCase.kt +++ b/domain/feedback/src/main/java/com/tangem/domain/feedback/SendFeedbackEmailUseCase.kt @@ -94,6 +94,7 @@ class SendFeedbackEmailUseCase( is FeedbackEmailType.PreActivatedWallet, is FeedbackEmailType.CardAttestationFailed, is FeedbackEmailType.BiometricsAuthenticationFailed, + is FeedbackEmailType.AppUpdateProblem, is FeedbackEmailType.Visa.Dispute, is FeedbackEmailType.Visa.DisputeV2, is FeedbackEmailType.Visa.FeatureIsBeta, diff --git a/domain/feedback/src/main/java/com/tangem/domain/feedback/utils/EmailMessageBodyResolver.kt b/domain/feedback/src/main/java/com/tangem/domain/feedback/utils/EmailMessageBodyResolver.kt index 112112773f..a1cdd441e9 100644 --- a/domain/feedback/src/main/java/com/tangem/domain/feedback/utils/EmailMessageBodyResolver.kt +++ b/domain/feedback/src/main/java/com/tangem/domain/feedback/utils/EmailMessageBodyResolver.kt @@ -32,6 +32,7 @@ class EmailMessageBodyResolver( is FeedbackEmailType.ScanningProblem, is FeedbackEmailType.CardAttestationFailed, is FeedbackEmailType.BiometricsAuthenticationFailed, + is FeedbackEmailType.AppUpdateProblem, -> addPhoneInfoBody() is FeedbackEmailType.Visa.Activation -> addUserRequestBody(type.walletMetaInfo) is FeedbackEmailType.Visa.DirectUserRequest -> addUserRequestBody(type.walletMetaInfo) diff --git a/domain/feedback/src/main/java/com/tangem/domain/feedback/utils/EmailMessageTitleResolver.kt b/domain/feedback/src/main/java/com/tangem/domain/feedback/utils/EmailMessageTitleResolver.kt index 4a9c9d3e94..1d2e234ce8 100644 --- a/domain/feedback/src/main/java/com/tangem/domain/feedback/utils/EmailMessageTitleResolver.kt +++ b/domain/feedback/src/main/java/com/tangem/domain/feedback/utils/EmailMessageTitleResolver.kt @@ -30,6 +30,7 @@ internal class EmailMessageTitleResolver(private val resources: Resources) { is FeedbackEmailType.Visa.KycRejected, is FeedbackEmailType.PreActivatedWallet, is FeedbackEmailType.BackupProblem, + is FeedbackEmailType.AppUpdateProblem, -> R.string.feedback_preface_support is FeedbackEmailType.RateCanBeBetter -> R.string.feedback_preface_rate_negative is FeedbackEmailType.ScanningProblem -> R.string.feedback_preface_scan_failed diff --git a/domain/feedback/src/main/java/com/tangem/domain/feedback/utils/EmailSubjectResolver.kt b/domain/feedback/src/main/java/com/tangem/domain/feedback/utils/EmailSubjectResolver.kt index 9249b48b89..fd6c302f7b 100644 --- a/domain/feedback/src/main/java/com/tangem/domain/feedback/utils/EmailSubjectResolver.kt +++ b/domain/feedback/src/main/java/com/tangem/domain/feedback/utils/EmailSubjectResolver.kt @@ -40,6 +40,7 @@ internal class EmailSubjectResolver(private val resources: Resources) { } FeedbackEmailType.CardAttestationFailed -> "Card attestation failed" FeedbackEmailType.BiometricsAuthenticationFailed -> "Biometrics authentication failed" + FeedbackEmailType.AppUpdateProblem -> resources.getStringSafe(R.string.feedback_subject_support_tangem) is FeedbackEmailType.Visa.Activation -> "[Visa] [Activation] {auto-filled subject}" is FeedbackEmailType.Visa.DirectUserRequest -> "[Visa] {auto-filled subject}" is FeedbackEmailType.Visa.FailedIssueCard -> "[Visa] {auto-filled subject}" diff --git a/features/force-update/api/src/main/kotlin/com/tangem/features/forceupdate/ForceUpdateComponent.kt b/features/force-update/api/src/main/kotlin/com/tangem/features/forceupdate/ForceUpdateComponent.kt index 3eb6fcdd29..821b0f64fb 100644 --- a/features/force-update/api/src/main/kotlin/com/tangem/features/forceupdate/ForceUpdateComponent.kt +++ b/features/force-update/api/src/main/kotlin/com/tangem/features/forceupdate/ForceUpdateComponent.kt @@ -9,5 +9,5 @@ interface ForceUpdateComponent : ComposableContentComponent { data class Params(val mode: Mode) - enum class Mode { Force, Brick, OsTooOld, Optional } + enum class Mode { Force, Brick, OsTooOld } } \ No newline at end of file diff --git a/features/force-update/impl/build.gradle.kts b/features/force-update/impl/build.gradle.kts index 83fb3ba0a7..2ac31ea16a 100644 --- a/features/force-update/impl/build.gradle.kts +++ b/features/force-update/impl/build.gradle.kts @@ -33,6 +33,8 @@ dependencies { /** Domain modules */ implementation(projects.domain.appUpdate) + implementation(projects.domain.feedback) + implementation(projects.domain.feedback.models) /** DI */ implementation(deps.hilt.android) diff --git a/features/force-update/impl/src/main/kotlin/com/tangem/features/forceupdate/impl/model/ForceUpdateModel.kt b/features/force-update/impl/src/main/kotlin/com/tangem/features/forceupdate/impl/model/ForceUpdateModel.kt index 387306f78b..54d6c600c9 100644 --- a/features/force-update/impl/src/main/kotlin/com/tangem/features/forceupdate/impl/model/ForceUpdateModel.kt +++ b/features/force-update/impl/src/main/kotlin/com/tangem/features/forceupdate/impl/model/ForceUpdateModel.kt @@ -7,6 +7,8 @@ import com.tangem.core.navigation.url.AppStoreOpener import com.tangem.core.ui.extensions.resourceReference import com.tangem.domain.appupdate.model.AppUpdateState import com.tangem.domain.appupdate.usecase.GetAppUpdateStateUseCase +import com.tangem.domain.feedback.SendFeedbackEmailUseCase +import com.tangem.domain.feedback.models.FeedbackEmailType import com.tangem.features.forceupdate.ForceUpdateComponent import com.tangem.features.forceupdate.ForceUpdateContinuation import com.tangem.features.forceupdate.impl.R @@ -25,6 +27,7 @@ internal class ForceUpdateModel @Inject constructor( private val appStoreOpener: AppStoreOpener, private val forceUpdateContinuation: ForceUpdateContinuation, private val getAppUpdateStateUseCase: GetAppUpdateStateUseCase, + private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase, paramsContainer: ParamsContainer, ) : Model() { @@ -43,36 +46,24 @@ internal class ForceUpdateModel @Inject constructor( accent = Accent.Red, title = resourceReference(R.string.force_update_warning_title), description = resourceReference(R.string.force_update_warning_message), - isBlocking = true, onUpdateClick = ::onUpdateClick, - onLaterClick = null, - ) - ForceUpdateComponent.Mode.Optional -> ForceUpdateUM( - mode = mode, - accent = Accent.Yellow, - title = resourceReference(R.string.force_update_warning_title), - description = resourceReference(R.string.force_update_warning_message), - isBlocking = false, - onUpdateClick = ::onUpdateClick, - onLaterClick = ::onLaterClick, + onSupportClick = ::onSupportClick, ) ForceUpdateComponent.Mode.Brick -> ForceUpdateUM( mode = mode, accent = Accent.Red, title = resourceReference(R.string.force_update_brick_title), description = resourceReference(R.string.force_update_brick_description), - isBlocking = true, onUpdateClick = null, - onLaterClick = null, + onSupportClick = ::onSupportClick, ) ForceUpdateComponent.Mode.OsTooOld -> ForceUpdateUM( mode = mode, accent = Accent.Red, title = resourceReference(R.string.force_update_os_title), description = resourceReference(R.string.force_update_os_description), - isBlocking = true, onUpdateClick = null, - onLaterClick = null, + onSupportClick = null, ) } @@ -80,8 +71,10 @@ internal class ForceUpdateModel @Inject constructor( appStoreOpener.openStorePage() } - private fun onLaterClick() { - forceUpdateContinuation.dismiss() + private fun onSupportClick() { + modelScope.launch { + sendFeedbackEmailUseCase(type = FeedbackEmailType.AppUpdateProblem) + } } /** @@ -105,7 +98,8 @@ internal class ForceUpdateModel @Inject constructor( AppUpdateState.ForceUpdate -> ForceUpdateComponent.Mode.Force AppUpdateState.Brick -> ForceUpdateComponent.Mode.Brick AppUpdateState.OsTooOld -> ForceUpdateComponent.Mode.OsTooOld - AppUpdateState.OptionalUpdate -> ForceUpdateComponent.Mode.Optional - AppUpdateState.NoUpdate -> null + AppUpdateState.OptionalUpdate, + AppUpdateState.NoUpdate, + -> null } } \ No newline at end of file diff --git a/features/force-update/impl/src/main/kotlin/com/tangem/features/forceupdate/impl/ui/ForceUpdateContent.kt b/features/force-update/impl/src/main/kotlin/com/tangem/features/forceupdate/impl/ui/ForceUpdateContent.kt index b2f371f681..2bd1b0c2a1 100644 --- a/features/force-update/impl/src/main/kotlin/com/tangem/features/forceupdate/impl/ui/ForceUpdateContent.kt +++ b/features/force-update/impl/src/main/kotlin/com/tangem/features/forceupdate/impl/ui/ForceUpdateContent.kt @@ -1,7 +1,6 @@ package com.tangem.features.forceupdate.impl.ui import android.content.res.Configuration -import androidx.activity.compose.BackHandler import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -39,10 +38,6 @@ import com.tangem.features.forceupdate.impl.ui.state.ForceUpdateUM.Accent @Composable internal fun ForceUpdateContent(state: ForceUpdateUM, modifier: Modifier = Modifier) { - BackHandler(enabled = !state.isBlocking) { - state.onLaterClick?.invoke() - } - val accentColor = when (state.accent) { Accent.Red -> TangemTheme.colors3.icon.accent.red Accent.Yellow -> TangemTheme.colors3.icon.accent.yellow @@ -111,6 +106,14 @@ private fun Buttons(state: ForceUpdateUM, modifier: Modifier = Modifier) { modifier = modifier, verticalArrangement = Arrangement.spacedBy(12.dp), ) { + state.onSupportClick?.let { onClick -> + TangemButton( + modifier = Modifier.fillMaxWidth(), + variant = TangemButton.Variant.Secondary, + text = resourceReference(R.string.common_contact_support), + onClick = onClick, + ) + } state.onUpdateClick?.let { onClick -> TangemButton( modifier = Modifier.fillMaxWidth(), @@ -119,14 +122,6 @@ private fun Buttons(state: ForceUpdateUM, modifier: Modifier = Modifier) { onClick = onClick, ) } - state.onLaterClick?.let { onClick -> - TangemButton( - modifier = Modifier.fillMaxWidth(), - variant = TangemButton.Variant.Secondary, - text = resourceReference(R.string.common_later), - onClick = onClick, - ) - } } } @@ -154,9 +149,8 @@ private fun PreviewForce() { accent = Accent.Red, title = TextReference.Str("Update Required"), description = TextReference.Str("Please update the application to the latest version."), - isBlocking = true, onUpdateClick = {}, - onLaterClick = null, + onSupportClick = {}, ), ) } @@ -165,17 +159,16 @@ private fun PreviewForce() { @Preview(showBackground = true, widthDp = 360, heightDp = 780) @Preview(showBackground = true, widthDp = 360, heightDp = 780, uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun PreviewOptional() { +private fun PreviewBrick() { TangemThemePreviewRedesign { ForceUpdateContent( state = ForceUpdateUM( - mode = ForceUpdateComponent.Mode.Optional, - accent = Accent.Yellow, - title = TextReference.Str("Update Required"), - description = TextReference.Str("Please update the application to the latest version."), - isBlocking = false, - onUpdateClick = {}, - onLaterClick = {}, + mode = ForceUpdateComponent.Mode.Brick, + accent = Accent.Red, + title = TextReference.Str("Device not supported"), + description = TextReference.Str("This device can't run the OS version Tangem needs."), + onUpdateClick = null, + onSupportClick = {}, ), ) } diff --git a/features/force-update/impl/src/main/kotlin/com/tangem/features/forceupdate/impl/ui/state/ForceUpdateUM.kt b/features/force-update/impl/src/main/kotlin/com/tangem/features/forceupdate/impl/ui/state/ForceUpdateUM.kt index c187dc04ac..9ab3570cff 100644 --- a/features/force-update/impl/src/main/kotlin/com/tangem/features/forceupdate/impl/ui/state/ForceUpdateUM.kt +++ b/features/force-update/impl/src/main/kotlin/com/tangem/features/forceupdate/impl/ui/state/ForceUpdateUM.kt @@ -10,9 +10,8 @@ internal data class ForceUpdateUM( val accent: Accent, val title: TextReference, val description: TextReference, - val isBlocking: Boolean, val onUpdateClick: (() -> Unit)?, - val onLaterClick: (() -> Unit)?, + val onSupportClick: (() -> Unit)?, ) { enum class Accent { Red, Yellow } diff --git a/features/wallet/impl/build.gradle.kts b/features/wallet/impl/build.gradle.kts index e687109014..427f869789 100644 --- a/features/wallet/impl/build.gradle.kts +++ b/features/wallet/impl/build.gradle.kts @@ -80,6 +80,7 @@ dependencies { api(projects.domain.analytics) api(projects.domain.appCurrency) api(projects.domain.appTheme) + api(projects.domain.appUpdate) api(projects.domain.assetsdiscovery) api(projects.domain.balanceHiding) api(projects.domain.card) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletWarningsClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletWarningsClickIntents.kt index 7d5540b348..17f2eb01c2 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletWarningsClickIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletWarningsClickIntents.kt @@ -12,6 +12,7 @@ import com.tangem.core.analytics.models.event.AssetsDiscoveryAnalyticsEvent import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.ui.UiMessageSender import com.tangem.core.navigation.review.ReviewManager +import com.tangem.core.navigation.url.AppStoreOpener import com.tangem.domain.assetsdiscovery.usecase.AcknowledgeAssetsDiscoveryCompletionUseCase import com.tangem.domain.card.SetCardWasScannedUseCase import com.tangem.domain.common.wallets.UserWalletsListRepository @@ -73,6 +74,8 @@ internal interface WalletWarningsClickIntents { fun onSupportClick() + fun onSoftUpdateClick() + fun onBackupErrorClick() fun onNoteMigrationButtonClick(url: String) @@ -111,6 +114,7 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor( private val dispatchers: CoroutineDispatcherProvider, private val getWalletMetaInfoUseCase: GetWalletMetaInfoUseCase, private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase, + private val appStoreOpener: AppStoreOpener, private val multiNetworkStatusFetcher: MultiNetworkStatusFetcher, private val multiQuoteStatusFetcher: MultiQuoteStatusFetcher, private val multiStakingBalanceFetcher: MultiStakingBalanceFetcher, @@ -248,6 +252,10 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor( } } + override fun onSoftUpdateClick() { + appStoreOpener.openStorePage() + } + override fun onBackupErrorClick() { val userWallet = getSelectedUserWallet() ?: return diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/WalletWarningsAnalyticsSender.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/WalletWarningsAnalyticsSender.kt index 39c028fe87..2ae777b2c7 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/WalletWarningsAnalyticsSender.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/WalletWarningsAnalyticsSender.kt @@ -145,6 +145,7 @@ internal class WalletWarningsAnalyticsSender @Inject constructor( WalletNotificationUM.TangemPayUnreachable, is WalletNotificationUM.YieldBoostPromo, is WalletNotificationUM.AssetsDiscoveryCompleted, + is WalletNotificationUM.SoftUpdateAvailable, -> null } } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetWalletNotificationsFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetWalletNotificationsFactory.kt index b829542c37..f20ba2082f 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetWalletNotificationsFactory.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetWalletNotificationsFactory.kt @@ -5,6 +5,8 @@ import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.ui.ds.message.TangemMessageEffect import com.tangem.core.ui.extensions.resourceReference import com.tangem.domain.account.status.producer.SingleAccountStatusListProducer +import com.tangem.domain.appupdate.model.AppUpdateState +import com.tangem.domain.appupdate.usecase.GetAppUpdateStateUseCase import com.tangem.domain.assetsdiscovery.model.AssetsDiscoveryProgress import com.tangem.domain.assetsdiscovery.usecase.ObserveAssetsDiscoveryUseCase import com.tangem.domain.card.CardTypesResolver @@ -49,6 +51,7 @@ internal class GetWalletNotificationsFactory @Inject constructor( private val getAccessCodeSkippedUseCase: GetAccessCodeSkippedUseCase, private val hasSingleWalletSignedHashesUseCase: HasSingleWalletSignedHashesUseCase, private val observeAssetsDiscoveryUseCase: ObserveAssetsDiscoveryUseCase, + private val getAppUpdateStateUseCase: GetAppUpdateStateUseCase, ) { fun create(userWallet: UserWallet, clickIntents: WalletClickIntents): Flow> { val cardTypesResolver = (userWallet as? UserWallet.Cold)?.scanResponse?.cardTypesResolver @@ -68,7 +71,8 @@ internal class GetWalletNotificationsFactory @Inject constructor( flow2 = isNeedToBackupUseCase(userWallet.walletId).distinctUntilChanged(), flow3 = getAccessCodeSkippedUseCase(userWallet.walletId).distinctUntilChanged(), flow4 = assetsDiscoveryProgressFlow, - ) { accountList, isNeedToBackup, shouldAccessCodeSkipped, assetsDiscoveryProgress -> + flow5 = getAppUpdateStateUseCase.getBannerStateFlow(), + ) { accountList, isNeedToBackup, shouldAccessCodeSkipped, assetsDiscoveryProgress, appUpdateState -> val totalFiatBalance = accountList.totalFiatBalance val flattenCurrencies = accountList.flattenCurrencies() @@ -126,10 +130,22 @@ internal class GetWalletNotificationsFactory @Inject constructor( walletClickIntents = clickIntents, ) } + + addSoftUpdateBanner(appUpdateState, clickIntents) }.sortedBy { it.type.ordinal }.toImmutableList() } } + private fun MutableList.addSoftUpdateBanner( + appUpdateState: AppUpdateState, + clickIntents: WalletClickIntents, + ) { + addIf( + element = WalletNotificationUM.SoftUpdateAvailable(onUpdateClick = clickIntents::onSoftUpdateClick), + condition = appUpdateState == AppUpdateState.OptionalUpdate, + ) + } + private fun MutableList.addUsedOutdatedDataNotification(totalFiatBalance: TotalFiatBalance) { addIf( element = WalletNotificationUM.UsedOutdatedData, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletNotificationUM.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletNotificationUM.kt index da9e8aca22..d5ce3f8b39 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletNotificationUM.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletNotificationUM.kt @@ -352,6 +352,27 @@ internal sealed class WalletNotificationUM(val messageUM: TangemMessageUM, val t ), type = WalletNotificationType.Warning, ) + + data class SoftUpdateAvailable(val onUpdateClick: () -> Unit) : WalletNotificationUM( + messageUM = TangemMessageUM( + id = "SoftUpdateAvailableNotification", + title = resourceReference(id = CoreResR.string.force_update_banner_title), + subtitle = resourceReference(id = CoreResR.string.force_update_banner_message), + iconUM = TangemIconUM.Icon( + iconRes = R.drawable.ic_attention_default_24, + tintReference = { TangemTheme.colors2.graphic.status.attention }, + ), + messageEffect = TangemMessageEffect.Warning, + buttonsUM = persistentListOf( + TangemMessageButtonUM( + text = resourceReference(id = CoreResR.string.force_update_action), + type = TangemButtonType.Primary, + onClick = onUpdateClick, + ), + ), + ), + type = WalletNotificationType.Warning, + ) // endregion // region Promo diff --git a/features/wallet/impl/src/test/kotlin/com/tangem/feature/wallet/presentation/wallet/domain/GetWalletNotificationsFactoryTest.kt b/features/wallet/impl/src/test/kotlin/com/tangem/feature/wallet/presentation/wallet/domain/GetWalletNotificationsFactoryTest.kt index 8d5fc81b54..b11ed54c03 100644 --- a/features/wallet/impl/src/test/kotlin/com/tangem/feature/wallet/presentation/wallet/domain/GetWalletNotificationsFactoryTest.kt +++ b/features/wallet/impl/src/test/kotlin/com/tangem/feature/wallet/presentation/wallet/domain/GetWalletNotificationsFactoryTest.kt @@ -5,6 +5,8 @@ import com.tangem.common.test.domain.token.MockCryptoCurrencyFactory import com.tangem.domain.account.models.AccountStatusList import com.tangem.domain.account.status.producer.SingleAccountStatusListProducer import com.tangem.domain.account.status.supplier.SingleAccountStatusListSupplier +import com.tangem.domain.appupdate.model.AppUpdateState +import com.tangem.domain.appupdate.usecase.GetAppUpdateStateUseCase import com.tangem.domain.assetsdiscovery.model.AssetsDiscoveryProgress import com.tangem.domain.assetsdiscovery.usecase.ObserveAssetsDiscoveryUseCase import com.tangem.domain.card.CardTypesResolver @@ -56,6 +58,7 @@ internal class GetWalletNotificationsFactoryTest { private val getAccessCodeSkippedUseCase: GetAccessCodeSkippedUseCase = mockk() private val hasSingleWalletSignedHashesUseCase: HasSingleWalletSignedHashesUseCase = mockk() private val observeAssetsDiscoveryUseCase: ObserveAssetsDiscoveryUseCase = mockk() + private val getAppUpdateStateUseCase: GetAppUpdateStateUseCase = mockk() private val singleAccountStatusListSupplier: SingleAccountStatusListSupplier = mockk() private val clickIntents: WalletClickIntents = mockk(relaxed = true) @@ -73,6 +76,7 @@ internal class GetWalletNotificationsFactoryTest { getAccessCodeSkippedUseCase = getAccessCodeSkippedUseCase, hasSingleWalletSignedHashesUseCase = hasSingleWalletSignedHashesUseCase, observeAssetsDiscoveryUseCase = observeAssetsDiscoveryUseCase, + getAppUpdateStateUseCase = getAppUpdateStateUseCase, ) @BeforeEach @@ -85,6 +89,7 @@ internal class GetWalletNotificationsFactoryTest { getAccessCodeSkippedUseCase, hasSingleWalletSignedHashesUseCase, observeAssetsDiscoveryUseCase, + getAppUpdateStateUseCase, singleAccountStatusListSupplier, clickIntents, coldResolver, @@ -100,6 +105,7 @@ internal class GetWalletNotificationsFactoryTest { every { isNeedToBackupUseCase(any()) } returns flowOf(false) every { getAccessCodeSkippedUseCase(any()) } returns flowOf(true) every { observeAssetsDiscoveryUseCase(any()) } returns flowOf(AssetsDiscoveryProgress.Idle) + every { getAppUpdateStateUseCase.getBannerStateFlow() } returns flowOf(AppUpdateState.NoUpdate) every { hasSingleWalletSignedHashesUseCase(any(), any()) } returns flowOf(false) // Balance is loaded and non-zero, so both the outdated-data and add-funds banners stay hidden. stubAccountStatusList(balance = LOADED_NON_ZERO) @@ -648,6 +654,42 @@ internal class GetWalletNotificationsFactoryTest { } // endregion + // region SoftUpdate + @Test + fun `GIVEN optional update WHEN create THEN soft-update banner is shown`() = runTest { + // Arrange + every { getAppUpdateStateUseCase.getBannerStateFlow() } returns flowOf(AppUpdateState.OptionalUpdate) + + // Act + val result = factory.create(coldWallet, clickIntents).first() + + // Assert + assertThat(result.any { it is WalletNotificationUM.SoftUpdateAvailable }).isTrue() + } + + @ParameterizedTest + @MethodSource("provideNonOptionalUpdateStates") + fun `GIVEN non-optional update state WHEN create THEN soft-update banner is hidden`( + state: AppUpdateState, + ) = runTest { + // Arrange + every { getAppUpdateStateUseCase.getBannerStateFlow() } returns flowOf(state) + + // Act + val result = factory.create(coldWallet, clickIntents).first() + + // Assert + assertThat(result.none { it is WalletNotificationUM.SoftUpdateAvailable }).isTrue() + } + + private fun provideNonOptionalUpdateStates() = listOf( + AppUpdateState.NoUpdate, + AppUpdateState.ForceUpdate, + AppUpdateState.Brick, + AppUpdateState.OsTooOld, + ) + // endregion + // region TangemPay warnings @ParameterizedTest @MethodSource("provideTangemPayModels")