Compare commits

...

1 Commits

Author SHA1 Message Date
Simon Chen
cdb0f48f08 feat!: upsell through a course cert preview pop up
Even if the learner is enrolled in the course as a audit learner, each course cards would show a link that, once clicked upon, will pop a modal with verified cert preview and upgrade button
2023-10-25 13:37:58 -04:00
13 changed files with 152 additions and 23 deletions

2
.env
View File

@@ -41,4 +41,4 @@ ACCOUNT_PROFILE_URL=''
ENABLE_NOTICES=''
CAREER_LINK_URL=''
OPTIMIZELY_FULL_STACK_SDK_KEY=''
EXPERIMENT_08_23_VAN_PAINTED_DOOR=true
EXPERIMENT_08_23_VAN_PAINTED_DOOR='true'

View File

@@ -48,3 +48,4 @@ ACCOUNT_PROFILE_URL='http://localhost:1995'
ENABLE_NOTICES=''
CAREER_LINK_URL=''
OPTIMIZELY_FULL_STACK_SDK_KEY=''
EXPERIMENT_08_23_VAN_PAINTED_DOOR=true

View File

@@ -0,0 +1,29 @@
import { getConfig } from '@edx/frontend-platform';
import { useIntl } from '@edx/frontend-platform/i18n';
import { reduxHooks } from 'hooks';
import messages from './messages';
export const useCertificatePreviewData = () => {
const { formatMessage } = useIntl();
const selectedCardId = reduxHooks.useCertificatePreviewData().cardId;
const { courseId } = reduxHooks.useCardCourseRunData(selectedCardId) || {};
const courseTitle = courseId;
const header = formatMessage(messages.previewTitle, { courseTitle });
const closePreviewModal = reduxHooks.useUpdateCertificatePreviewModalCallback(null);
const getCertificatePreviewUrl = () => `${getConfig().LMS_BASE_URL}/certificates/upsell/course/${courseId}`;
return {
showModal: selectedCardId != null,
header,
closePreviewModal,
getCertificatePreviewUrl,
};
};
export default useCertificatePreviewData;

View File

@@ -0,0 +1,48 @@
import React from 'react';
import PropTypes from 'prop-types';
import {
ModalDialog,
} from '@edx/paragon';
import { UpgradeButton } from '../CourseCard/components/CourseCardActions/UpgradeButton';
import useCertificatePreviewData from './hooks';
export const CertificatePreviewModal = ({
cardId,
}) => {
const {
showModal,
header,
closePreviewModal,
getCertificatePreviewUrl,
} = useCertificatePreviewData();
return (
<ModalDialog
isOpen={showModal}
onClose={closePreviewModal}
hasCloseButton
isFullscreenOnMobile
size="lg"
className="p-4 px-4.5"
title={header}
>
<h3>{header}</h3>
<div>
<iframe
title={header}
src={getCertificatePreviewUrl()}
width={725}
height={400}
/>
</div>
<UpgradeButton
cardId={cardId}
/>
</ModalDialog>
);
};
CertificatePreviewModal.propTypes = {
cardId: PropTypes.string.isRequired,
};
export default CertificatePreviewModal;

View File

@@ -0,0 +1,12 @@
/* eslint-disable quotes */
import { StrictDict } from 'utils';
export const messages = StrictDict({
previewTitle: {
id: 'learner-dash.certificatePreview.title',
description: 'The title of the email settings modal',
defaultMessage: 'Your certificate preview for {courseTitle} ',
},
});
export default messages;

View File

@@ -47,6 +47,7 @@ export const useCardDetailsData = ({ cardId }) => {
} = reduxHooks.useCardEntitlementData(cardId);
const openSessionModal = reduxHooks.useUpdateSelectSessionModalCallback(cardId);
const openCertificatePreview = reduxHooks.useUpdateCertificatePreviewModalCallback(cardId);
return {
providerName: providerName || formatMessage(messages.unknownProviderName),
@@ -57,6 +58,7 @@ export const useCardDetailsData = ({ cardId }) => {
openSessionModal,
courseNumber,
changeOrLeaveSessionMessage: formatMessage(messages.changeOrLeaveSessionButton),
openCertificatePreview,
};
};

View File

@@ -16,23 +16,35 @@ export const CourseCardDetails = ({ cardId }) => {
openSessionModal,
courseNumber,
changeOrLeaveSessionMessage,
openCertificatePreview,
} = useCardDetailsData({ cardId });
return (
<span className="small" data-testid="CourseCardDetails">
{providerName} {courseNumber}
{!(isEntitlement && !isFulfilled) && accessMessage && (
`${accessMessage}`
)}
{isEntitlement && isFulfilled && canChange ? (
<>
{' • '}
<Button variant="link" size="inline" className="m-0 p-0" onClick={openSessionModal}>
{changeOrLeaveSessionMessage}
</Button>
</>
) : null}
</span>
<div>
<span className="small" data-testid="CourseCardDetails">
{providerName} {courseNumber}
{!(isEntitlement && !isFulfilled) && accessMessage && (
`${accessMessage}`
)}
{isEntitlement && isFulfilled && canChange ? (
<>
{' • '}
<Button variant="link" size="inline" className="m-0 p-0" onClick={openSessionModal}>
{changeOrLeaveSessionMessage}
</Button>
</>
) : null}
</span>
<Button
variant="link"
size="inline"
className="float-right"
data-testid="certificate-preview"
onClick={openCertificatePreview}
>
Preview Your Certificate
</Button>
</div>
);
};

