react-native-iap 14.6.0 → 14.6.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.
@@ -1222,7 +1222,9 @@ class HybridRnIap : HybridRnIapSpec() {
1222
1222
  val propsMap = mutableMapOf<String, Any?>("provider" to providerString)
1223
1223
  params.iapkit?.let { iapkit ->
1224
1224
  val iapkitMap = mutableMapOf<String, Any?>()
1225
- iapkit.apiKey?.let { iapkitMap["apiKey"] = it }
1225
+ // Use provided apiKey, or fallback to AndroidManifest meta-data (set by config plugin)
1226
+ val apiKey = iapkit.apiKey ?: getIapkitApiKeyFromManifest()
1227
+ apiKey?.let { iapkitMap["apiKey"] = it }
1226
1228
  iapkit.google?.let { google ->
1227
1229
  iapkitMap["google"] = mapOf("purchaseToken" to google.purchaseToken)
1228
1230
  }
@@ -1656,6 +1658,22 @@ class HybridRnIap : HybridRnIapSpec() {
1656
1658
  }
1657
1659
  }
1658
1660
 
1661
+ /**
1662
+ * Read IAPKit API key from AndroidManifest.xml meta-data (set by config plugin).
1663
+ * Config plugin sets: <meta-data android:name="dev.iapkit.API_KEY" android:value="..." />
1664
+ */
1665
+ private fun getIapkitApiKeyFromManifest(): String? {
1666
+ return try {
1667
+ val appInfo = context.packageManager.getApplicationInfo(
1668
+ context.packageName,
1669
+ android.content.pm.PackageManager.GET_META_DATA
1670
+ )
1671
+ appInfo.metaData?.getString("dev.iapkit.API_KEY")
1672
+ } catch (e: Exception) {
1673
+ null
1674
+ }
1675
+ }
1676
+
1659
1677
  private fun toErrorResult(
1660
1678
  error: OpenIAPError,
1661
1679
  productId: String? = null,
@@ -88,7 +88,7 @@ class HybridRnIap: HybridRnIapSpec {
88
88
  ])
89
89
 
90
90
  if skus.isEmpty {
91
- throw PurchaseError.make(code: .emptySkuList)
91
+ throw OpenIapException.make(code: .emptySkuList)
92
92
  }
93
93
 
94
94
  var productsById: [String: NitroProduct] = [:]
@@ -242,9 +242,12 @@ class HybridRnIap: HybridRnIapSpec {
242
242
  let payloads = RnIapHelper.sanitizeArray(OpenIapSerialization.purchases(purchases))
243
243
  RnIapLog.result("getAvailablePurchases", payloads)
244
244
  return payloads.map { RnIapHelper.convertPurchaseDictionary($0) }
245
+ } catch let purchaseError as PurchaseError {
246
+ RnIapLog.failure("getAvailablePurchases", error: purchaseError)
247
+ throw OpenIapException.from(purchaseError)
245
248
  } catch {
246
249
  RnIapLog.failure("getAvailablePurchases", error: error)
247
- throw error
250
+ throw OpenIapException.make(code: .serviceError, message: error.localizedDescription)
248
251
  }
249
252
  }
250
253
  }
@@ -259,9 +262,12 @@ class HybridRnIap: HybridRnIapSpec {
259
262
  let payloads = RnIapHelper.sanitizeArray(subscriptions.map { OpenIapSerialization.encode($0) })
260
263
  RnIapLog.result("getActiveSubscriptions", payloads)
261
264
  return payloads.map { RnIapHelper.convertActiveSubscriptionDictionary($0) }
265
+ } catch let purchaseError as PurchaseError {
266
+ RnIapLog.failure("getActiveSubscriptions", error: purchaseError)
267
+ throw OpenIapException.from(purchaseError)
262
268
  } catch {
263
269
  RnIapLog.failure("getActiveSubscriptions", error: error)
264
- throw error
270
+ throw OpenIapException.make(code: .serviceError, message: error.localizedDescription)
265
271
  }
266
272
  }
267
273
  }
