@voipsdk/react-native-voipcloud 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/README.md +172 -0
  2. package/android/build.gradle +61 -0
  3. package/android/src/main/AndroidManifest.xml +2 -0
  4. package/android/src/main/java/com/voipcloud/sdk/rn/VoipCloudModule.java +428 -0
  5. package/android/src/main/java/com/voipcloud/sdk/rn/VoipCloudPackage.java +21 -0
  6. package/android/src/main/java/com/voipcloud/sdk/rn/internal/Events.java +38 -0
  7. package/android/src/main/java/com/voipcloud/sdk/rn/internal/ManagedAccount.java +63 -0
  8. package/android/src/main/java/com/voipcloud/sdk/rn/internal/ManagedCall.java +179 -0
  9. package/android/src/main/java/com/voipcloud/sdk/rn/internal/ManagedCallRegistry.java +26 -0
  10. package/android/src/main/libs/voipcloud-release.aar +0 -0
  11. package/ios/VoipCloud.mm +40 -0
  12. package/lib/commonjs/License.js +51 -0
  13. package/lib/commonjs/License.js.map +1 -0
  14. package/lib/commonjs/VoipCloud.js +89 -0
  15. package/lib/commonjs/VoipCloud.js.map +1 -0
  16. package/lib/commonjs/events.js +29 -0
  17. package/lib/commonjs/events.js.map +1 -0
  18. package/lib/commonjs/index.js +27 -0
  19. package/lib/commonjs/index.js.map +1 -0
  20. package/lib/commonjs/types.js +2 -0
  21. package/lib/commonjs/types.js.map +1 -0
  22. package/lib/module/License.js +45 -0
  23. package/lib/module/License.js.map +1 -0
  24. package/lib/module/VoipCloud.js +83 -0
  25. package/lib/module/VoipCloud.js.map +1 -0
  26. package/lib/module/events.js +23 -0
  27. package/lib/module/events.js.map +1 -0
  28. package/lib/module/index.js +4 -0
  29. package/lib/module/index.js.map +1 -0
  30. package/lib/module/types.js +2 -0
  31. package/lib/module/types.js.map +1 -0
  32. package/lib/typescript/License.d.ts +31 -0
  33. package/lib/typescript/VoipCloud.d.ts +52 -0
  34. package/lib/typescript/events.d.ts +6 -0
  35. package/lib/typescript/index.d.ts +4 -0
  36. package/lib/typescript/types.d.ts +136 -0
  37. package/package.json +76 -0
  38. package/react-native-voipcloud.podspec +16 -0
  39. package/src/License.ts +52 -0
  40. package/src/VoipCloud.ts +98 -0
  41. package/src/events.ts +23 -0
  42. package/src/index.ts +23 -0
  43. package/src/types.ts +144 -0
