UNPKG

posthog-js

Version:

Posthog-js allows you to automatically capture usage and send events to PostHog.

1,723 lines • 88.1 kB
"use strict";
var __assign = (this && this.__assign) || function () {
    __assign = Object.assign || function(t) {
        for (var s, i = 1, n = arguments.length; i < n; i++) {
            s = arguments[i];
            for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
                t[p] = s[p];
        }
        return t;
    };
    return __assign.apply(this, arguments);
};
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
    function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
    return new (P || (P = Promise))(function (resolve, reject) {
        function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
        function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
        function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
        step((generator = generator.apply(thisArg, _arguments || [])).next());
    });
};
var __generator = (this && this.__generator) || function (thisArg, body) {
    var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype);
    return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
    function verb(n) { return function (v) { return step([n, v]); }; }
    function step(op) {
        if (f) throw new TypeError("Generator is already executing.");
        while (g && (g = 0, op[0] && (_ = 0)), _) try {
            if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
            if (y = 0, t) op = [op[0] & 2, t.value];
            switch (op[0]) {
                case 0: case 1: t = op; break;
                case 4: _.label++; return { value: op[1], done: false };
                case 5: _.label++; y = op[1]; op = [0]; continue;
                case 7: op = _.ops.pop(); _.trys.pop(); continue;
                default:
                    if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
                    if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
                    if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
                    if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
                    if (t[2]) _.ops.pop();
                    _.trys.pop(); continue;
            }
            op = body.call(thisArg, _);
        } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
        if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
    }
};
var __read = (this && this.__read) || function (o, n) {
    var m = typeof Symbol === "function" && o[Symbol.iterator];
    if (!m) return o;
    var i = m.call(o), r, ar = [], e;
    try {
        while ((n === void 0 || n-- > 0) && !(r = i.next()).done) ar.push(r.value);
    }
    catch (error) { e = { error: error }; }
    finally {
        try {
            if (r && !r.done && (m = i["return"])) m.call(i);
        }
        finally { if (e) throw e.error; }
    }
    return ar;
};
var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) {
    if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) {
        if (ar || !(i in from)) {
            if (!ar) ar = Array.prototype.slice.call(from, 0, i);
            ar[i] = from[i];
        }
    }
    return to.concat(ar || Array.prototype.slice.call(from));
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.ConversationsManager = void 0;
exports.initConversations = initConversations;
var jsx_runtime_1 = require("preact/jsx-runtime");
var preact_1 = require("preact");
var core_1 = require("@posthog/core");
var constants_1 = require("../../../constants");
var persistence_1 = require("./persistence");
var ConversationsWidget_1 = require("./components/ConversationsWidget");
var logger_1 = require("@posthog/browser-common/utils/logger");
var globals_1 = require("@posthog/browser-common/utils/globals");
var request_utils_1 = require("@posthog/browser-common/utils/request-utils");
var url_utils_1 = require("./url-utils");
var general_utils_1 = require("@posthog/browser-common/utils/general-utils");
var errors_1 = require("./errors");
var logger = (0, logger_1.createLogger)('[ConversationsManager]');
var createConversationsRequestError = function (response, operation, fallbackMessage, rateLimitIsWarning, logFailure) {
    var _a, _b, _c;
    if (rateLimitIsWarning === void 0) { rateLimitIsWarning = false; }
    if (logFailure === void 0) { logFailure = true; }
    if (response.statusCode === 0) {
        // Fetch failures are already logged by `_send_request`. XHR reports status 0
        // without an error, so keep one warning for that otherwise-unlogged path.
        var error = (0, errors_1.createConversationsError)('network', 'Unable to reach the server. Please check your connection and try again.');
        if (response.error) {
            var errorWithCause = error;
            errorWithCause.cause = response.error;
        }
        else if (logFailure) {
            logger.warn("Network error ".concat(operation));
        }
        return error;
    }
    if (response.statusCode === 429) {
        if (logFailure) {
            if (rateLimitIsWarning) {
                logger.warn("Rate limited ".concat(operation));
            }
            else {
                logger.error(fallbackMessage, { status: response.statusCode });
            }
        }
        return (0, errors_1.createConversationsError)('rate_limit', 'Too many requests. Please wait before trying again.');
    }
    var message = ((_a = response.json) === null || _a === void 0 ? void 0 : _a.error) || ((_b = response.json) === null || _b === void 0 ? void 0 : _b.detail) || ((_c = response.json) === null || _c === void 0 ? void 0 : _c.message) || fallbackMessage;
    if (logFailure) {
        logger.error(fallbackMessage, { status: response.statusCode });
    }
    return (0, errors_1.createConversationsError)('http', message);
};
var createInvalidConversationsResponseError = function (operation, logFailure) {
    if (logFailure === void 0) { logFailure = true; }
    var message = 'Invalid response from server';
    if (logFailure) {
        logger.error(message, { operation: operation });
    }
    return (0, errors_1.createConversationsError)('invalid_response', message);
};
var WIDGET_CONTAINER_ID = 'ph-conversations-widget-container';
// Polling cadence. The widget polls faster while open (so replies feel live) and
// much slower while closed (the badge can lag a few seconds). Idle widgets with no
// conversation don't poll at all — see _hasSomethingToPoll.
var POLL_INTERVAL_OPEN_MS = 5000; // 5 seconds
var POLL_INTERVAL_CLOSED_MS = 15000; // 15 seconds
// On HTTP 429 we back off exponentially instead of hammering the shared team budget,
// which is what starves visitor sends (the send and poll budgets are separate server-side,
// but polling still competes with everyone else's polls).
var POLL_RATE_LIMIT_BASE_MS = 5000; // 5 seconds
var POLL_RATE_LIMIT_MAX_MS = 60000; // 1 minute
// How often to check that the widget container is still attached to the DOM.
// SPA frameworks that replace document.body on navigation (e.g. Turbo Drive)
// detach our container; this watcher re-attaches it so the widget survives.
var REATTACH_CHECK_INTERVAL_MS = 1000; // 1 second
var RESTORE_EXCHANGE_ENDPOINT = '/api/conversations/v1/widget/restore';
var RESTORE_REQUEST_ENDPOINT = '/api/conversations/v1/widget/restore/request';
// Polling runs every 5s while the widget is visible, so repeated status-0
// failures can become an endless blocked-request loop. Match the browser event
// retry budget and pause polling after this many consecutive online failures.
var MAX_CONSECUTIVE_POLLING_STATUS_ZERO_FAILURES = 3;
// Singleton guard: only one ConversationsManager per page.
// The toolbar's internal PostHog instance is excluded from creating a manager
// (see PostHogConversations.loadIfEnabled), so this always belongs to the main instance.
var _activeManager = null;
var ConversationsManager = /** @class */ (function () {
    function ConversationsManager(config, _posthog) {
        var _this = this;
        this._posthog = _posthog;
        this._widgetRef = null;
        this._containerElement = null;
        this._currentTicketId = null;
        this._pollTimeoutId = null;
        // Guards against a second _startPolling() while the first poll is still in flight
        // (the timeout id isn't set until the poll resolves and schedules the next tick).
        this._pollLoopRunning = false;
        // Bumped by every start/stop. An in-flight poll chain captures the generation it
        // started with and abandons itself if a stop/restart happened while it awaited,
        // so open/close (which stops then starts) can never leave two loops running.
        this._pollGeneration = 0;
        this._consecutivePollingRateLimitFailures = 0;
        this._reattachIntervalId = null;
        this._lastMessageTimestamp = null;
        this._isPollingMessages = false;
        this._isPollingTickets = false;
        this._unsubscribeIdentifyListener = null;
        this._unreadCount = 0;
        this._widgetState = 'closed';
        this._isWidgetRendered = false;
        this._hasProcessedRestoreToken = false;
        this._initializeWidgetPromise = null;
        // Bumped by destroy() (and so by reset()). Async initialization captures it before
        // awaiting a request and stops if it changed, so a manager torn down mid-request
        // doesn't persist state, render, or start polling afterwards.
        this._lifecycleGeneration = 0;
        // View state management for ticket list vs message view
        this._currentView = 'messages';
        this._tickets = [];
        this._showTicketList = false;
        this._consecutivePollingStatusZeroFailures = 0;
        this._onOnline = function () {
            _this._consecutivePollingStatusZeroFailures = 0;
            _this._consecutivePollingRateLimitFailures = 0;
            // Clearing the streak alone leaves any pending long backoff timeout in place,
            // so messages/unread would stay stale until it fires (up to a minute).
            // Restarting polls immediately and reschedules at the normal cadence.
            if (_this._pollLoopRunning) {
                _this._stopPolling();
                _this._startPolling();
            }
        };
        /**
         * Handle user identification from the widget form
         */
        this._handleIdentify = function (traits) {
            // Save traits to localStorage
            _this._persistence.saveUserTraits(traits);
            // Track identification
            _this._posthog.capture('$conversations_user_identified', {
                hasName: !!traits.name,
                hasEmail: !!traits.email,
            });
        };
        this._handleRequestRestoreLink = function (email) { return __awaiter(_this, void 0, void 0, function () {
            var response;
            return __generator(this, function (_a) {
                switch (_a.label) {
                    case 0: return [4 /*yield*/, this.requestRestoreLink(email)];
                    case 1:
                        response = _a.sent();
                        this._posthog.capture('$conversations_restore_link_requested', {
                            hasEmail: !!email,
                        });
                        return [2 /*return*/, response];
                }
            });
        }); };
        /**
         * Handle sending a message from the widget
         */
        this._handleSendMessage = function (message) { return __awaiter(_this, void 0, void 0, function () {
            var userTraits;
            var _this = this;
            var _a;
            return __generator(this, function (_b) {
                switch (_b.label) {
                    case 0:
                        userTraits = ((_a = this._widgetRef) === null || _a === void 0 ? void 0 : _a.getUserTraits()) || undefined;
                        // Call the public API method (which handles tracking and state updates).
                        // Any rejection is propagated to the widget, which renders the error state and logs it -
                        // logging it here as well would surface a benign network failure as a captured exception.
                        return [4 /*yield*/, this.sendMessage(message, userTraits)
                            // Poll for response immediately (sendMessage already resumed the loop)
                        ];
                    case 1:
                        // Call the public API method (which handles tracking and state updates).
                        // Any rejection is propagated to the widget, which renders the error state and logs it -
                        // logging it here as well would surface a benign network failure as a captured exception.
                        _b.sent();
                        // Poll for response immediately (sendMessage already resumed the loop)
                        setTimeout(function () { return _this._pollMessages(); }, 1000);
                        return [2 /*return*/];
                }
            });
        }); };
        /**
         * Handle widget state changes
         */
        this._handleStateChange = function (state) {
            _this._widgetState = state;
            logger.info('Widget state changed', { state: state, view: _this._currentView });
            _this._posthog.capture('$conversations_widget_state_changed', {
                state: state,
                view: _this._currentView,
                ticketId: _this._currentTicketId,
            });
            _this._persistence.saveWidgetState(state);
            // Restart the loop so the new open/closed cadence takes effect immediately
            // (and opening triggers a fresh poll rather than waiting out a closed interval).
            if (_this._isWidgetRendered) {
                _this._stopPolling();
                _this._startPolling();
            }
            // Mark messages as read when widget opens (only if in message view with a ticket)
            if (state === 'open') {
                if (_this._currentView === 'messages' && _this._unreadCount > 0 && _this._currentTicketId) {
                    _this._markMessagesAsRead();
                }
            }
        };
        /**
         * Poll for new messages
         */
        this._pollMessages = function () { return __awaiter(_this, void 0, void 0, function () {
            return __generator(this, function (_a) {
                switch (_a.label) {
                    case 0:
                        if (this._isPollingMessages ||
                            !this._currentTicketId ||
                            (0, request_utils_1.isStatusZeroFailureCircuitBreakerTripped)(this._consecutivePollingStatusZeroFailures, MAX_CONSECUTIVE_POLLING_STATUS_ZERO_FAILURES)) {
                            return [2 /*return*/];
                        }
                        this._isPollingMessages = true;
                        _a.label = 1;
                    case 1:
                        _a.trys.push([1, , 3, 4]);
                        return [4 /*yield*/, this._loadMessages()];
                    case 2:
                        _a.sent();
                        return [3 /*break*/, 4];
                    case 3:
                        this._isPollingMessages = false;
                        return [7 /*endfinally*/];
                    case 4: return [2 /*return*/];
                }
            });
        }); };
        /**
         * Poll for tickets list
         */
        this._pollTickets = function () { return __awaiter(_this, void 0, void 0, function () {
            return __generator(this, function (_a) {
                switch (_a.label) {
                    case 0:
                        if (this._isPollingTickets ||
                            (0, request_utils_1.isStatusZeroFailureCircuitBreakerTripped)(this._consecutivePollingStatusZeroFailures, MAX_CONSECUTIVE_POLLING_STATUS_ZERO_FAILURES)) {
                            return [2 /*return*/];
                        }
                        this._isPollingTickets = true;
                        _a.label = 1;
                    case 1:
                        _a.trys.push([1, , 3, 4]);
                        return [4 /*yield*/, this._loadTickets()];
                    case 2:
                        _a.sent();
                        return [3 /*break*/, 4];
                    case 3:
                        this._isPollingTickets = false;
                        return [7 /*endfinally*/];
                    case 4: return [2 /*return*/];
                }
            });
        }); };
        /**
         * Main poll function that polls based on current view
         */
        this._poll = function () { return __awaiter(_this, void 0, void 0, function () {
            return __generator(this, function (_a) {
                switch (_a.label) {
                    case 0:
                        if (this._currentView === 'restore_request') {
                            return [2 /*return*/];
                        }
                        if (!(this._currentView === 'messages')) return [3 /*break*/, 2];
                        return [4 /*yield*/, this._pollMessages()];
                    case 1:
                        _a.sent();
                        return [3 /*break*/, 4];
                    case 2: return [4 /*yield*/, this._pollTickets()];
                    case 3:
                        _a.sent();
                        _a.label = 4;
                    case 4: return [2 /*return*/];
                }
            });
        }); };
        /**
         * Handle view changes from the widget
         */
        this._handleViewChange = function (view) {
            logger.info('View changed', { from: _this._currentView, to: view });
            _this._currentView = view;
        };
        /**
         * Handle ticket selection from the list
         */
        this._handleSelectTicket = function (ticketId) { return __awaiter(_this, void 0, void 0, function () {
            var _a, _b, _c;
            return __generator(this, function (_d) {
                switch (_d.label) {
                    case 0:
                        // Switch to this ticket
                        this._switchToTicketIfNeeded(ticketId);
                        // Clear messages and reset timestamp
                        this._lastMessageTimestamp = null;
                        (_a = this._widgetRef) === null || _a === void 0 ? void 0 : _a.clearMessages();
                        // Switch view to messages
                        this._currentView = 'messages';
                        (_b = this._widgetRef) === null || _b === void 0 ? void 0 : _b.setView('messages');
                        // Push resolved state for this ticket so MessagesView locks the input if needed
                        (_c = this._widgetRef) === null || _c === void 0 ? void 0 : _c.setCurrentTicketResolved(this._isCurrentTicketResolved());
                        // Selecting a ticket gives the loop something to poll again
                        this._startPolling();
                        // Load messages for the selected ticket
                        return [4 /*yield*/, this._loadMessages()
                            // Mark as read if widget is open
                        ];
                    case 1:
                        // Load messages for the selected ticket
                        _d.sent();
                        // Mark as read if widget is open
                        if (this._isWidgetOpen() && this._unreadCount > 0) {
                            this._markMessagesAsRead();
                        }
                        return [2 /*return*/];
                }
            });
        }); };
        /**
         * Handle new conversation request
         */
        this._handleNewConversation = function () {
            var _a, _b, _c;
            logger.info('New conversation requested');
            // Clear current ticket
            _this._currentTicketId = null;
            _this._persistence.clearTicketId();
            // Reset timestamp
            _this._lastMessageTimestamp = null;
            // Switch view to messages
            _this._currentView = 'messages';
            (_a = _this._widgetRef) === null || _a === void 0 ? void 0 : _a.setView('messages');
            // Fresh ticket is never resolved — unlock the input
            (_b = _this._widgetRef) === null || _b === void 0 ? void 0 : _b.setCurrentTicketResolved(false);
            // Clear messages and add greeting
            (_c = _this._widgetRef) === null || _c === void 0 ? void 0 : _c.clearMessages(true);
        };
        /**
         * Handle back to tickets request
         */
        this._handleBackToTickets = function () { return __awaiter(_this, void 0, void 0, function () {
            var _a, _b;
            return __generator(this, function (_c) {
                switch (_c.label) {
                    case 0:
                        logger.info('Back to tickets requested');
                        // Switch view to tickets
                        this._currentView = 'tickets';
                        (_a = this._widgetRef) === null || _a === void 0 ? void 0 : _a.setView('tickets');
                        // Load tickets
                        (_b = this._widgetRef) === null || _b === void 0 ? void 0 : _b.setTicketsLoading(true);
                        return [4 /*yield*/, this._loadTickets()
                            // Resume the loop if it had stopped (tickets view polls the list)
                        ];
                    case 1:
                        _c.sent();
                        // Resume the loop if it had stopped (tickets view polls the list)
                        this._startPolling();
                        // Track back to tickets
                        this._posthog.capture('$conversations_back_to_tickets');
                        return [2 /*return*/];
                }
            });
        }); };
        this._pollThenSchedule = function (generation) { return __awaiter(_this, void 0, void 0, function () {
            var error_1;
            return __generator(this, function (_a) {
                switch (_a.label) {
                    case 0:
                        _a.trys.push([0, 2, , 3]);
                        return [4 /*yield*/, this._poll()];
                    case 1:
                        _a.sent();
                        return [3 /*break*/, 3];
                    case 2:
                        error_1 = _a.sent();
                        // _poll is defensively coded not to throw, but never let an unexpected
                        // error kill the loop (which would leave _pollLoopRunning stuck true).
                        logger.error('Polling iteration failed', error_1);
                        return [3 /*break*/, 3];
                    case 3:
                        // A stop/restart bumped the generation while the request was in flight;
                        // abandon this chain so we don't end up running two loops.
                        if (generation !== this._pollGeneration) {
                            return [2 /*return*/];
                        }
                        this._scheduleNextPoll(generation);
                        return [2 /*return*/];
                }
            });
        }); };
        this._config = config;
        this._persistence = new persistence_1.ConversationsPersistence(_posthog);
        this._widgetSessionId = this._persistence.getOrCreateWidgetSessionId();
        // Determine if widget should be shown based on config and domain
        this._isWidgetEnabled = config.widgetEnabled === true;
        this._isDomainAllowed = (0, url_utils_1.isCurrentDomainAllowed)(config.domains);
        if (globals_1.window) {
            (0, general_utils_1.addEventListener)(globals_1.window, 'online', this._onOnline);
        }
        this._initialize();
    }
    ConversationsManager.prototype._currentUrl = function () {
        var _a;
        var href = (_a = globals_1.window === null || globals_1.window === void 0 ? void 0 : globals_1.window.location) === null || _a === void 0 ? void 0 : _a.href;
        return this._posthog.config.disable_capture_url_hashes ? (0, core_1.stripUrlHash)(href) : href;
    };
    /**
     * Send a message programmatically via the API
     * Creates a new ticket if none exists or if newTicket is true
     *
     * @param message - The message text to send
     * @param userTraits - Optional user identification data (name, email)
     * @param newTicket - If true, forces creation of a new ticket (ignores current ticket)
     * @returns Promise with the response including ticket_id and message_id
     */
    ConversationsManager.prototype.sendMessage = function (message, userTraits, newTicket) {
        return __awaiter(this, void 0, void 0, function () {
            var ticketId, isNewTicket, token;
            var _this = this;
            return __generator(this, function (_a) {
                ticketId = newTicket ? null : this._currentTicketId;
                isNewTicket = !ticketId;
                token = this._config.token;
                // oxlint-disable-next-line compat/compat
                return [2 /*return*/, new Promise(function (resolve, reject) {
                        var personTraits = _this._getPersonTraits();
                        var name = (userTraits === null || userTraits === void 0 ? void 0 : userTraits.name) || personTraits.name || null;
                        var email = (userTraits === null || userTraits === void 0 ? void 0 : userTraits.email) || personTraits.email || null;
                        var identity = _this._identityFields();
                        var payload = {
                            message: message.trim(),
                            traits: {
                                name: name,
                                email: email,
                            },
                            ticket_id: ticketId,
                        };
                        if (identity) {
                            Object.assign(payload, identity);
                            payload.distinct_id = identity.identity_distinct_id;
                        }
                        else {
                            payload.widget_session_id = _this._widgetSessionId;
                            payload.distinct_id = _this._posthog.get_distinct_id();
                        }
                        try {
                            // Capture session ID - sent with every message
                            var capturedSessionId = _this._posthog.get_session_id();
                            if (capturedSessionId) {
                                payload.session_id = capturedSessionId;
                            }
                            // Capture session replay URL with timestamp - sent with every message
                            var replayUrl = _this._posthog.get_session_replay_url({
                                withTimestamp: true,
                                timestampLookBack: 30,
                            });
                            // Capture current URL - only for new tickets to record where user started
                            var currentUrl = isNewTicket ? _this._currentUrl() : undefined;
                            if (replayUrl || currentUrl) {
                                payload.session_context = {
                                    session_replay_url: replayUrl || undefined,
                                    current_url: currentUrl || undefined,
                                };
                            }
                        }
                        catch (error) {
                            // Log error but don't fail message sending
                            logger.warn('Failed to capture session context', error);
                        }
                        _this._posthog._send_request({
                            url: _this._posthog.requestRouter.endpointFor('api', '/api/conversations/v1/widget/message'),
                            method: 'POST',
                            data: payload,
                            headers: {
                                'X-Conversations-Token': token,
                            },
                            callback: function (response) {
                                if (response.statusCode !== 200 && response.statusCode !== 201) {
                                    reject(createConversationsRequestError(response, 'sending message', 'Failed to send message', true));
                                    return;
                                }
                                if (!response.json) {
                                    reject(createInvalidConversationsResponseError('sending message'));
                                    return;
                                }
                                var data = response.json;
                                // Update current ticket ID if this was a new ticket
                                // This happens when: 1) No ticket existed, or 2) User forced new ticket creation
                                if (isNewTicket && data.ticket_id) {
                                    _this._currentTicketId = data.ticket_id;
                                    _this._persistence.saveTicketId(data.ticket_id);
                                    logger.info('New ticket created', {
                                        ticketId: data.ticket_id,
                                        forced: newTicket === true,
                                    });
                                }
                                // Track message sent
                                _this._posthog.capture('$conversations_message_sent', {
                                    ticketId: data.ticket_id,
                                    isNewTicket: isNewTicket,
                                    messageLength: message.length,
                                });
                                // Update last message timestamp
                                _this._lastMessageTimestamp = data.created_at;
                                // A ticket now exists, so resume polling if the loop had stopped
                                // because there was nothing to poll. Only when a widget is rendered —
                                // programmatic API-only usage never polls.
                                if (_this._isWidgetRendered) {
                                    _this._startPolling();
                                }
                                resolve(data);
                            },
                        });
                    })];
            });
        });
    };
    /**
     * Switch to a different ticket if an explicit ticketId is provided
     * This ensures subsequent operations (sendMessage, etc.) use the correct ticket
     */
    ConversationsManager.prototype._switchToTicketIfNeeded = function (ticketId) {
        if (ticketId && ticketId !== this._currentTicketId) {
            this._currentTicketId = ticketId;
            this._persistence.saveTicketId(ticketId);
            // Reset last message timestamp when switching tickets
            this._lastMessageTimestamp = null;
        }
    };
    /** Fetch messages via the API */
    ConversationsManager.prototype.getMessages = function (ticketId, after) {
        return __awaiter(this, void 0, void 0, function () {
            var targetTicketId, token;
            var _this = this;
            return __generator(this, function (_a) {
                targetTicketId = ticketId || this._currentTicketId;
                if (!targetTicketId) {
                    throw new Error('No ticket ID provided and no active conversation');
                }
                // Switch to this ticket if explicitly provided
                this._switchToTicketIfNeeded(ticketId);
                token = this._config.token;
                // oxlint-disable-next-line compat/compat
                return [2 /*return*/, new Promise(function (resolve, reject) {
                        var identity = _this._identityFields();
                        var queryParams = {
                            limit: '50',
                        };
                        if (identity) {
                            Object.assign(queryParams, identity);
                        }
                        else {
                            queryParams.widget_session_id = _this._widgetSessionId;
                        }
                        if (after) {
                            queryParams.after = after;
                        }
                        _this._posthog._send_request({
                            url: _this._posthog.requestRouter.endpointFor('api', "/api/conversations/v1/widget/messages/".concat(targetTicketId, "?").concat((0, request_utils_1.formDataToQuery)(queryParams))),
                            method: 'GET',
                            timestampMode: 'query',
                            headers: {
                                'X-Conversations-Token': token,
                            },
                            callback: function (response) {
                                _this._trackPollingEndpointReachability(response.statusCode);
                                _this._trackPollingRateLimit(response.statusCode);
                                if (response.statusCode !== 200) {
                                    reject(createConversationsRequestError(response, 'fetching messages', 'Failed to fetch messages'));
                                    return;
                                }
                                if (!response.json) {
                                    reject(createInvalidConversationsResponseError('fetching messages'));
                                    return;
                                }
                                var data = response.json;
                                resolve(data);
                            },
                        });
                    })];
            });
        });
    };
    /** Mark messages as read via the API */
    ConversationsManager.prototype.markAsRead = function (ticketId) {
        return __awaiter(this, void 0, void 0, function () {
            var targetTicketId, token;
            var _this = this;
            return __generator(this, function (_a) {
                targetTicketId = ticketId || this._currentTicketId;
                if (!targetTicketId) {
                    throw new Error('No ticket ID provided and no active conversation');
                }
                // Switch to this ticket if explicitly provided
                this._switchToTicketIfNeeded(ticketId);
                token = this._config.token;
                logger.info('Marking messages as read', { ticketId: targetTicketId });
                // oxlint-disable-next-line compat/compat
                return [2 /*return*/, new Promise(function (resolve, reject) {
                        var identity = _this._identityFields();
                        var data = identity || { widget_session_id: _this._widgetSessionId };
                        _this._posthog._send_request({
                            url: _this._posthog.requestRouter.endpointFor('api', "/api/conversations/v1/widget/messages/".concat(targetTicketId, "/read")),
                            method: 'POST',
                            data: data,
                            headers: {
                                'X-Conversations-Token': token,
                            },
                            callback: function (response) {
                                if (response.statusCode !== 200) {
                                    reject(createConversationsRequestError(response, 'marking messages as read', 'Failed to mark messages as read'));
                                    return;
                                }
                                if (!response.json) {
                                    reject(createInvalidConversationsResponseError('marking messages as read'));
                                    return;
                                }
                                var responseData = response.json;
                                resolve(responseData);
                            },
                        });
                    })];
            });
        });
    };
    /**
     * Initialize the conversations manager.
     * Always initializes persistence and event listeners for API usage.
     * Only renders the widget if widgetEnabled is true AND domain is allowed.
     */
    ConversationsManager.prototype._initialize = function () {
        var _this = this;
        if (!globals_1.document || !globals_1.window) {
            logger.info('Conversations not available: Document or window not available');
            return;
        }
        // In identity mode, restore is unnecessary and the persisted anonymous
        // ticket belongs to a different principal -- clear it before completing.
        if (this._identityFields()) {
            this._persistence.clearTicketId();
            this._completeInitialization();
            return;
        }
        var restoreToken = (0, url_utils_1.getRestoreTokenFromUrl)();
        if (restoreToken && !this._hasProcessedRestoreToken) {
            this._hasProcessedRestoreToken = true;
            // Clear the token from the URL immediately, then again after a tick and
            // after the restore completes.  SPA routers (Next.js, React Router, etc.)
            // maintain their own URL state and can overwrite a single replaceState call.
            (0, url_utils_1.clearRestoreTokenFromUrl)();
            setTimeout(url_utils_1.clearRestoreTokenFromUrl, 0);
            var generation_1 = this._lifecycleGeneration;
            this._restoreFromTokenWithRetry(restoreToken)
                .catch(function (error) {
                if (!(0, errors_1.isConversationsError)(error)) {
                    logger.warn('Failed to restore conversations from URL token', error);
                }
            })
                .finally(function () {
                (0, url_utils_1.clearRestoreTokenFromUrl)();
                if (generation_1 !== _this._lifecycleGeneration) {
                    return;
                }
                _this._completeInitialization();
            });
            return;
        }
        this._completeInitialization();
    };
    ConversationsManager.prototype._completeInitialization = function () {
        this._hasProcessedRestoreToken = true;
        // Load any existing ticket ID from localStorage
        this._currentTicketId = this._persistence.loadTicketId();
        logger.info('Loaded ticket ID from storage', { ticketId: this._currentTicketId });
        // Track conversations API loaded (separate from widget loaded)
        this._posthog.capture('$conversations_loaded', {
            hasExistingTicket: !!this._currentTicketId,
            widgetEnabled: this._isWidgetEnabled,
            domainAllowed: this._isDomainAllowed,
        });
        // Only render widget if both widgetEnabled and domain is allowed
        if (this._isWidgetEnabled && this._isDomainAllowed) {
            this._initializeWidget();
        }
        else {
            logger.info('Widget not rendered', {
                widgetEnabled: this._isWidgetEnabled,
                domainAllowed: this._isDomainAllowed,
            });
        }
        // Listen for identify events to hide identification form when user identifies
        this._setupIdentifyListener();
    };
    ConversationsManager.prototype._restoreFromTokenWithRetry = function (restoreToken) {
        return __awaiter(this, void 0, void 0, function () {
            var error_2, requestLayerHandledNetworkError;
            return __generator(this, function (_a) {
                switch (_a.label) {
                    case 0:
                        _a.trys.push([0, 2, , 4]);
                        return [4 /*yield*/, this._restoreFromToken(restoreToken)];
                    case 1: return [2 /*return*/, _a.sent()];
                    case 2:
                        error_2 = _a.sent();
                        requestLayerHandledNetworkError = (0, errors_1.isConversationsError)(error_2) && error_2.kind === 'network' && 'cause' in error_2;
                        if (!requestLayerHandledNetworkError) {
                            logger.warn('Restore token exchange failed, retrying once', error_2);
                        }
                        return [4 /*yield*/, this._restoreFromToken(restoreToken)];
                    case 3: return [2 /*return*/, _a.sent()];
                    case 4: return [2 /*return*/];
                }
            });
        });
    };
    ConversationsManager.prototype._restoreFromToken = function (restoreToken) {
        return __awaiter(this, void 0, void 0, function () {
            var token, generation, payload, data;
            var _this = this;
            var _a;
            return __generator(this, function (_b) {
                switch (_b.label) {
                    case 0:
                        token = this._config.token;
                        generation = this._lifecycleGeneration;
                        payload = {
                            restore_token: restoreToken,
                            widget_session_id: this._widgetSessionId,
                            distinct_id: this._posthog.get_distinct_id(),
                            current_url: this._currentUrl(),
                        };
                        return [4 /*yield*/, new Promise(function (resolve, reject) {
                                _this._posthog._send_request({
                                    url: _this._posthog.requestRouter.endpointFor('api', RESTORE_EXCHANGE_ENDPOINT),
                                    method: 'POST',
                                    data: payload,
                                    headers: {
                                        'X-Conversations-Token': token,
                                    },
                                    callback: function (response) {
                                        if (response.statusCode !== 200) {
                                            reject(createConversationsRequestError(response, 'restoring conversations', 'Failed to restore conversations', true, false));
                                            return;
                                        }
                                        if (!response.json) {
                                            reject(createInvalidConversationsResponseError('restoring conversations', false));
                                            return;
                                        }
                                        resolve(response.json);
                                    },
                                });
                            })];
                    case 1:
                        data = _b.sent();
                        if (data.status !== 'success') {
                            logger.info('Restore token was not accepted', { status: data.status, code: data.code });
                            return [2 /*return*/, data];
                        }
                        if (generation !== this._lifecycleGeneration) {
                            logger.info('Conversations destroyed during restore, discarding the result');
                            return [2 /*return*/, data];
                        }
                        this._lastMessageTimestamp = null;
                        this._unreadCount = 0;
                        // Apply the canonical widget_session_id from the server if provided
                        if (data.widget_session_id) {
                            this._widgetSessionId = data.widget_session_id;
                            this._persistence.setWidgetSessionId(data.widget_session_id);
                        }
                        if ((_a = data.migrated_ticket_ids) === null || _a === void 0 ? void 0 : _a.length) {
                            this._currentTicketId = data.migrated_ticket_ids[0];
                            this._persistence.saveTicketId(this._currentTicketId);
                            // Poll straight away so messages and ticket list are fresh, and resume
                            // the loop in case it had stopped while there was no conversation. At
                            // initial boot the widget isn't rendered yet — _doInitializeWidget starts
                            // the loop after restore completes, so only restart an already-live widget.
                            if (this._isWidgetRendered) {
                                this._startPolling();
                            }
                            void this._pollMessages();
                            void this._pollTickets();
                        }
                        else {
                            this._currentTicketId = null;
                            this._persistence.clearTicketId();
                        }
                        return [2 /*return*/, data];
                }
            });
        });
    };
    /**
     * Initialize and render the widget UI
     * Uses a promise guard to prevent race conditions from concurrent calls
     */
    ConversationsManager.prototype._initializeWidget = function () {
        if (this._isWidgetRendered) {
            return Promise.resolve();
        }
        if (this._initializeWidgetPromise) {
            return this._initializeWidgetPromise;
        }
        this._initializeWidgetPromise = this._doInitializeWidget();
        return this._initializeWidgetPromise;
    };
    ConversationsManager.prototype._doInitializeWidget = function () {
        return __awaiter(this, void 0, void 0, function () {
            var savedState, initialState, initialUserTraits, generation, _a, initialView, tickets;
            return __generator(this, function (_b) {
                switch (_b.label) {
                    case 0:
                        savedState = this._persistence.loadWidgetState();
                        initialState = savedState === 'open' ? 'open' : 'closed';
                        this._widgetState = initialState;
                        initialUserTraits = this._getInitialUserTraits();
                        generation = this._lifecycleGeneration;
                        return [4 /*yield*/, this._determineInitialView()
                            // destroy()/reset() ran while the tickets request was in flight
                        ];
                    case 1:
                        _a = _b.sent(), initialView = _a.view, tickets = _a.tickets;
                        // destroy()/reset() ran while the tickets request was in flight
                        if (generation !== this._lifecycleGeneration) {
                            logger.info('Conversations destroyed during widget initialization, not rendering');
                            return [2 /*return*/];
                        }
                        this._currentView = initialView;
                        // Render the widget with initial view
                        this._renderWidget(initialState, initialUserTraits, initialView, tickets);
                        this._isWidgetRendered = true;
                        this._posthog.capture('$conversations_widget_loaded', {
                            hasExistingTicket: !!this._currentTicketId,
                            initialState: initialState,
                            initialView: initialView,
                            ticketCount: tickets.length,
                            hasUserTraits: !!initialUserTraits,
                        });
                        // Start polling — the first poll fires immediately and loads messages or tickets
                        this._startPolling();
                        // Keep the widget attached across SPA navigations that replace document.body
                        this._startReattachWatcher();
                        return [2 /*return*/];
                }
            });
        });
    };
    /**
     * Extract name and email from PostHog's stored person properties.
     *
     * Person properties set via posthog.identify() are stored under the
     * $stored_person_properties persistence key, not as top-level props.
     * We check both locations plus the super-properties for completeness.
     */
    ConversationsManager.prototype._getPersonTraits = function () {
        var _a;
        var superProps = ((_a = this._posthog.persistence) === null || _a === void 0 ? void 0 : _a.props) || {};
        var storedPersonProps = this._posthog.get_property(constants_1.STORED_PERSON_PROPERTIES_KEY) || {};
        var name = storedPersonProps.$name || storedPersonProps.name || superProps.$name || superProps.name || undefined;
        var email = storedPersonProps.$email || storedPersonProps.email || superProps.$email || superProps.email || undefined;
        return { name: name, email: email };
    };
    /**
     * Get initial user traits from PostHog or localStorage
     */
    ConversationsManager.prototype._getInitialUserTraits = function () {
        var _a = this._getPersonTraits(), name = _a.name, email = _a.email;
        if (name || email) {
            return {
                name: name || undefined,
                email: email || undefined,
            };
        }
        // Otherwise, check localStorage for previously saved traits
        var savedTraits = this._persistence.loadUserTraits();
        if (savedTraits && (savedTraits.name || savedTraits.email)) {
            return savedTraits;
        }
        return null;
    };
    /**
     * Mark messages as read
     */
    ConversationsManager.prototype._markMessagesAsRead = function () {
        return __awaiter(this, void 0, void 0, function () {
            var response, error_3;
            var _a;
            return __generator(this, function (_b) {
                switch (_b.label) {
                    case 0:
                        if (!this._currentTicketId) {
                            return [2 /*return*/];
                        }
                        _b.label = 1;
                    case 1:
                        _b.trys.push([1, 3, , 4]);
                        return [4 /*yield*/, this.markAsRead(this._currentTicketId)];
                    case 2:
                        response = _b.sent();
                        this._unreadCount = response.unread_count;
                        // Update the widget to reflect the new unread count
                        (_a = this._widgetRef) === null || _a === void 0 ? void 0 : _a.setUnreadCount(0);
                        logger.info('Messages marked as read', { unreadCount: response.unread_count });
                        return [3 /*break*/, 4];
                    case 3:
                        error_3 = _b.sent();
                        if (!(0, errors_1.isConversationsError)(error_3)) {
                            logger.error('Failed to mark messages as read', error_3);
                        }
                        return [3 /*break*/, 4];
                    case 4: return [2 /*return*/];
                }
            });
        });
    };
    /**
     * Load messages for the current ticket
     */
    ConversationsManager.prototype._loadMessages = function () {
        return __awaiter(this, void 0, void 0, function () {
            var identityBefore, ticketBefore, response, lastMessage, error_4;
            var _a, _b;
            return __generator(this, function (_c) {
                switch (_c.label) {
                    case 0:
                        if (!this._currentTicketId) {
                            return [2 /*return*/];
                        }
                        _c.label = 1;
                    case 1:
                        _c.trys.push([1, 3, , 4]);
                        identityBefore = this._posthog.config.identity_distinct_id;
                        ticketBefore = this._currentTicketId;
                        return [4 /*yield*/, this.getMessages(this._currentTicketId, this._lastMessageTimestamp || undefined)
                            // Discard stale response if identity or ticket changed while in-flight
                        ];
                    case 2:
                        response = _c.sent();
                        // Discard stale response if identity or ticket changed while in-flight
                        if (this._posthog.config.identity_distinct_id !== identityBefore ||
                            this._currentTicketId !== ticketBefore) {
                            return [2 /*return*/];
                        }
                        // Update unread count from response
                        if ((0, core_1.isNumber)(response.unread_count)) {
                            this._unreadCount = response.unread_count;
                            (_a = this._widgetRef) === null || _a === void 0 ? void 0 : _a.setUnreadCount(response.unread_count);
                            // If widget is open and there are unread messages, mark them as read
                            if (response.unread_count > 0 && this._isWidgetOpen()) {
                                this._markMessagesAsRead();
                            }
                        }
                        // Sync ticket_status so an agent-side resolve flips the UI to locked state even
                        // while we're polling messages (ticket polling doesn't run in messages view).
                        // Using ticketBefore (non-null, validated by the stale check above) avoids relying
                        // on TS narrowing of the mutable this._currentTicketId field.
                        if (response.ticket_status) {
                            this._applyTicketStatusUpdate(ticketBefore, response.ticket_status);
                        }
                        if (response.messages.length > 0) {
                            (_b = this._widgetRef) === null || _b === void 0 ? void 0 : _b.addMessages(response.messages);
                            lastMessage = response.messages[response.messages.length - 1];
                            this._lastMessageTimestamp = lastMessage.created_at;
                        }
                        return [3 /*break*/, 4];
                    case 3:
                        error_4 = _c.sent();
                        if (!(0, errors_1.isConversationsError)(error_4)) {
                            logger.error('Failed to load messages', error_4);
                        }
                        return [3 /*break*/, 4];
                    case 4: return [2 /*return*/];
                }
            });
        });
    };
    ConversationsManager.prototype._isWidgetOpen = function () {
        return this._widgetState === 'open';
    };
    /**
     * Load tickets list from API
     */
    ConversationsManager.prototype._loadTickets = function () {
        return __awaiter(this, void 0, void 0, function () {
            var response, totalUnread, error_5;
            var _a, _b, _c;
            return __generator(this, function (_d) {
                switch (_d.label) {
                    case 0:
                        _d.trys.push([0, 2, , 3]);
                        return [4 /*yield*/, this.getTickets()];
                    case 1:
                        response = _d.sent();
                        this._tickets = response.results;
                        this._showTicketList = this._computeShowTicketList(response.results);
                        (_a = this._widgetRef) === null || _a === void 0 ? void 0 : _a.updateTickets(response.results, this._showTicketList);
                        totalUnread = response.results.reduce(function (sum, t) { return sum + (t.unread_count || 0); }, 0);
                        this._unreadCount = totalUnread;
                        (_b = this._widgetRef) === null || _b === void 0 ? void 0 : _b.setUnreadCount(totalUnread);
                        (_c = this._widgetRef) === null || _c === void 0 ? void 0 : _c.setCurrentTicketResolved(this._isCurrentTicketResolved());
                        logger.info('Tickets loaded', { count: response.results.length, totalUnread: totalUnread });
                        return [3 /*break*/, 3];
                    case 2:
                        error_5 = _d.sent();
                        if (!(0, errors_1.isConversationsError)(error_5)) {
                            logger.error('Failed to load tickets', error_5);
                        }
                        return [3 /*break*/, 3];
                    case 3: return [2 /*return*/];
                }
            });
        });
    };
    ConversationsManager.prototype._computeShowTicketList = function (tickets) {
        // Surface the ticket-list hub (which holds the "New conversation" button and
        // the back-to-tickets navigation) whenever the user has any ticket -- including
        // a single open one. Otherwise a user with one unresolved ticket is locked into
        // that conversation with no way to start a second one.
        return tickets.length >= 1;
    };
    ConversationsManager.prototype._isCurrentTicketResolved = function () {
        var _this = this;
        if (!this._currentTicketId) {
            return false;
        }
        var ticket = this._tickets.find(function (t) { return t.id === _this._currentTicketId; });
        return (ticket === null || ticket === void 0 ? void 0 : ticket.status) === 'resolved';
    };
    /**
     * Patch the local _tickets cache with a new status for a given ticket and push
     * any UI-relevant changes (resolved lock + list visibility) to the widget.
     */
    ConversationsManager.prototype._applyTicketStatusUpdate = function (ticketId, status) {
        var _a, _b;
        var idx = this._tickets.findIndex(function (t) { return t.id === ticketId; });
        if (idx === -1) {
            return;
        }
        if (this._tickets[idx].status === status) {
            return;
        }
        this._tickets = __spreadArray(__spreadArray(__spreadArray([], __read(this._tickets.slice(0, idx)), false), [
            __assign(__assign({}, this._tickets[idx]), { status: status })
        ], false), __read(this._tickets.slice(idx + 1)), false);
        this._showTicketList = this._computeShowTicketList(this._tickets);
        (_a = this._widgetRef) === null || _a === void 0 ? void 0 : _a.updateTickets(this._tickets, this._showTicketList);
        (_b = this._widgetRef) === null || _b === void 0 ? void 0 : _b.setCurrentTicketResolved(this._isCurrentTicketResolved());
    };
    ConversationsManager.prototype._trackPollingEndpointReachability = function (statusCode) {
        this._consecutivePollingStatusZeroFailures = (0, request_utils_1.updateStatusZeroFailureCount)(statusCode, this._consecutivePollingStatusZeroFailures, MAX_CONSECUTIVE_POLLING_STATUS_ZERO_FAILURES, function () {
            return logger.warn('Conversations polling requests are failing before receiving an HTTP response; this can happen due to network issues, CORS, browser blocking, or ad blockers. Stopped polling conversations; will try again when connectivity changes.');
        });
    };
    /**
     * Track HTTP 429s on polling requests so the loop can back off. Any 2xx
     * clears the streak; a 429 lengthens the next poll delay exponentially.
     */
    ConversationsManager.prototype._trackPollingRateLimit = function (statusCode) {
        if (statusCode === 429) {
            this._consecutivePollingRateLimitFailures += 1;
        }
        else if (statusCode >= 200 && statusCode < 300) {
            this._consecutivePollingRateLimitFailures = 0;
        }
    };
    /**
     * Determine initial view based on ticket count
     */
    ConversationsManager.prototype._determineInitialView = function () {
        return __awaiter(this, void 0, void 0, function () {
            var identityBefore, generation, response, view, error_6;
            return __generator(this, function (_a) {
                switch (_a.label) {
                    case 0:
                        _a.trys.push([0, 2, , 3]);
                        identityBefore = this._posthog.config.identity_distinct_id;
                        generation = this._lifecycleGeneration;
                        return [4 /*yield*/, this.getTickets()
                            // If identity changed while the request was in-flight, discard this
                            // stale response -- setIdentity/clearIdentity already triggered a
                            // fresh _loadTicketsAndReconcileView() with the correct credentials.
                            // Also discard it if the manager was destroyed/reset meanwhile, so the
                            // ticket id isn't written back to the storage reset() just cleared.
                        ];
                    case 1:
                        response = _a.sent();
                        // If identity changed while the request was in-flight, discard this
                        // stale response -- setIdentity/clearIdentity already triggered a
                        // fresh _loadTicketsAndReconcileView() with the correct credentials.
                        // Also discard it if the manager was destroyed/reset meanwhile, so the
                        // ticket id isn't written back to the storage reset() just cleared.
                        if (this._posthog.config.identity_distinct_id !== identityBefore ||
                            generation !== this._lifecycleGeneration) {
                            return [2 /*return*/, { view: 'messages', tickets: [] }];
                        }
                        view = this._applyTicketsToState(response.results);
                        return [2 /*return*/, { view: view, tickets: response.results }];
                    case 2:
                        error_6 = _a.sent();
                        if (!(0, errors_1.isConversationsError)(error_6)) {
                            logger.error('Failed to determine initial view', error_6);
                        }
                        return [2 /*return*/, { view: 'messages', tickets: [] }];
                    case 3: return [2 /*return*/];
                }
            });
        });
    };
    /**
     * Apply a fetched ticket list to internal state and return the appropriate view.
     * Shared by _determineInitialView (widget boot) and _loadTicketsAndReconcileView
     * (identity change at runtime).
     */
    ConversationsManager.prototype._applyTicketsToState = function (tickets) {
        this._tickets = tickets;
        this._showTicketList = this._computeShowTicketList(tickets);
        var totalUnread = tickets.reduce(function (sum, t) { return sum + (t.unread_count || 0); }, 0);
        this._unreadCount = totalUnread;
        if (tickets.length >= 2) {
            this._currentTicketId = null;
            return 'tickets';
        }
        // Single resolved ticket: show list so user can start a new conversation instead of writing to it
        if (tickets.length === 1 && tickets[0].status === 'resolved') {
            this._currentTicketId = null;
            this._persistence.clearTicketId();
            return 'tickets';
        }
        if (tickets.length === 1) {
            this._currentTicketId = tickets[0].id;
            this._persistence.saveTicketId(tickets[0].id);
        }
        return 'messages';
    };
    /**
     * Start the self-scheduling poll loop.
     *
     * Idempotent: safe to call from any state-changing handler (send, restore,
     * identity change, ticket selection). The loop polls immediately, then
     * reschedules itself with a delay derived from widget state and rate-limit
     * backoff (see _nextPollDelayMs), and stops itself when there is nothing to
     * poll (see _hasSomethingToPoll).
     */
    ConversationsManager.prototype._startPolling = function () {
        if (this._pollLoopRunning) {
            return; // Already polling
        }
        this._pollLoopRunning = true;
        var generation = ++this._pollGeneration;
        logger.info('Started polling', { view: this._currentView });
        void this._pollThenSchedule(generation);
    };
    /**
     * Schedule the next poll, or stop the loop when there is nothing to poll.
     * An idle widget with no conversation generates zero polling traffic.
     */
    ConversationsManager.prototype._scheduleNextPoll = function (generation) {
        var _this = this;
        if (!this._pollLoopRunning || generation !== this._pollGeneration) {
            return;
        }
        if (!this._hasSomethingToPoll()) {
            this._pollTimeoutId = null;
            this._pollLoopRunning = false;
            logger.info('Nothing to poll, pausing until next user action');
            return;
        }
        this._pollTimeoutId = globals_1.window === null || globals_1.window === void 0 ? void 0 : globals_1.window.setTimeout(function () { return void _this._pollThenSchedule(generation); }, this._nextPollDelayMs());
    };
    /**
     * Delay before the next poll: exponential backoff while rate limited,
     * otherwise the open/closed cadence.
     */
    ConversationsManager.prototype._nextPollDelayMs = function () {
        if (this._consecutivePollingRateLimitFailures > 0) {
            return Math.min(POLL_RATE_LIMIT_BASE_MS * Math.pow(2, (this._consecutivePollingRateLimitFailures - 1)), POLL_RATE_LIMIT_MAX_MS);
        }
        return this._isWidgetOpen() ? POLL_INTERVAL_OPEN_MS : POLL_INTERVAL_CLOSED_MS;
    };
    /**
     * Whether the current view has anything worth polling for. Messages view
     * needs an active ticket; tickets view needs at least one ticket. The
     * restore-request view never polls.
     */
    ConversationsManager.prototype._hasSomethingToPoll = function () {
        if (this._currentView === 'restore_request') {
            return false;
        }
        if (this._currentView === 'messages') {
            return !!this._currentTicketId;
        }
        return this._tickets.length > 0;
    };
    /**
     * Stop the poll loop.
     */
    ConversationsManager.prototype._stopPolling = function () {
        if (this._pollTimeoutId != null) {
            globals_1.window === null || globals_1.window === void 0 ? void 0 : globals_1.window.clearTimeout(this._pollTimeoutId);
            this._pollTimeoutId = null;
            logger.info('Stopped polling for messages');
        }
        this._pollLoopRunning = false;
        // Invalidate any in-flight poll chain so it won't reschedule itself.
        this._pollGeneration++;
    };
    /**
     * Re-attach the widget container if it has been detached from the DOM.
     *
     * SPA frameworks such as Turbo Drive (Hotwire) and Rails Turbo replace the
     * entire document.body on navigation. That removes our container — which was
     * appended to body in _renderWidget — without any teardown call, so the
     * widget silently disappears until a full page reload. Re-attaching the same
     * element (rather than re-rendering) preserves widget state and avoids
     * refetching messages.
     */
    ConversationsManager.prototype._reattachWidgetIfDetached = function () {
        // hide()/destroy() clear the container intentionally — don't fight them
        if (!this._isWidgetRendered || !this._containerElement) {
            return;
        }
        // Still in the DOM — nothing to do
        if (this._containerElement.isConnected) {
            return;
        }
        if (!(globals_1.document === null || globals_1.document === void 0 ? void 0 : globals_1.document.body)) {
            return;
        }
        globals_1.document.body.appendChild(this._containerElement);
        logger.info('Re-attached widget after it was detached from the DOM');
    };
    /**
     * Start watching for the widget being detached from the DOM (e.g. by an SPA
     * navigation that replaces document.body) and re-attach it when it happens.
     */
    ConversationsManager.prototype._startReattachWatcher = function () {
        var _this = this;
        if (this._reattachIntervalId) {
            return; // Already watching
        }
        this._reattachIntervalId = globals_1.window === null || globals_1.window === void 0 ? void 0 : globals_1.window.setInterval(function () {
            _this._reattachWidgetIfDetached();
        }, REATTACH_CHECK_INTERVAL_MS);
    };
    /**
     * Stop the DOM re-attach watcher.
     */
    ConversationsManager.prototype._stopReattachWatcher = function () {
        if (this._reattachIntervalId) {
            globals_1.window === null || globals_1.window === void 0 ? void 0 : globals_1.window.clearInterval(this._reattachIntervalId);
            this._reattachIntervalId = null;
        }
    };
    /**
     * Setup listener for identify events.
     * When user calls posthog.identify(), hide the identification form
     * since we now know who they are.
     */
    ConversationsManager.prototype._setupIdentifyListener = function () {
        var _this = this;
        this._unsubscribeIdentifyListener = this._posthog.on('eventCaptured', function (event) {
            var _a;
            if (event.event === '$identify') {
                // User just identified - hide the identification form if it's showing
                (_a = _this._widgetRef) === null || _a === void 0 ? void 0 : _a.setUserIdentified();
            }
        });
    };
    /**
     * Show the widget (render it to DOM).
     * The widget respects its saved state (open/closed).
     * Note: Domain restrictions still apply - widget won't render on disallowed domains.
     */
    ConversationsManager.prototype.show = function () {
        // Check domain restrictions - don't render on disallowed domains
        if (!this._isDomainAllowed) {
            logger.warn('Cannot show widget: current domain is not allowed');
            return;
        }
        // If widget isn't rendered yet, render it now
        if (!this._isWidgetRendered) {
            this._initializeWidget();
        }
    };
    /**
     * Hide and remove the widget from the DOM.
     * Conversation data is preserved - call show() to re-render.
     */
    ConversationsManager.prototype.hide = function () {
        // Stop polling when widget is hidden (save resources)
        this._stopPolling();
        // Stop re-attaching — the widget is being removed intentionally
        this._stopReattachWatcher();
        if (this._containerElement) {
            (0, preact_1.render)(null, this._containerElement);
            this._containerElement.remove();
            this._containerElement = null;
        }
        this._widgetRef = null;
        this._isWidgetRendered = false;
        this._initializeWidgetPromise = null;
        // Reset timestamp so show() will re-fetch all messages
        this._lastMessageTimestamp = null;
    };
    /**
     * Check if the widget is currently visible (rendered in DOM)
     */
    ConversationsManager.prototype.isVisible = function () {
        return this._isWidgetRendered;
    };
    /** Get tickets list for the current widget session or verified identity */
    ConversationsManager.prototype.getTickets = function (options) {
        return __awaiter(this, void 0, void 0, function () {
            var token, identity, queryParams;
            var _this = this;
            var _a, _b;
            return __generator(this, function (_c) {
                token = this._config.token;
                identity = this._identityFields();
                queryParams = {
                    limit: String((_a = options === null || options === void 0 ? void 0 : options.limit) !== null && _a !== void 0 ? _a : 20),
                    offset: String((_b = options === null || options === void 0 ? void 0 : options.offset) !== null && _b !== void 0 ? _b : 0),
                };
                if (identity) {
                    Object.assign(queryParams, identity);
                }
                else {
                    queryParams.widget_session_id = this._widgetSessionId;
                }
                if (options === null || options === void 0 ? void 0 : options.status) {
                    queryParams.status = options.status;
                }
                // oxlint-disable-next-line compat/compat
                return [2 /*return*/, new Promise(function (resolve, reject) {
                        _this._posthog._send_request({
                            url: _this._posthog.requestRouter.endpointFor('api', "/api/conversations/v1/widget/tickets?".concat((0, request_utils_1.formDataToQuery)(queryParams))),
                            method: 'GET',
                            timestampMode: 'query',
                            headers: {
                                'X-Conversations-Token': token,
                            },
                            callback: function (response) {
                                _this._trackPollingEndpointReachability(response.statusCode);
                                _this._trackPollingRateLimit(response.statusCode);
                                if (response.statusCode !== 200) {
                                    reject(createConversationsRequestError(response, 'fetching tickets', 'Failed to fetch tickets'));
                                    return;
                                }
                                if (!response.json) {
                                    reject(createInvalidConversationsResponseError('fetching tickets'));
                                    return;
                                }
                                var data = response.json;
                                resolve(data);
                            },
                        });
                    })];
            });
        });
    };
    ConversationsManager.prototype.requestRestoreLink = function (email) {
        return __awaiter(this, void 0, void 0, function () {
            var normalizedEmail, token, payload;
            var _this = this;
            return __generator(this, function (_a) {
                if (this._identityFields()) {
                    return [2 /*return*/, { ok: true }];
                }
                normalizedEmail = email.trim().toLowerCase();
                if (!normalizedEmail) {
                    throw new Error('Email is required');
                }
                token = this._config.token;
                payload = {
                    email: normalizedEmail,
                    request_url: this._currentUrl() || '',
                };
                // oxlint-disable-next-line compat/compat
                return [2 /*return*/, new Promise(function (resolve, reject) {
                        _this._posthog._send_request({
                            url: _this._posthog.requestRouter.endpointFor('api', RESTORE_REQUEST_ENDPOINT),
                            method: 'POST',
                            data: payload,
                            headers: {
                                'X-Conversations-Token': token,
                            },
                            callback: function (response) {
                                if (response.statusCode !== 200) {
                                    reject(createConversationsRequestError(response, 'requesting restore link', 'Failed to request restore link'));
                                    return;
                                }
                                resolve({ ok: true });
                            },
                        });
                    })];
            });
        });
    };
    ConversationsManager.prototype.restoreFromToken = function (restoreToken) {
        return __awaiter(this, void 0, void 0, function () {
            var normalizedToken;
            return __generator(this, function (_a) {
                switch (_a.label) {
                    case 0:
                        if (this._identityFields()) {
                            return [2 /*return*/, { status: 'success' }];
                        }
                        normalizedToken = restoreToken.trim();
                        if (!normalizedToken) {
                            throw new Error('Restore token is required');
                        }
                        _a.label = 1;
                    case 1:
                        _a.trys.push([1, , 3, 4]);
                        return [4 /*yield*/, this._restoreFromTokenWithRetry(normalizedToken)];
                    case 2: return [2 /*return*/, _a.sent()];
                    case 3:
                        (0, url_utils_1.clearRestoreTokenFromUrl)();
                        return [7 /*endfinally*/];
                    case 4: return [2 /*return*/];
                }
            });
        });
    };
    ConversationsManager.prototype.restoreFromUrlToken = function () {
        return __awaiter(this, void 0, void 0, function () {
            var restoreToken;
            return __generator(this, function (_a) {
                switch (_a.label) {
                    case 0:
                        if (this._identityFields()) {
                            return [2 /*return*/, null];
                        }
                        restoreToken = (0, url_utils_1.getRestoreTokenFromUrl)();
                        if (!restoreToken) {
                            return [2 /*return*/, null];
                        }
                        _a.label = 1;
                    case 1:
                        _a.trys.push([1, , 3, 4]);
                        return [4 /*yield*/, this.restoreFromToken(restoreToken)];
                    case 2: return [2 /*return*/, _a.sent()];
                    case 3:
                        (0, url_utils_1.clearRestoreTokenFromUrl)();
                        return [7 /*endfinally*/];
                    case 4: return [2 /*return*/];
                }
            });
        });
    };
    /**
     * Get the current active ticket ID
     * Returns null if no conversation has been started yet
     */
    ConversationsManager.prototype.getCurrentTicketId = function () {
        return this._currentTicketId;
    };
    /**
     * Get the widget session ID (persistent browser identifier)
     * This ID is used for access control and stays the same across page loads
     */
    ConversationsManager.prototype.getWidgetSessionId = function () {
        return this._widgetSessionId;
    };
    ConversationsManager.prototype._identityFields = function () {
        var id = this._posthog.config.identity_distinct_id;
        var hash = this._posthog.config.identity_hash;
        if (!id || !hash) {
            return null;
        }
        var fields = {
            identity_distinct_id: id,
            identity_hash: hash,
        };
        var claims = this._posthog.config.identity_claims;
        if (claims) {
            Object.entries(claims).forEach(function (_a) {
                var _b = __read(_a, 2), field = _b[0], claim = _b[1];
                var isReservedField = field === 'distinct_id' || field === 'hash' || field.startsWith('hash_');
                if (field &&
                    !isReservedField &&
                    claim &&
                    typeof claim.value === 'string' &&
                    claim.value.length > 0 &&
                    typeof claim.hash === 'string' &&
                    claim.hash.length > 0) {
                    fields["identity_".concat(field)] = claim.value;
                    fields["identity_hash_".concat(field)] = claim.hash;
                }
            });
        }
        return fields;
    };
    ConversationsManager.prototype.setIdentity = function () {
        var _a;
        this._resetConversationState();
        (_a = this._widgetRef) === null || _a === void 0 ? void 0 : _a.setIdentityMode(true);
        void this._loadTicketsAndReconcileView();
    };
    ConversationsManager.prototype.clearIdentity = function () {
        var _a;
        this._resetConversationState();
        (_a = this._widgetRef) === null || _a === void 0 ? void 0 : _a.setIdentityMode(false);
        void this._loadTicketsAndReconcileView();
    };
    ConversationsManager.prototype._resetConversationState = function () {
        var _a;
        this._currentTicketId = null;
        this._persistence.clearTicketId();
        this._lastMessageTimestamp = null;
        (_a = this._widgetRef) === null || _a === void 0 ? void 0 : _a.clearMessages(true);
    };
    ConversationsManager.prototype._loadTicketsAndReconcileView = function () {
        return __awaiter(this, void 0, void 0, function () {
            var identityBefore, response, view, error_7;
            var _a, _b, _c, _d;
            return __generator(this, function (_e) {
                switch (_e.label) {
                    case 0:
                        _e.trys.push([0, 2, , 3]);
                        identityBefore = this._posthog.config.identity_distinct_id;
                        return [4 /*yield*/, this.getTickets()];
                    case 1:
                        response = _e.sent();
                        if (this._posthog.config.identity_distinct_id !== identityBefore) {
                            return [2 /*return*/];
                        }
                        view = this._applyTicketsToState(response.results);
                        (_a = this._widgetRef) === null || _a === void 0 ? void 0 : _a.updateTickets(response.results, this._showTicketList);
                        (_b = this._widgetRef) === null || _b === void 0 ? void 0 : _b.setUnreadCount(this._unreadCount);
                        (_c = this._widgetRef) === null || _c === void 0 ? void 0 : _c.setCurrentTicketResolved(this._isCurrentTicketResolved());
                        this._currentView = view;
                        (_d = this._widgetRef) === null || _d === void 0 ? void 0 : _d.setView(view);
                        // Identity change may have surfaced (or cleared) conversations; resume
                        // the loop if it had stopped. It stops itself again if there's nothing to poll.
                        if (this._isWidgetRendered) {
                            this._startPolling();
                        }
                        if (view === 'messages' && this._currentTicketId) {
                            void this._loadMessages();
                        }
                        return [3 /*break*/, 3];
                    case 2:
                        error_7 = _e.sent();
                        if (!(0, errors_1.isConversationsError)(error_7)) {
                            logger.error('Failed to load tickets after identity change', error_7);
                        }
                        return [3 /*break*/, 3];
                    case 3: return [2 /*return*/];
                }
            });
        });
    };
    /**
     * Clean up the widget
     */
    ConversationsManager.prototype.destroy = function () {
        this._lifecycleGeneration++;
        this._stopPolling();
        this._stopReattachWatcher();
        if (globals_1.window) {
            globals_1.window.removeEventListener('online', this._onOnline);
        }
        // Unsubscribe from identify events
        if (this._unsubscribeIdentifyListener) {
            this._unsubscribeIdentifyListener();
            this._unsubscribeIdentifyListener = null;
        }
        if (this._containerElement) {
            (0, preact_1.render)(null, this._containerElement);
            this._containerElement.remove();
            this._containerElement = null;
        }
        this._widgetRef = null;
        if (_activeManager === this) {
            _activeManager = null;
        }
        logger.info('Widget destroyed');
    };
    /**
     * Reset all conversation data and destroy the widget.
     * Called on posthog.reset() to start fresh.
     */
    ConversationsManager.prototype.reset = function () {
        // Clear all persisted conversation data
        this._persistence.clearAll();
        // Reset local state
        this._currentTicketId = null;
        this._lastMessageTimestamp = null;
        this._unreadCount = 0;
        this._consecutivePollingStatusZeroFailures = 0;
        // Destroy the widget
        this.destroy();
        logger.info('Conversations reset');
    };
    /**
     * Render the widget to the DOM
     */
    ConversationsManager.prototype._renderWidget = function (initialState, initialUserTraits, initialView, initialTickets) {
        var _this = this;
        if (initialView === void 0) { initialView = 'messages'; }
        if (initialTickets === void 0) { initialTickets = []; }
        if (!globals_1.document) {
            logger.info('Conversations widget not rendered: Document not available');
            return;
        }
        // Create container if it doesn't exist
        var container = globals_1.document.getElementById(WIDGET_CONTAINER_ID);
        if (!container) {
            if (!globals_1.document.body) {
                logger.info('Conversations widget not rendered: Document body not available yet');
                return;
            }
            container = globals_1.document.createElement('div');
            container.id = WIDGET_CONTAINER_ID;
            globals_1.document.body.appendChild(container);
        }
        this._containerElement = container;
        // Render widget with ref
        (0, preact_1.render)((0, jsx_runtime_1.jsx)(ConversationsWidget_1.ConversationsWidget, { ref: function (ref) {
                _this._widgetRef = ref;
            }, config: this._config, initialState: initialState, initialUserTraits: initialUserTraits, isUserIdentified: this._posthog._isIdentified(), isIdentityMode: !(0, core_1.isNull)(this._identityFields()), initialView: initialView, initialTickets: initialTickets, showTicketList: this._showTicketList, onSendMessage: this._handleSendMessage, onStateChange: this._handleStateChange, onIdentify: this._handleIdentify, onRequestRestoreLink: this._handleRequestRestoreLink, onSelectTicket: this._handleSelectTicket, onNewConversation: this._handleNewConversation, onBackToTickets: this._handleBackToTickets, onViewChange: this._handleViewChange }), container);
    };
    return ConversationsManager;
}());
exports.ConversationsManager = ConversationsManager;
/**
 * Initialize the conversations widget.
 * This is the entry point called from the lazy-loaded bundle.
 *
 * Singleton guard: only one ConversationsManager per page. The toolbar's
 * internal PostHog instance is excluded upstream (see loadIfEnabled), so
 * this always belongs to the customer's main instance.
 */
function initConversations(config, posthog) {
    if (_activeManager) {
        return _activeManager;
    }
    _activeManager = new ConversationsManager(config, posthog);
    return _activeManager;
}
//# sourceMappingURL=index.js.map