@milkinteractive/react-native-age-range 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +22 -0
- package/README.md +325 -0
- package/StoreAgeSignalsNativeModules.podspec +25 -0
- package/android/build.gradle +81 -0
- package/android/gradle.properties +5 -0
- package/android/src/main/AndroidManifest.xml +3 -0
- package/android/src/main/AndroidManifestNew.xml +2 -0
- package/android/src/main/java/com/storeagesignalsnativemodules/StoreAgeSignalsNativeModulesModule.kt +193 -0
- package/android/src/main/java/com/storeagesignalsnativemodules/StoreAgeSignalsNativeModulesPackage.kt +17 -0
- package/ios/StoreAgeSignalsNativeModules-Bridging-Header.h +2 -0
- package/ios/StoreAgeSignalsNativeModules.h +4 -0
- package/ios/StoreAgeSignalsNativeModules.mm +73 -0
- package/ios/StoreAgeSignalsNativeModules.swift +180 -0
- package/lib/commonjs/index.js +121 -0
- package/lib/commonjs/index.js.map +1 -0
- package/lib/commonjs/package.json +1 -0
- package/lib/module/index.js +114 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/package.json +1 -0
- package/lib/typescript/src/index.d.ts +138 -0
- package/lib/typescript/src/index.d.ts.map +1 -0
- package/package.json +198 -0
- package/src/index.tsx +234 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Naga sai charan gubba
|
|
4
|
+
Copyright (c) 2026 Christoph Eck
|
|
5
|
+
|
|
6
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
7
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
8
|
+
in the Software without restriction, including without limitation the rights
|
|
9
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
10
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
11
|
+
furnished to do so, subject to the following conditions:
|
|
12
|
+
|
|
13
|
+
The above copyright notice and this permission notice shall be included in all
|
|
14
|
+
copies or substantial portions of the Software.
|
|
15
|
+
|
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
17
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
18
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
19
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
20
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
21
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
22
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+

