Skip to content

Commit c63a4f5

Browse files
authored
Request notification permissions when adding bookmark (#1572)
* Request notification permissions when adding bookmark * no local notifications on Wear * Fix for mobile and exclude jvm
1 parent 9db74cd commit c63a4f5

File tree

13 files changed

+135
-3
lines changed

13 files changed

+135
-3
lines changed

gradle/libs.versions.toml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,9 @@ materialkolor = "2.0.0"
3838
multiplatform-settings = "1.3.0"
3939
nav-compose = "2.8.9"
4040
okio = "3.10.2"
41+
permissions = "0.19.1"
42+
permissionsCompose = "0.19.1"
43+
permissionsNotifications = "0.19.1"
4144
protolayout = "1.3.0-alpha10"
4245
robolectric = "4.14.1"
4346
room = "2.6.1"
@@ -86,6 +89,9 @@ apollo-runtime = { module = "com.apollographql.apollo:apollo-runtime" }
8689
apollo-execution-spring = { module = "com.apollographql.execution:apollo-execution-spring", version.ref = "apollo-kotlin-execution" }
8790
apollo-execution-reporting = { module = "com.apollographql.execution:apollo-execution-reporting", version.ref = "apollo-kotlin-execution" }
8891
apollo-execution-gradle-plugin = { module = "com.apollographql.execution:apollo-execution-gradle-plugin", version.ref = "apollo-kotlin-execution" }
92+
permissions-compose = { module = "dev.icerock.moko:permissions-compose", version.ref = "permissionsCompose" }
93+
permissions-notifications = { module = "dev.icerock.moko:permissions-notifications", version.ref = "permissionsNotifications" }
94+
permissions = { module = "dev.icerock.moko:permissions", version.ref = "permissions" }
8995
spring-boot = { module = "org.springframework.boot:spring-boot", version.ref = "spring" }
9096
spring-boot-starter-logging = { module = "org.springframework.boot:spring-boot-starter-logging", version.ref = "spring" }
9197
spring-webflux = "org.springframework:spring-webflux:6.2.5"

shared/build.gradle.kts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,10 @@ kotlin {
112112
dependencies {
113113
api(libs.firebase.mpp.auth)
114114
api(libs.apollo.normalized.cache.sqlite)
115+
116+
implementation(libs.permissions)
117+
implementation(libs.permissions.notifications)
118+
implementation(libs.permissions.compose)
115119
}
116120
}
117121

@@ -121,6 +125,7 @@ kotlin {
121125

122126
androidMain {
123127
dependsOn(mobileMain)
128+
124129
dependencies {
125130
api(project(":proto"))
126131
api(libs.androidx.lifecycle.viewmodel.ktx)

shared/src/commonMain/kotlin/dev/johnoreilly/confetti/AppSettings.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@ class AppSettings(val settings: FlowSettings) {
2424
)
2525
}
2626

27+
val notificationsActiveFlow: Flow<Boolean>
28+
get() = experimentalFeaturesEnabledFlow
29+
2730
val experimentalFeaturesEnabledFlow = settings
2831
.getBooleanFlow(EXPERIMENTAL_FEATURES_ENABLED, false)
2932

shared/src/commonMain/kotlin/dev/johnoreilly/confetti/decompose/SessionsComponent.kt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import com.arkivanov.decompose.childContext
99
import com.arkivanov.decompose.value.Value
1010
import com.arkivanov.essenty.lifecycle.doOnStart
1111
import conferenceDateFormat
12+
import dev.johnoreilly.confetti.AppSettings
1213
import dev.johnoreilly.confetti.ConfettiRepository
1314
import dev.johnoreilly.confetti.GetBookmarksQuery
1415
import dev.johnoreilly.confetti.GetConferenceDataQuery
@@ -140,6 +141,7 @@ class SessionsSimpleComponent(
140141
private val searchQuery = MutableStateFlow("")
141142
private val isRefreshing = MutableStateFlow(false)
142143
private val selectedSessionId = MutableStateFlow<String?>(null)
144+
private val appSettings: AppSettings by inject()
143145

144146
val uiState: StateFlow<SessionsUiState> =
145147
combineUiState()
@@ -251,6 +253,7 @@ class SessionsSimpleComponent(
251253
isRefreshing,
252254
searchQuery,
253255
selectedSessionId,
256+
appSettings.notificationsActiveFlow,
254257
::uiStates
255258
)
256259
}
@@ -265,6 +268,7 @@ class SessionsSimpleComponent(
265268
isRefreshing: Boolean,
266269
searchString: String,
267270
selectedSessionId: String?,
271+
notificationsActive: Boolean,
268272
): SessionsUiState {
269273
val bookmarksResponse = refreshData.bookmarksResponse
270274
val sessionsResponse = refreshData.sessionsResponse
@@ -317,6 +321,7 @@ class SessionsSimpleComponent(
317321
isRefreshing = isRefreshing,
318322
searchString = searchString,
319323
selectedSessionId = selectedSessionId,
324+
notificationsActive = notificationsActive
320325
)
321326
}
322327
}
@@ -340,5 +345,6 @@ sealed interface SessionsUiState {
340345
val isRefreshing: Boolean,
341346
val searchString: String,
342347
val selectedSessionId: String?,
348+
val notificationsActive: Boolean
343349
) : SessionsUiState
344350
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package dev.johnoreilly.confetti.permissions
2+
3+
sealed interface NotificationPermissionState {
4+
fun maybeRequest() {}
5+
6+
data object NotApplicable: NotificationPermissionState
7+
8+
object NotDetermined: NotificationPermissionState
9+
10+
class Requestable(private val onRequest: () -> Unit): NotificationPermissionState {
11+
override fun maybeRequest() {
12+
onRequest()
13+
}
14+
}
15+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
package dev.johnoreilly.confetti.permissions
2+
3+
import androidx.compose.runtime.Composable
4+
5+
@Composable
6+
expect fun rememberNotificationPermissionState(notificationsActive: Boolean?): NotificationPermissionState

shared/src/commonMain/kotlin/dev/johnoreilly/confetti/ui/sessions/SessionsUI.kt

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
1212
import com.arkivanov.decompose.extensions.compose.subscribeAsState
1313
import dev.johnoreilly.confetti.decompose.SessionsComponent
1414
import dev.johnoreilly.confetti.decompose.SessionsUiState
15+
import dev.johnoreilly.confetti.permissions.rememberNotificationPermissionState
1516
import dev.johnoreilly.confetti.ui.HomeScaffold
1617
import dev.johnoreilly.confetti.utils.isExpanded
1718
import kotlinx.coroutines.flow.receiveAsFlow
@@ -26,6 +27,9 @@ fun SessionsUI(
2627
) {
2728
val uiState by component.uiState.subscribeAsState()
2829

30+
val notificationPermissionState =
31+
rememberNotificationPermissionState((uiState as? SessionsUiState.Success)?.notificationsActive)
32+
2933
val title = (uiState as? SessionsUiState.Success)?.conferenceName ?: ""
3034
HomeScaffold(
3135
title = title,
@@ -48,7 +52,11 @@ fun SessionsUI(
4852
SessionListView(
4953
uiState = uiState,
5054
sessionSelected = component::onSessionClicked,
51-
addBookmark = component::addBookmark,
55+
addBookmark = {
56+
// Bookmarks might be sent as notifications
57+
notificationPermissionState.maybeRequest()
58+
component.addBookmark(it)
59+
},
5260
removeBookmark = component::removeBookmark,
5361
onRefresh = component::refresh,
5462
onNavigateToSignIn = component::onSignInClicked,

shared/src/commonMain/kotlin/dev/johnoreilly/confetti/ui/settings/SettingsUI.kt

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import androidx.compose.foundation.layout.Column
88
import androidx.compose.foundation.layout.Row
99
import androidx.compose.foundation.layout.Spacer
1010
import androidx.compose.foundation.layout.fillMaxWidth
11-
import androidx.compose.foundation.layout.height
1211
import androidx.compose.foundation.layout.padding
1312
import androidx.compose.foundation.layout.width
1413
import androidx.compose.foundation.lazy.LazyColumn
@@ -55,6 +54,7 @@ import dev.johnoreilly.confetti.decompose.DarkThemeConfig
5554
import dev.johnoreilly.confetti.decompose.DeveloperSettings
5655
import dev.johnoreilly.confetti.decompose.SettingsComponent
5756
import dev.johnoreilly.confetti.decompose.UserEditableSettings
57+
import dev.johnoreilly.confetti.permissions.rememberNotificationPermissionState
5858
import org.jetbrains.compose.resources.stringResource
5959

6060
@Composable
@@ -150,6 +150,29 @@ fun SettingsUI(
150150
}
151151
}
152152
}
153+
154+
if (developerSettings != null && supportsNotifications) {
155+
item {
156+
val notificationPermissionState =
157+
rememberNotificationPermissionState(userEditableSettings?.useExperimentalFeatures)
158+
159+
Column(modifier = Modifier.padding(8.dp)) {
160+
Button(
161+
onClick = { notificationPermissionState.maybeRequest() },
162+
) {
163+
Text("Request Notification Permission")
164+
}
165+
}
166+
}
167+
168+
// item {
169+
// Column(modifier = Modifier.padding(8.dp)) {
170+
// Button(onClick = { controller.openAppSettings() }) {
171+
// Text("App Notification Settings")
172+
// }
173+
// }
174+
// }
175+
}
153176
}
154177

155178
HorizontalDivider()
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package dev.johnoreilly.confetti.permissions
2+
3+
import androidx.compose.runtime.Composable
4+
5+
@Composable
6+
actual fun rememberNotificationPermissionState(notificationsActive: Boolean?): NotificationPermissionState {
7+
return NotificationPermissionState.NotApplicable
8+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
package dev.johnoreilly.confetti.permissions
2+
3+
import androidx.compose.runtime.Composable
4+
import androidx.compose.runtime.remember
5+
import androidx.compose.runtime.rememberCoroutineScope
6+
import dev.icerock.moko.permissions.Permission
7+
import dev.icerock.moko.permissions.PermissionState
8+
import dev.icerock.moko.permissions.PermissionsController
9+
import dev.icerock.moko.permissions.compose.BindEffect
10+
import dev.icerock.moko.permissions.compose.PermissionsControllerFactory
11+
import dev.icerock.moko.permissions.compose.rememberPermissionsControllerFactory
12+
import dev.icerock.moko.permissions.notifications.REMOTE_NOTIFICATION
13+
import kotlinx.coroutines.launch
14+
15+
@Composable
16+
actual fun rememberNotificationPermissionState(notificationsActive: Boolean?): NotificationPermissionState {
17+
return when (notificationsActive) {
18+
true -> {
19+
val factory: PermissionsControllerFactory = rememberPermissionsControllerFactory()
20+
val controller: PermissionsController = remember(factory) { factory.createPermissionsController() }
21+
BindEffect(controller)
22+
val coroutineScope = rememberCoroutineScope()
23+
return remember(coroutineScope) {
24+
NotificationPermissionState.Requestable {
25+
coroutineScope.launch {
26+
val permissionState = controller.getPermissionState(Permission.REMOTE_NOTIFICATION)
27+
when (permissionState) {
28+
PermissionState.NotDetermined, PermissionState.NotGranted -> {
29+
controller.providePermission(Permission.REMOTE_NOTIFICATION)
30+
}
31+
32+
else -> {}
33+
}
34+
35+
}
36+
}
37+
}
38+
}
39+
false -> NotificationPermissionState.NotApplicable
40+
else -> NotificationPermissionState.NotDetermined
41+
}
42+
}

0 commit comments

Comments
 (0)