Index: /mindlytics-order-notifications-for-woocommerce/tags/1.0.0/assets/admin.css
===================================================================
--- /mindlytics-order-notifications-for-woocommerce/tags/1.0.0/assets/admin.css	(revision 3794792)
+++ /mindlytics-order-notifications-for-woocommerce/tags/1.0.0/assets/admin.css	(revision 3794792)
@@ -0,0 +1,89 @@
+/* Mindlytics WooCommerce admin styles */
+
+.mindlytics-settings-wrap {
+	max-width: 760px;
+}
+
+.mindlytics-settings-wrap h1 {
+	margin-bottom: 16px;
+}
+
+.mindlytics-connect-card {
+	background: #fff;
+	border: 1px solid #c3c4c7;
+	border-radius: 4px;
+	padding: 20px 24px;
+	margin-bottom: 16px;
+}
+
+.mindlytics-connect-card h2 {
+	margin-top: 0;
+	font-size: 14px;
+	font-weight: 600;
+}
+
+.mindlytics-status-row {
+	display: flex;
+	align-items: center;
+	gap: 10px;
+	margin-bottom: 12px;
+}
+
+.mindlytics-status-badge {
+	display: inline-flex;
+	align-items: center;
+	padding: 3px 10px;
+	border-radius: 12px;
+	font-size: 12px;
+	font-weight: 600;
+	line-height: 1.4;
+	color: #fff;
+}
+
+.mindlytics-status-connected {
+	background-color: #00a32a;
+}
+
+.mindlytics-status-disconnected {
+	background-color: #646970;
+}
+
+.mindlytics-meta {
+	color: #646970;
+	font-size: 13px;
+	margin-top: 0;
+}
+
+.mindlytics-connect-card p {
+	margin-bottom: 12px;
+}
+
+.mindlytics-connect-card p:last-child {
+	margin-bottom: 0;
+}
+
+.mindlytics-connect-card details summary {
+	cursor: pointer;
+	font-weight: 600;
+	padding: 4px 0;
+	user-select: none;
+}
+
+.mindlytics-details-body {
+	margin-top: 12px;
+	padding-top: 12px;
+	border-top: 1px solid #f0f0f1;
+}
+
+.mindlytics-details-body ol {
+	margin-left: 20px;
+}
+
+.mindlytics-details-body li {
+	margin-bottom: 6px;
+}
+
+#mindlytics-notice {
+	margin: 0 0 16px;
+	padding: 10px 14px;
+}
Index: /mindlytics-order-notifications-for-woocommerce/tags/1.0.0/assets/admin.js
===================================================================
--- /mindlytics-order-notifications-for-woocommerce/tags/1.0.0/assets/admin.js	(revision 3794792)
+++ /mindlytics-order-notifications-for-woocommerce/tags/1.0.0/assets/admin.js	(revision 3794792)
@@ -0,0 +1,142 @@
+/* global mindlyticsAdmin */
+(function () {
+	'use strict';
+
+	// ── Connect ───────────────────────────────────────────────────────────────
+
+	function openConnectPopup() {
+		fetch( mindlyticsAdmin.ajaxUrl, {
+			method: 'POST',
+			headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
+			body: new URLSearchParams( {
+				action: 'mindlytics_open_connect',
+				nonce:  mindlyticsAdmin.nonce,
+			} ),
+		} )
+			.then( function ( r ) { return r.json(); } )
+			.then( function ( data ) {
+				if ( ! data.success ) {
+					showError( data.data && data.data.message ? data.data.message : 'An error occurred.' );
+					return;
+				}
+
+				var popup = window.open(
+					data.data.popup_url,
+					'mindlytics_connect',
+					'width=640,height=720,scrollbars=yes,resizable=yes'
+				);
+
+				window.addEventListener( 'message', function onMessage( event ) {
+					// Only accept messages from the Mindlytics app origin.
+					// appOrigin is set by PHP to match MINDLYTICS_APP_URL (dev or prod).
+				if ( event.origin !== mindlyticsAdmin.appOrigin ) {
+						return;
+					}
+					if ( ! event.data ) { return; }
+					if ( event.data.type !== 'MINDLYTICS_CONNECTED' && event.data.type !== 'MINDLYTICS_CONNECT_ERROR' ) {
+						return;
+					}
+
+					window.removeEventListener( 'message', onMessage );
+					if ( popup && ! popup.closed ) { popup.close(); }
+
+					if ( event.data.type === 'MINDLYTICS_CONNECTED' ) {
+						window.location.reload();
+					} else {
+						showError( event.data.error || 'Connection failed. Please try again.' );
+					}
+				} );
+			} )
+			.catch( function ( err ) {
+				showError( err.message || 'Failed to open connect window.' );
+			} );
+	}
+
+	// ── Disconnect ────────────────────────────────────────────────────────────
+
+	function disconnect() {
+		if ( ! window.confirm( 'Disconnect this store from Mindlytics? Abandoned cart tracking will stop immediately.' ) ) {
+			return;
+		}
+
+		fetch( mindlyticsAdmin.ajaxUrl, {
+			method: 'POST',
+			headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
+			body: new URLSearchParams( {
+				action: 'mindlytics_disconnect',
+				nonce:  mindlyticsAdmin.nonce,
+			} ),
+		} )
+			.then( function ( r ) { return r.json(); } )
+			.then( function ( data ) {
+				if ( data.success ) {
+					window.location.reload();
+				} else {
+					showError( data.data && data.data.message ? data.data.message : 'Disconnect failed.' );
+				}
+			} )
+			.catch( function ( err ) {
+				showError( err.message || 'Disconnect failed.' );
+			} );
+	}
+
+	// ── Save settings ─────────────────────────────────────────────────────────
+
+	function saveSettings( event ) {
+		event.preventDefault();
+		var form    = event.currentTarget;
+		var input   = form.querySelector( '[name="mindlytics_timeout"]' );
+		var timeout = input ? parseInt( input.value, 10 ) : 60;
+
+		fetch( mindlyticsAdmin.ajaxUrl, {
+			method: 'POST',
+			headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
+			body: new URLSearchParams( {
+				action:             'mindlytics_save_settings',
+				nonce:              mindlyticsAdmin.nonce,
+				mindlytics_timeout: timeout,
+			} ),
+		} )
+			.then( function ( r ) { return r.json(); } )
+			.then( function ( data ) {
+				if ( data.success ) {
+					showSuccess( 'Settings saved.' );
+				} else {
+					showError( data.data && data.data.message ? data.data.message : 'Failed to save settings.' );
+				}
+			} )
+			.catch( function ( err ) {
+				showError( err.message || 'Failed to save settings.' );
+			} );
+	}
+
+	// ── UI helpers ────────────────────────────────────────────────────────────
+
+	function showError( msg ) {
+		var el = document.getElementById( 'mindlytics-notice' );
+		if ( ! el ) { return; }
+		el.textContent = msg;
+		el.className   = 'notice notice-error';
+		el.removeAttribute( 'hidden' );
+	}
+
+	function showSuccess( msg ) {
+		var el = document.getElementById( 'mindlytics-notice' );
+		if ( ! el ) { return; }
+		el.textContent = msg;
+		el.className   = 'notice notice-success';
+		el.removeAttribute( 'hidden' );
+	}
+
+	// ── Bootstrap ─────────────────────────────────────────────────────────────
+
+	document.addEventListener( 'DOMContentLoaded', function () {
+		var connectBtn    = document.getElementById( 'mindlytics-connect-btn' );
+		var disconnectBtn = document.getElementById( 'mindlytics-disconnect-btn' );
+		var settingsForm  = document.getElementById( 'mindlytics-settings-form' );
+
+		if ( connectBtn )    { connectBtn.addEventListener( 'click', openConnectPopup ); }
+		if ( disconnectBtn ) { disconnectBtn.addEventListener( 'click', disconnect ); }
+		if ( settingsForm )  { settingsForm.addEventListener( 'submit', saveSettings ); }
+	} );
+}());
Index: /mindlytics-order-notifications-for-woocommerce/tags/1.0.0/includes/class-mindlytics-abandoned-cart.php
===================================================================
--- /mindlytics-order-notifications-for-woocommerce/tags/1.0.0/includes/class-mindlytics-abandoned-cart.php	(revision 3794792)
+++ /mindlytics-order-notifications-for-woocommerce/tags/1.0.0/includes/class-mindlytics-abandoned-cart.php	(revision 3794792)
@@ -0,0 +1,283 @@
+<?php
+if ( ! defined( 'ABSPATH' ) ) {
+	exit;
+}
+
+/**
+ * Detects abandoned carts and sends webhook events to the Mindlytics backend.
+ */
+class Mindlytics_Abandoned_Cart {
+
+	const CRON_HOOK     = 'mindlytics_check_abandoned_carts';
+	const CRON_INTERVAL = 'mindlytics_five_minutes';
+	const CART_PREFIX   = 'mindlytics_cart_';
+	const INDEX_KEY     = 'mindlytics_cart_index';
+
+	/**
+	 * Constructor — registers hooks.
+	 */
+	public function __construct() {
+		add_filter( 'cron_schedules',                                  array( $this, 'handle_cron_schedules' ) );
+		add_action( 'woocommerce_before_checkout_form',                array( $this, 'handle_checkout_form_shown' ) );
+		add_action( 'woocommerce_checkout_order_processed',            array( $this, 'handle_order_processed' ), 10, 3 );
+		add_action( self::CRON_HOOK,                                   array( $this, 'handle_cron_check_abandoned_carts' ) );
+	}
+
+	/**
+	 * Schedule the abandoned-cart cron event if not already scheduled.
+	 *
+	 * @since 1.0.0
+	 */
+	public static function schedule_cron(): void {
+		if ( ! wp_next_scheduled( self::CRON_HOOK ) ) {
+			wp_schedule_event( time(), self::CRON_INTERVAL, self::CRON_HOOK );
+		}
+	}
+
+	/**
+	 * Register the five-minute cron interval.
+	 *
+	 * @since 1.0.0
+	 * @internal
+	 * @param array $schedules Existing cron schedules.
+	 * @return array
+	 */
+	public function handle_cron_schedules( array $schedules ): array {
+		if ( ! isset( $schedules[ self::CRON_INTERVAL ] ) ) {
+			$schedules[ self::CRON_INTERVAL ] = array(
+				'interval' => 5 * MINUTE_IN_SECONDS,
+				'display'  => __( 'Every 5 minutes', 'mindlytics-order-notifications-for-woocommerce' ),
+			);
+		}
+		return $schedules;
+	}
+
+	/**
+	 * Capture a cart snapshot when the checkout page is displayed.
+	 *
+	 * Only captures if the cart is non-empty and a session ID is available.
+	 * The abandonment clock is not reset on subsequent page loads if the cart
+	 * contents (cart_hash) have not changed.
+	 *
+	 * @since 1.0.0
+	 * @internal
+	 */
+	public function handle_checkout_form_shown(): void {
+		if ( ! WC()->cart || WC()->cart->is_empty() ) {
+			return;
+		}
+
+		$session_id = WC()->session ? WC()->session->get_customer_id() : null;
+		if ( empty( $session_id ) ) {
+			return;
+		}
+
+		$cart_hash  = WC()->cart->get_cart_hash();
+		$option_key = self::CART_PREFIX . $session_id;
+		$existing   = get_option( $option_key );
+
+		// Don't reset the abandonment clock if cart contents haven't changed.
+		if ( is_array( $existing ) && false === $existing['fired'] && isset( $existing['cart_hash'] ) && $existing['cart_hash'] === $cart_hash ) {
+			return;
+		}
+
+		$customer_data = $this->get_customer_data();
+
+		$snapshot = array(
+			'session_id'  => sanitize_text_field( $session_id ),
+			'cart_hash'   => sanitize_text_field( $cart_hash ),
+			'cart_total'  => wc_format_decimal( WC()->cart->get_total( 'edit' ) ),
+			'cart_items'  => $this->serialize_cart_items( WC()->cart->get_cart() ),
+			'phone'       => sanitize_text_field( $customer_data['phone'] ),
+			'email'       => sanitize_email( $customer_data['email'] ),
+			'first_name'  => sanitize_text_field( $customer_data['first_name'] ),
+			'captured_at' => gmdate( 'c' ),
+			'fired'       => false,
+		);
+
+		update_option( $option_key, $snapshot, false );
+		$this->add_to_cart_index( $session_id );
+	}
+
+	/**
+	 * Mark the cart as completed when an order is processed.
+	 *
+	 * @since 1.0.0
+	 * @internal
+	 * @param int      $order_id    The new order ID.
+	 * @param array    $posted_data Posted checkout data.
+	 * @param WC_Order $order       The created order object.
+	 */
+	public function handle_order_processed( int $order_id, array $posted_data, WC_Order $order ): void {
+		$session_id = WC()->session ? WC()->session->get_customer_id() : null;
+		if ( empty( $session_id ) ) {
+			return;
+		}
+
+		$option_key = self::CART_PREFIX . $session_id;
+		$snapshot   = get_option( $option_key );
+		if ( ! is_array( $snapshot ) ) {
+			return;
+		}
+
+		$snapshot['fired'] = true;
+		update_option( $option_key, $snapshot, false );
+	}
+
+	/**
+	 * WP Cron callback: find and fire webhooks for abandoned carts.
+	 *
+	 * Uses wp_prime_option_caches() (WP 6.4+) to batch-load all cart snapshots
+	 * before iterating, reducing per-cart DB queries to zero.
+	 *
+	 * @since 1.0.0
+	 * @internal
+	 */
+	public function handle_cron_check_abandoned_carts(): void {
+		$shop_id = get_option( 'mindlytics_shop_id' );
+		$secret  = get_option( 'mindlytics_webhook_secret' );
+
+		if ( empty( $shop_id ) || empty( $secret ) ) {
+			return;
+		}
+
+		$index = $this->get_cart_index();
+		if ( empty( $index ) ) {
+			return;
+		}
+
+		$option_keys = array_map(
+			static fn( string $sid ): string => self::CART_PREFIX . $sid,
+			$index
+		);
+
+		// Prime object cache for all cart options in one SQL query.
+		wp_prime_option_caches( $option_keys );
+
+		$timeout_seconds = absint( get_option( 'mindlytics_abandoned_cart_timeout', 60 ) ) * MINUTE_IN_SECONDS;
+		$now             = time();
+		$cleaned_index   = $index;
+
+		foreach ( $index as $session_id ) {
+			$option_key = self::CART_PREFIX . $session_id;
+			$snapshot   = get_option( $option_key );
+
+			if ( ! is_array( $snapshot ) ) {
+				$cleaned_index = array_diff( $cleaned_index, array( $session_id ) );
+				continue;
+			}
+
+			if ( true === $snapshot['fired'] ) {
+				delete_option( $option_key );
+				$cleaned_index = array_diff( $cleaned_index, array( $session_id ) );
+				continue;
+			}
+
+			$captured_at = strtotime( $snapshot['captured_at'] ?? '' );
+			if ( false === $captured_at || ( $now - $captured_at ) < $timeout_seconds ) {
+				continue;
+			}
+
+			if ( empty( $snapshot['phone'] ) ) {
+				continue;
+			}
+
+			$result = Mindlytics_Api::instance()->send_abandoned_cart( $snapshot, $shop_id, $secret );
+
+			if ( ! is_wp_error( $result ) ) {
+				$snapshot['fired'] = true;
+				update_option( $option_key, $snapshot, false );
+				delete_option( $option_key );
+				$cleaned_index = array_diff( $cleaned_index, array( $session_id ) );
+			}
+		}
+
+		update_option( self::INDEX_KEY, array_values( $cleaned_index ), false );
+	}
+
+	/**
+	 * Retrieve the current customer's phone, email, and first name.
+	 *
+	 * Reads from the WC session (works for guests mid-checkout) or from
+	 * user meta for logged-in customers.
+	 *
+	 * @since 1.0.0
+	 * @return array{phone: string, email: string, first_name: string}
+	 */
+	private function get_customer_data(): array {
+		$phone      = '';
+		$email      = '';
+		$first_name = '';
+
+		if ( is_user_logged_in() ) {
+			$user_id    = get_current_user_id();
+			$phone      = (string) get_user_meta( $user_id, 'billing_phone', true );
+			$email      = (string) get_user_meta( $user_id, 'billing_email', true );
+			$first_name = (string) get_user_meta( $user_id, 'billing_first_name', true );
+		}
+
+		if ( WC()->session ) {
+			$customer = WC()->session->get( 'customer' );
+			if ( is_array( $customer ) ) {
+				$phone      = $phone ?: ( $customer['phone'] ?? '' );
+				$email      = $email ?: ( $customer['email'] ?? '' );
+				$first_name = $first_name ?: ( $customer['first_name'] ?? '' );
+			}
+		}
+
+		return compact( 'phone', 'email', 'first_name' );
+	}
+
+	/**
+	 * Serialize cart contents into a compact array for storage.
+	 *
+	 * @since 1.0.0
+	 * @param array $cart_contents WooCommerce cart contents.
+	 * @return array
+	 */
+	private function serialize_cart_items( array $cart_contents ): array {
+		$items = array();
+		foreach ( $cart_contents as $item ) {
+			/** @var WC_Product|false $product */
+			$product = $item['data'] ?? false;
+			if ( ! $product instanceof WC_Product ) {
+				continue;
+			}
+			$image_id = $product->get_image_id();
+			$items[]  = array(
+				'product_id' => absint( $item['product_id'] ?? 0 ),
+				'name'       => sanitize_text_field( $product->get_name() ),
+				'quantity'   => absint( $item['quantity'] ?? 1 ),
+				'price'      => wc_format_decimal( $product->get_price( 'edit' ) ),
+				'sku'        => sanitize_text_field( $product->get_sku() ),
+				'image_url'  => $image_id ? esc_url_raw( (string) wp_get_attachment_url( $image_id ) ) : '',
+			);
+		}
+		return $items;
+	}
+
+	/**
+	 * Add a session ID to the cart index.
+	 *
+	 * @since 1.0.0
+	 * @param string $session_id WooCommerce session/customer ID.
+	 */
+	private function add_to_cart_index( string $session_id ): void {
+		$index = $this->get_cart_index();
+		if ( ! in_array( $session_id, $index, true ) ) {
+			$index[] = sanitize_text_field( $session_id );
+			update_option( self::INDEX_KEY, $index, false );
+		}
+	}
+
+	/**
+	 * Get the cart index array.
+	 *
+	 * @since 1.0.0
+	 * @return string[]
+	 */
+	private function get_cart_index(): array {
+		$index = get_option( self::INDEX_KEY, array() );
+		return is_array( $index ) ? $index : array();
+	}
+}
Index: /mindlytics-order-notifications-for-woocommerce/tags/1.0.0/includes/class-mindlytics-admin.php
===================================================================
--- /mindlytics-order-notifications-for-woocommerce/tags/1.0.0/includes/class-mindlytics-admin.php	(revision 3794792)
+++ /mindlytics-order-notifications-for-woocommerce/tags/1.0.0/includes/class-mindlytics-admin.php	(revision 3794792)
@@ -0,0 +1,180 @@
+<?php
+if ( ! defined( 'ABSPATH' ) ) {
+	exit;
+}
+
+/**
+ * Manages the WP admin menu, settings page, and AJAX handlers.
+ */
+class Mindlytics_Admin {
+
+	/**
+	 * Constructor — registers hooks.
+	 */
+	public function __construct() {
+		add_action( 'admin_menu',                              array( $this, 'handle_admin_menu' ) );
+		add_action( 'admin_enqueue_scripts',                   array( $this, 'handle_admin_enqueue_scripts' ) );
+		add_action( 'wp_ajax_mindlytics_open_connect',         array( $this, 'handle_ajax_open_connect' ) );
+		add_action( 'wp_ajax_mindlytics_disconnect',           array( $this, 'handle_ajax_disconnect' ) );
+		add_action( 'wp_ajax_mindlytics_save_settings',        array( $this, 'handle_ajax_save_settings' ) );
+	}
+
+	/**
+	 * Register the Mindlytics submenu under WooCommerce.
+	 *
+	 * @since 1.0.0
+	 * @internal
+	 */
+	public function handle_admin_menu(): void {
+		add_submenu_page(
+			'woocommerce',
+			__( 'Mindlytics', 'mindlytics-order-notifications-for-woocommerce' ),
+			__( 'Mindlytics', 'mindlytics-order-notifications-for-woocommerce' ),
+			'manage_woocommerce',
+			'mindlytics-settings',
+			array( $this, 'render_settings_page' )
+		);
+	}
+
+	/**
+	 * Enqueue admin scripts and styles on the Mindlytics settings page only.
+	 *
+	 * @since 1.0.0
+	 * @internal
+	 * @param string $hook_suffix Current admin page hook suffix.
+	 */
+	public function handle_admin_enqueue_scripts( string $hook_suffix ): void {
+		if ( 'woocommerce_page_mindlytics-settings' !== $hook_suffix ) {
+			return;
+		}
+
+		wp_enqueue_style(
+			'mindlytics-admin',
+			MINDLYTICS_WC_URL . 'assets/admin.css',
+			array(),
+			MINDLYTICS_WC_VERSION
+		);
+
+		wp_enqueue_script(
+			'mindlytics-admin',
+			MINDLYTICS_WC_URL . 'assets/admin.js',
+			array(),
+			MINDLYTICS_WC_VERSION,
+			true
+		);
+
+		wp_localize_script(
+			'mindlytics-admin',
+			'mindlyticsAdmin',
+			array(
+				'ajaxUrl'     => admin_url( 'admin-ajax.php' ),
+				'nonce'       => wp_create_nonce( 'mindlytics_admin' ),
+				'isConnected' => $this->is_connected(),
+				'shopName'    => esc_html( (string) get_option( 'mindlytics_shop_name', '' ) ),
+				'appOrigin'   => esc_url_raw( MINDLYTICS_APP_URL ),
+			)
+		);
+	}
+
+	/**
+	 * Render the Mindlytics settings page.
+	 *
+	 * @since 1.0.0
+	 */
+	public function render_settings_page(): void {
+		if ( ! current_user_can( 'manage_woocommerce' ) ) {
+			wp_die( esc_html__( 'You do not have sufficient permissions to access this page.', 'mindlytics-order-notifications-for-woocommerce' ) );
+		}
+		include MINDLYTICS_WC_DIR . 'includes/views/settings-page.php';
+	}
+
+	/**
+	 * AJAX: generate a connect nonce and return the popup URL.
+	 *
+	 * @since 1.0.0
+	 * @internal
+	 */
+	public function handle_ajax_open_connect(): void {
+		check_ajax_referer( 'mindlytics_admin', 'nonce' );
+
+		if ( ! current_user_can( 'manage_woocommerce' ) ) {
+			wp_send_json_error( array( 'message' => __( 'Unauthorized.', 'mindlytics-order-notifications-for-woocommerce' ) ), 403 );
+		}
+
+		$state = wp_generate_password( 32, false );
+		update_option( 'mindlytics_pending_state', $state, false );
+
+		// Allow overriding URLs via constants — useful in local dev where siteurl
+		// includes a port or is otherwise unreachable from the remote backend.
+		$callback_url = defined( 'MINDLYTICS_CALLBACK_URL' )
+			? MINDLYTICS_CALLBACK_URL
+			: rest_url( 'mindlytics/v1/connected' );
+
+		$site_url = defined( 'MINDLYTICS_SITE_URL' )
+			? MINDLYTICS_SITE_URL
+			: get_site_url();
+		$popup_url = esc_url_raw( MINDLYTICS_APP_URL . '/integrations/woocommerce/connect' )
+			. '?' . http_build_query(
+				array(
+					'site_url'     => $site_url,
+					'callback_url' => $callback_url,
+					'state'        => $state,
+				)
+			);
+
+		wp_send_json_success( array( 'popup_url' => $popup_url ) );
+	}
+
+	/**
+	 * AJAX: disconnect the store (local operation — clears wp_options only).
+	 *
+	 * @since 1.0.0
+	 * @internal
+	 */
+	public function handle_ajax_disconnect(): void {
+		check_ajax_referer( 'mindlytics_admin', 'nonce' );
+
+		if ( ! current_user_can( 'manage_woocommerce' ) ) {
+			wp_send_json_error( array( 'message' => __( 'Unauthorized.', 'mindlytics-order-notifications-for-woocommerce' ) ), 403 );
+		}
+
+		delete_option( 'mindlytics_shop_id' );
+		delete_option( 'mindlytics_webhook_secret' );
+		delete_option( 'mindlytics_connected_at' );
+		delete_option( 'mindlytics_shop_name' );
+
+		wp_send_json_success( array( 'disconnected' => true ) );
+	}
+
+	/**
+	 * AJAX: save the abandoned cart timeout setting.
+	 *
+	 * @since 1.0.0
+	 * @internal
+	 */
+	public function handle_ajax_save_settings(): void {
+		check_ajax_referer( 'mindlytics_admin', 'nonce' );
+
+		if ( ! current_user_can( 'manage_woocommerce' ) ) {
+			wp_send_json_error( array( 'message' => __( 'Unauthorized.', 'mindlytics-order-notifications-for-woocommerce' ) ), 403 );
+		}
+
+		$timeout = absint( $_POST['mindlytics_timeout'] ?? 60 );
+		$timeout = max( 30, min( 1440, $timeout ) );
+
+		update_option( 'mindlytics_abandoned_cart_timeout', $timeout );
+
+		wp_send_json_success( array( 'timeout' => $timeout ) );
+	}
+
+	/**
+	 * Determine whether the store is currently connected.
+	 *
+	 * @since 1.0.0
+	 * @return bool
+	 */
+	private function is_connected(): bool {
+		return ! empty( get_option( 'mindlytics_shop_id' ) )
+			&& ! empty( get_option( 'mindlytics_webhook_secret' ) );
+	}
+}
Index: /mindlytics-order-notifications-for-woocommerce/tags/1.0.0/includes/class-mindlytics-api.php
===================================================================
--- /mindlytics-order-notifications-for-woocommerce/tags/1.0.0/includes/class-mindlytics-api.php	(revision 3794792)
+++ /mindlytics-order-notifications-for-woocommerce/tags/1.0.0/includes/class-mindlytics-api.php	(revision 3794792)
@@ -0,0 +1,111 @@
+<?php
+if ( ! defined( 'ABSPATH' ) ) {
+	exit;
+}
+
+/**
+ * HTTP client for outbound calls to the Mindlytics backend.
+ */
+class Mindlytics_Api {
+
+	/**
+	 * Singleton instance.
+	 *
+	 * @var self|null
+	 */
+	private static ?self $_instance = null;
+
+	/**
+	 * Get the singleton instance.
+	 *
+	 * @since 1.0.0
+	 * @return self
+	 */
+	public static function instance(): self {
+		if ( null === self::$_instance ) {
+			self::$_instance = new self();
+		}
+		return self::$_instance;
+	}
+
+	/**
+	 * Send an abandoned cart webhook to the Mindlytics backend.
+	 *
+	 * Delivery ID is SHA-1 of (shop_id + cart_hash + captured_at) for backend
+	 * deduplication. Uses hex-only webhook_secret — WooCommerce runs
+	 * wp_specialchars_decode() on secrets, so hex avoids any entity-decode side-effects.
+	 *
+	 * @since 1.0.0
+	 * @param array  $snapshot Cart snapshot stored in wp_options.
+	 * @param string $shop_id  Stored shop UUID.
+	 * @param string $secret   Stored 64-char hex webhook secret.
+	 * @return array|WP_Error Decoded response body or WP_Error on failure.
+	 */
+	public function send_abandoned_cart( array $snapshot, string $shop_id, string $secret ) {
+		$delivery_id = sha1( $shop_id . ( $snapshot['cart_hash'] ?? '' ) . ( $snapshot['captured_at'] ?? '' ) );
+
+		$items = array();
+		foreach ( $snapshot['cart_items'] ?? array() as $item ) {
+			$items[] = array(
+				'name'     => $item['name'] ?? '',
+				'quantity' => $item['quantity'] ?? 1,
+				'price'    => $item['price'] ?? '0.00',
+			);
+		}
+
+		$body = wp_json_encode(
+			array(
+				'session_id'  => $snapshot['session_id'] ?? '',
+				'cart_hash'   => $snapshot['cart_hash'] ?? '',
+				'total'       => $snapshot['cart_total'] ?? '0.00',
+				'currency'    => get_woocommerce_currency(),
+				'items'       => $items,
+				'customer'    => array(
+					'phone' => $snapshot['phone'] ?? '',
+					'email' => $snapshot['email'] ?? '',
+					'first_name' => $snapshot['first_name'] ?? '',
+				),
+				'checkout_url' => wc_get_checkout_url() . '?cart_hash=' . rawurlencode( $snapshot['cart_hash'] ?? '' ),
+				'abandoned_at' => $snapshot['captured_at'] ?? '',
+				'site_url'    => get_site_url(),
+			)
+		);
+
+		$response = wp_remote_post(
+			MINDLYTICS_API_URL . '/api/integrations/woocommerce/webhooks',
+			array(
+				'headers'    => array(
+					'Content-Type'                => 'application/json',
+					'X-Mindlytics-Webhook-Secret' => $secret,
+					'X-Mindlytics-Shop-Id'        => $shop_id,
+					'X-Mindlytics-Topic'          => 'abandoned_cart',
+					'X-Mindlytics-Delivery-Id'    => $delivery_id,
+				),
+				'body'       => $body,
+				'timeout'    => 15,
+				'sslverify'  => true,
+				'user-agent' => 'Mindlytics-WooCommerce/' . MINDLYTICS_WC_VERSION,
+			)
+		);
+
+		if ( is_wp_error( $response ) ) {
+			return $response;
+		}
+
+		$status_code = wp_remote_retrieve_response_code( $response );
+
+		if ( $status_code < 200 || $status_code >= 300 ) {
+			return new WP_Error(
+				'mindlytics_api_error',
+				sprintf(
+					/* translators: %d: HTTP status code */
+					__( 'Mindlytics API returned HTTP %d.', 'mindlytics-order-notifications-for-woocommerce' ),
+					(int) $status_code
+				)
+			);
+		}
+
+		$decoded = json_decode( wp_remote_retrieve_body( $response ), true );
+		return is_array( $decoded ) ? $decoded : array();
+	}
+}
Index: /mindlytics-order-notifications-for-woocommerce/tags/1.0.0/includes/class-mindlytics-connect.php
===================================================================
--- /mindlytics-order-notifications-for-woocommerce/tags/1.0.0/includes/class-mindlytics-connect.php	(revision 3794792)
+++ /mindlytics-order-notifications-for-woocommerce/tags/1.0.0/includes/class-mindlytics-connect.php	(revision 3794792)
@@ -0,0 +1,155 @@
+<?php
+if ( ! defined( 'ABSPATH' ) ) {
+	exit;
+}
+
+/**
+ * Handles the OAuth connect callback REST endpoint.
+ */
+class Mindlytics_Connect {
+
+	/**
+	 * Constructor — registers hooks.
+	 */
+	public function __construct() {
+		add_action( 'rest_api_init', array( $this, 'handle_rest_api_init' ) );
+	}
+
+	/**
+	 * Register the Mindlytics REST API routes.
+	 *
+	 * @since 1.0.0
+	 * @internal
+	 */
+	public function handle_rest_api_init(): void {
+		register_rest_route(
+			'mindlytics/v1',
+			'/connected',
+			array(
+				'methods'             => WP_REST_Server::CREATABLE,
+				'callback'            => array( $this, 'handle_connected_callback' ),
+				// Called server-to-server by the Mindlytics backend, so there is no
+				// logged-in user; access is gated by the one-time connect state instead.
+				'permission_callback' => array( $this, 'verify_connect_state' ),
+				'args'                => array(
+					'state'          => array(
+						'required' => true,
+						'type'     => 'string',
+					),
+					'shop_id'        => array(
+						'required'          => true,
+						'type'              => 'string',
+						'sanitize_callback' => 'sanitize_text_field',
+						'validate_callback' => array( $this, 'validate_uuid' ),
+					),
+					'webhook_secret' => array(
+						'required'          => true,
+						'type'              => 'string',
+						'sanitize_callback' => array( $this, 'sanitize_hex_secret' ),
+					),
+					'connected'      => array(
+						'required' => true,
+						'type'     => 'boolean',
+					),
+					'shop_name'      => array(
+						'required'          => false,
+						'type'              => 'string',
+						'sanitize_callback' => 'sanitize_text_field',
+					),
+				),
+			)
+		);
+	}
+
+	/**
+	 * Handle the connect callback POST from the Mindlytics backend.
+	 *
+	 * @since 1.0.0
+	 * @param WP_REST_Request $request Full REST request object.
+	 * @return WP_REST_Response|WP_Error
+	 */
+	public function handle_connected_callback( WP_REST_Request $request ) {
+		if ( true !== $request->get_param( 'connected' ) ) {
+			return new WP_Error(
+				'not_connected',
+				__( 'Connection was not approved.', 'mindlytics-order-notifications-for-woocommerce' ),
+				array( 'status' => 400 )
+			);
+		}
+
+		$webhook_secret = $request->get_param( 'webhook_secret' );
+
+		// Enforce 64-char hex (backend generates 32 random bytes → 64 hex chars).
+		if ( ! preg_match( '/^[0-9a-f]{64}$/', $webhook_secret ) ) {
+			return new WP_Error(
+				'invalid_secret',
+				__( 'Invalid webhook secret format.', 'mindlytics-order-notifications-for-woocommerce' ),
+				array( 'status' => 400 )
+			);
+		}
+
+		$shop_id   = $request->get_param( 'shop_id' );
+		$shop_name = $request->get_param( 'shop_name' ) ?: get_bloginfo( 'name' );
+
+		update_option( 'mindlytics_shop_id',        $shop_id );
+		update_option( 'mindlytics_webhook_secret', $webhook_secret );
+		update_option( 'mindlytics_connected_at',   gmdate( 'c' ) );
+		update_option( 'mindlytics_shop_name',      sanitize_text_field( $shop_name ) );
+
+		// Consume pending state nonce.
+		delete_option( 'mindlytics_pending_state' );
+
+		return new WP_REST_Response( array( 'received' => true ), 200 );
+	}
+
+	/**
+	 * Permission check: the request must carry the state issued when the admin
+	 * clicked "Connect". The state is single-use (deleted on success).
+	 *
+	 * @since 1.0.0
+	 * @param WP_REST_Request $request Full REST request object.
+	 * @return true|WP_Error
+	 */
+	public function verify_connect_state( WP_REST_Request $request ) {
+		$expected = (string) get_option( 'mindlytics_pending_state', '' );
+		$received = (string) $request->get_param( 'state' );
+
+		if ( '' === $expected || ! hash_equals( $expected, $received ) ) {
+			return new WP_Error(
+				'invalid_state',
+				__( 'Invalid or expired connection request.', 'mindlytics-order-notifications-for-woocommerce' ),
+				array( 'status' => 403 )
+			);
+		}
+
+		return true;
+	}
+
+	/**
+	 * Validate that a parameter is a valid UUID v4.
+	 *
+	 * @since 1.0.0
+	 * @param string $value The value to validate.
+	 * @return bool
+	 */
+	public function validate_uuid( string $value ): bool {
+		return (bool) preg_match(
+			'/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i',
+			$value
+		);
+	}
+
+	/**
+	 * Sanitize a webhook secret to lowercase hex characters only.
+	 *
+	 * WooCommerce runs wp_specialchars_decode() on webhook secrets before HMAC
+	 * signing. Storing hex-only avoids any HTML entity corruption.
+	 *
+	 * @since 1.0.0
+	 * @param string $value Raw value from request.
+	 * @return string Lowercase hex string.
+	 */
+	public function sanitize_hex_secret( string $value ): string {
+		return strtolower( preg_replace( '/[^0-9a-fA-F]/', '', $value ) );
+	}
+}
Index: /mindlytics-order-notifications-for-woocommerce/tags/1.0.0/includes/views/settings-page.php
===================================================================
--- /mindlytics-order-notifications-for-woocommerce/tags/1.0.0/includes/views/settings-page.php	(revision 3794792)
+++ /mindlytics-order-notifications-for-woocommerce/tags/1.0.0/includes/views/settings-page.php	(revision 3794792)
@@ -0,0 +1,117 @@
+<?php
+if ( ! defined( 'ABSPATH' ) ) {
+	exit;
+}
+
+$mindlytics_is_connected  = ! empty( get_option( 'mindlytics_shop_id' ) ) && ! empty( get_option( 'mindlytics_webhook_secret' ) );
+$mindlytics_shop_name     = esc_html( (string) get_option( 'mindlytics_shop_name', get_bloginfo( 'name' ) ) );
+$mindlytics_connected_at  = get_option( 'mindlytics_connected_at', '' );
+$mindlytics_cart_timeout  = absint( get_option( 'mindlytics_abandoned_cart_timeout', 60 ) );
+?>
+<div class="wrap mindlytics-settings-wrap">
+	<h1><?php esc_html_e( 'Mindlytics WhatsApp', 'mindlytics-order-notifications-for-woocommerce' ); ?></h1>
+
+	<div id="mindlytics-notice" class="notice" hidden></div>
+
+	<div class="mindlytics-connect-card">
+		<?php if ( $mindlytics_is_connected ) : ?>
+
+			<div class="mindlytics-status-row">
+				<span class="mindlytics-status-badge mindlytics-status-connected">
+					<?php esc_html_e( 'Connected', 'mindlytics-order-notifications-for-woocommerce' ); ?>
+				</span>
+				<strong><?php echo esc_html( $mindlytics_shop_name ); ?></strong>
+			</div>
+
+			<?php if ( $mindlytics_connected_at ) : ?>
+				<p class="mindlytics-meta">
+					<?php
+					printf(
+						/* translators: %s: formatted date */
+						esc_html__( 'Connected on %s', 'mindlytics-order-notifications-for-woocommerce' ),
+						esc_html( date_i18n( get_option( 'date_format' ), strtotime( $mindlytics_connected_at ) ) )
+					);
+					?>
+				</p>
+			<?php endif; ?>
+
+			<p>
+				<a href="<?php echo esc_url( MINDLYTICS_APP_URL ); ?>" class="button" target="_blank" rel="noopener">
+					<?php esc_html_e( 'Open dashboard', 'mindlytics-order-notifications-for-woocommerce' ); ?>
+				</a>
+				<button id="mindlytics-disconnect-btn" type="button" class="button">
+					<?php esc_html_e( 'Disconnect', 'mindlytics-order-notifications-for-woocommerce' ); ?>
+				</button>
+			</p>
+
+		<?php else : ?>
+
+			<div class="mindlytics-status-row">
+				<span class="mindlytics-status-badge mindlytics-status-disconnected">
+					<?php esc_html_e( 'Not connected', 'mindlytics-order-notifications-for-woocommerce' ); ?>
+				</span>
+			</div>
+
+			<p><?php esc_html_e( 'Connect your WooCommerce store to Mindlytics to send WhatsApp abandoned cart messages and order notifications.', 'mindlytics-order-notifications-for-woocommerce' ); ?></p>
+
+			<p>
+				<button id="mindlytics-connect-btn" type="button" class="button button-primary">
+					<?php esc_html_e( 'Connect to Mindlytics', 'mindlytics-order-notifications-for-woocommerce' ); ?>
+				</button>
+			</p>
+
+		<?php endif; ?>
+	</div>
+
+	<?php if ( $mindlytics_is_connected ) : ?>
+	<div class="mindlytics-connect-card">
+		<h2><?php esc_html_e( 'Abandoned cart settings', 'mindlytics-order-notifications-for-woocommerce' ); ?></h2>
+		<form id="mindlytics-settings-form">
+			<?php wp_nonce_field( 'mindlytics_admin', 'mindlytics_settings_nonce' ); ?>
+			<table class="form-table" role="presentation">
+				<tr>
+					<th scope="row">
+						<label for="mindlytics-timeout">
+							<?php esc_html_e( 'Abandoned cart delay (minutes)', 'mindlytics-order-notifications-for-woocommerce' ); ?>
+						</label>
+					</th>
+					<td>
+						<input
+							type="number"
+							id="mindlytics-timeout"
+							name="mindlytics_timeout"
+							value="<?php echo esc_attr( (string) $mindlytics_cart_timeout ); ?>"
+							min="30"
+							max="1440"
+							step="1"
+							class="small-text"
+						/>
+						<p class="description">
+							<?php esc_html_e( 'Wait this many minutes after checkout abandonment before sending a WhatsApp message. Minimum 30, maximum 1440 (24 hours).', 'mindlytics-order-notifications-for-woocommerce' ); ?>
+						</p>
+					</td>
+				</tr>
+			</table>
+			<p class="submit">
+				<button type="submit" class="button button-primary">
+					<?php esc_html_e( 'Save changes', 'mindlytics-order-notifications-for-woocommerce' ); ?>
+				</button>
+			</p>
+		</form>
+	</div>
+	<?php endif; ?>
+
+	<div class="mindlytics-connect-card">
+		<details>
+			<summary><?php esc_html_e( 'How to fully disconnect', 'mindlytics-order-notifications-for-woocommerce' ); ?></summary>
+			<div class="mindlytics-details-body">
+				<p><?php esc_html_e( 'Clicking "Disconnect" above removes the connection from this plugin. To also stop WooCommerce from sending order and customer webhooks to Mindlytics:', 'mindlytics-order-notifications-for-woocommerce' ); ?></p>
+				<ol>
+					<li><?php esc_html_e( 'Go to WooCommerce → Settings → Advanced → Webhooks.', 'mindlytics-order-notifications-for-woocommerce' ); ?></li>
+					<li><?php esc_html_e( 'Delete the four webhooks named "Mindlytics — order.created", "Mindlytics — order.updated", "Mindlytics — customer.created", and "Mindlytics — customer.updated".', 'mindlytics-order-notifications-for-woocommerce' ); ?></li>
+					<li><?php esc_html_e( 'Alternatively, you can disconnect from the Mindlytics dashboard under Integrations → WooCommerce, which will remove the webhooks automatically.', 'mindlytics-order-notifications-for-woocommerce' ); ?></li>
+				</ol>
+			</div>
+		</details>
+	</div>
+</div>
Index: /mindlytics-order-notifications-for-woocommerce/tags/1.0.0/languages/mindlytics-order-notifications-for-woocommerce.pot
===================================================================
--- /mindlytics-order-notifications-for-woocommerce/tags/1.0.0/languages/mindlytics-order-notifications-for-woocommerce.pot	(revision 3794792)
+++ /mindlytics-order-notifications-for-woocommerce/tags/1.0.0/languages/mindlytics-order-notifications-for-woocommerce.pot	(revision 3794792)
@@ -0,0 +1,137 @@
+# Copyright (C) 2026 Mindlytics
+# This file is distributed under the GPL v2 or later.
+msgid ""
+msgstr ""
+"Project-Id-Version: Mindlytics – Order Notifications for WooCommerce 1.0.0\n"
+"Report-Msgid-Bugs-To: https://mindlytics.in\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: LANGUAGE <LL@li.org>\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"POT-Creation-Date: 2026-05-06T00:00:00+00:00\n"
+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
+"X-Generator: WP-CLI 2.x\n"
+"X-Domain: mindlytics-order-notifications-for-woocommerce\n"
+
+#. Plugin Name of the plugin
+msgid "WhatsApp Notifications for WooCommerce - Mindlytics"
+msgstr ""
+
+#. Plugin URI of the plugin
+msgid "https://mindlytics.in"
+msgstr ""
+
+#. Description of the plugin
+msgid "Connect your WooCommerce store to Mindlytics for WhatsApp abandoned cart recovery and order notifications."
+msgstr ""
+
+#. Author of the plugin
+msgid "Mindlytics"
+msgstr ""
+
+#: mindlytics-woocommerce.php
+msgid "Mindlytics WhatsApp requires WordPress 6.4 or higher."
+msgstr ""
+
+#: mindlytics-woocommerce.php
+msgid "Mindlytics WhatsApp requires WooCommerce to be installed and active."
+msgstr ""
+
+#: includes/class-mindlytics-admin.php
+msgid "Mindlytics"
+msgstr ""
+
+#: includes/class-mindlytics-admin.php
+msgid "Unauthorized."
+msgstr ""
+
+#: includes/class-mindlytics-connect.php
+msgid "Connection was not approved."
+msgstr ""
+
+#: includes/class-mindlytics-connect.php
+msgid "Invalid webhook secret format."
+msgstr ""
+
+#: includes/class-mindlytics-api.php
+#. translators: %d: HTTP status code
+msgid "Mindlytics API returned HTTP %d."
+msgstr ""
+
+#: includes/class-mindlytics-abandoned-cart.php
+msgid "Every 5 minutes"
+msgstr ""
+
+#: includes/views/settings-page.php
+msgid "Mindlytics WhatsApp"
+msgstr ""
+
+#: includes/views/settings-page.php
+msgid "Connected"
+msgstr ""
+
+#: includes/views/settings-page.php
+#. translators: %s: formatted date
+msgid "Connected on %s"
+msgstr ""
+
+#: includes/views/settings-page.php
+msgid "Open dashboard"
+msgstr ""
+
+#: includes/views/settings-page.php
+msgid "Disconnect"
+msgstr ""
+
+#: includes/views/settings-page.php
+msgid "Not connected"
+msgstr ""
+
+#: includes/views/settings-page.php
+msgid "Connect your WooCommerce store to Mindlytics to send WhatsApp abandoned cart messages and order notifications."
+msgstr ""
+
+#: includes/views/settings-page.php
+msgid "Connect to Mindlytics"
+msgstr ""
+
+#: includes/views/settings-page.php
+msgid "Abandoned cart settings"
+msgstr ""
+
+#: includes/views/settings-page.php
+msgid "Abandoned cart delay (minutes)"
+msgstr ""
+
+#: includes/views/settings-page.php
+msgid "Wait this many minutes after checkout abandonment before sending a WhatsApp message. Minimum 30, maximum 1440 (24 hours)."
+msgstr ""
+
+#: includes/views/settings-page.php
+msgid "Save changes"
+msgstr ""
+
+#: includes/views/settings-page.php
+msgid "How to fully disconnect"
+msgstr ""
+
+#: includes/views/settings-page.php
+msgid "Clicking \"Disconnect\" above removes the connection from this plugin. To also stop WooCommerce from sending order and customer webhooks to Mindlytics:"
+msgstr ""
+
+#: includes/views/settings-page.php
+msgid "Go to WooCommerce → Settings → Advanced → Webhooks."
+msgstr ""
+
+#: includes/views/settings-page.php
+msgid "Delete the four webhooks named \"Mindlytics — order.created\", \"Mindlytics — order.updated\", \"Mindlytics — customer.created\", and \"Mindlytics — customer.updated\"."
+msgstr ""
+
+#: includes/views/settings-page.php
+msgid "Alternatively, you can disconnect from the Mindlytics dashboard under Integrations → WooCommerce, which will remove the webhooks automatically."
+msgstr ""
+
+#: includes/views/settings-page.php
+msgid "You do not have sufficient permissions to access this page."
+msgstr ""
Index: /mindlytics-order-notifications-for-woocommerce/tags/1.0.0/mindlytics-woocommerce.php
===================================================================
--- /mindlytics-order-notifications-for-woocommerce/tags/1.0.0/mindlytics-woocommerce.php	(revision 3794792)
+++ /mindlytics-order-notifications-for-woocommerce/tags/1.0.0/mindlytics-woocommerce.php	(revision 3794792)
@@ -0,0 +1,131 @@
+<?php
+/**
+ * Plugin Name:       Mindlytics – Order Notifications for WooCommerce
+ * Plugin URI:        https://app.mindlytics.in/integrations/woocommerce
+ * Description:       Connect your WooCommerce store to Mindlytics for WhatsApp abandoned cart recovery and order notifications.
+ * Version:           1.0.0
+ * Requires at least: 6.4
+ * Requires PHP:      7.4
+ * Requires Plugins:  woocommerce
+ * Author:            Mindlytics
+ * Author URI:        https://mindlytics.in
+ * License:           GPL-2.0-or-later
+ * License URI:       https://www.gnu.org/licenses/gpl-2.0.html
+ * Text Domain:       mindlytics-order-notifications-for-woocommerce
+ * WC requires at least: 5.0
+ * WC tested up to:   9.0
+ */
+
+if ( ! defined( 'ABSPATH' ) ) {
+	exit;
+}
+
+define( 'MINDLYTICS_WC_VERSION', '1.0.0' );
+define( 'MINDLYTICS_WC_FILE',    __FILE__ );
+define( 'MINDLYTICS_WC_DIR',     plugin_dir_path( __FILE__ ) );
+define( 'MINDLYTICS_WC_URL',     plugin_dir_url( __FILE__ ) );
+
+define( 'MINDLYTICS_API_URL', 'https://wbp-api.mindlytics.in' );
+define( 'MINDLYTICS_APP_URL', 'https://app.mindlytics.in' );
+
+/**
+ * Main plugin class.
+ */
+class Mindlytics_WooCommerce {
+
+	/**
+	 * Singleton instance.
+	 *
+	 * @var self|null
+	 */
+	private static ?self $_instance = null;
+
+	/**
+	 * Get the singleton instance.
+	 *
+	 * @since 1.0.0
+	 * @return self
+	 */
+	public static function instance(): self {
+		if ( null === self::$_instance ) {
+			self::$_instance = new self();
+		}
+		return self::$_instance;
+	}
+
+	/**
+	 * Constructor — loads text domain and registers the plugins_loaded hook.
+	 */
+	private function __construct() {
+		add_action( 'plugins_loaded', array( $this, 'handle_plugins_loaded' ) );
+	}
+
+	/**
+	 * Boot the plugin once all plugins are loaded.
+	 *
+	 * @since 1.0.0
+	 * @internal
+	 */
+	public function handle_plugins_loaded(): void {
+		if ( version_compare( $GLOBALS['wp_version'], '6.4', '<' ) ) {
+			add_action(
+				'admin_notices',
+				static function () {
+					echo '<div class="notice notice-error"><p>' .
+						esc_html__( 'Mindlytics WhatsApp requires WordPress 6.4 or higher.', 'mindlytics-order-notifications-for-woocommerce' ) .
+						'</p></div>';
+				}
+			);
+			return;
+		}
+
+		if ( ! class_exists( 'WooCommerce' ) ) {
+			add_action(
+				'admin_notices',
+				static function () {
+					echo '<div class="notice notice-error"><p>' .
+						esc_html__( 'Mindlytics WhatsApp requires WooCommerce to be installed and active.', 'mindlytics-order-notifications-for-woocommerce' ) .
+						'</p></div>';
+				}
+			);
+			return;
+		}
+
+		$this->includes();
+
+		new Mindlytics_Admin();
+		new Mindlytics_Connect();
+		new Mindlytics_Abandoned_Cart();
+
+		// Scheduled on init rather than on activation: the custom interval is only
+		// registered once the class above has hooked cron_schedules, and its label
+		// is translated (translations must not load before init). No-op when the
+		// event already exists.
+		add_action( 'init', array( 'Mindlytics_Abandoned_Cart', 'schedule_cron' ) );
+	}
+
+	/**
+	 * Require all plugin class files.
+	 *
+	 * @since 1.0.0
+	 */
+	private function includes(): void {
+		require_once MINDLYTICS_WC_DIR . 'includes/class-mindlytics-api.php';
+		require_once MINDLYTICS_WC_DIR . 'includes/class-mindlytics-connect.php';
+		require_once MINDLYTICS_WC_DIR . 'includes/class-mindlytics-abandoned-cart.php';
+		require_once MINDLYTICS_WC_DIR . 'includes/class-mindlytics-admin.php';
+	}
+
+	/**
+	 * Plugin deactivation handler.
+	 *
+	 * @since 1.0.0
+	 */
+	public static function deactivate(): void {
+		wp_clear_scheduled_hook( 'mindlytics_check_abandoned_carts' );
+	}
+}
+
+register_deactivation_hook( MINDLYTICS_WC_FILE, array( 'Mindlytics_WooCommerce', 'deactivate' ) );
+
+Mindlytics_WooCommerce::instance();
Index: /mindlytics-order-notifications-for-woocommerce/tags/1.0.0/readme.txt
===================================================================
--- /mindlytics-order-notifications-for-woocommerce/tags/1.0.0/readme.txt	(revision 3794792)
+++ /mindlytics-order-notifications-for-woocommerce/tags/1.0.0/readme.txt	(revision 3794792)
@@ -0,0 +1,87 @@
+=== Mindlytics – Order Notifications for WooCommerce ===
+Contributors: mindlyticsdev
+Tags: whatsapp, woocommerce, abandoned-cart, order-notification, whatsapp-business
+Requires at least: 6.4
+Tested up to: 7.1
+Requires PHP: 7.4
+WC requires at least: 5.0
+WC tested up to: 9.0
+Stable tag: 1.0.0
+License: GPL-2.0-or-later
+License URI: https://www.gnu.org/licenses/gpl-2.0.html
+
+Connect your WooCommerce store to Mindlytics for WhatsApp abandoned cart recovery and order notifications.
+
+== Description ==
+
+**Mindlytics WhatsApp for WooCommerce** connects your store to the Mindlytics platform so you can send automated WhatsApp messages to your customers.
+
+**Features:**
+
+* **Abandoned cart recovery** — automatically detects when a logged-in customer leaves the checkout page and sends a WhatsApp reminder after a configurable delay.
+* **Order notifications** — sends WhatsApp messages on order creation, shipping, delivery, and cancellation (configured in the Mindlytics dashboard).
+* **One-click connect** — no API keys to copy. Click "Connect to Mindlytics", log in to your Mindlytics account, approve access, and you're done.
+* **Template mapping** — choose which WhatsApp message template fires for each event in the Mindlytics dashboard.
+
+**Requirements:**
+
+* WordPress 6.4 or higher
+* WooCommerce 5.0 or higher
+* A Mindlytics account (free trial available at mindlytics.in)
+
+== Installation ==
+
+1. Upload the plugin folder to `/wp-content/plugins/` or install via Plugins > Add new plugin.
+2. Activate the plugin via Plugins > Installed plugins.
+3. Go to WooCommerce > Mindlytics.
+4. Click "Connect to Mindlytics" and follow the steps.
+
+== Frequently Asked Questions ==
+
+= Does this plugin work without a Mindlytics account? =
+
+No. You need a Mindlytics account to use this plugin. Create one at mindlytics.in.
+
+= Does the plugin send WhatsApp messages directly? =
+
+No. The plugin forwards events (abandoned carts) to the Mindlytics backend, which sends WhatsApp messages using the Meta WhatsApp Business API. Order events are forwarded by native WooCommerce webhooks registered at connect time.
+
+= Why do I need to delete webhooks manually when disconnecting? =
+
+When you connect, Mindlytics registers four native WooCommerce webhooks on your store. Disconnecting from the plugin clears the local connection, but the WC webhooks remain until deleted from WooCommerce > Settings > Advanced > Webhooks — or disconnected from the Mindlytics dashboard, which removes them automatically.
+
+= Does this plugin support guest checkouts? =
+
+Abandoned cart detection requires a phone number. For guest checkouts, the phone number is only available after the customer enters it on the checkout form. Guests who abandon before typing their phone number will not receive a WhatsApp message.
+
+= What is the minimum abandoned cart delay? =
+
+30 minutes. The default is 60 minutes. The maximum is 1440 minutes (24 hours).
+
+= What external services does this plugin use? =
+
+This plugin connects to **Mindlytics** (mindlytics.in) to send WhatsApp messages. Specifically:
+
+* During connect, your browser is redirected to the Mindlytics app to authenticate your account.
+* Abandoned cart events are sent to the Mindlytics backend for processing.
+* Order and customer data is forwarded via WooCommerce native webhooks to the Mindlytics backend.
+
+See the [Mindlytics Privacy Policy](https://mindlytics.in/privacy-policy) for how data is handled.
+
+No data is sent to external services until you explicitly connect your store.
+
+== Screenshots ==
+
+1. Settings page — not connected state with connect button.
+2. Settings page — connected state showing store name and disconnect option.
+3. Mindlytics dashboard showing WooCommerce template mappings.
+4. Example WhatsApp abandoned cart message received by a customer.
+
+== Changelog ==
+
+= 1.0.0 =
+* Initial release.
+* One-click OAuth connect flow.
+* Abandoned cart detection via WP Cron.
+* Configurable abandonment delay (30–1440 minutes).
+* WooCommerce > Mindlytics admin menu.
Index: /mindlytics-order-notifications-for-woocommerce/trunk/assets/admin.css
===================================================================
--- /mindlytics-order-notifications-for-woocommerce/trunk/assets/admin.css	(revision 3794792)
+++ /mindlytics-order-notifications-for-woocommerce/trunk/assets/admin.css	(revision 3794792)
@@ -0,0 +1,89 @@
+/* Mindlytics WooCommerce admin styles */
+
+.mindlytics-settings-wrap {
+	max-width: 760px;
+}
+
+.mindlytics-settings-wrap h1 {
+	margin-bottom: 16px;
+}
+
+.mindlytics-connect-card {
+	background: #fff;
+	border: 1px solid #c3c4c7;
+	border-radius: 4px;
+	padding: 20px 24px;
+	margin-bottom: 16px;
+}
+
+.mindlytics-connect-card h2 {
+	margin-top: 0;
+	font-size: 14px;
+	font-weight: 600;
+}
+
+.mindlytics-status-row {
+	display: flex;
+	align-items: center;
+	gap: 10px;
+	margin-bottom: 12px;
+}
+
+.mindlytics-status-badge {
+	display: inline-flex;
+	align-items: center;
+	padding: 3px 10px;
+	border-radius: 12px;
+	font-size: 12px;
+	font-weight: 600;
+	line-height: 1.4;
+	color: #fff;
+}
+
+.mindlytics-status-connected {
+	background-color: #00a32a;
+}
+
+.mindlytics-status-disconnected {
+	background-color: #646970;
+}
+
+.mindlytics-meta {
+	color: #646970;
+	font-size: 13px;
+	margin-top: 0;
+}
+
+.mindlytics-connect-card p {
+	margin-bottom: 12px;
+}
+
+.mindlytics-connect-card p:last-child {
+	margin-bottom: 0;
+}
+
+.mindlytics-connect-card details summary {
+	cursor: pointer;
+	font-weight: 600;
+	padding: 4px 0;
+	user-select: none;
+}
+
+.mindlytics-details-body {
+	margin-top: 12px;
+	padding-top: 12px;
+	border-top: 1px solid #f0f0f1;
+}
+
+.mindlytics-details-body ol {
+	margin-left: 20px;
+}
+
+.mindlytics-details-body li {
+	margin-bottom: 6px;
+}
+
+#mindlytics-notice {
+	margin: 0 0 16px;
+	padding: 10px 14px;
+}
Index: /mindlytics-order-notifications-for-woocommerce/trunk/assets/admin.js
===================================================================
--- /mindlytics-order-notifications-for-woocommerce/trunk/assets/admin.js	(revision 3794792)
+++ /mindlytics-order-notifications-for-woocommerce/trunk/assets/admin.js	(revision 3794792)
@@ -0,0 +1,142 @@
+/* global mindlyticsAdmin */
+(function () {
+	'use strict';
+
+	// ── Connect ───────────────────────────────────────────────────────────────
+
+	function openConnectPopup() {
+		fetch( mindlyticsAdmin.ajaxUrl, {
+			method: 'POST',
+			headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
+			body: new URLSearchParams( {
+				action: 'mindlytics_open_connect',
+				nonce:  mindlyticsAdmin.nonce,
+			} ),
+		} )
+			.then( function ( r ) { return r.json(); } )
+			.then( function ( data ) {
+				if ( ! data.success ) {
+					showError( data.data && data.data.message ? data.data.message : 'An error occurred.' );
+					return;
+				}
+
+				var popup = window.open(
+					data.data.popup_url,
+					'mindlytics_connect',
+					'width=640,height=720,scrollbars=yes,resizable=yes'
+				);
+
+				window.addEventListener( 'message', function onMessage( event ) {
+					// Only accept messages from the Mindlytics app origin.
+					// appOrigin is set by PHP to match MINDLYTICS_APP_URL (dev or prod).
+				if ( event.origin !== mindlyticsAdmin.appOrigin ) {
+						return;
+					}
+					if ( ! event.data ) { return; }
+					if ( event.data.type !== 'MINDLYTICS_CONNECTED' && event.data.type !== 'MINDLYTICS_CONNECT_ERROR' ) {
+						return;
+					}
+
+					window.removeEventListener( 'message', onMessage );
+					if ( popup && ! popup.closed ) { popup.close(); }
+
+					if ( event.data.type === 'MINDLYTICS_CONNECTED' ) {
+						window.location.reload();
+					} else {
+						showError( event.data.error || 'Connection failed. Please try again.' );
+					}
+				} );
+			} )
+			.catch( function ( err ) {
+				showError( err.message || 'Failed to open connect window.' );
+			} );
+	}
+
+	// ── Disconnect ────────────────────────────────────────────────────────────
+
+	function disconnect() {
+		if ( ! window.confirm( 'Disconnect this store from Mindlytics? Abandoned cart tracking will stop immediately.' ) ) {
+			return;
+		}
+
+		fetch( mindlyticsAdmin.ajaxUrl, {
+			method: 'POST',
+			headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
+			body: new URLSearchParams( {
+				action: 'mindlytics_disconnect',
+				nonce:  mindlyticsAdmin.nonce,
+			} ),
+		} )
+			.then( function ( r ) { return r.json(); } )
+			.then( function ( data ) {
+				if ( data.success ) {
+					window.location.reload();
+				} else {
+					showError( data.data && data.data.message ? data.data.message : 'Disconnect failed.' );
+				}
+			} )
+			.catch( function ( err ) {
+				showError( err.message || 'Disconnect failed.' );
+			} );
+	}
+
+	// ── Save settings ─────────────────────────────────────────────────────────
+
+	function saveSettings( event ) {
+		event.preventDefault();
+		var form    = event.currentTarget;
+		var input   = form.querySelector( '[name="mindlytics_timeout"]' );
+		var timeout = input ? parseInt( input.value, 10 ) : 60;
+
+		fetch( mindlyticsAdmin.ajaxUrl, {
+			method: 'POST',
+			headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
+			body: new URLSearchParams( {
+				action:             'mindlytics_save_settings',
+				nonce:              mindlyticsAdmin.nonce,
+				mindlytics_timeout: timeout,
+			} ),
+		} )
+			.then( function ( r ) { return r.json(); } )
+			.then( function ( data ) {
+				if ( data.success ) {
+					showSuccess( 'Settings saved.' );
+				} else {
+					showError( data.data && data.data.message ? data.data.message : 'Failed to save settings.' );
+				}
+			} )
+			.catch( function ( err ) {
+				showError( err.message || 'Failed to save settings.' );
+			} );
+	}
+
+	// ── UI helpers ────────────────────────────────────────────────────────────
+
+	function showError( msg ) {
+		var el = document.getElementById( 'mindlytics-notice' );
+		if ( ! el ) { return; }
+		el.textContent = msg;
+		el.className   = 'notice notice-error';
+		el.removeAttribute( 'hidden' );
+	}
+
+	function showSuccess( msg ) {
+		var el = document.getElementById( 'mindlytics-notice' );
+		if ( ! el ) { return; }
+		el.textContent = msg;
+		el.className   = 'notice notice-success';
+		el.removeAttribute( 'hidden' );
+	}
+
+	// ── Bootstrap ─────────────────────────────────────────────────────────────
+
+	document.addEventListener( 'DOMContentLoaded', function () {
+		var connectBtn    = document.getElementById( 'mindlytics-connect-btn' );
+		var disconnectBtn = document.getElementById( 'mindlytics-disconnect-btn' );
+		var settingsForm  = document.getElementById( 'mindlytics-settings-form' );
+
+		if ( connectBtn )    { connectBtn.addEventListener( 'click', openConnectPopup ); }
+		if ( disconnectBtn ) { disconnectBtn.addEventListener( 'click', disconnect ); }
+		if ( settingsForm )  { settingsForm.addEventListener( 'submit', saveSettings ); }
+	} );
+}());
Index: /mindlytics-order-notifications-for-woocommerce/trunk/includes/class-mindlytics-abandoned-cart.php
===================================================================
--- /mindlytics-order-notifications-for-woocommerce/trunk/includes/class-mindlytics-abandoned-cart.php	(revision 3794792)
+++ /mindlytics-order-notifications-for-woocommerce/trunk/includes/class-mindlytics-abandoned-cart.php	(revision 3794792)
@@ -0,0 +1,283 @@
+<?php
+if ( ! defined( 'ABSPATH' ) ) {
+	exit;
+}
+
+/**
+ * Detects abandoned carts and sends webhook events to the Mindlytics backend.
+ */
+class Mindlytics_Abandoned_Cart {
+
+	const CRON_HOOK     = 'mindlytics_check_abandoned_carts';
+	const CRON_INTERVAL = 'mindlytics_five_minutes';
+	const CART_PREFIX   = 'mindlytics_cart_';
+	const INDEX_KEY     = 'mindlytics_cart_index';
+
+	/**
+	 * Constructor — registers hooks.
+	 */
+	public function __construct() {
+		add_filter( 'cron_schedules',                                  array( $this, 'handle_cron_schedules' ) );
+		add_action( 'woocommerce_before_checkout_form',                array( $this, 'handle_checkout_form_shown' ) );
+		add_action( 'woocommerce_checkout_order_processed',            array( $this, 'handle_order_processed' ), 10, 3 );
+		add_action( self::CRON_HOOK,                                   array( $this, 'handle_cron_check_abandoned_carts' ) );
+	}
+
+	/**
+	 * Schedule the abandoned-cart cron event if not already scheduled.
+	 *
+	 * @since 1.0.0
+	 */
+	public static function schedule_cron(): void {
+		if ( ! wp_next_scheduled( self::CRON_HOOK ) ) {
+			wp_schedule_event( time(), self::CRON_INTERVAL, self::CRON_HOOK );
+		}
+	}
+
+	/**
+	 * Register the five-minute cron interval.
+	 *
+	 * @since 1.0.0
+	 * @internal
+	 * @param array $schedules Existing cron schedules.
+	 * @return array
+	 */
+	public function handle_cron_schedules( array $schedules ): array {
+		if ( ! isset( $schedules[ self::CRON_INTERVAL ] ) ) {
+			$schedules[ self::CRON_INTERVAL ] = array(
+				'interval' => 5 * MINUTE_IN_SECONDS,
+				'display'  => __( 'Every 5 minutes', 'mindlytics-order-notifications-for-woocommerce' ),
+			);
+		}
+		return $schedules;
+	}
+
+	/**
+	 * Capture a cart snapshot when the checkout page is displayed.
+	 *
+	 * Only captures if the cart is non-empty and a session ID is available.
+	 * The abandonment clock is not reset on subsequent page loads if the cart
+	 * contents (cart_hash) have not changed.
+	 *
+	 * @since 1.0.0
+	 * @internal
+	 */
+	public function handle_checkout_form_shown(): void {
+		if ( ! WC()->cart || WC()->cart->is_empty() ) {
+			return;
+		}
+
+		$session_id = WC()->session ? WC()->session->get_customer_id() : null;
+		if ( empty( $session_id ) ) {
+			return;
+		}
+
+		$cart_hash  = WC()->cart->get_cart_hash();
+		$option_key = self::CART_PREFIX . $session_id;
+		$existing   = get_option( $option_key );
+
+		// Don't reset the abandonment clock if cart contents haven't changed.
+		if ( is_array( $existing ) && false === $existing['fired'] && isset( $existing['cart_hash'] ) && $existing['cart_hash'] === $cart_hash ) {
+			return;
+		}
+
+		$customer_data = $this->get_customer_data();
+
+		$snapshot = array(
+			'session_id'  => sanitize_text_field( $session_id ),
+			'cart_hash'   => sanitize_text_field( $cart_hash ),
+			'cart_total'  => wc_format_decimal( WC()->cart->get_total( 'edit' ) ),
+			'cart_items'  => $this->serialize_cart_items( WC()->cart->get_cart() ),
+			'phone'       => sanitize_text_field( $customer_data['phone'] ),
+			'email'       => sanitize_email( $customer_data['email'] ),
+			'first_name'  => sanitize_text_field( $customer_data['first_name'] ),
+			'captured_at' => gmdate( 'c' ),
+			'fired'       => false,
+		);
+
+		update_option( $option_key, $snapshot, false );
+		$this->add_to_cart_index( $session_id );
+	}
+
+	/**
+	 * Mark the cart as completed when an order is processed.
+	 *
+	 * @since 1.0.0
+	 * @internal
+	 * @param int      $order_id    The new order ID.
+	 * @param array    $posted_data Posted checkout data.
+	 * @param WC_Order $order       The created order object.
+	 */
+	public function handle_order_processed( int $order_id, array $posted_data, WC_Order $order ): void {
+		$session_id = WC()->session ? WC()->session->get_customer_id() : null;
+		if ( empty( $session_id ) ) {
+			return;
+		}
+
+		$option_key = self::CART_PREFIX . $session_id;
+		$snapshot   = get_option( $option_key );
+		if ( ! is_array( $snapshot ) ) {
+			return;
+		}
+
+		$snapshot['fired'] = true;
+		update_option( $option_key, $snapshot, false );
+	}
+
+	/**
+	 * WP Cron callback: find and fire webhooks for abandoned carts.
+	 *
+	 * Uses wp_prime_option_caches() (WP 6.4+) to batch-load all cart snapshots
+	 * before iterating, reducing per-cart DB queries to zero.
+	 *
+	 * @since 1.0.0
+	 * @internal
+	 */
+	public function handle_cron_check_abandoned_carts(): void {
+		$shop_id = get_option( 'mindlytics_shop_id' );
+		$secret  = get_option( 'mindlytics_webhook_secret' );
+
+		if ( empty( $shop_id ) || empty( $secret ) ) {
+			return;
+		}
+
+		$index = $this->get_cart_index();
+		if ( empty( $index ) ) {
+			return;
+		}
+
+		$option_keys = array_map(
+			static fn( string $sid ): string => self::CART_PREFIX . $sid,
+			$index
+		);
+
+		// Prime object cache for all cart options in one SQL query.
+		wp_prime_option_caches( $option_keys );
+
+		$timeout_seconds = absint( get_option( 'mindlytics_abandoned_cart_timeout', 60 ) ) * MINUTE_IN_SECONDS;
+		$now             = time();
+		$cleaned_index   = $index;
+
+		foreach ( $index as $session_id ) {
+			$option_key = self::CART_PREFIX . $session_id;
+			$snapshot   = get_option( $option_key );
+
+			if ( ! is_array( $snapshot ) ) {
+				$cleaned_index = array_diff( $cleaned_index, array( $session_id ) );
+				continue;
+			}
+
+			if ( true === $snapshot['fired'] ) {
+				delete_option( $option_key );
+				$cleaned_index = array_diff( $cleaned_index, array( $session_id ) );
+				continue;
+			}
+
+			$captured_at = strtotime( $snapshot['captured_at'] ?? '' );
+			if ( false === $captured_at || ( $now - $captured_at ) < $timeout_seconds ) {
+				continue;
+			}
+
+			if ( empty( $snapshot['phone'] ) ) {
+				continue;
+			}
+
+			$result = Mindlytics_Api::instance()->send_abandoned_cart( $snapshot, $shop_id, $secret );
+
+			if ( ! is_wp_error( $result ) ) {
+				$snapshot['fired'] = true;
+				update_option( $option_key, $snapshot, false );
+				delete_option( $option_key );
+				$cleaned_index = array_diff( $cleaned_index, array( $session_id ) );
+			}
+		}
+
+		update_option( self::INDEX_KEY, array_values( $cleaned_index ), false );
+	}
+
+	/**
+	 * Retrieve the current customer's phone, email, and first name.
+	 *
+	 * Reads from the WC session (works for guests mid-checkout) or from
+	 * user meta for logged-in customers.
+	 *
+	 * @since 1.0.0
+	 * @return array{phone: string, email: string, first_name: string}
+	 */
+	private function get_customer_data(): array {
+		$phone      = '';
+		$email      = '';
+		$first_name = '';
+
+		if ( is_user_logged_in() ) {
+			$user_id    = get_current_user_id();
+			$phone      = (string) get_user_meta( $user_id, 'billing_phone', true );
+			$email      = (string) get_user_meta( $user_id, 'billing_email', true );
+			$first_name = (string) get_user_meta( $user_id, 'billing_first_name', true );
+		}
+
+		if ( WC()->session ) {
+			$customer = WC()->session->get( 'customer' );
+			if ( is_array( $customer ) ) {
+				$phone      = $phone ?: ( $customer['phone'] ?? '' );
+				$email      = $email ?: ( $customer['email'] ?? '' );
+				$first_name = $first_name ?: ( $customer['first_name'] ?? '' );
+			}
+		}
+
+		return compact( 'phone', 'email', 'first_name' );
+	}
+
+	/**
+	 * Serialize cart contents into a compact array for storage.
+	 *
+	 * @since 1.0.0
+	 * @param array $cart_contents WooCommerce cart contents.
+	 * @return array
+	 */
+	private function serialize_cart_items( array $cart_contents ): array {
+		$items = array();
+		foreach ( $cart_contents as $item ) {
+			/** @var WC_Product|false $product */
+			$product = $item['data'] ?? false;
+			if ( ! $product instanceof WC_Product ) {
+				continue;
+			}
+			$image_id = $product->get_image_id();
+			$items[]  = array(
+				'product_id' => absint( $item['product_id'] ?? 0 ),
+				'name'       => sanitize_text_field( $product->get_name() ),
+				'quantity'   => absint( $item['quantity'] ?? 1 ),
+				'price'      => wc_format_decimal( $product->get_price( 'edit' ) ),
+				'sku'        => sanitize_text_field( $product->get_sku() ),
+				'image_url'  => $image_id ? esc_url_raw( (string) wp_get_attachment_url( $image_id ) ) : '',
+			);
+		}
+		return $items;
+	}
+
+	/**
+	 * Add a session ID to the cart index.
+	 *
+	 * @since 1.0.0
+	 * @param string $session_id WooCommerce session/customer ID.
+	 */
+	private function add_to_cart_index( string $session_id ): void {
+		$index = $this->get_cart_index();
+		if ( ! in_array( $session_id, $index, true ) ) {
+			$index[] = sanitize_text_field( $session_id );
+			update_option( self::INDEX_KEY, $index, false );
+		}
+	}
+
+	/**
+	 * Get the cart index array.
+	 *
+	 * @since 1.0.0
+	 * @return string[]
+	 */
+	private function get_cart_index(): array {
+		$index = get_option( self::INDEX_KEY, array() );
+		return is_array( $index ) ? $index : array();
+	}
+}
Index: /mindlytics-order-notifications-for-woocommerce/trunk/includes/class-mindlytics-admin.php
===================================================================
--- /mindlytics-order-notifications-for-woocommerce/trunk/includes/class-mindlytics-admin.php	(revision 3794792)
+++ /mindlytics-order-notifications-for-woocommerce/trunk/includes/class-mindlytics-admin.php	(revision 3794792)
@@ -0,0 +1,180 @@
+<?php
+if ( ! defined( 'ABSPATH' ) ) {
+	exit;
+}
+
+/**
+ * Manages the WP admin menu, settings page, and AJAX handlers.
+ */
+class Mindlytics_Admin {
+
+	/**
+	 * Constructor — registers hooks.
+	 */
+	public function __construct() {
+		add_action( 'admin_menu',                              array( $this, 'handle_admin_menu' ) );
+		add_action( 'admin_enqueue_scripts',                   array( $this, 'handle_admin_enqueue_scripts' ) );
+		add_action( 'wp_ajax_mindlytics_open_connect',         array( $this, 'handle_ajax_open_connect' ) );
+		add_action( 'wp_ajax_mindlytics_disconnect',           array( $this, 'handle_ajax_disconnect' ) );
+		add_action( 'wp_ajax_mindlytics_save_settings',        array( $this, 'handle_ajax_save_settings' ) );
+	}
+
+	/**
+	 * Register the Mindlytics submenu under WooCommerce.
+	 *
+	 * @since 1.0.0
+	 * @internal
+	 */
+	public function handle_admin_menu(): void {
+		add_submenu_page(
+			'woocommerce',
+			__( 'Mindlytics', 'mindlytics-order-notifications-for-woocommerce' ),
+			__( 'Mindlytics', 'mindlytics-order-notifications-for-woocommerce' ),
+			'manage_woocommerce',
+			'mindlytics-settings',
+			array( $this, 'render_settings_page' )
+		);
+	}
+
+	/**
+	 * Enqueue admin scripts and styles on the Mindlytics settings page only.
+	 *
+	 * @since 1.0.0
+	 * @internal
+	 * @param string $hook_suffix Current admin page hook suffix.
+	 */
+	public function handle_admin_enqueue_scripts( string $hook_suffix ): void {
+		if ( 'woocommerce_page_mindlytics-settings' !== $hook_suffix ) {
+			return;
+		}
+
+		wp_enqueue_style(
+			'mindlytics-admin',
+			MINDLYTICS_WC_URL . 'assets/admin.css',
+			array(),
+			MINDLYTICS_WC_VERSION
+		);
+
+		wp_enqueue_script(
+			'mindlytics-admin',
+			MINDLYTICS_WC_URL . 'assets/admin.js',
+			array(),
+			MINDLYTICS_WC_VERSION,
+			true
+		);
+
+		wp_localize_script(
+			'mindlytics-admin',
+			'mindlyticsAdmin',
+			array(
+				'ajaxUrl'     => admin_url( 'admin-ajax.php' ),
+				'nonce'       => wp_create_nonce( 'mindlytics_admin' ),
+				'isConnected' => $this->is_connected(),
+				'shopName'    => esc_html( (string) get_option( 'mindlytics_shop_name', '' ) ),
+				'appOrigin'   => esc_url_raw( MINDLYTICS_APP_URL ),
+			)
+		);
+	}
+
+	/**
+	 * Render the Mindlytics settings page.
+	 *
+	 * @since 1.0.0
+	 */
+	public function render_settings_page(): void {
+		if ( ! current_user_can( 'manage_woocommerce' ) ) {
+			wp_die( esc_html__( 'You do not have sufficient permissions to access this page.', 'mindlytics-order-notifications-for-woocommerce' ) );
+		}
+		include MINDLYTICS_WC_DIR . 'includes/views/settings-page.php';
+	}
+
+	/**
+	 * AJAX: generate a connect nonce and return the popup URL.
+	 *
+	 * @since 1.0.0
+	 * @internal
+	 */
+	public function handle_ajax_open_connect(): void {
+		check_ajax_referer( 'mindlytics_admin', 'nonce' );
+
+		if ( ! current_user_can( 'manage_woocommerce' ) ) {
+			wp_send_json_error( array( 'message' => __( 'Unauthorized.', 'mindlytics-order-notifications-for-woocommerce' ) ), 403 );
+		}
+
+		$state = wp_generate_password( 32, false );
+		update_option( 'mindlytics_pending_state', $state, false );
+
+		// Allow overriding URLs via constants — useful in local dev where siteurl
+		// includes a port or is otherwise unreachable from the remote backend.
+		$callback_url = defined( 'MINDLYTICS_CALLBACK_URL' )
+			? MINDLYTICS_CALLBACK_URL
+			: rest_url( 'mindlytics/v1/connected' );
+
+		$site_url = defined( 'MINDLYTICS_SITE_URL' )
+			? MINDLYTICS_SITE_URL
+			: get_site_url();
+		$popup_url = esc_url_raw( MINDLYTICS_APP_URL . '/integrations/woocommerce/connect' )
+			. '?' . http_build_query(
+				array(
+					'site_url'     => $site_url,
+					'callback_url' => $callback_url,
+					'state'        => $state,
+				)
+			);
+
+		wp_send_json_success( array( 'popup_url' => $popup_url ) );
+	}
+
+	/**
+	 * AJAX: disconnect the store (local operation — clears wp_options only).
+	 *
+	 * @since 1.0.0
+	 * @internal
+	 */
+	public function handle_ajax_disconnect(): void {
+		check_ajax_referer( 'mindlytics_admin', 'nonce' );
+
+		if ( ! current_user_can( 'manage_woocommerce' ) ) {
+			wp_send_json_error( array( 'message' => __( 'Unauthorized.', 'mindlytics-order-notifications-for-woocommerce' ) ), 403 );
+		}
+
+		delete_option( 'mindlytics_shop_id' );
+		delete_option( 'mindlytics_webhook_secret' );
+		delete_option( 'mindlytics_connected_at' );
+		delete_option( 'mindlytics_shop_name' );
+
+		wp_send_json_success( array( 'disconnected' => true ) );
+	}
+
+	/**
+	 * AJAX: save the abandoned cart timeout setting.
+	 *
+	 * @since 1.0.0
+	 * @internal
+	 */
+	public function handle_ajax_save_settings(): void {
+		check_ajax_referer( 'mindlytics_admin', 'nonce' );
+
+		if ( ! current_user_can( 'manage_woocommerce' ) ) {
+			wp_send_json_error( array( 'message' => __( 'Unauthorized.', 'mindlytics-order-notifications-for-woocommerce' ) ), 403 );
+		}
+
+		$timeout = absint( $_POST['mindlytics_timeout'] ?? 60 );
+		$timeout = max( 30, min( 1440, $timeout ) );
+
+		update_option( 'mindlytics_abandoned_cart_timeout', $timeout );
+
+		wp_send_json_success( array( 'timeout' => $timeout ) );
+	}
+
+	/**
+	 * Determine whether the store is currently connected.
+	 *
+	 * @since 1.0.0
+	 * @return bool
+	 */
+	private function is_connected(): bool {
+		return ! empty( get_option( 'mindlytics_shop_id' ) )
+			&& ! empty( get_option( 'mindlytics_webhook_secret' ) );
+	}
+}
Index: /mindlytics-order-notifications-for-woocommerce/trunk/includes/class-mindlytics-api.php
===================================================================
--- /mindlytics-order-notifications-for-woocommerce/trunk/includes/class-mindlytics-api.php	(revision 3794792)
+++ /mindlytics-order-notifications-for-woocommerce/trunk/includes/class-mindlytics-api.php	(revision 3794792)
@@ -0,0 +1,111 @@
+<?php
+if ( ! defined( 'ABSPATH' ) ) {
+	exit;
+}
+
+/**
+ * HTTP client for outbound calls to the Mindlytics backend.
+ */
+class Mindlytics_Api {
+
+	/**
+	 * Singleton instance.
+	 *
+	 * @var self|null
+	 */
+	private static ?self $_instance = null;
+
+	/**
+	 * Get the singleton instance.
+	 *
+	 * @since 1.0.0
+	 * @return self
+	 */
+	public static function instance(): self {
+		if ( null === self::$_instance ) {
+			self::$_instance = new self();
+		}
+		return self::$_instance;
+	}
+
+	/**
+	 * Send an abandoned cart webhook to the Mindlytics backend.
+	 *
+	 * Delivery ID is SHA-1 of (shop_id + cart_hash + captured_at) for backend
+	 * deduplication. Uses hex-only webhook_secret — WooCommerce runs
+	 * wp_specialchars_decode() on secrets, so hex avoids any entity-decode side-effects.
+	 *
+	 * @since 1.0.0
+	 * @param array  $snapshot Cart snapshot stored in wp_options.
+	 * @param string $shop_id  Stored shop UUID.
+	 * @param string $secret   Stored 64-char hex webhook secret.
+	 * @return array|WP_Error Decoded response body or WP_Error on failure.
+	 */
+	public function send_abandoned_cart( array $snapshot, string $shop_id, string $secret ) {
+		$delivery_id = sha1( $shop_id . ( $snapshot['cart_hash'] ?? '' ) . ( $snapshot['captured_at'] ?? '' ) );
+
+		$items = array();
+		foreach ( $snapshot['cart_items'] ?? array() as $item ) {
+			$items[] = array(
+				'name'     => $item['name'] ?? '',
+				'quantity' => $item['quantity'] ?? 1,
+				'price'    => $item['price'] ?? '0.00',
+			);
+		}
+
+		$body = wp_json_encode(
+			array(
+				'session_id'  => $snapshot['session_id'] ?? '',
+				'cart_hash'   => $snapshot['cart_hash'] ?? '',
+				'total'       => $snapshot['cart_total'] ?? '0.00',
+				'currency'    => get_woocommerce_currency(),
+				'items'       => $items,
+				'customer'    => array(
+					'phone' => $snapshot['phone'] ?? '',
+					'email' => $snapshot['email'] ?? '',
+					'first_name' => $snapshot['first_name'] ?? '',
+				),
+				'checkout_url' => wc_get_checkout_url() . '?cart_hash=' . rawurlencode( $snapshot['cart_hash'] ?? '' ),
+				'abandoned_at' => $snapshot['captured_at'] ?? '',
+				'site_url'    => get_site_url(),
+			)
+		);
+
+		$response = wp_remote_post(
+			MINDLYTICS_API_URL . '/api/integrations/woocommerce/webhooks',
+			array(
+				'headers'    => array(
+					'Content-Type'                => 'application/json',
+					'X-Mindlytics-Webhook-Secret' => $secret,
+					'X-Mindlytics-Shop-Id'        => $shop_id,
+					'X-Mindlytics-Topic'          => 'abandoned_cart',
+					'X-Mindlytics-Delivery-Id'    => $delivery_id,
+				),
+				'body'       => $body,
+				'timeout'    => 15,
+				'sslverify'  => true,
+				'user-agent' => 'Mindlytics-WooCommerce/' . MINDLYTICS_WC_VERSION,
+			)
+		);
+
+		if ( is_wp_error( $response ) ) {
+			return $response;
+		}
+
+		$status_code = wp_remote_retrieve_response_code( $response );
+
+		if ( $status_code < 200 || $status_code >= 300 ) {
+			return new WP_Error(
+				'mindlytics_api_error',
+				sprintf(
+					/* translators: %d: HTTP status code */
+					__( 'Mindlytics API returned HTTP %d.', 'mindlytics-order-notifications-for-woocommerce' ),
+					(int) $status_code
+				)
+			);
+		}
+
+		$decoded = json_decode( wp_remote_retrieve_body( $response ), true );
+		return is_array( $decoded ) ? $decoded : array();
+	}
+}
Index: /mindlytics-order-notifications-for-woocommerce/trunk/includes/class-mindlytics-connect.php
===================================================================
--- /mindlytics-order-notifications-for-woocommerce/trunk/includes/class-mindlytics-connect.php	(revision 3794792)
+++ /mindlytics-order-notifications-for-woocommerce/trunk/includes/class-mindlytics-connect.php	(revision 3794792)
@@ -0,0 +1,155 @@
+<?php
+if ( ! defined( 'ABSPATH' ) ) {
+	exit;
+}
+
+/**
+ * Handles the OAuth connect callback REST endpoint.
+ */
+class Mindlytics_Connect {
+
+	/**
+	 * Constructor — registers hooks.
+	 */
+	public function __construct() {
+		add_action( 'rest_api_init', array( $this, 'handle_rest_api_init' ) );
+	}
+
+	/**
+	 * Register the Mindlytics REST API routes.
+	 *
+	 * @since 1.0.0
+	 * @internal
+	 */
+	public function handle_rest_api_init(): void {
+		register_rest_route(
+			'mindlytics/v1',
+			'/connected',
+			array(
+				'methods'             => WP_REST_Server::CREATABLE,
+				'callback'            => array( $this, 'handle_connected_callback' ),
+				// Called server-to-server by the Mindlytics backend, so there is no
+				// logged-in user; access is gated by the one-time connect state instead.
+				'permission_callback' => array( $this, 'verify_connect_state' ),
+				'args'                => array(
+					'state'          => array(
+						'required' => true,
+						'type'     => 'string',
+					),
+					'shop_id'        => array(
+						'required'          => true,
+						'type'              => 'string',
+						'sanitize_callback' => 'sanitize_text_field',
+						'validate_callback' => array( $this, 'validate_uuid' ),
+					),
+					'webhook_secret' => array(
+						'required'          => true,
+						'type'              => 'string',
+						'sanitize_callback' => array( $this, 'sanitize_hex_secret' ),
+					),
+					'connected'      => array(
+						'required' => true,
+						'type'     => 'boolean',
+					),
+					'shop_name'      => array(
+						'required'          => false,
+						'type'              => 'string',
+						'sanitize_callback' => 'sanitize_text_field',
+					),
+				),
+			)
+		);
+	}
+
+	/**
+	 * Handle the connect callback POST from the Mindlytics backend.
+	 *
+	 * @since 1.0.0
+	 * @param WP_REST_Request $request Full REST request object.
+	 * @return WP_REST_Response|WP_Error
+	 */
+	public function handle_connected_callback( WP_REST_Request $request ) {
+		if ( true !== $request->get_param( 'connected' ) ) {
+			return new WP_Error(
+				'not_connected',
+				__( 'Connection was not approved.', 'mindlytics-order-notifications-for-woocommerce' ),
+				array( 'status' => 400 )
+			);
+		}
+
+		$webhook_secret = $request->get_param( 'webhook_secret' );
+
+		// Enforce 64-char hex (backend generates 32 random bytes → 64 hex chars).
+		if ( ! preg_match( '/^[0-9a-f]{64}$/', $webhook_secret ) ) {
+			return new WP_Error(
+				'invalid_secret',
+				__( 'Invalid webhook secret format.', 'mindlytics-order-notifications-for-woocommerce' ),
+				array( 'status' => 400 )
+			);
+		}
+
+		$shop_id   = $request->get_param( 'shop_id' );
+		$shop_name = $request->get_param( 'shop_name' ) ?: get_bloginfo( 'name' );
+
+		update_option( 'mindlytics_shop_id',        $shop_id );
+		update_option( 'mindlytics_webhook_secret', $webhook_secret );
+		update_option( 'mindlytics_connected_at',   gmdate( 'c' ) );
+		update_option( 'mindlytics_shop_name',      sanitize_text_field( $shop_name ) );
+
+		// Consume pending state nonce.
+		delete_option( 'mindlytics_pending_state' );
+
+		return new WP_REST_Response( array( 'received' => true ), 200 );
+	}
+
+	/**
+	 * Permission check: the request must carry the state issued when the admin
+	 * clicked "Connect". The state is single-use (deleted on success).
+	 *
+	 * @since 1.0.0
+	 * @param WP_REST_Request $request Full REST request object.
+	 * @return true|WP_Error
+	 */
+	public function verify_connect_state( WP_REST_Request $request ) {
+		$expected = (string) get_option( 'mindlytics_pending_state', '' );
+		$received = (string) $request->get_param( 'state' );
+
+		if ( '' === $expected || ! hash_equals( $expected, $received ) ) {
+			return new WP_Error(
+				'invalid_state',
+				__( 'Invalid or expired connection request.', 'mindlytics-order-notifications-for-woocommerce' ),
+				array( 'status' => 403 )
+			);
+		}
+
+		return true;
+	}
+
+	/**
+	 * Validate that a parameter is a valid UUID v4.
+	 *
+	 * @since 1.0.0
+	 * @param string $value The value to validate.
+	 * @return bool
+	 */
+	public function validate_uuid( string $value ): bool {
+		return (bool) preg_match(
+			'/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i',
+			$value
+		);
+	}
+
+	/**
+	 * Sanitize a webhook secret to lowercase hex characters only.
+	 *
+	 * WooCommerce runs wp_specialchars_decode() on webhook secrets before HMAC
+	 * signing. Storing hex-only avoids any HTML entity corruption.
+	 *
+	 * @since 1.0.0
+	 * @param string $value Raw value from request.
+	 * @return string Lowercase hex string.
+	 */
+	public function sanitize_hex_secret( string $value ): string {
+		return strtolower( preg_replace( '/[^0-9a-fA-F]/', '', $value ) );
+	}
+}
Index: /mindlytics-order-notifications-for-woocommerce/trunk/includes/views/settings-page.php
===================================================================
--- /mindlytics-order-notifications-for-woocommerce/trunk/includes/views/settings-page.php	(revision 3794792)
+++ /mindlytics-order-notifications-for-woocommerce/trunk/includes/views/settings-page.php	(revision 3794792)
@@ -0,0 +1,117 @@
+<?php
+if ( ! defined( 'ABSPATH' ) ) {
+	exit;
+}
+
+$mindlytics_is_connected  = ! empty( get_option( 'mindlytics_shop_id' ) ) && ! empty( get_option( 'mindlytics_webhook_secret' ) );
+$mindlytics_shop_name     = esc_html( (string) get_option( 'mindlytics_shop_name', get_bloginfo( 'name' ) ) );
+$mindlytics_connected_at  = get_option( 'mindlytics_connected_at', '' );
+$mindlytics_cart_timeout  = absint( get_option( 'mindlytics_abandoned_cart_timeout', 60 ) );
+?>
+<div class="wrap mindlytics-settings-wrap">
+	<h1><?php esc_html_e( 'Mindlytics WhatsApp', 'mindlytics-order-notifications-for-woocommerce' ); ?></h1>
+
+	<div id="mindlytics-notice" class="notice" hidden></div>
+
+	<div class="mindlytics-connect-card">
+		<?php if ( $mindlytics_is_connected ) : ?>
+
+			<div class="mindlytics-status-row">
+				<span class="mindlytics-status-badge mindlytics-status-connected">
+					<?php esc_html_e( 'Connected', 'mindlytics-order-notifications-for-woocommerce' ); ?>
+				</span>
+				<strong><?php echo esc_html( $mindlytics_shop_name ); ?></strong>
+			</div>
+
+			<?php if ( $mindlytics_connected_at ) : ?>
+				<p class="mindlytics-meta">
+					<?php
+					printf(
+						/* translators: %s: formatted date */
+						esc_html__( 'Connected on %s', 'mindlytics-order-notifications-for-woocommerce' ),
+						esc_html( date_i18n( get_option( 'date_format' ), strtotime( $mindlytics_connected_at ) ) )
+					);
+					?>
+				</p>
+			<?php endif; ?>
+
+			<p>
+				<a href="<?php echo esc_url( MINDLYTICS_APP_URL ); ?>" class="button" target="_blank" rel="noopener">
+					<?php esc_html_e( 'Open dashboard', 'mindlytics-order-notifications-for-woocommerce' ); ?>
+				</a>
+				<button id="mindlytics-disconnect-btn" type="button" class="button">
+					<?php esc_html_e( 'Disconnect', 'mindlytics-order-notifications-for-woocommerce' ); ?>
+				</button>
+			</p>
+
+		<?php else : ?>
+
+			<div class="mindlytics-status-row">
+				<span class="mindlytics-status-badge mindlytics-status-disconnected">
+					<?php esc_html_e( 'Not connected', 'mindlytics-order-notifications-for-woocommerce' ); ?>
+				</span>
+			</div>
+
+			<p><?php esc_html_e( 'Connect your WooCommerce store to Mindlytics to send WhatsApp abandoned cart messages and order notifications.', 'mindlytics-order-notifications-for-woocommerce' ); ?></p>
+
+			<p>
+				<button id="mindlytics-connect-btn" type="button" class="button button-primary">
+					<?php esc_html_e( 'Connect to Mindlytics', 'mindlytics-order-notifications-for-woocommerce' ); ?>
+				</button>
+			</p>
+
+		<?php endif; ?>
+	</div>
+
+	<?php if ( $mindlytics_is_connected ) : ?>
+	<div class="mindlytics-connect-card">
+		<h2><?php esc_html_e( 'Abandoned cart settings', 'mindlytics-order-notifications-for-woocommerce' ); ?></h2>
+		<form id="mindlytics-settings-form">
+			<?php wp_nonce_field( 'mindlytics_admin', 'mindlytics_settings_nonce' ); ?>
+			<table class="form-table" role="presentation">
+				<tr>
+					<th scope="row">
+						<label for="mindlytics-timeout">
+							<?php esc_html_e( 'Abandoned cart delay (minutes)', 'mindlytics-order-notifications-for-woocommerce' ); ?>
+						</label>
+					</th>
+					<td>
+						<input
+							type="number"
+							id="mindlytics-timeout"
+							name="mindlytics_timeout"
+							value="<?php echo esc_attr( (string) $mindlytics_cart_timeout ); ?>"
+							min="30"
+							max="1440"
+							step="1"
+							class="small-text"
+						/>
+						<p class="description">
+							<?php esc_html_e( 'Wait this many minutes after checkout abandonment before sending a WhatsApp message. Minimum 30, maximum 1440 (24 hours).', 'mindlytics-order-notifications-for-woocommerce' ); ?>
+						</p>
+					</td>
+				</tr>
+			</table>
+			<p class="submit">
+				<button type="submit" class="button button-primary">
+					<?php esc_html_e( 'Save changes', 'mindlytics-order-notifications-for-woocommerce' ); ?>
+				</button>
+			</p>
+		</form>
+	</div>
+	<?php endif; ?>
+
+	<div class="mindlytics-connect-card">
+		<details>
+			<summary><?php esc_html_e( 'How to fully disconnect', 'mindlytics-order-notifications-for-woocommerce' ); ?></summary>
+			<div class="mindlytics-details-body">
+				<p><?php esc_html_e( 'Clicking "Disconnect" above removes the connection from this plugin. To also stop WooCommerce from sending order and customer webhooks to Mindlytics:', 'mindlytics-order-notifications-for-woocommerce' ); ?></p>
+				<ol>
+					<li><?php esc_html_e( 'Go to WooCommerce → Settings → Advanced → Webhooks.', 'mindlytics-order-notifications-for-woocommerce' ); ?></li>
+					<li><?php esc_html_e( 'Delete the four webhooks named "Mindlytics — order.created", "Mindlytics — order.updated", "Mindlytics — customer.created", and "Mindlytics — customer.updated".', 'mindlytics-order-notifications-for-woocommerce' ); ?></li>
+					<li><?php esc_html_e( 'Alternatively, you can disconnect from the Mindlytics dashboard under Integrations → WooCommerce, which will remove the webhooks automatically.', 'mindlytics-order-notifications-for-woocommerce' ); ?></li>
+				</ol>
+			</div>
+		</details>
+	</div>
+</div>
Index: /mindlytics-order-notifications-for-woocommerce/trunk/languages/mindlytics-order-notifications-for-woocommerce.pot
===================================================================
--- /mindlytics-order-notifications-for-woocommerce/trunk/languages/mindlytics-order-notifications-for-woocommerce.pot	(revision 3794792)
+++ /mindlytics-order-notifications-for-woocommerce/trunk/languages/mindlytics-order-notifications-for-woocommerce.pot	(revision 3794792)
@@ -0,0 +1,137 @@
+# Copyright (C) 2026 Mindlytics
+# This file is distributed under the GPL v2 or later.
+msgid ""
+msgstr ""
+"Project-Id-Version: Mindlytics – Order Notifications for WooCommerce 1.0.0\n"
+"Report-Msgid-Bugs-To: https://mindlytics.in\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: LANGUAGE <LL@li.org>\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"POT-Creation-Date: 2026-05-06T00:00:00+00:00\n"
+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
+"X-Generator: WP-CLI 2.x\n"
+"X-Domain: mindlytics-order-notifications-for-woocommerce\n"
+
+#. Plugin Name of the plugin
+msgid "WhatsApp Notifications for WooCommerce - Mindlytics"
+msgstr ""
+
+#. Plugin URI of the plugin
+msgid "https://mindlytics.in"
+msgstr ""
+
+#. Description of the plugin
+msgid "Connect your WooCommerce store to Mindlytics for WhatsApp abandoned cart recovery and order notifications."
+msgstr ""
+
+#. Author of the plugin
+msgid "Mindlytics"
+msgstr ""
+
+#: mindlytics-woocommerce.php
+msgid "Mindlytics WhatsApp requires WordPress 6.4 or higher."
+msgstr ""
+
+#: mindlytics-woocommerce.php
+msgid "Mindlytics WhatsApp requires WooCommerce to be installed and active."
+msgstr ""
+
+#: includes/class-mindlytics-admin.php
+msgid "Mindlytics"
+msgstr ""
+
+#: includes/class-mindlytics-admin.php
+msgid "Unauthorized."
+msgstr ""
+
+#: includes/class-mindlytics-connect.php
+msgid "Connection was not approved."
+msgstr ""
+
+#: includes/class-mindlytics-connect.php
+msgid "Invalid webhook secret format."
+msgstr ""
+
+#: includes/class-mindlytics-api.php
+#. translators: %d: HTTP status code
+msgid "Mindlytics API returned HTTP %d."
+msgstr ""
+
+#: includes/class-mindlytics-abandoned-cart.php
+msgid "Every 5 minutes"
+msgstr ""
+
+#: includes/views/settings-page.php
+msgid "Mindlytics WhatsApp"
+msgstr ""
+
+#: includes/views/settings-page.php
+msgid "Connected"
+msgstr ""
+
+#: includes/views/settings-page.php
+#. translators: %s: formatted date
+msgid "Connected on %s"
+msgstr ""
+
+#: includes/views/settings-page.php
+msgid "Open dashboard"
+msgstr ""
+
+#: includes/views/settings-page.php
+msgid "Disconnect"
+msgstr ""
+
+#: includes/views/settings-page.php
+msgid "Not connected"
+msgstr ""
+
+#: includes/views/settings-page.php
+msgid "Connect your WooCommerce store to Mindlytics to send WhatsApp abandoned cart messages and order notifications."
+msgstr ""
+
+#: includes/views/settings-page.php
+msgid "Connect to Mindlytics"
+msgstr ""
+
+#: includes/views/settings-page.php
+msgid "Abandoned cart settings"
+msgstr ""
+
+#: includes/views/settings-page.php
+msgid "Abandoned cart delay (minutes)"
+msgstr ""
+
+#: includes/views/settings-page.php
+msgid "Wait this many minutes after checkout abandonment before sending a WhatsApp message. Minimum 30, maximum 1440 (24 hours)."
+msgstr ""
+
+#: includes/views/settings-page.php
+msgid "Save changes"
+msgstr ""
+
+#: includes/views/settings-page.php
+msgid "How to fully disconnect"
+msgstr ""
+
+#: includes/views/settings-page.php
+msgid "Clicking \"Disconnect\" above removes the connection from this plugin. To also stop WooCommerce from sending order and customer webhooks to Mindlytics:"
+msgstr ""
+
+#: includes/views/settings-page.php
+msgid "Go to WooCommerce → Settings → Advanced → Webhooks."
+msgstr ""
+
+#: includes/views/settings-page.php
+msgid "Delete the four webhooks named \"Mindlytics — order.created\", \"Mindlytics — order.updated\", \"Mindlytics — customer.created\", and \"Mindlytics — customer.updated\"."
+msgstr ""
+
+#: includes/views/settings-page.php
+msgid "Alternatively, you can disconnect from the Mindlytics dashboard under Integrations → WooCommerce, which will remove the webhooks automatically."
+msgstr ""
+
+#: includes/views/settings-page.php
+msgid "You do not have sufficient permissions to access this page."
+msgstr ""
Index: /mindlytics-order-notifications-for-woocommerce/trunk/mindlytics-woocommerce.php
===================================================================
--- /mindlytics-order-notifications-for-woocommerce/trunk/mindlytics-woocommerce.php	(revision 3794792)
+++ /mindlytics-order-notifications-for-woocommerce/trunk/mindlytics-woocommerce.php	(revision 3794792)
@@ -0,0 +1,131 @@
+<?php
+/**
+ * Plugin Name:       Mindlytics – Order Notifications for WooCommerce
+ * Plugin URI:        https://app.mindlytics.in/integrations/woocommerce
+ * Description:       Connect your WooCommerce store to Mindlytics for WhatsApp abandoned cart recovery and order notifications.
+ * Version:           1.0.0
+ * Requires at least: 6.4
+ * Requires PHP:      7.4
+ * Requires Plugins:  woocommerce
+ * Author:            Mindlytics
+ * Author URI:        https://mindlytics.in
+ * License:           GPL-2.0-or-later
+ * License URI:       https://www.gnu.org/licenses/gpl-2.0.html
+ * Text Domain:       mindlytics-order-notifications-for-woocommerce
+ * WC requires at least: 5.0
+ * WC tested up to:   9.0
+ */
+
+if ( ! defined( 'ABSPATH' ) ) {
+	exit;
+}
+
+define( 'MINDLYTICS_WC_VERSION', '1.0.0' );
+define( 'MINDLYTICS_WC_FILE',    __FILE__ );
+define( 'MINDLYTICS_WC_DIR',     plugin_dir_path( __FILE__ ) );
+define( 'MINDLYTICS_WC_URL',     plugin_dir_url( __FILE__ ) );
+
+define( 'MINDLYTICS_API_URL', 'https://wbp-api.mindlytics.in' );
+define( 'MINDLYTICS_APP_URL', 'https://app.mindlytics.in' );
+
+/**
+ * Main plugin class.
+ */
+class Mindlytics_WooCommerce {
+
+	/**
+	 * Singleton instance.
+	 *
+	 * @var self|null
+	 */
+	private static ?self $_instance = null;
+
+	/**
+	 * Get the singleton instance.
+	 *
+	 * @since 1.0.0
+	 * @return self
+	 */
+	public static function instance(): self {
+		if ( null === self::$_instance ) {
+			self::$_instance = new self();
+		}
+		return self::$_instance;
+	}
+
+	/**
+	 * Constructor — loads text domain and registers the plugins_loaded hook.
+	 */
+	private function __construct() {
+		add_action( 'plugins_loaded', array( $this, 'handle_plugins_loaded' ) );
+	}
+
+	/**
+	 * Boot the plugin once all plugins are loaded.
+	 *
+	 * @since 1.0.0
+	 * @internal
+	 */
+	public function handle_plugins_loaded(): void {
+		if ( version_compare( $GLOBALS['wp_version'], '6.4', '<' ) ) {
+			add_action(
+				'admin_notices',
+				static function () {
+					echo '<div class="notice notice-error"><p>' .
+						esc_html__( 'Mindlytics WhatsApp requires WordPress 6.4 or higher.', 'mindlytics-order-notifications-for-woocommerce' ) .
+						'</p></div>';
+				}
+			);
+			return;
+		}
+
+		if ( ! class_exists( 'WooCommerce' ) ) {
+			add_action(
+				'admin_notices',
+				static function () {
+					echo '<div class="notice notice-error"><p>' .
+						esc_html__( 'Mindlytics WhatsApp requires WooCommerce to be installed and active.', 'mindlytics-order-notifications-for-woocommerce' ) .
+						'</p></div>';
+				}
+			);
+			return;
+		}
+
+		$this->includes();
+
+		new Mindlytics_Admin();
+		new Mindlytics_Connect();
+		new Mindlytics_Abandoned_Cart();
+
+		// Scheduled on init rather than on activation: the custom interval is only
+		// registered once the class above has hooked cron_schedules, and its label
+		// is translated (translations must not load before init). No-op when the
+		// event already exists.
+		add_action( 'init', array( 'Mindlytics_Abandoned_Cart', 'schedule_cron' ) );
+	}
+
+	/**
+	 * Require all plugin class files.
+	 *
+	 * @since 1.0.0
+	 */
+	private function includes(): void {
+		require_once MINDLYTICS_WC_DIR . 'includes/class-mindlytics-api.php';
+		require_once MINDLYTICS_WC_DIR . 'includes/class-mindlytics-connect.php';
+		require_once MINDLYTICS_WC_DIR . 'includes/class-mindlytics-abandoned-cart.php';
+		require_once MINDLYTICS_WC_DIR . 'includes/class-mindlytics-admin.php';
+	}
+
+	/**
+	 * Plugin deactivation handler.
+	 *
+	 * @since 1.0.0
+	 */
+	public static function deactivate(): void {
+		wp_clear_scheduled_hook( 'mindlytics_check_abandoned_carts' );
+	}
+}
+
+register_deactivation_hook( MINDLYTICS_WC_FILE, array( 'Mindlytics_WooCommerce', 'deactivate' ) );
+
+Mindlytics_WooCommerce::instance();
Index: /mindlytics-order-notifications-for-woocommerce/trunk/readme.txt
===================================================================
--- /mindlytics-order-notifications-for-woocommerce/trunk/readme.txt	(revision 3794792)
+++ /mindlytics-order-notifications-for-woocommerce/trunk/readme.txt	(revision 3794792)
@@ -0,0 +1,87 @@
+=== Mindlytics – Order Notifications for WooCommerce ===
+Contributors: mindlyticsdev
+Tags: whatsapp, woocommerce, abandoned-cart, order-notification, whatsapp-business
+Requires at least: 6.4
+Tested up to: 7.1
+Requires PHP: 7.4
+WC requires at least: 5.0
+WC tested up to: 9.0
+Stable tag: 1.0.0
+License: GPL-2.0-or-later
+License URI: https://www.gnu.org/licenses/gpl-2.0.html
+
+Connect your WooCommerce store to Mindlytics for WhatsApp abandoned cart recovery and order notifications.
+
+== Description ==
+
+**Mindlytics WhatsApp for WooCommerce** connects your store to the Mindlytics platform so you can send automated WhatsApp messages to your customers.
+
+**Features:**
+
+* **Abandoned cart recovery** — automatically detects when a logged-in customer leaves the checkout page and sends a WhatsApp reminder after a configurable delay.
+* **Order notifications** — sends WhatsApp messages on order creation, shipping, delivery, and cancellation (configured in the Mindlytics dashboard).
+* **One-click connect** — no API keys to copy. Click "Connect to Mindlytics", log in to your Mindlytics account, approve access, and you're done.
+* **Template mapping** — choose which WhatsApp message template fires for each event in the Mindlytics dashboard.
+
+**Requirements:**
+
+* WordPress 6.4 or higher
+* WooCommerce 5.0 or higher
+* A Mindlytics account (free trial available at mindlytics.in)
+
+== Installation ==
+
+1. Upload the plugin folder to `/wp-content/plugins/` or install via Plugins > Add new plugin.
+2. Activate the plugin via Plugins > Installed plugins.
+3. Go to WooCommerce > Mindlytics.
+4. Click "Connect to Mindlytics" and follow the steps.
+
+== Frequently Asked Questions ==
+
+= Does this plugin work without a Mindlytics account? =
+
+No. You need a Mindlytics account to use this plugin. Create one at mindlytics.in.
+
+= Does the plugin send WhatsApp messages directly? =
+
+No. The plugin forwards events (abandoned carts) to the Mindlytics backend, which sends WhatsApp messages using the Meta WhatsApp Business API. Order events are forwarded by native WooCommerce webhooks registered at connect time.
+
+= Why do I need to delete webhooks manually when disconnecting? =
+
+When you connect, Mindlytics registers four native WooCommerce webhooks on your store. Disconnecting from the plugin clears the local connection, but the WC webhooks remain until deleted from WooCommerce > Settings > Advanced > Webhooks — or disconnected from the Mindlytics dashboard, which removes them automatically.
+
+= Does this plugin support guest checkouts? =
+
+Abandoned cart detection requires a phone number. For guest checkouts, the phone number is only available after the customer enters it on the checkout form. Guests who abandon before typing their phone number will not receive a WhatsApp message.
+
+= What is the minimum abandoned cart delay? =
+
+30 minutes. The default is 60 minutes. The maximum is 1440 minutes (24 hours).
+
+= What external services does this plugin use? =
+
+This plugin connects to **Mindlytics** (mindlytics.in) to send WhatsApp messages. Specifically:
+
+* During connect, your browser is redirected to the Mindlytics app to authenticate your account.
+* Abandoned cart events are sent to the Mindlytics backend for processing.
+* Order and customer data is forwarded via WooCommerce native webhooks to the Mindlytics backend.
+
+See the [Mindlytics Privacy Policy](https://mindlytics.in/privacy-policy) for how data is handled.
+
+No data is sent to external services until you explicitly connect your store.
+
+== Screenshots ==
+
+1. Settings page — not connected state with connect button.
+2. Settings page — connected state showing store name and disconnect option.
+3. Mindlytics dashboard showing WooCommerce template mappings.
+4. Example WhatsApp abandoned cart message received by a customer.
+
+== Changelog ==
+
+= 1.0.0 =
+* Initial release.
+* One-click OAuth connect flow.
+* Abandoned cart detection via WP Cron.
+* Configurable abandonment delay (30–1440 minutes).
+* WooCommerce > Mindlytics admin menu.