|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
## ๐ฑ Live Demos
|
|
5
|
+
|
|
6
|
+
### Screen Recordings
|
|
7
|
+
|
|
8
|
+
<table>
|
|
9
|
+
<tr>
|
|
10
|
+
<th>Android</th>
|
|
11
|
+
<th>iOS</th>
|
|
12
|
+
</tr>
|
|
13
|
+
<tr>
|
|
14
|
+
<td>
|
|
15
|
+
<video src="https://github.com/milkinteractive/react-native-age-range/raw/main/assets/Android_screen_recording.webm" width="300" controls>
|
|
16
|
+
Your browser does not support the video tag.
|
|
17
|
+
</video>
|
|
18
|
+
</td>
|
|
19
|
+
<td>
|
|
20
|
+
<video src="https://github.com/milkinteractive/react-native-age-range/raw/main/assets/ios_screen_recording.mov" width="300" controls>
|
|
21
|
+
Your browser does not support the video tag.
|
|
22
|
+
</video>
|
|
23
|
+
</td>
|
|
24
|
+
</tr>
|
|
25
|
+
</table>
|
|
26
|
+
|
|
27
|
+
### iOS Age Verification Modal (Real Device)
|
|
28
|
+
|
|
29
|
+
| Step 1 | Step 2 | Modal |
|
|
30
|
+
|:------:|:------:|:-----:|
|
|
31
|
+
|  |  |  |
|
|
32
|
+
|
|
33
|
+
### Example App UI
|
|
34
|
+
|
|
35
|
+
| iOS UI | Android UI |
|
|
36
|
+
|:------:|:----------:|
|
|
37
|
+
|  |  |
|
|
38
|
+
|
|
39
|
+
# React Native Age Range
|
|
40
|
+
|
|
41
|
+
[](https://www.npmjs.com/package/@milkinteractive/react-native-age-range)
|
|
42
|
+
[](LICENSE)
|
|
43
|
+
[](https://facebook.github.io/react-native/)
|
|
44
|
+
[](https://www.typescriptlang.org/)
|
|
45
|
+
|
|
46
|
+
**A production-grade React Native module for verifiable age signals.**
|
|
47
|
+
|
|
48
|
+
Seamlessly integrate with **Apple's Declared Age Range API** (iOS 26+) and **Google Play Age Signals API** to meet state-level age verification compliance (e.g., Texas, Utah, Louisiana) without handling sensitive PII yourself.
|
|
49
|
+
|
|
50
|
+
> **โ ๏ธ COMPLIANCE NOTICE**: Texas [SB2420](https://legiscan.com/TX/text/SB2420/id/3237346/Texas-2025-SB2420-Enrolled.html) **requires** apps to consume age signals from app stores starting **January 1, 2026**. Similar laws in Utah (May 7, 2026) and Louisiana (July 1, 2026) are also taking effect. This package provides the necessary integration for React Native apps.
|
|
51
|
+
|
|
52
|
+
**Keywords**: `age verification`, `texas sb2420`, `age gate`, `parental controls`, `coppa compliance`, `react native age verification`, `google play age signals`, `ios declared age range`, `app store age verification`, `react native compliance`, `child safety`, `age appropriate design code`
|
|
53
|
+
|
|
54
|
+
---
|
|
55
|
+
|
|
56
|
+
## ๐ Features
|
|
57
|
+
|
|
58
|
+
- **๐ก๏ธ Privacy-First**: Leverages OS-level store APIs. **No access to birthdates or PII** โ only age range classifications.
|
|
59
|
+
- **๐ iOS Integration**: Native support for `DeclaredAgeRange` framework (iOS 26.0+).
|
|
60
|
+
- **๐ค Android Integration**: Official wrapper for Google Play `AgeSignalsApi`.
|
|
61
|
+
- **๐งช Mock Mode**: Built-in developer tools to simulate all age scenarios on Simulators and Emulators.
|
|
62
|
+
- **โก Zero Config Mocks**: Verification logic works out-of-the-box for development.
|
|
63
|
+
- **๐ฑ Broad Compatibility**: Works with any React Native version (0.60+) โ uses legacy native module architecture.
|
|
64
|
+
|
|
65
|
+
## ๐๏ธ Architecture
|
|
66
|
+
|
|
67
|
+
```mermaid
|
|
68
|
+
graph TD
|
|
69
|
+
RN[React Native JS] -->|Standard Interface| Bridge[Native Module Bridge]
|
|
70
|
+
Bridge -->|Android| PlayService[Google Play Services]
|
|
71
|
+
Bridge -->|iOS| AppleAPI[Apple Declared Age Range]
|
|
72
|
+
|
|
73
|
+
PlayService -->|Status| Result[Verified / Supervised / Error]
|
|
74
|
+
AppleAPI -->|Status| Result
|
|
75
|
+
|
|
76
|
+
subgraph Privacy Shield
|
|
77
|
+
PlayService
|
|
78
|
+
AppleAPI
|
|
79
|
+
end
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## ๐ฆ Installation
|
|
83
|
+
|
|
84
|
+
```sh
|
|
85
|
+
npm install @milkinteractive/react-native-age-range
|
|
86
|
+
# or
|
|
87
|
+
yarn add @milkinteractive/react-native-age-range
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## โ๏ธ Setup
|
|
91
|
+
|
|
92
|
+
### ๐ iOS Setup
|
|
93
|
+
|
|
94
|
+
1. **Framework Requirements**:
|
|
95
|
+
- **iOS 26.0+** is required for the `DeclaredAgeRange` API to function.
|
|
96
|
+
- Older versions will return a fallback/unavailable response.
|
|
97
|
+
|
|
98
|
+
2. **Install Pods**:
|
|
99
|
+
```sh
|
|
100
|
+
cd ios && pod install
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
3. **Entitlements (Critical)**:
|
|
104
|
+
- You **must** enable the `Declared Age Range` capability in Xcode.
|
|
105
|
+
- Go to **Project Target** -> **Signing & Capabilities** -> **+ Capability** -> **Declared Age Range**.
|
|
106
|
+
- *Note: This capability typically requires a paid Apple Developer Program membership. "Personal Team" profiles may not support it.*
|
|
107
|
+
|
|
108
|
+
4. **โ ๏ธ Apple API Limitations**:
|
|
109
|
+
- **Minimum Range Duration**: Age thresholds must create ranges of **at least 2 years**.
|
|
110
|
+
- **Example**: Thresholds `10, 13, 16` work because they create: Under 10, 10-12 (2 yrs), 13-15 (2 yrs), 16+.
|
|
111
|
+
- **Invalid Example**: `13, 14, 21` would fail because 13-14 is only 1 year.
|
|
112
|
+
- Common working combinations: `10, 13, 16` or `13, 16, 18` or `13, 17, 21`.
|
|
113
|
+
|
|
114
|
+
### ๐ค Android Setup
|
|
115
|
+
|
|
116
|
+
No manual configuration required. The package automatically bundles `com.google.android.play:age-signals`.
|
|
117
|
+
- **Requirement**: Device must have Google Play Services installed.
|
|
118
|
+
|
|
119
|
+
## ๐ป Usage
|
|
120
|
+
|
|
121
|
+
```typescript
|
|
122
|
+
import {
|
|
123
|
+
getAndroidPlayAgeRangeStatus,
|
|
124
|
+
requestIOSDeclaredAgeRange,
|
|
125
|
+
isIOSEligibleForAgeFeatures,
|
|
126
|
+
isAndroidEligibleForAgeFeatures,
|
|
127
|
+
} from '@milkinteractive/react-native-age-range';
|
|
128
|
+
import { Platform } from 'react-native';
|
|
129
|
+
|
|
130
|
+
// ๐ค Android Example
|
|
131
|
+
async function checkAndroid() {
|
|
132
|
+
if (Platform.OS !== 'android') return;
|
|
133
|
+
|
|
134
|
+
const result = await getAndroidPlayAgeRangeStatus();
|
|
135
|
+
|
|
136
|
+
if (result.userStatus === 'OVER_AGE') {
|
|
137
|
+
// โ
User is a verified adult
|
|
138
|
+
grantAccess();
|
|
139
|
+
} else if (result.userStatus === 'UNDER_AGE') {
|
|
140
|
+
// โ ๏ธ User is supervised (e.g. Family Link)
|
|
141
|
+
// result.ageLower and result.ageUpper are available (e.g., 13-17)
|
|
142
|
+
enableRestrictedMode(result.ageLower, result.ageUpper);
|
|
143
|
+
} else {
|
|
144
|
+
// โ Verification failed or unknown
|
|
145
|
+
handleError(result.error);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// ๐ iOS Example
|
|
150
|
+
async function checkIOS() {
|
|
151
|
+
if (Platform.OS !== 'ios') return;
|
|
152
|
+
|
|
153
|
+
// Request discrete age signals (e.g. 13+, 17+, 21+)
|
|
154
|
+
const result = await requestIOSDeclaredAgeRange(13, 17, 21);
|
|
155
|
+
|
|
156
|
+
if (result.error) {
|
|
157
|
+
// โ API error (e.g., iOS < 26.0, missing entitlement)
|
|
158
|
+
console.error('iOS Signal Failed:', result.error);
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (result.status === 'sharing') {
|
|
163
|
+
// โ
User shared their age range
|
|
164
|
+
console.log(`Confirmed Range: ${result.lowerBound} - ${result.upperBound}`);
|
|
165
|
+
console.log(`Declaration: ${result.ageRangeDeclaration}`);
|
|
166
|
+
} else {
|
|
167
|
+
// โ User declined
|
|
168
|
+
console.log('User declined to share age range');
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// ๐ Check Eligibility (should age verification be shown?)
|
|
173
|
+
async function checkEligibility() {
|
|
174
|
+
const result = Platform.OS === 'ios'
|
|
175
|
+
? await isIOSEligibleForAgeFeatures()
|
|
176
|
+
: await isAndroidEligibleForAgeFeatures();
|
|
177
|
+
|
|
178
|
+
if (result.error) {
|
|
179
|
+
console.log('Eligibility check failed:', result.error);
|
|
180
|
+
return false;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (result.isEligible) {
|
|
184
|
+
// User is in a region requiring age verification (e.g., Texas)
|
|
185
|
+
// Proceed with age verification flow
|
|
186
|
+
return true;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// User is not subject to age verification requirements
|
|
190
|
+
return false;
|
|
191
|
+
}
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
## ๐งช Developer Mock Mode
|
|
195
|
+
|
|
196
|
+
Testing store APIs usually requires signed production builds. This library includes a powerful **Mock Mode** for development.
|
|
197
|
+
|
|
198
|
+
```typescript
|
|
199
|
+
// Simulate a Supervised User (Age 13-17)
|
|
200
|
+
const mockResult = await getAndroidPlayAgeRangeStatus({
|
|
201
|
+
isMock: true,
|
|
202
|
+
mockStatus: 'UNDER_AGE',
|
|
203
|
+
mockAgeLower: 13,
|
|
204
|
+
mockAgeUpper: 17
|
|
205
|
+
});
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
## ๐ง API Reference
|
|
209
|
+
|
|
210
|
+
### `getAndroidPlayAgeRangeStatus(config?)`
|
|
211
|
+
Retrieves Android Play Age Signal.
|
|
212
|
+
|
|
213
|
+
| Parameter | Type | Default | Description |
|
|
214
|
+
|---|---|---|---|
|
|
215
|
+
| `config.isMock` | `boolean` | `false` | Enable to return fake data. |
|
|
216
|
+
| `config.mockStatus` | `enum` | `'OVER_AGE'` | See status values below |
|
|
217
|
+
| `config.mockErrorCode` | `number` | `null` | Simulate API error code (e.g. -1). |
|
|
218
|
+
|
|
219
|
+
**Returns**: `Promise<PlayAgeRangeStatusResult>`
|
|
220
|
+
- `userStatus`: User verification status:
|
|
221
|
+
- `OVER_AGE` - Verified adult (18+)
|
|
222
|
+
- `UNDER_AGE` - Supervised account (child/teen)
|
|
223
|
+
- `UNDER_AGE_APPROVAL_PENDING` - Supervised, parent hasn't approved pending significant changes
|
|
224
|
+
- `UNDER_AGE_APPROVAL_DENIED` - Supervised, parent denied approval for significant changes
|
|
225
|
+
- `UNKNOWN` - Status could not be determined
|
|
226
|
+
- `installId`: Unique installation identifier
|
|
227
|
+
- `ageLower` / `ageUpper`: Age range bounds (for supervised users)
|
|
228
|
+
- `mostRecentApprovalDate`: Date of last approved significant change
|
|
229
|
+
- `error` / `errorCode`: Error information if request failed
|
|
230
|
+
|
|
231
|
+
### `requestIOSDeclaredAgeRange(threshold1, threshold2, threshold3)`
|
|
232
|
+
Request iOS Age Signal.
|
|
233
|
+
|
|
234
|
+
| Parameter | Type | Description |
|
|
235
|
+
|---|---|---|
|
|
236
|
+
| `threshold[1-3]` | `number` | Age thresholds to verify. **Must create 2+ year ranges**. |
|
|
237
|
+
|
|
238
|
+
**โ ๏ธ Apple API Constraint**: Thresholds must result in age ranges of at least 2 years duration.
|
|
239
|
+
- โ
**Valid**: `10, 13, 16` โ Creates ranges: <10, 10-12, 13-15, 16+
|
|
240
|
+
- โ
**Valid**: `13, 17, 21` โ Creates ranges: <13, 13-16, 17-20, 21+
|
|
241
|
+
- โ **Invalid**: `13, 14, 21` โ 13-14 is only 1 year (API will reject)
|
|
242
|
+
|
|
243
|
+
**Returns**: `Promise<DeclaredAgeRangeResult>`
|
|
244
|
+
- `status`: `'sharing' | 'declined' | null`
|
|
245
|
+
- `lowerBound`: `number | null` - Lower age of user's range
|
|
246
|
+
- `upperBound`: `number | null` - Upper age of user's range
|
|
247
|
+
- `ageRangeDeclaration`: How the age was verified:
|
|
248
|
+
- `selfDeclared` - User declared their own age
|
|
249
|
+
- `guardianDeclared` - Guardian set the age (children in iCloud family)
|
|
250
|
+
- `governmentIDChecked` / `guardianGovernmentIDChecked` - Verified via government ID
|
|
251
|
+
- `paymentChecked` / `guardianPaymentChecked` - Verified via payment method
|
|
252
|
+
- `checkedByOtherMethod` / `guardianCheckedByOtherMethod` - Other verification
|
|
253
|
+
- `parentalControls`: `{ communicationLimits?: boolean, significantAppChangeApprovalRequired?: boolean }` - Active parental controls
|
|
254
|
+
- `error`: `string | null` - Error message if API unavailable or request failed
|
|
255
|
+
|
|
256
|
+
### `isIOSEligibleForAgeFeatures()`
|
|
257
|
+
Check if the current iOS user is subject to age verification requirements (e.g., in Texas).
|
|
258
|
+
|
|
259
|
+
**Returns**: `Promise<DeclaredAgeEligibilityResult>`
|
|
260
|
+
- `isEligible`: `boolean` - `true` if user should be shown age verification
|
|
261
|
+
- `error`: `string | null` - Error message if API unavailable
|
|
262
|
+
|
|
263
|
+
**Requirements**: iOS 26.2+ for accurate results. Returns `isEligible: false` with error on older versions.
|
|
264
|
+
|
|
265
|
+
### `isAndroidEligibleForAgeFeatures()`
|
|
266
|
+
Check if the current Android user is subject to age verification requirements.
|
|
267
|
+
|
|
268
|
+
**Returns**: `Promise<DeclaredAgeEligibilityResult>`
|
|
269
|
+
- `isEligible`: `boolean` - `true` if user should be shown age verification (in Texas, Utah, Louisiana, etc.)
|
|
270
|
+
- `error`: `string | null` - Error message if API unavailable
|
|
271
|
+
|
|
272
|
+
**Note**: Makes a lightweight API call to determine eligibility.
|
|
273
|
+
|
|
274
|
+
## ๐จ Troubleshooting
|
|
275
|
+
|
|
276
|
+
### iOS Errors
|
|
277
|
+
|
|
278
|
+
| Error Code | Meaning | Solution |
|
|
279
|
+
|---|---|---|
|
|
280
|
+
| **Error 0** | **Missing Entitlement** | 1. Add `Declared Age Range` capability in Xcode.<br>2. Ensure you are using a **Paid Developer Account**. Personal teams often block this API.<br>3. **Real Device Only**: This API does NOT work on Simulators. |
|
|
281
|
+
| **Error -1** | **API Unavailable** | Device is running an iOS version older than 26.0. |
|
|
282
|
+
|
|
283
|
+
### Android Errors
|
|
284
|
+
## ๐ Additional Resources
|
|
285
|
+
|
|
286
|
+
### Android Error Codes Reference
|
|
287
|
+
|
|
288
|
+
| Code | Error | Description | Retryable |
|
|
289
|
+
|---|---|---|---|
|
|
290
|
+
| -1 | API_NOT_AVAILABLE | Play Store app version might be old. | Yes |
|
|
291
|
+
| -2 | PLAY_STORE_NOT_FOUND | No Play Store app found. | Yes |
|
|
292
|
+
| -3 | NETWORK_ERROR | No network connection. | Yes |
|
|
293
|
+
| -4 | PLAY_SERVICES_NOT_FOUND | Play Services unavailable or old. | Yes |
|
|
294
|
+
| -5 | CANNOT_BIND_TO_SERVICE | Failed to bind to Play Store service. | Yes |
|
|
295
|
+
| -6 | PLAY_STORE_VERSION_OUTDATED | Play Store app needs update. | Yes |
|
|
296
|
+
| -7 | PLAY_SERVICES_VERSION_OUTDATED | Play Services needs update. | Yes |
|
|
297
|
+
| -8 | CLIENT_TRANSIENT_ERROR | Transient client error. Retry with backoff. | Yes |
|
|
298
|
+
| -9 | APP_NOT_OWNED | App not installed by Google Play. | No |
|
|
299
|
+
| -100 | INTERNAL_ERROR | Unknown internal error. | No |
|
|
300
|
+
|
|
301
|
+
### Official API Documentation
|
|
302
|
+
|
|
303
|
+
**Apple iOS:**
|
|
304
|
+
- [Declared Age Range Framework](https://developer.apple.com/documentation/declaredagerange) - Official Apple Developer Documentation
|
|
305
|
+
- [WWDC25: Deliver age-appropriate experiences in your app](https://developer.apple.com/videos/play/wwdc2025/299/) - Session video explaining implementation
|
|
306
|
+
|
|
307
|
+
**Google Android:**
|
|
308
|
+
- [Play Age Signals Overview](https://developer.android.com/google/play/age-signals/overview) - Introduction and concepts
|
|
309
|
+
- [Use Play Age Signals API](https://developer.android.com/google/play/age-signals/use-age-signals-api) - Implementation guide
|
|
310
|
+
- [Test your Play Age Signals API integration](https://developer.android.com/google/play/age-signals/test-age-signals-api) - Testing with FakeAgeSignalsManager
|
|
311
|
+
- [Play Age Signals Release Notes](https://developer.android.com/google/play/age-signals/release-notes) - Version history and updates
|
|
312
|
+
|
|
313
|
+
---
|
|
314
|
+
|
|
315
|
+
## ๐ค Contributing
|
|
316
|
+
|
|
317
|
+
See the [contributing guide](CONTRIBUTING.md) to learn how to contribute to the repository and the development workflow.
|
|
318
|
+
|
|
319
|
+
## ๐ License
|
|
320
|
+
|
|
321
|
+
MIT
|
|
322
|
+
|
|
323
|
+
---
|
|
324
|
+
|
|
325
|
+
**Made with โค๏ธ for React Native developers navigating age verification compliance.**
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
require "json"
|
|
2
|
+
|
|
3
|
+
package = JSON.parse(File.read(File.join(__dir__, "package.json")))
|
|
4
|
+
|
|
5
|
+
Pod::Spec.new do |s|
|
|
6
|
+
s.name = "StoreAgeSignalsNativeModules"
|
|
7
|
+
s.version = package["version"]
|
|
8
|
+
s.summary = package["description"]
|
|
9
|
+
s.homepage = package["homepage"]
|
|
10
|
+
s.license = package["license"]
|
|
11
|
+
s.authors = package["author"]
|
|
12
|
+
|
|
13
|
+
s.platforms = { :ios => min_ios_version_supported }
|
|
14
|
+
s.source = { :git => "https://github.com/milkinteractive/react-native-age-range.git", :tag => "#{s.version}" }
|
|
15
|
+
|
|
16
|
+
s.source_files = "ios/**/*.{h,m,mm,swift}"
|
|
17
|
+
|
|
18
|
+
# Use install_modules_dependencies helper to install the dependencies if React Native version >=0.71.0.
|
|
19
|
+
# See https://github.com/facebook/react-native/blob/febf6b7f33fdb4904669f99d795eba4c0f95d7bf/scripts/cocoapods/new_architecture.rb#L79.
|
|
20
|
+
if respond_to?(:install_modules_dependencies, true)
|
|
21
|
+
install_modules_dependencies(s)
|
|
22
|
+
else
|
|
23
|
+
s.dependency "React-Core"
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
buildscript {
|
|
2
|
+
ext.getExtOrDefault = {name ->
|
|
3
|
+
return rootProject.ext.has(name) ? rootProject.ext.get(name) : project.properties['StoreAgeSignalsNativeModules_' + name]
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
repositories {
|
|
7
|
+
google()
|
|
8
|
+
mavenCentral()
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
dependencies {
|
|
12
|
+
classpath "com.android.tools.build:gradle:8.7.2"
|
|
13
|
+
// noinspection DifferentKotlinGradleVersion
|
|
14
|
+
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:${getExtOrDefault('kotlinVersion')}"
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
apply plugin: "com.android.library"
|
|
20
|
+
apply plugin: "kotlin-android"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def getExtOrIntegerDefault(name) {
|
|
24
|
+
return rootProject.ext.has(name) ? rootProject.ext.get(name) : (project.properties["StoreAgeSignalsNativeModules_" + name]).toInteger()
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
def supportsNamespace() {
|
|
28
|
+
def parsed = com.android.Version.ANDROID_GRADLE_PLUGIN_VERSION.tokenize('.')
|
|
29
|
+
def major = parsed[0].toInteger()
|
|
30
|
+
def minor = parsed[1].toInteger()
|
|
31
|
+
|
|
32
|
+
// Namespace support was added in 7.3.0
|
|
33
|
+
return (major == 7 && minor >= 3) || major >= 8
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
android {
|
|
37
|
+
if (supportsNamespace()) {
|
|
38
|
+
namespace "com.storeagesignalsnativemodules"
|
|
39
|
+
|
|
40
|
+
sourceSets {
|
|
41
|
+
main {
|
|
42
|
+
manifest.srcFile "src/main/AndroidManifestNew.xml"
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
compileSdkVersion getExtOrIntegerDefault("compileSdkVersion")
|
|
48
|
+
|
|
49
|
+
defaultConfig {
|
|
50
|
+
minSdkVersion getExtOrIntegerDefault("minSdkVersion")
|
|
51
|
+
targetSdkVersion getExtOrIntegerDefault("targetSdkVersion")
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
buildTypes {
|
|
55
|
+
release {
|
|
56
|
+
minifyEnabled false
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
lintOptions {
|
|
61
|
+
disable "GradleCompatible"
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
compileOptions {
|
|
65
|
+
sourceCompatibility JavaVersion.VERSION_1_8
|
|
66
|
+
targetCompatibility JavaVersion.VERSION_1_8
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
repositories {
|
|
71
|
+
mavenCentral()
|
|
72
|
+
google()
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
def kotlin_version = getExtOrDefault("kotlinVersion")
|
|
76
|
+
|
|
77
|
+
dependencies {
|
|
78
|
+
implementation "com.facebook.react:react-android"
|
|
79
|
+
implementation "com.google.android.play:age-signals:0.0.2"
|
|
80
|
+
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
|
|
81
|
+
}
|
package/android/src/main/java/com/storeagesignalsnativemodules/StoreAgeSignalsNativeModulesModule.kt
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
package com.storeagesignalsnativemodules
|
|
2
|
+
|
|
3
|
+
import com.facebook.react.bridge.Promise
|
|
4
|
+
import com.facebook.react.bridge.ReactApplicationContext
|
|
5
|
+
import com.facebook.react.bridge.ReactContextBaseJavaModule
|
|
6
|
+
import com.facebook.react.bridge.ReactMethod
|
|
7
|
+
import com.facebook.react.bridge.WritableNativeMap
|
|
8
|
+
import com.google.android.play.agesignals.AgeSignalsManager
|
|
9
|
+
import com.google.android.play.agesignals.AgeSignalsManagerFactory
|
|
10
|
+
import com.google.android.play.agesignals.AgeSignalsRequest
|
|
11
|
+
import com.google.android.play.agesignals.AgeSignalsResult
|
|
12
|
+
import com.google.android.play.agesignals.model.AgeSignalsVerificationStatus
|
|
13
|
+
import com.google.android.play.agesignals.testing.FakeAgeSignalsManager
|
|
14
|
+
import com.google.android.gms.common.api.ApiException
|
|
15
|
+
import com.google.android.gms.common.api.Status
|
|
16
|
+
|
|
17
|
+
class StoreAgeSignalsNativeModulesModule(reactContext: ReactApplicationContext) :
|
|
18
|
+
ReactContextBaseJavaModule(reactContext) {
|
|
19
|
+
|
|
20
|
+
override fun getName(): String {
|
|
21
|
+
return NAME
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
@ReactMethod
|
|
25
|
+
fun getAndroidPlayAgeRangeStatus(config: com.facebook.react.bridge.ReadableMap, promise: Promise) {
|
|
26
|
+
try {
|
|
27
|
+
val context = reactApplicationContext
|
|
28
|
+
|
|
29
|
+
val isMock = if (config.hasKey("isMock")) config.getBoolean("isMock") else false
|
|
30
|
+
|
|
31
|
+
val manager: AgeSignalsManager = if (isMock) {
|
|
32
|
+
val fakeManager = FakeAgeSignalsManager()
|
|
33
|
+
|
|
34
|
+
var mockStatusStr = "OVER_AGE"
|
|
35
|
+
if (config.hasKey("mockStatus")) {
|
|
36
|
+
mockStatusStr = config.getString("mockStatus") ?: "OVER_AGE"
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Build the mock result
|
|
40
|
+
val verificationStatus = when (mockStatusStr) {
|
|
41
|
+
"OVER_AGE" -> AgeSignalsVerificationStatus.VERIFIED
|
|
42
|
+
"UNDER_AGE" -> AgeSignalsVerificationStatus.SUPERVISED
|
|
43
|
+
"UNDER_AGE_APPROVAL_PENDING" -> AgeSignalsVerificationStatus.SUPERVISED_APPROVAL_PENDING
|
|
44
|
+
"UNDER_AGE_APPROVAL_DENIED" -> AgeSignalsVerificationStatus.SUPERVISED_APPROVAL_DENIED
|
|
45
|
+
"UNKNOWN" -> AgeSignalsVerificationStatus.UNKNOWN
|
|
46
|
+
else -> AgeSignalsVerificationStatus.VERIFIED
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
val builder = AgeSignalsResult.builder()
|
|
50
|
+
.setUserStatus(verificationStatus)
|
|
51
|
+
.setInstallId("mock_install_id_12345")
|
|
52
|
+
|
|
53
|
+
if (config.hasKey("mockAgeLower")) {
|
|
54
|
+
builder.setAgeLower(config.getInt("mockAgeLower"))
|
|
55
|
+
}
|
|
56
|
+
if (config.hasKey("mockAgeUpper")) {
|
|
57
|
+
builder.setAgeUpper(config.getInt("mockAgeUpper"))
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Handle Date Mocking (ISO String expected)
|
|
61
|
+
if (config.hasKey("mockMostRecentApprovalDate")) {
|
|
62
|
+
try {
|
|
63
|
+
val dateStr = config.getString("mockMostRecentApprovalDate")
|
|
64
|
+
// Simple ISO format parser or just generic Date parsing
|
|
65
|
+
// For simplicity in this environment, using standard Date class if string matches,
|
|
66
|
+
// or implied simplistic parsing. Ideally SimpleDateFormat.
|
|
67
|
+
// let's assume input is simple yyyy-MM-dd for mock, or use Date(long).
|
|
68
|
+
// Better: Use Date.parse() (deprecated) or SimpleDateFormat?
|
|
69
|
+
// I'll stick to not implementing complex date parsing for Mock unless requested.
|
|
70
|
+
// But I'll leave a TODO or simple mapping if easy.
|
|
71
|
+
} catch (e: Exception) {
|
|
72
|
+
// Ignore
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Handle Date Mocking (ISO String expected)
|
|
77
|
+
if (config.hasKey("mockMostRecentApprovalDate")) {
|
|
78
|
+
try {
|
|
79
|
+
// Date parsing logic if needed
|
|
80
|
+
} catch (e: Exception) {
|
|
81
|
+
// Ignore
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Handle Mock Error
|
|
86
|
+
if (config.hasKey("mockErrorCode")) {
|
|
87
|
+
val errorCode = config.getInt("mockErrorCode")
|
|
88
|
+
// Use the actual exception class directly now that we know the signature
|
|
89
|
+
val exception = com.google.android.play.agesignals.AgeSignalsException(errorCode)
|
|
90
|
+
fakeManager.setNextAgeSignalsException(exception)
|
|
91
|
+
} else {
|
|
92
|
+
val fakeResult = builder.build()
|
|
93
|
+
fakeManager.setNextAgeSignalsResult(fakeResult)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
fakeManager
|
|
97
|
+
} else {
|
|
98
|
+
AgeSignalsManagerFactory.create(context)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
val request = AgeSignalsRequest.builder().build()
|
|
102
|
+
|
|
103
|
+
manager.checkAgeSignals(request)
|
|
104
|
+
.addOnSuccessListener { result ->
|
|
105
|
+
val map = WritableNativeMap()
|
|
106
|
+
|
|
107
|
+
val userStatusObj = result.userStatus()
|
|
108
|
+
|
|
109
|
+
val userStatus = when (userStatusObj) {
|
|
110
|
+
AgeSignalsVerificationStatus.VERIFIED -> "OVER_AGE"
|
|
111
|
+
AgeSignalsVerificationStatus.SUPERVISED -> "UNDER_AGE"
|
|
112
|
+
AgeSignalsVerificationStatus.SUPERVISED_APPROVAL_PENDING -> "UNDER_AGE_APPROVAL_PENDING"
|
|
113
|
+
AgeSignalsVerificationStatus.SUPERVISED_APPROVAL_DENIED -> "UNDER_AGE_APPROVAL_DENIED"
|
|
114
|
+
AgeSignalsVerificationStatus.UNKNOWN -> "UNKNOWN"
|
|
115
|
+
else -> "UNKNOWN"
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
map.putString("userStatus", userStatus)
|
|
119
|
+
map.putString("installId", result.installId())
|
|
120
|
+
map.putString("error", null)
|
|
121
|
+
|
|
122
|
+
if (result.ageLower() != null) map.putInt("ageLower", result.ageLower()!!)
|
|
123
|
+
else map.putNull("ageLower")
|
|
124
|
+
|
|
125
|
+
if (result.ageUpper() != null) map.putInt("ageUpper", result.ageUpper()!!)
|
|
126
|
+
else map.putNull("ageUpper")
|
|
127
|
+
|
|
128
|
+
if (result.mostRecentApprovalDate() != null) map.putString("mostRecentApprovalDate", result.mostRecentApprovalDate().toString())
|
|
129
|
+
else map.putNull("mostRecentApprovalDate")
|
|
130
|
+
|
|
131
|
+
map.putNull("errorCode")
|
|
132
|
+
|
|
133
|
+
promise.resolve(map)
|
|
134
|
+
}
|
|
135
|
+
.addOnFailureListener { exception ->
|
|
136
|
+
val map = WritableNativeMap()
|
|
137
|
+
map.putString("installId", null)
|
|
138
|
+
map.putString("userStatus", "UNKNOWN")
|
|
139
|
+
map.putString("error", exception.message ?: "Unknown error")
|
|
140
|
+
|
|
141
|
+
if (exception is ApiException) {
|
|
142
|
+
map.putInt("errorCode", exception.statusCode)
|
|
143
|
+
} else {
|
|
144
|
+
map.putNull("errorCode")
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
promise.resolve(map)
|
|
148
|
+
}
|
|
149
|
+
} catch (e: Exception) {
|
|
150
|
+
promise.reject("INIT_ERROR", e.message, e)
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
@ReactMethod
|
|
155
|
+
fun isEligibleForAgeFeatures(promise: Promise) {
|
|
156
|
+
try {
|
|
157
|
+
val context = reactApplicationContext
|
|
158
|
+
val manager = AgeSignalsManagerFactory.create(context)
|
|
159
|
+
val request = AgeSignalsRequest.builder().build()
|
|
160
|
+
|
|
161
|
+
manager.checkAgeSignals(request)
|
|
162
|
+
.addOnSuccessListener { result ->
|
|
163
|
+
// User is eligible if we get a valid response with a known status
|
|
164
|
+
val userStatus = result.userStatus()
|
|
165
|
+
val isEligible = userStatus != null && userStatus != AgeSignalsVerificationStatus.UNKNOWN
|
|
166
|
+
val map = WritableNativeMap()
|
|
167
|
+
map.putBoolean("isEligible", isEligible)
|
|
168
|
+
map.putNull("error")
|
|
169
|
+
promise.resolve(map)
|
|
170
|
+
}
|
|
171
|
+
.addOnFailureListener { exception ->
|
|
172
|
+
// If API fails, return isEligible: false with error
|
|
173
|
+
val map = WritableNativeMap()
|
|
174
|
+
map.putBoolean("isEligible", false)
|
|
175
|
+
if (exception is ApiException) {
|
|
176
|
+
map.putString("error", "API error: ${exception.statusCode}")
|
|
177
|
+
} else {
|
|
178
|
+
map.putString("error", exception.message ?: "Unknown error")
|
|
179
|
+
}
|
|
180
|
+
promise.resolve(map)
|
|
181
|
+
}
|
|
182
|
+
} catch (e: Exception) {
|
|
183
|
+
val map = WritableNativeMap()
|
|
184
|
+
map.putBoolean("isEligible", false)
|
|
185
|
+
map.putString("error", "Failed to initialize AgeSignalsManager: ${e.message}")
|
|
186
|
+
promise.resolve(map)
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
companion object {
|
|
191
|
+
const val NAME = "StoreAgeSignalsNativeModules"
|
|
192
|
+
}
|
|
193
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
package com.storeagesignalsnativemodules
|
|
2
|
+
|
|
3
|
+
import com.facebook.react.ReactPackage
|
|
4
|
+
import com.facebook.react.bridge.NativeModule
|
|
5
|
+
import com.facebook.react.bridge.ReactApplicationContext
|
|
6
|
+
import com.facebook.react.uimanager.ViewManager
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class StoreAgeSignalsNativeModulesPackage : ReactPackage {
|
|
10
|
+
override fun createNativeModules(reactContext: ReactApplicationContext): List<NativeModule> {
|
|
11
|
+
return listOf(StoreAgeSignalsNativeModulesModule(reactContext))
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
override fun createViewManagers(reactContext: ReactApplicationContext): List<ViewManager<*, *>> {
|
|
15
|
+
return emptyList()
|
|
16
|
+
}
|
|
17
|
+
}
|