expo-iap 2.9.0-rc.3 → 2.9.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 (46) hide show
  1. package/CHANGELOG.md +29 -6
  2. package/CLAUDE.md +40 -0
  3. package/CONTRIBUTING.md +4 -3
  4. package/build/helpers/subscription.d.ts +3 -0
  5. package/build/helpers/subscription.d.ts.map +1 -1
  6. package/build/helpers/subscription.js +10 -5
  7. package/build/helpers/subscription.js.map +1 -1
  8. package/build/index.d.ts.map +1 -1
  9. package/build/index.js +29 -10
  10. package/build/index.js.map +1 -1
  11. package/build/modules/ios.d.ts +4 -5
  12. package/build/modules/ios.d.ts.map +1 -1
  13. package/build/modules/ios.js +2 -3
  14. package/build/modules/ios.js.map +1 -1
  15. package/build/types/ExpoIapAndroid.types.d.ts +2 -2
  16. package/build/types/ExpoIapAndroid.types.d.ts.map +1 -1
  17. package/build/types/ExpoIapAndroid.types.js.map +1 -1
  18. package/build/types/ExpoIapIOS.types.d.ts +3 -3
  19. package/build/types/ExpoIapIOS.types.d.ts.map +1 -1
  20. package/build/types/ExpoIapIOS.types.js.map +1 -1
  21. package/build/useIAP.d.ts +1 -1
  22. package/build/useIAP.d.ts.map +1 -1
  23. package/build/useIAP.js +54 -33
  24. package/build/useIAP.js.map +1 -1
  25. package/build/utils/constants.d.ts +4 -0
  26. package/build/utils/constants.d.ts.map +1 -0
  27. package/build/utils/constants.js +12 -0
  28. package/build/utils/constants.js.map +1 -0
  29. package/ios/ExpoIap.podspec +1 -1
  30. package/ios/ExpoIapModule.swift +341 -334
  31. package/jest.config.js +19 -14
  32. package/package.json +2 -3
  33. package/plugin/build/withIAP.js +25 -23
  34. package/plugin/build/withLocalOpenIAP.js +5 -1
  35. package/plugin/src/withIAP.ts +39 -31
  36. package/plugin/src/withLocalOpenIAP.ts +8 -2
  37. package/plugin/tsconfig.tsbuildinfo +1 -1
  38. package/src/helpers/subscription.ts +35 -23
  39. package/src/index.ts +50 -33
  40. package/src/modules/ios.ts +4 -5
  41. package/src/types/ExpoIapAndroid.types.ts +4 -3
  42. package/src/types/ExpoIapIOS.types.ts +3 -4
  43. package/src/useIAP.ts +73 -52
  44. package/src/utils/constants.ts +14 -0
  45. package/ios/ProductStore.swift +0 -27
  46. package/ios/Types.swift +0 -96
