Skip to content

Commit 2488432

Browse files
jmartinespElementBotbmarty
authored
Hide encryption history + FTUE flow (#839)
* First attempt at implementing encrypted history banner and removing old UTDs * Get the right behavior in the timeline * Implement the designs * Extract post-processing logic, add tests * Add encryption banner to timeline screenshots * Create FTUE feature to handle welcome screen and analytics * Move classes to their own packages, add tests for `DefaultFtueState`. * Remove unnecessary private MutableStateFlow * Move some FTUE related methods and classes back to the `impl` module * Handle back press at each FTUE step * Remove unneeded `TestScope` receiver for `createState` in tests. * Use light & dark previews for the banner view. * Move color customization from `TextStyle` to `Text` component. * Rename `InfoList` design components, use them in `AnalyticsOptInView` too. * Cleanup MatrixClient. * Fix copy&paste error Co-authored-by: Benoit Marty <[email protected]> * Fix typo * Fix Maestro tests --------- Co-authored-by: ElementBot <[email protected]> Co-authored-by: Benoit Marty <[email protected]>
1 parent b42343f commit 2488432

File tree

62 files changed

+1714
-123
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

62 files changed

+1714
-123
lines changed

.maestro/tests/account/login.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ appId: ${APP_ID}
2323
- inputText: ${PASSWORD}
2424
- pressKey: Enter
2525
- tapOn: "Continue"
26+
- runFlow: ../assertions/assertWelcomeScreenDisplayed.yaml
27+
- tapOn: "Continue"
2628
- runFlow: ../assertions/assertAnalyticsDisplayed.yaml
2729
- tapOn: "Not now"
2830
- runFlow: ../assertions/assertHomeDisplayed.yaml
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
appId: ${APP_ID}
2+
---
3+
- extendedWaitUntil:
4+
visible:
5+
id: "welcome_screen-title"
6+
timeout: 10_000

appnav/build.gradle.kts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,8 @@ dependencies {
5454
implementation(projects.tests.uitests)
5555
implementation(libs.coil)
5656

57+
implementation(projects.features.ftue.api)
58+
5759
implementation(projects.services.apperror.impl)
5860
implementation(projects.services.appnavstate.api)
5961
implementation(projects.services.analytics.api)

appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt

Lines changed: 24 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ package io.element.android.appnav
1919
import android.os.Parcelable
2020
import androidx.compose.foundation.layout.Box
2121
import androidx.compose.runtime.Composable
22+
import androidx.compose.runtime.collectAsState
23+
import androidx.compose.runtime.getValue
2224
import androidx.compose.ui.Modifier
2325
import androidx.lifecycle.Lifecycle
2426
import androidx.lifecycle.lifecycleScope
@@ -41,14 +43,15 @@ import io.element.android.anvilannotations.ContributesNode
4143
import io.element.android.appnav.loggedin.LoggedInNode
4244
import io.element.android.appnav.room.RoomFlowNode
4345
import io.element.android.appnav.room.RoomLoadedFlowNode
44-
import io.element.android.features.analytics.api.AnalyticsEntryPoint
4546
import io.element.android.features.createroom.api.CreateRoomEntryPoint
4647
import io.element.android.features.invitelist.api.InviteListEntryPoint
4748
import io.element.android.features.networkmonitor.api.NetworkMonitor
4849
import io.element.android.features.networkmonitor.api.NetworkStatus
4950
import io.element.android.features.preferences.api.PreferencesEntryPoint
5051
import io.element.android.features.roomlist.api.RoomListEntryPoint
5152
import io.element.android.features.verifysession.api.VerifySessionEntryPoint
53+
import io.element.android.features.ftue.api.FtueEntryPoint
54+
import io.element.android.features.ftue.api.state.FtueState
5255
import io.element.android.libraries.architecture.BackstackNode
5356
import io.element.android.libraries.architecture.NodeInputs
5457
import io.element.android.libraries.architecture.animation.rememberDefaultTransitionHandler
@@ -64,13 +67,10 @@ import io.element.android.libraries.matrix.api.core.RoomId
6467
import io.element.android.libraries.matrix.api.sync.SyncState
6568
import io.element.android.libraries.matrix.ui.di.MatrixUIBindings
6669
import io.element.android.libraries.push.api.notifications.NotificationDrawerManager
67-
import io.element.android.services.analytics.api.AnalyticsService
6870
import io.element.android.services.appnavstate.api.AppNavigationStateService
6971
import kotlinx.coroutines.CoroutineScope
7072
import kotlinx.coroutines.flow.combine
7173
import kotlinx.coroutines.flow.distinctUntilChanged
72-
import kotlinx.coroutines.flow.launchIn
73-
import kotlinx.coroutines.flow.onEach
7474
import kotlinx.coroutines.launch
7575
import kotlinx.parcelize.Parcelize
7676

@@ -81,14 +81,14 @@ class LoggedInFlowNode @AssistedInject constructor(
8181
private val roomListEntryPoint: RoomListEntryPoint,
8282
private val preferencesEntryPoint: PreferencesEntryPoint,
8383
private val createRoomEntryPoint: CreateRoomEntryPoint,
84-
private val analyticsOptInEntryPoint: AnalyticsEntryPoint,
8584
private val appNavigationStateService: AppNavigationStateService,
8685
private val verifySessionEntryPoint: VerifySessionEntryPoint,
8786
private val inviteListEntryPoint: InviteListEntryPoint,
88-
private val analyticsService: AnalyticsService,
87+
private val ftueEntryPoint: FtueEntryPoint,
8988
private val coroutineScope: CoroutineScope,
9089
private val networkMonitor: NetworkMonitor,
9190
private val notificationDrawerManager: NotificationDrawerManager,
91+
private val ftueState: FtueState,
9292
snackbarDispatcher: SnackbarDispatcher,
9393
) : BackstackNode<LoggedInFlowNode.NavTarget>(
9494
backstack = BackStack(
@@ -99,19 +99,6 @@ class LoggedInFlowNode @AssistedInject constructor(
9999
plugins = plugins
100100
) {
101101

102-
private fun observeAnalyticsState() {
103-
analyticsService.didAskUserConsent()
104-
.distinctUntilChanged()
105-
.onEach { isConsentAsked ->
106-
if (isConsentAsked) {
107-
backstack.removeLast(NavTarget.AnalyticsOptIn)
108-
} else {
109-
backstack.push(NavTarget.AnalyticsOptIn)
110-
}
111-
}
112-
.launchIn(lifecycleScope)
113-
}
114-
115102
interface Callback : Plugin {
116103
fun onOpenBugReport() = Unit
117104
}
@@ -136,7 +123,7 @@ class LoggedInFlowNode @AssistedInject constructor(
136123

137124
override fun onBuilt() {
138125
super.onBuilt()
139-
observeAnalyticsState()
126+
140127
lifecycle.subscribe(
141128
onCreate = {
142129
plugins<LifecycleCallback>().forEach { it.onFlowCreated(id, inputs.matrixClient) }
@@ -146,6 +133,10 @@ class LoggedInFlowNode @AssistedInject constructor(
146133
// TODO We do not support Space yet, so directly navigate to main space
147134
appNavigationStateService.onNavigateToSpace(id, MAIN_SPACE)
148135
loggedInFlowProcessor.observeEvents(coroutineScope)
136+
137+
if (ftueState.shouldDisplayFlow.value) {
138+
backstack.push(NavTarget.Ftue)
139+
}
149140
},
150141
onResume = {
151142
syncService.startSync()
@@ -209,7 +200,7 @@ class LoggedInFlowNode @AssistedInject constructor(
209200
object InviteList : NavTarget
210201

211202
@Parcelize
212-
object AnalyticsOptIn : NavTarget
203+
object Ftue : NavTarget
213204
}
214205

215206
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
@@ -306,8 +297,13 @@ class LoggedInFlowNode @AssistedInject constructor(
306297
.callback(callback)
307298
.build()
308299
}
309-
NavTarget.AnalyticsOptIn -> {
310-
analyticsOptInEntryPoint.createNode(this, buildContext)
300+
NavTarget.Ftue -> {
301+
ftueEntryPoint.nodeBuilder(this, buildContext)
302+
.callback(object : FtueEntryPoint.Callback {
303+
override fun onFtueFlowFinished() {
304+
backstack.pop()
305+
}
306+
}).build()
311307
}
312308
}
313309
}
@@ -335,7 +331,11 @@ class LoggedInFlowNode @AssistedInject constructor(
335331
transitionHandler = rememberDefaultTransitionHandler(),
336332
)
337333

338-
PermanentChild(navTarget = NavTarget.Permanent)
334+
val isFtueDisplayed by ftueState.shouldDisplayFlow.collectAsState()
335+
336+
if (!isFtueDisplayed) {
337+
PermanentChild(navTarget = NavTarget.Permanent)
338+
}
339339
}
340340
}
341341

build.gradle.kts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -246,7 +246,7 @@ koverMerged {
246246
name = "Check code coverage of states"
247247
target = kotlinx.kover.api.VerificationTarget.CLASS
248248
overrideClassFilter {
249-
includes += "*State"
249+
includes += "^*State$"
250250
excludes += "io.element.android.libraries.matrix.api.timeline.item.event.OtherState$*"
251251
excludes += "io.element.android.libraries.matrix.api.timeline.item.event.EventSendState$*"
252252
excludes += "io.element.android.libraries.matrix.api.room.RoomMembershipState*"
@@ -262,6 +262,8 @@ koverMerged {
262262
excludes += "io.element.android.libraries.maplibre.compose.CameraPositionState*"
263263
excludes += "io.element.android.libraries.maplibre.compose.SaveableCameraPositionState"
264264
excludes += "io.element.android.libraries.maplibre.compose.SymbolState*"
265+
excludes += "io.element.android.features.ftue.api.state.*"
266+
excludes += "io.element.android.features.ftue.impl.welcome.state.*"
265267
}
266268
bound {
267269
minValue = 90

features/analytics/impl/src/main/kotlin/io/element/android/features/analytics/impl/AnalyticsOptInView.kt

Lines changed: 56 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,11 @@
1616

1717
package io.element.android.features.analytics.impl
1818

19+
import androidx.activity.compose.BackHandler
1920
import androidx.compose.foundation.background
2021
import androidx.compose.foundation.clickable
21-
import androidx.compose.foundation.layout.Arrangement
2222
import androidx.compose.foundation.layout.Box
2323
import androidx.compose.foundation.layout.Column
24-
import androidx.compose.foundation.layout.Row
2524
import androidx.compose.foundation.layout.fillMaxSize
2625
import androidx.compose.foundation.layout.fillMaxWidth
2726
import androidx.compose.foundation.layout.imePadding
@@ -48,6 +47,8 @@ import androidx.compose.ui.unit.dp
4847
import io.element.android.features.analytics.api.AnalyticsOptInEvents
4948
import io.element.android.libraries.designsystem.atomic.molecules.ButtonColumnMolecule
5049
import io.element.android.libraries.designsystem.atomic.molecules.IconTitleSubtitleMolecule
50+
import io.element.android.libraries.designsystem.atomic.molecules.InfoListItem
51+
import io.element.android.libraries.designsystem.atomic.molecules.InfoListOrganism
5152
import io.element.android.libraries.designsystem.atomic.pages.HeaderFooterPage
5253
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
5354
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
@@ -60,6 +61,7 @@ import io.element.android.libraries.designsystem.theme.temporaryColorBgSpecial
6061
import io.element.android.libraries.designsystem.utils.LogCompositions
6162
import io.element.android.libraries.theme.ElementTheme
6263
import io.element.android.libraries.ui.strings.CommonStrings
64+
import kotlinx.collections.immutable.persistentListOf
6365

6466
@Composable
6567
fun AnalyticsOptInView(
@@ -69,14 +71,30 @@ fun AnalyticsOptInView(
6971
) {
7072
LogCompositions(tag = "Analytics", msg = "Root")
7173
val eventSink = state.eventSink
74+
75+
fun onTermsAccepted() {
76+
eventSink(AnalyticsOptInEvents.EnableAnalytics(true))
77+
}
78+
79+
fun onTermsDeclined() {
80+
eventSink(AnalyticsOptInEvents.EnableAnalytics(false))
81+
}
82+
83+
BackHandler(onBack = ::onTermsDeclined)
7284
HeaderFooterPage(
7385
modifier = modifier
7486
.fillMaxSize()
7587
.systemBarsPadding()
7688
.imePadding(),
7789
header = { AnalyticsOptInHeader(state, onClickTerms) },
7890
content = { AnalyticsOptInContent() },
79-
footer = { AnalyticsOptInFooter(eventSink) })
91+
footer = {
92+
AnalyticsOptInFooter(
93+
onTermsAccepted = ::onTermsAccepted,
94+
onTermsDeclined = ::onTermsDeclined,
95+
)
96+
}
97+
)
8098
}
8199

82100
@Composable
@@ -114,6 +132,19 @@ private fun AnalyticsOptInHeader(
114132
}
115133
}
116134

135+
@Composable
136+
private fun CheckIcon(modifier: Modifier = Modifier) {
137+
Icon(
138+
modifier = Modifier
139+
.size(20.dp)
140+
.background(color = MaterialTheme.colorScheme.background, shape = CircleShape)
141+
.padding(2.dp),
142+
imageVector = Icons.Rounded.Check,
143+
contentDescription = null,
144+
tint = ElementTheme.colors.textActionAccent,
145+
)
146+
}
147+
117148
@Composable
118149
private fun AnalyticsOptInContent(
119150
modifier: Modifier = Modifier,
@@ -125,80 +156,45 @@ private fun AnalyticsOptInContent(
125156
verticalBias = -0.4f
126157
)
127158
) {
128-
Column(
129-
verticalArrangement = Arrangement.spacedBy(4.dp)
130-
) {
131-
AnalyticsOptInContentRow(
132-
text = stringResource(id = R.string.screen_analytics_prompt_data_usage),
133-
idx = 0
134-
)
135-
AnalyticsOptInContentRow(
136-
text = stringResource(id = R.string.screen_analytics_prompt_third_party_sharing),
137-
idx = 1
138-
)
139-
AnalyticsOptInContentRow(
140-
text = stringResource(id = R.string.screen_analytics_prompt_settings),
141-
idx = 2
142-
)
143-
}
144-
}
145-
}
146-
147-
@Composable
148-
private fun AnalyticsOptInContentRow(
149-
text: String,
150-
idx: Int,
151-
modifier: Modifier = Modifier,
152-
) {
153-
val radius = 14.dp
154-
val bgShape = when (idx) {
155-
0 -> RoundedCornerShape(topStart = radius, topEnd = radius)
156-
2 -> RoundedCornerShape(bottomStart = radius, bottomEnd = radius)
157-
else -> RoundedCornerShape(0.dp)
158-
}
159-
Row(
160-
modifier = modifier
161-
.fillMaxWidth()
162-
.background(
163-
color = ElementTheme.colors.temporaryColorBgSpecial,
164-
shape = bgShape,
165-
)
166-
.padding(vertical = 12.dp, horizontal = 20.dp),
167-
) {
168-
Icon(
169-
modifier = Modifier
170-
.size(20.dp)
171-
.background(color = MaterialTheme.colorScheme.background, shape = CircleShape)
172-
.padding(2.dp),
173-
imageVector = Icons.Rounded.Check,
174-
contentDescription = null,
175-
tint = ElementTheme.colors.textActionAccent,
176-
)
177-
Text(
178-
modifier = Modifier.padding(start = 16.dp),
179-
text = text,
180-
style = ElementTheme.typography.fontBodyMdMedium,
181-
color = MaterialTheme.colorScheme.primary,
159+
InfoListOrganism(
160+
items = persistentListOf(
161+
InfoListItem(
162+
message = stringResource(id = R.string.screen_analytics_prompt_data_usage),
163+
iconComposable = { CheckIcon() },
164+
),
165+
InfoListItem(
166+
message = stringResource(id = R.string.screen_analytics_prompt_third_party_sharing),
167+
iconComposable = { CheckIcon() },
168+
),
169+
InfoListItem(
170+
message = stringResource(id = R.string.screen_analytics_prompt_settings),
171+
iconComposable = { CheckIcon() },
172+
),
173+
),
174+
textStyle = ElementTheme.typography.fontBodyMdMedium,
175+
iconTint = ElementTheme.colors.textPrimary,
176+
backgroundColor = ElementTheme.colors.temporaryColorBgSpecial
182177
)
183178
}
184179
}
185180

186181
@Composable
187182
private fun AnalyticsOptInFooter(
188-
eventSink: (AnalyticsOptInEvents) -> Unit,
183+
onTermsAccepted: () -> Unit,
184+
onTermsDeclined: () -> Unit,
189185
modifier: Modifier = Modifier,
190186
) {
191187
ButtonColumnMolecule(
192188
modifier = modifier,
193189
) {
194190
Button(
195-
onClick = { eventSink(AnalyticsOptInEvents.EnableAnalytics(true)) },
191+
onClick = onTermsAccepted,
196192
modifier = Modifier.fillMaxWidth(),
197193
) {
198194
Text(text = stringResource(id = CommonStrings.action_ok))
199195
}
200196
TextButton(
201-
onClick = { eventSink(AnalyticsOptInEvents.EnableAnalytics(false)) },
197+
onClick = onTermsDeclined,
202198
modifier = Modifier.fillMaxWidth(),
203199
) {
204200
Text(text = stringResource(id = CommonStrings.action_not_now))

features/ftue/api/build.gradle.kts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
/*
2+
* Copyright (c) 2023 New Vector Ltd
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
plugins {
18+
id("io.element.android-library")
19+
}
20+
21+
android {
22+
namespace = "io.element.android.features.ftue.api"
23+
}
24+
25+
dependencies {
26+
implementation(projects.libraries.architecture)
27+
}

0 commit comments

Comments
 (0)