Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

P20-119: Add pre-lockdown parental permission banner #58434

Merged
merged 34 commits into from
May 18, 2024
Merged
Show file tree
Hide file tree
Changes from 30 commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
35954d5
P20-57: Add pre-lockdown parent permission modal
artem-vavilov Apr 23, 2024
90cd18e
P20-57: Hide the modal behind CPA experience `cpa_all_user_lockout_wa…
artem-vavilov May 1, 2024
5a6fd0c
P20-57: Display modal when parent permission is not requested
artem-vavilov May 1, 2024
a6d42ee
P20-57: Use `ChildAccount` namespace
artem-vavilov May 1, 2024
eda3c1f
P20-57: Rename reducer
artem-vavilov May 1, 2024
117349e
P20-57: Add param `show_pre_lockdown_parent_permission_modal` to forc…
artem-vavilov May 1, 2024
6558300
P20-57: Add i18n corrections
artem-vavilov May 1, 2024
8451021
P20-57: Replace promises with async/await in reducer
artem-vavilov May 1, 2024
854f205
P20-57: Rename component
artem-vavilov May 2, 2024
fa4e836
P20-57: Small fix
artem-vavilov May 2, 2024
d552e10
Merge remote-tracking branch 'origin' into P20-57/pre-lockdown-parren…
artem-vavilov May 2, 2024
33fd338
P20-57: Fix currentLocale
artem-vavilov May 2, 2024
36ce9b1
P20-57: Fix apps/src/util/locale-do-not-import.js
artem-vavilov May 2, 2024
1b9b01c
P20-57: Sort i18n strings alphabetically
artem-vavilov May 3, 2024
dd2dea5
P20-57: Fix `Cpa.cpa_experience` desc
artem-vavilov May 3, 2024
84d4e4a
P20-57: Improve component file structure
artem-vavilov May 3, 2024
1d4c211
Merge remote-tracking branch 'origin' into P20-57/pre-lockdown-parren…
artem-vavilov May 6, 2024
34a931d
P20-119: Add pre-lockdown parental permission banner
artem-vavilov May 2, 2024
854da70
P20-119: Replace `Date()` with `new Date().toISOString()`
artem-vavilov May 9, 2024
8567fc1
P20-119: Add colors prop to `Notification` component
artem-vavilov May 9, 2024
d27ca22
P20-119: Fix component stories
artem-vavilov May 9, 2024
37c0077
Merge remote-tracking branch 'origin' into P20-119/parental-permissio…
artem-vavilov May 10, 2024
cb3a568
P20-119: Report events to Amplitude instead of Statsig
artem-vavilov May 10, 2024
0e74324
P20-119: Report banner shown event
artem-vavilov May 10, 2024
eb58bb2
P20-119: Report inSection to Amplitude
artem-vavilov May 10, 2024
76eb2be
P20-119: Render banner only when user is signed in
artem-vavilov May 13, 2024
43bced6
P20-119: Fix Policies::ChildAccountTest
artem-vavilov May 13, 2024
814c865
Merge remote-tracking branch 'origin' into P20-119/parental-permissio…
artem-vavilov May 13, 2024
eb83640
P20-119: Fix Unit page
artem-vavilov May 13, 2024
61e5047
P20-119: Fix lint complaints
artem-vavilov May 13, 2024
6c80476
Merge remote-tracking branch 'origin' into P20-119/parental-permissio…
artem-vavilov May 17, 2024
8c6449b
P20-119: Fix typos
artem-vavilov May 17, 2024
1a0b25c
P20-119: Fix banner data on script page
artem-vavilov May 17, 2024
40862e5
P20-119: Fix tests for `Policies::ChildAccount.user_predates_policy?`
artem-vavilov May 17, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions apps/i18n/common/en_us.json
Original file line number Diff line number Diff line change
Expand Up @@ -1996,6 +1996,9 @@
"plugged": "Plugged",
"pluggedLessonsNote": "*Online or ‘plugged’ lessons are automatically marked as complete on your behalf once 80% of your class has completed 60% of the available lesson.",
"picturePassword": "Picture password",
"policyCompliance_parentalPermissionBanner_title": "Heads up!",
"policyCompliance_parentalPermissionBanner_desc": "You will need your parent or guardian’s permission by {lockoutDate} to keep using your Code.org account.",
"policyCompliance_parentalPermissionBanner_button": "Get permission",
"policyCompliance_parentalPermissionModal_newRequestForm_title": "Heads up! Your account may soon be locked.",
"policyCompliance_parentalPermissionModal_newRequestForm_text1": "Starting on {lockoutDate}, **you will need your parent or guardian’s permission to keep your personal login** with Code.org.",
"policyCompliance_parentalPermissionModal_newRequestForm_text2": "To avoid having your account locked, please share your parent or guardian’s email address so they can grant you permission.",
Expand Down
18 changes: 11 additions & 7 deletions apps/src/lib/util/AnalyticsConstants.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,17 @@ const EVENTS = {
CAP_STATE_FORM_SHOW: 'CAP State Form Shown',
CAP_STATE_FORM_PROVIDED: 'CAP State Form Submitted',
CAP_STATE_FORM_DISMISSED: 'CAP State Form Sign Out Button Clicked',
CPA_PARENT_EMAIL_BANNER_SHOWN: 'cpa_parent_email_banner_shown',
CPA_PARENT_EMAIL_BANNER_CLICKED: 'cpa_parent_email_banner_clicked',
CPA_PARENT_EMAIL_BANNER_SUBMITTED: 'cpa_parent_email_banner_submitted',
CPA_PARENT_EMAIL_BANNER_UPDATED: 'cpa_parent_email_banner_updated',
CPA_PARENT_EMAIL_BANNER_RESEND: 'cpa_parent_email_banner_resend',
CPA_PARENT_EMAIL_BANNER_CLOSED: 'cpa_parent_email_banner_closed',
CPA_PARENT_EMAIL_MODAL_SHOWN: 'cpa_parent_email_modal_shown',
CPA_PARENT_EMAIL_MODAL_SUBMITTED: 'cpa_parent_email_modal_submitted',
CPA_PARENT_EMAIL_MODAL_UPDATED: 'cpa_parent_email_modal_updated',
CPA_PARENT_EMAIL_MODAL_RESEND: 'cpa_parent_email_modal_resend',
CPA_PARENT_EMAIL_MODAL_CLOSED: 'cpa_parent_email_modal_closed',

// School Selection Component
COUNTRY_SELECTED: 'User Selects Country',
Expand Down Expand Up @@ -273,13 +284,6 @@ const EVENTS = {
CHAT_ACTION: 'Student takes a chat action',
SAVE_MODEL_CARD_INFO: 'Student saves their model card info',
PUBLISH_MODEL_CARD_INFO: 'Student publishes their model card info',

// CPA
CPA_PARENT_EMAIL_MODAL_SHOWN: 'cpa_parent_email_modal_shown',
CPA_PARENT_EMAIL_MODAL_SUBMITTED: 'cpa_parent_email_modal_submitted',
CPA_PARENT_EMAIL_MODAL_UPDATED: 'cpa_parent_email_modal_updated',
CPA_PARENT_EMAIL_MODAL_RESEND: 'cpa_parent_email_modal_resend',
CPA_PARENT_EMAIL_MODAL_CLOSED: 'cpa_parent_email_modal_closed',
};

const EVENT_GROUP_NAMES = {
Expand Down
15 changes: 14 additions & 1 deletion apps/src/redux/parentalPermissionRequestReducer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,19 @@ export const REQUEST_PARENTAL_PERMISSION_SUCCESS =
'requestParentalPermission/success';
const REQUEST_PARENTAL_PERMISSION_FAILURE = 'requestParentalPermission/failure';

interface ParentalPermissionRequest {
const RESET_PARENTAL_PERMISSION_REQUEST = 'resetParentalPermissionRequest';

export interface ParentalPermissionRequest {
parent_email: string;
requested_at: string;
resends_sent: number;
consent_status: string;
}

type ParentalPermissionRequestAction =
| {
type: typeof RESET_PARENTAL_PERMISSION_REQUEST;
}
| {
type: typeof FETCH_PENDING_PERMISSION_REQUEST_PERFORM;
}
Expand Down Expand Up @@ -62,6 +67,10 @@ export default function parentalPermissionRequestReducer(
action: ParentalPermissionRequestAction
): ParentalPermissionRequestState {
switch (action.type) {
case RESET_PARENTAL_PERMISSION_REQUEST:
return {
isLoading: false,
};
case FETCH_PENDING_PERMISSION_REQUEST_PERFORM:
return {
...state,
Expand Down Expand Up @@ -169,3 +178,7 @@ export const requestParentalPermission = async (
});
}
};

export const resetParentalPermissionRequest = (
dispatch: Dispatch<ParentalPermissionRequestAction>
) => dispatch({type: RESET_PARENTAL_PERMISSION_REQUEST});
8 changes: 8 additions & 0 deletions apps/src/sites/studio/pages/home/_homepage.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {initializeHiddenScripts} from '@cdo/apps/code-studio/hiddenLessonRedux';
import {queryParams, updateQueryParam} from '@cdo/apps/code-studio/utils';
import locales, {setLocaleCode} from '@cdo/apps/redux/localesRedux';
import mapboxReducer, {setMapboxAccessToken} from '@cdo/apps/redux/mapbox';
import ParentalPermissionBanner from '@cdo/apps/templates/policy_compliance/ParentalPermissionBanner';

$(document).ready(showHomepage);

Expand Down Expand Up @@ -80,6 +81,12 @@ function showHomepage() {
}

const announcement = getTeacherAnnouncement(announcementOverride);
const parentalPermissionBanner = homepageData.parentalPermissionBanner && (
<ParentalPermissionBanner
key="parentla-permission-bnner"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

two subtle typos in the key: parental-permission-banner

{...homepageData.parentalPermissionBanner}
/>
);

ReactDOM.render(
<Provider store={store}>
Expand Down Expand Up @@ -130,6 +137,7 @@ function showHomepage() {
homepageData.showStudentAsVerifiedTeacherWarning
}
specialAnnouncement={studentSpecialAnnouncement}
topComponents={[parentalPermissionBanner]}
/>
)}
</div>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import React from 'react';
import ReactDOM from 'react-dom';
import {Provider} from 'react-redux';

import {getStore} from '@cdo/apps/redux';
import getScriptData from '@cdo/apps/util/getScriptData';
import ParentalPermissionBanner from '@cdo/apps/templates/policy_compliance/ParentalPermissionBanner';

document.addEventListener('DOMContentLoaded', () => {
ReactDOM.render(
<Provider store={getStore()}>
<ParentalPermissionBanner lockoutDate={getScriptData('lockoutDate')} />
</Provider>,
document.getElementById('parental-permission-banner-container')
);
});
Original file line number Diff line number Diff line change
@@ -1,20 +1,60 @@
import React from 'react';
import ReactDOM from 'react-dom';
import moment from 'moment';

import {tryGetLocalStorage, trySetLocalStorage} from '@cdo/apps/utils';
import getScriptData from '@cdo/apps/util/getScriptData';
import ParentalPermissionModal from '@cdo/apps/templates/policy_compliance/ParentalPermissionModal';
import analyticsReporter from '@cdo/apps/lib/util/AnalyticsReporter';
import {EVENTS, PLATFORMS} from '@cdo/apps/lib/util/AnalyticsConstants';

const SHOW_DELAY = 86400; // 1 day

document.addEventListener('DOMContentLoaded', () => {
const renderModal = () => {
const lockoutDate = new Date(getScriptData('lockoutDate'));
const inSection = getScriptData('inSection');

const reportEvent = (eventName, payload = {}) => {
payload.isSection = inSection;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this supposed to be isSection or inSection in the event?

analyticsReporter.sendEvent(eventName, payload, PLATFORMS.AMPLITUDE);
};

const handleClose = () => {
reportEvent(EVENTS.CPA_PARENT_EMAIL_MODAL_CLOSED);
};

const handleSubmit = parentalPermissionRequest => {
reportEvent(EVENTS.CPA_PARENT_EMAIL_MODAL_SUBMITTED, {
consentStatus: parentalPermissionRequest.consent_status,
});
};

return ReactDOM.render(
<ParentalPermissionModal lockoutDate={lockoutDate} />,
const handleResend = (prevPPR, PPR) => {
reportEvent(EVENTS.CPA_PARENT_EMAIL_MODAL_RESEND, {
oldConsentStatus: prevPPR.consent_status,
newConsentStatus: PPR.consent_status,
});
};

const handleUpdate = (prevPPR, PPR) => {
reportEvent(EVENTS.CPA_PARENT_EMAIL_MODAL_UPDATED, {
oldConsentStatus: prevPPR.consent_status,
newConsentStatus: PPR.consent_status,
});
};

ReactDOM.render(
<ParentalPermissionModal
lockoutDate={getScriptData('lockoutDate')}
onClose={handleClose}
onSubmit={handleSubmit}
onResend={handleResend}
onUpdate={handleUpdate}
/>,
document.getElementById('parental-permission-modal-container')
);

reportEvent(EVENTS.CPA_PARENT_EMAIL_MODAL_SHOWN);
};

if (getScriptData('forceDisplay')) {
Expand Down
8 changes: 8 additions & 0 deletions apps/src/sites/studio/pages/scripts/show.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import progress from '@cdo/apps/code-studio/progress';
import UnitOverview from '@cdo/apps/code-studio/components/progress/UnitOverview.jsx';
import {setStudentDefaultsSummaryView} from '@cdo/apps/code-studio/progressRedux';
import {updateQueryParam, queryParams} from '@cdo/apps/code-studio/utils';
import ParentalPermissionBanner from '@cdo/apps/templates/policy_compliance/ParentalPermissionBanner';

import locales, {setLocaleCode} from '../../../../redux/localesRedux';

Expand All @@ -38,6 +39,9 @@ $(document).ready(initPage);
function initPage() {
const script = document.querySelector('script[data-scriptoverview]');
const config = JSON.parse(script.dataset.scriptoverview);
const parentalPermissionBannerData =
script.dataset.parentalPermissionBanner &&
JSON.parse(script.dataset.parentalPermissionBanner);

const {scriptData, plcBreadcrumb} = config;
const store = getStore();
Expand Down Expand Up @@ -104,6 +108,10 @@ function initPage() {

ReactDOM.render(
<Provider store={store}>
{parentalPermissionBannerData && (
<ParentalPermissionBanner {...parentalPermissionBannerData} />
)}

<UnitOverview
id={scriptData.id}
courseId={scriptData.course_id}
Expand Down
2 changes: 2 additions & 0 deletions apps/src/templates/CurrentUserState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,6 @@ export interface CurrentUserState {
over21: boolean;
showProgressTableV2: boolean;
progressTableV2ClosedBeta: boolean;
childAccountComplianceState: string | null;
inSection: boolean | null;
}
23 changes: 21 additions & 2 deletions apps/src/templates/Notification.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import trackEvent from '../util/trackEvent';
import Button from './Button';

export const NotificationType = {
default: 'default',
information: 'information',
success: 'success',
failure: 'failure',
Expand Down Expand Up @@ -50,6 +51,7 @@ const Notification = ({
type,
tooltipText,
width,
colors,
}) => {
const [open, setOpen] = useState(true);

Expand Down Expand Up @@ -127,7 +129,7 @@ const Notification = ({
return null;
}

const colorStyles = styles.colors[type];
const colorStyles = {...styles.colors[type], ...colors};

const tooltipId = _.uniqueId();

Expand All @@ -136,7 +138,9 @@ const Notification = ({
<div style={{...colorStyles, ...mainStyle}}>
{type !== NotificationType.course && (
<div style={{...styles.iconBox, ...colorStyles, ...iconStyles}}>
<FontAwesome icon={icons[type]} style={styles.icon} />
{icons[type] && (
<FontAwesome icon={icons[type]} style={styles.icon} />
)}
</div>
)}
<div style={styles.contentBox}>
Expand Down Expand Up @@ -258,6 +262,17 @@ Notification.propTypes = {

// Can be specified to override default width
width: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),

colors: PropTypes.shape({
backgroundColor: PropTypes.string,
borderColor: PropTypes.string,
color: PropTypes.string,
}),
};

Notification.defaultProps = {
type: NotificationType.default,
colors: {},
};

const styles = {
Expand Down Expand Up @@ -333,6 +348,10 @@ const styles = {
marginBottom: 18,
},
colors: {
[NotificationType.default]: {
borderColor: color.teal,
backgroundColor: color.teal,
},
[NotificationType.information]: {
borderColor: color.teal,
color: color.teal,
Expand Down
3 changes: 3 additions & 0 deletions apps/src/templates/currentUserRedux.js
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ const initialState = {
under13: true,
over21: false,
childAccountComplianceState: null,
inSection: null,
};

export default function currentUser(state = initialState, action) {
Expand Down Expand Up @@ -230,6 +231,7 @@ export default function currentUser(state = initialState, action) {
date_progress_table_invitation_last_delayed,
has_seen_progress_table_v2_invitation,
child_account_compliance_state,
in_section,
} = action.serverUser;
analyticsReport.setUserProperties(
id,
Expand Down Expand Up @@ -262,6 +264,7 @@ export default function currentUser(state = initialState, action) {
date_progress_table_invitation_last_delayed,
hasSeenProgressTableInvite: has_seen_progress_table_v2_invitation,
childAccountComplianceState: child_account_compliance_state,
inSection: in_section,
};
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import {Meta, StoryFn} from '@storybook/react';
import React from 'react';
import {Provider} from 'react-redux';
import sinon from 'sinon';

import {getStore, registerReducers} from '@cdo/apps/redux';
import currentUser, {
setInitialData,
} from '@cdo/apps/templates/currentUserRedux';

import ParentalPermissionBanner from '.';

const store = getStore();
registerReducers({currentUser});
store.dispatch(
setInitialData({
id: 1,
childAccountComplianceState: 's',
})
);

export default {
component: ParentalPermissionBanner,
decorators: [
(Story: StoryFn) => (
<Provider store={store}>
<Story />
</Provider>
),
],
} as Meta;

const spy = sinon.spy();
export const Default = () => {
const state = {
parentalPermissionRequest: null,
};

// @ts-expect-error eslint-disable-next-line @typescript-eslint/ban-ts-comment
React.useReducer.restore && React.useReducer.restore();
sinon.stub(React, 'useReducer').returns([state, spy]);

return <ParentalPermissionBanner lockoutDate={new Date().toISOString()} />;
};
Loading