expo-iap 2.7.13 → 2.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/.copilot-instructions.md +5 -5
  2. package/.cursorrules +31 -13
  3. package/CHANGELOG.md +16 -1
  4. package/CLAUDE.md +5 -1
  5. package/android/src/main/java/expo/modules/iap/ExpoIapModule.kt +2 -0
  6. package/build/ExpoIap.types.d.ts +24 -11
  7. package/build/ExpoIap.types.d.ts.map +1 -1
  8. package/build/ExpoIap.types.js.map +1 -1
  9. package/build/helpers/subscription.d.ts.map +1 -1
  10. package/build/helpers/subscription.js +24 -17
  11. package/build/helpers/subscription.js.map +1 -1
  12. package/build/index.d.ts +2 -2
  13. package/build/index.d.ts.map +1 -1
  14. package/build/index.js +12 -17
  15. package/build/index.js.map +1 -1
  16. package/build/modules/ios.d.ts +10 -23
  17. package/build/modules/ios.d.ts.map +1 -1
  18. package/build/modules/ios.js +9 -23
  19. package/build/modules/ios.js.map +1 -1
  20. package/build/types/ExpoIapAndroid.types.d.ts +6 -0
  21. package/build/types/ExpoIapAndroid.types.d.ts.map +1 -1
  22. package/build/types/ExpoIapAndroid.types.js.map +1 -1
  23. package/build/types/{ExpoIapIos.types.d.ts → ExpoIapIOS.types.d.ts} +45 -33
  24. package/build/types/{ExpoIapIos.types.d.ts.map → ExpoIapIOS.types.d.ts.map} +1 -1
  25. package/build/types/ExpoIapIOS.types.js +2 -0
  26. package/build/types/ExpoIapIOS.types.js.map +1 -0
  27. package/build/{useIap.d.ts → useIAP.d.ts} +2 -2
  28. package/build/{useIap.d.ts.map → useIAP.d.ts.map} +1 -1
  29. package/build/{useIap.js → useIAP.js} +7 -6
  30. package/build/useIAP.js.map +1 -0
  31. package/bun.lock +677 -61
  32. package/ios/ExpoIapModule.swift +46 -45
  33. package/jest.config.js +43 -0
  34. package/package.json +8 -3
  35. package/src/ExpoIap.types.ts +34 -15
  36. package/src/helpers/subscription.ts +27 -20
  37. package/src/index.ts +16 -24
  38. package/src/modules/ios.ts +13 -40
  39. package/src/types/ExpoIapAndroid.types.ts +6 -0
  40. package/src/types/{ExpoIapIos.types.ts → ExpoIapIOS.types.ts} +46 -32
  41. package/src/{useIap.ts → useIAP.ts} +7 -6
  42. package/build/types/ExpoIapIos.types.js +0 -2
  43. package/build/types/ExpoIapIos.types.js.map +0 -1
  44. package/build/useIap.js.map +0 -1
@@ -22,12 +22,12 @@ struct IapEvent {
22
22
  }
23
23
 
24
24
  @available(iOS 15.0, *)
