Skip to content

Commit d26a2ee

Browse files
PM-18681 - Update Showing Coach Mark Tour Logic To Account for Org Only Policy (#4854)
1 parent 1eb741a commit d26a2ee

File tree

2 files changed

+78
-8
lines changed

2 files changed

+78
-8
lines changed

app/src/main/java/com/x8bit/bitwarden/data/platform/manager/FirstTimeActionManagerImpl.kt

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import com.x8bit.bitwarden.data.platform.manager.model.CoachMarkTourType
99
import com.x8bit.bitwarden.data.platform.manager.model.FirstTimeState
1010
import com.x8bit.bitwarden.data.platform.manager.model.FlagKey
1111
import com.x8bit.bitwarden.data.vault.datasource.disk.VaultDiskSource
12+
import com.x8bit.bitwarden.data.vault.datasource.network.model.PolicyTypeJson
1213
import kotlinx.coroutines.CoroutineScope
1314
import kotlinx.coroutines.ExperimentalCoroutinesApi
1415
import kotlinx.coroutines.flow.Flow
@@ -158,7 +159,7 @@ class FirstTimeActionManagerImpl @Inject constructor(
158159
override val shouldShowAddLoginCoachMarkFlow: Flow<Boolean>
159160
get() = settingsDiskSource
160161
.getShouldShowAddLoginCoachMarkFlow()
161-
.map { it ?: true }
162+
.map { it != false }
162163
.mapFalseIfAnyLoginCiphersAvailable()
163164
.combine(
164165
featureFlagManager.getFeatureFlagFlow(FlagKey.OnboardingFlow),
@@ -172,11 +173,13 @@ class FirstTimeActionManagerImpl @Inject constructor(
172173
override val shouldShowGeneratorCoachMarkFlow: Flow<Boolean>
173174
get() = settingsDiskSource
174175
.getShouldShowGeneratorCoachMarkFlow()
175-
.map { it ?: true }
176+
.map { it != false }
176177
.mapFalseIfAnyLoginCiphersAvailable()
177178
.combine(
178179
featureFlagManager.getFeatureFlagFlow(FlagKey.OnboardingFlow),
179180
) { shouldShow, featureFlagEnabled ->
181+
// If the feature flag is off always return true so observers know
182+
// the card has not been shown.
180183
shouldShow && featureFlagEnabled
181184
}
182185
.distinctUntilChanged()
@@ -298,8 +301,8 @@ class FirstTimeActionManagerImpl @Inject constructor(
298301
}
299302

300303
/**
301-
* If there are any existing "Login" type ciphers then we'll map the current value
302-
* of the receiver Flow to `false`.
304+
* Maps the receiver Flow to `false` if a personal (non-org) Login cipher exists,
305+
* unless an `ONLY_ORG` policy is active, in which case it remains `true`.
303306
*/
304307
@OptIn(ExperimentalCoroutinesApi::class)
305308
private fun Flow<Boolean>.mapFalseIfAnyLoginCiphersAvailable(): Flow<Boolean> =
@@ -310,10 +313,14 @@ class FirstTimeActionManagerImpl @Inject constructor(
310313
combine(
311314
flow = this,
312315
flow2 = vaultDiskSource.getCiphers(activeUserId),
313-
) { currentValue, ciphers ->
314-
currentValue && ciphers.none {
316+
flow3 = authDiskSource.getPoliciesFlow(activeUserId),
317+
) { receiverCurrentValue, ciphers, policies ->
318+
val hasLoginsWithNoOrganizations = ciphers.any {
315319
it.login != null && it.organizationId == null
316320
}
321+
val onlyOrgPolicy = policies?.any { it.type == PolicyTypeJson.ONLY_ORG } == true
322+
323+
receiverCurrentValue && (!hasLoginsWithNoOrganizations || onlyOrgPolicy)
317324
}
318325
}
319326
.distinctUntilChanged()

app/src/test/java/com/x8bit/bitwarden/data/platform/manager/FirstTimeActionManagerTest.kt

Lines changed: 65 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,9 @@ import com.x8bit.bitwarden.data.platform.manager.model.CoachMarkTourType
1515
import com.x8bit.bitwarden.data.platform.manager.model.FirstTimeState
1616
import com.x8bit.bitwarden.data.platform.manager.model.FlagKey
1717
import com.x8bit.bitwarden.data.vault.datasource.disk.VaultDiskSource
18+
import com.x8bit.bitwarden.data.vault.datasource.network.model.PolicyTypeJson
1819
import com.x8bit.bitwarden.data.vault.datasource.network.model.SyncResponseJson
20+
import com.x8bit.bitwarden.data.vault.datasource.network.model.createMockPolicy
1921
import io.mockk.every
2022
import io.mockk.mockk
2123
import kotlinx.coroutines.flow.MutableStateFlow
@@ -425,21 +427,82 @@ class FirstTimeActionManagerTest {
425427
@Test
426428
fun `if there are login ciphers attached to an organization we should show coach marks`() =
427429
runTest {
428-
val mockJsonWithNoLoginAndWithOrganizationId = mockk<SyncResponseJson.Cipher> {
430+
val mockJsonWithLoginAndWithOrganizationId = mockk<SyncResponseJson.Cipher> {
429431
every { login } returns mockk()
430432
every { organizationId } returns "1234"
431433
}
432434
fakeAuthDiskSource.userState = MOCK_USER_STATE
433435
// Enable feature flag so flow emits updates from disk.
434436
mutableOnboardingFeatureFlow.update { true }
435437
mutableCiphersListFlow.update {
436-
listOf(mockJsonWithNoLoginAndWithOrganizationId)
438+
listOf(mockJsonWithLoginAndWithOrganizationId)
437439
}
438440
firstTimeActionManager.shouldShowGeneratorCoachMarkFlow.test {
439441
assertTrue(awaitItem())
440442
}
441443
}
442444

445+
@Test
446+
fun `if a user has a org only policy with no login items we show coach marks`() =
447+
runTest {
448+
val userState = MOCK_USER_STATE
449+
fakeAuthDiskSource.userState = userState
450+
fakeAuthDiskSource.storePolicies(
451+
userState.activeUserId,
452+
listOf(
453+
createMockPolicy(
454+
isEnabled = true,
455+
number = 2,
456+
organizationId = "1234",
457+
type = PolicyTypeJson.ONLY_ORG,
458+
),
459+
),
460+
)
461+
// Enable feature flag so flow emits updates from disk.
462+
mutableOnboardingFeatureFlow.update { true }
463+
464+
firstTimeActionManager.shouldShowGeneratorCoachMarkFlow.test {
465+
assertTrue(awaitItem())
466+
// Make sure when we change the disk source that we honor that value.
467+
fakeSettingsDiskSource.storeShouldShowGeneratorCoachMark(shouldShow = false)
468+
assertFalse(awaitItem())
469+
}
470+
}
471+
472+
@Test
473+
fun `if a user has a org only policy with login items we show coach marks`() =
474+
runTest {
475+
val userState = MOCK_USER_STATE
476+
fakeAuthDiskSource.userState = userState
477+
fakeAuthDiskSource.storePolicies(
478+
userState.activeUserId,
479+
listOf(
480+
createMockPolicy(
481+
isEnabled = true,
482+
number = 2,
483+
organizationId = "1234",
484+
type = PolicyTypeJson.ONLY_ORG,
485+
),
486+
),
487+
)
488+
val mockJsonWithLoginAndWithOrganizationId = mockk<SyncResponseJson.Cipher> {
489+
every { login } returns mockk()
490+
every { organizationId } returns "1234"
491+
}
492+
mutableCiphersListFlow.update {
493+
listOf(mockJsonWithLoginAndWithOrganizationId)
494+
}
495+
// Enable feature flag so flow emits updates from disk.
496+
mutableOnboardingFeatureFlow.update { true }
497+
498+
firstTimeActionManager.shouldShowGeneratorCoachMarkFlow.test {
499+
assertTrue(awaitItem())
500+
// Make sure when we change the disk source that we honor that value.
501+
fakeSettingsDiskSource.storeShouldShowGeneratorCoachMark(shouldShow = false)
502+
assertFalse(awaitItem())
503+
}
504+
}
505+
443506
@Suppress("MaxLineLength")
444507
@Test
445508
fun `markCoachMarkTourCompleted for the GENERATOR type sets the value to true in the disk source for should show generator coach mark`() {

0 commit comments

Comments
 (0)