react-native-iap 14.1.1-rc.1 → 14.2.0

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.
Files changed (46) hide show
  1. package/NitroIap.podspec +2 -0
  2. package/README.md +9 -9
  3. package/android/build.gradle +2 -1
  4. package/android/consumer-rules.pro +7 -0
  5. package/app.plugin.js +1 -1
  6. package/ios/HybridRnIap.swift +380 -1192
  7. package/lib/module/helpers/subscription.js.map +1 -1
  8. package/lib/module/hooks/useIAP.js +74 -58
  9. package/lib/module/hooks/useIAP.js.map +1 -1
  10. package/lib/module/index.js +20 -3
  11. package/lib/module/index.js.map +1 -1
  12. package/lib/module/types.js +8 -0
  13. package/lib/module/types.js.map +1 -1
  14. package/lib/module/utils/error.js.map +1 -1
  15. package/lib/module/utils/errorMapping.js +33 -0
  16. package/lib/module/utils/errorMapping.js.map +1 -0
  17. package/lib/module/utils/type-bridge.js +19 -0
  18. package/lib/module/utils/type-bridge.js.map +1 -1
  19. package/lib/typescript/src/helpers/subscription.d.ts.map +1 -1
  20. package/lib/typescript/src/hooks/useIAP.d.ts +4 -4
  21. package/lib/typescript/src/hooks/useIAP.d.ts.map +1 -1
  22. package/lib/typescript/src/index.d.ts +7 -3
  23. package/lib/typescript/src/index.d.ts.map +1 -1
  24. package/lib/typescript/src/types.d.ts +19 -0
  25. package/lib/typescript/src/types.d.ts.map +1 -1
  26. package/lib/typescript/src/utils/error.d.ts.map +1 -1
  27. package/lib/typescript/src/utils/errorMapping.d.ts +5 -0
  28. package/lib/typescript/src/utils/errorMapping.d.ts.map +1 -0
  29. package/lib/typescript/src/utils/type-bridge.d.ts +3 -2
  30. package/lib/typescript/src/utils/type-bridge.d.ts.map +1 -1
  31. package/package.json +5 -2
  32. package/plugin/tsconfig.tsbuildinfo +1 -1
  33. package/src/helpers/subscription.ts +30 -30
  34. package/src/hooks/useIAP.ts +252 -230
  35. package/src/index.ts +366 -340
  36. package/src/types.ts +21 -0
  37. package/src/utils/error.ts +19 -19
  38. package/src/utils/errorMapping.ts +44 -0
  39. package/src/utils/type-bridge.ts +127 -93
  40. package/ios/ErrorUtils.swift +0 -153
  41. package/ios/ProductStore.swift +0 -43
  42. package/ios/reactnativeiap.xcodeproj/project.xcworkspace/contents.xcworkspacedata +0 -7
  43. package/ios/reactnativeiap.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +0 -8
  44. package/plugin/build/src/withIAP.d.ts +0 -3
  45. package/plugin/build/src/withIAP.js +0 -81
  46. package/plugin/build/tsconfig.tsbuildinfo +0 -1
@@ -1,9 +1,6 @@
1
1
  import Foundation
2
2
  import NitroModules
3
- import StoreKit
4
- #if os(iOS) || os(tvOS)
5
- import UIKit
6
- #endif
3
+ import OpenIAP
7
4
 
8
5
  @available(iOS 15.0, *)
