expo-iap 2.4.4 → 2.4.5

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.
@@ -186,7 +186,32 @@ public class ExpoIapModule: Module {
186
186
  Name("ExpoIap")
187
187
 
188
188
  Constants([
189
- "PI": Double.pi
189
+ "ERROR_CODES": [
190
+ "E_UNKNOWN": IapErrorCode.unknown,
191
+ "E_SERVICE_ERROR": IapErrorCode.serviceError,
192
+ "E_USER_CANCELLED": IapErrorCode.userCancelled,
193
+ "E_USER_ERROR": IapErrorCode.userError,
194
+ "E_ITEM_UNAVAILABLE": IapErrorCode.itemUnavailable,
195
+ "E_REMOTE_ERROR": IapErrorCode.remoteError,
196
+ "E_NETWORK_ERROR": IapErrorCode.networkError,
197
+ "E_RECEIPT_FAILED": IapErrorCode.receiptFailed,
198
+ "E_RECEIPT_FINISHED_FAILED": IapErrorCode.receiptFinishedFailed,
199
+ "E_NOT_PREPARED": IapErrorCode.notPrepared,
200
+ "E_NOT_ENDED": IapErrorCode.notEnded,
201
+ "E_ALREADY_OWNED": IapErrorCode.alreadyOwned,
202
+ "E_DEVELOPER_ERROR": IapErrorCode.developerError,
203
+ "E_PURCHASE_ERROR": IapErrorCode.purchaseError,
204
+ "E_SYNC_ERROR": IapErrorCode.syncError,
205
+ "E_DEFERRED_PAYMENT": IapErrorCode.deferredPayment,
206
+ "E_TRANSACTION_VALIDATION_FAILED": IapErrorCode.transactionValidationFailed,
207
+ "E_BILLING_RESPONSE_JSON_PARSE_ERROR": IapErrorCode.billingResponseJsonParseError,
208
+ "E_INTERRUPTED": IapErrorCode.interrupted,
209
+ "E_IAP_NOT_AVAILABLE": IapErrorCode.iapNotAvailable,
210
+ "E_ACTIVITY_UNAVAILABLE": IapErrorCode.activityUnavailable,
211
+ "E_ALREADY_PREPARED": IapErrorCode.alreadyPrepared,
212
+ "E_PENDING": IapErrorCode.pending,
213
+ "E_CONNECTION_CLOSED": IapErrorCode.connectionClosed,
214
+ ]
190
215
  ])
191
216
 
192
217
  Events(IapEvent.PurchaseUpdated, IapEvent.PurchaseError)
@@ -208,7 +233,7 @@ public class ExpoIapModule: Module {
208
233
 
209
234
  AsyncFunction("getItems") { (skus: [String]) -> [[String: Any?]?] in
210
235
  guard let productStore = self.productStore else {
211
- throw Exception(name: "ExpoIapModule", description: "Connection not initialized", code: "1")
236
+ throw Exception(name: "ExpoIapModule", description: "Connection not initialized", code: IapErrorCode.notPrepared)
212
237
  }
213
238
 
214
239
  do {
@@ -294,9 +319,9 @@ public class ExpoIapModule: Module {
294
319
  }
295
320
  } catch StoreError.failedVerification {
296
321
  let err = [
297
- "responseCode": IapErrors.E_TRANSACTION_VALIDATION_FAILED.rawValue,
322
+ "responseCode": IapErrorCode.transactionValidationFailed,
298
323
  "debugMessage": StoreError.failedVerification.localizedDescription,
299
- "code": IapErrors.E_TRANSACTION_VALIDATION_FAILED.rawValue,
324
+ "code": IapErrorCode.transactionValidationFailed,
300
325
  "message": StoreError.failedVerification.localizedDescription,
301
326
  "productId": "unknown",
302
327
  ]
@@ -305,9 +330,9 @@ public class ExpoIapModule: Module {
305
330
  }
306
331
  } catch {
307
332
  let err = [
308
- "responseCode": IapErrors.E_UNKNOWN.rawValue,
333
+ "responseCode": IapErrorCode.unknown,
309
334
  "debugMessage": error.localizedDescription,
310
- "code": IapErrors.E_UNKNOWN.rawValue,
335
+ "code": IapErrorCode.unknown,
311
336
  "message": error.localizedDescription,
312
337
  "productId": "unknown",
313
338
  ]
@@ -325,7 +350,7 @@ public class ExpoIapModule: Module {
325
350
  appAccountToken: String?, quantity: Int, discountOffer: [String: String]?
326
351
  ) -> [String: Any?]? in
327
352
  guard let productStore = self.productStore else {
328
- throw Exception(name: "ExpoIapModule", description: "Connection not initialized", code: "1")
353
+ throw Exception(name: "ExpoIapModule", description: "Connection not initialized", code: IapErrorCode.serviceError)
329
354
  }
330
355
 
331
356
  let product: Product? = await productStore.getProduct(productID: sku)
@@ -355,7 +380,7 @@ public class ExpoIapModule: Module {
355
380
  options.insert(.appAccountToken(appAccountUUID))
356
381
  }
357
382
  guard let windowScene = await self.currentWindowScene() else {
358
- throw Exception(name: "ExpoIapModule", description: "Could not find window scene", code: "2")
383
+ throw Exception(name: "ExpoIapModule", description: "Could not find window scene", code: IapErrorCode.serviceError)
359
384
  }
360
385
  let result: Product.PurchaseResult
361
386
  #if swift(>=5.9)
@@ -398,20 +423,20 @@ public class ExpoIapModule: Module {
398
423
  return serialized
399
424
  }
400
425
  case .userCancelled:
401
- throw Exception(name: "ExpoIapModule", description: "User cancelled the purchase", code: "3")
426
+ throw Exception(name: "ExpoIapModule", description: "User cancelled the purchase", code: IapErrorCode.userCancelled)
402
427
  case .pending:
403
- throw Exception(name: "ExpoIapModule", description: "The payment was deferred", code: "4")
428
+ throw Exception(name: "ExpoIapModule", description: "The payment was deferred", code: IapErrorCode.deferredPayment)
404
429
  @unknown default:
405
- throw Exception(name: "ExpoIapModule", description: "Unknown purchase result", code: "5")
430
+ throw Exception(name: "ExpoIapModule", description: "Unknown purchase result", code: IapErrorCode.unknown)
406
431
  }
407
432
  } catch {
408
433
  if error is Exception {
409
434
  throw error
410
435
  }
411
- throw Exception(name: "ExpoIapModule", description: "Purchase failed: \(error.localizedDescription)", code: "6")
436
+ throw Exception(name: "ExpoIapModule", description: "Purchase failed: \(error.localizedDescription)", code: IapErrorCode.purchaseError)
412
437
  }
413
438
  } else {
414
- throw Exception(name: "ExpoIapModule", description: "Invalid product ID", code: "7")
439
+ throw Exception(name: "ExpoIapModule", description: "Invalid product ID", code: IapErrorCode.itemUnavailable)
415
440
  }
416
441
  }
