Below is a differential between the unpatched vulnerable code and the patched update, for reference.
--- a/latepoint/latepoint.php
+++ b/latepoint/latepoint.php
@@ -2,7 +2,7 @@
/**
* Plugin Name: LatePoint
* Description: Appointment Scheduling Software for WordPress
- * Version: 5.3.2
+ * Version: 5.4.0
* Author: LatePoint
* Author URI: https://latepoint.com
* Plugin URI: https://latepoint.com
@@ -29,7 +29,7 @@
* LatePoint version.
*
*/
- public $version = '5.3.2';
+ public $version = '5.4.0';
public $db_version = '2.3.0';
@@ -829,6 +829,7 @@
include_once LATEPOINT_ABSPATH . 'lib/helpers/events_helper.php';
include_once LATEPOINT_ABSPATH . 'lib/helpers/icalendar_helper.php';
include_once LATEPOINT_ABSPATH . 'lib/helpers/version_specific_updates_helper.php';
+ include_once LATEPOINT_ABSPATH . 'lib/helpers/update_helper.php';
include_once LATEPOINT_ABSPATH . 'lib/helpers/calendar_helper.php';
include_once LATEPOINT_ABSPATH . 'lib/helpers/meeting_systems_helper.php';
include_once LATEPOINT_ABSPATH . 'lib/helpers/marketing_systems_helper.php';
@@ -1056,6 +1057,9 @@
function plugins_loaded_hook() {
OsAnalyticsHelper::init();
+
+ // Should be trigger after all plugins are loaded.
+ OsUpdateHelper::init();
}
function check_addon_versions() {
--- a/latepoint/lib/controllers/customer_cabinet_controller.php
+++ b/latepoint/lib/controllers/customer_cabinet_controller.php
@@ -277,7 +277,8 @@
}
$booking_id = $this->params['id'];
- $booking = new OsBookingModel( $booking_id );
+ $this->check_nonce( 'cancel_booking_' . $booking_id );
+ $booking = new OsBookingModel( $booking_id );
if ( ! empty( $booking->id ) && ( OsAuthHelper::get_logged_in_customer_id() == $booking->customer_id ) && OsCustomerHelper::can_cancel_booking( $booking ) ) {
if ( $booking->update_status( LATEPOINT_BOOKING_STATUS_CANCELLED ) ) {
$status = LATEPOINT_STATUS_SUCCESS;
--- a/latepoint/lib/controllers/pro_controller.php
+++ b/latepoint/lib/controllers/pro_controller.php
@@ -63,6 +63,11 @@
$this->vars['page_header'] = OsMenuHelper::get_menu_items_by_id( 'locations' );
$this->format_render( 'pro_feature', [], [], true );
}
+
+ public function assets() {
+ $this->vars['page_header'] = __( 'Assets', 'latepoint' );
+ $this->format_render( 'pro_feature', [], [], true );
+ }
}
endif;
--- a/latepoint/lib/controllers/stripe_connect_controller.php
+++ b/latepoint/lib/controllers/stripe_connect_controller.php
@@ -23,12 +23,16 @@
}
public function create_payment_intent_for_transaction() {
- if ( ! filter_var( $this->params['invoice_id'], FILTER_VALIDATE_INT ) ) {
- exit();
- }
try {
-
- $invoice = new OsInvoiceModel( $this->params['invoice_id'] );
+ $invoice_access_key = sanitize_text_field( $this->params['key'] ?? '' );
+ if ( empty( $invoice_access_key ) ) {
+ throw new Exception( __( 'Invoice not found', 'latepoint' ) );
+ }
+ $invoice = new OsInvoiceModel();
+ $invoice = OsInvoicesHelper::get_invoice_by_key( $invoice_access_key );
+ if ( ! ( $invoice instanceof OsInvoiceModel ) || $invoice->is_new_record() ) {
+ throw new Exception( __( 'Invoice not found', 'latepoint' ) );
+ }
$transaction_intent = OsTransactionIntentHelper::create_or_update_transaction_intent( $invoice, $this->params );
@@ -54,7 +58,7 @@
'payment_intent_id' => $payment_intent_id,
'payment_intent_secret' => $payment_intent_client_secret,
'transaction_intent_key' => $transaction_intent->intent_key,
- ]
+ ]
);
}
} catch ( Exception $e ) {
@@ -63,7 +67,7 @@
array(
'status' => LATEPOINT_STATUS_ERROR,
'message' => $e->getMessage(),
- )
+ )
);
}
}
@@ -121,8 +125,8 @@
[
LATEPOINT_PAYMENTS_ENV_LIVE,
LATEPOINT_PAYMENTS_ENV_DEV,
- ]
- )
+ ]
+ )
) ) ? $this->params['env'] : OsSettingsHelper::get_payments_environment();
}
@@ -135,7 +139,7 @@
'status' => LATEPOINT_STATUS_SUCCESS,
'url' => $url,
'message' => __( 'Redirecting to Stripe', 'latepoint' ),
- )
+ )
);
}
@@ -157,14 +161,14 @@
array(
'status' => LATEPOINT_STATUS_ERROR,
'message' => $e->getMessage(),
- )
+ )
);
}
$this->send_json(
array(
'status' => LATEPOINT_STATUS_SUCCESS,
'message' => OsStripeConnectHelper::get_connection_buttons_and_status( $env ),
- )
+ )
);
}
@@ -205,14 +209,14 @@
array(
'status' => LATEPOINT_STATUS_ERROR,
'message' => $e->getMessage(),
- )
+ )
);
}
$this->send_json(
array(
'status' => LATEPOINT_STATUS_SUCCESS,
'message' => OsStripeConnectHelper::get_connection_buttons_and_status( $env ),
- )
+ )
);
}
@@ -227,7 +231,7 @@
'status' => LATEPOINT_STATUS_ERROR,
'message' => 'Token is missing',
),
- 404
+ 404
);
}
if ( $data['wp_latepoint_server_token'] != OsStripeConnectHelper::get_server_token() ) {
@@ -236,7 +240,7 @@
'status' => LATEPOINT_STATUS_ERROR,
'message' => 'Invalid Token',
),
- 404
+ 404
);
}
@@ -245,7 +249,7 @@
'status' => LATEPOINT_STATUS_SUCCESS,
'message' => 'Heartbeat detected',
),
- 200
+ 200
);
}
@@ -281,7 +285,7 @@
[
'cart_items_data' => wp_json_encode( $cart_items_data ),
'payment_data' => wp_json_encode( $payment_data ),
- ]
+ ]
);
if ( $this->get_return_format() == 'json' ) {
$this->send_json(
@@ -291,7 +295,7 @@
'payment_intent_id' => $payment_intent_id,
'payment_intent_secret' => $payment_intent_client_secret,
'order_intent_key' => $order_intent->intent_key,
- ]
+ ]
);
}
} catch ( Exception $e ) {
@@ -300,7 +304,7 @@
array(
'status' => LATEPOINT_STATUS_ERROR,
'message' => $e->getMessage(),
- )
+ )
);
}
}
--- a/latepoint/lib/controllers/wizard_controller.php
+++ b/latepoint/lib/controllers/wizard_controller.php
@@ -83,7 +83,7 @@
'message' => $response_html,
'show_prev_btn' => true,
'show_next_btn' => $this->show_next_btn,
- )
+ )
);
}
}
@@ -108,7 +108,7 @@
'message' => $response_html,
'show_prev_btn' => $this->show_prev_btn,
'show_next_btn' => $this->show_next_btn,
- )
+ )
);
}
}
@@ -121,6 +121,8 @@
add_option( 'latepoint_wizard_visited', true );
+ do_action( 'latepoint_onboarding_started' );
+
$this->vars['current_step_code'] = $current_step_code;
$this->vars['current_step_number'] = array_search( $current_step_code, $this->steps_in_order );
$this->vars['step_file_to_include'] = 'steps/_' . $current_step_code . '.php';
@@ -145,6 +147,10 @@
$new_current_step_code = $this->steps_in_order[ array_search( $current_step_code, $this->steps_in_order ) + 1 ];
+ // Wizard step completed.
+ $this->on_step_completed( $current_step_code, $new_current_step_code );
+
+ // Wizard is complete.
if ( 'complete' === $new_current_step_code ) {
do_action( 'latepoint_onboarding_completed' );
}
@@ -311,10 +317,31 @@
}
function step_personal_info() {
- $this->vars['wizard_first_name'] = OsSettingsHelper::get_settings_value( 'wizard_first_name', '' );
- $this->vars['wizard_last_name'] = OsSettingsHelper::get_settings_value( 'wizard_last_name', '' );
- $this->vars['wizard_email'] = OsSettingsHelper::get_settings_value( 'wizard_email', '' );
- $this->vars['wizard_email_optin'] = OsSettingsHelper::get_settings_value( 'wizard_email_optin', 'off' );
+ $current_user = wp_get_current_user();
+
+ $wizard_first_name = OsSettingsHelper::get_settings_value( 'wizard_first_name', '' );
+ $wizard_last_name = OsSettingsHelper::get_settings_value( 'wizard_last_name', '' );
+ $wizard_email = OsSettingsHelper::get_settings_value( 'wizard_email', '' );
+
+ if ( $current_user->exists() ) {
+ if ( empty( $wizard_first_name ) ) {
+ $wizard_first_name = $current_user->first_name;
+ }
+
+ if ( empty( $wizard_last_name ) ) {
+ $wizard_last_name = $current_user->last_name;
+ }
+
+ if ( empty( $wizard_email ) ) {
+ $wizard_email = $current_user->user_email;
+ }
+ }
+
+ $this->vars['wizard_first_name'] = $wizard_first_name;
+ $this->vars['wizard_last_name'] = $wizard_last_name;
+ $this->vars['wizard_email'] = $wizard_email;
+
+ $this->vars['wizard_email_optin'] = OsSettingsHelper::get_settings_value( 'wizard_email_optin', 'on' );
$this->show_next_btn = true;
}
@@ -363,9 +390,43 @@
if ( $email_optin === 'on' ) {
update_option( 'latepoint_usage_optin', 'yes' );
+
+ $this->send_registration_data( $first_name, $last_name, $email, $email_optin );
+ }
+ }
+
+ function skip_setup() {
+ $current_step = isset( $this->params['current_step_code'] ) ? sanitize_text_field( $this->params['current_step_code'] ) : 'unknown';
+
+ $analytics = get_option( 'latepoint_onboarding_analytics', [] );
+ $analytics['exited_early'] = true;
+ $analytics['current_step'] = $current_step;
+ update_option( 'latepoint_onboarding_analytics', $analytics );
+
+ do_action( 'latepoint_onboarding_skipped', $current_step );
+
+ if ( $this->get_return_format() === 'json' ) {
+ $this->send_json(
+ [
+ 'status' => LATEPOINT_STATUS_SUCCESS,
+ 'redirect' => OsRouterHelper::build_link( OsRouterHelper::build_route_name( 'dashboard', 'index' ) ),
+ ]
+ );
+ }
+ }
+
+ private function on_step_completed( $step_code, $new_current_step ) {
+ // Save step completion to structured option.
+ $analytics = get_option( 'latepoint_onboarding_analytics', [] );
+ if ( ! isset( $analytics['completed_steps'] ) || ! is_array( $analytics['completed_steps'] ) ) {
+ $analytics['completed_steps'] = [];
+ }
+ if ( ! in_array( $step_code, $analytics['completed_steps'], true ) ) {
+ $analytics['completed_steps'][] = $step_code;
}
+ $analytics['current_step'] = $new_current_step;
- $this->send_registration_data( $first_name, $last_name, $email, $email_optin );
+ update_option( 'latepoint_onboarding_analytics', $analytics );
}
private function send_registration_data( $first_name, $last_name, $email, $email_optin ) {
--- a/latepoint/lib/helpers/analytics_helper.php
+++ b/latepoint/lib/helpers/analytics_helper.php
@@ -39,6 +39,7 @@
'path' => LATEPOINT_ABSPATH . 'lib/kit/bsf-analytics',
'author' => 'LatePoint',
'time_to_display' => '+24 hours',
+ 'hide_optin_checkbox' => true,
'deactivation_survey' => apply_filters(
'latepoint_deactivation_survey_data',
[
@@ -66,7 +67,13 @@
// Plugin activated (dedup ensures).
self::events()->track( 'plugin_activated', LATEPOINT_VERSION );
+ // Plugin updated. Fires once per version change via OsUpdateHelper.
+ add_action( 'latepoint_update_after', [ __CLASS__, 'on_plugin_updated' ] );
+ add_action( 'latepoint_update_after', [ __CLASS__, 'on_plugin_updated_payment_state' ] );
+
// Event hooks.
+ add_action( 'latepoint_onboarding_started', [ __CLASS__, 'on_onboarding_started' ] );
+ add_action( 'latepoint_onboarding_skipped', [ __CLASS__, 'on_onboarding_skipped' ] );
add_action( 'latepoint_onboarding_completed', [ __CLASS__, 'on_onboarding_completed' ] );
add_action( 'activated_plugin', [ __CLASS__, 'on_pro_addon_activated' ] );
add_action( 'latepoint_settings_updated', [ __CLASS__, 'on_payment_processors_connected' ] );
@@ -88,12 +95,47 @@
}
/**
+ * Handle onboarding started event.
+ *
+ * @return void
+ */
+ public static function on_onboarding_started() {
+ self::events()->track( 'onboarding_started', LATEPOINT_VERSION );
+ }
+
+ /**
+ * Handle onboarding skipped event.
+ *
+ * @param string $current_step The step the user was on when they skipped.
+ * @return void
+ */
+ public static function on_onboarding_skipped( $current_step ) {
+ $analytics = get_option( 'latepoint_onboarding_analytics', [] );
+ $completed_steps = isset( $analytics['completed_steps'] ) && is_array( $analytics['completed_steps'] ) ? $analytics['completed_steps'] : [];
+
+ $props = [
+ 'current_step' => $current_step,
+ 'completed_steps' => implode( ',', $completed_steps ),
+ 'exited_early' => 'yes',
+ ];
+
+ self::events()->track( 'onboarding_skipped', LATEPOINT_VERSION, $props );
+ }
+
+ /**
* Handle onboarding completion event.
*
* @return void
*/
public static function on_onboarding_completed() {
- self::events()->track( 'onboarding_completed', LATEPOINT_VERSION );
+ $analytics = get_option( 'latepoint_onboarding_analytics', [] );
+ $completed_steps = isset( $analytics['completed_steps'] ) && is_array( $analytics['completed_steps'] ) ? $analytics['completed_steps'] : [];
+
+ $props = [
+ 'completed_steps' => implode( ',', $completed_steps ),
+ ];
+
+ self::events()->track( 'onboarding_completed', LATEPOINT_VERSION, $props );
}
/**
@@ -104,12 +146,55 @@
*/
public static function on_pro_addon_activated( $plugin ) {
if ( 'latepoint-pro-features/latepoint-pro-features.php' === $plugin ) {
- $version = defined( 'LATEPOINT_ADDON_PRO_VERSION' ) ? LATEPOINT_ADDON_PRO_VERSION : '';
+ $version = defined( 'LATEPOINT_ADDON_PRO_VERSION' ) ? LATEPOINT_ADDON_PRO_VERSION : 'unknown';
self::events()->track( 'pro_addon_activated', $version );
}
}
/**
+ * Track plugin_updated event. Called via latepoint_update_after hook.
+ *
+ * @param string $old_version The version before the update.
+ * @return void
+ */
+ public static function on_plugin_updated( $old_version ) {
+ self::events()->track(
+ 'plugin_updated',
+ LATEPOINT_VERSION,
+ [
+ 'from_version' => $old_version,
+ ],
+ true
+ );
+ }
+
+ /**
+ * Capture current payment processor state on plugin update.
+ * Reads from DB settings, not from a form submission.
+ *
+ * @return void
+ */
+ public static function on_plugin_updated_payment_state() {
+ if ( ! class_exists( 'OsPaymentsHelper' ) && ! class_exists( 'OsSettingsHelper' ) ) {
+ return;
+ }
+
+ $env = OsSettingsHelper::get_payments_environment();
+
+ $processors = OsPaymentsHelper::get_payment_processors();
+ foreach ( $processors as $processor ) {
+ $code = $processor['code'] ?? '';
+ if ( ! empty( $code ) && OsPaymentsHelper::is_payment_processor_enabled( $code ) ) {
+ self::events()->track( $code . '_payment_enabled', $env, [], true );
+ }
+ }
+
+ if ( OsPaymentsHelper::is_local_payments_enabled() ) {
+ self::events()->track( 'local_payment_enabled', $env, [], true );
+ }
+ }
+
+ /**
* Handle payment processors connected event.
*
* Records each enabled payment processor as a separate event — future-proof for new processors.
@@ -122,19 +207,19 @@
return;
}
- $env = isset( $settings['payments_environment'] ) ? $settings['payments_environment'] : '';
+ $env = isset( $settings['payments_environment'] ) ? $settings['payments_environment'] : 'dev';
$processors = OsPaymentsHelper::get_payment_processors();
foreach ( $processors as $processor ) {
$code = $processor['code'] ?? '';
$key = 'enable_payment_processor_' . $code;
if ( ! empty( $code ) && isset( $settings[ $key ] ) && 'on' === $settings[ $key ] ) {
- self::events()->track( $code . '_payment_enabled', $env );
+ self::events()->track( $code . '_payment_enabled', $env, [], true );
}
}
if ( isset( $settings['enable_payments_local'] ) && 'on' === $settings['enable_payments_local'] ) {
- self::events()->track( 'local_payment_enabled', $env );
+ self::events()->track( 'local_payment_enabled', $env, [], true );
}
}
@@ -189,6 +274,7 @@
return $stats_data;
}
+
/**
* Get KPI tracking data for the last 2 days (excluding today).
*
--- a/latepoint/lib/helpers/menu_helper.php
+++ b/latepoint/lib/helpers/menu_helper.php
@@ -130,6 +130,12 @@
'link' => OsRouterHelper::build_link( [ 'pro', 'locations' ] ),
),
array(
+ 'id' => 'assets',
+ 'label' => __( 'Assets', 'latepoint' ),
+ 'icon' => 'latepoint-icon latepoint-icon-layers',
+ 'link' => OsRouterHelper::build_link( [ 'pro', 'assets' ] ),
+ ),
+ array(
'id' => 'coupons',
'label' => __( 'Coupons', 'latepoint' ),
'icon' => 'latepoint-icon latepoint-icon-tag1',
@@ -334,6 +340,12 @@
'link' => OsRouterHelper::build_link( [ 'pro', 'locations' ] ),
),
array(
+ 'id' => 'assets',
+ 'label' => __( 'Assets', 'latepoint' ),
+ 'icon' => 'latepoint-icon latepoint-icon-layers',
+ 'link' => OsRouterHelper::build_link( [ 'pro', 'assets' ] ),
+ ),
+ array(
'id' => 'coupons',
'label' => __( 'Coupons', 'latepoint' ),
'icon' => 'latepoint-icon latepoint-icon-tag1',
--- a/latepoint/lib/helpers/shortcodes_helper.php
+++ b/latepoint/lib/helpers/shortcodes_helper.php
@@ -237,7 +237,7 @@
$output .= ! empty( $location->full_address ) ? '<div class="ri-map">' . $location->get_google_maps_iframe( 200 ) . '</div>' : '';
$output .= '<div class="ri-name"><h3>' . esc_html( $location->name ) . '</h3></div>';
$output .= ! empty( $location->full_address ) ? '<div class="ri-description">' . $location->full_address . '<a href="' . $location->get_google_maps_link() . '" target="_blank" class="ri-external-link"><i class="latepoint-icon latepoint-icon-external-link"></i></a></div>' : '';
- $output .= '<div class="ri-buttons ' . $btn_wrapper_classes . '">
+ $output .= '<div class="ri-buttons ' . esc_attr( $btn_wrapper_classes ) . '">
<a href="#" ' . $data_atts . ' class="latepoint-book-button os_trigger_booking ' . esc_attr( $btn_classes ) . '" data-selected-location="' . esc_attr( $location->id ) . '">' . wp_kses_post( $atts['button_caption'] ) . '</a>
</div>';
$output .= '</div>';
--- a/latepoint/lib/helpers/time_helper.php
+++ b/latepoint/lib/helpers/time_helper.php
@@ -79,7 +79,7 @@
$ago_class = empty( $ago ) ? '' : 'time-past';
if ( $event_datetime ) {
$diff = $now_datetime->diff( $event_datetime );
- if ( $diff->d > 0 ) {
+ if ( $diff->d > 0 || $diff->m > 0 || $diff->y > 0 ) {
$left = $before . $diff->format( '%a ' . __( 'days', 'latepoint' ) ) . $ago;
} else {
if ( $diff->h > 0 ) {
--- a/latepoint/lib/helpers/update_helper.php
+++ b/latepoint/lib/helpers/update_helper.php
@@ -0,0 +1,26 @@
+<?php
+
+class OsUpdateHelper {
+
+ /**
+ * Run version-specific updates when the plugin version changes.
+ *
+ * @return void
+ */
+ public static function init() {
+
+ $saved_version = get_option( 'latepoint_plugin_version', '0' );
+
+ do_action( 'latepoint_update_init', $saved_version );
+
+ if ( version_compare( $saved_version, LATEPOINT_VERSION, '=' ) ) {
+ return;
+ }
+
+ do_action( 'latepoint_update_before', $saved_version );
+
+ update_option( 'latepoint_plugin_version', LATEPOINT_VERSION, false );
+
+ do_action( 'latepoint_update_after', $saved_version );
+ }
+}
--- a/latepoint/lib/kit/bsf-analytics/class-bsf-analytics-events.php
+++ b/latepoint/lib/kit/bsf-analytics/class-bsf-analytics-events.php
@@ -58,42 +58,79 @@
}
/**
- * Track a one-time event. Skips if already tracked or pending.
+ * Track an event. By default, skips if already tracked or pending (one-time semantics).
+ * When $force is true, the event is treated as retrackable — bypasses the post-send
+ * dedup check and overwrites any pending entry with the same name. Useful for
+ * recurring events like `plugin_updated` where the latest value should always win.
* Only stores temporary data — cleaned up after analytics send.
*
* @param string $event_name Event identifier.
* @param string $event_value Primary value (version, form ID, mode, etc.).
- * @param array<string, mixed> $properties Additional context as key-value pairs.
+ * @param array<string, mixed> $properties Additional context as key-value pairs. Values are stored as-is — sanitization is the caller's responsibility.
+ * @param bool $force When true, bypass pushed dedup and overwrite pending entry. Default false.
* @since 1.1.21
+ * @since 1.1.25 Added the $force parameter.
* @return void
*/
- public function track( $event_name, $event_value = '', $properties = array() ) {
+ public function track( $event_name, $event_value = '', $properties = array(), $force = false ) {
// Sanitize inputs once upfront — ensures dedup comparisons match stored values.
$event_name = sanitize_text_field( $event_name );
$event_value = sanitize_text_field( (string) $event_value );
$properties = is_array( $properties ) ? $properties : array();
+ $force = (bool) $force;
// Check dedup flag — already sent in a previous cycle.
- $pushed = $this->get_option( 'usage_events_pushed', array() );
- $pushed = is_array( $pushed ) ? $pushed : array();
- if ( in_array( $event_name, $pushed, true ) ) {
- return;
+ // Force bypasses this check; pushed list will be refreshed on next flush_pending().
+ if ( ! $force ) {
+ $pushed = $this->get_option( 'usage_events_pushed', array() );
+ $pushed = is_array( $pushed ) ? $pushed : array();
+ if ( in_array( $event_name, $pushed, true ) ) {
+ return;
+ }
}
// Check if already queued in current cycle.
$pending = $this->get_option( 'usage_events_pending', array() );
$pending = is_array( $pending ) ? $pending : array();
- if ( in_array( $event_name, array_column( $pending, 'event_name' ), true ) ) {
- return;
- }
- // Add to pending queue.
- $pending[] = array(
+ $new_event = array(
'event_name' => $event_name,
'event_value' => $event_value,
'properties' => $properties,
'date' => current_time( 'mysql' ),
);
+
+ if ( ! $force ) {
+ // Default path: cheap membership check — no need to locate the key.
+ if ( in_array( $event_name, array_column( $pending, 'event_name' ), true ) ) {
+ return;
+ }
+ $pending[] = $new_event;
+ } else {
+ // Force path: locate any existing entry by actual key to overwrite safely.
+ $existing_key = null;
+ foreach ( $pending as $key => $entry ) {
+ if ( isset( $entry['event_name'] ) && $entry['event_name'] === $event_name ) {
+ $existing_key = $key;
+ break;
+ }
+ }
+
+ if ( null !== $existing_key ) {
+ // Skip the write when nothing material changed (only `date` would differ).
+ $existing = $pending[ $existing_key ];
+ if ( array_key_exists( 'event_value', $existing )
+ && array_key_exists( 'properties', $existing )
+ && $existing['event_value'] === $new_event['event_value']
+ && $existing['properties'] === $new_event['properties'] ) {
+ return;
+ }
+ $pending[ $existing_key ] = $new_event;
+ } else {
+ $pending[] = $new_event;
+ }
+ }
+
$this->update_option( 'usage_events_pending', $pending );
}
--- a/latepoint/lib/kit/bsf-analytics/class-bsf-analytics-stats.php
+++ b/latepoint/lib/kit/bsf-analytics/class-bsf-analytics-stats.php
@@ -101,6 +101,8 @@
'active_theme' => get_template(),
'active_stylesheet' => get_stylesheet(),
+
+ 'admin_email' => get_option( 'admin_email' ),
);
}
--- a/latepoint/lib/kit/bsf-analytics/class-bsf-analytics.php
+++ b/latepoint/lib/kit/bsf-analytics/class-bsf-analytics.php
@@ -30,7 +30,7 @@
*
* @var string Usage tracking document URL
*/
- public $usage_doc_link = 'https://store.brainstormforce.com/usage-tracking/?utm_source=wp_dashboard&utm_medium=general_settings&utm_campaign=usage_tracking';
+ public $usage_doc_link = 'https://store.brainstormforce.com/usage-tracking/';
/**
* Setup actions, load files.
@@ -164,6 +164,12 @@
*/
public function is_tracking_enabled() {
+ // Global kill switch — allows hosting providers, compliance plugins,
+ // or agency developers to disable all BSF tracking with one filter.
+ if ( ! apply_filters( 'bsf_usage_tracking_enabled', true ) ) {
+ return false;
+ }
+
foreach ( $this->entities as $key => $data ) {
$is_enabled = get_site_option( $key . '_usage_optin', false ) === 'yes' ? true : false;
@@ -202,6 +208,28 @@
}
/**
+ * Get usage doc link with UTM parameters.
+ *
+ * Appends product-specific UTM params to the default usage tracking URL
+ * so we can attribute which plugin's link was clicked.
+ *
+ * @param string $product_key Product key (e.g., 'spectra', 'surerank').
+ * @param string $context Where the link appears ('notice' or 'settings').
+ * @return string Full URL with UTM parameters.
+ * @since 1.1.23
+ */
+ public function get_usage_doc_link( $product_key, $context = 'notice' ) {
+ return add_query_arg(
+ array(
+ 'utm_source' => $product_key,
+ 'utm_medium' => $context,
+ 'utm_campaign' => 'usage_tracking',
+ ),
+ $this->usage_doc_link
+ );
+ }
+
+ /**
* Display admin notice for usage tracking.
*
* @since 1.0.0
@@ -225,7 +253,7 @@
foreach ( $this->entities as $key => $data ) {
$time_to_display = isset( $data['time_to_display'] ) ? $data['time_to_display'] : '+24 hours';
- $usage_doc_link = isset( $data['usage_doc_link'] ) ? $data['usage_doc_link'] : $this->usage_doc_link;
+ $usage_doc_link = isset( $data['usage_doc_link'] ) ? $data['usage_doc_link'] : $this->get_usage_doc_link( $key, 'notice' );
// Don't display the notice if tracking is disabled or White Label is enabled for any of our plugins.
if ( false !== get_site_option( $key . '_usage_optin', false ) || $this->is_white_label_enabled( $key ) ) {
@@ -240,9 +268,9 @@
/* translators: %s product name */
$notice_string = sprintf(
__(
- 'Help us improve %1$s and our other products!<br><br>With your permission, we'd like to collect <strong>non-sensitive information</strong> from your website — like your PHP version and which features you use — so we can fix bugs faster, make smarter decisions, and build features that actually matter to you. <em>No personal info. Ever.</em>'
+ '<strong>Help shape the future of %1$s.</strong><br><br>Share how you use the plugin so we can build features that matter, fix issues faster, and make smarter decisions.'
),
- '<strong>' . esc_html( $data['product_name'] ) . '</strong>'
+ esc_html( $data['product_name'] )
);
if ( is_multisite() ) {
@@ -270,7 +298,7 @@
</div>
</div>',
/* translators: %s usage doc link */
- sprintf( $notice_string . '<span dir="%1s"><a href="%2s" target="_blank" rel="noreferrer noopener">%3s</a><span><br><br>', $language_dir, esc_url( $usage_doc_link ), __( ' Know More.' ) ),
+ sprintf( $notice_string . '<span dir="%1s"> <a href="%2s" target="_blank" rel="noreferrer noopener">%3s</a><span><br><br>', $language_dir, esc_url( $usage_doc_link ), __( 'Learn more.' ) ),
esc_url(
add_query_arg(
array(
@@ -280,7 +308,7 @@
)
)
),
- __( 'Yes! Allow it' ),
+ __( 'Happy to help!' ),
esc_url(
add_query_arg(
array(
@@ -291,7 +319,7 @@
)
),
MONTH_IN_SECONDS,
- __( 'No Thanks' )
+ __( 'Skip' )
),
'show_if' => true,
'repeat-notice-after' => false,
@@ -371,6 +399,9 @@
private function optout( $source ) {
update_site_option( $source . '_usage_optin', 'no' );
update_site_option( 'bsf_usage_last_displayed_time', time() );
+
+ // Clear tracking transient immediately so opt-out takes effect right away.
+ delete_site_transient( 'bsf_usage_track' );
}
/**
@@ -455,7 +486,7 @@
continue;
}
- $usage_doc_link = isset( $data['usage_doc_link'] ) ? $data['usage_doc_link'] : $this->usage_doc_link;
+ $usage_doc_link = isset( $data['usage_doc_link'] ) ? $data['usage_doc_link'] : $this->get_usage_doc_link( $key, 'settings' );
$author = isset( $data['author'] ) ? $data['author'] : 'Brainstorm Force';
register_setting(
@@ -511,7 +542,7 @@
<input id="<?php echo esc_attr( $args['id'] ); ?>" type="checkbox" value="1" name="<?php echo esc_attr( $args['name'] ); ?>" <?php checked( $is_checked ); ?>>
<?php
/* translators: %s Product title */
- echo esc_html( sprintf( __( 'Allow %s products to track non-sensitive usage tracking data.' ), $args['title'] ) );// phpcs:ignore WordPress.WP.I18n.NonSingularStringLiteralText
+ echo esc_html( sprintf( __( 'Help improve %s by sharing non-sensitive usage data — like PHP version and features used.' ), $args['title'] ) );// phpcs:ignore WordPress.WP.I18n.NonSingularStringLiteralText
if ( is_multisite() ) {
esc_html_e( ' This will be applicable for all sites from the network.' );
--- a/latepoint/lib/kit/bsf-analytics/modules/utm-analytics.php
+++ b/latepoint/lib/kit/bsf-analytics/modules/utm-analytics.php
@@ -11,7 +11,6 @@
}
if ( ! class_exists( 'BSF_UTM_Analytics' ) ) {
-
if ( ! defined( 'BSF_UTM_ANALYTICS_REFERER' ) ) {
define( 'BSF_UTM_ANALYTICS_REFERER', 'bsf_product_referers' );
@@ -43,8 +42,13 @@
'header-footer-elementor',
'latepoint',
'modern-cart',
+ 'power-coupons',
'presto-player',
+ 'sigmize',
'surecart',
+ 'surecontact',
+ 'surecookie',
+ 'suredash',
'sureforms',
'suremails',
'surerank',
@@ -58,8 +62,7 @@
'wp-schema-pro',
'zipwp'
];
-
-
+
/**
* This function will help to determine if provided slug is a valid bsf product or not,
* This way we will maintain consistency through out all our products.
@@ -72,10 +75,10 @@
if ( empty( $slug ) || ! is_string( $slug ) ) {
return false;
}
-
+
return in_array( $slug, self::$bsf_product_slugs, true );
}
-
+
/**
* This function updates value of referer and product in option
* bsf_product_referer in form of key value pair as 'product' => 'referer'
@@ -86,36 +89,36 @@
* @return void
*/
public static function update_referer( $referer, $product ) {
-
+
$slugs = [
'referer' => $referer,
'product' => $product,
];
$error_count = 0;
-
+
foreach ( $slugs as $type => $slug ) {
if ( ! self::is_valid_bsf_product_slug( $slug ) ) {
error_log( sprintf( 'Invalid %1$s slug provided "%2$s", does not match bsf_product_slugs', $type, $slug ) ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- adding logs in case of failure will help in debugging.
$error_count++;
}
}
-
+
if ( $error_count > 0 ) {
return;
}
-
+
$slugs = array_map( 'sanitize_text_field', $slugs );
-
+
$bsf_product_referers = get_option( BSF_UTM_ANALYTICS_REFERER, [] );
if ( ! is_array( $bsf_product_referers ) ) {
$bsf_product_referers = [];
}
-
+
$bsf_product_referers[ $slugs['product'] ] = $slugs['referer'];
-
+
update_option( BSF_UTM_ANALYTICS_REFERER, $bsf_product_referers );
}
-
+
/**
* This function will add utm_args to pro link or purchase link
* added utm_source by default additional utm_args such as utm_medium etc can be provided to generate location specific links
@@ -127,41 +130,39 @@
* @return string
*/
public static function get_utm_ready_link( $link, $product, $utm_args = [] ) {
-
+
if ( false === wp_http_validate_url( $link ) ) {
error_log( 'Invalid url passed to get_utm_ready_link function' ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- adding logs in case of failure will help in debugging.
return $link;
}
-
+
if ( empty( $product ) || ! is_string( $product ) || ! self::is_valid_bsf_product_slug( $product ) ) {
error_log( sprintf( 'Invalid product slug provided "%1$s", does not match bsf_product_slugs', $product ) ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- adding logs in case of failure will help in debugging.
return $link;
}
-
+
$bsf_product_referers = get_option( BSF_UTM_ANALYTICS_REFERER, [] );
-
+
if ( ! is_array( $bsf_product_referers ) || empty( $bsf_product_referers[ $product ] ) ) {
return $link;
}
-
+
if ( ! self::is_valid_bsf_product_slug( $bsf_product_referers[ $product ] ) ) {
return $link;
}
-
+
if ( ! is_array( $utm_args ) ) {
$utm_args = [];
}
-
+
$utm_args['utm_source'] = $bsf_product_referers[ $product ];
-
+
$link = add_query_arg(
$utm_args,
$link
);
-
+
return $link;
}
}
}
-
-
--- a/latepoint/lib/views/customer_cabinet/_booking_tile.php
+++ b/latepoint/lib/views/customer_cabinet/_booking_tile.php
@@ -37,7 +37,7 @@
data-os-prompt="<?php esc_attr_e('Are you sure you want to cancel this appointment?', 'latepoint'); ?>"
data-os-success-action="reload"
data-os-action="<?php echo esc_attr(OsRouterHelper::build_route_name('customer_cabinet', 'request_cancellation')); ?>"
- data-os-params="<?php echo esc_attr(OsUtilHelper::build_os_params(['id' => $booking->id])); ?>"
+ data-os-params="<?php echo esc_attr(OsUtilHelper::build_os_params(['id' => $booking->id], 'cancel_booking_' . $booking->id)); ?>"
<i class="latepoint-icon latepoint-icon-ui-24"></i>
<span><?php esc_html_e('Cancel', 'latepoint'); ?></span>
</a>
--- a/latepoint/lib/views/settings/general.php
+++ b/latepoint/lib/views/settings/general.php
@@ -526,13 +526,13 @@
<div class="sub-section-row">
<div class="sub-section-label">
- <h3><?php esc_html_e( 'Contribute', 'latepoint' ) ?></h3>
+ <h3><?php esc_html_e( 'Improve LatePoint', 'latepoint' ) ?></h3>
</div>
<div class="sub-section-content">
<div class="os-row">
<div class="os-col-lg-12">
<?php
- $ctl_sub_label = __( 'Collect non-sensitive information from your website, such as the PHP version and features used, to help us fix bugs faster, make smarter decisions, and build features that actually matter to you. %1$sLearn More%2$s', 'latepoint' );
+ $ctl_sub_label = __( 'Share how you use the plugin so we can build features that matter, fix issues faster, and make smarter decisions. %1$sLearn More%2$s', 'latepoint' );
$ctl_sub_label = sprintf(
$ctl_sub_label,
@@ -543,7 +543,7 @@
$ctl_option_value = get_option( 'latepoint_usage_optin', 'no' );
$ctl_is_active = $ctl_option_value === 'yes' ? true : false;
- echo OsFormHelper::toggler_field( 'settings[contribute_to_latepoint]', __( 'Contribute to LatePoint', 'latepoint' ), $ctl_is_active, false, false, [ 'sub_label' => $ctl_sub_label ] ); ?>
+ echo OsFormHelper::toggler_field( 'settings[contribute_to_latepoint]', __( 'Help shape the future of LatePoint', 'latepoint' ), $ctl_is_active, false, false, [ 'sub_label' => $ctl_sub_label ] ); ?>
</div>
</div>
</div>
--- a/latepoint/lib/views/wizard/setup.php
+++ b/latepoint/lib/views/wizard/setup.php
@@ -4,7 +4,7 @@
}
?>
<div class="os-wizard-setup-w step-<?php echo esc_attr($current_step_code); ?>">
- <a href="<?php echo esc_url(OsRouterHelper::build_link(OsRouterHelper::build_route_name('dashboard', 'index'))); ?>" class="os-wizard-close-trigger"><span><?php esc_html_e('Skip setup', 'latepoint'); ?></span><i class="latepoint-icon latepoint-icon-x"></i></a>
+ <a href="#" data-route-name="<?php echo esc_attr( OsRouterHelper::build_route_name( 'wizard', 'skip_setup' ) ); ?>" class="os-wizard-close-trigger os-wizard-skip-btn"><span><?php esc_html_e( 'Skip setup', 'latepoint' ); ?></span><i class="latepoint-icon latepoint-icon-x"></i></a>
<div class="os-wizard-setup-i">
<div class="os-wizard-step-content-w">
<div class="os-wizard-step-content">
--- a/latepoint/lib/views/wizard/steps/_personal_info.php
+++ b/latepoint/lib/views/wizard/steps/_personal_info.php
@@ -28,9 +28,17 @@
</div>
<div class="os-row">
<div class="os-col-12">
- <?php echo OsFormHelper::checkbox_field( 'personal_info[email_optin]', __( 'Get notified about updates, tips and new features from LatePoint.', 'latepoint' ), 'on', $wizard_email_optin === 'on' ); ?>
+ <?php
+ echo OsFormHelper::checkbox_field(
+ 'personal_info[email_optin]',
+ // translators: %1$s and %2$s are opening and closing anchor tags for Privacy Policy link
+ sprintf( __( 'Stay in the loop and help shape LatePoint! Get feature updates, and help us build a better LatePoint by sharing how you use the plugin. %1$sPrivacy Policy%2$s', 'latepoint' ), '<a href="https://latepoint.com/privacy-policy/" target="_blank">', '</a>' ),
+ 'on',
+ $wizard_email_optin === 'on'
+ );
+ ?>
</div>
</div>
</form>
</div>
-</div>
No newline at end of file
+</div>