← Back to docs

vBulletin invite-complete hook utan onboarding-sidan

Language: SV | EN | SV

vBulletin invite-complete hook utan onboarding-sidan

Det här är den korta, praktiska guiden för att låta ett vBulletin-hookat frontend-script prata direkt med Tools efter att forumregistreringen ser klar ut.

Målet är att slippa skicka användaren genom hela den publika onboarding-sidan när forumet redan känner till användaren och invite-koden.

Endpoint

Publik endpoint:

  • POST /api/vbulletin/onboarding/invite/complete

Exempel:

  • produktion: https://tools.tornevall.net/api/vbulletin/onboarding/invite/complete
  • dev/test: https://tools.tornevall.com/api/vbulletin/onboarding/invite/complete

Viktigt om auth

Den här endpointen använder inga bearer tokens och ska inte skyddas med något separat frontend-token.

Säkerhetsmodellen här bygger i stället på:

  • en riktig invite-kod
  • valfria forumidentitetshints
  • server-side-kontroll i Tools av att rätt forumkonto verkligen matchar inviten
  • extra profilfältskontroll när onboardingflödet använder ett invitefält som field66

Vad endpointen accepterar

Request-body:

{
  "invite_code": "invite-code-here",
  "forum_user_id": 123,
  "forum_username": "snowrider",
  "forum_email": "user@example.com"
}

Regler:

  • invite_code är obligatorisk
  • forum_user_id är den starkaste extra identitetshinten när den finns
  • forum_username är bra som fallback
  • forum_email är bara en hint och räknas inte ensam som bevis

URL i invitefältet? Tools strippar den nu

Om invitefältet i forumet råkar innehålla en hel URL i stället för bara själva koden, så normaliserar Tools nu detta server-side.

Det betyder att följande fortfarande fungerar:

https://tools.tornevall.net/vbulletin/onboarding/snowboarding/ABC123

Tools försöker då plocka ut själva invitekoden (ABC123) innan lookupen körs.

Det är ändå klokt att även rensa värdet client-side innan det skickas.

Rekommenderad hook-konfiguration

För den aktuella hooken i vBulletin:

  • Hook location: header_head
  • Template name: snowball_header_script

Hook arguments:

userinfo=userinfo
bbuserinfo=bbuserinfo

Det gör att scriptet kan läsa vanliga användarfält från både userinfo och bbuserinfo i templaten.

Anpassad bootstrap för användardata

Din nuvarande bootstrapdel bör anpassas så att den klarar både tomma värden och URL-baserade invitefält.

Exempel:

<script>
(function () {
    function firstNonEmpty() {
        for (var i = 0; i < arguments.length; i += 1) {
            var value = String(arguments[i] || '').trim();
            if (value !== '') {
                return value;
            }
        }
        return '';
    }

    window.TornevallTools = window.TornevallTools || {};
    window.TornevallTools.vbulletin = window.TornevallTools.vbulletin || {};
    window.TornevallTools.vbulletin.user = window.TornevallTools.vbulletin.user || {};

    window.TornevallTools.vbulletin.user.userid = firstNonEmpty(
        "{vb:raw bbuserinfo.userid}",
        "{vb:raw userinfo.userid}"
    );

    window.TornevallTools.vbulletin.user.username = firstNonEmpty(
        "{vb:raw bbuserinfo.username}",
        "{vb:raw userinfo.username}"
    );

    window.TornevallTools.vbulletin.user.invitationCodeRaw = firstNonEmpty(
        "{vb:raw bbuserinfo.field66}",
        "{vb:raw userinfo.field66}"
    );

    window.TornevallTools.vbulletin.user.invitationFieldId = 66;
})();
</script>

Rekommenderad normalisering av invitekoden i frontend

Använd en liten helper innan API-anropet görs.

