releasebird-javascript-sdk 1.0.89 → 1.0.91

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.
@@ -1,19 +1,22 @@
1
1
  import RbirdSessionManager from "./RbirdSessionManager";
2
+ import RbirdWebsiteWidget from "./RbirdWebsiteWidget";
2
3
  import { RbirdBannerManager } from "./RbirdBannerManager";
3
4
  import { RbirdFormManager } from "./RbirdFormManager";
4
5
  import { RbirdSurveyManager } from "./RbirdSurveyManager";
5
6
 
6
7
  /**
7
- * Manages automations and their execution in the SDK.
8
- * Automations are event-driven workflows that can trigger actions
9
- * like showing banners, forms, surveys, or sending chat messages.
8
+ * Manages automations by sending events to the backend and executing commands.
9
+ * The actual workflow execution happens on the backend - this manager handles:
10
+ * - Sending events to the backend
11
+ * - Polling for pending commands
12
+ * - Executing UI commands (show banner, form, survey, send chat)
10
13
  */
11
14
  export class RbirdAutomationManager {
12
15
  static instance = null;
13
- automations = [];
14
16
  apiKey = null;
15
17
  firstSeenTimestamp = null;
16
- executedAutomations = new Set(); // Track which automations have been executed
18
+ pollingInterval = null;
19
+ isPolling = false;
17
20
 
18
21
  static getInstance() {
19
22
  if (!this.instance) {
@@ -30,14 +33,18 @@ export class RbirdAutomationManager {
30
33
  this.apiKey = apiKey;
31
34
  this.firstSeenTimestamp = this.getFirstSeenTimestamp();
32
35
 
33
- // Load automations from backend
34
- await this.loadAutomations();
35
-
36
36
  // Register event handlers
37
37
  this.registerEventHandlers();
38
38
 
39
- // Check for first_seen triggers immediately
40
- this.checkFirstSeenTriggers();
39
+ // Start command polling
40
+ this.startCommandPolling();
41
+
42
+ // Send first_seen event
43
+ await this.sendEvent('first_seen', {
44
+ timeSinceFirstSeen: (Date.now() - this.firstSeenTimestamp) / 1000
45
+ });
46
+
47
+ console.log('[Rbird] Automation manager initialized');
41
48
  }
42
49
 
43
50
  /**
@@ -53,297 +60,251 @@ export class RbirdAutomationManager {
53
60
  return now;
54
61
  }
55
62
 
56
- /**
57
- * Load active automations from the backend
58
- */
59
- async loadAutomations() {
60
- try {
61
- const sessionManager = RbirdSessionManager.getInstance();
62
- const baseUrl = sessionManager.apiUrl || 'https://api.releasebird.com';
63
-
64
- const response = await fetch(`${baseUrl}/papi/ewidget/automations`, {
65
- method: 'GET',
66
- headers: {
67
- 'Content-Type': 'application/json',
68
- 'apiKey': this.apiKey,
69
- 'languageCode': navigator.language?.split('-')[0] || 'en',
70
- },
71
- });
72
-
73
- if (response.ok) {
74
- this.automations = await response.json();
75
- console.log('[Rbird] Loaded automations:', this.automations.length);
76
- }
77
- } catch (error) {
78
- console.error('[Rbird] Error loading automations:', error);
79
- }
80
- }
81
-
82
63
  /**
83
64
  * Register event handlers for automation triggers
84
65
  */
85
66
  registerEventHandlers() {
86
- // Listen for page visits
87
- window.addEventListener('popstate', () => this.handleEvent('page_visit'));
67
+ // Listen for page visits (navigation)
68
+ window.addEventListener('popstate', () => this.sendPageVisitEvents());
69
+
70
+ // Also track initial page load
71
+ this.sendPageVisitEvents();
88
72
 
89
73
  // Listen for identify events
90
74
  document.addEventListener('rbird:identify', (e) => {
91
- this.handleEvent('user_identified', e.detail);
75
+ this.sendEvent('user_identified', e.detail);
76
+ // Also send user_page_visit when user is identified
77
+ this.sendEvent('user_page_visit');
92
78
  });
93
79
 
94
80
  // Listen for form submissions
95
81
  document.addEventListener('rbird:form_submitted', (e) => {
96
- this.handleEvent('form_submitted', e.detail);
82
+ this.sendEvent('form_submitted', e.detail);
97
83
  });
98
84
 
99
85
  // Listen for survey completions
100
86
  document.addEventListener('rbird:survey_completed', (e) => {
101
- this.handleEvent('survey_completed', e.detail);
87
+ this.sendEvent('survey_completed', e.detail);
102
88
  });
103
89
 
104
90
  // Listen for custom events
105
91
  document.addEventListener('rbird:track', (e) => {
106
- this.handleEvent(e.detail?.eventName, e.detail?.data);
92
+ this.sendEvent(e.detail?.eventName, e.detail?.data);
107
93
  });
108
94
  }
109
95
 
110
96
  /**
111
- * Check for first_seen triggers on initialization
97
+ * Send page visit events based on user identification status.
98
+ * Sends:
99
+ * - page_visit: always (for backward compatibility)
100
+ * - visitor_page_visit: if user is NOT identified (anonymous)
101
+ * - user_page_visit: if user IS identified
112
102
  */
113
- checkFirstSeenTriggers() {
114
- const timeSinceFirstSeen = (Date.now() - this.firstSeenTimestamp) / 1000; // in seconds
115
-
116
- this.automations.forEach((automation) => {
117
- const triggerNode = this.findTriggerNode(automation);
118
- if (triggerNode && triggerNode.data?.eventType === 'first_seen') {
119
- this.evaluateAndExecute(automation, triggerNode, { timeSinceFirstSeen });
120
- }
121
- });
103
+ sendPageVisitEvents() {
104
+ // Always send generic page_visit for backward compatibility
105
+ this.sendEvent('page_visit');
106
+
107
+ // Send specific event based on identification status
108
+ const peopleId = this.getPeopleId();
109
+ if (peopleId) {
110
+ this.sendEvent('user_page_visit');
111
+ } else {
112
+ this.sendEvent('visitor_page_visit');
113
+ }
122
114
  }
123
115
 
124
116
  /**
125
- * Handle an event and check for matching automations
117
+ * Send an event to the backend
126
118
  * @param {string} eventType - The type of event
127
119
  * @param {Object} eventData - Additional event data
128
120
  */
129
- handleEvent(eventType, eventData = {}) {
130
- this.automations.forEach((automation) => {
131
- const triggerNode = this.findTriggerNode(automation);
132
- if (triggerNode && triggerNode.data?.eventType === eventType) {
133
- this.evaluateAndExecute(automation, triggerNode, eventData);
121
+ async sendEvent(eventType, eventData = {}) {
122
+ if (!eventType) return;
123
+
124
+ try {
125
+ const response = await fetch(`${API}/ewidget/events`, {
126
+ method: 'POST',
127
+ headers: {
128
+ 'Content-Type': 'application/json',
129
+ 'apiKey': this.apiKey,
130
+ 'peopleId': this.getPeopleId() || '',
131
+ 'anonymousId': this.getAnonymousId() || ''
132
+ },
133
+ body: JSON.stringify({
134
+ eventType,
135
+ context: {
136
+ ...eventData,
137
+ pageUrl: window.location.href,
138
+ pagePath: window.location.pathname
139
+ },
140
+ firstSeenTimestamp: this.firstSeenTimestamp
141
+ })
142
+ });
143
+
144
+ if (!response.ok) {
145
+ console.warn('[Rbird] Failed to send event:', response.status);
146
+ } else {
147
+ console.log('[Rbird] Event sent:', eventType);
134
148
  }
135
- });
149
+ } catch (error) {
150
+ console.error('[Rbird] Error sending event:', error);
151
+ }
136
152
  }
137
153
 
138
154
  /**
139
- * Find the trigger node in an automation
140
- * @param {Object} automation - The automation object
141
- * @returns {Object|null} The trigger node or null
155
+ * Get the current people ID from session
142
156
  */
143
- findTriggerNode(automation) {
144
- return automation.nodes?.find((node) => node.type === 'trigger');
157
+ getPeopleId() {
158
+ return RbirdSessionManager.getInstance().getState()?.identify?.peopleId;
145
159
  }
146
160
 
147
161
  /**
148
- * Evaluate trigger conditions and execute the automation
149
- * @param {Object} automation - The automation object
150
- * @param {Object} triggerNode - The trigger node
151
- * @param {Object} context - The event context
162
+ * Get the anonymous ID from session
152
163
  */
153
- evaluateAndExecute(automation, triggerNode, context) {
154
- // Check if automation was already executed (for once-only automations)
155
- if (this.executedAutomations.has(automation.id)) {
156
- return;
157
- }
158
-
159
- // Evaluate trigger conditions
160
- if (!this.evaluateTriggerConditions(triggerNode, context)) {
161
- return;
162
- }
163
-
164
- // Mark as executed
165
- this.executedAutomations.add(automation.id);
166
-
167
- // Execute the workflow starting from the trigger node
168
- this.executeWorkflow(automation, triggerNode.id, context);
164
+ getAnonymousId() {
165
+ return RbirdSessionManager.getInstance().getState()?.identify?.anonymousId ||
166
+ localStorage.getItem('rbird_anonymous_id') ||
167
+ this.generateAnonymousId();
169
168
  }
170
169
 
171
170
  /**
172
- * Evaluate trigger conditions
173
- * @param {Object} triggerNode - The trigger node
174
- * @param {Object} context - The event context
175
- * @returns {boolean} Whether conditions are met
171
+ * Generate and store an anonymous ID
176
172
  */
177
- evaluateTriggerConditions(triggerNode, context) {
178
- const conditions = triggerNode.data?.conditions || [];
179
-
180
- for (const condition of conditions) {
181
- const value = this.getContextValue(condition.property, context);
182
-
183
- switch (condition.operator) {
184
- case 'less_than':
185
- if (!(parseFloat(value) < parseFloat(condition.value))) return false;
186
- break;
187
- case 'greater_than':
188
- if (!(parseFloat(value) > parseFloat(condition.value))) return false;
189
- break;
190
- case 'equals':
191
- if (value !== condition.value) return false;
192
- break;
193
- case 'contains':
194
- if (!String(value).includes(condition.value)) return false;
195
- break;
196
- }
197
- }
198
-
199
- return true;
173
+ generateAnonymousId() {
174
+ const id = 'anon_' + Math.random().toString(36).substr(2, 9);
175
+ localStorage.setItem('rbird_anonymous_id', id);
176
+ return id;
200
177
  }
201
178
 
202
179
  /**
203
- * Get a value from the context
204
- * @param {string} property - The property path
205
- * @param {Object} context - The context object
206
- * @returns {any} The value
180
+ * Start polling for pending commands
207
181
  */
208
- getContextValue(property, context) {
209
- if (property === 'time_since_first_seen' || property === 'timeSinceFirstSeen') {
210
- return context.timeSinceFirstSeen || (Date.now() - this.firstSeenTimestamp) / 1000;
182
+ startCommandPolling() {
183
+ if (this.pollingInterval) {
184
+ return; // Already polling
211
185
  }
212
186
 
213
- // Navigate nested properties like "user.language"
214
- const parts = property.split('.');
215
- let value = context;
216
- for (const part of parts) {
217
- value = value?.[part];
218
- }
219
- return value;
187
+ // Poll immediately once
188
+ this.pollCommands();
189
+
190
+ // Then poll every 3 seconds
191
+ this.pollingInterval = setInterval(() => this.pollCommands(), 3000);
220
192
  }
221
193
 
222
194
  /**
223
- * Execute a workflow from a starting node
224
- * @param {Object} automation - The automation object
225
- * @param {string} startNodeId - The ID of the starting node
226
- * @param {Object} context - The execution context
195
+ * Stop command polling
227
196
  */
228
- executeWorkflow(automation, startNodeId, context) {
229
- // Find next nodes to execute
230
- const nextEdges = automation.edges?.filter((edge) => edge.source === startNodeId) || [];
231
-
232
- for (const edge of nextEdges) {
233
- const targetNode = automation.nodes?.find((node) => node.id === edge.target);
234
- if (!targetNode) continue;
235
-
236
- if (targetNode.type === 'condition') {
237
- // Evaluate condition and follow appropriate path
238
- const conditionResult = this.evaluateCondition(targetNode, context);
239
- const handleToFollow = conditionResult ? 'true' : 'false';
240
-
241
- // Find edges from this condition with matching handle
242
- const conditionEdges = automation.edges?.filter(
243
- (e) => e.source === targetNode.id && e.sourceHandle === handleToFollow
244
- ) || [];
245
-
246
- for (const condEdge of conditionEdges) {
247
- this.executeWorkflow(automation, condEdge.source, context);
248
- }
249
- } else if (targetNode.type === 'action') {
250
- // Execute the action
251
- this.executeAction(targetNode, context);
252
-
253
- // Continue to next nodes if any
254
- this.executeWorkflow(automation, targetNode.id, context);
255
- }
197
+ stopCommandPolling() {
198
+ if (this.pollingInterval) {
199
+ clearInterval(this.pollingInterval);
200
+ this.pollingInterval = null;
256
201
  }
257
202
  }
258
203
 
259
204
  /**
260
- * Evaluate a condition node
261
- * @param {Object} conditionNode - The condition node
262
- * @param {Object} context - The execution context
263
- * @returns {boolean} The result of the condition
205
+ * Poll for pending commands from the backend
264
206
  */
265
- evaluateCondition(conditionNode, context) {
266
- const { property, operator, value } = conditionNode.data || {};
267
-
268
- let actualValue;
269
-
270
- // Get the value to compare
271
- if (property?.startsWith('user.')) {
272
- const userProp = property.replace('user.', '');
273
- actualValue = RbirdSessionManager.getInstance().getUser()?.[userProp];
274
- } else if (property?.startsWith('page.')) {
275
- const pageProp = property.replace('page.', '');
276
- if (pageProp === 'url') {
277
- actualValue = window.location.href;
278
- } else if (pageProp === 'path') {
279
- actualValue = window.location.pathname;
207
+ async pollCommands() {
208
+ if (this.isPolling) return; // Prevent concurrent polls
209
+ this.isPolling = true;
210
+
211
+ try {
212
+ const response = await fetch(`${API}/ewidget/commands`, {
213
+ method: 'GET',
214
+ headers: {
215
+ 'Content-Type': 'application/json',
216
+ 'apiKey': this.apiKey,
217
+ 'peopleId': this.getPeopleId() || '',
218
+ 'anonymousId': this.getAnonymousId() || ''
219
+ }
220
+ });
221
+
222
+ if (!response.ok) {
223
+ return;
280
224
  }
281
- } else {
282
- actualValue = context[property];
283
- }
284
225
 
285
- switch (operator) {
286
- case 'equals':
287
- return actualValue === value;
288
- case 'not_equals':
289
- return actualValue !== value;
290
- case 'contains':
291
- return String(actualValue).includes(value);
292
- case 'starts_with':
293
- return String(actualValue).startsWith(value);
294
- default:
295
- return false;
226
+ const commands = await response.json();
227
+
228
+ // Execute each command
229
+ for (const cmd of commands) {
230
+ await this.executeCommand(cmd);
231
+ await this.markDelivered(cmd.id);
232
+ }
233
+ } catch (error) {
234
+ // Silently fail - polling will retry
235
+ } finally {
236
+ this.isPolling = false;
296
237
  }
297
238
  }
298
239
 
299
240
  /**
300
- * Execute an action node
301
- * @param {Object} actionNode - The action node
302
- * @param {Object} context - The execution context
241
+ * Execute a command from the backend
242
+ * @param {Object} cmd - The command to execute
303
243
  */
304
- executeAction(actionNode, context) {
305
- const { actionType, config } = actionNode.data || {};
306
-
307
- console.log('[Rbird] Executing action:', actionType, config);
244
+ async executeCommand(cmd) {
245
+ console.log('[Rbird] Executing command:', cmd.type);
308
246
 
309
- switch (actionType) {
310
- case 'send_chat':
311
- this.executeSendChat(config?.message, context);
312
- break;
247
+ switch (cmd.type) {
313
248
  case 'show_banner':
314
- this.executeShowBanner(config?.bannerId);
249
+ this.executeShowBanner(cmd.targetId);
315
250
  break;
316
251
  case 'show_form':
317
- this.executeShowForm(config?.formId);
252
+ this.executeShowForm(cmd.targetId);
318
253
  break;
319
254
  case 'show_survey':
320
- this.executeShowSurvey(config?.surveyId);
255
+ this.executeShowSurvey(cmd.targetId);
321
256
  break;
322
- case 'webhook':
323
- this.executeWebhook(config?.webhookUrl, config?.webhookMethod || 'POST', context);
257
+ case 'send_chat':
258
+ this.executeSendChat(cmd.message, cmd.config);
324
259
  break;
260
+ default:
261
+ console.warn('[Rbird] Unknown command type:', cmd.type);
262
+ }
263
+ }
264
+
265
+ /**
266
+ * Mark a command as delivered
267
+ * @param {string} commandId - The command ID
268
+ */
269
+ async markDelivered(commandId) {
270
+ try {
271
+ await fetch(`${API}/ewidget/commands/${commandId}/delivered`, {
272
+ method: 'POST',
273
+ headers: {
274
+ 'Content-Type': 'application/json',
275
+ 'apiKey': this.apiKey
276
+ }
277
+ });
278
+ } catch (error) {
279
+ console.warn('[Rbird] Failed to mark command as delivered:', error);
325
280
  }
326
281
  }
327
282
 
328
283
  /**
329
- * Execute send_chat action
284
+ * Execute send_chat command
330
285
  */
331
- executeSendChat(message, context) {
286
+ executeSendChat(message, config) {
332
287
  if (!message) return;
333
288
 
334
- // Replace placeholders in message
335
- const processedMessage = this.processMessage(message, context);
289
+ console.log('[Rbird] Chat message triggered:', message);
336
290
 
337
- // Dispatch event for the chat widget to handle
338
- document.dispatchEvent(new CustomEvent('rbird:automation_chat', {
339
- detail: { message: processedMessage }
340
- }));
291
+ // Open the widget and send the message
292
+ const widget = RbirdWebsiteWidget.getInstance();
293
+ widget.openWebsiteWidget();
341
294
 
342
- console.log('[Rbird] Chat message triggered:', processedMessage);
295
+ // Send message to iframe to display automation message
296
+ setTimeout(() => {
297
+ if (widget.iframe) {
298
+ widget.iframe.contentWindow?.postMessage({
299
+ type: 'automationMessage',
300
+ message: message
301
+ }, '*');
302
+ }
303
+ }, 500); // Small delay to ensure iframe is ready
343
304
  }
344
305
 
345
306
  /**
346
- * Execute show_banner action
307
+ * Execute show_banner command
347
308
  */
348
309
  executeShowBanner(bannerId) {
349
310
  if (!bannerId) return;
@@ -351,7 +312,7 @@ export class RbirdAutomationManager {
351
312
  }
352
313
 
353
314
  /**
354
- * Execute show_form action
315
+ * Execute show_form command
355
316
  */
356
317
  executeShowForm(formId) {
357
318
  if (!formId) return;
@@ -359,48 +320,13 @@ export class RbirdAutomationManager {
359
320
  }
360
321
 
361
322
  /**
362
- * Execute show_survey action
323
+ * Execute show_survey command
363
324
  */
364
325
  executeShowSurvey(surveyId) {
365
326
  if (!surveyId) return;
366
327
  RbirdSurveyManager.getInstance().showSurvey(surveyId, {});
367
328
  }
368
329
 
369
- /**
370
- * Execute webhook action
371
- */
372
- async executeWebhook(url, method, context) {
373
- if (!url) return;
374
-
375
- try {
376
- const options = {
377
- method: method,
378
- headers: {
379
- 'Content-Type': 'application/json',
380
- },
381
- };
382
-
383
- if (method === 'POST') {
384
- options.body = JSON.stringify(context);
385
- }
386
-
387
- await fetch(url, options);
388
- console.log('[Rbird] Webhook executed:', url);
389
- } catch (error) {
390
- console.error('[Rbird] Webhook error:', error);
391
- }
392
- }
393
-
394
- /**
395
- * Process message placeholders
396
- */
397
- processMessage(message, context) {
398
- return message
399
- .replace(/\{\{user\.name\}\}/g, RbirdSessionManager.getInstance().getUser()?.name || '')
400
- .replace(/\{\{user\.email\}\}/g, RbirdSessionManager.getInstance().getUser()?.email || '')
401
- .replace(/\{\{page\.url\}\}/g, window.location.href);
402
- }
403
-
404
330
  /**
405
331
  * Track a custom event
406
332
  * @param {string} eventName - The name of the event
@@ -411,6 +337,13 @@ export class RbirdAutomationManager {
411
337
  detail: { eventName, data }
412
338
  }));
413
339
  }
340
+
341
+ /**
342
+ * Cleanup when manager is destroyed
343
+ */
344
+ destroy() {
345
+ this.stopCommandPolling();
346
+ }
414
347
  }
415
348
 
416
349
  export default RbirdAutomationManager;
@@ -990,6 +990,12 @@ export default class RbirdWebsiteWidget {
990
990
  }, 50);
991
991
  return true;
992
992
  }
993
+
994
+ // Even without saved position, update hide button position after DOM is ready
995
+ setTimeout(() => {
996
+ this.updateRelatedElementsPosition();
997
+ }, 50);
998
+
993
999
  return false;
994
1000
  }
995
1001
 
@@ -1001,27 +1007,20 @@ export default class RbirdWebsiteWidget {
1001
1007
 
1002
1008
  const buttonRect = this.websiteWidget.getBoundingClientRect();
1003
1009
 
1004
- // Check if we have a custom position (not default)
1005
- const savedPosition = this.loadWidgetPosition();
1006
- if (!savedPosition) return; // Use default CSS positioning
1007
-
1008
- // Update badge position
1009
- if (this.countBadge) {
1010
- this.countBadge.style.left = `${buttonRect.right - 10}px`;
1011
- this.countBadge.style.right = 'unset';
1012
- this.countBadge.style.bottom = `${window.innerHeight - buttonRect.top - 15}px`;
1013
- }
1014
-
1015
1010
  // Update hide button position (top right of bubble)
1016
- // Match CSS: bottom = spaceBottom + 45 = buttonRect.bottom + 45 - buttonRect.height
1017
- // Since buttonRect.top = window.innerHeight - buttonRect.bottom, and bubble height is ~50px
1018
- // bottom = window.innerHeight - buttonRect.top - 5 should equal spaceBottom + 45
1019
1011
  if (this.hideWidgetButton) {
1020
1012
  this.hideWidgetButton.style.left = `${buttonRect.right - 5}px`;
1021
1013
  this.hideWidgetButton.style.right = 'unset';
1022
1014
  this.hideWidgetButton.style.bottom = `${window.innerHeight - buttonRect.bottom + 55}px`;
1023
1015
  }
1024
1016
 
1017
+ // Always update badge position based on current button position
1018
+ if (this.countBadge) {
1019
+ this.countBadge.style.left = `${buttonRect.right - 10}px`;
1020
+ this.countBadge.style.right = 'unset';
1021
+ this.countBadge.style.bottom = `${window.innerHeight - buttonRect.top - 15}px`;
1022
+ }
1023
+
1025
1024
  // Update message bubbles container position
1026
1025
  if (this.messageBubblesContainer) {
1027
1026
  this.positionMessageBubbles();