cuoral-ionic 0.0.5 → 0.0.7

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.
Files changed (59) hide show
  1. package/README.md +72 -1
  2. package/android/.gradle/8.9/checksums/checksums.lock +0 -0
  3. package/android/.gradle/8.9/checksums/sha1-checksums.bin +0 -0
  4. package/android/build/.transforms/bb54161301273cf9b5b94a21c0fb3f23/transformed/classes/classes_dex/classes.dex +0 -0
  5. package/android/build/.transforms/f1aabffcd8b03aa664e77a79b3e1de5d/transformed/debug/debug_dex/com/cuoral/ionic/CuoralPlugin$1.dex +0 -0
  6. package/android/build/.transforms/f1aabffcd8b03aa664e77a79b3e1de5d/transformed/debug/debug_dex/com/cuoral/ionic/CuoralPlugin$2.dex +0 -0
  7. package/android/build/.transforms/f1aabffcd8b03aa664e77a79b3e1de5d/transformed/debug/debug_dex/com/cuoral/ionic/CuoralPlugin$3.dex +0 -0
  8. package/android/build/.transforms/f1aabffcd8b03aa664e77a79b3e1de5d/transformed/debug/debug_dex/com/cuoral/ionic/CuoralPlugin$4.dex +0 -0
  9. package/android/build/.transforms/f1aabffcd8b03aa664e77a79b3e1de5d/transformed/debug/debug_dex/com/cuoral/ionic/CuoralPlugin$5.dex +0 -0
  10. package/android/build/.transforms/f1aabffcd8b03aa664e77a79b3e1de5d/transformed/debug/debug_dex/com/cuoral/ionic/CuoralPlugin$6.dex +0 -0
  11. package/android/build/.transforms/f1aabffcd8b03aa664e77a79b3e1de5d/transformed/debug/debug_dex/com/cuoral/ionic/CuoralPlugin$InitiateCallback.dex +0 -0
  12. package/android/build/.transforms/f1aabffcd8b03aa664e77a79b3e1de5d/transformed/debug/debug_dex/com/cuoral/ionic/CuoralPlugin$UploadCallback.dex +0 -0
  13. package/android/build/.transforms/f1aabffcd8b03aa664e77a79b3e1de5d/transformed/debug/debug_dex/com/cuoral/ionic/CuoralPlugin.dex +0 -0
  14. package/android/build/.transforms/f1aabffcd8b03aa664e77a79b3e1de5d/transformed/debug/desugar_graph.bin +0 -0
  15. package/android/build/intermediates/compile_library_classes_jar/debug/classes.jar +0 -0
  16. package/android/build/intermediates/javac/debug/classes/com/cuoral/ionic/CuoralPlugin$1.class +0 -0
  17. package/android/build/intermediates/javac/debug/classes/com/cuoral/ionic/CuoralPlugin$2.class +0 -0
  18. package/android/build/intermediates/javac/debug/classes/com/cuoral/ionic/CuoralPlugin$3.class +0 -0
  19. package/android/build/intermediates/javac/debug/classes/com/cuoral/ionic/CuoralPlugin$4.class +0 -0
  20. package/android/build/intermediates/javac/debug/classes/com/cuoral/ionic/CuoralPlugin$5.class +0 -0
  21. package/android/build/intermediates/javac/debug/classes/com/cuoral/ionic/CuoralPlugin$6.class +0 -0
  22. package/android/build/intermediates/javac/debug/classes/com/cuoral/ionic/CuoralPlugin$InitiateCallback.class +0 -0
  23. package/android/build/intermediates/javac/debug/classes/com/cuoral/ionic/CuoralPlugin$UploadCallback.class +0 -0
  24. package/android/build/intermediates/javac/debug/classes/com/cuoral/ionic/CuoralPlugin.class +0 -0
  25. package/android/build/intermediates/runtime_library_classes_dir/debug/com/cuoral/ionic/CuoralPlugin$1.class +0 -0
  26. package/android/build/intermediates/runtime_library_classes_dir/debug/com/cuoral/ionic/CuoralPlugin$2.class +0 -0
  27. package/android/build/intermediates/runtime_library_classes_dir/debug/com/cuoral/ionic/CuoralPlugin$3.class +0 -0
  28. package/android/build/intermediates/runtime_library_classes_dir/debug/com/cuoral/ionic/CuoralPlugin$4.class +0 -0
  29. package/android/build/intermediates/runtime_library_classes_dir/debug/com/cuoral/ionic/CuoralPlugin$5.class +0 -0
  30. package/android/build/intermediates/runtime_library_classes_dir/debug/com/cuoral/ionic/CuoralPlugin$6.class +0 -0
  31. package/android/build/intermediates/runtime_library_classes_dir/debug/com/cuoral/ionic/CuoralPlugin$InitiateCallback.class +0 -0
  32. package/android/build/intermediates/runtime_library_classes_dir/debug/com/cuoral/ionic/CuoralPlugin$UploadCallback.class +0 -0
  33. package/android/build/intermediates/runtime_library_classes_dir/debug/com/cuoral/ionic/CuoralPlugin.class +0 -0
  34. package/android/build/intermediates/runtime_library_classes_jar/debug/classes.jar +0 -0
  35. package/android/build/tmp/compileDebugJavaWithJavac/previous-compilation-data.bin +0 -0
  36. package/android/build.gradle +1 -0
  37. package/android/src/main/java/com/cuoral/ionic/CuoralPlugin.java +205 -5
  38. package/dist/cuoral.d.ts +27 -0
  39. package/dist/cuoral.d.ts.map +1 -1
  40. package/dist/cuoral.js +137 -7
  41. package/dist/index.esm.js +471 -17
  42. package/dist/index.esm.js.map +1 -1
  43. package/dist/index.js +490 -17
  44. package/dist/index.js.map +1 -1
  45. package/dist/intelligence.d.ts +51 -0
  46. package/dist/intelligence.d.ts.map +1 -1
  47. package/dist/intelligence.js +307 -0
  48. package/dist/plugin.d.ts +15 -2
  49. package/dist/plugin.d.ts.map +1 -1
  50. package/dist/plugin.js +23 -10
  51. package/dist/types.d.ts +4 -0
  52. package/dist/types.d.ts.map +1 -1
  53. package/dist/types.js +4 -0
  54. package/ios/Plugin/CuoralPlugin.swift +249 -13
  55. package/package.json +4 -2
  56. package/src/cuoral.ts +151 -8
  57. package/src/intelligence.ts +375 -0
  58. package/src/plugin.ts +39 -11
  59. package/src/types.ts +4 -0
