appsonair-react-native-apppush 0.0.1-alpha

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.
Files changed (46) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +540 -0
  3. package/android/build.gradle +177 -0
  4. package/android/gradle.properties +15 -0
  5. package/android/src/main/AndroidManifest.xml +2 -0
  6. package/android/src/main/AndroidManifestNew.xml +2 -0
  7. package/android/src/main/java/com/appsonairreactnativeapppush/AppsonairReactNativeApppushModuleImpl.kt +767 -0
  8. package/android/src/main/java/com/appsonairreactnativeapppush/AppsonairReactNativeApppushPackage.kt +43 -0
  9. package/android/src/newarch/java/com/appsonairreactnativeapppush/AppsonairReactNativeApppushModule.kt +199 -0
  10. package/android/src/oldarch/java/com/appsonairreactnativeapppush/AppsonairReactNativeApppushModule.kt +249 -0
  11. package/appsonair-react-native-apppush.podspec +77 -0
  12. package/ios/AppsonairReactNativeApppush-Bridging-Header.h +12 -0
  13. package/ios/AppsonairReactNativeApppush.h +28 -0
  14. package/ios/AppsonairReactNativeApppush.mm +494 -0
  15. package/ios/AppsonairReactNativeApppushImpl.swift +594 -0
  16. package/lib/commonjs/NativeAppsonairApppush.js +48 -0
  17. package/lib/commonjs/NativeAppsonairApppush.js.map +1 -0
  18. package/lib/commonjs/index.js +686 -0
  19. package/lib/commonjs/index.js.map +1 -0
  20. package/lib/commonjs/types.js +2 -0
  21. package/lib/commonjs/types.js.map +1 -0
  22. package/lib/module/NativeAppsonairApppush.js +47 -0
  23. package/lib/module/NativeAppsonairApppush.js.map +1 -0
  24. package/lib/module/index.js +612 -0
  25. package/lib/module/index.js.map +1 -0
  26. package/lib/module/types.js +2 -0
  27. package/lib/module/types.js.map +1 -0
  28. package/lib/typescript/commonjs/package.json +1 -0
  29. package/lib/typescript/commonjs/src/NativeAppsonairApppush.d.ts +134 -0
  30. package/lib/typescript/commonjs/src/NativeAppsonairApppush.d.ts.map +1 -0
  31. package/lib/typescript/commonjs/src/index.d.ts +392 -0
  32. package/lib/typescript/commonjs/src/index.d.ts.map +1 -0
  33. package/lib/typescript/commonjs/src/types.d.ts +221 -0
  34. package/lib/typescript/commonjs/src/types.d.ts.map +1 -0
  35. package/lib/typescript/module/package.json +1 -0
  36. package/lib/typescript/module/src/NativeAppsonairApppush.d.ts +134 -0
  37. package/lib/typescript/module/src/NativeAppsonairApppush.d.ts.map +1 -0
  38. package/lib/typescript/module/src/index.d.ts +392 -0
  39. package/lib/typescript/module/src/index.d.ts.map +1 -0
  40. package/lib/typescript/module/src/types.d.ts +221 -0
  41. package/lib/typescript/module/src/types.d.ts.map +1 -0
  42. package/package.json +119 -0
  43. package/react-native.config.js +14 -0
  44. package/src/NativeAppsonairApppush.ts +188 -0
  45. package/src/index.tsx +745 -0
  46. package/src/types.ts +284 -0
