@ajuarezso/capacitor-native-location-auth 1.0.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) 2026 Anthony Juarez Solis
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/Package.swift ADDED
@@ -0,0 +1,30 @@
1
+ // swift-tools-version: 5.9
2
+
3
+ import PackageDescription
4
+
5
+ let package = Package(
6
+ name: "NativeLocationAuth",
7
+ platforms: [.iOS(.v15)],
8
+ products: [
9
+ .library(
10
+ name: "NativeLocationAuth",
11
+ targets: ["NativeLocationAuthPlugin"])
12
+ ],
13
+ dependencies: [
14
+ // Rango amplio para coexistir con plugins que aún están en
15
+ // capacitor-swift-pm 7.x (caso de @capacitor-community/background-geolocation
16
+ // que sigue pinneado a 7.0.0..<8.0.0). El plugin custom funciona con
17
+ // las APIs base de Capacitor que existen en ambas versiones.
18
+ .package(url: "https://github.com/ionic-team/capacitor-swift-pm.git", "7.0.0"..<"9.0.0")
19
+ ],
20
+ targets: [
21
+ .target(
22
+ name: "NativeLocationAuthPlugin",
23
+ dependencies: [
24
+ .product(name: "Capacitor", package: "capacitor-swift-pm"),
25
+ .product(name: "Cordova", package: "capacitor-swift-pm"),
26
+ ],
27
+ path: "ios/Sources/NativeLocationAuthPlugin"
28
+ )
29
+ ]
30
+ )
package/README.md ADDED
@@ -0,0 +1,155 @@
1
+ # @ajuarezso/capacitor-native-location-auth
2
+
3
+ Capacitor plugin to distinguish the **4 real levels of location authorization** (`notDetermined` / `whenInUse` / `always` / `denied`) on iOS and Android.
4
+
5
+ Fills the gap left by [`@capacitor/geolocation`](https://capacitorjs.com/docs/apis/geolocation), which only reports generic `granted` / `denied` / `prompt` without distinguishing foreground vs background.
6
+
7
+ Designed for apps that need to build a **2-step educational permission flow**:
8
+ 1. Ask user for `whenInUse` first (foreground only) — iOS shows a simple dialog with no "Always" option mixed in.
9
+ 2. After foreground granted, ask for `always` (background) — iOS shows a dedicated upgrade dialog ("Allow always" vs "Keep while using").
10
+
11
+ This is the UX flow Apple recommends and that apps like Uber, Rappi, and DoorDash use.
12
+
13
+ ## Install
14
+
15
+ ```bash
16
+ npm install @ajuarezso/capacitor-native-location-auth
17
+ npx cap sync
18
+ ```
19
+
20
+ ## iOS Setup
21
+
22
+ In `ios/App/App/Info.plist`, ensure you have both keys:
23
+
24
+ ```xml
25
+ <key>NSLocationWhenInUseUsageDescription</key>
26
+ <string>Your app needs location to ...</string>
27
+ <key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
28
+ <string>Your app needs background location to ...</string>
29
+ ```
30
+
31
+ If you need background tracking, also add `location` to `UIBackgroundModes`.
32
+
33
+ ## Android Setup
34
+
35
+ In `android/app/src/main/AndroidManifest.xml`:
36
+
37
+ ```xml
38
+ <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
39
+ <uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
40
+ ```
41
+
42
+ `ACCESS_BACKGROUND_LOCATION` is only needed for Android 10+ (API 29+).
43
+
44
+ ## API
45
+
46
+ ### Types
47
+
48
+ ```ts
49
+ type LocationAuthStatus = 'notDetermined' | 'whenInUse' | 'always' | 'denied';
50
+
51
+ interface LocationAuthStatusResult {
52
+ status: LocationAuthStatus;
53
+ }
54
+ ```
55
+
56
+ ### `getStatus(): Promise<LocationAuthStatusResult>`
57
+
58
+ Returns the current authorization level without prompting.
59
+
60
+ ```ts
61
+ import { NativeLocationAuth } from '@ajuarezso/capacitor-native-location-auth';
62
+
63
+ const { status } = await NativeLocationAuth.getStatus();
64
+ // 'notDetermined' | 'whenInUse' | 'always' | 'denied'
65
+ ```
66
+
67
+ ### `requestWhenInUse(): Promise<LocationAuthStatusResult>`
68
+
69
+ Requests **foreground-only** authorization ("While using the app").
70
+
71
+ - **iOS**: calls `CLLocationManager.requestWhenInUseAuthorization()`. Shows a system dialog with only `whenInUse` / `oneTime` / `deny` — **not** the combined 4-option dialog that `Geolocation.requestPermissions()` would show.
72
+ - **Android**: requests `ACCESS_FINE_LOCATION`.
73
+
74
+ If status is already `whenInUse` or `always`, resolves immediately without showing a dialog. If permanently denied, returns `'denied'` and you should direct user to Settings.
75
+
76
+ ```ts
77
+ const { status } = await NativeLocationAuth.requestWhenInUse();
78
+ if (status === 'whenInUse') {
79
+ // Now you can ask for 'always' separately
80
+ }
81
+ ```
82
+
83
+ ### `requestAlways(): Promise<LocationAuthStatusResult>`
84
+
85
+ Requests **background** authorization ("Always allow"). Call this **after** `requestWhenInUse()` succeeded.
86
+
87
+ - **iOS**: calls `CLLocationManager.requestAlwaysAuthorization()`. If status is `whenInUse`, iOS shows the upgrade dialog ("Allow always" vs "Keep while using"). If user already denied, no dialog is shown — only Settings can change it.
88
+ - **Android 10+**: requests `ACCESS_BACKGROUND_LOCATION` with its own runtime dialog ("Allow all the time" vs "Allow only while using").
89
+ - **Android <10**: resolves immediately with `'always'` because background isn't a separate permission below API 29.
90
+
91
+ ```ts
92
+ const { status } = await NativeLocationAuth.requestAlways();
93
+ if (status === 'always') {
94
+ // Start background tracking
95
+ }
96
+ ```
97
+
98
+ ## Complete 2-step Flow Example
99
+
100
+ ```ts
101
+ import { NativeLocationAuth } from '@ajuarezso/capacitor-native-location-auth';
102
+
103
+ async function enableBackgroundLocation() {
104
+ const initial = await NativeLocationAuth.getStatus();
105
+
106
+ if (initial.status === 'always') {
107
+ // Already authorized for background; start tracking
108
+ return startTracking();
109
+ }
110
+
111
+ if (initial.status === 'denied') {
112
+ // Permanently denied; direct user to Settings
113
+ return openSettings();
114
+ }
115
+
116
+ // Step 1: ask for foreground
117
+ await showEducationalModal({ title: 'Enable location', cta: 'Continue' });
118
+ const fg = await NativeLocationAuth.requestWhenInUse();
119
+ if (fg.status === 'denied') return openSettings();
120
+
121
+ // Step 2: ask for background
122
+ await showEducationalModal({ title: 'One more permission', cta: 'Continue' });
123
+ const bg = await NativeLocationAuth.requestAlways();
124
+ if (bg.status === 'always') {
125
+ startTracking();
126
+ } else {
127
+ // User kept only whenInUse — start tracking anyway,
128
+ // they'll just need to keep the app open
129
+ startTracking({ foregroundOnly: true });
130
+ }
131
+ }
132
+ ```
133
+
134
+ ## Why This Plugin?
135
+
136
+ The official `@capacitor/geolocation` plugin reports permission as a generic `granted` / `denied` — you cannot tell whether the user has `whenInUse` or `always`. This is fine for apps that only need foreground location, but breaks for apps that need background tracking (delivery drivers, fitness apps, geofencing, etc.).
137
+
138
+ Other plugins:
139
+
140
+ | Plugin | Distinguishes whenInUse vs always? | Cost |
141
+ |---|---|---|
142
+ | `@capacitor/geolocation` | ❌ | Free |
143
+ | `@capacitor-community/background-geolocation` | ❌ | Free |
144
+ | `@capgo/background-geolocation` | ❌ | Free |
145
+ | `@transistorsoft/capacitor-background-geolocation` | ✅ | $300/year for Android production |
146
+ | **`@ajuarezso/capacitor-native-location-auth`** | ✅ | **Free** |
147
+
148
+ ## Platforms
149
+
150
+ - iOS 14+
151
+ - Android API 24+ (Android 7+)
152
+
153
+ ## License
154
+
155
+ MIT — see [LICENSE](./LICENSE).
@@ -0,0 +1,52 @@
1
+ ext {
2
+ androidxAppCompatVersion = project.hasProperty('androidxAppCompatVersion') ? rootProject.ext.androidxAppCompatVersion : '1.7.0'
3
+ }
4
+
5
+ buildscript {
6
+ repositories {
7
+ google()
8
+ mavenCentral()
9
+ }
10
+ dependencies {
11
+ classpath 'com.android.tools.build:gradle:8.7.2'
12
+ classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.25"
13
+ }
14
+ }
15
+
16
+ apply plugin: 'com.android.library'
17
+ apply plugin: 'kotlin-android'
18
+
19
+ android {
20
+ namespace "com.masterpedidos.nativelocationauth"
21
+ compileSdk project.hasProperty('compileSdkVersion') ? rootProject.ext.compileSdkVersion : 35
22
+ defaultConfig {
23
+ minSdkVersion project.hasProperty('minSdkVersion') ? rootProject.ext.minSdkVersion : 23
24
+ targetSdkVersion project.hasProperty('targetSdkVersion') ? rootProject.ext.targetSdkVersion : 35
25
+ }
26
+ buildTypes {
27
+ release {
28
+ minifyEnabled false
29
+ proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
30
+ }
31
+ }
32
+ lintOptions {
33
+ abortOnError false
34
+ }
35
+ compileOptions {
36
+ sourceCompatibility JavaVersion.VERSION_17
37
+ targetCompatibility JavaVersion.VERSION_17
38
+ }
39
+ kotlinOptions {
40
+ jvmTarget = "17"
41
+ }
42
+ }
43
+
44
+ repositories {
45
+ google()
46
+ mavenCentral()
47
+ }
48
+
49
+ dependencies {
50
+ implementation "androidx.appcompat:appcompat:$androidxAppCompatVersion"
51
+ implementation project(':capacitor-android')
52
+ }
@@ -0,0 +1,3 @@
1
+ <?xml version="1.0" encoding="utf-8"?>
2
+ <manifest xmlns:android="http://schemas.android.com/apk/res/android">
3
+ </manifest>
@@ -0,0 +1,133 @@
1
+ package com.masterpedidos.nativelocationauth
2
+
3
+ import android.Manifest
4
+ import android.content.pm.PackageManager
5
+ import android.os.Build
6
+ import androidx.core.content.ContextCompat
7
+ import com.getcapacitor.JSObject
8
+ import com.getcapacitor.PermissionState
9
+ import com.getcapacitor.Plugin
10
+ import com.getcapacitor.PluginCall
11
+ import com.getcapacitor.PluginMethod
12
+ import com.getcapacitor.annotation.CapacitorPlugin
13
+ import com.getcapacitor.annotation.Permission
14
+ import com.getcapacitor.annotation.PermissionCallback
15
+
16
+ @CapacitorPlugin(
17
+ name = "NativeLocationAuth",
18
+ permissions = [
19
+ Permission(
20
+ alias = "whenInUse",
21
+ strings = [Manifest.permission.ACCESS_FINE_LOCATION]
22
+ ),
23
+ Permission(
24
+ alias = "background",
25
+ strings = [Manifest.permission.ACCESS_BACKGROUND_LOCATION]
26
+ )
27
+ ]
28
+ )
29
+ class NativeLocationAuthPlugin : Plugin() {
30
+
31
+ /**
32
+ * Distingue los 4 niveles reales de autorización combinando los 2 permisos
33
+ * Android. Mapping a las strings que el TS espera (consistente con iOS):
34
+ * - ACCESS_FINE_LOCATION granted + ACCESS_BACKGROUND_LOCATION granted → "always"
35
+ * - ACCESS_FINE_LOCATION granted + ACCESS_BACKGROUND_LOCATION denied → "whenInUse"
36
+ * - ACCESS_FINE_LOCATION denied (rechazo permanente) → "denied"
37
+ * - ACCESS_FINE_LOCATION nunca preguntado (primera vez) → "notDetermined"
38
+ *
39
+ * Android < 10 (API 29) NO tiene ACCESS_BACKGROUND_LOCATION como permiso
40
+ * separado — si tiene FINE granted, automáticamente puede tracker en
41
+ * background, mapeado a "always" para compat con el flow.
42
+ */
43
+ @PluginMethod
44
+ fun getStatus(call: PluginCall) {
45
+ call.resolve(JSObject().put("status", currentStatusString()))
46
+ }
47
+
48
+ /**
49
+ * Pide ACCESS_FINE_LOCATION (foreground). Equivalente Android del
50
+ * requestWhenInUseAuthorization() de iOS. Si ya está granted, resuelve
51
+ * inmediatamente; si fue rechazado permanente ("no preguntar más"),
52
+ * retorna "denied" para que el frontend dirija al user a Settings.
53
+ */
54
+ @PluginMethod
55
+ fun requestWhenInUse(call: PluginCall) {
56
+ val fineGranted = ContextCompat.checkSelfPermission(
57
+ context,
58
+ Manifest.permission.ACCESS_FINE_LOCATION
59
+ ) == PackageManager.PERMISSION_GRANTED
60
+ if (fineGranted) {
61
+ call.resolve(JSObject().put("status", currentStatusString()))
62
+ return
63
+ }
64
+ requestPermissionForAlias("whenInUse", call, "whenInUsePermsCallback")
65
+ }
66
+
67
+ /**
68
+ * Pide ACCESS_BACKGROUND_LOCATION (Android 10+ / API 29+). Apple's
69
+ * requestAlwaysAuthorization() equivalente. Requiere que FINE ya esté
70
+ * granted antes (Android lo enforcea). En Android < 10 retorna "always"
71
+ * inmediato porque background no es permiso separado.
72
+ */
73
+ @PluginMethod
74
+ fun requestAlways(call: PluginCall) {
75
+ // Android < 10: FINE granted implica background tracking — no hay
76
+ // permiso de background separado.
77
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
78
+ call.resolve(JSObject().put("status", currentStatusString()))
79
+ return
80
+ }
81
+ val fineGranted = ContextCompat.checkSelfPermission(
82
+ context,
83
+ Manifest.permission.ACCESS_FINE_LOCATION
84
+ ) == PackageManager.PERMISSION_GRANTED
85
+ if (!fineGranted) {
86
+ // Android rechazaría el request de background si FINE no está granted
87
+ call.reject("Necesitas conceder primero el permiso de ubicación en uso")
88
+ return
89
+ }
90
+ val bgGranted = ContextCompat.checkSelfPermission(
91
+ context,
92
+ Manifest.permission.ACCESS_BACKGROUND_LOCATION
93
+ ) == PackageManager.PERMISSION_GRANTED
94
+ if (bgGranted) {
95
+ call.resolve(JSObject().put("status", "always"))
96
+ return
97
+ }
98
+ requestPermissionForAlias("background", call, "backgroundPermsCallback")
99
+ }
100
+
101
+ @PermissionCallback
102
+ private fun whenInUsePermsCallback(call: PluginCall) {
103
+ call.resolve(JSObject().put("status", currentStatusString()))
104
+ }
105
+
106
+ @PermissionCallback
107
+ private fun backgroundPermsCallback(call: PluginCall) {
108
+ call.resolve(JSObject().put("status", currentStatusString()))
109
+ }
110
+
111
+ private fun currentStatusString(): String {
112
+ val fineStatus = ContextCompat.checkSelfPermission(
113
+ context,
114
+ Manifest.permission.ACCESS_FINE_LOCATION
115
+ )
116
+ if (fineStatus != PackageManager.PERMISSION_GRANTED) {
117
+ // No podemos distinguir 100% entre "nunca preguntado" y "rechazado
118
+ // permanente" sin estado persistido. El frontend pedirá permiso y
119
+ // si falla mostrará toast "ve a Settings".
120
+ return "notDetermined"
121
+ }
122
+ // FINE granted. Background solo aplica en Android 10+.
123
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
124
+ val backgroundStatus = ContextCompat.checkSelfPermission(
125
+ context,
126
+ Manifest.permission.ACCESS_BACKGROUND_LOCATION
127
+ )
128
+ return if (backgroundStatus == PackageManager.PERMISSION_GRANTED) "always" else "whenInUse"
129
+ }
130
+ // Pre-Android 10: FINE granted implica background tracking permitido
131
+ return "always"
132
+ }
133
+ }
@@ -0,0 +1,44 @@
1
+ export type LocationAuthStatus = 'notDetermined' | 'whenInUse' | 'always' | 'denied';
2
+ export interface LocationAuthStatusResult {
3
+ status: LocationAuthStatus;
4
+ }
5
+ export interface NativeLocationAuthPlugin {
6
+ /**
7
+ * Retorna el nivel exacto de autorización de location.
8
+ * - `notDetermined`: nunca preguntado al user.
9
+ * - `whenInUse`: foreground autorizado, background NO.
10
+ * - `always`: foreground + background autorizados (Android: `ACCESS_BACKGROUND_LOCATION`; iOS: `authorizedAlways`).
11
+ * - `denied`: rechazado o restricted.
12
+ */
13
+ getStatus(): Promise<LocationAuthStatusResult>;
14
+ /**
15
+ * Solicita autorización solo de foreground ("Mientras la app esté en uso").
16
+ *
17
+ * - iOS: llama a `CLLocationManager.requestWhenInUseAuthorization()`. Muestra
18
+ * un dialog con SOLO opciones whenInUse/oneTime/deny, sin la opción "Always"
19
+ * mezclada (a diferencia de `Geolocation.requestPermissions()` de Capacitor
20
+ * que pide el nivel declarado en Info.plist con dialog combinado de 4 opciones).
21
+ * - Android: pide `ACCESS_FINE_LOCATION` con UI estándar.
22
+ *
23
+ * Si el estado ya es `whenInUse` o `always`, resuelve inmediato sin mostrar
24
+ * dialog. Si fue rechazado permanente, retorna `'denied'` y el frontend
25
+ * debe dirigir al user a Settings.
26
+ */
27
+ requestWhenInUse(): Promise<LocationAuthStatusResult>;
28
+ /**
29
+ * Solicita autorización de background ("Permitir siempre"). Llamar SOLO
30
+ * después de tener `whenInUse` autorizado.
31
+ *
32
+ * - iOS: llama a `CLLocationManager.requestAlwaysAuthorization()`. Si el
33
+ * estado es `whenInUse`, iOS muestra el dialog de upgrade (Always vs
34
+ * Keep WhenInUse). Si el user ya rechazó antes, NO se muestra dialog y
35
+ * retorna el estado actual — solo Settings puede cambiarlo.
36
+ * - Android 10+ (API 29+): pide `ACCESS_BACKGROUND_LOCATION` con dialog
37
+ * propio. El user puede elegir "Permitir todo el tiempo" o "Permitir
38
+ * solo mientras se usa la app".
39
+ * - Android < 10: resuelve inmediato con `always` porque background no es
40
+ * un permiso separado en API < 29.
41
+ */
42
+ requestAlways(): Promise<LocationAuthStatusResult>;
43
+ }
44
+ export declare const NativeLocationAuth: NativeLocationAuthPlugin;
package/dist/index.js ADDED
@@ -0,0 +1,2 @@
1
+ import { registerPlugin } from '@capacitor/core';
2
+ export const NativeLocationAuth = registerPlugin('NativeLocationAuth');
@@ -0,0 +1,113 @@
1
+ import Foundation
2
+ import Capacitor
3
+ import CoreLocation
4
+
5
+ @objc(NativeLocationAuthPlugin)
6
+ public class NativeLocationAuthPlugin: CAPPlugin, CAPBridgedPlugin, CLLocationManagerDelegate {
7
+ public let identifier = "NativeLocationAuthPlugin"
8
+ public let jsName = "NativeLocationAuth"
9
+ public let pluginMethods: [CAPPluginMethod] = [
10
+ CAPPluginMethod(name: "getStatus", returnType: CAPPluginReturnPromise),
11
+ CAPPluginMethod(name: "requestWhenInUse", returnType: CAPPluginReturnPromise),
12
+ CAPPluginMethod(name: "requestAlways", returnType: CAPPluginReturnPromise),
13
+ ]
14
+
15
+ private let locationManager = CLLocationManager()
16
+ private var pendingCall: CAPPluginCall?
17
+
18
+ override public func load() {
19
+ locationManager.delegate = self
20
+ }
21
+
22
+ /// Distingue los 4 niveles reales de `CLAuthorizationStatus` y los expone
23
+ /// al JS como strings consistentes con Android. Apple deprecó la API estática
24
+ /// `CLLocationManager.authorizationStatus()` en iOS 14 — usar la propiedad
25
+ /// de instancia.
26
+ @objc func getStatus(_ call: CAPPluginCall) {
27
+ call.resolve(["status": currentStatusString()])
28
+ }
29
+
30
+ /// Pide `WhenInUseAuthorization` específicamente. iOS muestra el dialog
31
+ /// SOLO con opciones "Mientras la app esté en uso" / "Solo esta vez" /
32
+ /// "No permitir" — sin la opción "Permitir siempre" mezclada (a diferencia
33
+ /// de `Geolocation.requestPermissions()` de Capacitor que pide el nivel
34
+ /// declarado en Info.plist y muestra el dialog combinado).
35
+ /// Si el estado actual ya es `whenInUse` o `always`, no muestra dialog y
36
+ /// resuelve inmediatamente con el estado actual.
37
+ @objc func requestWhenInUse(_ call: CAPPluginCall) {
38
+ if pendingCall != nil {
39
+ call.reject("Otra solicitud de autorización ya está en curso")
40
+ return
41
+ }
42
+ let currentStatus = currentAuthorizationStatus()
43
+ if currentStatus != .notDetermined {
44
+ // Ya tiene una decisión — no se muestra dialog, resolvemos directo
45
+ call.resolve(["status": currentStatusString()])
46
+ return
47
+ }
48
+ pendingCall = call
49
+ call.keepAlive = true
50
+ DispatchQueue.main.async { [weak self] in
51
+ self?.locationManager.requestWhenInUseAuthorization()
52
+ }
53
+ }
54
+
55
+ /// Pide `AlwaysAuthorization`. Si el estado actual es `whenInUse`, iOS
56
+ /// muestra el dialog para upgrade a "Permitir siempre" vs "Mantener
57
+ /// mientras esté en uso". Si está en `notDetermined`, iOS pide always
58
+ /// desde cero (sin pasar por whenInUse). Si ya es `always`, resuelve
59
+ /// inmediato.
60
+ @objc func requestAlways(_ call: CAPPluginCall) {
61
+ if pendingCall != nil {
62
+ call.reject("Otra solicitud de autorización ya está en curso")
63
+ return
64
+ }
65
+ let currentStatus = currentAuthorizationStatus()
66
+ if currentStatus == .authorizedAlways {
67
+ call.resolve(["status": "always"])
68
+ return
69
+ }
70
+ if currentStatus == .denied || currentStatus == .restricted {
71
+ // Apple no permite re-pedir si el user rechazó — solo Settings
72
+ call.resolve(["status": "denied"])
73
+ return
74
+ }
75
+ pendingCall = call
76
+ call.keepAlive = true
77
+ DispatchQueue.main.async { [weak self] in
78
+ self?.locationManager.requestAlwaysAuthorization()
79
+ }
80
+ }
81
+
82
+ /// Delegate callback — iOS lo dispara cuando el usuario responde un
83
+ /// dialog de autorización (o cuando cambia el permiso desde Settings).
84
+ /// Resolvemos el pendingCall con el nuevo estado.
85
+ public func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
86
+ guard let call = pendingCall else { return }
87
+ // Si todavía es notDetermined, iOS aún no terminó de procesar — esperamos
88
+ // al próximo callback. Eso pasa la primera vez que se llama con el
89
+ // dialog visible (el initial event antes del user input).
90
+ let status = currentAuthorizationStatus()
91
+ if status == .notDetermined { return }
92
+ pendingCall = nil
93
+ call.resolve(["status": currentStatusString()])
94
+ }
95
+
96
+ private func currentAuthorizationStatus() -> CLAuthorizationStatus {
97
+ if #available(iOS 14.0, *) {
98
+ return locationManager.authorizationStatus
99
+ } else {
100
+ return CLLocationManager.authorizationStatus()
101
+ }
102
+ }
103
+
104
+ private func currentStatusString() -> String {
105
+ switch currentAuthorizationStatus() {
106
+ case .notDetermined: return "notDetermined"
107
+ case .authorizedWhenInUse: return "whenInUse"
108
+ case .authorizedAlways: return "always"
109
+ case .denied, .restricted: return "denied"
110
+ @unknown default: return "denied"
111
+ }
112
+ }
113
+ }
package/package.json ADDED
@@ -0,0 +1,59 @@
1
+ {
2
+ "name": "@ajuarezso/capacitor-native-location-auth",
3
+ "version": "1.0.0",
4
+ "description": "Capacitor plugin to distinguish the 4 real levels of location authorization (notDetermined / whenInUse / always / denied) on iOS and Android. Fills the gap of @capacitor/geolocation which only reports generic granted/denied without distinguishing foreground vs background.",
5
+ "main": "dist/index.js",
6
+ "module": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "files": [
9
+ "dist/",
10
+ "ios/Sources/",
11
+ "android/src/",
12
+ "android/build.gradle",
13
+ "Package.swift",
14
+ "README.md",
15
+ "LICENSE"
16
+ ],
17
+ "author": "Anthony Juarez Solis <anthonyjuarezsolis@icloud.com>",
18
+ "license": "MIT",
19
+ "keywords": [
20
+ "capacitor",
21
+ "plugin",
22
+ "ios",
23
+ "android",
24
+ "location",
25
+ "geolocation",
26
+ "permissions",
27
+ "authorization",
28
+ "background-location",
29
+ "whenInUse",
30
+ "always",
31
+ "CLLocationManager"
32
+ ],
33
+ "repository": {
34
+ "type": "git",
35
+ "url": "https://github.com/anthonyjuarezsolis/capacitor-native-location-auth.git"
36
+ },
37
+ "bugs": {
38
+ "url": "https://github.com/anthonyjuarezsolis/capacitor-native-location-auth/issues"
39
+ },
40
+ "homepage": "https://github.com/anthonyjuarezsolis/capacitor-native-location-auth#readme",
41
+ "scripts": {
42
+ "build": "rm -rf dist && tsc"
43
+ },
44
+ "capacitor": {
45
+ "ios": {
46
+ "src": "ios"
47
+ },
48
+ "android": {
49
+ "src": "android"
50
+ }
51
+ },
52
+ "devDependencies": {
53
+ "@capacitor/core": "^8.0.0",
54
+ "typescript": "^5.4.0"
55
+ },
56
+ "peerDependencies": {
57
+ "@capacitor/core": "^7.0.0 || ^8.0.0"
58
+ }
59
+ }