Plugin Directory

Changeset 3376645


Ignore:
Timestamp:
10/11/2025 11:49:03 AM (6 months ago)
Author:
synoveo
Message:

Deploy synoveo v1.1.0

Location:
synoveo/trunk
Files:
8 edited

Legend:

Unmodified
Added
Removed
  • synoveo/trunk/assets/css/admin.css

    r3375269 r3376645  
    4343    background: #dcfce7;
    4444    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;
    45208}
    46209
  • synoveo/trunk/assets/js/frontend-connect.js

    r3375269 r3376645  
    3232    document.getElementById('connect-button').disabled = true;
    3333   
    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   
    3557    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)
    4763    })
    4864    .then(response => response.json())
  • synoveo/trunk/assets/js/google-business-connector.js

    r3375269 r3376645  
    2222   
    2323    /**
     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    /**
    24120     * UNIFIED ENTRY POINT: Start Growing Your Revenue
    25121     * This is THE function that all "Start Growing Your Revenue" buttons should call
    26122     */
    27123    async startGrowthProcess() {
    28         console.log('🚀 Starting Synoveo growth process...');
     124        SynoveoLogger.info('Starting Synoveo growth process...', 'GoogleBusiness');
    29125       
    30126        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
    32135            let status;
    33136            try {
     
    59162     */
    60163    async startConnectionFlow() {
    61         // Show connection wizard
     164        // Show connection wizard and proceed directly to Google OAuth
    62165        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
    65205        try {
    66             console.log('🔍 DEBUG: Starting auth URL request...');
     206            SynoveoLogger.debug('Starting Google OAuth flow...', 'GoogleBusiness');
    67207            const authUrlResponse = await this.getGoogleAuthUrl();
    68             console.log('🔍 DEBUG: Auth URL response received:', authUrlResponse);
    69208           
    70209            if (authUrlResponse.success) {
    71                 console.log('🔍 DEBUG: Success! Showing Google auth step with URL:', authUrlResponse.data.auth_url);
    72210                this.showGoogleAuthStep(authUrlResponse.data.auth_url);
    73211            } else {
    74                 console.log('🔍 DEBUG: Failed! Error:', authUrlResponse.error);
    75212                throw new Error(authUrlResponse.error);
    76213            }
    77214           
    78215        } 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);
    81217            this.showErrorState('Failed to initialize Google connection');
    82218        }
     
    87223     */
    88224    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       
    91234        console.log('🔍 DEBUG: getGoogleAuthUrl response:', response);
    92235        console.log('🔍 DEBUG: Response success:', response?.success);
     
    154297                    } else if (locations.length === 1) {
    155298                        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                       
    156308                        // Single business - connect directly
    157309                        await this.connectToSelectedBusiness(authCode, locations[0].name);
     
    176328     */
    177329    async listBusinessLocations(authCode) {
     330        // Get stable user identifier
     331        const userIdentifier = this.getOrCreateUserIdentifier();
     332       
    178333        return this.makeAjaxCall('synoveo_list_business_locations', {
    179             auth_code: authCode
     334            auth_code: authCode,
     335            user_identifier: userIdentifier
    180336        });
    181337    }
     
    186342    async connectToSelectedBusiness(authCode, locationId) {
    187343        try {
    188             const connectionResponse = await this.makeAjaxCall('synoveo_select_business_location', {
     344            // Get stable identifier
     345            const userIdentifier = this.getOrCreateUserIdentifier();
     346           
     347            const requestData = {
    189348                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);
    192356           
    193357            if (connectionResponse.success) {
     
    304468        wizard.style.display = 'flex';
    305469        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        });
    306533    }
    307534   
     
    548775   
    549776    /**
     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    /**
    550829     * Select specific business location
    551830     */
    552831    async selectBusiness(locationId, authCode) {
    553832        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           
    554841            await this.connectToSelectedBusiness(authCode, locationId);
    555842            this.closeBusinessSelectionModal();
     
    7171004       
    7181005        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        }
    7191536    }
    7201537}
  • synoveo/trunk/includes/admin/class-synoveo-admin-controller.php

    r3375574 r3376645  
    3636            add_action( 'wp_ajax_synoveo_save_field_sources', array( $this, 'handle_save_field_sources' ) );
    3737            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' ) );
    3849
    3950        }
     
    570581
    571582    /**
     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    /**
    572702     * Handle save address manual AJAX request (NEW: Address-only handler)
    573703     */
     
    11941324
    11951325        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        );
    12101387
    12111388            SYNOVEO_Logger::debug( '🔍 DEBUG: API response: ' . wp_json_encode( $api_response ), 'Admin' );
     
    15931770
    15941771        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'] ?? '' ) );
    15961774
    15971775            if ( empty( $auth_code ) ) {
     
    15991777                return;
    16001778            }
     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            }
    16011794
    16021795            // List user's business locations via Synoveo API
    16031796            $api_response = $this->api_service->call_api(
    16041797                'google-business/list-locations',
    1605                 array(
    1606                     'authCode' => $auth_code,
    1607                 )
     1798                $api_data
    16081799            );
    16091800
     
    16421833            $selected_location_id = sanitize_text_field( wp_unslash( $_POST['location_id'] ?? '' ) );
    16431834            $session_id           = sanitize_text_field( wp_unslash( $_POST['session_id'] ?? '' ) );
     1835            $user_identifier      = sanitize_text_field( wp_unslash( $_POST['user_identifier'] ?? '' ) );
    16441836
    16451837            if ( empty( $auth_code ) || empty( $selected_location_id ) ) {
     
    16481840            }
    16491841
     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
    16501855            // Connect to selected business location via Synoveo API
    16511856            $api_response = $this->api_service->call_api(
    16521857                '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            }
    16671873                // Initialize lifecycle storage with selected location
    16681874                $status                = $this->ensure_address_status();
     
    35703776        }
    35713777    }
     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    }
    35723875}
  • synoveo/trunk/includes/services/class-synoveo-api-service.php

    r3375269 r3376645  
    190190    }
    191191    /**
    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>
    193203     */
    194204    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)
    195212        $domain = wp_parse_url( home_url(), PHP_URL_HOST );
    196213        $domain = strtolower( trim( $domain ) );
     
    295312
    296313            // 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           
    297317            $error_response = array(
    298318                'success'     => false,
    299                 'error'       => $response_data['error'] ?? 'API request failed',
     319                'error'       => $error_message,
    300320                'status_code' => $status_code,
    301321                'retry_after' => $retry_after,
    302322            );
     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            }
    303328
    304329            // Pass through Google Business Profile error fields if present
  • synoveo/trunk/includes/services/class-synoveo-frontend-service.php

    r3375269 r3376645  
    6969        wp_enqueue_style( 'synoveo-frontend-connect', SYNOVEO_PLUGIN_URL . 'assets/css/frontend-connect.css', array(), SYNOVEO_VERSION );
    7070
    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 );
    7373
    7474        // Pass data to JavaScript
     
    9494    public function handle_oauth_callback( string $auth_code ): array {
    9595        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 );
    97105
    98106            if ( ! $api_response['success'] ) {
     
    118126    public function connect_single_business( string $auth_code, array $location ): bool {
    119127        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
    120140            $connect_response = $this->api_service->call_api(
    121141                'google-business/connect',
    122                 array(
    123                     'authCode'           => $auth_code,
    124                     'selectedLocationId' => $location['name'],
    125                     'wordpressUrl'       => home_url(),
    126                 )
     142                $data
    127143            );
    128144
  • synoveo/trunk/readme.txt

    r3375574 r3376645  
    55Tested up to: 6.8
    66Requires PHP: 7.4
    7 Stable tag: 1.0.7
     7Stable tag: 1.1.0
    88License: GPLv2 or later
    99License URI: https://www.gnu.org/licenses/gpl-2.0.html
     
    253253
    254254== 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)
    255277
    256278= 1.0.7 =
  • synoveo/trunk/synoveo.php

    r3375574 r3376645  
    33 * Plugin Name: Synoveo – Turn Your Google Profile Into a Sales Engine
    44 * 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.7
     5 * 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
    77 * Author: Synoveo (CODE75)
    88 * Author URI: https://www.synoveo.com
     
    2424// Plugin constants
    2525// Single source of truth for version number
    26 $base_version = '1.0.7'; // ← Change only this when bumping version
     26$base_version = '1.1.0'; // ← Change only this when bumping version
    2727define( 'SYNOVEO_VERSION', defined( 'WP_DEBUG' ) && WP_DEBUG ? $base_version . '-dev-' . time() : $base_version );
    2828define( 'SYNOVEO_PLUGIN_URL', plugin_dir_url( __FILE__ ) );
     
    412412                'time_format'   => get_option( 'time_format' ) === 'H:i' ? '24h' : '12h',
    413413                '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', '' ),
    414415            )
    415416        );
Note: See TracChangeset for help on using the changeset viewer.