react-native-iap 14.4.11 → 14.4.12

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 (76) hide show
  1. package/android/src/main/java/com/margelo/nitro/iap/HybridRnIap.kt +159 -10
  2. package/ios/HybridRnIap.swift +115 -2
  3. package/lib/module/hooks/useIAP.js +24 -3
  4. package/lib/module/hooks/useIAP.js.map +1 -1
  5. package/lib/module/index.js +275 -2
  6. package/lib/module/index.js.map +1 -1
  7. package/lib/module/types.js +18 -0
  8. package/lib/module/types.js.map +1 -1
  9. package/lib/typescript/plugin/src/withIAP.d.ts +27 -0
  10. package/lib/typescript/plugin/src/withIAP.d.ts.map +1 -1
  11. package/lib/typescript/src/hooks/useIAP.d.ts +6 -1
  12. package/lib/typescript/src/hooks/useIAP.d.ts.map +1 -1
  13. package/lib/typescript/src/index.d.ts +133 -0
  14. package/lib/typescript/src/index.d.ts.map +1 -1
  15. package/lib/typescript/src/specs/RnIap.nitro.d.ts +72 -2
  16. package/lib/typescript/src/specs/RnIap.nitro.d.ts.map +1 -1
  17. package/lib/typescript/src/types.d.ts +116 -13
  18. package/lib/typescript/src/types.d.ts.map +1 -1
  19. package/nitrogen/generated/android/NitroIapOnLoad.cpp +2 -0
  20. package/nitrogen/generated/android/c++/JAlternativeBillingModeAndroid.hpp +62 -0
  21. package/nitrogen/generated/android/c++/JExternalPurchaseLinkResultIOS.hpp +58 -0
  22. package/nitrogen/generated/android/c++/JExternalPurchaseNoticeAction.hpp +59 -0
  23. package/nitrogen/generated/android/c++/JExternalPurchaseNoticeResultIOS.hpp +60 -0
  24. package/nitrogen/generated/android/c++/JFunc_void_UserChoiceBillingDetails.hpp +78 -0
  25. package/nitrogen/generated/android/c++/JHybridRnIapSpec.cpp +132 -3
  26. package/nitrogen/generated/android/c++/JHybridRnIapSpec.hpp +9 -1
  27. package/nitrogen/generated/android/c++/JInitConnectionConfig.hpp +55 -0
  28. package/nitrogen/generated/android/c++/JPurchaseAndroid.hpp +4 -0
  29. package/nitrogen/generated/android/c++/JPurchaseIOS.hpp +4 -0
  30. package/nitrogen/generated/android/c++/JUserChoiceBillingDetails.hpp +75 -0
  31. package/nitrogen/generated/android/kotlin/com/margelo/nitro/iap/AlternativeBillingModeAndroid.kt +22 -0
  32. package/nitrogen/generated/android/kotlin/com/margelo/nitro/iap/ExternalPurchaseLinkResultIOS.kt +32 -0
  33. package/nitrogen/generated/android/kotlin/com/margelo/nitro/iap/ExternalPurchaseNoticeAction.kt +21 -0
  34. package/nitrogen/generated/android/kotlin/com/margelo/nitro/iap/ExternalPurchaseNoticeResultIOS.kt +32 -0
  35. package/nitrogen/generated/android/kotlin/com/margelo/nitro/iap/Func_void_UserChoiceBillingDetails.kt +81 -0
  36. package/nitrogen/generated/android/kotlin/com/margelo/nitro/iap/HybridRnIapSpec.kt +43 -1
  37. package/nitrogen/generated/android/kotlin/com/margelo/nitro/iap/InitConnectionConfig.kt +29 -0
  38. package/nitrogen/generated/android/kotlin/com/margelo/nitro/iap/PurchaseAndroid.kt +3 -0
  39. package/nitrogen/generated/android/kotlin/com/margelo/nitro/iap/PurchaseIOS.kt +3 -0
  40. package/nitrogen/generated/android/kotlin/com/margelo/nitro/iap/UserChoiceBillingDetails.kt +32 -0
  41. package/nitrogen/generated/ios/NitroIap-Swift-Cxx-Bridge.cpp +24 -0
  42. package/nitrogen/generated/ios/NitroIap-Swift-Cxx-Bridge.hpp +156 -0
  43. package/nitrogen/generated/ios/NitroIap-Swift-Cxx-Umbrella.hpp +18 -0
  44. package/nitrogen/generated/ios/c++/HybridRnIapSpecSwift.hpp +81 -3
  45. package/nitrogen/generated/ios/swift/AlternativeBillingModeAndroid.swift +44 -0
  46. package/nitrogen/generated/ios/swift/ExternalPurchaseLinkResultIOS.swift +65 -0
  47. package/nitrogen/generated/ios/swift/ExternalPurchaseNoticeAction.swift +40 -0
  48. package/nitrogen/generated/ios/swift/ExternalPurchaseNoticeResultIOS.swift +65 -0
  49. package/nitrogen/generated/ios/swift/Func_void_ExternalPurchaseLinkResultIOS.swift +47 -0
  50. package/nitrogen/generated/ios/swift/Func_void_ExternalPurchaseNoticeResultIOS.swift +47 -0
  51. package/nitrogen/generated/ios/swift/Func_void_UserChoiceBillingDetails.swift +47 -0
  52. package/nitrogen/generated/ios/swift/HybridRnIapSpec.swift +9 -1
  53. package/nitrogen/generated/ios/swift/HybridRnIapSpec_cxx.swift +168 -2
  54. package/nitrogen/generated/ios/swift/InitConnectionConfig.swift +47 -0
  55. package/nitrogen/generated/ios/swift/PurchaseAndroid.swift +31 -1
  56. package/nitrogen/generated/ios/swift/PurchaseIOS.swift +31 -1
  57. package/nitrogen/generated/ios/swift/UserChoiceBillingDetails.swift +58 -0
  58. package/nitrogen/generated/shared/c++/AlternativeBillingModeAndroid.hpp +80 -0
  59. package/nitrogen/generated/shared/c++/ExternalPurchaseLinkResultIOS.hpp +72 -0
  60. package/nitrogen/generated/shared/c++/ExternalPurchaseNoticeAction.hpp +76 -0
  61. package/nitrogen/generated/shared/c++/ExternalPurchaseNoticeResultIOS.hpp +74 -0
  62. package/nitrogen/generated/shared/c++/HybridRnIapSpec.cpp +8 -0
  63. package/nitrogen/generated/shared/c++/HybridRnIapSpec.hpp +22 -2
  64. package/nitrogen/generated/shared/c++/InitConnectionConfig.hpp +69 -0
  65. package/nitrogen/generated/shared/c++/PurchaseAndroid.hpp +5 -1
  66. package/nitrogen/generated/shared/c++/PurchaseIOS.hpp +5 -1
  67. package/nitrogen/generated/shared/c++/UserChoiceBillingDetails.hpp +72 -0
  68. package/openiap-versions.json +3 -3
  69. package/package.json +1 -1
  70. package/plugin/build/withIAP.d.ts +27 -0
  71. package/plugin/build/withIAP.js +91 -1
  72. package/plugin/src/withIAP.ts +162 -0
  73. package/src/hooks/useIAP.ts +47 -1
  74. package/src/index.ts +313 -2
  75. package/src/specs/RnIap.nitro.ts +99 -1
  76. package/src/types.ts +124 -13
