@knocklabs/client 0.14.4 → 0.14.5

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/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.14.5
4
+
5
+ ### Patch Changes
6
+
7
+ - 8f00623: activation location rules support for guides
8
+
3
9
  ## 0.14.4
4
10
 
5
11
  ### Patch Changes
@@ -1,2 +1,2 @@
1
- "use strict";var l=Object.defineProperty;var m=(o,e,t)=>e in o?l(o,e,{enumerable:!0,configurable:!0,writable:!0,value:t}):o[e]=t;var u=(o,e,t)=>m(o,typeof e!="symbol"?e+"":e,t);Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const g=require("@tanstack/store"),h=o=>[...o].sort((e,t)=>t.priority-e.priority||new Date(t.inserted_at).getTime()-new Date(e.inserted_at).getTime()),k=o=>`/v1/users/${o}/guides`;class y{constructor(e,t,s={}){u(this,"store");u(this,"socket");u(this,"socketChannel");u(this,"socketChannelTopic");u(this,"socketEventTypes",["guide.added","guide.updated","guide.removed"]);this.knock=e,this.channelId=t,this.targetParams=s,this.store=new g.Store({guides:[],queries:{}});const{socket:r}=this.knock.client();this.socket=r,this.socketChannelTopic=`guides:${t}`,this.knock.log("[Guide] Initialized a guide client")}async fetch(e){this.knock.failIfNotAuthenticated(),this.knock.log("[Guide] Loading all eligible guides");const t=this.buildQueryParams(e==null?void 0:e.filters),s=this.formatQueryKey(t),r=this.store.state.queries[s];if(r)return r;this.store.setState(i=>({...i,queries:{...i.queries,[s]:{status:"loading"}}}));let n;try{const i=await this.knock.user.getGuides(this.channelId,t);n={status:"ok"},this.store.setState(a=>({...a,guides:i.entries.map(d=>this.localCopy(d)),queries:{...a.queries,[s]:n}}))}catch(i){n={status:"error",error:i},this.store.setState(a=>({...a,queries:{...a.queries,[s]:n}}))}return n}subscribe(){if(!this.socket)return;this.knock.failIfNotAuthenticated(),this.knock.log("[Guide] Subscribing to real time updates"),this.socket.isConnected()||this.socket.connect(),this.socketChannel&&this.unsubscribe();const e={...this.targetParams,user_id:this.knock.userId},t=this.socket.channel(this.socketChannelTopic,e);for(const s of this.socketEventTypes)t.on(s,r=>this.handleSocketEvent(r));["closed","errored"].includes(t.state)&&t.join(),this.socketChannel=t}unsubscribe(){if(this.socketChannel){this.knock.log("[Guide] Unsubscribing from real time updates");for(const e of this.socketEventTypes)this.socketChannel.off(e);this.socketChannel.leave(),this.socketChannel=void 0}}handleSocketEvent(e){const{event:t,data:s}=e;switch(t){case"guide.added":return this.addGuide(e);case"guide.updated":return s.eligible?this.replaceOrAddGuide(e):this.removeGuide(e);case"guide.removed":return this.removeGuide(e);default:return}}select(e,t={}){return e.guides.filter(s=>!(t.type&&t.type!==s.type||t.key&&t.key!==s.key))}async markAsSeen(e,t){this.knock.log(`[Guide] Marking as seen (Guide key: ${e.key}, Step ref:${t.ref})`);const s=this.setStepMessageAttrs(e.key,t.ref,{seen_at:new Date().toISOString()});if(!s)return;const r={...this.buildEngagementEventBaseParams(e,s),content:s.content,data:this.targetParams.data,tenant:this.targetParams.tenant};return this.knock.user.markGuideStepAs("seen",r),s}async markAsInteracted(e,t,s){this.knock.log(`[Guide] Marking as interacted (Guide key: ${e.key}, Step ref:${t.ref})`);const r=new Date().toISOString(),n=this.setStepMessageAttrs(e.key,t.ref,{read_at:r,interacted_at:r});if(!n)return;const i={...this.buildEngagementEventBaseParams(e,n),metadata:s};return this.knock.user.markGuideStepAs("interacted",i),n}async markAsArchived(e,t){this.knock.log(`[Guide] Marking as archived (Guide key: ${e.key}, Step ref:${t.ref})`);const s=this.setStepMessageAttrs(e.key,t.ref,{archived_at:new Date().toISOString()});if(!s)return;const r=this.buildEngagementEventBaseParams(e,s);return this.knock.user.markGuideStepAs("archived",r),s}localCopy(e){const t=this,s={...e};return s.steps=e.steps.map(({message:r,...n})=>{const i={...n,message:{...r},markAsSeen(){if(!this.message.seen_at)return t.markAsSeen(s,this)},markAsInteracted({metadata:a}={}){return t.markAsInteracted(s,this,a)},markAsArchived(){if(!this.message.archived_at)return t.markAsArchived(s,this)}};return i.markAsSeen=i.markAsSeen.bind(i),i.markAsInteracted=i.markAsInteracted.bind(i),i.markAsArchived=i.markAsArchived.bind(i),i}),s}buildQueryParams(e={}){const t={...this.targetParams,...e};let s=Object.fromEntries(Object.entries(t).filter(([r,n])=>n!=null));return s=s.data?{...s,data:JSON.stringify(s.data)}:s,s}formatQueryKey(e){const s=Object.keys(e).sort().map(n=>`${encodeURIComponent(n)}=${encodeURIComponent(e[n])}`).join("&"),r=k(this.knock.userId);return s?`${r}?${s}`:r}setStepMessageAttrs(e,t,s){let r;return this.store.setState(n=>{const i=n.guides.map(a=>{if(a.key!==e)return a;const d=a.steps.map(c=>(c.ref!==t||(c.message={...c.message,...s},r=c),c));return{...a,steps:d}});return{...n,guides:i}}),r}buildEngagementEventBaseParams(e,t){return{message_id:t.message.id,channel_id:e.channel_id,guide_key:e.key,guide_id:e.id,guide_step_ref:t.ref}}addGuide({data:e}){const t=this.localCopy(e.guide);this.store.setState(s=>({...s,guides:h([...s.guides,t])}))}replaceOrAddGuide({data:e}){const t=this.localCopy(e.guide);this.store.setState(s=>{let r=!1;const n=s.guides.map(i=>i.key!==t.key?i:(r=!0,t));return{...s,guides:h(r?n:[...n,t])}})}removeGuide({data:e}){this.store.setState(t=>{const s=t.guides.filter(r=>r.key!==e.guide.key);return{...t,guides:s}})}}exports.KnockGuideClient=y;exports.guidesApiRootPath=k;
1
+ "use strict";var k=Object.defineProperty;var m=(o,e,t)=>e in o?k(o,e,{enumerable:!0,configurable:!0,writable:!0,value:t}):o[e]=t;var c=(o,e,t)=>m(o,typeof e!="symbol"?e+"":e,t);Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const p=require("@tanstack/store"),g=require("urlpattern-polyfill"),u=o=>[...o].sort((e,t)=>t.priority-e.priority||new Date(t.inserted_at).getTime()-new Date(e.inserted_at).getTime()),l=o=>`/v1/users/${o}/guides`;class f{constructor(e,t,s={},n={}){c(this,"store");c(this,"socket");c(this,"socketChannel");c(this,"socketChannelTopic");c(this,"socketEventTypes",["guide.added","guide.updated","guide.removed"]);c(this,"pushStateFn");c(this,"replaceStateFn");this.knock=e,this.channelId=t,this.targetParams=s,this.options=n;const{trackLocationFromWindow:i=!0}=n,r=i?window==null?void 0:window.location.href:void 0;this.store=new p.Store({guides:[],queries:{},location:r});const{socket:a}=this.knock.client();this.socket=a,this.socketChannelTopic=`guides:${t}`,i&&this.listenForLocationChangesFromWindow(),this.knock.log("[Guide] Initialized a guide client")}cleanup(){this.unsubscribe(),this.removeEventListeners()}async fetch(e){this.knock.failIfNotAuthenticated(),this.knock.log("[Guide] Loading all eligible guides");const t=this.buildQueryParams(e==null?void 0:e.filters),s=this.formatQueryKey(t),n=this.store.state.queries[s];if(n)return n;this.store.setState(r=>({...r,queries:{...r.queries,[s]:{status:"loading"}}}));let i;try{const r=await this.knock.user.getGuides(this.channelId,t);i={status:"ok"},this.store.setState(a=>({...a,guides:r.entries.map(h=>this.localCopy(h)),queries:{...a.queries,[s]:i}}))}catch(r){i={status:"error",error:r},this.store.setState(a=>({...a,queries:{...a.queries,[s]:i}}))}return i}subscribe(){if(!this.socket)return;this.knock.failIfNotAuthenticated(),this.knock.log("[Guide] Subscribing to real time updates"),this.socket.isConnected()||this.socket.connect(),this.socketChannel&&this.unsubscribe();const e={...this.targetParams,user_id:this.knock.userId},t=this.socket.channel(this.socketChannelTopic,e);for(const s of this.socketEventTypes)t.on(s,n=>this.handleSocketEvent(n));["closed","errored"].includes(t.state)&&t.join(),this.socketChannel=t}unsubscribe(){if(this.socketChannel){this.knock.log("[Guide] Unsubscribing from real time updates");for(const e of this.socketEventTypes)this.socketChannel.off(e);this.socketChannel.leave(),this.socketChannel=void 0}}handleSocketEvent(e){const{event:t,data:s}=e;switch(t){case"guide.added":return this.addGuide(e);case"guide.updated":return s.eligible?this.replaceOrAddGuide(e):this.removeGuide(e);case"guide.removed":return this.removeGuide(e);default:return}}select(e,t={}){return e.guides.filter(s=>{if(t.type&&t.type!==s.type||t.key&&t.key!==s.key)return!1;const n=s.activation_location_rules||[];return!(n.length>0&&e.location&&!n.reduce((r,a)=>{if(r===!1)return!1;switch(a.directive){case"allow":return r===!0||a.pattern.test(e.location)?!0:void 0;case"block":return a.pattern.test(e.location)?!1:r}},void 0))})}async markAsSeen(e,t){this.knock.log(`[Guide] Marking as seen (Guide key: ${e.key}, Step ref:${t.ref})`);const s=this.setStepMessageAttrs(e.key,t.ref,{seen_at:new Date().toISOString()});if(!s)return;const n={...this.buildEngagementEventBaseParams(e,s),content:s.content,data:this.targetParams.data,tenant:this.targetParams.tenant};return this.knock.user.markGuideStepAs("seen",n),s}async markAsInteracted(e,t,s){this.knock.log(`[Guide] Marking as interacted (Guide key: ${e.key}, Step ref:${t.ref})`);const n=new Date().toISOString(),i=this.setStepMessageAttrs(e.key,t.ref,{read_at:n,interacted_at:n});if(!i)return;const r={...this.buildEngagementEventBaseParams(e,i),metadata:s};return this.knock.user.markGuideStepAs("interacted",r),i}async markAsArchived(e,t){this.knock.log(`[Guide] Marking as archived (Guide key: ${e.key}, Step ref:${t.ref})`);const s=this.setStepMessageAttrs(e.key,t.ref,{archived_at:new Date().toISOString()});if(!s)return;const n=this.buildEngagementEventBaseParams(e,s);return this.knock.user.markGuideStepAs("archived",n),s}localCopy(e){const t=this,s={...e};return s.steps=e.steps.map(({message:n,...i})=>{const r={...i,message:{...n},markAsSeen(){if(!this.message.seen_at)return t.markAsSeen(s,this)},markAsInteracted({metadata:a}={}){return t.markAsInteracted(s,this,a)},markAsArchived(){if(!this.message.archived_at)return t.markAsArchived(s,this)}};return r.markAsSeen=r.markAsSeen.bind(r),r.markAsInteracted=r.markAsInteracted.bind(r),r.markAsArchived=r.markAsArchived.bind(r),r}),s.activation_location_rules=e.activation_location_rules.map(n=>({...n,pattern:new g.URLPattern({pathname:n.pathname})})),s}buildQueryParams(e={}){const t={...this.targetParams,...e};let s=Object.fromEntries(Object.entries(t).filter(([n,i])=>i!=null));return s=s.data?{...s,data:JSON.stringify(s.data)}:s,s}formatQueryKey(e){const s=Object.keys(e).sort().map(i=>`${encodeURIComponent(i)}=${encodeURIComponent(e[i])}`).join("&"),n=l(this.knock.userId);return s?`${n}?${s}`:n}setStepMessageAttrs(e,t,s){let n;return this.store.setState(i=>{const r=i.guides.map(a=>{if(a.key!==e)return a;const h=a.steps.map(d=>(d.ref!==t||(d.message={...d.message,...s},n=d),d));return{...a,steps:h}});return{...i,guides:r}}),n}buildEngagementEventBaseParams(e,t){return{message_id:t.message.id,channel_id:e.channel_id,guide_key:e.key,guide_id:e.id,guide_step_ref:t.ref}}addGuide({data:e}){const t=this.localCopy(e.guide);this.store.setState(s=>({...s,guides:u([...s.guides,t])}))}replaceOrAddGuide({data:e}){const t=this.localCopy(e.guide);this.store.setState(s=>{let n=!1;const i=s.guides.map(r=>r.key!==t.key?r:(n=!0,t));return{...s,guides:u(n?i:[...i,t])}})}removeGuide({data:e}){this.store.setState(t=>{const s=t.guides.filter(n=>n.key!==e.guide.key);return{...t,guides:s}})}handleLocationChange(){const e=window.location.href;this.store.state.location!==e&&(this.knock.log(`[Guide] Handle Location change: ${e}`),this.store.setState(t=>({...t,location:e})))}listenForLocationChangesFromWindow(){if(window!=null&&window.history){window.addEventListener("popstate",this.handleLocationChange),window.addEventListener("hashchange",this.handleLocationChange);const e=window.history.pushState,t=window.history.replaceState;window.history.pushState=new Proxy(e,{apply:(s,n,i)=>{Reflect.apply(s,n,i),setTimeout(()=>{this.handleLocationChange()},0)}}),window.history.replaceState=new Proxy(t,{apply:(s,n,i)=>{Reflect.apply(s,n,i),setTimeout(()=>{this.handleLocationChange()},0)}}),this.pushStateFn=e,this.replaceStateFn=t}else this.knock.log("[Guide] Unable to access the `window.history` object to detect location changes")}removeEventListeners(){window.removeEventListener("popstate",this.handleLocationChange),window.removeEventListener("hashchange",this.handleLocationChange),this.pushStateFn&&(window.history.pushState=this.pushStateFn,this.pushStateFn=void 0),this.replaceStateFn&&(window.history.replaceState=this.replaceStateFn,this.replaceStateFn=void 0)}}exports.KnockGuideClient=f;exports.guidesApiRootPath=l;
2
2
  //# sourceMappingURL=client.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"client.js","sources":["../../../../src/clients/guide/client.ts"],"sourcesContent":["import { GenericData } from \"@knocklabs/types\";\nimport { Store } from \"@tanstack/store\";\nimport { Channel, Socket } from \"phoenix\";\n\nimport Knock from \"../../knock\";\n\nconst sortGuides = (guides: KnockGuide[]) => {\n return [...guides].sort(\n (a, b) =>\n b.priority - a.priority ||\n new Date(b.inserted_at).getTime() - new Date(a.inserted_at).getTime(),\n );\n};\n\n//\n// Guides API (via User client)\n//\n\nexport const guidesApiRootPath = (userId: string | undefined | null) =>\n `/v1/users/${userId}/guides`;\n\ninterface StepMessageState {\n id: string;\n seen_at: string | null;\n read_at: string | null;\n interacted_at: string | null;\n archived_at: string | null;\n link_clicked_at: string | null;\n}\n\ninterface GuideStepData {\n ref: string;\n schema_key: string;\n schema_semver: string;\n schema_variant_key: string;\n message: StepMessageState;\n // eslint-disable-next-line\n content: any;\n}\n\ninterface GuideData {\n __typename: \"Guide\";\n channel_id: string;\n id: string;\n key: string;\n priority: number;\n type: string;\n semver: string;\n steps: GuideStepData[];\n inserted_at: string;\n updated_at: string;\n}\n\nexport interface KnockGuideStep extends GuideStepData {\n markAsSeen: () => void;\n markAsInteracted: (params?: { metadata?: GenericData }) => void;\n markAsArchived: () => void;\n}\n\nexport interface KnockGuide extends GuideData {\n steps: KnockGuideStep[];\n}\n\ntype GetGuidesQueryParams = {\n data?: string;\n tenant?: string;\n type?: string;\n};\n\ntype GetGuidesResponse = {\n entries: GuideData[];\n};\n\nexport type GuideEngagementEventBaseParams = {\n // Base params required for all engagement update events\n message_id: string;\n channel_id: string;\n guide_key: string;\n guide_id: string;\n guide_step_ref: string;\n};\n\ntype MarkAsSeenParams = GuideEngagementEventBaseParams & {\n // Rendered step content seen by the recipient\n content: GenericData;\n // Target params\n data?: GenericData;\n tenant?: string;\n};\ntype MarkAsInteractedParams = GuideEngagementEventBaseParams;\ntype MarkAsArchivedParams = GuideEngagementEventBaseParams;\n\ntype MarkGuideAsResponse = {\n status: \"ok\";\n};\n\ntype SocketEventType = \"guide.added\" | \"guide.updated\" | \"guide.removed\";\n\ntype SocketEventPayload<E extends SocketEventType, D> = {\n topic: string;\n event: E;\n data: D;\n};\n\ntype GuideAddedEvent = SocketEventPayload<\n \"guide.added\",\n { guide: GuideData; eligible: true }\n>;\n\ntype GuideUpdatedEvent = SocketEventPayload<\n \"guide.updated\",\n { guide: GuideData; eligible: boolean }\n>;\n\ntype GuideRemovedEvent = SocketEventPayload<\n \"guide.removed\",\n { guide: Pick<GuideData, \"key\"> }\n>;\n\ntype GuideSocketEvent = GuideAddedEvent | GuideUpdatedEvent | GuideRemovedEvent;\n\n//\n// Guides client\n//\n\ntype QueryKey = string;\n\ntype QueryStatus = {\n status: \"loading\" | \"ok\" | \"error\";\n error?: Error;\n};\n\ntype StoreState = {\n guides: KnockGuide[];\n queries: Record<QueryKey, QueryStatus>;\n};\n\ntype QueryFilterParams = Pick<GetGuidesQueryParams, \"type\">;\n\nexport type SelectFilterParams = {\n key?: string;\n type?: string;\n};\n\nexport type TargetParams = {\n data?: GenericData | undefined;\n tenant?: string | undefined;\n};\n\nexport class KnockGuideClient {\n public store: Store<StoreState, (state: StoreState) => StoreState>;\n\n // Phoenix channels for real time guide updates over websocket\n private socket: Socket | undefined;\n private socketChannel: Channel | undefined;\n private socketChannelTopic: string;\n private socketEventTypes = [\"guide.added\", \"guide.updated\", \"guide.removed\"];\n\n constructor(\n readonly knock: Knock,\n readonly channelId: string,\n readonly targetParams: TargetParams = {},\n ) {\n this.store = new Store<StoreState>({\n guides: [],\n queries: {},\n });\n\n // In server environments we might not have a socket connection.\n const { socket: maybeSocket } = this.knock.client();\n this.socket = maybeSocket;\n this.socketChannelTopic = `guides:${channelId}`;\n\n this.knock.log(\"[Guide] Initialized a guide client\");\n }\n\n async fetch(opts?: { filters?: QueryFilterParams }) {\n this.knock.failIfNotAuthenticated();\n this.knock.log(\"[Guide] Loading all eligible guides\");\n\n const queryParams = this.buildQueryParams(opts?.filters);\n const queryKey = this.formatQueryKey(queryParams);\n\n // If already fetched before, then noop.\n const maybeQueryStatus = this.store.state.queries[queryKey];\n if (maybeQueryStatus) {\n return maybeQueryStatus;\n }\n\n // Mark this query status as loading.\n this.store.setState((state) => ({\n ...state,\n queries: { ...state.queries, [queryKey]: { status: \"loading\" } },\n }));\n\n let queryStatus: QueryStatus;\n try {\n const data = await this.knock.user.getGuides<\n GetGuidesQueryParams,\n GetGuidesResponse\n >(this.channelId, queryParams);\n queryStatus = { status: \"ok\" };\n\n this.store.setState((state) => ({\n ...state,\n // For now assume a single fetch to get all eligible guides. When/if\n // we implement incremental loads, then this will need to be a merge\n // and sort operation.\n guides: data.entries.map((g) => this.localCopy(g)),\n queries: { ...state.queries, [queryKey]: queryStatus },\n }));\n } catch (e) {\n queryStatus = { status: \"error\", error: e as Error };\n\n this.store.setState((state) => ({\n ...state,\n queries: { ...state.queries, [queryKey]: queryStatus },\n }));\n }\n\n return queryStatus;\n }\n\n subscribe() {\n if (!this.socket) return;\n this.knock.failIfNotAuthenticated();\n this.knock.log(\"[Guide] Subscribing to real time updates\");\n\n // Ensure a live socket connection if not yet connected.\n if (!this.socket.isConnected()) {\n this.socket.connect();\n }\n\n // If there's an existing connected channel, then disconnect.\n if (this.socketChannel) {\n this.unsubscribe();\n }\n\n // Join the channel topic and subscribe to supported events.\n const params = { ...this.targetParams, user_id: this.knock.userId };\n const newChannel = this.socket.channel(this.socketChannelTopic, params);\n\n for (const eventType of this.socketEventTypes) {\n newChannel.on(eventType, (payload) => this.handleSocketEvent(payload));\n }\n\n if ([\"closed\", \"errored\"].includes(newChannel.state)) {\n newChannel.join();\n }\n\n // Track the joined channel.\n this.socketChannel = newChannel;\n }\n\n unsubscribe() {\n if (!this.socketChannel) return;\n this.knock.log(\"[Guide] Unsubscribing from real time updates\");\n\n // Unsubscribe from the socket events and leave the channel.\n for (const eventType of this.socketEventTypes) {\n this.socketChannel.off(eventType);\n }\n this.socketChannel.leave();\n\n // Unset the channel.\n this.socketChannel = undefined;\n }\n\n private handleSocketEvent(payload: GuideSocketEvent) {\n const { event, data } = payload;\n\n switch (event) {\n case \"guide.added\":\n return this.addGuide(payload);\n\n case \"guide.updated\":\n return data.eligible\n ? this.replaceOrAddGuide(payload)\n : this.removeGuide(payload);\n\n case \"guide.removed\":\n return this.removeGuide(payload);\n\n default:\n return;\n }\n }\n\n //\n // Store selector\n //\n\n select(state: StoreState, filters: SelectFilterParams = {}) {\n // TODO(KNO-7790): Need to evaluate activation rules also.\n\n return state.guides.filter((guide) => {\n if (filters.type && filters.type !== guide.type) {\n return false;\n }\n\n if (filters.key && filters.key !== guide.key) {\n return false;\n }\n\n return true;\n });\n }\n\n //\n // Engagement event handlers\n //\n // Make an optimistic update on the client side first, then send an engagement\n // event to the backend.\n //\n\n async markAsSeen(guide: GuideData, step: GuideStepData) {\n this.knock.log(\n `[Guide] Marking as seen (Guide key: ${guide.key}, Step ref:${step.ref})`,\n );\n\n const updatedStep = this.setStepMessageAttrs(guide.key, step.ref, {\n seen_at: new Date().toISOString(),\n });\n if (!updatedStep) return;\n\n const params = {\n ...this.buildEngagementEventBaseParams(guide, updatedStep),\n content: updatedStep.content,\n data: this.targetParams.data,\n tenant: this.targetParams.tenant,\n };\n\n this.knock.user.markGuideStepAs<MarkAsSeenParams, MarkGuideAsResponse>(\n \"seen\",\n params,\n );\n\n return updatedStep;\n }\n\n async markAsInteracted(\n guide: GuideData,\n step: GuideStepData,\n metadata?: GenericData,\n ) {\n this.knock.log(\n `[Guide] Marking as interacted (Guide key: ${guide.key}, Step ref:${step.ref})`,\n );\n\n const ts = new Date().toISOString();\n const updatedStep = this.setStepMessageAttrs(guide.key, step.ref, {\n read_at: ts,\n interacted_at: ts,\n });\n if (!updatedStep) return;\n\n const params = {\n ...this.buildEngagementEventBaseParams(guide, updatedStep),\n metadata,\n };\n\n this.knock.user.markGuideStepAs<\n MarkAsInteractedParams,\n MarkGuideAsResponse\n >(\"interacted\", params);\n\n return updatedStep;\n }\n\n async markAsArchived(guide: GuideData, step: GuideStepData) {\n this.knock.log(\n `[Guide] Marking as archived (Guide key: ${guide.key}, Step ref:${step.ref})`,\n );\n\n const updatedStep = this.setStepMessageAttrs(guide.key, step.ref, {\n archived_at: new Date().toISOString(),\n });\n if (!updatedStep) return;\n\n const params = this.buildEngagementEventBaseParams(guide, updatedStep);\n\n this.knock.user.markGuideStepAs<MarkAsArchivedParams, MarkGuideAsResponse>(\n \"archived\",\n params,\n );\n\n return updatedStep;\n }\n\n //\n // Helpers\n //\n\n private localCopy(remoteGuide: GuideData) {\n // eslint-disable-next-line @typescript-eslint/no-this-alias\n const self = this;\n\n // Build a local copy with helper methods added.\n const localGuide = { ...remoteGuide };\n\n localGuide.steps = remoteGuide.steps.map(({ message, ...rest }) => {\n const localStep = {\n ...rest,\n message: { ...message },\n markAsSeen() {\n // Send a seen event if it has not been previously seen.\n if (this.message.seen_at) return;\n return self.markAsSeen(localGuide, this);\n },\n markAsInteracted({ metadata }: { metadata?: GenericData } = {}) {\n // Always send an interaction event through.\n return self.markAsInteracted(localGuide, this, metadata);\n },\n markAsArchived() {\n // Send an archived event if it has not been previously archived.\n if (this.message.archived_at) return;\n return self.markAsArchived(localGuide, this);\n },\n };\n\n // Bind all engagement action handler methods to the local step object so\n // they can operate on itself.\n localStep.markAsSeen = localStep.markAsSeen.bind(localStep);\n localStep.markAsInteracted = localStep.markAsInteracted.bind(localStep);\n localStep.markAsArchived = localStep.markAsArchived.bind(localStep);\n\n return localStep;\n });\n\n return localGuide as KnockGuide;\n }\n\n private buildQueryParams(filterParams: QueryFilterParams = {}) {\n // Combine the target params with the given filter params.\n const combinedParams = { ...this.targetParams, ...filterParams };\n\n // Prune out any keys that have an undefined or null value.\n let params = Object.fromEntries(\n Object.entries(combinedParams).filter(\n ([_k, v]) => v !== undefined && v !== null,\n ),\n );\n\n // Encode target data as a JSON string, if provided.\n params = params.data\n ? { ...params, data: JSON.stringify(params.data) }\n : params;\n\n return params as GetGuidesQueryParams;\n }\n\n private formatQueryKey(queryParams: GenericData) {\n const sortedKeys = Object.keys(queryParams).sort();\n\n const queryStr = sortedKeys\n .map(\n (key) =>\n `${encodeURIComponent(key)}=${encodeURIComponent(queryParams[key])}`,\n )\n .join(\"&\");\n\n const basePath = guidesApiRootPath(this.knock.userId);\n return queryStr ? `${basePath}?${queryStr}` : basePath;\n }\n\n private setStepMessageAttrs(\n guideKey: string,\n stepRef: string,\n attrs: Partial<StepMessageState>,\n ) {\n let updatedStep: KnockGuideStep | undefined;\n\n this.store.setState((state) => {\n const guides = state.guides.map((guide) => {\n if (guide.key !== guideKey) return guide;\n\n const steps = guide.steps.map((step) => {\n if (step.ref !== stepRef) return step;\n\n // Mutate in place and maintain the same obj ref so to make it easier\n // to use in hook deps.\n step.message = { ...step.message, ...attrs };\n updatedStep = step;\n\n return step;\n });\n return { ...guide, steps };\n });\n return { ...state, guides };\n });\n\n return updatedStep;\n }\n\n private buildEngagementEventBaseParams(\n guide: GuideData,\n step: GuideStepData,\n ) {\n return {\n message_id: step.message.id,\n channel_id: guide.channel_id,\n guide_key: guide.key,\n guide_id: guide.id,\n guide_step_ref: step.ref,\n };\n }\n\n private addGuide({ data }: GuideAddedEvent) {\n const guide = this.localCopy(data.guide);\n\n this.store.setState((state) => {\n return { ...state, guides: sortGuides([...state.guides, guide]) };\n });\n }\n\n private replaceOrAddGuide({ data }: GuideUpdatedEvent) {\n const guide = this.localCopy(data.guide);\n\n this.store.setState((state) => {\n let replaced = false;\n\n const guides = state.guides.map((g) => {\n if (g.key !== guide.key) return g;\n replaced = true;\n return guide;\n });\n\n return {\n ...state,\n guides: replaced ? sortGuides(guides) : sortGuides([...guides, guide]),\n };\n });\n }\n\n private removeGuide({ data }: GuideUpdatedEvent | GuideRemovedEvent) {\n this.store.setState((state) => {\n const guides = state.guides.filter((g) => g.key !== data.guide.key);\n return { ...state, guides };\n });\n }\n}\n"],"names":["sortGuides","guides","a","b","guidesApiRootPath","userId","KnockGuideClient","knock","channelId","targetParams","__publicField","Store","maybeSocket","opts","queryParams","queryKey","maybeQueryStatus","state","queryStatus","data","g","e","params","newChannel","eventType","payload","event","filters","guide","step","updatedStep","metadata","ts","remoteGuide","self","localGuide","message","rest","localStep","filterParams","combinedParams","_k","v","queryStr","key","basePath","guideKey","stepRef","attrs","steps","replaced"],"mappings":"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"}
