cuoral-ionic 0.0.1

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.
@@ -0,0 +1,819 @@
1
+ import { registerPlugin, Capacitor, WebPlugin } from '@capacitor/core';
2
+
3
+ /**
4
+ * Message types for communication between WebView and Native code
5
+ */
6
+ var CuoralMessageType;
7
+ (function (CuoralMessageType) {
8
+ // Screen Recording
9
+ CuoralMessageType["START_RECORDING"] = "CUORAL_START_RECORDING";
10
+ CuoralMessageType["STOP_RECORDING"] = "CUORAL_STOP_RECORDING";
11
+ CuoralMessageType["RECORDING_STARTED"] = "CUORAL_RECORDING_STARTED";
12
+ CuoralMessageType["RECORDING_STOPPED"] = "CUORAL_RECORDING_STOPPED";
13
+ CuoralMessageType["RECORDING_ERROR"] = "CUORAL_RECORDING_ERROR";
14
+ // Screenshot
15
+ CuoralMessageType["TAKE_SCREENSHOT"] = "CUORAL_TAKE_SCREENSHOT";
16
+ CuoralMessageType["SCREENSHOT_TAKEN"] = "CUORAL_SCREENSHOT_TAKEN";
17
+ CuoralMessageType["SCREENSHOT_ERROR"] = "CUORAL_SCREENSHOT_ERROR";
18
+ // Widget Communication
19
+ CuoralMessageType["WIDGET_READY"] = "CUORAL_WIDGET_READY";
20
+ CuoralMessageType["WIDGET_CLOSED"] = "CUORAL_WIDGET_CLOSED";
21
+ // File Upload
22
+ CuoralMessageType["UPLOAD_FILE"] = "CUORAL_UPLOAD_FILE";
23
+ CuoralMessageType["UPLOAD_PROGRESS"] = "CUORAL_UPLOAD_PROGRESS";
24
+ CuoralMessageType["UPLOAD_COMPLETE"] = "CUORAL_UPLOAD_COMPLETE";
25
+ CuoralMessageType["UPLOAD_ERROR"] = "CUORAL_UPLOAD_ERROR";
26
+ })(CuoralMessageType || (CuoralMessageType = {}));
27
+
28
+ /**
29
+ * Register the Capacitor plugin
30
+ */
31
+ const CuoralPlugin = registerPlugin('CuoralPlugin', {
32
+ web: () => Promise.resolve().then(function () { return web; }).then(m => new m.CuoralPluginWeb()),
33
+ });
34
+ /**
35
+ * High-level API for easier integration
36
+ */
37
+ class CuoralRecorder {
38
+ constructor() {
39
+ this.isRecording = false;
40
+ }
41
+ /**
42
+ * Start recording with automatic permission handling
43
+ */
44
+ async startRecording(options) {
45
+ try {
46
+ // Check if already recording
47
+ if (this.isRecording) {
48
+ console.warn('[Cuoral] Already recording');
49
+ return false;
50
+ }
51
+ // Check support
52
+ const { supported } = await CuoralPlugin.isRecordingSupported();
53
+ if (!supported) {
54
+ throw new Error('Screen recording is not supported on this device');
55
+ }
56
+ // Request permissions
57
+ const { granted } = await CuoralPlugin.requestPermissions();
58
+ if (!granted) {
59
+ throw new Error('Recording permissions not granted');
60
+ }
61
+ // Start recording
62
+ const result = await CuoralPlugin.startRecording(options);
63
+ if (result.success) {
64
+ this.isRecording = true;
65
+ this.recordingStartTime = Date.now();
66
+ // Post message to widget
67
+ this.postMessage({
68
+ type: CuoralMessageType.RECORDING_STARTED,
69
+ payload: { timestamp: this.recordingStartTime },
70
+ });
71
+ }
72
+ return result.success;
73
+ }
74
+ catch (error) {
75
+ console.error('[Cuoral] Failed to start recording:', error);
76
+ this.postMessage({
77
+ type: CuoralMessageType.RECORDING_ERROR,
78
+ payload: { error: error.message },
79
+ });
80
+ return false;
81
+ }
82
+ }
83
+ /**
84
+ * Stop recording
85
+ */
86
+ async stopRecording() {
87
+ try {
88
+ if (!this.isRecording) {
89
+ console.warn('[Cuoral] Not recording');
90
+ return null;
91
+ }
92
+ const result = await CuoralPlugin.stopRecording();
93
+ if (result.success) {
94
+ this.isRecording = false;
95
+ const duration = this.recordingStartTime
96
+ ? Math.floor((Date.now() - this.recordingStartTime) / 1000)
97
+ : 0;
98
+ // Post message to widget
99
+ this.postMessage({
100
+ type: CuoralMessageType.RECORDING_STOPPED,
101
+ payload: {
102
+ filePath: result.filePath,
103
+ duration: result.duration || duration,
104
+ },
105
+ });
106
+ return {
107
+ filePath: result.filePath,
108
+ duration: result.duration || duration,
109
+ };
110
+ }
111
+ return null;
112
+ }
113
+ catch (error) {
114
+ console.error('[Cuoral] Failed to stop recording:', error);
115
+ this.postMessage({
116
+ type: CuoralMessageType.RECORDING_ERROR,
117
+ payload: { error: error.message },
118
+ });
119
+ return null;
120
+ }
121
+ }
122
+ /**
123
+ * Take screenshot
124
+ */
125
+ async takeScreenshot(options) {
126
+ try {
127
+ const screenshot = await CuoralPlugin.takeScreenshot(options);
128
+ // Post message to widget
129
+ this.postMessage({
130
+ type: CuoralMessageType.SCREENSHOT_TAKEN,
131
+ payload: screenshot,
132
+ });
133
+ return screenshot;
134
+ }
135
+ catch (error) {
136
+ console.error('[Cuoral] Failed to take screenshot:', error);
137
+ this.postMessage({
138
+ type: CuoralMessageType.SCREENSHOT_ERROR,
139
+ payload: { error: error.message },
140
+ });
141
+ return null;
142
+ }
143
+ }
144
+ /**
145
+ * Get current recording state
146
+ */
147
+ async getState() {
148
+ return await CuoralPlugin.getRecordingState();
149
+ }
150
+ /**
151
+ * Post message to WebView
152
+ */
153
+ postMessage(message) {
154
+ if (typeof window !== 'undefined' && window.postMessage) {
155
+ window.postMessage(message, '*');
156
+ }
157
+ }
158
+ }
159
+
160
+ /**
161
+ * Bridge for bidirectional communication between WebView and Native code
162
+ */
163
+ class CuoralBridge {
164
+ constructor(config) {
165
+ this.messageHandlers = new Map();
166
+ this.isInitialized = false;
167
+ this.widgetIframe = null;
168
+ this.config = config;
169
+ this.setupMessageListener();
170
+ }
171
+ /**
172
+ * Initialize the bridge
173
+ */
174
+ initialize() {
175
+ if (this.isInitialized) {
176
+ this.log('Bridge already initialized');
177
+ return;
178
+ }
179
+ this.isInitialized = true;
180
+ this.log('Bridge initialized');
181
+ // Notify widget that bridge is ready
182
+ this.sendToWidget({
183
+ type: CuoralMessageType.WIDGET_READY,
184
+ timestamp: Date.now(),
185
+ });
186
+ }
187
+ /**
188
+ * Send message to native code
189
+ */
190
+ sendToNative(message) {
191
+ if (!message.timestamp) {
192
+ message.timestamp = Date.now();
193
+ }
194
+ this.log('Sending to native:', message);
195
+ // For Capacitor - use window.postMessage
196
+ if (window.webkit?.messageHandlers?.cuoral) {
197
+ // iOS WKWebView
198
+ window.webkit.messageHandlers.cuoral.postMessage(message);
199
+ }
200
+ else if (window.CuoralAndroid) {
201
+ // Android JavascriptInterface
202
+ window.CuoralAndroid.postMessage(JSON.stringify(message));
203
+ }
204
+ else {
205
+ // Fallback - use postMessage for any other WebView
206
+ window.postMessage(message, '*');
207
+ }
208
+ }
209
+ /**
210
+ * Send message to widget iframe
211
+ */
212
+ sendToWidget(message) {
213
+ if (!message.timestamp) {
214
+ message.timestamp = Date.now();
215
+ }
216
+ this.log('Sending to widget:', message);
217
+ // Find the iframe element
218
+ const iframe = document.getElementById('cuoral-widget-iframe');
219
+ if (iframe?.contentWindow) {
220
+ try {
221
+ // Use postMessage for cross-origin communication
222
+ iframe.contentWindow.postMessage(message, '*');
223
+ this.log('Message sent via postMessage');
224
+ return;
225
+ }
226
+ catch (error) {
227
+ this.log('Error using postMessage:', error);
228
+ }
229
+ }
230
+ else {
231
+ this.log('Iframe not found or not ready');
232
+ }
233
+ // Fallback: Use localStorage for same-origin
234
+ try {
235
+ const storageKey = `cuoral_message_${Date.now()}`;
236
+ localStorage.setItem(storageKey, JSON.stringify(message));
237
+ // Clean up after 5 seconds
238
+ setTimeout(() => {
239
+ localStorage.removeItem(storageKey);
240
+ }, 5000);
241
+ // Trigger a storage event by updating a counter
242
+ const counter = parseInt(localStorage.getItem('cuoral_message_counter') || '0');
243
+ localStorage.setItem('cuoral_message_counter', (counter + 1).toString());
244
+ }
245
+ catch (error) {
246
+ this.log('Error using localStorage:', error);
247
+ }
248
+ }
249
+ /**
250
+ * Register message handler
251
+ */
252
+ on(type, handler) {
253
+ if (!this.messageHandlers.has(type)) {
254
+ this.messageHandlers.set(type, []);
255
+ }
256
+ const handlers = this.messageHandlers.get(type);
257
+ handlers.push(handler);
258
+ this.log(`Registered handler for ${type}`);
259
+ // Return unsubscribe function
260
+ return () => {
261
+ const index = handlers.indexOf(handler);
262
+ if (index > -1) {
263
+ handlers.splice(index, 1);
264
+ }
265
+ };
266
+ }
267
+ /**
268
+ * Setup window message listener
269
+ */
270
+ setupMessageListener() {
271
+ window.addEventListener('message', (event) => {
272
+ // Only process messages FROM the iframe
273
+ const widgetFrame = document.querySelector('iframe[src*="mobile.html"]');
274
+ if (widgetFrame && event.source === widgetFrame.contentWindow) {
275
+ this.widgetIframe = widgetFrame;
276
+ }
277
+ else if (widgetFrame) {
278
+ return;
279
+ }
280
+ // Validate message structure
281
+ if (!event.data || !event.data.type) {
282
+ return;
283
+ }
284
+ const message = event.data;
285
+ // Check if it's a Cuoral message
286
+ if (!Object.values(CuoralMessageType).includes(message.type)) {
287
+ return;
288
+ }
289
+ this.log('Cuoral message received:', message);
290
+ // Call custom handler if provided
291
+ if (this.config.onMessage) {
292
+ this.config.onMessage(message);
293
+ }
294
+ // Call registered handlers
295
+ const handlers = this.messageHandlers.get(message.type);
296
+ if (handlers) {
297
+ handlers.forEach(handler => handler(message.payload));
298
+ }
299
+ });
300
+ }
301
+ /**
302
+ * Debug logging
303
+ */
304
+ log(...args) {
305
+ if (this.config.debug) {
306
+ console.log('[CuoralBridge]', ...args);
307
+ }
308
+ }
309
+ /**
310
+ * Destroy the bridge
311
+ */
312
+ destroy() {
313
+ this.messageHandlers.clear();
314
+ this.isInitialized = false;
315
+ this.log('Bridge destroyed');
316
+ }
317
+ }
318
+
319
+ /**
320
+ * Cuoral Modal Manager
321
+ * Handles full-screen modal display with floating button
322
+ */
323
+ class CuoralModal {
324
+ constructor(widgetUrl, showFloatingButton = true) {
325
+ this.isOpen = false;
326
+ this.widgetUrl = widgetUrl;
327
+ this.showFloatingButton = showFloatingButton;
328
+ }
329
+ /**
330
+ * Initialize the modal and floating button
331
+ */
332
+ initialize() {
333
+ if (this.showFloatingButton) {
334
+ this.createFloatingButton();
335
+ }
336
+ this.createModal();
337
+ }
338
+ /**
339
+ * Create floating chat button
340
+ */
341
+ createFloatingButton() {
342
+ this.floatingButton = document.createElement('div');
343
+ this.floatingButton.id = 'cuoral-floating-button';
344
+ this.floatingButton.innerHTML = `
345
+ <svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
346
+ <path d="M20 2H4C2.9 2 2 2.9 2 4V22L6 18H20C21.1 18 22 17.1 22 16V4C22 2.9 21.1 2 20 2ZM20 16H6L4 18V4H20V16Z" fill="white"/>
347
+ </svg>
348
+ `;
349
+ // Styles
350
+ Object.assign(this.floatingButton.style, {
351
+ position: 'fixed',
352
+ bottom: '20px',
353
+ right: '20px',
354
+ width: '56px',
355
+ height: '56px',
356
+ borderRadius: '50%',
357
+ backgroundColor: '#007AFF',
358
+ display: 'flex',
359
+ alignItems: 'center',
360
+ justifyContent: 'center',
361
+ cursor: 'pointer',
362
+ boxShadow: '0 4px 12px rgba(0, 122, 255, 0.4)',
363
+ zIndex: '999999',
364
+ transition: 'transform 0.2s, box-shadow 0.2s'
365
+ });
366
+ // Hover effect
367
+ this.floatingButton.addEventListener('mouseenter', () => {
368
+ if (this.floatingButton) {
369
+ this.floatingButton.style.transform = 'scale(1.1)';
370
+ this.floatingButton.style.boxShadow = '0 6px 16px rgba(0, 122, 255, 0.5)';
371
+ }
372
+ });
373
+ this.floatingButton.addEventListener('mouseleave', () => {
374
+ if (this.floatingButton) {
375
+ this.floatingButton.style.transform = 'scale(1)';
376
+ this.floatingButton.style.boxShadow = '0 4px 12px rgba(0, 122, 255, 0.4)';
377
+ }
378
+ });
379
+ // Click to open modal
380
+ this.floatingButton.addEventListener('click', () => {
381
+ this.open();
382
+ });
383
+ document.body.appendChild(this.floatingButton);
384
+ }
385
+ /**
386
+ * Create modal structure
387
+ */
388
+ createModal() {
389
+ // Modal container
390
+ this.modalElement = document.createElement('div');
391
+ this.modalElement.id = 'cuoral-modal';
392
+ Object.assign(this.modalElement.style, {
393
+ position: 'fixed',
394
+ top: '0',
395
+ left: '0',
396
+ width: '100%',
397
+ height: '100%',
398
+ backgroundColor: 'rgba(0, 0, 0, 0.5)',
399
+ display: 'none',
400
+ alignItems: 'center',
401
+ justifyContent: 'center',
402
+ zIndex: '1000000',
403
+ opacity: '0',
404
+ transition: 'opacity 0.3s ease',
405
+ padding: '60px 0'
406
+ });
407
+ // Modal content (with margin)
408
+ const modalContent = document.createElement('div');
409
+ Object.assign(modalContent.style, {
410
+ width: '100%',
411
+ height: '100%',
412
+ maxWidth: '100%',
413
+ maxHeight: '100%',
414
+ position: 'relative',
415
+ backgroundColor: 'transparent',
416
+ display: 'flex',
417
+ flexDirection: 'column',
418
+ borderRadius: '12px',
419
+ overflow: 'hidden'
420
+ });
421
+ // Close button
422
+ const closeButton = document.createElement('div');
423
+ closeButton.innerHTML = `
424
+ <svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
425
+ <path d="M18 6L6 18M6 6L18 18" stroke="white" stroke-width="2" stroke-linecap="round"/>
426
+ </svg>
427
+ `;
428
+ Object.assign(closeButton.style, {
429
+ position: 'absolute',
430
+ top: '20px',
431
+ right: '20px',
432
+ width: '40px',
433
+ height: '40px',
434
+ borderRadius: '50%',
435
+ backgroundColor: 'rgba(0, 0, 0, 0.5)',
436
+ display: 'flex',
437
+ alignItems: 'center',
438
+ justifyContent: 'center',
439
+ cursor: 'pointer',
440
+ zIndex: '1000001',
441
+ transition: 'background-color 0.2s'
442
+ });
443
+ closeButton.addEventListener('mouseenter', () => {
444
+ closeButton.style.backgroundColor = 'rgba(0, 0, 0, 0.7)';
445
+ });
446
+ closeButton.addEventListener('mouseleave', () => {
447
+ closeButton.style.backgroundColor = 'rgba(0, 0, 0, 0.5)';
448
+ });
449
+ closeButton.addEventListener('click', () => {
450
+ this.close();
451
+ });
452
+ // Iframe
453
+ this.iframeElement = document.createElement('iframe');
454
+ this.iframeElement.id = 'cuoral-widget-iframe';
455
+ this.iframeElement.src = this.widgetUrl;
456
+ Object.assign(this.iframeElement.style, {
457
+ width: '100%',
458
+ height: '100%',
459
+ border: 'none',
460
+ backgroundColor: 'white'
461
+ });
462
+ modalContent.appendChild(closeButton);
463
+ modalContent.appendChild(this.iframeElement);
464
+ this.modalElement.appendChild(modalContent);
465
+ // Close on backdrop click
466
+ this.modalElement.addEventListener('click', (e) => {
467
+ if (e.target === this.modalElement) {
468
+ this.close();
469
+ }
470
+ });
471
+ document.body.appendChild(this.modalElement);
472
+ }
473
+ /**
474
+ * Open the modal
475
+ */
476
+ open() {
477
+ if (this.isOpen || !this.modalElement)
478
+ return;
479
+ this.isOpen = true;
480
+ this.modalElement.style.display = 'flex';
481
+ // Trigger animation
482
+ setTimeout(() => {
483
+ if (this.modalElement) {
484
+ this.modalElement.style.opacity = '1';
485
+ }
486
+ }, 10);
487
+ // Hide floating button when modal is open
488
+ if (this.floatingButton) {
489
+ this.floatingButton.style.display = 'none';
490
+ }
491
+ // Prevent body scroll
492
+ document.body.style.overflow = 'hidden';
493
+ }
494
+ /**
495
+ * Close the modal
496
+ */
497
+ close() {
498
+ if (!this.isOpen || !this.modalElement)
499
+ return;
500
+ this.isOpen = false;
501
+ this.modalElement.style.opacity = '0';
502
+ setTimeout(() => {
503
+ if (this.modalElement) {
504
+ this.modalElement.style.display = 'none';
505
+ }
506
+ // Show floating button again
507
+ if (this.floatingButton) {
508
+ this.floatingButton.style.display = 'flex';
509
+ }
510
+ // Restore body scroll
511
+ document.body.style.overflow = '';
512
+ }, 300);
513
+ }
514
+ /**
515
+ * Check if modal is open
516
+ */
517
+ isModalOpen() {
518
+ return this.isOpen;
519
+ }
520
+ /**
521
+ * Destroy modal and floating button
522
+ */
523
+ destroy() {
524
+ if (this.modalElement) {
525
+ this.modalElement.remove();
526
+ }
527
+ if (this.floatingButton) {
528
+ this.floatingButton.remove();
529
+ }
530
+ document.body.style.overflow = '';
531
+ }
532
+ /**
533
+ * Get iframe element for communication
534
+ */
535
+ getIframe() {
536
+ return this.iframeElement;
537
+ }
538
+ }
539
+
540
+ /**
541
+ * Main Cuoral class - simple API for users
542
+ */
543
+ class Cuoral {
544
+ constructor(options) {
545
+ this.options = {
546
+ showFloatingButton: true,
547
+ useModal: true,
548
+ ...options
549
+ };
550
+ // Determine widget base URL
551
+ const baseUrl = options.widgetBaseUrl || Cuoral.PRODUCTION_WIDGET_URL;
552
+ const params = new URLSearchParams({
553
+ auto_start: 'true',
554
+ key: options.publicKey,
555
+ _t: Date.now().toString(),
556
+ });
557
+ if (options.email)
558
+ params.set('email', options.email);
559
+ if (options.firstName)
560
+ params.set('first_name', options.firstName);
561
+ if (options.lastName)
562
+ params.set('last_name', options.lastName);
563
+ const widgetUrl = `${baseUrl}?${params.toString()}`;
564
+ // Initialize modal if enabled
565
+ if (this.options.useModal) {
566
+ this.modal = new CuoralModal(widgetUrl, this.options.showFloatingButton);
567
+ }
568
+ // Initialize bridge and recorder
569
+ this.bridge = new CuoralBridge({
570
+ widgetUrl,
571
+ debug: options.debug || false
572
+ });
573
+ this.recorder = new CuoralRecorder();
574
+ // Setup automatic message handlers
575
+ this.setupMessageHandlers();
576
+ }
577
+ /**
578
+ * Initialize Cuoral
579
+ */
580
+ initialize() {
581
+ this.bridge.initialize();
582
+ // Initialize modal if enabled
583
+ if (this.modal) {
584
+ this.modal.initialize();
585
+ }
586
+ }
587
+ /**
588
+ * Open the widget modal
589
+ */
590
+ openModal() {
591
+ if (this.modal) {
592
+ this.modal.open();
593
+ }
594
+ }
595
+ /**
596
+ * Close the widget modal
597
+ */
598
+ closeModal() {
599
+ if (this.modal) {
600
+ this.modal.close();
601
+ }
602
+ }
603
+ /**
604
+ * Check if modal is open
605
+ */
606
+ isModalOpen() {
607
+ return this.modal?.isModalOpen() || false;
608
+ }
609
+ /**
610
+ * Get the widget URL for iframe embedding
611
+ */
612
+ getWidgetUrl() {
613
+ const baseUrl = this.options.widgetBaseUrl || Cuoral.PRODUCTION_WIDGET_URL;
614
+ const params = new URLSearchParams({
615
+ auto_start: 'true',
616
+ key: this.options.publicKey,
617
+ _t: Date.now().toString(),
618
+ });
619
+ if (this.options.email)
620
+ params.set('email', this.options.email);
621
+ if (this.options.firstName)
622
+ params.set('first_name', this.options.firstName);
623
+ if (this.options.lastName)
624
+ params.set('last_name', this.options.lastName);
625
+ return `${baseUrl}?${params.toString()}`;
626
+ }
627
+ /**
628
+ * Clean up resources
629
+ */
630
+ destroy() {
631
+ this.bridge.destroy();
632
+ // Destroy modal if exists
633
+ if (this.modal) {
634
+ this.modal.destroy();
635
+ }
636
+ }
637
+ /**
638
+ * Setup automatic message handlers
639
+ */
640
+ setupMessageHandlers() {
641
+ // Handle start recording requests from widget
642
+ this.bridge.on(CuoralMessageType.START_RECORDING, async () => {
643
+ await this.recorder.startRecording();
644
+ });
645
+ // Handle stop recording requests from widget
646
+ this.bridge.on(CuoralMessageType.STOP_RECORDING, async () => {
647
+ const result = await this.recorder.stopRecording();
648
+ if (result) {
649
+ // Convert file path to web-accessible URL
650
+ const capacitorUrl = result.filePath ? Capacitor.convertFileSrc(result.filePath) : '';
651
+ try {
652
+ // Fetch the video blob from the capacitor URL
653
+ const response = await fetch(capacitorUrl);
654
+ const videoBlob = await response.blob();
655
+ // Convert blob to base64 data URL so it can be serialized and transferred cross-origin
656
+ const reader = new FileReader();
657
+ reader.onloadend = () => {
658
+ const base64data = reader.result;
659
+ // Send the base64 data via bridge to CDN widget
660
+ this.bridge.sendToWidget({
661
+ type: CuoralMessageType.RECORDING_STOPPED,
662
+ payload: {
663
+ videoData: base64data,
664
+ videoType: 'video/mp4',
665
+ originalPath: result.filePath,
666
+ duration: result.duration,
667
+ timestamp: Date.now()
668
+ }
669
+ });
670
+ };
671
+ reader.onerror = (error) => {
672
+ console.error('[Cuoral] Error reading blob:', error);
673
+ this.bridge.sendToWidget({
674
+ type: CuoralMessageType.RECORDING_STOPPED,
675
+ payload: {
676
+ error: 'Failed to process recording',
677
+ duration: result.duration,
678
+ timestamp: Date.now()
679
+ }
680
+ });
681
+ };
682
+ // Start reading the blob as data URL
683
+ reader.readAsDataURL(videoBlob);
684
+ }
685
+ catch (error) {
686
+ console.error('[Cuoral] Error processing video file:', error);
687
+ this.bridge.sendToWidget({
688
+ type: CuoralMessageType.RECORDING_STOPPED,
689
+ payload: {
690
+ error: 'Failed to process recording',
691
+ duration: result.duration,
692
+ timestamp: Date.now()
693
+ }
694
+ });
695
+ }
696
+ }
697
+ else {
698
+ console.error('[Cuoral] No result from stopRecording');
699
+ }
700
+ });
701
+ }
702
+ }
703
+ Cuoral.PRODUCTION_WIDGET_URL = 'https://js.cuoral.com/mobile.html';
704
+ Cuoral.DEV_WIDGET_URL = 'assets/mobile.html';
705
+
706
+ /**
707
+ * Web implementation (for testing in browser)
708
+ */
709
+ class CuoralPluginWeb extends WebPlugin {
710
+ constructor() {
711
+ super(...arguments);
712
+ this.isRecording = false;
713
+ }
714
+ async startRecording(options) {
715
+ console.log('[Cuoral Web] Start recording', options);
716
+ // Check if Screen Capture API is available
717
+ if (!navigator.mediaDevices || !navigator.mediaDevices.getDisplayMedia) {
718
+ throw new Error('Screen recording is not supported in this browser');
719
+ }
720
+ try {
721
+ // Request screen capture
722
+ const stream = await navigator.mediaDevices.getDisplayMedia({
723
+ video: true,
724
+ audio: options?.includeAudio || false,
725
+ });
726
+ this.isRecording = true;
727
+ this.recordingStartTime = Date.now();
728
+ // Store stream for later stopping
729
+ window.__cuoralRecordingStream = stream;
730
+ return { success: true };
731
+ }
732
+ catch (error) {
733
+ console.error('[Cuoral Web] Failed to start recording:', error);
734
+ return { success: false };
735
+ }
736
+ }
737
+ async stopRecording() {
738
+ console.log('[Cuoral Web] Stop recording');
739
+ if (!this.isRecording) {
740
+ return { success: false };
741
+ }
742
+ const stream = window.__cuoralRecordingStream;
743
+ if (stream) {
744
+ stream.getTracks().forEach(track => track.stop());
745
+ delete window.__cuoralRecordingStream;
746
+ }
747
+ const duration = this.recordingStartTime
748
+ ? Math.floor((Date.now() - this.recordingStartTime) / 1000)
749
+ : 0;
750
+ this.isRecording = false;
751
+ this.recordingStartTime = undefined;
752
+ return {
753
+ success: true,
754
+ duration,
755
+ filePath: 'web-recording-not-saved',
756
+ };
757
+ }
758
+ async getRecordingState() {
759
+ return {
760
+ isRecording: this.isRecording,
761
+ duration: this.recordingStartTime
762
+ ? Math.floor((Date.now() - this.recordingStartTime) / 1000)
763
+ : undefined,
764
+ };
765
+ }
766
+ async takeScreenshot(options) {
767
+ console.log('[Cuoral Web] Take screenshot', options);
768
+ try {
769
+ // Use Screen Capture API
770
+ const stream = await navigator.mediaDevices.getDisplayMedia({
771
+ video: { width: 1920, height: 1080 },
772
+ });
773
+ // Capture frame from video stream
774
+ const video = document.createElement('video');
775
+ video.srcObject = stream;
776
+ video.play();
777
+ await new Promise(resolve => {
778
+ video.onloadedmetadata = resolve;
779
+ });
780
+ const canvas = document.createElement('canvas');
781
+ canvas.width = video.videoWidth;
782
+ canvas.height = video.videoHeight;
783
+ const ctx = canvas.getContext('2d');
784
+ ctx?.drawImage(video, 0, 0);
785
+ // Stop stream
786
+ stream.getTracks().forEach(track => track.stop());
787
+ // Convert to base64
788
+ const format = options?.format || 'png';
789
+ const quality = options?.quality || 0.92;
790
+ const base64 = canvas.toDataURL(`image/${format}`, quality);
791
+ return {
792
+ base64: base64.split(',')[1],
793
+ mimeType: `image/${format}`,
794
+ width: canvas.width,
795
+ height: canvas.height,
796
+ };
797
+ }
798
+ catch (error) {
799
+ console.error('[Cuoral Web] Failed to take screenshot:', error);
800
+ throw error;
801
+ }
802
+ }
803
+ async isRecordingSupported() {
804
+ const supported = !!(navigator.mediaDevices && navigator.mediaDevices.getDisplayMedia);
805
+ return { supported };
806
+ }
807
+ async requestPermissions() {
808
+ // Web doesn't require explicit permissions - they're requested on demand
809
+ return { granted: true };
810
+ }
811
+ }
812
+
813
+ var web = /*#__PURE__*/Object.freeze({
814
+ __proto__: null,
815
+ CuoralPluginWeb: CuoralPluginWeb
816
+ });
817
+
818
+ export { Cuoral, CuoralBridge, CuoralMessageType, CuoralModal, CuoralPlugin, CuoralRecorder };
819
+ //# sourceMappingURL=index.esm.js.map