From 6b000e6e23eb08d172270221e149da60ae77d5c3 Mon Sep 17 00:00:00 2001 From: Jerry Hu Date: Thu, 4 Jun 2026 20:33:16 -0400 Subject: [PATCH 1/7] Add ability to prevent unmute for active calls --- src/android/CordovaCall.java | 3 +++ src/ios/CordovaCall.h | 1 + src/ios/CordovaCall.m | 38 +++++++++++++++++++++++++++++++++++- www/CordovaCall.js | 8 ++++++++ 4 files changed, 49 insertions(+), 1 deletion(-) diff --git a/src/android/CordovaCall.java b/src/android/CordovaCall.java index 083694c..7a17ec9 100644 --- a/src/android/CordovaCall.java +++ b/src/android/CordovaCall.java @@ -310,6 +310,9 @@ public void run() { this.unmute(); this.callbackContext.success("Unmuted Successfully"); return true; + } else if (action.equals("setAllowUnmute")) { + callbackContext.success(); + return true; } else if (action.equals("speakerOn")) { this.speakerOn(); return true; diff --git a/src/ios/CordovaCall.h b/src/ios/CordovaCall.h index a0b76ee..c9461f3 100644 --- a/src/ios/CordovaCall.h +++ b/src/ios/CordovaCall.h @@ -26,6 +26,7 @@ - (void)setRingtone:(CDVInvokedUrlCommand*)command; - (void)setIncludeInRecents:(CDVInvokedUrlCommand*)command; - (void)setDTMFState:(CDVInvokedUrlCommand*)command; +- (void)setAllowUnmute:(CDVInvokedUrlCommand*)command; - (void)setVideo:(CDVInvokedUrlCommand*)command; - (void)receiveCall:(CDVInvokedUrlCommand*)command; diff --git a/src/ios/CordovaCall.m b/src/ios/CordovaCall.m index 053b5e1..3294dfa 100644 --- a/src/ios/CordovaCall.m +++ b/src/ios/CordovaCall.m @@ -289,6 +289,20 @@ - (void)setDTMFState:(CDVInvokedUrlCommand*)command [self.commandDelegate sendPluginResult:pluginResult callbackId:command.callbackId]; } +- (void)setAllowUnmute:(CDVInvokedUrlCommand*)command +{ + NSString *sessionId = [command.arguments objectAtIndex:0]; + BOOL value = [[command.arguments objectAtIndex:1] boolValue]; + CDVPluginResult *pluginResult = nil; + if (self.activeCalls[sessionId]) { + self.activeCalls[sessionId][@"allowUnmute"] = @(value); + pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsString:@"allowUnmute Changed Successfully"]; + } else { + pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsString:@"No active call for sessionId"]; + } + [self.commandDelegate sendPluginResult:pluginResult callbackId:command.callbackId]; +} + - (void)setVideo:(CDVInvokedUrlCommand*)command { CDVPluginResult* pluginResult = nil; @@ -1113,6 +1127,27 @@ - (void)provider:(CXProvider *)provider performSetMutedCallAction:(CXSetMutedCal } [self logMessage:[NSString stringWithFormat:@"CallKit performSetMutedCallAction received %@ event, sessionId: %@", isMuted ? @"mute" : @"unmute", sessionId]]; + // If this is an unmute request and allowUnmute is NO for this session, fail the action. + BOOL allowUnmute = [self.activeCalls[sessionId][@"allowUnmute"] boolValue]; + if (!isMuted && !allowUnmute) { + [self logMessage:@"performSetMutedCallAction: allowUnmute is NO, reject unmute action"]; + // Failing the action will cause CallKit to revert the UI toggle back to "muted" state, + // which is the desired behavior when unmuting is disallowed. + NSMutableDictionary *entry = self.activeCalls[sessionId][@"callbackMap"][action.UUID.UUIDString]; + if (entry) { + // UI-initiated mute/unmute: emit to event listeners only. + [self.activeCalls[sessionId][@"callbackMap"] removeObjectForKey:action.UUID.UUIDString]; + NSString *cbId = entry[@"callbackId"]; + if (cbId) { + NSDictionary *resultDict = @{ @"message": @"unmute not permitted", @"sessionId": sessionId }; + CDVPluginResult *result = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsDictionary:resultDict]; + [self.commandDelegate sendPluginResult:result callbackId:cbId]; + } + } + [action fail]; + return; + } + [action fulfill]; if (self.activeCalls[sessionId][@"callbackMap"][action.UUID.UUIDString]) { @@ -1224,7 +1259,8 @@ - (NSMutableDictionary *)newActiveCallEntryWithUUID:(NSUUID *)callUUID { @"callbackMap": [NSMutableDictionary dictionary], @"pendingActivateAudioSessionEmits": [NSMutableArray array], @"pendingDeactivateAudioSessionEmits": [NSMutableArray array], - @"pendingDismiss": @NO + @"pendingDismiss": @NO, + @"allowUnmute": @YES } mutableCopy]; } diff --git a/www/CordovaCall.js b/www/CordovaCall.js index 6ecbb67..a00c9f2 100644 --- a/www/CordovaCall.js +++ b/www/CordovaCall.js @@ -44,6 +44,14 @@ exports.setDTMFState = function (value, success, error) { } }; +exports.setAllowUnmute = function (sessionId, value, success, error) { + if (typeof value == "boolean") { + exec(success, error, "CordovaCall", "setAllowUnmute", [sessionId, value]); + } else { + error("Value Must Be True Or False"); + } +}; + exports.setVideo = function (value, success, error) { if (typeof value == "boolean") { exec(success, error, "CordovaCall", "setVideo", [value]); From 52972622bf24b2d1273c358c18c933a39dd360e3 Mon Sep 17 00:00:00 2001 From: Jerry Hu <4749863+jerry2013@users.noreply.github.com> Date: Thu, 4 Jun 2026 20:48:02 -0400 Subject: [PATCH 2/7] Apply suggestions from code review Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> Co-authored-by: Jerry Hu <4749863+jerry2013@users.noreply.github.com> --- src/android/CordovaCall.java | 2 +- src/ios/CordovaCall.m | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/android/CordovaCall.java b/src/android/CordovaCall.java index 7a17ec9..2e7b7b1 100644 --- a/src/android/CordovaCall.java +++ b/src/android/CordovaCall.java @@ -311,7 +311,7 @@ public void run() { this.callbackContext.success("Unmuted Successfully"); return true; } else if (action.equals("setAllowUnmute")) { - callbackContext.success(); + callbackContext.error("setAllowUnmute is not supported on Android"); return true; } else if (action.equals("speakerOn")) { this.speakerOn(); diff --git a/src/ios/CordovaCall.m b/src/ios/CordovaCall.m index 3294dfa..ae4f0c5 100644 --- a/src/ios/CordovaCall.m +++ b/src/ios/CordovaCall.m @@ -1135,7 +1135,6 @@ - (void)provider:(CXProvider *)provider performSetMutedCallAction:(CXSetMutedCal // which is the desired behavior when unmuting is disallowed. NSMutableDictionary *entry = self.activeCalls[sessionId][@"callbackMap"][action.UUID.UUIDString]; if (entry) { - // UI-initiated mute/unmute: emit to event listeners only. [self.activeCalls[sessionId][@"callbackMap"] removeObjectForKey:action.UUID.UUIDString]; NSString *cbId = entry[@"callbackId"]; if (cbId) { From 3e312621bd7d9f7b03014c7776a85a0b82154341 Mon Sep 17 00:00:00 2001 From: Jerry Hu Date: Fri, 5 Jun 2026 08:40:29 -0400 Subject: [PATCH 3/7] fix error handling --- www/CordovaCall.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/www/CordovaCall.js b/www/CordovaCall.js index a00c9f2..840c531 100644 --- a/www/CordovaCall.js +++ b/www/CordovaCall.js @@ -32,7 +32,7 @@ exports.setIncludeInRecents = function (value, success, error) { if (typeof value == "boolean") { exec(success, error, "CordovaCall", "setIncludeInRecents", [value]); } else { - error("Value Must Be True Or False"); + if (typeof error == "function") { error("Value Must Be True Or False"); } } }; @@ -40,7 +40,7 @@ exports.setDTMFState = function (value, success, error) { if (typeof value == "boolean") { exec(success, error, "CordovaCall", "setDTMFState", [value]); } else { - error("Value Must Be True Or False"); + if (typeof error == "function") { error("Value Must Be True Or False"); } } }; @@ -48,7 +48,7 @@ exports.setAllowUnmute = function (sessionId, value, success, error) { if (typeof value == "boolean") { exec(success, error, "CordovaCall", "setAllowUnmute", [sessionId, value]); } else { - error("Value Must Be True Or False"); + if (typeof error == "function") { error("Value Must Be True Or False"); } } }; @@ -56,7 +56,7 @@ exports.setVideo = function (value, success, error) { if (typeof value == "boolean") { exec(success, error, "CordovaCall", "setVideo", [value]); } else { - error("Value Must Be True Or False"); + if (typeof error == "function") { error("Value Must Be True Or False"); } } }; From 0b5ec9fddd10a23ab273d88894010b96ec61e274 Mon Sep 17 00:00:00 2001 From: Jerry Hu Date: Fri, 5 Jun 2026 08:41:43 -0400 Subject: [PATCH 4/7] ios only --- www/CordovaCall.js | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/www/CordovaCall.js b/www/CordovaCall.js index 840c531..9faba10 100644 --- a/www/CordovaCall.js +++ b/www/CordovaCall.js @@ -44,14 +44,6 @@ exports.setDTMFState = function (value, success, error) { } }; -exports.setAllowUnmute = function (sessionId, value, success, error) { - if (typeof value == "boolean") { - exec(success, error, "CordovaCall", "setAllowUnmute", [sessionId, value]); - } else { - if (typeof error == "function") { error("Value Must Be True Or False"); } - } -}; - exports.setVideo = function (value, success, error) { if (typeof value == "boolean") { exec(success, error, "CordovaCall", "setVideo", [value]); @@ -162,6 +154,14 @@ exports.log = function (message) { exec(null, null, "CordovaCall", "log", [message]); } +exports.setAllowUnmute = function (sessionId, value, success, error) { + if (typeof value == "boolean") { + exec(success, error, "CordovaCall", "setAllowUnmute", [sessionId, value]); + } else { + if (typeof error == "function") { error("Value Must Be True Or False"); } + } +}; + exports.keepAlive = function (callback) { exec(callback, null, "CordovaCall", "keepAlive", []); } From 5b48d62adf9480ce9f4b4028a306776af2c022c9 Mon Sep 17 00:00:00 2001 From: Jerry Hu Date: Fri, 5 Jun 2026 09:54:25 -0400 Subject: [PATCH 5/7] Enhance mute/unmute handling by pre-populating callbackMap and improving error management --- src/ios/CordovaCall.m | 52 +++++++++++++++++++++---------------------- 1 file changed, 25 insertions(+), 27 deletions(-) diff --git a/src/ios/CordovaCall.m b/src/ios/CordovaCall.m index ae4f0c5..ae49d4b 100644 --- a/src/ios/CordovaCall.m +++ b/src/ios/CordovaCall.m @@ -526,10 +526,13 @@ - (void)mute:(CDVInvokedUrlCommand*)command CXSetMutedCallAction *muteAction = [[CXSetMutedCallAction alloc] initWithCallUUID:call.UUID muted:YES]; CXTransaction *transaction = [[CXTransaction alloc] initWithAction:muteAction]; [self logMessage:[NSString stringWithFormat:@"Programmatically Muting Call: %@", sessionId]]; + // Pre-populate callbackMap before requestTransaction: performSetMutedCallAction fires + // before the requestTransaction completion block, so the entry must already be present. + self.activeCalls[sessionId][@"callbackMap"][muteAction.UUID.UUIDString] = [@{ @"callbackId": command.callbackId, @"event": @"mute" } mutableCopy]; [self.callController requestTransaction:transaction completion:^(NSError * _Nullable error) { - if (error == nil) { - self.activeCalls[sessionId][@"callbackMap"][muteAction.UUID.UUIDString] = [@{ @"callbackId": command.callbackId, @"event": @"mute" } mutableCopy]; - } else { + if (error != nil) { + // Transaction was rejected before reaching performSetMutedCallAction — clean up. + [self.activeCalls[sessionId][@"callbackMap"] removeObjectForKey:muteAction.UUID.UUIDString]; [self logMessage:@"Error occurred muting Call"]; NSDictionary *resultDict = @{ @"message": @"An error occurred", @"sessionId": sessionId }; CDVPluginResult *pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsDictionary:resultDict]; @@ -552,10 +555,13 @@ - (void)unmute:(CDVInvokedUrlCommand*)command CXSetMutedCallAction *unmuteAction = [[CXSetMutedCallAction alloc] initWithCallUUID:call.UUID muted:NO]; CXTransaction *transaction = [[CXTransaction alloc] initWithAction:unmuteAction]; [self logMessage:[NSString stringWithFormat:@"Programmatically Unmuting Call: %@", sessionId]]; + // Pre-populate callbackMap before requestTransaction: performSetMutedCallAction fires + // before the requestTransaction completion block, so the entry must already be present. + self.activeCalls[sessionId][@"callbackMap"][unmuteAction.UUID.UUIDString] = [@{ @"callbackId": command.callbackId, @"event": @"unmute" } mutableCopy]; [self.callController requestTransaction:transaction completion:^(NSError * _Nullable error) { - if (error == nil) { - self.activeCalls[sessionId][@"callbackMap"][unmuteAction.UUID.UUIDString] = [@{ @"callbackId": command.callbackId, @"event": @"unmute" } mutableCopy]; - } else { + if (error != nil) { + // Transaction was rejected before reaching performSetMutedCallAction — clean up. + [self.activeCalls[sessionId][@"callbackMap"] removeObjectForKey:unmuteAction.UUID.UUIDString]; [self logMessage:@"Error occurred unmuting Call"]; NSDictionary *resultDict = @{ @"message": @"An error occurred", @"sessionId": sessionId }; CDVPluginResult *pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsDictionary:resultDict]; @@ -1127,32 +1133,24 @@ - (void)provider:(CXProvider *)provider performSetMutedCallAction:(CXSetMutedCal } [self logMessage:[NSString stringWithFormat:@"CallKit performSetMutedCallAction received %@ event, sessionId: %@", isMuted ? @"mute" : @"unmute", sessionId]]; - // If this is an unmute request and allowUnmute is NO for this session, fail the action. - BOOL allowUnmute = [self.activeCalls[sessionId][@"allowUnmute"] boolValue]; - if (!isMuted && !allowUnmute) { - [self logMessage:@"performSetMutedCallAction: allowUnmute is NO, reject unmute action"]; - // Failing the action will cause CallKit to revert the UI toggle back to "muted" state, - // which is the desired behavior when unmuting is disallowed. - NSMutableDictionary *entry = self.activeCalls[sessionId][@"callbackMap"][action.UUID.UUIDString]; - if (entry) { - [self.activeCalls[sessionId][@"callbackMap"] removeObjectForKey:action.UUID.UUIDString]; - NSString *cbId = entry[@"callbackId"]; - if (cbId) { - NSDictionary *resultDict = @{ @"message": @"unmute not permitted", @"sessionId": sessionId }; - CDVPluginResult *result = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsDictionary:resultDict]; - [self.commandDelegate sendPluginResult:result callbackId:cbId]; - } - } - [action fail]; + if (self.activeCalls[sessionId][@"callbackMap"][action.UUID.UUIDString]) { + [action fulfill]; + + // Programmatic mute/unmute: resolve the JS promise only. + [self resolveCommandForSessionId:sessionId actionUUIDString:action.UUID.UUIDString]; return; } - [action fulfill]; + BOOL allowUnmute = [self.activeCalls[sessionId][@"allowUnmute"] boolValue]; + if (!isMuted && !allowUnmute) { + // If this is an unmute request and allowUnmute is NO for this session, fail the action. + [action fail]; - if (self.activeCalls[sessionId][@"callbackMap"][action.UUID.UUIDString]) { - // Programmatic mute/unmute: resolve the JS promise only. - [self resolveCommandForSessionId:sessionId actionUUIDString:action.UUID.UUIDString]; + [self logMessage:@"performSetMutedCallAction: allowUnmute is NO, reject unmute action"]; + return; } else { + [action fulfill]; + // UI-initiated mute/unmute: emit to event listeners only. for (id callbackId in callbackIds[isMuted ? @"mute" : @"unmute"]) { [self logMessage:[NSString stringWithFormat:@"Sending %@ event to JS", isMuted ? @"mute" : @"unmute"]]; From df2312657f635179335153c3d2ca64a2d615f519 Mon Sep 17 00:00:00 2001 From: Jerry Hu Date: Fri, 5 Jun 2026 14:11:38 -0400 Subject: [PATCH 6/7] Bump version to 2.2.3 --- package.json | 2 +- plugin.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 29c53c0..ae7a2f1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "cordova-plugin-callkit", - "version": "2.2.2", + "version": "2.2.3", "description": "Cordova plugin that lets you use iOS CallKit UI (with PushKit) and Android ConnectionService UI", "cordova": { "id": "cordova-plugin-callkit", diff --git a/plugin.xml b/plugin.xml index ab0e2f1..5417b40 100644 --- a/plugin.xml +++ b/plugin.xml @@ -1,5 +1,5 @@ - + Cordova CallKit From 9c2711008c414fc33191a42b9ab283415c7ebc32 Mon Sep 17 00:00:00 2001 From: Jerry Hu <4749863+jerry2013@users.noreply.github.com> Date: Fri, 5 Jun 2026 14:21:59 -0400 Subject: [PATCH 7/7] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- src/android/CordovaCall.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/android/CordovaCall.java b/src/android/CordovaCall.java index 2e7b7b1..250a8fc 100644 --- a/src/android/CordovaCall.java +++ b/src/android/CordovaCall.java @@ -311,7 +311,7 @@ public void run() { this.callbackContext.success("Unmuted Successfully"); return true; } else if (action.equals("setAllowUnmute")) { - callbackContext.error("setAllowUnmute is not supported on Android"); + this.callbackContext.error("setAllowUnmute is not supported on Android"); return true; } else if (action.equals("speakerOn")) { this.speakerOn();