@umituz/react-native-subscription 2.22.0 → 2.22.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@umituz/react-native-subscription",
3
- "version": "2.22.0",
3
+ "version": "2.22.1",
4
4
  "description": "Complete subscription management with RevenueCat, paywall UI, and credits system for React Native apps",
5
5
  "main": "./src/index.ts",
6
6
  "types": "./src/index.ts",
@@ -1,9 +1,13 @@
1
1
  /**
2
2
  * Purchase Handler
3
3
  * Handles RevenueCat purchase operations for both subscriptions and consumables
4
+ *
5
+ * IMPORTANT: Uses race condition with CustomerInfo listener to handle
6
+ * known RevenueCat bug where purchasePackage can hang indefinitely.
7
+ * @see https://github.com/RevenueCat/react-native-purchases/issues/1082
4
8
  */
5
9
 
6
- import Purchases, { type PurchasesPackage } from "react-native-purchases";
10
+ import Purchases, { type PurchasesPackage, type CustomerInfo } from "react-native-purchases";
7
11
  import type { PurchaseResult } from "../../application/ports/IRevenueCatService";
8
12
  import {
9
13
  RevenueCatPurchaseError,
@@ -20,6 +24,50 @@ import {
20
24
  } from "../utils/PremiumStatusSyncer";
21
25
  import { getSavedPurchase, clearSavedPurchase } from "../../../presentation/hooks/useAuthAwarePurchase";
22
26
 
27
+ const PURCHASE_LISTENER_TIMEOUT_MS = 30000; // 30 seconds fallback timeout
28
+
29
+ /**
30
+ * Creates a promise that resolves when CustomerInfo listener detects premium
31
+ * This is a workaround for RevenueCat bug where purchasePackage can hang
32
+ */
33
+ function createPremiumListenerPromise(
34
+ entitlementId: string
35
+ ): { promise: Promise<CustomerInfo>; cleanup: () => void } {
36
+ let cleanup: () => void = () => {};
37
+ let timeoutId: ReturnType<typeof setTimeout> | null = null;
38
+
39
+ const promise = new Promise<CustomerInfo>((resolve, reject) => {
40
+ const listener = (info: CustomerInfo) => {
41
+ const isPremium = !!info.entitlements.active[entitlementId];
42
+ if (isPremium) {
43
+ if (__DEV__) {
44
+ console.log('[DEBUG PurchaseHandler] Listener detected premium!');
45
+ }
46
+ cleanup();
47
+ resolve(info);
48
+ }
49
+ };
50
+
51
+ Purchases.addCustomerInfoUpdateListener(listener);
52
+
53
+ // Fallback timeout - if neither purchasePackage nor listener resolves
54
+ timeoutId = setTimeout(() => {
55
+ cleanup();
56
+ reject(new Error('Purchase timed out. Please try again.'));
57
+ }, PURCHASE_LISTENER_TIMEOUT_MS);
58
+
59
+ cleanup = () => {
60
+ Purchases.removeCustomerInfoUpdateListener(listener);
61
+ if (timeoutId) {
62
+ clearTimeout(timeoutId);
63
+ timeoutId = null;
64
+ }
65
+ };
66
+ });
67
+
68
+ return { promise, cleanup };
69
+ }
70
+
23
71
  export interface PurchaseHandlerDeps {
24
72
  config: RevenueCatConfig;
25
73
  isInitialized: () => boolean;
@@ -63,9 +111,13 @@ export async function handlePurchase(
63
111
  const consumableIds = deps.config.consumableProductIdentifiers || [];
64
112
  const isConsumable = isConsumableProduct(pkg, consumableIds);
65
113
 
114
+ // Set up listener-based detection as fallback for purchasePackage bug
115
+ const entitlementIdentifier = deps.config.entitlementIdentifier;
116
+ const { promise: listenerPromise, cleanup: cleanupListener } = createPremiumListenerPromise(entitlementIdentifier);
117
+
66
118
  try {
67
119
  if (__DEV__) {
68
- console.log('[DEBUG PurchaseHandler] Calling Purchases.purchasePackage...', {
120
+ console.log('[DEBUG PurchaseHandler] Calling Purchases.purchasePackage with listener fallback...', {
69
121
  productId: pkg.product.identifier,
70
122
  packageIdentifier: pkg.identifier,
71
123
  offeringIdentifier: pkg.offeringIdentifier,
@@ -74,12 +126,28 @@ export async function handlePurchase(
74
126
  }
75
127
 
76
128
  const startTime = Date.now();
77
- const purchaseResult = await Purchases.purchasePackage(pkg);
129
+
130
+ // Race between purchasePackage and listener detection
131
+ // This handles the known RevenueCat bug where purchasePackage can hang
132
+ const purchasePromise = Purchases.purchasePackage(pkg).then(result => ({
133
+ source: 'purchasePackage' as const,
134
+ customerInfo: result.customerInfo
135
+ }));
136
+
137
+ const listenerWithSource = listenerPromise.then(info => ({
138
+ source: 'listener' as const,
139
+ customerInfo: info
140
+ }));
141
+
142
+ const result = await Promise.race([purchasePromise, listenerWithSource]);
143
+ cleanupListener();
144
+
78
145
  const duration = Date.now() - startTime;
79
- const customerInfo = purchaseResult.customerInfo;
146
+ const customerInfo = result.customerInfo;
80
147
 
81
148
  if (__DEV__) {
82
- console.log('[DEBUG PurchaseHandler] Purchases.purchasePackage returned', {
149
+ console.log('[DEBUG PurchaseHandler] Purchase resolved via:', {
150
+ source: result.source,
83
151
  duration: `${duration}ms`,
84
152
  productId: pkg.product.identifier
85
153
  });
@@ -118,7 +186,6 @@ export async function handlePurchase(
118
186
  };
119
187
  }
120
188
 
121
- const entitlementIdentifier = deps.config.entitlementIdentifier;
122
189
  const isPremium = !!customerInfo.entitlements.active[entitlementIdentifier];
123
190
 
124
191
  if (__DEV__) {
@@ -173,6 +240,9 @@ export async function handlePurchase(
173
240
  pkg.product.identifier
174
241
  );
175
242
  } catch (error) {
243
+ // Ensure listener cleanup on error
244
+ cleanupListener();
245
+
176
246
  if (__DEV__) {
177
247
  console.error('[DEBUG PurchaseHandler] Purchase error caught', {
178
248
  error,