Skip to content

Commit

Permalink
feat: add trial days remaining banner (#72)
Browse files Browse the repository at this point in the history
* feat: add trial days remaining banner

- temp commit as I set up the code

* temp: working version w/ debug logs

* feat: better functionality for days remaining

* fix: set auditTrialLengthDays + nits

* feat: add upgrade url to days remaining banner

* feat: simplify refresh chat summary call

* temp: rollback point for tests

* feat: only refresh chat-summary on 1st msg

* temp: attempting to mock useModel + factories

* temp: some nits

* fix: re-syncing: mock useModel + thunk work

* temp: progressing on using new audit trial hooks

* temp: removing useModel mock

* temp: trying to get hook to work

* chore: lint

* temp: attempting to test Sidebar

- getting a strange error where hooks aren't being picked up...

* chore: lint

* chore: trying to get around this weird lint error

* test: completed working unit tests

* fix: add upgrade eligibility gate for refresh

* chore: lint

* chore: more nits :D

* chore: make intl name more descriptive

* fix: remove paywall placeholder + rebase main pt 2

* feat: track event on upgrade link click
  • Loading branch information
ilee2u authored Dec 17, 2024
1 parent 38de6a3 commit daaa1cb
Show file tree
Hide file tree
Showing 8 changed files with 159 additions and 31 deletions.
5 changes: 4 additions & 1 deletion src/components/MessageForm/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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));
}
};

