@umituz/react-native-subscription 2.35.22 → 2.35.24
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 +1 -1
- package/src/domains/revenuecat/infrastructure/services/ConfigurationStateManager.ts +18 -12
- package/src/domains/revenuecat/infrastructure/services/RevenueCatInitializer.types.ts +1 -1
- package/src/domains/revenuecat/infrastructure/services/userSwitchHandler.ts +33 -2
- package/src/domains/subscription/application/initializer/BackgroundInitializer.ts +5 -1
- package/src/domains/subscription/infrastructure/hooks/customer-info/useCustomerInfo.ts +41 -46
- package/src/domains/subscription/infrastructure/hooks/subscriptionQueryKeys.ts +1 -0
- package/src/domains/subscription/infrastructure/hooks/useSubscriptionPackages.ts +8 -11
- package/src/domains/subscription/infrastructure/services/listeners/CustomerInfoHandler.ts +19 -3
- package/src/domains/subscription/infrastructure/utils/InitializationCache.ts +25 -27
- package/src/domains/subscription/infrastructure/utils/PremiumStatusSyncer.ts +10 -1
- package/src/domains/subscription/presentation/useFeatureGate.ts +4 -3
- package/src/domains/subscription/presentation/useSubscriptionStatus.ts +5 -5
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.24",
|
|
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",
|
|
@@ -3,6 +3,7 @@ import type { InitializeResult } from "../../../../shared/application/ports/IRev
|
|
|
3
3
|
export class ConfigurationStateManager {
|
|
4
4
|
private _isPurchasesConfigured = false;
|
|
5
5
|
private _configurationPromise: Promise<InitializeResult> | null = null;
|
|
6
|
+
private _resolveConfiguration: ((value: InitializeResult) => void) | null = null;
|
|
6
7
|
|
|
7
8
|
get isPurchasesConfigured(): boolean {
|
|
8
9
|
return this._isPurchasesConfigured;
|
|
@@ -21,16 +22,17 @@ export class ConfigurationStateManager {
|
|
|
21
22
|
throw new Error('Configuration already in progress');
|
|
22
23
|
}
|
|
23
24
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
capturedResolve = resolve;
|
|
25
|
+
// Create promise and store resolve function atomically
|
|
26
|
+
this._configurationPromise = new Promise<InitializeResult>((resolve) => {
|
|
27
|
+
this._resolveConfiguration = resolve;
|
|
28
28
|
});
|
|
29
29
|
|
|
30
|
+
// Return resolve function
|
|
30
31
|
return (value: InitializeResult) => {
|
|
31
|
-
if (
|
|
32
|
-
|
|
33
|
-
|
|
32
|
+
if (this._resolveConfiguration) {
|
|
33
|
+
const resolve = this._resolveConfiguration;
|
|
34
|
+
this._resolveConfiguration = null;
|
|
35
|
+
resolve(value);
|
|
34
36
|
}
|
|
35
37
|
};
|
|
36
38
|
}
|
|
@@ -38,18 +40,22 @@ export class ConfigurationStateManager {
|
|
|
38
40
|
completeConfiguration(success: boolean): void {
|
|
39
41
|
this._isPurchasesConfigured = success;
|
|
40
42
|
|
|
41
|
-
|
|
43
|
+
// Cleanup promise state immediately (no setTimeout)
|
|
44
|
+
// If promise hasn't resolved yet, that's fine - it will still resolve via the callback
|
|
45
|
+
if (this._configurationPromise) {
|
|
42
46
|
this._configurationPromise = null;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Clear resolve function if it still exists
|
|
50
|
+
if (this._resolveConfiguration) {
|
|
51
|
+
this._resolveConfiguration = null;
|
|
47
52
|
}
|
|
48
53
|
}
|
|
49
54
|
|
|
50
55
|
reset(): void {
|
|
51
56
|
this._isPurchasesConfigured = false;
|
|
52
57
|
this._configurationPromise = null;
|
|
58
|
+
this._resolveConfiguration = null;
|
|
53
59
|
}
|
|
54
60
|
}
|
|
55
61
|
|
|
@@ -5,5 +5,5 @@ export interface InitializerDeps {
|
|
|
5
5
|
isInitialized: () => boolean;
|
|
6
6
|
getCurrentUserId: () => string | null;
|
|
7
7
|
setInitialized: (value: boolean) => void;
|
|
8
|
-
setCurrentUserId: (userId: string | undefined) => void;
|
|
8
|
+
setCurrentUserId: (userId: string | null | undefined) => void;
|
|
9
9
|
}
|
|
@@ -2,8 +2,8 @@ import Purchases, { type CustomerInfo } from "react-native-purchases";
|
|
|
2
2
|
import type { InitializeResult } from "../../../../shared/application/ports/IRevenueCatService";
|
|
3
3
|
import type { InitializerDeps } from "./RevenueCatInitializer.types";
|
|
4
4
|
import { FAILED_INITIALIZATION_RESULT } from "./initializerConstants";
|
|
5
|
-
import { syncPremiumStatus } from "../../../subscription/infrastructure/utils/PremiumStatusSyncer";
|
|
6
5
|
import { UserSwitchMutex } from "./UserSwitchMutex";
|
|
6
|
+
import { getPremiumEntitlement } from "../../core/types";
|
|
7
7
|
|
|
8
8
|
declare const __DEV__: boolean;
|
|
9
9
|
|
|
@@ -151,7 +151,38 @@ export async function handleInitialConfiguration(
|
|
|
151
151
|
});
|
|
152
152
|
}
|
|
153
153
|
|
|
154
|
-
|
|
154
|
+
// Sync premium status via callback (if configured)
|
|
155
|
+
if (deps.config.onPremiumStatusChanged) {
|
|
156
|
+
try {
|
|
157
|
+
const premiumEntitlement = getPremiumEntitlement(
|
|
158
|
+
customerInfo,
|
|
159
|
+
deps.config.entitlementIdentifier
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
if (premiumEntitlement) {
|
|
163
|
+
await deps.config.onPremiumStatusChanged(
|
|
164
|
+
currentUserId,
|
|
165
|
+
true,
|
|
166
|
+
premiumEntitlement.productIdentifier,
|
|
167
|
+
premiumEntitlement.expirationDate ?? undefined,
|
|
168
|
+
premiumEntitlement.willRenew,
|
|
169
|
+
premiumEntitlement.periodType as "NORMAL" | "INTRO" | "TRIAL" | undefined
|
|
170
|
+
);
|
|
171
|
+
} else {
|
|
172
|
+
await deps.config.onPremiumStatusChanged(
|
|
173
|
+
currentUserId,
|
|
174
|
+
false,
|
|
175
|
+
undefined,
|
|
176
|
+
undefined,
|
|
177
|
+
undefined,
|
|
178
|
+
undefined
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
} catch (error) {
|
|
182
|
+
// Log error but don't fail initialization
|
|
183
|
+
console.error('[UserSwitchHandler] Premium status sync callback failed:', error);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
155
186
|
|
|
156
187
|
return buildSuccessResult(deps, customerInfo, offerings);
|
|
157
188
|
} catch (error) {
|
|
@@ -36,8 +36,12 @@ export async function startBackgroundInitialization(config: SubscriptionInitConf
|
|
|
36
36
|
try {
|
|
37
37
|
await initializeInBackground(revenueCatUserId);
|
|
38
38
|
lastUserId = revenueCatUserId;
|
|
39
|
-
} catch (
|
|
39
|
+
} catch (error) {
|
|
40
40
|
// Background re-initialization errors are non-critical, already logged by SubscriptionManager
|
|
41
|
+
console.error('[BackgroundInitializer] Reinitialization failed:', {
|
|
42
|
+
userId: revenueCatUserId,
|
|
43
|
+
error: error instanceof Error ? error.message : String(error)
|
|
44
|
+
});
|
|
41
45
|
}
|
|
42
46
|
}, AUTH_STATE_DEBOUNCE_MS);
|
|
43
47
|
};
|
|
@@ -1,57 +1,52 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Customer Info Hook
|
|
3
|
+
* Fetches customer info without registering a listener
|
|
4
|
+
* CustomerInfoListenerManager handles all listener logic
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { useQuery, useQueryClient } from "@umituz/react-native-design-system";
|
|
8
|
+
import { useEffect, useRef } from "react";
|
|
9
|
+
import Purchases from "react-native-purchases";
|
|
3
10
|
import type { UseCustomerInfoResult } from "./types";
|
|
11
|
+
import { SUBSCRIPTION_QUERY_KEYS } from "../subscriptionQueryKeys";
|
|
4
12
|
|
|
5
13
|
export function useCustomerInfo(): UseCustomerInfoResult {
|
|
6
|
-
const
|
|
7
|
-
const
|
|
8
|
-
const [isFetching, setIsFetching] = useState(false);
|
|
9
|
-
const [error, setError] = useState<string | null>(null);
|
|
10
|
-
|
|
11
|
-
const fetchCustomerInfo = useCallback(async () => {
|
|
12
|
-
try {
|
|
13
|
-
setIsFetching(true);
|
|
14
|
-
setError(null);
|
|
15
|
-
|
|
16
|
-
const info = await Purchases.getCustomerInfo();
|
|
17
|
-
|
|
18
|
-
setCustomerInfo(info);
|
|
19
|
-
} catch (err) {
|
|
20
|
-
const errorMessage =
|
|
21
|
-
err instanceof Error ? err.message : "Failed to fetch customer info";
|
|
22
|
-
setError(errorMessage);
|
|
23
|
-
} finally {
|
|
24
|
-
setLoading(false);
|
|
25
|
-
setIsFetching(false);
|
|
26
|
-
}
|
|
27
|
-
}, []);
|
|
28
|
-
|
|
29
|
-
const listenerRef = useRef<((info: CustomerInfo) => void) | null>(null);
|
|
14
|
+
const queryClient = useQueryClient();
|
|
15
|
+
const mountedRef = useRef(true);
|
|
30
16
|
|
|
31
17
|
useEffect(() => {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
const listener = (info: CustomerInfo) => {
|
|
35
|
-
setCustomerInfo(info);
|
|
36
|
-
setError(null);
|
|
37
|
-
};
|
|
38
|
-
|
|
39
|
-
listenerRef.current = listener;
|
|
40
|
-
Purchases.addCustomerInfoUpdateListener(listener);
|
|
41
|
-
|
|
18
|
+
mountedRef.current = true;
|
|
42
19
|
return () => {
|
|
43
|
-
|
|
44
|
-
Purchases.removeCustomerInfoUpdateListener(listenerRef.current);
|
|
45
|
-
listenerRef.current = null;
|
|
46
|
-
}
|
|
20
|
+
mountedRef.current = false;
|
|
47
21
|
};
|
|
48
|
-
}, [
|
|
22
|
+
}, []);
|
|
23
|
+
|
|
24
|
+
const query = useQuery({
|
|
25
|
+
queryKey: SUBSCRIPTION_QUERY_KEYS.customerInfo,
|
|
26
|
+
queryFn: async () => {
|
|
27
|
+
const info = await Purchases.getCustomerInfo();
|
|
28
|
+
return info;
|
|
29
|
+
},
|
|
30
|
+
staleTime: 30 * 1000, // 30 seconds
|
|
31
|
+
gcTime: 5 * 60 * 1000, // 5 minutes
|
|
32
|
+
refetchOnMount: true,
|
|
33
|
+
refetchOnWindowFocus: false,
|
|
34
|
+
retry: 2,
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
// Expose refetch as a method
|
|
38
|
+
const refetch = async () => {
|
|
39
|
+
if (!mountedRef.current) return;
|
|
40
|
+
await queryClient.invalidateQueries({
|
|
41
|
+
queryKey: SUBSCRIPTION_QUERY_KEYS.customerInfo,
|
|
42
|
+
});
|
|
43
|
+
};
|
|
49
44
|
|
|
50
45
|
return {
|
|
51
|
-
customerInfo,
|
|
52
|
-
loading,
|
|
53
|
-
error,
|
|
54
|
-
refetch
|
|
55
|
-
isFetching,
|
|
46
|
+
customerInfo: query.data ?? null,
|
|
47
|
+
loading: query.isLoading,
|
|
48
|
+
error: query.error?.message ?? null,
|
|
49
|
+
refetch,
|
|
50
|
+
isFetching: query.isFetching,
|
|
56
51
|
};
|
|
57
52
|
}
|
|
@@ -13,6 +13,7 @@ export const SUBSCRIPTION_QUERY_KEYS = {
|
|
|
13
13
|
packages: ["subscription", "packages"] as const,
|
|
14
14
|
initialized: (userId: string) =>
|
|
15
15
|
["subscription", "initialized", userId] as const,
|
|
16
|
+
customerInfo: ["subscription", "customerInfo"] as const,
|
|
16
17
|
} as const;
|
|
17
18
|
|
|
18
19
|
|
|
@@ -26,22 +26,19 @@ export const useSubscriptionPackages = () => {
|
|
|
26
26
|
const queryClient = useQueryClient();
|
|
27
27
|
const prevUserIdRef = useRef(userId);
|
|
28
28
|
|
|
29
|
+
// Check if initialized (BackgroundInitializer handles initialization)
|
|
30
|
+
const isInitialized = userId
|
|
31
|
+
? SubscriptionManager.isInitializedForUser(userId)
|
|
32
|
+
: SubscriptionManager.isInitialized();
|
|
33
|
+
|
|
29
34
|
const query = useQuery({
|
|
30
35
|
queryKey: [...SUBSCRIPTION_QUERY_KEYS.packages, userId ?? "anonymous"] as const,
|
|
31
36
|
queryFn: async () => {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
await SubscriptionManager.initialize(userId);
|
|
35
|
-
}
|
|
36
|
-
} else {
|
|
37
|
-
if (!SubscriptionManager.isInitialized()) {
|
|
38
|
-
await SubscriptionManager.initialize(undefined);
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
|
|
37
|
+
// No side effects - just fetch packages
|
|
38
|
+
// Initialization is handled by BackgroundInitializer
|
|
42
39
|
return SubscriptionManager.getPackages();
|
|
43
40
|
},
|
|
44
|
-
enabled: isConfigured,
|
|
41
|
+
enabled: isConfigured && isInitialized,
|
|
45
42
|
gcTime: 5 * 60 * 1000,
|
|
46
43
|
staleTime: 2 * 60 * 1000,
|
|
47
44
|
refetchOnMount: true,
|
|
@@ -14,8 +14,13 @@ async function handleRenewal(
|
|
|
14
14
|
|
|
15
15
|
try {
|
|
16
16
|
await onRenewalDetected(userId, productId, expirationDate, customerInfo);
|
|
17
|
-
} catch (
|
|
17
|
+
} catch (error) {
|
|
18
18
|
// Callback errors should not break customer info processing
|
|
19
|
+
console.error('[CustomerInfoHandler] Renewal callback failed:', {
|
|
20
|
+
userId,
|
|
21
|
+
productId,
|
|
22
|
+
error: error instanceof Error ? error.message : String(error)
|
|
23
|
+
});
|
|
19
24
|
}
|
|
20
25
|
}
|
|
21
26
|
|
|
@@ -31,8 +36,15 @@ async function handlePlanChange(
|
|
|
31
36
|
|
|
32
37
|
try {
|
|
33
38
|
await onPlanChanged(userId, newProductId, previousProductId, isUpgrade, customerInfo);
|
|
34
|
-
} catch (
|
|
39
|
+
} catch (error) {
|
|
35
40
|
// Callback errors should not break customer info processing
|
|
41
|
+
console.error('[CustomerInfoHandler] Plan change callback failed:', {
|
|
42
|
+
userId,
|
|
43
|
+
newProductId,
|
|
44
|
+
previousProductId,
|
|
45
|
+
isUpgrade,
|
|
46
|
+
error: error instanceof Error ? error.message : String(error)
|
|
47
|
+
});
|
|
36
48
|
}
|
|
37
49
|
}
|
|
38
50
|
|
|
@@ -43,8 +55,12 @@ async function handlePremiumStatusSync(
|
|
|
43
55
|
): Promise<void> {
|
|
44
56
|
try {
|
|
45
57
|
await syncPremiumStatus(config, userId, customerInfo);
|
|
46
|
-
} catch (
|
|
58
|
+
} catch (error) {
|
|
47
59
|
// Sync errors are logged by PremiumStatusSyncer, don't break processing
|
|
60
|
+
console.error('[CustomerInfoHandler] Premium status sync failed:', {
|
|
61
|
+
userId,
|
|
62
|
+
error: error instanceof Error ? error.message : String(error)
|
|
63
|
+
});
|
|
48
64
|
}
|
|
49
65
|
}
|
|
50
66
|
|
|
@@ -1,52 +1,51 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Initialization Cache
|
|
3
3
|
* Manages promise caching and user state for initialization
|
|
4
|
-
* Thread-safe: Uses
|
|
4
|
+
* Thread-safe: Uses atomic promise-based locking pattern
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
export class InitializationCache {
|
|
8
8
|
private initPromise: Promise<boolean> | null = null;
|
|
9
9
|
private currentUserId: string | null = null;
|
|
10
|
-
//
|
|
11
|
-
private initializationInProgress = false;
|
|
12
|
-
// Track which userId the promise is for (separate from currentUserId which is set after completion)
|
|
10
|
+
// Track which userId the promise is for
|
|
13
11
|
private promiseUserId: string | null = null;
|
|
14
|
-
// Track promise completion state
|
|
12
|
+
// Track promise completion state
|
|
15
13
|
private promiseCompleted = true;
|
|
14
|
+
// Pending initialization queue
|
|
15
|
+
private pendingQueue: Map<string, Promise<boolean>> = new Map();
|
|
16
16
|
|
|
17
17
|
/**
|
|
18
18
|
* Atomically check if reinitialization is needed AND reserve the slot
|
|
19
19
|
* Returns: { shouldInit: boolean, existingPromise: Promise | null }
|
|
20
20
|
*/
|
|
21
21
|
tryAcquireInitialization(userId: string): { shouldInit: boolean; existingPromise: Promise<boolean> | null } {
|
|
22
|
-
//
|
|
23
|
-
|
|
24
|
-
|
|
22
|
+
// Check if there's already a pending promise for this user in the queue
|
|
23
|
+
const queuedPromise = this.pendingQueue.get(userId);
|
|
24
|
+
if (queuedPromise) {
|
|
25
|
+
return { shouldInit: false, existingPromise: queuedPromise };
|
|
25
26
|
}
|
|
26
27
|
|
|
27
|
-
// If already initialized for this user and promise completed successfully
|
|
28
|
-
|
|
29
|
-
|
|
28
|
+
// If already initialized for this user and promise completed successfully
|
|
29
|
+
if (
|
|
30
|
+
this.initPromise &&
|
|
31
|
+
this.currentUserId === userId &&
|
|
32
|
+
this.promiseCompleted &&
|
|
33
|
+
this.promiseUserId === userId
|
|
34
|
+
) {
|
|
30
35
|
return { shouldInit: false, existingPromise: this.initPromise };
|
|
31
36
|
}
|
|
32
37
|
|
|
33
|
-
// Different user
|
|
34
|
-
|
|
35
|
-
if (!this.initializationInProgress) {
|
|
36
|
-
this.initializationInProgress = true;
|
|
37
|
-
this.promiseUserId = userId;
|
|
38
|
-
this.promiseCompleted = false;
|
|
39
|
-
return { shouldInit: true, existingPromise: null };
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
// If we reach here, initialization is in progress for a different user
|
|
43
|
-
// Don't return another user's promise - caller should retry
|
|
44
|
-
return { shouldInit: false, existingPromise: null };
|
|
38
|
+
// Different user or not initialized - need to initialize
|
|
39
|
+
return { shouldInit: true, existingPromise: null };
|
|
45
40
|
}
|
|
46
41
|
|
|
47
42
|
setPromise(promise: Promise<boolean>, userId: string): void {
|
|
43
|
+
// Add to pending queue immediately (atomic operation)
|
|
44
|
+
this.pendingQueue.set(userId, promise);
|
|
45
|
+
|
|
48
46
|
this.initPromise = promise;
|
|
49
47
|
this.promiseUserId = userId;
|
|
48
|
+
this.promiseCompleted = false;
|
|
50
49
|
|
|
51
50
|
const targetUserId = userId;
|
|
52
51
|
|
|
@@ -69,9 +68,8 @@ export class InitializationCache {
|
|
|
69
68
|
return false;
|
|
70
69
|
})
|
|
71
70
|
.finally(() => {
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
}
|
|
71
|
+
// Remove from queue when complete
|
|
72
|
+
this.pendingQueue.delete(targetUserId);
|
|
75
73
|
});
|
|
76
74
|
}
|
|
77
75
|
|
|
@@ -82,8 +80,8 @@ export class InitializationCache {
|
|
|
82
80
|
reset(): void {
|
|
83
81
|
this.initPromise = null;
|
|
84
82
|
this.currentUserId = null;
|
|
85
|
-
this.initializationInProgress = false;
|
|
86
83
|
this.promiseUserId = null;
|
|
87
84
|
this.promiseCompleted = true;
|
|
85
|
+
this.pendingQueue.clear();
|
|
88
86
|
}
|
|
89
87
|
}
|
|
@@ -46,6 +46,10 @@ export async function syncPremiumStatus(
|
|
|
46
46
|
}
|
|
47
47
|
return { success: true };
|
|
48
48
|
} catch (error) {
|
|
49
|
+
console.error('[PremiumStatusSyncer] Premium status callback failed:', {
|
|
50
|
+
userId,
|
|
51
|
+
error: error instanceof Error ? error.message : String(error)
|
|
52
|
+
});
|
|
49
53
|
|
|
50
54
|
return {
|
|
51
55
|
success: false,
|
|
@@ -68,8 +72,13 @@ export async function notifyPurchaseCompleted(
|
|
|
68
72
|
|
|
69
73
|
try {
|
|
70
74
|
await config.onPurchaseCompleted(userId, productId, customerInfo, source, packageType);
|
|
71
|
-
} catch (
|
|
75
|
+
} catch (error) {
|
|
72
76
|
// Silently fail callback notifications to prevent crashing the main flow
|
|
77
|
+
console.error('[PremiumStatusSyncer] Purchase completed callback failed:', {
|
|
78
|
+
userId,
|
|
79
|
+
productId,
|
|
80
|
+
error: error instanceof Error ? error.message : String(error)
|
|
81
|
+
});
|
|
73
82
|
}
|
|
74
83
|
}
|
|
75
84
|
|
|
@@ -25,7 +25,6 @@ export function useFeatureGate(params: UseFeatureGateParams): UseFeatureGateResu
|
|
|
25
25
|
const { creditBalanceRef, hasSubscriptionRef, onShowPaywallRef, requiredCreditsRef, isCreditsLoadedRef } = useSyncedRefs(creditBalance, hasSubscription, onShowPaywall, requiredCredits, isCreditsLoaded);
|
|
26
26
|
|
|
27
27
|
useEffect(() => {
|
|
28
|
-
|
|
29
28
|
const shouldExecute = canExecuteAuthAction(
|
|
30
29
|
isWaitingForAuthCreditsRef.current,
|
|
31
30
|
isCreditsLoaded,
|
|
@@ -46,9 +45,11 @@ export function useFeatureGate(params: UseFeatureGateParams): UseFeatureGateResu
|
|
|
46
45
|
if (isWaitingForAuthCreditsRef.current && isCreditsLoaded && pendingActionRef.current) {
|
|
47
46
|
isWaitingForAuthCreditsRef.current = false;
|
|
48
47
|
isWaitingForPurchaseRef.current = true;
|
|
49
|
-
|
|
48
|
+
// Use ref to avoid unstable callback dependency
|
|
49
|
+
onShowPaywallRef.current(requiredCreditsRef.current);
|
|
50
50
|
}
|
|
51
|
-
|
|
51
|
+
// Removed onShowPaywall from dependencies - using ref instead
|
|
52
|
+
}, [isCreditsLoaded, creditBalance, hasSubscription, requiredCredits, onShowPaywallRef, requiredCreditsRef]);
|
|
52
53
|
|
|
53
54
|
useEffect(() => {
|
|
54
55
|
|
|
@@ -19,7 +19,9 @@ export const useSubscriptionStatus = (): SubscriptionStatusResult => {
|
|
|
19
19
|
const queryClient = useQueryClient();
|
|
20
20
|
const isConfigured = SubscriptionManager.isConfigured();
|
|
21
21
|
|
|
22
|
-
|
|
22
|
+
// Check if initialized (BackgroundInitializer handles initialization)
|
|
23
|
+
const isInitialized = userId ? SubscriptionManager.isInitializedForUser(userId) : false;
|
|
24
|
+
const queryEnabled = isAuthenticated(userId) && isConfigured && isInitialized;
|
|
23
25
|
|
|
24
26
|
const { data, status, error, refetch } = useQuery({
|
|
25
27
|
queryKey: subscriptionStatusQueryKeys.user(userId),
|
|
@@ -28,10 +30,8 @@ export const useSubscriptionStatus = (): SubscriptionStatusResult => {
|
|
|
28
30
|
return null;
|
|
29
31
|
}
|
|
30
32
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
}
|
|
34
|
-
|
|
33
|
+
// No side effects - just check premium status
|
|
34
|
+
// Initialization is handled by BackgroundInitializer
|
|
35
35
|
try {
|
|
36
36
|
const result = await SubscriptionManager.checkPremiumStatus();
|
|
37
37
|
return result;
|