cuoral-ionic 0.0.8 → 0.1.0

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,17 @@
1
+ require 'json'
2
+
3
+ package = JSON.parse(File.read(File.join(__dir__, 'package.json')))
4
+
5
+ Pod::Spec.new do |s|
6
+ s.name = 'CuoralIonic'
7
+ s.version = package['version']
8
+ s.summary = package['description']
9
+ s.license = package['license']
10
+ s.homepage = package['repository']['url']
11
+ s.author = package['author']
12
+ s.source = { :git => package['repository']['url'], :tag => s.version.to_s }
13
+ s.source_files = 'ios/Plugin/**/*.{swift,h,m,c,cc,mm,cpp}'
14
+ s.ios.deployment_target = '13.0'
15
+ s.dependency 'Capacitor'
16
+ s.swift_version = '5.1'
17
+ end
package/README.md CHANGED
@@ -57,6 +57,33 @@ Add to your `Info.plist`:
57
57
 
58
58
  **Note:** Screen recording permission is requested automatically by iOS when recording starts. No additional permissions are needed for crash tracking or intelligence features.
59
59
 
60
+ ### 📱 Google Play Store Declaration (Android Only)
61
+
62
+ **IMPORTANT:** When submitting your app to Google Play Store, you'll be asked to declare why your app uses `FOREGROUND_SERVICE_MEDIA_PROJECTION` (screen recording permission).
63
+
64
+ **What to Do:**
65
+
66
+ 1. Go to Play Console → **Policy** → **App Content** → **Foreground Service Types**
67
+ 2. Find `FOREGROUND_SERVICE_MEDIA_PROJECTION` → Click **Manage**
68
+ 3. Select category: **Customer Support / Bug Reporting**
69
+ 4. Provide justification:
70
+
71
+ ```
72
+ Our app uses screen recording exclusively for customer support purposes when users
73
+ initiate support requests. Users explicitly control when recording starts/stops via
74
+ the support widget. All recordings are transmitted securely to our support platform
75
+ to help resolve user-reported issues. No recording occurs without user action.
76
+ ```
77
+
78
+ **Why This is Required:**
79
+
80
+ - Screen recording is a "sensitive permission" that requires declaration
81
+ - Google wants to ensure it's used appropriately (which it is - user-initiated support)
82
+ - This is a simple form, not a special approval process
83
+ - No code changes needed - your app is already configured correctly
84
+
85
+ **Note:** iOS App Store does not require this declaration.
86
+
60
87
  ## Quick Start
61
88
 
62
89
  ### Option 1: Modal with Floating Button (Recommended)
@@ -313,9 +340,37 @@ interface CuoralOptions {
313
340
  widgetBaseUrl?: string; // Optional: Custom widget URL (default: CDN)
314
341
  showFloatingButton?: boolean; // Optional: Show floating chat button (default: true)
315
342
  useModal?: boolean; // Optional: Use modal display mode (default: true)
343
+ primaryColor?: string; // Optional: Custom primary color (auto-fetched from backend if not provided)
316
344
  }
317
345
  ```
318
346
 
347
+ ### Branding & Colors
348
+
349
+ The floating button automatically uses your organization's brand color configured in the Cuoral dashboard. During `initialize()`, the SDK fetches your organization's configuration from the backend:
350
+
351
+ ```typescript
352
+ // Automatic (Recommended):
353
+ const cuoral = new Cuoral({ publicKey: 'your-key' });
354
+ await cuoral.initialize(); // Fetches your brand color from backend
355
+
356
+ // Manual override (Optional):
357
+ const cuoral = new Cuoral({
358
+ publicKey: 'your-key',
359
+ primaryColor: '#FF5733', // Force specific color
360
+ });
361
+ await cuoral.initialize();
362
+ ```
363
+
364
+ **How it works:**
365
+
366
+ - SDK fetches session configuration (`POST /conversation/session/get`) during initialization
367
+ - Extracts `color` from `configuration` object in response
368
+ - Applies color to floating button background and shadows
369
+ - Falls back to default blue (#007AFF) if color not provided
370
+
371
+ **Configure your brand color:**
372
+ Go to Cuoral Dashboard → Settings → Branding → Primary Color
373
+
319
374
  ### Widget URL
320
375
 
321
376
  By default, the widget loads from `https://js.cuoral.com/mobile.html` (production CDN).
