@onesignal/capacitor-plugin 1.0.1 → 1.0.3

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,7 +10,11 @@ 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'
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
  )
@@ -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.8.1"
17
+ onesignal = "5.9.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 = "010003"
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 = "010003"
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.1",
3
+ "version": "1.0.3",
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",
@@ -56,21 +56,20 @@
56
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",
63
63
  "happy-dom": "^20.9.0",
64
64
  "typescript": "^5.9.3",
65
- "vite": "npm:@voidzero-dev/vite-plus-core@latest",
66
65
  "vite-plus": "0.1.20"
67
66
  },
68
67
  "peerDependencies": {
69
- "@capacitor/core": "^7.0.0"
68
+ "@capacitor/core": ">=7.0.0"
70
69
  },
71
70
  "overrides": {
72
- "vite": "npm:@voidzero-dev/vite-plus-core@latest",
73
- "vitest": "npm:@voidzero-dev/vite-plus-test@latest"
71
+ "vite": "npm:@voidzero-dev/vite-plus-core@0.1.20",
72
+ "vitest": "npm:@voidzero-dev/vite-plus-test@0.1.20"
74
73
  },
75
74
  "packageManager": "bun@1.3.13",
76
75
  "capacitor": {