AA-449: Show discounted price in upgrade buttons & course exit (#311)
If an offer is active for the user, show the discounted price (and a struck-out original price) on upgrade buttons in the course sock and outline sidebar. Also show the discount code and price in the course exit upgrade screen.
This commit is contained in:
@@ -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 {
|
||||
</div>
|
||||
<div className="position-relative flex-grow-1 d-flex flex-column justify-content-end align-items-md-end">
|
||||
<div style={{ position: 'sticky', bottom: '4rem' }}>
|
||||
<a
|
||||
href={this.verifiedMode.upgradeUrl}
|
||||
className="btn btn-success btn-lg btn-upgrade focusable mb-3"
|
||||
<UpgradeButton
|
||||
size="lg"
|
||||
offer={this.props.offer}
|
||||
verifiedMode={this.props.verifiedMode}
|
||||
className="mb-3"
|
||||
data-creative="original_sock"
|
||||
data-position="sock"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="coursesock.upsell.upgrade"
|
||||
defaultMessage="Upgrade ({symbol}{price} {currency})"
|
||||
values={{
|
||||
symbol: this.verifiedMode.currencySymbol,
|
||||
price: this.verifiedMode.price,
|
||||
currency: this.verifiedMode.currency,
|
||||
}}
|
||||
/>
|
||||
</a>
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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();
|
||||
|
||||
80
src/generic/upgrade-button/FormattedPricing.jsx
Normal file
80
src/generic/upgrade-button/FormattedPricing.jsx
Normal file
@@ -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 (
|
||||
<>
|
||||
<span className="font-weight-bold">{discountedPrice}</span>
|
||||
(
|
||||
<span className="sr-only">
|
||||
{intl.formatMessage(messages.srInlinePrices, { originalPrice })}
|
||||
</span>
|
||||
<span aria-hidden="true">
|
||||
<del>{originalPrice}</del>
|
||||
</span>
|
||||
)
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<span className="sr-only">
|
||||
{intl.formatMessage(messages.srPrices, { discountedPrice, originalPrice })}
|
||||
</span>
|
||||
<span aria-hidden="true">
|
||||
{/* the price discount and price original classes can be removed post REV-1512 experiment */}
|
||||
<span className="price discount">{discountedPrice}</span> <del className="price original">{originalPrice}</del>
|
||||
</span>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
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);
|
||||
59
src/generic/upgrade-button/UpgradeButton.jsx
Normal file
59
src/generic/upgrade-button/UpgradeButton.jsx
Normal file
@@ -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 (
|
||||
<Button
|
||||
variant="success"
|
||||
href={url}
|
||||
onClick={onClick}
|
||||
{...rest}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="learning.upgradeButton.buttonText"
|
||||
defaultMessage="Upgrade ({pricing})"
|
||||
values={{
|
||||
pricing: (
|
||||
<FormattedPricing
|
||||
offer={offer}
|
||||
verifiedMode={verifiedMode}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
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);
|
||||
7
src/generic/upgrade-button/index.js
Normal file
7
src/generic/upgrade-button/index.js
Normal file
@@ -0,0 +1,7 @@
|
||||
import FormattedPricing from './FormattedPricing';
|
||||
import UpgradeButton from './UpgradeButton';
|
||||
|
||||
export {
|
||||
FormattedPricing,
|
||||
UpgradeButton,
|
||||
};
|
||||
14
src/generic/upgrade-button/messages.js
Normal file
14
src/generic/upgrade-button/messages.js
Normal file
@@ -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;
|
||||
Reference in New Issue
Block a user