@umituz/react-native-subscription 2.27.70 → 2.27.72
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/credits/presentation/useDeductCredit.ts +17 -7
- package/src/domains/paywall/components/PaywallModal.tsx +13 -0
- package/src/domains/paywall/index.ts +0 -3
- package/src/domains/subscription/infrastructure/hooks/usePurchasePackage.ts +36 -2
- package/src/domains/subscription/infrastructure/managers/SubscriptionManager.ts +18 -6
- package/src/domains/subscription/infrastructure/services/CustomerInfoListenerManager.ts +8 -0
- package/src/domains/subscription/infrastructure/services/RevenueCatInitializer.ts +18 -11
- package/src/domains/subscription/infrastructure/services/RevenueCatService.ts +5 -2
- package/src/domains/subscription/presentation/useAuthAwarePurchase.ts +12 -0
- package/src/domains/subscription/presentation/useAuthSubscriptionSync.ts +5 -2
- package/src/domains/subscription/presentation/useSavedPurchaseAutoExecution.ts +2 -13
- package/src/domains/subscription/presentation/useSubscriptionStatus.ts +12 -1
- package/src/presentation/stores/purchaseLoadingStore.ts +20 -1
- package/src/domains/index.ts +0 -6
- package/src/domains/paywall/components/index.ts +0 -15
- package/src/domains/wallet/presentation/components/index.ts +0 -23
- package/src/presentation/hooks/__tests__/useUserTier.authenticated.test.ts +0 -79
- package/src/presentation/hooks/__tests__/useUserTier.guest.test.ts +0 -70
- package/src/presentation/hooks/__tests__/useUserTier.states.test.ts +0 -167
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umituz/react-native-subscription",
|
|
3
|
-
"version": "2.27.
|
|
3
|
+
"version": "2.27.72",
|
|
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",
|
|
@@ -42,24 +42,34 @@ export const useDeductCredit = ({
|
|
|
42
42
|
await queryClient.cancelQueries({ queryKey: creditsQueryKeys.user(userId) });
|
|
43
43
|
const previousCredits = queryClient.getQueryData<UserCredits>(creditsQueryKeys.user(userId));
|
|
44
44
|
|
|
45
|
-
//
|
|
46
|
-
if (!previousCredits
|
|
47
|
-
return { previousCredits, skippedOptimistic: true };
|
|
45
|
+
// Improved optimistic update logic
|
|
46
|
+
if (!previousCredits) {
|
|
47
|
+
return { previousCredits: null, skippedOptimistic: true };
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
+
// If credits are insufficient, show 0 but don't skip optimistic update
|
|
51
|
+
// This provides better UX by showing the user what will happen
|
|
52
|
+
const newCredits = Math.max(0, previousCredits.credits - cost);
|
|
53
|
+
|
|
50
54
|
queryClient.setQueryData<UserCredits | null>(creditsQueryKeys.user(userId), (old) => {
|
|
51
55
|
if (!old) return old;
|
|
52
56
|
return {
|
|
53
57
|
...old,
|
|
54
|
-
credits:
|
|
58
|
+
credits: newCredits,
|
|
55
59
|
lastUpdatedAt: timezoneService.getNow()
|
|
56
60
|
};
|
|
57
61
|
});
|
|
58
|
-
|
|
62
|
+
|
|
63
|
+
return {
|
|
64
|
+
previousCredits,
|
|
65
|
+
skippedOptimistic: false,
|
|
66
|
+
wasInsufficient: previousCredits.credits < cost
|
|
67
|
+
};
|
|
59
68
|
},
|
|
60
69
|
onError: (_err, _cost, context) => {
|
|
61
|
-
//
|
|
62
|
-
if (
|
|
70
|
+
// Restore previous credits on error
|
|
71
|
+
// Skip restoration if credits were insufficient (optimistic update showed 0, which is correct)
|
|
72
|
+
if (userId && context?.previousCredits && !context.skippedOptimistic && !context.wasInsufficient) {
|
|
63
73
|
queryClient.setQueryData(creditsQueryKeys.user(userId), context.previousCredits);
|
|
64
74
|
}
|
|
65
75
|
},
|
|
@@ -59,6 +59,14 @@ export const PaywallModal: React.FC<PaywallModalProps> = React.memo((props) => {
|
|
|
59
59
|
setSelectedPlanId(null);
|
|
60
60
|
}, [packages]);
|
|
61
61
|
|
|
62
|
+
// Cleanup state when modal closes to prevent stale state
|
|
63
|
+
useEffect(() => {
|
|
64
|
+
if (!visible) {
|
|
65
|
+
setSelectedPlanId(null);
|
|
66
|
+
setIsLocalProcessing(false);
|
|
67
|
+
}
|
|
68
|
+
}, [visible]);
|
|
69
|
+
|
|
62
70
|
// Combined processing state
|
|
63
71
|
const isProcessing = isLocalProcessing || isGlobalPurchasing;
|
|
64
72
|
|
|
@@ -83,6 +91,11 @@ export const PaywallModal: React.FC<PaywallModalProps> = React.memo((props) => {
|
|
|
83
91
|
console.log("[PaywallModal] onPurchase completed");
|
|
84
92
|
}
|
|
85
93
|
}
|
|
94
|
+
} catch (error) {
|
|
95
|
+
// Error is handled by usePurchasePackage, just log for debugging
|
|
96
|
+
if (__DEV__) {
|
|
97
|
+
console.error("[PaywallModal] Purchase failed:", error);
|
|
98
|
+
}
|
|
86
99
|
} finally {
|
|
87
100
|
setIsLocalProcessing(false);
|
|
88
101
|
endPurchase();
|
|
@@ -90,8 +90,42 @@ export const usePurchasePackage = () => {
|
|
|
90
90
|
userId: userId ?? "ANONYMOUS",
|
|
91
91
|
});
|
|
92
92
|
}
|
|
93
|
-
|
|
94
|
-
|
|
93
|
+
|
|
94
|
+
let title = "Purchase Error";
|
|
95
|
+
let message = "Unable to complete purchase. Please try again.";
|
|
96
|
+
|
|
97
|
+
if (error instanceof Error) {
|
|
98
|
+
// Handle RevenueCat-specific error codes
|
|
99
|
+
const errorCode = (error as any).code;
|
|
100
|
+
const errorMessage = error.message;
|
|
101
|
+
|
|
102
|
+
switch (errorCode) {
|
|
103
|
+
case "PURCHASE_CANCELLED":
|
|
104
|
+
title = "Purchase Cancelled";
|
|
105
|
+
message = "The purchase was cancelled.";
|
|
106
|
+
break;
|
|
107
|
+
case "PURCHASE_INVALID":
|
|
108
|
+
title = "Invalid Purchase";
|
|
109
|
+
message = "The purchase is invalid. Please contact support.";
|
|
110
|
+
break;
|
|
111
|
+
case "PRODUCT_ALREADY_OWNED":
|
|
112
|
+
title = "Already Owned";
|
|
113
|
+
message = "You already own this subscription. Please restore your purchase.";
|
|
114
|
+
break;
|
|
115
|
+
case "NETWORK_ERROR":
|
|
116
|
+
title = "Network Error";
|
|
117
|
+
message = "Please check your internet connection and try again.";
|
|
118
|
+
break;
|
|
119
|
+
case "INVALID_CREDENTIALS":
|
|
120
|
+
title = "Configuration Error";
|
|
121
|
+
message = "App is not configured correctly. Please contact support.";
|
|
122
|
+
break;
|
|
123
|
+
default:
|
|
124
|
+
message = errorMessage || message;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
showError(title, message);
|
|
95
129
|
},
|
|
96
130
|
});
|
|
97
131
|
};
|
|
@@ -41,12 +41,24 @@ class SubscriptionManagerImpl {
|
|
|
41
41
|
if (!shouldInit && existingPromise) return existingPromise;
|
|
42
42
|
|
|
43
43
|
const promise = (async () => {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
44
|
+
try {
|
|
45
|
+
await initializeRevenueCatService(this.managerConfig!.config);
|
|
46
|
+
this.serviceInstance = getRevenueCatService();
|
|
47
|
+
if (!this.serviceInstance) {
|
|
48
|
+
if (__DEV__) {
|
|
49
|
+
console.error('[SubscriptionManager] Service instance not available after initialization');
|
|
50
|
+
}
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
this.packageHandler!.setService(this.serviceInstance);
|
|
54
|
+
const result = await this.serviceInstance.initialize(effectiveUserId);
|
|
55
|
+
return result.success;
|
|
56
|
+
} catch (error) {
|
|
57
|
+
if (__DEV__) {
|
|
58
|
+
console.error('[SubscriptionManager] Initialization failed:', error);
|
|
59
|
+
}
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
50
62
|
})();
|
|
51
63
|
|
|
52
64
|
this.state.initCache.setPromise(promise, effectiveUserId);
|
|
@@ -133,7 +133,15 @@ export class CustomerInfoListenerManager {
|
|
|
133
133
|
}
|
|
134
134
|
|
|
135
135
|
destroy(): void {
|
|
136
|
+
if (__DEV__) {
|
|
137
|
+
console.log('[CustomerInfoListenerManager] Destroying listener manager');
|
|
138
|
+
}
|
|
136
139
|
this.removeListener();
|
|
137
140
|
this.clearUserId();
|
|
141
|
+
// Reset renewal state to ensure clean state
|
|
142
|
+
this.renewalState = {
|
|
143
|
+
previousExpirationDate: null,
|
|
144
|
+
previousProductId: null,
|
|
145
|
+
};
|
|
138
146
|
}
|
|
139
147
|
}
|
|
@@ -11,12 +11,19 @@ export interface InitializerDeps {
|
|
|
11
11
|
setCurrentUserId: (userId: string) => void;
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
14
|
+
// State management to prevent race conditions
|
|
15
|
+
const configurationState = {
|
|
16
|
+
isPurchasesConfigured: false,
|
|
17
|
+
isLogHandlerConfigured: false,
|
|
18
|
+
configurationInProgress: false,
|
|
19
|
+
configurationPromise: null as Promise<ReturnType<typeof initializeSDK>> | null,
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
// Simple lock mechanism to prevent concurrent configurations
|
|
23
|
+
let configurationLocks = new Set<string>();
|
|
17
24
|
|
|
18
25
|
function configureLogHandler(): void {
|
|
19
|
-
if (isLogHandlerConfigured) return;
|
|
26
|
+
if (configurationState.isLogHandlerConfigured) return;
|
|
20
27
|
if (typeof Purchases.setLogHandler !== 'function') return;
|
|
21
28
|
try {
|
|
22
29
|
Purchases.setLogHandler((logLevel, message) => {
|
|
@@ -24,7 +31,7 @@ function configureLogHandler(): void {
|
|
|
24
31
|
if (ignoreMessages.some(m => message.includes(m))) return;
|
|
25
32
|
if (logLevel === LOG_LEVEL.ERROR && __DEV__) console.error('[RevenueCat]', message);
|
|
26
33
|
});
|
|
27
|
-
isLogHandlerConfigured = true;
|
|
34
|
+
configurationState.isLogHandlerConfigured = true;
|
|
28
35
|
} catch {
|
|
29
36
|
// Native module not available (Expo Go)
|
|
30
37
|
}
|
|
@@ -52,7 +59,7 @@ export async function initializeSDK(
|
|
|
52
59
|
}
|
|
53
60
|
}
|
|
54
61
|
|
|
55
|
-
if (isPurchasesConfigured) {
|
|
62
|
+
if (configurationState.isPurchasesConfigured) {
|
|
56
63
|
try {
|
|
57
64
|
const currentAppUserId = await Purchases.getAppUserID();
|
|
58
65
|
let customerInfo;
|
|
@@ -71,9 +78,9 @@ export async function initializeSDK(
|
|
|
71
78
|
}
|
|
72
79
|
}
|
|
73
80
|
|
|
74
|
-
if (configurationInProgress) {
|
|
81
|
+
if (configurationState.configurationInProgress) {
|
|
75
82
|
await new Promise(resolve => setTimeout(resolve, 100));
|
|
76
|
-
if (isPurchasesConfigured) return initializeSDK(deps, userId, apiKey);
|
|
83
|
+
if (configurationState.isPurchasesConfigured) return initializeSDK(deps, userId, apiKey);
|
|
77
84
|
return { success: false, offering: null, isPremium: false };
|
|
78
85
|
}
|
|
79
86
|
|
|
@@ -83,13 +90,13 @@ export async function initializeSDK(
|
|
|
83
90
|
return { success: false, offering: null, isPremium: false };
|
|
84
91
|
}
|
|
85
92
|
|
|
86
|
-
configurationInProgress = true;
|
|
93
|
+
configurationState.configurationInProgress = true;
|
|
87
94
|
try {
|
|
88
95
|
configureLogHandler();
|
|
89
96
|
if (__DEV__) console.log('[RevenueCat] Configuring:', key.substring(0, 10) + '...');
|
|
90
97
|
|
|
91
98
|
await Purchases.configure({ apiKey: key, appUserID: userId });
|
|
92
|
-
isPurchasesConfigured = true;
|
|
99
|
+
configurationState.isPurchasesConfigured = true;
|
|
93
100
|
deps.setInitialized(true);
|
|
94
101
|
deps.setCurrentUserId(userId);
|
|
95
102
|
|
|
@@ -108,6 +115,6 @@ export async function initializeSDK(
|
|
|
108
115
|
if (__DEV__) console.error('[RevenueCat] Init failed:', error);
|
|
109
116
|
return { success: false, offering: null, isPremium: false };
|
|
110
117
|
} finally {
|
|
111
|
-
configurationInProgress = false;
|
|
118
|
+
configurationState.configurationInProgress = false;
|
|
112
119
|
}
|
|
113
120
|
}
|
|
@@ -108,8 +108,11 @@ export class RevenueCatService implements IRevenueCatService {
|
|
|
108
108
|
try {
|
|
109
109
|
await Purchases.logOut();
|
|
110
110
|
this.stateManager.setInitialized(false);
|
|
111
|
-
} catch {
|
|
112
|
-
//
|
|
111
|
+
} catch (error) {
|
|
112
|
+
// Log error for debugging but don't throw - reset is cleanup operation
|
|
113
|
+
if (__DEV__) {
|
|
114
|
+
console.error('[RevenueCatService] Reset failed:', error);
|
|
115
|
+
}
|
|
113
116
|
}
|
|
114
117
|
}
|
|
115
118
|
}
|
|
@@ -28,6 +28,18 @@ export const configureAuthProvider = (provider: PurchaseAuthProvider): void => {
|
|
|
28
28
|
globalAuthProvider = provider;
|
|
29
29
|
};
|
|
30
30
|
|
|
31
|
+
/**
|
|
32
|
+
* Cleanup method to reset global auth provider state
|
|
33
|
+
* Call this when app is shutting down or auth system is being reset
|
|
34
|
+
*/
|
|
35
|
+
export const cleanupAuthProvider = (): void => {
|
|
36
|
+
if (__DEV__) {
|
|
37
|
+
console.log("[useAuthAwarePurchase] Cleaning up auth provider");
|
|
38
|
+
}
|
|
39
|
+
globalAuthProvider = null;
|
|
40
|
+
clearSavedPurchase();
|
|
41
|
+
};
|
|
42
|
+
|
|
31
43
|
const savePurchase = (pkg: PurchasesPackage, source: PurchaseSource): void => {
|
|
32
44
|
savedPurchaseState = { pkg, source, timestamp: Date.now() };
|
|
33
45
|
};
|
|
@@ -48,8 +48,11 @@ export function useAuthSubscriptionSync(
|
|
|
48
48
|
await initialize(userId);
|
|
49
49
|
isInitializedRef.current = true;
|
|
50
50
|
}
|
|
51
|
-
} catch {
|
|
52
|
-
//
|
|
51
|
+
} catch (error) {
|
|
52
|
+
// Log error for debugging but don't crash the auth flow
|
|
53
|
+
if (__DEV__) {
|
|
54
|
+
console.error('[useAuthSubscriptionSync] Initialization failed:', error);
|
|
55
|
+
}
|
|
53
56
|
}
|
|
54
57
|
|
|
55
58
|
previousUserIdRef.current = userId;
|
|
@@ -44,25 +44,14 @@ export const useSavedPurchaseAutoExecution = (
|
|
|
44
44
|
const startPurchaseRef = useRef(startPurchase);
|
|
45
45
|
const endPurchaseRef = useRef(endPurchase);
|
|
46
46
|
|
|
47
|
+
// Consolidate all ref updates into a single effect
|
|
47
48
|
useEffect(() => {
|
|
48
49
|
purchasePackageRef.current = purchasePackage;
|
|
49
|
-
}, [purchasePackage]);
|
|
50
|
-
|
|
51
|
-
useEffect(() => {
|
|
52
50
|
onSuccessRef.current = onSuccess;
|
|
53
|
-
}, [onSuccess]);
|
|
54
|
-
|
|
55
|
-
useEffect(() => {
|
|
56
51
|
onErrorRef.current = onError;
|
|
57
|
-
}, [onError]);
|
|
58
|
-
|
|
59
|
-
useEffect(() => {
|
|
60
52
|
startPurchaseRef.current = startPurchase;
|
|
61
|
-
}, [startPurchase]);
|
|
62
|
-
|
|
63
|
-
useEffect(() => {
|
|
64
53
|
endPurchaseRef.current = endPurchase;
|
|
65
|
-
}, [endPurchase]);
|
|
54
|
+
}, [purchasePackage, onSuccess, onError, startPurchase, endPurchase]);
|
|
66
55
|
|
|
67
56
|
useEffect(() => {
|
|
68
57
|
const isAuthenticated = !!userId && !isAnonymous;
|
|
@@ -34,7 +34,18 @@ export const useSubscriptionStatus = (): SubscriptionStatusResult => {
|
|
|
34
34
|
if (!userId) {
|
|
35
35
|
return { isPremium: false, expirationDate: null };
|
|
36
36
|
}
|
|
37
|
-
|
|
37
|
+
|
|
38
|
+
try {
|
|
39
|
+
const result = await SubscriptionManager.checkPremiumStatus();
|
|
40
|
+
// Ensure we always return a valid object even if result is null/undefined
|
|
41
|
+
return result ?? { isPremium: false, expirationDate: null };
|
|
42
|
+
} catch (error) {
|
|
43
|
+
if (__DEV__) {
|
|
44
|
+
console.error('[useSubscriptionStatus] Failed to check premium status:', error);
|
|
45
|
+
}
|
|
46
|
+
// Return default state on error to prevent crashes
|
|
47
|
+
return { isPremium: false, expirationDate: null };
|
|
48
|
+
}
|
|
38
49
|
},
|
|
39
50
|
enabled: !!userId && SubscriptionManager.isInitializedForUser(userId),
|
|
40
51
|
staleTime: 30 * 1000, // 30 seconds
|
|
@@ -32,10 +32,22 @@ const initialState: PurchaseLoadingState = {
|
|
|
32
32
|
purchaseSource: null,
|
|
33
33
|
};
|
|
34
34
|
|
|
35
|
-
export const usePurchaseLoadingStore = create<PurchaseLoadingStore>((set) => ({
|
|
35
|
+
export const usePurchaseLoadingStore = create<PurchaseLoadingStore>((set, get) => ({
|
|
36
36
|
...initialState,
|
|
37
37
|
|
|
38
38
|
startPurchase: (productId, source) => {
|
|
39
|
+
const currentState = get();
|
|
40
|
+
if (currentState.isPurchasing) {
|
|
41
|
+
if (__DEV__) {
|
|
42
|
+
console.warn("[PurchaseLoadingStore] startPurchase called while purchase already in progress:", {
|
|
43
|
+
currentProductId: currentState.purchasingProductId,
|
|
44
|
+
newProductId: productId,
|
|
45
|
+
currentSource: currentState.purchaseSource,
|
|
46
|
+
newSource: source,
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
// Still update to the new purchase to recover from potential stuck state
|
|
50
|
+
}
|
|
39
51
|
if (__DEV__) {
|
|
40
52
|
console.log("[PurchaseLoadingStore] startPurchase:", { productId, source });
|
|
41
53
|
}
|
|
@@ -47,6 +59,13 @@ export const usePurchaseLoadingStore = create<PurchaseLoadingStore>((set) => ({
|
|
|
47
59
|
},
|
|
48
60
|
|
|
49
61
|
endPurchase: () => {
|
|
62
|
+
const currentState = get();
|
|
63
|
+
if (!currentState.isPurchasing) {
|
|
64
|
+
if (__DEV__) {
|
|
65
|
+
console.warn("[PurchaseLoadingStore] endPurchase called while no purchase in progress");
|
|
66
|
+
}
|
|
67
|
+
// Reset to initial state to recover from potential stuck state
|
|
68
|
+
}
|
|
50
69
|
if (__DEV__) {
|
|
51
70
|
console.log("[PurchaseLoadingStore] endPurchase");
|
|
52
71
|
}
|
package/src/domains/index.ts
DELETED
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Paywall Components Index
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
export { PaywallContainer } from "./PaywallContainer";
|
|
6
|
-
export type { PaywallContainerProps, TrialConfig } from "./PaywallContainer.types";
|
|
7
|
-
|
|
8
|
-
export { PaywallModal } from "./PaywallModal";
|
|
9
|
-
export type { PaywallModalProps } from "./PaywallModal";
|
|
10
|
-
|
|
11
|
-
export { PaywallHeader } from "./PaywallHeader";
|
|
12
|
-
export { PaywallFooter } from "./PaywallFooter";
|
|
13
|
-
export { FeatureList } from "./FeatureList";
|
|
14
|
-
export { FeatureItem } from "./FeatureItem";
|
|
15
|
-
export { PlanCard } from "./PlanCard";
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Wallet Components Index
|
|
3
|
-
*
|
|
4
|
-
* Export all wallet-related components.
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
export {
|
|
8
|
-
BalanceCard,
|
|
9
|
-
type BalanceCardProps,
|
|
10
|
-
type BalanceCardTranslations,
|
|
11
|
-
} from "./BalanceCard";
|
|
12
|
-
|
|
13
|
-
export {
|
|
14
|
-
TransactionItem,
|
|
15
|
-
type TransactionItemProps,
|
|
16
|
-
type TransactionItemTranslations,
|
|
17
|
-
} from "./TransactionItem";
|
|
18
|
-
|
|
19
|
-
export {
|
|
20
|
-
TransactionList,
|
|
21
|
-
type TransactionListProps,
|
|
22
|
-
type TransactionListTranslations,
|
|
23
|
-
} from "./TransactionList";
|
|
@@ -1,79 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* useUserTier Hook Tests - Authenticated Users
|
|
3
|
-
*
|
|
4
|
-
* Tests for authenticated user scenarios
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import React from 'react';
|
|
8
|
-
import { create } from 'react-test-renderer';
|
|
9
|
-
import { useUserTier, type UseUserTierParams } from '../useUserTier';
|
|
10
|
-
|
|
11
|
-
// Test component that uses hook
|
|
12
|
-
function TestComponent({ params }: { params: UseUserTierParams }) {
|
|
13
|
-
const tierInfo = useUserTier(params);
|
|
14
|
-
return React.createElement('div', { 'data-testid': 'tier-info' }, JSON.stringify(tierInfo));
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
describe('useUserTier - Authenticated Users', () => {
|
|
18
|
-
it('should return premium tier for authenticated premium users', () => {
|
|
19
|
-
const params: UseUserTierParams = {
|
|
20
|
-
isGuest: false,
|
|
21
|
-
userId: 'user123',
|
|
22
|
-
isPremium: true,
|
|
23
|
-
};
|
|
24
|
-
|
|
25
|
-
const component = create(React.createElement(TestComponent, { params }));
|
|
26
|
-
const tree = component.toJSON();
|
|
27
|
-
const tierInfo = JSON.parse((tree as any).children[0]);
|
|
28
|
-
|
|
29
|
-
expect(tierInfo.tier).toBe('premium');
|
|
30
|
-
expect(tierInfo.isPremium).toBe(true);
|
|
31
|
-
expect(tierInfo.isGuest).toBe(false);
|
|
32
|
-
expect(tierInfo.isAuthenticated).toBe(true);
|
|
33
|
-
expect(tierInfo.userId).toBe('user123');
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
it('should return freemium tier for authenticated non-premium users', () => {
|
|
37
|
-
const params: UseUserTierParams = {
|
|
38
|
-
isGuest: false,
|
|
39
|
-
userId: 'user123',
|
|
40
|
-
isPremium: false,
|
|
41
|
-
};
|
|
42
|
-
|
|
43
|
-
const component = create(React.createElement(TestComponent, { params }));
|
|
44
|
-
const tree = component.toJSON();
|
|
45
|
-
const tierInfo = JSON.parse((tree as any).children[0]);
|
|
46
|
-
|
|
47
|
-
expect(tierInfo.tier).toBe('freemium');
|
|
48
|
-
expect(tierInfo.isPremium).toBe(false);
|
|
49
|
-
expect(tierInfo.isGuest).toBe(false);
|
|
50
|
-
expect(tierInfo.isAuthenticated).toBe(true);
|
|
51
|
-
expect(tierInfo.userId).toBe('user123');
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
it('should handle different userId formats', () => {
|
|
55
|
-
const testCases = [
|
|
56
|
-
'user123',
|
|
57
|
-
'user-with-dashes',
|
|
58
|
-
'user_with_underscores',
|
|
59
|
-
'user@example.com',
|
|
60
|
-
'1234567890',
|
|
61
|
-
];
|
|
62
|
-
|
|
63
|
-
testCases.forEach(userId => {
|
|
64
|
-
const params: UseUserTierParams = {
|
|
65
|
-
isGuest: false,
|
|
66
|
-
userId,
|
|
67
|
-
isPremium: false,
|
|
68
|
-
};
|
|
69
|
-
|
|
70
|
-
const component = create(React.createElement(TestComponent, { params }));
|
|
71
|
-
const tree = component.toJSON();
|
|
72
|
-
const tierInfo = JSON.parse((tree as any).children[0]);
|
|
73
|
-
|
|
74
|
-
expect(tierInfo.userId).toBe(userId);
|
|
75
|
-
expect(tierInfo.isAuthenticated).toBe(true);
|
|
76
|
-
expect(tierInfo.isGuest).toBe(false);
|
|
77
|
-
});
|
|
78
|
-
});
|
|
79
|
-
});
|
|
@@ -1,70 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* useUserTier Hook Tests - Guest Users
|
|
3
|
-
*
|
|
4
|
-
* Tests for guest user scenarios
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import React from 'react';
|
|
8
|
-
import { create } from 'react-test-renderer';
|
|
9
|
-
import { useUserTier, type UseUserTierParams } from '../useUserTier';
|
|
10
|
-
|
|
11
|
-
// Test component that uses the hook
|
|
12
|
-
function TestComponent({ params }: { params: UseUserTierParams }) {
|
|
13
|
-
const tierInfo = useUserTier(params);
|
|
14
|
-
return React.createElement('div', { 'data-testid': 'tier-info' }, JSON.stringify(tierInfo));
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
describe('useUserTier - Guest Users', () => {
|
|
18
|
-
it('should return guest tier for guest users', () => {
|
|
19
|
-
const params: UseUserTierParams = {
|
|
20
|
-
isGuest: true,
|
|
21
|
-
userId: null,
|
|
22
|
-
isPremium: false,
|
|
23
|
-
};
|
|
24
|
-
|
|
25
|
-
const component = create(React.createElement(TestComponent, { params }));
|
|
26
|
-
const tree = component.toJSON();
|
|
27
|
-
const tierInfo = JSON.parse((tree as any).children[0]);
|
|
28
|
-
|
|
29
|
-
expect(tierInfo.tier).toBe('guest');
|
|
30
|
-
expect(tierInfo.isPremium).toBe(false);
|
|
31
|
-
expect(tierInfo.isGuest).toBe(true);
|
|
32
|
-
expect(tierInfo.isAuthenticated).toBe(false);
|
|
33
|
-
expect(tierInfo.userId).toBe(null);
|
|
34
|
-
expect(tierInfo.isLoading).toBe(false);
|
|
35
|
-
expect(tierInfo.error).toBe(null);
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
it('should ignore isPremium for guest users', () => {
|
|
39
|
-
const params: UseUserTierParams = {
|
|
40
|
-
isGuest: true,
|
|
41
|
-
userId: null,
|
|
42
|
-
isPremium: true, // Even if true, guest should be false
|
|
43
|
-
};
|
|
44
|
-
|
|
45
|
-
const component = create(React.createElement(TestComponent, { params }));
|
|
46
|
-
const tree = component.toJSON();
|
|
47
|
-
const tierInfo = JSON.parse((tree as any).children[0]);
|
|
48
|
-
|
|
49
|
-
expect(tierInfo.tier).toBe('guest');
|
|
50
|
-
expect(tierInfo.isPremium).toBe(false); // Guest users NEVER have premium
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
it('should handle guest user with userId provided', () => {
|
|
54
|
-
const params: UseUserTierParams = {
|
|
55
|
-
isGuest: true,
|
|
56
|
-
userId: 'user123', // Even with userId, isGuest=true takes precedence
|
|
57
|
-
isPremium: true,
|
|
58
|
-
};
|
|
59
|
-
|
|
60
|
-
const component = create(React.createElement(TestComponent, { params }));
|
|
61
|
-
const tree = component.toJSON();
|
|
62
|
-
const tierInfo = JSON.parse((tree as any).children[0]);
|
|
63
|
-
|
|
64
|
-
expect(tierInfo.tier).toBe('guest');
|
|
65
|
-
expect(tierInfo.isPremium).toBe(false);
|
|
66
|
-
expect(tierInfo.isGuest).toBe(true);
|
|
67
|
-
expect(tierInfo.isAuthenticated).toBe(false);
|
|
68
|
-
expect(tierInfo.userId).toBe(null); // Should be null for guests
|
|
69
|
-
});
|
|
70
|
-
});
|
|
@@ -1,167 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* useUserTier Hook Tests - States and Memoization
|
|
3
|
-
*
|
|
4
|
-
* Tests for loading/error states and memoization
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import React from 'react';
|
|
8
|
-
import { create } from 'react-test-renderer';
|
|
9
|
-
import { useUserTier, type UseUserTierParams } from '../useUserTier';
|
|
10
|
-
|
|
11
|
-
// Test component that uses hook
|
|
12
|
-
function TestComponent({ params }: { params: UseUserTierParams }) {
|
|
13
|
-
const tierInfo = useUserTier(params);
|
|
14
|
-
return React.createElement('div', { 'data-testid': 'tier-info' }, JSON.stringify(tierInfo));
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
describe('useUserTier - States and Memoization', () => {
|
|
18
|
-
describe('Loading and error states', () => {
|
|
19
|
-
it('should pass through loading state', () => {
|
|
20
|
-
const params: UseUserTierParams = {
|
|
21
|
-
isGuest: false,
|
|
22
|
-
userId: 'user123',
|
|
23
|
-
isPremium: false,
|
|
24
|
-
isLoading: true,
|
|
25
|
-
};
|
|
26
|
-
|
|
27
|
-
const component = create(React.createElement(TestComponent, { params }));
|
|
28
|
-
const tree = component.toJSON();
|
|
29
|
-
const tierInfo = JSON.parse((tree as any).children[0]);
|
|
30
|
-
|
|
31
|
-
expect(tierInfo.isLoading).toBe(true);
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
it('should pass through error state', () => {
|
|
35
|
-
const params: UseUserTierParams = {
|
|
36
|
-
isGuest: false,
|
|
37
|
-
userId: 'user123',
|
|
38
|
-
isPremium: false,
|
|
39
|
-
error: 'Failed to fetch premium status',
|
|
40
|
-
};
|
|
41
|
-
|
|
42
|
-
const component = create(React.createElement(TestComponent, { params }));
|
|
43
|
-
const tree = component.toJSON();
|
|
44
|
-
const tierInfo = JSON.parse((tree as any).children[0]);
|
|
45
|
-
|
|
46
|
-
expect(tierInfo.error).toBe('Failed to fetch premium status');
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
it('should default loading to false', () => {
|
|
50
|
-
const params: UseUserTierParams = {
|
|
51
|
-
isGuest: false,
|
|
52
|
-
userId: 'user123',
|
|
53
|
-
isPremium: false,
|
|
54
|
-
};
|
|
55
|
-
|
|
56
|
-
const component = create(React.createElement(TestComponent, { params }));
|
|
57
|
-
const tree = component.toJSON();
|
|
58
|
-
const tierInfo = JSON.parse((tree as any).children[0]);
|
|
59
|
-
|
|
60
|
-
expect(tierInfo.isLoading).toBe(false);
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
it('should default error to null', () => {
|
|
64
|
-
const params: UseUserTierParams = {
|
|
65
|
-
isGuest: false,
|
|
66
|
-
userId: 'user123',
|
|
67
|
-
isPremium: false,
|
|
68
|
-
};
|
|
69
|
-
|
|
70
|
-
const component = create(React.createElement(TestComponent, { params }));
|
|
71
|
-
const tree = component.toJSON();
|
|
72
|
-
const tierInfo = JSON.parse((tree as any).children[0]);
|
|
73
|
-
|
|
74
|
-
expect(tierInfo.error).toBe(null);
|
|
75
|
-
});
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
describe('Memoization', () => {
|
|
79
|
-
it('should recalculate when isGuest changes', () => {
|
|
80
|
-
let params: UseUserTierParams = {
|
|
81
|
-
isGuest: false,
|
|
82
|
-
userId: 'user123',
|
|
83
|
-
isPremium: true,
|
|
84
|
-
};
|
|
85
|
-
|
|
86
|
-
const component = create(React.createElement(TestComponent, { params }));
|
|
87
|
-
let tree = component.toJSON();
|
|
88
|
-
let tierInfo = JSON.parse((tree as any).children[0]);
|
|
89
|
-
expect(tierInfo.tier).toBe('premium');
|
|
90
|
-
|
|
91
|
-
params = {
|
|
92
|
-
isGuest: true,
|
|
93
|
-
userId: null,
|
|
94
|
-
isPremium: true,
|
|
95
|
-
};
|
|
96
|
-
component.update(React.createElement(TestComponent, { params }));
|
|
97
|
-
tree = component.toJSON();
|
|
98
|
-
tierInfo = JSON.parse((tree as any).children[0]);
|
|
99
|
-
expect(tierInfo.tier).toBe('guest');
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
it('should recalculate when userId changes', () => {
|
|
103
|
-
let params: UseUserTierParams = {
|
|
104
|
-
isGuest: false,
|
|
105
|
-
userId: 'user123',
|
|
106
|
-
isPremium: true,
|
|
107
|
-
};
|
|
108
|
-
|
|
109
|
-
const component = create(React.createElement(TestComponent, { params }));
|
|
110
|
-
let tree = component.toJSON();
|
|
111
|
-
let tierInfo = JSON.parse((tree as any).children[0]);
|
|
112
|
-
expect(tierInfo.userId).toBe('user123');
|
|
113
|
-
|
|
114
|
-
params = {
|
|
115
|
-
isGuest: false,
|
|
116
|
-
userId: 'user456',
|
|
117
|
-
isPremium: true,
|
|
118
|
-
};
|
|
119
|
-
component.update(React.createElement(TestComponent, { params }));
|
|
120
|
-
tree = component.toJSON();
|
|
121
|
-
tierInfo = JSON.parse((tree as any).children[0]);
|
|
122
|
-
expect(tierInfo.userId).toBe('user456');
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
it('should recalculate when isPremium changes', () => {
|
|
126
|
-
let params: UseUserTierParams = {
|
|
127
|
-
isGuest: false,
|
|
128
|
-
userId: 'user123',
|
|
129
|
-
isPremium: false,
|
|
130
|
-
};
|
|
131
|
-
|
|
132
|
-
const component = create(React.createElement(TestComponent, { params }));
|
|
133
|
-
let tree = component.toJSON();
|
|
134
|
-
let tierInfo = JSON.parse((tree as any).children[0]);
|
|
135
|
-
expect(tierInfo.tier).toBe('freemium');
|
|
136
|
-
|
|
137
|
-
params = {
|
|
138
|
-
isGuest: false,
|
|
139
|
-
userId: 'user123',
|
|
140
|
-
isPremium: true,
|
|
141
|
-
};
|
|
142
|
-
component.update(React.createElement(TestComponent, { params }));
|
|
143
|
-
tree = component.toJSON();
|
|
144
|
-
tierInfo = JSON.parse((tree as any).children[0]);
|
|
145
|
-
expect(tierInfo.tier).toBe('premium');
|
|
146
|
-
});
|
|
147
|
-
|
|
148
|
-
it('should not recalculate when params are the same', () => {
|
|
149
|
-
const params: UseUserTierParams = {
|
|
150
|
-
isGuest: false,
|
|
151
|
-
userId: 'user123',
|
|
152
|
-
isPremium: true,
|
|
153
|
-
};
|
|
154
|
-
|
|
155
|
-
const component = create(React.createElement(TestComponent, { params }));
|
|
156
|
-
const tree1 = component.toJSON();
|
|
157
|
-
const tierInfo1 = JSON.parse((tree1 as any).children[0]);
|
|
158
|
-
|
|
159
|
-
// Update with same params
|
|
160
|
-
component.update(React.createElement(TestComponent, { params }));
|
|
161
|
-
const tree2 = component.toJSON();
|
|
162
|
-
const tierInfo2 = JSON.parse((tree2 as any).children[0]);
|
|
163
|
-
|
|
164
|
-
expect(tierInfo1).toEqual(tierInfo2);
|
|
165
|
-
});
|
|
166
|
-
});
|
|
167
|
-
});
|