<script>
(function () {
    function normalizeInviteCode(rawValue) {
        var value = String(rawValue || '').trim();

        if (!value) {
            return '';
        }

        try {
            var parsed = new URL(value, window.location.origin);
            var fromQuery = parsed.searchParams.get('invite_code')
                || parsed.searchParams.get('inviteKey')
                || parsed.searchParams.get('invite_key');

            if (fromQuery) {
                return String(fromQuery).trim();
            }

            var segments = parsed.pathname.split('/').filter(Boolean);
            var onboardingIndex = segments.indexOf('onboarding');
            if (onboardingIndex !== -1 && segments.length > onboardingIndex + 2) {
                return String(segments[onboardingIndex + 2] || '').trim();
            }
        } catch (error) {
            // Inte en URL, använd råvärdet som det är.
        }

        return value;
    }

    window.TornevallTools = window.TornevallTools || {};
    window.TornevallTools.vbulletin = window.TornevallTools.vbulletin || {};
    window.TornevallTools.vbulletin.normalizeInviteCode = normalizeInviteCode;
})();
</script>

Exempel: skicka completion direkt till Tools

Det här är den praktiska kärnan.

<script>
(function () {
    var toolsApiBase = 'https://tools.tornevall.net';

    function normalizeInviteCode(rawValue) {
        if (
            window.TornevallTools
            && window.TornevallTools.vbulletin
            && typeof window.TornevallTools.vbulletin.normalizeInviteCode === 'function'
        ) {
            return window.TornevallTools.vbulletin.normalizeInviteCode(rawValue);
        }

        return String(rawValue || '').trim();
    }

    function getForumUser() {
        return (
            window.TornevallTools
            && window.TornevallTools.vbulletin
            && window.TornevallTools.vbulletin.user
        ) || {};
    }

    function completeInviteWithTools() {
        var user = getForumUser();
        var inviteCode = normalizeInviteCode(user.invitationCodeRaw || user.invitationCode || '');
        var forumUserId = parseInt(user.userid, 10) || 0;
        var forumUsername = String(user.username || '').trim();

        if (!inviteCode || !forumUserId) {
            return Promise.resolve(null);
        }

        return fetch(toolsApiBase + '/api/vbulletin/onboarding/invite/complete', {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json',
                'Accept': 'application/json'
            },
            body: JSON.stringify({
                invite_code: inviteCode,
                forum_user_id: forumUserId,
                forum_username: forumUsername
            })
        }).then(function (response) {
            return response.json().then(function (data) {
                return {
                    ok: response.ok,
                    status: response.status,
                    data: data
                };
            });
        });
    }

    window.TornevallTools = window.TornevallTools || {};
    window.TornevallTools.vbulletin = window.TornevallTools.vbulletin || {};
    window.TornevallTools.vbulletin.completeInviteWithTools = completeInviteWithTools;
})();
</script>

Exempel: snygg popup vid lyckad insläppning

När API:t svarar att åtkomsten faktiskt är klar (access_granted=true) kan du visa en tydlig popup.

