@umituz/react-native-subscription 2.27.71 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@umituz/react-native-subscription",
3
- "version": "2.27.71",
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
- // Skip optimistic update if insufficient credits to prevent showing 0
46
- if (!previousCredits || previousCredits.credits < cost) {
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: old.credits - cost,
58
+ credits: newCredits,
55
59
  lastUpdatedAt: timezoneService.getNow()
56
60
  };
57
61
  });
58
- return { previousCredits, skippedOptimistic: false };
62
+
63
+ return {
64
+ previousCredits,
65
+ skippedOptimistic: false,
66
+ wasInsufficient: previousCredits.credits < cost
67
+ };
59
68
  },
60
69
  onError: (_err, _cost, context) => {
61
- // Always restore previous credits on error if we have them
62
- if (userId && context?.previousCredits && !context.skippedOptimistic) {
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();
@@ -6,8 +6,5 @@
6
6
  // Entities
7
7
  export * from "./entities/types";
8
8
 
9
- // Components
10
- export * from "./components";
11
-
12
9
  // Hooks
13
10
  export * from "./hooks";
@@ -90,8 +90,42 @@ export const usePurchasePackage = () => {
90
90
  userId: userId ?? "ANONYMOUS",
91
91
  });
92
92
  }
93
- const message = error instanceof Error ? error.message : "An error occurred";
94
- showError("Purchase Error", message);
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
- await initializeRevenueCatService(this.managerConfig!.config);
45
- this.serviceInstance = getRevenueCatService();
46
- if (!this.serviceInstance) return false;
47
- this.packageHandler!.setService(this.serviceInstance);
48
- const result = await this.serviceInstance.initialize(effectiveUserId);
49
- return result.success;
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
- let isPurchasesConfigured = false;
15
- let isLogHandlerConfigured = false;
16
- let configurationInProgress = false;
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
- // Silent error handling
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
- // Prevent unhandled promise rejection from async auth callback
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
- return SubscriptionManager.checkPremiumStatus();
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
  }
@@ -1,6 +0,0 @@
1
- /**
2
- * Domains Index
3
- */
4
-
5
- export * from "./paywall";
6
- export * from "./config";
@@ -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
- });