agora-appbuilder-core 4.1.20 → 4.1.21

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 (89) hide show
  1. package/package.json +1 -1
  2. package/template/bridge/rtm/web/__tests__/storage.test.ts +130 -0
  3. package/template/bridge/rtm/web/index.ts +44 -19
  4. package/template/customization-api/customize.tsx +6 -0
  5. package/template/customization-api/index.ts +11 -0
  6. package/template/customization-api/typeDefinition.ts +6 -0
  7. package/template/defaultConfig.js +2 -2
  8. package/template/src/assets/live-reactions/1f440.gif +0 -0
  9. package/template/src/assets/live-reactions/1f44d_1f3fb.gif +0 -0
  10. package/template/src/assets/live-reactions/1f44d_1f3fc.gif +0 -0
  11. package/template/src/assets/live-reactions/1f44d_1f3fd.gif +0 -0
  12. package/template/src/assets/live-reactions/1f44d_1f3fe.gif +0 -0
  13. package/template/src/assets/live-reactions/1f44d_1f3ff.gif +0 -0
  14. package/template/src/assets/live-reactions/1f44f_1f3fb.gif +0 -0
  15. package/template/src/assets/live-reactions/1f44f_1f3fc.gif +0 -0
  16. package/template/src/assets/live-reactions/1f44f_1f3fd.gif +0 -0
  17. package/template/src/assets/live-reactions/1f44f_1f3fe.gif +0 -0
  18. package/template/src/assets/live-reactions/1f44f_1f3ff.gif +0 -0
  19. package/template/src/assets/live-reactions/1f4a1.gif +0 -0
  20. package/template/src/assets/live-reactions/1f4af.gif +0 -0
  21. package/template/src/assets/live-reactions/1f525.gif +0 -0
  22. package/template/src/assets/live-reactions/1f60e.gif +0 -0
  23. package/template/src/assets/live-reactions/1f643.gif +0 -0
  24. package/template/src/assets/live-reactions/1f64c.gif +0 -0
  25. package/template/src/assets/live-reactions/1f64c_1f3fb.gif +0 -0
  26. package/template/src/assets/live-reactions/1f64c_1f3fc.gif +0 -0
  27. package/template/src/assets/live-reactions/1f64c_1f3fd.gif +0 -0
  28. package/template/src/assets/live-reactions/1f64c_1f3fe.gif +0 -0
  29. package/template/src/assets/live-reactions/1f64c_1f3ff.gif +0 -0
  30. package/template/src/assets/live-reactions/1f64f.gif +0 -0
  31. package/template/src/assets/live-reactions/1f64f_1f3fb.gif +0 -0
  32. package/template/src/assets/live-reactions/1f64f_1f3fc.gif +0 -0
  33. package/template/src/assets/live-reactions/1f64f_1f3fd.gif +0 -0
  34. package/template/src/assets/live-reactions/1f64f_1f3fe.gif +0 -0
  35. package/template/src/assets/live-reactions/1f64f_1f3ff.gif +0 -0
  36. package/template/src/assets/live-reactions/1f680.gif +0 -0
  37. package/template/src/assets/live-reactions/2705.gif +0 -0
  38. package/template/src/assets/live-reactions/animated/1f440.json +1 -0
  39. package/template/src/assets/live-reactions/animated/1f44d_1f3fb.json +1 -0
  40. package/template/src/assets/live-reactions/animated/1f44d_1f3fc.json +1 -0
  41. package/template/src/assets/live-reactions/animated/1f44d_1f3fd.json +1 -0
  42. package/template/src/assets/live-reactions/animated/1f44d_1f3fe.json +1 -0
  43. package/template/src/assets/live-reactions/animated/1f44d_1f3ff.json +1 -0
  44. package/template/src/assets/live-reactions/animated/1f44f_1f3fb.json +1 -0
  45. package/template/src/assets/live-reactions/animated/1f44f_1f3fc.json +1 -0
  46. package/template/src/assets/live-reactions/animated/1f44f_1f3fd.json +1 -0
  47. package/template/src/assets/live-reactions/animated/1f44f_1f3fe.json +1 -0
  48. package/template/src/assets/live-reactions/animated/1f44f_1f3ff.json +1 -0
  49. package/template/src/assets/live-reactions/animated/1f4a1.json +1 -0
  50. package/template/src/assets/live-reactions/animated/1f4af.json +1 -0
  51. package/template/src/assets/live-reactions/animated/1f525.json +1 -0
  52. package/template/src/assets/live-reactions/animated/1f60e.json +1 -0
  53. package/template/src/assets/live-reactions/animated/1f643.json +1 -0
  54. package/template/src/assets/live-reactions/animated/1f64c.json +1 -0
  55. package/template/src/assets/live-reactions/animated/1f64c_1f3fb.json +1 -0
  56. package/template/src/assets/live-reactions/animated/1f64c_1f3fc.json +1 -0
  57. package/template/src/assets/live-reactions/animated/1f64c_1f3fd.json +1 -0
  58. package/template/src/assets/live-reactions/animated/1f64c_1f3fe.json +1 -0
  59. package/template/src/assets/live-reactions/animated/1f64c_1f3ff.json +1 -0
  60. package/template/src/assets/live-reactions/animated/1f64f.json +1 -0
  61. package/template/src/assets/live-reactions/animated/1f64f_1f3fb.json +1 -0
  62. package/template/src/assets/live-reactions/animated/1f64f_1f3fc.json +1 -0
  63. package/template/src/assets/live-reactions/animated/1f64f_1f3fd.json +1 -0
  64. package/template/src/assets/live-reactions/animated/1f64f_1f3fe.json +1 -0
  65. package/template/src/assets/live-reactions/animated/1f64f_1f3ff.json +1 -0
  66. package/template/src/assets/live-reactions/animated/1f680.json +1 -0
  67. package/template/src/assets/live-reactions/animated/2705.json +1 -0
  68. package/template/src/assets/live-reactions/custom/ty.png +0 -0
  69. package/template/src/components/RTMConfigure.tsx +15 -10
  70. package/template/src/components/StorageContext.tsx +3 -0
  71. package/template/src/components/reactions/LiveReactionBadge.tsx +20 -2
  72. package/template/src/components/reactions/LiveReactionButton.tsx +200 -44
  73. package/template/src/components/reactions/LiveReactionStageOverlay.native.tsx +34 -10
  74. package/template/src/components/reactions/LiveReactionStageOverlay.tsx +30 -7
  75. package/template/src/components/reactions/catalog.ts +241 -0
  76. package/template/src/components/useVideoCall.tsx +20 -2
  77. package/template/src/rtm/__tests__/isRemoteDeparture.test.ts +19 -0
  78. package/template/src/rtm/isRemoteDeparture.ts +4 -0
  79. package/template/src/subComponents/caption/__tests__/sttRequestBody.test.ts +61 -0
  80. package/template/src/subComponents/caption/__tests__/sttSessionId.native.test.ts +76 -0
  81. package/template/src/subComponents/caption/__tests__/sttSessionId.web.test.ts +190 -0
  82. package/template/src/subComponents/caption/__tests__/useSTTAPI.test.tsx +129 -0
  83. package/template/src/subComponents/caption/sttRequestBody.ts +54 -0
  84. package/template/src/subComponents/caption/sttSessionCoordinator.ts +315 -0
  85. package/template/src/subComponents/caption/sttSessionId.ts +21 -0
  86. package/template/src/subComponents/caption/sttSessionId.web.ts +21 -0
  87. package/template/src/subComponents/caption/useSTTAPI.tsx +13 -33
  88. package/template/src/utils/__tests__/useEndCall.test.tsx +282 -0
  89. package/template/src/utils/useEndCall.ts +63 -22
