diff --git a/NOTIFICATION_PERMISSION_ISSUE_ANALYSIS.md b/NOTIFICATION_PERMISSION_ISSUE_ANALYSIS.md
new file mode 100644
index 00000000..15f0bfce
--- /dev/null
+++ b/NOTIFICATION_PERMISSION_ISSUE_ANALYSIS.md
@@ -0,0 +1,300 @@
+# Notification Permission Issue Analysis & Resolution
+
+## Issue Summary
+
+**Problem**: Android notification permission requests were failing on SDK 36 with the following symptoms:
+- No permission dialog was shown to users
+- Permission requests immediately returned `DeniedAlwaysException`
+- Logs showed permission was being auto-denied without user interaction
+
+**Root Cause**: Missing `targetSdk` configuration in the app's build setup
+
+**Impact**: Complete failure of notification permission functionality on Android 13+ devices
+
+---
+
+## Technical Deep Dive
+
+### What Actually Happened
+
+#### 1. **Build Configuration Issue**
+```gradle
+// BEFORE (Broken)
+android {
+ compileSdk = "35" // ✓ Present
+ // targetSdk missing ❌
+ minSdk = "26"
+}
+
+// AFTER (Fixed)
+android {
+ compileSdk = "35"
+ targetSdk = "35" // ✓ Added
+ minSdk = "26"
+}
+```
+
+#### 2. **Android's Permission Protection Mechanism**
+When `targetSdk` is not specified:
+- Android defaults to a very old API level (often API 22 or lower)
+- Android 13+ has a security feature that **auto-denies** `POST_NOTIFICATIONS` for apps targeting API < 33
+- No dialog is shown - the permission is silently denied
+- This appears as immediate `false` return from permission requests
+
+#### 3. **The Investigation Process**
+
+**Step 1: Initial Debugging**
+- Added comprehensive logging to trace permission flow
+- Suspected race conditions between `BindEffect` and permission requests
+- Fixed race conditions but issue persisted
+
+**Step 2: Log Analysis**
+```
+handlePermissionRequest called for REMOTE_NOTIFICATION
+Platform permissions: [android.permission.POST_NOTIFICATIONS]
+About to launch permission request...
+handlePermissionResult called with: {android.permission.POST_NOTIFICATIONS=false}
+```
+
+**Key Insight**: Permission was launching but immediately returning `false` without user interaction
+
+**Step 3: Root Cause Discovery**
+- Checked app's build configuration
+- Found `compileSdk = "35"` but **no `targetSdk` defined**
+- Realized Android was treating the app as targeting old API levels
+
+---
+
+## Android 13+ Notification Permission Behavior
+
+### Critical Requirements
+
+1. **App Must Target API 33+**
+ ```xml
+
+
+ ```
+
+2. **Permission Must Be Declared**
+ ```xml
+
+ ```
+
+3. **Manual Request Required**
+ - Notification permission is **denied by default** on Android 13+
+ - Apps must explicitly request it via runtime permission API
+ - No automatic granting or prompting
+
+### Special Characteristics of POST_NOTIFICATIONS
+
+Unlike other Android permissions, `POST_NOTIFICATIONS` has unique behaviors:
+
+1. **`shouldShowRequestPermissionRationale()` is unreliable**
+ - Often returns `false` even on first denial
+ - Cannot be used to distinguish "never asked" vs "permanently denied"
+
+2. **Auto-denial for old target SDK**
+ - Apps targeting < API 33 get automatic denial
+ - No dialog shown, no user interaction
+
+3. **System-level notification settings**
+ - Users can disable notifications at system level
+ - Must be considered in permission state logic
+
+---
+
+## The Complete Solution
+
+### 1. **Build Configuration Fix**
+
+**File: `gradle/libs.versions.toml`**
+```toml
+[versions]
+compileSdk = "35"
+targetSdk = "35" # ← Added this line
+minSdk = "26"
+```
+
+**File: `build.gradle.kts`**
+```kotlin
+android {
+ defaultConfig {
+ minSdk = libs.versions.minSdk.get().toInt()
+ targetSdk = libs.versions.targetSdk.get().toInt() // ← Added this line
+ // ...
+ }
+}
+```
+
+### 2. **Permission Manager Enhancements**
+
+#### Race Condition Prevention
+```kotlin
+// Prevent automatic permission requests in ViewModel init
+// Use manual button triggers instead
+private fun init() {
+ // Don't automatically request permission
+ setState { copy(isInitialized = true) }
+}
+```
+
+#### Android 13+ Specific Logic
+```kotlin
+// Special handling for notification permissions
+if (permission == Permission.REMOTE_NOTIFICATION &&
+ Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+
+ // Use request count instead of shouldShowRequestPermissionRationale
+ val exception = if (requestCount <= 1) {
+ DeniedException(permission) // Allow retry
+ } else {
+ DeniedAlwaysException(permission) // Redirect to settings
+ }
+}
+```
+
+#### State Tracking
+```kotlin
+// Manual tracking via SharedPreferences
+private fun trackPermissionRequest(permission: Permission) {
+ val prefs = context.getSharedPreferences("permission_prefs", Context.MODE_PRIVATE)
+ val currentCount = prefs.getInt("notification_request_count", 0)
+ prefs.edit()
+ .putBoolean("notification_permission_requested", true)
+ .putInt("notification_request_count", currentCount + 1)
+ .apply()
+}
+```
+
+### 3. **UI Flow Improvements**
+
+```kotlin
+// Clear separation of concerns
+sealed class Event : ViewEvent {
+ object Init : Event()
+ object EnablePushNotifications : Event() // Manual trigger
+ object ClearPermissionState : Event() // Debug helper
+}
+```
+
+---
+
+## Key Lessons Learned
+
+### 1. **Always Set targetSdk**
+- `targetSdk` is not optional for modern Android development
+- Missing `targetSdk` can cause subtle, hard-to-debug issues
+- Always match or closely track the latest stable Android API
+
+### 2. **Android 13+ Notification Permissions Are Special**
+- Different behavior from other runtime permissions
+- Requires API 33+ target to function at all
+- `shouldShowRequestPermissionRationale()` is unreliable
+- Manual state tracking is necessary
+
+### 3. **Race Conditions in Compose**
+- `BindEffect` and `LaunchedEffect` can race during composition
+- Don't trigger permission requests automatically in ViewModel init
+- Use manual user triggers for better UX and timing control
+
+### 4. **Comprehensive Logging is Essential**
+- Added detailed logging throughout permission flow
+- Logs revealed the auto-denial behavior
+- Logging should be removable for production builds
+
+### 5. **Android Security Model Evolution**
+- Each Android version tightens permission requirements
+- Old apps may suddenly break on new Android versions
+- Regular testing on latest Android versions is crucial
+
+---
+
+## Best Practices Going Forward
+
+### 1. **Build Configuration**
+```kotlin
+// Always specify all three SDK versions
+android {
+ compileSdk = 35 // Latest for compilation
+ targetSdk = 35 // Latest stable for features
+ minSdk = 26 // Minimum supported version
+}
+```
+
+### 2. **Permission Request Timing**
+```kotlin
+// ❌ Don't do this
+class ViewModel {
+ private fun init() {
+ requestPermission() // Race condition risk
+ }
+}
+
+// ✅ Do this instead
+class ViewModel {
+ fun onUserRequestPermission() {
+ requestPermission() // User-triggered, safe timing
+ }
+}
+```
+
+### 3. **Permission State Management**
+```kotlin
+// Always provide fallback state tracking
+private fun getPermissionState(): PermissionState {
+ return when {
+ // Check actual permission status first
+ isGranted() -> PermissionState.Granted
+
+ // For notifications, use manual tracking
+ isNotificationPermission() -> getNotificationState()
+
+ // For others, use standard Android APIs
+ else -> getStandardPermissionState()
+ }
+}
+```
+
+### 4. **Error Handling**
+```kotlin
+try {
+ permissionManager.requestPermission(Permission.REMOTE_NOTIFICATION)
+} catch (e: DeniedException) {
+ // Show retry option
+ showPermissionRationale()
+} catch (e: DeniedAlwaysException) {
+ // Redirect to settings
+ showSettingsRedirect()
+} catch (e: RequestCanceledException) {
+ // Handle cancellation gracefully
+ logPermissionCanceled()
+}
+```
+
+---
+
+## Testing Strategy
+
+### 1. **Build Configuration Testing**
+- Test app installation on different Android versions
+- Verify permission dialogs appear correctly
+- Test with different `targetSdk` values
+
+### 2. **Permission Flow Testing**
+- First-time permission request
+- Permission denial and retry
+- Permanent denial and settings redirect
+- App reinstallation scenarios
+
+### 3. **Edge Case Testing**
+- Activity destruction during permission request
+- Multiple rapid permission requests
+- Background/foreground state changes
+
+---
+
+## Conclusion
+
+This issue highlights the complexity of Android permission management and the importance of staying current with Android development best practices. The root cause was a missing `targetSdk` configuration, but the investigation revealed several other improvements that make the permission system more robust and user-friendly.
+
+The comprehensive solution addresses not just the immediate issue but also improves the overall permission management architecture to handle future Android changes more gracefully.
\ No newline at end of file
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 0e5bf7a1..3ced2a4e 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -1,5 +1,6 @@
[versions]
compileSdk = "35"
+targetSdk = "35"
minSdk = "26"
jvm = "17"
gradle = "8.7.1"
diff --git a/samples/catalog-app-shared/build.gradle.kts b/samples/catalog-app-shared/build.gradle.kts
index 5c74fc99..9cb1c45e 100644
--- a/samples/catalog-app-shared/build.gradle.kts
+++ b/samples/catalog-app-shared/build.gradle.kts
@@ -77,6 +77,7 @@ android {
defaultConfig {
minSdk = libs.versions.minSdk.get().toInt()
+ targetSdk = libs.versions.targetSdk.get().toInt()
versionCode = CatalogConfigs.VERSION_CODE
versionName = CatalogConfigs.VERSION_NAME
diff --git a/samples/catalog-app-shared/src/commonMain/kotlin/com/metacto/catalogapp/presentation/notifications/NotificationsSamplesContract.kt b/samples/catalog-app-shared/src/commonMain/kotlin/com/metacto/catalogapp/presentation/notifications/NotificationsSamplesContract.kt
index 31aaad4d..9ce9dfa0 100644
--- a/samples/catalog-app-shared/src/commonMain/kotlin/com/metacto/catalogapp/presentation/notifications/NotificationsSamplesContract.kt
+++ b/samples/catalog-app-shared/src/commonMain/kotlin/com/metacto/catalogapp/presentation/notifications/NotificationsSamplesContract.kt
@@ -12,6 +12,7 @@ class NotificationsSamplesContract {
sealed class Event : ViewEvent {
data object Init : Event()
+ data object EnablePushNotifications : Event()
}
sealed class Effect : ViewSideEffect
diff --git a/samples/catalog-app-shared/src/commonMain/kotlin/com/metacto/catalogapp/presentation/notifications/NotificationsSamplesViewModel.kt b/samples/catalog-app-shared/src/commonMain/kotlin/com/metacto/catalogapp/presentation/notifications/NotificationsSamplesViewModel.kt
index 1cde5abc..37756f05 100644
--- a/samples/catalog-app-shared/src/commonMain/kotlin/com/metacto/catalogapp/presentation/notifications/NotificationsSamplesViewModel.kt
+++ b/samples/catalog-app-shared/src/commonMain/kotlin/com/metacto/catalogapp/presentation/notifications/NotificationsSamplesViewModel.kt
@@ -4,6 +4,12 @@ import com.metacto.catalogapp.presentation.base.BaseViewModel
import com.metacto.catalogapp.presentation.notifications.NotificationsSamplesContract.Effect
import com.metacto.catalogapp.presentation.notifications.NotificationsSamplesContract.Event
import com.metacto.catalogapp.presentation.notifications.NotificationsSamplesContract.State
+import com.metacto.core.ui.globalState.models.LoadingType
+import com.metacto.core.ui.permissions.IPermissionManager
+import com.metacto.core.ui.permissions.enums.Permission
+import com.metacto.core.ui.permissions.exceptions.DeniedAlwaysException
+import com.metacto.kmm.logger.Logger
+import org.koin.core.component.inject
class NotificationsSamplesViewModel : BaseViewModel() {
@@ -11,15 +17,30 @@ class NotificationsSamplesViewModel : BaseViewModel() {
override fun handleEvents(event: Event): Any = when (event) {
Event.Init -> init()
+ Event.EnablePushNotifications -> handleEnablePushNotificationClick()
}
private fun init() {
// Validate if already initialized
if (currentState.isInitialized) return
- // Init
-
+ // Init - Don't automatically request permission, let user trigger it manually
+
// Update the flag
setState { copy(isInitialized = true) }
}
+
+ private fun handleEnablePushNotificationClick() = executeCatching(
+ loadingType = LoadingType.NoLoading,
+ block = {
+ permissionManager.requestPermission(Permission.REMOTE_NOTIFICATION)
+ Logger("handleEnablePushNotificationClick").log("Notifications enabled")
+ },
+ shouldShowErrorMessage = { throwable ->
+ if (throwable is DeniedAlwaysException) {
+ Logger("handleEnablePushNotificationClick").log("DeniedAlwaysException")
+ }
+ false
+ }
+ )
}
diff --git a/samples/catalog-app-shared/src/commonMain/kotlin/com/metacto/catalogapp/presentation/notifications/components/NotificationsSamplesContent.kt b/samples/catalog-app-shared/src/commonMain/kotlin/com/metacto/catalogapp/presentation/notifications/components/NotificationsSamplesContent.kt
index a288af20..3ef774db 100644
--- a/samples/catalog-app-shared/src/commonMain/kotlin/com/metacto/catalogapp/presentation/notifications/components/NotificationsSamplesContent.kt
+++ b/samples/catalog-app-shared/src/commonMain/kotlin/com/metacto/catalogapp/presentation/notifications/components/NotificationsSamplesContent.kt
@@ -1,10 +1,15 @@
package com.metacto.catalogapp.presentation.notifications.components
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
import com.metacto.core.ui.navigation.NavManager
import com.metacto.catalogapp.presentation.components.containers.AppScreenColumn
import com.metacto.catalogapp.presentation.notifications.NotificationsSamplesContract.Event
import com.metacto.catalogapp.presentation.notifications.NotificationsSamplesContract.State
+import com.metacto.core.ui.components.buttons.PrimaryFilledButton
import org.koin.compose.koinInject
@Composable
@@ -25,6 +30,15 @@ internal fun NotificationsSamplesContent(
navManager.goBack()
},
) {
- // TODO: Render content
+ // Button to request notification permission
+ PrimaryFilledButton(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(16.dp),
+ text = "Enable Push Notifications",
+ onClick = {
+ onEvent(Event.EnablePushNotifications)
+ }
+ )
}
}
diff --git a/ui/core-ui/src/androidMain/kotlin/com/metacto/core/ui/permissions/PermissionManager.kt b/ui/core-ui/src/androidMain/kotlin/com/metacto/core/ui/permissions/PermissionManager.kt
index a3293fe6..51317ccc 100644
--- a/ui/core-ui/src/androidMain/kotlin/com/metacto/core/ui/permissions/PermissionManager.kt
+++ b/ui/core-ui/src/androidMain/kotlin/com/metacto/core/ui/permissions/PermissionManager.kt
@@ -1,5 +1,6 @@
package com.metacto.core.ui.permissions
+import android.Manifest
import android.app.Activity
import android.content.Context
import android.content.pm.PackageManager
@@ -26,7 +27,62 @@ import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withTimeoutOrNull
import java.util.UUID
import kotlin.coroutines.suspendCoroutine
+import kotlin.math.max
+/**
+ * Android Implementation of Permission Manager
+ *
+ * This class handles the complex permission management for Android, with special handling
+ * for different Android versions and permission types. It addresses several Android-specific
+ * challenges:
+ *
+ * ## Key Challenges Solved:
+ *
+ * ### 1. Android 13+ Notification Permissions
+ * - POST_NOTIFICATIONS permission introduced in API 33
+ * - Apps must target API 33+ or permission is auto-denied without dialog
+ * - Solution: Proper targetSdk configuration allows standard Android APIs to work
+ *
+ * ### 2. Race Conditions
+ * - Permission requests must happen after Activity is bound
+ * - BindEffect and permission requests can race during screen composition
+ * - Solution: Proper activity lifecycle management and mutex protection
+ *
+ * ### 3. Permission State Tracking
+ * - Android provides shouldShowRequestPermissionRationale() for state management
+ * - With proper targetSdk configuration, standard Android APIs work reliably
+ * - Solution: Use Android's built-in permission state logic
+ *
+ * ### 4. Activity Lifecycle Management
+ * - Permission launchers tied to Activity lifecycle
+ * - Activity can be destroyed during permission flow
+ * - Solution: StateFlow-based activity holder with lifecycle observers
+ *
+ * ## Implementation Details:
+ *
+ * ### Permission Request Flow:
+ * 1. Check if permission already granted → return early
+ * 2. Get ActivityResultLauncher from bound activity
+ * 3. Map Permission enum to Android permission strings
+ * 4. Launch permission request via ActivityResultContracts
+ * 5. Handle result in callback with proper exception mapping
+ *
+ * ### Notification Permission Special Handling:
+ * - Uses standard shouldShowRequestPermissionRationale for state management
+ * - Considers system notification settings for older Android versions
+ * - Relies on Android's built-in permission logic with proper targetSdk
+ *
+ * ### Error Handling:
+ * - DeniedException: User denied, can ask again
+ * - DeniedAlwaysException: Permanently denied, redirect to settings
+ * - RequestCanceledException: Dialog dismissed without selection
+ * - IllegalStateException: Activity not bound or other setup issues
+ *
+ * @param context Application context for permission checks
+ *
+ * @see IPermissionManager for interface documentation
+ * @see BindEffect for proper activity binding in Compose
+ */
internal class PermissionManager(private val context: Context) : IPermissionManager {
private val activityHolder = MutableStateFlow(null)
private val launcherHolder = MutableStateFlow>?>(null)
@@ -34,6 +90,18 @@ internal class PermissionManager(private val context: Context) : IPermissionMana
private val mutex = Mutex()
private val key = UUID.randomUUID().toString()
+ /**
+ * Binds the PermissionManager to a ComponentActivity.
+ *
+ * This method MUST be called before requesting any permissions. It:
+ * 1. Stores the activity reference for permission requests
+ * 2. Sets up the ActivityResultLauncher for handling permission dialogs
+ * 3. Registers lifecycle observers to clean up when activity is destroyed
+ *
+ * ⚠️ Critical: This must be called from the main thread and before any permission requests.
+ *
+ * @param activity The ComponentActivity to bind to
+ */
override fun bind(activity: ComponentActivity) {
activityHolder.value = activity
setupPermissionLauncher(activity)
@@ -44,6 +112,11 @@ internal class PermissionManager(private val context: Context) : IPermissionMana
permission: Permission,
openAppSettingsIfRequired: Boolean
) {
+ // Check if permission is already granted
+ if (isPermissionGranted(permission)) {
+ return // Permission already granted, no need to request
+ }
+
if (openAppSettingsIfRequired) {
handlePermissionRequestWithSettings(permission)
} else {
@@ -103,9 +176,15 @@ internal class PermissionManager(private val context: Context) : IPermissionMana
private fun handlePermissionDenial(callback: PermissionCallback, permission: String) {
val activity = activityHolder.value ?: return
+
+ // Use standard Android permission logic for all permissions
+ // Now that targetSdk is correctly set, shouldShowRequestPermissionRationale should work properly
val exception = if (shouldShowRequestPermissionRationale(activity, permission)) {
+ // User denied permission but we can ask again
com.metacto.core.ui.permissions.exceptions.DeniedException(callback.permission)
} else {
+ // User permanently denied permission or this is first request
+ // Let Android handle the logic through its standard APIs
DeniedAlwaysException(callback.permission)
}
callback.callback(Result.failure(exception))
@@ -141,22 +220,43 @@ internal class PermissionManager(private val context: Context) : IPermissionMana
} ?: throw IllegalStateException(getBindErrorMessage())
}
- private suspend fun awaitActivity(): Activity {
- return activityHolder.value ?: withTimeoutOrNull(AWAIT_ACTIVITY_TIMEOUT_MS) {
- activityHolder.filterNotNull().first()
- } ?: throw IllegalStateException(getBindErrorMessage())
- }
private fun isNotificationPermission(permission: Permission): Boolean =
permission == Permission.REMOTE_NOTIFICATION &&
- Build.VERSION.SDK_INT in VERSIONS_WITHOUT_NOTIFICATION_PERMISSION
+ Build.VERSION.SDK_INT > VERSIONS_WITHOUT_NOTIFICATION_PERMISSION.max()
private fun getNotificationPermissionState(): PermissionState {
- val isEnabled = NotificationManagerCompat.from(context).areNotificationsEnabled()
- return if (isEnabled) PermissionState.Granted else PermissionState.DeniedAlways
+ return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ val permissionStatus = ContextCompat.checkSelfPermission(
+ context,
+ Manifest.permission.POST_NOTIFICATIONS
+ )
+
+ when (permissionStatus) {
+ PackageManager.PERMISSION_GRANTED -> PermissionState.Granted
+ PackageManager.PERMISSION_DENIED -> {
+ // For Android 13+, we can rely on standard shouldShowRequestPermissionRationale
+ // now that targetSdk is properly configured
+ val activity = activityHolder.value
+ if (activity != null && shouldShowRequestPermissionRationale(activity, Manifest.permission.POST_NOTIFICATIONS)) {
+ PermissionState.Denied // User denied but can ask again
+ } else {
+ PermissionState.NotDetermined // Never asked or permanently denied
+ }
+ }
+ else -> PermissionState.NotDetermined
+ }
+ } else {
+ // For older Android versions, check system notification settings
+ if (NotificationManagerCompat.from(context).areNotificationsEnabled()) {
+ PermissionState.Granted
+ } else {
+ PermissionState.Denied
+ }
+ }
}
- private suspend fun getRuntimePermissionState(permission: Permission): PermissionState {
+ private fun getRuntimePermissionState(permission: Permission): PermissionState {
val permissions = permission.toPlatformPermission()
val status = permissions.map {
ContextCompat.checkSelfPermission(context, it)
@@ -164,13 +264,29 @@ internal class PermissionManager(private val context: Context) : IPermissionMana
return when {
status.all { it == PackageManager.PERMISSION_GRANTED } -> PermissionState.Granted
- permissions.all { !shouldShowRequestPermissionRationale(it) } -> PermissionState.Denied
- else -> PermissionState.NotDetermined
+ else -> {
+ val activity = activityHolder.value
+ if (activity != null) {
+ val shouldShowRationale = permissions.any {
+ ActivityCompat.shouldShowRequestPermissionRationale(activity, it)
+ }
+
+ if (shouldShowRationale) {
+ // User has denied the permission but can ask again
+ PermissionState.Denied
+ } else {
+ // Permission never asked or permanently denied
+ // Now that targetSdk is properly set, we can rely on Android's standard logic
+ PermissionState.NotDetermined
+ }
+ } else {
+ // If no activity available, assume permission can be requested
+ PermissionState.NotDetermined
+ }
+ }
}
}
- private suspend fun shouldShowRequestPermissionRationale(permission: String): Boolean =
- shouldShowRequestPermissionRationale(awaitActivity(), permission)
private fun shouldShowRequestPermissionRationale(
activity: Activity,
diff --git a/ui/core-ui/src/commonMain/kotlin/com/metacto/core/ui/permissions/IPermissionManager.kt b/ui/core-ui/src/commonMain/kotlin/com/metacto/core/ui/permissions/IPermissionManager.kt
index 9b8eb93e..a877c053 100644
--- a/ui/core-ui/src/commonMain/kotlin/com/metacto/core/ui/permissions/IPermissionManager.kt
+++ b/ui/core-ui/src/commonMain/kotlin/com/metacto/core/ui/permissions/IPermissionManager.kt
@@ -3,11 +3,76 @@ package com.metacto.core.ui.permissions
import com.metacto.core.ui.permissions.enums.Permission
import com.metacto.core.ui.permissions.enums.PermissionState
+/**
+ * Permission Manager Interface
+ *
+ * Provides a unified API for requesting and managing permissions across platforms.
+ * This interface handles the complexity of different permission systems between Android and iOS.
+ *
+ * Key Features:
+ * - Cross-platform permission management
+ * - Automatic permission state tracking
+ * - Smart handling of Android 13+ notification permissions
+ * - Race condition prevention through proper activity binding
+ *
+ * Important Notes:
+ * - For Android: Requires app to target API 33+ for notification permissions to work
+ * - Must call bind() with Activity before requesting permissions
+ * - Uses BindEffect in Compose to handle activity lifecycle automatically
+ *
+ * Usage Example:
+ * ```kotlin
+ * // In Compose screen
+ * val permissionManager = koinInject()
+ * BindEffect(permissionManager) // Binds to current activity
+ *
+ * // Request permission
+ * try {
+ * permissionManager.requestPermission(Permission.REMOTE_NOTIFICATION)
+ * // Permission granted
+ * } catch (e: DeniedException) {
+ * // User denied, can ask again
+ * } catch (e: DeniedAlwaysException) {
+ * // User permanently denied, redirect to settings
+ * }
+ * ```
+ */
expect interface IPermissionManager {
+ /**
+ * Requests a specific permission from the user.
+ *
+ * This method will:
+ * 1. Check if permission is already granted (returns immediately if true)
+ * 2. Show the system permission dialog
+ * 3. Handle the user's response appropriately
+ *
+ * @param permission The permission to request (e.g., REMOTE_NOTIFICATION, CAMERA)
+ * @param openAppSettingsIfRequired If true, will automatically open app settings
+ * when permission is permanently denied (DeniedAlwaysException)
+ *
+ * @throws DeniedException When user denies permission but can be asked again
+ * @throws DeniedAlwaysException When user permanently denies permission
+ * @throws RequestCanceledException When permission request is cancelled
+ * @throws IllegalStateException When PermissionManager is not bound to activity
+ */
suspend fun requestPermission(
permission: Permission,
openAppSettingsIfRequired: Boolean = true
)
+
+ /**
+ * Checks if a specific permission is currently granted.
+ *
+ * @param permission The permission to check
+ * @return true if permission is granted, false otherwise
+ */
suspend fun isPermissionGranted(permission: Permission): Boolean
+
+ /**
+ * Gets the current state of a permission.
+ *
+ * @param permission The permission to check
+ * @return PermissionState.Granted, PermissionState.Denied, or PermissionState.NotDetermined
+ */
suspend fun getPermissionState(permission: Permission): PermissionState
}