Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions src/components/mobile/MobileCourseViewer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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) {
Expand All @@ -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;
Expand Down
43 changes: 43 additions & 0 deletions src/services/certificateService.ts
Original file line number Diff line number Diff line change
@@ -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<Certificate | null> {
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;
Loading