expo-callkit-telecom 0.1.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/LICENSE +21 -0
- package/README.md +197 -0
- package/android/build.gradle +32 -0
- package/android/src/main/AndroidManifest.xml +33 -0
- package/android/src/main/java/expo/modules/callkittelecom/ExpoCallKitTelecomModule.kt +384 -0
- package/android/src/main/java/expo/modules/callkittelecom/IncomingCallActivity.kt +275 -0
- package/android/src/main/java/expo/modules/callkittelecom/events/CallEventEmitter.kt +151 -0
- package/android/src/main/java/expo/modules/callkittelecom/events/CallEvents.kt +59 -0
- package/android/src/main/java/expo/modules/callkittelecom/managers/CallAudioManager.kt +361 -0
- package/android/src/main/java/expo/modules/callkittelecom/managers/CallManager.kt +891 -0
- package/android/src/main/java/expo/modules/callkittelecom/managers/CallNotificationManager.kt +445 -0
- package/android/src/main/java/expo/modules/callkittelecom/managers/CaptureSessionManager.kt +27 -0
- package/android/src/main/java/expo/modules/callkittelecom/managers/DialtonePlayer.kt +171 -0
- package/android/src/main/java/expo/modules/callkittelecom/managers/FulfillRequestManager.kt +150 -0
- package/android/src/main/java/expo/modules/callkittelecom/managers/VoIPPushManager.kt +54 -0
- package/android/src/main/java/expo/modules/callkittelecom/models/CallModels.kt +269 -0
- package/android/src/main/java/expo/modules/callkittelecom/services/CallNotificationReceiver.kt +54 -0
- package/android/src/main/java/expo/modules/callkittelecom/services/ExpoCallKitTelecomMessagingService.kt +161 -0
- package/android/src/main/java/expo/modules/callkittelecom/store/CallStore.kt +181 -0
- package/android/src/main/java/expo/modules/callkittelecom/utils/CallKitTelecomLog.kt +52 -0
- package/android/src/main/java/expo/modules/callkittelecom/utils/PermissionUtils.kt +28 -0
- package/android/src/main/res/drawable/expo_callkit_telecom_bg_answer.xml +9 -0
- package/android/src/main/res/drawable/expo_callkit_telecom_bg_avatar.xml +5 -0
- package/android/src/main/res/drawable/expo_callkit_telecom_bg_decline.xml +9 -0
- package/android/src/main/res/drawable/expo_callkit_telecom_ic_answer.xml +9 -0
- package/android/src/main/res/drawable/expo_callkit_telecom_ic_decline.xml +9 -0
- package/android/src/main/res/drawable/expo_callkit_telecom_ic_videocam.xml +9 -0
- package/android/src/main/res/layout/activity_incoming_call.xml +169 -0
- package/app.json +8 -0
- package/app.plugin.js +1 -0
- package/build/Calls.d.ts +577 -0
- package/build/Calls.d.ts.map +1 -0
- package/build/Calls.js +715 -0
- package/build/Calls.js.map +1 -0
- package/build/Calls.types.d.ts +203 -0
- package/build/Calls.types.d.ts.map +1 -0
- package/build/Calls.types.js +2 -0
- package/build/Calls.types.js.map +1 -0
- package/build/ExpoCallKitTelecomModule.d.ts +3 -0
- package/build/ExpoCallKitTelecomModule.d.ts.map +1 -0
- package/build/ExpoCallKitTelecomModule.js +4 -0
- package/build/ExpoCallKitTelecomModule.js.map +1 -0
- package/build/hooks/index.d.ts +2 -0
- package/build/hooks/index.d.ts.map +1 -0
- package/build/hooks/index.js +2 -0
- package/build/hooks/index.js.map +1 -0
- package/build/hooks/useVoIPPushToken.d.ts +14 -0
- package/build/hooks/useVoIPPushToken.d.ts.map +1 -0
- package/build/hooks/useVoIPPushToken.js +26 -0
- package/build/hooks/useVoIPPushToken.js.map +1 -0
- package/build/index.d.ts +4 -0
- package/build/index.d.ts.map +1 -0
- package/build/index.js +4 -0
- package/build/index.js.map +1 -0
- package/expo-module.config.json +10 -0
- package/ios/AppDelegateSubscriber.swift +93 -0
- package/ios/ExpoCallKitTelecom.podspec +31 -0
- package/ios/ExpoCallKitTelecomLogger.swift +55 -0
- package/ios/ExpoCallKitTelecomModule.swift +503 -0
- package/ios/Managers/AudioManager.swift +363 -0
- package/ios/Managers/CallEventEmitter.swift +199 -0
- package/ios/Managers/CallManager+CXProviderDelegate.swift +195 -0
- package/ios/Managers/CallManager.swift +714 -0
- package/ios/Managers/CaptureSessionManager.swift +54 -0
- package/ios/Managers/DialtonePlayer.swift +126 -0
- package/ios/Managers/FulfillRequestManager.swift +154 -0
- package/ios/Managers/VoIPPushManager+PKPushRegistryDelegate.swift +123 -0
- package/ios/Managers/VoIPPushManager.swift +58 -0
- package/ios/Models/CallEvents.swift +263 -0
- package/ios/Models/CallOptions.swift +15 -0
- package/ios/Models/CallParticipant.swift +37 -0
- package/ios/Models/CallSession.swift +80 -0
- package/ios/Models/IncomingCallEvent.swift +196 -0
- package/ios/Stores/CallStore.swift +149 -0
- package/package.json +56 -0
- package/plugin/build/constants.d.ts +3 -0
- package/plugin/build/constants.js +7 -0
- package/plugin/build/withExpoCallKitTelecom.d.ts +67 -0
- package/plugin/build/withExpoCallKitTelecom.js +16 -0
- package/plugin/build/withExpoCallKitTelecomAndroid.d.ts +3 -0
- package/plugin/build/withExpoCallKitTelecomAndroid.js +177 -0
- package/plugin/build/withExpoCallKitTelecomIos.d.ts +3 -0
- package/plugin/build/withExpoCallKitTelecomIos.js +195 -0
- package/plugin/src/constants.ts +4 -0
- package/plugin/src/withExpoCallKitTelecom.ts +83 -0
- package/plugin/src/withExpoCallKitTelecomAndroid.ts +293 -0
- package/plugin/src/withExpoCallKitTelecomIos.ts +276 -0
- package/src/Calls.ts +848 -0
- package/src/Calls.types.ts +275 -0
- package/src/ExpoCallKitTelecomModule.ts +4 -0
- package/src/hooks/index.ts +1 -0
- package/src/hooks/useVoIPPushToken.ts +34 -0
- package/src/index.ts +3 -0
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
import AVFoundation
|
|
2
|
+
import CallKit
|
|
3
|
+
import Foundation
|
|
4
|
+
|
|
5
|
+
/// Protocol for events that can be sent to JS.
|
|
6
|
+
protocol CallEvent {
|
|
7
|
+
static var name: String { get }
|
|
8
|
+
var body: [String: Any] { get }
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// MARK: - Constants
|
|
12
|
+
|
|
13
|
+
private enum CallEventKeys {
|
|
14
|
+
static let id = "id"
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// MARK: - Call Session Events
|
|
18
|
+
|
|
19
|
+
struct CallSessionAddedEvent: CallEvent {
|
|
20
|
+
static let name = "onCallSessionAdded"
|
|
21
|
+
|
|
22
|
+
let session: CallSession
|
|
23
|
+
|
|
24
|
+
var body: [String: Any] {
|
|
25
|
+
["session": session.toDictionary()]
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
struct CallSessionUpdatedEvent: CallEvent {
|
|
30
|
+
static let name = "onCallSessionUpdated"
|
|
31
|
+
|
|
32
|
+
let session: CallSession
|
|
33
|
+
|
|
34
|
+
var body: [String: Any] {
|
|
35
|
+
["session": session.toDictionary()]
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
struct CallSessionRemovedEvent: CallEvent {
|
|
40
|
+
static let name = "onCallSessionRemoved"
|
|
41
|
+
|
|
42
|
+
let id: UUID
|
|
43
|
+
|
|
44
|
+
var body: [String: Any] {
|
|
45
|
+
[CallEventKeys.id: id.uuidString]
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// MARK: - Audio Session Events
|
|
50
|
+
|
|
51
|
+
struct AudioSessionCallInfo {
|
|
52
|
+
let id: UUID
|
|
53
|
+
let status: CallSession.Status
|
|
54
|
+
|
|
55
|
+
init(from session: CallSession) {
|
|
56
|
+
self.id = session.id
|
|
57
|
+
self.status = session.status
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
func toDictionary() -> [String: Any] {
|
|
61
|
+
[
|
|
62
|
+
"id": id.uuidString,
|
|
63
|
+
"status": status.rawValue,
|
|
64
|
+
]
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
struct AudioSessionActivatedEvent: CallEvent {
|
|
69
|
+
static let name = "onAudioSessionActivated"
|
|
70
|
+
|
|
71
|
+
let calls: [AudioSessionCallInfo]
|
|
72
|
+
|
|
73
|
+
var body: [String: Any] {
|
|
74
|
+
["calls": calls.map { $0.toDictionary() }]
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
struct AudioSessionDeactivatedEvent: CallEvent {
|
|
79
|
+
static let name = "onAudioSessionDeactivated"
|
|
80
|
+
|
|
81
|
+
let calls: [AudioSessionCallInfo]
|
|
82
|
+
|
|
83
|
+
var body: [String: Any] {
|
|
84
|
+
["calls": calls.map { $0.toDictionary() }]
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
struct AudioRouteChangedEvent: CallEvent {
|
|
89
|
+
static let name = "onAudioRouteChanged"
|
|
90
|
+
|
|
91
|
+
let inputs: [[String: String]]
|
|
92
|
+
let outputs: [[String: String]]
|
|
93
|
+
|
|
94
|
+
var body: [String: Any] {
|
|
95
|
+
[
|
|
96
|
+
"currentRoute": [
|
|
97
|
+
"inputs": inputs,
|
|
98
|
+
"outputs": outputs,
|
|
99
|
+
]
|
|
100
|
+
]
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// MARK: - Call Intent Events
|
|
105
|
+
|
|
106
|
+
struct CallIntentReceivedEvent: CallEvent {
|
|
107
|
+
static let name = "onCallIntentReceived"
|
|
108
|
+
|
|
109
|
+
let handle: String
|
|
110
|
+
let handleType: String
|
|
111
|
+
let hasVideo: Bool
|
|
112
|
+
|
|
113
|
+
var body: [String: Any] {
|
|
114
|
+
[
|
|
115
|
+
"handle": handle,
|
|
116
|
+
"handleType": handleType,
|
|
117
|
+
"hasVideo": hasVideo,
|
|
118
|
+
]
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// MARK: - Call Action Events
|
|
123
|
+
|
|
124
|
+
struct IncomingCallReportedEvent: CallEvent {
|
|
125
|
+
static let name = "onIncomingCallReported"
|
|
126
|
+
|
|
127
|
+
let id: UUID
|
|
128
|
+
|
|
129
|
+
var body: [String: Any] {
|
|
130
|
+
[CallEventKeys.id: id.uuidString]
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
struct OutgoingCallStartedEvent: CallEvent {
|
|
135
|
+
static let name = "onOutgoingCallStarted"
|
|
136
|
+
|
|
137
|
+
let id: UUID
|
|
138
|
+
|
|
139
|
+
var body: [String: Any] {
|
|
140
|
+
[CallEventKeys.id: id.uuidString]
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
struct CallAnsweredEvent: CallEvent {
|
|
145
|
+
static let name = "onCallAnswered"
|
|
146
|
+
|
|
147
|
+
let id: UUID
|
|
148
|
+
let requestId: UUID
|
|
149
|
+
|
|
150
|
+
var body: [String: Any] {
|
|
151
|
+
[
|
|
152
|
+
CallEventKeys.id: id.uuidString,
|
|
153
|
+
"requestId": requestId.uuidString,
|
|
154
|
+
]
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
struct CallEndedEvent: CallEvent {
|
|
159
|
+
static let name = "onCallEnded"
|
|
160
|
+
|
|
161
|
+
let id: UUID
|
|
162
|
+
|
|
163
|
+
var body: [String: Any] {
|
|
164
|
+
[CallEventKeys.id: id.uuidString]
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
struct CallReportedEnded: CallEvent {
|
|
169
|
+
static let name = "onCallReportedEnded"
|
|
170
|
+
|
|
171
|
+
let id: UUID
|
|
172
|
+
let reason: CXCallEndedReason
|
|
173
|
+
|
|
174
|
+
var body: [String: Any] {
|
|
175
|
+
[
|
|
176
|
+
CallEventKeys.id: id.uuidString,
|
|
177
|
+
"reason": Self.reasonString(for: reason),
|
|
178
|
+
]
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
private static func reasonString(for reason: CXCallEndedReason) -> String {
|
|
182
|
+
switch reason {
|
|
183
|
+
case .failed: return "failed"
|
|
184
|
+
case .remoteEnded: return "remoteEnded"
|
|
185
|
+
case .unanswered: return "unanswered"
|
|
186
|
+
case .answeredElsewhere: return "answeredElsewhere"
|
|
187
|
+
case .declinedElsewhere: return "declinedElsewhere"
|
|
188
|
+
@unknown default: return "unknown"
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
struct SetMutedActionEvent: CallEvent {
|
|
194
|
+
static let name = "onSetMutedAction"
|
|
195
|
+
|
|
196
|
+
let id: UUID
|
|
197
|
+
let isMuted: Bool
|
|
198
|
+
|
|
199
|
+
var body: [String: Any] {
|
|
200
|
+
[
|
|
201
|
+
CallEventKeys.id: id.uuidString,
|
|
202
|
+
"isMuted": isMuted,
|
|
203
|
+
]
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
struct SetHeldActionEvent: CallEvent {
|
|
208
|
+
static let name = "onSetHeldAction"
|
|
209
|
+
|
|
210
|
+
let id: UUID
|
|
211
|
+
let isOnHold: Bool
|
|
212
|
+
|
|
213
|
+
var body: [String: Any] {
|
|
214
|
+
[
|
|
215
|
+
CallEventKeys.id: id.uuidString,
|
|
216
|
+
"isOnHold": isOnHold,
|
|
217
|
+
]
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
struct DTMFEvent: CallEvent {
|
|
222
|
+
static let name = "onDTMF"
|
|
223
|
+
|
|
224
|
+
let id: UUID
|
|
225
|
+
let digits: String
|
|
226
|
+
|
|
227
|
+
var body: [String: Any] {
|
|
228
|
+
[
|
|
229
|
+
CallEventKeys.id: id.uuidString,
|
|
230
|
+
"digits": digits,
|
|
231
|
+
]
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
struct VideoChangedEvent: CallEvent {
|
|
236
|
+
static let name = "onVideoChanged"
|
|
237
|
+
|
|
238
|
+
let id: UUID
|
|
239
|
+
let hasVideo: Bool
|
|
240
|
+
|
|
241
|
+
var body: [String: Any] {
|
|
242
|
+
[
|
|
243
|
+
CallEventKeys.id: id.uuidString,
|
|
244
|
+
"hasVideo": hasVideo,
|
|
245
|
+
]
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// MARK: - VoIP Push Events
|
|
250
|
+
|
|
251
|
+
struct VoIPPushTokenUpdatedEvent: CallEvent {
|
|
252
|
+
static let name = "onVoIPPushTokenUpdated"
|
|
253
|
+
|
|
254
|
+
let token: String?
|
|
255
|
+
|
|
256
|
+
var body: [String: Any] {
|
|
257
|
+
var result: [String: Any] = ["type": "APNS_VOIP"]
|
|
258
|
+
if let token = token {
|
|
259
|
+
result["token"] = token
|
|
260
|
+
}
|
|
261
|
+
return result
|
|
262
|
+
}
|
|
263
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import ExpoModulesCore
|
|
2
|
+
import Foundation
|
|
3
|
+
|
|
4
|
+
struct CallOptions: Equatable {
|
|
5
|
+
var hasVideo: Bool
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
struct CallOptionsRecord: Record {
|
|
9
|
+
@Field
|
|
10
|
+
var hasVideo: Bool = false
|
|
11
|
+
|
|
12
|
+
func toModel() -> CallOptions {
|
|
13
|
+
CallOptions(hasVideo: hasVideo)
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import ExpoModulesCore
|
|
2
|
+
import Foundation
|
|
3
|
+
|
|
4
|
+
struct CallParticipant: Equatable {
|
|
5
|
+
let id: String
|
|
6
|
+
let phoneNumber: String?
|
|
7
|
+
let email: String?
|
|
8
|
+
let displayName: String?
|
|
9
|
+
let avatarUrl: String?
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
struct CallParticipantRecord: Record {
|
|
13
|
+
@Field
|
|
14
|
+
var id: String
|
|
15
|
+
|
|
16
|
+
@Field
|
|
17
|
+
var phoneNumber: String?
|
|
18
|
+
|
|
19
|
+
@Field
|
|
20
|
+
var email: String?
|
|
21
|
+
|
|
22
|
+
@Field
|
|
23
|
+
var displayName: String?
|
|
24
|
+
|
|
25
|
+
@Field
|
|
26
|
+
var avatarUrl: String?
|
|
27
|
+
|
|
28
|
+
func toModel() -> CallParticipant {
|
|
29
|
+
CallParticipant(
|
|
30
|
+
id: id,
|
|
31
|
+
phoneNumber: phoneNumber,
|
|
32
|
+
email: email,
|
|
33
|
+
displayName: displayName,
|
|
34
|
+
avatarUrl: avatarUrl
|
|
35
|
+
)
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
/// Represents an active CallKit session.
|
|
4
|
+
struct CallSession: Equatable {
|
|
5
|
+
let id: UUID
|
|
6
|
+
var options: CallOptions
|
|
7
|
+
let origin: Origin
|
|
8
|
+
let remoteParticipants: [CallParticipant]
|
|
9
|
+
let incomingCallEvent: IncomingCallEvent?
|
|
10
|
+
var status: Status
|
|
11
|
+
var connectedAt: Date?
|
|
12
|
+
var isMuted: Bool
|
|
13
|
+
var isOnHold: Bool
|
|
14
|
+
var dtmfDigits: String?
|
|
15
|
+
|
|
16
|
+
enum Origin: String {
|
|
17
|
+
case incoming
|
|
18
|
+
case outgoingApp
|
|
19
|
+
case outgoingSystem
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
enum Status: String {
|
|
23
|
+
case requesting
|
|
24
|
+
case ringing
|
|
25
|
+
case connecting
|
|
26
|
+
case connected
|
|
27
|
+
case ended
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// MARK: - Serialization
|
|
32
|
+
|
|
33
|
+
extension CallSession {
|
|
34
|
+
func toDictionary() -> [String: Any] {
|
|
35
|
+
var dict: [String: Any] = [
|
|
36
|
+
"id": id.uuidString,
|
|
37
|
+
"origin": origin.rawValue,
|
|
38
|
+
"options": [
|
|
39
|
+
"hasVideo": options.hasVideo
|
|
40
|
+
],
|
|
41
|
+
"remoteParticipants": remoteParticipants.map { participant in
|
|
42
|
+
var p: [String: Any] = [
|
|
43
|
+
"id": participant.id
|
|
44
|
+
]
|
|
45
|
+
if let phoneNumber = participant.phoneNumber {
|
|
46
|
+
p["phoneNumber"] = phoneNumber
|
|
47
|
+
}
|
|
48
|
+
if let email = participant.email {
|
|
49
|
+
p["email"] = email
|
|
50
|
+
}
|
|
51
|
+
if let displayName = participant.displayName {
|
|
52
|
+
p["displayName"] = displayName
|
|
53
|
+
}
|
|
54
|
+
if let avatarUrl = participant.avatarUrl {
|
|
55
|
+
p["avatarUrl"] = avatarUrl
|
|
56
|
+
}
|
|
57
|
+
return p
|
|
58
|
+
},
|
|
59
|
+
"status": status.rawValue,
|
|
60
|
+
"isMuted": isMuted,
|
|
61
|
+
"isOnHold": isOnHold,
|
|
62
|
+
]
|
|
63
|
+
|
|
64
|
+
if let dtmfDigits = dtmfDigits {
|
|
65
|
+
dict["dtmfDigits"] = dtmfDigits
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if let connectedAt = connectedAt {
|
|
69
|
+
dict["connectedAt"] = ISO8601DateFormatter().string(
|
|
70
|
+
from: connectedAt
|
|
71
|
+
)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if let incomingCallEvent = incomingCallEvent {
|
|
75
|
+
dict["incomingCallEvent"] = incomingCallEvent.toDictionary()
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return dict
|
|
79
|
+
}
|
|
80
|
+
}
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import ExpoModulesCore
|
|
2
|
+
import Foundation
|
|
3
|
+
|
|
4
|
+
/// A validated incoming call event, parsed from a push payload or supplied
|
|
5
|
+
/// directly by JS via `reportIncomingCall`.
|
|
6
|
+
///
|
|
7
|
+
/// Mirrors the TS `IncomingCallEvent` declared in `src/Calls.types.ts`.
|
|
8
|
+
struct IncomingCallEvent: Equatable {
|
|
9
|
+
let eventId: String
|
|
10
|
+
/// Server-assigned id for this call (distinct from the native UUID assigned
|
|
11
|
+
/// by CallKit). Use this to talk to your backend about the call.
|
|
12
|
+
let serverCallId: String
|
|
13
|
+
let caller: Caller
|
|
14
|
+
let hasVideo: Bool
|
|
15
|
+
let startedAt: Date?
|
|
16
|
+
/// App-defined extra fields forwarded verbatim from the push payload.
|
|
17
|
+
/// Opaque to the library; excluded from `==` since `[String: Any]` is not
|
|
18
|
+
/// `Equatable` and events with the same `eventId` are treated as equivalent.
|
|
19
|
+
let metadata: [String: Any]?
|
|
20
|
+
|
|
21
|
+
struct Caller: Equatable {
|
|
22
|
+
let id: String
|
|
23
|
+
let displayName: String?
|
|
24
|
+
let avatarUrl: String?
|
|
25
|
+
let phoneNumber: String?
|
|
26
|
+
let email: String?
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
static func == (lhs: IncomingCallEvent, rhs: IncomingCallEvent) -> Bool {
|
|
30
|
+
lhs.eventId == rhs.eventId
|
|
31
|
+
&& lhs.serverCallId == rhs.serverCallId
|
|
32
|
+
&& lhs.caller == rhs.caller
|
|
33
|
+
&& lhs.hasVideo == rhs.hasVideo
|
|
34
|
+
&& lhs.startedAt == rhs.startedAt
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
func toDictionary() -> [String: Any] {
|
|
38
|
+
var dict: [String: Any] = [
|
|
39
|
+
"eventId": eventId,
|
|
40
|
+
"serverCallId": serverCallId,
|
|
41
|
+
"hasVideo": hasVideo,
|
|
42
|
+
"caller": {
|
|
43
|
+
var c: [String: Any] = ["id": caller.id]
|
|
44
|
+
if let displayName = caller.displayName {
|
|
45
|
+
c["displayName"] = displayName
|
|
46
|
+
}
|
|
47
|
+
if let avatarUrl = caller.avatarUrl {
|
|
48
|
+
c["avatarUrl"] = avatarUrl
|
|
49
|
+
}
|
|
50
|
+
if let phoneNumber = caller.phoneNumber {
|
|
51
|
+
c["phoneNumber"] = phoneNumber
|
|
52
|
+
}
|
|
53
|
+
if let email = caller.email {
|
|
54
|
+
c["email"] = email
|
|
55
|
+
}
|
|
56
|
+
return c
|
|
57
|
+
}(),
|
|
58
|
+
]
|
|
59
|
+
if let startedAt = startedAt {
|
|
60
|
+
dict["startedAt"] = ISO8601DateFormatter().string(from: startedAt)
|
|
61
|
+
}
|
|
62
|
+
if let metadata = metadata {
|
|
63
|
+
dict["metadata"] = metadata
|
|
64
|
+
}
|
|
65
|
+
return dict
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// MARK: - Expo Records (JS → Native, via `reportIncomingCall`)
|
|
70
|
+
|
|
71
|
+
struct IncomingCallCallerRecord: Record {
|
|
72
|
+
@Field
|
|
73
|
+
var id: String = ""
|
|
74
|
+
|
|
75
|
+
@Field
|
|
76
|
+
var displayName: String?
|
|
77
|
+
|
|
78
|
+
@Field
|
|
79
|
+
var avatarUrl: String?
|
|
80
|
+
|
|
81
|
+
@Field
|
|
82
|
+
var phoneNumber: String?
|
|
83
|
+
|
|
84
|
+
@Field
|
|
85
|
+
var email: String?
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
struct IncomingCallEventRecord: Record {
|
|
89
|
+
@Field
|
|
90
|
+
var eventId: String = ""
|
|
91
|
+
|
|
92
|
+
@Field
|
|
93
|
+
var serverCallId: String = ""
|
|
94
|
+
|
|
95
|
+
@Field
|
|
96
|
+
var caller: IncomingCallCallerRecord = IncomingCallCallerRecord()
|
|
97
|
+
|
|
98
|
+
@Field
|
|
99
|
+
var hasVideo: Bool = false
|
|
100
|
+
|
|
101
|
+
@Field
|
|
102
|
+
var startedAt: String?
|
|
103
|
+
|
|
104
|
+
@Field
|
|
105
|
+
var metadata: [String: Any]?
|
|
106
|
+
|
|
107
|
+
func toModel() throws -> IncomingCallEvent {
|
|
108
|
+
let startedAtDate: Date?
|
|
109
|
+
if let raw = startedAt, !raw.isEmpty {
|
|
110
|
+
startedAtDate = IncomingCallEventParser.parseRFC3339Date(raw) ?? Date()
|
|
111
|
+
} else {
|
|
112
|
+
startedAtDate = nil
|
|
113
|
+
}
|
|
114
|
+
return IncomingCallEvent(
|
|
115
|
+
eventId: eventId,
|
|
116
|
+
serverCallId: serverCallId,
|
|
117
|
+
caller: IncomingCallEvent.Caller(
|
|
118
|
+
id: caller.id,
|
|
119
|
+
displayName: caller.displayName,
|
|
120
|
+
avatarUrl: caller.avatarUrl,
|
|
121
|
+
phoneNumber: caller.phoneNumber,
|
|
122
|
+
email: caller.email
|
|
123
|
+
),
|
|
124
|
+
hasVideo: hasVideo,
|
|
125
|
+
startedAt: startedAtDate,
|
|
126
|
+
metadata: metadata
|
|
127
|
+
)
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// MARK: - Push-payload parsing (PushKit / FCM dictionary → model)
|
|
132
|
+
|
|
133
|
+
/// Parses an `IncomingCallEvent` from a VoIP push payload.
|
|
134
|
+
///
|
|
135
|
+
/// The payload must wrap the event under the top-level key `"incoming_call"`.
|
|
136
|
+
/// There is no fallback to a flat top-level shape. Inner keys are camelCase
|
|
137
|
+
/// to match the TS contract and the example server's wire format.
|
|
138
|
+
enum IncomingCallEventParser {
|
|
139
|
+
static func parse(from payload: [AnyHashable: Any]) -> IncomingCallEvent? {
|
|
140
|
+
guard let event = payload["incoming_call"] as? [AnyHashable: Any] else {
|
|
141
|
+
return nil
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
let eventId = event["eventId"] as? String ?? ""
|
|
145
|
+
let serverCallId = event["serverCallId"] as? String ?? ""
|
|
146
|
+
guard !eventId.isEmpty, !serverCallId.isEmpty else {
|
|
147
|
+
return nil
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
guard let callerDict = event["caller"] as? [AnyHashable: Any],
|
|
151
|
+
let callerId = callerDict["id"] as? String,
|
|
152
|
+
!callerId.isEmpty
|
|
153
|
+
else {
|
|
154
|
+
return nil
|
|
155
|
+
}
|
|
156
|
+
let caller = IncomingCallEvent.Caller(
|
|
157
|
+
id: callerId,
|
|
158
|
+
displayName: callerDict["displayName"] as? String,
|
|
159
|
+
avatarUrl: callerDict["avatarUrl"] as? String,
|
|
160
|
+
phoneNumber: callerDict["phoneNumber"] as? String,
|
|
161
|
+
email: callerDict["email"] as? String
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
let hasVideo = event["hasVideo"] as? Bool ?? false
|
|
165
|
+
|
|
166
|
+
let startedAt: Date?
|
|
167
|
+
if let raw = event["startedAt"] as? String {
|
|
168
|
+
startedAt = parseRFC3339Date(raw)
|
|
169
|
+
} else {
|
|
170
|
+
startedAt = nil
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
let metadata = event["metadata"] as? [String: Any]
|
|
174
|
+
|
|
175
|
+
return IncomingCallEvent(
|
|
176
|
+
eventId: eventId,
|
|
177
|
+
serverCallId: serverCallId,
|
|
178
|
+
caller: caller,
|
|
179
|
+
hasVideo: hasVideo,
|
|
180
|
+
startedAt: startedAt,
|
|
181
|
+
metadata: metadata
|
|
182
|
+
)
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/// Parses an RFC 3339 timestamp. Handles optional fractional seconds.
|
|
186
|
+
static func parseRFC3339Date(_ string: String) -> Date? {
|
|
187
|
+
let withFraction = ISO8601DateFormatter()
|
|
188
|
+
withFraction.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
|
|
189
|
+
if let date = withFraction.date(from: string) {
|
|
190
|
+
return date
|
|
191
|
+
}
|
|
192
|
+
let noFraction = ISO8601DateFormatter()
|
|
193
|
+
noFraction.formatOptions = [.withInternetDateTime]
|
|
194
|
+
return noFraction.date(from: string)
|
|
195
|
+
}
|
|
196
|
+
}
|