@@ -274,9 +280,12 @@ class HybridRnIap: HybridRnIapSpec {
274
280
  let hasActive = try await OpenIapModule.shared.hasActiveSubscriptions(subscriptionIds)
275
281
  RnIapLog.result("hasActiveSubscriptions", hasActive)
276
282
  return hasActive
283
+ } catch let purchaseError as PurchaseError {
284
+ RnIapLog.failure("hasActiveSubscriptions", error: purchaseError)
285
+ throw OpenIapException.from(purchaseError)
277
286
  } catch {
278
287
  RnIapLog.failure("hasActiveSubscriptions", error: error)
279
- throw error
288
+ throw OpenIapException.make(code: .serviceError, message: error.localizedDescription)
280
289
  }
281
290
  }
282
291
  }
@@ -297,7 +306,7 @@ class HybridRnIap: HybridRnIapSpec {
297
306
  purchasePayload = ["transactionIdentifier": iosParams.transactionId]
298
307
  }
299
308
  guard let purchasePayload else {
300
- throw PurchaseError.make(code: .purchaseError, message: "Missing purchase context for \(iosParams.transactionId)")
309
+ throw OpenIapException.make(code: .purchaseError, message: "Missing purchase context for \(iosParams.transactionId)")
301
310
  }
302
311
  let sanitizedPayload = RnIapHelper.sanitizeDictionary(purchasePayload)
303
312
  RnIapLog.payload("finishTransaction.nativePayload", sanitizedPayload)
@@ -311,7 +320,7 @@ class HybridRnIap: HybridRnIapSpec {
311
320
  } catch {
312
321
  RnIapLog.failure("finishTransaction", error: error)
313
322
  let tid = iosParams.transactionId
314
- throw PurchaseError.make(code: .purchaseError, message: "Transaction not found: \(tid)")
323
+ throw OpenIapException.make(code: .purchaseError, message: "Transaction not found: \(tid)")
315
324
  }
316
325
  }
317
326
  }
@@ -321,7 +330,7 @@ class HybridRnIap: HybridRnIapSpec {
321
330
  do {
322
331
  // Extract SKU from apple options (new platform-specific structure)
323
332
  guard let appleOptions = params.apple, !appleOptions.sku.isEmpty else {
324
- throw PurchaseError.make(code: .developerError, message: "Missing required parameter: apple.sku")
333
+ throw OpenIapException.make(code: .developerError, message: "Missing required parameter: apple.sku")
325
334
  }
326
335
  let sku = appleOptions.sku
327
336
 
@@ -348,9 +357,12 @@ class HybridRnIap: HybridRnIapSpec {
348
357
  latestTransaction: latest
349
358
  )
350
359
  return .first(mapped)
360
+ } catch let purchaseError as PurchaseError {
361
+ RnIapLog.failure("validateReceiptIOS", error: purchaseError)
362
+ throw OpenIapException.from(purchaseError)
351
363
  } catch {
352
364
  RnIapLog.failure("validateReceiptIOS", error: error)
353
- throw PurchaseError.make(code: .receiptFailed, message: error.localizedDescription)
365
+ throw OpenIapException.make(code: .purchaseVerificationFailed, message: error.localizedDescription)
354
366
  }
355
367
  }
356
368
  }
@@ -364,8 +376,11 @@ class HybridRnIap: HybridRnIapSpec {
364
376
  var propsDict: [String: Any] = ["provider": params.provider.stringValue]
365
377
  if let iapkit = params.iapkit {
366
378
  var iapkitDict: [String: Any] = [:]
379
+ // Use provided apiKey, or fallback to Info.plist IAPKitAPIKey (set by config plugin)
367
380
  if let apiKey = iapkit.apiKey {
368
381
  iapkitDict["apiKey"] = apiKey
382
+ } else if let plistApiKey = Bundle.main.object(forInfoDictionaryKey: "IAPKitAPIKey") as? String {
383
+ iapkitDict["apiKey"] = plistApiKey
369
384
  }
370
385
  if let apple = iapkit.apple {
371
386
  iapkitDict["apple"] = ["jws": apple.jws]
@@ -404,9 +419,13 @@ class HybridRnIap: HybridRnIapSpec {
404
419
  errors: nitroErrors,
405
420
  provider: PurchaseVerificationProvider(fromString: result.provider.rawValue) ?? .iapkit
406
421
  )
422
+ } catch let purchaseError as PurchaseError {
423
+ // Convert PurchaseError to OpenIapException to preserve message through Nitro bridge
424
+ RnIapLog.failure("verifyPurchaseWithProvider", error: purchaseError)
425
+ throw OpenIapException.from(purchaseError)
407
426
  } catch {
408
427
  RnIapLog.failure("verifyPurchaseWithProvider", error: error)
409
- throw PurchaseError.make(code: .receiptFailed, message: error.localizedDescription)
428
+ throw OpenIapException.make(code: .purchaseVerificationFailed, message: error.localizedDescription)
410
429
  }
411
430
  }
412
431
  }
