@virex-tech/paywallo-sdk 1.4.1 → 1.5.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.
@@ -0,0 +1,8 @@
1
+ #import <React/RCTBridgeModule.h>
2
+
3
+ @interface RCT_EXTERN_MODULE(PaywalloDevice, NSObject)
4
+
5
+ RCT_EXTERN_METHOD(getDeviceInfo:(RCTPromiseResolveBlock)resolve
6
+ reject:(RCTPromiseRejectBlock)reject)
7
+
8
+ @end
@@ -0,0 +1,91 @@
1
+ import Foundation
2
+ import UIKit
3
+ import CoreTelephony
4
+
5
+ @objc(PaywalloDevice)
6
+ class PaywalloDevice: NSObject {
7
+
8
+ // requiresMainQueueSetup = true so UIDevice properties are read on the main thread
9
+ @objc static func requiresMainQueueSetup() -> Bool { return true }
10
+
11
+ // MARK: - Get Device Info
12
+
13
+ @objc
14
+ func getDeviceInfo(
15
+ _ resolve: @escaping RCTPromiseResolveBlock,
16
+ reject: @escaping RCTPromiseRejectBlock
17
+ ) {
18
+ // UIDevice properties are MainActor — must be read on the main thread
19
+ DispatchQueue.main.async {
20
+ let device = UIDevice.current
21
+ let processInfo = ProcessInfo.processInfo
22
+ let bundle = Bundle.main
23
+ let info = bundle.infoDictionary
24
+
25
+ let deviceId = device.identifierForVendor?.uuidString ?? ""
26
+ let model = device.model
27
+ let systemVersion = device.systemVersion
28
+ let systemName = device.systemName
29
+ let appVersion = info?["CFBundleShortVersionString"] as? String ?? "unknown"
30
+ let buildNumber = info?["CFBundleVersion"] as? String ?? "unknown"
31
+ let bundleId = bundle.bundleIdentifier ?? "unknown"
32
+ let totalRam = NSNumber(value: processInfo.physicalMemory)
33
+
34
+ // Disk stats — off main thread safe but called here for simplicity
35
+ var totalDisk: NSNumber = 0
36
+ var freeDisk: NSNumber = 0
37
+ do {
38
+ let attrs = try FileManager.default.attributesOfFileSystem(forPath: NSHomeDirectory())
39
+ if let size = attrs[.systemSize] as? NSNumber { totalDisk = size }
40
+ if let free = attrs[.systemFreeSize] as? NSNumber { freeDisk = free }
41
+ } catch {
42
+ // fallback stays 0
43
+ }
44
+
45
+ // Model identifier (sysctl — safe on any thread)
46
+ let modelId = self.getModelIdentifier()
47
+
48
+ // Carrier: CTTelephonyNetworkInfo is deprecated in iOS 16+ but still functional.
49
+ // Using conditional to suppress warning; no replacement API exists for carrier name.
50
+ var carrier = "unknown"
51
+ let networkInfo = CTTelephonyNetworkInfo()
52
+ if let providers = networkInfo.serviceSubscriberCellularProviders,
53
+ let first = providers.values.first,
54
+ let name = first.carrierName
55
+ {
56
+ carrier = name
57
+ }
58
+
59
+ let result: NSDictionary = [
60
+ "deviceId": deviceId,
61
+ "model": model,
62
+ "modelId": modelId,
63
+ "systemVersion": systemVersion,
64
+ "appVersion": appVersion,
65
+ "buildNumber": buildNumber,
66
+ "bundleId": bundleId,
67
+ "brand": "Apple",
68
+ "systemName": systemName,
69
+ "totalDisk": totalDisk,
70
+ "freeDisk": freeDisk,
71
+ "totalRam": totalRam,
72
+ "carrier": carrier,
73
+ "idfv": deviceId,
74
+ "installerPackage": NSNull(),
75
+ ]
76
+
77
+ resolve(result)
78
+ }
79
+ }
80
+
81
+ // MARK: - Helpers
82
+
83
+ private func getModelIdentifier() -> String {
84
+ var size = 0
85
+ sysctlbyname("hw.machine", nil, &size, nil, 0)
86
+ guard size > 0 else { return "unknown" }
87
+ var machine = [CChar](repeating: 0, count: size)
88
+ sysctlbyname("hw.machine", &machine, &size, nil, 0)
89
+ return String(cString: machine)
90
+ }
91
+ }
@@ -1,4 +1,6 @@
1
1
  import Foundation
