@onesignal/capacitor-plugin 1.0.0 → 1.0.2

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.
@@ -10,11 +10,15 @@ Pod::Spec.new do |s|
10
10
  s.homepage = package['homepage']
11
11
  s.author = 'OneSignal'
12
12
  s.source = { :git => package['repository']['url'], :tag => s.version.to_s }
13
- s.source_files = 'ios/Sources/OneSignalCapacitorPlugin/**/*.swift'
13
+ s.source_files = [
14
+ 'ios/Sources/OneSignalCapacitorPlugin/**/*.swift',
15
+ 'ios/Sources/OSCapacitorLaunchOptions/**/*.{h,m}'
16
+ ]
17
+ s.public_header_files = 'ios/Sources/OSCapacitorLaunchOptions/include/*.h'
14
18
 
15
19
  s.ios.deployment_target = '14.0'
16
20
  s.swift_version = '5.9'
17
21
 
18
22
  s.dependency 'Capacitor'
19
- s.dependency 'OneSignalXCFramework', '5.5.0'
23
+ s.dependency 'OneSignalXCFramework', '5.5.1'
20
24
  end
package/Package.swift CHANGED
@@ -16,6 +16,16 @@ let package = Package(
16
16
  .package(url: "https://github.com/OneSignal/OneSignal-XCFramework", from: "5.0.0")
17
17
  ],
