Changeset 3376645
- Timestamp:
- 10/11/2025 11:49:03 AM (6 months ago)
- Location:
- synoveo/trunk
- Files:
-
- 8 edited
-
assets/css/admin.css (modified) (1 diff)
-
assets/js/frontend-connect.js (modified) (1 diff)
-
assets/js/google-business-connector.js (modified) (9 diffs)
-
includes/admin/class-synoveo-admin-controller.php (modified) (8 diffs)
-
includes/services/class-synoveo-api-service.php (modified) (2 diffs)
-
includes/services/class-synoveo-frontend-service.php (modified) (3 diffs)
-
readme.txt (modified) (2 diffs)
-
synoveo.php (modified) (3 diffs)
Legend:
- Unmodified
- Added
- Removed
-
synoveo/trunk/assets/css/admin.css
r3375269 r3376645 43 43 background: #dcfce7; 44 44 color: #166534; 45 } 46 47 /* Email Verification Modal Styles */ 48 .synoveo-modal { 49 position: fixed; 50 top: 0; 51 left: 0; 52 width: 100%; 53 height: 100%; 54 background-color: rgba(0, 0, 0, 0.5); 55 display: flex; 56 justify-content: center; 57 align-items: center; 58 z-index: 9999; 59 } 60 61 .synoveo-modal-content { 62 background: white; 63 padding: 30px; 64 border-radius: 8px; 65 max-width: 500px; 66 width: 90%; 67 box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15); 68 } 69 70 .synoveo-modal-header { 71 margin-bottom: 20px; 72 } 73 74 .synoveo-modal-header h3 { 75 margin-top: 0; 76 color: #333; 77 font-size: 18px; 78 } 79 80 .synoveo-modal-body { 81 margin-bottom: 20px; 82 } 83 84 .synoveo-modal-buttons { 85 display: flex; 86 gap: 10px; 87 justify-content: flex-end; 88 margin-top: 20px; 89 } 90 91 .synoveo-modal-buttons button { 92 padding: 10px 20px; 93 border: none; 94 border-radius: 4px; 95 cursor: pointer; 96 font-size: 14px; 97 } 98 99 .synoveo-modal-buttons .button-primary { 100 background-color: #0073aa; 101 color: white; 102 } 103 104 .synoveo-modal-buttons .button-primary:hover { 105 background-color: #005a87; 106 } 107 108 .synoveo-modal-buttons .button { 109 background-color: #f1f1f1; 110 color: #333; 111 } 112 113 .synoveo-modal-buttons .button:hover { 114 background-color: #e1e1e1; 115 } 116 117 .synoveo-modal input[type="email"], 118 .synoveo-modal input[type="text"] { 119 width: 100%; 120 padding: 12px; 121 border: 1px solid #ddd; 122 border-radius: 4px; 123 font-size: 16px; 124 margin: 10px 0; 125 box-sizing: border-box; 126 } 127 128 .synoveo-modal input[type="text"] { 129 text-align: center; 130 letter-spacing: 2px; 131 } 132 133 .synoveo-modal .code-expiry { 134 text-align: center; 135 color: #666; 136 font-size: 12px; 137 margin-top: 10px; 138 } 139 140 /* Modal step transitions */ 141 .synoveo-modal-step { 142 display: none; 143 } 144 145 .synoveo-modal-step.active { 146 display: block; 147 } 148 149 /* Success step styling */ 150 .synoveo-success-step { 151 text-align: center; 152 padding: 20px; 153 } 154 155 .synoveo-success-icon { 156 font-size: 48px; 157 margin-bottom: 20px; 158 display: block; 159 } 160 161 .synoveo-success-title { 162 color: #10b981; 163 margin-bottom: 10px; 164 font-size: 20px; 165 font-weight: 600; 166 } 167 168 .synoveo-success-message { 169 margin-bottom: 20px; 170 color: #666; 171 } 172 173 /* Wizard section transitions */ 174 .synoveo-wizard-section { 175 display: none; 176 } 177 178 .synoveo-wizard-section.active { 179 display: block; 180 } 181 182 .synoveo-input { 183 width: 100%; 184 padding: 12px; 185 border: 1px solid #ddd; 186 border-radius: 4px; 187 font-size: 16px; 188 margin: 10px 0; 189 box-sizing: border-box; 190 } 191 192 .synoveo-code-input { 193 text-align: center; 194 letter-spacing: 2px; 195 } 196 197 .synoveo-wizard-buttons { 198 display: flex; 199 gap: 10px; 200 justify-content: flex-end; 201 margin-top: 20px; 202 } 203 204 .synoveo-wizard-description { 205 margin-bottom: 20px; 206 color: #666; 207 line-height: 1.5; 45 208 } 46 209 -
synoveo/trunk/assets/js/frontend-connect.js
r3375269 r3376645 32 32 document.getElementById('connect-button').disabled = true; 33 33 34 // WordPress AJAX call with proper security (uses localized data) 34 // Get stable identifier from localStorage or WordPress 35 const userIdentifier = localStorage.getItem('synoveo_lite_identifier') || 36 synoveoFrontendData.apiKey || 37 ''; 38 39 if (!userIdentifier) { 40 console.error('❌ No user identifier found'); 41 document.getElementById('error-state').classList.add('active'); 42 document.getElementById('error-message').textContent = 'Registration required. Please return to the dashboard.'; 43 return; 44 } 45 46 const requestBody = { 47 action: 'synoveo_select_business_location', 48 location_id: selectedLocationId, 49 auth_code: synoveoFrontendData.authCode, 50 session_id: synoveoFrontendData.sessionId, 51 nonce: synoveoFrontendData.nonce, 52 user_identifier: userIdentifier 53 }; 54 55 console.log('📤 [Frontend] Connecting with identifier:', userIdentifier); 56 35 57 fetch(synoveoFrontendData.ajaxUrl, { 36 method: 'POST', 37 headers: { 38 'Content-Type': 'application/x-www-form-urlencoded', 39 }, 40 body: new URLSearchParams({ 41 action: 'synoveo_select_business_location', 42 location_id: selectedLocationId, 43 auth_code: synoveoFrontendData.authCode, 44 session_id: synoveoFrontendData.sessionId, 45 nonce: synoveoFrontendData.nonce 46 }) 58 method: 'POST', 59 headers: { 60 'Content-Type': 'application/x-www-form-urlencoded', 61 }, 62 body: new URLSearchParams(requestBody) 47 63 }) 48 64 .then(response => response.json()) -
synoveo/trunk/assets/js/google-business-connector.js
r3375269 r3376645 22 22 23 23 /** 24 * Get or generate stable Lite user identifier 25 * Priority: WordPress option > localStorage > generate new 26 */ 27 getOrCreateUserIdentifier() { 28 // First, check if WordPress has it (source of truth) 29 let identifier = window.synoveo_ajax?.api_key; 30 31 // If WordPress has synoveo_lite_ key, use it 32 if (identifier && identifier.startsWith('synoveo_lite_')) { 33 localStorage.setItem('synoveo_lite_identifier', identifier); 34 return identifier; 35 } 36 37 // Check localStorage 38 identifier = localStorage.getItem('synoveo_lite_identifier'); 39 if (identifier && identifier.startsWith('synoveo_lite_')) { 40 return identifier; 41 } 42 43 // Generate new stable identifier (deterministic based on domain) 44 const domain = window.location.hostname 45 .replace(/[^a-z0-9]/g, '') 46 .substring(0, 8); 47 48 // Use deterministic hash based on domain + fixed salt 49 const salt = 'synoveo_stable_2025'; 50 const hashInput = domain + salt; 51 52 // Simple deterministic hash (same input = same output) 53 let hash = 0; 54 for (let i = 0; i < hashInput.length; i++) { 55 const char = hashInput.charCodeAt(i); 56 hash = ((hash << 5) - hash) + char; 57 hash = hash & hash; // Convert to 32-bit integer 58 } 59 60 // Convert to alphanumeric string 61 const hashStr = Math.abs(hash).toString(36).substring(0, 15).padEnd(15, '0'); 62 63 identifier = `synoveo_lite_${domain}${hashStr}`; 64 localStorage.setItem('synoveo_lite_identifier', identifier); 65 66 SynoveoLogger.info(`Generated stable identifier: ${identifier}`, 'GoogleBusiness'); 67 return identifier; 68 } 69 70 /** 71 * Register Lite user if not already registered 72 */ 73 async registerUserIfNeeded() { 74 const identifier = this.getOrCreateUserIdentifier(); 75 76 // Check if already registered 77 const registered = localStorage.getItem('synoveo_lite_registered'); 78 if (registered === 'true') { 79 SynoveoLogger.debug('User already registered', 'GoogleBusiness'); 80 return { success: true, identifier }; 81 } 82 83 try { 84 const response = await this.makeAjaxCall('synoveo_register_lite_user', { 85 user_identifier: identifier 86 }); 87 88 console.log('🔍 Registration response:', response); 89 90 if (response.success) { 91 localStorage.setItem('synoveo_lite_registered', 'true'); 92 SynoveoLogger.info(`User registered: ${response.data.userId}`, 'GoogleBusiness'); 93 return { success: true, identifier, data: response.data }; 94 } else { 95 const errorMsg = response.data?.message || response.error || response.data || 'Unknown error'; 96 console.error('❌ Registration failed:', errorMsg, response); 97 SynoveoLogger.error('Registration failed: ' + errorMsg, 'GoogleBusiness'); 98 return { success: false, error: errorMsg }; 99 } 100 } catch (error) { 101 console.error('❌ Registration exception:', error); 102 SynoveoLogger.error('Registration error: ' + (error.message || JSON.stringify(error)), 'GoogleBusiness'); 103 return { success: false, error: error.message || 'Registration failed' }; 104 } 105 } 106 107 /** 108 * Clear Lite user identifier from localStorage 109 * This should only be called if user wants to completely reset their account 110 */ 111 clearLiteUserIdentifier() { 112 if (confirm('⚠️ This will completely reset your Synoveo identity. You will lose access to your current business data. Are you sure?')) { 113 localStorage.removeItem('synoveo_lite_identifier'); 114 console.log('🗑️ Lite user identifier cleared from localStorage'); 115 alert('✅ Your Synoveo identity has been reset. Refresh the page to start fresh.'); 116 } 117 } 118 119 /** 24 120 * UNIFIED ENTRY POINT: Start Growing Your Revenue 25 121 * This is THE function that all "Start Growing Your Revenue" buttons should call 26 122 */ 27 123 async startGrowthProcess() { 28 console.log('🚀 Starting Synoveo growth process...');124 SynoveoLogger.info('Starting Synoveo growth process...', 'GoogleBusiness'); 29 125 30 126 try { 31 // Step 1: Check current connection status 127 // STEP 1: Ensure user is registered 128 const registrationResult = await this.registerUserIfNeeded(); 129 if (!registrationResult.success) { 130 this.showErrorState('Failed to initialize. Please refresh and try again.'); 131 return; 132 } 133 134 // STEP 2: Check connection status 32 135 let status; 33 136 try { … … 59 162 */ 60 163 async startConnectionFlow() { 61 // Show connection wizard 164 // Show connection wizard and proceed directly to Google OAuth 62 165 this.showConnectionWizard(); 63 64 // Step 1: Get Google authorization URL 166 this.proceedToGoogleAuth(); 167 } 168 169 /** 170 * Proceed to Google OAuth after email verification 171 */ 172 async proceedToGoogleAuth() { 173 // Check if email verification is needed (for Lite users) 174 let userIdentifier = window.synoveo_ajax?.api_key; 175 if (!userIdentifier || userIdentifier === '') { 176 userIdentifier = localStorage.getItem('synoveo_lite_identifier') || null; 177 } 178 179 // For Lite users without API key, show email verification first 180 if (!userIdentifier || userIdentifier === '' || (userIdentifier && userIdentifier.startsWith('synoveo_lite_'))) { 181 SynoveoLogger.debug('Lite user detected - checking email verification...', 'GoogleBusiness'); 182 183 try { 184 const emailStatus = await this.checkEmailVerificationStatus(); 185 SynoveoLogger.debug(`Email status: ${JSON.stringify(emailStatus)}`, 'GoogleBusiness'); 186 187 // Handle nested data structure from WordPress AJAX response 188 const emailVerified = emailStatus.success && emailStatus.data?.data?.emailVerified; 189 190 if (!emailVerified) { 191 SynoveoLogger.debug('Email verification required - showing wizard', 'GoogleBusiness'); 192 this.showEmailVerificationInWizard(); 193 return; 194 } else { 195 SynoveoLogger.debug('Email already verified - proceeding to Google OAuth', 'GoogleBusiness'); 196 } 197 } catch (error) { 198 SynoveoLogger.error(error, 'GoogleBusiness'); 199 this.showEmailVerificationInWizard(); 200 return; 201 } 202 } 203 204 // Email verified or Pro/Business user - proceed to Google OAuth 65 205 try { 66 console.log('🔍 DEBUG: Starting auth URL request...');206 SynoveoLogger.debug('Starting Google OAuth flow...', 'GoogleBusiness'); 67 207 const authUrlResponse = await this.getGoogleAuthUrl(); 68 console.log('🔍 DEBUG: Auth URL response received:', authUrlResponse);69 208 70 209 if (authUrlResponse.success) { 71 console.log('🔍 DEBUG: Success! Showing Google auth step with URL:', authUrlResponse.data.auth_url);72 210 this.showGoogleAuthStep(authUrlResponse.data.auth_url); 73 211 } else { 74 console.log('🔍 DEBUG: Failed! Error:', authUrlResponse.error);75 212 throw new Error(authUrlResponse.error); 76 213 } 77 214 78 215 } catch (error) { 79 console.error('❌ Failed to get Google auth URL:', error); 80 console.log('🔍 DEBUG: Full error object:', error); 216 console.error('Failed to get Google auth URL:', error); 81 217 this.showErrorState('Failed to initialize Google connection'); 82 218 } … … 87 223 */ 88 224 async getGoogleAuthUrl() { 89 console.log('🔍 DEBUG: Calling getGoogleAuthUrl...'); 90 const response = await this.makeAjaxCall('synoveo_get_google_auth_url'); 225 // Get stable user identifier 226 const userIdentifier = this.getOrCreateUserIdentifier(); 227 228 console.log('🔍 DEBUG: Calling getGoogleAuthUrl with identifier:', userIdentifier); 229 230 const response = await this.makeAjaxCall('synoveo_get_google_auth_url', { 231 user_identifier: userIdentifier 232 }); 233 91 234 console.log('🔍 DEBUG: getGoogleAuthUrl response:', response); 92 235 console.log('🔍 DEBUG: Response success:', response?.success); … … 154 297 } else if (locations.length === 1) { 155 298 console.log('🏢 Single business found - connecting directly'); 299 300 // ENFORCE PLAN LIMITS: Check before single-location connection 301 const allowed = await this.checkPlanBeforeConnect(locations[0].name); 302 if (!allowed) { 303 SynoveoLogger.error('Connection blocked by plan limits', 'GoogleBusiness'); 304 this.showErrorState('Your plan does not allow this connection.'); 305 return; 306 } 307 156 308 // Single business - connect directly 157 309 await this.connectToSelectedBusiness(authCode, locations[0].name); … … 176 328 */ 177 329 async listBusinessLocations(authCode) { 330 // Get stable user identifier 331 const userIdentifier = this.getOrCreateUserIdentifier(); 332 178 333 return this.makeAjaxCall('synoveo_list_business_locations', { 179 auth_code: authCode 334 auth_code: authCode, 335 user_identifier: userIdentifier 180 336 }); 181 337 } … … 186 342 async connectToSelectedBusiness(authCode, locationId) { 187 343 try { 188 const connectionResponse = await this.makeAjaxCall('synoveo_select_business_location', { 344 // Get stable identifier 345 const userIdentifier = this.getOrCreateUserIdentifier(); 346 347 const requestData = { 189 348 auth_code: authCode, 190 location_id: locationId 191 }); 349 location_id: locationId, 350 user_identifier: userIdentifier 351 }; 352 353 SynoveoLogger.debug(`Connecting with identifier: ${userIdentifier}`, 'GoogleBusiness'); 354 355 const connectionResponse = await this.makeAjaxCall('synoveo_select_business_location', requestData); 192 356 193 357 if (connectionResponse.success) { … … 304 468 wizard.style.display = 'flex'; 305 469 document.body.style.overflow = 'hidden'; 470 } 471 472 /** 473 * Show email verification step inside the main wizard 474 */ 475 showEmailVerificationInWizard() { 476 const wizardContent = document.querySelector('.synoveo-wizard-content'); 477 if (!wizardContent) return; 478 479 wizardContent.innerHTML = ` 480 <div class="synoveo-wizard-header"> 481 <div class="synoveo-wizard-close" onclick="window.synoveoGoogleConnector.closeWizard()">×</div> 482 <h2>Verify Your Email</h2> 483 <p class="synoveo-step-indicator">Step 1 of 2</p> 484 </div> 485 <div class="synoveo-wizard-body"> 486 <div id="email-input-section" class="synoveo-wizard-section active"> 487 <p class="synoveo-wizard-description">To connect your Google Business Profile, please verify your email address:</p> 488 <form id="synoveo-wizard-email-form"> 489 <input type="email" id="wizard-user-email" class="synoveo-input" placeholder="Enter your email address" required> 490 <div class="synoveo-wizard-buttons"> 491 <button type="button" class="button" onclick="window.synoveoGoogleConnector.closeWizard()">Cancel</button> 492 <button type="submit" class="button button-primary" id="wizard-send-code-btn">Send Verification Code</button> 493 </div> 494 </form> 495 </div> 496 497 <div id="code-verification-section" class="synoveo-wizard-section"> 498 <p class="synoveo-wizard-description">We've sent a 6-digit verification code to <strong id="wizard-email-display"></strong></p> 499 <form id="synoveo-wizard-code-form"> 500 <input type="hidden" id="wizard-verification-email" value=""> 501 <input type="text" id="wizard-verification-code" class="synoveo-input synoveo-code-input" placeholder="000000" maxlength="6" required> 502 <div class="synoveo-wizard-buttons"> 503 <button type="button" class="button" onclick="window.synoveoGoogleConnector.goBackToEmailInput()">Back</button> 504 <button type="button" class="button" id="wizard-resend-btn">Resend Code</button> 505 <button type="submit" class="button button-primary" id="wizard-verify-btn">Verify & Continue</button> 506 </div> 507 </form> 508 <p class="code-expiry">Code expires in 10 minutes</p> 509 </div> 510 </div> 511 `; 512 513 // Attach event listeners 514 document.getElementById('synoveo-wizard-email-form').addEventListener('submit', (e) => { 515 e.preventDefault(); 516 this.handleWizardEmailSubmission(); 517 }); 518 519 document.getElementById('synoveo-wizard-code-form').addEventListener('submit', (e) => { 520 e.preventDefault(); 521 this.handleWizardCodeVerification(); 522 }); 523 524 document.getElementById('wizard-resend-btn').addEventListener('click', () => { 525 const email = document.getElementById('wizard-verification-email').value; 526 this.resendWizardCode(email); 527 }); 528 529 // Auto-format code input 530 document.getElementById('wizard-verification-code').addEventListener('input', function(e) { 531 e.target.value = e.target.value.replace(/\D/g, '').slice(0, 6); 532 }); 306 533 } 307 534 … … 548 775 549 776 /** 777 * Check plan limits before connecting a location (preflight validation) 778 */ 779 async checkPlanBeforeConnect(locationId) { 780 try { 781 SynoveoLogger.debug('🔍 Checking plan limits before connection...', 'GoogleBusiness'); 782 783 // Get stable user identifier 784 const userIdentifier = this.getOrCreateUserIdentifier(); 785 786 const response = await this.makeAjaxCall('synoveo_check_business_rules', { 787 location_id: locationId || null, 788 user_identifier: userIdentifier 789 }); 790 791 if (response.success && response.data) { 792 if (!response.data.allowed) { 793 // Plan limit reached or other restriction 794 const message = response.data.message || 'Your plan does not allow this connection.'; 795 SynoveoLogger.error('🚫 Plan limit: ' + message, 'GoogleBusiness'); 796 797 // Show user-friendly notification 798 if (window.synoveoCardManager && typeof window.synoveoCardManager.showNotification === 'function') { 799 window.synoveoCardManager.showNotification(message, 'error'); 800 } else { 801 alert(message); 802 } 803 804 return false; 805 } 806 807 // Allowed - log and proceed 808 SynoveoLogger.debug('✅ Plan check passed, proceeding with connection', 'GoogleBusiness'); 809 return true; 810 } 811 812 // If check fails, show generic error but allow (fail-open for resilience) 813 SynoveoLogger.error('⚠️ Unable to verify plan limits, proceeding with caution', 'GoogleBusiness'); 814 return true; 815 816 } catch (error) { 817 console.error('❌ Plan check error:', error); 818 819 // Show notification but fail-open to prevent blocking valid users 820 if (window.synoveoCardManager && typeof window.synoveoCardManager.showNotification === 'function') { 821 window.synoveoCardManager.showNotification('Unable to verify your plan limits. Proceeding...', 'warning'); 822 } 823 824 return true; // Fail-open: allow connection if check fails 825 } 826 } 827 828 /** 550 829 * Select specific business location 551 830 */ 552 831 async selectBusiness(locationId, authCode) { 553 832 try { 833 // ENFORCE PLAN LIMITS: Check before connection 834 const allowed = await this.checkPlanBeforeConnect(locationId); 835 if (!allowed) { 836 SynoveoLogger.error('Connection blocked by plan limits', 'GoogleBusiness'); 837 this.closeBusinessSelectionModal(); 838 return; 839 } 840 554 841 await this.connectToSelectedBusiness(authCode, locationId); 555 842 this.closeBusinessSelectionModal(); … … 717 1004 718 1005 return result; 1006 } 1007 1008 /** 1009 * Check email verification status 1010 */ 1011 async checkEmailVerificationStatus() { 1012 try { 1013 // Get user identifier - check both api_key and localStorage 1014 let userIdentifier = window.synoveo_ajax?.api_key; 1015 if (!userIdentifier || userIdentifier === '') { 1016 userIdentifier = localStorage.getItem('synoveo_lite_identifier'); 1017 } 1018 1019 SynoveoLogger.debug(`Checking email verification for: ${userIdentifier}`, 'GoogleBusiness'); 1020 1021 const response = await this.makeAjaxCall('synoveo_check_email_status', { 1022 user_identifier: userIdentifier 1023 }); 1024 1025 SynoveoLogger.debug(`Email verification response: ${JSON.stringify(response)}`, 'GoogleBusiness'); 1026 return response; 1027 } catch (error) { 1028 SynoveoLogger.error(error, 'GoogleBusiness'); 1029 return { success: false, error: error.message }; 1030 } 1031 } 1032 1033 /** 1034 * Handle email submission in wizard 1035 */ 1036 async handleWizardEmailSubmission() { 1037 SynoveoLogger.debug('Starting email submission...', 'GoogleBusiness'); 1038 1039 const email = document.getElementById('wizard-user-email').value; 1040 const sendBtn = document.getElementById('wizard-send-code-btn'); 1041 1042 // Get or generate user identifier 1043 let userIdentifier = window.synoveo_ajax?.api_key; 1044 if (!userIdentifier || userIdentifier === '') { 1045 userIdentifier = localStorage.getItem('synoveo_lite_identifier'); 1046 if (!userIdentifier) { 1047 const siteIdentifier = window.location.hostname.replace(/[^a-z0-9]/g, '').substring(0, 8); 1048 const randomSuffix = Math.random().toString(36).substring(2, 15); 1049 userIdentifier = `synoveo_lite_${siteIdentifier}${randomSuffix}`; 1050 localStorage.setItem('synoveo_lite_identifier', userIdentifier); 1051 SynoveoLogger.debug(`Generated new Lite identifier: ${userIdentifier}`, 'GoogleBusiness'); 1052 } else { 1053 SynoveoLogger.debug(`Using existing identifier: ${userIdentifier}`, 'GoogleBusiness'); 1054 } 1055 } else { 1056 SynoveoLogger.debug(`Using API key: ${userIdentifier}`, 'GoogleBusiness'); 1057 } 1058 1059 if (!email) { 1060 alert('Please enter your email address'); 1061 return; 1062 } 1063 1064 SynoveoLogger.debug(`Sending verification code to: ${email}`, 'GoogleBusiness'); 1065 1066 sendBtn.disabled = true; 1067 sendBtn.textContent = 'Sending...'; 1068 1069 try { 1070 const response = await this.makeAjaxCall('synoveo_send_verification_code', { 1071 email: email, 1072 user_identifier: userIdentifier 1073 }); 1074 1075 SynoveoLogger.debug(`Verification code response: ${JSON.stringify(response)}`, 'GoogleBusiness'); 1076 1077 if (response.success) { 1078 // Handle nested data structure from WordPress AJAX response 1079 const emailVerified = response.data?.data?.emailVerified || response.data?.emailVerified; 1080 1081 if (emailVerified) { 1082 SynoveoLogger.debug('Email already verified, proceeding to OAuth', 'GoogleBusiness'); 1083 // Email already verified - transition directly to Google OAuth in the same wizard 1084 sendBtn.textContent = 'Verified! Loading...'; 1085 const authUrlResponse = await this.getGoogleAuthUrl(); 1086 if (authUrlResponse.success) { 1087 // Replace wizard content with OAuth step 1088 this.showGoogleAuthStep(authUrlResponse.data.auth_url); 1089 } else { 1090 throw new Error(authUrlResponse.error); 1091 } 1092 } else { 1093 SynoveoLogger.debug('Code sent, transitioning to verification step', 'GoogleBusiness'); 1094 this.transitionWizardToCodeStep(email); 1095 } 1096 } else { 1097 SynoveoLogger.error(response.data?.message || 'Failed to send code', 'GoogleBusiness'); 1098 alert('Error: ' + (response.data?.message || 'Failed to send verification code')); 1099 sendBtn.disabled = false; 1100 sendBtn.textContent = 'Send Verification Code'; 1101 } 1102 } catch (error) { 1103 SynoveoLogger.error(error, 'GoogleBusiness'); 1104 alert('Error sending verification code. Please try again.'); 1105 sendBtn.disabled = false; 1106 sendBtn.textContent = 'Send Verification Code'; 1107 } 1108 } 1109 1110 /** 1111 * Transition wizard to code verification step 1112 */ 1113 transitionWizardToCodeStep(email) { 1114 SynoveoLogger.debug(`Transitioning to code step for: ${email}`, 'GoogleBusiness'); 1115 1116 const emailSection = document.getElementById('email-input-section'); 1117 const codeSection = document.getElementById('code-verification-section'); 1118 const emailDisplay = document.getElementById('wizard-email-display'); 1119 const verificationEmail = document.getElementById('wizard-verification-email'); 1120 1121 if (!emailSection || !codeSection || !emailDisplay || !verificationEmail) { 1122 SynoveoLogger.error('Missing wizard elements - cannot transition', 'GoogleBusiness'); 1123 alert('Error: Could not transition to code verification step. Missing UI elements.'); 1124 return; 1125 } 1126 1127 emailDisplay.textContent = email; 1128 verificationEmail.value = email; 1129 emailSection.classList.remove('active'); 1130 codeSection.classList.add('active'); 1131 1132 // Update header 1133 const headerTitle = document.querySelector('.synoveo-wizard-header h2'); 1134 if (headerTitle) { 1135 headerTitle.textContent = 'Enter Verification Code'; 1136 } 1137 1138 // Focus on code input 1139 setTimeout(() => { 1140 const codeInput = document.getElementById('wizard-verification-code'); 1141 if (codeInput) { 1142 codeInput.focus(); 1143 } 1144 }, 100); 1145 1146 SynoveoLogger.debug('Transition to code step complete', 'GoogleBusiness'); 1147 } 1148 1149 /** 1150 * Go back to email input in wizard 1151 */ 1152 goBackToEmailInput() { 1153 document.getElementById('email-input-section').classList.add('active'); 1154 document.getElementById('code-verification-section').classList.remove('active'); 1155 document.querySelector('.synoveo-wizard-header h2').textContent = 'Verify Your Email'; 1156 document.getElementById('wizard-verification-code').value = ''; 1157 const sendBtn = document.getElementById('wizard-send-code-btn'); 1158 sendBtn.disabled = false; 1159 sendBtn.textContent = 'Send Verification Code'; 1160 } 1161 1162 /** 1163 * Handle code verification in wizard 1164 */ 1165 async handleWizardCodeVerification() { 1166 SynoveoLogger.debug('Starting code verification...', 'GoogleBusiness'); 1167 1168 const code = document.getElementById('wizard-verification-code').value; 1169 const email = document.getElementById('wizard-verification-email').value; 1170 const verifyBtn = document.getElementById('wizard-verify-btn'); 1171 1172 let userIdentifier = window.synoveo_ajax?.api_key; 1173 if (!userIdentifier || userIdentifier === '') { 1174 userIdentifier = localStorage.getItem('synoveo_lite_identifier'); 1175 } 1176 1177 SynoveoLogger.debug(`Verifying code for: ${email}`, 'GoogleBusiness'); 1178 1179 if (code.length !== 6) { 1180 alert('Please enter a valid 6-digit code'); 1181 return; 1182 } 1183 1184 verifyBtn.disabled = true; 1185 verifyBtn.textContent = 'Verifying...'; 1186 1187 try { 1188 const response = await this.makeAjaxCall('synoveo_verify_code', { 1189 code: code, 1190 email: email, 1191 user_identifier: userIdentifier 1192 }); 1193 1194 SynoveoLogger.debug(`Code verification response: ${JSON.stringify(response)}`, 'GoogleBusiness'); 1195 1196 if (response.success) { 1197 SynoveoLogger.debug('Code verified successfully, loading OAuth step', 'GoogleBusiness'); 1198 const authUrlResponse = await this.getGoogleAuthUrl(); 1199 if (authUrlResponse.success) { 1200 this.showGoogleAuthStep(authUrlResponse.data.auth_url); 1201 } else { 1202 throw new Error(authUrlResponse.error); 1203 } 1204 } else { 1205 SynoveoLogger.error(response.data?.message || 'Invalid code', 'GoogleBusiness'); 1206 alert('Error: ' + (response.data?.message || 'Invalid verification code')); 1207 verifyBtn.disabled = false; 1208 verifyBtn.textContent = 'Verify & Continue'; 1209 } 1210 } catch (error) { 1211 SynoveoLogger.error(error, 'GoogleBusiness'); 1212 alert('Error verifying code. Please try again.'); 1213 verifyBtn.disabled = false; 1214 verifyBtn.textContent = 'Verify & Continue'; 1215 } 1216 } 1217 1218 /** 1219 * Resend code in wizard 1220 */ 1221 async resendWizardCode(email) { 1222 let userIdentifier = window.synoveo_ajax?.api_key; 1223 if (!userIdentifier || userIdentifier === '') { 1224 userIdentifier = localStorage.getItem('synoveo_lite_identifier'); 1225 } 1226 1227 try { 1228 const response = await this.makeAjaxCall('synoveo_send_verification_code', { 1229 email: email, 1230 user_identifier: userIdentifier 1231 }); 1232 1233 if (response.success) { 1234 alert('Verification code resent successfully!'); 1235 } else { 1236 alert('Error: ' + (response.data?.message || 'Failed to resend code')); 1237 } 1238 } catch (error) { 1239 console.error('Error resending code:', error); 1240 alert('Error resending code. Please try again.'); 1241 } 1242 } 1243 1244 /** 1245 * DEPRECATED: Old standalone email modal - keeping for backward compatibility but should not be used 1246 */ 1247 showEmailCollectionModal() { 1248 const modal = document.createElement('div'); 1249 modal.className = 'synoveo-modal'; 1250 modal.id = 'synoveo-email-modal'; 1251 modal.style.display = 'flex'; 1252 1253 modal.innerHTML = ` 1254 <div class="synoveo-modal-content"> 1255 <div class="synoveo-modal-header" id="modal-header"> 1256 <h3 id="modal-title">Connect Your Business</h3> 1257 <p id="modal-description">To connect your Google Business Profile, please provide your email address:</p> 1258 </div> 1259 <div class="synoveo-modal-body"> 1260 <!-- Email Input Step (shown first) --> 1261 <div id="email-step" class="synoveo-modal-step active"> 1262 <form id="synoveo-email-form"> 1263 <input type="email" id="user-email" placeholder="Enter your email address" required> 1264 <div class="synoveo-modal-buttons"> 1265 <button type="button" class="button" onclick="window.synoveoGoogleConnector.closeEmailModal()">Cancel</button> 1266 <button type="submit" class="button button-primary" id="send-code-btn">Send Verification Code</button> 1267 </div> 1268 </form> 1269 </div> 1270 1271 <!-- Code Verification Step (hidden initially) --> 1272 <div id="code-step" class="synoveo-modal-step"> 1273 <form id="synoveo-code-form"> 1274 <input type="hidden" id="verification-email" value=""> 1275 <input type="text" id="verification-code" placeholder="000000" maxlength="6" required> 1276 <div class="synoveo-modal-buttons"> 1277 <button type="button" class="button" onclick="window.synoveoGoogleConnector.goBackToEmailStep()">Back</button> 1278 <button type="button" class="button" id="resend-btn">Resend Code</button> 1279 <button type="submit" class="button button-primary" id="verify-btn">Verify & Continue</button> 1280 </div> 1281 </form> 1282 <p class="code-expiry">Code expires in 10 minutes</p> 1283 </div> 1284 1285 <!-- Success Step (hidden initially) --> 1286 <div id="success-step" class="synoveo-modal-step synoveo-success-step"> 1287 <span class="synoveo-success-icon">✓</span> 1288 <h3 class="synoveo-success-title">Email Verified</h3> 1289 <p class="synoveo-success-message">Redirecting to Google authorization...</p> 1290 </div> 1291 </div> 1292 </div> 1293 `; 1294 1295 document.body.appendChild(modal); 1296 document.body.style.overflow = 'hidden'; 1297 1298 // Handle email form submission 1299 document.getElementById('synoveo-email-form').addEventListener('submit', (e) => { 1300 e.preventDefault(); 1301 this.handleEmailSubmission(); 1302 }); 1303 1304 // Handle code form submission 1305 document.getElementById('synoveo-code-form').addEventListener('submit', (e) => { 1306 e.preventDefault(); 1307 this.handleCodeVerification(); 1308 }); 1309 1310 // Handle resend button 1311 document.getElementById('resend-btn').addEventListener('click', () => { 1312 const email = document.getElementById('verification-email').value; 1313 this.resendCode(email); 1314 }); 1315 1316 // Auto-format code input 1317 document.getElementById('verification-code').addEventListener('input', function(e) { 1318 e.target.value = e.target.value.replace(/\D/g, '').slice(0, 6); 1319 }); 1320 1321 console.log('📧 Unified email verification modal shown'); 1322 } 1323 1324 /** 1325 * Handle email submission 1326 */ 1327 async handleEmailSubmission() { 1328 const email = document.getElementById('user-email').value; 1329 const sendBtn = document.getElementById('send-code-btn'); 1330 1331 // Get or generate user identifier 1332 let userIdentifier = window.synoveo_ajax?.api_key; 1333 1334 // If no API key exists (new Lite user), check localStorage or generate one 1335 if (!userIdentifier || userIdentifier === '') { 1336 // Check if we already generated one in this session 1337 userIdentifier = localStorage.getItem('synoveo_lite_identifier'); 1338 1339 if (!userIdentifier) { 1340 // Generate a new Lite user identifier based on site/domain 1341 const siteIdentifier = window.location.hostname.replace(/[^a-z0-9]/g, '').substring(0, 8); 1342 const randomSuffix = Math.random().toString(36).substring(2, 15); 1343 userIdentifier = `synoveo_lite_${siteIdentifier}${randomSuffix}`; 1344 1345 // Store it in localStorage so it persists 1346 localStorage.setItem('synoveo_lite_identifier', userIdentifier); 1347 console.log('📝 Generated and saved new Lite user identifier:', userIdentifier); 1348 } else { 1349 console.log('📝 Reusing existing Lite user identifier from localStorage:', userIdentifier); 1350 } 1351 } 1352 1353 if (!email) { 1354 alert('Please enter your email address'); 1355 return; 1356 } 1357 1358 // Disable button and show loading state 1359 sendBtn.disabled = true; 1360 sendBtn.textContent = 'Sending...'; 1361 1362 try { 1363 const response = await this.makeAjaxCall('synoveo_send_verification_code', { 1364 email: email, 1365 user_identifier: userIdentifier 1366 }); 1367 1368 if (response.success) { 1369 if (response.data.emailVerified) { 1370 // Email already verified, show success and proceed to Google OAuth 1371 this.transitionToSuccessStep(); 1372 setTimeout(() => { 1373 this.closeEmailModal(); 1374 this.proceedToGoogleAuth(); 1375 }, 1500); 1376 } else { 1377 // Transition to code verification step within same modal 1378 this.transitionToCodeStep(email); 1379 } 1380 } else { 1381 alert('Error: ' + (response.data?.message || 'Failed to send verification code')); 1382 sendBtn.disabled = false; 1383 sendBtn.textContent = 'Send Verification Code'; 1384 } 1385 } catch (error) { 1386 console.error('Error sending verification code:', error); 1387 alert('Error sending verification code. Please try again.'); 1388 sendBtn.disabled = false; 1389 sendBtn.textContent = 'Send Verification Code'; 1390 } 1391 } 1392 1393 /** 1394 * Transition modal to code verification step 1395 */ 1396 transitionToCodeStep(email) { 1397 // Update modal title and description 1398 document.getElementById('modal-title').textContent = 'Enter Verification Code'; 1399 document.getElementById('modal-description').innerHTML = `We've sent a 6-digit verification code to <strong>${email}</strong>. Please enter it below:`; 1400 1401 // Store email for verification 1402 document.getElementById('verification-email').value = email; 1403 1404 // Hide email step, show code step using classes 1405 document.getElementById('email-step').classList.remove('active'); 1406 document.getElementById('code-step').classList.add('active'); 1407 1408 // Focus on code input 1409 setTimeout(() => { 1410 document.getElementById('verification-code').focus(); 1411 }, 100); 1412 1413 console.log('Transitioned to code verification step'); 1414 } 1415 1416 /** 1417 * Go back to email input step 1418 */ 1419 goBackToEmailStep() { 1420 // Update modal title and description 1421 document.getElementById('modal-title').textContent = 'Connect Your Business'; 1422 document.getElementById('modal-description').textContent = 'To connect your Google Business Profile, please provide your email address:'; 1423 1424 // Show email step, hide code step using classes 1425 document.getElementById('email-step').classList.add('active'); 1426 document.getElementById('code-step').classList.remove('active'); 1427 1428 // Clear code input 1429 document.getElementById('verification-code').value = ''; 1430 1431 // Re-enable send button 1432 const sendBtn = document.getElementById('send-code-btn'); 1433 sendBtn.disabled = false; 1434 sendBtn.textContent = 'Send Verification Code'; 1435 1436 console.log('Returned to email input step'); 1437 } 1438 1439 /** 1440 * Transition modal to success step 1441 */ 1442 transitionToSuccessStep() { 1443 // Hide both email and code steps using classes 1444 document.getElementById('email-step').classList.remove('active'); 1445 document.getElementById('code-step').classList.remove('active'); 1446 1447 // Show success step 1448 document.getElementById('success-step').classList.add('active'); 1449 1450 // Update header 1451 document.getElementById('modal-title').textContent = 'Success'; 1452 document.getElementById('modal-description').textContent = ''; 1453 1454 console.log('Transitioned to success step'); 1455 } 1456 1457 /** 1458 * Handle code verification 1459 */ 1460 async handleCodeVerification() { 1461 const code = document.getElementById('verification-code').value; 1462 const email = document.getElementById('verification-email').value; 1463 const verifyBtn = document.getElementById('verify-btn'); 1464 1465 // Get or retrieve user identifier from localStorage 1466 let userIdentifier = window.synoveo_ajax?.api_key; 1467 if (!userIdentifier || userIdentifier === '') { 1468 userIdentifier = localStorage.getItem('synoveo_lite_identifier'); 1469 } 1470 1471 if (code.length !== 6) { 1472 alert('Please enter a valid 6-digit code'); 1473 return; 1474 } 1475 1476 // Disable button and show loading state 1477 verifyBtn.disabled = true; 1478 verifyBtn.textContent = 'Verifying...'; 1479 1480 try { 1481 const response = await this.makeAjaxCall('synoveo_verify_code', { 1482 code: code, 1483 email: email, 1484 user_identifier: userIdentifier 1485 }); 1486 1487 if (response.success) { 1488 // Show success step 1489 this.transitionToSuccessStep(); 1490 1491 // After a brief delay, close modal and proceed to Google OAuth 1492 setTimeout(() => { 1493 this.closeEmailModal(); 1494 this.proceedToGoogleAuth(); 1495 }, 1500); 1496 } else { 1497 alert('Error: ' + (response.data?.message || 'Invalid verification code')); 1498 verifyBtn.disabled = false; 1499 verifyBtn.textContent = 'Verify & Continue'; 1500 } 1501 } catch (error) { 1502 console.error('Error verifying code:', error); 1503 alert('Error verifying code. Please try again.'); 1504 verifyBtn.disabled = false; 1505 verifyBtn.textContent = 'Verify & Continue'; 1506 } 1507 } 1508 1509 /** 1510 * Resend verification code 1511 */ 1512 async resendCode(email) { 1513 try { 1514 const response = await this.makeAjaxCall('synoveo_send_verification_code', { email: email }); 1515 1516 if (response.success) { 1517 alert('Verification code sent! Please check your email.'); 1518 } else { 1519 alert('Error: ' + response.data.message); 1520 } 1521 } catch (error) { 1522 console.error('Error resending code:', error); 1523 alert('Error resending code. Please try again.'); 1524 } 1525 } 1526 1527 /** 1528 * Close email modal 1529 */ 1530 closeEmailModal() { 1531 const modal = document.getElementById('synoveo-email-modal'); 1532 if (modal) { 1533 modal.remove(); 1534 document.body.style.overflow = 'auto'; 1535 } 719 1536 } 720 1537 } -
synoveo/trunk/includes/admin/class-synoveo-admin-controller.php
r3375574 r3376645 36 36 add_action( 'wp_ajax_synoveo_save_field_sources', array( $this, 'handle_save_field_sources' ) ); 37 37 add_action( 'wp_ajax_synoveo_get_business_info', array( $this, 'handle_get_business_info' ) ); 38 39 // Email verification AJAX handlers 40 add_action( 'wp_ajax_synoveo_check_email_status', array( $this, 'handle_check_email_status' ) ); 41 add_action( 'wp_ajax_synoveo_send_verification_code', array( $this, 'handle_send_verification_code' ) ); 42 add_action( 'wp_ajax_synoveo_verify_code', array( $this, 'handle_verify_code' ) ); 43 44 // Business rules validation AJAX handler 45 add_action( 'wp_ajax_synoveo_check_business_rules', array( $this, 'handle_check_business_rules' ) ); 46 47 // Lite user registration AJAX handler 48 add_action( 'wp_ajax_synoveo_register_lite_user', array( $this, 'handle_register_lite_user' ) ); 38 49 39 50 } … … 570 581 571 582 /** 583 * Check business rules before Google connection (AJAX preflight validation) 584 * 585 * Validates whether a user can connect to a Google Business Profile location 586 * based on their plan type, email verification status, and current usage. 587 * Returns structured response indicating if connection is allowed and why. 588 * 589 * @since 2.0.0 590 * @return void Sends JSON response with enforcement result via wp_send_json_success/error 591 */ 592 public function handle_check_business_rules(): void { 593 $this->verify_nonce_or_die( 'synoveo_nonce' ); 594 if ( ! current_user_can( 'manage_options' ) ) { 595 wp_send_json_error( array( 'message' => __( 'Insufficient permissions.', 'synoveo' ) ), 403 ); 596 } 597 598 try { 599 // Get location ID from request (optional) 600 $location_id = isset( $_POST['location_id'] ) ? sanitize_text_field( wp_unslash( $_POST['location_id'] ) ) : null; 601 $user_identifier = isset( $_POST['user_identifier'] ) ? sanitize_text_field( wp_unslash( $_POST['user_identifier'] ) ) : ''; 602 603 // Fallback to WordPress option if not provided 604 if ( empty( $user_identifier ) ) { 605 $user_identifier = get_option( 'synoveo_user_identifier', '' ); 606 } 607 608 // Build API request 609 $api_data = array( 610 'locationId' => $location_id, 611 ); 612 613 // Include user_identifier for Lite users 614 if ( ! empty( $user_identifier ) && str_starts_with( $user_identifier, 'synoveo_lite_' ) ) { 615 $api_data['user_identifier'] = $user_identifier; 616 } 617 618 // Call backend API to check business rules 619 $response = $this->api_service->call_api( 620 'google-business/check-business-rules', 621 $api_data 622 ); 623 624 if ( $response['success'] && isset( $response['data'] ) ) { 625 // Return the enforcement result 626 wp_send_json_success( $response['data'] ); 627 } else { 628 wp_send_json_error( 629 array( 630 'message' => $response['error'] ?? __( 'Failed to check business rules.', 'synoveo' ), 631 ) 632 ); 633 } 634 } catch ( Exception $e ) { 635 SYNOVEO_Logger::error( '[AJAX] handle_check_business_rules: ' . $e->getMessage(), 'AJAX' ); 636 wp_send_json_error( 637 array( 638 /* translators: %s: error message */ 639 'message' => sprintf( __( 'Failed to check business rules: %s', 'synoveo' ), $e->getMessage() ), 640 ) 641 ); 642 } 643 } 644 645 /** 646 * Handle Lite user registration (idempotent) 647 * 648 * Registers a new Lite user via the backend API and stores the user identifier 649 * in WordPress options as the source of truth for future API calls. This endpoint 650 * is idempotent and safe to call multiple times - it will return the existing user 651 * if already registered. 652 * 653 * Security: Requires nonce verification and manage_options capability. 654 * The user_identifier is stored locally in wp_options and sent to the backend 655 * registration endpoint which creates or retrieves the user record. 656 * 657 * @since 2.0.0 658 * @return void Sends JSON response with userId and registration status via wp_send_json_success/error 659 */ 660 public function handle_register_lite_user(): void { 661 $this->verify_nonce_or_die( 'synoveo_nonce' ); 662 663 if ( ! current_user_can( 'manage_options' ) ) { 664 wp_send_json_error( array( 'message' => __( 'Unauthorized', 'synoveo' ) ) ); 665 return; 666 } 667 668 $user_identifier = sanitize_text_field( wp_unslash( $_POST['user_identifier'] ?? '' ) ); 669 670 if ( empty( $user_identifier ) || ! str_starts_with( $user_identifier, 'synoveo_lite_' ) ) { 671 wp_send_json_error( array( 'message' => __( 'Invalid user identifier', 'synoveo' ) ) ); 672 return; 673 } 674 675 try { 676 // Store identifier in WordPress options (source of truth) 677 update_option( 'synoveo_user_identifier', $user_identifier ); 678 679 // Call backend registration endpoint 680 $response = $this->api_service->call_api( 681 'auth/register-lite', 682 array( 683 'user_identifier' => $user_identifier, 684 'site_url' => home_url(), 685 'site_name' => get_bloginfo( 'name' ), 686 ) 687 ); 688 689 if ( $response['success'] ) { 690 SYNOVEO_Logger::info( '✅ Lite user registered: ' . wp_json_encode( $response['data'] ), 'AdminController' ); 691 wp_send_json_success( $response['data'] ); 692 } else { 693 wp_send_json_error( array( 'message' => $response['error'] ?? __( 'Registration failed', 'synoveo' ) ) ); 694 } 695 } catch ( Exception $e ) { 696 SYNOVEO_Logger::error( 'Lite user registration failed: ' . $e->getMessage(), 'AdminController' ); 697 wp_send_json_error( array( 'message' => __( 'Registration failed', 'synoveo' ) ) ); 698 } 699 } 700 701 /** 572 702 * Handle save address manual AJAX request (NEW: Address-only handler) 573 703 */ … … 1194 1324 1195 1325 try { 1196 $redirect_uri = admin_url( 'admin.php?page=synoveo-dashboard' ); 1197 $state = wp_create_nonce( 'synoveo_google_auth' ); 1198 1199 SYNOVEO_Logger::debug( '🔍 DEBUG: Calling API with redirectUri: ' . $redirect_uri, 'Admin' ); 1200 SYNOVEO_Logger::debug( '🔍 DEBUG: State: ' . $state, 'Admin' ); 1201 1202 // Generate Google OAuth URL via API 1203 $api_response = $this->api_service->call_api( 1204 'auth/google-oauth-url', 1205 array( 1206 'return_to' => $redirect_uri, 1207 'state' => $state, 1208 ) 1209 ); 1326 // ENFORCE EMAIL VERIFICATION: Check before generating OAuth URL 1327 $license_manager = SYNOVEO_License_Manager::getInstance(); 1328 $current_plan = $license_manager->get_current_plan(); 1329 1330 if ( $current_plan === 'lite' ) { 1331 // For Lite users, verify email is verified before OAuth 1332 $email_check = $this->api_service->call_api( 1333 'auth/check-email-status', 1334 array( 1335 'user_identifier' => get_option( 'synoveo_api_key', '' ), 1336 ) 1337 ); 1338 1339 if ( $email_check['success'] && isset( $email_check['data'] ) ) { 1340 $email_verified = $email_check['data']['emailVerified'] ?? false; 1341 1342 if ( ! $email_verified ) { 1343 SYNOVEO_Logger::debug( '🚫 Email not verified - blocking OAuth URL generation', 'Admin' ); 1344 wp_send_json_error( 1345 array( 1346 'message' => __( 'Please verify your email before connecting to Google Business Profile.', 'synoveo' ), 1347 'code' => 'EMAIL_NOT_VERIFIED', 1348 ) 1349 ); 1350 return; 1351 } 1352 1353 SYNOVEO_Logger::debug( '✅ Email verified - proceeding with OAuth URL generation', 'Admin' ); 1354 } 1355 } 1356 1357 $redirect_uri = admin_url( 'admin.php?page=synoveo-dashboard' ); 1358 $state = wp_create_nonce( 'synoveo_google_auth' ); 1359 1360 // Get user_identifier from request (sent by frontend) 1361 $user_identifier = sanitize_text_field( wp_unslash( $_POST['user_identifier'] ?? '' ) ); 1362 1363 // Fallback to WordPress option if not provided 1364 if ( empty( $user_identifier ) ) { 1365 $user_identifier = get_option( 'synoveo_user_identifier', '' ); 1366 } 1367 1368 SYNOVEO_Logger::debug( '🔍 DEBUG: Calling API with redirectUri: ' . $redirect_uri, 'Admin' ); 1369 SYNOVEO_Logger::debug( '🔍 DEBUG: State: ' . $state, 'Admin' ); 1370 SYNOVEO_Logger::debug( '🔍 DEBUG: user_identifier: ' . $user_identifier, 'Admin' ); 1371 1372 // Build API request 1373 $api_request = array( 1374 'return_to' => $redirect_uri, 1375 ); 1376 1377 // Include user_identifier for Lite users 1378 if ( ! empty( $user_identifier ) && str_starts_with( $user_identifier, 'synoveo_lite_' ) ) { 1379 $api_request['user_identifier'] = $user_identifier; 1380 } 1381 1382 // Generate Google OAuth URL via API 1383 $api_response = $this->api_service->call_api( 1384 'auth/google-oauth-url', 1385 $api_request 1386 ); 1210 1387 1211 1388 SYNOVEO_Logger::debug( '🔍 DEBUG: API response: ' . wp_json_encode( $api_response ), 'Admin' ); … … 1593 1770 1594 1771 try { 1595 $auth_code = sanitize_text_field( wp_unslash( $_POST['auth_code'] ?? '' ) ); 1772 $auth_code = sanitize_text_field( wp_unslash( $_POST['auth_code'] ?? '' ) ); 1773 $user_identifier = sanitize_text_field( wp_unslash( $_POST['user_identifier'] ?? '' ) ); 1596 1774 1597 1775 if ( empty( $auth_code ) ) { … … 1599 1777 return; 1600 1778 } 1779 1780 // Fallback to WordPress option if not provided 1781 if ( empty( $user_identifier ) ) { 1782 $user_identifier = get_option( 'synoveo_user_identifier', '' ); 1783 } 1784 1785 // Build API request 1786 $api_data = array( 1787 'authCode' => $auth_code, 1788 ); 1789 1790 // Include user_identifier for Lite users 1791 if ( ! empty( $user_identifier ) && str_starts_with( $user_identifier, 'synoveo_lite_' ) ) { 1792 $api_data['user_identifier'] = $user_identifier; 1793 } 1601 1794 1602 1795 // List user's business locations via Synoveo API 1603 1796 $api_response = $this->api_service->call_api( 1604 1797 'google-business/list-locations', 1605 array( 1606 'authCode' => $auth_code, 1607 ) 1798 $api_data 1608 1799 ); 1609 1800 … … 1642 1833 $selected_location_id = sanitize_text_field( wp_unslash( $_POST['location_id'] ?? '' ) ); 1643 1834 $session_id = sanitize_text_field( wp_unslash( $_POST['session_id'] ?? '' ) ); 1835 $user_identifier = sanitize_text_field( wp_unslash( $_POST['user_identifier'] ?? '' ) ); 1644 1836 1645 1837 if ( empty( $auth_code ) || empty( $selected_location_id ) ) { … … 1648 1840 } 1649 1841 1842 // Build API request data 1843 $api_data = array( 1844 'authCode' => $auth_code, 1845 'selectedLocationId' => $selected_location_id, 1846 'sessionId' => $session_id, 1847 'wordpressUrl' => home_url(), 1848 ); 1849 1850 // For Lite users, include user_identifier from frontend 1851 if ( ! empty( $user_identifier ) ) { 1852 $api_data['user_identifier'] = $user_identifier; 1853 } 1854 1650 1855 // Connect to selected business location via Synoveo API 1651 1856 $api_response = $this->api_service->call_api( 1652 1857 'google-business/connect', 1653 array( 1654 'authCode' => $auth_code, 1655 'selectedLocationId' => $selected_location_id, 1656 'sessionId' => $session_id, 1657 'wordpressUrl' => home_url(), 1658 ) 1659 ); 1660 1661 if ( $api_response['success'] ) { 1662 // Save connection status locally 1663 update_option( 'synoveo_google_business_connected', true ); 1664 update_option( 'synoveo_google_connected_at', current_time( 'mysql' ) ); 1665 update_option( 'synoveo_google_business_name', $api_response['data']['businessName'] ?? '' ); 1666 update_option( 'synoveo_selected_location_id', $selected_location_id ); 1858 $api_data 1859 ); 1860 1861 if ( $api_response['success'] ) { 1862 // Save connection status locally 1863 update_option( 'synoveo_google_business_connected', true ); 1864 update_option( 'synoveo_google_connected_at', current_time( 'mysql' ) ); 1865 update_option( 'synoveo_google_business_name', $api_response['data']['businessName'] ?? '' ); 1866 update_option( 'synoveo_selected_location_id', $selected_location_id ); 1867 1868 // Cache the Synoveo business_id returned by the API (critical for GBP Compare) 1869 if ( isset( $api_response['data']['businessId'] ) ) { 1870 update_option( 'synoveo_business_id', (int) $api_response['data']['businessId'] ); 1871 SYNOVEO_Logger::info( '✅ Cached business_id: ' . $api_response['data']['businessId'], 'AdminController' ); 1872 } 1667 1873 // Initialize lifecycle storage with selected location 1668 1874 $status = $this->ensure_address_status(); … … 3570 3776 } 3571 3777 } 3778 3779 /** 3780 * Handle email verification status check 3781 */ 3782 public function handle_check_email_status() { 3783 try { 3784 $this->verify_nonce_or_die( 'synoveo_nonce' ); 3785 3786 $user_identifier = sanitize_text_field( $_POST['user_identifier'] ?? '' ); 3787 3788 if ( empty( $user_identifier ) ) { 3789 wp_send_json_error( array( 'message' => __( 'User identifier required', 'synoveo' ) ) ); 3790 return; 3791 } 3792 3793 $api_response = $this->api_service->call_api( 3794 'auth/check-email-status', 3795 array( 3796 'user_identifier' => $user_identifier 3797 ) 3798 ); 3799 3800 wp_send_json_success( $api_response ); 3801 } catch ( Exception $e ) { 3802 wp_send_json_error( array( 'message' => __( 'Failed to check email status', 'synoveo' ) ) ); 3803 } 3804 } 3805 3806 /** 3807 * Handle sending verification code 3808 */ 3809 public function handle_send_verification_code() { 3810 try { 3811 $this->verify_nonce_or_die( 'synoveo_nonce' ); 3812 3813 $email = sanitize_email( $_POST['email'] ); 3814 $user_identifier = sanitize_text_field( $_POST['user_identifier'] ?? '' ); 3815 3816 if ( ! is_email( $email ) ) { 3817 wp_send_json_error( array( 'message' => __( 'Please provide a valid email address', 'synoveo' ) ) ); 3818 return; 3819 } 3820 3821 if ( empty( $user_identifier ) ) { 3822 wp_send_json_error( array( 'message' => __( 'User identifier required', 'synoveo' ) ) ); 3823 return; 3824 } 3825 3826 $api_response = $this->api_service->call_api( 3827 'auth/send-verification-code', 3828 array( 3829 'email' => $email, 3830 'userIdentifier' => $user_identifier 3831 ) 3832 ); 3833 3834 wp_send_json_success( $api_response ); 3835 } catch ( Exception $e ) { 3836 wp_send_json_error( array( 'message' => __( 'Failed to send verification code', 'synoveo' ) ) ); 3837 } 3838 } 3839 3840 /** 3841 * Handle code verification 3842 */ 3843 public function handle_verify_code() { 3844 try { 3845 $this->verify_nonce_or_die( 'synoveo_nonce' ); 3846 3847 $code = sanitize_text_field( $_POST['code'] ); 3848 $email = sanitize_email( $_POST['email'] ); 3849 $user_identifier = sanitize_text_field( $_POST['user_identifier'] ?? '' ); 3850 3851 if ( strlen( $code ) !== 6 || ! is_numeric( $code ) ) { 3852 wp_send_json_error( array( 'message' => __( 'Please enter a valid 6-digit code', 'synoveo' ) ) ); 3853 return; 3854 } 3855 3856 if ( empty( $user_identifier ) ) { 3857 wp_send_json_error( array( 'message' => __( 'User identifier required', 'synoveo' ) ) ); 3858 return; 3859 } 3860 3861 $api_response = $this->api_service->call_api( 3862 'auth/verify-code', 3863 array( 3864 'code' => $code, 3865 'email' => $email, 3866 'userIdentifier' => $user_identifier 3867 ) 3868 ); 3869 3870 wp_send_json_success( $api_response ); 3871 } catch ( Exception $e ) { 3872 wp_send_json_error( array( 'message' => __( 'Failed to verify code', 'synoveo' ) ) ); 3873 } 3874 } 3572 3875 } -
synoveo/trunk/includes/services/class-synoveo-api-service.php
r3375269 r3376645 190 190 } 191 191 /** 192 * Generate deterministic API key for Lite users based on domain 192 * Generate or retrieve deterministic API key for Lite users 193 * 194 * Priority order: 195 * 1. Uses registered user_identifier from WordPress options (source of truth) 196 * 2. Falls back to generating deterministic key based on domain 197 * 198 * This ensures consistent API key usage across all Lite user interactions 199 * and prevents duplicate user creation by always using the registered identifier. 200 * 201 * @since 2.0.0 202 * @return string The Lite user identifier in format synoveo_lite_<hash> 193 203 */ 194 204 private function getLiteApiKey(): string { 205 // PRIORITY: Use registered user_identifier from WordPress option (source of truth) 206 $stored_identifier = get_option( 'synoveo_user_identifier', '' ); 207 if ( ! empty( $stored_identifier ) && str_starts_with( $stored_identifier, 'synoveo_lite_' ) ) { 208 return $stored_identifier; 209 } 210 211 // Fallback: Generate deterministic key based on domain (same logic as backend) 195 212 $domain = wp_parse_url( home_url(), PHP_URL_HOST ); 196 213 $domain = strtolower( trim( $domain ) ); … … 295 312 296 313 // CRITICAL FIX: Pass through ALL error fields from API, not just 'error' 314 // Support both 'error' and 'message' fields (for plan enforcement errors) 315 $error_message = $response_data['message'] ?? $response_data['error'] ?? 'API request failed'; 316 297 317 $error_response = array( 298 318 'success' => false, 299 'error' => $ response_data['error'] ?? 'API request failed',319 'error' => $error_message, 300 320 'status_code' => $status_code, 301 321 'retry_after' => $retry_after, 302 322 ); 323 324 // Pass through error code if present (for plan enforcement) 325 if ( isset( $response_data['code'] ) ) { 326 $error_response['code'] = $response_data['code']; 327 } 303 328 304 329 // Pass through Google Business Profile error fields if present -
synoveo/trunk/includes/services/class-synoveo-frontend-service.php
r3375269 r3376645 69 69 wp_enqueue_style( 'synoveo-frontend-connect', SYNOVEO_PLUGIN_URL . 'assets/css/frontend-connect.css', array(), SYNOVEO_VERSION ); 70 70 71 // Enqueue frontend connection JavaScript 72 wp_enqueue_script( 'synoveo-frontend-connect', SYNOVEO_PLUGIN_URL . 'assets/js/frontend-connect.js', array( ), SYNOVEO_VERSION, true );71 // Enqueue frontend connection JavaScript with jQuery dependency 72 wp_enqueue_script( 'synoveo-frontend-connect', SYNOVEO_PLUGIN_URL . 'assets/js/frontend-connect.js', array( 'jquery' ), SYNOVEO_VERSION, true ); 73 73 74 74 // Pass data to JavaScript … … 94 94 public function handle_oauth_callback( string $auth_code ): array { 95 95 try { 96 $api_response = $this->api_service->call_api( 'google-business/list-locations', array( 'authCode' => $auth_code ) ); 96 // Fetch stable identifier from WP option (source of truth) 97 $user_identifier = get_option( 'synoveo_user_identifier', '' ); 98 99 $payload = array( 'authCode' => $auth_code ); 100 if ( ! empty( $user_identifier ) && str_starts_with( $user_identifier, 'synoveo_lite_' ) ) { 101 $payload['user_identifier'] = $user_identifier; 102 } 103 104 $api_response = $this->api_service->call_api( 'google-business/list-locations', $payload ); 97 105 98 106 if ( ! $api_response['success'] ) { … … 118 126 public function connect_single_business( string $auth_code, array $location ): bool { 119 127 try { 128 // Fetch stable identifier from WP option (source of truth) 129 $user_identifier = get_option( 'synoveo_user_identifier', '' ); 130 131 $data = array( 132 'authCode' => $auth_code, 133 'selectedLocationId' => $location['name'], 134 'wordpressUrl' => home_url(), 135 ); 136 if ( ! empty( $user_identifier ) && str_starts_with( $user_identifier, 'synoveo_lite_' ) ) { 137 $data['user_identifier'] = $user_identifier; 138 } 139 120 140 $connect_response = $this->api_service->call_api( 121 141 'google-business/connect', 122 array( 123 'authCode' => $auth_code, 124 'selectedLocationId' => $location['name'], 125 'wordpressUrl' => home_url(), 126 ) 142 $data 127 143 ); 128 144 -
synoveo/trunk/readme.txt
r3375574 r3376645 5 5 Tested up to: 6.8 6 6 Requires PHP: 7.4 7 Stable tag: 1. 0.77 Stable tag: 1.1.0 8 8 License: GPLv2 or later 9 9 License URI: https://www.gnu.org/licenses/gpl-2.0.html … … 253 253 254 254 == Changelog == 255 256 = 1.1.0 = 257 * **MAJOR:** Implemented comprehensive plan-based business rules enforcement across all endpoints 258 * **MAJOR:** Added explicit Lite user registration flow to prevent duplicate user creation 259 * **MAJOR:** Enhanced middleware to check email verification status and set lite_verified plan type 260 * **SECURITY:** Removed automatic user creation - users must explicitly register 261 * **SECURITY:** Added email verification gate before Google OAuth for Lite users 262 * **ENHANCEMENT:** Deterministic user identifier generation for stable cross-session identity 263 * **ENHANCEMENT:** User identifier now stored in WordPress options as source of truth 264 * **ENHANCEMENT:** Added plan validation preflight check before Google connection 265 * **ENHANCEMENT:** Implemented idempotent reconnection - existing locations can be reconnected 266 * **ENHANCEMENT:** Business name automatically updated from Google Business Profile data 267 * **ENHANCEMENT:** OAuth session tokens cached in Redis for improved performance 268 * **BUGFIX:** Fixed middleware to prioritize user_identifier from request body over header-generated hash 269 * **BUGFIX:** Fixed getLiteApiKey() to use registered identifier instead of regenerating hash 270 * **BUGFIX:** Fixed enforcement logic to check existing location BEFORE applying limits 271 * **BUGFIX:** Removed metadata column from registration (compatibility with production schema) 272 * **PERFORMANCE:** Cleaned up unused imports and dead code 273 * **COMPLIANCE:** Added complete PHPDoc blocks for all new methods (@since 2.0.0) 274 * **API:** New endpoint POST /auth/register-lite for explicit user registration 275 * **API:** New endpoint POST /google-business/check-business-rules for preflight validation 276 * **API:** Enhanced middleware with Lite verification awareness (checks DB email_verified flag) 255 277 256 278 = 1.0.7 = -
synoveo/trunk/synoveo.php
r3375574 r3376645 3 3 * Plugin Name: Synoveo – Turn Your Google Profile Into a Sales Engine 4 4 * Plugin URI: https://www.synoveo.com 5 * Description: Grow revenue by improving your online business presence. Synoveo keeps your Google Business Profile accurate and up-to-date , driving more calls, bookings, visits, and sales.6 * Version: 1. 0.75 * Description: Grow revenue by improving your online business presence. Synoveo keeps your Google Business Profile accurate and up-to-date. 6 * Version: 1.1.0 7 7 * Author: Synoveo (CODE75) 8 8 * Author URI: https://www.synoveo.com … … 24 24 // Plugin constants 25 25 // Single source of truth for version number 26 $base_version = '1. 0.7'; // ← Change only this when bumping version26 $base_version = '1.1.0'; // ← Change only this when bumping version 27 27 define( 'SYNOVEO_VERSION', defined( 'WP_DEBUG' ) && WP_DEBUG ? $base_version . '-dev-' . time() : $base_version ); 28 28 define( 'SYNOVEO_PLUGIN_URL', plugin_dir_url( __FILE__ ) ); … … 412 412 'time_format' => get_option( 'time_format' ) === 'H:i' ? '24h' : '12h', 413 413 'debug_enabled' => ( defined( 'WP_DEBUG' ) && WP_DEBUG ) || ( defined( 'SYNOVEO_DEBUG' ) && SYNOVEO_DEBUG ) || get_option( 'synoveo_debug_logging', false ), 414 'api_key' => get_option( 'synoveo_api_key', '' ), 414 415 ) 415 416 );
Note: See TracChangeset
for help on using the changeset viewer.