@@ -170,15 +170,14 @@ export const beginRefundRequestIOS = (
170
170
 
171
171
  /**
172
172
  * Shows the system UI for managing subscriptions.
173
- * When the user changes subscription renewal status, the system will emit events to
174
- * purchaseUpdatedListener and transactionUpdatedIOS listeners.
173
+ * Returns an array of subscriptions that had status changes after the UI is closed.
175
174
  *
176
- * @returns Promise resolving to null on success
175
+ * @returns Promise<Purchase[]> - Array of subscriptions with status changes (e.g., auto-renewal toggled)
177
176
  * @throws Error if called on non-iOS platform
178
177
  *
179
178
  * @platform iOS
180
179
  */
181
- export const showManageSubscriptionsIOS = (): Promise<null> => {
180
+ export const showManageSubscriptionsIOS = (): Promise<Purchase[]> => {
182
181
  return ExpoIapModule.showManageSubscriptionsIOS();
183
182
  };
184
183
 
@@ -417,7 +416,7 @@ export const beginRefundRequest = (
417
416
  /**
418
417
  * @deprecated Use `showManageSubscriptionsIOS` instead. This function will be removed in version 3.0.0.
419
418
  */
420
- export const showManageSubscriptions = (): Promise<null> => {
419
+ export const showManageSubscriptions = (): Promise<Purchase[]> => {
421
420
  console.warn(
422
421
  '`showManageSubscriptions` is deprecated. Use `showManageSubscriptionsIOS` instead. This function will be removed in version 3.0.0.',
423
422
  );
@@ -31,7 +31,7 @@ type ProductSubscriptionAndroidOfferDetail = {
31
31
  export type ProductAndroid = ProductCommon & {
32
32
  nameAndroid: string;
33
33
  oneTimePurchaseOfferDetailsAndroid?: ProductAndroidOneTimePurchaseOfferDetail;
34
- platform: "android";
34
+ platform: 'android';
35
35
  subscriptionOfferDetailsAndroid?: ProductSubscriptionAndroidOfferDetail[];
36
36
  /**
37
37
  * @deprecated Use `nameAndroid` instead. This field will be removed in v2.9.0.
@@ -144,7 +144,7 @@ export const PurchaseStateAndroid = PurchaseAndroidState;
144
144
 
145
145
  // Legacy naming for backward compatibility
146
146
  export type ProductPurchaseAndroid = PurchaseCommon & {
147
- platform: "android";
147
+ platform: 'android';
148
148
  /**
149
149
  * @deprecated Use `purchaseToken` instead. This field will be removed in a future version.
150
150
  */
@@ -167,7 +167,8 @@ export type PurchaseAndroid = ProductPurchaseAndroid;
167
167
  /**
168
168
  * @deprecated Use `ProductAndroidOneTimePurchaseOfferDetail` instead. This type will be removed in v2.9.0.
169
169
  */
170
- export type OneTimePurchaseOfferDetails = ProductAndroidOneTimePurchaseOfferDetail;
170
+ export type OneTimePurchaseOfferDetails =
171
+ ProductAndroidOneTimePurchaseOfferDetail;
171
172
 
172
173
  /**
173
174
  * @deprecated Use `ProductSubscriptionAndroidOfferDetail` instead. This type will be removed in v2.9.0.
@@ -30,7 +30,7 @@ export type ProductIOS = ProductCommon & {
30
30
  displayNameIOS: string;
31
31
  isFamilyShareableIOS: boolean;
32
32
  jsonRepresentationIOS: string;
33
- platform: "ios";
33
+ platform: 'ios';
34
34
  subscriptionInfoIOS?: SubscriptionInfo;
35
35
  /**
36
36
  * @deprecated Use `displayNameIOS` instead. This field will be removed in v2.9.0.
@@ -69,7 +69,7 @@ export type ProductSubscriptionIOS = ProductIOS & {
69
69
  introductoryPricePaymentModeIOS?: PaymentMode;
70
70
  introductoryPriceNumberOfPeriodsIOS?: string;
71
71
  introductoryPriceSubscriptionPeriodIOS?: SubscriptionIosPeriod;
72
- platform: "ios";
72
+ platform: 'ios';
73
73
  subscriptionPeriodNumberIOS?: string;
74
74
  subscriptionPeriodUnitIOS?: SubscriptionIosPeriod;
75
75
  /**
@@ -140,7 +140,7 @@ export type ProductStatusIOS = {
140
140
  // Legacy naming for backward compatibility
141
141
  export type ProductPurchaseIOS = PurchaseCommon & {
142
142
  // iOS basic fields
143
- platform: "ios";
143
+ platform: 'ios';
144
144
  quantityIOS?: number;
145
145
  originalTransactionDateIOS?: number;
146
146
  originalTransactionIdentifierIOS?: string;
@@ -179,7 +179,6 @@ export type ProductPurchaseIOS = PurchaseCommon & {
179
179
  // Preferred naming
180
180
  export type PurchaseIOS = ProductPurchaseIOS;
181
181
 
182
-
183
182
  export type AppTransactionIOS = {
184
183
  appTransactionId?: string; // Only available in iOS 18.4+
185
184
  originalPlatform?: string; // Only available in iOS 18.4+
package/src/useIAP.ts CHANGED
@@ -100,16 +100,12 @@ type UseIap = {
100
100
  requestPurchaseOnPromotedProductIOS: () => Promise<void>;
101
101
  /** @deprecated Use requestPurchaseOnPromotedProductIOS instead */
102
102
  buyPromotedProductIOS: () => Promise<void>;
103
- getActiveSubscriptions: (
104
- subscriptionIds?: string[],
105
- ) => Promise<ActiveSubscription[]>;
103
+ getActiveSubscriptions: (subscriptionIds?: string[]) => Promise<void>;
106
104
  hasActiveSubscriptions: (subscriptionIds?: string[]) => Promise<boolean>;
107
105
  };
108
106
 
109
107
  export interface UseIAPOptions {
110
- onPurchaseSuccess?: (
111
- purchase: Purchase,
112
- ) => void;
108
+ onPurchaseSuccess?: (purchase: Purchase) => void;
113
109
  onPurchaseError?: (error: PurchaseError) => void;
114
110
  onSyncError?: (error: Error) => void;
115
111
  shouldAutoSyncPurchases?: boolean; // New option to control auto-syncing
@@ -125,12 +121,8 @@ export function useIAP(options?: UseIAPOptions): UseIap {
125
121
  const [products, setProducts] = useState<Product[]>([]);
126
122
  const [promotedProductsIOS] = useState<Purchase[]>([]);
127
123
  const [subscriptions, setSubscriptions] = useState<SubscriptionProduct[]>([]);
128
- const [purchaseHistories, setPurchaseHistories] = useState<Purchase[]>(
129
- [],
130
- );
131
- const [availablePurchases, setAvailablePurchases] = useState<
132
- Purchase[]
133
- >([]);
124
+ const [purchaseHistories, setPurchaseHistories] = useState<Purchase[]>([]);
125
+ const [availablePurchases, setAvailablePurchases] = useState<Purchase[]>([]);
134
126
  const [currentPurchase, setCurrentPurchase] = useState<Purchase>();
135
127
  const [promotedProductIOS, setPromotedProductIOS] = useState<Product>();
136
128
  const [currentPurchaseError, setCurrentPurchaseError] =
@@ -231,6 +223,7 @@ export function useIAP(options?: UseIAPOptions): UseIap {
231
223
  }): Promise<void> => {
232
224
  try {
233
225
  const result = await fetchProducts(params);
226
+
234
227
  if (params.type === 'subs') {
235
228
  setSubscriptions((prevSubscriptions) =>
236
229
  mergeWithDuplicateCheck(
@@ -261,7 +254,7 @@ export function useIAP(options?: UseIAPOptions): UseIap {
261
254
  type?: 'inapp' | 'subs';
262
255
  }): Promise<void> => {
263
256
  console.warn(
264
- "`requestProducts` is deprecated in useIAP hook. Use the new `fetchProducts` method instead. The 'request' prefix should only be used for event-based operations."
257
+ "`requestProducts` is deprecated in useIAP hook. Use the new `fetchProducts` method instead. The 'request' prefix should only be used for event-based operations.",
265
258
  );
266
259
  return fetchProductsInternal(params);
267
260
  },
@@ -270,7 +263,10 @@ export function useIAP(options?: UseIAPOptions): UseIap {
270
263
 
271
264
  const getAvailablePurchasesInternal = useCallback(async (): Promise<void> => {
272
265
  try {
273
- const result = await getAvailablePurchases();
266
+ const result = await getAvailablePurchases({
267
+ alsoPublishToEventListenerIOS: false,
268
+ onlyIncludeActiveItemsIOS: true,
269
+ });
274
270
  setAvailablePurchases(result);
275
271
  } catch (error) {
276
272
  console.error('Error fetching available purchases:', error);
@@ -278,16 +274,13 @@ export function useIAP(options?: UseIAPOptions): UseIap {
278
274
  }, []);
279
275
 
280
276
  const getActiveSubscriptionsInternal = useCallback(
281
- async (subscriptionIds?: string[]): Promise<ActiveSubscription[]> => {
277
+ async (subscriptionIds?: string[]): Promise<void> => {
282
278
  try {
283
279
  const result = await getActiveSubscriptions(subscriptionIds);
284
280
  setActiveSubscriptions(result);
285
- return result;
286
281
  } catch (error) {
287
282
  console.error('Error getting active subscriptions:', error);
288
- // Don't clear existing activeSubscriptions on error - preserve current state
289
- // This prevents the UI from showing empty state when there are temporary network issues
290
- return [];
283
+ // Preserve existing state on error
291
284
  }
292
285
  },
293
286
  [],
@@ -406,48 +399,76 @@ export function useIAP(options?: UseIAPOptions): UseIap {
406
399
  );
407
400
 
408
401
  const initIapWithSubscriptions = useCallback(async (): Promise<void> => {
409
- const result = await initConnection();
410
- setConnected(result);
402
+ // CRITICAL: Register listeners BEFORE initConnection to avoid race condition
403
+ // Events might fire immediately after initConnection, so listeners must be ready
404
+ console.log('[useIAP] Setting up event listeners BEFORE initConnection...');
405
+
406
+ // Register purchase update listener BEFORE initConnection to avoid race conditions.
407
+ subscriptionsRef.current.purchaseUpdate = purchaseUpdatedListener(
408
+ async (purchase: Purchase) => {
409
+ console.log('[useIAP] Purchase success callback triggered:', purchase);
410
+ setCurrentPurchaseError(undefined);
411
+ setCurrentPurchase(purchase);
412
+
413
+ if ('expirationDateIOS' in purchase) {
414
+ await refreshSubscriptionStatus(purchase.id);
415
+ }
411
416
 
412
- if (result) {
413
- subscriptionsRef.current.purchaseUpdate = purchaseUpdatedListener(
414
- async (purchase: Purchase) => {
415
- setCurrentPurchaseError(undefined);
416
- setCurrentPurchase(purchase);
417
+ if (optionsRef.current?.onPurchaseSuccess) {
418
+ optionsRef.current.onPurchaseSuccess(purchase);
419
+ }
420
+ },
421
+ );
417
422
 
418
- if ('expirationDateIOS' in purchase) {
419
- await refreshSubscriptionStatus(purchase.id);
420
- }
423
+ // IMPORTANT: Do NOT register the purchase error listener until after initConnection succeeds.
424
+ // Some platforms may emit an initialization error event (E_INIT_CONNECTION) during startup.
425
+ // Delaying registration prevents noisy, misleading errors before the connection is ready.
421
426
 
422
- if (optionsRef.current?.onPurchaseSuccess) {
423
- optionsRef.current.onPurchaseSuccess(purchase);
424
- }
425
- },
426
- );
427
-
428
- subscriptionsRef.current.purchaseError = purchaseErrorListener(
429
- (error: PurchaseError) => {
430
- setCurrentPurchase(undefined);
431
- setCurrentPurchaseError(error);
427
+ if (Platform.OS === 'ios') {
428
+ // iOS promoted products listener
429
+ subscriptionsRef.current.promotedProductsIOS = promotedProductListenerIOS(
430
+ (product: Product) => {
431
+ console.log('[useIAP] Promoted product callback triggered:', product);
432
+ setPromotedProductIOS(product);
432
433
 
433
- if (optionsRef.current?.onPurchaseError) {
434
- optionsRef.current.onPurchaseError(error);
434
+ if (optionsRef.current?.onPromotedProductIOS) {
435
+ optionsRef.current.onPromotedProductIOS(product);
435
436
  }
436
437
  },
437
438
  );
439
+ }
438
440
 
439
- if (Platform.OS === 'ios') {
440
- // iOS promoted products listener
441
- subscriptionsRef.current.promotedProductsIOS =
442
- promotedProductListenerIOS((product: Product) => {
443
- setPromotedProductIOS(product);
444
-
445
- if (optionsRef.current?.onPromotedProductIOS) {
446
- optionsRef.current.onPromotedProductIOS(product);
447
- }
448
- });
449
- }
441
+ console.log(
442
+ '[useIAP] Event listeners registered, now calling initConnection...',
443
+ );
444
+
445
+ // NOW call initConnection after listeners are ready
446
+ const result = await initConnection();
447
+ setConnected(result);
448
+ console.log('[useIAP] initConnection result:', result);
449
+ if (!result) {
450
+ // If connection failed, clean up listeners
451
+ console.warn('[useIAP] Connection failed, cleaning up listeners...');
452
+ subscriptionsRef.current.purchaseUpdate?.remove();
453
+ subscriptionsRef.current.promotedProductsIOS?.remove();
454
+ subscriptionsRef.current.purchaseUpdate = undefined;
455
+ subscriptionsRef.current.promotedProductsIOS = undefined;
456
+ // Do not register error listener when connection fails
457
+ return;
450
458
  }
459
+
460
+ // Now that the connection is established, register the purchase error listener.
461
+ subscriptionsRef.current.purchaseError = purchaseErrorListener(
462
+ (error: PurchaseError) => {
463
+ console.log('[useIAP] Purchase error callback triggered:', error);
464
+ setCurrentPurchase(undefined);
465
+ setCurrentPurchaseError(error);
466
+
467
+ if (optionsRef.current?.onPurchaseError) {
468
+ optionsRef.current.onPurchaseError(error);
469
+ }
470
+ },
471
+ );
451
472
  }, [refreshSubscriptionStatus]);
452
473
 
453
474
  useEffect(() => {
@@ -0,0 +1,14 @@
1
+ // Centralized product ID constants for examples and internal usage
2
+ // Rename guide: subscriptionIds -> SUBSCRIPTION_PRODUCT_IDS, PRODUCT_IDS remains the same name
3
+
4
+ // One-time purchase product IDs (consumables/non-consumables)
5
+ export const PRODUCT_IDS: string[] = [
6
+ 'dev.hyo.martie.10bulbs',
7
+ 'dev.hyo.martie.30bulbs',
8
+ ];
9
+
10
+ // Subscription product IDs
11
+ export const SUBSCRIPTION_PRODUCT_IDS: string[] = ['dev.hyo.martie.premium'];
12
+
13
+ // Optionally export a single default subscription for convenience
14
+ export const DEFAULT_SUBSCRIPTION_PRODUCT_ID = SUBSCRIPTION_PRODUCT_IDS[0];
@@ -1,27 +0,0 @@
1
- import Foundation
2
- import StoreKit
3
-
4
- @available(iOS 15.0, *)
5
- actor ProductStore {
6
- private(set) var products: [String: Product] = [:]
7
-
8
- func addProduct(_ product: Product) {
9
- self.products[product.id] = product
10
- }
11
-
12
- func getAllProducts() -> [Product] {
13
- return Array(self.products.values)
14
- }
15
-
16
- func getProduct(productID: String) -> Product? {
17
- return self.products[productID]
18
- }
19
-
20
- func removeAll() {
21
- products.removeAll()
22
- }
23
-
24
- func performOnActor(_ action: @escaping (isolated ProductStore) -> Void) async {
25
- action(self)
26
- }
27
- }
package/ios/Types.swift DELETED
@@ -1,96 +0,0 @@
1
- //
2
- // IapTypes.swift
3
- // RNIap
4
- //
5
- // Created by Andres Aguilar on 8/18/22.
6
- //
7
-
8
- import Foundation
9
- import StoreKit
10
-
11
- public enum StoreError: Error {
12
- case failedVerification
13
- }
14
-
15
- // Error codes for IAP operations - centralized error code management
16
- struct IapErrorCode {
17
- // Constants for code usage - safe pattern without force unwrapping
18
- static let unknown = "E_UNKNOWN"
19
- static let serviceError = "E_SERVICE_ERROR"
20
- static let userCancelled = "E_USER_CANCELLED"
21
- static let userError = "E_USER_ERROR"
22
- static let itemUnavailable = "E_ITEM_UNAVAILABLE"
23
- static let remoteError = "E_REMOTE_ERROR"
24
- static let networkError = "E_NETWORK_ERROR"
25
- static let receiptFailed = "E_RECEIPT_FAILED"
26
- static let receiptFinishedFailed = "E_RECEIPT_FINISHED_FAILED"
27
- static let notPrepared = "E_NOT_PREPARED"
28
- static let notEnded = "E_NOT_ENDED"
29
- static let alreadyOwned = "E_ALREADY_OWNED"
30
- static let developerError = "E_DEVELOPER_ERROR"
31
- static let purchaseError = "E_PURCHASE_ERROR"
32
- static let syncError = "E_SYNC_ERROR"
33
- static let deferredPayment = "E_DEFERRED_PAYMENT"
34
- static let transactionValidationFailed = "E_TRANSACTION_VALIDATION_FAILED"
35
- static let billingResponseJsonParseError = "E_BILLING_RESPONSE_JSON_PARSE_ERROR"
36
- static let interrupted = "E_INTERRUPTED"
37
- static let iapNotAvailable = "E_IAP_NOT_AVAILABLE"
38
- static let activityUnavailable = "E_ACTIVITY_UNAVAILABLE"
39
- static let alreadyPrepared = "E_ALREADY_PREPARED"
40
- static let pending = "E_PENDING"
41
- static let connectionClosed = "E_CONNECTION_CLOSED"
42
-
43
- // Cached dictionary for Constants export - using constants as keys to avoid duplication
44
- private static let _cachedDictionary: [String: String] = [
45
- unknown: unknown,
46
- serviceError: serviceError,
47
- userCancelled: userCancelled,
48
- userError: userError,
49
- itemUnavailable: itemUnavailable,
50
- remoteError: remoteError,
51
- networkError: networkError,
52
- receiptFailed: receiptFailed,
53
- receiptFinishedFailed: receiptFinishedFailed,
54
- notPrepared: notPrepared,
55
- notEnded: notEnded,
56
- alreadyOwned: alreadyOwned,
57
- developerError: developerError,
58
- purchaseError: purchaseError,
59
- syncError: syncError,
60
- deferredPayment: deferredPayment,
61
- transactionValidationFailed: transactionValidationFailed,
62
- billingResponseJsonParseError: billingResponseJsonParseError,
63
- interrupted: interrupted,
64
- iapNotAvailable: iapNotAvailable,
65
- activityUnavailable: activityUnavailable,
66
- alreadyPrepared: alreadyPrepared,
67
- pending: pending,
68
- connectionClosed: connectionClosed
69
- ]
70
-
71
- // Return cached dictionary - no allocation on repeated calls
72
- static func toDictionary() -> [String: String] {
73
- return _cachedDictionary
74
- }
75
- }
76
-
77
- // Based on https://stackoverflow.com/a/40135192/570612
78
- extension Date {
79
- var millisecondsSince1970: Int64 {
80
- return Int64((self.timeIntervalSince1970 * 1000.0).rounded())
81
- }
82
-
83
- var millisecondsSince1970String: String {
84
- return String(self.millisecondsSince1970)
85
- }
86
-
87
- init(milliseconds: Int64) {
88
- self = Date(timeIntervalSince1970: TimeInterval(milliseconds) / 1000)
89
- }
90
- }
91
-
92
- extension SKProductsRequest {
93
- var key: String {
94
- return String(self.hashValue)
95
- }
96
- }