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

View File

@@ -16,23 +16,35 @@ export const CourseCardDetails = ({ cardId }) => {
openSessionModal, openSessionModal,
courseNumber, courseNumber,
changeOrLeaveSessionMessage, changeOrLeaveSessionMessage,
openCertificatePreview,
} = useCardDetailsData({ cardId }); } = useCardDetailsData({ cardId });
return ( return (
<span className="small" data-testid="CourseCardDetails"> <div>
{providerName} {courseNumber} <span className="small" data-testid="CourseCardDetails">
{!(isEntitlement && !isFulfilled) && accessMessage && ( {providerName} {courseNumber}
`${accessMessage}` {!(isEntitlement && !isFulfilled) && accessMessage && (
)} `${accessMessage}`
{isEntitlement && isFulfilled && canChange ? ( )}
<> {isEntitlement && isFulfilled && canChange ? (
{' • '} <>
<Button variant="link" size="inline" className="m-0 p-0" onClick={openSessionModal}> {' • '}
{changeOrLeaveSessionMessage} <Button variant="link" size="inline" className="m-0 p-0" onClick={openSessionModal}>
</Button> {changeOrLeaveSessionMessage}
</> </Button>
) : null} </>
</span> ) : 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 { StrictDict } from '@edx/react-unit-test-utils';
import EmailSettingsModal from 'containers/EmailSettingsModal'; import EmailSettingsModal from 'containers/EmailSettingsModal';
import CertificatePreviewModal from 'containers/CertificatePreviewModal';
import UnenrollConfirmModal from 'containers/UnenrollConfirmModal'; import UnenrollConfirmModal from 'containers/UnenrollConfirmModal';
import { reduxHooks } from 'hooks'; import { reduxHooks } from 'hooks';
import SocialShareMenu from './SocialShareMenu'; import SocialShareMenu from './SocialShareMenu';
@@ -32,6 +33,7 @@ export const CourseCardMenu = ({ cardId }) => {
const { shouldShowUnenrollItem, shouldShowDropdown } = useOptionVisibility(cardId); const { shouldShowUnenrollItem, shouldShowDropdown } = useOptionVisibility(cardId);
const { isMasquerading } = reduxHooks.useMasqueradeData(); const { isMasquerading } = reduxHooks.useMasqueradeData();
const { isEmailEnabled } = reduxHooks.useCardEnrollmentData(cardId); const { isEmailEnabled } = reduxHooks.useCardEnrollmentData(cardId);
const showCertificatePreviewModal = reduxHooks.useShowCertificatePreviewModal(cardId);
if (!shouldShowDropdown) { if (!shouldShowDropdown) {
return null; return null;
@@ -73,6 +75,11 @@ export const CourseCardMenu = ({ cardId }) => {
cardId={cardId} cardId={cardId}
/> />
)} )}
{showCertificatePreviewModal && (
<CertificatePreviewModal
cardId={cardId}
/>
)}
</> </>
); );
}; };

View File

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

View File

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

View File

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

View File

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