PluginProbe
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO / 1.0.0
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO v1.0.0
2.7.0 2.6.0 2.5.0 2.4.0 2.3.0 2.2.0 2.1.1 2.1.0 2.0.2 2.0.1 2.0.0 1.32.0 1.31.0 1.30.0 1.29.0 1.28.0 1.27.0 1.26.0 1.25.0 trunk 1.0.0 1.0.1 1.0.2 1.1.0 1.10.0 All 48 releases
thinkrank / src / admin / components / essential-seo / IntegrationsTab.js

IntegrationsTab.js in ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO 1.0.0, at src/admin/components/essential-seo/IntegrationsTab.js

1,006 lines 45.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /**
2 * Integrations Tab Component
3 *
4 * Centralized management for all external API integrations
5 * Following DRY and KISS principles for clean, maintainable code
6 *
7 * @package ThinkRank
8 * @since 1.0.0
9 */
10
11 import { useState, useEffect } from '@wordpress/element';
12 import { __ } from '@wordpress/i18n';
13 import {
14 Card,
15 CardBody,
16 CardHeader,
17 Spinner,
18 Notice,
19 TextControl,
20 ToggleControl,
21 Button,
22 Flex,
23 FlexItem,
24 __experimentalText as Text
25 } from '@wordpress/components';
26 import apiFetch from '@wordpress/api-fetch';
27
28 /**
29 * Get boolean settings list for integrations
30 * Following schema settings pattern for consistent boolean handling
31 */
32 const getBooleanIntegrationsSettings = () => [
33 'ga4_auto_inject',
34 'ga4_anonymize_ip',
35 'ga4_exclude_admin',
36 'ga4_tracking_verified',
37 'auto_test_connections',
38 'retry_failed_requests'
39 ];
40
41 /**
42 * Normalize integrations settings data types
43 * Following schema settings pattern for consistent boolean handling
44 */
45 const normalizeIntegrationsDataTypes = (settings) => {
46 const normalized = { ...settings };
47
48 // Convert boolean settings
49 getBooleanIntegrationsSettings().forEach(key => {
50 if (key in normalized) {
51 normalized[key] = Boolean(normalized[key]);
52 }
53 });
54
55 return normalized;
56 };
57
58 /**
59 * Default integration settings
60 * KISS: Simple, flat structure for Google API keys only
61 */
62 const getDefaultIntegrationsSettings = () => ({
63 // Google API Keys
64 google_analytics_api_key: '',
65 google_search_console_api_key: '',
66 google_pagespeed_api_key: '',
67
68 // GA4 Tracking Settings
69 ga4_measurement_id: '',
70 ga4_auto_inject: false,
71 ga4_anonymize_ip: false,
72 ga4_exclude_admin: false,
73 ga4_tracking_verified: false,
74 ga4_last_verification: '',
75
76 // API Configuration
77 api_timeout: 30,
78 cache_duration: 3600,
79
80 // Connection settings
81 auto_test_connections: true,
82 retry_failed_requests: true
83 });
84
85 /**
86 * Default social media settings
87 * DRY: Reuse existing social media settings structure
88 */
89 const getDefaultSocialSettings = () => ({
90 // Facebook settings
91 facebook_app_id: '',
92 facebook_admins: '',
93
94 // Pinterest settings
95 pinterest_site_verification: '',
96
97 // Instagram settings
98 instagram_verification: '',
99
100 // TikTok settings
101 tiktok_verification: '',
102
103 // YouTube settings
104 youtube_channel_id: '',
105
106 // WhatsApp Business settings
107 whatsapp_business_id: ''
108 });
109
110 /**
111 * Integrations Tab Component
112 * DRY: Reuses patterns from Analytics tab but simplified
113 */
114 const IntegrationsTab = ({ activeSubSection = 'google-services' }) => {
115 const [isLoading, setIsLoading] = useState(true);
116 const [isSaving, setIsSaving] = useState(false);
117 const [isTestingConnection, setIsTestingConnection] = useState(false);
118 const [integrationsSettings, setIntegrationsSettings] = useState(getDefaultIntegrationsSettings());
119 const [socialSettings, setSocialSettings] = useState(getDefaultSocialSettings());
120 const [hasChanges, setHasChanges] = useState(false);
121 const [notice, setNotice] = useState(null);
122 const [connectionStatus, setConnectionStatus] = useState(null);
123
124 // GA4 Tracking specific state
125 const [conflicts, setConflicts] = useState([]);
126 const [isCheckingConflicts, setIsCheckingConflicts] = useState(false);
127 const [verificationResult, setVerificationResult] = useState(null);
128 const [isVerifying, setIsVerifying] = useState(false);
129
130 // Load settings on mount
131 useEffect(() => {
132 loadSettings();
133 }, []);
134
135 /**
136 * Load integration settings
137 * DRY: Reuses Settings Manager pattern for both categories
138 */
139 const loadSettings = async () => {
140 try {
141 setIsLoading(true);
142
143 // Load integrations settings (Google API keys)
144 const integrationsResponse = await apiFetch({
145 path: '/thinkrank/v1/integrations/settings',
146 method: 'GET'
147 });
148
149 // Load social platform settings (IDs and verification codes with selective encryption)
150 const socialResponse = await apiFetch({
151 path: '/thinkrank/v1/social-platforms/settings',
152 method: 'GET'
153 });
154
155 if (integrationsResponse.success) {
156 const loadedSettings = { ...getDefaultIntegrationsSettings(), ...integrationsResponse.data.settings };
157
158 // Masked API keys (XXXX pattern) will be displayed in password fields
159 // The save logic prevents these masked values from being sent back to the server
160
161 // Normalize boolean values to ensure proper types
162 const normalizedSettings = normalizeIntegrationsDataTypes(loadedSettings);
163 setIntegrationsSettings(normalizedSettings);
164 } else {
165 setIntegrationsSettings(getDefaultIntegrationsSettings());
166 }
167
168 if (socialResponse.success) {
169 const loadedSocialSettings = { ...getDefaultSocialSettings(), ...socialResponse.data.settings };
170
171 // Masked verification codes (XXXX pattern) will be displayed in text fields
172 // Public IDs (facebook_app_id, facebook_admins, youtube_channel_id, whatsapp_business_id) are not encrypted and show full values
173 // Verification codes (pinterest_site_verification, instagram_verification, tiktok_verification) are encrypted and show masked values
174 // The save logic prevents masked values from being sent back to the server
175
176 setSocialSettings(loadedSocialSettings);
177 } else {
178 setSocialSettings(getDefaultSocialSettings());
179 }
180 } catch (error) {
181 console.error('Failed to load integration settings:', error);
182 setNotice({
183 status: 'error',
184 message: __('Failed to load integration settings. Please refresh the page.', 'thinkrank')
185 });
186 } finally {
187 setIsLoading(false);
188 }
189 };
190
191 /**
192 * Handle integrations setting changes (Google API keys)
193 * KISS: Simple state update pattern
194 */
195 const handleIntegrationsSettingChange = (key, value) => {
196 setIntegrationsSettings(prev => ({ ...prev, [key]: value }));
197 setHasChanges(true);
198 setNotice(null);
199 };
200
201 /**
202 * Handle social setting changes (Platform IDs and verification codes)
203 * DRY: Same pattern as integrations settings
204 */
205 const handleSocialSettingChange = (key, value) => {
206 setSocialSettings(prev => ({ ...prev, [key]: value }));
207 setHasChanges(true);
208 setNotice(null);
209 };
210
211 /**
212 * Save settings for current active tab only
213 * KISS: Only save the current tab's settings
214 */
215 const saveSettings = async () => {
216 try {
217 setIsSaving(true);
218 setNotice(null);
219
220 if (activeSubSection === 'google-services') {
221 // Save Google Services (Integrations) settings only
222 const integrationsData = { ...integrationsSettings };
223
224 // Remove empty API keys and masked placeholders to avoid overwriting existing encrypted keys
225 if (!integrationsData.google_analytics_api_key || !integrationsData.google_analytics_api_key.trim() || integrationsData.google_analytics_api_key.includes('XXXX')) {
226 delete integrationsData.google_analytics_api_key;
227 }
228 if (!integrationsData.google_search_console_api_key || !integrationsData.google_search_console_api_key.trim() || integrationsData.google_search_console_api_key.includes('XXXX')) {
229 delete integrationsData.google_search_console_api_key;
230 }
231 if (!integrationsData.google_pagespeed_api_key || !integrationsData.google_pagespeed_api_key.trim() || integrationsData.google_pagespeed_api_key.includes('XXXX')) {
232 delete integrationsData.google_pagespeed_api_key;
233 }
234
235 // Normalize boolean values before saving (following schema settings pattern)
236 const normalizedIntegrationsData = normalizeIntegrationsDataTypes(integrationsData);
237
238 const response = await apiFetch({
239 path: '/thinkrank/v1/integrations/settings',
240 method: 'POST',
241 data: { settings: normalizedIntegrationsData }
242 });
243
244 if (response.success) {
245 setHasChanges(false);
246 setNotice({
247 status: 'success',
248 message: __('Google Services settings saved successfully!', 'thinkrank')
249 });
250 } else {
251 throw new Error('Failed to save Google Services settings');
252 }
253
254 } else if (activeSubSection === 'social-platforms') {
255 // Save Social Platforms settings only
256 const socialData = { ...socialSettings };
257
258 // Remove empty verification codes to avoid overwriting existing encrypted codes
259 if (!socialData.pinterest_site_verification?.trim()) {
260 delete socialData.pinterest_site_verification;
261 }
262 if (!socialData.instagram_verification?.trim()) {
263 delete socialData.instagram_verification;
264 }
265 if (!socialData.tiktok_verification?.trim()) {
266 delete socialData.tiktok_verification;
267 }
268
269 const response = await apiFetch({
270 path: '/thinkrank/v1/social-platforms/settings',
271 method: 'POST',
272 data: { settings: socialData }
273 });
274
275 if (response.success) {
276 setHasChanges(false);
277 setNotice({
278 status: 'success',
279 message: __('Social Platform settings saved successfully!', 'thinkrank')
280 });
281 } else {
282 throw new Error('Failed to save Social Platform settings');
283 }
284 }
285
286 } catch (error) {
287 console.error('Save error:', error);
288 setNotice({
289 status: 'error',
290 message: __('Failed to save settings. Please try again.', 'thinkrank')
291 });
292 } finally {
293 setIsSaving(false);
294 }
295 };
296
297 /**
298 * Test API connections
299 * DRY: Reuses connection testing pattern
300 */
301 const testConnections = async () => {
302 try {
303 setIsTestingConnection(true);
304 setNotice(null);
305
306 const response = await apiFetch({
307 path: '/thinkrank/v1/integrations/test-connections',
308 method: 'POST'
309 });
310
311 if (response.success) {
312 setConnectionStatus(response.data);
313 setNotice({
314 status: 'success',
315 message: __('Connection test completed successfully!', 'thinkrank')
316 });
317 } else {
318 throw new Error(response.error || 'Connection test failed');
319 }
320 } catch (error) {
321 console.error('Connection test error:', error);
322 setNotice({
323 status: 'error',
324 message: __('Connection test failed. Please check your API keys.', 'thinkrank')
325 });
326 } finally {
327 setIsTestingConnection(false);
328 }
329 };
330
331 /**
332 * Verify GA4 tracking
333 * Following ThinkRank API patterns
334 */
335 const verifyTracking = async () => {
336 if (!integrationsSettings.ga4_measurement_id) {
337 setNotice({
338 status: 'error',
339 message: __('Please enter a GA4 Measurement ID first.', 'thinkrank')
340 });
341 return;
342 }
343
344 setIsVerifying(true);
345 setVerificationResult(null);
346
347 try {
348 const response = await apiFetch({
349 path: '/thinkrank/v1/integrations/verify-ga4-tracking',
350 method: 'POST',
351 data: {
352 measurement_id: integrationsSettings.ga4_measurement_id
353 }
354 });
355
356 if (response.success) {
357 setVerificationResult(response.data);
358
359 // Update tracking settings if verification successful
360 if (response.data.success) {
361 setIntegrationsSettings(prev => ({
362 ...prev,
363 ga4_tracking_verified: true,
364 ga4_last_verification: new Date().toLocaleString()
365 }));
366 setHasChanges(true);
367 }
368 }
369 } catch (error) {
370 setVerificationResult({
371 success: false,
372 message: __('Verification failed. Please try again.', 'thinkrank')
373 });
374 } finally {
375 setIsVerifying(false);
376 }
377 };
378
379 /**
380 * Check for GA4 conflicts
381 * Following ThinkRank API patterns
382 */
383 const checkConflicts = async () => {
384 setIsCheckingConflicts(true);
385
386 try {
387 const response = await apiFetch({
388 path: '/thinkrank/v1/integrations/detect-ga4-conflicts',
389 method: 'GET'
390 });
391
392 if (response.success) {
393 setConflicts(response.data.conflicts || []);
394 }
395 } catch (error) {
396 console.error('Conflict detection failed:', error);
397 } finally {
398 setIsCheckingConflicts(false);
399 }
400 };
401
402 // Check conflicts when measurement ID changes
403 useEffect(() => {
404 if (integrationsSettings.ga4_measurement_id) {
405 checkConflicts();
406 } else {
407 setConflicts([]);
408 }
409 }, [integrationsSettings.ga4_measurement_id]);
410
411 /**
412 * Get tracking status indicator
413 * Following ThinkRank status patterns
414 */
415 const getTrackingStatus = () => {
416 if (!integrationsSettings.ga4_measurement_id) {
417 return { status: 'inactive', label: __('Not Configured', 'thinkrank') };
418 }
419
420 if (integrationsSettings.ga4_tracking_verified) {
421 return { status: 'active', label: __('Verified & Active', 'thinkrank') };
422 }
423
424 if (integrationsSettings.ga4_auto_inject) {
425 return { status: 'pending', label: __('Configured (Not Verified)', 'thinkrank') };
426 }
427
428 return { status: 'manual', label: __('Manual Setup', 'thinkrank') };
429 };
430
431 /**
432 * Render Google Services section
433 * KISS: Clean, focused component
434 */
435 const renderGoogleServices = () => (
436 <div className="thinkrank-tab-content">
437 {/* Google Services Integration Workflow */}
438 <Card size="small" className="thinkrank-mb-md">
439 <CardBody>
440 <div style={{
441 padding: '12px',
442 backgroundColor: '#e8f5e8',
443 border: '1px solid #c8e6c8',
444 borderRadius: '4px',
445 marginBottom: '16px'
446 }}>
447 <h4 style={{ margin: '0 0 8px 0', color: '#2e7d32' }}>
448 {__('Google Services Integration', 'thinkrank')}
449 </h4>
450 <ol style={{ margin: '0', paddingLeft: '20px', color: '#2e7d32' }}>
451 <li>{__('Get API keys from Google Cloud Console', 'thinkrank')}</li>
452 <li>{__('Configure API keys below', 'thinkrank')}</li>
453 <li>{__('Enable real-time data fetching', 'thinkrank')}</li>
454 <li>{__('Monitor performance and analytics', 'thinkrank')}</li>
455 </ol>
456 </div>
457 </CardBody>
458 </Card>
459
460 <div className="thinkrank-settings-grid">
461 <Card size="small" className="thinkrank-mb-md">
462 <CardHeader>
463 <h3>{__('Google API Keys', 'thinkrank')}</h3>
464 <Text variant="muted">
465 {__('Configure API keys for Google services integration', 'thinkrank')}
466 </Text>
467 </CardHeader>
468 <CardBody>
469 <TextControl
470 label={__('Google Analytics Data API Key', 'thinkrank')}
471 value={integrationsSettings.google_analytics_api_key}
472 onChange={(value) => handleIntegrationsSettingChange('google_analytics_api_key', value)}
473 placeholder="AIzaSyD-9tSrke72PouQMnMX-a7UUAVNCKH6dsI"
474 help={
475 <>
476 {__('Required for GA4 Analytics data retrieval. This will be encrypted and stored securely. ', 'thinkrank')}
477 <a href="https://console.cloud.google.com/apis/library/analyticsdata.googleapis.com" target="_blank" rel="noopener noreferrer">
478 {__('Enable Analytics Data API →', 'thinkrank')}
479 </a>
480 {__(' | ', 'thinkrank')}
481 <a href="https://console.cloud.google.com/apis/credentials" target="_blank" rel="noopener noreferrer">
482 {__('Create API Key →', 'thinkrank')}
483 </a>
484 </>
485 }
486 type="text"
487 __next40pxDefaultSize={true}
488 __nextHasNoMarginBottom={true}
489 className="thinkrank-mb-sm"
490 />
491
492 <TextControl
493 label={__('Google Search Console API Key', 'thinkrank')}
494 value={integrationsSettings.google_search_console_api_key}
495 onChange={(value) => handleIntegrationsSettingChange('google_search_console_api_key', value)}
496 placeholder="AIzaSyB-1uEFiQPa4GX-LnQpVMz2E4KHrEEsB5Y"
497 help={
498 <>
499 {__('Required for Search Console data and keyword rankings. This will be encrypted and stored securely. ', 'thinkrank')}
500 <a href="https://console.cloud.google.com/apis/library/searchconsole.googleapis.com" target="_blank" rel="noopener noreferrer">
501 {__('Enable Search Console API →', 'thinkrank')}
502 </a>
503 {__(' | ', 'thinkrank')}
504 <a href="https://console.cloud.google.com/apis/credentials" target="_blank" rel="noopener noreferrer">
505 {__('Create API Key →', 'thinkrank')}
506 </a>
507 </>
508 }
509 type="text"
510 __next40pxDefaultSize={true}
511 __nextHasNoMarginBottom={true}
512 className="thinkrank-mb-sm"
513 />
514
515 <TextControl
516 label={__('Google PageSpeed Insights API Key', 'thinkrank')}
517 value={integrationsSettings.google_pagespeed_api_key}
518 onChange={(value) => handleIntegrationsSettingChange('google_pagespeed_api_key', value)}
519 placeholder="AIzaSyC-3vWFiQPa4GX-LnQpVMz2E4KHrEEsC6Z"
520 help={
521 <>
522 {__('Required for Core Web Vitals and performance data. This will be encrypted and stored securely. ', 'thinkrank')}
523 <a href="https://console.cloud.google.com/apis/library/pagespeedonline.googleapis.com" target="_blank" rel="noopener noreferrer">
524 {__('Enable PageSpeed Insights API →', 'thinkrank')}
525 </a>
526 {__(' | ', 'thinkrank')}
527 <a href="https://console.cloud.google.com/apis/credentials" target="_blank" rel="noopener noreferrer">
528 {__('Create API Key →', 'thinkrank')}
529 </a>
530 </>
531 }
532 type="text"
533 __next40pxDefaultSize={true}
534 __nextHasNoMarginBottom={true}
535 />
536 </CardBody>
537 </Card>
538
539 {/* Test Connection Button */}
540 <Card size="small" className="thinkrank-mb-md">
541 <CardHeader>
542 <h3>{__('Connection Testing', 'thinkrank')}</h3>
543 </CardHeader>
544 <CardBody>
545 <Flex justify="space-between" align="center">
546 <FlexItem>
547 <Text>
548 {__('Test your API connections to ensure everything is working correctly.', 'thinkrank')}
549 </Text>
550 </FlexItem>
551 <FlexItem>
552 <Button
553 variant="secondary"
554 onClick={testConnections}
555 isBusy={isTestingConnection}
556 disabled={isTestingConnection}
557 >
558 {isTestingConnection ? __('Testing...', 'thinkrank') : __('Test Connections', 'thinkrank')}
559 </Button>
560 </FlexItem>
561 </Flex>
562 </CardBody>
563 </Card>
564
565 {/* Connection Status Display */}
566 {connectionStatus && (
567 <Card size="small" className="thinkrank-mt-md thinkrank-mb-md">
568 <CardHeader>
569 <h3>{__('Connection Status', 'thinkrank')}</h3>
570 </CardHeader>
571 <CardBody>
572 <div className="thinkrank-space-y-4">
573 {Object.entries(connectionStatus).map(([service, status]) => (
574 <Flex key={service} justify="space-between" align="center" className="thinkrank-py-2">
575 <FlexItem>
576 <Text>
577 {service === 'google_analytics' ? __('Google Analytics', 'thinkrank') :
578 service === 'search_console' ? __('Search Console', 'thinkrank') :
579 service === 'pagespeed' ? __('PageSpeed', 'thinkrank') :
580 service.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase())}
581 </Text>
582 </FlexItem>
583 <FlexItem>
584 <span className={`thinkrank-inline-flex thinkrank-items-center thinkrank-px-2 thinkrank-py-1 thinkrank-rounded thinkrank-text-xs thinkrank-font-medium thinkrank-border ${
585 status.status === 'configured' ? 'thinkrank-bg-green thinkrank-bg-opacity-10 thinkrank-text-green thinkrank-border-green thinkrank-border-opacity-20' :
586 status.status === 'error' ? 'thinkrank-bg-red thinkrank-bg-opacity-10 thinkrank-text-red thinkrank-border-red thinkrank-border-opacity-20' :
587 'thinkrank-bg-gray-100 thinkrank-text-secondary thinkrank-border-light'
588 }`}>
589 {status.status === 'configured' ? __('Configured', 'thinkrank') :
590 status.status === 'error' ? __('Error', 'thinkrank') :
591 __('Not Configured', 'thinkrank')}
592 </span>
593 </FlexItem>
594 </Flex>
595 ))}
596 </div>
597 </CardBody>
598 </Card>
599 )}
600 </div>
601
602 {/* GA4 Tracking Management Card */}
603 <Card size="small" className="thinkrank-mb-md">
604 <CardHeader>
605 <h3>{__('Google Analytics Tracking', 'thinkrank')}</h3>
606 <Text variant="muted">
607 {__('Configure GA4 tracking code injection for your website', 'thinkrank')}
608 </Text>
609 </CardHeader>
610 <CardBody>
611 {/* Status Indicator */}
612 <div className="thinkrank-mb-md">
613 <Flex justify="space-between" align="center">
614 <FlexItem>
615 <Text>{__('Tracking Status:', 'thinkrank')}</Text>
616 </FlexItem>
617 <FlexItem>
618 {(() => {
619 const status = getTrackingStatus();
620 const statusConfig = {
621 active: { color: 'green', icon: '�
622 ' },
623 pending: { color: 'orange', icon: '' },
624 manual: { color: 'blue', icon: '🔧' },
625 inactive: { color: 'gray', icon: '' }
626 };
627 const config = statusConfig[status.status] || statusConfig.inactive;
628
629 return (
630 <span className={`thinkrank-inline-flex thinkrank-items-center thinkrank-px-2 thinkrank-py-1 thinkrank-rounded thinkrank-text-xs thinkrank-font-medium thinkrank-border ${
631 config.color === 'green' ? 'thinkrank-bg-green thinkrank-bg-opacity-10 thinkrank-text-green thinkrank-border-green thinkrank-border-opacity-20' :
632 config.color === 'orange' ? 'thinkrank-bg-orange thinkrank-bg-opacity-10 thinkrank-text-orange thinkrank-border-orange thinkrank-border-opacity-20' :
633 config.color === 'blue' ? 'thinkrank-bg-blue thinkrank-bg-opacity-10 thinkrank-text-blue thinkrank-border-blue thinkrank-border-opacity-20' :
634 'thinkrank-bg-gray-100 thinkrank-text-secondary thinkrank-border-light'
635 }`}>
636 {config.icon} {status.label}
637 </span>
638 );
639 })()}
640 {isCheckingConflicts && <Spinner style={{ marginLeft: '10px' }} />}
641 </FlexItem>
642 </Flex>
643 </div>
644
645 {/* Measurement ID */}
646 <TextControl
647 label={__('GA4 Measurement ID', 'thinkrank')}
648 value={integrationsSettings.ga4_measurement_id}
649 onChange={(value) => handleIntegrationsSettingChange('ga4_measurement_id', value)}
650 placeholder="G-XXXXXXXXXX"
651 pattern="G-[A-Z0-9]{10}"
652 help={
653 <>
654 {__('Your GA4 Measurement ID from Google Analytics. ', 'thinkrank')}
655 <a href="https://support.google.com/analytics/answer/9539598" target="_blank" rel="noopener noreferrer">
656 {__('Find your Measurement ID →', 'thinkrank')}
657 </a>
658 </>
659 }
660 __next40pxDefaultSize={true}
661 __nextHasNoMarginBottom={true}
662 className="thinkrank-mb-sm"
663 />
664
665 {/* Conflict Warnings */}
666 {conflicts.length > 0 && (
667 <Notice status="warning" isDismissible={false} className="thinkrank-mb-sm">
668 <strong>{__('Potential Conflicts Detected:', 'thinkrank')}</strong>
669 <ul style={{ marginTop: '8px', marginBottom: '0' }}>
670 {conflicts.map((conflict, index) => (
671 <li key={index}>
672 <strong>{conflict.name}</strong>
673 {conflict.type === 'plugin' && (
674 <span> - {__('Consider disabling auto-inject to avoid duplicate tracking', 'thinkrank')}</span>
675 )}
676 {conflict.type === 'theme' && (
677 <span> - {__('Manual GA4 detected in theme files', 'thinkrank')}</span>
678 )}
679 </li>
680 ))}
681 </ul>
682 </Notice>
683 )}
684
685 {/* Auto-inject Toggle */}
686 <ToggleControl
687 label={__('Auto-inject GA4 tracking code', 'thinkrank')}
688 checked={integrationsSettings.ga4_auto_inject}
689 onChange={(value) => handleIntegrationsSettingChange('ga4_auto_inject', value)}
690 help={__('Automatically add GA4 tracking code to your website. Disable if you\'ve already installed GA4 manually or via another plugin.', 'thinkrank')}
691 __nextHasNoMarginBottom={true}
692 className="thinkrank-mb-sm"
693 />
694
695 {/* Advanced Options */}
696 {integrationsSettings.ga4_auto_inject && (
697 <div className="thinkrank-p-4 thinkrank-bg-gray-50 thinkrank-border thinkrank-border-light thinkrank-rounded thinkrank-mt-md">
698 <h4 className="thinkrank-mt-0 thinkrank-mb-3 thinkrank-text-primary thinkrank-text-sm thinkrank-font-semibold">{__('Advanced Options', 'thinkrank')}</h4>
699
700
701
702 <ToggleControl
703 label={__('Anonymize IP addresses', 'thinkrank')}
704 checked={integrationsSettings.ga4_anonymize_ip}
705 onChange={(value) => handleIntegrationsSettingChange('ga4_anonymize_ip', value)}
706 help={__('Anonymize visitor IP addresses for GDPR compliance.', 'thinkrank')}
707 __nextHasNoMarginBottom={true}
708 className="thinkrank-mb-sm"
709 />
710
711 <ToggleControl
712 label={__('Exclude admin users', 'thinkrank')}
713 checked={integrationsSettings.ga4_exclude_admin}
714 onChange={(value) => handleIntegrationsSettingChange('ga4_exclude_admin', value)}
715 help={__('Exclude logged-in administrators from tracking.', 'thinkrank')}
716 __nextHasNoMarginBottom={true}
717 className="thinkrank-mb-sm"
718 />
719 </div>
720 )}
721
722 {/* Verification Section */}
723 <div className="thinkrank-pt-4 thinkrank-border-t thinkrank-border-light thinkrank-mt-md">
724 <Flex justify="space-between" align="center">
725 <FlexItem>
726 <Button
727 variant="secondary"
728 onClick={verifyTracking}
729 isBusy={isVerifying}
730 disabled={!integrationsSettings.ga4_measurement_id}
731 >
732 {__('Verify Tracking', 'thinkrank')}
733 </Button>
734 </FlexItem>
735
736 {integrationsSettings.ga4_tracking_verified && integrationsSettings.ga4_last_verification && (
737 <FlexItem>
738 <Text variant="muted" size="small">
739 {__('Last verified:', 'thinkrank')} {integrationsSettings.ga4_last_verification}
740 </Text>
741 </FlexItem>
742 )}
743 </Flex>
744
745 {verificationResult && (
746 <Notice
747 status={verificationResult.success ? 'success' : 'warning'}
748 isDismissible={false}
749 className="thinkrank-mt-sm"
750 >
751 {verificationResult.message}
752 </Notice>
753 )}
754 </div>
755 </CardBody>
756 </Card>
757 </div>
758 );
759
760 /**
761 * Render Social Platforms section
762 * DRY: Moved from Social Media tab, preserving exact functionality
763 */
764 const renderSocialPlatforms = () => (
765 <div className="thinkrank-tab-content">
766 {/* Social Platform Verification Workflow */}
767 <Card size="small" className="thinkrank-mb-md">
768 <CardBody>
769 <div style={{
770 padding: '12px',
771 backgroundColor: '#e7f3ff',
772 border: '1px solid #b3d9ff',
773 borderRadius: '4px',
774 marginBottom: '16px'
775 }}>
776 <h4 style={{ margin: '0 0 8px 0', color: '#0073aa' }}>
777 {__('Social Platform Verification Workflow', 'thinkrank')}
778 </h4>
779 <ol style={{ margin: '0', paddingLeft: '20px', color: '#0073aa' }}>
780 <li>{__('Get verification codes from platform business accounts', 'thinkrank')}</li>
781 <li>{__('Configure platform IDs and verification codes below', 'thinkrank')}</li>
782 <li>{__('Verify website ownership with social platforms', 'thinkrank')}</li>
783 <li>{__('Access analytics and business features', 'thinkrank')}</li>
784 </ol>
785 </div>
786 </CardBody>
787 </Card>
788
789 <div className="thinkrank-settings-grid">
790 <Card size="small" className="thinkrank-mb-md">
791 <CardHeader>
792 <h3>{__('Facebook', 'thinkrank')}</h3>
793 </CardHeader>
794 <CardBody>
795 <TextControl
796 label={__('Facebook App ID', 'thinkrank')}
797 value={socialSettings.facebook_app_id}
798 onChange={(value) => handleSocialSettingChange('facebook_app_id', value)}
799 placeholder="123456789012XXXX"
800 help={
801 <>
802 {__('Your Facebook App ID for analytics. ', 'thinkrank')}
803 <a href="https://developers.facebook.com/apps/" target="_blank" rel="noopener noreferrer">
804 {__('Get your App ID here →', 'thinkrank')}
805 </a>
806 </>
807 }
808 __next40pxDefaultSize={true}
809 __nextHasNoMarginBottom={true}
810 className="thinkrank-mb-sm"
811 />
812
813 <TextControl
814 label={__('Facebook Admins', 'thinkrank')}
815 value={socialSettings.facebook_admins}
816 onChange={(value) => handleSocialSettingChange('facebook_admins', value)}
817 placeholder="100012345678XXXX,100087654321XXXX"
818 help={
819 <>
820 {__('Comma-separated Facebook user IDs for admin access. ', 'thinkrank')}
821 <a href="https://www.facebook.com/help/1503421039731588" target="_blank" rel="noopener noreferrer">
822 {__('How to find your Facebook ID →', 'thinkrank')}
823 </a>
824 </>
825 }
826 __next40pxDefaultSize={true}
827 __nextHasNoMarginBottom={true}
828 />
829 </CardBody>
830 </Card>
831
832 <Card size="small" className="thinkrank-mb-md">
833 <CardHeader>
834 <h3>{__('Pinterest', 'thinkrank')}</h3>
835 </CardHeader>
836 <CardBody>
837 <TextControl
838 label={__('Pinterest Site Verification', 'thinkrank')}
839 value={socialSettings.pinterest_site_verification}
840 onChange={(value) => handleSocialSettingChange('pinterest_site_verification', value)}
841 placeholder="a1b2c3d4e5f6789XXXX"
842 help={
843 <>
844 {__('Pinterest site verification code for Pinterest Business. This will be encrypted and stored securely. ', 'thinkrank')}
845 <a href="https://help.pinterest.com/en/business/article/claim-your-website" target="_blank" rel="noopener noreferrer">
846 {__('Get verification code →', 'thinkrank')}
847 </a>
848 </>
849 }
850 __next40pxDefaultSize={true}
851 __nextHasNoMarginBottom={true}
852 />
853 </CardBody>
854 </Card>
855
856 <Card size="small" className="thinkrank-mb-md">
857 <CardHeader>
858 <h3>{__('Instagram', 'thinkrank')}</h3>
859 </CardHeader>
860 <CardBody>
861 <TextControl
862 label={__('Instagram Verification', 'thinkrank')}
863 value={socialSettings.instagram_verification}
864 onChange={(value) => handleSocialSettingChange('instagram_verification', value)}
865 placeholder="ig_business_verify_123XXXX"
866 help={
867 <>
868 {__('Instagram verification code for business features. This will be encrypted and stored securely. ', 'thinkrank')}
869 <a href="https://business.instagram.com/getting-started" target="_blank" rel="noopener noreferrer">
870 {__('Instagram Business setup →', 'thinkrank')}
871 </a>
872 </>
873 }
874 __next40pxDefaultSize={true}
875 __nextHasNoMarginBottom={true}
876 />
877 </CardBody>
878 </Card>
879
880 <Card size="small" className="thinkrank-mb-md">
881 <CardHeader>
882 <h3>{__('TikTok', 'thinkrank')}</h3>
883 </CardHeader>
884 <CardBody>
885 <TextControl
886 label={__('TikTok Verification', 'thinkrank')}
887 value={socialSettings.tiktok_verification}
888 onChange={(value) => handleSocialSettingChange('tiktok_verification', value)}
889 placeholder="tiktok_biz_verify_456XXXX"
890 help={
891 <>
892 {__('TikTok verification code for business features. This will be encrypted and stored securely. ', 'thinkrank')}
893 <a href="https://support.tiktok.com/en/using-tiktok/growing-your-audience/switching-to-a-creator-or-business-account" target="_blank" rel="noopener noreferrer">
894 {__('TikTok Business account setup →', 'thinkrank')}
895 </a>
896 </>
897 }
898 __next40pxDefaultSize={true}
899 __nextHasNoMarginBottom={true}
900 />
901 </CardBody>
902 </Card>
903
904 <Card size="small" className="thinkrank-mb-md">
905 <CardHeader>
906 <h3>{__('YouTube', 'thinkrank')}</h3>
907 </CardHeader>
908 <CardBody>
909 <TextControl
910 label={__('YouTube Channel ID', 'thinkrank')}
911 value={socialSettings.youtube_channel_id}
912 onChange={(value) => handleSocialSettingChange('youtube_channel_id', value)}
913 placeholder="UCabcdefghijklmnXXXX"
914 help={
915 <>
916 {__('Your YouTube channel ID (starts with UC). ', 'thinkrank')}
917 <a href="https://support.google.com/youtube/answer/3250431" target="_blank" rel="noopener noreferrer">
918 {__('Find your Channel ID →', 'thinkrank')}
919 </a>
920 </>
921 }
922 __next40pxDefaultSize={true}
923 __nextHasNoMarginBottom={true}
924 />
925 </CardBody>
926 </Card>
927
928 <Card size="small" className="thinkrank-mb-md">
929 <CardHeader>
930 <h3>{__('WhatsApp Business', 'thinkrank')}</h3>
931 </CardHeader>
932 <CardBody>
933 <TextControl
934 label={__('WhatsApp Business ID', 'thinkrank')}
935 value={socialSettings.whatsapp_business_id}
936 onChange={(value) => handleSocialSettingChange('whatsapp_business_id', value)}
937 placeholder="1234567890XXXX"
938 help={
939 <>
940 {__('Your WhatsApp Business account ID (digits only). ', 'thinkrank')}
941 <a href="https://developers.facebook.com/docs/graph-api/reference/whats-app-business-account/" target="_blank" rel="noopener noreferrer">
942 {__('WhatsApp Business API setup →', 'thinkrank')}
943 </a>
944 </>
945 }
946 __next40pxDefaultSize={true}
947 __nextHasNoMarginBottom={true}
948 />
949 </CardBody>
950 </Card>
951 </div>
952 </div>
953 );
954
955 if (isLoading) {
956 return (
957 <div style={{ textAlign: 'center', padding: '48px' }}>
958 <Spinner />
959 <Text>{__('Loading integration settings...', 'thinkrank')}</Text>
960 </div>
961 );
962 }
963
964 return (
965 <div className="thinkrank-integrations-tab">
966 {notice && (
967 <Notice
968 status={notice.status}
969 onRemove={() => setNotice(null)}
970 className="thinkrank-mb-md"
971 >
972 {notice.message}
973 </Notice>
974 )}
975
976 {/* Render content based on active sub-section */}
977 {(() => {
978 switch (activeSubSection) {
979 case 'google-services':
980 return renderGoogleServices();
981 case 'social-platforms':
982 return renderSocialPlatforms();
983 default:
984 return renderGoogleServices();
985 }
986 })()}
987
988 {/* Save Button */}
989 <Flex justify="flex-end" className="thinkrank-mt-lg">
990 <FlexItem>
991 <Button
992 variant="primary"
993 onClick={saveSettings}
994 isBusy={isSaving}
995 disabled={!hasChanges || isSaving}
996 >
997 {isSaving ? __('Saving...', 'thinkrank') : __('Save Changes', 'thinkrank')}
998 </Button>
999 </FlexItem>
1000 </Flex>
1001 </div>
1002 );
1003 };
1004
1005 export default IntegrationsTab;
1006