eventbrite-for-the-events-c.../includes/class-eb4tec-settings.php
Laurence Horrocks-Barlow f3bc795d9a Initial release of Eventbrite for The Events Calendar (v1.0.0)
Bidirectional sync between Eventbrite and The Events Calendar, with
WooCommerce ticket purchasing that bypasses Eventbrite's processing
fees by registering buyers as free attendees via API. Includes venue/
organizer sync, QR code ticket generation, attendee management with
CSV export, scheduled sync via WP-Cron, and real-time Eventbrite
webhooks.
2026-05-17 08:48:04 +01:00

547 lines
21 KiB
PHP

<?php
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
final class EB4TEC_Settings {
public function __construct() {
add_action( 'admin_menu', [ $this, 'add_admin_menu' ] );
add_action( 'admin_post_eb4tec_save_settings', [ $this, 'handle_settings_save' ] );
add_action( 'wp_ajax_eb4tec_validate_token', [ $this, 'ajax_validate_token' ] );
add_action( 'wp_ajax_eb4tec_register_webhook', [ $this, 'ajax_register_webhook' ] );
add_action( 'wp_ajax_eb4tec_delete_webhook', [ $this, 'ajax_delete_webhook' ] );
add_action( 'admin_enqueue_scripts', [ $this, 'admin_enqueue_scripts' ] );
add_action( 'admin_notices', [ $this, 'admin_notices' ] );
}
public function add_admin_menu(): void {
add_submenu_page(
'edit.php?post_type=tribe_events',
__( 'Eventbrite Sync', 'eb4tec' ),
__( 'Eventbrite Sync', 'eb4tec' ),
'manage_options',
'eb4tec-settings',
[ $this, 'render_settings_page' ]
);
// Attendees submenu page is registered by EB4TEC_Attendees.
}
public function render_settings_page(): void {
if ( ! current_user_can( 'manage_options' ) ) {
return;
}
$active_tab = sanitize_key( $_GET['tab'] ?? 'api' );
$org_name = esc_html( get_option( 'eb4tec_org_name', '' ) );
$last_sync = (int) get_option( 'eb4tec_last_sync_timestamp', 0 );
$sync_result = get_transient( 'eb4tec_last_sync_result' );
echo '<div class="wrap">';
echo '<h1>' . esc_html__( 'Eventbrite for The Events Calendar', 'eb4tec' ) . '</h1>';
// Sync status bar.
if ( $last_sync ) {
$sync_time = human_time_diff( $last_sync );
echo '<p class="description">';
printf(
esc_html__( 'Last sync: %s ago', 'eb4tec' ),
esc_html( $sync_time )
);
if ( is_array( $sync_result ) ) {
printf(
' — ' . esc_html__( '%d pulled, %d pushed, %d errors.', 'eb4tec' ),
(int) ( $sync_result['pulled'] ?? 0 ),
(int) ( $sync_result['pushed'] ?? 0 ),
(int) ( $sync_result['errors'] ?? 0 )
);
}
echo '</p>';
}
// Sync Now button.
echo '<form method="post" action="' . esc_url( admin_url( 'admin-post.php' ) ) . '" style="display:inline-block;margin-bottom:12px;">';
wp_nonce_field( 'eb4tec_sync_now', '_eb4tec_sync_nonce' );
echo '<input type="hidden" name="action" value="eb4tec_sync_now">';
submit_button( __( 'Sync Now', 'eb4tec' ), 'secondary', 'submit', false );
echo '</form>';
// Tabs.
$tabs = [
'api' => __( 'API & Credentials', 'eb4tec' ),
'sync' => __( 'Sync Settings', 'eb4tec' ),
'woocommerce' => __( 'WooCommerce', 'eb4tec' ),
];
echo '<nav class="nav-tab-wrapper">';
foreach ( $tabs as $slug => $label ) {
$class = ( $slug === $active_tab ) ? 'nav-tab nav-tab-active' : 'nav-tab';
$url = add_query_arg( [ 'page' => 'eb4tec-settings', 'tab' => $slug ], admin_url( 'edit.php?post_type=tribe_events' ) );
printf( '<a href="%s" class="%s">%s</a>', esc_url( $url ), esc_attr( $class ), esc_html( $label ) );
}
echo '</nav>';
echo '<form method="post" action="' . esc_url( admin_url( 'admin-post.php' ) ) . '">';
wp_nonce_field( 'eb4tec_save_settings', '_eb4tec_settings_nonce' );
echo '<input type="hidden" name="action" value="eb4tec_save_settings">';
echo '<input type="hidden" name="eb4tec_tab" value="' . esc_attr( $active_tab ) . '">';
echo '<table class="form-table" role="presentation"><tbody>';
match ( $active_tab ) {
'api' => $this->render_tab_api( $org_name ),
'sync' => $this->render_tab_sync(),
'woocommerce' => $this->render_tab_woocommerce(),
default => $this->render_tab_api( $org_name ),
};
echo '</tbody></table>';
submit_button();
echo '</form>';
echo '</div>';
}
private function render_tab_api( string $org_name ): void {
$token = get_option( 'eb4tec_api_token', '' );
$token_display = $token ? str_repeat( '•', 20 ) : '';
$org_id = esc_attr( get_option( 'eb4tec_org_id', '' ) );
?>
<tr>
<th scope="row"><label for="eb4tec_api_token"><?php esc_html_e( 'Eventbrite API Token', 'eb4tec' ); ?></label></th>
<td>
<input type="password" id="eb4tec_api_token" name="eb4tec_api_token"
class="regular-text" value="<?php echo esc_attr( $token_display ); ?>"
placeholder="<?php esc_attr_e( 'Paste your private token here', 'eb4tec' ); ?>"
autocomplete="new-password">
<p class="description">
<?php esc_html_e( 'Find your token at eventbrite.com → Account Settings → Developer Links → API Keys.', 'eb4tec' ); ?>
</p>
<button type="button" id="eb4tec-validate-token" class="button button-secondary" style="margin-top:6px;">
<?php esc_html_e( 'Validate & Fetch Organization', 'eb4tec' ); ?>
</button>
<span id="eb4tec-validate-result" style="margin-left:8px;"></span>
</td>
</tr>
<tr>
<th scope="row"><label for="eb4tec_org_id"><?php esc_html_e( 'Organization ID', 'eb4tec' ); ?></label></th>
<td>
<input type="text" id="eb4tec_org_id" name="eb4tec_org_id"
class="regular-text" value="<?php echo $org_id; ?>">
<?php if ( $org_name ) : ?>
<p class="description"><?php echo esc_html( sprintf( __( 'Connected: %s', 'eb4tec' ), $org_name ) ); ?></p>
<?php endif; ?>
</td>
</tr>
<tr>
<th scope="row"><?php esc_html_e( 'Webhook URL', 'eb4tec' ); ?></th>
<td>
<code><?php echo esc_url( add_query_arg( 'eb4tec_webhook', '1', home_url( '/' ) ) ); ?></code>
<br><br>
<button type="button" id="eb4tec-register-webhook" class="button button-secondary">
<?php esc_html_e( 'Register Webhook with Eventbrite', 'eb4tec' ); ?>
</button>
<button type="button" id="eb4tec-delete-webhook" class="button button-secondary" style="margin-left:6px;">
<?php esc_html_e( 'Remove Webhook', 'eb4tec' ); ?>
</button>
<span id="eb4tec-webhook-result" style="margin-left:8px;"></span>
<p class="description"><?php esc_html_e( 'Register this URL with Eventbrite to receive real-time event updates.', 'eb4tec' ); ?></p>
</td>
</tr>
<?php
}
private function render_tab_sync(): void {
$interval = get_option( 'eb4tec_sync_interval', 'hourly' );
$direction = get_option( 'eb4tec_sync_direction', 'both' );
$label = get_option( 'eb4tec_wp_ticket_label', 'WordPress Purchase' );
$visible = get_option( 'eb4tec_wp_ticket_visible', 'hidden' );
$publish = get_option( 'eb4tec_auto_publish', '1' );
$venues = get_option( 'eb4tec_sync_venues', '1' );
$orgs = get_option( 'eb4tec_sync_organizers', '1' );
$intervals = [
'hourly' => __( 'Hourly', 'eb4tec' ),
'twicedaily' => __( 'Twice Daily', 'eb4tec' ),
'daily' => __( 'Daily', 'eb4tec' ),
];
$directions = [
'both' => __( 'Both directions', 'eb4tec' ),
'eb_to_tec' => __( 'Eventbrite → WordPress only', 'eb4tec' ),
'tec_to_eb' => __( 'WordPress → Eventbrite only', 'eb4tec' ),
];
?>
<tr>
<th scope="row"><label for="eb4tec_sync_interval"><?php esc_html_e( 'Sync Interval', 'eb4tec' ); ?></label></th>
<td>
<select id="eb4tec_sync_interval" name="eb4tec_sync_interval">
<?php foreach ( $intervals as $val => $label_text ) : ?>
<option value="<?php echo esc_attr( $val ); ?>" <?php selected( $interval, $val ); ?>>
<?php echo esc_html( $label_text ); ?>
</option>
<?php endforeach; ?>
</select>
</td>
</tr>
<tr>
<th scope="row"><label for="eb4tec_sync_direction"><?php esc_html_e( 'Sync Direction', 'eb4tec' ); ?></label></th>
<td>
<select id="eb4tec_sync_direction" name="eb4tec_sync_direction">
<?php foreach ( $directions as $val => $label_text ) : ?>
<option value="<?php echo esc_attr( $val ); ?>" <?php selected( $direction, $val ); ?>>
<?php echo esc_html( $label_text ); ?>
</option>
<?php endforeach; ?>
</select>
</td>
</tr>
<tr>
<th scope="row"><label for="eb4tec_wp_ticket_label"><?php esc_html_e( 'WordPress Ticket Label on Eventbrite', 'eb4tec' ); ?></label></th>
<td>
<input type="text" id="eb4tec_wp_ticket_label" name="eb4tec_wp_ticket_label"
class="regular-text" value="<?php echo esc_attr( $label ); ?>">
<p class="description"><?php esc_html_e( 'The name of the hidden free ticket class created on Eventbrite for WordPress purchases.', 'eb4tec' ); ?></p>
</td>
</tr>
<tr>
<th scope="row"><label for="eb4tec_wp_ticket_visible"><?php esc_html_e( 'WordPress Ticket Visibility on Eventbrite', 'eb4tec' ); ?></label></th>
<td>
<select id="eb4tec_wp_ticket_visible" name="eb4tec_wp_ticket_visible">
<option value="hidden" <?php selected( $visible, 'hidden' ); ?>><?php esc_html_e( 'Hidden (recommended)', 'eb4tec' ); ?></option>
<option value="visible" <?php selected( $visible, 'visible' ); ?>><?php esc_html_e( 'Visible', 'eb4tec' ); ?></option>
</select>
</td>
</tr>
<tr>
<th scope="row"><?php esc_html_e( 'Auto-publish Events', 'eb4tec' ); ?></th>
<td>
<label>
<input type="checkbox" name="eb4tec_auto_publish" value="1" <?php checked( $publish, '1' ); ?>>
<?php esc_html_e( 'Automatically publish events pushed to Eventbrite', 'eb4tec' ); ?>
</label>
</td>
</tr>
<tr>
<th scope="row"><?php esc_html_e( 'Sync Venues', 'eb4tec' ); ?></th>
<td>
<label>
<input type="checkbox" name="eb4tec_sync_venues" value="1" <?php checked( $venues, '1' ); ?>>
<?php esc_html_e( 'Sync venue data between Eventbrite and The Events Calendar', 'eb4tec' ); ?>
</label>
</td>
</tr>
<tr>
<th scope="row"><?php esc_html_e( 'Sync Organizers', 'eb4tec' ); ?></th>
<td>
<label>
<input type="checkbox" name="eb4tec_sync_organizers" value="1" <?php checked( $orgs, '1' ); ?>>
<?php esc_html_e( 'Sync organizer data between Eventbrite and The Events Calendar', 'eb4tec' ); ?>
</label>
</td>
</tr>
<?php
}
private function render_tab_woocommerce(): void {
$cat_id = (int) get_option( 'eb4tec_woo_category_id', 0 );
$qr_size = (int) get_option( 'eb4tec_qr_size', 200 );
$qr_in_email = get_option( 'eb4tec_qr_in_email', '1' );
$qr_on_order = get_option( 'eb4tec_qr_on_order_page', '1' );
$delete_data = get_option( 'eb4tec_delete_on_uninstall', '' );
$categories = get_terms( [
'taxonomy' => 'product_cat',
'hide_empty' => false,
] );
?>
<tr>
<th scope="row"><label for="eb4tec_woo_category_id"><?php esc_html_e( 'Ticket Product Category', 'eb4tec' ); ?></label></th>
<td>
<select id="eb4tec_woo_category_id" name="eb4tec_woo_category_id">
<option value="0"><?php esc_html_e( '— None —', 'eb4tec' ); ?></option>
<?php if ( ! is_wp_error( $categories ) ) : ?>
<?php foreach ( $categories as $cat ) : ?>
<option value="<?php echo esc_attr( $cat->term_id ); ?>" <?php selected( $cat_id, $cat->term_id ); ?>>
<?php echo esc_html( $cat->name ); ?>
</option>
<?php endforeach; ?>
<?php endif; ?>
</select>
<p class="description"><?php esc_html_e( 'WooCommerce products created for event tickets will be assigned to this category.', 'eb4tec' ); ?></p>
</td>
</tr>
<tr>
<th scope="row"><label for="eb4tec_qr_size"><?php esc_html_e( 'QR Code Size (px)', 'eb4tec' ); ?></label></th>
<td>
<input type="number" id="eb4tec_qr_size" name="eb4tec_qr_size"
value="<?php echo esc_attr( $qr_size ); ?>" min="100" max="500" step="10" class="small-text">
</td>
</tr>
<tr>
<th scope="row"><?php esc_html_e( 'QR Code in Email', 'eb4tec' ); ?></th>
<td>
<label>
<input type="checkbox" name="eb4tec_qr_in_email" value="1" <?php checked( $qr_in_email, '1' ); ?>>
<?php esc_html_e( 'Include QR code in the WooCommerce order confirmation email', 'eb4tec' ); ?>
</label>
</td>
</tr>
<tr>
<th scope="row"><?php esc_html_e( 'QR Code on Order Page', 'eb4tec' ); ?></th>
<td>
<label>
<input type="checkbox" name="eb4tec_qr_on_order_page" value="1" <?php checked( $qr_on_order, '1' ); ?>>
<?php esc_html_e( 'Show QR code on the order confirmation / thank-you page', 'eb4tec' ); ?>
</label>
</td>
</tr>
<tr>
<th scope="row"><?php esc_html_e( 'Delete Data on Uninstall', 'eb4tec' ); ?></th>
<td>
<label>
<input type="checkbox" name="eb4tec_delete_on_uninstall" value="1" <?php checked( $delete_data, '1' ); ?>>
<?php esc_html_e( 'Remove all plugin settings and post meta when the plugin is deleted', 'eb4tec' ); ?>
</label>
</td>
</tr>
<?php
}
public function handle_settings_save(): void {
if ( ! current_user_can( 'manage_options' ) ) {
wp_die( esc_html__( 'Insufficient permissions.', 'eb4tec' ) );
}
check_admin_referer( 'eb4tec_save_settings', '_eb4tec_settings_nonce' );
$tab = sanitize_key( $_POST['eb4tec_tab'] ?? 'api' );
if ( 'api' === $tab ) {
$raw_token = $_POST['eb4tec_api_token'] ?? '';
// Only update if a real value was submitted (not the masked placeholder).
if ( $raw_token && ! str_contains( $raw_token, '•' ) ) {
update_option( 'eb4tec_api_token', sanitize_text_field( $raw_token ), false );
}
update_option( 'eb4tec_org_id', sanitize_text_field( $_POST['eb4tec_org_id'] ?? '' ) );
}
if ( 'sync' === $tab ) {
$interval = sanitize_key( $_POST['eb4tec_sync_interval'] ?? 'hourly' );
if ( ! in_array( $interval, [ 'hourly', 'twicedaily', 'daily' ], true ) ) {
$interval = 'hourly';
}
$old_interval = get_option( 'eb4tec_sync_interval', 'hourly' );
update_option( 'eb4tec_sync_interval', $interval );
if ( $interval !== $old_interval ) {
EB4TEC_Cron::unschedule();
EB4TEC_Cron::schedule();
}
$direction = sanitize_key( $_POST['eb4tec_sync_direction'] ?? 'both' );
if ( ! in_array( $direction, [ 'both', 'eb_to_tec', 'tec_to_eb' ], true ) ) {
$direction = 'both';
}
update_option( 'eb4tec_sync_direction', $direction );
update_option( 'eb4tec_wp_ticket_label', sanitize_text_field( $_POST['eb4tec_wp_ticket_label'] ?? 'WordPress Purchase' ) );
$visible = sanitize_key( $_POST['eb4tec_wp_ticket_visible'] ?? 'hidden' );
update_option( 'eb4tec_wp_ticket_visible', in_array( $visible, [ 'hidden', 'visible' ], true ) ? $visible : 'hidden' );
update_option( 'eb4tec_auto_publish', isset( $_POST['eb4tec_auto_publish'] ) ? '1' : '' );
update_option( 'eb4tec_sync_venues', isset( $_POST['eb4tec_sync_venues'] ) ? '1' : '' );
update_option( 'eb4tec_sync_organizers', isset( $_POST['eb4tec_sync_organizers'] ) ? '1' : '' );
}
if ( 'woocommerce' === $tab ) {
update_option( 'eb4tec_woo_category_id', absint( $_POST['eb4tec_woo_category_id'] ?? 0 ) );
$qr_size = max( 100, min( 500, (int) ( $_POST['eb4tec_qr_size'] ?? 200 ) ) );
update_option( 'eb4tec_qr_size', $qr_size );
update_option( 'eb4tec_qr_in_email', isset( $_POST['eb4tec_qr_in_email'] ) ? '1' : '' );
update_option( 'eb4tec_qr_on_order_page', isset( $_POST['eb4tec_qr_on_order_page'] ) ? '1' : '' );
update_option( 'eb4tec_delete_on_uninstall', isset( $_POST['eb4tec_delete_on_uninstall'] ) ? '1' : '' );
}
$redirect = add_query_arg( [
'page' => 'eb4tec-settings',
'tab' => $tab,
'updated' => '1',
], admin_url( 'edit.php?post_type=tribe_events' ) );
wp_safe_redirect( $redirect );
exit;
}
public function ajax_validate_token(): void {
check_ajax_referer( 'eb4tec_admin_ajax', 'nonce' );
if ( ! current_user_can( 'manage_options' ) ) {
wp_send_json_error( [ 'message' => __( 'Insufficient permissions.', 'eb4tec' ) ] );
}
$token = sanitize_text_field( $_POST['token'] ?? '' );
if ( empty( $token ) ) {
wp_send_json_error( [ 'message' => __( 'No token provided.', 'eb4tec' ) ] );
}
// Temporarily update the token so the API client picks it up.
$old_token = get_option( 'eb4tec_api_token', '' );
update_option( 'eb4tec_api_token', $token, false );
$api = new EB4TEC_API_Client();
$result = $api->get_user_me();
if ( is_wp_error( $result ) ) {
update_option( 'eb4tec_api_token', $old_token, false );
wp_send_json_error( [ 'message' => $result->get_error_message() ] );
}
// Fetch organization ID.
$orgs = isset( $result['organizations'] ) ? $result['organizations'] : [];
if ( empty( $orgs ) ) {
// Try getting org from user profile.
$org_id = '';
$org_name = $result['name'] ?? '';
} else {
$org = reset( $orgs );
$org_id = $org['id'] ?? '';
$org_name = $org['name'] ?? '';
}
update_option( 'eb4tec_org_id', sanitize_text_field( $org_id ) );
update_option( 'eb4tec_org_name', sanitize_text_field( $org_name ) );
wp_send_json_success( [
'message' => sprintf( __( 'Connected as %s (Org: %s)', 'eb4tec' ), esc_html( $result['name'] ?? '' ), esc_html( $org_name ) ),
'org_id' => $org_id,
'org_name' => $org_name,
] );
}
public function ajax_register_webhook(): void {
check_ajax_referer( 'eb4tec_admin_ajax', 'nonce' );
if ( ! current_user_can( 'manage_options' ) ) {
wp_send_json_error( [ 'message' => __( 'Insufficient permissions.', 'eb4tec' ) ] );
}
$secret = get_option( 'eb4tec_webhook_secret', '' );
if ( empty( $secret ) ) {
$secret = wp_generate_password( 32, false );
update_option( 'eb4tec_webhook_secret', $secret );
}
$endpoint_url = add_query_arg( 'eb4tec_webhook', '1', home_url( '/' ) );
$api = new EB4TEC_API_Client();
$result = $api->create_webhook( [
'webhook' => [
'endpoint_url' => $endpoint_url,
'actions' => 'event.created,event.published,event.updated,event.unpublished,attendee.updated',
],
] );
if ( is_wp_error( $result ) ) {
wp_send_json_error( [ 'message' => $result->get_error_message() ] );
}
update_option( 'eb4tec_webhook_id', sanitize_text_field( $result['id'] ?? '' ) );
wp_send_json_success( [ 'message' => __( 'Webhook registered successfully.', 'eb4tec' ) ] );
}
public function ajax_delete_webhook(): void {
check_ajax_referer( 'eb4tec_admin_ajax', 'nonce' );
if ( ! current_user_can( 'manage_options' ) ) {
wp_send_json_error( [ 'message' => __( 'Insufficient permissions.', 'eb4tec' ) ] );
}
$webhook_id = get_option( 'eb4tec_webhook_id', '' );
if ( empty( $webhook_id ) ) {
wp_send_json_error( [ 'message' => __( 'No webhook ID stored.', 'eb4tec' ) ] );
}
$api = new EB4TEC_API_Client();
$result = $api->delete_webhook( $webhook_id );
if ( is_wp_error( $result ) ) {
wp_send_json_error( [ 'message' => $result->get_error_message() ] );
}
delete_option( 'eb4tec_webhook_id' );
wp_send_json_success( [ 'message' => __( 'Webhook removed.', 'eb4tec' ) ] );
}
public function admin_enqueue_scripts( string $hook ): void {
$screen = get_current_screen();
if ( ! $screen || ! str_contains( $screen->id ?? '', 'eb4tec' ) ) {
return;
}
$nonce = wp_create_nonce( 'eb4tec_admin_ajax' );
wp_add_inline_script(
'jquery',
"(function($){ const nonce = '{$nonce}';
$('#eb4tec-validate-token').on('click', function(){
var token = $('#eb4tec_api_token').val();
$.post(ajaxurl, {action:'eb4tec_validate_token',nonce:nonce,token:token}, function(r){
var el = $('#eb4tec-validate-result');
if(r.success){ el.css('color','green').text(r.data.message); $('#eb4tec_org_id').val(r.data.org_id); }
else { el.css('color','red').text(r.data.message); }
});
});
$('#eb4tec-register-webhook').on('click', function(){
$.post(ajaxurl, {action:'eb4tec_register_webhook',nonce:nonce}, function(r){
var el = $('#eb4tec-webhook-result');
if(r.success){ el.css('color','green').text(r.data.message); }
else { el.css('color','red').text(r.data.message); }
});
});
$('#eb4tec-delete-webhook').on('click', function(){
$.post(ajaxurl, {action:'eb4tec_delete_webhook',nonce:nonce}, function(r){
var el = $('#eb4tec-webhook-result');
if(r.success){ el.css('color','green').text(r.data.message); }
else { el.css('color','red').text(r.data.message); }
});
});
})(jQuery);"
);
}
public function admin_notices(): void {
if ( ! current_user_can( 'manage_options' ) ) {
return;
}
$screen = get_current_screen();
if ( ! $screen || ! str_contains( $screen->id ?? '', 'eb4tec' ) ) {
return;
}
if ( ! empty( $_GET['updated'] ) ) {
echo '<div class="notice notice-success is-dismissible"><p>' .
esc_html__( 'Settings saved.', 'eb4tec' ) .
'</p></div>';
}
if ( get_transient( 'eb4tec_rate_limit_warning' ) ) {
echo '<div class="notice notice-warning is-dismissible"><p>' .
esc_html__( 'Eventbrite API rate limit is nearly exhausted. Sync operations may be delayed.', 'eb4tec' ) .
'</p></div>';
}
// Show "Sync Now" result.
$user_id = get_current_user_id();
$sync_result = get_transient( "eb4tec_sync_now_result_{$user_id}" );
if ( is_array( $sync_result ) ) {
delete_transient( "eb4tec_sync_now_result_{$user_id}" );
$msg = sprintf(
__( 'Sync complete — %d pulled from Eventbrite, %d pushed to Eventbrite, %d errors.', 'eb4tec' ),
(int) ( $sync_result['pulled'] ?? 0 ),
(int) ( $sync_result['pushed'] ?? 0 ),
(int) ( $sync_result['errors'] ?? 0 )
);
$class = ( ( $sync_result['errors'] ?? 0 ) > 0 ) ? 'notice-warning' : 'notice-success';
echo '<div class="notice ' . esc_attr( $class ) . ' is-dismissible"><p>' . esc_html( $msg ) . '</p></div>';
}
}
}