417
442
 
@@ -421,7 +446,7 @@ public class ExpoIapModule: Module {
421
446
 
422
447
  AsyncFunction("subscriptionStatus") { (sku: String) -> [[String: Any?]?]? in
423
448
  guard let productStore = self.productStore else {
424
- throw Exception(name: "ExpoIapModule", description: "Connection not initialized", code: "1")
449
+ throw Exception(name: "ExpoIapModule", description: "Connection not initialized", code: IapErrorCode.serviceError)
425
450
  }
426
451
 
427
452
  do {
@@ -436,13 +461,13 @@ public class ExpoIapModule: Module {
436
461
  if error is Exception {
437
462
  throw error
438
463
  }
439
- throw Exception(name: "ExpoIapModule", description: "Error getting subscription status: \(error.localizedDescription)", code: "2")
464
+ throw Exception(name: "ExpoIapModule", description: "Error getting subscription status: \(error.localizedDescription)", code: IapErrorCode.serviceError)
440
465
  }
441
466
  }
442
467
 
443
468
  AsyncFunction("currentEntitlement") { (sku: String) -> [String: Any?]? in
444
469
  guard let productStore = self.productStore else {
445
- throw Exception(name: "ExpoIapModule", description: "Connection not initialized", code: "1")
470
+ throw Exception(name: "ExpoIapModule", description: "Connection not initialized", code: IapErrorCode.serviceError)
446
471
  }
447
472
 
448
473
  if let product = await productStore.getProduct(productID: sku) {
@@ -451,24 +476,24 @@ public class ExpoIapModule: Module {
451
476
  let transaction = try self.checkVerified(result)
452
477
  return serializeTransaction(transaction)
453
478
  } catch StoreError.failedVerification {
454
- throw Exception(name: "ExpoIapModule", description: "Failed to verify transaction for sku \(sku)", code: "2")
479
+ throw Exception(name: "ExpoIapModule", description: "Failed to verify transaction for sku \(sku)", code: IapErrorCode.transactionValidationFailed)
455
480
  } catch {
456
481
  if error is Exception {
457
482
  throw error
458
483
  }
459
- throw Exception(name: "ExpoIapModule", description: "Error fetching entitlement for sku \(sku): \(error.localizedDescription)", code: "3")
484
+ throw Exception(name: "ExpoIapModule", description: "Error fetching entitlement for sku \(sku): \(error.localizedDescription)", code: IapErrorCode.serviceError)
460
485
  }
461
486
  } else {
462
- throw Exception(name: "ExpoIapModule", description: "Can't find entitlement for sku \(sku)", code: "4")
487
+ throw Exception(name: "ExpoIapModule", description: "Can't find entitlement for sku \(sku)", code: IapErrorCode.itemUnavailable)
463
488
  }
464
489
  } else {
465
- throw Exception(name: "ExpoIapModule", description: "Can't find product for sku \(sku)", code: "5")
490
+ throw Exception(name: "ExpoIapModule", description: "Can't find product for sku \(sku)", code: IapErrorCode.itemUnavailable)
466
491
  }
467
492
  }
468
493
 
469
494
  AsyncFunction("latestTransaction") { (sku: String) -> [String: Any?]? in
470
495
  guard let productStore = self.productStore else {
471
- throw Exception(name: "ExpoIapModule", description: "Connection not initialized", code: "1")
496
+ throw Exception(name: "ExpoIapModule", description: "Connection not initialized", code: IapErrorCode.serviceError)
472
497
  }
473
498
 
474
499
  if let product = await productStore.getProduct(productID: sku) {
@@ -477,18 +502,18 @@ public class ExpoIapModule: Module {
477
502
  let transaction = try self.checkVerified(result)
478
503
  return serializeTransaction(transaction)
479
504
  } catch StoreError.failedVerification {
480
- throw Exception(name: "ExpoIapModule", description: "Failed to verify transaction for sku \(sku)", code: "2")
505
+ throw Exception(name: "ExpoIapModule", description: "Failed to verify transaction for sku \(sku)", code: IapErrorCode.transactionValidationFailed)
481
506
  } catch {
482
507
  if error is Exception {
483
508
  throw error
484
509
  }
485
- throw Exception(name: "ExpoIapModule", description: "Error fetching latest transaction for sku \(sku): \(error.localizedDescription)", code: "3")
510
+ throw Exception(name: "ExpoIapModule", description: "Error fetching latest transaction for sku \(sku): \(error.localizedDescription)", code: IapErrorCode.serviceError)
486
511
  }
487
512
  } else {
488
- throw Exception(name: "ExpoIapModule", description: "Can't find latest transaction for sku \(sku)", code: "4")
513
+ throw Exception(name: "ExpoIapModule", description: "Can't find latest transaction for sku \(sku)", code: IapErrorCode.itemUnavailable)
489
514
  }
490
515
  } else {
491
- throw Exception(name: "ExpoIapModule", description: "Can't find product for sku \(sku)", code: "5")
516
+ throw Exception(name: "ExpoIapModule", description: "Can't find product for sku \(sku)", code: IapErrorCode.itemUnavailable)
492
517
  }
493
518
  }
