UNPKG

posthog-js

Version:

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

605 lines • 29.8 kB
"use strict"; var __extends = (this && this.__extends) || (function () { var extendStatics = function (d, b) { extendStatics = Object.setPrototypeOf || ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; }; return extendStatics(d, b); }; return function (d, b) { if (typeof b !== "function" && b !== null) throw new TypeError("Class extends value " + String(b) + " is not a constructor or null"); extendStatics(d, b); function __() { this.constructor = d; } d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); }; })(); 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.ConversationsWidget = void 0; var jsx_runtime_1 = require("preact/jsx-runtime"); var preact_1 = require("preact"); var logger_1 = require("@posthog/browser-common/utils/logger"); var styles_1 = require("./styles"); var OpenChatButton_1 = require("./OpenChatButton"); var CloseChatButton_1 = require("./CloseChatButton"); var TicketListView_1 = require("./TicketListView"); var IdentificationFormView_1 = require("./IdentificationFormView"); var RestoreRequestView_1 = require("./RestoreRequestView"); var MessagesView_1 = require("./MessagesView"); var errors_1 = require("../errors"); var logger = (0, logger_1.createLogger)('[ConversationsWidget]'); var ConversationsWidget = /** @class */ (function (_super) { __extends(ConversationsWidget, _super); function ConversationsWidget(props) { var _this = _super.call(this, props) || this; _this._messagesEndRef = null; _this._handleToggleOpen = function () { _this.setState(function (prevState) { return ({ state: prevState.state === 'open' ? 'closed' : 'open', }); }); }; _this._handleClose = function () { _this.setState({ state: 'closed' }); }; _this._handleSelectTicket = function (ticketId) { if (_this.props.onSelectTicket) { _this.props.onSelectTicket(ticketId); } }; _this._handleNewConversation = function () { if (_this.props.onNewConversation) { _this.props.onNewConversation(); } }; _this._handleBackToTickets = function () { if (_this.props.onBackToTickets) { _this.props.onBackToTickets(); } }; _this._handleOpenRestoreRequest = function () { _this.setState(function (prevState) { var _a; return ({ view: 'restore_request', restoreEmail: prevState.restoreEmail || ((_a = prevState.userTraits) === null || _a === void 0 ? void 0 : _a.email) || '', restoreEmailError: null, restoreRequestSuccess: false, }); }); if (_this.props.onViewChange) { _this.props.onViewChange('restore_request'); } }; _this._handleCloseRestoreRequest = function () { var returnView = _this.state.showTicketList ? 'tickets' : 'messages'; _this.setState({ view: returnView, restoreEmailError: null, restoreRequestSuccess: false }); if (_this.props.onViewChange) { _this.props.onViewChange(returnView); } }; _this._handleInputChange = function (e) { var target = e.target; _this.setState({ inputValue: target.value }); }; _this._handleKeyPress = function (e) { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); _this._handleSendMessage(); } }; // Identification form handlers _this._handleFormNameChange = function (e) { var target = e.target; _this.setState({ formName: target.value }); }; _this._handleFormEmailChange = function (e) { var target = e.target; _this.setState({ formEmail: target.value, formEmailError: null }); }; _this._handleRestoreEmailChange = function (e) { var target = e.target; _this.setState({ restoreEmail: target.value, restoreEmailError: null, restoreRequestSuccess: false, }); }; _this._handleFormSubmit = function (e) { e.preventDefault(); var _a = _this.state, formEmail = _a.formEmail, formName = _a.formName; var _b = _this.props, config = _b.config, onIdentify = _b.onIdentify; // Validate email if required if (config.requireEmail && !formEmail.trim()) { _this.setState({ formEmailError: 'Email is required' }); return; } if (formEmail.trim() && !_this._validateEmail(formEmail.trim())) { _this.setState({ formEmailError: 'Please enter a valid email address' }); return; } // Create traits object var traits = {}; if (formName.trim()) { traits.name = formName.trim(); } if (formEmail.trim()) { traits.email = formEmail.trim(); } // Navigate to appropriate view after identification var nextView = _this.state.showTicketList ? 'tickets' : 'messages'; // Update state and notify parent _this.setState({ userTraits: traits, view: nextView, }); if (onIdentify) { onIdentify(traits); } if (_this.props.onViewChange) { _this.props.onViewChange(nextView); } }; _this._handleRestoreRequestSubmit = function (e) { return __awaiter(_this, void 0, void 0, function () { var email, error_1; return __generator(this, function (_a) { switch (_a.label) { case 0: e.preventDefault(); if (!this.props.onRequestRestoreLink) { return [2 /*return*/]; } email = this.state.restoreEmail.trim(); if (!email) { this.setState({ restoreEmailError: 'Email is required' }); return [2 /*return*/]; } if (!this._validateEmail(email)) { this.setState({ restoreEmailError: 'Please enter a valid email address' }); return [2 /*return*/]; } this.setState({ restoreRequestLoading: true, restoreEmailError: null, }); _a.label = 1; case 1: _a.trys.push([1, 3, , 4]); return [4 /*yield*/, this.props.onRequestRestoreLink(email)]; case 2: _a.sent(); this.setState({ restoreRequestLoading: false, restoreRequestSuccess: true, }); return [3 /*break*/, 4]; case 3: error_1 = _a.sent(); if (!(0, errors_1.isConversationsError)(error_1)) { logger.error('Failed to request restore link', error_1); } this.setState({ restoreRequestLoading: false, restoreEmailError: error_1 instanceof Error ? error_1.message : 'Failed to request restore link', }); return [3 /*break*/, 4]; case 4: return [2 /*return*/]; } }); }); }; _this._handleSendMessage = function () { return __awaiter(_this, void 0, void 0, function () { var inputValue, trimmedMessage, userMessage, error_2; return __generator(this, function (_a) { switch (_a.label) { case 0: inputValue = this.state.inputValue; trimmedMessage = inputValue.trim(); if (!trimmedMessage) { return [2 /*return*/]; } userMessage = { id: "temp-".concat(Date.now()), content: trimmedMessage, author_type: 'customer', author_name: 'You', created_at: new Date().toISOString(), is_private: false, }; this.setState({ messages: __spreadArray(__spreadArray([], __read(this.state.messages), false), [userMessage], false), inputValue: '', isLoading: true, error: null, }); _a.label = 1; case 1: _a.trys.push([1, 3, , 4]); return [4 /*yield*/, this.props.onSendMessage(trimmedMessage) // Success - message will be updated via addMessage() ]; case 2: _a.sent(); // Success - message will be updated via addMessage() this.setState({ isLoading: false }); return [3 /*break*/, 4]; case 3: error_2 = _a.sent(); // Known failures are logged once by the manager (or request layer). Only surface // unexpected callback failures here so handled errors are not captured twice. if (!(0, errors_1.isConversationsError)(error_2)) { logger.error('Failed to send message', error_2); } this.setState(function (prevState) { return ({ isLoading: false, error: error_2 instanceof Error ? error_2.message : 'Failed to send message', messages: prevState.messages.filter(function (m) { return m.id !== userMessage.id; }), }); }); return [3 /*break*/, 4]; case 4: return [2 /*return*/]; } }); }); }; var isIdentityMode = props.isIdentityMode || false; // Determine if we need to show the identification form var userTraits = props.initialUserTraits || null; var needsIdentification = _this._needsIdentification(props.config, userTraits, props.isUserIdentified, isIdentityMode); // If identification is needed, start with that view; otherwise use the provided initial view var initialView = needsIdentification ? 'identification' : props.initialView || 'messages'; _this.state = { state: props.initialState || 'closed', view: initialView, messages: [], tickets: props.initialTickets || [], ticketsLoading: false, inputValue: '', isLoading: false, error: null, formName: (userTraits === null || userTraits === void 0 ? void 0 : userTraits.name) || '', formEmail: (userTraits === null || userTraits === void 0 ? void 0 : userTraits.email) || '', formEmailError: null, userTraits: userTraits, unreadCount: 0, showTicketList: props.showTicketList || false, isCurrentTicketResolved: false, isIdentityMode: isIdentityMode, restoreEmail: (userTraits === null || userTraits === void 0 ? void 0 : userTraits.email) || '', restoreEmailError: null, restoreRequestLoading: false, restoreRequestSuccess: false, }; return _this; } /** * Check if we need to show the identification form */ ConversationsWidget.prototype._needsIdentification = function (config, traits, isUserIdentified, isIdentityMode) { // Server-verified identity mode -- identity is already established if (isIdentityMode) { return false; } // If user is already identified via PostHog, no form needed // They've called posthog.identify() so we have their identity if (isUserIdentified) { return false; } // If requireEmail is not set, no identification needed if (!config.requireEmail) { return false; } // If we already have an email, no form needed if (traits === null || traits === void 0 ? void 0 : traits.email) { return false; } return true; }; ConversationsWidget.prototype.componentDidMount = function () { // Add greeting message if no messages exist and we're in message view if (this.state.view === 'messages' && this.state.messages.length === 0 && this.props.config.greetingText) { this._addGreetingMessage(); } }; ConversationsWidget.prototype.componentDidUpdate = function (_prevProps, prevState) { // Scroll to bottom when messages change if (this.state.messages.length !== prevState.messages.length) { this._scrollToBottom(); } // Notify parent of state changes if (this.state.state !== prevState.state && this.props.onStateChange) { this.props.onStateChange(this.state.state); } // Scroll to bottom when opening if (this.state.state === 'open' && prevState.state !== 'open') { this._scrollToBottom(); } }; ConversationsWidget.prototype._addGreetingMessage = function () { var greetingMessage = { id: 'greeting', content: this.props.config.greetingText || 'Hi! How can we help?', author_type: 'AI', author_name: 'Support', created_at: new Date().toISOString(), is_private: false, }; this.setState({ messages: [greetingMessage] }); }; ConversationsWidget.prototype._scrollToBottom = function () { if (this._messagesEndRef) { this._messagesEndRef.scrollIntoView({ behavior: 'smooth' }); } }; ConversationsWidget.prototype._validateEmail = function (email) { // Basic email validation var emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; return emailRegex.test(email); }; /** * Public method to add messages from outside */ ConversationsWidget.prototype.addMessages = function (messages) { this.setState(function (prevState) { // Filter out duplicates var existingIds = new Set(prevState.messages.map(function (m) { return m.id; })); var newMessages = messages.filter(function (m) { return !existingIds.has(m.id); }); if (newMessages.length > 0) { return { messages: __spreadArray(__spreadArray([], __read(prevState.messages), false), __read(newMessages), false), }; } return null; }); }; /** * Public method to show the widget */ ConversationsWidget.prototype.show = function () { this.setState({ state: 'open' }); }; /** * Public method to hide the widget */ ConversationsWidget.prototype.hide = function () { this.setState({ state: 'closed' }); }; /** * Get user traits (either provided via form or from props) */ ConversationsWidget.prototype.getUserTraits = function () { return this.state.userTraits; }; /** * Called when user identifies via posthog.identify() * Navigates away from identification form since we now know who they are */ ConversationsWidget.prototype.setUserIdentified = function () { if (this.state.view === 'identification') { var nextView = this.state.showTicketList ? 'tickets' : 'messages'; this.setState({ view: nextView }); if (this.props.onViewChange) { this.props.onViewChange(nextView); } } }; /** * Set the unread message count (called by manager) */ ConversationsWidget.prototype.setUnreadCount = function (count) { this.setState({ unreadCount: count }); }; /** * Update the tickets list (called by manager during polling) */ ConversationsWidget.prototype.updateTickets = function (tickets, showTicketList) { this.setState({ tickets: tickets, ticketsLoading: false, showTicketList: showTicketList, }); }; /** * Set whether the current ticket (if any) is in the resolved state. * Called by the manager whenever the current ticket or tickets list changes. */ ConversationsWidget.prototype.setCurrentTicketResolved = function (resolved) { this.setState({ isCurrentTicketResolved: resolved }); }; /** * Set the current view (tickets list or messages) */ ConversationsWidget.prototype.setView = function (view) { this.setState({ view: view }); if (this.props.onViewChange) { this.props.onViewChange(view); } }; /** * Get the current view */ ConversationsWidget.prototype.getView = function () { return this.state.view; }; /** * Set tickets loading state */ ConversationsWidget.prototype.setTicketsLoading = function (loading) { this.setState({ ticketsLoading: loading }); }; /** * Update identity mode state (called by manager on setIdentity/clearIdentity) */ ConversationsWidget.prototype.setIdentityMode = function (isIdentityMode) { var _this = this; var nextView; this.setState(function (prevState) { var update = { isIdentityMode: isIdentityMode }; var viewNeedsReset = prevState.view === 'identification' || prevState.view === 'restore_request' || prevState.view === 'messages'; if (viewNeedsReset) { nextView = prevState.showTicketList ? 'tickets' : 'messages'; update.view = nextView; } return update; }, function () { if (nextView && _this.props.onViewChange) { _this.props.onViewChange(nextView); } }); }; /** * Clear messages (used when switching tickets or starting new conversation) * @param addGreeting - If true, adds the greeting message after clearing */ ConversationsWidget.prototype.clearMessages = function (addGreeting) { var _this = this; if (addGreeting === void 0) { addGreeting = false; } this.setState({ messages: [] }, function () { if (addGreeting && _this.props.config.greetingText) { _this._addGreetingMessage(); } }); }; ConversationsWidget.prototype._renderIdentificationForm = function (styles) { return ((0, jsx_runtime_1.jsx)(IdentificationFormView_1.IdentificationFormView, { config: this.props.config, styles: styles, formName: this.state.formName, formEmail: this.state.formEmail, formEmailError: this.state.formEmailError, onNameChange: this._handleFormNameChange, onEmailChange: this._handleFormEmailChange, onSubmit: this._handleFormSubmit })); }; ConversationsWidget.prototype._renderBackButton = function (styles) { var onClick = this.state.view === 'restore_request' ? this._handleCloseRestoreRequest : this._handleBackToTickets; return ((0, jsx_runtime_1.jsx)("button", { style: styles.backButton, onClick: onClick, "aria-label": "Back to conversations", children: (0, jsx_runtime_1.jsx)("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", children: (0, jsx_runtime_1.jsx)("polyline", { points: "15 18 9 12 15 6" }) }) })); }; ConversationsWidget.prototype._renderTicketList = function (styles) { var _a = this.state, tickets = _a.tickets, ticketsLoading = _a.ticketsLoading; return ((0, jsx_runtime_1.jsx)(TicketListView_1.TicketListView, { tickets: tickets, isLoading: ticketsLoading, styles: styles, onSelectTicket: this._handleSelectTicket, onNewConversation: this._handleNewConversation, onOpenRestoreRequest: this._handleOpenRestoreRequest })); }; ConversationsWidget.prototype._renderMessages = function (styles, primaryColor, placeholderText) { var _this = this; return ((0, jsx_runtime_1.jsx)(MessagesView_1.MessagesView, { styles: styles, primaryColor: primaryColor, placeholderText: placeholderText, messages: this.state.messages, inputValue: this.state.inputValue, isLoading: this.state.isLoading, error: this.state.error, isResolved: this.state.isCurrentTicketResolved, onInputChange: this._handleInputChange, onKeyDown: this._handleKeyPress, onSendMessage: this._handleSendMessage, onStartNewConversation: this._handleNewConversation, messagesEndRef: function (el) { _this._messagesEndRef = el; } })); }; ConversationsWidget.prototype._renderRestoreRequestView = function (styles) { return ((0, jsx_runtime_1.jsx)(RestoreRequestView_1.RestoreRequestView, { styles: styles, restoreEmail: this.state.restoreEmail, restoreEmailError: this.state.restoreEmailError, restoreRequestLoading: this.state.restoreRequestLoading, restoreRequestSuccess: this.state.restoreRequestSuccess, onEmailChange: this._handleRestoreEmailChange, onSubmit: this._handleRestoreRequestSubmit })); }; /** * Get the title for the current view */ ConversationsWidget.prototype._getTitle = function (view) { switch (view) { case 'tickets': return 'Conversations'; case 'restore_request': return 'Restore conversations'; case 'identification': return 'Support chat'; case 'messages': return 'Support chat'; } }; /** * Render the content for the current view */ ConversationsWidget.prototype._renderViewContent = function (styles, primaryColor, placeholderText) { switch (this.state.view) { case 'identification': return this._renderIdentificationForm(styles); case 'restore_request': return this._renderRestoreRequestView(styles); case 'tickets': return this._renderTicketList(styles); case 'messages': return this._renderMessages(styles, primaryColor, placeholderText); } }; ConversationsWidget.prototype.render = function () { var config = this.props.config; var _a = this.state, state = _a.state, view = _a.view; var primaryColor = config.color || '#5375ff'; var widgetPosition = config.widgetPosition || 'bottom_right'; var placeholderText = config.placeholderText || 'Type your message...'; var styles = (0, styles_1.getStyles)(primaryColor, widgetPosition); // Button only (closed state) if (state === 'closed') { return ((0, jsx_runtime_1.jsx)(OpenChatButton_1.OpenChatButton, { primaryColor: primaryColor, position: widgetPosition, handleToggleOpen: this._handleToggleOpen, unreadCount: this.state.unreadCount })); } // Open state var windowStyle = __assign(__assign({}, styles.window), styles.windowOpen); // Show back button in message view when the ticket list is available, or in restore request view var showBackButton = (view === 'messages' && this.state.showTicketList) || view === 'restore_request'; // Show recover footer only in tickets and messages views, and not in identity mode var showRecoverFooter = !this.state.isIdentityMode && (view === 'tickets' || view === 'messages'); return ((0, jsx_runtime_1.jsx)("div", { style: styles.widget, children: (0, jsx_runtime_1.jsxs)("div", { style: windowStyle, children: [(0, jsx_runtime_1.jsxs)("div", { style: styles.header, children: [(0, jsx_runtime_1.jsxs)("div", { style: showBackButton ? styles.headerWithBack : styles.headerTitle, children: [showBackButton && this._renderBackButton(styles), (0, jsx_runtime_1.jsx)("span", { style: styles.headerTitle, children: this._getTitle(view) })] }), (0, jsx_runtime_1.jsx)("div", { style: styles.headerActions, children: (0, jsx_runtime_1.jsx)(CloseChatButton_1.CloseChatButton, { primaryColor: primaryColor, handleClose: this._handleClose }) })] }), this._renderViewContent(styles, primaryColor, placeholderText), showRecoverFooter && ((0, jsx_runtime_1.jsxs)("div", { style: styles.recoverFooter, children: ["Don't see your previous tickets?", ' ', (0, jsx_runtime_1.jsx)("button", { type: "button", style: styles.recoverFooterLink, onClick: this._handleOpenRestoreRequest, children: "Recover them here" })] }))] }) })); }; return ConversationsWidget; }(preact_1.Component)); exports.ConversationsWidget = ConversationsWidget; //# sourceMappingURL=ConversationsWidget.js.map