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:
Matthew Piatetsky
2021-05-11 14:06:03 -04:00
committed by GitHub
parent 0f69ed5502
commit d0bcb19754
13 changed files with 283 additions and 65 deletions

View File

@@ -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) => {

View File

@@ -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>
&nbsp;{intl.formatMessage(messages.streakDiscountMessage)}&nbsp;
<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);

View File

@@ -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 {

View File

@@ -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, youre on a roll!');
expect(screen.getByText('3 day streak')).toBeInTheDocument();
expect(screen.getByText('Keep it up, youre 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('Youve 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();
});
});

View File

@@ -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: 'Youve 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;