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.
package/src/cuoral.ts ADDED
@@ -0,0 +1,208 @@
1
+ import { CuoralBridge } from './bridge';
2
+ import { CuoralRecorder } from './plugin';
3
+ import { CuoralMessageType } from './types';
4
+ import { CuoralModal } from './modal';
5
+ import { Capacitor } from '@capacitor/core';
6
+
7
+ export interface CuoralOptions {
8
+ publicKey: string;
9
+ email?: string;
10
+ firstName?: string;
11
+ lastName?: string;
12
+ debug?: boolean;
13
+ widgetBaseUrl?: string; // Allow custom widget URL
14
+ showFloatingButton?: boolean; // Show floating chat button (default: true)
15
+ useModal?: boolean; // Use modal display mode (default: true)
16
+ }
17
+
18
+ /**
19
+ * Main Cuoral class - simple API for users
20
+ */
21
+ export class Cuoral {
22
+ private bridge: CuoralBridge;
23
+ private recorder: CuoralRecorder;
24
+ private modal?: CuoralModal;
25
+ private options: CuoralOptions;
26
+ private static readonly PRODUCTION_WIDGET_URL = 'https://js.cuoral.com/mobile.html';
27
+ private static readonly DEV_WIDGET_URL = 'assets/mobile.html';
28
+
29
+ constructor(options: CuoralOptions) {
30
+ this.options = {
31
+ showFloatingButton: true,
32
+ useModal: true,
33
+ ...options
34
+ };
35
+
36
+ // Determine widget base URL
37
+ const baseUrl = options.widgetBaseUrl || Cuoral.PRODUCTION_WIDGET_URL;
38
+ const params = new URLSearchParams({
39
+ auto_start: 'true',
40
+ key: options.publicKey,
41
+ _t: Date.now().toString(),
42
+ });
43
+
44
+ if (options.email) params.set('email', options.email);
45
+ if (options.firstName) params.set('first_name', options.firstName);
46
+ if (options.lastName) params.set('last_name', options.lastName);
47
+
48
+ const widgetUrl = `${baseUrl}?${params.toString()}`;
49
+
50
+ // Initialize modal if enabled
51
+ if (this.options.useModal) {
52
+ this.modal = new CuoralModal(widgetUrl, this.options.showFloatingButton);
53
+ }
54
+
55
+ // Initialize bridge and recorder
56
+ this.bridge = new CuoralBridge({
57
+ widgetUrl,
58
+ debug: options.debug || false
59
+ });
60
+
61
+ this.recorder = new CuoralRecorder();
62
+
63
+ // Setup automatic message handlers
64
+ this.setupMessageHandlers();
65
+ }
66
+
67
+ /**
68
+ * Initialize Cuoral
69
+ */
70
+ public initialize(): void {
71
+ this.bridge.initialize();
72
+
73
+ // Initialize modal if enabled
74
+ if (this.modal) {
75
+ this.modal.initialize();
76
+ }
77
+ }
78
+
79
+ /**
80
+ * Open the widget modal
81
+ */
82
+ public openModal(): void {
83
+ if (this.modal) {
84
+ this.modal.open();
85
+ }
86
+ }
87
+
88
+ /**
89
+ * Close the widget modal
90
+ */
91
+ public closeModal(): void {
92
+ if (this.modal) {
93
+ this.modal.close();
94
+ }
95
+ }
96
+
97
+ /**
98
+ * Check if modal is open
99
+ */
100
+ public isModalOpen(): boolean {
101
+ return this.modal?.isModalOpen() || false;
102
+ }
103
+
104
+ /**
105
+ * Get the widget URL for iframe embedding
106
+ */
107
+ public getWidgetUrl(): string {
108
+ const baseUrl = this.options.widgetBaseUrl || Cuoral.PRODUCTION_WIDGET_URL;
109
+ const params = new URLSearchParams({
110
+ auto_start: 'true',
111
+ key: this.options.publicKey,
112
+ _t: Date.now().toString(),
113
+ });
114
+
115
+ if (this.options.email) params.set('email', this.options.email);
116
+ if (this.options.firstName) params.set('first_name', this.options.firstName);
117
+ if (this.options.lastName) params.set('last_name', this.options.lastName);
118
+
119
+ return `${baseUrl}?${params.toString()}`;
120
+ }
121
+
122
+ /**
123
+ * Clean up resources
124
+ */
125
+ public destroy(): void {
126
+ this.bridge.destroy();
127
+
128
+ // Destroy modal if exists
129
+ if (this.modal) {
130
+ this.modal.destroy();
131
+ }
132
+ }
133
+
134
+ /**
135
+ * Setup automatic message handlers
136
+ */
137
+ private setupMessageHandlers(): void {
138
+ // Handle start recording requests from widget
139
+ this.bridge.on(CuoralMessageType.START_RECORDING, async () => {
140
+ const success = await this.recorder.startRecording();
141
+ if (!success) {
142
+ // Error already handled by recorder
143
+ }
144
+ });
145
+
146
+ // Handle stop recording requests from widget
147
+ this.bridge.on(CuoralMessageType.STOP_RECORDING, async () => {
148
+ const result = await this.recorder.stopRecording();
149
+
150
+ if (result) {
151
+ // Convert file path to web-accessible URL
152
+ const capacitorUrl = result.filePath ? Capacitor.convertFileSrc(result.filePath) : '';
153
+
154
+ try {
155
+ // Fetch the video blob from the capacitor URL
156
+ const response = await fetch(capacitorUrl);
157
+ const videoBlob = await response.blob();
158
+
159
+ // Convert blob to base64 data URL so it can be serialized and transferred cross-origin
160
+ const reader = new FileReader();
161
+ reader.onloadend = () => {
162
+ const base64data = reader.result as string;
163
+
164
+ // Send the base64 data via bridge to CDN widget
165
+ this.bridge.sendToWidget({
166
+ type: CuoralMessageType.RECORDING_STOPPED,
167
+ payload: {
168
+ videoData: base64data,
169
+ videoType: 'video/mp4',
170
+ originalPath: result.filePath,
171
+ duration: result.duration,
172
+ timestamp: Date.now()
173
+ }
174
+ });
175
+ };
176
+
177
+ reader.onerror = (error) => {
178
+ console.error('[Cuoral] Error reading blob:', error);
179
+ this.bridge.sendToWidget({
180
+ type: CuoralMessageType.RECORDING_STOPPED,
181
+ payload: {
182
+ error: 'Failed to process recording',
183
+ duration: result.duration,
184
+ timestamp: Date.now()
185
+ }
186
+ });
187
+ };
188
+
189
+ // Start reading the blob as data URL
190
+ reader.readAsDataURL(videoBlob);
191
+
192
+ } catch (error) {
193
+ console.error('[Cuoral] Error processing video file:', error);
194
+ this.bridge.sendToWidget({
195
+ type: CuoralMessageType.RECORDING_STOPPED,
196
+ payload: {
197
+ error: 'Failed to process recording',
198
+ duration: result.duration,
199
+ timestamp: Date.now()
200
+ }
201
+ });
202
+ }
203
+ } else {
204
+ console.error('[Cuoral] No result from stopRecording');
205
+ }
206
+ });
207
+ }
208
+ }
package/src/index.ts ADDED
@@ -0,0 +1,5 @@
1
+ export * from './plugin';
2
+ export * from './bridge';
3
+ export * from './types';
4
+ export * from './cuoral';
5
+ export * from './modal';
package/src/modal.ts ADDED
@@ -0,0 +1,257 @@
1
+ /**
2
+ * Cuoral Modal Manager
3
+ * Handles full-screen modal display with floating button
4
+ */
5
+ export class CuoralModal {
6
+ private modalElement?: HTMLDivElement;
7
+ private iframeElement?: HTMLIFrameElement;
8
+ private floatingButton?: HTMLDivElement;
9
+ private isOpen = false;
10
+ private widgetUrl: string;
11
+ private showFloatingButton: boolean;
12
+
13
+ constructor(widgetUrl: string, showFloatingButton = true) {
14
+ this.widgetUrl = widgetUrl;
15
+ this.showFloatingButton = showFloatingButton;
16
+ }
17
+
18
+ /**
19
+ * Initialize the modal and floating button
20
+ */
21
+ public initialize(): void {
22
+ if (this.showFloatingButton) {
23
+ this.createFloatingButton();
24
+ }
25
+ this.createModal();
26
+ }
27
+
28
+ /**
29
+ * Create floating chat button
30
+ */
31
+ private createFloatingButton(): void {
32
+ this.floatingButton = document.createElement('div');
33
+ this.floatingButton.id = 'cuoral-floating-button';
34
+ this.floatingButton.innerHTML = `
35
+ <svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
36
+ <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"/>
37
+ </svg>
38
+ `;
39
+
40
+ // Styles
41
+ Object.assign(this.floatingButton.style, {
42
+ position: 'fixed',
43
+ bottom: '20px',
44
+ right: '20px',
45
+ width: '56px',
46
+ height: '56px',
47
+ borderRadius: '50%',
48
+ backgroundColor: '#007AFF',
49
+ display: 'flex',
50
+ alignItems: 'center',
51
+ justifyContent: 'center',
52
+ cursor: 'pointer',
53
+ boxShadow: '0 4px 12px rgba(0, 122, 255, 0.4)',
54
+ zIndex: '999999',
55
+ transition: 'transform 0.2s, box-shadow 0.2s'
56
+ });
57
+
58
+ // Hover effect
59
+ this.floatingButton.addEventListener('mouseenter', () => {
60
+ if (this.floatingButton) {
61
+ this.floatingButton.style.transform = 'scale(1.1)';
62
+ this.floatingButton.style.boxShadow = '0 6px 16px rgba(0, 122, 255, 0.5)';
63
+ }
64
+ });
65
+
66
+ this.floatingButton.addEventListener('mouseleave', () => {
67
+ if (this.floatingButton) {
68
+ this.floatingButton.style.transform = 'scale(1)';
69
+ this.floatingButton.style.boxShadow = '0 4px 12px rgba(0, 122, 255, 0.4)';
70
+ }
71
+ });
72
+
73
+ // Click to open modal
74
+ this.floatingButton.addEventListener('click', () => {
75
+ this.open();
76
+ });
77
+
78
+ document.body.appendChild(this.floatingButton);
79
+ }
80
+
81
+ /**
82
+ * Create modal structure
83
+ */
84
+ private createModal(): void {
85
+ // Modal container
86
+ this.modalElement = document.createElement('div');
87
+ this.modalElement.id = 'cuoral-modal';
88
+
89
+ Object.assign(this.modalElement.style, {
90
+ position: 'fixed',
91
+ top: '0',
92
+ left: '0',
93
+ width: '100%',
94
+ height: '100%',
95
+ backgroundColor: 'rgba(0, 0, 0, 0.5)',
96
+ display: 'none',
97
+ alignItems: 'center',
98
+ justifyContent: 'center',
99
+ zIndex: '1000000',
100
+ opacity: '0',
101
+ transition: 'opacity 0.3s ease',
102
+ padding: '60px 0'
103
+ });
104
+
105
+ // Modal content (with margin)
106
+ const modalContent = document.createElement('div');
107
+ Object.assign(modalContent.style, {
108
+ width: '100%',
109
+ height: '100%',
110
+ maxWidth: '100%',
111
+ maxHeight: '100%',
112
+ position: 'relative',
113
+ backgroundColor: 'transparent',
114
+ display: 'flex',
115
+ flexDirection: 'column',
116
+ borderRadius: '12px',
117
+ overflow: 'hidden'
118
+ });
119
+
120
+ // Close button
121
+ const closeButton = document.createElement('div');
122
+ closeButton.innerHTML = `
123
+ <svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
124
+ <path d="M18 6L6 18M6 6L18 18" stroke="white" stroke-width="2" stroke-linecap="round"/>
125
+ </svg>
126
+ `;
127
+
128
+ Object.assign(closeButton.style, {
129
+ position: 'absolute',
130
+ top: '20px',
131
+ right: '20px',
132
+ width: '40px',
133
+ height: '40px',
134
+ borderRadius: '50%',
135
+ backgroundColor: 'rgba(0, 0, 0, 0.5)',
136
+ display: 'flex',
137
+ alignItems: 'center',
138
+ justifyContent: 'center',
139
+ cursor: 'pointer',
140
+ zIndex: '1000001',
141
+ transition: 'background-color 0.2s'
142
+ });
143
+
144
+ closeButton.addEventListener('mouseenter', () => {
145
+ closeButton.style.backgroundColor = 'rgba(0, 0, 0, 0.7)';
146
+ });
147
+
148
+ closeButton.addEventListener('mouseleave', () => {
149
+ closeButton.style.backgroundColor = 'rgba(0, 0, 0, 0.5)';
150
+ });
151
+
152
+ closeButton.addEventListener('click', () => {
153
+ this.close();
154
+ });
155
+
156
+ // Iframe
157
+ this.iframeElement = document.createElement('iframe');
158
+ this.iframeElement.id = 'cuoral-widget-iframe';
159
+ this.iframeElement.src = this.widgetUrl;
160
+
161
+ Object.assign(this.iframeElement.style, {
162
+ width: '100%',
163
+ height: '100%',
164
+ border: 'none',
165
+ backgroundColor: 'white'
166
+ });
167
+
168
+ modalContent.appendChild(closeButton);
169
+ modalContent.appendChild(this.iframeElement);
170
+ this.modalElement.appendChild(modalContent);
171
+
172
+ // Close on backdrop click
173
+ this.modalElement.addEventListener('click', (e) => {
174
+ if (e.target === this.modalElement) {
175
+ this.close();
176
+ }
177
+ });
178
+
179
+ document.body.appendChild(this.modalElement);
180
+ }
181
+
182
+ /**
183
+ * Open the modal
184
+ */
185
+ public open(): void {
186
+ if (this.isOpen || !this.modalElement) return;
187
+
188
+ this.isOpen = true;
189
+ this.modalElement.style.display = 'flex';
190
+
191
+ // Trigger animation
192
+ setTimeout(() => {
193
+ if (this.modalElement) {
194
+ this.modalElement.style.opacity = '1';
195
+ }
196
+ }, 10);
197
+
198
+ // Hide floating button when modal is open
199
+ if (this.floatingButton) {
200
+ this.floatingButton.style.display = 'none';
201
+ }
202
+
203
+ // Prevent body scroll
204
+ document.body.style.overflow = 'hidden';
205
+ }
206
+
207
+ /**
208
+ * Close the modal
209
+ */
210
+ public close(): void {
211
+ if (!this.isOpen || !this.modalElement) return;
212
+
213
+ this.isOpen = false;
214
+ this.modalElement.style.opacity = '0';
215
+
216
+ setTimeout(() => {
217
+ if (this.modalElement) {
218
+ this.modalElement.style.display = 'none';
219
+ }
220
+
221
+ // Show floating button again
222
+ if (this.floatingButton) {
223
+ this.floatingButton.style.display = 'flex';
224
+ }
225
+
226
+ // Restore body scroll
227
+ document.body.style.overflow = '';
228
+ }, 300);
229
+ }
230
+
231
+ /**
232
+ * Check if modal is open
233
+ */
234
+ public isModalOpen(): boolean {
235
+ return this.isOpen;
236
+ }
237
+
238
+ /**
239
+ * Destroy modal and floating button
240
+ */
241
+ public destroy(): void {
242
+ if (this.modalElement) {
243
+ this.modalElement.remove();
244
+ }
245
+ if (this.floatingButton) {
246
+ this.floatingButton.remove();
247
+ }
248
+ document.body.style.overflow = '';
249
+ }
250
+
251
+ /**
252
+ * Get iframe element for communication
253
+ */
254
+ public getIframe(): HTMLIFrameElement | undefined {
255
+ return this.iframeElement;
256
+ }
257
+ }
package/src/plugin.ts ADDED
@@ -0,0 +1,190 @@
1
+ import { registerPlugin } from '@capacitor/core';
2
+ import {
3
+ CuoralMessageType,
4
+ RecordingOptions,
5
+ RecordingState,
6
+ ScreenshotOptions,
7
+ ScreenshotData,
8
+ } from './types';
9
+
10
+ /**
11
+ * Capacitor Plugin Interface
12
+ */
13
+ export interface CuoralPluginInterface {
14
+ /**
15
+ * Start screen recording
16
+ */
17
+ startRecording(options?: RecordingOptions): Promise<{ success: boolean }>;
18
+
19
+ /**
20
+ * Stop screen recording
21
+ */
22
+ stopRecording(): Promise<{ success: boolean; filePath?: string; duration?: number }>;
23
+
24
+ /**
25
+ * Get recording state
26
+ */
27
+ getRecordingState(): Promise<RecordingState>;
28
+
29
+ /**
30
+ * Take screenshot
31
+ */
32
+ takeScreenshot(options?: ScreenshotOptions): Promise<ScreenshotData>;
33
+
34
+ /**
35
+ * Check if recording is supported
36
+ */
37
+ isRecordingSupported(): Promise<{ supported: boolean }>;
38
+
39
+ /**
40
+ * Request recording permissions
41
+ */
42
+ requestPermissions(): Promise<{ granted: boolean }>;
43
+ }
44
+
45
+ /**
46
+ * Register the Capacitor plugin
47
+ */
48
+ const CuoralPlugin = registerPlugin<CuoralPluginInterface>('CuoralPlugin', {
49
+ web: () => import('./web').then(m => new m.CuoralPluginWeb()),
50
+ });
51
+
52
+ export { CuoralPlugin };
53
+
54
+ /**
55
+ * High-level API for easier integration
56
+ */
57
+ export class CuoralRecorder {
58
+ private isRecording: boolean = false;
59
+ private recordingStartTime?: number;
60
+
61
+ /**
62
+ * Start recording with automatic permission handling
63
+ */
64
+ async startRecording(options?: RecordingOptions): Promise<boolean> {
65
+ try {
66
+ // Check if already recording
67
+ if (this.isRecording) {
68
+ console.warn('[Cuoral] Already recording');
69
+ return false;
70
+ }
71
+
72
+ // Check support
73
+ const { supported } = await CuoralPlugin.isRecordingSupported();
74
+ if (!supported) {
75
+ throw new Error('Screen recording is not supported on this device');
76
+ }
77
+
78
+ // Request permissions
79
+ const { granted } = await CuoralPlugin.requestPermissions();
80
+ if (!granted) {
81
+ throw new Error('Recording permissions not granted');
82
+ }
83
+
84
+ // Start recording
85
+ const result = await CuoralPlugin.startRecording(options);
86
+ if (result.success) {
87
+ this.isRecording = true;
88
+ this.recordingStartTime = Date.now();
89
+
90
+ // Post message to widget
91
+ this.postMessage({
92
+ type: CuoralMessageType.RECORDING_STARTED,
93
+ payload: { timestamp: this.recordingStartTime },
94
+ });
95
+ }
96
+
97
+ return result.success;
98
+ } catch (error) {
99
+ console.error('[Cuoral] Failed to start recording:', error);
100
+ this.postMessage({
101
+ type: CuoralMessageType.RECORDING_ERROR,
102
+ payload: { error: (error as Error).message },
103
+ });
104
+ return false;
105
+ }
106
+ }
107
+
108
+ /**
109
+ * Stop recording
110
+ */
111
+ async stopRecording(): Promise<{ filePath?: string; duration?: number } | null> {
112
+ try {
113
+ if (!this.isRecording) {
114
+ console.warn('[Cuoral] Not recording');
115
+ return null;
116
+ }
117
+
118
+ const result = await CuoralPlugin.stopRecording();
119
+ if (result.success) {
120
+ this.isRecording = false;
121
+ const duration = this.recordingStartTime
122
+ ? Math.floor((Date.now() - this.recordingStartTime) / 1000)
123
+ : 0;
124
+
125
+ // Post message to widget
126
+ this.postMessage({
127
+ type: CuoralMessageType.RECORDING_STOPPED,
128
+ payload: {
129
+ filePath: result.filePath,
130
+ duration: result.duration || duration,
131
+ },
132
+ });
133
+
134
+ return {
135
+ filePath: result.filePath,
136
+ duration: result.duration || duration,
137
+ };
138
+ }
139
+
140
+ return null;
141
+ } catch (error) {
142
+ console.error('[Cuoral] Failed to stop recording:', error);
143
+ this.postMessage({
144
+ type: CuoralMessageType.RECORDING_ERROR,
145
+ payload: { error: (error as Error).message },
146
+ });
147
+ return null;
148
+ }
149
+ }
150
+
151
+ /**
152
+ * Take screenshot
153
+ */
154
+ async takeScreenshot(options?: ScreenshotOptions): Promise<ScreenshotData | null> {
155
+ try {
156
+ const screenshot = await CuoralPlugin.takeScreenshot(options);
157
+
158
+ // Post message to widget
159
+ this.postMessage({
160
+ type: CuoralMessageType.SCREENSHOT_TAKEN,
161
+ payload: screenshot,
162
+ });
163
+
164
+ return screenshot;
165
+ } catch (error) {
166
+ console.error('[Cuoral] Failed to take screenshot:', error);
167
+ this.postMessage({
168
+ type: CuoralMessageType.SCREENSHOT_ERROR,
169
+ payload: { error: (error as Error).message },
170
+ });
171
+ return null;
172
+ }
173
+ }
174
+
175
+ /**
176
+ * Get current recording state
177
+ */
178
+ async getState(): Promise<RecordingState> {
179
+ return await CuoralPlugin.getRecordingState();
180
+ }
181
+
182
+ /**
183
+ * Post message to WebView
184
+ */
185
+ private postMessage(message: any): void {
186
+ if (typeof window !== 'undefined' && window.postMessage) {
187
+ window.postMessage(message, '*');
188
+ }
189
+ }
190
+ }