<script>
(function () {
    function showInviteSuccessPopup(message, continueUrl) {
        var existing = document.getElementById('tools-invite-success-popup');
        if (existing) {
            existing.remove();
        }

        var overlay = document.createElement('div');
        overlay.id = 'tools-invite-success-popup';
        overlay.innerHTML = ''
            + '<div class="tools-invite-popup-backdrop"></div>'
            + '<div class="tools-invite-popup-card" role="dialog" aria-live="polite" aria-modal="true">'
            + '  <div class="tools-invite-popup-icon">✓</div>'
            + '  <h2>Grattis!</h2>'
            + '  <p>' + (message || 'Du är nu insläppt i forumet.') + '</p>'
            + '  <div class="tools-invite-popup-actions">'
            + (continueUrl
            ? '    <a class="tools-invite-popup-button" href="' + continueUrl + '">Fortsätt till forumet</a>'
            : '')
            + '    <button type="button" class="tools-invite-popup-close">Stäng</button>'
            + '  </div>'
            + '</div>';

        var style = document.createElement('style');
        style.textContent = ''
            + '#tools-invite-success-popup { position: fixed; inset: 0; z-index: 2147483647; }'
            + '.tools-invite-popup-backdrop { position: absolute; inset: 0; background: rgba(10, 16, 25, 0.55); }'
            + '.tools-invite-popup-card {'
            + '  position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);'
            + '  width: min(92vw, 460px); padding: 28px 24px; border-radius: 18px;'
            + '  background: linear-gradient(180deg, #ffffff, #f5fbf7); box-shadow: 0 20px 60px rgba(0,0,0,0.25);'
            + '  text-align: center; font-family: Arial, sans-serif; color: #123226;'
            + '}'
            + '.tools-invite-popup-icon {'
            + '  width: 56px; height: 56px; margin: 0 auto 14px auto; border-radius: 50%;'
            + '  display: flex; align-items: center; justify-content: center;'
            + '  background: #2f9e68; color: #fff; font-size: 28px; font-weight: bold;'
            + '}'
            + '.tools-invite-popup-actions { display: flex; gap: 12px; justify-content: center; flex-wrap: wrap; margin-top: 18px; }'
            + '.tools-invite-popup-button, .tools-invite-popup-close {'
            + '  appearance: none; border: 0; border-radius: 999px; padding: 12px 18px;'
            + '  background: #2f9e68; color: #fff; text-decoration: none; cursor: pointer; font-weight: 600;'
            + '}'
            + '.tools-invite-popup-close { background: #5b6770; }';

        overlay.appendChild(style);
        document.body.appendChild(overlay);

        var closeButton = overlay.querySelector('.tools-invite-popup-close');
        if (closeButton) {
            closeButton.addEventListener('click', function () {
                overlay.remove();
            });
        }
    }

    window.TornevallTools = window.TornevallTools || {};
    window.TornevallTools.vbulletin = window.TornevallTools.vbulletin || {};
    window.TornevallTools.vbulletin.showInviteSuccessPopup = showInviteSuccessPopup;
})();
</script>

Exempel: hela flödet tillsammans

<script>
(function () {
    function runInviteCompletionFlow() {
        if (!window.TornevallTools || !window.TornevallTools.vbulletin) {
            return;
        }

        var api = window.TornevallTools.vbulletin;
        if (typeof api.completeInviteWithTools !== 'function') {
            return;
        }

        api.completeInviteWithTools().then(function (result) {
            if (!result || !result.ok || !result.data || !result.data.ok) {
                return;
            }

            if (result.data.access_granted && typeof api.showInviteSuccessPopup === 'function') {
                var continueUrl = result.data.request && result.data.request.public_status_url
                    ? result.data.request.public_status_url
                    : 'https://forum.tornevall.net';

                api.showInviteSuccessPopup(
                    result.data.message || 'Grattis, du är nu insläppt.',
                    continueUrl
                );
            }
        }).catch(function (error) {
            if (window.console && console.warn) {
                console.warn('Invite completion failed:', error);
            }
        });
    }

    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', runInviteCompletionFlow);
    } else {
        runInviteCompletionFlow();
    }
})();
</script>

Rekommenderat beteende efter svar från API:t

När access_granted=true

  • visa popupen direkt
  • länka vidare till forumet eller den personliga statussidan
  • använd inte onboarding-sidan som mellanlandning

När requires_manual_review=true

  • visa hellre en neutral informationsruta än ett grattis
  • tala om att registreringen är mottagen men väntar på granskning

När svaret blir 404 eller 422

  • logga felet i console först
  • visa inte ett falskt “grattis”
  • kontrollera i första hand att rätt invitekod verkligen fanns i profilfältet

