mirror of
https://github.com/bitwarden/android.git
synced 2026-04-30 05:01:55 -05:00
[PM-33507] feat: Add premium upgrade banner dismissal persistence (#6657)
This commit is contained in:
@@ -123,6 +123,24 @@ interface SettingsDiskSource : FlightRecorderDiskSource {
|
||||
*/
|
||||
fun getIntroducingArchiveActionCardDismissedFlow(userId: String): Flow<Boolean?>
|
||||
|
||||
/**
|
||||
* Retrieves the stored value of whether the premium upgrade banner has been dismissed.
|
||||
*/
|
||||
fun getPremiumUpgradeBannerDismissed(userId: String): Boolean?
|
||||
|
||||
/**
|
||||
* Stores whether the premium upgrade banner has been dismissed.
|
||||
*/
|
||||
fun storePremiumUpgradeBannerDismissed(
|
||||
userId: String,
|
||||
isDismissed: Boolean?,
|
||||
)
|
||||
|
||||
/**
|
||||
* Emits updates that track [getPremiumUpgradeBannerDismissed] for the given [userId].
|
||||
*/
|
||||
fun getPremiumUpgradeBannerDismissedFlow(userId: String): Flow<Boolean?>
|
||||
|
||||
/**
|
||||
* Retrieves the biometric integrity validity for the given [userId] and
|
||||
* [systemBioIntegrityState].
|
||||
|
||||
@@ -51,6 +51,8 @@ private const val IS_DYNAMIC_COLORS_ENABLED = "isDynamicColorsEnabled"
|
||||
private const val BROWSER_AUTOFILL_DIALOG_RESHOW_TIME = "browserAutofillDialogReshowTime"
|
||||
private const val INTRODUCING_ARCHIVE_ACTION_CARD_DISMISSED =
|
||||
"introducingArchiveActionCardDismissed"
|
||||
private const val PREMIUM_UPGRADE_BANNER_DISMISSED =
|
||||
"premiumUpgradeBannerDismissed"
|
||||
|
||||
/**
|
||||
* Primary implementation of [SettingsDiskSource].
|
||||
@@ -92,6 +94,9 @@ class SettingsDiskSourceImpl(
|
||||
private val mutableIntroducingArchiveActionCardDismissedFlowMap =
|
||||
mutableMapOf<String, MutableSharedFlow<Boolean?>>()
|
||||
|
||||
private val mutablePremiumUpgradeBannerDismissedFlowMap =
|
||||
mutableMapOf<String, MutableSharedFlow<Boolean?>>()
|
||||
|
||||
private val mutableIsIconLoadingDisabledFlow = bufferedMutableSharedFlow<Boolean?>()
|
||||
|
||||
private val mutableIsCrashLoggingEnabledFlow = bufferedMutableSharedFlow<Boolean?>()
|
||||
@@ -246,6 +251,7 @@ class SettingsDiskSourceImpl(
|
||||
// - should show add login coach mark
|
||||
// - should show generator coach mark
|
||||
// - should show introducing archive action card dismissed
|
||||
// - premium upgrade banner dismissed
|
||||
}
|
||||
|
||||
override fun getIntroducingArchiveActionCardDismissed(userId: String): Boolean? =
|
||||
@@ -268,6 +274,26 @@ class SettingsDiskSourceImpl(
|
||||
getMutableIntroducingArchiveActionCardDismissedFlow(userId = userId)
|
||||
.onSubscription { emit(getIntroducingArchiveActionCardDismissed(userId = userId)) }
|
||||
|
||||
override fun getPremiumUpgradeBannerDismissed(userId: String): Boolean? =
|
||||
getBoolean(
|
||||
key = PREMIUM_UPGRADE_BANNER_DISMISSED.appendIdentifier(identifier = userId),
|
||||
)
|
||||
|
||||
override fun storePremiumUpgradeBannerDismissed(
|
||||
userId: String,
|
||||
isDismissed: Boolean?,
|
||||
) {
|
||||
putBoolean(
|
||||
key = PREMIUM_UPGRADE_BANNER_DISMISSED.appendIdentifier(identifier = userId),
|
||||
value = isDismissed,
|
||||
)
|
||||
getMutablePremiumUpgradeBannerDismissedFlow(userId = userId).tryEmit(isDismissed)
|
||||
}
|
||||
|
||||
override fun getPremiumUpgradeBannerDismissedFlow(userId: String): Flow<Boolean?> =
|
||||
getMutablePremiumUpgradeBannerDismissedFlow(userId = userId)
|
||||
.onSubscription { emit(getPremiumUpgradeBannerDismissed(userId = userId)) }
|
||||
|
||||
override fun getAccountBiometricIntegrityValidity(
|
||||
userId: String,
|
||||
systemBioIntegrityState: String,
|
||||
@@ -612,6 +638,13 @@ class SettingsDiskSourceImpl(
|
||||
bufferedMutableSharedFlow(replay = 1)
|
||||
}
|
||||
|
||||
private fun getMutablePremiumUpgradeBannerDismissedFlow(
|
||||
userId: String,
|
||||
): MutableSharedFlow<Boolean?> =
|
||||
mutablePremiumUpgradeBannerDismissedFlowMap.getOrPut(userId) {
|
||||
bufferedMutableSharedFlow(replay = 1)
|
||||
}
|
||||
|
||||
private fun getMutableLastSyncFlow(
|
||||
userId: String,
|
||||
): MutableSharedFlow<Instant?> =
|
||||
|
||||
@@ -252,6 +252,16 @@ interface SettingsRepository : FlightRecorderManager {
|
||||
*/
|
||||
fun dismissIntroducingArchiveActionCard()
|
||||
|
||||
/**
|
||||
* Gets updates for whether the premium upgrade banner is dismissed.
|
||||
*/
|
||||
fun getPremiumUpgradeBannerDismissedFlow(): StateFlow<Boolean>
|
||||
|
||||
/**
|
||||
* Stores that the premium upgrade banner has been dismissed for the active user.
|
||||
*/
|
||||
fun dismissPremiumUpgradeBanner()
|
||||
|
||||
/**
|
||||
* Stores the encrypted user key for biometrics, allowing it to be used to unlock the current
|
||||
* user's vault.
|
||||
|
||||
@@ -523,6 +523,29 @@ class SettingsRepositoryImpl(
|
||||
}
|
||||
}
|
||||
|
||||
override fun getPremiumUpgradeBannerDismissedFlow(): StateFlow<Boolean> {
|
||||
val userId = activeUserId ?: return MutableStateFlow(value = false)
|
||||
return settingsDiskSource
|
||||
.getPremiumUpgradeBannerDismissedFlow(userId = userId)
|
||||
.map { it ?: false }
|
||||
.stateIn(
|
||||
scope = unconfinedScope,
|
||||
started = SharingStarted.Eagerly,
|
||||
initialValue = settingsDiskSource
|
||||
.getPremiumUpgradeBannerDismissed(userId = userId)
|
||||
?: false,
|
||||
)
|
||||
}
|
||||
|
||||
override fun dismissPremiumUpgradeBanner() {
|
||||
activeUserId?.let {
|
||||
settingsDiskSource.storePremiumUpgradeBannerDismissed(
|
||||
userId = it,
|
||||
isDismissed = true,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun setupBiometricsKey(cipher: Cipher): BiometricsKeyResult {
|
||||
val userId = activeUserId
|
||||
?: return BiometricsKeyResult.Error(error = NoActiveUserException())
|
||||
|
||||
@@ -142,6 +142,10 @@ class SettingsDiskSourceTest {
|
||||
userId = userId,
|
||||
isDismissed = true,
|
||||
)
|
||||
settingsDiskSource.storePremiumUpgradeBannerDismissed(
|
||||
userId = userId,
|
||||
isDismissed = true,
|
||||
)
|
||||
settingsDiskSource.storeInlineAutofillEnabled(
|
||||
userId = userId,
|
||||
isInlineAutofillEnabled = true,
|
||||
@@ -175,6 +179,9 @@ class SettingsDiskSourceTest {
|
||||
assertTrue(
|
||||
settingsDiskSource.getIntroducingArchiveActionCardDismissed(userId = userId) ?: false,
|
||||
)
|
||||
assertTrue(
|
||||
settingsDiskSource.getPremiumUpgradeBannerDismissed(userId = userId) ?: false,
|
||||
)
|
||||
|
||||
// These should be cleared
|
||||
assertNull(settingsDiskSource.getVaultTimeoutInMinutes(userId = userId))
|
||||
@@ -824,6 +831,44 @@ class SettingsDiskSourceTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `getPremiumUpgradeBannerDismissed when values are present should pull from SharedPreferences`() {
|
||||
val baseKey = "bwPreferencesStorage:premiumUpgradeBannerDismissed"
|
||||
val mockUserId = "mockUserId"
|
||||
val key = "${baseKey}_$mockUserId"
|
||||
assertNull(settingsDiskSource.getPremiumUpgradeBannerDismissed(userId = mockUserId))
|
||||
fakeSharedPreferences.edit { putBoolean(key, true) }
|
||||
assertEquals(
|
||||
true,
|
||||
settingsDiskSource.getPremiumUpgradeBannerDismissed(userId = mockUserId),
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `getPremiumUpgradeBannerDismissedFlow should react to changes in storePremiumUpgradeBannerDismissed`() =
|
||||
runTest {
|
||||
val mockUserId = "mockUserId"
|
||||
settingsDiskSource
|
||||
.getPremiumUpgradeBannerDismissedFlow(userId = mockUserId)
|
||||
.test {
|
||||
// The initial values of the Flow and the property are in sync
|
||||
assertNull(
|
||||
settingsDiskSource
|
||||
.getPremiumUpgradeBannerDismissed(userId = mockUserId),
|
||||
)
|
||||
assertNull(awaitItem())
|
||||
|
||||
// Updating the disk source updates shared preferences
|
||||
settingsDiskSource.storePremiumUpgradeBannerDismissed(
|
||||
userId = mockUserId,
|
||||
isDismissed = true,
|
||||
)
|
||||
assertEquals(true, awaitItem())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `storePullToRefreshEnabled for non-null values should update SharedPreferences`() {
|
||||
val pullToRefreshBaseKey = "bwPreferencesStorage:syncOnRefresh"
|
||||
|
||||
@@ -67,6 +67,7 @@ class FakeSettingsDiskSource(
|
||||
private val storedDisableAutofillSavePrompt = mutableMapOf<String, Boolean?>()
|
||||
private val storedPullToRefreshEnabled = mutableMapOf<String, Boolean?>()
|
||||
private var storedIntroducingArchiveActionCardDismissed = mutableMapOf<String, Boolean?>()
|
||||
private var storedPremiumUpgradeBannerDismissed = mutableMapOf<String, Boolean?>()
|
||||
private val storedInlineAutofillEnabled = mutableMapOf<String, Boolean?>()
|
||||
private val storedBlockedAutofillUris = mutableMapOf<String, List<String>?>()
|
||||
private var storedIsIconLoadingDisabled: Boolean? = null
|
||||
@@ -112,6 +113,9 @@ class FakeSettingsDiskSource(
|
||||
private val mutableIntroducingArchiveActionCardDismissedFlow =
|
||||
mutableMapOf<String, MutableSharedFlow<Boolean?>>()
|
||||
|
||||
private val mutablePremiumUpgradeBannerDismissedFlow =
|
||||
mutableMapOf<String, MutableSharedFlow<Boolean?>>()
|
||||
|
||||
override var appLanguage: AppLanguage?
|
||||
get() = storedAppLanguage
|
||||
set(value) {
|
||||
@@ -341,6 +345,18 @@ class FakeSettingsDiskSource(
|
||||
getMutableIntroducingArchiveActionCardDismissedFlow(userId = userId).tryEmit(isDismissed)
|
||||
}
|
||||
|
||||
override fun getPremiumUpgradeBannerDismissed(userId: String): Boolean? =
|
||||
storedPremiumUpgradeBannerDismissed[userId]
|
||||
|
||||
override fun getPremiumUpgradeBannerDismissedFlow(userId: String): Flow<Boolean?> =
|
||||
getMutablePremiumUpgradeBannerDismissedFlow(userId = userId)
|
||||
.onSubscription { emit(getPremiumUpgradeBannerDismissed(userId = userId)) }
|
||||
|
||||
override fun storePremiumUpgradeBannerDismissed(userId: String, isDismissed: Boolean?) {
|
||||
storedPremiumUpgradeBannerDismissed[userId] = isDismissed
|
||||
getMutablePremiumUpgradeBannerDismissedFlow(userId = userId).tryEmit(isDismissed)
|
||||
}
|
||||
|
||||
override fun getInlineAutofillEnabled(userId: String): Boolean? =
|
||||
storedInlineAutofillEnabled[userId]
|
||||
|
||||
@@ -498,6 +514,13 @@ class FakeSettingsDiskSource(
|
||||
assertEquals(expected, storedIntroducingArchiveActionCardDismissed[userId])
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts that the stored premium upgrade banner dismissed matches the [expected] one.
|
||||
*/
|
||||
fun assertPremiumUpgradeBannerDismissed(userId: String, expected: Boolean?) {
|
||||
assertEquals(expected, storedPremiumUpgradeBannerDismissed[userId])
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts that the stored last sync time matches the [expected] one.
|
||||
*/
|
||||
@@ -562,6 +585,13 @@ class FakeSettingsDiskSource(
|
||||
bufferedMutableSharedFlow(replay = 1)
|
||||
}
|
||||
|
||||
private fun getMutablePremiumUpgradeBannerDismissedFlow(
|
||||
userId: String,
|
||||
): MutableSharedFlow<Boolean?> =
|
||||
mutablePremiumUpgradeBannerDismissedFlow.getOrPut(userId) {
|
||||
bufferedMutableSharedFlow(replay = 1)
|
||||
}
|
||||
|
||||
private fun getMutableShowAutoFillSettingBadgeFlow(
|
||||
userId: String,
|
||||
): MutableSharedFlow<Boolean?> = mutableShowAutoFillSettingBadgeFlowMap.getOrPut(userId) {
|
||||
|
||||
@@ -886,6 +886,38 @@ class SettingsRepositoryTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `dismissPremiumUpgradeBanner should properly update SettingsDiskSource`() {
|
||||
fakeAuthDiskSource.userState = MOCK_USER_STATE
|
||||
settingsRepository.dismissPremiumUpgradeBanner()
|
||||
assertEquals(
|
||||
true,
|
||||
fakeSettingsDiskSource.getPremiumUpgradeBannerDismissed(userId = USER_ID),
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `getPremiumUpgradeBannerDismissedFlow should react to changes in SettingsDiskSource`() =
|
||||
runTest {
|
||||
fakeAuthDiskSource.userState = MOCK_USER_STATE
|
||||
settingsRepository
|
||||
.getPremiumUpgradeBannerDismissedFlow()
|
||||
.test {
|
||||
assertFalse(awaitItem())
|
||||
fakeSettingsDiskSource.storePremiumUpgradeBannerDismissed(
|
||||
userId = USER_ID,
|
||||
isDismissed = true,
|
||||
)
|
||||
assertTrue(awaitItem())
|
||||
fakeSettingsDiskSource.storePremiumUpgradeBannerDismissed(
|
||||
userId = USER_ID,
|
||||
isDismissed = false,
|
||||
)
|
||||
assertFalse(awaitItem())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `storePullToRefreshEnabled should properly update SettingsDiskSource`() {
|
||||
fakeAuthDiskSource.userState = MOCK_USER_STATE
|
||||
|
||||
Reference in New Issue
Block a user