Skip to content

Commit

Permalink
Replace notification permission dialog with a screen (#1223)
Browse files Browse the repository at this point in the history
* Replace notification permission dialog with a screen

---------

Co-authored-by: ElementBot <benoitm+elementbot@element.io>
  • Loading branch information
jmartinesp and ElementBot authored Sep 5, 2023
1 parent d4b527f commit cfdccc9
Show file tree
Hide file tree
Showing 43 changed files with 1,027 additions and 69 deletions.
2 changes: 0 additions & 2 deletions appnav/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,6 @@ dependencies {
implementation(projects.libraries.designsystem)
implementation(projects.libraries.matrixui)
implementation(projects.libraries.uiStrings)
implementation(projects.libraries.permissions.api)
implementation(projects.libraries.permissions.noop)

implementation(libs.coil)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,6 @@

package io.element.android.appnav.loggedin

import android.Manifest
import android.os.Build
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
Expand All @@ -30,8 +28,6 @@ import io.element.android.features.networkmonitor.api.NetworkStatus
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.roomlist.RoomListService
import io.element.android.libraries.permissions.api.PermissionsPresenter
import io.element.android.libraries.permissions.noop.NoopPermissionsPresenter
import io.element.android.libraries.push.api.PushService
import kotlinx.coroutines.delay
import javax.inject.Inject
Expand All @@ -40,20 +36,10 @@ private const val DELAY_BEFORE_SHOWING_SYNC_SPINNER_IN_MILLIS = 1500L

class LoggedInPresenter @Inject constructor(
private val matrixClient: MatrixClient,
private val permissionsPresenterFactory: PermissionsPresenter.Factory,
private val networkMonitor: NetworkMonitor,
private val pushService: PushService,
) : Presenter<LoggedInState> {

private val postNotificationPermissionsPresenter by lazy {
// Ask for POST_NOTIFICATION PERMISSION on Android 13+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
permissionsPresenterFactory.create(Manifest.permission.POST_NOTIFICATIONS)
} else {
NoopPermissionsPresenter()
}
}

@Composable
override fun present(): LoggedInState {
LaunchedEffect(Unit) {
Expand All @@ -66,7 +52,6 @@ class LoggedInPresenter @Inject constructor(

val roomListState by matrixClient.roomListService.state.collectAsState()
val networkStatus by networkMonitor.connectivity.collectAsState()
val permissionsState = postNotificationPermissionsPresenter.present()
var showSyncSpinner by remember {
mutableStateOf(false)
}
Expand All @@ -82,7 +67,6 @@ class LoggedInPresenter @Inject constructor(
}
return LoggedInState(
showSyncSpinner = showSyncSpinner,
permissionsState = permissionsState,
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,6 @@

package io.element.android.appnav.loggedin

import io.element.android.libraries.permissions.api.PermissionsState

data class LoggedInState(
val showSyncSpinner: Boolean,
val permissionsState: PermissionsState,
)
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@
package io.element.android.appnav.loggedin

import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.libraries.permissions.api.createDummyPostNotificationPermissionsState

open class LoggedInStateProvider : PreviewParameterProvider<LoggedInState> {
override val values: Sequence<LoggedInState>
Expand All @@ -32,5 +31,4 @@ fun aLoggedInState(
showSyncSpinner: Boolean = true,
) = LoggedInState(
showSyncSpinner = showSyncSpinner,
permissionsState = createDummyPostNotificationPermissionsState(),
)
Original file line number Diff line number Diff line change
Expand Up @@ -23,21 +23,16 @@ import androidx.compose.foundation.layout.systemBarsPadding
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
import io.element.android.libraries.androidutils.system.openAppSettingsPage
import io.element.android.libraries.designsystem.preview.DayNightPreviews
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.permissions.api.PermissionsView

@Composable
fun LoggedInView(
state: LoggedInState,
modifier: Modifier = Modifier
) {
val context = LocalContext.current

Box(
modifier = modifier
.fillMaxSize()
Expand All @@ -49,10 +44,6 @@ fun LoggedInView(
.align(Alignment.TopCenter),
isVisible = state.showSyncSpinner,
)
PermissionsView(
state = state.permissionsState,
openSystemSettings = context::openAppSettingsPage
)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,6 @@ import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.roomlist.RoomListService
import io.element.android.libraries.matrix.test.FakeMatrixClient
import io.element.android.libraries.matrix.test.roomlist.FakeRoomListService
import io.element.android.libraries.permissions.api.PermissionsPresenter
import io.element.android.libraries.permissions.noop.NoopPermissionsPresenter
import io.element.android.libraries.push.api.PushService
import io.element.android.libraries.pushproviders.api.Distributor
import io.element.android.libraries.pushproviders.api.PushProvider
Expand All @@ -43,7 +41,7 @@ class LoggedInPresenterTest {
presenter.present()
}.test {
val initialState = awaitItem()
assertThat(initialState.permissionsState.permission).isEmpty()
assertThat(initialState.showSyncSpinner).isFalse()
}
}

Expand All @@ -68,11 +66,6 @@ class LoggedInPresenterTest {
): LoggedInPresenter {
return LoggedInPresenter(
matrixClient = FakeMatrixClient(roomListService = roomListService),
permissionsPresenterFactory = object : PermissionsPresenter.Factory {
override fun create(permission: String): PermissionsPresenter {
return NoopPermissionsPresenter()
}
},
networkMonitor = FakeNetworkMonitor(networkStatus),
pushService = object : PushService {
override fun notificationStyleChanged() {
Expand Down
1 change: 1 addition & 0 deletions changelog.d/897.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add a notification permission screen to the initial flow.
6 changes: 6 additions & 0 deletions features/ftue/impl/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,10 @@ dependencies {
implementation(projects.libraries.testtags)
implementation(projects.features.analytics.api)
implementation(projects.services.analytics.api)
implementation(projects.libraries.permissions.api)
implementation(projects.libraries.permissions.noop)
implementation(projects.services.toolbox.api)
implementation(projects.services.toolbox.test)

testImplementation(libs.test.junit)
testImplementation(libs.coroutines.test)
Expand All @@ -51,6 +55,8 @@ dependencies {
testImplementation(libs.test.turbine)
testImplementation(projects.libraries.matrix.test)
testImplementation(projects.services.analytics.test)
testImplementation(projects.libraries.permissions.impl)
testImplementation(projects.libraries.permissions.test)

ksp(libs.showkase.processor)
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import io.element.android.anvilannotations.ContributesNode
import io.element.android.features.analytics.api.AnalyticsEntryPoint
import io.element.android.features.ftue.api.FtueEntryPoint
import io.element.android.features.ftue.impl.migration.MigrationScreenNode
import io.element.android.features.ftue.impl.notifications.NotificationsOptInNode
import io.element.android.features.ftue.impl.state.DefaultFtueState
import io.element.android.features.ftue.impl.state.FtueStep
import io.element.android.features.ftue.impl.welcome.WelcomeNode
Expand Down Expand Up @@ -79,6 +80,9 @@ class FtueFlowNode @AssistedInject constructor(
@Parcelize
data object WelcomeScreen : NavTarget

@Parcelize
data object NotificationsOptIn : NavTarget

@Parcelize
data object AnalyticsOptIn : NavTarget
}
Expand Down Expand Up @@ -124,6 +128,14 @@ class FtueFlowNode @AssistedInject constructor(
}
createNode<WelcomeNode>(buildContext, listOf(callback))
}
NavTarget.NotificationsOptIn -> {
val callback = object : NotificationsOptInNode.Callback {
override fun onNotificationsOptInFinished() {
lifecycleScope.launch { moveToNextStep() }
}
}
createNode<NotificationsOptInNode>(buildContext, listOf(callback))
}
NavTarget.AnalyticsOptIn -> {
analyticsEntryPoint.createNode(this, buildContext)
}
Expand All @@ -138,6 +150,9 @@ class FtueFlowNode @AssistedInject constructor(
FtueStep.WelcomeScreen -> {
backstack.newRoot(NavTarget.WelcomeScreen)
}
FtueStep.NotificationsOptIn -> {
backstack.newRoot(NavTarget.NotificationsOptIn)
}
FtueStep.AnalyticsOptIn -> {
backstack.replace(NavTarget.AnalyticsOptIn)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package io.element.android.features.ftue.impl.notifications

sealed interface NotificationsOptInEvents {
data object ContinueClicked : NotificationsOptInEvents
data object NotNowClicked : NotificationsOptInEvents
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package io.element.android.features.ftue.impl.notifications

import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import io.element.android.anvilannotations.ContributesNode
import io.element.android.libraries.architecture.NodeInputs
import io.element.android.libraries.architecture.inputs
import io.element.android.libraries.di.AppScope

@ContributesNode(AppScope::class)
class NotificationsOptInNode @AssistedInject constructor(
@Assisted buildContext: BuildContext,
@Assisted plugins: List<Plugin>,
private val presenterFactory: NotificationsOptInPresenter.Factory,
) : Node(buildContext, plugins = plugins) {

interface Callback: NodeInputs {
fun onNotificationsOptInFinished()
}

private val callback = inputs<Callback>()

private val presenter: NotificationsOptInPresenter by lazy {
presenterFactory.create(callback)
}

@Composable
override fun View(modifier: Modifier) {
val state = presenter.present()
NotificationsOptInView(
state = state,
onBack = { callback.onNotificationsOptInFinished() },
modifier = modifier
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package io.element.android.features.ftue.impl.notifications

import android.Manifest
import android.os.Build
import androidx.annotation.RequiresApi
import androidx.compose.runtime.Composable
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.permissions.api.PermissionStateProvider
import io.element.android.libraries.permissions.api.PermissionsEvents
import io.element.android.libraries.permissions.api.PermissionsPresenter
import io.element.android.libraries.permissions.noop.NoopPermissionsPresenter
import io.element.android.services.toolbox.api.sdk.BuildVersionSdkIntProvider
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch

class NotificationsOptInPresenter @AssistedInject constructor(
private val permissionsPresenterFactory: PermissionsPresenter.Factory,
@Assisted private val callback: NotificationsOptInNode.Callback,
private val appCoroutineScope: CoroutineScope,
private val permissionStateProvider: PermissionStateProvider,
private val buildVersionSdkIntProvider: BuildVersionSdkIntProvider,
) : Presenter<NotificationsOptInState> {

@AssistedFactory
interface Factory {
fun create(callback: NotificationsOptInNode.Callback): NotificationsOptInPresenter
}

private val postNotificationPermissionsPresenter by lazy {
// Ask for POST_NOTIFICATION PERMISSION on Android 13+
if (buildVersionSdkIntProvider.isAtLeast(Build.VERSION_CODES.TIRAMISU)) {
permissionsPresenterFactory.create(Manifest.permission.POST_NOTIFICATIONS)
} else {
NoopPermissionsPresenter()
}
}

@Composable
override fun present(): NotificationsOptInState {
val notificationPremissionsState = postNotificationPermissionsPresenter.present()

fun handleEvents(event: NotificationsOptInEvents) {
when (event) {
NotificationsOptInEvents.ContinueClicked -> {
if (notificationPremissionsState.permissionGranted) {
callback.onNotificationsOptInFinished()
} else {
notificationPremissionsState.eventSink(PermissionsEvents.OpenSystemDialog)
}
}
NotificationsOptInEvents.NotNowClicked -> {
if (buildVersionSdkIntProvider.isAtLeast(Build.VERSION_CODES.TIRAMISU)) {
appCoroutineScope.setPermissionDenied()
}
callback.onNotificationsOptInFinished()
}
}
}

return NotificationsOptInState(
notificationsPermissionState = notificationPremissionsState,
eventSink = ::handleEvents
)
}

@RequiresApi(Build.VERSION_CODES.TIRAMISU)
private fun CoroutineScope.setPermissionDenied() = launch {
permissionStateProvider.setPermissionDenied(Manifest.permission.POST_NOTIFICATIONS, true)
}
}
Loading

0 comments on commit cfdccc9

Please sign in to comment.