releasebird-javascript-sdk 1.0.88 → 1.0.89
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/build/index.js +1 -1
- package/package.json +1 -1
- package/published/1.0.88/index.js +1 -1
- package/published/1.0.89/index.js +1 -0
- package/published/latest/index.js +1 -1
- package/src/RbirdAutomationManager.js +416 -0
- package/src/RbirdWebsiteWidget.js +47 -7
- package/src/index.js +20 -0
|
@@ -0,0 +1,416 @@
|
|
|
1
|
+
import RbirdSessionManager from "./RbirdSessionManager";
|
|
2
|
+
import { RbirdBannerManager } from "./RbirdBannerManager";
|
|
3
|
+
import { RbirdFormManager } from "./RbirdFormManager";
|
|
4
|
+
import { RbirdSurveyManager } from "./RbirdSurveyManager";
|
|
5
|
+
|
|
6
|
+
/**
|
|
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.
|
|
10
|
+
*/
|
|
11
|
+
export class RbirdAutomationManager {
|
|
12
|
+
static instance = null;
|
|
13
|
+
automations = [];
|
|
14
|
+
apiKey = null;
|
|
15
|
+
firstSeenTimestamp = null;
|
|
16
|
+
executedAutomations = new Set(); // Track which automations have been executed
|
|
17
|
+
|
|
18
|
+
static getInstance() {
|
|
19
|
+
if (!this.instance) {
|
|
20
|
+
this.instance = new RbirdAutomationManager();
|
|
21
|
+
}
|
|
22
|
+
return this.instance;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Initialize the automation manager
|
|
27
|
+
* @param {string} apiKey - The API key for the project
|
|
28
|
+
*/
|
|
29
|
+
async init(apiKey) {
|
|
30
|
+
this.apiKey = apiKey;
|
|
31
|
+
this.firstSeenTimestamp = this.getFirstSeenTimestamp();
|
|
32
|
+
|
|
33
|
+
// Load automations from backend
|
|
34
|
+
await this.loadAutomations();
|
|
35
|
+
|
|
36
|
+
// Register event handlers
|
|
37
|
+
this.registerEventHandlers();
|
|
38
|
+
|
|
39
|
+
// Check for first_seen triggers immediately
|
|
40
|
+
this.checkFirstSeenTriggers();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Get or set the first_seen timestamp
|
|
45
|
+
*/
|
|
46
|
+
getFirstSeenTimestamp() {
|
|
47
|
+
const stored = localStorage.getItem('rbird_first_seen');
|
|
48
|
+
if (stored) {
|
|
49
|
+
return parseInt(stored, 10);
|
|
50
|
+
}
|
|
51
|
+
const now = Date.now();
|
|
52
|
+
localStorage.setItem('rbird_first_seen', now.toString());
|
|
53
|
+
return now;
|
|
54
|
+
}
|
|
55
|
+
|
|
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
|
+
/**
|
|
83
|
+
* Register event handlers for automation triggers
|
|
84
|
+
*/
|
|
85
|
+
registerEventHandlers() {
|
|
86
|
+
// Listen for page visits
|
|
87
|
+
window.addEventListener('popstate', () => this.handleEvent('page_visit'));
|
|
88
|
+
|
|
89
|
+
// Listen for identify events
|
|
90
|
+
document.addEventListener('rbird:identify', (e) => {
|
|
91
|
+
this.handleEvent('user_identified', e.detail);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
// Listen for form submissions
|
|
95
|
+
document.addEventListener('rbird:form_submitted', (e) => {
|
|
96
|
+
this.handleEvent('form_submitted', e.detail);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
// Listen for survey completions
|
|
100
|
+
document.addEventListener('rbird:survey_completed', (e) => {
|
|
101
|
+
this.handleEvent('survey_completed', e.detail);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
// Listen for custom events
|
|
105
|
+
document.addEventListener('rbird:track', (e) => {
|
|
106
|
+
this.handleEvent(e.detail?.eventName, e.detail?.data);
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Check for first_seen triggers on initialization
|
|
112
|
+
*/
|
|
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
|
+
});
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Handle an event and check for matching automations
|
|
126
|
+
* @param {string} eventType - The type of event
|
|
127
|
+
* @param {Object} eventData - Additional event data
|
|
128
|
+
*/
|
|
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);
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Find the trigger node in an automation
|
|
140
|
+
* @param {Object} automation - The automation object
|
|
141
|
+
* @returns {Object|null} The trigger node or null
|
|
142
|
+
*/
|
|
143
|
+
findTriggerNode(automation) {
|
|
144
|
+
return automation.nodes?.find((node) => node.type === 'trigger');
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
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
|
|
152
|
+
*/
|
|
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);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
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
|
|
176
|
+
*/
|
|
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;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
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
|
|
207
|
+
*/
|
|
208
|
+
getContextValue(property, context) {
|
|
209
|
+
if (property === 'time_since_first_seen' || property === 'timeSinceFirstSeen') {
|
|
210
|
+
return context.timeSinceFirstSeen || (Date.now() - this.firstSeenTimestamp) / 1000;
|
|
211
|
+
}
|
|
212
|
+
|
|
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;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
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
|
|
227
|
+
*/
|
|
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
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
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
|
|
264
|
+
*/
|
|
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;
|
|
280
|
+
}
|
|
281
|
+
} else {
|
|
282
|
+
actualValue = context[property];
|
|
283
|
+
}
|
|
284
|
+
|
|
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;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Execute an action node
|
|
301
|
+
* @param {Object} actionNode - The action node
|
|
302
|
+
* @param {Object} context - The execution context
|
|
303
|
+
*/
|
|
304
|
+
executeAction(actionNode, context) {
|
|
305
|
+
const { actionType, config } = actionNode.data || {};
|
|
306
|
+
|
|
307
|
+
console.log('[Rbird] Executing action:', actionType, config);
|
|
308
|
+
|
|
309
|
+
switch (actionType) {
|
|
310
|
+
case 'send_chat':
|
|
311
|
+
this.executeSendChat(config?.message, context);
|
|
312
|
+
break;
|
|
313
|
+
case 'show_banner':
|
|
314
|
+
this.executeShowBanner(config?.bannerId);
|
|
315
|
+
break;
|
|
316
|
+
case 'show_form':
|
|
317
|
+
this.executeShowForm(config?.formId);
|
|
318
|
+
break;
|
|
319
|
+
case 'show_survey':
|
|
320
|
+
this.executeShowSurvey(config?.surveyId);
|
|
321
|
+
break;
|
|
322
|
+
case 'webhook':
|
|
323
|
+
this.executeWebhook(config?.webhookUrl, config?.webhookMethod || 'POST', context);
|
|
324
|
+
break;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Execute send_chat action
|
|
330
|
+
*/
|
|
331
|
+
executeSendChat(message, context) {
|
|
332
|
+
if (!message) return;
|
|
333
|
+
|
|
334
|
+
// Replace placeholders in message
|
|
335
|
+
const processedMessage = this.processMessage(message, context);
|
|
336
|
+
|
|
337
|
+
// Dispatch event for the chat widget to handle
|
|
338
|
+
document.dispatchEvent(new CustomEvent('rbird:automation_chat', {
|
|
339
|
+
detail: { message: processedMessage }
|
|
340
|
+
}));
|
|
341
|
+
|
|
342
|
+
console.log('[Rbird] Chat message triggered:', processedMessage);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Execute show_banner action
|
|
347
|
+
*/
|
|
348
|
+
executeShowBanner(bannerId) {
|
|
349
|
+
if (!bannerId) return;
|
|
350
|
+
RbirdBannerManager.getInstance().showBanner(bannerId);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Execute show_form action
|
|
355
|
+
*/
|
|
356
|
+
executeShowForm(formId) {
|
|
357
|
+
if (!formId) return;
|
|
358
|
+
RbirdFormManager.getInstance().showForm(formId, {});
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* Execute show_survey action
|
|
363
|
+
*/
|
|
364
|
+
executeShowSurvey(surveyId) {
|
|
365
|
+
if (!surveyId) return;
|
|
366
|
+
RbirdSurveyManager.getInstance().showSurvey(surveyId, {});
|
|
367
|
+
}
|
|
368
|
+
|
|
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
|
+
/**
|
|
405
|
+
* Track a custom event
|
|
406
|
+
* @param {string} eventName - The name of the event
|
|
407
|
+
* @param {Object} data - Additional event data
|
|
408
|
+
*/
|
|
409
|
+
track(eventName, data = {}) {
|
|
410
|
+
document.dispatchEvent(new CustomEvent('rbird:track', {
|
|
411
|
+
detail: { eventName, data }
|
|
412
|
+
}));
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
export default RbirdAutomationManager;
|
|
@@ -804,6 +804,20 @@ export default class RbirdWebsiteWidget {
|
|
|
804
804
|
this.dragHandle.addEventListener('touchstart', (e) => this.startDrag(e), { passive: false });
|
|
805
805
|
document.addEventListener('touchmove', (e) => this.onDrag(e), { passive: false });
|
|
806
806
|
document.addEventListener('touchend', (e) => this.endDrag(e));
|
|
807
|
+
|
|
808
|
+
// Window resize listener to update position when viewport changes
|
|
809
|
+
window.addEventListener('resize', () => this.onWindowResize());
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
/**
|
|
813
|
+
* Handle window resize - update widget position to stay relative to viewport
|
|
814
|
+
*/
|
|
815
|
+
onWindowResize() {
|
|
816
|
+
const savedPosition = this.loadWidgetPosition();
|
|
817
|
+
if (savedPosition && this.widgetWrapper) {
|
|
818
|
+
this.updateWidgetPosition(savedPosition.x, savedPosition.y);
|
|
819
|
+
this.updateRelatedElementsPosition();
|
|
820
|
+
}
|
|
807
821
|
}
|
|
808
822
|
|
|
809
823
|
/**
|
|
@@ -904,11 +918,16 @@ export default class RbirdWebsiteWidget {
|
|
|
904
918
|
}
|
|
905
919
|
|
|
906
920
|
/**
|
|
907
|
-
* Save widget position to localStorage
|
|
921
|
+
* Save widget position to localStorage as percentage of viewport
|
|
908
922
|
*/
|
|
909
923
|
saveWidgetPosition(x, y) {
|
|
910
924
|
try {
|
|
911
|
-
|
|
925
|
+
// Store position as percentage of viewport for responsive behavior
|
|
926
|
+
const position = {
|
|
927
|
+
xPercent: x / window.innerWidth,
|
|
928
|
+
yPercent: y / window.innerHeight,
|
|
929
|
+
timestamp: Date.now()
|
|
930
|
+
};
|
|
912
931
|
window.localStorage.setItem('rbird_widget_position', JSON.stringify(position));
|
|
913
932
|
} catch (e) {
|
|
914
933
|
console.warn('[RbirdWidget] Could not save position to localStorage:', e);
|
|
@@ -916,17 +935,38 @@ export default class RbirdWebsiteWidget {
|
|
|
916
935
|
}
|
|
917
936
|
|
|
918
937
|
/**
|
|
919
|
-
* Load widget position from localStorage
|
|
938
|
+
* Load widget position from localStorage and convert to current viewport pixels
|
|
920
939
|
*/
|
|
921
940
|
loadWidgetPosition() {
|
|
922
941
|
try {
|
|
923
942
|
const saved = window.localStorage.getItem('rbird_widget_position');
|
|
924
943
|
if (saved) {
|
|
925
944
|
const position = JSON.parse(saved);
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
945
|
+
|
|
946
|
+
// Handle new percentage-based format
|
|
947
|
+
if (position.xPercent !== undefined && position.yPercent !== undefined) {
|
|
948
|
+
const x = position.xPercent * window.innerWidth;
|
|
949
|
+
const y = position.yPercent * window.innerHeight;
|
|
950
|
+
|
|
951
|
+
// Ensure position is within viewport bounds
|
|
952
|
+
const bubbleSize = 60; // approximate bubble size
|
|
953
|
+
const clampedX = Math.max(0, Math.min(x, window.innerWidth - bubbleSize));
|
|
954
|
+
const clampedY = Math.max(0, Math.min(y, window.innerHeight - bubbleSize));
|
|
955
|
+
|
|
956
|
+
return { x: clampedX, y: clampedY };
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
// Handle legacy absolute pixel format - convert to percentage and save
|
|
960
|
+
if (position.x !== undefined && position.y !== undefined) {
|
|
961
|
+
// Clamp to current viewport
|
|
962
|
+
const bubbleSize = 60;
|
|
963
|
+
const clampedX = Math.max(0, Math.min(position.x, window.innerWidth - bubbleSize));
|
|
964
|
+
const clampedY = Math.max(0, Math.min(position.y, window.innerHeight - bubbleSize));
|
|
965
|
+
|
|
966
|
+
// Convert to percentage and save for future
|
|
967
|
+
this.saveWidgetPosition(clampedX, clampedY);
|
|
968
|
+
|
|
969
|
+
return { x: clampedX, y: clampedY };
|
|
930
970
|
}
|
|
931
971
|
}
|
|
932
972
|
} catch (e) {
|
package/src/index.js
CHANGED
|
@@ -5,6 +5,7 @@ import { ReleasebirdConsoleLogger } from "./ReleasebirdConsoleLogger";
|
|
|
5
5
|
import { RbirdBannerManager } from "./RbirdBannerManager";
|
|
6
6
|
import { RbirdFormManager } from "./RbirdFormManager";
|
|
7
7
|
import { RbirdSurveyManager } from "./RbirdSurveyManager";
|
|
8
|
+
import { RbirdAutomationManager } from "./RbirdAutomationManager";
|
|
8
9
|
|
|
9
10
|
class Rbird {
|
|
10
11
|
|
|
@@ -98,6 +99,11 @@ class Rbird {
|
|
|
98
99
|
// Initialize survey manager
|
|
99
100
|
const surveyManager = RbirdSurveyManager.getInstance();
|
|
100
101
|
surveyManager.init(apiKey);
|
|
102
|
+
|
|
103
|
+
// Initialize automation manager
|
|
104
|
+
const automationManager = RbirdAutomationManager.getInstance();
|
|
105
|
+
automationManager.init(apiKey);
|
|
106
|
+
|
|
101
107
|
resolve();
|
|
102
108
|
});
|
|
103
109
|
});
|
|
@@ -183,6 +189,20 @@ class Rbird {
|
|
|
183
189
|
}
|
|
184
190
|
}
|
|
185
191
|
|
|
192
|
+
/**
|
|
193
|
+
* Track a custom event for automations
|
|
194
|
+
* @param {string} eventName - The name of the event
|
|
195
|
+
* @param {Object} data - Additional event data
|
|
196
|
+
*/
|
|
197
|
+
static track(eventName, data = {}) {
|
|
198
|
+
if (typeof window === 'undefined') return;
|
|
199
|
+
try {
|
|
200
|
+
RbirdAutomationManager.getInstance().track(eventName, data);
|
|
201
|
+
} catch (e) {
|
|
202
|
+
console.error(e);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
186
206
|
}
|
|
187
207
|
|
|
188
208
|
export const runFunctionWhenDomIsReady = (callback) => {
|