@umituz/react-native-subscription 2.35.18 → 2.35.20

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.35.18",
3
+ "version": "2.35.20",
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",
@@ -0,0 +1,79 @@
1
+ /**
2
+ * UserSwitchMutex
3
+ * Prevents concurrent Purchases.logIn() calls that cause RevenueCat 429 errors
4
+ */
5
+
6
+ class UserSwitchMutexImpl {
7
+ private activeSwitchPromise: Promise<any> | null = null;
8
+ private activeUserId: string | null = null;
9
+ private lastSwitchTime = 0;
10
+ private readonly MIN_SWITCH_INTERVAL_MS = 1000; // Minimum 1 second between switches
11
+
12
+ /**
13
+ * Acquires the lock for user switch operation
14
+ * Returns existing promise if a switch is in progress for the same user
15
+ * Waits if a switch is in progress for a different user
16
+ */
17
+ async acquire(userId: string): Promise<{ shouldProceed: boolean; existingPromise?: Promise<any> }> {
18
+ // If a switch is in progress for the exact same user, return the existing promise
19
+ if (this.activeSwitchPromise && this.activeUserId === userId) {
20
+ if (typeof __DEV__ !== 'undefined' && __DEV__) {
21
+ console.log('[UserSwitchMutex] Switch already in progress for this user, returning existing promise');
22
+ }
23
+ return { shouldProceed: false, existingPromise: this.activeSwitchPromise };
24
+ }
25
+
26
+ // If a switch is in progress for a different user, wait for it to complete
27
+ if (this.activeSwitchPromise) {
28
+ if (typeof __DEV__ !== 'undefined' && __DEV__) {
29
+ console.log('[UserSwitchMutex] Waiting for active switch to complete...');
30
+ }
31
+ try {
32
+ await this.activeSwitchPromise;
33
+ } catch (error) {
34
+ // Ignore error, just wait for completion
35
+ }
36
+
37
+ // Add a small delay to avoid rapid-fire requests
38
+ const timeSinceLastSwitch = Date.now() - this.lastSwitchTime;
39
+ if (timeSinceLastSwitch < this.MIN_SWITCH_INTERVAL_MS) {
40
+ const delayNeeded = this.MIN_SWITCH_INTERVAL_MS - timeSinceLastSwitch;
41
+ if (typeof __DEV__ !== 'undefined' && __DEV__) {
42
+ console.log(`[UserSwitchMutex] Rate limiting: waiting ${delayNeeded}ms`);
43
+ }
44
+ await new Promise(resolve => setTimeout(resolve, delayNeeded));
45
+ }
46
+ }
47
+
48
+ this.activeUserId = userId;
49
+ return { shouldProceed: true };
50
+ }
51
+
52
+ /**
53
+ * Sets the active promise for the current switch operation
54
+ */
55
+ setPromise(promise: Promise<any>): void {
56
+ this.activeSwitchPromise = promise;
57
+ this.lastSwitchTime = Date.now();
58
+
59
+ // Clear the lock when the promise completes (success or failure)
60
+ promise
61
+ .finally(() => {
62
+ if (this.activeSwitchPromise === promise) {
63
+ this.activeSwitchPromise = null;
64
+ this.activeUserId = null;
65
+ }
66
+ });
67
+ }
68
+
69
+ /**
70
+ * Resets the mutex state
71
+ */
72
+ reset(): void {
73
+ this.activeSwitchPromise = null;
74
+ this.activeUserId = null;
75
+ this.lastSwitchTime = 0;
76
+ }
77
+ }
78
+
79
+ export const UserSwitchMutex = new UserSwitchMutexImpl();
@@ -3,6 +3,7 @@ import type { InitializeResult } from "../../../../shared/application/ports/IRev
3
3
  import type { InitializerDeps } from "./RevenueCatInitializer.types";
4
4
  import { FAILED_INITIALIZATION_RESULT } from "./initializerConstants";
5
5
  import { syncPremiumStatus } from "../../../subscription/infrastructure/utils/PremiumStatusSyncer";
6
+ import { UserSwitchMutex } from "./UserSwitchMutex";
6
7
 
7
8
  declare const __DEV__: boolean;
8
9
 
