diff --git a/src/course-home/data/__snapshots__/redux.test.js.snap b/src/course-home/data/__snapshots__/redux.test.js.snap index 3f60ebef..62fd74d6 100644 --- a/src/course-home/data/__snapshots__/redux.test.js.snap +++ b/src/course-home/data/__snapshots__/redux.test.js.snap @@ -59,6 +59,11 @@ Object { }, ], "title": "Demonstration Course", + "verifiedMode": Object { + "currencySymbol": "$", + "price": 10, + "upgradeUrl": "test", + }, }, }, "dates": Object { @@ -347,6 +352,11 @@ Object { }, ], "title": "Demonstration Course", + "verifiedMode": Object { + "currencySymbol": "$", + "price": 10, + "upgradeUrl": "test", + }, }, }, "outline": Object { diff --git a/src/course-home/outline-tab/widgets/UpgradeCard.test.jsx b/src/course-home/outline-tab/widgets/UpgradeCard.test.jsx index 4e80aae3..ff54f214 100644 --- a/src/course-home/outline-tab/widgets/UpgradeCard.test.jsx +++ b/src/course-home/outline-tab/widgets/UpgradeCard.test.jsx @@ -145,7 +145,7 @@ describe('Upgrade Card', () => { expect(screen.getByText(/Unlock your access/s).textContent).toMatch('Unlock your access to all course activities, including graded assignments'); expect(screen.getByText(/to course content and materials/s).textContent).toMatch('Full access to course content and materials, even after the course ends'); expect(screen.getByText(/Support our.*?at edX/s).textContent).toMatch('Support our non-profit mission at edX'); - expect(screen.getByText(/Upgrade for/).textContent).toMatch('126.65 (149)'); + expect(screen.getByText(/Upgrade for/).textContent).toMatch('$126.65 ($149)'); expect(screen.getByText(/Use code.*?at checkout/s).textContent).toMatch('Use code Welcome15 at checkout'); }); @@ -174,7 +174,7 @@ describe('Upgrade Card', () => { expect(screen.getByText(/Unlock your access/s).textContent).toMatch('Unlock your access to all course activities, including graded assignments'); expect(screen.getByText(/to course content and materials/s).textContent).toMatch('Full access to course content and materials, even after the course ends'); expect(screen.getByText(/Support our.*?at edX/s).textContent).toMatch('Support our non-profit mission at edX'); - expect(screen.getByText(/Upgrade for/).textContent).toMatch('126.65 (149)'); + expect(screen.getByText(/Upgrade for/).textContent).toMatch('$126.65 ($149)'); expect(screen.getByText(/Use code.*?at checkout/s).textContent).toMatch('Use code Welcome15 at checkout'); }); @@ -203,7 +203,7 @@ describe('Upgrade Card', () => { expect(screen.getByText(/Unlock your access/s).textContent).toMatch('Unlock your access to all course activities, including graded assignments'); expect(screen.getByText(/to course content and materials/s).textContent).toMatch('Full access to course content and materials, even after the course ends'); expect(screen.getByText(/Support our.*?at edX/s).textContent).toMatch('Support our non-profit mission at edX'); - expect(screen.getByText(/Upgrade for/).textContent).toMatch('126.65 (149)'); + expect(screen.getByText(/Upgrade for/).textContent).toMatch('$126.65 ($149)'); expect(screen.getByText(/Use code.*?at checkout/s).textContent).toMatch('Use code Welcome15 at checkout'); }); @@ -230,7 +230,7 @@ describe('Upgrade Card', () => { expect(screen.getByText('5 days left')).toBeInTheDocument(); // setting the time to 12 will mean that it's slightly less than 12 expect(screen.getByText(/You will lose all access to this course.*?on/s).textContent).toMatch('You will lose all access to this course, including any progress, on April 18.'); expect(screen.getByText(/Upgrading your course enables you/s).textContent).toMatch('Upgrading your course enables you to pursue a verified certificate and unlocks numerous features. Learn more about the benefits of upgrading.'); - expect(screen.getByText(/Upgrade for/).textContent).toMatch('126.65 (149)'); + expect(screen.getByText(/Upgrade for/).textContent).toMatch('$126.65 ($149)'); expect(screen.getByText(/Use code.*?at checkout/s).textContent).toMatch('Use code Welcome15 at checkout'); }); }); diff --git a/src/generic/upgrade-button/FormattedPricing.jsx b/src/generic/upgrade-button/FormattedPricing.jsx index a1e061f9..d13bc477 100644 --- a/src/generic/upgrade-button/FormattedPricing.jsx +++ b/src/generic/upgrade-button/FormattedPricing.jsx @@ -12,9 +12,13 @@ function FormattedPricing(props) { verifiedMode, } = props; + let currencySymbol; + if (verifiedMode) { + currencySymbol = verifiedMode.currencySymbol; + } + if (!offer) { const { - currencySymbol, price, } = verifiedMode; return `${currencySymbol}${price}`; @@ -49,7 +53,7 @@ function FormattedPricing(props) { {intl.formatMessage(messages.srPrices, { discountedPrice, originalPrice })} > ); diff --git a/src/generic/upgrade-button/UpgradeButton.jsx b/src/generic/upgrade-button/UpgradeButton.jsx index 84ff94e1..6be38d52 100644 --- a/src/generic/upgrade-button/UpgradeButton.jsx +++ b/src/generic/upgrade-button/UpgradeButton.jsx @@ -9,6 +9,7 @@ function UpgradeButton(props) { const { intl, offer, + variant, onClick, verifiedMode, ...rest @@ -19,7 +20,7 @@ function UpgradeButton(props) { return ( + ); +} + +UpgradeNowButton.defaultProps = { + offer: null, + onClick: null, + variant: 'primary', +}; + +UpgradeNowButton.propTypes = { + intl: intlShape.isRequired, + offer: PropTypes.shape({ + upgradeUrl: PropTypes.string.isRequired, + }), + onClick: PropTypes.func, + verifiedMode: PropTypes.shape({ + upgradeUrl: PropTypes.string.isRequired, + }).isRequired, + variant: PropTypes.string, +}; + +export default injectIntl(UpgradeNowButton); diff --git a/src/generic/upgrade-button/index.js b/src/generic/upgrade-button/index.js index 845dc5cf..0d81f3af 100644 --- a/src/generic/upgrade-button/index.js +++ b/src/generic/upgrade-button/index.js @@ -1,7 +1,9 @@ import FormattedPricing from './FormattedPricing'; import UpgradeButton from './UpgradeButton'; +import UpgradeNowButton from './UpgradeNowButton'; export { FormattedPricing, UpgradeButton, + UpgradeNowButton, }; diff --git a/src/setupTest.js b/src/setupTest.js index 7f35ba45..745bb50f 100755 --- a/src/setupTest.js +++ b/src/setupTest.js @@ -36,6 +36,12 @@ window.getComputedStyle = jest.fn(() => ({ getPropertyValue: jest.fn(), })); +// Mock Intersection Observer which is unavailable in the context of a test. +global.IntersectionObserver = jest.fn(function mockIntersectionObserver() { + this.observe = jest.fn(); + this.disconnect = jest.fn(); +}); + // Mock media queries because any component that uses `react-break` for responsive breakpoints will // run into `TypeError: window.matchMedia is not a function`. This avoids that for all of our tests now. Object.defineProperty(window, 'matchMedia', { diff --git a/src/shared/data/__factories__/courseMetadataBase.factory.js b/src/shared/data/__factories__/courseMetadataBase.factory.js index a5914b9c..9c822cfb 100644 --- a/src/shared/data/__factories__/courseMetadataBase.factory.js +++ b/src/shared/data/__factories__/courseMetadataBase.factory.js @@ -12,6 +12,11 @@ export default new Factory() original_user_is_staff: false, number: 'DemoX', org: 'edX', + verifiedMode: { + upgradeUrl: 'test', + price: 10, + currencySymbol: '$', + }, }) .attr( 'tabs', ['id', 'host'], (id, host) => { diff --git a/src/shared/streak-celebration/StreakCelebrationModal.jsx b/src/shared/streak-celebration/StreakCelebrationModal.jsx index 34894108..f4d6020f 100644 --- a/src/shared/streak-celebration/StreakCelebrationModal.jsx +++ b/src/shared/streak-celebration/StreakCelebrationModal.jsx @@ -1,10 +1,15 @@ import React, { useEffect, useState } from 'react'; import PropTypes from 'prop-types'; -import { FormattedMessage, injectIntl, intlShape } from '@edx/frontend-platform/i18n'; -import { Lightbulb } from '@edx/paragon/icons'; -import { Icon, Modal } from '@edx/paragon'; +import { + FormattedMessage, injectIntl, intlShape, +} from '@edx/frontend-platform/i18n'; +import { Lightbulb, MoneyFilled } from '@edx/paragon/icons'; +import { + Alert, Icon, ModalDialog, +} from '@edx/paragon'; import { layoutGenerator } from 'react-break'; import { useDispatch } from 'react-redux'; +import { UpgradeNowButton } from '../../generic/upgrade-button'; import { useModel } from '../../generic/model-store'; import StreakMobileImage from './assets/Streak_mobile.png'; @@ -37,7 +42,8 @@ function getRandomFactoid(intl, streakLength) { } function StreakModal({ - courseId, metadataModel, streakLengthToCelebrate, intl, open, ...rest + courseId, metadataModel, streakLengthToCelebrate, intl, isStreakCelebrationOpen, + closeStreakCelebration, AA759ExperimentEnabled, verifiedMode, ...rest }) { const { org, celebrations } = useModel(metadataModel, courseId); const factoid = getRandomFactoid(intl, streakLengthToCelebrate); @@ -54,10 +60,10 @@ function StreakModal({ const dispatch = useDispatch(); useEffect(() => { - if (open) { + if (isStreakCelebrationOpen) { recordStreakCelebration(org, courseId); } - }, [open, org, courseId]); + }, [isStreakCelebrationOpen, org, courseId]); function CloseText() { return ( @@ -68,43 +74,112 @@ function StreakModal({ ); } + const upgradeUrl = `${verifiedMode.upgradeUrl}&code=3DayStreak`; + const mode = { + currencySymbol: verifiedMode.currencySymbol, + price: verifiedMode.price, + upgradeUrl, + }; + + const offer = { + discountedPrice: (mode.price * 0.85).toFixed(2).toString(), + originalPrice: mode.price.toString(), + upgradeUrl: mode.upgradeUrl, + }; + + const title = `${streakLengthToCelebrate} ${intl.formatMessage(messages.streakHeader)}`; + return ( -
{intl.formatMessage(messages.streakBody)}
-
-
-
-
{intl.formatMessage(messages.streakBody)}
+
+
+
+