@umituz/react-native-subscription 2.35.17 → 2.35.19

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.17",
3
+ "version": "2.35.19",
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();
@@ -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
  }