react-native-iap 14.2.0 → 14.2.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/NitroIap.podspec CHANGED
@@ -29,7 +29,7 @@ Pod::Spec.new do |s|
29
29
  s.dependency 'React-jsi'
30
30
  s.dependency 'React-callinvoker'
31
31
  # OpenIAP Apple for StoreKit 2 integration
32
- s.dependency 'openiap', '~> 1.1.8'
32
+ s.dependency 'openiap', '~> 1.1.9'
33
33
 
34
34
  install_modules_dependencies(s)
35
35
  end
@@ -21,6 +21,10 @@ class HybridRnIap: HybridRnIapSpec {
21
21
  private var purchaseUpdatedListeners: [(NitroPurchase) -> Void] = []
22
22
  private var purchaseErrorListeners: [(NitroPurchaseResult) -> Void] = []
23
23
  private var promotedProductListeners: [(NitroProduct) -> Void] = []
24
+ // Deduplication for purchase error events
25
+ private var lastPurchaseErrorCode: String? = nil
26
+ private var lastPurchaseErrorProductId: String? = nil
27
+ private var lastPurchaseErrorTimestamp: TimeInterval = 0
24
28
 
25
29
  // MARK: - Initialization
26
30
 
@@ -53,6 +57,9 @@ class HybridRnIap: HybridRnIapSpec {
53
57
  self.purchaseErrorSub = OpenIapModule.shared.purchaseErrorListener { [weak self] error in
54
58
  guard let self else { return }
55
59
  Task { @MainActor in
60
+ #if DEBUG
61
+ print("[HybridRnIap] purchaseError event: code=\(error.code), productId=\(error.productId ?? "-")")
62
+ #endif
56
63
  let nitroError = self.createPurchaseErrorResult(
57
64
  code: error.code,
58
65
  message: error.message,
@@ -108,6 +115,9 @@ class HybridRnIap: HybridRnIapSpec {
108
115
  self.purchaseErrorSub = OpenIapModule.shared.purchaseErrorListener { [weak self] error in
109
116
  guard let self else { return }
110
117
  Task { @MainActor in
118
+ #if DEBUG
119
+ print("[HybridRnIap] purchaseError event: code=\(error.code), productId=\(error.productId ?? "-")")
120
+ #endif
111
121
  let nitroError = self.createPurchaseErrorResult(
112
122
  code: error.code,
113
123
  message: error.message,
@@ -182,7 +192,8 @@ class HybridRnIap: HybridRnIapSpec {
182
192
  let products = try await OpenIapModule.shared.fetchProducts(req)
183
193
  return products.map { self.convertOpenIapProductToNitroProduct($0) }
184
194
  } catch {
185
- throw OpenIapFailure.storeKitError(error: error)
195
+ // Propagate OpenIAP error
196
+ throw error
186
197
  }
187
198
  }
188
199
  }
@@ -198,7 +209,19 @@ class HybridRnIap: HybridRnIapSpec {
198
209
  return
199
210
  }
200
211
  do {
201
- try self.ensureConnection()
212
+ // Event-first behavior: don't reject Promise on connection issues
213
+ guard self.isInitialized else {
214
+ #if DEBUG
215
+ print("[HybridRnIap] requestPurchase while not initialized; sending E_INIT_CONNECTION")
216
+ #endif
217
+ let err = self.createPurchaseErrorResult(
218
+ code: OpenIapError.E_INIT_CONNECTION,
219
+ message: "IAP store connection not initialized",
220
+ productId: iosRequest.sku
221
+ )
222
+ self.sendPurchaseError(err)
223
+ return
224
+ }
202
225
  // Delegate purchase to OpenIAP. It emits success/error events which we bridge above.
203
226
  let props = OpenIapRequestPurchaseProps(
204
227
  sku: iosRequest.sku,
@@ -216,13 +239,14 @@ class HybridRnIap: HybridRnIapSpec {
216
239
  )
217
240
  _ = try await OpenIapModule.shared.requestPurchase(props)
218
241
  } catch {
219
- // OpenIAP will emit error event; also forward here for safety
242
+ // Ensure an error reaches JS even if OpenIAP threw before emitting.
243
+ // Use simple de-duplication window to avoid double-emitting.
220
244
  let err = self.createPurchaseErrorResult(
221
245
  code: OpenIapError.E_SERVICE_ERROR,
222
246
  message: error.localizedDescription,
223
247
  productId: iosRequest.sku
224
248
  )
225
- self.sendPurchaseError(err)
249
+ self.sendPurchaseErrorDedup(err)
226
250
  }
227
251
  }
228
252
  }
@@ -236,7 +260,8 @@ class HybridRnIap: HybridRnIapSpec {
236
260
  let purchases = try await OpenIapModule.shared.getAvailablePurchases(props)
237
261
  return purchases.map { self.convertOpenIapPurchaseToNitroPurchase($0) }
238
262
  } catch {
239
- throw OpenIapFailure.networkError
263
+ // Propagate OpenIAP error or map to network error
264
+ throw OpenIapError.make(code: OpenIapError.E_NETWORK_ERROR)
240
265
  }
241
266
  }
242
267
  }
@@ -250,7 +275,7 @@ class HybridRnIap: HybridRnIapSpec {
250
275
  return .first(ok)
251
276
  } catch {
252
277
  let tid = iosParams.transactionId
253
- throw OpenIapFailure.verificationFailed(reason: "Transaction not found: \(tid)")
278
+ throw OpenIapError.make(code: OpenIapError.E_PURCHASE_ERROR, message: "Transaction not found: \(tid)")
254
279
  }
255
280
  }
256
281
  }
@@ -271,7 +296,7 @@ class HybridRnIap: HybridRnIapSpec {
271
296
  )
272
297
  return .first(mapped)
273
298
  } catch {
274
- throw OpenIapFailure.invalidReceipt
299
+ throw OpenIapError.make(code: OpenIapError.E_RECEIPT_FAILED, message: error.localizedDescription)
275
300
  }
276
301
  }
