@cap-kit/people 8.0.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/CapKitPeople.podspec +20 -0
- package/LICENSE +21 -0
- package/Package.swift +28 -0
- package/README.md +1177 -0
- package/android/build.gradle +101 -0
- package/android/src/main/AndroidManifest.xml +4 -0
- package/android/src/main/java/io/capkit/people/PeopleImpl.kt +1003 -0
- package/android/src/main/java/io/capkit/people/PeopleObserver.kt +80 -0
- package/android/src/main/java/io/capkit/people/PeoplePlugin.kt +766 -0
- package/android/src/main/java/io/capkit/people/config/PeopleConfig.kt +44 -0
- package/android/src/main/java/io/capkit/people/error/PeopleError.kt +90 -0
- package/android/src/main/java/io/capkit/people/error/PeopleErrorMessages.kt +39 -0
- package/android/src/main/java/io/capkit/people/logger/PeopleLogger.kt +85 -0
- package/android/src/main/java/io/capkit/people/models/ContactModels.kt +64 -0
- package/android/src/main/java/io/capkit/people/utils/PeopleUtils.kt +133 -0
- package/android/src/main/res/.gitkeep +0 -0
- package/dist/docs.json +1449 -0
- package/dist/esm/definitions.d.ts +775 -0
- package/dist/esm/definitions.js +31 -0
- package/dist/esm/definitions.js.map +1 -0
- package/dist/esm/index.d.ts +15 -0
- package/dist/esm/index.js +18 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/web.d.ts +120 -0
- package/dist/esm/web.js +252 -0
- package/dist/esm/web.js.map +1 -0
- package/dist/plugin.cjs +300 -0
- package/dist/plugin.cjs.map +1 -0
- package/dist/plugin.js +303 -0
- package/dist/plugin.js.map +1 -0
- package/ios/Sources/PeoplePlugin/PeopleImpl.swift +463 -0
- package/ios/Sources/PeoplePlugin/PeoplePlugin.swift +627 -0
- package/ios/Sources/PeoplePlugin/PrivacyInfo.xcprivacy +13 -0
- package/ios/Sources/PeoplePlugin/Utils/PeopleUtils.swift +120 -0
- package/ios/Sources/PeoplePlugin/Version.swift +16 -0
- package/ios/Sources/PeoplePlugin/config/PeopleConfig.swift +56 -0
- package/ios/Sources/PeoplePlugin/error/PeopleError.swift +89 -0
- package/ios/Sources/PeoplePlugin/error/PeopleErrorMessages.swift +25 -0
- package/ios/Sources/PeoplePlugin/logger/PeopleLogging.swift +69 -0
- package/ios/Sources/PeoplePlugin/models/ContactModels.swift +68 -0
- package/ios/Tests/PeoplePluginTests/PeoplePluginTests.swift +10 -0
- package/package.json +119 -0
|
@@ -0,0 +1,627 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import Capacitor
|
|
3
|
+
import Contacts
|
|
4
|
+
import ContactsUI
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
Capacitor Bridge for People.
|
|
8
|
+
Implements CNContactPickerDelegate for Zero-Permission access.
|
|
9
|
+
*/
|
|
10
|
+
@objc(PeoplePlugin)
|
|
11
|
+
public class PeoplePlugin: CAPPlugin, CAPBridgedPlugin, CNContactPickerDelegate {
|
|
12
|
+
|
|
13
|
+
// MARK: - Properties
|
|
14
|
+
|
|
15
|
+
/// An instance of the implementation class that contains the plugin's core functionality.
|
|
16
|
+
private let implementation = PeopleImpl()
|
|
17
|
+
|
|
18
|
+
/// Internal storage for the plugin configuration read from capacitor.config.ts.
|
|
19
|
+
private var config: PeopleConfig?
|
|
20
|
+
|
|
21
|
+
/// The unique identifier for the plugin used by the Capacitor bridge.
|
|
22
|
+
public let identifier = "PeoplePlugin"
|
|
23
|
+
|
|
24
|
+
/// The name used to reference this plugin in JavaScript.
|
|
25
|
+
public let jsName = "People"
|
|
26
|
+
|
|
27
|
+
// State for the active picker call
|
|
28
|
+
private var savedPickCall: CAPPluginCall?
|
|
29
|
+
|
|
30
|
+
// State for observing changes (not yet implemented)
|
|
31
|
+
private var peopleObserverActive = false
|
|
32
|
+
|
|
33
|
+
// Stores the NotificationCenter observer token so it can be removed reliably
|
|
34
|
+
private var peopleObserverToken: NSObjectProtocol?
|
|
35
|
+
|
|
36
|
+
private func beginPickCall(_ call: CAPPluginCall) -> Bool {
|
|
37
|
+
objc_sync_enter(self)
|
|
38
|
+
defer { objc_sync_exit(self) }
|
|
39
|
+
if savedPickCall != nil { return false }
|
|
40
|
+
savedPickCall = call
|
|
41
|
+
return true
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
private func clearPickCall() {
|
|
45
|
+
objc_sync_enter(self)
|
|
46
|
+
savedPickCall = nil
|
|
47
|
+
objc_sync_exit(self)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* A list of methods exposed by this plugin. These methods can be called from the JavaScript side.
|
|
52
|
+
*/
|
|
53
|
+
public let pluginMethods: [CAPPluginMethod] = [
|
|
54
|
+
CAPPluginMethod(name: "getCapabilities", returnType: CAPPluginReturnPromise),
|
|
55
|
+
CAPPluginMethod(name: "pickContact", returnType: CAPPluginReturnPromise),
|
|
56
|
+
CAPPluginMethod(name: "getContact", returnType: CAPPluginReturnPromise),
|
|
57
|
+
CAPPluginMethod(name: "getContacts", returnType: CAPPluginReturnPromise),
|
|
58
|
+
CAPPluginMethod(name: "searchPeople", returnType: CAPPluginReturnPromise),
|
|
59
|
+
CAPPluginMethod(name: "listGroups", returnType: CAPPluginReturnPromise),
|
|
60
|
+
CAPPluginMethod(name: "createGroup", returnType: CAPPluginReturnPromise),
|
|
61
|
+
CAPPluginMethod(name: "deleteGroup", returnType: CAPPluginReturnPromise),
|
|
62
|
+
CAPPluginMethod(name: "addPeopleToGroup", returnType: CAPPluginReturnPromise),
|
|
63
|
+
CAPPluginMethod(name: "removePeopleFromGroup", returnType: CAPPluginReturnPromise),
|
|
64
|
+
CAPPluginMethod(name: "createContact", returnType: CAPPluginReturnPromise),
|
|
65
|
+
CAPPluginMethod(name: "updateContact", returnType: CAPPluginReturnPromise),
|
|
66
|
+
CAPPluginMethod(name: "deleteContact", returnType: CAPPluginReturnPromise),
|
|
67
|
+
CAPPluginMethod(name: "mergeContacts", returnType: CAPPluginReturnPromise),
|
|
68
|
+
CAPPluginMethod(name: "checkPermissions", returnType: CAPPluginReturnPromise),
|
|
69
|
+
CAPPluginMethod(name: "requestPermissions", returnType: CAPPluginReturnPromise),
|
|
70
|
+
CAPPluginMethod(name: "addListener", returnType: CAPPluginReturnNone),
|
|
71
|
+
CAPPluginMethod(name: "removeListener", returnType: CAPPluginReturnNone),
|
|
72
|
+
CAPPluginMethod(name: "removeAllListeners", returnType: CAPPluginReturnNone),
|
|
73
|
+
CAPPluginMethod(name: "getPluginVersion", returnType: CAPPluginReturnPromise)
|
|
74
|
+
]
|
|
75
|
+
|
|
76
|
+
// MARK: - Lifecycle
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Plugin lifecycle entry point.
|
|
80
|
+
*
|
|
81
|
+
* Called once when the plugin is loaded. This method initializes the configuration
|
|
82
|
+
* and prepares the native implementation.
|
|
83
|
+
*/
|
|
84
|
+
override public func load() {
|
|
85
|
+
// Initialize PeopleConfig with the correct type
|
|
86
|
+
let cfg = PeopleConfig(plugin: self)
|
|
87
|
+
self.config = cfg
|
|
88
|
+
implementation.applyConfig(cfg)
|
|
89
|
+
|
|
90
|
+
// Log if verbose logging is enabled
|
|
91
|
+
PeopleLogger.debug("Plugin loaded.")
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
deinit {
|
|
95
|
+
// Ensure NotificationCenter observers are always removed
|
|
96
|
+
stopPeopleObserver()
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// MARK: - Error Mapping
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Rejects the call using standardized error codes from the native PeopleError enum.
|
|
103
|
+
*/
|
|
104
|
+
private func reject(
|
|
105
|
+
_ call: CAPPluginCall,
|
|
106
|
+
error: PeopleError
|
|
107
|
+
) {
|
|
108
|
+
// Use the centralized errorCode and message defined in PeopleError.swift
|
|
109
|
+
call.reject(error.message, error.errorCode)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
private func handleError(_ call: CAPPluginCall, _ error: Error) {
|
|
113
|
+
if let peopleError = error as? PeopleError {
|
|
114
|
+
call.reject(peopleError.message, peopleError.errorCode)
|
|
115
|
+
} else {
|
|
116
|
+
reject(call, error: .initFailed(error.localizedDescription))
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
private func ensureContactsPermission(_ call: CAPPluginCall) -> Bool {
|
|
121
|
+
let status = CNContactStore.authorizationStatus(for: .contacts)
|
|
122
|
+
if #available(iOS 18.0, *) {
|
|
123
|
+
if status == .authorized || status == .limited {
|
|
124
|
+
return true
|
|
125
|
+
}
|
|
126
|
+
} else if status == .authorized {
|
|
127
|
+
return true
|
|
128
|
+
}
|
|
129
|
+
reject(call, error: .permissionDenied("Permission denied"))
|
|
130
|
+
return false
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
private func hasWritableCreateContactField(_ contactData: [String: Any]) -> Bool {
|
|
134
|
+
let writableKeys: Set<String> = [
|
|
135
|
+
"name", "organization", "birthday",
|
|
136
|
+
"phones", "emails", "addresses", "urls",
|
|
137
|
+
"note", "image"
|
|
138
|
+
]
|
|
139
|
+
|
|
140
|
+
for key in writableKeys {
|
|
141
|
+
guard let value = contactData[key] else { continue }
|
|
142
|
+
|
|
143
|
+
if let dict = value as? [String: Any], !dict.isEmpty { return true }
|
|
144
|
+
if let array = value as? [Any], !array.isEmpty { return true }
|
|
145
|
+
if let stringValue = value as? String, !stringValue.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { return true }
|
|
146
|
+
}
|
|
147
|
+
return false
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// MARK: - Capabilities
|
|
151
|
+
|
|
152
|
+
/// Retrieves the plugin capabilities based on authorization status.
|
|
153
|
+
@objc func getCapabilities(_ call: CAPPluginCall) {
|
|
154
|
+
let status = CNContactStore.authorizationStatus(for: .contacts)
|
|
155
|
+
let caps = implementation.getCapabilities(authStatus: status)
|
|
156
|
+
call.resolve(caps)
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// MARK: - Zero-Permission Picker
|
|
160
|
+
|
|
161
|
+
/// Opens the contact picker for user to select a contact.
|
|
162
|
+
@objc func pickContact(_ call: CAPPluginCall) {
|
|
163
|
+
if !beginPickCall(call) {
|
|
164
|
+
return reject(call, error: .conflict("Another pickContact call is already in progress"))
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// 1. Extract and validate projection before opening the UI
|
|
168
|
+
let projection = call.getArray("projection", String.self) ?? ["name", "phones", "emails"]
|
|
169
|
+
|
|
170
|
+
do {
|
|
171
|
+
try PeopleUtils.validateProjection(projection)
|
|
172
|
+
} catch {
|
|
173
|
+
clearPickCall()
|
|
174
|
+
handleError(call, error)
|
|
175
|
+
return
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Dispatch UI to Main Thread
|
|
179
|
+
DispatchQueue.main.async { [weak self] in
|
|
180
|
+
guard let self = self, let viewController = self.bridge?.viewController else {
|
|
181
|
+
self?.clearPickCall()
|
|
182
|
+
self?.reject(call, error: .initFailed("Unable to access ViewController"))
|
|
183
|
+
return
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
let picker = CNContactPickerViewController()
|
|
187
|
+
picker.delegate = self
|
|
188
|
+
|
|
189
|
+
viewController.present(picker, animated: true, completion: nil)
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// MARK: - CNContactPickerDelegate
|
|
194
|
+
|
|
195
|
+
/// User selected a contact
|
|
196
|
+
public func contactPicker(_ picker: CNContactPickerViewController, didSelect contact: CNContact) {
|
|
197
|
+
guard let call = self.savedPickCall else { return }
|
|
198
|
+
defer { clearPickCall() }
|
|
199
|
+
let projectionJS = call.getArray("projection", String.self) ?? ["name", "phones", "emails"]
|
|
200
|
+
|
|
201
|
+
// Use static mapper from Utils instead of implementation member
|
|
202
|
+
let contactData = PeopleUtils.mapToContactData(contact, projection: projectionJS)
|
|
203
|
+
call.resolve(["contact": contactToJS(contactData)])
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/// User cancelled
|
|
207
|
+
public func contactPickerDidCancel(_ picker: CNContactPickerViewController) {
|
|
208
|
+
guard let call = self.savedPickCall else { return }
|
|
209
|
+
defer { clearPickCall() }
|
|
210
|
+
self.reject(call, error: .cancelled("User cancelled selection"))
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// MARK: - Event Listeners
|
|
214
|
+
|
|
215
|
+
/// Adds a listener for contact changes.
|
|
216
|
+
@objc override public func addListener(_ call: CAPPluginCall) {
|
|
217
|
+
guard let eventName = call.getString("eventName"), !eventName.isEmpty else {
|
|
218
|
+
return reject(call, error: .invalidInput(PeopleErrorMessages.eventNameRequired))
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
guard eventName == "peopleChange" else {
|
|
222
|
+
return reject(call, error: .invalidInput(PeopleErrorMessages.unsupportedEventName(eventName)))
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
if !ensureContactsPermission(call) { return }
|
|
226
|
+
|
|
227
|
+
call.keepAlive = true
|
|
228
|
+
super.addListener(call)
|
|
229
|
+
if !peopleObserverActive { startPeopleObserver() }
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/// Remove a listener for contact changes.
|
|
233
|
+
@objc override public func removeListener(_ call: CAPPluginCall) {
|
|
234
|
+
super.removeListener(call)
|
|
235
|
+
|
|
236
|
+
// Release the listener call to prevent retained calls and potential leaks.
|
|
237
|
+
bridge?.releaseCall(withID: call.callbackId)
|
|
238
|
+
|
|
239
|
+
if !hasListeners("peopleChange") {
|
|
240
|
+
stopPeopleObserver()
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/// Removes all listeners for contact changes.
|
|
245
|
+
@objc override public func removeAllListeners(_ call: CAPPluginCall) {
|
|
246
|
+
super.removeAllListeners(call)
|
|
247
|
+
stopPeopleObserver()
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
private func startPeopleObserver() {
|
|
251
|
+
guard !peopleObserverActive else { return }
|
|
252
|
+
peopleObserverToken = NotificationCenter.default.addObserver(
|
|
253
|
+
forName: .CNContactStoreDidChange,
|
|
254
|
+
object: nil,
|
|
255
|
+
queue: .main
|
|
256
|
+
) { [weak self] _ in
|
|
257
|
+
guard let self = self else { return }
|
|
258
|
+
|
|
259
|
+
// Ensure a consistent payload for "peopleChange" listeners.
|
|
260
|
+
// Always include "ids" as an array (may be empty) and a "type" string with default "update".
|
|
261
|
+
if self.hasListeners("peopleChange") {
|
|
262
|
+
// iOS does not provide specific IDs in CNContactStoreDidChange.
|
|
263
|
+
let changedIds: [String] = []
|
|
264
|
+
let eventType: String = "update"
|
|
265
|
+
|
|
266
|
+
self.notifyListeners("peopleChange", data: [
|
|
267
|
+
"ids": changedIds,
|
|
268
|
+
"type": eventType
|
|
269
|
+
])
|
|
270
|
+
} else {
|
|
271
|
+
self.stopPeopleObserver()
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
peopleObserverActive = true
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
private func stopPeopleObserver() {
|
|
278
|
+
if let token = peopleObserverToken {
|
|
279
|
+
NotificationCenter.default.removeObserver(token)
|
|
280
|
+
peopleObserverToken = nil
|
|
281
|
+
}
|
|
282
|
+
peopleObserverActive = false
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// MARK: - Permissions
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Checks permission status.
|
|
289
|
+
*/
|
|
290
|
+
@objc override public func checkPermissions(_ call: CAPPluginCall) {
|
|
291
|
+
let status = CNContactStore.authorizationStatus(for: .contacts)
|
|
292
|
+
var state: String
|
|
293
|
+
if #available(iOS 18.0, *) {
|
|
294
|
+
switch status {
|
|
295
|
+
case .authorized, .limited: state = "granted"
|
|
296
|
+
case .denied, .restricted: state = "denied"
|
|
297
|
+
case .notDetermined: state = "prompt"
|
|
298
|
+
@unknown default: state = "prompt"
|
|
299
|
+
}
|
|
300
|
+
} else {
|
|
301
|
+
switch status {
|
|
302
|
+
case .authorized: state = "granted"
|
|
303
|
+
case .denied, .restricted: state = "denied"
|
|
304
|
+
case .notDetermined: state = "prompt"
|
|
305
|
+
@unknown default: state = "prompt"
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
call.resolve(["contacts": state])
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Requests permission.
|
|
313
|
+
*/
|
|
314
|
+
@objc override public func requestPermissions(_ call: CAPPluginCall) {
|
|
315
|
+
// Fail fast if the host app is missing the required Info.plist usage description key.
|
|
316
|
+
let usageDescription =
|
|
317
|
+
Bundle.main.object(forInfoDictionaryKey: "NSContactsUsageDescription") as? String
|
|
318
|
+
if usageDescription == nil || usageDescription?.isEmpty == true {
|
|
319
|
+
return reject(
|
|
320
|
+
call,
|
|
321
|
+
error: .initFailed(PeopleErrorMessages.missingContactsUsageDescription)
|
|
322
|
+
)
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
let store = CNContactStore()
|
|
326
|
+
store.requestAccess(for: .contacts) { [weak self] granted, error in
|
|
327
|
+
guard let self = self else { return }
|
|
328
|
+
DispatchQueue.main.async {
|
|
329
|
+
if let error = error {
|
|
330
|
+
self.reject(call, error: .initFailed(error.localizedDescription))
|
|
331
|
+
return
|
|
332
|
+
}
|
|
333
|
+
call.resolve(["contacts": granted ? "granted" : "denied"])
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// MARK: - Contact Fetching
|
|
339
|
+
|
|
340
|
+
/// Retrieves a list of contacts based on provided options.
|
|
341
|
+
@objc func getContacts(_ call: CAPPluginCall) {
|
|
342
|
+
if !ensureContactsPermission(call) { return }
|
|
343
|
+
|
|
344
|
+
let projection = call.getArray("projection", String.self) ?? ["name", "phones", "emails"]
|
|
345
|
+
let limit = call.getInt("limit") ?? 10
|
|
346
|
+
let offset = call.getInt("offset") ?? 0
|
|
347
|
+
let includeTotal = call.getBool("includeTotal") ?? false
|
|
348
|
+
|
|
349
|
+
do {
|
|
350
|
+
// Validation Block
|
|
351
|
+
try PeopleUtils.validateProjection(projection)
|
|
352
|
+
|
|
353
|
+
let result = try implementation.fetchContacts(projection: projection, limit: limit, offset: offset, includeTotal: includeTotal)
|
|
354
|
+
call.resolve([
|
|
355
|
+
"contacts": result.contacts.map { contactToJS($0) },
|
|
356
|
+
"totalCount": result.totalCount
|
|
357
|
+
])
|
|
358
|
+
} catch { handleError(call, error) }
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/// Fetch a specific contact by ID
|
|
362
|
+
@objc func getContact(_ call: CAPPluginCall) {
|
|
363
|
+
if !ensureContactsPermission(call) { return }
|
|
364
|
+
|
|
365
|
+
let id = call.getString("id", "")
|
|
366
|
+
if id.isEmpty { return reject(call, error: .invalidInput("id is required")) }
|
|
367
|
+
let projection = call.getArray("projection", String.self) ?? ["name", "phones", "emails"]
|
|
368
|
+
|
|
369
|
+
do {
|
|
370
|
+
// Validation Block
|
|
371
|
+
try PeopleUtils.validateProjection(projection)
|
|
372
|
+
|
|
373
|
+
if let contact = try implementation.fetchContactById(id: id, projection: projection) {
|
|
374
|
+
call.resolve(["contact": contactToJS(contact)])
|
|
375
|
+
} else {
|
|
376
|
+
reject(call, error: .notFound(PeopleErrorMessages.contactNotFound))
|
|
377
|
+
}
|
|
378
|
+
} catch { handleError(call, error) }
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/// Searches for contacts matching a query string.
|
|
382
|
+
@objc func searchPeople(_ call: CAPPluginCall) {
|
|
383
|
+
if !ensureContactsPermission(call) { return }
|
|
384
|
+
|
|
385
|
+
guard let query = call.getString("query"), !query.isEmpty else {
|
|
386
|
+
return reject(call, error: .invalidInput("Missing query"))
|
|
387
|
+
}
|
|
388
|
+
let projection = call.getArray("projection", String.self) ?? ["name", "phones", "emails"]
|
|
389
|
+
let limit = call.getInt("limit") ?? 50
|
|
390
|
+
|
|
391
|
+
do {
|
|
392
|
+
// Validation Block
|
|
393
|
+
try PeopleUtils.validateProjection(projection)
|
|
394
|
+
|
|
395
|
+
let result = try implementation.searchContacts(query: query, projection: projection, limit: limit)
|
|
396
|
+
call.resolve([
|
|
397
|
+
"contacts": result.contacts.map { contactToJS($0) },
|
|
398
|
+
"totalCount": result.totalCount
|
|
399
|
+
])
|
|
400
|
+
} catch { handleError(call, error) }
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// MARK: - Group Management
|
|
404
|
+
|
|
405
|
+
/// Deletes a contact group.
|
|
406
|
+
@objc func listGroups(_ call: CAPPluginCall) {
|
|
407
|
+
if !ensureContactsPermission(call) { return }
|
|
408
|
+
|
|
409
|
+
do {
|
|
410
|
+
let result = try implementation.listGroups()
|
|
411
|
+
call.resolve(["groups": result.groups.map { groupToJS($0) }])
|
|
412
|
+
} catch { handleError(call, error) }
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/// Creates a new contact group.
|
|
416
|
+
@objc func createGroup(_ call: CAPPluginCall) {
|
|
417
|
+
if !ensureContactsPermission(call) { return }
|
|
418
|
+
|
|
419
|
+
guard let name = call.getString("name"), !name.isEmpty else {
|
|
420
|
+
return reject(call, error: .invalidInput("Group name is required"))
|
|
421
|
+
}
|
|
422
|
+
do {
|
|
423
|
+
let groupData = try implementation.createGroup(name: name)
|
|
424
|
+
call.resolve(["group": groupToJS(groupData)])
|
|
425
|
+
} catch { handleError(call, error) }
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/// Deletes a group by ID.
|
|
429
|
+
@objc func deleteGroup(_ call: CAPPluginCall) {
|
|
430
|
+
if !ensureContactsPermission(call) { return }
|
|
431
|
+
|
|
432
|
+
guard let groupId = call.getString("groupId") else {
|
|
433
|
+
return reject(call, error: .invalidInput("Group ID is required"))
|
|
434
|
+
}
|
|
435
|
+
do {
|
|
436
|
+
try implementation.deleteGroup(groupId: groupId)
|
|
437
|
+
call.resolve()
|
|
438
|
+
} catch { handleError(call, error) }
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
/// Adds people to a group.
|
|
442
|
+
@objc func addPeopleToGroup(_ call: CAPPluginCall) {
|
|
443
|
+
if !ensureContactsPermission(call) { return }
|
|
444
|
+
|
|
445
|
+
guard let groupId = call.getString("groupId"),
|
|
446
|
+
let contactIds = call.getArray("contactIds", String.self) else {
|
|
447
|
+
return reject(call, error: .invalidInput("Group ID and Contact IDs are required"))
|
|
448
|
+
}
|
|
449
|
+
do {
|
|
450
|
+
try implementation.addPeopleToGroup(groupId: groupId, contactIds: contactIds)
|
|
451
|
+
call.resolve()
|
|
452
|
+
} catch { handleError(call, error) }
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
/// Removes contacts from a group.
|
|
456
|
+
@objc func removePeopleFromGroup(_ call: CAPPluginCall) {
|
|
457
|
+
if !ensureContactsPermission(call) { return }
|
|
458
|
+
|
|
459
|
+
guard let groupId = call.getString("groupId"),
|
|
460
|
+
let contactIds = call.getArray("contactIds", String.self) else {
|
|
461
|
+
return reject(call, error: .invalidInput("Group ID and Contact IDs are required"))
|
|
462
|
+
}
|
|
463
|
+
do {
|
|
464
|
+
try implementation.removePeopleFromGroup(groupId: groupId, contactIds: contactIds)
|
|
465
|
+
call.resolve()
|
|
466
|
+
} catch { handleError(call, error) }
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
// MARK: - CRUD
|
|
470
|
+
|
|
471
|
+
/// Creates a new contact.
|
|
472
|
+
@objc func createContact(_ call: CAPPluginCall) {
|
|
473
|
+
if !ensureContactsPermission(call) { return }
|
|
474
|
+
|
|
475
|
+
guard let contactData = call.getObject("contact") else {
|
|
476
|
+
return reject(call, error: .invalidInput("Contact data is required"))
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
guard hasWritableCreateContactField(contactData) else {
|
|
480
|
+
return reject(call, error: .invalidInput(PeopleErrorMessages.atLeastOneWritableFieldRequired))
|
|
481
|
+
}
|
|
482
|
+
do {
|
|
483
|
+
let newContact = try implementation.createContact(contactData: contactData)
|
|
484
|
+
call.resolve(["contact": contactToJS(newContact)])
|
|
485
|
+
} catch { handleError(call, error) }
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
/// Updates an existing contact.
|
|
489
|
+
@objc func updateContact(_ call: CAPPluginCall) {
|
|
490
|
+
if !ensureContactsPermission(call) { return }
|
|
491
|
+
|
|
492
|
+
guard let contactId = call.getString("contactId"),
|
|
493
|
+
let contactData = call.getObject("contact") else {
|
|
494
|
+
return reject(call, error: .invalidInput("Contact ID and data are required"))
|
|
495
|
+
}
|
|
496
|
+
do {
|
|
497
|
+
let updatedContact = try implementation.updateContact(contactId: contactId, contactData: contactData)
|
|
498
|
+
call.resolve(["contact": contactToJS(updatedContact)])
|
|
499
|
+
} catch { handleError(call, error) }
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
/// Merges two contacts.
|
|
503
|
+
@objc func mergeContacts(_ call: CAPPluginCall) {
|
|
504
|
+
if !ensureContactsPermission(call) { return }
|
|
505
|
+
|
|
506
|
+
guard let sourceId = call.getString("sourceContactId"),
|
|
507
|
+
let destId = call.getString("destinationContactId") else {
|
|
508
|
+
return reject(call, error: .invalidInput("Both source and destination IDs are required"))
|
|
509
|
+
}
|
|
510
|
+
if sourceId == destId {
|
|
511
|
+
return reject(call, error: .invalidInput("sourceContactId and destinationContactId must be different"))
|
|
512
|
+
}
|
|
513
|
+
do {
|
|
514
|
+
let mergedContact = try implementation.mergeContacts(sourceContactId: sourceId, destinationContactId: destId)
|
|
515
|
+
call.resolve(["contact": contactToJS(mergedContact)])
|
|
516
|
+
} catch { handleError(call, error) }
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
/// Deletes a contact.
|
|
520
|
+
@objc func deleteContact(_ call: CAPPluginCall) {
|
|
521
|
+
if !ensureContactsPermission(call) { return }
|
|
522
|
+
|
|
523
|
+
guard let contactId = call.getString("contactId") else {
|
|
524
|
+
return reject(call, error: .invalidInput("Contact ID is required"))
|
|
525
|
+
}
|
|
526
|
+
do {
|
|
527
|
+
try implementation.deleteContact(contactId: contactId)
|
|
528
|
+
call.resolve()
|
|
529
|
+
} catch { handleError(call, error) }
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
// MARK: - Version
|
|
533
|
+
|
|
534
|
+
/**
|
|
535
|
+
* Retrieves the current native plugin version.
|
|
536
|
+
*
|
|
537
|
+
* This version is synchronized from the project's package.json during the build process.
|
|
538
|
+
*
|
|
539
|
+
* - Parameter call: CAPPluginCall used to return the version string.
|
|
540
|
+
*/
|
|
541
|
+
@objc func getPluginVersion(_ call: CAPPluginCall) {
|
|
542
|
+
call.resolve(["version": PluginVersion.number])
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
// MARK: - Private Mapping Helpers
|
|
546
|
+
|
|
547
|
+
/// JS marshalling helper for UnifiedContact payloads.
|
|
548
|
+
/// Responsibility: bridge layer only (ContactData → JSObject).
|
|
549
|
+
/// Mapping from CNContact → ContactData is handled in PeopleUtils.
|
|
550
|
+
private func contactToJS(_ contact: ContactData) -> JSObject {
|
|
551
|
+
var res = JSObject()
|
|
552
|
+
res["id"] = contact.id
|
|
553
|
+
|
|
554
|
+
var nameObj = JSObject()
|
|
555
|
+
nameObj["display"] = contact.displayName ?? ""
|
|
556
|
+
if let first = contact.firstName { nameObj["given"] = first }
|
|
557
|
+
if let last = contact.lastName { nameObj["family"] = last }
|
|
558
|
+
res["name"] = nameObj
|
|
559
|
+
|
|
560
|
+
if let org = contact.organization {
|
|
561
|
+
var orgObj = JSObject()
|
|
562
|
+
orgObj["company"] = org.company
|
|
563
|
+
orgObj["title"] = org.title
|
|
564
|
+
orgObj["department"] = org.department
|
|
565
|
+
res["organization"] = orgObj
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
if let birthday = contact.birthday {
|
|
569
|
+
var bday = JSObject()
|
|
570
|
+
bday["day"] = birthday.day
|
|
571
|
+
bday["month"] = birthday.month
|
|
572
|
+
if let year = birthday.year, year != NSDateComponentUndefined { bday["year"] = year }
|
|
573
|
+
res["birthday"] = bday
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
if let phones = contact.phones {
|
|
577
|
+
let mappedPhones: [JSObject] = phones.map { phoneData in
|
|
578
|
+
var phone = JSObject()
|
|
579
|
+
phone["label"] = phoneData.label
|
|
580
|
+
phone["number"] = phoneData.number
|
|
581
|
+
return phone
|
|
582
|
+
}
|
|
583
|
+
res["phones"] = mappedPhones
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
if let emails = contact.emails {
|
|
587
|
+
let mappedEmails: [JSObject] = emails.map { emailData in
|
|
588
|
+
var email = JSObject()
|
|
589
|
+
email["label"] = emailData.label
|
|
590
|
+
email["address"] = emailData.address
|
|
591
|
+
return email
|
|
592
|
+
}
|
|
593
|
+
res["emails"] = mappedEmails
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
if let addresses = contact.addresses {
|
|
597
|
+
let mappedAddresses: [JSObject] = addresses.map { addressData in
|
|
598
|
+
var address = JSObject()
|
|
599
|
+
address["label"] = addressData.label
|
|
600
|
+
address["formatted"] = addressData.formatted
|
|
601
|
+
address["street"] = addressData.street
|
|
602
|
+
address["city"] = addressData.city
|
|
603
|
+
address["region"] = addressData.region
|
|
604
|
+
address["postcode"] = addressData.postcode
|
|
605
|
+
address["country"] = addressData.country
|
|
606
|
+
return address
|
|
607
|
+
}
|
|
608
|
+
res["addresses"] = mappedAddresses
|
|
609
|
+
}
|
|
610
|
+
if let urls = contact.urls { res["urls"] = urls }
|
|
611
|
+
if let note = contact.note { res["note"] = note }
|
|
612
|
+
if let image = contact.image { res["image"] = image }
|
|
613
|
+
|
|
614
|
+
return res
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
/// JS marshalling helper for contact groups.
|
|
618
|
+
/// Responsibility: bridge layer only (GroupData → JSObject).
|
|
619
|
+
private func groupToJS(_ group: GroupData) -> JSObject {
|
|
620
|
+
var res = JSObject()
|
|
621
|
+
res["id"] = group.id
|
|
622
|
+
res["name"] = group.name
|
|
623
|
+
res["source"] = group.source
|
|
624
|
+
res["readOnly"] = group.readOnly
|
|
625
|
+
return res
|
|
626
|
+
}
|
|
627
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
3
|
+
<plist version="1.0">
|
|
4
|
+
<dict>
|
|
5
|
+
<!--
|
|
6
|
+
This plugin does not access any of Apple's "Required Reason APIs".
|
|
7
|
+
If you add any Required Reason API usage in the future, you must declare it here
|
|
8
|
+
with the appropriate category and reason code(s).
|
|
9
|
+
-->
|
|
10
|
+
<key>NSPrivacyAccessedAPITypes</key>
|
|
11
|
+
<array/>
|
|
12
|
+
</dict>
|
|
13
|
+
</plist>
|