diff --git a/src/alerts/offer-alert/OfferAlert.jsx b/src/alerts/offer-alert/OfferAlert.jsx index 89644ef2..f271e111 100644 --- a/src/alerts/offer-alert/OfferAlert.jsx +++ b/src/alerts/offer-alert/OfferAlert.jsx @@ -6,6 +6,7 @@ import { import { Hyperlink } from '@edx/paragon'; import { Alert, ALERT_TYPES } from '../../generic/user-messages'; +import { FormattedPricing } from '../../generic/upgrade-button'; import messages from './messages'; function OfferAlert({ intl, payload }) { @@ -20,26 +21,12 @@ function OfferAlert({ intl, payload }) { const { code, - discountedPrice, expirationDate, - originalPrice, percentage, upgradeUrl, } = offer; const timezoneFormatArgs = userTimezone ? { timeZone: userTimezone } : {}; - const fullPricing = ( - <> - - {intl.formatMessage(messages.srPrices, { discountedPrice, originalPrice })} - - - - ); - return ( {/* the first-purchase-offer-banner class can be removed post REV-1512 experiment */} @@ -57,7 +44,7 @@ function OfferAlert({ intl, payload }) { {...timezoneFormatArgs} /> ), - fullPricing, + fullPricing: , percentage, }} /> diff --git a/src/alerts/offer-alert/messages.js b/src/alerts/offer-alert/messages.js index f3e46123..149c0d07 100644 --- a/src/alerts/offer-alert/messages.js +++ b/src/alerts/offer-alert/messages.js @@ -1,10 +1,6 @@ import { defineMessages } from '@edx/frontend-platform/i18n'; const messages = defineMessages({ - srPrices: { - id: 'learning.offer.screenReaderPrices', - defaultMessage: 'Original price: {originalPrice}, discount price: {discountedPrice}', - }, upgradeNow: { id: 'learning.offer.upgradeNow', defaultMessage: 'Upgrade now', diff --git a/src/course-home/outline-tab/OutlineTab.jsx b/src/course-home/outline-tab/OutlineTab.jsx index f79e62c2..58ce6762 100644 --- a/src/course-home/outline-tab/OutlineTab.jsx +++ b/src/course-home/outline-tab/OutlineTab.jsx @@ -185,7 +185,7 @@ function OutlineTab({ intl }) { )} - {canShowUpgradeSock && } + {canShowUpgradeSock && } ); } diff --git a/src/course-home/outline-tab/widgets/UpgradeCard.jsx b/src/course-home/outline-tab/widgets/UpgradeCard.jsx index 0fa9dd2c..8fa715fd 100644 --- a/src/course-home/outline-tab/widgets/UpgradeCard.jsx +++ b/src/course-home/outline-tab/widgets/UpgradeCard.jsx @@ -7,11 +7,13 @@ import { Button } from '@edx/paragon'; import messages from '../messages'; import { useModel } from '../../../generic/model-store'; +import { UpgradeButton } from '../../../generic/upgrade-button'; import VerifiedCert from '../../../generic/assets/edX_certificate.png'; function UpgradeCard({ courseId, intl, onLearnMore }) { const { org } = useModel('courses', courseId); const { + offer, verifiedMode, } = useModel('outline', courseId); @@ -44,16 +46,11 @@ function UpgradeCard({ courseId, intl, onLearnMore }) { style={{ width: '124px' }} />
- + verifiedMode={verifiedMode} + /> {onLearnMore && ( + ); + } 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 = ( + + ); 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' && ( - - )} - {certStatus === 'downloadable' && linkedinAddToProfileUrl && ( - - )} - {buttonLocation && ( - - )} +
+ {buttonPrefix} + {buttonLocation && ( + + )} + {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 ( + <> + {discountedPrice} +  ( + + {intl.formatMessage(messages.srInlinePrices, { originalPrice })} + + + ) + + ); + } + + return ( + <> + + {intl.formatMessage(messages.srPrices, { discountedPrice, originalPrice })} + + + + ); +} + +FormattedPricing.defaultProps = { + inline: false, + offer: null, + verifiedMode: null, +}; + +FormattedPricing.propTypes = { + inline: PropTypes.bool, + intl: intlShape.isRequired, + offer: PropTypes.shape({ + discountedPrice: PropTypes.string.isRequired, + originalPrice: PropTypes.string.isRequired, + upgradeUrl: PropTypes.string.isRequired, + }), + verifiedMode: PropTypes.shape({ + currencySymbol: PropTypes.string.isRequired, + price: PropTypes.number.isRequired, + upgradeUrl: PropTypes.string.isRequired, + }), +}; + +export default injectIntl(FormattedPricing); diff --git a/src/generic/upgrade-button/UpgradeButton.jsx b/src/generic/upgrade-button/UpgradeButton.jsx new file mode 100644 index 00000000..c9d67e8c --- /dev/null +++ b/src/generic/upgrade-button/UpgradeButton.jsx @@ -0,0 +1,59 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { FormattedMessage, injectIntl, intlShape } from '@edx/frontend-platform/i18n'; +import { Button } from '@edx/paragon'; + +import FormattedPricing from './FormattedPricing'; + +function UpgradeButton(props) { + const { + intl, + offer, + onClick, + verifiedMode, + ...rest + } = props; + + // Prefer offer's url in case it is ever different (though it is not at time of this writing) + const url = offer ? offer.upgradeUrl : verifiedMode.upgradeUrl; + + return ( + + ); +} + +UpgradeButton.defaultProps = { + offer: null, + onClick: null, +}; + +UpgradeButton.propTypes = { + intl: intlShape.isRequired, + offer: PropTypes.shape({ + upgradeUrl: PropTypes.string.isRequired, + }), + onClick: PropTypes.func, + verifiedMode: PropTypes.shape({ + upgradeUrl: PropTypes.string.isRequired, + }).isRequired, +}; + +export default injectIntl(UpgradeButton); diff --git a/src/generic/upgrade-button/index.js b/src/generic/upgrade-button/index.js new file mode 100644 index 00000000..845dc5cf --- /dev/null +++ b/src/generic/upgrade-button/index.js @@ -0,0 +1,7 @@ +import FormattedPricing from './FormattedPricing'; +import UpgradeButton from './UpgradeButton'; + +export { + FormattedPricing, + UpgradeButton, +}; diff --git a/src/generic/upgrade-button/messages.js b/src/generic/upgrade-button/messages.js new file mode 100644 index 00000000..efb0f297 --- /dev/null +++ b/src/generic/upgrade-button/messages.js @@ -0,0 +1,14 @@ +import { defineMessages } from '@edx/frontend-platform/i18n'; + +const messages = defineMessages({ + srPrices: { + id: 'learning.offer.screenReaderPrices', // historic id + defaultMessage: 'Original price: {originalPrice}, discount price: {discountedPrice}', + }, + srInlinePrices: { + id: 'learning.upgradeButton.screenReaderInlinePrices', + defaultMessage: 'Original price: {originalPrice}', + }, +}); + +export default messages;