@@ -4,6 +4,8 @@ import {
4
4
  withAndroidManifest,
5
5
  withAppBuildGradle,
6
6
  withPodfile,
7
+ withEntitlementsPlist,
8
+ withInfoPlist,
7
9
  } from 'expo/config-plugins';
8
10
  import type {ConfigPlugin} from 'expo/config-plugins';
9
11
  import type {ExpoConfig} from '@expo/config-types';
@@ -165,6 +167,154 @@ const withIapAndroid: ConfigPlugin = (config) => {
165
167
  return config;
166
168
  };
167
169
 
170
+ export interface IosAlternativeBillingConfig {
171
+ /** Country codes where external purchases are supported (ISO 3166-1 alpha-2) */
172
+ countries?: string[];
173
+ /** External purchase URLs per country (iOS 15.4+) */
174
+ links?: Record<string, string>;
175
+ /** Multiple external purchase URLs per country (iOS 17.5+, up to 5 per country) */
176
+ multiLinks?: Record<string, string[]>;
177
+ /** Custom link regions (iOS 18.1+) */
178
+ customLinkRegions?: string[];
179
+ /** Streaming link regions for music apps (iOS 18.2+) */
180
+ streamingLinkRegions?: string[];
181
+ /** Enable external purchase link entitlement */
182
+ enableExternalPurchaseLink?: boolean;
183
+ /** Enable external purchase link streaming entitlement (music apps only) */
184
+ enableExternalPurchaseLinkStreaming?: boolean;
185
+ }
186
+
187
+ /** Add external purchase entitlements and Info.plist configuration */
188
+ const withIosAlternativeBilling: ConfigPlugin<
189
+ IosAlternativeBillingConfig | undefined
190
+ > = (config, options) => {
191
+ if (!options || !options.countries || options.countries.length === 0) {
192
+ return config;
193
+ }
194
+
195
+ // Add entitlements
196
+ config = withEntitlementsPlist(config, (config) => {
197
+ // Always add basic external purchase entitlement when countries are specified
198
+ config.modResults['com.apple.developer.storekit.external-purchase'] = true;
199
+ if (!hasLoggedPluginExecution) {
200
+ console.log(
201
+ '✅ Added com.apple.developer.storekit.external-purchase to entitlements',
202
+ );
203
+ }
204
+
205
+ // Add external purchase link entitlement if enabled
206
+ if (options.enableExternalPurchaseLink) {
207
+ config.modResults['com.apple.developer.storekit.external-purchase-link'] =
208
+ true;
209
+ if (!hasLoggedPluginExecution) {
210
+ console.log(
211
+ '✅ Added com.apple.developer.storekit.external-purchase-link to entitlements',
212
+ );
213
+ }
214
+ }
215
+
216
+ // Add streaming entitlement if enabled
217
+ if (options.enableExternalPurchaseLinkStreaming) {
218
+ config.modResults[
219
+ 'com.apple.developer.storekit.external-purchase-link-streaming'
220
+ ] = true;
221
+ if (!hasLoggedPluginExecution) {
222
+ console.log(
223
+ '✅ Added com.apple.developer.storekit.external-purchase-link-streaming to entitlements',
224
+ );
225
+ }
226
+ }
227
+
228
+ return config;
229
+ });
230
+
231
+ // Add Info.plist configuration
232
+ config = withInfoPlist(config, (config) => {
233
+ const plist = config.modResults;
234
+
235
+ // Helper function to normalize country codes to uppercase
236
+ const normalize = (code: string) => code.trim().toUpperCase();
237
+
238
+ // 1. SKExternalPurchase (Required)
239
+ const normalizedCountries = options.countries?.map(normalize);
240
+ plist.SKExternalPurchase = normalizedCountries;
241
+ if (!hasLoggedPluginExecution) {
242
+ console.log(
243
+ `✅ Added SKExternalPurchase with countries: ${normalizedCountries?.join(
244
+ ', ',
245
+ )}`,
246
+ );
247
+ }
248
+
249
+ // 2. SKExternalPurchaseLink (Optional - iOS 15.4+)
250
+ if (options.links && Object.keys(options.links).length > 0) {
251
+ plist.SKExternalPurchaseLink = Object.fromEntries(
252
+ Object.entries(options.links).map(([code, url]) => [
253
+ normalize(code),
254
+ url,
255
+ ]),
256
+ );
257
+ if (!hasLoggedPluginExecution) {
258
+ console.log(
259
+ `✅ Added SKExternalPurchaseLink for ${
260
+ Object.keys(options.links).length
261
+ } countries`,
262
+ );
263
+ }
264
+ }
265
+
266
+ // 3. SKExternalPurchaseMultiLink (iOS 17.5+)
267
+ if (options.multiLinks && Object.keys(options.multiLinks).length > 0) {
268
+ plist.SKExternalPurchaseMultiLink = Object.fromEntries(
269
+ Object.entries(options.multiLinks).map(([code, urls]) => [
270
+ normalize(code),
271
+ urls,
272
+ ]),
273
+ );
274
+ if (!hasLoggedPluginExecution) {
275
+ console.log(
276
+ `✅ Added SKExternalPurchaseMultiLink for ${
277
+ Object.keys(options.multiLinks).length
278
+ } countries`,
279
+ );
280
+ }
281
+ }
282
+
283
+ // 4. SKExternalPurchaseCustomLinkRegions (iOS 18.1+)
284
+ if (options.customLinkRegions && options.customLinkRegions.length > 0) {
285
+ plist.SKExternalPurchaseCustomLinkRegions =
286
+ options.customLinkRegions.map(normalize);
287
+ if (!hasLoggedPluginExecution) {
288
+ console.log(
289
+ `✅ Added SKExternalPurchaseCustomLinkRegions: ${options.customLinkRegions
290
+ .map(normalize)
291
+ .join(', ')}`,
292
+ );
293
+ }
294
+ }
295
+
296
+ // 5. SKExternalPurchaseLinkStreamingRegions (iOS 18.2+)
297
+ if (
298
+ options.streamingLinkRegions &&
299
+ options.streamingLinkRegions.length > 0
300
+ ) {
301
+ plist.SKExternalPurchaseLinkStreamingRegions =
302
+ options.streamingLinkRegions.map(normalize);
303
+ if (!hasLoggedPluginExecution) {
304
+ console.log(
305
+ `✅ Added SKExternalPurchaseLinkStreamingRegions: ${options.streamingLinkRegions
306
+ .map(normalize)
307
+ .join(', ')}`,
308
+ );
309
+ }
310
+ }
311
+
312
+ return config;
313
+ });
314
+
315
+ return config;
316
+ };
317
+
168
318
  type IapPluginProps = {
169
319
  ios?: {
170
320
  // Enable to inject Folly coroutine-disabling macros into Podfile during prebuild
@@ -172,6 +322,13 @@ type IapPluginProps = {
172
322
  // @deprecated Use 'with-folly-no-coroutines'. Kept for backward compatibility.
173
323
  'with-folly-no-couroutines'?: boolean;
174
324
  };
325
+ /**
326
+ * iOS Alternative Billing configuration.
327
+ * Configure external purchase countries, links, and entitlements.
328
+ * Requires approval from Apple.
329
+ * @platform ios
330
+ */
331
+ iosAlternativeBilling?: IosAlternativeBillingConfig;
175
332
  };
176
333
 
177
334
  const withIapIosFollyWorkaround: ConfigPlugin<IapPluginProps | undefined> = (
@@ -236,6 +393,10 @@ const withIAP: ConfigPlugin<IapPluginProps | undefined> = (config, props) => {
236
393
  try {
237
394
  let result = withIapAndroid(config);
238
395
  result = withIapIosFollyWorkaround(result, props);
396
+ // Add iOS alternative billing configuration if provided
397
+ if (props?.iosAlternativeBilling) {
398
+ result = withIosAlternativeBilling(result, props.iosAlternativeBilling);
399
+ }
239
400
  // Set flag after first execution to prevent duplicate logs
240
401
  hasLoggedPluginExecution = true;
241
402
  return result;
@@ -270,4 +431,5 @@ const pluginExport: IapPluginCallable = ((
270
431
  props?: IapPluginProps,
271
432
  ) => _wrapped(config, props)) as unknown as IapPluginCallable;
272
433
 
434
+ export {withIosAlternativeBilling};
273
435
  export {pluginExport as default};
@@ -19,6 +19,10 @@ import {
19
19
  restorePurchases as restorePurchasesTopLevel,
20
20
  getPromotedProductIOS,
21
21
  requestPurchaseOnPromotedProductIOS,
22
+ checkAlternativeBillingAvailabilityAndroid,
23
+ showAlternativeBillingDialogAndroid,
24
+ createAlternativeBillingTokenAndroid,
25
+ userChoiceBillingListenerAndroid,
22
26
  } from '../';
23
27
 
24
28
  // Types
@@ -27,6 +31,8 @@ import type {
27
31
  ProductQueryType,
28
32
  RequestPurchaseProps,
29
33
  RequestPurchaseResult,
34
+ AlternativeBillingModeAndroid,
35
+ UserChoiceBillingDetails,
30
36
  } from '../types';
31
37
  import type {
32
38
  ActiveSubscription,
@@ -75,12 +81,20 @@ type UseIap = {
75
81
  subscriptionIds?: string[],
76
82
  ) => Promise<ActiveSubscription[]>;
77
83
  hasActiveSubscriptions: (subscriptionIds?: string[]) => Promise<boolean>;
84
+ // Alternative billing (Android)
85
+ checkAlternativeBillingAvailabilityAndroid?: () => Promise<boolean>;
86
+ showAlternativeBillingDialogAndroid?: () => Promise<boolean>;
87
+ createAlternativeBillingTokenAndroid?: (
88
+ sku?: string,
89
+ ) => Promise<string | null>;
78
90
  };
79
91
 
80
92
  export interface UseIapOptions {
81
93
  onPurchaseSuccess?: (purchase: Purchase) => void;
82
94
  onPurchaseError?: (error: PurchaseError) => void;
83
95
  onPromotedProductIOS?: (product: Product) => void;
96
+ onUserChoiceBillingAndroid?: (details: UserChoiceBillingDetails) => void;
97
+ alternativeBillingModeAndroid?: AlternativeBillingModeAndroid;
84
98
  }
85
99
 
86
100
  /**
@@ -133,6 +147,7 @@ export function useIAP(options?: UseIapOptions): UseIap {
133
147
  purchaseUpdate?: EventSubscription;
134
148
  purchaseError?: EventSubscription;
135
149
  promotedProductIOS?: EventSubscription;
150
+ userChoiceBillingAndroid?: EventSubscription;
136
151
  }>({});
137
152
 
138
153
  const subscriptionsRefState = useRef<ProductSubscription[]>([]);
@@ -346,7 +361,29 @@ export function useIAP(options?: UseIapOptions): UseIap {
346
361
  );
347
362
  }
348
363
 
349
- const result = await initConnection();
364
+ // Add user choice billing listener for Android (if provided)
365
+ if (
366
+ Platform.OS === 'android' &&
367
+ optionsRef.current?.onUserChoiceBillingAndroid
368
+ ) {
369
+ subscriptionsRef.current.userChoiceBillingAndroid =
370
+ userChoiceBillingListenerAndroid((details) => {
371
+ if (optionsRef.current?.onUserChoiceBillingAndroid) {
372
+ optionsRef.current.onUserChoiceBillingAndroid(details);
373
+ }
374
+ });
375
+ }
376
+
377
+ // Initialize connection with config
378
+ const config =
379
+ Platform.OS === 'android' &&
380
+ optionsRef.current?.alternativeBillingModeAndroid
381
+ ? {
382
+ alternativeBillingModeAndroid:
383
+ optionsRef.current.alternativeBillingModeAndroid,
384
+ }
385
+ : undefined;
386
+ const result = await initConnection(config);
350
387
  setConnected(result);
351
388
  if (!result) {
352
389
  // Clean up some listeners but leave purchaseError for potential retries
@@ -364,6 +401,7 @@ export function useIAP(options?: UseIapOptions): UseIap {
364
401
  currentSubscriptions.purchaseUpdate?.remove();
365
402
  currentSubscriptions.purchaseError?.remove();
366
403
  currentSubscriptions.promotedProductIOS?.remove();
404
+ currentSubscriptions.userChoiceBillingAndroid?.remove();
367
405
  // Keep connection alive across screens to avoid race conditions
368
406
  setConnected(false);
369
407
  };
@@ -393,5 +431,13 @@ export function useIAP(options?: UseIapOptions): UseIap {
393
431
  requestPurchaseOnPromotedProductIOS,
394
432
  getActiveSubscriptions: getActiveSubscriptionsInternal,
395
433
  hasActiveSubscriptions: hasActiveSubscriptionsInternal,
434
+ // Alternative billing (Android only)
435
+ ...(Platform.OS === 'android'
436
+ ? {
437
+ checkAlternativeBillingAvailabilityAndroid,
438
+ showAlternativeBillingDialogAndroid,
439
+ createAlternativeBillingTokenAndroid,
440
+ }
441
+ : {}),
396
442
  };
397
443
  }
package/src/index.ts CHANGED
@@ -292,6 +292,82 @@ export const promotedProductListenerIOS = (
292
292
  };
293
293
  };
294
294
 
295
+ /**
296
+ * Add a listener for user choice billing events (Android only).
297
+ * Fires when a user selects alternative billing in the User Choice Billing dialog.
298
+ *
299
+ * @param listener - Function to call when user chooses alternative billing
300
+ * @returns EventSubscription with remove() method to unsubscribe
301
+ * @platform Android
302
+ *
303
+ * @example
304
+ * ```typescript
305
+ * const subscription = userChoiceBillingListenerAndroid((details) => {
306
+ * console.log('User chose alternative billing');
307
+ * console.log('Products:', details.products);
308
+ * console.log('Token:', details.externalTransactionToken);
309
+ *
310
+ * // Send token to backend for Google Play reporting
311
+ * await reportToGooglePlay(details.externalTransactionToken);
312
+ * });
313
+ *
314
+ * // Later, remove the listener
315
+ * subscription.remove();
316
+ * ```
317
+ */
318
+ type NitroUserChoiceBillingListener = Parameters<
319
+ RnIap['addUserChoiceBillingListenerAndroid']
320
+ >[0];
321
+ const userChoiceBillingListenerMap = new WeakMap<
322
+ (details: any) => void,
323
+ NitroUserChoiceBillingListener
324
+ >();
325
+
326
+ export const userChoiceBillingListenerAndroid = (
327
+ listener: (details: any) => void,
328
+ ): EventSubscription => {
329
+ if (Platform.OS !== 'android') {
330
+ RnIapConsole.warn(
331
+ 'userChoiceBillingListenerAndroid: This listener is only available on Android',
332
+ );
333
+ return {remove: () => {}};
334
+ }
335
+
336
+ const wrappedListener: NitroUserChoiceBillingListener = (details) => {
337
+ listener(details);
338
+ };
339
+
340
+ userChoiceBillingListenerMap.set(listener, wrappedListener);
341
+ let attached = false;
342
+ try {
343
+ IAP.instance.addUserChoiceBillingListenerAndroid(wrappedListener);
344
+ attached = true;
345
+ } catch (e) {
346
+ const msg = toErrorMessage(e);
347
+ if (msg.includes('Nitro runtime not installed')) {
348
+ RnIapConsole.warn(
349
+ '[userChoiceBillingListenerAndroid] Nitro not ready yet; listener inert until initConnection()',
350
+ );
351
+ } else {
352
+ throw e;
353
+ }
354
+ }
355
+
356
+ return {
357
+ remove: () => {
358
+ const wrapped = userChoiceBillingListenerMap.get(listener);
359
+ if (wrapped) {
360
+ if (attached) {
361
+ try {
362
+ IAP.instance.removeUserChoiceBillingListenerAndroid(wrapped);
363
+ } catch {}
364
+ }
365
+ userChoiceBillingListenerMap.delete(listener);
366
+ }
367
+ },
368
+ };
369
+ };
370
+
295
371
  // ------------------------------
296
372
  // Query API
297
373
  // ------------------------------
@@ -832,10 +908,32 @@ export const getTransactionJwsIOS: QueryField<'getTransactionJwsIOS'> = async (
832
908
 
833
909
  /**
834
910
  * Initialize connection to the store
911
+ * @param config - Optional configuration including alternative billing mode for Android
912
+ * @param config.alternativeBillingModeAndroid - Alternative billing mode: 'none', 'user-choice', or 'alternative-only'
913
+ *
914
+ * @example
915
+ * ```typescript
916
+ * // Standard billing (default)
917
+ * await initConnection();
918
+ *
919
+ * // User choice billing (Android)
920
+ * await initConnection({
921
+ * alternativeBillingModeAndroid: 'user-choice'
922
+ * });
923
+ *
924
+ * // Alternative billing only (Android)
925
+ * await initConnection({
926
+ * alternativeBillingModeAndroid: 'alternative-only'
927
+ * });
928
+ * ```
835
929
  */
836
- export const initConnection: MutationField<'initConnection'> = async () => {
930
+ export const initConnection: MutationField<'initConnection'> = async (
931
+ config,
932
+ ) => {
837
933
  try {
838
- return await IAP.instance.initConnection();
934
+ return await IAP.instance.initConnection(
935
+ config as Record<string, unknown> | undefined,
936
+ );
839
937
  } catch (error) {
840
938
  RnIapConsole.error('Failed to initialize IAP connection:', error);
841
939
  throw error;
@@ -1648,3 +1746,216 @@ const normalizeProductQueryType = (
1648
1746
  }
1649
1747
  return 'in-app';
1650
1748
  };
1749
+
1750
+ // ============================================================================
1751
+ // ALTERNATIVE BILLING APIs
1752
+ // ============================================================================
1753
+
1754
+ // ------------------------------
1755
+ // Android Alternative Billing
1756
+ // ------------------------------
1757
+
1758
+ /**
1759
+ * Check if alternative billing is available for this user/device (Android only).
1760
+ * Step 1 of alternative billing flow.
1761
+ *
1762
+ * @returns Promise<boolean> - true if available, false otherwise
1763
+ * @throws Error if billing client not ready
1764
+ * @platform Android
1765
+ *
1766
+ * @example
1767
+ * ```typescript
1768
+ * const isAvailable = await checkAlternativeBillingAvailabilityAndroid();
1769
+ * if (isAvailable) {
1770
+ * // Proceed with alternative billing flow
1771
+ * }
1772
+ * ```
1773
+ */
1774
+ export const checkAlternativeBillingAvailabilityAndroid: MutationField<
1775
+ 'checkAlternativeBillingAvailabilityAndroid'
1776
+ > = async () => {
1777
+ if (Platform.OS !== 'android') {
1778
+ throw new Error('Alternative billing is only supported on Android');
1779
+ }
1780
+ try {
1781
+ return await IAP.instance.checkAlternativeBillingAvailabilityAndroid();
1782
+ } catch (error) {
1783
+ RnIapConsole.error(
1784
+ 'Failed to check alternative billing availability:',
1785
+ error,
1786
+ );
1787
+ throw error;
1788
+ }
1789
+ };
1790
+
1791
+ /**
1792
+ * Show alternative billing information dialog to user (Android only).
1793
+ * Step 2 of alternative billing flow.
1794
+ * Must be called BEFORE processing payment in your payment system.
1795
+ *
1796
+ * @returns Promise<boolean> - true if user accepted, false if user canceled
1797
+ * @throws Error if billing client not ready
1798
+ * @platform Android
1799
+ *
1800
+ * @example
1801
+ * ```typescript
1802
+ * const userAccepted = await showAlternativeBillingDialogAndroid();
1803
+ * if (userAccepted) {
1804
+ * // Process payment in your payment system
1805
+ * const success = await processCustomPayment();
1806
+ * if (success) {
1807
+ * // Create reporting token
1808
+ * const token = await createAlternativeBillingTokenAndroid();
1809
+ * // Send token to your backend for Google Play reporting
1810
+ * }
1811
+ * }
1812
+ * ```
1813
+ */
1814
+ export const showAlternativeBillingDialogAndroid: MutationField<
1815
+ 'showAlternativeBillingDialogAndroid'
1816
+ > = async () => {
1817
+ if (Platform.OS !== 'android') {
1818
+ throw new Error('Alternative billing is only supported on Android');
1819
+ }
1820
+ try {
1821
+ return await IAP.instance.showAlternativeBillingDialogAndroid();
1822
+ } catch (error) {
1823
+ RnIapConsole.error('Failed to show alternative billing dialog:', error);
1824
+ throw error;
1825
+ }
1826
+ };
1827
+
1828
+ /**
1829
+ * Create external transaction token for Google Play reporting (Android only).
1830
+ * Step 3 of alternative billing flow.
1831
+ * Must be called AFTER successful payment in your payment system.
1832
+ * Token must be reported to Google Play backend within 24 hours.
1833
+ *
1834
+ * @param sku - Optional product SKU that was purchased
1835
+ * @returns Promise<string | null> - Token string or null if creation failed
1836
+ * @throws Error if billing client not ready
1837
+ * @platform Android
1838
+ *
1839
+ * @example
1840
+ * ```typescript
1841
+ * const token = await createAlternativeBillingTokenAndroid('premium_subscription');
1842
+ * if (token) {
1843
+ * // Send token to your backend
1844
+ * await fetch('/api/report-transaction', {
1845
+ * method: 'POST',
1846
+ * body: JSON.stringify({ token, sku: 'premium_subscription' })
1847
+ * });
1848
+ * }
1849
+ * ```
1850
+ */
1851
+ export const createAlternativeBillingTokenAndroid: MutationField<
1852
+ 'createAlternativeBillingTokenAndroid'
1853
+ > = async (sku?: string) => {
1854
+ if (Platform.OS !== 'android') {
1855
+ throw new Error('Alternative billing is only supported on Android');
1856
+ }
1857
+ try {
1858
+ return await IAP.instance.createAlternativeBillingTokenAndroid(sku ?? null);
1859
+ } catch (error) {
1860
+ RnIapConsole.error('Failed to create alternative billing token:', error);
1861
+ throw error;
1862
+ }
1863
+ };
1864
+
1865
+ // ------------------------------
1866
+ // iOS External Purchase
1867
+ // ------------------------------
1868
+
1869
+ /**
1870
+ * Check if the device can present an external purchase notice sheet (iOS 18.2+).
1871
+ *
1872
+ * @returns Promise<boolean> - true if notice sheet can be presented
1873
+ * @platform iOS
1874
+ *
1875
+ * @example
1876
+ * ```typescript
1877
+ * const canPresent = await canPresentExternalPurchaseNoticeIOS();
1878
+ * if (canPresent) {
1879
+ * // Present notice before external purchase
1880
+ * const result = await presentExternalPurchaseNoticeSheetIOS();
1881
+ * }
1882
+ * ```
1883
+ */
1884
+ export const canPresentExternalPurchaseNoticeIOS: QueryField<
1885
+ 'canPresentExternalPurchaseNoticeIOS'
1886
+ > = async () => {
1887
+ if (Platform.OS !== 'ios') {
1888
+ return false;
1889
+ }
1890
+ try {
1891
+ return await IAP.instance.canPresentExternalPurchaseNoticeIOS();
1892
+ } catch (error) {
1893
+ RnIapConsole.error(
1894
+ 'Failed to check external purchase notice availability:',
1895
+ error,
1896
+ );
1897
+ return false;
1898
+ }
1899
+ };
1900
+
1901
+ /**
1902
+ * Present an external purchase notice sheet to inform users about external purchases (iOS 18.2+).
1903
+ * This must be called before opening an external purchase link.
1904
+ *
1905
+ * @returns Promise<ExternalPurchaseNoticeResultIOS> - Result with action and error if any
1906
+ * @platform iOS
1907
+ *
1908
+ * @example
1909
+ * ```typescript
1910
+ * const result = await presentExternalPurchaseNoticeSheetIOS();
1911
+ * if (result.result === 'continue') {
1912
+ * // User chose to continue, open external purchase link
1913
+ * await presentExternalPurchaseLinkIOS('https://your-website.com/purchase');
1914
+ * }
1915
+ * ```
1916
+ */
1917
+ export const presentExternalPurchaseNoticeSheetIOS: MutationField<
1918
+ 'presentExternalPurchaseNoticeSheetIOS'
1919
+ > = async () => {
1920
+ if (Platform.OS !== 'ios') {
1921
+ throw new Error('External purchase is only supported on iOS');
1922
+ }
1923
+ try {
1924
+ return (await IAP.instance.presentExternalPurchaseNoticeSheetIOS()) as any;
1925
+ } catch (error) {
1926
+ RnIapConsole.error(
1927
+ 'Failed to present external purchase notice sheet:',
1928
+ error,
1929
+ );
1930
+ throw error;
1931
+ }
1932
+ };
1933
+
1934
+ /**
1935
+ * Present an external purchase link to redirect users to your website (iOS 16.0+).
1936
+ *
1937
+ * @param url - The external purchase URL to open
1938
+ * @returns Promise<ExternalPurchaseLinkResultIOS> - Result with success status and error if any
1939
+ * @platform iOS
1940
+ *
1941
+ * @example
1942
+ * ```typescript
1943
+ * const result = await presentExternalPurchaseLinkIOS('https://your-website.com/purchase');
1944
+ * if (result.success) {
1945
+ * console.log('User completed external purchase');
1946
+ * }
1947
+ * ```
1948
+ */
1949
+ export const presentExternalPurchaseLinkIOS: MutationField<
1950
+ 'presentExternalPurchaseLinkIOS'
1951
+ > = async (url) => {
1952
+ if (Platform.OS !== 'ios') {
1953
+ throw new Error('External purchase is only supported on iOS');
1954
+ }
1955
+ try {
1956
+ return (await IAP.instance.presentExternalPurchaseLinkIOS(url)) as any;
1957
+ } catch (error) {
1958
+ RnIapConsole.error('Failed to present external purchase link:', error);
1959
+ throw error;
1960
+ }
1961
+ };