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.
- package/LICENSE +21 -0
- package/README.md +540 -0
- package/android/build.gradle +177 -0
- package/android/gradle.properties +15 -0
- package/android/src/main/AndroidManifest.xml +2 -0
- package/android/src/main/AndroidManifestNew.xml +2 -0
- package/android/src/main/java/com/appsonairreactnativeapppush/AppsonairReactNativeApppushModuleImpl.kt +767 -0
- package/android/src/main/java/com/appsonairreactnativeapppush/AppsonairReactNativeApppushPackage.kt +43 -0
- package/android/src/newarch/java/com/appsonairreactnativeapppush/AppsonairReactNativeApppushModule.kt +199 -0
- package/android/src/oldarch/java/com/appsonairreactnativeapppush/AppsonairReactNativeApppushModule.kt +249 -0
- package/appsonair-react-native-apppush.podspec +77 -0
- package/ios/AppsonairReactNativeApppush-Bridging-Header.h +12 -0
- package/ios/AppsonairReactNativeApppush.h +28 -0
- package/ios/AppsonairReactNativeApppush.mm +494 -0
- package/ios/AppsonairReactNativeApppushImpl.swift +594 -0
- package/lib/commonjs/NativeAppsonairApppush.js +48 -0
- package/lib/commonjs/NativeAppsonairApppush.js.map +1 -0
- package/lib/commonjs/index.js +686 -0
- package/lib/commonjs/index.js.map +1 -0
- package/lib/commonjs/types.js +2 -0
- package/lib/commonjs/types.js.map +1 -0
- package/lib/module/NativeAppsonairApppush.js +47 -0
- package/lib/module/NativeAppsonairApppush.js.map +1 -0
- package/lib/module/index.js +612 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/types.js +2 -0
- package/lib/module/types.js.map +1 -0
- package/lib/typescript/commonjs/package.json +1 -0
- package/lib/typescript/commonjs/src/NativeAppsonairApppush.d.ts +134 -0
- package/lib/typescript/commonjs/src/NativeAppsonairApppush.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/index.d.ts +392 -0
- package/lib/typescript/commonjs/src/index.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/types.d.ts +221 -0
- package/lib/typescript/commonjs/src/types.d.ts.map +1 -0
- package/lib/typescript/module/package.json +1 -0
- package/lib/typescript/module/src/NativeAppsonairApppush.d.ts +134 -0
- package/lib/typescript/module/src/NativeAppsonairApppush.d.ts.map +1 -0
- package/lib/typescript/module/src/index.d.ts +392 -0
- package/lib/typescript/module/src/index.d.ts.map +1 -0
- package/lib/typescript/module/src/types.d.ts +221 -0
- package/lib/typescript/module/src/types.d.ts.map +1 -0
- package/package.json +119 -0
- package/react-native.config.js +14 -0
- package/src/NativeAppsonairApppush.ts +188 -0
- package/src/index.tsx +745 -0
- 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":[]}
|