@umituz/react-native-subscription 1.2.0 → 1.3.1
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/index.ts +50 -19
- package/src/presentation/hooks/__tests__/useUserTier.authenticated.test.ts +79 -0
- package/src/presentation/hooks/__tests__/useUserTier.guest.test.ts +70 -0
- package/src/presentation/hooks/__tests__/useUserTier.states.test.ts +167 -0
- package/src/presentation/hooks/usePremiumGate.ts +116 -0
- package/src/presentation/hooks/useUserTier.ts +78 -0
- package/src/presentation/hooks/useUserTierWithRepository.ts +171 -0
- package/src/utils/__tests__/authUtils.test.ts +52 -0
- package/src/utils/__tests__/edgeCases.test.ts +84 -0
- package/src/utils/__tests__/premiumUtils.test.ts +178 -0
- package/src/utils/__tests__/tierUtils.test.ts +148 -0
- package/src/utils/__tests__/validation.test.ts +108 -0
- package/src/utils/authUtils.ts +65 -0
- package/src/utils/premiumAsyncUtils.ts +60 -0
- package/src/utils/premiumStatusUtils.ts +79 -0
- package/src/utils/premiumUtils.ts +9 -0
- package/src/utils/tierUtils.ts +97 -0
- package/src/utils/types.ts +37 -0
- package/src/utils/userTierUtils.ts +81 -0
- package/src/utils/validation.ts +119 -0
- package/src/utils/dateUtils.test.ts +0 -116
- package/src/utils/dateUtils.ts +0 -147
- package/src/utils/periodUtils.ts +0 -104
- package/src/utils/planDetectionUtils.test.ts +0 -47
- package/src/utils/planDetectionUtils.ts +0 -40
- package/src/utils/priceUtils.test.ts +0 -35
- package/src/utils/priceUtils.ts +0 -31
- package/src/utils/subscriptionConstants.ts +0 -70
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* User Tier Validation Tests
|
|
3
|
+
*
|
|
4
|
+
* Tests for validation functions and type guards
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
isValidUserTier,
|
|
9
|
+
isUserTierInfo,
|
|
10
|
+
validateUserId,
|
|
11
|
+
validateIsGuest,
|
|
12
|
+
validateIsPremium,
|
|
13
|
+
validateFetcher,
|
|
14
|
+
type UserTier,
|
|
15
|
+
type UserTierInfo,
|
|
16
|
+
type PremiumStatusFetcher,
|
|
17
|
+
} from '../validation';
|
|
18
|
+
|
|
19
|
+
describe('isValidUserTier', () => {
|
|
20
|
+
it('should return true for valid tiers', () => {
|
|
21
|
+
expect(isValidUserTier('guest')).toBe(true);
|
|
22
|
+
expect(isValidUserTier('freemium')).toBe(true);
|
|
23
|
+
expect(isValidUserTier('premium')).toBe(true);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('should return false for invalid values', () => {
|
|
27
|
+
expect(isValidUserTier('invalid')).toBe(false);
|
|
28
|
+
expect(isValidUserTier('')).toBe(false);
|
|
29
|
+
expect(isValidUserTier(null)).toBe(false);
|
|
30
|
+
expect(isValidUserTier(undefined)).toBe(false);
|
|
31
|
+
expect(isValidUserTier(123)).toBe(false);
|
|
32
|
+
expect(isValidUserTier({})).toBe(false);
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
describe('isUserTierInfo', () => {
|
|
37
|
+
it('should return true for valid UserTierInfo', () => {
|
|
38
|
+
const validInfo: UserTierInfo = {
|
|
39
|
+
tier: 'premium',
|
|
40
|
+
isPremium: true,
|
|
41
|
+
isGuest: false,
|
|
42
|
+
isAuthenticated: true,
|
|
43
|
+
userId: 'user123',
|
|
44
|
+
};
|
|
45
|
+
expect(isUserTierInfo(validInfo)).toBe(true);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('should return false for invalid objects', () => {
|
|
49
|
+
expect(isUserTierInfo(null)).toBe(false);
|
|
50
|
+
expect(isUserTierInfo(undefined)).toBe(false);
|
|
51
|
+
expect(isUserTierInfo('string')).toBe(false);
|
|
52
|
+
expect(isUserTierInfo({})).toBe(false);
|
|
53
|
+
expect(isUserTierInfo({ tier: 'invalid' })).toBe(false);
|
|
54
|
+
expect(isUserTierInfo({ tier: 'premium' })).toBe(false);
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
describe('validateUserId', () => {
|
|
59
|
+
it('should not throw for valid userId', () => {
|
|
60
|
+
expect(() => validateUserId('user123')).not.toThrow();
|
|
61
|
+
expect(() => validateUserId(null)).not.toThrow();
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('should throw for invalid userId', () => {
|
|
65
|
+
expect(() => validateUserId('')).toThrow(TypeError);
|
|
66
|
+
expect(() => validateUserId(' ')).toThrow(TypeError);
|
|
67
|
+
expect(() => validateUserId(123 as any)).toThrow(TypeError);
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
describe('validateIsGuest', () => {
|
|
72
|
+
it('should not throw for valid isGuest', () => {
|
|
73
|
+
expect(() => validateIsGuest(true)).not.toThrow();
|
|
74
|
+
expect(() => validateIsGuest(false)).not.toThrow();
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('should throw for invalid isGuest', () => {
|
|
78
|
+
expect(() => validateIsGuest('true' as any)).toThrow(TypeError);
|
|
79
|
+
expect(() => validateIsGuest(1 as any)).toThrow(TypeError);
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
describe('validateIsPremium', () => {
|
|
84
|
+
it('should not throw for valid isPremium', () => {
|
|
85
|
+
expect(() => validateIsPremium(true)).not.toThrow();
|
|
86
|
+
expect(() => validateIsPremium(false)).not.toThrow();
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('should throw for invalid isPremium', () => {
|
|
90
|
+
expect(() => validateIsPremium('true' as any)).toThrow(TypeError);
|
|
91
|
+
expect(() => validateIsPremium(1 as any)).toThrow(TypeError);
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
describe('validateFetcher', () => {
|
|
96
|
+
it('should not throw for valid fetcher', () => {
|
|
97
|
+
const validFetcher: PremiumStatusFetcher = {
|
|
98
|
+
isPremium: async () => true,
|
|
99
|
+
};
|
|
100
|
+
expect(() => validateFetcher(validFetcher)).not.toThrow();
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('should throw for invalid fetcher', () => {
|
|
104
|
+
expect(() => validateFetcher(null as any)).toThrow(TypeError);
|
|
105
|
+
expect(() => validateFetcher({} as any)).toThrow(TypeError);
|
|
106
|
+
expect(() => validateFetcher({ isPremium: 'not a function' } as any)).toThrow(TypeError);
|
|
107
|
+
});
|
|
108
|
+
});
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Authentication Utilities
|
|
3
|
+
*
|
|
4
|
+
* Centralized logic for authentication checks
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { validateIsGuest, validateUserId } from './validation';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Check if user is authenticated
|
|
11
|
+
*
|
|
12
|
+
* This is the SINGLE SOURCE OF TRUTH for authentication check.
|
|
13
|
+
* All apps should use this function for consistent authentication logic.
|
|
14
|
+
*
|
|
15
|
+
* @param isGuest - Whether user is a guest
|
|
16
|
+
* @param userId - User ID (null for guests)
|
|
17
|
+
* @returns Whether user is authenticated
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* ```typescript
|
|
21
|
+
* // Guest user
|
|
22
|
+
* isAuthenticated(true, null); // false
|
|
23
|
+
*
|
|
24
|
+
* // Authenticated user
|
|
25
|
+
* isAuthenticated(false, 'user123'); // true
|
|
26
|
+
* ```
|
|
27
|
+
*/
|
|
28
|
+
export function isAuthenticated(
|
|
29
|
+
isGuest: boolean,
|
|
30
|
+
userId: string | null,
|
|
31
|
+
): boolean {
|
|
32
|
+
validateIsGuest(isGuest);
|
|
33
|
+
validateUserId(userId);
|
|
34
|
+
|
|
35
|
+
return !isGuest && userId !== null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Check if user is guest
|
|
40
|
+
*
|
|
41
|
+
* This is the SINGLE SOURCE OF TRUTH for guest check.
|
|
42
|
+
* All apps should use this function for consistent guest logic.
|
|
43
|
+
*
|
|
44
|
+
* @param isGuest - Whether user is a guest
|
|
45
|
+
* @param userId - User ID (null for guests)
|
|
46
|
+
* @returns Whether user is a guest
|
|
47
|
+
*
|
|
48
|
+
* @example
|
|
49
|
+
* ```typescript
|
|
50
|
+
* // Guest user
|
|
51
|
+
* isGuest(true, null); // true
|
|
52
|
+
*
|
|
53
|
+
* // Authenticated user
|
|
54
|
+
* isGuest(false, 'user123'); // false
|
|
55
|
+
* ```
|
|
56
|
+
*/
|
|
57
|
+
export function isGuest(
|
|
58
|
+
isGuestFlag: boolean,
|
|
59
|
+
userId: string | null,
|
|
60
|
+
): boolean {
|
|
61
|
+
validateIsGuest(isGuestFlag);
|
|
62
|
+
validateUserId(userId);
|
|
63
|
+
|
|
64
|
+
return isGuestFlag || userId === null;
|
|
65
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Async Premium Utilities
|
|
3
|
+
*
|
|
4
|
+
* Async premium status fetching and tier determination
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { getIsPremium } from './premiumStatusUtils';
|
|
8
|
+
import type { PremiumStatusFetcher } from './types';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Get user tier info asynchronously with fetcher
|
|
12
|
+
*
|
|
13
|
+
* This function combines getUserTierInfo and getIsPremium logic.
|
|
14
|
+
* All tier determination logic is centralized here.
|
|
15
|
+
*
|
|
16
|
+
* @param isGuest - Whether user is a guest
|
|
17
|
+
* @param userId - User ID (null for guests)
|
|
18
|
+
* @param fetcher - Premium status fetcher (app-specific implementation)
|
|
19
|
+
* @returns Promise<UserTierInfo> - User tier information
|
|
20
|
+
*/
|
|
21
|
+
export async function getUserTierInfoAsync(
|
|
22
|
+
isGuestFlag: boolean,
|
|
23
|
+
userId: string | null,
|
|
24
|
+
fetcher: PremiumStatusFetcher,
|
|
25
|
+
): Promise<import('./types').UserTierInfo> {
|
|
26
|
+
// Import here to avoid circular dependency
|
|
27
|
+
const { getUserTierInfo } = await import('./tierUtils');
|
|
28
|
+
|
|
29
|
+
// Get isPremium using centralized logic (async mode)
|
|
30
|
+
const isPremium = await getIsPremium(isGuestFlag, userId, fetcher);
|
|
31
|
+
|
|
32
|
+
// Get tier info using centralized logic
|
|
33
|
+
return getUserTierInfo(isGuestFlag, userId, isPremium);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Check if user has premium access (async version with fetcher)
|
|
38
|
+
*
|
|
39
|
+
* This function combines getIsPremium and checkPremiumAccess logic.
|
|
40
|
+
* Guest users NEVER have premium access.
|
|
41
|
+
*
|
|
42
|
+
* @param isGuest - Whether user is a guest
|
|
43
|
+
* @param userId - User ID (null for guests)
|
|
44
|
+
* @param fetcher - Premium status fetcher (app-specific implementation)
|
|
45
|
+
* @returns Promise<boolean> - Whether user has premium access
|
|
46
|
+
*/
|
|
47
|
+
export async function checkPremiumAccessAsync(
|
|
48
|
+
isGuestFlag: boolean,
|
|
49
|
+
userId: string | null,
|
|
50
|
+
fetcher: PremiumStatusFetcher,
|
|
51
|
+
): Promise<boolean> {
|
|
52
|
+
// Import here to avoid circular dependency
|
|
53
|
+
const { checkPremiumAccess } = await import('./tierUtils');
|
|
54
|
+
|
|
55
|
+
// Get isPremium using centralized logic (async mode)
|
|
56
|
+
const isPremium = await getIsPremium(isGuestFlag, userId, fetcher);
|
|
57
|
+
|
|
58
|
+
// Apply premium access check logic
|
|
59
|
+
return checkPremiumAccess(isGuestFlag, userId, isPremium);
|
|
60
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Premium Status Utilities
|
|
3
|
+
*
|
|
4
|
+
* Core premium status determination logic
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { validateIsGuest, validateUserId, validateFetcher } from './validation';
|
|
8
|
+
import type { PremiumStatusFetcher } from './types';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Get isPremium value with centralized logic
|
|
12
|
+
*
|
|
13
|
+
* This function handles the complete logic for determining premium status:
|
|
14
|
+
* - Guest users NEVER have premium (returns false immediately)
|
|
15
|
+
* - Authenticated users: uses provided isPremium value OR fetches using fetcher
|
|
16
|
+
*
|
|
17
|
+
* This is the SINGLE SOURCE OF TRUTH for isPremium determination.
|
|
18
|
+
* All apps should use this function instead of directly calling their premium service.
|
|
19
|
+
*
|
|
20
|
+
* Two usage modes:
|
|
21
|
+
* 1. Sync mode: If you already have isPremium value, pass it directly
|
|
22
|
+
* 2. Async mode: If you need to fetch from database, pass a fetcher function
|
|
23
|
+
*
|
|
24
|
+
* @param isGuest - Whether user is a guest
|
|
25
|
+
* @param userId - User ID (null for guests)
|
|
26
|
+
* @param isPremiumOrFetcher - Either boolean (sync) or PremiumStatusFetcher (async)
|
|
27
|
+
* @returns boolean (sync) or Promise<boolean> (async) - Whether user has premium subscription
|
|
28
|
+
*/
|
|
29
|
+
// Sync overload: when isPremium value is already known
|
|
30
|
+
export function getIsPremium(
|
|
31
|
+
isGuestFlag: boolean,
|
|
32
|
+
userId: string | null,
|
|
33
|
+
isPremium: boolean,
|
|
34
|
+
): boolean;
|
|
35
|
+
|
|
36
|
+
// Async overload: when fetcher is provided
|
|
37
|
+
export function getIsPremium(
|
|
38
|
+
isGuestFlag: boolean,
|
|
39
|
+
userId: string | null,
|
|
40
|
+
fetcher: PremiumStatusFetcher,
|
|
41
|
+
): Promise<boolean>;
|
|
42
|
+
|
|
43
|
+
// Implementation
|
|
44
|
+
export function getIsPremium(
|
|
45
|
+
isGuestFlag: boolean,
|
|
46
|
+
userId: string | null,
|
|
47
|
+
isPremiumOrFetcher: boolean | PremiumStatusFetcher,
|
|
48
|
+
): boolean | Promise<boolean> {
|
|
49
|
+
validateIsGuest(isGuestFlag);
|
|
50
|
+
validateUserId(userId);
|
|
51
|
+
|
|
52
|
+
// Guest users NEVER have premium - this is centralized logic
|
|
53
|
+
if (isGuestFlag || userId === null) {
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Check if it's a boolean (sync mode) or fetcher (async mode)
|
|
58
|
+
if (typeof isPremiumOrFetcher === 'boolean') {
|
|
59
|
+
// Sync mode: return the provided isPremium value
|
|
60
|
+
return isPremiumOrFetcher;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Async mode: validate fetcher and fetch premium status
|
|
64
|
+
validateFetcher(isPremiumOrFetcher);
|
|
65
|
+
|
|
66
|
+
// Authenticated users: fetch premium status using app's fetcher
|
|
67
|
+
// Package handles the logic, app handles the database operation
|
|
68
|
+
return (async () => {
|
|
69
|
+
try {
|
|
70
|
+
return await isPremiumOrFetcher.isPremium(userId);
|
|
71
|
+
} catch (error) {
|
|
72
|
+
// If fetcher throws, assume not premium (fail-safe)
|
|
73
|
+
// Apps should handle errors in their fetcher implementation
|
|
74
|
+
throw new Error(
|
|
75
|
+
`Failed to fetch premium status: ${error instanceof Error ? error.message : String(error)}`
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
})();
|
|
79
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Premium Utilities
|
|
3
|
+
*
|
|
4
|
+
* Re-export of premium status utilities for backward compatibility
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
// Re-export for backward compatibility
|
|
8
|
+
export { getIsPremium } from './premiumStatusUtils';
|
|
9
|
+
export { getUserTierInfoAsync, checkPremiumAccessAsync } from './premiumAsyncUtils';
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* User Tier Core Utilities
|
|
3
|
+
*
|
|
4
|
+
* Core logic for determining user tier and premium status
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { validateIsGuest, validateUserId, validateIsPremium } from './validation';
|
|
8
|
+
import type { UserTierInfo } from './types';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Determine user tier from auth state and premium status
|
|
12
|
+
*
|
|
13
|
+
* This is the SINGLE SOURCE OF TRUTH for tier determination.
|
|
14
|
+
* All apps should use this function for consistent tier logic.
|
|
15
|
+
*
|
|
16
|
+
* @param isGuest - Whether user is a guest
|
|
17
|
+
* @param userId - User ID (null for guests)
|
|
18
|
+
* @param isPremium - Whether user has active premium subscription
|
|
19
|
+
* @returns User tier information
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* ```typescript
|
|
23
|
+
* const tierInfo = getUserTierInfo(false, 'user123', true);
|
|
24
|
+
* // Returns: { tier: 'premium', isPremium: true, isGuest: false, isAuthenticated: true, userId: 'user123' }
|
|
25
|
+
*
|
|
26
|
+
* const guestInfo = getUserTierInfo(true, null, false);
|
|
27
|
+
* // Returns: { tier: 'guest', isPremium: false, isGuest: true, isAuthenticated: false, userId: null }
|
|
28
|
+
* ```
|
|
29
|
+
*/
|
|
30
|
+
export function getUserTierInfo(
|
|
31
|
+
isGuestFlag: boolean,
|
|
32
|
+
userId: string | null,
|
|
33
|
+
isPremium: boolean,
|
|
34
|
+
): UserTierInfo {
|
|
35
|
+
validateIsGuest(isGuestFlag);
|
|
36
|
+
validateUserId(userId);
|
|
37
|
+
validateIsPremium(isPremium);
|
|
38
|
+
|
|
39
|
+
// Guest users are always freemium, never premium
|
|
40
|
+
if (isGuestFlag || userId === null) {
|
|
41
|
+
return {
|
|
42
|
+
tier: 'guest',
|
|
43
|
+
isPremium: false,
|
|
44
|
+
isGuest: true,
|
|
45
|
+
isAuthenticated: false,
|
|
46
|
+
userId: null,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Authenticated users: premium or freemium
|
|
51
|
+
return {
|
|
52
|
+
tier: isPremium ? 'premium' : 'freemium',
|
|
53
|
+
isPremium,
|
|
54
|
+
isGuest: false,
|
|
55
|
+
isAuthenticated: true,
|
|
56
|
+
userId,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Check if user has premium access (synchronous version)
|
|
62
|
+
*
|
|
63
|
+
* Guest users NEVER have premium access, regardless of isPremium value.
|
|
64
|
+
*
|
|
65
|
+
* @param isGuest - Whether user is a guest
|
|
66
|
+
* @param userId - User ID (null for guests)
|
|
67
|
+
* @param isPremium - Whether user has active premium subscription
|
|
68
|
+
* @returns Whether user has premium access
|
|
69
|
+
*
|
|
70
|
+
* @example
|
|
71
|
+
* ```typescript
|
|
72
|
+
* // Guest user - always false
|
|
73
|
+
* checkPremiumAccess(true, null, true); // false
|
|
74
|
+
*
|
|
75
|
+
* // Authenticated premium user
|
|
76
|
+
* checkPremiumAccess(false, 'user123', true); // true
|
|
77
|
+
*
|
|
78
|
+
* // Authenticated freemium user
|
|
79
|
+
* checkPremiumAccess(false, 'user123', false); // false
|
|
80
|
+
* ```
|
|
81
|
+
*/
|
|
82
|
+
export function checkPremiumAccess(
|
|
83
|
+
isGuestFlag: boolean,
|
|
84
|
+
userId: string | null,
|
|
85
|
+
isPremium: boolean,
|
|
86
|
+
): boolean {
|
|
87
|
+
validateIsGuest(isGuestFlag);
|
|
88
|
+
validateUserId(userId);
|
|
89
|
+
validateIsPremium(isPremium);
|
|
90
|
+
|
|
91
|
+
// Guest users never have premium access
|
|
92
|
+
if (isGuestFlag || userId === null) {
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return isPremium;
|
|
97
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* User Tier Types
|
|
3
|
+
*
|
|
4
|
+
* Type definitions for user tier system
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export type UserTier = 'guest' | 'freemium' | 'premium';
|
|
8
|
+
|
|
9
|
+
export interface UserTierInfo {
|
|
10
|
+
/** User tier classification */
|
|
11
|
+
tier: UserTier;
|
|
12
|
+
|
|
13
|
+
/** Whether user has premium access */
|
|
14
|
+
isPremium: boolean;
|
|
15
|
+
|
|
16
|
+
/** Whether user is a guest (not authenticated) */
|
|
17
|
+
isGuest: boolean;
|
|
18
|
+
|
|
19
|
+
/** Whether user is authenticated */
|
|
20
|
+
isAuthenticated: boolean;
|
|
21
|
+
|
|
22
|
+
/** User ID (null for guests) */
|
|
23
|
+
userId: string | null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Premium status fetcher interface
|
|
28
|
+
* Apps should implement this to provide premium status from their database
|
|
29
|
+
*/
|
|
30
|
+
export interface PremiumStatusFetcher {
|
|
31
|
+
/**
|
|
32
|
+
* Check if user has active premium subscription
|
|
33
|
+
* @param userId - User ID (never null, this is only called for authenticated users)
|
|
34
|
+
* @returns Promise<boolean> - Whether user has premium subscription
|
|
35
|
+
*/
|
|
36
|
+
isPremium(userId: string): Promise<boolean>;
|
|
37
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tier Comparison Utilities
|
|
3
|
+
*
|
|
4
|
+
* Utilities for comparing and checking user tiers
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { UserTier } from './types';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Compare two tiers to determine if first tier has higher or equal access than second
|
|
11
|
+
*
|
|
12
|
+
* Tier hierarchy: guest < freemium < premium
|
|
13
|
+
*
|
|
14
|
+
* @param tier1 - First tier to compare
|
|
15
|
+
* @param tier2 - Second tier to compare
|
|
16
|
+
* @returns Whether tier1 has higher or equal access than tier2
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* ```typescript
|
|
20
|
+
* hasTierAccess('premium', 'freemium'); // true
|
|
21
|
+
* hasTierAccess('freemium', 'premium'); // false
|
|
22
|
+
* hasTierAccess('premium', 'premium'); // true
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
export function hasTierAccess(tier1: UserTier, tier2: UserTier): boolean {
|
|
26
|
+
const tierLevels: Record<UserTier, number> = {
|
|
27
|
+
guest: 0,
|
|
28
|
+
freemium: 1,
|
|
29
|
+
premium: 2,
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
return tierLevels[tier1] >= tierLevels[tier2];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Check if tier is premium
|
|
37
|
+
*
|
|
38
|
+
* @param tier - Tier to check
|
|
39
|
+
* @returns Whether tier is premium
|
|
40
|
+
*
|
|
41
|
+
* @example
|
|
42
|
+
* ```typescript
|
|
43
|
+
* isTierPremium('premium'); // true
|
|
44
|
+
* isTierPremium('freemium'); // false
|
|
45
|
+
* ```
|
|
46
|
+
*/
|
|
47
|
+
export function isTierPremium(tier: UserTier): boolean {
|
|
48
|
+
return tier === 'premium';
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Check if tier is freemium
|
|
53
|
+
*
|
|
54
|
+
* @param tier - Tier to check
|
|
55
|
+
* @returns Whether tier is freemium
|
|
56
|
+
*
|
|
57
|
+
* @example
|
|
58
|
+
* ```typescript
|
|
59
|
+
* isTierFreemium('freemium'); // true
|
|
60
|
+
* isTierFreemium('premium'); // false
|
|
61
|
+
* ```
|
|
62
|
+
*/
|
|
63
|
+
export function isTierFreemium(tier: UserTier): boolean {
|
|
64
|
+
return tier === 'freemium';
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Check if tier is guest
|
|
69
|
+
*
|
|
70
|
+
* @param tier - Tier to check
|
|
71
|
+
* @returns Whether tier is guest
|
|
72
|
+
*
|
|
73
|
+
* @example
|
|
74
|
+
* ```typescript
|
|
75
|
+
* isTierGuest('guest'); // true
|
|
76
|
+
* isTierGuest('premium'); // false
|
|
77
|
+
* ```
|
|
78
|
+
*/
|
|
79
|
+
export function isTierGuest(tier: UserTier): boolean {
|
|
80
|
+
return tier === 'guest';
|
|
81
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* User Tier Validation Utilities
|
|
3
|
+
*
|
|
4
|
+
* Type guards and validation functions for user tier system
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { UserTier, UserTierInfo } from './types';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Type guard to check if a value is a valid UserTier
|
|
11
|
+
*
|
|
12
|
+
* @param value - Value to check
|
|
13
|
+
* @returns Whether value is a valid UserTier
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* ```typescript
|
|
17
|
+
* if (isValidUserTier(someValue)) {
|
|
18
|
+
* // TypeScript knows someValue is UserTier
|
|
19
|
+
* }
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
export function isValidUserTier(value: unknown): value is UserTier {
|
|
23
|
+
return value === 'guest' || value === 'freemium' || value === 'premium';
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Type guard to check if an object is a valid UserTierInfo
|
|
28
|
+
*
|
|
29
|
+
* @param value - Value to check
|
|
30
|
+
* @returns Whether value is a valid UserTierInfo
|
|
31
|
+
*
|
|
32
|
+
* @example
|
|
33
|
+
* ```typescript
|
|
34
|
+
* if (isUserTierInfo(someValue)) {
|
|
35
|
+
* // TypeScript knows someValue is UserTierInfo
|
|
36
|
+
* }
|
|
37
|
+
* ```
|
|
38
|
+
*/
|
|
39
|
+
export function isUserTierInfo(value: unknown): value is UserTierInfo {
|
|
40
|
+
if (typeof value !== 'object' || value === null) {
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const obj = value as Record<string, unknown>;
|
|
45
|
+
|
|
46
|
+
return (
|
|
47
|
+
isValidUserTier(obj.tier) &&
|
|
48
|
+
typeof obj.isPremium === 'boolean' &&
|
|
49
|
+
typeof obj.isGuest === 'boolean' &&
|
|
50
|
+
typeof obj.isAuthenticated === 'boolean' &&
|
|
51
|
+
(obj.userId === null || typeof obj.userId === 'string')
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Validate userId parameter
|
|
57
|
+
*
|
|
58
|
+
* @param userId - User ID to validate
|
|
59
|
+
* @throws {TypeError} If userId is invalid
|
|
60
|
+
*/
|
|
61
|
+
export function validateUserId(userId: string | null): void {
|
|
62
|
+
if (userId !== null && typeof userId !== 'string') {
|
|
63
|
+
throw new TypeError(
|
|
64
|
+
`Invalid userId: expected string or null, got ${typeof userId}`
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (userId !== null && userId.trim() === '') {
|
|
69
|
+
throw new TypeError('Invalid userId: cannot be empty string');
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Validate isGuest parameter
|
|
75
|
+
*
|
|
76
|
+
* @param isGuest - isGuest flag to validate
|
|
77
|
+
* @throws {TypeError} If isGuest is invalid
|
|
78
|
+
*/
|
|
79
|
+
export function validateIsGuest(isGuest: boolean): void {
|
|
80
|
+
if (typeof isGuest !== 'boolean') {
|
|
81
|
+
throw new TypeError(
|
|
82
|
+
`Invalid isGuest: expected boolean, got ${typeof isGuest}`
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Validate isPremium parameter
|
|
89
|
+
*
|
|
90
|
+
* @param isPremium - isPremium flag to validate
|
|
91
|
+
* @throws {TypeError} If isPremium is invalid
|
|
92
|
+
*/
|
|
93
|
+
export function validateIsPremium(isPremium: boolean): void {
|
|
94
|
+
if (typeof isPremium !== 'boolean') {
|
|
95
|
+
throw new TypeError(
|
|
96
|
+
`Invalid isPremium: expected boolean, got ${typeof isPremium}`
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Validate PremiumStatusFetcher
|
|
103
|
+
*
|
|
104
|
+
* @param fetcher - Fetcher to validate
|
|
105
|
+
* @throws {TypeError} If fetcher is invalid
|
|
106
|
+
*/
|
|
107
|
+
export function validateFetcher(fetcher: import('./types').PremiumStatusFetcher): void {
|
|
108
|
+
if (typeof fetcher !== 'object' || fetcher === null) {
|
|
109
|
+
throw new TypeError(
|
|
110
|
+
`Invalid fetcher: expected object, got ${typeof fetcher}`
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (typeof fetcher.isPremium !== 'function') {
|
|
115
|
+
throw new TypeError(
|
|
116
|
+
'Invalid fetcher: isPremium must be a function'
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
}
|