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})();