expo-iap 3.1.8 → 3.1.10

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 (51) hide show
  1. package/android/src/main/java/expo/modules/iap/ExpoIapHelper.kt +69 -2
  2. package/android/src/main/java/expo/modules/iap/ExpoIapModule.kt +62 -4
  3. package/build/index.d.ts +32 -2
  4. package/build/index.d.ts.map +1 -1
  5. package/build/index.js +40 -15
  6. package/build/index.js.map +1 -1
  7. package/build/modules/android.d.ts +68 -0
  8. package/build/modules/android.d.ts.map +1 -1
  9. package/build/modules/android.js +74 -0
  10. package/build/modules/android.js.map +1 -1
  11. package/build/modules/ios.d.ts +23 -0
  12. package/build/modules/ios.d.ts.map +1 -1
  13. package/build/modules/ios.js +34 -3
  14. package/build/modules/ios.js.map +1 -1
  15. package/build/types.d.ts +116 -13
  16. package/build/types.d.ts.map +1 -1
  17. package/build/types.js.map +1 -1
  18. package/build/useIAP.d.ts +8 -0
  19. package/build/useIAP.d.ts.map +1 -1
  20. package/build/useIAP.js +11 -1
  21. package/build/useIAP.js.map +1 -1
  22. package/bun.lockb +0 -0
  23. package/coverage/clover.xml +258 -234
  24. package/coverage/coverage-final.json +3 -3
  25. package/coverage/lcov-report/index.html +27 -27
  26. package/coverage/lcov-report/src/helpers/index.html +1 -1
  27. package/coverage/lcov-report/src/helpers/subscription.ts.html +1 -1
  28. package/coverage/lcov-report/src/index.html +19 -19
  29. package/coverage/lcov-report/src/index.ts.html +136 -31
  30. package/coverage/lcov-report/src/modules/android.ts.html +257 -8
  31. package/coverage/lcov-report/src/modules/index.html +23 -23
  32. package/coverage/lcov-report/src/modules/ios.ts.html +137 -11
  33. package/coverage/lcov-report/src/utils/debug.ts.html +1 -1
  34. package/coverage/lcov-report/src/utils/errorMapping.ts.html +1 -1
  35. package/coverage/lcov-report/src/utils/index.html +1 -1
  36. package/coverage/lcov.info +473 -429
  37. package/ios/ExpoIapHelper.swift +4 -0
  38. package/ios/ExpoIapModule.swift +33 -1
  39. package/openiap-versions.json +3 -3
  40. package/package.json +1 -1
  41. package/plugin/build/withIAP.d.ts +26 -0
  42. package/plugin/build/withIAP.js +67 -3
  43. package/plugin/build/withLocalOpenIAP.d.ts +2 -0
  44. package/plugin/build/withLocalOpenIAP.js +7 -0
  45. package/plugin/src/withIAP.ts +141 -3
  46. package/plugin/src/withLocalOpenIAP.ts +14 -4
  47. package/src/index.ts +49 -14
  48. package/src/modules/android.ts +83 -0
  49. package/src/modules/ios.ts +45 -3
  50. package/src/types.ts +124 -13
  51. package/src/useIAP.ts +26 -1
@@ -6,6 +6,8 @@ import ExpoIapModule from '../ExpoIapModule';
6
6
 
7
7
  // Types