494
519
 
@@ -498,7 +523,7 @@ public class ExpoIapModule: Module {
498
523
  self.transactions.removeValue(forKey: transactionIdentifier)
499
524
  return true
500
525
  } else {
501
- throw Exception(name: "ExpoIapModule", description: "Invalid transaction ID", code: "8")
526
+ throw Exception(name: "ExpoIapModule", description: "Invalid transaction ID", code: IapErrorCode.developerError)
502
527
  }
503
528
  }
504
529
 
@@ -514,7 +539,7 @@ public class ExpoIapModule: Module {
514
539
  if error is Exception {
515
540
  throw error
516
541
  }
517
- throw Exception(name: "ExpoIapModule", description: "Error synchronizing with the AppStore: \(error.localizedDescription)", code: "9")
542
+ throw Exception(name: "ExpoIapModule", description: "Error synchronizing with the AppStore: \(error.localizedDescription)", code: IapErrorCode.syncError)
518
543
  }
519
544
  }
520
545
 
@@ -523,14 +548,14 @@ public class ExpoIapModule: Module {
523
548
  SKPaymentQueue.default().presentCodeRedemptionSheet()
524
549
  return true
525
550
  #else
526
- throw Exception(name: "ExpoIapModule", description: "This method is not available on tvOS", code: "10")
551
+ throw Exception(name: "ExpoIapModule", description: "This method is not available on tvOS", code: IapErrorCode.serviceError)
527
552
  #endif
528
553
  }
529
554
 
