Bungie Code Redeemer

Automatically redeem multiple shop codes one by one

За да инсталирате този скрипт, трябва да имате инсталирано разширение като Tampermonkey, Greasemonkey или Violentmonkey.

За да инсталирате този скрипт, трябва да инсталирате разширение, като например Tampermonkey .

За да инсталирате този скрипт, трябва да имате инсталирано разширение като Tampermonkey или Violentmonkey.

За да инсталирате този скрипт, трябва да имате инсталирано разширение като Tampermonkey или Userscripts.

За да инсталирате скрипта, трябва да инсталирате разширение като Tampermonkey.

За да инсталирате този скрипт, трябва да имате инсталиран скриптов мениджър.

(Вече имам скриптов мениджър, искам да го инсталирам!)

За да инсталирате този стил, трябва да инсталирате разширение като Stylus.

За да инсталирате този стил, трябва да инсталирате разширение като Stylus.

За да инсталирате този стил, трябва да инсталирате разширение като Stylus.

За да инсталирате този стил, трябва да имате инсталиран мениджър на потребителски стилове.

За да инсталирате този стил, трябва да имате инсталиран мениджър на потребителски стилове.

За да инсталирате този стил, трябва да имате инсталиран мениджър на потребителски стилове.

(Вече имам инсталиран мениджър на стиловете, искам да го инсталирам!)

// ==UserScript==
// @name         Bungie Code Redeemer
// @namespace    https://github.com/LasmGratel/
// @version      1.1
// @description  Automatically redeem multiple shop codes one by one
// @author       Lasm Gratel
// @match        https://www.bungie.net/7/en/codes/redeem
// @grant        none
// @license      MIT
// ==/UserScript==