18
18
  targets: [
19
+ // Obj-C helper that captures the iOS launchOptions dictionary at
20
+ // process start (via +load + UIApplicationDidFinishLaunchingNotification)
21
+ // so cold-start notification taps are still available when the JS
22
+ // layer initializes the plugin later. SPM cannot mix Swift and Obj-C
23
+ // in the same target, so this lives as its own target.
24
+ .target(
25
+ name: "OSCapacitorLaunchOptions",
26
+ path: "ios/Sources/OSCapacitorLaunchOptions",
27
+ publicHeadersPath: "include"
28
+ ),
19
29
  .target(
20
30
  name: "OnesignalCapacitorPlugin",
21
31
  dependencies: [
@@ -28,7 +38,8 @@ let package = Package(
28
38
  .product(name: "OneSignalFramework", package: "OneSignal-XCFramework"),
29
39
  .product(name: "OneSignalInAppMessages", package: "OneSignal-XCFramework"),
30
40
  .product(name: "OneSignalLocation", package: "OneSignal-XCFramework"),
31
- .product(name: "OneSignalExtension", package: "OneSignal-XCFramework")
41
+ .product(name: "OneSignalExtension", package: "OneSignal-XCFramework"),
42
+ "OSCapacitorLaunchOptions"
32
43
  ],
33
44
  path: "ios/Sources/OneSignalCapacitorPlugin"
34
45
  )
package/README.md CHANGED
@@ -24,13 +24,13 @@ See the `examples/demo` directory for a full working example.
24
24
 
25
25
  <docgen-index>
26
26
 
27
- - [`initialize(...)`](#initialize)
28
- - [`login(...)`](#login)
29
- - [`logout()`](#logout)
30
- - [`setConsentRequired(...)`](#setconsentrequired)
31
- - [`setConsentGiven(...)`](#setconsentgiven)
32
- - [Interfaces](#interfaces)
33
- - [Type Aliases](#type-aliases)
27
+ * [`initialize(...)`](#initialize)
28
+ * [`login(...)`](#login)
29
+ * [`logout()`](#logout)
30
+ * [`setConsentRequired(...)`](#setconsentrequired)
31
+ * [`setConsentGiven(...)`](#setconsentgiven)
32
+ * [Interfaces](#interfaces)
33
+ * [Type Aliases](#type-aliases)
34
34
 
35
35
  </docgen-index>
36
36
 
@@ -51,7 +51,8 @@ Initialize the SDK with your OneSignal app ID. Call during app startup.
51
51
  | ----------- | ------------------- |
52
52
  | **`appId`** | <code>string</code> |
53
53
 
54
- ---
54
+ --------------------
55
+
55
56
 
56
57
  ### login(...)
57
58
 
@@ -65,7 +66,8 @@ Log in to OneSignal as the user identified by `externalId`, switching the user c
65
66
  | ---------------- | ------------------- |
66
67
  | **`externalId`** | <code>string</code> |
67
68
 
68
- ---
69
+ --------------------
70
+
69
71
 
70
72
  ### logout()
71
73
 
@@ -75,7 +77,8 @@ logout() => Promise<void>
75
77
 
76
78
  Log out the current user. The SDK will reference a new device-scoped user.
77
79
 
78
- ---
80
+ --------------------
81
+
79
82
 
80
83
  ### setConsentRequired(...)
81
84
 
@@ -89,7 +92,8 @@ Set whether user privacy consent is required before sending data to OneSignal. C
89
92
  | -------------- | -------------------- |
90
93
  | **`required`** | <code>boolean</code> |
91
94
 
92
- ---
95
+ --------------------
96
+
93
97
 
94
98
  ### setConsentGiven(...)
95
99
 
@@ -103,10 +107,12 @@ Indicate whether the user has granted privacy consent.
103
107
  | ------------- | -------------------- |
104
108
  | **`granted`** | <code>boolean</code> |
105
109
 
106
- ---
110
+ --------------------
111
+
107
112
 
108
113
  ### Interfaces
109
114
 
115
+
110
116
  #### OneSignalDebugAPI
111
117
 
112
118
  Debug helpers exposed via `OneSignal.Debug`.
@@ -116,6 +122,7 @@ Debug helpers exposed via `OneSignal.Debug`.
116
122
  | **setLogLevel** | (logLevel: <a href="#loglevel">LogLevel</a>) =&gt; void | Set the log level printed to LogCat (Android) or the Xcode console (iOS). |
117
123
  | **setAlertLevel** | (visualLogLevel: <a href="#loglevel">LogLevel</a>) =&gt; void | Set the log level shown to the user as alert dialogs. |
118
124
 
125
+
119
126
  #### OneSignalUserAPI
120
127
 
121
128
  Current-user operations exposed via `OneSignal.User`.
@@ -146,12 +153,14 @@ Current-user operations exposed via `OneSignal.User`.
146
153
  | **getExternalId** | () =&gt; Promise&lt;string \| null&gt; | Get the external ID set via `login`, or null if the user is anonymous. |
147
154
  | **trackEvent** | (name: string, properties?: object \| undefined) =&gt; Promise&lt;void&gt; | Track a custom event with an optional set of JSON-serializable properties. |
148
155
 
156
+
149
157
  #### UserChangedState
150
158
 
151
159
  | Prop | Type |
152
160
  | ------------- | ----------------------------------------------- |
153
161
  | **`current`** | <code><a href="#userstate">UserState</a></code> |
154
162
 
163
+
155
164
  #### UserState
156
165
 
157
166
  | Prop | Type |
@@ -159,6 +168,7 @@ Current-user operations exposed via `OneSignal.User`.
159
168
  | **`onesignalId`** | <code>string</code> |
160
169
  | **`externalId`** | <code>string</code> |
161
170
 
171
+
162
172
  #### OneSignalPushSubscriptionAPI
163
173
 
164
174
  Push subscription state and controls exposed via `OneSignal.User.pushSubscription`.
@@ -173,6 +183,7 @@ Push subscription state and controls exposed via `OneSignal.User.pushSubscriptio
173
183
  | **optIn** | () =&gt; Promise&lt;void&gt; | Opt the user in to push notifications. Prompts for permission if needed. |
174
184
  | **optOut** | () =&gt; Promise&lt;void&gt; | Opt the user out of push notifications on this device. |
175
185
 
186
+
176
187
  #### PushSubscriptionChangedState
177
188
 
178
189
  | Prop | Type |
@@ -180,6 +191,7 @@ Push subscription state and controls exposed via `OneSignal.User.pushSubscriptio
180
191
  | **`previous`** | <code><a href="#pushsubscriptionstate">PushSubscriptionState</a></code> |
181
192
  | **`current`** | <code><a href="#pushsubscriptionstate">PushSubscriptionState</a></code> |
182
193
 
194
+
183
195
  #### PushSubscriptionState
184
196
 
185
197
  | Prop | Type |
@@ -188,6 +200,7 @@ Push subscription state and controls exposed via `OneSignal.User.pushSubscriptio
188
200
  | **`token`** | <code>string</code> |
189
201
  | **`optedIn`** | <code>boolean</code> |
190
202
 
203
+
191
204
  #### OneSignalNotificationsAPI
192
205
 
193
206
  Notification permission and event handling exposed via `OneSignal.Notifications`.
@@ -205,6 +218,7 @@ Notification permission and event handling exposed via `OneSignal.Notifications`
205
218
  | **removeNotification** | (id: number) =&gt; Promise&lt;void&gt; | Android only. Cancel a single notification by its Android notification ID. |
206
219
  | **removeGroupedNotifications** | (id: string) =&gt; Promise&lt;void&gt; | Android only. Cancel a group of notifications by group key. |
207
220
 
221
+
208
222
  #### NotificationClickEvent
209
223
 
210
224
  | Prop | Type |
@@ -212,6 +226,7 @@ Notification permission and event handling exposed via `OneSignal.Notifications`
212
226
  | **`result`** | <code><a href="#notificationclickresult">NotificationClickResult</a></code> |
213
227
  | **`notification`** | <code>OSNotification</code> |
214
228
 
229
+
215
230
  #### NotificationClickResult
216
231
 
217
232
  | Prop | Type |
@@ -219,6 +234,7 @@ Notification permission and event handling exposed via `OneSignal.Notifications`
219
234
  | **`actionId`** | <code>string</code> |
220
235
  | **`url`** | <code>string</code> |
221
236
 
237
+
222
238
  #### OneSignalInAppMessagesAPI
223
239
 
224
240
  In-app message triggers and event handling exposed via `OneSignal.InAppMessages`.
@@ -235,6 +251,7 @@ In-app message triggers and event handling exposed via `OneSignal.InAppMessages`
235
251
  | **setPaused** | (pause: boolean) =&gt; void | Pause or resume the display of in-app messages. |
236
252
  | **getPaused** | () =&gt; Promise&lt;boolean&gt; | Whether in-app messaging is currently paused. |
237
253
 
254
+
238
255
  #### InAppMessageClickEvent
239
256
 
240
257
  | Prop | Type |
@@ -242,12 +259,14 @@ In-app message triggers and event handling exposed via `OneSignal.InAppMessages`
242
259
  | **`message`** | <code><a href="#osinappmessage">OSInAppMessage</a></code> |
243
260
  | **`result`** | <code><a href="#inappmessageclickresult">InAppMessageClickResult</a></code> |
244
261
 
262
+
245
263
  #### OSInAppMessage
246
264
 
247
265
  | Prop | Type |
248
266
  | --------------- | ------------------- |
249
267
  | **`messageId`** | <code>string</code> |
250
268
 
269
+
251
270
  #### InAppMessageClickResult
252
271
 
253
272
  | Prop | Type |
@@ -257,30 +276,35 @@ In-app message triggers and event handling exposed via `OneSignal.InAppMessages`
257
276
  | **`url`** | <code>string</code> |
258
277
  | **`urlTarget`** | <code><a href="#inappmessageactionurltype">InAppMessageActionUrlType</a></code> |
259
278
 
279
+
260
280
  #### InAppMessageWillDisplayEvent
261
281
 
262
282
  | Prop | Type |
263
283
  | ------------- | --------------------------------------------------------- |
264
284
  | **`message`** | <code><a href="#osinappmessage">OSInAppMessage</a></code> |
265
285
 
286
+
266
287
  #### InAppMessageDidDisplayEvent
267
288
 
268
289
  | Prop | Type |
269
290
  | ------------- | --------------------------------------------------------- |
270
291
  | **`message`** | <code><a href="#osinappmessage">OSInAppMessage</a></code> |
271
292
 
293
+
272
294
  #### InAppMessageWillDismissEvent
273
295
 
274
296
  | Prop | Type |
275
297
  | ------------- | --------------------------------------------------------- |
276
298
  | **`message`** | <code><a href="#osinappmessage">OSInAppMessage</a></code> |
277
299
 
300
+
278
301
  #### InAppMessageDidDismissEvent
279
302
 
280
303
  | Prop | Type |
281
304
  | ------------- | --------------------------------------------------------- |
282
305
  | **`message`** | <code><a href="#osinappmessage">OSInAppMessage</a></code> |
283
306
 
307
+
284
308
  #### OneSignalSessionAPI
285
309
 
286
310
  Outcome reporting exposed via `OneSignal.Session`.
@@ -291,6 +315,7 @@ Outcome reporting exposed via `OneSignal.Session`.
291
315
  | **addUniqueOutcome** | (name: string) =&gt; Promise&lt;void&gt; | <a href="#record">Record</a> a unique outcome with the given name against the current session. |
292
316
  | **addOutcomeWithValue** | (name: string, value: number) =&gt; Promise&lt;void&gt; | <a href="#record">Record</a> an outcome with the given name and value against the current session. |
293
317
 
318
+
294
319
  #### OneSignalLocationAPI
295
320
 
296
321
  Location permission and sharing exposed via `OneSignal.Location`.
@@ -301,6 +326,7 @@ Location permission and sharing exposed via `OneSignal.Location`.
301
326
  | **setShared** | (shared: boolean) =&gt; void | Enable or disable sharing the device location with OneSignal. |
302
327
  | **isShared** | () =&gt; Promise&lt;boolean&gt; | Whether the device location is currently shared with OneSignal. |
303
328
 
329
+
304
330
  #### OneSignalLiveActivitiesAPI
305
331
 
306
332
  Live activity controls exposed via `OneSignal.LiveActivities`. iOS only unless noted.
@@ -314,48 +340,56 @@ Live activity controls exposed via `OneSignal.LiveActivities`. iOS only unless n
314
340
  | **setupDefault** | (options?: <a href="#liveactivitysetupoptions">LiveActivitySetupOptions</a> \| undefined) =&gt; Promise&lt;void&gt; | Set up the OneSignal default live activity, optionally enabling pushToStart/pushToUpdate. |
315
341
  | **startDefault** | (activityId: string, attributes: <a href="#record">Record</a>&lt;string, unknown&gt;, content: <a href="#record">Record</a>&lt;string, unknown&gt;) =&gt; Promise&lt;void&gt; | Start a live activity backed by the OneSignal default attributes type. |
316
342
 
343
+
317
344
  ### Type Aliases
318
345
 
346
+
319
347
  #### LogLevel
320
348
 
321
349
  <code>(typeof <a href="#loglevel">LogLevel</a>)[keyof typeof LogLevel]</code>
322
350
 
351
+
323
352
  #### Record
324
353
 
325
354
  Construct a type with a set of properties K of type T
326
355
 
327
- <code>{
328
- [P in K]: T;
329
- }</code>
356
+ <code>{
330
357
  [P in K]: T;
331
358
  }</code>
359
+
332
360
 
333
361
  #### OSNotificationPermission
334
362
 
335
363
  <code>(typeof <a href="#osnotificationpermission">OSNotificationPermission</a>)[keyof typeof OSNotificationPermission]</code>
336
364
 
365
+
337
366
  #### NotificationEventName
338
367
 
339
368
  <code>'click' | 'foregroundWillDisplay' | 'permissionChange'</code>
340
369
 
370
+
341
371
  #### NotificationEventTypeMap
342
372
 
343
373
  <code>{ click: <a href="#notificationclickevent">NotificationClickEvent</a>; foregroundWillDisplay: NotificationWillDisplayEvent; permissionChange: boolean; }</code>
344
374
 
375
+
345
376
  #### InAppMessageEventName
346
377
 
347
378
  <code>'click' | 'willDisplay' | 'didDisplay' | 'willDismiss' | 'didDismiss'</code>
348
379
 
380
+
349
381
  #### InAppMessageEventTypeMap
350
382
 
351
383
  <code>{ click: <a href="#inappmessageclickevent">InAppMessageClickEvent</a>; willDisplay: <a href="#inappmessagewilldisplayevent">InAppMessageWillDisplayEvent</a>; didDisplay: <a href="#inappmessagediddisplayevent">InAppMessageDidDisplayEvent</a>; willDismiss: <a href="#inappmessagewilldismissevent">InAppMessageWillDismissEvent</a>; didDismiss: <a href="#inappmessagediddismissevent">InAppMessageDidDismissEvent</a>; }</code>
352
384
 
385
+
353
386
  #### InAppMessageActionUrlType
354
387
 
355
388
  <code>'browser' | 'webview' | 'replacement'</code>
356
389
 
390
+
357
391
  #### LiveActivitySetupOptions
358
392
 
359
393
  The setup options for `OneSignal.LiveActivities.setupDefault`.
360
394
 
361
- <code>{ /** _ When true, OneSignal will listen for pushToStart tokens for the `OneSignalLiveActivityAttributes` structure. _/ enablePushToStart: boolean; /** _ When true, OneSignal will listen for pushToUpdate tokens for each start live activity that uses the _ `OneSignalLiveActivityAttributes` structure. \*/ enablePushToUpdate: boolean; }</code>
395
+ <code>{ /** * When true, OneSignal will listen for pushToStart tokens for the `OneSignalLiveActivityAttributes` structure. */ enablePushToStart: boolean; /** * When true, OneSignal will listen for pushToUpdate tokens for each start live activity that uses the * `OneSignalLiveActivityAttributes` structure. */ enablePushToUpdate: boolean; }</code>
362
396
 
363
397
  </docgen-api>
@@ -14,7 +14,7 @@ compileSdk = "35"
14
14
  junit = "4.13.2"
15
15
  kotlin = "1.9.25"
16
16
  minSdk = "23"
17
- onesignal = "5.7.7"
17
+ onesignal = "5.8.1"
18
18
  targetSdk = "35"
19
19
 
20
20
  [libraries]
@@ -1,5 +1,6 @@
1
1
  package com.onesignal.capacitor
2
2
 
3
+ import android.app.Application
3
4
  import com.getcapacitor.JSObject
4
5
  import com.getcapacitor.Plugin
5
6
  import com.getcapacitor.PluginCall
@@ -7,6 +8,7 @@ import com.getcapacitor.PluginMethod
7
8
  import com.getcapacitor.annotation.CapacitorPlugin
8
9
  import com.onesignal.OneSignal
9
10
  import com.onesignal.common.OneSignalWrapper
11
+ import com.onesignal.core.internal.application.IApplicationService
10
12
  import com.onesignal.inAppMessages.IInAppMessageClickEvent
11
13
  import com.onesignal.inAppMessages.IInAppMessageClickListener
12
14
  import com.onesignal.inAppMessages.IInAppMessageDidDismissEvent
@@ -24,8 +26,8 @@ import com.onesignal.user.state.IUserStateObserver
24
26
  import com.onesignal.user.state.UserChangedState
25
27
  import com.onesignal.user.subscriptions.IPushSubscriptionObserver
26
28
  import com.onesignal.user.subscriptions.PushSubscriptionChangedState
27
- import kotlinx.coroutines.CoroutineScope
28
- import kotlinx.coroutines.Dispatchers
29
+ import kotlinx.coroutines.MainScope
30
+ import kotlinx.coroutines.cancel
29
31
  import kotlinx.coroutines.launch
30
32
  import org.json.JSONArray
31
33
  import org.json.JSONObject
@@ -37,11 +39,82 @@ class OneSignalCapacitorPlugin : Plugin(),
37
39
  IInAppMessageLifecycleListener,
38
40
  IInAppMessageClickListener {
39
41
 
42
+ companion object {
43
+ // Mirror of iOS UNAuthorizationStatus values so the JS layer can use a
44
+ // single permissionNative() return shape across platforms. Android only
45
+ // distinguishes denied/authorized; the iOS-specific notDetermined (0),
46
+ // provisional (3), and ephemeral (4) states do not apply here.
47
+ private const val PERMISSION_DENIED = 1
48
+ private const val PERMISSION_AUTHORIZED = 2
49
+ }
50
+
40
51
  private val notificationWillDisplayCache = mutableMapOf<String, INotificationWillDisplayEvent>()
41
52
  private val preventDefaultCache = mutableSetOf<String>()
42
- private var pendingClickEvent: INotificationClickEvent? = null
53
+ private var initialized = false
54
+
55
+ // Class-scoped scope so launched permission/location coroutines are tied
56
+ // to the plugin instance lifetime. Cancelled in handleOnDestroy so a
57
+ // pending permission dialog that resolves after the activity dies cannot
58
+ // call into the dead Capacitor bridge.
59
+ private val pluginScope = MainScope()
60
+
61
+ private val permissionObserver = object : IPermissionObserver {
62
+ override fun onNotificationPermissionChange(permission: Boolean) {
63
+ val ret = JSObject()
64
+ ret.put("permission", permission)
65
+ notifyListeners("permissionChange", ret)
66
+ }
67
+ }
68
+
69
+ private val pushSubscriptionObserver = object : IPushSubscriptionObserver {
70
+ override fun onPushSubscriptionChange(state: PushSubscriptionChangedState) {
71
+ val ret = JSObject()
72
+ val prev = JSObject()
73
+ prev.put("id", state.previous.id.ifEmpty { JSONObject.NULL })
74
+ prev.put("token", state.previous.token.ifEmpty { JSONObject.NULL })
75
+ prev.put("optedIn", state.previous.optedIn)
76
+ ret.put("previous", prev)
77
+
78
+ val curr = JSObject()
79
+ curr.put("id", state.current.id.ifEmpty { JSONObject.NULL })
80
+ curr.put("token", state.current.token.ifEmpty { JSONObject.NULL })
81
+ curr.put("optedIn", state.current.optedIn)
82
+ ret.put("current", curr)
83
+
84
+ notifyListeners("pushSubscriptionChange", ret)
85
+ }
86
+ }
87
+
88
+ private val userStateObserver = object : IUserStateObserver {
89
+ override fun onUserStateChange(state: UserChangedState) {
90
+ val ret = JSObject()
91
+ val curr = JSObject()
92
+ curr.put("onesignalId", state.current.onesignalId.ifEmpty { JSONObject.NULL })
93
+ curr.put("externalId", state.current.externalId.ifEmpty { JSONObject.NULL })
94
+ ret.put("current", curr)
95
+ notifyListeners("userStateChange", ret)
96
+ }
97
+ }
43
98
 
44
- // region Core
99
+ override fun handleOnDestroy() {
100
+ // Detach this dead plugin instance so the next plugin instance can
101
+ // receive events; otherwise the SDK keeps firing on this stale
102
+ // instance and skips its unprocessed-click replay queue.
103
+ runCatching {
104
+ OneSignal.Notifications.removePermissionObserver(permissionObserver)
105
+ OneSignal.Notifications.removeForegroundLifecycleListener(this)
106
+ OneSignal.Notifications.removeClickListener(this)
107
+ OneSignal.User.pushSubscription.removeObserver(pushSubscriptionObserver)
108
+ OneSignal.User.removeObserver(userStateObserver)
109
+ OneSignal.InAppMessages.removeLifecycleListener(this)
110
+ OneSignal.InAppMessages.removeClickListener(this)
111
+ }
112
+ pluginScope.cancel()
113
+ // Caches aren't explicitly cleared: GC reclaims them with this
114
+ // instance once the listener removals above run, and runtime size is
115
+ // bounded by consume-on-read in proceedWithWillDisplay/displayNotification.
116
+ super.handleOnDestroy()
117
+ }
45
118
 
46
119
  @PluginMethod
47
120
  fun initialize(call: PluginCall) {
@@ -51,68 +124,56 @@ class OneSignalCapacitorPlugin : Plugin(),
51
124
  return
52
125
  }
53
126
 
127
+ // initialize() is idempotent: JS may call it multiple times per
128
+ // activity (effect re-runs, hot reload). Listeners are detached in
129
+ // handleOnDestroy and the next plugin instance starts fresh.
130
+ if (initialized) {
131
+ call.resolve()
132
+ return
133
+ }
134
+ initialized = true
135
+
54
136
  OneSignalWrapper.sdkType = "capacitor"
55
- OneSignalWrapper.sdkVersion = "010000"
137
+ OneSignalWrapper.sdkVersion = "010002"
56
138
  OneSignal.initWithContext(context, appId)
57
139
 
58
- OneSignal.Notifications.addPermissionObserver(object : IPermissionObserver {
59
- override fun onNotificationPermissionChange(permission: Boolean) {
60
- val ret = JSObject()
61
- ret.put("permission", permission)
62
- notifyListeners("permissionChange", ret)
63
- }
64
- })
140
+ // If the SDK was initialized from a non-Activity context (FCM/work
141
+ // managers) before this call, its ALC missed MainActivity.onResume
142
+ // and isInForeground stays false. Forward the missed events now.
143
+ nudgeApplicationServiceForeground()
65
144
 
145
+ OneSignal.Notifications.addPermissionObserver(permissionObserver)
66
146
  OneSignal.Notifications.addForegroundLifecycleListener(this)
67
147
  OneSignal.Notifications.addClickListener(this)
68
-
69
- OneSignal.User.pushSubscription.addObserver(object : IPushSubscriptionObserver {
70
- override fun onPushSubscriptionChange(state: PushSubscriptionChangedState) {
71
- val ret = JSObject()
72
- val prev = JSObject()
73
- prev.put("id", state.previous.id.ifEmpty { JSONObject.NULL })
74
- prev.put("token", state.previous.token.ifEmpty { JSONObject.NULL })
75
- prev.put("optedIn", state.previous.optedIn)
76
- ret.put("previous", prev)
77
-
78
- val curr = JSObject()
79
- curr.put("id", state.current.id.ifEmpty { JSONObject.NULL })
80
- curr.put("token", state.current.token.ifEmpty { JSONObject.NULL })
81
- curr.put("optedIn", state.current.optedIn)
82
- ret.put("current", curr)
83
-
84
- notifyListeners("pushSubscriptionChange", ret)
85
- }
86
- })
87
-
88
- OneSignal.User.addObserver(object : IUserStateObserver {
89
- override fun onUserStateChange(state: UserChangedState) {
90
- val ret = JSObject()
91
- val curr = JSObject()
92
- curr.put("onesignalId", state.current.onesignalId.ifEmpty { JSONObject.NULL })
93
- curr.put("externalId", state.current.externalId.ifEmpty { JSONObject.NULL })
94
- ret.put("current", curr)
95
- notifyListeners("userStateChange", ret)
96
- }
97
- })
98
-
148
+ OneSignal.User.pushSubscription.addObserver(pushSubscriptionObserver)
149
+ OneSignal.User.addObserver(userStateObserver)
99
150
  OneSignal.InAppMessages.addLifecycleListener(this)
100
151
  OneSignal.InAppMessages.addClickListener(this)
101
152
 
102
- pendingClickEvent?.let { event ->
103
- val ret = JSObject()
104
- val clickResult = JSObject()
105
- clickResult.put("actionId", event.result.actionId)
106
- clickResult.put("url", event.result.url)
107
- ret.put("result", clickResult)
108
- ret.put("notification", JSObject(event.notification.rawPayload))
109
- notifyListeners("notificationClick", ret)
110
- pendingClickEvent = null
111
- }
112
-
113
153
  call.resolve()
114
154
  }
115
155
 
156
+ /** Forward the missed activity-resume to the SDK so isInForeground is
157
+ * correct on cold start. No-op if the SDK already saw the resume. */
158
+ private fun nudgeApplicationServiceForeground() {
159
+ val activity = activity ?: return
160
+ val appSvc = runCatching { OneSignal.getServiceOrNull<IApplicationService>() }.getOrNull() ?: return
161
+ if (appSvc.isInForeground && appSvc.current === activity) return
162
+ val callbacks = appSvc as? Application.ActivityLifecycleCallbacks ?: return
163
+ callbacks.onActivityStarted(activity)
164
+ callbacks.onActivityResumed(activity)
165
+ }
166
+
167
+ private fun buildClickEventJson(event: INotificationClickEvent): JSObject {
168
+ val ret = JSObject()
169
+ val clickResult = JSObject()
170
+ clickResult.put("actionId", event.result.actionId)
171
+ clickResult.put("url", event.result.url)
172
+ ret.put("result", clickResult)
173
+ ret.put("notification", serializeNotification(event.notification))
174
+ return ret
175
+ }
176
+
116
177
  @PluginMethod
117
178
  fun login(call: PluginCall) {
118
179
  val externalId = call.getString("externalId")
@@ -144,8 +205,6 @@ class OneSignalCapacitorPlugin : Plugin(),
144
205
  call.resolve()
145
206
  }
146
207
 
147
- // endregion
148
-
149
208
  // region Debug
150
209
 
151
210
  @PluginMethod
@@ -387,14 +446,15 @@ class OneSignalCapacitorPlugin : Plugin(),
387
446
  @PluginMethod
388
447
  fun permissionNative(call: PluginCall) {
389
448
  val ret = JSObject()
390
- ret.put("permission", if (OneSignal.Notifications.permission) 2 else 1)
449
+ val status = if (OneSignal.Notifications.permission) PERMISSION_AUTHORIZED else PERMISSION_DENIED
450
+ ret.put("permission", status)
391
451
  call.resolve(ret)
392
452
  }
393
453
 
394
454
  @PluginMethod
395
455
  fun requestPermission(call: PluginCall) {
396
456
  val fallback = call.getBoolean("fallbackToSettings") ?: false
397
- CoroutineScope(Dispatchers.Main).launch {
457
+ pluginScope.launch {
398
458
  val accepted = OneSignal.Notifications.requestPermission(fallback)
399
459
  val ret = JSObject()
400
460
  ret.put("permission", accepted)
@@ -411,8 +471,12 @@ class OneSignalCapacitorPlugin : Plugin(),
411
471
 
412
472
  @PluginMethod
413
473
  fun registerForProvisionalAuthorization(call: PluginCall) {
474
+ // Provisional authorization is an iOS-only concept (UNUserNotification
475
+ // .provisional). Android has no equivalent quiet-delivery permission
476
+ // tier, so report `accepted = false` rather than misleading the JS
477
+ // layer into thinking a quiet permission was granted.
414
478
  val ret = JSObject()
415
- ret.put("accepted", true)
479
+ ret.put("accepted", false)
416
480
  call.resolve(ret)
417
481
  }
418
482
 
@@ -467,12 +531,15 @@ class OneSignalCapacitorPlugin : Plugin(),
467
531
  call.reject("notificationId is required")
468
532
  return
469
533
  }
470
- val event = notificationWillDisplayCache[notificationId]
534
+ // JS always dispatches this after the listener loop, even when a
535
+ // listener already called display(). Missing entry = already handled.
536
+ val event = notificationWillDisplayCache.remove(notificationId)
471
537
  if (event == null) {
472
- call.reject("Could not find notification will display event")
538
+ preventDefaultCache.remove(notificationId)
539
+ call.resolve()
473
540
  return
474
541
  }
475
- if (!preventDefaultCache.contains(notificationId)) {
542
+ if (!preventDefaultCache.remove(notificationId)) {
476
543
  event.notification.display()
477
544
  }
478
545
  call.resolve()
@@ -485,11 +552,13 @@ class OneSignalCapacitorPlugin : Plugin(),
485
552
  call.reject("notificationId is required")
486
553
  return
487
554
  }
488
- val event = notificationWillDisplayCache[notificationId]
555
+ val event = notificationWillDisplayCache.remove(notificationId)
489
556
  if (event == null) {
490
- call.reject("Could not find notification will display event")
557
+ preventDefaultCache.remove(notificationId)
558
+ call.resolve()
491
559
  return
492
560
  }
561
+ preventDefaultCache.remove(notificationId)
493
562
  event.notification.display()
494
563
  call.resolve()
495
564
  }
@@ -588,7 +657,7 @@ class OneSignalCapacitorPlugin : Plugin(),
588
657
 
589
658
  @PluginMethod
590
659
  fun requestLocationPermission(call: PluginCall) {
591
- CoroutineScope(Dispatchers.Main).launch {
660
+ pluginScope.launch {
592
661
  OneSignal.Location.requestPermission()
593
662
  call.resolve()
594
663
  }
@@ -610,7 +679,9 @@ class OneSignalCapacitorPlugin : Plugin(),
610
679
 
611
680
  // endregion
612
681
 
613
- // region Live Activities (no-op on Android)
682
+ // region Live Activities
683
+ // iOS-only feature; methods below are no-ops on Android so cross-platform
684
+ // JS code can call them unconditionally. No warnings — silent success.
614
685
 
615
686
  @PluginMethod
616
687
  fun enterLiveActivity(call: PluginCall) {
@@ -647,6 +718,10 @@ class OneSignalCapacitorPlugin : Plugin(),
647
718
  // region Observer Callbacks
648
719
 
649
720
  override fun onWillDisplay(event: INotificationWillDisplayEvent) {
721
+ // No retainUntilConsumed needed: foreground will-display only fires
722
+ // while the app is foregrounded, so the JS layer's listener is
723
+ // already attached. Contrast with onClick() below, which can fire
724
+ // before the WebView finishes booting on a cold-start tap.
650
725
  val notificationId = event.notification.notificationId ?: return
651
726
  notificationWillDisplayCache[notificationId] = event
652
727
  event.preventDefault()
@@ -654,17 +729,11 @@ class OneSignalCapacitorPlugin : Plugin(),
654
729
  }
655
730
 
656
731
  override fun onClick(event: INotificationClickEvent) {
657
- if (bridge != null) {
658
- val ret = JSObject()
659
- val clickResult = JSObject()
660
- clickResult.put("actionId", event.result.actionId)
661
- clickResult.put("url", event.result.url)
662
- ret.put("result", clickResult)
663
- ret.put("notification", serializeNotification(event.notification))
664
- notifyListeners("notificationClick", ret)
665
- } else {
666
- pendingClickEvent = event
667
- }
732
+ // retainUntilConsumed lets Capacitor hold this event until the JS-side
733
+ // click listener attaches. On Android the OneSignal SDK can deliver a
734
+ // cold-start click before the WebView has finished booting and the JS
735
+ // layer has called addEventListener('click', ...).
736
+ notifyListeners("notificationClick", buildClickEventJson(event), true)
668
737
  }
669
738
 
670
739
  private fun serializeNotification(notification: INotification): JSObject {
package/dist/index.d.ts CHANGED
@@ -366,6 +366,7 @@ interface PushSubscriptionChangedState {
366
366
  declare class PushSubscription implements OneSignalPushSubscriptionAPI {
367
367
  private _plugin;
368
368
  private _subscriptionObserverList;
369
+ private _hasRegisteredChangeListener;
369
370
  constructor(plugin: OneSignalCapacitorPlugin);
370
371
  private _processFunctionList;
371
372
  /**
@@ -388,8 +389,9 @@ declare class PushSubscription implements OneSignalPushSubscriptionAPI {
388
389
  getOptedInAsync(): Promise<boolean>;
389
390
  /**
390
391
  * Add a callback that fires when the OneSignal push subscription state changes.
391
- * @param {(event: PushSubscriptionChangedState)=>void} listener
392
- * @returns void
392
+ * The bridge subscription is registered once per namespace instance; subsequent
393
+ * subscribers append to the local list to avoid orphaned bridge handlers
394
+ * across hot-reload cycles.
393
395
  */
394
396
  addEventListener(_event: 'change', listener: (event: PushSubscriptionChangedState) => void): void;
395
397
  /**
@@ -474,6 +476,7 @@ declare class User implements OneSignalUserAPI {
474
476
  pushSubscription: PushSubscription;
475
477
  private _plugin;
476
478
  private _userStateObserverList;
479
+ private _hasRegisteredChangeListener;
477
480
  constructor(plugin: OneSignalCapacitorPlugin);
478
481
  private _processFunctionList;
479
482
  /**
@@ -565,8 +568,9 @@ declare class User implements OneSignalUserAPI {
565
568
  }>;
566
569
  /**
567
570
  * Add a callback that fires when the OneSignal User state changes.
568
- * @param {(event: UserChangedState)=>void} listener
569
- * @returns void
571
+ * The bridge subscription is registered once per namespace instance; subsequent
572
+ * subscribers append to the local list to avoid orphaned bridge handlers
573
+ * across hot-reload cycles.
570
574
  */
571
575
  addEventListener(_event: 'change', listener: (event: UserChangedState) => void): void;
572
576
  /**
@@ -804,13 +808,19 @@ declare class InAppMessages implements OneSignalInAppMessagesAPI {
804
808
  private _didDisplayInAppMessageListeners;
805
809
  private _willDismissInAppMessageListeners;
806
810
  private _didDismissInAppMessageListeners;
811
+ private _hasRegisteredClickListener;
812
+ private _hasRegisteredWillDisplayListener;
813
+ private _hasRegisteredDidDisplayListener;
814
+ private _hasRegisteredWillDismissListener;
815
+ private _hasRegisteredDidDismissListener;
807
816
  constructor(plugin: OneSignalCapacitorPlugin);
808
817
  private _processFunctionList;
809
818
  /**
810
819
  * Add event listeners for In-App Message click and/or lifecycle events.
811
- * @param event
812
- * @param listener
813
- * @returns
820
+ * Each native event channel is bridged once per namespace instance; subsequent
821
+ * subscribers are appended to the local list. Without this guard, hot-reload
822
+ * cycles and effect re-runs leak orphaned bridge subscriptions that fan a
823
+ * single native event into N JS callbacks.
814
824
  */
815
825
  addEventListener<K extends InAppMessageEventName>(event: K, listener: (event: InAppMessageEventTypeMap[K]) => void): void;
816
826
  /**
package/dist/index.js CHANGED
@@ -62,6 +62,11 @@ var InAppMessages = class {
62
62
  this._didDisplayInAppMessageListeners = [];
63
63
  this._willDismissInAppMessageListeners = [];
64
64
  this._didDismissInAppMessageListeners = [];
65
+ this._hasRegisteredClickListener = false;
66
+ this._hasRegisteredWillDisplayListener = false;
67
+ this._hasRegisteredDidDisplayListener = false;
68
+ this._hasRegisteredWillDismissListener = false;
69
+ this._hasRegisteredDidDismissListener = false;
65
70
  this._plugin = plugin;
66
71
  }
67
72
  _processFunctionList(array, param) {
@@ -69,36 +74,52 @@ var InAppMessages = class {
69
74
  }
70
75
  /**
71
76
  * Add event listeners for In-App Message click and/or lifecycle events.
72
- * @param event
73
- * @param listener
74
- * @returns
77
+ * Each native event channel is bridged once per namespace instance; subsequent
78
+ * subscribers are appended to the local list. Without this guard, hot-reload
79
+ * cycles and effect re-runs leak orphaned bridge subscriptions that fan a
80
+ * single native event into N JS callbacks.
75
81
  */
76
82
  addEventListener(event, listener) {
77
83
  if (event === "click") {
78
84
  this._inAppMessageClickListeners.push(listener);
79
- this._plugin.addListener("inAppMessageClick", (json) => {
80
- this._processFunctionList(this._inAppMessageClickListeners, json);
81
- });
85
+ if (!this._hasRegisteredClickListener) {
86
+ this._hasRegisteredClickListener = true;
87
+ this._plugin.addListener("inAppMessageClick", (json) => {
88
+ this._processFunctionList(this._inAppMessageClickListeners, json);
89
+ });
90
+ }
82
91
  } else if (event === "willDisplay") {
83
92
  this._willDisplayInAppMessageListeners.push(listener);
84
- this._plugin.addListener("inAppMessageWillDisplay", (event) => {
85
- this._processFunctionList(this._willDisplayInAppMessageListeners, event);
86
- });
93
+ if (!this._hasRegisteredWillDisplayListener) {
94
+ this._hasRegisteredWillDisplayListener = true;
95
+ this._plugin.addListener("inAppMessageWillDisplay", (event) => {
96
+ this._processFunctionList(this._willDisplayInAppMessageListeners, event);
97
+ });
98
+ }
87
99
  } else if (event === "didDisplay") {
88
100
  this._didDisplayInAppMessageListeners.push(listener);
89
- this._plugin.addListener("inAppMessageDidDisplay", (event) => {
90
- this._processFunctionList(this._didDisplayInAppMessageListeners, event);
91
- });
101
+ if (!this._hasRegisteredDidDisplayListener) {
102
+ this._hasRegisteredDidDisplayListener = true;
103
+ this._plugin.addListener("inAppMessageDidDisplay", (event) => {
104
+ this._processFunctionList(this._didDisplayInAppMessageListeners, event);
105
+ });
106
+ }
92
107
  } else if (event === "willDismiss") {
93
108
  this._willDismissInAppMessageListeners.push(listener);
94
- this._plugin.addListener("inAppMessageWillDismiss", (event) => {
95
- this._processFunctionList(this._willDismissInAppMessageListeners, event);
96
- });
109
+ if (!this._hasRegisteredWillDismissListener) {
110
+ this._hasRegisteredWillDismissListener = true;
111
+ this._plugin.addListener("inAppMessageWillDismiss", (event) => {
112
+ this._processFunctionList(this._willDismissInAppMessageListeners, event);
113
+ });
114
+ }
97
115
  } else if (event === "didDismiss") {
98
116
  this._didDismissInAppMessageListeners.push(listener);
99
- this._plugin.addListener("inAppMessageDidDismiss", (event) => {
100
- this._processFunctionList(this._didDismissInAppMessageListeners, event);
101
- });
117
+ if (!this._hasRegisteredDidDismissListener) {
118
+ this._hasRegisteredDidDismissListener = true;
119
+ this._plugin.addListener("inAppMessageDidDismiss", (event) => {
120
+ this._processFunctionList(this._didDismissInAppMessageListeners, event);
121
+ });
122
+ }
102
123
  }
103
124
  }
104
125
  /**
@@ -544,6 +565,7 @@ var Session = class {
544
565
  var PushSubscription = class {
545
566
  constructor(plugin) {
546
567
  this._subscriptionObserverList = [];
568
+ this._hasRegisteredChangeListener = false;
547
569
  this._plugin = plugin;
548
570
  }
549
571
  _processFunctionList(array, param) {
@@ -575,14 +597,18 @@ var PushSubscription = class {
575
597
  }
576
598
  /**
577
599
  * Add a callback that fires when the OneSignal push subscription state changes.
578
- * @param {(event: PushSubscriptionChangedState)=>void} listener
579
- * @returns void
600
+ * The bridge subscription is registered once per namespace instance; subsequent
601
+ * subscribers append to the local list to avoid orphaned bridge handlers
602
+ * across hot-reload cycles.
580
603
  */
581
604
  addEventListener(_event, listener) {
582
605
  this._subscriptionObserverList.push(listener);
583
- this._plugin.addListener("pushSubscriptionChange", (state) => {
584
- this._processFunctionList(this._subscriptionObserverList, state);
585
- });
606
+ if (!this._hasRegisteredChangeListener) {
607
+ this._hasRegisteredChangeListener = true;
608
+ this._plugin.addListener("pushSubscriptionChange", (state) => {
609
+ this._processFunctionList(this._subscriptionObserverList, state);
610
+ });
611
+ }
586
612
  }
587
613
  /**
588
614
  * Remove a push subscription observer that has been previously added.
@@ -612,6 +638,7 @@ var PushSubscription = class {
612
638
  var User = class {
613
639
  constructor(plugin) {
614
640
  this._userStateObserverList = [];
641
+ this._hasRegisteredChangeListener = false;
615
642
  this._plugin = plugin;
616
643
  this.pushSubscription = new PushSubscription(plugin);
617
644
  }
@@ -737,14 +764,18 @@ var User = class {
737
764
  }
738
765
  /**
739
766
  * Add a callback that fires when the OneSignal User state changes.
740
- * @param {(event: UserChangedState)=>void} listener
741
- * @returns void
767
+ * The bridge subscription is registered once per namespace instance; subsequent
768
+ * subscribers append to the local list to avoid orphaned bridge handlers
769
+ * across hot-reload cycles.
742
770
  */
743
771
  addEventListener(_event, listener) {
744
772
  this._userStateObserverList.push(listener);
745
- this._plugin.addListener("userStateChange", (state) => {
746
- this._processFunctionList(this._userStateObserverList, state);
747
- });
773
+ if (!this._hasRegisteredChangeListener) {
774
+ this._hasRegisteredChangeListener = true;
775
+ this._plugin.addListener("userStateChange", (state) => {
776
+ this._processFunctionList(this._userStateObserverList, state);
777
+ });
778
+ }
748
779
  }
749
780
  /**
750
781
  * Remove a User State observer that has been previously added.
@@ -0,0 +1,89 @@
1
+ #import "OSCapacitorLaunchOptions.h"
2
+ #import <UIKit/UIKit.h>
3
+ #import <UserNotifications/UserNotifications.h>
4
+ #import <objc/runtime.h>
5
+
6
+ @implementation OSCapacitorLaunchOptions
7
+
8
+ static NSDictionary *_capturedLaunchOptions = nil;
9
+ static NSMutableArray<UNNotificationResponse *> *_capturedColdStartResponses = nil;
10
+ // Flips to YES the first time consumeColdStartResponses runs (i.e. after the JS
11
+ // layer has called OneSignal.initialize and drained whatever was queued). The
12
+ // swizzle stays installed for the process lifetime, so without this flag every
13
+ // subsequent warm/background tap would be retained in the array forever and
14
+ // _capturedColdStartResponses would grow monotonically. Once consumed, new
15
+ // taps fall through to the host delegate without being captured.
16
+ static BOOL _coldStartResponsesConsumed = NO;
17
+
18
+ + (void)load {
19
+ _capturedColdStartResponses = [NSMutableArray array];
20
+ [[NSNotificationCenter defaultCenter]
21
+ addObserver:self
22
+ selector:@selector(applicationDidFinishLaunching:)
23
+ name:UIApplicationDidFinishLaunchingNotification
24
+ object:nil];
25
+ }
26
+
27
+ + (void)applicationDidFinishLaunching:(NSNotification *)notification {
28
+ _capturedLaunchOptions = notification.userInfo;
29
+
30
+ [[NSNotificationCenter defaultCenter]
31
+ removeObserver:self
32
+ name:UIApplicationDidFinishLaunchingNotification
33
+ object:nil];
34
+
35
+ // Wrap the UN delegate's didReceiveNotificationResponse so we can hold on
36
+ // to the UNNotificationResponse iOS hands us on cold start. The OneSignal
37
+ // iOS SDK drops cold-start responses inside processNotificationResponse:
38
+ // when no appId is set yet, which is always true on cold start because the
39
+ // JS layer has not called OneSignal.initialize yet. The plugin replays the
40
+ // captured response after initialize() so the SDK can fire its click
41
+ // listeners normally.
42
+ id unDelegate = [UNUserNotificationCenter currentNotificationCenter].delegate;
43
+ if (!unDelegate) return;
44
+
45
+ SEL didReceiveSel = NSSelectorFromString(@"userNotificationCenter:didReceiveNotificationResponse:withCompletionHandler:");
46
+ Class delegateClass = [unDelegate class];
47
+ Method original = class_getInstanceMethod(delegateClass, didReceiveSel);
48
+ if (!original) return;
49
+
50
+ __block IMP originalIMP = method_getImplementation(original);
51
+ IMP newIMP = imp_implementationWithBlock(^(id self_, UNUserNotificationCenter *center, UNNotificationResponse *response, void (^completionHandler)(void)) {
52
+ // Queue every response that arrives before the JS layer drains us.
53
+ // The JS bundle can take multiple seconds to load on cold start (worse
54
+ // in dev builds), and the user can tap a second notification from the
55
+ // shade in that window. Overwriting would silently lose the earlier
56
+ // tap. consumeColdStartResponses clears the queue and flips the
57
+ // _coldStartResponsesConsumed flag once initialize() has handed every
58
+ // response to OSNotificationsManager; from that point on, taps fall
59
+ // straight through to the host delegate so the array can't grow.
60
+ if (!_coldStartResponsesConsumed) {
61
+ [_capturedColdStartResponses addObject:response];
62
+ }
63
+ ((void(*)(id, SEL, UNUserNotificationCenter*, UNNotificationResponse*, void(^)(void)))originalIMP)(self_, didReceiveSel, center, response, completionHandler);
64
+ });
65
+
66
+ // Try to add the method to the delegate's exact class first. If it succeeds,
67
+ // the IMP came from a parent class (inherited) and we've safely shadowed it
68
+ // on this subclass only, leaving sibling subclasses untouched. If it fails,
69
+ // the method is owned by this exact class and method_setImplementation is
70
+ // safe — it won't leak the wrap up the inheritance chain.
71
+ if (!class_addMethod(delegateClass, didReceiveSel, newIMP, method_getTypeEncoding(original))) {
72
+ method_setImplementation(original, newIMP);
73
+ }
74
+ }
75
+
76
+ + (NSDictionary *)launchOptions {
77
+ return _capturedLaunchOptions;
78
+ }
79
+
80
+ + (NSArray<UNNotificationResponse *> *)pendingColdStartResponses {
81
+ return [_capturedColdStartResponses copy];
82
+ }
83
+
84
+ + (void)consumeColdStartResponses {
85
+ _coldStartResponsesConsumed = YES;
86
+ [_capturedColdStartResponses removeAllObjects];
87
+ }
88
+
89
+ @end
@@ -0,0 +1,35 @@
1
+ #import <Foundation/Foundation.h>
2
+ #import <UserNotifications/UserNotifications.h>
3
+
4
+ NS_ASSUME_NONNULL_BEGIN
5
+
6
+ /// Captures cold-start state from iOS that the OneSignal SDK would otherwise
7
+ /// drop because its JS-driven initialize() runs too late.
8
+ ///
9
+ /// This class subscribes to UIApplicationDidFinishLaunchingNotification from
10
+ /// +load (executed by dyld at process start, before main()) so:
11
+ /// * launchOptions are captured before any other code can lose them, and
12
+ /// * the UNUserNotificationCenter delegate's
13
+ /// didReceiveNotificationResponse: is wrapped so we can hold on to the
14
+ /// UNNotificationResponse for a cold-start tap. The OneSignal iOS SDK
15
+ /// drops it inside processNotificationResponse: when no appId is set
16
+ /// yet (OSNotificationsManager.m). We replay it after the JS layer
17
+ /// finishes calling OneSignal.initialize.
18
+ @interface OSCapacitorLaunchOptions : NSObject
19
+
20
+ @property (class, readonly, nullable) NSDictionary *launchOptions;
21
+
22
+ /// UNNotificationResponses delivered by iOS while the JS layer was still
23
+ /// booting (i.e. before OneSignal.initialize ran and we drained the queue).
24
+ /// Empty for warm starts and after consumeColdStartResponses has been called.
25
+ /// Multiple entries are possible: the user can tap a second notification from
26
+ /// the shade while the JS bundle is still loading, especially in dev builds.
27
+ @property (class, readonly, nonnull) NSArray<UNNotificationResponse *> *pendingColdStartResponses;
28
+
29
+ /// Mark the captured cold-start responses as consumed so they are not replayed
30
+ /// twice. Call after handing each response off to the OneSignal iOS SDK.
31
+ + (void)consumeColdStartResponses;
32
+
33
+ @end
34
+
35
+ NS_ASSUME_NONNULL_END
@@ -2,16 +2,12 @@ import Foundation
2
2
  import Capacitor
3
3
  import OneSignalFramework
4
4
  import OneSignalLiveActivities
5
+ #if SWIFT_PACKAGE
6
+ import OSCapacitorLaunchOptions
7
+ #endif
5
8
 
6
9
  @objc(OneSignalCapacitorPlugin)
7
- public class OneSignalCapacitorPlugin: CAPPlugin, CAPBridgedPlugin,
8
- OSNotificationPermissionObserver,
9
- OSNotificationLifecycleListener,
10
- OSNotificationClickListener,
11
- OSPushSubscriptionObserver,
12
- OSInAppMessageLifecycleListener,
13
- OSInAppMessageClickListener,
14
- OSUserStateObserver {
10
+ public class OneSignalCapacitorPlugin: CAPPlugin, CAPBridgedPlugin {
15
11
 
16
12
  public let identifier = "OneSignalCapacitorPlugin"
17
13
  public let jsName = "OneSignalCapacitor"
@@ -72,8 +68,28 @@ public class OneSignalCapacitorPlugin: CAPPlugin, CAPBridgedPlugin,
72
68
  ]
73
69
 
74
70
  private var notificationWillDisplayCache = [String: OSNotificationWillDisplayEvent]()
75
- private var preventDefaultCache = [String: OSNotificationWillDisplayEvent]()
76
- private var pendingClickEvent: OSNotificationClickEvent?
71
+ private var preventDefaultCache = Set<String>()
72
+ private var initialized = false
73
+
74
+ // Observer/listener forwarder. The iOS SDK strong-retains conformers of
75
+ // the lifecycle/click protocols (NSMutableArray-backed), so registering
76
+ // self directly creates a retain cycle that makes deinit unreachable.
77
+ // The proxy holds a weak ref back to us, so dropping the last external
78
+ // ref to the plugin lets deinit run and clean up the proxy registration.
79
+ private var listenerProxy: OneSignalListenerProxy?
80
+
81
+ deinit {
82
+ // Only present after initialize() ran. Removes are idempotent on the
83
+ // SDK side; we still gate to avoid touching an uninitialized SDK.
84
+ guard let listenerProxy = listenerProxy else { return }
85
+ OneSignal.Notifications.removePermissionObserver(listenerProxy)
86
+ OneSignal.Notifications.removeForegroundLifecycleListener(listenerProxy)
87
+ OneSignal.Notifications.removeClickListener(listenerProxy)
88
+ OneSignal.User.pushSubscription.removeObserver(listenerProxy)
89
+ OneSignal.User.removeObserver(listenerProxy)
90
+ OneSignal.InAppMessages.removeLifecycleListener(listenerProxy)
91
+ OneSignal.InAppMessages.removeClickListener(listenerProxy)
92
+ }
77
93
 
78
94
  // MARK: - Core
79
95
 
@@ -82,21 +98,56 @@ public class OneSignalCapacitorPlugin: CAPPlugin, CAPBridgedPlugin,
82
98
  call.reject("appId is required")
83
99
  return
84
100
  }
85
- OneSignalWrapper.sdkType = "capacitor"
86
- OneSignalWrapper.sdkVersion = "010000"
87
- OneSignal.initialize(appId, withLaunchOptions: nil)
88
- OneSignal.Notifications.addPermissionObserver(self)
89
- OneSignal.Notifications.addForegroundLifecycleListener(self)
90
- OneSignal.Notifications.addClickListener(self)
91
- OneSignal.User.pushSubscription.addObserver(self)
92
- OneSignal.User.addObserver(self)
93
- OneSignal.InAppMessages.addLifecycleListener(self)
94
- OneSignal.InAppMessages.addClickListener(self)
95
101
 
96
- if let pending = pendingClickEvent {
97
- sendNotificationClickEvent(pending)
98
- pendingClickEvent = nil
102
+ // initialize() is idempotent: JS may call it multiple times per
103
+ // plugin instance (effect re-runs, hot reload). The iOS SDK's
104
+ // listener arrays don't dedupe, so unguarded re-entry would
105
+ // double-fire foreground/click events.
106
+ if initialized {
107
+ call.resolve()
108
+ return
99
109
  }
110
+ initialized = true
111
+
112
+ OneSignalWrapper.sdkType = "capacitor"
113
+ OneSignalWrapper.sdkVersion = "010002"
114
+ // OSCapacitorLaunchOptions's +load captures the dictionary from
115
+ // UIApplicationDidFinishLaunchingNotification at process start (before
116
+ // main()), so cold-start notification taps that arrive via launchOptions
117
+ // are still available to the OneSignal iOS SDK when the JS layer
118
+ // initializes us.
119
+ OneSignal.initialize(appId, withLaunchOptions: OSCapacitorLaunchOptions.launchOptions)
120
+
121
+ // The OneSignal iOS SDK drops cold-start UNNotificationResponse objects
122
+ // inside processNotificationResponse: when no appId is set yet, which
123
+ // is always true on cold start because iOS delivers the response before
124
+ // OneSignal.initialize() runs from JS. OSCapacitorLaunchOptions's
125
+ // delegate wrap queues every response that arrives in that window so we
126
+ // can replay them here, in arrival order, after initialize has set the
127
+ // appId. The queue can hold more than one entry if the user tapped a
128
+ // second notification from the shade while the JS bundle was loading.
129
+ for response in OSCapacitorLaunchOptions.pendingColdStartResponses {
130
+ OSNotificationsManager.processNotificationResponse(response)
131
+ }
132
+ // Always consume, even if the queue was empty. consumeColdStartResponses
133
+ // also flips a one-way flag that tells the swizzle to stop capturing
134
+ // future taps; without this unconditional call, sessions that cold-start
135
+ // without a notification tap would never flip the flag and any later
136
+ // warm/background taps would accumulate in the static array forever.
137
+ OSCapacitorLaunchOptions.consumeColdStartResponses()
138
+
139
+ let proxy = OneSignalListenerProxy()
140
+ proxy.owner = self
141
+ listenerProxy = proxy
142
+
143
+ OneSignal.Notifications.addPermissionObserver(proxy)
144
+ OneSignal.Notifications.addForegroundLifecycleListener(proxy)
145
+ OneSignal.Notifications.addClickListener(proxy)
146
+ OneSignal.User.pushSubscription.addObserver(proxy)
147
+ OneSignal.User.addObserver(proxy)
148
+ OneSignal.InAppMessages.addLifecycleListener(proxy)
149
+ OneSignal.InAppMessages.addClickListener(proxy)
150
+
100
151
  call.resolve()
101
152
  }
102
153
 
@@ -320,7 +371,7 @@ public class OneSignalCapacitorPlugin: CAPPlugin, CAPBridgedPlugin,
320
371
  return
321
372
  }
322
373
  event.preventDefault()
323
- preventDefaultCache[notificationId] = event
374
+ preventDefaultCache.insert(notificationId)
324
375
  call.resolve()
325
376
  }
326
377
 
@@ -329,11 +380,15 @@ public class OneSignalCapacitorPlugin: CAPPlugin, CAPBridgedPlugin,
329
380
  call.reject("notificationId is required")
330
381
  return
331
382
  }
332
- guard let event = notificationWillDisplayCache[notificationId] else {
333
- call.reject("Could not find notification will display event")
383
+ // JS always dispatches this after the listener loop, even when a
384
+ // listener already called display(). Missing entry = already handled.
385
+ guard let event = notificationWillDisplayCache.removeValue(forKey: notificationId) else {
386
+ preventDefaultCache.remove(notificationId)
387
+ call.resolve()
334
388
  return
335
389
  }
336
- if preventDefaultCache[notificationId] == nil {
390
+ let wasPrevented = preventDefaultCache.remove(notificationId) != nil
391
+ if !wasPrevented {
337
392
  event.notification.display()
338
393
  }
339
394
  call.resolve()
@@ -344,10 +399,12 @@ public class OneSignalCapacitorPlugin: CAPPlugin, CAPBridgedPlugin,
344
399
  call.reject("notificationId is required")
345
400
  return
346
401
  }
347
- guard let event = notificationWillDisplayCache[notificationId] else {
348
- call.reject("Could not find notification will display event")
402
+ guard let event = notificationWillDisplayCache.removeValue(forKey: notificationId) else {
403
+ preventDefaultCache.remove(notificationId)
404
+ call.resolve()
349
405
  return
350
406
  }
407
+ preventDefaultCache.remove(notificationId)
351
408
  event.notification.display()
352
409
  call.resolve()
353
410
  }
@@ -569,18 +626,16 @@ public class OneSignalCapacitorPlugin: CAPPlugin, CAPBridgedPlugin,
569
626
  }
570
627
 
571
628
  public func onClick(event: OSNotificationClickEvent) {
572
- if bridge != nil {
573
- sendNotificationClickEvent(event)
574
- } else {
575
- pendingClickEvent = event
576
- }
577
- }
578
-
579
- private func sendNotificationClickEvent(_ event: OSNotificationClickEvent) {
580
- if let data = event.stringify().data(using: .utf8),
581
- let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any] {
582
- notifyListeners("notificationClick", data: json)
629
+ // retainUntilConsumed lets Capacitor hold this event until a JS click
630
+ // listener attaches. On cold start the plugin's initialize() replays
631
+ // the OneSignal click before JS has had a chance to call
632
+ // addEventListener('click', ...), so without this a cold-start tap
633
+ // would fire before any JS listener exists and be lost.
634
+ guard let data = event.stringify().data(using: .utf8),
635
+ let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else {
636
+ return
583
637
  }
638
+ notifyListeners("notificationClick", data: json, retainUntilConsumed: true)
584
639
  }
585
640
 
586
641
  @objc(onWillDisplayInAppMessage:)
@@ -637,3 +692,66 @@ public class OneSignalCapacitorPlugin: CAPPlugin, CAPBridgedPlugin,
637
692
  ])
638
693
  }
639
694
  }
695
+
696
+ // Forwarding proxy for the OneSignal iOS SDK observer/listener APIs.
697
+ // Registered with the SDK in place of the plugin so the SDK's strong
698
+ // retention of click/lifecycle listeners (NSMutableArray-backed) does not
699
+ // pin the plugin and make its deinit unreachable. The proxy holds a weak
700
+ // ref back to the plugin; once external holders drop the plugin its
701
+ // deinit runs, removes the proxy from the SDK, and the proxy is then
702
+ // released too.
703
+ final class OneSignalListenerProxy: NSObject,
704
+ OSNotificationPermissionObserver,
705
+ OSNotificationLifecycleListener,
706
+ OSNotificationClickListener,
707
+ OSPushSubscriptionObserver,
708
+ OSInAppMessageLifecycleListener,
709
+ OSInAppMessageClickListener,
710
+ OSUserStateObserver {
711
+
712
+ weak var owner: OneSignalCapacitorPlugin?
713
+
714
+ public func onNotificationPermissionDidChange(_ permission: Bool) {
715
+ owner?.onNotificationPermissionDidChange(permission)
716
+ }
717
+
718
+ public func onPushSubscriptionDidChange(state: OSPushSubscriptionChangedState) {
719
+ owner?.onPushSubscriptionDidChange(state: state)
720
+ }
721
+
722
+ public func onUserStateDidChange(state: OSUserChangedState) {
723
+ owner?.onUserStateDidChange(state: state)
724
+ }
725
+
726
+ public func onWillDisplay(event: OSNotificationWillDisplayEvent) {
727
+ owner?.onWillDisplay(event: event)
728
+ }
729
+
730
+ public func onClick(event: OSNotificationClickEvent) {
731
+ owner?.onClick(event: event)
732
+ }
733
+
734
+ @objc(onWillDisplayInAppMessage:)
735
+ public func onWillDisplay(event: OSInAppMessageWillDisplayEvent) {
736
+ owner?.onWillDisplay(event: event)
737
+ }
738
+
739
+ @objc(onDidDisplayInAppMessage:)
740
+ public func onDidDisplay(event: OSInAppMessageDidDisplayEvent) {
741
+ owner?.onDidDisplay(event: event)
742
+ }
743
+
744
+ @objc(onWillDismissInAppMessage:)
745
+ public func onWillDismiss(event: OSInAppMessageWillDismissEvent) {
746
+ owner?.onWillDismiss(event: event)
747
+ }
748
+
749
+ @objc(onDidDismissInAppMessage:)
750
+ public func onDidDismiss(event: OSInAppMessageDidDismissEvent) {
751
+ owner?.onDidDismiss(event: event)
752
+ }
753
+
754
+ public func onClick(event: OSInAppMessageClickEvent) {
755
+ owner?.onClick(event: event)
756
+ }
757
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onesignal/capacitor-plugin",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "description": "OneSignal is a high volume Push Notification service for mobile apps. This is the pure Capacitor plugin for OneSignal, providing push notifications, in-app messaging, and more.",
5
5
  "keywords": [
6
6
  "apns",
@@ -53,10 +53,10 @@
53
53
  "test": "vp test",
54
54
  "test:coverage": "vp test run --coverage",
55
55
  "test:run": "vp test run",
56
- "verify": "vp build && vp test run && vp lint"
56
+ "verify": "vp pack && vp test run && vp lint"
57
57
  },
58
58
  "devDependencies": {
59
- "@capacitor/core": "^7.0.0",
59
+ "@capacitor/core": "7.0.0",
60
60
  "@capacitor/docgen": "^0.2.2",
61
61
  "@types/bun": "latest",
62
62
  "@vitest/coverage-v8": "^4.1.2",
@@ -66,7 +66,7 @@
66
66
  "vite-plus": "0.1.20"
67
67
  },
68
68
  "peerDependencies": {
69
- "@capacitor/core": "^7.0.0"
69
+ "@capacitor/core": ">=7.0.0"
70
70
  },
71
71
  "overrides": {
72
72
  "vite": "npm:@voidzero-dev/vite-plus-core@latest",