+ ,
+ }
+ }
+ />
+
+`;
+
+exports[`EntitlementBanner snapshot: no sessions available 1`] = `
+
+
+ test-support-email
+ ,
+ }
+ }
+ />
+
+`;
diff --git a/src/containers/CourseCard/components/Banners/messages.js b/src/containers/CourseCard/components/Banners/messages.js
index d6889c4..052d800 100644
--- a/src/containers/CourseCard/components/Banners/messages.js
+++ b/src/containers/CourseCard/components/Banners/messages.js
@@ -29,7 +29,67 @@ export const messages = StrictDict({
certRestricted: {
id: 'learner-dash.courseCard.banners.certificateRestricted',
description: 'Restricted certificate warning message',
- defaultMessage: 'Your Certificate of Achievement is being held pending confirmation that the issuance of your Certificate is in compliance with strict U.S. embargoes on Iran, Cuba, Syria, and Sudan. If you think our system has mistakenly identified you as being connected with one of those countries, please let us know by contacting ',
+ defaultMessage: 'Your Certificate of Achievement is being held pending confirmation that the issuance of your Certificate is in compliance with strict U.S. embargoes on Iran, Cuba, Syria, and Sudan. If you think our system has mistakenly identified you as being connected with one of those countries, please let us know by contacting {supportEmail}.',
+ },
+ certRefundContactBilling: {
+ id: 'learner-dash.courseCard.banners.certificateRefundContactBilling',
+ description: 'Message to learners to contact billing for certificate refunds',
+ defaultMessage: 'If you would like a refund on your Certificate of Achievement, please contact our billing address {billingEmail}',
+ },
+ passingGrade: {
+ id: 'learner-dash.courseCard.banners.passingGrade',
+ description: 'Message to learners with minimum passing grade for the course',
+ defaultMessage: 'Grade required to pass the course: {minPassingGrade}',
+ },
+ notEligibleForCert: {
+ id: 'learner-dash.courseCard.banners.notEligibleForCert',
+ description: 'Certificate inelligibility message',
+ defaultMessage: 'You are not eligible for a certificate.',
+ },
+ viewGrades: {
+ id: 'learner-dash.courseCard.banners.viewGrades',
+ description: 'Gradses link text',
+ defaultMessage: 'View grades.',
+ },
+ certReady: {
+ id: 'learner-dash.courseCard.banners.certReady',
+ description: 'Certificate ready message',
+ defaultMessage: 'Congratulations. Your certificate is ready.',
+ },
+ viewCertificate: {
+ id: 'learner-dash.courseCard.banners.viewCertificate',
+ description: 'Certificate link text',
+ defaultMessage: 'View Certificate.',
+ },
+ certMinGrade: {
+ id: 'learner-dash.courseCard.banners.certMinGrade',
+ description: 'Passing grade requirement message',
+ defaultMessage: 'Grade required for a certificate: {minPassingGrade}',
+ },
+ downloadCertificate: {
+ id: 'learner-dash.courseCard.banners.downloadCertificate',
+ description: 'Certificate download link text',
+ defaultMessage: 'Download Certificate.',
+ },
+ gradeAndCertReadyAfter: {
+ id: 'learner-dash.courseCard.banners.gradseAndCertReadyAfter',
+ description: 'Grade and certificate availability date message',
+ defaultMessage: 'Your grade and certificate will be ready after {availableDate}.',
+ },
+ entitlementsUnavailable: {
+ id: 'learner-dash.courseCard.banners.entitlementsUnavailable',
+ description: 'Entitlements course message when no sessions are available',
+ defaultMessage: 'There are no sessions available at the moment. The course team will create new sessions soon. If no sessions appear, please contact {emailLink} for information.',
+ },
+ entitlementsExpiringSoon: {
+ id: 'learner-dash.courseCard.banners.entitlementsExpiringSoon',
+ description: 'Entitlements course message when the entitlement is expiring soon.',
+ defaultMessage: 'You must {selectSessionButton} by {changeDeadline} to access the course.',
+ },
+ selectSession: {
+ id: 'learner-dash.courseCard.banners.selectSession',
+ description: 'Entitlements session selection link text',
+ defaultMessage: 'select a session',
},
});
diff --git a/src/containers/CourseCard/hooks.test.js b/src/containers/CourseCard/hooks.test.js
index a537bac..1233d91 100644
--- a/src/containers/CourseCard/hooks.test.js
+++ b/src/containers/CourseCard/hooks.test.js
@@ -127,7 +127,7 @@ describe('CourseCard hooks', () => {
describe('if verified and ended', () => {
it('returns course ended message with course end date', () => {
- runHook({ courseRun: { isFinished: true } });
+ runHook({ courseRun: { isArchived: true } });
expect(out).toEqual(formatMessage(
messages.courseEnded,
{ endDate: formatDate(courseRunData.endDate) },
diff --git a/src/data/redux/app/hooks.js b/src/data/redux/app/hooks.js
deleted file mode 100644
index 09c9de6..0000000
--- a/src/data/redux/app/hooks.js
+++ /dev/null
@@ -1,23 +0,0 @@
-import { useSelector } from 'react-redux';
-
-import selectors from './selectors';
-
-const { courseCard } = selectors;
-
-export const useEmailConfirmationData = () => useSelector(selectors.emailConfirmation);
-export const useEnterpriseDashboardData = () => useSelector(selectors.enterpriseDashboards);
-export const usePlatformSettingsData = () => useSelector(selectors.platformSettings);
-
-// eslint-disable-next-line
-export const useCourseCardData = (selector) => (courseNumber) => useSelector(
- (state) => selector(selectors.courseData(state)[courseNumber]),
-);
-
-export const useCardCertificateData = useCourseCardData(courseCard.certificates);
-export const useCardCourseData = useCourseCardData(courseCard.course);
-export const useCardCourseRunData = useCourseCardData(courseCard.courseRun);
-export const useCardEnrollmentData = useCourseCardData(courseCard.enrollment);
-export const useCardEntitlementsData = useCourseCardData(courseCard.entitlements);
-export const useCardGradesData = useCourseCardData(courseCard.grades);
-export const useCardProviderData = useCourseCardData(courseCard.provider);
-export const useCardRelatedProgramsData = useCourseCardData(courseCard.relatedPrograms);
diff --git a/src/data/redux/app/reducer.js b/src/data/redux/app/reducer.js
index dec92e8..0ef8192 100644
--- a/src/data/redux/app/reducer.js
+++ b/src/data/redux/app/reducer.js
@@ -9,6 +9,7 @@ const initialState = {
enterpriseDashboards: {},
platformSettings: {},
suggestedCourses: {},
+ filterState: {},
};
// eslint-disable-next-line no-unused-vars
@@ -16,18 +17,23 @@ const app = createSlice({
name: 'app',
initialState,
reducers: {
- loadEnrollments: (state, { payload }) => ({
+ loadCourses: (state, { payload: { enrollments, entitlements } }) => ({
...state,
- enrollments: payload.map(curr => curr.courseRun.courseNumber),
- courseData: payload.reduce(
- (obj, curr) => ({
- ...obj,
- [curr.courseRun.courseNumber]: curr,
- }),
- {},
- ),
+ enrollments: [
+ ...enrollments.map(curr => curr.courseRun.courseNumber),
+ ...entitlements.map(curr => curr.courseRun.courseNumber),
+ ],
+ courseData: {
+ ...entitlements.reduce(
+ (obj, curr) => ({ ...obj, [curr.courseRun.courseNumber]: curr }),
+ {},
+ ),
+ ...enrollments.reduce(
+ (obj, curr) => ({ ...obj, [curr.courseRun.courseNumber]: curr }),
+ {},
+ ),
+ },
}),
- loadEntitlements: (state, { payload }) => ({ ...state, entitlements: payload }),
loadGlobalData: (state, { payload }) => ({
...state,
emailConfirmation: payload.emailConfirmation,
diff --git a/src/data/redux/app/reducer.test.js b/src/data/redux/app/reducer.test.js
index 549ad75..f46b2fb 100644
--- a/src/data/redux/app/reducer.test.js
+++ b/src/data/redux/app/reducer.test.js
@@ -20,45 +20,66 @@ describe('app reducer', () => {
});
};
describe('action handlers', () => {
- test('loadEntitlements loads entitlements from payload', () => {
- testAction(
- actions.loadEntitlements(testValue),
- { entitlements: testValue },
- );
- });
- describe('loadEnrollments', () => {
- const enrollments = [
+ describe('loadCourses', () => {
+ const courseIds = [
'course-1',
'course-2',
'course-3',
];
- const courseData = {
- [enrollments[0]]: {
- courseRun: { courseNumber: enrollments[0] },
+ const entitlementIds = [
+ 'entitlement-course-1',
+ 'entitlement-course-2',
+ ];
+ const enrollmentData = [
+ {
+ courseRun: { courseNumber: courseIds[0] },
course: 1,
some: 'data',
},
- [enrollments[1]]: {
- courseRun: { courseNumber: enrollments[1] },
+ {
+ courseRun: { courseNumber: courseIds[1] },
course: 2,
some: 'other data',
},
- [enrollments[2]]: {
- courseRun: { courseNumber: enrollments[2] },
+ {
+ courseRun: { courseNumber: courseIds[2] },
course: 3,
some: 'still different data',
},
- };
- const enrollmentData = enrollments.map(v => courseData[v]);
+ ];
+ const entitlementData = [
+ {
+ courseRun: { courseNumber: entitlementIds[0] },
+ course: 4,
+ some: 'STILL different data',
+ },
+ {
+ courseRun: { courseNumber: entitlementIds[1] },
+ course: 5,
+ some: 'still DIFFERENT data',
+ },
+ ];
let out;
beforeEach(() => {
- out = reducer(testState, actions.loadEnrollments(enrollmentData));
+ out = reducer(testState, actions.loadCourses({
+ enrollments: enrollmentData,
+ entitlements: entitlementData,
+ }));
});
it('loads list of courseRun ids into enrollments field', () => {
- expect(out.enrollments).toEqual(enrollments);
+ expect(out.enrollments).toEqual([
+ ...courseIds,
+ ...entitlementIds,
+ ]);
});
it('loads object keyed by courseRun ids into courseData field', () => {
- expect(out.courseData).toEqual(courseData);
+ expect(out.courseData).toEqual({
+ [courseIds[0]]: enrollmentData[0],
+ [courseIds[1]]: enrollmentData[1],
+ [courseIds[2]]: enrollmentData[2],
+ [entitlementIds[0]]: entitlementData[0],
+ [entitlementIds[1]]: entitlementData[1],
+ });
});
});
});
diff --git a/src/data/redux/app/selectors.js b/src/data/redux/app/selectors.js
index 7923f43..3d61ee7 100644
--- a/src/data/redux/app/selectors.js
+++ b/src/data/redux/app/selectors.js
@@ -26,11 +26,15 @@ const mkCardSelector = (sel) => (state, courseNumber) => (
sel(courseCardData(state, courseNumber))
);
+const dateSixMonthsFromNow = new Date();
+dateSixMonthsFromNow.setDate(dateSixMonthsFromNow.getDate() + 180);
+
export const courseCard = StrictDict({
certificates: mkCardSelector(({ certificates }) => ({
availableDate: certificates.availableDate,
- downloadUrl: certificates.downloadUrls?.download,
- previewUrl: certificates.downloadUrls?.preview,
+ certDownloadUrl: certificates.certDownloadUrl,
+ honorCertDownloadUrl: certificates.honorCertDownloadUrl,
+ certPreviewUrl: certificates.certPreviewUrl,
isDownloadable: certificates.isDownloadable,
isEarnedButUnavailable: certificates.isEarned && !certificates.isAvailable,
isRestricted: certificates.isRestricted,
@@ -58,13 +62,20 @@ export const courseCard = StrictDict({
lastEnrolled: enrollment.lastEnrollment,
isEnrolled: enrollment.isEnrolled,
})),
- entitlements: mkCardSelector(({ entitlements }) => ({
- canChange: entitlements.canChange,
- entitlementSessions: entitlements.availableSessions,
- isEntitlement: entitlements.isEntitlement,
- isExpired: entitlements.isExpired,
- isFulfilled: entitlements.isFulfilled,
- })),
+ entitlements: mkCardSelector(({ entitlements }) => {
+ const deadline = new Date(entitlements.changeDeadline);
+ const showExpirationWarning = deadline > new Date() && deadline <= dateSixMonthsFromNow;
+ return {
+ canChange: entitlements.canChange,
+ entitlementSessions: entitlements.availableSessions,
+ isEntitlement: entitlements.isEntitlement,
+ isExpired: entitlements.isExpired,
+ isFulfilled: entitlements.isFulfilled,
+ hasSessions: entitlements.availableSessions?.length > 0,
+ changeDeadline: entitlements.changeDeadline,
+ showExpirationWarning,
+ };
+ }),
grades: mkCardSelector(({ grades }) => ({ isPassing: grades.isPassing })),
provider: mkCardSelector(({ provider }) => ({ name: provider?.name })),
relatedPrograms: mkCardSelector(({ relatedPrograms }) => ({
diff --git a/src/data/redux/thunkActions/app.js b/src/data/redux/thunkActions/app.js
index 6bc34dc..196f085 100644
--- a/src/data/redux/thunkActions/app.js
+++ b/src/data/redux/thunkActions/app.js
@@ -15,8 +15,7 @@ import requests from './requests';
export const initialize = () => (dispatch) => (
dispatch(requests.initializeList({
onSuccess: (({ enrollments, entitlements, ...globalData }) => {
- dispatch(actions.app.loadEnrollments(enrollments));
- dispatch(actions.app.loadEntitlements(entitlements));
+ dispatch(actions.app.loadCourses({ enrollments, entitlements }));
dispatch(actions.app.loadGlobalData(globalData));
}),
}))
@@ -25,8 +24,7 @@ export const initialize = () => (dispatch) => (
export const refreshList = () => (dispatch) => (
dispatch(requests.initializeList({
onSuccess: (({ enrollments, entitlements, ...globalData }) => {
- dispatch(actions.app.loadEnrollments(enrollments));
- dispatch(actions.app.loadEntitlements(entitlements));
+ dispatch(actions.app.loadCourses({ enrollments, entitlements }));
dispatch(actions.app.loadGlobalData(globalData));
}),
}))
diff --git a/src/data/services/lms/api.js b/src/data/services/lms/api.js
index 0556f6f..e1ba3b8 100644
--- a/src/data/services/lms/api.js
+++ b/src/data/services/lms/api.js
@@ -17,7 +17,7 @@ import {
*********************************************************************************/
const initializeList = () => Promise.resolve({
enrollments: fakeData.courseRunData,
- entitlements: fakeData.entitlementCourses,
+ entitlements: fakeData.entitlementData,
...fakeData.globalData,
});
diff --git a/src/data/services/lms/fakeData/courses.js b/src/data/services/lms/fakeData/courses.js
index da83787..23d2ad4 100644
--- a/src/data/services/lms/fakeData/courses.js
+++ b/src/data/services/lms/fakeData/courses.js
@@ -48,6 +48,9 @@ const logos = {
const pastDate = '11/11/2000';
const futureDate = '11/11/3030';
+const soonDate = new Date();
+soonDate.setDate(soonDate.getDate() + 60);
+const soonDateStr = soonDate.toDateString();
const globalData = {
emailConfirmation: {
@@ -100,7 +103,9 @@ export const genCertificateData = (data = {}) => ({
isAvailable: false,
isEarned: false,
isDownloadable: false,
- downloadUrls: null, // { preview, download }
+ certPreviewUrl: 'edx.com/courses/my-course-url/cert-preview',
+ certDownloadUrl: 'edx.com/courses/my-course-url/cert-download',
+ honorCertDownloadUrl: 'edx.com/courses/my-course-url/honor-cert-download',
...data,
});
@@ -224,10 +229,8 @@ export const courseRuns = [
isAvailable: true,
isDownloadable: true,
availableDate: pastDate,
- downloadUrls: {
- preview: logos.edx,
- download: logos.social,
- },
+ certDownloadUrl: logos.social,
+ certPreviewUrl: logos.edx,
}),
entitlements: { isEntitlement: false },
},
@@ -241,9 +244,7 @@ export const courseRuns = [
isAvailable: true,
isDownloadable: true,
availableDate: pastDate,
- downloadUrls: {
- download: logos.social,
- },
+ certDownloadUrl: logos.social,
}),
entitlements: { isEntitlement: false },
},
@@ -319,7 +320,6 @@ export const courseRuns = [
export const entitlementCourses = [
{
- course: { title: genCourseTitle(100) },
entitlements: {
isEntitlement: true,
availableSessions,
@@ -331,7 +331,17 @@ export const entitlementCourses = [
isExpired: false,
},
}, {
- course: { title: genCourseTitle(101) },
+ entitlements: {
+ isEntitlement: true,
+ availableSessions,
+ isRefundable: true,
+ isFulfilled: false,
+ canViewCourse: false,
+ changeDeadline: soonDateStr,
+ canChange: true,
+ isExpired: false,
+ },
+ }, {
entitlements: {
isEntitlement: true,
availableSessions,
@@ -343,10 +353,9 @@ export const entitlementCourses = [
isExpired: false,
},
}, {
- course: { title: genCourseTitle(102) },
entitlements: {
isEntitlement: true,
- availableSessions,
+ availableSessions: [],
isRefundable: true,
isFulfilled: false,
canViewCourse: false,
@@ -387,13 +396,45 @@ export const courseRunData = courseRuns.map(
...data,
courseRun: genCourseRunData({ ...data.courseRun, courseNumber }),
...iteratedData[providerIndex],
- credit: { isPurchased: false, requestStatus: null },
+ };
+ },
+);
+
+export const entitlementData = entitlementCourses.map(
+ (data, index) => {
+ const title = genCourseTitle(100 + index);
+ const courseNumber = genCourseID(100 + index);
+ const providerIndex = index % 3;
+ const iteratedData = [
+ {
+ provider: providers.edx,
+ course: { title, bannerUrl: logos.edx },
+ relatedPrograms,
+ },
+ {
+ provider: providers.mit,
+ course: { title, bannerUrl: logos.science },
+ relatedPrograms: [relatedPrograms[0]],
+ },
+ {
+ provider: null,
+ course: { title, bannerUrl: logos.social },
+ relatedPrograms: [],
+ },
+ ];
+ return {
+ ...data,
+ enrollment: genEnrollmentData(),
+ grades: { isPassing: true },
+ certificates: genCertificateData(),
+ courseRun: genCourseRunData({ ...data.courseRun, courseNumber }),
+ ...iteratedData[providerIndex],
};
},
);
export default {
courseRunData,
- entitlementCourses,
+ entitlementData,
globalData,
};
diff --git a/src/setupTest.jsx b/src/setupTest.jsx
index 8b8d4d9..c613c94 100755
--- a/src/setupTest.jsx
+++ b/src/setupTest.jsx
@@ -19,7 +19,7 @@ jest.mock('@edx/frontend-platform/i18n', () => {
const i18n = jest.requireActual('@edx/frontend-platform/i18n');
const PropTypes = jest.requireActual('prop-types');
const { formatMessage } = jest.requireActual('./testUtils');
- const formatDate = jest.fn().mockName('useIntl.formatDate');
+ const formatDate = jest.fn(date => date).mockName('useIntl.formatDate');
return {
...i18n,
intlShape: PropTypes.shape({
@@ -86,6 +86,7 @@ jest.mock('@edx/paragon', () => jest.requireActual('testUtils').mockNestedCompon
Hyperlink: 'Hyperlink',
Icon: 'Icon',
IconButton: 'IconButton',
+ MailtoLink: 'MailtoLink',
ModalDialog: {
Header: 'ModalDialog.Header',
Body: 'ModalDialog.Body',
diff --git a/src/test/app.test.jsx b/src/test/app.test.jsx
index 7c4f801..2d2f78a 100644
--- a/src/test/app.test.jsx
+++ b/src/test/app.test.jsx
@@ -80,7 +80,8 @@ const mockApi = () => {
resolveFns.init = {
success: () => resolve({
enrollments: fakeData.courseRunData,
- entitlements: fakeData.entitlementCourses,
+ entitlements: fakeData.entitlementData,
+ ...fakeData.globalData,
}),
};
}));
@@ -148,7 +149,6 @@ describe('ESG app integration tests', () => {
);
cardDetails = inspector.get.card.details(card);
- console.log({ enrollment: courseData.enrollment });
[
courseData.provider.name,
courseNumber,