Giveaway Auto Enter

Automatically enter giveaways on extrasforamazon.com

// ==UserScript==
// @name         Giveaway Auto Enter
// @namespace    http://tampermonkey.net/
// @version      1.1
// @description  Automatically enter giveaways on extrasforamazon.com
// @author       sacrosaunt
// @match        https://extrasforamazon.com/app/giveaways*
// @grant        none
// ==/UserScript==

(function() {
    'use strict';

    /**
     * Creates and injects the auto-enter button into the navigation bar
     * This function handles the UI setup for the giveaway automation tool
     */
    function createButton() {
        // Check if button already exists to avoid duplicates
        let existingButton = document.getElementById('giveaway-auto-enter-btn');
        if (existingButton) {
            return; // Button already exists, don't recreate
        }

        // Find the navigation container on the right side of the page
        // Try multiple selectors for compatibility across different page layouts
        const navRightContainer = document.querySelector('.nav_top_inner-wrap-right') || 
                                  document.querySelector('.nav_top-content > div:last-child');
        
        // If navigation container found, create button immediately
        if (navRightContainer) {
            createButtonElement(navRightContainer);
            return;
        }

        // If navigation container not found, keep checking until it appears
        // This ensures the button is added as soon as the container element is available
        const checkInterval = setInterval(() => {
            const container = document.querySelector('.nav_top_inner-wrap-right') || 
                             document.querySelector('.nav_top-content > div:last-child');
            
            if (container) {
                clearInterval(checkInterval); // Stop checking
                createButtonElement(container);
            }
        }, 100); // Check every 100ms for better responsiveness
        
        // Add a timeout to prevent infinite checking (10 seconds max)
        setTimeout(() => {
            clearInterval(checkInterval);
            // If still no container found after timeout, create fallback button
            if (!document.getElementById('giveaway-auto-enter-btn')) {
                const fallbackContainer = document.createElement('div');
                fallbackContainer.style.cssText = `
                    position: fixed;
                    top: 20px;
                    right: 20px;
                    z-index: 10000;
                `;
                document.body.appendChild(fallbackContainer);
                createButtonElement(fallbackContainer);
            }
        }, 10000);
    }

    /**
     * Creates the actual button element and adds it to the specified container
     * @param {Element} container - The container to add the button to
     */
    function createButtonElement(container) {
        // Create the auto-enter button element
        const button = document.createElement('button');
        button.id = 'giveaway-auto-enter-btn';
        button.className = 'themeButton primaryLinkButton';
        button.title = 'Enter All Giveaways';
        button.disabled = true; // Initially disabled until DOM is ready
        
        // Apply custom styling to match the site's design
        button.style.cssText = `
            color: rgb(34, 51, 68);
            border: none;
            background: transparent;
            padding: 12px 16px;
            border-radius: 4px;
            cursor: not-allowed;
            font-size: 18px;
            margin-right: 10px;
            display: inline-flex;
            align-items: center;
            justify-content: center;
            opacity: 0.6;
        `;
        
        // Add gift icon to the button
        button.innerHTML = '<i class="fal fa-gift" style="font-size: 2.8rem;"></i>';
        
        // Add hover effects for better user experience (only when enabled)
        button.addEventListener('mouseenter', () => {
            if (!button.disabled) {
                button.style.opacity = '0.8';
            }
        });
        
        button.addEventListener('mouseleave', () => {
            if (!button.disabled) {
                button.style.opacity = '1';
            }
        });
        
        // Insert button at the beginning of the container
        container.insertBefore(button, container.firstChild);
        
        // Check if button should be enabled immediately
        checkAndEnableButton();
    }

    /**
     * Checks if the button should be enabled and enables it if conditions are met
     * This ensures buttons are enabled regardless of when they're created
     */
    function checkAndEnableButton() {
        const button = document.getElementById('giveaway-auto-enter-btn');
        if (button && document.readyState !== 'loading') {
            enableGiveawayAutomation();
        }
    }

    /**
     * Main function that automates entering all eligible giveaways
     * This is the core functionality that processes giveaways one by one
     */
    function autoEnterGiveaways() {
        /**
         * Creates a toast notification system for user feedback
         * Shows success, info, and error messages during the automation process
         * @returns {Object} Toast system with show method
         */
        function createToastSystem() {
            // Check if toast container already exists
            let toastContainer = document.getElementById('giveaway-extension-toasts');
            if (!toastContainer) {
                // Create toast container for notifications
                toastContainer = document.createElement('div');
                toastContainer.id = 'giveaway-extension-toasts';
                toastContainer.style.cssText = `
                    position: fixed;
                    bottom: 20px;
                    right: 20px;
                    z-index: 10000;
                    display: flex;
                    flex-direction: column;
                    align-items: flex-end;
                    gap: 10px;
                `;
                document.body.appendChild(toastContainer);
            }
            
            // Add CSS styles for toast notifications
            const style = document.createElement('style');
            style.textContent = `
                .giveaway-toast {
                    background-color: rgba(0, 0, 0, 0.8);
                    color: white;
                    padding: 12px 20px;
                    border-radius: 4px;
                    box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
                    font-family: Arial, sans-serif;
                    font-size: 14px;
                    max-width: 300px;
                    opacity: 0;
                    transform: translateY(20px);
                    transition: opacity 0.3s, transform 0.3s;
                }
                .giveaway-toast.show {
                    opacity: 1;
                    transform: translateY(0);
                }
                .giveaway-toast.success {
                    border-left: 4px solid #4CAF50;
                }
                .giveaway-toast.info {
                    border-left: 4px solid #2196F3;
                }
                .giveaway-toast.error {
                    border-left: 4px solid #F44336;
                }
            `;
            document.head.appendChild(style);
            
            // Return toast system with show method
            return {
                show: function(message, type = 'info', duration = 3000) {
                    const toast = document.createElement('div');
                    toast.className = `giveaway-toast ${type}`;
                    toast.textContent = message;
                    
                    toastContainer.appendChild(toast);
                    
                    // Animate toast in
                    setTimeout(() => toast.classList.add('show'), 10);
                    
                    // Auto-remove toast after duration
                    setTimeout(() => {
                        toast.classList.remove('show');
                        setTimeout(() => toastContainer.removeChild(toast), 300);
                    }, duration);
                    
                }
            };
        }
        
        /**
         * Waits for an element to appear in the DOM
         * Uses MutationObserver for efficient element detection
         * @param {string} selector - CSS selector for the target element
         * @param {number} timeout - Maximum time to wait in milliseconds
         * @returns {Promise<Element>} Promise that resolves with the found element
         */
        function waitForElement(selector, timeout = 10000) {
            return new Promise((resolve, reject) => {
                // Check if element already exists
                const element = document.querySelector(selector);
                if (element) {
                    return resolve(element);
                }
                
                // Set up observer to watch for DOM changes
                const observer = new MutationObserver((mutations, obs) => {
                    const element = document.querySelector(selector);
                    if (element) {
                        obs.disconnect();
                        resolve(element);
                    }
                });
                
                // Start observing DOM changes
                observer.observe(document.body, {
                    childList: true,
                    subtree: true,
                    attributes: true
                });
                
                // Set timeout to prevent infinite waiting
                setTimeout(() => {
                    observer.disconnect();
                    reject(new Error(`Timeout waiting for ${selector}`));
                }, timeout);
            });
        }
        
        /**
         * Waits for the page to fully load
         * @returns {Promise} Promise that resolves when page is loaded
         */
        function waitForPageLoad() {
            return new Promise(resolve => {
                if (document.readyState === 'complete') {
                    return resolve();
                }
                
                window.addEventListener('load', resolve, { once: true });
            });
        }
        
        /**
         * Waits for the DOM to become stable (no more rapid changes)
         * This prevents issues with elements being modified while we're trying to interact with them
         * @param {number} timeoutMs - Time to wait for stability in milliseconds
         * @returns {Promise} Promise that resolves when DOM is stable
         */
        function waitForDomStable(timeoutMs = 100) {
            return new Promise(resolve => {
                let timeout;
                
                // Watch for DOM mutations and reset timer
                const observer = new MutationObserver(() => {
                    clearTimeout(timeout);
                    timeout = setTimeout(() => {
                        observer.disconnect();
                        resolve();
                    }, timeoutMs);
                });
                
                // Start observing DOM changes
                observer.observe(document.body, {
                    childList: true,
                    subtree: true,
                    attributes: true
                });
                
                // Fallback timeout if no mutations occur
                timeout = setTimeout(() => {
                    observer.disconnect();
                    resolve();
                }, timeoutMs);
            });
        }
        
        /**
         * Main processing loop that enters giveaways one by one
         * Handles the entire workflow from finding eligible giveaways to completing entries
         */
        async function processGiveaways() {
            const toast = createToastSystem();

            // Show initial status message
            toast.show('Starting...', 'info', 2000);
            
            let continueProcessing = true;
            
            // Main processing loop
            while (continueProcessing) {
                // Wait for page to be ready before processing
                await waitForPageLoad();
                await waitForDomStable();
                
                // Find all eligible giveaways (those that can be entered)
                const eligibleGiveaways = document.querySelectorAll('button.giveaway-tile.eligible');
                
                // If no eligible giveaways found, we're done
                if (eligibleGiveaways.length === 0) {
                    toast.show('All giveaways have been entered!', 'success', 5000);
                    continueProcessing = false;
                    break;
                }
                
                try {
                    // Process the first eligible giveaway
                    const giveaway = eligibleGiveaways[0];
                    
                    // Extract giveaway name for user feedback
                    const nameElement = giveaway.querySelector('.giveaway-tile_name');
                    const giveawayName = nameElement?.textContent?.trim() || "giveaway";
                    
                    // Click on the giveaway to open it
                    giveaway.click();
                    
                    // Wait for and click the enter button
                    const enterButton = await waitForElement('div.giveaway-enter-form button[type="submit"]');
                    
                    // Small delay to ensure form is ready
                    await new Promise(r => setTimeout(r, 100));
                    
                    enterButton.click();
                    toast.show(`Successfully entered ${giveawayName}!`, 'success');
                    
                    // Wait for DOM to stabilize after entry
                    await waitForDomStable();
                    
                    // Find and click back button to return to giveaway list
                    const backButton = await waitForElement('div.giveaway-overlay_header button');
                    backButton.click();
                    
                    // Wait for page to stabilize before continuing
                    await waitForDomStable();
                    
                } catch (error) {
                    // Handle errors during giveaway processing
                    toast.show(`Error: ${error.message}`, 'error', 5000);
                    
                    // Try to recover by clicking back button if possible
                    try {
                        const backButton = document.querySelector('div.giveaway-overlay_header button');
                        if (backButton) backButton.click();
                        await waitForDomStable();
                    } catch (e) {
                        toast.show("Couldn't find back button, trying to continue...", 'info');
                    }
                    
                    // Implement retry logic (one retry)
                    if (continueProcessing === true) {
                        continueProcessing = "last_try";
                        toast.show("Encountered an error, will try once more", 'info');
                    } else {
                        continueProcessing = false;
                        toast.show("Too many errors, stopping process", 'error', 5000);
                    }
                }
            }
        }
        
        // Start the giveaway processing
        processGiveaways();
    }

    // Initialize the button immediately for better user experience
    // The button will be visible early, but giveaway automation will wait for DOM to be ready
    createButton();

    // Wait for DOM content to be fully loaded before allowing giveaway automation to start
    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', () => {
            // Enable the button functionality once DOM is ready
            enableGiveawayAutomation();
        });
    } else {
        // DOM is already loaded, enable immediately
        enableGiveawayAutomation();
    }

    /**
     * Enables the giveaway automation functionality once the DOM is ready
     * This ensures all necessary elements are available before processing begins
     */
    function enableGiveawayAutomation() {
        const button = document.getElementById('giveaway-auto-enter-btn');
        if (button) {
            // Remove any existing listeners and add the main one
            button.removeEventListener('click', autoEnterGiveaways);
            button.addEventListener('click', autoEnterGiveaways);
            
            // Enable the button (remove any disabled state)
            button.disabled = false;
            button.style.opacity = '1';
            button.style.cursor = 'pointer';
        }
    }

})();