@@ -110,10 +110,19 @@ public class CuoralPlugin: CAPPlugin {
110
110
  do {
111
111
  assetWriter = try AVAssetWriter(url: outputURL, fileType: .mp4)
112
112
 
113
+ // Reduced quality settings to prevent huge file sizes
114
+ // 2.5 Mbps bitrate = ~18MB per minute (down from ~600MB per minute)
115
+ let compressionProperties: [String: Any] = [
116
+ AVVideoAverageBitRateKey: 2_500_000, // 2.5 Mbps
117
+ AVVideoMaxKeyFrameIntervalKey: 30,
118
+ AVVideoProfileLevelKey: AVVideoProfileLevelH264BaselineAutoLevel
119
+ ]
120
+
113
121
  let videoSettings: [String: Any] = [
114
122
  AVVideoCodecKey: AVVideoCodecType.h264,
115
123
  AVVideoWidthKey: UIScreen.main.bounds.width * UIScreen.main.scale,
116
- AVVideoHeightKey: UIScreen.main.bounds.height * UIScreen.main.scale
124
+ AVVideoHeightKey: UIScreen.main.bounds.height * UIScreen.main.scale,
125
+ AVVideoCompressionPropertiesKey: compressionProperties
117
126
  ]
118
127
 
119
128
  videoInput = AVAssetWriterInput(mediaType: .video, outputSettings: videoSettings)
@@ -184,6 +193,12 @@ public class CuoralPlugin: CAPPlugin {
184
193
 
185
194
  isRecording = false
186
195
 
196
+ // Get upload parameters
197
+ let shouldUpload = call.getBool("autoUpload") ?? true
198
+ let sessionId = call.getString("sessionId") ?? ""
199
+ let publicKey = call.getString("publicKey") ?? ""
200
+ let customerId = call.getString("customerId") ?? ""
201
+
187
202
  recorder.stopCapture { [weak self] error in
188
203
  guard let self = self else { return }
189
204
 
@@ -208,20 +223,241 @@ public class CuoralPlugin: CAPPlugin {
208
223
  let duration = self.recordingStartTime.map { Date().timeIntervalSince($0) } ?? 0
209
224
  let filePath = self.videoOutputURL?.path ?? ""
210
225
 
211
- call.resolve([
212
- "success": true,
213
- "filePath": filePath,
214
- "duration": Int(duration)
215
- ])
216
-
217
- // Clean up
218
- self.assetWriter = nil
219
- self.videoInput = nil
220
- self.videoOutputURL = nil
221
- self.recordingStartTime = nil
222
- self.firstFrameTime = nil
226
+ // If autoUpload is enabled, upload the video
227
+ if shouldUpload && !sessionId.isEmpty && !publicKey.isEmpty {
228
+ self.uploadRecording(
229
+ filePath: filePath,
230
+ sessionId: sessionId,
231
+ publicKey: publicKey,
232
+ customerId: customerId,
233
+ duration: Int(duration)
234
+ ) { success, error in
235
+ if success {
236
+ call.resolve([
237
+ "success": true,
238
+ "filePath": filePath,
239
+ "duration": Int(duration),
240
+ "uploaded": true
241
+ ])
242
+ } else {
243
+ call.resolve([
244
+ "success": true,
245
+ "filePath": filePath,
246
+ "duration": Int(duration),
247
+ "uploaded": false,
248
+ "uploadError": error ?? "Unknown error"
249
+ ])
250
+ }
251
+
252
+ // Clean up
253
+ self.assetWriter = nil
254
+ self.videoInput = nil
255
+ self.videoOutputURL = nil
256
+ self.recordingStartTime = nil
257
+ self.firstFrameTime = nil
258
+ }
259
+ } else {
260
+ // No upload, just return file path
261
+ call.resolve([
262
+ "success": true,
263
+ "filePath": filePath,
264
+ "duration": Int(duration),
265
+ "uploaded": false
266
+ ])
267
+
268
+ // Clean up
269
+ self.assetWriter = nil
270
+ self.videoInput = nil
271
+ self.videoOutputURL = nil
272
+ self.recordingStartTime = nil
273
+ self.firstFrameTime = nil
274
+ }
275
+ }
276
+ }
277
+ }
278
+
279
+ /**
280
+ * Upload recording to Cuoral backend
281
+ */
282
+ private func uploadRecording(
283
+ filePath: String,
284
+ sessionId: String,
285
+ publicKey: String,
286
+ customerId: String,
287
+ duration: Int,
288
+ completion: @escaping (Bool, String?) -> Void
289
+ ) {
290
+ guard let fileURL = URL(string: "file://\(filePath)"),
291
+ FileManager.default.fileExists(atPath: filePath) else {
292
+ completion(false, "File not found")
293
+ return
294
+ }
295
+
296
+ // Read video data
297
+ guard let videoData = try? Data(contentsOf: fileURL) else {
298
+ completion(false, "Failed to read video file")
299
+ return
300
+ }
301
+
302
+ // Step 1: Initiate recording to get record_id
303
+ initiateRecording(sessionId: sessionId, customerId: customerId, publicKey: publicKey) { recordId, error in
304
+ if let error = error {
305
+ completion(false, "Failed to initiate recording: \(error)")
306
+ return
223
307
  }
308
+
309
+ guard let recordId = recordId else {
310
+ completion(false, "No record ID returned")
311
+ return
312
+ }
313
+
314
+ // Step 2: Upload video with the record_id
315
+ self.uploadVideo(
316
+ videoData: videoData,
317
+ recordId: recordId,
318
+ publicKey: publicKey,
319
+ customerId: customerId,
320
+ duration: duration,
321
+ completion: completion
322
+ )
323
+ }
324
+ }
325
+
326
+ /**
327
+ * Step 1: Initiate recording session
328
+ */
329
+ private func initiateRecording(
330
+ sessionId: String,
331
+ customerId: String,
332
+ publicKey: String,
333
+ completion: @escaping (String?, String?) -> Void
334
+ ) {
335
+ let url = URL(string: "https://api.cuoral.com/customer-intelligence/initiate/record")!
336
+ var request = URLRequest(url: url)
337
+ request.httpMethod = "POST"
338
+ request.setValue("application/json", forHTTPHeaderField: "Content-Type")
339
+ request.setValue("*/*", forHTTPHeaderField: "Accept")
340
+ request.setValue(publicKey, forHTTPHeaderField: "x-org-id")
341
+
342
+ // API needs both session_id and customer_id (matching widget.js)
343
+ let body: [String: String] = [
344
+ "session_id": sessionId,
345
+ "customer_id": customerId
346
+ ]
347
+
348
+ guard let jsonData = try? JSONSerialization.data(withJSONObject: body) else {
349
+ completion(nil, "Failed to serialize JSON")
350
+ return
224
351
  }
352
+
353
+ request.httpBody = jsonData
354
+ request.timeoutInterval = 30.0
355
+
356
+ URLSession.shared.dataTask(with: request) { data, response, error in
357
+ if let error = error {
358
+ completion(nil, error.localizedDescription)
359
+ return
360
+ }
361
+
362
+ guard let httpResponse = response as? HTTPURLResponse else {
363
+ completion(nil, "Invalid response")
364
+ return
365
+ }
366
+
367
+ // API returns JSON: { status: 'success', record_id: '...' }
368
+ guard httpResponse.statusCode == 200, let data = data else {
369
+ completion(nil, "Failed to initiate recording (status \(httpResponse.statusCode))")
370
+ return
371
+ }
372
+
373
+ // Parse JSON response
374
+ guard let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
375
+ let status = json["status"] as? String,
376
+ status == "success",
377
+ let recordId = json["record_id"] as? String else {
378
+ completion(nil, "Invalid response format")
379
+ return
380
+ }
381
+
382
+ completion(recordId, nil)
383
+ }.resume()
384
+ }
385
+
386
+ /**
387
+ * Step 2: Upload video to the initiated recording
388
+ */
389
+ private func uploadVideo(
390
+ videoData: Data,
391
+ recordId: String,
392
+ publicKey: String,
393
+ customerId: String,
394
+ duration: Int,
395
+ completion: @escaping (Bool, String?) -> Void
396
+ ) {
397
+ // Create multipart form data
398
+ let boundary = "Boundary-\(UUID().uuidString)"
399
+ var body = Data()
400
+
401
+ // Add form fields (API spec: only record_id is required)
402
+ let parameters: [String: String] = [
403
+ "record_id": recordId,
404
+ "console_error": "[]",
405
+ "api_response_log": "[]",
406
+ "page_view": "[]"
407
+ ]
408
+
409
+ for (key, value) in parameters {
410
+ body.append("--\(boundary)\r\n".data(using: .utf8)!)
411
+ body.append("Content-Disposition: form-data; name=\"\(key)\"\r\n\r\n".data(using: .utf8)!)
412
+ body.append("\(value)\r\n".data(using: .utf8)!)
413
+ }
414
+
415
+ // Add video file
416
+ body.append("--\(boundary)\r\n".data(using: .utf8)!)
417
+ body.append("Content-Disposition: form-data; name=\"record_media\"; filename=\"recording.mp4\"\r\n".data(using: .utf8)!)
418
+ body.append("Content-Type: video/mp4\r\n\r\n".data(using: .utf8)!)
419
+ body.append(videoData)
420
+ body.append("\r\n".data(using: .utf8)!)
421
+ body.append("--\(boundary)--\r\n".data(using: .utf8)!)
422
+
423
+ // Create request
424
+ let url = URL(string: "https://api.cuoral.com/customer-intelligence/update/record")!
425
+ var request = URLRequest(url: url)
426
+ request.httpMethod = "POST"
427
+ request.setValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type")
428
+ request.setValue("*/*", forHTTPHeaderField: "Accept")
429
+ // Note: widget.js doesn't send x-org-id for update endpoint
430
+ request.httpBody = body
431
+ request.timeoutInterval = 60.0 // 60 seconds for upload
432
+
433
+ // Send request
434
+ URLSession.shared.dataTask(with: request) { data, response, error in
435
+ if let error = error {
436
+ completion(false, error.localizedDescription)
437
+ return
438
+ }
439
+
440
+ guard let httpResponse = response as? HTTPURLResponse else {
441
+ completion(false, "Invalid response")
442
+ return
443
+ }
444
+
445
+ // API returns JSON: { status: 'success' }
446
+ guard httpResponse.statusCode == 200, let data = data else {
447
+ completion(false, "Upload failed with status \(httpResponse.statusCode)")
448
+ return
449
+ }
450
+
451
+ // Parse JSON response
452
+ guard let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
453
+ let status = json["status"] as? String,
454
+ status == "success" else {
455
+ completion(false, "Upload failed")
456
+ return
457
+ }
458
+
459
+ completion(true, nil)
460
+ }.resume()
225
461
  }
226
462
 
227
463
  @objc func getRecordingState(_ call: CAPPluginCall) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cuoral-ionic",
3
- "version": "0.0.5",
3
+ "version": "0.0.7",
4
4
  "description": "Cuoral Ionic Framework Library - Proactive customer success platform with support ticketing, customer intelligence, screen recording, and engagement tools",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -51,7 +51,9 @@
51
51
  "rollup-plugin-typescript2": "^0.36.0",
52
52
  "typescript": "^5.0.0"
53
53
  },
54
- "dependencies": {},
54
+ "dependencies": {
55
+ "rrweb": "^1.1.3"
56
+ },
55
57
  "repository": {
56
58
  "type": "git",
57
59
  "url": "https://github.com/cuoral/cuoral-ionic.git"
package/src/cuoral.ts CHANGED
@@ -61,6 +61,12 @@ export class Cuoral {
61
61
  _t: Date.now().toString(),
62
62
  });
63
63
 
64
+ // Add session_id if available (for widget to use existing session)
65
+ const existingSessionId = localStorage.getItem('__x_loadID');
66
+ if (existingSessionId) {
67
+ params.set('cuoral_mobile_session_id', existingSessionId);
68
+ }
69
+
64
70
  if (options.email) params.set('email', options.email);
65
71
  if (options.firstName) params.set('first_name', options.firstName);
66
72
  if (options.lastName) params.set('last_name', options.lastName);
@@ -88,6 +94,14 @@ export class Cuoral {
88
94
  * Initialize Cuoral
89
95
  */
90
96
  public async initialize(): Promise<void> {
97
+ // Fetch session configuration and initialize intelligence if enabled by backend
98
+ await this.initializeIntelligence();
99
+
100
+ console.log('[Cuoral] Initialize - Session ID:', localStorage.getItem('__x_loadID'));
101
+
102
+ // Setup localStorage listener to detect when widget changes session
103
+ this.setupStorageListener();
104
+
91
105
  this.bridge.initialize();
92
106
 
93
107
  // Recreate modal if it was destroyed (e.g., after clearSession)
@@ -96,13 +110,12 @@ export class Cuoral {
96
110
  this.modal = new CuoralModal(widgetUrl, this.options.showFloatingButton);
97
111
  }
98
112
 
99
- // Initialize modal if enabled
113
+ // Update modal URL with session ID
100
114
  if (this.modal) {
115
+ const widgetUrl = this.getWidgetUrl();
116
+ this.modal.updateWidgetUrl(widgetUrl);
101
117
  this.modal.initialize();
102
118
  }
103
-
104
- // Fetch session configuration and initialize intelligence if enabled by backend
105
- await this.initializeIntelligence();
106
119
  }
107
120
 
108
121
  /**
@@ -139,8 +152,11 @@ export class Cuoral {
139
152
 
140
153
  // Only initialize intelligence if customer_intelligence is enabled in backend
141
154
  if (config && config.customer_intelligence === true) {
155
+ console.log('[Cuoral] Initializing intelligence with session:', sessionId);
142
156
  this.intelligence = new CuoralIntelligence(sessionId);
143
157
  this.intelligence.init();
158
+ } else {
159
+ console.log('[Cuoral] Intelligence not enabled or no config for session:', sessionId);
144
160
  }
145
161
  } catch (error) {
146
162
  console.warn('[Cuoral] Failed to initialize intelligence:', error);
@@ -205,6 +221,89 @@ export class Cuoral {
205
221
  }
206
222
  }
207
223
 
224
+ /**
225
+ * Update user profile for the current session
226
+ * Call this after user logs in to update the intelligence session with their profile
227
+ * @param email - User's email address
228
+ * @param name - User's full name
229
+ */
230
+ public async updateUserProfile(email: string, name: string): Promise<boolean> {
231
+ try {
232
+ const sessionId = localStorage.getItem('__x_loadID');
233
+ if (!sessionId) {
234
+ console.warn('[Cuoral] No session ID found, cannot update profile');
235
+ return false;
236
+ }
237
+
238
+ console.log('[Cuoral] Updating user profile for session:', sessionId);
239
+
240
+ const response = await fetch('https://api.cuoral.com/conversation/set-profile', {
241
+ method: 'POST',
242
+ headers: { 'Content-Type': 'application/json' },
243
+ body: JSON.stringify({
244
+ session_id: sessionId,
245
+ email: email,
246
+ name: name,
247
+ }),
248
+ });
249
+
250
+ if (!response.ok) {
251
+ console.error('[Cuoral] Failed to update profile:', response.statusText);
252
+ return false;
253
+ }
254
+
255
+ console.log('[Cuoral] ✓ User profile updated successfully for session:', sessionId);
256
+
257
+ // Store user info locally
258
+ this.options.email = email;
259
+ const nameParts = name.split(' ');
260
+ if (nameParts.length > 0) {
261
+ this.options.firstName = nameParts[0];
262
+ this.options.lastName = nameParts.slice(1).join(' ');
263
+ }
264
+
265
+ return true;
266
+ } catch (error) {
267
+ console.error('[Cuoral] Error updating user profile:', error);
268
+ return false;
269
+ }
270
+ }
271
+
272
+ /**
273
+ * Start native screen recording programmatically
274
+ * @returns Promise<boolean> - true if recording started successfully
275
+ */
276
+ public async startRecording(): Promise<boolean> {
277
+ try {
278
+ return await this.recorder.startRecording();
279
+ } catch (error) {
280
+ console.error('[Cuoral] Failed to start recording:', error);
281
+ return false;
282
+ }
283
+ }
284
+
285
+ /**
286
+ * Stop native screen recording programmatically
287
+ * Recording will be automatically uploaded to the portal
288
+ * @returns Promise<{filePath?: string; duration?: number; uploaded?: boolean} | null> - Recording result or null if failed
289
+ */
290
+ public async stopRecording(): Promise<{filePath?: string; duration?: number; uploaded?: boolean} | null> {
291
+ try {
292
+ const sessionId = localStorage.getItem('__x_loadID');
293
+ const customerId = localStorage.getItem('cuoralCustomerId');
294
+
295
+ return await this.recorder.stopRecording({
296
+ autoUpload: true,
297
+ sessionId: sessionId || undefined,
298
+ publicKey: this.options.publicKey,
299
+ customerId: customerId || undefined,
300
+ });
301
+ } catch (error) {
302
+ console.error('[Cuoral] Failed to stop recording:', error);
303
+ return null;
304
+ }
305
+ }
306
+
208
307
  /**
209
308
  * Open the widget modal
210
309
  */
@@ -248,7 +347,7 @@ export class Cuoral {
248
347
  // Add session_id if available
249
348
  const sessionId = localStorage.getItem('__x_loadID');
250
349
  if (sessionId) {
251
- params.set('session_id', sessionId);
350
+ params.set('cuoral_mobile_session_id', sessionId);
252
351
  }
253
352
 
254
353
  if (this.options.email) params.set('email', this.options.email);
@@ -307,6 +406,32 @@ export class Cuoral {
307
406
  }
308
407
  }
309
408
 
409
+ /**
410
+ * Setup localStorage listener for session changes
411
+ * Widget updates localStorage when creating new session, SDK detects and syncs
412
+ */
413
+ private setupStorageListener(): void {
414
+ // Poll localStorage every 2 seconds to detect session changes
415
+ // (storage event doesn't fire for same-window changes)
416
+ let lastKnownSession = localStorage.getItem('__x_loadID');
417
+
418
+ setInterval(() => {
419
+ const currentSession = localStorage.getItem('__x_loadID');
420
+ if (currentSession && currentSession !== lastKnownSession) {
421
+ console.log('[Cuoral] 🔄 Session changed in localStorage');
422
+ console.log('[Cuoral] Old session:', lastKnownSession);
423
+ console.log('[Cuoral] New session:', currentSession);
424
+
425
+ lastKnownSession = currentSession;
426
+
427
+ // Update intelligence to use new session
428
+ if (this.intelligence) {
429
+ this.intelligence.updateSessionId(currentSession);
430
+ }
431
+ }
432
+ }, 2000);
433
+ }
434
+
310
435
  /**
311
436
  * Clean up resources
312
437
  */
@@ -413,10 +538,28 @@ export class Cuoral {
413
538
 
414
539
  // Handle stop recording requests from widget
415
540
  this.bridge.on(CuoralMessageType.STOP_RECORDING, async () => {
416
- const result = await this.recorder.stopRecording();
541
+ const sessionId = localStorage.getItem('__x_loadID');
542
+ const customerId = localStorage.getItem('cuoralCustomerId');
417
543
 
418
- if (result) {
419
- // Convert file path to web-accessible URL
544
+ const result = await this.recorder.stopRecording({
545
+ autoUpload: true,
546
+ sessionId: sessionId || undefined,
547
+ publicKey: this.options.publicKey,
548
+ customerId: customerId || undefined,
549
+ });
550
+
551
+ if (result && result.uploaded) {
552
+ // Video was automatically uploaded, just notify widget
553
+ this.bridge.sendToWidget({
554
+ type: CuoralMessageType.RECORDING_UPLOADED,
555
+ payload: {
556
+ duration: result.duration,
557
+ uploaded: true,
558
+ timestamp: Date.now()
559
+ }
560
+ });
561
+ } else if (result) {
562
+ // Upload failed or was disabled, send video data to widget (old behavior)
420
563
  const capacitorUrl = result.filePath ? Capacitor.convertFileSrc(result.filePath) : '';
421
564
 
422
565
  try {