@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 @@
|
|
|
1
|
+
{"version":3,"file":"plugin.js","sources":["esm/definitions.js","esm/index.js","esm/web.js"],"sourcesContent":["/// <reference types=\"@capacitor/cli\" />\n/**\n * Standardized error codes used by the People plugin.\n *\n * These codes are returned when a Promise is rejected and can be caught\n * via try/catch blocks.\n *\n * @since 8.0.0\n */\nexport var PeopleErrorCode;\n(function (PeopleErrorCode) {\n /** The device does not have the requested hardware or the feature is not available on this platform. */\n PeopleErrorCode[\"UNAVAILABLE\"] = \"UNAVAILABLE\";\n /** The user cancelled an interactive flow. */\n PeopleErrorCode[\"CANCELLED\"] = \"CANCELLED\";\n /** The user denied the permission or the feature is disabled by the OS. */\n PeopleErrorCode[\"PERMISSION_DENIED\"] = \"PERMISSION_DENIED\";\n /** The plugin failed to initialize or perform an operation. */\n PeopleErrorCode[\"INIT_FAILED\"] = \"INIT_FAILED\";\n /** The input provided to the plugin method is invalid, missing, or malformed. */\n PeopleErrorCode[\"INVALID_INPUT\"] = \"INVALID_INPUT\";\n /** The requested type is not valid or supported. */\n PeopleErrorCode[\"UNKNOWN_TYPE\"] = \"UNKNOWN_TYPE\";\n /** The requested resource does not exist. */\n PeopleErrorCode[\"NOT_FOUND\"] = \"NOT_FOUND\";\n /** The operation conflicts with the current state. */\n PeopleErrorCode[\"CONFLICT\"] = \"CONFLICT\";\n /** The operation did not complete within the expected time. */\n PeopleErrorCode[\"TIMEOUT\"] = \"TIMEOUT\";\n})(PeopleErrorCode || (PeopleErrorCode = {}));\n//# sourceMappingURL=definitions.js.map","/**\n * @file index.ts\n * Main entry point for the People Capacitor Plugin.\n * This file handles the registration of the plugin with the Capacitor core runtime\n * and exports all necessary types for consumers.\n */\nimport { registerPlugin } from '@capacitor/core';\n/**\n * The People plugin instance.\n * It automatically lazy-loads the web implementation if running in a browser environment.\n * Use this instance to access all people functionality.\n */\nconst People = registerPlugin('People', {\n web: () => import('./web').then((m) => new m.PeopleWeb()),\n});\nexport * from './definitions';\nexport { People };\n//# sourceMappingURL=index.js.map","import { WebPlugin } from '@capacitor/core';\n/**\n * Class representing the web implementation of the PeoplePlugin.\n * This class extends the WebPlugin class and implements the PeoplePlugin interface.\n * It provides a base implementation for web-based functionality of the plugin.\n */\nexport class PeopleWeb extends WebPlugin {\n constructor() {\n super();\n }\n // -----------------------------------------------------------------------------\n // Capabilities\n // -----------------------------------------------------------------------------\n /**\n * Retrieves the capabilities of the People plugin on the web platform.\n *\n * @returns A promise resolving to the PeopleCapabilities object.\n */\n async getCapabilities() {\n return {\n canRead: false, // Web cannot bulk read the address book\n canWrite: false,\n canObserve: false,\n canManageGroups: false,\n canPickContact: true, // Only the Zero-Permission picker is supported via Contact Picker API\n };\n }\n // -----------------------------------------------------------------------------\n // Permissions\n // -----------------------------------------------------------------------------\n /**\n * Checks the permission status for the plugin.\n *\n * @returns A promise resolving to an object containing the permission states.\n */\n async checkPermissions() {\n return { contacts: 'prompt' };\n }\n /**\n * Requests the necessary permissions for the plugin.\n *\n * @returns A promise resolving to an object containing the updated permission states.\n */\n async requestPermissions() {\n return { contacts: 'prompt' };\n }\n // -----------------------------------------------------------------------------\n // Contact Picking\n // -----------------------------------------------------------------------------\n /**\n * Launches the OS contact picker UI.\n * On Web this uses the Contact Picker API if available.\n * * Architectural rules:\n * - Rejects with CANCELLED on user cancellation to match native behavior.\n */\n async pickContact(options) {\n var _a, _b, _c, _d;\n const contactsNavigator = navigator;\n const props = (options === null || options === void 0 ? void 0 : options.projection) || ['name', 'phones', 'emails'];\n const supportedProjection = new Set([\n 'name',\n 'organization',\n 'birthday',\n 'phones',\n 'emails',\n 'addresses',\n 'urls',\n 'note',\n ]);\n for (const field of props) {\n if (!supportedProjection.has(field)) {\n return Promise.reject({\n message: `Unsupported projection field: ${field}`,\n code: 'UNKNOWN_TYPE',\n });\n }\n }\n // Support for the modern Web Contact Picker API\n if ((_a = contactsNavigator.contacts) === null || _a === void 0 ? void 0 : _a.select) {\n try {\n // Marshalling: Map plugin projections to Web API properties\n const webProps = props.map((p) => {\n if (p === 'phones')\n return 'tel';\n if (p === 'emails')\n return 'email';\n return p;\n });\n const contacts = await contactsNavigator.contacts.select(webProps, { multiple: false });\n // Some browsers return an empty array instead of throwing on cancel; normalize to CANCELLED.\n if (!contacts || contacts.length === 0) {\n return Promise.reject({\n message: 'User cancelled selection',\n code: 'CANCELLED',\n });\n }\n const raw = contacts[0];\n return {\n contact: {\n id: 'web-ref',\n name: { display: ((_b = raw.name) === null || _b === void 0 ? void 0 : _b[0]) || 'Unknown' },\n phones: ((_c = raw.tel) === null || _c === void 0 ? void 0 : _c.map((t) => ({ number: t }))) || [],\n emails: ((_d = raw.email) === null || _d === void 0 ? void 0 : _d.map((e) => ({ address: e }))) || [],\n },\n };\n }\n catch (e) {\n const err = e;\n // Map specific Web API cancellation to the standardized CANCELLED code\n if (err.name === 'AbortError') {\n return Promise.reject({\n message: 'User cancelled selection',\n code: 'CANCELLED',\n });\n }\n throw this.unavailable(err.message || 'Web Contact Picker API failed');\n }\n }\n throw this.unavailable('Native Contact Picker not available in this browser.');\n }\n // -----------------------------------------------------------------------------\n // Directory Access (Not supported on Web)\n // -----------------------------------------------------------------------------\n /**\n * Retrieves contacts from the system directory.\n * @param _options - Options for retrieving contacts.\n *\n * @returns A promise resolving to the contacts result.\n */\n async getContacts() {\n throw this.unimplemented('getContacts is not available on Web.');\n }\n /**\n * Retrieves a single contact by ID.\n * @param _options - Options containing the contact ID.\n *\n * @returns A promise resolving to the contact.\n */\n async getContact() {\n throw this.unimplemented('getContact is not available on Web.');\n }\n /**\n * Searches contacts in the system directory.\n * @param _options - Options for searching contacts.\n *\n * @returns A promise resolving to the contacts result.\n */\n async searchPeople() {\n throw this.unimplemented('searchPeople is not available on Web.');\n }\n // -----------------------------------------------------------------------------\n // Group Management (Not supported on Web)\n // -----------------------------------------------------------------------------\n /**\n * Lists all available contact groups.\n * @returns A promise that is rejected because this feature is not available on Web.\n */\n async listGroups() {\n throw this.unimplemented('listGroups is not available on Web.');\n }\n /**\n * Creates a new contact group.\n * @returns A promise that is rejected because this feature is not available on Web.\n */\n async createGroup(_options) {\n void _options;\n throw this.unimplemented('createGroup is not available on Web.');\n }\n /**\n * Deletes a contact group.\n * @returns A promise that is rejected because this feature is not available on Web.\n */\n async deleteGroup(_options) {\n void _options;\n throw this.unimplemented('deleteGroup is not available on Web.');\n }\n /**\n * Adds contacts to a group.\n * @returns A promise that is rejected because this feature is not available on Web.\n */\n async addPeopleToGroup(_options) {\n void _options;\n throw this.unimplemented('addPeopleToGroup is not available on Web.');\n }\n /**\n * Removes contacts from a group.\n * @returns A promise that is rejected because this feature is not available on Web.\n */\n async removePeopleFromGroup(_options) {\n void _options;\n throw this.unimplemented('removePeopleFromGroup is not available on Web.');\n }\n // -----------------------------------------------------------------------------\n // CRUD (Not supported on Web)\n // -----------------------------------------------------------------------------\n /**\n * Creates a new contact.\n * @returns A promise that is rejected because this feature is not available on Web.\n */\n async createContact(_options) {\n void _options;\n throw this.unimplemented('createContact is not available on Web.');\n }\n /**\n * Updates an existing contact.\n * @returns A promise that is rejected because this feature is not available on Web.\n */\n async updateContact(_options) {\n void _options;\n throw this.unimplemented('updateContact is not available on Web.');\n }\n /**\n * Deletes a contact.\n * @returns A promise that is rejected because this feature is not available on Web.\n */\n async deleteContact(_options) {\n void _options;\n throw this.unimplemented('deleteContact is not available on Web.');\n }\n /**\n * Merges two contacts.\n * @returns A promise that is rejected because this feature is not available on Web.\n */\n async mergeContacts(_options) {\n void _options;\n throw this.unimplemented('mergeContacts is not available on Web.');\n }\n // -----------------------------------------------------------------------------\n // Plugin Info\n // -----------------------------------------------------------------------------\n /**\n * Returns the plugin version.\n *\n * On the Web, this value represents the JavaScript package version\n * rather than a native implementation.\n */\n async getPluginVersion() {\n return { version: 'web' };\n }\n // -----------------------------------------------------------------------------\n // Listener Handling\n // -----------------------------------------------------------------------------\n /**\n * Cleanup all listeners\n *\n * @returns A promise that resolves when all listeners are removed.\n */\n async removeAllListeners() {\n super.removeAllListeners();\n }\n}\n//# sourceMappingURL=web.js.map"],"names":["PeopleErrorCode","registerPlugin","WebPlugin"],"mappings":"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"}
|
|
@@ -0,0 +1,463 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import Contacts
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Native iOS implementation for the People plugin.
|
|
6
|
+
*
|
|
7
|
+
* Architectural rules:
|
|
8
|
+
* - MUST NOT access CAPPluginCall.
|
|
9
|
+
* - MUST NOT depend on Capacitor bridge APIs directly.
|
|
10
|
+
* - MUST throw PeopleError for specific failures.
|
|
11
|
+
*/
|
|
12
|
+
@objc public final class PeopleImpl: NSObject {
|
|
13
|
+
|
|
14
|
+
// MARK: - Properties
|
|
15
|
+
|
|
16
|
+
/// Cached plugin configuration containing logging and behavioral flags.
|
|
17
|
+
private var config: PeopleConfig?
|
|
18
|
+
|
|
19
|
+
/// Shared contact store instance for performance optimization.
|
|
20
|
+
private let store = CNContactStore()
|
|
21
|
+
|
|
22
|
+
// MARK: - Initialization
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Initializes the implementation instance.
|
|
26
|
+
*/
|
|
27
|
+
override init() {
|
|
28
|
+
super.init()
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// MARK: - Configuration
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Applies static plugin configuration.
|
|
35
|
+
*
|
|
36
|
+
* This method MUST be called exactly once from the Plugin bridge layer during `load()`.
|
|
37
|
+
* It synchronizes the native logger state with the provided configuration.
|
|
38
|
+
*
|
|
39
|
+
* - Parameter config: The immutable configuration container.
|
|
40
|
+
*/
|
|
41
|
+
public func applyConfig(_ config: PeopleConfig) {
|
|
42
|
+
precondition(
|
|
43
|
+
self.config == nil,
|
|
44
|
+
"PeopleImpl.applyConfig(_:) must be called exactly once"
|
|
45
|
+
)
|
|
46
|
+
self.config = config
|
|
47
|
+
PeopleLogger.verbose = config.verboseLogging
|
|
48
|
+
|
|
49
|
+
PeopleLogger.debug(
|
|
50
|
+
"Configuration applied. Verbose logging:",
|
|
51
|
+
config.verboseLogging
|
|
52
|
+
)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// MARK: - Capabilities
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Determines the capabilities based on the current authorization status.
|
|
59
|
+
*/
|
|
60
|
+
public func getCapabilities(authStatus: CNAuthorizationStatus) -> [String: Bool] {
|
|
61
|
+
let isAuthorized: Bool
|
|
62
|
+
if #available(iOS 18.0, *) {
|
|
63
|
+
isAuthorized = (authStatus == .authorized || authStatus == .limited)
|
|
64
|
+
} else {
|
|
65
|
+
isAuthorized = (authStatus == .authorized)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return [
|
|
69
|
+
"canRead": isAuthorized,
|
|
70
|
+
"canWrite": isAuthorized,
|
|
71
|
+
"canObserve": isAuthorized,
|
|
72
|
+
"canManageGroups": isAuthorized,
|
|
73
|
+
"canPickContact": true
|
|
74
|
+
]
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// MARK: - Contact Fetching
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Fetches contacts with pagination and projection using nominal models.
|
|
81
|
+
*/
|
|
82
|
+
public func fetchContacts(
|
|
83
|
+
projection: [String],
|
|
84
|
+
limit: Int,
|
|
85
|
+
offset: Int,
|
|
86
|
+
includeTotal: Bool = true
|
|
87
|
+
) throws -> GetContactsResultData {
|
|
88
|
+
|
|
89
|
+
let safeLimit = max(0, limit)
|
|
90
|
+
let safeOffset = max(0, offset)
|
|
91
|
+
let keys = keysForProjection(projection)
|
|
92
|
+
let request = CNContactFetchRequest(keysToFetch: keys)
|
|
93
|
+
|
|
94
|
+
var results: [ContactData] = []
|
|
95
|
+
var currentIndex = 0
|
|
96
|
+
let upperBound = safeOffset + safeLimit
|
|
97
|
+
|
|
98
|
+
try store.enumerateContacts(with: request) { contact, stop in
|
|
99
|
+
if currentIndex >= safeOffset && currentIndex < upperBound {
|
|
100
|
+
results.append(PeopleUtils.mapToContactData(contact, projection: projection))
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// When includeTotal is true we must continue enumerating to compute totalCount.
|
|
104
|
+
currentIndex += 1
|
|
105
|
+
|
|
106
|
+
if !includeTotal && currentIndex >= upperBound {
|
|
107
|
+
stop.pointee = true
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return GetContactsResultData(
|
|
112
|
+
contacts: results,
|
|
113
|
+
totalCount: includeTotal ? currentIndex : results.count
|
|
114
|
+
)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Fetches a single contact by identifier using nominal models.
|
|
119
|
+
*/
|
|
120
|
+
public func fetchContactById(
|
|
121
|
+
id: String,
|
|
122
|
+
projection: [String]
|
|
123
|
+
) throws -> ContactData? {
|
|
124
|
+
|
|
125
|
+
let keys = keysForProjection(projection)
|
|
126
|
+
let contact = try store.unifiedContact(
|
|
127
|
+
withIdentifier: id,
|
|
128
|
+
keysToFetch: keys
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
return PeopleUtils.mapToContactData(contact, projection: projection)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Searches for contacts matching a query string using nominal models.
|
|
136
|
+
*/
|
|
137
|
+
public func searchContacts(
|
|
138
|
+
query: String,
|
|
139
|
+
projection: [String],
|
|
140
|
+
limit: Int
|
|
141
|
+
) throws -> GetContactsResultData {
|
|
142
|
+
|
|
143
|
+
let trimmedQuery = query.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
144
|
+
guard !trimmedQuery.isEmpty else { return GetContactsResultData(contacts: [], totalCount: 0) }
|
|
145
|
+
|
|
146
|
+
let safeLimit = max(0, limit)
|
|
147
|
+
let keys = keysForProjection(projection)
|
|
148
|
+
let predicate = CNContact.predicateForContacts(matchingName: trimmedQuery)
|
|
149
|
+
let request = CNContactFetchRequest(keysToFetch: keys)
|
|
150
|
+
request.predicate = predicate
|
|
151
|
+
|
|
152
|
+
var results: [ContactData] = []
|
|
153
|
+
var totalCount = 0
|
|
154
|
+
|
|
155
|
+
try store.enumerateContacts(with: request) { contact, _ in
|
|
156
|
+
totalCount += 1
|
|
157
|
+
|
|
158
|
+
if results.count < safeLimit {
|
|
159
|
+
results.append(PeopleUtils.mapToContactData(contact, projection: projection))
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return GetContactsResultData(contacts: results, totalCount: totalCount)
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Determines the CNKeyDescriptors needed for the given projection.
|
|
168
|
+
*/
|
|
169
|
+
private func keysForProjection(_ projection: [String]) -> [CNKeyDescriptor] {
|
|
170
|
+
let normalizedProjection = Set(projection)
|
|
171
|
+
var keyNames: Set<String> = [
|
|
172
|
+
CNContactIdentifierKey,
|
|
173
|
+
CNContactGivenNameKey,
|
|
174
|
+
CNContactFamilyNameKey
|
|
175
|
+
]
|
|
176
|
+
|
|
177
|
+
let projectionMap: [String: [String]] = [
|
|
178
|
+
"name": [CNContactGivenNameKey, CNContactFamilyNameKey],
|
|
179
|
+
"phones": [CNContactPhoneNumbersKey],
|
|
180
|
+
"emails": [CNContactEmailAddressesKey],
|
|
181
|
+
"organization": [CNContactOrganizationNameKey, CNContactJobTitleKey, CNContactDepartmentNameKey],
|
|
182
|
+
"birthday": [CNContactBirthdayKey],
|
|
183
|
+
"image": [CNContactThumbnailImageDataKey],
|
|
184
|
+
"addresses": [CNContactPostalAddressesKey],
|
|
185
|
+
"urls": [CNContactUrlAddressesKey],
|
|
186
|
+
"note": [CNContactNoteKey]
|
|
187
|
+
]
|
|
188
|
+
|
|
189
|
+
for field in normalizedProjection {
|
|
190
|
+
if let mappedKeys = projectionMap[field] {
|
|
191
|
+
keyNames.formUnion(mappedKeys)
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return keyNames.map { $0 as CNKeyDescriptor }
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// MARK: - Group Management
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Fetches all contact groups from the contact store using nominal models.
|
|
202
|
+
*/
|
|
203
|
+
public func listGroups() throws -> ListGroupsResultData {
|
|
204
|
+
let groups = try store.groups(matching: nil)
|
|
205
|
+
let result = groups.map { PeopleUtils.mapToGroupData($0) }
|
|
206
|
+
return ListGroupsResultData(groups: result)
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Creates a new group with the given name.
|
|
211
|
+
*/
|
|
212
|
+
public func createGroup(name: String) throws -> GroupData {
|
|
213
|
+
let saveRequest = CNSaveRequest()
|
|
214
|
+
let newGroup = CNMutableGroup()
|
|
215
|
+
newGroup.name = name
|
|
216
|
+
|
|
217
|
+
saveRequest.add(newGroup, toContainerWithIdentifier: nil)
|
|
218
|
+
try store.execute(saveRequest)
|
|
219
|
+
|
|
220
|
+
let groups = try store.groups(matching: CNGroup.predicateForGroups(withIdentifiers: [newGroup.identifier]))
|
|
221
|
+
if let createdGroup = groups.first {
|
|
222
|
+
return PeopleUtils.mapToGroupData(createdGroup)
|
|
223
|
+
} else {
|
|
224
|
+
throw PeopleError.initFailed("Failed to retrieve created group")
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Deletes a group by its identifier.
|
|
230
|
+
*/
|
|
231
|
+
public func deleteGroup(groupId: String) throws {
|
|
232
|
+
let saveRequest = CNSaveRequest()
|
|
233
|
+
let groups = try store.groups(matching: CNGroup.predicateForGroups(withIdentifiers: [groupId]))
|
|
234
|
+
|
|
235
|
+
guard let groupToDelete = groups.first else {
|
|
236
|
+
throw PeopleError.notFound("Group not found")
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
saveRequest.delete(groupToDelete.mutableCopy() as! CNMutableGroup)
|
|
240
|
+
try store.execute(saveRequest)
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Adds contacts to a group.
|
|
245
|
+
*/
|
|
246
|
+
public func addPeopleToGroup(groupId: String, contactIds: [String]) throws {
|
|
247
|
+
let saveRequest = CNSaveRequest()
|
|
248
|
+
let groups = try store.groups(matching: CNGroup.predicateForGroups(withIdentifiers: [groupId]))
|
|
249
|
+
|
|
250
|
+
guard let group = groups.first else {
|
|
251
|
+
throw PeopleError.unavailable("Group not found")
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
let contacts = try store.unifiedContacts(matching: CNContact.predicateForContacts(withIdentifiers: contactIds), keysToFetch: [])
|
|
255
|
+
for contact in contacts {
|
|
256
|
+
saveRequest.addMember(contact, to: group)
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
try store.execute(saveRequest)
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Removes contacts from a group.
|
|
264
|
+
*/
|
|
265
|
+
public func removePeopleFromGroup(groupId: String, contactIds: [String]) throws {
|
|
266
|
+
let saveRequest = CNSaveRequest()
|
|
267
|
+
let groups = try store.groups(matching: CNGroup.predicateForGroups(withIdentifiers: [groupId]))
|
|
268
|
+
|
|
269
|
+
guard let group = groups.first else {
|
|
270
|
+
throw PeopleError.unavailable("Group not found")
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
let contacts = try store.unifiedContacts(matching: CNContact.predicateForContacts(withIdentifiers: contactIds), keysToFetch: [])
|
|
274
|
+
for contact in contacts {
|
|
275
|
+
saveRequest.removeMember(contact, from: group)
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
try store.execute(saveRequest)
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// MARK: - CRUD
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Creates a new contact and returns a nominal model.
|
|
285
|
+
*/
|
|
286
|
+
public func createContact(contactData: [String: Any]) throws -> ContactData {
|
|
287
|
+
let saveRequest = CNSaveRequest()
|
|
288
|
+
let newContact = CNMutableContact()
|
|
289
|
+
|
|
290
|
+
// Name mapping
|
|
291
|
+
if let nameDict = contactData["name"] as? [String: String] {
|
|
292
|
+
newContact.givenName = nameDict["given"] ?? ""
|
|
293
|
+
newContact.familyName = nameDict["family"] ?? ""
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// Phones mapping
|
|
297
|
+
if let phonesArray = contactData["phones"] as? [[String: String]] {
|
|
298
|
+
newContact.phoneNumbers = phonesArray.map { phoneDict in
|
|
299
|
+
let number = CNPhoneNumber(stringValue: phoneDict["number"] ?? "")
|
|
300
|
+
let label = phoneDict["label"] ?? CNLabelPhoneNumberMobile
|
|
301
|
+
return CNLabeledValue(label: label, value: number)
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// Emails mapping
|
|
306
|
+
if let emailsArray = contactData["emails"] as? [[String: String]] {
|
|
307
|
+
newContact.emailAddresses = emailsArray.map { emailDict in
|
|
308
|
+
let address = (emailDict["address"] ?? "") as NSString
|
|
309
|
+
let label = emailDict["label"] ?? CNLabelWork
|
|
310
|
+
return CNLabeledValue(label: label, value: address)
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
newContact.note = "[cap-owned]"
|
|
315
|
+
saveRequest.add(newContact, toContainerWithIdentifier: nil)
|
|
316
|
+
try store.execute(saveRequest)
|
|
317
|
+
|
|
318
|
+
return PeopleUtils.mapToContactData(newContact, projection: ["name", "phones", "emails"])
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Deletes a contact by its identifier.
|
|
323
|
+
*/
|
|
324
|
+
public func deleteContact(contactId: String) throws {
|
|
325
|
+
let saveRequest = CNSaveRequest()
|
|
326
|
+
let keysToFetch = [CNContactIdentifierKey as CNKeyDescriptor, CNContactNoteKey as CNKeyDescriptor]
|
|
327
|
+
let contact = try store.unifiedContact(withIdentifier: contactId, keysToFetch: keysToFetch)
|
|
328
|
+
|
|
329
|
+
guard isAppOwned(contact: contact) else {
|
|
330
|
+
throw PeopleError.permissionDenied("Cannot modify a contact that is not owned by the app.")
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
saveRequest.delete(contact.mutableCopy() as! CNMutableContact)
|
|
334
|
+
try store.execute(saveRequest)
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Updates an existing contact and returns the updated nominal model.
|
|
339
|
+
*/
|
|
340
|
+
public func updateContact(contactId: String, contactData: [String: Any]) throws -> ContactData {
|
|
341
|
+
let saveRequest = CNSaveRequest()
|
|
342
|
+
let keysToFetch = [
|
|
343
|
+
CNContactIdentifierKey as CNKeyDescriptor,
|
|
344
|
+
CNContactNoteKey as CNKeyDescriptor,
|
|
345
|
+
CNContactGivenNameKey as CNKeyDescriptor,
|
|
346
|
+
CNContactFamilyNameKey as CNKeyDescriptor,
|
|
347
|
+
CNContactPhoneNumbersKey as CNKeyDescriptor,
|
|
348
|
+
CNContactEmailAddressesKey as CNKeyDescriptor,
|
|
349
|
+
CNContactPostalAddressesKey as CNKeyDescriptor,
|
|
350
|
+
CNContactUrlAddressesKey as CNKeyDescriptor
|
|
351
|
+
]
|
|
352
|
+
let contact = try store.unifiedContact(withIdentifier: contactId, keysToFetch: keysToFetch)
|
|
353
|
+
|
|
354
|
+
guard isAppOwned(contact: contact) else {
|
|
355
|
+
throw PeopleError.permissionDenied("Cannot update a contact that is not owned by the app.")
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
let mutableContact = contact.mutableCopy() as! CNMutableContact
|
|
359
|
+
|
|
360
|
+
// Patch semantics for name:
|
|
361
|
+
// - absent field: keep current value
|
|
362
|
+
// - present subfield: update that subfield only
|
|
363
|
+
if let nameDict = contactData["name"] as? [String: Any] {
|
|
364
|
+
if let given = nameDict["given"] as? String {
|
|
365
|
+
mutableContact.givenName = given
|
|
366
|
+
}
|
|
367
|
+
if let family = nameDict["family"] as? String {
|
|
368
|
+
mutableContact.familyName = family
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// Patch semantics for arrays:
|
|
373
|
+
// - absent key: keep current array
|
|
374
|
+
// - present key with []: clear array
|
|
375
|
+
// - present key with values: replace with provided values
|
|
376
|
+
if contactData.keys.contains("phones"),
|
|
377
|
+
let phonesArray = contactData["phones"] as? [[String: String]] {
|
|
378
|
+
mutableContact.phoneNumbers = phonesArray.map { phoneDict in
|
|
379
|
+
let number = CNPhoneNumber(stringValue: phoneDict["number"] ?? "")
|
|
380
|
+
let label = phoneDict["label"] ?? CNLabelPhoneNumberMobile
|
|
381
|
+
return CNLabeledValue(label: label, value: number)
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
if contactData.keys.contains("emails"),
|
|
386
|
+
let emailsArray = contactData["emails"] as? [[String: String]] {
|
|
387
|
+
mutableContact.emailAddresses = emailsArray.map { emailDict in
|
|
388
|
+
let address = (emailDict["address"] ?? "") as NSString
|
|
389
|
+
let label = emailDict["label"] ?? CNLabelWork
|
|
390
|
+
return CNLabeledValue(label: label, value: address)
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
if contactData.keys.contains("addresses"),
|
|
395
|
+
let addressesArray = contactData["addresses"] as? [[String: String]] {
|
|
396
|
+
mutableContact.postalAddresses = addressesArray.map { addressDict in
|
|
397
|
+
let postal = CNMutablePostalAddress()
|
|
398
|
+
postal.street = addressDict["street"] ?? ""
|
|
399
|
+
postal.city = addressDict["city"] ?? ""
|
|
400
|
+
postal.state = addressDict["region"] ?? ""
|
|
401
|
+
postal.postalCode = addressDict["postcode"] ?? ""
|
|
402
|
+
postal.country = addressDict["country"] ?? ""
|
|
403
|
+
let label = addressDict["label"] ?? CNLabelHome
|
|
404
|
+
return CNLabeledValue(label: label, value: postal)
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
if contactData.keys.contains("urls"),
|
|
409
|
+
let urlsArray = contactData["urls"] as? [String] {
|
|
410
|
+
mutableContact.urlAddresses = urlsArray.map { url in
|
|
411
|
+
CNLabeledValue(label: CNLabelURLAddressHomePage, value: url as NSString)
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
saveRequest.update(mutableContact)
|
|
416
|
+
try store.execute(saveRequest)
|
|
417
|
+
|
|
418
|
+
return PeopleUtils.mapToContactData(mutableContact, projection: ["name", "phones", "emails"])
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
/**
|
|
422
|
+
* Merges two contacts and returns the final nominal model with deduplicated data.
|
|
423
|
+
*/
|
|
424
|
+
public func mergeContacts(sourceContactId: String, destinationContactId: String) throws -> ContactData {
|
|
425
|
+
let saveRequest = CNSaveRequest()
|
|
426
|
+
let keysToFetch = [
|
|
427
|
+
CNContactIdentifierKey as CNKeyDescriptor,
|
|
428
|
+
CNContactNoteKey as CNKeyDescriptor,
|
|
429
|
+
CNContactPhoneNumbersKey as CNKeyDescriptor,
|
|
430
|
+
CNContactEmailAddressesKey as CNKeyDescriptor
|
|
431
|
+
]
|
|
432
|
+
|
|
433
|
+
let sourceContact = try store.unifiedContact(withIdentifier: sourceContactId, keysToFetch: keysToFetch)
|
|
434
|
+
let destinationContact = try store.unifiedContact(withIdentifier: destinationContactId, keysToFetch: keysToFetch)
|
|
435
|
+
|
|
436
|
+
guard isAppOwned(contact: sourceContact), isAppOwned(contact: destinationContact) else {
|
|
437
|
+
throw PeopleError.permissionDenied("Cannot merge contacts that are not owned by the app.")
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
let mutableDestinationContact = destinationContact.mutableCopy() as! CNMutableContact
|
|
441
|
+
|
|
442
|
+
// Deduplicate Phone Numbers by string value
|
|
443
|
+
let existingPhones = destinationContact.phoneNumbers.map { $0.value.stringValue }
|
|
444
|
+
let newPhones = sourceContact.phoneNumbers.filter { !existingPhones.contains($0.value.stringValue) }
|
|
445
|
+
mutableDestinationContact.phoneNumbers = destinationContact.phoneNumbers + newPhones
|
|
446
|
+
|
|
447
|
+
// Deduplicate Email Addresses
|
|
448
|
+
let existingEmails = destinationContact.emailAddresses.map { $0.value as String }
|
|
449
|
+
let newEmails = sourceContact.emailAddresses.filter { !existingEmails.contains($0.value as String) }
|
|
450
|
+
mutableDestinationContact.emailAddresses = destinationContact.emailAddresses + newEmails
|
|
451
|
+
|
|
452
|
+
saveRequest.update(mutableDestinationContact)
|
|
453
|
+
saveRequest.delete(sourceContact.mutableCopy() as! CNMutableContact)
|
|
454
|
+
|
|
455
|
+
try store.execute(saveRequest)
|
|
456
|
+
|
|
457
|
+
return PeopleUtils.mapToContactData(mutableDestinationContact, projection: ["name", "phones", "emails"])
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
private func isAppOwned(contact: CNContact) -> Bool {
|
|
461
|
+
return contact.note.contains("[cap-owned]")
|
|
462
|
+
}
|
|
463
|
+
}
|