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.
- package/README.md +72 -1
- package/android/.gradle/8.9/checksums/checksums.lock +0 -0
- package/android/.gradle/8.9/checksums/sha1-checksums.bin +0 -0
- package/android/build/.transforms/bb54161301273cf9b5b94a21c0fb3f23/transformed/classes/classes_dex/classes.dex +0 -0
- package/android/build/.transforms/f1aabffcd8b03aa664e77a79b3e1de5d/transformed/debug/debug_dex/com/cuoral/ionic/CuoralPlugin$1.dex +0 -0
- package/android/build/.transforms/f1aabffcd8b03aa664e77a79b3e1de5d/transformed/debug/debug_dex/com/cuoral/ionic/CuoralPlugin$2.dex +0 -0
- package/android/build/.transforms/f1aabffcd8b03aa664e77a79b3e1de5d/transformed/debug/debug_dex/com/cuoral/ionic/CuoralPlugin$3.dex +0 -0
- package/android/build/.transforms/f1aabffcd8b03aa664e77a79b3e1de5d/transformed/debug/debug_dex/com/cuoral/ionic/CuoralPlugin$4.dex +0 -0
- package/android/build/.transforms/f1aabffcd8b03aa664e77a79b3e1de5d/transformed/debug/debug_dex/com/cuoral/ionic/CuoralPlugin$5.dex +0 -0
- package/android/build/.transforms/f1aabffcd8b03aa664e77a79b3e1de5d/transformed/debug/debug_dex/com/cuoral/ionic/CuoralPlugin$6.dex +0 -0
- package/android/build/.transforms/f1aabffcd8b03aa664e77a79b3e1de5d/transformed/debug/debug_dex/com/cuoral/ionic/CuoralPlugin$InitiateCallback.dex +0 -0
- package/android/build/.transforms/f1aabffcd8b03aa664e77a79b3e1de5d/transformed/debug/debug_dex/com/cuoral/ionic/CuoralPlugin$UploadCallback.dex +0 -0
- package/android/build/.transforms/f1aabffcd8b03aa664e77a79b3e1de5d/transformed/debug/debug_dex/com/cuoral/ionic/CuoralPlugin.dex +0 -0
- package/android/build/.transforms/f1aabffcd8b03aa664e77a79b3e1de5d/transformed/debug/desugar_graph.bin +0 -0
- package/android/build/intermediates/compile_library_classes_jar/debug/classes.jar +0 -0
- package/android/build/intermediates/javac/debug/classes/com/cuoral/ionic/CuoralPlugin$1.class +0 -0
- package/android/build/intermediates/javac/debug/classes/com/cuoral/ionic/CuoralPlugin$2.class +0 -0
- package/android/build/intermediates/javac/debug/classes/com/cuoral/ionic/CuoralPlugin$3.class +0 -0
- package/android/build/intermediates/javac/debug/classes/com/cuoral/ionic/CuoralPlugin$4.class +0 -0
- package/android/build/intermediates/javac/debug/classes/com/cuoral/ionic/CuoralPlugin$5.class +0 -0
- package/android/build/intermediates/javac/debug/classes/com/cuoral/ionic/CuoralPlugin$6.class +0 -0
- package/android/build/intermediates/javac/debug/classes/com/cuoral/ionic/CuoralPlugin$InitiateCallback.class +0 -0
- package/android/build/intermediates/javac/debug/classes/com/cuoral/ionic/CuoralPlugin$UploadCallback.class +0 -0
- package/android/build/intermediates/javac/debug/classes/com/cuoral/ionic/CuoralPlugin.class +0 -0
- package/android/build/intermediates/runtime_library_classes_dir/debug/com/cuoral/ionic/CuoralPlugin$1.class +0 -0
- package/android/build/intermediates/runtime_library_classes_dir/debug/com/cuoral/ionic/CuoralPlugin$2.class +0 -0
- package/android/build/intermediates/runtime_library_classes_dir/debug/com/cuoral/ionic/CuoralPlugin$3.class +0 -0
- package/android/build/intermediates/runtime_library_classes_dir/debug/com/cuoral/ionic/CuoralPlugin$4.class +0 -0
- package/android/build/intermediates/runtime_library_classes_dir/debug/com/cuoral/ionic/CuoralPlugin$5.class +0 -0
- package/android/build/intermediates/runtime_library_classes_dir/debug/com/cuoral/ionic/CuoralPlugin$6.class +0 -0
- package/android/build/intermediates/runtime_library_classes_dir/debug/com/cuoral/ionic/CuoralPlugin$InitiateCallback.class +0 -0
- package/android/build/intermediates/runtime_library_classes_dir/debug/com/cuoral/ionic/CuoralPlugin$UploadCallback.class +0 -0
- package/android/build/intermediates/runtime_library_classes_dir/debug/com/cuoral/ionic/CuoralPlugin.class +0 -0
- package/android/build/intermediates/runtime_library_classes_jar/debug/classes.jar +0 -0
- package/android/build/tmp/compileDebugJavaWithJavac/previous-compilation-data.bin +0 -0
- package/android/build.gradle +1 -0
- package/android/src/main/java/com/cuoral/ionic/CuoralPlugin.java +205 -5
- package/dist/cuoral.d.ts +27 -0
- package/dist/cuoral.d.ts.map +1 -1
- package/dist/cuoral.js +137 -7
- package/dist/index.esm.js +471 -17
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +490 -17
- package/dist/index.js.map +1 -1
- package/dist/intelligence.d.ts +51 -0
- package/dist/intelligence.d.ts.map +1 -1
- package/dist/intelligence.js +307 -0
- package/dist/plugin.d.ts +15 -2
- package/dist/plugin.d.ts.map +1 -1
- package/dist/plugin.js +23 -10
- package/dist/types.d.ts +4 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +4 -0
- package/ios/Plugin/CuoralPlugin.swift +249 -13
- package/package.json +4 -2
- package/src/cuoral.ts +151 -8
- package/src/intelligence.ts +375 -0
- package/src/plugin.ts +39 -11
- 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
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
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.
|
|
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
|
-
//
|
|
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('
|
|
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
|
|
541
|
+
const sessionId = localStorage.getItem('__x_loadID');
|
|
542
|
+
const customerId = localStorage.getItem('cuoralCustomerId');
|
|
417
543
|
|
|
418
|
-
|
|
419
|
-
|
|
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 {
|