Expand Down
11 changes: 10 additions & 1 deletion src/components/MessageForm/index.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,14 @@ import {
getChatResponse,
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(),
Expand Down Expand Up @@ -81,6 +86,9 @@ describe('<MessageForm />', () => {
beforeEach(() => {
jest.resetAllMocks();
usePromptExperimentDecision.mockReturnValue([]);
useCourseUpgrade.mockReturnValue({
upgradeable: true,
});
});

describe('when rendered', () => {
Expand Down Expand Up @@ -137,7 +145,7 @@ describe('<MessageForm />', () => {

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);
});

Expand Down Expand Up @@ -187,6 +195,7 @@ describe('<MessageForm />', () => {
expect(getChatResponse).toHaveBeenCalledWith(
defaultProps.courseId,
defaultProps.unitId,
true,
OPTIMIZELY_PROMPT_EXPERIMENT_VARIATION_KEYS.UPDATED_PROMPT,
);
expect(mockDispatch).toHaveBeenCalledTimes(3);
Expand Down
5 changes: 5 additions & 0 deletions src/components/Sidebar/Sidebar.scss
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,11 @@
height: 30px;
}
}

.trial-header {
font-size: 0.9em;
background-color: #F49974;
}
}

.separator {
Expand Down
55 changes: 51 additions & 4 deletions src/components/Sidebar/index.jsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,21 @@
import React, { useRef, useEffect } from 'react';
import { useSelector } from 'react-redux';
import PropTypes from 'prop-types';

import {
Icon,
IconButton,
} from '@openedx/paragon';
import { Close } from '@openedx/paragon/icons';

import { useCourseUpgrade } from '../../hooks';
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 UpgradePanel from '../UpgradePanel';
import { useCourseUpgrade, useTrackEvent } from '../../hooks';
import { ReactComponent as XpertLogo } from '../../assets/xpert-logo.svg';
import './Sidebar.scss';

Expand All @@ -30,7 +31,11 @@ const Sidebar = ({
messageList,
} = useSelector(state => state.learningAssistant);

const { upgradeable, auditTrialExpired } = useCourseUpgrade();
const {
upgradeable, upgradeUrl, auditTrialExpired, auditTrialDaysRemaining,
} = useCourseUpgrade();

const { track } = useTrackEvent();

const chatboxContainerRef = useRef(null);

Expand Down Expand Up @@ -80,11 +85,53 @@ const Sidebar = ({
<MessageForm courseId={courseId} shouldAutofocus unitId={unitId} />
);

const handleUpgradeLinkClick = () => {
track('edx.ui.lms.learning_assistant.days_remaining_banner_upgrade_click');
};

const getUpgradeLink = () => (
<a
onClick={handleUpgradeLinkClick}
target="_blank"
href={upgradeUrl}
rel="noreferrer"
data-testid="days_remaining_banner_upgrade_link"
>
Upgrade
</a>
);

const getDaysRemainingMessage = () => { // eslint-disable-line consistent-return
if (auditTrialDaysRemaining > 1) {
const intlRelativeTime = new Intl.RelativeTimeFormat({ style: 'long' });
return (
<div data-testid="days-remaining-message">
Your trial ends {intlRelativeTime.format(auditTrialDaysRemaining, 'day')}. {getUpgradeLink()} for full access to Xpert.
</div>
);
} if (auditTrialDaysRemaining === 1) {
return (
<div data-testid="trial-ends-today-message">
Your trial ends today! {getUpgradeLink()} for full access to Xpert.
</div>
);
}
};

const getSidebar = () => (
<div className="h-100 d-flex flex-column justify-content-stretch" data-testid="sidebar-xpert">
<div
className="h-100 d-flex flex-column justify-content-stretch"
data-testid="sidebar-xpert"
>
<div className="p-3 sidebar-header" data-testid="sidebar-xpert-header">
<XpertLogo />
</div>
{upgradeable
&& (
<div className="p-3 trial-header" data-testid="get-days-remaining-message">
{getDaysRemainingMessage()}
</div>
)}
<span className="separator" />
<ChatBox
chatboxContainerRef={chatboxContainerRef}
Expand Down
73 changes: 60 additions & 13 deletions src/components/Sidebar/index.test.jsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,21 @@
import React from 'react';
import { screen, act } from '@testing-library/react';
import { fireEvent, screen, act } from '@testing-library/react';

import { usePromptExperimentDecision } from '../../experiments';
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 '.';

jest.mock('../../hooks', () => ({
useCourseUpgrade: jest.fn(),
useTrackEvent: jest.fn(),
}));

jest.mock('../../utils/surveyMonkey', () => jest.fn());

jest.mock('@edx/frontend-platform/analytics', () => ({
Expand All @@ -34,11 +41,6 @@ jest.mock('../../experiments', () => ({
usePromptExperimentDecision: jest.fn(),
}));

jest.mock('../../hooks', () => ({
useCourseUpgrade: jest.fn(),
useTrackEvent: jest.fn(),
}));

const defaultProps = {
courseId: 'some-course-id',
isOpen: true,
Expand Down Expand Up @@ -67,11 +69,12 @@ const render = async (props = {}, sliceState = {}) => {
};

describe('<Sidebar />', () => {
beforeEach(() => {
beforeEach(async () => {
jest.resetAllMocks();
useCourseUpgrade.mockReturnValue({ upgradeable: false });
useTrackEvent.mockReturnValue({ track: jest.fn() });
usePromptExperimentDecision.mockReturnValue([]);
useCourseUpgrade.mockReturnValue([]);
useTrackEvent.mockReturnValue({ track: jest.fn() });
});

describe('when it\'s open', () => {
Expand All @@ -90,13 +93,57 @@ describe('<Sidebar />', () => {
expect(screen.queryByTestId('sidebar-xpert')).toBeInTheDocument();
});

it('should not render xpert if audit trial is expired', () => {
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();
expect(screen.queryByTestId('get-days-remaining-message')).toBeInTheDocument();
expect(screen.queryByTestId('days-remaining-message')).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();
expect(screen.queryByTestId('get-days-remaining-message')).toBeInTheDocument();
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,
upgradeUrl: 'https://mockurl.com',
auditTrialDaysRemaining: 0,
});
render(undefined, { disclosureAcknowledged: true });
expect(screen.queryByTestId('sidebar-xpert')).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();
});
});

Expand Down
28 changes: 22 additions & 6 deletions src/data/thunks.js
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -55,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;
Expand All @@ -68,6 +69,13 @@ 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 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));
}

dispatch(setApiIsLoading(false));
dispatch(addChatMessage(message.role, message.content, courseId, promptExperimentVariationKey));
} catch (error) {
Expand Down Expand Up @@ -129,11 +137,19 @@ export function getLearningAssistantChatSummary(courseId) {
}

// Audit Trial
const auditTrial = data.audit_trial;

// If returned audit trial data is not empty
if (Object.keys(auditTrial).length !== 0) {
dispatch(setAuditTrial(auditTrial));
const auditTrial = {
startDate: data.audit_trial.start_date,
expirationDate: data.audit_trial.expiration_date,
};

// 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)); }
Expand Down
11 changes: 6 additions & 5 deletions src/data/thunks.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ describe('Thunks unit tests', () => {

describe('addChatMessage()', () => {
const mockDate = new Date(2024, 1, 1);
beforeAll(() => {

beforeEach(async () => {
jest.useFakeTimers('modern');
jest.setSystemTime(mockDate);
});
Expand Down Expand Up @@ -110,8 +111,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',
},
});

Expand Down Expand Up @@ -158,8 +159,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',
},
});

Expand Down
2 changes: 1 addition & 1 deletion src/utils/optimizelyExperiment.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down

0 comments on commit daaa1cb

Please sign in to comment.