@milkinteractive/react-native-age-range 1.0.4 โ 1.0.6
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 +125 -0
- package/ios/StoreAgeSignalsNativeModules.mm +37 -10
- package/ios/StoreAgeSignalsNativeModules.swift +237 -11
- package/lib/commonjs/index.js +186 -5
- package/lib/commonjs/index.js.map +1 -1
- package/lib/module/index.js +183 -5
- package/lib/module/index.js.map +1 -1
- package/lib/typescript/src/index.d.ts +142 -5
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/index.tsx +221 -8
package/README.md
CHANGED
|
@@ -191,6 +191,86 @@ async function checkEligibility() {
|
|
|
191
191
|
}
|
|
192
192
|
```
|
|
193
193
|
|
|
194
|
+
## ๐ก๏ธ PermissionKit Integration (iOS 26+)
|
|
195
|
+
|
|
196
|
+
This package also supports Apple's **PermissionKit** framework for apps that need parental consent flows. Use these APIs when the `parentalControls` flags from `requestIOSDeclaredAgeRange()` indicate they're needed.
|
|
197
|
+
|
|
198
|
+
### When to Use PermissionKit
|
|
199
|
+
|
|
200
|
+
After calling `requestIOSDeclaredAgeRange()`, check the `parentalControls` object:
|
|
201
|
+
|
|
202
|
+
```typescript
|
|
203
|
+
const ageResult = await requestIOSDeclaredAgeRange(13, 17, 21);
|
|
204
|
+
|
|
205
|
+
if (ageResult.parentalControls?.significantAppChangeApprovalRequired) {
|
|
206
|
+
// App updates require parental approval โ use requestIOSSignificantChangeApproval()
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (ageResult.parentalControls?.communicationLimits) {
|
|
210
|
+
// Communication with unknown contacts requires approval โ use requestIOSCommunicationPermission()
|
|
211
|
+
}
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
### Significant Change Approval
|
|
215
|
+
|
|
216
|
+
When your app has significant updates that require parental consent:
|
|
217
|
+
|
|
218
|
+
```typescript
|
|
219
|
+
import { requestIOSSignificantChangeApproval } from '@milkinteractive/react-native-age-range';
|
|
220
|
+
|
|
221
|
+
const result = await requestIOSSignificantChangeApproval();
|
|
222
|
+
|
|
223
|
+
if (result.status === 'pending') {
|
|
224
|
+
// Request sent to parent/guardian via Messages
|
|
225
|
+
// Show "waiting for approval" UI
|
|
226
|
+
} else if (result.error) {
|
|
227
|
+
console.error('Significant change request failed:', result.error);
|
|
228
|
+
}
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
### Communication Permissions
|
|
232
|
+
|
|
233
|
+
For apps with chat/messaging features, request permission to communicate with unknown contacts:
|
|
234
|
+
|
|
235
|
+
```typescript
|
|
236
|
+
import {
|
|
237
|
+
getIOSKnownCommunicationHandles,
|
|
238
|
+
requestIOSCommunicationPermission,
|
|
239
|
+
} from '@milkinteractive/react-native-age-range';
|
|
240
|
+
|
|
241
|
+
// 1. Check which contacts are already known (approved)
|
|
242
|
+
const knownResult = await getIOSKnownCommunicationHandles([
|
|
243
|
+
{ handle: 'user@example.com', handleKind: 'email' },
|
|
244
|
+
{ handle: 'gamer123', handleKind: 'custom' },
|
|
245
|
+
]);
|
|
246
|
+
|
|
247
|
+
// 2. Request permission for unknown contacts
|
|
248
|
+
const unknownContacts = contacts.filter(
|
|
249
|
+
c => !knownResult.knownHandles.includes(c.handle)
|
|
250
|
+
);
|
|
251
|
+
|
|
252
|
+
if (unknownContacts.length > 0) {
|
|
253
|
+
const permResult = await requestIOSCommunicationPermission(
|
|
254
|
+
unknownContacts.map(c => ({
|
|
255
|
+
handle: c.handle,
|
|
256
|
+
handleKind: 'custom',
|
|
257
|
+
displayName: c.displayName, // Shown to parent in approval request
|
|
258
|
+
})),
|
|
259
|
+
['message', 'call'] // Requested communication actions
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
### PermissionKit Requirements
|
|
265
|
+
|
|
266
|
+
- **iOS 26.0+** for Significant Change API
|
|
267
|
+
- **iOS 26.2+** for Communication Limits API
|
|
268
|
+
- **Family Sharing** must be enabled on the device
|
|
269
|
+
- **Communication Limits** must be enabled by parent/guardian
|
|
270
|
+
- No additional entitlements required (uses system permissions)
|
|
271
|
+
|
|
272
|
+
> **Note**: PermissionKit features require a real device with Family Sharing configured. Simulator testing has limited functionality.
|
|
273
|
+
|
|
194
274
|
## ๐งช Developer Mock Mode
|
|
195
275
|
|
|
196
276
|
Testing store APIs usually requires signed production builds. This library includes a powerful **Mock Mode** for development.
|
|
@@ -271,6 +351,49 @@ Check if the current Android user is subject to age verification requirements.
|
|
|
271
351
|
|
|
272
352
|
**Note**: Makes a lightweight API call to determine eligibility.
|
|
273
353
|
|
|
354
|
+
### `requestIOSSignificantChangeApproval()`
|
|
355
|
+
Request parental approval for significant app changes (iOS PermissionKit).
|
|
356
|
+
|
|
357
|
+
**Requirements**: iOS 26.0+
|
|
358
|
+
|
|
359
|
+
**Returns**: `Promise<SignificantChangeResult>`
|
|
360
|
+
- `status`: `'approved' | 'denied' | 'pending' | null` - Current approval status
|
|
361
|
+
- `error`: `string | null` - Error message if request failed
|
|
362
|
+
|
|
363
|
+
**Usage**: Call when `parentalControls.significantAppChangeApprovalRequired` is `true`.
|
|
364
|
+
|
|
365
|
+
### `requestIOSCommunicationPermission(contacts, actions?)`
|
|
366
|
+
Request permission for a child to communicate with specified contacts (iOS PermissionKit).
|
|
367
|
+
|
|
368
|
+
**Requirements**: iOS 26.2+
|
|
369
|
+
|
|
370
|
+
| Parameter | Type | Description |
|
|
371
|
+
|---|---|---|
|
|
372
|
+
| `contacts` | `CommunicationContact[]` | Contacts to request permission for |
|
|
373
|
+
| `actions` | `CommunicationAction[]` | Optional: `['message']`, `['call']`, `['video']` (default: `['message']`) |
|
|
374
|
+
|
|
375
|
+
**CommunicationContact**:
|
|
376
|
+
- `handle`: `string` - Unique identifier (phone, email, username)
|
|
377
|
+
- `handleKind`: `'phoneNumber' | 'email' | 'custom'`
|
|
378
|
+
- `displayName`: `string` (optional) - Shown to parent in approval request
|
|
379
|
+
|
|
380
|
+
**Returns**: `Promise<CommunicationPermissionResult>`
|
|
381
|
+
- `granted`: `boolean` - Whether the permission dialog was shown successfully
|
|
382
|
+
- `error`: `string | null` - Error message if request failed
|
|
383
|
+
|
|
384
|
+
### `getIOSKnownCommunicationHandles(handles)`
|
|
385
|
+
Check which handles are recognized by the system (known contacts).
|
|
386
|
+
|
|
387
|
+
**Requirements**: iOS 26.2+
|
|
388
|
+
|
|
389
|
+
| Parameter | Type | Description |
|
|
390
|
+
|---|---|---|
|
|
391
|
+
| `handles` | `CommunicationContact[]` | Handles to check |
|
|
392
|
+
|
|
393
|
+
**Returns**: `Promise<KnownHandlesResult>`
|
|
394
|
+
- `knownHandles`: `string[]` - Array of handle values that are known/approved
|
|
395
|
+
- `error`: `string | null` - Error message if check failed
|
|
396
|
+
|
|
274
397
|
## ๐จ Troubleshooting
|
|
275
398
|
|
|
276
399
|
### iOS Errors
|
|
@@ -303,6 +426,8 @@ Check if the current Android user is subject to age verification requirements.
|
|
|
303
426
|
**Apple iOS:**
|
|
304
427
|
- [Declared Age Range Framework](https://developer.apple.com/documentation/declaredagerange) - Official Apple Developer Documentation
|
|
305
428
|
- [WWDC25: Deliver age-appropriate experiences in your app](https://developer.apple.com/videos/play/wwdc2025/299/) - Session video explaining implementation
|
|
429
|
+
- [PermissionKit Framework](https://developer.apple.com/documentation/PermissionKit) - Official PermissionKit Documentation
|
|
430
|
+
- [WWDC25: Enhance child safety with PermissionKit](https://developer.apple.com/videos/play/wwdc2025/293/) - PermissionKit session video
|
|
306
431
|
|
|
307
432
|
**Google Android:**
|
|
308
433
|
- [Play Age Signals Overview](https://developer.android.com/google/play/age-signals/overview) - Introduction and concepts
|
|
@@ -39,17 +39,17 @@ RCT_EXPORT_METHOD(getAndroidPlayAgeRangeStatus : (RCTPromiseResolveBlock)
|
|
|
39
39
|
resolve(result);
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
-
RCT_EXPORT_METHOD(requestIOSDeclaredAgeRange : (
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
//
|
|
48
|
-
//
|
|
42
|
+
RCT_EXPORT_METHOD(requestIOSDeclaredAgeRange : (nonnull NSNumber *)firstThresholdAge
|
|
43
|
+
secondThresholdAge : (NSNumber *)secondThresholdAge
|
|
44
|
+
thirdThresholdAge : (NSNumber *)thirdThresholdAge
|
|
45
|
+
resolve : (RCTPromiseResolveBlock)resolve
|
|
46
|
+
reject : (RCTPromiseRejectBlock)reject) {
|
|
47
|
+
// Convert NSNumber to NSNumber (handling nil for optional params)
|
|
48
|
+
// Swift side expects NSNumber which can represent nil via NSNull
|
|
49
49
|
[_swiftImpl
|
|
50
|
-
requestIOSDeclaredAgeRangeWithFirstThresholdAge
|
|
51
|
-
secondThresholdAge
|
|
52
|
-
thirdThresholdAge
|
|
50
|
+
requestIOSDeclaredAgeRangeWithFirstThresholdAge:firstThresholdAge
|
|
51
|
+
secondThresholdAge:secondThresholdAge
|
|
52
|
+
thirdThresholdAge:thirdThresholdAge
|
|
53
53
|
resolve:resolve
|
|
54
54
|
reject:reject];
|
|
55
55
|
}
|
|
@@ -59,6 +59,33 @@ RCT_EXPORT_METHOD(isEligibleForAgeFeatures : (RCTPromiseResolveBlock)
|
|
|
59
59
|
[_swiftImpl isEligibleForAgeFeaturesWithResolve:resolve reject:reject];
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
+
// MARK: - PermissionKit: Significant Change API
|
|
63
|
+
|
|
64
|
+
RCT_EXPORT_METHOD(requestSignificantChangeApproval : (RCTPromiseResolveBlock)
|
|
65
|
+
resolve reject : (RCTPromiseRejectBlock)reject) {
|
|
66
|
+
[_swiftImpl requestSignificantChangeApprovalWithResolve:resolve reject:reject];
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// MARK: - PermissionKit: Communication Limits API
|
|
70
|
+
|
|
71
|
+
RCT_EXPORT_METHOD(requestCommunicationPermission : (NSArray *)contacts
|
|
72
|
+
actions : (NSArray *)actions
|
|
73
|
+
resolve : (RCTPromiseResolveBlock)resolve
|
|
74
|
+
reject : (RCTPromiseRejectBlock)reject) {
|
|
75
|
+
[_swiftImpl requestCommunicationPermissionWithContacts:contacts
|
|
76
|
+
actions:actions
|
|
77
|
+
resolve:resolve
|
|
78
|
+
reject:reject];
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
RCT_EXPORT_METHOD(getKnownCommunicationHandles : (NSArray *)handles
|
|
82
|
+
resolve : (RCTPromiseResolveBlock)resolve
|
|
83
|
+
reject : (RCTPromiseRejectBlock)reject) {
|
|
84
|
+
[_swiftImpl getKnownCommunicationHandlesWithHandles:handles
|
|
85
|
+
resolve:resolve
|
|
86
|
+
reject:reject];
|
|
87
|
+
}
|
|
88
|
+
|
|
62
89
|
// Don't compile this code when we build for the old architecture.
|
|
63
90
|
// Don't compile this code when we build for the old architecture.
|
|
64
91
|
#ifdef RCT_NEW_ARCH_ENABLED
|
|
@@ -5,6 +5,11 @@ import StoreKit
|
|
|
5
5
|
#if canImport(DeclaredAgeRange)
|
|
6
6
|
import DeclaredAgeRange
|
|
7
7
|
#endif
|
|
8
|
+
|
|
9
|
+
#if canImport(PermissionKit)
|
|
10
|
+
import PermissionKit
|
|
11
|
+
#endif
|
|
12
|
+
|
|
8
13
|
import UIKit
|
|
9
14
|
|
|
10
15
|
// Check if DeclaredAgeRange type exists (iOS 18+) or handled via availability checks
|
|
@@ -21,8 +26,8 @@ public class StoreAgeSignalsNativeModulesSwift: NSObject {
|
|
|
21
26
|
@objc
|
|
22
27
|
public func requestIOSDeclaredAgeRange(
|
|
23
28
|
firstThresholdAge: NSNumber,
|
|
24
|
-
secondThresholdAge: NSNumber
|
|
25
|
-
thirdThresholdAge: NSNumber
|
|
29
|
+
secondThresholdAge: NSNumber?,
|
|
30
|
+
thirdThresholdAge: NSNumber?,
|
|
26
31
|
resolve: @escaping RCTPromiseResolveBlock,
|
|
27
32
|
reject: @escaping RCTPromiseRejectBlock
|
|
28
33
|
) {
|
|
@@ -36,16 +41,21 @@ public class StoreAgeSignalsNativeModulesSwift: NSObject {
|
|
|
36
41
|
reject("VIEW_CONTROLLER_ERROR", "Could not find top view controller", nil)
|
|
37
42
|
return
|
|
38
43
|
}
|
|
39
|
-
|
|
44
|
+
|
|
45
|
+
// Convert NSNumber to Int, handling optional parameters
|
|
40
46
|
let t1 = Int(truncating: firstThresholdAge)
|
|
41
|
-
let t2 = Int(truncating:
|
|
42
|
-
let t3 = Int(truncating:
|
|
43
|
-
|
|
44
|
-
//
|
|
45
|
-
let response
|
|
46
|
-
|
|
47
|
-
in: viewController
|
|
48
|
-
|
|
47
|
+
let t2 = secondThresholdAge.map { Int(truncating: $0) }
|
|
48
|
+
let t3 = thirdThresholdAge.map { Int(truncating: $0) }
|
|
49
|
+
|
|
50
|
+
// Call API with provided thresholds (variadic params require separate calls)
|
|
51
|
+
let response: AgeRangeService.Response
|
|
52
|
+
if let t2, let t3 {
|
|
53
|
+
response = try await AgeRangeService.shared.requestAgeRange(ageGates: t1, t2, t3, in: viewController)
|
|
54
|
+
} else if let t2 {
|
|
55
|
+
response = try await AgeRangeService.shared.requestAgeRange(ageGates: t1, t2, in: viewController)
|
|
56
|
+
} else {
|
|
57
|
+
response = try await AgeRangeService.shared.requestAgeRange(ageGates: t1, in: viewController)
|
|
58
|
+
}
|
|
49
59
|
|
|
50
60
|
var statusString = "declined"
|
|
51
61
|
var lowerBound: NSNumber? = nil
|
|
@@ -160,6 +170,222 @@ public class StoreAgeSignalsNativeModulesSwift: NSObject {
|
|
|
160
170
|
#endif
|
|
161
171
|
}
|
|
162
172
|
|
|
173
|
+
// MARK: - PermissionKit: Significant Change API
|
|
174
|
+
|
|
175
|
+
@objc
|
|
176
|
+
public func requestSignificantChangeApproval(
|
|
177
|
+
resolve: @escaping RCTPromiseResolveBlock,
|
|
178
|
+
reject: @escaping RCTPromiseRejectBlock
|
|
179
|
+
) {
|
|
180
|
+
#if compiler(>=6.0) && canImport(PermissionKit)
|
|
181
|
+
if #available(iOS 26.0, *) {
|
|
182
|
+
Task { @MainActor in
|
|
183
|
+
do {
|
|
184
|
+
guard let viewController = self.topViewController() else {
|
|
185
|
+
reject("VIEW_CONTROLLER_ERROR", "Could not find top view controller", nil)
|
|
186
|
+
return
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
var topic = SignificantAppUpdateTopic()
|
|
190
|
+
var question = PermissionQuestion(significantAppUpdateTopic: topic)
|
|
191
|
+
|
|
192
|
+
try await AskCenter.current.ask(question: question, in: viewController)
|
|
193
|
+
|
|
194
|
+
// If we get here without error, the request was shown successfully
|
|
195
|
+
// The actual approval status is delivered asynchronously via updates
|
|
196
|
+
let resultMap: [String: Any?] = [
|
|
197
|
+
"status": "pending",
|
|
198
|
+
"error": nil
|
|
199
|
+
]
|
|
200
|
+
resolve(resultMap)
|
|
201
|
+
|
|
202
|
+
} catch {
|
|
203
|
+
var errorMsg = error.localizedDescription
|
|
204
|
+
if errorMsg.contains("region") {
|
|
205
|
+
errorMsg += ". (Hint: User may be in a region that does not support this feature.)"
|
|
206
|
+
}
|
|
207
|
+
reject("ERR_IOS_SIGNIFICANT_CHANGE", errorMsg, error)
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
} else {
|
|
211
|
+
resolve(["status": nil, "error": "Requires iOS 26.0+"])
|
|
212
|
+
}
|
|
213
|
+
#else
|
|
214
|
+
resolve(["status": nil, "error": "PermissionKit SDK not available"])
|
|
215
|
+
#endif
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// MARK: - PermissionKit: Communication Limits API
|
|
219
|
+
|
|
220
|
+
@objc
|
|
221
|
+
public func requestCommunicationPermission(
|
|
222
|
+
contacts: NSArray,
|
|
223
|
+
actions: NSArray,
|
|
224
|
+
resolve: @escaping RCTPromiseResolveBlock,
|
|
225
|
+
reject: @escaping RCTPromiseRejectBlock
|
|
226
|
+
) {
|
|
227
|
+
#if compiler(>=6.0) && canImport(PermissionKit)
|
|
228
|
+
if #available(iOS 26.2, *) {
|
|
229
|
+
Task { @MainActor in
|
|
230
|
+
do {
|
|
231
|
+
guard let viewController = self.topViewController() else {
|
|
232
|
+
reject("VIEW_CONTROLLER_ERROR", "Could not find top view controller", nil)
|
|
233
|
+
return
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Parse contacts from JS
|
|
237
|
+
var personInfoList: [PersonInformation] = []
|
|
238
|
+
for contact in contacts {
|
|
239
|
+
guard let contactDict = contact as? [String: Any],
|
|
240
|
+
let handleValue = contactDict["handle"] as? String,
|
|
241
|
+
let handleKindStr = contactDict["handleKind"] as? String else {
|
|
242
|
+
continue
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
let handleKind: CommunicationHandle.Kind
|
|
246
|
+
switch handleKindStr {
|
|
247
|
+
case "phoneNumber":
|
|
248
|
+
handleKind = .phoneNumber
|
|
249
|
+
case "email":
|
|
250
|
+
handleKind = .email
|
|
251
|
+
default:
|
|
252
|
+
handleKind = .custom
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
let handle = CommunicationHandle(value: handleValue, kind: handleKind)
|
|
256
|
+
|
|
257
|
+
// Optional display name
|
|
258
|
+
var nameComponents: PersonNameComponents? = nil
|
|
259
|
+
if let displayName = contactDict["displayName"] as? String {
|
|
260
|
+
var components = PersonNameComponents()
|
|
261
|
+
components.nickname = displayName
|
|
262
|
+
nameComponents = components
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
let personInfo = PersonInformation(handle: handle, nameComponents: nameComponents)
|
|
266
|
+
personInfoList.append(personInfo)
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
guard !personInfoList.isEmpty else {
|
|
270
|
+
reject("ERR_IOS_COMM_PERMISSION", "No valid contacts provided", nil)
|
|
271
|
+
return
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// Parse actions
|
|
275
|
+
var communicationActions: Set<CommunicationTopic.Action> = []
|
|
276
|
+
for action in actions {
|
|
277
|
+
if let actionStr = action as? String {
|
|
278
|
+
switch actionStr {
|
|
279
|
+
case "message":
|
|
280
|
+
communicationActions.insert(.message)
|
|
281
|
+
case "call":
|
|
282
|
+
communicationActions.insert(.call)
|
|
283
|
+
case "video":
|
|
284
|
+
communicationActions.insert(.video)
|
|
285
|
+
default:
|
|
286
|
+
break
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// Default to message if no actions specified
|
|
292
|
+
if communicationActions.isEmpty {
|
|
293
|
+
communicationActions.insert(.message)
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
var topic = CommunicationTopic(personInformation: personInfoList)
|
|
297
|
+
topic.actions = communicationActions
|
|
298
|
+
|
|
299
|
+
var question = PermissionQuestion(communicationTopic: topic)
|
|
300
|
+
|
|
301
|
+
try await CommunicationLimits.current.ask(question, in: viewController)
|
|
302
|
+
|
|
303
|
+
// If we get here without error, the request was shown successfully
|
|
304
|
+
let resultMap: [String: Any?] = [
|
|
305
|
+
"granted": true,
|
|
306
|
+
"error": nil
|
|
307
|
+
]
|
|
308
|
+
resolve(resultMap)
|
|
309
|
+
|
|
310
|
+
} catch {
|
|
311
|
+
var errorMsg = error.localizedDescription
|
|
312
|
+
if errorMsg.contains("region") {
|
|
313
|
+
errorMsg += ". (Hint: User may be in a region that does not support this feature.)"
|
|
314
|
+
} else if errorMsg.contains("Family Sharing") || errorMsg.contains("Communication Limits") {
|
|
315
|
+
errorMsg += ". (Hint: Family Sharing and Communication Limits must be enabled.)"
|
|
316
|
+
}
|
|
317
|
+
reject("ERR_IOS_COMM_PERMISSION", errorMsg, error)
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
} else {
|
|
321
|
+
resolve(["granted": false, "error": "Requires iOS 26.2+"])
|
|
322
|
+
}
|
|
323
|
+
#else
|
|
324
|
+
resolve(["granted": false, "error": "PermissionKit SDK not available"])
|
|
325
|
+
#endif
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
@objc
|
|
329
|
+
public func getKnownCommunicationHandles(
|
|
330
|
+
handles: NSArray,
|
|
331
|
+
resolve: @escaping RCTPromiseResolveBlock,
|
|
332
|
+
reject: @escaping RCTPromiseRejectBlock
|
|
333
|
+
) {
|
|
334
|
+
#if compiler(>=6.0) && canImport(PermissionKit)
|
|
335
|
+
if #available(iOS 26.2, *) {
|
|
336
|
+
Task {
|
|
337
|
+
do {
|
|
338
|
+
// Parse handles from JS
|
|
339
|
+
var communicationHandles: Set<CommunicationHandle> = []
|
|
340
|
+
for handle in handles {
|
|
341
|
+
guard let handleDict = handle as? [String: Any],
|
|
342
|
+
let handleValue = handleDict["handle"] as? String,
|
|
343
|
+
let handleKindStr = handleDict["handleKind"] as? String else {
|
|
344
|
+
continue
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
let handleKind: CommunicationHandle.Kind
|
|
348
|
+
switch handleKindStr {
|
|
349
|
+
case "phoneNumber":
|
|
350
|
+
handleKind = .phoneNumber
|
|
351
|
+
case "email":
|
|
352
|
+
handleKind = .email
|
|
353
|
+
default:
|
|
354
|
+
handleKind = .custom
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
let commHandle = CommunicationHandle(value: handleValue, kind: handleKind)
|
|
358
|
+
communicationHandles.insert(commHandle)
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
guard !communicationHandles.isEmpty else {
|
|
362
|
+
reject("ERR_IOS_KNOWN_HANDLES", "No valid handles provided", nil)
|
|
363
|
+
return
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
let knownHandles = await CommunicationLimits.current.knownHandles(in: communicationHandles)
|
|
367
|
+
|
|
368
|
+
// Convert back to string array for JS
|
|
369
|
+
let knownHandleValues = knownHandles.map { $0.value }
|
|
370
|
+
|
|
371
|
+
let resultMap: [String: Any?] = [
|
|
372
|
+
"knownHandles": knownHandleValues,
|
|
373
|
+
"error": nil
|
|
374
|
+
]
|
|
375
|
+
resolve(resultMap)
|
|
376
|
+
|
|
377
|
+
} catch {
|
|
378
|
+
reject("ERR_IOS_KNOWN_HANDLES", error.localizedDescription, error)
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
} else {
|
|
382
|
+
resolve(["knownHandles": [], "error": "Requires iOS 26.2+"])
|
|
383
|
+
}
|
|
384
|
+
#else
|
|
385
|
+
resolve(["knownHandles": [], "error": "PermissionKit SDK not available"])
|
|
386
|
+
#endif
|
|
387
|
+
}
|
|
388
|
+
|
|
163
389
|
// Helper to get top view controller
|
|
164
390
|
private func topViewController() -> UIViewController? {
|
|
165
391
|
guard let windowScene = UIApplication.shared.connectedScenes
|