react-native-iap 15.2.2 → 15.2.4

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 (35) hide show
  1. package/README.md +2 -2
  2. package/android/src/main/java/com/margelo/nitro/iap/HybridRnIap.kt +24 -8
  3. package/ios/HybridRnIap.swift +163 -70
  4. package/lib/module/hooks/useIAP.js +1 -1
  5. package/lib/module/hooks/useIAP.js.map +1 -1
  6. package/lib/module/index.js +65 -7
  7. package/lib/module/index.js.map +1 -1
  8. package/lib/module/types.js.map +1 -1
  9. package/lib/typescript/src/hooks/useIAP.d.ts +7 -1
  10. package/lib/typescript/src/hooks/useIAP.d.ts.map +1 -1
  11. package/lib/typescript/src/index.d.ts +3 -3
  12. package/lib/typescript/src/index.d.ts.map +1 -1
  13. package/lib/typescript/src/specs/RnIap.nitro.d.ts +6 -4
  14. package/lib/typescript/src/specs/RnIap.nitro.d.ts.map +1 -1
  15. package/lib/typescript/src/types.d.ts +16 -2
  16. package/lib/typescript/src/types.d.ts.map +1 -1
  17. package/nitrogen/generated/android/c++/JHybridRnIapSpec.cpp +11 -6
  18. package/nitrogen/generated/android/c++/JHybridRnIapSpec.hpp +2 -2
  19. package/nitrogen/generated/android/c++/JNitroPurchaseUpdatedListenerOptions.hpp +61 -0
  20. package/nitrogen/generated/android/kotlin/com/margelo/nitro/iap/HybridRnIapSpec.kt +4 -9
  21. package/nitrogen/generated/android/kotlin/com/margelo/nitro/iap/NitroPurchaseUpdatedListenerOptions.kt +38 -0
  22. package/nitrogen/generated/ios/NitroIap-Swift-Cxx-Bridge.hpp +27 -0
  23. package/nitrogen/generated/ios/NitroIap-Swift-Cxx-Umbrella.hpp +3 -0
  24. package/nitrogen/generated/ios/c++/HybridRnIapSpecSwift.hpp +9 -4
  25. package/nitrogen/generated/ios/swift/HybridRnIapSpec.swift +2 -2
  26. package/nitrogen/generated/ios/swift/HybridRnIapSpec_cxx.swift +8 -12
  27. package/nitrogen/generated/ios/swift/NitroPurchaseUpdatedListenerOptions.swift +61 -0
  28. package/nitrogen/generated/shared/c++/HybridRnIapSpec.hpp +5 -2
  29. package/nitrogen/generated/shared/c++/NitroPurchaseUpdatedListenerOptions.hpp +85 -0
  30. package/openiap-versions.json +3 -3
  31. package/package.json +1 -1
  32. package/src/hooks/useIAP.ts +8 -0
  33. package/src/index.ts +105 -11
  34. package/src/specs/RnIap.nitro.ts +10 -5
  35. package/src/types.ts +19 -2
