Bungie Code Redeemer

Automatically redeem multiple shop codes one by one

Vous devrez installer une extension telle que Tampermonkey, Greasemonkey ou Violentmonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey ou Violentmonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey ou Userscripts pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey pour installer ce script.

Vous devrez installer une extension de gestionnaire de script utilisateur pour installer ce script.

(J'ai déjà un gestionnaire de scripts utilisateur, laissez-moi l'installer !)

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

(J'ai déjà un gestionnaire de style utilisateur, laissez-moi l'installer!)

// ==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;
    }

})();