PluginProbe
Search Atlas SEO – OTTO AI SEO Automation for WordPress / 2.6.9
Search Atlas SEO – OTTO AI SEO Automation for WordPress v2.6.9
2.6.26 2.6.25 2.6.24 2.6.23 2.6.22 2.6.21 2.6.20 2.6.19 2.6.18 2.6.17 2.6.16 2.6.15 2.6.14 2.6.13 2.6.12 2.6.11 2.6.10 2.6.9 2.6.8 2.6.7 2.6.6 2.6.5 2.6.4 2.6.3 2.5.23 All 138 releases
metasync / admin / js / metasync-admin.js

metasync-admin.js in Search Atlas SEO – OTTO AI SEO Automation for WordPress 2.6.9, at admin/js/metasync-admin.js

3,235 lines 108.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 (function ($) {
2 'use strict';
3
4 /**
5 * All of the code for your admin-facing JavaScript source
6 * should reside in this file.
7 *
8 * Note: It has been assumed you will write jQuery code here, so the
9 * $ function reference has been prepared for usage within the scope
10 * of this function.
11 *
12 * This enables you to define handlers, for when the DOM is ready:
13 *
14 * $(function() {
15 *
16 * });
17 *
18 * When the window is loaded:
19 *
20 * $( window ).load(function() {
21 *
22 * });
23 *
24 * ...and/or other possibilities.
25 *
26 * Ideally, it is not considered best practise to attach more than a
27 * single DOM-ready or window-load handler for a particular page.
28 * Although scripts in the WordPress core, Plugins and Themes may be
29 * practising this, we should strive to set a better example in our own work.
30 */
31
32 // ========================================
33 // UTILITY FUNCTIONS (Consolidated to reduce duplication)
34 // ========================================
35
36 /**
37 * Get plugin name from config or use default
38 * @returns {string} Plugin name
39 */
40 function getPluginName() {
41 return window.MetasyncConfig && window.MetasyncConfig.pluginName ? window.MetasyncConfig.pluginName : 'Search Atlas';
42 }
43
44 /**
45 * Get OTTO name from config or use default
46 * @returns {string} OTTO name
47 */
48 function getOttoName() {
49 return window.MetasyncConfig && window.MetasyncConfig.ottoName ? window.MetasyncConfig.ottoName : 'OTTO';
50 }
51
52 /**
53 * Update integration status indicator in header
54 * @param {boolean} isIntegrated - Whether the integration is active
55 * @param {string} statusText - Status text to display
56 * @param {string} titleText - Tooltip text
57 */
58 function updateHeaderStatus(isIntegrated, statusText, titleText) {
59 var $statusIndicator = $('.metasync-integration-status');
60 if ($statusIndicator.length > 0) {
61 $statusIndicator.removeClass('integrated not-integrated warning');
62 if (statusText === 'Warning') {
63 $statusIndicator.addClass('warning');
64 } else if (isIntegrated) {
65 $statusIndicator.addClass('integrated');
66 } else {
67 $statusIndicator.addClass('not-integrated');
68 }
69 $statusIndicator.find('.status-text').text(statusText);
70 $statusIndicator.attr('title', titleText);
71 console.log('🔄 Updated header status to: ' + statusText);
72 }
73
74 // Keep compact header badge in sync with integration status
75 var $badge = $('.metasync-status');
76 if ($badge.length > 0) {
77 $badge.removeClass('connected disconnected warning');
78 if (isIntegrated) {
79 $badge.addClass('connected');
80 $badge.find('.status-text').text('Connected');
81 } else if (statusText === 'Warning') {
82 $badge.addClass('warning');
83 $badge.find('.status-text').text('Warning');
84 } else {
85 $badge.addClass('disconnected');
86 $badge.find('.status-text').text('Not Connected');
87 }
88 }
89
90 // Immediately sync admin bar toolbar status
91 var $adminBarContainer = $('#wp-admin-bar-searchatlas-status');
92 var $adminBarItem = $adminBarContainer.find('.ab-item');
93 if ($adminBarItem.length > 0) {
94 var allClasses = 'searchatlas-synced searchatlas-not-synced searchatlas-warning';
95 var pluginName = (window.MetasyncConfig && window.MetasyncConfig.pluginName) || 'SearchAtlas';
96
97 var targetEmoji, targetSvgCode, barClass, barTitle;
98 if (isIntegrated) {
99 targetEmoji = '\uD83D\uDFE2'; targetSvgCode = '1f7e2';
100 barClass = 'searchatlas-synced';
101 barTitle = pluginName + ' - Synced';
102 } else if (statusText === 'Warning') {
103 targetEmoji = '\uD83D\uDFE1'; targetSvgCode = '1f7e1';
104 barClass = 'searchatlas-warning';
105 barTitle = pluginName + ' - ' + titleText;
106 } else {
107 targetEmoji = '\uD83D\uDD34'; targetSvgCode = '1f534';
108 barClass = 'searchatlas-not-synced';
109 barTitle = pluginName + ' - ' + (titleText || 'Not Synced');
110 }
111
112 // Update emoji (SVG image or text fallback)
113 var emojiImg = $adminBarItem.find('img.emoji');
114 if (emojiImg.length > 0) {
115 emojiImg.attr('alt', targetEmoji);
116 emojiImg.attr('src', emojiImg.attr('src').replace(/1f7e2\.svg|1f534\.svg|1f7e1\.svg/, targetSvgCode + '.svg'));
117 } else {
118 var newHtml = $adminBarItem.html().replace(/\uD83D\uDFE2|\uD83D\uDD34|\uD83D\uDFE1/, targetEmoji);
119 if (!newHtml.includes(targetEmoji) && newHtml.includes(pluginName)) {
120 newHtml = newHtml.replace(pluginName, pluginName + ' ' + targetEmoji);
121 }
122 $adminBarItem.html(newHtml);
123 }
124
125 $adminBarContainer.removeClass(allClasses).addClass(barClass);
126 $adminBarContainer.attr('title', barTitle);
127 $adminBarItem.attr('title', barTitle);
128 }
129 }
130
131 /**
132 * Collect whitelabel form fields for submission
133 * @returns {string} Serialized whitelabel field data
134 */
135 function collectWhitelabelFields() {
136 var whitelabelFields = [];
137
138 // Text/URL fields
139 var logoField = $('input[name="metasync_options[whitelabel][logo]"]');
140 var domainField = $('input[name="metasync_options[whitelabel][domain]"]');
141 var passwordField = $('input[name="metasync_options[whitelabel][settings_password]"]');
142
143 if (logoField.length > 0 && logoField.val()) {
144 whitelabelFields.push('metasync_options[whitelabel][logo]=' + encodeURIComponent(logoField.val()));
145 }
146 if (domainField.length > 0 && domainField.val()) {
147 whitelabelFields.push('metasync_options[whitelabel][domain]=' + encodeURIComponent(domainField.val()));
148 }
149 if (passwordField.length > 0 && passwordField.val()) {
150 whitelabelFields.push('metasync_options[whitelabel][settings_password]=' + encodeURIComponent(passwordField.val()));
151 }
152
153 // Checkbox fields (handle both checked and unchecked)
154 var hideFields = ['hide_dashboard', 'hide_settings', 'hide_indexation_control',
155 'hide_redirections', 'hide_robots', 'hide_sync_log',
156 'hide_compatibility', 'hide_advanced'];
157
158 hideFields.forEach(function (fieldName) {
159 var checkbox = $('input[name="metasync_options[whitelabel][' + fieldName + ']"]');
160 if (checkbox.length > 0) {
161 var value = checkbox.is(':checked') ? '1' : '0';
162 whitelabelFields.push('metasync_options[whitelabel][' + fieldName + ']=' + value);
163 }
164 });
165
166 return whitelabelFields.join('&');
167 }
168
169 /**
170 * Display notice message in plugin area
171 * @param {string} type - Notice type: 'success' or 'error'
172 * @param {string} title - Notice title
173 * @param {string} message - Notice message
174 * @param {string} cssClass - Additional CSS class for the notice
175 * @param {number} autoHideDelay - Auto-hide delay in ms (0 = no auto-hide)
176 */
177 function showPluginNotice(type, title, message, cssClass, autoHideDelay) {
178 cssClass = cssClass || 'metasync-notice';
179 autoHideDelay = autoHideDelay || 0;
180
181 var noticeClass = type === 'success' ? 'notice-success' : 'notice-error';
182 var noticeHTML = '<div class="notice ' + noticeClass + ' is-dismissible ' + cssClass + '" style="margin: 20px 0; padding: 12px;">' +
183 '<p><strong>' + title + '</strong><br/>' + message + '</p>' +
184 '</div>';
185
186 // Remove existing notices of same class
187 $('.' + cssClass).remove();
188
189 // Insert between navigation menu and page content
190 var $navWrapper = $('.metasync-nav-wrapper');
191 if ($navWrapper.length > 0) {
192 $navWrapper.after(noticeHTML);
193 } else {
194 $('.metasync-dashboard-wrap').prepend(noticeHTML);
195 }
196
197 // Scroll to the top to ensure visibility
198 $('html, body').animate({ scrollTop: 0 }, 'slow');
199
200 // Auto-hide if delay specified
201 if (autoHideDelay > 0) {
202 setTimeout(function () {
203 $('.' + cssClass).fadeOut(300, function () {
204 $(this).remove();
205 });
206 }, autoHideDelay);
207 }
208 }
209
210 /**
211 * Prevent dashboard.js interference with button
212 * @param {jQuery} $button - Button element to protect
213 */
214 function preventDashboardInterference($button) {
215 $button.removeClass('dashboard-loading');
216 $button.addClass('no-loading metasync-sa-connect-protected');
217 $button.prop('disabled', false);
218 }
219
220 // ========================================
221 // ORIGINAL FUNCTIONS
222 // ========================================
223
224 function metasync_syncPostsAndPages() {
225 wp.ajax.post('metasync_send_customer_params', {})
226 .done(function (response) {
227 console.log(response);
228 });
229 }
230
231 function metasyncGenerateAPIKey() {
232 return Math.random().toString(36).substring(2, 15) +
233 Math.random().toString(36).substring(2, 15);
234 }
235
236 function metasyncLGLogin(user, pass) {
237 jQuery.post(ajaxurl, {
238 action: 'metasync_lglogin',
239 username: user, password: pass
240 }, function (response) {
241 if (typeof response.token !== 'undefined') {
242 $('#linkgraph_token').val(response.token);
243 $('#linkgraph_customer_id').val(response.customer_id);
244 $('.input.lguser,#lgerror').addClass('hidden');
245 localStorage.setItem('token', response.token);
246 } else {
247 $('#lgerror').text(response.detail + ' (' + response.kind + ')').removeClass('hidden');
248 }
249 }
250 );
251 }
252
253 function setToken() {
254 if ($('#linkgraph_token') && $('#linkgraph_token').val()) {
255 localStorage.setItem('token', $('#linkgraph_token').val());
256 }
257 }
258
259 // Search Atlas Connect functions
260 // Handles 1-click authentication to retrieve Search Atlas API key and Otto UUID.
261 // Does NOT create WordPress login sessions.
262 var saConnectPollingInterval = null;
263 var saConnectWindow = null;
264
265 function handleSearchAtlasConnect() {
266 var $button = $('#connect-searchatlas-btn');
267 // Only check for button (status/progress elements are created dynamically)
268 if (!$button.length) {
269 return;
270 }
271
272 // Enhanced loading state with spinner and CSS class (prevent dashboard.js conflicts)
273 $button.prop('disabled', true)
274 .addClass('connecting no-loading') // Add 'no-loading' to prevent dashboard.js interference
275 .removeClass('dashboard-loading') // Remove any existing dashboard loading
276 .html('<span class="metasync-sa-connect-loading"></span> Initializing...');
277
278 // Hide any existing status/progress containers (may not exist yet)
279 $('#sa-connect-status-message').hide();
280 $('.metasync-sa-connect-progress').hide();
281
282 // Initialize progress display immediately (no separate status message)
283 initializeProgressDisplay();
284
285 // Generate nonce for WordPress AJAX security
286 var ajaxNonce = metaSync.sa_connect_nonce || '';
287 if (!ajaxNonce) {
288 return;
289 }
290
291 // Make AJAX call to generate SSO URL
292 var ajaxUrl = ajaxurl || metaSync.ajax_url;
293
294
295 $.ajax({
296 url: ajaxUrl,
297 type: 'POST',
298 data: {
299 action: 'metasync_generate_connect_url',
300 nonce: ajaxNonce
301 },
302 timeout: 30000, // 30 second timeout
303 success: function (response) {
304
305
306 if (response.success) {
307
308 // Update button state
309 $button.removeClass('connecting dashboard-loading')
310 .addClass('authenticating no-loading') // Maintain no-loading class
311 .html('<span class="metasync-sa-connect-loading"></span> Opening Authentication...');
312
313
314
315 // Small delay for better UX (let user see the message)
316 setTimeout(function () {
317 console.log('🔍 Opening connect popup with URL:', response.data.connect_url);
318
319 // Detect mobile device for better experience
320 var isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
321 var windowFeatures;
322
323
324
325 if (isMobile) {
326 // On mobile, open in same tab for better experience
327 showConnectInfo('📱 Mobile Authentication',
328 'Opening ' + getPluginName() + ' authentication. You\'ll be redirected back after logging in.');
329 window.location.href = response.data.connect_url;
330 return;
331 } else {
332 // Desktop: enhanced popup window
333 var screenWidth = window.screen.width;
334 var screenHeight = window.screen.height;
335 var windowWidth = Math.min(650, screenWidth * 0.8);
336 var windowHeight = Math.min(750, screenHeight * 0.8);
337 var left = (screenWidth - windowWidth) / 2;
338 var top = (screenHeight - windowHeight) / 2;
339
340 windowFeatures = 'width=' + windowWidth +
341 ',height=' + windowHeight +
342 ',left=' + left +
343 ',top=' + top +
344 ',scrollbars=yes,resizable=yes,toolbar=no,location=yes,status=yes';
345
346
347 }
348
349
350
351 // Open SSO URL in popup with enhanced window properties
352 saConnectWindow = window.open(
353 response.data.connect_url,
354 'searchatlas-connect',
355 windowFeatures
356 );
357
358
359
360 // Enhanced popup blocked detection
361 setTimeout(function () {
362 if (!saConnectWindow || saConnectWindow.closed || typeof saConnectWindow.closed === 'undefined') {
363 showConnectError('🚫 Popup Blocked',
364 'Your browser blocked the authentication popup. Please allow popups for this site and try again.',
365 [{
366 text: '🔄 Try Again',
367 action: function () {
368 handleSearchAtlasConnect();
369 }
370 }, {
371 text: '📝 How to Enable Popups',
372 action: function () {
373 showPopupHelp();
374 }
375 }, {
376 text: '🖥️ Open in New Tab',
377 action: function () {
378 window.open(response.data.connect_url, '_blank');
379 startSearchAtlasPolling(response.data.nonce_token);
380 }
381 }]
382 );
383 resetConnectButton();
384 return;
385 }
386
387 // Add focus to popup window
388 try {
389 saConnectWindow.focus();
390 } catch(e) {
391 // Ignore focus errors
392 }
393
394 // Update progress display and start polling
395 updateProgress(10, 1, 6); // Show initial progress
396 console.log('🔍 Starting connect polling...');
397 setTimeout(function () {
398 startSearchAtlasPolling(response.data.nonce_token);
399 }, 200);
400
401 }, 100); // Small delay to let popup settle
402
403 }, 500); // 500ms delay for better UX
404
405 } else {
406 var errorMessage = response.data.message || 'Failed to generate connect URL';
407 showConnectError('❌ Connection Failed',
408 errorMessage,
409 [{
410 text: '🔄 Retry Connection',
411 action: function () {
412 handleSearchAtlasConnect();
413 }
414 }]
415 );
416 resetConnectButton();
417 }
418 },
419 error: function (xhr, status, error) {
420 console.error('🐛 DEBUG: AJAX error occurred:', {
421 xhr: xhr,
422 status: status,
423 error: error,
424 responseText: xhr.responseText,
425 responseJSON: xhr.responseJSON,
426 readyState: xhr.readyState,
427 ajaxUrl: ajaxUrl
428 });
429
430 var errorMessage = 'Network error occurred while connecting to ' + getPluginName();
431
432 // Provide specific error messages based on the error type
433 if (status === 'timeout') {
434 errorMessage = 'Request timed out. Please check your internet connection and try again.';
435 } else if (status === 'error') {
436 if (xhr.status === 0) {
437 errorMessage = 'Unable to connect. Please check if WordPress admin-ajax.php is accessible.';
438 } else if (xhr.status === 403) {
439 errorMessage = 'Access denied. Please refresh the page and try again.';
440 } else if (xhr.status === 500) {
441 errorMessage = 'Server error occurred. Please check server logs for details.';
442 } else {
443 errorMessage = 'HTTP Error ' + xhr.status + ': ' + xhr.statusText;
444 }
445 } else if (xhr.responseJSON && xhr.responseJSON.message) {
446 errorMessage = xhr.responseJSON.message;
447 }
448
449 showConnectError('🌐 Network Error', errorMessage,
450 [{
451 text: '🔄 Retry Connection',
452 action: function () {
453 handleSearchAtlasConnect();
454 }
455 }, {
456 text: '🔧 Check Network',
457 action: function () {
458 console.log('Network diagnostics:', {
459 status: status,
460 error: error,
461 xhr: xhr
462 });
463 }
464 }]
465 );
466 resetConnectButton();
467 }
468 });
469 }
470
471 function resetConnectButton() {
472 var $button = $('#connect-searchatlas-btn');
473 var $progressContainer = $('.metasync-sa-connect-progress');
474 var hasApiKey = $('#searchatlas-api-key').val().trim() !== '';
475
476 $button.prop('disabled', false)
477 .removeClass('connecting authenticating success dashboard-loading') // Remove all loading classes
478 .html(hasApiKey ? '🔄 Re-authenticate with ' + getPluginName() : '🔗 Connect to ' + getPluginName());
479 $progressContainer.hide();
480 }
481
482 function startSearchAtlasPolling(nonceToken) {
483 var pollCount = 0;
484 var maxPolls = 12; // Poll for 60 seconds (12 * 5 seconds)
485 var $progressContainer = $('.metasync-sa-connect-progress');
486 var $progressFill = $('.metasync-sa-connect-progress-fill');
487 var $progressText = $('.metasync-sa-connect-progress-text');
488 var $button = $('#connect-searchatlas-btn');
489
490 // Progress display should already be initialized, just update it
491 updateProgress(0, 0, maxPolls);
492
493
494 saConnectPollingInterval = setInterval(function () {
495 pollCount++;
496
497
498 // Update progress bar
499 var progress = Math.min((pollCount / maxPolls) * 100, 100);
500 updateProgress(progress, pollCount, maxPolls);
501
502 // Check if window was closed manually - but continue polling
503 if (saConnectWindow && saConnectWindow.closed) {
504 // Popup closed, but continue polling to check for authentication success
505 console.log('🔍 Connect popup closed, continuing to poll for authentication success...');
506 saConnectWindow = null; // Clear reference to closed window
507
508 // Update UI to show we're still checking
509 updateProgress(75, pollCount, maxPolls);
510 $progressText.text('Popup closed - checking authentication status...');
511
512 // Continue polling - don't return, let the polling continue
513 }
514
515 // Stop polling after max attempts
516 if (pollCount >= maxPolls) {
517 stopSearchAtlasPolling();
518 if (saConnectWindow) {
519 saConnectWindow.close();
520 }
521
522 // �
523 Reset the authentication flow while keeping the timeout component
524 resetConnectButton();
525
526 showConnectError('⏰ Authentication Timeout',
527 'The authentication process timed out after 60 seconds. Please complete the authentication more quickly or check for network issues.',
528 [{
529 text: '🔄 Try Again',
530 action: function () {
531 handleSearchAtlasConnect();
532 }
533 }, {
534 text: '💬 Contact Support',
535 action: function () {
536 var supportEmail = metaSync.support_email || 'support@searchatlas.com';
537 window.open('mailto:' + supportEmail + '?subject=Connect Authentication Timeout (30s)', '_blank');
538 }
539 }]
540 );
541 return;
542 }
543
544 // Update button state periodically
545 if (pollCount % 2 === 0) { // Every 10 seconds
546 var timeLeft = Math.ceil((maxPolls - pollCount) * 5);
547 $button.html('<span class="metasync-sa-connect-loading"></span> Waiting for Authentication (' + timeLeft + 's left)');
548 }
549
550 // Check if API key was updated
551 $.ajax({
552 url: ajaxurl,
553 type: 'POST',
554 data: {
555 action: 'metasync_check_connect_status',
556 nonce: metaSync.sa_connect_nonce || '',
557 nonce_token: nonceToken
558 },
559 success: function (response) {
560 if (response.success && response.data.updated) {
561 stopSearchAtlasPolling();
562 if (saConnectWindow) {
563 saConnectWindow.close();
564 }
565
566 // Show completion animation
567 updateProgress(100, maxPolls, maxPolls);
568
569 var statusCode = response.data.status_code || 200;
570
571 // Handle different status codes with enhanced UX
572 if (statusCode === 200) {
573 // Success: Update all UI elements to reflect connected state
574 updateUIForConnectedState(response.data.api_key, response.data.otto_pixel_uuid);
575
576 // Refresh Plugin Auth Token field to show the auto-generated token
577 // This will either show the existing token or the newly auto-generated one
578 $.ajax({
579 url: ajaxurl,
580 type: 'POST',
581 data: {
582 action: 'metasync_get_plugin_auth_token',
583 nonce: metaSync.sa_connect_nonce || ''
584 },
585 success: function (tokenResponse) {
586 if (tokenResponse.success && tokenResponse.data.plugin_auth_token) {
587 $('#apikey').val(tokenResponse.data.plugin_auth_token);
588 console.log('🔑 Plugin Auth Token field updated after connect success');
589 }
590 },
591 error: function () {
592 console.log('⚠️ Could not refresh Plugin Auth Token field, but connect authentication was successful');
593 }
594 });
595
596 $button.removeClass('connecting authenticating dashboard-loading')
597 .addClass('success no-loading')
598 .html('�
599 Authentication Complete!');
600
601 // Add success animation to container
602 $button.closest('.metasync-sa-connect-container').addClass('metasync-sa-connect-success-animation');
603 setTimeout(function () {
604 $button.closest('.metasync-sa-connect-container').removeClass('metasync-sa-connect-success-animation');
605 }, 600);
606
607 // Track 1-click activation in GA4
608 var hasExistingApiKey = $('#searchatlas-api-key').val() && $('#searchatlas-api-key').val().trim() !== '';
609 $.ajax({
610 url: metaSync.ajax_url,
611 type: 'POST',
612 data: {
613 action: 'metasync_track_one_click_activation',
614 nonce: metaSync.nonce,
615 auth_method: 'searchatlas_connect',
616 is_reconnection: hasExistingApiKey
617 }
618 });
619 if (typeof window.metasyncGA4Track === 'function') {
620 window.metasyncGA4Track('one_click_activation', {
621 auth_method: 'searchatlas_connect',
622 is_reconnection: hasExistingApiKey
623 });
624 }
625
626 showConnectSuccess('🎉 Authentication Successful',
627 'Your ' + getPluginName() + ' account has been synced successfully! The page will reload to apply your new settings.',
628 [{
629 text: '🔄 Reload Now',
630 action: function () {
631 location.reload();
632 }
633 }]
634 );
635
636 // Auto-reload with countdown
637 var countdown = 3;
638 var countdownInterval = setInterval(function () {
639 countdown--;
640 if (countdown > 0) {
641 $button.html('�
642 Reloading in ' + countdown + '...');
643 } else {
644 clearInterval(countdownInterval);
645 location.reload();
646 }
647 }, 1000);
648
649 } else if (statusCode === 404) {
650 // Website not registered
651 var effectiveDomain = response.data.effective_domain || metaSync.dashboard_domain;
652 showConnectNotRegistered(effectiveDomain);
653 resetConnectButton();
654
655 } else if (statusCode === 500) {
656 // Server error
657 showConnectError('🔧 Server Error',
658 'A server error occurred during authentication. This is usually temporary.',
659 [{
660 text: '🔄 Try Again',
661 action: function () {
662 handleSearchAtlasConnect();
663 }
664 }, {
665 text: '💬 Contact Support',
666 action: function () {
667 var supportEmail = metaSync.support_email || 'support@searchatlas.com';
668 window.open('mailto:' + supportEmail + '?subject=Connect Server Error (Code 500)', '_blank');
669 }
670 }]
671 );
672 resetConnectButton();
673
674 } else {
675 // Unknown status
676 showConnectError('❓ Unexpected Status',
677 'Received an unexpected status code (' + statusCode + ') during authentication.',
678 [{
679 text: '🔄 Try Again',
680 action: function () {
681 handleSearchAtlasConnect();
682 }
683 }]
684 );
685 resetConnectButton();
686 }
687 }
688 },
689 error: function (xhr, status, error) {
690 // Continue polling even if individual request fails, but provide feedback
691 if (pollCount % 6 === 0) { // Every 30 seconds, show a subtle warning
692 console.log('Connect polling request failed, continuing... Error:', error);
693 // Don't show error to user for temporary network issues during polling
694 }
695
696 }
697 });
698 }, 5000); // Poll every 5 seconds
699 }
700
701 function initializeProgressDisplay() {
702 var $progressContainer = $('.metasync-sa-connect-progress');
703 var $button = $('#connect-searchatlas-btn');
704
705 // Hide any existing status messages to avoid duplication
706 hideConnectStatus();
707
708 // Create progress elements if they don't exist
709 if ($progressContainer.length === 0) {
710 var progressHTML = `
711 <div class="metasync-sa-connect-progress">
712 <div class="metasync-sa-connect-progress-header">
713 <strong>🔐 Authentication in Progress</strong>
714 <span class="metasync-sa-connect-progress-time">Connecting...</span>
715 </div>
716 <div class="metasync-sa-connect-progress-bar">
717 <div class="metasync-sa-connect-progress-fill"></div>
718 </div>
719 <div class="metasync-sa-connect-progress-text">
720 Establishing secure connection to ' + getPluginName() + '...
721 </div>
722 </div>
723 `;
724 $button.closest('.metasync-sa-connect-container').append(progressHTML);
725 $progressContainer = $('.metasync-sa-connect-progress');
726 }
727
728 $progressContainer.show().find('.metasync-sa-connect-progress-fill').css('width', '0%');
729 }
730
731 function updateProgress(percentage, currentPoll, maxPolls) {
732 var $progressFill = $('.metasync-sa-connect-progress-fill');
733 var $progressTime = $('.metasync-sa-connect-progress-time');
734 var $progressText = $('.metasync-sa-connect-progress-text');
735
736 // Update progress bar
737 $progressFill.css('width', percentage + '%');
738
739 // Update time display (now in seconds)
740 var timeElapsed = currentPoll * 5;
741 var timeRemaining = (maxPolls - currentPoll) * 5;
742 $progressTime.text(timeElapsed + 's elapsed, ' + timeRemaining + 's remaining');
743
744 // Update progress text based on time elapsed (optimized for 60-second timeout)
745 var progressMessages = [
746 'Establishing connection and opening authentication window...',
747 'Please complete authentication in the popup window...',
748 'Almost done! Finalizing your authentication...'
749 ];
750
751 var messageIndex = Math.floor((currentPoll / maxPolls) * progressMessages.length);
752 messageIndex = Math.min(messageIndex, progressMessages.length - 1);
753 $progressText.text(progressMessages[messageIndex]);
754 }
755
756 // Update the old function name for compatibility
757 function startConnectPolling(nonceToken) {
758 return startSearchAtlasPolling(nonceToken);
759 }
760
761 function stopSearchAtlasPolling() {
762 if (saConnectPollingInterval) {
763 clearInterval(saConnectPollingInterval);
764 saConnectPollingInterval = null;
765 }
766 }
767
768 // Legacy function for backward compatibility
769 function stopConnectPolling() {
770 return stopSearchAtlasPolling();
771 }
772
773 function showConnectSuccess(title, message, actions) {
774 showConnectStatus('success', title, message, actions);
775 }
776
777 function showConnectError(title, message, actions) {
778 showConnectStatus('error', title, message, actions);
779 }
780
781 function showConnectInfo(title, message, actions) {
782 showConnectStatus('info', title, message, actions);
783 }
784
785 function showConnectWarning(title, message, actions) {
786 showConnectStatus('warning', title, message, actions);
787 }
788
789 function showConnectStatus(type, title, message, actions) {
790 var $statusContainer = $('#sa-connect-status-message');
791 var $button = $('#connect-searchatlas-btn');
792
793 // Create enhanced status container if it doesn't exist
794 if ($statusContainer.length === 0 || !$statusContainer.hasClass('metasync-sa-connect-status')) {
795 // Create new enhanced status container
796 var statusHTML = '<div id="sa-connect-status-message" class="metasync-sa-connect-status"></div>';
797 $button.closest('.metasync-sa-connect-container').length === 0 ?
798 $button.parent().append(statusHTML) :
799 $button.closest('.metasync-sa-connect-container').append(statusHTML);
800 $statusContainer = $('#sa-connect-status-message');
801 }
802
803 // Build status content
804 var html = '<div class="metasync-sa-connect-status-content">';
805 html += '<div class="metasync-sa-connect-status-title">' + title + '</div>';
806 if (message) {
807 html += '<div class="metasync-sa-connect-status-message">' + message + '</div>';
808 }
809 html += '</div>';
810
811 // Add action buttons if provided
812 if (actions && actions.length > 0) {
813 html += '<div class="metasync-sa-connect-actions">';
814 actions.forEach(function (action, index) {
815 var buttonClass = action.primary ? 'primary' : 'secondary';
816 html += '<button type="button" class="metasync-sa-connect-btn ' + buttonClass + '" data-action="' + index + '">';
817 html += action.text;
818 html += '</button>';
819 });
820 html += '</div>';
821 }
822
823 // Update status container with animation
824 $statusContainer
825 .removeClass('success error info warning')
826 .addClass(type)
827 .html(html)
828 .hide()
829 .slideDown(300);
830
831 // Bind action handlers
832 if (actions && actions.length > 0) {
833 $statusContainer.find('.metasync-sa-connect-btn').off('click').on('click', function () {
834 var $actionBtn = $(this);
835 var actionIndex = parseInt($actionBtn.data('action'));
836 if (actions[actionIndex] && typeof actions[actionIndex].action === 'function') {
837 var originalText = $actionBtn.text();
838 $actionBtn.prop('disabled', true)
839 .addClass('no-loading') // Prevent dashboard.js conflicts
840 .removeClass('dashboard-loading')
841 .html('<span class="metasync-sa-connect-loading"></span> ' + originalText);
842 setTimeout(function () {
843 actions[actionIndex].action();
844 }, 100);
845 }
846 });
847 }
848
849 // Auto-scroll to status message for better visibility
850 if (type === 'error' || type === 'warning' || type === 'success') {
851 setTimeout(function () {
852 $('html, body').animate({
853 scrollTop: $statusContainer.offset().top - 100
854 }, 300);
855 }, 100);
856 }
857 }
858
859 function showConnectNotRegistered(dashboardDomain) {
860 // Use dashboard domain if provided, otherwise fallback to effective domain (includes whitelabel)
861 var domain = dashboardDomain || metaSync.dashboard_domain;
862 var registerUrl = domain + '/seo-automation-v3/create-project';
863
864 showConnectWarning(
865 '⚠️ Website Not Registered',
866 'Your website hasn\'t been registered with ' + getPluginName() + ' yet. Registration is required to enable 1-click connect to retrieve your Search Atlas API key and Otto UUID.',
867 [{
868 text: '🌐 Register Website',
869 action: function () {
870 try {
871 var parsedRegister = new URL(registerUrl);
872 if (parsedRegister.protocol === 'https:' || parsedRegister.protocol === 'http:') {
873 var a = document.createElement('a');
874 a.href = parsedRegister.href;
875 a.target = '_blank';
876 a.rel = 'noopener';
877 a.click();
878 }
879 } catch (e) { /* invalid URL */ }
880 },
881 primary: true
882 }, {
883 text: '📚 Learn More About Registration',
884 action: function () {
885 var docDomain = metaSync.documentation_domain || 'https://searchatlas.com';
886 window.open(docDomain, '_blank');
887 }
888 }, {
889 text: '🔄 Try Authentication Again',
890 action: function () {
891 setTimeout(function () {
892 handleSearchAtlasConnect();
893 }, 500);
894 }
895 }]
896 );
897 }
898
899 function hideConnectStatus() {
900 $('#sa-connect-status-message').slideUp(300);
901 $('.metasync-sa-connect-progress').slideUp(300);
902 }
903
904 function showPopupHelp() {
905 var helpContent = `
906 <div style="max-width: 500px;">
907 <h3>🔧 How to Enable Popups</h3>
908 <p><strong>Chrome/Edge:</strong></p>
909 <ol>
910 <li>Click the popup blocked icon in the address bar</li>
911 <li>Select "Always allow popups from this site"</li>
912 <li>Reload the page and try again</li>
913 </ol>
914 <p><strong>Firefox:</strong></p>
915 <ol>
916 <li>Click the shield icon in the address bar</li>
917 <li>Turn off "Block popup windows"</li>
918 <li>Refresh and try again</li>
919 </ol>
920 <p><strong>Safari:</strong></p>
921 <ol>
922 <li>Go to Safari → Preferences → Websites</li>
923 <li>Select "Pop-up Windows" on the left</li>
924 <li>Set this website to "Allow"</li>
925 </ol>
926 </div>
927 `;
928
929 showConnectInfo('📝 Popup Help', helpContent, [{
930 text: '�
931 Got it, Try Again',
932 action: function () {
933 handleSearchAtlasConnect();
934 },
935 primary: true
936 }]);
937 }
938
939 function enhancedErrorRecovery(error, context) {
940 console.group('🔍 Connect Error Diagnostics');
941 console.log('Error Context:', context);
942 console.log('Error Details:', error);
943 console.log('Browser Info:', {
944 userAgent: navigator.userAgent,
945 cookieEnabled: navigator.cookieEnabled,
946 language: navigator.language,
947 platform: navigator.platform
948 });
949 console.log('Current Time:', new Date().toISOString());
950 console.groupEnd();
951
952 // Provide contextual recovery suggestions
953 var recoverySuggestions = [];
954
955 if (context === 'network') {
956 recoverySuggestions = [
957 'Check your internet connection',
958 'Disable VPN or proxy if enabled',
959 'Try refreshing the page',
960 'Clear browser cache and cookies'
961 ];
962 } else if (context === 'popup') {
963 recoverySuggestions = [
964 'Allow popups for this website',
965 'Disable ad blockers temporarily',
966 'Try using a different browser',
967 'Check if firewall is blocking the request'
968 ];
969 } else if (context === 'timeout') {
970 recoverySuggestions = [
971 'Complete authentication within 60 seconds',
972 'Check if the popup window needs attention',
973 'Ensure you have your ' + getPluginName() + ' login ready',
974 'Try the authentication process again',
975 'Contact support if timeouts persist'
976 ];
977 }
978
979 return recoverySuggestions;
980 }
981
982 // Add enhanced page visibility handling
983 function handlePageVisibilityChange() {
984 if (document.hidden && saConnectWindow && !saConnectWindow.closed) {
985 // Page became hidden while SSO is in progress
986 showConnectInfo('👁️ Page Hidden',
987 'This page is now in the background. The authentication will continue, but you may want to return to this tab to see the results.');
988 }
989 }
990
991 // Initialize enhanced features when document is ready
992 $(document).ready(function () {
993
994 // Hide Jetpack identity crisis container on plugin pages
995 if ($('.metasync-dashboard-wrap').length > 0) {
996 $('#jp-identity-crisis-container, .jp-identity-crisis-container').hide();
997 }
998
999 // Check for URL parameters and show success/error messages
1000 const urlParams = new URLSearchParams(window.location.search);
1001
1002 // Show success message for cleared logs
1003 if (urlParams.get('log_cleared') === '1') {
1004 showSyncSuccess('🧹 Error Logs Cleared', 'All error log entries have been successfully cleared.');
1005 }
1006
1007 // Show success message for cleared error summary
1008 if (urlParams.get('error_summary_cleared') === '1') {
1009 showSyncSuccess('📊 Error Summary Cleared', 'Error summary has been cleared successfully.');
1010 }
1011
1012 // Show error message for failed clear operation
1013 if (urlParams.get('clear_error') === '1') {
1014 showSyncError('❌ Clear Failed', 'Unable to clear the error logs. Please check permissions or try again.');
1015 }
1016
1017 // Show error message for failed error summary clear
1018 if (urlParams.get('error_summary_error') === '1') {
1019 showSyncError('❌ Clear Failed', 'Unable to clear the error summary. Please try again.');
1020 }
1021
1022 // Check global variables are available
1023
1024 // Test AJAX connectivity using our specific endpoint
1025 if (typeof ajaxurl !== 'undefined' && ajaxurl) {
1026 $.ajax({
1027 url: ajaxurl,
1028 type: 'POST',
1029 data: {
1030 action: 'metasync_test_ajax_endpoint',
1031 nonce: metaSync.sa_connect_nonce
1032 },
1033 timeout: 10000,
1034 success: function (response) {
1035 if (!response.success) {
1036 console.warn('🐛 DEBUG: AJAX endpoint reached but returned success=false:', response.data);
1037 }
1038 },
1039 error: function (xhr, status, error) {
1040 console.error('🐛 DEBUG: AJAX test failed:', {
1041 xhr: xhr,
1042 status: status,
1043 error: error,
1044 responseText: xhr.responseText,
1045 ajaxurl: ajaxurl
1046 });
1047
1048 // Try alternative AJAX test
1049 $.post(ajaxurl, {
1050 action: 'wp_ajax_nopriv_heartbeat'
1051 }).done(function (response2) {
1052 }).fail(function (xhr2) {
1053 console.error('🐛 DEBUG: Alternative AJAX also failed:', xhr2);
1054 });
1055 }
1056 });
1057 }
1058
1059 // Check if SSO button exists and is functional
1060 var $connectButton = $('#connect-searchatlas-btn');
1061
1062 // Test direct click event binding and fix dashboard interference
1063 if ($connectButton.length > 0) {
1064 // Aggressively prevent dashboard loading interference
1065 preventDashboardInterference($connectButton);
1066
1067 $connectButton.off('click').on('click', function (e) {
1068
1069 // Prevent dashboard.js from interfering
1070 preventDashboardInterference($(this));
1071
1072 // Call handleSearchAtlasConnect if not already disabled by another process
1073 if (!$(this).hasClass('connecting') && !$(this).hasClass('authenticating')) {
1074 handleSearchAtlasConnect();
1075 } else {
1076 }
1077 });
1078 }
1079
1080 // Add page visibility change handler
1081 if (typeof document.hidden !== 'undefined') {
1082 document.addEventListener('visibilitychange', handlePageVisibilityChange);
1083 }
1084
1085 // Add keyboard shortcuts for better accessibility
1086 $(document).on('keydown', function (e) {
1087 // Escape key to cancel ongoing SSO process
1088 if (e.key === 'Escape' && saConnectPollingInterval) {
1089 if (confirm('Cancel the ongoing authentication process?')) {
1090 stopSearchAtlasPolling();
1091 if (saConnectWindow) {
1092 saConnectWindow.close();
1093 }
1094 showConnectInfo('⏸️ Authentication Cancelled', 'You cancelled the authentication process.');
1095 resetConnectButton();
1096 }
1097 }
1098 });
1099
1100 // Add connection status indicator
1101 function updateConnectionStatus() {
1102 var $button = $('#connect-searchatlas-btn');
1103 var $apiKeyField = $('#searchatlas-api-key');
1104
1105 // Only update status if we're on a page with the API key field (General Settings)
1106 // On other pages, preserve the PHP-determined status in the header
1107 if ($apiKeyField.length === 0) {
1108 return; // Don't update status on pages without the API key field
1109 }
1110
1111 var hasApiKey = $apiKeyField.val() && $apiKeyField.val().trim() !== '';
1112 var hasOttoUuid = metaSync.otto_pixel_uuid && metaSync.otto_pixel_uuid.trim() !== '';
1113 var isFullyConnected = hasApiKey && hasOttoUuid;
1114
1115 // Update button text based on connection state
1116 if (!$button.prop('disabled')) {
1117 if (isFullyConnected) {
1118 $button.html('🔄 Re-authenticate with ' + getPluginName());
1119 } else if (hasApiKey && !hasOttoUuid) {
1120 $button.html('🔧 Complete Authentication Setup');
1121 } else {
1122 $button.html('🔗 Connect to ' + getPluginName());
1123 }
1124 }
1125
1126 // Update header status indicator (only on General Settings page)
1127 if (isFullyConnected) {
1128 updateHeaderStatus(true, 'Synced', getPluginName() + ' API key and ' + getOttoName() + ' UUID are configured');
1129 } else if (hasApiKey && !hasOttoUuid) {
1130 updateHeaderStatus(false, 'Warning', 'Connected but ' + getOttoName() + ' UUID is missing — deploys will not work. Please reconnect.');
1131 } else {
1132 updateHeaderStatus(false, 'Not Synced', 'Missing ' + getPluginName() + ' API key or ' + getOttoName() + ' UUID');
1133 }
1134 }
1135
1136 // Monitor API key field changes
1137 $('#searchatlas-api-key').on('input', updateConnectionStatus);
1138
1139 // Initial status update
1140 updateConnectionStatus();
1141
1142 // Initialize dashboard iframe functionality
1143 initializeDashboardIframe();
1144
1145 // Settings dropdown now handled by inline script in HTML
1146
1147 // Add debug function for connection status (accessible in console)
1148 window.debugConnectionStatus = function () {
1149 var apiKey = $('#searchatlas-api-key').val();
1150 var hasApiKey = apiKey.trim() !== '';
1151
1152 console.log('🔍 Connection Status Debug:', {
1153 searchatlas_api_key: hasApiKey ? (apiKey.substring(0, 8) + '...') : 'EMPTY',
1154 otto_pixel_uuid: metaSync.otto_pixel_uuid || 'NOT SET',
1155 connection_state: hasApiKey && metaSync.otto_pixel_uuid ? 'CONNECTED' :
1156 hasApiKey ? 'PARTIAL (Missing ' + getOttoName() + ' UUID)' : 'NOT CONNECTED',
1157 dashboard_tab_visible: hasApiKey && metaSync.otto_pixel_uuid ? 'YES' : 'NO',
1158 status_indicator_should_show: hasApiKey && metaSync.otto_pixel_uuid ? 'Synced' : 'Not Synced'
1159 });
1160 };
1161
1162
1163 });
1164
1165 // Settings dropdown is now handled by inline script in HTML for better reliability
1166
1167 /**
1168 * Initialize Dashboard Iframe functionality
1169 * Adds loading states and error handling for the embedded dashboard
1170 */
1171 function initializeDashboardIframe() {
1172 var $iframe = $('#metasync-dashboard-iframe');
1173
1174 if ($iframe.length === 0) {
1175 return; // No iframe on this page
1176 }
1177
1178 // Add loading indicator
1179 var $wrapper = $('.metasync-dashboard-iframe-wrapper');
1180 var loadingHTML = '<div class="metasync-dashboard-iframe-loading"><div class="spinner"></div><p>Loading dashboard...</p></div>';
1181 $wrapper.append(loadingHTML);
1182
1183 // Handle iframe load events
1184 $iframe.on('load', function () {
1185 $('.metasync-dashboard-iframe-loading').fadeOut(300);
1186
1187 // Log successful load
1188 console.log('Dashboard iframe loaded successfully');
1189 });
1190
1191 // Handle iframe error events
1192 $iframe.on('error', function () {
1193 $('.metasync-dashboard-iframe-loading').html(
1194 '<div style="text-align: center; color: #dc3232;">' +
1195 '<h3>❌ Dashboard Loading Error</h3>' +
1196 '<p>Unable to load the dashboard. Please check your connection.</p>' +
1197 '<button type="button" class="button button-primary" onclick="location.reload();">🔄 Reload Page</button>' +
1198 '</div>'
1199 );
1200
1201 console.error('Dashboard iframe failed to load');
1202 });
1203
1204 // Add keyboard shortcut for refreshing iframe
1205 $(document).on('keydown', function (e) {
1206 // Ctrl/Cmd + R on dashboard page refreshes iframe
1207 if ((e.ctrlKey || e.metaKey) && e.key === 'r' && $iframe.length > 0) {
1208 e.preventDefault();
1209 refreshDashboardIframe();
1210 }
1211 });
1212
1213 // Handle iframe resize for better mobile experience
1214 function adjustIframeHeight() {
1215 if (window.innerWidth <= 768) {
1216 $iframe.height(600);
1217 } else {
1218 $iframe.height(800);
1219 }
1220 }
1221
1222 // Adjust on window resize
1223 $(window).on('resize', adjustIframeHeight);
1224 adjustIframeHeight(); // Initial adjustment
1225 }
1226
1227 /**
1228 * Refresh Dashboard Iframe
1229 * Reloads the iframe content with loading indicator
1230 */
1231 function refreshDashboardIframe() {
1232 var $iframe = $('#metasync-dashboard-iframe');
1233 var $wrapper = $('.metasync-dashboard-iframe-wrapper');
1234
1235 if ($iframe.length === 0) {
1236 return;
1237 }
1238
1239 // Show loading indicator
1240 $('.metasync-dashboard-iframe-loading').remove();
1241 var loadingHTML = '<div class="metasync-dashboard-iframe-loading"><div class="spinner"></div><p>Refreshing dashboard...</p></div>';
1242 $wrapper.append(loadingHTML);
1243
1244 // Refresh iframe
1245 var currentSrc = $iframe.attr('src');
1246 $iframe.attr('src', '');
1247 setTimeout(function () {
1248 $iframe.attr('src', currentSrc);
1249 }, 100);
1250
1251 console.log('Dashboard iframe refresh initiated');
1252 }
1253
1254 /**
1255 * Handle Search Atlas Authentication Reset.
1256 * Shows confirmation dialog and clears the Search Atlas API key and Otto UUID.
1257 */
1258 function handleSearchAtlasResetAuth() {
1259 // Show confirmation dialog
1260 var confirmed = confirm(
1261 '⚠️ Disconnect ' + getPluginName() + ' Account\n\n' +
1262 'This will:\n' +
1263 '• Remove your ' + getPluginName() + ' API key\n' +
1264 '• Clear all authentication tokens\n' +
1265 '• Reset connection timestamps\n' +
1266 '• Clear cached authentication data\n\n' +
1267 'You will need to re-authenticate to use ' + getPluginName() + ' features.\n\n' +
1268 'Are you sure you want to continue?'
1269 );
1270
1271 if (!confirmed) {
1272 return;
1273 }
1274
1275 var $resetButton = $('#reset-searchatlas-auth');
1276 var $connectButton = $('#connect-searchatlas-btn');
1277 var $apiKeyField = $('#searchatlas-api-key');
1278
1279 // Show loading state (prevent dashboard.js conflicts)
1280 $resetButton.prop('disabled', true)
1281 .addClass('no-loading') // Prevent dashboard.js interference
1282 .removeClass('dashboard-loading')
1283 .html('<span class="metasync-sa-connect-loading"></span> Disconnecting...');
1284
1285 // Show status message
1286 showConnectInfo('🔄 Disconnecting', 'Clearing your ' + getPluginName() + ' authentication data...');
1287
1288 // Make AJAX call to reset authentication
1289 $.ajax({
1290 url: ajaxurl,
1291 type: 'POST',
1292 data: {
1293 action: 'metasync_reset_authentication',
1294 nonce: metaSync.reset_auth_nonce
1295 },
1296 success: function (response) {
1297 if (response.success) {
1298 // Clear the API key field
1299 $apiKeyField.val('');
1300
1301 // Update button states
1302 $connectButton.html('🔗 Connect to ' + getPluginName());
1303 $resetButton.remove(); // Remove reset button since no longer connected
1304
1305 // Show clean success message without duplicate connect functionality
1306 showConnectSuccess('�
1307 Account Disconnected',
1308 'Your ' + getPluginName() + ' authentication has been completely reset. All authentication data has been cleared.',
1309 [{
1310 text: '📄 View What Was Cleared',
1311 action: function () {
1312 showClearedDataDetails(response.data.cleared_data);
1313 },
1314 primary: true
1315 }, {
1316 text: '�
1317 Got it',
1318 action: function () {
1319 hideConnectStatus();
1320 }
1321 }]
1322 );
1323
1324 // Update page elements to reflect disconnected state
1325 updateUIForDisconnectedState();
1326
1327 } else {
1328 showConnectError('❌ Reset Failed',
1329 response.data.message || 'Failed to reset authentication',
1330 [{
1331 text: '🔄 Try Again',
1332 action: function () {
1333 handleSearchAtlasResetAuth();
1334 }
1335 }, {
1336 text: '💬 Contact Support',
1337 action: function () {
1338 var supportEmail = metaSync.support_email || 'support@searchatlas.com';
1339 window.open('mailto:' + supportEmail + '?subject=Authentication Reset Failed', '_blank');
1340 }
1341 }]
1342 );
1343 }
1344 },
1345 error: function (xhr, status, error) {
1346 showConnectError('🌐 Network Error',
1347 'A network error occurred while trying to reset authentication.',
1348 [{
1349 text: '🔄 Try Again',
1350 action: function () {
1351 handleSearchAtlasResetAuth();
1352 }
1353 }]
1354 );
1355 },
1356 complete: function () {
1357 // Reset button state
1358 $resetButton.prop('disabled', false)
1359 .removeClass('dashboard-loading no-loading')
1360 .html('🔓 Disconnect Account');
1361 }
1362 });
1363 }
1364
1365 /**
1366 * Show details of what data was cleared during reset
1367 */
1368 function showClearedDataDetails(clearedData) {
1369 var details = '<div style="max-width: 500px;"><h3>🗑️ Data Cleared</h3><ul style="text-align: left; margin: 15px 0;">';
1370
1371 for (var key in clearedData) {
1372 var displayName = key.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase());
1373 details += '<li><strong>' + displayName + ':</strong> ' + clearedData[key] + '</li>';
1374 }
1375
1376 details += '</ul><p style="color: #666; font-size: 13px;">This data has been permanently removed. You can safely reconnect with a new or existing ' + getPluginName() + ' account.</p></div>';
1377
1378 showConnectInfo('📋 Reset Details', details, [{
1379 text: '�
1380 Got it',
1381 action: function () {
1382 hideConnectStatus();
1383 // Highlight the main connect button briefly to guide user attention
1384 var $mainButton = $('#connect-searchatlas-btn');
1385 if ($mainButton.length > 0) {
1386 $mainButton.addClass('metasync-pulse');
1387 setTimeout(function () {
1388 $mainButton.removeClass('metasync-pulse');
1389 }, 2000);
1390 }
1391 },
1392 primary: true
1393 }]);
1394 }
1395
1396 /**
1397 * Update UI elements when account is disconnected
1398 */
1399 function updateUIForDisconnectedState() {
1400 // Update API key field placeholder
1401 $('#searchatlas-api-key').attr('placeholder', 'Your API key will appear here after authentication');
1402
1403 // �
1404 Clear OTTO Pixel UUID field
1405 $('input[name="metasync_options[general][otto_pixel_uuid]"]').val('');
1406
1407 // Note: OTTO SSR is always enabled by default, no checkbox to uncheck
1408
1409 // Remove synced indicator from API key field
1410 $('.metasync-sa-connect-container').find('span:contains("✓ Synced")').remove();
1411 $('label[for="searchatlas-api-key"]').find('span').remove(); // Remove any status spans
1412
1413 // Update header status indicator to "Not Synced"
1414 updateHeaderStatus(false, 'Not Synced', 'Missing ' + getPluginName() + ' API key or ' + getOttoName() + ' UUID');
1415
1416 // Update metaSync object for JavaScript state tracking
1417 if (typeof metaSync !== 'undefined') {
1418 metaSync.searchatlas_api_key = false;
1419 metaSync.otto_pixel_uuid = '';
1420 metaSync.is_connected = false;
1421 }
1422
1423 // Update descriptions to reflect disconnected state
1424 $('.metasync-sa-connect-description').html(
1425 'Connect your ' + getPluginName() + ' account with one click. This will automatically configure your API key below and enable all plugin features.'
1426 );
1427
1428 // Clear timestamp display if it exists
1429 $('#sendAuthTokenTimestamp').fadeOut(300);
1430
1431 // Header status already updated above - no need for additional connection status call
1432
1433 console.log('🔄 UI updated to reflect disconnected state - cleared API key, OTTO UUID, and OTTO enable checkbox');
1434
1435 // Show clean success message without duplicate connect button
1436 setTimeout(function () {
1437 showConnectSuccess('�
1438 Account Disconnected',
1439 'Your ' + getPluginName() + ' authentication has been completely reset. Use the "Connect to ' + getPluginName() + '" button above to reconnect.',
1440 [{
1441 text: '�
1442 Got it',
1443 action: function () {
1444 hideConnectStatus();
1445 },
1446 primary: true
1447 }]
1448 );
1449 }, 500); // Shorter delay since no action is needed
1450 }
1451
1452 /**
1453 * Update UI elements when account is connected/authenticated
1454 * Complementary function to updateUIForDisconnectedState()
1455 */
1456 function updateUIForConnectedState(apiKey, ottoPixelUuid) {
1457 // Update API key field
1458 if (apiKey) {
1459 $('#searchatlas-api-key').val(apiKey);
1460 }
1461
1462 // �
1463 Update OTTO Pixel UUID field
1464 if (ottoPixelUuid) {
1465 $('input[name="metasync_options[general][otto_pixel_uuid]"]').val(ottoPixelUuid);
1466 }
1467
1468 // Note: OTTO SSR is always enabled by default, no checkbox needed
1469
1470 // Update header status indicator based on UUID presence
1471 if (ottoPixelUuid) {
1472 updateHeaderStatus(true, 'Synced', 'Authentication completed - heartbeat sync will be validated on next page load');
1473 } else {
1474 updateHeaderStatus(false, 'Warning', 'Connected but ' + getOttoName() + ' UUID is missing — deploys will not work. Please reconnect.');
1475 }
1476
1477 // Update metaSync object for JavaScript state tracking
1478 if (typeof metaSync !== 'undefined') {
1479 metaSync.searchatlas_api_key = true;
1480 metaSync.is_connected = true;
1481 if (ottoPixelUuid) {
1482 metaSync.otto_pixel_uuid = ottoPixelUuid;
1483 }
1484 }
1485
1486 // Update descriptions to reflect connected state
1487 $('.metasync-sa-connect-description').html(
1488 'Your ' + getPluginName() + ' account is connected and synced successfully. All plugin features are now enabled.'
1489 );
1490
1491 console.log('�
1492 UI updated to reflect connected state - API key set, OTTO UUID set (SSR always enabled)');
1493 }
1494
1495 function addClassTableRowLocalSEO() {
1496 if (document.getElementsByClassName('form-table') && document.getElementById('local_seo_person_organization')) {
1497 const myElement = document.getElementsByTagName('tr');
1498
1499 for (let i = 0; i < myElement.length; i++) {
1500 myElement[i].classList.add('metasync-seo-' + (i + 10));
1501 }
1502 }
1503 }
1504
1505 function addClassTableRowSiteInfo() {
1506 if (document.getElementsByClassName('form-table') && document.getElementById('site_info_type')) {
1507 const myElement = document.getElementsByTagName('tr');
1508
1509 for (let i = 0; i < myElement.length; i++) {
1510 myElement[i].classList.add('metasync-site-info-' + (i + 10));
1511 }
1512 }
1513 }
1514
1515 function uploadMedia(title, text, input, src, closeBtn) {
1516
1517 var mediaUploader;
1518
1519 // If the uploader object has already been created, reopen the dialog
1520 if (mediaUploader) {
1521 mediaUploader.open();
1522 return;
1523 }
1524 // Extend the wp.media object
1525 mediaUploader = wp.media.frames.file_frame = wp.media({
1526 title: title,
1527 button: {
1528 text: text
1529 }, multiple: false
1530 });
1531
1532 // When a file is selected, grab the URL and set it as the text field's value
1533 mediaUploader.on('select', function () {
1534 var attachment = mediaUploader.state().get('selection').first().toJSON();
1535 jQuery('#' + input).val(attachment.id);
1536 jQuery('#' + src).attr('src', attachment.url);
1537 jQuery('#' + src).attr('width', 300);
1538 jQuery('#' + closeBtn).attr('type', 'button');
1539 jQuery('#' + src).show();
1540 jQuery('#' + closeBtn).show();
1541 });
1542 // Open the uploader dialog
1543 mediaUploader.open();
1544 }
1545
1546 function getLocalSeoOnLoadPage() {
1547 if (document.getElementsByClassName('form-table') && document.getElementById('local_seo_person_organization')) {
1548 var $type = $('#local_seo_person_organization').val();
1549 const classes = ['17', '18', '19', '20', '21', '24', '25'];
1550 if ($type === 'Person') {
1551 for (let i = 0; i < classes.length; i++) {
1552 $('.metasync-seo-' + classes[i]).hide();
1553 }
1554 $('.metasync-seo-15').show();
1555 } else {
1556 for (let i = 0; i < classes.length; i++) {
1557 $('.metasync-seo-' + classes[i]).show();
1558 }
1559 $('.metasync-seo-15').hide();
1560 }
1561 }
1562 }
1563
1564 function siteInfoOnLoadPage() {
1565 if (document.getElementsByClassName('form-table') && document.getElementById('site_info_type')) {
1566 var $type = $('#site_info_type').val();
1567 const classes = ['18', '19'];
1568 if ($type === 'blog' || $type === 'portfolio' || $type === 'otherpersonal') {
1569 for (let i = 0; i < classes.length; i++) {
1570 $('.metasync-site-info-' + classes[i]).hide();
1571 }
1572 } else {
1573 for (let i = 0; i < classes.length; i++) {
1574 $('.metasync-site-info-' + classes[i]).show();
1575 }
1576 }
1577 }
1578 }
1579
1580 function deleteTime() {
1581 $(this).parent().remove();
1582 }
1583
1584 function hideElementById(id) {
1585 if ($('#' + id)) {
1586 $('#' + id).hide();
1587 }
1588 }
1589
1590 function removeValueById(id) {
1591 if ($('#' + id)) {
1592 $('#' + id).val('');
1593 }
1594 }
1595
1596 $(function () {
1597 $('#addNewTime').on('click', function () {
1598 $('#daysTime').append(
1599 '<li>' +
1600 '<select name="metasync_options[localseo][days][]">' +
1601 '<option value="Monday">Monday</option>' +
1602 '<option value="Tuseday">Tuseday</option>' +
1603 '<option value="Wednesday">Wednesday</option>' +
1604 '<option value="Thursday">Thursday</option>' +
1605 '<option value="Friday">Friday</option>' +
1606 '<option value="Saturday">Saturday</option>' +
1607 '<option value="Sunday">Sunday</option>' +
1608 '</select>' +
1609 '<input type="text" name="metasync_options[localseo][times][]">' +
1610 '<button id="timeDelete">Delete</button>' +
1611 '</li>');
1612 return;
1613 });
1614 $(document).on('click', '#timeDelete', deleteTime);
1615 });
1616
1617 function deleteNumber() {
1618 $(this).parent().remove();
1619 }
1620
1621 $(function () {
1622 $('#addNewNumber').on('click', function () {
1623 $('#phone-numbers').append(
1624 '<li>' +
1625 '<select name="metasync_options[localseo][phonetype][]">' +
1626 '<option value="Customer Service">Customer Service</option>' +
1627 '<option value="Technical Support">Technical Support</option>' +
1628 '<option value="Billing Support">Billing Support</option>' +
1629 '<option value="Bill Payment">Bill Payment</option>' +
1630 '<option value="Sales">Sales</option>' +
1631 '<option value="Reservations">Reservations</option>' +
1632 '<option value="Credit Card Support">Credit Card Support</option>' +
1633 '<option value="Emergency">Emergency</option>' +
1634 '<option value="Baggage Tracking">Baggage Tracking</option>' +
1635 '<option value="Roadside Assistance">Roadside Assistance</option>' +
1636 '<option value="Package Tracking">Package Tracking</option>' +
1637 '</select>' +
1638 '<input type="text" name="metasync_options[localseo][phonenumber][]">' +
1639 '<button id="number-delete">Delete</button>' +
1640 '</li>');
1641 return;
1642 });
1643 $(document).on('click', '#number-delete', deleteNumber);
1644 });
1645
1646 function deleteSourceUrl() {
1647 $(this).parent().remove();
1648 }
1649 $(function () {
1650 $('#addNewSourceUrl').on('click', function () {
1651 $('#source_urls').append(
1652 '<li>' +
1653 '<input type="text" class="regular-text" name="source_url[]">' +
1654 '<select name="search_type[]">' +
1655 '<option value="exact">Exact</option>' +
1656 '<option value="contain">Contain</option>' +
1657 '<option value="start">Start With</option>' +
1658 '<option value="end">End With</option>' +
1659 '</select>' +
1660 '<button id="source_url_delete">Remove</button>' +
1661 '</li>');
1662 return;
1663 });
1664 $(document).on('click', '#source_url_delete', deleteSourceUrl);
1665 });
1666
1667 $(function () {
1668
1669 setToken();
1670
1671 $('body').on('click', '#wp_metasync_sync', function (e) {
1672 e.preventDefault();
1673 metasync_syncPostsAndPages();
1674 });
1675 $('body').on('click', '#metasync_settings_genkey_btn', function () {
1676 $('#apikey').val(metasyncGenerateAPIKey());
1677 });
1678 $('body').on('click', '#lgloginbtn', function () {
1679 // Hide any existing error messages first
1680 $('#lgerror').addClass('hidden').hide();
1681
1682 if ($('#lgusername').val() === '' || $('#lgpassword').val() === '') {
1683 $('.input.lguser').toggleClass('hidden');
1684 } else {
1685 metasyncLGLogin($('#lgusername').val(), $('#lgpassword').val());
1686 }
1687 });
1688
1689 // Enhanced SSO Connect button event handler
1690 // Aggressive event binding that overrides dashboard.js interference
1691 $('body').off('click', '#connect-searchatlas-btn').on('click', '#connect-searchatlas-btn', function (e) {
1692
1693 // Aggressively prevent dashboard interference
1694 preventDashboardInterference($(this));
1695
1696 // Only proceed if button is not in SSO process
1697 if (!$(this).hasClass('connecting') && !$(this).hasClass('authenticating')) {
1698 e.preventDefault();
1699 e.stopPropagation();
1700
1701
1702 handleSearchAtlasConnect();
1703 } else {
1704 }
1705 });
1706
1707 // Also add a direct event listener as backup
1708 setTimeout(function () {
1709 var $btn = $('#connect-searchatlas-btn');
1710 if ($btn.length > 0) {
1711 $btn[0].addEventListener('click', function (e) {
1712 e.preventDefault();
1713 e.stopPropagation();
1714
1715 // Force enable the button and clean classes
1716 preventDashboardInterference($(this));
1717
1718 if (!$(this).hasClass('connecting') && !$(this).hasClass('authenticating')) {
1719 handleSearchAtlasConnect();
1720 }
1721 }, true); // Use capture phase to get event before other handlers
1722 }
1723 }, 500);
1724
1725 // Monitor button state changes and fix interference
1726 setTimeout(function () {
1727 var $btn = $('#connect-searchatlas-btn');
1728 if ($btn.length > 0) {
1729 // Store original button state for restoration
1730 var buttonState = {
1731 disabled: $btn.prop('disabled'),
1732 style: $btn.attr('style'),
1733 pointerEvents: $btn.css('pointer-events'),
1734 zIndex: $btn.css('z-index'),
1735 position: $btn.css('position'),
1736 classes: $btn.attr('class')
1737 };
1738
1739 // Monitor for unwanted changes to the button
1740 var observer = new MutationObserver(function (mutations) {
1741 mutations.forEach(function (mutation) {
1742 if (mutation.type === 'attributes') {
1743 // Monitor for unwanted attribute changes
1744
1745 // Fix dashboard interference automatically
1746 if (mutation.attributeName === 'class' && $btn.hasClass('dashboard-loading')) {
1747 preventDashboardInterference($btn);
1748 }
1749
1750 if (mutation.attributeName === 'disabled' && $btn.prop('disabled') && !$btn.hasClass('connecting')) {
1751 preventDashboardInterference($btn);
1752 }
1753 }
1754 });
1755 });
1756
1757 observer.observe($btn[0], {
1758 attributes: true,
1759 attributeOldValue: true,
1760 attributeFilter: ['class', 'disabled', 'style']
1761 });
1762 }
1763 }, 1000);
1764
1765 // SSO Reset button event handler
1766 $('body').on('click', '#reset-searchatlas-auth', function (e) {
1767 e.preventDefault();
1768 handleSearchAtlasResetAuth();
1769 });
1770
1771 $('body').on('click', '#local_seo_logo_close_btn', function () {
1772 removeValueById('local_seo_logo');
1773 hideElementById('local_seo_business_logo');
1774 hideElementById('local_seo_logo_close_btn');
1775 });
1776
1777 $('body').on('click', '#site_google_logo_close_btn', function () {
1778 removeValueById('site_google_logo');
1779 hideElementById('site_google_logo_img');
1780 hideElementById('site_google_logo_close_btn');
1781 });
1782
1783 $('body').on('click', '#site_social_image_close_btn', function () {
1784 removeValueById('site_social_share_image');
1785 hideElementById('site_social_share_img');
1786 hideElementById('site_social_image_close_btn');
1787 });
1788
1789 $('body').on('click', '#logo_upload_button', function () {
1790 uploadMedia('Logo', 'Add', 'local_seo_logo', 'local_seo_business_logo', 'local_seo_logo_close_btn');
1791 });
1792
1793 $('body').on('click', '#google_logo_btn', function () {
1794 uploadMedia('Site Google Logo', 'Add', 'site_google_logo', 'site_google_logo_img', 'site_google_logo_close_btn');
1795 });
1796
1797 $('body').on('click', '#social_share_image_btn', function () {
1798 uploadMedia('Site Social Share Image', 'Add', 'site_social_share_image', 'site_social_share_img', 'site_social_image_close_btn');
1799 });
1800
1801 $('body').on('click', '#robots_common1', function () {
1802 $('#robots_common1').prop('checked', true);
1803 $('#robots_common2').prop('checked', false);
1804 });
1805
1806 $('body').on('click', '#robots_common2', function () {
1807 $('#robots_common1').prop('checked', false);
1808 $('#robots_common2').prop('checked', true);
1809 });
1810
1811 addClassTableRowLocalSEO();
1812
1813 addClassTableRowSiteInfo();
1814
1815 getLocalSeoOnLoadPage();
1816
1817 siteInfoOnLoadPage();
1818
1819 $('#local_seo_person_organization').change(function () {
1820 const classes = ['17', '18', '19', '20', '21', '24', '25'];
1821 if (this.value === 'Person') {
1822 for (let i = 0; i < classes.length; i++) {
1823 $('.metasync-seo-' + classes[i]).hide();
1824 }
1825 $('.metasync-seo-15').show();
1826 } else {
1827 for (let i = 0; i < classes.length; i++) {
1828 $('.metasync-seo-' + classes[i]).show();
1829 }
1830 $('.metasync-seo-15').hide();
1831 }
1832 });
1833
1834 $('#site_info_type').change(function () {
1835 const classes = ['18', '19'];
1836 if (this.value === 'blog' || this.value === 'portfolio' || this.value === 'otherpersonal') {
1837 for (let i = 0; i < classes.length; i++) {
1838 $('.metasync-site-info-' + classes[i]).hide();
1839 }
1840 } else {
1841 for (let i = 0; i < classes.length; i++) {
1842 $('.metasync-site-info-' + classes[i]).show();
1843 }
1844 }
1845 });
1846
1847 $('#metasync-giapi-response').hide();
1848
1849 $('body').on('click', '#metasync-btn-send', function () {
1850
1851 var url = $('#metasync-giapi-url');
1852 var action = $('input[type="radio"]:checked');
1853 var response = $('#metasync-giapi-response');
1854
1855 var urls = url.val().split('\n').filter(Boolean);
1856
1857 var urls_str = urls[0];
1858 var is_bulk = false;
1859 if (urls.length > 1) {
1860 urls_str = urls;
1861 is_bulk = true;
1862 }
1863
1864 jQuery.ajax({
1865 method: 'POST',
1866 url: 'admin-ajax.php',
1867 data: {
1868 action: 'metasync_send_giapi',
1869 metasync_giapi_url: url.val(),
1870 metasync_giapi_action: action.val()
1871 }
1872 })
1873 .always(function (info) {
1874
1875 response.show();
1876
1877 $('.result-action').html('<strong>' + action.val() + '</strong>' + ' <br> ' + urls_str);
1878
1879 if (!is_bulk) {
1880 if (typeof info.error !== 'undefined') {
1881 $('.result-status-code').text(info.error.code).siblings('.result-message').text(info.error.message);
1882 } else {
1883 var d = new Date();
1884 $('.result-status-code').text('Success').siblings('.result-message').text(d.toString());
1885 }
1886 } else {
1887 $('.result-status-code').text('Success').siblings('.result-message').text('Success');
1888 if (typeof info.error !== 'undefined') {
1889 $('.result-status-code').text(info.error.code).siblings('.result-message').text(info.error.message);
1890 } else {
1891 $.each(info, function (index, val) {
1892
1893 if (typeof val.error !== 'undefined') {
1894 var error_code = '';
1895 if (typeof val.error.code !== 'undefined') {
1896 error_code = val.error.code;
1897 }
1898 var error_message = '';
1899 if (typeof val.error.message !== 'undefined') {
1900 error_message = val.error.message;
1901 }
1902 $('.result-status-code').text(error_code).siblings('.result-message').text(val.error.message);
1903 }
1904 });
1905 }
1906 }
1907 });
1908 });
1909
1910 $('body').on('click', '#cancel-redirection', function () {
1911 $('#add-redirection-form').hide();
1912 $('#add-redirection').focus();
1913 });
1914
1915 $('body').on('click', '.redirect_type', function () {
1916 if ($(this).val() === '410' || $(this).val() === '451') {
1917 $('#destination_url').val('');
1918 $('#destination').hide();
1919 } else {
1920 $('#destination').show();
1921 }
1922 });
1923
1924 if ($('#post_redirection').is(':checked')) {
1925 $('.hide').fadeIn('slow');
1926 }
1927 $('body').on('change', '#post_redirection', function () {
1928 if (this.checked) {
1929 $('.hide').fadeIn('slow');
1930 } else {
1931 $('.hide').fadeOut('slow');
1932 }
1933 });
1934
1935 $(document).ready(function () {
1936 if ($('#post_redirection').is(':checked')
1937 && ($('#post_redirection_type').val() === '410'
1938 || $('#post_redirection_type').val() === '451')) {
1939 $('#post_redirect_url').hide();
1940 }
1941 });
1942
1943 $('#post_redirection_type').change(function () {
1944 if ($('#post_redirection').is(':checked')
1945 && ($(this).val() === '410'
1946 || $(this).val() === '451')) {
1947 $('#post_redirect_url').hide();
1948 } else {
1949 $('#post_redirect_url').show();
1950 }
1951 });
1952
1953 });
1954
1955 $(function () {
1956 var psconsole = $('#error-code-box');
1957 if (psconsole.length) {
1958 psconsole.scrollTop(psconsole[0].scrollHeight - psconsole.height());
1959 }
1960 });
1961
1962 $(function () {
1963 $('#copy-clipboard-btn').on('click', function () {
1964 var hiddenInput = document.createElement('input');
1965 hiddenInput.setAttribute('value', document.getElementById('error-code-box').value);
1966 document.body.appendChild(hiddenInput);
1967 hiddenInput.select();
1968 document.execCommand('copy');
1969 document.body.removeChild(hiddenInput);
1970 });
1971 });
1972
1973 function dateFormat() {
1974 var months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
1975 var m = new Date();
1976 return months[m.getMonth()] + ' ' + ('0' + m.getDate()).slice(-2) + ', ' + m.getFullYear() + ' ' + (m.getHours() > 12 ? '0' + m.getHours() % 12 : '0' + m.getHours()).slice(-2) + ':' + ('0' + m.getMinutes()).slice(-2) + ':' + ('0' + m.getSeconds()).slice(-2) + ' ' + (m.getHours() > 12 ? 'PM' : 'AM');
1977 }
1978
1979 function sendCustomerParams(is_hb = false) {
1980 // Only show alerts for manual sync (not heartbeat)
1981 const showAlerts = !is_hb;
1982
1983 // Remove any existing sync-related notices
1984 if (showAlerts) {
1985 $('.metasync-sync-notice, .metasync-sync-error').remove();
1986 }
1987
1988 // DEBUG: Log request parameters
1989 const requestData = {
1990 action: 'metasync_send_customer_params',
1991 nonce: metaSync.nonce,
1992 is_heart_beat : is_hb
1993 };
1994
1995 jQuery.ajax({
1996 type: 'post',
1997 url: 'admin-ajax.php',
1998 data: requestData,
1999 beforeSend: function () {
2000 if (showAlerts) {
2001 // Show loading state on button
2002 $('#sendAuthToken').prop('disabled', true).html('🔄 Syncing...');
2003 }
2004 },
2005 success: function (response) {
2006 // Reset button state
2007 if (showAlerts) {
2008 $('#sendAuthToken').prop('disabled', false).html('🔄 Sync Now');
2009 }
2010
2011 if ($('#searchatlas-api-key') && $('#searchatlas-api-key').val() === '') {
2012 $('#sendAuthTokenTimestamp').html('Please save your ' + getPluginName() + ' API key');
2013 $('#sendAuthTokenTimestamp').css({ color: 'red' });
2014
2015 // Remove stale PHP-rendered "✓ Synced" label.
2016 $('label[for="searchatlas-api-key"]').find('span:contains("Synced")').remove();
2017
2018 // Update header status to "Not Synced" for missing API key
2019 updateHeaderStatus(false, 'Not Synced', 'Not Synced - API key required');
2020
2021 if (showAlerts) {
2022 showSyncError('⚠️ API Key Required', 'Please save your ' + getPluginName() + ' API key in the settings above before syncing.');
2023 }
2024
2025 } else if (response && response.throttled) {
2026 // Handle throttling response
2027 var remainingMinutes = response.remaining_minutes || 5;
2028 //$('#sendAuthTokenTimestamp').html('Please wait ' + remainingMinutes + ' minutes before syncing again');
2029 //$('#sendAuthTokenTimestamp').css({ color: 'orange' });
2030
2031 // Disable sync button and show countdown
2032 if (showAlerts) {
2033 $('#sendAuthToken').prop('disabled', true).text('⏰ Throttled (' + remainingMinutes + 'm)');
2034
2035 // Start countdown timer
2036 var countdownInterval = setInterval(function () {
2037 remainingMinutes--;
2038 if (remainingMinutes <= 0) {
2039 clearInterval(countdownInterval);
2040 $('#sendAuthToken').prop('disabled', false).text('🔄 Sync Now');
2041 $('#sendAuthTokenTimestamp').text('Ready to sync');
2042 $('#sendAuthTokenTimestamp').css({ color: 'green' });
2043 } else {
2044 $('#sendAuthToken').text('⏰ Throttled (' + remainingMinutes + 'm)');
2045 //$('#sendAuthTokenTimestamp').html('Please wait ' + remainingMinutes + ' minutes before syncing again');
2046 }
2047 }, 60000); // Update every minute
2048 }
2049
2050 // Update header status to show throttling
2051 updateHeaderStatus(false, 'Throttled', 'Throttled - Please wait ' + remainingMinutes + ' minutes');
2052
2053 if (showAlerts) {
2054 showSyncError('⏰ Request Throttled', response.message || 'Please wait ' + remainingMinutes + ' minutes before making another sync request.');
2055 }
2056
2057 } else if (response && response.detail) {
2058 $('#sendAuthTokenTimestamp').html('Please provide a valid ' + getPluginName() + ' API key');
2059 $('#sendAuthTokenTimestamp').css({ color: 'red' });
2060
2061 // Remove stale PHP-rendered "✓ Synced" label — key is invalid on the server.
2062 $('label[for="searchatlas-api-key"]').find('span:contains("Synced")').remove();
2063
2064 // Update header status to "Not Synced" for invalid API key
2065 updateHeaderStatus(false, 'Not Synced', 'Not Synced - Invalid API key');
2066
2067 if (showAlerts) {
2068 showSyncError('❌ Invalid API Key', 'Please provide a valid ' + getPluginName() + ' API key.');
2069 }
2070
2071 } else if (response === null || !response.id) {
2072 // Update header status to "Not Synced" after failed sync
2073 updateHeaderStatus(false, 'Not Synced', 'Not Synced - Data synchronization failed');
2074
2075 if (showAlerts) {
2076 showSyncError('❌ Sync Failed', 'Something went wrong during synchronization. Please check your connection and try again.');
2077 }
2078 // Keep existing commented behavior for timestamp
2079
2080 } else {
2081 var dateString = dateFormat();
2082 $('#sendAuthTokenTimestamp').html(dateString);
2083 $('#sendAuthTokenTimestamp').css({ color: 'green' });
2084
2085 // Update header status — check if UUID is present before declaring fully synced
2086 var hasOttoUuid = metaSync.otto_pixel_uuid && metaSync.otto_pixel_uuid.trim() !== '';
2087 if (hasOttoUuid) {
2088 updateHeaderStatus(true, 'Synced', 'Synced - Data synchronization completed successfully');
2089 } else {
2090 updateHeaderStatus(false, 'Warning', 'Connected but ' + getOttoName() + ' UUID is missing — deploys will not work. Please reconnect.');
2091 }
2092
2093 if (showAlerts) {
2094 showSyncSuccess('�
2095 Sync Complete', 'Your categories and user data have been successfully synchronized with ' + getPluginName() + '.');
2096 }
2097 }
2098 },
2099 error: function (xhr, status, error) {
2100 // Update header status to "Not Synced" for network errors
2101 updateHeaderStatus(false, 'Not Synced', 'Not Synced - Network error during sync');
2102
2103 // Reset button state
2104 if (showAlerts) {
2105 $('#sendAuthToken').prop('disabled', false).html('🔄 Sync Now');
2106 showSyncError('❌ Network Error', 'Failed to connect to ' + getPluginName() + '. Please check your internet connection and try again.');
2107 }
2108 }
2109 });
2110 }
2111
2112 /**
2113 * Show sync success notification (wrapper for consolidated function)
2114 */
2115 function showSyncSuccess(title, message) {
2116 showPluginNotice('success', title, message, 'metasync-sync-notice', 4000);
2117 }
2118
2119 /**
2120 * Show sync error notification (wrapper for consolidated function)
2121 */
2122 function showSyncError(title, message) {
2123 showPluginNotice('error', title, message, 'metasync-sync-error', 0);
2124 }
2125
2126 function clear_otto_caches() {
2127 jQuery.ajax({
2128 url: ajaxurl,
2129 type: 'GET',
2130 data: {
2131 action: 'metasync_clear_otto_cache',
2132 clear_otto_cache: 1
2133 },
2134 success: function (response) {
2135 const now = new Date();
2136 $('#clear_otto_caches').text('Cache Cleared ' + now.toLocaleTimeString());
2137 console.log('Cleared SSR Caches');
2138 }
2139 });
2140 }
2141
2142 jQuery(document).ready(function () {
2143
2144 // sendCustomerParams();
2145 $('#sendAuthToken').on('click', function (e) {
2146 e.preventDefault();
2147 sendCustomerParams();
2148
2149 });
2150
2151 // handle otto clear cache button
2152 $('#clear_otto_caches').on('click', function (e){
2153 e.preventDefault();
2154 clear_otto_caches();
2155 });
2156
2157 // Handle General Setting Page form Submit
2158 $('#metaSyncGeneralSetting').on('submit', function (e) {
2159 // Check if this is a "Clear Error Logs" submission
2160 var formData = $(this).serialize();
2161 if (formData.indexOf('clear_log=yes') !== -1) {
2162 // This is a clear error logs submission - allow normal HTML form submission
2163 console.log('Clear Error Logs form detected - allowing HTML submission');
2164 return true; // Let the form submit normally
2165 }
2166
2167 // Check if this is a "Plugin Access Roles" submission - allow normal HTML form submission
2168 if (formData.indexOf('save_plugin_access_roles=yes') !== -1) {
2169 console.log('Plugin Access Roles form detected - allowing HTML submission');
2170 return true; // Let the form submit normally
2171 }
2172
2173 e.preventDefault(); // Prevent the default form submission for regular settings
2174 var actionField = $(this).find('input[name="action"]');
2175 var optionPage= $(this).find('input[name="option_page"]');
2176 var wpHttpReferer= $(this).find('input[name="_wp_http_referer"]');
2177 var wpnonce= $(this).find('input[name="_wpnonce"]');
2178 if(actionField.length > 0) {
2179 actionField.remove(); // Remove the action field if it exists
2180 optionPage.remove(); // Remove the action field if it exists
2181 wpHttpReferer.remove(); // Remove the action field if it exists
2182 wpnonce.remove(); // Remove the action field if it exists
2183 }
2184 // Re-serialize the form data after removing unwanted fields
2185 formData = $(this).serialize();
2186
2187 // Check if whitelabel data is in form data, if not add it manually
2188 if (formData.indexOf('whitelabel') === -1) {
2189 // Manually collect and add ALL whitelabel fields using helper function
2190 var whitelabelData = collectWhitelabelFields();
2191 if (whitelabelData) {
2192 formData += '&' + whitelabelData;
2193 }
2194 }
2195
2196 // Get current tab from URL
2197 var urlParams = new URLSearchParams(window.location.search);
2198 var currentTab = urlParams.get('tab') || 'general';
2199
2200 $.ajax({
2201 url: metaSync.ajax_url, // The AJAX URL provided by WordPress
2202 type: 'POST',
2203 data: formData + '&action=meta_sync_save_settings&active_tab=' + encodeURIComponent(currentTab), // Add the action and current tab
2204 success: function (response) {
2205 // Handle success response
2206 if(response.success){
2207 // get value of input field white_label_plugin_menu_slug
2208 const whiteLableUrl = $('#metaSyncGeneralSetting input[name="metasync_options[general][white_label_plugin_menu_slug]"]').val();
2209 // check condition if it is empty or not and redirect it
2210
2211 // add the tag query to the window location
2212 let tabParam = new URLSearchParams(window.location.search).get('tab');
2213 let tabQuery = tabParam ? '&tab=' + encodeURIComponent(tabParam) : '';
2214
2215 // Handle undefined or empty white label URL — sanitize to valid slug characters only
2216 const rawSlug = (whiteLableUrl && whiteLableUrl !== '') ? whiteLableUrl : 'searchatlas';
2217 const pageSlug = encodeURIComponent(rawSlug.replace(/[^a-zA-Z0-9_\-]/g, '')) || 'searchatlas';
2218 var redirectUrl = metaSync.admin_url + '?page=' + pageSlug + tabQuery;
2219 try {
2220 var parsedRedirect = new URL(redirectUrl, window.location.origin);
2221 if (parsedRedirect.origin === window.location.origin) {
2222 var navLink = document.createElement('a');
2223 navLink.href = parsedRedirect.href;
2224 navLink.click();
2225 }
2226 } catch (e) { /* invalid URL */ }
2227 }else {
2228 // Handle error response
2229 const errors = response.data?.errors || [];
2230
2231 // Build error notice using DOM methods to avoid XSS
2232 var $errorNotice = $('<div>').addClass('notice notice-error metasync-error-wrap');
2233 if (Array.isArray(errors)) {
2234 var $ul = $('<ul>');
2235 errors.forEach(function (err) {
2236 $ul.append($('<li>').text(err));
2237 });
2238 $errorNotice.append($ul);
2239 }
2240
2241 // Remove previous error notices
2242 $('.metasync-error-wrap').remove();
2243
2244 // Insert the error message before the form
2245 $('#metaSyncGeneralSetting').before($errorNotice);
2246
2247 // Scroll to the top to ensure visibility
2248 $('html, body').animate({ scrollTop: 0 }, 'slow');
2249 }
2250
2251 },
2252 error: function (error) {
2253 // Handle error response
2254 alert('There was an error saving the settings.');
2255 console.log(error);
2256 }
2257 });
2258 });
2259
2260 //hook into heartbeat-send: client will send the message 'marco' in the 'client' var inside the data array
2261 jQuery(document).on('heartbeat-send', function (e, data) {
2262 e.preventDefault();
2263
2264 // adding heart beat label
2265 sendCustomerParams(true);
2266 });
2267
2268 //hook into heartbeat-tick: client looks for a 'server' var in the data array and logs it to console
2269 jQuery(document).on('heartbeat-tick', function (e, data) {
2270 // console.log('heartbeat-tick:', data);
2271 // if(data['server'])
2272 // console.log('Server: ' + data['server']);
2273 });
2274
2275 //hook into heartbeat-error: in case of error, let's log some stuff
2276 jQuery(document).on('heartbeat-error', function (e, jqXHR, textStatus, error) {
2277 console.log('BEGIN ERROR');
2278 console.log(textStatus);
2279 console.log(error);
2280 console.log('END ERROR');
2281 });
2282
2283 // Unsaved changes warning functionality
2284 var hasUnsavedChanges = false;
2285 var initialFormData = {};
2286
2287 // Check if we're on the Advanced tab (has its own save buttons per section)
2288 function isAdvancedTab() {
2289 return window.location.href.indexOf('tab=advanced') > -1;
2290 }
2291
2292 // Initialize form change detection
2293 function initializeUnsavedChangesDetection() {
2294 // Skip unsaved changes detection on Advanced tab - it has its own section-specific save buttons
2295 if (isAdvancedTab()) {
2296 return;
2297 }
2298
2299 var $forms = $('#metaSyncGeneralSetting, #metaSyncSeoControlsForm, form[method="post"][action*="options.php"]');
2300
2301 if ($forms.length === 0) {
2302 return; // No forms to track
2303 }
2304
2305 // Store initial form data
2306 $forms.each(function () {
2307 var formId = $(this).attr('id') || 'form_' + Math.random().toString(36).substr(2, 9);
2308 initialFormData[formId] = $(this).serialize();
2309 });
2310
2311 // Track changes on form inputs
2312 $forms.on('input change', 'input, select, textarea', function () {
2313 checkForChanges();
2314 });
2315
2316 // Special handling for media uploads and other dynamic changes
2317 $forms.on('DOMSubtreeModified', function () {
2318 setTimeout(checkForChanges, 100); // Small delay to allow DOM changes to complete
2319 });
2320 }
2321
2322 // Check if form data has changed
2323 function checkForChanges() {
2324 // Skip on Advanced tab
2325 if (isAdvancedTab()) {
2326 return;
2327 }
2328
2329 var $forms = $('#metaSyncGeneralSetting, form[method="post"][action*="options.php"]');
2330 var currentHasChanges = false;
2331
2332 $forms.each(function () {
2333 var formId = $(this).attr('id') || 'form_' + Math.random().toString(36).substr(2, 9);
2334 var currentData = $(this).serialize();
2335
2336 if (initialFormData[formId] && currentData !== initialFormData[formId]) {
2337 currentHasChanges = true;
2338 }
2339 });
2340
2341 hasUnsavedChanges = currentHasChanges;
2342 updateUnsavedChangesIndicator();
2343 }
2344
2345 // Update visual indicator for unsaved changes
2346 function updateUnsavedChangesIndicator() {
2347 var $saveButtons = $('input[type="submit"], button[type="submit"]').filter('[name="submit"], [value*="Save"]');
2348 var $forms = $('#metaSyncGeneralSetting, form[method="post"][action*="options.php"]');
2349
2350 if (hasUnsavedChanges) {
2351 // Add modern visual indicator to save buttons
2352 $saveButtons.each(function () {
2353 if (!$(this).find('.unsaved-indicator').length) {
2354 $(this).prepend('<span class="unsaved-indicator">●</span>');
2355 }
2356 });
2357
2358 // Add visual styling to forms
2359 $forms.addClass('has-unsaved-changes');
2360
2361 // Show sticky notification
2362 showUnsavedChangesNotification();
2363 } else {
2364 // Remove indicators
2365 $saveButtons.find('.unsaved-indicator').remove();
2366 $forms.removeClass('has-unsaved-changes');
2367
2368 // Hide sticky notification
2369 window.hideUnsavedChangesNotification();
2370 }
2371 }
2372
2373 // Show sticky notification for unsaved changes
2374 function showUnsavedChangesNotification() {
2375 var $notification = $('.metasync-unsaved-notification');
2376
2377 if ($notification.length === 0) {
2378 // Create notification if it doesn't exist
2379 var notificationHTML =
2380 '<div class="metasync-unsaved-notification">' +
2381 '<div class="notification-content">' +
2382 '<div class="notification-icon">●</div>' +
2383 '<div class="notification-message">You have unsaved changes</div>' +
2384 '</div>' +
2385 '<div class="notification-actions">' +
2386 '<button class="notification-button primary" onclick="saveChanges()">Save Now</button>' +
2387 '<button class="notification-button" onclick="discardChanges()">Discard</button>' +
2388 '<button class="close-notification" onclick="hideUnsavedChangesNotification()">×</button>' +
2389 '</div>' +
2390 '</div>';
2391
2392 $('body').append(notificationHTML);
2393 $notification = $('.metasync-unsaved-notification');
2394 }
2395
2396 // Show with animation
2397 setTimeout(function () {
2398 $notification.addClass('show');
2399 }, 100);
2400 }
2401
2402 // Hide sticky notification
2403 window.hideUnsavedChangesNotification = function () {
2404 var $notification = $('.metasync-unsaved-notification');
2405 $notification.removeClass('show');
2406 };
2407
2408 // Scroll to save button functionality
2409 window.scrollToSaveButton = function () {
2410 var $saveButton = $('input[type="submit"], button[type="submit"]').filter('[name="submit"], [value*="Save"]').first();
2411 if ($saveButton.length) {
2412 // Hide notification temporarily while scrolling
2413 window.hideUnsavedChangesNotification();
2414
2415 $('html, body').animate({
2416 scrollTop: $saveButton.offset().top - 100
2417 }, 500, function () {
2418 // Add highlight animation
2419 $saveButton.css('animation', 'save-button-highlight 1s ease-in-out');
2420
2421 // Remove animation after it completes
2422 setTimeout(function () {
2423 $saveButton.css('animation', '');
2424 }, 1000);
2425 });
2426 }
2427 };
2428
2429 // Discard changes functionality
2430 window.discardChanges = function () {
2431 if (confirm('Are you sure you want to discard all unsaved changes? This action cannot be undone.')) {
2432 // Reload the page to discard changes
2433 window.location.reload();
2434 }
2435 };
2436
2437
2438
2439 // Warning for in-page navigation (tab links)
2440 $('.metasync-nav-tab, .nav-tab').on('click', function (e) {
2441 if (hasUnsavedChanges) {
2442 var tabName = $(this).text().trim();
2443 var confirmed = confirm('⚠️ Unsaved Changes Alert\n\nYou have unsaved changes that will be lost if you navigate to "' + tabName + '".\n\nWould you like to:\n• Click "Cancel" to stay and save your changes\n• Click "OK" to discard changes and continue');
2444 if (!confirmed) {
2445 e.preventDefault();
2446 return false;
2447 } else {
2448 // If user confirms, clear the unsaved changes state
2449 hasUnsavedChanges = false;
2450 updateUnsavedChangesIndicator();
2451 }
2452 }
2453 });
2454
2455 // Clear unsaved changes flag when form is successfully submitted
2456 $('#metaSyncGeneralSetting').on('submit', function () {
2457 // Form submission handler already exists above, so we just need to listen for successful response
2458 var originalAjaxHandler = $(this).data('events') && $(this).data('events').submit;
2459 });
2460
2461 // Listen for successful form submission to clear the unsaved changes flag
2462 $(document).ajaxSuccess(function (event, xhr, settings) {
2463 if (settings.data && typeof settings.data === 'string' && settings.data.indexOf('action=meta_sync_save_settings') > -1) {
2464 try {
2465 var response = JSON.parse(xhr.responseText);
2466 if (response.success) {
2467 hasUnsavedChanges = false;
2468 updateUnsavedChangesIndicator();
2469 // Update initial form data after successful save
2470 var $forms = $('#metaSyncGeneralSetting, form[method="post"][action*="options.php"]');
2471 $forms.each(function () {
2472 var formId = $(this).attr('id') || 'form_' + Math.random().toString(36).substr(2, 9);
2473 initialFormData[formId] = $(this).serialize();
2474 });
2475 }
2476 } catch (e) {
2477 // Response is not JSON, ignore
2478 }
2479 }
2480 });
2481
2482 // Save Changes function - use AJAX instead of form submission
2483 window.saveChanges = function () {
2484 isSaving = true;
2485
2486 // Completely remove the floating notification immediately when clicked
2487 var $notification = $('.metasync-unsaved-notification');
2488 if ($notification.length > 0) {
2489 $notification.remove(); // Completely remove from DOM, no animations
2490 }
2491
2492 var $form = $('#metaSyncGeneralSetting');
2493 if ($form.length > 0) {
2494 // Get form data and submit via AJAX
2495 var formData = $form.serialize();
2496
2497 // Ensure ALL whitelabel data is included using helper function
2498 if (formData.indexOf('whitelabel') === -1) {
2499 var whitelabelData = collectWhitelabelFields();
2500 if (whitelabelData) {
2501 formData += '&' + whitelabelData;
2502 }
2503 }
2504
2505 // Get current tab from URL
2506 var urlParams = new URLSearchParams(window.location.search);
2507 var currentTab = urlParams.get('tab') || 'general';
2508
2509 formData += '&action=meta_sync_save_settings&active_tab=' + encodeURIComponent(currentTab);
2510
2511 $.ajax({
2512 url: metaSync.ajax_url,
2513 type: 'POST',
2514 data: formData,
2515 success: function (response) {
2516 if (response.success) {
2517 // Clear unsaved changes flag
2518 hasUnsavedChanges = false;
2519 updateUnsavedChangesIndicator();
2520
2521 // Clear any previous save notices (success, error, or warning)
2522 $('.metasync-save-notice').remove();
2523
2524 // Show temporary success indication in plugin area
2525 var noticeType, noticeMessage, noAutoDismiss = false;
2526 if (response.data && response.data.api_key_validated === true) {
2527 noticeType = 'notice-success';
2528 noticeMessage = $('<span/>').text('Settings saved successfully! API key verified & connected. Reloading\u2026').html();
2529 updateHeaderStatus(true, 'Synced', getPluginName() + ' connected');
2530 } else if (response.data && response.data.api_key_validated === false) {
2531 noticeType = 'notice-error';
2532 noticeMessage = $('<span/>').text(response.data.warning || 'The API key could not be verified. Your other settings were saved.').html();
2533 noAutoDismiss = true;
2534 // Revert the input field to the previously saved valid key from the backend
2535 var revertKey = (response.data.previous_api_key !== undefined) ? response.data.previous_api_key : '';
2536 $('#searchatlas-api-key').val(revertKey);
2537 } else if (response.data && response.data.api_key_removed === true) {
2538 // API key was cleared — show warning, update status, page will reload
2539 noticeType = 'notice-warning';
2540 noticeMessage = $('<span/>').text((response.data.warning || 'The API key has been removed.') + ' Reloading\u2026').html();
2541 updateHeaderStatus(false, 'Disconnected', getPluginName() + ' disconnected');
2542 } else if (response.data && response.data.warning) {
2543 // Network failure — key saved but unverified
2544 noticeType = 'notice-warning';
2545 noticeMessage = $('<span/>').text(response.data.warning).html();
2546 } else {
2547 noticeType = 'notice-success';
2548 noticeMessage = $('<span/>').text('Settings saved successfully!').html();
2549 }
2550 var successNotice = '<div class="notice ' + noticeType + ' is-dismissible metasync-save-notice" style="margin: 20px 0; padding: 12px;"><p><strong>' + noticeMessage + '</strong></p></div>';
2551
2552 // Insert between navigation menu and page content
2553 var $navWrapper = $('.metasync-nav-wrapper');
2554 if ($navWrapper.length > 0) {
2555 // Position after navigation menu but before first dashboard card or form
2556 $navWrapper.after(successNotice);
2557 } else {
2558 // Fallback: insert at top of settings page
2559 $('.metasync-dashboard-wrap').prepend(successNotice);
2560 }
2561
2562 // Scroll to the notice for better visibility
2563 $('html, body').animate({ scrollTop: 0 }, 'slow');
2564
2565 // Reload page when API key status changed so server-rendered
2566 // sections (One-Click Authentication, promo sidebar) update
2567 if (response.data && (response.data.api_key_validated === true || response.data.api_key_removed === true)) {
2568 setTimeout(function () {
2569 location.reload();
2570 }, 1500);
2571 return;
2572 }
2573
2574 // Auto-dismiss success/warning notices; keep errors visible
2575 if (!noAutoDismiss) {
2576 setTimeout(function () {
2577 $('.metasync-save-notice').fadeOut(300, function () {
2578 $(this).remove();
2579 });
2580 }, 3000);
2581 }
2582 } else {
2583 // Handle validation errors
2584 var errors = response.data && response.data.errors ? response.data.errors : [];
2585
2586 // Build error notice using DOM methods to avoid XSS
2587 var $errorNotice = $('<div>').addClass('notice notice-error metasync-error-wrap').css({ margin: '20px', padding: '12px' });
2588 if (Array.isArray(errors)) {
2589 var $ul = $('<ul>');
2590 for (var i = 0; i < errors.length; i++) {
2591 $ul.append($('<li>').text(errors[i]));
2592 }
2593 $errorNotice.append($ul);
2594 } else {
2595 var message = 'An error occurred while saving settings.';
2596 if (response.data && response.data.message) {
2597 message = response.data.message;
2598 }
2599 $errorNotice.append($('<p>').text(message));
2600 }
2601
2602 // Insert error notice in plugin area
2603 var $navWrapper = $('.metasync-nav-wrapper');
2604 if ($navWrapper.length > 0) {
2605 $navWrapper.after($errorNotice);
2606 } else {
2607 // Fallback: insert at top of plugin content area
2608 $('.metasync-dashboard-wrap').prepend($errorNotice);
2609 }
2610
2611 // Scroll to the error message for better visibility
2612 $('html, body').animate({ scrollTop: 0 }, 'slow');
2613
2614 setTimeout(function () {
2615 $('.metasync-error-wrap').fadeOut(300, function () {
2616 $(this).remove();
2617 });
2618 }, 5000);
2619 }
2620 isSaving = false;
2621 },
2622 error: function (xhr, status, error) {
2623 // Handle AJAX error
2624 var errorMessage = 'There was an error saving the settings. Please try again.';
2625 if (xhr.responseJSON && xhr.responseJSON.data && xhr.responseJSON.data.message) {
2626 errorMessage = xhr.responseJSON.data.message;
2627 }
2628
2629 var ajaxErrorNotice = '<div class="notice notice-error is-dismissible metasync-ajax-error" style="margin: 20px 0; padding: 12px;"><p><strong>❌ Error:</strong> ' + errorMessage + '</p></div>';
2630
2631 // Insert between navigation menu and page content
2632 var $navWrapper = $('.metasync-nav-wrapper');
2633 if ($navWrapper.length > 0) {
2634 // Position after navigation menu but before first dashboard card or form
2635 $navWrapper.after(ajaxErrorNotice);
2636 } else {
2637 // Fallback: insert at top of settings page
2638 $('.metasync-dashboard-wrap').prepend(ajaxErrorNotice);
2639 }
2640
2641 // Scroll to the error message for better visibility
2642 $('html, body').animate({ scrollTop: 0 }, 'slow');
2643
2644 setTimeout(function () {
2645 $('.metasync-ajax-error').fadeOut(300, function () {
2646 $(this).remove();
2647 });
2648 }, 5000);
2649
2650 isSaving = false;
2651 }
2652 });
2653 }
2654 };
2655
2656 // Enhanced beforeunload message (disabled when saving)
2657 var isSaving = false;
2658 $(window).on('beforeunload', function (e) {
2659 if (hasUnsavedChanges && !isSaving) {
2660 var message = '🔄 You have unsaved changes in MetaSync settings that will be lost if you leave this page.';
2661 e.returnValue = message; // For older browsers
2662 return message;
2663 }
2664 });
2665
2666 // Initialize the detection when page loads
2667 setTimeout(initializeUnsavedChangesDetection, 1000); // Small delay to ensure all elements are loaded
2668
2669 // Indexation Control form AJAX save functionality
2670 function initializeSeoControlsSaveHandler() {
2671 // Only initialize if we're on the Indexation Control page
2672 if ($('#metaSyncSeoControlsForm').length > 0) {
2673 // Override the saveChanges function for Indexation Control form
2674 window.saveChanges = function () {
2675 var $form = $('#metaSyncSeoControlsForm');
2676 if ($form.length > 0) {
2677 // Get form data and submit via AJAX
2678 var formData = $form.serialize();
2679 formData += '&action=meta_sync_save_seo_controls';
2680
2681 // Remove the notification immediately
2682 var $notification = $('.metasync-unsaved-notification');
2683 if ($notification.length > 0) {
2684 $notification.remove();
2685 }
2686
2687 // Clear previous messages
2688 $('#seo-controls-messages').empty();
2689
2690 $.ajax({
2691 url: ajaxurl || metaSync.ajax_url,
2692 type: 'POST',
2693 data: formData,
2694 dataType: 'json',
2695 success: function (response) {
2696 if (response.success) {
2697 // Show success message above Indexation Control section
2698 var $successNotice = $('<div>').addClass('notice notice-success is-dismissible').css('margin-bottom', '20px');
2699 var $successP = $('<p>');
2700 $successP.append($('<strong>').text('�
2701 Success! '));
2702 $successP[0].appendChild(document.createTextNode(response.data.message));
2703 $successNotice.append($successP);
2704 $('#seo-controls-messages').empty().append($successNotice);
2705
2706 // Clear unsaved changes state
2707 hasUnsavedChanges = false;
2708 if (typeof updateUnsavedChangesIndicator === 'function') {
2709 updateUnsavedChangesIndicator();
2710 }
2711
2712 // Update initial form data to current state after successful save
2713 var formId = $form.attr('id') || 'metaSyncSeoControlsForm';
2714 initialFormData[formId] = $form.serialize();
2715
2716 // Auto-hide success notice after 5 seconds
2717 setTimeout(function () {
2718 $('#seo-controls-messages .notice-success').fadeOut();
2719 }, 5000);
2720 } else {
2721 // Show error message above Indexation Control section
2722 var $errorNoticeCtrl = $('<div>').addClass('notice notice-error is-dismissible').css('margin-bottom', '20px');
2723 var $errorPCtrl = $('<p>');
2724 $errorPCtrl.append($('<strong>').text('❌ Error! '));
2725 $errorPCtrl[0].appendChild(document.createTextNode(response.data.message || 'Failed to save settings'));
2726 $errorNoticeCtrl.append($errorPCtrl);
2727 $('#seo-controls-messages').empty().append($errorNoticeCtrl);
2728 }
2729
2730 // Make dismiss buttons work
2731 $('#seo-controls-messages .notice-dismissible').each(function () {
2732 var $notice = $(this);
2733 if (!$notice.find('.notice-dismiss').length) {
2734 $notice.append('<button type="button" class="notice-dismiss"><span class="screen-reader-text">Dismiss this notice.</span></button>');
2735 }
2736 $notice.find('.notice-dismiss').on('click', function () {
2737 $notice.fadeOut();
2738 });
2739 });
2740 },
2741 error: function (xhr, status, error) {
2742 // Show error message above Indexation Control section
2743 $('#seo-controls-messages').html(
2744 '<div class="notice notice-error is-dismissible" style="margin-bottom: 20px;">' +
2745 '<p><strong>❌ Error!</strong> Network error occurred while saving. Please check your connection and try again.</p>' +
2746 '</div>'
2747 );
2748 console.error('AJAX Error:', error);
2749 console.error('XHR Status:', xhr.status);
2750 console.error('XHR Response:', xhr.responseText);
2751
2752 // Don't clear unsaved changes on network error
2753 // User may want to retry or fix the issue
2754 },
2755 complete: function () {
2756 // Re-enable save button if it was disabled
2757 $('.metasync-save-button').prop('disabled', false);
2758 }
2759 });
2760 } else {
2761 // Fallback to regular form submission
2762 $('form[method="post"]').first().submit();
2763 }
2764 };
2765 }
2766 }
2767
2768 // Initialize Indexation Control functionality
2769 initializeSeoControlsSaveHandler();
2770
2771 /**
2772 * Handle "All Roles" checkbox behavior for Content Genius sync
2773 * When "All Roles" is checked, uncheck all other role checkboxes
2774 * When any specific role is checked, uncheck "All Roles"
2775 *
2776 * @since 1.0.0
2777 * @returns {void}
2778 */
2779 function initializeContentGeniusRoleCheckboxes() {
2780 try {
2781 // Cache selectors for better performance
2782 var allRolesCheckbox = $('input[name="metasync_options[general][content_genius_sync_roles][]"][value="all"]');
2783 var roleCheckboxes = $('input[name="metasync_options[general][content_genius_sync_roles][]"]').not('[value="all"]');
2784
2785 // Exit early if elements don't exist
2786 if (allRolesCheckbox.length === 0 && roleCheckboxes.length === 0) {
2787 return;
2788 }
2789
2790 // When "All Roles" is checked, uncheck all other checkboxes
2791 if (allRolesCheckbox.length > 0) {
2792 allRolesCheckbox.off('change.metasyncRoles').on('change.metasyncRoles', function () {
2793 try {
2794 if ($(this).is(':checked')) {
2795 roleCheckboxes.prop('checked', false);
2796 // Visual feedback
2797 roleCheckboxes.closest('.metasync-role-option').removeClass('active');
2798 $(this).closest('.metasync-role-option-all').addClass('active');
2799 } else {
2800 $(this).closest('.metasync-role-option-all').removeClass('active');
2801 }
2802 } catch (e) {
2803 console.warn('MetaSync: Error handling "All Roles" checkbox change:', e);
2804 }
2805 });
2806 }
2807
2808 // When any specific role is checked, uncheck "All Roles"
2809 if (roleCheckboxes.length > 0) {
2810 roleCheckboxes.off('change.metasyncRoles').on('change.metasyncRoles', function () {
2811 try {
2812 if ($(this).is(':checked')) {
2813 allRolesCheckbox.prop('checked', false);
2814 allRolesCheckbox.closest('.metasync-role-option-all').removeClass('active');
2815 // Visual feedback
2816 $(this).closest('.metasync-role-option').addClass('active');
2817 } else {
2818 $(this).closest('.metasync-role-option').removeClass('active');
2819 }
2820 } catch (e) {
2821 console.warn('MetaSync: Error handling role checkbox change:', e);
2822 }
2823 });
2824 }
2825
2826 // Initialize active states on page load
2827 initializeRoleCheckboxStates();
2828
2829 } catch (error) {
2830 console.error('MetaSync: Failed to initialize Content Genius role checkboxes:', error);
2831 }
2832 }
2833
2834 /**
2835 * Initialize active states for role checkboxes on page load
2836 *
2837 * @since 1.0.0
2838 * @returns {void}
2839 */
2840 function initializeRoleCheckboxStates() {
2841 try {
2842 var allRolesCheckbox = $('input[name="metasync_options[general][content_genius_sync_roles][]"][value="all"]');
2843 var roleCheckboxes = $('input[name="metasync_options[general][content_genius_sync_roles][]"]').not('[value="all"]');
2844
2845 // Set active state for "All Roles" if checked
2846 if (allRolesCheckbox.is(':checked')) {
2847 allRolesCheckbox.closest('.metasync-role-option-all').addClass('active');
2848 }
2849
2850 // Set active state for checked roles
2851 roleCheckboxes.each(function () {
2852 if ($(this).is(':checked')) {
2853 $(this).closest('.metasync-role-option').addClass('active');
2854 }
2855 });
2856 } catch (e) {
2857 console.warn('MetaSync: Error initializing role checkbox states:', e);
2858 }
2859 }
2860
2861 // Initialize Content Genius role checkboxes with safety wrapper
2862 if (typeof $ !== 'undefined' && $.fn) {
2863 initializeContentGeniusRoleCheckboxes();
2864 } else {
2865 console.warn('MetaSync: jQuery not available for role checkbox initialization');
2866 }
2867
2868 // ========================================
2869 // SETTINGS ACCORDION FUNCTIONALITY
2870 // ========================================
2871
2872 /**
2873 * Initialize accordion sections with localStorage state persistence
2874 */
2875 function initSettingsAccordion() {
2876 var $accordionSections = $('.metasync-accordion-section');
2877
2878 if ($accordionSections.length === 0) {
2879 return; // No accordion on this page
2880 }
2881
2882 console.log('🎨 Initializing settings accordion with ' + $accordionSections.length + ' sections');
2883
2884 // Restore saved state from localStorage
2885 restoreAccordionState();
2886
2887 // Click handler for accordion headers
2888 $('.metasync-accordion-header').on('click', function (e) {
2889 toggleAccordionSection($(this));
2890 });
2891
2892 // Keyboard navigation (Enter/Space)
2893 $('.metasync-accordion-header').on('keydown', function (e) {
2894 if (e.key === 'Enter' || e.key === ' ') {
2895 e.preventDefault();
2896 toggleAccordionSection($(this));
2897 }
2898 });
2899 }
2900
2901 /**
2902 * Toggle accordion section open/closed
2903 * @param {jQuery} $header - The clicked header element
2904 */
2905 function toggleAccordionSection($header) {
2906 var $section = $header.closest('.metasync-accordion-section');
2907 var $content = $section.find('.metasync-accordion-content');
2908 var isOpen = $header.attr('aria-expanded') === 'true';
2909 var sectionKey = $section.data('section');
2910
2911 if (isOpen) {
2912 // Close section
2913 $header.attr('aria-expanded', 'false');
2914 $content.attr('data-state', 'closed');
2915 console.log('📁 Closed accordion section: ' + sectionKey);
2916 } else {
2917 // Open section
2918 $header.attr('aria-expanded', 'true');
2919 $content.attr('data-state', 'open');
2920 console.log('📂 Opened accordion section: ' + sectionKey);
2921 }
2922
2923 // Save state to localStorage
2924 saveAccordionState();
2925 }
2926
2927 /**
2928 * Save accordion state to localStorage
2929 */
2930 function saveAccordionState() {
2931 var state = {};
2932
2933 $('.metasync-accordion-section').each(function () {
2934 var sectionKey = $(this).data('section');
2935 var isOpen = $(this).find('.metasync-accordion-header').attr('aria-expanded') === 'true';
2936 state[sectionKey] = isOpen;
2937 });
2938
2939 try {
2940 localStorage.setItem('metasync_accordion_state', JSON.stringify(state));
2941 } catch (e) {
2942 console.warn('⚠️ Could not save accordion state to localStorage:', e);
2943 }
2944 }
2945
2946 /**
2947 * Restore accordion state from localStorage
2948 */
2949 function restoreAccordionState() {
2950 try {
2951 var savedState = localStorage.getItem('metasync_accordion_state');
2952 if (!savedState) {
2953 return; // No saved state, use defaults
2954 }
2955
2956 var state = JSON.parse(savedState);
2957
2958 $('.metasync-accordion-section').each(function () {
2959 var $section = $(this);
2960 var sectionKey = $section.data('section');
2961 var $header = $section.find('.metasync-accordion-header');
2962 var $content = $section.find('.metasync-accordion-content');
2963
2964 if (Object.prototype.hasOwnProperty.call(state, sectionKey)) {
2965 var shouldBeOpen = state[sectionKey];
2966 $header.attr('aria-expanded', shouldBeOpen ? 'true' : 'false');
2967 $content.attr('data-state', shouldBeOpen ? 'open' : 'closed');
2968 }
2969 });
2970
2971 console.log('💾 Restored accordion state from localStorage');
2972 } catch (e) {
2973 console.warn('⚠️ Could not restore accordion state:', e);
2974 }
2975 }
2976
2977 // Initialize accordion when DOM is ready
2978 initSettingsAccordion();
2979
2980 // ========================================
2981 // TOOLTIP SYSTEM
2982 // ========================================
2983
2984 /**
2985 * Initialize tooltip functionality
2986 */
2987 function initTooltipSystem() {
2988 console.log('🔍 Tooltip system initialization started');
2989
2990 var $tooltipTriggers = $('.metasync-tooltip-trigger');
2991 var currentTooltip = null;
2992
2993 console.log('🔍 Found ' + $tooltipTriggers.length + ' tooltip triggers');
2994
2995 if ($tooltipTriggers.length === 0) {
2996 console.warn('⚠️ No tooltip triggers found on this page');
2997 return; // No tooltips on this page
2998 }
2999
3000 console.log('💡 Initializing tooltip system with ' + $tooltipTriggers.length + ' tooltips');
3001
3002 // Click handler for tooltip triggers
3003 $tooltipTriggers.on('click', function (e) {
3004 e.preventDefault();
3005 e.stopPropagation();
3006
3007 var $trigger = $(this);
3008 var tooltipId = $trigger.data('tooltip-id');
3009 var $tooltip = $('#tooltip-' + tooltipId);
3010
3011 console.log('🖱️ Tooltip trigger clicked:', tooltipId);
3012 console.log('🎯 Tooltip element found:', $tooltip.length);
3013
3014 // Close other tooltips
3015 if (currentTooltip && currentTooltip[0] !== $tooltip[0]) {
3016 currentTooltip.removeClass('show');
3017 }
3018
3019 // Toggle current tooltip
3020 if ($tooltip.hasClass('show')) {
3021 $tooltip.removeClass('show');
3022 currentTooltip = null;
3023 } else {
3024 $tooltip.addClass('show');
3025 currentTooltip = $tooltip;
3026 positionTooltip($trigger, $tooltip);
3027 }
3028 });
3029
3030 // Hover handler (desktop only)
3031 var hideTimeout;
3032
3033 if (window.innerWidth > 768) {
3034 // Show tooltip on trigger hover
3035 $tooltipTriggers.on('mouseenter', function () {
3036 var $trigger = $(this);
3037 var tooltipId = $trigger.data('tooltip-id');
3038 var $tooltip = $('#tooltip-' + tooltipId);
3039
3040 // Clear any pending hide timeout
3041 clearTimeout(hideTimeout);
3042
3043 console.log('🖱️ Hover on trigger:', tooltipId);
3044
3045 // Close other tooltips
3046 if (currentTooltip && currentTooltip[0] !== $tooltip[0]) {
3047 currentTooltip.removeClass('show');
3048 }
3049
3050 $tooltip.addClass('show');
3051 currentTooltip = $tooltip;
3052 positionTooltip($trigger, $tooltip);
3053 });
3054
3055 // Start hide timer when leaving trigger
3056 $tooltipTriggers.on('mouseleave', function () {
3057 var tooltipId = $(this).data('tooltip-id');
3058 var $tooltip = $('#tooltip-' + tooltipId);
3059
3060 console.log('🖱️ Mouse left trigger:', tooltipId);
3061
3062 // Delay hiding to allow moving to tooltip
3063 hideTimeout = setTimeout(function () {
3064 // Only hide if not hovering tooltip
3065 if (!$tooltip.is(':hover')) {
3066 console.log('⏱️ Hiding tooltip:', tooltipId);
3067 $tooltip.removeClass('show');
3068 if (currentTooltip && currentTooltip[0] === $tooltip[0]) {
3069 currentTooltip = null;
3070 }
3071 } else {
3072 console.log('✋ Mouse is over tooltip, keeping visible');
3073 }
3074 }, 200);
3075 });
3076
3077 // Cancel hide when entering tooltip
3078 $('.metasync-tooltip').on('mouseenter', function () {
3079 console.log('🎯 Mouse entered tooltip');
3080 clearTimeout(hideTimeout);
3081 $(this).addClass('show');
3082 });
3083
3084 // Hide when leaving tooltip
3085 $('.metasync-tooltip').on('mouseleave', function () {
3086 console.log('🎯 Mouse left tooltip');
3087 var $tooltip = $(this);
3088
3089 hideTimeout = setTimeout(function () {
3090 var tooltipId = $tooltip.attr('id').replace('tooltip-', '');
3091 var $trigger = $('[data-tooltip-id="' + tooltipId + '"]');
3092
3093 // Only hide if not hovering trigger
3094 if (!$trigger.is(':hover')) {
3095 console.log('⏱️ Hiding tooltip from tooltip leave');
3096 $tooltip.removeClass('show');
3097 if (currentTooltip && currentTooltip[0] === $tooltip[0]) {
3098 currentTooltip = null;
3099 }
3100 } else {
3101 console.log('✋ Mouse is back on trigger, keeping visible');
3102 }
3103 }, 200);
3104 });
3105 }
3106
3107 // Close tooltip when clicking outside
3108 $(document).on('click', function (e) {
3109 if (!$(e.target).closest('.metasync-tooltip-trigger, .metasync-tooltip').length) {
3110 if (currentTooltip) {
3111 currentTooltip.removeClass('show');
3112 currentTooltip = null;
3113 }
3114 }
3115 });
3116
3117 // Keyboard accessibility - ESC to close
3118 $(document).on('keydown', function (e) {
3119 if (e.key === 'Escape' && currentTooltip) {
3120 currentTooltip.removeClass('show');
3121 currentTooltip = null;
3122 }
3123 });
3124
3125 // Reposition tooltips on window resize
3126 $(window).on('resize', function () {
3127 if (currentTooltip) {
3128 var tooltipId = currentTooltip.attr('id').replace('tooltip-', '');
3129 var $trigger = $('[data-tooltip-id="' + tooltipId + '"]');
3130 positionTooltip($trigger, currentTooltip);
3131 }
3132 });
3133 }
3134
3135 /**
3136 * Position tooltip relative to trigger
3137 * @param {jQuery} $trigger - The trigger button
3138 * @param {jQuery} $tooltip - The tooltip element
3139 */
3140 function positionTooltip($trigger, $tooltip) {
3141 // Skip positioning on mobile (uses fixed positioning)
3142 if (window.innerWidth <= 768) {
3143 return;
3144 }
3145
3146 var triggerRect = $trigger[0].getBoundingClientRect();
3147 var tooltipWidth = $tooltip.outerWidth();
3148 var viewportWidth = $(window).width();
3149 var spaceRight = viewportWidth - triggerRect.right;
3150
3151 // Check if tooltip would overflow on the right
3152 if (spaceRight < tooltipWidth + 20) {
3153 // Position on the left side
3154 $tooltip.attr('data-position', 'left');
3155 } else {
3156 // Position on the right side (default)
3157 $tooltip.attr('data-position', 'right');
3158 }
3159 }
3160
3161 // Initialize tooltip system
3162 initTooltipSystem();
3163
3164 // PR3: Burst ping — 10 min polling when UNREGISTERED or KEY_PENDING. Stop after 5 failed attempts.
3165 (function () {
3166 if (typeof metaSync === 'undefined' || !metaSync.heartbeat_state) {
3167 return;
3168 }
3169 var state = metaSync.heartbeat_state;
3170 if (state !== 'UNREGISTERED' && state !== 'KEY_PENDING') {
3171 return;
3172 }
3173
3174 var INTERVAL_MS = 10 * 60 * 1000;
3175 var MAX_ATTEMPTS = 5;
3176 var attempts = 0;
3177 var lastState = state;
3178 var intervalId = null;
3179
3180 function stop() {
3181 if (intervalId) {
3182 clearInterval(intervalId);
3183 intervalId = null;
3184 }
3185 }
3186
3187 intervalId = setInterval(function () {
3188 $.post(metaSync.ajax_url, {
3189 action: 'metasync_burst_ping',
3190 nonce: metaSync.burst_ping_nonce
3191 })
3192 .done(function (res) {
3193 if (!res || !res.data) {
3194 attempts++;
3195 if (attempts >= MAX_ATTEMPTS) {
3196 stop();
3197 }
3198 return;
3199 }
3200 var data = res.data;
3201 var newState = data.state || lastState;
3202 if (newState !== lastState) {
3203 attempts = 0;
3204 lastState = newState;
3205 }
3206 if (data.heartbeat_confirmed || newState === 'CONNECTED') {
3207 stop();
3208 if (newState === 'CONNECTED' && typeof updateHeaderStatus === 'function') {
3209 var hasOttoUuid = typeof metaSync !== 'undefined' && metaSync.otto_pixel_uuid && metaSync.otto_pixel_uuid.trim() !== '';
3210 if (hasOttoUuid) {
3211 updateHeaderStatus(true, 'Synced', 'Heartbeat confirmed');
3212 } else {
3213 updateHeaderStatus(false, 'Warning', 'Connected but OTTO UUID is missing — deploys will not work. Please reconnect.');
3214 }
3215 }
3216 return;
3217 }
3218 attempts++;
3219 if (attempts >= MAX_ATTEMPTS) {
3220 stop();
3221 }
3222 })
3223 .fail(function () {
3224 attempts++;
3225 if (attempts >= MAX_ATTEMPTS) {
3226 stop();
3227 }
3228 });
3229 }, INTERVAL_MS);
3230 })();
3231
3232 });
3233
3234 })(jQuery);
3235