@umituz/react-native-subscription 2.43.2 → 2.43.3
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/subscription/infrastructure/hooks/usePurchasePackage.ts +2 -14
- package/src/domains/subscription/infrastructure/hooks/useRestorePurchase.ts +2 -14
- package/src/domains/subscription/presentation/featureGateActions.ts +27 -24
- package/src/domains/subscription/presentation/hooks/useFeatureGateState.ts +76 -0
- package/src/domains/subscription/presentation/useAuthAwarePurchase.ts +11 -17
- package/src/domains/subscription/presentation/useFeatureGate.ts +32 -48
- package/src/domains/subscription/presentation/utils/authCheckUtils.ts +42 -0
- package/src/index.ts +8 -0
- package/src/shared/infrastructure/react-query/utils/cacheInvalidation.ts +97 -0
- package/src/shared/infrastructure/react-query/utils/index.ts +6 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umituz/react-native-subscription",
|
|
3
|
-
"version": "2.43.
|
|
3
|
+
"version": "2.43.3",
|
|
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",
|
|
@@ -6,10 +6,8 @@ import {
|
|
|
6
6
|
selectUserId,
|
|
7
7
|
} from "@umituz/react-native-auth";
|
|
8
8
|
import { SubscriptionManager } from "../../infrastructure/managers/SubscriptionManager";
|
|
9
|
-
import { SUBSCRIPTION_QUERY_KEYS } from "./subscriptionQueryKeys";
|
|
10
|
-
import { subscriptionStatusQueryKeys } from "../../presentation/useSubscriptionStatus";
|
|
11
|
-
import { creditsQueryKeys } from "../../../credits/presentation/creditsQueryKeys";
|
|
12
9
|
import { getErrorMessage } from "../../../revenuecat/core/errors/RevenueCatErrorHandler";
|
|
10
|
+
import { invalidateSubscriptionCaches } from "../../../../shared/infrastructure/react-query/utils";
|
|
13
11
|
|
|
14
12
|
interface PurchaseMutationResult {
|
|
15
13
|
success: boolean;
|
|
@@ -39,17 +37,7 @@ export const usePurchasePackage = () => {
|
|
|
39
37
|
onSuccess: (result) => {
|
|
40
38
|
if (result.success) {
|
|
41
39
|
showSuccess("Purchase Successful", "Your subscription is now active!");
|
|
42
|
-
queryClient
|
|
43
|
-
queryKey: SUBSCRIPTION_QUERY_KEYS.packages,
|
|
44
|
-
});
|
|
45
|
-
if (userId) {
|
|
46
|
-
queryClient.invalidateQueries({
|
|
47
|
-
queryKey: subscriptionStatusQueryKeys.user(userId),
|
|
48
|
-
});
|
|
49
|
-
queryClient.invalidateQueries({
|
|
50
|
-
queryKey: creditsQueryKeys.user(userId),
|
|
51
|
-
});
|
|
52
|
-
}
|
|
40
|
+
invalidateSubscriptionCaches(queryClient, userId);
|
|
53
41
|
} else {
|
|
54
42
|
showError("Purchase Failed", "Unable to complete purchase. Please try again.");
|
|
55
43
|
}
|
|
@@ -5,10 +5,8 @@ import {
|
|
|
5
5
|
selectUserId,
|
|
6
6
|
} from "@umituz/react-native-auth";
|
|
7
7
|
import { SubscriptionManager } from "../../infrastructure/managers/SubscriptionManager";
|
|
8
|
-
import { SUBSCRIPTION_QUERY_KEYS } from "./subscriptionQueryKeys";
|
|
9
|
-
import { subscriptionStatusQueryKeys } from "../../presentation/useSubscriptionStatus";
|
|
10
|
-
import { creditsQueryKeys } from "../../../credits/presentation/creditsQueryKeys";
|
|
11
8
|
import { getErrorMessage } from "../../../revenuecat/core/errors/RevenueCatErrorHandler";
|
|
9
|
+
import { invalidateSubscriptionCaches } from "../../../../shared/infrastructure/react-query/utils";
|
|
12
10
|
|
|
13
11
|
interface RestoreResult {
|
|
14
12
|
success: boolean;
|
|
@@ -31,17 +29,7 @@ export const useRestorePurchase = () => {
|
|
|
31
29
|
},
|
|
32
30
|
onSuccess: (result) => {
|
|
33
31
|
if (result.success) {
|
|
34
|
-
queryClient
|
|
35
|
-
queryKey: SUBSCRIPTION_QUERY_KEYS.packages,
|
|
36
|
-
});
|
|
37
|
-
if (userId) {
|
|
38
|
-
queryClient.invalidateQueries({
|
|
39
|
-
queryKey: subscriptionStatusQueryKeys.user(userId),
|
|
40
|
-
});
|
|
41
|
-
queryClient.invalidateQueries({
|
|
42
|
-
queryKey: creditsQueryKeys.user(userId),
|
|
43
|
-
});
|
|
44
|
-
}
|
|
32
|
+
invalidateSubscriptionCaches(queryClient, userId);
|
|
45
33
|
|
|
46
34
|
if (result.productId) {
|
|
47
35
|
showSuccess("Restore Successful", "Your subscription has been restored!");
|
|
@@ -1,50 +1,53 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { FeatureGateState } from "./hooks/useFeatureGateState";
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
/**
|
|
4
|
+
* Executes a feature action with proper auth, subscription, and credit checks.
|
|
5
|
+
* Queues the action if waiting for auth completion or purchase flow.
|
|
6
|
+
*
|
|
7
|
+
* @param action - The action to execute when conditions are met
|
|
8
|
+
* @param isAuthenticated - Whether user is authenticated
|
|
9
|
+
* @param onShowAuthModal - Callback to show auth modal
|
|
10
|
+
* @param state - Feature gate state containing all refs and flags
|
|
11
|
+
* @returns true if action was executed immediately, false if queued/pending
|
|
12
|
+
*/
|
|
13
|
+
export function executeFeatureAction(
|
|
4
14
|
action: () => void | Promise<void>,
|
|
5
15
|
isAuthenticated: boolean,
|
|
6
16
|
onShowAuthModal: (callback: () => void | Promise<void>) => void,
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
requiredCreditsRef: MutableRefObject<number>,
|
|
10
|
-
onShowPaywallRef: MutableRefObject<(requiredCredits?: number) => void>,
|
|
11
|
-
pendingActionRef: MutableRefObject<(() => void | Promise<void>) | null>,
|
|
12
|
-
isWaitingForAuthCreditsRef: MutableRefObject<boolean>,
|
|
13
|
-
isWaitingForPurchaseRef: MutableRefObject<boolean>,
|
|
14
|
-
isCreditsLoadedRef: MutableRefObject<boolean>
|
|
15
|
-
): boolean => {
|
|
17
|
+
state: FeatureGateState
|
|
18
|
+
): boolean {
|
|
16
19
|
|
|
17
20
|
if (!isAuthenticated) {
|
|
18
21
|
const postAuthAction = () => {
|
|
19
|
-
if (hasSubscriptionRef.current || creditBalanceRef.current >= requiredCreditsRef.current) {
|
|
22
|
+
if (state.hasSubscriptionRef.current || state.creditBalanceRef.current >= state.requiredCreditsRef.current) {
|
|
20
23
|
action();
|
|
21
24
|
return;
|
|
22
25
|
}
|
|
23
26
|
|
|
24
|
-
if (isCreditsLoadedRef.current) {
|
|
25
|
-
pendingActionRef.current = action;
|
|
26
|
-
isWaitingForPurchaseRef.current = true;
|
|
27
|
-
onShowPaywallRef.current(requiredCreditsRef.current);
|
|
27
|
+
if (state.isCreditsLoadedRef.current) {
|
|
28
|
+
state.pendingActionRef.current = action;
|
|
29
|
+
state.isWaitingForPurchaseRef.current = true;
|
|
30
|
+
state.onShowPaywallRef.current(state.requiredCreditsRef.current);
|
|
28
31
|
return;
|
|
29
32
|
}
|
|
30
|
-
pendingActionRef.current = action;
|
|
31
|
-
isWaitingForAuthCreditsRef.current = true;
|
|
33
|
+
state.pendingActionRef.current = action;
|
|
34
|
+
state.isWaitingForAuthCreditsRef.current = true;
|
|
32
35
|
};
|
|
33
36
|
onShowAuthModal(postAuthAction);
|
|
34
37
|
return false;
|
|
35
38
|
}
|
|
36
39
|
|
|
37
|
-
if (hasSubscriptionRef.current) {
|
|
40
|
+
if (state.hasSubscriptionRef.current) {
|
|
38
41
|
action();
|
|
39
42
|
return true;
|
|
40
43
|
}
|
|
41
44
|
|
|
42
|
-
if (creditBalanceRef.current < requiredCreditsRef.current) {
|
|
43
|
-
pendingActionRef.current = action;
|
|
44
|
-
isWaitingForPurchaseRef.current = true;
|
|
45
|
-
onShowPaywallRef.current(requiredCreditsRef.current);
|
|
45
|
+
if (state.creditBalanceRef.current < state.requiredCreditsRef.current) {
|
|
46
|
+
state.pendingActionRef.current = action;
|
|
47
|
+
state.isWaitingForPurchaseRef.current = true;
|
|
48
|
+
state.onShowPaywallRef.current(state.requiredCreditsRef.current);
|
|
46
49
|
return false;
|
|
47
50
|
}
|
|
48
51
|
action();
|
|
49
52
|
return true;
|
|
50
|
-
}
|
|
53
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { useRef } from "react";
|
|
2
|
+
import type { UseFeatureGateParams } from "../useFeatureGate.types";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Internal state management for useFeatureGate hook.
|
|
6
|
+
* Encapsulates all refs and state to reduce parameter passing.
|
|
7
|
+
*/
|
|
8
|
+
export interface FeatureGateState {
|
|
9
|
+
// Action queue
|
|
10
|
+
pendingActionRef: React.MutableRefObject<(() => void | Promise<void>) | null>;
|
|
11
|
+
|
|
12
|
+
// Previous values for change detection
|
|
13
|
+
prevCreditBalanceRef: React.MutableRefObject<number | undefined>;
|
|
14
|
+
prevHasSubscriptionRef: React.MutableRefObject<boolean>;
|
|
15
|
+
|
|
16
|
+
// Waiting flags for async operations
|
|
17
|
+
isWaitingForAuthCreditsRef: React.MutableRefObject<boolean>;
|
|
18
|
+
isWaitingForPurchaseRef: React.MutableRefObject<boolean>;
|
|
19
|
+
|
|
20
|
+
// Live refs (synced with current values)
|
|
21
|
+
creditBalanceRef: React.MutableRefObject<number>;
|
|
22
|
+
hasSubscriptionRef: React.MutableRefObject<boolean>;
|
|
23
|
+
onShowPaywallRef: React.MutableRefObject<(requiredCredits?: number) => void>;
|
|
24
|
+
requiredCreditsRef: React.MutableRefObject<number>;
|
|
25
|
+
isCreditsLoadedRef: React.MutableRefObject<boolean>;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Creates and initializes all refs for useFeatureGate.
|
|
30
|
+
* This encapsulates ref creation and initialization logic.
|
|
31
|
+
*/
|
|
32
|
+
export function useFeatureGateRefs(params: UseFeatureGateParams): FeatureGateState {
|
|
33
|
+
const {
|
|
34
|
+
creditBalance,
|
|
35
|
+
hasSubscription = false,
|
|
36
|
+
onShowPaywall,
|
|
37
|
+
requiredCredits = 1,
|
|
38
|
+
isCreditsLoaded = true,
|
|
39
|
+
} = params;
|
|
40
|
+
|
|
41
|
+
const pendingActionRef = useRef<(() => void | Promise<void>) | null>(null);
|
|
42
|
+
const prevCreditBalanceRef = useRef(creditBalance);
|
|
43
|
+
const prevHasSubscriptionRef = useRef(hasSubscription);
|
|
44
|
+
const isWaitingForAuthCreditsRef = useRef(false);
|
|
45
|
+
const isWaitingForPurchaseRef = useRef(false);
|
|
46
|
+
|
|
47
|
+
return {
|
|
48
|
+
pendingActionRef,
|
|
49
|
+
prevCreditBalanceRef,
|
|
50
|
+
prevHasSubscriptionRef,
|
|
51
|
+
isWaitingForAuthCreditsRef,
|
|
52
|
+
isWaitingForPurchaseRef,
|
|
53
|
+
creditBalanceRef: useRef(creditBalance),
|
|
54
|
+
hasSubscriptionRef: useRef(hasSubscription),
|
|
55
|
+
onShowPaywallRef: useRef(onShowPaywall),
|
|
56
|
+
requiredCreditsRef: useRef(requiredCredits),
|
|
57
|
+
isCreditsLoadedRef: useRef(isCreditsLoaded),
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Updates live refs when their source values change.
|
|
63
|
+
* Call this in a useEffect or when values update.
|
|
64
|
+
*/
|
|
65
|
+
export function updateLiveRefs(
|
|
66
|
+
state: FeatureGateState,
|
|
67
|
+
params: Pick<UseFeatureGateParams, 'creditBalance' | 'hasSubscription' | 'onShowPaywall' | 'requiredCredits' | 'isCreditsLoaded'>
|
|
68
|
+
): void {
|
|
69
|
+
const { creditBalance, hasSubscription, onShowPaywall, requiredCredits, isCreditsLoaded } = params;
|
|
70
|
+
|
|
71
|
+
state.creditBalanceRef.current = creditBalance;
|
|
72
|
+
state.hasSubscriptionRef.current = hasSubscription;
|
|
73
|
+
state.onShowPaywallRef.current = onShowPaywall;
|
|
74
|
+
state.requiredCreditsRef.current = requiredCredits;
|
|
75
|
+
state.isCreditsLoadedRef.current = isCreditsLoaded;
|
|
76
|
+
}
|
|
@@ -3,6 +3,7 @@ import type { PurchasesPackage } from "react-native-purchases";
|
|
|
3
3
|
import { usePremium } from "./usePremium";
|
|
4
4
|
import type { PurchaseSource } from "../core/SubscriptionConstants";
|
|
5
5
|
import { authPurchaseStateManager } from "../infrastructure/utils/authPurchaseState";
|
|
6
|
+
import { requireAuthentication } from "./utils/authCheckUtils";
|
|
6
7
|
|
|
7
8
|
export const configureAuthProvider = (provider: import("../infrastructure/utils/authPurchaseState").PurchaseAuthProvider): void => {
|
|
8
9
|
authPurchaseStateManager.configure(provider);
|
|
@@ -26,6 +27,10 @@ interface UseAuthAwarePurchaseResult {
|
|
|
26
27
|
executeSavedPurchase: () => Promise<boolean>;
|
|
27
28
|
}
|
|
28
29
|
|
|
30
|
+
/**
|
|
31
|
+
* Hook for purchase operations that handle authentication.
|
|
32
|
+
* Automatically saves pending purchases and shows auth modal when needed.
|
|
33
|
+
*/
|
|
29
34
|
export const useAuthAwarePurchase = (
|
|
30
35
|
params?: UseAuthAwarePurchaseParams
|
|
31
36
|
): UseAuthAwarePurchaseResult => {
|
|
@@ -50,11 +55,10 @@ export const useAuthAwarePurchase = (
|
|
|
50
55
|
}
|
|
51
56
|
}, [purchasePackage]);
|
|
52
57
|
|
|
58
|
+
// Auto-execute saved purchase when user authenticates
|
|
53
59
|
useEffect(() => {
|
|
54
60
|
const authProvider = authPurchaseStateManager.getProvider();
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
const hasUser = authProvider.hasFirebaseUser();
|
|
61
|
+
const hasUser = authProvider && authProvider.hasFirebaseUser();
|
|
58
62
|
const hasSavedPurchase = !!authPurchaseStateManager.getSavedPurchase();
|
|
59
63
|
|
|
60
64
|
if (hasUser && hasSavedPurchase && !isExecutingSavedRef.current) {
|
|
@@ -69,18 +73,14 @@ export const useAuthAwarePurchase = (
|
|
|
69
73
|
async (pkg: PurchasesPackage, source?: PurchaseSource): Promise<boolean> => {
|
|
70
74
|
const authProvider = authPurchaseStateManager.getProvider();
|
|
71
75
|
|
|
72
|
-
if (!authProvider) {
|
|
73
|
-
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
if (!authProvider.hasFirebaseUser()) {
|
|
76
|
+
if (!requireAuthentication(authProvider)) {
|
|
77
|
+
// User not authenticated, purchase saved and auth modal shown
|
|
77
78
|
authPurchaseStateManager.savePurchase(pkg, source || params?.source || "settings");
|
|
78
|
-
authProvider.showAuthModal();
|
|
79
79
|
return false;
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
+
// User authenticated, proceed with purchase
|
|
82
83
|
const result = await purchasePackage(pkg);
|
|
83
|
-
|
|
84
84
|
return result;
|
|
85
85
|
},
|
|
86
86
|
[purchasePackage, params?.source]
|
|
@@ -89,17 +89,11 @@ export const useAuthAwarePurchase = (
|
|
|
89
89
|
const handleRestore = useCallback(async (): Promise<boolean> => {
|
|
90
90
|
const authProvider = authPurchaseStateManager.getProvider();
|
|
91
91
|
|
|
92
|
-
if (!authProvider) {
|
|
93
|
-
return false;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
if (!authProvider.hasFirebaseUser()) {
|
|
97
|
-
authProvider.showAuthModal();
|
|
92
|
+
if (!requireAuthentication(authProvider)) {
|
|
98
93
|
return false;
|
|
99
94
|
}
|
|
100
95
|
|
|
101
96
|
const result = await restorePurchase();
|
|
102
|
-
|
|
103
97
|
return result;
|
|
104
98
|
}, [restorePurchase]);
|
|
105
99
|
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { useCallback,
|
|
1
|
+
import { useCallback, useEffect } from "react";
|
|
2
2
|
import type { UseFeatureGateParams, UseFeatureGateResult } from "./useFeatureGate.types";
|
|
3
3
|
import { DEFAULT_REQUIRED_CREDITS, canExecuteAuthAction, canExecutePurchaseAction } from "../application/featureGate/featureGateBusinessRules";
|
|
4
|
-
import { useSyncedRefs } from "./featureGateRefs";
|
|
5
4
|
import { executeFeatureAction } from "./featureGateActions";
|
|
5
|
+
import { useFeatureGateRefs, updateLiveRefs } from "./hooks/useFeatureGateState";
|
|
6
6
|
|
|
7
7
|
export function useFeatureGate(params: UseFeatureGateParams): UseFeatureGateResult {
|
|
8
8
|
const {
|
|
@@ -11,73 +11,64 @@ export function useFeatureGate(params: UseFeatureGateParams): UseFeatureGateResu
|
|
|
11
11
|
hasSubscription = false,
|
|
12
12
|
creditBalance,
|
|
13
13
|
requiredCredits = DEFAULT_REQUIRED_CREDITS,
|
|
14
|
-
onShowPaywall,
|
|
15
14
|
isCreditsLoaded = true,
|
|
16
15
|
} = params;
|
|
17
16
|
|
|
18
|
-
const
|
|
19
|
-
const prevCreditBalanceRef = useRef(creditBalance);
|
|
20
|
-
// Separate ref to track previous subscription state for canExecutePurchaseAction.
|
|
21
|
-
// NOTE: Must NOT use hasSubscriptionRef from useSyncedRefs here because useSyncedRefs
|
|
22
|
-
// effects run BEFORE this effect (React runs effects in definition order), so
|
|
23
|
-
// hasSubscriptionRef.current would already be the NEW value when we check it.
|
|
24
|
-
const prevHasSubscriptionRef = useRef(hasSubscription);
|
|
25
|
-
const isWaitingForPurchaseRef = useRef(false);
|
|
26
|
-
const isWaitingForAuthCreditsRef = useRef(false);
|
|
17
|
+
const state = useFeatureGateRefs(params);
|
|
27
18
|
|
|
28
|
-
|
|
19
|
+
// Update live refs when params change
|
|
20
|
+
useEffect(() => {
|
|
21
|
+
updateLiveRefs(state, params);
|
|
22
|
+
});
|
|
29
23
|
|
|
24
|
+
// Handle post-auth credit loading and action execution
|
|
30
25
|
useEffect(() => {
|
|
31
26
|
const shouldExecute = canExecuteAuthAction(
|
|
32
|
-
isWaitingForAuthCreditsRef.current,
|
|
27
|
+
state.isWaitingForAuthCreditsRef.current,
|
|
33
28
|
isCreditsLoaded,
|
|
34
|
-
!!pendingActionRef.current,
|
|
29
|
+
!!state.pendingActionRef.current,
|
|
35
30
|
hasSubscription,
|
|
36
31
|
creditBalance,
|
|
37
32
|
requiredCredits
|
|
38
33
|
);
|
|
39
34
|
|
|
40
35
|
if (shouldExecute) {
|
|
41
|
-
isWaitingForAuthCreditsRef.current = false;
|
|
42
|
-
const action = pendingActionRef.current!;
|
|
43
|
-
pendingActionRef.current = null;
|
|
36
|
+
state.isWaitingForAuthCreditsRef.current = false;
|
|
37
|
+
const action = state.pendingActionRef.current!;
|
|
38
|
+
state.pendingActionRef.current = null;
|
|
44
39
|
action();
|
|
45
40
|
return;
|
|
46
41
|
}
|
|
47
42
|
|
|
48
|
-
if (isWaitingForAuthCreditsRef.current && isCreditsLoaded && pendingActionRef.current) {
|
|
49
|
-
isWaitingForAuthCreditsRef.current = false;
|
|
50
|
-
isWaitingForPurchaseRef.current = true;
|
|
51
|
-
|
|
52
|
-
onShowPaywallRef.current(requiredCreditsRef.current);
|
|
43
|
+
if (state.isWaitingForAuthCreditsRef.current && isCreditsLoaded && state.pendingActionRef.current) {
|
|
44
|
+
state.isWaitingForAuthCreditsRef.current = false;
|
|
45
|
+
state.isWaitingForPurchaseRef.current = true;
|
|
46
|
+
state.onShowPaywallRef.current(state.requiredCreditsRef.current);
|
|
53
47
|
}
|
|
54
|
-
|
|
55
|
-
}, [isCreditsLoaded, creditBalance, hasSubscription, requiredCredits, onShowPaywallRef, requiredCreditsRef]);
|
|
48
|
+
}, [isCreditsLoaded, creditBalance, hasSubscription, requiredCredits, state]);
|
|
56
49
|
|
|
50
|
+
// Handle post-purchase action execution
|
|
57
51
|
useEffect(() => {
|
|
58
|
-
// Use prevHasSubscriptionRef (updated AFTER check) not hasSubscriptionRef from useSyncedRefs
|
|
59
|
-
// (which is already updated to new value before this effect runs - race condition fix)
|
|
60
52
|
const shouldExecute = canExecutePurchaseAction(
|
|
61
|
-
isWaitingForPurchaseRef.current,
|
|
53
|
+
state.isWaitingForPurchaseRef.current,
|
|
62
54
|
creditBalance,
|
|
63
|
-
prevCreditBalanceRef.current ?? 0,
|
|
55
|
+
state.prevCreditBalanceRef.current ?? 0,
|
|
64
56
|
hasSubscription,
|
|
65
|
-
prevHasSubscriptionRef.current,
|
|
66
|
-
!!pendingActionRef.current
|
|
57
|
+
state.prevHasSubscriptionRef.current,
|
|
58
|
+
!!state.pendingActionRef.current
|
|
67
59
|
);
|
|
68
60
|
|
|
69
61
|
if (shouldExecute) {
|
|
70
|
-
const action = pendingActionRef.current!;
|
|
71
|
-
pendingActionRef.current = null;
|
|
72
|
-
isWaitingForPurchaseRef.current = false;
|
|
62
|
+
const action = state.pendingActionRef.current!;
|
|
63
|
+
state.pendingActionRef.current = null;
|
|
64
|
+
state.isWaitingForPurchaseRef.current = false;
|
|
73
65
|
action();
|
|
74
66
|
}
|
|
75
67
|
|
|
76
|
-
// Update AFTER check
|
|
77
|
-
prevCreditBalanceRef.current = creditBalance;
|
|
78
|
-
prevHasSubscriptionRef.current = hasSubscription;
|
|
79
|
-
|
|
80
|
-
}, [creditBalance, hasSubscription]);
|
|
68
|
+
// Update prev refs AFTER check for next render
|
|
69
|
+
state.prevCreditBalanceRef.current = creditBalance;
|
|
70
|
+
state.prevHasSubscriptionRef.current = hasSubscription;
|
|
71
|
+
}, [creditBalance, hasSubscription, state]);
|
|
81
72
|
|
|
82
73
|
const requireFeature = useCallback(
|
|
83
74
|
(action: () => void | Promise<void>): boolean => {
|
|
@@ -85,17 +76,10 @@ export function useFeatureGate(params: UseFeatureGateParams): UseFeatureGateResu
|
|
|
85
76
|
action,
|
|
86
77
|
isAuthenticated,
|
|
87
78
|
onShowAuthModal,
|
|
88
|
-
|
|
89
|
-
creditBalanceRef,
|
|
90
|
-
requiredCreditsRef,
|
|
91
|
-
onShowPaywallRef,
|
|
92
|
-
pendingActionRef,
|
|
93
|
-
isWaitingForAuthCreditsRef,
|
|
94
|
-
isWaitingForPurchaseRef,
|
|
95
|
-
isCreditsLoadedRef
|
|
79
|
+
state
|
|
96
80
|
);
|
|
97
81
|
},
|
|
98
|
-
[isAuthenticated, onShowAuthModal,
|
|
82
|
+
[isAuthenticated, onShowAuthModal, state]
|
|
99
83
|
);
|
|
100
84
|
|
|
101
85
|
return {
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Authentication check utilities for purchase flows.
|
|
3
|
+
* Extracted to reduce duplication in useAuthAwarePurchase.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export interface PurchaseAuthProvider {
|
|
7
|
+
hasFirebaseUser: () => boolean;
|
|
8
|
+
showAuthModal: () => void;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Checks if auth provider is available and user is authenticated.
|
|
13
|
+
*
|
|
14
|
+
* @param authProvider - Auth provider from authPurchaseStateManager
|
|
15
|
+
* @returns true if user is authenticated, false otherwise
|
|
16
|
+
*/
|
|
17
|
+
export function isUserAuthenticated(authProvider: PurchaseAuthProvider | null): boolean {
|
|
18
|
+
if (!authProvider) {
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
return authProvider.hasFirebaseUser();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Ensures user is authenticated before proceeding with an action.
|
|
26
|
+
* If not authenticated, shows auth modal and returns false.
|
|
27
|
+
*
|
|
28
|
+
* @param authProvider - Auth provider from authPurchaseStateManager
|
|
29
|
+
* @returns true if authenticated, false if auth modal was shown
|
|
30
|
+
*/
|
|
31
|
+
export function requireAuthentication(authProvider: PurchaseAuthProvider | null): boolean {
|
|
32
|
+
if (!authProvider) {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (!authProvider.hasFirebaseUser()) {
|
|
37
|
+
authProvider.showAuthModal();
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return true;
|
|
42
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -51,6 +51,14 @@ export {
|
|
|
51
51
|
} from "./shared/utils/Result";
|
|
52
52
|
export type { Result, Success, Failure } from "./shared/utils/Result";
|
|
53
53
|
|
|
54
|
+
// Cache Invalidation Utilities
|
|
55
|
+
export {
|
|
56
|
+
invalidateSubscriptionCaches,
|
|
57
|
+
invalidateSubscriptionStatus,
|
|
58
|
+
invalidateCredits,
|
|
59
|
+
invalidateAllUserData,
|
|
60
|
+
} from "./shared/infrastructure/react-query/utils";
|
|
61
|
+
|
|
54
62
|
// Infrastructure Layer (Services & Repositories)
|
|
55
63
|
export { initializeSubscription } from "./domains/subscription/application/initializer/SubscriptionInitializer";
|
|
56
64
|
export type { SubscriptionInitConfig, CreditPackageConfig } from "./domains/subscription/application/SubscriptionInitializerTypes";
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import type { QueryClient } from "@tanstack/react-query";
|
|
2
|
+
import { SUBSCRIPTION_QUERY_KEYS } from "../../../../domains/subscription/infrastructure/hooks/subscriptionQueryKeys";
|
|
3
|
+
import { subscriptionStatusQueryKeys } from "../../../../domains/subscription/presentation/useSubscriptionStatus";
|
|
4
|
+
import { creditsQueryKeys } from "../../../../domains/credits/presentation/creditsQueryKeys";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Centralized cache invalidation utilities for subscription-related queries.
|
|
8
|
+
* This ensures consistent cache invalidation across all mutations and removes code duplication.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Invalidate all subscription-related caches for a specific user.
|
|
13
|
+
* This includes:
|
|
14
|
+
* - Subscription packages
|
|
15
|
+
* - Subscription status
|
|
16
|
+
* - Credits
|
|
17
|
+
*
|
|
18
|
+
* @param queryClient - TanStack Query client instance
|
|
19
|
+
* @param userId - User ID to invalidate caches for
|
|
20
|
+
*/
|
|
21
|
+
export function invalidateSubscriptionCaches(
|
|
22
|
+
queryClient: QueryClient,
|
|
23
|
+
userId: string | null | undefined
|
|
24
|
+
): void {
|
|
25
|
+
if (!userId) {
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Invalidate packages (global, not user-specific)
|
|
30
|
+
queryClient.invalidateQueries({
|
|
31
|
+
queryKey: SUBSCRIPTION_QUERY_KEYS.packages,
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
// Invalidate subscription status (user-specific)
|
|
35
|
+
queryClient.invalidateQueries({
|
|
36
|
+
queryKey: subscriptionStatusQueryKeys.user(userId),
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
// Invalidate credits (user-specific)
|
|
40
|
+
queryClient.invalidateQueries({
|
|
41
|
+
queryKey: creditsQueryKeys.user(userId),
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Invalidate only subscription status cache.
|
|
47
|
+
* Use this when only subscription status changes, not credits.
|
|
48
|
+
*
|
|
49
|
+
* @param queryClient - TanStack Query client instance
|
|
50
|
+
* @param userId - User ID to invalidate cache for
|
|
51
|
+
*/
|
|
52
|
+
export function invalidateSubscriptionStatus(
|
|
53
|
+
queryClient: QueryClient,
|
|
54
|
+
userId: string | null | undefined
|
|
55
|
+
): void {
|
|
56
|
+
if (!userId) {
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
queryClient.invalidateQueries({
|
|
61
|
+
queryKey: subscriptionStatusQueryKeys.user(userId),
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Invalidate only credits cache.
|
|
67
|
+
* Use this when only credits change, not subscription status.
|
|
68
|
+
*
|
|
69
|
+
* @param queryClient - TanStack Query client instance
|
|
70
|
+
* @param userId - User ID to invalidate cache for
|
|
71
|
+
*/
|
|
72
|
+
export function invalidateCredits(
|
|
73
|
+
queryClient: QueryClient,
|
|
74
|
+
userId: string | null | undefined
|
|
75
|
+
): void {
|
|
76
|
+
if (!userId) {
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
queryClient.invalidateQueries({
|
|
81
|
+
queryKey: creditsQueryKeys.user(userId),
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Invalidate all caches for a user (subscription + credits).
|
|
87
|
+
* Alias for invalidateSubscriptionCaches for better semantic clarity.
|
|
88
|
+
*
|
|
89
|
+
* @param queryClient - TanStack Query client instance
|
|
90
|
+
* @param userId - User ID to invalidate caches for
|
|
91
|
+
*/
|
|
92
|
+
export function invalidateAllUserData(
|
|
93
|
+
queryClient: QueryClient,
|
|
94
|
+
userId: string | null | undefined
|
|
95
|
+
): void {
|
|
96
|
+
invalidateSubscriptionCaches(queryClient, userId);
|
|
97
|
+
}
|