277
302
  }
@@ -283,7 +308,7 @@ class HybridRnIap: HybridRnIapSpec {
283
308
  do {
284
309
  return try await OpenIapModule.shared.getStorefrontIOS()
285
310
  } catch {
286
- throw OpenIapFailure.storeKitError(error: error)
311
+ throw OpenIapError.make(code: OpenIapError.E_SERVICE_ERROR, message: error.localizedDescription)
287
312
  }
288
313
  }
289
314
  }
@@ -347,7 +372,7 @@ class HybridRnIap: HybridRnIapSpec {
347
372
  do {
348
373
  try await OpenIapModule.shared.requestPurchaseOnPromotedProductIOS()
349
374
  } catch {
350
- throw OpenIapFailure.productNotFound(id: "promoted-product")
375
+ // Event-only: OpenIAP will emit purchaseError for this flow. Avoid Promise rejection.
351
376
  }
352
377
  }
353
378
  }
@@ -359,7 +384,7 @@ class HybridRnIap: HybridRnIapSpec {
359
384
  return ok
360
385
  } catch {
361
386
  // Fallback with explicit error for simulator or unsupported cases
362
- throw OpenIapFailure.notSupported
387
+ throw OpenIapError.make(code: OpenIapError.E_FEATURE_NOT_SUPPORTED)
363
388
  }
364
389
  }
365
390
  }
@@ -423,7 +448,7 @@ class HybridRnIap: HybridRnIapSpec {
423
448
  }
424
449
  return nil
425
450
  } catch {
426
- throw OpenIapFailure.productNotFound(id: sku)
451
+ throw OpenIapError.make(code: OpenIapError.E_SKU_NOT_FOUND, productId: sku)
427
452
  }
428
453
  }
429
454
  }
@@ -437,7 +462,7 @@ class HybridRnIap: HybridRnIapSpec {
437
462
  }
438
463
  return nil
439
464
  } catch {
440
- throw OpenIapFailure.productNotFound(id: sku)
465
+ throw OpenIapError.make(code: OpenIapError.E_SKU_NOT_FOUND, productId: sku)
441
466
  }
442
467
  }
443
468
  }
@@ -459,7 +484,7 @@ class HybridRnIap: HybridRnIapSpec {
459
484
  let ok = try await OpenIapModule.shared.syncIOS()
460
485
  return ok
461
486
  } catch {
462
- throw OpenIapFailure.networkError
487
+ throw OpenIapError.make(code: OpenIapError.E_SERVICE_ERROR, message: error.localizedDescription)
463
488
  }
