@stream-io/react-native-callingx 0.3.1 → 0.5.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/CHANGELOG.md +16 -0
- package/Callingx.podspec +1 -1
- package/android/src/main/java/io/getstream/rn/callingx/notifications/CallNotificationManager.kt +18 -1
- package/dist/module/spec/NativeCallingx.js.map +1 -1
- package/dist/typescript/src/spec/NativeCallingx.d.ts +9 -2
- package/dist/typescript/src/spec/NativeCallingx.d.ts.map +1 -1
- package/dist/typescript/src/types.d.ts +17 -6
- package/dist/typescript/src/types.d.ts.map +1 -1
- package/ios/AudioSessionManager.swift +49 -17
- package/ios/Callingx.mm +11 -6
- package/ios/CallingxCall.swift +3 -5
- package/ios/CallingxImpl.swift +266 -210
- package/ios/CallingxLog.swift +67 -0
- package/ios/CallingxSessionOwnership.swift +37 -0
- package/ios/Settings.swift +2 -6
- package/ios/UUIDStorage.swift +10 -30
- package/ios/VoipNotificationsManager.swift +20 -43
- package/ios/VoipPushHandler.h +29 -0
- package/ios/VoipPushHandler.m +195 -0
- package/package.json +10 -10
- package/src/spec/NativeCallingx.ts +9 -2
- package/src/types.ts +13 -6
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import os
|
|
3
|
+
|
|
4
|
+
/// Unified-logging namespace for the Callingx package.
|
|
5
|
+
///
|
|
6
|
+
/// Replaces the previous `NSLog` usage. Each category maps to one of the
|
|
7
|
+
/// `[Tag]` prefixes that used to be hand-built into the log strings, so logs
|
|
8
|
+
/// can be filtered by subsystem/category in Console.app or `log stream`:
|
|
9
|
+
///
|
|
10
|
+
/// log stream --predicate 'subsystem == "io.getstream.callingx"' --level debug
|
|
11
|
+
///
|
|
12
|
+
/// The subsystem matches the queue-label convention already used across this
|
|
13
|
+
/// package (e.g. `io.getstream.callingx.pendingActions`).
|
|
14
|
+
enum CallingxLog {
|
|
15
|
+
private static let subsystem = "io.getstream.callingx"
|
|
16
|
+
|
|
17
|
+
static let core = Logger(subsystem: subsystem, category: "Callingx")
|
|
18
|
+
static let uuid = Logger(subsystem: subsystem, category: "UUIDStorage")
|
|
19
|
+
static let voip = Logger(subsystem: subsystem, category: "VoipNotifications")
|
|
20
|
+
static let push = Logger(subsystem: subsystem, category: "VoipPush")
|
|
21
|
+
static let settings = Logger(subsystem: subsystem, category: "Settings")
|
|
22
|
+
static let audio = Logger(subsystem: subsystem, category: "AudioSession")
|
|
23
|
+
static let js = Logger(subsystem: subsystem, category: "JS")
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
extension Logger {
|
|
27
|
+
/// Logs a pre-built message at `.debug`, marking the whole string public so
|
|
28
|
+
/// values stay readable when streaming logs (incl. release/TestFlight).
|
|
29
|
+
/// `@autoclosure` keeps construction lazy: when the log is not being
|
|
30
|
+
/// collected the string is never built.
|
|
31
|
+
func debugPublic(_ message: @autoclosure @escaping () -> String) {
|
|
32
|
+
debug("\(message(), privacy: .public)")
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/// Public-message variant at `.error` (persisted by default in the unified log).
|
|
36
|
+
func errorPublic(_ message: @autoclosure @escaping () -> String) {
|
|
37
|
+
error("\(message(), privacy: .public)")
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/// Objective-C bridge so `Callingx.mm` and `VoipPushHandler.m` log through the
|
|
42
|
+
/// same `os.Logger` path. `Logger`'s interpolation API is Swift-only, hence the
|
|
43
|
+
/// thin `@objc` wrapper.
|
|
44
|
+
@objc public final class CallingxLogBridge: NSObject {
|
|
45
|
+
@objc public static func pushDebug(_ message: String) {
|
|
46
|
+
CallingxLog.push.debugPublic(message)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
@objc public static func pushError(_ message: String) {
|
|
50
|
+
CallingxLog.push.errorPublic(message)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/// Routes JS-originated logs (from the `log(message, level)` TurboModule
|
|
54
|
+
/// method) to the matching os.Logger severity.
|
|
55
|
+
@objc public static func js(_ message: String, level: String) {
|
|
56
|
+
switch level {
|
|
57
|
+
case "error":
|
|
58
|
+
CallingxLog.js.error("\(message, privacy: .public)")
|
|
59
|
+
case "warn":
|
|
60
|
+
CallingxLog.js.warning("\(message, privacy: .public)")
|
|
61
|
+
case "info":
|
|
62
|
+
CallingxLog.js.info("\(message, privacy: .public)")
|
|
63
|
+
default:
|
|
64
|
+
CallingxLog.js.debug("\(message, privacy: .public)")
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
/// Cross-package handoff guard. `CallingxImpl` flips `callingxOwnsSession`
|
|
4
|
+
/// to `true` at CX-action entry points (`CXStartCallAction.perform`,
|
|
5
|
+
/// `CXAnswerCallAction.perform`, re-asserted in `provider(_:didActivate:)`)
|
|
6
|
+
/// and back to `false` in `provider(_:didDeactivate:)` when no calls remain
|
|
7
|
+
/// (`UUIDStorage.count() == 0`) or on `providerDidReset`.
|
|
8
|
+
///
|
|
9
|
+
/// `StreamInCallManager` (in `@stream-io/video-react-native-sdk`) consults this
|
|
10
|
+
/// flag from its AudioDeviceModule publisher sink and no-ops when callingx owns
|
|
11
|
+
/// the session, so the two packages don't write conflicting `AVAudioSession`
|
|
12
|
+
/// configurations during the transient overlap when a CallKit call is winding
|
|
13
|
+
/// down and the SDK is about to take over (or vice versa).
|
|
14
|
+
///
|
|
15
|
+
/// Exposed as an `@objcMembers NSObject` with an `@objc class var` so that
|
|
16
|
+
/// react-native-sdk can read it via `NSClassFromString("Callingx.CallingxSessionOwnership")`
|
|
17
|
+
/// + KVC on the class object — `@stream-io/react-native-callingx` is an *optional*
|
|
18
|
+
/// peer dependency of react-native-sdk, so a direct `import Callingx` is not safe.
|
|
19
|
+
@objcMembers
|
|
20
|
+
public class CallingxSessionOwnership: NSObject {
|
|
21
|
+
|
|
22
|
+
private static let lock = NSLock()
|
|
23
|
+
private static var _callingxOwnsSession: Bool = false
|
|
24
|
+
|
|
25
|
+
@objc public class var callingxOwnsSession: Bool {
|
|
26
|
+
get {
|
|
27
|
+
lock.lock()
|
|
28
|
+
defer { lock.unlock() }
|
|
29
|
+
return _callingxOwnsSession
|
|
30
|
+
}
|
|
31
|
+
set {
|
|
32
|
+
lock.lock()
|
|
33
|
+
defer { lock.unlock() }
|
|
34
|
+
_callingxOwnsSession = newValue
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
package/ios/Settings.swift
CHANGED
|
@@ -10,9 +10,7 @@ import UIKit
|
|
|
10
10
|
}
|
|
11
11
|
|
|
12
12
|
public static func setSettings(_ options: [String: Any]?) {
|
|
13
|
-
|
|
14
|
-
NSLog("%@","[Settings][setSettings] options = \(String(describing: options))")
|
|
15
|
-
#endif
|
|
13
|
+
CallingxLog.settings.debugPublic("[setSettings] options = \(String(describing: options))")
|
|
16
14
|
|
|
17
15
|
var settings: [String: Any] = getSettings()
|
|
18
16
|
|
|
@@ -45,9 +43,7 @@ import UIKit
|
|
|
45
43
|
}
|
|
46
44
|
|
|
47
45
|
public static func getProviderConfiguration() -> CXProviderConfiguration {
|
|
48
|
-
|
|
49
|
-
NSLog("%@","[Settings][getProviderConfiguration]")
|
|
50
|
-
#endif
|
|
46
|
+
CallingxLog.settings.debugPublic("[getProviderConfiguration]")
|
|
51
47
|
|
|
52
48
|
let settings = getSettings()
|
|
53
49
|
let providerConfiguration = CXProviderConfiguration()
|
package/ios/UUIDStorage.swift
CHANGED
|
@@ -17,9 +17,7 @@ import Foundation
|
|
|
17
17
|
public func getOrCreateCall(forCid cid: String, isOutgoing: Bool = false) -> CallingxCall {
|
|
18
18
|
return queue.sync {
|
|
19
19
|
if let existing = callsByCid[cid] {
|
|
20
|
-
|
|
21
|
-
NSLog("%@","[UUIDStorage] getOrCreateCall: found existing \(existing)")
|
|
22
|
-
#endif
|
|
20
|
+
CallingxLog.uuid.debugPublic("getOrCreateCall: found existing \(existing)")
|
|
23
21
|
return existing
|
|
24
22
|
}
|
|
25
23
|
|
|
@@ -28,9 +26,7 @@ import Foundation
|
|
|
28
26
|
let uuidString = uuid.uuidString.lowercased()
|
|
29
27
|
callsByCid[cid] = call
|
|
30
28
|
callsByUUID[uuidString] = call
|
|
31
|
-
|
|
32
|
-
NSLog("%@","[UUIDStorage] getOrCreateCall: created \(call)")
|
|
33
|
-
#endif
|
|
29
|
+
CallingxLog.uuid.debugPublic("getOrCreateCall: created \(call)")
|
|
34
30
|
return call
|
|
35
31
|
}
|
|
36
32
|
}
|
|
@@ -62,9 +58,7 @@ import Foundation
|
|
|
62
58
|
public func getOrCreateUUID(forCid cid: String) -> UUID {
|
|
63
59
|
return queue.sync {
|
|
64
60
|
if let existing = callsByCid[cid] {
|
|
65
|
-
|
|
66
|
-
NSLog("%@","[UUIDStorage] getUUIDForCid: found existing UUID \(existing.uuid.uuidString.lowercased()) for cid \(cid)")
|
|
67
|
-
#endif
|
|
61
|
+
CallingxLog.uuid.debugPublic("getUUIDForCid: found existing UUID \(existing.uuid.uuidString.lowercased()) for cid \(cid)")
|
|
68
62
|
return existing.uuid
|
|
69
63
|
}
|
|
70
64
|
|
|
@@ -73,9 +67,7 @@ import Foundation
|
|
|
73
67
|
let uuidString = uuid.uuidString.lowercased()
|
|
74
68
|
callsByCid[cid] = call
|
|
75
69
|
callsByUUID[uuidString] = call
|
|
76
|
-
|
|
77
|
-
NSLog("%@","[UUIDStorage] getUUIDForCid: created new UUID \(uuidString) for cid \(cid)")
|
|
78
|
-
#endif
|
|
70
|
+
CallingxLog.uuid.debugPublic("getUUIDForCid: created new UUID \(uuidString) for cid \(cid)")
|
|
79
71
|
return uuid
|
|
80
72
|
}
|
|
81
73
|
}
|
|
@@ -90,9 +82,7 @@ import Foundation
|
|
|
90
82
|
return queue.sync {
|
|
91
83
|
let uuidString = uuid.uuidString.lowercased()
|
|
92
84
|
let cid = callsByUUID[uuidString]?.cid
|
|
93
|
-
|
|
94
|
-
NSLog("%@","[UUIDStorage] getCidForUUID: UUID \(uuidString) -> cid \(cid ?? "(not found)")")
|
|
95
|
-
#endif
|
|
85
|
+
CallingxLog.uuid.debugPublic("getCidForUUID: UUID \(uuidString) -> cid \(cid ?? "(not found)")")
|
|
96
86
|
return cid
|
|
97
87
|
}
|
|
98
88
|
}
|
|
@@ -103,13 +93,9 @@ import Foundation
|
|
|
103
93
|
if let call = callsByUUID[uuidString] {
|
|
104
94
|
callsByCid.removeValue(forKey: call.cid)
|
|
105
95
|
callsByUUID.removeValue(forKey: uuidString)
|
|
106
|
-
|
|
107
|
-
NSLog("%@","[UUIDStorage] removeCidForUUID: removed cid \(call.cid) for UUID \(uuidString)")
|
|
108
|
-
#endif
|
|
96
|
+
CallingxLog.uuid.debugPublic("removeCidForUUID: removed cid \(call.cid) for UUID \(uuidString)")
|
|
109
97
|
} else {
|
|
110
|
-
|
|
111
|
-
NSLog("%@","[UUIDStorage] removeCidForUUID: no cid found for UUID \(uuidString)")
|
|
112
|
-
#endif
|
|
98
|
+
CallingxLog.uuid.debugPublic("removeCidForUUID: no cid found for UUID \(uuidString)")
|
|
113
99
|
}
|
|
114
100
|
}
|
|
115
101
|
}
|
|
@@ -120,13 +106,9 @@ import Foundation
|
|
|
120
106
|
let uuidString = call.uuid.uuidString.lowercased()
|
|
121
107
|
callsByUUID.removeValue(forKey: uuidString)
|
|
122
108
|
callsByCid.removeValue(forKey: cid)
|
|
123
|
-
|
|
124
|
-
NSLog("%@","[UUIDStorage] removeCid: removed cid \(cid) with UUID \(uuidString)")
|
|
125
|
-
#endif
|
|
109
|
+
CallingxLog.uuid.debugPublic("removeCid: removed cid \(cid) with UUID \(uuidString)")
|
|
126
110
|
} else {
|
|
127
|
-
|
|
128
|
-
NSLog("%@","[UUIDStorage] removeCid: no UUID found for cid \(cid)")
|
|
129
|
-
#endif
|
|
111
|
+
CallingxLog.uuid.debugPublic("removeCid: no UUID found for cid \(cid)")
|
|
130
112
|
}
|
|
131
113
|
}
|
|
132
114
|
}
|
|
@@ -136,9 +118,7 @@ import Foundation
|
|
|
136
118
|
let count = callsByCid.count
|
|
137
119
|
callsByCid.removeAll()
|
|
138
120
|
callsByUUID.removeAll()
|
|
139
|
-
|
|
140
|
-
NSLog("%@","[UUIDStorage] removeAllObjects: cleared \(count) entries")
|
|
141
|
-
#endif
|
|
121
|
+
CallingxLog.uuid.debugPublic("removeAllObjects: cleared \(count) entries")
|
|
142
122
|
}
|
|
143
123
|
}
|
|
144
124
|
|
|
@@ -20,7 +20,7 @@ typealias RNVoipPushNotificationCompletion = () -> Void
|
|
|
20
20
|
private static var isVoipRegistered = false
|
|
21
21
|
private static var lastVoipToken = ""
|
|
22
22
|
private static var voipRegistry: PKPushRegistry?
|
|
23
|
-
|
|
23
|
+
|
|
24
24
|
private var canSendEvents: Bool = false
|
|
25
25
|
private var delayedEvents: [[String:Any]] = []
|
|
26
26
|
|
|
@@ -55,41 +55,26 @@ typealias RNVoipPushNotificationCompletion = () -> Void
|
|
|
55
55
|
|
|
56
56
|
@objc public static func voipRegistration() {
|
|
57
57
|
if isVoipRegistered {
|
|
58
|
-
|
|
59
|
-
NSLog("%@","[VoipNotificationsManager] voipRegistration is already registered. return _lastVoipToken = \(lastVoipToken)")
|
|
60
|
-
#endif
|
|
58
|
+
CallingxLog.voip.debugPublic("voipRegistration is already registered. return _lastVoipToken = \(lastVoipToken)")
|
|
61
59
|
let voipPushManager = VoipNotificationsManager.shared()
|
|
62
60
|
voipPushManager.sendEventWithNameWrapper(name: VoipNotificationsEvents.registered, body: ["token": lastVoipToken])
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
// Store the registry to prevent deallocation
|
|
75
|
-
voipRegistry.desiredPushTypes = [.voIP]
|
|
76
|
-
VoipNotificationsManager.voipRegistry = voipRegistry
|
|
77
|
-
|
|
78
|
-
isVoipRegistered = true
|
|
79
|
-
} else {
|
|
80
|
-
#if DEBUG
|
|
81
|
-
NSLog("%@","[VoipNotificationsManager] voipRegistration appDelegate not found. return")
|
|
82
|
-
#endif
|
|
83
|
-
}
|
|
84
|
-
}
|
|
61
|
+
return
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
CallingxLog.voip.debugPublic("voipRegistration enter")
|
|
65
|
+
|
|
66
|
+
DispatchQueue.main.async {
|
|
67
|
+
let registry = PKPushRegistry(queue: DispatchQueue.main)
|
|
68
|
+
registry.delegate = VoipPushHandler.sharedInstance()
|
|
69
|
+
registry.desiredPushTypes = [.voIP]
|
|
70
|
+
VoipNotificationsManager.voipRegistry = registry
|
|
71
|
+
isVoipRegistered = true
|
|
85
72
|
}
|
|
86
73
|
}
|
|
87
74
|
|
|
88
75
|
@objc public static func didUpdatePushCredentials(_ credentials: PKPushCredentials, forType type: String) {
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
#endif
|
|
92
|
-
|
|
76
|
+
CallingxLog.voip.debug("didUpdatePushCredentials credentials.token = \(credentials.token, privacy: .private), type = \(type, privacy: .public)")
|
|
77
|
+
|
|
93
78
|
let voipTokenLength = credentials.token.count
|
|
94
79
|
if voipTokenLength == 0 {
|
|
95
80
|
return
|
|
@@ -102,10 +87,8 @@ typealias RNVoipPushNotificationCompletion = () -> Void
|
|
|
102
87
|
}
|
|
103
88
|
|
|
104
89
|
@objc public static func didReceiveIncomingPushWithPayload(_ payload: PKPushPayload, forType type: String) {
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
#endif
|
|
108
|
-
|
|
90
|
+
CallingxLog.voip.debug("didReceiveIncomingPushWithPayload payload.dictionaryPayload = \(payload.dictionaryPayload, privacy: .private), type = \(type, privacy: .public)")
|
|
91
|
+
|
|
109
92
|
let dictionaryPayload: [String: Any] = Dictionary(uniqueKeysWithValues: payload.dictionaryPayload.map { (key, value) in
|
|
110
93
|
(String(describing: key), value)
|
|
111
94
|
})
|
|
@@ -118,9 +101,7 @@ typealias RNVoipPushNotificationCompletion = () -> Void
|
|
|
118
101
|
@objc public func getInitialEvents() -> [[String: Any]] {
|
|
119
102
|
var events: [[String: Any]] = []
|
|
120
103
|
let action = {
|
|
121
|
-
|
|
122
|
-
NSLog("%@","[VoipNotificationsManager][getInitialEvents] delayedEvents = \(self.delayedEvents)")
|
|
123
|
-
#endif
|
|
104
|
+
CallingxLog.voip.debugPublic("[getInitialEvents] delayedEvents = \(self.delayedEvents)")
|
|
124
105
|
|
|
125
106
|
events = self.delayedEvents
|
|
126
107
|
self.delayedEvents = []
|
|
@@ -147,9 +128,7 @@ typealias RNVoipPushNotificationCompletion = () -> Void
|
|
|
147
128
|
}
|
|
148
129
|
|
|
149
130
|
private func sendEventWithNameWrapper(name: String, body: [String: Any]?) {
|
|
150
|
-
|
|
151
|
-
NSLog("%@","[VoipNotificationsManager] sendEventWithNameWrapper: \(name)")
|
|
152
|
-
#endif
|
|
131
|
+
CallingxLog.voip.debugPublic("sendEventWithNameWrapper: \(name)")
|
|
153
132
|
|
|
154
133
|
let sendEventAction = {
|
|
155
134
|
var dictionary: [String: Any] = ["eventName": name]
|
|
@@ -161,9 +140,7 @@ typealias RNVoipPushNotificationCompletion = () -> Void
|
|
|
161
140
|
self.eventEmitter?.emitVoipEvent(dictionary)
|
|
162
141
|
} else {
|
|
163
142
|
self.delayedEvents.append(dictionary)
|
|
164
|
-
|
|
165
|
-
NSLog("%@","[VoipNotificationsManager] delayedEvents: \(self.delayedEvents)")
|
|
166
|
-
#endif
|
|
143
|
+
CallingxLog.voip.debugPublic("delayedEvents: \(self.delayedEvents)")
|
|
167
144
|
}
|
|
168
145
|
}
|
|
169
146
|
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
#import <Foundation/Foundation.h>
|
|
2
|
+
#import <PushKit/PushKit.h>
|
|
3
|
+
|
|
4
|
+
NS_ASSUME_NONNULL_BEGIN
|
|
5
|
+
|
|
6
|
+
@interface VoipPushHandler : NSObject <PKPushRegistryDelegate>
|
|
7
|
+
|
|
8
|
+
+ (instancetype)sharedInstance;
|
|
9
|
+
|
|
10
|
+
/** Handles a legacy `pushRegistry:didReceiveIncomingPushWithPayload:forType:withCompletionHandler:` callback. */
|
|
11
|
+
+ (void)handleIncomingPush:(PKPushPayload *)payload
|
|
12
|
+
forType:(NSString *)type
|
|
13
|
+
completionHandler:(void (^_Nullable)(void))completion;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Handles an iOS 26.4+ `pushRegistry:didReceiveIncomingVoIPPushWithPayload:metadata:withCompletionHandler:`
|
|
17
|
+
* callback. Gated behind `__IPHONE_26_4` because `PKVoIPPushMetadata` only
|
|
18
|
+
* exists in the iOS 26.4 SDK; on older Xcode this declaration is omitted and
|
|
19
|
+
* PushKit dispatches to the legacy `handleIncomingPush:forType:` path instead.
|
|
20
|
+
*/
|
|
21
|
+
#ifdef __IPHONE_26_4
|
|
22
|
+
+ (void)handleIncomingVoIPPush:(PKPushPayload *)payload
|
|
23
|
+
metadata:(PKVoIPPushMetadata * _Nullable)metadata
|
|
24
|
+
completionHandler:(void (^_Nullable)(void))completion API_AVAILABLE(ios(26.4));
|
|
25
|
+
#endif
|
|
26
|
+
|
|
27
|
+
@end
|
|
28
|
+
|
|
29
|
+
NS_ASSUME_NONNULL_END
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
#import "VoipPushHandler.h"
|
|
2
|
+
#import "CallingxPublic.h"
|
|
3
|
+
#import <UIKit/UIKit.h>
|
|
4
|
+
|
|
5
|
+
// Import Swift generated header for VoipNotificationsManager
|
|
6
|
+
#if __has_include("Callingx-Swift.h")
|
|
7
|
+
#import "Callingx-Swift.h"
|
|
8
|
+
#else
|
|
9
|
+
#import <Callingx/Callingx-Swift.h>
|
|
10
|
+
#endif
|
|
11
|
+
|
|
12
|
+
static NSString *const DEFAULT_DISPLAY_NAME = @"Unknown Caller";
|
|
13
|
+
|
|
14
|
+
@interface Callingx (VoipPushHandlerInternal)
|
|
15
|
+
+ (BOOL)shouldSkipIncomingPushInForeground;
|
|
16
|
+
@end
|
|
17
|
+
|
|
18
|
+
#pragma mark - Helpers
|
|
19
|
+
|
|
20
|
+
// applicationState must be read on the main thread (PushKit delivers on
|
|
21
|
+
// main, so the common path skips dispatch). Treat Inactive as foreground:
|
|
22
|
+
// covers brief transitions and system overlays.
|
|
23
|
+
static BOOL isAppInForeground(void) {
|
|
24
|
+
__block UIApplicationState state = UIApplicationStateActive;
|
|
25
|
+
void (^readState)(void) = ^{
|
|
26
|
+
state = [UIApplication sharedApplication].applicationState;
|
|
27
|
+
};
|
|
28
|
+
if ([NSThread isMainThread]) {
|
|
29
|
+
readState();
|
|
30
|
+
} else {
|
|
31
|
+
dispatch_sync(dispatch_get_main_queue(), readState);
|
|
32
|
+
}
|
|
33
|
+
return state != UIApplicationStateBackground;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Extracts CallKit-display fields from the Stream payload and reports the
|
|
37
|
+
// incoming call via Callingx.
|
|
38
|
+
static void reportIncomingCallFromStreamPayload(NSDictionary *streamPayload,
|
|
39
|
+
void (^_Nullable completion)(void)) {
|
|
40
|
+
NSString *callCid = streamPayload[@"call_cid"];
|
|
41
|
+
NSString *callDisplayName = streamPayload[@"call_display_name"];
|
|
42
|
+
NSString *createdByDisplayName = streamPayload[@"created_by_display_name"];
|
|
43
|
+
NSString *createdCallerName = callDisplayName.length > 0 ? callDisplayName : createdByDisplayName;
|
|
44
|
+
NSString *localizedCallerName = createdCallerName.length > 0 ? createdCallerName : DEFAULT_DISPLAY_NAME;
|
|
45
|
+
NSString *createdById = streamPayload[@"created_by_id"];
|
|
46
|
+
NSString *handle = createdById.length > 0 ? createdById : localizedCallerName;
|
|
47
|
+
NSString *videoIncluded = streamPayload[@"video"];
|
|
48
|
+
BOOL hasVideo = [videoIncluded isEqualToString:@"false"] ? NO : YES;
|
|
49
|
+
|
|
50
|
+
[Callingx reportNewIncomingCall:callCid
|
|
51
|
+
handle:handle
|
|
52
|
+
handleType:@"generic"
|
|
53
|
+
hasVideo:hasVideo
|
|
54
|
+
localizedCallerName:localizedCallerName
|
|
55
|
+
supportsHolding:NO
|
|
56
|
+
supportsDTMF:NO
|
|
57
|
+
supportsGrouping:NO
|
|
58
|
+
supportsUngrouping:NO
|
|
59
|
+
payload:streamPayload
|
|
60
|
+
withCompletionHandler:completion];
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
#pragma mark - Implementation
|
|
64
|
+
|
|
65
|
+
@implementation VoipPushHandler
|
|
66
|
+
|
|
67
|
+
+ (instancetype)sharedInstance {
|
|
68
|
+
static VoipPushHandler *instance;
|
|
69
|
+
static dispatch_once_t onceToken;
|
|
70
|
+
dispatch_once(&onceToken, ^{
|
|
71
|
+
instance = [[self alloc] init];
|
|
72
|
+
});
|
|
73
|
+
return instance;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
#pragma mark - Static orchestration
|
|
77
|
+
|
|
78
|
+
+ (void)handleIncomingPush:(PKPushPayload *)payload
|
|
79
|
+
forType:(NSString *)type
|
|
80
|
+
completionHandler:(void (^_Nullable)(void))completion {
|
|
81
|
+
NSDictionary *streamPayload = payload.dictionaryPayload[@"stream"];
|
|
82
|
+
if (!streamPayload) {
|
|
83
|
+
[CallingxLogBridge pushError:@"[handleIncomingPush] Stream payload not found"];
|
|
84
|
+
if (completion) {
|
|
85
|
+
completion();
|
|
86
|
+
}
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
NSString *callCid = streamPayload[@"call_cid"];
|
|
91
|
+
if (!callCid) {
|
|
92
|
+
[CallingxLogBridge pushError:@"[handleIncomingPush] Missing required field: call_cid"];
|
|
93
|
+
if (completion) {
|
|
94
|
+
completion();
|
|
95
|
+
}
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (![Callingx canRegisterCall]) {
|
|
100
|
+
if (completion) {
|
|
101
|
+
completion();
|
|
102
|
+
}
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
reportIncomingCallFromStreamPayload(streamPayload, completion);
|
|
107
|
+
[VoipNotificationsManager didReceiveIncomingPushWithPayload:payload forType:type];
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
#pragma mark - PKPushRegistryDelegate (managed mode)
|
|
111
|
+
|
|
112
|
+
- (void)pushRegistry:(PKPushRegistry *)registry
|
|
113
|
+
didUpdatePushCredentials:(PKPushCredentials *)credentials
|
|
114
|
+
forType:(PKPushType)type {
|
|
115
|
+
[VoipNotificationsManager didUpdatePushCredentials:credentials forType:(NSString *)type];
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
- (void)pushRegistry:(PKPushRegistry *)registry
|
|
119
|
+
didReceiveIncomingPushWithPayload:(PKPushPayload *)payload
|
|
120
|
+
forType:(PKPushType)type
|
|
121
|
+
withCompletionHandler:(void (^)(void))completion {
|
|
122
|
+
[VoipPushHandler handleIncomingPush:payload
|
|
123
|
+
forType:(NSString *)type
|
|
124
|
+
completionHandler:completion];
|
|
125
|
+
[CallingxLogBridge pushDebug:@"[pushRegistry:didReceiveIncomingPushWithPayload:forType:withCompletionHandler:] completion"];
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
#ifdef __IPHONE_26_4
|
|
129
|
+
+ (void)handleIncomingVoIPPush:(PKPushPayload *)payload
|
|
130
|
+
metadata:(PKVoIPPushMetadata * _Nullable)metadata
|
|
131
|
+
completionHandler:(void (^_Nullable)(void))completion {
|
|
132
|
+
NSDictionary *streamPayload = payload.dictionaryPayload[@"stream"];
|
|
133
|
+
if (!streamPayload) {
|
|
134
|
+
[CallingxLogBridge pushError:@"[handleIncomingVoIPPush] Stream payload not found"];
|
|
135
|
+
if (completion) {
|
|
136
|
+
completion();
|
|
137
|
+
}
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
NSString *callCid = streamPayload[@"call_cid"];
|
|
142
|
+
if (!callCid) {
|
|
143
|
+
[CallingxLogBridge pushError:@"[handleIncomingVoIPPush] Missing required field: call_cid"];
|
|
144
|
+
if (completion) {
|
|
145
|
+
completion();
|
|
146
|
+
}
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
NSString *type = @"PKPushTypeVoIP";
|
|
151
|
+
BOOL mustReport = metadata ? metadata.mustReport : YES;
|
|
152
|
+
|
|
153
|
+
// Both skip paths require mustReport == NO; skipping while YES risks
|
|
154
|
+
// PushKit terminating the app.
|
|
155
|
+
if (!mustReport && ![Callingx canRegisterCall]) {
|
|
156
|
+
// Busy reject: drop without forwarding to JS.
|
|
157
|
+
if (completion) {
|
|
158
|
+
completion();
|
|
159
|
+
}
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (!mustReport &&
|
|
164
|
+
[Callingx shouldSkipIncomingPushInForeground] &&
|
|
165
|
+
isAppInForeground()) {
|
|
166
|
+
// Foreground skip: hide CallKit, let JS render the ringing UI.
|
|
167
|
+
[VoipNotificationsManager didReceiveIncomingPushWithPayload:payload forType:type];
|
|
168
|
+
if (completion) {
|
|
169
|
+
completion();
|
|
170
|
+
}
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
reportIncomingCallFromStreamPayload(streamPayload, completion);
|
|
175
|
+
[VoipNotificationsManager didReceiveIncomingPushWithPayload:payload forType:type];
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// iOS 26.4 added a new VoIP push selector that carries a `PKVoIPPushMetadata`
|
|
179
|
+
// argument (notably `mustReport`). The type only exists in the iOS 26.4 SDK,
|
|
180
|
+
// so the `#ifdef __IPHONE_26_4` gate ensures this file still compiles on
|
|
181
|
+
// older Xcode versions — older Xcode simply doesn't emit this method, and
|
|
182
|
+
// PushKit on those builds dispatches to the legacy selector above.
|
|
183
|
+
- (void)pushRegistry:(PKPushRegistry *)registry
|
|
184
|
+
didReceiveIncomingVoIPPushWithPayload:(PKPushPayload *)payload
|
|
185
|
+
metadata:(PKVoIPPushMetadata *)metadata
|
|
186
|
+
withCompletionHandler:(void (^)(void))completion
|
|
187
|
+
API_AVAILABLE(ios(26.4)) {
|
|
188
|
+
[VoipPushHandler handleIncomingVoIPPush:payload
|
|
189
|
+
metadata:metadata
|
|
190
|
+
completionHandler:completion];
|
|
191
|
+
[CallingxLogBridge pushDebug:@"[pushRegistry:didReceiveIncomingVoIPPushWithPayload:metadata:withCompletionHandler:] completion"];
|
|
192
|
+
}
|
|
193
|
+
#endif
|
|
194
|
+
|
|
195
|
+
@end
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stream-io/react-native-callingx",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "CallKit and Telecom API capabilities for React Native",
|
|
5
5
|
"main": "./dist/module/index.js",
|
|
6
6
|
"module": "./dist/module/index.js",
|
|
@@ -59,20 +59,20 @@
|
|
|
59
59
|
"registry": "https://registry.npmjs.org/"
|
|
60
60
|
},
|
|
61
61
|
"devDependencies": {
|
|
62
|
-
"@react-native-community/cli": "20.
|
|
63
|
-
"@react-native/babel-preset": "0.
|
|
64
|
-
"@stream-io/react-native-webrtc": "
|
|
65
|
-
"@types/react": "^19.
|
|
62
|
+
"@react-native-community/cli": "20.1.3",
|
|
63
|
+
"@react-native/babel-preset": "0.85.3",
|
|
64
|
+
"@stream-io/react-native-webrtc": "145.0.0",
|
|
65
|
+
"@types/react": "^19.2.15",
|
|
66
66
|
"del-cli": "^6.0.0",
|
|
67
|
-
"react": "19.
|
|
68
|
-
"react-native": "0.
|
|
69
|
-
"react-native-builder-bob": "^0.
|
|
70
|
-
"typescript": "^5.9.
|
|
67
|
+
"react": "19.2.3",
|
|
68
|
+
"react-native": "0.85.3",
|
|
69
|
+
"react-native-builder-bob": "^0.41.0",
|
|
70
|
+
"typescript": "^5.9.3"
|
|
71
71
|
},
|
|
72
72
|
"peerDependencies": {
|
|
73
73
|
"@react-native-firebase/app": ">=23.0.0",
|
|
74
74
|
"@react-native-firebase/messaging": ">=23.0.0",
|
|
75
|
-
"@stream-io/react-native-webrtc": "
|
|
75
|
+
"@stream-io/react-native-webrtc": "^145.0.0",
|
|
76
76
|
"react": "*",
|
|
77
77
|
"react-native": "*"
|
|
78
78
|
},
|
|
@@ -48,11 +48,14 @@ export interface Spec extends TurboModule {
|
|
|
48
48
|
getInitialEvents(): Array<{
|
|
49
49
|
eventName: string;
|
|
50
50
|
params: {
|
|
51
|
-
callId
|
|
51
|
+
callId?: string;
|
|
52
52
|
cause?: string;
|
|
53
53
|
muted?: boolean;
|
|
54
54
|
hold?: boolean;
|
|
55
55
|
source?: string;
|
|
56
|
+
phase?: string;
|
|
57
|
+
reason?: string;
|
|
58
|
+
shouldResume?: boolean;
|
|
56
59
|
};
|
|
57
60
|
}>;
|
|
58
61
|
|
|
@@ -147,10 +150,14 @@ export interface Spec extends TurboModule {
|
|
|
147
150
|
readonly onNewEvent: EventEmitter<{
|
|
148
151
|
eventName: string;
|
|
149
152
|
params: {
|
|
150
|
-
callId
|
|
153
|
+
callId?: string;
|
|
151
154
|
cause?: string;
|
|
152
155
|
muted?: boolean;
|
|
153
156
|
hold?: boolean;
|
|
157
|
+
source?: string;
|
|
158
|
+
phase?: string;
|
|
159
|
+
reason?: string;
|
|
160
|
+
shouldResume?: boolean;
|
|
154
161
|
};
|
|
155
162
|
}>;
|
|
156
163
|
|
package/src/types.ts
CHANGED
|
@@ -292,14 +292,12 @@ export type InfoDisplayOptions = {
|
|
|
292
292
|
};
|
|
293
293
|
|
|
294
294
|
export type EventData = {
|
|
295
|
-
eventName:
|
|
296
|
-
|
|
297
|
-
};
|
|
295
|
+
[K in EventName]: { eventName: K; params: EventParams[K] };
|
|
296
|
+
}[EventName];
|
|
298
297
|
|
|
299
298
|
export type VoipEventData = {
|
|
300
|
-
eventName:
|
|
301
|
-
|
|
302
|
-
};
|
|
299
|
+
[K in VoipEventName]: { eventName: K; params: VoipEventParams[K] };
|
|
300
|
+
}[VoipEventName];
|
|
303
301
|
|
|
304
302
|
export type EventName =
|
|
305
303
|
| 'answerCall'
|
|
@@ -307,11 +305,19 @@ export type EventName =
|
|
|
307
305
|
| 'didDisplayIncomingCall'
|
|
308
306
|
| 'didToggleHoldCallAction'
|
|
309
307
|
| 'didChangeAudioRoute'
|
|
308
|
+
| 'didAudioInterruption'
|
|
310
309
|
| 'didReceiveStartCallAction'
|
|
311
310
|
| 'didPerformSetMutedCallAction'
|
|
312
311
|
| 'didActivateAudioSession'
|
|
313
312
|
| 'didDeactivateAudioSession';
|
|
314
313
|
|
|
314
|
+
export type IOSAudioInterruptionEvent = {
|
|
315
|
+
source: 'callingx';
|
|
316
|
+
phase: 'began' | 'ended';
|
|
317
|
+
reason?: 'default' | 'builtInMicMuted' | 'routeDisconnected' | (string & {});
|
|
318
|
+
shouldResume?: boolean;
|
|
319
|
+
};
|
|
320
|
+
|
|
315
321
|
export type EventParams = {
|
|
316
322
|
answerCall: {
|
|
317
323
|
callId: string;
|
|
@@ -337,6 +343,7 @@ export type EventParams = {
|
|
|
337
343
|
callId: string;
|
|
338
344
|
output: string;
|
|
339
345
|
};
|
|
346
|
+
didAudioInterruption: IOSAudioInterruptionEvent;
|
|
340
347
|
didReceiveStartCallAction: {
|
|
341
348
|
callId: string;
|
|
342
349
|
};
|