2
+
3
+ #if canImport(FBSDKCoreKit)
2
4
  import FBSDKCoreKit
3
5
 
4
6
  @objc(PaywalloFBBridge)
@@ -75,3 +77,52 @@ class PaywalloFBBridge: NSObject {
75
77
  return false
76
78
  }
77
79
  }
80
+
81
+ #else
82
+
83
+ // Stub: FBSDKCoreKit not available — all methods are no-ops
84
+ @objc(PaywalloFBBridge)
85
+ class PaywalloFBBridge: NSObject {
86
+
87
+ @objc
88
+ func getAnonymousID(
89
+ _ resolve: @escaping RCTPromiseResolveBlock,
90
+ reject: @escaping RCTPromiseRejectBlock
91
+ ) {
92
+ resolve(nil)
93
+ }
94
+
95
+ @objc
96
+ func logEvent(
97
+ _ name: String,
98
+ params: NSDictionary,
99
+ resolve: @escaping RCTPromiseResolveBlock,
100
+ reject: @escaping RCTPromiseRejectBlock
101
+ ) {
102
+ resolve(nil)
103
+ }
104
+
105
+ @objc
106
+ func logPurchase(
107
+ _ amount: Double,
108
+ currency: String,
109
+ params: NSDictionary,
110
+ resolve: @escaping RCTPromiseResolveBlock,
111
+ reject: @escaping RCTPromiseRejectBlock
112
+ ) {
113
+ resolve(nil)
114
+ }
115
+
116
+ @objc
117
+ func setUserID(_ userId: String) {}
118
+
119
+ @objc
120
+ func flush() {}
121
+
122
+ @objc
123
+ static func requiresMainQueueSetup() -> Bool {
124
+ return false
125
+ }
126
+ }
127
+
128
+ #endif
@@ -0,0 +1,35 @@
1
+ #import <React/RCTBridgeModule.h>
2
+
3
+ @interface RCT_EXTERN_MODULE(PaywalloStorage, NSObject)
4
+
5
+ RCT_EXTERN_METHOD(secureGet:(NSString *)key
6
+ resolve:(RCTPromiseResolveBlock)resolve
7
+ reject:(RCTPromiseRejectBlock)reject)
8
+
9
+ RCT_EXTERN_METHOD(secureSet:(NSString *)key
10
+ value:(NSString *)value
11
+ resolve:(RCTPromiseResolveBlock)resolve
12
+ reject:(RCTPromiseRejectBlock)reject)
13
+
14
+ RCT_EXTERN_METHOD(secureRemove:(NSString *)key
15
+ resolve:(RCTPromiseResolveBlock)resolve
16
+ reject:(RCTPromiseRejectBlock)reject)
17
+
18
+ RCT_EXTERN_METHOD(legacySecureGet:(NSString *)legacyService
19
+ resolve:(RCTPromiseResolveBlock)resolve
20
+ reject:(RCTPromiseRejectBlock)reject)
21
+
22
+ RCT_EXTERN_METHOD(get:(NSString *)key
23
+ resolve:(RCTPromiseResolveBlock)resolve
24
+ reject:(RCTPromiseRejectBlock)reject)
25
+
26
+ RCT_EXTERN_METHOD(set:(NSString *)key
27
+ value:(NSString *)value
28
+ resolve:(RCTPromiseResolveBlock)resolve
29
+ reject:(RCTPromiseRejectBlock)reject)
30
+
31
+ RCT_EXTERN_METHOD(remove:(NSString *)key
32
+ resolve:(RCTPromiseResolveBlock)resolve
33
+ reject:(RCTPromiseRejectBlock)reject)
34
+
35
+ @end
@@ -0,0 +1,171 @@
1
+ import Foundation
2
+ import Security
3
+
4
+ @objc(PaywalloStorage)
5
+ class PaywalloStorage: NSObject {
6
+
7
+ private let service = "com.paywallo.sdk"
8
+ // Serial queue keeps Keychain ops off the main thread and serialized
9
+ private let keychainQueue = DispatchQueue(label: "com.paywallo.sdk.keychain", qos: .userInitiated)
10
+
11
+ // MARK: - Secure Storage (Keychain)
12
+
13
+ @objc
14
+ func secureGet(
15
+ _ key: String,
16
+ resolve: @escaping RCTPromiseResolveBlock,
17
+ reject: @escaping RCTPromiseRejectBlock
18
+ ) {
19
+ keychainQueue.async {
20
+ // kSecAttrAccessible must NOT be included in CopyMatching queries —
21
+ // it is an attribute filter and causes errSecItemNotFound when the
22
+ // stored item's accessibility differs from the queried value.
23
+ let query: [String: Any] = [
24
+ kSecClass as String: kSecClassGenericPassword,
25
+ kSecAttrService as String: self.service,
26
+ kSecAttrAccount as String: key,
27
+ kSecReturnData as String: true,
28
+ kSecMatchLimit as String: kSecMatchLimitOne,
29
+ ]
30
+
31
+ var item: CFTypeRef?
32
+ let status = SecItemCopyMatching(query as CFDictionary, &item)
33
+
34
+ if status == errSecSuccess,
35
+ let data = item as? Data,
36
+ let value = String(data: data, encoding: .utf8)
37
+ {
38
+ resolve(value)
39
+ } else {
40
+ resolve(nil)
41
+ }
42
+ }
43
+ }
44
+
45
+ @objc
46
+ func secureSet(
47
+ _ key: String,
48
+ value: String,
49
+ resolve: @escaping RCTPromiseResolveBlock,
50
+ reject: @escaping RCTPromiseRejectBlock
51
+ ) {
52
+ guard let data = value.data(using: .utf8) else {
53
+ resolve(false)
54
+ return
55
+ }
56
+
57
+ keychainQueue.async {
58
+ let query: [String: Any] = [
59
+ kSecClass as String: kSecClassGenericPassword,
60
+ kSecAttrService as String: self.service,
61
+ kSecAttrAccount as String: key,
62
+ ]
63
+
64
+ let attributes: [String: Any] = [
65
+ kSecValueData as String: data,
66
+ kSecAttrAccessible as String: kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly,
67
+ ]
68
+
69
+ let updateStatus = SecItemUpdate(query as CFDictionary, attributes as CFDictionary)
70
+
71
+ if updateStatus == errSecItemNotFound {
72
+ var addQuery = query
73
+ addQuery[kSecValueData as String] = data
74
+ addQuery[kSecAttrAccessible as String] = kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly
75
+ let addStatus = SecItemAdd(addQuery as CFDictionary, nil)
76
+ resolve(addStatus == errSecSuccess)
77
+ } else {
78
+ resolve(updateStatus == errSecSuccess)
79
+ }
80
+ }
81
+ }
82
+
83
+ @objc
84
+ func secureRemove(
85
+ _ key: String,
86
+ resolve: @escaping RCTPromiseResolveBlock,
87
+ reject: @escaping RCTPromiseRejectBlock
88
+ ) {
89
+ keychainQueue.async {
90
+ let query: [String: Any] = [
91
+ kSecClass as String: kSecClassGenericPassword,
92
+ kSecAttrService as String: self.service,
93
+ kSecAttrAccount as String: key,
94
+ ]
95
+
96
+ SecItemDelete(query as CFDictionary)
97
+ resolve(true)
98
+ }
99
+ }
100
+
101
+ // MARK: - Legacy Migration (SDK ≤1.5.0 → ≥1.5.2)
102
+ // react-native-keychain stored: service="com.paywallo.sdk.{key}", account="panel"
103
+ // This read-only helper is called once during migration and never written to.
104
+
105
+ @objc
106
+ func legacySecureGet(
107
+ _ legacyService: String,
108
+ resolve: @escaping RCTPromiseResolveBlock,
109
+ reject: @escaping RCTPromiseRejectBlock
110
+ ) {
111
+ keychainQueue.async {
112
+ let query: [String: Any] = [
113
+ kSecClass as String: kSecClassGenericPassword,
114
+ kSecAttrService as String: legacyService,
115
+ kSecAttrAccount as String: "panel",
116
+ kSecReturnData as String: true,
117
+ kSecMatchLimit as String: kSecMatchLimitOne,
118
+ ]
119
+
120
+ var item: CFTypeRef?
121
+ let status = SecItemCopyMatching(query as CFDictionary, &item)
122
+
123
+ if status == errSecSuccess,
124
+ let data = item as? Data,
125
+ let value = String(data: data, encoding: .utf8)
126
+ {
127
+ resolve(value)
128
+ } else {
129
+ resolve(nil)
130
+ }
131
+ }
132
+ }
133
+
134
+ // MARK: - Regular Storage (UserDefaults)
135
+
136
+ @objc
137
+ func get(
138
+ _ key: String,
139
+ resolve: @escaping RCTPromiseResolveBlock,
140
+ reject: @escaping RCTPromiseRejectBlock
141
+ ) {
142
+ let value = UserDefaults.standard.string(forKey: key)
143
+ resolve(value)
144
+ }
145
+
146
+ @objc
147
+ func set(
148
+ _ key: String,
149
+ value: String,
150
+ resolve: @escaping RCTPromiseResolveBlock,
151
+ reject: @escaping RCTPromiseRejectBlock
152
+ ) {
153
+ UserDefaults.standard.set(value, forKey: key)
154
+ resolve(true)
155
+ }
156
+
157
+ @objc
158
+ func remove(
159
+ _ key: String,
160
+ resolve: @escaping RCTPromiseResolveBlock,
161
+ reject: @escaping RCTPromiseRejectBlock
162
+ ) {
163
+ UserDefaults.standard.removeObject(forKey: key)
164
+ resolve(true)
165
+ }
166
+
167
+ @objc
168
+ static func requiresMainQueueSetup() -> Bool {
169
+ return false
170
+ }
171
+ }
@@ -23,7 +23,9 @@ class PaywalloStoreKit: NSObject {
23
23
  for await result in Transaction.updates {
24
24
  guard let _ = self else { return }
25
25
  if case .verified(let transaction) = result {
26
+ #if DEBUG
26
27
  print("[Paywallo:StoreKit] Transaction update: \(transaction.productID) status=\(transaction.revocationDate != nil ? "revoked" : "active") id=\(transaction.id)")
28
+ #endif
27
29
  await transaction.finish()
28
30
  }
29
31
  }
@@ -38,7 +40,11 @@ class PaywalloStoreKit: NSObject {
38
40
  resolve: @escaping RCTPromiseResolveBlock,
39
41
  reject: @escaping RCTPromiseRejectBlock
40
42
  ) {
41
- Task {
43
+ Task { [weak self] in
44
+ guard let self else {
45
+ reject("MODULE_DEALLOCATED", "Module was deallocated", nil)
46
+ return
47
+ }
42
48
  do {
43
49
  let products = try await Product.products(for: Set(productIds))
44
50
  let result = products.map { self.productToDict($0) }
@@ -58,18 +64,26 @@ class PaywalloStoreKit: NSObject {
58
64
  resolve: @escaping RCTPromiseResolveBlock,
59
65
  reject: @escaping RCTPromiseRejectBlock
60
66
  ) {
61
- Task {
67
+ Task { [weak self] in
68
+ guard self != nil else {
69
+ reject("MODULE_DEALLOCATED", "Module was deallocated", nil)
70
+ return
71
+ }
62
72
  do {
63
73
  var clearedCount = 0
64
74
  for await result in Transaction.unfinished {
65
75
  if case .verified(let tx) = result {
76
+ #if DEBUG
66
77
  print("[Paywallo:StoreKit] Clearing unfinished tx: \(tx.productID) id=\(tx.id)")
78
+ #endif
67
79
  await tx.finish()
68
80
  clearedCount += 1
69
81
  }
70
82
  }
71
83
  if clearedCount > 0 {
84
+ #if DEBUG
72
85
  print("[Paywallo:StoreKit] Cleared \(clearedCount) unfinished transaction(s)")
86
+ #endif
73
87
  }
74
88
 
75
89
  let products = try await Product.products(for: [productId])
@@ -190,6 +190,26 @@
190
190
  return;
191
191
  }
192
192
 
193
+ // Allow HTTPS, about: (initial blank), and schemes that must be handed off to the OS
194
+ // (mailto, App Store). Block everything else — including plain HTTP, javascript:,
195
+ // data:, and blob: — to prevent mixed-content and injection attacks.
196
+ NSString *scheme = url.scheme.lowercaseString;
197
+ NSSet<NSString *> *allowedSchemes = [NSSet setWithObjects:
198
+ @"https", @"about", @"mailto", @"itms-apps", @"itms-services", nil];
199
+ if (scheme && ![allowedSchemes containsObject:scheme]) {
200
+ decisionHandler(WKNavigationActionPolicyCancel);
201
+ return;
202
+ }
203
+
204
+ // mailto: and App Store links must be opened by the OS, not loaded inside the WebView.
205
+ if ([scheme isEqualToString:@"mailto"] ||
206
+ [scheme isEqualToString:@"itms-apps"] ||
207
+ [scheme isEqualToString:@"itms-services"]) {
208
+ [[UIApplication sharedApplication] openURL:url options:@{} completionHandler:nil];
209
+ decisionHandler(WKNavigationActionPolicyCancel);
210
+ return;
211
+ }
212
+
193
213
  decisionHandler(WKNavigationActionPolicyAllow);
194
214
  }
195
215
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@virex-tech/paywallo-sdk",
3
- "version": "1.4.1",
3
+ "version": "1.5.2",
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",
@@ -18,7 +18,8 @@
18
18
  "ios",
19
19
  "android",
20
20
  "PaywalloSdk.podspec",
21
- "react-native.config.js"
21
+ "react-native.config.js",
22
+ "LICENSE"
22
23
  ],
23
24
  "react-native": "./dist/index.mjs",
24
25
  "scripts": {
@@ -60,20 +61,20 @@
60
61
  "engines": {
61
62
  "node": ">=18.0.0"
62
63
  },
63
- "dependencies": {
64
- "@react-native-async-storage/async-storage": ">=1.0.0",
65
- "expo-application": ">=5.0.0",
66
- "expo-tracking-transparency": ">=0.5.0",
67
- "react-native-device-info": ">=10.0.0",
68
- "react-native-keychain": "^10.0.0"
69
- },
70
64
  "peerDependencies": {
71
- "react": ">=18.0.0 || >=19.0.0",
72
- "react-native": ">=0.72.0"
65
+ "expo-tracking-transparency": "*",
66
+ "react": "*",
67
+ "react-native": "*"
68
+ },
69
+ "peerDependenciesMeta": {
70
+ "expo-tracking-transparency": {
71
+ "optional": true
72
+ }
73
73
  },
74
74
  "devDependencies": {
75
75
  "@eslint/js": "^9.0.0",
76
76
  "@types/react": "^19.0.0",
77
+ "@vitest/coverage-v8": "^4.1.4",
77
78
  "eslint": "^9.0.0",
78
79
  "eslint-config-prettier": "^10.0.0",
79
80
  "eslint-plugin-simple-import-sort": "^12.0.0",