@@ -418,9 +437,12 @@ class HybridRnIap: HybridRnIapSpec {
418
437
  let storefront = try await OpenIapModule.shared.getStorefrontIOS()
419
438
  RnIapLog.result("getStorefront", storefront)
420
439
  return storefront
440
+ } catch let purchaseError as PurchaseError {
441
+ RnIapLog.failure("getStorefront", error: purchaseError)
442
+ throw OpenIapException.from(purchaseError)
421
443
  } catch {
422
444
  RnIapLog.failure("getStorefront", error: error)
423
- throw PurchaseError.make(code: .serviceError, message: error.localizedDescription)
445
+ throw OpenIapException.make(code: .serviceError, message: error.localizedDescription)
424
446
  }
425
447
  }
426
448
  }
@@ -481,9 +503,12 @@ class HybridRnIap: HybridRnIapSpec {
481
503
  let payload = RnIapHelper.sanitizeDictionary(OpenIapSerialization.encode(product))
482
504
  RnIapLog.result("getPromotedProductIOS", payload)
483
505
  return RnIapHelper.convertProductDictionary(payload)
506
+ } catch let purchaseError as PurchaseError {
507
+ RnIapLog.failure("getPromotedProductIOS", error: purchaseError)
508
+ throw OpenIapException.from(purchaseError)
484
509
  } catch {
485
510
  RnIapLog.failure("getPromotedProductIOS", error: error)
486
- throw PurchaseError.make(code: .serviceError, message: error.localizedDescription)
511
+ throw OpenIapException.make(code: .serviceError, message: error.localizedDescription)
487
512
  }
488
513
  }
489
514
  }
@@ -515,11 +540,11 @@ class HybridRnIap: HybridRnIapSpec {
515
540
  } catch {
516
541
  // Fallback with explicit error for simulator or unsupported cases
517
542
  RnIapLog.failure("presentCodeRedemptionSheetIOS", error: error)
518
- throw PurchaseError.make(code: .featureNotSupported)
543
+ throw OpenIapException.make(code: .featureNotSupported)
519
544
  }
520
545
  }
521
546
  }
