Skip to content

Connection: protected owner errors manual fix #43593

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

Merged
merged 21 commits into from
Jun 4, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Significance: minor
Type: added

Connection: error handling for protected owner on WPcom.
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,13 @@ import styles from './styles.module.scss';
* @return {React.Component} The `ConnectionErrorNotice` component.
*/
const ConnectionErrorNotice = props => {
const { message, isRestoringConnection, restoreConnectionCallback, restoreConnectionError } =
props;
const {
message,
isRestoringConnection,
restoreConnectionCallback,
restoreConnectionError,
actions = [], // New prop for custom actions
} = props;

const [ isBiggerThanMedium ] = useBreakpointMatch( [ 'md' ], [ '>' ] );
const wrapperClassName =
Expand Down Expand Up @@ -73,6 +78,39 @@ const ConnectionErrorNotice = props => {
</Notice>
) : null;

// Determine which actions to show
let actionButtons = [];

if ( actions.length > 0 ) {
// Use custom actions
actionButtons = actions.map( ( action, index ) => (
<a
key={ index }
onClick={ action.onClick }
onKeyDown={ action.onClick }
className={ `${ styles.button } ${ action.variant === 'primary' ? styles.primary : '' }` }
href="#"
>
{ action.isLoading
? action.loadingText || __( 'Loading…', 'jetpack-connection-js' )
: action.label }
</a>
) );
} else if ( restoreConnectionCallback ) {
// Use default restore connection action for backward compatibility
actionButtons = [
<a
key="restore"
onClick={ restoreConnectionCallback }
onKeyDown={ restoreConnectionCallback }
className={ styles.button }
href="#"
>
{ __( 'Restore Connection', 'jetpack-connection-js' ) }
</a>,
];
}

return (
<>
{ errorRender }
Expand All @@ -81,30 +119,31 @@ const ConnectionErrorNotice = props => {
{ icon }
{ message }
</div>
{ restoreConnectionCallback && (
<a
onClick={ restoreConnectionCallback }
onKeyDown={ restoreConnectionCallback }
className={ styles.button }
href="#"
>
{ __( 'Restore Connection', 'jetpack-connection-js' ) }
</a>
) }
{ actionButtons.length > 0 && <div className={ styles.actions }>{ actionButtons }</div> }
</Notice>
</>
);
};

ConnectionErrorNotice.propTypes = {
/** The notice message. */
message: PropTypes.string.isRequired,
message: PropTypes.oneOfType( [ PropTypes.string, PropTypes.element ] ).isRequired,
/** "Restore Connection" button callback. */
restoreConnectionCallback: PropTypes.func,
/** Whether connection restore is in progress. */
isRestoringConnection: PropTypes.bool,
/** The connection error text if there is one. */
restoreConnectionError: PropTypes.string,
/** Array of custom action objects. */
actions: PropTypes.arrayOf(
PropTypes.shape( {
label: PropTypes.string.isRequired,
onClick: PropTypes.func.isRequired,
isLoading: PropTypes.bool,
loadingText: PropTypes.string,
variant: PropTypes.oneOf( [ 'primary', 'secondary' ] ),
} )
),
};

export default ConnectionErrorNotice;
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,16 @@
margin-top: 24px;
background: #000;
border-radius: var( --jp-border-radius );

&.primary {
background: var( --jp-green-50 );
}
}

.actions {
display: flex;
gap: calc( var( --spacing-base ) * 2 ); // 16px
flex-wrap: wrap;
}

