@umituz/react-native-subscription 1.1.1 → 1.3.0

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.
Files changed (89) hide show
  1. package/package.json +7 -23
  2. package/src/index.ts +50 -19
  3. package/src/presentation/hooks/__tests__/useUserTier.authenticated.test.ts +79 -0
  4. package/src/presentation/hooks/__tests__/useUserTier.guest.test.ts +70 -0
  5. package/src/presentation/hooks/__tests__/useUserTier.states.test.ts +167 -0
  6. package/src/presentation/hooks/usePremiumGate.ts +116 -0
  7. package/src/presentation/hooks/useUserTier.ts +78 -0
  8. package/src/presentation/hooks/useUserTierWithRepository.ts +171 -0
  9. package/src/utils/__tests__/authUtils.test.ts +52 -0
  10. package/src/utils/__tests__/edgeCases.test.ts +84 -0
  11. package/src/utils/__tests__/premiumUtils.test.ts +178 -0
  12. package/src/utils/__tests__/tierUtils.test.ts +148 -0
  13. package/src/utils/__tests__/validation.test.ts +108 -0
  14. package/src/utils/authUtils.ts +65 -0
  15. package/src/utils/premiumAsyncUtils.ts +60 -0
  16. package/src/utils/premiumStatusUtils.ts +79 -0
  17. package/src/utils/premiumUtils.ts +9 -0
  18. package/src/utils/tierUtils.ts +97 -0
  19. package/src/utils/types.ts +37 -0
  20. package/src/utils/userTierUtils.ts +81 -0
  21. package/src/utils/validation.ts +119 -0
  22. package/lib/application/ports/ISubscriptionRepository.d.ts +0 -25
  23. package/lib/application/ports/ISubscriptionRepository.d.ts.map +0 -1
  24. package/lib/application/ports/ISubscriptionRepository.js +0 -9
  25. package/lib/application/ports/ISubscriptionRepository.js.map +0 -1
  26. package/lib/application/ports/ISubscriptionService.d.ts +0 -28
  27. package/lib/application/ports/ISubscriptionService.d.ts.map +0 -1
  28. package/lib/application/ports/ISubscriptionService.js +0 -6
  29. package/lib/application/ports/ISubscriptionService.js.map +0 -1
  30. package/lib/domain/entities/SubscriptionStatus.d.ts +0 -31
  31. package/lib/domain/entities/SubscriptionStatus.d.ts.map +0 -1
  32. package/lib/domain/entities/SubscriptionStatus.js +0 -39
  33. package/lib/domain/entities/SubscriptionStatus.js.map +0 -1
  34. package/lib/domain/errors/SubscriptionError.d.ts +0 -18
  35. package/lib/domain/errors/SubscriptionError.d.ts.map +0 -1
  36. package/lib/domain/errors/SubscriptionError.js +0 -30
  37. package/lib/domain/errors/SubscriptionError.js.map +0 -1
  38. package/lib/domain/value-objects/SubscriptionConfig.d.ts +0 -15
  39. package/lib/domain/value-objects/SubscriptionConfig.d.ts.map +0 -1
  40. package/lib/domain/value-objects/SubscriptionConfig.js +0 -6
  41. package/lib/domain/value-objects/SubscriptionConfig.js.map +0 -1
  42. package/lib/index.d.ts +0 -33
  43. package/lib/index.d.ts.map +0 -1
  44. package/lib/index.js +0 -43
  45. package/lib/index.js.map +0 -1
  46. package/lib/infrastructure/services/ActivationHandler.d.ts +0 -20
  47. package/lib/infrastructure/services/ActivationHandler.d.ts.map +0 -1
  48. package/lib/infrastructure/services/ActivationHandler.js +0 -71
  49. package/lib/infrastructure/services/ActivationHandler.js.map +0 -1
  50. package/lib/infrastructure/services/SubscriptionService.d.ts +0 -22
  51. package/lib/infrastructure/services/SubscriptionService.d.ts.map +0 -1
  52. package/lib/infrastructure/services/SubscriptionService.js +0 -110
  53. package/lib/infrastructure/services/SubscriptionService.js.map +0 -1
  54. package/lib/presentation/hooks/useSubscription.d.ts +0 -33
  55. package/lib/presentation/hooks/useSubscription.d.ts.map +0 -1
  56. package/lib/presentation/hooks/useSubscription.js +0 -129
  57. package/lib/presentation/hooks/useSubscription.js.map +0 -1
  58. package/lib/utils/dateUtils.d.ts +0 -39
  59. package/lib/utils/dateUtils.d.ts.map +0 -1
  60. package/lib/utils/dateUtils.js +0 -117
  61. package/lib/utils/dateUtils.js.map +0 -1
  62. package/lib/utils/dateValidationUtils.d.ts +0 -20
  63. package/lib/utils/dateValidationUtils.d.ts.map +0 -1
  64. package/lib/utils/dateValidationUtils.js +0 -39
  65. package/lib/utils/dateValidationUtils.js.map +0 -1
  66. package/lib/utils/periodUtils.d.ts +0 -38
  67. package/lib/utils/periodUtils.d.ts.map +0 -1
  68. package/lib/utils/periodUtils.js +0 -70
  69. package/lib/utils/periodUtils.js.map +0 -1
  70. package/lib/utils/planDetectionUtils.d.ts +0 -17
  71. package/lib/utils/planDetectionUtils.d.ts.map +0 -1
  72. package/lib/utils/planDetectionUtils.js +0 -31
  73. package/lib/utils/planDetectionUtils.js.map +0 -1
  74. package/lib/utils/priceUtils.d.ts +0 -23
  75. package/lib/utils/priceUtils.d.ts.map +0 -1
  76. package/lib/utils/priceUtils.js +0 -29
  77. package/lib/utils/priceUtils.js.map +0 -1
  78. package/lib/utils/subscriptionConstants.d.ts +0 -62
  79. package/lib/utils/subscriptionConstants.d.ts.map +0 -1
  80. package/lib/utils/subscriptionConstants.js +0 -61
  81. package/lib/utils/subscriptionConstants.js.map +0 -1
  82. package/src/utils/dateUtils.test.ts +0 -116
  83. package/src/utils/dateUtils.ts +0 -147
  84. package/src/utils/periodUtils.ts +0 -104
  85. package/src/utils/planDetectionUtils.test.ts +0 -47
  86. package/src/utils/planDetectionUtils.ts +0 -40
  87. package/src/utils/priceUtils.test.ts +0 -35
  88. package/src/utils/priceUtils.ts +0 -31
  89. package/src/utils/subscriptionConstants.ts +0 -70
