@umituz/react-native-subscription 2.35.18 → 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.
|
|
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,
|
|
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
|
}
|