&.bigger-than-medium {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,38 +1,130 @@
import { __ } from '@wordpress/i18n';
import ConnectionErrorNotice from '../../components/connection-error-notice';
import useConnection from '../../components/use-connection';
import useRestoreConnection from '../../hooks/use-restore-connection/index.jsx';

/**
* Helper function to generate user creation URL with email prepopulation
*
* @param {object} connectionError - The connection error object
* @param {string} baseUrl - Base admin URL (defaults to '/wp-admin/')
* @return {string} The complete URL for user creation with email parameters
*/
export function getProtectedOwnerCreateAccountUrl( connectionError, baseUrl = '/wp-admin/' ) {
let redirectUrl = baseUrl + 'user-new.php';

// Add protected owner email if available for prepopulation
if ( connectionError?.error_data?.wpcom_user_email ) {
const params = new URLSearchParams( {
jetpack_protected_owner_email: connectionError.error_data.wpcom_user_email,
jetpack_create_missing_account: '1',
} );
redirectUrl += '?' + params.toString();
} else if ( connectionError?.error_data?.email ) {
const params = new URLSearchParams( {
jetpack_protected_owner_email: connectionError.error_data.email,
jetpack_create_missing_account: '1',
} );
redirectUrl += '?' + params.toString();
}

return redirectUrl;
}

/**
* Connection error notice hook.
* Returns a ConnectionErrorNotice component and the conditional flag on whether
* Returns connection error data and conditional flag on whether
* to render the component or not.
*
* @return {object} - The hook data.
*/
export default function useConnectionErrorNotice() {
const { connectionErrors } = useConnection( {} );
const connectionErrorList = Object.values( connectionErrors ).shift();
const connectionErrorMessage =
const firstError =
connectionErrorList &&
Object.values( connectionErrorList ).length &&
Object.values( connectionErrorList ).shift().error_message;
Object.values( connectionErrorList ).shift();

const connectionErrorMessage = firstError && firstError.error_message;

// Return all connection errors, including protected owner errors
const hasConnectionError = Boolean( connectionErrorMessage );

return { hasConnectionError, connectionErrorMessage };
return {
hasConnectionError,
connectionErrorMessage,
connectionError: firstError, // Full error object with error_type, etc.
connectionErrors, // All errors for advanced use cases
};
}

export const ConnectionError = () => {
const { hasConnectionError, connectionErrorMessage } = useConnectionErrorNotice();
export const ConnectionError = ( {
onCreateMissingAccount = null, // Custom handler for protected owner errors
trackingCallback = null, // Custom tracking function
customActions = null, // Function that returns custom actions based on error
} = {} ) => {
const { hasConnectionError, connectionErrorMessage, connectionError } =
useConnectionErrorNotice();
const { restoreConnection, isRestoringConnection, restoreConnectionError } =
useRestoreConnection();

return hasConnectionError ? (
if ( ! hasConnectionError ) {
return null;
}

// Determine error type
const isProtectedOwnerError = connectionError && connectionError.error_type === 'protected_owner';

// Build actions array based on error type
let actions = [];

if ( customActions ) {
// Use provided custom actions function
actions = customActions( connectionError, { restoreConnection, isRestoringConnection } );
} else if ( isProtectedOwnerError && onCreateMissingAccount ) {
// Handle protected owner error with custom handler
actions = [
{
label: __( 'Create missing account', 'jetpack-connection-js' ),
onClick: () => {
if ( trackingCallback ) {
trackingCallback( 'jetpack_connection_protected_owner_create_account_attempt', {} );
}
onCreateMissingAccount();
},
variant: 'primary',
},
];
} else if ( ! isProtectedOwnerError ) {
// Standard connection error - use restore connection
actions = [
{
label: __( 'Restore Connection', 'jetpack-connection-js' ),
onClick: () => {
if ( trackingCallback ) {
trackingCallback( 'jetpack_connection_error_notice_reconnect_cta_click', {} );
}
restoreConnection();
},
isLoading: isRestoringConnection,
loadingText: __( 'Reconnecting Jetpack…', 'jetpack-connection-js' ),
},
];
}

// For protected owner errors without custom handler, don't show the component
if ( isProtectedOwnerError && ! onCreateMissingAccount && ! customActions ) {
return null;
}

return (
<ConnectionErrorNotice
isRestoringConnection={ isRestoringConnection }
restoreConnectionError={ restoreConnectionError }
restoreConnectionCallback={ restoreConnection }
restoreConnectionCallback={ actions.length === 0 ? restoreConnection : null } // Fallback for backward compatibility
message={ connectionErrorMessage }
actions={ actions }
/>
) : null;
);
};
Loading
Loading