@poolse/react-native-calls 0.4.0 → 0.4.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.
|
@@ -17,18 +17,25 @@ public class PoolseCallsModule: Module {
|
|
|
17
17
|
PoolseCallsSetup.registerVoipEarly()
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
+
// The listeners are process-level statics read on main: assign and
|
|
21
|
+
// clear them on main too (these closures run on the module's queue).
|
|
20
22
|
OnStartObserving {
|
|
21
|
-
|
|
22
|
-
self
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
23
|
+
DispatchQueue.main.async { [weak self] in
|
|
24
|
+
guard self != nil else { return }
|
|
25
|
+
CallCore.listener = { [weak self] snapshot in
|
|
26
|
+
self?.sendEvent("onCallState", ["snapshot": snapshot ?? NSNull()])
|
|
27
|
+
}
|
|
28
|
+
VoipPush.tokenListener = { [weak self] token in
|
|
29
|
+
self?.sendEvent("onVoipToken", ["token": token, "environment": PoolseCallsModule.environment()])
|
|
30
|
+
}
|
|
26
31
|
}
|
|
27
32
|
}
|
|
28
33
|
|
|
29
34
|
OnStopObserving {
|
|
30
|
-
|
|
31
|
-
|
|
35
|
+
DispatchQueue.main.async {
|
|
36
|
+
CallCore.listener = nil
|
|
37
|
+
VoipPush.tokenListener = nil
|
|
38
|
+
}
|
|
32
39
|
}
|
|
33
40
|
|
|
34
41
|
AsyncFunction("configure") { (apiBaseUrl: String, options: [String: Any]?) in
|
|
@@ -45,6 +45,7 @@ final class CallCore {
|
|
|
45
45
|
defaults.set(trimmed, forKey: "apiBaseUrl")
|
|
46
46
|
defaults.set(ringback, forKey: "ringback")
|
|
47
47
|
defaults.set(apnsEnvironment, forKey: "apnsEnvironment")
|
|
48
|
+
writeConfigFile(apiBaseUrl: trimmed, ringback: ringback, apnsEnvironment: apnsEnvironment)
|
|
48
49
|
configLock.lock()
|
|
49
50
|
_config = CallCoreConfig(apiBaseUrl: trimmed, ringback: ringback, apnsEnvironment: apnsEnvironment)
|
|
50
51
|
configLock.unlock()
|
|
@@ -52,18 +53,65 @@ final class CallCore {
|
|
|
52
53
|
|
|
53
54
|
/// The APNs environment last configured (persisted), if any.
|
|
54
55
|
static var apnsEnvironment: String? {
|
|
55
|
-
config?.apnsEnvironment ?? defaults.string(forKey: "apnsEnvironment")
|
|
56
|
+
config?.apnsEnvironment ?? defaults.string(forKey: "apnsEnvironment") ?? readConfigFile()?.apnsEnvironment
|
|
56
57
|
}
|
|
57
58
|
|
|
58
59
|
private static func restoreConfig() {
|
|
59
|
-
guard config == nil
|
|
60
|
+
guard config == nil else { return }
|
|
61
|
+
let restored: CallCoreConfig
|
|
62
|
+
if let url = defaults.string(forKey: "apiBaseUrl") {
|
|
63
|
+
restored = CallCoreConfig(apiBaseUrl: url,
|
|
64
|
+
ringback: defaults.string(forKey: "ringback"),
|
|
65
|
+
apnsEnvironment: defaults.string(forKey: "apnsEnvironment"))
|
|
66
|
+
} else if let fromFile = readConfigFile() {
|
|
67
|
+
// Locked since reboot: UserDefaults is unreadable before first
|
|
68
|
+
// unlock, the unprotected file is not.
|
|
69
|
+
restored = fromFile
|
|
70
|
+
} else {
|
|
71
|
+
return
|
|
72
|
+
}
|
|
60
73
|
configLock.lock()
|
|
61
|
-
_config =
|
|
62
|
-
ringback: defaults.string(forKey: "ringback"),
|
|
63
|
-
apnsEnvironment: defaults.string(forKey: "apnsEnvironment"))
|
|
74
|
+
_config = restored
|
|
64
75
|
configLock.unlock()
|
|
65
76
|
}
|
|
66
77
|
|
|
78
|
+
// MARK: config file (readable before first unlock)
|
|
79
|
+
|
|
80
|
+
/// Application Support/dev.poolse.callcore/config.json, written with
|
|
81
|
+
/// FileProtectionType.none so a PushKit wake while the device has been
|
|
82
|
+
/// locked since reboot can still rebuild the core. Holds no secrets —
|
|
83
|
+
/// only the API origin, a sound name and the APNs environment.
|
|
84
|
+
private static var configFileURL: URL? {
|
|
85
|
+
guard let base = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask).first else { return nil }
|
|
86
|
+
return base.appendingPathComponent("dev.poolse.callcore", isDirectory: true).appendingPathComponent("config.json")
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
private static func writeConfigFile(apiBaseUrl: String, ringback: String?, apnsEnvironment: String?) {
|
|
90
|
+
guard let url = configFileURL else { return }
|
|
91
|
+
var obj: [String: Any] = ["apiBaseUrl": apiBaseUrl]
|
|
92
|
+
if let ringback = ringback { obj["ringback"] = ringback }
|
|
93
|
+
if let apnsEnvironment = apnsEnvironment { obj["apnsEnvironment"] = apnsEnvironment }
|
|
94
|
+
do {
|
|
95
|
+
let fm = FileManager.default
|
|
96
|
+
let dir = url.deletingLastPathComponent()
|
|
97
|
+
try fm.createDirectory(at: dir, withIntermediateDirectories: true,
|
|
98
|
+
attributes: [.protectionKey: FileProtectionType.none])
|
|
99
|
+
let data = try JSONSerialization.data(withJSONObject: obj)
|
|
100
|
+
try data.write(to: url, options: [.atomic, .noFileProtection])
|
|
101
|
+
try? fm.setAttributes([.protectionKey: FileProtectionType.none], ofItemAtPath: url.path)
|
|
102
|
+
} catch {
|
|
103
|
+
NSLog("[PoolseCalls] config file write failed: %@", "\(error)")
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
private static func readConfigFile() -> CallCoreConfig? {
|
|
108
|
+
guard let url = configFileURL, let data = try? Data(contentsOf: url),
|
|
109
|
+
let obj = (try? JSONSerialization.jsonObject(with: data)) as? [String: Any],
|
|
110
|
+
let api = obj["apiBaseUrl"] as? String, !api.isEmpty else { return nil }
|
|
111
|
+
return CallCoreConfig(apiBaseUrl: api, ringback: obj["ringback"] as? String,
|
|
112
|
+
apnsEnvironment: obj["apnsEnvironment"] as? String)
|
|
113
|
+
}
|
|
114
|
+
|
|
67
115
|
/// The core, or nil if this install was never configured. Main thread.
|
|
68
116
|
static func getOrNull() -> CallCore? {
|
|
69
117
|
if let i = instance { return i }
|
|
@@ -89,7 +89,7 @@ final class CallKitController: NSObject, CXProviderDelegate {
|
|
|
89
89
|
func reportIncoming(ring: RingContext) {
|
|
90
90
|
let hasVideo = ring.media == "video"
|
|
91
91
|
let title = ring.conversationName ?? ring.callerName
|
|
92
|
-
if let existing = calls.values.first(where: { $0.callId == ring.callId }) {
|
|
92
|
+
if let existing = calls.values.first(where: { $0.callId == ring.callId && !$0.ended }) {
|
|
93
93
|
provider.reportNewIncomingCall(with: existing.uuid, update: update(title: title, handle: ring.conversationId, video: hasVideo)) { _ in }
|
|
94
94
|
return
|
|
95
95
|
}
|
|
@@ -99,8 +99,13 @@ final class CallKitController: NSObject, CXProviderDelegate {
|
|
|
99
99
|
guard let error = error else { return }
|
|
100
100
|
NSLog("[PoolseCalls] CallKit refused incoming call %@: %@", ring.callId, error.localizedDescription)
|
|
101
101
|
// Refused (DND, blocked, busy group): CallKit holds nothing for it.
|
|
102
|
-
|
|
103
|
-
|
|
102
|
+
// End the ring in the core too — silently (no decline is sent), so
|
|
103
|
+
// the engine is not left RINGING_IN with no UI, counting as busy.
|
|
104
|
+
DispatchQueue.main.async {
|
|
105
|
+
t.ended = true
|
|
106
|
+
self?.calls.removeValue(forKey: t.uuid)
|
|
107
|
+
CallCore.peek()?.onRingEnded(callId: ring.callId, reason: "cancelled")
|
|
108
|
+
}
|
|
104
109
|
}
|
|
105
110
|
}
|
|
106
111
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@poolse/react-native-calls",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.1",
|
|
4
4
|
"description": "Native call core for poolse — Kotlin/Swift calling engine with a thin React Native bridge. Ring, answer and carry audio with the app killed and the phone locked.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|