expo-iap 3.1.38 → 3.2.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.
Files changed (43) hide show
  1. package/.prettierignore +1 -0
  2. package/README.md +1 -22
  3. package/android/src/main/java/expo/modules/iap/ExpoIapModule.kt +141 -13
  4. package/build/index.d.ts +15 -6
  5. package/build/index.d.ts.map +1 -1
  6. package/build/index.js +46 -29
  7. package/build/index.js.map +1 -1
  8. package/build/modules/android.d.ts +53 -1
  9. package/build/modules/android.d.ts.map +1 -1
  10. package/build/modules/android.js +61 -0
  11. package/build/modules/android.js.map +1 -1
  12. package/build/modules/ios.d.ts.map +1 -1
  13. package/build/modules/ios.js +4 -2
  14. package/build/modules/ios.js.map +1 -1
  15. package/build/types.d.ts +349 -20
  16. package/build/types.d.ts.map +1 -1
  17. package/build/types.js.map +1 -1
  18. package/build/useIAP.d.ts.map +1 -1
  19. package/build/useIAP.js +2 -0
  20. package/build/useIAP.js.map +1 -1
  21. package/coverage/clover.xml +176 -166
  22. package/coverage/coverage-final.json +3 -3
  23. package/coverage/lcov-report/index.html +23 -23
  24. package/coverage/lcov-report/src/index.html +14 -14
  25. package/coverage/lcov-report/src/index.ts.html +90 -39
  26. package/coverage/lcov-report/src/modules/android.ts.html +233 -8
  27. package/coverage/lcov-report/src/modules/index.html +15 -15
  28. package/coverage/lcov-report/src/modules/ios.ts.html +13 -7
  29. package/coverage/lcov-report/src/utils/debug.ts.html +1 -1
  30. package/coverage/lcov-report/src/utils/errorMapping.ts.html +1 -1
  31. package/coverage/lcov-report/src/utils/index.html +1 -1
  32. package/coverage/lcov.info +305 -284
  33. package/ios/ExpoIapModule.swift +1 -1
  34. package/openiap-versions.json +3 -3
  35. package/package.json +1 -1
  36. package/plugin/build/withIAP.d.ts +8 -5
  37. package/plugin/build/withIAP.js +12 -3
  38. package/plugin/src/withIAP.ts +18 -9
  39. package/src/index.ts +46 -29
  40. package/src/modules/android.ts +75 -0
  41. package/src/modules/ios.ts +4 -2
  42. package/src/types.ts +370 -21
  43. package/src/useIAP.ts +2 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "expo-iap",
3
- "version": "3.1.38",
3
+ "version": "3.2.1",
4
4
  "description": "In App Purchase module in Expo",
5
5
  "main": "build/index.js",
6
6
  "types": "build/index.d.ts",
@@ -1,6 +1,12 @@
1
1
  import { ConfigPlugin } from 'expo/config-plugins';
2
2
  import { withIosAlternativeBilling, type IOSAlternativeBillingConfig } from './withIosAlternativeBilling';
3
3
  export interface ExpoIapPluginOptions {
4
+ /**
5
+ * IAPKit API key for server-side receipt verification.
6
+ * Get your API key from https://iapkit.com
7
+ * This will be available via `Constants.expoConfig?.extra?.iapkitApiKey`
8
+ */
9
+ iapkitApiKey?: string;
4
10
  /** Local development path for OpenIAP library */
5
11
  localPath?: string | {
6
12
  ios?: string;
@@ -46,11 +52,8 @@ export interface ExpoIapPluginOptions {
46
52
  */
47
53
  horizonAppId?: string;
48
54
  };
49
- /** @deprecated Use ios.alternativeBilling instead */
50
- iosAlternativeBilling?: IOSAlternativeBillingConfig;
51
- /** @deprecated Use android.horizonAppId instead */
52
- horizonAppId?: string;
53
55
  }
54
- export { withIosAlternativeBilling };
56
+ declare const withIap: ConfigPlugin<ExpoIapPluginOptions | void>;
57
+ export { withIosAlternativeBilling, withIap };
55
58
  declare const _default: ConfigPlugin<void | ExpoIapPluginOptions>;
56
59
  export default _default;
@@ -36,7 +36,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
36
36
  return (mod && mod.__esModule) ? mod : { "default": mod };
37
37
  };
