Repository navigation
Conversation
…tion requestTransaction: only NSLogged a refused CXTransaction, so JavaScript never learned that, for example, a startCall was refused with MaximumCallGroupsReached; the app kept a call CallKit did not know about. Emit RNCallKeepDidFailCallAction with the call's UUID, the action, the CXErrorCodeRequestTransactionError name and the localized description. Adds the JS listener, the typings and the README entry. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01N7ztWHg5Dnq1KeknJQnfex
|
Thanks, a refused 1. The event is buffered from CallKit's private queue, which can crash the app. Test: 3,000 refused The root cause predates this PR: // requestTransaction:, error branch
NSDictionary *body = @{ /* same payload */ };
dispatch_async(dispatch_get_main_queue(), ^{
[self sendEventWithNameWrapper:RNCallKeepDidFailCallAction body:body];
});
RCT_EXPORT_METHOD(getInitialEvents:(RCTPromiseResolveBlock)resolve
reject:(RCTPromiseRejectBlock)reject)
{
dispatch_async(dispatch_get_main_queue(), ^{
resolve([self->_delayedEvents copy]);
});
}
RCT_EXPORT_METHOD(clearInitialEvents)
{
dispatch_async(dispatch_get_main_queue(), ^{
self->_delayedEvents = [NSMutableArray array];
});
}
2. A handler that cleans up with 3. The README example can hang up a live call. This one comes from reading the code, not from a test: an unentitled process can't hold a call. Note that #877 also changes how |
|
Update: I ran the same tests on a real iPhone 15 Pro Max (iOS 26.6.1) with the real CallKit. That includes point 3, which I had only from reading the code.
|
|
Thanks again for this, @mjerris. I've taken it further in #911: a
Closing this one in favor of #911. Reviews and device checks there are welcome. |
This came out of building and device-testing a SignalWire React Native wrapper on iOS 27 and Android 17, which uses callkeep for CallKit and ConnectionService. The case that prompted it: a refused
startCall, which leaves the app with a call CallKit knows nothing about and no event saying so.Summary
requestTransaction:(ios/RNCallKeep/RNCallKeep.m) now emits a new event,RNCallKeepDidFailCallAction(JS:didFailCallAction), whenCXCallControllercompletes a transaction with an error. Until now it onlyNSLogged the error. The existing NSLog stays.
Payload:
The field names and the PascalCase error-code names follow the existing
didDisplayIncomingCallpayload (error,errorCode: 'CallUUIDAlreadyExists' | …), so the two failure events read the same way.errorCodeis mapped fromCXErrorDomainRequestTransactioncodes 1–8. An error from another domain, oran unlisted code, becomes
Unknown. Code 8 (CallIsProtected) is matched as aliteral so the file still builds with SDKs that predate it.
Motivation
A refused
CXStartCallActionis invisible to JavaScript today. The commoncase is
startCallwhile a call is active withmaximumCallGroups: '1'.CallKit refuses with
MaximumCallGroupsReached: no outgoing call UI appears,didReceiveStartCallActionnever fires, and the only trace isin the device log. The app has already started its own call (a SIP/WebRTC leg)
for that UUID, and nothing tells it to stop. It keeps a phantom call outside
CallKit: no system UI, no audio session from CallKit, and nothing for the user
to end it with. The same blind spot applies to a refused answer, end, hold,
mute or DTMF action (for example
UnknownCallUUIDafter the call has alreadygone).
With the event, an app can drop or hang up the call it started:
Changes
ios/RNCallKeep/RNCallKeep.m: the event name, an entry insupportedEvents, the emission in therequestTransaction:error branch,and two helpers,
getCallActionName:andgetRequestTransactionErrorCode:.The event goes through
sendEventWithNameWrapper:, so one sent before JS islistening is replayed in
didLoadWithEvents, like the other events.actions.js: thedidFailCallActionlistener.index.d.ts:NativeEvents.didFailCallActionand itsEventsPayloadtype.README.md: the events table row and adidFailCallActionsection with anexample.
Android: no change. Telecom reports its own failures there
(
onCreateOutgoingConnectionFailed/createIncomingConnectionFailed), andrequestTransaction:is iOS only.Behaviour change
None, apart from the new event. Nothing that worked before behaves
differently. The NSLog is still written, and no existing event, method or
payload changed.
Testing
clang -fsyntax-only -fobjc-arc -fmodulesforarm64-apple-ios15.1 against the iPhoneOS SDK (Xcode 27), with React Native
0.81.5's headers. It compiles with no errors, and gives the same 22 warnings
as
master(all pre-existing: nullability and deprecations). The file wasnot built in an app, and the event was not seen on a device.
tsccheck of adidFailCallActionlistener againstthe updated
index.d.ts. It passes, and a deliberately wrong type isrejected, which shows the payload types are in effect.
check loaded
actions.jswith a mockedNativeEventEmitter, attachedlisteners.didFailCallAction, emittedRNCallKeepDidFailCallAction, and gotthe payload in the handler.
maximumCallGroups: '1', start a call,then
startCalla second UUID. ExpectdidFailCallActionwithaction: 'startCall'anderrorCode: 'MaximumCallGroupsReached'.Release note: npm lags the repo
npm's
latestis still 4.3.16, published 2024-11-18(
npm view react-native-callkeep time).masterhas a "release 4.3.17"commit and a
4.3.17tag (2026-09-10), but 4.3.17 was never published.masteris 15 commits (8 non-merge) ahead of the 4.3.16 commit and 6 ahead of the
4.3.17tag. Apps installing from npm get none of that work, and they wouldnot get this event either until a release ships. A published 4.3.17 (or
4.3.18 with this change) would help.
🤖 Generated with Claude Code
https://claude.ai/code/session_01N7ztWHg5Dnq1KeknJQnfex