expo-iap 3.2.0 → 3.2.2

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 (40) hide show
  1. package/CLAUDE.md +10 -4
  2. package/README.md +2 -23
  3. package/android/src/main/java/expo/modules/iap/ExpoIapModule.kt +141 -13
  4. package/build/index.d.ts.map +1 -1
  5. package/build/index.js +40 -12
  6. package/build/index.js.map +1 -1
  7. package/build/modules/android.d.ts +53 -1
  8. package/build/modules/android.d.ts.map +1 -1
  9. package/build/modules/android.js +61 -0
  10. package/build/modules/android.js.map +1 -1
  11. package/build/modules/ios.d.ts.map +1 -1
  12. package/build/modules/ios.js +4 -2
  13. package/build/modules/ios.js.map +1 -1
  14. package/build/types.d.ts +321 -24
  15. package/build/types.d.ts.map +1 -1
  16. package/build/types.js.map +1 -1
  17. package/coverage/clover.xml +115 -100
  18. package/coverage/coverage-final.json +3 -3
  19. package/coverage/lcov-report/index.html +26 -26
  20. package/coverage/lcov-report/src/index.html +19 -19
  21. package/coverage/lcov-report/src/index.ts.html +117 -24
  22. package/coverage/lcov-report/src/modules/android.ts.html +233 -8
  23. package/coverage/lcov-report/src/modules/index.html +15 -15
  24. package/coverage/lcov-report/src/modules/ios.ts.html +13 -7
  25. package/coverage/lcov-report/src/utils/debug.ts.html +1 -1
  26. package/coverage/lcov-report/src/utils/errorMapping.ts.html +1 -1
  27. package/coverage/lcov-report/src/utils/index.html +1 -1
  28. package/coverage/lcov.info +274 -244
  29. package/ios/ExpoIap.podspec +7 -5
  30. package/ios/ExpoIapHelper.swift +1 -1
  31. package/ios/ExpoIapModule.swift +1 -1
  32. package/openiap-versions.json +3 -3
  33. package/package.json +1 -1
  34. package/plugin/build/withIAP.d.ts +8 -5
  35. package/plugin/build/withIAP.js +12 -3
  36. package/plugin/src/withIAP.ts +18 -9
  37. package/src/index.ts +43 -12
  38. package/src/modules/android.ts +75 -0
  39. package/src/modules/ios.ts +4 -2
  40. package/src/types.ts +340 -24
@@ -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
@@ -727,29 +727,33 @@ export const deepLinkToSubscriptions: MutationField<
727
727
  export const validateReceipt: MutationField<'validateReceipt'> = async (
728
728
  options,
729
729
  ) => {
730
- const {sku, androidOptions} = options as MutationValidateReceiptArgs;
730
+ const {apple, google} = options as MutationValidateReceiptArgs;
731
731
 
732
732
  if (Platform.OS === 'ios') {
733
- return validateReceiptIOS({sku});
733
+ if (!apple?.sku) {
734
+ throw new Error('iOS validation requires apple.sku');
735
+ }
736
+ return validateReceiptIOS({apple: {sku: apple.sku}});
734
737
  }
735
738
 
736
739
  if (Platform.OS === 'android') {
737
740
  if (
738
- !androidOptions ||
739
- !androidOptions.packageName ||
740
- !androidOptions.productToken ||
741
- !androidOptions.accessToken
741
+ !google ||
742
+ !google.sku ||
743
+ !google.packageName ||
744
+ !google.purchaseToken ||
745
+ !google.accessToken
742
746
  ) {
743
747
  throw new Error(
744
- 'Android validation requires packageName, productToken, and accessToken',
748
+ 'Android validation requires google.sku, google.packageName, google.purchaseToken, and google.accessToken',
745
749
  );
746
750
  }
747
751
  return validateReceiptAndroid({
748
- packageName: androidOptions.packageName,
749
- productId: sku,
750
- productToken: androidOptions.productToken,
751
- accessToken: androidOptions.accessToken,
752
- 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,
753
757
  });
754
758
  }
755
759
 
@@ -804,6 +808,33 @@ export const verifyPurchaseWithProvider: MutationField<
804
808
  'verifyPurchaseWithProvider'
805
809
  > = async (options) => {
806
810
  if (Platform.OS === 'ios' || Platform.OS === 'android') {
811
+ // Auto-fill apiKey from config if not provided and provider is iapkit
812
+ if (
813
+ options.provider === 'iapkit' &&
814
+ options.iapkit &&
815
+ !options.iapkit.apiKey
816
+ ) {
817
+ try {
818
+ // Dynamically import expo-constants to avoid hard dependency
819
+ const {default: Constants} = await import('expo-constants');
820
+ const configApiKey = Constants.expoConfig?.extra?.iapkitApiKey;
821
+ if (configApiKey) {
822
+ options = {
823
+ ...options,
824
+ iapkit: {
825
+ ...options.iapkit,
826
+ apiKey: configApiKey,
827
+ },
828
+ };
829
+ }
830
+ } catch {
831
+ throw new Error(
832
+ 'expo-constants is required for auto-filling iapkitApiKey from config. ' +
833
+ 'Please install it: npx expo install expo-constants\n' +
834
+ 'Or provide apiKey directly in verifyPurchaseWithProvider options.',
835
+ );
836
+ }
837
+ }
807
838
  return ExpoIapModule.verifyPurchaseWithProvider(options);
808
839
  }
809
840
 
@@ -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(