@feedvalue/core 0.1.8 → 0.1.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +207 -11
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +179 -3
- package/dist/index.d.ts +179 -3
- package/dist/index.js +207 -12
- package/dist/index.js.map +1 -1
- package/dist/umd/index.min.js +3 -3
- package/dist/umd/index.min.js.map +1 -1
- package/package.json +1 -1
package/dist/umd/index.min.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"use strict";var FeedValueCore=(()=>{var v=Object.defineProperty;var _=Object.getOwnPropertyDescriptor;var V=Object.getOwnPropertyNames;var I=Object.prototype.hasOwnProperty;var D=(o,e,t)=>e in o?v(o,e,{enumerable:!0,configurable:!0,writable:!0,value:t}):o[e]=t;var R=(o,e)=>{for(var t in e)v(o,t,{get:e[t],enumerable:!0})},A=(o,e,t,i)=>{if(e&&typeof e=="object"||typeof e=="function")for(let s of V(e))!I.call(o,s)&&s!==t&&v(o,s,{get:()=>e[s],enumerable:!(i=_(e,s))||i.enumerable});return o};var B=o=>A(v({},"__esModule",{value:!0}),o);var n=(o,e,t)=>D(o,typeof e!="symbol"?e+"":e,t);var O={};R(O,{ApiClient:()=>f,DEFAULT_API_BASE_URL:()=>p,FeedValue:()=>x,TypedEventEmitter:()=>h,clearFingerprint:()=>S,generateFingerprint:()=>b});var h=class{constructor(){n(this,"listeners",new Map)}on(e,t){this.listeners.has(e)||this.listeners.set(e,new Set),this.listeners.get(e).add(t)}once(e,t){let i=((...s)=>{this.off(e,i),t(...s)});this.on(e,i)}off(e,t){if(!t){this.listeners.delete(e);return}let i=this.listeners.get(e);i&&(i.delete(t),i.size===0&&this.listeners.delete(e))}emit(e,...t){let i=this.listeners.get(e);if(i)for(let s of i)try{s(...t)}catch(r){console.error(`[FeedValue] Error in ${e} handler:`,r)}}removeAllListeners(){this.listeners.clear()}};var p="https://api.feedvalue.com";var f=class{constructor(e=p,t=!1){n(this,"baseUrl");n(this,"debug");n(this,"pendingRequests",new Map);n(this,"configCache",new Map);n(this,"submissionToken",null);n(this,"tokenExpiresAt",null);n(this,"fingerprint",null);this.baseUrl=e.replace(/\/$/,""),this.debug=t}validateWidgetId(e){if(!e||typeof e!="string")throw new Error("Widget ID is required");if(!/^[a-zA-Z0-9_-]+$/.test(e))throw new Error("Invalid widget ID format: only alphanumeric characters, underscores, and hyphens are allowed");if(e.length>64)throw new Error("Widget ID exceeds maximum length of 64 characters")}setFingerprint(e){this.fingerprint=e}getFingerprint(){return this.fingerprint}hasValidToken(){return!this.submissionToken||!this.tokenExpiresAt?!1:Date.now()/1e3<this.tokenExpiresAt-30}async fetchConfig(e,t=!1){this.validateWidgetId(e);let i=`config:${e}`;if(t)this.log("Bypassing cache for token refresh",{widgetId:e});else{let l=this.configCache.get(i);if(l&&Date.now()<l.expiresAt)return this.log("Config cache hit",{widgetId:e}),l.data}let s=`fetchConfig:${e}`,r=this.pendingRequests.get(s);if(r)return this.log("Deduplicating config request",{widgetId:e}),r;let d=this.doFetchConfig(e);this.pendingRequests.set(s,d);try{return await d}finally{this.pendingRequests.delete(s)}}async doFetchConfig(e){let t=`${this.baseUrl}/api/v1/widgets/${e}/config`,i={};this.fingerprint&&(i["X-Client-Fingerprint"]=this.fingerprint),this.log("Fetching config",{widgetId:e,url:t,hasFingerprint:!!this.fingerprint,fingerprintPreview:this.fingerprint?`${this.fingerprint.substring(0,8)}...`:null});let s=await fetch(t,{method:"GET",headers:i});if(!s.ok){let l=await this.parseError(s);throw new Error(l)}let r=await s.json();r.submission_token?(this.submissionToken=r.submission_token,this.tokenExpiresAt=r.token_expires_at??null,this.log("Submission token stored",{expiresAt:this.tokenExpiresAt?new Date(this.tokenExpiresAt*1e3).toISOString():"unknown"})):this.log("No submission token in response",{hasWidgetId:!!r.widget_id,hasConfig:!!r.config,responseKeys:Object.keys(r)});let d=`config:${e}`;return this.configCache.set(d,{data:r,expiresAt:Date.now()+3e5}),this.log("Config fetched",{widgetId:e}),r}async submitFeedback(e,t,i){this.validateWidgetId(e);let s=`${this.baseUrl}/api/v1/widgets/${e}/feedback`;if(this.hasValidToken()||(this.log("Token expired, refreshing..."),await this.fetchConfig(e,!0)),!this.submissionToken)throw new Error("No submission token available");let r={"Content-Type":"application/json","X-Submission-Token":this.submissionToken};this.fingerprint&&(r["X-Client-Fingerprint"]=this.fingerprint),this.log("Submitting feedback",{widgetId:e});let d={...t.metadata,...i&&Object.keys(i).length>0&&{user:i}},l=await fetch(s,{method:"POST",headers:r,body:JSON.stringify({message:t.message,metadata:Object.keys(d).length>0?d:void 0,...t.customFieldValues&&{customFieldValues:t.customFieldValues}})});if(l.status===429){let a=l.headers.get("X-RateLimit-Reset"),g=a?Math.ceil(parseInt(a,10)-Date.now()/1e3):60;throw new Error(`Rate limited. Try again in ${g} seconds.`)}if(l.status===403){let a=await l.json().catch(()=>({detail:"Access denied"}));if(a.detail?.code&&a.detail?.message)throw new Error(a.detail.message);let g=typeof a.detail=="string"?a.detail:"";if((g.includes("token")||g.includes("expired"))&&(this.log("Token rejected, refreshing..."),this.submissionToken=null,await this.fetchConfig(e,!0),this.submissionToken)){r["X-Submission-Token"]=this.submissionToken;let u=await fetch(s,{method:"POST",headers:r,body:JSON.stringify({message:t.message,metadata:t.metadata,...t.customFieldValues&&{customFieldValues:t.customFieldValues},...i&&Object.keys(i).length>0&&{user:i}})});if(u.ok)return u.json()}throw new Error(g||"Access denied")}if(!l.ok){let a=await this.parseError(l);throw new Error(a)}let c=await l.json();if(c.blocked)throw new Error(c.message||"Unable to submit feedback");return this.log("Feedback submitted",{feedbackId:c.feedback_id}),c}async parseError(e){try{let t=await e.json();return t.detail||t.message||t.error||`HTTP ${e.status}`}catch{return`HTTP ${e.status}: ${e.statusText}`}}clearCache(){this.configCache.clear(),this.submissionToken=null,this.tokenExpiresAt=null,this.log("Cache cleared")}log(e,t){this.debug&&console.log(`[FeedValue API] ${e}`,t??"")}};var w="fv_fingerprint";function E(){if(typeof crypto>"u"||typeof crypto.getRandomValues!="function")throw new Error("crypto.getRandomValues is required but not available. Ensure you are running in a modern browser or Node.js 15+.");let o=new Uint8Array(16);return crypto.getRandomValues(o),Array.from(o).map(e=>e.toString(16).padStart(2,"0")).join("")}function b(){if(typeof window>"u"||typeof sessionStorage>"u")return E();let o=sessionStorage.getItem(w);if(o){if(o.includes("-")){let t=o.replace(/-/g,"");try{sessionStorage.setItem(w,t)}catch{}return t}return o}let e=E();try{sessionStorage.setItem(w,e)}catch{}return e}function S(){if(typeof sessionStorage<"u")try{sessionStorage.removeItem(w)}catch{}}var U=3e3,k=["angry","disappointed","satisfied","excited"],T=1e4,F=1e3,M={theme:"auto",autoShow:!0,debug:!1,locale:"en"},y=new Map,m=class m{constructor(e){n(this,"widgetId");n(this,"apiClient");n(this,"emitter");n(this,"headless");n(this,"config");n(this,"widgetConfig",null);n(this,"state",{isReady:!1,isOpen:!1,isVisible:!0,error:null,isSubmitting:!1});n(this,"stateSubscribers",new Set);n(this,"stateSnapshot");n(this,"_userData",{});n(this,"_userId",null);n(this,"_userTraits",{});n(this,"triggerButton",null);n(this,"modal",null);n(this,"overlay",null);n(this,"stylesInjected",!1);n(this,"autoCloseTimeout",null);n(this,"isDestroyed",!1);this.widgetId=e.widgetId,this.headless=e.headless??!1,this.config={...M,...e.config},this.apiClient=new f(e.apiBaseUrl??p,this.config.debug),this.emitter=new h,this.stateSnapshot={...this.state},this.log("Instance created",{widgetId:this.widgetId,headless:this.headless})}static init(e){let t=y.get(e.widgetId);if(t)return t;let i=new m(e);return y.set(e.widgetId,i),i.init().catch(s=>{console.error("[FeedValue] Initialization failed:",s)}),i}static getInstance(e){return y.get(e)}async init(){if(this.state.isReady){this.log("Already initialized");return}if(this.isDestroyed){this.log("Instance destroyed before init could complete");return}try{this.log("Initializing...");let e=b();this.apiClient.setFingerprint(e);let t=await this.apiClient.fetchConfig(this.widgetId);if(this.isDestroyed){this.log("Instance destroyed during config fetch, aborting init");return}this.widgetConfig={widgetId:t.widget_id,widgetKey:t.widget_key,appId:"",config:{position:t.config.position??"bottom-right",triggerText:t.config.triggerText??"Feedback",triggerIcon:t.config.triggerIcon??"none",formTitle:t.config.formTitle??"Share your feedback",submitButtonText:t.config.submitButtonText??"Submit",thankYouMessage:t.config.thankYouMessage??"Thank you for your feedback!",showBranding:t.config.showBranding??!0,customFields:t.config.customFields},styling:{primaryColor:t.styling.primaryColor??"#3b82f6",backgroundColor:t.styling.backgroundColor??"#ffffff",textColor:t.styling.textColor??"#1f2937",buttonTextColor:t.styling.buttonTextColor??"#ffffff",borderRadius:t.styling.borderRadius??"8px",customCSS:t.styling.customCSS}},!this.headless&&typeof window<"u"&&typeof document<"u"&&this.renderWidget(),this.updateState({isReady:!0,error:null}),this.emitter.emit("ready"),this.log("Initialized successfully")}catch(e){let t=e instanceof Error?e:new Error(String(e));throw this.updateState({error:t}),this.emitter.emit("error",t),t}}destroy(){this.log("Destroying..."),this.isDestroyed=!0,this.autoCloseTimeout&&(clearTimeout(this.autoCloseTimeout),this.autoCloseTimeout=null),this.triggerButton?.remove(),this.modal?.remove(),this.overlay?.remove(),document.getElementById("fv-widget-styles")?.remove(),document.getElementById("fv-widget-custom-styles")?.remove(),this.triggerButton=null,this.modal=null,this.overlay=null,this.widgetConfig=null,this.stateSubscribers.clear(),this.emitter.removeAllListeners(),this.apiClient.clearCache(),y.delete(this.widgetId),this.state={isReady:!1,isOpen:!1,isVisible:!1,error:null,isSubmitting:!1},this.log("Destroyed")}open(){if(!this.state.isReady){this.log("Cannot open: not ready");return}this.updateState({isOpen:!0}),this.headless||(this.overlay?.classList.add("fv-widget-open"),this.modal?.classList.add("fv-widget-open")),this.emitter.emit("open"),this.log("Opened")}close(){this.updateState({isOpen:!1}),this.headless||(this.overlay?.classList.remove("fv-widget-open"),this.modal?.classList.remove("fv-widget-open")),this.emitter.emit("close"),this.log("Closed")}toggle(){this.state.isOpen?this.close():this.open()}show(){this.updateState({isVisible:!0}),!this.headless&&this.triggerButton&&(this.triggerButton.style.display=""),this.log("Shown")}hide(){this.updateState({isVisible:!1}),!this.headless&&this.triggerButton&&(this.triggerButton.style.display="none"),this.log("Hidden")}isOpen(){return this.state.isOpen}isVisible(){return this.state.isVisible}isReady(){return this.state.isReady}isHeadless(){return this.headless}setData(e){this._userData={...this._userData,...e},this.log("User data set",e)}identify(e,t){this._userId=e,t&&(this._userTraits={...this._userTraits,...t}),this.log("User identified",{userId:e,traits:t})}reset(){this._userData={},this._userId=null,this._userTraits={},this.log("User data reset")}getUserData(){return{userId:this._userId,data:{...this._userData},traits:{...this._userTraits}}}async submit(e){if(!this.state.isReady)throw new Error("Widget not ready");this.validateFeedback(e),this.updateState({isSubmitting:!0});try{let t={message:e.message,sentiment:e.sentiment,customFieldValues:e.customFieldValues,metadata:{page_url:typeof window<"u"?window.location.href:"",referrer:typeof document<"u"?document.referrer:void 0,user_agent:typeof navigator<"u"?navigator.userAgent:void 0,...e.metadata}},i=this.buildSubmissionUserData();await this.apiClient.submitFeedback(this.widgetId,t,i),this.emitter.emit("submit",t),this.log("Feedback submitted",i?{withUserData:!0}:void 0)}catch(t){let i=t instanceof Error?t:new Error(String(t));throw this.emitter.emit("error",i),i}finally{this.updateState({isSubmitting:!1})}}buildSubmissionUserData(){let e=this._userId!==null,t=Object.keys(this._userData).length>0,i=Object.keys(this._userTraits).length>0;if(!e&&!t&&!i)return;let s={};this._userId&&(s.user_id=this._userId);let r=this._userData.email??this._userTraits.email,d=this._userData.name??this._userTraits.name;if(r&&(s.email=r),d&&(s.name=d),i){let{email:l,name:c,...a}=this._userTraits;Object.keys(a).length>0&&(s.traits=a)}if(t){let{email:l,name:c,...a}=this._userData,g={};for(let[u,C]of Object.entries(a))C!==void 0&&(g[u]=C);Object.keys(g).length>0&&(s.custom_data=g)}return s}validateFeedback(e){if(!e.message?.trim())throw new Error("Feedback message is required");if(e.message.length>T)throw new Error(`Feedback message exceeds maximum length of ${T} characters`);if(e.sentiment!==void 0&&!k.includes(e.sentiment))throw new Error(`Invalid sentiment value. Must be one of: ${k.join(", ")}`);if(e.customFieldValues){for(let[t,i]of Object.entries(e.customFieldValues))if(typeof t!="string"||typeof i!="string")throw new Error("Custom field values must be strings")}if(e.metadata){for(let[t,i]of Object.entries(e.metadata))if(typeof i=="string"&&i.length>F)throw new Error(`Metadata field "${t}" exceeds maximum length of ${F} characters`)}}on(e,t){this.emitter.on(e,t)}once(e,t){this.emitter.once(e,t)}off(e,t){this.emitter.off(e,t)}async waitUntilReady(){if(!this.state.isReady){if(this.state.error)throw this.state.error;return new Promise((e,t)=>{this.once("ready",()=>e()),this.once("error",i=>t(i))})}}setConfig(e){this.config={...this.config,...e},this.log("Config updated",e)}getConfig(){return{...this.config}}getWidgetConfig(){return this.widgetConfig}subscribe(e){return this.stateSubscribers.add(e),()=>{this.stateSubscribers.delete(e)}}getSnapshot(){return this.stateSnapshot}updateState(e){this.state={...this.state,...e},this.stateSnapshot={...this.state},this.emitter.emit("stateChange",this.stateSnapshot);for(let t of this.stateSubscribers)t()}renderWidget(){this.widgetConfig&&(this.stylesInjected||(this.injectStyles(),this.stylesInjected=!0),this.renderTrigger(),this.renderModal())}sanitizeCSS(e){let t=[/url\s*\(/gi,/@import/gi,/expression\s*\(/gi,/javascript:/gi,/behavior\s*:/gi,/-moz-binding/gi];for(let i of t)if(i.test(e))return console.warn("[FeedValue] Blocked potentially unsafe CSS pattern"),"";return e}injectStyles(){if(!this.widgetConfig)return;let{styling:e,config:t}=this.widgetConfig,i=document.createElement("style");if(i.id="fv-widget-styles",i.textContent=this.getBaseStyles(e,t.position),document.head.appendChild(i),e.customCSS){let s=this.sanitizeCSS(e.customCSS);if(s){let r=document.createElement("style");r.id="fv-widget-custom-styles",r.textContent=s,document.head.appendChild(r)}}}getBaseStyles(e,t){let i=this.getPositionStyles(t),s=this.getModalPositionStyles(t);return`
|
|
1
|
+
"use strict";var FeedValueCore=(()=>{var b=Object.defineProperty;var _=Object.getOwnPropertyDescriptor;var U=Object.getOwnPropertyNames;var V=Object.prototype.hasOwnProperty;var I=(d,e,t)=>e in d?b(d,e,{enumerable:!0,configurable:!0,writable:!0,value:t}):d[e]=t;var D=(d,e)=>{for(var t in e)b(d,t,{get:e[t],enumerable:!0})},O=(d,e,t,i)=>{if(e&&typeof e=="object"||typeof e=="function")for(let o of U(e))!V.call(d,o)&&o!==t&&b(d,o,{get:()=>e[o],enumerable:!(i=_(e,o))||i.enumerable});return d};var A=d=>O(b({},"__esModule",{value:!0}),d);var a=(d,e,t)=>I(d,typeof e!="symbol"?e+"":e,t);var B={};D(B,{ApiClient:()=>p,DEFAULT_API_BASE_URL:()=>f,FeedValue:()=>x,NEGATIVE_OPTIONS_MAP:()=>R,TypedEventEmitter:()=>h,clearFingerprint:()=>k,generateFingerprint:()=>v});var h=class{constructor(){a(this,"listeners",new Map)}on(e,t){this.listeners.has(e)||this.listeners.set(e,new Set),this.listeners.get(e).add(t)}once(e,t){let i=((...o)=>{this.off(e,i),t(...o)});this.on(e,i)}off(e,t){if(!t){this.listeners.delete(e);return}let i=this.listeners.get(e);i&&(i.delete(t),i.size===0&&this.listeners.delete(e))}emit(e,...t){let i=this.listeners.get(e);if(i)for(let o of i)try{o(...t)}catch(s){console.error(`[FeedValue] Error in ${e} handler:`,s)}}removeAllListeners(){this.listeners.clear()}};var f="https://api.feedvalue.com";var p=class{constructor(e=f,t=!1){a(this,"baseUrl");a(this,"debug");a(this,"pendingRequests",new Map);a(this,"configCache",new Map);a(this,"submissionToken",null);a(this,"tokenExpiresAt",null);a(this,"fingerprint",null);this.baseUrl=e.replace(/\/$/,""),this.debug=t}validateWidgetId(e){if(!e||typeof e!="string")throw new Error("Widget ID is required");if(!/^[a-zA-Z0-9_-]+$/.test(e))throw new Error("Invalid widget ID format: only alphanumeric characters, underscores, and hyphens are allowed");if(e.length>64)throw new Error("Widget ID exceeds maximum length of 64 characters")}setFingerprint(e){this.fingerprint=e}getFingerprint(){return this.fingerprint}hasValidToken(){return!this.submissionToken||!this.tokenExpiresAt?!1:Date.now()/1e3<this.tokenExpiresAt-30}async fetchConfig(e,t=!1){this.validateWidgetId(e);let i=`config:${e}`;if(t)this.log("Bypassing cache for token refresh",{widgetId:e});else{let n=this.configCache.get(i);if(n&&Date.now()<n.expiresAt)return this.log("Config cache hit",{widgetId:e}),n.data}let o=`fetchConfig:${e}`,s=this.pendingRequests.get(o);if(s)return this.log("Deduplicating config request",{widgetId:e}),s;let r=this.doFetchConfig(e);this.pendingRequests.set(o,r);try{return await r}finally{this.pendingRequests.delete(o)}}async doFetchConfig(e){let t=`${this.baseUrl}/api/v1/widgets/${e}/config`,i={};this.fingerprint&&(i["X-Client-Fingerprint"]=this.fingerprint),this.log("Fetching config",{widgetId:e,url:t,hasFingerprint:!!this.fingerprint,fingerprintPreview:this.fingerprint?`${this.fingerprint.substring(0,8)}...`:null});let o=await fetch(t,{method:"GET",headers:i});if(!o.ok){let n=await this.parseError(o);throw new Error(n)}let s=await o.json();s.submission_token?(this.submissionToken=s.submission_token,this.tokenExpiresAt=s.token_expires_at??null,this.log("Submission token stored",{expiresAt:this.tokenExpiresAt?new Date(this.tokenExpiresAt*1e3).toISOString():"unknown"})):this.log("No submission token in response",{hasWidgetId:!!s.widget_id,hasConfig:!!s.config,responseKeys:Object.keys(s)});let r=`config:${e}`;return this.configCache.set(r,{data:s,expiresAt:Date.now()+3e5}),this.log("Config fetched",{widgetId:e}),s}async submitFeedback(e,t,i){this.validateWidgetId(e);let o=`${this.baseUrl}/api/v1/widgets/${e}/feedback`;if(this.hasValidToken()||(this.log("Token expired, refreshing..."),await this.fetchConfig(e,!0)),!this.submissionToken)throw new Error("No submission token available");let s={"Content-Type":"application/json","X-Submission-Token":this.submissionToken};this.fingerprint&&(s["X-Client-Fingerprint"]=this.fingerprint),this.log("Submitting feedback",{widgetId:e});let r={...t.metadata,...i&&Object.keys(i).length>0&&{user:i}},n=await fetch(o,{method:"POST",headers:s,body:JSON.stringify({message:t.message,metadata:Object.keys(r).length>0?r:void 0,...t.customFieldValues&&{customFieldValues:t.customFieldValues}})});if(n.status===429){let l=n.headers.get("X-RateLimit-Reset"),c=l?Math.ceil(parseInt(l,10)-Date.now()/1e3):60;throw new Error(`Rate limited. Try again in ${c} seconds.`)}if(n.status===403){let l=await n.json().catch(()=>({detail:"Access denied"}));if(l.detail?.code&&l.detail?.message)throw new Error(l.detail.message);let c=typeof l.detail=="string"?l.detail:"";if((c.includes("token")||c.includes("expired"))&&(this.log("Token rejected, refreshing..."),this.submissionToken=null,await this.fetchConfig(e,!0),this.submissionToken)){s["X-Submission-Token"]=this.submissionToken;let u=await fetch(o,{method:"POST",headers:s,body:JSON.stringify({message:t.message,metadata:t.metadata,...t.customFieldValues&&{customFieldValues:t.customFieldValues},...i&&Object.keys(i).length>0&&{user:i}})});if(u.ok)return u.json()}throw new Error(c||"Access denied")}if(!n.ok){let l=await this.parseError(n);throw new Error(l)}let g=await n.json();if(g.blocked)throw new Error(g.message||"Unable to submit feedback");return this.log("Feedback submitted",{feedbackId:g.feedback_id}),g}async submitReaction(e,t){this.validateWidgetId(e);let i=`${this.baseUrl}/api/v1/widgets/${e}/react`;if(this.hasValidToken()||(this.log("Token expired, refreshing..."),await this.fetchConfig(e,!0)),!this.submissionToken)throw new Error("No submission token available");let o={"Content-Type":"application/json","X-Submission-Token":this.submissionToken};this.fingerprint&&(o["X-Client-Fingerprint"]=this.fingerprint),this.log("Submitting reaction",{widgetId:e,value:t.value});let s=await fetch(i,{method:"POST",headers:o,body:JSON.stringify({value:t.value,followUp:t.followUp,metadata:t.metadata})});if(s.status===429){let n=s.headers.get("X-RateLimit-Reset"),g=n?Math.ceil(parseInt(n,10)-Date.now()/1e3):60;throw new Error(`Rate limited. Try again in ${g} seconds.`)}if(s.status===403){let n=await s.json().catch(()=>({detail:"Access denied"}));if(n.detail?.code&&n.detail?.message)throw new Error(n.detail.message);let g=typeof n.detail=="string"?n.detail:"";if((g.includes("token")||g.includes("expired"))&&(this.log("Token rejected, refreshing..."),this.submissionToken=null,await this.fetchConfig(e,!0),this.submissionToken)){o["X-Submission-Token"]=this.submissionToken;let l=await fetch(i,{method:"POST",headers:o,body:JSON.stringify({value:t.value,followUp:t.followUp,metadata:t.metadata})});if(l.ok)return l.json()}throw new Error(g||"Access denied")}if(s.status===400){let n=await this.parseError(s);throw new Error(n)}if(!s.ok){let n=await this.parseError(s);throw new Error(n)}let r=await s.json();if(r.blocked)throw new Error(r.message||"Unable to submit reaction");return this.log("Reaction submitted",{submissionId:r.submission_id}),r}async parseError(e){try{let t=await e.json();return t.detail||t.message||t.error||`HTTP ${e.status}`}catch{return`HTTP ${e.status}: ${e.statusText}`}}clearCache(){this.configCache.clear(),this.submissionToken=null,this.tokenExpiresAt=null,this.log("Cache cleared")}log(e,t){this.debug&&console.log(`[FeedValue API] ${e}`,t??"")}};var w="fv_fingerprint";function E(){if(typeof crypto>"u"||typeof crypto.getRandomValues!="function")throw new Error("crypto.getRandomValues is required but not available. Ensure you are running in a modern browser or Node.js 15+.");let d=new Uint8Array(16);return crypto.getRandomValues(d),Array.from(d).map(e=>e.toString(16).padStart(2,"0")).join("")}function v(){if(typeof window>"u"||typeof sessionStorage>"u")return E();let d=sessionStorage.getItem(w);if(d){if(d.includes("-")){let t=d.replace(/-/g,"");try{sessionStorage.setItem(w,t)}catch{}return t}return d}let e=E();try{sessionStorage.setItem(w,e)}catch{}return e}function k(){if(typeof sessionStorage<"u")try{sessionStorage.removeItem(w)}catch{}}var P=3e3,S=["angry","disappointed","satisfied","excited"],T=1e4,F=1e3,M={theme:"auto",autoShow:!0,debug:!1,locale:"en"},y=new Map,m=class m{constructor(e){a(this,"widgetId");a(this,"apiClient");a(this,"emitter");a(this,"headless");a(this,"config");a(this,"widgetConfig",null);a(this,"state",{isReady:!1,isOpen:!1,isVisible:!0,error:null,isSubmitting:!1});a(this,"stateSubscribers",new Set);a(this,"stateSnapshot");a(this,"_userData",{});a(this,"_userId",null);a(this,"_userTraits",{});a(this,"triggerButton",null);a(this,"modal",null);a(this,"overlay",null);a(this,"stylesInjected",!1);a(this,"autoCloseTimeout",null);a(this,"isDestroyed",!1);this.widgetId=e.widgetId,this.headless=e.headless??!1,this.config={...M,...e.config},this.apiClient=new p(e.apiBaseUrl??f,this.config.debug),this.emitter=new h,this.stateSnapshot={...this.state},this.log("Instance created",{widgetId:this.widgetId,headless:this.headless})}static init(e){let t=y.get(e.widgetId);if(t)return t;let i=new m(e);return y.set(e.widgetId,i),i.init().catch(o=>{console.error("[FeedValue] Initialization failed:",o)}),i}static getInstance(e){return y.get(e)}async init(){if(this.state.isReady){this.log("Already initialized");return}if(this.isDestroyed){this.log("Instance destroyed before init could complete");return}try{this.log("Initializing...");let e=v();this.apiClient.setFingerprint(e);let t=await this.apiClient.fetchConfig(this.widgetId);if(this.isDestroyed){this.log("Instance destroyed during config fetch, aborting init");return}let i={position:t.config.position??"bottom-right",triggerText:t.config.triggerText??"Feedback",triggerIcon:t.config.triggerIcon??"none",formTitle:t.config.formTitle??"Share your feedback",submitButtonText:t.config.submitButtonText??"Submit",thankYouMessage:t.config.thankYouMessage??"Thank you for your feedback!",showBranding:t.config.showBranding??!0,customFields:t.config.customFields,...t.config.template&&{template:t.config.template},...t.config.options&&{options:t.config.options},followUpLabel:t.config.followUpLabel??"Tell us more (optional)",submitText:t.config.submitText??"Send"};this.widgetConfig={widgetId:t.widget_id,widgetKey:t.widget_key,appId:"",type:t.type??"feedback",config:i,styling:{primaryColor:t.styling.primaryColor??"#3b82f6",backgroundColor:t.styling.backgroundColor??"#ffffff",textColor:t.styling.textColor??"#1f2937",buttonTextColor:t.styling.buttonTextColor??"#ffffff",borderRadius:t.styling.borderRadius??"8px",customCSS:t.styling.customCSS}},!this.headless&&typeof window<"u"&&typeof document<"u"&&this.renderWidget(),this.updateState({isReady:!0,error:null}),this.emitter.emit("ready"),this.log("Initialized successfully")}catch(e){let t=e instanceof Error?e:new Error(String(e));throw this.updateState({error:t}),this.emitter.emit("error",t),t}}destroy(){this.log("Destroying..."),this.isDestroyed=!0,this.autoCloseTimeout&&(clearTimeout(this.autoCloseTimeout),this.autoCloseTimeout=null),this.triggerButton?.remove(),this.modal?.remove(),this.overlay?.remove(),document.getElementById("fv-widget-styles")?.remove(),document.getElementById("fv-widget-custom-styles")?.remove(),this.triggerButton=null,this.modal=null,this.overlay=null,this.widgetConfig=null,this.stateSubscribers.clear(),this.emitter.removeAllListeners(),this.apiClient.clearCache(),y.delete(this.widgetId),this.state={isReady:!1,isOpen:!1,isVisible:!1,error:null,isSubmitting:!1},this.log("Destroyed")}open(){if(!this.state.isReady){this.log("Cannot open: not ready");return}this.updateState({isOpen:!0}),this.headless||(this.overlay?.classList.add("fv-widget-open"),this.modal?.classList.add("fv-widget-open")),this.emitter.emit("open"),this.log("Opened")}close(){this.updateState({isOpen:!1}),this.headless||(this.overlay?.classList.remove("fv-widget-open"),this.modal?.classList.remove("fv-widget-open")),this.emitter.emit("close"),this.log("Closed")}toggle(){this.state.isOpen?this.close():this.open()}show(){this.updateState({isVisible:!0}),!this.headless&&this.triggerButton&&(this.triggerButton.style.display=""),this.log("Shown")}hide(){this.updateState({isVisible:!1}),!this.headless&&this.triggerButton&&(this.triggerButton.style.display="none"),this.log("Hidden")}isOpen(){return this.state.isOpen}isVisible(){return this.state.isVisible}isReady(){return this.state.isReady}isHeadless(){return this.headless}setData(e){this._userData={...this._userData,...e},this.log("User data set",e)}identify(e,t){this._userId=e,t&&(this._userTraits={...this._userTraits,...t}),this.log("User identified",{userId:e,traits:t})}reset(){this._userData={},this._userId=null,this._userTraits={},this.log("User data reset")}getUserData(){return{userId:this._userId,data:{...this._userData},traits:{...this._userTraits}}}async submit(e){if(!this.state.isReady)throw new Error("Widget not ready");this.validateFeedback(e),this.updateState({isSubmitting:!0});try{let t={message:e.message,sentiment:e.sentiment,customFieldValues:e.customFieldValues,metadata:{page_url:typeof window<"u"?window.location.href:"",referrer:typeof document<"u"?document.referrer:void 0,...e.metadata}},i=this.buildSubmissionUserData();await this.apiClient.submitFeedback(this.widgetId,t,i),this.emitter.emit("submit",t),this.log("Feedback submitted",i?{withUserData:!0}:void 0)}catch(t){let i=t instanceof Error?t:new Error(String(t));throw this.emitter.emit("error",i),i}finally{this.updateState({isSubmitting:!1})}}buildSubmissionUserData(){let e=this._userId!==null,t=Object.keys(this._userData).length>0,i=Object.keys(this._userTraits).length>0;if(!e&&!t&&!i)return;let o={};this._userId&&(o.user_id=this._userId);let s=this._userData.email??this._userTraits.email,r=this._userData.name??this._userTraits.name;if(s&&(o.email=s),r&&(o.name=r),i){let{email:n,name:g,...l}=this._userTraits;Object.keys(l).length>0&&(o.traits=l)}if(t){let{email:n,name:g,...l}=this._userData,c={};for(let[u,C]of Object.entries(l))C!==void 0&&(c[u]=C);Object.keys(c).length>0&&(o.custom_data=c)}return o}getReactionOptions(){if(!this.widgetConfig||this.widgetConfig.type!=="reaction")return null;let e=this.widgetConfig.config;return e.template?this.getTemplateOptions(e.template):e.options??null}getTemplateOptions(e){return{thumbs:[{label:"Helpful",value:"helpful",icon:"thumbs-up",showFollowUp:!1},{label:"Not Helpful",value:"not_helpful",icon:"thumbs-down",showFollowUp:!0}],helpful:[{label:"Yes",value:"yes",icon:"check",showFollowUp:!1},{label:"No",value:"no",icon:"x",showFollowUp:!0}],emoji:[{label:"Angry",value:"angry",icon:"\u{1F620}",showFollowUp:!0},{label:"Disappointed",value:"disappointed",icon:"\u{1F61E}",showFollowUp:!0},{label:"Neutral",value:"neutral",icon:"\u{1F610}",showFollowUp:!1},{label:"Satisfied",value:"satisfied",icon:"\u{1F60A}",showFollowUp:!1},{label:"Excited",value:"excited",icon:"\u{1F60D}",showFollowUp:!1}],rating:[{label:"1",value:"1",icon:"\u2B50",showFollowUp:!0},{label:"2",value:"2",icon:"\u2B50\u2B50",showFollowUp:!0},{label:"3",value:"3",icon:"\u2B50\u2B50\u2B50",showFollowUp:!1},{label:"4",value:"4",icon:"\u2B50\u2B50\u2B50\u2B50",showFollowUp:!1},{label:"5",value:"5",icon:"\u2B50\u2B50\u2B50\u2B50\u2B50",showFollowUp:!1}]}[e]??[]}async react(e,t){if(!this.state.isReady)throw new Error("Widget not ready");if(!this.widgetConfig||this.widgetConfig.type!=="reaction")throw new Error("This is not a reaction widget");let i=this.getReactionOptions();if(!i)throw new Error("No reaction options configured");let o=i.find(s=>s.value===e);if(!o){let s=i.map(r=>r.value).join(", ");throw new Error(`Invalid reaction value. Must be one of: ${s}`)}this.emitter.emit("react",{value:e,hasFollowUp:o.showFollowUp}),this.updateState({isSubmitting:!0});try{let s={value:e,metadata:{page_url:typeof window<"u"?window.location.href:""},...t?.followUp&&{followUp:t.followUp}};await this.apiClient.submitReaction(this.widgetId,s);let r=t?.followUp?{value:e,followUp:t.followUp}:{value:e};this.emitter.emit("reactSubmit",r),this.log("Reaction submitted",{value:e})}catch(s){let r=s instanceof Error?s:new Error(String(s));throw this.emitter.emit("reactError",r),r}finally{this.updateState({isSubmitting:!1})}}isReaction(){return this.widgetConfig?.type==="reaction"}getWidgetType(){return this.widgetConfig?.type??"feedback"}validateFeedback(e){if(!e.message?.trim())throw new Error("Feedback message is required");if(e.message.length>T)throw new Error(`Feedback message exceeds maximum length of ${T} characters`);if(e.sentiment!==void 0&&!S.includes(e.sentiment))throw new Error(`Invalid sentiment value. Must be one of: ${S.join(", ")}`);if(e.customFieldValues){for(let[t,i]of Object.entries(e.customFieldValues))if(typeof t!="string"||typeof i!="string")throw new Error("Custom field values must be strings")}if(e.metadata){for(let[t,i]of Object.entries(e.metadata))if(typeof i=="string"&&i.length>F)throw new Error(`Metadata field "${t}" exceeds maximum length of ${F} characters`)}}on(e,t){this.emitter.on(e,t)}once(e,t){this.emitter.once(e,t)}off(e,t){this.emitter.off(e,t)}async waitUntilReady(){if(!this.state.isReady){if(this.state.error)throw this.state.error;return new Promise((e,t)=>{this.once("ready",()=>e()),this.once("error",i=>t(i))})}}setConfig(e){this.config={...this.config,...e},this.log("Config updated",e)}getConfig(){return{...this.config}}getWidgetConfig(){return this.widgetConfig}subscribe(e){return this.stateSubscribers.add(e),()=>{this.stateSubscribers.delete(e)}}getSnapshot(){return this.stateSnapshot}updateState(e){this.state={...this.state,...e},this.stateSnapshot={...this.state},this.emitter.emit("stateChange",this.stateSnapshot);for(let t of this.stateSubscribers)t()}renderWidget(){this.widgetConfig&&(this.stylesInjected||(this.injectStyles(),this.stylesInjected=!0),this.renderTrigger(),this.renderModal())}sanitizeCSS(e){let t=[/url\s*\(/gi,/@import/gi,/expression\s*\(/gi,/javascript:/gi,/behavior\s*:/gi,/-moz-binding/gi];for(let i of t)if(i.test(e))return console.warn("[FeedValue] Blocked potentially unsafe CSS pattern"),"";return e}injectStyles(){if(!this.widgetConfig)return;let{styling:e,config:t}=this.widgetConfig,i=document.createElement("style");if(i.id="fv-widget-styles",i.textContent=this.getBaseStyles(e,t.position),document.head.appendChild(i),e.customCSS){let o=this.sanitizeCSS(e.customCSS);if(o){let s=document.createElement("style");s.id="fv-widget-custom-styles",s.textContent=o,document.head.appendChild(s)}}}getBaseStyles(e,t){let i=this.getPositionStyles(t),o=this.getModalPositionStyles(t);return`
|
|
2
2
|
.fv-widget-trigger {
|
|
3
3
|
position: fixed;
|
|
4
4
|
${i}
|
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
}
|
|
50
50
|
.fv-widget-modal {
|
|
51
51
|
position: fixed;
|
|
52
|
-
${
|
|
52
|
+
${o}
|
|
53
53
|
background-color: ${e.backgroundColor};
|
|
54
54
|
color: ${e.textColor};
|
|
55
55
|
border-radius: ${e.borderRadius};
|
|
@@ -148,5 +148,5 @@
|
|
|
148
148
|
color: ${e.primaryColor};
|
|
149
149
|
text-decoration: none;
|
|
150
150
|
}
|
|
151
|
-
`}getPositionStyles(e){switch(e){case"bottom-left":return"bottom: 20px; left: 20px;";case"top-right":return"top: 20px; right: 20px;";case"top-left":return"top: 20px; left: 20px;";case"center":return"top: 50%; left: 50%; transform: translate(-50%, -50%);";default:return"bottom: 20px; right: 20px;"}}getModalPositionStyles(e){switch(e){case"bottom-left":return"bottom: 20px; left: 20px;";case"bottom-right":return"bottom: 20px; right: 20px;";case"top-right":return"top: 20px; right: 20px;";case"top-left":return"top: 20px; left: 20px;";default:return"top: 50%; left: 50%; transform: translate(-50%, -50%);"}}parseSvgString(e){let i=new DOMParser().parseFromString(e,"image/svg+xml"),
|
|
151
|
+
`}getPositionStyles(e){switch(e){case"bottom-left":return"bottom: 20px; left: 20px;";case"top-right":return"top: 20px; right: 20px;";case"top-left":return"top: 20px; left: 20px;";case"center":return"top: 50%; left: 50%; transform: translate(-50%, -50%);";default:return"bottom: 20px; right: 20px;"}}getModalPositionStyles(e){switch(e){case"bottom-left":return"bottom: 20px; left: 20px;";case"bottom-right":return"bottom: 20px; right: 20px;";case"top-right":return"top: 20px; right: 20px;";case"top-left":return"top: 20px; left: 20px;";default:return"top: 50%; left: 50%; transform: translate(-50%, -50%);"}}parseSvgString(e){let i=new DOMParser().parseFromString(e,"image/svg+xml"),o=i.querySelector("parsererror");if(o)return this.log("SVG parse error:",o.textContent),null;let s=i.querySelector("svg");return!s||s.nodeName!=="svg"?null:s}renderTrigger(){if(!this.widgetConfig)return;let{triggerIcon:e,triggerText:t}=this.widgetConfig.config,i=e&&e!=="none";this.triggerButton=document.createElement("button");let o=!1;if(i){let s=m.TRIGGER_ICONS[e];if(s){let r=this.parseSvgString(s);r?(this.triggerButton.className="fv-widget-trigger fv-widget-trigger-icon",this.triggerButton.appendChild(r),o=!0):this.log(`SVG parsing failed for icon: ${e}, falling back to text`)}else this.log(`Icon not found: ${e}, falling back to text`)}o||(this.triggerButton.className="fv-widget-trigger",this.triggerButton.textContent=t||"Feedback"),this.triggerButton.addEventListener("click",()=>this.open()),document.body.appendChild(this.triggerButton)}renderModal(){if(!this.widgetConfig)return;let{config:e}=this.widgetConfig;this.overlay=document.createElement("div"),this.overlay.className="fv-widget-overlay",this.overlay.addEventListener("click",()=>this.close()),document.body.appendChild(this.overlay),this.modal=document.createElement("div"),this.modal.className="fv-widget-modal";let t=document.createElement("div");t.className="fv-widget-header";let i=document.createElement("h2");i.className="fv-widget-title",i.textContent=e.formTitle,t.appendChild(i);let o=document.createElement("button");o.className="fv-widget-close",o.setAttribute("aria-label","Close"),o.textContent="\xD7",o.addEventListener("click",()=>this.close()),t.appendChild(o),this.modal.appendChild(t);let s=document.createElement("form");s.className="fv-widget-form",s.id="fv-feedback-form";let r=document.createElement("textarea");r.className="fv-widget-textarea",r.id="fv-feedback-content",r.placeholder="Tell us what you think...",r.required=!0,s.appendChild(r);let n=document.createElement("button");n.type="submit",n.className="fv-widget-submit",n.textContent=e.submitButtonText,s.appendChild(n);let g=document.createElement("div");if(g.className="fv-widget-error",g.id="fv-error-message",s.appendChild(g),s.addEventListener("submit",l=>this.handleFormSubmit(l)),this.modal.appendChild(s),e.showBranding){let l=document.createElement("div");l.className="fv-widget-branding";let c=document.createTextNode("Powered by ");l.appendChild(c);let u=document.createElement("a");u.href="https://feedvalue.com",u.target="_blank",u.rel="noopener noreferrer",u.textContent="FeedValue",l.appendChild(u),this.modal.appendChild(l)}document.body.appendChild(this.modal)}async handleFormSubmit(e){e.preventDefault();let t=document.getElementById("fv-feedback-content"),i=this.modal?.querySelector(".fv-widget-submit"),o=document.getElementById("fv-error-message");if(!t?.value.trim()){this.showError("Please enter your feedback");return}try{i.disabled=!0,i.textContent="Submitting...",o&&(o.style.display="none"),await this.submit({message:t.value.trim()}),this.showSuccess()}catch(s){let r=s instanceof Error?s.message:"Failed to submit";this.showError(r)}finally{i.disabled=!1,i.textContent=this.widgetConfig?.config.submitButtonText??"Submit"}}showError(e){let t=document.getElementById("fv-error-message");t&&(t.textContent=e,t.style.display="block")}showSuccess(){if(!this.modal||!this.widgetConfig)return;this.autoCloseTimeout&&(clearTimeout(this.autoCloseTimeout),this.autoCloseTimeout=null),this.modal.textContent="";let e=document.createElement("div");e.style.cssText="text-align: center; padding: 40px 20px;";let t=document.createElement("div");t.style.cssText="font-size: 48px; margin-bottom: 16px;",t.textContent="\u2713",e.appendChild(t);let i=document.createElement("div");i.style.cssText="font-size: 16px; margin-bottom: 24px;",i.textContent=this.widgetConfig.config.thankYouMessage,e.appendChild(i);let o=document.createElement("button");o.className="fv-widget-submit",o.textContent="Close",o.addEventListener("click",()=>{this.close(),this.resetForm()}),e.appendChild(o),this.modal.appendChild(e),this.autoCloseTimeout=setTimeout(()=>{this.state.isOpen&&(this.close(),this.resetForm()),this.autoCloseTimeout=null},P)}resetForm(){this.modal&&(this.modal.textContent="",this.modal.remove(),this.modal=null,this.renderModal())}log(e,t){this.config.debug&&console.log(`[FeedValue] ${e}`,t??"")}};a(m,"TRIGGER_ICONS",{chat:'<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M2.992 16.342a2 2 0 0 1 .094 1.167l-1.065 3.29a1 1 0 0 0 1.236 1.168l3.413-.998a2 2 0 0 1 1.099.092 10 10 0 1 0-4.777-4.719"/></svg>',message:'<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 17a2 2 0 0 1-2 2H6.828a2 2 0 0 0-1.414.586l-2.202 2.202A.71.71 0 0 1 2 21.286V5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2z"/></svg>',feedback:'<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M16 10a2 2 0 0 1-2 2H6.828a2 2 0 0 0-1.414.586l-2.202 2.202A.71.71 0 0 1 2 14.286V4a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2z"/><path d="M20 9a2 2 0 0 1 2 2v10.286a.71.71 0 0 1-1.212.502l-2.202-2.202A2 2 0 0 0 17.172 19H10a2 2 0 0 1-2-2v-1"/></svg>',comment:'<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 17a2 2 0 0 1-2 2H6.828a2 2 0 0 0-1.414.586l-2.202 2.202A.71.71 0 0 1 2 21.286V5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2z"/><path d="M7 11h10"/><path d="M7 15h6"/><path d="M7 7h8"/></svg>',help:'<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/><path d="M12 17h.01"/></svg>',lightbulb:'<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M15 14c.2-1 .7-1.7 1.5-2.5 1-.9 1.5-2.2 1.5-3.5A6 6 0 0 0 6 8c0 1 .2 2.2 1.5 3.5.7.7 1.3 1.5 1.5 2.5"/><path d="M9 18h6"/><path d="M10 22h4"/></svg>'});var x=m;var R={thumbs:["not_helpful"],helpful:["no"],emoji:["angry","disappointed"],rating:["1","2"]};return A(B);})();
|
|
152
152
|
//# sourceMappingURL=index.min.js.map
|