@@ -0,0 +1,85 @@
1
+ ///
2
+ /// NitroPurchaseUpdatedListenerOptions.hpp
3
+ /// This file was generated by nitrogen. DO NOT MODIFY THIS FILE.
4
+ /// https://github.com/mrousavy/nitro
5
+ /// Copyright © Marc Rousavy @ Margelo
6
+ ///
7
+
8
+ #pragma once
9
+
10
+ #if __has_include(<NitroModules/JSIConverter.hpp>)
11
+ #include <NitroModules/JSIConverter.hpp>
12
+ #else
13
+ #error NitroModules cannot be found! Are you sure you installed NitroModules properly?
14
+ #endif
15
+ #if __has_include(<NitroModules/NitroDefines.hpp>)
16
+ #include <NitroModules/NitroDefines.hpp>
17
+ #else
18
+ #error NitroModules cannot be found! Are you sure you installed NitroModules properly?
19
+ #endif
20
+ #if __has_include(<NitroModules/JSIHelpers.hpp>)
21
+ #include <NitroModules/JSIHelpers.hpp>
22
+ #else
23
+ #error NitroModules cannot be found! Are you sure you installed NitroModules properly?
24
+ #endif
25
+ #if __has_include(<NitroModules/PropNameIDCache.hpp>)
26
+ #include <NitroModules/PropNameIDCache.hpp>
27
+ #else
28
+ #error NitroModules cannot be found! Are you sure you installed NitroModules properly?
29
+ #endif
30
+
31
+
32
+
33
+ #include <NitroModules/Null.hpp>
34
+ #include <variant>
35
+ #include <optional>
36
+
37
+ namespace margelo::nitro::iap {
38
+
39
+ /**
40
+ * A struct which can be represented as a JavaScript object (NitroPurchaseUpdatedListenerOptions).
41
+ */
42
+ struct NitroPurchaseUpdatedListenerOptions final {
43
+ public:
44
+ std::optional<std::variant<nitro::NullType, bool>> dedupeTransactionIOS SWIFT_PRIVATE;
45
+
46
+ public:
47
+ NitroPurchaseUpdatedListenerOptions() = default;
48
+ explicit NitroPurchaseUpdatedListenerOptions(std::optional<std::variant<nitro::NullType, bool>> dedupeTransactionIOS): dedupeTransactionIOS(dedupeTransactionIOS) {}
49
+
50
+ public:
51
+ friend bool operator==(const NitroPurchaseUpdatedListenerOptions& lhs, const NitroPurchaseUpdatedListenerOptions& rhs) = default;
52
+ };
53
+
54
+ } // namespace margelo::nitro::iap
55
+
56
+ namespace margelo::nitro {
57
+
58
+ // C++ NitroPurchaseUpdatedListenerOptions <> JS NitroPurchaseUpdatedListenerOptions (object)
59
+ template <>
60
+ struct JSIConverter<margelo::nitro::iap::NitroPurchaseUpdatedListenerOptions> final {
61
+ static inline margelo::nitro::iap::NitroPurchaseUpdatedListenerOptions fromJSI(jsi::Runtime& runtime, const jsi::Value& arg) {
62
+ jsi::Object obj = arg.asObject(runtime);
63
+ return margelo::nitro::iap::NitroPurchaseUpdatedListenerOptions(
64
+ JSIConverter<std::optional<std::variant<nitro::NullType, bool>>>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "dedupeTransactionIOS")))
65
+ );
66
+ }
67
+ static inline jsi::Value toJSI(jsi::Runtime& runtime, const margelo::nitro::iap::NitroPurchaseUpdatedListenerOptions& arg) {
68
+ jsi::Object obj(runtime);
69
+ obj.setProperty(runtime, PropNameIDCache::get(runtime, "dedupeTransactionIOS"), JSIConverter<std::optional<std::variant<nitro::NullType, bool>>>::toJSI(runtime, arg.dedupeTransactionIOS));
70
+ return obj;
71
+ }
72
+ static inline bool canConvert(jsi::Runtime& runtime, const jsi::Value& value) {
73
+ if (!value.isObject()) {
74
+ return false;
75
+ }
76
+ jsi::Object obj = value.getObject(runtime);
77
+ if (!nitro::isPlainObject(runtime, obj)) {
78
+ return false;
79
+ }
80
+ if (!JSIConverter<std::optional<std::variant<nitro::NullType, bool>>>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "dedupeTransactionIOS")))) return false;
81
+ return true;
82
+ }
83
+ };
84
+
85
+ } // namespace margelo::nitro
@@ -1,5 +1,5 @@
1
1
  {
2
- "spec": "2.0.1",
3
- "google": "2.1.4",
4
- "apple": "2.1.7"
2
+ "spec": "2.0.2",
3
+ "google": "2.1.5",
4
+ "apple": "2.1.9"
5
5
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-iap",
3
- "version": "15.2.2",
3
+ "version": "15.2.4",
4
4
  "description": "React Native In-App Purchases module for iOS and Android using Nitro",
5
5
  "main": "./lib/module/index.js",
6
6
  "types": "./lib/typescript/src/index.d.ts",
@@ -48,6 +48,7 @@ import type {
48
48
  Product,
49
49
  Purchase,
50
50
  PurchaseError,
51
+ PurchaseUpdatedListenerOptions,
51
52
  ProductSubscription,
52
53
  } from '../types';
53
54
  import type {MutationFinishTransactionArgs} from '../types';
@@ -268,6 +269,12 @@ type UseIap = {
268
269
 
269
270
  export interface UseIapOptions {
270
271
  onPurchaseSuccess?: (purchase: Purchase) => void;
272
+ /**
273
+ * Options for the purchase success listener. iOS defaults to suppressing
274
+ * StoreKit replay events for the same transaction ID; set
275
+ * `dedupeTransactionIOS` to false only for diagnostics.
276
+ */
277
+ purchaseUpdatedListenerOptions?: PurchaseUpdatedListenerOptions | null;
271
278
  onPurchaseError?: (error: PurchaseError) => void;
272
279
  /** Callback for non-purchase errors (fetchProducts, getAvailablePurchases, etc.) */
273
280
  onError?: (error: Error) => void;
@@ -593,6 +600,7 @@ export function useIAP(options?: UseIapOptions): UseIap {
593
600
  optionsRef.current.onPurchaseSuccess(purchase);
594
601
  }
595
602
  },
603
+ optionsRef.current?.purchaseUpdatedListenerOptions,
596
604
  );
597
605
  }
