@symbiote-native/age-range 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 A. Prokopenko
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,140 @@
1
+ # @symbiote-native/age-range
2
+
3
+ Show age-appropriate content, or meet an age-assurance law, without collecting a birth date
4
+ yourself: ask the platform. One API for every [SymbioteNative](../../README.md) adapter (React,
5
+ Vue, Svelte, Solid and Angular).
6
+
7
+ It wraps [`expo-age-range`](https://github.com/expo/expo/tree/main/packages/expo-age-range)
8
+ (Apple's Declared Age Range API on iOS 26+, and Google Play's Age Signals). Like
9
+ [`@symbiote-native/application`](../application), everything here is a one-shot async call (or a
10
+ single sync setter) with no per-instance state, so there is no hook, composable or service to wrap:
11
+ every adapter entry point is a plain re-export of the same `core`.
12
+
13
+ ## Install
14
+
15
+ **New app:**
16
+
17
+ ```bash
18
+ npx @symbiote-native/cli new my-app --age-range
19
+ ```
20
+
21
+ **Existing SymbioteNative app:**
22
+
23
+ ```bash
24
+ npx @symbiote-native/cli add --age-range
25
+ ```
26
+
27
+ Either way: installs `@symbiote-native/age-range` and wires the native autolinking automatically,
28
+ see [`@symbiote-native/cli`](../cli).
29
+
30
+ <details>
31
+ <summary>Manual install (no CLI, wiring native autolinking by hand)</summary>
32
+
33
+ ```bash
34
+ npm install @symbiote-native/age-range
35
+ ```
36
+
37
+ `expo-age-range` and `expo-modules-core` come along as regular dependencies, pinned to exact
38
+ versions. Never install either yourself, and never add the `expo` meta-package to your project
39
+ (it bundles its own Metro/Babel pipeline, which conflicts with this project's own).
40
+
41
+ ## Required one-time step: native autolinking wiring
42
+
43
+ Unlike a plain RN native module, `expo-age-range`'s native code is discovered by
44
+ `expo-modules-autolinking`, not RN's own `react-native.config.cjs` mechanism. This needs wiring
45
+ into the native host app **once**, covering this package and every other `expo-modules-core`
46
+ package with zero further changes:
47
+
48
+ | Platform | Touches |
49
+ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
50
+ | iOS | `ios/Podfile`, add `use_expo_modules!` |
51
+ | iOS | `AppDelegate.swift`, Expo's runtime-bootstrap hook |
52
+ | Android | `settings.gradle` / `app/build.gradle`, resolve and include the Expo Gradle projects |
53
+ | 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) |
54
+
55
+ Full mechanics live in the `symbiote-expo-native-module` project skill. Reference
56
+ implementation: `examples/expo-react/ios/Podfile` and
57
+ `examples/expo-react/android/app/src/main/java/com/canaryexpo/MainApplication.kt`.
58
+
59
+ No app-level permission strings are needed. Apple's age-range prompt and Google Play's Age
60
+ Signals consent screen are both system UI, carrying no `Info.plist`/manifest entry of their own.
61
+
62
+ **iOS needs one entitlement, added by hand.** Build with Xcode 26 or later and add
63
+ `com.apple.developer.declared-age-range` (`<true/>`) to your app's `.entitlements` file.
64
+ `native-link.json` does not generate it.
65
+
66
+ </details>
67
+
68
+ ## Shape
69
+
70
+ ```
71
+ src/core/ requestAgeRangeAsync / isEligibleForAgeFeaturesAsync /
72
+ showSignificantUpdateAcknowledgmentAsync / getRequiredRegulatoryFeaturesAsync /
73
+ requestAgeSignalsAccessAsync / setFakeAgeSignals, plus IAgeRangeRequest /
74
+ IAgeRangeResponse / IAgeSignalsStatus / IFakeAgeSignals / IAgeRangeRegulatoryFeature.
75
+ native-module.ts resolves the native module via expo-modules-core's
76
+ requireNativeModule.
77
+ src/angular/ @symbiote-native/age-range/angular, export * from '../core'
78
+ ```
79
+
80
+ `./react`, `./vue`, `./svelte`, and `./solid` are `exports`-map aliases straight onto `src/core/`,
81
+ no physical per-framework file, since there's nothing to subscribe to or clean up. `./angular`
82
+ stays a physical file/subpath since Angular ships through a separate `ngc`/AOT build (`build-ngc/`).
83
+
84
+ ## API
85
+
86
+ ```ts
87
+ requestAgeRangeAsync(options: IAgeRangeRequest): Promise<IAgeRangeResponse>
88
+ isEligibleForAgeFeaturesAsync(): Promise<boolean | null>
89
+
90
+ showSignificantUpdateAcknowledgmentAsync(updateDescription: string): Promise<void> // ios 26.4+, no-op elsewhere
91
+ getRequiredRegulatoryFeaturesAsync(): Promise<IAgeRangeRegulatoryFeature[] | null> // ios 26.4+, null elsewhere
92
+
93
+ requestAgeSignalsAccessAsync(): Promise<IAgeSignalsStatus | null> // android only, null elsewhere
94
+ setFakeAgeSignals(fake: IFakeAgeSignals | null): void // android only, no-op elsewhere
95
+ ```
96
+
97
+ ```ts
98
+ import {
99
+ requestAgeRangeAsync,
100
+ isEligibleForAgeFeaturesAsync,
101
+ } from '@symbiote-native/age-range';
102
+ // or the framework-scoped entry points, identical surface, re-exported verbatim:
103
+ import { requestAgeRangeAsync } from '@symbiote-native/age-range/react';
104
+ import { requestAgeRangeAsync } from '@symbiote-native/age-range/vue';
105
+ import { requestAgeRangeAsync } from '@symbiote-native/age-range/angular';
106
+ ```
107
+
108
+ ## Notes
109
+
110
+ - **Recommended pattern**: call `isEligibleForAgeFeaturesAsync`/`getRequiredRegulatoryFeaturesAsync`
111
+ first and only prompt with `requestAgeRangeAsync` when the result says the user actually needs
112
+ it. Both resolve `null` where the OS can't answer (iOS below 26.2/26.4, Android, web), and
113
+ `null` means "unknown", never "not required".
114
+ - On Android, `requestAgeSignalsAccessAsync` must resolve `'SHARED'` before `requestAgeRangeAsync`
115
+ reports anything besides `null` fields. Play Age Signals requires the separate consent screen
116
+ first, unlike iOS where the consent is folded into `requestAgeRangeAsync` itself.
117
+ - `setFakeAgeSignals` only works in a debuggable build. The native side throws otherwise unless
118
+ the argument is `null` (which always goes back to real signals).
119
+ - **Test on a real device.** Simulators and emulators may not behave as expected; the canary
120
+ `AgeRangeScreen` in the `examples/expo-*` apps exercises every call.
121
+
122
+ ## Common questions
123
+
124
+ - **iOS `IOS_ENTITLEMENT_ERROR`.** Add `com.apple.developer.declared-age-range`; the Simulator lacks the feature.
125
+ - **Xcode?** 26.0 or later.
126
+ - **Android `API_NOT_AVAILABLE` / `PLAY_STORE_NOT_FOUND` / `NETWORK_ERROR`.** Needs an up-to-date
127
+ Play Store, a network, and a Google Play install.
128
+ - **No range on Android.** Reported only while the user consents to share it.
129
+
130
+ Sources: [Expo docs: AgeRange](https://docs.expo.dev/versions/latest/sdk/age-range/),
131
+ [expo/expo#46365](https://github.com/expo/expo/issues/46365),
132
+ [AgeSignalsException](https://developer.android.com/google/play/age-signals/reference/com/google/android/play/agesignals/AgeSignalsException).
133
+
134
+ ## Test it
135
+
136
+ No Fabric/Descriptor angle at all, every export here is a one-shot async function or a plain sync
137
+ setter, never a view or per-instance state. Tests inject a fake native-module object in place of
138
+ the real `requireNativeModule` resolution (`src/core/age-range.test.ts`, `vitest`), no
139
+ `installFabric()`, no ViewConfig. Native behavior itself is verified on-device (see the parent
140
+ [README](../../README.md) for the project's testing model).
@@ -0,0 +1 @@
1
+ export * from '../core';
@@ -0,0 +1,2 @@
1
+ // No per-instance state or event stream to wrap in an injectable service, plain re-export
2
+ export * from '../core/index.js';
@@ -0,0 +1,12 @@
1
+ import type { IAgeRangeRegulatoryFeature, IAgeRangeRequest, IAgeRangeResponse, IAgeSignalsStatus, IFakeAgeSignals } from './types';
2
+ /** Prompts the user to share their age range; the OS may cache the response for later requests */
3
+ export declare function requestAgeRangeAsync(options: IAgeRangeRequest): Promise<IAgeRangeResponse>;
4
+ export declare function isEligibleForAgeFeaturesAsync(): Promise<boolean | null>;
5
+ /** @platform ios, no-op elsewhere */
6
+ export declare function showSignificantUpdateAcknowledgmentAsync(updateDescription: string): Promise<void>;
7
+ /** @platform ios, `null` elsewhere means unknown rather than none required */
8
+ export declare function getRequiredRegulatoryFeaturesAsync(): Promise<IAgeRangeRegulatoryFeature[] | null>;
9
+ /** @platform android, `null` elsewhere means unknown rather than not shared */
10
+ export declare function requestAgeSignalsAccessAsync(): Promise<IAgeSignalsStatus | null>;
11
+ /** @platform android, no-op elsewhere; throws off a non-debuggable build unless `fake` is `null` */
12
+ export declare function setFakeAgeSignals(fake: IFakeAgeSignals | null): void;
@@ -0,0 +1,38 @@
1
+ import { Platform } from 'expo-modules-core';
2
+ import { expoAgeRange } from './native-module.js';
3
+ /** Prompts the user to share their age range; the OS may cache the response for later requests */
4
+ export async function requestAgeRangeAsync(options) {
5
+ return expoAgeRange.requestAgeRangeAsync(options);
6
+ }
7
+ export async function isEligibleForAgeFeaturesAsync() {
8
+ return expoAgeRange.isEligibleForAgeFeaturesAsync();
9
+ }
10
+ /** @platform ios, no-op elsewhere */
11
+ export async function showSignificantUpdateAcknowledgmentAsync(updateDescription) {
12
+ if (Platform.OS !== 'ios' ||
13
+ !expoAgeRange.showSignificantUpdateAcknowledgmentAsync) {
14
+ return;
15
+ }
16
+ await expoAgeRange.showSignificantUpdateAcknowledgmentAsync(updateDescription);
17
+ }
18
+ /** @platform ios, `null` elsewhere means unknown rather than none required */
19
+ export async function getRequiredRegulatoryFeaturesAsync() {
20
+ if (Platform.OS !== 'ios' ||
21
+ !expoAgeRange.getRequiredRegulatoryFeaturesAsync) {
22
+ return null;
23
+ }
24
+ return expoAgeRange.getRequiredRegulatoryFeaturesAsync();
25
+ }
26
+ /** @platform android, `null` elsewhere means unknown rather than not shared */
27
+ export async function requestAgeSignalsAccessAsync() {
28
+ if (Platform.OS !== 'android' || !expoAgeRange.requestAgeSignalsAccessAsync) {
29
+ return null;
30
+ }
31
+ return expoAgeRange.requestAgeSignalsAccessAsync();
32
+ }
33
+ /** @platform android, no-op elsewhere; throws off a non-debuggable build unless `fake` is `null` */
34
+ export function setFakeAgeSignals(fake) {
35
+ if (Platform.OS !== 'android' || !expoAgeRange.setFakeAgeSignals)
36
+ return;
37
+ expoAgeRange.setFakeAgeSignals(fake);
38
+ }
@@ -0,0 +1,2 @@
1
+ export { getRequiredRegulatoryFeaturesAsync, isEligibleForAgeFeaturesAsync, requestAgeRangeAsync, requestAgeSignalsAccessAsync, setFakeAgeSignals, showSignificantUpdateAcknowledgmentAsync, } from './age-range';
2
+ export type { IAgeRangeRegulatoryFeature, IAgeRangeRequest, IAgeRangeResponse, IAgeSignalsStatus, IFakeAgeSignals, } from './types';
@@ -0,0 +1 @@
1
+ export { getRequiredRegulatoryFeaturesAsync, isEligibleForAgeFeaturesAsync, requestAgeRangeAsync, requestAgeSignalsAccessAsync, setFakeAgeSignals, showSignificantUpdateAcknowledgmentAsync, } from './age-range.js';
@@ -0,0 +1,10 @@
1
+ import type { IAgeRangeRegulatoryFeature, IAgeRangeRequest, IAgeRangeResponse, IAgeSignalsStatus, IFakeAgeSignals } from './types';
2
+ export type INativeAgeRangeModule = {
3
+ requestAgeRangeAsync(options: IAgeRangeRequest): Promise<IAgeRangeResponse>;
4
+ isEligibleForAgeFeaturesAsync(): Promise<boolean | null>;
5
+ showSignificantUpdateAcknowledgmentAsync?(updateDescription: string): Promise<void>;
6
+ getRequiredRegulatoryFeaturesAsync?(): Promise<IAgeRangeRegulatoryFeature[] | null>;
7
+ requestAgeSignalsAccessAsync?(): Promise<IAgeSignalsStatus | null>;
8
+ setFakeAgeSignals?(fake: IFakeAgeSignals | null): void;
9
+ };
10
+ export declare const expoAgeRange: INativeAgeRangeModule;
@@ -0,0 +1,3 @@
1
+ import { requireNativeModule } from 'expo-modules-core';
2
+ const EXPO_AGE_RANGE_MODULE_NAME = 'ExpoAgeRange';
3
+ export const expoAgeRange = requireNativeModule(EXPO_AGE_RANGE_MODULE_NAME);
@@ -0,0 +1,50 @@
1
+ /** @platform ios */
2
+ export type IAgeRangeRequest = {
3
+ /** The required minimum age for your app */
4
+ threshold1: number;
5
+ threshold2?: number;
6
+ threshold3?: number;
7
+ };
8
+ export type IAgeRangeResponse = {
9
+ lowerBound: number | null;
10
+ upperBound: number | null;
11
+ /** @platform ios */
12
+ ageRangeDeclaration?: 'selfDeclared' | 'guardianDeclared' | 'confirmed' | null;
13
+ /** @platform ios */
14
+ activeParentalControls?: string[];
15
+ /** @platform android */
16
+ installId?: string | null;
17
+ /** @platform android */
18
+ ageRangeSource?: 'TIER_A' | 'TIER_B' | 'TIER_C' | 'TIER_D' | null;
19
+ /** @platform android */
20
+ significantChangeStatus?: 'APPROVED' | 'PENDING' | 'DECLINED' | null;
21
+ /** @platform android */
22
+ significantChangeApprovalDate?: number | null;
23
+ /** Deprecated, use `significantChangeApprovalDate`. @platform android */
24
+ mostRecentApprovalDate?: number | null;
25
+ };
26
+ /** @platform android */
27
+ export type IAgeSignalsStatus = 'SHARED' | 'NOT_SHARED' | 'VERIFICATION_REQUIRED';
28
+ /** @platform android, omitted fields report as `null` */
29
+ export type IFakeAgeSignals = {
30
+ lowerBound?: number | null;
31
+ upperBound?: number | null;
32
+ installId?: string | null;
33
+ ageRangeSource?: 'TIER_A' | 'TIER_B' | 'TIER_C' | 'TIER_D' | null;
34
+ significantChangeStatus?: 'APPROVED' | 'PENDING' | 'DECLINED' | null;
35
+ significantChangeApprovalDate?: number | null;
36
+ ageSignalsStatus?: IAgeSignalsStatus | null;
37
+ errorCode?: never;
38
+ } | {
39
+ /** https://developer.android.com/google/play/age-signals/handle-errors */
40
+ errorCode: number;
41
+ lowerBound?: never;
42
+ upperBound?: never;
43
+ installId?: never;
44
+ ageRangeSource?: never;
45
+ significantChangeStatus?: never;
46
+ significantChangeApprovalDate?: never;
47
+ ageSignalsStatus?: never;
48
+ };
49
+ /** @platform ios */
50
+ export type IAgeRangeRegulatoryFeature = 'declaredAgeRangeRequired' | 'significantAppChangeRequiresAdultNotification' | 'significantAppChangeRequiresParentalConsent';
@@ -0,0 +1,3 @@
1
+ // Ported verbatim from expo-age-range's ExpoAgeRange.types.ts (sdk-57), renamed with this
2
+ // repo's `I`-prefix convention
3
+ export {};
@@ -0,0 +1 @@
1
+ export * from '../core';
@@ -0,0 +1,3 @@
1
+ // No per-instance state or event stream to wrap in an injectable service, plain re-export
2
+ export * from '../core';
3
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/angular/index.ts"],"names":[],"mappings":"AAAA,0FAA0F;AAC1F,cAAc,SAAS,CAAC"}
@@ -0,0 +1,12 @@
1
+ import type { IAgeRangeRegulatoryFeature, IAgeRangeRequest, IAgeRangeResponse, IAgeSignalsStatus, IFakeAgeSignals } from './types';
2
+ /** Prompts the user to share their age range; the OS may cache the response for later requests */
3
+ export declare function requestAgeRangeAsync(options: IAgeRangeRequest): Promise<IAgeRangeResponse>;
4
+ export declare function isEligibleForAgeFeaturesAsync(): Promise<boolean | null>;
5
+ /** @platform ios, no-op elsewhere */
6
+ export declare function showSignificantUpdateAcknowledgmentAsync(updateDescription: string): Promise<void>;
7
+ /** @platform ios, `null` elsewhere means unknown rather than none required */
8
+ export declare function getRequiredRegulatoryFeaturesAsync(): Promise<IAgeRangeRegulatoryFeature[] | null>;
9
+ /** @platform android, `null` elsewhere means unknown rather than not shared */
10
+ export declare function requestAgeSignalsAccessAsync(): Promise<IAgeSignalsStatus | null>;
11
+ /** @platform android, no-op elsewhere; throws off a non-debuggable build unless `fake` is `null` */
12
+ export declare function setFakeAgeSignals(fake: IFakeAgeSignals | null): void;
@@ -0,0 +1,39 @@
1
+ import { Platform } from 'expo-modules-core';
2
+ import { expoAgeRange } from './native-module';
3
+ /** Prompts the user to share their age range; the OS may cache the response for later requests */
4
+ export async function requestAgeRangeAsync(options) {
5
+ return expoAgeRange.requestAgeRangeAsync(options);
6
+ }
7
+ export async function isEligibleForAgeFeaturesAsync() {
8
+ return expoAgeRange.isEligibleForAgeFeaturesAsync();
9
+ }
10
+ /** @platform ios, no-op elsewhere */
11
+ export async function showSignificantUpdateAcknowledgmentAsync(updateDescription) {
12
+ if (Platform.OS !== 'ios' ||
13
+ !expoAgeRange.showSignificantUpdateAcknowledgmentAsync) {
14
+ return;
15
+ }
16
+ await expoAgeRange.showSignificantUpdateAcknowledgmentAsync(updateDescription);
17
+ }
18
+ /** @platform ios, `null` elsewhere means unknown rather than none required */
19
+ export async function getRequiredRegulatoryFeaturesAsync() {
20
+ if (Platform.OS !== 'ios' ||
21
+ !expoAgeRange.getRequiredRegulatoryFeaturesAsync) {
22
+ return null;
23
+ }
24
+ return expoAgeRange.getRequiredRegulatoryFeaturesAsync();
25
+ }
26
+ /** @platform android, `null` elsewhere means unknown rather than not shared */
27
+ export async function requestAgeSignalsAccessAsync() {
28
+ if (Platform.OS !== 'android' || !expoAgeRange.requestAgeSignalsAccessAsync) {
29
+ return null;
30
+ }
31
+ return expoAgeRange.requestAgeSignalsAccessAsync();
32
+ }
33
+ /** @platform android, no-op elsewhere; throws off a non-debuggable build unless `fake` is `null` */
34
+ export function setFakeAgeSignals(fake) {
35
+ if (Platform.OS !== 'android' || !expoAgeRange.setFakeAgeSignals)
36
+ return;
37
+ expoAgeRange.setFakeAgeSignals(fake);
38
+ }
39
+ //# sourceMappingURL=age-range.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"age-range.js","sourceRoot":"","sources":["../../src/core/age-range.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AAC7C,OAAO,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAS/C,kGAAkG;AAClG,MAAM,CAAC,KAAK,UAAU,oBAAoB,CACxC,OAAyB;IAEzB,OAAO,YAAY,CAAC,oBAAoB,CAAC,OAAO,CAAC,CAAC;AACpD,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,6BAA6B;IACjD,OAAO,YAAY,CAAC,6BAA6B,EAAE,CAAC;AACtD,CAAC;AAED,qCAAqC;AACrC,MAAM,CAAC,KAAK,UAAU,wCAAwC,CAC5D,iBAAyB;IAEzB,IACE,QAAQ,CAAC,EAAE,KAAK,KAAK;QACrB,CAAC,YAAY,CAAC,wCAAwC,EACtD,CAAC;QACD,OAAO;IACT,CAAC;IACD,MAAM,YAAY,CAAC,wCAAwC,CACzD,iBAAiB,CAClB,CAAC;AACJ,CAAC;AAED,8EAA8E;AAC9E,MAAM,CAAC,KAAK,UAAU,kCAAkC;IAGtD,IACE,QAAQ,CAAC,EAAE,KAAK,KAAK;QACrB,CAAC,YAAY,CAAC,kCAAkC,EAChD,CAAC;QACD,OAAO,IAAI,CAAC;IACd,CAAC;IACD,OAAO,YAAY,CAAC,kCAAkC,EAAE,CAAC;AAC3D,CAAC;AAED,+EAA+E;AAC/E,MAAM,CAAC,KAAK,UAAU,4BAA4B;IAChD,IAAI,QAAQ,CAAC,EAAE,KAAK,SAAS,IAAI,CAAC,YAAY,CAAC,4BAA4B,EAAE,CAAC;QAC5E,OAAO,IAAI,CAAC;IACd,CAAC;IACD,OAAO,YAAY,CAAC,4BAA4B,EAAE,CAAC;AACrD,CAAC;AAED,oGAAoG;AACpG,MAAM,UAAU,iBAAiB,CAAC,IAA4B;IAC5D,IAAI,QAAQ,CAAC,EAAE,KAAK,SAAS,IAAI,CAAC,YAAY,CAAC,iBAAiB;QAAE,OAAO;IACzE,YAAY,CAAC,iBAAiB,CAAC,IAAI,CAAC,CAAC;AACvC,CAAC"}
@@ -0,0 +1,2 @@
1
+ export { getRequiredRegulatoryFeaturesAsync, isEligibleForAgeFeaturesAsync, requestAgeRangeAsync, requestAgeSignalsAccessAsync, setFakeAgeSignals, showSignificantUpdateAcknowledgmentAsync, } from './age-range';
2
+ export type { IAgeRangeRegulatoryFeature, IAgeRangeRequest, IAgeRangeResponse, IAgeSignalsStatus, IFakeAgeSignals, } from './types';
@@ -0,0 +1,2 @@
1
+ export { getRequiredRegulatoryFeaturesAsync, isEligibleForAgeFeaturesAsync, requestAgeRangeAsync, requestAgeSignalsAccessAsync, setFakeAgeSignals, showSignificantUpdateAcknowledgmentAsync, } from './age-range';
2
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/core/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,kCAAkC,EAClC,6BAA6B,EAC7B,oBAAoB,EACpB,4BAA4B,EAC5B,iBAAiB,EACjB,wCAAwC,GACzC,MAAM,aAAa,CAAC"}
@@ -0,0 +1,10 @@
1
+ import type { IAgeRangeRegulatoryFeature, IAgeRangeRequest, IAgeRangeResponse, IAgeSignalsStatus, IFakeAgeSignals } from './types';
2
+ export type INativeAgeRangeModule = {
3
+ requestAgeRangeAsync(options: IAgeRangeRequest): Promise<IAgeRangeResponse>;
4
+ isEligibleForAgeFeaturesAsync(): Promise<boolean | null>;
5
+ showSignificantUpdateAcknowledgmentAsync?(updateDescription: string): Promise<void>;
6
+ getRequiredRegulatoryFeaturesAsync?(): Promise<IAgeRangeRegulatoryFeature[] | null>;
7
+ requestAgeSignalsAccessAsync?(): Promise<IAgeSignalsStatus | null>;
8
+ setFakeAgeSignals?(fake: IFakeAgeSignals | null): void;
9
+ };
10
+ export declare const expoAgeRange: INativeAgeRangeModule;
@@ -0,0 +1,4 @@
1
+ import { requireNativeModule } from 'expo-modules-core';
2
+ const EXPO_AGE_RANGE_MODULE_NAME = 'ExpoAgeRange';
3
+ export const expoAgeRange = requireNativeModule(EXPO_AGE_RANGE_MODULE_NAME);
4
+ //# sourceMappingURL=native-module.js.map
@@ -0,0 +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;AASxD,MAAM,0BAA0B,GAAG,cAAc,CAAC;AAiBlD,MAAM,CAAC,MAAM,YAAY,GAAG,mBAAmB,CAC7C,0BAA0B,CAC3B,CAAC"}
@@ -0,0 +1,50 @@
1
+ /** @platform ios */
2
+ export type IAgeRangeRequest = {
3
+ /** The required minimum age for your app */
4
+ threshold1: number;
5
+ threshold2?: number;
6
+ threshold3?: number;
7
+ };
8
+ export type IAgeRangeResponse = {
9
+ lowerBound: number | null;
10
+ upperBound: number | null;
11
+ /** @platform ios */
12
+ ageRangeDeclaration?: 'selfDeclared' | 'guardianDeclared' | 'confirmed' | null;
13
+ /** @platform ios */
14
+ activeParentalControls?: string[];
15
+ /** @platform android */
16
+ installId?: string | null;
17
+ /** @platform android */
18
+ ageRangeSource?: 'TIER_A' | 'TIER_B' | 'TIER_C' | 'TIER_D' | null;
19
+ /** @platform android */
20
+ significantChangeStatus?: 'APPROVED' | 'PENDING' | 'DECLINED' | null;
21
+ /** @platform android */
22
+ significantChangeApprovalDate?: number | null;
23
+ /** Deprecated, use `significantChangeApprovalDate`. @platform android */
24
+ mostRecentApprovalDate?: number | null;
25
+ };
26
+ /** @platform android */
27
+ export type IAgeSignalsStatus = 'SHARED' | 'NOT_SHARED' | 'VERIFICATION_REQUIRED';
28
+ /** @platform android, omitted fields report as `null` */
29
+ export type IFakeAgeSignals = {
30
+ lowerBound?: number | null;
31
+ upperBound?: number | null;
32
+ installId?: string | null;
33
+ ageRangeSource?: 'TIER_A' | 'TIER_B' | 'TIER_C' | 'TIER_D' | null;
34
+ significantChangeStatus?: 'APPROVED' | 'PENDING' | 'DECLINED' | null;
35
+ significantChangeApprovalDate?: number | null;
36
+ ageSignalsStatus?: IAgeSignalsStatus | null;
37
+ errorCode?: never;
38
+ } | {
39
+ /** https://developer.android.com/google/play/age-signals/handle-errors */
40
+ errorCode: number;
41
+ lowerBound?: never;
42
+ upperBound?: never;
43
+ installId?: never;
44
+ ageRangeSource?: never;
45
+ significantChangeStatus?: never;
46
+ significantChangeApprovalDate?: never;
47
+ ageSignalsStatus?: never;
48
+ };
49
+ /** @platform ios */
50
+ export type IAgeRangeRegulatoryFeature = 'declaredAgeRangeRequired' | 'significantAppChangeRequiresAdultNotification' | 'significantAppChangeRequiresParentalConsent';
@@ -0,0 +1,4 @@
1
+ // Ported verbatim from expo-age-range's ExpoAgeRange.types.ts (sdk-57), renamed with this
2
+ // repo's `I`-prefix convention
3
+ export {};
4
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/core/types.ts"],"names":[],"mappings":"AAAA,0FAA0F;AAC1F,+BAA+B"}
@@ -0,0 +1,12 @@
1
+ {
2
+ "android": {
3
+ "gradleProjectName": "expo-age-range",
4
+ "modules": [
5
+ {
6
+ "importPath": "expo.modules.agerange.AgeRangeModule",
7
+ "className": "AgeRangeModule",
8
+ "nativeName": "ExpoAgeRange"
9
+ }
10
+ ]
11
+ }
12
+ }
package/package.json ADDED
@@ -0,0 +1,150 @@
1
+ {
2
+ "name": "@symbiote-native/age-range",
3
+ "version": "0.1.0",
4
+ "description": "expo-age-range wrapped for SymbioteNative — one framework-agnostic core, built once and reachable from the React, Vue, Svelte, Solid, and Angular adapters. Apple's Declared Age Range API and Google Play's Age Signals.",
5
+ "license": "MIT",
6
+ "keywords": [
7
+ "react-native",
8
+ "symbiote-native",
9
+ "expo-age-range",
10
+ "react",
11
+ "vue",
12
+ "svelte",
13
+ "solid",
14
+ "angular",
15
+ "age-range",
16
+ "age-signals"
17
+ ],
18
+ "repository": {
19
+ "type": "git",
20
+ "url": "git+https://github.com/OneEyed1366/symbiote-native.git",
21
+ "directory": "packages/age-range"
22
+ },
23
+ "homepage": "https://github.com/OneEyed1366/symbiote-native/tree/master/packages/age-range#readme",
24
+ "bugs": {
25
+ "url": "https://github.com/OneEyed1366/symbiote-native/issues"
26
+ },
27
+ "author": "Andrey Prokopenko <psevdoproger@gmail.com>",
28
+ "type": "module",
29
+ "main": "./build/core/index.js",
30
+ "module": "./build/core/index.js",
31
+ "types": "./build/core/index.d.ts",
32
+ "exports": {
33
+ ".": {
34
+ "types": "./build/core/index.d.ts",
35
+ "default": "./build/core/index.js"
36
+ },
37
+ "./vue": {
38
+ "types": "./build/core/index.d.ts",
39
+ "default": "./build/core/index.js"
40
+ },
41
+ "./react": {
42
+ "types": "./build/core/index.d.ts",
43
+ "default": "./build/core/index.js"
44
+ },
45
+ "./svelte": {
46
+ "types": "./build/core/index.d.ts",
47
+ "default": "./build/core/index.js"
48
+ },
49
+ "./solid": {
50
+ "types": "./build/core/index.d.ts",
51
+ "default": "./build/core/index.js"
52
+ },
53
+ "./angular": {
54
+ "types": "./build-ngc/angular/index.d.ts",
55
+ "react-native": "./build-ngc/angular/index.js",
56
+ "default": "./src/angular/index.ts"
57
+ }
58
+ },
59
+ "files": [
60
+ "src",
61
+ "build",
62
+ "build-ngc",
63
+ "native-link.json",
64
+ "!src/**/*.test.*",
65
+ "!src/**/*.spec.*",
66
+ "!src/**/*.detox.*"
67
+ ],
68
+ "publishConfig": {
69
+ "access": "public"
70
+ },
71
+ "dependencies": {
72
+ "expo-age-range": "57.0.5",
73
+ "expo-modules-core": "57.0.20"
74
+ },
75
+ "peerDependencies": {
76
+ "@angular/core": ">=20",
77
+ "@vue/runtime-core": "^3.5.13",
78
+ "react": ">=19.0.0",
79
+ "react-native": ">=0.86",
80
+ "solid-js": ">=1.9.0",
81
+ "svelte": ">=5.56.0",
82
+ "vue": ">=3.5.0",
83
+ "@symbiote-native/angular": "^3.1.3",
84
+ "@symbiote-native/react": "^3.1.0",
85
+ "@symbiote-native/svelte": "^3.0.5",
86
+ "@symbiote-native/engine": "^1.4.0",
87
+ "@symbiote-native/vue": "^3.1.0",
88
+ "@symbiote-native/solid": "^3.0.5"
89
+ },
90
+ "peerDependenciesMeta": {
91
+ "@symbiote-native/angular": {
92
+ "optional": true
93
+ },
94
+ "@symbiote-native/react": {
95
+ "optional": true
96
+ },
97
+ "@symbiote-native/solid": {
98
+ "optional": true
99
+ },
100
+ "@symbiote-native/svelte": {
101
+ "optional": true
102
+ },
103
+ "@symbiote-native/vue": {
104
+ "optional": true
105
+ },
106
+ "@angular/core": {
107
+ "optional": true
108
+ },
109
+ "@vue/runtime-core": {
110
+ "optional": true
111
+ },
112
+ "react": {
113
+ "optional": true
114
+ },
115
+ "solid-js": {
116
+ "optional": true
117
+ },
118
+ "svelte": {
119
+ "optional": true
120
+ },
121
+ "vue": {
122
+ "optional": true
123
+ }
124
+ },
125
+ "devDependencies": {
126
+ "@angular/compiler": "~22.0.8",
127
+ "@angular/compiler-cli": "~22.0.8",
128
+ "@angular/core": "~22.0.8",
129
+ "@types/node": "^26.0.0",
130
+ "@types/react": "^19.2.0",
131
+ "@vue/runtime-core": "^3.5.13",
132
+ "react": "19.2.3",
133
+ "solid-js": "^1.9.14",
134
+ "svelte": "^5.56.0",
135
+ "typescript": "~6.0.0",
136
+ "@symbiote-native/engine": "1.4.0",
137
+ "@symbiote-native/test-utils": "0.4.5",
138
+ "@symbiote-native/angular": "3.1.3",
139
+ "@symbiote-native/solid": "3.0.5",
140
+ "@symbiote-native/react": "3.1.0",
141
+ "@symbiote-native/vue": "3.1.0",
142
+ "@symbiote-native/svelte": "3.0.5"
143
+ },
144
+ "scripts": {
145
+ "typecheck": "tsc --build",
146
+ "clean": "rm -rf build-ngc",
147
+ "ng:build": "pnpm run clean && ngc -p tsconfig.angular.json",
148
+ "format": "prettier --write \"src/**/*.{ts,tsx}\""
149
+ }
150
+ }
@@ -0,0 +1,2 @@
1
+ // No per-instance state or event stream to wrap in an injectable service, plain re-export
2
+ export * from '../core';
@@ -0,0 +1,62 @@
1
+ import { Platform } from 'expo-modules-core';
2
+ import { expoAgeRange } from './native-module';
3
+ import type {
4
+ IAgeRangeRegulatoryFeature,
5
+ IAgeRangeRequest,
6
+ IAgeRangeResponse,
7
+ IAgeSignalsStatus,
8
+ IFakeAgeSignals,
9
+ } from './types';
10
+
11
+ /** Prompts the user to share their age range; the OS may cache the response for later requests */
12
+ export async function requestAgeRangeAsync(
13
+ options: IAgeRangeRequest,
14
+ ): Promise<IAgeRangeResponse> {
15
+ return expoAgeRange.requestAgeRangeAsync(options);
16
+ }
17
+
18
+ export async function isEligibleForAgeFeaturesAsync(): Promise<boolean | null> {
19
+ return expoAgeRange.isEligibleForAgeFeaturesAsync();
20
+ }
21
+
22
+ /** @platform ios, no-op elsewhere */
23
+ export async function showSignificantUpdateAcknowledgmentAsync(
24
+ updateDescription: string,
25
+ ): Promise<void> {
26
+ if (
27
+ Platform.OS !== 'ios' ||
28
+ !expoAgeRange.showSignificantUpdateAcknowledgmentAsync
29
+ ) {
30
+ return;
31
+ }
32
+ await expoAgeRange.showSignificantUpdateAcknowledgmentAsync(
33
+ updateDescription,
34
+ );
35
+ }
36
+
37
+ /** @platform ios, `null` elsewhere means unknown rather than none required */
38
+ export async function getRequiredRegulatoryFeaturesAsync(): Promise<
39
+ IAgeRangeRegulatoryFeature[] | null
40
+ > {
41
+ if (
42
+ Platform.OS !== 'ios' ||
43
+ !expoAgeRange.getRequiredRegulatoryFeaturesAsync
44
+ ) {
45
+ return null;
46
+ }
47
+ return expoAgeRange.getRequiredRegulatoryFeaturesAsync();
48
+ }
49
+
50
+ /** @platform android, `null` elsewhere means unknown rather than not shared */
51
+ export async function requestAgeSignalsAccessAsync(): Promise<IAgeSignalsStatus | null> {
52
+ if (Platform.OS !== 'android' || !expoAgeRange.requestAgeSignalsAccessAsync) {
53
+ return null;
54
+ }
55
+ return expoAgeRange.requestAgeSignalsAccessAsync();
56
+ }
57
+
58
+ /** @platform android, no-op elsewhere; throws off a non-debuggable build unless `fake` is `null` */
59
+ export function setFakeAgeSignals(fake: IFakeAgeSignals | null): void {
60
+ if (Platform.OS !== 'android' || !expoAgeRange.setFakeAgeSignals) return;
61
+ expoAgeRange.setFakeAgeSignals(fake);
62
+ }
@@ -0,0 +1,15 @@
1
+ export {
2
+ getRequiredRegulatoryFeaturesAsync,
3
+ isEligibleForAgeFeaturesAsync,
4
+ requestAgeRangeAsync,
5
+ requestAgeSignalsAccessAsync,
6
+ setFakeAgeSignals,
7
+ showSignificantUpdateAcknowledgmentAsync,
8
+ } from './age-range';
9
+ export type {
10
+ IAgeRangeRegulatoryFeature,
11
+ IAgeRangeRequest,
12
+ IAgeRangeResponse,
13
+ IAgeSignalsStatus,
14
+ IFakeAgeSignals,
15
+ } from './types';
@@ -0,0 +1,29 @@
1
+ import { requireNativeModule } from 'expo-modules-core';
2
+ import type {
3
+ IAgeRangeRegulatoryFeature,
4
+ IAgeRangeRequest,
5
+ IAgeRangeResponse,
6
+ IAgeSignalsStatus,
7
+ IFakeAgeSignals,
8
+ } from './types';
9
+
10
+ const EXPO_AGE_RANGE_MODULE_NAME = 'ExpoAgeRange';
11
+
12
+ // The iOS-only and android-only methods stay optional here; age-range.ts checks Platform.OS
13
+ // before calling through, same convention as packages/application/src/core/native-module.ts
14
+ export type INativeAgeRangeModule = {
15
+ requestAgeRangeAsync(options: IAgeRangeRequest): Promise<IAgeRangeResponse>;
16
+ isEligibleForAgeFeaturesAsync(): Promise<boolean | null>;
17
+ showSignificantUpdateAcknowledgmentAsync?(
18
+ updateDescription: string,
19
+ ): Promise<void>;
20
+ getRequiredRegulatoryFeaturesAsync?(): Promise<
21
+ IAgeRangeRegulatoryFeature[] | null
22
+ >;
23
+ requestAgeSignalsAccessAsync?(): Promise<IAgeSignalsStatus | null>;
24
+ setFakeAgeSignals?(fake: IFakeAgeSignals | null): void;
25
+ };
26
+
27
+ export const expoAgeRange = requireNativeModule<INativeAgeRangeModule>(
28
+ EXPO_AGE_RANGE_MODULE_NAME,
29
+ );
@@ -0,0 +1,64 @@
1
+ // Ported verbatim from expo-age-range's ExpoAgeRange.types.ts (sdk-57), renamed with this
2
+ // repo's `I`-prefix convention
3
+
4
+ /** @platform ios */
5
+ export type IAgeRangeRequest = {
6
+ /** The required minimum age for your app */
7
+ threshold1: number;
8
+ threshold2?: number;
9
+ threshold3?: number;
10
+ };
11
+
12
+ export type IAgeRangeResponse = {
13
+ lowerBound: number | null;
14
+ upperBound: number | null;
15
+ /** @platform ios */
16
+ ageRangeDeclaration?:
17
+ 'selfDeclared' | 'guardianDeclared' | 'confirmed' | null;
18
+ /** @platform ios */
19
+ activeParentalControls?: string[];
20
+ /** @platform android */
21
+ installId?: string | null;
22
+ /** @platform android */
23
+ ageRangeSource?: 'TIER_A' | 'TIER_B' | 'TIER_C' | 'TIER_D' | null;
24
+ /** @platform android */
25
+ significantChangeStatus?: 'APPROVED' | 'PENDING' | 'DECLINED' | null;
26
+ /** @platform android */
27
+ significantChangeApprovalDate?: number | null;
28
+ /** Deprecated, use `significantChangeApprovalDate`. @platform android */
29
+ mostRecentApprovalDate?: number | null;
30
+ };
31
+
32
+ /** @platform android */
33
+ export type IAgeSignalsStatus =
34
+ 'SHARED' | 'NOT_SHARED' | 'VERIFICATION_REQUIRED';
35
+
36
+ /** @platform android, omitted fields report as `null` */
37
+ export type IFakeAgeSignals =
38
+ | {
39
+ lowerBound?: number | null;
40
+ upperBound?: number | null;
41
+ installId?: string | null;
42
+ ageRangeSource?: 'TIER_A' | 'TIER_B' | 'TIER_C' | 'TIER_D' | null;
43
+ significantChangeStatus?: 'APPROVED' | 'PENDING' | 'DECLINED' | null;
44
+ significantChangeApprovalDate?: number | null;
45
+ ageSignalsStatus?: IAgeSignalsStatus | null;
46
+ errorCode?: never;
47
+ }
48
+ | {
49
+ /** https://developer.android.com/google/play/age-signals/handle-errors */
50
+ errorCode: number;
51
+ lowerBound?: never;
52
+ upperBound?: never;
53
+ installId?: never;
54
+ ageRangeSource?: never;
55
+ significantChangeStatus?: never;
56
+ significantChangeApprovalDate?: never;
57
+ ageSignalsStatus?: never;
58
+ };
59
+
60
+ /** @platform ios */
61
+ export type IAgeRangeRegulatoryFeature =
62
+ | 'declaredAgeRangeRequired'
63
+ | 'significantAppChangeRequiresAdultNotification'
64
+ | 'significantAppChangeRequiresParentalConsent';