@@ -0,0 +1,594 @@
1
+ import Foundation
2
+ import AppsOnAir_AppPush
3
+
4
+ #if canImport(React)
5
+ import React
6
+ #endif
7
+
8
+ /**
9
+ Swift half of the iOS bridge.
10
+
11
+ The ObjC++ class in AppsonairReactNativeApppush.mm owns one of these and forwards
12
+ every call to it. The split exists because the two halves can only do one job
13
+ each: the native `AppPushService` API is Swift-only (its static members are not
14
+ `@objc`, so ObjC cannot reach them), while the Codegen-generated
15
+ `NativeAppsonairApppushSpec` protocol is ObjC-only and cannot be adopted from Swift.
16
+
17
+ Threading: `AppPushService` is `@MainActor`-isolated in its entirety (parity A4),
18
+ and React Native calls native modules off the main queue. Every method here
19
+ therefore hops via `Task { @MainActor in ... }` before touching the SDK, and
20
+ resolves its promise from inside that hop.
21
+ */
22
+ @objc(AppsonairReactNativeApppushImpl)
23
+ public class AppsonairReactNativeApppushImpl: NSObject {
24
+
25
+ /// Set by the ObjC++ class to `sendEventWithName:body:`. Nil until the module
26
+ /// has JS listeners, which is why every emit site checks it.
27
+ @objc public var eventSink: ((String, [String: Any]) -> Void)?
28
+
29
+ // Event names. Duplicated in src/index.tsx and the Android bridge -- a rename
30
+ // has to land in all three at once.
31
+ private enum Event {
32
+ static let tokenUpdated = "AppsonairPush:onTokenUpdated"
33
+ static let notificationReceived = "AppsonairPush:onNotificationReceived"
34
+ static let notificationOpened = "AppsonairPush:onNotificationOpened"
35
+ static let notificationWillDisplay = "AppsonairPush:onNotificationWillDisplay"
36
+ static let permissionChanged = "AppsonairPush:onPermissionChanged"
37
+ static let subscriptionChanged = "AppsonairPush:onSubscriptionChanged"
38
+ static let userStateChanged = "AppsonairPush:onUserStateChanged"
39
+ static let silentNotification = "AppsonairPush:onSilentNotification"
40
+ static let error = "AppsonairPush:onError"
41
+ }
42
+
43
+ private var listenersRegistered = false
44
+
45
+ private func emit(_ name: String, _ body: [String: Any]) {
46
+ eventSink?(name, body)
47
+ }
48
+
49
+ // MARK: - Promise helpers
50
+ //
51
+ // Parity I1/I2: the JS layer guards initialisation and validates arguments, so
52
+ // these exist for the residual case where the SDK itself refuses a call. iOS
53
+ // logs and returns rather than throwing, so there is nothing to catch -- the
54
+ // helpers are only about getting onto the main actor before every SDK touch.
55
+
56
+ private func onMain(_ resolve: @escaping RCTPromiseResolveBlock, _ work: @escaping @MainActor () -> Any?) {
57
+ Task { @MainActor in
58
+ resolve(work())
59
+ }
60
+ }
61
+
62
+ private func onMainVoid(_ resolve: @escaping RCTPromiseResolveBlock, _ work: @escaping @MainActor () -> Void) {
63
+ Task { @MainActor in
64
+ work()
65
+ resolve(nil)
66
+ }
67
+ }
68
+
69
+ // MARK: - Lifecycle
70
+
71
+ @objc public func initialize(
72
+ _ config: NSDictionary,
73
+ resolve: @escaping RCTPromiseResolveBlock,
74
+ reject: @escaping RCTPromiseRejectBlock
75
+ ) {
76
+ Task { @MainActor in
77
+ let debug = (config["debug"] as? NSNumber)?.boolValue ?? false
78
+
79
+ // `swizzle: true` is not configurable on purpose. With it off the host app
80
+ // must forward APNs callbacks from its own AppDelegate, which a JS-only
81
+ // integration has no way to do -- so the wrapper always takes the automatic path.
82
+ AppPushService.initialize(debug: debug, swizzle: true)
83
+
84
+ self.registerListeners()
85
+
86
+ // Parity H1: iOS needs an explicit registration step that Android has no
87
+ // equivalent for, and BGTaskScheduler requires it before the app finishes
88
+ // launching. Doing it here keeps scheduleBackgroundSync() symmetrical.
89
+ AppsOnAirBackgroundSync.registerHandlers()
90
+
91
+ resolve(nil)
92
+ }
93
+ }
94
+
95
+ @MainActor
96
+ private func registerListeners() {
97
+ guard !listenersRegistered else { return }
98
+ listenersRegistered = true
99
+
100
+ AppPushService.setListener(self)
101
+ AppPushService.Notifications.addClickListener(self)
102
+ AppPushService.Notifications.addForegroundLifecycleListener(self)
103
+ AppPushService.Notifications.addPermissionObserver(self)
104
+ AppPushService.User.pushSubscription.addObserver(self)
105
+ AppPushService.User.addObserver(self)
106
+
107
+ // Parity F5: silent push is an iOS-only concept. The completion handler must
108
+ // be called or the OS penalises the app's background budget, so it is invoked
109
+ // immediately -- JS gets the payload but cannot extend the background window.
110
+ AppPushService.onSilentPushReceived = { [weak self] userInfo, completion in
111
+ self?.emit(Event.silentNotification, ["data": Self.flatten(userInfo)])
112
+ completion(.newData)
113
+ }
114
+ }
115
+
116
+ // MARK: - Identity
117
+
118
+ @objc public func getDeviceId(_ resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
119
+ onMain(resolve) { AppPushService.deviceId }
120
+ }
121
+
122
+ @objc public func getSubscriptionId(_ resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
123
+ onMain(resolve) { AppPushService.subscriptionId }
124
+ }
125
+
126
+ @objc public func setSubscriptionId(_ id: String, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
127
+ onMainVoid(resolve) { AppPushService.setSubscriptionId(id) }
128
+ }
129
+
130
+ @objc public func getExternalId(_ resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
131
+ onMain(resolve) { AppPushService.User.externalId }
132
+ }
133
+
134
+ @objc public func login(_ externalId: String, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
135
+ onMainVoid(resolve) { AppPushService.login(externalId) }
136
+ }
137
+
138
+ @objc public func logout(_ resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
139
+ onMainVoid(resolve) { AppPushService.logout() }
140
+ }
141
+
142
+ // MARK: - Token
143
+
144
+ @objc public func getToken(_ resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
145
+ onMain(resolve) { AppPushService.User.pushSubscription.token }
146
+ }
147
+
148
+ /// Parity D4: APNs re-registration is OS-driven, so there is nothing to refresh.
149
+ @objc public func refreshToken(_ resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
150
+ resolve(nil)
151
+ }
152
+
153
+ /// Parity B2: a Firebase concept with no APNs equivalent.
154
+ @objc public func getInstallationId(_ resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
155
+ resolve(nil)
156
+ }
157
+
158
+ @objc public func getApnsEnvironment(_ resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
159
+ onMain(resolve) { AppPushService.apnsEnvironment.rawValue }
160
+ }
161
+
162
+ // MARK: - Permissions
163
+
164
+ @objc public func requestPermission(
165
+ _ fallbackToSettings: Bool,
166
+ resolve: @escaping RCTPromiseResolveBlock,
167
+ reject: @escaping RCTPromiseRejectBlock
168
+ ) {
169
+ Task { @MainActor in
170
+ AppPushService.Notifications.requestPermission(fallbackToSettings: fallbackToSettings)
171
+ // requestPermission() is fire-and-forget on iOS too; refreshPermission()
172
+ // awaits the authorization-status read, which settles once the system
173
+ // prompt has been answered.
174
+ let granted = await AppPushService.Notifications.refreshPermission()
175
+ resolve(granted)
176
+ }
177
+ }
178
+
179
+ @objc public func getPermission(_ resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
180
+ Task { @MainActor in
181
+ // Parity E3: `Notifications.permission` is a cache refreshed on launch and
182
+ // foreground. Refreshing first is what makes this agree with Android's live read.
183
+ resolve(await AppPushService.Notifications.refreshPermission())
184
+ }
185
+ }
186
+
187
+ @objc public func getPermissionStatus(_ resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
188
+ Task { @MainActor in
189
+ _ = await AppPushService.Notifications.refreshPermission()
190
+ let status: String
191
+ switch AppPushService.Notifications.permissionNative {
192
+ case .notDetermined: status = "notDetermined"
193
+ case .denied: status = "denied"
194
+ case .authorized: status = "authorized"
195
+ case .provisional: status = "provisional"
196
+ case .ephemeral: status = "ephemeral"
197
+ }
198
+ resolve(status)
199
+ }
200
+ }
201
+
202
+ @objc public func canRequestPermission(_ resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
203
+ onMain(resolve) { AppPushService.Notifications.canRequestPermission }
204
+ }
205
+
206
+ @objc public func registerForProvisionalAuthorization(_ resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
207
+ onMainVoid(resolve) { AppPushService.Notifications.registerForProvisionalAuthorization() }
208
+ }
209
+
210
+ // MARK: - Notifications
211
+
212
+ @objc public func clearAllNotifications(_ resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
213
+ onMainVoid(resolve) { AppPushService.Notifications.clearAllNotifications() }
214
+ }
215
+
216
+ @objc public func removeNotification(_ notificationId: String, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
217
+ onMainVoid(resolve) {
218
+ AppPushService.Notifications.removeNotification(withIdentifier: notificationId)
219
+ }
220
+ }
221
+
222
+ @objc public func removeNotifications(_ notificationIds: NSArray, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
223
+ let ids = notificationIds.compactMap { $0 as? String }
224
+ onMainVoid(resolve) {
225
+ AppPushService.Notifications.removeNotifications(withIdentifiers: ids)
226
+ }
227
+ }
228
+
229
+ /// Parity F3: group keys are an Android/FCM concept.
230
+ @objc public func removeNotificationGroup(_ groupKey: String, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
231
+ resolve(nil)
232
+ }
233
+
234
+ /// Parity F4: notification channels are an Android 8+ concept.
235
+ @objc public func createNotificationChannel(_ config: NSDictionary, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
236
+ resolve(nil)
237
+ }
238
+
239
+ @objc public func deleteNotificationChannel(_ channelId: String, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
240
+ resolve(nil)
241
+ }
242
+
243
+ // MARK: - Badges
244
+
245
+ @objc public func getBadgeCount(_ resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
246
+ onMain(resolve) { AppPushService.badgeCount }
247
+ }
248
+
249
+ @objc public func setBadgeCount(_ count: Double, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
250
+ onMainVoid(resolve) { AppPushService.setBadgeCount(Int(count)) }
251
+ }
252
+
253
+ @objc public func incrementBadgeCount(_ delta: Double, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
254
+ onMain(resolve) { AppPushService.incrementBadgeCount(by: Int(delta)) }
255
+ }
256
+
257
+ @objc public func clearBadgeCount(_ resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
258
+ onMainVoid(resolve) { AppPushService.clearBadgeCount() }
259
+ }
260
+
261
+ @objc public func setAutoClearBadgeOnForeground(_ enabled: Bool, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
262
+ onMainVoid(resolve) { AppPushService.autoClearBadgeOnForeground = enabled }
263
+ }
264
+
265
+ /// Runtime override of the `AppsOnAirDisableAutoRegister` Info.plist default.
266
+ @objc public func setAutoRegisterForRemoteNotifications(_ enabled: Bool, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
267
+ onMainVoid(resolve) { AppPushService.autoRegisterForRemoteNotifications = enabled }
268
+ }
269
+
270
+ // MARK: - User
271
+
272
+ @objc public func addTag(_ key: String, value: String, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
273
+ onMainVoid(resolve) { AppPushService.User.addTag(key: key, value: value) }
274
+ }
275
+
276
+ @objc public func addTags(_ tags: NSDictionary, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
277
+ let map = Self.stringMap(tags)
278
+ onMainVoid(resolve) { AppPushService.User.addTags(map) }
279
+ }
280
+
281
+ @objc public func removeTag(_ key: String, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
282
+ onMainVoid(resolve) { AppPushService.User.removeTag(key) }
283
+ }
284
+
285
+ @objc public func removeTags(_ keys: NSArray, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
286
+ let list = keys.compactMap { $0 as? String }
287
+ onMainVoid(resolve) { AppPushService.User.removeTags(list) }
288
+ }
289
+
290
+ @objc public func getTags(_ resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
291
+ onMain(resolve) { AppPushService.User.getTags() }
292
+ }
293
+
294
+ @objc public func addAlias(_ label: String, id: String, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
295
+ onMainVoid(resolve) { AppPushService.User.addAlias(label: label, id: id) }
296
+ }
297
+
298
+ @objc public func addAliases(_ aliases: NSDictionary, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
299
+ let map = Self.stringMap(aliases)
300
+ onMainVoid(resolve) { AppPushService.User.addAliases(map) }
301
+ }
302
+
303
+ @objc public func removeAlias(_ label: String, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
304
+ onMainVoid(resolve) { AppPushService.User.removeAlias(label) }
305
+ }
306
+
307
+ @objc public func removeAliases(_ labels: NSArray, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
308
+ let list = labels.compactMap { $0 as? String }
309
+ onMainVoid(resolve) { AppPushService.User.removeAliases(list) }
310
+ }
311
+
312
+ @objc public func addEmail(_ address: String, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
313
+ onMainVoid(resolve) { AppPushService.User.addEmail(address) }
314
+ }
315
+
316
+ @objc public func removeEmail(_ address: String, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
317
+ onMainVoid(resolve) { AppPushService.User.removeEmail(address) }
318
+ }
319
+
320
+ @objc public func setLanguage(_ languageCode: String, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
321
+ onMainVoid(resolve) { AppPushService.User.setLanguage(languageCode) }
322
+ }
323
+
324
+ @objc public func getLanguage(_ resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
325
+ onMain(resolve) { AppPushService.User.language }
326
+ }
327
+
328
+ @objc public func getPushSubscription(_ resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
329
+ onMain(resolve) {
330
+ let subscription = AppPushService.User.pushSubscription
331
+ return [
332
+ "id": subscription.id as Any,
333
+ "token": subscription.token as Any,
334
+ "optedIn": subscription.optedIn
335
+ ] as [String: Any]
336
+ }
337
+ }
338
+
339
+ @objc public func optIn(_ resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
340
+ onMainVoid(resolve) { AppPushService.User.pushSubscription.optIn() }
341
+ }
342
+
343
+ @objc public func optOut(_ resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
344
+ onMainVoid(resolve) { AppPushService.User.pushSubscription.optOut() }
345
+ }
346
+
347
+ /**
348
+ `AppPushService.isOptedIn` is the `enabled` flag the SDK reports to
349
+ /subscriptions: an APNs token exists, the user has not opted out, and the OS
350
+ currently grants permission. Android answers the same question by reading the
351
+ value back from the backend, so the two agree in steady state but diverge the
352
+ moment permission is revoked in Settings -- documented on the JS side.
353
+ */
354
+ @objc public func getOptedIn(_ resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
355
+ onMain(resolve) { AppPushService.isOptedIn }
356
+ }
357
+
358
+ // MARK: - Consent
359
+
360
+ @objc public func setConsentRequired(_ required: Bool, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
361
+ onMainVoid(resolve) { AppPushService.consentRequired = required }
362
+ }
363
+
364
+ @objc public func getConsentRequired(_ resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
365
+ onMain(resolve) { AppPushService.consentRequired }
366
+ }
367
+
368
+ @objc public func setConsentGiven(_ given: Bool, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
369
+ onMainVoid(resolve) { AppPushService.consentGiven = given }
370
+ }
371
+
372
+ @objc public func getConsentGiven(_ resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
373
+ onMain(resolve) { AppPushService.consentGiven }
374
+ }
375
+
376
+ // MARK: - Test device
377
+
378
+ @objc public func setTestDevice(_ enabled: Bool, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
379
+ onMainVoid(resolve) { AppPushService.isTestDevice = enabled }
380
+ }
381
+
382
+ @objc public func isTestDevice(_ resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
383
+ onMain(resolve) { AppPushService.isTestDevice }
384
+ }
385
+
386
+ // MARK: - Background sync
387
+
388
+ @objc public func scheduleBackgroundSync(_ options: NSDictionary, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
389
+ // Parity H1: the cross-platform option is minutes; BGTaskScheduler wants a
390
+ // minimum delay in seconds.
391
+ let minutes = (options["intervalMinutes"] as? NSNumber)?.doubleValue ?? 60
392
+ // `requireNetwork` is deliberately unread -- BGAppRefreshTask has no such constraint.
393
+ onMainVoid(resolve) {
394
+ AppsOnAirBackgroundSync.scheduleIfNeeded(minimumDelay: minutes * 60)
395
+ }
396
+ }
397
+
398
+ @objc public func cancelBackgroundSync(_ resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
399
+ onMainVoid(resolve) { AppsOnAirBackgroundSync.cancelPending() }
400
+ }
401
+
402
+ // MARK: - Debug
403
+
404
+ @objc public func setLogLevel(_ level: String, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
405
+ let mapped: LogLevel
406
+ switch level {
407
+ case "none": mapped = .none
408
+ case "fatal": mapped = .fatal
409
+ case "error": mapped = .error
410
+ case "warn": mapped = .warn
411
+ case "info": mapped = .info
412
+ case "debug": mapped = .debug
413
+ case "verbose": mapped = .verbose
414
+ default:
415
+ reject("invalidArgument", "Unknown log level: \(level)", nil)
416
+ return
417
+ }
418
+ onMainVoid(resolve) { AppPushService.Debug.setLogLevel(mapped) }
419
+ }
420
+
421
+ // MARK: - Foreground display control
422
+
423
+ /**
424
+ Parity F7 / B4: a no-op on iOS, by necessity rather than by choice.
425
+
426
+ `AppPushService.handleWillPresent(notification:)` returns its presentation
427
+ options synchronously, so there is no completion handler to hold open while
428
+ JS decides. By the time this call arrives the notification has already been
429
+ presented. The event still fires so JS sees the notification, but
430
+ `preventDefault()` is honoured on Android only -- see the README.
431
+ */
432
+ @objc public func completeNotificationWillDisplay(
433
+ _ notificationId: String,
434
+ display: Bool,
435
+ resolve: @escaping RCTPromiseResolveBlock,
436
+ reject: @escaping RCTPromiseRejectBlock
437
+ ) {
438
+ resolve(nil)
439
+ }
440
+
441
+ // MARK: - Conversions
442
+
443
+ /// Parity C1: iOS's nested `[AnyHashable: Any]` is flattened so `data` means the
444
+ /// same thing as Android's `Map<String, String>`.
445
+ private static func flatten(_ userInfo: [AnyHashable: Any]) -> [String: String] {
446
+ var out: [String: String] = [:]
447
+ for (key, value) in userInfo {
448
+ guard let key = key as? String else { continue }
449
+ switch value {
450
+ case let string as String: out[key] = string
451
+ case let number as NSNumber: out[key] = number.stringValue
452
+ default:
453
+ // Objects and arrays survive as JSON so no payload key is silently lost.
454
+ if let data = try? JSONSerialization.data(withJSONObject: value),
455
+ let json = String(data: data, encoding: .utf8) {
456
+ out[key] = json
457
+ }
458
+ }
459
+ }
460
+ return out
461
+ }
462
+
463
+ private static func stringMap(_ dictionary: NSDictionary) -> [String: String] {
464
+ var out: [String: String] = [:]
465
+ for (key, value) in dictionary {
466
+ guard let key = key as? String, let value = value as? String else { continue }
467
+ out[key] = value
468
+ }
469
+ return out
470
+ }
471
+
472
+ private static func serialize(_ notification: PushNotification) -> [String: Any] {
473
+ return [
474
+ "id": notification.id as Any,
475
+ "campaignId": notification.campaignId as Any,
476
+ "templateId": notification.templateId as Any,
477
+ "sentAt": notification.sentAt as Any,
478
+ "title": notification.title as Any,
479
+ "subtitle": notification.subtitle as Any,
480
+ "body": notification.body as Any,
481
+ "launchUrl": notification.launchUrl as Any,
482
+ "imageUrl": notification.imageUrl as Any,
483
+ "attachments": notification.attachments.map {
484
+ ["id": $0.id as Any, "url": $0.url]
485
+ },
486
+ "actionButtons": notification.actionButtons.map {
487
+ ["id": $0.id, "title": $0.title]
488
+ },
489
+ "badgeIncrement": notification.badgeIncrement as Any,
490
+ "collapseId": notification.collapseId as Any,
491
+ // Parity C1: Android-only payload keys, always null here.
492
+ "sound": NSNull(),
493
+ "channelId": NSNull(),
494
+ "data": flatten(notification.userInfo),
495
+ "rawPayload": flatten(notification.userInfo)
496
+ ]
497
+ }
498
+ }
499
+
500
+ // MARK: - SDK listener conformances
501
+ //
502
+ // Split into extensions so each protocol's methods sit next to the event they emit.
503
+
504
+ extension AppsonairReactNativeApppushImpl: PushListener {
505
+ public func onAPNsTokenUpdated(token: String, environment: APNsEnvironment) {
506
+ // Parity B1: one event shape for both platforms. Android sends `environment: null`.
507
+ emit(Event.tokenUpdated, ["token": token, "environment": environment.rawValue])
508
+ }
509
+
510
+ public func onNotificationReceived(notification: PushNotification) {
511
+ emit(Event.notificationReceived, ["notification": Self.serialize(notification)])
512
+ }
513
+
514
+ public func onNotificationOpened(notification: PushNotification) {
515
+ emit(Event.notificationOpened, [
516
+ "notification": Self.serialize(notification),
517
+ "actionId": NSNull(),
518
+ "url": notification.launchUrl as Any
519
+ ])
520
+ }
521
+
522
+ public func onError(_ error: PushError) {
523
+ // Parity C5: `apnsRegistrationFailed` and Android's TOKEN_FETCH_FAILED both
524
+ // surface as the shared `tokenRegistrationFailed`.
525
+ let code: String
526
+ switch error.code {
527
+ case .notInitialized: code = "notInitialized"
528
+ case .permissionDenied: code = "permissionDenied"
529
+ case .apnsRegistrationFailed: code = "tokenRegistrationFailed"
530
+ case .unknown: code = "unknown"
531
+ }
532
+ emit(Event.error, ["code": code, "message": error.message])
533
+ }
534
+ }
535
+
536
+ extension AppsonairReactNativeApppushImpl: NotificationClickListener {
537
+ public func onClick(event: NotificationClickEvent) {
538
+ emit(Event.notificationOpened, [
539
+ "notification": Self.serialize(event.notification),
540
+ "actionId": event.result.actionId as Any,
541
+ "url": event.result.url as Any
542
+ ])
543
+ }
544
+ }
545
+
546
+ extension AppsonairReactNativeApppushImpl: NotificationLifecycleListener {
547
+ public func onWillDisplay(event: NotificationWillDisplayEvent) {
548
+ // Informational only on iOS -- see completeNotificationWillDisplay().
549
+ emit(Event.notificationWillDisplay, [
550
+ "notification": Self.serialize(event.notification)
551
+ ])
552
+ }
553
+ }
554
+
555
+ extension AppsonairReactNativeApppushImpl: NotificationPermissionObserver {
556
+ public func onNotificationPermissionDidChange(_ permission: Bool) {
557
+ emit(Event.permissionChanged, ["granted": permission])
558
+ }
559
+ }
560
+
561
+ extension AppsonairReactNativeApppushImpl: PushSubscriptionObserver {
562
+ public func onPushSubscriptionDidChange(state: PushSubscriptionChangedState) {
563
+ // `PushSubscriptionChangedState` carries only `token` and `optedIn` on both
564
+ // platforms -- the subscription id lives on the SDK singleton, which is
565
+ // @MainActor-isolated, hence the hop. The Android bridge reads it the same way,
566
+ // so both platforms report the same `id` on both sides of the change.
567
+ Task { @MainActor in
568
+ let id = AppPushService.subscriptionId
569
+ self.emit(Event.subscriptionChanged, [
570
+ "previous": [
571
+ "id": id as Any,
572
+ "token": state.previous.token as Any,
573
+ "optedIn": state.previous.optedIn
574
+ ],
575
+ "current": [
576
+ "id": id as Any,
577
+ "token": state.current.token as Any,
578
+ "optedIn": state.current.optedIn
579
+ ]
580
+ ])
581
+ }
582
+ }
583
+ }
584
+
585
+ extension AppsonairReactNativeApppushImpl: UserStateObserver {
586
+ public func onUserStateDidChange(state: UserChangedState) {
587
+ emit(Event.userStateChanged, [
588
+ "current": [
589
+ "externalId": state.current.externalId as Any,
590
+ "appsOnAirId": state.current.appsOnAirId
591
+ ]
592
+ ])
593
+ }
594
+ }
@@ -0,0 +1,48 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.default = void 0;
7
+ var _reactNative = require("react-native");
8
+ /**
9
+ * Codegen spec for the AppsOnAir Push TurboModule.
10
+ *
11
+ * This file is consumed by React Native Codegen and compiles to
12
+ * `NativeAppsonairApppushSpec` — a Java abstract class and an ObjC protocol that
13
+ * the native modules implement under the New Architecture. On the Old
14
+ * Architecture the same JS object is served by the legacy bridge module, which
15
+ * exposes an identical method set (see android/src/oldarch and the
16
+ * `RCT_NEW_ARCH_ENABLED` guard in ios/AppsonairReactNativeApppush.mm).
17
+ *
18
+ * Constraints this file must respect — codegen rejects or silently mistypes
19
+ * anything else, so do not "improve" the signatures below:
20
+ *
21
+ * - Parameters and returns are primitives, nullable primitives, `Array<T>`,
22
+ * or `UnsafeObject`. Named object types are avoided deliberately -- see the
23
+ * note above `Spec`.
24
+ * - No string-literal unions. `logLevel`, `importance` and `environment` are
25
+ * plain `string` here and are narrowed to their union types in `index.tsx`,
26
+ * which is the type-safe surface consumers actually import.
27
+ * - No `Record<K, V>` — use `UnsafeObject`.
28
+ * - Methods are `Promise`-returning or `void`; there are no sync methods, so
29
+ * the module never blocks the JS thread.
30
+ */
31
+ /**
32
+ * Object parameters are `UnsafeObject`, not named object types, and that is
33
+ * load-bearing rather than lazy.
34
+ *
35
+ * Codegen compiles a named object type into a generated C++ struct, so the
36
+ * ObjC signature becomes `JS::NativeAppsonairApppush::NativePushConfig &` under the
37
+ * New Architecture while the Old Architecture bridge still passes
38
+ * `NSDictionary *`. The two would no longer share a selector, and
39
+ * ios/AppsonairReactNativeApppush.mm could not implement both from one method body.
40
+ * `UnsafeObject` maps to `NSDictionary *` on both, and to `ReadableMap` on both
41
+ * on Android.
42
+ *
43
+ * The shapes these accept are typed for consumers in src/types.ts -- `PushConfig`,
44
+ * `NotificationChannelConfig` and `BackgroundSyncOptions` -- which is the surface
45
+ * anyone actually imports. Nothing here is untyped at the call site.
46
+ */
47
+ var _default = exports.default = _reactNative.TurboModuleRegistry.getEnforcing('AppsonairReactNativeApppush');
48
+ //# sourceMappingURL=NativeAppsonairApppush.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"names":["_reactNative","require","_default","exports","default","TurboModuleRegistry","getEnforcing"],"sourceRoot":"../../src","sources":["NativeAppsonairApppush.ts"],"mappings":";;;;;;AAAA,IAAAA,YAAA,GAAAC,OAAA;AAGA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AAfA,IAAAC,QAAA,GAAAC,OAAA,CAAAC,OAAA,GA8JeC,gCAAmB,CAACC,YAAY,CAC7C,6BACF,CAAC","ignoreList":[]}