@virex-tech/paywallo-sdk 2.0.0 → 2.0.1

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.
@@ -3,7 +3,7 @@
3
3
 
4
4
  @interface RCT_EXTERN_MODULE(PaywalloNotifications, RCTEventEmitter)
5
5
 
6
- RCT_EXTERN_METHOD(requestPermission:(NSDictionary *)options
6
+ RCT_EXTERN_METHOD(requestPermission:(nonnull NSDictionary *)options
7
7
  resolve:(RCTPromiseResolveBlock)resolve
8
8
  reject:(RCTPromiseRejectBlock)reject)
9
9
 
@@ -13,6 +13,9 @@ RCT_EXTERN_METHOD(hasPermission:(RCTPromiseResolveBlock)resolve
13
13
  RCT_EXTERN_METHOD(getToken:(RCTPromiseResolveBlock)resolve
14
14
  reject:(RCTPromiseRejectBlock)reject)
15
15
 
16
+ RCT_EXTERN_METHOD(registerForPush:(RCTPromiseResolveBlock)resolve
17
+ reject:(RCTPromiseRejectBlock)reject)
18
+
16
19
  RCT_EXTERN_METHOD(getInitialNotification:(RCTPromiseResolveBlock)resolve
17
20
  reject:(RCTPromiseRejectBlock)reject)
18
21
 
@@ -10,9 +10,7 @@ class PaywalloNotifications: RCTEventEmitter {
10
10
  private static let messageReceivedEvent = "PaywalloMessageReceived"
11
11
  private static let notificationOpenedEvent = "PaywalloNotificationOpened"
12
12
 
13
- // MARK: - Pre-subscribe buffer (same pattern as PaywalloStoreKit)
14
- // hasListeners is read/written from bufferQueue to avoid races between
15
- // startObserving (main thread) and bufferOrEmit (NotificationCenter delivery thread).
13
+ // MARK: - Pre-subscribe buffer
16
14
  private var _hasListeners = false
17
15
  private var preSubscribeBuffer: [[String: Any]] = []
18
16
  private let bufferQueue = DispatchQueue(label: "com.paywallo.notifications.buffer")
@@ -24,7 +22,7 @@ class PaywalloNotifications: RCTEventEmitter {
24
22
 
25
23
  // MARK: - RCTEventEmitter Overrides
26
24
 
27
- override static func requiresMainQueueSetup() -> Bool { return false }
25
+ override static func requiresMainQueueSetup() -> Bool { return true }
28
26
 
29
27
  override func supportedEvents() -> [String] {
30
28
  return [
@@ -46,13 +44,8 @@ class PaywalloNotifications: RCTEventEmitter {
46
44
  // MARK: - Buffer Management
47
45
 
48
46
  private func bufferOrEmit(eventName: String, body: [String: Any]) {
49
- // Determine whether to emit or buffer inside the lock, but never call
50
- // sendEvent (which may dispatch back to main) while holding the lock —
51
- // that would risk deadlock or re-entrancy on a sync queue.
52
47
  let shouldEmit: Bool = bufferQueue.sync {
53
- if _hasListeners {
54
- return true
55
- }
48
+ if _hasListeners { return true }
56
49
  var entry = body
57
50
  entry["_eventName"] = eventName
58
51
  if preSubscribeBuffer.count >= PaywalloNotifications.maxBufferSize {
@@ -62,12 +55,13 @@ class PaywalloNotifications: RCTEventEmitter {
62
55
  return false
63
56
  }
64
57
  if shouldEmit {
65
- sendEvent(withName: eventName, body: body)
58
+ DispatchQueue.main.async {
59
+ self.sendEvent(withName: eventName, body: body)
60
+ }
66
61
  }
67
62
  }
68
63
 
69
64
  private func drainPreSubscribeBuffer() {
70
- // Copy and clear while holding the lock; emit outside the lock.
71
65
  let drained: [[String: Any]] = bufferQueue.sync {
72
66
  let copy = preSubscribeBuffer
73
67
  preSubscribeBuffer.removeAll()
@@ -76,13 +70,16 @@ class PaywalloNotifications: RCTEventEmitter {
76
70
  for entry in drained {
77
71
  var body = entry
78
72
  guard let eventName = body.removeValue(forKey: "_eventName") as? String else { continue }
79
- sendEvent(withName: eventName, body: body)
73
+ DispatchQueue.main.async {
74
+ self.sendEvent(withName: eventName, body: body)
75
+ }
80
76
  }
81
77
  }
82
78
 
83
79
  // MARK: - Public Methods (exposed to JS)
84
80
 
85
- @objc func requestPermission(
81
+ @objc(requestPermission:resolve:reject:)
82
+ func requestPermission(
86
83
  _ options: NSDictionary,
87
84
  resolve: @escaping RCTPromiseResolveBlock,
88
85
  reject: @escaping RCTPromiseRejectBlock
@@ -95,52 +92,70 @@ class PaywalloNotifications: RCTEventEmitter {
95
92
  authOptions.insert(.provisional)
96
93
  }
97
94
 
98
- center.requestAuthorization(options: authOptions) { granted, error in
99
- if let error = error {
100
- reject("PERMISSION_ERROR", error.localizedDescription, error)
95
+ center.requestAuthorization(options: authOptions) { [weak self] _, error in
96
+ guard let self = self else {
97
+ DispatchQueue.main.async { reject("MODULE_RELEASED", "Module was released", nil) }
101
98
  return
102
99
  }
103
100
 
104
- DispatchQueue.main.async {
105
- UIApplication.shared.registerForRemoteNotifications()
101
+ if let error = error {
102
+ DispatchQueue.main.async { reject("PERMISSION_ERROR", error.localizedDescription, error) }
103
+ return
106
104
  }
107
105
 
108
- // Return status string matching SDK's PushPermissionStatus type
109
106
  self.getPermissionStatusString { status in
110
- resolve(status)
107
+ DispatchQueue.main.async { resolve(status) }
111
108
  }
112
109
  }
113
110
  }
114
111
 
115
- @objc func hasPermission(
112
+ @objc(hasPermission:reject:)
113
+ func hasPermission(
116
114
  _ resolve: @escaping RCTPromiseResolveBlock,
117
115
  reject: @escaping RCTPromiseRejectBlock
118
116
  ) {
119
117
  getPermissionStatusString { status in
120
- resolve(status)
118
+ DispatchQueue.main.async { resolve(status) }
121
119
  }
122
120
  }
123
121
 
124
- @objc func getToken(
122
+ @objc(getToken:reject:)
123
+ func getToken(
125
124
  _ resolve: @escaping RCTPromiseResolveBlock,
126
125
  reject: @escaping RCTPromiseRejectBlock
127
126
  ) {
128
- if let token = PaywalloNotifications.latestAPNSToken {
129
- resolve(token)
130
- } else {
131
- resolve(nil)
127
+ DispatchQueue.main.async {
128
+ if let token = PaywalloNotifications.latestAPNSToken {
129
+ resolve(token)
130
+ } else {
131
+ resolve(nil)
132
+ }
132
133
  }
133
134
  }
134
135
 
135
- @objc func getInitialNotification(
136
+ @objc(registerForPush:reject:)
137
+ func registerForPush(
136
138
  _ resolve: @escaping RCTPromiseResolveBlock,
137
139
  reject: @escaping RCTPromiseRejectBlock
138
140
  ) {
139
- if let notification = PaywalloNotifications.initialNotification {
140
- PaywalloNotifications.initialNotification = nil // consume once
141
- resolve(notification)
142
- } else {
143
- resolve(nil)
141
+ DispatchQueue.main.async {
142
+ UIApplication.shared.registerForRemoteNotifications()
143
+ resolve(true)
144
+ }
145
+ }
146
+
147
+ @objc(getInitialNotification:reject:)
148
+ func getInitialNotification(
149
+ _ resolve: @escaping RCTPromiseResolveBlock,
150
+ reject: @escaping RCTPromiseRejectBlock
151
+ ) {
152
+ DispatchQueue.main.async {
153
+ if let notification = PaywalloNotifications.initialNotification {
154
+ PaywalloNotifications.initialNotification = nil
155
+ resolve(notification)
156
+ } else {
157
+ resolve(nil)
158
+ }
144
159
  }
145
160
  }
146
161
 
@@ -157,44 +172,10 @@ class PaywalloNotifications: RCTEventEmitter {
157
172
  case .ephemeral: status = "ephemeral"
158
173
  @unknown default: status = "notDetermined"
159
174
  }
160
- completion(status)
175
+ DispatchQueue.main.async { completion(status) }
161
176
  }
162
177
  }
163
178
 
164
- // MARK: - Static methods called from swizzled AppDelegate
165
-
166
- @objc static func didRegisterForRemoteNotifications(deviceToken: Data) {
167
- let token = deviceToken.map { String(format: "%02x", $0) }.joined()
168
- latestAPNSToken = token
169
- // Emit to JS — need to find the module instance through the bridge
170
- // This is handled via NotificationCenter since we can't access the bridge statically
171
- NotificationCenter.default.post(
172
- name: NSNotification.Name("PaywalloAPNSTokenReceived"),
173
- object: nil,
174
- userInfo: ["token": token]
175
- )
176
- }
177
-
178
- @objc static func didReceiveRemoteNotification(userInfo: [AnyHashable: Any]) {
179
- let payload = extractPayload(from: userInfo)
180
- NotificationCenter.default.post(
181
- name: NSNotification.Name("PaywalloRemoteNotificationReceived"),
182
- object: nil,
183
- userInfo: ["payload": payload]
184
- )
185
- }
186
-
187
- @objc static func didOpenNotification(userInfo: [AnyHashable: Any]) {
188
- let payload = extractPayload(from: userInfo)
189
- // If module not ready yet, store as initial notification
190
- initialNotification = payload
191
- NotificationCenter.default.post(
192
- name: NSNotification.Name("PaywalloNotificationOpened"),
193
- object: nil,
194
- userInfo: ["payload": payload]
195
- )
196
- }
197
-
198
179
  // MARK: - Payload Extraction
199
180
 
200
181
  private static func extractPayload(from userInfo: [AnyHashable: Any]) -> [String: Any] {
@@ -209,7 +190,6 @@ class PaywalloNotifications: RCTEventEmitter {
209
190
  }
210
191
  }
211
192
 
212
- // Extract custom data (everything except "aps")
213
193
  var data: [String: String] = [:]
214
194
  for (key, value) in userInfo {
215
195
  guard let key = key as? String, key != "aps" else { continue }
@@ -222,11 +202,10 @@ class PaywalloNotifications: RCTEventEmitter {
222
202
  return payload
223
203
  }
224
204
 
225
- // MARK: - NotificationCenter observers (bridge between static calls and instance)
205
+ // MARK: - Init: observe token from AppDelegate
226
206
 
227
207
  override init() {
228
208
  super.init()
229
- PaywalloNotificationsSwizzle.swizzleIfNeeded()
230
209
 
231
210
  NotificationCenter.default.addObserver(
232
211
  self, selector: #selector(onAPNSTokenReceived(_:)),
@@ -240,6 +219,11 @@ class PaywalloNotifications: RCTEventEmitter {
240
219
  self, selector: #selector(onNotificationOpened(_:)),
241
220
  name: NSNotification.Name("PaywalloNotificationOpened"), object: nil
242
221
  )
222
+
223
+ // Check if a token was already captured before this observer registered (race condition fix)
224
+ if let existing = PaywalloNotifications.latestAPNSToken {
225
+ bufferOrEmit(eventName: PaywalloNotifications.tokenRefreshEvent, body: ["token": existing])
226
+ }
243
227
  }
244
228
 
245
229
  deinit {
@@ -248,7 +232,10 @@ class PaywalloNotifications: RCTEventEmitter {
248
232
 
249
233
  @objc private func onAPNSTokenReceived(_ notification: Notification) {
250
234
  guard let token = notification.userInfo?["token"] as? String else { return }
251
- bufferOrEmit(eventName: PaywalloNotifications.tokenRefreshEvent, body: ["token": token])
235
+ DispatchQueue.main.async {
236
+ PaywalloNotifications.latestAPNSToken = token
237
+ self.bufferOrEmit(eventName: PaywalloNotifications.tokenRefreshEvent, body: ["token": token])
238
+ }
252
239
  }
253
240
 
254
241
  @objc private func onRemoteNotificationReceived(_ notification: Notification) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@virex-tech/paywallo-sdk",
3
- "version": "2.0.0",
3
+ "version": "2.0.1",
4
4
  "description": "SDK React Native para integração com Paywallo - Paywalls, Subscriptions e Analytics",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",
@@ -1,126 +0,0 @@
1
- import Foundation
2
- import UIKit
3
-
4
- /// Swizzles UIApplicationDelegate methods to intercept APNS callbacks.
5
- /// Runs automatically at load time via swizzleIfNeeded() called from PaywalloNotifications.init().
6
- /// Same approach used by OneSignal, Firebase, and other push SDKs.
7
- @objc class PaywalloNotificationsSwizzle: NSObject {
8
-
9
- private static var hasSwizzled = false
10
-
11
- /// Call once at module init time to swizzle AppDelegate APNS methods.
12
- @objc static func swizzleIfNeeded() {
13
- guard !hasSwizzled else { return }
14
-
15
- // Defer to main queue — UIApplication.shared.delegate is set after app launch.
16
- // Set hasSwizzled only after the delegate is confirmed, so a nil-delegate
17
- // early call doesn't permanently suppress swizzling.
18
- DispatchQueue.main.async {
19
- guard let delegate = UIApplication.shared.delegate else { return }
20
- guard !hasSwizzled else { return }
21
- hasSwizzled = true
22
- let delegateClass: AnyClass = type(of: delegate)
23
-
24
- swizzleDidRegisterForRemoteNotifications(delegateClass)
25
- swizzleDidFailToRegisterForRemoteNotifications(delegateClass)
26
- swizzleDidReceiveRemoteNotification(delegateClass)
27
- }
28
- }
29
-
30
- // MARK: - didRegisterForRemoteNotificationsWithDeviceToken
31
-
32
- private static func swizzleDidRegisterForRemoteNotifications(_ cls: AnyClass) {
33
- let original = #selector(UIApplicationDelegate.application(_:didRegisterForRemoteNotificationsWithDeviceToken:))
34
- let swizzled = #selector(PaywalloNotificationsSwizzle.pw_didRegisterForRemoteNotifications(_:deviceToken:))
35
- swizzle(cls: cls, original: original, swizzled: swizzled)
36
- }
37
-
38
- // MARK: - didFailToRegisterForRemoteNotificationsWithError
39
-
40
- private static func swizzleDidFailToRegisterForRemoteNotifications(_ cls: AnyClass) {
41
- let original = #selector(UIApplicationDelegate.application(_:didFailToRegisterForRemoteNotificationsWithError:))
42
- let swizzled = #selector(PaywalloNotificationsSwizzle.pw_didFailToRegisterForRemoteNotifications(_:error:))
43
- swizzle(cls: cls, original: original, swizzled: swizzled)
44
- }
45
-
46
- // MARK: - didReceiveRemoteNotification (with completion handler)
47
-
48
- private static func swizzleDidReceiveRemoteNotification(_ cls: AnyClass) {
49
- let original = #selector(UIApplicationDelegate.application(_:didReceiveRemoteNotification:fetchCompletionHandler:))
50
- let swizzled = #selector(PaywalloNotificationsSwizzle.pw_didReceiveRemoteNotification(_:userInfo:completionHandler:))
51
- swizzle(cls: cls, original: original, swizzled: swizzled)
52
- }
53
-
54
- // MARK: - Core swizzle helper
55
-
56
- /// Track whether the original method existed before swizzle for each selector,
57
- /// so the pw_ implementations know whether to forward (exchange case) or not (add case).
58
- private static var didExchange: [String: Bool] = [:]
59
-
60
- /// Exchanges implementations if the method exists on cls, otherwise adds it directly.
61
- private static func swizzle(cls: AnyClass, original: Selector, swizzled: Selector) {
62
- guard let swizzledMethod = class_getInstanceMethod(PaywalloNotificationsSwizzle.self, swizzled) else {
63
- return
64
- }
65
-
66
- let key = NSStringFromSelector(original)
67
-
68
- if let originalMethod = class_getInstanceMethod(cls, original) {
69
- // Method exists on the AppDelegate — exchange so both run.
70
- // After exchange: calling `original` runs our pw_ body,
71
- // calling `pw_` from within pw_ runs the ORIGINAL body (not recursion).
72
- method_exchangeImplementations(originalMethod, swizzledMethod)
73
- didExchange[key] = true
74
- } else {
75
- // Method not implemented in the host AppDelegate — add our impl directly.
76
- // pw_ methods must NOT call themselves here (no original to forward to).
77
- let types = method_getTypeEncoding(swizzledMethod)
78
- class_addMethod(cls, original, method_getImplementation(swizzledMethod), types)
79
- didExchange[key] = false
80
- }
81
- }
82
-
83
- // MARK: - Swizzled implementations
84
- // After method_exchangeImplementations:
85
- // - The original selector on AppDelegate points to our pw_ body
86
- // - Calling pw_ from within itself calls the ORIGINAL AppDelegate implementation
87
- // (because the IMP under pw_ was exchanged to the original). Not infinite recursion.
88
- //
89
- // When class_addMethod was used instead (no original method):
90
- // - There is no original to forward to, so we skip the self-call to avoid
91
- // actual infinite recursion / stack overflow.
92
-
93
- // All pw_ methods are called on the main thread (UIApplicationDelegate callbacks
94
- // are main-thread-only). didExchange is written on main by swizzleIfNeeded, so
95
- // reading it here is safe without additional locking.
96
-
97
- @objc func pw_didRegisterForRemoteNotifications(_ application: UIApplication, deviceToken: Data) {
98
- PaywalloNotifications.didRegisterForRemoteNotifications(deviceToken: deviceToken)
99
- let key = NSStringFromSelector(#selector(UIApplicationDelegate.application(_:didRegisterForRemoteNotificationsWithDeviceToken:)))
100
- if PaywalloNotificationsSwizzle.didExchange[key] == true {
101
- pw_didRegisterForRemoteNotifications(application, deviceToken: deviceToken)
102
- }
103
- }
104
-
105
- @objc func pw_didFailToRegisterForRemoteNotifications(_ application: UIApplication, error: Error) {
106
- let key = NSStringFromSelector(#selector(UIApplicationDelegate.application(_:didFailToRegisterForRemoteNotificationsWithError:)))
107
- if PaywalloNotificationsSwizzle.didExchange[key] == true {
108
- pw_didFailToRegisterForRemoteNotifications(application, error: error)
109
- }
110
- }
111
-
112
- @objc func pw_didReceiveRemoteNotification(
113
- _ application: UIApplication,
114
- userInfo: [AnyHashable: Any],
115
- completionHandler: @escaping (UIBackgroundFetchResult) -> Void
116
- ) {
117
- PaywalloNotifications.didReceiveRemoteNotification(userInfo: userInfo)
118
- let key = NSStringFromSelector(#selector(UIApplicationDelegate.application(_:didReceiveRemoteNotification:fetchCompletionHandler:)))
119
- if PaywalloNotificationsSwizzle.didExchange[key] == true {
120
- pw_didReceiveRemoteNotification(application, userInfo: userInfo, completionHandler: completionHandler)
121
- } else {
122
- // No original handler — we must call the completion handler ourselves
123
- completionHandler(.noData)
124
- }
125
- }
126
- }