package/README.md ADDED
@@ -0,0 +1,172 @@
1
+ # react-native-voipcloud
2
+
3
+ > Platform: Android (RN 0.70+, Android 12+).
4
+
5
+ ---
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ npm install react-native-voipcloud
11
+ # or:
12
+ yarn add react-native-voipcloud
13
+ ```
14
+
15
+ Android autolinks the `VoipCloudPackage` if you are on React Native 0.60+.
16
+
17
+ For older RN versions, add manually to `MainApplication.java`:
18
+ ```java
19
+ packages.add(new com.voipcloud.sdk.rn.VoipCloudPackage());
20
+ ```
21
+
22
+ Required `AndroidManifest.xml` permissions (declare in **your** app):
23
+ ```xml
24
+ <uses-permission android:name="android.permission.INTERNET" />
25
+ <uses-permission android:name="android.permission.RECORD_AUDIO" />
26
+ <uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
27
+ <uses-permission android:name="android.permission.WAKE_LOCK" />
28
+ <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
29
+ <uses-permission android:name="android.permission.FOREGROUND_SERVICE_PHONE_CALL" />
30
+ ```
31
+
32
+ Runtime permissions to request:
33
+ `RECORD_AUDIO`, `POST_NOTIFICATIONS` (Android 13+), `BLUETOOTH_CONNECT`.
34
+
35
+ ---
36
+
37
+ ## Quick start
38
+
39
+ ```ts
40
+ import { VoipCloud } from 'react-native-voipcloud';
41
+
42
+ // 1. Apply the license your operator issued.
43
+ try {
44
+ await VoipCloud.License.set(myToken);
45
+ } catch (e) {
46
+ // VC_LICENSE — token format / signature / expiry / bundle issues.
47
+ }
48
+
49
+ // 2. Start the engine.
50
+ await VoipCloud.init({ userAgent: 'acme-voip/1.0' });
51
+
52
+ // 3. Register a SIP account.
53
+ const accId = await VoipCloud.register({
54
+ uri: 'sip:alice@example.com',
55
+ registrar: 'sip:example.com:5061;transport=tls',
56
+ username: 'alice',
57
+ password: '••••••',
58
+ transport: 'TLS',
59
+ });
60
+
61
+ // 4. Make a call.
62
+ const callId = await VoipCloud.makeCall(accId, 'sip:bob@example.com');
63
+
64
+ // 5. In-call controls.
65
+ await VoipCloud.hold(callId); // put on hold
66
+ await VoipCloud.unhold(callId); // resume
67
+ await VoipCloud.mute(callId, true); // mute mic
68
+ await VoipCloud.sendDtmf(callId, '1234#'); // send DTMF
69
+ await VoipCloud.transferCall(callId, 'sip:charlie@example.com'); // blind transfer
70
+ await VoipCloud.hangup(callId); // end this call
71
+ await VoipCloud.hangupAllCalls(); // end every active call
72
+
73
+ // 6. Subscribe to events.
74
+ const sub = VoipCloud.on('callState', ({ callId, state }) => {
75
+ console.log('call', callId, '→', state);
76
+ });
77
+ const subMedia = VoipCloud.on('callMediaState', ({ callId, hasAudio, isHold, isMuted }) => {
78
+ console.log('media', callId, { hasAudio, isHold, isMuted });
79
+ });
80
+ // sub.remove() / subMedia.remove() when done
81
+
82
+ // 7. License refresh is automatic.
83
+ // If your token's payload includes a `ref` field, the SDK starts a
84
+ // 24 h refresher in the background after `License.set` succeeds.
85
+ // No URL configuration needed on the app side.
86
+ //
87
+ // Optional: stop the refresher when the user logs out.
88
+ await VoipCloud.License.stopRefresh();
89
+
90
+ // 8. Tear down the SDK on app exit.
91
+ await VoipCloud.shutdown();
92
+ ```
93
+
94
+ ---
95
+
96
+ ## Error codes (Promise rejection.code)
97
+
98
+ Mọi method trả Promise; khi reject, `code` mang một trong các mã sau.
99
+ Cụ thể lỗi license được tách thành các mã granular để app dễ hiển thị
100
+ thông báo phù hợp.
101
+
102
+ License (từ `License.set` / `License.status`):
103
+
104
+ | code | Ý nghĩa |
105
+ |-----------------------|------------------------------------------------------|
106
+ | `VC_NO_LICENSE` | Chưa cài token, hoặc token đã bị reset |
107
+ | `VC_BAD_FORMAT` | Token sai định dạng (không phải base64url JWT) |
108
+ | `VC_BAD_SIGNATURE` | Chữ ký Ed25519 không hợp lệ |
109
+ | `VC_EXPIRED` | Token hết hạn |
110
+ | `VC_NOT_ACTIVE` | Token bị tạm khoá (status ≠ active) |
111
+ | `VC_REVOKED` | Token đã bị thu hồi |
112
+ | `VC_BUNDLE_MISMATCH` | Bundle id app không khớp `bundle_id` trong token |
113
+ | `VC_PAYLOAD_TOO_BIG` | Payload token quá lớn |
114
+ | `VC_CLOCK_SKEW` | Đồng hồ thiết bị bị chỉnh lùi so với mốc server |
115
+ | `VC_LICENSE` | Lỗi license chung không khớp các mã trên |
116
+ | `VC_STATUS` | Đọc trạng thái thất bại |
117
+
118
+ Endpoint / call:
119
+
120
+ | code | Ý nghĩa |
121
+ |-----------------------|------------------------------------------------------|
122
+ | `VC_INIT` | Endpoint không khởi động được |
123
+ | `VC_SHUTDOWN` | Lỗi khi `shutdown()` dọn endpoint |
124
+ | `VC_REGISTER` | `register()` thất bại |
125
+ | `VC_UNREGISTER` | `unregister()` thất bại |
126
+ | `VC_NO_ACCOUNT` | accountId không tồn tại |
127
+ | `VC_MAKE_CALL` | `makeCall()` thất bại (bao gồm `VC_CCU_EXCEEDED`) |
128
+ | `VC_CCU_EXCEEDED` | Vượt số cuộc gọi đồng thời license cho phép |
129
+ | `VC_NO_CALL` | callId không tồn tại |
130
+ | `VC_ANSWER` | `answer()` thất bại |
131
+ | `VC_HANGUP` | `hangup()` thất bại |
132
+ | `VC_HOLD` / `VC_UNHOLD` | `hold()` / `unhold()` thất bại |
133
+ | `VC_MUTE` | `mute()` thất bại (vd: chưa init endpoint) |
134
+ | `VC_DTMF` | `sendDtmf()` thất bại |
135
+ | `VC_TRANSFER` | `transferCall()` thất bại |
136
+ | `VC_HANGUP_ALL` | `hangupAllCalls()` thất bại |
137
+
138
+ `message` đính kèm là `pj_strerror` từ PJSIP, hữu ích khi log.
139
+
140
+ ---
141
+
142
+ ## Events
143
+
144
+ Account / call lifecycle (do PJSIP phát):
145
+
146
+ ```ts
147
+ VoipCloud.on('registrationState', e => /* { accountId, code, reason } */);
148
+ VoipCloud.on('incomingCall', e => /* { accountId, callId, from, to } */);
149
+ VoipCloud.on('callState', e => /* { callId, state, code, reason } */);
150
+ VoipCloud.on('callMediaState', e => /* { callId, hasAudio, isHold, isMuted } */);
151
+ ```
152
+
153
+ Per-action xác nhận (bridge phát sau khi action thành công — tiện cho
154
+ UI bind 1-1 với từng nút bấm):
155
+
156
+ ```ts
157
+ VoipCloud.on('callHold', e => /* { callId } */);
158
+ VoipCloud.on('callUnhold', e => /* { callId } */);
159
+ VoipCloud.on('callMute', e => /* { callId, isMuted } */);
160
+ VoipCloud.on('callDtmfSent', e => /* { callId, digits } */);
161
+ VoipCloud.on('callTransfer', e => /* { callId, destinationUri } */);
162
+ ```
163
+
164
+ Remote-driven (PJSIP forward từ callback gốc):
165
+
166
+ ```ts
167
+ // REFER multi-stage: nhận một event mỗi NOTIFY; finalNotify=true là kết quả cuối.
168
+ VoipCloud.on('callTransferStatus', e => /* { callId, statusCode, reason, finalNotify } */);
169
+
170
+ // DTMF do đầu remote gửi qua RTP (RFC 2833).
171
+ VoipCloud.on('incomingDtmf', e => /* { callId, digit, duration } */);
172
+ ```
@@ -0,0 +1,61 @@
1
+ // react-native-voipcloud — Android library module.
2
+ //
3
+ // Consumer apps that include this package via Gradle pull in:
4
+ // * com.voipcloud.sdk.* from voipcloud-release.aar
5
+ // * com.voipcloud.sdk.rn.* React Native bridge classes (this module)
6
+ //
7
+ // The AAR ships its own ProGuard rules (proguard.txt) which R8 auto-
8
+ // applies to the consumer release build; no extra setup needed.
9
+
10
+ buildscript {
11
+ repositories {
12
+ google()
13
+ mavenCentral()
14
+ }
15
+ dependencies {
16
+ // Pinned to the AGP version known to compile with React Native
17
+ // 0.74 + AS Iguana. Bump in lockstep with peerDependencies.
18
+ classpath 'com.android.tools.build:gradle:8.2.1'
19
+ }
20
+ }
21
+
22
+ apply plugin: 'com.android.library'
23
+
24
+ def safeExtGet(prop, fallback) {
25
+ rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback
26
+ }
27
+
28
+ android {
29
+ namespace 'com.voipcloud.sdk.rn'
30
+ compileSdkVersion safeExtGet('compileSdkVersion', 34)
31
+
32
+ defaultConfig {
33
+ minSdkVersion safeExtGet('minSdkVersion', 31)
34
+ targetSdkVersion safeExtGet('targetSdkVersion', 34)
35
+
36
+ ndk {
37
+ // The AAR ships native libs for these three ABIs only.
38
+ abiFilters 'arm64-v8a', 'armeabi-v7a', 'x86_64'
39
+ }
40
+ }
41
+
42
+ compileOptions {
43
+ sourceCompatibility JavaVersion.VERSION_17
44
+ targetCompatibility JavaVersion.VERSION_17
45
+ }
46
+ }
47
+
48
+ repositories {
49
+ google()
50
+ mavenCentral()
51
+ flatDir { dirs 'src/main/libs' }
52
+ }
53
+
54
+ dependencies {
55
+ // Prebuilt PJSIP + license layer (com.voipcloud.sdk.*).
56
+ implementation(name: 'voipcloud-release', ext: 'aar')
57
+
58
+ // Pin to a permissive range — the consumer app controls the actual
59
+ // React Native version via its own classpath.
60
+ implementation 'com.facebook.react:react-native:+'
61
+ }
@@ -0,0 +1,2 @@
1
+ <?xml version="1.0" encoding="utf-8"?>
2
+ <manifest xmlns:android="http://schemas.android.com/apk/res/android" />
@@ -0,0 +1,428 @@
1
+ package com.voipcloud.sdk.rn;
2
+
3
+ import android.provider.Settings;
4
+ import androidx.annotation.NonNull;
5
+
6
+ import com.facebook.react.bridge.Arguments;
7
+ import com.facebook.react.bridge.Promise;
8
+ import com.facebook.react.bridge.ReactApplicationContext;
9
+ import com.facebook.react.bridge.ReactContextBaseJavaModule;
10
+ import com.facebook.react.bridge.ReactMethod;
11
+ import com.facebook.react.bridge.ReadableMap;
12
+ import com.facebook.react.bridge.WritableMap;
13
+
14
+ import com.voipcloud.sdk.AccountConfig;
15
+ import com.voipcloud.sdk.AuthCredInfo;
16
+ import com.voipcloud.sdk.CallOpParam;
17
+ import com.voipcloud.sdk.CallSendDtmfParam;
18
+ import com.voipcloud.sdk.Endpoint;
19
+ import com.voipcloud.sdk.EpConfig;
20
+ import com.voipcloud.sdk.License;
21
+ import com.voipcloud.sdk.LicenseRefresher;
22
+ import com.voipcloud.sdk.TransportConfig;
23
+ import com.voipcloud.sdk.pjsip_status_code;
24
+ import com.voipcloud.sdk.pjsip_transport_type_e;
25
+ import com.voipcloud.sdk.pjsua_destroy_flag;
26
+
27
+ import com.voipcloud.sdk.rn.internal.Events;
28
+ import com.voipcloud.sdk.rn.internal.ManagedAccount;
29
+ import com.voipcloud.sdk.rn.internal.ManagedCall;
30
+ import com.voipcloud.sdk.rn.internal.ManagedCallRegistry;
31
+
32
+ import java.util.concurrent.ConcurrentHashMap;
33
+ import java.util.concurrent.atomic.AtomicInteger;
34
+
35
+ /**
36
+ * React Native bridge for the VoipCloud SDK. All methods are async
37
+ * Promises; license-gate failures rejecting with code "VC_<ENUM>".
38
+ */
39
+ public class VoipCloudModule extends ReactContextBaseJavaModule {
40
+
41
+ static {
42
+ // Load the prebuilt PJSIP + license-layer library. Names must
43
+ // match jniLibs path inside the AAR: jni/<abi>/libvoipcloud.so.
44
+ // libc++_shared is co-located and pulled in implicitly by the
45
+ // loader as a NEEDED dependency.
46
+ System.loadLibrary("voipcloud");
47
+ }
48
+
49
+ private final ReactApplicationContext ctx;
50
+ private final ManagedCallRegistry calls = new ManagedCallRegistry();
51
+ private final ConcurrentHashMap<Integer, ManagedAccount> accounts = new ConcurrentHashMap<>();
52
+ private final AtomicInteger nextAccKey = new AtomicInteger(1);
53
+
54
+ private Endpoint endpoint;
55
+ private boolean inited = false;
56
+ /** Last token accepted by License.set; reused by the refresher. */
57
+ private volatile String licenseToken = null;
58
+ /** Refresh URL currently driving LicenseRefresher. Used to detect
59
+ * when a token rotation also rotates the refresh endpoint. */
60
+ private volatile String activeRefreshUrl = null;
61
+
62
+ public VoipCloudModule(ReactApplicationContext ctx) {
63
+ super(ctx);
64
+ this.ctx = ctx;
65
+
66
+ // Tell native the consumer app's package up front so subsequent
67
+ // License.set() can enforce the bundle_id binding embedded in
68
+ // the token. Also anchor the clock-skew detector.
69
+ License.setBundle(ctx.getPackageName());
70
+ License.setStateDir(ctx.getFilesDir().getAbsolutePath());
71
+ }
72
+
73
+ @NonNull
74
+ @Override
75
+ public String getName() { return "VoipCloud"; }
76
+
77
+ // ============== License ============================================
78
+
79
+ @ReactMethod
80
+ public void setLicense(String token, Promise promise) {
81
+ try {
82
+ License.set(token);
83
+ licenseToken = token;
84
+ bootstrapRefresher(License.refreshUrl());
85
+ promise.resolve(null);
86
+ } catch (Throwable t) {
87
+ // Stop any refresher tied to the previous (now-rejected) token.
88
+ stopRefresherInternal();
89
+ promise.reject(mapLicenseError(t), t.getMessage(), t);
90
+ }
91
+ }
92
+
93
+ /**
94
+ * Start, restart, or no-op the LicenseRefresher to match the URL
95
+ * embedded in the most recently accepted token. The consumer app
96
+ * never specifies a URL — it travels inside the signed token.
97
+ */
98
+ private void bootstrapRefresher(String refreshUrl) {
99
+ if (refreshUrl == null || refreshUrl.isEmpty()) {
100
+ // Token opted out of refresh. Stop any leftover loop.
101
+ stopRefresherInternal();
102
+ return;
103
+ }
104
+ // If the URL is unchanged, the existing refresher is still
105
+ // correct — just leave it running so its next 24 h fire reuses
106
+ // the now-updated token via its internal cache.
107
+ if (refreshUrl.equals(activeRefreshUrl)) return;
108
+
109
+ // URL changed (or first start) — restart with the new endpoint.
110
+ try { LicenseRefresher.stop(); } catch (Throwable ignored) {}
111
+ LicenseRefresher.start(ctx, refreshUrl, licenseToken, autoDeviceId());
112
+ activeRefreshUrl = refreshUrl;
113
+ }
114
+
115
+ private void stopRefresherInternal() {
116
+ try { LicenseRefresher.stop(); } catch (Throwable ignored) {}
117
+ activeRefreshUrl = null;
118
+ }
119
+
120
+ /**
121
+ * Stable per-install identifier without asking the consumer. On
122
+ * Android 8+ the ANDROID_ID is scoped to package+signing key, so
123
+ * it survives reinstalls of the same APK but rotates if the app is
124
+ * sideloaded by another publisher — exactly the semantics a
125
+ * license server wants.
126
+ */
127
+ private String autoDeviceId() {
128
+ try {
129
+ String id = Settings.Secure.getString(
130
+ ctx.getContentResolver(), Settings.Secure.ANDROID_ID);
131
+ if (id != null && !id.isEmpty()) return id;
132
+ } catch (Throwable ignored) {}
133
+ // Fall back to a manifest-derived label.
134
+ return ctx.getPackageName() + ":" + android.os.Build.FINGERPRINT;
135
+ }
136
+
137
+ /** Translate a PJSUA2 Error into the granular VC_* code surfaced to JS. */
138
+ private static String mapLicenseError(Throwable t) {
139
+ String msg = t.getMessage();
140
+ if (msg == null) return "VC_LICENSE";
141
+ if (msg.contains("license not set")) return "VC_NO_LICENSE";
142
+ if (msg.contains("malformed")) return "VC_BAD_FORMAT";
143
+ if (msg.contains("signature invalid")) return "VC_BAD_SIGNATURE";
144
+ if (msg.contains("license expired")) return "VC_EXPIRED";
145
+ if (msg.contains("not active")) return "VC_NOT_ACTIVE";
146
+ if (msg.contains("revoked")) return "VC_REVOKED";
147
+ if (msg.contains("bundle id mismatch")) return "VC_BUNDLE_MISMATCH";
148
+ if (msg.contains("concurrent calls")) return "VC_CCU_EXCEEDED";
149
+ if (msg.contains("payload too large")) return "VC_PAYLOAD_TOO_BIG";
150
+ if (msg.contains("clock rolled back")) return "VC_CLOCK_SKEW";
151
+ return "VC_LICENSE";
152
+ }
153
+
154
+ @ReactMethod
155
+ public void getLicenseStatus(Promise promise) {
156
+ try {
157
+ WritableMap m = Arguments.createMap();
158
+ m.putBoolean("valid", License.isValid());
159
+ m.putString ("status", License.status());
160
+ m.putString ("customerId", License.customerId());
161
+ m.putInt ("ccu", License.maxCcu());
162
+ m.putInt ("currentCalls",License.currentCalls());
163
+ m.putDouble ("expiry", License.expiryUnix());
164
+ m.putDouble ("serverTime", License.serverTime());
165
+ promise.resolve(m);
166
+ } catch (Throwable t) {
167
+ promise.reject("VC_STATUS", t.getMessage(), t);
168
+ }
169
+ }
170
+
171
+ /**
172
+ * The refresher bootstraps itself from token.ref during
173
+ * setLicense(); the consumer never starts it manually. We expose
174
+ * stop() only so apps can pause refresh during user logout / SDK
175
+ * shutdown.
176
+ */
177
+ @ReactMethod
178
+ public void stopLicenseRefresh(Promise promise) {
179
+ stopRefresherInternal();
180
+ promise.resolve(null);
181
+ }
182
+
183
+ // ============== Endpoint lifecycle =================================
184
+
185
+ @ReactMethod
186
+ public void init(ReadableMap config, Promise promise) {
187
+ try {
188
+ if (inited) { promise.resolve(null); return; }
189
+ endpoint = new Endpoint();
190
+ endpoint.libCreate();
191
+
192
+ EpConfig epCfg = new EpConfig();
193
+ if (config.hasKey("userAgent"))
194
+ epCfg.getUaConfig().setUserAgent(config.getString("userAgent"));
195
+ if (config.hasKey("stunServer")) {
196
+ com.voipcloud.sdk.StringVector v = new com.voipcloud.sdk.StringVector();
197
+ v.add(config.getString("stunServer"));
198
+ epCfg.getUaConfig().setStunServer(v);
199
+ }
200
+ if (config.hasKey("logLevel"))
201
+ epCfg.getLogConfig().setLevel(config.getInt("logLevel"));
202
+
203
+ endpoint.libInit(epCfg);
204
+
205
+ // Default transports — TLS handled per-account via registrar URI.
206
+ TransportConfig udp = new TransportConfig();
207
+ endpoint.transportCreate(pjsip_transport_type_e.PJSIP_TRANSPORT_UDP, udp);
208
+ TransportConfig tcp = new TransportConfig();
209
+ endpoint.transportCreate(pjsip_transport_type_e.PJSIP_TRANSPORT_TCP, tcp);
210
+
211
+ endpoint.libStart();
212
+ inited = true;
213
+ promise.resolve(null);
214
+ } catch (Throwable t) {
215
+ promise.reject("VC_INIT", t.getMessage(), t);
216
+ }
217
+ }
218
+
219
+ @ReactMethod
220
+ public void shutdown(Promise promise) {
221
+ try {
222
+ calls.clear();
223
+ for (ManagedAccount a : accounts.values()) a.delete();
224
+ accounts.clear();
225
+ if (endpoint != null) {
226
+ endpoint.libDestroy(pjsua_destroy_flag.PJSUA_DESTROY_NO_NETWORK);
227
+ endpoint.delete();
228
+ endpoint = null;
229
+ }
230
+ inited = false;
231
+ promise.resolve(null);
232
+ } catch (Throwable t) {
233
+ promise.reject("VC_SHUTDOWN", t.getMessage(), t);
234
+ }
235
+ }
236
+
237
+ // ============== Account ============================================
238
+
239
+ @ReactMethod
240
+ public void register(ReadableMap cfg, Promise promise) {
241
+ try {
242
+ AccountConfig acfg = new AccountConfig();
243
+ acfg.setIdUri(cfg.getString("uri"));
244
+ acfg.getRegConfig().setRegistrarUri(cfg.getString("registrar"));
245
+
246
+ AuthCredInfo cred = new AuthCredInfo(
247
+ "digest", "*",
248
+ cfg.getString("username"), 0,
249
+ cfg.getString("password"));
250
+ acfg.getSipConfig().getAuthCreds().add(cred);
251
+
252
+ ManagedAccount acc = new ManagedAccount(ctx, calls);
253
+ acc.create(acfg);
254
+ int id = acc.accountId();
255
+ accounts.put(id, acc);
256
+ promise.resolve(id);
257
+ } catch (Throwable t) {
258
+ promise.reject("VC_REGISTER", t.getMessage(), t);
259
+ }
260
+ }
261
+
262
+ @ReactMethod
263
+ public void unregister(int accountId, Promise promise) {
264
+ try {
265
+ ManagedAccount acc = accounts.remove(accountId);
266
+ if (acc != null) {
267
+ acc.setRegistration(false);
268
+ acc.delete();
269
+ }
270
+ promise.resolve(null);
271
+ } catch (Throwable t) {
272
+ promise.reject("VC_UNREGISTER", t.getMessage(), t);
273
+ }
274
+ }
275
+
276
+ // ============== Call ===============================================
277
+
278
+ @ReactMethod
279
+ public void makeCall(int accountId, String uri, Promise promise) {
280
+ try {
281
+ ManagedAccount acc = accounts.get(accountId);
282
+ if (acc == null) { promise.reject("VC_NO_ACCOUNT", "unknown accountId"); return; }
283
+ ManagedCall call = new ManagedCall(ctx, acc);
284
+ call.makeCall(uri, new CallOpParam(true));
285
+ calls.put(call);
286
+ promise.resolve(call.callId());
287
+ } catch (Throwable t) {
288
+ promise.reject("VC_MAKE_CALL", t.getMessage(), t);
289
+ }
290
+ }
291
+
292
+ @ReactMethod
293
+ public void answer(int callId, Promise promise) {
294
+ try {
295
+ ManagedCall call = calls.get(callId);
296
+ if (call == null) { promise.reject("VC_NO_CALL", "unknown callId"); return; }
297
+ CallOpParam op = new CallOpParam();
298
+ op.setStatusCode(pjsip_status_code.PJSIP_SC_OK);
299
+ call.answer(op);
300
+ promise.resolve(null);
301
+ } catch (Throwable t) {
302
+ promise.reject("VC_ANSWER", t.getMessage(), t);
303
+ }
304
+ }
305
+
306
+ @ReactMethod
307
+ public void hangup(int callId, Promise promise) {
308
+ try {
309
+ ManagedCall call = calls.get(callId);
310
+ if (call == null) { promise.resolve(null); return; }
311
+ CallOpParam op = new CallOpParam();
312
+ op.setStatusCode(pjsip_status_code.PJSIP_SC_DECLINE);
313
+ call.hangup(op);
314
+ calls.remove(callId);
315
+ promise.resolve(null);
316
+ } catch (Throwable t) {
317
+ promise.reject("VC_HANGUP", t.getMessage(), t);
318
+ }
319
+ }
320
+
321
+ @ReactMethod
322
+ public void hold(int callId, Promise promise) {
323
+ try {
324
+ ManagedCall call = calls.get(callId);
325
+ if (call == null) { promise.reject("VC_NO_CALL", "unknown callId"); return; }
326
+ call.setHold(new CallOpParam(true));
327
+ WritableMap ev = Arguments.createMap();
328
+ ev.putInt("callId", callId);
329
+ Events.emit(getReactApplicationContext(), Events.CALL_HOLD, ev);
330
+ promise.resolve(null);
331
+ } catch (Throwable t) {
332
+ promise.reject("VC_HOLD", t.getMessage(), t);
333
+ }
334
+ }
335
+
336
+ @ReactMethod
337
+ public void unhold(int callId, Promise promise) {
338
+ try {
339
+ ManagedCall call = calls.get(callId);
340
+ if (call == null) { promise.reject("VC_NO_CALL", "unknown callId"); return; }
341
+ // PJSUA2 re-INVITE without an explicit hold flag re-offers
342
+ // sendrecv SDP; that releases the existing hold.
343
+ call.reinvite(new CallOpParam(true));
344
+ WritableMap ev = Arguments.createMap();
345
+ ev.putInt("callId", callId);
346
+ Events.emit(getReactApplicationContext(), Events.CALL_UNHOLD, ev);
347
+ promise.resolve(null);
348
+ } catch (Throwable t) {
349
+ promise.reject("VC_UNHOLD", t.getMessage(), t);
350
+ }
351
+ }
352
+
353
+ @ReactMethod
354
+ public void mute(int callId, boolean on, Promise promise) {
355
+ try {
356
+ ManagedCall call = calls.get(callId);
357
+ if (call == null) { promise.reject("VC_NO_CALL", "unknown callId"); return; }
358
+ if (endpoint == null) { promise.reject("VC_INIT", "endpoint not started"); return; }
359
+ call.setMuted(endpoint, on);
360
+ WritableMap ev = Arguments.createMap();
361
+ ev.putInt("callId", callId);
362
+ ev.putBoolean("isMuted", on);
363
+ Events.emit(getReactApplicationContext(), Events.CALL_MUTE, ev);
364
+ promise.resolve(null);
365
+ } catch (Throwable t) {
366
+ promise.reject("VC_MUTE", t.getMessage(), t);
367
+ }
368
+ }
369
+
370
+ @ReactMethod
371
+ public void sendDtmf(int callId, String digits, Promise promise) {
372
+ try {
373
+ ManagedCall call = calls.get(callId);
374
+ if (call == null) { promise.reject("VC_NO_CALL", "unknown callId"); return; }
375
+ CallSendDtmfParam p = new CallSendDtmfParam();
376
+ p.setDigits(digits);
377
+ call.sendDtmf(p);
378
+ WritableMap ev = Arguments.createMap();
379
+ ev.putInt("callId", callId);
380
+ ev.putString("digits", digits);
381
+ Events.emit(getReactApplicationContext(), Events.CALL_DTMF_SENT, ev);
382
+ promise.resolve(null);
383
+ } catch (Throwable t) {
384
+ promise.reject("VC_DTMF", t.getMessage(), t);
385
+ }
386
+ }
387
+
388
+ /**
389
+ * Blind-transfer a call (REFER). The Promise resolves once the
390
+ * REFER is *sent* — progress of the remote-side INVITE comes
391
+ * through `callTransferStatus` events (statusCode + finalNotify).
392
+ */
393
+ @ReactMethod
394
+ public void transferCall(int callId, String destinationUri, Promise promise) {
395
+ try {
396
+ ManagedCall call = calls.get(callId);
397
+ if (call == null) { promise.reject("VC_NO_CALL", "unknown callId"); return; }
398
+ call.xfer(destinationUri, new CallOpParam(true));
399
+ WritableMap ev = Arguments.createMap();
400
+ ev.putInt("callId", callId);
401
+ ev.putString("destinationUri", destinationUri);
402
+ Events.emit(getReactApplicationContext(), Events.CALL_TRANSFER, ev);
403
+ promise.resolve(null);
404
+ } catch (Throwable t) {
405
+ promise.reject("VC_TRANSFER", t.getMessage(), t);
406
+ }
407
+ }
408
+
409
+ /**
410
+ * End every active call in one shot. Useful for SDK shutdown or
411
+ * a global "end all" UI action. Each call still emits its own
412
+ * DISCONNECTED callState.
413
+ */
414
+ @ReactMethod
415
+ public void hangupAllCalls(Promise promise) {
416
+ try {
417
+ if (endpoint != null) endpoint.hangupAllCalls();
418
+ promise.resolve(null);
419
+ } catch (Throwable t) {
420
+ promise.reject("VC_HANGUP_ALL", t.getMessage(), t);
421
+ }
422
+ }
423
+
424
+ // RN event-emitter contract — required on RN 0.65+ when used with
425
+ // NativeEventEmitter on the JS side.
426
+ @ReactMethod public void addListener(String eventName) { /* no-op */ }
427
+ @ReactMethod public void removeListeners(int count) { /* no-op */ }
428
+ }
@@ -0,0 +1,21 @@
1
+ package com.voipcloud.sdk.rn;
2
+
3
+ import com.facebook.react.ReactPackage;
4
+ import com.facebook.react.bridge.NativeModule;
5
+ import com.facebook.react.bridge.ReactApplicationContext;
6
+ import com.facebook.react.uimanager.ViewManager;
7
+
8
+ import java.util.Collections;
9
+ import java.util.List;
10
+
11
+ public class VoipCloudPackage implements ReactPackage {
12
+ @Override
13
+ public List<NativeModule> createNativeModules(ReactApplicationContext ctx) {
14
+ return Collections.singletonList(new VoipCloudModule(ctx));
15
+ }
16
+
17
+ @Override
18
+ public List<ViewManager> createViewManagers(ReactApplicationContext ctx) {
19
+ return Collections.emptyList();
20
+ }
21
+ }