@virex-tech/paywallo-sdk 1.5.4 → 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.
- package/PaywalloSdk.podspec +1 -1
- package/android/build.gradle +2 -1
- package/android/src/main/AndroidManifest.xml +21 -0
- package/android/src/main/java/com/paywallo/sdk/PaywalloFirebaseMessagingService.kt +67 -0
- package/android/src/main/java/com/paywallo/sdk/PaywalloNotificationsModule.kt +169 -0
- package/android/src/main/java/com/paywallo/sdk/PaywalloSdkPackage.kt +2 -1
- package/android/src/main/java/com/paywallo/sdk/PaywalloStoreKitModule.kt +103 -0
- package/dist/index.d.mts +560 -39
- package/dist/index.d.ts +560 -39
- package/dist/index.js +6843 -3709
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +6832 -3700
- package/dist/index.mjs.map +1 -1
- package/ios/PaywalloNotifications.m +22 -0
- package/ios/PaywalloNotifications.swift +250 -0
- package/ios/PaywalloStoreKit.m +4 -1
- package/ios/PaywalloStoreKit.swift +152 -8
- package/ios/PaywalloWebView.m +2 -3
- package/package.json +11 -4
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
#import <React/RCTBridgeModule.h>
|
|
2
|
+
#import <React/RCTEventEmitter.h>
|
|
3
|
+
|
|
4
|
+
@interface RCT_EXTERN_MODULE(PaywalloNotifications, RCTEventEmitter)
|
|
5
|
+
|
|
6
|
+
RCT_EXTERN_METHOD(requestPermission:(nonnull NSDictionary *)options
|
|
7
|
+
resolve:(RCTPromiseResolveBlock)resolve
|
|
8
|
+
reject:(RCTPromiseRejectBlock)reject)
|
|
9
|
+
|
|
10
|
+
RCT_EXTERN_METHOD(hasPermission:(RCTPromiseResolveBlock)resolve
|
|
11
|
+
reject:(RCTPromiseRejectBlock)reject)
|
|
12
|
+
|
|
13
|
+
RCT_EXTERN_METHOD(getToken:(RCTPromiseResolveBlock)resolve
|
|
14
|
+
reject:(RCTPromiseRejectBlock)reject)
|
|
15
|
+
|
|
16
|
+
RCT_EXTERN_METHOD(registerForPush:(RCTPromiseResolveBlock)resolve
|
|
17
|
+
reject:(RCTPromiseRejectBlock)reject)
|
|
18
|
+
|
|
19
|
+
RCT_EXTERN_METHOD(getInitialNotification:(RCTPromiseResolveBlock)resolve
|
|
20
|
+
reject:(RCTPromiseRejectBlock)reject)
|
|
21
|
+
|
|
22
|
+
@end
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import React
|
|
3
|
+
import UserNotifications
|
|
4
|
+
|
|
5
|
+
@objc(PaywalloNotifications)
|
|
6
|
+
class PaywalloNotifications: RCTEventEmitter {
|
|
7
|
+
|
|
8
|
+
// MARK: - Event Names
|
|
9
|
+
private static let tokenRefreshEvent = "PaywalloTokenRefresh"
|
|
10
|
+
private static let messageReceivedEvent = "PaywalloMessageReceived"
|
|
11
|
+
private static let notificationOpenedEvent = "PaywalloNotificationOpened"
|
|
12
|
+
|
|
13
|
+
// MARK: - Pre-subscribe buffer
|
|
14
|
+
private var _hasListeners = false
|
|
15
|
+
private var preSubscribeBuffer: [[String: Any]] = []
|
|
16
|
+
private let bufferQueue = DispatchQueue(label: "com.paywallo.notifications.buffer")
|
|
17
|
+
private static let maxBufferSize = 100
|
|
18
|
+
|
|
19
|
+
// MARK: - State
|
|
20
|
+
private static var latestAPNSToken: String?
|
|
21
|
+
private static var initialNotification: [String: Any]?
|
|
22
|
+
|
|
23
|
+
// MARK: - RCTEventEmitter Overrides
|
|
24
|
+
|
|
25
|
+
override static func requiresMainQueueSetup() -> Bool { return true }
|
|
26
|
+
|
|
27
|
+
override func supportedEvents() -> [String] {
|
|
28
|
+
return [
|
|
29
|
+
PaywalloNotifications.tokenRefreshEvent,
|
|
30
|
+
PaywalloNotifications.messageReceivedEvent,
|
|
31
|
+
PaywalloNotifications.notificationOpenedEvent,
|
|
32
|
+
]
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
override func startObserving() {
|
|
36
|
+
bufferQueue.sync { _hasListeners = true }
|
|
37
|
+
drainPreSubscribeBuffer()
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
override func stopObserving() {
|
|
41
|
+
bufferQueue.sync { _hasListeners = false }
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// MARK: - Buffer Management
|
|
45
|
+
|
|
46
|
+
private func bufferOrEmit(eventName: String, body: [String: Any]) {
|
|
47
|
+
let shouldEmit: Bool = bufferQueue.sync {
|
|
48
|
+
if _hasListeners { return true }
|
|
49
|
+
var entry = body
|
|
50
|
+
entry["_eventName"] = eventName
|
|
51
|
+
if preSubscribeBuffer.count >= PaywalloNotifications.maxBufferSize {
|
|
52
|
+
preSubscribeBuffer.removeFirst()
|
|
53
|
+
}
|
|
54
|
+
preSubscribeBuffer.append(entry)
|
|
55
|
+
return false
|
|
56
|
+
}
|
|
57
|
+
if shouldEmit {
|
|
58
|
+
DispatchQueue.main.async {
|
|
59
|
+
self.sendEvent(withName: eventName, body: body)
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
private func drainPreSubscribeBuffer() {
|
|
65
|
+
let drained: [[String: Any]] = bufferQueue.sync {
|
|
66
|
+
let copy = preSubscribeBuffer
|
|
67
|
+
preSubscribeBuffer.removeAll()
|
|
68
|
+
return copy
|
|
69
|
+
}
|
|
70
|
+
for entry in drained {
|
|
71
|
+
var body = entry
|
|
72
|
+
guard let eventName = body.removeValue(forKey: "_eventName") as? String else { continue }
|
|
73
|
+
DispatchQueue.main.async {
|
|
74
|
+
self.sendEvent(withName: eventName, body: body)
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// MARK: - Public Methods (exposed to JS)
|
|
80
|
+
|
|
81
|
+
@objc(requestPermission:resolve:reject:)
|
|
82
|
+
func requestPermission(
|
|
83
|
+
_ options: NSDictionary,
|
|
84
|
+
resolve: @escaping RCTPromiseResolveBlock,
|
|
85
|
+
reject: @escaping RCTPromiseRejectBlock
|
|
86
|
+
) {
|
|
87
|
+
let provisional = options["provisional"] as? Bool ?? false
|
|
88
|
+
let center = UNUserNotificationCenter.current()
|
|
89
|
+
|
|
90
|
+
var authOptions: UNAuthorizationOptions = [.alert, .sound, .badge]
|
|
91
|
+
if provisional {
|
|
92
|
+
authOptions.insert(.provisional)
|
|
93
|
+
}
|
|
94
|
+
|
|
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) }
|
|
98
|
+
return
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if let error = error {
|
|
102
|
+
DispatchQueue.main.async { reject("PERMISSION_ERROR", error.localizedDescription, error) }
|
|
103
|
+
return
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
self.getPermissionStatusString { status in
|
|
107
|
+
DispatchQueue.main.async { resolve(status) }
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
@objc(hasPermission:reject:)
|
|
113
|
+
func hasPermission(
|
|
114
|
+
_ resolve: @escaping RCTPromiseResolveBlock,
|
|
115
|
+
reject: @escaping RCTPromiseRejectBlock
|
|
116
|
+
) {
|
|
117
|
+
getPermissionStatusString { status in
|
|
118
|
+
DispatchQueue.main.async { resolve(status) }
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
@objc(getToken:reject:)
|
|
123
|
+
func getToken(
|
|
124
|
+
_ resolve: @escaping RCTPromiseResolveBlock,
|
|
125
|
+
reject: @escaping RCTPromiseRejectBlock
|
|
126
|
+
) {
|
|
127
|
+
DispatchQueue.main.async {
|
|
128
|
+
if let token = PaywalloNotifications.latestAPNSToken {
|
|
129
|
+
resolve(token)
|
|
130
|
+
} else {
|
|
131
|
+
resolve(nil)
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
@objc(registerForPush:reject:)
|
|
137
|
+
func registerForPush(
|
|
138
|
+
_ resolve: @escaping RCTPromiseResolveBlock,
|
|
139
|
+
reject: @escaping RCTPromiseRejectBlock
|
|
140
|
+
) {
|
|
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
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// MARK: - Permission Helper
|
|
163
|
+
|
|
164
|
+
private func getPermissionStatusString(completion: @escaping (String) -> Void) {
|
|
165
|
+
UNUserNotificationCenter.current().getNotificationSettings { settings in
|
|
166
|
+
let status: String
|
|
167
|
+
switch settings.authorizationStatus {
|
|
168
|
+
case .authorized: status = "granted"
|
|
169
|
+
case .denied: status = "denied"
|
|
170
|
+
case .provisional: status = "provisional"
|
|
171
|
+
case .notDetermined: status = "notDetermined"
|
|
172
|
+
case .ephemeral: status = "ephemeral"
|
|
173
|
+
@unknown default: status = "notDetermined"
|
|
174
|
+
}
|
|
175
|
+
DispatchQueue.main.async { completion(status) }
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// MARK: - Payload Extraction
|
|
180
|
+
|
|
181
|
+
private static func extractPayload(from userInfo: [AnyHashable: Any]) -> [String: Any] {
|
|
182
|
+
var payload: [String: Any] = [:]
|
|
183
|
+
|
|
184
|
+
if let aps = userInfo["aps"] as? [String: Any] {
|
|
185
|
+
if let alert = aps["alert"] as? [String: Any] {
|
|
186
|
+
payload["title"] = alert["title"] as? String ?? ""
|
|
187
|
+
payload["body"] = alert["body"] as? String ?? ""
|
|
188
|
+
} else if let alert = aps["alert"] as? String {
|
|
189
|
+
payload["body"] = alert
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
var data: [String: String] = [:]
|
|
194
|
+
for (key, value) in userInfo {
|
|
195
|
+
guard let key = key as? String, key != "aps" else { continue }
|
|
196
|
+
data[key] = "\(value)"
|
|
197
|
+
}
|
|
198
|
+
if !data.isEmpty {
|
|
199
|
+
payload["data"] = data
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return payload
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// MARK: - Init: observe token from AppDelegate
|
|
206
|
+
|
|
207
|
+
override init() {
|
|
208
|
+
super.init()
|
|
209
|
+
|
|
210
|
+
NotificationCenter.default.addObserver(
|
|
211
|
+
self, selector: #selector(onAPNSTokenReceived(_:)),
|
|
212
|
+
name: NSNotification.Name("PaywalloAPNSTokenReceived"), object: nil
|
|
213
|
+
)
|
|
214
|
+
NotificationCenter.default.addObserver(
|
|
215
|
+
self, selector: #selector(onRemoteNotificationReceived(_:)),
|
|
216
|
+
name: NSNotification.Name("PaywalloRemoteNotificationReceived"), object: nil
|
|
217
|
+
)
|
|
218
|
+
NotificationCenter.default.addObserver(
|
|
219
|
+
self, selector: #selector(onNotificationOpened(_:)),
|
|
220
|
+
name: NSNotification.Name("PaywalloNotificationOpened"), object: nil
|
|
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
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
deinit {
|
|
230
|
+
NotificationCenter.default.removeObserver(self)
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
@objc private func onAPNSTokenReceived(_ notification: Notification) {
|
|
234
|
+
guard let token = notification.userInfo?["token"] as? String else { return }
|
|
235
|
+
DispatchQueue.main.async {
|
|
236
|
+
PaywalloNotifications.latestAPNSToken = token
|
|
237
|
+
self.bufferOrEmit(eventName: PaywalloNotifications.tokenRefreshEvent, body: ["token": token])
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
@objc private func onRemoteNotificationReceived(_ notification: Notification) {
|
|
242
|
+
guard let payload = notification.userInfo?["payload"] as? [String: Any] else { return }
|
|
243
|
+
bufferOrEmit(eventName: PaywalloNotifications.messageReceivedEvent, body: payload)
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
@objc private func onNotificationOpened(_ notification: Notification) {
|
|
247
|
+
guard let payload = notification.userInfo?["payload"] as? [String: Any] else { return }
|
|
248
|
+
bufferOrEmit(eventName: PaywalloNotifications.notificationOpenedEvent, body: payload)
|
|
249
|
+
}
|
|
250
|
+
}
|
package/ios/PaywalloStoreKit.m
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
#import <React/RCTBridgeModule.h>
|
|
2
|
+
#import <React/RCTEventEmitter.h>
|
|
2
3
|
|
|
3
|
-
@interface RCT_EXTERN_MODULE(PaywalloStoreKit,
|
|
4
|
+
@interface RCT_EXTERN_MODULE(PaywalloStoreKit, RCTEventEmitter)
|
|
5
|
+
|
|
6
|
+
RCT_EXTERN_METHOD(supportedEvents)
|
|
4
7
|
|
|
5
8
|
RCT_EXTERN_METHOD(getProducts:(NSArray *)productIds
|
|
6
9
|
resolve:(RCTPromiseResolveBlock)resolve
|
|
@@ -1,11 +1,28 @@
|
|
|
1
1
|
import Foundation
|
|
2
2
|
import StoreKit
|
|
3
|
+
import React
|
|
3
4
|
|
|
4
5
|
@available(iOS 15.0, *)
|
|
5
6
|
@objc(PaywalloStoreKit)
|
|
6
|
-
class PaywalloStoreKit:
|
|
7
|
+
class PaywalloStoreKit: RCTEventEmitter {
|
|
7
8
|
|
|
8
9
|
private var updateListenerTask: Task<Void, Never>?
|
|
10
|
+
private var hasListeners = false
|
|
11
|
+
|
|
12
|
+
// Buffer for transaction events that arrive before JS subscribes via
|
|
13
|
+
// NativeEventEmitter. StoreKit 2 starts delivering Transaction.updates
|
|
14
|
+
// as soon as the module is constructed (cold start), but JS may not
|
|
15
|
+
// have called addListener yet. Without buffering, those payloads are
|
|
16
|
+
// silently dropped and sales go untracked.
|
|
17
|
+
// In-memory only; capped at maxBufferedEvents to avoid unbounded growth.
|
|
18
|
+
private var preSubscribeBuffer: [[String: Any]] = []
|
|
19
|
+
private let bufferQueue = DispatchQueue(label: "com.paywallo.storekit.buffer")
|
|
20
|
+
private static let maxBufferedEvents = 100
|
|
21
|
+
|
|
22
|
+
// Event name emitted for every StoreKit 2 Transaction.updates signal
|
|
23
|
+
// (renewals, refunds, cancellations). JS side subscribes via
|
|
24
|
+
// NativeEventEmitter and fans the payload out to the EventBatcher.
|
|
25
|
+
private static let transactionUpdateEvent = "PaywalloTransactionUpdate"
|
|
9
26
|
|
|
10
27
|
override init() {
|
|
11
28
|
super.init()
|
|
@@ -16,22 +33,154 @@ class PaywalloStoreKit: NSObject {
|
|
|
16
33
|
updateListenerTask?.cancel()
|
|
17
34
|
}
|
|
18
35
|
|
|
36
|
+
// MARK: - RCTEventEmitter overrides
|
|
37
|
+
|
|
38
|
+
override func supportedEvents() -> [String]! {
|
|
39
|
+
return [PaywalloStoreKit.transactionUpdateEvent]
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
override func startObserving() {
|
|
43
|
+
hasListeners = true
|
|
44
|
+
drainPreSubscribeBuffer()
|
|
45
|
+
NotificationCenter.default.addObserver(
|
|
46
|
+
self,
|
|
47
|
+
selector: #selector(handleAppForeground),
|
|
48
|
+
name: UIApplication.didBecomeActiveNotification,
|
|
49
|
+
object: nil
|
|
50
|
+
)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
override func stopObserving() {
|
|
54
|
+
hasListeners = false
|
|
55
|
+
NotificationCenter.default.removeObserver(self, name: UIApplication.didBecomeActiveNotification, object: nil)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
@objc private func handleAppForeground() {
|
|
59
|
+
guard updateListenerTask == nil || updateListenerTask!.isCancelled else { return }
|
|
60
|
+
#if DEBUG
|
|
61
|
+
print("[Paywallo:StoreKit] Restarting transaction listener after foreground")
|
|
62
|
+
#endif
|
|
63
|
+
startTransactionListener()
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/// Drains any transaction events that were buffered before JS subscribed,
|
|
67
|
+
/// emitting them in FIFO order. Called from `startObserving()` once the
|
|
68
|
+
/// JS-side NativeEventEmitter has registered its listener.
|
|
69
|
+
private func drainPreSubscribeBuffer() {
|
|
70
|
+
let buffered: [[String: Any]] = bufferQueue.sync {
|
|
71
|
+
let copy = preSubscribeBuffer
|
|
72
|
+
preSubscribeBuffer.removeAll()
|
|
73
|
+
return copy
|
|
74
|
+
}
|
|
75
|
+
guard !buffered.isEmpty else { return }
|
|
76
|
+
#if DEBUG
|
|
77
|
+
print("[Paywallo:StoreKit] Draining \(buffered.count) pre-subscribe transaction event(s)")
|
|
78
|
+
#endif
|
|
79
|
+
for payload in buffered {
|
|
80
|
+
sendEvent(withName: PaywalloStoreKit.transactionUpdateEvent, body: payload)
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
override static func requiresMainQueueSetup() -> Bool {
|
|
85
|
+
return false
|
|
86
|
+
}
|
|
87
|
+
|
|
19
88
|
// MARK: - Transaction Listener
|
|
20
89
|
|
|
21
90
|
private func startTransactionListener() {
|
|
22
91
|
updateListenerTask = Task { [weak self] in
|
|
23
92
|
for await result in Transaction.updates {
|
|
24
|
-
guard let
|
|
93
|
+
guard let self = self else { return }
|
|
25
94
|
if case .verified(let transaction) = result {
|
|
26
95
|
#if DEBUG
|
|
27
|
-
print("[Paywallo:StoreKit] Transaction update: \(transaction.productID)
|
|
96
|
+
print("[Paywallo:StoreKit] Transaction update: \(transaction.productID) revoked=\(transaction.revocationDate != nil) id=\(transaction.id)")
|
|
28
97
|
#endif
|
|
98
|
+
await self.emitTransactionUpdate(transaction)
|
|
29
99
|
await transaction.finish()
|
|
30
100
|
}
|
|
31
101
|
}
|
|
32
102
|
}
|
|
33
103
|
}
|
|
34
104
|
|
|
105
|
+
/// Classifies a StoreKit 2 `Transaction` coming from `Transaction.updates`
|
|
106
|
+
/// into one of three domain events — `renewed`, `refunded`, `canceled` —
|
|
107
|
+
/// and forwards a minimal payload to JS via RCTEventEmitter.
|
|
108
|
+
///
|
|
109
|
+
/// Classification rules:
|
|
110
|
+
/// - `revocationDate != nil` + `revocationReason == .developerIssue` → `canceled`
|
|
111
|
+
/// - `revocationDate != nil` (any other reason, incl. `.other`) → `refunded`
|
|
112
|
+
/// - otherwise → `renewed` (auto-renewable subscription renewal or
|
|
113
|
+
/// restored purchase)
|
|
114
|
+
private func emitTransactionUpdate(_ transaction: Transaction) async {
|
|
115
|
+
let type: String
|
|
116
|
+
if transaction.revocationDate != nil {
|
|
117
|
+
if let reason = transaction.revocationReason, reason == .developerIssue {
|
|
118
|
+
type = "canceled"
|
|
119
|
+
} else {
|
|
120
|
+
type = "refunded"
|
|
121
|
+
}
|
|
122
|
+
} else {
|
|
123
|
+
type = "renewed"
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
var payload: [String: Any] = [
|
|
127
|
+
"type": type,
|
|
128
|
+
"transactionId": String(transaction.id),
|
|
129
|
+
"productId": transaction.productID,
|
|
130
|
+
]
|
|
131
|
+
|
|
132
|
+
// Best-effort amount + currency lookup. Products are cached on the JS
|
|
133
|
+
// side but StoreKit 2 also lets us fetch by id synchronously-ish; if
|
|
134
|
+
// the fetch fails we simply omit the fields (server-side webhook will
|
|
135
|
+
// still carry the truth).
|
|
136
|
+
if let priceInfo = await resolveProductPrice(productId: transaction.productID) {
|
|
137
|
+
payload["amount"] = priceInfo.amount
|
|
138
|
+
payload["currency"] = priceInfo.currency
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if hasListeners {
|
|
142
|
+
sendEvent(withName: PaywalloStoreKit.transactionUpdateEvent, body: payload)
|
|
143
|
+
} else {
|
|
144
|
+
// JS not yet subscribed (cold-start race). Buffer up to
|
|
145
|
+
// maxBufferedEvents; drop oldest on overflow to bound memory.
|
|
146
|
+
bufferQueue.sync {
|
|
147
|
+
if preSubscribeBuffer.count >= PaywalloStoreKit.maxBufferedEvents {
|
|
148
|
+
preSubscribeBuffer.removeFirst()
|
|
149
|
+
#if DEBUG
|
|
150
|
+
print("[Paywallo:StoreKit] Pre-subscribe buffer overflow — dropping oldest event")
|
|
151
|
+
#endif
|
|
152
|
+
}
|
|
153
|
+
preSubscribeBuffer.append(payload)
|
|
154
|
+
}
|
|
155
|
+
#if DEBUG
|
|
156
|
+
print("[Paywallo:StoreKit] Buffered transaction event (no JS listener yet): \(transaction.productID)")
|
|
157
|
+
#endif
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
private func resolveProductPrice(productId: String) async -> (amount: Double, currency: String)? {
|
|
162
|
+
do {
|
|
163
|
+
let products = try await Product.products(for: [productId])
|
|
164
|
+
guard let product = products.first else { return nil }
|
|
165
|
+
let amount = NSDecimalNumber(decimal: product.price).doubleValue
|
|
166
|
+
var currency: String = "USD"
|
|
167
|
+
if let json = try? JSONSerialization.jsonObject(with: product.jsonRepresentation) as? [String: Any],
|
|
168
|
+
let attrs = json["attributes"] as? [String: Any],
|
|
169
|
+
let offers = attrs["offers"] as? [[String: Any]],
|
|
170
|
+
let firstOffer = offers.first,
|
|
171
|
+
let currencyCode = firstOffer["currencyCode"] as? String {
|
|
172
|
+
currency = currencyCode
|
|
173
|
+
} else if #available(iOS 16, *) {
|
|
174
|
+
currency = Locale.current.currency?.identifier ?? "USD"
|
|
175
|
+
} else {
|
|
176
|
+
currency = Locale.current.currencyCode ?? "USD"
|
|
177
|
+
}
|
|
178
|
+
return (amount, currency)
|
|
179
|
+
} catch {
|
|
180
|
+
return nil
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
35
184
|
// MARK: - Get Products
|
|
36
185
|
|
|
37
186
|
@objc
|
|
@@ -263,9 +412,4 @@ class PaywalloStoreKit: NSObject {
|
|
|
263
412
|
return "P\(period.value)D"
|
|
264
413
|
}
|
|
265
414
|
}
|
|
266
|
-
|
|
267
|
-
@objc
|
|
268
|
-
static func requiresMainQueueSetup() -> Bool {
|
|
269
|
-
return false
|
|
270
|
-
}
|
|
271
415
|
}
|
package/ios/PaywalloWebView.m
CHANGED
|
@@ -195,7 +195,7 @@
|
|
|
195
195
|
// data:, and blob: — to prevent mixed-content and injection attacks.
|
|
196
196
|
NSString *scheme = url.scheme.lowercaseString;
|
|
197
197
|
NSSet<NSString *> *allowedSchemes = [NSSet setWithObjects:
|
|
198
|
-
@"https", @"about", @"mailto", @"itms-apps",
|
|
198
|
+
@"https", @"about", @"mailto", @"itms-apps", nil];
|
|
199
199
|
if (scheme && ![allowedSchemes containsObject:scheme]) {
|
|
200
200
|
decisionHandler(WKNavigationActionPolicyCancel);
|
|
201
201
|
return;
|
|
@@ -203,8 +203,7 @@
|
|
|
203
203
|
|
|
204
204
|
// mailto: and App Store links must be opened by the OS, not loaded inside the WebView.
|
|
205
205
|
if ([scheme isEqualToString:@"mailto"] ||
|
|
206
|
-
[scheme isEqualToString:@"itms-apps"]
|
|
207
|
-
[scheme isEqualToString:@"itms-services"]) {
|
|
206
|
+
[scheme isEqualToString:@"itms-apps"]) {
|
|
208
207
|
[[UIApplication sharedApplication] openURL:url options:@{} completionHandler:nil];
|
|
209
208
|
decisionHandler(WKNavigationActionPolicyCancel);
|
|
210
209
|
return;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@virex-tech/paywallo-sdk",
|
|
3
|
-
"version": "
|
|
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",
|
|
@@ -11,7 +11,8 @@
|
|
|
11
11
|
"react-native": "./dist/index.mjs",
|
|
12
12
|
"import": "./dist/index.mjs",
|
|
13
13
|
"require": "./dist/index.js"
|
|
14
|
-
}
|
|
14
|
+
},
|
|
15
|
+
"./package.json": "./package.json"
|
|
15
16
|
},
|
|
16
17
|
"files": [
|
|
17
18
|
"dist",
|
|
@@ -42,7 +43,9 @@
|
|
|
42
43
|
"react-native",
|
|
43
44
|
"mobile",
|
|
44
45
|
"subscription",
|
|
45
|
-
"paywallo"
|
|
46
|
+
"paywallo",
|
|
47
|
+
"push-notifications",
|
|
48
|
+
"fcm"
|
|
46
49
|
],
|
|
47
50
|
"author": "Virex Tech",
|
|
48
51
|
"license": "MIT",
|
|
@@ -62,11 +65,15 @@
|
|
|
62
65
|
"node": ">=18.0.0"
|
|
63
66
|
},
|
|
64
67
|
"peerDependencies": {
|
|
68
|
+
"@react-native-firebase/app": "^22.0.0",
|
|
65
69
|
"expo-tracking-transparency": "*",
|
|
66
70
|
"react": "*",
|
|
67
71
|
"react-native": "*"
|
|
68
72
|
},
|
|
69
73
|
"peerDependenciesMeta": {
|
|
74
|
+
"@react-native-firebase/app": {
|
|
75
|
+
"optional": true
|
|
76
|
+
},
|
|
70
77
|
"expo-tracking-transparency": {
|
|
71
78
|
"optional": true
|
|
72
79
|
}
|
|
@@ -86,6 +93,6 @@
|
|
|
86
93
|
"vitest": "^4.1.2"
|
|
87
94
|
},
|
|
88
95
|
"dependencies": {
|
|
89
|
-
"
|
|
96
|
+
"zod": "^4.3.6"
|
|
90
97
|
}
|
|
91
98
|
}
|