omikit-plugin 4.1.6 → 4.1.8

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 CHANGED
@@ -54,8 +54,8 @@ The [omikit-plugin](https://www.npmjs.com/package/omikit-plugin) enables VoIP/SI
54
54
 
55
55
  | Platform | SDK | Version |
56
56
  |----------|-----|---------|
57
- | Android | OMIKIT | 2.6.9 |
58
- | iOS | OmiKit | 1.11.9 |
57
+ | Android | OMICore | 2.7.0 |
58
+ | iOS | OmiKit | 1.11.23 |
59
59
 
60
60
  ### Platform Requirements
61
61
 
@@ -1086,6 +1086,7 @@ await initCallWithUserPassword({
1086
1086
  | `initCallWithUserPassword(data)` | `Promise<boolean>` | Login with SIP username/password |
1087
1087
  | `initCallWithApiKey(data)` | `Promise<boolean>` | Login with API key |
1088
1088
  | `logout()` | `Promise<boolean>` | Logout and unregister SIP |
1089
+ | `logoutAndWait()` | `Promise<boolean>` | (v4.1.7+) Logout that **resolves only after** the SDK finishes the backend `devices/remove` HTTP call and clears local state. Use before an immediate re-login to avoid race conditions |
1089
1090
 
1090
1091
  ### Call Control
1091
1092
 
@@ -1135,12 +1136,165 @@ await initCallWithUserPassword({
1135
1136
  | Function | Returns | Description |
1136
1137
  |----------|---------|-------------|
1137
1138
  | `getProjectId()` | `Promise<string\|null>` | Current project ID |
1138
- | `getAppId()` | `Promise<string\|null>` | Current app ID |
1139
- | `getDeviceId()` | `Promise<string\|null>` | Current device ID |
1139
+ | `getAppId()` | `Promise<string\|null>` | Current app ID. (v4.1.7+ Android) Sourced from `OmiClient.getAppId()` to match `getOmiDevices()` payload |
1140
+ | `getDeviceId()` | `Promise<string\|null>` | Current device ID. (v4.1.7+ Android) Sourced from `OmiClient.getDeviceId()` to match `getOmiDevices()` payload |
1140
1141
  | `getFcmToken()` | `Promise<string\|null>` | FCM push token |
1141
1142
  | `getSipInfo()` | `Promise<string\|null>` | SIP info (`user@realm`) |
1142
1143
  | `getVoipToken()` | `Promise<string\|null>` | VoIP token (iOS only) |
1143
1144
 
1145
+ ### On-Premise Endpoint Configuration (v4.1.8+) — Native Only
1146
+
1147
+ > For enterprise customers self-hosting OMI infrastructure. Override SDK default endpoints / SIP proxy / STUN / TURN with the customer's own hosts. Each field is **optional** — omit any field to keep the SDK default. Persisted natively across app relaunches.
1148
+
1149
+ **There is no JavaScript API for this feature.** Config must be set in `AppDelegate` (iOS) / `MainApplication` (Android), BEFORE React Native bootstraps. Setting from JS would race the SDK's first HTTP / SIP call (FCM token, push registration, VoIP push handler) and those requests would hit the default cloud endpoints. The native API persists the config so subsequent app launches are race-free.
1150
+
1151
+ #### iOS — `AppDelegate.m`
1152
+
1153
+ Add the call at the top of `didFinishLaunchingWithOptions:`, BEFORE the existing RN bootstrap and BEFORE any other Omi call:
1154
+
1155
+ ```objc
1156
+ #import <OmiKit/OmiKit.h>
1157
+
1158
+ - (BOOL)application:(UIApplication *)application
1159
+ didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
1160
+
1161
+ [OmiClient setOnPremiseInfoWithMobileSdkHost:@"omisdk.your-domain.com"
1162
+ callEventHost:@"call-event.your-domain.com"
1163
+ publicApiHost:@"public.your-domain.com"
1164
+ pushInfoHost:@"push-info.your-domain.com"
1165
+ app2AppHost:@"app-2-app.your-domain.com"
1166
+ logUploadHost:@"log-upload.your-domain.com"
1167
+ sipProxy:@"sig.your-domain.com:5222"
1168
+ stunServer:@"stun.your-domain.com:3478"
1169
+ turnServer:@"turn.your-domain.com:2222"
1170
+ turnUsername:@"your-turn-user"
1171
+ turnPassword:@"your-turn-pass"];
1172
+
1173
+ // ... existing RN bootstrap (RCTAppDelegate / RCTReactNativeFactory)
1174
+ }
1175
+ ```
1176
+
1177
+ To revert: `[OmiClient clearOnPremiseInfo];`
1178
+
1179
+ #### Android — `MainApplication.kt`
1180
+
1181
+ Add the call at the top of `onCreate()`, BEFORE `SoLoader.init` and RN init:
1182
+
1183
+ ```kotlin
1184
+ import vn.vihat.omicall.omisdk.OmiClient
1185
+
1186
+ class MainApplication : Application(), ReactApplication {
1187
+ override fun onCreate() {
1188
+ super.onCreate()
1189
+
1190
+ OmiClient.setOnPremiseInfo(
1191
+ this,
1192
+ mobileSdkHost = "omisdk.your-domain.com",
1193
+ callEventHost = "call-event.your-domain.com",
1194
+ publicApiHost = "public.your-domain.com",
1195
+ pushInfoHost = "push-info.your-domain.com",
1196
+ app2AppHost = "app-2-app.your-domain.com",
1197
+ logUploadHost = "log-upload.your-domain.com",
1198
+ sipProxy = "sig.your-domain.com:5222",
1199
+ stunServer = "stun.your-domain.com:3478",
1200
+ turnServer = "turn.your-domain.com:2222",
1201
+ turnUsername = "your-turn-user",
1202
+ turnPassword = "your-turn-pass",
1203
+ )
1204
+
1205
+ SoLoader.init(this, false)
1206
+ // ... existing RN bootstrap
1207
+ }
1208
+ }
1209
+ ```
1210
+
1211
+ To revert: `OmiClient.clearOnPremiseInfo(this)`.
1212
+
1213
+ #### Field Reference
1214
+
1215
+ **HTTP host groups** (SDK replaces scheme + host only; path + query preserved 100%):
1216
+
1217
+ | Field | Replaces |
1218
+ |-------|----------|
1219
+ | `mobileSdkHost` | `omisdk-v1*.omicrm.com` — devices, extensions, network info, ICE provider, rtp log |
1220
+ | `callEventHost` | `call-event-v2*.omicrm.com` — call-action APIs |
1221
+ | `publicApiHost` | `public-v1*.omicrm.com` — init call API |
1222
+ | `pushInfoHost` | `push-info-v2*.omicrm.com` — has-answered |
1223
+ | `app2AppHost` | `app-2-app*.omicrm.com` — agent/customer login |
1224
+ | `logUploadHost` | `elastic-v2*.omicrm.com` — log upload |
1225
+
1226
+ **SIP / Media** (`"host:port"` format):
1227
+
1228
+ | Field | SDK default |
1229
+ |-------|------------|
1230
+ | `sipProxy` | `171.244.138.14:5222` |
1231
+ | `stunServer` | `stun.omicrm.com:3478` |
1232
+ | `turnServer` | `turn.omicrm.com:2222` |
1233
+ | `turnUsername` | embedded credentials |
1234
+ | `turnPassword` | embedded credentials |
1235
+
1236
+ #### Behavior Notes
1237
+
1238
+ - Config is **persisted** natively (iOS `NSUserDefaults` key `omicall/onpremise_config_v1`, Android `SharedPreferences` `omicall_onpremise`/`config_v1`) — no need to set on every app launch after the first.
1239
+ - Priority: **HTTP** → on-premise > SDK default. **SIP / Media** → on-premise > dynamic API provider > SDK default.
1240
+ - **Android DNS**: when on-premise is active, the SDK bypasses custom public DNS (`8.8.8.8` / `1.1.1.1`) and uses **system DNS** so internal hostnames resolve over the customer's private network / VPN. Applies to both OkHttp HTTP layer and PJSIP native.
1241
+ - Empty strings, null, and missing fields are treated identically as "keep SDK default" for that field.
1242
+ - Requires native SDK: iOS `OmiKit ≥ 1.11.23`, Android `OMICore ≥ 2.7.0`.
1243
+ - Backward compatible: clients that do not call `setOnPremiseInfo` see byte-for-byte identical behavior to earlier versions.
1244
+
1245
+ ### Backend Device Registration Check (v4.1.7+)
1246
+
1247
+ > Read-only diagnostics for verifying the local device record still exists on the OMI backend. Useful after app reinstall, account migration, or backend cleanup. The SDK **never** auto-logouts — your app decides the recovery action.
1248
+
1249
+ | Function | Returns | Description |
1250
+ |----------|---------|-------------|
1251
+ | `getOmiDevices()` | `Promise<OmiDeviceInfo[]>` | Fetch all devices registered on the OMI backend for the active SIP user. Empty array when not logged in / network error / parse error — never rejects |
1252
+ | `isCurrentDeviceRegistered()` | `Promise<boolean>` | `true` if the local `deviceId` + `appId` is in the backend list. Returns `false` early when not logged in (no HTTP call) |
1253
+ | `needsReLogin()` | `Promise<boolean>` | `true` when SIP user is set locally but the backend has no matching device (stale session — user must logout + login again) |
1254
+ | `findSipNumberByDeviceId(devices, deviceId)` | `string \| null` | Pure JS helper. Returns the `sipNumber` for the given device ID in the array, or `null` if not found |
1255
+
1256
+ **`OmiDeviceInfo` shape (camelCase, normalized at JS layer):**
1257
+
1258
+ ```typescript
1259
+ type OmiDeviceInfo = {
1260
+ deviceId: string;
1261
+ token: string;
1262
+ deviceType: 'ios' | 'android' | string;
1263
+ voipToken: string;
1264
+ appId: string;
1265
+ createdTime: string;
1266
+ projectId: string;
1267
+ sipNumber: string;
1268
+ };
1269
+ ```
1270
+
1271
+ **Recommended usage:**
1272
+
1273
+ ```typescript
1274
+ import {
1275
+ getOmiDevices,
1276
+ needsReLogin,
1277
+ findSipNumberByDeviceId,
1278
+ getDeviceId,
1279
+ logoutAndWait,
1280
+ } from 'omikit-plugin';
1281
+
1282
+
1283
+ // Or: verify the SIP account matches the one bound to this device
1284
+ const devices = await getOmiDevices();
1285
+ const myDeviceId = await getDeviceId();
1286
+ const boundSip = findSipNumberByDeviceId(devices, myDeviceId ?? '');
1287
+ if (boundSip !== expectedUsername) {
1288
+ await logoutAndWait();
1289
+ // Then re-login with correct credentials
1290
+ }
1291
+ ```
1292
+
1293
+ **Notes:**
1294
+
1295
+ - Each call performs a fresh HTTP request — no caching. Call once after login / on foreground, not in tight loops.
1296
+ - Requires native SDK: iOS `OmiKit ≥ 1.11.19`, Android `OMICore ≥ 2.6.21`.
1297
+
1144
1298
  ### Notification Control
1145
1299
 
1146
1300
  | Function | Returns | Description |
@@ -65,7 +65,7 @@ dependencies {
65
65
  // OMISDK
66
66
  implementation("androidx.work:work-runtime:2.8.1")
67
67
  implementation "androidx.security:security-crypto:1.1.0-alpha06"
68
- api "io.omicrm.vihat:omi-sdk:2.6.9"
68
+ api "io.omicrm.vihat:omi-sdk:2.7.0"
69
69
 
70
70
  // React Native — resolved from consumer's node_modules
71
71
  implementation "com.facebook.react:react-native:+"
@@ -1151,6 +1151,36 @@ class OmikitPluginModule(reactContext: ReactApplicationContext?) :
1151
1151
  }
1152
1152
  }
1153
1153
 
1154
+ /// Logout and wait for the SDK to fully clean up before resolving.
1155
+ ///
1156
+ /// `OmiClient.logout(onCompleted)` is a `suspend fun` that:
1157
+ /// - Fires HTTP `devices/remove` (fire-and-forget background)
1158
+ /// - Clears local SharedPreferences
1159
+ /// - Stops SipService and polls for PJSIP shutdown (up to 5s internally)
1160
+ /// - Invokes `onCompleted` only after the stack is fully down
1161
+ ///
1162
+ /// We pass an empty callback purely to opt into the SDK's internal wait
1163
+ /// loop — without it the SDK skips waiting and returns immediately.
1164
+ /// The plugin's coroutine `await`s the suspend call, so the JS promise
1165
+ /// resolves only once the SDK signals completion (or its 5s internal
1166
+ /// timeout elapses).
1167
+ @ReactMethod
1168
+ fun logoutAndWait(promise: Promise) {
1169
+ val ctx = reactApplicationContext
1170
+ if (ctx == null) {
1171
+ promise.resolve(true)
1172
+ return
1173
+ }
1174
+ mainScope.launch {
1175
+ try {
1176
+ OmiClient.getInstance(ctx).logout { /* opt-in to internal wait */ }
1177
+ } catch (_: Throwable) {
1178
+ // ignored — local state still cleaned by the SDK, safe to proceed
1179
+ }
1180
+ promise.resolve(true)
1181
+ }
1182
+ }
1183
+
1154
1184
  @ReactMethod
1155
1185
  fun getCurrentUser(promise: Promise) {
1156
1186
  mainScope.launch {
@@ -1230,13 +1260,22 @@ class OmikitPluginModule(reactContext: ReactApplicationContext?) :
1230
1260
  @ReactMethod
1231
1261
  fun getAppId(promise: Promise) {
1232
1262
  try {
1263
+ // Prefer SDK public API (OmiSDK 2.6.21+) — guarantees same value used when
1264
+ // adding device to backend, so consumers can match against getOmiDevices().
1265
+ val ctx = reactApplicationContext
1266
+ if (ctx != null) {
1267
+ val sdkAppId = OmiClient.getInstance(ctx).getAppId()
1268
+ if (sdkAppId.isNotEmpty()) {
1269
+ promise.resolve(sdkAppId)
1270
+ return
1271
+ }
1272
+ }
1233
1273
  val info = OmiClient.registrationInfo
1234
1274
  if (info?.appId != null) {
1235
1275
  promise.resolve(info.appId)
1236
1276
  return
1237
1277
  }
1238
- // Fallback: get package name of the host app
1239
- promise.resolve(reactApplicationContext?.packageName)
1278
+ promise.resolve(ctx?.packageName)
1240
1279
  } catch (e: Throwable) {
1241
1280
  promise.resolve(null)
1242
1281
  }
@@ -1245,15 +1284,24 @@ class OmikitPluginModule(reactContext: ReactApplicationContext?) :
1245
1284
  @ReactMethod
1246
1285
  fun getDeviceId(promise: Promise) {
1247
1286
  try {
1287
+ // Prefer SDK public API (OmiSDK 2.6.21+) — guarantees same value used when
1288
+ // adding device to backend, so consumers can match against getOmiDevices().
1289
+ val ctx = reactApplicationContext
1290
+ if (ctx != null) {
1291
+ val sdkDeviceId = OmiClient.getInstance(ctx).getDeviceId()
1292
+ if (sdkDeviceId.isNotEmpty()) {
1293
+ promise.resolve(sdkDeviceId)
1294
+ return
1295
+ }
1296
+ }
1248
1297
  val info = OmiClient.registrationInfo
1249
1298
  if (info?.deviceId != null) {
1250
1299
  promise.resolve(info.deviceId)
1251
1300
  return
1252
1301
  }
1253
- // Fallback: get Android ID directly
1254
1302
  val androidId = try {
1255
1303
  Settings.Secure.getString(
1256
- reactApplicationContext?.contentResolver,
1304
+ ctx?.contentResolver,
1257
1305
  Settings.Secure.ANDROID_ID
1258
1306
  )
1259
1307
  } catch (e: Exception) { null }
@@ -1305,6 +1353,87 @@ class OmikitPluginModule(reactContext: ReactApplicationContext?) :
1305
1353
  promise.resolve(null)
1306
1354
  }
1307
1355
 
1356
+ // MARK: - Backend Device Registration Check APIs (OmiSDK 2.6.20+)
1357
+
1358
+ /// Fetch the list of devices currently registered on OMI backend for the
1359
+ /// active SIP user. Returns empty array on logout / network failure.
1360
+ @ReactMethod
1361
+ fun getOmiDevices(promise: Promise) {
1362
+ val ctx = reactApplicationContext
1363
+ if (ctx == null) {
1364
+ promise.resolve(WritableNativeArray())
1365
+ return
1366
+ }
1367
+ mainScope.launch {
1368
+ val devices = withContext(Dispatchers.IO) {
1369
+ try {
1370
+ OmiClient.getInstance(ctx).getOmiDevices()
1371
+ } catch (_: Throwable) {
1372
+ emptyList()
1373
+ }
1374
+ }
1375
+ val array: WritableArray = WritableNativeArray()
1376
+ for (d in devices) {
1377
+ val map: WritableMap = WritableNativeMap()
1378
+ map.putString("device_id", d.deviceId)
1379
+ map.putString("token", d.token)
1380
+ map.putString("device_type", d.deviceType)
1381
+ map.putString("voip_token", d.voipToken)
1382
+ map.putString("app_id", d.appId)
1383
+ // created_time may overflow Int — use Double for cross-bridge safety.
1384
+ if (d.createdTime != null) {
1385
+ map.putDouble("created_time", d.createdTime!!.toDouble())
1386
+ }
1387
+ map.putString("project_id", d.projectId)
1388
+ map.putString("sipNumber", d.sipNumber)
1389
+ array.pushMap(map)
1390
+ }
1391
+ promise.resolve(array)
1392
+ }
1393
+ }
1394
+
1395
+ /// Verify whether THIS device is registered on backend for the current SIP
1396
+ /// user. Returns false early when not logged in.
1397
+ @ReactMethod
1398
+ fun isCurrentDeviceRegistered(promise: Promise) {
1399
+ val ctx = reactApplicationContext
1400
+ if (ctx == null) {
1401
+ promise.resolve(false)
1402
+ return
1403
+ }
1404
+ mainScope.launch {
1405
+ val registered = withContext(Dispatchers.IO) {
1406
+ try {
1407
+ OmiClient.getInstance(ctx).isCurrentDeviceRegistered()
1408
+ } catch (_: Throwable) {
1409
+ false
1410
+ }
1411
+ }
1412
+ promise.resolve(registered)
1413
+ }
1414
+ }
1415
+
1416
+ /// Returns true when a SIP user is set locally but no matching device exists
1417
+ /// on backend — i.e. user must logout + login again to re-register.
1418
+ @ReactMethod
1419
+ fun needsReLogin(promise: Promise) {
1420
+ val ctx = reactApplicationContext
1421
+ if (ctx == null) {
1422
+ promise.resolve(false)
1423
+ return
1424
+ }
1425
+ mainScope.launch {
1426
+ val needs = withContext(Dispatchers.IO) {
1427
+ try {
1428
+ OmiClient.getInstance(ctx).needsReLogin()
1429
+ } catch (_: Throwable) {
1430
+ false
1431
+ }
1432
+ }
1433
+ promise.resolve(needs)
1434
+ }
1435
+ }
1436
+
1308
1437
  @ReactMethod
1309
1438
  fun getAudio(promise: Promise) {
1310
1439
  val inputs = OmiClient.getInstance(reactApplicationContext!!).getAudioOutputs()
@@ -621,12 +621,16 @@ func startCall(_ phoneNumber: String, isVideo: Bool, completion: @escaping (_: S
621
621
  try? call.sendDTMF(character)
622
622
  }
623
623
 
624
- /// Toogle mtue
625
- func toggleMute() {
624
+ /// Toggle mute via CallKit so the native call UI (lock screen, banner) stays in sync.
625
+ /// Completion fires after performSetMutedCallAction: updates call.muted — safe to read there.
626
+ func toggleMute(completion: ((Error?) -> Void)? = nil) {
626
627
  guard let call = getAvailableCall() else {
628
+ completion?(nil)
627
629
  return
628
630
  }
629
- try? call.toggleMute()
631
+ omiLib.callManager.toggleMute(for: call) { error in
632
+ completion?(error)
633
+ }
630
634
  }
631
635
 
632
636
  /// Toogle hold
@@ -86,6 +86,11 @@ RCT_EXTERN_METHOD(toggleOmiVideo:(RCTPromiseResolveBlock)resolve
86
86
  RCT_EXTERN_METHOD(logout:(RCTPromiseResolveBlock)resolve
87
87
  rejecter:(RCTPromiseRejectBlock)reject)
88
88
 
89
+ // Logout and wait — resolves ONLY after the SDK has finished its async cleanup
90
+ // (HTTP devices/remove + local state reset). Use this before re-login.
91
+ RCT_EXTERN_METHOD(logoutAndWait:(RCTPromiseResolveBlock)resolve
92
+ rejecter:(RCTPromiseRejectBlock)reject)
93
+
89
94
  // Register video event
90
95
  RCT_EXTERN_METHOD(registerVideoEvent:(RCTPromiseResolveBlock)resolve
91
96
  rejecter:(RCTPromiseRejectBlock)reject)
@@ -135,6 +140,16 @@ RCT_EXTERN_METHOD(getAppId:(RCTPromiseResolveBlock)resolve
135
140
  RCT_EXTERN_METHOD(getVoipToken:(RCTPromiseResolveBlock)resolve
136
141
  rejecter:(RCTPromiseRejectBlock)reject)
137
142
 
143
+ // Backend device registration check APIs (OmiKit iOS 1.11.19 / OmiSDK Android 2.6.20)
144
+ RCT_EXTERN_METHOD(getOmiDevices:(RCTPromiseResolveBlock)resolve
145
+ rejecter:(RCTPromiseRejectBlock)reject)
146
+
147
+ RCT_EXTERN_METHOD(isCurrentDeviceRegistered:(RCTPromiseResolveBlock)resolve
148
+ rejecter:(RCTPromiseRejectBlock)reject)
149
+
150
+ RCT_EXTERN_METHOD(needsReLogin:(RCTPromiseResolveBlock)resolve
151
+ rejecter:(RCTPromiseRejectBlock)reject)
152
+
138
153
  // Get audio
139
154
  RCT_EXTERN_METHOD(getAudio:(RCTPromiseResolveBlock)resolve
140
155
  rejecter:(RCTPromiseRejectBlock)reject)
@@ -158,12 +158,13 @@ public class OmikitPlugin: RCTEventEmitter {
158
158
  }
159
159
 
160
160
  @objc(toggleMute:rejecter:)
161
- func toggleMute(resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) -> Void {
162
- CallManager.shareInstance().toggleMute()
163
- if let call = CallManager.shareInstance().getAvailableCall() {
164
- resolve(call.muted)
161
+ func toggleMute(resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) -> Void {
162
+ CallManager.shareInstance().toggleMute { [weak self] _ in
163
+ // Read call.muted AFTER CallKit completes — value is authoritative at this point.
164
+ let muted = CallManager.shareInstance().getAvailableCall()?.muted ?? false
165
+ resolve(muted)
166
+ self?.sendMuteStatus()
165
167
  }
166
- sendMuteStatus()
167
168
  }
168
169
 
169
170
  @objc(toggleSpeaker:rejecter:)
@@ -377,6 +378,21 @@ public class OmikitPlugin: RCTEventEmitter {
377
378
  CallManager.shareInstance().logout()
378
379
  resolve(true)
379
380
  }
381
+
382
+ /// Logout and wait for the SDK to fully clean up.
383
+ /// Uses OmiKit 1.11.x `logoutWithCompletion:` which fires on main thread
384
+ /// after HTTP devices/remove returns AND local SIP state (`currentSip`) is
385
+ /// reset. Safe to call `initCallWithUserPassword` immediately after this resolves.
386
+ ///
387
+ /// Resolves with the `success` flag from the SDK (true = backend confirmed
388
+ /// remove, false = local cleanup happened but server call failed — either
389
+ /// way the local state is now sane).
390
+ @objc(logoutAndWait:rejecter:)
391
+ func logoutAndWait(resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) -> Void {
392
+ OmiClient.logout { success in
393
+ resolve(success)
394
+ }
395
+ }
380
396
 
381
397
 
382
398
  @objc(registerVideoEvent:rejecter:)
@@ -537,6 +553,44 @@ public class OmikitPlugin: RCTEventEmitter {
537
553
  resolve(OmiClient.getVoipToken())
538
554
  }
539
555
 
556
+ // MARK: - Backend Device Registration Check APIs (OmiKit 1.11.19)
557
+
558
+ /// Fetch the list of devices currently registered on OMI backend for active SIP user.
559
+ /// Runs on a background queue because OmiKit performs a synchronous HTTP request.
560
+ @objc(getOmiDevices:rejecter:)
561
+ func getOmiDevices(resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) -> Void {
562
+ DispatchQueue.global(qos: .userInitiated).async {
563
+ let devices = OmiClient.getOmiDevices()
564
+ DispatchQueue.main.async {
565
+ resolve(devices)
566
+ }
567
+ }
568
+ }
569
+
570
+ /// Verify whether THIS device is registered on backend for the current SIP user.
571
+ /// Hits the network (via getOmiDevices) — dispatch off main thread.
572
+ @objc(isCurrentDeviceRegistered:rejecter:)
573
+ func isCurrentDeviceRegistered(resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) -> Void {
574
+ DispatchQueue.global(qos: .userInitiated).async {
575
+ let registered = OmiClient.isCurrentDeviceRegistered()
576
+ DispatchQueue.main.async {
577
+ resolve(registered)
578
+ }
579
+ }
580
+ }
581
+
582
+ /// Convenience guard built on top of isCurrentDeviceRegistered. Returns true
583
+ /// when a SIP user is set locally but no matching device exists on backend.
584
+ @objc(needsReLogin:rejecter:)
585
+ func needsReLogin(resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) -> Void {
586
+ DispatchQueue.global(qos: .userInitiated).async {
587
+ let needs = OmiClient.needsReLogin()
588
+ DispatchQueue.main.async {
589
+ resolve(needs)
590
+ }
591
+ }
592
+ }
593
+
540
594
  // MARK: - Audio Methods
541
595
  @objc(getAudio:rejecter:)
542
596
  func getAudio(resolve: @escaping RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) {
@@ -638,6 +692,70 @@ public class OmikitPlugin: RCTEventEmitter {
638
692
  ]
639
693
  }
640
694
 
695
+ // Event emitter — works across Old Architecture, New Architecture (Fabric),
696
+ // and bridgeless modes.
697
+ //
698
+ // Why the custom override:
699
+ // In NewArch, the JS side talks to the codegen TurboModule wrapper, which
700
+ // does NOT forward `addListener`/`removeListeners` to this Swift instance.
701
+ // RCTEventEmitter's private `_listenerCount` therefore stays at 0, and
702
+ // the default `sendEvent` short-circuits ("Sending X with no listeners
703
+ // registered") — dropping every event.
704
+ //
705
+ // The fix:
706
+ // 1. Forward `addListener`/`removeListeners` to super so the legacy
707
+ // counter is incremented when JS subscribes via the interop bridge.
708
+ // 2. Track an independent counter as a safety net for paths where super
709
+ // cannot increment (pure bridgeless callableJSModules-only mode).
710
+ // 3. In `sendEvent`, try `super.sendEvent` first (uses RN-wired dispatch
711
+ // for the current architecture), then fall back to direct calls to
712
+ // `RCTDeviceEventEmitter` via bridge or callableJSModules. JS code
713
+ // subscribes through the global `DeviceEventEmitter`, which is the
714
+ // same channel.
715
+ private var jsListenerCount: Int = 0
716
+
717
+ @objc public override func addListener(_ eventName: String!) {
718
+ super.addListener(eventName)
719
+ jsListenerCount += 1
720
+ if jsListenerCount == 1 {
721
+ startObserving()
722
+ }
723
+ }
724
+
725
+ @objc public override func removeListeners(_ count: Double) {
726
+ super.removeListeners(count)
727
+ jsListenerCount = max(0, jsListenerCount - Int(count))
728
+ if jsListenerCount == 0 {
729
+ stopObserving()
730
+ }
731
+ }
732
+
733
+ @objc public override func sendEvent(withName name: String!, body: Any!) {
734
+ // Preferred: super uses RN's wired dispatch for the active architecture.
735
+ // Requires the counter to be non-zero, which is guaranteed by our
736
+ // `addListener` forwarding above whenever the legacy bridge is involved.
737
+ if jsListenerCount > 0 {
738
+ super.sendEvent(withName: name, body: body)
739
+ return
740
+ }
741
+ // Fallback 1: bridge route — works on Old Arch and NewArch interop bridge.
742
+ if let bridge = self.bridge {
743
+ bridge.enqueueJSCall(
744
+ "RCTDeviceEventEmitter",
745
+ method: "emit",
746
+ args: body != nil ? [name as Any, body as Any] : [name as Any],
747
+ completion: nil
748
+ )
749
+ return
750
+ }
751
+ // Fallback 2: bridgeless mode — only callableJSModules is available.
752
+ self.callableJSModules?.invokeModule(
753
+ "RCTDeviceEventEmitter",
754
+ method: "emit",
755
+ withArgs: body != nil ? [name as Any, body as Any] : [name as Any]
756
+ )
757
+ }
758
+
641
759
  // MARK: - Stub Methods for TurboModule Compatibility
642
760
  // These methods are Android-only but required by Codegen spec
643
761
 
@@ -1 +1 @@
1
- {"version":3,"names":["_reactNative","require","_default","TurboModuleRegistry","get","exports","default"],"sourceRoot":"../../src","sources":["NativeOmikitPlugin.ts"],"mappings":";;;;;;AACA,IAAAA,YAAA,GAAAC,OAAA;AAAmD,IAAAC,QAAA,GAqKpCC,gCAAmB,CAACC,GAAG,CAAO,cAAc,CAAC;AAAAC,OAAA,CAAAC,OAAA,GAAAJ,QAAA"}
1
+ {"version":3,"names":["_reactNative","require","_default","TurboModuleRegistry","get","exports","default"],"sourceRoot":"../../src","sources":["NativeOmikitPlugin.ts"],"mappings":";;;;;;AACA,IAAAA,YAAA,GAAAC,OAAA;AAAmD,IAAAC,QAAA,GA8KpCC,gCAAmB,CAACC,GAAG,CAAO,cAAc,CAAC;AAAAC,OAAA,CAAAC,OAAA,GAAAJ,QAAA"}