diff --git a/projects/packages/account-protection/src/assets/check.svg b/projects/packages/account-protection/src/assets/check.svg new file mode 100644 index 0000000000000..d88457419a8b1 --- /dev/null +++ b/projects/packages/account-protection/src/assets/check.svg @@ -0,0 +1,4 @@ + \ No newline at end of file diff --git a/projects/packages/account-protection/src/assets/cross.svg b/projects/packages/account-protection/src/assets/cross.svg new file mode 100644 index 0000000000000..3c33e4931cada --- /dev/null +++ b/projects/packages/account-protection/src/assets/cross.svg @@ -0,0 +1,4 @@ + \ No newline at end of file diff --git a/projects/packages/account-protection/src/assets/info.svg b/projects/packages/account-protection/src/assets/info.svg new file mode 100644 index 0000000000000..67e27b83571a6 --- /dev/null +++ b/projects/packages/account-protection/src/assets/info.svg @@ -0,0 +1,3 @@ + \ No newline at end of file diff --git a/projects/packages/account-protection/src/assets/jetpack-logo.svg b/projects/packages/account-protection/src/assets/jetpack-logo.svg index b91e3c5c216f5..dd6ad79d05a91 100644 --- a/projects/packages/account-protection/src/assets/jetpack-logo.svg +++ b/projects/packages/account-protection/src/assets/jetpack-logo.svg @@ -3,10 +3,12 @@ x="0px" y="0px" height="32px" + viewBox='0 0 118 32' aria-labelledby="jetpack-logo" role="img" >
', { + class: 'strength', + text: 'Validating...', + } ); + + const branding = $( '
', { class: 'powered-by', text: 'Powered by ' } ),
+ $( '', { src: jetpackData.logo, alt: 'Jetpack Logo' } )
+ );
+
+ strengthMeterWrapper.append( strengthText, branding );
+ UIComponents.validationCheckList.before( strengthMeterWrapper );
+
+ UIComponents.strengthMeter = {
+ wrapper: strengthMeterWrapper,
+ text: strengthText,
+ branding,
+ };
+ }
+
+ /**
+ * Generate and append the initial validation checklist state
+ */
+ function initializeValidationChecklist() {
+ Object.entries( jetpackData.validationInitialState ).forEach( ( [ key, value ] ) => {
+ const listItem = $( '
', {
+ text: value.message,
+ class: 'validation-message',
+ } );
+
+ let infoIconPopover = null;
+ if ( value.info ) {
+ infoIconPopover = createInfoIconPopover( value.info );
+ }
+
+ listItem.append( validationIcon, validationMessage, infoIconPopover );
+ UIComponents.validationCheckList.append( listItem );
+
+ UIComponents.validationChecklistItems[ key ] = {
+ icon: validationIcon,
+ text: validationMessage,
+ item: listItem,
+ };
+ } );
+ }
+
+ /**
+ * Bind events to the UI components
+ */
+ function bindEvents() {
+ const { passwordInput } = UIComponents.core;
+
+ passwordInput.on( 'input', validatePassword );
+ passwordInput.on( 'pwupdate', validatePassword );
+ }
+
+ /**
+ * Validate the current password input
+ */
+ function validatePassword() {
+ const { passwordInput, passwordStrengthResults } = UIComponents.core;
+
+ const password = passwordInput.val();
+
+ if ( currentAjaxRequest ) {
+ const oldRequest = currentAjaxRequest;
+ currentAjaxRequest = null;
+ oldRequest.abort();
+ }
+
+ if ( ! password?.trim() ) {
+ applyStyling( [], true );
+ return;
+ }
+
+ // Ensure core strength meter is hidden
+ passwordStrengthResults.hide();
+
+ renderLoadingState();
+
+ currentAjaxRequest = $.ajax( {
+ url: jetpackData.ajaxurl,
+ type: 'POST',
+ data: {
+ action: 'validate_password_ajax',
+ nonce: jetpackData.nonce,
+ password: password,
+ user_specific: jetpackData.userSpecific,
+ },
+ success: handleValidationResponse,
+ error: handleValidationError,
+ } );
+ }
+
+ /**
+ * Handles the password validation response.
+ * @param {object} response - The response object.
+ */
+ function handleValidationResponse( response ) {
+ currentAjaxRequest = null;
+
+ if ( response.success ) {
+ const failedValidationConditions = updateValidationChecklist( response.data.state );
+ applyStyling( failedValidationConditions );
+ } else {
+ restoreCoreStrengthMeter();
+ }
+ }
+
+ /**
+ * Handles validation errors.
+ * @param {object} jqXHR - The jqXHR object.
+ * @param {any} textStatus - The status of the request.
+ */
+ function handleValidationError( jqXHR, textStatus ) {
+ if ( textStatus !== 'abort' ) {
+ restoreCoreStrengthMeter();
+ }
+ }
+
+ /**
+ * Updates the validation checklist based on the response data.
+ *
+ * @param {object} state - The validation state.
+ * @return {object} - The failed conditions.
+ */
+ function updateValidationChecklist( state ) {
+ const failedConditions = [];
+
+ // Manually update core strength meter status
+ const corePasswordStrengthResultsClass =
+ UIComponents.core.passwordStrengthResults.attr( 'class' ) || '';
+ const coreValidationFailed = ! (
+ corePasswordStrengthResultsClass.includes( 'strong' ) ||
+ corePasswordStrengthResultsClass.includes( 'good' )
+ );
+
+ Object.entries( state ).forEach( ( [ key, item ] ) => {
+ const validationFailed = key === 'core' ? coreValidationFailed : item.status;
+ const checklistItem = UIComponents.validationChecklistItems[ key ];
+
+ // Display the core and backslash validation items they fail
+ if ( [ 'core', 'contains_backslash' ].includes( key ) ) {
+ checklistItem.item.css( 'display', validationFailed ? 'flex' : 'none' );
+ }
+
+ if ( checklistItem ) {
+ checklistItem.icon.attr(
+ 'src',
+ validationFailed ? jetpackData.crossIcon : jetpackData.checkIcon
+ );
+ checklistItem.icon.attr( 'alt', validationFailed ? 'Invalid' : 'Valid' );
+ checklistItem.text.css( { color: validationFailed ? '#E65054' : '#008710' } );
+ }
+
+ if ( validationFailed ) failedConditions.push( key );
+ } );
+
+ return failedConditions;
+ }
+
+ /**
+ *
+ * Apply styling based on validation results
+ *
+ * @param {Array} failedValidationConditions - Array containing failed validation conditions keys
+ * @param {boolean} passwordIsEmpty - Whether the password input is empty
+ */
+ function applyStyling( failedValidationConditions, passwordIsEmpty = false ) {
+ if ( passwordIsEmpty ) {
+ renderEmptyInputState();
+ return;
+ }
+
+ const isPasswordStrong = failedValidationConditions.length === 0;
+ const color = isPasswordStrong ? '#9DD977' : '#FFABAF';
+ const strengthText = isPasswordStrong ? 'Strong' : 'Weak';
+
+ const {
+ weakPasswordConfirmation,
+ weakPasswordConfirmationCheckbox,
+ submitButtons,
+ passwordInput,
+ } = UIComponents.core;
+ const { wrapper, text } = UIComponents.strengthMeter;
+
+ if ( isPasswordStrong || weakPasswordConfirmationCheckbox.prop( 'checked' ) ) {
+ submitButtons.prop( 'disabled', false );
+ } else {
+ submitButtons.prop( 'disabled', true );
+ }
+
+ weakPasswordConfirmation.css( 'display', isPasswordStrong ? 'none' : 'table-row' );
+
+ text.text( strengthText );
+ wrapper.css( 'background-color', color );
+
+ passwordInput.css( {
+ 'border-color': color,
+ 'border-radius': '4px 4px 0px 0px',
+ } );
+
+ UIComponents.passwordValidationStatus.show();
+ }
+
+ /**
+ * Render the empty input state
+ */
+ function renderEmptyInputState() {
+ UIComponents.passwordValidationStatus.hide();
+ UIComponents.core.passwordInput.removeAttr( 'style' );
+ }
+
+ /**
+ * Render the loading state
+ */
+ function renderLoadingState() {
+ const { passwordInput, weakPasswordConfirmation, submitButtons } = UIComponents.core;
+ submitButtons.prop( 'disabled', true );
+ weakPasswordConfirmation.hide();
+
+ Object.values( UIComponents.validationChecklistItems ).forEach( ( { icon, text } ) => {
+ icon.attr( 'src', jetpackData.loadingIcon );
+ icon.attr( 'alt', 'Validating...' );
+ text.css( { color: '#3C434A', transition: 'color 0.2s ease-in-out' } );
+ } );
+
+ UIComponents.strengthMeter.text.text( 'Validating...' );
+ UIComponents.strengthMeter.wrapper.css( 'background-color', '#C3C4C7' );
+ passwordInput.css( {
+ 'border-color': '#C3C4C7',
+ 'border-radius': '4px 4px 0px 0px',
+ } );
+
+ UIComponents.passwordValidationStatus.show();
+ }
+
+ /**
+ * Resets UI to core strength meter.
+ */
+ function restoreCoreStrengthMeter() {
+ renderEmptyInputState();
+ UIComponents.core.passwordStrengthResults.show();
+ }
+
+ /**
+ * Creates an info popover element.
+ *
+ * @param {string} infoText - The text to display in the popover.
+ * @return {jQuery} - The info popover element.
+ */
+ function createInfoIconPopover( infoText ) {
+ const infoIcon = $( '', { src: jetpackData.infoIcon, alt: 'Info', class: 'info-icon' } );
+ const popover = $( '