From 794cab225875f711bdb1e3149db7821b3e4623c0 Mon Sep 17 00:00:00 2001 From: ilee2u Date: Mon, 9 Dec 2024 17:27:21 -0500 Subject: [PATCH 01/25] feat: add trial days remaining banner - temp commit as I set up the code --- src/components/Sidebar/Sidebar.scss | 4 ++ src/components/Sidebar/index.jsx | 19 ++++++++ src/data/thunks.js | 12 ++++- src/widgets/Xpert.jsx | 75 ++++++++++++++++++++++++++++- 4 files changed, 108 insertions(+), 2 deletions(-) diff --git a/src/components/Sidebar/Sidebar.scss b/src/components/Sidebar/Sidebar.scss index 7dfff12e..48445774 100644 --- a/src/components/Sidebar/Sidebar.scss +++ b/src/components/Sidebar/Sidebar.scss @@ -31,6 +31,10 @@ height: 30px; } } + + .trial-header { + background-color: #F49974; + } } .separator { diff --git a/src/components/Sidebar/index.jsx b/src/components/Sidebar/index.jsx index d9bc6497..5e63e767 100644 --- a/src/components/Sidebar/index.jsx +++ b/src/components/Sidebar/index.jsx @@ -23,6 +23,7 @@ const Sidebar = ({ isOpen, setIsOpen, unitId, + daysRemainingMessage, }) => { const { apiError, @@ -80,11 +81,29 @@ const Sidebar = ({ ); + // const showAuditTrialBanner = () => { + // // if expired, show the banner + // if (Object.keys(daysRemainingMessage).length !== 0) { + // if (auditTrialData.expired) { + // return true; + // } + // return false; + // } + // }; + + // const getAuditTrialBannerMessage = () => { + // return {daysRemainingMessage} + // }; + const getSidebar = () => (
+ {} +
+ {daysRemainingMessage} +
{ + // // if enrollment mode is NOT upgrade eligible, there's no audit trial data + // if (isUpgradeEligible) { + // return None + + // // if enrollment mode IS upgrade eligible, return if the trial is expired + // // TEMP NOTE: A trial is auto-created if one does not yet exist so ideally this + // // returns true in the case that xpert is used for the first time in a course + // // the user is upgrade eligible in + // } else { + // // const auditTrialExpirationDate = new Date(auditTrial.expirationDate); + // // temp just to test this out + // const auditTrialExpirationDate = Date.now() + 1; + + // const daysRemaining = Math.ceil(auditTrialExpirationDate - Date.now()) + // console.log("daysRemaining", daysRemaining); + // if (daysRemaining > 0) { + // console.log("YEET") + // setAuditTrialDaysRemaining(daysRemaining); + // // return { + // // expired: false, + // // daysRemaining: daysRemaining, + // // }; + // } + // setAuditTrialDaysRemaining(0); + // // return { + // // expired: true, + // // daysRemaining: daysRemaining, + // // }; + // } + // }, [auditTrial]); + + const getDaysRemainingMessage = () => { + // if enrollment mode is NOT upgrade eligible, there's no audit trial data + if (isUpgradeEligible) { + return None + + // if enrollment mode IS upgrade eligible, return if the trial is expired + // TEMP NOTE: A trial is auto-created if one does not yet exist so ideally this + // returns true in the case that xpert is used for the first time in a course + // the user is upgrade eligible in + } else { + const auditTrialExpirationDate = new Date(auditTrial.expirationDate); + const oneDay = 24 * 60 * 60 * 1000; // hours*minutes*seconds*milliseconds + const daysRemaining = Math.ceil((auditTrialExpirationDate - Date.now()) / oneDay); + + // console.log("auditTrial:", auditTrial) + // console.log("auditTrialExpirationDate:", auditTrialExpirationDate) + // console.log("daysRemaining:", daysRemaining) + if (daysRemaining > 1) { + return ( +
+ {daysRemaining} days remaining. Upgrade for full access to Xpert. +
+ ); + } else if (daysRemaining === 1) { + return ( +
+ Your trial ends today! Upgrade for full access to Xpert. +
+ ); + } else { + return ( +
+ Your trial has expired. Upgrade for full access to Xpert. +
+ ); + }; + } + }; + return isEnabled ? ( From 717a0a40dd714d4d875006ec84c967b6a35b9e62 Mon Sep 17 00:00:00 2001 From: ilee2u Date: Tue, 10 Dec 2024 14:18:03 -0500 Subject: [PATCH 02/25] temp: working version w/ debug logs --- src/components/Sidebar/index.jsx | 63 ++++++++++++++++++++------- src/data/api.js | 1 + src/data/thunks.js | 18 ++++---- src/widgets/Xpert.jsx | 73 -------------------------------- 4 files changed, 58 insertions(+), 97 deletions(-) diff --git a/src/components/Sidebar/index.jsx b/src/components/Sidebar/index.jsx index 5e63e767..0bf9cbf3 100644 --- a/src/components/Sidebar/index.jsx +++ b/src/components/Sidebar/index.jsx @@ -23,10 +23,11 @@ const Sidebar = ({ isOpen, setIsOpen, unitId, - daysRemainingMessage, + isUpgradeEligible, }) => { const { apiError, + auditTrial, disclosureAcknowledged, messageList, } = useSelector(state => state.learningAssistant); @@ -81,20 +82,51 @@ const Sidebar = ({ ); - // const showAuditTrialBanner = () => { - // // if expired, show the banner - // if (Object.keys(daysRemainingMessage).length !== 0) { - // if (auditTrialData.expired) { - // return true; - // } - // return false; - // } - // }; - - // const getAuditTrialBannerMessage = () => { - // return {daysRemainingMessage} - // }; + const getDaysRemainingMessage = () => { + // if enrollment mode is NOT upgrade eligible, there's no audit trial data + if (isUpgradeEligible) { + return None + + // if enrollment mode IS upgrade eligible, return if the trial is expired + // TEMP NOTE: A trial is auto-created if one does not yet exist so ideally this + // returns true in the case that xpert is used for the first time in a course + // the user is upgrade eligible in + } else { + const auditTrialExpirationDate = new Date(auditTrial.expirationDate); + const oneDay = 24 * 60 * 60 * 1000; // hours*minutes*seconds*milliseconds + const daysRemaining = Math.ceil((auditTrialExpirationDate - Date.now()) / oneDay); + + // console.log("auditTrial:", auditTrial) + // console.log("auditTrialExpirationDate:", auditTrialExpirationDate) + // console.log("daysRemaining:", daysRemaining) + if (daysRemaining > 1) { + return ( +
+ {daysRemaining} days remaining. Upgrade for full access to Xpert. +
+ ); + } else if (daysRemaining === 1) { + return ( +
+ Your trial ends today! Upgrade for full access to Xpert. +
+ ); + } else { + // TODO: Show the upgrade screen instead of this banner + return ( +
+ Your trial has expired. Upgrade for full access to Xpert. +
+ ); + }; + } + }; + /** + * if no audit trial, and chat message endpoint success, + * we know an audit trial just started so write "[xpert_trial_length] days" + * and re-call the chat summary endpoint + */ const getSidebar = () => (
@@ -102,7 +134,7 @@ const Sidebar = ({
{}
- {daysRemainingMessage} + {getDaysRemainingMessage()}
{ - // // if enrollment mode is NOT upgrade eligible, there's no audit trial data - // if (isUpgradeEligible) { - // return None - - // // if enrollment mode IS upgrade eligible, return if the trial is expired - // // TEMP NOTE: A trial is auto-created if one does not yet exist so ideally this - // // returns true in the case that xpert is used for the first time in a course - // // the user is upgrade eligible in - // } else { - // // const auditTrialExpirationDate = new Date(auditTrial.expirationDate); - // // temp just to test this out - // const auditTrialExpirationDate = Date.now() + 1; - - // const daysRemaining = Math.ceil(auditTrialExpirationDate - Date.now()) - // console.log("daysRemaining", daysRemaining); - // if (daysRemaining > 0) { - // console.log("YEET") - // setAuditTrialDaysRemaining(daysRemaining); - // // return { - // // expired: false, - // // daysRemaining: daysRemaining, - // // }; - // } - // setAuditTrialDaysRemaining(0); - // // return { - // // expired: true, - // // daysRemaining: daysRemaining, - // // }; - // } - // }, [auditTrial]); - - const getDaysRemainingMessage = () => { - // if enrollment mode is NOT upgrade eligible, there's no audit trial data - if (isUpgradeEligible) { - return None - - // if enrollment mode IS upgrade eligible, return if the trial is expired - // TEMP NOTE: A trial is auto-created if one does not yet exist so ideally this - // returns true in the case that xpert is used for the first time in a course - // the user is upgrade eligible in - } else { - const auditTrialExpirationDate = new Date(auditTrial.expirationDate); - const oneDay = 24 * 60 * 60 * 1000; // hours*minutes*seconds*milliseconds - const daysRemaining = Math.ceil((auditTrialExpirationDate - Date.now()) / oneDay); - - // console.log("auditTrial:", auditTrial) - // console.log("auditTrialExpirationDate:", auditTrialExpirationDate) - // console.log("daysRemaining:", daysRemaining) - if (daysRemaining > 1) { - return ( -
- {daysRemaining} days remaining. Upgrade for full access to Xpert. -
- ); - } else if (daysRemaining === 1) { - return ( -
- Your trial ends today! Upgrade for full access to Xpert. -
- ); - } else { - return ( -
- Your trial has expired. Upgrade for full access to Xpert. -
- ); - }; - } - }; - return isEnabled ? ( From 4d196f21195a5ca5f0d34cb3d0d5ec3fd7701524 Mon Sep 17 00:00:00 2001 From: ilee2u Date: Tue, 10 Dec 2024 16:26:38 -0500 Subject: [PATCH 03/25] feat: better functionality for days remaining --- src/components/Sidebar/index.jsx | 14 +++++--------- src/data/api.js | 2 -- src/data/thunks.js | 17 +++++++---------- src/utils/optimizelyExperiment.js | 2 +- 4 files changed, 13 insertions(+), 22 deletions(-) diff --git a/src/components/Sidebar/index.jsx b/src/components/Sidebar/index.jsx index 0bf9cbf3..e3fef907 100644 --- a/src/components/Sidebar/index.jsx +++ b/src/components/Sidebar/index.jsx @@ -82,23 +82,19 @@ const Sidebar = ({ ); + + // Get this to work const getDaysRemainingMessage = () => { // if enrollment mode is NOT upgrade eligible, there's no audit trial data - if (isUpgradeEligible) { - return None + if (!isUpgradeEligible) { + return // if enrollment mode IS upgrade eligible, return if the trial is expired - // TEMP NOTE: A trial is auto-created if one does not yet exist so ideally this - // returns true in the case that xpert is used for the first time in a course - // the user is upgrade eligible in } else { const auditTrialExpirationDate = new Date(auditTrial.expirationDate); const oneDay = 24 * 60 * 60 * 1000; // hours*minutes*seconds*milliseconds const daysRemaining = Math.ceil((auditTrialExpirationDate - Date.now()) / oneDay); - // console.log("auditTrial:", auditTrial) - // console.log("auditTrialExpirationDate:", auditTrialExpirationDate) - // console.log("daysRemaining:", daysRemaining) if (daysRemaining > 1) { return (
@@ -112,7 +108,7 @@ const Sidebar = ({
); } else { - // TODO: Show the upgrade screen instead of this banner + // TODO: Show the upgrade screen instead of this banner, to be done in future ticket return (
Your trial has expired. Upgrade for full access to Xpert. diff --git a/src/data/api.js b/src/data/api.js index 7e47549d..520b6957 100644 --- a/src/data/api.js +++ b/src/data/api.js @@ -16,7 +16,6 @@ async function fetchChatResponse(courseId, messageList, unitId, customQueryParam let queryString = new URLSearchParams(queryParams); queryString = queryString.toString(); - const url = new URL(`${getConfig().CHAT_RESPONSE_URL}/${courseId}?${queryString}`); const { data } = await getAuthenticatedHttpClient().post(url.href, payload); @@ -27,7 +26,6 @@ async function fetchLearningAssistantChatSummary(courseId) { const url = new URL(`${getConfig().CHAT_RESPONSE_URL}/${courseId}/chat-summary`); const { data } = await getAuthenticatedHttpClient().get(url.href); - console.log("DATA:", data) return data; } diff --git a/src/data/thunks.js b/src/data/thunks.js index 7e00ca9e..47eb2c31 100644 --- a/src/data/thunks.js +++ b/src/data/thunks.js @@ -70,6 +70,9 @@ export function getChatResponse(courseId, unitId, promptExperimentVariationKey = dispatch(setApiIsLoading(false)); dispatch(addChatMessage(message.role, message.content, courseId, promptExperimentVariationKey)); + if (message.audit_trial_created) { + dispatch(getLearningAssistantChatSummary(courseId)); + } } catch (error) { dispatch(setApiError()); dispatch(setApiIsLoading(false)); @@ -129,16 +132,10 @@ export function getLearningAssistantChatSummary(courseId) { } // Audit Trial - const auditTrial = data.audit_trial; - - // temp forcing of expiration date value just to test this out - // const startDate = new Date(Date.now()); - // const expirationDate = new Date(); - // expirationDate.setDate(expirationDate.getDate() + 2); - // const auditTrial = { - // startDate: startDate.toISOString(), - // expirationDate: expirationDate.toISOString(), - // }; + const auditTrial = { + startDate: data.audit_trial.start_date, + expirationDate: data.audit_trial.expiration_date, + }; console.log("thunks auditTrial:", auditTrial); // If returned audit trial data is not empty diff --git a/src/utils/optimizelyExperiment.js b/src/utils/optimizelyExperiment.js index 1ca3e241..c9772a82 100644 --- a/src/utils/optimizelyExperiment.js +++ b/src/utils/optimizelyExperiment.js @@ -3,7 +3,7 @@ import { getOptimizely } from '../data/optimizely'; const trackChatBotMessageOptimizely = (userId, userAttributes = {}) => { const optimizelyInstance = getOptimizely(); - if (!optimizelyInstance) { return; } + if (!optimizelyInstance || Object.keys(optimizelyInstance).length === 0) { return; } optimizelyInstance.onReady().then(() => { optimizelyInstance.track('learning_assistant_chat_message', userId, userAttributes); From 43f2c7807477df5875dc4ba70925c7b8535f1547 Mon Sep 17 00:00:00 2001 From: ilee2u Date: Wed, 11 Dec 2024 16:51:19 -0500 Subject: [PATCH 04/25] fix: set auditTrialLengthDays + nits --- src/components/Sidebar/Sidebar.scss | 1 + src/components/Sidebar/index.jsx | 83 ++++++++++++++++------------- src/data/thunks.js | 52 ++++++++++++++++-- 3 files changed, 97 insertions(+), 39 deletions(-) diff --git a/src/components/Sidebar/Sidebar.scss b/src/components/Sidebar/Sidebar.scss index 48445774..8d385146 100644 --- a/src/components/Sidebar/Sidebar.scss +++ b/src/components/Sidebar/Sidebar.scss @@ -33,6 +33,7 @@ } .trial-header { + font-size: 0.9em; background-color: #F49974; } } diff --git a/src/components/Sidebar/index.jsx b/src/components/Sidebar/index.jsx index e3fef907..86df8ab2 100644 --- a/src/components/Sidebar/index.jsx +++ b/src/components/Sidebar/index.jsx @@ -7,7 +7,9 @@ import { } from '@openedx/paragon'; import { Close } from '@openedx/paragon/icons'; -import { useCourseUpgrade } from '../../hooks'; +// Commenting this out for now until we figure out a solution for getting the upgrade url +// import { useModel } from '@src/generic/model-store'; + import showSurvey from '../../utils/surveyMonkey'; import APIError from '../APIError'; @@ -36,6 +38,17 @@ const Sidebar = ({ const chatboxContainerRef = useRef(null); + // Commenting this out for now until we figure out a solution for getting the upgrade url + // const courseHomeMeta = useModel('courseHomeMeta', courseId); + // const { + // verifiedMode, + // } = courseHomeMeta; + + // const course = useModel('coursewareMeta', courseId); + // const { + // offer, + // } = course; + // this use effect is intended to scroll to the bottom of the chat window, in the case // that a message is larger than the chat window height. useEffect(() => { @@ -82,40 +95,36 @@ const Sidebar = ({ ); - // Get this to work const getDaysRemainingMessage = () => { - // if enrollment mode is NOT upgrade eligible, there's no audit trial data - if (!isUpgradeEligible) { - return - - // if enrollment mode IS upgrade eligible, return if the trial is expired - } else { - const auditTrialExpirationDate = new Date(auditTrial.expirationDate); - const oneDay = 24 * 60 * 60 * 1000; // hours*minutes*seconds*milliseconds - const daysRemaining = Math.ceil((auditTrialExpirationDate - Date.now()) / oneDay); - - if (daysRemaining > 1) { - return ( -
- {daysRemaining} days remaining. Upgrade for full access to Xpert. -
- ); - } else if (daysRemaining === 1) { - return ( -
- Your trial ends today! Upgrade for full access to Xpert. -
- ); - } else { - // TODO: Show the upgrade screen instead of this banner, to be done in future ticket - return ( -
- Your trial has expired. Upgrade for full access to Xpert. -
- ); - }; + const auditTrialExpirationDate = new Date(auditTrial.expirationDate); + const oneDay = 24 * 60 * 60 * 1000; // hours*minutes*seconds*milliseconds + const daysRemaining = Math.ceil((auditTrialExpirationDate - Date.now()) / oneDay); + + // Commenting this out for now until we figure out a solution for getting the upgrade url + // const upgradeURL = offer ? offer.upgradeUrl : verifiedMode.upgradeUrl; + const upgradeURL = ''; + + if (daysRemaining > 1) { + const irtl = new Intl.RelativeTimeFormat({ style: 'long' }); + return ( +
+ Your trial ends {irtl.format(daysRemaining, 'day')}. Upgrade for full access to Xpert. +
+ ); + } if (daysRemaining === 1) { + return ( +
+ Your trial ends today! Upgrade for full access to Xpert. +
+ ); } + // TODO: Show the upgrade screen instead of this banner, to be done in future ticket + return ( +
+ Your trial has expired. Upgrade for full access to Xpert. +
+ ); }; /** @@ -128,10 +137,12 @@ const Sidebar = ({
- {} -
- {getDaysRemainingMessage()} -
+ {isUpgradeEligible + && ( +
+ {getDaysRemainingMessage()} +
+ )} { + dispatch(setApiIsLoading(true)); + + try { + const data = await fetchLearningAssistantChatSummary(courseId); + + // Enabled + dispatch(setIsEnabled(data.enabled)); + + // Message History + const rawMessageList = data.message_history; + + // If returned message history data is not empty + if (rawMessageList.length) { + const messageList = rawMessageList + .map(({ timestamp, ...msg }) => ({ + ...msg, + timestamp: new Date(timestamp).toString(), // Parse ISO time to Date() + })); + + dispatch(setMessageList({ messageList })); + + // If it has chat history, then we assume the user already aknowledged. + dispatch(setDisclosureAcknowledged(true)); + } + + // Audit Trial + const auditTrial = { + startDate: data.audit_trial.start_date, + expirationDate: data.audit_trial.expiration_date, + }; + + // If returned audit trial data is not empty + if (Object.keys(auditTrial).length !== 0) { + dispatch(setAuditTrial(auditTrial)); + } + + dispatch(setAuditTrialLengthDays(data.audit_trial_length_days)); + } catch (error) { + dispatch(setApiError()); + } + dispatch(setApiIsLoading(false)); + }; +} + export function addChatMessage(role, content, courseId, promptExperimentVariationKey = undefined) { return (dispatch, getState) => { const { messageList, conversationId } = getState().learningAssistant; @@ -70,9 +116,9 @@ export function getChatResponse(courseId, unitId, promptExperimentVariationKey = dispatch(setApiIsLoading(false)); dispatch(addChatMessage(message.role, message.content, courseId, promptExperimentVariationKey)); - if (message.audit_trial_created) { - dispatch(getLearningAssistantChatSummary(courseId)); - } + // NOTE for self: There could be a case where the user just keeps a tab open to keep + // their trial going. Though this is unlikely, this call prevents the trial from continuing in the UI + dispatch(getLearningAssistantChatSummary(courseId)); } catch (error) { dispatch(setApiError()); dispatch(setApiIsLoading(false)); From 5fa9f47d2fc841d5edbf4b0351a8858771a57c98 Mon Sep 17 00:00:00 2001 From: ilee2u Date: Thu, 12 Dec 2024 12:33:57 -0500 Subject: [PATCH 05/25] feat: add upgrade url to days remaining banner --- src/components/Sidebar/index.jsx | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/src/components/Sidebar/index.jsx b/src/components/Sidebar/index.jsx index 86df8ab2..7778d71d 100644 --- a/src/components/Sidebar/index.jsx +++ b/src/components/Sidebar/index.jsx @@ -1,6 +1,9 @@ import React, { useRef, useEffect } from 'react'; import { useSelector } from 'react-redux'; import PropTypes from 'prop-types'; + +import { useModel } from '@src/generic/model-store'; + import { Icon, IconButton, @@ -38,16 +41,15 @@ const Sidebar = ({ const chatboxContainerRef = useRef(null); - // Commenting this out for now until we figure out a solution for getting the upgrade url - // const courseHomeMeta = useModel('courseHomeMeta', courseId); - // const { - // verifiedMode, - // } = courseHomeMeta; + const courseHomeMeta = useModel('courseHomeMeta', courseId); + const { + verifiedMode, + } = courseHomeMeta; - // const course = useModel('coursewareMeta', courseId); - // const { - // offer, - // } = course; + const course = useModel('coursewareMeta', courseId); + const { + offer, + } = course; // this use effect is intended to scroll to the bottom of the chat window, in the case // that a message is larger than the chat window height. @@ -101,28 +103,26 @@ const Sidebar = ({ const oneDay = 24 * 60 * 60 * 1000; // hours*minutes*seconds*milliseconds const daysRemaining = Math.ceil((auditTrialExpirationDate - Date.now()) / oneDay); - // Commenting this out for now until we figure out a solution for getting the upgrade url - // const upgradeURL = offer ? offer.upgradeUrl : verifiedMode.upgradeUrl; - const upgradeURL = ''; + const upgradeURL = offer ? offer.upgradeUrl : verifiedMode.upgradeUrl; if (daysRemaining > 1) { const irtl = new Intl.RelativeTimeFormat({ style: 'long' }); return (
- Your trial ends {irtl.format(daysRemaining, 'day')}. Upgrade for full access to Xpert. + Your trial ends {irtl.format(daysRemaining, 'day')}. Upgrade for full access to Xpert.
); } if (daysRemaining === 1) { return (
- Your trial ends today! Upgrade for full access to Xpert. + Your trial ends today! Upgrade for full access to Xpert.
); } // TODO: Show the upgrade screen instead of this banner, to be done in future ticket return (
- Your trial has expired. Upgrade for full access to Xpert. + Your trial has expired. Upgrade for full access to Xpert.
); }; From 078db1302d7c6777db2a02485e63e94a768b70cf Mon Sep 17 00:00:00 2001 From: ilee2u Date: Thu, 12 Dec 2024 12:34:14 -0500 Subject: [PATCH 06/25] feat: simplify refresh chat summary call --- src/data/slice.js | 5 ----- src/data/thunks.js | 5 +---- 2 files changed, 1 insertion(+), 9 deletions(-) diff --git a/src/data/slice.js b/src/data/slice.js index fc64c47e..91982422 100644 --- a/src/data/slice.js +++ b/src/data/slice.js @@ -12,7 +12,6 @@ export const initialState = { sidebarIsOpen: false, isEnabled: false, auditTrial: {}, - auditTrialLengthDays: null, }; export const learningAssistantSlice = createSlice({ @@ -49,9 +48,6 @@ export const learningAssistantSlice = createSlice({ setAuditTrial: (state, { payload }) => { state.auditTrial = payload; }, - setAuditTrialLengthDays: (state, { payload }) => { - state.auditTrialLengthDays = payload; - }, }, }); @@ -66,7 +62,6 @@ export const { setSidebarIsOpen, setIsEnabled, setAuditTrial, - setAuditTrialLengthDays, } = learningAssistantSlice.actions; export const { diff --git a/src/data/thunks.js b/src/data/thunks.js index 70b2a1ef..6f6bf2cb 100644 --- a/src/data/thunks.js +++ b/src/data/thunks.js @@ -17,7 +17,6 @@ import { setSidebarIsOpen, setIsEnabled, setAuditTrial, - setAuditTrialLengthDays, } from './slice'; import { OPTIMIZELY_PROMPT_EXPERIMENT_KEY } from './optimizely'; @@ -59,7 +58,6 @@ export function getLearningAssistantChatSummary(courseId) { dispatch(setAuditTrial(auditTrial)); } - dispatch(setAuditTrialLengthDays(data.audit_trial_length_days)); } catch (error) { dispatch(setApiError()); } @@ -116,8 +114,7 @@ export function getChatResponse(courseId, unitId, promptExperimentVariationKey = dispatch(setApiIsLoading(false)); dispatch(addChatMessage(message.role, message.content, courseId, promptExperimentVariationKey)); - // NOTE for self: There could be a case where the user just keeps a tab open to keep - // their trial going. Though this is unlikely, this call prevents the trial from continuing in the UI + // Refresh chat summary so we can tell if the user has initiated an audit trial dispatch(getLearningAssistantChatSummary(courseId)); } catch (error) { dispatch(setApiError()); From 9890f24595ef4ee21b0efe77f430b0892c3c67ae Mon Sep 17 00:00:00 2001 From: ilee2u Date: Thu, 12 Dec 2024 12:37:51 -0500 Subject: [PATCH 07/25] temp: rollback point for tests --- src/components/Sidebar/index.jsx | 11 ++++------- src/data/api.js | 1 + src/data/thunks.js | 1 - 3 files changed, 5 insertions(+), 8 deletions(-) diff --git a/src/components/Sidebar/index.jsx b/src/components/Sidebar/index.jsx index 7778d71d..a13cc296 100644 --- a/src/components/Sidebar/index.jsx +++ b/src/components/Sidebar/index.jsx @@ -2,16 +2,13 @@ import React, { useRef, useEffect } from 'react'; import { useSelector } from 'react-redux'; import PropTypes from 'prop-types'; -import { useModel } from '@src/generic/model-store'; - import { Icon, IconButton, } from '@openedx/paragon'; import { Close } from '@openedx/paragon/icons'; -// Commenting this out for now until we figure out a solution for getting the upgrade url -// import { useModel } from '@src/generic/model-store'; +import { useModel } from '@src/generic/model-store'; // eslint-disable-line import/no-unresolved import showSurvey from '../../utils/surveyMonkey'; @@ -109,20 +106,20 @@ const Sidebar = ({ const irtl = new Intl.RelativeTimeFormat({ style: 'long' }); return (
- Your trial ends {irtl.format(daysRemaining, 'day')}. Upgrade for full access to Xpert. + Your trial ends {irtl.format(daysRemaining, 'day')}. Upgrade for full access to Xpert.
); } if (daysRemaining === 1) { return (
- Your trial ends today! Upgrade for full access to Xpert. + Your trial ends today! Upgrade for full access to Xpert.
); } // TODO: Show the upgrade screen instead of this banner, to be done in future ticket return (
- Your trial has expired. Upgrade for full access to Xpert. + Your trial has expired. Upgrade for full access to Xpert.
); }; diff --git a/src/data/api.js b/src/data/api.js index 520b6957..0f90283f 100644 --- a/src/data/api.js +++ b/src/data/api.js @@ -16,6 +16,7 @@ async function fetchChatResponse(courseId, messageList, unitId, customQueryParam let queryString = new URLSearchParams(queryParams); queryString = queryString.toString(); + const url = new URL(`${getConfig().CHAT_RESPONSE_URL}/${courseId}?${queryString}`); const { data } = await getAuthenticatedHttpClient().post(url.href, payload); diff --git a/src/data/thunks.js b/src/data/thunks.js index 6f6bf2cb..af542281 100644 --- a/src/data/thunks.js +++ b/src/data/thunks.js @@ -57,7 +57,6 @@ export function getLearningAssistantChatSummary(courseId) { if (Object.keys(auditTrial).length !== 0) { dispatch(setAuditTrial(auditTrial)); } - } catch (error) { dispatch(setApiError()); } From 77231f7971a7e1c0588ef140b697f91670c19e7f Mon Sep 17 00:00:00 2001 From: ilee2u Date: Thu, 12 Dec 2024 13:12:56 -0500 Subject: [PATCH 08/25] feat: only refresh chat-summary on 1st msg --- src/data/thunks.js | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/data/thunks.js b/src/data/thunks.js index af542281..51e40d9d 100644 --- a/src/data/thunks.js +++ b/src/data/thunks.js @@ -19,6 +19,7 @@ import { setAuditTrial, } from './slice'; import { OPTIMIZELY_PROMPT_EXPERIMENT_KEY } from './optimizely'; +import { camelCaseObject } from '@edx/frontend-platform'; export function getLearningAssistantChatSummary(courseId) { return async (dispatch) => { @@ -48,14 +49,11 @@ export function getLearningAssistantChatSummary(courseId) { } // Audit Trial - const auditTrial = { - startDate: data.audit_trial.start_date, - expirationDate: data.audit_trial.expiration_date, - }; + const auditTrial = data.audit_trial; // If returned audit trial data is not empty if (Object.keys(auditTrial).length !== 0) { - dispatch(setAuditTrial(auditTrial)); + dispatch(setAuditTrial(camelCaseObject(auditTrial))); } } catch (error) { dispatch(setApiError()); @@ -111,10 +109,14 @@ export function getChatResponse(courseId, unitId, promptExperimentVariationKey = const customQueryParams = promptExperimentVariationKey ? { responseVariation: promptExperimentVariationKey } : {}; const message = await fetchChatResponse(courseId, messageList, unitId, customQueryParams); + // Refresh chat summary only on the first message so we can tell if the user has initiated an audit trial + // NOTE: This is a bit of a hacky solution that may be refined later + if (messageList.length === 1) { + dispatch(getLearningAssistantChatSummary(courseId)); + } + dispatch(setApiIsLoading(false)); dispatch(addChatMessage(message.role, message.content, courseId, promptExperimentVariationKey)); - // Refresh chat summary so we can tell if the user has initiated an audit trial - dispatch(getLearningAssistantChatSummary(courseId)); } catch (error) { dispatch(setApiError()); dispatch(setApiIsLoading(false)); From 09477325e6606cbc615e747eab4a5eee8ba59290 Mon Sep 17 00:00:00 2001 From: ilee2u Date: Thu, 12 Dec 2024 13:14:13 -0500 Subject: [PATCH 09/25] temp: attempting to mock useModel + factories --- package-lock.json | 9 +++++++++ package.json | 1 + src/components/Sidebar/index.test.jsx | 8 +++++++- src/data/__factories__/courseHomeMeta.factory.js | 16 ++++++++++++++++ src/data/__factories__/courseMeta.factory.js | 8 ++++++++ src/data/__factories__/index.js | 2 ++ src/data/thunks.test.js | 8 ++++---- src/setupTest.js | 12 ++++++++++++ 8 files changed, 59 insertions(+), 5 deletions(-) create mode 100644 src/data/__factories__/courseHomeMeta.factory.js create mode 100644 src/data/__factories__/courseMeta.factory.js create mode 100644 src/data/__factories__/index.js diff --git a/package-lock.json b/package-lock.json index 5b60c440..d4257a77 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,7 @@ "@edx/brand": "npm:@edx/brand-openedx@1.2.0", "@optimizely/react-sdk": "^2.9.2", "react-markdown": "^8.0.5", + "rosie": "^2.1.1", "uuid": "9.0.0" }, "devDependencies": { @@ -21822,6 +21823,14 @@ "rimraf": "bin.js" } }, + "node_modules/rosie": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/rosie/-/rosie-2.1.1.tgz", + "integrity": "sha512-2AXB7WrIZXtKMZ6Q/PlozqPF5nu/x7NEvRJZOblrJuprrPfm5gL8JVvJPj9aaib9F8IUALnLUFhzXrwEtnI5cQ==", + "engines": { + "node": ">=10" + } + }, "node_modules/rtlcss": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/rtlcss/-/rtlcss-4.1.1.tgz", diff --git a/package.json b/package.json index d7af8941..7542a8ef 100644 --- a/package.json +++ b/package.json @@ -45,6 +45,7 @@ "@edx/brand": "npm:@edx/brand-openedx@1.2.0", "@optimizely/react-sdk": "^2.9.2", "react-markdown": "^8.0.5", + "rosie": "^2.1.1", "uuid": "9.0.0" }, "peerDependencies": { diff --git a/src/components/Sidebar/index.test.jsx b/src/components/Sidebar/index.test.jsx index 5f40a144..0e890899 100644 --- a/src/components/Sidebar/index.test.jsx +++ b/src/components/Sidebar/index.test.jsx @@ -1,6 +1,9 @@ import React from 'react'; import { screen, act } from '@testing-library/react'; +import { Factory } from 'rosie'; +import { setModel } from '@src/generic/model-store'; // eslint-disable-line import/no-unresolved + import { usePromptExperimentDecision } from '../../experiments'; import { render as renderComponent } from '../../utils/utils.test'; import { initialState } from '../../data/slice'; @@ -67,7 +70,10 @@ const render = async (props = {}, sliceState = {}) => { }; describe('', () => { - beforeEach(() => { + beforeEach(async () => { + setModel('courseHomeMeta', Factory.build('courseHomeMeta')); + setModel('coursewareMeta', Factory.build('coursewareMeta')); + jest.resetAllMocks(); useCourseUpgrade.mockReturnValue({ upgradeable: false }); useTrackEvent.mockReturnValue({ track: jest.fn() }); diff --git a/src/data/__factories__/courseHomeMeta.factory.js b/src/data/__factories__/courseHomeMeta.factory.js new file mode 100644 index 00000000..fff4f60d --- /dev/null +++ b/src/data/__factories__/courseHomeMeta.factory.js @@ -0,0 +1,16 @@ +import { Factory } from 'rosie'; // eslint-disable-line import/no-extraneous-dependencies + +Factory.define('courseHomeMeta') + .option('host', 'http://localhost:18000') + .attr('number', 'DemoX') + .attr('title', 'Demonstration Course') + .attr('org', 'edX') + .attr('userTimezone', null) + .attr('verifiedMode', ['host'], (host) => ({ + access_expiration_date: '2050-01-01T12:00:00', + currency: 'USD', + currencySymbol: '$', + price: 149, + sku: 'ABCD1234', + upgradeUrl: `${host}/dashboard`, + })); diff --git a/src/data/__factories__/courseMeta.factory.js b/src/data/__factories__/courseMeta.factory.js new file mode 100644 index 00000000..1c9fea0c --- /dev/null +++ b/src/data/__factories__/courseMeta.factory.js @@ -0,0 +1,8 @@ +import { Factory } from 'rosie'; // eslint-disable-line import/no-extraneous-dependencies + +Factory.define('coursewareMeta') + .attr('accessExpiration', null) + .attr('contentTypeGatingEnabled', false) + .attr('marketingUrl', 'http://www.example.com') + .attr('offer', null) + .attr('timeOffsetMillis', 0); diff --git a/src/data/__factories__/index.js b/src/data/__factories__/index.js new file mode 100644 index 00000000..74a8dc4e --- /dev/null +++ b/src/data/__factories__/index.js @@ -0,0 +1,2 @@ +import './courseHomeMeta.factory'; +import './coursewareMeta.factory'; diff --git a/src/data/thunks.test.js b/src/data/thunks.test.js index 6214cc3f..478386a6 100644 --- a/src/data/thunks.test.js +++ b/src/data/thunks.test.js @@ -110,8 +110,8 @@ describe('Thunks unit tests', () => { expect(dispatch).toHaveBeenNthCalledWith(5, { type: 'learning-assistant/setAuditTrial', payload: { - start_date: '2024-12-02T14:59:16.148236Z', - expiration_date: '9999-12-16T14:59:16.148236Z', + startDate: '2024-12-02T14:59:16.148236Z', + expirationDate: '9999-12-16T14:59:16.148236Z', }, }); @@ -158,8 +158,8 @@ describe('Thunks unit tests', () => { expect(dispatch).toHaveBeenNthCalledWith(3, { type: 'learning-assistant/setAuditTrial', payload: { - start_date: '2024-12-02T14:59:16.148236Z', - expiration_date: '9999-12-16T14:59:16.148236Z', + startDate: '2024-12-02T14:59:16.148236Z', + expirationDate: '9999-12-16T14:59:16.148236Z', }, }); diff --git a/src/setupTest.js b/src/setupTest.js index 57647f19..d4b216b5 100644 --- a/src/setupTest.js +++ b/src/setupTest.js @@ -11,3 +11,15 @@ jest.mock('@src/generic/model-store', () => ({ useModel: jest.fn() }), { virtual mergeConfig({ ...process.env, }); + +const mockModelStore = {}; +jest.mock( + '@src/generic/model-store', + () => ({ + useModel: jest.fn((type) => mockModelStore[type]), + setModel: jest.fn((type, data) => { + mockModelStore[type] = data; + }), + }), + { virtual: true }, +); From 11c0562efaea22f6a5e38a2c0380e1d7e85b3da6 Mon Sep 17 00:00:00 2001 From: ilee2u Date: Fri, 13 Dec 2024 13:11:43 -0500 Subject: [PATCH 10/25] temp: some nits --- src/components/Sidebar/index.jsx | 12 +++++------- src/data/thunks.js | 9 +++++++++ 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/src/components/Sidebar/index.jsx b/src/components/Sidebar/index.jsx index a13cc296..95b7a20f 100644 --- a/src/components/Sidebar/index.jsx +++ b/src/components/Sidebar/index.jsx @@ -34,7 +34,7 @@ const Sidebar = ({ messageList, } = useSelector(state => state.learningAssistant); - const { upgradeable, auditTrialExpired } = useCourseUpgrade(); + const auditTrialExpirationDate = new Date(auditTrial.expirationDate); const chatboxContainerRef = useRef(null); @@ -94,11 +94,9 @@ const Sidebar = ({ ); - // Get this to work - const getDaysRemainingMessage = () => { - const auditTrialExpirationDate = new Date(auditTrial.expirationDate); - const oneDay = 24 * 60 * 60 * 1000; // hours*minutes*seconds*milliseconds - const daysRemaining = Math.ceil((auditTrialExpirationDate - Date.now()) / oneDay); + const getDaysRemainingMessage = (auditTrialExpirationDate) => { + const millisecondsInOneDay = 24 * 60 * 60 * 1000; // hours*minutes*seconds*milliseconds + const daysRemaining = Math.ceil((auditTrialExpirationDate - Date.now()) / millisecondsInOneDay); const upgradeURL = offer ? offer.upgradeUrl : verifiedMode.upgradeUrl; @@ -137,7 +135,7 @@ const Sidebar = ({ {isUpgradeEligible && (
- {getDaysRemainingMessage()} + {getDaysRemainingMessage(auditTrialExpirationDate)}
)} diff --git a/src/data/thunks.js b/src/data/thunks.js index 51e40d9d..762fb4c9 100644 --- a/src/data/thunks.js +++ b/src/data/thunks.js @@ -50,6 +50,15 @@ export function getLearningAssistantChatSummary(courseId) { // Audit Trial const auditTrial = data.audit_trial; + auditTrial.start_date="POTATOES"; + auditTrial.expiration_date="POTATOES"; + try { + new Date(auditTrial.start_date); + new Date(auditTrial.expiration_date); + } catch { + // TODO: How to validate the data here? + throw new Error("AAAA:", TypeError); + } // If returned audit trial data is not empty if (Object.keys(auditTrial).length !== 0) { From 8ef838f62045b14f7fbf1874aee014811e695476 Mon Sep 17 00:00:00 2001 From: ilee2u Date: Fri, 13 Dec 2024 14:31:15 -0500 Subject: [PATCH 11/25] fix: re-syncing: mock useModel + thunk work --- src/data/slice.js | 5 ++++ src/data/thunks.js | 63 ++++++----------------------------------- src/data/thunks.test.js | 6 +++- 3 files changed, 19 insertions(+), 55 deletions(-) diff --git a/src/data/slice.js b/src/data/slice.js index 91982422..fc64c47e 100644 --- a/src/data/slice.js +++ b/src/data/slice.js @@ -12,6 +12,7 @@ export const initialState = { sidebarIsOpen: false, isEnabled: false, auditTrial: {}, + auditTrialLengthDays: null, }; export const learningAssistantSlice = createSlice({ @@ -48,6 +49,9 @@ export const learningAssistantSlice = createSlice({ setAuditTrial: (state, { payload }) => { state.auditTrial = payload; }, + setAuditTrialLengthDays: (state, { payload }) => { + state.auditTrialLengthDays = payload; + }, }, }); @@ -62,6 +66,7 @@ export const { setSidebarIsOpen, setIsEnabled, setAuditTrial, + setAuditTrialLengthDays, } = learningAssistantSlice.actions; export const { diff --git a/src/data/thunks.js b/src/data/thunks.js index 762fb4c9..367d2e24 100644 --- a/src/data/thunks.js +++ b/src/data/thunks.js @@ -17,60 +17,11 @@ import { setSidebarIsOpen, setIsEnabled, setAuditTrial, + setAuditTrialLengthDays, } from './slice'; import { OPTIMIZELY_PROMPT_EXPERIMENT_KEY } from './optimizely'; import { camelCaseObject } from '@edx/frontend-platform'; -export function getLearningAssistantChatSummary(courseId) { - return async (dispatch) => { - dispatch(setApiIsLoading(true)); - - try { - const data = await fetchLearningAssistantChatSummary(courseId); - - // Enabled - dispatch(setIsEnabled(data.enabled)); - - // Message History - const rawMessageList = data.message_history; - - // If returned message history data is not empty - if (rawMessageList.length) { - const messageList = rawMessageList - .map(({ timestamp, ...msg }) => ({ - ...msg, - timestamp: new Date(timestamp).toString(), // Parse ISO time to Date() - })); - - dispatch(setMessageList({ messageList })); - - // If it has chat history, then we assume the user already aknowledged. - dispatch(setDisclosureAcknowledged(true)); - } - - // Audit Trial - const auditTrial = data.audit_trial; - auditTrial.start_date="POTATOES"; - auditTrial.expiration_date="POTATOES"; - try { - new Date(auditTrial.start_date); - new Date(auditTrial.expiration_date); - } catch { - // TODO: How to validate the data here? - throw new Error("AAAA:", TypeError); - } - - // If returned audit trial data is not empty - if (Object.keys(auditTrial).length !== 0) { - dispatch(setAuditTrial(camelCaseObject(auditTrial))); - } - } catch (error) { - dispatch(setApiError()); - } - dispatch(setApiIsLoading(false)); - }; -} - export function addChatMessage(role, content, courseId, promptExperimentVariationKey = undefined) { return (dispatch, getState) => { const { messageList, conversationId } = getState().learningAssistant; @@ -189,11 +140,15 @@ export function getLearningAssistantChatSummary(courseId) { startDate: data.audit_trial.start_date, expirationDate: data.audit_trial.expiration_date, }; - console.log("thunks auditTrial:", auditTrial); - // If returned audit trial data is not empty - if (Object.keys(auditTrial).length !== 0) { - dispatch(setAuditTrial(auditTrial)); + // Validate audit trial data & dates + const auditTrialDatesValid = !( + Number.isNaN(Date.parse(auditTrial.startDate)) || + Number.isNaN(Date.parse(auditTrial.expirationDate)) + ); + + if (Object.keys(auditTrial).length !== 0 && auditTrialDatesValid) { + dispatch(setAuditTrial(camelCaseObject(auditTrial))); } if (data.audit_trial_length_days) { dispatch(setAuditTrialLengthDays(data.audit_trial_length_days)); } diff --git a/src/data/thunks.test.js b/src/data/thunks.test.js index 478386a6..b08957c2 100644 --- a/src/data/thunks.test.js +++ b/src/data/thunks.test.js @@ -1,5 +1,7 @@ import { v4 as uuidv4 } from 'uuid'; +import { Factory } from 'rosie'; import { sendTrackEvent } from '@edx/frontend-platform/analytics'; +import { setModel } from '@src/generic/model-store'; // eslint-disable-line import/no-unresolved import { fetchLearningAssistantChatSummary } from './api'; @@ -26,7 +28,9 @@ describe('Thunks unit tests', () => { describe('addChatMessage()', () => { const mockDate = new Date(2024, 1, 1); - beforeAll(() => { + beforeEach(async () => { + setModel('coursewareMeta', Factory.build('coursewareMeta')); + setModel('courseHomeMeta', Factory.build('courseHomeMeta')); jest.useFakeTimers('modern'); jest.setSystemTime(mockDate); }); From 13b9e278c46fd2c9735f653d3deea15c58fd205d Mon Sep 17 00:00:00 2001 From: ilee2u Date: Fri, 13 Dec 2024 14:43:04 -0500 Subject: [PATCH 12/25] temp: progressing on using new audit trial hooks --- src/components/Sidebar/index.jsx | 34 +++++++++++++++++--------------- src/data/thunks.test.js | 1 + 2 files changed, 19 insertions(+), 16 deletions(-) diff --git a/src/components/Sidebar/index.jsx b/src/components/Sidebar/index.jsx index 95b7a20f..5109113a 100644 --- a/src/components/Sidebar/index.jsx +++ b/src/components/Sidebar/index.jsx @@ -17,6 +17,7 @@ import ChatBox from '../ChatBox'; import Disclosure from '../Disclosure'; import UpgradePanel from '../UpgradePanel'; import MessageForm from '../MessageForm'; +import { useCourseUpgrade, useTrackEvent } from '../../hooks'; import { ReactComponent as XpertLogo } from '../../assets/xpert-logo.svg'; import './Sidebar.scss'; @@ -29,12 +30,11 @@ const Sidebar = ({ }) => { const { apiError, - auditTrial, disclosureAcknowledged, messageList, } = useSelector(state => state.learningAssistant); - const auditTrialExpirationDate = new Date(auditTrial.expirationDate); + const { upgradeUrl, auditTrialDaysRemaining } = useCourseUpgrade(); const chatboxContainerRef = useRef(null); @@ -94,20 +94,27 @@ const Sidebar = ({ ); - const getDaysRemainingMessage = (auditTrialExpirationDate) => { - const millisecondsInOneDay = 24 * 60 * 60 * 1000; // hours*minutes*seconds*milliseconds - const daysRemaining = Math.ceil((auditTrialExpirationDate - Date.now()) / millisecondsInOneDay); + /** + * isUpgradeEligible - can they have an audit trial? + not staff + enrolled as audit or honor + course has verified mode + enable xpert audit setting is true + if isUpgradeEligible: + show all the audit trial data, expired or not. + */ + const getDaysRemainingMessage = () => { const upgradeURL = offer ? offer.upgradeUrl : verifiedMode.upgradeUrl; - if (daysRemaining > 1) { + if (auditTrialDaysRemaining > 1) { const irtl = new Intl.RelativeTimeFormat({ style: 'long' }); return (
- Your trial ends {irtl.format(daysRemaining, 'day')}. Upgrade for full access to Xpert. + Your trial ends {irtl.format(auditTrialDaysRemaining, 'day')}. Upgrade for full access to Xpert.
); - } if (daysRemaining === 1) { + } if (auditTrialDaysRemaining === 1) { return (
Your trial ends today! Upgrade for full access to Xpert. @@ -122,11 +129,6 @@ const Sidebar = ({ ); }; - /** - * if no audit trial, and chat message endpoint success, - * we know an audit trial just started so write "[xpert_trial_length] days" - * and re-call the chat summary endpoint - */ const getSidebar = () => (
@@ -134,9 +136,9 @@ const Sidebar = ({
{isUpgradeEligible && ( -
- {getDaysRemainingMessage(auditTrialExpirationDate)} -
+
+ {getDaysRemainingMessage()} +
)} { describe('addChatMessage()', () => { const mockDate = new Date(2024, 1, 1); + beforeEach(async () => { setModel('coursewareMeta', Factory.build('coursewareMeta')); setModel('courseHomeMeta', Factory.build('courseHomeMeta')); From 2a3d1d7d8195aaf57d3112f264fc818711d7a589 Mon Sep 17 00:00:00 2001 From: ilee2u Date: Fri, 13 Dec 2024 15:11:53 -0500 Subject: [PATCH 13/25] temp: removing useModel mock --- src/data/__factories__/courseHomeMeta.factory.js | 16 ---------------- src/data/__factories__/courseMeta.factory.js | 8 -------- src/data/__factories__/index.js | 2 -- src/data/thunks.test.js | 4 ---- src/setupTest.js | 12 ------------ 5 files changed, 42 deletions(-) delete mode 100644 src/data/__factories__/courseHomeMeta.factory.js delete mode 100644 src/data/__factories__/courseMeta.factory.js delete mode 100644 src/data/__factories__/index.js diff --git a/src/data/__factories__/courseHomeMeta.factory.js b/src/data/__factories__/courseHomeMeta.factory.js deleted file mode 100644 index fff4f60d..00000000 --- a/src/data/__factories__/courseHomeMeta.factory.js +++ /dev/null @@ -1,16 +0,0 @@ -import { Factory } from 'rosie'; // eslint-disable-line import/no-extraneous-dependencies - -Factory.define('courseHomeMeta') - .option('host', 'http://localhost:18000') - .attr('number', 'DemoX') - .attr('title', 'Demonstration Course') - .attr('org', 'edX') - .attr('userTimezone', null) - .attr('verifiedMode', ['host'], (host) => ({ - access_expiration_date: '2050-01-01T12:00:00', - currency: 'USD', - currencySymbol: '$', - price: 149, - sku: 'ABCD1234', - upgradeUrl: `${host}/dashboard`, - })); diff --git a/src/data/__factories__/courseMeta.factory.js b/src/data/__factories__/courseMeta.factory.js deleted file mode 100644 index 1c9fea0c..00000000 --- a/src/data/__factories__/courseMeta.factory.js +++ /dev/null @@ -1,8 +0,0 @@ -import { Factory } from 'rosie'; // eslint-disable-line import/no-extraneous-dependencies - -Factory.define('coursewareMeta') - .attr('accessExpiration', null) - .attr('contentTypeGatingEnabled', false) - .attr('marketingUrl', 'http://www.example.com') - .attr('offer', null) - .attr('timeOffsetMillis', 0); diff --git a/src/data/__factories__/index.js b/src/data/__factories__/index.js deleted file mode 100644 index 74a8dc4e..00000000 --- a/src/data/__factories__/index.js +++ /dev/null @@ -1,2 +0,0 @@ -import './courseHomeMeta.factory'; -import './coursewareMeta.factory'; diff --git a/src/data/thunks.test.js b/src/data/thunks.test.js index b1e0ec0d..8692bd95 100644 --- a/src/data/thunks.test.js +++ b/src/data/thunks.test.js @@ -1,7 +1,5 @@ import { v4 as uuidv4 } from 'uuid'; -import { Factory } from 'rosie'; import { sendTrackEvent } from '@edx/frontend-platform/analytics'; -import { setModel } from '@src/generic/model-store'; // eslint-disable-line import/no-unresolved import { fetchLearningAssistantChatSummary } from './api'; @@ -30,8 +28,6 @@ describe('Thunks unit tests', () => { const mockDate = new Date(2024, 1, 1); beforeEach(async () => { - setModel('coursewareMeta', Factory.build('coursewareMeta')); - setModel('courseHomeMeta', Factory.build('courseHomeMeta')); jest.useFakeTimers('modern'); jest.setSystemTime(mockDate); }); diff --git a/src/setupTest.js b/src/setupTest.js index d4b216b5..57647f19 100644 --- a/src/setupTest.js +++ b/src/setupTest.js @@ -11,15 +11,3 @@ jest.mock('@src/generic/model-store', () => ({ useModel: jest.fn() }), { virtual mergeConfig({ ...process.env, }); - -const mockModelStore = {}; -jest.mock( - '@src/generic/model-store', - () => ({ - useModel: jest.fn((type) => mockModelStore[type]), - setModel: jest.fn((type, data) => { - mockModelStore[type] = data; - }), - }), - { virtual: true }, -); From f3496eb88deaa9c0d3e28e8b41930d86b5b7e5ac Mon Sep 17 00:00:00 2001 From: ilee2u Date: Fri, 13 Dec 2024 15:12:23 -0500 Subject: [PATCH 14/25] temp: trying to get hook to work --- src/components/Sidebar/index.jsx | 23 +++++------------------ src/hooks/use-course-upgrade.js | 2 +- 2 files changed, 6 insertions(+), 19 deletions(-) diff --git a/src/components/Sidebar/index.jsx b/src/components/Sidebar/index.jsx index 5109113a..cb2230fc 100644 --- a/src/components/Sidebar/index.jsx +++ b/src/components/Sidebar/index.jsx @@ -8,8 +8,6 @@ import { } from '@openedx/paragon'; import { Close } from '@openedx/paragon/icons'; -import { useModel } from '@src/generic/model-store'; // eslint-disable-line import/no-unresolved - import showSurvey from '../../utils/surveyMonkey'; import APIError from '../APIError'; @@ -17,7 +15,7 @@ import ChatBox from '../ChatBox'; import Disclosure from '../Disclosure'; import UpgradePanel from '../UpgradePanel'; import MessageForm from '../MessageForm'; -import { useCourseUpgrade, useTrackEvent } from '../../hooks'; +import { useCourseUpgrade } from '../../hooks'; import { ReactComponent as XpertLogo } from '../../assets/xpert-logo.svg'; import './Sidebar.scss'; @@ -38,16 +36,6 @@ const Sidebar = ({ const chatboxContainerRef = useRef(null); - const courseHomeMeta = useModel('courseHomeMeta', courseId); - const { - verifiedMode, - } = courseHomeMeta; - - const course = useModel('coursewareMeta', courseId); - const { - offer, - } = course; - // this use effect is intended to scroll to the bottom of the chat window, in the case // that a message is larger than the chat window height. useEffect(() => { @@ -104,27 +92,26 @@ const Sidebar = ({ show all the audit trial data, expired or not. */ const getDaysRemainingMessage = () => { - - const upgradeURL = offer ? offer.upgradeUrl : verifiedMode.upgradeUrl; + console.log("auditTrialDaysRemaining", auditTrialDaysRemaining) if (auditTrialDaysRemaining > 1) { const irtl = new Intl.RelativeTimeFormat({ style: 'long' }); return (
- Your trial ends {irtl.format(auditTrialDaysRemaining, 'day')}. Upgrade for full access to Xpert. + Your trial ends {irtl.format(auditTrialDaysRemaining, 'day')}. Upgrade for full access to Xpert.
); } if (auditTrialDaysRemaining === 1) { return (
- Your trial ends today! Upgrade for full access to Xpert. + Your trial ends today! Upgrade for full access to Xpert.
); } // TODO: Show the upgrade screen instead of this banner, to be done in future ticket return (
- Your trial has expired. Upgrade for full access to Xpert. + Your trial has expired. Upgrade for full access to Xpert.
); }; diff --git a/src/hooks/use-course-upgrade.js b/src/hooks/use-course-upgrade.js index 9373d151..59ad1625 100644 --- a/src/hooks/use-course-upgrade.js +++ b/src/hooks/use-course-upgrade.js @@ -40,7 +40,6 @@ export default function useCourseUpgrade() { auditTrialLengthDays, auditTrial, } = useSelector(state => state.learningAssistant); - const upgradeUrl = offer?.upgradeUrl || verifiedMode?.upgradeUrl; if (!isUpgradeEligible || !upgradeUrl) { return { upgradeable: false }; } @@ -53,6 +52,7 @@ export default function useCourseUpgrade() { auditTrialDaysRemaining = Math.ceil((auditTrialExpirationDate - Date.now()) / millisecondsInOneDay); auditTrialExpired = auditTrialDaysRemaining < 0; + console.log({upgradeUrl}, {auditTrialDaysRemaining}) } const isFBE = !!accessExpiration && !!datesBannerInfo?.contentTypeGatingEnabled; From 41bd3cd7f23a84b169c7ce5e8bd040261427f9a8 Mon Sep 17 00:00:00 2001 From: ilee2u Date: Fri, 13 Dec 2024 15:29:02 -0500 Subject: [PATCH 15/25] chore: lint --- src/components/Sidebar/index.jsx | 11 ----------- src/data/thunks.js | 7 ++++--- src/hooks/use-course-upgrade.js | 1 - src/widgets/Xpert.jsx | 2 +- 4 files changed, 5 insertions(+), 16 deletions(-) diff --git a/src/components/Sidebar/index.jsx b/src/components/Sidebar/index.jsx index cb2230fc..83126a4f 100644 --- a/src/components/Sidebar/index.jsx +++ b/src/components/Sidebar/index.jsx @@ -82,18 +82,7 @@ const Sidebar = ({ ); - /** - * isUpgradeEligible - can they have an audit trial? - not staff - enrolled as audit or honor - course has verified mode - enable xpert audit setting is true - if isUpgradeEligible: - show all the audit trial data, expired or not. - */ const getDaysRemainingMessage = () => { - console.log("auditTrialDaysRemaining", auditTrialDaysRemaining) - if (auditTrialDaysRemaining > 1) { const irtl = new Intl.RelativeTimeFormat({ style: 'long' }); return ( diff --git a/src/data/thunks.js b/src/data/thunks.js index 367d2e24..5d654646 100644 --- a/src/data/thunks.js +++ b/src/data/thunks.js @@ -1,6 +1,7 @@ import { sendTrackEvent } from '@edx/frontend-platform/analytics'; import { getAuthenticatedUser } from '@edx/frontend-platform/auth'; +import { camelCaseObject } from '@edx/frontend-platform'; import trackChatBotMessageOptimizely from '../utils/optimizelyExperiment'; import { fetchChatResponse, @@ -20,7 +21,6 @@ import { setAuditTrialLengthDays, } from './slice'; import { OPTIMIZELY_PROMPT_EXPERIMENT_KEY } from './optimizely'; -import { camelCaseObject } from '@edx/frontend-platform'; export function addChatMessage(role, content, courseId, promptExperimentVariationKey = undefined) { return (dispatch, getState) => { @@ -72,6 +72,7 @@ export function getChatResponse(courseId, unitId, promptExperimentVariationKey = // Refresh chat summary only on the first message so we can tell if the user has initiated an audit trial // NOTE: This is a bit of a hacky solution that may be refined later if (messageList.length === 1) { + // eslint-disable-next-line no-use-before-define dispatch(getLearningAssistantChatSummary(courseId)); } @@ -143,8 +144,8 @@ export function getLearningAssistantChatSummary(courseId) { // Validate audit trial data & dates const auditTrialDatesValid = !( - Number.isNaN(Date.parse(auditTrial.startDate)) || - Number.isNaN(Date.parse(auditTrial.expirationDate)) + Number.isNaN(Date.parse(auditTrial.startDate)) + || Number.isNaN(Date.parse(auditTrial.expirationDate)) ); if (Object.keys(auditTrial).length !== 0 && auditTrialDatesValid) { diff --git a/src/hooks/use-course-upgrade.js b/src/hooks/use-course-upgrade.js index 59ad1625..ad47c5e8 100644 --- a/src/hooks/use-course-upgrade.js +++ b/src/hooks/use-course-upgrade.js @@ -52,7 +52,6 @@ export default function useCourseUpgrade() { auditTrialDaysRemaining = Math.ceil((auditTrialExpirationDate - Date.now()) / millisecondsInOneDay); auditTrialExpired = auditTrialDaysRemaining < 0; - console.log({upgradeUrl}, {auditTrialDaysRemaining}) } const isFBE = !!accessExpiration && !!datesBannerInfo?.contentTypeGatingEnabled; diff --git a/src/widgets/Xpert.jsx b/src/widgets/Xpert.jsx index 78fcd244..2a5b32d3 100644 --- a/src/widgets/Xpert.jsx +++ b/src/widgets/Xpert.jsx @@ -1,5 +1,5 @@ import PropTypes from 'prop-types'; -import { useEffect, useMemo, useState } from 'react'; +import { useEffect, useMemo } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { updateSidebarIsOpen, getLearningAssistantChatSummary } from '../data/thunks'; From b49f15f0a84e6a8aab07621df8711f8842175fb0 Mon Sep 17 00:00:00 2001 From: ilee2u Date: Fri, 13 Dec 2024 15:48:24 -0500 Subject: [PATCH 16/25] temp: attempting to test Sidebar - getting a strange error where hooks aren't being picked up... --- package-lock.json | 9 ----- package.json | 1 - src/components/Sidebar/index.jsx | 10 +++--- src/components/Sidebar/index.test.jsx | 50 +++++++++++++++++++++------ src/hooks/use-course-upgrade.js | 1 + 5 files changed, 44 insertions(+), 27 deletions(-) diff --git a/package-lock.json b/package-lock.json index d4257a77..5b60c440 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,6 @@ "@edx/brand": "npm:@edx/brand-openedx@1.2.0", "@optimizely/react-sdk": "^2.9.2", "react-markdown": "^8.0.5", - "rosie": "^2.1.1", "uuid": "9.0.0" }, "devDependencies": { @@ -21823,14 +21822,6 @@ "rimraf": "bin.js" } }, - "node_modules/rosie": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/rosie/-/rosie-2.1.1.tgz", - "integrity": "sha512-2AXB7WrIZXtKMZ6Q/PlozqPF5nu/x7NEvRJZOblrJuprrPfm5gL8JVvJPj9aaib9F8IUALnLUFhzXrwEtnI5cQ==", - "engines": { - "node": ">=10" - } - }, "node_modules/rtlcss": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/rtlcss/-/rtlcss-4.1.1.tgz", diff --git a/package.json b/package.json index 7542a8ef..d7af8941 100644 --- a/package.json +++ b/package.json @@ -45,7 +45,6 @@ "@edx/brand": "npm:@edx/brand-openedx@1.2.0", "@optimizely/react-sdk": "^2.9.2", "react-markdown": "^8.0.5", - "rosie": "^2.1.1", "uuid": "9.0.0" }, "peerDependencies": { diff --git a/src/components/Sidebar/index.jsx b/src/components/Sidebar/index.jsx index 83126a4f..00dd01db 100644 --- a/src/components/Sidebar/index.jsx +++ b/src/components/Sidebar/index.jsx @@ -24,7 +24,6 @@ const Sidebar = ({ isOpen, setIsOpen, unitId, - isUpgradeEligible, }) => { const { apiError, @@ -32,7 +31,7 @@ const Sidebar = ({ messageList, } = useSelector(state => state.learningAssistant); - const { upgradeUrl, auditTrialDaysRemaining } = useCourseUpgrade(); + const { upgradeable, upgradeUrl, auditTrialDaysRemaining } = useCourseUpgrade(); const chatboxContainerRef = useRef(null); @@ -86,13 +85,13 @@ const Sidebar = ({ if (auditTrialDaysRemaining > 1) { const irtl = new Intl.RelativeTimeFormat({ style: 'long' }); return ( -
+
Your trial ends {irtl.format(auditTrialDaysRemaining, 'day')}. Upgrade for full access to Xpert.
); } if (auditTrialDaysRemaining === 1) { return ( -
+
Your trial ends today! Upgrade for full access to Xpert.
); @@ -110,7 +109,7 @@ const Sidebar = ({
- {isUpgradeEligible + {upgradeable && (
{getDaysRemainingMessage()} @@ -170,7 +169,6 @@ Sidebar.propTypes = { isOpen: PropTypes.bool.isRequired, setIsOpen: PropTypes.func.isRequired, unitId: PropTypes.string.isRequired, - isUpgradeEligible: PropTypes.bool.isRequired, }; export default Sidebar; diff --git a/src/components/Sidebar/index.test.jsx b/src/components/Sidebar/index.test.jsx index 0e890899..baf4ecb8 100644 --- a/src/components/Sidebar/index.test.jsx +++ b/src/components/Sidebar/index.test.jsx @@ -1,10 +1,8 @@ import React from 'react'; import { screen, act } from '@testing-library/react'; -import { Factory } from 'rosie'; -import { setModel } from '@src/generic/model-store'; // eslint-disable-line import/no-unresolved - import { usePromptExperimentDecision } from '../../experiments'; +import { useCourseUpgrade } from '../../hooks'; import { render as renderComponent } from '../../utils/utils.test'; import { initialState } from '../../data/slice'; import showSurvey from '../../utils/surveyMonkey'; @@ -39,7 +37,6 @@ jest.mock('../../experiments', () => ({ jest.mock('../../hooks', () => ({ useCourseUpgrade: jest.fn(), - useTrackEvent: jest.fn(), })); const defaultProps = { @@ -71,13 +68,11 @@ const render = async (props = {}, sliceState = {}) => { describe('', () => { beforeEach(async () => { - setModel('courseHomeMeta', Factory.build('courseHomeMeta')); - setModel('coursewareMeta', Factory.build('coursewareMeta')); - jest.resetAllMocks(); useCourseUpgrade.mockReturnValue({ upgradeable: false }); useTrackEvent.mockReturnValue({ track: jest.fn() }); usePromptExperimentDecision.mockReturnValue([]); + useCourseUpgrade.mockReturnValue([]); }); describe('when it\'s open', () => { @@ -96,13 +91,46 @@ describe('', () => { expect(screen.queryByTestId('sidebar-xpert')).toBeInTheDocument(); }); - it('should not render xpert if audit trial is expired', () => { + // TODO: Write out these tests. + it('If auditTrialDaysRemaining > 1, show days remaining', () => { useCourseUpgrade.mockReturnValue({ upgradeable: true, - auditTrialExpired: true, + upgradeUrl: 'https://mockurl.com', + auditTrialDaysRemaining: 2, }); - render(); - expect(screen.queryByTestId('sidebar-xpert')).not.toBeInTheDocument(); + render(undefined, { disclosureAcknowledged: true }); + expect(screen.queryByTestId('sidebar-xpert')).toBeInTheDocument(); + + const daysRemainingMessage = screen.queryByTestId('x-days-remaining-message'); + expect(daysRemainingMessage).toBeInTheDocument(); + }); + + it('If auditTrialDaysRemaining === 1, say final day', () => { + useCourseUpgrade.mockReturnValue({ + upgradeable: true, + upgradeUrl: 'https://mockurl.com', + auditTrialDaysRemaining: 1, + }); + render(undefined, { disclosureAcknowledged: true }); + expect(screen.queryByTestId('sidebar-xpert')).toBeInTheDocument(); + + const trialEndsTodayMessage = screen.queryByTestId('trial-ends-today'); + expect(trialEndsTodayMessage).toBeInTheDocument(); + }); + + it('If auditTrialDaysRemaining < 1, do not show either of those', () => { + useCourseUpgrade.mockReturnValue({ + upgradeable: true, + upgradeUrl: 'https://mockurl.com', + auditTrialDaysRemaining: 0, + }); + render(undefined, { disclosureAcknowledged: true }); + expect(screen.queryByTestId('sidebar-xpert')).toBeInTheDocument(); + + const daysRemainingMessage = screen.queryByTestId('x-days-remaining-message'); + const trialEndsTodayMessage = screen.queryByTestId('trial-ends-today'); + expect(daysRemainingMessage).not.toBeInTheDocument(); + expect(trialEndsTodayMessage).not.toBeInTheDocument(); }); }); diff --git a/src/hooks/use-course-upgrade.js b/src/hooks/use-course-upgrade.js index ad47c5e8..26eb602d 100644 --- a/src/hooks/use-course-upgrade.js +++ b/src/hooks/use-course-upgrade.js @@ -52,6 +52,7 @@ export default function useCourseUpgrade() { auditTrialDaysRemaining = Math.ceil((auditTrialExpirationDate - Date.now()) / millisecondsInOneDay); auditTrialExpired = auditTrialDaysRemaining < 0; + console.log({ upgradeUrl }, { auditTrialDaysRemaining }); } const isFBE = !!accessExpiration && !!datesBannerInfo?.contentTypeGatingEnabled; From f1ccd040ad337a4bcb7b769bb43b85f04ce1bae2 Mon Sep 17 00:00:00 2001 From: ilee2u Date: Fri, 13 Dec 2024 15:50:19 -0500 Subject: [PATCH 17/25] chore: lint --- src/components/Sidebar/index.jsx | 4 ++-- src/hooks/use-course-upgrade.js | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/Sidebar/index.jsx b/src/components/Sidebar/index.jsx index 00dd01db..81c3372d 100644 --- a/src/components/Sidebar/index.jsx +++ b/src/components/Sidebar/index.jsx @@ -85,13 +85,13 @@ const Sidebar = ({ if (auditTrialDaysRemaining > 1) { const irtl = new Intl.RelativeTimeFormat({ style: 'long' }); return ( -
+
Your trial ends {irtl.format(auditTrialDaysRemaining, 'day')}. Upgrade for full access to Xpert.
); } if (auditTrialDaysRemaining === 1) { return ( -
+
Your trial ends today! Upgrade for full access to Xpert.
); diff --git a/src/hooks/use-course-upgrade.js b/src/hooks/use-course-upgrade.js index 26eb602d..3d3037bc 100644 --- a/src/hooks/use-course-upgrade.js +++ b/src/hooks/use-course-upgrade.js @@ -52,7 +52,7 @@ export default function useCourseUpgrade() { auditTrialDaysRemaining = Math.ceil((auditTrialExpirationDate - Date.now()) / millisecondsInOneDay); auditTrialExpired = auditTrialDaysRemaining < 0; - console.log({ upgradeUrl }, { auditTrialDaysRemaining }); + // console.log({ upgradeUrl }, { auditTrialDaysRemaining }); } const isFBE = !!accessExpiration && !!datesBannerInfo?.contentTypeGatingEnabled; From 9a48fd89af28ba27f50c3ae148ccbb893569cb7e Mon Sep 17 00:00:00 2001 From: ilee2u Date: Fri, 13 Dec 2024 16:04:11 -0500 Subject: [PATCH 18/25] chore: trying to get around this weird lint error --- src/components/Sidebar/index.jsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/components/Sidebar/index.jsx b/src/components/Sidebar/index.jsx index 81c3372d..79a16c89 100644 --- a/src/components/Sidebar/index.jsx +++ b/src/components/Sidebar/index.jsx @@ -95,8 +95,7 @@ const Sidebar = ({ Your trial ends today! Upgrade for full access to Xpert.
); - } - // TODO: Show the upgrade screen instead of this banner, to be done in future ticket + } // TODO: Show the upgrade screen instead of this banner, to be done in future ticket return (
Your trial has expired. Upgrade for full access to Xpert. From a8fa79d574b7537ae1f4017bd5305d10813258e1 Mon Sep 17 00:00:00 2001 From: ilee2u Date: Mon, 16 Dec 2024 15:11:25 -0500 Subject: [PATCH 19/25] test: completed working unit tests --- src/components/Sidebar/index.jsx | 6 +++--- src/components/Sidebar/index.test.jsx | 31 +++++++++++++-------------- src/setupTest.js | 12 +++++++++++ 3 files changed, 30 insertions(+), 19 deletions(-) diff --git a/src/components/Sidebar/index.jsx b/src/components/Sidebar/index.jsx index 79a16c89..a540d0b1 100644 --- a/src/components/Sidebar/index.jsx +++ b/src/components/Sidebar/index.jsx @@ -85,13 +85,13 @@ const Sidebar = ({ if (auditTrialDaysRemaining > 1) { const irtl = new Intl.RelativeTimeFormat({ style: 'long' }); return ( -
+
Your trial ends {irtl.format(auditTrialDaysRemaining, 'day')}. Upgrade for full access to Xpert.
); } if (auditTrialDaysRemaining === 1) { return ( -
+
Your trial ends today! Upgrade for full access to Xpert.
); @@ -110,7 +110,7 @@ const Sidebar = ({
{upgradeable && ( -
+
{getDaysRemainingMessage()}
)} diff --git a/src/components/Sidebar/index.test.jsx b/src/components/Sidebar/index.test.jsx index baf4ecb8..a35be1cc 100644 --- a/src/components/Sidebar/index.test.jsx +++ b/src/components/Sidebar/index.test.jsx @@ -2,7 +2,7 @@ import React from 'react'; import { screen, act } from '@testing-library/react'; import { usePromptExperimentDecision } from '../../experiments'; -import { useCourseUpgrade } from '../../hooks'; +import { useCourseUpgrade, useTrackEvent } from '../../hooks'; import { render as renderComponent } from '../../utils/utils.test'; import { initialState } from '../../data/slice'; import showSurvey from '../../utils/surveyMonkey'; @@ -10,6 +10,11 @@ import { useCourseUpgrade, useTrackEvent } from '../../hooks'; import Sidebar from '.'; +jest.mock('../../hooks', () => ({ + useCourseUpgrade: jest.fn(), + useTrackEvent: jest.fn(() => ({ track: jest.fn() })), +})); + jest.mock('../../utils/surveyMonkey', () => jest.fn()); jest.mock('@edx/frontend-platform/analytics', () => ({ @@ -35,10 +40,6 @@ jest.mock('../../experiments', () => ({ usePromptExperimentDecision: jest.fn(), })); -jest.mock('../../hooks', () => ({ - useCourseUpgrade: jest.fn(), -})); - const defaultProps = { courseId: 'some-course-id', isOpen: true, @@ -73,6 +74,8 @@ describe('', () => { useTrackEvent.mockReturnValue({ track: jest.fn() }); usePromptExperimentDecision.mockReturnValue([]); useCourseUpgrade.mockReturnValue([]); + const mockedTrackEvent = jest.fn(); + useTrackEvent.mockReturnValue({ track: mockedTrackEvent }); }); describe('when it\'s open', () => { @@ -100,9 +103,8 @@ describe('', () => { }); render(undefined, { disclosureAcknowledged: true }); expect(screen.queryByTestId('sidebar-xpert')).toBeInTheDocument(); - - const daysRemainingMessage = screen.queryByTestId('x-days-remaining-message'); - expect(daysRemainingMessage).toBeInTheDocument(); + expect(screen.queryByTestId('get-days-remaining-message')).toBeInTheDocument(); + expect(screen.queryByTestId('days-remaining-message')).toBeInTheDocument(); }); it('If auditTrialDaysRemaining === 1, say final day', () => { @@ -113,9 +115,8 @@ describe('', () => { }); render(undefined, { disclosureAcknowledged: true }); expect(screen.queryByTestId('sidebar-xpert')).toBeInTheDocument(); - - const trialEndsTodayMessage = screen.queryByTestId('trial-ends-today'); - expect(trialEndsTodayMessage).toBeInTheDocument(); + expect(screen.queryByTestId('get-days-remaining-message')).toBeInTheDocument(); + expect(screen.queryByTestId('trial-ends-today')).not.toBeInTheDocument(); }); it('If auditTrialDaysRemaining < 1, do not show either of those', () => { @@ -126,11 +127,9 @@ describe('', () => { }); render(undefined, { disclosureAcknowledged: true }); expect(screen.queryByTestId('sidebar-xpert')).toBeInTheDocument(); - - const daysRemainingMessage = screen.queryByTestId('x-days-remaining-message'); - const trialEndsTodayMessage = screen.queryByTestId('trial-ends-today'); - expect(daysRemainingMessage).not.toBeInTheDocument(); - expect(trialEndsTodayMessage).not.toBeInTheDocument(); + expect(screen.queryByTestId('get-days-remaining-message')).toBeInTheDocument(); + expect(screen.queryByTestId('days-remaining-message')).not.toBeInTheDocument(); + expect(screen.queryByTestId('trial-ends-today')).not.toBeInTheDocument(); }); }); diff --git a/src/setupTest.js b/src/setupTest.js index 57647f19..0a188726 100644 --- a/src/setupTest.js +++ b/src/setupTest.js @@ -11,3 +11,15 @@ jest.mock('@src/generic/model-store', () => ({ useModel: jest.fn() }), { virtual mergeConfig({ ...process.env, }); + +// const mockModelStore = {}; +// jest.mock( +// '@src/generic/model-store', +// () => ({ +// useModel: jest.fn((type) => mockModelStore[type]), +// setModel: jest.fn((type, data) => { +// mockModelStore[type] = data; +// }), +// }), +// { virtual: true }, +// ); From 48e16b89c7939eb0500857dcd9f52fd7841796e9 Mon Sep 17 00:00:00 2001 From: ilee2u Date: Mon, 16 Dec 2024 15:49:06 -0500 Subject: [PATCH 20/25] fix: add upgrade eligibility gate for refresh --- src/components/MessageForm/index.jsx | 5 ++++- src/components/MessageForm/index.test.jsx | 12 +++++++++++- src/data/thunks.js | 8 ++++---- src/hooks/use-course-upgrade.js | 1 - src/widgets/Xpert.jsx | 2 +- 5 files changed, 20 insertions(+), 8 deletions(-) diff --git a/src/components/MessageForm/index.jsx b/src/components/MessageForm/index.jsx index 69c3c23f..cae5616f 100644 --- a/src/components/MessageForm/index.jsx +++ b/src/components/MessageForm/index.jsx @@ -10,11 +10,14 @@ import { getChatResponse, updateCurrentMessage, } from '../../data/thunks'; +import { useCourseUpgrade } from '../../hooks'; import { usePromptExperimentDecision } from '../../experiments'; import './MessageForm.scss'; const MessageForm = ({ courseId, shouldAutofocus, unitId }) => { + const { upgradeable } = useCourseUpgrade(); + const { apiIsLoading, currentMessage, apiError } = useSelector(state => state.learningAssistant); const dispatch = useDispatch(); const inputRef = useRef(); @@ -35,7 +38,7 @@ const MessageForm = ({ courseId, shouldAutofocus, unitId }) => { if (currentMessage) { dispatch(acknowledgeDisclosure(true)); dispatch(addChatMessage('user', currentMessage, courseId, promptExperimentVariationKey)); - dispatch(getChatResponse(courseId, unitId, promptExperimentVariationKey)); + dispatch(getChatResponse(courseId, unitId, upgradeable, promptExperimentVariationKey)); } }; diff --git a/src/components/MessageForm/index.test.jsx b/src/components/MessageForm/index.test.jsx index 6c8dba2b..c17d8022 100644 --- a/src/components/MessageForm/index.test.jsx +++ b/src/components/MessageForm/index.test.jsx @@ -10,11 +10,17 @@ import { acknowledgeDisclosure, addChatMessage, getChatResponse, + getLearningAssistantChatSummary, updateCurrentMessage, } from '../../data/thunks'; +import { useCourseUpgrade } from '../../hooks'; import MessageForm from '.'; +jest.mock('../../hooks', () => ({ + useCourseUpgrade: jest.fn(), +})); + jest.mock('../../utils/surveyMonkey', () => ({ showControlSurvey: jest.fn(), showVariationSurvey: jest.fn(), @@ -81,6 +87,9 @@ describe('', () => { beforeEach(() => { jest.resetAllMocks(); usePromptExperimentDecision.mockReturnValue([]); + useCourseUpgrade.mockReturnValue({ + upgradeable: true, + }); }); describe('when rendered', () => { @@ -137,7 +146,7 @@ describe('', () => { expect(acknowledgeDisclosure).toHaveBeenCalledWith(true); expect(addChatMessage).toHaveBeenCalledWith('user', currentMessage, defaultProps.courseId, undefined); - expect(getChatResponse).toHaveBeenCalledWith(defaultProps.courseId, defaultProps.unitId, undefined); + expect(getChatResponse).toHaveBeenCalledWith(defaultProps.courseId, defaultProps.unitId, true, undefined); expect(mockDispatch).toHaveBeenCalledTimes(3); }); @@ -187,6 +196,7 @@ describe('', () => { expect(getChatResponse).toHaveBeenCalledWith( defaultProps.courseId, defaultProps.unitId, + true, OPTIMIZELY_PROMPT_EXPERIMENT_VARIATION_KEYS.UPDATED_PROMPT, ); expect(mockDispatch).toHaveBeenCalledTimes(3); diff --git a/src/data/thunks.js b/src/data/thunks.js index 5d654646..ac069283 100644 --- a/src/data/thunks.js +++ b/src/data/thunks.js @@ -56,7 +56,7 @@ export function addChatMessage(role, content, courseId, promptExperimentVariatio }; } -export function getChatResponse(courseId, unitId, promptExperimentVariationKey = undefined) { +export function getChatResponse(courseId, unitId, upgradeable, promptExperimentVariationKey = undefined) { return async (dispatch, getState) => { const { userId } = getAuthenticatedUser(); const { messageList } = getState().learningAssistant; @@ -69,9 +69,9 @@ export function getChatResponse(courseId, unitId, promptExperimentVariationKey = const customQueryParams = promptExperimentVariationKey ? { responseVariation: promptExperimentVariationKey } : {}; const message = await fetchChatResponse(courseId, messageList, unitId, customQueryParams); - // Refresh chat summary only on the first message so we can tell if the user has initiated an audit trial - // NOTE: This is a bit of a hacky solution that may be refined later - if (messageList.length === 1) { + // Refresh chat summary only on the first message for an upgrade eligible user + // so we can tell if the user has just initiated an audit trial + if (messageList.length === 1 && upgradeable) { // eslint-disable-next-line no-use-before-define dispatch(getLearningAssistantChatSummary(courseId)); } diff --git a/src/hooks/use-course-upgrade.js b/src/hooks/use-course-upgrade.js index 3d3037bc..ad47c5e8 100644 --- a/src/hooks/use-course-upgrade.js +++ b/src/hooks/use-course-upgrade.js @@ -52,7 +52,6 @@ export default function useCourseUpgrade() { auditTrialDaysRemaining = Math.ceil((auditTrialExpirationDate - Date.now()) / millisecondsInOneDay); auditTrialExpired = auditTrialDaysRemaining < 0; - // console.log({ upgradeUrl }, { auditTrialDaysRemaining }); } const isFBE = !!accessExpiration && !!datesBannerInfo?.contentTypeGatingEnabled; diff --git a/src/widgets/Xpert.jsx b/src/widgets/Xpert.jsx index 2a5b32d3..65539d69 100644 --- a/src/widgets/Xpert.jsx +++ b/src/widgets/Xpert.jsx @@ -30,7 +30,7 @@ const Xpert = ({ }; useEffect(() => { - dispatch(getLearningAssistantChatSummary(courseId)); + dispatch(getLearningAssistantChatSummary(courseId, isUpgradeEligible)); }, [dispatch, courseId]); return isEnabled ? ( From 4d9f530cbfe5d9a23b202d90943d9631a5641e5f Mon Sep 17 00:00:00 2001 From: ilee2u Date: Mon, 16 Dec 2024 15:51:01 -0500 Subject: [PATCH 21/25] chore: lint --- src/components/MessageForm/index.test.jsx | 1 - src/widgets/Xpert.jsx | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/components/MessageForm/index.test.jsx b/src/components/MessageForm/index.test.jsx index c17d8022..220bfbdf 100644 --- a/src/components/MessageForm/index.test.jsx +++ b/src/components/MessageForm/index.test.jsx @@ -10,7 +10,6 @@ import { acknowledgeDisclosure, addChatMessage, getChatResponse, - getLearningAssistantChatSummary, updateCurrentMessage, } from '../../data/thunks'; import { useCourseUpgrade } from '../../hooks'; diff --git a/src/widgets/Xpert.jsx b/src/widgets/Xpert.jsx index 65539d69..2a5b32d3 100644 --- a/src/widgets/Xpert.jsx +++ b/src/widgets/Xpert.jsx @@ -30,7 +30,7 @@ const Xpert = ({ }; useEffect(() => { - dispatch(getLearningAssistantChatSummary(courseId, isUpgradeEligible)); + dispatch(getLearningAssistantChatSummary(courseId)); }, [dispatch, courseId]); return isEnabled ? ( From 3d2d0c15dc5197d101875db8935be0ade24b411e Mon Sep 17 00:00:00 2001 From: ilee2u Date: Mon, 16 Dec 2024 16:51:03 -0500 Subject: [PATCH 22/25] chore: more nits :D --- src/components/Sidebar/index.test.jsx | 1 - src/hooks/use-course-upgrade.js | 1 + src/setupTest.js | 12 ------------ 3 files changed, 1 insertion(+), 13 deletions(-) diff --git a/src/components/Sidebar/index.test.jsx b/src/components/Sidebar/index.test.jsx index a35be1cc..b038ae69 100644 --- a/src/components/Sidebar/index.test.jsx +++ b/src/components/Sidebar/index.test.jsx @@ -94,7 +94,6 @@ describe('', () => { expect(screen.queryByTestId('sidebar-xpert')).toBeInTheDocument(); }); - // TODO: Write out these tests. it('If auditTrialDaysRemaining > 1, show days remaining', () => { useCourseUpgrade.mockReturnValue({ upgradeable: true, diff --git a/src/hooks/use-course-upgrade.js b/src/hooks/use-course-upgrade.js index ad47c5e8..9373d151 100644 --- a/src/hooks/use-course-upgrade.js +++ b/src/hooks/use-course-upgrade.js @@ -40,6 +40,7 @@ export default function useCourseUpgrade() { auditTrialLengthDays, auditTrial, } = useSelector(state => state.learningAssistant); + const upgradeUrl = offer?.upgradeUrl || verifiedMode?.upgradeUrl; if (!isUpgradeEligible || !upgradeUrl) { return { upgradeable: false }; } diff --git a/src/setupTest.js b/src/setupTest.js index 0a188726..57647f19 100644 --- a/src/setupTest.js +++ b/src/setupTest.js @@ -11,15 +11,3 @@ jest.mock('@src/generic/model-store', () => ({ useModel: jest.fn() }), { virtual mergeConfig({ ...process.env, }); - -// const mockModelStore = {}; -// jest.mock( -// '@src/generic/model-store', -// () => ({ -// useModel: jest.fn((type) => mockModelStore[type]), -// setModel: jest.fn((type, data) => { -// mockModelStore[type] = data; -// }), -// }), -// { virtual: true }, -// ); From 7c97e5f831422c9c31936fc10a28cda2512a07fd Mon Sep 17 00:00:00 2001 From: ilee2u Date: Tue, 17 Dec 2024 13:25:03 -0500 Subject: [PATCH 23/25] chore: make intl name more descriptive --- src/components/Sidebar/index.jsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/Sidebar/index.jsx b/src/components/Sidebar/index.jsx index a540d0b1..0134acad 100644 --- a/src/components/Sidebar/index.jsx +++ b/src/components/Sidebar/index.jsx @@ -83,10 +83,10 @@ const Sidebar = ({ const getDaysRemainingMessage = () => { if (auditTrialDaysRemaining > 1) { - const irtl = new Intl.RelativeTimeFormat({ style: 'long' }); + const intlRelativeTime = new Intl.RelativeTimeFormat({ style: 'long' }); return (
- Your trial ends {irtl.format(auditTrialDaysRemaining, 'day')}. Upgrade for full access to Xpert. + Your trial ends {intlRelativeTime.format(auditTrialDaysRemaining, 'day')}. Upgrade for full access to Xpert.
); } if (auditTrialDaysRemaining === 1) { From 1ac0c541be1f63ef5772d5a4d540e5e0b4ea4de1 Mon Sep 17 00:00:00 2001 From: ilee2u Date: Tue, 17 Dec 2024 13:45:20 -0500 Subject: [PATCH 24/25] fix: remove paywall placeholder + rebase main pt 2 --- src/components/Sidebar/index.jsx | 13 +++++-------- src/components/Sidebar/index.test.jsx | 5 +++-- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/src/components/Sidebar/index.jsx b/src/components/Sidebar/index.jsx index 0134acad..f2172099 100644 --- a/src/components/Sidebar/index.jsx +++ b/src/components/Sidebar/index.jsx @@ -31,7 +31,9 @@ const Sidebar = ({ messageList, } = useSelector(state => state.learningAssistant); - const { upgradeable, upgradeUrl, auditTrialDaysRemaining } = useCourseUpgrade(); + const { + upgradeable, upgradeUrl, auditTrialExpired, auditTrialDaysRemaining, + } = useCourseUpgrade(); const chatboxContainerRef = useRef(null); @@ -81,7 +83,7 @@ const Sidebar = ({ ); - const getDaysRemainingMessage = () => { + const getDaysRemainingMessage = () => { // eslint-disable-line consistent-return if (auditTrialDaysRemaining > 1) { const intlRelativeTime = new Intl.RelativeTimeFormat({ style: 'long' }); return ( @@ -95,12 +97,7 @@ const Sidebar = ({ Your trial ends today! Upgrade for full access to Xpert.
); - } // TODO: Show the upgrade screen instead of this banner, to be done in future ticket - return ( -
- Your trial has expired. Upgrade for full access to Xpert. -
- ); + } }; const getSidebar = () => ( diff --git a/src/components/Sidebar/index.test.jsx b/src/components/Sidebar/index.test.jsx index b038ae69..2894cfbd 100644 --- a/src/components/Sidebar/index.test.jsx +++ b/src/components/Sidebar/index.test.jsx @@ -2,11 +2,12 @@ import React from 'react'; import { screen, act } from '@testing-library/react'; import { usePromptExperimentDecision } from '../../experiments'; -import { useCourseUpgrade, useTrackEvent } from '../../hooks'; +import { + useCourseUpgrade, useTrackEvent, +} from '../../hooks'; import { render as renderComponent } from '../../utils/utils.test'; import { initialState } from '../../data/slice'; import showSurvey from '../../utils/surveyMonkey'; -import { useCourseUpgrade, useTrackEvent } from '../../hooks'; import Sidebar from '.'; From 0352c858cfb11af12d9fee599de1e991b8104485 Mon Sep 17 00:00:00 2001 From: ilee2u Date: Tue, 17 Dec 2024 14:21:29 -0500 Subject: [PATCH 25/25] feat: track event on upgrade link click --- src/components/Sidebar/index.jsx | 31 ++++++++++++++++++++++----- src/components/Sidebar/index.test.jsx | 24 ++++++++++++++++----- 2 files changed, 45 insertions(+), 10 deletions(-) diff --git a/src/components/Sidebar/index.jsx b/src/components/Sidebar/index.jsx index f2172099..5f6425d8 100644 --- a/src/components/Sidebar/index.jsx +++ b/src/components/Sidebar/index.jsx @@ -13,9 +13,9 @@ import showSurvey from '../../utils/surveyMonkey'; import APIError from '../APIError'; import ChatBox from '../ChatBox'; import Disclosure from '../Disclosure'; -import UpgradePanel from '../UpgradePanel'; import MessageForm from '../MessageForm'; -import { useCourseUpgrade } from '../../hooks'; +import UpgradePanel from '../UpgradePanel'; +import { useCourseUpgrade, useTrackEvent } from '../../hooks'; import { ReactComponent as XpertLogo } from '../../assets/xpert-logo.svg'; import './Sidebar.scss'; @@ -35,6 +35,8 @@ const Sidebar = ({ upgradeable, upgradeUrl, auditTrialExpired, auditTrialDaysRemaining, } = useCourseUpgrade(); + const { track } = useTrackEvent(); + const chatboxContainerRef = useRef(null); // this use effect is intended to scroll to the bottom of the chat window, in the case @@ -83,25 +85,44 @@ const Sidebar = ({ ); + const handleUpgradeLinkClick = () => { + track('edx.ui.lms.learning_assistant.days_remaining_banner_upgrade_click'); + }; + + const getUpgradeLink = () => ( + + Upgrade + + ); + const getDaysRemainingMessage = () => { // eslint-disable-line consistent-return if (auditTrialDaysRemaining > 1) { const intlRelativeTime = new Intl.RelativeTimeFormat({ style: 'long' }); return (
- Your trial ends {intlRelativeTime.format(auditTrialDaysRemaining, 'day')}. Upgrade for full access to Xpert. + Your trial ends {intlRelativeTime.format(auditTrialDaysRemaining, 'day')}. {getUpgradeLink()} for full access to Xpert.
); } if (auditTrialDaysRemaining === 1) { return (
- Your trial ends today! Upgrade for full access to Xpert. + Your trial ends today! {getUpgradeLink()} for full access to Xpert.
); } }; const getSidebar = () => ( -
+
diff --git a/src/components/Sidebar/index.test.jsx b/src/components/Sidebar/index.test.jsx index 2894cfbd..2f6f5421 100644 --- a/src/components/Sidebar/index.test.jsx +++ b/src/components/Sidebar/index.test.jsx @@ -1,5 +1,5 @@ import React from 'react'; -import { screen, act } from '@testing-library/react'; +import { fireEvent, screen, act } from '@testing-library/react'; import { usePromptExperimentDecision } from '../../experiments'; import { @@ -13,7 +13,7 @@ import Sidebar from '.'; jest.mock('../../hooks', () => ({ useCourseUpgrade: jest.fn(), - useTrackEvent: jest.fn(() => ({ track: jest.fn() })), + useTrackEvent: jest.fn(), })); jest.mock('../../utils/surveyMonkey', () => jest.fn()); @@ -72,11 +72,9 @@ describe('', () => { beforeEach(async () => { jest.resetAllMocks(); useCourseUpgrade.mockReturnValue({ upgradeable: false }); - useTrackEvent.mockReturnValue({ track: jest.fn() }); usePromptExperimentDecision.mockReturnValue([]); useCourseUpgrade.mockReturnValue([]); - const mockedTrackEvent = jest.fn(); - useTrackEvent.mockReturnValue({ track: mockedTrackEvent }); + useTrackEvent.mockReturnValue({ track: jest.fn() }); }); describe('when it\'s open', () => { @@ -119,6 +117,22 @@ describe('', () => { expect(screen.queryByTestId('trial-ends-today')).not.toBeInTheDocument(); }); + it('should call track event on click', () => { + useCourseUpgrade.mockReturnValue({ + upgradeable: true, + upgradeUrl: 'https://mockurl.com', + auditTrialDaysRemaining: 1, + }); + const mockedTrackEvent = jest.fn(); + useTrackEvent.mockReturnValue({ track: mockedTrackEvent }); + + render(undefined, { disclosureAcknowledged: true }); + const upgradeLink = screen.queryByTestId('days_remaining_banner_upgrade_link'); + expect(mockedTrackEvent).not.toHaveBeenCalled(); + fireEvent.click(upgradeLink); + expect(mockedTrackEvent).toHaveBeenCalledWith('edx.ui.lms.learning_assistant.days_remaining_banner_upgrade_click'); + }); + it('If auditTrialDaysRemaining < 1, do not show either of those', () => { useCourseUpgrade.mockReturnValue({ upgradeable: true,