@@ -22,6 +23,27 @@ function isAnonymousId(userId: string): boolean {
22
23
  export async function handleUserSwitch(
23
24
  deps: InitializerDeps,
24
25
  userId: string
26
+ ): Promise<InitializeResult> {
27
+ const mutexKey = userId || '__anonymous__';
28
+
29
+ // Acquire mutex to prevent concurrent Purchases.logIn() calls
30
+ const { shouldProceed, existingPromise } = await UserSwitchMutex.acquire(mutexKey);
31
+
32
+ if (!shouldProceed && existingPromise) {
33
+ if (typeof __DEV__ !== 'undefined' && __DEV__) {
34
+ console.log('[UserSwitchHandler] Using result from active switch operation');
35
+ }
36
+ return existingPromise;
37
+ }
38
+
39
+ const switchOperation = performUserSwitch(deps, userId);
40
+ UserSwitchMutex.setPromise(switchOperation);
41
+ return switchOperation;
42
+ }
43
+
44
+ async function performUserSwitch(
45
+ deps: InitializerDeps,
46
+ userId: string
25
47
  ): Promise<InitializeResult> {
26
48
  try {
27
49
  const currentAppUserId = await Purchases.getAppUserID();
@@ -45,7 +67,7 @@ export async function handleUserSwitch(
45
67
  if (typeof __DEV__ !== 'undefined' && __DEV__) {
46
68
  console.log('[UserSwitchHandler] Calling Purchases.logIn() to switch from anonymous to:', normalizedUserId);
47
69
  }
48
- const result = await Purchases.logIn(normalizedUserId);
70
+ const result = await Purchases.logIn(normalizedUserId!);
49
71
  customerInfo = result.customerInfo;
50
72
  if (typeof __DEV__ !== 'undefined' && __DEV__) {
51
73
  console.log('[UserSwitchHandler] ✅ Purchases.logIn() successful, created:', result.created);
@@ -106,7 +128,7 @@ export async function handleInitialConfiguration(
106
128
  });
107
129
  }
108
130
 
109
- await Purchases.configure({ apiKey, appUserID: normalizedUserId });
131
+ await Purchases.configure({ apiKey, appUserID: normalizedUserId || undefined });
110
132
  deps.setInitialized(true);
111
133
  deps.setCurrentUserId(normalizedUserId);
112
134
 
@@ -2,7 +2,12 @@ import { SubscriptionManager } from "../../infrastructure/managers/SubscriptionM
2
2
  import { getCurrentUserId, setupAuthStateListener } from "../SubscriptionAuthListener";
3
3
  import type { SubscriptionInitConfig } from "../SubscriptionInitializerTypes";
4
4
 
5
+ const AUTH_STATE_DEBOUNCE_MS = 500; // Wait 500ms before processing auth state changes
6
+
5
7
  export async function startBackgroundInitialization(config: SubscriptionInitConfig): Promise<() => void> {
8
+ let debounceTimer: NodeJS.Timeout | null = null;
9
+ let lastUserId: string | undefined = undefined;
10
+
6
11
  const initializeInBackground = async (revenueCatUserId?: string): Promise<void> => {
7
12
  if (typeof __DEV__ !== 'undefined' && __DEV__) {
8
13
  console.log('[BackgroundInitializer] initializeInBackground called with userId:', revenueCatUserId || '(undefined - anonymous)');
@@ -10,12 +15,40 @@ export async function startBackgroundInitialization(config: SubscriptionInitConf
10
15
  await SubscriptionManager.initialize(revenueCatUserId);
11
16
  };
12
17
 
18
+ const debouncedInitialize = (revenueCatUserId?: string): void => {
19
+ // Clear any pending initialization
20
+ if (debounceTimer) {
21
+ clearTimeout(debounceTimer);
22
+ }
23
+
24
+ // If userId hasn't changed, skip
25
+ if (lastUserId === revenueCatUserId) {
26
+ if (typeof __DEV__ !== 'undefined' && __DEV__) {
27
+ console.log('[BackgroundInitializer] UserId unchanged, skipping reinitialization');
28
+ }
29
+ return;
30
+ }
31
+
32
+ debounceTimer = setTimeout(async () => {
33
+ if (typeof __DEV__ !== 'undefined' && __DEV__) {
34
+ console.log('[BackgroundInitializer] Auth state listener triggered, reinitializing with userId:', revenueCatUserId || '(undefined - anonymous)');
35
+ }
36
+ try {
37
+ await initializeInBackground(revenueCatUserId);
38
+ lastUserId = revenueCatUserId;
39
+ } catch (_error) {
40
+ // Background re-initialization errors are non-critical, already logged by SubscriptionManager
41
+ }
42
+ }, AUTH_STATE_DEBOUNCE_MS);
43
+ };
44
+
13
45
  const auth = config.getFirebaseAuth();
14
46
  if (!auth) {
15
47
  throw new Error("Firebase auth is not available");
16
48
  }
17
49
 
18
50
  const initialRevenueCatUserId = getCurrentUserId(() => auth);
51
+ lastUserId = initialRevenueCatUserId;
19
52
 
20
53
  if (typeof __DEV__ !== 'undefined' && __DEV__) {
21
54
  console.log('[BackgroundInitializer] Initial RevenueCat userId:', initialRevenueCatUserId || '(undefined - anonymous)');
@@ -23,18 +56,12 @@ export async function startBackgroundInitialization(config: SubscriptionInitConf
23
56
 
24
57
  await initializeInBackground(initialRevenueCatUserId);
25
58
 
26
- const unsubscribe = setupAuthStateListener(() => auth, async (newRevenueCatUserId) => {
27
- if (typeof __DEV__ !== 'undefined' && __DEV__) {
28
- console.log('[BackgroundInitializer] Auth state listener triggered, reinitializing with userId:', newRevenueCatUserId || '(undefined - anonymous)');
29
- }
30
- try {
31
- await initializeInBackground(newRevenueCatUserId);
32
- } catch (_error) {
33
- // Background re-initialization errors are non-critical, already logged by SubscriptionManager
34
- }
35
- });
59
+ const unsubscribe = setupAuthStateListener(() => auth, debouncedInitialize);
36
60
 
37
61
  return () => {
62
+ if (debounceTimer) {
63
+ clearTimeout(debounceTimer);
64
+ }
38
65
  if (unsubscribe) {
39
66
  unsubscribe();
40
67
  }
@@ -66,7 +66,7 @@ class SubscriptionManagerImpl {
66
66
  });
67
67
  }
68
68
 
69
- const { service, success } = await performServiceInitialization(this.managerConfig.config, userId);
69
+ const { service, success } = await performServiceInitialization(this.managerConfig!.config, userId);
70
70
  this.serviceInstance = service ?? null;
71
71
  this.ensurePackageHandlerInitialized();
72
72
 
@@ -119,7 +119,7 @@ class SubscriptionManagerImpl {
119
119
 
120
120
  getEntitlementId(): string {
121
121
  this.ensureConfigured();
122
- return this.managerConfig.config.entitlementIdentifier ?? '';
122
+ return this.managerConfig!.config.entitlementIdentifier ?? '';
123
123
  }
124
124
  }
125
125