VFS Global Auto Appointment Booker

Automatically monitors and books available appointment slots on VFS Global

Size

22.9 KB

Version

1.1.1

Created

Dec 9, 2025

Updated

3 days ago

1// ==UserScript==
2// @name		VFS Global Auto Appointment Booker
3// @description		Automatically monitors and books available appointment slots on VFS Global
4// @version		1.1.1
5// @match		https://*.visa.vfsglobal.com/*
6// @icon		https://liftassets.vfsglobal.com/_angular/assets/images/global/vfs.ico
7// @grant		GM.getValue
8// @grant		GM.setValue
9// @grant		GM.notification
10// ==/UserScript==
11(function() {
12    'use strict';
13
14    console.log('VFS Global Auto Appointment Booker - Initialized');
15
16    // Configuration
17    const CONFIG = {
18        CHECK_INTERVAL: 30000, // Check every 30 seconds
19        AUTO_BOOK_ENABLED_KEY: 'vfs_auto_book_enabled',
20        LAST_CHECK_KEY: 'vfs_last_check_time',
21        BOOKING_ATTEMPTS_KEY: 'vfs_booking_attempts',
22        MAX_RETRY_ATTEMPTS: 3
23    };
24
25    // State management
26    let isMonitoring = false;
27    let monitoringInterval = null;
28    let statusPanel = null;
29
30    // Utility function to wait for element
31    function waitForElement(selector, timeout = 10000) {
32        return new Promise((resolve, reject) => {
33            const element = document.querySelector(selector);
34            if (element) {
35                resolve(element);
36                return;
37            }
38
39            const observer = new MutationObserver((mutations, obs) => {
40                const element = document.querySelector(selector);
41                if (element) {
42                    obs.disconnect();
43                    resolve(element);
44                }
45            });
46
47            observer.observe(document.body, {
48                childList: true,
49                subtree: true
50            });
51
52            setTimeout(() => {
53                observer.disconnect();
54                reject(new Error(`Element ${selector} not found within ${timeout}ms`));
55            }, timeout);
56        });
57    }
58
59    // Debounce function
60    function debounce(func, wait) {
61        let timeout;
62        return function executedFunction(...args) {
63            const later = () => {
64                clearTimeout(timeout);
65                func(...args);
66            };
67            clearTimeout(timeout);
68            timeout = setTimeout(later, wait);
69        };
70    }
71
72    // Update status in UI
73    function updateStatus(message, type = 'info') {
74        console.log(`[VFS Auto Booker] ${message}`);
75        if (statusPanel) {
76            const statusText = statusPanel.querySelector('.vfs-status-text');
77            const timestamp = statusPanel.querySelector('.vfs-timestamp');
78            if (statusText) {
79                statusText.textContent = message;
80                statusText.className = `vfs-status-text vfs-status-${type}`;
81            }
82            if (timestamp) {
83                timestamp.textContent = new Date().toLocaleTimeString();
84            }
85        }
86    }
87
88    // Check for available appointment slots
89    async function checkAvailableSlots() {
90        try {
91            console.log('Checking for available appointment slots...');
92            
93            // Find all future dates that are not disabled
94            const availableDates = Array.from(document.querySelectorAll('.fc-daygrid-day.fc-day-future:not([aria-disabled="true"])'));
95            
96            if (availableDates.length === 0) {
97                updateStatus('No available dates found', 'warning');
98                return null;
99            }
100
101            console.log(`Found ${availableDates.length} available dates`);
102            
103            // Get the first available date
104            const firstAvailableDate = availableDates[0];
105            const dateStr = firstAvailableDate.getAttribute('data-date');
106            
107            updateStatus(`Found available date: ${dateStr}`, 'success');
108            return firstAvailableDate;
109            
110        } catch (error) {
111            console.error('Error checking available slots:', error);
112            updateStatus(`Error: ${error.message}`, 'error');
113            return null;
114        }
115    }
116
117    // Click on a date to see available time slots
118    async function selectDate(dateElement) {
119        try {
120            console.log('Selecting date...');
121            updateStatus('Selecting available date...', 'info');
122            
123            // Click on the date
124            const dateLink = dateElement.querySelector('.fc-daygrid-day-number');
125            if (dateLink) {
126                dateLink.click();
127                console.log('Date clicked');
128                
129                // Wait for time slots to appear
130                await new Promise(resolve => setTimeout(resolve, 2000));
131                return true;
132            }
133            
134            return false;
135        } catch (error) {
136            console.error('Error selecting date:', error);
137            updateStatus(`Error selecting date: ${error.message}`, 'error');
138            return false;
139        }
140    }
141
142    // Select the first available time slot
143    async function selectTimeSlot() {
144        try {
145            console.log('Looking for time slots...');
146            updateStatus('Looking for available time slots...', 'info');
147            
148            // Wait for time slot container to appear
149            await waitForElement('.time-slot-container, .slot-container, [class*="slot"], [class*="time"]', 5000);
150            
151            // Look for available time slot buttons
152            const timeSlotSelectors = [
153                'button[class*="slot"]:not([disabled])',
154                'button[class*="time"]:not([disabled])',
155                '.time-slot:not(.disabled)',
156                '.slot-available',
157                'button.btn-brand-orange:not([disabled])'
158            ];
159            
160            let timeSlotButton = null;
161            for (const selector of timeSlotSelectors) {
162                timeSlotButton = document.querySelector(selector);
163                if (timeSlotButton) {
164                    console.log(`Found time slot with selector: ${selector}`);
165                    break;
166                }
167            }
168            
169            if (timeSlotButton) {
170                console.log('Clicking time slot button...');
171                timeSlotButton.click();
172                updateStatus('Time slot selected!', 'success');
173                await new Promise(resolve => setTimeout(resolve, 1000));
174                return true;
175            } else {
176                updateStatus('No time slots available', 'warning');
177                return false;
178            }
179            
180        } catch (error) {
181            console.error('Error selecting time slot:', error);
182            updateStatus(`Error selecting time slot: ${error.message}`, 'error');
183            return false;
184        }
185    }
186
187    // Click the Play/Continue button
188    async function clickPlayButton() {
189        try {
190            console.log('Looking for Play/Continue button...');
191            updateStatus('Proceeding to next step...', 'info');
192            
193            const playButton = document.querySelector('button.btn-brand-orange, button[class*="continue"], button[class*="next"]');
194            
195            if (playButton && !playButton.disabled) {
196                console.log('Clicking Play/Continue button...');
197                playButton.click();
198                updateStatus('Proceeding with booking...', 'success');
199                await new Promise(resolve => setTimeout(resolve, 2000));
200                return true;
201            } else {
202                updateStatus('Play button not available', 'warning');
203                return false;
204            }
205            
206        } catch (error) {
207            console.error('Error clicking play button:', error);
208            updateStatus(`Error: ${error.message}`, 'error');
209            return false;
210        }
211    }
212
213    // Auto-accept Terms and Conditions and click confirm/pay button
214    async function autoAcceptAndConfirm() {
215        try {
216            console.log('Looking for Terms and Conditions checkbox...');
217            
218            // Find the Terms and Conditions checkbox - try multiple selectors
219            let termsCheckbox = document.querySelector('mat-checkbox.consent-checkbox');
220            
221            if (termsCheckbox) {
222                // Check if it's already checked
223                const isChecked = termsCheckbox.classList.contains('mat-mdc-checkbox-checked');
224                
225                if (!isChecked) {
226                    console.log('Clicking Terms and Conditions checkbox...');
227                    termsCheckbox.click();
228                    console.log('Terms and Conditions accepted');
229                    await new Promise(resolve => setTimeout(resolve, 1000));
230                } else {
231                    console.log('Terms and Conditions already accepted');
232                }
233            } else {
234                console.log('Terms and Conditions checkbox not found');
235            }
236            
237            // Wait a moment for the button to become enabled
238            await new Promise(resolve => setTimeout(resolve, 1500));
239            
240            // Find and click the confirm/pay button
241            const confirmButton = document.querySelector('button#trigger[type="submit"]');
242            
243            if (confirmButton && !confirmButton.disabled) {
244                console.log('Clicking confirm/pay button...');
245                confirmButton.click();
246                console.log('Confirm button clicked successfully');
247                return true;
248            } else {
249                console.log('Confirm button not available or disabled');
250                return false;
251            }
252            
253        } catch (error) {
254            console.error('Error in auto-accept and confirm:', error);
255            return false;
256        }
257    }
258
259    // Check if we're on the review/payment page and auto-click
260    async function handleReviewPage() {
261        if (window.location.href.includes('review-pay') || window.location.href.includes('review_pay') || window.location.href.includes('review')) {
262            console.log('Review/Payment page detected');
263            
264            // Wait for the page to fully load
265            try {
266                await waitForElement('mat-checkbox.consent-checkbox, button#trigger', 10000);
267                console.log('Elements found, proceeding with auto-click');
268            } catch (error) {
269                console.log('Elements not found within timeout, page may require authentication');
270                return;
271            }
272            
273            // Wait a bit more to ensure everything is loaded
274            await new Promise(resolve => setTimeout(resolve, 1000));
275            
276            // Auto-accept and confirm
277            const success = await autoAcceptAndConfirm();
278            
279            if (success) {
280                console.log('Successfully accepted terms and clicked confirm');
281            } else {
282                console.log('Failed to complete auto-accept and confirm');
283            }
284        }
285    }
286
287    // Main booking automation function
288    async function attemptBooking() {
289        try {
290            const attempts = await GM.getValue(CONFIG.BOOKING_ATTEMPTS_KEY, 0);
291            
292            if (attempts >= CONFIG.MAX_RETRY_ATTEMPTS) {
293                updateStatus('Max retry attempts reached. Please check manually.', 'error');
294                await stopMonitoring();
295                return false;
296            }
297
298            updateStatus('Starting booking attempt...', 'info');
299            console.log('=== Starting Booking Attempt ===');
300            
301            // Step 1: Check for available slots
302            const availableDate = await checkAvailableSlots();
303            if (!availableDate) {
304                updateStatus('No available dates. Will retry...', 'warning');
305                return false;
306            }
307
308            // Step 2: Select the date
309            const dateSelected = await selectDate(availableDate);
310            if (!dateSelected) {
311                updateStatus('Failed to select date. Will retry...', 'error');
312                await GM.setValue(CONFIG.BOOKING_ATTEMPTS_KEY, attempts + 1);
313                return false;
314            }
315
316            // Step 3: Select time slot
317            const timeSlotSelected = await selectTimeSlot();
318            if (!timeSlotSelected) {
319                updateStatus('No time slots available. Will retry...', 'warning');
320                return false;
321            }
322
323            // Step 4: Click Play/Continue button
324            const playClicked = await clickPlayButton();
325            if (!playClicked) {
326                updateStatus('Failed to proceed. Will retry...', 'error');
327                await GM.setValue(CONFIG.BOOKING_ATTEMPTS_KEY, attempts + 1);
328                return false;
329            }
330
331            // Success!
332            updateStatus('Booking process initiated successfully!', 'success');
333            console.log('=== Booking Attempt Successful ===');
334            
335            // Show browser notification
336            if (typeof GM.notification !== 'undefined') {
337                GM.notification({
338                    title: 'VFS Appointment Booking',
339                    text: 'Appointment slot found and booking initiated!',
340                    timeout: 10000
341                });
342            }
343            
344            // Reset attempts counter
345            await GM.setValue(CONFIG.BOOKING_ATTEMPTS_KEY, 0);
346            
347            // Stop monitoring after successful booking
348            await stopMonitoring();
349            
350            return true;
351            
352        } catch (error) {
353            console.error('Error in booking attempt:', error);
354            updateStatus(`Booking error: ${error.message}`, 'error');
355            return false;
356        }
357    }
358
359    // Start monitoring for available slots
360    async function startMonitoring() {
361        if (isMonitoring) {
362            console.log('Monitoring already active');
363            return;
364        }
365
366        isMonitoring = true;
367        await GM.setValue(CONFIG.AUTO_BOOK_ENABLED_KEY, true);
368        updateStatus('Monitoring started. Checking for slots...', 'info');
369        console.log('Started monitoring for available slots');
370
371        // Update UI
372        if (statusPanel) {
373            const toggleBtn = statusPanel.querySelector('.vfs-toggle-btn');
374            if (toggleBtn) {
375                toggleBtn.textContent = 'Stop Auto-Booking';
376                toggleBtn.classList.remove('vfs-btn-start');
377                toggleBtn.classList.add('vfs-btn-stop');
378            }
379        }
380
381        // Initial check
382        await attemptBooking();
383
384        // Set up interval for periodic checks
385        monitoringInterval = setInterval(async () => {
386            console.log('Periodic check for available slots...');
387            await GM.setValue(CONFIG.LAST_CHECK_KEY, Date.now());
388            await attemptBooking();
389        }, CONFIG.CHECK_INTERVAL);
390    }
391
392    // Stop monitoring
393    async function stopMonitoring() {
394        if (!isMonitoring) {
395            console.log('Monitoring not active');
396            return;
397        }
398
399        isMonitoring = false;
400        await GM.setValue(CONFIG.AUTO_BOOK_ENABLED_KEY, false);
401        
402        if (monitoringInterval) {
403            clearInterval(monitoringInterval);
404            monitoringInterval = null;
405        }
406
407        updateStatus('Monitoring stopped', 'info');
408        console.log('Stopped monitoring');
409
410        // Update UI
411        if (statusPanel) {
412            const toggleBtn = statusPanel.querySelector('.vfs-toggle-btn');
413            if (toggleBtn) {
414                toggleBtn.textContent = 'Start Auto-Booking';
415                toggleBtn.classList.remove('vfs-btn-stop');
416                toggleBtn.classList.add('vfs-btn-start');
417            }
418        }
419    }
420
421    // Create control panel UI
422    function createControlPanel() {
423        // Add styles
424        const styles = `
425            .vfs-auto-booker-panel {
426                position: fixed;
427                top: 20px;
428                right: 20px;
429                background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
430                border-radius: 12px;
431                padding: 20px;
432                box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
433                z-index: 999999;
434                min-width: 320px;
435                font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
436                color: white;
437            }
438            
439            .vfs-panel-header {
440                font-size: 18px;
441                font-weight: 700;
442                margin-bottom: 15px;
443                display: flex;
444                align-items: center;
445                justify-content: space-between;
446            }
447            
448            .vfs-panel-title {
449                display: flex;
450                align-items: center;
451                gap: 8px;
452            }
453            
454            .vfs-status-container {
455                background: rgba(255, 255, 255, 0.15);
456                border-radius: 8px;
457                padding: 12px;
458                margin-bottom: 15px;
459                backdrop-filter: blur(10px);
460            }
461            
462            .vfs-status-text {
463                font-size: 14px;
464                margin-bottom: 5px;
465                font-weight: 500;
466            }
467            
468            .vfs-status-info { color: #e0e7ff; }
469            .vfs-status-success { color: #86efac; }
470            .vfs-status-warning { color: #fde047; }
471            .vfs-status-error { color: #fca5a5; }
472            
473            .vfs-timestamp {
474                font-size: 11px;
475                opacity: 0.8;
476            }
477            
478            .vfs-controls {
479                display: flex;
480                gap: 10px;
481                flex-direction: column;
482            }
483            
484            .vfs-btn {
485                padding: 12px 20px;
486                border: none;
487                border-radius: 8px;
488                font-size: 14px;
489                font-weight: 600;
490                cursor: pointer;
491                transition: all 0.3s ease;
492                text-transform: uppercase;
493                letter-spacing: 0.5px;
494            }
495            
496            .vfs-btn:hover {
497                transform: translateY(-2px);
498                box-shadow: 0 5px 15px rgba(0, 0, 0, 0.3);
499            }
500            
501            .vfs-btn-start {
502                background: linear-gradient(135deg, #10b981 0%, #059669 100%);
503                color: white;
504            }
505            
506            .vfs-btn-stop {
507                background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%);
508                color: white;
509            }
510            
511            .vfs-btn-check {
512                background: rgba(255, 255, 255, 0.2);
513                color: white;
514                border: 1px solid rgba(255, 255, 255, 0.3);
515            }
516            
517            .vfs-close-btn {
518                background: transparent;
519                border: none;
520                color: white;
521                font-size: 24px;
522                cursor: pointer;
523                padding: 0;
524                width: 30px;
525                height: 30px;
526                display: flex;
527                align-items: center;
528                justify-content: center;
529                border-radius: 50%;
530                transition: background 0.3s ease;
531            }
532            
533            .vfs-close-btn:hover {
534                background: rgba(255, 255, 255, 0.2);
535            }
536            
537            .vfs-info {
538                font-size: 12px;
539                opacity: 0.9;
540                margin-top: 10px;
541                padding-top: 10px;
542                border-top: 1px solid rgba(255, 255, 255, 0.2);
543            }
544        `;
545        
546        TM_addStyle(styles);
547
548        // Create panel
549        const panel = document.createElement('div');
550        panel.className = 'vfs-auto-booker-panel';
551        panel.innerHTML = `
552            <div class="vfs-panel-header">
553                <div class="vfs-panel-title">
554                    <span>🤖</span>
555                    <span>Auto Appointment Booker</span>
556                </div>
557                <button class="vfs-close-btn" title="Close Panel">×</button>
558            </div>
559            
560            <div class="vfs-status-container">
561                <div class="vfs-status-text vfs-status-info">Ready to start monitoring</div>
562                <div class="vfs-timestamp">${new Date().toLocaleTimeString()}</div>
563            </div>
564            
565            <div class="vfs-controls">
566                <button class="vfs-btn vfs-btn-start vfs-toggle-btn">Start Auto-Booking</button>
567                <button class="vfs-btn vfs-btn-check vfs-check-now-btn">Check Now</button>
568            </div>
569            
570            <div class="vfs-info">
571                ℹ️ Checks every 30 seconds for available slots
572            </div>
573        `;
574
575        document.body.appendChild(panel);
576        statusPanel = panel;
577
578        // Add event listeners
579        const toggleBtn = panel.querySelector('.vfs-toggle-btn');
580        const checkNowBtn = panel.querySelector('.vfs-check-now-btn');
581        const closeBtn = panel.querySelector('.vfs-close-btn');
582
583        toggleBtn.addEventListener('click', async () => {
584            if (isMonitoring) {
585                await stopMonitoring();
586            } else {
587                await startMonitoring();
588            }
589        });
590
591        checkNowBtn.addEventListener('click', async () => {
592            updateStatus('Manual check initiated...', 'info');
593            await attemptBooking();
594        });
595
596        closeBtn.addEventListener('click', () => {
597            panel.style.display = 'none';
598        });
599
600        console.log('Control panel created');
601    }
602
603    // Initialize the extension
604    async function init() {
605        console.log('Initializing VFS Auto Appointment Booker...');
606        
607        // Wait for page to be ready
608        if (document.readyState === 'loading') {
609            document.addEventListener('DOMContentLoaded', init);
610            return;
611        }
612
613        // Check if we're on the review/payment page
614        if (window.location.href.includes('review-pay') || window.location.href.includes('review_pay') || window.location.href.includes('review')) {
615            console.log('Review/Payment page detected, auto-clicking...');
616            await handleReviewPage();
617            return;
618        }
619
620        // Check if we're on the booking page
621        if (!window.location.href.includes('book-appointment') && !window.location.href.includes('book_appointment') && !window.location.href.includes('book-an-appointment')) {
622            console.log('Not on booking page, extension inactive');
623            return;
624        }
625
626        // Wait for the calendar to load
627        try {
628            await waitForElement('.fc-daygrid-day', 10000);
629            console.log('Calendar detected, creating control panel...');
630            
631            // Create control panel
632            setTimeout(() => {
633                createControlPanel();
634                
635                // Check if auto-booking was previously enabled
636                GM.getValue(CONFIG.AUTO_BOOK_ENABLED_KEY, false).then(enabled => {
637                    if (enabled) {
638                        console.log('Auto-booking was previously enabled, resuming...');
639                        startMonitoring();
640                    }
641                });
642            }, 1000);
643            
644        } catch (error) {
645            console.error('Failed to initialize:', error);
646        }
647    }
648
649    // Start the extension
650    init();
651
652})();
VFS Global Auto Appointment Booker | Robomonkey