diff --git a/src/components/mobile/MobileCourseViewer.tsx b/src/components/mobile/MobileCourseViewer.tsx index b1f0415..05ea179 100644 --- a/src/components/mobile/MobileCourseViewer.tsx +++ b/src/components/mobile/MobileCourseViewer.tsx @@ -21,6 +21,7 @@ import { useCourseProgress, useDynamicFontSize } from '../../hooks'; import { useAnalytics } from '../../hooks/useAnalytics'; import { useInAppReview, useReviewMetrics } from '../../hooks/useInAppReview'; import { usePrefetchImages } from '../../hooks/usePrefetchImages'; +import certificateService from '../../services/certificateService'; import { ReviewTrigger } from '../../services/inAppReview'; import { useReviewStore } from '../../store/reviewStore'; import { Course, Lesson, Note } from '../../types/course'; @@ -246,6 +247,9 @@ const MobileCourseViewer = ({ // eslint-disable-next-line react-hooks/exhaustive-deps }, [course.id]); + // #833: ensure a certificate is only requested once per completion. + const certificateRequestedRef = useRef(false); + // Track course completion useEffect(() => { if (progress) { @@ -257,6 +261,12 @@ const MobileCourseViewer = ({ progress: overallProgress, }); + // #833: generate the certificate of completion for the finished course. + if (!certificateRequestedRef.current) { + certificateRequestedRef.current = true; + void certificateService.generateCertificate(course.id, course.title); + } + // Track and request review trackCourseComplete(); const coursesCompleted = useReviewStore.getState().coursesCompleted; diff --git a/src/services/certificateService.ts b/src/services/certificateService.ts new file mode 100644 index 0000000..e32bdcc --- /dev/null +++ b/src/services/certificateService.ts @@ -0,0 +1,43 @@ +import { apiService } from './api'; +import { logger } from '../utils/logger'; + +export interface Certificate { + id: string; + courseId: string; + courseTitle: string; + /** URL of the generated certificate (PDF or image). */ + url: string; + issuedAt: string; +} + +/** + * #833: request a certificate of completion from the backend once a course is + * completed. Best-effort — failures are logged and swallowed so certificate + * generation never blocks the course-completion flow. + */ +export const certificateService = { + async generateCertificate( + courseId: string, + courseTitle?: string + ): Promise { + try { + const response: unknown = await apiService.post('/api/certificates/generate', { + courseId, + courseTitle, + }); + // apiService.post returns the raw axios response; unwrap .data when present. + const certificate = ( + response && typeof response === 'object' && 'data' in response + ? (response as { data: unknown }).data + : response + ) as Certificate; + logger.info('Certificate generated', { courseId }); + return certificate; + } catch (error) { + logger.warn('Certificate generation failed', { courseId, error: String(error) }); + return null; + } + }, +}; + +export default certificateService;