@@ -0,0 +1,171 @@
1
+ /**
2
+ * useUserTierWithRepository Hook
3
+ *
4
+ * Complete hook that automatically fetches premium status from repository
5
+ * and provides user tier information. This eliminates the need for app-specific
6
+ * useUserTier wrappers.
7
+ *
8
+ * This hook combines:
9
+ * - Auth state (from AuthProvider)
10
+ * - Premium status fetching (from ISubscriptionRepository)
11
+ * - Tier logic (from useUserTier)
12
+ *
13
+ * @example
14
+ * ```typescript
15
+ * import { useUserTierWithRepository } from '@umituz/react-native-premium';
16
+ * import { useAuth } from '@domains/auth';
17
+ * import { premiumRepository } from '@/infrastructure/repositories/PremiumRepository';
18
+ *
19
+ * const { tier, isPremium, isGuest, isLoading, refresh } = useUserTierWithRepository({
20
+ * auth: useAuth(),
21
+ * repository: premiumRepository,
22
+ * });
23
+ * ```
24
+ */
25
+
26
+ import { useEffect, useState, useCallback } from 'react';
27
+ import { useUserTier, type UseUserTierParams } from './useUserTier';
28
+ import type { ISubscriptionRepository } from '../../application/ports/ISubscriptionRepository';
29
+
30
+ /**
31
+ * Auth provider interface
32
+ * Apps should provide an object that matches this interface
33
+ */
34
+ export interface AuthProvider {
35
+ /** Current user object (null for guests) */
36
+ user: { uid: string } | null;
37
+ /** Whether user is a guest */
38
+ isGuest: boolean;
39
+ /** Whether user is authenticated */
40
+ isAuthenticated: boolean;
41
+ }
42
+
43
+ export interface UseUserTierWithRepositoryParams {
44
+ /** Auth provider (e.g., result of useAuth hook) */
45
+ auth: AuthProvider;
46
+ /** Subscription repository for fetching premium status */
47
+ repository: ISubscriptionRepository;
48
+ }
49
+
50
+ export interface UseUserTierWithRepositoryResult {
51
+ /** User tier: 'guest' | 'freemium' | 'premium' */
52
+ tier: 'guest' | 'freemium' | 'premium';
53
+ /** Whether user has premium access */
54
+ isPremium: boolean;
55
+ /** Whether user is a guest */
56
+ isGuest: boolean;
57
+ /** Whether user is authenticated */
58
+ isAuthenticated: boolean;
59
+ /** User ID (null for guests) */
60
+ userId: string | null;
61
+ /** Whether premium status is currently loading */
62
+ isLoading: boolean;
63
+ /** Premium status error (if any) */
64
+ error: string | null;
65
+ /** Refresh premium status from repository */
66
+ refresh: () => Promise<void>;
67
+ }
68
+
69
+ /**
70
+ * Hook that automatically fetches premium status and provides user tier information
71
+ *
72
+ * This hook eliminates the need for app-specific useUserTier wrappers by:
73
+ * 1. Automatically fetching premium status from repository
74
+ * 2. Handling loading and error states
75
+ * 3. Providing refresh functionality
76
+ * 4. Using centralized tier logic from useUserTier
77
+ */
78
+ export function useUserTierWithRepository(
79
+ params: UseUserTierWithRepositoryParams,
80
+ ): UseUserTierWithRepositoryResult {
81
+ const { auth, repository } = params;
82
+ const { user, isGuest, isAuthenticated } = auth;
83
+
84
+ const [isPremium, setIsPremium] = useState<boolean>(false);
85
+ const [isLoading, setIsLoading] = useState<boolean>(true);
86
+ const [error, setError] = useState<string | null>(null);
87
+
88
+ // Fetch premium status from repository
89
+ const fetchPremiumStatus = useCallback(async (signal?: AbortSignal) => {
90
+ // Guest users are never premium - no need to fetch
91
+ if (!isAuthenticated || !user) {
92
+ setIsPremium(false);
93
+ setIsLoading(false);
94
+ setError(null);
95
+ return;
96
+ }
97
+
98
+ try {
99
+ setIsLoading(true);
100
+ setError(null);
101
+
102
+ const status = await repository.getSubscriptionStatus(user.uid);
103
+
104
+ // Check if operation was aborted
105
+ if (signal?.aborted) {
106
+ return;
107
+ }
108
+
109
+ const isPremiumValue =
110
+ status !== null && repository.isSubscriptionValid(status);
111
+
112
+ // Check again before setting state
113
+ if (!signal?.aborted) {
114
+ setIsPremium(isPremiumValue);
115
+ setIsLoading(false);
116
+ }
117
+ } catch (err) {
118
+ // Don't set state if operation was aborted
119
+ if (!signal?.aborted) {
120
+ const errorMessage =
121
+ err instanceof Error ? err.message : 'Failed to fetch premium status';
122
+ setError(errorMessage);
123
+ setIsPremium(false);
124
+ setIsLoading(false);
125
+ }
126
+ }
127
+ }, [isAuthenticated, user, repository]);
128
+
129
+ // Fetch premium status when auth state changes
130
+ useEffect(() => {
131
+ const abortController = new AbortController();
132
+
133
+ fetchPremiumStatus(abortController.signal).catch(() => {
134
+ // Error is handled in fetchPremiumStatus
135
+ });
136
+
137
+ return () => {
138
+ abortController.abort();
139
+ };
140
+ }, [fetchPremiumStatus]);
141
+
142
+ // Refresh function
143
+ const refresh = useCallback(async () => {
144
+ if (!isAuthenticated || !user) {
145
+ return;
146
+ }
147
+ const abortController = new AbortController();
148
+ try {
149
+ await fetchPremiumStatus(abortController.signal);
150
+ } finally {
151
+ abortController.abort();
152
+ }
153
+ }, [isAuthenticated, user, fetchPremiumStatus]);
154
+
155
+ // Use base useUserTier hook for tier logic
156
+ const useUserTierParams: UseUserTierParams = {
157
+ isGuest: isGuest || !isAuthenticated,
158
+ userId: user?.uid || null,
159
+ isPremium,
160
+ isLoading,
161
+ error,
162
+ };
163
+
164
+ const tierInfo = useUserTier(useUserTierParams);
165
+
166
+ return {
167
+ ...tierInfo,
168
+ refresh,
169
+ };
170
+ }
171
+
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Authentication Utilities Tests
3
+ *
4
+ * Tests for authentication check functions
5
+ */
6
+
7
+ import {
8
+ isAuthenticated,
9
+ isGuest,
10
+ } from '../authUtils';
11
+
12
+ describe('isAuthenticated', () => {
13
+ it('should return false for guest users', () => {
14
+ expect(isAuthenticated(true, null)).toBe(false);
15
+ expect(isAuthenticated(true, 'user123')).toBe(false);
16
+ });
17
+
18
+ it('should return false when userId is null', () => {
19
+ expect(isAuthenticated(false, null)).toBe(false);
20
+ });
21
+
22
+ it('should return true for authenticated users', () => {
23
+ expect(isAuthenticated(false, 'user123')).toBe(true);
24
+ });
25
+
26
+ it('should throw error for invalid inputs', () => {
27
+ expect(() => isAuthenticated('invalid' as any, null)).toThrow(TypeError);
28
+ expect(() => isAuthenticated(true, 123 as any)).toThrow(TypeError);
29
+ expect(() => isAuthenticated(true, '' as any)).toThrow(TypeError);
30
+ });
31
+ });
32
+
33
+ describe('isGuest', () => {
34
+ it('should return true for guest users', () => {
35
+ expect(isGuest(true, null)).toBe(true);
36
+ expect(isGuest(true, 'user123')).toBe(true);
37
+ });
38
+
39
+ it('should return true when userId is null', () => {
40
+ expect(isGuest(false, null)).toBe(true);
41
+ });
42
+
43
+ it('should return false for authenticated users', () => {
44
+ expect(isGuest(false, 'user123')).toBe(false);
45
+ });
46
+
47
+ it('should throw error for invalid inputs', () => {
48
+ expect(() => isGuest('invalid' as any, null)).toThrow(TypeError);
49
+ expect(() => isGuest(true, 123 as any)).toThrow(TypeError);
50
+ expect(() => isGuest(true, '' as any)).toThrow(TypeError);
51
+ });
52
+ });
@@ -0,0 +1,84 @@
1
+ /**
2
+ * Edge Cases Tests
3
+ *
4
+ * Tests for edge cases and special scenarios
5
+ */
6
+
7
+ import {
8
+ getUserTierInfo,
9
+ } from '../tierUtils';
10
+ import {
11
+ isAuthenticated,
12
+ isGuest,
13
+ } from '../authUtils';
14
+ import { validateUserId } from '../validation';
15
+
16
+ describe('Edge Cases', () => {
17
+ describe('User ID validation', () => {
18
+ it('should handle empty string userId as invalid', () => {
19
+ expect(() => validateUserId('')).toThrow(TypeError);
20
+ });
21
+
22
+ it('should handle whitespace-only userId as invalid', () => {
23
+ expect(() => validateUserId(' ')).toThrow(TypeError);
24
+ });
25
+
26
+ it('should handle very long userId strings', () => {
27
+ const longUserId = 'a'.repeat(1000);
28
+ const result = getUserTierInfo(false, longUserId, true);
29
+ expect(result.userId).toBe(longUserId);
30
+ expect(result.tier).toBe('premium');
31
+ });
32
+
33
+ it('should handle special characters in userId', () => {
34
+ const specialUserId = 'user-123_test@example.com';
35
+ const result = getUserTierInfo(false, specialUserId, false);
36
+ expect(result.userId).toBe(specialUserId);
37
+ expect(result.tier).toBe('freemium');
38
+ });
39
+ });
40
+
41
+ describe('Authentication edge cases', () => {
42
+ it('should handle conflicting auth states consistently', () => {
43
+ // isGuest=true but userId provided - should prioritize guest logic
44
+ expect(isAuthenticated(true, 'user123')).toBe(false);
45
+ expect(isGuest(true, 'user123')).toBe(true);
46
+
47
+ const result = getUserTierInfo(true, 'user123', true);
48
+ expect(result.tier).toBe('guest');
49
+ expect(result.isPremium).toBe(false);
50
+ });
51
+
52
+ it('should handle isGuest=false but null userId', () => {
53
+ // isGuest=false but userId=null - should treat as guest
54
+ expect(isAuthenticated(false, null)).toBe(false);
55
+ expect(isGuest(false, null)).toBe(true);
56
+
57
+ const result = getUserTierInfo(false, null, true);
58
+ expect(result.tier).toBe('guest');
59
+ expect(result.isPremium).toBe(false);
60
+ });
61
+ });
62
+
63
+ describe('Premium status edge cases', () => {
64
+ it('should ignore isPremium for guest users regardless of value', () => {
65
+ const guestTrue = getUserTierInfo(true, null, true);
66
+ const guestFalse = getUserTierInfo(true, null, false);
67
+
68
+ expect(guestTrue.isPremium).toBe(false);
69
+ expect(guestFalse.isPremium).toBe(false);
70
+ expect(guestTrue.tier).toBe('guest');
71
+ expect(guestFalse.tier).toBe('guest');
72
+ });
73
+
74
+ it('should handle authenticated users with various premium states', () => {
75
+ const premium = getUserTierInfo(false, 'user123', true);
76
+ const freemium = getUserTierInfo(false, 'user123', false);
77
+
78
+ expect(premium.tier).toBe('premium');
79
+ expect(premium.isPremium).toBe(true);
80
+ expect(freemium.tier).toBe('freemium');
81
+ expect(freemium.isPremium).toBe(false);
82
+ });
83
+ });
84
+ });
@@ -0,0 +1,178 @@
1
+ /**
2
+ * Premium Utilities Tests
3
+ *
4
+ * Tests for premium status fetching and async functions
5
+ */
6
+
7
+ import {
8
+ getIsPremium,
9
+ } from '../premiumStatusUtils';
10
+ import {
11
+ getUserTierInfoAsync,
12
+ checkPremiumAccessAsync,
13
+ } from '../premiumAsyncUtils';
14
+ import type { PremiumStatusFetcher } from '../types';
15
+
16
+ describe('getIsPremium', () => {
17
+ describe('Sync mode (boolean isPremium)', () => {
18
+ it('should return false for guest users', () => {
19
+ const result = getIsPremium(true, null, true);
20
+ expect(result).toBe(false);
21
+ });
22
+
23
+ it('should return false when userId is null', () => {
24
+ const result = getIsPremium(false, null, true);
25
+ expect(result).toBe(false);
26
+ });
27
+
28
+ it('should return true when isPremium is true', () => {
29
+ const result = getIsPremium(false, 'user123', true);
30
+ expect(result).toBe(true);
31
+ });
32
+
33
+ it('should return false when isPremium is false', () => {
34
+ const result = getIsPremium(false, 'user123', false);
35
+ expect(result).toBe(false);
36
+ });
37
+
38
+ it('should throw error for invalid inputs', () => {
39
+ expect(() => getIsPremium('invalid' as any, null, true)).toThrow(TypeError);
40
+ expect(() => getIsPremium(true, 123 as any, true)).toThrow(TypeError);
41
+ });
42
+ });
43
+
44
+ describe('Async mode (fetcher)', () => {
45
+ const mockFetcher: PremiumStatusFetcher = {
46
+ isPremium: jest.fn(),
47
+ };
48
+
49
+ beforeEach(() => {
50
+ jest.clearAllMocks();
51
+ });
52
+
53
+ it('should return false for guest users without calling fetcher', async () => {
54
+ const result = await getIsPremium(true, null, mockFetcher);
55
+ expect(result).toBe(false);
56
+ expect(mockFetcher.isPremium).not.toHaveBeenCalled();
57
+ });
58
+
59
+ it('should return false when userId is null without calling fetcher', async () => {
60
+ const result = await getIsPremium(false, null, mockFetcher);
61
+ expect(result).toBe(false);
62
+ expect(mockFetcher.isPremium).not.toHaveBeenCalled();
63
+ });
64
+
65
+ it('should call fetcher for authenticated users', async () => {
66
+ (mockFetcher.isPremium as jest.Mock).mockResolvedValue(true);
67
+
68
+ const result = await getIsPremium(false, 'user123', mockFetcher);
69
+ expect(result).toBe(true);
70
+ expect(mockFetcher.isPremium).toHaveBeenCalledWith('user123');
71
+ expect(mockFetcher.isPremium).toHaveBeenCalledTimes(1);
72
+ });
73
+
74
+ it('should return false when fetcher returns false', async () => {
75
+ (mockFetcher.isPremium as jest.Mock).mockResolvedValue(false);
76
+
77
+ const result = await getIsPremium(false, 'user123', mockFetcher);
78
+ expect(result).toBe(false);
79
+ expect(mockFetcher.isPremium).toHaveBeenCalledWith('user123');
80
+ });
81
+
82
+ it('should throw error when fetcher throws Error', async () => {
83
+ const error = new Error('Database error');
84
+ (mockFetcher.isPremium as jest.Mock).mockRejectedValue(error);
85
+
86
+ await expect(getIsPremium(false, 'user123', mockFetcher)).rejects.toThrow(
87
+ 'Failed to fetch premium status: Database error'
88
+ );
89
+ });
90
+
91
+ it('should throw error when fetcher throws non-Error', async () => {
92
+ const error = 'String error';
93
+ (mockFetcher.isPremium as jest.Mock).mockRejectedValue(error);
94
+
95
+ await expect(getIsPremium(false, 'user123', mockFetcher)).rejects.toThrow(
96
+ 'Failed to fetch premium status: String error'
97
+ );
98
+ });
99
+
100
+ it('should throw error for invalid inputs', () => {
101
+ // Invalid isGuest/userId - validation happens before async check (sync)
102
+ expect(() => getIsPremium('invalid' as any, null, mockFetcher)).toThrow(TypeError);
103
+ expect(() => getIsPremium(true, 123 as any, mockFetcher)).toThrow(TypeError);
104
+
105
+ // Invalid fetcher - validation happens in async mode but throws sync
106
+ // Use authenticated user (not guest) to reach fetcher validation
107
+ expect(() => getIsPremium(false, 'user123', null as any)).toThrow(TypeError);
108
+ expect(() => getIsPremium(false, 'user123', {} as any)).toThrow(TypeError);
109
+ });
110
+ });
111
+ });
112
+
113
+ describe('getUserTierInfoAsync', () => {
114
+ const mockFetcher: PremiumStatusFetcher = {
115
+ isPremium: jest.fn(),
116
+ };
117
+
118
+ beforeEach(() => {
119
+ jest.clearAllMocks();
120
+ });
121
+
122
+ it('should return guest tier for guest users', async () => {
123
+ const result = await getUserTierInfoAsync(true, null, mockFetcher);
124
+ expect(result.tier).toBe('guest');
125
+ expect(result.isPremium).toBe(false);
126
+ expect(mockFetcher.isPremium).not.toHaveBeenCalled();
127
+ });
128
+
129
+ it('should return premium tier when fetcher returns true', async () => {
130
+ (mockFetcher.isPremium as jest.Mock).mockResolvedValue(true);
131
+
132
+ const result = await getUserTierInfoAsync(false, 'user123', mockFetcher);
133
+ expect(result.tier).toBe('premium');
134
+ expect(result.isPremium).toBe(true);
135
+ expect(result.isGuest).toBe(false);
136
+ expect(result.isAuthenticated).toBe(true);
137
+ });
138
+
139
+ it('should return freemium tier when fetcher returns false', async () => {
140
+ (mockFetcher.isPremium as jest.Mock).mockResolvedValue(false);
141
+
142
+ const result = await getUserTierInfoAsync(false, 'user123', mockFetcher);
143
+ expect(result.tier).toBe('freemium');
144
+ expect(result.isPremium).toBe(false);
145
+ expect(result.isGuest).toBe(false);
146
+ expect(result.isAuthenticated).toBe(true);
147
+ });
148
+ });
149
+
150
+ describe('checkPremiumAccessAsync', () => {
151
+ const mockFetcher: PremiumStatusFetcher = {
152
+ isPremium: jest.fn(),
153
+ };
154
+
155
+ beforeEach(() => {
156
+ jest.clearAllMocks();
157
+ });
158
+
159
+ it('should return false for guest users', async () => {
160
+ const result = await checkPremiumAccessAsync(true, null, mockFetcher);
161
+ expect(result).toBe(false);
162
+ expect(mockFetcher.isPremium).not.toHaveBeenCalled();
163
+ });
164
+
165
+ it('should return true when fetcher returns true', async () => {
166
+ (mockFetcher.isPremium as jest.Mock).mockResolvedValue(true);
167
+
168
+ const result = await checkPremiumAccessAsync(false, 'user123', mockFetcher);
169
+ expect(result).toBe(true);
170
+ });
171
+
172
+ it('should return false when fetcher returns false', async () => {
173
+ (mockFetcher.isPremium as jest.Mock).mockResolvedValue(false);
174
+
175
+ const result = await checkPremiumAccessAsync(false, 'user123', mockFetcher);
176
+ expect(result).toBe(false);
177
+ });
178
+ });
@@ -0,0 +1,148 @@
1
+ /**
2
+ * Tier Utilities Tests
3
+ *
4
+ * Tests for tier determination and comparison functions
5
+ */
6
+
7
+ import {
8
+ getUserTierInfo,
9
+ checkPremiumAccess,
10
+ } from '../tierUtils';
11
+ import {
12
+ hasTierAccess,
13
+ isTierPremium,
14
+ isTierFreemium,
15
+ isTierGuest,
16
+ } from '../userTierUtils';
17
+
18
+ describe('getUserTierInfo', () => {
19
+ describe('Guest users', () => {
20
+ it('should return guest tier when isGuest is true', () => {
21
+ const result = getUserTierInfo(true, null, false);
22
+ expect(result.tier).toBe('guest');
23
+ expect(result.isPremium).toBe(false);
24
+ expect(result.isGuest).toBe(true);
25
+ expect(result.isAuthenticated).toBe(false);
26
+ expect(result.userId).toBe(null);
27
+ });
28
+
29
+ it('should return guest tier when userId is null', () => {
30
+ const result = getUserTierInfo(false, null, false);
31
+ expect(result.tier).toBe('guest');
32
+ expect(result.isPremium).toBe(false);
33
+ expect(result.isGuest).toBe(true);
34
+ expect(result.isAuthenticated).toBe(false);
35
+ expect(result.userId).toBe(null);
36
+ });
37
+
38
+ it('should ignore isPremium for guest users', () => {
39
+ const result = getUserTierInfo(true, null, true);
40
+ expect(result.tier).toBe('guest');
41
+ expect(result.isPremium).toBe(false); // Guest users NEVER have premium
42
+ });
43
+ });
44
+
45
+ describe('Authenticated users', () => {
46
+ it('should return premium tier for authenticated premium users', () => {
47
+ const result = getUserTierInfo(false, 'user123', true);
48
+ expect(result.tier).toBe('premium');
49
+ expect(result.isPremium).toBe(true);
50
+ expect(result.isGuest).toBe(false);
51
+ expect(result.isAuthenticated).toBe(true);
52
+ expect(result.userId).toBe('user123');
53
+ });
54
+
55
+ it('should return freemium tier for authenticated non-premium users', () => {
56
+ const result = getUserTierInfo(false, 'user123', false);
57
+ expect(result.tier).toBe('freemium');
58
+ expect(result.isPremium).toBe(false);
59
+ expect(result.isGuest).toBe(false);
60
+ expect(result.isAuthenticated).toBe(true);
61
+ expect(result.userId).toBe('user123');
62
+ });
63
+ });
64
+
65
+ it('should throw error for invalid inputs', () => {
66
+ expect(() => getUserTierInfo('invalid' as any, null, false)).toThrow(TypeError);
67
+ expect(() => getUserTierInfo(true, 123 as any, false)).toThrow(TypeError);
68
+ expect(() => getUserTierInfo(true, null, 'invalid' as any)).toThrow(TypeError);
69
+ });
70
+ });
71
+
72
+ describe('checkPremiumAccess', () => {
73
+ it('should return false for guest users', () => {
74
+ expect(checkPremiumAccess(true, null, true)).toBe(false);
75
+ expect(checkPremiumAccess(true, null, false)).toBe(false);
76
+ });
77
+
78
+ it('should return false when userId is null', () => {
79
+ expect(checkPremiumAccess(false, null, true)).toBe(false);
80
+ });
81
+
82
+ it('should return true for authenticated premium users', () => {
83
+ expect(checkPremiumAccess(false, 'user123', true)).toBe(true);
84
+ });
85
+
86
+ it('should return false for authenticated freemium users', () => {
87
+ expect(checkPremiumAccess(false, 'user123', false)).toBe(false);
88
+ });
89
+
90
+ it('should throw error for invalid inputs', () => {
91
+ expect(() => checkPremiumAccess('invalid' as any, null, true)).toThrow(TypeError);
92
+ expect(() => checkPremiumAccess(true, 123 as any, true)).toThrow(TypeError);
93
+ expect(() => checkPremiumAccess(true, null, 'invalid' as any)).toThrow(TypeError);
94
+ });
95
+ });
96
+
97
+ describe('hasTierAccess', () => {
98
+ it('should return true when tier1 has higher access', () => {
99
+ expect(hasTierAccess('premium', 'freemium')).toBe(true);
100
+ expect(hasTierAccess('premium', 'guest')).toBe(true);
101
+ expect(hasTierAccess('freemium', 'guest')).toBe(true);
102
+ });
103
+
104
+ it('should return true when tiers are equal', () => {
105
+ expect(hasTierAccess('premium', 'premium')).toBe(true);
106
+ expect(hasTierAccess('freemium', 'freemium')).toBe(true);
107
+ expect(hasTierAccess('guest', 'guest')).toBe(true);
108
+ });
109
+
110
+ it('should return false when tier1 has lower access', () => {
111
+ expect(hasTierAccess('freemium', 'premium')).toBe(false);
112
+ expect(hasTierAccess('guest', 'premium')).toBe(false);
113
+ expect(hasTierAccess('guest', 'freemium')).toBe(false);
114
+ });
115
+ });
116
+
117
+ describe('isTierPremium', () => {
118
+ it('should return true for premium tier', () => {
119
+ expect(isTierPremium('premium')).toBe(true);
120
+ });
121
+
122
+ it('should return false for non-premium tiers', () => {
123
+ expect(isTierPremium('freemium')).toBe(false);
124
+ expect(isTierPremium('guest')).toBe(false);
125
+ });
126
+ });
127
+
128
+ describe('isTierFreemium', () => {
129
+ it('should return true for freemium tier', () => {
130
+ expect(isTierFreemium('freemium')).toBe(true);
131
+ });
132
+
133
+ it('should return false for non-freemium tiers', () => {
134
+ expect(isTierFreemium('premium')).toBe(false);
135
+ expect(isTierFreemium('guest')).toBe(false);
136
+ });
137
+ });
138
+
139
+ describe('isTierGuest', () => {
140
+ it('should return true for guest tier', () => {
141
+ expect(isTierGuest('guest')).toBe(true);
142
+ });
143
+
144
+ it('should return false for non-guest tiers', () => {
145
+ expect(isTierGuest('premium')).toBe(false);
146
+ expect(isTierGuest('freemium')).toBe(false);
147
+ });
148
+ });