@klyftig/android-perms 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,23 @@
1
+ # @klyftig/android-perms
2
+
3
+ Expo native module (Android only): the alarm-permission probes and settings
4
+ deep links that notifee does not expose. Extracted from Sleepwise's
5
+ `modules/sleepwise-android-perms` (2026-09-15); the Kotlin identifier is still
6
+ `SleepwiseAndroidPerms`.
7
+
8
+ ```ts
9
+ import {
10
+ canUseFullScreenIntent, // Android 14+: revocable for non-alarm-category apps
11
+ isNotificationPolicyAccessGranted, // DND bypass
12
+ canDrawOverlays, // SYSTEM_ALERT_WINDOW
13
+ getManufacturer, // for OEM battery-killer guidance
14
+ openAndroidSettings, // deep link to the matching system screen
15
+ } from '@klyftig/android-perms';
16
+ ```
17
+
18
+ `@klyftig/alarm-core`'s `getAlarmPermissionStatus()` / `openPermissionScreen`
19
+ compose these with notifee's own probes into one status object for onboarding
20
+ and the Settings "alarm health" rows.
21
+
22
+ Off-device (iOS / web / jest) every probe returns a safe default. Autolinks
23
+ from `node_modules`; needs a dev client.
@@ -0,0 +1,19 @@
1
+ apply plugin: 'com.android.library'
2
+ apply plugin: 'kotlin-android'
3
+
4
+ group = 'expo.modules.sleepwiseandroidperms'
5
+ version = '0.1.0'
6
+
7
+ def expoModulesCorePlugin = new File(project(":expo-modules-core").projectDir.absolutePath, "ExpoModulesCorePlugin.gradle")
8
+ apply from: expoModulesCorePlugin
9
+ applyKotlinExpoModulesCorePlugin()
10
+ useCoreDependencies()
11
+ useDefaultAndroidSdkVersions()
12
+ useExpoPublishing()
13
+
14
+ android {
15
+ namespace "expo.modules.sleepwiseandroidperms"
16
+ defaultConfig {
17
+ minSdkVersion 24
18
+ }
19
+ }
@@ -0,0 +1 @@
1
+ <manifest xmlns:android="http://schemas.android.com/apk/res/android" />
@@ -0,0 +1,95 @@
1
+ package expo.modules.sleepwiseandroidperms
2
+
3
+ import android.app.NotificationManager
4
+ import android.content.Context
5
+ import android.content.Intent
6
+ import android.net.Uri
7
+ import android.os.Build
8
+ import android.provider.Settings
9
+ import expo.modules.kotlin.modules.Module
10
+ import expo.modules.kotlin.modules.ModuleDefinition
11
+
12
+ /**
13
+ * The alarm grants notifee doesn't expose (Phase 3 guided permissions):
14
+ *
15
+ * - USE_FULL_SCREEN_INTENT: on Android 14+ (API 34) it is a per-app grant that
16
+ * Play can revoke for non-alarm apps; without it the full-screen ringing
17
+ * screen never appears over the lock screen (only a heads-up).
18
+ * - Notification-policy ("Do Not Disturb") access: without it the channel's
19
+ * bypassDnd stays false and a phone in DND swallows the alarm.
20
+ * - SYSTEM_ALERT_WINDOW (overlay): Alarmy asks for it on Android 10+ as an
21
+ * extra guarantee for drawing over other apps.
22
+ *
23
+ * Each has a status read + a deep link to the exact system screen.
24
+ */
25
+ class SleepwiseAndroidPermsModule : Module() {
26
+ override fun definition() = ModuleDefinition {
27
+ Name("SleepwiseAndroidPerms")
28
+
29
+ Function("canUseFullScreenIntent") {
30
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
31
+ notificationManager().canUseFullScreenIntent()
32
+ } else {
33
+ true // auto-granted before API 34
34
+ }
35
+ }
36
+
37
+ Function("openFullScreenIntentSettings") {
38
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
39
+ startSettings(Intent(Settings.ACTION_MANAGE_APP_USE_FULL_SCREEN_INTENT).apply {
40
+ data = Uri.parse("package:${context.packageName}")
41
+ })
42
+ } else {
43
+ openAppDetails()
44
+ }
45
+ }
46
+
47
+ Function("isNotificationPolicyAccessGranted") {
48
+ notificationManager().isNotificationPolicyAccessGranted
49
+ }
50
+
51
+ Function("openNotificationPolicyAccessSettings") {
52
+ startSettings(Intent(Settings.ACTION_NOTIFICATION_POLICY_ACCESS_SETTINGS))
53
+ }
54
+
55
+ Function("canDrawOverlays") {
56
+ Settings.canDrawOverlays(context)
57
+ }
58
+
59
+ Function("openOverlaySettings") {
60
+ startSettings(Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION).apply {
61
+ data = Uri.parse("package:${context.packageName}")
62
+ })
63
+ }
64
+
65
+ Function("openAppDetailsSettings") {
66
+ openAppDetails()
67
+ }
68
+
69
+ Function("getManufacturer") {
70
+ Build.MANUFACTURER ?: ""
71
+ }
72
+ }
73
+
74
+ private val context: Context
75
+ get() = requireNotNull(appContext.reactContext) { "React context unavailable" }
76
+
77
+ private fun notificationManager(): NotificationManager =
78
+ context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
79
+
80
+ private fun openAppDetails() {
81
+ startSettings(Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
82
+ data = Uri.parse("package:${context.packageName}")
83
+ })
84
+ }
85
+
86
+ private fun startSettings(intent: Intent) {
87
+ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
88
+ try {
89
+ context.startActivity(intent)
90
+ } catch (_: Exception) {
91
+ // Some OEM builds lack the specific screen — fall back to the app's page.
92
+ if (intent.action != Settings.ACTION_APPLICATION_DETAILS_SETTINGS) openAppDetails()
93
+ }
94
+ }
95
+ }
@@ -0,0 +1,6 @@
1
+ {
2
+ "platforms": ["android"],
3
+ "android": {
4
+ "modules": ["expo.modules.sleepwiseandroidperms.SleepwiseAndroidPermsModule"]
5
+ }
6
+ }
package/index.ts ADDED
@@ -0,0 +1,49 @@
1
+ /**
2
+ * JS surface for `sleepwise-android-perms` — the alarm grants notifee doesn't
3
+ * expose. Android-only; every call degrades to a safe value elsewhere (and in
4
+ * jest), so the permissions helper never has to branch on platform itself.
5
+ */
6
+ import { Platform } from 'react-native';
7
+ import { requireNativeModule } from 'expo-modules-core';
8
+
9
+ interface NativePerms {
10
+ canUseFullScreenIntent(): boolean;
11
+ openFullScreenIntentSettings(): void;
12
+ isNotificationPolicyAccessGranted(): boolean;
13
+ openNotificationPolicyAccessSettings(): void;
14
+ canDrawOverlays(): boolean;
15
+ openOverlaySettings(): void;
16
+ openAppDetailsSettings(): void;
17
+ getManufacturer(): string;
18
+ }
19
+
20
+ let native: NativePerms | null = null;
21
+ if (Platform.OS === 'android') {
22
+ try {
23
+ native = requireNativeModule<NativePerms>('SleepwiseAndroidPerms');
24
+ } catch {
25
+ native = null;
26
+ }
27
+ }
28
+
29
+ export const isAndroidPermsAvailable = native != null;
30
+
31
+ /** null = unknown (not Android / module missing). */
32
+ export function canUseFullScreenIntent(): boolean | null {
33
+ return native ? native.canUseFullScreenIntent() : null;
34
+ }
35
+ export function isNotificationPolicyAccessGranted(): boolean | null {
36
+ return native ? native.isNotificationPolicyAccessGranted() : null;
37
+ }
38
+ export function canDrawOverlays(): boolean | null {
39
+ return native ? native.canDrawOverlays() : null;
40
+ }
41
+ export function getManufacturer(): string {
42
+ return native ? native.getManufacturer() : '';
43
+ }
44
+ export const openAndroidSettings = {
45
+ fullScreenIntent: () => native?.openFullScreenIntentSettings(),
46
+ notificationPolicy: () => native?.openNotificationPolicyAccessSettings(),
47
+ overlay: () => native?.openOverlaySettings(),
48
+ appDetails: () => native?.openAppDetailsSettings(),
49
+ };
package/package.json ADDED
@@ -0,0 +1,29 @@
1
+ {
2
+ "name": "@klyftig/android-perms",
3
+ "version": "0.1.0",
4
+ "description": "Android alarm-permission status + settings deep links notifee doesn't cover (full-screen intent, DND access, overlay, manufacturer)",
5
+ "main": "index.ts",
6
+ "types": "index.ts",
7
+ "files": [
8
+ "index.ts",
9
+ "expo-module.config.json",
10
+ "android/build.gradle",
11
+ "android/src",
12
+ "ios"
13
+ ],
14
+ "keywords": [
15
+ "expo",
16
+ "expo-module",
17
+ "react-native",
18
+ "alarm",
19
+ "klyftig"
20
+ ],
21
+ "license": "UNLICENSED",
22
+ "peerDependencies": {
23
+ "expo": "*",
24
+ "expo-modules-core": "*"
25
+ },
26
+ "publishConfig": {
27
+ "access": "public"
28
+ }
29
+ }