Skip to content

Commit 9ba2e06

Browse files
authored
add userEntitlements endpoints (#938)
2 parents a26b4f2 + a1125c1 commit 9ba2e06

File tree

17 files changed

+157
-12
lines changed

17 files changed

+157
-12
lines changed

packages/cli/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"publishConfig": {
44
"access": "public"
55
},
6-
"version": "0.1.13-10",
6+
"version": "0.1.13-11",
77
"type": "module",
88
"description": "CLI scaffolding tool for vue-skuilder projects",
99
"bin": {

packages/client/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"publishConfig": {
44
"access": "public"
55
},
6-
"version": "0.1.13-10",
6+
"version": "0.1.13-11",
77
"license": "MIT",
88
"main": "dist/index.js",
99
"module": "dist/index.esm.js",

packages/common-ui/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"publishConfig": {
44
"access": "public"
55
},
6-
"version": "0.1.13-10",
6+
"version": "0.1.13-11",
77
"main": "./dist/common-ui.umd.js",
88
"module": "./dist/common-ui.es.js",
99
"types": "./dist/index.d.ts",
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/common/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"publishConfig": {
44
"access": "public"
55
},
6-
"version": "0.1.13-10",
6+
"version": "0.1.13-11",
77
"type": "module",
88
"main": "dist/index.js",
99
"module": "dist/index.mjs",

packages/courseware/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"publishConfig": {
44
"access": "public"
55
},
6-
"version": "0.1.13-10",
6+
"version": "0.1.13-11",
77
"type": "module",
88
"main": "./dist/index.cjs.js",
99
"module": "./dist/index.mjs",

packages/db/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"publishConfig": {
44
"access": "public"
55
},
6-
"version": "0.1.13-10",
6+
"version": "0.1.13-11",
77
"description": "Database layer for vue-skuilder",
88
"main": "dist/index.js",
99
"module": "dist/index.mjs",

packages/db/src/core/types/user.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,26 @@
11
import { CourseElo } from '@vue-skuilder/common';
22
import { Moment } from 'moment';
33

4+
export interface SessionTrackingData {
5+
peekSessionCount: number;
6+
studySessionCount: number;
7+
sessionCount: number; // total
8+
firstSessionDate: string;
9+
lastSessionDate: string;
10+
signupPrompted: boolean;
11+
promptDismissalCount: number;
12+
studyModeAcknowledged: boolean;
13+
}
14+
415
export interface UserConfig {
516
darkMode: boolean;
617
likesConfetti: boolean;
718
sessionTimeLimit: number; // Session time limit in minutes
819
email?: string; // Optional email for verification flows (added for enhanced auth)
20+
21+
// Session tracking for trial enforcement (per-course)
22+
// Key is courseId (e.g., 'letterspractice-basic')
23+
sessionTracking?: Record<string, SessionTrackingData>;
924
}
1025

1126
export interface ActivityRecord {

0 commit comments

Comments
 (0)