@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 +21 -0
- package/Package.swift +30 -0
- package/README.md +155 -0
- package/android/build.gradle +52 -0
- package/android/src/main/AndroidManifest.xml +3 -0
- package/android/src/main/java/com/masterpedidos/nativelocationauth/NativeLocationAuthPlugin.kt +133 -0
- package/dist/index.d.ts +44 -0
- package/dist/index.js +2 -0
- package/ios/Sources/NativeLocationAuthPlugin/NativeLocationAuthPlugin.swift +113 -0
- package/package.json +59 -0
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
|
+
}
|
package/android/src/main/java/com/masterpedidos/nativelocationauth/NativeLocationAuthPlugin.kt
ADDED
|
@@ -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
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -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,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
|
+
}
|