@@ -0,0 +1,21 @@
1
+ import RTMEngine from '../../rtm/RTMEngine';
2
+ import getUniqueID from '../../utils/getUniqueID';
3
+ import {createSTTSessionCoordinator} from './sttSessionCoordinator';
4
+
5
+ export {
6
+ createSTTSessionCoordinator,
7
+ STT_SESSION_ID_KEY,
8
+ } from './sttSessionCoordinator';
9
+
10
+ const coordinator = createSTTSessionCoordinator({
11
+ getClient: () => RTMEngine.getInstance().engine,
12
+ createId: getUniqueID,
13
+ wait: milliseconds =>
14
+ new Promise(resolve => setTimeout(resolve, milliseconds)),
15
+ });
16
+
17
+ export const ensureSTTSessionId = coordinator.ensureSTTSessionId;
18
+ export const isOnlyLocalRTMParticipant = coordinator.isOnlyLocalRTMParticipant;
19
+ export const clearSTTSessionIdIfLast = coordinator.clearSTTSessionIdIfLast;
20
+ export const cleanupSTTSessionOnEnd = coordinator.cleanupSTTSessionOnEnd;
21
+ export const resetSTTSessionIdCache = coordinator.resetSTTSessionIdCache;
@@ -1,10 +1,12 @@
1
1
  import React, {useContext} from 'react';
