diff --git a/src/course-home/progress-tab/CertificateBanner.jsx b/src/course-home/progress-tab/CertificateBanner.jsx index fb5537ba..3adba823 100644 --- a/src/course-home/progress-tab/CertificateBanner.jsx +++ b/src/course-home/progress-tab/CertificateBanner.jsx @@ -6,7 +6,7 @@ import { requestCert } from '../data/thunks'; import { useModel } from '../../generic/model-store'; import messages from './messages'; -import VerifiedCert from '../../courseware/course/sequence/lock-paywall/assets/edx-verified-mini-cert.png'; +import VerifiedCert from '../../generic/assets/edX_verified_certificate.png'; function CertificateBanner({ intl }) { const { diff --git a/src/courseware/course/course-exit/CourseCelebration.jsx b/src/courseware/course/course-exit/CourseCelebration.jsx index eb71c62c..fd62f7d9 100644 --- a/src/courseware/course/course-exit/CourseCelebration.jsx +++ b/src/courseware/course/course-exit/CourseCelebration.jsx @@ -13,11 +13,12 @@ import { getAuthenticatedUser } from '@edx/frontend-platform/auth'; import CelebrationMobile from './assets/celebration_456x328.gif'; import CelebrationDesktop from './assets/celebration_750x540.gif'; -import certificate from './assets/edx_certificate.png'; -import certificateLocked from './assets/edx_certificate_locked.png'; +import certificate from '../../../generic/assets/edX_verified_certificate.png'; +import certificateLocked from '../../../generic/assets/edX_locked_verified_certificate.png'; import messages from './messages'; import { useModel } from '../../../generic/model-store'; import { requestCert } from '../../../course-home/data/thunks'; +import ProgramCompletion from './ProgramCompletion'; import DashboardFootnote from './DashboardFootnote'; import UpgradeFootnote from './UpgradeFootnote'; import SocialIcons from '../../social-share/SocialIcons'; @@ -40,6 +41,7 @@ function CourseCelebration({ intl }) { certificateData, end, linkedinAddToProfileUrl, + relatedPrograms, verifiedMode, verifyIdentityUrl, } = useModel('courses', courseId); @@ -307,6 +309,15 @@ function CourseCelebration({ intl }) { )} )} + {relatedPrograms && relatedPrograms.map(program => ( + + ))} {footnote} diff --git a/src/courseware/course/course-exit/CourseExit.test.jsx b/src/courseware/course/course-exit/CourseExit.test.jsx index ea176258..f104177b 100644 --- a/src/courseware/course/course-exit/CourseExit.test.jsx +++ b/src/courseware/course/course-exit/CourseExit.test.jsx @@ -190,6 +190,81 @@ describe('Course Exit Pages', () => { expect(screen.getByRole('link', { name: 'View my certificate' })).toBeInTheDocument(); expect(screen.getByRole('link', { name: 'Add to LinkedIn profile' })).toBeInTheDocument(); }); + + describe('Program Completion experience', () => { + beforeEach(() => { + setMetadata({ + certificate_data: { + cert_status: 'downloadable', + cert_web_view_url: '/certificates/cooluuidgoeshere', + }, + }); + }); + + it('Does not render ProgramCompletion no related programs', async () => { + await fetchAndRender(); + expect(screen.queryByTestId('program-completion')).not.toBeInTheDocument(); + }); + + it('Does not render ProgramCompletion if program is incomplete', async () => { + setMetadata({ + related_programs: [{ + progress: { + completed: 1, + in_progress: 1, + not_started: 1, + }, + slug: 'micromasters', + title: 'Example MicroMasters Program', + uuid: '123456', + url: 'http://localhost:18000/dashboard/programs/123456', + }], + }); + await fetchAndRender(); + + expect(screen.queryByTestId('program-completion')).not.toBeInTheDocument(); + }); + + it('Renders ProgramCompletion if program is complete', async () => { + setMetadata({ + related_programs: [{ + progress: { + completed: 3, + in_progress: 0, + not_started: 0, + }, + slug: 'micromasters', + title: 'Example MicroMasters Program', + uuid: '123456', + url: 'http://localhost:18000/dashboard/programs/123456', + }], + }); + await fetchAndRender(); + + expect(screen.queryByTestId('program-completion')).toBeInTheDocument(); + expect(screen.queryByTestId('micromasters')).toBeInTheDocument(); + }); + + it('Does not render ProgramCompletion if program is an excluded type', async () => { + setMetadata({ + related_programs: [{ + progress: { + completed: 3, + in_progress: 0, + not_started: 0, + }, + slug: 'excluded-program-type', + title: 'Example Excluded Program', + uuid: '123456', + url: 'http://localhost:18000/dashboard/programs/123456', + }], + }); + await fetchAndRender(); + + expect(screen.queryByTestId('program-completion')).not.toBeInTheDocument(); + expect(screen.queryByTestId('excluded-program-type')).not.toBeInTheDocument(); + }); + }); }); describe('Course Non-passing Experience', () => { diff --git a/src/courseware/course/course-exit/ProgramCompletion.jsx b/src/courseware/course/course-exit/ProgramCompletion.jsx new file mode 100644 index 00000000..c855a456 --- /dev/null +++ b/src/courseware/course/course-exit/ProgramCompletion.jsx @@ -0,0 +1,128 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +import { getConfig } from '@edx/frontend-platform'; +import { FormattedMessage, injectIntl, intlShape } from '@edx/frontend-platform/i18n'; +import { Alert, Button, Hyperlink } from '@edx/paragon'; +import microBachelorsCertImage from '../../../generic/assets/edX_microBachelors_certificate.png'; +import microMastersCertImage from '../../../generic/assets/edX_microMasters_certificate.png'; +import professionalCertImage from '../../../generic/assets/edX_professionalCertificate_certificate.png'; +import xSeriesCertImage from '../../../generic/assets/edX_xSeries_certificate.png'; +import messages from './messages'; + +/** + * Note for Open edX developers: + * There are pieces of this component that are hard-coded and specific to edX that may not apply to your organization. + * This includes mentions of our edX program types (MicroMasters, MicroBachelors, Professional Certificate, and + * XSeries), along with their respective support article URLs and image variable names. + * + * Currently, this component will not render unless the learner's completed course has a related program of one of the + * four aforementioned types. This will not impact the parent components (i.e. CourseCelebration will render normally). + */ + +function ProgramCompletion({ + intl, + progress, + title, + type, + url, +}) { + if (progress.notStarted !== 0 || progress.inProgress !== 0) { + return null; + } + + let certImage; + switch (type) { + case 'microbachelors': + certImage = microBachelorsCertImage; + break; + case 'micromasters': + certImage = microMastersCertImage; + break; + case 'professional-certificate': + certImage = professionalCertImage; + break; + case 'xseries': + certImage = xSeriesCertImage; + break; + default: + return null; + } + + const programLink = ( + + {intl.formatMessage(messages.dashboardLink)} + + ); + + return ( + +
+
{intl.formatMessage(messages.programsLastCourseHeader, { title })}
+

+ +

+ {type === 'microbachelors' && ( + <> +

+ + {intl.formatMessage(messages.microBachelorsLearnMore)} + +

+ + + )} + {type === 'micromasters' && ( +

+ {intl.formatMessage(messages.microMastersMessage)} + {' '} + + {intl.formatMessage(messages.microMastersLearnMore)} + +

+ )} +
+
+ {`${intl.formatMessage(messages.certificateImage)}`} +
+
+ ); +} + +ProgramCompletion.propTypes = { + intl: intlShape.isRequired, + progress: PropTypes.shape({ + completed: PropTypes.number.isRequired, + inProgress: PropTypes.number.isRequired, + notStarted: PropTypes.number.isRequired, + }).isRequired, + title: PropTypes.string.isRequired, + type: PropTypes.string.isRequired, + url: PropTypes.string.isRequired, +}; + +export default injectIntl(ProgramCompletion); diff --git a/src/courseware/course/course-exit/assets/edx_certificate.png b/src/courseware/course/course-exit/assets/edx_certificate.png deleted file mode 100644 index 965b9fce..00000000 Binary files a/src/courseware/course/course-exit/assets/edx_certificate.png and /dev/null differ diff --git a/src/courseware/course/course-exit/assets/edx_certificate_locked.png b/src/courseware/course/course-exit/assets/edx_certificate_locked.png deleted file mode 100644 index dd9187c6..00000000 Binary files a/src/courseware/course/course-exit/assets/edx_certificate_locked.png and /dev/null differ diff --git a/src/courseware/course/course-exit/messages.js b/src/courseware/course/course-exit/messages.js index c2a2aaaf..ec3448fb 100644 --- a/src/courseware/course/course-exit/messages.js +++ b/src/courseware/course/course-exit/messages.js @@ -1,6 +1,11 @@ import { defineMessages } from '@edx/frontend-platform/i18n'; const messages = defineMessages({ + applyForCredit: { + id: 'courseExit.programs.applyForCredit', + defaultMessage: 'Apply for credit', + description: 'Button for the learner to apply for course credit', + }, certificateHeaderDownloadable: { id: 'courseCelebration.certificateHeader.downloadable', defaultMessage: 'Your certificate is available!', @@ -42,7 +47,7 @@ const messages = defineMessages({ dashboardLink: { id: 'courseExit.dashboardLink', defaultMessage: 'Dashboard', - description: "Link to user's dashboard", + description: 'Link to user’s dashboard', }, downloadButton: { id: 'courseCelebration.downloadButton', @@ -69,7 +74,19 @@ const messages = defineMessages({ linkedinAddToProfileButton: { id: 'courseCelebration.linkedinAddToProfileButton', defaultMessage: 'Add to LinkedIn profile', - description: "Button to add certificate information to the user's LinkedIn profile", + description: 'Button to add certificate information to the user’s LinkedIn profile', + }, + microBachelorsLearnMore: { + id: 'courseExit.programs.microBachelors.learnMore', + defaultMessage: 'Learn more about how your MicroBachelors credential can be applied for credit.', + }, + microMastersLearnMore: { + id: 'courseExit.programs.microMasters.learnMore', + defaultMessage: 'Learn more about the process of applying MicroMasters certificates to Master’s degrees.', + }, + microMastersMessage: { + id: 'courseExit.programs.microMasters.mastersMessage', + defaultMessage: 'If you’re interested in using your MicroMasters certificate towards a Master’s program, you can get started today!', }, nextButtonComplete: { id: 'learn.sequence.navigation.complete.button', // for historical reasons @@ -82,7 +99,11 @@ const messages = defineMessages({ profileLink: { id: 'courseExit.profileLink', defaultMessage: 'Profile', - description: "Link to user's profile", + description: 'Link to user’s profile', + }, + programsLastCourseHeader: { + id: 'courseExit.programs.lastCourse', + defaultMessage: 'You have completed the last course in {title}!', }, requestCertificateBodyText: { id: 'courseCelebration.requestCertificateBodyText', diff --git a/src/courseware/course/course-sock/CourseSock.jsx b/src/courseware/course/course-sock/CourseSock.jsx index 60407235..9fd7e7f9 100644 --- a/src/courseware/course/course-sock/CourseSock.jsx +++ b/src/courseware/course/course-sock/CourseSock.jsx @@ -4,7 +4,7 @@ import { getConfig } from '@edx/frontend-platform'; import { FormattedMessage } from '@edx/frontend-platform/i18n'; import LearnerQuote1 from './assets/learner-quote.png'; import LearnerQuote2 from './assets/learner-quote2.png'; -import VerifiedCert from './assets/verified-cert.png'; +import VerifiedCert from '../../../generic/assets/edX_verified_certificate.png'; export default class CourseSock extends Component { constructor(props) { diff --git a/src/courseware/course/course-sock/assets/verified-cert.png b/src/courseware/course/course-sock/assets/verified-cert.png deleted file mode 100644 index 1b2be6cb..00000000 Binary files a/src/courseware/course/course-sock/assets/verified-cert.png and /dev/null differ diff --git a/src/courseware/course/sequence/lock-paywall/LockPaywall.jsx b/src/courseware/course/sequence/lock-paywall/LockPaywall.jsx index c7cc179d..212a100c 100644 --- a/src/courseware/course/sequence/lock-paywall/LockPaywall.jsx +++ b/src/courseware/course/sequence/lock-paywall/LockPaywall.jsx @@ -5,7 +5,7 @@ import { faLock } from '@fortawesome/free-solid-svg-icons'; import { injectIntl, intlShape } from '@edx/frontend-platform/i18n'; import messages from './messages'; -import VerifiedCert from './assets/edx-verified-mini-cert.png'; +import VerifiedCert from '../../../../generic/assets/edX_verified_certificate.png'; import { useModel } from '../../../../generic/model-store'; function LockPaywall({ diff --git a/src/courseware/course/sequence/lock-paywall/assets/edx-verified-mini-cert.png b/src/courseware/course/sequence/lock-paywall/assets/edx-verified-mini-cert.png deleted file mode 100644 index 35b9b9f1..00000000 Binary files a/src/courseware/course/sequence/lock-paywall/assets/edx-verified-mini-cert.png and /dev/null differ diff --git a/src/courseware/data/__factories__/courseMetadata.factory.js b/src/courseware/data/__factories__/courseMetadata.factory.js index 37ca76ee..4bb364e6 100644 --- a/src/courseware/data/__factories__/courseMetadata.factory.js +++ b/src/courseware/data/__factories__/courseMetadata.factory.js @@ -57,6 +57,7 @@ Factory.define('courseMetadata') certificate_data: null, verify_identity_url: null, linkedin_add_to_profile_url: null, + related_programs: null, }).attr( 'tabs', ['tabs', 'id'], (passedTabs, id) => { if (passedTabs) { diff --git a/src/courseware/data/api.js b/src/courseware/data/api.js index 36d0795f..c69eed20 100644 --- a/src/courseware/data/api.js +++ b/src/courseware/data/api.js @@ -140,6 +140,7 @@ function normalizeMetadata(metadata) { certificateData: camelCaseObject(metadata.certificate_data), verifyIdentityUrl: metadata.verify_identity_url, linkedinAddToProfileUrl: metadata.linkedin_add_to_profile_url, + relatedPrograms: camelCaseObject(metadata.related_programs), }; } diff --git a/src/generic/assets/edX_locked_verified_certificate.png b/src/generic/assets/edX_locked_verified_certificate.png new file mode 100644 index 00000000..65aa5276 Binary files /dev/null and b/src/generic/assets/edX_locked_verified_certificate.png differ diff --git a/src/generic/assets/edX_microBachelors_certificate.png b/src/generic/assets/edX_microBachelors_certificate.png new file mode 100644 index 00000000..f8700574 Binary files /dev/null and b/src/generic/assets/edX_microBachelors_certificate.png differ diff --git a/src/generic/assets/edX_microMasters_certificate.png b/src/generic/assets/edX_microMasters_certificate.png new file mode 100644 index 00000000..038948ee Binary files /dev/null and b/src/generic/assets/edX_microMasters_certificate.png differ diff --git a/src/generic/assets/edX_professionalCertificate_certificate.png b/src/generic/assets/edX_professionalCertificate_certificate.png new file mode 100644 index 00000000..737f845a Binary files /dev/null and b/src/generic/assets/edX_professionalCertificate_certificate.png differ diff --git a/src/generic/assets/edX_verified_certificate.png b/src/generic/assets/edX_verified_certificate.png new file mode 100644 index 00000000..e148f522 Binary files /dev/null and b/src/generic/assets/edX_verified_certificate.png differ diff --git a/src/generic/assets/edX_xSeries_certificate.png b/src/generic/assets/edX_xSeries_certificate.png new file mode 100644 index 00000000..58c0baf2 Binary files /dev/null and b/src/generic/assets/edX_xSeries_certificate.png differ diff --git a/src/index.jsx b/src/index.jsx index cf6bf633..9e12f009 100755 --- a/src/index.jsx +++ b/src/index.jsx @@ -80,6 +80,7 @@ initialize({ handlers: { config: () => { mergeConfig({ + CREDENTIALS_BASE_URL: process.env.CREDENTIALS_BASE_URL || null, INSIGHTS_BASE_URL: process.env.INSIGHTS_BASE_URL || null, SOCIAL_UTM_MILESTONE_CAMPAIGN: process.env.SOCIAL_UTM_MILESTONE_CAMPAIGN || null, STUDIO_BASE_URL: process.env.STUDIO_BASE_URL || null,