1
+ {"version":3,"file":"client.js","sources":["../../../../src/clients/guide/client.ts"],"sourcesContent":["import { GenericData } from \"@knocklabs/types\";\nimport { Store } from \"@tanstack/store\";\nimport { Channel, Socket } from \"phoenix\";\nimport { URLPattern } from \"urlpattern-polyfill\";\n\nimport Knock from \"../../knock\";\n\nconst sortGuides = (guides: KnockGuide[]) => {\n return [...guides].sort(\n (a, b) =>\n b.priority - a.priority ||\n new Date(b.inserted_at).getTime() - new Date(a.inserted_at).getTime(),\n );\n};\n\n//\n// Guides API (via User client)\n//\n\nexport const guidesApiRootPath = (userId: string | undefined | null) =>\n `/v1/users/${userId}/guides`;\n\ninterface StepMessageState {\n id: string;\n seen_at: string | null;\n read_at: string | null;\n interacted_at: string | null;\n archived_at: string | null;\n link_clicked_at: string | null;\n}\n\ninterface GuideStepData {\n ref: string;\n schema_key: string;\n schema_semver: string;\n schema_variant_key: string;\n message: StepMessageState;\n // eslint-disable-next-line\n content: any;\n}\n\ninterface GuideActivationLocationRuleData {\n directive: \"allow\" | \"block\";\n pathname: string;\n}\n\ninterface GuideData {\n __typename: \"Guide\";\n channel_id: string;\n id: string;\n key: string;\n priority: number;\n type: string;\n semver: string;\n steps: GuideStepData[];\n activation_location_rules: GuideActivationLocationRuleData[];\n inserted_at: string;\n updated_at: string;\n}\n\nexport interface KnockGuideStep extends GuideStepData {\n markAsSeen: () => void;\n markAsInteracted: (params?: { metadata?: GenericData }) => void;\n markAsArchived: () => void;\n}\n\ninterface KnockGuideActivationLocationRule\n extends GuideActivationLocationRuleData {\n pattern: URLPattern;\n}\n\nexport interface KnockGuide extends GuideData {\n steps: KnockGuideStep[];\n activation_location_rules: KnockGuideActivationLocationRule[];\n}\n\ntype GetGuidesQueryParams = {\n data?: string;\n tenant?: string;\n type?: string;\n};\n\ntype GetGuidesResponse = {\n entries: GuideData[];\n};\n\nexport type GuideEngagementEventBaseParams = {\n // Base params required for all engagement update events\n message_id: string;\n channel_id: string;\n guide_key: string;\n guide_id: string;\n guide_step_ref: string;\n};\n\ntype MarkAsSeenParams = GuideEngagementEventBaseParams & {\n // Rendered step content seen by the recipient\n content: GenericData;\n // Target params\n data?: GenericData;\n tenant?: string;\n};\ntype MarkAsInteractedParams = GuideEngagementEventBaseParams;\ntype MarkAsArchivedParams = GuideEngagementEventBaseParams;\n\ntype MarkGuideAsResponse = {\n status: \"ok\";\n};\n\ntype SocketEventType = \"guide.added\" | \"guide.updated\" | \"guide.removed\";\n\ntype SocketEventPayload<E extends SocketEventType, D> = {\n topic: string;\n event: E;\n data: D;\n};\n\ntype GuideAddedEvent = SocketEventPayload<\n \"guide.added\",\n { guide: GuideData; eligible: true }\n>;\n\ntype GuideUpdatedEvent = SocketEventPayload<\n \"guide.updated\",\n { guide: GuideData; eligible: boolean }\n>;\n\ntype GuideRemovedEvent = SocketEventPayload<\n \"guide.removed\",\n { guide: Pick<GuideData, \"key\"> }\n>;\n\ntype GuideSocketEvent = GuideAddedEvent | GuideUpdatedEvent | GuideRemovedEvent;\n\n//\n// Guides client\n//\n\ntype QueryKey = string;\n\ntype QueryStatus = {\n status: \"loading\" | \"ok\" | \"error\";\n error?: Error;\n};\n\ntype StoreState = {\n guides: KnockGuide[];\n queries: Record<QueryKey, QueryStatus>;\n location: string | undefined;\n};\n\ntype QueryFilterParams = Pick<GetGuidesQueryParams, \"type\">;\n\nexport type SelectFilterParams = {\n key?: string;\n type?: string;\n};\n\nexport type TargetParams = {\n data?: GenericData | undefined;\n tenant?: string | undefined;\n};\n\ntype ConstructorOpts = {\n trackLocationFromWindow?: boolean;\n};\n\nexport class KnockGuideClient {\n public store: Store<StoreState, (state: StoreState) => StoreState>;\n\n // Phoenix channels for real time guide updates over websocket\n private socket: Socket | undefined;\n private socketChannel: Channel | undefined;\n private socketChannelTopic: string;\n private socketEventTypes = [\"guide.added\", \"guide.updated\", \"guide.removed\"];\n\n // Original history methods to monkey patch, or restore in cleanups.\n private pushStateFn: History[\"pushState\"] | undefined;\n private replaceStateFn: History[\"replaceState\"] | undefined;\n\n constructor(\n readonly knock: Knock,\n readonly channelId: string,\n readonly targetParams: TargetParams = {},\n readonly options: ConstructorOpts = {},\n ) {\n const { trackLocationFromWindow = true } = options;\n\n const location = trackLocationFromWindow\n ? window?.location.href\n : undefined;\n\n this.store = new Store<StoreState>({\n guides: [],\n queries: {},\n location,\n });\n\n // In server environments we might not have a socket connection.\n const { socket: maybeSocket } = this.knock.client();\n this.socket = maybeSocket;\n this.socketChannelTopic = `guides:${channelId}`;\n\n if (trackLocationFromWindow) {\n this.listenForLocationChangesFromWindow();\n }\n\n this.knock.log(\"[Guide] Initialized a guide client\");\n }\n\n cleanup() {\n this.unsubscribe();\n this.removeEventListeners();\n }\n\n async fetch(opts?: { filters?: QueryFilterParams }) {\n this.knock.failIfNotAuthenticated();\n this.knock.log(\"[Guide] Loading all eligible guides\");\n\n const queryParams = this.buildQueryParams(opts?.filters);\n const queryKey = this.formatQueryKey(queryParams);\n\n // If already fetched before, then noop.\n const maybeQueryStatus = this.store.state.queries[queryKey];\n if (maybeQueryStatus) {\n return maybeQueryStatus;\n }\n\n // Mark this query status as loading.\n this.store.setState((state) => ({\n ...state,\n queries: { ...state.queries, [queryKey]: { status: \"loading\" } },\n }));\n\n let queryStatus: QueryStatus;\n try {\n const data = await this.knock.user.getGuides<\n GetGuidesQueryParams,\n GetGuidesResponse\n >(this.channelId, queryParams);\n queryStatus = { status: \"ok\" };\n\n this.store.setState((state) => ({\n ...state,\n // For now assume a single fetch to get all eligible guides. When/if\n // we implement incremental loads, then this will need to be a merge\n // and sort operation.\n guides: data.entries.map((g) => this.localCopy(g)),\n queries: { ...state.queries, [queryKey]: queryStatus },\n }));\n } catch (e) {\n queryStatus = { status: \"error\", error: e as Error };\n\n this.store.setState((state) => ({\n ...state,\n queries: { ...state.queries, [queryKey]: queryStatus },\n }));\n }\n\n return queryStatus;\n }\n\n subscribe() {\n if (!this.socket) return;\n this.knock.failIfNotAuthenticated();\n this.knock.log(\"[Guide] Subscribing to real time updates\");\n\n // Ensure a live socket connection if not yet connected.\n if (!this.socket.isConnected()) {\n this.socket.connect();\n }\n\n // If there's an existing connected channel, then disconnect.\n if (this.socketChannel) {\n this.unsubscribe();\n }\n\n // Join the channel topic and subscribe to supported events.\n const params = { ...this.targetParams, user_id: this.knock.userId };\n const newChannel = this.socket.channel(this.socketChannelTopic, params);\n\n for (const eventType of this.socketEventTypes) {\n newChannel.on(eventType, (payload) => this.handleSocketEvent(payload));\n }\n\n if ([\"closed\", \"errored\"].includes(newChannel.state)) {\n newChannel.join();\n }\n\n // Track the joined channel.\n this.socketChannel = newChannel;\n }\n\n unsubscribe() {\n if (!this.socketChannel) return;\n this.knock.log(\"[Guide] Unsubscribing from real time updates\");\n\n // Unsubscribe from the socket events and leave the channel.\n for (const eventType of this.socketEventTypes) {\n this.socketChannel.off(eventType);\n }\n this.socketChannel.leave();\n\n // Unset the channel.\n this.socketChannel = undefined;\n }\n\n private handleSocketEvent(payload: GuideSocketEvent) {\n const { event, data } = payload;\n\n switch (event) {\n case \"guide.added\":\n return this.addGuide(payload);\n\n case \"guide.updated\":\n return data.eligible\n ? this.replaceOrAddGuide(payload)\n : this.removeGuide(payload);\n\n case \"guide.removed\":\n return this.removeGuide(payload);\n\n default:\n return;\n }\n }\n\n //\n // Store selector\n //\n\n select(state: StoreState, filters: SelectFilterParams = {}) {\n return state.guides.filter((guide) => {\n if (filters.type && filters.type !== guide.type) {\n return false;\n }\n\n if (filters.key && filters.key !== guide.key) {\n return false;\n }\n\n const locationRules = guide.activation_location_rules || [];\n\n if (locationRules.length > 0 && state.location) {\n const allowed = locationRules.reduce<boolean | undefined>(\n (acc, rule) => {\n // Any matched block rule prevails so no need to evaluate further\n // as soon as there is one.\n if (acc === false) return false;\n\n // At this point we either have a matched allow rule (acc is true),\n // or no matched rule found yet (acc is undefined).\n\n switch (rule.directive) {\n case \"allow\": {\n // No need to evaluate more allow rules once we matched one\n // since any matched allowed rule means allow.\n if (acc === true) return true;\n\n const matched = rule.pattern.test(state.location);\n return matched ? true : undefined;\n }\n\n case \"block\": {\n // Always test block rules (unless already matched to block)\n // because they'd prevail over matched allow rules.\n const matched = rule.pattern.test(state.location);\n return matched ? false : acc;\n }\n }\n },\n undefined,\n );\n\n if (!allowed) return false;\n }\n\n return true;\n });\n }\n\n //\n // Engagement event handlers\n //\n // Make an optimistic update on the client side first, then send an engagement\n // event to the backend.\n //\n\n async markAsSeen(guide: GuideData, step: GuideStepData) {\n this.knock.log(\n `[Guide] Marking as seen (Guide key: ${guide.key}, Step ref:${step.ref})`,\n );\n\n const updatedStep = this.setStepMessageAttrs(guide.key, step.ref, {\n seen_at: new Date().toISOString(),\n });\n if (!updatedStep) return;\n\n const params = {\n ...this.buildEngagementEventBaseParams(guide, updatedStep),\n content: updatedStep.content,\n data: this.targetParams.data,\n tenant: this.targetParams.tenant,\n };\n\n this.knock.user.markGuideStepAs<MarkAsSeenParams, MarkGuideAsResponse>(\n \"seen\",\n params,\n );\n\n return updatedStep;\n }\n\n async markAsInteracted(\n guide: GuideData,\n step: GuideStepData,\n metadata?: GenericData,\n ) {\n this.knock.log(\n `[Guide] Marking as interacted (Guide key: ${guide.key}, Step ref:${step.ref})`,\n );\n\n const ts = new Date().toISOString();\n const updatedStep = this.setStepMessageAttrs(guide.key, step.ref, {\n read_at: ts,\n interacted_at: ts,\n });\n if (!updatedStep) return;\n\n const params = {\n ...this.buildEngagementEventBaseParams(guide, updatedStep),\n metadata,\n };\n\n this.knock.user.markGuideStepAs<\n MarkAsInteractedParams,\n MarkGuideAsResponse\n >(\"interacted\", params);\n\n return updatedStep;\n }\n\n async markAsArchived(guide: GuideData, step: GuideStepData) {\n this.knock.log(\n `[Guide] Marking as archived (Guide key: ${guide.key}, Step ref:${step.ref})`,\n );\n\n const updatedStep = this.setStepMessageAttrs(guide.key, step.ref, {\n archived_at: new Date().toISOString(),\n });\n if (!updatedStep) return;\n\n const params = this.buildEngagementEventBaseParams(guide, updatedStep);\n\n this.knock.user.markGuideStepAs<MarkAsArchivedParams, MarkGuideAsResponse>(\n \"archived\",\n params,\n );\n\n return updatedStep;\n }\n\n //\n // Helpers\n //\n\n private localCopy(remoteGuide: GuideData) {\n // eslint-disable-next-line @typescript-eslint/no-this-alias\n const self = this;\n\n // Build a local copy with helper methods added.\n const localGuide = { ...remoteGuide };\n\n localGuide.steps = remoteGuide.steps.map(({ message, ...rest }) => {\n const localStep = {\n ...rest,\n message: { ...message },\n markAsSeen() {\n // Send a seen event if it has not been previously seen.\n if (this.message.seen_at) return;\n return self.markAsSeen(localGuide, this);\n },\n markAsInteracted({ metadata }: { metadata?: GenericData } = {}) {\n // Always send an interaction event through.\n return self.markAsInteracted(localGuide, this, metadata);\n },\n markAsArchived() {\n // Send an archived event if it has not been previously archived.\n if (this.message.archived_at) return;\n return self.markAsArchived(localGuide, this);\n },\n };\n\n // Bind all engagement action handler methods to the local step object so\n // they can operate on itself.\n localStep.markAsSeen = localStep.markAsSeen.bind(localStep);\n localStep.markAsInteracted = localStep.markAsInteracted.bind(localStep);\n localStep.markAsArchived = localStep.markAsArchived.bind(localStep);\n\n return localStep;\n });\n\n localGuide.activation_location_rules =\n remoteGuide.activation_location_rules.map((rule) => {\n return {\n ...rule,\n pattern: new URLPattern({ pathname: rule.pathname }),\n };\n });\n\n return localGuide as KnockGuide;\n }\n\n private buildQueryParams(filterParams: QueryFilterParams = {}) {\n // Combine the target params with the given filter params.\n const combinedParams = { ...this.targetParams, ...filterParams };\n\n // Prune out any keys that have an undefined or null value.\n let params = Object.fromEntries(\n Object.entries(combinedParams).filter(\n ([_k, v]) => v !== undefined && v !== null,\n ),\n );\n\n // Encode target data as a JSON string, if provided.\n params = params.data\n ? { ...params, data: JSON.stringify(params.data) }\n : params;\n\n return params as GetGuidesQueryParams;\n }\n\n private formatQueryKey(queryParams: GenericData) {\n const sortedKeys = Object.keys(queryParams).sort();\n\n const queryStr = sortedKeys\n .map(\n (key) =>\n `${encodeURIComponent(key)}=${encodeURIComponent(queryParams[key])}`,\n )\n .join(\"&\");\n\n const basePath = guidesApiRootPath(this.knock.userId);\n return queryStr ? `${basePath}?${queryStr}` : basePath;\n }\n\n private setStepMessageAttrs(\n guideKey: string,\n stepRef: string,\n attrs: Partial<StepMessageState>,\n ) {\n let updatedStep: KnockGuideStep | undefined;\n\n this.store.setState((state) => {\n const guides = state.guides.map((guide) => {\n if (guide.key !== guideKey) return guide;\n\n const steps = guide.steps.map((step) => {\n if (step.ref !== stepRef) return step;\n\n // Mutate in place and maintain the same obj ref so to make it easier\n // to use in hook deps.\n step.message = { ...step.message, ...attrs };\n updatedStep = step;\n\n return step;\n });\n return { ...guide, steps };\n });\n return { ...state, guides };\n });\n\n return updatedStep;\n }\n\n private buildEngagementEventBaseParams(\n guide: GuideData,\n step: GuideStepData,\n ) {\n return {\n message_id: step.message.id,\n channel_id: guide.channel_id,\n guide_key: guide.key,\n guide_id: guide.id,\n guide_step_ref: step.ref,\n };\n }\n\n private addGuide({ data }: GuideAddedEvent) {\n const guide = this.localCopy(data.guide);\n\n this.store.setState((state) => {\n return { ...state, guides: sortGuides([...state.guides, guide]) };\n });\n }\n\n private replaceOrAddGuide({ data }: GuideUpdatedEvent) {\n const guide = this.localCopy(data.guide);\n\n this.store.setState((state) => {\n let replaced = false;\n\n const guides = state.guides.map((g) => {\n if (g.key !== guide.key) return g;\n replaced = true;\n return guide;\n });\n\n return {\n ...state,\n guides: replaced ? sortGuides(guides) : sortGuides([...guides, guide]),\n };\n });\n }\n\n private removeGuide({ data }: GuideUpdatedEvent | GuideRemovedEvent) {\n this.store.setState((state) => {\n const guides = state.guides.filter((g) => g.key !== data.guide.key);\n return { ...state, guides };\n });\n }\n\n private handleLocationChange() {\n const href = window.location.href;\n if (this.store.state.location === href) return;\n\n this.knock.log(`[Guide] Handle Location change: ${href}`);\n\n this.store.setState((state) => ({ ...state, location: href }));\n }\n\n private listenForLocationChangesFromWindow() {\n if (window?.history) {\n // 1. Listen for browser back/forward button clicks.\n window.addEventListener(\"popstate\", this.handleLocationChange);\n\n // 2. Listen for hash changes in case it's used for routing.\n window.addEventListener(\"hashchange\", this.handleLocationChange);\n\n // 3. Monkey-patch history methods to catch programmatic navigation.\n const pushStateFn = window.history.pushState;\n const replaceStateFn = window.history.replaceState;\n\n // Use setTimeout to allow the browser state to potentially settle.\n window.history.pushState = new Proxy(pushStateFn, {\n apply: (target, history, args) => {\n Reflect.apply(target, history, args);\n setTimeout(() => {\n this.handleLocationChange();\n }, 0);\n },\n });\n window.history.replaceState = new Proxy(replaceStateFn, {\n apply: (target, history, args) => {\n Reflect.apply(target, history, args);\n setTimeout(() => {\n this.handleLocationChange();\n }, 0);\n },\n });\n\n // 4. Keep refs to the original handlers so we can restore during cleanup.\n this.pushStateFn = pushStateFn;\n this.replaceStateFn = replaceStateFn;\n } else {\n this.knock.log(\n \"[Guide] Unable to access the `window.history` object to detect location changes\",\n );\n }\n }\n\n private removeEventListeners() {\n window.removeEventListener(\"popstate\", this.handleLocationChange);\n window.removeEventListener(\"hashchange\", this.handleLocationChange);\n\n if (this.pushStateFn) {\n window.history.pushState = this.pushStateFn;\n this.pushStateFn = undefined;\n }\n if (this.replaceStateFn) {\n window.history.replaceState = this.replaceStateFn;\n this.replaceStateFn = undefined;\n }\n }\n}\n"],"names":["sortGuides","guides","a","b","guidesApiRootPath","userId","KnockGuideClient","knock","channelId","targetParams","options","__publicField","trackLocationFromWindow","location","Store","maybeSocket","opts","queryParams","queryKey","maybeQueryStatus","state","queryStatus","data","g","e","params","newChannel","eventType","payload","event","filters","guide","locationRules","acc","rule","step","updatedStep","metadata","ts","remoteGuide","self","localGuide","message","rest","localStep","URLPattern","filterParams","combinedParams","_k","v","queryStr","key","basePath","guideKey","stepRef","attrs","steps","replaced","href","pushStateFn","replaceStateFn","target","history","args"],"mappings":"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"}
@@ -1,59 +1,69 @@
1
- var k = Object.defineProperty;
2
- var l = (o, e, t) => e in o ? k(o, e, { enumerable: !0, configurable: !0, writable: !0, value: t }) : o[e] = t;
3
- var u = (o, e, t) => l(o, typeof e != "symbol" ? e + "" : e, t);
1
+ var l = Object.defineProperty;
2
+ var k = (o, e, t) => e in o ? l(o, e, { enumerable: !0, configurable: !0, writable: !0, value: t }) : o[e] = t;
3
+ var c = (o, e, t) => k(o, typeof e != "symbol" ? e + "" : e, t);
4
4
  import { Store as m } from "@tanstack/store";
