expo-iap 2.7.4 → 2.7.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.
@@ -18,6 +18,7 @@ func logDebug(_ message: String) {
18
18
  struct IapEvent {
19
19
  static let PurchaseUpdated = "purchase-updated"
20
20
  static let PurchaseError = "purchase-error"
21
+ static let PromotedProductIOS = "promoted-product-ios"
21
22
  }
22
23
 
23
24
  @available(iOS 15.0, *)
@@ -225,6 +226,9 @@ public class ExpoIapModule: Module {
225
226
  private var updateListenerTask: Task<Void, Error>?
226
227
  private var subscriptionPollingTask: Task<Void, Error>?
227
228
  private var pollingSkus: Set<String> = []
229
+ private var paymentObserver: PaymentObserver?
230
+ private var promotedPayment: SKPayment?
231
+ private var promotedProduct: SKProduct?
228
232
 
229
233
  public func definition() -> ModuleDefinition {
230
234
  Name("ExpoIap")
@@ -247,6 +251,13 @@ public class ExpoIapModule: Module {
247
251
 
248
252
  Function("initConnection") { () -> Bool in
249
253
  self.productStore = ProductStore()
254
+
255
+ // Set up PaymentObserver for promoted products
256
+ if self.paymentObserver == nil {
257
+ self.paymentObserver = PaymentObserver(module: self)
258
+ SKPaymentQueue.default().add(self.paymentObserver!)
259
+ }
260
+
250
261
  return AppStore.canMakePayments
251
262
  }
252
263
 
@@ -303,6 +314,42 @@ public class ExpoIapModule: Module {
303
314
  )
304
315
  }
305
316
  }
317
+
318
+ AsyncFunction("getPromotedProduct") { () -> [String: Any?]? in
319
+ guard let product = self.promotedProduct else {
320
+ return nil
321
+ }
322
+
323
+ // Convert SKProduct to dictionary
324
+ return [
325
+ "productIdentifier": product.productIdentifier,
326
+ "localizedTitle": product.localizedTitle,
327
+ "localizedDescription": product.localizedDescription,
328
+ "price": product.price.doubleValue,
329
+ "priceLocale": [
330
+ "currencyCode": product.priceLocale.currencyCode ?? "",
331
+ "currencySymbol": product.priceLocale.currencySymbol ?? "",
332
+ "countryCode": product.priceLocale.regionCode ?? ""
333
+ ]
334
+ ]
335
+ }
336
+
337
+ AsyncFunction("buyPromotedProduct") { () -> Void in
338
+ guard let payment = self.promotedPayment else {
339
+ throw Exception(
340
+ name: "ExpoIapModule",
341
+ description: "No promoted product available",
342
+ code: IapErrorCode.itemUnavailable
343
+ )
344
+ }
345
+
346
+ // Add the deferred payment to the queue
347
+ SKPaymentQueue.default().add(payment)
348
+
349
+ // Clear the promoted product data
350
+ self.promotedPayment = nil
351
+ self.promotedProduct = nil
352
+ }
306
353
 
307
354
  AsyncFunction("getItems") { (skus: [String]) -> [[String: Any?]?] in
308
355
  guard let productStore = self.productStore else {
@@ -335,6 +382,13 @@ public class ExpoIapModule: Module {
335
382
  self.transactions.removeAll()
336
383
  self.productStore = nil
337
384
  self.removeTransactionObserver()
385
+
386
+ // Remove PaymentObserver
387
+ if let observer = self.paymentObserver {
388
+ SKPaymentQueue.default().remove(observer)
389
+ self.paymentObserver = nil
390
+ }
391
+
338
392
  return true
339
393
  }
340
394
 
@@ -921,4 +975,47 @@ public class ExpoIapModule: Module {
921
975
  throw Exception(name: "ExpoIapModule", description: "App Store receipt not found", code: IapErrorCode.receiptFailed)
922
976
  }
923
977
  }
978
+
979
+ // Called by PaymentObserver when a promoted product is received
980
+ func handlePromotedProduct(payment: SKPayment, product: SKProduct) {
981
+ self.promotedPayment = payment
982
+ self.promotedProduct = product
983
+
984
+ if hasListeners {
985
+ let productData: [String: Any] = [
986
+ "productIdentifier": product.productIdentifier,
987
+ "localizedTitle": product.localizedTitle,
988
+ "localizedDescription": product.localizedDescription,
989
+ "price": product.price.doubleValue,
990
+ "priceLocale": [
991
+ "currencyCode": product.priceLocale.currencyCode ?? "",
992
+ "currencySymbol": product.priceLocale.currencySymbol ?? "",
993
+ "countryCode": product.priceLocale.regionCode ?? ""
994
+ ]
995
+ ]
996
+ sendEvent(IapEvent.PromotedProductIOS, productData)
997
+ }
998
+ }
999
+ }
1000
+
1001
+ // PaymentObserver for handling promoted products
1002
+ @available(iOS 15.0, *)
1003
+ class PaymentObserver: NSObject, SKPaymentTransactionObserver {
1004
+ weak var module: ExpoIapModule?
1005
+
1006
+ init(module: ExpoIapModule) {
1007
+ self.module = module
1008
+ }
1009
+
1010
+ // Required by SKPaymentTransactionObserver protocol but not used
1011
+ func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
1012
+ // We don't handle transactions here as StoreKit 2 handles them in ExpoIapModule
1013
+ }
1014
+
1015
+ // Handle promoted products from App Store
1016
+ func paymentQueue(_ queue: SKPaymentQueue, shouldAddStorePayment payment: SKPayment, for product: SKProduct) -> Bool {
1017
+ module?.handlePromotedProduct(payment: payment, product: product)
1018
+ // Return false to defer the payment
1019
+ return false
1020
+ }
924
1021
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "expo-iap",
3
- "version": "2.7.4",
3
+ "version": "2.7.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",
@@ -1 +1 @@
1
- {"root":["./src/withIAP.ts"],"version":"5.8.3"}
1
+ {"root":["./src/withiap.ts"],"version":"5.8.3"}
@@ -365,8 +365,7 @@ export interface AndroidRequestPurchaseProps {
365
365
  /**
366
366
  * Android-specific subscription request parameters
367
367
  */
368
- export interface AndroidRequestSubscriptionProps
369
- extends AndroidRequestPurchaseProps {
368
+ export interface AndroidRequestSubscriptionProps extends AndroidRequestPurchaseProps {
370
369
  readonly purchaseTokenAndroid?: string;
371
370
  readonly replacementModeAndroid?: number;
372
371
  readonly subscriptionOffers: {
@@ -409,36 +408,28 @@ export type RequestSubscriptionProps = PlatformRequestSubscriptionProps;
409
408
  * Includes both unified and old platform-specific formats
410
409
  * @deprecated Use RequestPurchaseProps with platform-specific structure instead
411
410
  */
412
- export type LegacyRequestPurchasePropsAll =
413
- | UnifiedRequestPurchaseProps
414
- | LegacyRequestPurchaseProps;
411
+ export type LegacyRequestPurchasePropsAll = UnifiedRequestPurchaseProps | LegacyRequestPurchaseProps;
415
412
 
416
413
  /**
417
414
  * Legacy request subscription parameters (deprecated)
418
415
  * Includes both unified and old platform-specific formats
419
416
  * @deprecated Use RequestSubscriptionProps with platform-specific structure instead
420
417
  */
421
- export type LegacyRequestSubscriptionPropsAll =
422
- | UnifiedRequestSubscriptionProps
423
- | LegacyRequestSubscriptionProps;
418
+ export type LegacyRequestSubscriptionPropsAll = UnifiedRequestSubscriptionProps | LegacyRequestSubscriptionProps;
424
419
 
425
420
  /**
426
421
  * All supported request purchase parameters
427
422
  * Used internally for backward compatibility
428
423
  * @internal
429
424
  */
430
- export type RequestPurchasePropsWithLegacy =
431
- | RequestPurchaseProps
432
- | LegacyRequestPurchasePropsAll;
425
+ export type RequestPurchasePropsWithLegacy = RequestPurchaseProps | LegacyRequestPurchasePropsAll;
433
426
 
434
427
  /**
435
428
  * All supported request subscription parameters
436
429
  * Used internally for backward compatibility
437
430
  * @internal
438
431
  */
439
- export type RequestSubscriptionPropsWithLegacy =
440
- | RequestSubscriptionProps
441
- | LegacyRequestSubscriptionPropsAll;
432
+ export type RequestSubscriptionPropsWithLegacy = RequestSubscriptionProps | LegacyRequestSubscriptionPropsAll;
442
433
 
443
434
  // ============================================================================
444
435
  // Type Guards and Utility Functions
@@ -446,19 +437,19 @@ export type RequestSubscriptionPropsWithLegacy =
446
437
 
447
438
  // Type guards to check which API style is being used
448
439
  export function isPlatformRequestProps(
449
- props: RequestPurchasePropsWithLegacy | RequestSubscriptionPropsWithLegacy,
440
+ props: RequestPurchasePropsWithLegacy | RequestSubscriptionPropsWithLegacy
450
441
  ): props is PlatformRequestPurchaseProps | PlatformRequestSubscriptionProps {
451
442
  return 'ios' in props || 'android' in props;
452
443
  }
453
444
 
454
445
  export function isUnifiedRequestProps(
455
- props: RequestPurchasePropsWithLegacy | RequestSubscriptionPropsWithLegacy,
446
+ props: RequestPurchasePropsWithLegacy | RequestSubscriptionPropsWithLegacy
456
447
  ): props is UnifiedRequestPurchaseProps | UnifiedRequestSubscriptionProps {
457
448
  return 'sku' in props || 'skus' in props;
458
449
  }
459
450
 
460
451
  export function isLegacyRequestProps(
461
- props: RequestPurchasePropsWithLegacy | RequestSubscriptionPropsWithLegacy,
452
+ props: RequestPurchasePropsWithLegacy | RequestSubscriptionPropsWithLegacy
462
453
  ): props is LegacyRequestPurchaseProps | LegacyRequestSubscriptionProps {
463
454
  return 'productId' in props || 'productIds' in props;
464
455
  }
package/src/index.ts CHANGED
@@ -21,8 +21,12 @@ import {
21
21
  isPlatformRequestProps,
22
22
  isUnifiedRequestProps,
23
23
  } from './ExpoIap.types';
24
- import {ProductPurchaseAndroid} from './types/ExpoIapAndroid.types';
25
- import {PaymentDiscount} from './types/ExpoIapIos.types';
24
+ import {
25
+ ProductPurchaseAndroid,
26
+ } from './types/ExpoIapAndroid.types';
27
+ import {
28
+ PaymentDiscount,
29
+ } from './types/ExpoIapIos.types';
26
30
 
27
31
  // Export all types
28
32
  export * from './ExpoIap.types';
@@ -38,6 +42,7 @@ export enum IapEvent {
38
42
  PurchaseError = 'purchase-error',
39
43
  /** @deprecated Use PurchaseUpdated instead. This will be removed in a future version. */
40
44
  TransactionIapUpdated = 'iap-transaction-updated',
45
+ PromotedProductIOS = 'promoted-product-ios',
41
46
  }
42
47
 
43
48
  export function setValueAsync(value: string) {
@@ -72,13 +77,43 @@ export const purchaseErrorListener = (
72
77
  return emitter.addListener(IapEvent.PurchaseError, listener);
73
78
  };
74
79
 
80
+ /**
81
+ * iOS-only listener for App Store promoted product events.
82
+ * This fires when a user taps on a promoted product in the App Store.
83
+ *
84
+ * @param listener - Callback function that receives the promoted product details
85
+ * @returns EventSubscription that can be used to unsubscribe
86
+ *
87
+ * @example
88
+ * ```typescript
89
+ * const subscription = promotedProductListenerIOS((product) => {
90
+ * console.log('Promoted product:', product);
91
+ * // Handle the promoted product
92
+ * });
93
+ *
94
+ * // Later, clean up
95
+ * subscription.remove();
96
+ * ```
97
+ *
98
+ * @platform iOS
99
+ */
100
+ export const promotedProductListenerIOS = (
101
+ listener: (product: Product) => void,
102
+ ) => {
103
+ if (Platform.OS !== 'ios') {
104
+ console.warn('promotedProductListenerIOS: This listener is only available on iOS');
105
+ return { remove: () => {} };
106
+ }
107
+ return emitter.addListener(IapEvent.PromotedProductIOS, listener);
108
+ };
109
+
75
110
  export function initConnection() {
76
111
  return ExpoIapModule.initConnection();
77
112
  }
78
113
 
79
114
  export const getProducts = async (skus: string[]): Promise<Product[]> => {
80
115
  console.warn(
81
- "`getProducts` is deprecated. Use `requestProducts({ skus, type: 'inapp' })` instead. This function will be removed in version 3.0.0.",
116
+ '`getProducts` is deprecated. Use `requestProducts({ skus, type: \'inapp\' })` instead. This function will be removed in version 3.0.0.',
82
117
  );
83
118
  if (!skus?.length) {
84
119
  return Promise.reject(new Error('"skus" is required'));
@@ -112,7 +147,7 @@ export const getSubscriptions = async (
112
147
  skus: string[],
113
148
  ): Promise<SubscriptionProduct[]> => {
114
149
  console.warn(
115
- "`getSubscriptions` is deprecated. Use `requestProducts({ skus, type: 'subs' })` instead. This function will be removed in version 3.0.0.",
150
+ '`getSubscriptions` is deprecated. Use `requestProducts({ skus, type: \'subs\' })` instead. This function will be removed in version 3.0.0.',
116
151
  );
117
152
  if (!skus?.length) {
118
153
  return Promise.reject(new Error('"skus" is required'));
@@ -236,7 +271,7 @@ export const getPurchaseHistory = ({
236
271
  onlyIncludeActiveItems?: boolean;
237
272
  } = {}): Promise<ProductPurchase[]> => {
238
273
  console.warn(
239
- '`getPurchaseHistory` is deprecated. Use `getPurchaseHistories` instead. This function will be removed in version 3.0.0.',
274
+ "`getPurchaseHistory` is deprecated. Use `getPurchaseHistories` instead. This function will be removed in version 3.0.0.",
240
275
  );
241
276
  return getPurchaseHistories({
242
277
  alsoPublishToEventListener,
@@ -286,8 +321,9 @@ export const getAvailablePurchases = ({
286
321
  ),
287
322
  android: async () => {
288
323
  const products = await ExpoIapModule.getAvailableItemsByType('inapp');
289
- const subscriptions =
290
- await ExpoIapModule.getAvailableItemsByType('subs');
324
+ const subscriptions = await ExpoIapModule.getAvailableItemsByType(
325
+ 'subs',
326
+ );
291
327
  return products.concat(subscriptions);
292
328
  },
293
329
  }) || (() => Promise.resolve([]))
@@ -306,6 +342,7 @@ const offerToRecordIos = (
306
342
  };
307
343
  };
308
344
 
345
+
309
346
  // Define discriminated union with explicit type parameter
310
347
  // Using legacy types internally for backward compatibility
311
348
  type PurchaseRequest =
@@ -335,8 +372,7 @@ const normalizeRequestProps = (
335
372
  if (platform === 'ios') {
336
373
  return {
337
374
  sku: request.sku || (request.skus?.[0] ?? ''),
338
- andDangerouslyFinishTransactionAutomaticallyIOS:
339
- request.andDangerouslyFinishTransactionAutomaticallyIOS,
375
+ andDangerouslyFinishTransactionAutomaticallyIOS: request.andDangerouslyFinishTransactionAutomaticallyIOS,
340
376
  appAccountToken: request.appAccountToken,
341
377
  quantity: request.quantity,
342
378
  withOffer: request.withOffer,
@@ -348,7 +384,7 @@ const normalizeRequestProps = (
348
384
  obfuscatedProfileIdAndroid: request.obfuscatedProfileIdAndroid,
349
385
  isOfferPersonalized: request.isOfferPersonalized,
350
386
  };
351
-
387
+
352
388
  // Add subscription-specific fields if present
353
389
  if ('subscriptionOffers' in request && request.subscriptionOffers) {
354
390
  androidRequest.subscriptionOffers = request.subscriptionOffers;
@@ -359,7 +395,7 @@ const normalizeRequestProps = (
359
395
  if ('replacementModeAndroid' in request) {
360
396
  androidRequest.replacementModeAndroid = request.replacementModeAndroid;
361
397
  }
362
-
398
+
363
399
  return androidRequest;
364
400
  }
365
401
  }
@@ -370,11 +406,11 @@ const normalizeRequestProps = (
370
406
 
371
407
  /**
372
408
  * Request a purchase for products or subscriptions.
373
- *
409
+ *
374
410
  * @param requestObj - Purchase request configuration
375
411
  * @param requestObj.request - Platform-specific purchase parameters
376
412
  * @param requestObj.type - Type of purchase: 'inapp' for products (default) or 'subs' for subscriptions
377
- *
413
+ *
378
414
  * @example
379
415
  * ```typescript
380
416
  * // Product purchase
@@ -385,12 +421,12 @@ const normalizeRequestProps = (
385
421
  * },
386
422
  * type: 'inapp'
387
423
  * });
388
- *
424
+ *
389
425
  * // Subscription purchase
390
426
  * await requestPurchase({
391
427
  * request: {
392
428
  * ios: { sku: subscriptionId },
393
- * android: {
429
+ * android: {
394
430
  * skus: [subscriptionId],
395
431
  * subscriptionOffers: [{ sku: subscriptionId, offerToken: 'token' }]
396
432
  * }
@@ -412,7 +448,7 @@ export const requestPurchase = (
412
448
 
413
449
  if (Platform.OS === 'ios') {
414
450
  const normalizedRequest = normalizeRequestProps(request, 'ios');
415
-
451
+
416
452
  if (!normalizedRequest?.sku) {
417
453
  throw new Error(
418
454
  'Invalid request for iOS. The `sku` property is required and must be a string.',
@@ -445,7 +481,7 @@ export const requestPurchase = (
445
481
 
446
482
  if (Platform.OS === 'android') {
447
483
  const normalizedRequest = normalizeRequestProps(request, 'android');
448
-
484
+
449
485
  if (!normalizedRequest?.skus?.length) {
450
486
  throw new Error(
451
487
  'Invalid request for Android. The `skus` property is required and must be a non-empty array.',
@@ -509,7 +545,7 @@ export const requestPurchase = (
509
545
 
510
546
  /**
511
547
  * @deprecated Use `requestPurchase({ request, type: 'subs' })` instead. This method will be removed in version 3.0.0.
512
- *
548
+ *
513
549
  * @example
514
550
  * ```typescript
515
551
  * // Old way (deprecated)
@@ -518,12 +554,12 @@ export const requestPurchase = (
518
554
  * // or for Android
519
555
  * skus: [subscriptionId],
520
556
  * });
521
- *
557
+ *
522
558
  * // New way (recommended)
523
559
  * await requestPurchase({
524
560
  * request: {
525
561
  * ios: { sku: subscriptionId },
526
- * android: {
562
+ * android: {
527
563
  * skus: [subscriptionId],
528
564
  * subscriptionOffers: [{ sku: subscriptionId, offerToken: 'token' }]
529
565
  * }
@@ -588,16 +624,16 @@ export const finishTransaction = ({
588
624
 
589
625
  /**
590
626
  * Retrieves the current storefront information from iOS App Store
591
- *
627
+ *
592
628
  * @returns Promise resolving to the storefront country code
593
629
  * @throws Error if called on non-iOS platform
594
- *
630
+ *
595
631
  * @example
596
632
  * ```typescript
597
633
  * const storefront = await getStorefrontIOS();
598
634
  * console.log(storefront); // 'US'
599
635
  * ```
600
- *
636
+ *
601
637
  * @platform iOS
602
638
  */
603
639
  export const getStorefrontIOS = (): Promise<string> => {
@@ -102,9 +102,11 @@ export const acknowledgePurchaseAndroid = ({
102
102
  * Open the Google Play Store to redeem offer codes (Android only).
103
103
  * Note: Google Play does not provide a direct API to redeem codes within the app.
104
104
  * This function opens the Play Store where users can manually enter their codes.
105
- *
105
+ *
106
106
  * @returns {Promise<void>}
107
107
  */
108
108
  export const openRedeemOfferCodeAndroid = async (): Promise<void> => {
109
- return Linking.openURL(`https://play.google.com/redeem?code=`);
109
+ return Linking.openURL(
110
+ `https://play.google.com/redeem?code=`
111
+ );
110
112
  };