(function () {
    'use strict';

    // ─── CONFIG ────────────────────────────────────────────────────────────────
    // Delay (ms) between each step – increase if the site is slow
    const STEP_DELAY    = 500;   // after typing / before clicking
    const BETWEEN_DELAY = 1200;  // after "Redeem Another Code", before next code
    // ───────────────────────────────────────────────────────────────────────────

    /* ── Helper: trigger React's synthetic onChange on an <input> ── */
    function setReactInputValue(input, value) {
        const nativeSetter = Object.getOwnPropertyDescriptor(
            window.HTMLInputElement.prototype, 'value'
        ).set;
        nativeSetter.call(input, value);
        input.dispatchEvent(new Event('input',  { bubbles: true }));
        input.dispatchEvent(new Event('change', { bubbles: true }));
    }

    /* ── Helper: wait until a DOM element matching selector appears ── */
    function waitForElement(selector, root = document.body, timeout = 12000) {
        return new Promise((resolve, reject) => {
            const found = root.querySelector(selector);
            if (found) return resolve(found);

            const obs = new MutationObserver(() => {
                const el = root.querySelector(selector);
                if (el) { obs.disconnect(); resolve(el); }
            });
            obs.observe(root, { childList: true, subtree: true });
            setTimeout(() => { obs.disconnect(); reject(new Error('Timeout: ' + selector)); }, timeout);
        });
    }

    /* ── Helper: wait until a <button> whose trimmed text starts with `text` ── */
    function waitForButton(text, timeout = 12000) {
        function find() {
            return Array.from(document.querySelectorAll('button'))
                        .find(b => b.textContent.trim().startsWith(text));
        }
        return new Promise((resolve, reject) => {
            const el = find();
            if (el) return resolve(el);

            const obs = new MutationObserver(() => {
                const el = find();
                if (el) { obs.disconnect(); resolve(el); }
            });
            obs.observe(document.body, { childList: true, subtree: true });
            setTimeout(() => { obs.disconnect(); reject(new Error('Timeout waiting for button: ' + text)); }, timeout);
        });
    }

    /**
     * After clicking "Redeem Code", race between:
     *   { type: 'success' }  – "Redeem Another Code" button appeared
     *   { type: 'error', message, okayBtn }  – error modal appeared
     */
    function waitForSuccessOrError(timeout = 15000) {
        return new Promise((resolve, reject) => {
            function check() {
                // Success path
                const another = Array.from(document.querySelectorAll('button'))
                    .find(b => b.textContent.trim().startsWith('Redeem Another Code'));
                if (another) return { type: 'success', btn: another };

                // Error modal path – button with labeloverride="OKAY"
                const okay = document.querySelector('button[labeloverride="OKAY"]');
                if (okay) {
                    // Grab the human-readable error description if available
                    const descEl = document.querySelector('[class*="ConfirmationModal_description"] [class*="errorContent"], [class*="ConfirmationModal_description"]');
                    const message = descEl ? descEl.textContent.trim() : 'Unknown error';
                    return { type: 'error', message, okayBtn: okay };
                }
                return null;
            }

            const immediate = check();
            if (immediate) return resolve(immediate);

            const obs = new MutationObserver(() => {
                const result = check();
                if (result) { obs.disconnect(); resolve(result); }
            });
            obs.observe(document.body, { childList: true, subtree: true });
            setTimeout(() => { obs.disconnect(); reject(new Error('Timeout waiting for redemption result')); }, timeout);
        });
    }

    /**
     * Normalize a raw string to XXX-XXX-XXX format.
     * Strips all non-alphanumeric chars, validates exactly 9 chars remain.
     * Returns the formatted code or null if invalid.
     */
    function normalizeCode(raw) {
        const stripped = raw.replace(/[^A-Za-z0-9]/g, '').toUpperCase();
        if (!/^[A-Z0-9]{9}$/.test(stripped)) return null;
        return `${stripped.slice(0, 3)}-${stripped.slice(3, 6)}-${stripped.slice(6, 9)}`;
    }

    const sleep = ms => new Promise(r => setTimeout(r, ms));

    /* ══════════════════════════════════════════════════════════════════
       BUILD THE FLOATING PANEL
    ══════════════════════════════════════════════════════════════════ */
    const panel = Object.assign(document.createElement('div'), {
        id: 'cr-panel',
    });
    Object.assign(panel.style, {
        position:   'fixed',
        top:        '20px',
        right:      '20px',
        background: '#16213e',
        color:      '#eee',
        padding:    '14px 16px',
        borderRadius: '10px',
        zIndex:     '2147483647',
        fontFamily: 'monospace, sans-serif',
        fontSize:   '13px',
        minWidth:   '280px',
        boxShadow:  '0 4px 20px rgba(0,0,0,0.6)',
        userSelect: 'none',
    });

    panel.innerHTML = `
        <div style="font-size:15px;font-weight:bold;margin-bottom:10px;color:#e94560;">
            🎁 Code Redeemer
        </div>
        <textarea id="cr-codes" rows="6" placeholder="Paste one code per line…"
            style="width:100%;box-sizing:border-box;background:#0f3460;color:#eee;
                   border:1px solid #e94560;border-radius:6px;padding:6px;
                   font-family:monospace;font-size:13px;resize:vertical;"></textarea>
        <div id="cr-status"  style="margin:8px 0;color:#a8dadc;min-height:18px;">Ready – paste codes above</div>
        <div id="cr-progress" style="font-size:12px;color:#aaa;margin-bottom:8px;"></div>
        <div id="cr-log-wrap" style="display:none;margin-bottom:8px;">
            <div style="font-size:12px;color:#e94560;margin-bottom:4px;">⚠ Error log:</div>
            <div id="cr-log"
                style="max-height:90px;overflow-y:auto;background:#0a0a1a;border:1px solid #333;
                       border-radius:4px;padding:4px 6px;font-size:11px;color:#f0a500;
                       word-break:break-all;"></div>
        </div>
        <div style="display:flex;gap:8px;">
            <button id="cr-start"
                style="flex:1;background:#e94560;color:#fff;border:none;padding:7px 10px;
                       border-radius:6px;cursor:pointer;font-size:13px;">
                ▶ Start
            </button>
            <button id="cr-close"
                style="background:#333;color:#ccc;border:none;padding:7px 10px;
                       border-radius:6px;cursor:pointer;font-size:13px;">
                ✕
            </button>
        </div>
    `;

    document.body.appendChild(panel);

    /* Make panel draggable */
    let dragging = false, ox = 0, oy = 0;
    panel.addEventListener('mousedown', e => {
        if (['TEXTAREA','BUTTON'].includes(e.target.tagName)) return;
        dragging = true; ox = e.clientX - panel.offsetLeft; oy = e.clientY - panel.offsetTop;
    });
    document.addEventListener('mousemove', e => {
        if (!dragging) return;
        panel.style.left = (e.clientX - ox) + 'px';
        panel.style.top  = (e.clientY - oy) + 'px';
        panel.style.right = 'auto';
    });
    document.addEventListener('mouseup', () => { dragging = false; });

    document.getElementById('cr-close').onclick = () => panel.remove();

    /* ══════════════════════════════════════════════════════════════════
       MAIN REDEMPTION LOOP
    ══════════════════════════════════════════════════════════════════ */
    document.getElementById('cr-start').onclick = async function () {
        const textarea = document.getElementById('cr-codes');
        const codes = textarea.value
            .split('\n')
            .map(s => s.trim())
            .filter(Boolean);

        if (!codes.length) {
            setStatus('⚠ No codes entered!');
            return;
        }

        this.disabled = true;
        this.style.opacity = '0.5';
        textarea.disabled = true;

        // ── Normalize & validate all codes up-front ──────────────────────────
        const validated = [];
        for (const raw of codes) {
            const normalized = normalizeCode(raw);
            if (normalized) {
                validated.push(normalized);
            } else {
                logError(`INVALID (skipped): "${raw}" — must contain exactly 9 alphanumeric chars`);
            }
        }

        if (!validated.length) {
            setStatus('⚠ No valid codes found!');
            this.disabled = false;
            this.style.opacity = '1';
            textarea.disabled = false;
            return;
        }

        const total = validated.length;

        for (let i = 0; i < total; i++) {
            const code = validated[i];
            setProgress(i, total, code);

            try {
                /* 1. Wait for the input box to be present */
                setStatus('⏳ Waiting for input field…');
                const inputBox = await waitForElement('[class*="CodesRedemptionForm_inputBox"]');
                const input    = inputBox.querySelector('input') || inputBox;

                await sleep(STEP_DELAY);

                /* 2. Type the code (React-safe) */
                input.focus();
                setReactInputValue(input, code);
                setStatus(`✏ Entering code: ${code}`);

                await sleep(STEP_DELAY);

                /* 3. Click "Redeem Code" */
                const redeemBtn = await waitForButton('Redeem Code');
                redeemBtn.click();
                setStatus(`🚀 Redeeming: ${code}…`);

                /* 4. Race: success modal OR error modal */
                setStatus('⏳ Waiting for result…');
                const result = await waitForSuccessOrError();

                if (result.type === 'error') {
                    // ── Error path ──────────────────────────────────────────
                    logError(`${code}: ${result.message}`);
                    setStatus(`⚠ Error on ${code} — see log`);

                    // Dismiss the modal
                    result.okayBtn.click();
                    await sleep(STEP_DELAY);

                    // Clear the input so the form is clean for the next code
                    const inputBox2 = document.querySelector('[class*="CodesRedemptionForm_inputBox"]');
                    if (inputBox2) {
                        const inp2 = inputBox2.querySelector('input') || inputBox2;
                        setReactInputValue(inp2, '');
                    }

                    await sleep(BETWEEN_DELAY);
                } else {
                    // ── Success path ─────────────────────────────────────────
                    setStatus(`✅ Code ${i + 1}/${total} redeemed: ${code}`);
                    await sleep(STEP_DELAY);

                    /* 5. Click "Redeem Another Code" to go back */
                    result.btn.click();
                    await sleep(BETWEEN_DELAY);
                }

            } catch (err) {
                setStatus(`❌ Error on "${code}": ${err.message}`);
                logError(`${code}: ${err.message}`);
                await sleep(3000);
            }
        }

        setProgress(total, total, '');
        setStatus('🎉 All codes processed!');
        this.disabled = false;
        this.style.opacity = '1';
        textarea.disabled = false;
    };

    function setStatus(msg) {
        const el = document.getElementById('cr-status');
        if (el) el.textContent = msg;
    }

    function setProgress(done, total, current) {
        const el = document.getElementById('cr-progress');
        if (!el) return;
        el.textContent = total
            ? `Progress: ${done}/${total}${current ? '  |  Current: ' + current : ''}`
            : '';
    }

    function logError(msg) {
        const wrap = document.getElementById('cr-log-wrap');
        const log  = document.getElementById('cr-log');
        if (!wrap || !log) return;
        wrap.style.display = 'block';
        const line = document.createElement('div');
        line.style.borderBottom = '1px solid #222';
        line.style.paddingBottom = '2px';
        line.style.marginBottom  = '2px';
        const ts = new Date().toLocaleTimeString();
        line.textContent = `[${ts}] ${msg}`;
        log.appendChild(line);
        log.scrollTop = log.scrollHeight;
    }

})();