expo-iap 2.4.3 → 2.4.5-rc.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.
@@ -123,8 +123,7 @@ func serializeProduct(_ p: Product) -> [String: Any?] {
123
123
  "id": p.id,
124
124
  "title": p.displayName,
125
125
  "isFamilyShareable": p.isFamilyShareable,
126
- "jsonRepresentation": serializeDebug(
127
- String(data: p.jsonRepresentation, encoding: .utf8) ?? ""),
126
+ "jsonRepresentation": String(data: p.jsonRepresentation, encoding: .utf8),
128
127
  "price": p.price,
129
128
  "subscription": p.subscription,
130
129
  "type": p.type,
@@ -187,7 +186,32 @@ public class ExpoIapModule: Module {
187
186
  Name("ExpoIap")
188
187
 
189
188
  Constants([
190
- "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
+ ]
191
215
  ])
192
216
 
193
217
  Events(IapEvent.PurchaseUpdated, IapEvent.PurchaseError)
@@ -209,7 +233,7 @@ public class ExpoIapModule: Module {
209
233
 
210
234
  AsyncFunction("getItems") { (skus: [String]) -> [[String: Any?]?] in
211
235
  guard let productStore = self.productStore else {
212
- throw Exception(name: "ExpoIapModule", description: "Connection not initialized", code: "1")
236
+ throw Exception(name: "ExpoIapModule", description: "Connection not initialized", code: IapErrorCode.notPrepared)
213
237
  }
214
238
 
215
239
  do {
@@ -295,9 +319,9 @@ public class ExpoIapModule: Module {
295
319
  }
296
320
  } catch StoreError.failedVerification {
297
321
  let err = [
298
- "responseCode": IapErrors.E_TRANSACTION_VALIDATION_FAILED.rawValue,
322
+ "responseCode": IapErrorCode.transactionValidationFailed,
299
323
  "debugMessage": StoreError.failedVerification.localizedDescription,
300
- "code": IapErrors.E_TRANSACTION_VALIDATION_FAILED.rawValue,
324
+ "code": IapErrorCode.transactionValidationFailed,
301
325
  "message": StoreError.failedVerification.localizedDescription,
302
326
  "productId": "unknown",
303
327
  ]
@@ -306,9 +330,9 @@ public class ExpoIapModule: Module {
306
330
  }
307
331
  } catch {
308
332
  let err = [
309
- "responseCode": IapErrors.E_UNKNOWN.rawValue,
333
+ "responseCode": IapErrorCode.unknown,
310
334
  "debugMessage": error.localizedDescription,
311
- "code": IapErrors.E_UNKNOWN.rawValue,
335
+ "code": IapErrorCode.unknown,
312
336
  "message": error.localizedDescription,
313
337
  "productId": "unknown",
314
338
  ]
@@ -326,7 +350,7 @@ public class ExpoIapModule: Module {
326
350
  appAccountToken: String?, quantity: Int, discountOffer: [String: String]?
327
351
  ) -> [String: Any?]? in
328
352
  guard let productStore = self.productStore else {
329
- throw Exception(name: "ExpoIapModule", description: "Connection not initialized", code: "1")
353
+ throw Exception(name: "ExpoIapModule", description: "Connection not initialized", code: IapErrorCode.serviceError)
330
354
  }
331
355
 
332
356
  let product: Product? = await productStore.getProduct(productID: sku)
@@ -356,7 +380,7 @@ public class ExpoIapModule: Module {
356
380
  options.insert(.appAccountToken(appAccountUUID))
357
381
  }
358
382
  guard let windowScene = await self.currentWindowScene() else {
359
- 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)
360
384
  }
361
385
  let result: Product.PurchaseResult
362
386
  #if swift(>=5.9)
@@ -399,20 +423,20 @@ public class ExpoIapModule: Module {
399
423
  return serialized
400
424
  }
401
425
  case .userCancelled:
402
- throw Exception(name: "ExpoIapModule", description: "User cancelled the purchase", code: "3")
426
+ throw Exception(name: "ExpoIapModule", description: "User cancelled the purchase", code: IapErrorCode.userCancelled)
403
427
  case .pending:
404
- throw Exception(name: "ExpoIapModule", description: "The payment was deferred", code: "4")
428
+ throw Exception(name: "ExpoIapModule", description: "The payment was deferred", code: IapErrorCode.deferredPayment)
405
429
  @unknown default:
406
- throw Exception(name: "ExpoIapModule", description: "Unknown purchase result", code: "5")
430
+ throw Exception(name: "ExpoIapModule", description: "Unknown purchase result", code: IapErrorCode.unknown)
407
431
  }
408
432
  } catch {
409
433
  if error is Exception {
410
434
  throw error
411
435
  }
412
- throw Exception(name: "ExpoIapModule", description: "Purchase failed: \(error.localizedDescription)", code: "6")
436
+ throw Exception(name: "ExpoIapModule", description: "Purchase failed: \(error.localizedDescription)", code: IapErrorCode.purchaseError)
413
437
  }
414
438
  } else {
415
- throw Exception(name: "ExpoIapModule", description: "Invalid product ID", code: "7")
439
+ throw Exception(name: "ExpoIapModule", description: "Invalid product ID", code: IapErrorCode.itemUnavailable)
416
440
  }
