@symbiote-native/cellular 0.1.0 → 0.3.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 +77 -31
- package/build/angular/services/permissions.service/index.d.ts +4 -0
- package/build/angular/services/permissions.service/index.js +19 -2
- package/build/react/hooks/use-permissions/index.d.ts +2 -1
- package/build/react/hooks/use-permissions/index.js +17 -4
- package/build/solid/index.d.ts +2 -0
- package/build/solid/index.js +7 -0
- package/build/solid/primitives/create-permissions.d.ts +8 -0
- package/build/solid/primitives/create-permissions.js +55 -0
- package/build/svelte/index.d.ts +2 -0
- package/build/svelte/index.js +6 -0
- package/build/svelte/runes/use-permissions.svelte.d.ts +7 -0
- package/build/svelte/runes/use-permissions.svelte.js +55 -0
- package/build/vue/composables/use-permissions/index.d.ts +1 -0
- package/build/vue/composables/use-permissions/index.js +12 -2
- package/build-ngc/angular/services/permissions.service/index.d.ts +4 -0
- package/build-ngc/angular/services/permissions.service/index.js +23 -5
- package/build-ngc/angular/services/permissions.service/index.js.map +1 -1
- package/build-ngc/core/cellular.js.map +1 -1
- package/build-ngc/core/native-module.js.map +1 -1
- package/package.json +42 -14
- package/src/angular/services/permissions.service/index.ts +22 -2
- package/src/core/cellular.ts +16 -4
- package/src/core/native-module.ts +3 -1
- package/src/react/hooks/use-permissions/index.ts +20 -4
- package/src/solid/index.ts +7 -0
- package/src/solid/primitives/create-permissions.ts +69 -0
- package/src/svelte/index.ts +6 -0
- package/src/svelte/runes/use-permissions.svelte.ts +69 -0
- package/src/svelte/svelte-ambient.d.ts +6 -0
- package/src/vue/composables/use-permissions/index.ts +13 -2
package/README.md
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
A wrapper package for [SymbioteNative](../../README.md) that makes
|
|
4
4
|
[`expo-cellular`](https://docs.expo.dev/versions/latest/sdk/cellular/) — cellular connection
|
|
5
|
-
generation, carrier/SIM info, and VoIP-support detection — reachable from **every** adapter
|
|
6
|
-
React, Vue, and Angular, not just React. Like [`@symbiote-native/sensors`](../sensors) and
|
|
5
|
+
generation, carrier/SIM info, and VoIP-support detection — reachable from **every** adapter:
|
|
6
|
+
React, Vue, Svelte, Solid, and Angular, not just React. Like [`@symbiote-native/sensors`](../sensors) and
|
|
7
7
|
unlike this repo's plain-RN-module/view wrappers ([`@symbiote-native/slider`](../slider),
|
|
8
8
|
[`@symbiote-native/splash-screen`](../splash-screen)), `expo-cellular` is built on
|
|
9
9
|
`expo-modules-core`, so its native code autolinks straight out of `node_modules` via
|
|
@@ -30,13 +30,13 @@ Unlike a plain RN native module, `expo-cellular`'s native code is discovered by
|
|
|
30
30
|
into the native host app **once**, covering this package and every other `expo-modules-core`
|
|
31
31
|
package with zero further changes:
|
|
32
32
|
|
|
33
|
-
| Platform | Touches
|
|
34
|
-
|
|
35
|
-
| iOS
|
|
36
|
-
| iOS
|
|
37
|
-
| Android
|
|
38
|
-
| Android
|
|
39
|
-
| Android
|
|
33
|
+
| Platform | Touches |
|
|
34
|
+
| -------- | --------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
35
|
+
| iOS | `ios/Podfile` — add `use_expo_modules!` |
|
|
36
|
+
| iOS | `AppDelegate.swift` — Expo's runtime-bootstrap hook |
|
|
37
|
+
| Android | `settings.gradle` / `app/build.gradle` — resolve and include the Expo Gradle projects |
|
|
38
|
+
| Android | `MainApplication.kt` — Expo's bootstrap hook, plus a hand-written native-module name map (there's no `expo` meta-package here to auto-generate one) |
|
|
39
|
+
| Android | `AndroidManifest.xml` — the `READ_PHONE_STATE` permission, needed to read carrier/SIM info |
|
|
40
40
|
|
|
41
41
|
Full mechanics — the Podfile pieces that normally ship inside the `expo` package, the `expo`
|
|
42
42
|
peer-dependency exclusion list, per-module permission strings — live in the
|
|
@@ -51,19 +51,26 @@ src/core/ cellular.ts (the free functions) + types.ts (CellularGene
|
|
|
51
51
|
expo-modules-core's requireNativeModule.
|
|
52
52
|
src/react/hooks/ @symbiote-native/cellular/react — usePermissions
|
|
53
53
|
src/vue/composables/ @symbiote-native/cellular/vue — usePermissions
|
|
54
|
+
src/svelte/runes/ @symbiote-native/cellular/svelte — usePermissions
|
|
55
|
+
src/solid/primitives/ @symbiote-native/cellular/solid — createPermissions (Solid says
|
|
56
|
+
create*, not use*)
|
|
54
57
|
src/angular/services/ @symbiote-native/cellular/angular — PermissionsService
|
|
55
58
|
```
|
|
56
59
|
|
|
57
60
|
Each adapter's `usePermissions` hook/composable/service is a thin lifecycle wrapper (fetch on
|
|
58
61
|
mount, expose `request`/`get` to re-check imperatively) over the same `core` functions — the
|
|
59
|
-
permission-fetch logic is written once and shared by
|
|
62
|
+
permission-fetch logic is written once and shared by every adapter.
|
|
60
63
|
|
|
61
64
|
## Use it
|
|
62
65
|
|
|
63
66
|
```tsx
|
|
64
67
|
// React
|
|
65
68
|
import { useEffect, useState } from 'react';
|
|
66
|
-
import {
|
|
69
|
+
import {
|
|
70
|
+
CellularGeneration,
|
|
71
|
+
getCarrierNameAsync,
|
|
72
|
+
getCellularGenerationAsync,
|
|
73
|
+
} from '@symbiote-native/cellular';
|
|
67
74
|
import { usePermissions } from '@symbiote-native/cellular/react';
|
|
68
75
|
|
|
69
76
|
function CellularScreen() {
|
|
@@ -72,15 +79,19 @@ function CellularScreen() {
|
|
|
72
79
|
const [status, requestPermission] = usePermissions();
|
|
73
80
|
|
|
74
81
|
useEffect(() => {
|
|
75
|
-
Promise.all([getCellularGenerationAsync(), getCarrierNameAsync()]).then(
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
82
|
+
Promise.all([getCellularGenerationAsync(), getCarrierNameAsync()]).then(
|
|
83
|
+
([gen, carrier]) => {
|
|
84
|
+
setGeneration(gen);
|
|
85
|
+
setCarrierName(carrier);
|
|
86
|
+
},
|
|
87
|
+
);
|
|
79
88
|
}, []);
|
|
80
89
|
|
|
81
90
|
return (
|
|
82
91
|
<>
|
|
83
|
-
<Text>
|
|
92
|
+
<Text>
|
|
93
|
+
{generation === null ? 'checking…' : CellularGeneration[generation]}
|
|
94
|
+
</Text>
|
|
84
95
|
<Text>{carrierName ?? 'checking…'}</Text>
|
|
85
96
|
<Text>{status === null ? 'checking…' : status.status}</Text>
|
|
86
97
|
<Button title="Request permission" onPress={() => requestPermission()} />
|
|
@@ -93,7 +104,11 @@ function CellularScreen() {
|
|
|
93
104
|
<!-- Vue -->
|
|
94
105
|
<script setup lang="ts">
|
|
95
106
|
import { onMounted, ref } from 'vue';
|
|
96
|
-
import {
|
|
107
|
+
import {
|
|
108
|
+
CellularGeneration,
|
|
109
|
+
getCarrierNameAsync,
|
|
110
|
+
getCellularGenerationAsync,
|
|
111
|
+
} from '@symbiote-native/cellular';
|
|
97
112
|
import { usePermissions } from '@symbiote-native/cellular/vue';
|
|
98
113
|
|
|
99
114
|
const generation = ref<CellularGeneration | null>(null);
|
|
@@ -101,14 +116,18 @@ const carrierName = ref<string | null>(null);
|
|
|
101
116
|
const { status, request: requestPermission } = usePermissions();
|
|
102
117
|
|
|
103
118
|
onMounted(() => {
|
|
104
|
-
Promise.all([getCellularGenerationAsync(), getCarrierNameAsync()]).then(
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
119
|
+
Promise.all([getCellularGenerationAsync(), getCarrierNameAsync()]).then(
|
|
120
|
+
([gen, carrier]) => {
|
|
121
|
+
generation.value = gen;
|
|
122
|
+
carrierName.value = carrier;
|
|
123
|
+
},
|
|
124
|
+
);
|
|
108
125
|
});
|
|
109
126
|
</script>
|
|
110
127
|
<template>
|
|
111
|
-
<Text>{{
|
|
128
|
+
<Text>{{
|
|
129
|
+
generation === null ? 'checking…' : CellularGeneration[generation]
|
|
130
|
+
}}</Text>
|
|
112
131
|
<Text>{{ carrierName ?? 'checking…' }}</Text>
|
|
113
132
|
<Text>{{ status === null ? 'checking…' : status.status }}</Text>
|
|
114
133
|
<Button title="Request permission" :onPress="() => requestPermission()" />
|
|
@@ -118,15 +137,25 @@ onMounted(() => {
|
|
|
118
137
|
```ts
|
|
119
138
|
// Angular
|
|
120
139
|
import { Component, inject, signal } from '@angular/core';
|
|
121
|
-
import {
|
|
140
|
+
import {
|
|
141
|
+
CellularGeneration,
|
|
142
|
+
PermissionsService,
|
|
143
|
+
getCarrierNameAsync,
|
|
144
|
+
getCellularGenerationAsync,
|
|
145
|
+
} from '@symbiote-native/cellular/angular';
|
|
122
146
|
|
|
123
147
|
@Component({
|
|
124
148
|
selector: 'CellularScreen',
|
|
125
149
|
template: `
|
|
126
|
-
<Text>{{
|
|
150
|
+
<Text>{{
|
|
151
|
+
generation() === null ? 'checking…' : CellularGeneration[generation()!]
|
|
152
|
+
}}</Text>
|
|
127
153
|
<Text>{{ carrierName() ?? 'checking…' }}</Text>
|
|
128
154
|
<Text>{{ permissionStatus()?.status ?? 'checking…' }}</Text>
|
|
129
|
-
<Button
|
|
155
|
+
<Button
|
|
156
|
+
title="Request permission"
|
|
157
|
+
(press)="permissionsService.request()"
|
|
158
|
+
></Button>
|
|
130
159
|
`,
|
|
131
160
|
})
|
|
132
161
|
export class CellularScreen {
|
|
@@ -138,10 +167,12 @@ export class CellularScreen {
|
|
|
138
167
|
readonly carrierName = signal<string | null>(null);
|
|
139
168
|
|
|
140
169
|
constructor() {
|
|
141
|
-
Promise.all([getCellularGenerationAsync(), getCarrierNameAsync()]).then(
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
170
|
+
Promise.all([getCellularGenerationAsync(), getCarrierNameAsync()]).then(
|
|
171
|
+
([generation, carrier]) => {
|
|
172
|
+
this.generation.set(generation);
|
|
173
|
+
this.carrierName.set(carrier);
|
|
174
|
+
},
|
|
175
|
+
);
|
|
145
176
|
}
|
|
146
177
|
}
|
|
147
178
|
```
|
|
@@ -191,11 +222,16 @@ Plus `CellularGeneration` (enum) and `PermissionResponse`/`PermissionStatus`/
|
|
|
191
222
|
ported by hand.
|
|
192
223
|
|
|
193
224
|
```ts
|
|
194
|
-
import {
|
|
225
|
+
import {
|
|
226
|
+
getCellularGenerationAsync,
|
|
227
|
+
getCarrierNameAsync,
|
|
228
|
+
} from '@symbiote-native/cellular';
|
|
195
229
|
|
|
196
230
|
// framework-scoped entry points re-export the same free functions, plus usePermissions:
|
|
197
231
|
import { usePermissions } from '@symbiote-native/cellular/react';
|
|
198
232
|
import { usePermissions } from '@symbiote-native/cellular/vue';
|
|
233
|
+
import { usePermissions } from '@symbiote-native/cellular/svelte';
|
|
234
|
+
import { createPermissions } from '@symbiote-native/cellular/solid';
|
|
199
235
|
import { PermissionsService } from '@symbiote-native/cellular/angular';
|
|
200
236
|
```
|
|
201
237
|
|
|
@@ -216,6 +252,16 @@ const [status, requestPermission, getPermission] = usePermissions();
|
|
|
216
252
|
const { status, request, get } = usePermissions();
|
|
217
253
|
```
|
|
218
254
|
|
|
255
|
+
```ts
|
|
256
|
+
// Svelte — the same object, boxed as getters: permissions.status
|
|
257
|
+
const permissions = usePermissions();
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
```ts
|
|
261
|
+
// Solid — createPermissions, and status/error are accessors: status()
|
|
262
|
+
const { status, request, get } = createPermissions();
|
|
263
|
+
```
|
|
264
|
+
|
|
219
265
|
```ts
|
|
220
266
|
// Angular — connect() returns a readonly Signal; request()/get() live on the service itself
|
|
221
267
|
readonly status = inject(PermissionsService).connect();
|
|
@@ -227,6 +273,6 @@ readonly status = inject(PermissionsService).connect();
|
|
|
227
273
|
No Fabric/Descriptor angle at all — cellular info is a pure async-function + permission surface,
|
|
228
274
|
never a view. Tests inject a fake native-module object in place of the real
|
|
229
275
|
`requireNativeModule` resolution (`src/core/cellular.test.ts`,
|
|
230
|
-
`src/{react,vue,angular}/**/*.test.{ts,tsx}`, run via `vitest`) — no `installFabric()`, no
|
|
276
|
+
`src/{react,vue,svelte,solid,angular}/**/*.test.{ts,tsx}`, run via `vitest`) — no `installFabric()`, no
|
|
231
277
|
ViewConfig. Native rendering itself is verified on-device (see the parent
|
|
232
278
|
[README](../../README.md)).
|
|
@@ -2,6 +2,10 @@ import { type Signal } from '@angular/core';
|
|
|
2
2
|
import { type PermissionResponse } from '../../../core';
|
|
3
3
|
export declare class PermissionsService {
|
|
4
4
|
private readonly status;
|
|
5
|
+
private readonly fetchError;
|
|
6
|
+
private isAutoFetchStarted;
|
|
7
|
+
/** Why the auto-fetch left `status` at null. Cleared by the next successful get()/request(). */
|
|
8
|
+
readonly error: Signal<Error | null>;
|
|
5
9
|
connect(): Signal<PermissionResponse | null>;
|
|
6
10
|
get(): Promise<PermissionResponse>;
|
|
7
11
|
request(): Promise<PermissionResponse>;
|
|
@@ -37,6 +37,11 @@ var __runInitializers = (this && this.__runInitializers) || function (thisArg, i
|
|
|
37
37
|
//
|
|
38
38
|
// readonly status = inject(PermissionsService).connect();
|
|
39
39
|
// // template: {{ status()?.granted }}
|
|
40
|
+
//
|
|
41
|
+
// The auto-fetch has nobody to reject to, so its failure lands in the `error` signal instead of
|
|
42
|
+
// escaping as an unhandled rejection: a null status with a non-null error is how a consumer tells
|
|
43
|
+
// "the fetch failed" from "not fetched yet". get()/request() still reject to their direct caller —
|
|
44
|
+
// only the automatic call is made safe.
|
|
40
45
|
import { Injectable, signal } from '@angular/core';
|
|
41
46
|
import { getPermissionsAsync, requestPermissionsAsync, } from '../../../core/index.js';
|
|
42
47
|
let PermissionsService = (() => {
|
|
@@ -54,20 +59,32 @@ let PermissionsService = (() => {
|
|
|
54
59
|
__runInitializers(_classThis, _classExtraInitializers);
|
|
55
60
|
}
|
|
56
61
|
status = signal(null);
|
|
62
|
+
fetchError = signal(null);
|
|
63
|
+
isAutoFetchStarted = false;
|
|
64
|
+
/** Why the auto-fetch left `status` at null. Cleared by the next successful get()/request(). */
|
|
65
|
+
error = this.fetchError.asReadonly();
|
|
57
66
|
connect() {
|
|
58
|
-
|
|
59
|
-
|
|
67
|
+
// Latched rather than guarded on `status() === null`: a failed fetch leaves status null
|
|
68
|
+
// forever, so the old guard turned every later connect() into another native call — unbounded
|
|
69
|
+
// once connect() is reached from a change-detected expression instead of a field initializer.
|
|
70
|
+
if (!this.isAutoFetchStarted) {
|
|
71
|
+
this.isAutoFetchStarted = true;
|
|
72
|
+
this.get().catch((cause) => {
|
|
73
|
+
this.fetchError.set(cause instanceof Error ? cause : new Error(String(cause)));
|
|
74
|
+
});
|
|
60
75
|
}
|
|
61
76
|
return this.status.asReadonly();
|
|
62
77
|
}
|
|
63
78
|
async get() {
|
|
64
79
|
const response = await getPermissionsAsync();
|
|
65
80
|
this.status.set(response);
|
|
81
|
+
this.fetchError.set(null);
|
|
66
82
|
return response;
|
|
67
83
|
}
|
|
68
84
|
async request() {
|
|
69
85
|
const response = await requestPermissionsAsync();
|
|
70
86
|
this.status.set(response);
|
|
87
|
+
this.fetchError.set(null);
|
|
71
88
|
return response;
|
|
72
89
|
}
|
|
73
90
|
};
|
|
@@ -1,28 +1,41 @@
|
|
|
1
1
|
// React lifecycle wiring over the framework-agnostic core (core/cellular.ts) — fetches the
|
|
2
2
|
// current permission status on mount, then exposes request/get to re-check imperatively.
|
|
3
|
+
//
|
|
4
|
+
// The mount fetch has nobody to reject to, so its failure lands in the 4th tuple slot (`error`)
|
|
5
|
+
// instead of escaping as an unhandled rejection: a null status with a non-null error is how a
|
|
6
|
+
// consumer tells "the fetch failed" from "not fetched yet". get()/request() still reject to their
|
|
7
|
+
// direct caller — only the automatic call is made safe.
|
|
3
8
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
4
9
|
import { getPermissionsAsync, requestPermissionsAsync, } from '../../../core/index.js';
|
|
5
10
|
export function usePermissions() {
|
|
6
11
|
const isMounted = useRef(true);
|
|
7
12
|
const [status, setStatus] = useState(null);
|
|
13
|
+
const [error, setError] = useState(null);
|
|
8
14
|
const getPermission = useCallback(async () => {
|
|
9
15
|
const response = await getPermissionsAsync();
|
|
10
|
-
if (isMounted.current)
|
|
16
|
+
if (isMounted.current) {
|
|
11
17
|
setStatus(response);
|
|
18
|
+
setError(null);
|
|
19
|
+
}
|
|
12
20
|
return response;
|
|
13
21
|
}, []);
|
|
14
22
|
const requestPermission = useCallback(async () => {
|
|
15
23
|
const response = await requestPermissionsAsync();
|
|
16
|
-
if (isMounted.current)
|
|
24
|
+
if (isMounted.current) {
|
|
17
25
|
setStatus(response);
|
|
26
|
+
setError(null);
|
|
27
|
+
}
|
|
18
28
|
return response;
|
|
19
29
|
}, []);
|
|
20
30
|
useEffect(() => {
|
|
21
31
|
isMounted.current = true;
|
|
22
|
-
|
|
32
|
+
getPermission().catch((cause) => {
|
|
33
|
+
if (isMounted.current)
|
|
34
|
+
setError(cause instanceof Error ? cause : new Error(String(cause)));
|
|
35
|
+
});
|
|
23
36
|
return () => {
|
|
24
37
|
isMounted.current = false;
|
|
25
38
|
};
|
|
26
39
|
}, [getPermission]);
|
|
27
|
-
return [status, requestPermission, getPermission];
|
|
40
|
+
return [status, requestPermission, getPermission, error];
|
|
28
41
|
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
// @symbiote-native/cellular/solid: the Solid entry over the framework-agnostic core, plus
|
|
2
|
+
// createPermissions — the only stateful surface this package needs a lifecycle wrapper for. The
|
|
3
|
+
// lifecycle bucket is `primitives/` and the name is `create*` (never `hooks/`/`use*`, that is
|
|
4
|
+
// React's term and Solid's word for consuming an existing thing) — see
|
|
5
|
+
// adapters/solid/src/primitives for the same convention.
|
|
6
|
+
export { createPermissions } from './primitives/create-permissions.js';
|
|
7
|
+
export * from '../core/index.js';
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { type Accessor } from 'solid-js';
|
|
2
|
+
import { type PermissionResponse } from '../../core';
|
|
3
|
+
export declare function createPermissions(): {
|
|
4
|
+
status: Accessor<PermissionResponse | null>;
|
|
5
|
+
error: Accessor<Error | null>;
|
|
6
|
+
request: () => Promise<PermissionResponse>;
|
|
7
|
+
get: () => Promise<PermissionResponse>;
|
|
8
|
+
};
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
// Solid lifecycle wiring over the framework-agnostic core (core/cellular.ts) — the Solid twin of
|
|
2
|
+
// React's usePermissions hook, Vue's composable, Svelte's rune and Angular's PermissionsService.
|
|
3
|
+
//
|
|
4
|
+
// `primitives/` and `create*`, not `hooks/`/`use*`: Solid's ecosystem calls a composable reactive
|
|
5
|
+
// function a PRIMITIVE and reserves `use*` for consuming something that already exists. Full
|
|
6
|
+
// rationale in adapters/solid/src/primitives/create-color-scheme.ts's header.
|
|
7
|
+
//
|
|
8
|
+
// Returns ACCESSORS, never a snapshot: a Solid component body runs ONCE, so a plain returned value
|
|
9
|
+
// would freeze at "not fetched yet" and never move again.
|
|
10
|
+
//
|
|
11
|
+
// The initial fetch is fired SYNCHRONOUSLY from the primitive body rather than from a mount hook
|
|
12
|
+
// (React's useEffect, Vue's onMounted, Svelte's $effect) — same reason create-color-scheme
|
|
13
|
+
// subscribes synchronously: nothing can interleave between construction and the fetch starting, so
|
|
14
|
+
// there is no window in which a consumer sees a primitive that has not asked for status yet.
|
|
15
|
+
//
|
|
16
|
+
// That fetch has nobody to reject to, so its failure lands in `error` instead of escaping as an
|
|
17
|
+
// unhandled rejection: a null status with a non-null error is how a consumer tells "the fetch
|
|
18
|
+
// failed" from "not fetched yet". get()/request() still reject to their direct caller — only the
|
|
19
|
+
// automatic call is made safe.
|
|
20
|
+
//
|
|
21
|
+
// `onCleanup` drops the writes of a call still in flight when the owner is disposed — the Solid
|
|
22
|
+
// equivalent of React's isMounted ref. Outside a component / createRoot there is no owner to hang
|
|
23
|
+
// it on, so those late writes land instead (harmless: nothing reads a disposed signal).
|
|
24
|
+
import { createSignal, onCleanup } from 'solid-js';
|
|
25
|
+
import { getPermissionsAsync, requestPermissionsAsync, } from '../../core/index.js';
|
|
26
|
+
export function createPermissions() {
|
|
27
|
+
const [status, setStatus] = createSignal(null);
|
|
28
|
+
const [error, setError] = createSignal(null);
|
|
29
|
+
let isDisposed = false;
|
|
30
|
+
const record = (response) => {
|
|
31
|
+
if (isDisposed)
|
|
32
|
+
return;
|
|
33
|
+
setStatus(response);
|
|
34
|
+
setError(null);
|
|
35
|
+
};
|
|
36
|
+
const get = async () => {
|
|
37
|
+
const response = await getPermissionsAsync();
|
|
38
|
+
record(response);
|
|
39
|
+
return response;
|
|
40
|
+
};
|
|
41
|
+
const request = async () => {
|
|
42
|
+
const response = await requestPermissionsAsync();
|
|
43
|
+
record(response);
|
|
44
|
+
return response;
|
|
45
|
+
};
|
|
46
|
+
get().catch((cause) => {
|
|
47
|
+
if (isDisposed)
|
|
48
|
+
return;
|
|
49
|
+
setError(cause instanceof Error ? cause : new Error(String(cause)));
|
|
50
|
+
});
|
|
51
|
+
onCleanup(() => {
|
|
52
|
+
isDisposed = true;
|
|
53
|
+
});
|
|
54
|
+
return { status, error, request, get };
|
|
55
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
// @symbiote-native/cellular/svelte: the Svelte entry over the framework-agnostic core, plus
|
|
2
|
+
// usePermissions — the only stateful surface this package needs a lifecycle wrapper for. The
|
|
3
|
+
// lifecycle bucket is `runes/` (never `hooks/`/`composables/`, those are React's and Vue's terms)
|
|
4
|
+
// — see adapters/svelte/src/runes for the same convention.
|
|
5
|
+
export { usePermissions } from './runes/use-permissions.svelte';
|
|
6
|
+
export * from '../core/index.js';
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { type PermissionResponse } from '../../core';
|
|
2
|
+
export declare function usePermissions(): {
|
|
3
|
+
readonly status: PermissionResponse | null;
|
|
4
|
+
readonly error: Error | null;
|
|
5
|
+
request: () => Promise<PermissionResponse>;
|
|
6
|
+
get: () => Promise<PermissionResponse>;
|
|
7
|
+
};
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
// Svelte lifecycle wiring over the framework-agnostic core (core/cellular.ts) — fetches the
|
|
2
|
+
// current permission status on mount, then exposes request/get to re-check imperatively. The
|
|
3
|
+
// Svelte twin of vue/composables/use-permissions and react/hooks/use-permissions.
|
|
4
|
+
//
|
|
5
|
+
// `.svelte.ts` (not `.ts`): runes ($state/$effect) are only usable in a file with this extension
|
|
6
|
+
// outside an actual `.svelte` component. `runes/` is Svelte's own term for the bucket React calls
|
|
7
|
+
// `hooks/` and Vue calls `composables/` — see adapters/svelte/src/runes.
|
|
8
|
+
//
|
|
9
|
+
// Vue's onMounted collapses into a `$effect` with no teardown (there is no ongoing subscription
|
|
10
|
+
// here, just a one-shot get). The effect only WRITES `status` (through `get()`), never reads it,
|
|
11
|
+
// so its dependency set stays empty and it fires exactly once on mount.
|
|
12
|
+
//
|
|
13
|
+
// `status` comes back as a GETTER on the returned object rather than a raw `$state` variable:
|
|
14
|
+
// Svelte 5 reactivity is lexically scoped to the declaring module, so a raw `let x = $state(...)`
|
|
15
|
+
// handed out of a plain function arrives dead at the caller. Reading `.status` is the Svelte
|
|
16
|
+
// equivalent of unwrapping Vue's `Ref` via `.value`.
|
|
17
|
+
//
|
|
18
|
+
// The mount fetch has nobody to reject to, so its failure lands in `error` (a second boxed getter,
|
|
19
|
+
// same reason as `status`) instead of escaping as an unhandled rejection: a null status with a
|
|
20
|
+
// non-null error is how a consumer tells "the fetch failed" from "not fetched yet".
|
|
21
|
+
// get()/request() still reject to their direct caller — only the automatic call is made safe.
|
|
22
|
+
import { getPermissionsAsync, requestPermissionsAsync, } from '../../core/index.js';
|
|
23
|
+
export function usePermissions() {
|
|
24
|
+
let status = $state(null);
|
|
25
|
+
let error = $state(null);
|
|
26
|
+
const get = async () => {
|
|
27
|
+
const response = await getPermissionsAsync();
|
|
28
|
+
status = response;
|
|
29
|
+
error = null;
|
|
30
|
+
return response;
|
|
31
|
+
};
|
|
32
|
+
const request = async () => {
|
|
33
|
+
const response = await requestPermissionsAsync();
|
|
34
|
+
status = response;
|
|
35
|
+
error = null;
|
|
36
|
+
return response;
|
|
37
|
+
};
|
|
38
|
+
$effect(() => {
|
|
39
|
+
// The catch runs after the effect has settled, so writing `error` there adds no dependency and
|
|
40
|
+
// the effect still fires exactly once.
|
|
41
|
+
get().catch((cause) => {
|
|
42
|
+
error = cause instanceof Error ? cause : new Error(String(cause));
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
return {
|
|
46
|
+
get status() {
|
|
47
|
+
return status;
|
|
48
|
+
},
|
|
49
|
+
get error() {
|
|
50
|
+
return error;
|
|
51
|
+
},
|
|
52
|
+
request,
|
|
53
|
+
get,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
@@ -2,6 +2,7 @@ import { type Ref } from '@vue/runtime-core';
|
|
|
2
2
|
import { type PermissionResponse } from '../../../core';
|
|
3
3
|
export declare function usePermissions(): {
|
|
4
4
|
status: Ref<PermissionResponse | null>;
|
|
5
|
+
error: Ref<Error | null>;
|
|
5
6
|
request: () => Promise<PermissionResponse>;
|
|
6
7
|
get: () => Promise<PermissionResponse>;
|
|
7
8
|
};
|
|
@@ -1,21 +1,31 @@
|
|
|
1
1
|
// Vue lifecycle wiring over the framework-agnostic core (core/cellular.ts) — fetches the
|
|
2
2
|
// current permission status on mount, then exposes request/get to re-check imperatively.
|
|
3
|
+
//
|
|
4
|
+
// The mount fetch has nobody to reject to, so its failure lands in the `error` ref instead of
|
|
5
|
+
// escaping as an unhandled rejection: a null status with a non-null error is how a consumer tells
|
|
6
|
+
// "the fetch failed" from "not fetched yet". get()/request() still reject to their direct caller —
|
|
7
|
+
// only the automatic call is made safe.
|
|
3
8
|
import { onMounted, ref } from '@vue/runtime-core';
|
|
4
9
|
import { getPermissionsAsync, requestPermissionsAsync, } from '../../../core/index.js';
|
|
5
10
|
export function usePermissions() {
|
|
6
11
|
const status = ref(null);
|
|
12
|
+
const error = ref(null);
|
|
7
13
|
const get = async () => {
|
|
8
14
|
const response = await getPermissionsAsync();
|
|
9
15
|
status.value = response;
|
|
16
|
+
error.value = null;
|
|
10
17
|
return response;
|
|
11
18
|
};
|
|
12
19
|
const request = async () => {
|
|
13
20
|
const response = await requestPermissionsAsync();
|
|
14
21
|
status.value = response;
|
|
22
|
+
error.value = null;
|
|
15
23
|
return response;
|
|
16
24
|
};
|
|
17
25
|
onMounted(() => {
|
|
18
|
-
|
|
26
|
+
get().catch((cause) => {
|
|
27
|
+
error.value = cause instanceof Error ? cause : new Error(String(cause));
|
|
28
|
+
});
|
|
19
29
|
});
|
|
20
|
-
return { status, request, get };
|
|
30
|
+
return { status, error, request, get };
|
|
21
31
|
}
|
|
@@ -3,6 +3,10 @@ import { type PermissionResponse } from '../../../core';
|
|
|
3
3
|
import * as i0 from "@angular/core";
|
|
4
4
|
export declare class PermissionsService {
|
|
5
5
|
private readonly status;
|
|
6
|
+
private readonly fetchError;
|
|
7
|
+
private isAutoFetchStarted;
|
|
8
|
+
/** Why the auto-fetch left `status` at null. Cleared by the next successful get()/request(). */
|
|
9
|
+
readonly error: Signal<Error | null>;
|
|
6
10
|
connect(): Signal<PermissionResponse | null>;
|
|
7
11
|
get(): Promise<PermissionResponse>;
|
|
8
12
|
request(): Promise<PermissionResponse>;
|
|
@@ -3,32 +3,50 @@
|
|
|
3
3
|
//
|
|
4
4
|
// readonly status = inject(PermissionsService).connect();
|
|
5
5
|
// // template: {{ status()?.granted }}
|
|
6
|
+
//
|
|
7
|
+
// The auto-fetch has nobody to reject to, so its failure lands in the `error` signal instead of
|
|
8
|
+
// escaping as an unhandled rejection: a null status with a non-null error is how a consumer tells
|
|
9
|
+
// "the fetch failed" from "not fetched yet". get()/request() still reject to their direct caller —
|
|
10
|
+
// only the automatic call is made safe.
|
|
6
11
|
import { Injectable, signal } from '@angular/core';
|
|
7
12
|
import { getPermissionsAsync, requestPermissionsAsync, } from '../../../core';
|
|
8
13
|
import * as i0 from "@angular/core";
|
|
9
14
|
export class PermissionsService {
|
|
10
15
|
status = signal(null, /* @ts-ignore */
|
|
11
16
|
...(ngDevMode ? [{ debugName: "status" }] : /* istanbul ignore next */ []));
|
|
17
|
+
fetchError = signal(null, /* @ts-ignore */
|
|
18
|
+
...(ngDevMode ? [{ debugName: "fetchError" }] : /* istanbul ignore next */ []));
|
|
19
|
+
isAutoFetchStarted = false;
|
|
20
|
+
/** Why the auto-fetch left `status` at null. Cleared by the next successful get()/request(). */
|
|
21
|
+
error = this.fetchError.asReadonly();
|
|
12
22
|
connect() {
|
|
13
|
-
|
|
14
|
-
|
|
23
|
+
// Latched rather than guarded on `status() === null`: a failed fetch leaves status null
|
|
24
|
+
// forever, so the old guard turned every later connect() into another native call — unbounded
|
|
25
|
+
// once connect() is reached from a change-detected expression instead of a field initializer.
|
|
26
|
+
if (!this.isAutoFetchStarted) {
|
|
27
|
+
this.isAutoFetchStarted = true;
|
|
28
|
+
this.get().catch((cause) => {
|
|
29
|
+
this.fetchError.set(cause instanceof Error ? cause : new Error(String(cause)));
|
|
30
|
+
});
|
|
15
31
|
}
|
|
16
32
|
return this.status.asReadonly();
|
|
17
33
|
}
|
|
18
34
|
async get() {
|
|
19
35
|
const response = await getPermissionsAsync();
|
|
20
36
|
this.status.set(response);
|
|
37
|
+
this.fetchError.set(null);
|
|
21
38
|
return response;
|
|
22
39
|
}
|
|
23
40
|
async request() {
|
|
24
41
|
const response = await requestPermissionsAsync();
|
|
25
42
|
this.status.set(response);
|
|
43
|
+
this.fetchError.set(null);
|
|
26
44
|
return response;
|
|
27
45
|
}
|
|
28
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.
|
|
29
|
-
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.
|
|
46
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: PermissionsService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
47
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: PermissionsService, providedIn: 'root' });
|
|
30
48
|
}
|
|
31
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.
|
|
49
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: PermissionsService, decorators: [{
|
|
32
50
|
type: Injectable,
|
|
33
51
|
args: [{ providedIn: 'root' }]
|
|
34
52
|
}] });
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../src/angular/services/permissions.service/index.ts"],"names":[],"mappings":"AAAA,wFAAwF;AACxF,uEAAuE;AACvE,EAAE;AACF,4DAA4D;AAC5D,yCAAyC;AACzC,OAAO,EAAE,UAAU,EAAE,MAAM,EAAe,MAAM,eAAe,CAAC;AAChE,OAAO,EACL,mBAAmB,EACnB,uBAAuB,GAExB,MAAM,eAAe,CAAC;;AAGvB,MAAM,OAAO,kBAAkB;IACZ,MAAM,GAAG,MAAM,CAA4B,IAAI;+EAAC,CAAC;
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../src/angular/services/permissions.service/index.ts"],"names":[],"mappings":"AAAA,wFAAwF;AACxF,uEAAuE;AACvE,EAAE;AACF,4DAA4D;AAC5D,yCAAyC;AACzC,EAAE;AACF,gGAAgG;AAChG,kGAAkG;AAClG,mGAAmG;AACnG,wCAAwC;AACxC,OAAO,EAAE,UAAU,EAAE,MAAM,EAAe,MAAM,eAAe,CAAC;AAChE,OAAO,EACL,mBAAmB,EACnB,uBAAuB,GAExB,MAAM,eAAe,CAAC;;AAGvB,MAAM,OAAO,kBAAkB;IACZ,MAAM,GAAG,MAAM,CAA4B,IAAI;+EAAC,CAAC;IACjD,UAAU,GAAG,MAAM,CAAe,IAAI;mFAAC,CAAC;IACjD,kBAAkB,GAAG,KAAK,CAAC;IAEnC,gGAAgG;IACvF,KAAK,GAAyB,IAAI,CAAC,UAAU,CAAC,UAAU,EAAE,CAAC;IAEpE,OAAO;QACL,wFAAwF;QACxF,8FAA8F;QAC9F,8FAA8F;QAC9F,IAAI,CAAC,IAAI,CAAC,kBAAkB,EAAE,CAAC;YAC7B,IAAI,CAAC,kBAAkB,GAAG,IAAI,CAAC;YAC/B,IAAI,CAAC,GAAG,EAAE,CAAC,KAAK,CAAC,CAAC,KAAc,EAAE,EAAE;gBAClC,IAAI,CAAC,UAAU,CAAC,GAAG,CACjB,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,KAAK,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAC1D,CAAC;YACJ,CAAC,CAAC,CAAC;QACL,CAAC;QACD,OAAO,IAAI,CAAC,MAAM,CAAC,UAAU,EAAE,CAAC;IAClC,CAAC;IAED,KAAK,CAAC,GAAG;QACP,MAAM,QAAQ,GAAG,MAAM,mBAAmB,EAAE,CAAC;QAC7C,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;QAC1B,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QAC1B,OAAO,QAAQ,CAAC;IAClB,CAAC;IAED,KAAK,CAAC,OAAO;QACX,MAAM,QAAQ,GAAG,MAAM,uBAAuB,EAAE,CAAC;QACjD,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;QAC1B,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QAC1B,OAAO,QAAQ,CAAC;IAClB,CAAC;uGAnCU,kBAAkB;2GAAlB,kBAAkB,cADL,MAAM;;2FACnB,kBAAkB;kBAD9B,UAAU;mBAAC,EAAE,UAAU,EAAE,MAAM,EAAE"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cellular.js","sourceRoot":"","sources":["../../src/core/cellular.ts"],"names":[],"mappings":"AAAA,8FAA8F;AAC9F,iGAAiG;AACjG,iGAAiG;AACjG,4FAA4F;AAC5F,gGAAgG;AAChG,gGAAgG;AAChG,2FAA2F;AAC3F,OAAO,EACL,QAAQ,EACR,gBAAgB,EAChB,mBAAmB,GAEpB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAC/C,OAAO,EAAE,kBAAkB,EAAE,MAAM,SAAS,CAAC;AAE7C,MAAM,kBAAkB,GAAG,eAAe,CAAC;AAE3C,MAAM,2BAA2B,GAAuB;IACtD,MAAM,EAAE,gBAAgB,CAAC,OAAO;IAChC,OAAO,EAAE,OAAO;IAChB,OAAO,EAAE,IAAI;IACb,WAAW,EAAE,IAAI;CAClB,CAAC;AAEF,8DAA8D;AAC9D,MAAM,CAAC,KAAK,UAAU,0BAA0B;IAC9C,IAAI,CAAC,YAAY,CAAC,0BAA0B,EAAE,CAAC;QAC7C,MAAM,IAAI,mBAAmB,
|
|
1
|
+
{"version":3,"file":"cellular.js","sourceRoot":"","sources":["../../src/core/cellular.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"native-module.js","sourceRoot":"","sources":["../../src/core/native-module.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AAIxD,MAAM,yBAAyB,GAAG,cAAc,CAAC;AAmBjD,MAAM,CAAC,MAAM,YAAY,GAAG,mBAAmB,
|
|
1
|
+
{"version":3,"file":"native-module.js","sourceRoot":"","sources":["../../src/core/native-module.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AAIxD,MAAM,yBAAyB,GAAG,cAAc,CAAC;AAmBjD,MAAM,CAAC,MAAM,YAAY,GAAG,mBAAmB,CAC7C,yBAAyB,CAC1B,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@symbiote-native/cellular",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "expo-cellular wrapped for SymbioteNative — one framework-agnostic core, built once and reachable from the React, Vue, and Angular adapters. Cellular generation (2G/3G/4G/5G), carrier/SIM info on Android (ISO country code, carrier name, MCC/MNC), VoIP-support detection, and the permission get/request pair gating carrier info on Android.",
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"description": "expo-cellular wrapped for SymbioteNative — one framework-agnostic core, built once and reachable from the React, Vue, Svelte, Solid, and Angular adapters. Cellular generation (2G/3G/4G/5G), carrier/SIM info on Android (ISO country code, carrier name, MCC/MNC), VoIP-support detection, and the permission get/request pair gating carrier info on Android.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
@@ -30,6 +30,14 @@
|
|
|
30
30
|
"types": "./build/react/index.d.ts",
|
|
31
31
|
"default": "./build/react/index.js"
|
|
32
32
|
},
|
|
33
|
+
"./svelte": {
|
|
34
|
+
"types": "./build/svelte/index.d.ts",
|
|
35
|
+
"default": "./build/svelte/index.js"
|
|
36
|
+
},
|
|
37
|
+
"./solid": {
|
|
38
|
+
"types": "./build/solid/index.d.ts",
|
|
39
|
+
"default": "./build/solid/index.js"
|
|
40
|
+
},
|
|
33
41
|
"./angular": {
|
|
34
42
|
"types": "./build-ngc/angular/index.d.ts",
|
|
35
43
|
"react-native": "./build-ngc/angular/index.js",
|
|
@@ -53,15 +61,19 @@
|
|
|
53
61
|
"expo-modules-core": "57.0.5"
|
|
54
62
|
},
|
|
55
63
|
"peerDependencies": {
|
|
64
|
+
"@symbiote-native/angular": ">=0.6.2",
|
|
56
65
|
"@symbiote-native/engine": ">=0.1.7",
|
|
66
|
+
"@symbiote-native/react": ">=0.2.8",
|
|
67
|
+
"@symbiote-native/solid": ">=0.0.1",
|
|
68
|
+
"@symbiote-native/svelte": ">=0.1.0",
|
|
69
|
+
"@symbiote-native/vue": ">=0.3.8",
|
|
57
70
|
"@angular/core": ">=20",
|
|
58
71
|
"@vue/runtime-core": "^3.5.13",
|
|
59
72
|
"react": ">=19.0.0",
|
|
60
73
|
"react-native": ">=0.86",
|
|
61
|
-
"
|
|
62
|
-
"
|
|
63
|
-
"
|
|
64
|
-
"@symbiote-native/vue": "0.3.8"
|
|
74
|
+
"solid-js": ">=1.9.0",
|
|
75
|
+
"svelte": ">=5.56.0",
|
|
76
|
+
"vue": ">=3.5.0"
|
|
65
77
|
},
|
|
66
78
|
"peerDependenciesMeta": {
|
|
67
79
|
"@symbiote-native/angular": {
|
|
@@ -70,6 +82,12 @@
|
|
|
70
82
|
"@symbiote-native/react": {
|
|
71
83
|
"optional": true
|
|
72
84
|
},
|
|
85
|
+
"@symbiote-native/solid": {
|
|
86
|
+
"optional": true
|
|
87
|
+
},
|
|
88
|
+
"@symbiote-native/svelte": {
|
|
89
|
+
"optional": true
|
|
90
|
+
},
|
|
73
91
|
"@symbiote-native/vue": {
|
|
74
92
|
"optional": true
|
|
75
93
|
},
|
|
@@ -82,24 +100,34 @@
|
|
|
82
100
|
"react": {
|
|
83
101
|
"optional": true
|
|
84
102
|
},
|
|
103
|
+
"solid-js": {
|
|
104
|
+
"optional": true
|
|
105
|
+
},
|
|
106
|
+
"svelte": {
|
|
107
|
+
"optional": true
|
|
108
|
+
},
|
|
85
109
|
"vue": {
|
|
86
110
|
"optional": true
|
|
87
111
|
}
|
|
88
112
|
},
|
|
89
113
|
"devDependencies": {
|
|
90
|
-
"@angular/compiler": "
|
|
91
|
-
"@angular/compiler-cli": "
|
|
92
|
-
"@angular/core": "
|
|
114
|
+
"@angular/compiler": "~22.0.8",
|
|
115
|
+
"@angular/compiler-cli": "~22.0.8",
|
|
116
|
+
"@angular/core": "~22.0.8",
|
|
93
117
|
"@types/node": "^26.0.0",
|
|
94
118
|
"@types/react": "^19.2.0",
|
|
95
119
|
"@vue/runtime-core": "^3.5.13",
|
|
96
120
|
"react": "19.2.3",
|
|
121
|
+
"solid-js": "^1.9.14",
|
|
122
|
+
"svelte": "^5.56.0",
|
|
97
123
|
"typescript": "~6.0.0",
|
|
98
|
-
"@symbiote-native/angular": "0.
|
|
99
|
-
"@symbiote-native/engine": "0.
|
|
100
|
-
"@symbiote-native/react": "0.
|
|
101
|
-
"@symbiote-native/
|
|
102
|
-
"@symbiote-native/
|
|
124
|
+
"@symbiote-native/angular": "0.8.0",
|
|
125
|
+
"@symbiote-native/engine": "0.3.0",
|
|
126
|
+
"@symbiote-native/react": "0.4.0",
|
|
127
|
+
"@symbiote-native/solid": "0.1.0",
|
|
128
|
+
"@symbiote-native/svelte": "0.3.0",
|
|
129
|
+
"@symbiote-native/test-utils": "0.2.0",
|
|
130
|
+
"@symbiote-native/vue": "0.5.0"
|
|
103
131
|
},
|
|
104
132
|
"scripts": {
|
|
105
133
|
"typecheck": "tsc --build",
|
|
@@ -3,6 +3,11 @@
|
|
|
3
3
|
//
|
|
4
4
|
// readonly status = inject(PermissionsService).connect();
|
|
5
5
|
// // template: {{ status()?.granted }}
|
|
6
|
+
//
|
|
7
|
+
// The auto-fetch has nobody to reject to, so its failure lands in the `error` signal instead of
|
|
8
|
+
// escaping as an unhandled rejection: a null status with a non-null error is how a consumer tells
|
|
9
|
+
// "the fetch failed" from "not fetched yet". get()/request() still reject to their direct caller —
|
|
10
|
+
// only the automatic call is made safe.
|
|
6
11
|
import { Injectable, signal, type Signal } from '@angular/core';
|
|
7
12
|
import {
|
|
8
13
|
getPermissionsAsync,
|
|
@@ -13,10 +18,23 @@ import {
|
|
|
13
18
|
@Injectable({ providedIn: 'root' })
|
|
14
19
|
export class PermissionsService {
|
|
15
20
|
private readonly status = signal<PermissionResponse | null>(null);
|
|
21
|
+
private readonly fetchError = signal<Error | null>(null);
|
|
22
|
+
private isAutoFetchStarted = false;
|
|
23
|
+
|
|
24
|
+
/** Why the auto-fetch left `status` at null. Cleared by the next successful get()/request(). */
|
|
25
|
+
readonly error: Signal<Error | null> = this.fetchError.asReadonly();
|
|
16
26
|
|
|
17
27
|
connect(): Signal<PermissionResponse | null> {
|
|
18
|
-
|
|
19
|
-
|
|
28
|
+
// Latched rather than guarded on `status() === null`: a failed fetch leaves status null
|
|
29
|
+
// forever, so the old guard turned every later connect() into another native call — unbounded
|
|
30
|
+
// once connect() is reached from a change-detected expression instead of a field initializer.
|
|
31
|
+
if (!this.isAutoFetchStarted) {
|
|
32
|
+
this.isAutoFetchStarted = true;
|
|
33
|
+
this.get().catch((cause: unknown) => {
|
|
34
|
+
this.fetchError.set(
|
|
35
|
+
cause instanceof Error ? cause : new Error(String(cause)),
|
|
36
|
+
);
|
|
37
|
+
});
|
|
20
38
|
}
|
|
21
39
|
return this.status.asReadonly();
|
|
22
40
|
}
|
|
@@ -24,12 +42,14 @@ export class PermissionsService {
|
|
|
24
42
|
async get(): Promise<PermissionResponse> {
|
|
25
43
|
const response = await getPermissionsAsync();
|
|
26
44
|
this.status.set(response);
|
|
45
|
+
this.fetchError.set(null);
|
|
27
46
|
return response;
|
|
28
47
|
}
|
|
29
48
|
|
|
30
49
|
async request(): Promise<PermissionResponse> {
|
|
31
50
|
const response = await requestPermissionsAsync();
|
|
32
51
|
this.status.set(response);
|
|
52
|
+
this.fetchError.set(null);
|
|
33
53
|
return response;
|
|
34
54
|
}
|
|
35
55
|
}
|
package/src/core/cellular.ts
CHANGED
|
@@ -26,7 +26,10 @@ const GRANTED_PERMISSION_RESPONSE: PermissionResponse = {
|
|
|
26
26
|
/** Gets the generation of the device's current connection. */
|
|
27
27
|
export async function getCellularGenerationAsync(): Promise<CellularGeneration> {
|
|
28
28
|
if (!expoCellular.getCellularGenerationAsync) {
|
|
29
|
-
throw new UnavailabilityError(
|
|
29
|
+
throw new UnavailabilityError(
|
|
30
|
+
NATIVE_MODULE_NAME,
|
|
31
|
+
'getCellularGenerationAsync',
|
|
32
|
+
);
|
|
30
33
|
}
|
|
31
34
|
return expoCellular.getCellularGenerationAsync();
|
|
32
35
|
}
|
|
@@ -83,7 +86,10 @@ export async function getMobileCountryCodeAsync(): Promise<string | null> {
|
|
|
83
86
|
return null;
|
|
84
87
|
}
|
|
85
88
|
if (!expoCellular.getMobileCountryCodeAsync) {
|
|
86
|
-
throw new UnavailabilityError(
|
|
89
|
+
throw new UnavailabilityError(
|
|
90
|
+
NATIVE_MODULE_NAME,
|
|
91
|
+
'getMobileCountryCodeAsync',
|
|
92
|
+
);
|
|
87
93
|
}
|
|
88
94
|
return expoCellular.getMobileCountryCodeAsync();
|
|
89
95
|
}
|
|
@@ -97,7 +103,10 @@ export async function getMobileNetworkCodeAsync(): Promise<string | null> {
|
|
|
97
103
|
return null;
|
|
98
104
|
}
|
|
99
105
|
if (!expoCellular.getMobileNetworkCodeAsync) {
|
|
100
|
-
throw new UnavailabilityError(
|
|
106
|
+
throw new UnavailabilityError(
|
|
107
|
+
NATIVE_MODULE_NAME,
|
|
108
|
+
'getMobileNetworkCodeAsync',
|
|
109
|
+
);
|
|
101
110
|
}
|
|
102
111
|
return expoCellular.getMobileNetworkCodeAsync();
|
|
103
112
|
}
|
|
@@ -123,7 +132,10 @@ export async function getPermissionsAsync(): Promise<PermissionResponse> {
|
|
|
123
132
|
export async function requestPermissionsAsync(): Promise<PermissionResponse> {
|
|
124
133
|
if (Platform.OS === 'android') {
|
|
125
134
|
if (!expoCellular.requestPermissionsAsync) {
|
|
126
|
-
throw new UnavailabilityError(
|
|
135
|
+
throw new UnavailabilityError(
|
|
136
|
+
NATIVE_MODULE_NAME,
|
|
137
|
+
'requestPermissionsAsync',
|
|
138
|
+
);
|
|
127
139
|
}
|
|
128
140
|
return expoCellular.requestPermissionsAsync();
|
|
129
141
|
}
|
|
@@ -21,4 +21,6 @@ export type INativeCellularModule = {
|
|
|
21
21
|
requestPermissionsAsync?(): Promise<PermissionResponse>;
|
|
22
22
|
};
|
|
23
23
|
|
|
24
|
-
export const expoCellular = requireNativeModule<INativeCellularModule>(
|
|
24
|
+
export const expoCellular = requireNativeModule<INativeCellularModule>(
|
|
25
|
+
EXPO_CELLULAR_MODULE_NAME,
|
|
26
|
+
);
|
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
// React lifecycle wiring over the framework-agnostic core (core/cellular.ts) — fetches the
|
|
2
2
|
// current permission status on mount, then exposes request/get to re-check imperatively.
|
|
3
|
+
//
|
|
4
|
+
// The mount fetch has nobody to reject to, so its failure lands in the 4th tuple slot (`error`)
|
|
5
|
+
// instead of escaping as an unhandled rejection: a null status with a non-null error is how a
|
|
6
|
+
// consumer tells "the fetch failed" from "not fetched yet". get()/request() still reject to their
|
|
7
|
+
// direct caller — only the automatic call is made safe.
|
|
3
8
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
4
9
|
import {
|
|
5
10
|
getPermissionsAsync,
|
|
@@ -11,29 +16,40 @@ export function usePermissions(): [
|
|
|
11
16
|
PermissionResponse | null,
|
|
12
17
|
() => Promise<PermissionResponse>,
|
|
13
18
|
() => Promise<PermissionResponse>,
|
|
19
|
+
Error | null,
|
|
14
20
|
] {
|
|
15
21
|
const isMounted = useRef(true);
|
|
16
22
|
const [status, setStatus] = useState<PermissionResponse | null>(null);
|
|
23
|
+
const [error, setError] = useState<Error | null>(null);
|
|
17
24
|
|
|
18
25
|
const getPermission = useCallback(async () => {
|
|
19
26
|
const response = await getPermissionsAsync();
|
|
20
|
-
if (isMounted.current)
|
|
27
|
+
if (isMounted.current) {
|
|
28
|
+
setStatus(response);
|
|
29
|
+
setError(null);
|
|
30
|
+
}
|
|
21
31
|
return response;
|
|
22
32
|
}, []);
|
|
23
33
|
|
|
24
34
|
const requestPermission = useCallback(async () => {
|
|
25
35
|
const response = await requestPermissionsAsync();
|
|
26
|
-
if (isMounted.current)
|
|
36
|
+
if (isMounted.current) {
|
|
37
|
+
setStatus(response);
|
|
38
|
+
setError(null);
|
|
39
|
+
}
|
|
27
40
|
return response;
|
|
28
41
|
}, []);
|
|
29
42
|
|
|
30
43
|
useEffect(() => {
|
|
31
44
|
isMounted.current = true;
|
|
32
|
-
|
|
45
|
+
getPermission().catch((cause: unknown) => {
|
|
46
|
+
if (isMounted.current)
|
|
47
|
+
setError(cause instanceof Error ? cause : new Error(String(cause)));
|
|
48
|
+
});
|
|
33
49
|
return () => {
|
|
34
50
|
isMounted.current = false;
|
|
35
51
|
};
|
|
36
52
|
}, [getPermission]);
|
|
37
53
|
|
|
38
|
-
return [status, requestPermission, getPermission];
|
|
54
|
+
return [status, requestPermission, getPermission, error];
|
|
39
55
|
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
// @symbiote-native/cellular/solid: the Solid entry over the framework-agnostic core, plus
|
|
2
|
+
// createPermissions — the only stateful surface this package needs a lifecycle wrapper for. The
|
|
3
|
+
// lifecycle bucket is `primitives/` and the name is `create*` (never `hooks/`/`use*`, that is
|
|
4
|
+
// React's term and Solid's word for consuming an existing thing) — see
|
|
5
|
+
// adapters/solid/src/primitives for the same convention.
|
|
6
|
+
export { createPermissions } from './primitives/create-permissions';
|
|
7
|
+
export * from '../core';
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
// Solid lifecycle wiring over the framework-agnostic core (core/cellular.ts) — the Solid twin of
|
|
2
|
+
// React's usePermissions hook, Vue's composable, Svelte's rune and Angular's PermissionsService.
|
|
3
|
+
//
|
|
4
|
+
// `primitives/` and `create*`, not `hooks/`/`use*`: Solid's ecosystem calls a composable reactive
|
|
5
|
+
// function a PRIMITIVE and reserves `use*` for consuming something that already exists. Full
|
|
6
|
+
// rationale in adapters/solid/src/primitives/create-color-scheme.ts's header.
|
|
7
|
+
//
|
|
8
|
+
// Returns ACCESSORS, never a snapshot: a Solid component body runs ONCE, so a plain returned value
|
|
9
|
+
// would freeze at "not fetched yet" and never move again.
|
|
10
|
+
//
|
|
11
|
+
// The initial fetch is fired SYNCHRONOUSLY from the primitive body rather than from a mount hook
|
|
12
|
+
// (React's useEffect, Vue's onMounted, Svelte's $effect) — same reason create-color-scheme
|
|
13
|
+
// subscribes synchronously: nothing can interleave between construction and the fetch starting, so
|
|
14
|
+
// there is no window in which a consumer sees a primitive that has not asked for status yet.
|
|
15
|
+
//
|
|
16
|
+
// That fetch has nobody to reject to, so its failure lands in `error` instead of escaping as an
|
|
17
|
+
// unhandled rejection: a null status with a non-null error is how a consumer tells "the fetch
|
|
18
|
+
// failed" from "not fetched yet". get()/request() still reject to their direct caller — only the
|
|
19
|
+
// automatic call is made safe.
|
|
20
|
+
//
|
|
21
|
+
// `onCleanup` drops the writes of a call still in flight when the owner is disposed — the Solid
|
|
22
|
+
// equivalent of React's isMounted ref. Outside a component / createRoot there is no owner to hang
|
|
23
|
+
// it on, so those late writes land instead (harmless: nothing reads a disposed signal).
|
|
24
|
+
import { createSignal, onCleanup, type Accessor } from 'solid-js';
|
|
25
|
+
import {
|
|
26
|
+
getPermissionsAsync,
|
|
27
|
+
requestPermissionsAsync,
|
|
28
|
+
type PermissionResponse,
|
|
29
|
+
} from '../../core';
|
|
30
|
+
|
|
31
|
+
export function createPermissions(): {
|
|
32
|
+
status: Accessor<PermissionResponse | null>;
|
|
33
|
+
error: Accessor<Error | null>;
|
|
34
|
+
request: () => Promise<PermissionResponse>;
|
|
35
|
+
get: () => Promise<PermissionResponse>;
|
|
36
|
+
} {
|
|
37
|
+
const [status, setStatus] = createSignal<PermissionResponse | null>(null);
|
|
38
|
+
const [error, setError] = createSignal<Error | null>(null);
|
|
39
|
+
let isDisposed = false;
|
|
40
|
+
|
|
41
|
+
const record = (response: PermissionResponse): void => {
|
|
42
|
+
if (isDisposed) return;
|
|
43
|
+
setStatus(response);
|
|
44
|
+
setError(null);
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const get = async (): Promise<PermissionResponse> => {
|
|
48
|
+
const response = await getPermissionsAsync();
|
|
49
|
+
record(response);
|
|
50
|
+
return response;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const request = async (): Promise<PermissionResponse> => {
|
|
54
|
+
const response = await requestPermissionsAsync();
|
|
55
|
+
record(response);
|
|
56
|
+
return response;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
get().catch((cause: unknown) => {
|
|
60
|
+
if (isDisposed) return;
|
|
61
|
+
setError(cause instanceof Error ? cause : new Error(String(cause)));
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
onCleanup(() => {
|
|
65
|
+
isDisposed = true;
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
return { status, error, request, get };
|
|
69
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
// @symbiote-native/cellular/svelte: the Svelte entry over the framework-agnostic core, plus
|
|
2
|
+
// usePermissions — the only stateful surface this package needs a lifecycle wrapper for. The
|
|
3
|
+
// lifecycle bucket is `runes/` (never `hooks/`/`composables/`, those are React's and Vue's terms)
|
|
4
|
+
// — see adapters/svelte/src/runes for the same convention.
|
|
5
|
+
export { usePermissions } from './runes/use-permissions.svelte';
|
|
6
|
+
export * from '../core';
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
// Svelte lifecycle wiring over the framework-agnostic core (core/cellular.ts) — fetches the
|
|
2
|
+
// current permission status on mount, then exposes request/get to re-check imperatively. The
|
|
3
|
+
// Svelte twin of vue/composables/use-permissions and react/hooks/use-permissions.
|
|
4
|
+
//
|
|
5
|
+
// `.svelte.ts` (not `.ts`): runes ($state/$effect) are only usable in a file with this extension
|
|
6
|
+
// outside an actual `.svelte` component. `runes/` is Svelte's own term for the bucket React calls
|
|
7
|
+
// `hooks/` and Vue calls `composables/` — see adapters/svelte/src/runes.
|
|
8
|
+
//
|
|
9
|
+
// Vue's onMounted collapses into a `$effect` with no teardown (there is no ongoing subscription
|
|
10
|
+
// here, just a one-shot get). The effect only WRITES `status` (through `get()`), never reads it,
|
|
11
|
+
// so its dependency set stays empty and it fires exactly once on mount.
|
|
12
|
+
//
|
|
13
|
+
// `status` comes back as a GETTER on the returned object rather than a raw `$state` variable:
|
|
14
|
+
// Svelte 5 reactivity is lexically scoped to the declaring module, so a raw `let x = $state(...)`
|
|
15
|
+
// handed out of a plain function arrives dead at the caller. Reading `.status` is the Svelte
|
|
16
|
+
// equivalent of unwrapping Vue's `Ref` via `.value`.
|
|
17
|
+
//
|
|
18
|
+
// The mount fetch has nobody to reject to, so its failure lands in `error` (a second boxed getter,
|
|
19
|
+
// same reason as `status`) instead of escaping as an unhandled rejection: a null status with a
|
|
20
|
+
// non-null error is how a consumer tells "the fetch failed" from "not fetched yet".
|
|
21
|
+
// get()/request() still reject to their direct caller — only the automatic call is made safe.
|
|
22
|
+
import {
|
|
23
|
+
getPermissionsAsync,
|
|
24
|
+
requestPermissionsAsync,
|
|
25
|
+
type PermissionResponse,
|
|
26
|
+
} from '../../core';
|
|
27
|
+
|
|
28
|
+
export function usePermissions(): {
|
|
29
|
+
readonly status: PermissionResponse | null;
|
|
30
|
+
readonly error: Error | null;
|
|
31
|
+
request: () => Promise<PermissionResponse>;
|
|
32
|
+
get: () => Promise<PermissionResponse>;
|
|
33
|
+
} {
|
|
34
|
+
let status = $state<PermissionResponse | null>(null);
|
|
35
|
+
let error = $state<Error | null>(null);
|
|
36
|
+
|
|
37
|
+
const get = async (): Promise<PermissionResponse> => {
|
|
38
|
+
const response = await getPermissionsAsync();
|
|
39
|
+
status = response;
|
|
40
|
+
error = null;
|
|
41
|
+
return response;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const request = async (): Promise<PermissionResponse> => {
|
|
45
|
+
const response = await requestPermissionsAsync();
|
|
46
|
+
status = response;
|
|
47
|
+
error = null;
|
|
48
|
+
return response;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
$effect(() => {
|
|
52
|
+
// The catch runs after the effect has settled, so writing `error` there adds no dependency and
|
|
53
|
+
// the effect still fires exactly once.
|
|
54
|
+
get().catch((cause: unknown) => {
|
|
55
|
+
error = cause instanceof Error ? cause : new Error(String(cause));
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
return {
|
|
60
|
+
get status(): PermissionResponse | null {
|
|
61
|
+
return status;
|
|
62
|
+
},
|
|
63
|
+
get error(): Error | null {
|
|
64
|
+
return error;
|
|
65
|
+
},
|
|
66
|
+
request,
|
|
67
|
+
get,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
// Pulls in svelte's ambient global declarations ($state/$effect/...) for this package's tsc
|
|
2
|
+
// program. adapters/svelte gets these for free because render.ts does a real value import from
|
|
3
|
+
// 'svelte' (any file in a program importing a module loads its ambient globals for the whole
|
|
4
|
+
// program); this package has no such import, so a triple-slash reference does the same job
|
|
5
|
+
// explicitly rather than relying on an incidental side-effect.
|
|
6
|
+
/// <reference types="svelte" />
|
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
// Vue lifecycle wiring over the framework-agnostic core (core/cellular.ts) — fetches the
|
|
2
2
|
// current permission status on mount, then exposes request/get to re-check imperatively.
|
|
3
|
+
//
|
|
4
|
+
// The mount fetch has nobody to reject to, so its failure lands in the `error` ref instead of
|
|
5
|
+
// escaping as an unhandled rejection: a null status with a non-null error is how a consumer tells
|
|
6
|
+
// "the fetch failed" from "not fetched yet". get()/request() still reject to their direct caller —
|
|
7
|
+
// only the automatic call is made safe.
|
|
3
8
|
import { onMounted, ref, type Ref } from '@vue/runtime-core';
|
|
4
9
|
import {
|
|
5
10
|
getPermissionsAsync,
|
|
@@ -9,26 +14,32 @@ import {
|
|
|
9
14
|
|
|
10
15
|
export function usePermissions(): {
|
|
11
16
|
status: Ref<PermissionResponse | null>;
|
|
17
|
+
error: Ref<Error | null>;
|
|
12
18
|
request: () => Promise<PermissionResponse>;
|
|
13
19
|
get: () => Promise<PermissionResponse>;
|
|
14
20
|
} {
|
|
15
21
|
const status = ref<PermissionResponse | null>(null);
|
|
22
|
+
const error = ref<Error | null>(null);
|
|
16
23
|
|
|
17
24
|
const get = async () => {
|
|
18
25
|
const response = await getPermissionsAsync();
|
|
19
26
|
status.value = response;
|
|
27
|
+
error.value = null;
|
|
20
28
|
return response;
|
|
21
29
|
};
|
|
22
30
|
|
|
23
31
|
const request = async () => {
|
|
24
32
|
const response = await requestPermissionsAsync();
|
|
25
33
|
status.value = response;
|
|
34
|
+
error.value = null;
|
|
26
35
|
return response;
|
|
27
36
|
};
|
|
28
37
|
|
|
29
38
|
onMounted(() => {
|
|
30
|
-
|
|
39
|
+
get().catch((cause: unknown) => {
|
|
40
|
+
error.value = cause instanceof Error ? cause : new Error(String(cause));
|
|
41
|
+
});
|
|
31
42
|
});
|
|
32
43
|
|
|
33
|
-
return { status, request, get };
|
|
44
|
+
return { status, error, request, get };
|
|
34
45
|
}
|