2
2
  import StorageContext from '../../components/StorageContext';
3
3
  import {useRoomInfo} from '../../components/room-info/useRoomInfo';
4
- import {LanguageTranslationConfig} from './useCaption';
4
+ import {type LanguageTranslationConfig} from './useCaption';
5
5
  import {PropsContext, useLocalUid} from '../../../agora-rn-uikit';
6
6
  import {logger, LogSource} from '../../logger/AppBuilderLogger';
7
7
  import getUniqueID from '../../utils/getUniqueID';
8
+ import {buildSTTRequestBody, type STTMethod} from './sttRequestBody';
9
+ import {ensureSTTSessionId} from './sttSessionId';
8
10
 
9
11
  export interface STTAPIResponse {
10
12
  success: boolean;
@@ -57,7 +59,7 @@ const useSTTAPI = (): IuseSTTAPI => {
57
59
  }, [rtcProps]);
58
60
 
59
61
  const apiCall = async (
60
- method: 'startv7' | 'update' | 'stopv7',
62
+ method: STTMethod,
61
63
  botUid: number,
62
64
  translationConfig?: LanguageTranslationConfig,
63
65
  ): Promise<STTAPIResponse> => {
@@ -68,14 +70,19 @@ const useSTTAPI = (): IuseSTTAPI => {
68
70
  // Calculate which user this bot belongs to
69
71
  const ownerUid = botUid - 900000000;
70
72
 
71
- let requestBody: any = {
73
+ const requestBody = await buildSTTRequestBody({
74
+ method,
75
+ botUid,
72
76
  passphrase:
73
77
  roomIdRef?.current?.host || roomIdRef?.current?.attendee || '',
74
- dataStream_uid: botUid,
75
- encryption_mode: $config.ENCRYPTION_ENABLED
78
+ encryptionMode: $config.ENCRYPTION_ENABLED
76
79
  ? rtcPropsRef?.current.encryption.mode
77
80
  : null,
78
- };
81
+ localUid: localUidRef.current,
82
+ channelName: rtcPropsRef?.current?.channel || '',
83
+ translationConfig,
84
+ resolveSessionId: ensureSTTSessionId,
85
+ });
79
86
 
80
87
  console.log(
81
88
  `[STT_BOT_SUBSCRIPTION] ${method.toUpperCase()} - Bot UID: ${botUid} will subscribe to User UID: ${ownerUid}`,
@@ -86,33 +93,6 @@ const useSTTAPI = (): IuseSTTAPI => {
86
93
  translationConfig,
87
94
  },
88
95
  );
89
- // Add translate_config only for start/update methods
90
- if (translationConfig?.source?.[0]) {
91
- requestBody.lang = translationConfig.source;
92
- // Sanitize payload: remove source language from targets to avoid API errors
93
- const sanitizedTargets =
94
- translationConfig?.targets?.filter(
95
- target => target !== translationConfig?.source[0],
96
- ) || [];
97
- const shouldTranslate = sanitizedTargets.length > 0;
98
- // Add translate_config payload only if targets exist
99
- if (shouldTranslate) {
100
- requestBody.translate_config = [
101
- {
102
- source_lang: translationConfig.source[0],
103
- target_lang: sanitizedTargets,
104
- },
105
- ];
106
- if (method === 'update') {
107
- requestBody.translate = true;
108
- }
109
- } else if (method === 'update') {
110
- // If method is update and no targets are passed
111
- requestBody.translate = false;
112
- }
113
- requestBody.subscribeAudioUids = [`${localUidRef.current}`];
114
- }
115
-
116
96
  const response = await fetch(`${STT_API_URL}/${method}`, {
117
97
  method: 'POST',
118
98
  headers: {
@@ -0,0 +1,282 @@
1
+ import React from 'react';
2
+ import TestRenderer, {act} from 'react-test-renderer';
3
+ import {afterEach, beforeEach, describe, expect, it, jest} from '@jest/globals';
4
+
5
+ jest.mock('customization-implementation', () => ({
6
+ useCustomization: () => undefined,
7
+ }));
8
+
9
+ jest.mock('customization-api', () => ({
10
+ useCaption: jest.fn(),
11
+ useContent: jest.fn(),
12
+ useRoomInfo: jest.fn(),
13
+ }));
14
+
15
+ jest.mock('../../../agora-rn-uikit', () => {
16
+ const ReactModule = require('react');
17
+ return {
18
+ PropsContext: ReactModule.createContext({}),
19
+ DispatchContext: ReactModule.createContext({}),
20
+ useLocalUid: jest.fn(() => 123),
21
+ };
22
+ });
23
+
24
+ jest.mock('../../components/Router', () => ({
25
+ useHistory: () => ({}),
26
+ }));
27
+
28
+ jest.mock('../../subComponents/LocalEndCall', () => ({
29
+ stopForegroundService: jest.fn(),
30
+ }));
31
+
32
+ jest.mock('../../rtm/RTMEngine', () => {
33
+ const unsubscribe = jest.fn();
34
+ return {
35
+ __esModule: true,
36
+ default: {
37
+ getInstance: () => ({engine: {unsubscribe}}),
38
+ },
39
+ };
40
+ });
41
+
42
+ jest.mock('../../auth/config', () => ({ENABLE_AUTH: true}));
43
+
44
+ jest.mock('../../auth/AuthProvider', () => ({
45
+ useAuth: () => ({authLogin: jest.fn()}),
46
+ }));
47
+
48
+ jest.mock('../../components/chat/chatConfigure', () => ({
49
+ useChatConfigure: () => ({deleteChatUser: jest.fn()}),
50
+ }));
51
+
52
+ jest.mock('../common', () => ({
53
+ isWebInternal: jest.fn(),
54
+ }));
55
+
56
+ jest.mock('../../subComponents/caption/sttSessionId', () => ({
57
+ cleanupSTTSessionOnEnd: jest.fn(),
58
+ }));
59
+
60
+ import {useCaption, useContent, useRoomInfo} from 'customization-api';
61
+ import {DispatchContext, PropsContext} from '../../../agora-rn-uikit';
62
+ import RTMEngine from '../../rtm/RTMEngine';
63
+ import {cleanupSTTSessionOnEnd} from '../../subComponents/caption/sttSessionId';
64
+ import {isWebInternal} from '../common';
65
+ import useEndCall from '../useEndCall';
66
+
67
+ const mockUseCaption = useCaption as jest.Mock<any>;
68
+ const mockUseContent = useContent as jest.Mock<any>;
69
+ const mockUseRoomInfo = useRoomInfo as jest.Mock<any>;
70
+ const mockCleanupSTTSessionOnEnd = cleanupSTTSessionOnEnd as jest.Mock<any>;
71
+ const mockIsWebInternal = isWebInternal as jest.Mock<any>;
72
+ const mockUnsubscribe = RTMEngine.getInstance().engine
73
+ .unsubscribe as jest.Mock<any>;
74
+
75
+ const renderHook = (dispatch: (action: any) => void) => {
76
+ let endCall!: ReturnType<typeof useEndCall>;
77
+ let renderer!: TestRenderer.ReactTestRenderer;
78
+
79
+ act(() => {
80
+ renderer = TestRenderer.create(
81
+ <PropsContext.Provider value={{rtcProps: {channel: 'room'}} as any}>
82
+ <DispatchContext.Provider value={{dispatch} as any}>
83
+ {React.createElement(() => {
84
+ endCall = useEndCall();
85
+ return null;
86
+ })}
87
+ </DispatchContext.Provider>
88
+ </PropsContext.Provider>,
89
+ );
90
+ });
91
+
92
+ return {endCall, renderer};
93
+ };
94
+
95
+ describe('useEndCall STT session cleanup', () => {
96
+ const mockStopSTT = jest.fn<any>();
97
+
98
+ beforeEach(() => {
99
+ jest.useFakeTimers();
100
+ jest.clearAllMocks();
101
+ mockUseCaption.mockReturnValue({
102
+ isSTTActive: true,
103
+ stopSTTBotSession: mockStopSTT,
104
+ });
105
+ mockUseContent.mockReturnValue({
106
+ defaultContent: {
107
+ 123: {
108
+ type: 'rtc',
109
+ isHost: 'true',
110
+ offline: false,
111
+ },
112
+ },
113
+ });
114
+ mockUseRoomInfo.mockReturnValue({data: {isHost: true}});
115
+ mockStopSTT.mockResolvedValue(undefined);
116
+ mockUnsubscribe.mockResolvedValue(undefined);
117
+ });
118
+
119
+ afterEach(() => {
120
+ jest.useRealTimers();
121
+ });
122
+
123
+ it('awaits web cleanup before RTM unsubscribe and end-call dispatch', async () => {
124
+ const calls: string[] = [];
125
+ mockIsWebInternal.mockReturnValue(true);
126
+ mockStopSTT.mockImplementation(async () => {
127
+ calls.push('stop');
128
+ });
129
+ mockCleanupSTTSessionOnEnd.mockImplementation(
130
+ async (_channel, _uid, _active, stop) => {
131
+ calls.push('cleanup-start');
132
+ await stop();
133
+ calls.push('cleanup-end');
134
+ },
135
+ );
136
+ mockUnsubscribe.mockImplementation(async () => {
137
+ calls.push('unsubscribe');
138
+ });
139
+ const {endCall, renderer} = renderHook(() => calls.push('dispatch'));
140
+
141
+ await act(async () => {
142
+ await endCall();
143
+ });
144
+
145
+ expect(mockCleanupSTTSessionOnEnd).toHaveBeenCalledWith(
146
+ 'room',
147
+ '123',
148
+ true,
149
+ mockStopSTT,
150
+ );
151
+ expect(calls).toEqual([
152
+ 'cleanup-start',
153
+ 'stop',
154
+ 'cleanup-end',
155
+ 'unsubscribe',
156
+ ]);
157
+
158
+ act(() => jest.runOnlyPendingTimers());
159
+ expect(calls).toEqual([
160
+ 'cleanup-start',
161
+ 'stop',
162
+ 'cleanup-end',
163
+ 'unsubscribe',
164
+ 'dispatch',
165
+ ]);
166
+ renderer.unmount();
167
+ });
168
+
169
+ it('awaits native cleanup before RTM unsubscribe and end-call dispatch', async () => {
170
+ const calls: string[] = [];
171
+ mockIsWebInternal.mockReturnValue(false);
172
+ mockStopSTT.mockImplementation(async () => {
173
+ calls.push('stop');
174
+ });
175
+ mockCleanupSTTSessionOnEnd.mockImplementation(
176
+ async (_channel, _uid, _active, stop) => {
177
+ calls.push('cleanup-start');
178
+ await stop();
179
+ calls.push('cleanup-end');
180
+ },
181
+ );
182
+ mockUnsubscribe.mockImplementation(async () => {
183
+ calls.push('unsubscribe');
184
+ });
185
+ const {endCall, renderer} = renderHook(() => calls.push('dispatch'));
186
+
187
+ await act(async () => {
188
+ await endCall();
189
+ });
190
+
191
+ expect(mockCleanupSTTSessionOnEnd).toHaveBeenCalledWith(
192
+ 'room',
193
+ '123',
194
+ true,
195
+ expect.any(Function),
196
+ );
197
+ expect(calls).toEqual([
198
+ 'cleanup-start',
199
+ 'stop',
200
+ 'cleanup-end',
201
+ 'unsubscribe',
202
+ ]);
203
+
204
+ act(() => jest.runOnlyPendingTimers());
205
+ expect(calls).toEqual([
206
+ 'cleanup-start',
207
+ 'stop',
208
+ 'cleanup-end',
209
+ 'unsubscribe',
210
+ 'dispatch',
211
+ ]);
212
+ renderer.unmount();
213
+ });
214
+
215
+ it('falls back to the native last-host stop when RTM cleanup fails', async () => {
216
+ const calls: string[] = [];
217
+ mockIsWebInternal.mockReturnValue(false);
218
+ jest.spyOn(console, 'error').mockImplementation(() => {});
219
+ mockCleanupSTTSessionOnEnd.mockImplementation(async () => {
220
+ calls.push('cleanup');
221
+ throw new Error('RTM unavailable');
222
+ });
223
+ mockStopSTT.mockImplementation(async () => {
224
+ calls.push('stop');
225
+ });
226
+ mockUnsubscribe.mockImplementation(async () => {
227
+ calls.push('unsubscribe');
228
+ });
229
+ const {endCall, renderer} = renderHook(() => calls.push('dispatch'));
230
+
231
+ await act(async () => {
232
+ await endCall();
233
+ });
234
+
235
+ expect(mockStopSTT).toHaveBeenCalledTimes(1);
236
+ expect(calls).toEqual(['cleanup', 'stop', 'unsubscribe']);
237
+
238
+ act(() => jest.runOnlyPendingTimers());
239
+ expect(calls).toEqual(['cleanup', 'stop', 'unsubscribe', 'dispatch']);
240
+ renderer.unmount();
241
+ });
242
+
243
+ it('does not stop native STT twice when metadata removal fails after stop', async () => {
244
+ mockIsWebInternal.mockReturnValue(false);
245
+ jest.spyOn(console, 'error').mockImplementation(() => {});
246
+ mockCleanupSTTSessionOnEnd.mockImplementation(
247
+ async (_channel, _uid, _active, stop) => {
248
+ await stop();
249
+ throw new Error('metadata removal failed');
250
+ },
251
+ );
252
+ const {endCall, renderer} = renderHook(() => {});
253
+
254
+ await act(async () => {
255
+ await endCall();
256
+ });
257
+
258
+ expect(mockStopSTT).toHaveBeenCalledTimes(1);
259
+ renderer.unmount();
260
+ });
261
+
262
+ it('continues native end-call when RTM cleanup and fallback stop both fail', async () => {
263
+ const calls: string[] = [];
264
+ mockIsWebInternal.mockReturnValue(false);
265
+ jest.spyOn(console, 'error').mockImplementation(() => {});
266
+ mockCleanupSTTSessionOnEnd.mockRejectedValue(new Error('RTM unavailable'));
267
+ mockStopSTT.mockRejectedValue(new Error('stop unavailable'));
268
+ mockUnsubscribe.mockImplementation(async () => {
269
+ calls.push('unsubscribe');
270
+ });
271
+ const {endCall, renderer} = renderHook(() => calls.push('dispatch'));
272
+
273
+ await act(async () => {
274
+ await endCall();
275
+ });
276
+
277
+ expect(calls).toEqual(['unsubscribe']);
278
+ act(() => jest.runOnlyPendingTimers());
279
+ expect(calls).toEqual(['unsubscribe', 'dispatch']);
280
+ renderer.unmount();
281
+ });
282
+ });
@@ -1,17 +1,15 @@
1
1
  import {useContext} from 'react';
2
2
  import {useCustomization} from 'customization-implementation';
3
- import {
4
- useCaption,
5
- useContent,
6
- useRoomInfo,
7
- } from 'customization-api';
8
- import {PropsContext, DispatchContext} from '../../agora-rn-uikit';
3
+ import {useCaption, useContent, useRoomInfo} from 'customization-api';
4
+ import {PropsContext, DispatchContext, useLocalUid} from '../../agora-rn-uikit';
9
5
  import {useHistory} from '../components/Router';
10
6
  import {stopForegroundService} from '../subComponents/LocalEndCall';
11
7
  import RTMEngine from '../rtm/RTMEngine';
12
8
  import {ENABLE_AUTH} from '../auth/config';
13
9
  import {useAuth} from '../auth/AuthProvider';
14
10
  import {useChatConfigure} from '../components/chat/chatConfigure';
11
+ import {isWebInternal} from './common';
12
+ import {cleanupSTTSessionOnEnd} from '../subComponents/caption/sttSessionId';
15
13
 
16
14
  const useEndCall = () => {
17
15
  const history = useHistory();
@@ -25,6 +23,7 @@ const useEndCall = () => {
25
23
 
26
24
  const {rtcProps} = useContext(PropsContext);
27
25
  const {dispatch} = useContext(DispatchContext);
26
+ const localUid = useLocalUid();
28
27
 
29
28
  const beforeEndCall = useCustomization(
30
29
  data =>
@@ -43,31 +42,73 @@ const useEndCall = () => {
43
42
  console.log('debugging error on beforeEndCall', error);
44
43
  }
45
44
 
46
- setTimeout(() => {
47
- dispatch({
48
- type: 'EndCall',
49
- value: [],
45
+ const scheduleEndCall = () => {
46
+ setTimeout(() => {
47
+ dispatch({
48
+ type: 'EndCall',
49
+ value: [],
50
+ });
50
51
  });
51
- });
52
+ };
53
+ const isWebCall = isWebInternal();
54
+
52
55
  // stopping foreground servie on end call
53
56
  stopForegroundService();
54
- // stopping STT on call end,if only last user is remaining in call
55
- const usersInCall = Object.entries(defaultContent).filter(
56
- item =>
57
- item[1].type === 'rtc' && item[1].isHost === 'true' && !item[1].offline,
58
- );
59
- if (usersInCall.length === 1 && isSTTActive) {
60
- console.log('Stopping stt api as only one host is in the call');
61
- stopSTTBotSession().catch(error => {
62
- console.log('Error stopping stt', error);
63
- });
57
+
58
+ let nativeStopPromise: Promise<void> | undefined;
59
+ const stopNativeSTTOnce = (): Promise<void> => {
60
+ if (!nativeStopPromise) {
61
+ nativeStopPromise = stopSTTBotSession().catch(error => {
62
+ nativeStopPromise = undefined;
63
+ throw error;
64
+ });
65
+ }
66
+ return nativeStopPromise;
67
+ };
68
+ const stopSTT = isWebCall ? stopSTTBotSession : stopNativeSTTOnce;
69
+
70
+ try {
71
+ await cleanupSTTSessionOnEnd(
72
+ rtcProps.channel,
73
+ String(localUid),
74
+ isSTTActive,
75
+ stopSTT,
76
+ );
77
+ } catch (error) {
78
+ console.error(
79
+ `Failed to clean up the ${isWebCall ? 'web' : 'native'} STT session ID`,
80
+ error,
81
+ );
82
+ if (!isWebCall && isSTTActive) {
83
+ const usersInCall = Object.entries(defaultContent).filter(
84
+ item =>
85
+ item[1].type === 'rtc' &&
86
+ item[1].isHost === 'true' &&
87
+ !item[1].offline,
88
+ );
89
+ if (usersInCall.length === 1) {
90
+ try {
91
+ await stopNativeSTTOnce();
92
+ } catch (stopError) {
93
+ console.error(
94
+ 'Failed to stop native STT during fallback',
95
+ stopError,
96
+ );
97
+ }
98
+ }
99
+ }
64
100
  }
65
101
 
66
102
  // removing user from chat server
67
103
  if ($config.CHAT) {
68
104
  deleteChatUser();
69
105
  }
70
- RTMEngine.getInstance().engine.unsubscribe(rtcProps.channel);
106
+ try {
107
+ await RTMEngine.getInstance().engine.unsubscribe(rtcProps.channel);
108
+ } catch (error) {
109
+ console.error('Failed to unsubscribe from the RTM channel', error);
110
+ }
111
+ scheduleEndCall();
71
112
  if (!ENABLE_AUTH) {
72
113
  // await authLogout();
73
114
  await authLogin();