Back

97.82% Statements 45/46
100% Branches 49/49
88.88% Functions 8/9
97.82% Lines 45/46

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182                            3x                                                                             25x 2x 2x           1x       23x 8x 3x       23x 23x       23x 2x     21x       21x   3x       1x 1x 1x               18x   4x 4x       1x 1x 1x                   14x       4x 4x       4x                       21x 9x 9x 9x 9x 9x     9x 1x     1x 1x 1x                 8x 1x     1x 1x 1x                   21x    
import { __ } from '@wordpress/i18n';
import type {
	Action,
	ConnectionErrorData,
	ConnectionErrorObject,
	ConnectionErrorProps,
	RestoreConnection,
} from './types';
 
/**
 * Default tracking event fired when the fallback "Restore Connection" CTA is clicked.
 * Consumers (e.g. My Jetpack) can override this with their own namespaced event.
 */
export const DEFAULT_RECONNECT_TRACKING_EVENT =
	'jetpack_connection_error_notice_reconnect_cta_click';
 
/**
 * The resolver's options: the public `ConnectionErrorProps` plus the two fields
 * the hook supplies internally (`restoreConnection` / `isRestoringConnection`).
 */
export interface ResolveActionsOptions extends ConnectionErrorProps {
	/** Initiates a connection restore (the default fallback action). */
	restoreConnection: RestoreConnection;
	/** Whether a connection restore is currently in progress. */
	isRestoringConnection: boolean;
}
 
/**
 * Resolve a connection error object into a list of actionable buttons.
 *
 * This is the single source of truth for turning an error's `error_data`
 * (named handler, URL + label, or fallback) into `{ label, onClick, … }`
 * actions. Both the package's `<ConnectionError />` component and external
 * consumers (My Jetpack) call this so the copy/CTA logic lives in one place.
 *
 * @param {ConnectionErrorObject} connectionError - The effective connection error.
 * @param {ResolveActionsOptions} options         - Resolution options/helpers.
 * @return {Action[]} The resolved actions (may be empty).
 */
export function resolveConnectionErrorActions(
	connectionError: ConnectionErrorObject,
	{
		actionHandlers = {},
		trackingCallback = null,
		customActions = null,
		restoreConnection,
		isRestoringConnection,
		reconnectTrackingEvent = DEFAULT_RECONNECT_TRACKING_EVENT,
		navigate = ( url: string ) => {
			window.location.href = url;
		},
	}: ResolveActionsOptions
): Action[] {
	if ( customActions ) {
		try {
			return customActions( connectionError, {
				restoreConnection,
				isRestoringConnection,
			} );
		} catch {
			// Silently fall back to no actions if customActions throws.
			return [];
		}
	}
 
	const track = ( event?: string ) => {
		if ( trackingCallback && event ) {
			trackingCallback( event, {} );
		}
	};
 
	const errorData: ConnectionErrorData = connectionError.error_data ?? {};
	const suggestedAction = errorData.action;
 
	// An explicit 'none' action marks the error as informational only (for example,
	// a secondary admin viewing a locked connection owner's error). Surface no CTA.
	if ( suggestedAction === 'none' ) {
		return [];
	}
 
	const actionHandler = suggestedAction ? actionHandlers[ suggestedAction ] : undefined;
 
	let actions: Action[];
 
	if ( suggestedAction && actionHandler ) {
		// Named handler from the error data.
		actions = [
			{
				label: errorData.action_label || __( 'Take Action', 'jetpack-connection-js' ),
				onClick: () => {
					try {
						track( errorData.tracking_event );
						actionHandler( connectionError );
					} catch {
						// Silently fail if the action handler throws.
					}
				},
				variant: errorData.action_variant || 'primary',
			},
		];
	} else if ( errorData.action_url && errorData.action_label ) {
		// Generic link action - requires both URL and label for clarity.
		const actionUrl = errorData.action_url;
		actions = [
			{
				label: errorData.action_label,
				onClick: () => {
					try {
						track( errorData.tracking_event );
						navigate( actionUrl );
					} catch {
						// Silently fail if navigation throws.
					}
				},
				variant: errorData.action_variant || 'primary',
			},
		];
	} else {
		// Default action - restore connection.
		actions = [
			{
				label: __( 'Restore Connection', 'jetpack-connection-js' ),
				onClick: () => {
					try {
						track( reconnectTrackingEvent );
						// restoreConnection() rejects asynchronously; its failure is surfaced to
						// the user via restoreConnectionError → getReconnectErrorMessage, so we
						// acknowledge the rejection here to avoid an unhandled promise rejection.
						restoreConnection().catch( () => {} );
					} catch {
						// Silently fail if restore connection throws synchronously.
					}
				},
				isLoading: isRestoringConnection,
				loadingText: __( 'Reconnecting Jetpack…', 'jetpack-connection-js' ),
			},
		];
	}
 
	// Add secondary action if available (only for custom errors, not the default restore).
	if ( actions.length > 0 && ( suggestedAction || errorData.action_url ) ) {
		const secondaryAction = errorData.secondary_action;
		const secondaryActionHandler = secondaryAction ? actionHandlers[ secondaryAction ] : undefined;
		const secondaryActionUrl = errorData.secondary_action_url;
		const secondaryActionLabel = errorData.secondary_action_label;
		const secondaryActionVariant = errorData.secondary_action_variant || 'secondary';
 
		// Secondary action with handler.
		if ( secondaryAction && secondaryActionHandler && secondaryActionLabel ) {
			actions.push( {
				label: secondaryActionLabel,
				onClick: () => {
					try {
						track( errorData.secondary_tracking_event );
						secondaryActionHandler( connectionError );
					} catch {
						// Silently fail if the secondary action handler throws.
					}
				},
				variant: secondaryActionVariant,
			} );
		}
		// Secondary action with URL (requires both URL and label).
		else if ( secondaryActionUrl && secondaryActionLabel ) {
			actions.push( {
				label: secondaryActionLabel,
				onClick: () => {
					try {
						track( errorData.secondary_tracking_event );
						navigate( secondaryActionUrl );
					} catch {
						// Silently fail if the secondary action navigation throws.
					}
				},
				variant: secondaryActionVariant,
			} );
		}
	}
 
	return actions;
}