Skip to content

Commit 638632d

Browse files
committed
add userEntitlements endpoints
1 parent dd4d752 commit 638632d

File tree

4 files changed

+130
-0
lines changed

4 files changed

+130
-0
lines changed
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
import { ref, computed } from 'vue';
2+
import { getUserStatus } from '../services/authAPI';
3+
import type { Entitlement, UserEntitlements } from '../services/authAPI';
4+
5+
export interface TrialStatus {
6+
isActive: boolean;
7+
isPaid: boolean;
8+
daysRemaining: number | null;
9+
expiresDate: string | null;
10+
}
11+
12+
/**
13+
* Composable for managing user entitlements and trial status
14+
*
15+
* @param courseId - The course identifier to check entitlements for
16+
* @returns Object with entitlements data and helper methods
17+
*
18+
* @example
19+
* ```typescript
20+
* const { trialStatus, hasPremiumAccess, fetchEntitlements, loading } = useEntitlements('letterspractice-basic');
21+
*
22+
* onMounted(async () => {
23+
* await fetchEntitlements();
24+
* console.log('Days remaining:', trialStatus.value.daysRemaining);
25+
* });
26+
* ```
27+
*/
28+
export function useEntitlements(courseId: string) {
29+
const entitlements = ref<UserEntitlements>({});
30+
const loading = ref(false);
31+
const error = ref<string | null>(null);
32+
33+
/**
34+
* Get trial status for the specified course
35+
*/
36+
const trialStatus = computed<TrialStatus>(() => {
37+
const entitlement: Entitlement | undefined = entitlements.value[courseId];
38+
39+
if (!entitlement) {
40+
return {
41+
isActive: false,
42+
isPaid: false,
43+
daysRemaining: null,
44+
expiresDate: null,
45+
};
46+
}
47+
48+
if (entitlement.status === 'paid') {
49+
return {
50+
isActive: true,
51+
isPaid: true,
52+
daysRemaining: null,
53+
expiresDate: null,
54+
};
55+
}
56+
57+
// Trial status
58+
const expiresDate = entitlement.expires;
59+
if (!expiresDate) {
60+
return {
61+
isActive: true,
62+
isPaid: false,
63+
daysRemaining: null,
64+
expiresDate: null,
65+
};
66+
}
67+
68+
const expires = new Date(expiresDate);
69+
const now = new Date();
70+
const daysLeft = Math.ceil((expires.getTime() - now.getTime()) / (1000 * 60 * 60 * 24));
71+
72+
return {
73+
isActive: daysLeft > 0,
74+
isPaid: false,
75+
daysRemaining: Math.max(0, daysLeft),
76+
expiresDate,
77+
};
78+
});
79+
80+
/**
81+
* Whether the user has premium (paid) access to the course
82+
*/
83+
const hasPremiumAccess = computed<boolean>(() => {
84+
return trialStatus.value.isPaid;
85+
});
86+
87+
/**
88+
* Fetch entitlements from the backend
89+
*/
90+
async function fetchEntitlements(): Promise<void> {
91+
loading.value = true;
92+
error.value = null;
93+
94+
try {
95+
const result = await getUserStatus();
96+
if (result.ok) {
97+
entitlements.value = result.entitlements || {};
98+
} else {
99+
error.value = result.error || 'Failed to fetch entitlements';
100+
console.error('[useEntitlements] Error:', error.value);
101+
}
102+
} catch (e) {
103+
error.value = e instanceof Error ? e.message : 'Unknown error';
104+
console.error('[useEntitlements] Exception:', e);
105+
} finally {
106+
loading.value = false;
107+
}
108+
}
109+
110+
return {
111+
entitlements,
112+
trialStatus,
113+
hasPremiumAccess,
114+
loading,
115+
error,
116+
fetchEntitlements,
117+
};
118+
}

packages/common-ui/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ export { default as CardHistoryViewer } from './components/CardHistoryViewer.vue
2727
// Composables
2828
export * from './composables/CompositionViewable';
2929
export * from './composables/Displayable';
30+
export * from './composables/useEntitlements';
3031

3132
/*
3233
Study Session Components

packages/common-ui/src/services/authAPI.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,10 +52,20 @@ export interface VerifyEmailResponse extends AuthResponse {
5252
username?: string;
5353
}
5454

55+
export interface Entitlement {
56+
status: 'trial' | 'paid';
57+
registrationDate: string;
58+
purchaseDate?: string;
59+
expires?: string;
60+
}
61+
62+
export type UserEntitlements = Record<string, Entitlement>;
63+
5564
export interface UserStatusResponse extends AuthResponse {
5665
username?: string;
5766
status?: 'pending_verification' | 'verified' | 'suspended';
5867
email?: string | null;
68+
entitlements?: UserEntitlements;
5969
}
6070

6171
/**

packages/express/src/routes/auth.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,7 @@ router.get('/status', (req: Request, res: Response) => {
175175
username: userDoc.name,
176176
status: userDoc.status || 'pending_verification', // Default to pending if not explicitly set
177177
email: userDoc.email || null,
178+
entitlements: userDoc.entitlements || {},
178179
});
179180
} catch (error) {
180181
logger.error('Error fetching user status:', error);

0 commit comments

Comments
 (0)