522
-
547
+
523
548
  func clearTransactionIOS() throws -> Promise<Void> {
524
549
  return Promise.async {
525
550
  do {
@@ -587,11 +612,11 @@ class HybridRnIap: HybridRnIapSpec {
587
612
  return Optional<NitroPurchase>.none
588
613
  } catch {
589
614
  RnIapLog.failure("currentEntitlementIOS", error: error)
590
- throw PurchaseError.make(code: .skuNotFound, productId: sku)
615
+ throw OpenIapException.make(code: .skuNotFound, productId: sku)
591
616
  }
592
617
  }
593
618
  }
594
-
619
+
595
620
  func latestTransactionIOS(sku: String) throws -> Promise<NitroPurchase?> {
596
621
  return Promise.async {
597
622
  try self.ensureConnection()
@@ -613,11 +638,11 @@ class HybridRnIap: HybridRnIapSpec {
613
638
  return Optional<NitroPurchase>.none
614
639
  } catch {
615
640
  RnIapLog.failure("latestTransactionIOS", error: error)
616
- throw PurchaseError.make(code: .skuNotFound, productId: sku)
641
+ throw OpenIapException.make(code: .skuNotFound, productId: sku)
617
642
  }
618
643
  }
619
644
  }
620
-
645
+
621
646
  func getPendingTransactionsIOS() throws -> Promise<[NitroPurchase]> {
622
647
  return Promise.async {
623
648
  do {
@@ -651,13 +676,16 @@ class HybridRnIap: HybridRnIapSpec {
651
676
  let ok = try await OpenIapModule.shared.syncIOS()
652
677
  RnIapLog.result("syncIOS", ok)
653
678
  return ok
679
+ } catch let purchaseError as PurchaseError {
680
+ RnIapLog.failure("syncIOS", error: purchaseError)
681
+ throw OpenIapException.from(purchaseError)
654
682
  } catch {
655
683
  RnIapLog.failure("syncIOS", error: error)
656
- throw PurchaseError.make(code: .serviceError, message: error.localizedDescription)
684
+ throw OpenIapException.make(code: .serviceError, message: error.localizedDescription)
657
685
  }
658
686
  }
659
687
  }
660
-
688
+
661
689
  func showManageSubscriptionsIOS() throws -> Promise<[NitroPurchase]> {
662
690
  return Promise.async {
663
691
  try self.ensureConnection()
@@ -675,9 +703,12 @@ class HybridRnIap: HybridRnIapSpec {
675
703
  let payloads = RnIapHelper.sanitizeArray(OpenIapSerialization.purchases(purchases))
676
704
  RnIapLog.result("showManageSubscriptionsIOS", payloads)
677
705
  return payloads.map { RnIapHelper.convertPurchaseDictionary($0) }
706
+ } catch let purchaseError as PurchaseError {
707
+ RnIapLog.failure("showManageSubscriptionsIOS", error: purchaseError)
708
+ throw OpenIapException.from(purchaseError)
678
709
  } catch {
679
710
  RnIapLog.failure("showManageSubscriptionsIOS", error: error)
680
- throw PurchaseError.make(code: .serviceError, message: error.localizedDescription)
711
+ throw OpenIapException.make(code: .serviceError, message: error.localizedDescription)
681
712
  }
682
713
  }
683
714
  }
@@ -690,13 +721,16 @@ class HybridRnIap: HybridRnIapSpec {
690
721
  try await OpenIapModule.shared.deepLinkToSubscriptions(nil)
691
722
  RnIapLog.result("deepLinkToSubscriptionsIOS", true)
692
723
  return true
724
+ } catch let purchaseError as PurchaseError {
725
+ RnIapLog.failure("deepLinkToSubscriptionsIOS", error: purchaseError)
726
+ throw OpenIapException.from(purchaseError)
693
727
  } catch {
694
728
  RnIapLog.failure("deepLinkToSubscriptionsIOS", error: error)
695
- throw PurchaseError.make(code: .serviceError, message: error.localizedDescription)
729
+ throw OpenIapException.make(code: .serviceError, message: error.localizedDescription)
696
730
  }
697
731
  }
698
732
  }
699
-
733
+
700
734
  func isEligibleForIntroOfferIOS(groupID: String) throws -> Promise<Bool> {
701
735
  return Promise.async {
702
736
  RnIapLog.payload("isEligibleForIntroOfferIOS", ["groupID": groupID])
@@ -716,10 +750,10 @@ class HybridRnIap: HybridRnIapSpec {
716
750
  return receipt
717
751
  } catch let purchaseError as PurchaseError {
718
752
  RnIapLog.failure("getReceiptDataIOS", error: purchaseError)
719
- throw purchaseError
753
+ throw OpenIapException.from(purchaseError)
720
754
  } catch {
721
755
  RnIapLog.failure("getReceiptDataIOS", error: error)
722
- throw PurchaseError.make(code: .receiptFailed, message: error.localizedDescription)
756
+ throw OpenIapException.make(code: .receiptFailed, message: error.localizedDescription)
723
757
  }
724
758
  }
725
759
  }
@@ -734,10 +768,10 @@ class HybridRnIap: HybridRnIapSpec {
734
768
  return receipt
735
769
  } catch let purchaseError as PurchaseError {
736
770
  RnIapLog.failure("getReceiptIOS", error: purchaseError)
737
- throw purchaseError
771
+ throw OpenIapException.from(purchaseError)
738
772
  } catch {
739
773
  RnIapLog.failure("getReceiptIOS", error: error)
740
- throw PurchaseError.make(code: .receiptFailed, message: error.localizedDescription)
774
+ throw OpenIapException.make(code: .receiptFailed, message: error.localizedDescription)
741
775
  }