Kort manual

  1. Lägg hooken i header_head.
  2. Skicka med hook arguments:
    • userinfo=userinfo
    • bbuserinfo=bbuserinfo
  3. Spara råvärdet från field66 som invitationCodeRaw.
  4. Normalisera värdet innan POST.
  5. Skicka bara invitekod + forumidentitet till Tools.
  6. Skicka inga bearer tokens.
  7. Visa popup först när API:t verkligen svarar med lyckad insläppning.

Färdigt exempel: aktuell Snowball-autoverify som fungerar

Det här är det nuvarande kompletta exemplet på hur aktiveringen kan ske i snowball_header_script.

Praktiskt beteende i detta exempel:

  • sätter upp window.TornevallTools.vbulletin.user
  • använder inviteCompleteEndpoint direkt i hooken
  • behåller både invitationCodeRaw och normaliserad invitationCode
  • försöker verifiera invitekoden automatiskt efter sidladdning
  • stoppar om invitekoden saknas eller användaren redan har verifierats i samma browser-session
  • visar en stylad popup vid lyckad eller misslyckad verifiering
  • behandlar 404 invite code not found som ett stopp-läge i stället för att spamma om samma fel under samma session
<script>
(function () {
    window.TornevallTools = window.TornevallTools || new Object();
    window.TornevallTools.vbulletin = window.TornevallTools.vbulletin || new Object();
    window.TornevallTools.vbulletin.user = window.TornevallTools.vbulletin.user || new Object();

    window.TornevallTools.vbulletin.user.userid = "{vb:raw user.userid}";
    window.TornevallTools.vbulletin.user.username = "{vb:raw user.username}";
    window.TornevallTools.vbulletin.user.invitationCodeRaw = "{vb:raw user.field66}";
    window.TornevallTools.vbulletin.user.invitationCode = "{vb:raw user.field66}";
    window.TornevallTools.vbulletin.user.invitationFieldId = 66;
    window.TornevallTools.vbulletin.user.inviteCompleteEndpoint = "https://tools.tornevall.net/api/vbulletin/onboarding/invite/complete";
})();

(function () {
    var path = decodeURIComponent(window.location.pathname).toLowerCase();

    if (path === "/snowball" || path === "/snowball/") {
        window.location.replace("/snowball/forum");
        return;
    }

    if (path === "/snowball/forum" || path.startsWith("/snowball/forum/")) {
        document.documentElement.classList.add("is-snowball-forum");
    }
})();
</script>

<style>
html.is-snowball-forum #header .site-logo a > img {
    visibility: hidden !important;
}

#tools-vb-member-widgets {
    max-width: 1080px;
    margin: 0 auto 24px auto;
    padding: 18px 22px;
    border-left: 5px solid #6faf8b;
    border-radius: 10px;
    background: #f7fff9;
    box-shadow: 0 8px 22px rgba(0, 0, 0, 0.08);
    text-align: center;
    font-weight: 600;
}

#tools-vb-member-widgets .tools-vb-widget-line {
    display: block;
    margin: 4px 0;
}

#tools-vb-widget-loading {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 8px;
    opacity: 0.75;
}

.tools-vb-spinner {
    width: 14px;
    height: 14px;
    border: 2px solid rgba(0, 0, 0, 0.18);
    border-top-color: rgba(0, 0, 0, 0.65);
    border-radius: 50%;
    animation: toolsVbSpin 0.8s linear infinite;
}

@keyframes toolsVbSpin {
    to {
        transform: rotate(360deg);
    }
}

#tools-snowball-autoverify-popup {
    position: fixed;
    right: 22px;
    bottom: 22px;
    z-index: 999999;
    max-width: 420px;
    padding: 18px 20px;
    border-left: 5px solid #6faf8b;
    border-radius: 12px;
    background: #f7fff9;
    color: #1d2b20;
    box-shadow: 0 14px 36px rgba(0, 0, 0, 0.22);
    font-family: inherit;
    line-height: 1.45;
    opacity: 0;
    transform: translateY(14px);
    transition: opacity 180ms ease, transform 180ms ease;
}

