Compare commits

...

36 Commits

Author SHA1 Message Date
edX requirements bot
a1c35f134c chore: update browserslist DB (#1421)
Co-authored-by: abdullahwaheed <42172960+abdullahwaheed@users.noreply.github.com>
2026-03-16 00:53:02 +00:00
renovate[bot]
41af76f027 fix(deps): update dependency qs to v6.15.0 (#1418)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-09 04:52:00 +00:00
edX requirements bot
57a3b0963c chore: update browserslist DB (#1417)
Co-authored-by: abdullahwaheed <42172960+abdullahwaheed@users.noreply.github.com>
2026-03-09 00:47:58 +00:00
Emad Rad
92dcdd0a98 fix: update button labels in ConfirmationModal for better localization
Close #1415
2026-03-03 15:23:57 -03:00
edX requirements bot
29f0cefc1e chore: update browserslist DB (#1414)
Co-authored-by: abdullahwaheed <42172960+abdullahwaheed@users.noreply.github.com>
2026-03-02 00:50:06 +00:00
renovate[bot]
794a1d57b9 fix(deps): update dependency bowser to v2.14.1 (#1411)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-23 22:37:50 +00:00
edX requirements bot
fe46e8a1a6 chore: update browserslist DB (#1413)
Co-authored-by: abdullahwaheed <42172960+abdullahwaheed@users.noreply.github.com>
2026-02-23 21:07:21 +00:00
renovate[bot]
a525d3c22e fix(deps): update dependency core-js to v3.48.0 (#1412)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-23 21:00:37 +00:00
renovate[bot]
81a2c3c0d2 chore(deps): update dependency @edx/frontend-platform to v8.5.5 (#1410)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-16 05:49:56 +00:00
renovate[bot]
0018eafdcc chore(deps): update dependency @edx/browserslist-config to v1.5.1 (#1409)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-16 05:49:31 +00:00
renovate[bot]
e7db2ef753 fix(deps): update dependency qs to v6.14.2 [security] (#1408)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-16 01:12:05 +00:00
edX requirements bot
2e986d9b74 chore: update browserslist DB (#1407)
Co-authored-by: abdullahwaheed <42172960+abdullahwaheed@users.noreply.github.com>
2026-02-16 00:59:58 +00:00
Brian Smith
7c85195a27 fix(deps): regenerate package-lock.json (#1405)
* fix(deps): regenerate package-lock.json

Co-Authored-By: Claude Code <noreply@anthropic.com>

* test: update snapshots

Co-Authored-By: Claude Code <noreply@anthropic.com>

---------

Co-authored-by: Claude Code <noreply@anthropic.com>
2026-02-12 09:41:39 -05:00
edX requirements bot
b686acf5f5 chore: update browserslist DB (#1406)
Co-authored-by: abdullahwaheed <42172960+abdullahwaheed@users.noreply.github.com>
2026-02-09 00:48:51 +00:00
Anton Melser
166deeafbd docs: Generify currently supported node version 2026-01-27 09:57:11 -03:00
renovate[bot]
f33fe7d0e5 chore(deps): update dependency @edx/frontend-platform to v8.5.4 (#1404)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-26 08:50:30 +00:00
edX requirements bot
e2896dbf94 chore: update browserslist DB (#1403)
Co-authored-by: abdullahwaheed <42172960+abdullahwaheed@users.noreply.github.com>
2026-01-26 00:42:27 +00:00
renovate[bot]
f07d266a43 chore(deps): update react-router monorepo to v6.30.3 (#1402)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-19 06:45:57 +00:00
edX requirements bot
69cdc5f191 chore: update browserslist DB (#1392)
Co-authored-by: abdullahwaheed <42172960+abdullahwaheed@users.noreply.github.com>
2026-01-19 00:41:31 +00:00
Awais Ansari
4f51f71acc feat: implemented notifications configurations V3 API (#1401)
* feat: implemented notifications configurations V3 API

* fix: removed default daily email cadence when email toggle is turned on
2026-01-15 18:56:02 +05:00
renovate[bot]
c70eca1fde fix(deps): update dependency qs to v6.14.1 [security] (#1400)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-13 20:21:35 +00:00
Diana Villalvazo
39dc5bbbd2 docs: update owner/maintainer (#1398) 2026-01-13 13:27:29 -06:00
renovate[bot]
7fa61f3714 fix(deps): update dependency bowser to v2.13.1 (#1393)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-13 18:43:37 +00:00
Diana Villalvazo
8ce7c1599d test: fix X/twitter broken tests (#1399) 2026-01-13 13:39:44 -05:00
Stanislav
03bdcff331 feat: Change Twitter to X (#1215) 2025-12-02 21:46:31 +05:00
Awais Ansari
d73d840e93 feat: added env parse to boolean functionality (#1389) 2025-12-01 19:39:53 +05:00
edX requirements bot
d23b5f53df chore: update browserslist DB (#1388)
Co-authored-by: abdullahwaheed <42172960+abdullahwaheed@users.noreply.github.com>
2025-12-01 00:44:00 +00:00
Awais Ansari
da98bfa021 refactor: simplify notifications channels flag logic (#1381) 2025-11-24 16:20:58 +05:00
renovate[bot]
c37640aa69 fix(deps): update dependency core-js to v3.47.0 (#1384)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-24 05:10:14 +00:00
renovate[bot]
ea35227389 fix(deps): update dependency bowser to v2.13.0 (#1383)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-24 05:10:00 +00:00
edX requirements bot
c35bf95c1c chore: update browserslist DB (#1382)
Co-authored-by: abdullahwaheed <42172960+abdullahwaheed@users.noreply.github.com>
2025-11-24 00:39:08 +00:00
renovate[bot]
bd26928154 chore(deps): update react-router monorepo to v6.30.2 (#1379)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-17 05:47:51 +00:00
edX requirements bot
cdc8efe17b chore: update browserslist DB (#1378)
Co-authored-by: abdullahwaheed <42172960+abdullahwaheed@users.noreply.github.com>
2025-11-17 00:37:06 +00:00
edX requirements bot
8b6535ea58 chore: update browserslist DB (#1372)
Co-authored-by: abdullahwaheed <42172960+abdullahwaheed@users.noreply.github.com>
2025-11-10 00:37:40 +00:00
renovate[bot]
4c9498971a fix(deps): update dependency redux-saga to v1.4.2 (#1370)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-03 06:43:39 +00:00
edX requirements bot
a6b6a3f940 chore: update browserslist DB (#1369)
Co-authored-by: abdullahwaheed <42172960+abdullahwaheed@users.noreply.github.com>
2025-11-03 00:37:33 +00:00
27 changed files with 3143 additions and 6599 deletions

View File

@@ -20,5 +20,5 @@ Include a link to the sandbox for design changes or screenshot for before and af
#### Post-merge Checklist
* [ ] Deploy the changes to prod after verifying on stage or ask **@openedx/edx-infinity** to do it.
* [ ] Deploy the changes to prod after verifying on stage or ask **@jacobo-dominguez-wgu** to do it.
* [ ] 🎉 🙌 Celebrate! Thanks for your contribution.

View File

@@ -89,9 +89,9 @@ Cloning and Startup
``git clone https://github.com/openedx/frontend-app-account.git``
2. Use node v18.x.
2. Use the version of Node specified in the ``.nvmrc`` file.
The current version of the micro-frontend build scripts support node 18.
The current version of the micro-frontend build scripts supports the version of Node found in ``.nvmrc``.
Using other major versions of node *may* work, but this is unsupported. For
convenience, this repository includes an .nvmrc file to help in setting the
correct node version via `nvm <https://github.com/nvm-sh/nvm>`_.

View File

@@ -14,6 +14,6 @@ metadata:
openedx.org/arch-interest-groups: ""
openedx.org/release: "master"
spec:
owner: group:2u-infinity
owner: jacobo-dominguez-wgu
type: 'website'
lifecycle: 'production'

15
codecov.yml Normal file
View File

@@ -0,0 +1,15 @@
coverage:
status:
project:
default:
enabled: yes
target: auto
threshold: 0%
patch:
default:
enabled: yes
target: auto
threshold: 0%
ignore:
- "src/i18n"
- "src/index.jsx"

9470
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -44,9 +44,9 @@
"@tensorflow-models/blazeface": "0.1.0",
"@tensorflow/tfjs-converter": "4.22.0",
"@tensorflow/tfjs-core": "4.22.0",
"bowser": "2.12.1",
"bowser": "2.14.1",
"classnames": "2.5.1",
"core-js": "3.46.0",
"core-js": "3.48.0",
"font-awesome": "4.7.0",
"form-urlencoded": "6.1.6",
"formdata-polyfill": "4.0.10",
@@ -64,7 +64,7 @@
"long": "5.3.2",
"memoize-one": "^6.0.0",
"prop-types": "15.8.1",
"qs": "6.14.0",
"qs": "6.15.0",
"react": "18.3.1",
"react-dom": "18.3.1",
"react-helmet": "6.1.0",
@@ -77,14 +77,14 @@
"redux": "4.2.1",
"redux-devtools-extension": "2.13.9",
"redux-logger": "3.0.6",
"redux-saga": "1.3.0",
"redux-saga": "1.4.2",
"redux-thunk": "2.4.2",
"regenerator-runtime": "0.14.1",
"reselect": "^5.1.1",
"universal-cookie": "7.2.2"
},
"devDependencies": {
"@edx/browserslist-config": "1.5.0",
"@edx/browserslist-config": "1.5.1",
"@openedx/frontend-build": "^14.6.2",
"@testing-library/jest-dom": "6.9.1",
"@testing-library/react": "14.3.1",

View File

@@ -764,11 +764,11 @@ class AccountSettingsPage extends React.Component {
{...editableFieldProps}
/>
<EditableField
name="social_link_twitter"
name="social_link_x"
type="text"
value={this.props.formValues.social_link_twitter}
label={this.props.intl.formatMessage(messages['account.settings.field.social.platform.name.twitter'])}
emptyLabel={this.props.intl.formatMessage(messages['account.settings.field.social.platform.name.twitter.empty'])}
value={this.props.formValues.social_link_x}
label={this.props.intl.formatMessage(messages['account.settings.field.social.platform.name.xTwitter'])}
emptyLabel={this.props.intl.formatMessage(messages['account.settings.field.social.platform.name.xTwitter.empty'])}
{...editableFieldProps}
/>
</div>
@@ -905,7 +905,7 @@ AccountSettingsPage.propTypes = {
phone_number: PropTypes.string,
social_link_linkedin: PropTypes.string,
social_link_facebook: PropTypes.string,
social_link_twitter: PropTypes.string,
social_link_x: PropTypes.string,
time_zone: PropTypes.string,
state: PropTypes.string,
useVerifiedNameForCerts: PropTypes.bool.isRequired,

View File

@@ -509,15 +509,15 @@ const messages = defineMessages({
defaultMessage: 'Delete My Account',
description: 'Header for the user account deletion area',
},
'account.settings.field.social.platform.name.twitter': {
id: 'account.settings.field.social.platform.name.twitter',
defaultMessage: 'Twitter',
description: 'Label for Twitter',
'account.settings.field.social.platform.name.xTwitter': {
id: 'account.settings.field.social.platform.name.xTwitter',
defaultMessage: 'X (Twitter)',
description: 'Label for X (Twitter)',
},
'account.settings.field.social.platform.name.twitter.empty': {
id: 'account.settings.field.social.platform.name.twitter.empty',
defaultMessage: 'Add Twitter profile',
description: 'Placeholder for an empty Twitter field',
'account.settings.field.social.platform.name.xTwitter.empty': {
id: 'account.settings.field.social.platform.name.xTwitter.empty',
defaultMessage: 'Add X profile',
description: 'Placeholder for an empty X field',
},
'account.settings.field.social.platform.name.facebook': {

View File

@@ -11,7 +11,7 @@ import { postVerifiedNameConfig } from '../certificate-preference/data/service';
import { FIELD_LABELS } from './constants';
const SOCIAL_PLATFORMS = [
{ id: 'twitter', key: 'social_link_twitter' },
{ id: 'xTwitter', key: 'social_link_x' },
{ id: 'facebook', key: 'social_link_facebook' },
{ id: 'linkedin', key: 'social_link_linkedin' },
];

View File

@@ -38,24 +38,24 @@ describe('account service', () => {
it('returns unpacked account data', async () => {
const apiResponse = {
username: 'testuser',
social_links: [{ platform: 'twitter', social_link: 'http://t' }],
social_links: [{ platform: 'xTwitter', social_link: 'http://t' }],
language_proficiencies: [{ code: 'en' }],
};
mockHttpClient.get.mockResolvedValue({ data: apiResponse });
const result = await getAccount('testuser');
expect(mockHttpClient.get).toHaveBeenCalledWith('http://lms.test/api/user/v1/accounts/testuser');
expect(result.social_link_twitter).toEqual('http://t');
expect(result.social_link_x).toEqual('http://t');
expect(result.language_proficiencies).toEqual('en');
});
});
describe('patchAccount', () => {
it('sends packed commit data and returns unpacked response', async () => {
const commit = { social_link_twitter: 'http://t' };
const commit = { social_link_x: 'http://t' };
const apiResponse = {
username: 'testuser',
social_links: [{ platform: 'twitter', social_link: 'http://t' }],
social_links: [{ platform: 'xTwitter', social_link: 'http://t' }],
language_proficiencies: [],
};
mockHttpClient.patch.mockResolvedValue({ data: apiResponse });
@@ -63,10 +63,10 @@ describe('account service', () => {
const result = await patchAccount('testuser', commit);
expect(mockHttpClient.patch).toHaveBeenCalledWith(
'http://lms.test/api/user/v1/accounts/testuser',
expect.objectContaining({ social_links: [{ platform: 'twitter', social_link: 'http://t' }] }),
expect.objectContaining({ social_links: [{ platform: 'xTwitter', social_link: 'http://t' }] }),
expect.any(Object),
);
expect(result.social_link_twitter).toEqual('http://t');
expect(result.social_link_x).toEqual('http://t');
});
});

View File

@@ -81,8 +81,8 @@ export class ConfirmationModal extends Component {
isOverflowVisible
footerNode={(
<ActionRow>
<Button variant="link" onClick={onCancel}>Cancel</Button>
<Button variant="danger" onClick={onSubmit}>Yes, Delete</Button>
<Button variant="link" onClick={onCancel}>{intl.formatMessage(messages['account.settings.delete.account.modal.confirm.cancel'])}</Button>
<Button variant="danger" onClick={onSubmit}>{intl.formatMessage(messages['account.settings.delete.account.modal.confirm.delete'])}</Button>
</ActionRow>
)}
>

View File

@@ -38,6 +38,7 @@ exports[`ConfirmationModal should match empty password confirmation modal snapsh
data-testid="modal-backdrop"
onClick={[MockFunction]}
onKeyDown={[MockFunction]}
role="presentation"
/>
<div
aria-label="Are you sure?"
@@ -247,15 +248,17 @@ exports[`ConfirmationModal should match open confirmation modal snapshot 1`] = `
"width": "1px",
}
}
tabIndex={-1}
tabIndex={0}
/>,
<div
className="pgn__modal-layer"
data-focus-lock-disabled="disabled"
data-focus-lock-disabled={false}
onBlur={[Function]}
onFocus={[Function]}
onMouseDown={[Function]}
onScrollCapture={[Function]}
onTouchMoveCapture={[Function]}
onTouchStart={[Function]}
onWheelCapture={[Function]}
>
<div
@@ -266,6 +269,7 @@ exports[`ConfirmationModal should match open confirmation modal snapshot 1`] = `
data-testid="modal-backdrop"
onClick={[MockFunction]}
onKeyDown={[MockFunction]}
role="presentation"
/>
<div
aria-label="Are you sure?"
@@ -393,7 +397,7 @@ exports[`ConfirmationModal should match open confirmation modal snapshot 1`] = `
"width": "1px",
}
}
tabIndex={-1}
tabIndex={0}
/>,
]
`;

View File

@@ -23,15 +23,17 @@ exports[`SuccessModal should match open success modal snapshot 1`] = `
"width": "1px",
}
}
tabIndex={-1}
tabIndex={0}
/>,
<div
className="pgn__modal-layer"
data-focus-lock-disabled="disabled"
data-focus-lock-disabled={false}
onBlur={[Function]}
onFocus={[Function]}
onMouseDown={[Function]}
onScrollCapture={[Function]}
onTouchMoveCapture={[Function]}
onTouchStart={[Function]}
onWheelCapture={[Function]}
>
<div
@@ -42,6 +44,7 @@ exports[`SuccessModal should match open success modal snapshot 1`] = `
data-testid="modal-backdrop"
onClick={[MockFunction]}
onKeyDown={[MockFunction]}
role="presentation"
/>
<div
className="mw-sm p-5 bg-white mx-auto my-3"
@@ -84,7 +87,7 @@ exports[`SuccessModal should match open success modal snapshot 1`] = `
"width": "1px",
}
}
tabIndex={-1}
tabIndex={0}
/>,
]
`;

View File

@@ -11,6 +11,7 @@ import { IntlProvider, injectIntl } from '@edx/frontend-platform/i18n';
import AccountSettingsPage from '../AccountSettingsPage';
import mockData from './mockData';
import messages from '../AccountSettingsPage.messages';
const mockDispatch = jest.fn();
jest.mock('@edx/frontend-platform/analytics', () => ({
@@ -189,7 +190,7 @@ describe('AccountSettingsPage', () => {
expect(screen.getByText('https://linkedin.com/in/testuser')).toBeInTheDocument();
expect(screen.getByText('Add Facebook profile')).toBeInTheDocument();
expect(screen.getByText('Add Twitter profile')).toBeInTheDocument();
expect(screen.getByText(messages['account.settings.field.social.platform.name.xTwitter.empty'].defaultMessage)).toBeInTheDocument();
});
it('renders Site Preferences section with correct field values', () => {

View File

@@ -71,5 +71,5 @@ export function useFeedbackWrapper() {
export function useIsOnMobile() {
const windowSize = useWindowSize();
return windowSize.width <= breakpoints.small.minWidth;
return windowSize.width <= breakpoints.small.maxWidth;
}

View File

@@ -65,8 +65,8 @@ initialize({
config: () => {
mergeConfig({
SUPPORT_URL: process.env.SUPPORT_URL,
SHOW_PUSH_CHANNEL: process.env.SHOW_PUSH_CHANNEL === 'true',
SHOW_EMAIL_CHANNEL: process.env.SHOW_EMAIL_CHANNEL || 'false',
SHOW_PUSH_CHANNEL: process.env.SHOW_PUSH_CHANNEL || false,
SHOW_EMAIL_CHANNEL: process.env.SHOW_EMAIL_CHANNEL || false,
ENABLE_COPPA_COMPLIANCE: (process.env.ENABLE_COPPA_COMPLIANCE || false),
ENABLE_ACCOUNT_DELETION: (process.env.ENABLE_ACCOUNT_DELETION !== 'false'),
COUNTRIES_WITH_DELETE_ACCOUNT_DISABLED: JSON.parse(process.env.COUNTRIES_WITH_DELETE_ACCOUNT_DISABLED || '[]'),

View File

@@ -18,9 +18,7 @@ import {
selectUpdatePreferencesStatus,
} from './data/selectors';
import { notificationChannels, shouldHideAppPreferences } from './data/utils';
import {
EMAIL, EMAIL_CADENCE, EMAIL_CADENCE_PREFERENCES, MIXED,
} from './data/constants';
import { EMAIL, EMAIL_CADENCE } from './data/constants';
const NotificationPreferenceColumn = ({ appId, channel, appPreference }) => {
const dispatch = useDispatch();
@@ -39,13 +37,11 @@ const NotificationPreferenceColumn = ({ appId, channel, appPreference }) => {
return checked;
}, []);
const getEmailCadence = useCallback((notificationChannel, checked, innerText, emailCadence) => {
const getEmailCadence = useCallback((notificationChannel, innerText, emailCadence) => {
if (notificationChannel === EMAIL_CADENCE) {
return innerText;
}
if (notificationChannel === EMAIL && checked) {
return EMAIL_CADENCE_PREFERENCES.DAILY;
}
return emailCadence;
}, []);
@@ -56,7 +52,6 @@ const NotificationPreferenceColumn = ({ appId, channel, appPreference }) => {
const value = getValue(notificationChannel, innerText, checked);
const emailCadence = getEmailCadence(
notificationChannel,
checked,
innerText,
appNotificationPreference.emailCadence,
);
@@ -66,12 +61,11 @@ const NotificationPreferenceColumn = ({ appId, channel, appPreference }) => {
notificationType,
notificationChannel,
value,
emailCadence !== MIXED ? emailCadence : undefined,
emailCadence,
));
}, [appPreferences, getValue, getEmailCadence, dispatch, appId]);
const renderPreference = (preference) => (
(preference?.coreNotificationTypes?.length > 0 || preference.id !== 'core') && (
<div
key={`${preference.id}-${channel}`}
id={`${preference.id}-${channel}`}
@@ -100,7 +94,6 @@ const NotificationPreferenceColumn = ({ appId, channel, appPreference }) => {
/>
)}
</div>
)
);
return (

View File

@@ -3,7 +3,7 @@ import { Provider } from 'react-redux';
import configureStore from 'redux-mock-store';
import { BrowserRouter as Router } from 'react-router-dom';
import { setConfig } from '@edx/frontend-platform';
import { setConfig, mergeConfig } from '@edx/frontend-platform';
import * as auth from '@edx/frontend-platform/auth';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import { fireEvent, render, screen } from '@testing-library/react';
@@ -110,9 +110,10 @@ describe('Notification Preferences', () => {
let store;
beforeEach(() => {
setConfig({
mergeConfig({
SHOW_EMAIL_CHANNEL: '',
});
SHOW_PUSH_CHANNEL: '',
}, 'App loadConfig override handler');
store = setupStore({
...defaultPreferences,
@@ -173,6 +174,59 @@ describe('Notification Preferences', () => {
expect(screen.getAllByTestId('email-cadence-button')[0]).toBeDisabled();
expect(screen.getByTestId('toggle-newGrade-web')).not.toBeDisabled();
});
it('does not render push channel when SHOW_PUSH_CHANNEL is false', async () => {
setConfig({
SHOW_PUSH_CHANNEL: '',
});
store = setupStore({
...defaultPreferences,
status: SUCCESS_STATUS,
selectedCourse: '',
});
await render(notificationPreferences(store));
expect(screen.queryByTestId('toggle-core-push')).not.toBeInTheDocument();
});
it('renders push channel when SHOW_PUSH_CHANNEL is true', async () => {
setConfig({
SHOW_PUSH_CHANNEL: 'true',
});
store = setupStore({
...defaultPreferences,
status: SUCCESS_STATUS,
selectedCourse: '',
});
await render(notificationPreferences(store));
expect(screen.queryByTestId('toggle-core-push')).toBeInTheDocument();
});
it('does not render email channel when SHOW_EMAIL_CHANNEL is false', async () => {
setConfig({
SHOW_EMAIL_CHANNEL: '',
});
store = setupStore({
...defaultPreferences,
status: SUCCESS_STATUS,
selectedCourse: '',
});
await render(notificationPreferences(store));
expect(screen.queryByTestId('toggle-core-email')).not.toBeInTheDocument();
});
it('renders email channel when SHOW_EMAIL_CHANNEL is true', async () => {
setConfig({
SHOW_EMAIL_CHANNEL: 'true',
});
store = setupStore({
...defaultPreferences,
status: SUCCESS_STATUS,
selectedCourse: '',
});
await render(notificationPreferences(store));
expect(screen.queryByTestId('toggle-core-email')).toBeInTheDocument();
});
});
describe('Notification Preferences API v2 Logic', () => {
@@ -195,7 +249,7 @@ describe('Notification Preferences API v2 Logic', () => {
describe('getNotificationPreferences', () => {
it('should call the v2 configurations URL', async () => {
const expectedUrl = `${LMS_BASE_URL}/api/notifications/v2/configurations/`;
const expectedUrl = `${LMS_BASE_URL}/api/notifications/v3/configurations/`;
await getNotificationPreferences();
@@ -206,7 +260,7 @@ describe('Notification Preferences API v2 Logic', () => {
describe('postPreferenceToggle', () => {
it('should call the v2 configurations URL with PUT method', async () => {
const expectedUrl = `${LMS_BASE_URL}/api/notifications/v2/configurations/`;
const expectedUrl = `${LMS_BASE_URL}/api/notifications/v3/configurations/`;
const testArgs = ['app_name', 'notification_type', 'web', true, 'daily'];
await postPreferenceToggle(...testArgs);

View File

@@ -23,7 +23,6 @@ const NotificationTypes = ({ appId }) => {
return (
<div className="d-flex flex-column mr-auto px-0">
{preferences.map(preference => (
(preference?.coreNotificationTypes?.length > 0 || preference.id !== 'core') && (
<>
<div
key={preference.id}
@@ -56,8 +55,6 @@ const NotificationTypes = ({ appId }) => {
</div>
)}
</>
)
))}
</div>
);

View File

@@ -5,7 +5,6 @@ export const EMAIL_CADENCE_PREFERENCES = {
};
export const EMAIL_CADENCE = 'email_cadence';
export const EMAIL = 'email';
export const MIXED = 'Mixed';
export const RequestStatus = /** @type {const} */ ({
IN_PROGRESS: 'in-progress',
SUCCESSFUL: 'successful',

View File

@@ -3,7 +3,7 @@ import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
import snakeCase from 'lodash.snakecase';
export const getNotificationPreferences = async () => {
const url = `${getConfig().LMS_BASE_URL}/api/notifications/v2/configurations/`;
const url = `${getConfig().LMS_BASE_URL}/api/notifications/v3/configurations/`;
const { data } = await getAuthenticatedHttpClient().get(url);
return data;
};
@@ -16,13 +16,13 @@ export const postPreferenceToggle = async (
emailCadence,
) => {
const patchData = snakeCaseObject({
notificationApp,
notificationApp: snakeCase(notificationApp),
notificationType: snakeCase(notificationType),
notificationChannel,
value,
emailCadence,
});
const url = `${getConfig().LMS_BASE_URL}/api/notifications/v2/configurations/`;
const url = `${getConfig().LMS_BASE_URL}/api/notifications/v3/configurations/`;
const { data } = await getAuthenticatedHttpClient().put(url, patchData);
return data;
};

View File

@@ -38,7 +38,7 @@ describe('Notification Preferences Service', () => {
const result = await getNotificationPreferences();
expect(mockHttpClient.get).toHaveBeenCalledWith(
'http://test.lms/api/notifications/v2/configurations/',
'http://test.lms/api/notifications/v3/configurations/',
);
expect(result).toEqual(mockData);
});
@@ -50,7 +50,7 @@ describe('Notification Preferences Service', () => {
mockHttpClient.put.mockResolvedValue({ data: mockData });
const result = await postPreferenceToggle(
'appName',
'app_name',
'someType',
'email',
true,
@@ -58,9 +58,9 @@ describe('Notification Preferences Service', () => {
);
expect(mockHttpClient.put).toHaveBeenCalledWith(
'http://test.lms/api/notifications/v2/configurations/',
'http://test.lms/api/notifications/v3/configurations/',
expect.objectContaining({
notification_app: 'appName',
notification_app: 'app_name',
notification_type: 'some_type',
notification_channel: 'email',
value: true,

View File

@@ -35,7 +35,7 @@ const normalizePreferences = (responseData) => {
const apps = appKeys.map((appId) => ({
id: appId,
enabled: preferences[appId].enabled,
}));
})).sort((a, b) => a.id.localeCompare(b.id));
const nonEditable = {};
const preferenceList = appKeys.map(appId => {
@@ -50,7 +50,6 @@ const normalizePreferences = (responseData) => {
info: preferences[appId].notificationTypes[preferenceId].info || '',
emailCadence: preferences[appId].notificationTypes[preferenceId].emailCadence
|| EMAIL_CADENCE_PREFERENCES.DAILY,
coreNotificationTypes: preferences[appId].coreNotificationTypes || [],
}
));
nonEditable[appId] = preferences[appId].nonEditable;
@@ -122,7 +121,7 @@ export const updatePreferenceToggle = (
const emailCadenceData = await togglePreference(
EMAIL_CADENCE,
value,
EMAIL_CADENCE_PREFERENCES.DAILY,
emailCadence,
);
handleSuccessResponse(emailCadenceData);

View File

@@ -1,19 +1,14 @@
import { getConfig } from '@edx/frontend-platform';
import { parseEnvBoolean } from '../../utils';
export const notificationChannels = () => ({
WEB: 'web',
...(getConfig().SHOW_PUSH_CHANNEL && { PUSH: 'push' }),
...(getConfig().SHOW_EMAIL_CHANNEL === 'true' && { EMAIL: 'email' }),
...(parseEnvBoolean(getConfig().SHOW_PUSH_CHANNEL) && { PUSH: 'push' }),
...(parseEnvBoolean(getConfig().SHOW_EMAIL_CHANNEL) && { EMAIL: 'email' }),
});
export const shouldHideAppPreferences = (preferences, appId) => {
const appPreferences = preferences.filter(pref => pref.appId === appId);
if (appPreferences.length !== 1) {
return false;
}
const firstPreference = appPreferences[0];
return firstPreference?.id === 'core' && (!firstPreference.coreNotificationTypes?.length);
return appPreferences.length === 0;
};

View File

@@ -22,7 +22,7 @@ const messages = defineMessages({
id: 'notification.preference.title',
defaultMessage: `{
text, select,
core {Activity notifications}
groupedNotification {Activity notifications}
newDiscussionPost {New discussion posts}
newQuestionPost {New question posts}
contentReported {Reported content}

View File

@@ -1,6 +1,8 @@
import 'core-js/stable';
import 'regenerator-runtime/runtime';
import '@testing-library/jest-dom';
import { initialize, mergeConfig } from '@edx/frontend-platform';
import { MockAuthService } from '@edx/frontend-platform/auth';
import MockedPluginSlot from './tests/MockedPluginSlot';
@@ -9,3 +11,39 @@ jest.mock('@openedx/frontend-plugin-framework', () => ({
Plugin: () => 'Plugin',
PluginSlot: MockedPluginSlot,
}));
mergeConfig({
SUPPORT_URL: process.env.SUPPORT_URL || 'https://support.example.com',
SHOW_PUSH_CHANNEL: process.env.SHOW_PUSH_CHANNEL || false,
SHOW_EMAIL_CHANNEL: process.env.SHOW_EMAIL_CHANNEL || false,
ENABLE_COPPA_COMPLIANCE: (process.env.ENABLE_COPPA_COMPLIANCE || false),
ENABLE_ACCOUNT_DELETION: (process.env.ENABLE_ACCOUNT_DELETION !== 'false'),
COUNTRIES_WITH_DELETE_ACCOUNT_DISABLED: JSON.parse(process.env.COUNTRIES_WITH_DELETE_ACCOUNT_DISABLED || '[]'),
ENABLE_DOB_UPDATE: (process.env.ENABLE_DOB_UPDATE || false),
MARKETING_EMAILS_OPT_IN: (process.env.MARKETING_EMAILS_OPT_IN || false),
PASSWORD_RESET_SUPPORT_LINK: process.env.PASSWORD_RESET_SUPPORT_LINK || 'https://support.example.com/password-reset',
LEARNER_FEEDBACK_URL: process.env.LEARNER_FEEDBACK_URL || 'https://support.example.com/feedback',
}, 'App loadConfig override handler');
initialize({
handlers: {
config: () => {
mergeConfig({
authenticatedUser: {
userId: 'abc123',
username: 'Mock User',
roles: [],
administrator: false,
},
});
},
},
messages: [],
authService: MockAuthService,
});
global.ResizeObserver = jest.fn().mockImplementation(() => ({
observe: jest.fn(),
unobserve: jest.fn(),
disconnect: jest.fn(),
}));

View File

@@ -38,3 +38,13 @@ export function getMostRecentApprovedOrPendingVerifiedName(verifiedNames) {
return applicableName;
}
/**
* Parse an environment variable string value to a boolean.
* @param {string} value the environment variable string value
* @returns {boolean} the parsed boolean value
*/
export const parseEnvBoolean = (value) => {
if (!value) { return false; }
return String(value).toLowerCase() === 'true';
};