742
776
  }
743
777
  }
@@ -752,14 +786,14 @@ class HybridRnIap: HybridRnIapSpec {
752
786
  return receipt
753
787
  } catch let purchaseError as PurchaseError {
754
788
  RnIapLog.failure("requestReceiptRefreshIOS", error: purchaseError)
755
- throw purchaseError
789
+ throw OpenIapException.from(purchaseError)
756
790
  } catch {
757
791
  RnIapLog.failure("requestReceiptRefreshIOS", error: error)
758
- throw PurchaseError.make(code: .receiptFailed, message: error.localizedDescription)
792
+ throw OpenIapException.make(code: .receiptFailed, message: error.localizedDescription)
759
793
  }
760
794
  }
761
795
  }
762
-
796
+
763
797
  func isTransactionVerifiedIOS(sku: String) throws -> Promise<Bool> {
764
798
  return Promise.async {
765
799
  try self.ensureConnection()
@@ -781,11 +815,11 @@ class HybridRnIap: HybridRnIapSpec {
781
815
  return jws
782
816
  } catch {
783
817
  RnIapLog.failure("getTransactionJwsIOS", error: error)
784
- throw PurchaseError.make(code: .transactionValidationFailed, message: "Can't find transaction for sku \(sku)")
818
+ throw OpenIapException.make(code: .transactionValidationFailed, message: "Can't find transaction for sku \(sku)")
785
819
  }
786
820
  }
787
821
  }