8
8
  import type {
9
+ ExternalPurchaseLinkResultIOS,
10
+ ExternalPurchaseNoticeResultIOS,
9
11
  MutationField,
10
12
  ProductIOS,
11
13
  Purchase,
@@ -49,7 +51,7 @@ export function isProductIOS<T extends {platform?: string}>(
49
51
  * @platform iOS
50
52
  */
51
53
  export const syncIOS: MutationField<'syncIOS'> = async () => {
52
- return Boolean(await ExpoIapModule.syncIOS());
54
+ return !!(await ExpoIapModule.syncIOS());
53
55
  };
54
56
 
55
57
  /**
@@ -264,7 +266,7 @@ export const validateReceiptIOS =
264
266
  export const presentCodeRedemptionSheetIOS: MutationField<
265
267
  'presentCodeRedemptionSheetIOS'
266
268
  > = async () => {
267
- return Boolean(await ExpoIapModule.presentCodeRedemptionSheetIOS());
269
+ return !!(await ExpoIapModule.presentCodeRedemptionSheetIOS());
268
270
  };
269
271
 
270
272
  /**
@@ -342,7 +344,7 @@ export const getPendingTransactionsIOS: QueryField<
342
344
  export const clearTransactionIOS: MutationField<
343
345
  'clearTransactionIOS'
344
346
  > = async () => {
345
- return Boolean(await ExpoIapModule.clearTransactionIOS());
347
+ return !!(await ExpoIapModule.clearTransactionIOS());
346
348
  };
347
349
 
348
350
  /**
@@ -354,4 +356,44 @@ export const clearTransactionIOS: MutationField<
354
356
  export const deepLinkToSubscriptionsIOS = (): Promise<void> =>
355
357
  Linking.openURL('https://apps.apple.com/account/subscriptions');
356
358
 
359
+ /**
360
+ * Check if the device can present an external purchase notice sheet (iOS 18.2+).
361
+ *
362
+ * @returns Promise resolving to true if the notice sheet can be presented
363
+ * @platform iOS
364
+ */
365
+ export const canPresentExternalPurchaseNoticeIOS: QueryField<
366
+ 'canPresentExternalPurchaseNoticeIOS'
367
+ > = async () => {
368
+ return !!(await ExpoIapModule.canPresentExternalPurchaseNoticeIOS());
369
+ };
370
+
371
+ /**
372
+ * Present an external purchase notice sheet to inform users about external purchases (iOS 18.2+).
373
+ * This must be called before opening an external purchase link.
374
+ *
375
+ * @returns Promise resolving to the result with action and error if any
376
+ * @platform iOS
377
+ */
378
+ export const presentExternalPurchaseNoticeSheetIOS: MutationField<
379
+ 'presentExternalPurchaseNoticeSheetIOS'
380
+ > = async () => {
381
+ const result = await ExpoIapModule.presentExternalPurchaseNoticeSheetIOS();
382
+ return result as ExternalPurchaseNoticeResultIOS;
383
+ };
384
+
385
+ /**
386
+ * Present an external purchase link to redirect users to your website (iOS 16.0+).
387
+ *
388
+ * @param url - The external purchase URL to open
389
+ * @returns Promise resolving to the result with success status and error if any
390
+ * @platform iOS
391
+ */
392
+ export const presentExternalPurchaseLinkIOS: MutationField<
393
+ 'presentExternalPurchaseLinkIOS'
394
+ > = async (url: string) => {
395
+ const result = await ExpoIapModule.presentExternalPurchaseLinkIOS(url);
396
+ return result as ExternalPurchaseLinkResultIOS;
397
+ };
398
+
357
399
  // iOS-specific APIs only; cross-platform wrappers live in src/index.ts
package/src/types.ts CHANGED
@@ -5,17 +5,33 @@
5
5
 
6
6
  export interface ActiveSubscription {
7
7
  autoRenewingAndroid?: (boolean | null);
8
+ basePlanIdAndroid?: (string | null);
9
+ /**
10
+ * The current plan identifier. This is:
11
+ * - On Android: the basePlanId (e.g., "premium", "premium-year")
12
+ * - On iOS: the productId (e.g., "com.example.premium_monthly", "com.example.premium_yearly")
13
+ * This provides a unified way to identify which specific plan/tier the user is subscribed to.
14
+ */
15
+ currentPlanId?: (string | null);
8
16
  daysUntilExpirationIOS?: (number | null);
9
17
  environmentIOS?: (string | null);
10
18
  expirationDateIOS?: (number | null);
11
19
  isActive: boolean;
12
20
  productId: string;
13
21
  purchaseToken?: (string | null);
22
+ /** Required for subscription upgrade/downgrade on Android */
23
+ purchaseTokenAndroid?: (string | null);
14
24
  transactionDate: number;
15
25
  transactionId: string;
16
26
  willExpireSoon?: (boolean | null);
17
27
  }
18
28
 
29
+ /**
30
+ * Alternative billing mode for Android
31
+ * Controls which billing system is used
32
+ */
33
+ export type AlternativeBillingModeAndroid = 'none' | 'user-choice' | 'alternative-only';
34
+
19
35
  export interface AndroidSubscriptionOfferInput {
20
36
  /** Offer token */
21
37
  offerToken: string;
@@ -126,21 +142,67 @@ export enum ErrorCode {
126
142
  UserError = 'user-error'
127
143
  }
128
144
 
145
+ /** Result of presenting an external purchase link (iOS 18.2+) */
146
+ export interface ExternalPurchaseLinkResultIOS {
147
+ /** Optional error message if the presentation failed */
148
+ error?: (string | null);
149
+ /** Whether the user completed the external purchase flow */
150
+ success: boolean;
151
+ }
152
+
153
+ /** User actions on external purchase notice sheet (iOS 18.2+) */
154
+ export type ExternalPurchaseNoticeAction = 'continue' | 'dismissed';
155
+
156
+ /** Result of presenting external purchase notice sheet (iOS 18.2+) */
157
+ export interface ExternalPurchaseNoticeResultIOS {
158
+ /** Optional error message if the presentation failed */
159
+ error?: (string | null);
160
+ /** Notice result indicating user action */
161
+ result: ExternalPurchaseNoticeAction;
162
+ }
163
+
129
164
  export type FetchProductsResult = Product[] | ProductSubscription[] | null;
130
165
 
131
- export type IapEvent = 'purchase-updated' | 'purchase-error' | 'promoted-product-ios';
166
+ export type IapEvent = 'purchase-updated' | 'purchase-error' | 'promoted-product-ios' | 'user-choice-billing-android';
132
167
 
133
168
  export type IapPlatform = 'ios' | 'android';
134
169
 
170
+ /** Connection initialization configuration */
171
+ export interface InitConnectionConfig {
172
+ /**
173
+ * Alternative billing mode for Android
174
+ * If not specified, defaults to NONE (standard Google Play billing)
175
+ */
176
+ alternativeBillingModeAndroid?: (AlternativeBillingModeAndroid | null);
177
+ }
178
+
135
179
  export interface Mutation {
136
180
  /** Acknowledge a non-consumable purchase or subscription */
137
181
  acknowledgePurchaseAndroid: Promise<boolean>;
138
182
  /** Initiate a refund request for a product (iOS 15+) */
139
183
  beginRefundRequestIOS?: Promise<(string | null)>;
184
+ /**
185
+ * Check if alternative billing is available for this user/device
186
+ * Step 1 of alternative billing flow
187
+ *
188
+ * Returns true if available, false otherwise
189
+ * Throws OpenIapError.NotPrepared if billing client not ready
190
+ */
191
+ checkAlternativeBillingAvailabilityAndroid: Promise<boolean>;
140
192
  /** Clear pending transactions from the StoreKit payment queue */
141
193
  clearTransactionIOS: Promise<boolean>;
142
194
  /** Consume a purchase token so it can be repurchased */
143
195
  consumePurchaseAndroid: Promise<boolean>;
196
+ /**
197
+ * Create external transaction token for Google Play reporting
198
+ * Step 3 of alternative billing flow
199
+ * Must be called AFTER successful payment in your payment system
200
+ * Token must be reported to Google Play backend within 24 hours
201
+ *
202
+ * Returns token string, or null if creation failed
203
+ * Throws OpenIapError.NotPrepared if billing client not ready
204
+ */
205
+ createAlternativeBillingTokenAndroid?: Promise<(string | null)>;
144
206
  /** Open the native subscription management surface */
145
207
  deepLinkToSubscriptions: Promise<void>;
146
208
  /** Close the platform billing connection */
@@ -151,12 +213,25 @@ export interface Mutation {
151
213
  initConnection: Promise<boolean>;
152
214
  /** Present the App Store code redemption sheet */
153
215
  presentCodeRedemptionSheetIOS: Promise<boolean>;
216
+ /** Present external purchase custom link with StoreKit UI (iOS 18.2+) */
217
+ presentExternalPurchaseLinkIOS: Promise<ExternalPurchaseLinkResultIOS>;
218
+ /** Present external purchase notice sheet (iOS 18.2+) */
219
+ presentExternalPurchaseNoticeSheetIOS: Promise<ExternalPurchaseNoticeResultIOS>;
154
220
  /** Initiate a purchase flow; rely on events for final state */
155
221
  requestPurchase?: Promise<(Purchase | Purchase[] | null)>;
156
222
  /** Purchase the promoted product surfaced by the App Store */
157
223
  requestPurchaseOnPromotedProductIOS: Promise<boolean>;
158
224
  /** Restore completed purchases across platforms */
159
225
  restorePurchases: Promise<void>;
226
+ /**
227
+ * Show alternative billing information dialog to user
228
+ * Step 2 of alternative billing flow
229
+ * Must be called BEFORE processing payment in your payment system
230
+ *
231
+ * Returns true if user accepted, false if user canceled
232
+ * Throws OpenIapError.NotPrepared if billing client not ready
233
+ */
234
+ showAlternativeBillingDialogAndroid: Promise<boolean>;
160
235
  /** Open subscription management UI and return changed purchases (iOS 15+) */
161
236
  showManageSubscriptionsIOS: Promise<PurchaseIOS[]>;
162
237
  /** Force a StoreKit sync for transactions (iOS 15+) */
@@ -181,16 +256,24 @@ export interface MutationFinishTransactionArgs {
181
256
  }
182
257
 
183
258
 
259
+ export type MutationInitConnectionArgs = (InitConnectionConfig | null) | undefined;
260
+
261
+ export type MutationPresentExternalPurchaseLinkIosArgs = string;
262
+
184
263
  export type MutationRequestPurchaseArgs =
185
264
  | {
186
265
  /** Per-platform purchase request props */
187
266
  request: RequestPurchasePropsByPlatforms;
188
267
  type: 'in-app';
268
+ /** Use alternative billing (Google Play alternative billing, Apple external purchase link) */
269
+ useAlternativeBilling?: boolean | null;
189
270
  }
190
271
  | {
191
272
  /** Per-platform subscription request props */
192
273
  request: RequestSubscriptionPropsByPlatforms;
193
274
  type: 'subs';
275
+ /** Use alternative billing (Google Play alternative billing, Apple external purchase link) */
276
+ useAlternativeBilling?: boolean | null;
194
277
  };
195
278
 
196
279
 
@@ -333,6 +416,7 @@ export type Purchase = PurchaseAndroid | PurchaseIOS;
333
416
 
334
417
  export interface PurchaseAndroid extends PurchaseCommon {
335
418
  autoRenewingAndroid?: (boolean | null);
419
+ currentPlanId?: (string | null);
336
420
  dataAndroid?: (string | null);
337
421
  developerPayloadAndroid?: (string | null);
338
422
  id: string;
@@ -353,6 +437,13 @@ export interface PurchaseAndroid extends PurchaseCommon {
353
437
  }
354
438
 
355
439
  export interface PurchaseCommon {
440
+ /**
441
+ * The current plan identifier. This is:
442
+ * - On Android: the basePlanId (e.g., "premium", "premium-year")
443
+ * - On iOS: the productId (e.g., "com.example.premium_monthly", "com.example.premium_yearly")
444
+ * This provides a unified way to identify which specific plan/tier the user is subscribed to.
445
+ */
446
+ currentPlanId?: (string | null);
356
447
  id: string;
357
448
  ids?: (string[] | null);
358
449
  isAutoRenewing: boolean;
@@ -377,6 +468,7 @@ export interface PurchaseIOS extends PurchaseCommon {
377
468
  countryCodeIOS?: (string | null);
378
469
  currencyCodeIOS?: (string | null);
379
470
  currencySymbolIOS?: (string | null);
471
+ currentPlanId?: (string | null);
380
472
  environmentIOS?: (string | null);
381
473
  expirationDateIOS?: (number | null);
382
474
  id: string;
@@ -405,17 +497,7 @@ export interface PurchaseIOS extends PurchaseCommon {
405
497
  webOrderLineItemIdIOS?: (string | null);
406
498
  }
407
499
 
408
- export interface PurchaseInput {
409
- id: string;
410
- ids?: (string[] | null);
411
- isAutoRenewing: boolean;
412
- platform: IapPlatform;
413
- productId: string;
414
- purchaseState: PurchaseState;
415
- purchaseToken?: (string | null);
416
- quantity: number;
417
- transactionDate: number;
418
- }
500
+ export type PurchaseInput = Purchase;
419
501
 
420
502
  export interface PurchaseOfferIOS {
421
503
  id: string;
@@ -433,6 +515,8 @@ export interface PurchaseOptions {
433
515
  export type PurchaseState = 'pending' | 'purchased' | 'failed' | 'restored' | 'deferred' | 'unknown';
434
516
 
435
517
  export interface Query {
518
+ /** Check if external purchase notice sheet can be presented (iOS 18.2+) */
519
+ canPresentExternalPurchaseNoticeIOS: Promise<boolean>;
436
520
  /** Get current StoreKit 2 entitlements (iOS 15+) */
437
521
  currentEntitlementIOS?: Promise<(PurchaseIOS | null)>;
438
522
  /** Retrieve products or subscriptions from the store */
@@ -584,11 +668,15 @@ export type RequestPurchaseProps =
584
668
  /** Per-platform purchase request props */
585
669
  request: RequestPurchasePropsByPlatforms;
586
670
  type: 'in-app';
671
+ /** Use alternative billing (Google Play alternative billing, Apple external purchase link) */
672
+ useAlternativeBilling?: boolean | null;
587
673
  }
588
674
  | {
589
675
  /** Per-platform subscription request props */
590
676
  request: RequestSubscriptionPropsByPlatforms;
591
677
  type: 'subs';
678
+ /** Use alternative billing (Google Play alternative billing, Apple external purchase link) */
679
+ useAlternativeBilling?: boolean | null;
592
680
  };
593
681
 
594
682
  export interface RequestPurchasePropsByPlatforms {
@@ -639,6 +727,11 @@ export interface Subscription {
639
727
  purchaseError: PurchaseError;
640
728
  /** Fires when a purchase completes successfully or a pending purchase resolves */
641
729
  purchaseUpdated: Purchase;
730
+ /**
731
+ * Fires when a user selects alternative billing in the User Choice Billing dialog (Android only)
732
+ * Only triggered when the user selects alternative billing instead of Google Play billing
733
+ */
734
+ userChoiceBillingAndroid: UserChoiceBillingDetails;
642
735
  }
643
736
 
644
737
 
@@ -673,10 +766,22 @@ export interface SubscriptionStatusIOS {
673
766
  state: string;
674
767
  }
675
768
 
769
+ /**
770
+ * User Choice Billing event details (Android)
771
+ * Fired when a user selects alternative billing in the User Choice Billing dialog
772
+ */
773
+ export interface UserChoiceBillingDetails {
774
+ /** Token that must be reported to Google Play within 24 hours */
775
+ externalTransactionToken: string;
776
+ /** List of product IDs selected by the user */
777
+ products: string[];
778
+ }
779
+
676
780
  export type VoidResult = void;
677
781
 
678
782
  // -- Query helper types (auto-generated)
679
783
  export type QueryArgsMap = {
784
+ canPresentExternalPurchaseNoticeIOS: never;
680
785
  currentEntitlementIOS: QueryCurrentEntitlementIosArgs;
681
786
  fetchProducts: QueryFetchProductsArgs;
682
787
  getActiveSubscriptions: QueryGetActiveSubscriptionsArgs;
@@ -712,16 +817,21 @@ export type QueryFieldMap = {
712
817
  export type MutationArgsMap = {
713
818
  acknowledgePurchaseAndroid: MutationAcknowledgePurchaseAndroidArgs;
714
819
  beginRefundRequestIOS: MutationBeginRefundRequestIosArgs;
820
+ checkAlternativeBillingAvailabilityAndroid: never;
715
821
  clearTransactionIOS: never;
716
822
  consumePurchaseAndroid: MutationConsumePurchaseAndroidArgs;
823
+ createAlternativeBillingTokenAndroid: never;
717
824
  deepLinkToSubscriptions: MutationDeepLinkToSubscriptionsArgs;
718
825
  endConnection: never;
719
826
  finishTransaction: MutationFinishTransactionArgs;
720
- initConnection: never;
827
+ initConnection: MutationInitConnectionArgs;
721
828
  presentCodeRedemptionSheetIOS: never;
829
+ presentExternalPurchaseLinkIOS: MutationPresentExternalPurchaseLinkIosArgs;
830
+ presentExternalPurchaseNoticeSheetIOS: never;
722
831
  requestPurchase: MutationRequestPurchaseArgs;
723
832
  requestPurchaseOnPromotedProductIOS: never;
724
833
  restorePurchases: never;
834
+ showAlternativeBillingDialogAndroid: never;
725
835
  showManageSubscriptionsIOS: never;
726
836
  syncIOS: never;
727
837
  validateReceipt: MutationValidateReceiptArgs;
@@ -744,6 +854,7 @@ export type SubscriptionArgsMap = {
744
854
  promotedProductIOS: never;
745
855
  purchaseError: never;
746
856
  purchaseUpdated: never;
857
+ userChoiceBillingAndroid: never;
747
858
  };
748
859
 
749
860
  export type SubscriptionField<K extends keyof Subscription> =
package/src/useIAP.ts CHANGED
@@ -26,6 +26,11 @@ import {
26
26
  requestPurchaseOnPromotedProductIOS,
27
27
  syncIOS,
28
28
  } from './modules/ios';
29
+ import {
30
+ checkAlternativeBillingAvailabilityAndroid,
31
+ showAlternativeBillingDialogAndroid,
32
+ createAlternativeBillingTokenAndroid,
33
+ } from './modules/android';
29
34
 
30
35
  // Types
31
36
  import type {
@@ -80,12 +85,22 @@ type UseIap = {
80
85
  requestPurchaseOnPromotedProductIOS: () => Promise<boolean>;
81
86
  getActiveSubscriptions: (subscriptionIds?: string[]) => Promise<void>;
82
87
  hasActiveSubscriptions: (subscriptionIds?: string[]) => Promise<boolean>;
88
+ checkAlternativeBillingAvailabilityAndroid: () => Promise<boolean>;
89
+ showAlternativeBillingDialogAndroid: () => Promise<boolean>;
90
+ createAlternativeBillingTokenAndroid: (
91
+ sku?: string,
92
+ ) => Promise<string | null>;
83
93
  };
84
94
 
85
95
  export interface UseIAPOptions {
86
96
  onPurchaseSuccess?: (purchase: Purchase) => void;
87
97
  onPurchaseError?: (error: PurchaseError) => void;
88
98
  onPromotedProductIOS?: (product: Product) => void;
99
+ /**
100
+ * Alternative billing mode for Android
101
+ * If not specified, defaults to NONE (standard Google Play billing)
102
+ */
103
+ alternativeBillingModeAndroid?: 'none' | 'user-choice' | 'alternative-only';
89
104
  }
90
105
 
91
106
  /**
@@ -419,7 +434,13 @@ export function useIAP(options?: UseIAPOptions): UseIap {
419
434
  }
420
435
 
421
436
  // NOW call initConnection after listeners are ready
422
- const result = await initConnection();
437
+ const config = optionsRef.current?.alternativeBillingModeAndroid
438
+ ? {
439
+ alternativeBillingModeAndroid:
440
+ optionsRef.current.alternativeBillingModeAndroid,
441
+ }
442
+ : undefined;
443
+ const result = await initConnection(config);
423
444
  setConnected(result);
424
445
  if (!result) {
425
446
  // If connection failed, clean up listeners
@@ -466,5 +487,9 @@ export function useIAP(options?: UseIAPOptions): UseIap {
466
487
  requestPurchaseOnPromotedProductIOS,
467
488
  getActiveSubscriptions: getActiveSubscriptionsInternal,
468
489
  hasActiveSubscriptions: hasActiveSubscriptionsInternal,
490
+ // Alternative billing methods (Android only)
491
+ checkAlternativeBillingAvailabilityAndroid,
492
+ showAlternativeBillingDialogAndroid,
493
+ createAlternativeBillingTokenAndroid,
469
494
  };
470
495
  }