@telnyx/react-voice-commons-sdk 0.1.2 → 0.1.3
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/CHANGELOG.md +42 -0
- package/TelnyxVoiceCommons.podspec +31 -31
- package/ios/CallKitBridge.m +43 -43
- package/ios/CallKitBridge.swift +874 -879
- package/ios/VoicePnBridge.m +30 -30
- package/ios/VoicePnBridge.swift +86 -86
- package/lib/callkit/callkit-coordinator.d.ts +110 -117
- package/lib/callkit/callkit-coordinator.js +664 -727
- package/lib/callkit/callkit.d.ts +41 -41
- package/lib/callkit/callkit.js +252 -242
- package/lib/callkit/index.js +15 -47
- package/lib/callkit/use-callkit.d.ts +19 -19
- package/lib/callkit/use-callkit.js +270 -310
- package/lib/context/TelnyxVoiceContext.d.ts +9 -9
- package/lib/context/TelnyxVoiceContext.js +10 -13
- package/lib/hooks/use-callkit-coordinator.d.ts +9 -17
- package/lib/hooks/use-callkit-coordinator.js +45 -50
- package/lib/hooks/useAppReadyNotifier.js +13 -15
- package/lib/hooks/useAppStateHandler.d.ts +6 -11
- package/lib/hooks/useAppStateHandler.js +95 -110
- package/lib/hooks/useNetworkStateHandler.d.ts +0 -0
- package/lib/hooks/useNetworkStateHandler.js +0 -0
- package/lib/index.d.ts +3 -21
- package/lib/index.js +50 -201
- package/lib/internal/CallKitHandler.d.ts +6 -6
- package/lib/internal/CallKitHandler.js +96 -104
- package/lib/internal/callkit-manager.d.ts +57 -57
- package/lib/internal/callkit-manager.js +299 -316
- package/lib/internal/calls/call-state-controller.d.ts +73 -86
- package/lib/internal/calls/call-state-controller.js +263 -307
- package/lib/internal/session/session-manager.d.ts +71 -75
- package/lib/internal/session/session-manager.js +360 -424
- package/lib/internal/user-defaults-helpers.js +49 -39
- package/lib/internal/voice-pn-bridge.d.ts +114 -12
- package/lib/internal/voice-pn-bridge.js +212 -5
- package/lib/models/call-state.d.ts +46 -44
- package/lib/models/call-state.js +70 -68
- package/lib/models/call.d.ts +161 -133
- package/lib/models/call.js +454 -382
- package/lib/models/config.d.ts +11 -18
- package/lib/models/config.js +37 -35
- package/lib/models/connection-state.d.ts +10 -10
- package/lib/models/connection-state.js +16 -16
- package/lib/telnyx-voice-app.d.ts +28 -28
- package/lib/telnyx-voice-app.js +463 -481
- package/lib/telnyx-voip-client.d.ts +167 -167
- package/lib/telnyx-voip-client.js +385 -390
- package/package.json +11 -4
- package/src/callkit/callkit-coordinator.ts +18 -34
- package/src/hooks/useNetworkStateHandler.ts +0 -0
- package/src/internal/calls/call-state-controller.ts +81 -58
- package/src/internal/session/session-manager.ts +42 -26
- package/src/internal/voice-pn-bridge.ts +250 -2
- package/src/models/call-state.ts +8 -1
- package/src/models/call.ts +119 -5
- package/src/telnyx-voice-app.tsx +87 -40
- package/src/telnyx-voip-client.ts +15 -3
- package/src/types/telnyx-sdk.d.ts +16 -2
|
@@ -1,4 +1,10 @@
|
|
|
1
|
-
import { NativeModules } from 'react-native';
|
|
1
|
+
import { NativeModules, DeviceEventEmitter, EmitterSubscription } from 'react-native';
|
|
2
|
+
|
|
3
|
+
export interface CallActionEvent {
|
|
4
|
+
action: string;
|
|
5
|
+
callId?: string;
|
|
6
|
+
timestamp: number;
|
|
7
|
+
}
|
|
2
8
|
|
|
3
9
|
export interface VoicePnBridgeInterface {
|
|
4
10
|
getPendingPushAction(): Promise<{
|
|
@@ -7,6 +13,25 @@ export interface VoicePnBridgeInterface {
|
|
|
7
13
|
}>;
|
|
8
14
|
setPendingPushAction(action: string, metadata: string): Promise<boolean>;
|
|
9
15
|
clearPendingPushAction(): Promise<boolean>;
|
|
16
|
+
|
|
17
|
+
// Call action methods (reliable @ReactMethod pattern)
|
|
18
|
+
getPendingCallAction(): Promise<{
|
|
19
|
+
action: string | null;
|
|
20
|
+
callId: string | null;
|
|
21
|
+
timestamp: number | null;
|
|
22
|
+
}>;
|
|
23
|
+
clearPendingCallAction(): Promise<boolean>;
|
|
24
|
+
|
|
25
|
+
// Call control methods (Android specific)
|
|
26
|
+
endCall(callId: string | null): Promise<boolean>;
|
|
27
|
+
showOngoingCallNotification(
|
|
28
|
+
callerName: string | null,
|
|
29
|
+
callerNumber: string | null,
|
|
30
|
+
callId: string | null
|
|
31
|
+
): Promise<boolean>;
|
|
32
|
+
hideOngoingCallNotification(): Promise<boolean>;
|
|
33
|
+
hideIncomingCallNotification(): Promise<boolean>;
|
|
34
|
+
|
|
10
35
|
// Additional UserDefaults methods
|
|
11
36
|
getVoipToken(): Promise<string | null>;
|
|
12
37
|
getPendingVoipPush(): Promise<string | null>;
|
|
@@ -15,4 +40,227 @@ export interface VoicePnBridgeInterface {
|
|
|
15
40
|
clearPendingVoipAction(): Promise<boolean>;
|
|
16
41
|
}
|
|
17
42
|
|
|
18
|
-
|
|
43
|
+
const NativeBridge: VoicePnBridgeInterface = NativeModules.VoicePnBridge;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Enhanced VoicePnBridge with call control and event handling capabilities
|
|
47
|
+
*/
|
|
48
|
+
export class VoicePnBridge {
|
|
49
|
+
/**
|
|
50
|
+
* Get any pending push notification action from native side
|
|
51
|
+
*/
|
|
52
|
+
static async getPendingPushAction(): Promise<{ action?: string; metadata?: string }> {
|
|
53
|
+
try {
|
|
54
|
+
const result = await NativeBridge.getPendingPushAction();
|
|
55
|
+
return {
|
|
56
|
+
action: result?.action || undefined,
|
|
57
|
+
metadata: result?.metadata || undefined,
|
|
58
|
+
};
|
|
59
|
+
} catch (error) {
|
|
60
|
+
console.error('VoicePnBridge: Error getting pending push action:', error);
|
|
61
|
+
return {};
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Set a pending push notification action to native side
|
|
67
|
+
*/
|
|
68
|
+
static async setPendingPushAction(action: string, metadata: string): Promise<boolean> {
|
|
69
|
+
try {
|
|
70
|
+
return await NativeBridge.setPendingPushAction(action, metadata);
|
|
71
|
+
} catch (error) {
|
|
72
|
+
console.error('VoicePnBridge: Error setting pending push action:', error);
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Get any pending call action from native side (reliable polling pattern)
|
|
79
|
+
*/
|
|
80
|
+
static async getPendingCallAction(): Promise<{
|
|
81
|
+
action?: string;
|
|
82
|
+
callId?: string;
|
|
83
|
+
timestamp?: number;
|
|
84
|
+
}> {
|
|
85
|
+
try {
|
|
86
|
+
const result = await NativeBridge.getPendingCallAction();
|
|
87
|
+
return {
|
|
88
|
+
action: result?.action || undefined,
|
|
89
|
+
callId: result?.callId || undefined,
|
|
90
|
+
timestamp: result?.timestamp || undefined,
|
|
91
|
+
};
|
|
92
|
+
} catch (error) {
|
|
93
|
+
console.error('VoicePnBridge: Error getting pending call action:', error);
|
|
94
|
+
return {};
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Clear any pending call action
|
|
100
|
+
*/
|
|
101
|
+
static async clearPendingCallAction(): Promise<boolean> {
|
|
102
|
+
try {
|
|
103
|
+
return await NativeBridge.clearPendingCallAction();
|
|
104
|
+
} catch (error) {
|
|
105
|
+
console.error('VoicePnBridge: Error clearing pending call action:', error);
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Clear any pending push notification action
|
|
112
|
+
*/
|
|
113
|
+
static async clearPendingPushAction(): Promise<boolean> {
|
|
114
|
+
try {
|
|
115
|
+
return await NativeBridge.clearPendingPushAction();
|
|
116
|
+
} catch (error) {
|
|
117
|
+
console.error('VoicePnBridge: Error clearing pending push action:', error);
|
|
118
|
+
return false;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* React Native → Android: End/hang up the current call
|
|
124
|
+
* This will hide the ongoing call notification and notify the native side
|
|
125
|
+
*/
|
|
126
|
+
static async endCall(callId?: string): Promise<boolean> {
|
|
127
|
+
try {
|
|
128
|
+
return await NativeBridge.endCall(callId || null);
|
|
129
|
+
} catch (error) {
|
|
130
|
+
console.error('VoicePnBridge: Error ending call:', error);
|
|
131
|
+
return false;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* React Native → Android: Show ongoing call notification to keep app alive
|
|
137
|
+
* Should be called when a call becomes active to prevent background termination
|
|
138
|
+
*/
|
|
139
|
+
static async showOngoingCallNotification(
|
|
140
|
+
callerName?: string,
|
|
141
|
+
callerNumber?: string,
|
|
142
|
+
callId?: string
|
|
143
|
+
): Promise<boolean> {
|
|
144
|
+
try {
|
|
145
|
+
return await NativeBridge.showOngoingCallNotification(
|
|
146
|
+
callerName || null,
|
|
147
|
+
callerNumber || null,
|
|
148
|
+
callId || null
|
|
149
|
+
);
|
|
150
|
+
} catch (error) {
|
|
151
|
+
console.error('VoicePnBridge: Error showing ongoing call notification:', error);
|
|
152
|
+
return false;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* React Native → Android: Hide ongoing call notification
|
|
158
|
+
* Should be called when a call ends to clean up notifications
|
|
159
|
+
*/
|
|
160
|
+
static async hideOngoingCallNotification(): Promise<boolean> {
|
|
161
|
+
try {
|
|
162
|
+
return await NativeBridge.hideOngoingCallNotification();
|
|
163
|
+
} catch (error) {
|
|
164
|
+
console.error('VoicePnBridge: Error hiding ongoing call notification:', error);
|
|
165
|
+
return false;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* React Native → Android: Hide incoming call notification
|
|
171
|
+
* Useful for dismissing notifications when call is answered/rejected in app
|
|
172
|
+
*/
|
|
173
|
+
static async hideIncomingCallNotification(): Promise<boolean> {
|
|
174
|
+
try {
|
|
175
|
+
return await NativeBridge.hideIncomingCallNotification();
|
|
176
|
+
} catch (error) {
|
|
177
|
+
console.error('VoicePnBridge: Error hiding incoming call notification:', error);
|
|
178
|
+
return false;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Get VoIP token from native storage
|
|
184
|
+
*/
|
|
185
|
+
static async getVoipToken(): Promise<string | null> {
|
|
186
|
+
try {
|
|
187
|
+
return await NativeBridge.getVoipToken();
|
|
188
|
+
} catch (error) {
|
|
189
|
+
console.error('VoicePnBridge: Error getting VoIP token:', error);
|
|
190
|
+
return null;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Get pending VoIP push from native storage
|
|
196
|
+
*/
|
|
197
|
+
static async getPendingVoipPush(): Promise<string | null> {
|
|
198
|
+
try {
|
|
199
|
+
return await NativeBridge.getPendingVoipPush();
|
|
200
|
+
} catch (error) {
|
|
201
|
+
console.error('VoicePnBridge: Error getting pending VoIP push:', error);
|
|
202
|
+
return null;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Clear pending VoIP push from native storage
|
|
208
|
+
*/
|
|
209
|
+
static async clearPendingVoipPush(): Promise<boolean> {
|
|
210
|
+
try {
|
|
211
|
+
return await NativeBridge.clearPendingVoipPush();
|
|
212
|
+
} catch (error) {
|
|
213
|
+
console.error('VoicePnBridge: Error clearing pending VoIP push:', error);
|
|
214
|
+
return false;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Get pending VoIP action from native storage
|
|
220
|
+
*/
|
|
221
|
+
static async getPendingVoipAction(): Promise<string | null> {
|
|
222
|
+
try {
|
|
223
|
+
return await NativeBridge.getPendingVoipAction();
|
|
224
|
+
} catch (error) {
|
|
225
|
+
console.error('VoicePnBridge: Error getting pending VoIP action:', error);
|
|
226
|
+
return null;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Clear pending VoIP action from native storage
|
|
232
|
+
*/
|
|
233
|
+
static async clearPendingVoipAction(): Promise<boolean> {
|
|
234
|
+
try {
|
|
235
|
+
return await NativeBridge.clearPendingVoipAction();
|
|
236
|
+
} catch (error) {
|
|
237
|
+
console.error('VoicePnBridge: Error clearing pending VoIP action:', error);
|
|
238
|
+
return false;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Android → React Native: Listen for immediate call action events from notification buttons
|
|
244
|
+
* Use this for active calls where immediate response is needed (e.g., ending ongoing calls)
|
|
245
|
+
*/
|
|
246
|
+
static addCallActionListener(listener: (event: CallActionEvent) => void): EmitterSubscription {
|
|
247
|
+
return DeviceEventEmitter.addListener('TelnyxCallAction', listener);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Remove call action listener
|
|
252
|
+
*/
|
|
253
|
+
static removeCallActionListener(subscription: EmitterSubscription): void {
|
|
254
|
+
subscription.remove();
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Remove all call action listeners
|
|
259
|
+
*/
|
|
260
|
+
static removeAllCallActionListeners(): void {
|
|
261
|
+
DeviceEventEmitter.removeAllListeners('TelnyxCallAction');
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// Export the native bridge for direct access if needed
|
|
266
|
+
export { NativeBridge as VoicePnBridgeNative };
|
package/src/models/call-state.ts
CHANGED
|
@@ -22,6 +22,9 @@ export enum TelnyxCallState {
|
|
|
22
22
|
|
|
23
23
|
/** Call failed to connect or was rejected */
|
|
24
24
|
FAILED = 'FAILED',
|
|
25
|
+
|
|
26
|
+
/** Call was dropped due to network issues */
|
|
27
|
+
DROPPED = 'DROPPED',
|
|
25
28
|
}
|
|
26
29
|
|
|
27
30
|
/**
|
|
@@ -79,7 +82,11 @@ export const CallStateHelpers = {
|
|
|
79
82
|
* Is the call in a terminated state?
|
|
80
83
|
*/
|
|
81
84
|
isTerminated(state: TelnyxCallState): boolean {
|
|
82
|
-
return
|
|
85
|
+
return (
|
|
86
|
+
state === TelnyxCallState.ENDED ||
|
|
87
|
+
state === TelnyxCallState.FAILED ||
|
|
88
|
+
state === TelnyxCallState.DROPPED
|
|
89
|
+
);
|
|
83
90
|
},
|
|
84
91
|
|
|
85
92
|
/**
|
package/src/models/call.ts
CHANGED
|
@@ -24,8 +24,17 @@ export class Call {
|
|
|
24
24
|
private readonly _telnyxCall: TelnyxCall,
|
|
25
25
|
private readonly _callId: string,
|
|
26
26
|
private readonly _destination: string,
|
|
27
|
-
private readonly _isIncoming: boolean
|
|
27
|
+
private readonly _isIncoming: boolean,
|
|
28
|
+
isReattached: boolean = false,
|
|
29
|
+
private readonly _originalCallerName?: string,
|
|
30
|
+
private readonly _originalCallerNumber?: string
|
|
28
31
|
) {
|
|
32
|
+
// Set initial state based on whether this is a reattached call
|
|
33
|
+
if (isReattached) {
|
|
34
|
+
console.log('Call: Setting initial state to ACTIVE for reattached call');
|
|
35
|
+
this._callState.next(TelnyxCallState.ACTIVE);
|
|
36
|
+
}
|
|
37
|
+
|
|
29
38
|
this._setupCallListeners();
|
|
30
39
|
}
|
|
31
40
|
|
|
@@ -85,6 +94,24 @@ export class Call {
|
|
|
85
94
|
return this._duration.value;
|
|
86
95
|
}
|
|
87
96
|
|
|
97
|
+
/**
|
|
98
|
+
* Custom headers received from the WebRTC INVITE message.
|
|
99
|
+
* These headers are passed during call initiation and can contain application-specific information.
|
|
100
|
+
* Format should be [{"name": "X-Header-Name", "value": "Value"}] where header names must start with "X-".
|
|
101
|
+
*/
|
|
102
|
+
get inviteCustomHeaders(): { name: string; value: string }[] | null {
|
|
103
|
+
return this._telnyxCall.inviteCustomHeaders;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Custom headers received from the WebRTC ANSWER message.
|
|
108
|
+
* These headers are passed during call acceptance and can contain application-specific information.
|
|
109
|
+
* Format should be [{"name": "X-Header-Name", "value": "Value"}] where header names must start with "X-".
|
|
110
|
+
*/
|
|
111
|
+
get answerCustomHeaders(): { name: string; value: string }[] | null {
|
|
112
|
+
return this._telnyxCall.answerCustomHeaders;
|
|
113
|
+
}
|
|
114
|
+
|
|
88
115
|
/**
|
|
89
116
|
* Get the underlying Telnyx Call object (for internal use)
|
|
90
117
|
* @internal
|
|
@@ -163,8 +190,9 @@ export class Call {
|
|
|
163
190
|
|
|
164
191
|
/**
|
|
165
192
|
* Answer the incoming call
|
|
193
|
+
* @param customHeaders Optional custom headers to include with the answer
|
|
166
194
|
*/
|
|
167
|
-
async answer(): Promise<void> {
|
|
195
|
+
async answer(customHeaders?: { name: string; value: string }[]): Promise<void> {
|
|
168
196
|
if (!CallStateHelpers.canAnswer(this.currentState)) {
|
|
169
197
|
throw new Error(`Cannot answer call in state: ${this.currentState}`);
|
|
170
198
|
}
|
|
@@ -184,7 +212,8 @@ export class Call {
|
|
|
184
212
|
console.log('Call: Setting state to CONNECTING before answering');
|
|
185
213
|
this._callState.next(TelnyxCallState.CONNECTING);
|
|
186
214
|
|
|
187
|
-
|
|
215
|
+
// Pass custom headers to the underlying Telnyx call
|
|
216
|
+
await this._telnyxCall.answer(customHeaders);
|
|
188
217
|
} catch (error) {
|
|
189
218
|
console.error('Failed to answer call:', error);
|
|
190
219
|
throw error;
|
|
@@ -193,8 +222,9 @@ export class Call {
|
|
|
193
222
|
|
|
194
223
|
/**
|
|
195
224
|
* Hang up the call
|
|
225
|
+
* @param customHeaders Optional custom headers to include with the hangup request
|
|
196
226
|
*/
|
|
197
|
-
async hangup(): Promise<void> {
|
|
227
|
+
async hangup(customHeaders?: { name: string; value: string }[]): Promise<void> {
|
|
198
228
|
if (!CallStateHelpers.canHangup(this.currentState)) {
|
|
199
229
|
throw new Error(`Cannot hang up call in state: ${this.currentState}`);
|
|
200
230
|
}
|
|
@@ -211,7 +241,19 @@ export class Call {
|
|
|
211
241
|
}
|
|
212
242
|
|
|
213
243
|
// Fallback for Android or when CallKit is not available
|
|
214
|
-
await this._telnyxCall.hangup();
|
|
244
|
+
await this._telnyxCall.hangup(customHeaders);
|
|
245
|
+
|
|
246
|
+
// On Android, also notify the native side to hide ongoing notification
|
|
247
|
+
if (Platform.OS === 'android') {
|
|
248
|
+
try {
|
|
249
|
+
const { VoicePnBridge } = await import('../internal/voice-pn-bridge');
|
|
250
|
+
await VoicePnBridge.endCall(this._callId);
|
|
251
|
+
console.log('Call: Notified Android to hide ongoing notification');
|
|
252
|
+
} catch (error) {
|
|
253
|
+
console.error('Call: Failed to notify Android about call end:', error);
|
|
254
|
+
// Don't fail the hangup if notification hiding fails
|
|
255
|
+
}
|
|
256
|
+
}
|
|
215
257
|
} catch (error) {
|
|
216
258
|
console.error('Failed to hang up call:', error);
|
|
217
259
|
throw error;
|
|
@@ -327,11 +369,79 @@ export class Call {
|
|
|
327
369
|
// Start duration timer when call becomes active
|
|
328
370
|
if (telnyxState === TelnyxCallState.ACTIVE && !this._startTime) {
|
|
329
371
|
this._startDurationTimer();
|
|
372
|
+
|
|
373
|
+
// Show ongoing call notification on Android when call becomes active
|
|
374
|
+
// This covers both locally answered calls and calls that become active from remote side
|
|
375
|
+
if (Platform.OS === 'android') {
|
|
376
|
+
(async () => {
|
|
377
|
+
try {
|
|
378
|
+
const { VoicePnBridge } = await import('../internal/voice-pn-bridge');
|
|
379
|
+
|
|
380
|
+
// Extract caller information based on call direction
|
|
381
|
+
let callerNumber: string | undefined;
|
|
382
|
+
let callerName: string | undefined;
|
|
383
|
+
|
|
384
|
+
if (this._isIncoming) {
|
|
385
|
+
// For incoming calls, use the remote caller ID (who's calling us)
|
|
386
|
+
callerNumber = this._telnyxCall.remoteCallerIdNumber;
|
|
387
|
+
callerName = this._telnyxCall.remoteCallerIdName;
|
|
388
|
+
} else {
|
|
389
|
+
// For outgoing calls, use our own caller ID (what we're showing to them)
|
|
390
|
+
// These are the values we set when making the call
|
|
391
|
+
callerNumber = this._telnyxCall.localCallerIdNumber || this._originalCallerNumber;
|
|
392
|
+
callerName = this._telnyxCall.localCallerIdName || this._originalCallerName;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// Fallback logic for better notification display - avoid "Unknown" when possible
|
|
396
|
+
let displayName: string;
|
|
397
|
+
let displayNumber: string;
|
|
398
|
+
|
|
399
|
+
if (this._isIncoming) {
|
|
400
|
+
// For incoming calls: use caller name or fall back to caller number, then destination
|
|
401
|
+
displayName = callerName || callerNumber || this._destination;
|
|
402
|
+
displayNumber = callerNumber || this._destination;
|
|
403
|
+
} else {
|
|
404
|
+
// For outgoing calls: use our caller ID or descriptive text
|
|
405
|
+
displayName = callerName || `${this._destination}`;
|
|
406
|
+
displayNumber = callerNumber || this._destination;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
await VoicePnBridge.showOngoingCallNotification(
|
|
410
|
+
displayName,
|
|
411
|
+
displayNumber,
|
|
412
|
+
this._callId
|
|
413
|
+
);
|
|
414
|
+
console.log('Call: Showed ongoing call notification on Android (call active)', {
|
|
415
|
+
isIncoming: this._isIncoming,
|
|
416
|
+
callerName: displayName,
|
|
417
|
+
callerNumber: displayNumber,
|
|
418
|
+
});
|
|
419
|
+
} catch (error) {
|
|
420
|
+
console.error('Call: Failed to show ongoing call notification on active:', error);
|
|
421
|
+
}
|
|
422
|
+
})();
|
|
423
|
+
}
|
|
330
424
|
}
|
|
331
425
|
|
|
332
426
|
// Stop duration timer when call ends
|
|
333
427
|
if (CallStateHelpers.isTerminated(telnyxState)) {
|
|
334
428
|
this._stopDurationTimer();
|
|
429
|
+
|
|
430
|
+
// Clean up ongoing call notification on Android when call ends
|
|
431
|
+
if (Platform.OS === 'android') {
|
|
432
|
+
(async () => {
|
|
433
|
+
try {
|
|
434
|
+
const { VoicePnBridge } = await import('../internal/voice-pn-bridge');
|
|
435
|
+
await VoicePnBridge.endCall(this._callId);
|
|
436
|
+
console.log('Call: Cleaned up ongoing call notification (call terminated)');
|
|
437
|
+
} catch (error) {
|
|
438
|
+
console.error(
|
|
439
|
+
'Call: Failed to clean up ongoing call notification on termination:',
|
|
440
|
+
error
|
|
441
|
+
);
|
|
442
|
+
}
|
|
443
|
+
})();
|
|
444
|
+
}
|
|
335
445
|
}
|
|
336
446
|
});
|
|
337
447
|
}
|
|
@@ -346,6 +456,8 @@ export class Call {
|
|
|
346
456
|
case 'ringing':
|
|
347
457
|
case 'new':
|
|
348
458
|
return TelnyxCallState.RINGING;
|
|
459
|
+
case 'connecting':
|
|
460
|
+
return TelnyxCallState.CONNECTING;
|
|
349
461
|
case 'active':
|
|
350
462
|
case 'answered':
|
|
351
463
|
return TelnyxCallState.ACTIVE;
|
|
@@ -357,6 +469,8 @@ export class Call {
|
|
|
357
469
|
case 'failed':
|
|
358
470
|
case 'rejected':
|
|
359
471
|
return TelnyxCallState.FAILED;
|
|
472
|
+
case 'dropped':
|
|
473
|
+
return TelnyxCallState.DROPPED;
|
|
360
474
|
default:
|
|
361
475
|
console.warn(`Unknown call state: ${telnyxState}`);
|
|
362
476
|
return TelnyxCallState.RINGING;
|
package/src/telnyx-voice-app.tsx
CHANGED
|
@@ -262,27 +262,15 @@ const TelnyxVoiceAppComponent: React.FC<TelnyxVoiceAppProps> = ({
|
|
|
262
262
|
// If auto-reconnection fails, redirect to login screen
|
|
263
263
|
if (!success) {
|
|
264
264
|
log('Auto-reconnection failed - redirecting to login screen');
|
|
265
|
-
// Import router dynamically to avoid circular dependency issues
|
|
266
|
-
const { router } = require('expo-router');
|
|
267
|
-
|
|
268
|
-
// Small delay to ensure state is settled
|
|
269
|
-
setTimeout(() => {
|
|
270
|
-
router.replace('/');
|
|
271
|
-
}, 100);
|
|
272
265
|
}
|
|
273
266
|
} catch (e) {
|
|
274
267
|
log('Auto-reconnection error:', e);
|
|
275
|
-
|
|
276
268
|
// On error, also redirect to login
|
|
277
269
|
log('Auto-reconnection error - redirecting to login screen');
|
|
278
|
-
const { router } = require('expo-router');
|
|
279
|
-
setTimeout(() => {
|
|
280
|
-
router.replace('/');
|
|
281
|
-
}, 100);
|
|
282
270
|
}
|
|
283
271
|
}, [voipClient, log]);
|
|
284
272
|
|
|
285
|
-
// Check for initial push notification when app launches
|
|
273
|
+
// Check for initial push notification action when app launches
|
|
286
274
|
const checkForInitialPushNotification = useCallback(
|
|
287
275
|
async (fromAppResume: boolean = false) => {
|
|
288
276
|
log(`checkForInitialPushNotification called${fromAppResume ? ' (from app resume)' : ''}`);
|
|
@@ -309,6 +297,42 @@ const TelnyxVoiceAppComponent: React.FC<TelnyxVoiceAppProps> = ({
|
|
|
309
297
|
|
|
310
298
|
if (VoicePnBridge) {
|
|
311
299
|
log('Checking for pending push actions via VoicePnBridge');
|
|
300
|
+
|
|
301
|
+
// First check for pending call actions (notification button taps like hangup/answer)
|
|
302
|
+
const pendingCallAction = await VoicePnBridge.getPendingCallAction();
|
|
303
|
+
log('Raw pending call action response:', pendingCallAction);
|
|
304
|
+
|
|
305
|
+
if (pendingCallAction && pendingCallAction.action != null) {
|
|
306
|
+
log('Found pending call action:', pendingCallAction);
|
|
307
|
+
|
|
308
|
+
// Handle call actions directly
|
|
309
|
+
if (pendingCallAction.action === 'hangup' && pendingCallAction.callId) {
|
|
310
|
+
log(
|
|
311
|
+
'Processing hangup action from notification for call:',
|
|
312
|
+
pendingCallAction.callId
|
|
313
|
+
);
|
|
314
|
+
|
|
315
|
+
// Find and hangup the call
|
|
316
|
+
const activeCall = voipClient.currentActiveCall;
|
|
317
|
+
if (activeCall && activeCall.callId === pendingCallAction.callId) {
|
|
318
|
+
log('Hanging up active call from notification action');
|
|
319
|
+
try {
|
|
320
|
+
await activeCall.hangup();
|
|
321
|
+
log('Call hung up successfully from notification action');
|
|
322
|
+
} catch (error) {
|
|
323
|
+
log('Error hanging up call from notification action:', error);
|
|
324
|
+
}
|
|
325
|
+
} else {
|
|
326
|
+
log('No matching active call found for hangup action');
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// Clear the pending action
|
|
330
|
+
await VoicePnBridge.clearPendingCallAction();
|
|
331
|
+
return; // Don't process as push data
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// Then check for regular push notification data
|
|
312
336
|
const pendingAction = await VoicePnBridge.getPendingPushAction();
|
|
313
337
|
log('Raw pending action response:', pendingAction);
|
|
314
338
|
|
|
@@ -317,33 +341,12 @@ const TelnyxVoiceAppComponent: React.FC<TelnyxVoiceAppProps> = ({
|
|
|
317
341
|
|
|
318
342
|
// Parse the metadata if it's a string
|
|
319
343
|
let metadata = pendingAction.metadata;
|
|
320
|
-
|
|
321
|
-
try
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
// If JSON parsing fails, try parsing Android key-value format
|
|
327
|
-
// Format: "{call_id=value, action=value}"
|
|
328
|
-
log('JSON parse failed, trying Android key-value format');
|
|
329
|
-
try {
|
|
330
|
-
const cleanedString = metadata.replace(/[{}]/g, '').trim();
|
|
331
|
-
const pairs = cleanedString.split(',').map((pair) => pair.trim());
|
|
332
|
-
const parsed: Record<string, any> = {};
|
|
333
|
-
|
|
334
|
-
for (const pair of pairs) {
|
|
335
|
-
const [key, value] = pair.split('=').map((s) => s.trim());
|
|
336
|
-
if (key && value) {
|
|
337
|
-
parsed[key] = value;
|
|
338
|
-
}
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
metadata = parsed;
|
|
342
|
-
log('Parsed metadata as Android key-value format:', metadata);
|
|
343
|
-
} catch (parseError) {
|
|
344
|
-
log('Failed to parse metadata in any format, using as-is:', parseError);
|
|
345
|
-
}
|
|
346
|
-
}
|
|
344
|
+
try {
|
|
345
|
+
// First try parsing as JSON
|
|
346
|
+
metadata = JSON.parse(metadata);
|
|
347
|
+
log('Parsed metadata as JSON:', metadata);
|
|
348
|
+
} catch (e) {
|
|
349
|
+
log('JSON parse failed, trying Android key-value format');
|
|
347
350
|
}
|
|
348
351
|
|
|
349
352
|
// Create push data structure that matches what the VoIP client expects
|
|
@@ -516,6 +519,41 @@ const TelnyxVoiceAppComponent: React.FC<TelnyxVoiceAppProps> = ({
|
|
|
516
519
|
}
|
|
517
520
|
});
|
|
518
521
|
|
|
522
|
+
// Listen for immediate call action events from notification buttons (Android only)
|
|
523
|
+
let callActionSubscription: any = null;
|
|
524
|
+
if (Platform.OS === 'android') {
|
|
525
|
+
try {
|
|
526
|
+
const { VoicePnBridge } = require('./internal/voice-pn-bridge');
|
|
527
|
+
callActionSubscription = VoicePnBridge.addCallActionListener((event) => {
|
|
528
|
+
log(`Received immediate call action: ${event.action} for callId: ${event.callId}`);
|
|
529
|
+
|
|
530
|
+
// Handle immediate call actions (mainly for ending active calls from notification)
|
|
531
|
+
if (
|
|
532
|
+
event.action === 'hangup' ||
|
|
533
|
+
event.action === 'endCall' ||
|
|
534
|
+
event.action === 'reject'
|
|
535
|
+
) {
|
|
536
|
+
log(`Processing immediate end call action for callId: ${event.callId}`);
|
|
537
|
+
|
|
538
|
+
// Find the call by ID and end it
|
|
539
|
+
const targetCall = voipClient.currentCalls.find((call) => call.callId === event.callId);
|
|
540
|
+
if (targetCall) {
|
|
541
|
+
log(`Found active call ${event.callId}, ending it immediately`);
|
|
542
|
+
targetCall.hangup().catch((error) => {
|
|
543
|
+
log(`Error ending call ${event.callId}:`, error);
|
|
544
|
+
});
|
|
545
|
+
} else {
|
|
546
|
+
log(`No active call found with ID ${event.callId}`);
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
});
|
|
550
|
+
|
|
551
|
+
log('Call action listener registered for immediate notification handling');
|
|
552
|
+
} catch (e) {
|
|
553
|
+
log('Error setting up call action listener (VoicePnBridge not available):', e);
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
|
|
519
557
|
// Add app state listener if not skipping web background detection or not on web
|
|
520
558
|
// AND if app state management is enabled in the client options
|
|
521
559
|
let appStateSubscription: any = null;
|
|
@@ -538,6 +576,15 @@ const TelnyxVoiceAppComponent: React.FC<TelnyxVoiceAppProps> = ({
|
|
|
538
576
|
return () => {
|
|
539
577
|
connectionStateSubscription.unsubscribe();
|
|
540
578
|
callsSubscription.unsubscribe();
|
|
579
|
+
if (callActionSubscription) {
|
|
580
|
+
try {
|
|
581
|
+
const { VoicePnBridge } = require('./internal/voice-pn-bridge');
|
|
582
|
+
VoicePnBridge.removeCallActionListener(callActionSubscription);
|
|
583
|
+
log('Call action listener removed');
|
|
584
|
+
} catch (e) {
|
|
585
|
+
log('Error removing call action listener:', e);
|
|
586
|
+
}
|
|
587
|
+
}
|
|
541
588
|
if (appStateSubscription) {
|
|
542
589
|
appStateSubscription.remove();
|
|
543
590
|
}
|
|
@@ -286,12 +286,19 @@ export class TelnyxVoipClient {
|
|
|
286
286
|
* Initiates a new outgoing call.
|
|
287
287
|
*
|
|
288
288
|
* @param destination The destination number or SIP URI to call
|
|
289
|
-
* @param
|
|
289
|
+
* @param callerName Optional caller name to display
|
|
290
|
+
* @param callerNumber Optional caller ID number
|
|
291
|
+
* @param customHeaders Optional custom headers to include with the call
|
|
290
292
|
* @returns A Promise that completes with the Call object once the invitation has been sent
|
|
291
293
|
*
|
|
292
294
|
* The call's state can be monitored through the returned Call object's streams.
|
|
293
295
|
*/
|
|
294
|
-
async newCall(
|
|
296
|
+
async newCall(
|
|
297
|
+
destination: string,
|
|
298
|
+
callerName?: string,
|
|
299
|
+
callerNumber?: string,
|
|
300
|
+
customHeaders?: Record<string, string>
|
|
301
|
+
): Promise<Call> {
|
|
295
302
|
this._throwIfDisposed();
|
|
296
303
|
|
|
297
304
|
if (!destination || destination.trim() === '') {
|
|
@@ -306,7 +313,12 @@ export class TelnyxVoipClient {
|
|
|
306
313
|
console.log('TelnyxVoipClient: Creating new call to:', destination);
|
|
307
314
|
}
|
|
308
315
|
|
|
309
|
-
return await this._callStateController.newCall(
|
|
316
|
+
return await this._callStateController.newCall(
|
|
317
|
+
destination,
|
|
318
|
+
callerName,
|
|
319
|
+
callerNumber,
|
|
320
|
+
customHeaders
|
|
321
|
+
);
|
|
310
322
|
}
|
|
311
323
|
|
|
312
324
|
// ========== Push Notification Methods ==========
|