38
38
  Object.defineProperty(exports, "__esModule", { value: true });
39
- exports.withIosAlternativeBilling = void 0;
39
+ exports.withIap = exports.withIosAlternativeBilling = void 0;
40
40
  const config_plugins_1 = require("expo/config-plugins");
41
41
  const fs = __importStar(require("fs"));
42
42
  const path = __importStar(require("path"));
@@ -219,10 +219,18 @@ const withIapIOS = (config, options) => {
219
219
  };
220
220
  const withIap = (config, options) => {
221
221
  try {
222
+ // Add iapkitApiKey to extra if provided
223
+ if (options?.iapkitApiKey) {
224
+ config.extra = {
225
+ ...config.extra,
226
+ iapkitApiKey: options.iapkitApiKey,
227
+ };
228
+ logOnce('🔑 [expo-iap] Added iapkitApiKey to config.extra');
229
+ }
222
230
  // Read Horizon configuration from modules
223
231
  const isHorizonEnabled = options?.modules?.horizon ?? false;
224
- const horizonAppId = options?.android?.horizonAppId ?? options?.horizonAppId;
225
- const iosAlternativeBilling = options?.ios?.alternativeBilling ?? options?.iosAlternativeBilling;
232
+ const horizonAppId = options?.android?.horizonAppId;
233
+ const iosAlternativeBilling = options?.ios?.alternativeBilling;
226
234
  logOnce(`🔍 [expo-iap] Config values: horizonAppId=${horizonAppId}, isHorizonEnabled=${isHorizonEnabled}`);
227
235
  // Respect explicit flag; fall back to presence of localPath only when flag is unset
228
236
  const isLocalDev = options?.enableLocalDev ?? !!options?.localPath;
@@ -270,4 +278,5 @@ const withIap = (config, options) => {
270
278
  return config;
271
279
  }
272
280
  };
281
+ exports.withIap = withIap;
273
282
  exports.default = (0, config_plugins_1.createRunOncePlugin)(withIap, pkg.name, pkg.version);
@@ -289,6 +289,12 @@ const withIapIOS: ConfigPlugin<IOSAlternativeBillingConfig | undefined> = (
289
289
  };
290
290
 
291
291
  export interface ExpoIapPluginOptions {
292
+ /**
293
+ * IAPKit API key for server-side receipt verification.
294
+ * Get your API key from https://iapkit.com
295
+ * This will be available via `Constants.expoConfig?.extra?.iapkitApiKey`
296
+ */
297
+ iapkitApiKey?: string;
292
298
  /** Local development path for OpenIAP library */
293
299
  localPath?:
294
300
  | string
@@ -336,10 +342,6 @@ export interface ExpoIapPluginOptions {
336
342
  */
337
343
  horizonAppId?: string;
338
344
  };
339
- /** @deprecated Use ios.alternativeBilling instead */
340
- iosAlternativeBilling?: IOSAlternativeBillingConfig;
341
- /** @deprecated Use android.horizonAppId instead */
342
- horizonAppId?: string;
343
345
  }
344
346
 
345
347
  const withIap: ConfigPlugin<ExpoIapPluginOptions | void> = (
@@ -347,13 +349,20 @@ const withIap: ConfigPlugin<ExpoIapPluginOptions | void> = (
347
349
  options,
348
350
  ) => {
349
351
  try {
352
+ // Add iapkitApiKey to extra if provided
353
+ if (options?.iapkitApiKey) {
354
+ config.extra = {
355
+ ...config.extra,
356
+ iapkitApiKey: options.iapkitApiKey,
357
+ };
358
+ logOnce('🔑 [expo-iap] Added iapkitApiKey to config.extra');
359
+ }
360
+
350
361
  // Read Horizon configuration from modules
351
362
  const isHorizonEnabled = options?.modules?.horizon ?? false;
352
363
 
353
- const horizonAppId =
354
- options?.android?.horizonAppId ?? options?.horizonAppId;
355
- const iosAlternativeBilling =
356
- options?.ios?.alternativeBilling ?? options?.iosAlternativeBilling;
364
+ const horizonAppId = options?.android?.horizonAppId;
365
+ const iosAlternativeBilling = options?.ios?.alternativeBilling;
357
366
 
358
367
  logOnce(
359
368
  `🔍 [expo-iap] Config values: horizonAppId=${horizonAppId}, isHorizonEnabled=${isHorizonEnabled}`,
@@ -416,5 +425,5 @@ const withIap: ConfigPlugin<ExpoIapPluginOptions | void> = (
416
425
  }
417
426
  };
418
427
 
419
- export {withIosAlternativeBilling};
428
+ export {withIosAlternativeBilling, withIap};
420
429
  export default createRunOncePlugin(withIap, pkg.name, pkg.version);
package/src/index.ts CHANGED
@@ -424,24 +424,28 @@ function normalizeRequestProps(
424
424
  | RequestSubscriptionPropsByPlatforms,
425
425
  platform: 'ios' | 'android',
426
426
  ) {
427
- // Platform-specific format - directly return the appropriate platform data
428
- return platform === 'ios' ? request.ios : request.android;
427
+ // Support both new (apple/google) and legacy (ios/android) field names
428
+ // New fields take precedence over deprecated ones
429
+ if (platform === 'ios') {
430
+ return request.apple ?? request.ios;
431
+ }
432
+ return request.google ?? request.android;
429
433
  }
430
434
 
431
435
  /**
432
436
  * Request a purchase for products or subscriptions.
433
437
  *
434
438
  * @param requestObj - Purchase request configuration
435
- * @param requestObj.request - Platform-specific purchase parameters
439
+ * @param requestObj.request - Store-specific purchase parameters
436
440
  * @param requestObj.type - Type of purchase: 'in-app' for products (default) or 'subs' for subscriptions
437
441
  *
438
442
  * @example
439
443
  * ```typescript
440
- * // Product purchase
444
+ * // Product purchase (recommended: use apple/google)
441
445
  * await requestPurchase({
442
446
  * request: {
443
- * ios: { sku: productId },
444
- * android: { skus: [productId] }
447
+ * apple: { sku: productId },
448
+ * google: { skus: [productId] }
445
449
  * },
446
450
  * type: 'in-app'
447
451
  * });
@@ -449,14 +453,23 @@ function normalizeRequestProps(
449
453
  * // Subscription purchase
450
454
  * await requestPurchase({
451
455
  * request: {
452
- * ios: { sku: subscriptionId },
453
- * android: {
456
+ * apple: { sku: subscriptionId },
457
+ * google: {
454
458
  * skus: [subscriptionId],
455
459
  * subscriptionOffers: [{ sku: subscriptionId, offerToken: 'token' }]
456
460
  * }
457
461
  * },
458
462
  * type: 'subs'
459
463
  * });
464
+ *
465
+ * // Legacy format (deprecated, but still supported)
466
+ * await requestPurchase({
467
+ * request: {
468
+ * ios: { sku: productId },
469
+ * android: { skus: [productId] }
470
+ * },
471
+ * type: 'in-app'
472
+ * });
460
473
  * ```
461
474
  */
462
475
  export const requestPurchase: MutationField<'requestPurchase'> = async (
@@ -471,12 +484,12 @@ export const requestPurchase: MutationField<'requestPurchase'> = async (
471
484
 
472
485
  if (!normalizedRequest?.sku) {
473
486
  throw new Error(
474
- 'Invalid request for iOS. The `sku` property is required and must be a string.\n\n' +
487
+ 'Invalid request for Apple. The `sku` property is required and must be a string.\n\n' +
475
488
  'Expected format:\n' +
476
489
  ' requestPurchase({\n' +
477
490
  ' request: {\n' +
478
- ' android: { skus: ["product_id"] },\n' +
479
- ' ios: { sku: "product_id" }\n' +
491
+ ' apple: { sku: "product_id" },\n' +
492
+ ' google: { skus: ["product_id"] }\n' +
480
493
  ' },\n' +
481
494
  ' type: "in-app"\n' +
482
495
  ' })\n\n' +
@@ -519,12 +532,12 @@ export const requestPurchase: MutationField<'requestPurchase'> = async (
519
532
 
520
533
  if (!normalizedRequest?.skus?.length) {
521
534
  throw new Error(
522
- 'Invalid request for Android. The `skus` property is required and must be a non-empty array.\n\n' +
535
+ 'Invalid request for Google. The `skus` property is required and must be a non-empty array.\n\n' +
523
536
  'Expected format:\n' +
524
537
  ' requestPurchase({\n' +
525
538
  ' request: {\n' +
526
- ' android: { skus: ["product_id"] },\n' +
527
- ' ios: { sku: "product_id" }\n' +
539
+ ' apple: { sku: "product_id" },\n' +
540
+ ' google: { skus: ["product_id"] }\n' +
528
541
  ' },\n' +
529
542
  ' type: "in-app"\n' +
530
543
  ' })\n\n' +
@@ -561,12 +574,12 @@ export const requestPurchase: MutationField<'requestPurchase'> = async (
561
574
 
562
575
  if (!normalizedRequest?.skus?.length) {
563
576
  throw new Error(
564
- 'Invalid request for Android. The `skus` property is required and must be a non-empty array.\n\n' +
577
+ 'Invalid request for Google. The `skus` property is required and must be a non-empty array.\n\n' +
565
578
  'Expected format:\n' +
566
579
  ' requestPurchase({\n' +
567
580
  ' request: {\n' +
568
- ' android: { skus: ["subscription_id"] },\n' +
569
- ' ios: { sku: "subscription_id" }\n' +
581
+ ' apple: { sku: "subscription_id" },\n' +
582
+ ' google: { skus: ["subscription_id"] }\n' +
570
583
  ' },\n' +
571
584
  ' type: "subs"\n' +
572
585
  ' })\n\n' +
@@ -714,29 +727,33 @@ export const deepLinkToSubscriptions: MutationField<
714
727
  export const validateReceipt: MutationField<'validateReceipt'> = async (
715
728
  options,
716
729
  ) => {
717
- const {sku, androidOptions} = options as MutationValidateReceiptArgs;
730
+ const {apple, google} = options as MutationValidateReceiptArgs;
718
731
 
719
732
  if (Platform.OS === 'ios') {
720
- return validateReceiptIOS({sku});
733
+ if (!apple?.sku) {
734
+ throw new Error('iOS validation requires apple.sku');
735
+ }
736
+ return validateReceiptIOS({apple: {sku: apple.sku}});
721
737
  }
722
738
 
723
739
  if (Platform.OS === 'android') {
724
740
  if (
725
- !androidOptions ||
726
- !androidOptions.packageName ||
727
- !androidOptions.productToken ||
728
- !androidOptions.accessToken
741
+ !google ||
742
+ !google.sku ||
743
+ !google.packageName ||
744
+ !google.purchaseToken ||
745
+ !google.accessToken
729
746
  ) {
730
747
  throw new Error(
731
- 'Android validation requires packageName, productToken, and accessToken',
748
+ 'Android validation requires google.sku, google.packageName, google.purchaseToken, and google.accessToken',
732
749
  );
733
750
  }
734
751
  return validateReceiptAndroid({
735
- packageName: androidOptions.packageName,
736
- productId: sku,
737
- productToken: androidOptions.productToken,
738
- accessToken: androidOptions.accessToken,
739
- isSub: androidOptions.isSub ?? undefined,
752
+ packageName: google.packageName,
753
+ productId: google.sku,
754
+ productToken: google.purchaseToken,
755
+ accessToken: google.accessToken,
756
+ isSub: google.isSub ?? undefined,
740
757
  });
741
758
  }
742
759
 
@@ -6,7 +6,11 @@ import ExpoIapModule from '../ExpoIapModule';
6
6
 
7
7
  // Types
8
8
  import type {
9
+ BillingProgramAndroid,
10
+ BillingProgramAvailabilityResultAndroid,
11
+ BillingProgramReportingDetailsAndroid,
9
12
  DeepLinkOptions,
13
+ LaunchExternalLinkParamsAndroid,
10
14
  MutationField,
11
15
  VerifyPurchaseResultAndroid,
12
16
  } from '../types';
@@ -248,3 +252,74 @@ export const createAlternativeBillingTokenAndroid: MutationField<
248
252
  > = async (sku?: string) => {
249
253
  return ExpoIapModule.createAlternativeBillingTokenAndroid(sku);
250
254
  };
255
+
256
+ // ============================================================================
257
+ // Billing Programs API (Google Play Billing Library 8.2.0+)
258
+ // ============================================================================
259
+
260
+ /**
261
+ * Check if a specific billing program is available for this user/device (Android only).
262
+ * Available in Google Play Billing Library 8.2.0+.
263
+ *
264
+ * @param program - The billing program to check ('external-offer' or 'external-content-link')
265
+ * @returns Promise resolving to availability result
266
+ *
267
+ * @example
268
+ * ```typescript
269
+ * const result = await isBillingProgramAvailableAndroid('external-offer');
270
+ * if (result.isAvailable) {
271
+ * // Proceed with billing program flow
272
+ * }
273
+ * ```
274
+ */
275
+ export const isBillingProgramAvailableAndroid = async (
276
+ program: BillingProgramAndroid,
277
+ ): Promise<BillingProgramAvailabilityResultAndroid> => {
278
+ return ExpoIapModule.isBillingProgramAvailableAndroid(program);
279
+ };
280
+
281
+ /**
282
+ * Launch an external link for the specified billing program (Android only).
283
+ * Available in Google Play Billing Library 8.2.0+.
284
+ *
285
+ * @param params - The external link parameters
286
+ * @returns Promise resolving when the link is launched
287
+ *
288
+ * @example
289
+ * ```typescript
290
+ * await launchExternalLinkAndroid({
291
+ * billingProgram: 'external-offer',
292
+ * launchMode: 'launch-in-external-browser-or-app',
293
+ * linkType: 'link-to-digital-content-offer',
294
+ * linkUri: 'https://your-payment-site.com',
295
+ * });
296
+ * ```
297
+ */
298
+ export const launchExternalLinkAndroid = async (
299
+ params: LaunchExternalLinkParamsAndroid,
300
+ ): Promise<void> => {
301
+ return ExpoIapModule.launchExternalLinkAndroid(params);
302
+ };
303
+
304
+ /**
305
+ * Create billing program reporting details for Google Play reporting (Android only).
306
+ * Available in Google Play Billing Library 8.2.0+.
307
+ *
308
+ * Must be called AFTER successful payment in your payment system.
309
+ * Token must be reported to Google Play backend within 24 hours.
310
+ *
311
+ * @param program - The billing program type
312
+ * @returns Promise resolving to reporting details including the external transaction token
313
+ *
314
+ * @example
315
+ * ```typescript
316
+ * const details = await createBillingProgramReportingDetailsAndroid('external-offer');
317
+ * // Report details.externalTransactionToken to Google Play within 24 hours
318
+ * await reportToGooglePlay(details.externalTransactionToken);
319
+ * ```
320
+ */
321
+ export const createBillingProgramReportingDetailsAndroid = async (
322
+ program: BillingProgramAndroid,
323
+ ): Promise<BillingProgramReportingDetailsAndroid> => {
324
+ return ExpoIapModule.createBillingProgramReportingDetailsAndroid(program);
325
+ };
@@ -237,10 +237,12 @@ export const getTransactionJwsIOS: QueryField<'getTransactionJwsIOS'> = async (
237
237
  */
238
238
  const validateReceiptIOSImpl = async (props: VerifyPurchaseProps | string) => {
239
239
  const sku =
240
- typeof props === 'string' ? props : (props as VerifyPurchaseProps)?.sku;
240
+ typeof props === 'string'
241
+ ? props
242
+ : (props as VerifyPurchaseProps)?.apple?.sku;
241
243
 
242
244
  if (!sku) {
243
- throw new Error('validateReceiptIOS requires a SKU');
245
+ throw new Error('validateReceiptIOS requires a SKU (via apple.sku)');
244
246
  }
245
247
 
246
248
  return (await ExpoIapModule.validateReceiptIOS(