@@ -406,6 +461,7 @@ async stopUserRecording() {
406
461
  ```
407
462
 
408
463
  **Use Cases:**
464
+
409
465
  - Allow users to record their issue before contacting support
410
466
  - Implement custom recording UI in your app
411
467
  - Record specific user flows programmatically
@@ -413,6 +469,81 @@ async stopUserRecording() {
413
469
 
414
470
  **Note:** Recording still requires user permission on iOS (microphone access). The video file is automatically processed and available for playback in the support widget.
415
471
 
472
+ ## Dual Session Capture
473
+
474
+ Cuoral provides **two complementary ways** to capture user sessions:
475
+
476
+ ### 📹 Native Video Screen Recording (Mobile)
477
+
478
+ **When:** User explicitly taps "Record" button in widget or via programmatic API
479
+
480
+ ✅ **What it captures:**
481
+
482
+ - Exact visual output (pixel-perfect replay)
483
+ - All UI elements including Shadow DOM components
484
+ - Animations, transitions, gestures
485
+ - Everything the user sees
486
+
487
+ 📹 **File Size:** ~11 MB per minute (1.5 Mbps bitrate)
488
+ 📤 **Upload:** Automatic to secure backend
489
+ 🎯 **Best for:** Visual bug reproduction, UI issues, specific user flows
490
+
491
+ ### 🎬 DOM Replay Recording (rrweb - Always On)
492
+
493
+ **When:** Automatically running in background during entire session
494
+
495
+ ✅ **What it captures:**
496
+
497
+ - DOM structure and mutations
498
+ - User interactions (clicks, scrolls, inputs)
499
+ - Network requests and console errors
500
+ - Page navigation and state changes
501
+ - Custom events (rage clicks, form submissions)
502
+
503
+ 💾 **Data Size:** Lightweight (~100-500 KB per session)
504
+ 📤 **Upload:** Batched every 10 seconds
505
+ 🎯 **Best for:** Understanding user journey, behavior analytics, session context
506
+
507
+ ### 🔄 How They Work Together
508
+
509
+ Both features run **simultaneously** to give you complete visibility:
510
+
511
+ 1. **DOM Replay (rrweb)** - Always recording in background, captures user journey and interactions
512
+ 2. **Video Recording** - User-initiated, captures visual output when they hit an issue
513
+
514
+ **Example Workflow:**
515
+
516
+ - User navigates app → rrweb captures all interactions
517
+ - User hits a bug → User taps "Record" → Native video captures visual issue
518
+ - Support team sees both: The journey (rrweb) + The exact visual problem (video)
519
+
520
+ **Privacy:** DOM replay masks password inputs. Other fields captured for support context. Native video captures exact screen content when user explicitly records.
521
+
522
+ ### 🎨 Shadow DOM Handling (Ionic Components)
523
+
524
+ **Problem:** Ionic uses Shadow DOM which rrweb cannot natively access.
525
+ **Solution:** The SDK automatically captures Shadow DOM content every 5 seconds.
526
+
527
+ **How it works:**
528
+
529
+ 1. SDK scans all elements with Shadow DOM (ion-button, ion-content, etc.)
530
+ 2. Serializes Shadow DOM innerHTML to `data-cuoral-shadow-*` attributes
531
+ 3. rrweb captures these attributes (it can see regular attributes)
532
+ 4. Your replay dashboard uses these attributes to reconstruct the UI
533
+
534
+ **To verify it's working:**
535
+ Open DevTools → Inspect any Ionic element → Should see `data-cuoral-shadow-html` attribute
536
+
537
+ **If pages still show black:**
538
+ See [SHADOW_DOM_CAPTURE_GUIDE.md](SHADOW_DOM_CAPTURE_GUIDE.md) for optimization tips:
539
+
540
+ - Use open Shadow DOM (not closed)
541
+ - Add data-content attributes to critical elements
542
+ - Use semantic CSS classes
543
+ - The guide includes code examples and troubleshooting
544
+
545
+ **Note:** Native video recording (user-initiated) always works perfectly - no Shadow DOM limitations.
546
+
416
547
  ### Manual Intelligence Tracking
417
548
 
418
549
  Track custom events beyond automatic tracking:
@@ -421,15 +552,14 @@ Track custom events beyond automatic tracking:
421
552
  // Track page/screen view
422
553
  this.cuoral.trackPageView('/checkout', {
423
554
  cart_items: 3,
424
- total_value: 99.99
555
+ total_value: 99.99,
425
556
  });
426
557
 
427
558
  // Track error manually
428
- this.cuoral.trackError(
429
- 'Payment failed',
430
- error.stack,
431
- { payment_method: 'credit_card', amount: 99.99 }
432
- );
559
+ this.cuoral.trackError('Payment failed', error.stack, {
560
+ payment_method: 'credit_card',
561
+ amount: 99.99,
562
+ });
433
563
  ```
434
564
 
435
565
  ## What Gets Handled Automatically
package/dist/cuoral.d.ts CHANGED
@@ -7,6 +7,7 @@ export interface CuoralOptions {
7
7
  widgetBaseUrl?: string;
8
8
  showFloatingButton?: boolean;
9
9
  useModal?: boolean;
10
+ primaryColor?: string;
10
11
  }
11
12
  /**
12
13
  * Main Cuoral class - simple API for users
@@ -1 +1 @@
1
- {"version":3,"file":"cuoral.d.ts","sourceRoot":"","sources":["../src/cuoral.ts"],"names":[],"mappings":"AAOA,MAAM,WAAW,aAAa;IAC5B,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAeD;;GAEG;AACH,qBAAa,MAAM;IACjB,OAAO,CAAC,MAAM,CAAe;IAC7B,OAAO,CAAC,QAAQ,CAAiB;IACjC,OAAO,CAAC,KAAK,CAAC,CAAc;IAC5B,OAAO,CAAC,YAAY,CAAC,CAAqB;IAC1C,OAAO,CAAC,OAAO,CAAgB;IAC/B,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,qBAAqB,CAAuC;IACpF,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,cAAc,CAAwB;gBAElD,OAAO,EAAE,aAAa;IA4DlC;;OAEG;IACU,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC;IAyBxC;;OAEG;YACW,sBAAsB;IA2CpC;;OAEG;YACW,yBAAyB;IAoCvC;;OAEG;IACI,aAAa,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,GAAG,GAAG,IAAI;IAM1D;;OAEG;IACI,UAAU,CAAC,OAAO,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,GAAG,GAAG,IAAI;IAM7E;;;;;OAKG;IACU,iBAAiB,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IA0C7E;;;OAGG;IACU,cAAc,IAAI,OAAO,CAAC,OAAO,CAAC;IAS/C;;;;OAIG;IACU,aAAa,IAAI,OAAO,CAAC;QAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,EAAE,OAAO,CAAA;KAAC,GAAG,IAAI,CAAC;IAiBxG;;OAEG;IACI,SAAS,IAAI,IAAI;IASxB;;OAEG;IACI,UAAU,IAAI,IAAI;IAMzB;;OAEG;IACI,WAAW,IAAI,OAAO;IAI7B;;OAEG;IACI,YAAY,IAAI,MAAM;IAuB7B;;OAEG;IACU,YAAY,IAAI,OAAO,CAAC,IAAI,CAAC;IA6C1C;;;OAGG;IACH,OAAO,CAAC,oBAAoB;IAsB5B;;OAEG;IACI,OAAO,IAAI,IAAI;IActB;;OAEG;IACH,OAAO,CAAC,oBAAoB;IAa5B;;OAEG;YACW,eAAe;IAkC7B;;OAEG;YACW,UAAU;IAmBxB;;OAEG;IACH,OAAO,CAAC,oBAAoB;CAyG7B"}
1
+ {"version":3,"file":"cuoral.d.ts","sourceRoot":"","sources":["../src/cuoral.ts"],"names":[],"mappings":"AAOA,MAAM,WAAW,aAAa;IAC5B,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAkBD;;GAEG;AACH,qBAAa,MAAM;IACjB,OAAO,CAAC,MAAM,CAAe;IAC7B,OAAO,CAAC,QAAQ,CAAiB;IACjC,OAAO,CAAC,KAAK,CAAC,CAAc;IAC5B,OAAO,CAAC,YAAY,CAAC,CAAqB;IAC1C,OAAO,CAAC,OAAO,CAAgB;IAC/B,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,qBAAqB,CAAuC;IACpF,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,cAAc,CAAwB;gBAElD,OAAO,EAAE,aAAa;IAyDlC;;OAEG;IACU,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC;IA0BxC;;OAEG;YACW,sBAAsB;IAoDpC;;OAEG;YACW,yBAAyB;IAoCvC;;OAEG;IACI,aAAa,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,GAAG,GAAG,IAAI;IAM1D;;OAEG;IACI,UAAU,CAAC,OAAO,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,GAAG,GAAG,IAAI;IAM7E;;;;;OAKG;IACU,iBAAiB,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IA0C7E;;;OAGG;IACU,cAAc,IAAI,OAAO,CAAC,OAAO,CAAC;IAS/C;;;;OAIG;IACU,aAAa,IAAI,OAAO,CAAC;QAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,EAAE,OAAO,CAAA;KAAC,GAAG,IAAI,CAAC;IAiBxG;;OAEG;IACI,SAAS,IAAI,IAAI;IASxB;;OAEG;IACI,UAAU,IAAI,IAAI;IAMzB;;OAEG;IACI,WAAW,IAAI,OAAO;IAI7B;;OAEG;IACI,YAAY,IAAI,MAAM;IAuB7B;;OAEG;IACU,YAAY,IAAI,OAAO,CAAC,IAAI,CAAC;IA6C1C;;;OAGG;IACH,OAAO,CAAC,oBAAoB;IAsB5B;;OAEG;IACI,OAAO,IAAI,IAAI;IActB;;OAEG;IACH,OAAO,CAAC,oBAAoB;IAa5B;;OAEG;YACW,eAAe;IAkC7B;;OAEG;YACW,UAAU;IAmBxB;;OAEG;IACH,OAAO,CAAC,oBAAoB;CAyG7B"}
package/dist/cuoral.js CHANGED
@@ -39,10 +39,7 @@ export class Cuoral {
39
39
  if (options.lastName)
40
40
  params.set('last_name', options.lastName);
41
41
  const widgetUrl = `${baseUrl}?${params.toString()}`;
42
- // Initialize modal if enabled
43
- if (this.options.useModal) {
44
- this.modal = new CuoralModal(widgetUrl, this.options.showFloatingButton);
45
- }
42
+ // Note: Modal will be created in initialize() after fetching primary color from backend
46
43
  // Initialize bridge and recorder
47
44
  this.bridge = new CuoralBridge({
48
45
  widgetUrl,
@@ -63,6 +60,7 @@ export class Cuoral {
63
60
  */
64
61
  async initialize() {
65
62
  // Fetch session configuration and initialize intelligence if enabled by backend
63
+ // This also fetches the organization's primary color
66
64
  await this.initializeIntelligence();
67
65
  console.log('[Cuoral] Initialize - Session ID:', localStorage.getItem('__x_loadID'));
68
66
  // Setup localStorage listener to detect when widget changes session
@@ -71,7 +69,7 @@ export class Cuoral {
71
69
  // Recreate modal if it was destroyed (e.g., after clearSession)
72
70
  if (this.options.useModal && !this.modal) {
73
71
  const widgetUrl = this.getWidgetUrl();
74
- this.modal = new CuoralModal(widgetUrl, this.options.showFloatingButton);
72
+ this.modal = new CuoralModal(widgetUrl, this.options.showFloatingButton, this.options.primaryColor);
75
73
  }
76
74
  // Update modal URL with session ID
77
75
  if (this.modal) {
@@ -96,6 +94,15 @@ export class Cuoral {
96
94
  }
97
95
  // Fetch session configuration
98
96
  const config = await this.fetchSessionConfiguration(sessionId);
97
+ // Extract organization's primary color from config if available
98
+ if (!this.options.primaryColor && config?.color) {
99
+ this.options.primaryColor = config.color;
100
+ console.log('[Cuoral] Using organization primary color:', this.options.primaryColor);
101
+ }
102
+ else if (!this.options.primaryColor) {
103
+ this.options.primaryColor = '#007AFF';
104
+ console.log('[Cuoral] Using default primary color');
105
+ }
99
106
  // If session was invalid/expired, try to initiate a new one
100
107
  if (!config && !localStorage.getItem('__x_loadID')) {
101
108
  sessionId = await this.initiateSession();
package/dist/index.esm.js CHANGED
@@ -361,10 +361,11 @@ class CuoralBridge {
361
361
  * Handles full-screen modal display with floating button
362
362
  */
363
363
  class CuoralModal {
364
- constructor(widgetUrl, showFloatingButton = true) {
364
+ constructor(widgetUrl, showFloatingButton = true, primaryColor = '#007AFF') {
365
365
  this.isOpen = false;
366
366
  this.widgetUrl = widgetUrl;
367
367
  this.showFloatingButton = showFloatingButton;
368
+ this.primaryColor = primaryColor;
368
369
  }
369
370
  /**
370
371
  * Update the widget URL (e.g., to include new session_id)
@@ -432,12 +433,12 @@ class CuoralModal {
432
433
  width: '56px',
433
434
  height: '56px',
434
435
  borderRadius: '50%',
435
- backgroundColor: '#007AFF',
436
+ backgroundColor: this.primaryColor,
436
437
  display: 'flex',
437
438
  alignItems: 'center',
438
439
  justifyContent: 'center',
439
440
  cursor: 'pointer',
440
- boxShadow: '0 4px 12px rgba(0, 122, 255, 0.4)',
441
+ boxShadow: `0 4px 12px ${this.primaryColor}66`, // 40% opacity
441
442
  zIndex: '999999',
442
443
  transition: 'transform 0.2s, box-shadow 0.2s'
443
444
  });
@@ -445,13 +446,13 @@ class CuoralModal {
445
446
  this.floatingButton.addEventListener('mouseenter', () => {
446
447
  if (this.floatingButton) {
447
448
  this.floatingButton.style.transform = 'scale(1.1)';
448
- this.floatingButton.style.boxShadow = '0 6px 16px rgba(0, 122, 255, 0.5)';
449
+ this.floatingButton.style.boxShadow = `0 6px 16px ${this.primaryColor}80`; // 50% opacity
449
450
  }
450
451
  });
451
452
  this.floatingButton.addEventListener('mouseleave', () => {
452
453
  if (this.floatingButton) {
453
454
  this.floatingButton.style.transform = 'scale(1)';
454
- this.floatingButton.style.boxShadow = '0 4px 12px rgba(0, 122, 255, 0.4)';
455
+ this.floatingButton.style.boxShadow = `0 4px 12px ${this.primaryColor}66`; // 40% opacity
455
456
  }
456
457
  });
457
458
  // Click to open modal
@@ -650,11 +651,13 @@ class CuoralIntelligence {
650
651
  // Session replay state
651
652
  this.rrwebStopFn = null;
652
653
  this.rrwebEvents = [];
654
+ this.rrwebEmit = null; // Store emit function for custom events
653
655
  this.customEvents = [];
654
656
  this.sessionReplayTimer = null;
655
657
  this.clickTimestamps = new Map();
656
658
  this.rageClickThreshold = 5; // 5 rapid clicks
657
659
  this.rageClickWindowMs = 2000; // Within 2 seconds
660
+ this.shadowDOMNodeMap = new Map(); // Map elements to unique IDs
658
661
  this.sessionId = sessionId;
659
662
  }
660
663
  /**
@@ -1164,27 +1167,65 @@ class CuoralIntelligence {
1164
1167
  }
1165
1168
  /**
1166
1169
  * Setup session replay with rrweb
1170
+ * Captures everything for maximum replay fidelity
1167
1171
  */
1168
1172
  setupSessionReplay() {
1169
1173
  if (!this.sessionId) {
1170
1174
  console.warn('[Cuoral Intelligence] Session replay requires a session ID');
1171
1175
  return;
1172
1176
  }
1173
- // Start rrweb recording
1177
+ // Start rrweb recording with aggressive capture settings
1174
1178
  this.rrwebStopFn = rrweb.record({
1175
1179
  emit: (event) => {
1176
1180
  this.rrwebEvents.push(event);
1181
+ this.rrwebEmit = this.rrwebEmit || ((e) => this.rrwebEvents.push(e)); // Store emit reference
1177
1182
  },
1178
- checkoutEveryNms: 60000, // Full snapshot every minute
1183
+ // Take full snapshots more frequently to catch dynamic content
1184
+ checkoutEveryNms: 30000, // Every 30 seconds (was 60s)
1185
+ // Capture everything - minimize sampling throttling
1179
1186
  sampling: {
1180
- scroll: 150, // Throttle scroll events
1181
- media: 800,
1182
- input: 'last', // Only record final input value (privacy)
1187
+ scroll: 50, // Capture scroll very frequently (was 150)
1188
+ media: 200, // Capture media interactions frequently (was 800)
1189
+ input: 'last', // Only record final input value (privacy for passwords)
1190
+ mousemove: true, // Capture all mouse movements
1191
+ mouseInteraction: true, // Capture all mouse interactions
1192
+ },
1193
+ // Privacy: Only mask sensitive inputs (passwords, credit cards)
1194
+ // Everything else is captured for maximum replay fidelity
1195
+ maskAllInputs: false, // Don't mask all inputs
1196
+ maskInputOptions: {
1197
+ password: true, // Mask password fields
1198
+ email: false, // Capture emails
1199
+ text: false, // Capture text inputs
1200
+ textarea: false, // Capture textareas
1201
+ select: false, // Capture select dropdowns
1202
+ // Mask credit card patterns
1203
+ },
1204
+ // Capture all content types
1205
+ recordCanvas: true, // Capture canvas elements (charts, games, etc.)
1206
+ // Inline everything for perfect replay
1207
+ inlineStylesheet: true, // Inline all external stylesheets
1208
+ inlineImages: true, // Inline images as base64 (larger but more reliable)
1209
+ collectFonts: true, // Capture custom fonts
1210
+ // Block/ignore classes - customers can add these to sensitive elements
1211
+ blockClass: 'cuoral-block', // Add to elements that should be blocked (replaced with placeholder)
1212
+ ignoreClass: 'cuoral-ignore', // Add to elements that should be ignored (not captured)
1213
+ // Capture mutations aggressively
1214
+ slimDOMOptions: {
1215
+ // Don't slim anything - capture full fidelity
1216
+ script: false, // Keep scripts
1217
+ comment: false, // Keep comments
1218
+ headFavicon: false, // Keep favicons
1219
+ headWhitespace: false, // Keep whitespace
1220
+ headMetaSocial: false, // Keep meta tags
1221
+ headMetaRobots: false, // Keep robots meta
1222
+ headMetaHttpEquiv: false, // Keep http-equiv
1223
+ headMetaAuthorship: false, // Keep authorship
1224
+ headMetaDescKeywords: false, // Keep description/keywords
1183
1225
  },
1184
- maskAllInputs: true, // Mask sensitive inputs (privacy)
1185
- blockClass: 'cuoral-block',
1186
- ignoreClass: 'cuoral-ignore',
1187
1226
  });
1227
+ // Setup Shadow DOM observer to capture Ionic components
1228
+ this.setupShadowDOMObserver();
1188
1229
  // Setup custom event tracking
1189
1230
  this.setupClickTracking();
1190
1231
  this.setupScrollTracking();
@@ -1336,6 +1377,159 @@ class CuoralIntelligence {
1336
1377
  });
1337
1378
  });
1338
1379
  }
1380
+ /**
1381
+ * Setup Shadow DOM observer to capture Ionic component content
1382
+ * Emits custom rrweb events with Shadow DOM snapshots for viewer reconstruction
1383
+ */
1384
+ setupShadowDOMObserver() {
1385
+ let shadowNodeIdCounter = 1;
1386
+ let captureTimeout = null;
1387
+ const captureShadowDOM = (immediate = false) => {
1388
+ // Debounce rapid calls (e.g., during scroll)
1389
+ if (!immediate && captureTimeout) {
1390
+ clearTimeout(captureTimeout);
1391
+ }
1392
+ const doCapture = () => {
1393
+ if (!this.rrwebEmit)
1394
+ return;
1395
+ const shadowDOMData = [];
1396
+ const elementsWithShadowDOM = document.querySelectorAll('*');
1397
+ elementsWithShadowDOM.forEach((element) => {
1398
+ if (element.shadowRoot) {
1399
+ try {
1400
+ // Assign unique ID to element if it doesn't have one
1401
+ if (!this.shadowDOMNodeMap.has(element)) {
1402
+ this.shadowDOMNodeMap.set(element, shadowNodeIdCounter++);
1403
+ }
1404
+ const nodeId = this.shadowDOMNodeMap.get(element);
1405
+ const tagName = element.tagName.toLowerCase();
1406
+ // Capture Shadow DOM HTML
1407
+ const shadowHTML = element.shadowRoot.innerHTML;
1408
+ // Capture ALL styles from Shadow DOM
1409
+ const styleSheets = [];
1410
+ // 1. Capture inline <style> tags
1411
+ const shadowStyles = element.shadowRoot.querySelectorAll('style');
1412
+ shadowStyles.forEach((styleEl) => {
1413
+ if (styleEl.textContent) {
1414
+ styleSheets.push(styleEl.textContent);
1415
+ }
1416
+ });
1417
+ // 2. Capture adoptedStyleSheets (modern CSS API)
1418
+ if (element.shadowRoot.adoptedStyleSheets) {
1419
+ try {
1420
+ element.shadowRoot.adoptedStyleSheets.forEach((sheet) => {
1421
+ try {
1422
+ const rules = Array.from(sheet.cssRules).map(rule => rule.cssText).join('\n');
1423
+ if (rules)
1424
+ styleSheets.push(rules);
1425
+ }
1426
+ catch (e) {
1427
+ // CORS blocked stylesheet
1428
+ }
1429
+ });
1430
+ }
1431
+ catch (e) {
1432
+ // Browser doesn't support adoptedStyleSheets
1433
+ }
1434
+ }
1435
+ // 3. Capture external <link> stylesheets in Shadow DOM
1436
+ const shadowLinks = element.shadowRoot.querySelectorAll('link[rel="stylesheet"]');
1437
+ shadowLinks.forEach((linkEl) => {
1438
+ const href = linkEl.href;
1439
+ if (href) {
1440
+ // Store link href so dashboard can also load it
1441
+ styleSheets.push(`/* @import url("${href}"); */`);
1442
+ }
1443
+ });
1444
+ // 4. Capture CSS custom properties from host element
1445
+ // Ionic components use CSS variables extensively (--ion-color-*, etc.)
1446
+ const computedStyle = window.getComputedStyle(element);
1447
+ const cssVariables = [];
1448
+ // Get all CSS custom properties from host
1449
+ for (let i = 0; i < computedStyle.length; i++) {
1450
+ const propertyName = computedStyle[i];
1451
+ if (propertyName.startsWith('--')) {
1452
+ const propertyValue = computedStyle.getPropertyValue(propertyName).trim();
1453
+ if (propertyValue) {
1454
+ cssVariables.push(`${propertyName}: ${propertyValue};`);
1455
+ }
1456
+ }
1457
+ }
1458
+ // Add CSS variables as a :host rule
1459
+ if (cssVariables.length > 0) {
1460
+ styleSheets.push(`:host { ${cssVariables.join(' ')} }`);
1461
+ }
1462
+ // Get element's CSS selector for reconstruction
1463
+ const selector = this.getElementSelector(element);
1464
+ // Capture element attributes (Ionic uses color="primary", mode="ios", etc.)
1465
+ const attributes = {};
1466
+ for (let i = 0; i < element.attributes.length; i++) {
1467
+ const attr = element.attributes[i];
1468
+ // Skip internal attributes
1469
+ if (!attr.name.startsWith('data-cuoral') && !attr.name.startsWith('ng-')) {
1470
+ attributes[attr.name] = attr.value;
1471
+ }
1472
+ }
1473
+ // Create Shadow DOM snapshot
1474
+ shadowDOMData.push({
1475
+ nodeId,
1476
+ tagName,
1477
+ selector,
1478
+ attributes, // NEW: Include element attributes
1479
+ shadowHTML,
1480
+ styleSheets,
1481
+ timestamp: Date.now(),
1482
+ });
1483
+ // Also set data attribute as fallback
1484
+ element.setAttribute('data-cuoral-shadow-id', String(nodeId));
1485
+ }
1486
+ catch (error) {
1487
+ // Shadow DOM might be closed
1488
+ console.warn('[Cuoral Intelligence] Failed to capture Shadow DOM:', error);
1489
+ }
1490
+ }
1491
+ });
1492
+ // Emit custom event with Shadow DOM data
1493
+ if (shadowDOMData.length > 0 && this.rrwebEmit) {
1494
+ this.rrwebEmit({
1495
+ type: 5, // CustomEvent type in rrweb
1496
+ data: {
1497
+ tag: 'cuoral-shadow-dom',
1498
+ payload: {
1499
+ shadows: shadowDOMData,
1500
+ url: window.location.href,
1501
+ },
1502
+ },
1503
+ timestamp: Date.now(),
1504
+ });
1505
+ console.log(`[Cuoral Intelligence] Captured ${shadowDOMData.length} Shadow DOM elements`);
1506
+ }
1507
+ };
1508
+ if (immediate) {
1509
+ doCapture();
1510
+ }
1511
+ else {
1512
+ // Debounce: wait 500ms after last call
1513
+ captureTimeout = setTimeout(doCapture, 500);
1514
+ }
1515
+ };
1516
+ // Run immediately on init
1517
+ captureShadowDOM(true);
1518
+ // Observe DOM mutations to catch dynamically added Shadow DOM elements
1519
+ const observer = new MutationObserver(() => {
1520
+ captureShadowDOM(false); // Debounced
1521
+ });
1522
+ observer.observe(document.body, {
1523
+ childList: true,
1524
+ subtree: true,
1525
+ });
1526
+ // Capture on scroll events (debounced)
1527
+ window.addEventListener('scroll', () => captureShadowDOM(false), { passive: true });
1528
+ // Also listen for Ionic scroll events
1529
+ document.addEventListener('ionScroll', () => captureShadowDOM(false), { passive: true });
1530
+ // Periodic capture as fallback (every 5 seconds now, since we have scroll capture)
1531
+ setInterval(() => captureShadowDOM(true), 5000);
1532
+ }
1339
1533
  /**
1340
1534
  * Track custom business events (flows, features, etc.)
1341
1535
  */
@@ -1464,10 +1658,7 @@ class Cuoral {
1464
1658
  if (options.lastName)
1465
1659
  params.set('last_name', options.lastName);
1466
1660
  const widgetUrl = `${baseUrl}?${params.toString()}`;
1467
- // Initialize modal if enabled
1468
- if (this.options.useModal) {
1469
- this.modal = new CuoralModal(widgetUrl, this.options.showFloatingButton);
1470
- }
1661
+ // Note: Modal will be created in initialize() after fetching primary color from backend
1471
1662
  // Initialize bridge and recorder
1472
1663
  this.bridge = new CuoralBridge({
1473
1664
  widgetUrl,
@@ -1488,6 +1679,7 @@ class Cuoral {
1488
1679
  */
1489
1680
  async initialize() {
1490
1681
  // Fetch session configuration and initialize intelligence if enabled by backend
1682
+ // This also fetches the organization's primary color
1491
1683
  await this.initializeIntelligence();
1492
1684
  console.log('[Cuoral] Initialize - Session ID:', localStorage.getItem('__x_loadID'));
1493
1685
  // Setup localStorage listener to detect when widget changes session
@@ -1496,7 +1688,7 @@ class Cuoral {
1496
1688
  // Recreate modal if it was destroyed (e.g., after clearSession)
1497
1689
  if (this.options.useModal && !this.modal) {
1498
1690
  const widgetUrl = this.getWidgetUrl();
1499
- this.modal = new CuoralModal(widgetUrl, this.options.showFloatingButton);
1691
+ this.modal = new CuoralModal(widgetUrl, this.options.showFloatingButton, this.options.primaryColor);
1500
1692
  }
1501
1693
  // Update modal URL with session ID
1502
1694
  if (this.modal) {
@@ -1521,6 +1713,15 @@ class Cuoral {
1521
1713
  }
1522
1714
  // Fetch session configuration
1523
1715
  const config = await this.fetchSessionConfiguration(sessionId);
1716
+ // Extract organization's primary color from config if available
1717
+ if (!this.options.primaryColor && config?.color) {
1718
+ this.options.primaryColor = config.color;
1719
+ console.log('[Cuoral] Using organization primary color:', this.options.primaryColor);
1720
+ }
1721
+ else if (!this.options.primaryColor) {
1722
+ this.options.primaryColor = '#007AFF';
1723
+ console.log('[Cuoral] Using default primary color');
1724
+ }
1524
1725
  // If session was invalid/expired, try to initiate a new one
1525
1726
  if (!config && !localStorage.getItem('__x_loadID')) {
1526
1727
  sessionId = await this.initiateSession();
@@ -1 +1 @@
1
- {"version":3,"file":"index.esm.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
1
+ {"version":3,"file":"index.esm.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}