Greasy Fork is available in English.

Fanatical Keys Backup

Displays a text area with game titles and keys so you can copy them out easily.

// ==UserScript==
// @name         Fanatical Keys Backup
// @namespace    Lex@GreasyFork
// @version      0.2.5
// @description  Displays a text area with game titles and keys so you can copy them out easily.
// @author       Lex
// @match        https://www.fanatical.com/en/orders*
// @grant        none
// ==/UserScript==

(function() {
    'use strict';

    // Formats games array to a string to be displayed
    // Games is an array [ [title, key], ... ]
    function formatGames(games) {
        // Ignore games which do not have keys revealed
        games = games.filter(e => e[1]);
        // Format the output as tab-separated
        games = games.map(e => e[0]+"\t"+e[1]);
        return games.join("\n");
    }

    function getGames(bundle) {
        let is = bundle.querySelectorAll(".new-order-item");
        return Array.prototype.map.call(is, i => {
            const gameTitleElement = i.getElementsByClassName("game-name");
            const gameTitle = gameTitleElement.length > 0 ? gameTitleElement[0].textContent.trim() : "";
            const keyElement = i.querySelector("[aria-label='reveal-key']");
            const gameKey = keyElement ? keyElement.value : "";
            return [gameTitle, gameKey];
        });
    }

    function revealAllKeys(bundle) {
        const revealButtons = bundle.querySelectorAll(".key-container button.btn-block");
        revealButtons.forEach(b => { b.click() });
        this.style.display = "none";
    }

    function createRevealButton(bundle) {
        let btn = document.createElement("button");
        btn.type = "button"; // no default behavior
        btn.innerText = "Reveal this bundle's keys";
        btn.onclick = revealAllKeys.bind(btn, bundle);
        return btn;
    }

    // Adds a textarea to the bottom of the games listing with all the titles and keys
    function handleBundle(bundles) {
        const bundle = bundles.at(-1);
        const bundleName = bundles[0].querySelector(".bundle-name")?.textContent.trim() ?? "No Title";
        const games = bundles.flatMap(bundle => getGames(bundle));
        const keyCount = games.filter(e => e[1]).length;
        const gameStr = formatGames(games);

        let notify = bundle.querySelector(".ktt-notify");
        if (!notify) {
            notify = document.createElement("div");
            notify.className = "ktt-notify";
            bundle.append(notify);
            if (games.length != keyCount) {
                const btn = createRevealButton(bundle);
                notify.before(btn);
            }
        }

        const color = games.length === keyCount ? "" : "red";
        let newInner = `Dumping keys for ${bundleName}: Found ${games.length} items and <span style="background-color:${color}">${keyCount} keys</span>.`;
        if (games.length != keyCount) {
            newInner += " Are some keys not revealed?";
        }
        if (notify.innerHTML != newInner) {
            notify.innerHTML = newInner;
        }

        let area = bundle.querySelector(".ktt");
        if (!area) {
            area = document.createElement("textarea");
            area.className = "ktt";
            area.style.width = "100%";
            area.setAttribute('readonly', true);
            bundle.append(area);
        }
        if (area.value != gameStr) {
            area.value = gameStr;
            // Adjust the height so all the contents are visible
            area.style.height = "";
            area.style.height = area.scrollHeight + 20 + "px";
        }
    }

    var loopCount = 0;
    function handleOrderPage() {
        // There can be more than one bundle in an order
        let bundleItemsContainers = [...new Set([...document.querySelectorAll('.new-order-item')].map(item => item.closest('section')))];

        // combine bundle groups into their overall bundle header
        // this is needed because mystery bundles span multiple bundle containers for some reason
        let currentBundleGroup = [];
        const bundleGroups = [currentBundleGroup];
        for (const bundleItemsContainer of bundleItemsContainers) {
            if (bundleItemsContainer.firstChild.className === "bundle-name-container" && currentBundleGroup.length > 0) {
                // Indicates a new bundle group is starting
                currentBundleGroup = [];
                bundleGroups.push(currentBundleGroup);
            }
            currentBundleGroup.push(bundleItemsContainer);
        }

        if (bundleGroups.length > 0 && bundleGroups[0].length > 0) {
            //console.log(`Found ${bundleGroups.length} bundle(s)`);
            bundleGroups.forEach(handleBundle);
            if (loopCount++ < 100) {
              setTimeout(handleOrderPage, 500);
            }
        } else {
            if (loopCount++ < 100) {
                setTimeout(handleOrderPage, 100);
            }
        }
    }

    handleOrderPage();
})();