View File

@@ -7,6 +7,7 @@ import { MoreVert } from '@edx/paragon/icons';
import { StrictDict } from '@edx/react-unit-test-utils';
import EmailSettingsModal from 'containers/EmailSettingsModal';
import CertificatePreviewModal from 'containers/CertificatePreviewModal';
import UnenrollConfirmModal from 'containers/UnenrollConfirmModal';
import { reduxHooks } from 'hooks';
import SocialShareMenu from './SocialShareMenu';
@@ -32,6 +33,7 @@ export const CourseCardMenu = ({ cardId }) => {
const { shouldShowUnenrollItem, shouldShowDropdown } = useOptionVisibility(cardId);
const { isMasquerading } = reduxHooks.useMasqueradeData();
const { isEmailEnabled } = reduxHooks.useCardEnrollmentData(cardId);
const showCertificatePreviewModal = reduxHooks.useShowCertificatePreviewModal(cardId);
if (!shouldShowDropdown) {
return null;
@@ -73,6 +75,11 @@ export const CourseCardMenu = ({ cardId }) => {
cardId={cardId}
/>
)}
{showCertificatePreviewModal && (
<CertificatePreviewModal
cardId={cardId}
/>
)}
</>
);
};

View File

@@ -6,14 +6,12 @@ import PaintedDoorExperimentProvider from 'widgets/RecommendationsPaintedDoorBtn
export const AppWrapper = ({
children,
}) => {
if (process.env.EXPERIMENT_08_23_VAN_PAINTED_DOOR) {
return (
<PaintedDoorExperimentProvider>
{children}
</PaintedDoorExperimentProvider>
);
}
return children;
console.log(`process.env.EXPERIMENT_08_23_VAN_PAINTED_DOOR = ${Boolean(process.env.EXPERIMENT_08_23_VAN_PAINTED_DOOR)}`);
return (
<PaintedDoorExperimentProvider>
{children}
</PaintedDoorExperimentProvider>
);
};
AppWrapper.propTypes = {
children: PropTypes.oneOfType([

View File

@@ -12,6 +12,7 @@ const initialState = {
suggestedCourses: [],
filterState: {},
selectSessionModal: {},
certificatePreviewModal: {},
};
export const cardId = (val) => `card-${val}`;
@@ -48,6 +49,10 @@ const app = createSlice({
...state,
selectSessionModal: { cardId: payload },
}),
updateCertificatePreviewModal: (state, { payload }) => ({
...state,
certificatePreviewModal: { cardId: payload },
}),
setPageNumber: (state, { payload }) => ({ ...state, pageNumber: payload }),
},
});

View File

@@ -19,9 +19,15 @@ export const showSelectSessionModal = createSelector(
(data) => data.cardId != null,
);
export const showCertificatePreviewModal = createSelector(
[simpleSelectors.certificatePreviewModal],
(data) => data.cardId != null,
);
export default StrictDict({
numCourses,
hasCourses,
hasAvailableDashboards,
showSelectSessionModal,
showCertificatePreviewModal,
});

View File

@@ -14,6 +14,7 @@ export const simpleSelectors = StrictDict({
emailConfirmation: mkSimpleSelector(app => app.emailConfirmation),
enterpriseDashboard: mkSimpleSelector(app => app.enterpriseDashboard || {}),
selectSessionModal: mkSimpleSelector(app => app.selectSessionModal),
certificatePreviewModal: mkSimpleSelector(app => app.certificatePreviewModal),
pageNumber: mkSimpleSelector(app => app.pageNumber),
socialShareSettings: mkSimpleSelector(app => app.socialShareSettings),
});

View File

@@ -13,6 +13,8 @@ export const useEmailConfirmationData = () => useSelector(selectors.emailConfirm
export const useEnterpriseDashboardData = () => useSelector(selectors.enterpriseDashboard);
export const usePlatformSettingsData = () => useSelector(selectors.platformSettings);
export const useSelectSessionModalData = () => useSelector(selectors.selectSessionModal);
export const useCertificatePreviewData = () => useSelector(selectors.certificatePreviewModal);
export const useSocialShareSettings = () => useSelector(selectors.socialShareSettings);
/** global-level meta-selectors **/
@@ -22,6 +24,7 @@ export const useCurrentCourseList = (opts) => useSelector(
state => selectors.currentList(state, opts),
);
export const useShowSelectSessionModal = () => useSelector(selectors.showSelectSessionModal);
export const useShowCertificatePreviewModal = () => useSelector(selectors.showCertificatePreviewModal);
// eslint-disable-next-line
export const useCourseCardData = (selector) => (cardId) => useSelector(
@@ -67,6 +70,11 @@ export const useUpdateSelectSessionModalCallback = (cardId) => {
return () => dispatch(actions.updateSelectSessionModal(cardId));
};
export const useUpdateCertificatePreviewModalCallback = (cardId) => {
const dispatch = useDispatch();
return () => dispatch(actions.updateCertificatePreviewModal(cardId));
};
export const useTrackCourseEvent = (tracker, cardId, ...args) => {
const { courseId } = module.useCardCourseRunData(cardId);
return (e) => tracker(courseId, ...args)(e);