@symbiote-native/tracking-transparency 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 +66 -25
- package/build/angular/services/permissions.service/index.d.ts +4 -0
- package/build/angular/services/permissions.service/index.js +19 -2
- package/build/core/tracking-transparency.js +1 -1
- 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 +41 -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 +49 -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/native-module.js.map +1 -1
- package/build-ngc/core/tracking-transparency.js +1 -1
- package/build-ngc/core/tracking-transparency.js.map +1 -1
- package/package.json +42 -14
- package/src/angular/services/permissions.service/index.ts +22 -2
- package/src/core/native-module.ts +4 -3
- package/src/core/tracking-transparency.ts +9 -2
- package/src/react/hooks/use-permissions/index.ts +20 -4
- package/src/solid/index.ts +8 -0
- package/src/solid/primitives/create-permissions.ts +56 -0
- package/src/svelte/index.ts +7 -0
- package/src/svelte/runes/use-permissions.svelte.ts +63 -0
- package/src/svelte/svelte-ambient.d.ts +6 -0
- package/src/vue/composables/use-permissions/index.ts +13 -2
package/README.md
CHANGED
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
A wrapper package for [SymbioteNative](../../README.md) that makes
|
|
4
4
|
[`expo-tracking-transparency`](https://github.com/expo/expo/tree/main/packages/expo-tracking-transparency)
|
|
5
5
|
— the iOS App Tracking Transparency prompt, permission get/request, and the advertising-ID getter
|
|
6
|
-
— usable from **every** adapter, React, Vue, and Angular, not just React. Like
|
|
6
|
+
— usable from **every** adapter, React, Vue, Svelte, Solid, and Angular, not just React. Like
|
|
7
7
|
[`@symbiote-native/brightness`](../brightness), this package's surface is mostly stateless free
|
|
8
|
-
async functions; only the permission surface gets its own lifecycle
|
|
8
|
+
async functions; only the permission surface gets its own per-adapter lifecycle wrapper,
|
|
9
9
|
mirroring upstream's own `usePermissions`. Built the same way as
|
|
10
10
|
[`@symbiote-native/local-auth`](../local-auth), [`@symbiote-native/battery`](../battery), and
|
|
11
11
|
[`@symbiote-native/brightness`](../brightness) — an `expo-modules-core`-based wrapper (see the
|
|
@@ -34,13 +34,13 @@ Unlike a plain RN native module, `expo-tracking-transparency`'s native code is d
|
|
|
34
34
|
this repo use — this needs wiring into the native host app **once**, covering this package and
|
|
35
35
|
every other `expo-modules-core` package with zero further changes:
|
|
36
36
|
|
|
37
|
-
| Platform | Touches
|
|
38
|
-
|
|
39
|
-
| iOS
|
|
40
|
-
| iOS
|
|
41
|
-
| iOS
|
|
42
|
-
| Android
|
|
43
|
-
| Android
|
|
37
|
+
| Platform | Touches |
|
|
38
|
+
| -------- | --------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
39
|
+
| iOS | `ios/Podfile` — add `use_expo_modules!` |
|
|
40
|
+
| iOS | `AppDelegate.swift` — Expo's runtime-bootstrap hook |
|
|
41
|
+
| iOS | `Info.plist` — `NSUserTrackingUsageDescription`, required to show the ATT prompt |
|
|
42
|
+
| Android | `settings.gradle` / `app/build.gradle` — resolve and include the Expo Gradle projects |
|
|
43
|
+
| 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) |
|
|
44
44
|
|
|
45
45
|
Full mechanics — the Podfile pieces that normally ship inside the `expo` package, the `expo`
|
|
46
46
|
peer-dependency exclusion list — live in the `symbiote-expo-native-module` skill. The Android
|
|
@@ -56,13 +56,16 @@ src/core/ getAdvertisingId, get/requestTrackingPermissionsAsync,
|
|
|
56
56
|
expo-modules-core's requireNativeModule.
|
|
57
57
|
src/react/hooks/ @symbiote-native/tracking-transparency/react — usePermissions
|
|
58
58
|
src/vue/composables/ @symbiote-native/tracking-transparency/vue — usePermissions (same name)
|
|
59
|
+
src/svelte/runes/ @symbiote-native/tracking-transparency/svelte — usePermissions (same name)
|
|
60
|
+
src/solid/primitives/ @symbiote-native/tracking-transparency/solid — createPermissions
|
|
61
|
+
(Solid reserves `use*` for consuming existing state)
|
|
59
62
|
src/angular/services/ @symbiote-native/tracking-transparency/angular — PermissionsService
|
|
60
63
|
```
|
|
61
64
|
|
|
62
|
-
Each adapter's
|
|
63
|
-
`requestTrackingPermissionsAsync` pair in its own lifecycle idiom (auto-fetch
|
|
64
|
-
a tuple/object/signal). `getAdvertisingId`/`isAvailable` are stateless
|
|
65
|
-
written once in `core/` and shared verbatim by
|
|
65
|
+
Each adapter's wrapper puts the same one-shot `getTrackingPermissionsAsync`/
|
|
66
|
+
`requestTrackingPermissionsAsync` pair in its own lifecycle idiom (auto-fetch when the wrapper is
|
|
67
|
+
created, exposed as a tuple/object/signal/accessor). `getAdvertisingId`/`isAvailable` are stateless
|
|
68
|
+
free-function re-exports, written once in `core/` and shared verbatim by every adapter.
|
|
66
69
|
|
|
67
70
|
Upstream's own `createPermissionHook`/`useTrackingPermissions` is **not** ported — that helper is
|
|
68
71
|
React-only (built on `useState`/`useEffect`), and this repo's convention is for each adapter to
|
|
@@ -98,7 +101,8 @@ function TrackingScreen() {
|
|
|
98
101
|
import { usePermissions } from '@symbiote-native/tracking-transparency/vue';
|
|
99
102
|
import { getAdvertisingId } from '@symbiote-native/tracking-transparency';
|
|
100
103
|
|
|
101
|
-
const { status: permissionStatus, request: requestPermission } =
|
|
104
|
+
const { status: permissionStatus, request: requestPermission } =
|
|
105
|
+
usePermissions();
|
|
102
106
|
</script>
|
|
103
107
|
|
|
104
108
|
<template>
|
|
@@ -117,7 +121,10 @@ const { status: permissionStatus, request: requestPermission } = usePermissions(
|
|
|
117
121
|
```ts
|
|
118
122
|
// Angular
|
|
119
123
|
import { Component, inject } from '@angular/core';
|
|
120
|
-
import {
|
|
124
|
+
import {
|
|
125
|
+
PermissionsService,
|
|
126
|
+
getAdvertisingId,
|
|
127
|
+
} from '@symbiote-native/tracking-transparency/angular';
|
|
121
128
|
|
|
122
129
|
@Component({
|
|
123
130
|
selector: 'TrackingScreen',
|
|
@@ -128,7 +135,9 @@ import { PermissionsService, getAdvertisingId } from '@symbiote-native/tracking-
|
|
|
128
135
|
<Pressable (press)="permissionsService.request()">
|
|
129
136
|
<Text>Request tracking permission</Text>
|
|
130
137
|
</Pressable>
|
|
131
|
-
<Pressable (press)="logAdvertisingId()"
|
|
138
|
+
<Pressable (press)="logAdvertisingId()"
|
|
139
|
+
><Text>Log advertising ID</Text></Pressable
|
|
140
|
+
>
|
|
132
141
|
</View>
|
|
133
142
|
`,
|
|
134
143
|
})
|
|
@@ -142,7 +151,30 @@ export class TrackingScreen {
|
|
|
142
151
|
}
|
|
143
152
|
```
|
|
144
153
|
|
|
145
|
-
|
|
154
|
+
```tsx
|
|
155
|
+
// Solid — the accessors are CALLED; a Solid component body runs once, so a snapshot would freeze.
|
|
156
|
+
import { createPermissions } from '@symbiote-native/tracking-transparency/solid';
|
|
157
|
+
import { getAdvertisingId } from '@symbiote-native/tracking-transparency';
|
|
158
|
+
|
|
159
|
+
function TrackingScreen() {
|
|
160
|
+
const { status: permissionStatus, request: requestPermission } =
|
|
161
|
+
createPermissions();
|
|
162
|
+
|
|
163
|
+
return (
|
|
164
|
+
<View>
|
|
165
|
+
<Text>{permissionStatus()?.status ?? 'checking…'}</Text>
|
|
166
|
+
<Pressable onPress={() => void requestPermission()}>
|
|
167
|
+
<Text>Request tracking permission</Text>
|
|
168
|
+
</Pressable>
|
|
169
|
+
<Pressable onPress={() => console.log(getAdvertisingId())}>
|
|
170
|
+
<Text>Log advertising ID</Text>
|
|
171
|
+
</Pressable>
|
|
172
|
+
</View>
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
The examples above mirror the real canary demo screens —
|
|
146
178
|
`examples/expo-react/screens/TrackingTransparencyScreen.tsx`,
|
|
147
179
|
`examples/expo-vue-sfc/screens/TrackingTransparencyScreen.vue`,
|
|
148
180
|
`examples/expo-vue-tsx/screens/TrackingTransparencyScreen.tsx`,
|
|
@@ -168,26 +200,35 @@ Plus `PermissionStatus`/`PermissionResponse`/`PermissionExpiration`/`PermissionH
|
|
|
168
200
|
re-exported verbatim from `expo-modules-core`, never the `expo` meta-package.
|
|
169
201
|
|
|
170
202
|
```ts
|
|
171
|
-
import {
|
|
203
|
+
import {
|
|
204
|
+
getAdvertisingId,
|
|
205
|
+
getTrackingPermissionsAsync,
|
|
206
|
+
} from '@symbiote-native/tracking-transparency';
|
|
172
207
|
|
|
173
|
-
// framework-scoped entry points re-export the same free functions, plus a
|
|
174
|
-
//
|
|
208
|
+
// framework-scoped entry points re-export the same free functions, plus a per-adapter
|
|
209
|
+
// permission wrapper:
|
|
175
210
|
import { usePermissions } from '@symbiote-native/tracking-transparency/react';
|
|
176
211
|
import { usePermissions } from '@symbiote-native/tracking-transparency/vue';
|
|
212
|
+
import { usePermissions } from '@symbiote-native/tracking-transparency/svelte';
|
|
213
|
+
import { createPermissions } from '@symbiote-native/tracking-transparency/solid';
|
|
177
214
|
import { PermissionsService } from '@symbiote-native/tracking-transparency/angular';
|
|
178
215
|
```
|
|
179
216
|
|
|
180
|
-
`usePermissions`
|
|
181
|
-
`[status, request, get]` tuple (React) / `{ status, request, get }` object
|
|
182
|
-
|
|
217
|
+
`usePermissions` auto-fetches the current permission status on mount and returns a
|
|
218
|
+
`[status, request, get, error]` tuple (React) / `{ status, error, request, get }` object
|
|
219
|
+
(Vue/Svelte); Solid's `createPermissions` returns the same object with `Accessor`s instead of
|
|
220
|
+
`Ref`s, and Angular's `PermissionsService.connect()` returns a readonly `Signal`:
|
|
183
221
|
|
|
184
222
|
```ts
|
|
185
223
|
// React
|
|
186
224
|
const [status, request] = usePermissions();
|
|
187
225
|
|
|
188
|
-
// Vue
|
|
226
|
+
// Vue / Svelte
|
|
189
227
|
const { status, request } = usePermissions();
|
|
190
228
|
|
|
229
|
+
// Solid — status is an accessor, called at the read site
|
|
230
|
+
const { status, request } = createPermissions();
|
|
231
|
+
|
|
191
232
|
// Angular
|
|
192
233
|
readonly status = inject(PermissionsService).connect();
|
|
193
234
|
```
|
|
@@ -204,7 +245,7 @@ readonly status = inject(PermissionsService).connect();
|
|
|
204
245
|
|
|
205
246
|
No Fabric/Descriptor angle at all — tracking-transparency is a pure async-function + permission
|
|
206
247
|
surface, never a view. Tests inject a fake native-module object in place of the real
|
|
207
|
-
`requireNativeModule` resolution (`src/core/*.test.ts`, `src/{react,vue,angular}/**/*.test.{ts,tsx}`,
|
|
248
|
+
`requireNativeModule` resolution (`src/core/*.test.ts`, `src/{react,vue,svelte,solid,angular}/**/*.test.{ts,tsx}`,
|
|
208
249
|
`vitest`), the same pattern `expo-tracking-transparency` itself uses upstream — no
|
|
209
250
|
`installFabric()` for the core layer, no ViewConfig. Native rendering itself is verified on-device
|
|
210
251
|
(see the parent [README](../../README.md) for the project's testing model).
|
|
@@ -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>;
|
|
@@ -39,6 +39,11 @@ var __runInitializers = (this && this.__runInitializers) || function (thisArg, i
|
|
|
39
39
|
//
|
|
40
40
|
// readonly status = inject(PermissionsService).connect();
|
|
41
41
|
// // template: {{ status()?.granted }}
|
|
42
|
+
//
|
|
43
|
+
// The auto-fetch has nobody to reject to, so its failure lands in the `error` signal instead of
|
|
44
|
+
// escaping as an unhandled rejection: a null status with a non-null error is how a consumer tells
|
|
45
|
+
// "the fetch failed" from "not fetched yet". get()/request() still reject to their direct caller —
|
|
46
|
+
// only the automatic call is made safe.
|
|
42
47
|
import { Injectable, signal } from '@angular/core';
|
|
43
48
|
import { getTrackingPermissionsAsync, requestTrackingPermissionsAsync, } from '../../../core/index.js';
|
|
44
49
|
let PermissionsService = (() => {
|
|
@@ -56,20 +61,32 @@ let PermissionsService = (() => {
|
|
|
56
61
|
__runInitializers(_classThis, _classExtraInitializers);
|
|
57
62
|
}
|
|
58
63
|
status = signal(null);
|
|
64
|
+
fetchError = signal(null);
|
|
65
|
+
isAutoFetchStarted = false;
|
|
66
|
+
/** Why the auto-fetch left `status` at null. Cleared by the next successful get()/request(). */
|
|
67
|
+
error = this.fetchError.asReadonly();
|
|
59
68
|
connect() {
|
|
60
|
-
|
|
61
|
-
|
|
69
|
+
// Latched rather than guarded on `status() === null`: a failed fetch leaves status null
|
|
70
|
+
// forever, so the old guard turned every later connect() into another native call — unbounded
|
|
71
|
+
// once connect() is reached from a change-detected expression instead of a field initializer.
|
|
72
|
+
if (!this.isAutoFetchStarted) {
|
|
73
|
+
this.isAutoFetchStarted = true;
|
|
74
|
+
this.get().catch((cause) => {
|
|
75
|
+
this.fetchError.set(cause instanceof Error ? cause : new Error(String(cause)));
|
|
76
|
+
});
|
|
62
77
|
}
|
|
63
78
|
return this.status.asReadonly();
|
|
64
79
|
}
|
|
65
80
|
async get() {
|
|
66
81
|
const response = await getTrackingPermissionsAsync();
|
|
67
82
|
this.status.set(response);
|
|
83
|
+
this.fetchError.set(null);
|
|
68
84
|
return response;
|
|
69
85
|
}
|
|
70
86
|
async request() {
|
|
71
87
|
const response = await requestTrackingPermissionsAsync();
|
|
72
88
|
this.status.set(response);
|
|
89
|
+
this.fetchError.set(null);
|
|
73
90
|
return response;
|
|
74
91
|
}
|
|
75
92
|
};
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
// NOT ported here — that helper is React-only (built on useState/useEffect), and this repo's
|
|
6
6
|
// convention is for each adapter to hand-roll its own permission hook instead (react/hooks,
|
|
7
7
|
// vue/composables, angular/services), same as brightness/cellular already do.
|
|
8
|
-
import { Platform, PermissionStatus, UnavailabilityError } from 'expo-modules-core';
|
|
8
|
+
import { Platform, PermissionStatus, UnavailabilityError, } from 'expo-modules-core';
|
|
9
9
|
import { expoTrackingTransparency } from './native-module.js';
|
|
10
10
|
const NATIVE_MODULE_NAME = 'TrackingTransparency';
|
|
11
11
|
/**
|
|
@@ -2,29 +2,42 @@
|
|
|
2
2
|
// auto-fetches the current permission status on mount, then exposes get/request as imperative
|
|
3
3
|
// callbacks. Mirrors the shape @symbiote-native/cellular's own usePermissions ships in parallel,
|
|
4
4
|
// so both stay byte-for-byte consistent.
|
|
5
|
+
//
|
|
6
|
+
// The mount fetch has nobody to reject to, so its failure lands in the 4th tuple slot (`error`)
|
|
7
|
+
// instead of escaping as an unhandled rejection: a null status with a non-null error is how a
|
|
8
|
+
// consumer tells "the fetch failed" from "not fetched yet". get()/request() still reject to their
|
|
9
|
+
// direct caller — only the automatic call is made safe.
|
|
5
10
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
6
11
|
import { getTrackingPermissionsAsync, requestTrackingPermissionsAsync, } from '../../../core/index.js';
|
|
7
12
|
export function usePermissions() {
|
|
8
13
|
const isMounted = useRef(true);
|
|
9
14
|
const [status, setStatus] = useState(null);
|
|
15
|
+
const [error, setError] = useState(null);
|
|
10
16
|
const getPermission = useCallback(async () => {
|
|
11
17
|
const response = await getTrackingPermissionsAsync();
|
|
12
|
-
if (isMounted.current)
|
|
18
|
+
if (isMounted.current) {
|
|
13
19
|
setStatus(response);
|
|
20
|
+
setError(null);
|
|
21
|
+
}
|
|
14
22
|
return response;
|
|
15
23
|
}, []);
|
|
16
24
|
const requestPermission = useCallback(async () => {
|
|
17
25
|
const response = await requestTrackingPermissionsAsync();
|
|
18
|
-
if (isMounted.current)
|
|
26
|
+
if (isMounted.current) {
|
|
19
27
|
setStatus(response);
|
|
28
|
+
setError(null);
|
|
29
|
+
}
|
|
20
30
|
return response;
|
|
21
31
|
}, []);
|
|
22
32
|
useEffect(() => {
|
|
23
33
|
isMounted.current = true;
|
|
24
|
-
|
|
34
|
+
getPermission().catch((cause) => {
|
|
35
|
+
if (isMounted.current)
|
|
36
|
+
setError(cause instanceof Error ? cause : new Error(String(cause)));
|
|
37
|
+
});
|
|
25
38
|
return () => {
|
|
26
39
|
isMounted.current = false;
|
|
27
40
|
};
|
|
28
41
|
}, [getPermission]);
|
|
29
|
-
return [status, requestPermission, getPermission];
|
|
42
|
+
return [status, requestPermission, getPermission, error];
|
|
30
43
|
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
// @symbiote-native/tracking-transparency/solid: the Solid entry over the framework-agnostic core.
|
|
2
|
+
// Same reasoning as the React, Vue and Svelte entries — only the permission surface gets a
|
|
3
|
+
// lifecycle primitive, every other export is a plain re-export — mirrors the lifecycle-bucket
|
|
4
|
+
// naming convention of adapters/solid/src/primitives (never `hooks/`/`composables/`/`runes/`,
|
|
5
|
+
// those are React's, Vue's and Svelte's terms).
|
|
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,41 @@
|
|
|
1
|
+
// createPermissions — the Solid twin of React's `usePermissions` hook, Vue's composable and
|
|
2
|
+
// Svelte's rune, over the framework-agnostic core (core/tracking-transparency.ts). Auto-fetches
|
|
3
|
+
// the current permission status, then exposes get/request as imperative callbacks.
|
|
4
|
+
//
|
|
5
|
+
// `primitives/` and `create*`, never `hooks/`+`use*`: Solid's ecosystem calls a composable
|
|
6
|
+
// reactive function a PRIMITIVE and reserves `use*` for consuming something that already exists.
|
|
7
|
+
// Full rationale in adapters/solid/src/primitives/create-color-scheme.ts.
|
|
8
|
+
//
|
|
9
|
+
// Returns ACCESSORS, never snapshots: a Solid component body runs ONCE, so returned values would
|
|
10
|
+
// pin the caller to the null pre-fetch state forever.
|
|
11
|
+
//
|
|
12
|
+
// The auto-fetch fires from the primitive body rather than a mount effect — the shape
|
|
13
|
+
// `createResource` uses, and the reason this primitive needs no lifecycle hook at all. Its failure
|
|
14
|
+
// lands in `error` instead of escaping as an unhandled rejection: a null status with a non-null
|
|
15
|
+
// error is how a consumer tells "the fetch failed" from "not fetched yet". get()/request() still
|
|
16
|
+
// reject to their direct caller — only the automatic call is made safe.
|
|
17
|
+
//
|
|
18
|
+
// No isMounted guard around the late write, unlike React's hook: once the owning root is disposed
|
|
19
|
+
// nothing is subscribed to these signals any more, so a resolution arriving afterwards is inert.
|
|
20
|
+
import { createSignal } from 'solid-js';
|
|
21
|
+
import { getTrackingPermissionsAsync, requestTrackingPermissionsAsync, } from '../../core/index.js';
|
|
22
|
+
export function createPermissions() {
|
|
23
|
+
const [status, setStatus] = createSignal(null);
|
|
24
|
+
const [error, setError] = createSignal(null);
|
|
25
|
+
const get = async () => {
|
|
26
|
+
const response = await getTrackingPermissionsAsync();
|
|
27
|
+
setStatus(response);
|
|
28
|
+
setError(null);
|
|
29
|
+
return response;
|
|
30
|
+
};
|
|
31
|
+
const request = async () => {
|
|
32
|
+
const response = await requestTrackingPermissionsAsync();
|
|
33
|
+
setStatus(response);
|
|
34
|
+
setError(null);
|
|
35
|
+
return response;
|
|
36
|
+
};
|
|
37
|
+
get().catch((cause) => {
|
|
38
|
+
setError(cause instanceof Error ? cause : new Error(String(cause)));
|
|
39
|
+
});
|
|
40
|
+
return { status, error, request, get };
|
|
41
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
// @symbiote-native/tracking-transparency/svelte: the Svelte entry over the framework-agnostic core.
|
|
2
|
+
// Same reasoning as the React and Vue entries — only the permission surface gets a lifecycle rune,
|
|
3
|
+
// every other export is a plain re-export — mirrors the lifecycle-bucket naming convention of
|
|
4
|
+
// adapters/svelte/src/runes (never `hooks/`/`composables/`, those are React's and Vue's terms).
|
|
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,49 @@
|
|
|
1
|
+
// Svelte lifecycle wiring over the framework-agnostic core (core/tracking-transparency.ts) —
|
|
2
|
+
// auto-fetches the current permission status on mount, then exposes get/request as imperative
|
|
3
|
+
// callbacks. Just a one-shot get with no ongoing subscription, so `$effect` needs no cleanup.
|
|
4
|
+
//
|
|
5
|
+
// `.svelte.ts` (not `.ts`): runes ($state/$effect) only work in files with this extension
|
|
6
|
+
// outside an actual `.svelte` component. `status` is returned as a getter, NOT a bare
|
|
7
|
+
// `$state`: Svelte 5 reactivity is lexically scoped to the declaring module and doesn't
|
|
8
|
+
// survive being returned as a raw value from a plain function — mirrors how Vue's `Ref`
|
|
9
|
+
// needs `.value` to unwrap.
|
|
10
|
+
//
|
|
11
|
+
// The mount fetch has nobody to reject to, so its failure lands in `error` (a second boxed getter,
|
|
12
|
+
// same reason as `status`) instead of escaping as an unhandled rejection: a null status with a
|
|
13
|
+
// non-null error is how a consumer tells "the fetch failed" from "not fetched yet".
|
|
14
|
+
// get()/request() still reject to their direct caller — only the automatic call is made safe.
|
|
15
|
+
import { getTrackingPermissionsAsync, requestTrackingPermissionsAsync, } from '../../core/index.js';
|
|
16
|
+
export function usePermissions() {
|
|
17
|
+
let status = $state(null);
|
|
18
|
+
let error = $state(null);
|
|
19
|
+
const get = async () => {
|
|
20
|
+
const response = await getTrackingPermissionsAsync();
|
|
21
|
+
status = response;
|
|
22
|
+
error = null;
|
|
23
|
+
return response;
|
|
24
|
+
};
|
|
25
|
+
const request = async () => {
|
|
26
|
+
const response = await requestTrackingPermissionsAsync();
|
|
27
|
+
status = response;
|
|
28
|
+
error = null;
|
|
29
|
+
return response;
|
|
30
|
+
};
|
|
31
|
+
$effect(() => {
|
|
32
|
+
// `get` is a plain closure, not reactive state, so the effect has an empty dependency set and
|
|
33
|
+
// runs exactly once on mount — the twin of Vue's onMounted. The catch runs after the effect
|
|
34
|
+
// has settled, so writing `error` there adds no dependency and does not re-arm it.
|
|
35
|
+
get().catch((cause) => {
|
|
36
|
+
error = cause instanceof Error ? cause : new Error(String(cause));
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
return {
|
|
40
|
+
get status() {
|
|
41
|
+
return status;
|
|
42
|
+
},
|
|
43
|
+
get error() {
|
|
44
|
+
return error;
|
|
45
|
+
},
|
|
46
|
+
request,
|
|
47
|
+
get,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
@@ -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
|
};
|
|
@@ -2,22 +2,32 @@
|
|
|
2
2
|
// auto-fetches the current permission status on mount (mirrors use-battery-level's
|
|
3
3
|
// onMounted/onUnmounted style, but there is no ongoing subscription to tear down here, just a
|
|
4
4
|
// one-shot get), then exposes get/request as imperative callbacks.
|
|
5
|
+
//
|
|
6
|
+
// The mount fetch has nobody to reject to, so its failure lands in the `error` ref instead of
|
|
7
|
+
// escaping as an unhandled rejection: a null status with a non-null error is how a consumer tells
|
|
8
|
+
// "the fetch failed" from "not fetched yet". get()/request() still reject to their direct caller —
|
|
9
|
+
// only the automatic call is made safe.
|
|
5
10
|
import { onMounted, ref } from '@vue/runtime-core';
|
|
6
11
|
import { getTrackingPermissionsAsync, requestTrackingPermissionsAsync, } from '../../../core/index.js';
|
|
7
12
|
export function usePermissions() {
|
|
8
13
|
const status = ref(null);
|
|
14
|
+
const error = ref(null);
|
|
9
15
|
const get = async () => {
|
|
10
16
|
const response = await getTrackingPermissionsAsync();
|
|
11
17
|
status.value = response;
|
|
18
|
+
error.value = null;
|
|
12
19
|
return response;
|
|
13
20
|
};
|
|
14
21
|
const request = async () => {
|
|
15
22
|
const response = await requestTrackingPermissionsAsync();
|
|
16
23
|
status.value = response;
|
|
24
|
+
error.value = null;
|
|
17
25
|
return response;
|
|
18
26
|
};
|
|
19
27
|
onMounted(() => {
|
|
20
|
-
|
|
28
|
+
get().catch((cause) => {
|
|
29
|
+
error.value = cause instanceof Error ? cause : new Error(String(cause));
|
|
30
|
+
});
|
|
21
31
|
});
|
|
22
|
-
return { status, request, get };
|
|
32
|
+
return { status, error, request, get };
|
|
23
33
|
}
|
|
@@ -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>;
|
|
@@ -5,32 +5,50 @@
|
|
|
5
5
|
//
|
|
6
6
|
// readonly status = inject(PermissionsService).connect();
|
|
7
7
|
// // template: {{ status()?.granted }}
|
|
8
|
+
//
|
|
9
|
+
// The auto-fetch has nobody to reject to, so its failure lands in the `error` signal instead of
|
|
10
|
+
// escaping as an unhandled rejection: a null status with a non-null error is how a consumer tells
|
|
11
|
+
// "the fetch failed" from "not fetched yet". get()/request() still reject to their direct caller —
|
|
12
|
+
// only the automatic call is made safe.
|
|
8
13
|
import { Injectable, signal } from '@angular/core';
|
|
9
14
|
import { getTrackingPermissionsAsync, requestTrackingPermissionsAsync, } from '../../../core';
|
|
10
15
|
import * as i0 from "@angular/core";
|
|
11
16
|
export class PermissionsService {
|
|
12
17
|
status = signal(null, /* @ts-ignore */
|
|
13
18
|
...(ngDevMode ? [{ debugName: "status" }] : /* istanbul ignore next */ []));
|
|
19
|
+
fetchError = signal(null, /* @ts-ignore */
|
|
20
|
+
...(ngDevMode ? [{ debugName: "fetchError" }] : /* istanbul ignore next */ []));
|
|
21
|
+
isAutoFetchStarted = false;
|
|
22
|
+
/** Why the auto-fetch left `status` at null. Cleared by the next successful get()/request(). */
|
|
23
|
+
error = this.fetchError.asReadonly();
|
|
14
24
|
connect() {
|
|
15
|
-
|
|
16
|
-
|
|
25
|
+
// Latched rather than guarded on `status() === null`: a failed fetch leaves status null
|
|
26
|
+
// forever, so the old guard turned every later connect() into another native call — unbounded
|
|
27
|
+
// once connect() is reached from a change-detected expression instead of a field initializer.
|
|
28
|
+
if (!this.isAutoFetchStarted) {
|
|
29
|
+
this.isAutoFetchStarted = true;
|
|
30
|
+
this.get().catch((cause) => {
|
|
31
|
+
this.fetchError.set(cause instanceof Error ? cause : new Error(String(cause)));
|
|
32
|
+
});
|
|
17
33
|
}
|
|
18
34
|
return this.status.asReadonly();
|
|
19
35
|
}
|
|
20
36
|
async get() {
|
|
21
37
|
const response = await getTrackingPermissionsAsync();
|
|
22
38
|
this.status.set(response);
|
|
39
|
+
this.fetchError.set(null);
|
|
23
40
|
return response;
|
|
24
41
|
}
|
|
25
42
|
async request() {
|
|
26
43
|
const response = await requestTrackingPermissionsAsync();
|
|
27
44
|
this.status.set(response);
|
|
45
|
+
this.fetchError.set(null);
|
|
28
46
|
return response;
|
|
29
47
|
}
|
|
30
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.
|
|
31
|
-
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.
|
|
48
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: PermissionsService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
49
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: PermissionsService, providedIn: 'root' });
|
|
32
50
|
}
|
|
33
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.
|
|
51
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: PermissionsService, decorators: [{
|
|
34
52
|
type: Injectable,
|
|
35
53
|
args: [{ providedIn: 'root' }]
|
|
36
54
|
}] });
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../src/angular/services/permissions.service/index.ts"],"names":[],"mappings":"AAAA,mFAAmF;AACnF,4FAA4F;AAC5F,yFAAyF;AACzF,+BAA+B;AAC/B,EAAE;AACF,4DAA4D;AAC5D,yCAAyC;AACzC,OAAO,EAAE,UAAU,EAAE,MAAM,EAAe,MAAM,eAAe,CAAC;AAChE,OAAO,EACL,2BAA2B,EAC3B,+BAA+B,GAEhC,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,mFAAmF;AACnF,4FAA4F;AAC5F,yFAAyF;AACzF,+BAA+B;AAC/B,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,2BAA2B,EAC3B,+BAA+B,GAEhC,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,2BAA2B,EAAE,CAAC;QACrD,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,+BAA+B,EAAE,CAAC;QACzD,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":"native-module.js","sourceRoot":"","sources":["../../src/core/native-module.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AAGxD,MAAM,sCAAsC,GAAG,0BAA0B,CAAC;AAe1E,MAAM,CAAC,MAAM,wBAAwB,
|
|
1
|
+
{"version":3,"file":"native-module.js","sourceRoot":"","sources":["../../src/core/native-module.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AAGxD,MAAM,sCAAsC,GAAG,0BAA0B,CAAC;AAe1E,MAAM,CAAC,MAAM,wBAAwB,GACnC,mBAAmB,CACjB,sCAAsC,CACvC,CAAC"}
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
// NOT ported here — that helper is React-only (built on useState/useEffect), and this repo's
|
|
6
6
|
// convention is for each adapter to hand-roll its own permission hook instead (react/hooks,
|
|
7
7
|
// vue/composables, angular/services), same as brightness/cellular already do.
|
|
8
|
-
import { Platform, PermissionStatus, UnavailabilityError } from 'expo-modules-core';
|
|
8
|
+
import { Platform, PermissionStatus, UnavailabilityError, } from 'expo-modules-core';
|
|
9
9
|
import { expoTrackingTransparency } from './native-module';
|
|
10
10
|
const NATIVE_MODULE_NAME = 'TrackingTransparency';
|
|
11
11
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tracking-transparency.js","sourceRoot":"","sources":["../../src/core/tracking-transparency.ts"],"names":[],"mappings":"AAAA,iGAAiG;AACjG,6FAA6F;AAC7F,wFAAwF;AACxF,8FAA8F;AAC9F,6FAA6F;AAC7F,4FAA4F;AAC5F,8EAA8E;AAC9E,OAAO,
|
|
1
|
+
{"version":3,"file":"tracking-transparency.js","sourceRoot":"","sources":["../../src/core/tracking-transparency.ts"],"names":[],"mappings":"AAAA,iGAAiG;AACjG,6FAA6F;AAC7F,wFAAwF;AACxF,8FAA8F;AAC9F,6FAA6F;AAC7F,4FAA4F;AAC5F,8EAA8E;AAC9E,OAAO,EACL,QAAQ,EACR,gBAAgB,EAChB,mBAAmB,GACpB,MAAM,mBAAmB,CAAC;AAG3B,OAAO,EAAE,wBAAwB,EAAE,MAAM,iBAAiB,CAAC;AAE3D,MAAM,kBAAkB,GAAG,sBAAsB,CAAC;AAElD;;;;GAIG;AACH,MAAM,UAAU,gBAAgB;IAC9B,MAAM,aAAa,GAAG,wBAAwB,CAAC,gBAAgB,EAAE,CAAC;IAClE,IAAI,aAAa,KAAK,sCAAsC,EAAE,CAAC;QAC7D,OAAO,IAAI,CAAC;IACd,CAAC;IACD,OAAO,aAAa,CAAC;AACvB,CAAC;AAED,MAAM,gCAAgC,GAAuB;IAC3D,OAAO,EAAE,IAAI;IACb,OAAO,EAAE,OAAO;IAChB,WAAW,EAAE,IAAI;IACjB,MAAM,EAAE,gBAAgB,CAAC,OAAO;CACjC,CAAC;AAEF;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,+BAA+B;IACnD,IAAI,QAAQ,CAAC,EAAE,KAAK,KAAK,EAAE,CAAC;QAC1B,OAAO,OAAO,CAAC,OAAO,CAAC,gCAAgC,CAAC,CAAC;IAC3D,CAAC;IACD,IAAI,CAAC,wBAAwB,CAAC,uBAAuB,EAAE,CAAC;QACtD,MAAM,IAAI,mBAAmB,CAC3B,kBAAkB,EAClB,yBAAyB,CAC1B,CAAC;IACJ,CAAC;IACD,OAAO,MAAM,wBAAwB,CAAC,uBAAuB,EAAE,CAAC;AAClE,CAAC;AAED;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,2BAA2B;IAC/C,IAAI,QAAQ,CAAC,EAAE,KAAK,KAAK,EAAE,CAAC;QAC1B,OAAO,OAAO,CAAC,OAAO,CAAC,gCAAgC,CAAC,CAAC;IAC3D,CAAC;IACD,IAAI,CAAC,wBAAwB,CAAC,mBAAmB,EAAE,CAAC;QAClD,MAAM,IAAI,mBAAmB,CAAC,kBAAkB,EAAE,qBAAqB,CAAC,CAAC;IAC3E,CAAC;IACD,OAAO,MAAM,wBAAwB,CAAC,mBAAmB,EAAE,CAAC;AAC9D,CAAC;AAED,uEAAuE;AACvE,MAAM,UAAU,WAAW;IACzB,OAAO,OAAO,CAAC,wBAAwB,CAAC,CAAC;AAC3C,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@symbiote-native/tracking-transparency",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "expo-tracking-transparency wrapped for SymbioteNative — one framework-agnostic core, built once and reachable from the React, Vue, and Angular adapters. iOS App Tracking Transparency prompt (permission get/request, always-granted on Android/web) plus the advertising-ID getter.",
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"description": "expo-tracking-transparency wrapped for SymbioteNative — one framework-agnostic core, built once and reachable from the React, Vue, Svelte, Solid, and Angular adapters. iOS App Tracking Transparency prompt (permission get/request, always-granted on Android/web) plus the advertising-ID getter.",
|
|
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/
|
|
99
|
-
"@symbiote-native/
|
|
100
|
-
"@symbiote-native/
|
|
101
|
-
"@symbiote-native/
|
|
102
|
-
"@symbiote-native/
|
|
124
|
+
"@symbiote-native/engine": "0.3.0",
|
|
125
|
+
"@symbiote-native/react": "0.4.0",
|
|
126
|
+
"@symbiote-native/angular": "0.8.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",
|
|
@@ -5,6 +5,11 @@
|
|
|
5
5
|
//
|
|
6
6
|
// readonly status = inject(PermissionsService).connect();
|
|
7
7
|
// // template: {{ status()?.granted }}
|
|
8
|
+
//
|
|
9
|
+
// The auto-fetch has nobody to reject to, so its failure lands in the `error` signal instead of
|
|
10
|
+
// escaping as an unhandled rejection: a null status with a non-null error is how a consumer tells
|
|
11
|
+
// "the fetch failed" from "not fetched yet". get()/request() still reject to their direct caller —
|
|
12
|
+
// only the automatic call is made safe.
|
|
8
13
|
import { Injectable, signal, type Signal } from '@angular/core';
|
|
9
14
|
import {
|
|
10
15
|
getTrackingPermissionsAsync,
|
|
@@ -15,10 +20,23 @@ import {
|
|
|
15
20
|
@Injectable({ providedIn: 'root' })
|
|
16
21
|
export class PermissionsService {
|
|
17
22
|
private readonly status = signal<PermissionResponse | null>(null);
|
|
23
|
+
private readonly fetchError = signal<Error | null>(null);
|
|
24
|
+
private isAutoFetchStarted = false;
|
|
25
|
+
|
|
26
|
+
/** Why the auto-fetch left `status` at null. Cleared by the next successful get()/request(). */
|
|
27
|
+
readonly error: Signal<Error | null> = this.fetchError.asReadonly();
|
|
18
28
|
|
|
19
29
|
connect(): Signal<PermissionResponse | null> {
|
|
20
|
-
|
|
21
|
-
|
|
30
|
+
// Latched rather than guarded on `status() === null`: a failed fetch leaves status null
|
|
31
|
+
// forever, so the old guard turned every later connect() into another native call — unbounded
|
|
32
|
+
// once connect() is reached from a change-detected expression instead of a field initializer.
|
|
33
|
+
if (!this.isAutoFetchStarted) {
|
|
34
|
+
this.isAutoFetchStarted = true;
|
|
35
|
+
this.get().catch((cause: unknown) => {
|
|
36
|
+
this.fetchError.set(
|
|
37
|
+
cause instanceof Error ? cause : new Error(String(cause)),
|
|
38
|
+
);
|
|
39
|
+
});
|
|
22
40
|
}
|
|
23
41
|
return this.status.asReadonly();
|
|
24
42
|
}
|
|
@@ -26,12 +44,14 @@ export class PermissionsService {
|
|
|
26
44
|
async get(): Promise<PermissionResponse> {
|
|
27
45
|
const response = await getTrackingPermissionsAsync();
|
|
28
46
|
this.status.set(response);
|
|
47
|
+
this.fetchError.set(null);
|
|
29
48
|
return response;
|
|
30
49
|
}
|
|
31
50
|
|
|
32
51
|
async request(): Promise<PermissionResponse> {
|
|
33
52
|
const response = await requestTrackingPermissionsAsync();
|
|
34
53
|
this.status.set(response);
|
|
54
|
+
this.fetchError.set(null);
|
|
35
55
|
return response;
|
|
36
56
|
}
|
|
37
57
|
}
|
|
@@ -16,6 +16,7 @@ export type INativeTrackingTransparencyModule = {
|
|
|
16
16
|
getPermissionsAsync?(): Promise<PermissionResponse>;
|
|
17
17
|
};
|
|
18
18
|
|
|
19
|
-
export const expoTrackingTransparency =
|
|
20
|
-
|
|
21
|
-
|
|
19
|
+
export const expoTrackingTransparency =
|
|
20
|
+
requireNativeModule<INativeTrackingTransparencyModule>(
|
|
21
|
+
EXPO_TRACKING_TRANSPARENCY_MODULE_NAME,
|
|
22
|
+
);
|
|
@@ -5,7 +5,11 @@
|
|
|
5
5
|
// NOT ported here — that helper is React-only (built on useState/useEffect), and this repo's
|
|
6
6
|
// convention is for each adapter to hand-roll its own permission hook instead (react/hooks,
|
|
7
7
|
// vue/composables, angular/services), same as brightness/cellular already do.
|
|
8
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
Platform,
|
|
10
|
+
PermissionStatus,
|
|
11
|
+
UnavailabilityError,
|
|
12
|
+
} from 'expo-modules-core';
|
|
9
13
|
import type { PermissionResponse } from 'expo-modules-core';
|
|
10
14
|
|
|
11
15
|
import { expoTrackingTransparency } from './native-module';
|
|
@@ -41,7 +45,10 @@ export async function requestTrackingPermissionsAsync(): Promise<PermissionRespo
|
|
|
41
45
|
return Promise.resolve(androidAndWebPermissionsResponse);
|
|
42
46
|
}
|
|
43
47
|
if (!expoTrackingTransparency.requestPermissionsAsync) {
|
|
44
|
-
throw new UnavailabilityError(
|
|
48
|
+
throw new UnavailabilityError(
|
|
49
|
+
NATIVE_MODULE_NAME,
|
|
50
|
+
'requestPermissionsAsync',
|
|
51
|
+
);
|
|
45
52
|
}
|
|
46
53
|
return await expoTrackingTransparency.requestPermissionsAsync();
|
|
47
54
|
}
|
|
@@ -2,6 +2,11 @@
|
|
|
2
2
|
// auto-fetches the current permission status on mount, then exposes get/request as imperative
|
|
3
3
|
// callbacks. Mirrors the shape @symbiote-native/cellular's own usePermissions ships in parallel,
|
|
4
4
|
// so both stay byte-for-byte consistent.
|
|
5
|
+
//
|
|
6
|
+
// The mount fetch has nobody to reject to, so its failure lands in the 4th tuple slot (`error`)
|
|
7
|
+
// instead of escaping as an unhandled rejection: a null status with a non-null error is how a
|
|
8
|
+
// consumer tells "the fetch failed" from "not fetched yet". get()/request() still reject to their
|
|
9
|
+
// direct caller — only the automatic call is made safe.
|
|
5
10
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
6
11
|
import {
|
|
7
12
|
getTrackingPermissionsAsync,
|
|
@@ -13,29 +18,40 @@ export function usePermissions(): [
|
|
|
13
18
|
PermissionResponse | null,
|
|
14
19
|
() => Promise<PermissionResponse>,
|
|
15
20
|
() => Promise<PermissionResponse>,
|
|
21
|
+
Error | null,
|
|
16
22
|
] {
|
|
17
23
|
const isMounted = useRef(true);
|
|
18
24
|
const [status, setStatus] = useState<PermissionResponse | null>(null);
|
|
25
|
+
const [error, setError] = useState<Error | null>(null);
|
|
19
26
|
|
|
20
27
|
const getPermission = useCallback(async () => {
|
|
21
28
|
const response = await getTrackingPermissionsAsync();
|
|
22
|
-
if (isMounted.current)
|
|
29
|
+
if (isMounted.current) {
|
|
30
|
+
setStatus(response);
|
|
31
|
+
setError(null);
|
|
32
|
+
}
|
|
23
33
|
return response;
|
|
24
34
|
}, []);
|
|
25
35
|
|
|
26
36
|
const requestPermission = useCallback(async () => {
|
|
27
37
|
const response = await requestTrackingPermissionsAsync();
|
|
28
|
-
if (isMounted.current)
|
|
38
|
+
if (isMounted.current) {
|
|
39
|
+
setStatus(response);
|
|
40
|
+
setError(null);
|
|
41
|
+
}
|
|
29
42
|
return response;
|
|
30
43
|
}, []);
|
|
31
44
|
|
|
32
45
|
useEffect(() => {
|
|
33
46
|
isMounted.current = true;
|
|
34
|
-
|
|
47
|
+
getPermission().catch((cause: unknown) => {
|
|
48
|
+
if (isMounted.current)
|
|
49
|
+
setError(cause instanceof Error ? cause : new Error(String(cause)));
|
|
50
|
+
});
|
|
35
51
|
return () => {
|
|
36
52
|
isMounted.current = false;
|
|
37
53
|
};
|
|
38
54
|
}, [getPermission]);
|
|
39
55
|
|
|
40
|
-
return [status, requestPermission, getPermission];
|
|
56
|
+
return [status, requestPermission, getPermission, error];
|
|
41
57
|
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// @symbiote-native/tracking-transparency/solid: the Solid entry over the framework-agnostic core.
|
|
2
|
+
// Same reasoning as the React, Vue and Svelte entries — only the permission surface gets a
|
|
3
|
+
// lifecycle primitive, every other export is a plain re-export — mirrors the lifecycle-bucket
|
|
4
|
+
// naming convention of adapters/solid/src/primitives (never `hooks/`/`composables/`/`runes/`,
|
|
5
|
+
// those are React's, Vue's and Svelte's terms).
|
|
6
|
+
|
|
7
|
+
export { createPermissions } from './primitives/create-permissions';
|
|
8
|
+
export * from '../core';
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// createPermissions — the Solid twin of React's `usePermissions` hook, Vue's composable and
|
|
2
|
+
// Svelte's rune, over the framework-agnostic core (core/tracking-transparency.ts). Auto-fetches
|
|
3
|
+
// the current permission status, then exposes get/request as imperative callbacks.
|
|
4
|
+
//
|
|
5
|
+
// `primitives/` and `create*`, never `hooks/`+`use*`: Solid's ecosystem calls a composable
|
|
6
|
+
// reactive function a PRIMITIVE and reserves `use*` for consuming something that already exists.
|
|
7
|
+
// Full rationale in adapters/solid/src/primitives/create-color-scheme.ts.
|
|
8
|
+
//
|
|
9
|
+
// Returns ACCESSORS, never snapshots: a Solid component body runs ONCE, so returned values would
|
|
10
|
+
// pin the caller to the null pre-fetch state forever.
|
|
11
|
+
//
|
|
12
|
+
// The auto-fetch fires from the primitive body rather than a mount effect — the shape
|
|
13
|
+
// `createResource` uses, and the reason this primitive needs no lifecycle hook at all. Its failure
|
|
14
|
+
// lands in `error` instead of escaping as an unhandled rejection: a null status with a non-null
|
|
15
|
+
// error is how a consumer tells "the fetch failed" from "not fetched yet". get()/request() still
|
|
16
|
+
// reject to their direct caller — only the automatic call is made safe.
|
|
17
|
+
//
|
|
18
|
+
// No isMounted guard around the late write, unlike React's hook: once the owning root is disposed
|
|
19
|
+
// nothing is subscribed to these signals any more, so a resolution arriving afterwards is inert.
|
|
20
|
+
|
|
21
|
+
import { createSignal, type Accessor } from 'solid-js';
|
|
22
|
+
import {
|
|
23
|
+
getTrackingPermissionsAsync,
|
|
24
|
+
requestTrackingPermissionsAsync,
|
|
25
|
+
type PermissionResponse,
|
|
26
|
+
} from '../../core';
|
|
27
|
+
|
|
28
|
+
export function createPermissions(): {
|
|
29
|
+
status: Accessor<PermissionResponse | null>;
|
|
30
|
+
error: Accessor<Error | null>;
|
|
31
|
+
request: () => Promise<PermissionResponse>;
|
|
32
|
+
get: () => Promise<PermissionResponse>;
|
|
33
|
+
} {
|
|
34
|
+
const [status, setStatus] = createSignal<PermissionResponse | null>(null);
|
|
35
|
+
const [error, setError] = createSignal<Error | null>(null);
|
|
36
|
+
|
|
37
|
+
const get = async (): Promise<PermissionResponse> => {
|
|
38
|
+
const response = await getTrackingPermissionsAsync();
|
|
39
|
+
setStatus(response);
|
|
40
|
+
setError(null);
|
|
41
|
+
return response;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const request = async (): Promise<PermissionResponse> => {
|
|
45
|
+
const response = await requestTrackingPermissionsAsync();
|
|
46
|
+
setStatus(response);
|
|
47
|
+
setError(null);
|
|
48
|
+
return response;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
get().catch((cause: unknown) => {
|
|
52
|
+
setError(cause instanceof Error ? cause : new Error(String(cause)));
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
return { status, error, request, get };
|
|
56
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
// @symbiote-native/tracking-transparency/svelte: the Svelte entry over the framework-agnostic core.
|
|
2
|
+
// Same reasoning as the React and Vue entries — only the permission surface gets a lifecycle rune,
|
|
3
|
+
// every other export is a plain re-export — mirrors the lifecycle-bucket naming convention of
|
|
4
|
+
// adapters/svelte/src/runes (never `hooks/`/`composables/`, those are React's and Vue's terms).
|
|
5
|
+
|
|
6
|
+
export { usePermissions } from './runes/use-permissions.svelte';
|
|
7
|
+
export * from '../core';
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// Svelte lifecycle wiring over the framework-agnostic core (core/tracking-transparency.ts) —
|
|
2
|
+
// auto-fetches the current permission status on mount, then exposes get/request as imperative
|
|
3
|
+
// callbacks. Just a one-shot get with no ongoing subscription, so `$effect` needs no cleanup.
|
|
4
|
+
//
|
|
5
|
+
// `.svelte.ts` (not `.ts`): runes ($state/$effect) only work in files with this extension
|
|
6
|
+
// outside an actual `.svelte` component. `status` is returned as a getter, NOT a bare
|
|
7
|
+
// `$state`: Svelte 5 reactivity is lexically scoped to the declaring module and doesn't
|
|
8
|
+
// survive being returned as a raw value from a plain function — mirrors how Vue's `Ref`
|
|
9
|
+
// needs `.value` to unwrap.
|
|
10
|
+
//
|
|
11
|
+
// The mount fetch has nobody to reject to, so its failure lands in `error` (a second boxed getter,
|
|
12
|
+
// same reason as `status`) instead of escaping as an unhandled rejection: a null status with a
|
|
13
|
+
// non-null error is how a consumer tells "the fetch failed" from "not fetched yet".
|
|
14
|
+
// get()/request() still reject to their direct caller — only the automatic call is made safe.
|
|
15
|
+
import {
|
|
16
|
+
getTrackingPermissionsAsync,
|
|
17
|
+
requestTrackingPermissionsAsync,
|
|
18
|
+
type PermissionResponse,
|
|
19
|
+
} from '../../core';
|
|
20
|
+
|
|
21
|
+
export function usePermissions(): {
|
|
22
|
+
readonly status: PermissionResponse | null;
|
|
23
|
+
readonly error: Error | null;
|
|
24
|
+
request: () => Promise<PermissionResponse>;
|
|
25
|
+
get: () => Promise<PermissionResponse>;
|
|
26
|
+
} {
|
|
27
|
+
let status = $state<PermissionResponse | null>(null);
|
|
28
|
+
let error = $state<Error | null>(null);
|
|
29
|
+
|
|
30
|
+
const get = async (): Promise<PermissionResponse> => {
|
|
31
|
+
const response = await getTrackingPermissionsAsync();
|
|
32
|
+
status = response;
|
|
33
|
+
error = null;
|
|
34
|
+
return response;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const request = async (): Promise<PermissionResponse> => {
|
|
38
|
+
const response = await requestTrackingPermissionsAsync();
|
|
39
|
+
status = response;
|
|
40
|
+
error = null;
|
|
41
|
+
return response;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
$effect(() => {
|
|
45
|
+
// `get` is a plain closure, not reactive state, so the effect has an empty dependency set and
|
|
46
|
+
// runs exactly once on mount — the twin of Vue's onMounted. The catch runs after the effect
|
|
47
|
+
// has settled, so writing `error` there adds no dependency and does not re-arm it.
|
|
48
|
+
get().catch((cause: unknown) => {
|
|
49
|
+
error = cause instanceof Error ? cause : new Error(String(cause));
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
return {
|
|
54
|
+
get status(): PermissionResponse | null {
|
|
55
|
+
return status;
|
|
56
|
+
},
|
|
57
|
+
get error(): Error | null {
|
|
58
|
+
return error;
|
|
59
|
+
},
|
|
60
|
+
request,
|
|
61
|
+
get,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
@@ -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" />
|
|
@@ -2,6 +2,11 @@
|
|
|
2
2
|
// auto-fetches the current permission status on mount (mirrors use-battery-level's
|
|
3
3
|
// onMounted/onUnmounted style, but there is no ongoing subscription to tear down here, just a
|
|
4
4
|
// one-shot get), then exposes get/request as imperative callbacks.
|
|
5
|
+
//
|
|
6
|
+
// The mount fetch has nobody to reject to, so its failure lands in the `error` ref instead of
|
|
7
|
+
// escaping as an unhandled rejection: a null status with a non-null error is how a consumer tells
|
|
8
|
+
// "the fetch failed" from "not fetched yet". get()/request() still reject to their direct caller —
|
|
9
|
+
// only the automatic call is made safe.
|
|
5
10
|
import { onMounted, ref, type Ref } from '@vue/runtime-core';
|
|
6
11
|
import {
|
|
7
12
|
getTrackingPermissionsAsync,
|
|
@@ -11,26 +16,32 @@ import {
|
|
|
11
16
|
|
|
12
17
|
export function usePermissions(): {
|
|
13
18
|
status: Ref<PermissionResponse | null>;
|
|
19
|
+
error: Ref<Error | null>;
|
|
14
20
|
request: () => Promise<PermissionResponse>;
|
|
15
21
|
get: () => Promise<PermissionResponse>;
|
|
16
22
|
} {
|
|
17
23
|
const status = ref<PermissionResponse | null>(null);
|
|
24
|
+
const error = ref<Error | null>(null);
|
|
18
25
|
|
|
19
26
|
const get = async () => {
|
|
20
27
|
const response = await getTrackingPermissionsAsync();
|
|
21
28
|
status.value = response;
|
|
29
|
+
error.value = null;
|
|
22
30
|
return response;
|
|
23
31
|
};
|
|
24
32
|
|
|
25
33
|
const request = async () => {
|
|
26
34
|
const response = await requestTrackingPermissionsAsync();
|
|
27
35
|
status.value = response;
|
|
36
|
+
error.value = null;
|
|
28
37
|
return response;
|
|
29
38
|
};
|
|
30
39
|
|
|
31
40
|
onMounted(() => {
|
|
32
|
-
|
|
41
|
+
get().catch((cause: unknown) => {
|
|
42
|
+
error.value = cause instanceof Error ? cause : new Error(String(cause));
|
|
43
|
+
});
|
|
33
44
|
});
|
|
34
45
|
|
|
35
|
-
return { status, request, get };
|
|
46
|
+
return { status, error, request, get };
|
|
36
47
|
}
|