9
6
  class HybridRnIap: HybridRnIapSpec {
@@ -11,14 +8,14 @@ class HybridRnIap: HybridRnIapSpec {
11
8
 
12
9
  private var updateListenerTask: Task<Void, Never>?
13
10
  private var isInitialized: Bool = false
14
- private var productStore: ProductStore?
15
- private var transactions: [String: Transaction] = [:]
16
- private var paymentObserver: PaymentObserver?
11
+ private var isInitializing: Bool = false
12
+ // No local StoreKit cache; rely on OpenIAP
13
+ // OpenIAP event subscriptions
14
+ private var purchaseUpdatedSub: Subscription?
15
+ private var purchaseErrorSub: Subscription?
16
+ private var promotedProductSub: Subscription?
17
17
 
18
- // Promoted products
19
- private var promotedProduct: Product?
20
- private var promotedPayment: SKPayment?
21
- private var promotedSKProduct: SKProduct?
18
+ // Promoted products are handled via OpenIAP listener
22
19
 
23
20
  // Event listeners
24
21
  private var purchaseUpdatedListeners: [(NitroPurchase) -> Void] = []
@@ -36,32 +33,136 @@ class HybridRnIap: HybridRnIapSpec {
36
33
  }
37
34
 
38
35
  // MARK: - Public Methods (Cross-platform)
36
+
37
+
39
38
 
40
39
  func initConnection() throws -> Promise<Bool> {
41
40
  return Promise.async {
42
- // Clean up any existing state first (important for hot reload)
43
- self.cleanupExistingState()
44
-
45
- // StoreKit 2 doesn't require explicit connection initialization
46
- // Just verify that the store is available
47
- let canMakePayments = SKPaymentQueue.canMakePayments()
48
- if canMakePayments {
49
- self.isInitialized = true
50
- self.productStore = ProductStore()
51
-
52
- // Set up PaymentObserver for promoted products
53
- if self.paymentObserver == nil {
54
- self.paymentObserver = PaymentObserver(iapModule: self)
55
- SKPaymentQueue.default().add(self.paymentObserver!)
41
+ // If already initialized or initializing, ensure listeners are attached and return immediately
42
+ if self.isInitialized || self.isInitializing {
43
+ if self.purchaseUpdatedSub == nil {
44
+ self.purchaseUpdatedSub = OpenIapModule.shared.purchaseUpdatedListener { [weak self] openIapPurchase in
45
+ guard let self else { return }
46
+ Task { @MainActor in
47
+ let nitro = self.convertOpenIapPurchaseToNitroPurchase(openIapPurchase)
48
+ self.sendPurchaseUpdate(nitro)
49
+ }
50
+ }
51
+ }
52
+ if self.purchaseErrorSub == nil {
53
+ self.purchaseErrorSub = OpenIapModule.shared.purchaseErrorListener { [weak self] error in
54
+ guard let self else { return }
55
+ Task { @MainActor in
56
+ let nitroError = self.createPurchaseErrorResult(
57
+ code: error.code,
58
+ message: error.message,
59
+ productId: error.productId
60
+ )
61
+ self.sendPurchaseError(nitroError)
62
+ }
63
+ }
64
+ }
65
+ if self.promotedProductSub == nil {
66
+ self.promotedProductSub = OpenIapModule.shared.promotedProductListenerIOS { [weak self] productId in
67
+ guard let self else { return }
68
+ Task {
69
+ do {
70
+ let req = OpenIapProductRequest(skus: [productId], type: "all")
71
+ let products = try await OpenIapModule.shared.fetchProducts(req)
72
+ if let p = products.first {
73
+ let nitro = self.convertOpenIapProductToNitroProduct(p)
74
+ await MainActor.run {
75
+ for listener in self.promotedProductListeners { listener(nitro) }
76
+ }
77
+ }
78
+ } catch {
79
+ let id = productId
80
+ await MainActor.run {
81
+ var minimal = NitroProduct()
82
+ minimal.id = id
83
+ minimal.title = id
84
+ minimal.type = "inapp"
85
+ minimal.platform = "ios"
86
+ for listener in self.promotedProductListeners { listener(minimal) }
87
+ }
88
+ }
89
+ }
90
+ }
91
+ }
92
+ return true
93
+ }
94
+
95
+ // Begin non-blocking initialization
96
+ self.isInitializing = true
97
+ // Pre-attach listeners so events are not missed
98
+ if self.purchaseUpdatedSub == nil {
99
+ self.purchaseUpdatedSub = OpenIapModule.shared.purchaseUpdatedListener { [weak self] openIapPurchase in
100
+ guard let self else { return }
101
+ Task { @MainActor in
102
+ let nitro = self.convertOpenIapPurchaseToNitroPurchase(openIapPurchase)
103
+ self.sendPurchaseUpdate(nitro)
104
+ }
56
105
  }
57
- } else {
58
- let errorJson = ErrorUtils.createErrorJson(
59
- code: IapErrorCode.iapNotAvailable,
60
- message: "In-app purchases are not available on this device"
106
+ }
107
+ if self.purchaseErrorSub == nil {
108
+ self.purchaseErrorSub = OpenIapModule.shared.purchaseErrorListener { [weak self] error in
109
+ guard let self else { return }
110
+ Task { @MainActor in
111
+ let nitroError = self.createPurchaseErrorResult(
112
+ code: error.code,
113
+ message: error.message,
114
+ productId: error.productId
115
+ )
116
+ self.sendPurchaseError(nitroError)
117
+ }
118
+ }
119
+ }
120
+ if self.promotedProductSub == nil {
121
+ self.promotedProductSub = OpenIapModule.shared.promotedProductListenerIOS { [weak self] productId in
122
+ guard let self else { return }
123
+ Task {
124
+ do {
125
+ let req = OpenIapProductRequest(skus: [productId], type: "all")
126
+ let products = try await OpenIapModule.shared.fetchProducts(req)
127
+ if let p = products.first {
128
+ let nitro = self.convertOpenIapProductToNitroProduct(p)
129
+ await MainActor.run {
130
+ for listener in self.promotedProductListeners { listener(nitro) }
131
+ }
132
+ }
133
+ } catch {
134
+ let id = productId
135
+ await MainActor.run {
136
+ var minimal = NitroProduct()
137
+ minimal.id = id
138
+ minimal.title = id
139
+ minimal.type = "inapp"
140
+ minimal.platform = "ios"
141
+ for listener in self.promotedProductListeners { listener(minimal) }
142
+ }
143
+ }
144
+ }
145
+ }
146
+ }
147
+
148
+ // Perform initialization and only report success when native init succeeds
149
+ do {
150
+ let ok = try await OpenIapModule.shared.initConnection()
151
+ self.isInitialized = ok
152
+ self.isInitializing = false
153
+ return ok
154
+ } catch {
155
+ // Surface as event and keep flags consistent
156
+ let err = self.createPurchaseErrorResult(
157
+ code: OpenIapError.E_INIT_CONNECTION,
158
+ message: error.localizedDescription,
159
+ productId: nil
61
160
  )
62
- throw NSError(domain: "RnIap", code: -1, userInfo: [NSLocalizedDescriptionKey: errorJson])
161
+ self.sendPurchaseError(err)
162
+ self.isInitialized = false
163
+ self.isInitializing = false
164
+ return false
63
165
  }
64
- return canMakePayments
65
166
  }
66
167
  }
67
168
 
@@ -76,144 +177,52 @@ class HybridRnIap: HybridRnIapSpec {
76
177
  return Promise.async {
77
178
  do {
78
179
  try self.ensureConnection()
79
- print("[RnIap] fetchProducts called with skus: \(skus), type: \(type)")
80
-
81
- // Fetch products from StoreKit 2
82
- let storeProducts = try await StoreKit.Product.products(for: Set(skus))
83
- print("[RnIap] StoreKit returned \(storeProducts.count) products")
84
-
85
- // Store products in ProductStore
86
- if let productStore = self.productStore {
87
- await productStore.addProducts(storeProducts)
88
- }
89
-
90
- // Convert StoreKit products to NitroProduct
91
- let nitroProducts = storeProducts.map { storeProduct in
92
- print("[RnIap] Converting product: \(storeProduct.id)")
93
- return self.convertToNitroProduct(storeProduct, type: type)
94
- }
95
-
96
- print("[RnIap] Returning \(nitroProducts.count) NitroProducts")
97
- return nitroProducts
180
+ // Prefer OpenIAP for fetching
181
+ let req = OpenIapProductRequest(skus: skus, type: type)
182
+ let products = try await OpenIapModule.shared.fetchProducts(req)
183
+ return products.map { self.convertOpenIapProductToNitroProduct($0) }
98
184
  } catch {
99
- print("[RnIap] Error fetching products: \(error)")
100
- let errorJson = ErrorUtils.createErrorJson(from: error)
101
- throw NSError(domain: "RnIap", code: -1, userInfo: [NSLocalizedDescriptionKey: errorJson])
185
+ throw OpenIapFailure.storeKitError(error: error)
102
186
  }
103
187
  }
104
188
  }
105
189
 
106
190
  func requestPurchase(request: NitroPurchaseRequest) throws -> Promise<Void> {
107
191
  return Promise.async {
108
- // iOS implementation
109
192
  guard let iosRequest = request.ios else {
110
- // No iOS request, send error event
111
193
  let error = self.createPurchaseErrorResult(
112
- code: IapErrorCode.userError,
194
+ code: OpenIapError.E_USER_ERROR,
113
195
  message: "No iOS request provided"
114
196
  )
115
197
  self.sendPurchaseError(error)
116
198
  return
117
199
  }
118
-
119
200
  do {
120
201
  try self.ensureConnection()
121
- guard let productStore = self.productStore else {
122
- let error = self.createPurchaseErrorResult(
123
- code: IapErrorCode.notPrepared,
124
- message: "Product store not initialized",
125
- productId: iosRequest.sku
126
- )
127
- self.sendPurchaseError(error)
128
- return
129
- }
130
-
131
- // Get product from store or fetch if not cached
132
- var product: Product? = await productStore.getProduct(productID: iosRequest.sku)
133
-
134
- if product == nil {
135
- // Try fetching from StoreKit if not in cache
136
- let products = try await Product.products(for: [iosRequest.sku])
137
- guard let fetchedProduct = products.first else {
138
- let error = self.createPurchaseErrorResult(
139
- code: IapErrorCode.itemUnavailable,
140
- message: "Invalid product ID: \(iosRequest.sku)",
141
- productId: iosRequest.sku
142
- )
143
- self.sendPurchaseError(error)
144
- return
145
- }
146
- // Store for future use
147
- await productStore.addProduct(fetchedProduct)
148
- product = fetchedProduct
149
- }
150
-
151
- // Purchase the product - this will send events internally
152
- try await self.purchaseProductWithEvents(
153
- product!,
202
+ // Delegate purchase to OpenIAP. It emits success/error events which we bridge above.
203
+ let props = OpenIapRequestPurchaseProps(
154
204
  sku: iosRequest.sku,
155
- andDangerouslyFinishTransactionAutomatically: iosRequest.andDangerouslyFinishTransactionAutomatically ?? false,
205
+ andDangerouslyFinishTransactionAutomatically: iosRequest.andDangerouslyFinishTransactionAutomatically,
156
206
  appAccountToken: iosRequest.appAccountToken,
157
- quantity: iosRequest.quantity,
158
- withOffer: iosRequest.withOffer
207
+ quantity: iosRequest.quantity != nil ? Int(iosRequest.quantity!) : nil,
208
+ withOffer: iosRequest.withOffer.flatMap { dict in
209
+ guard let id = dict["identifier"],
210
+ let key = dict["keyIdentifier"],
211
+ let nonce = dict["nonce"],
212
+ let sig = dict["signature"],
213
+ let ts = dict["timestamp"] else { return nil }
214
+ return OpenIapDiscountOffer(identifier: id, keyIdentifier: key, nonce: nonce, signature: sig, timestamp: ts)
215
+ }
159
216
  )
217
+ _ = try await OpenIapModule.shared.requestPurchase(props)
160
218
  } catch {
161
- // Map StoreKit errors to proper error codes
162
- if let nsError = error as NSError? {
163
- var errorCode = IapErrorCode.purchaseError
164
- var errorMessage = error.localizedDescription
165
-
166
- switch nsError.domain {
167
- case "SKErrorDomain":
168
- switch nsError.code {
169
- case 0: // SKError.unknown
170
- errorCode = IapErrorCode.unknown
171
- case 1: // SKError.clientInvalid
172
- errorCode = IapErrorCode.serviceError
173
- case 2: // SKError.paymentCancelled
174
- errorCode = IapErrorCode.userCancelled
175
- errorMessage = "User cancelled the purchase"
176
- case 3: // SKError.paymentInvalid
177
- errorCode = IapErrorCode.userError
178
- case 4: // SKError.paymentNotAllowed
179
- errorCode = IapErrorCode.userError
180
- errorMessage = "Payment not allowed"
181
- case 5: // SKError.storeProductNotAvailable
182
- errorCode = IapErrorCode.itemUnavailable
183
- case 6: // SKError.cloudServicePermissionDenied
184
- errorCode = IapErrorCode.serviceError
185
- case 7: // SKError.cloudServiceNetworkConnectionFailed
186
- errorCode = IapErrorCode.networkError
187
- case 8: // SKError.cloudServiceRevoked
188
- errorCode = IapErrorCode.serviceError
189
- default:
190
- errorCode = IapErrorCode.purchaseError
191
- }
192
- case "NSURLErrorDomain":
193
- errorCode = IapErrorCode.networkError
194
- errorMessage = "Network error: \(error.localizedDescription)"
195
- default:
196
- if error.localizedDescription.lowercased().contains("network") {
197
- errorCode = IapErrorCode.networkError
198
- } else if error.localizedDescription.lowercased().contains("cancelled") {
199
- errorCode = IapErrorCode.userCancelled
200
- }
201
- }
202
-
203
- let error = self.createPurchaseErrorResult(
204
- code: errorCode,
205
- message: errorMessage,
206
- productId: iosRequest.sku
207
- )
208
- self.sendPurchaseError(error)
209
- } else {
210
- let error = self.createPurchaseErrorResult(
211
- code: IapErrorCode.purchaseError,
212
- message: error.localizedDescription,
213
- productId: iosRequest.sku
214
- )
215
- self.sendPurchaseError(error)
216
- }
219
+ // OpenIAP will emit error event; also forward here for safety
220
+ let err = self.createPurchaseErrorResult(
221
+ code: OpenIapError.E_SERVICE_ERROR,
222
+ message: error.localizedDescription,
223
+ productId: iosRequest.sku
224
+ )
225
+ self.sendPurchaseError(err)
217
226
  }
218
227
  }
219
228
  }
@@ -221,134 +230,48 @@ class HybridRnIap: HybridRnIapSpec {
221
230
  func getAvailablePurchases(options: NitroAvailablePurchasesOptions?) throws -> Promise<[NitroPurchase]> {
222
231
  return Promise.async {
223
232
  try self.ensureConnection()
224
-
225
- // Support both new IOS suffixed and deprecated parameters
226
- let _ = options?.ios?.alsoPublishToEventListenerIOS ?? options?.ios?.alsoPublishToEventListener ?? false
227
- let onlyIncludeActiveItems = options?.ios?.onlyIncludeActiveItemsIOS ?? options?.ios?.onlyIncludeActiveItems ?? false
228
-
229
- var purchases: [NitroPurchase] = []
230
-
231
- // Get all transactions
232
- for await verification in Transaction.currentEntitlements {
233
- switch verification {
234
- case .verified(let transaction):
235
- // Get JWS representation from verification result
236
- let jwsRepresentation = verification.jwsRepresentation
237
-
238
- if onlyIncludeActiveItems {
239
- // Only include active subscriptions and non-consumables
240
- if transaction.productType == .nonConsumable ||
241
- (transaction.productType == .autoRenewable && transaction.revocationDate == nil) {
242
- if let products = try? await StoreKit.Product.products(for: [transaction.productID]),
243
- let product = products.first {
244
- purchases.append(self.convertToNitroPurchase(transaction, product: product, jwsRepresentation: jwsRepresentation))
245
- }
246
- }
247
- } else {
248
- // Include all transactions
249
- if let products = try? await StoreKit.Product.products(for: [transaction.productID]),
250
- let product = products.first {
251
- purchases.append(self.convertToNitroPurchase(transaction, product: product, jwsRepresentation: jwsRepresentation))
252
- }
253
- }
254
- case .unverified:
255
- continue
256
- }
233
+ do {
234
+ let onlyActive = options?.ios?.onlyIncludeActiveItemsIOS ?? options?.ios?.onlyIncludeActiveItems ?? false
235
+ let props = OpenIapPurchaseOptions(alsoPublishToEventListenerIOS: false, onlyIncludeActiveItemsIOS: onlyActive)
236
+ let purchases = try await OpenIapModule.shared.getAvailablePurchases(props)
237
+ return purchases.map { self.convertOpenIapPurchaseToNitroPurchase($0) }
238
+ } catch {
239
+ throw OpenIapFailure.networkError
257
240
  }
258
-
259
- return purchases
260
241
  }
261
242
  }
262
243
 
263
244
  func finishTransaction(params: NitroFinishTransactionParams) throws -> Promise<Variant_Bool_NitroPurchaseResult> {
264
245
  return Promise.async {
265
- // iOS implementation
266
- guard let iosParams = params.ios else {
267
- // No iOS params, return success
268
- return .first(true)
269
- }
270
-
246
+ guard let iosParams = params.ios else { return .first(true) }
271
247
  try self.ensureConnection()
272
-
273
- // Find the transaction
274
- for await verification in Transaction.all {
275
- switch verification {
276
- case .verified(let transaction):
277
- if String(transaction.id) == iosParams.transactionId {
278
- await transaction.finish()
279
- return .first(true)
280
- }
281
- case .unverified:
282
- continue
283
- }
248
+ do {
249
+ let ok = try await OpenIapModule.shared.finishTransaction(transactionIdentifier: iosParams.transactionId)
250
+ return .first(ok)
251
+ } catch {
252
+ let tid = iosParams.transactionId
253
+ throw OpenIapFailure.verificationFailed(reason: "Transaction not found: \(tid)")
284
254
  }
285
-
286
- let errorJson = ErrorUtils.createErrorJson(
287
- code: IapErrorCode.itemUnavailable,
288
- message: "Transaction not found: \(iosParams.transactionId)"
289
- )
290
- throw NSError(domain: "RnIap", code: 0, userInfo: [NSLocalizedDescriptionKey: errorJson])
291
255
  }
292
256
  }
293
257
 
294
258
  func validateReceipt(params: NitroReceiptValidationParams) throws -> Promise<Variant_NitroReceiptValidationResultIOS_NitroReceiptValidationResultAndroid> {
295
259
  return Promise.async {
296
260
  do {
297
- // Get the app receipt data
298
- guard let receiptURL = Bundle.main.appStoreReceiptURL,
299
- let receiptData = try? Data(contentsOf: receiptURL) else {
300
- let errorJson = ErrorUtils.createErrorJson(
301
- code: IapErrorCode.receiptFailed,
302
- message: "App receipt not found or could not be read"
303
- )
304
- throw NSError(domain: "RnIap", code: -1, userInfo: [NSLocalizedDescriptionKey: errorJson])
261
+ let result = try await OpenIapModule.shared.validateReceiptIOS(OpenIapReceiptValidationProps(sku: params.sku))
262
+ var latest: NitroPurchase? = nil
263
+ if let tx = result.latestTransaction {
264
+ latest = self.convertOpenIapPurchaseToNitroPurchase(tx)
305
265
  }
306
-
307
- let receiptDataBase64 = receiptData.base64EncodedString()
308
-
309
- // For StoreKit 2, we can use Transaction.currentEntitlements or Transaction.all
310
- // to get the latest transaction for the specified SKU
311
- var latestTransaction: NitroPurchase? = nil
312
-
313
- // Find the latest transaction for the specified SKU
314
- for await verificationResult in Transaction.currentEntitlements {
315
- switch verificationResult {
316
- case .verified(let transaction):
317
- if transaction.productID == params.sku {
318
- // Fetch the product details for this transaction
319
- if let products = try? await StoreKit.Product.products(for: [transaction.productID]),
320
- let product = products.first {
321
- latestTransaction = self.convertToNitroPurchase(transaction, product: product, jwsRepresentation: nil)
322
- }
323
- break
324
- }
325
- case .unverified(_, let verificationError):
326
- // Handle unverified transactions if needed
327
- print("Unverified transaction for SKU \(params.sku): \(verificationError)")
328
- }
329
- }
330
-
331
- // For StoreKit 2, the receipt is always considered valid if we can read it
332
- // and the transaction verification passed
333
- let isValid = latestTransaction != nil
334
-
335
- // Generate JWS representation (simplified for now)
336
- let jwsRepresentation = receiptDataBase64 // In a real implementation, this would be the actual JWS
337
-
338
- let result = NitroReceiptValidationResultIOS(
339
- isValid: isValid,
340
- receiptData: receiptDataBase64,
341
- jwsRepresentation: jwsRepresentation,
342
- latestTransaction: latestTransaction
266
+ let mapped = NitroReceiptValidationResultIOS(
267
+ isValid: result.isValid,
268
+ receiptData: result.receiptData,
269
+ jwsRepresentation: result.jwsRepresentation,
270
+ latestTransaction: latest
343
271
  )
344
- return Variant_NitroReceiptValidationResultIOS_NitroReceiptValidationResultAndroid.first(result)
345
-
272
+ return .first(mapped)
346
273
  } catch {
347
- let errorJson = ErrorUtils.createErrorJson(
348
- code: IapErrorCode.receiptFailed,
349
- message: "Receipt validation failed: \(error.localizedDescription)"
350
- )
351
- throw NSError(domain: "RnIap", code: -1, userInfo: [NSLocalizedDescriptionKey: errorJson])
274
+ throw OpenIapFailure.invalidReceipt
352
275
  }
353
276
  }
354
277
  }
@@ -357,146 +280,96 @@ class HybridRnIap: HybridRnIapSpec {
357
280
 
358
281
  func getStorefrontIOS() throws -> Promise<String> {
359
282
  return Promise.async {
360
- // Get the current storefront from StoreKit 2
361
- if let storefront = await Storefront.current {
362
- // Return the country code (e.g., "USA", "GBR", "KOR")
363
- return storefront.countryCode
364
- } else {
365
- // If no storefront is available, throw an error
366
- let errorJson = ErrorUtils.createErrorJson(
367
- code: IapErrorCode.unknown,
368
- message: "Unable to retrieve storefront information"
369
- )
370
- throw NSError(domain: "RnIap", code: -1, userInfo: [NSLocalizedDescriptionKey: errorJson])
283
+ do {
284
+ return try await OpenIapModule.shared.getStorefrontIOS()
285
+ } catch {
286
+ throw OpenIapFailure.storeKitError(error: error)
371
287
  }
372
288
  }
373
289
  }
374
290
 
375
291
  func getAppTransactionIOS() throws -> Promise<String?> {
376
292
  return Promise.async {
377
- if #available(iOS 16.0, *) {
378
- #if compiler(>=5.7)
379
- let verificationResult = try await AppTransaction.shared
380
-
381
- let appTransaction: AppTransaction
382
- switch verificationResult {
383
- case .verified(let verified):
384
- appTransaction = verified
385
- case .unverified(_, _):
293
+ do {
294
+ if #available(iOS 16.0, *) {
295
+ if let appTx = try await OpenIapModule.shared.getAppTransactionIOS() {
296
+ var result: [String: Any?] = [
297
+ "bundleId": appTx.bundleId,
298
+ "appVersion": appTx.appVersion,
299
+ "originalAppVersion": appTx.originalAppVersion,
300
+ "originalPurchaseDate": appTx.originalPurchaseDate.timeIntervalSince1970 * 1000,
301
+ "deviceVerification": appTx.deviceVerification,
302
+ "deviceVerificationNonce": appTx.deviceVerificationNonce,
303
+ "environment": appTx.environment,
304
+ "signedDate": appTx.signedDate.timeIntervalSince1970 * 1000,
305
+ "appId": appTx.appId,
306
+ "appVersionId": appTx.appVersionId,
307
+ "preorderDate": appTx.preorderDate != nil ? (appTx.preorderDate!.timeIntervalSince1970 * 1000) : nil
308
+ ]
309
+ result["appTransactionId"] = appTx.appTransactionId
310
+ result["originalPlatform"] = appTx.originalPlatform
311
+ let jsonData = try JSONSerialization.data(withJSONObject: result, options: [])
312
+ return String(data: jsonData, encoding: .utf8)
313
+ }
314
+ return nil
315
+ } else {
386
316
  return nil
387
317
  }
388
-
389
- var result: [String: Any?] = [
390
- "bundleId": appTransaction.bundleID,
391
- "appVersion": appTransaction.appVersion,
392
- "originalAppVersion": appTransaction.originalAppVersion,
393
- "originalPurchaseDate": appTransaction.originalPurchaseDate.timeIntervalSince1970 * 1000,
394
- "deviceVerification": appTransaction.deviceVerification.base64EncodedString(),
395
- "deviceVerificationNonce": appTransaction.deviceVerificationNonce.uuidString,
396
- "environment": appTransaction.environment.rawValue,
397
- "signedDate": appTransaction.signedDate.timeIntervalSince1970 * 1000,
398
- "appId": appTransaction.appID,
399
- "appVersionId": appTransaction.appVersionID,
400
- "preorderDate": appTransaction.preorderDate.map { $0.timeIntervalSince1970 * 1000 }
401
- ]
402
-
403
- // iOS 18.4+ properties - only compile with Xcode 16.4+ (Swift 6.1+)
404
- // This prevents build failures on Xcode 16.3 and below
405
- #if swift(>=6.1)
406
- if #available(iOS 18.4, *) {
407
- result["appTransactionId"] = appTransaction.appTransactionID
408
- result["originalPlatform"] = appTransaction.originalPlatform.rawValue
409
- }
410
- #endif
411
-
412
- // Convert dictionary to JSON string
413
- let jsonData = try JSONSerialization.data(withJSONObject: result, options: [])
414
- return String(data: jsonData, encoding: .utf8)
415
- #else
416
- let errorJson = ErrorUtils.createErrorJson(
417
- code: IapErrorCode.unknown,
418
- message: "getAppTransaction requires Xcode 15.0+ with iOS 16.0 SDK for compilation"
419
- )
420
- throw NSError(domain: "RnIap", code: -1, userInfo: [NSLocalizedDescriptionKey: errorJson])
421
- #endif
422
- } else {
423
- let errorJson = ErrorUtils.createErrorJson(
424
- code: IapErrorCode.unknown,
425
- message: "getAppTransaction requires iOS 16.0 or later"
426
- )
427
- throw NSError(domain: "RnIap", code: -1, userInfo: [NSLocalizedDescriptionKey: errorJson])
318
+ } catch {
319
+ return nil
428
320
  }
429
321
  }
430
322
  }
431
323
 
432
324
  func requestPromotedProductIOS() throws -> Promise<NitroProduct?> {
433
325
  return Promise.async {
434
- // Return the stored promoted product if available
435
- if let product = self.promotedProduct {
436
- // Convert Product to NitroProduct
437
- return self.convertProductToNitroProduct(product, type: "inapp")
438
- } else if let skProduct = self.promotedSKProduct {
439
- // Convert SKProduct to NitroProduct (backward compatibility)
440
- // First get the StoreKit 2 Product
441
- if let products = try? await StoreKit.Product.products(for: [skProduct.productIdentifier]),
442
- let product = products.first {
443
- return self.convertProductToNitroProduct(product, type: "inapp")
326
+ do {
327
+ if let p = try await OpenIapModule.shared.getPromotedProductIOS() {
328
+ var n = NitroProduct()
329
+ n.id = p.productIdentifier
330
+ n.title = p.localizedTitle
331
+ n.description = p.localizedDescription
332
+ n.type = "inapp"
333
+ n.platform = "ios"
334
+ n.price = p.price
335
+ n.currency = p.priceLocale.currencyCode
336
+ return n
444
337
  }
338
+ return nil
339
+ } catch {
340
+ return nil
445
341
  }
446
- return nil
447
342
  }
448
343
  }
449
344
 
450
345
  func buyPromotedProductIOS() throws -> Promise<Void> {
451
346
  return Promise.async {
452
- // Check if we have a promoted payment to process
453
- guard let payment = self.promotedPayment else {
454
- let errorJson = ErrorUtils.createErrorJson(
455
- code: IapErrorCode.itemUnavailable,
456
- message: "No promoted product available"
457
- )
458
- throw NSError(domain: "RnIap", code: -1, userInfo: [NSLocalizedDescriptionKey: errorJson])
347
+ do {
348
+ try await OpenIapModule.shared.requestPurchaseOnPromotedProductIOS()
349
+ } catch {
350
+ throw OpenIapFailure.productNotFound(id: "promoted-product")
459
351
  }
460
-
461
- // Add the deferred payment to the queue
462
- SKPaymentQueue.default().add(payment)
463
-
464
- // Clear the promoted product data
465
- self.promotedPayment = nil
466
- self.promotedProduct = nil
467
352
  }
468
353
  }
469
354
 
470
355
  func presentCodeRedemptionSheetIOS() throws -> Promise<Bool> {
471
356
  return Promise.async {
472
- // Present the App Store's code redemption sheet
473
- #if !targetEnvironment(simulator)
474
- await MainActor.run {
475
- SKPaymentQueue.default().presentCodeRedemptionSheet()
357
+ do {
358
+ let ok = try await OpenIapModule.shared.presentCodeRedemptionSheetIOS()
359
+ return ok
360
+ } catch {
361
+ // Fallback with explicit error for simulator or unsupported cases
362
+ throw OpenIapFailure.notSupported
476
363
  }
477
- return true
478
- #else
479
- // Not available on simulator
480
- let errorJson = ErrorUtils.createErrorJson(
481
- code: IapErrorCode.itemUnavailable,
482
- message: "Code redemption sheet is not available on simulator"
483
- )
484
- throw NSError(domain: "RnIap", code: -1, userInfo: [NSLocalizedDescriptionKey: errorJson])
485
- #endif
486
364
  }
487
365
  }
488
366
 
489
367
  func clearTransactionIOS() throws -> Promise<Void> {
490
368
  return Promise.async {
491
- // Clear all unfinished transactions
492
- for await result in Transaction.unfinished {
493
- do {
494
- let transaction = try self.checkVerified(result)
495
- await transaction.finish()
496
- self.transactions.removeValue(forKey: String(transaction.id))
497
- } catch {
498
- print("Failed to finish transaction: \(error.localizedDescription)")
499
- }
369
+ do {
370
+ try await OpenIapModule.shared.clearTransactionIOS()
371
+ } catch {
372
+ // ignore
500
373
  }
501
374
  }
502
375
  }
@@ -506,39 +379,37 @@ class HybridRnIap: HybridRnIapSpec {
506
379
  func subscriptionStatusIOS(sku: String) throws -> Promise<[NitroSubscriptionStatus]?> {
507
380
  return Promise.async {
508
381
  try self.ensureConnection()
509
-
510
- // Get the product from the store
511
- guard let products = try? await StoreKit.Product.products(for: [sku]),
512
- let product = products.first,
513
- let subscription = product.subscription else {
514
- return []
515
- }
516
-
517
- // Get subscription status
518
- let statuses = try await subscription.status
519
-
520
- return statuses.map { status in
521
- // Get renewal info if available
522
- var renewalInfo: NitroSubscriptionRenewalInfo? = nil
523
- switch status.renewalInfo {
524
- case .verified(let info):
525
- renewalInfo = NitroSubscriptionRenewalInfo(
526
- autoRenewStatus: info.willAutoRenew,
527
- autoRenewPreference: info.autoRenewPreference,
528
- expirationReason: info.expirationReason.map { Double($0.rawValue) },
529
- gracePeriodExpirationDate: info.gracePeriodExpirationDate?.timeIntervalSince1970,
530
- currentProductID: info.currentProductID,
531
- platform: "ios"
532
- )
533
- case .unverified:
534
- renewalInfo = nil
382
+ do {
383
+ if let statuses = try await OpenIapModule.shared.subscriptionStatusIOS(sku: sku) {
384
+ return statuses.map { s in
385
+ var renewal: NitroSubscriptionRenewalInfo? = nil
386
+ if let r = s.renewalInfo {
387
+ renewal = NitroSubscriptionRenewalInfo(
388
+ autoRenewStatus: r.autoRenewStatus,
389
+ autoRenewPreference: r.autoRenewPreference,
390
+ expirationReason: r.expirationReason.map { Double($0) },
391
+ gracePeriodExpirationDate: r.gracePeriodExpirationDate?.timeIntervalSince1970,
392
+ currentProductID: r.currentProductID,
393
+ platform: "ios"
394
+ )
395
+ }
396
+ let isActive: Bool
397
+ switch s.state {
398
+ case .subscribed:
399
+ isActive = true
400
+ default:
401
+ isActive = false
402
+ }
403
+ return NitroSubscriptionStatus(
404
+ state: isActive ? 1 : 0,
405
+ platform: "ios",
406
+ renewalInfo: renewal
407
+ )
408
+ }
535
409
  }
536
-
537
- return NitroSubscriptionStatus(
538
- state: Double(status.state.rawValue),
539
- platform: "ios",
540
- renewalInfo: renewalInfo
541
- )
410
+ return []
411
+ } catch {
412
+ return []
542
413
  }
543
414
  }
544
415
  }
@@ -546,43 +417,13 @@ class HybridRnIap: HybridRnIapSpec {
546
417
  func currentEntitlementIOS(sku: String) throws -> Promise<NitroPurchase?> {
547
418
  return Promise.async {
548
419
  try self.ensureConnection()
549
-
550
- // Get the product from the store
551
- guard let products = try? await StoreKit.Product.products(for: [sku]),
552
- let product = products.first else {
553
- let errorJson = ErrorUtils.createErrorJson(
554
- code: IapErrorCode.itemUnavailable,
555
- message: "Can't find product for sku \(sku)"
556
- )
557
- throw NSError(domain: "RnIap", code: -1, userInfo: [NSLocalizedDescriptionKey: errorJson])
558
- }
559
-
560
- // Get current entitlement
561
- guard let entitlement = await product.currentEntitlement else {
562
- let errorJson = ErrorUtils.createErrorJson(
563
- code: IapErrorCode.itemUnavailable,
564
- message: "Can't find entitlement for sku \(sku)"
565
- )
566
- throw NSError(domain: "RnIap", code: -1, userInfo: [NSLocalizedDescriptionKey: errorJson])
567
- }
568
-
569
- switch entitlement {
570
- case .verified(let transaction):
571
- // Get renewal info if this is a subscription
572
- var renewalInfo: Product.SubscriptionInfo.RenewalInfo? = nil
573
- if let subscription = product.subscription,
574
- let status = try? await subscription.status.first {
575
- if case .verified(let info) = status.renewalInfo {
576
- renewalInfo = info
577
- }
420
+ do {
421
+ if let p = try await OpenIapModule.shared.currentEntitlementIOS(sku: sku) {
422
+ return self.convertOpenIapPurchaseToNitroPurchase(p)
578
423
  }
579
- return self.convertToNitroPurchase(transaction, product: product, jwsRepresentation: entitlement.jwsRepresentation, renewalInfo: renewalInfo)
580
- case .unverified:
581
- let errorJson = ErrorUtils.createErrorJson(
582
- code: IapErrorCode.transactionValidationFailed,
583
- message: "Failed to verify transaction for sku \(sku)"
584
- )
585
- throw NSError(domain: "RnIap", code: -1, userInfo: [NSLocalizedDescriptionKey: errorJson])
424
+ return nil
425
+ } catch {
426
+ throw OpenIapFailure.productNotFound(id: sku)
586
427
  }
587
428
  }
588
429
  }
@@ -590,206 +431,68 @@ class HybridRnIap: HybridRnIapSpec {
590
431
  func latestTransactionIOS(sku: String) throws -> Promise<NitroPurchase?> {
591
432
  return Promise.async {
592
433
  try self.ensureConnection()
593
-
594
- // Get the product from the store
595
- guard let products = try? await StoreKit.Product.products(for: [sku]),
596
- let product = products.first else {
597
- let errorJson = ErrorUtils.createErrorJson(
598
- code: IapErrorCode.itemUnavailable,
599
- message: "Can't find product for sku \(sku)"
600
- )
601
- throw NSError(domain: "RnIap", code: -1, userInfo: [NSLocalizedDescriptionKey: errorJson])
602
- }
603
-
604
- // Get latest transaction
605
- guard let latestTransaction = await product.latestTransaction else {
606
- let errorJson = ErrorUtils.createErrorJson(
607
- code: IapErrorCode.itemUnavailable,
608
- message: "Can't find latest transaction for sku \(sku)"
609
- )
610
- throw NSError(domain: "RnIap", code: -1, userInfo: [NSLocalizedDescriptionKey: errorJson])
611
- }
612
-
613
- switch latestTransaction {
614
- case .verified(let transaction):
615
- // Get renewal info if this is a subscription
616
- var renewalInfo: Product.SubscriptionInfo.RenewalInfo? = nil
617
- if let subscription = product.subscription,
618
- let status = try? await subscription.status.first {
619
- if case .verified(let info) = status.renewalInfo {
620
- renewalInfo = info
621
- }
434
+ do {
435
+ if let p = try await OpenIapModule.shared.latestTransactionIOS(sku: sku) {
436
+ return self.convertOpenIapPurchaseToNitroPurchase(p)
622
437
  }
623
- return self.convertToNitroPurchase(transaction, product: product, jwsRepresentation: latestTransaction.jwsRepresentation, renewalInfo: renewalInfo)
624
- case .unverified:
625
- let errorJson = ErrorUtils.createErrorJson(
626
- code: IapErrorCode.transactionValidationFailed,
627
- message: "Failed to verify transaction for sku \(sku)"
628
- )
629
- throw NSError(domain: "RnIap", code: -1, userInfo: [NSLocalizedDescriptionKey: errorJson])
438
+ return nil
439
+ } catch {
440
+ throw OpenIapFailure.productNotFound(id: sku)
630
441
  }
631
442
  }
632
443
  }
633
444
 
634
445
  func getPendingTransactionsIOS() throws -> Promise<[NitroPurchase]> {
635
446
  return Promise.async {
636
- // Return the transactions we're currently tracking
637
- var pendingPurchases: [NitroPurchase] = []
638
-
639
- for (_, transaction) in self.transactions {
640
- // Get product info for each transaction
641
- if let products = try? await StoreKit.Product.products(for: [transaction.productID]),
642
- let product = products.first {
643
- pendingPurchases.append(self.convertToNitroPurchase(transaction, product: product))
644
- }
447
+ do {
448
+ let pending = try await OpenIapModule.shared.getPendingTransactionsIOS()
449
+ return pending.map { self.convertOpenIapPurchaseToNitroPurchase($0) }
450
+ } catch {
451
+ return []
645
452
  }
646
-
647
- return pendingPurchases
648
453
  }
649
454
  }
650
455
 
651
456
  func syncIOS() throws -> Promise<Bool> {
652
457
  return Promise.async {
653
458
  do {
654
- // Sync with the App Store
655
- try await AppStore.sync()
656
- return true
459
+ let ok = try await OpenIapModule.shared.syncIOS()
460
+ return ok
657
461
  } catch {
658
- let errorJson = ErrorUtils.createErrorJson(
659
- code: IapErrorCode.syncError,
660
- message: "Error synchronizing with the AppStore: \(error.localizedDescription)"
661
- )
662
- throw NSError(domain: "RnIap", code: -1, userInfo: [NSLocalizedDescriptionKey: errorJson])
462
+ throw OpenIapFailure.networkError
663
463
  }
664
464
  }
665
465
  }
666
466
 
667
467
  func showManageSubscriptionsIOS() throws -> Promise<[NitroPurchase]> {
668
468
  return Promise.async {
669
- #if !os(tvOS)
670
- // Get the active window scene
671
- guard let windowScene = await MainActor.run(body: {
672
- UIApplication.shared.connectedScenes
673
- .compactMap({ $0 as? UIWindowScene })
674
- .first(where: { $0.activationState == .foregroundActive })
675
- }) else {
676
- let errorJson = ErrorUtils.createErrorJson(
677
- code: IapErrorCode.serviceError,
678
- message: "Cannot find active window scene"
679
- )
680
- throw NSError(domain: "RnIap", code: -1, userInfo: [NSLocalizedDescriptionKey: errorJson])
681
- }
682
-
683
- // Get current subscription statuses before showing UI
684
- var beforeStatuses: [String: Bool] = [:]
685
- var subscriptionSkus = await self.productStore?.getAllSubscriptionProductIds() ?? []
686
-
687
- // Fallback: If ProductStore is empty, derive SKUs from current entitlements
688
- if subscriptionSkus.isEmpty {
689
- var ids = Set<String>()
690
- for await verification in Transaction.currentEntitlements {
691
- if case .verified(let t) = verification, t.productType == .autoRenewable {
692
- ids.insert(t.productID)
693
- }
694
- }
695
- subscriptionSkus = Array(ids)
696
- }
697
-
698
- for sku in subscriptionSkus {
699
- if let product = await self.fetchProductIfNeeded(sku: sku),
700
- let status = try? await product.subscription?.status.first {
701
- var willAutoRenew = false
702
- if case .verified(let info) = status.renewalInfo {
703
- willAutoRenew = info.willAutoRenew
704
- }
705
- beforeStatuses[sku] = willAutoRenew
706
- }
707
- }
708
-
709
- // Show the management UI
710
- try await AppStore.showManageSubscriptions(in: windowScene)
711
-
712
- // Wait a bit for changes to propagate
713
- try? await Task.sleep(nanoseconds: 1_500_000_000) // 1.5 seconds
714
-
715
- // Check for changes and return updated subscriptions
716
- var updatedSubscriptions: [NitroPurchase] = []
717
-
718
- for sku in subscriptionSkus {
719
- if let product = await self.fetchProductIfNeeded(sku: sku),
720
- let status = try? await product.subscription?.status.first,
721
- let result = await product.latestTransaction {
722
-
723
- // Check current status
724
- var currentWillAutoRenew = false
725
- var currentRenewalInfo: Product.SubscriptionInfo.RenewalInfo? = nil
726
- if case .verified(let info) = status.renewalInfo {
727
- currentWillAutoRenew = info.willAutoRenew
728
- currentRenewalInfo = info
729
- }
730
-
731
- // Check if status changed
732
- let previousWillAutoRenew = beforeStatuses[sku] ?? false
733
- if previousWillAutoRenew != currentWillAutoRenew {
734
- // Status changed, include in result
735
- do {
736
- let transaction = try self.checkVerified(result)
737
- let purchase = self.convertToNitroPurchase(
738
- transaction,
739
- product: product,
740
- jwsRepresentation: result.jwsRepresentation,
741
- renewalInfo: currentRenewalInfo
742
- )
743
-
744
- // Add renewal info as additional data
745
- // Note: We'll add this info through the purchase token or other field
746
- // since NitroPurchase doesn't have a dedicated renewal info field
747
-
748
- updatedSubscriptions.append(purchase)
749
- } catch {
750
- // Skip if verification fails
751
- }
752
- }
753
- }
469
+ do {
470
+ // Trigger system UI
471
+ _ = try await OpenIapModule.shared.showManageSubscriptionsIOS()
472
+ // Return current entitlements as approximation of updates
473
+ let purchases = try await OpenIapModule.shared.getAvailablePurchases(OpenIapPurchaseOptions(alsoPublishToEventListenerIOS: false, onlyIncludeActiveItemsIOS: true))
474
+ return purchases.map { self.convertOpenIapPurchaseToNitroPurchase($0) }
475
+ } catch {
476
+ throw OpenIapFailure.storeKitError(error: error)
754
477
  }
755
-
756
- return updatedSubscriptions
757
- #else
758
- let errorJson = ErrorUtils.createErrorJson(
759
- code: IapErrorCode.serviceError,
760
- message: "This method is not available on tvOS"
761
- )
762
- throw NSError(domain: "RnIap", code: -1, userInfo: [NSLocalizedDescriptionKey: errorJson])
763
- #endif
764
478
  }
765
479
  }
766
480
 
767
481
  func isEligibleForIntroOfferIOS(groupID: String) throws -> Promise<Bool> {
768
482
  return Promise.async {
769
- return await Product.SubscriptionInfo.isEligibleForIntroOffer(for: groupID)
483
+ return await OpenIapModule.shared.isEligibleForIntroOfferIOS(groupID: groupID)
770
484
  }
771
485
  }
772
486
 
773
487
  func getReceiptDataIOS() throws -> Promise<String> {
774
488
  return Promise.async {
775
- if let appStoreReceiptURL = Bundle.main.appStoreReceiptURL,
776
- FileManager.default.fileExists(atPath: appStoreReceiptURL.path) {
777
- do {
778
- let receiptData = try Data(contentsOf: appStoreReceiptURL, options: .alwaysMapped)
779
- return receiptData.base64EncodedString(options: [])
780
- } catch {
781
- let errorJson = ErrorUtils.createErrorJson(
782
- code: IapErrorCode.receiptFailed,
783
- message: "Error reading receipt data: \(error.localizedDescription)"
784
- )
785
- throw NSError(domain: "RnIap", code: -1, userInfo: [NSLocalizedDescriptionKey: errorJson])
489
+ do {
490
+ if let receipt = try await OpenIapModule.shared.getReceiptDataIOS() {
491
+ return receipt
786
492
  }
787
- } else {
788
- let errorJson = ErrorUtils.createErrorJson(
789
- code: IapErrorCode.receiptFailed,
790
- message: "App Store receipt not found"
791
- )
792
- throw NSError(domain: "RnIap", code: -1, userInfo: [NSLocalizedDescriptionKey: errorJson])
493
+ throw OpenIapFailure.invalidReceipt
494
+ } catch {
495
+ throw OpenIapFailure.invalidReceipt
793
496
  }
794
497
  }
795
498
  }
@@ -797,129 +500,48 @@ class HybridRnIap: HybridRnIapSpec {
797
500
  func isTransactionVerifiedIOS(sku: String) throws -> Promise<Bool> {
798
501
  return Promise.async {
799
502
  try self.ensureConnection()
800
-
801
- // Get the product from the store
802
- guard let products = try? await StoreKit.Product.products(for: [sku]),
803
- let product = products.first,
804
- let result = await product.latestTransaction else {
805
- return false
806
- }
807
-
808
- // Check if transaction is verified
809
- switch result {
810
- case .verified:
811
- return true
812
- case .unverified:
813
- return false
814
- }
503
+ return await OpenIapModule.shared.isTransactionVerifiedIOS(sku: sku)
815
504
  }
816
505
  }
817
506
 
818
507
  func getTransactionJwsIOS(sku: String) throws -> Promise<String?> {
819
508
  return Promise.async {
820
509
  try self.ensureConnection()
821
-
822
- // Get the product from the store
823
- guard let products = try? await StoreKit.Product.products(for: [sku]),
824
- let product = products.first,
825
- let result = await product.latestTransaction else {
826
- let errorJson = ErrorUtils.createErrorJson(
827
- code: IapErrorCode.itemUnavailable,
828
- message: "Can't find transaction for sku \(sku)"
829
- )
830
- throw NSError(domain: "RnIap", code: -1, userInfo: [NSLocalizedDescriptionKey: errorJson])
510
+ do { return try await OpenIapModule.shared.getTransactionJwsIOS(sku: sku) } catch {
511
+ throw OpenIapFailure.verificationFailed(reason: "Can't find transaction for sku \(sku)")
831
512
  }
832
-
833
- return result.jwsRepresentation
834
513
  }
835
514
  }
836
515
 
837
516
  func beginRefundRequestIOS(sku: String) throws -> Promise<String?> {
838
517
  return Promise.async {
839
- #if !os(tvOS)
840
- if #available(macOS 12.0, *) {
841
- // Find the latest transaction for the SKU
842
- var latestTransaction: Transaction? = nil
843
-
844
- for await result in Transaction.currentEntitlements {
845
- switch result {
846
- case .verified(let transaction):
847
- if transaction.productID == sku {
848
- latestTransaction = transaction
849
- break
850
- }
851
- case .unverified(_, _):
852
- continue
853
- }
854
- }
855
-
856
- guard let transaction = latestTransaction else {
857
- let errorJson = ErrorUtils.createErrorJson(
858
- code: IapErrorCode.itemUnavailable,
859
- message: "Can't find transaction for SKU \(sku)"
860
- )
861
- throw NSError(domain: "RnIap", code: -1, userInfo: [NSLocalizedDescriptionKey: errorJson])
862
- }
863
-
864
- // Begin refund request
865
- do {
866
- // Get the active window scene
867
- guard let windowScene = await MainActor.run(body: {
868
- UIApplication.shared.connectedScenes
869
- .compactMap({ $0 as? UIWindowScene })
870
- .first(where: { $0.activationState == .foregroundActive })
871
- }) else {
872
- let errorJson = ErrorUtils.createErrorJson(
873
- code: IapErrorCode.serviceError,
874
- message: "Cannot find active window scene"
875
- )
876
- throw NSError(domain: "RnIap", code: -1, userInfo: [NSLocalizedDescriptionKey: errorJson])
877
- }
878
-
879
- let refundStatus = try await transaction.beginRefundRequest(in: windowScene)
880
-
881
- // Convert refund status to string
882
- switch refundStatus {
883
- case .success:
884
- return "success"
885
- case .userCancelled:
886
- return "userCancelled"
887
- @unknown default:
888
- return "unknown"
889
- }
890
- } catch {
891
- let errorJson = ErrorUtils.createErrorJson(
892
- code: IapErrorCode.serviceError,
893
- message: "Failed to begin refund request: \(error.localizedDescription)"
894
- )
895
- throw NSError(domain: "RnIap", code: -1, userInfo: [NSLocalizedDescriptionKey: errorJson])
896
- }
897
- } else {
898
- // Refund request is only available on iOS 15+
899
- let errorJson = ErrorUtils.createErrorJson(
900
- code: IapErrorCode.itemUnavailable,
901
- message: "Refund request requires iOS 15.0 or later"
902
- )
903
- throw NSError(domain: "RnIap", code: -1, userInfo: [NSLocalizedDescriptionKey: errorJson])
904
- }
905
- #else
906
- // Not available on tvOS
907
- let errorJson = ErrorUtils.createErrorJson(
908
- code: IapErrorCode.itemUnavailable,
909
- message: "Refund request is not available on tvOS"
910
- )
911
- throw NSError(domain: "RnIap", code: -1, userInfo: [NSLocalizedDescriptionKey: errorJson])
912
- #endif
518
+ do { return try await OpenIapModule.shared.beginRefundRequestIOS(sku: sku) } catch { return nil }
913
519
  }
914
520
  }
915
521
 
916
522
  func addPromotedProductListenerIOS(listener: @escaping (NitroProduct) -> Void) throws {
917
523
  promotedProductListeners.append(listener)
918
524
 
919
- // If we already have a promoted product, notify the new listener immediately
920
- if let product = promotedProduct {
921
- let nitroProduct = convertProductToNitroProduct(product, type: "inapp")
922
- listener(nitroProduct)
525
+ // If a promoted product is already available from OpenIAP, notify immediately
526
+ Task {
527
+ if let p = try? await OpenIapModule.shared.getPromotedProductIOS() {
528
+ let id = p.productIdentifier
529
+ let title = p.localizedTitle
530
+ let desc = p.localizedDescription
531
+ let price = p.price
532
+ let currency = p.priceLocale.currencyCode
533
+ await MainActor.run {
534
+ var n = NitroProduct()
535
+ n.id = id
536
+ n.title = title
537
+ n.description = desc
538
+ n.type = "inapp"
539
+ n.platform = "ios"
540
+ n.price = price
541
+ n.currency = currency
542
+ listener(n)
543
+ }
544
+ }
923
545
  }
924
546
  }
925
547
 
@@ -953,443 +575,10 @@ class HybridRnIap: HybridRnIapSpec {
953
575
 
954
576
  // MARK: - Private Helper Methods
955
577
 
956
- private func fetchProductIfNeeded(sku: String) async -> Product? {
957
- // Check if product is already cached
958
- if let product = await self.productStore?.getProduct(productID: sku) {
959
- return product
960
- }
961
-
962
- // Fetch from StoreKit and cache it
963
- if let products = try? await StoreKit.Product.products(for: [sku]),
964
- let product = products.first {
965
- await self.productStore?.addProduct(product)
966
- return product
967
- }
968
-
969
- return nil
970
- }
971
-
972
- private func purchaseProduct(
973
- _ product: Product,
974
- sku: String,
975
- andDangerouslyFinishTransactionAutomatically: Bool,
976
- appAccountToken: String?,
977
- quantity: Double?,
978
- withOffer: [String: String]?
979
- ) async throws -> NitroPurchase {
980
- // Prepare purchase options
981
- var options: Set<Product.PurchaseOption> = []
982
-
983
- // Add quantity if specified
984
- if let quantity = quantity, quantity > 0 {
985
- options.insert(.quantity(Int(quantity)))
986
- }
987
-
988
- // Add promotional offer if provided
989
- if let offerID = withOffer?["identifier"],
990
- let keyID = withOffer?["keyIdentifier"],
991
- let nonce = withOffer?["nonce"],
992
- let signature = withOffer?["signature"],
993
- let timestamp = withOffer?["timestamp"],
994
- let uuidNonce = UUID(uuidString: nonce),
995
- let signatureData = Data(base64Encoded: signature),
996
- let timestampInt = Int(timestamp) {
997
- options.insert(
998
- .promotionalOffer(
999
- offerID: offerID,
1000
- keyID: keyID,
1001
- nonce: uuidNonce,
1002
- signature: signatureData,
1003
- timestamp: timestampInt
1004
- )
1005
- )
1006
- }
1007
-
1008
- // Add app account token if provided
1009
- if let appAccountToken = appAccountToken,
1010
- let appAccountUUID = UUID(uuidString: appAccountToken) {
1011
- options.insert(.appAccountToken(appAccountUUID))
1012
- }
1013
-
1014
- // Get window scene for iOS 17+ purchase confirmation
1015
- let windowScene = await currentWindowScene()
1016
-
1017
- // Perform the purchase
1018
- let result: Product.PurchaseResult
1019
- #if swift(>=5.9)
1020
- if #available(iOS 17.0, tvOS 17.0, *) {
1021
- if let windowScene = windowScene {
1022
- result = try await product.purchase(confirmIn: windowScene, options: options)
1023
- } else {
1024
- result = try await product.purchase(options: options)
1025
- }
1026
- } else {
1027
- #if !os(visionOS)
1028
- result = try await product.purchase(options: options)
1029
- #endif
1030
- }
1031
- #elseif !os(visionOS)
1032
- result = try await product.purchase(options: options)
1033
- #endif
1034
-
1035
- switch result {
1036
- case .success(let verification):
1037
- let transaction = try checkVerified(verification)
1038
-
1039
- // Store transaction if not auto-finishing
1040
- if !andDangerouslyFinishTransactionAutomatically {
1041
- self.transactions[String(transaction.id)] = transaction
1042
- }
1043
-
1044
- // Get JWS representation
1045
- let jwsRepresentation = verification.jwsRepresentation
1046
-
1047
- // Create purchase object
1048
- let purchase = self.convertToNitroPurchase(
1049
- transaction,
1050
- product: product,
1051
- jwsRepresentation: jwsRepresentation
1052
- )
1053
-
1054
- // Finish transaction if requested
1055
- if andDangerouslyFinishTransactionAutomatically {
1056
- await transaction.finish()
1057
- }
1058
-
1059
- return purchase
1060
-
1061
- case .userCancelled:
1062
- let errorJson = ErrorUtils.createErrorJson(
1063
- code: IapErrorCode.userCancelled,
1064
- message: "User cancelled the purchase",
1065
- productId: sku
1066
- )
1067
- throw NSError(domain: "RnIap", code: 1, userInfo: [NSLocalizedDescriptionKey: errorJson])
1068
-
1069
- case .pending:
1070
- let errorJson = ErrorUtils.createErrorJson(
1071
- code: IapErrorCode.deferredPayment,
1072
- message: "The payment was deferred",
1073
- productId: sku
1074
- )
1075
- throw NSError(domain: "RnIap", code: 2, userInfo: [NSLocalizedDescriptionKey: errorJson])
1076
-
1077
- @unknown default:
1078
- let errorJson = ErrorUtils.createErrorJson(
1079
- code: IapErrorCode.unknown,
1080
- message: "Unknown purchase result",
1081
- productId: sku
1082
- )
1083
- throw NSError(domain: "RnIap", code: 0, userInfo: [NSLocalizedDescriptionKey: errorJson])
1084
- }
1085
- }
1086
-
1087
- private func purchaseProductWithEvents(
1088
- _ product: Product,
1089
- sku: String,
1090
- andDangerouslyFinishTransactionAutomatically: Bool,
1091
- appAccountToken: String?,
1092
- quantity: Double?,
1093
- withOffer: [String: String]?
1094
- ) async throws {
1095
- // Prepare purchase options
1096
- var options: Set<Product.PurchaseOption> = []
1097
-
1098
- // Add quantity if specified
1099
- if let quantity = quantity, quantity > 0 {
1100
- options.insert(.quantity(Int(quantity)))
1101
- }
1102
-
1103
- // Add promotional offer if provided
1104
- if let offerID = withOffer?["identifier"],
1105
- let keyID = withOffer?["keyIdentifier"],
1106
- let nonce = withOffer?["nonce"],
1107
- let signature = withOffer?["signature"],
1108
- let timestamp = withOffer?["timestamp"],
1109
- let uuidNonce = UUID(uuidString: nonce),
1110
- let signatureData = Data(base64Encoded: signature),
1111
- let timestampInt = Int(timestamp) {
1112
- options.insert(
1113
- .promotionalOffer(
1114
- offerID: offerID,
1115
- keyID: keyID,
1116
- nonce: uuidNonce,
1117
- signature: signatureData,
1118
- timestamp: timestampInt
1119
- )
1120
- )
1121
- }
1122
-
1123
- // Add app account token if provided
1124
- if let appAccountToken = appAccountToken,
1125
- let appAccountUUID = UUID(uuidString: appAccountToken) {
1126
- options.insert(.appAccountToken(appAccountUUID))
1127
- }
1128
-
1129
- // Get window scene for iOS 17+ purchase confirmation
1130
- let windowScene = await currentWindowScene()
1131
-
1132
- // Perform the purchase
1133
- let result: Product.PurchaseResult
1134
- #if swift(>=5.9)
1135
- if #available(iOS 17.0, tvOS 17.0, *) {
1136
- if let windowScene = windowScene {
1137
- result = try await product.purchase(confirmIn: windowScene, options: options)
1138
- } else {
1139
- result = try await product.purchase(options: options)
1140
- }
1141
- } else {
1142
- #if !os(visionOS)
1143
- result = try await product.purchase(options: options)
1144
- #endif
1145
- }
1146
- #elseif !os(visionOS)
1147
- result = try await product.purchase(options: options)
1148
- #endif
1149
-
1150
- switch result {
1151
- case .success(let verification):
1152
- let transaction = try checkVerified(verification)
1153
-
1154
- // Store transaction if not auto-finishing
1155
- if !andDangerouslyFinishTransactionAutomatically {
1156
- self.transactions[String(transaction.id)] = transaction
1157
- }
1158
-
1159
- // Get JWS representation
1160
- let jwsRepresentation = verification.jwsRepresentation
1161
-
1162
- // Create purchase object
1163
- let purchase = self.convertToNitroPurchase(
1164
- transaction,
1165
- product: product,
1166
- jwsRepresentation: jwsRepresentation
1167
- )
1168
-
1169
- // Finish transaction if requested
1170
- if andDangerouslyFinishTransactionAutomatically {
1171
- await transaction.finish()
1172
- }
1173
-
1174
- // Send purchase update event
1175
- self.sendPurchaseUpdate(purchase)
1176
-
1177
- case .userCancelled:
1178
- let error = self.createPurchaseErrorResult(
1179
- code: IapErrorCode.userCancelled,
1180
- message: "User cancelled the purchase",
1181
- productId: sku
1182
- )
1183
- self.sendPurchaseError(error)
1184
-
1185
- case .pending:
1186
- let error = self.createPurchaseErrorResult(
1187
- code: IapErrorCode.deferredPayment,
1188
- message: "The payment was deferred",
1189
- productId: sku
1190
- )
1191
- self.sendPurchaseError(error)
1192
-
1193
- @unknown default:
1194
- let error = self.createPurchaseErrorResult(
1195
- code: IapErrorCode.unknown,
1196
- message: "Unknown purchase result",
1197
- productId: sku
1198
- )
1199
- self.sendPurchaseError(error)
1200
- }
1201
- }
1202
-
1203
- private func checkVerified<T>(_ result: VerificationResult<T>) throws -> T {
1204
- switch result {
1205
- case .verified(let safe):
1206
- return safe
1207
- case .unverified(_, let error):
1208
- let errorJson = ErrorUtils.createErrorJson(
1209
- code: IapErrorCode.transactionValidationFailed,
1210
- message: "Transaction verification failed: \(error)",
1211
- underlyingError: error
1212
- )
1213
- throw NSError(domain: "RnIap", code: -1, userInfo: [NSLocalizedDescriptionKey: errorJson])
1214
- }
1215
- }
1216
-
1217
578
  private func ensureConnection() throws {
1218
579
  guard isInitialized else {
1219
- let errorJson = ErrorUtils.createErrorJson(
1220
- code: IapErrorCode.notPrepared,
1221
- message: "Connection not initialized. Call initConnection() first."
1222
- )
1223
- throw NSError(domain: "RnIap", code: -1, userInfo: [NSLocalizedDescriptionKey: errorJson])
1224
- }
1225
-
1226
- guard SKPaymentQueue.canMakePayments() else {
1227
- let errorJson = ErrorUtils.createErrorJson(
1228
- code: IapErrorCode.iapNotAvailable,
1229
- message: "In-app purchases are not available on this device"
1230
- )
1231
- throw NSError(domain: "RnIap", code: -1, userInfo: [NSLocalizedDescriptionKey: errorJson])
1232
- }
1233
- }
1234
-
1235
- @MainActor
1236
- private func currentWindowScene() async -> UIWindowScene? {
1237
- #if os(iOS) || os(tvOS)
1238
- // Find the active window scene
1239
- for scene in UIApplication.shared.connectedScenes {
1240
- if let windowScene = scene as? UIWindowScene,
1241
- windowScene.activationState == .foregroundActive {
1242
- return windowScene
1243
- }
1244
- }
1245
-
1246
- // Fallback to first window scene if no active one found
1247
- for scene in UIApplication.shared.connectedScenes {
1248
- if let windowScene = scene as? UIWindowScene {
1249
- return windowScene
1250
- }
1251
- }
1252
- #endif
1253
- return nil
1254
- }
1255
-
1256
- private func convertToNitroProduct(_ storeProduct: StoreKit.Product, type: String) -> NitroProduct {
1257
- var product = NitroProduct()
1258
-
1259
- // Basic fields
1260
- product.id = storeProduct.id
1261
- product.title = storeProduct.displayName
1262
- product.description = storeProduct.description
1263
- // Map StoreKit type to cross-platform compatible string: "inapp" | "subs"
1264
- // and set detailed iOS product type
1265
- #if swift(>=5.7)
1266
- switch storeProduct.type {
1267
- case .consumable:
1268
- product.type = "inapp"
1269
- product.typeIOS = "consumable"
1270
- case .nonConsumable:
1271
- product.type = "inapp"
1272
- product.typeIOS = "nonConsumable"
1273
- case .autoRenewable:
1274
- product.type = "subs"
1275
- product.typeIOS = "autoRenewableSubscription"
1276
- case .nonRenewable:
1277
- product.type = "subs"
1278
- product.typeIOS = "nonRenewingSubscription"
1279
- default:
1280
- product.type = type // fallback to requested filter
1281
- product.typeIOS = nil
1282
- }
1283
- #else
1284
- product.type = type
1285
- #endif
1286
- product.displayName = storeProduct.displayName
1287
- product.displayPrice = storeProduct.displayPrice
1288
- product.platform = "ios"
1289
-
1290
- // Price and currency - priceFormatStyle.currencyCode is not optional
1291
- product.currency = storeProduct.priceFormatStyle.currencyCode
1292
-
1293
- // Convert Decimal price to Double - price is not optional
1294
- product.price = NSDecimalNumber(decimal: storeProduct.price).doubleValue
1295
-
1296
- // iOS specific fields
1297
- product.isFamilyShareableIOS = storeProduct.isFamilyShareable
1298
-
1299
- // Set JSON representation
1300
- product.jsonRepresentationIOS = String(data: storeProduct.jsonRepresentation, encoding: .utf8)
1301
- ?? storeProduct.jsonRepresentation.base64EncodedString()
1302
-
1303
- // Subscription information
1304
- if let subscription = storeProduct.subscription {
1305
- // Subscription period - value is Int, need to convert to Double
1306
- product.subscriptionPeriodUnitIOS = getPeriodString(subscription.subscriptionPeriod.unit)
1307
- product.subscriptionPeriodNumberIOS = Double(subscription.subscriptionPeriod.value)
1308
-
1309
- // Introductory offer
1310
- if let introOffer = subscription.introductoryOffer {
1311
- product.introductoryPriceIOS = introOffer.displayPrice
1312
- product.introductoryPriceAsAmountIOS = NSDecimalNumber(decimal: introOffer.price).doubleValue
1313
- product.introductoryPricePaymentModeIOS = String(introOffer.paymentMode.rawValue)
1314
- product.introductoryPriceNumberOfPeriodsIOS = Double(introOffer.periodCount)
1315
- product.introductoryPriceSubscriptionPeriodIOS = getPeriodString(introOffer.period.unit)
1316
- }
580
+ throw OpenIapFailure.restoreFailed(reason: "Connection not initialized. Call initConnection() first.")
1317
581
  }
1318
-
1319
- return product
1320
- }
1321
-
1322
- private func convertProductToNitroProduct(_ product: Product, type: String) -> NitroProduct {
1323
- return convertToNitroProduct(product, type: type)
1324
- }
1325
-
1326
- private func getPeriodString(_ unit: StoreKit.Product.SubscriptionPeriod.Unit) -> String {
1327
- switch unit {
1328
- case .day: return "DAY"
1329
- case .week: return "WEEK"
1330
- case .month: return "MONTH"
1331
- case .year: return "YEAR"
1332
- @unknown default: return ""
1333
- }
1334
- }
1335
-
1336
- private func getPurchaseState(_ transaction: Transaction) -> String {
1337
- // Map StoreKit 2 transaction states to our unified PurchaseState enum
1338
- if transaction.revocationDate != nil {
1339
- return "failed"
1340
- }
1341
-
1342
- // Check if transaction needs finishing (pending)
1343
- // In StoreKit 2, transactions are automatically finished unless we handle them manually
1344
- // We consider a transaction as "purchased" once it's verified
1345
-
1346
- // Note: StoreKit 2 doesn't have direct equivalents for all states
1347
- // - "restored" is handled separately through restore purchases flow
1348
- // - "deferred" happens with parental controls but isn't exposed in Transaction
1349
- // - "pending" isn't directly available in StoreKit 2
1350
-
1351
- return "purchased"
1352
- }
1353
-
1354
- private func convertToNitroPurchase(_ transaction: Transaction, product: StoreKit.Product, jwsRepresentation: String? = nil, renewalInfo: Product.SubscriptionInfo.RenewalInfo? = nil) -> NitroPurchase {
1355
- var purchase = NitroPurchase()
1356
-
1357
- // Basic fields
1358
- purchase.id = String(transaction.id)
1359
- purchase.productId = transaction.productID
1360
- purchase.transactionDate = transaction.purchaseDate.timeIntervalSince1970 * 1000 // Convert to milliseconds
1361
- purchase.platform = "ios"
1362
-
1363
- // Common fields
1364
- purchase.quantity = Double(transaction.purchasedQuantity)
1365
- purchase.purchaseState = getPurchaseState(transaction)
1366
- // For iOS, check renewal info first if available, otherwise fall back to expiration date check
1367
- if let renewalInfo = renewalInfo {
1368
- purchase.isAutoRenewing = renewalInfo.willAutoRenew
1369
- } else {
1370
- purchase.isAutoRenewing = (product.type == .autoRenewable && transaction.expirationDate != nil && transaction.expirationDate! > Date())
1371
- }
1372
-
1373
- // iOS specific fields
1374
- purchase.quantityIOS = Double(transaction.purchasedQuantity)
1375
-
1376
- // originalID is not optional in StoreKit 2
1377
- purchase.originalTransactionIdentifierIOS = String(transaction.originalID)
1378
-
1379
- // originalPurchaseDate is not optional
1380
- purchase.originalTransactionDateIOS = transaction.originalPurchaseDate.timeIntervalSince1970 * 1000
1381
-
1382
- if let appAccountToken = transaction.appAccountToken {
1383
- purchase.appAccountToken = appAccountToken.uuidString
1384
- }
1385
-
1386
- // Store the JWS representation as purchaseToken for verification
1387
- // JWS is passed from VerificationResult
1388
- if let jws = jwsRepresentation {
1389
- purchase.purchaseToken = jws
1390
- }
1391
-
1392
- return purchase
1393
582
  }
1394
583
 
1395
584
  private func sendPurchaseUpdate(_ purchase: NitroPurchase) {
@@ -1418,65 +607,64 @@ class HybridRnIap: HybridRnIapSpec {
1418
607
  updateListenerTask?.cancel()
1419
608
  updateListenerTask = nil
1420
609
  isInitialized = false
1421
- productStore = nil
1422
- transactions.removeAll()
1423
610
 
1424
- // Remove payment observer if exists
1425
- if let observer = paymentObserver {
1426
- SKPaymentQueue.default().remove(observer)
1427
- paymentObserver = nil
1428
- }
1429
-
1430
- // Clear promoted products
1431
- promotedProduct = nil
1432
- promotedPayment = nil
1433
- promotedSKProduct = nil
1434
611
 
612
+ // Remove OpenIAP listeners & end connection
613
+ if let sub = purchaseUpdatedSub { OpenIapModule.shared.removeListener(sub) }
614
+ if let sub = purchaseErrorSub { OpenIapModule.shared.removeListener(sub) }
615
+ if let sub = promotedProductSub { OpenIapModule.shared.removeListener(sub) }
616
+ purchaseUpdatedSub = nil
617
+ purchaseErrorSub = nil
618
+ promotedProductSub = nil
619
+ Task { _ = try? await OpenIapModule.shared.endConnection() }
620
+
1435
621
  // Clear event listeners
1436
622
  purchaseUpdatedListeners.removeAll()
1437
623
  purchaseErrorListeners.removeAll()
1438
624
  promotedProductListeners.removeAll()
1439
625
  }
1440
-
1441
- // Method called by PaymentObserver when a promoted product is received
1442
- func handlePromotedProduct(payment: SKPayment, product: SKProduct) {
1443
- self.promotedPayment = payment
1444
- self.promotedSKProduct = product
1445
-
1446
- // Convert SKProduct to StoreKit 2 Product and then to NitroProduct
1447
- Task {
1448
- if let products = try? await StoreKit.Product.products(for: [product.productIdentifier]),
1449
- let storeKit2Product = products.first {
1450
- self.promotedProduct = storeKit2Product
1451
-
1452
- // Notify all promoted product listeners
1453
- let nitroProduct = self.convertProductToNitroProduct(storeKit2Product, type: "inapp")
1454
- for listener in self.promotedProductListeners {
1455
- listener(nitroProduct)
1456
- }
1457
- }
1458
- }
1459
- }
1460
- }
1461
626
 
1462
- // PaymentObserver for handling promoted products
1463
- @available(iOS 15.0, *)
1464
- class PaymentObserver: NSObject, SKPaymentTransactionObserver {
1465
- weak var iapModule: HybridRnIap?
1466
-
1467
- init(iapModule: HybridRnIap) {
1468
- self.iapModule = iapModule
627
+ // MARK: - OpenIAP -> Nitro mappers
628
+ private func convertOpenIapProductToNitroProduct(_ p: OpenIapProduct) -> NitroProduct {
629
+ var n = NitroProduct()
630
+ n.id = p.id
631
+ n.title = p.title
632
+ n.description = p.description
633
+ n.type = p.type
634
+ n.displayName = p.displayName ?? p.displayNameIOS
635
+ n.displayPrice = p.displayPrice
636
+ n.currency = p.currency
637
+ n.price = p.price
638
+ n.platform = p.platform
639
+ // iOS specifics
640
+ n.typeIOS = p.typeIOS.rawValue
641
+ n.isFamilyShareableIOS = p.isFamilyShareableIOS
642
+ n.jsonRepresentationIOS = p.jsonRepresentationIOS
643
+ n.subscriptionPeriodUnitIOS = p.subscriptionPeriodUnitIOS
644
+ if let num = p.subscriptionPeriodNumberIOS, let d = Double(num) { n.subscriptionPeriodNumberIOS = d }
645
+ n.introductoryPriceIOS = p.introductoryPriceIOS
646
+ if let amt = p.introductoryPriceAsAmountIOS, let d = Double(amt) { n.introductoryPriceAsAmountIOS = d }
647
+ n.introductoryPricePaymentModeIOS = p.introductoryPricePaymentModeIOS
648
+ if let cnt = p.introductoryPriceNumberOfPeriodsIOS, let d = Double(cnt) { n.introductoryPriceNumberOfPeriodsIOS = d }
649
+ n.introductoryPriceSubscriptionPeriodIOS = p.introductoryPriceSubscriptionPeriodIOS
650
+ return n
1469
651
  }
1470
-
1471
- // Required by SKPaymentTransactionObserver protocol but not used
1472
- func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
1473
- // We don't handle transactions here as StoreKit 2 handles them in HybridRnIap
1474
- }
1475
-
1476
- // Handle promoted products from App Store
1477
- func paymentQueue(_ queue: SKPaymentQueue, shouldAddStorePayment payment: SKPayment, for product: SKProduct) -> Bool {
1478
- iapModule?.handlePromotedProduct(payment: payment, product: product)
1479
- // Return false to defer the payment
1480
- return false
652
+
653
+ private func convertOpenIapPurchaseToNitroPurchase(_ p: OpenIapPurchase) -> NitroPurchase {
654
+ var n = NitroPurchase()
655
+ n.id = p.id
656
+ n.productId = p.productId
657
+ n.transactionDate = p.transactionDate
658
+ n.purchaseToken = p.purchaseToken
659
+ n.platform = p.platform
660
+ n.quantity = Double(p.quantity)
661
+ n.purchaseState = p.purchaseState.rawValue
662
+ n.isAutoRenewing = p.isAutoRenewing
663
+ // iOS specifics
664
+ if let q = p.quantityIOS { n.quantityIOS = Double(q) }
665
+ n.originalTransactionDateIOS = p.originalTransactionDateIOS
666
+ n.originalTransactionIdentifierIOS = p.originalTransactionIdentifierIOS
667
+ n.appAccountToken = p.appAccountToken
668
+ return n
1481
669
  }
1482
670
  }