#tools-snowball-autoverify-popup.is-visible {
    opacity: 1;
    transform: translateY(0);
}

#tools-snowball-autoverify-popup.is-error {
    border-left-color: #d0051b;
    background: #fff7f7;
    color: #3a1414;
}

#tools-snowball-autoverify-popup .tools-snowball-popup-title {
    display: block;
    margin-bottom: 6px;
    padding-right: 24px;
    font-size: 16px;
    font-weight: 800;
}

#tools-snowball-autoverify-popup .tools-snowball-popup-message {
    display: block;
    padding-right: 24px;
    font-size: 14px;
}

#tools-snowball-autoverify-popup .tools-snowball-popup-close {
    position: absolute;
    top: 7px;
    right: 9px;
    border: 0;
    background: transparent;
    color: inherit;
    font-size: 20px;
    line-height: 20px;
    cursor: pointer;
    opacity: 0.7;
}

#tools-snowball-autoverify-popup .tools-snowball-popup-close:hover {
    opacity: 1;
}
</style>

<link rel="preload" as="image" href="https://forum.tornevall.net/filedata/fetch?photoid=1063354" fetchpriority="high">
<link rel="preconnect" href="https://tools.tornevall.net" crossorigin>
<link rel="preload" as="script" href="https://tools.tornevall.net/api/managed-scripts/vbulletin/bundle.js">
<link rel="preload" as="script" href="https://tools.tornevall.net/vbulletin/widgets/latest-member.js?group_ids=155&variant=plain&container_id=tools-vb-latest-member-widget&template=Vi+v%C3%A4lkomnar+v%C3%A5r+senaste+medlem+%7Bname%7D">
<link rel="preload" as="script" href="https://tools.tornevall.net/vbulletin/widgets/member-count.js?group_ids=155&variant=plain&container_id=tools-vb-member-count-widget&template=Sn%C3%B6bollseffekten+v%C3%A4xer+-+vi+%C3%A4r+nu+%7Bcount%7D+medlemmar">

<script defer src="https://tools.tornevall.net/api/managed-scripts/vbulletin/bundle.js"></script>

<script>
(function () {
    var snowballLogoUrl = "https://forum.tornevall.net/filedata/fetch?photoid=1063354";
    var path = decodeURIComponent(window.location.pathname).toLowerCase();

    if (!(path === "/snowball/forum" || path.startsWith("/snowball/forum/"))) {
        return;
    }

    var preloadedLogo = new Image();
    preloadedLogo.fetchPriority = "high";
    preloadedLogo.src = snowballLogoUrl;

    function swapSnowballLogo() {
        var logo = document.querySelector("#header .site-logo a > img");

        if (!logo) {
            return false;
        }

        logo.src = snowballLogoUrl;
        logo.setAttribute("data-orig-src", snowballLogoUrl);
        logo.alt = "Snöbollseffekten";
        logo.title = "Snöbollseffekten";
        logo.style.setProperty("visibility", "visible", "important");

        return true;
    }

    if (!swapSnowballLogo()) {
        var observer = new MutationObserver(function () {
            if (swapSnowballLogo()) {
                observer.disconnect();
            }
        });

        observer.observe(document.documentElement, {
            childList: true,
            subtree: true
        });

        window.setTimeout(function () {
            observer.disconnect();
        }, 5000);
    }
})();

