@zyratalk1/zyra-twilio-wrapper 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,75 @@
1
+ ### React Native VOIP SDK
2
+
3
+ React Native VOIP SDK architecture with:
4
+ - JS public API layer
5
+ - RN bridge layer
6
+ - Twilio native Voice SDK adapter for Android and iOS
7
+ - Core auth/call/conference/retry/config modules
8
+
9
+ ### Technical Design
10
+
11
+ - `react-native-voip-sdk-design.md`
12
+ - `react-native-voip-sdk-design.docx`
13
+ - `react-native-voip-sdk-design.pdf`
14
+
15
+ ### Consumer Initialization
16
+
17
+ Consumer app must provide `sdkToken`.
18
+ `accessToken` is optional at constructor time and can be supplied later in `authenticate`.
19
+
20
+ ```ts
21
+ const sdk = new ZyraTwilioWrapper({
22
+ serverUrl: "https://your-backend.com",
23
+ identity: "user_123",
24
+ sdkToken: "YOUR_SDK_TOKEN",
25
+ // optional:
26
+ // accessToken: "TWILIO_ACCESS_TOKEN",
27
+ });
28
+
29
+ // Optional: provide access token during authenticate
30
+ await sdk.authenticate({ accessToken: "TWILIO_ACCESS_TOKEN" });
31
+ ```
32
+
33
+ ### Android App Setup (Consumer App)
34
+
35
+ Update `MainActivity.kt` with `VoiceActivityProxy` hooks:
36
+
37
+ ```kotlin
38
+ private val activityProxy = VoiceActivityProxy(this) { permission -> /* optional UX */ }
39
+
40
+ override fun onCreate(savedInstanceState: Bundle?) {
41
+ super.onCreate(savedInstanceState)
42
+ activityProxy.onCreate(savedInstanceState)
43
+ }
44
+
45
+ override fun onStart() { super.onStart(); activityProxy.onStart() }
46
+ override fun onStop() { activityProxy.onStop(); super.onStop() }
47
+ override fun onNewIntent(intent: Intent?) { super.onNewIntent(intent); activityProxy.onNewIntent(intent) }
48
+ override fun onDestroy() { activityProxy.onDestroy(); super.onDestroy() }
49
+ ```
50
+
51
+ Update `MainApplication.kt` with `VoiceApplicationProxy` hooks:
52
+
53
+ ```kotlin
54
+ private val voiceApplicationProxy = VoiceApplicationProxy(this)
55
+
56
+ override fun onCreate() {
57
+ super.onCreate()
58
+ voiceApplicationProxy.onCreate()
59
+ }
60
+
61
+ override fun onTerminate() {
62
+ voiceApplicationProxy.onTerminate()
63
+ super.onTerminate()
64
+ }
65
+ ```
66
+
67
+ ### iOS App Setup (Consumer App)
68
+
69
+ Enable the following Xcode capabilities for the app target:
70
+ - Background Modes:
71
+ - Audio, AirPlay, and Picture in Picture
72
+ - Voice over IP
73
+ - Push Notifications
74
+
75
+ Use physical iOS devices for end-to-end call testing.
@@ -0,0 +1,407 @@
1
+ interface Config {
2
+ serverUrl: string;
3
+ identity: string;
4
+ waitUrl?: string;
5
+ sdkToken: string;
6
+ accessToken?: string;
7
+ requestTimeoutMs?: number;
8
+ retry?: {
9
+ maxRetries?: number;
10
+ baseDelayMs?: number;
11
+ maxDelayMs?: number;
12
+ jitterRatio?: number;
13
+ circuitBreakerThreshold?: number;
14
+ circuitBreakerCooldownMs?: number;
15
+ };
16
+ }
17
+ interface TwilioConferenceParticipant {
18
+ accountSid: string;
19
+ callSid: string;
20
+ callSidToCoach: string | null;
21
+ coaching: boolean;
22
+ conferenceSid: string;
23
+ dateCreated: string;
24
+ dateUpdated: string;
25
+ endConferenceOnExit: boolean;
26
+ hold: boolean;
27
+ label: string | null;
28
+ muted: boolean;
29
+ queueTime: string | null;
30
+ startConferenceOnEnter: boolean;
31
+ status: 'queued' | 'ringing' | 'in-progress' | 'connected' | 'completed' | string;
32
+ uri: string;
33
+ }
34
+ interface TwilioConference {
35
+ conferenceSid: string;
36
+ conferences: TwilioConferenceDetails;
37
+ }
38
+ interface TwilioConferenceDetails {
39
+ accountSid: string;
40
+ apiVersion: string;
41
+ callSidEndingConference: string | null;
42
+ dateCreated: string;
43
+ dateUpdated: string;
44
+ friendlyName: string;
45
+ reasonConferenceEnded: string | null;
46
+ region: string;
47
+ sid: string;
48
+ status: 'init' | 'in-progress' | 'completed' | 'terminated' | string;
49
+ subresourceUris: {
50
+ participants: string;
51
+ recordings: string;
52
+ [key: string]: string;
53
+ };
54
+ uri: string;
55
+ }
56
+ interface WebhookConfigResult {
57
+ configured: boolean;
58
+ webhookUrl: string | null;
59
+ }
60
+ type CallResponse<T = void> = {
61
+ success: true;
62
+ message: string;
63
+ data: T;
64
+ } | {
65
+ success: false;
66
+ error: Error;
67
+ };
68
+ /**
69
+ * Input types
70
+ */
71
+ type WelcomeType = "audio" | "tts";
72
+ /**
73
+ * Output type (matches backend output)
74
+ */
75
+ interface WelcomeConfigResult {
76
+ success: boolean;
77
+ message: string;
78
+ welcomeType: WelcomeType;
79
+ routingConfigId: number;
80
+ }
81
+ /**
82
+ * Routing Config
83
+ */
84
+ type RoutingStrategy = "RING_ALL" | "RING_SEQUENCE";
85
+ type FallbackActionType = "TRANSFER" | "AI" | "VOICEMAIL";
86
+ interface FallbackAction {
87
+ type: FallbackActionType;
88
+ target: {
89
+ phoneNumber: string;
90
+ } | null;
91
+ }
92
+ interface RoutingMember {
93
+ id: number;
94
+ priorityOrder: number;
95
+ isEnabled: boolean;
96
+ userType: "PHONE" | "DESKTOP";
97
+ identifier: string | null;
98
+ email: string | null;
99
+ phoneNumber: string | null;
100
+ ringDuration: number | null;
101
+ }
102
+ interface GetRoutingConfigResult {
103
+ strategy: RoutingStrategy;
104
+ defaultRingTimeout: number | null;
105
+ maxAttempts: number | null;
106
+ fallbackAction: FallbackAction;
107
+ members: RoutingMember[];
108
+ }
109
+ interface SetRoutingMember {
110
+ priorityOrder: number;
111
+ enabled: boolean;
112
+ userType: "PHONE" | "DESKTOP";
113
+ identifier: string;
114
+ ringDuration: number;
115
+ contact: {
116
+ email: string | null;
117
+ phoneNumber: string | null;
118
+ };
119
+ }
120
+ interface SetRoutingConfigInput {
121
+ strategy: {
122
+ type: RoutingStrategy;
123
+ defaultRingTimeout?: number;
124
+ maxAttempts?: number;
125
+ };
126
+ routingMembers: SetRoutingMember[];
127
+ fallbackAction: FallbackAction;
128
+ }
129
+ interface SetRoutingConfigResult {
130
+ success: boolean;
131
+ message: string;
132
+ strategy: RoutingStrategy;
133
+ memberCount: number;
134
+ }
135
+ interface CreateParseRuleEntry {
136
+ dataType: "rule" | "parseData";
137
+ fieldName: string;
138
+ parseOptions?: unknown;
139
+ parseOptionCode?: unknown;
140
+ fieldRules?: unknown;
141
+ }
142
+ interface ParseRuleRow {
143
+ id: number;
144
+ userId: number;
145
+ dataType: string | string[];
146
+ fieldName: string[];
147
+ fieldRules: string | null;
148
+ fieldRulesParsed: Record<string, any> | null;
149
+ parseOptions?: string | null;
150
+ parseOptionCode?: string | null;
151
+ createdAt: string;
152
+ updatedAt: string;
153
+ }
154
+ interface ListParseRulesInput {
155
+ page?: number;
156
+ limit?: number;
157
+ dataType?: "rule" | "parseData";
158
+ activeOnly?: boolean;
159
+ }
160
+ interface ListParseRulesResult {
161
+ data: ParseRuleRow[];
162
+ total: number;
163
+ page: number;
164
+ limit: number;
165
+ }
166
+ interface CreateParseRuleInput {
167
+ parseDataEntries: CreateParseRuleEntry[];
168
+ }
169
+ interface CreateParseRuleResult {
170
+ id: number;
171
+ userId: number;
172
+ dataType: string;
173
+ fieldName: string[];
174
+ fieldRules: string | null;
175
+ fieldRulesParsed?: Record<string, unknown> | null;
176
+ parseOptions?: string | null;
177
+ parseOptionCode?: string | null;
178
+ createdAt: string;
179
+ updatedAt: string;
180
+ }
181
+
182
+ type CallDirection = "INBOUND" | "OUTBOUND";
183
+ type InboundCallState = "RINGING" | "ANSWERED" | "ON_HOLD" | "RESUMED" | "ENDED" | "FAILED";
184
+ type OutboundCallState = "INITIATED" | "DIALING" | "RINGING" | "CONNECTED" | "MERGED" | "ENDED" | "FAILED";
185
+ type CallState = InboundCallState | OutboundCallState;
186
+ interface ConsumerAuth {
187
+ consumerId: string;
188
+ accessToken: string;
189
+ refreshToken?: string;
190
+ expiresAt: number;
191
+ scopes: string[];
192
+ }
193
+ interface Participant {
194
+ id: string;
195
+ name?: string;
196
+ number?: string;
197
+ status: string;
198
+ }
199
+ interface CallSession {
200
+ callId: string;
201
+ direction: CallDirection;
202
+ status: CallState;
203
+ participants: Participant[];
204
+ conferenceRoomId: string | null;
205
+ }
206
+ interface RetryPolicyConfig {
207
+ maxRetries: number;
208
+ baseDelayMs: number;
209
+ maxDelayMs: number;
210
+ jitterRatio: number;
211
+ circuitBreakerThreshold: number;
212
+ circuitBreakerCooldownMs: number;
213
+ }
214
+ interface SDKConfig {
215
+ serverUrl: string;
216
+ consumerId: string;
217
+ sdkToken: string;
218
+ accessToken?: string;
219
+ identity?: string;
220
+ waitUrl?: string;
221
+ enableAndroidVoiceRegister?: boolean;
222
+ requestTimeoutMs?: number;
223
+ retry?: Partial<RetryPolicyConfig>;
224
+ }
225
+ interface AuthPayload {
226
+ accessToken?: string;
227
+ assertion?: string;
228
+ metadata?: Record<string, unknown>;
229
+ }
230
+ interface NativeCommandResult<T = Record<string, unknown>> {
231
+ success: boolean;
232
+ data?: T;
233
+ error?: string;
234
+ }
235
+ interface NativeModule {
236
+ initializeSDK(config: SDKConfig): Promise<NativeCommandResult>;
237
+ authenticate(payload?: AuthPayload): Promise<NativeCommandResult<ConsumerAuth>>;
238
+ refreshAuth?(): Promise<NativeCommandResult<ConsumerAuth>>;
239
+ startCall(input: {
240
+ number: string;
241
+ callId: string;
242
+ }): Promise<NativeCommandResult<CallSession>>;
243
+ answerCall(input: {
244
+ callId: string;
245
+ }): Promise<NativeCommandResult>;
246
+ rejectCall?(input: {
247
+ callId: string;
248
+ }): Promise<NativeCommandResult>;
249
+ endCall(input: {
250
+ callId: string;
251
+ }): Promise<NativeCommandResult>;
252
+ holdCall(input: {
253
+ callId: string;
254
+ }): Promise<NativeCommandResult>;
255
+ resumeCall(input: {
256
+ callId: string;
257
+ }): Promise<NativeCommandResult>;
258
+ mergeCall(input: {
259
+ callId: string;
260
+ }): Promise<NativeCommandResult<CallSession>>;
261
+ toggleMute?(input: {
262
+ callId: string;
263
+ mute: boolean;
264
+ }): Promise<NativeCommandResult>;
265
+ addNewCallee?(input: {
266
+ callId: string;
267
+ newParticipantNo: string;
268
+ }): Promise<NativeCommandResult>;
269
+ getConferenceId?(input: {
270
+ conferenceName: string;
271
+ }): Promise<NativeCommandResult<Record<string, unknown>>>;
272
+ getParticipants(input: {
273
+ callId: string;
274
+ }): Promise<NativeCommandResult<Participant[]>>;
275
+ getParticipantsByCallSid?(input: {
276
+ callSid: string;
277
+ }): Promise<NativeCommandResult<Participant[]>>;
278
+ disconnectParticipant(input: {
279
+ participantId: string;
280
+ }): Promise<NativeCommandResult>;
281
+ removeParticipant?(input: {
282
+ conferenceSid: string;
283
+ callSid: string;
284
+ }): Promise<NativeCommandResult>;
285
+ addListener?(eventName: string, callback: (payload: Record<string, unknown>) => void): {
286
+ remove: () => void;
287
+ };
288
+ }
289
+ type SDKEventName = "onReady" | "onCallRinging" | "onCallConnected" | "onParticipantJoined" | "onParticipantLeft" | "onCallEnded" | "onCallStateChanged" | "onAuthStateChanged" | "onMissedCall" | "onError";
290
+ /**
291
+ * Options for {@link ReactNativeVoipSdk.makeCall}.
292
+ * - `parallel`: start a second Twilio `voice.connect` leg while another call is active (independent lifecycle).
293
+ * - Omitted / false: if a call is already active, add a conference participant via `addNewCallee` (existing behavior).
294
+ */
295
+ interface MakeCallOptions {
296
+ parallel?: boolean;
297
+ }
298
+
299
+ type EventPayload = Record<string, unknown>;
300
+ type Listener = (payload: EventPayload) => void;
301
+ declare class BridgeEventEmitter {
302
+ private listeners;
303
+ on(eventName: SDKEventName | string, listener: Listener): () => void;
304
+ off(eventName: SDKEventName | string, listener: Listener): void;
305
+ emit(eventName: SDKEventName | string, payload: EventPayload): void;
306
+ removeAllListeners(): void;
307
+ }
308
+
309
+ declare class BridgeClient {
310
+ readonly events: BridgeEventEmitter;
311
+ readonly nativeModule: NativeModule;
312
+ private nativeSubscriptions;
313
+ constructor(nativeModule?: NativeModule);
314
+ on(eventName: SDKEventName | string, listener: (payload: Record<string, unknown>) => void): () => void;
315
+ emit(eventName: SDKEventName | string, payload: Record<string, unknown>): void;
316
+ destroy(): void;
317
+ private subscribeToNativeEvents;
318
+ }
319
+
320
+ declare class ReactNativeVoipSdk {
321
+ private readonly sdkConfig;
322
+ protected readonly bridge: BridgeClient;
323
+ private readonly retry;
324
+ private readonly authManager;
325
+ private readonly callEngine;
326
+ private readonly conferenceManager;
327
+ private readonly configService;
328
+ private initialized;
329
+ protected activeCallId: string | null;
330
+ /** Tracks every in-flight call leg (parallel or single); kept in sync with native `onCallEnded`. */
331
+ protected activeCallIds: Set<string>;
332
+ private unsubscribeOnCallEnded;
333
+ protected internalConferenceName: string;
334
+ constructor(config: Config, nativeModule?: NativeModule);
335
+ on(eventName: SDKEventName | string, listener: (payload: Record<string, unknown>) => void): () => void;
336
+ initializeSDK(config?: Partial<SDKConfig>): Promise<void>;
337
+ authenticate(payload?: AuthPayload): Promise<ConsumerAuth>;
338
+ startCall(number: string): Promise<CallSession>;
339
+ answerCall(callId: string): Promise<void>;
340
+ endCall(callId: string): Promise<void>;
341
+ holdCall(callId: string): Promise<void>;
342
+ resumeCall(callId: string): Promise<void>;
343
+ mergeCall(callId: string): Promise<CallSession>;
344
+ getParticipants(callId: string): Promise<Participant[]>;
345
+ disconnectParticipant(participantId: string): Promise<void>;
346
+ init(): Promise<void>;
347
+ verifySDKToken(): Promise<boolean>;
348
+ /**
349
+ * Start a call or add to a conference.
350
+ * Use `makeCall(to, { parallel: true })` (or `addCall(to)`) while a call is active to open a second
351
+ * independent leg; failures on that leg are isolated to its `callId` in `onError` / `onCallEnded`.
352
+ */
353
+ makeCall(to: string, options?: MakeCallOptions): Promise<CallResponse<CallSession>>;
354
+ /** Start a parallel outbound call while another call is active (alias for `makeCall(to, { parallel: true })`). */
355
+ addCall(to: string): Promise<CallResponse<CallSession>>;
356
+ /** All active call legs (parallel-aware). */
357
+ getActiveCallIds(): string[];
358
+ acceptCall(callId?: string): Promise<CallResponse<null>>;
359
+ rejectCall(callId: string): Promise<CallResponse<null>>;
360
+ hangup(callId?: string): Promise<CallResponse<null>>;
361
+ hold(callId: string): Promise<CallResponse<null>>;
362
+ resume(callId: string): Promise<CallResponse<null>>;
363
+ merge(callId: string): Promise<CallResponse<CallSession>>;
364
+ toggleMute(mute: boolean, callId?: string): Promise<CallResponse<null>>;
365
+ makeCallIncoming(to: string, callSid: string): Promise<CallResponse<CallSession>>;
366
+ holdAndAddParticipant(callSid: string, number: string): Promise<CallResponse<unknown>>;
367
+ holdAndAddParticipantIncoming(callSid: string, number: string): Promise<CallResponse<unknown>>;
368
+ getConferenceId(): Promise<CallResponse<unknown>>;
369
+ getParticipantsIncoming(callSid: string): Promise<CallResponse<unknown>>;
370
+ getParticipantsOutgoing(callSid: string): Promise<CallResponse<unknown>>;
371
+ removeParticipant(conferenceSid: string, callSid: string): Promise<CallResponse<unknown>>;
372
+ getRoutingConfig(): Promise<CallResponse<GetRoutingConfigResult>>;
373
+ setRoutingConfig(input: SetRoutingConfigInput): Promise<CallResponse<SetRoutingConfigResult>>;
374
+ createParseRule(input: CreateParseRuleInput): Promise<CallResponse<CreateParseRuleResult[]>>;
375
+ listParseRules(input?: ListParseRulesInput): Promise<CallResponse<ListParseRulesResult>>;
376
+ registerWebhook(webhookUrl: string): Promise<CallResponse<{
377
+ webhookUrl: string;
378
+ }>>;
379
+ deregisterWebhook(): Promise<CallResponse<null>>;
380
+ isWebhookConfigured(): Promise<CallResponse<WebhookConfigResult>>;
381
+ welcomeMessageConfig(welcomeType: "audio" | "tts", welcomeMessage: string): Promise<CallResponse<WelcomeConfigResult>>;
382
+ destroy(): void;
383
+ private pickNextActiveCallId;
384
+ private ensureAuthenticated;
385
+ }
386
+
387
+ /**
388
+ * Backward-compatible wrapper class retaining the original export name.
389
+ * Existing consumers can continue to construct `new ZyraTwilioWrapper(config)`.
390
+ */
391
+ declare class ZyraTwilioWrapper extends ReactNativeVoipSdk {
392
+ activeCallSid: string | null;
393
+ conferenceName: string;
394
+ conferenceDetail: TwilioConference | null;
395
+ participants: TwilioConferenceParticipant[];
396
+ waitUrl: string;
397
+ onReady?: () => void;
398
+ onIncoming?: (payload: Record<string, unknown>) => void;
399
+ onDisconnect?: () => void;
400
+ onError?: (error: Error) => void;
401
+ onConnect?: (payload: Record<string, unknown>) => void;
402
+ onMissedCall?: () => void;
403
+ constructor(config: Config, nativeModule?: NativeModule);
404
+ destroy(): void;
405
+ }
406
+
407
+ export { type AuthPayload, type CallSession, type ConsumerAuth, type MakeCallOptions, type NativeModule, type Participant, ReactNativeVoipSdk, type SDKConfig, type SDKEventName, ZyraTwilioWrapper as default };