417
441
  }
418
442
 
@@ -422,7 +446,7 @@ public class ExpoIapModule: Module {
422
446
 
423
447
  AsyncFunction("subscriptionStatus") { (sku: String) -> [[String: Any?]?]? in
424
448
  guard let productStore = self.productStore else {
425
- throw Exception(name: "ExpoIapModule", description: "Connection not initialized", code: "1")
449
+ throw Exception(name: "ExpoIapModule", description: "Connection not initialized", code: IapErrorCode.serviceError)
426
450
  }
427
451
 
428
452
  do {
@@ -437,13 +461,13 @@ public class ExpoIapModule: Module {
437
461
  if error is Exception {
438
462
  throw error
439
463
  }
440
- 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)
441
465
  }
442
466
  }
443
467
 
444
468
  AsyncFunction("currentEntitlement") { (sku: String) -> [String: Any?]? in
445
469
  guard let productStore = self.productStore else {
446
- throw Exception(name: "ExpoIapModule", description: "Connection not initialized", code: "1")
470
+ throw Exception(name: "ExpoIapModule", description: "Connection not initialized", code: IapErrorCode.serviceError)
447
471
  }
448
472
 
449
473
  if let product = await productStore.getProduct(productID: sku) {
@@ -452,24 +476,24 @@ public class ExpoIapModule: Module {
452
476
  let transaction = try self.checkVerified(result)
453
477
  return serializeTransaction(transaction)
454
478
  } catch StoreError.failedVerification {
455
- 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)
456
480
  } catch {
457
481
  if error is Exception {
458
482
  throw error
459
483
  }
460
- 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)
461
485
  }
462
486
  } else {
463
- 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)
464
488
  }
465
489
  } else {
466
- 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)
467
491
  }
468
492
  }
469
493
 
470
494
  AsyncFunction("latestTransaction") { (sku: String) -> [String: Any?]? in
471
495
  guard let productStore = self.productStore else {
472
- throw Exception(name: "ExpoIapModule", description: "Connection not initialized", code: "1")
496
+ throw Exception(name: "ExpoIapModule", description: "Connection not initialized", code: IapErrorCode.serviceError)
473
497
  }
474
498
 
475
499
  if let product = await productStore.getProduct(productID: sku) {
@@ -478,18 +502,18 @@ public class ExpoIapModule: Module {
478
502
  let transaction = try self.checkVerified(result)
479
503
  return serializeTransaction(transaction)
480
504
  } catch StoreError.failedVerification {
481
- 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)
482
506
  } catch {
483
507
  if error is Exception {
484
508
  throw error
485
509
  }
486
- 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)
487
511
  }
488
512
  } else {
489
- 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)
490
514
  }
491
515
  } else {
492
- 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)
493
517
  }
494
518
  }
495
519
 
@@ -499,7 +523,7 @@ public class ExpoIapModule: Module {
499
523
  self.transactions.removeValue(forKey: transactionIdentifier)
500
524
  return true
501
525
  } else {
502
- throw Exception(name: "ExpoIapModule", description: "Invalid transaction ID", code: "8")
526
+ throw Exception(name: "ExpoIapModule", description: "Invalid transaction ID", code: IapErrorCode.developerError)
503
527
  }
504
528
  }
505
529
 
@@ -515,7 +539,7 @@ public class ExpoIapModule: Module {
515
539
  if error is Exception {
516
540
  throw error
517
541
  }
518
- 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)
519
543
  }
520
544
  }
521
545
 
@@ -524,14 +548,14 @@ public class ExpoIapModule: Module {
524
548
  SKPaymentQueue.default().presentCodeRedemptionSheet()
525
549
  return true
526
550
  #else
527
- 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)
528
552
  #endif
529
553
  }
530
554
 
531
555
  AsyncFunction("showManageSubscriptions") { () -> Bool in
532
556
  #if !os(tvOS)
533
557
  guard let windowScene = await self.currentWindowScene() else {
534
- 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)
535
559
  }