(function () {
    var user = window.TornevallTools
        && window.TornevallTools.vbulletin
        && window.TornevallTools.vbulletin.user
            ? window.TornevallTools.vbulletin.user
            : new Object();

    function trimString(value) {
        return String(value || "").replace(/^\s+|\s+$/g, "");
    }

    function normalizeInviteCode(value) {
        var raw = trimString(value);

        if (!raw) {
            return "";
        }

        try {
            if (/^https?:\/\//i.test(raw)) {
                var url = new URL(raw);
                var parts = url.pathname.split("/");
                var index = parts.length - 1;

                while (index >= 0) {
                    if (parts[index]) {
                        return decodeURIComponent(parts[index]);
                    }

                    index--;
                }
            }
        } catch (error) {
        }

        return raw;
    }

    function showSnowballPopup(title, message, isError) {
        var existing = document.getElementById("tools-snowball-autoverify-popup");

        if (existing && existing.parentNode) {
            existing.parentNode.removeChild(existing);
        }

        var popup = document.createElement("div");
        popup.id = "tools-snowball-autoverify-popup";

        if (isError) {
            popup.className = "is-error";
        }

        var closeButton = document.createElement("button");
        closeButton.type = "button";
        closeButton.className = "tools-snowball-popup-close";
        closeButton.setAttribute("aria-label", "Stäng");
        closeButton.appendChild(document.createTextNode("×"));

        var titleNode = document.createElement("span");
        titleNode.className = "tools-snowball-popup-title";
        titleNode.appendChild(document.createTextNode(title));

        var messageNode = document.createElement("span");
        messageNode.className = "tools-snowball-popup-message";
        messageNode.appendChild(document.createTextNode(message));

        closeButton.onclick = function () {
            if (popup.parentNode) {
                popup.parentNode.removeChild(popup);
            }
        };

        popup.appendChild(closeButton);
        popup.appendChild(titleNode);
        popup.appendChild(messageNode);

        document.body.appendChild(popup);

        window.setTimeout(function () {
            popup.className = popup.className
                ? popup.className + " is-visible"
                : "is-visible";
        }, 20);

        window.setTimeout(function () {
            if (popup && popup.parentNode) {
                popup.className = popup.className.replace(/\bis-visible\b/g, "");
            }
        }, 9000);

        window.setTimeout(function () {
            if (popup && popup.parentNode) {
                popup.parentNode.removeChild(popup);
            }
        }, 9500);
    }

    function parseJsonResponse(response) {
        return response.text().then(function (text) {
            var payload = null;

            try {
                payload = text ? JSON.parse(text) : null;
            } catch (error) {
                payload = {
                    ok: false,
                    message: text || "Empty response",
                    raw: text || ""
                };
            }

            if (!payload) {
                payload = new Object();
            }

            payload.httpStatus = response.status;
            payload.httpOk = response.ok;

            return payload;
        });
    }

    function isInviteNotFound(data) {
        var message = trimString(data && data.message ? data.message : "").toLowerCase();

        return data
            && Number(data.httpStatus) === 404
            && (
                message === "invite code not found."
                || message === "invite code not found"
                || message.indexOf("invite code not found") !== -1
            );
    }

    function isSuccessResponse(data) {
        return data
            && (
                data.ok === true
                || data.success === true
                || data.completed === true
                || data.status === "completed"
                || data.status === "ok"
                || data.status === "success"
            );
    }

    function runSnowballAutoverify() {
        var endpoint = trimString(user.inviteCompleteEndpoint || "https://tools.tornevall.net/api/vbulletin/onboarding/invite/complete");
        var userid = trimString(user.userid || "");
        var username = trimString(user.username || "");
        var inviteCodeRaw = trimString(user.invitationCodeRaw || user.invitationCode || "");
        var inviteCode = normalizeInviteCode(inviteCodeRaw);

        if (window.console && console.log) {
            console.log("Snowball autoverify boot:", {
                endpoint: endpoint,
                userid: userid,
                username: username,
                inviteCodeRaw: inviteCodeRaw,
                inviteCode: inviteCode
            });
        }

        if (!endpoint) {
            if (window.console && console.info) {
                console.info("Snowball autoverify skipped: missing endpoint");
            }

            return;
        }

        if (!userid || userid === "0") {
            if (window.console && console.info) {
                console.info("Snowball autoverify skipped: missing userid");
            }

            return;
        }

        if (!inviteCode) {
            if (window.console && console.info) {
                console.info("Snowball autoverify skipped: missing invite code");
            }

            return;
        }

        var storageKey = "tools_snowball_invite_complete_" + userid + "_" + inviteCode;

        try {
            if (window.sessionStorage && sessionStorage.getItem(storageKey) === "done") {
                if (window.console && console.info) {
                    console.info("Snowball autoverify skipped: already done in this browser session");
                }

                return;
            }

            if (window.sessionStorage && sessionStorage.getItem(storageKey) === "not_found") {
                if (window.console && console.info) {
                    console.info("Snowball autoverify skipped: invite code was already reported as not found in this browser session");
                }

                return;
            }

            if (window.sessionStorage && sessionStorage.getItem(storageKey) === "running") {
                if (window.console && console.info) {
                    console.info("Snowball autoverify skipped: already running");
                }

                return;
            }

            if (window.sessionStorage) {
                sessionStorage.setItem(storageKey, "running");
            }
        } catch (error) {
        }

        var body = new FormData();
        body.append("invite_code", inviteCode);
        body.append("invite_code_raw", inviteCodeRaw);
        body.append("forum_user_id", userid);
        body.append("forum_username", username);
        body.append("userid", userid);
        body.append("username", username);
        body.append("field_id", String(user.invitationFieldId || 66));

        if (window.console && console.log) {
            console.log("Snowball autoverify POST:", endpoint);
        }

        fetch(endpoint, {
            method: "POST",
            body: body,
            credentials: "omit"
        })
            .then(parseJsonResponse)
            .then(function (data) {
                if (window.console && console.log) {
                    console.log("Snowball autoverify response:", data);
                }

                if (isSuccessResponse(data)) {
                    try {
                        if (window.sessionStorage) {
                            sessionStorage.setItem(storageKey, "done");
                        }
                    } catch (error) {
                    }

                    showSnowballPopup(
                        "Grattis, du är nu insläppt!",
                        "Din inbjudningskod är verifierad och forumåtkomsten har uppdaterats.",
                        false
                    );

                    return;
                }

                if (isInviteNotFound(data)) {
                    try {
                        if (window.sessionStorage) {
                            sessionStorage.setItem(storageKey, "not_found");
                        }
                    } catch (error) {
                    }

                    if (window.console && console.info) {
                        console.info("Snowball autoverify stopped: invite code not found in Tools.", {
                            inviteCode: inviteCode,
                            httpStatus: data.httpStatus,
                            message: data.message || ""
                        });
                    }

                    return;
                }

                try {
                    if (window.sessionStorage) {
                        sessionStorage.removeItem(storageKey);
                    }
                } catch (error) {
                }

                if (window.console && console.info) {
                    console.info("Snowball autoverify did not complete:", data);
                }

                showSnowballPopup(
                    "Inbjudan kunde inte verifieras",
                    data && data.message ? data.message : "Tools API svarade, men bekräftade inte insläppet.",
                    true
                );
            })
            .catch(function (error) {
                try {
                    if (window.sessionStorage) {
                        sessionStorage.removeItem(storageKey);
                    }
                } catch (storageError) {
                }

                if (window.console && console.warn) {
                    console.warn("Snowball autoverify network error:", error);
                }

                showSnowballPopup(
                    "Inbjudan kunde inte verifieras",
                    "Nätverksanropet till Tools kunde inte slutföras just nu.",
                    true
                );
            });
    }

    if (document.readyState === "loading") {
        document.addEventListener("DOMContentLoaded", runSnowballAutoverify);
    } else {
        runSnowballAutoverify();
    }
})();
</script>

Varför det här exemplet är användbart

  • Det visar hela det verkliga hook-flödet i ett stycke.
  • Det använder exakt samma publika endpoint som Tools nu dokumenterar.
  • Det visar skillnaden mellan invitationCodeRaw och den normaliserade invitekoden.
  • Det visar hur man kan låta autoverify köra tyst när invitekoden inte finns, men ge tydlig popup när verklig insläppning eller verkligt fel inträffar.