| 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 |
|