@revrag-ai/embed-react-native 1.0.5 → 1.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 (94) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +13 -382
  3. package/android/CMakeLists.txt +15 -0
  4. package/android/build.gradle +77 -25
  5. package/android/cpp-adapter.cpp +8 -0
  6. package/android/generated/java/com/revragai/embedreactnative/NativeEmbedReactNativeSpec.java +37 -0
  7. package/android/generated/jni/CMakeLists.txt +49 -0
  8. package/android/generated/jni/RNEmbedReactNativeSpec-generated.cpp +32 -0
  9. package/android/generated/jni/RNEmbedReactNativeSpec.h +31 -0
  10. package/android/generated/jni/react/renderer/components/RNEmbedReactNativeSpec/RNEmbedReactNativeSpecJSI-generated.cpp +28 -0
  11. package/android/generated/jni/react/renderer/components/RNEmbedReactNativeSpec/RNEmbedReactNativeSpecJSI.h +67 -0
  12. package/android/gradle.properties +5 -5
  13. package/cpp/revrag-ai-embed-react-native.cpp +7 -0
  14. package/cpp/revrag-ai-embed-react-native.h +8 -0
  15. package/ios/EmbedReactNative.h +9 -0
  16. package/ios/{Onwid.mm → EmbedReactNative.mm} +4 -4
  17. package/ios/generated/RNEmbedReactNativeSpec/RNEmbedReactNativeSpec-generated.mm +29 -0
  18. package/ios/generated/RNEmbedReactNativeSpec/RNEmbedReactNativeSpec.h +50 -0
  19. package/ios/generated/RNEmbedReactNativeSpecJSI-generated.cpp +28 -0
  20. package/ios/generated/RNEmbedReactNativeSpecJSI.h +67 -0
  21. package/lib/commonjs/NativeEmbedReactNative.js +9 -0
  22. package/lib/commonjs/api/api.js +256 -0
  23. package/lib/commonjs/api/types/embed.api.types.js +2 -0
  24. package/lib/commonjs/components/Embed/EmbedAudioWave.js +157 -0
  25. package/lib/commonjs/components/Embed/EmbedButton.js +511 -0
  26. package/lib/commonjs/components/Embed/EmbedVoice.js +131 -0
  27. package/lib/commonjs/components/styles/EmbedButton.style.js +248 -0
  28. package/lib/commonjs/events/embed.event.js +74 -0
  29. package/lib/commonjs/hooks/initialize.js +102 -0
  30. package/lib/commonjs/hooks/initialize.livekit.js +20 -0
  31. package/lib/commonjs/hooks/types/initialize.types.js +2 -0
  32. package/lib/commonjs/hooks/types/voiceAgent.types.js +6 -0
  33. package/lib/commonjs/hooks/voiceagent.js +358 -0
  34. package/lib/commonjs/index.js +34 -0
  35. package/lib/commonjs/index.types.js +22 -0
  36. package/lib/commonjs/store/store.key.js +46 -0
  37. package/lib/commonjs/utils/reanimated.helper.js +100 -0
  38. package/lib/module/NativeEmbedReactNative.js +5 -0
  39. package/lib/module/api/api.js +248 -0
  40. package/lib/module/api/types/embed.api.types.js +2 -0
  41. package/lib/module/components/Embed/EmbedAudioWave.js +152 -0
  42. package/lib/module/components/Embed/EmbedButton.js +506 -0
  43. package/lib/module/components/Embed/EmbedVoice.js +127 -0
  44. package/lib/module/components/styles/EmbedButton.style.js +243 -0
  45. package/lib/module/events/embed.event.js +70 -0
  46. package/lib/module/hooks/initialize.js +79 -75
  47. package/lib/module/hooks/{initializelivekit.js → initialize.livekit.js} +7 -4
  48. package/lib/module/hooks/types/initialize.types.js +2 -0
  49. package/lib/module/hooks/types/voiceAgent.types.js +4 -0
  50. package/lib/module/hooks/voiceagent.js +353 -0
  51. package/lib/module/index.js +6 -60
  52. package/lib/module/index.types.js +23 -0
  53. package/lib/module/store/store.key.js +38 -0
  54. package/lib/module/utils/reanimated.helper.js +94 -0
  55. package/lib/typescript/commonjs/package.json +1 -0
  56. package/lib/typescript/module/package.json +1 -0
  57. package/package.json +69 -27
  58. package/react-native.config.js +8 -14
  59. package/revrag-ai-embed-react-native.podspec +41 -0
  60. package/Onwid.podspec +0 -20
  61. package/ios/Onwid.h +0 -5
  62. package/lib/index.d.ts +0 -77
  63. package/lib/module/Event/onwid.js +0 -74
  64. package/lib/module/NativeOnwid.js +0 -4
  65. package/lib/module/component/OnwidButton.js +0 -366
  66. package/lib/module/component/audiowave.js +0 -137
  67. package/lib/module/component/voice.js +0 -103
  68. package/lib/module/hooks/initialize.types.js +0 -2
  69. package/lib/module/hooks/voiceAgent.js +0 -334
  70. package/lib/module/hooks/voiceAgent.types.js +0 -2
  71. package/lib/module/onwidApi/api.js +0 -184
  72. package/lib/module/onwidApi/api.types.js +0 -2
  73. package/lib/module/store.key.js +0 -47
  74. package/lib/module/style/onwidButton.style.js +0 -230
  75. package/lib/module/utils/reanimatedHelpers.js +0 -87
  76. package/lib/module/utils/utils.js +0 -1
  77. package/lib/typescript/Event/onwid.d.ts +0 -13
  78. package/lib/typescript/NativeOnwid.d.ts +0 -6
  79. package/lib/typescript/component/OnwidButton.d.ts +0 -28
  80. package/lib/typescript/component/audiowave.d.ts +0 -6
  81. package/lib/typescript/component/voice.d.ts +0 -15
  82. package/lib/typescript/hooks/initialize.d.ts +0 -2
  83. package/lib/typescript/hooks/initialize.types.d.ts +0 -5
  84. package/lib/typescript/hooks/initializelivekit.d.ts +0 -3
  85. package/lib/typescript/hooks/voiceAgent.d.ts +0 -2
  86. package/lib/typescript/hooks/voiceAgent.types.d.ts +0 -16
  87. package/lib/typescript/index.d.ts +0 -27
  88. package/lib/typescript/onwidApi/api.d.ts +0 -53
  89. package/lib/typescript/onwidApi/api.types.d.ts +0 -21
  90. package/lib/typescript/store.key.d.ts +0 -3
  91. package/lib/typescript/style/onwidButton.style.d.ts +0 -98
  92. package/lib/typescript/utils/reanimatedHelpers.d.ts +0 -29
  93. package/lib/typescript/utils/utils.d.ts +0 -0
  94. package/scripts/verify-setup.js +0 -90