464
489
  }
465
490
  }
@@ -473,7 +498,7 @@ class HybridRnIap: HybridRnIapSpec {
473
498
  let purchases = try await OpenIapModule.shared.getAvailablePurchases(OpenIapPurchaseOptions(alsoPublishToEventListenerIOS: false, onlyIncludeActiveItemsIOS: true))
474
499
  return purchases.map { self.convertOpenIapPurchaseToNitroPurchase($0) }
475
500
  } catch {
476
- throw OpenIapFailure.storeKitError(error: error)
501
+ throw OpenIapError.make(code: OpenIapError.E_SERVICE_ERROR, message: error.localizedDescription)
477
502
  }
478
503
  }
479
504
  }
@@ -490,9 +515,9 @@ class HybridRnIap: HybridRnIapSpec {
490
515
  if let receipt = try await OpenIapModule.shared.getReceiptDataIOS() {
491
516
  return receipt
492
517
  }
493
- throw OpenIapFailure.invalidReceipt
518
+ throw OpenIapError.make(code: OpenIapError.E_RECEIPT_FAILED)
494
519
  } catch {
495
- throw OpenIapFailure.invalidReceipt
520
+ throw OpenIapError.make(code: OpenIapError.E_RECEIPT_FAILED, message: error.localizedDescription)
496
521
  }
497
522
  }
498
523
  }
@@ -508,7 +533,7 @@ class HybridRnIap: HybridRnIapSpec {
508
533
  return Promise.async {
509
534
  try self.ensureConnection()
510
535
  do { return try await OpenIapModule.shared.getTransactionJwsIOS(sku: sku) } catch {
511
- throw OpenIapFailure.verificationFailed(reason: "Can't find transaction for sku \(sku)")
536
+ throw OpenIapError.make(code: OpenIapError.E_TRANSACTION_VALIDATION_FAILED, message: "Can't find transaction for sku \(sku)")
512
537
  }
513
538
  }
514
539
  }
@@ -577,7 +602,7 @@ class HybridRnIap: HybridRnIapSpec {
577
602
 
578
603
  private func ensureConnection() throws {
579
604
  guard isInitialized else {
580
- throw OpenIapFailure.restoreFailed(reason: "Connection not initialized. Call initConnection() first.")
605
+ throw OpenIapError.make(code: OpenIapError.E_INIT_CONNECTION, message: "Connection not initialized. Call initConnection() first.")
581
606
  }
582
607
  }
583
608
 
@@ -588,10 +613,25 @@ class HybridRnIap: HybridRnIapSpec {
588
613
  }
589
614
 
590
615
  private func sendPurchaseError(_ error: NitroPurchaseResult) {
616
+ // Update last error for deduplication
617
+ lastPurchaseErrorCode = error.code
618
+ lastPurchaseErrorProductId = error.purchaseToken
619
+ lastPurchaseErrorTimestamp = Date().timeIntervalSince1970
591
620
  for listener in purchaseErrorListeners {
592
621
  listener(error)
593
622
  }
594
623
  }
624
+
625
+ private func sendPurchaseErrorDedup(_ error: NitroPurchaseResult) {
626
+ let now = Date().timeIntervalSince1970
627
+ let sameCode = (error.code == lastPurchaseErrorCode)
628
+ let sameProduct = (error.purchaseToken == lastPurchaseErrorProductId)
629
+ let withinWindow = (now - lastPurchaseErrorTimestamp) < 0.3
630
+ if sameCode && sameProduct && withinWindow {
631
+ return
632
+ }
633
+ sendPurchaseError(error)
634
+ }
595
635
 
596
636
  private func createPurchaseErrorResult(code: String, message: String, productId: String? = nil) -> NitroPurchaseResult {
597
637
  var result = NitroPurchaseResult()
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-iap",
3
- "version": "14.2.0",
3
+ "version": "14.2.1",
4
4
  "description": "React Native In-App Purchases module for iOS and Android using Nitro",
5
5
  "main": "./lib/module/index.js",
6
6
  "types": "./lib/typescript/src/index.d.ts",