536
560
  // Get all subscription products before showing the management UI
537
561
  let subscriptionSkus = await self.getAllSubscriptionProductIds()
@@ -542,7 +566,7 @@ public class ExpoIapModule: Module {
542
566
  self.pollForSubscriptionStatusChanges()
543
567
  return true
544
568
  #else
545
- 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)
546
570
  #endif
547
571
  }
548
572
 
@@ -568,26 +592,26 @@ public class ExpoIapModule: Module {
568
592
  guard let product = await self.productStore?.getProduct(productID: sku),
569
593
  let result = await product.latestTransaction
570
594
  else {
571
- 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)
572
596
  }
573
597
 
574
598
  do {
575
599
  let transaction = try self.checkVerified(result)
576
600
  guard let windowScene = await self.currentWindowScene() else {
577
- 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)
578
602
  }
579
603
  let refundStatus = try await transaction.beginRefundRequest(in: windowScene)
580
604
  return serialize(refundStatus)
581
605
  } catch StoreError.failedVerification {
582
- 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)
583
607
  } catch {
584
608
  if error is Exception {
585
609
  throw error
586
610
  }
587
- 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)
588
612
  }
589
613
  #else
590
- 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)
591
615
  #endif
592
616
  }
593
617
 
@@ -602,7 +626,7 @@ public class ExpoIapModule: Module {
602
626
 
603
627
  AsyncFunction("isTransactionVerified") { (sku: String) -> Bool in
604
628
  guard let productStore = self.productStore else {
605
- throw Exception(name: "ExpoIapModule", description: "Connection not initialized", code: "1")
629
+ throw Exception(name: "ExpoIapModule", description: "Connection not initialized", code: IapErrorCode.serviceError)
606
630
  }
607
631
 
608
632
  if let product = await productStore.getProduct(productID: sku),
@@ -620,20 +644,20 @@ public class ExpoIapModule: Module {
620
644
 
621
645
  AsyncFunction("getTransactionJws") { (sku: String) -> String? in
622
646
  guard let productStore = self.productStore else {
623
- throw Exception(name: "ExpoIapModule", description: "Connection not initialized", code: "1")
647
+ throw Exception(name: "ExpoIapModule", description: "Connection not initialized", code: IapErrorCode.serviceError)
624
648
  }
625
649
 
626
650
  if let product = await productStore.getProduct(productID: sku),
627
651
  let result = await product.latestTransaction {
628
652
  return result.jwsRepresentation
629
653
  } else {
630
- 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)
631
655
  }
632
656
  }
633
657
 
634
658
  AsyncFunction("validateReceiptIos") { (sku: String) -> [String: Any] in
635
659
  guard let productStore = self.productStore else {
636
- throw Exception(name: "ExpoIapModule", description: "Connection not initialized", code: "1")
660
+ throw Exception(name: "ExpoIapModule", description: "Connection not initialized", code: IapErrorCode.serviceError)
637
661
  }
638
662
 
639
663
  // Get receipt data
@@ -698,9 +722,9 @@ public class ExpoIapModule: Module {
698
722
  } catch {
699
723
  if self.hasListeners {
700
724
  let err = [
701
- "responseCode": IapErrors.E_TRANSACTION_VALIDATION_FAILED.rawValue,
725
+ "responseCode": IapErrorCode.transactionValidationFailed,
702
726
  "debugMessage": error.localizedDescription,
703
- "code": IapErrors.E_TRANSACTION_VALIDATION_FAILED.rawValue,
727
+ "code": IapErrorCode.transactionValidationFailed,
704
728
  "message": error.localizedDescription,
705
729
  ]
706
730
  self.sendEvent(IapEvent.PurchaseError, err)
@@ -818,10 +842,10 @@ public class ExpoIapModule: Module {
818
842
  let receiptData = try Data(contentsOf: appStoreReceiptURL, options: .alwaysMapped)
819
843
  return receiptData.base64EncodedString(options: [])
820
844
  } catch {
821
- 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)
822
846
  }
823
847
  } else {
824
- 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)
825
849
  }
826
850
  }
827
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.3",
3
+ "version": "2.4.5-rc.1",
4
4
  "description": "In App Purchase module in Expo",
5
5
  "main": "build/index.js",
6
6
  "types": "build/index.d.ts",
@@ -43,7 +43,7 @@
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": "*",
@@ -1 +1 @@
1
- {"root":["./src/withIAP.ts"],"version":"5.8.3"}
1
+ {"root":["./src/withiap.ts"],"version":"5.8.3"}
@@ -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;