@@ -0,0 +1,506 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * @file OnwidButton.tsx
5
+ * @description A customizable floating action button component for React Native applications.
6
+ * This component provides a draggable, expandable button with animation support and gradient styling.
7
+ */
8
+
9
+ import LottieView from 'lottie-react-native';
10
+ import { useEffect, useMemo, useRef, useState } from 'react';
11
+ import { Dimensions, Image, Text, TouchableOpacity, View } from 'react-native';
12
+ import { Gesture, GestureDetector } from 'react-native-gesture-handler';
13
+ import LinearGradient from 'react-native-linear-gradient';
14
+ import { useVoiceAgent } from "../../hooks/voiceagent.js";
15
+ import { getAgentData } from "../../store/store.key.js";
16
+ import { createEmbedButtonStyles } from "../styles/EmbedButton.style.js";
17
+ import { getReanimatedAPI, showReanimatedSetupError } from "../../utils/reanimated.helper.js";
18
+ import Voice from "./EmbedVoice.js";
19
+ import { WaveformVisualizer } from "./EmbedAudioWave.js";
20
+
21
+ // Get reanimated API with fallbacks
22
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
23
+ const {
24
+ useSharedValue,
25
+ useAnimatedStyle,
26
+ withTiming,
27
+ withSpring,
28
+ withRepeat,
29
+ withSequence,
30
+ runOnJS,
31
+ Easing,
32
+ Animated,
33
+ isAvailable: isReanimatedAvailable
34
+ } = getReanimatedAPI();
35
+
36
+ // Show warning if reanimated is not available
37
+ if (!isReanimatedAvailable) {
38
+ showReanimatedSetupError();
39
+ }
40
+ const {
41
+ width: SCREEN_WIDTH,
42
+ height: SCREEN_HEIGHT
43
+ } = Dimensions.get('window');
44
+ const BUTTON_WIDTH = 60;
45
+ const EXPANDED_WIDTH = SCREEN_WIDTH * 0.9;
46
+ const BUTTON_HEIGHT = 60;
47
+ const GRADIENT_COLORS = ['#1E0844', '#B391F3'];
48
+
49
+ // Define mic icons as base64 images for portability
50
+ const MIC_ON_ICON = 'https://revrag-dev.s3.ap-south-1.amazonaws.com/Avatars/Mute+button.png';
51
+ const MIC_OFF_ICON = 'https://revrag-dev.s3.ap-south-1.amazonaws.com/Avatars/unmute.png';
52
+
53
+ // Add end call icon
54
+ const END_CALL_ICON = 'https://revrag-dev.s3.ap-south-1.amazonaws.com/Avatars/end+button.png';
55
+ const AMPLIFY_ANIMATION = 'https://revrag-dev.s3.ap-south-1.amazonaws.com/Avatars/amplify.json';
56
+
57
+ /**
58
+ * Default styles configuration for the button
59
+ */
60
+ const defaultStyles = {
61
+ buttonWidth: 60,
62
+ buttonHeight: 60,
63
+ borderRadius: 100,
64
+ marginBottom: 20,
65
+ spacing: {
66
+ SMALL: 10,
67
+ MEDIUM: 15,
68
+ LARGE: 25
69
+ }
70
+ };
71
+
72
+ /**
73
+ * OnwidButton Component
74
+ *
75
+ * A floating action button that can be dragged around the screen and expanded to show additional content.
76
+ * Features include:
77
+ * - Draggable functionality
78
+ * - Expandable menu
79
+ * - Animated transitions
80
+ * - Gradient background
81
+ * - Customizable styling
82
+ *
83
+ * @component
84
+ * @example
85
+ * ```tsx
86
+ * <OnwidButton
87
+ * isOpen={false}
88
+ * onPress={(isOpen) => console.log('Button pressed:', isOpen)}
89
+ * menuComponent={<YourMenuComponent />}
90
+ * />
91
+ * ```
92
+ */
93
+ export function EmbedButton() {
94
+ const {
95
+ initializeVoiceAgent,
96
+ tokenDetails,
97
+ endCall,
98
+ isLoading,
99
+ isMicMuted,
100
+ muteMic,
101
+ unmuteMic,
102
+ connectionState,
103
+ roomRef
104
+ } = useVoiceAgent();
105
+ // State management
106
+ const [configData, setConfigData] = useState(null);
107
+ const [isOpen, setIsOpen] = useState(false);
108
+ const [callDuration, setCallDuration] = useState(0);
109
+ const timerRef = useRef(null);
110
+ const lottieRef = useRef(null);
111
+
112
+ // Animation values
113
+ const isPressed = useSharedValue(false);
114
+ const offset = useSharedValue({
115
+ x: 0,
116
+ y: 0
117
+ });
118
+ const start = useSharedValue({
119
+ x: 0,
120
+ y: 0
121
+ });
122
+ const menuAnimation = useSharedValue(0);
123
+ const buttonWidth = useSharedValue(BUTTON_WIDTH);
124
+ const buttonScale = useSharedValue(1);
125
+ // Styles
126
+ const styles = createEmbedButtonStyles(defaultStyles);
127
+ const [isAutoOpen, setIsAutoOpen] = useState(false);
128
+ useEffect(() => {
129
+ const autoOpenTimer = setTimeout(() => {
130
+ if (!isOpen) {
131
+ console.log('autoOpenTimer', isOpen);
132
+ setIsAutoOpen(true);
133
+ }
134
+ }, 15000); // 15 seconds
135
+
136
+ return () => {
137
+ clearTimeout(autoOpenTimer);
138
+ };
139
+ }, [isOpen]);
140
+
141
+ /**
142
+ * Fetch agent configuration data
143
+ */
144
+ useEffect(() => {
145
+ const fetchAgentData = async () => {
146
+ try {
147
+ const data = await getAgentData('@config_data');
148
+ setConfigData(data?.ui_config);
149
+ } catch (error) {
150
+ console.error('Error retrieving agent data:', error);
151
+ }
152
+ };
153
+ fetchAgentData();
154
+ }, []);
155
+
156
+ /**
157
+ * Set up a timer to track call duration when connected
158
+ */
159
+ useEffect(() => {
160
+ if (connectionState === 'connected' && !timerRef.current) {
161
+ timerRef.current = setInterval(() => {
162
+ setCallDuration(prev => prev + 1);
163
+ }, 1000);
164
+ } else if (connectionState !== 'connected' && timerRef.current) {
165
+ clearInterval(timerRef.current);
166
+ timerRef.current = null;
167
+
168
+ // If we were previously connected and now disconnected, show an error
169
+ if (callDuration > 0) {
170
+ console.log('Call unexpectedly disconnected after', callDuration, 'seconds');
171
+ }
172
+ }
173
+ return () => {
174
+ if (timerRef.current) {
175
+ clearInterval(timerRef.current);
176
+ timerRef.current = null;
177
+ }
178
+ };
179
+ }, [connectionState, callDuration]);
180
+
181
+ /**
182
+ * Handle menu animation and button width transitions
183
+ */
184
+ useEffect(() => {
185
+ menuAnimation.value = withTiming(isOpen ? 0.8 : 0, {
186
+ duration: 300
187
+ });
188
+ buttonWidth.value = withTiming(isOpen ? EXPANDED_WIDTH : BUTTON_WIDTH);
189
+ }, [isOpen, menuAnimation, buttonWidth]);
190
+
191
+ // Add breathing animation when button is closed but isAutoOpen is true
192
+ useEffect(() => {
193
+ if (!isOpen && isAutoOpen) {
194
+ // Start breathing animation with faster speed
195
+ buttonScale.value = withRepeat(withSequence(withTiming(1.1, {
196
+ duration: 1500,
197
+ // Reduced from 1000ms to 600ms
198
+ easing: Easing.inOut(Easing.ease)
199
+ }), withTiming(1, {
200
+ duration: 1500,
201
+ // Reduced from 1000ms to 600ms
202
+ easing: Easing.inOut(Easing.ease)
203
+ })), -1,
204
+ // Infinite repeat
205
+ false // Don't reverse
206
+ );
207
+ } else {
208
+ // Reset animation
209
+ buttonScale.value = withTiming(1, {
210
+ duration: 300
211
+ });
212
+ }
213
+ }, [isOpen, isAutoOpen]);
214
+
215
+ /**
216
+ * Animated styles for the button
217
+ */
218
+ const animatedStyles = useAnimatedStyle(() => {
219
+ const maxX = SCREEN_WIDTH - (isOpen ? EXPANDED_WIDTH : BUTTON_WIDTH) - 35;
220
+ const clampedX = Math.min(Math.max(offset.value.x, -maxX), 0);
221
+ return {
222
+ width: buttonWidth.value,
223
+ height: BUTTON_HEIGHT,
224
+ transform: [{
225
+ translateX: clampedX
226
+ }, {
227
+ translateY: offset.value.y
228
+ }, {
229
+ scale: withSpring(isPressed.value ? 0.95 : buttonScale.value)
230
+ }],
231
+ justifyContent: isOpen ? 'space-between' : 'flex-start',
232
+ overflow: 'hidden'
233
+ };
234
+ });
235
+
236
+ /**
237
+ * Animated styles for the text
238
+ */
239
+ const animatedTextStyles = useAnimatedStyle(() => {
240
+ const maxX = SCREEN_WIDTH;
241
+ const clampedX = Math.min(Math.max(offset.value.x, -maxX), 0);
242
+ return {
243
+ transform: [{
244
+ translateX: clampedX
245
+ }, {
246
+ translateY: offset.value.y
247
+ }, {
248
+ scale: withSpring(isPressed.value ? 1 : 1)
249
+ }]
250
+ };
251
+ });
252
+
253
+ /**
254
+ * Pan gesture handler for drag functionality
255
+ */
256
+ const gesture = Gesture.Pan().onBegin(() => {
257
+ isPressed.value = true;
258
+ if (isAutoOpen) {
259
+ runOnJS(setIsAutoOpen)(false);
260
+ }
261
+ }).onUpdate(e => {
262
+ const maxX = SCREEN_WIDTH - (isOpen ? EXPANDED_WIDTH : BUTTON_WIDTH) - 0;
263
+ const newX = Math.min(Math.max(e.translationX + start.value.x, -maxX), 0);
264
+ const maxY = SCREEN_HEIGHT - 150;
265
+ const newY = Math.min(Math.max(e.translationY + start.value.y, -maxY), 0);
266
+ offset.value = {
267
+ x: newX,
268
+ y: newY
269
+ };
270
+ }).onEnd(() => {
271
+ start.value = {
272
+ x: offset.value.x,
273
+ y: offset.value.y
274
+ };
275
+ }).onFinalize(() => {
276
+ isPressed.value = false;
277
+ });
278
+
279
+ /**
280
+ * Handle button press events
281
+ */
282
+ const handlePress = () => {
283
+ console.log('handlePress', isOpen);
284
+ console.log('isAutoOpen', isAutoOpen);
285
+ // cleanup();
286
+ setIsOpen(!isOpen);
287
+ setIsAutoOpen(false);
288
+ };
289
+ const handleStartCall = async () => {
290
+ setCallDuration(0);
291
+ await initializeVoiceAgent();
292
+ };
293
+
294
+ /**
295
+ * Render the button icon/animation
296
+ */
297
+
298
+ const remoteSource = useMemo(() => ({
299
+ uri: configData?.icon_animation || AMPLIFY_ANIMATION
300
+ }), [configData?.icon_animation]);
301
+ const renderIcon = () => {
302
+ // When isAutoOpen is true, we don't play the Lottie animation
303
+ return /*#__PURE__*/_jsx(View, {
304
+ children: /*#__PURE__*/_jsx(LottieView, {
305
+ ref: lottieRef,
306
+ source: remoteSource,
307
+ autoPlay: true,
308
+ loop: true,
309
+ style: styles.iconImage,
310
+ enableMergePathsAndroidForKitKatAndAbove: true,
311
+ enableSafeModeAndroid: true
312
+ })
313
+ });
314
+ };
315
+ const handleConnected = () => {
316
+ console.log('Call connected');
317
+ };
318
+ const handleEndCall = async () => {
319
+ setIsOpen(false);
320
+ if (timerRef.current) {
321
+ clearInterval(timerRef.current);
322
+ timerRef.current = null;
323
+ }
324
+ setCallDuration(0);
325
+ await endCall();
326
+ };
327
+ const handleMicToggle = () => {
328
+ if (isMicMuted) {
329
+ unmuteMic();
330
+ } else {
331
+ muteMic();
332
+ }
333
+ };
334
+
335
+ // Format duration to MM:SS
336
+ const formatDuration = seconds => {
337
+ const minutes = Math.floor(seconds / 60);
338
+ const remainingSeconds = seconds % 60;
339
+ return `${minutes.toString().padStart(2, '0')}:${remainingSeconds.toString().padStart(2, '0')}`;
340
+ };
341
+
342
+ // Get the status text based on current state
343
+ const getStatusText = () => {
344
+ if (isLoading) {
345
+ return 'Connecting...';
346
+ } else if (tokenDetails?.token) {
347
+ return `${formatDuration(callDuration)}`;
348
+ } else {
349
+ return configData?.agent_type || 'Onboarding Agent';
350
+ }
351
+ };
352
+ if (!configData) return null;
353
+ return /*#__PURE__*/_jsxs(View, {
354
+ style: styles.container,
355
+ children: [isAutoOpen && !isOpen && /*#__PURE__*/_jsx(Animated.View, {
356
+ style: [animatedTextStyles, {
357
+ position: 'absolute',
358
+ borderRadius: 5,
359
+ paddingVertical: 2,
360
+ paddingHorizontal: 10,
361
+ backgroundColor: 'rgba(0, 0, 0, 0.5)',
362
+ bottom: BUTTON_HEIGHT + 40
363
+ // right: Math.abs(offset.value.x) + BUTTON_WIDTH + 0,
364
+ }],
365
+ children: /*#__PURE__*/_jsx(Text, {
366
+ style: {
367
+ color: 'white',
368
+ fontSize: 10,
369
+ fontWeight: '500'
370
+ },
371
+ children: configData?.popup_description || 'Revrag'
372
+ })
373
+ }), /*#__PURE__*/_jsx(GestureDetector, {
374
+ gesture: gesture,
375
+ children: /*#__PURE__*/_jsx(Animated.View, {
376
+ style: [styles.button, animatedStyles, styles.buttonContent, {
377
+ pointerEvents: 'auto'
378
+ }],
379
+ children: /*#__PURE__*/_jsxs(LinearGradient, {
380
+ colors: configData?.gradient || GRADIENT_COLORS,
381
+ start: {
382
+ x: 0,
383
+ y: 0
384
+ },
385
+ end: {
386
+ x: 1,
387
+ y: 0
388
+ },
389
+ style: [styles.linearGradient, {
390
+ width: '100%',
391
+ flexDirection: 'row',
392
+ alignItems: 'center',
393
+ paddingHorizontal: 0,
394
+ paddingLeft: 0,
395
+ paddingRight: isOpen ? 5 : 0
396
+ }],
397
+ angle: 0,
398
+ angleCenter: {
399
+ x: 0.5,
400
+ y: 0.5
401
+ },
402
+ children: [tokenDetails?.token && /*#__PURE__*/_jsx(Voice, {
403
+ url: tokenDetails?.server_url,
404
+ token: tokenDetails?.token,
405
+ onDisconnected: handleEndCall,
406
+ roomRef: roomRef,
407
+ onConnected: handleConnected
408
+ }), /*#__PURE__*/_jsx(View, {
409
+ style: [styles.rowContainer, {
410
+ flexShrink: 0,
411
+ width: BUTTON_WIDTH,
412
+ padding: 0,
413
+ margin: 0
414
+ }],
415
+ children: /*#__PURE__*/_jsx(TouchableOpacity, {
416
+ onPress: handlePress,
417
+ style: styles.pressable,
418
+ children: renderIcon()
419
+ })
420
+ }), isOpen && /*#__PURE__*/_jsxs(View, {
421
+ style: {
422
+ flex: 1,
423
+ flexDirection: 'row',
424
+ height: BUTTON_HEIGHT - 10,
425
+ marginLeft: 0,
426
+ marginRight: 0
427
+ },
428
+ children: [/*#__PURE__*/_jsxs(View, {
429
+ style: {
430
+ flex: 1,
431
+ justifyContent: 'center',
432
+ alignItems: 'flex-start',
433
+ paddingLeft: 8,
434
+ paddingRight: 4
435
+ },
436
+ children: [/*#__PURE__*/_jsx(Text, {
437
+ style: [styles.agentNameText, {
438
+ flexShrink: 1,
439
+ textAlign: 'left'
440
+ }],
441
+ children: configData?.agent_name || 'Revrag'
442
+ }), /*#__PURE__*/_jsx(Text, {
443
+ style: [styles.statusText, {
444
+ flexShrink: 1,
445
+ textAlign: 'left'
446
+ }],
447
+ children: getStatusText()
448
+ })]
449
+ }), /*#__PURE__*/_jsx(View, {
450
+ style: {
451
+ flex: 1,
452
+ justifyContent: 'center',
453
+ alignItems: 'center',
454
+ paddingHorizontal: 4
455
+ },
456
+ children: /*#__PURE__*/_jsx(WaveformVisualizer, {
457
+ roomRef: roomRef
458
+ })
459
+ }), /*#__PURE__*/_jsxs(View, {
460
+ style: {
461
+ flex: 1,
462
+ justifyContent: 'center',
463
+ alignItems: 'flex-end',
464
+ paddingLeft: 4,
465
+ paddingRight: 8
466
+ },
467
+ children: [!tokenDetails?.token && /*#__PURE__*/_jsx(TouchableOpacity, {
468
+ onPress: handleStartCall,
469
+ style: styles.startCallButton,
470
+ children: /*#__PURE__*/_jsx(Text, {
471
+ style: styles.startCallText,
472
+ children: configData?.start_call_text || 'Start Call'
473
+ })
474
+ }), tokenDetails?.token && /*#__PURE__*/_jsxs(View, {
475
+ style: [styles.buttonContainer],
476
+ children: [/*#__PURE__*/_jsx(TouchableOpacity, {
477
+ style: styles.muteButton,
478
+ onPress: handleMicToggle,
479
+ children: /*#__PURE__*/_jsx(Image, {
480
+ source: {
481
+ uri: isMicMuted ? MIC_OFF_ICON : MIC_ON_ICON
482
+ },
483
+ style: styles.buttonImage
484
+ })
485
+ }), /*#__PURE__*/_jsx(TouchableOpacity, {
486
+ onPress: handleEndCall,
487
+ style: styles.endCallButton,
488
+ children: /*#__PURE__*/_jsx(Image, {
489
+ source: {
490
+ uri: END_CALL_ICON
491
+ },
492
+ style: styles.buttonImage
493
+ })
494
+ })]
495
+ })]
496
+ })]
497
+ })]
498
+ })
499
+ })
500
+ })]
501
+ });
502
+ }
503
+
504
+ // Export default for easier imports
505
+ export default EmbedButton;
506
+ //# sourceMappingURL=EmbedButton.js.map
@@ -0,0 +1,127 @@
1
+ "use strict";
2
+
3
+ import { AudioSession, LiveKitRoom } from '@livekit/react-native';
4
+ import { useEffect, useRef, useState } from 'react';
5
+
6
+ // export interface VoiceProps {
7
+ // url: string;
8
+ // token: string;
9
+ // onDisconnected: (data: string) => void;
10
+ // onConnected: (data: string) => void;
11
+ // }
12
+ import { jsx as _jsx } from "react/jsx-runtime";
13
+ // Track audio session state globally to prevent multiple starts/stops
14
+ let audioSessionActive = false;
15
+ let audioSessionStarting = false;
16
+ let audioSessionStopping = false;
17
+
18
+ // Track connection to prevent unmounting while connected
19
+ let activeConnectionToken = null;
20
+ const Voice = props => {
21
+ const {
22
+ url,
23
+ token,
24
+ onDisconnected,
25
+ onConnected,
26
+ roomRef
27
+ } = props;
28
+ const [audioSessionStarted, setAudioSessionStarted] = useState(audioSessionActive);
29
+ const mountedRef = useRef(true);
30
+ const connectedRef = useRef(false);
31
+
32
+ // Start audio session safely - prevent multiple simultaneous starts
33
+ const startAudioSession = async () => {
34
+ if (audioSessionActive || audioSessionStarting) {
35
+ console.log('Audio session already active or starting, skipping');
36
+ return;
37
+ }
38
+ try {
39
+ audioSessionStarting = true;
40
+ console.log('Starting audio session');
41
+ await AudioSession.startAudioSession();
42
+ audioSessionActive = true;
43
+ if (mountedRef.current) {
44
+ setAudioSessionStarted(true);
45
+ }
46
+ } catch (err) {
47
+ console.error('Failed to start audio session:', err);
48
+ } finally {
49
+ audioSessionStarting = false;
50
+ }
51
+ };
52
+
53
+ // Stop audio session safely - prevent multiple simultaneous stops
54
+ const stopAudioSession = async () => {
55
+ if (!audioSessionActive || audioSessionStopping) {
56
+ console.log('Audio session already inactive or stopping, skipping');
57
+ return;
58
+ }
59
+ try {
60
+ audioSessionStopping = true;
61
+ console.log('Stopping audio session');
62
+ await AudioSession.stopAudioSession();
63
+ audioSessionActive = false;
64
+ } catch (err) {
65
+ console.error('Failed to stop audio session:', err);
66
+ } finally {
67
+ audioSessionStopping = false;
68
+ }
69
+ };
70
+
71
+ // Setup audio session
72
+ useEffect(() => {
73
+ mountedRef.current = true;
74
+ startAudioSession();
75
+ return () => {
76
+ mountedRef.current = false;
77
+
78
+ // IMPORTANT: Don't stop the audio session on unmount if there might be an active call
79
+ // This prevents audio session start/stop loops when components remount
80
+ if (roomRef.current?.state !== 'connected' && !connectedRef.current) {
81
+ stopAudioSession();
82
+ } else {
83
+ console.log('Skipping audio session stop because room is still connected');
84
+ }
85
+ };
86
+ }, []);
87
+
88
+ // Track connection state changes to avoid unmounting while connected
89
+ useEffect(() => {
90
+ if (token) {
91
+ activeConnectionToken = token;
92
+ }
93
+ return () => {
94
+ // Only clear token when unmounting with this specific token
95
+ if (activeConnectionToken === token) {
96
+ activeConnectionToken = null;
97
+ }
98
+ };
99
+ }, [token]);
100
+
101
+ // Only render LiveKitRoom when audio session is ready
102
+ if (!audioSessionStarted) {
103
+ return null;
104
+ }
105
+
106
+ // IMPORTANT: Never return empty fragment when connected!
107
+ // Instead, always render the LiveKitRoom component to maintain the connection
108
+ return /*#__PURE__*/_jsx(LiveKitRoom, {
109
+ serverUrl: url,
110
+ token: token,
111
+ screen: false,
112
+ audio: true,
113
+ onConnected: () => {
114
+ console.log('LiveKitRoom connected');
115
+ connectedRef.current = true;
116
+ onConnected('connected');
117
+ },
118
+ room: roomRef.current || undefined,
119
+ onDisconnected: () => {
120
+ console.log('LiveKitRoom disconnected');
121
+ connectedRef.current = false;
122
+ onDisconnected('disconnected');
123
+ }
124
+ });
125
+ };
126
+ export default Voice;
127
+ //# sourceMappingURL=EmbedVoice.js.map