quala-widget
Version:
Smart feedback widget for SaaS trial optimization
1,357 lines (1,356 loc) • 48.7 kB
JavaScript
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