-
- {intl.formatMessage(messages.upgradeButton, {
- price: verifiedMode.price,
- symbol: verifiedMode.currencySymbol,
- })}
-
+ verifiedMode={verifiedMode}
+ />
{onLearnMore && (
)}
- {canShowUpgradeSock &&
}
+ {canShowUpgradeSock &&
}
>
);
diff --git a/src/courseware/course/course-exit/CourseCelebration.jsx b/src/courseware/course/course-exit/CourseCelebration.jsx
index 0191ed3b..9e9f192c 100644
--- a/src/courseware/course/course-exit/CourseCelebration.jsx
+++ b/src/courseware/course/course-exit/CourseCelebration.jsx
@@ -17,6 +17,7 @@ import CelebrationMobile from './assets/celebration_456x328.gif';
import CelebrationDesktop from './assets/celebration_750x540.gif';
import certificate from '../../../generic/assets/edX_certificate.png';
import certificateLocked from '../../../generic/assets/edX_locked_certificate.png';
+import { FormattedPricing } from '../../../generic/upgrade-button';
import messages from './messages';
import { useModel } from '../../../generic/model-store';
import { requestCert } from '../../../course-home/data/thunks';
@@ -43,6 +44,7 @@ function CourseCelebration({ intl }) {
certificateData,
end,
linkedinAddToProfileUrl,
+ offer,
org,
relatedPrograms,
verifiedMode,
@@ -86,10 +88,12 @@ function CourseCelebration({ intl }) {
);
+ let buttonPrefix = null;
let buttonLocation;
let buttonText;
let buttonVariant = 'outline-primary';
let buttonEvent = null;
+ let buttonSuffix = null;
let certificateImage = certificate;
let footnote;
let message;
@@ -118,6 +122,19 @@ function CourseCelebration({ intl }) {
buttonLocation = downloadUrl;
buttonText = intl.formatMessage(messages.downloadButton);
}
+ if (linkedinAddToProfileUrl) {
+ buttonPrefix = (
+
logClick(org, courseId, administrator, 'linkedin_add_to_profile')}
+ style={{ backgroundColor: LINKEDIN_BLUE, border: 'none' }}
+ >
+
+ {`${intl.formatMessage(messages.linkedinAddToProfileButton)}`}
+
+ );
+ }
buttonEvent = 'view_cert';
visitEvent = 'celebration_with_cert';
footnote =
;
@@ -151,8 +168,20 @@ function CourseCelebration({ intl }) {
break;
}
case 'requesting':
- buttonText = intl.formatMessage(messages.requestCertificateButton);
+ // The requesting status needs a different button because it does a POST instead of a GET.
+ // So we don't set buttonLocation and instead define a custom button as a buttonPrefix.
buttonEvent = 'request_cert';
+ buttonPrefix = (
+
{
+ logClick(org, courseId, administrator, buttonEvent);
+ dispatch(requestCert(courseId));
+ }}
+ >
+ {intl.formatMessage(messages.requestCertificateButton)}
+
+ );
certHeader = intl.formatMessage(messages.certificateHeaderRequestable);
message = (
{intl.formatMessage(messages.requestCertificateBodyText)}
);
visitEvent = 'celebration_with_requestable_cert';
@@ -193,7 +222,7 @@ function CourseCelebration({ intl }) {
assignments in this course. Upon completion, you will receive a verified certificate which is a
valuable credential to improve your job prospects and advance your career, or highlight your
certificate in school applications."
- values={{ price: verifiedMode.currencySymbol + verifiedMode.price }}
+ values={{ price:
}}
/>
{getConfig().SUPPORT_URL_VERIFIED_CERTIFICATE && (
@@ -211,6 +240,20 @@ function CourseCelebration({ intl }) {
buttonEvent = 'upgrade';
buttonLocation = verifiedMode.upgradeUrl;
buttonVariant = 'primary';
+ if (offer) {
+ buttonSuffix = (
+
+ {offer.code}),
+ percent: offer.percentage,
+ }}
+ />
+
+ );
+ }
certificateImage = certificateLocked;
visitEvent = 'celebration_upgrade';
if (verifiedMode.accessExpirationDate) {
@@ -270,39 +313,19 @@ function CourseCelebration({ intl }) {
{certHeader}
{message}
- {/* The requesting status needs a different button because it does a POST instead of a GET */}
- {certStatus === 'requesting' && (
-
{
- logClick(org, courseId, administrator, buttonEvent);
- dispatch(requestCert(courseId));
- }}
- >
- {buttonText}
-
- )}
- {certStatus === 'downloadable' && linkedinAddToProfileUrl && (
-
logClick(org, courseId, administrator, 'linkedin_add_to_profile')}
- style={{ backgroundColor: LINKEDIN_BLUE, border: 'none' }}
- >
-
- {`${intl.formatMessage(messages.linkedinAddToProfileButton)}`}
-
- )}
- {buttonLocation && (
-
logClick(org, courseId, administrator, buttonEvent)}
- >
- {buttonText}
-
- )}
+
+ {buttonPrefix}
+ {buttonLocation && (
+ logClick(org, courseId, administrator, buttonEvent)}
+ >
+ {buttonText}
+
+ )}
+ {buttonSuffix}
+
{certStatus !== 'unverified' && (
diff --git a/src/generic/course-sock/CourseSock.jsx b/src/generic/course-sock/CourseSock.jsx
index ef399682..18a576f1 100644
--- a/src/generic/course-sock/CourseSock.jsx
+++ b/src/generic/course-sock/CourseSock.jsx
@@ -4,12 +4,12 @@ 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 { UpgradeButton } from '../upgrade-button';
import VerifiedCert from '../assets/edX_certificate.png';
export default class CourseSock extends Component {
constructor(props) {
super(props);
- this.verifiedMode = props.verifiedMode;
this.state = { showUpsell: false };
this.sockElement = React.createRef();
}
@@ -31,7 +31,7 @@ export default class CourseSock extends Component {
}
render() {
- if (!this.verifiedMode) {
+ if (!this.props.verifiedMode) {
return null;
}
@@ -65,22 +65,14 @@ export default class CourseSock extends Component {
@@ -215,12 +207,11 @@ export default class CourseSock extends Component {
}
}
-CourseSock.propTypes = {
- verifiedMode: PropTypes.shape({
- price: PropTypes.number,
- currency: PropTypes.string,
- currencySymbol: PropTypes.string,
- sku: PropTypes.string,
- upgradeUrl: PropTypes.string,
- }).isRequired,
+CourseSock.defaultProps = {
+ offer: null,
+};
+
+CourseSock.propTypes = {
+ offer: PropTypes.shape({}),
+ verifiedMode: PropTypes.shape({}).isRequired,
};
diff --git a/src/generic/course-sock/CourseSock.test.jsx b/src/generic/course-sock/CourseSock.test.jsx
index 5ee1197e..65e68082 100644
--- a/src/generic/course-sock/CourseSock.test.jsx
+++ b/src/generic/course-sock/CourseSock.test.jsx
@@ -32,8 +32,8 @@ describe('Course Sock', () => {
fireEvent.click(upsellButton);
expect(screen.getByText('edX Verified Certificate')).toBeInTheDocument();
- const { currencySymbol, price, currency } = mockData.verifiedMode;
- expect(screen.getByText(`Upgrade (${currencySymbol}${price} ${currency})`)).toBeInTheDocument();
+ const { currencySymbol, price } = mockData.verifiedMode;
+ expect(screen.getByText(`Upgrade (${currencySymbol}${price})`)).toBeInTheDocument();
fireEvent.click(upsellButton);
expect(screen.queryByText('edX Verified Certificate')).not.toBeInTheDocument();
diff --git a/src/generic/upgrade-button/FormattedPricing.jsx b/src/generic/upgrade-button/FormattedPricing.jsx
new file mode 100644
index 00000000..50efc1d9
--- /dev/null
+++ b/src/generic/upgrade-button/FormattedPricing.jsx
@@ -0,0 +1,80 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
+
+import messages from './messages';
+
+function FormattedPricing(props) {
+ const {
+ inline,
+ intl,
+ offer,
+ verifiedMode,
+ } = props;
+
+ if (!offer) {
+ const {
+ currencySymbol,
+ price,
+ } = verifiedMode;
+ return `${currencySymbol}${price}`;
+ }
+
+ const {
+ discountedPrice,
+ originalPrice,
+ } = offer;
+
+ // The inline style is meant for being embedded in a sentence - it bolds the discount and leaves the original price
+ // as a parenthetical. The normal styling is more suited for a button, where the price and discount are side by side.
+ if (inline) {
+ return (
+ <>
+