530
555
  AsyncFunction("showManageSubscriptions") { () -> Bool in
531
556
  #if !os(tvOS)
532
557
  guard let windowScene = await self.currentWindowScene() else {
533
- throw Exception(name: "ExpoIapModule", description: "Cannot find window scene or not available on macOS", code: "11")
558
+ throw Exception(name: "ExpoIapModule", description: "Cannot find window scene or not available on macOS", code: IapErrorCode.serviceError)
534
559
  }
535
560
  // Get all subscription products before showing the management UI
536
561
  let subscriptionSkus = await self.getAllSubscriptionProductIds()
@@ -541,7 +566,7 @@ public class ExpoIapModule: Module {
541
566
  self.pollForSubscriptionStatusChanges()
542
567
  return true
543
568
  #else
544
- throw Exception(name: "ExpoIapModule", description: "This method is not available on tvOS", code: "12")
569
+ throw Exception(name: "ExpoIapModule", description: "This method is not available on tvOS", code: IapErrorCode.serviceError)
545
570
  #endif
546
571
  }
547
572
 
@@ -567,26 +592,26 @@ public class ExpoIapModule: Module {
567
592
  guard let product = await self.productStore?.getProduct(productID: sku),
568
593
  let result = await product.latestTransaction
569
594
  else {
570
- throw Exception(name: "ExpoIapModule", description: "Can't find product or transaction for sku \(sku)", code: "5")
595
+ throw Exception(name: "ExpoIapModule", description: "Can't find product or transaction for sku \(sku)", code: IapErrorCode.itemUnavailable)
571
596
  }
572
597
 
573
598
  do {
574
599
  let transaction = try self.checkVerified(result)
575
600
  guard let windowScene = await self.currentWindowScene() else {
576
- throw Exception(name: "ExpoIapModule", description: "Cannot find window scene or not available on macOS", code: "11")
601
+ throw Exception(name: "ExpoIapModule", description: "Cannot find window scene or not available on macOS", code: IapErrorCode.serviceError)
577
602
  }
578
603
  let refundStatus = try await transaction.beginRefundRequest(in: windowScene)
579
604
  return serialize(refundStatus)
580
605
  } catch StoreError.failedVerification {
581
- throw Exception(name: "ExpoIapModule", description: "Failed to verify transaction for sku \(sku)", code: "2")
606
+ throw Exception(name: "ExpoIapModule", description: "Failed to verify transaction for sku \(sku)", code: IapErrorCode.transactionValidationFailed)
582
607
  } catch {
583
608
  if error is Exception {
584
609
  throw error
585
610
  }
586
- throw Exception(name: "ExpoIapModule", description: "Failed to refund purchase: \(error.localizedDescription)", code: "3")
611
+ throw Exception(name: "ExpoIapModule", description: "Failed to refund purchase: \(error.localizedDescription)", code: IapErrorCode.serviceError)
587
612
  }
588
613
  #else
589
- throw Exception(name: "ExpoIapModule", description: "This method is not available on tvOS", code: "12")
614
+ throw Exception(name: "ExpoIapModule", description: "This method is not available on tvOS", code: IapErrorCode.serviceError)
590
615
  #endif
591
616
  }
592
617
 
@@ -601,7 +626,7 @@ public class ExpoIapModule: Module {
601
626
 
602
627
  AsyncFunction("isTransactionVerified") { (sku: String) -> Bool in
603
628
  guard let productStore = self.productStore else {
604
- throw Exception(name: "ExpoIapModule", description: "Connection not initialized", code: "1")
629
+ throw Exception(name: "ExpoIapModule", description: "Connection not initialized", code: IapErrorCode.serviceError)
605
630
  }
606
631
 
607
632
  if let product = await productStore.getProduct(productID: sku),
@@ -619,20 +644,20 @@ public class ExpoIapModule: Module {
619
644
 
620
645
  AsyncFunction("getTransactionJws") { (sku: String) -> String? in
621
646
  guard let productStore = self.productStore else {
622
- throw Exception(name: "ExpoIapModule", description: "Connection not initialized", code: "1")
647
+ throw Exception(name: "ExpoIapModule", description: "Connection not initialized", code: IapErrorCode.serviceError)
623
648
  }
624
649
 
625
650
  if let product = await productStore.getProduct(productID: sku),
626
651
  let result = await product.latestTransaction {
627
652
  return result.jwsRepresentation
628
653
  } else {
629
- throw Exception(name: "ExpoIapModule", description: "Can't find transaction for sku \(sku)", code: "5")
654
+ throw Exception(name: "ExpoIapModule", description: "Can't find transaction for sku \(sku)", code: IapErrorCode.itemUnavailable)
630
655
  }
631
656
  }
632
657
 
633
658
  AsyncFunction("validateReceiptIos") { (sku: String) -> [String: Any] in
634
659
  guard let productStore = self.productStore else {
635
- throw Exception(name: "ExpoIapModule", description: "Connection not initialized", code: "1")
660
+ throw Exception(name: "ExpoIapModule", description: "Connection not initialized", code: IapErrorCode.serviceError)
636
661
  }
637
662
 
638
663
  // Get receipt data
@@ -697,9 +722,9 @@ public class ExpoIapModule: Module {
697
722
  } catch {
698
723
  if self.hasListeners {
699
724
  let err = [
700
- "responseCode": IapErrors.E_TRANSACTION_VALIDATION_FAILED.rawValue,
725
+ "responseCode": IapErrorCode.transactionValidationFailed,
701
726
  "debugMessage": error.localizedDescription,
702
- "code": IapErrors.E_TRANSACTION_VALIDATION_FAILED.rawValue,
727
+ "code": IapErrorCode.transactionValidationFailed,
703
728
  "message": error.localizedDescription,
704
729
  ]
705
730
  self.sendEvent(IapEvent.PurchaseError, err)
@@ -817,10 +842,10 @@ public class ExpoIapModule: Module {
817
842
  let receiptData = try Data(contentsOf: appStoreReceiptURL, options: .alwaysMapped)
818
843
  return receiptData.base64EncodedString(options: [])
819
844
  } catch {
820
- throw Exception(name: "ExpoIapModule", description: "Error reading receipt data: \(error.localizedDescription)", code: "13")
845
+ throw Exception(name: "ExpoIapModule", description: "Error reading receipt data: \(error.localizedDescription)", code: IapErrorCode.receiptFailed)
821
846
  }
822
847
  } else {
823
- throw Exception(name: "ExpoIapModule", description: "App Store receipt not found", code: "14")
848
+ throw Exception(name: "ExpoIapModule", description: "App Store receipt not found", code: IapErrorCode.receiptFailed)
824
849
  }
825
850
  }
826
851
  }
package/ios/Types.swift CHANGED
@@ -12,24 +12,32 @@ public enum StoreError: Error {
12
12
  case failedVerification
13
13
  }
14
14
 
15
- enum IapErrors: String, CaseIterable {
16
- case E_UNKNOWN = "E_UNKNOWN"
17
- case E_SERVICE_ERROR = "E_SERVICE_ERROR"
18
- case E_USER_CANCELLED = "E_USER_CANCELLED"
19
- case E_USER_ERROR = "E_USER_ERROR"
20
- case E_ITEM_UNAVAILABLE = "E_ITEM_UNAVAILABLE"
21
- case E_REMOTE_ERROR = "E_REMOTE_ERROR"
22
- case E_NETWORK_ERROR = "E_NETWORK_ERROR"
23
- case E_RECEIPT_FAILED = "E_RECEIPT_FAILED"
24
- case E_RECEIPT_FINISHED_FAILED = "E_RECEIPT_FINISHED_FAILED"
25
- case E_DEVELOPER_ERROR = "E_DEVELOPER_ERROR"
26
- case E_PURCHASE_ERROR = "E_PURCHASE_ERROR"
27
- case E_SYNC_ERROR = "E_SYNC_ERROR"
28
- case E_DEFERRED_PAYMENT = "E_DEFERRED_PAYMENT"
29
- case E_TRANSACTION_VALIDATION_FAILED = "E_TRANSACTION_VALIDATION_FAILED"
30
- func asInt() -> Int {
31
- return IapErrors.allCases.firstIndex(of: self)!
32
- }
15
+ // Error codes for IAP operations - centralized error code management
16
+ struct IapErrorCode {
17
+ static let unknown = "E_UNKNOWN"
18
+ static let serviceError = "E_SERVICE_ERROR"
19
+ static let userCancelled = "E_USER_CANCELLED"
20
+ static let userError = "E_USER_ERROR"
21
+ static let itemUnavailable = "E_ITEM_UNAVAILABLE"
22
+ static let remoteError = "E_REMOTE_ERROR"
23
+ static let networkError = "E_NETWORK_ERROR"
24
+ static let receiptFailed = "E_RECEIPT_FAILED"
25
+ static let receiptFinishedFailed = "E_RECEIPT_FINISHED_FAILED"
26
+ static let notPrepared = "E_NOT_PREPARED"
27
+ static let notEnded = "E_NOT_ENDED"
28
+ static let alreadyOwned = "E_ALREADY_OWNED"
29
+ static let developerError = "E_DEVELOPER_ERROR"
30
+ static let purchaseError = "E_PURCHASE_ERROR"
31
+ static let syncError = "E_SYNC_ERROR"
32
+ static let deferredPayment = "E_DEFERRED_PAYMENT"
33
+ static let transactionValidationFailed = "E_TRANSACTION_VALIDATION_FAILED"
34
+ static let billingResponseJsonParseError = "E_BILLING_RESPONSE_JSON_PARSE_ERROR"
35
+ static let interrupted = "E_INTERRUPTED"
36
+ static let iapNotAvailable = "E_IAP_NOT_AVAILABLE"
37
+ static let activityUnavailable = "E_ACTIVITY_UNAVAILABLE"
38
+ static let alreadyPrepared = "E_ALREADY_PREPARED"
39
+ static let pending = "E_PENDING"
40
+ static let connectionClosed = "E_CONNECTION_CLOSED"
33
41
  }
34
42
 
35
43
  // Based on https://stackoverflow.com/a/40135192/570612
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "expo-iap",
3
- "version": "2.4.4",
3
+ "version": "2.4.5",
4
4
  "description": "In App Purchase module in Expo",
5
5
  "main": "build/index.js",
6
6
  "types": "build/index.d.ts",
@@ -43,11 +43,14 @@
43
43
  "eslint-config-prettier": "^10.1.5",
44
44
  "eslint-plugin-prettier": "^5.4.1",
45
45
  "expo-module-scripts": "^4.1.7",
46
- "expo-modules-core": "~2.4.0"
46
+ "expo-modules-core": "^2.4.0"
47
47
  },
48
48
  "peerDependencies": {
49
49
  "expo": "*",
50
50
  "react": "*",
51
51
  "react-native": "*"
52
+ },
53
+ "expo": {
54
+ "plugin": "./app.plugin.js"
52
55
  }
53
56
  }
@@ -12,6 +12,7 @@ import {
12
12
  RequestSubscriptionIosProps,
13
13
  SubscriptionProductIos,
14
14
  } from './types/ExpoIapIos.types';
15
+ import {NATIVE_ERROR_CODES} from './ExpoIapModule';
15
16
 
16
17
  export type ChangeEventPayload = {
17
18
  value: string;
@@ -81,6 +82,10 @@ export type PurchaseResult = {
81
82
  purchaseTokenAndroid?: string;
82
83
  };
83
84
 
85
+ /**
86
+ * Centralized error codes for expo-iap
87
+ * These are mapped to platform-specific error codes and provide consistent error handling
88
+ */
84
89
  export enum ErrorCode {
85
90
  E_UNKNOWN = 'E_UNKNOWN',
86
91
  E_USER_CANCELLED = 'E_USER_CANCELLED',
@@ -99,8 +104,76 @@ export enum ErrorCode {
99
104
  E_DEFERRED_PAYMENT = 'E_DEFERRED_PAYMENT',
100
105
  E_INTERRUPTED = 'E_INTERRUPTED',
101
106
  E_IAP_NOT_AVAILABLE = 'E_IAP_NOT_AVAILABLE',
107
+ E_PURCHASE_ERROR = 'E_PURCHASE_ERROR',
108
+ E_SYNC_ERROR = 'E_SYNC_ERROR',
109
+ E_TRANSACTION_VALIDATION_FAILED = 'E_TRANSACTION_VALIDATION_FAILED',
110
+ E_ACTIVITY_UNAVAILABLE = 'E_ACTIVITY_UNAVAILABLE',
111
+ E_ALREADY_PREPARED = 'E_ALREADY_PREPARED',
112
+ E_PENDING = 'E_PENDING',
113
+ E_CONNECTION_CLOSED = 'E_CONNECTION_CLOSED',
102
114
  }
103
115
 
116
+ /**
117
+ * Platform-specific error code mappings
118
+ * Maps ErrorCode enum values to platform-specific integer codes
119
+ */
120
+ export const ErrorCodeMapping = {
121
+ ios: {
122
+ [ErrorCode.E_UNKNOWN]: 0,
123
+ [ErrorCode.E_SERVICE_ERROR]: 1,
124
+ [ErrorCode.E_USER_CANCELLED]: 2,
125
+ [ErrorCode.E_USER_ERROR]: 3,
126
+ [ErrorCode.E_ITEM_UNAVAILABLE]: 4,
127
+ [ErrorCode.E_REMOTE_ERROR]: 5,
128
+ [ErrorCode.E_NETWORK_ERROR]: 6,
129
+ [ErrorCode.E_RECEIPT_FAILED]: 7,
130
+ [ErrorCode.E_RECEIPT_FINISHED_FAILED]: 8,
131
+ [ErrorCode.E_DEVELOPER_ERROR]: 9,
132
+ [ErrorCode.E_PURCHASE_ERROR]: 10,
133
+ [ErrorCode.E_SYNC_ERROR]: 11,
134
+ [ErrorCode.E_DEFERRED_PAYMENT]: 12,
135
+ [ErrorCode.E_TRANSACTION_VALIDATION_FAILED]: 13,
136
+ [ErrorCode.E_NOT_PREPARED]: 14,
137
+ [ErrorCode.E_NOT_ENDED]: 15,
138
+ [ErrorCode.E_ALREADY_OWNED]: 16,
139
+ [ErrorCode.E_BILLING_RESPONSE_JSON_PARSE_ERROR]: 17,
140
+ [ErrorCode.E_INTERRUPTED]: 18,
141
+ [ErrorCode.E_IAP_NOT_AVAILABLE]: 19,
142
+ [ErrorCode.E_ACTIVITY_UNAVAILABLE]: 20,
143
+ [ErrorCode.E_ALREADY_PREPARED]: 21,
144
+ [ErrorCode.E_PENDING]: 22,
145
+ [ErrorCode.E_CONNECTION_CLOSED]: 23,
146
+ },
147
+ android: {
148
+ [ErrorCode.E_UNKNOWN]: 'E_UNKNOWN',
149
+ [ErrorCode.E_USER_CANCELLED]: 'E_USER_CANCELLED',
150
+ [ErrorCode.E_USER_ERROR]: 'E_USER_ERROR',
151
+ [ErrorCode.E_ITEM_UNAVAILABLE]: 'E_ITEM_UNAVAILABLE',
152
+ [ErrorCode.E_REMOTE_ERROR]: 'E_REMOTE_ERROR',
153
+ [ErrorCode.E_NETWORK_ERROR]: 'E_NETWORK_ERROR',
154
+ [ErrorCode.E_SERVICE_ERROR]: 'E_SERVICE_ERROR',
155
+ [ErrorCode.E_RECEIPT_FAILED]: 'E_RECEIPT_FAILED',
156
+ [ErrorCode.E_RECEIPT_FINISHED_FAILED]: 'E_RECEIPT_FINISHED_FAILED',
157
+ [ErrorCode.E_NOT_PREPARED]: 'E_NOT_PREPARED',
158
+ [ErrorCode.E_NOT_ENDED]: 'E_NOT_ENDED',
159
+ [ErrorCode.E_ALREADY_OWNED]: 'E_ALREADY_OWNED',
160
+ [ErrorCode.E_DEVELOPER_ERROR]: 'E_DEVELOPER_ERROR',
161
+ [ErrorCode.E_BILLING_RESPONSE_JSON_PARSE_ERROR]:
162
+ 'E_BILLING_RESPONSE_JSON_PARSE_ERROR',
163
+ [ErrorCode.E_DEFERRED_PAYMENT]: 'E_DEFERRED_PAYMENT',
164
+ [ErrorCode.E_INTERRUPTED]: 'E_INTERRUPTED',
165
+ [ErrorCode.E_IAP_NOT_AVAILABLE]: 'E_IAP_NOT_AVAILABLE',
166
+ [ErrorCode.E_PURCHASE_ERROR]: 'E_PURCHASE_ERROR',
167
+ [ErrorCode.E_SYNC_ERROR]: 'E_SYNC_ERROR',
168
+ [ErrorCode.E_TRANSACTION_VALIDATION_FAILED]:
169
+ 'E_TRANSACTION_VALIDATION_FAILED',
170
+ [ErrorCode.E_ACTIVITY_UNAVAILABLE]: 'E_ACTIVITY_UNAVAILABLE',
171
+ [ErrorCode.E_ALREADY_PREPARED]: 'E_ALREADY_PREPARED',
172
+ [ErrorCode.E_PENDING]: 'E_PENDING',
173
+ [ErrorCode.E_CONNECTION_CLOSED]: 'E_CONNECTION_CLOSED',
174
+ },
175
+ } as const;
176
+
104
177
  export class PurchaseError implements Error {
105
178
  constructor(
106
179
  public name: string,
@@ -109,6 +182,7 @@ export class PurchaseError implements Error {
109
182
  public debugMessage?: string,
110
183
  public code?: ErrorCode,
111
184
  public productId?: string,
185
+ public platform?: 'ios' | 'android',
112
186
  ) {
113
187
  this.name = '[expo-iap]: PurchaseError';
114
188
  this.message = message;
@@ -116,5 +190,104 @@ export class PurchaseError implements Error {
116
190
  this.debugMessage = debugMessage;
117
191
  this.code = code;
118
192
  this.productId = productId;
193
+ this.platform = platform;
194
+ }
195
+
196
+ /**
197
+ * Creates a PurchaseError from platform-specific error data
198
+ * @param errorData Raw error data from native modules
199
+ * @param platform Platform where the error occurred
200
+ * @returns Properly typed PurchaseError instance
201
+ */
202
+ static fromPlatformError(
203
+ errorData: any,
204
+ platform: 'ios' | 'android',
205
+ ): PurchaseError {
206
+ const errorCode = errorData.code
207
+ ? ErrorCodeUtils.fromPlatformCode(errorData.code, platform)
208
+ : ErrorCode.E_UNKNOWN;
209
+
210
+ return new PurchaseError(
211
+ '[expo-iap]: PurchaseError',
212
+ errorData.message || 'Unknown error occurred',
213
+ errorData.responseCode,
214
+ errorData.debugMessage,
215
+ errorCode,
216
+ errorData.productId,
217
+ platform,
218
+ );
219
+ }
220
+
221
+ /**
222
+ * Gets the platform-specific error code for this error
223
+ * @returns Platform-specific error code
224
+ */
225
+ getPlatformCode(): string | number | undefined {
226
+ if (!this.code || !this.platform) return undefined;
227
+ return ErrorCodeUtils.toPlatformCode(this.code, this.platform);
119
228
  }
120
229
  }
230
+
231
+ /**
232
+ * Utility functions for error code mapping and validation
233
+ */
234
+ export const ErrorCodeUtils = {
235
+ /**
236
+ * Gets the native error code for the current platform
237
+ * @param errorCode ErrorCode enum value
238
+ * @returns Platform-specific error code from native constants
239
+ */
240
+ getNativeErrorCode: (errorCode: ErrorCode): string => {
241
+ return NATIVE_ERROR_CODES[errorCode] || errorCode;
242
+ },
243
+
244
+ /**
245
+ * Maps a platform-specific error code back to the standardized ErrorCode enum
246
+ * @param platformCode Platform-specific error code (string for Android, number for iOS)
247
+ * @param platform Target platform
248
+ * @returns Corresponding ErrorCode enum value or E_UNKNOWN if not found
249
+ */
250
+ fromPlatformCode: (
251
+ platformCode: string | number,
252
+ platform: 'ios' | 'android',
253
+ ): ErrorCode => {
254
+ const mapping = ErrorCodeMapping[platform];
255
+
256
+ for (const [errorCode, mappedCode] of Object.entries(mapping)) {
257
+ if (mappedCode === platformCode) {
258
+ return errorCode as ErrorCode;
259
+ }
260
+ }
261
+
262
+ return ErrorCode.E_UNKNOWN;
263
+ },
264
+
265
+ /**
266
+ * Maps an ErrorCode enum to platform-specific code
267
+ * @param errorCode ErrorCode enum value
268
+ * @param platform Target platform
269
+ * @returns Platform-specific error code
270
+ */
271
+ toPlatformCode: (
272
+ errorCode: ErrorCode,
273
+ platform: 'ios' | 'android',
274
+ ): string | number => {
275
+ return (
276
+ ErrorCodeMapping[platform][errorCode] ??
277
+ (platform === 'ios' ? 0 : 'E_UNKNOWN')
278
+ );
279
+ },
280
+
281
+ /**
282
+ * Checks if an error code is valid for the specified platform
283
+ * @param errorCode ErrorCode enum value
284
+ * @param platform Target platform
285
+ * @returns True if the error code is supported on the platform
286
+ */
287
+ isValidForPlatform: (
288
+ errorCode: ErrorCode,
289
+ platform: 'ios' | 'android',
290
+ ): boolean => {
291
+ return errorCode in ErrorCodeMapping[platform];
292
+ },
293
+ };
@@ -2,4 +2,9 @@ import {requireNativeModule} from 'expo-modules-core';
2
2
 
3
3
  // It loads the native module object from the JSI or falls back to
4
4
  // the bridge module (from NativeModulesProxy) if the remote debugger is on.
5
- export default requireNativeModule('ExpoIap');
5
+ const ExpoIapModule = requireNativeModule('ExpoIap');
6
+
7
+ // Platform-specific error codes from native modules
8
+ export const NATIVE_ERROR_CODES = ExpoIapModule.ERROR_CODES || {};
9
+
10
+ export default ExpoIapModule;
package/src/index.ts CHANGED
@@ -380,3 +380,4 @@ export const finishTransaction = ({
380
380
  };
381
381
 
382
382
  export * from './useIap';
383
+ export * from './utils/errorMapping';
package/src/useIap.ts CHANGED
@@ -23,8 +23,8 @@ import {
23
23
  SubscriptionProduct,
24
24
  SubscriptionPurchase,
25
25
  } from './ExpoIap.types';
26
- import {EventSubscription} from 'expo-modules-core';
27
26
  import {Platform} from 'react-native';
27
+ import {EventSubscription} from 'expo-modules-core';
28
28
 
29
29
  type UseIap = {
30
30
  connected: boolean;