Compare commits

...

8 Commits

Author SHA1 Message Date
Adolfo R. Brandes
92208a0a7c 1.0.0-alpha.5 2026-02-09 20:21:28 -03:00
Adolfo R. Brandes
45535ee055 build: Add minimum node version warning
An issue was found when using Node version 24.9 with the latest
package-lock (see #123), one not reproducible with versions 24.12 and
above.  Add a warning that will be shown when running `npm ci`.
2026-01-15 09:49:57 -03:00
Adolfo R. Brandes
e53c4997bb fix: missing styles
The shell's SCSS must be explicitly loaded by site.config.dev.tsx.
2026-01-15 09:49:57 -03:00
Adolfo R. Brandes
f311539e12 fix: CI
App repos are no longer built.
2026-01-15 09:49:57 -03:00
Adolfo R. Brandes
c1070930bf fix: bump frontend-base
Bump frontend-base so we stop failing linting due to the nullish
coalescing rule we can't follow.
2026-01-15 09:49:57 -03:00
Adolfo R. Brandes
dad2887eed fix: linting errors 2026-01-15 09:49:57 -03:00
Adolfo R. Brandes
4f79099eca build: Upgrade to Node 24 2026-01-15 09:49:57 -03:00
Adolfo R. Brandes
49f42a8857 feat!: add design tokens support
BREAKING CHANGE: Pre-design-tokens theming is no longer supported.
2026-01-15 09:49:57 -03:00
19 changed files with 2731 additions and 2183 deletions

View File

@@ -35,9 +35,6 @@ jobs:
- name: Test
run: npm run test
- name: Build
run: npm run build
- name: Run Code Coverage
uses: codecov/codecov-action@v5
with:

2
.nvmrc
View File

@@ -1 +1 @@
20
24

4636
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,10 @@
{
"name": "@openedx/frontend-app-authn",
"version": "1.0.0-alpha.4",
"version": "1.0.0-alpha.5",
"description": "Frontend authentication",
"engines": {
"node": "^24.12"
},
"repository": {
"type": "git",
"url": "git+https://github.com/openedx/frontend-app-authn.git"
@@ -64,11 +67,12 @@
"babel-plugin-formatjs": "10.5.38",
"eslint-plugin-import": "2.31.0",
"jest": "^29.7.0",
"react-test-renderer": "^18.3.1"
"react-test-renderer": "^18.3.1",
"ts-jest": "^29.4.0"
},
"peerDependencies": {
"@openedx/frontend-base": "^1.0.0-alpha.6",
"@openedx/paragon": "^22",
"@openedx/frontend-base": "^1.0.0-alpha.8",
"@openedx/paragon": "^23",
"react": "^18",
"react-dom": "^18",
"react-redux": "^8",

View File

@@ -1,6 +1,2 @@
@use "@edx/brand/paragon/fonts";
@use "@edx/brand/paragon/variables";
@use "@openedx/paragon/scss/core/core";
@use "@edx/brand/paragon/overrides";
@use "@openedx/frontend-base/shell/app.scss";
@use "sass/style";

View File

@@ -16,8 +16,8 @@ export async function getThirdPartyAuthContext(urlParams) {
throw (e);
});
return {
fieldDescriptions: data.registrationFields || {},
optionalFields: data.optionalFields || {},
thirdPartyAuthContext: data.contextData || {},
fieldDescriptions: data.registrationFields ?? {},
optionalFields: data.optionalFields ?? {},
thirdPartyAuthContext: data.contextData ?? {},
};
}

View File

@@ -10,7 +10,7 @@ import { breakpoints } from '@openedx/paragon';
const useMobileResponsive = (breakpoint) => {
const [isMobileWindow, setIsMobileWindow] = useState();
const checkForMobile = () => {
setIsMobileWindow(window.matchMedia(`(max-width: ${breakpoint || breakpoints.small.maxWidth}px)`).matches);
setIsMobileWindow(window.matchMedia(`(max-width: ${breakpoint ?? breakpoints.small.maxWidth}px)`).matches);
};
useEffect(() => {
checkForMobile();

View File

@@ -20,7 +20,7 @@ export function* handleForgotPassword(action) {
yield put(forgotPasswordSuccess(action.payload.email));
} catch (e) {
if (e.response && e.response.status === 403) {
if (e.response?.status === 403) {
yield put(forgotPasswordForbidden());
logInfo(e);
} else {

View File

@@ -63,8 +63,8 @@ export const validateEmailAddress = (value, username, domainName) => {
const tldSuggestion = !DEFAULT_TOP_LEVEL_DOMAINS.includes(topLevelDomain);
const serviceSuggestion = getLevenshteinSuggestion(serviceLevelDomain, DEFAULT_SERVICE_PROVIDER_DOMAINS, 2);
if (DEFAULT_SERVICE_PROVIDER_DOMAINS.includes(serviceSuggestion || serviceLevelDomain)) {
suggestion = `${username}@${serviceSuggestion || serviceLevelDomain}.com`;
if (DEFAULT_SERVICE_PROVIDER_DOMAINS.includes(serviceSuggestion ?? serviceLevelDomain)) {
suggestion = `${username}@${serviceSuggestion ?? serviceLevelDomain}.com`;
}
if (!hasMultipleSubdomains && tldSuggestion) {

View File

@@ -74,7 +74,7 @@ const reducer = (state = defaultState, action = {}) => {
registrationError: { ...action.payload },
submitState: DEFAULT_STATE,
validations: null,
usernameSuggestions: usernameSuggestions || state.usernameSuggestions,
usernameSuggestions: usernameSuggestions ?? state.usernameSuggestions,
};
}
case REGISTRATION_CLEAR_BACKEND_ERROR: {
@@ -90,7 +90,7 @@ const reducer = (state = defaultState, action = {}) => {
return {
...state,
validations: validationWithoutUsernameSuggestions,
usernameSuggestions: usernameSuggestions || state.usernameSuggestions,
usernameSuggestions: usernameSuggestions ?? state.usernameSuggestions,
};
}
case REGISTER_FORM_VALIDATIONS.FAILURE:

View File

@@ -44,7 +44,7 @@ export function* fetchRealtimeValidations(action) {
yield put(fetchRealtimeValidationsSuccess(camelCaseObject(fieldValidations)));
} catch (e) {
if (e.response && e.response.status === 403) {
if (e.response?.status === 403) {
yield put(fetchRealtimeValidationsFailure());
logInfo(e);
} else {

View File

@@ -22,7 +22,7 @@ const getBackendValidations = createSelector(
const validationDecisions = {};
fields.forEach(field => {
validationDecisions[field] = registrationError[field][0].userMessage || '';
validationDecisions[field] = registrationError[field][0].userMessage ?? '';
});
return validationDecisions;
}

View File

@@ -18,8 +18,8 @@ export async function registerRequest(registrationInformation) {
});
return {
redirectUrl: data.redirect_url || `${getSiteConfig().lmsBaseUrl}/dashboard`,
success: data.success || false,
redirectUrl: data.redirect_url ?? `${getSiteConfig().lmsBaseUrl}/dashboard`,
success: data.success ?? false,
authenticatedUser: data.authenticated_user,
};
}

View File

@@ -43,9 +43,7 @@ export const isFormValid = (
Object.keys(payload).forEach(key => {
switch (key) {
case 'name':
if (!fieldErrors.name) {
fieldErrors.name = validateName(payload.name, formatMessage);
}
fieldErrors.name ||= validateName(payload.name, formatMessage);
if (fieldErrors.name) {
isValid = false;
}
@@ -71,17 +69,13 @@ export const isFormValid = (
break;
}
case 'username':
if (!fieldErrors.username) {
fieldErrors.username = validateUsername(payload.username, formatMessage);
}
fieldErrors.username ||= validateUsername(payload.username, formatMessage);
if (fieldErrors.username) {
isValid = false;
}
break;
case 'password':
if (!fieldErrors.password) {
fieldErrors.password = validatePasswordField(payload.password, formatMessage);
}
fieldErrors.password ||= validatePasswordField(payload.password, formatMessage);
if (fieldErrors.password) {
isValid = false;
}

View File

@@ -26,7 +26,7 @@ export function* handleValidateToken(action) {
yield put(passwordResetFailure(PASSWORD_RESET.INVALID_TOKEN));
}
} catch (err) {
if (err.response && err.response.status === 429) {
if (err.response?.status === 429) {
yield put(passwordResetFailure(PASSWORD_RESET.FORBIDDEN_REQUEST));
logInfo(err);
} else {
@@ -51,7 +51,7 @@ export function* handleResetPassword(action) {
yield put(resetPasswordFailure(PASSWORD_VALIDATION_ERROR, resetErrors));
}
} catch (err) {
if (err.response && err.response.status === 429) {
if (err.response?.status === 429) {
yield put(resetPasswordFailure(PASSWORD_RESET.FORBIDDEN_REQUEST));
logInfo(err);
} else {

View File

@@ -1,21 +1,19 @@
@use "@openedx/paragon/scss/core/core" as paragon;
.layout {
display: flex;
@include paragon.media-breakpoint-down('lg') {
@media (--pgn-size-breakpoint-max-width-lg) {
flex-direction: column;
justify-content: center;
align-items: center;
}
@include paragon.media-breakpoint-up('xl') {
@media (--pgn-size-breakpoint-min-width-xl) {
justify-content: space-between;
}
}
.content {
@include paragon.media-breakpoint-up('xl') {
@media (--pgn-size-breakpoint-min-width-xl) {
display: flex;
justify-content: center;
width: 50vw;
@@ -48,7 +46,7 @@
font-weight: 700;
line-height: 1;
@include paragon.media-breakpoint-down('xl') {
@media (--pgn-size-breakpoint-max-width-xl) {
font-size: 3.75rem;
}
@@ -61,7 +59,7 @@
margin-bottom: 0.5rem;
font-weight: 700;
@include paragon.media-breakpoint-down('xl') {
@media (-pgn-size-breakpoint-max-width-xl) {
font-size: 1.375rem;
line-height: 1.75rem;
}
@@ -73,7 +71,7 @@
}
.large-screen-left-container {
@include paragon.media-breakpoint-down('xl') {
@media (-pgn-size-breakpoint-max-width-xl) {
flex: 0 0 25%;
max-width: 25%;
}
@@ -86,42 +84,46 @@
.small-screen-top-stripe {
height: 0.25rem;
background-image: linear-gradient(102.02deg,
paragon.$brand-700,
paragon.$brand-700 20%,
paragon.$brand 20%,
);
background-image: linear-gradient(
102.02deg,
var(--pgn-color-brand-700),
var(--pgn-color-brand-700) 20%,
var(--pgn-color-brand-base) 20%,
);
background-repeat: no-repeat;
}
@include paragon.media-breakpoint-only('md') {
@media (--pgn-size-breakpoint-min-width-md) and (--pgn-size-breakpoint-max-width-md) {
.medium-screen-top-stripe {
display: flex;
height: 0.5rem;
background-image: linear-gradient(102.02deg,
paragon.$brand-700,
paragon.$brand-700 10%,
paragon.$brand 10%,
paragon.$brand 90%,
paragon.$primary-700 90%,
paragon.$primary-700 100%,
);
background-image: linear-gradient(
102.02deg,
var(--pgn-color-brand-700),
var(--pgn-color-brand-700) 10%,
var(--pgn-color-brand-base) 10%,
var(--pgn-color-brand-base) 90%,
var(--pgn-color-primary-700) 90%,
var(--pgn-color-primary-700) 100%,
);
background-repeat: no-repeat;
}
}
@include paragon.media-breakpoint-only('lg') {
@media (--pgn-size-breakpoint-min-width-lg) and (--pgn-size-breakpoint-max-width-lg){
.medium-screen-top-stripe {
display: flex;
height: 0.5rem;
background-image: linear-gradient(102.02deg,
paragon.$brand-700 10%,
paragon.$brand 10%,
paragon.$brand 65%,
paragon.$primary-700 65%,
paragon.$primary-700 75%,
paragon.$accent-a 75%,
paragon.$accent-a 75%);
background-image: linear-gradient(
102.02deg,
var(--pgn-color-brand-700) 10%,
var(--pgn-color-brand-base) 10%,
var(--pgn-color-brand-base) 65%,
var(--pgn-color-primary-700) 65%,
var(--pgn-color-primary-700) 75%,
var(--pgn-color-accent-a) 75%,
var(--pgn-color-accent-a) 75%
);
background-repeat: no-repeat;
}
}
@@ -130,44 +132,45 @@
display: none;
}
@include paragon.media-breakpoint-up('xl') {
.extra-large-screen-top-stripe {
display: flex;
height: 0.5rem;
background-image: linear-gradient(102.02deg,
paragon.$brand-700 10%,
paragon.$brand 10%,
paragon.$brand 45%,
paragon.$primary-700 45%,
paragon.$primary-700 55%,
paragon.$accent-a 55%,
paragon.$accent-a 75%,
paragon.$info-200 75%,
);
background-repeat: no-repeat;
}
@media (--pgn-size-breakpoint-min-width-xl) {
.extra-large-screen-top-stripe {
display: flex;
height: 0.5rem;
background-image: linear-gradient(
102.02deg,
var(--pgn-color-brand-700) 10%,
var(--pgn-color-brand-base) 10%,
var(--pgn-color-brand-base) 45%,
var(--pgn-color-primary-700) 45%,
var(--pgn-color-primary-700) 55%,
var(--pgn-color-accent-a) 55%,
var(--pgn-color-accent-a) 75%,
var(--pgn-color-info-200) 75%,
);
background-repeat: no-repeat;
}
}
.large-screen-svg-light,
.large-screen-svg-primary {
fill: paragon.$light-200;
fill: var(--pgn-color-light-200);
overflow: hidden;
position: absolute;
}
.large-screen-svg-primary {
fill: paragon.$primary-400;
fill: var(--pgn-color-primary-400);
}
.medium-screen-svg-light,
.medium-screen-svg-primary {
fill: paragon.$light-200;
fill: var(--pgn-color-light-200);
overflow: inherit;
position: absolute;
}
.medium-screen-svg-primary {
fill: paragon.$primary-400;
fill: var(--pgn-color-primary-400);
}
[dir=rtl] {
@@ -183,20 +186,20 @@
.small-yellow-line {
width: 80px;
height: 0;
border: 2px solid paragon.$accent-b;
border: 2px solid var(--pgn-color-accent-b);
transform: rotate(102.02deg);
}
.medium-yellow-line {
width: 120px;
height: 0;
border: 3px solid paragon.$accent-b;
border: 3px solid var(--pgn-color-accent-b);
transform: rotate(102.02deg);
}
.large-yellow-line {
width: 240px;
height: 0;
border: 3px solid paragon.$accent-b;
border: 3px solid var(--pgn-color-accent-b);
transform: rotate(102.02deg);
}

View File

@@ -1,5 +1,3 @@
@use "@openedx/paragon/scss/core/core" as paragon;
.pp-page__button-width {
min-width: 6rem;
}
@@ -15,7 +13,7 @@
margin-bottom: 0.5rem;
font-weight: 700;
@include paragon.media-breakpoint-down('md') {
@media (--pgn-size-breakpoint-max-width-md) {
line-height: 1.5rem;
font-size: 1.125rem;
}

View File

@@ -1,4 +1,3 @@
@use "@openedx/paragon/scss/core/core" as paragon;
@use "sass:map";
.register-button {
@@ -25,22 +24,22 @@
line-height: 1.25rem;
}
.alert-link {
color: paragon.$primary !important;
.alert-link {
color: var(--pgn-color-primary-base) !important;
&:hover {
text-decoration: underline;
color: paragon.$info-700 !important;
color: var(--pgn-color-info-700) !important;
}
}
}
.email-suggestion-alert-warning {
color: paragon.$info-500 !important;
color: var(--pgn-color-info-500) !important;
&:hover {
text-decoration: underline;
color: paragon.$info-700 !important;
color: var(--pgn-color-info-700) !important;
}
}
@@ -60,7 +59,7 @@
line-height: 24px;
font-size: 12px;
font-weight: normal;
color: paragon.$primary-700;
color: var(--pgn-color-primary-700);
}
.username-suggestion--label {
@@ -104,7 +103,7 @@
}
}
@media (max-width: map.get(paragon.$grid-breakpoints, "sm")) {
@media (--pgn-size-breakpoint-max-width-sm) {
.username-scroll-suggested--form-field {
width: 15rem;
}

View File

@@ -1,5 +1,3 @@
@use "@openedx/paragon/scss/core/core" as paragon;
// Load component based styles
@use "_base_component.scss";
@use "_registration.scss";
@@ -42,7 +40,7 @@ $elevation-level-2-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.15);
}
.main-content {
@extend .pt-4;
padding-top: var(--pgn-spacing-spacer-4) !important;
min-width: 464px !important;
}
@@ -82,15 +80,15 @@ $elevation-level-2-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.15);
.alert-link {
font-weight: normal;
text-decoration: underline;
color: paragon.$info-300 !important;
color: var(--pgn-color-info-300) !important;
&:hover {
color: paragon.$info-500 !important;
color: var(--pgn-color-info-500) !important;
}
}
.form-control {
background-color: $white !important;
background-color: var(--pgn-color-white) !important;
font-size: 0.875rem;
line-height: 1.5;
height: 2.75rem;
@@ -105,11 +103,11 @@ $elevation-level-2-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.15);
margin-bottom: 1rem;
font-size: 14px;
background-color: $white;
border: 1px solid paragon.$primary;
background-color: var(--pgn-color-white);
border: 1px solid var(--pgn-color-primary-base);
width: 224px;
height: 36px;
color: paragon.$primary;
color: var(--pgn-color-primary-base);
.btn-tpa__image-icon {
background-color: transparent;
@@ -134,8 +132,8 @@ $elevation-level-2-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.15);
}
.btn-tpa__font-container {
background-color: paragon.$primary;
color: $white;
background-color: var(--pgn-color-primary-base);
color: var(--pgn-color-white);
font-size: 11px;
margin-left: -6px;
@@ -145,7 +143,7 @@ $elevation-level-2-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.15);
}
.btn-oa2-facebook {
color: $white;
color: var(--pgn-color-white);
border-color: $facebook-blue;
background-color: $facebook-blue;
@@ -153,12 +151,12 @@ $elevation-level-2-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.15);
&:focus {
background-color: $facebook-focus-blue;
border: 1px solid $facebook-focus-blue;
color: $white;
color: var(--pgn-color-white);
}
}
.btn-oa2-google-oauth2 {
color: $white;
color: var(--pgn-color-white);
border-color: $google-blue;
background-color: $google-blue;
@@ -173,12 +171,12 @@ $elevation-level-2-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.15);
&:focus {
background-color: $google-focus-blue;
border: 1px solid $google-focus-blue;
color: $white;
color: var(--pgn-color-white);
}
}
.btn-oa2-apple-id {
color: $white;
color: var(--pgn-color-white);
border-color: $apple-black;
background-color: $apple-black;
font-size: 16px;
@@ -192,12 +190,12 @@ $elevation-level-2-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.15);
&:focus {
background-color: $apple-focus-black;
border: 1px solid $apple-focus-black;
color: $white;
color: var(--pgn-color-white);
}
}
.btn-oa2-azuread-oauth2 {
color: $white;
color: var(--pgn-color-white);
border-color: $microsoft-black;
background-color: $microsoft-black;
@@ -205,7 +203,7 @@ $elevation-level-2-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.15);
&:focus {
background-color: $microsoft-focus-black;
border: 1px solid $microsoft-focus-black;
color: $white;
color: var(--pgn-color-white);
}
}
@@ -216,9 +214,8 @@ $elevation-level-2-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.15);
}
.institute-icon {
@extend .mr-1;
@extend .text-gray;
margin: var(--pgn-spacing-spacer-1) !important;
color: var(--pgn-color-gray-base) !important;
display: inline-block;
margin-bottom: 0.25rem;
height: 18px;
@@ -234,7 +231,7 @@ $elevation-level-2-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.15);
}
.invalid-feedback {
color: paragon.$red;
color: var(--pgn-color-red);
}
.full-vertical-height {
@@ -292,24 +289,24 @@ select.form-control {
#password-requirement-left {
opacity: 1;
@extend .x-small;
font-size: var(--pgn-typography-font-size-xs) !important;
filter: drop-shadow($elevation-level-2-shadow) drop-shadow($elevation-level-2-shadow) !important;
right: 0.2rem !important;
.tooltip-inner {
background: $white;
background: var(--pgn-color-white);
display: block;
color: paragon.$gray-500;
color: var(--pgn-color-gray-500);
}
.arrow::before {
border-left-color: $white;
border-left-color: var(--pgn-color-white);
}
}
#password-requirement-top {
@extend .x-small;
filter: drop-shadow($elevation-level-2-shadow) drop-shadow($elevation-level-2-shadow) !important;
font-size: var(--pgn-typography-font-size-xs) !important;
filter: drop-shadow(var(--pgn-elevation-box-shadow-level-2)) drop-shadow(var(--pgn-elevation-box-shadow-level-2)) !important;
opacity: 1;
width: 90%;
bottom: 10px !important;
@@ -318,31 +315,31 @@ select.form-control {
.tooltip-inner {
min-width: 464px !important;
background: $white;
background: var(--pgn-color-white);
display: block;
color: paragon.$gray-500;
color: var(--pgn-color-gray-500);
}
.arrow::before {
border-top-color: $white;
border-top-color: var(--pgn-color-white);
}
}
.yellow-border {
border: 2px solid paragon.$accent-b;
border: 2px solid var(--pgn-color-accent-b);
}
.institutions__heading {
color: paragon.$primary-700;
color: var(--pgn-color-primary-700);
}
.logistration-button {
color: paragon.$gray-700;
color: var(--pgn-color-gray-700);
}
.logistration-button:hover {
color: paragon.$gray-700;
.logistration-button:hover{
color: var(--pgn-color-gray-700);
text-decoration: none;
}
@@ -358,7 +355,7 @@ select.form-control {
}
.has-floating-label {
color: paragon.$gray-500;
color: var(--pgn-color-gray-500);
}
.pgn__form-control-floating-label .pgn__form-control-floating-label-content {
@@ -370,9 +367,9 @@ select.form-control {
font-size: 0.75rem;
}
.form-group__form-field .form-control:focus~.pgn__form-control-floating-label .pgn__form-control-floating-label-content {
font-size: 16px;
color: paragon.$primary-700;
.form-group__form-field .form-control:focus ~ .pgn__form-control-floating-label .pgn__form-control-floating-label-content {
font-size: 16px;
color: var(--pgn-color-primary-700);
}
.form-group__form-field .form-control:not([value='']):not(:focus)~.pgn__form-control-floating-label .pgn__form-control-floating-label-content {
@@ -450,14 +447,14 @@ select.form-control {
}
.table-striped tbody tr:nth-of-type(odd) {
background-color: paragon.$light-200;
background-color: var(--pgn-color-light-200);
}
.institutions--provider-link {
font-weight: normal;
font-size: 0.875rem;
line-height: 1.5rem;
color: paragon.$primary-700
color: var(--pgn-color-primary-700)
}
.pgn__form-control-decorator-trailing {