25
- func serializeTransaction(_ transaction: Transaction, jwsRepresentationIos: String? = nil) -> [String: Any?] {
25
+ func serializeTransaction(_ transaction: Transaction, jwsRepresentationIOS: String? = nil) -> [String: Any?] {
26
26
  let _ =
27
27
  transaction.productType.rawValue.lowercased().contains("renewable")
28
28
  || transaction.expirationDate != nil
29
29
 
30
- var transactionReasonIos: String? = nil
30
+ var transactionReasonIOS: String? = nil
31
31
  var webOrderLineItemId: Int? = nil
32
32
  var jsonData: [String: Any]? = nil
33
33
  var jwsReceipt: String = ""
@@ -38,7 +38,7 @@ func serializeTransaction(_ transaction: Transaction, jwsRepresentationIos: Stri
38
38
  do {
39
39
  if let jsonObj = try JSONSerialization.jsonObject(with: jsonRep) as? [String: Any] {
40
40
  jsonData = jsonObj
41
- transactionReasonIos = jsonObj["transactionReason"] as? String
41
+ transactionReasonIOS = jsonObj["transactionReason"] as? String
42
42
  if let webOrderId = jsonObj["webOrderLineItemID"] as? NSNumber {
43
43
  webOrderLineItemId = webOrderId.intValue
44
44
  }
@@ -56,46 +56,47 @@ func serializeTransaction(_ transaction: Transaction, jwsRepresentationIos: Stri
56
56
  "transactionReceipt": jwsReceipt,
57
57
  "platform": "ios",
58
58
 
59
- "quantityIos": transaction.purchasedQuantity,
60
- "originalTransactionDateIos": transaction.originalPurchaseDate.timeIntervalSince1970 * 1000,
61
- "originalTransactionIdentifierIos": String(transaction.originalID),
59
+ "quantityIOS": transaction.purchasedQuantity,
60
+ "originalTransactionDateIOS": transaction.originalPurchaseDate.timeIntervalSince1970 * 1000,
61
+ "originalTransactionIdentifierIOS": String(transaction.originalID),
62
62
  "appAccountToken": transaction.appAccountToken?.uuidString,
63
63
 
64
- "appBundleIdIos": transaction.appBundleID,
65
- "productTypeIos": transaction.productType.rawValue,
66
- "subscriptionGroupIdIos": transaction.subscriptionGroupID,
64
+ "appBundleIdIOS": transaction.appBundleID,
65
+ "productTypeIOS": transaction.productType.rawValue,
66
+ "subscriptionGroupIdIOS": transaction.subscriptionGroupID,
67
67
 
68
- "webOrderLineItemIdIos": webOrderLineItemId,
68
+ "webOrderLineItemIdIOS": webOrderLineItemId,
69
69
 
70
- "expirationDateIos": transaction.expirationDate.map { $0.timeIntervalSince1970 * 1000 },
70
+ "expirationDateIOS": transaction.expirationDate.map { $0.timeIntervalSince1970 * 1000 },
71
71
 
72
- "isUpgradedIos": transaction.isUpgraded,
73
- "ownershipTypeIos": transaction.ownershipType.rawValue,
72
+ "isUpgradedIOS": transaction.isUpgraded,
73
+ "ownershipTypeIOS": transaction.ownershipType.rawValue,
74
74
 
75
- "revocationDateIos": transaction.revocationDate.map { $0.timeIntervalSince1970 * 1000 },
76
- "revocationReasonIos": transaction.revocationReason?.rawValue,
77
- "transactionReasonIos": transactionReasonIos,
75
+ "revocationDateIOS": transaction.revocationDate.map { $0.timeIntervalSince1970 * 1000 },
76
+ "revocationReasonIOS": transaction.revocationReason?.rawValue,
77
+ "transactionReasonIOS": transactionReasonIOS,
78
78
  ]
79
79
 
80
- if (jwsRepresentationIos != nil) {
81
- logDebug("serializeTransaction adding jwsRepresentationIos with length: \(jwsRepresentationIos!.count)")
82
- purchaseMap["jwsRepresentationIos"] = jwsRepresentationIos
80
+ if (jwsRepresentationIOS != nil) {
81
+ logDebug("serializeTransaction adding jwsRepresentationIOS with length: \(jwsRepresentationIOS!.count)")
82
+ purchaseMap["jwsRepresentationIOS"] = jwsRepresentationIOS
83
+ purchaseMap["purchaseToken"] = jwsRepresentationIOS
83
84
  } else {
84
- logDebug("serializeTransaction jwsRepresentationIos is nil")
85
+ logDebug("serializeTransaction jwsRepresentationIOS is nil")
85
86
  }
86
87
 
87
88
  if #available(iOS 16.0, *) {
88
- purchaseMap["environmentIos"] = transaction.environment.rawValue
89
+ purchaseMap["environmentIOS"] = transaction.environment.rawValue
89
90
  }
90
91
 
91
92
  if #available(iOS 17.0, *) {
92
- purchaseMap["storefrontCountryCodeIos"] = transaction.storefront.countryCode
93
- purchaseMap["reasonIos"] = transaction.reason.rawValue
93
+ purchaseMap["storefrontCountryCodeIOS"] = transaction.storefront.countryCode
94
+ purchaseMap["reasonIOS"] = transaction.reason.rawValue
94
95
  }
95
96
 
96
97
  if #available(iOS 17.2, *) {
97
98
  if let offer = transaction.offer {
98
- purchaseMap["offerIos"] = [
99
+ purchaseMap["offerIOS"] = [
99
100
  "id": offer.id ?? "",
100
101
  "type": offer.type.rawValue,
101
102
  "paymentMode": offer.paymentMode?.rawValue ?? "",
@@ -105,10 +106,10 @@ func serializeTransaction(_ transaction: Transaction, jwsRepresentationIos: Stri
105
106
 
106
107
  if #available(iOS 15.4, *), let jsonData = jsonData {
107
108
  if let price = jsonData["price"] as? NSNumber {
108
- purchaseMap["priceIos"] = price.doubleValue
109
+ purchaseMap["priceIOS"] = price.doubleValue
109
110
  }
110
111
  if let currency = jsonData["currency"] as? String {
111
- purchaseMap["currencyIos"] = currency
112
+ purchaseMap["currencyIOS"] = currency
112
113
  }
113
114
  }
114
115
 
@@ -117,7 +118,7 @@ func serializeTransaction(_ transaction: Transaction, jwsRepresentationIos: Stri
117
118
 
118
119
  private let DEFAULT_SUBSCRIPTION_PERIOD_UNIT = "DAY" // Default fallback unit for subscription periods.
119
120
 
120
- func getPeriodIos(_ unit: Product.SubscriptionPeriod.Unit) -> String {
121
+ func getPeriodIOS(_ unit: Product.SubscriptionPeriod.Unit) -> String {
121
122
  return switch (unit) {
122
123
  case .day: "DAY"
123
124
  case .week: "WEEK"
@@ -134,7 +135,7 @@ func serializeOffer(_ offer: Product.SubscriptionOffer?) -> [String: Any?]? {
134
135
  return [
135
136
  "id": offer.id,
136
137
  "period": [
137
- "unit": getPeriodIos(offer.period.unit),
138
+ "unit": getPeriodIOS(offer.period.unit),
138
139
  "value": offer.period.value
139
140
  ],
140
141
  "periodCount": offer.periodCount,
@@ -150,9 +151,9 @@ func serializeSubscription(_ s: Product.SubscriptionInfo?) -> [String: Any?]? {
150
151
  return [
151
152
  "introductoryOffer": serializeOffer(s.introductoryOffer),
152
153
  "promotionalOffers": s.promotionalOffers.map(serializeOffer),
153
- "subscriptionGroupID": s.subscriptionGroupID,
154
+ "subscriptionGroupId": s.subscriptionGroupID,
154
155
  "subscriptionPeriod": [
155
- "unit": getPeriodIos(s.subscriptionPeriod.unit),
156
+ "unit": getPeriodIOS(s.subscriptionPeriod.unit),
156
157
  "value": s.subscriptionPeriod.value
157
158
  ],
158
159
  ]
@@ -289,7 +290,7 @@ public class ExpoIapModule: Module {
289
290
  }
290
291
 
291
292
  var result: [String: Any?] = [
292
- "bundleID": appTransaction.bundleID,
293
+ "bundleId": appTransaction.bundleID,
293
294
  "appVersion": appTransaction.appVersion,
294
295
  "originalAppVersion": appTransaction.originalAppVersion,
295
296
  "originalPurchaseDate": appTransaction.originalPurchaseDate.timeIntervalSince1970 * 1000,
@@ -297,13 +298,13 @@ public class ExpoIapModule: Module {
297
298
  "deviceVerificationNonce": appTransaction.deviceVerificationNonce.uuidString,
298
299
  "environment": appTransaction.environment.rawValue,
299
300
  "signedDate": appTransaction.signedDate.timeIntervalSince1970 * 1000,
300
- "appID": appTransaction.appID,
301
- "appVersionID": appTransaction.appVersionID,
301
+ "appId": appTransaction.appID,
302
+ "appVersionId": appTransaction.appVersionID,
302
303
  "preorderDate": appTransaction.preorderDate.map { $0.timeIntervalSince1970 * 1000 }
303
304
  ]
304
305
 
305
306
  if #available(iOS 18.4, *) {
306
- result["appTransactionID"] = appTransaction.appTransactionID
307
+ result["appTransactionId"] = appTransaction.appTransactionID
307
308
  result["originalPlatform"] = appTransaction.originalPlatform.rawValue
308
309
  }
309
310
 
@@ -392,8 +393,8 @@ public class ExpoIapModule: Module {
392
393
 
393
394
  var purchasedItemsSerialized: [[String: Any?]] = []
394
395
 
395
- func addTransaction(transaction: Transaction, jwsRepresentationIos: String? = nil) {
396
- let serialized = serializeTransaction(transaction, jwsRepresentationIos: jwsRepresentationIos)
396
+ func addTransaction(transaction: Transaction, jwsRepresentationIOS: String? = nil) {
397
+ let serialized = serializeTransaction(transaction, jwsRepresentationIOS: jwsRepresentationIOS)
397
398
  purchasedItemsSerialized.append(serialized)
398
399
 
399
400
  if alsoPublishToEventListener {
@@ -407,7 +408,7 @@ public class ExpoIapModule: Module {
407
408
  do {
408
409
  let transaction = try self.checkVerified(verification)
409
410
  if !onlyIncludeActiveItems {
410
- addTransaction(transaction: transaction, jwsRepresentationIos: verification.jwsRepresentation)
411
+ addTransaction(transaction: transaction, jwsRepresentationIOS: verification.jwsRepresentation)
411
412
  continue
412
413
  }
413
414
  switch transaction.productType {
@@ -415,7 +416,7 @@ public class ExpoIapModule: Module {
415
416
  if await self.productStore?.getProduct(productID: transaction.productID)
416
417
  != nil
417
418
  {
418
- addTransaction(transaction: transaction, jwsRepresentationIos: verification.jwsRepresentation)
419
+ addTransaction(transaction: transaction, jwsRepresentationIOS: verification.jwsRepresentation)
419
420
  }
420
421
  case .nonRenewable:
421
422
  if await self.productStore?.getProduct(productID: transaction.productID)
@@ -425,7 +426,7 @@ public class ExpoIapModule: Module {
425
426
  let expirationDate = Calendar(identifier: .gregorian).date(
426
427
  byAdding: DateComponents(year: 1), to: transaction.purchaseDate)!
427
428
  if currentDate < expirationDate {
428
- addTransaction(transaction: transaction, jwsRepresentationIos: verification.jwsRepresentation)
429
+ addTransaction(transaction: transaction, jwsRepresentationIOS: verification.jwsRepresentation)
429
430
  }
430
431
  }
431
432
  default:
@@ -536,10 +537,10 @@ public class ExpoIapModule: Module {
536
537
  return nil
537
538
  } else {
538
539
  self.transactions[String(transaction.id)] = transaction
539
- let serialized = serializeTransaction(transaction, jwsRepresentationIos: verification.jwsRepresentation)
540
+ let serialized = serializeTransaction(transaction, jwsRepresentationIOS: verification.jwsRepresentation)
540
541
 
541
- // Debug: Check if jwsRepresentationIos is included in serialized result
542
- logDebug("buyProduct serialized includes JWS: \(serialized["jwsRepresentationIos"] != nil)")
542
+ // Debug: Check if jwsRepresentationIOS is included in serialized result
543
+ logDebug("buyProduct serialized includes JWS: \(serialized["jwsRepresentationIOS"] != nil)")
543
544
 
544
545
  self.sendEvent(IapEvent.PurchaseUpdated, serialized)
545
546
  return serialized
@@ -887,7 +888,7 @@ public class ExpoIapModule: Module {
887
888
  // If this doesn't throw, the transaction is verified
888
889
  let transaction = try self.checkVerified(result)
889
890
  isValid = true
890
- latestTransaction = serializeTransaction(transaction, jwsRepresentationIos: result.jwsRepresentation)
891
+ latestTransaction = serializeTransaction(transaction, jwsRepresentationIOS: result.jwsRepresentation)
891
892
  } catch {
892
893
  isValid = false
893
894
  }
@@ -973,7 +974,7 @@ public class ExpoIapModule: Module {
973
974
  let transaction = try self.checkVerified(result)
974
975
  self.transactions[String(transaction.id)] = transaction
975
976
  if self.hasListeners {
976
- let serialized = serializeTransaction(transaction, jwsRepresentationIos: result.jwsRepresentation)
977
+ let serialized = serializeTransaction(transaction, jwsRepresentationIOS: result.jwsRepresentation)
977
978
  self.sendEvent(IapEvent.PurchaseUpdated, serialized)
978
979
  }
979
980
  } catch {
@@ -1075,7 +1076,7 @@ public class ExpoIapModule: Module {
1075
1076
  previousWillAutoRenew != currentWillAutoRenew {
1076
1077
 
1077
1078
  // Use the jwsRepresentation when serializing the transaction
1078
- var purchaseMap = serializeTransaction(transaction, jwsRepresentationIos: result.jwsRepresentation)
1079
+ var purchaseMap = serializeTransaction(transaction, jwsRepresentationIOS: result.jwsRepresentation)
1079
1080
 
1080
1081
  if case .verified(let renewalInfo) = status.renewalInfo {
1081
1082
  if let renewalInfoDict = serializeRenewalInfo(.verified(renewalInfo)) {
package/jest.config.js ADDED
@@ -0,0 +1,43 @@
1
+ module.exports = {
2
+ preset: 'ts-jest',
3
+ testEnvironment: 'node',
4
+ roots: ['<rootDir>/src'],
5
+ testMatch: [
6
+ '**/__tests__/**/*.+(ts|tsx|js)',
7
+ '**/?(*.)+(spec|test).+(ts|tsx|js)',
8
+ ],
9
+ transform: {
10
+ '^.+\\.(ts|tsx)$': [
11
+ 'ts-jest',
12
+ {
13
+ tsconfig: {
14
+ jsx: 'react',
15
+ esModuleInterop: true,
16
+ allowSyntheticDefaultImports: true,
17
+ moduleResolution: 'node',
18
+ skipLibCheck: true,
19
+ },
20
+ },
21
+ ],
22
+ },
23
+ moduleNameMapper: {
24
+ '^react-native$': '<rootDir>/src/__mocks__/react-native.js',
25
+ '^expo-modules-core$': '<rootDir>/src/__mocks__/expo-modules-core.js',
26
+ },
27
+ collectCoverageFrom: [
28
+ 'src/**/*.{ts,tsx}',
29
+ '!src/**/*.d.ts',
30
+ '!src/**/__tests__/**',
31
+ '!src/**/__mocks__/**',
32
+ '!src/ExpoIapModule.ts',
33
+ '!src/ExpoIapModule.web.ts',
34
+ ],
35
+ coverageThreshold: {
36
+ global: {
37
+ branches: 15,
38
+ functions: 15,
39
+ lines: 15,
40
+ statements: 15,
41
+ },
42
+ },
43
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "expo-iap",
3
- "version": "2.7.13",
3
+ "version": "2.8.0",
4
4
  "description": "In App Purchase module in Expo",
5
5
  "main": "build/index.js",
6
6
  "types": "build/index.d.ts",
@@ -12,6 +12,8 @@
12
12
  "lint:prettier": "prettier --write \"**/*.{md,js,jsx,ts,tsx}\"",
13
13
  "lint:tsc": "tsc -p tsconfig.json --noEmit --skipLibCheck",
14
14
  "lint:ci": "bun run lint:tsc && bun run lint:eslint && bun run lint:prettier",
15
+ "test": "jest",
16
+ "test:coverage": "jest --coverage",
15
17
  "prepare": "expo-module prepare",
16
18
  "expo-module": "expo-module",
17
19
  "open:ios": "xed example/ios",
@@ -38,15 +40,18 @@
38
40
  "author": "hyochan <hyochan.dev@gmail.com> (https://github.com/hyochan)",
39
41
  "license": "MIT",
40
42
  "homepage": "https://github.com/hyochan/expo-iap#readme",
41
- "dependencies": {},
42
43
  "devDependencies": {
44
+ "@jest/globals": "^30.0.5",
45
+ "@types/jest": "^30.0.0",
43
46
  "@types/react": "~19.1.7",
44
47
  "eslint": "8.57.0",
45
48
  "eslint-config-expo": "^9.2.0",
46
49
  "eslint-config-prettier": "^10.1.5",
47
50
  "eslint-plugin-prettier": "^5.4.1",
48
51
  "expo-module-scripts": "^4.1.7",
49
- "expo-modules-core": "^2.4.0"
52
+ "expo-modules-core": "^2.4.0",
53
+ "jest": "^29.7.0",
54
+ "ts-jest": "^29.4.1"
50
55
  },
51
56
  "peerDependencies": {
52
57
  "expo": "*",
@@ -4,10 +4,10 @@ import {
4
4
  SubscriptionProductAndroid,
5
5
  } from './types/ExpoIapAndroid.types';
6
6
  import {
7
- ProductIos,
8
- ProductPurchaseIos,
9
- SubscriptionProductIos,
10
- } from './types/ExpoIapIos.types';
7
+ ProductIOS,
8
+ ProductPurchaseIOS,
9
+ SubscriptionProductIOS,
10
+ } from './types/ExpoIapIOS.types';
11
11
  import {NATIVE_ERROR_CODES} from './ExpoIapModule';
12
12
 
13
13
  export type ChangeEventPayload = {
@@ -33,6 +33,7 @@ export type PurchaseBase = {
33
33
  transactionId?: string; // @deprecated - use id instead
34
34
  transactionDate: number;
35
35
  transactionReceipt: string;
36
+ purchaseToken?: string; // Unified purchase token (jwsRepresentation for iOS, purchaseToken for Android)
36
37
  };
37
38
 
38
39
  // Define literal platform types for better type discrimination
@@ -42,29 +43,34 @@ export type AndroidPlatform = {platform: 'android'};
42
43
  // Platform-agnostic unified product types (public API)
43
44
  export type Product =
44
45
  | (ProductAndroid & AndroidPlatform)
45
- | (ProductIos & IosPlatform);
46
+ | (ProductIOS & IosPlatform);
46
47
 
47
48
  export type SubscriptionProduct =
48
49
  | (SubscriptionProductAndroid & AndroidPlatform)
49
- | (SubscriptionProductIos & IosPlatform);
50
+ | (SubscriptionProductIOS & IosPlatform);
50
51
 
51
52
  // ============================================================================
52
- // Legacy Types (For backward compatibility with useIap hook)
53
+ // Legacy Types (For backward compatibility with useIAP hook)
53
54
  // ============================================================================
54
55
 
55
56
  // Re-export platform-specific purchase types for legacy compatibility
56
57
  export type {ProductPurchaseAndroid} from './types/ExpoIapAndroid.types';
57
- export type {ProductPurchaseIos} from './types/ExpoIapIos.types';
58
+ export type {ProductPurchaseIOS} from './types/ExpoIapIOS.types';
59
+
60
+ /**
61
+ * @deprecated Use ProductPurchaseIOS instead. This alias will be removed in v3.0.0.
62
+ */
63
+ export type ProductPurchaseIos = ProductPurchaseIOS;
58
64
 
59
65
  // Union type for platform-specific purchase types (legacy support)
60
66
  export type ProductPurchase =
61
67
  | (ProductPurchaseAndroid & AndroidPlatform)
62
- | (ProductPurchaseIos & IosPlatform);
68
+ | (ProductPurchaseIOS & IosPlatform);
63
69
 
64
70
  // Union type for platform-specific subscription purchase types (legacy support)
65
71
  export type SubscriptionPurchase =
66
72
  | (ProductPurchaseAndroid & AndroidPlatform & {autoRenewingAndroid: boolean})
67
- | (ProductPurchaseIos & IosPlatform);
73
+ | (ProductPurchaseIOS & IosPlatform);
68
74
 
69
75
  export type Purchase = ProductPurchase | SubscriptionPurchase;
70
76
 
@@ -74,7 +80,11 @@ export type PurchaseResult = {
74
80
  debugMessage?: string;
75
81
  code?: string;
76
82
  message?: string;
83
+ /**
84
+ * @deprecated Use `purchaseToken` instead. This field will be removed in a future version.
85
+ */
77
86
  purchaseTokenAndroid?: string;
87
+ purchaseToken?: string;
78
88
  };
79
89
  /**
80
90
  * Centralized error codes for expo-iap
@@ -303,7 +313,7 @@ export interface UnifiedRequestPurchaseProps {
303
313
  readonly andDangerouslyFinishTransactionAutomaticallyIOS?: boolean;
304
314
  readonly appAccountToken?: string;
305
315
  readonly quantity?: number;
306
- readonly withOffer?: import('./types/ExpoIapIos.types').PaymentDiscount;
316
+ readonly withOffer?: import('./types/ExpoIapIOS.types').PaymentDiscount;
307
317
 
308
318
  // Android-specific properties (ignored on iOS)
309
319
  readonly obfuscatedAccountIdAndroid?: string;
@@ -318,12 +328,12 @@ export interface UnifiedRequestPurchaseProps {
318
328
  /**
319
329
  * iOS-specific purchase request parameters
320
330
  */
321
- export interface RequestPurchaseIosProps {
331
+ export interface RequestPurchaseIOSProps {
322
332
  readonly sku: string;
323
333
  readonly andDangerouslyFinishTransactionAutomaticallyIOS?: boolean;
324
334
  readonly appAccountToken?: string;
325
335
  readonly quantity?: number;
326
- readonly withOffer?: import('./types/ExpoIapIos.types').PaymentDiscount;
336
+ readonly withOffer?: import('./types/ExpoIapIOS.types').PaymentDiscount;
327
337
  }
328
338
 
329
339
  /**
@@ -354,7 +364,7 @@ export interface RequestSubscriptionAndroidProps
354
364
  * Allows clear separation of iOS and Android parameters
355
365
  */
356
366
  export interface RequestPurchasePropsByPlatforms {
357
- readonly ios?: RequestPurchaseIosProps;
367
+ readonly ios?: RequestPurchaseIOSProps;
358
368
  readonly android?: RequestPurchaseAndroidProps;
359
369
  }
360
370
 
@@ -362,7 +372,7 @@ export interface RequestPurchasePropsByPlatforms {
362
372
  * Modern platform-specific subscription request structure (v2.7.0+)
363
373
  */
364
374
  export interface RequestSubscriptionPropsByPlatforms {
365
- readonly ios?: RequestPurchaseIosProps;
375
+ readonly ios?: RequestPurchaseIOSProps;
366
376
  readonly android?: RequestSubscriptionAndroidProps;
367
377
  }
368
378
 
@@ -378,4 +388,13 @@ export type RequestPurchaseProps = RequestPurchasePropsByPlatforms;
378
388
  */
379
389
  export type RequestSubscriptionProps = RequestSubscriptionPropsByPlatforms;
380
390
 
391
+ // ============================================================================
392
+ // Deprecated Aliases for Backward Compatibility
393
+ // ============================================================================
394
+
395
+ /**
396
+ * @deprecated Use RequestPurchaseIOSProps instead. This alias will be removed in v3.0.0.
397
+ */
398
+ export type RequestPurchaseIosProps = RequestPurchaseIOSProps;
399
+
381
400
  // Note: Type guard functions are exported from index.ts to avoid conflicts
@@ -28,15 +28,16 @@ export const getActiveSubscriptions = async (
28
28
  const filteredPurchases = purchases.filter((purchase) => {
29
29
  // If specific IDs provided, filter by them
30
30
  if (subscriptionIds && subscriptionIds.length > 0) {
31
- if (!subscriptionIds.includes(purchase.id)) {
31
+ if (!subscriptionIds.includes(purchase.productId)) {
32
32
  return false;
33
33
  }
34
34
  }
35
35
 
36
36
  // Check if this purchase has subscription-specific fields
37
37
  const hasSubscriptionFields =
38
- ('expirationDateIos' in purchase && purchase.expirationDateIos) ||
39
- 'autoRenewingAndroid' in purchase;
38
+ ('expirationDateIOS' in purchase && purchase.expirationDateIOS) ||
39
+ 'autoRenewingAndroid' in purchase ||
40
+ ('environmentIOS' in purchase && purchase.environmentIOS === 'Sandbox');
40
41
 
41
42
  if (!hasSubscriptionFields) {
42
43
  return false;
@@ -44,19 +45,25 @@ export const getActiveSubscriptions = async (
44
45
 
45
46
  // Check if it's actually active
46
47
  if (Platform.OS === 'ios') {
47
- if ('expirationDateIos' in purchase && purchase.expirationDateIos) {
48
- return purchase.expirationDateIos > currentTime;
48
+ if ('expirationDateIOS' in purchase && purchase.expirationDateIOS) {
49
+ return purchase.expirationDateIOS > currentTime;
49
50
  }
50
- if (
51
- 'environmentIos' in purchase &&
52
- purchase.environmentIos === 'Sandbox'
53
- ) {
51
+ // For iOS purchases without expiration date (like Sandbox), we consider them active
52
+ // if they have the environmentIOS field and were created recently
53
+ if ('environmentIOS' in purchase && purchase.environmentIOS) {
54
54
  const dayInMs = 24 * 60 * 60 * 1000;
55
+ // If no expiration date, consider active if transaction is recent (within 24 hours for Sandbox)
55
56
  if (
56
- purchase.transactionDate &&
57
- currentTime - purchase.transactionDate < dayInMs
57
+ !('expirationDateIOS' in purchase) ||
58
+ !purchase.expirationDateIOS
58
59
  ) {
59
- return true;
60
+ if (
61
+ purchase.environmentIOS === 'Sandbox' &&
62
+ purchase.transactionDate &&
63
+ currentTime - purchase.transactionDate < dayInMs
64
+ ) {
65
+ return true;
66
+ }
60
67
  }
61
68
  }
62
69
  } else if (Platform.OS === 'android') {
@@ -70,26 +77,26 @@ export const getActiveSubscriptions = async (
70
77
  // Convert to ActiveSubscription format
71
78
  for (const purchase of filteredPurchases) {
72
79
  const subscription: ActiveSubscription = {
73
- productId: purchase.id,
80
+ productId: purchase.productId,
74
81
  isActive: true,
75
82
  };
76
83
 
77
84
  // Add platform-specific details
78
85
  if (Platform.OS === 'ios') {
79
- if ('expirationDateIos' in purchase && purchase.expirationDateIos) {
80
- const expirationDate = new Date(purchase.expirationDateIos);
86
+ if ('expirationDateIOS' in purchase && purchase.expirationDateIOS) {
87
+ const expirationDate = new Date(purchase.expirationDateIOS);
81
88
  subscription.expirationDateIOS = expirationDate;
82
89
 
83
- // Calculate days until expiration
84
- const daysUntilExpiration = Math.floor(
85
- (purchase.expirationDateIos - currentTime) / (1000 * 60 * 60 * 24),
90
+ // Calculate days until expiration (round to nearest day)
91
+ const daysUntilExpiration = Math.round(
92
+ (purchase.expirationDateIOS - currentTime) / (1000 * 60 * 60 * 24),
86
93
  );
87
94
  subscription.daysUntilExpirationIOS = daysUntilExpiration;
88
95
  subscription.willExpireSoon = daysUntilExpiration <= 7;
89
96
  }
90
97
 
91
- if ('environmentIos' in purchase) {
92
- subscription.environmentIOS = purchase.environmentIos;
98
+ if ('environmentIOS' in purchase) {
99
+ subscription.environmentIOS = purchase.environmentIOS;
93
100
  }
94
101
  } else if (Platform.OS === 'android') {
95
102
  if ('autoRenewingAndroid' in purchase) {
package/src/index.ts CHANGED
@@ -5,9 +5,9 @@ import {Platform} from 'react-native';
5
5
  // Internal modules
6
6
  import ExpoIapModule from './ExpoIapModule';
7
7
  import {
8
- isProductIos,
8
+ isProductIOS,
9
9
  validateReceiptIOS,
10
- deepLinkToSubscriptionsIos,
10
+ deepLinkToSubscriptionsIOS,
11
11
  } from './modules/ios';
12
12
  import {
13
13
  isProductAndroid,
@@ -28,13 +28,13 @@ import {
28
28
  SubscriptionPurchase,
29
29
  } from './ExpoIap.types';
30
30
  import {ProductPurchaseAndroid} from './types/ExpoIapAndroid.types';
31
- import {PaymentDiscount} from './types/ExpoIapIos.types';
31
+ import {PaymentDiscount} from './types/ExpoIapIOS.types';
32
32
 
33
33
  // Export all types
34
34
  export * from './ExpoIap.types';
35
35
  export * from './modules/android';
36
36
  export * from './modules/ios';
37
- export type {AppTransactionIOS} from './types/ExpoIapIos.types';
37
+ export type {AppTransactionIOS} from './types/ExpoIapIOS.types';
38
38
 
39
39
  // Export subscription helpers
40
40
  export {
@@ -135,7 +135,7 @@ export const getProducts = async (skus: string[]): Promise<Product[]> => {
135
135
  ios: async () => {
136
136
  const rawItems = await ExpoIapModule.getItems(skus);
137
137
  return rawItems.filter((item: unknown) => {
138
- if (!isProductIos(item)) return false;
138
+ if (!isProductIOS(item)) return false;
139
139
  return (
140
140
  typeof item === 'object' &&
141
141
  item !== null &&
@@ -169,7 +169,7 @@ export const getSubscriptions = async (
169
169
  ios: async () => {
170
170
  const rawItems = await ExpoIapModule.getItems(skus);
171
171
  return rawItems.filter((item: unknown) => {
172
- if (!isProductIos(item)) return false;
172
+ if (!isProductIOS(item)) return false;
173
173
  return (
174
174
  typeof item === 'object' &&
175
175
  item !== null &&
@@ -236,7 +236,7 @@ export const requestProducts = async ({
236
236
  if (Platform.OS === 'ios') {
237
237
  const rawItems = await ExpoIapModule.getItems(skus);
238
238
  const filteredItems = rawItems.filter((item: unknown) => {
239
- if (!isProductIos(item)) return false;
239
+ if (!isProductIOS(item)) return false;
240
240
  return (
241
241
  typeof item === 'object' &&
242
242
  item !== null &&
@@ -341,7 +341,7 @@ export const getAvailablePurchases = ({
341
341
  }) || (() => Promise.resolve([]))
342
342
  )();
343
343
 
344
- const offerToRecordIos = (
344
+ const offerToRecordIOS = (
345
345
  offer: PaymentDiscount | undefined,
346
346
  ): Record<keyof PaymentDiscount, string> | undefined => {
347
347
  if (!offer) return undefined;
@@ -436,7 +436,7 @@ export const requestPurchase = (
436
436
  } = normalizedRequest;
437
437
 
438
438
  return (async () => {
439
- const offer = offerToRecordIos(withOffer);
439
+ const offer = offerToRecordIOS(withOffer);
440
440
  const purchase = await ExpoIapModule.buyProduct(
441
441
  sku,
442
442
  andDangerouslyFinishTransactionAutomaticallyIOS,
@@ -491,13 +491,14 @@ export const requestPurchase = (
491
491
  subscriptionOffers = [],
492
492
  replacementModeAndroid = -1,
493
493
  purchaseTokenAndroid,
494
+ purchaseToken,
494
495
  } = normalizedRequest;
495
496
 
496
497
  return (async () => {
497
498
  return ExpoIapModule.buyItemByType({
498
499
  type: 'subs',
499
500
  skuArr: skus,
500
- purchaseToken: purchaseTokenAndroid,
501
+ purchaseToken: purchaseTokenAndroid || purchaseToken,
501
502
  replacementMode: replacementModeAndroid,
502
503
  obfuscatedAccountId: obfuscatedAccountIdAndroid,
503
504
  obfuscatedProfileId: obfuscatedProfileIdAndroid,
@@ -575,20 +576,11 @@ export const finishTransaction = ({
575
576
  android: async () => {
576
577
  const androidPurchase = purchase as ProductPurchaseAndroid;
577
578
 
578
- if (!('purchaseTokenAndroid' in androidPurchase)) {
579
- return Promise.reject(
580
- new Error('purchaseToken is required to finish transaction'),
581
- );
582
- }
583
579
  if (isConsumable) {
584
- return ExpoIapModule.consumeProduct(
585
- androidPurchase.purchaseTokenAndroid,
586
- );
587
- } else {
588
- return ExpoIapModule.acknowledgePurchase(
589
- androidPurchase.purchaseTokenAndroid,
590
- );
580
+ return ExpoIapModule.consumeProduct(androidPurchase.purchaseToken);
591
581
  }
582
+
583
+ return ExpoIapModule.acknowledgePurchase(androidPurchase.purchaseToken);
592
584
  },
593
585
  }) || (() => Promise.reject(new Error('Unsupported Platform')))
594
586
  )();
@@ -691,7 +683,7 @@ export const deepLinkToSubscriptions = (options: {
691
683
  packageNameAndroid?: string;
692
684
  }): Promise<void> => {
693
685
  if (Platform.OS === 'ios') {
694
- return deepLinkToSubscriptionsIos();
686
+ return deepLinkToSubscriptionsIOS();
695
687
  }
696
688
 
697
689
  if (Platform.OS === 'android') {
@@ -718,5 +710,5 @@ export const deepLinkToSubscriptions = (options: {
718
710
  return Promise.reject(new Error(`Unsupported platform: ${Platform.OS}`));
719
711
  };
720
712
 
721
- export * from './useIap';
713
+ export * from './useIAP';
722
714
  export * from './utils/errorMapping';