feat: Set up streak celebration discount experiment (#431)
As part of this work, the streak celebration has been migrated from a Paragon Modal to a Modal Dialog AA-759
This commit is contained in:
committed by
GitHub
parent
0f69ed5502
commit
d0bcb19754
@@ -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) => {
|
||||
|
||||
@@ -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 (
|
||||
<div>
|
||||
<Modal
|
||||
dialogClassName="streak-modal modal-dialog-centered"
|
||||
body={(
|
||||
<>
|
||||
<p>{intl.formatMessage(messages.streakBody)}</p>
|
||||
<p className="modal-image">
|
||||
<OnMobile>
|
||||
<img src={StreakMobileImage} alt="" className="img-fluid" />
|
||||
</OnMobile>
|
||||
<OnDesktop>
|
||||
<img src={StreakDesktopImage} alt="" className="img-fluid" />
|
||||
</OnDesktop>
|
||||
</p>
|
||||
<div className="row mt-3 mx-3 py-3 bg-light-300">
|
||||
<Icon className="col-small ml-3" src={Lightbulb} />
|
||||
<div className="col-11 factoid-wrapper">
|
||||
{randomFactoid}
|
||||
</div>
|
||||
<ModalDialog
|
||||
className="streak-modal modal-dialog-centered"
|
||||
title={title}
|
||||
onClose={() => {
|
||||
closeStreakCelebration();
|
||||
recordModalClosing(metadataModel, celebrations, org, courseId, dispatch);
|
||||
}}
|
||||
isOpen={isStreakCelebrationOpen}
|
||||
isFullscreenScroll
|
||||
{...rest}
|
||||
>
|
||||
<ModalDialog.Header className="modal-header">
|
||||
<ModalDialog.Title className="mr-0 modal-title">
|
||||
{title}
|
||||
</ModalDialog.Title>
|
||||
</ModalDialog.Header>
|
||||
<ModalDialog.Body className="modal-body">
|
||||
<p>{intl.formatMessage(messages.streakBody)}</p>
|
||||
<p className="modal-image">
|
||||
<OnMobile>
|
||||
<img src={StreakMobileImage} alt="" className="img-fluid" />
|
||||
</OnMobile>
|
||||
<OnDesktop>
|
||||
<img src={StreakDesktopImage} alt="" className="img-fluid" />
|
||||
</OnDesktop>
|
||||
</p>
|
||||
{ !AA759ExperimentEnabled && (
|
||||
<div className="d-flex py-3 bg-light-300">
|
||||
<Icon className="col-small ml-3" src={Lightbulb} />
|
||||
<div className="col-11 factoid-wrapper">
|
||||
{randomFactoid}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{ AA759ExperimentEnabled && (
|
||||
<Alert variant="success" className="px-0 d-flex">
|
||||
<Icon className="col-small ml-3 text-success-500" src={MoneyFilled} />
|
||||
<div className="col-11 factoid-wrapper">
|
||||
<b>{intl.formatMessage(messages.congratulations)}</b>
|
||||
{intl.formatMessage(messages.streakDiscountMessage)}
|
||||
<FormattedMessage
|
||||
id="learning.streakCelebration.streakAA759EndDateMessage"
|
||||
defaultMessage="Ends {date}."
|
||||
values={{
|
||||
date: new Date('2021-6-25 00:00').toLocaleDateString({ timeZone: 'UTC' }),
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</Alert>
|
||||
)}
|
||||
</ModalDialog.Body>
|
||||
<ModalDialog.Footer className="modal-footer d-block">
|
||||
{ AA759ExperimentEnabled && (
|
||||
<>
|
||||
<OnMobile>
|
||||
<UpgradeNowButton
|
||||
className="upgrade mb-3"
|
||||
size="sm"
|
||||
offer={offer}
|
||||
variant="brand"
|
||||
verifiedMode={mode}
|
||||
/>
|
||||
<ModalDialog.CloseButton variant="outline-brand" className="btn-sm">
|
||||
{intl.formatMessage(messages.streakButtonAA759)}
|
||||
</ModalDialog.CloseButton>
|
||||
</OnMobile>
|
||||
<OnDesktop>
|
||||
<UpgradeNowButton
|
||||
className="upgrade mb-3"
|
||||
offer={offer}
|
||||
variant="brand"
|
||||
verifiedMode={mode}
|
||||
/>
|
||||
<ModalDialog.CloseButton variant="outline-brand">
|
||||
{intl.formatMessage(messages.streakButtonAA759)}
|
||||
</ModalDialog.CloseButton>
|
||||
</OnDesktop>
|
||||
</>
|
||||
)}
|
||||
closeText={<CloseText />}
|
||||
onClose={() => {
|
||||
recordModalClosing(metadataModel, celebrations, org, courseId, dispatch);
|
||||
}}
|
||||
open={open}
|
||||
title={`${streakLengthToCelebrate} ${intl.formatMessage(messages.streakHeader)}`}
|
||||
{...rest}
|
||||
/>
|
||||
</div>
|
||||
{ !AA759ExperimentEnabled && (
|
||||
<ModalDialog.CloseButton className="px-5" variant="primary"><CloseText /></ModalDialog.CloseButton>
|
||||
)}
|
||||
</ModalDialog.Footer>
|
||||
</ModalDialog>
|
||||
);
|
||||
}
|
||||
|
||||
StreakModal.defaultProps = {
|
||||
open: false,
|
||||
isStreakCelebrationOpen: false,
|
||||
AA759ExperimentEnabled: false,
|
||||
};
|
||||
|
||||
StreakModal.propTypes = {
|
||||
@@ -112,7 +187,14 @@ StreakModal.propTypes = {
|
||||
metadataModel: PropTypes.string.isRequired,
|
||||
streakLengthToCelebrate: PropTypes.number.isRequired,
|
||||
intl: intlShape.isRequired,
|
||||
open: PropTypes.bool,
|
||||
isStreakCelebrationOpen: PropTypes.bool,
|
||||
closeStreakCelebration: PropTypes.func.isRequired,
|
||||
AA759ExperimentEnabled: PropTypes.bool,
|
||||
verifiedMode: PropTypes.shape({
|
||||
currencySymbol: PropTypes.string.isRequired,
|
||||
price: PropTypes.number.isRequired,
|
||||
upgradeUrl: PropTypes.string.isRequired,
|
||||
}).isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(StreakModal);
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
text-align: center;
|
||||
|
||||
.modal-header {
|
||||
padding-top: 1.875rem;
|
||||
padding-bottom: 0;
|
||||
border-bottom: 0; // override default hr line
|
||||
justify-content: center;
|
||||
@@ -19,6 +20,7 @@
|
||||
|
||||
.modal-title {
|
||||
padding-top: 1.25rem;
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
@@ -29,12 +31,6 @@
|
||||
.modal-footer {
|
||||
border-top: 0; // override default hr line
|
||||
justify-content: center;
|
||||
|
||||
button {
|
||||
@extend .btn-primary;
|
||||
font-size: 1.2rem;
|
||||
width: 50%;
|
||||
}
|
||||
}
|
||||
|
||||
.modal-image {
|
||||
|
||||
@@ -10,18 +10,19 @@ describe('Loaded Tab Page', () => {
|
||||
const mockData = { metadataModel: 'coursewareMeta' };
|
||||
|
||||
beforeAll(async () => {
|
||||
mockData.open = true;
|
||||
mockData.isStreakCelebrationOpen = true;
|
||||
mockData.streakLengthToCelebrate = 3;
|
||||
});
|
||||
|
||||
it('shows streak celebration modal', async () => {
|
||||
const courseMetadata = Factory.build('courseMetadata', { celebrations: { shouldCelebrateStreak: true } });
|
||||
const courseMetadata = Factory.build('courseMetadata', { celebrations: { streakLengthToCelebrate: 3 } });
|
||||
mockData.courseId = courseMetadata.id;
|
||||
mockData.verifiedMode = courseMetadata.verifiedMode;
|
||||
const testStore = await initializeTestStore({ courseMetadata }, false);
|
||||
render(<StreakModal {...mockData} courseId={courseMetadata.id} />, { store: testStore });
|
||||
|
||||
await screen.findByText('3 day streak');
|
||||
await screen.findByText('Keep it up, you’re on a roll!');
|
||||
expect(screen.getByText('3 day streak')).toBeInTheDocument();
|
||||
expect(screen.getByText('Keep it up, you’re on a roll!')).toBeInTheDocument();
|
||||
expect(sendTrackEvent).toHaveBeenCalledTimes(1);
|
||||
expect(sendTrackEvent).toHaveBeenCalledWith('edx.ui.lms.celebration.streak.opened', {
|
||||
org_key: courseMetadata.org,
|
||||
@@ -29,4 +30,32 @@ describe('Loaded Tab Page', () => {
|
||||
is_staff: false,
|
||||
});
|
||||
});
|
||||
|
||||
it('shows streak celebration modal AA-759 experiment', async () => {
|
||||
Object.defineProperty(window, 'matchMedia', {
|
||||
writable: true,
|
||||
value: jest.fn().mockImplementation(query => {
|
||||
const matches = !!(query === 'screen and (min-width: 575px)');
|
||||
return {
|
||||
matches,
|
||||
media: query,
|
||||
onchange: null,
|
||||
addListener: jest.fn(), // deprecated
|
||||
removeListener: jest.fn(), // deprecated
|
||||
addEventListener: jest.fn(),
|
||||
removeEventListener: jest.fn(),
|
||||
dispatchEvent: jest.fn(),
|
||||
};
|
||||
}),
|
||||
});
|
||||
const courseMetadata = Factory.build('courseMetadata', { celebrations: { shouldCelebrateStreak: 3 } });
|
||||
mockData.courseId = courseMetadata.id;
|
||||
mockData.verifiedMode = courseMetadata.verifiedMode;
|
||||
mockData.AA759ExperimentEnabled = true;
|
||||
const testStore = await initializeTestStore({ courseMetadata }, false);
|
||||
render(<StreakModal {...mockData} courseId={courseMetadata.id} />, { store: testStore });
|
||||
expect(screen.getByText('You’ve unlocked a 15% off discount when you upgrade this course for a limited time only.')).toBeInTheDocument();
|
||||
expect(screen.getByText('Ends 6/25/2021.')).toBeInTheDocument();
|
||||
expect(screen.getByText('Continue with course')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import { defineMessages } from '@edx/frontend-platform/i18n';
|
||||
|
||||
const messages = defineMessages({
|
||||
streakHeader: {
|
||||
id: 'learning.streakCelebration.header',
|
||||
defaultMessage: 'day streak',
|
||||
description: 'Will come after a number. For example, 3 day streak',
|
||||
congratulations: {
|
||||
id: 'learning.streakCelebration.congratulations',
|
||||
defaultMessage: 'Congratulations!',
|
||||
},
|
||||
streakBody: {
|
||||
id: 'learning.streakCelebration.body',
|
||||
@@ -19,6 +18,15 @@ const messages = defineMessages({
|
||||
defaultMessage: 'Close modal and continue',
|
||||
description: 'Screenreader label for streakButton text',
|
||||
},
|
||||
streakButtonAA759: {
|
||||
id: 'learning.streakCelebration.buttonAA759',
|
||||
defaultMessage: 'Continue with course',
|
||||
},
|
||||
streakHeader: {
|
||||
id: 'learning.streakCelebration.header',
|
||||
defaultMessage: 'day streak',
|
||||
description: 'Will come after a number. For example, 3 day streak',
|
||||
},
|
||||
streakFactoidABoldedSection: {
|
||||
id: 'learning.streakCelebration.factoidABoldedSection',
|
||||
defaultMessage: 'are 20x more likely to pass their course',
|
||||
@@ -29,6 +37,11 @@ const messages = defineMessages({
|
||||
defaultMessage: 'complete 5x as much course content on average',
|
||||
description: 'This bolded section is in the following sentence: Users who learn 3 days in a row {bolded_section} vs. those who don\'t.',
|
||||
},
|
||||
streakDiscountMessage: {
|
||||
id: 'learning.streakCelebration.streakDiscountMessage',
|
||||
defaultMessage: 'You’ve unlocked a 15% off discount when you upgrade this course for a limited time only.',
|
||||
description: 'This message describes a discount the user becomes eligible for when they hit their three day streak',
|
||||
},
|
||||
});
|
||||
|
||||
export default messages;
|
||||
|
||||
Reference in New Issue
Block a user