5
- const h = (o) => [...o].sort(
5
+ import { URLPattern as p } from "urlpattern-polyfill";
6
+ const u = (o) => [...o].sort(
6
7
  (e, t) => t.priority - e.priority || new Date(t.inserted_at).getTime() - new Date(e.inserted_at).getTime()
7
8
  ), g = (o) => `/v1/users/${o}/guides`;
8
- class p {
9
- constructor(e, t, s = {}) {
10
- u(this, "store");
9
+ class v {
10
+ constructor(e, t, s = {}, n = {}) {
11
+ c(this, "store");
11
12
  // Phoenix channels for real time guide updates over websocket
12
- u(this, "socket");
13
- u(this, "socketChannel");
14
- u(this, "socketChannelTopic");
15
- u(this, "socketEventTypes", ["guide.added", "guide.updated", "guide.removed"]);
16
- this.knock = e, this.channelId = t, this.targetParams = s, this.store = new m({
13
+ c(this, "socket");
14
+ c(this, "socketChannel");
15
+ c(this, "socketChannelTopic");
16
+ c(this, "socketEventTypes", ["guide.added", "guide.updated", "guide.removed"]);
17
+ // Original history methods to monkey patch, or restore in cleanups.
18
+ c(this, "pushStateFn");
19
+ c(this, "replaceStateFn");
20
+ this.knock = e, this.channelId = t, this.targetParams = s, this.options = n;
21
+ const { trackLocationFromWindow: i = !0 } = n, r = i ? window == null ? void 0 : window.location.href : void 0;
22
+ this.store = new m({
17
23
  guides: [],
18
- queries: {}
24
+ queries: {},
25
+ location: r
19
26
  });
20
- const { socket: r } = this.knock.client();
21
- this.socket = r, this.socketChannelTopic = `guides:${t}`, this.knock.log("[Guide] Initialized a guide client");
27
+ const { socket: a } = this.knock.client();
28
+ this.socket = a, this.socketChannelTopic = `guides:${t}`, i && this.listenForLocationChangesFromWindow(), this.knock.log("[Guide] Initialized a guide client");
29
+ }
30
+ cleanup() {
31
+ this.unsubscribe(), this.removeEventListeners();
22
32
  }
23
33
  async fetch(e) {
24
34
  this.knock.failIfNotAuthenticated(), this.knock.log("[Guide] Loading all eligible guides");
25
- const t = this.buildQueryParams(e == null ? void 0 : e.filters), s = this.formatQueryKey(t), r = this.store.state.queries[s];
26
- if (r)
27
- return r;
28
- this.store.setState((i) => ({
29
- ...i,
30
- queries: { ...i.queries, [s]: { status: "loading" } }
35
+ const t = this.buildQueryParams(e == null ? void 0 : e.filters), s = this.formatQueryKey(t), n = this.store.state.queries[s];
36
+ if (n)
37
+ return n;
38
+ this.store.setState((r) => ({
39
+ ...r,
40
+ queries: { ...r.queries, [s]: { status: "loading" } }
31
41
  }));
32
- let n;
42
+ let i;
33
43
  try {
34
- const i = await this.knock.user.getGuides(this.channelId, t);
35
- n = { status: "ok" }, this.store.setState((a) => ({
44
+ const r = await this.knock.user.getGuides(this.channelId, t);
45
+ i = { status: "ok" }, this.store.setState((a) => ({
36
46
  ...a,
37
47
  // For now assume a single fetch to get all eligible guides. When/if
38
48
  // we implement incremental loads, then this will need to be a merge
39
49
  // and sort operation.
40
- guides: i.entries.map((d) => this.localCopy(d)),
41
- queries: { ...a.queries, [s]: n }
50
+ guides: r.entries.map((d) => this.localCopy(d)),
51
+ queries: { ...a.queries, [s]: i }
42
52
  }));
43
- } catch (i) {
44
- n = { status: "error", error: i }, this.store.setState((a) => ({
53
+ } catch (r) {
54
+ i = { status: "error", error: r }, this.store.setState((a) => ({
45
55
  ...a,
46
- queries: { ...a.queries, [s]: n }
56
+ queries: { ...a.queries, [s]: i }
47
57
  }));
48
58
  }
49
- return n;
59
+ return i;
50
60
  }
51
61
  subscribe() {
52
62
  if (!this.socket) return;
53
63
  this.knock.failIfNotAuthenticated(), this.knock.log("[Guide] Subscribing to real time updates"), this.socket.isConnected() || this.socket.connect(), this.socketChannel && this.unsubscribe();
54
64
  const e = { ...this.targetParams, user_id: this.knock.userId }, t = this.socket.channel(this.socketChannelTopic, e);
55
65
  for (const s of this.socketEventTypes)
56
- t.on(s, (r) => this.handleSocketEvent(r));
66
+ t.on(s, (n) => this.handleSocketEvent(n));
57
67
  ["closed", "errored"].includes(t.state) && t.join(), this.socketChannel = t;
58
68
  }
59
69
  unsubscribe() {
@@ -81,7 +91,23 @@ class p {
81
91
  // Store selector
82
92
  //
83
93
  select(e, t = {}) {
84
- return e.guides.filter((s) => !(t.type && t.type !== s.type || t.key && t.key !== s.key));
94
+ return e.guides.filter((s) => {
95
+ if (t.type && t.type !== s.type || t.key && t.key !== s.key)
96
+ return !1;
97
+ const n = s.activation_location_rules || [];
98
+ return !(n.length > 0 && e.location && !n.reduce(
99
+ (r, a) => {
100
+ if (r === !1) return !1;
101
+ switch (a.directive) {
102
+ case "allow":
103
+ return r === !0 || a.pattern.test(e.location) ? !0 : void 0;
104
+ case "block":
105
+ return a.pattern.test(e.location) ? !1 : r;
106
+ }
107
+ },
108
+ void 0
109
+ ));
110
+ });
85
111
  }
86
112
  //
87
113
  // Engagement event handlers
@@ -97,7 +123,7 @@ class p {
97
123
  seen_at: (/* @__PURE__ */ new Date()).toISOString()
98
124
  });
99
125
  if (!s) return;
100
- const r = {
126
+ const n = {
101
127
  ...this.buildEngagementEventBaseParams(e, s),
102
128
  content: s.content,
103
129
  data: this.targetParams.data,
@@ -105,23 +131,23 @@ class p {
105
131
  };
106
132
  return this.knock.user.markGuideStepAs(
107
133
  "seen",
108
- r
134
+ n
109
135
  ), s;
110
136
  }
111
137
  async markAsInteracted(e, t, s) {
112
138
  this.knock.log(
113
139
  `[Guide] Marking as interacted (Guide key: ${e.key}, Step ref:${t.ref})`
114
140
  );
115
- const r = (/* @__PURE__ */ new Date()).toISOString(), n = this.setStepMessageAttrs(e.key, t.ref, {
116
- read_at: r,
117
- interacted_at: r
141
+ const n = (/* @__PURE__ */ new Date()).toISOString(), i = this.setStepMessageAttrs(e.key, t.ref, {
142
+ read_at: n,
143
+ interacted_at: n
118
144
  });
119
- if (!n) return;
120
- const i = {
121
- ...this.buildEngagementEventBaseParams(e, n),
145
+ if (!i) return;
146
+ const r = {
147
+ ...this.buildEngagementEventBaseParams(e, i),
122
148
  metadata: s
123
149
  };
124
- return this.knock.user.markGuideStepAs("interacted", i), n;
150
+ return this.knock.user.markGuideStepAs("interacted", r), i;
125
151
  }
126
152
  async markAsArchived(e, t) {
127
153
  this.knock.log(
@@ -131,10 +157,10 @@ class p {
131
157
  archived_at: (/* @__PURE__ */ new Date()).toISOString()
132
158
  });
133
159
  if (!s) return;
134
- const r = this.buildEngagementEventBaseParams(e, s);
160
+ const n = this.buildEngagementEventBaseParams(e, s);
135
161
  return this.knock.user.markGuideStepAs(
136
162
  "archived",
137
- r
163
+ n
138
164
  ), s;
139
165
  }
140
166
  //
@@ -142,10 +168,10 @@ class p {
142
168
  //
143
169
  localCopy(e) {
144
170
  const t = this, s = { ...e };
145
- return s.steps = e.steps.map(({ message: r, ...n }) => {
146
- const i = {
147
- ...n,
148
- message: { ...r },
171
+ return s.steps = e.steps.map(({ message: n, ...i }) => {
172
+ const r = {
173
+ ...i,
174
+ message: { ...n },
149
175
  markAsSeen() {
150
176
  if (!this.message.seen_at)
151
177
  return t.markAsSeen(s, this);
@@ -158,34 +184,37 @@ class p {
158
184
  return t.markAsArchived(s, this);
159
185
  }
160
186
  };
161
- return i.markAsSeen = i.markAsSeen.bind(i), i.markAsInteracted = i.markAsInteracted.bind(i), i.markAsArchived = i.markAsArchived.bind(i), i;
162
- }), s;
187
+ return r.markAsSeen = r.markAsSeen.bind(r), r.markAsInteracted = r.markAsInteracted.bind(r), r.markAsArchived = r.markAsArchived.bind(r), r;
188
+ }), s.activation_location_rules = e.activation_location_rules.map((n) => ({
189
+ ...n,
190
+ pattern: new p({ pathname: n.pathname })
191
+ })), s;
163
192
  }
164
193
  buildQueryParams(e = {}) {
165
194
  const t = { ...this.targetParams, ...e };
166
195
  let s = Object.fromEntries(
167
196
  Object.entries(t).filter(
168
- ([r, n]) => n != null
197
+ ([n, i]) => i != null
169
198
  )
170
199
  );
171
200
  return s = s.data ? { ...s, data: JSON.stringify(s.data) } : s, s;
172
201
  }
173
202
  formatQueryKey(e) {
174
203
  const s = Object.keys(e).sort().map(
175
- (n) => `${encodeURIComponent(n)}=${encodeURIComponent(e[n])}`
176
- ).join("&"), r = g(this.knock.userId);
177
- return s ? `${r}?${s}` : r;
204
+ (i) => `${encodeURIComponent(i)}=${encodeURIComponent(e[i])}`
205
+ ).join("&"), n = g(this.knock.userId);
206
+ return s ? `${n}?${s}` : n;
178
207
  }
179
208
  setStepMessageAttrs(e, t, s) {
180
- let r;
181
- return this.store.setState((n) => {
182
- const i = n.guides.map((a) => {
209
+ let n;
210
+ return this.store.setState((i) => {
211
+ const r = i.guides.map((a) => {
183
212
  if (a.key !== e) return a;
184
- const d = a.steps.map((c) => (c.ref !== t || (c.message = { ...c.message, ...s }, r = c), c));
213
+ const d = a.steps.map((h) => (h.ref !== t || (h.message = { ...h.message, ...s }, n = h), h));
185
214
  return { ...a, steps: d };
186
215
  });
187
- return { ...n, guides: i };
188
- }), r;
216
+ return { ...i, guides: r };
217
+ }), n;
189
218
  }
190
219
  buildEngagementEventBaseParams(e, t) {
191
220
  return {
@@ -198,28 +227,57 @@ class p {
198
227
  }
199
228
  addGuide({ data: e }) {
200
229
  const t = this.localCopy(e.guide);
201
- this.store.setState((s) => ({ ...s, guides: h([...s.guides, t]) }));
230
+ this.store.setState((s) => ({ ...s, guides: u([...s.guides, t]) }));
202
231
  }
203
232
  replaceOrAddGuide({ data: e }) {
204
233
  const t = this.localCopy(e.guide);
205
234
  this.store.setState((s) => {
206
- let r = !1;
207
- const n = s.guides.map((i) => i.key !== t.key ? i : (r = !0, t));
235
+ let n = !1;
236
+ const i = s.guides.map((r) => r.key !== t.key ? r : (n = !0, t));
208
237
  return {
209
238
  ...s,
210
- guides: h(r ? n : [...n, t])
239
+ guides: u(n ? i : [...i, t])
211
240
  };
212
241
  });
213
242
  }
214
243
  removeGuide({ data: e }) {
215
244
  this.store.setState((t) => {
216
- const s = t.guides.filter((r) => r.key !== e.guide.key);
245
+ const s = t.guides.filter((n) => n.key !== e.guide.key);
217
246
  return { ...t, guides: s };
218
247
  });
219
248
  }
249
+ handleLocationChange() {
250
+ const e = window.location.href;
251
+ this.store.state.location !== e && (this.knock.log(`[Guide] Handle Location change: ${e}`), this.store.setState((t) => ({ ...t, location: e })));
252
+ }
253
+ listenForLocationChangesFromWindow() {
254
+ if (window != null && window.history) {
255
+ window.addEventListener("popstate", this.handleLocationChange), window.addEventListener("hashchange", this.handleLocationChange);
256
+ const e = window.history.pushState, t = window.history.replaceState;
257
+ window.history.pushState = new Proxy(e, {
258
+ apply: (s, n, i) => {
259
+ Reflect.apply(s, n, i), setTimeout(() => {
260
+ this.handleLocationChange();
261
+ }, 0);
262
+ }
263
+ }), window.history.replaceState = new Proxy(t, {
264
+ apply: (s, n, i) => {
265
+ Reflect.apply(s, n, i), setTimeout(() => {
266
+ this.handleLocationChange();
267
+ }, 0);
268
+ }
269
+ }), this.pushStateFn = e, this.replaceStateFn = t;
270
+ } else
271
+ this.knock.log(
272
+ "[Guide] Unable to access the `window.history` object to detect location changes"
273
+ );
274
+ }
275
+ removeEventListeners() {
276
+ window.removeEventListener("popstate", this.handleLocationChange), window.removeEventListener("hashchange", this.handleLocationChange), this.pushStateFn && (window.history.pushState = this.pushStateFn, this.pushStateFn = void 0), this.replaceStateFn && (window.history.replaceState = this.replaceStateFn, this.replaceStateFn = void 0);
277
+ }
220
278
  }
221
279
  export {
222
- p as KnockGuideClient,
280
+ v as KnockGuideClient,
223
281
  g as guidesApiRootPath
224
282
  };
225
283
  //# sourceMappingURL=client.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"client.mjs","sources":["../../../../src/clients/guide/client.ts"],"sourcesContent":["import { GenericData } from \"@knocklabs/types\";\nimport { Store } from \"@tanstack/store\";\nimport { Channel, Socket } from \"phoenix\";\n\nimport Knock from \"../../knock\";\n\nconst sortGuides = (guides: KnockGuide[]) => {\n return [...guides].sort(\n (a, b) =>\n b.priority - a.priority ||\n new Date(b.inserted_at).getTime() - new Date(a.inserted_at).getTime(),\n );\n};\n\n//\n// Guides API (via User client)\n//\n\nexport const guidesApiRootPath = (userId: string | undefined | null) =>\n `/v1/users/${userId}/guides`;\n\ninterface StepMessageState {\n id: string;\n seen_at: string | null;\n read_at: string | null;\n interacted_at: string | null;\n archived_at: string | null;\n link_clicked_at: string | null;\n}\n\ninterface GuideStepData {\n ref: string;\n schema_key: string;\n schema_semver: string;\n schema_variant_key: string;\n message: StepMessageState;\n // eslint-disable-next-line\n content: any;\n}\n\ninterface GuideData {\n __typename: \"Guide\";\n channel_id: string;\n id: string;\n key: string;\n priority: number;\n type: string;\n semver: string;\n steps: GuideStepData[];\n inserted_at: string;\n updated_at: string;\n}\n\nexport interface KnockGuideStep extends GuideStepData {\n markAsSeen: () => void;\n markAsInteracted: (params?: { metadata?: GenericData }) => void;\n markAsArchived: () => void;\n}\n\nexport interface KnockGuide extends GuideData {\n steps: KnockGuideStep[];\n}\n\ntype GetGuidesQueryParams = {\n data?: string;\n tenant?: string;\n type?: string;\n};\n\ntype GetGuidesResponse = {\n entries: GuideData[];\n};\n\nexport type GuideEngagementEventBaseParams = {\n // Base params required for all engagement update events\n message_id: string;\n channel_id: string;\n guide_key: string;\n guide_id: string;\n guide_step_ref: string;\n};\n\ntype MarkAsSeenParams = GuideEngagementEventBaseParams & {\n // Rendered step content seen by the recipient\n content: GenericData;\n // Target params\n data?: GenericData;\n tenant?: string;\n};\ntype MarkAsInteractedParams = GuideEngagementEventBaseParams;\ntype MarkAsArchivedParams = GuideEngagementEventBaseParams;\n\ntype MarkGuideAsResponse = {\n status: \"ok\";\n};\n\ntype SocketEventType = \"guide.added\" | \"guide.updated\" | \"guide.removed\";\n\ntype SocketEventPayload<E extends SocketEventType, D> = {\n topic: string;\n event: E;\n data: D;\n};\n\ntype GuideAddedEvent = SocketEventPayload<\n \"guide.added\",\n { guide: GuideData; eligible: true }\n>;\n\ntype GuideUpdatedEvent = SocketEventPayload<\n \"guide.updated\",\n { guide: GuideData; eligible: boolean }\n>;\n\ntype GuideRemovedEvent = SocketEventPayload<\n \"guide.removed\",\n { guide: Pick<GuideData, \"key\"> }\n>;\n\ntype GuideSocketEvent = GuideAddedEvent | GuideUpdatedEvent | GuideRemovedEvent;\n\n//\n// Guides client\n//\n\ntype QueryKey = string;\n\ntype QueryStatus = {\n status: \"loading\" | \"ok\" | \"error\";\n error?: Error;\n};\n\ntype StoreState = {\n guides: KnockGuide[];\n queries: Record<QueryKey, QueryStatus>;\n};\n\ntype QueryFilterParams = Pick<GetGuidesQueryParams, \"type\">;\n\nexport type SelectFilterParams = {\n key?: string;\n type?: string;\n};\n\nexport type TargetParams = {\n data?: GenericData | undefined;\n tenant?: string | undefined;\n};\n\nexport class KnockGuideClient {\n public store: Store<StoreState, (state: StoreState) => StoreState>;\n\n // Phoenix channels for real time guide updates over websocket\n private socket: Socket | undefined;\n private socketChannel: Channel | undefined;\n private socketChannelTopic: string;\n private socketEventTypes = [\"guide.added\", \"guide.updated\", \"guide.removed\"];\n\n constructor(\n readonly knock: Knock,\n readonly channelId: string,\n readonly targetParams: TargetParams = {},\n ) {\n this.store = new Store<StoreState>({\n guides: [],\n queries: {},\n });\n\n // In server environments we might not have a socket connection.\n const { socket: maybeSocket } = this.knock.client();\n this.socket = maybeSocket;\n this.socketChannelTopic = `guides:${channelId}`;\n\n this.knock.log(\"[Guide] Initialized a guide client\");\n }\n\n async fetch(opts?: { filters?: QueryFilterParams }) {\n this.knock.failIfNotAuthenticated();\n this.knock.log(\"[Guide] Loading all eligible guides\");\n\n const queryParams = this.buildQueryParams(opts?.filters);\n const queryKey = this.formatQueryKey(queryParams);\n\n // If already fetched before, then noop.\n const maybeQueryStatus = this.store.state.queries[queryKey];\n if (maybeQueryStatus) {\n return maybeQueryStatus;\n }\n\n // Mark this query status as loading.\n this.store.setState((state) => ({\n ...state,\n queries: { ...state.queries, [queryKey]: { status: \"loading\" } },\n }));\n\n let queryStatus: QueryStatus;\n try {\n const data = await this.knock.user.getGuides<\n GetGuidesQueryParams,\n GetGuidesResponse\n >(this.channelId, queryParams);\n queryStatus = { status: \"ok\" };\n\n this.store.setState((state) => ({\n ...state,\n // For now assume a single fetch to get all eligible guides. When/if\n // we implement incremental loads, then this will need to be a merge\n // and sort operation.\n guides: data.entries.map((g) => this.localCopy(g)),\n queries: { ...state.queries, [queryKey]: queryStatus },\n }));\n } catch (e) {\n queryStatus = { status: \"error\", error: e as Error };\n\n this.store.setState((state) => ({\n ...state,\n queries: { ...state.queries, [queryKey]: queryStatus },\n }));\n }\n\n return queryStatus;\n }\n\n subscribe() {\n if (!this.socket) return;\n this.knock.failIfNotAuthenticated();\n this.knock.log(\"[Guide] Subscribing to real time updates\");\n\n // Ensure a live socket connection if not yet connected.\n if (!this.socket.isConnected()) {\n this.socket.connect();\n }\n\n // If there's an existing connected channel, then disconnect.\n if (this.socketChannel) {\n this.unsubscribe();\n }\n\n // Join the channel topic and subscribe to supported events.\n const params = { ...this.targetParams, user_id: this.knock.userId };\n const newChannel = this.socket.channel(this.socketChannelTopic, params);\n\n for (const eventType of this.socketEventTypes) {\n newChannel.on(eventType, (payload) => this.handleSocketEvent(payload));\n }\n\n if ([\"closed\", \"errored\"].includes(newChannel.state)) {\n newChannel.join();\n }\n\n // Track the joined channel.\n this.socketChannel = newChannel;\n }\n\n unsubscribe() {\n if (!this.socketChannel) return;\n this.knock.log(\"[Guide] Unsubscribing from real time updates\");\n\n // Unsubscribe from the socket events and leave the channel.\n for (const eventType of this.socketEventTypes) {\n this.socketChannel.off(eventType);\n }\n this.socketChannel.leave();\n\n // Unset the channel.\n this.socketChannel = undefined;\n }\n\n private handleSocketEvent(payload: GuideSocketEvent) {\n const { event, data } = payload;\n\n switch (event) {\n case \"guide.added\":\n return this.addGuide(payload);\n\n case \"guide.updated\":\n return data.eligible\n ? this.replaceOrAddGuide(payload)\n : this.removeGuide(payload);\n\n case \"guide.removed\":\n return this.removeGuide(payload);\n\n default:\n return;\n }\n }\n\n //\n // Store selector\n //\n\n select(state: StoreState, filters: SelectFilterParams = {}) {\n // TODO(KNO-7790): Need to evaluate activation rules also.\n\n return state.guides.filter((guide) => {\n if (filters.type && filters.type !== guide.type) {\n return false;\n }\n\n if (filters.key && filters.key !== guide.key) {\n return false;\n }\n\n return true;\n });\n }\n\n //\n // Engagement event handlers\n //\n // Make an optimistic update on the client side first, then send an engagement\n // event to the backend.\n //\n\n async markAsSeen(guide: GuideData, step: GuideStepData) {\n this.knock.log(\n `[Guide] Marking as seen (Guide key: ${guide.key}, Step ref:${step.ref})`,\n );\n\n const updatedStep = this.setStepMessageAttrs(guide.key, step.ref, {\n seen_at: new Date().toISOString(),\n });\n if (!updatedStep) return;\n\n const params = {\n ...this.buildEngagementEventBaseParams(guide, updatedStep),\n content: updatedStep.content,\n data: this.targetParams.data,\n tenant: this.targetParams.tenant,\n };\n\n this.knock.user.markGuideStepAs<MarkAsSeenParams, MarkGuideAsResponse>(\n \"seen\",\n params,\n );\n\n return updatedStep;\n }\n\n async markAsInteracted(\n guide: GuideData,\n step: GuideStepData,\n metadata?: GenericData,\n ) {\n this.knock.log(\n `[Guide] Marking as interacted (Guide key: ${guide.key}, Step ref:${step.ref})`,\n );\n\n const ts = new Date().toISOString();\n const updatedStep = this.setStepMessageAttrs(guide.key, step.ref, {\n read_at: ts,\n interacted_at: ts,\n });\n if (!updatedStep) return;\n\n const params = {\n ...this.buildEngagementEventBaseParams(guide, updatedStep),\n metadata,\n };\n\n this.knock.user.markGuideStepAs<\n MarkAsInteractedParams,\n MarkGuideAsResponse\n >(\"interacted\", params);\n\n return updatedStep;\n }\n\n async markAsArchived(guide: GuideData, step: GuideStepData) {\n this.knock.log(\n `[Guide] Marking as archived (Guide key: ${guide.key}, Step ref:${step.ref})`,\n );\n\n const updatedStep = this.setStepMessageAttrs(guide.key, step.ref, {\n archived_at: new Date().toISOString(),\n });\n if (!updatedStep) return;\n\n const params = this.buildEngagementEventBaseParams(guide, updatedStep);\n\n this.knock.user.markGuideStepAs<MarkAsArchivedParams, MarkGuideAsResponse>(\n \"archived\",\n params,\n );\n\n return updatedStep;\n }\n\n //\n // Helpers\n //\n\n private localCopy(remoteGuide: GuideData) {\n // eslint-disable-next-line @typescript-eslint/no-this-alias\n const self = this;\n\n // Build a local copy with helper methods added.\n const localGuide = { ...remoteGuide };\n\n localGuide.steps = remoteGuide.steps.map(({ message, ...rest }) => {\n const localStep = {\n ...rest,\n message: { ...message },\n markAsSeen() {\n // Send a seen event if it has not been previously seen.\n if (this.message.seen_at) return;\n return self.markAsSeen(localGuide, this);\n },\n markAsInteracted({ metadata }: { metadata?: GenericData } = {}) {\n // Always send an interaction event through.\n return self.markAsInteracted(localGuide, this, metadata);\n },\n markAsArchived() {\n // Send an archived event if it has not been previously archived.\n if (this.message.archived_at) return;\n return self.markAsArchived(localGuide, this);\n },\n };\n\n // Bind all engagement action handler methods to the local step object so\n // they can operate on itself.\n localStep.markAsSeen = localStep.markAsSeen.bind(localStep);\n localStep.markAsInteracted = localStep.markAsInteracted.bind(localStep);\n localStep.markAsArchived = localStep.markAsArchived.bind(localStep);\n\n return localStep;\n });\n\n return localGuide as KnockGuide;\n }\n\n private buildQueryParams(filterParams: QueryFilterParams = {}) {\n // Combine the target params with the given filter params.\n const combinedParams = { ...this.targetParams, ...filterParams };\n\n // Prune out any keys that have an undefined or null value.\n let params = Object.fromEntries(\n Object.entries(combinedParams).filter(\n ([_k, v]) => v !== undefined && v !== null,\n ),\n );\n\n // Encode target data as a JSON string, if provided.\n params = params.data\n ? { ...params, data: JSON.stringify(params.data) }\n : params;\n\n return params as GetGuidesQueryParams;\n }\n\n private formatQueryKey(queryParams: GenericData) {\n const sortedKeys = Object.keys(queryParams).sort();\n\n const queryStr = sortedKeys\n .map(\n (key) =>\n `${encodeURIComponent(key)}=${encodeURIComponent(queryParams[key])}`,\n )\n .join(\"&\");\n\n const basePath = guidesApiRootPath(this.knock.userId);\n return queryStr ? `${basePath}?${queryStr}` : basePath;\n }\n\n private setStepMessageAttrs(\n guideKey: string,\n stepRef: string,\n attrs: Partial<StepMessageState>,\n ) {\n let updatedStep: KnockGuideStep | undefined;\n\n this.store.setState((state) => {\n const guides = state.guides.map((guide) => {\n if (guide.key !== guideKey) return guide;\n\n const steps = guide.steps.map((step) => {\n if (step.ref !== stepRef) return step;\n\n // Mutate in place and maintain the same obj ref so to make it easier\n // to use in hook deps.\n step.message = { ...step.message, ...attrs };\n updatedStep = step;\n\n return step;\n });\n return { ...guide, steps };\n });\n return { ...state, guides };\n });\n\n return updatedStep;\n }\n\n private buildEngagementEventBaseParams(\n guide: GuideData,\n step: GuideStepData,\n ) {\n return {\n message_id: step.message.id,\n channel_id: guide.channel_id,\n guide_key: guide.key,\n guide_id: guide.id,\n guide_step_ref: step.ref,\n };\n }\n\n private addGuide({ data }: GuideAddedEvent) {\n const guide = this.localCopy(data.guide);\n\n this.store.setState((state) => {\n return { ...state, guides: sortGuides([...state.guides, guide]) };\n });\n }\n\n private replaceOrAddGuide({ data }: GuideUpdatedEvent) {\n const guide = this.localCopy(data.guide);\n\n this.store.setState((state) => {\n let replaced = false;\n\n const guides = state.guides.map((g) => {\n if (g.key !== guide.key) return g;\n replaced = true;\n return guide;\n });\n\n return {\n ...state,\n guides: replaced ? sortGuides(guides) : sortGuides([...guides, guide]),\n };\n });\n }\n\n private removeGuide({ data }: GuideUpdatedEvent | GuideRemovedEvent) {\n this.store.setState((state) => {\n const guides = state.guides.filter((g) => g.key !== data.guide.key);\n return { ...state, guides };\n });\n }\n}\n"],"names":["sortGuides","guides","a","b","guidesApiRootPath","userId","KnockGuideClient","knock","channelId","targetParams","__publicField","Store","maybeSocket","opts","queryParams","queryKey","maybeQueryStatus","state","queryStatus","data","g","e","params","newChannel","eventType","payload","event","filters","guide","step","updatedStep","metadata","ts","remoteGuide","self","localGuide","message","rest","localStep","filterParams","combinedParams","_k","v","queryStr","key","basePath","guideKey","stepRef","attrs","steps","replaced"],"mappings":"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"}
1
+ {"version":3,"file":"client.mjs","sources":["../../../../src/clients/guide/client.ts"],"sourcesContent":["import { GenericData } from \"@knocklabs/types\";\nimport { Store } from \"@tanstack/store\";\nimport { Channel, Socket } from \"phoenix\";\nimport { URLPattern } from \"urlpattern-polyfill\";\n\nimport Knock from \"../../knock\";\n\nconst sortGuides = (guides: KnockGuide[]) => {\n return [...guides].sort(\n (a, b) =>\n b.priority - a.priority ||\n new Date(b.inserted_at).getTime() - new Date(a.inserted_at).getTime(),\n );\n};\n\n//\n// Guides API (via User client)\n//\n\nexport const guidesApiRootPath = (userId: string | undefined | null) =>\n `/v1/users/${userId}/guides`;\n\ninterface StepMessageState {\n id: string;\n seen_at: string | null;\n read_at: string | null;\n interacted_at: string | null;\n archived_at: string | null;\n link_clicked_at: string | null;\n}\n\ninterface GuideStepData {\n ref: string;\n schema_key: string;\n schema_semver: string;\n schema_variant_key: string;\n message: StepMessageState;\n // eslint-disable-next-line\n content: any;\n}\n\ninterface GuideActivationLocationRuleData {\n directive: \"allow\" | \"block\";\n pathname: string;\n}\n\ninterface GuideData {\n __typename: \"Guide\";\n channel_id: string;\n id: string;\n key: string;\n priority: number;\n type: string;\n semver: string;\n steps: GuideStepData[];\n activation_location_rules: GuideActivationLocationRuleData[];\n inserted_at: string;\n updated_at: string;\n}\n\nexport interface KnockGuideStep extends GuideStepData {\n markAsSeen: () => void;\n markAsInteracted: (params?: { metadata?: GenericData }) => void;\n markAsArchived: () => void;\n}\n\ninterface KnockGuideActivationLocationRule\n extends GuideActivationLocationRuleData {\n pattern: URLPattern;\n}\n\nexport interface KnockGuide extends GuideData {\n steps: KnockGuideStep[];\n activation_location_rules: KnockGuideActivationLocationRule[];\n}\n\ntype GetGuidesQueryParams = {\n data?: string;\n tenant?: string;\n type?: string;\n};\n\ntype GetGuidesResponse = {\n entries: GuideData[];\n};\n\nexport type GuideEngagementEventBaseParams = {\n // Base params required for all engagement update events\n message_id: string;\n channel_id: string;\n guide_key: string;\n guide_id: string;\n guide_step_ref: string;\n};\n\ntype MarkAsSeenParams = GuideEngagementEventBaseParams & {\n // Rendered step content seen by the recipient\n content: GenericData;\n // Target params\n data?: GenericData;\n tenant?: string;\n};\ntype MarkAsInteractedParams = GuideEngagementEventBaseParams;\ntype MarkAsArchivedParams = GuideEngagementEventBaseParams;\n\ntype MarkGuideAsResponse = {\n status: \"ok\";\n};\n\ntype SocketEventType = \"guide.added\" | \"guide.updated\" | \"guide.removed\";\n\ntype SocketEventPayload<E extends SocketEventType, D> = {\n topic: string;\n event: E;\n data: D;\n};\n\ntype GuideAddedEvent = SocketEventPayload<\n \"guide.added\",\n { guide: GuideData; eligible: true }\n>;\n\ntype GuideUpdatedEvent = SocketEventPayload<\n \"guide.updated\",\n { guide: GuideData; eligible: boolean }\n>;\n\ntype GuideRemovedEvent = SocketEventPayload<\n \"guide.removed\",\n { guide: Pick<GuideData, \"key\"> }\n>;\n\ntype GuideSocketEvent = GuideAddedEvent | GuideUpdatedEvent | GuideRemovedEvent;\n\n//\n// Guides client\n//\n\ntype QueryKey = string;\n\ntype QueryStatus = {\n status: \"loading\" | \"ok\" | \"error\";\n error?: Error;\n};\n\ntype StoreState = {\n guides: KnockGuide[];\n queries: Record<QueryKey, QueryStatus>;\n location: string | undefined;\n};\n\ntype QueryFilterParams = Pick<GetGuidesQueryParams, \"type\">;\n\nexport type SelectFilterParams = {\n key?: string;\n type?: string;\n};\n\nexport type TargetParams = {\n data?: GenericData | undefined;\n tenant?: string | undefined;\n};\n\ntype ConstructorOpts = {\n trackLocationFromWindow?: boolean;\n};\n\nexport class KnockGuideClient {\n public store: Store<StoreState, (state: StoreState) => StoreState>;\n\n // Phoenix channels for real time guide updates over websocket\n private socket: Socket | undefined;\n private socketChannel: Channel | undefined;\n private socketChannelTopic: string;\n private socketEventTypes = [\"guide.added\", \"guide.updated\", \"guide.removed\"];\n\n // Original history methods to monkey patch, or restore in cleanups.\n private pushStateFn: History[\"pushState\"] | undefined;\n private replaceStateFn: History[\"replaceState\"] | undefined;\n\n constructor(\n readonly knock: Knock,\n readonly channelId: string,\n readonly targetParams: TargetParams = {},\n readonly options: ConstructorOpts = {},\n ) {\n const { trackLocationFromWindow = true } = options;\n\n const location = trackLocationFromWindow\n ? window?.location.href\n : undefined;\n\n this.store = new Store<StoreState>({\n guides: [],\n queries: {},\n location,\n });\n\n // In server environments we might not have a socket connection.\n const { socket: maybeSocket } = this.knock.client();\n this.socket = maybeSocket;\n this.socketChannelTopic = `guides:${channelId}`;\n\n if (trackLocationFromWindow) {\n this.listenForLocationChangesFromWindow();\n }\n\n this.knock.log(\"[Guide] Initialized a guide client\");\n }\n\n cleanup() {\n this.unsubscribe();\n this.removeEventListeners();\n }\n\n async fetch(opts?: { filters?: QueryFilterParams }) {\n this.knock.failIfNotAuthenticated();\n this.knock.log(\"[Guide] Loading all eligible guides\");\n\n const queryParams = this.buildQueryParams(opts?.filters);\n const queryKey = this.formatQueryKey(queryParams);\n\n // If already fetched before, then noop.\n const maybeQueryStatus = this.store.state.queries[queryKey];\n if (maybeQueryStatus) {\n return maybeQueryStatus;\n }\n\n // Mark this query status as loading.\n this.store.setState((state) => ({\n ...state,\n queries: { ...state.queries, [queryKey]: { status: \"loading\" } },\n }));\n\n let queryStatus: QueryStatus;\n try {\n const data = await this.knock.user.getGuides<\n GetGuidesQueryParams,\n GetGuidesResponse\n >(this.channelId, queryParams);\n queryStatus = { status: \"ok\" };\n\n this.store.setState((state) => ({\n ...state,\n // For now assume a single fetch to get all eligible guides. When/if\n // we implement incremental loads, then this will need to be a merge\n // and sort operation.\n guides: data.entries.map((g) => this.localCopy(g)),\n queries: { ...state.queries, [queryKey]: queryStatus },\n }));\n } catch (e) {\n queryStatus = { status: \"error\", error: e as Error };\n\n this.store.setState((state) => ({\n ...state,\n queries: { ...state.queries, [queryKey]: queryStatus },\n }));\n }\n\n return queryStatus;\n }\n\n subscribe() {\n if (!this.socket) return;\n this.knock.failIfNotAuthenticated();\n this.knock.log(\"[Guide] Subscribing to real time updates\");\n\n // Ensure a live socket connection if not yet connected.\n if (!this.socket.isConnected()) {\n this.socket.connect();\n }\n\n // If there's an existing connected channel, then disconnect.\n if (this.socketChannel) {\n this.unsubscribe();\n }\n\n // Join the channel topic and subscribe to supported events.\n const params = { ...this.targetParams, user_id: this.knock.userId };\n const newChannel = this.socket.channel(this.socketChannelTopic, params);\n\n for (const eventType of this.socketEventTypes) {\n newChannel.on(eventType, (payload) => this.handleSocketEvent(payload));\n }\n\n if ([\"closed\", \"errored\"].includes(newChannel.state)) {\n newChannel.join();\n }\n\n // Track the joined channel.\n this.socketChannel = newChannel;\n }\n\n unsubscribe() {\n if (!this.socketChannel) return;\n this.knock.log(\"[Guide] Unsubscribing from real time updates\");\n\n // Unsubscribe from the socket events and leave the channel.\n for (const eventType of this.socketEventTypes) {\n this.socketChannel.off(eventType);\n }\n this.socketChannel.leave();\n\n // Unset the channel.\n this.socketChannel = undefined;\n }\n\n private handleSocketEvent(payload: GuideSocketEvent) {\n const { event, data } = payload;\n\n switch (event) {\n case \"guide.added\":\n return this.addGuide(payload);\n\n case \"guide.updated\":\n return data.eligible\n ? this.replaceOrAddGuide(payload)\n : this.removeGuide(payload);\n\n case \"guide.removed\":\n return this.removeGuide(payload);\n\n default:\n return;\n }\n }\n\n //\n // Store selector\n //\n\n select(state: StoreState, filters: SelectFilterParams = {}) {\n return state.guides.filter((guide) => {\n if (filters.type && filters.type !== guide.type) {\n return false;\n }\n\n if (filters.key && filters.key !== guide.key) {\n return false;\n }\n\n const locationRules = guide.activation_location_rules || [];\n\n if (locationRules.length > 0 && state.location) {\n const allowed = locationRules.reduce<boolean | undefined>(\n (acc, rule) => {\n // Any matched block rule prevails so no need to evaluate further\n // as soon as there is one.\n if (acc === false) return false;\n\n // At this point we either have a matched allow rule (acc is true),\n // or no matched rule found yet (acc is undefined).\n\n switch (rule.directive) {\n case \"allow\": {\n // No need to evaluate more allow rules once we matched one\n // since any matched allowed rule means allow.\n if (acc === true) return true;\n\n const matched = rule.pattern.test(state.location);\n return matched ? true : undefined;\n }\n\n case \"block\": {\n // Always test block rules (unless already matched to block)\n // because they'd prevail over matched allow rules.\n const matched = rule.pattern.test(state.location);\n return matched ? false : acc;\n }\n }\n },\n undefined,\n );\n\n if (!allowed) return false;\n }\n\n return true;\n });\n }\n\n //\n // Engagement event handlers\n //\n // Make an optimistic update on the client side first, then send an engagement\n // event to the backend.\n //\n\n async markAsSeen(guide: GuideData, step: GuideStepData) {\n this.knock.log(\n `[Guide] Marking as seen (Guide key: ${guide.key}, Step ref:${step.ref})`,\n );\n\n const updatedStep = this.setStepMessageAttrs(guide.key, step.ref, {\n seen_at: new Date().toISOString(),\n });\n if (!updatedStep) return;\n\n const params = {\n ...this.buildEngagementEventBaseParams(guide, updatedStep),\n content: updatedStep.content,\n data: this.targetParams.data,\n tenant: this.targetParams.tenant,\n };\n\n this.knock.user.markGuideStepAs<MarkAsSeenParams, MarkGuideAsResponse>(\n \"seen\",\n params,\n );\n\n return updatedStep;\n }\n\n async markAsInteracted(\n guide: GuideData,\n step: GuideStepData,\n metadata?: GenericData,\n ) {\n this.knock.log(\n `[Guide] Marking as interacted (Guide key: ${guide.key}, Step ref:${step.ref})`,\n );\n\n const ts = new Date().toISOString();\n const updatedStep = this.setStepMessageAttrs(guide.key, step.ref, {\n read_at: ts,\n interacted_at: ts,\n });\n if (!updatedStep) return;\n\n const params = {\n ...this.buildEngagementEventBaseParams(guide, updatedStep),\n metadata,\n };\n\n this.knock.user.markGuideStepAs<\n MarkAsInteractedParams,\n MarkGuideAsResponse\n >(\"interacted\", params);\n\n return updatedStep;\n }\n\n async markAsArchived(guide: GuideData, step: GuideStepData) {\n this.knock.log(\n `[Guide] Marking as archived (Guide key: ${guide.key}, Step ref:${step.ref})`,\n );\n\n const updatedStep = this.setStepMessageAttrs(guide.key, step.ref, {\n archived_at: new Date().toISOString(),\n });\n if (!updatedStep) return;\n\n const params = this.buildEngagementEventBaseParams(guide, updatedStep);\n\n this.knock.user.markGuideStepAs<MarkAsArchivedParams, MarkGuideAsResponse>(\n \"archived\",\n params,\n );\n\n return updatedStep;\n }\n\n //\n // Helpers\n //\n\n private localCopy(remoteGuide: GuideData) {\n // eslint-disable-next-line @typescript-eslint/no-this-alias\n const self = this;\n\n // Build a local copy with helper methods added.\n const localGuide = { ...remoteGuide };\n\n localGuide.steps = remoteGuide.steps.map(({ message, ...rest }) => {\n const localStep = {\n ...rest,\n message: { ...message },\n markAsSeen() {\n // Send a seen event if it has not been previously seen.\n if (this.message.seen_at) return;\n return self.markAsSeen(localGuide, this);\n },\n markAsInteracted({ metadata }: { metadata?: GenericData } = {}) {\n // Always send an interaction event through.\n return self.markAsInteracted(localGuide, this, metadata);\n },\n markAsArchived() {\n // Send an archived event if it has not been previously archived.\n if (this.message.archived_at) return;\n return self.markAsArchived(localGuide, this);\n },\n };\n\n // Bind all engagement action handler methods to the local step object so\n // they can operate on itself.\n localStep.markAsSeen = localStep.markAsSeen.bind(localStep);\n localStep.markAsInteracted = localStep.markAsInteracted.bind(localStep);\n localStep.markAsArchived = localStep.markAsArchived.bind(localStep);\n\n return localStep;\n });\n\n localGuide.activation_location_rules =\n remoteGuide.activation_location_rules.map((rule) => {\n return {\n ...rule,\n pattern: new URLPattern({ pathname: rule.pathname }),\n };\n });\n\n return localGuide as KnockGuide;\n }\n\n private buildQueryParams(filterParams: QueryFilterParams = {}) {\n // Combine the target params with the given filter params.\n const combinedParams = { ...this.targetParams, ...filterParams };\n\n // Prune out any keys that have an undefined or null value.\n let params = Object.fromEntries(\n Object.entries(combinedParams).filter(\n ([_k, v]) => v !== undefined && v !== null,\n ),\n );\n\n // Encode target data as a JSON string, if provided.\n params = params.data\n ? { ...params, data: JSON.stringify(params.data) }\n : params;\n\n return params as GetGuidesQueryParams;\n }\n\n private formatQueryKey(queryParams: GenericData) {\n const sortedKeys = Object.keys(queryParams).sort();\n\n const queryStr = sortedKeys\n .map(\n (key) =>\n `${encodeURIComponent(key)}=${encodeURIComponent(queryParams[key])}`,\n )\n .join(\"&\");\n\n const basePath = guidesApiRootPath(this.knock.userId);\n return queryStr ? `${basePath}?${queryStr}` : basePath;\n }\n\n private setStepMessageAttrs(\n guideKey: string,\n stepRef: string,\n attrs: Partial<StepMessageState>,\n ) {\n let updatedStep: KnockGuideStep | undefined;\n\n this.store.setState((state) => {\n const guides = state.guides.map((guide) => {\n if (guide.key !== guideKey) return guide;\n\n const steps = guide.steps.map((step) => {\n if (step.ref !== stepRef) return step;\n\n // Mutate in place and maintain the same obj ref so to make it easier\n // to use in hook deps.\n step.message = { ...step.message, ...attrs };\n updatedStep = step;\n\n return step;\n });\n return { ...guide, steps };\n });\n return { ...state, guides };\n });\n\n return updatedStep;\n }\n\n private buildEngagementEventBaseParams(\n guide: GuideData,\n step: GuideStepData,\n ) {\n return {\n message_id: step.message.id,\n channel_id: guide.channel_id,\n guide_key: guide.key,\n guide_id: guide.id,\n guide_step_ref: step.ref,\n };\n }\n\n private addGuide({ data }: GuideAddedEvent) {\n const guide = this.localCopy(data.guide);\n\n this.store.setState((state) => {\n return { ...state, guides: sortGuides([...state.guides, guide]) };\n });\n }\n\n private replaceOrAddGuide({ data }: GuideUpdatedEvent) {\n const guide = this.localCopy(data.guide);\n\n this.store.setState((state) => {\n let replaced = false;\n\n const guides = state.guides.map((g) => {\n if (g.key !== guide.key) return g;\n replaced = true;\n return guide;\n });\n\n return {\n ...state,\n guides: replaced ? sortGuides(guides) : sortGuides([...guides, guide]),\n };\n });\n }\n\n private removeGuide({ data }: GuideUpdatedEvent | GuideRemovedEvent) {\n this.store.setState((state) => {\n const guides = state.guides.filter((g) => g.key !== data.guide.key);\n return { ...state, guides };\n });\n }\n\n private handleLocationChange() {\n const href = window.location.href;\n if (this.store.state.location === href) return;\n\n this.knock.log(`[Guide] Handle Location change: ${href}`);\n\n this.store.setState((state) => ({ ...state, location: href }));\n }\n\n private listenForLocationChangesFromWindow() {\n if (window?.history) {\n // 1. Listen for browser back/forward button clicks.\n window.addEventListener(\"popstate\", this.handleLocationChange);\n\n // 2. Listen for hash changes in case it's used for routing.\n window.addEventListener(\"hashchange\", this.handleLocationChange);\n\n // 3. Monkey-patch history methods to catch programmatic navigation.\n const pushStateFn = window.history.pushState;\n const replaceStateFn = window.history.replaceState;\n\n // Use setTimeout to allow the browser state to potentially settle.\n window.history.pushState = new Proxy(pushStateFn, {\n apply: (target, history, args) => {\n Reflect.apply(target, history, args);\n setTimeout(() => {\n this.handleLocationChange();\n }, 0);\n },\n });\n window.history.replaceState = new Proxy(replaceStateFn, {\n apply: (target, history, args) => {\n Reflect.apply(target, history, args);\n setTimeout(() => {\n this.handleLocationChange();\n }, 0);\n },\n });\n\n // 4. Keep refs to the original handlers so we can restore during cleanup.\n this.pushStateFn = pushStateFn;\n this.replaceStateFn = replaceStateFn;\n } else {\n this.knock.log(\n \"[Guide] Unable to access the `window.history` object to detect location changes\",\n );\n }\n }\n\n private removeEventListeners() {\n window.removeEventListener(\"popstate\", this.handleLocationChange);\n window.removeEventListener(\"hashchange\", this.handleLocationChange);\n\n if (this.pushStateFn) {\n window.history.pushState = this.pushStateFn;\n this.pushStateFn = undefined;\n }\n if (this.replaceStateFn) {\n window.history.replaceState = this.replaceStateFn;\n this.replaceStateFn = undefined;\n }\n }\n}\n"],"names":["sortGuides","guides","a","b","guidesApiRootPath","userId","KnockGuideClient","knock","channelId","targetParams","options","__publicField","trackLocationFromWindow","location","Store","maybeSocket","opts","queryParams","queryKey","maybeQueryStatus","state","queryStatus","data","g","e","params","newChannel","eventType","payload","event","filters","guide","locationRules","acc","rule","step","updatedStep","metadata","ts","remoteGuide","self","localGuide","message","rest","localStep","URLPattern","filterParams","combinedParams","_k","v","queryStr","key","basePath","guideKey","stepRef","attrs","steps","replaced","href","pushStateFn","replaceStateFn","target","history","args"],"mappings":"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"}
@@ -1,5 +1,6 @@
1
1
  import { GenericData } from '@knocklabs/types';
2
2
  import { Store } from '@tanstack/store';
3
+ import { URLPattern } from 'urlpattern-polyfill';
3
4
  import { default as Knock } from '../../knock';
4
5
  export declare const guidesApiRootPath: (userId: string | undefined | null) => string;
5
6
  interface StepMessageState {
@@ -18,6 +19,10 @@ interface GuideStepData {
18
19
  message: StepMessageState;
19
20
  content: any;
20
21
  }
22
+ interface GuideActivationLocationRuleData {
23
+ directive: "allow" | "block";
24
+ pathname: string;
25
+ }
21
26
  interface GuideData {
22
27
  __typename: "Guide";
23
28
  channel_id: string;
@@ -27,6 +32,7 @@ interface GuideData {
27
32
  type: string;
28
33
  semver: string;
29
34
  steps: GuideStepData[];
35
+ activation_location_rules: GuideActivationLocationRuleData[];
30
36
  inserted_at: string;
31
37
  updated_at: string;
32
38
  }
@@ -37,8 +43,12 @@ export interface KnockGuideStep extends GuideStepData {
37
43
  }) => void;
38
44
  markAsArchived: () => void;
39
45
  }
46
+ interface KnockGuideActivationLocationRule extends GuideActivationLocationRuleData {
47
+ pattern: URLPattern;
48
+ }
40
49
  export interface KnockGuide extends GuideData {
41
50
  steps: KnockGuideStep[];
51
+ activation_location_rules: KnockGuideActivationLocationRule[];
42
52
  }
43
53
  type GetGuidesQueryParams = {
44
54
  data?: string;
@@ -60,6 +70,7 @@ type QueryStatus = {
60
70
  type StoreState = {
61
71
  guides: KnockGuide[];
62
72
  queries: Record<QueryKey, QueryStatus>;
73
+ location: string | undefined;
63
74
  };
64
75
  type QueryFilterParams = Pick<GetGuidesQueryParams, "type">;
65
76
  export type SelectFilterParams = {
@@ -70,16 +81,23 @@ export type TargetParams = {
70
81
  data?: GenericData | undefined;
71
82
  tenant?: string | undefined;
72
83
  };
84
+ type ConstructorOpts = {
85
+ trackLocationFromWindow?: boolean;
86
+ };
73
87
  export declare class KnockGuideClient {
74
88
  readonly knock: Knock;
75
89
  readonly channelId: string;
76
90
  readonly targetParams: TargetParams;
91
+ readonly options: ConstructorOpts;
77
92
  store: Store<StoreState, (state: StoreState) => StoreState>;
78
93
  private socket;
79
94
  private socketChannel;
80
95
  private socketChannelTopic;
81
96
  private socketEventTypes;
82
- constructor(knock: Knock, channelId: string, targetParams?: TargetParams);
97
+ private pushStateFn;
98
+ private replaceStateFn;
99
+ constructor(knock: Knock, channelId: string, targetParams?: TargetParams, options?: ConstructorOpts);
100
+ cleanup(): void;
83
101
  fetch(opts?: {
84
102
  filters?: QueryFilterParams;
85
103
  }): Promise<QueryStatus>;
@@ -98,6 +116,9 @@ export declare class KnockGuideClient {
98
116
  private addGuide;
99
117
  private replaceOrAddGuide;
100
118
  private removeGuide;
119
+ private handleLocationChange;
120
+ private listenForLocationChangesFromWindow;
121
+ private removeEventListeners;
101
122
  }
102
123
  export {};
103
124
  //# sourceMappingURL=client.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../../../src/clients/guide/client.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../../../src/clients/guide/client.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@knocklabs/client",
3
- "version": "0.14.4",
3
+ "version": "0.14.5",
4
4
  "description": "The clientside library for interacting with Knock",
5
5
  "homepage": "https://github.com/knocklabs/javascript/tree/main/packages/client",
6
6
  "author": "@knocklabs",
@@ -78,6 +78,7 @@
78
78
  "eventemitter2": "^6.4.5",
79
79
  "jwt-decode": "^4.0.0",
80
80
  "phoenix": "1.7.19",
81
+ "urlpattern-polyfill": "^10.0.0",
81
82
  "zustand": "^4.5.6"
82
83
  }
83
84
  }
@@ -1,6 +1,7 @@
1
1
  import { GenericData } from "@knocklabs/types";
2
2
  import { Store } from "@tanstack/store";
3
3
  import { Channel, Socket } from "phoenix";
4
+ import { URLPattern } from "urlpattern-polyfill";
4
5
 
5
6
  import Knock from "../../knock";
6
7
 
@@ -38,6 +39,11 @@ interface GuideStepData {
38
39
  content: any;
39
40
  }
40
41
 
42
+ interface GuideActivationLocationRuleData {
43
+ directive: "allow" | "block";
44
+ pathname: string;
45
+ }
46
+
41
47
  interface GuideData {
42
48
  __typename: "Guide";
43
49
  channel_id: string;
@@ -47,6 +53,7 @@ interface GuideData {
47
53
  type: string;
48
54
  semver: string;
49
55
  steps: GuideStepData[];
56
+ activation_location_rules: GuideActivationLocationRuleData[];
50
57
  inserted_at: string;
51
58
  updated_at: string;
52
59
  }
@@ -57,8 +64,14 @@ export interface KnockGuideStep extends GuideStepData {
57
64
  markAsArchived: () => void;
58
65
  }
59
66
 
67
+ interface KnockGuideActivationLocationRule
68
+ extends GuideActivationLocationRuleData {
69
+ pattern: URLPattern;
70
+ }
71
+
60
72
  export interface KnockGuide extends GuideData {
61
73
  steps: KnockGuideStep[];
74
+ activation_location_rules: KnockGuideActivationLocationRule[];
62
75
  }
63
76
 
64
77
  type GetGuidesQueryParams = {
@@ -133,6 +146,7 @@ type QueryStatus = {
133
146
  type StoreState = {
134
147
  guides: KnockGuide[];
135
148
  queries: Record<QueryKey, QueryStatus>;
149
+ location: string | undefined;
136
150
  };
137
151
 
138
152
  type QueryFilterParams = Pick<GetGuidesQueryParams, "type">;
@@ -147,6 +161,10 @@ export type TargetParams = {
147
161
  tenant?: string | undefined;
148
162
  };
149
163
 
164
+ type ConstructorOpts = {
165
+ trackLocationFromWindow?: boolean;
166
+ };
167
+
150
168
  export class KnockGuideClient {
151
169
  public store: Store<StoreState, (state: StoreState) => StoreState>;
152
170
 
@@ -156,14 +174,26 @@ export class KnockGuideClient {
156
174
  private socketChannelTopic: string;
157
175
  private socketEventTypes = ["guide.added", "guide.updated", "guide.removed"];
158
176
 
177
+ // Original history methods to monkey patch, or restore in cleanups.
178
+ private pushStateFn: History["pushState"] | undefined;
179
+ private replaceStateFn: History["replaceState"] | undefined;
180
+
159
181
  constructor(
160
182
  readonly knock: Knock,
161
183
  readonly channelId: string,
162
184
  readonly targetParams: TargetParams = {},
185
+ readonly options: ConstructorOpts = {},
163
186
  ) {
187
+ const { trackLocationFromWindow = true } = options;
188
+
189
+ const location = trackLocationFromWindow
190
+ ? window?.location.href
191
+ : undefined;
192
+
164
193
  this.store = new Store<StoreState>({
165
194
  guides: [],
166
195
  queries: {},
196
+ location,
167
197
  });
168
198
 
169
199
  // In server environments we might not have a socket connection.
@@ -171,9 +201,18 @@ export class KnockGuideClient {
171
201
  this.socket = maybeSocket;
172
202
  this.socketChannelTopic = `guides:${channelId}`;
173
203
 
204
+ if (trackLocationFromWindow) {
205
+ this.listenForLocationChangesFromWindow();
206
+ }
207
+
174
208
  this.knock.log("[Guide] Initialized a guide client");
175
209
  }
176
210
 
211
+ cleanup() {
212
+ this.unsubscribe();
213
+ this.removeEventListeners();
214
+ }
215
+
177
216
  async fetch(opts?: { filters?: QueryFilterParams }) {
178
217
  this.knock.failIfNotAuthenticated();
179
218
  this.knock.log("[Guide] Loading all eligible guides");
@@ -291,8 +330,6 @@ export class KnockGuideClient {
291
330
  //
292
331
 
293
332
  select(state: StoreState, filters: SelectFilterParams = {}) {
294
- // TODO(KNO-7790): Need to evaluate activation rules also.
295
-
296
333
  return state.guides.filter((guide) => {
297
334
  if (filters.type && filters.type !== guide.type) {
298
335
  return false;
@@ -302,6 +339,42 @@ export class KnockGuideClient {
302
339
  return false;
303
340
  }
304
341
 
342
+ const locationRules = guide.activation_location_rules || [];
343
+
344
+ if (locationRules.length > 0 && state.location) {
345
+ const allowed = locationRules.reduce<boolean | undefined>(
346
+ (acc, rule) => {
347
+ // Any matched block rule prevails so no need to evaluate further
348
+ // as soon as there is one.
349
+ if (acc === false) return false;
350
+
351
+ // At this point we either have a matched allow rule (acc is true),
352
+ // or no matched rule found yet (acc is undefined).
353
+
354
+ switch (rule.directive) {
355
+ case "allow": {
356
+ // No need to evaluate more allow rules once we matched one
357
+ // since any matched allowed rule means allow.
358
+ if (acc === true) return true;
359
+
360
+ const matched = rule.pattern.test(state.location);
361
+ return matched ? true : undefined;
362
+ }
363
+
364
+ case "block": {
365
+ // Always test block rules (unless already matched to block)
366
+ // because they'd prevail over matched allow rules.
367
+ const matched = rule.pattern.test(state.location);
368
+ return matched ? false : acc;
369
+ }
370
+ }
371
+ },
372
+ undefined,
373
+ );
374
+
375
+ if (!allowed) return false;
376
+ }
377
+
305
378
  return true;
306
379
  });
307
380
  }
@@ -427,6 +500,14 @@ export class KnockGuideClient {
427
500
  return localStep;
428
501
  });
429
502
 
503
+ localGuide.activation_location_rules =
504
+ remoteGuide.activation_location_rules.map((rule) => {
505
+ return {
506
+ ...rule,
507
+ pattern: new URLPattern({ pathname: rule.pathname }),
508
+ };
509
+ });
510
+
430
511
  return localGuide as KnockGuide;
431
512
  }
432
513
 
@@ -538,4 +619,67 @@ export class KnockGuideClient {
538
619
  return { ...state, guides };
539
620
  });
540
621
  }
622
+
623
+ private handleLocationChange() {
624
+ const href = window.location.href;
625
+ if (this.store.state.location === href) return;
626
+
627
+ this.knock.log(`[Guide] Handle Location change: ${href}`);
628
+
629
+ this.store.setState((state) => ({ ...state, location: href }));
630
+ }
631
+
632
+ private listenForLocationChangesFromWindow() {
633
+ if (window?.history) {
634
+ // 1. Listen for browser back/forward button clicks.
635
+ window.addEventListener("popstate", this.handleLocationChange);
636
+
637
+ // 2. Listen for hash changes in case it's used for routing.
638
+ window.addEventListener("hashchange", this.handleLocationChange);
639
+
640
+ // 3. Monkey-patch history methods to catch programmatic navigation.
641
+ const pushStateFn = window.history.pushState;
642
+ const replaceStateFn = window.history.replaceState;
643
+
644
+ // Use setTimeout to allow the browser state to potentially settle.
645
+ window.history.pushState = new Proxy(pushStateFn, {
646
+ apply: (target, history, args) => {
647
+ Reflect.apply(target, history, args);
648
+ setTimeout(() => {
649
+ this.handleLocationChange();
650
+ }, 0);
651
+ },
652
+ });
653
+ window.history.replaceState = new Proxy(replaceStateFn, {
654
+ apply: (target, history, args) => {
655
+ Reflect.apply(target, history, args);
656
+ setTimeout(() => {
657
+ this.handleLocationChange();
658
+ }, 0);
659
+ },
660
+ });
661
+
662
+ // 4. Keep refs to the original handlers so we can restore during cleanup.
663
+ this.pushStateFn = pushStateFn;
664
+ this.replaceStateFn = replaceStateFn;
665
+ } else {
666
+ this.knock.log(
667
+ "[Guide] Unable to access the `window.history` object to detect location changes",
668
+ );
669
+ }
670
+ }
671
+
672
+ private removeEventListeners() {
673
+ window.removeEventListener("popstate", this.handleLocationChange);
674
+ window.removeEventListener("hashchange", this.handleLocationChange);
675
+
676
+ if (this.pushStateFn) {
677
+ window.history.pushState = this.pushStateFn;
678
+ this.pushStateFn = undefined;
679
+ }
680
+ if (this.replaceStateFn) {
681
+ window.history.replaceState = this.replaceStateFn;
682
+ this.replaceStateFn = undefined;
683
+ }
684
+ }
541
685
  }