@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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umituz/react-native-subscription",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.1",
|
|
4
4
|
"description": "Subscription management system for React Native apps - Database-first approach with secure validation",
|
|
5
5
|
"main": "./src/index.ts",
|
|
6
6
|
"types": "./src/index.ts",
|
package/src/index.ts
CHANGED
|
@@ -60,38 +60,69 @@ export {
|
|
|
60
60
|
export { useSubscription } from './presentation/hooks/useSubscription';
|
|
61
61
|
export type { UseSubscriptionResult } from './presentation/hooks/useSubscription';
|
|
62
62
|
|
|
63
|
+
export {
|
|
64
|
+
usePremiumGate,
|
|
65
|
+
type UsePremiumGateParams,
|
|
66
|
+
type UsePremiumGateResult,
|
|
67
|
+
} from './presentation/hooks/usePremiumGate';
|
|
68
|
+
|
|
69
|
+
export {
|
|
70
|
+
useUserTier,
|
|
71
|
+
type UseUserTierParams,
|
|
72
|
+
type UseUserTierResult,
|
|
73
|
+
} from './presentation/hooks/useUserTier';
|
|
74
|
+
|
|
75
|
+
export {
|
|
76
|
+
useUserTierWithRepository,
|
|
77
|
+
type UseUserTierWithRepositoryParams,
|
|
78
|
+
type UseUserTierWithRepositoryResult,
|
|
79
|
+
type AuthProvider,
|
|
80
|
+
} from './presentation/hooks/useUserTierWithRepository';
|
|
81
|
+
|
|
63
82
|
// =============================================================================
|
|
64
83
|
// UTILS
|
|
65
84
|
// =============================================================================
|
|
66
85
|
|
|
67
86
|
// Date utilities
|
|
68
|
-
export {
|
|
69
|
-
formatExpirationDate,
|
|
70
|
-
calculateExpirationDate,
|
|
71
|
-
} from './utils/dateUtils';
|
|
72
|
-
|
|
73
87
|
export {
|
|
74
88
|
isSubscriptionExpired,
|
|
75
89
|
getDaysUntilExpiration,
|
|
76
90
|
} from './utils/dateValidationUtils';
|
|
77
91
|
|
|
92
|
+
|
|
93
|
+
// =============================================================================
|
|
94
|
+
// USER TIER - Types & Utilities
|
|
95
|
+
// =============================================================================
|
|
96
|
+
|
|
97
|
+
export type {
|
|
98
|
+
UserTier,
|
|
99
|
+
UserTierInfo,
|
|
100
|
+
PremiumStatusFetcher,
|
|
101
|
+
} from './utils/types';
|
|
102
|
+
|
|
78
103
|
export {
|
|
79
|
-
|
|
80
|
-
|
|
104
|
+
getUserTierInfo,
|
|
105
|
+
checkPremiumAccess,
|
|
106
|
+
} from './utils/tierUtils';
|
|
81
107
|
|
|
82
|
-
|
|
83
|
-
|
|
108
|
+
export {
|
|
109
|
+
hasTierAccess,
|
|
110
|
+
isTierPremium,
|
|
111
|
+
isTierFreemium,
|
|
112
|
+
isTierGuest,
|
|
113
|
+
} from './utils/userTierUtils';
|
|
84
114
|
|
|
85
|
-
|
|
86
|
-
|
|
115
|
+
export {
|
|
116
|
+
isAuthenticated,
|
|
117
|
+
isGuest,
|
|
118
|
+
} from './utils/authUtils';
|
|
87
119
|
|
|
88
120
|
export {
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
} from './utils/subscriptionConstants';
|
|
121
|
+
isValidUserTier,
|
|
122
|
+
isUserTierInfo,
|
|
123
|
+
validateUserId,
|
|
124
|
+
validateIsGuest,
|
|
125
|
+
validateIsPremium,
|
|
126
|
+
validateFetcher,
|
|
127
|
+
} from './utils/validation';
|
|
97
128
|
|
|
@@ -0,0 +1,79 @@
|
|
|
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
|
+
});
|
|
@@ -0,0 +1,70 @@
|
|
|
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
|
+
});
|
|
@@ -0,0 +1,167 @@
|
|
|
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
|
+
});
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* usePremiumGate Hook
|
|
3
|
+
*
|
|
4
|
+
* Feature gating hook for premium-only features
|
|
5
|
+
* Provides a simple way to gate features behind premium subscription
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```typescript
|
|
9
|
+
* const { requirePremium, isPremium } = usePremiumGate({
|
|
10
|
+
* isPremium: subscriptionStore.isPremium,
|
|
11
|
+
* onPremiumRequired: () => navigation.navigate('Paywall'),
|
|
12
|
+
* });
|
|
13
|
+
*
|
|
14
|
+
* const handleGenerate = () => {
|
|
15
|
+
* requirePremium(() => {
|
|
16
|
+
* // This only runs if user is premium
|
|
17
|
+
* generateContent();
|
|
18
|
+
* });
|
|
19
|
+
* };
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import { useCallback, useMemo } from "react";
|
|
24
|
+
|
|
25
|
+
export interface UsePremiumGateParams {
|
|
26
|
+
/** Whether user has premium access */
|
|
27
|
+
isPremium: boolean;
|
|
28
|
+
/** Callback when premium is required but user is not premium */
|
|
29
|
+
onPremiumRequired: () => void;
|
|
30
|
+
/** Optional: Whether user is authenticated */
|
|
31
|
+
isAuthenticated?: boolean;
|
|
32
|
+
/** Optional: Callback when auth is required but user is not authenticated */
|
|
33
|
+
onAuthRequired?: () => void;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface UsePremiumGateResult {
|
|
37
|
+
/** Whether user has premium access */
|
|
38
|
+
isPremium: boolean;
|
|
39
|
+
/** Whether user is authenticated */
|
|
40
|
+
isAuthenticated: boolean;
|
|
41
|
+
/** Gate a feature behind premium - executes action if premium, else calls onPremiumRequired */
|
|
42
|
+
requirePremium: (action: () => void) => void;
|
|
43
|
+
/** Gate a feature behind auth - executes action if authenticated, else calls onAuthRequired */
|
|
44
|
+
requireAuth: (action: () => void) => void;
|
|
45
|
+
/** Gate a feature behind both auth and premium */
|
|
46
|
+
requirePremiumWithAuth: (action: () => void) => void;
|
|
47
|
+
/** Check if feature is accessible (premium check only) */
|
|
48
|
+
canAccess: boolean;
|
|
49
|
+
/** Check if feature is accessible (auth + premium) */
|
|
50
|
+
canAccessWithAuth: boolean;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function usePremiumGate(
|
|
54
|
+
params: UsePremiumGateParams
|
|
55
|
+
): UsePremiumGateResult {
|
|
56
|
+
const {
|
|
57
|
+
isPremium,
|
|
58
|
+
onPremiumRequired,
|
|
59
|
+
isAuthenticated = true,
|
|
60
|
+
onAuthRequired,
|
|
61
|
+
} = params;
|
|
62
|
+
|
|
63
|
+
const requirePremium = useCallback(
|
|
64
|
+
(action: () => void) => {
|
|
65
|
+
if (isPremium) {
|
|
66
|
+
action();
|
|
67
|
+
} else {
|
|
68
|
+
onPremiumRequired();
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
[isPremium, onPremiumRequired]
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
const requireAuth = useCallback(
|
|
75
|
+
(action: () => void) => {
|
|
76
|
+
if (isAuthenticated) {
|
|
77
|
+
action();
|
|
78
|
+
} else {
|
|
79
|
+
onAuthRequired?.();
|
|
80
|
+
}
|
|
81
|
+
},
|
|
82
|
+
[isAuthenticated, onAuthRequired]
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
const requirePremiumWithAuth = useCallback(
|
|
86
|
+
(action: () => void) => {
|
|
87
|
+
if (!isAuthenticated) {
|
|
88
|
+
onAuthRequired?.();
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
if (!isPremium) {
|
|
92
|
+
onPremiumRequired();
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
action();
|
|
96
|
+
},
|
|
97
|
+
[isAuthenticated, isPremium, onAuthRequired, onPremiumRequired]
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
const canAccess = useMemo(() => isPremium, [isPremium]);
|
|
101
|
+
|
|
102
|
+
const canAccessWithAuth = useMemo(
|
|
103
|
+
() => isAuthenticated && isPremium,
|
|
104
|
+
[isAuthenticated, isPremium]
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
return {
|
|
108
|
+
isPremium,
|
|
109
|
+
isAuthenticated,
|
|
110
|
+
requirePremium,
|
|
111
|
+
requireAuth,
|
|
112
|
+
requirePremiumWithAuth,
|
|
113
|
+
canAccess,
|
|
114
|
+
canAccessWithAuth,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useUserTier Hook
|
|
3
|
+
*
|
|
4
|
+
* Centralized hook for determining user tier (Guest, Freemium, Premium)
|
|
5
|
+
* Single source of truth for all premium/freemium/guest checks
|
|
6
|
+
*
|
|
7
|
+
* This hook only handles LOGICAL tier determination.
|
|
8
|
+
* Database operations should be handled by the app via PremiumStatusFetcher.
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```typescript
|
|
12
|
+
* const { tier, isPremium, isGuest } = useUserTier({
|
|
13
|
+
* isGuest: false,
|
|
14
|
+
* userId: 'user123',
|
|
15
|
+
* isPremium: true, // App should fetch this from database
|
|
16
|
+
* });
|
|
17
|
+
*
|
|
18
|
+
* // Simple, clean checks
|
|
19
|
+
* if (tier === "guest") {
|
|
20
|
+
* // Show guest upgrade card
|
|
21
|
+
* } else if (tier === "freemium") {
|
|
22
|
+
* // Show freemium limits
|
|
23
|
+
* } else {
|
|
24
|
+
* // Premium features
|
|
25
|
+
* }
|
|
26
|
+
* ```
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
import { useMemo } from 'react';
|
|
30
|
+
import { getUserTierInfo } from '../../utils/tierUtils';
|
|
31
|
+
import type { UserTierInfo } from '../../utils/types';
|
|
32
|
+
|
|
33
|
+
export interface UseUserTierParams {
|
|
34
|
+
/** Whether user is a guest */
|
|
35
|
+
isGuest: boolean;
|
|
36
|
+
/** User ID (null for guests) */
|
|
37
|
+
userId: string | null;
|
|
38
|
+
/** Whether user has active premium subscription (app should fetch from database) */
|
|
39
|
+
isPremium: boolean;
|
|
40
|
+
/** Optional: Loading state from app */
|
|
41
|
+
isLoading?: boolean;
|
|
42
|
+
/** Optional: Error state from app */
|
|
43
|
+
error?: string | null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface UseUserTierResult extends UserTierInfo {
|
|
47
|
+
/** Whether premium status is currently loading */
|
|
48
|
+
isLoading: boolean;
|
|
49
|
+
/** Premium status error (if any) */
|
|
50
|
+
error: string | null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Hook to get user tier information
|
|
55
|
+
* Combines auth state and premium status into single source of truth
|
|
56
|
+
*
|
|
57
|
+
* All premium/freemium/guest checks are centralized here:
|
|
58
|
+
* - Guest: isGuest || !userId → always freemium, never premium
|
|
59
|
+
* - Freemium: authenticated but !isPremium
|
|
60
|
+
* - Premium: authenticated && isPremium
|
|
61
|
+
*
|
|
62
|
+
* Note: This hook only handles LOGICAL tier determination.
|
|
63
|
+
* Database operations (fetching premium status) should be handled by the app.
|
|
64
|
+
*/
|
|
65
|
+
export function useUserTier(params: UseUserTierParams): UseUserTierResult {
|
|
66
|
+
const { isGuest, userId, isPremium, isLoading = false, error = null } = params;
|
|
67
|
+
|
|
68
|
+
// Calculate tier info using centralized logic
|
|
69
|
+
const tierInfo = useMemo(() => {
|
|
70
|
+
return getUserTierInfo(isGuest, userId, isPremium);
|
|
71
|
+
}, [isGuest, userId, isPremium]);
|
|
72
|
+
|
|
73
|
+
return {
|
|
74
|
+
...tierInfo,
|
|
75
|
+
isLoading,
|
|
76
|
+
error,
|
|
77
|
+
};
|
|
78
|
+
}
|