UNPKG

quala-widget

Version:

Smart feedback widget for SaaS trial optimization

1,357 lines (1,356 loc) 48.7 kB
var __defProp = Object.defineProperty; var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value); import { useState, useRef, useCallback, useEffect, createElement } from "react"; import { jsxs, jsx, Fragment } from "react/jsx-runtime"; const styles = { // Color palette (cupcake theme inspired) colors: { primary: "#65c3c8", base200: "#efeae6", base300: "#e7e2df", success: "#87d039", successContent: "#ffffff" }, // Component styles widget: { fontFamily: "system-ui, -apple-system, sans-serif" }, button: { base: { display: "inline-flex", alignItems: "center", justifyContent: "center", padding: "12px 24px", borderRadius: "12px", border: "none", fontSize: "14px", fontWeight: "600", cursor: "pointer", transition: "all 0.3s cubic-bezier(0.4, 0, 0.2, 1)", textDecoration: "none", outline: "none", boxShadow: "0 2px 4px rgba(0, 0, 0, 0.05)" }, primary: { backgroundColor: "#65c3c8", color: "#ffffff" }, primaryHover: { backgroundColor: "#5ab5ba", transform: "translateY(-1px)", boxShadow: "0 4px 8px rgba(0, 0, 0, 0.1)" }, ghost: { backgroundColor: "transparent", color: "#291334", border: "1px solid #e7e2df" }, ghostHover: { backgroundColor: "#efeae6", transform: "translateY(-1px)", boxShadow: "0 2px 4px rgba(0, 0, 0, 0.05)" }, small: { padding: "8px 12px", fontSize: "12px", minWidth: "auto", width: "32px", height: "32px", borderRadius: "50%", boxShadow: "none" }, loading: { opacity: 0.7, cursor: "not-allowed", transform: "none" } }, modal: { backdrop: { position: "fixed", top: 0, left: 0, right: 0, bottom: 0, backgroundColor: "rgba(0, 0, 0, 0.5)", display: "flex", alignItems: "center", justifyContent: "center", zIndex: 1e3, opacity: 0, visibility: "hidden", transition: "all 0.3s cubic-bezier(0.4, 0, 0.2, 1)", backdropFilter: "blur(4px)" }, backdropOpen: { opacity: 1, visibility: "visible" }, box: { backgroundColor: "#faf7f5", borderRadius: "24px", padding: "32px", width: "100%", maxWidth: "448px", maxHeight: "90vh", overflowY: "auto", position: "relative", boxShadow: "0 25px 50px -12px rgba(0, 0, 0, 0.25)", margin: "20px", transform: "scale(0.95) translateY(10px)", transition: "all 0.3s cubic-bezier(0.4, 0, 0.2, 1)" }, boxOpen: { transform: "scale(1) translateY(0)" } }, input: { base: { width: "100%", padding: "14px 16px", borderRadius: "12px", border: "2px solid #e7e2df", fontSize: "16px", backgroundColor: "#ffffff", color: "#291334", transition: "all 0.2s ease", outline: "none", boxShadow: "0 1px 2px rgba(0, 0, 0, 0.05)" }, focus: { borderColor: "#65c3c8", boxShadow: "0 0 0 4px rgba(101, 195, 200, 0.15)", transform: "translateY(-1px)" }, error: { borderColor: "#e74c3c", boxShadow: "0 0 0 4px rgba(231, 76, 60, 0.15)" }, textarea: { resize: "none", minHeight: "100px", fontFamily: "inherit", lineHeight: "1.5" } }, progress: { track: { width: "100%", height: "8px", backgroundColor: "#efeae6", borderRadius: "4px", overflow: "hidden", boxShadow: "inset 0 1px 2px rgba(0, 0, 0, 0.05)" }, bar: { height: "100%", backgroundColor: "#65c3c8", borderRadius: "4px", transition: "width 0.5s cubic-bezier(0.4, 0, 0.2, 1)", boxShadow: "0 1px 2px rgba(0, 0, 0, 0.1)" } }, rating: { container: { display: "flex", gap: "12px", justifyContent: "center", alignItems: "center", padding: "8px 0" }, star: { width: "40px", height: "40px", cursor: "pointer", transition: "all 0.2s cubic-bezier(0.4, 0, 0.2, 1)", border: "none", background: "none", fontSize: "28px", color: "#e7e2df", display: "flex", alignItems: "center", justifyContent: "center" }, starFilled: { color: "#eeaf3a", transform: "scale(1.1)" }, starHover: { transform: "scale(1.2)", color: "#eeaf3a" } }, radio: { container: { display: "flex", alignItems: "center", gap: "12px", padding: "16px", borderRadius: "12px", border: "2px solid #e7e2df", backgroundColor: "#ffffff", cursor: "pointer", transition: "all 0.2s cubic-bezier(0.4, 0, 0.2, 1)", boxShadow: "0 1px 2px rgba(0, 0, 0, 0.05)" }, containerSelected: { borderColor: "#65c3c8", backgroundColor: "rgba(101, 195, 200, 0.05)", transform: "translateY(-1px)", boxShadow: "0 4px 8px rgba(0, 0, 0, 0.1)" }, containerHover: { borderColor: "rgba(101, 195, 200, 0.5)", backgroundColor: "#efeae6", transform: "translateY(-1px)", boxShadow: "0 2px 4px rgba(0, 0, 0, 0.05)" }, input: { width: "20px", height: "20px", accentColor: "#65c3c8" } }, alert: { error: { display: "flex", alignItems: "center", gap: "12px", padding: "16px", borderRadius: "12px", backgroundColor: "rgba(231, 76, 60, 0.1)", border: "1px solid rgba(231, 76, 60, 0.2)", color: "#e74c3c", fontSize: "14px", boxShadow: "0 2px 4px rgba(0, 0, 0, 0.05)" } }, text: { title: { fontSize: "28px", fontWeight: "700", color: "#291334", marginBottom: "12px", lineHeight: "1.2" }, label: { fontSize: "16px", fontWeight: "500", color: "#291334", marginBottom: "8px", display: "block", lineHeight: "1.4" }, small: { fontSize: "14px", color: "rgba(41, 19, 52, 0.7)", lineHeight: "1.5" }, error: { fontSize: "14px", color: "#e74c3c", marginTop: "6px", display: "flex", alignItems: "center", gap: "6px" }, required: { color: "#e74c3c", marginLeft: "2px" } } }; const QualaWidget = ({ triggerText = "Start Trial", title = "Before you start your trial...", questions, onSubmit, onClose, onCancel, open: controlledOpen, onComplete, modalStyle = {} }) => { const isControlled = controlledOpen !== void 0; const [internalOpen, setInternalOpen] = useState(false); const open = isControlled ? controlledOpen : internalOpen; const [answers, setAnswers] = useState({}); const [errors, setErrors] = useState({}); const [loading, setLoading] = useState(false); const [success, setSuccess] = useState(false); const [currentStep, setCurrentStep] = useState(0); const inputRef = useRef(null); const textareaRef = useRef(null); const adjustTextareaHeight = useCallback(() => { if (textareaRef.current) { textareaRef.current.style.height = "auto"; textareaRef.current.style.height = `${textareaRef.current.scrollHeight}px`; } }, []); useEffect(() => { var _a, _b; if (open) { const currentQuestion = questions[currentStep]; if ((currentQuestion == null ? void 0 : currentQuestion.type) === "textarea") { (_a = textareaRef.current) == null ? void 0 : _a.focus(); } else if ((currentQuestion == null ? void 0 : currentQuestion.type) === "text") { (_b = inputRef.current) == null ? void 0 : _b.focus(); } adjustTextareaHeight(); } }, [open, currentStep, adjustTextareaHeight]); useEffect(() => { adjustTextareaHeight(); }, [answers, adjustTextareaHeight]); useEffect(() => { const handleEscape = (e) => { if (e.key === "Escape" && open) handleClose(); }; window.addEventListener("keydown", handleEscape); return () => window.removeEventListener("keydown", handleEscape); }, [open]); const handleChange = (id, value) => { setAnswers((prev) => ({ ...prev, [id]: value })); setErrors((prev) => ({ ...prev, [id]: "" })); const currentQuestion = questions[currentStep]; if ((currentQuestion == null ? void 0 : currentQuestion.id) === id && (currentQuestion.type === "rating" || currentQuestion.type === "boolean")) { setTimeout(() => { if (currentStep < questions.length - 1) { setCurrentStep(currentStep + 1); } }, 800); } }; const validate = () => { const newErrors = {}; questions.forEach(({ id, text, required, type = "text" }) => { const answer = answers[id]; if (required && (!answer || (type === "text" || type === "textarea") && !answer.trim())) { newErrors[id] = `${text} is required`; } }); setErrors(newErrors); return Object.keys(newErrors).length === 0; }; const handleSubmit = async () => { if (!validate()) return; setLoading(true); try { await onSubmit(answers); setSuccess(true); } catch { setErrors({ form: "Submission failed. Please try again." }); } finally { setLoading(false); } }; const handleClose = () => { if (!isControlled) setInternalOpen(false); success ? onComplete == null ? void 0 : onComplete({ answers }) : onCancel == null ? void 0 : onCancel(); setAnswers({}); setErrors({}); setLoading(false); setSuccess(false); setCurrentStep(0); onClose == null ? void 0 : onClose(); }; const handleOpen = () => { if (!isControlled) setInternalOpen(true); }; const progress = (currentStep + 1) / questions.length * 100; const handleStepAdvance = useCallback( (stepIndex) => { const currentQuestion = questions[stepIndex]; const currentAnswer = answers[currentQuestion.id]; const questionType = currentQuestion.type || "text"; if (currentQuestion.required && (!currentAnswer || (questionType === "text" || questionType === "textarea") && !currentAnswer.trim())) { setErrors((prev) => ({ ...prev, [currentQuestion.id]: `${currentQuestion.text} is required` })); return; } setErrors((prev) => ({ ...prev, [currentQuestion.id]: "" })); setCurrentStep(stepIndex + 1); }, [questions, answers] ); const getInputFocusStyle = () => ({ ...styles.input.focus, ...modalStyle.inputFocusColor && { borderColor: modalStyle.inputFocusColor, boxShadow: `0 0 0 3px ${modalStyle.inputFocusColor}20` } }); const createButtonStyle = (variant = "primary", isHovered = false, isLoading = false) => { const baseStyle = { ...styles.button.base, ...variant === "primary" && styles.button.primary, ...variant === "ghost" && styles.button.ghost, ...variant === "small" && styles.button.small, ...isHovered && variant === "primary" && styles.button.primaryHover, ...isHovered && variant === "ghost" && styles.button.ghostHover, ...isLoading && styles.button.loading }; if (variant === "primary" && modalStyle.buttonColor) { baseStyle.backgroundColor = modalStyle.buttonColor; } if (variant === "ghost" && modalStyle.backButtonColor) { baseStyle.backgroundColor = modalStyle.backButtonColor; } return baseStyle; }; return /* @__PURE__ */ jsxs("div", { style: styles.widget, children: [ !isControlled && /* @__PURE__ */ jsx( "button", { onClick: handleOpen, style: { ...createButtonStyle("primary"), ...modalStyle.triggerColor && { backgroundColor: modalStyle.triggerColor } }, onMouseEnter: (e) => { Object.assign(e.currentTarget.style, createButtonStyle("primary", true)); }, onMouseLeave: (e) => { Object.assign(e.currentTarget.style, createButtonStyle("primary", false)); }, children: triggerText } ), /* @__PURE__ */ jsx( "div", { style: { ...styles.modal.backdrop, ...open && styles.modal.backdropOpen, ...modalStyle.modalColor && { backgroundColor: modalStyle.modalColor } }, onClick: (e) => { if (e.target === e.currentTarget) { handleClose(); } }, children: /* @__PURE__ */ jsxs( "div", { style: { ...styles.modal.box, ...open && styles.modal.boxOpen, ...modalStyle.modalBoxColor && { backgroundColor: modalStyle.modalBoxColor } }, children: [ /* @__PURE__ */ jsx( "button", { onClick: handleClose, style: createButtonStyle("small"), onMouseEnter: (e) => { e.currentTarget.style.backgroundColor = styles.colors.base200; }, onMouseLeave: (e) => { e.currentTarget.style.backgroundColor = "transparent"; }, "aria-label": "Close modal", children: "✕" } ), success ? /* @__PURE__ */ jsxs("div", { style: { textAlign: "center", padding: "32px 0" }, children: [ /* @__PURE__ */ jsxs("div", { style: { marginBottom: "24px" }, children: [ /* @__PURE__ */ jsx("div", { style: { width: "64px", height: "64px", backgroundColor: styles.colors.success, color: styles.colors.successContent, borderRadius: "50%", display: "flex", alignItems: "center", justifyContent: "center", margin: "0 auto 16px", fontSize: "32px" }, children: "✓" }), /* @__PURE__ */ jsx("h3", { style: styles.text.title, children: "Thank you!" }), /* @__PURE__ */ jsx("p", { style: styles.text.small, children: "We'll get back to you shortly." }) ] }), /* @__PURE__ */ jsx( "button", { style: createButtonStyle("primary"), onClick: handleClose, onMouseEnter: (e) => { Object.assign(e.currentTarget.style, createButtonStyle("primary", true)); }, onMouseLeave: (e) => { Object.assign(e.currentTarget.style, createButtonStyle("primary", false)); }, children: "Close" } ) ] }) : /* @__PURE__ */ jsxs(Fragment, { children: [ /* @__PURE__ */ jsxs("div", { style: { marginBottom: "24px" }, children: [ /* @__PURE__ */ jsx("h3", { style: styles.text.title, children: title }), /* @__PURE__ */ jsx( "div", { style: { ...styles.progress.track, ...modalStyle.progressTrackColor && { backgroundColor: modalStyle.progressTrackColor } }, children: /* @__PURE__ */ jsx( "div", { style: { ...styles.progress.bar, width: `${progress}%`, ...modalStyle.progressBarColor && { backgroundColor: modalStyle.progressBarColor } } } ) } ), /* @__PURE__ */ jsxs("p", { style: { ...styles.text.small, marginTop: "8px" }, children: [ "Step ", currentStep + 1, " of ", questions.length ] }) ] }), /* @__PURE__ */ jsxs( "form", { onSubmit: (e) => { e.preventDefault(); handleSubmit(); }, style: { display: "flex", flexDirection: "column", gap: "24px" }, children: [ questions.map(({ id, text, required, type = "text", options, placeholder }, index) => /* @__PURE__ */ jsxs( "div", { style: { opacity: index === currentStep ? 1 : 0, display: index === currentStep ? "block" : "none", transition: "opacity 0.3s ease" }, children: [ /* @__PURE__ */ jsxs("label", { style: styles.text.label, children: [ text, " ", required && /* @__PURE__ */ jsx("span", { style: styles.text.required, children: "*" }) ] }), type === "text" && /* @__PURE__ */ jsx( "input", { ref: index === currentStep ? inputRef : null, type: "text", placeholder: placeholder || "Your answer...", style: { ...styles.input.base, ...errors[id] && styles.input.error }, value: answers[id] || "", onChange: (e) => handleChange(id, e.target.value), onFocus: (e) => { Object.assign(e.currentTarget.style, getInputFocusStyle()); }, onBlur: (e) => { if (!errors[id]) { e.currentTarget.style.borderColor = styles.colors.base300; e.currentTarget.style.boxShadow = "none"; } }, onKeyDown: (e) => { if (e.key === "Enter") { e.preventDefault(); if (currentStep < questions.length - 1) { handleStepAdvance(index); } else { handleSubmit(); } } }, required } ), type === "textarea" && /* @__PURE__ */ jsx( "textarea", { ref: index === currentStep ? textareaRef : null, placeholder: placeholder || "Tell us more...", style: { ...styles.input.base, ...styles.input.textarea, ...errors[id] && styles.input.error, maxHeight: "200px", overflow: "hidden" }, value: answers[id] || "", onChange: (e) => { handleChange(id, e.target.value); adjustTextareaHeight(); }, onFocus: (e) => { Object.assign(e.currentTarget.style, getInputFocusStyle()); }, onBlur: (e) => { if (!errors[id]) { e.currentTarget.style.borderColor = styles.colors.base300; e.currentTarget.style.boxShadow = "none"; } }, onKeyDown: (e) => { if ((e.ctrlKey || e.metaKey) && e.key === "Enter") { e.preventDefault(); if (currentStep < questions.length - 1) { handleStepAdvance(index); } else { handleSubmit(); } } }, required } ), type === "rating" && /* @__PURE__ */ jsxs("div", { style: { display: "flex", flexDirection: "column", gap: "12px" }, children: [ /* @__PURE__ */ jsx("div", { style: styles.rating.container, children: [1, 2, 3, 4, 5].map((rating) => /* @__PURE__ */ jsx( "button", { type: "button", style: { ...styles.rating.star, ...answers[id] && parseInt(answers[id]) >= rating && styles.rating.starFilled }, onClick: () => handleChange(id, rating.toString()), onMouseEnter: (e) => { Object.assign(e.currentTarget.style, styles.rating.starHover); }, onMouseLeave: (e) => { e.currentTarget.style.transform = "scale(1)"; }, children: "★" }, rating )) }), /* @__PURE__ */ jsxs("div", { style: { display: "flex", justifyContent: "space-between", ...styles.text.small }, children: [ /* @__PURE__ */ jsx("span", { children: "Very Poor" }), /* @__PURE__ */ jsx("span", { children: "Excellent" }) ] }), answers[id] && /* @__PURE__ */ jsxs("div", { style: { textAlign: "center", ...styles.text.small, fontWeight: "500" }, children: [ "Rating: ", answers[id], "/5" ] }) ] }), type === "multiple_choice" && options && /* @__PURE__ */ jsx("div", { style: { display: "flex", flexDirection: "column", gap: "12px" }, children: options.map((option, optIndex) => /* @__PURE__ */ jsxs( "label", { style: { ...styles.radio.container, ...answers[id] === option && styles.radio.containerSelected }, onMouseEnter: (e) => { if (answers[id] !== option) { Object.assign(e.currentTarget.style, styles.radio.containerHover); } }, onMouseLeave: (e) => { if (answers[id] !== option) { e.currentTarget.style.borderColor = styles.colors.base300; e.currentTarget.style.backgroundColor = "#ffffff"; } }, children: [ /* @__PURE__ */ jsx( "input", { type: "radio", name: `choice-${id}`, style: styles.radio.input, checked: answers[id] === option, onChange: () => handleChange(id, option) } ), /* @__PURE__ */ jsx("span", { style: { flex: 1, fontWeight: "500" }, children: option }) ] }, optIndex )) }), type === "boolean" && /* @__PURE__ */ jsxs("div", { style: { display: "flex", gap: "16px" }, children: [ /* @__PURE__ */ jsxs("label", { style: { ...styles.radio.container, flex: 1, justifyContent: "center", ...answers[id] === "true" && styles.radio.containerSelected }, children: [ /* @__PURE__ */ jsx( "input", { type: "radio", name: `bool-${id}`, style: styles.radio.input, checked: answers[id] === "true", onChange: () => handleChange(id, "true") } ), /* @__PURE__ */ jsx("span", { style: { fontWeight: "500" }, children: "Yes" }) ] }), /* @__PURE__ */ jsxs("label", { style: { ...styles.radio.container, flex: 1, justifyContent: "center", ...answers[id] === "false" && styles.radio.containerSelected }, children: [ /* @__PURE__ */ jsx( "input", { type: "radio", name: `bool-${id}`, style: styles.radio.input, checked: answers[id] === "false", onChange: () => handleChange(id, "false") } ), /* @__PURE__ */ jsx("span", { style: { fontWeight: "500" }, children: "No" }) ] }) ] }), errors[id] && /* @__PURE__ */ jsx("div", { style: styles.text.error, children: errors[id] }) ] }, id )), errors.form && /* @__PURE__ */ jsxs("div", { style: styles.alert.error, children: [ /* @__PURE__ */ jsx("span", { children: "⚠" }), /* @__PURE__ */ jsx("span", { children: errors.form }) ] }), /* @__PURE__ */ jsxs("div", { style: { display: "flex", justifyContent: "space-between", gap: "16px", marginTop: "32px" }, children: [ currentStep > 0 && /* @__PURE__ */ jsx( "button", { type: "button", style: createButtonStyle("ghost"), onClick: () => setCurrentStep(currentStep - 1), onMouseEnter: (e) => { Object.assign(e.currentTarget.style, createButtonStyle("ghost", true)); }, onMouseLeave: (e) => { Object.assign(e.currentTarget.style, createButtonStyle("ghost", false)); }, children: "Back" } ), currentStep < questions.length - 1 ? /* @__PURE__ */ jsx( "button", { type: "button", style: createButtonStyle("primary"), onClick: (e) => { e.preventDefault(); handleStepAdvance(currentStep); }, onMouseEnter: (e) => { Object.assign(e.currentTarget.style, createButtonStyle("primary", true)); }, onMouseLeave: (e) => { Object.assign(e.currentTarget.style, createButtonStyle("primary", false)); }, children: "Next" } ) : /* @__PURE__ */ jsx( "button", { type: "submit", style: createButtonStyle("primary", false, loading), disabled: loading, onMouseEnter: (e) => { if (!loading) { Object.assign(e.currentTarget.style, createButtonStyle("primary", true)); } }, onMouseLeave: (e) => { if (!loading) { Object.assign(e.currentTarget.style, createButtonStyle("primary", false)); } }, children: loading ? "Submitting..." : "Submit" } ) ] }) ] } ) ] }), /* @__PURE__ */ jsxs("div", { style: { textAlign: "center", ...styles.text.small, marginTop: "24px", fontSize: "12px" }, children: [ "Powered by ", /* @__PURE__ */ jsx( "a", { href: "https://getquala.xyz", target: "_blank", rel: "noopener noreferrer", style: { textDecoration: "underline", color: styles.colors.primary }, children: "Quala" } ) ] }) ] } ) } ) ] }); }; const _ReactRenderer = class _ReactRenderer { constructor() { __publicField(this, "createRootFn", null); __publicField(this, "legacyRender", null); __publicField(this, "isReady", false); __publicField(this, "debug", false); } static getInstance() { if (!_ReactRenderer.instance) { _ReactRenderer.instance = new _ReactRenderer(); } return _ReactRenderer.instance; } setDebug(debug) { this.debug = debug; } log(message, ...args) { if (this.debug) { console.log(message, ...args); } } async initialize() { var _a, _b; if (this.isReady) return; try { const ReactDOMClient = require("react-dom/client"); if (ReactDOMClient && ReactDOMClient.createRoot) { this.createRootFn = ReactDOMClient.createRoot; this.isReady = true; this.log("[Quala] Using direct require for react-dom/client"); return; } } catch (e) { } try { const ReactDOMClient = await import("./client-DFJurY5z.js").then((n) => n.c); this.createRootFn = ReactDOMClient.createRoot || ((_a = ReactDOMClient.default) == null ? void 0 : _a.createRoot); if (this.createRootFn) { this.isReady = true; this.log("[Quala] Using dynamic import for react-dom/client"); return; } } catch (e) { } try { const ReactDOM = require("react-dom"); if (ReactDOM && ReactDOM.render) { this.legacyRender = ReactDOM; this.isReady = true; this.log("[Quala] Using legacy ReactDOM.render"); return; } } catch (e) { } try { const ReactDOM = await import("react-dom"); const render = ReactDOM.render || ((_b = ReactDOM.default) == null ? void 0 : _b.render); if (render) { this.legacyRender = ReactDOM.default || ReactDOM; this.isReady = true; this.log("[Quala] Using dynamic import for legacy ReactDOM"); return; } } catch (e) { console.error("[Quala] All React DOM loading methods failed"); } } createRoot(container) { if (!this.isReady) { throw new Error("[Quala] Renderer not ready. Call initialize() first."); } if (this.createRootFn) { return this.createRootFn(container); } else if (this.legacyRender) { return { render: (element) => { this.legacyRender.render(element, container); }, unmount: () => { if (this.legacyRender.unmountComponentAtNode) { this.legacyRender.unmountComponentAtNode(container); } } }; } else { throw new Error("[Quala] No React rendering method available"); } } isInitialized() { return this.isReady; } }; __publicField(_ReactRenderer, "instance"); let ReactRenderer = _ReactRenderer; class QualaSdkClass { constructor() { __publicField(this, "isInitialized", false); __publicField(this, "apiKey", ""); __publicField(this, "apiBaseUrl", "https://www.getquala.xyz/api/widget"); __publicField(this, "sessionToken", null); __publicField(this, "currentQuestionGroupId", null); __publicField(this, "currentTriggerId", null); __publicField(this, "container"); __publicField(this, "root"); __publicField(this, "customStyles"); __publicField(this, "renderer", ReactRenderer.getInstance()); __publicField(this, "debug", false); } log(message, ...args) { if (this.debug) { console.log(message, ...args); } } async initialize(apiKey, modalStyle, options) { this.apiKey = apiKey; this.isInitialized = true; this.customStyles = modalStyle; this.debug = (options == null ? void 0 : options.debug) || false; this.renderer.setDebug(this.debug); if (options == null ? void 0 : options.apiBaseUrl) { this.apiBaseUrl = options.apiBaseUrl; } this.log("[Quala] SDK initialized with API URL:", this.apiBaseUrl); if (typeof window !== "undefined") { await this.renderer.initialize(); const pendingUser = localStorage.getItem("quala:pending-user"); if (pendingUser) { const user = JSON.parse(pendingUser); localStorage.removeItem("quala:pending-user"); this.identifyUser(user); } } } getApiUrl() { return this.apiBaseUrl; } setApiUrl(newUrl) { if (!this.isInitialized) { console.warn("[Quala] SDK not initialized yet"); return; } this.apiBaseUrl = newUrl; this.log("[Quala] API URL updated to:", this.apiBaseUrl); } validateKey() { return this.isInitialized && this.apiKey.length > 0; } async identifyUser(user) { const userTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone; if (!this.isInitialized) { console.warn("Quala not initialized yet. User will be identified after initialization."); if (typeof window !== "undefined") { localStorage.setItem("quala:pending-user", JSON.stringify(user)); } return; } try { const response = await fetch(`${this.apiBaseUrl}/init`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ apiKey: this.apiKey, userId: user.userId, email: user.email, trialStartDate: user.trialStartDate || (/* @__PURE__ */ new Date()).toISOString(), userTimezone }) }); if (!response.ok) { throw new Error(`Init API failed: ${response.status}`); } const data = await response.json(); this.log("Init response", data); this.sessionToken = data.sessionToken; this.currentQuestionGroupId = data.questionGroupId || null; this.currentTriggerId = data.triggerId || null; if (typeof window !== "undefined") { localStorage.setItem("quala:session", this.sessionToken); } this.log("[Quala] User identified, ready for dashboard-controlled feedback"); this.trackEvent("user_identified", { userId: user.userId, email: user.email }); } catch (error) { console.error("Quala: Failed to identify user", error); } } trackEvent(eventName, properties) { if (!this.validateKey()) { console.warn("Quala SDK not initialized or invalid API key"); return; } if (this.sessionToken) { fetch(`${this.apiBaseUrl}/track`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ sessionToken: this.sessionToken, event: this.mapEventName(eventName), data: properties || {} }) }).catch((error) => { console.error("[Quala] Track event failed:", error); }); } this.log(`[Quala] Track event: ${eventName}`, properties); } mapEventName(eventName) { if (eventName === "page_visit" || eventName === "time_update") { return eventName; } if (eventName === "milestone_reached") { return "milestone_reached"; } return "feature_used"; } async triggerSmartFeedback() { if (!this.validateKey() || !this.sessionToken) { console.warn("Quala SDK not initialized or no active session"); return; } const triggerInfo = await this.shouldShowFeedback(); if (triggerInfo.shouldShow) { const delay = triggerInfo.delayMs || 0; setTimeout(async () => { await this.showDashboardWidget(); this.trackEvent("feedback_widget_triggered", { trigger: "automated", questionGroupId: this.currentQuestionGroupId, trigerId: this.currentTriggerId, delayMs: delay }); }, delay); } } async shouldShowFeedback() { if (!this.sessionToken) { console.warn("Quala: No active session for triggering"); return { shouldShow: false }; } try { const response = await fetch(`${this.apiBaseUrl}/check-trigger`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ sessionToken: this.sessionToken }) }); if (!response.ok) { console.error("[Quala] Failed to check trigger conditions"); return { shouldShow: false }; } const data = await response.json(); this.currentQuestionGroupId = data.questionGroupId || null; this.currentTriggerId = data.triggerId || null; return { shouldShow: data.shouldShow, delayMs: data.delayMs }; } catch (error) { console.error("[Quala] Error checking if should show feedback:", error); return { shouldShow: false }; } } async showDashboardWidget() { if (!this.sessionToken || !this.currentQuestionGroupId) { console.warn("[Quala] No session or question group available"); return; } await this.showWidget(); } async showWidget(options) { if (!this.validateKey()) { console.warn("Quala SDK not initialized or invalid API key"); return; } if (!this.sessionToken || !this.currentQuestionGroupId) { console.warn("[Quala] No active session. Call identifyUser() first."); return; } try { const response = await fetch( `${this.apiBaseUrl}/questions?sessionToken=${this.sessionToken}&questionGroupId=${this.currentQuestionGroupId}` ); if (!response.ok) { throw new Error(`Questions API failed: ${response.status}`); } const data = await response.json(); if (!data.questions || data.questions.length === 0) { this.log("[Quala] No questions configured in dashboard"); return; } const title = "We'd love your feedback"; await this.renderWidget(title, data.questions, options == null ? void 0 : options.onComplete, options == null ? void 0 : options.onCancel); } catch (error) { console.error("[Quala] Failed to load widget:", error); } } async renderWidget(title, questions, onComplete, onCancel) { if (this.container) return; if (typeof window === "undefined") { console.warn("[Quala] Cannot render widget on server side"); return; } if (!this.renderer.isInitialized()) { await this.renderer.initialize(); } try { if (this.currentTriggerId) { this.markTriggerShown(); } this.container = document.createElement("div"); document.body.appendChild(this.container); this.root = this.renderer.createRoot(this.container); const handleClose = () => { if (!this.root || !this.container) return; try { this.root.unmount(); } catch (e) { console.warn("[Quala] Unmount failed:", e); } this.container.remove(); this.container = void 0; this.root = void 0; }; const handleSubmit = async (answers) => { try { await this.submitAnswers(answers); if (onComplete) { onComplete({ answers }); } } catch (error) { console.error("Quala: Submission failed", error); } }; const handleCancel = () => { if (onCancel) { onCancel(); } handleClose(); }; const widgetQuestions = questions.map((q) => ({ id: q.id, text: q.text, required: q.required, type: q.type, options: q.options, placeholder: q.placeholder })); const element = createElement(QualaWidget, { open: true, title, questions: widgetQuestions, onSubmit: handleSubmit, onCancel: handleCancel, onComplete, modalStyle: this.customStyles, onClose: handleClose }); this.root.render(element); this.trackEvent("feedback_widget_shown", { trigger: "manual", questionCount: questions.length }); this.log("[Quala] Widget rendered successfully"); } catch (error) { console.error("[Quala] Failed to render widget:", error); if (this.container) { this.container.remove(); this.container = void 0; this.root = void 0; } } } async markTriggerShown() { if (!this.sessionToken || !this.currentTriggerId) { console.warn("[Quala] Missing session token or trigger ID"); return; } try { const response = await fetch(`${this.apiBaseUrl}/mark-trigger`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ sessionToken: this.sessionToken, triggerId: this.currentTriggerId }) }); if (!response.ok) { console.error("[Quala] Failed to mark trigger as shown"); return; } this.log("[Quala] Trigger marked as shown"); } catch (error) { console.error("[Quala] Failed to mark trigger as shown:", error); } } async submitAnswers(answers) { if (!this.sessionToken || !this.currentQuestionGroupId) { console.error("[Quala] Missing session or question group for submission"); return; } try { const response = await fetch(`${this.apiBaseUrl}/submit`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ sessionToken: this.sessionToken, answers, questionGroupId: this.currentQuestionGroupId, triggerId: this.currentTriggerId, triggerType: "auto" }) }); if (!response.ok) { throw new Error(`Submit API failed: ${response.status}`); } const data = await response.json(); if (data.success) { if (typeof window !== "undefined") { localStorage.setItem("quala:lastResponse", Date.now().toString()); } this.trackEvent("feedback_flow_complete"); this.log("[Quala] Feedback submitted successfully"); } else { throw new Error(data.message || "Submission failed"); } } catch (error) { console.error("[Quala] Failed to submit feedback:", error); } } getSessionInfo() { return { sessionToken: this.sessionToken, questionGroupId: this.currentQuestionGroupId, triggerId: this.currentTriggerId, isInitialized: this.isInitialized, apiBaseUrl: this.apiBaseUrl, debug: this.debug }; } async checkTriggerAvailable() { const triggerInfo = await this.shouldShowFeedback(); return triggerInfo.shouldShow; } async refreshTriggerStatus() { if (!this.sessionToken) return; try { const triggerInfo = await this.shouldShowFeedback(); if (triggerInfo.shouldShow) { this.log("[Quala] Trigger now available after refresh"); return true; } } catch (error) { console.error("[Quala] Failed to refresh trigger status:", error); } return false; } destroy() { if (this.container && this.root) { try { this.root.unmount(); } catch (e) { console.warn("[Quala] Destroy failed:", e); } this.container.remove(); } this.isInitialized = false; this.sessionToken = null; this.currentQuestionGroupId = null; this.currentTriggerId = null; this.debug = false; if (typeof window !== "undefined") { localStorage.removeItem("quala:session"); localStorage.removeItem("quala:pending-user"); localStorage.removeItem("quala:lastResponse"); } } } const qualaSdk = new QualaSdkClass(); function useQuala() { const identifyUser = (user) => { qualaSdk.identifyUser(user); }; const trackEvent = (event, props = {}) => { qualaSdk.trackEvent(event, props); }; const triggerSmartFeedback = async () => { try { await qualaSdk.triggerSmartFeedback(); } catch (e) { console.error("Quala: Trigger feedback error", e); } }; return { identifyUser, trackEvent, triggerSmartFeedback }; } const useQualaInit = ({ apiKey, modalStyle, options }) => { useEffect(() => { qualaSdk.initialize(apiKey, modalStyle, options); return () => { qualaSdk.destroy(); }; }, [apiKey, options == null ? void 0 : options.apiBaseUrl, options == null ? void 0 : options.debug]); }; const VERSION = "1.0.0"; const Quala = { sdk: qualaSdk, // Preferred: Quala.sdk qualaSdk, // Alternative: Quala.qualaSdk useQuala, useQualaInit, QualaWidget, VERSION }; export { QualaWidget, VERSION, Quala as default, qualaSdk, useQuala, useQualaInit }; //# sourceMappingURL=quala.es.js.map