788
-
822
+
789
823
  func beginRefundRequestIOS(sku: String) throws -> Promise<String?> {
790
824
  return Promise.async {
791
825
  do {
@@ -919,7 +953,7 @@ class HybridRnIap: HybridRnIapSpec {
919
953
 
920
954
  private func ensureConnection() throws {
921
955
  guard isInitialized else {
922
- throw PurchaseError.make(code: .initConnection, message: "Connection not initialized. Call initConnection() first.")
956
+ throw OpenIapException.make(code: .initConnection, message: "Connection not initialized. Call initConnection() first.")
923
957
  }
924
958
  }
925
959
 
@@ -1021,7 +1055,7 @@ class HybridRnIap: HybridRnIapSpec {
1021
1055
 
1022
1056
  func deepLinkToSubscriptionsAndroid(options: NitroDeepLinkOptionsAndroid) throws -> Promise<Void> {
1023
1057
  return Promise.async {
1024
- throw PurchaseError.make(code: .featureNotSupported)
1058
+ throw OpenIapException.make(code: .featureNotSupported)
1025
1059
  }
1026
1060
  }
1027
1061
 
@@ -1029,19 +1063,19 @@ class HybridRnIap: HybridRnIapSpec {
1029
1063
 
1030
1064
  func checkAlternativeBillingAvailabilityAndroid() throws -> Promise<Bool> {
1031
1065
  return Promise.async {
1032
- throw PurchaseError.make(code: .featureNotSupported)
1066
+ throw OpenIapException.make(code: .featureNotSupported)
1033
1067
  }
1034
1068
  }
1035
1069
 
1036
1070
  func showAlternativeBillingDialogAndroid() throws -> Promise<Bool> {
1037
1071
  return Promise.async {
1038
- throw PurchaseError.make(code: .featureNotSupported)
1072
+ throw OpenIapException.make(code: .featureNotSupported)
1039
1073
  }
1040
1074
  }
1041
1075
 
1042
1076
  func createAlternativeBillingTokenAndroid(sku: String?) throws -> Promise<String?> {
1043
1077
  return Promise.async {
1044
- throw PurchaseError.make(code: .featureNotSupported)
1078
+ throw OpenIapException.make(code: .featureNotSupported)
1045
1079
  }
1046
1080
  }
1047
1081
 
@@ -1061,19 +1095,19 @@ class HybridRnIap: HybridRnIapSpec {
1061
1095
 
1062
1096
  func isBillingProgramAvailableAndroid(program: BillingProgramAndroid) throws -> Promise<NitroBillingProgramAvailabilityResultAndroid> {
1063
1097
  return Promise.async {
1064
- throw PurchaseError.make(code: .featureNotSupported, message: "Billing Programs API is Android-only")
1098
+ throw OpenIapException.make(code: .featureNotSupported, message: "Billing Programs API is Android-only")
1065
1099
  }
1066
1100
  }
1067
1101
 
1068
1102
  func createBillingProgramReportingDetailsAndroid(program: BillingProgramAndroid) throws -> Promise<NitroBillingProgramReportingDetailsAndroid> {
1069
1103
  return Promise.async {
1070
- throw PurchaseError.make(code: .featureNotSupported, message: "Billing Programs API is Android-only")
1104
+ throw OpenIapException.make(code: .featureNotSupported, message: "Billing Programs API is Android-only")
1071
1105
  }
1072
1106
  }
1073
1107
 
1074
1108
  func launchExternalLinkAndroid(params: NitroLaunchExternalLinkParamsAndroid) throws -> Promise<Bool> {
1075
1109
  return Promise.async {
1076
- throw PurchaseError.make(code: .featureNotSupported, message: "Billing Programs API is Android-only")
1110
+ throw OpenIapException.make(code: .featureNotSupported, message: "Billing Programs API is Android-only")
1077
1111
  }
1078
1112
  }
1079
1113
 
@@ -1089,12 +1123,15 @@ class HybridRnIap: HybridRnIapSpec {
1089
1123
  let canPresent = try await OpenIapModule.shared.canPresentExternalPurchaseNoticeIOS()
1090
1124
  RnIapLog.result("canPresentExternalPurchaseNoticeIOS", canPresent)
1091
1125
  return canPresent
1126
+ } catch let purchaseError as PurchaseError {
1127
+ RnIapLog.failure("canPresentExternalPurchaseNoticeIOS", error: purchaseError)
1128
+ throw OpenIapException.from(purchaseError)
1092
1129
  } catch {
1093
1130
  RnIapLog.failure("canPresentExternalPurchaseNoticeIOS", error: error)
1094
- throw PurchaseError.make(code: .serviceError, message: error.localizedDescription)
1131
+ throw OpenIapException.make(code: .serviceError, message: error.localizedDescription)
1095
1132
  }
1096
1133
  } else {
1097
- let err = PurchaseError.make(code: .featureNotSupported, message: "External purchase notice requires iOS 16.0 or later")
1134
+ let err = OpenIapException.make(code: .featureNotSupported, message: "External purchase notice requires iOS 16.0 or later")
1098
1135
  RnIapLog.failure("canPresentExternalPurchaseNoticeIOS", error: err)
1099
1136
  throw err
1100
1137
  }
@@ -1113,7 +1150,7 @@ class HybridRnIap: HybridRnIapSpec {
1113
1150
  // Convert OpenIAP action to Nitro action via raw value
1114
1151
  let actionString = result.result.rawValue
1115
1152
  guard let nitroAction = ExternalPurchaseNoticeAction(fromString: actionString) else {
1116
- throw PurchaseError.make(code: .serviceError, message: "Invalid action: \(actionString)")
1153
+ throw OpenIapException.make(code: .serviceError, message: "Invalid action: \(actionString)")
1117
1154
  }
1118
1155
 
1119
1156
  let nitroResult = ExternalPurchaseNoticeResultIOS(
@@ -1122,12 +1159,15 @@ class HybridRnIap: HybridRnIapSpec {
1122
1159
  )
1123
1160
  RnIapLog.result("presentExternalPurchaseNoticeSheetIOS", result)
1124
1161
  return nitroResult
1162
+ } catch let purchaseError as PurchaseError {
1163
+ RnIapLog.failure("presentExternalPurchaseNoticeSheetIOS", error: purchaseError)
1164
+ throw OpenIapException.from(purchaseError)
1125
1165
  } catch {
1126
1166
  RnIapLog.failure("presentExternalPurchaseNoticeSheetIOS", error: error)
1127
- throw PurchaseError.make(code: .serviceError, message: error.localizedDescription)
1167
+ throw OpenIapException.make(code: .serviceError, message: error.localizedDescription)
1128
1168
  }
1129
1169
  } else {
1130
- let err = PurchaseError.make(code: .featureNotSupported, message: "External purchase notice requires iOS 16.0 or later")
1170
+ let err = OpenIapException.make(code: .featureNotSupported, message: "External purchase notice requires iOS 16.0 or later")
1131
1171
  RnIapLog.failure("presentExternalPurchaseNoticeSheetIOS", error: err)
1132
1172
  throw err
1133
1173
  }
@@ -1148,12 +1188,15 @@ class HybridRnIap: HybridRnIapSpec {
1148
1188
  )
1149
1189
  RnIapLog.result("presentExternalPurchaseLinkIOS", result)
1150
1190
  return nitroResult
1191
+ } catch let purchaseError as PurchaseError {
1192
+ RnIapLog.failure("presentExternalPurchaseLinkIOS", error: purchaseError)
1193
+ throw OpenIapException.from(purchaseError)
1151
1194
  } catch {
1152
1195
  RnIapLog.failure("presentExternalPurchaseLinkIOS", error: error)
1153
- throw PurchaseError.make(code: .serviceError, message: error.localizedDescription)
1196
+ throw OpenIapException.make(code: .serviceError, message: error.localizedDescription)
1154
1197
  }
1155
1198
  } else {
1156
- let err = PurchaseError.make(code: .featureNotSupported, message: "External purchase link requires iOS 16.0 or later")
1199
+ let err = OpenIapException.make(code: .featureNotSupported, message: "External purchase link requires iOS 16.0 or later")
1157
1200
  RnIapLog.failure("presentExternalPurchaseLinkIOS", error: err)
1158
1201
  throw err
1159
1202
  }
@@ -1,6 +1,39 @@
1
1
  import Foundation
2
2
  import OpenIAP
3
3
 
4
+ /// Custom error that preserves error messages through Nitro bridge.
5
+ /// Similar to Android's OpenIapException, this wraps errors with JSON-serialized messages.
6
+ /// Uses NSError for better compatibility with Objective-C bridging in Nitro.
7
+ @available(iOS 15.0, *)
8
+ class OpenIapException: NSError {
9
+ static let domain = "com.margelo.nitro.rniap"
10
+
11
+ convenience init(_ json: String) {
12
+ self.init(domain: OpenIapException.domain, code: -1, userInfo: [NSLocalizedDescriptionKey: json])
13
+ }
14
+
15
+ static func make(code: ErrorCode, message: String? = nil, productId: String? = nil) -> OpenIapException {
16
+ let errorMessage = message ?? code.rawValue
17
+ var dict: [String: Any] = [
18
+ "code": code.rawValue,
19
+ "message": errorMessage
20
+ ]
21
+ if let productId = productId {
22
+ dict["productId"] = productId
23
+ }
24
+
25
+ if let data = try? JSONSerialization.data(withJSONObject: dict),
26
+ let json = String(data: data, encoding: .utf8) {
27
+ return OpenIapException(json)
28
+ }
29
+ return OpenIapException("{\"code\":\"\(code.rawValue)\",\"message\":\"\(errorMessage)\"}")
30
+ }
31
+
32
+ static func from(_ error: PurchaseError) -> OpenIapException {
33
+ return make(code: error.code, message: error.message, productId: error.productId)
34
+ }
35
+ }
36
+
4
37
  @available(iOS 15.0, *)
5
38
  enum RnIapHelper {
6
39
  // MARK: - Sanitizers
@@ -272,13 +305,13 @@ enum RnIapHelper {
272
305
 
273
306
  do {
274
307
  guard let receipt = try await OpenIapModule.shared.getReceiptDataIOS(), !receipt.isEmpty else {
275
- throw PurchaseError.make(code: .receiptFailed)
308
+ throw OpenIapException.make(code: .receiptFailed)
276
309
  }
277
310
  return receipt
278
311
  } catch let error as PurchaseError {
279
- throw error
312
+ throw OpenIapException.from(error)
280
313
  } catch {
281
- throw PurchaseError.make(code: .receiptFailed, message: error.localizedDescription)
314
+ throw OpenIapException.make(code: .receiptFailed, message: error.localizedDescription)
282
315
  }
283
316
  }
284
317
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-iap",
3
- "version": "14.6.0",
3
+ "version": "14.6.2",
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",