From c962734b824a2109d1d180f5f0b434bb9874a9d7 Mon Sep 17 00:00:00 2001 From: stylianosgakis Date: Thu, 12 Nov 2026 11:58:13 +0100 Subject: [PATCH 1/2] Move notification-permission sources to KMP source sets --- .../src/{main => androidMain}/AndroidManifest.xml | 0 .../android/notification/permission/NotificationPermission.kt | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename app/notification-permission/src/{main => androidMain}/AndroidManifest.xml (100%) rename app/notification-permission/src/{main => commonMain}/kotlin/com/hedvig/android/notification/permission/NotificationPermission.kt (100%) diff --git a/app/notification-permission/src/main/AndroidManifest.xml b/app/notification-permission/src/androidMain/AndroidManifest.xml similarity index 100% rename from app/notification-permission/src/main/AndroidManifest.xml rename to app/notification-permission/src/androidMain/AndroidManifest.xml diff --git a/app/notification-permission/src/main/kotlin/com/hedvig/android/notification/permission/NotificationPermission.kt b/app/notification-permission/src/commonMain/kotlin/com/hedvig/android/notification/permission/NotificationPermission.kt similarity index 100% rename from app/notification-permission/src/main/kotlin/com/hedvig/android/notification/permission/NotificationPermission.kt rename to app/notification-permission/src/commonMain/kotlin/com/hedvig/android/notification/permission/NotificationPermission.kt From 10f79ba870b4444cd6e45dc1dc4ae9ddf3c5c8c6 Mon Sep 17 00:00:00 2001 From: stylianosgakis Date: Thu, 12 Nov 2026 12:04:11 +0100 Subject: [PATCH 2/2] Convert notification-permission to Kotlin Multiplatform The module now has JVM and Android targets. NotificationPermissionState, NotificationPermissionDialog and the preview state are declared in commonMain. The state no longer extends Accompanist's PermissionState: it has the same member names (permission, status, launchPermissionRequest) and a project-owned PermissionStatus with isGranted and shouldShowRationale extensions, so call sites only swap the import package of those symbols. The Android actual is the existing Accompanist implementation, adapting its status to the new type. The JVM actual reads as denied, never shows a dialog and ignores requests. --- ...ClaimOutcomeNewClaimDestination.android.kt | 4 +- .../feature/home/home/ui/HomeDestination.kt | 2 +- .../profile/settings/SettingsDestination.kt | 6 +- .../feature/profile/tab/ProfileDestination.kt | 2 +- app/notification-permission/build.gradle.kts | 22 ++-- .../NotificationPermission.android.kt | 101 ++++++++++++++++++ .../permission/NotificationPermission.kt | 96 ++++------------- .../permission/NotificationPermission.jvm.kt | 25 +++++ 8 files changed, 168 insertions(+), 90 deletions(-) create mode 100644 app/notification-permission/src/androidMain/kotlin/com/hedvig/android/notification/permission/NotificationPermission.android.kt create mode 100644 app/notification-permission/src/jvmMain/kotlin/com/hedvig/android/notification/permission/NotificationPermission.jvm.kt diff --git a/app/feature/feature-claim-chat/src/androidMain/kotlin/com/hedvig/android/feature/claim/chat/ui/outcome/ClaimOutcomeNewClaimDestination.android.kt b/app/feature/feature-claim-chat/src/androidMain/kotlin/com/hedvig/android/feature/claim/chat/ui/outcome/ClaimOutcomeNewClaimDestination.android.kt index 95ff085292..4b01a38b91 100644 --- a/app/feature/feature-claim-chat/src/androidMain/kotlin/com/hedvig/android/feature/claim/chat/ui/outcome/ClaimOutcomeNewClaimDestination.android.kt +++ b/app/feature/feature-claim-chat/src/androidMain/kotlin/com/hedvig/android/feature/claim/chat/ui/outcome/ClaimOutcomeNewClaimDestination.android.kt @@ -18,8 +18,6 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.semantics.heading import androidx.compose.ui.semantics.semantics import androidx.compose.ui.unit.dp -import com.google.accompanist.permissions.isGranted -import com.google.accompanist.permissions.shouldShowRationale import com.hedvig.android.design.system.hedvig.HedvigBottomSheet import com.hedvig.android.design.system.hedvig.HedvigButton import com.hedvig.android.design.system.hedvig.HedvigText @@ -29,7 +27,9 @@ import com.hedvig.android.design.system.hedvig.api.HedvigBottomSheetState import com.hedvig.android.design.system.hedvig.rememberHedvigBottomSheetState import com.hedvig.android.design.system.hedvig.show import com.hedvig.android.notification.permission.NotificationPermissionState +import com.hedvig.android.notification.permission.isGranted import com.hedvig.android.notification.permission.rememberNotificationPermissionState +import com.hedvig.android.notification.permission.shouldShowRationale import hedvig.resources.CLAIMS_ACTIVATE_NOTIFICATIONS_BODY import hedvig.resources.CLAIMS_ACTIVATE_NOTIFICATIONS_CTA import hedvig.resources.CLAIM_CHAT_GET_NOTIFIED diff --git a/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/ui/HomeDestination.kt b/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/ui/HomeDestination.kt index 9ee06c7164..b3f4353c4c 100644 --- a/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/ui/HomeDestination.kt +++ b/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/ui/HomeDestination.kt @@ -93,7 +93,6 @@ import arrow.core.nonEmptyListOf import arrow.core.toNonEmptyListOrNull import coil3.ImageLoader import coil3.compose.AsyncImage -import com.google.accompanist.permissions.isGranted import com.hedvig.android.compose.pager.indicator.CardCarousel import com.hedvig.android.compose.ui.LocalClock import com.hedvig.android.compose.ui.dropUnlessResumed @@ -209,6 +208,7 @@ import com.hedvig.android.memberreminders.ui.homeInformationalReminders import com.hedvig.android.memberreminders.ui.missingPayinMethodReminder import com.hedvig.android.notification.permission.NotificationPermissionDialog import com.hedvig.android.notification.permission.NotificationPermissionState +import com.hedvig.android.notification.permission.isGranted import com.hedvig.android.notification.permission.rememberNotificationPermissionState import com.hedvig.android.notification.permission.rememberPreviewNotificationPermissionState import com.hedvig.android.pullrefresh.PullRefreshDefaults diff --git a/app/feature/feature-profile/src/main/kotlin/com/hedvig/android/feature/profile/settings/SettingsDestination.kt b/app/feature/feature-profile/src/main/kotlin/com/hedvig/android/feature/profile/settings/SettingsDestination.kt index ead73b7f2e..f4fd3b8e85 100644 --- a/app/feature/feature-profile/src/main/kotlin/com/hedvig/android/feature/profile/settings/SettingsDestination.kt +++ b/app/feature/feature-profile/src/main/kotlin/com/hedvig/android/feature/profile/settings/SettingsDestination.kt @@ -26,9 +26,6 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.google.accompanist.permissions.PermissionStatus -import com.google.accompanist.permissions.PermissionStatus.Granted -import com.google.accompanist.permissions.isGranted import com.hedvig.android.data.settings.datastore.AnalyticsConsent import com.hedvig.android.design.system.hedvig.DialogDefaults.ButtonSize.BIG import com.hedvig.android.design.system.hedvig.HedvigAlertDialog @@ -52,6 +49,9 @@ import com.hedvig.android.language.label import com.hedvig.android.memberreminders.ui.ReminderCardEnableNotifications import com.hedvig.android.notification.permission.NotificationPermissionDialog import com.hedvig.android.notification.permission.NotificationPermissionState +import com.hedvig.android.notification.permission.PermissionStatus +import com.hedvig.android.notification.permission.PermissionStatus.Granted +import com.hedvig.android.notification.permission.isGranted import com.hedvig.android.notification.permission.rememberNotificationPermissionState import com.hedvig.android.theme.Theme import hedvig.resources.GENERAL_DISABLED diff --git a/app/feature/feature-profile/src/main/kotlin/com/hedvig/android/feature/profile/tab/ProfileDestination.kt b/app/feature/feature-profile/src/main/kotlin/com/hedvig/android/feature/profile/tab/ProfileDestination.kt index 3992578a0d..3172d78f1b 100644 --- a/app/feature/feature-profile/src/main/kotlin/com/hedvig/android/feature/profile/tab/ProfileDestination.kt +++ b/app/feature/feature-profile/src/main/kotlin/com/hedvig/android/feature/profile/tab/ProfileDestination.kt @@ -49,7 +49,6 @@ import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameter import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.dropUnlessResumed -import com.google.accompanist.permissions.isGranted import com.hedvig.android.compose.ui.plus import com.hedvig.android.compose.ui.preview.PreviewContentWithProvidedParametersAnimatedOnClick import com.hedvig.android.data.coinsured.CoInsuredFlowType @@ -77,6 +76,7 @@ import com.hedvig.android.memberreminders.ui.MissingPayinMethodCard import com.hedvig.android.memberreminders.ui.cardReminders import com.hedvig.android.memberreminders.ui.missingPayinMethodReminder import com.hedvig.android.notification.permission.NotificationPermissionDialog +import com.hedvig.android.notification.permission.isGranted import com.hedvig.android.notification.permission.rememberNotificationPermissionState import com.hedvig.android.placeholder.PlaceholderHighlight import com.hedvig.android.pullrefresh.PullRefreshDefaults diff --git a/app/notification-permission/build.gradle.kts b/app/notification-permission/build.gradle.kts index df54831baf..50fb472326 100644 --- a/app/notification-permission/build.gradle.kts +++ b/app/notification-permission/build.gradle.kts @@ -1,5 +1,6 @@ plugins { - id("hedvig.android.library") + id("hedvig.multiplatform.library") + id("hedvig.multiplatform.library.android") id("hedvig.gradle.plugin") } @@ -7,10 +8,17 @@ hedvig { compose() } -dependencies { - api(libs.accompanist.permissions) - - implementation(libs.androidx.activity.compose) - implementation(projects.coreResources) - implementation(projects.designSystemHedvig) +kotlin { + sourceSets { + commonMain.dependencies { + implementation(libs.jetbrains.compose.runtime) + implementation(projects.coreResources) + implementation(projects.designSystemHedvig) + } + androidMain.dependencies { + implementation(libs.accompanist.permissions) + implementation(libs.androidx.activity.compose) + implementation(libs.jetbrains.compose.ui) + } + } } diff --git a/app/notification-permission/src/androidMain/kotlin/com/hedvig/android/notification/permission/NotificationPermission.android.kt b/app/notification-permission/src/androidMain/kotlin/com/hedvig/android/notification/permission/NotificationPermission.android.kt new file mode 100644 index 0000000000..42c14b93b8 --- /dev/null +++ b/app/notification-permission/src/androidMain/kotlin/com/hedvig/android/notification/permission/NotificationPermission.android.kt @@ -0,0 +1,101 @@ +@file:Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE") + +package com.hedvig.android.notification.permission + +import android.Manifest +import android.annotation.SuppressLint +import android.os.Build +import androidx.activity.compose.LocalActivity +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalInspectionMode +import com.google.accompanist.permissions.ExperimentalPermissionsApi +import com.google.accompanist.permissions.MutablePermissionState +import com.google.accompanist.permissions.PermissionLifecycleCheckerEffect +import com.google.accompanist.permissions.PermissionState as AccompanistPermissionState +import com.google.accompanist.permissions.PermissionStatus as AccompanistPermissionStatus +import com.google.accompanist.permissions.isGranted as accompanistIsGranted +import com.google.accompanist.permissions.rememberPermissionState + +private class NotificationPermissionStateImpl( + private val permissionState: AccompanistPermissionState, +) : NotificationPermissionState { + private var _showDialog by mutableStateOf(false) + + override val permission: String + get() = permissionState.permission + + override val status: PermissionStatus + get() = when (val status = permissionState.status) { + AccompanistPermissionStatus.Granted -> PermissionStatus.Granted + is AccompanistPermissionStatus.Denied -> PermissionStatus.Denied(status.shouldShowRationale) + } + + override val showDialog: Boolean + get() = _showDialog + + override fun launchPermissionRequest() { + permissionState.launchPermissionRequest() + } + + override fun dismissDialog() { + _showDialog = false + } + + fun showDialog() { + _showDialog = true + } +} + +/** + * Convenience function over [com.google.accompanist.permissions.rememberPermissionState] specifically for the + * notification. Use in conjuction with [NotificationPermissionDialog] to get an easy to use notification permission + * handling. + */ +@SuppressLint("InlinedApi") +@Composable +actual fun rememberNotificationPermissionState(onNotificationGranted: () -> Unit): NotificationPermissionState { + val context = LocalContext.current + val activity = LocalActivity.current + val permissionState = remember { + MutablePermissionState(Manifest.permission.POST_NOTIFICATIONS, context, activity) + } + val notificationPermissionState = remember { + NotificationPermissionStateImpl(permissionState) + } + PermissionLifecycleCheckerEffect(permissionState) + val launcher = rememberLauncherForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted -> + permissionState.refreshPermissionStatus() + if (isGranted) { + onNotificationGranted() + } else { + notificationPermissionState.showDialog() + } + } + DisposableEffect(permissionState, launcher) { + permissionState.launcher = launcher + onDispose { + permissionState.launcher = null + } + } + + return notificationPermissionState +} + +@OptIn(ExperimentalPermissionsApi::class) +@Composable +actual fun rememberNotificationPermissionStatus(): Boolean { + val isPreview = LocalInspectionMode.current + return if (!isPreview && Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + rememberPermissionState(Manifest.permission.POST_NOTIFICATIONS).status.accompanistIsGranted + } else { + true + } +} diff --git a/app/notification-permission/src/commonMain/kotlin/com/hedvig/android/notification/permission/NotificationPermission.kt b/app/notification-permission/src/commonMain/kotlin/com/hedvig/android/notification/permission/NotificationPermission.kt index 0d96b7681f..49d55371b8 100644 --- a/app/notification-permission/src/commonMain/kotlin/com/hedvig/android/notification/permission/NotificationPermission.kt +++ b/app/notification-permission/src/commonMain/kotlin/com/hedvig/android/notification/permission/NotificationPermission.kt @@ -1,91 +1,43 @@ -@file:Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE") - package com.hedvig.android.notification.permission -import android.Manifest -import android.annotation.SuppressLint -import android.os.Build -import androidx.activity.compose.LocalActivity -import androidx.activity.compose.rememberLauncherForActivityResult -import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.runtime.Composable -import androidx.compose.runtime.DisposableEffect -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.LocalInspectionMode -import com.google.accompanist.permissions.ExperimentalPermissionsApi -import com.google.accompanist.permissions.MutablePermissionState -import com.google.accompanist.permissions.PermissionLifecycleCheckerEffect -import com.google.accompanist.permissions.PermissionState -import com.google.accompanist.permissions.PermissionStatus -import com.google.accompanist.permissions.isGranted -import com.google.accompanist.permissions.rememberPermissionState -import com.google.accompanist.permissions.shouldShowRationale import com.hedvig.android.design.system.hedvig.PermissionDialog import hedvig.resources.CLAIMS_ACTIVATE_NOTIFICATIONS_BODY import hedvig.resources.Res import org.jetbrains.compose.resources.stringResource -interface NotificationPermissionState : PermissionState { +interface NotificationPermissionState { + val permission: String + val status: PermissionStatus val showDialog: Boolean + fun launchPermissionRequest() + fun dismissDialog() } -private class NotificationPermissionStateImpl( - private val permissionState: PermissionState, -) : NotificationPermissionState, PermissionState by permissionState { - private var _showDialog by mutableStateOf(false) +sealed interface PermissionStatus { + data object Granted : PermissionStatus - override val showDialog: Boolean - get() = _showDialog + data class Denied(val shouldShowRationale: Boolean) : PermissionStatus +} - override fun dismissDialog() { - _showDialog = false - } +val PermissionStatus.isGranted: Boolean + get() = this == PermissionStatus.Granted - fun showDialog() { - _showDialog = true +val PermissionStatus.shouldShowRationale: Boolean + get() = when (this) { + PermissionStatus.Granted -> false + is PermissionStatus.Denied -> shouldShowRationale } -} /** - * Convenience function over [com.google.accompanist.permissions.rememberPermissionState] specifically for the - * notification. Use in conjuction with [NotificationPermissionDialog] to get an easy to use notification permission - * handling. + * Notification permission state for the current screen. [onNotificationGranted] runs when a permission request is + * granted; a denied request makes [NotificationPermissionState.showDialog] true. Use in conjunction with + * [NotificationPermissionDialog]. */ -@SuppressLint("InlinedApi") @Composable -fun rememberNotificationPermissionState(onNotificationGranted: () -> Unit = {}): NotificationPermissionState { - val context = LocalContext.current - val activity = LocalActivity.current - val permissionState = remember { - MutablePermissionState(Manifest.permission.POST_NOTIFICATIONS, context, activity) - } - val notificationPermissionState = remember { - NotificationPermissionStateImpl(permissionState) - } - PermissionLifecycleCheckerEffect(permissionState) - val launcher = rememberLauncherForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted -> - permissionState.refreshPermissionStatus() - if (isGranted) { - onNotificationGranted() - } else { - notificationPermissionState.showDialog() - } - } - DisposableEffect(permissionState, launcher) { - permissionState.launcher = launcher - onDispose { - permissionState.launcher = null - } - } - - return notificationPermissionState -} +expect fun rememberNotificationPermissionState(onNotificationGranted: () -> Unit = {}): NotificationPermissionState @Composable fun NotificationPermissionDialog( @@ -103,16 +55,8 @@ fun NotificationPermissionDialog( } } -@OptIn(ExperimentalPermissionsApi::class) @Composable -fun rememberNotificationPermissionStatus(): Boolean { - val isPreview = LocalInspectionMode.current - return if (!isPreview && Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - rememberPermissionState(Manifest.permission.POST_NOTIFICATIONS).status.isGranted - } else { - true - } -} +expect fun rememberNotificationPermissionStatus(): Boolean fun rememberPreviewNotificationPermissionState( permissionStatus: PermissionStatus = PermissionStatus.Granted, diff --git a/app/notification-permission/src/jvmMain/kotlin/com/hedvig/android/notification/permission/NotificationPermission.jvm.kt b/app/notification-permission/src/jvmMain/kotlin/com/hedvig/android/notification/permission/NotificationPermission.jvm.kt new file mode 100644 index 0000000000..9006673499 --- /dev/null +++ b/app/notification-permission/src/jvmMain/kotlin/com/hedvig/android/notification/permission/NotificationPermission.jvm.kt @@ -0,0 +1,25 @@ +package com.hedvig.android.notification.permission + +import androidx.compose.runtime.Composable + +/** + * The JVM target has no notification permission to request, so the state always reads as denied and never shows a + * dialog. + */ +private object UnavailableNotificationPermissionState : NotificationPermissionState { + override val permission: String = "android.permission.POST_NOTIFICATIONS" + override val status: PermissionStatus = PermissionStatus.Denied(shouldShowRationale = false) + override val showDialog: Boolean = false + + override fun launchPermissionRequest() {} + + override fun dismissDialog() {} +} + +@Composable +actual fun rememberNotificationPermissionState(onNotificationGranted: () -> Unit): NotificationPermissionState { + return UnavailableNotificationPermissionState +} + +@Composable +actual fun rememberNotificationPermissionStatus(): Boolean = false