Updated on 2026-08-14
This commit is contained in:
parent
ad65a62b3f
commit
97a13da0d7
19 changed files with 176 additions and 56 deletions
|
|
@ -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<AppUpdateState> = 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)
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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}"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue