@sigx/lynx-permissions 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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Andreas Ekdahl
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,69 @@
1
+ # @sigx/lynx-permissions
2
+
3
+ **Android-only infrastructure module.** Provides the shared `PermissionHelper` + `MediaCapture` classes that `@sigx/lynx-camera`, `@sigx/lynx-image-picker`, `@sigx/lynx-location`, and `@sigx/lynx-notifications` all dispatch through to show OS permission dialogs and receive Activity Result callbacks. iOS doesn't need this — `UIImagePickerController`/`CLLocationManager`/etc. handle their own prompts.
4
+
5
+ You typically don't import this package directly — pull it in transitively by listing it after the modules that need it. Most apps that use Camera/ImagePicker/Location/Notifications will end up declaring it explicitly because the autolinker doesn't currently dedup peer requirements.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ pnpm add @sigx/lynx-permissions
11
+ ```
12
+
13
+ ```ts
14
+ // sigx.lynx.config.ts
15
+ export default defineLynxConfig({
16
+ modules: [
17
+ '@sigx/lynx-permissions',
18
+ '@sigx/lynx-camera',
19
+ '@sigx/lynx-image-picker',
20
+ // ...
21
+ ],
22
+ });
23
+ ```
24
+
25
+ `sigx prebuild` copies the Kotlin sources (`PermissionHelper.kt` + `MediaCapture.kt`) into your Android source tree.
26
+
27
+ ## How it works
28
+
29
+ The app template's `MainActivity.kt` reflectively wires this module on lifecycle hooks:
30
+
31
+ - `onResume` → `PermissionHelper.setActivity(this)`
32
+ - `onPause` → `PermissionHelper.clearActivity()`
33
+ - `onCreate` → `MediaCapture.register(this)` (Activity Result API launchers — must be wired before `STARTED` state)
34
+ - `onRequestPermissionsResult` → `PermissionHelper.onRequestPermissionsResult(...)`
35
+
36
+ The `try { Class.forName(...).getDeclaredField("INSTANCE").get(null) } catch { /* not present */ }` pattern means the wiring silently no-ops in apps that don't have this module on the classpath — so adding/removing it is a one-line change in `sigx.lynx.config.ts`.
37
+
38
+ ## Public API
39
+
40
+ The Kotlin classes are consumed by other native modules, not by JS. There's no `index.ts` export here — JS callers go through `@sigx/lynx-camera.requestPermission()` etc.
41
+
42
+ If you're authoring a new native module that needs runtime permissions on Android, reach into:
43
+
44
+ ```kotlin
45
+ import com.sigx.permissions.PermissionHelper
46
+
47
+ // In a coroutine-friendly context:
48
+ val granted = PermissionHelper.request(arrayOf(Manifest.permission.CAMERA))
49
+ ```
50
+
51
+ …and for `ActivityResultContracts`:
52
+
53
+ ```kotlin
54
+ import com.sigx.permissions.MediaCapture
55
+
56
+ // MediaCapture.takePicture(uri) etc. — wraps the system intents through
57
+ // pre-registered launchers so the call site doesn't need to deal with
58
+ // Activity Result lifecycle.
59
+ ```
60
+
61
+ ## Why a separate module
62
+
63
+ Camera + ImagePicker + Location + Notifications all need the same `requestPermissions()` plumbing on Android. Without a shared layer, each module would re-implement Activity Result wiring, hold its own static `Activity` reference, and fight over `onRequestPermissionsResult` request codes. Centralizing it here keeps each consumer module trivially small and avoids overlapping request-code namespaces.
64
+
65
+ iOS has no equivalent because the OS-level pickers (`UIImagePickerController`, `PHPickerViewController`, `CLLocationManager`) all handle their own permission flows internally.
66
+
67
+ ## Reference
68
+
69
+ The wiring in `packages/lynx-cli/templates/android/app/src/main/kotlin/__package__/MainActivity.kt` is the canonical example of how an `Activity` integrates with this module via reflection.
@@ -0,0 +1,203 @@
1
+ package com.sigx.permissions
2
+
3
+ import android.content.Context
4
+ import android.net.Uri
5
+ import android.util.Log
6
+ import androidx.activity.ComponentActivity
7
+ import androidx.activity.result.ActivityResultLauncher
8
+ import androidx.activity.result.PickVisualMediaRequest
9
+ import androidx.activity.result.contract.ActivityResultContracts
10
+ import androidx.core.content.FileProvider
11
+ import com.lynx.react.bridge.JavaOnlyMap
12
+ import java.io.File
13
+ import java.lang.ref.WeakReference
14
+
15
+ /**
16
+ * Module-level registry for Activity Result launchers. Camera + ImagePicker
17
+ * (and any future module needing Activity-result flows) delegate here so a
18
+ * single MainActivity hook covers them all.
19
+ *
20
+ * Design constraint: ActivityResultLaunchers MUST be registered BEFORE the
21
+ * Activity reaches STARTED state. So `register(activity)` has to be called
22
+ * from `onCreate()`, not lazily on first use. The MainActivity template
23
+ * wires this via the same reflective `callPermissionHelper(...)` path used
24
+ * for setActivity / clearActivity.
25
+ *
26
+ * Callbacks resolve once the user finishes the system UI (or cancels). We
27
+ * stash the pending callback on the singleton because the Activity-result
28
+ * launcher's own callback runs on the main thread but doesn't carry context;
29
+ * the module-side caller registered its callback via `takePicture(callback)`
30
+ * just before launching, and we route the result back to that callback.
31
+ *
32
+ * Concurrency: `pendingTakePictureCallback` and `pendingPickMediaCallback`
33
+ * are guarded by their nullness — only one camera launch or one picker
34
+ * launch at a time. Re-entrant calls overwrite the previous pending callback;
35
+ * the pre-empted callback is invoked with `error="cancelled"`.
36
+ */
37
+ object MediaCapture {
38
+
39
+ private const val TAG = "MediaCapture"
40
+
41
+ private var activityRef: WeakReference<ComponentActivity>? = null
42
+ private var takePictureLauncher: ActivityResultLauncher<Uri>? = null
43
+ private var pickMediaLauncher: ActivityResultLauncher<PickVisualMediaRequest>? = null
44
+
45
+ private var pendingPhotoUri: Uri? = null
46
+ private var pendingTakePictureCallback: ((JavaOnlyMap) -> Unit)? = null
47
+ private var pendingPickMediaCallback: ((JavaOnlyMap) -> Unit)? = null
48
+
49
+ /**
50
+ * Wire this Activity into the registry. Call from MainActivity.onCreate
51
+ * BEFORE super.onCreate() returns OR at the very top of onCreate (after
52
+ * super) — registerForActivityResult requires the host to be in INITIALIZED
53
+ * or CREATED state, throws afterwards.
54
+ */
55
+ fun register(activity: ComponentActivity) {
56
+ activityRef = WeakReference(activity)
57
+
58
+ takePictureLauncher = activity.registerForActivityResult(
59
+ ActivityResultContracts.TakePicture()
60
+ ) { success ->
61
+ val uri = pendingPhotoUri
62
+ val cb = pendingTakePictureCallback
63
+ pendingPhotoUri = null
64
+ pendingTakePictureCallback = null
65
+ val result = JavaOnlyMap()
66
+ if (success && uri != null) {
67
+ result.putString("uri", uri.toString())
68
+ result.putBoolean("canceled", false)
69
+ } else {
70
+ result.putString("error", "cancelled")
71
+ result.putBoolean("canceled", true)
72
+ }
73
+ cb?.invoke(result)
74
+ }
75
+
76
+ pickMediaLauncher = activity.registerForActivityResult(
77
+ ActivityResultContracts.PickVisualMedia()
78
+ ) { uri ->
79
+ val cb = pendingPickMediaCallback
80
+ pendingPickMediaCallback = null
81
+ val result = JavaOnlyMap()
82
+ if (uri != null) {
83
+ val assets = com.lynx.react.bridge.JavaOnlyArray()
84
+ val asset = JavaOnlyMap().apply {
85
+ putString("uri", uri.toString())
86
+ putString("type", "image")
87
+ }
88
+ assets.pushMap(asset)
89
+ result.putArray("assets", assets)
90
+ result.putBoolean("canceled", false)
91
+ } else {
92
+ result.putBoolean("canceled", true)
93
+ }
94
+ cb?.invoke(result)
95
+ }
96
+
97
+ Log.i(TAG, "registered launchers on ${activity.javaClass.simpleName}")
98
+ }
99
+
100
+ /**
101
+ * Clear the Activity reference. Call from MainActivity.onDestroy.
102
+ */
103
+ fun unregister() {
104
+ activityRef = null
105
+ takePictureLauncher = null
106
+ pickMediaLauncher = null
107
+ // Resolve any pending callbacks with cancel so JS Promises don't hang.
108
+ pendingTakePictureCallback?.invoke(JavaOnlyMap().apply {
109
+ putString("error", "activity destroyed")
110
+ putBoolean("canceled", true)
111
+ })
112
+ pendingPickMediaCallback?.invoke(JavaOnlyMap().apply {
113
+ putString("error", "activity destroyed")
114
+ putBoolean("canceled", true)
115
+ })
116
+ pendingTakePictureCallback = null
117
+ pendingPickMediaCallback = null
118
+ pendingPhotoUri = null
119
+ }
120
+
121
+ /**
122
+ * Launch the system camera. Requires CAMERA permission + a configured
123
+ * FileProvider authority "${packageName}.fileprovider" (the MainActivity
124
+ * AndroidManifest template seeds this). The captured photo lands at
125
+ * cacheDir/sigx-camera-<timestamp>.jpg; the JS callback receives a
126
+ * content:// URI pointing at it.
127
+ */
128
+ fun takePicture(context: Context, callback: (JavaOnlyMap) -> Unit) {
129
+ val activity = activityRef?.get()
130
+ val launcher = takePictureLauncher
131
+ if (activity == null || launcher == null) {
132
+ callback(JavaOnlyMap().apply {
133
+ putString("error", "MediaCapture not registered — wire MediaCapture.register(this) into MainActivity.onCreate")
134
+ putBoolean("canceled", true)
135
+ })
136
+ return
137
+ }
138
+ // Pre-empt any prior pending call.
139
+ pendingTakePictureCallback?.invoke(JavaOnlyMap().apply {
140
+ putString("error", "cancelled by new takePicture")
141
+ putBoolean("canceled", true)
142
+ })
143
+
144
+ val photoFile = File(context.cacheDir, "sigx-camera-${System.currentTimeMillis()}.jpg")
145
+ val authority = "${context.packageName}.fileprovider"
146
+ val photoUri = try {
147
+ FileProvider.getUriForFile(context, authority, photoFile)
148
+ } catch (e: Throwable) {
149
+ callback(JavaOnlyMap().apply {
150
+ putString("error", "FileProvider authority '$authority' not configured: ${e.message}")
151
+ putBoolean("canceled", true)
152
+ })
153
+ return
154
+ }
155
+
156
+ pendingPhotoUri = photoUri
157
+ pendingTakePictureCallback = callback
158
+ try {
159
+ launcher.launch(photoUri)
160
+ } catch (e: Throwable) {
161
+ pendingPhotoUri = null
162
+ pendingTakePictureCallback = null
163
+ callback(JavaOnlyMap().apply {
164
+ putString("error", e.message ?: "launcher.launch failed")
165
+ putBoolean("canceled", true)
166
+ })
167
+ }
168
+ }
169
+
170
+ /**
171
+ * Launch the system photo picker. No CAMERA / READ_MEDIA_IMAGES permission
172
+ * needed — Android 13+'s photo picker grants per-pick access on the fly.
173
+ * Returns a content:// URI for the picked image (or canceled=true).
174
+ */
175
+ fun pickImage(callback: (JavaOnlyMap) -> Unit) {
176
+ val launcher = pickMediaLauncher
177
+ if (launcher == null) {
178
+ callback(JavaOnlyMap().apply {
179
+ putString("error", "MediaCapture not registered — wire MediaCapture.register(this) into MainActivity.onCreate")
180
+ putBoolean("canceled", true)
181
+ })
182
+ return
183
+ }
184
+ pendingPickMediaCallback?.invoke(JavaOnlyMap().apply {
185
+ putString("error", "cancelled by new pickImage")
186
+ putBoolean("canceled", true)
187
+ })
188
+ pendingPickMediaCallback = callback
189
+ try {
190
+ launcher.launch(
191
+ PickVisualMediaRequest.Builder()
192
+ .setMediaType(ActivityResultContracts.PickVisualMedia.ImageOnly)
193
+ .build()
194
+ )
195
+ } catch (e: Throwable) {
196
+ pendingPickMediaCallback = null
197
+ callback(JavaOnlyMap().apply {
198
+ putString("error", e.message ?: "launcher.launch failed")
199
+ putBoolean("canceled", true)
200
+ })
201
+ }
202
+ }
203
+ }
@@ -0,0 +1,251 @@
1
+ package com.sigx.permissions
2
+
3
+ import android.Manifest
4
+ import android.app.Activity
5
+ import android.content.Context
6
+ import android.content.Intent
7
+ import android.content.pm.PackageManager
8
+ import android.net.Uri
9
+ import android.os.Build
10
+ import android.provider.Settings
11
+ import android.util.Log
12
+ import androidx.core.app.ActivityCompat
13
+ import androidx.core.content.ContextCompat
14
+ import com.lynx.react.bridge.JavaOnlyMap
15
+ import java.lang.ref.WeakReference
16
+
17
+ /**
18
+ * Shared permission utility for sigx-lynx-go native modules.
19
+ *
20
+ * NOT a LynxModule — a Kotlin utility class that other modules (CameraModule,
21
+ * LocationModule, etc.) call internally to check and request permissions.
22
+ *
23
+ * Requires [setActivity] to be called from MainActivity.onResume() so that
24
+ * runtime permission requests can show the OS dialog.
25
+ */
26
+ object PermissionHelper {
27
+
28
+ private const val TAG = "PermissionHelper"
29
+
30
+ private var activityRef: WeakReference<Activity>? = null
31
+ private var requestCode = 1000
32
+ private val pendingCallbacks = mutableMapOf<Int, (JavaOnlyMap) -> Unit>()
33
+
34
+ /**
35
+ * Set the current Activity reference. Call from MainActivity.onResume().
36
+ */
37
+ fun setActivity(activity: Activity) {
38
+ activityRef = WeakReference(activity)
39
+ }
40
+
41
+ /**
42
+ * Clear the Activity reference. Call from MainActivity.onPause().
43
+ */
44
+ fun clearActivity() {
45
+ activityRef = null
46
+ }
47
+
48
+ /**
49
+ * Map a sigx permission key to Android permission string(s).
50
+ * Handles API-level branching (e.g., READ_MEDIA_IMAGES on API 33+).
51
+ */
52
+ fun resolveAndroidPermissions(permission: String): List<String> {
53
+ return when (permission) {
54
+ "camera" -> listOf(Manifest.permission.CAMERA)
55
+ "microphone" -> listOf(Manifest.permission.RECORD_AUDIO)
56
+ "location_when_in_use", "location" -> listOf(
57
+ Manifest.permission.ACCESS_FINE_LOCATION,
58
+ Manifest.permission.ACCESS_COARSE_LOCATION
59
+ )
60
+ "location_always" -> listOf(Manifest.permission.ACCESS_BACKGROUND_LOCATION)
61
+ "photo_library" -> {
62
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
63
+ listOf(
64
+ Manifest.permission.READ_MEDIA_IMAGES,
65
+ Manifest.permission.READ_MEDIA_VIDEO
66
+ )
67
+ } else {
68
+ @Suppress("DEPRECATION")
69
+ listOf(Manifest.permission.READ_EXTERNAL_STORAGE)
70
+ }
71
+ }
72
+ "notifications" -> {
73
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
74
+ listOf(Manifest.permission.POST_NOTIFICATIONS)
75
+ } else {
76
+ emptyList() // No runtime permission needed pre-API 33
77
+ }
78
+ }
79
+ "contacts" -> listOf(Manifest.permission.READ_CONTACTS)
80
+ "calendar" -> listOf(Manifest.permission.READ_CALENDAR)
81
+ "bluetooth" -> {
82
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
83
+ listOf(
84
+ Manifest.permission.BLUETOOTH_SCAN,
85
+ Manifest.permission.BLUETOOTH_CONNECT
86
+ )
87
+ } else {
88
+ @Suppress("DEPRECATION")
89
+ listOf(Manifest.permission.BLUETOOTH)
90
+ }
91
+ }
92
+ else -> {
93
+ // Allow raw Android permission strings (e.g., "android.permission.CAMERA")
94
+ if (permission.startsWith("android.permission.")) {
95
+ listOf(permission)
96
+ } else {
97
+ Log.w(TAG, "Unknown permission key: $permission")
98
+ emptyList()
99
+ }
100
+ }
101
+ }
102
+ }
103
+
104
+ /**
105
+ * Check permission status without prompting.
106
+ * Returns a map with { status, canAskAgain }.
107
+ */
108
+ fun checkPermission(context: Context, permission: String): JavaOnlyMap {
109
+ val androidPerms = resolveAndroidPermissions(permission)
110
+ val result = JavaOnlyMap()
111
+
112
+ if (androidPerms.isEmpty()) {
113
+ // No runtime permission required (e.g., notifications pre-API 33)
114
+ result.putString("status", "granted")
115
+ result.putBoolean("canAskAgain", false)
116
+ return result
117
+ }
118
+
119
+ val allGranted = androidPerms.all { perm ->
120
+ ContextCompat.checkSelfPermission(context, perm) == PackageManager.PERMISSION_GRANTED
121
+ }
122
+
123
+ if (allGranted) {
124
+ result.putString("status", "granted")
125
+ result.putBoolean("canAskAgain", false)
126
+ return result
127
+ }
128
+
129
+ // Check if we can ask again (only meaningful if we have an Activity)
130
+ val activity = activityRef?.get()
131
+ val canAskAgain = if (activity != null) {
132
+ androidPerms.any { perm ->
133
+ ActivityCompat.shouldShowRequestPermissionRationale(activity, perm)
134
+ }
135
+ } else {
136
+ true // Assume we can ask if no Activity to check
137
+ }
138
+
139
+ // Determine if blocked (denied + can't ask again) vs undetermined (never asked)
140
+ val anyDeniedPermanently = activity != null && androidPerms.any { perm ->
141
+ ContextCompat.checkSelfPermission(context, perm) != PackageManager.PERMISSION_GRANTED &&
142
+ !ActivityCompat.shouldShowRequestPermissionRationale(activity, perm)
143
+ }
144
+
145
+ // If permission was never requested, shouldShowRationale returns false too,
146
+ // so we use SharedPreferences to track if we've asked before
147
+ val prefs = context.getSharedPreferences("sigx_permissions", Context.MODE_PRIVATE)
148
+ val hasAskedBefore = prefs.getBoolean("asked_$permission", false)
149
+
150
+ if (!hasAskedBefore) {
151
+ result.putString("status", "undetermined")
152
+ result.putBoolean("canAskAgain", true)
153
+ } else if (anyDeniedPermanently && !canAskAgain) {
154
+ result.putString("status", "blocked")
155
+ result.putBoolean("canAskAgain", false)
156
+ } else {
157
+ result.putString("status", "denied")
158
+ result.putBoolean("canAskAgain", canAskAgain)
159
+ }
160
+
161
+ return result
162
+ }
163
+
164
+ /**
165
+ * Request a permission, showing the OS dialog if possible.
166
+ * Calls back with { status, canAskAgain }.
167
+ */
168
+ fun requestPermission(context: Context, permission: String, callback: (JavaOnlyMap) -> Unit) {
169
+ val androidPerms = resolveAndroidPermissions(permission)
170
+
171
+ if (androidPerms.isEmpty()) {
172
+ val result = JavaOnlyMap()
173
+ result.putString("status", "granted")
174
+ result.putBoolean("canAskAgain", false)
175
+ callback(result)
176
+ return
177
+ }
178
+
179
+ // Check if already granted
180
+ val allGranted = androidPerms.all { perm ->
181
+ ContextCompat.checkSelfPermission(context, perm) == PackageManager.PERMISSION_GRANTED
182
+ }
183
+ if (allGranted) {
184
+ val result = JavaOnlyMap()
185
+ result.putString("status", "granted")
186
+ result.putBoolean("canAskAgain", false)
187
+ callback(result)
188
+ return
189
+ }
190
+
191
+ val activity = activityRef?.get()
192
+ if (activity == null) {
193
+ Log.w(TAG, "No Activity available to request permission: $permission")
194
+ val result = JavaOnlyMap()
195
+ result.putString("status", "denied")
196
+ result.putBoolean("canAskAgain", true)
197
+ callback(result)
198
+ return
199
+ }
200
+
201
+ // Mark that we've asked for this permission
202
+ val prefs = context.getSharedPreferences("sigx_permissions", Context.MODE_PRIVATE)
203
+ prefs.edit().putBoolean("asked_$permission", true).apply()
204
+
205
+ // Request the permission
206
+ val code = requestCode++
207
+ pendingCallbacks[code] = callback
208
+ ActivityCompat.requestPermissions(activity, androidPerms.toTypedArray(), code)
209
+ }
210
+
211
+ /**
212
+ * Called from MainActivity.onRequestPermissionsResult() to resolve pending callbacks.
213
+ */
214
+ fun onRequestPermissionsResult(
215
+ requestCode: Int,
216
+ permissions: Array<out String>,
217
+ grantResults: IntArray
218
+ ) {
219
+ val callback = pendingCallbacks.remove(requestCode) ?: return
220
+
221
+ val allGranted = grantResults.isNotEmpty() && grantResults.all {
222
+ it == PackageManager.PERMISSION_GRANTED
223
+ }
224
+
225
+ val result = JavaOnlyMap()
226
+ if (allGranted) {
227
+ result.putString("status", "granted")
228
+ result.putBoolean("canAskAgain", false)
229
+ } else {
230
+ val activity = activityRef?.get()
231
+ val canAskAgain = activity != null && permissions.any { perm ->
232
+ ActivityCompat.shouldShowRequestPermissionRationale(activity, perm)
233
+ }
234
+ result.putString("status", if (canAskAgain) "denied" else "blocked")
235
+ result.putBoolean("canAskAgain", canAskAgain)
236
+ }
237
+
238
+ callback(result)
239
+ }
240
+
241
+ /**
242
+ * Open the app's system settings page (for when permissions are blocked).
243
+ */
244
+ fun openSettings(context: Context) {
245
+ val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
246
+ data = Uri.fromParts("package", context.packageName, null)
247
+ addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
248
+ }
249
+ context.startActivity(intent)
250
+ }
251
+ }
@@ -0,0 +1,47 @@
1
+ package com.sigx.permissions
2
+
3
+ import android.app.Activity
4
+ import android.os.Bundle
5
+ import androidx.activity.ComponentActivity
6
+
7
+ /**
8
+ * Activity-lifecycle hook for `@sigx/lynx-permissions`.
9
+ *
10
+ * Discovered by the auto-linker via `sigx-module.json`'s
11
+ * `android.activityHook`. Wires the host Activity into [PermissionHelper]
12
+ * (so runtime permission prompts can show their OS dialog) and registers
13
+ * Activity-result launchers used by Camera + ImagePicker.
14
+ *
15
+ * `MediaCapture.register` MUST run before the Activity reaches STARTED — the
16
+ * Android Activity-result API enforces this — so we call it from `onCreate`
17
+ * rather than `onResume`.
18
+ */
19
+ object PermissionsActivityHook {
20
+
21
+ @JvmStatic
22
+ fun onCreate(activity: Activity, savedInstanceState: Bundle?) {
23
+ if (activity is ComponentActivity) {
24
+ MediaCapture.register(activity)
25
+ }
26
+ }
27
+
28
+ @JvmStatic
29
+ fun onResume(activity: Activity) {
30
+ PermissionHelper.setActivity(activity)
31
+ }
32
+
33
+ @JvmStatic
34
+ fun onPause(activity: Activity) {
35
+ PermissionHelper.clearActivity()
36
+ }
37
+
38
+ @JvmStatic
39
+ fun onRequestPermissionsResult(
40
+ activity: Activity,
41
+ requestCode: Int,
42
+ permissions: Array<String>,
43
+ grantResults: IntArray,
44
+ ) {
45
+ PermissionHelper.onRequestPermissionsResult(requestCode, permissions, grantResults)
46
+ }
47
+ }
package/package.json ADDED
@@ -0,0 +1,19 @@
1
+ {
2
+ "name": "@sigx/lynx-permissions",
3
+ "version": "0.1.0",
4
+ "description": "Shared Android permission helper for sigx-lynx native modules",
5
+ "type": "module",
6
+ "main": "./src/index.ts",
7
+ "types": "./src/index.ts",
8
+ "exports": {
9
+ ".": "./src/index.ts",
10
+ "./sigx-module.json": "./sigx-module.json"
11
+ },
12
+ "files": [
13
+ "src",
14
+ "android",
15
+ "sigx-module.json"
16
+ ],
17
+ "author": "Andreas Ekdahl",
18
+ "license": "MIT"
19
+ }
@@ -0,0 +1,13 @@
1
+ {
2
+ "name": "Permissions",
3
+ "package": "@sigx/lynx-permissions",
4
+ "description": "Shared Android permission helper (peer dep for camera/location/etc.)",
5
+ "platforms": ["android"],
6
+ "android": {
7
+ "activityHook": {
8
+ "class": "com.sigx.permissions.PermissionsActivityHook",
9
+ "methods": ["onCreate", "onResume", "onPause", "onRequestPermissionsResult"]
10
+ },
11
+ "sourceDir": "android"
12
+ }
13
+ }
package/src/index.ts ADDED
@@ -0,0 +1,18 @@
1
+ /**
2
+ * @sigx/lynx-permissions — shared Android permission helper.
3
+ *
4
+ * This package ships only Android Kotlin source (PermissionHelper.kt). It is
5
+ * a peer dependency of camera/location/imagepicker/notifications/filesystem
6
+ * modules that need to check or request runtime permissions. The host's
7
+ * Activity must call:
8
+ *
9
+ * - `PermissionHelper.setActivity(this)` in `onResume()`
10
+ * - `PermissionHelper.clearActivity()` in `onPause()`
11
+ * - `PermissionHelper.onRequestPermissionsResult(requestCode, permissions, grantResults)`
12
+ * in `onRequestPermissionsResult(...)`
13
+ *
14
+ * No JS surface today — modules call the Kotlin helper internally. If a
15
+ * cross-platform JS-side `permissions.request('camera')` API ever lands, it
16
+ * goes here.
17
+ */
18
+ export {};