598
606
 
package/src/index.ts CHANGED
@@ -10,6 +10,7 @@ import type {
10
10
  NitroReceiptValidationParams,
11
11
  NitroReceiptValidationResultIOS,
12
12
  NitroReceiptValidationResultAndroid,
13
+ NitroPurchaseUpdatedListenerOptions,
13
14
  NitroSubscriptionStatus,
14
15
  RnIap,
15
16
  } from './specs/RnIap.nitro';
@@ -30,6 +31,7 @@ import type {
30
31
  ProductSubscription,
31
32
  Purchase,
32
33
  PurchaseError,
34
+ PurchaseUpdatedListenerOptions,
33
35
  PurchaseIOS,
34
36
  QueryField,
35
37
  AppTransaction,
@@ -97,6 +99,9 @@ type NitroFinishTransactionParamsInternal = Parameters<
97
99
  RnIap['finishTransaction']
98
100
  >[0];
99
101
  type NitroPurchaseListener = Parameters<RnIap['addPurchaseUpdatedListener']>[0];
102
+ type NitroPurchaseUpdatedListenerOptionsParam = NonNullable<
103
+ Parameters<RnIap['addPurchaseUpdatedListener']>[1]
104
+ >;
100
105
  type NitroPurchaseErrorListener = Parameters<
101
106
  RnIap['addPurchaseErrorListener']
102
107
  >[0];
@@ -129,10 +134,7 @@ export type {
129
134
  UseWebhookEventsOptions,
130
135
  UseWebhookEventsResult,
131
136
  } from './hooks/useWebhookEvents';
132
- export {
133
- connectWebhookStream,
134
- parseWebhookEventData,
135
- } from './webhook-client';
137
+ export {connectWebhookStream, parseWebhookEventData} from './webhook-client';
136
138
  export type {
137
139
  WebhookEventPayload,
138
140
  WebhookEventStream,
@@ -232,11 +234,20 @@ const IAP = {
232
234
  // ============================================================================
233
235
 
234
236
  const purchaseUpdateJsListeners = new Set<(purchase: Purchase) => void>();
237
+ const purchaseUpdateDuplicateJsListeners = new Set<
238
+ (purchase: Purchase) => void
239
+ >();
235
240
  let purchaseUpdateNativeAttached = false;
236
- const purchaseUpdateNativeHandler: NitroPurchaseListener = (nitroPurchase) => {
241
+ let purchaseUpdateDuplicateNativeAttached = false;
242
+ let purchaseUpdateNativeToken: number | null = null;
243
+ let purchaseUpdateDuplicateNativeToken: number | null = null;
244
+ const emitPurchaseUpdateToListeners = (
245
+ nitroPurchase: Parameters<NitroPurchaseListener>[0],
246
+ listeners: Set<(purchase: Purchase) => void>,
247
+ ) => {
237
248
  if (validateNitroPurchase(nitroPurchase)) {
238
249
  const convertedPurchase = convertNitroPurchaseToPurchase(nitroPurchase);
239
- for (const listener of purchaseUpdateJsListeners) {
250
+ for (const listener of listeners) {
240
251
  try {
241
252
  listener(convertedPurchase);
242
253
  } catch (e) {
@@ -250,6 +261,17 @@ const purchaseUpdateNativeHandler: NitroPurchaseListener = (nitroPurchase) => {
250
261
  );
251
262
  }
252
263
  };
264
+ const purchaseUpdateNativeHandler: NitroPurchaseListener = (nitroPurchase) => {
265
+ emitPurchaseUpdateToListeners(nitroPurchase, purchaseUpdateJsListeners);
266
+ };
267
+ const purchaseUpdateDuplicateNativeHandler: NitroPurchaseListener = (
268
+ nitroPurchase,
269
+ ) => {
270
+ emitPurchaseUpdateToListeners(
271
+ nitroPurchase,
272
+ purchaseUpdateDuplicateJsListeners,
273
+ );
274
+ };
253
275
 
254
276
  const purchaseErrorJsListeners = new Set<(error: PurchaseError) => void>();
255
277
  let purchaseErrorNativeAttached = false;
@@ -299,6 +321,9 @@ const promotedProductNativeHandler: NitroPromotedProductListener = (
299
321
  */
300
322
  export const resetListenerState = (): void => {
301
323
  purchaseUpdateNativeAttached = false;
324
+ purchaseUpdateDuplicateNativeAttached = false;
325
+ purchaseUpdateNativeToken = null;
326
+ purchaseUpdateDuplicateNativeToken = null;
302
327
  purchaseErrorNativeAttached = false;
303
328
  promotedProductNativeAttached = false;
304
329
  userChoiceBillingNativeAttached = false;
@@ -306,6 +331,7 @@ export const resetListenerState = (): void => {
306
331
  subscriptionBillingIssueNativeAttached = false;
307
332
  // Clear all JS listeners since native side clears them in endConnection
308
333
  purchaseUpdateJsListeners.clear();
334
+ purchaseUpdateDuplicateJsListeners.clear();
309
335
  purchaseErrorJsListeners.clear();
310
336
  promotedProductJsListeners.clear();
311
337
  userChoiceBillingJsListeners.clear();
@@ -315,12 +341,22 @@ export const resetListenerState = (): void => {
315
341
 
316
342
  export const purchaseUpdatedListener = (
317
343
  listener: (purchase: Purchase) => void,
344
+ options?: PurchaseUpdatedListenerOptions | null,
318
345
  ): EventSubscription => {
319
- purchaseUpdateJsListeners.add(listener);
346
+ const receiveDuplicateTransactionUpdatesIOS =
347
+ Platform.OS === 'ios' && options?.dedupeTransactionIOS === false;
348
+ const listeners = receiveDuplicateTransactionUpdatesIOS
349
+ ? purchaseUpdateDuplicateJsListeners
350
+ : purchaseUpdateJsListeners;
320
351
 
321
- if (!purchaseUpdateNativeAttached) {
352
+ listeners.add(listener);
353
+
354
+ if (!purchaseUpdateNativeAttached && !receiveDuplicateTransactionUpdatesIOS) {
322
355
  try {
323
- IAP.instance.addPurchaseUpdatedListener(purchaseUpdateNativeHandler);
356
+ const token = IAP.instance.addPurchaseUpdatedListener(
357
+ purchaseUpdateNativeHandler,
358
+ );
359
+ purchaseUpdateNativeToken = typeof token === 'number' ? token : null;
324
360
  purchaseUpdateNativeAttached = true;
325
361
  } catch (e) {
326
362
  const msg = toErrorMessage(e);
@@ -334,9 +370,65 @@ export const purchaseUpdatedListener = (
334
370
  }
335
371
  }
336
372
 
373
+ if (
374
+ !purchaseUpdateDuplicateNativeAttached &&
375
+ receiveDuplicateTransactionUpdatesIOS
376
+ ) {
377
+ try {
378
+ const nativeOptions: NitroPurchaseUpdatedListenerOptions &
379
+ NitroPurchaseUpdatedListenerOptionsParam = {
380
+ dedupeTransactionIOS: false,
381
+ };
382
+ const token = IAP.instance.addPurchaseUpdatedListener(
383
+ purchaseUpdateDuplicateNativeHandler,
384
+ nativeOptions,
385
+ );
386
+ purchaseUpdateDuplicateNativeToken =
387
+ typeof token === 'number' ? token : null;
388
+ purchaseUpdateDuplicateNativeAttached = true;
389
+ } catch (e) {
390
+ const msg = toErrorMessage(e);
391
+ if (msg.includes('Nitro runtime not installed')) {
392
+ RnIapConsole.warn(
393
+ '[purchaseUpdatedListener] Nitro not ready yet; listener inert until initConnection()',
394
+ );
395
+ } else {
396
+ throw e;
397
+ }
398
+ }
399
+ }
400
+
401
+ let removed = false;
337
402
  return {
338
403
  remove: () => {
339
- purchaseUpdateJsListeners.delete(listener);
404
+ if (removed) {
405
+ return;
406
+ }
407
+ removed = true;
408
+ listeners.delete(listener);
409
+ if (listeners.size > 0) {
410
+ return;
411
+ }
412
+
413
+ const token = receiveDuplicateTransactionUpdatesIOS
414
+ ? purchaseUpdateDuplicateNativeToken
415
+ : purchaseUpdateNativeToken;
416
+ if (token == null) {
417
+ return;
418
+ }
419
+
420
+ try {
421
+ IAP.instance.removePurchaseUpdatedListener(token);
422
+ if (receiveDuplicateTransactionUpdatesIOS) {
423
+ purchaseUpdateDuplicateNativeToken = null;
424
+ purchaseUpdateDuplicateNativeAttached = false;
425
+ } else {
426
+ purchaseUpdateNativeToken = null;
427
+ purchaseUpdateNativeAttached = false;
428
+ }
429
+ } catch (e) {
430
+ RnIapConsole.warn('[purchaseUpdatedListener] native remove failed:', e);
431
+ }
340
432
  },
341
433
  };
342
434
  };
@@ -619,7 +711,9 @@ type NitroSubscriptionBillingIssueListener = Parameters<
619
711
  RnIap['addSubscriptionBillingIssueListener']
620
712
  >[0];
621
713
 
622
- const subscriptionBillingIssueJsListeners = new Set<(purchase: Purchase) => void>();
714
+ const subscriptionBillingIssueJsListeners = new Set<
715
+ (purchase: Purchase) => void
716
+ >();
623
717
  let subscriptionBillingIssueNativeAttached = false;
624
718
  const subscriptionBillingIssueNativeHandler: NitroSubscriptionBillingIssueListener =
625
719
  (nitroPurchase) => {
@@ -40,6 +40,7 @@ import type {
40
40
  PromotionalOfferJwsInputIOS,
41
41
  PurchaseCommon,
42
42
  PurchaseOptions,
43
+ PurchaseUpdatedListenerOptions,
43
44
  VerifyPurchaseAppleOptions,
44
45
  VerifyPurchaseGoogleOptions,
45
46
  VerifyPurchaseHorizonOptions,
@@ -146,6 +147,9 @@ export interface NitroReceiptValidationHorizonOptions {
146
147
  userId: VerifyPurchaseHorizonOptions['userId'];
147
148
  }
148
149
 
150
+ export interface NitroPurchaseUpdatedListenerOptions
151
+ extends PurchaseUpdatedListenerOptions {}
152
+
149
153
  export interface NitroReceiptValidationParams {
150
154
  apple?: NitroReceiptValidationAppleOptions | null;
151
155
  google?: NitroReceiptValidationGoogleOptions | null;
@@ -725,7 +729,10 @@ export interface RnIap extends HybridObject<{ios: 'swift'; android: 'kotlin'}> {
725
729
  * Add a listener for purchase updates
726
730
  * @param listener - Function to call when a purchase is updated
727
731
  */
728
- addPurchaseUpdatedListener(listener: (purchase: NitroPurchase) => void): void;
732
+ addPurchaseUpdatedListener(
733
+ listener: (purchase: NitroPurchase) => void,
734
+ options?: NitroPurchaseUpdatedListenerOptions,
735
+ ): number;
729
736
 
730
737
  /**
731
738
  * Add a listener for purchase errors
@@ -737,11 +744,9 @@ export interface RnIap extends HybridObject<{ios: 'swift'; android: 'kotlin'}> {
737
744
 
738
745
  /**
739
746
  * Remove a purchase updated listener
740
- * @param listener - Function to remove from listeners
747
+ * @param token - Token returned from addPurchaseUpdatedListener
741
748
  */
742
- removePurchaseUpdatedListener(
743
- listener: (purchase: NitroPurchase) => void,
744
- ): void;
749
+ removePurchaseUpdatedListener(token: number): void;
745
750
 
746
751
  /**
747
752
  * Remove a purchase error listener
package/src/types.ts CHANGED
@@ -1292,6 +1292,15 @@ export interface PurchaseOptions {
1292
1292
 
1293
1293
  export type PurchaseState = 'pending' | 'purchased' | 'unknown';
1294
1294
 
1295
+ export interface PurchaseUpdatedListenerOptions {
1296
+ /**
1297
+ * iOS only. Defaults to true. When false, listener callbacks also receive
1298
+ * StoreKit replay events for a transaction ID that was already emitted during
1299
+ * the current connection session. Android ignores this option.
1300
+ */
1301
+ dedupeTransactionIOS?: (boolean | null);
1302
+ }
1303
+
1295
1304
  export type PurchaseVerificationProvider = 'iapkit';
1296
1305
 
1297
1306
  export interface Query {
@@ -1734,7 +1743,12 @@ export interface Subscription {
1734
1743
  promotedProductIOS: string;
1735
1744
  /** Fires when a purchase fails or is cancelled */
1736
1745
  purchaseError: PurchaseError;
1737
- /** Fires when a purchase completes successfully or a pending purchase resolves */
1746
+ /**
1747
+ * Fires when a purchase completes successfully or a pending purchase resolves
1748
+ * Options can opt iOS listeners into duplicate StoreKit transaction replays
1749
+ * for diagnostics; default listeners receive one event per transaction ID
1750
+ * during a single connection session.
1751
+ */
1738
1752
  purchaseUpdated: Purchase;
1739
1753
  /**
1740
1754
  * Fires when an active subscription enters a billing-issue state that needs user action
@@ -1758,6 +1772,9 @@ export interface Subscription {
1758
1772
  }
1759
1773
 
1760
1774
 
1775
+
1776
+ export type SubscriptionPurchaseUpdatedArgs = (PurchaseUpdatedListenerOptions | null) | undefined;
1777
+
1761
1778
  export interface SubscriptionInfoIOS {
1762
1779
  introductoryOffer?: (SubscriptionOfferIOS | null);
1763
1780
  promotionalOffers?: (SubscriptionOfferIOS[] | null);
@@ -2218,7 +2235,7 @@ export type SubscriptionArgsMap = {
2218
2235
  developerProvidedBillingAndroid: never;
2219
2236
  promotedProductIOS: never;
2220
2237
  purchaseError: never;
2221
- purchaseUpdated: never;
2238
+ purchaseUpdated: SubscriptionPurchaseUpdatedArgs;
2222
2239
  subscriptionBillingIssue: never;
2223
2240
  userChoiceBillingAndroid: never;
2224
2241
  };