@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,766 @@
|
|
|
1
|
+
package io.capkit.people
|
|
2
|
+
|
|
3
|
+
import android.Manifest
|
|
4
|
+
import android.app.Activity
|
|
5
|
+
import android.content.Intent
|
|
6
|
+
import android.net.Uri
|
|
7
|
+
import android.provider.ContactsContract
|
|
8
|
+
import androidx.activity.result.ActivityResult
|
|
9
|
+
import com.getcapacitor.JSArray
|
|
10
|
+
import com.getcapacitor.JSObject
|
|
11
|
+
import com.getcapacitor.PermissionState
|
|
12
|
+
import com.getcapacitor.Plugin
|
|
13
|
+
import com.getcapacitor.PluginCall
|
|
14
|
+
import com.getcapacitor.PluginMethod
|
|
15
|
+
import com.getcapacitor.annotation.ActivityCallback
|
|
16
|
+
import com.getcapacitor.annotation.CapacitorPlugin
|
|
17
|
+
import com.getcapacitor.annotation.Permission
|
|
18
|
+
import com.getcapacitor.annotation.PermissionCallback
|
|
19
|
+
import io.capkit.people.config.PeopleConfig
|
|
20
|
+
import io.capkit.people.error.PeopleError
|
|
21
|
+
import io.capkit.people.error.PeopleErrorMessages
|
|
22
|
+
import io.capkit.people.logger.PeopleLogger
|
|
23
|
+
import io.capkit.people.utils.PeopleUtils
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Capacitor bridge for the People plugin.
|
|
27
|
+
*
|
|
28
|
+
* This class acts as the boundary between JavaScript and native Android code.
|
|
29
|
+
* It handles input parsing, configuration management, and delegates execution
|
|
30
|
+
* to the platform-specific implementation.
|
|
31
|
+
*/
|
|
32
|
+
@CapacitorPlugin(
|
|
33
|
+
name = "People",
|
|
34
|
+
permissions = [
|
|
35
|
+
Permission(
|
|
36
|
+
strings = [Manifest.permission.READ_CONTACTS, Manifest.permission.WRITE_CONTACTS],
|
|
37
|
+
alias = "contacts",
|
|
38
|
+
),
|
|
39
|
+
],
|
|
40
|
+
)
|
|
41
|
+
class PeoplePlugin : Plugin() {
|
|
42
|
+
// -----------------------------------------------------------------------------
|
|
43
|
+
// Properties
|
|
44
|
+
// -----------------------------------------------------------------------------
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Immutable plugin configuration read from capacitor.config.ts.
|
|
48
|
+
* * CONTRACT:
|
|
49
|
+
* - Initialized exactly once in `load()`.
|
|
50
|
+
* - Treated as read-only afterwards.
|
|
51
|
+
*/
|
|
52
|
+
private lateinit var config: PeopleConfig
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Native implementation layer containing core Android logic.
|
|
56
|
+
*
|
|
57
|
+
* CONTRACT:
|
|
58
|
+
* - Owned by the Plugin layer.
|
|
59
|
+
* - MUST NOT access PluginCall or Capacitor bridge APIs directly.
|
|
60
|
+
*/
|
|
61
|
+
private lateinit var implementation: PeopleImpl
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Observer state for monitoring system-wide contact changes.
|
|
65
|
+
*/
|
|
66
|
+
private var observer: PeopleObserver? = null
|
|
67
|
+
|
|
68
|
+
// -----------------------------------------------------------------------------
|
|
69
|
+
// Lifecycle
|
|
70
|
+
// -----------------------------------------------------------------------------
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Called once when the plugin is loaded by the Capacitor bridge.
|
|
74
|
+
*
|
|
75
|
+
* This method initializes the configuration container and the native
|
|
76
|
+
* implementation layer, ensuring all dependencies are injected.
|
|
77
|
+
*/
|
|
78
|
+
override fun load() {
|
|
79
|
+
super.load()
|
|
80
|
+
|
|
81
|
+
config = PeopleConfig(this)
|
|
82
|
+
implementation = PeopleImpl(context)
|
|
83
|
+
implementation.updateConfig(config)
|
|
84
|
+
|
|
85
|
+
PeopleLogger.verbose = config.verboseLogging
|
|
86
|
+
PeopleLogger.debug("Plugin loaded")
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Called when the plugin is being destroyed.
|
|
91
|
+
* Ensures all native resources are released.
|
|
92
|
+
*/
|
|
93
|
+
override fun handleOnDestroy() {
|
|
94
|
+
cleanupObserver()
|
|
95
|
+
super.handleOnDestroy()
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// -----------------------------------------------------------------------------
|
|
99
|
+
// Internal Helpers & Denial-Path Handling
|
|
100
|
+
// -----------------------------------------------------------------------------
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Helper to validate 'contacts' permission alias.
|
|
104
|
+
* Returns true if granted, otherwise rejects the call and returns false.
|
|
105
|
+
*/
|
|
106
|
+
private fun checkContactsPermission(call: PluginCall): Boolean {
|
|
107
|
+
if (getPermissionState("contacts") != PermissionState.GRANTED) {
|
|
108
|
+
reject(call, PeopleError.PermissionDenied(PeopleErrorMessages.PERMISSION_DENIED))
|
|
109
|
+
return false
|
|
110
|
+
}
|
|
111
|
+
return true
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Rejects the call with a message and a standardized error code.
|
|
116
|
+
* Ensure consistency with the JS PeopleErrorCode enum.
|
|
117
|
+
*/
|
|
118
|
+
private fun reject(
|
|
119
|
+
call: PluginCall,
|
|
120
|
+
error: PeopleError,
|
|
121
|
+
) {
|
|
122
|
+
val code =
|
|
123
|
+
when (error) {
|
|
124
|
+
is PeopleError.Unavailable -> "UNAVAILABLE"
|
|
125
|
+
is PeopleError.Cancelled -> "CANCELLED"
|
|
126
|
+
is PeopleError.PermissionDenied -> "PERMISSION_DENIED"
|
|
127
|
+
is PeopleError.InitFailed -> "INIT_FAILED"
|
|
128
|
+
is PeopleError.InvalidInput -> "INVALID_INPUT"
|
|
129
|
+
is PeopleError.UnknownType -> "UNKNOWN_TYPE"
|
|
130
|
+
is PeopleError.NotFound -> "NOT_FOUND"
|
|
131
|
+
is PeopleError.Conflict -> "CONFLICT"
|
|
132
|
+
is PeopleError.Timeout -> "TIMEOUT"
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Always use the message from the PeopleError instance
|
|
136
|
+
val message = error.message ?: "Unknown native error"
|
|
137
|
+
call.reject(message, code)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
private fun cleanupObserver() {
|
|
141
|
+
observer?.let {
|
|
142
|
+
try {
|
|
143
|
+
it.dispose()
|
|
144
|
+
context.contentResolver.unregisterContentObserver(it)
|
|
145
|
+
} catch (e: Exception) {
|
|
146
|
+
PeopleLogger.error("Error unregistering observer", e)
|
|
147
|
+
}
|
|
148
|
+
observer = null
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
private fun hasWritableCreateContactField(contactData: JSObject): Boolean {
|
|
153
|
+
val name = contactData.optJSONObject("name")
|
|
154
|
+
if (name != null && name.length() > 0) return true
|
|
155
|
+
|
|
156
|
+
val organization = contactData.optJSONObject("organization")
|
|
157
|
+
if (organization != null && organization.length() > 0) return true
|
|
158
|
+
|
|
159
|
+
val birthday = contactData.optJSONObject("birthday")
|
|
160
|
+
if (birthday != null && birthday.length() > 0) return true
|
|
161
|
+
|
|
162
|
+
val phones = contactData.optJSONArray("phones")
|
|
163
|
+
if (phones != null && phones.length() > 0) return true
|
|
164
|
+
|
|
165
|
+
val emails = contactData.optJSONArray("emails")
|
|
166
|
+
if (emails != null && emails.length() > 0) return true
|
|
167
|
+
|
|
168
|
+
val addresses = contactData.optJSONArray("addresses")
|
|
169
|
+
if (addresses != null && addresses.length() > 0) return true
|
|
170
|
+
|
|
171
|
+
val urls = contactData.optJSONArray("urls")
|
|
172
|
+
if (urls != null && urls.length() > 0) return true
|
|
173
|
+
|
|
174
|
+
if (contactData.optString("note", "").isNotBlank()) return true
|
|
175
|
+
if (contactData.optString("image", "").isNotBlank()) return true
|
|
176
|
+
|
|
177
|
+
return false
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// -----------------------------------------------------------------------------
|
|
181
|
+
// Zero-Permission Picker
|
|
182
|
+
// -----------------------------------------------------------------------------
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Launches the native contact picker (Zero-Permission).
|
|
186
|
+
* This allows the user to select a single contact without granting full address book access.
|
|
187
|
+
*/
|
|
188
|
+
@PluginMethod
|
|
189
|
+
fun pickContact(call: PluginCall) {
|
|
190
|
+
// 1. Prepare the intent
|
|
191
|
+
val intent = Intent(Intent.ACTION_PICK, ContactsContract.Contacts.CONTENT_URI)
|
|
192
|
+
|
|
193
|
+
// Persist the call before launching an external Activity (Capacitor v8 rule).
|
|
194
|
+
bridge.saveCall(call)
|
|
195
|
+
|
|
196
|
+
// 2. Start Activity using Capacitor Bridge
|
|
197
|
+
// Note: We use a specific callback name defined below
|
|
198
|
+
startActivityForResult(call, intent, "handlePickContactResult")
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Processes the result from the system Contact Picker activity.
|
|
203
|
+
*/
|
|
204
|
+
@ActivityCallback
|
|
205
|
+
private fun handlePickContactResult(
|
|
206
|
+
call: PluginCall,
|
|
207
|
+
result: ActivityResult,
|
|
208
|
+
) {
|
|
209
|
+
// Retrieve the persisted call (Capacitor v8 rule).
|
|
210
|
+
val savedCall = bridge.getSavedCall(call.callbackId) ?: call
|
|
211
|
+
try {
|
|
212
|
+
if (result.resultCode == Activity.RESULT_OK && result.data != null) {
|
|
213
|
+
val contactUri: Uri? = result.data?.data
|
|
214
|
+
if (contactUri != null) {
|
|
215
|
+
try {
|
|
216
|
+
val projectionArray = savedCall.getArray("projection")
|
|
217
|
+
val projection =
|
|
218
|
+
if (projectionArray != null && projectionArray.length() > 0) {
|
|
219
|
+
List(projectionArray.length()) { i -> projectionArray.getString(i) }
|
|
220
|
+
} else {
|
|
221
|
+
// Default projection: basic contact info
|
|
222
|
+
listOf("name", "phones", "emails")
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
val contact = implementation.getContactFromUri(contactUri, projection)
|
|
226
|
+
if (contact != null) {
|
|
227
|
+
val ret = JSObject()
|
|
228
|
+
ret.put("contact", contactToJS(contact))
|
|
229
|
+
savedCall.resolve(ret)
|
|
230
|
+
} else {
|
|
231
|
+
reject(savedCall, PeopleError.NotFound(PeopleErrorMessages.CONTACT_NOT_FOUND))
|
|
232
|
+
}
|
|
233
|
+
} catch (e: Exception) {
|
|
234
|
+
PeopleLogger.error("Error processing picked contact", e)
|
|
235
|
+
reject(savedCall, PeopleError.InitFailed(PeopleErrorMessages.ERROR_PROCESSING_CONTACT))
|
|
236
|
+
}
|
|
237
|
+
} else {
|
|
238
|
+
reject(savedCall, PeopleError.InitFailed(PeopleErrorMessages.NO_CONTACT_URI_RETURNED))
|
|
239
|
+
}
|
|
240
|
+
} else {
|
|
241
|
+
// User cancellation should be mapped explicitly.
|
|
242
|
+
reject(savedCall, PeopleError.Cancelled(PeopleErrorMessages.USER_CANCELLED_SELECTION))
|
|
243
|
+
}
|
|
244
|
+
} finally {
|
|
245
|
+
bridge.releaseCall(savedCall)
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// -----------------------------------------------------------------------------
|
|
250
|
+
// Event Listeners
|
|
251
|
+
// -----------------------------------------------------------------------------
|
|
252
|
+
|
|
253
|
+
@PluginMethod
|
|
254
|
+
override fun addListener(call: PluginCall) {
|
|
255
|
+
val eventName = call.getString("eventName")
|
|
256
|
+
|
|
257
|
+
if (eventName.isNullOrBlank()) {
|
|
258
|
+
return reject(call, PeopleError.InvalidInput(PeopleErrorMessages.EVENT_NAME_REQUIRED))
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
if (eventName != "peopleChange") {
|
|
262
|
+
return reject(call, PeopleError.InvalidInput(PeopleErrorMessages.unsupportedEventName(eventName)))
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
if (!checkContactsPermission(call)) return
|
|
266
|
+
|
|
267
|
+
call.setKeepAlive(true)
|
|
268
|
+
super.addListener(call)
|
|
269
|
+
|
|
270
|
+
if (observer == null) {
|
|
271
|
+
observer =
|
|
272
|
+
PeopleObserver { _, ids ->
|
|
273
|
+
// Ensure a consistent payload for "peopleChange" listeners.
|
|
274
|
+
// Always include "ids" as an array (may be empty) and a "type" string with default "update".
|
|
275
|
+
val payload = JSObject()
|
|
276
|
+
val changedIds: List<String> = ids ?: emptyList()
|
|
277
|
+
val idsArray = JSArray()
|
|
278
|
+
for (id in changedIds) {
|
|
279
|
+
idsArray.put(id)
|
|
280
|
+
}
|
|
281
|
+
payload.put("ids", idsArray)
|
|
282
|
+
|
|
283
|
+
payload.put("type", "update")
|
|
284
|
+
|
|
285
|
+
if (hasListeners("peopleChange")) {
|
|
286
|
+
notifyListeners("peopleChange", payload)
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
context.contentResolver.registerContentObserver(PeopleObserver.CONTACTS_URI, true, observer!!)
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
@PluginMethod
|
|
295
|
+
override fun removeListener(call: PluginCall) {
|
|
296
|
+
super.removeListener(call)
|
|
297
|
+
if (!hasListeners("peopleChange")) {
|
|
298
|
+
cleanupObserver()
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
@PluginMethod
|
|
303
|
+
override fun removeAllListeners(call: PluginCall) {
|
|
304
|
+
cleanupObserver()
|
|
305
|
+
super.removeAllListeners(call)
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// -----------------------------------------------------------------------------
|
|
309
|
+
// Systemic Access
|
|
310
|
+
// -----------------------------------------------------------------------------
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Retrieves a list of contacts from the device address book.
|
|
314
|
+
* Requires the 'contacts' permission to be granted.
|
|
315
|
+
*
|
|
316
|
+
* @param call PluginCall containing 'limit', 'offset', and 'projection'.
|
|
317
|
+
*/
|
|
318
|
+
@PluginMethod
|
|
319
|
+
fun getContacts(call: PluginCall) {
|
|
320
|
+
if (!checkContactsPermission(call)) return
|
|
321
|
+
|
|
322
|
+
// Ensure a safe default projection if none is provided to avoid empty results
|
|
323
|
+
val projectionArray = call.getArray("projection")
|
|
324
|
+
val projection =
|
|
325
|
+
if (projectionArray != null && projectionArray.length() > 0) {
|
|
326
|
+
List(projectionArray.length()) { i -> projectionArray.getString(i) }
|
|
327
|
+
} else {
|
|
328
|
+
// Default projection: basic contact info
|
|
329
|
+
listOf("name", "phones", "emails")
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
try {
|
|
333
|
+
PeopleUtils.validateProjection(projection)
|
|
334
|
+
} catch (e: PeopleError) {
|
|
335
|
+
return reject(call, e)
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
val limit = call.getInt("limit") ?: 50
|
|
339
|
+
val offset = call.getInt("offset") ?: 0
|
|
340
|
+
|
|
341
|
+
// Execute implementation logic (runs on plugin background thread)
|
|
342
|
+
val (contacts, total) =
|
|
343
|
+
implementation.getContacts(
|
|
344
|
+
projection,
|
|
345
|
+
limit,
|
|
346
|
+
offset,
|
|
347
|
+
)
|
|
348
|
+
|
|
349
|
+
val ret = JSObject()
|
|
350
|
+
ret.put("contacts", contactsToJSArray(contacts))
|
|
351
|
+
ret.put("totalCount", total)
|
|
352
|
+
|
|
353
|
+
call.resolve(ret)
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* Retrieves a single contact by its ID.
|
|
358
|
+
* Requires the 'contacts' permission to be granted.
|
|
359
|
+
*
|
|
360
|
+
* @param call PluginCall containing the 'id' of the contact.
|
|
361
|
+
*/
|
|
362
|
+
@PluginMethod
|
|
363
|
+
fun getContact(call: PluginCall) {
|
|
364
|
+
if (!checkContactsPermission(call)) return
|
|
365
|
+
|
|
366
|
+
val id = call.getString("id") ?: return reject(call, PeopleError.InvalidInput(PeopleErrorMessages.ID_REQUIRED))
|
|
367
|
+
|
|
368
|
+
// Apply safe default projection for single contact retrieval
|
|
369
|
+
val projectionArray = call.getArray("projection")
|
|
370
|
+
val projection =
|
|
371
|
+
if (projectionArray != null && projectionArray.length() > 0) {
|
|
372
|
+
List(projectionArray.length()) { i -> projectionArray.getString(i) }
|
|
373
|
+
} else {
|
|
374
|
+
// Default projection: basic contact info
|
|
375
|
+
listOf("name", "phones", "emails")
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
try {
|
|
379
|
+
PeopleUtils.validateProjection(projection)
|
|
380
|
+
} catch (e: PeopleError) {
|
|
381
|
+
return reject(call, e)
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
val contact = implementation.getContactById(id, projection)
|
|
385
|
+
if (contact == null) return reject(call, PeopleError.NotFound(PeopleErrorMessages.CONTACT_NOT_FOUND))
|
|
386
|
+
|
|
387
|
+
val ret = JSObject()
|
|
388
|
+
ret.put("contact", contactToJS(contact))
|
|
389
|
+
call.resolve(ret)
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* Searches for contacts based on a query string.
|
|
394
|
+
* Requires the 'contacts' permission to be granted.
|
|
395
|
+
*
|
|
396
|
+
* @param call PluginCall containing 'query', 'projection', and 'limit'.
|
|
397
|
+
*/
|
|
398
|
+
@PluginMethod
|
|
399
|
+
fun searchPeople(call: PluginCall) {
|
|
400
|
+
if (!checkContactsPermission(call)) return
|
|
401
|
+
|
|
402
|
+
val query =
|
|
403
|
+
call.getString("query") ?: return reject(call, PeopleError.InvalidInput(PeopleErrorMessages.QUERY_REQUIRED))
|
|
404
|
+
// Apply safe default projection for search results
|
|
405
|
+
val projectionArray = call.getArray("projection")
|
|
406
|
+
val projection =
|
|
407
|
+
if (projectionArray != null && projectionArray.length() > 0) {
|
|
408
|
+
List(projectionArray.length()) { i -> projectionArray.getString(i) }
|
|
409
|
+
} else {
|
|
410
|
+
// Default projection: basic contact info
|
|
411
|
+
listOf("name", "phones", "emails")
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
try {
|
|
415
|
+
PeopleUtils.validateProjection(projection)
|
|
416
|
+
} catch (e: PeopleError) {
|
|
417
|
+
return reject(call, e)
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
val limit = call.getInt("limit") ?: 50
|
|
421
|
+
|
|
422
|
+
// Execute implementation logic
|
|
423
|
+
val (contacts, total) =
|
|
424
|
+
implementation.searchContacts(
|
|
425
|
+
query,
|
|
426
|
+
projection,
|
|
427
|
+
limit,
|
|
428
|
+
)
|
|
429
|
+
|
|
430
|
+
val ret = JSObject()
|
|
431
|
+
ret.put("contacts", contactsToJSArray(contacts))
|
|
432
|
+
ret.put("totalCount", total)
|
|
433
|
+
|
|
434
|
+
call.resolve(ret)
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// -----------------------------------------------------------------------------
|
|
438
|
+
// CRUD & Group Management (Denied-path protected)
|
|
439
|
+
// -----------------------------------------------------------------------------
|
|
440
|
+
|
|
441
|
+
@PluginMethod
|
|
442
|
+
fun listGroups(call: PluginCall) {
|
|
443
|
+
if (!checkContactsPermission(call)) return
|
|
444
|
+
|
|
445
|
+
try {
|
|
446
|
+
val groups = implementation.listGroups()
|
|
447
|
+
val groupsArray = JSArray()
|
|
448
|
+
for (group in groups) {
|
|
449
|
+
groupsArray.put(groupToJS(group))
|
|
450
|
+
}
|
|
451
|
+
val ret = JSObject()
|
|
452
|
+
ret.put("groups", groupsArray)
|
|
453
|
+
call.resolve(ret)
|
|
454
|
+
} catch (e: PeopleError) {
|
|
455
|
+
reject(call, e)
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
@PluginMethod
|
|
460
|
+
fun createGroup(call: PluginCall) {
|
|
461
|
+
if (!checkContactsPermission(call)) return
|
|
462
|
+
|
|
463
|
+
val name =
|
|
464
|
+
call.getString("name") ?: return reject(call, PeopleError.InvalidInput(PeopleErrorMessages.GROUP_NAME_REQUIRED))
|
|
465
|
+
|
|
466
|
+
try {
|
|
467
|
+
val group = implementation.createGroup(name)
|
|
468
|
+
val ret = JSObject()
|
|
469
|
+
ret.put("group", groupToJS(group))
|
|
470
|
+
call.resolve(ret)
|
|
471
|
+
} catch (e: PeopleError) {
|
|
472
|
+
reject(call, e)
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
@PluginMethod
|
|
477
|
+
fun deleteGroup(call: PluginCall) {
|
|
478
|
+
if (!checkContactsPermission(call)) return
|
|
479
|
+
|
|
480
|
+
val groupId =
|
|
481
|
+
call.getString("groupId") ?: return reject(call, PeopleError.InvalidInput(PeopleErrorMessages.GROUP_ID_REQUIRED))
|
|
482
|
+
|
|
483
|
+
try {
|
|
484
|
+
implementation.deleteGroup(groupId)
|
|
485
|
+
call.resolve()
|
|
486
|
+
} catch (e: PeopleError) {
|
|
487
|
+
reject(call, e)
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
@PluginMethod
|
|
492
|
+
fun addPeopleToGroup(call: PluginCall) {
|
|
493
|
+
if (!checkContactsPermission(call)) return
|
|
494
|
+
|
|
495
|
+
val groupId =
|
|
496
|
+
call.getString("groupId") ?: return reject(call, PeopleError.InvalidInput(PeopleErrorMessages.GROUP_ID_REQUIRED))
|
|
497
|
+
val contactIdsArray =
|
|
498
|
+
call.getArray("contactIds")
|
|
499
|
+
?: return reject(call, PeopleError.InvalidInput(PeopleErrorMessages.CONTACT_IDS_REQUIRED))
|
|
500
|
+
|
|
501
|
+
val contactIds = List(contactIdsArray.length()) { i -> contactIdsArray.getString(i) }
|
|
502
|
+
|
|
503
|
+
try {
|
|
504
|
+
implementation.addPeopleToGroup(groupId, contactIds)
|
|
505
|
+
call.resolve()
|
|
506
|
+
} catch (e: PeopleError) {
|
|
507
|
+
reject(call, e)
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
@PluginMethod
|
|
512
|
+
fun removePeopleFromGroup(call: PluginCall) {
|
|
513
|
+
if (!checkContactsPermission(call)) return
|
|
514
|
+
|
|
515
|
+
val groupId =
|
|
516
|
+
call.getString("groupId") ?: return reject(call, PeopleError.InvalidInput(PeopleErrorMessages.GROUP_ID_REQUIRED))
|
|
517
|
+
val contactIdsArray =
|
|
518
|
+
call.getArray("contactIds")
|
|
519
|
+
?: return reject(call, PeopleError.InvalidInput(PeopleErrorMessages.CONTACT_IDS_REQUIRED))
|
|
520
|
+
|
|
521
|
+
val contactIds = List(contactIdsArray.length()) { i -> contactIdsArray.getString(i) }
|
|
522
|
+
|
|
523
|
+
try {
|
|
524
|
+
implementation.removePeopleFromGroup(groupId, contactIds)
|
|
525
|
+
call.resolve()
|
|
526
|
+
} catch (e: PeopleError) {
|
|
527
|
+
reject(call, e)
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
@PluginMethod
|
|
532
|
+
fun createContact(call: PluginCall) {
|
|
533
|
+
if (!checkContactsPermission(call)) return
|
|
534
|
+
|
|
535
|
+
val contactJS =
|
|
536
|
+
call.getObject("contact")
|
|
537
|
+
?: return reject(call, PeopleError.InvalidInput(PeopleErrorMessages.CONTACT_DATA_REQUIRED))
|
|
538
|
+
if (!hasWritableCreateContactField(contactJS)) {
|
|
539
|
+
return reject(call, PeopleError.InvalidInput(PeopleErrorMessages.AT_LEAST_ONE_WRITABLE_FIELD_REQUIRED))
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
// Marshalling: Safe data extraction using Capacitor v8 typed getters
|
|
543
|
+
val nameObj = contactJS.getJSObject("name")
|
|
544
|
+
val givenName = nameObj?.getString("given")
|
|
545
|
+
val familyName = nameObj?.getString("family")
|
|
546
|
+
|
|
547
|
+
// Marshalling: Extract JSONArrays using the standard getJSONArray method
|
|
548
|
+
val phones = PeopleUtils.extractStringList(contactJS.getJSONArray("phones"), "number")
|
|
549
|
+
|
|
550
|
+
// Delegate extraction to updated Utils helper
|
|
551
|
+
val emails = PeopleUtils.extractStringList(contactJS.getJSONArray("emails"), "address")
|
|
552
|
+
|
|
553
|
+
try {
|
|
554
|
+
val contact = implementation.createContact(givenName, familyName, phones, emails)
|
|
555
|
+
val ret = JSObject()
|
|
556
|
+
// Use Mapper to return JSObject to the Bridge
|
|
557
|
+
ret.put("contact", contactToJS(contact))
|
|
558
|
+
call.resolve(ret)
|
|
559
|
+
} catch (e: PeopleError) {
|
|
560
|
+
reject(call, e)
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
@PluginMethod
|
|
565
|
+
fun updateContact(call: PluginCall) {
|
|
566
|
+
if (!checkContactsPermission(call)) return
|
|
567
|
+
|
|
568
|
+
val contactId = call.getString("contactId")
|
|
569
|
+
val contactData = call.getObject("contact")
|
|
570
|
+
|
|
571
|
+
if (contactId.isNullOrEmpty() ||
|
|
572
|
+
contactData == null
|
|
573
|
+
) {
|
|
574
|
+
return reject(call, PeopleError.InvalidInput(PeopleErrorMessages.REQUIRED_FIELDS_MISSING))
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
// Marshalling: Extract only what the native Impl needs
|
|
578
|
+
val nameObj = contactData.getJSObject("name")
|
|
579
|
+
|
|
580
|
+
// Delegate to Impl using primitive types
|
|
581
|
+
try {
|
|
582
|
+
val updatedContact =
|
|
583
|
+
implementation.updateContact(
|
|
584
|
+
contactId,
|
|
585
|
+
nameObj?.getString("given"),
|
|
586
|
+
nameObj?.getString("family"),
|
|
587
|
+
)
|
|
588
|
+
val ret = JSObject()
|
|
589
|
+
// Map native model back to JSObject via Utils
|
|
590
|
+
ret.put("contact", contactToJS(updatedContact))
|
|
591
|
+
call.resolve(ret)
|
|
592
|
+
} catch (e: PeopleError) {
|
|
593
|
+
reject(call, e)
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
@PluginMethod
|
|
598
|
+
fun deleteContact(call: PluginCall) {
|
|
599
|
+
if (!checkContactsPermission(call)) return
|
|
600
|
+
|
|
601
|
+
val contactId =
|
|
602
|
+
call.getString("contactId")
|
|
603
|
+
?: return reject(call, PeopleError.InvalidInput(PeopleErrorMessages.CONTACT_ID_REQUIRED))
|
|
604
|
+
|
|
605
|
+
try {
|
|
606
|
+
implementation.deleteContact(contactId)
|
|
607
|
+
call.resolve()
|
|
608
|
+
} catch (e: PeopleError) {
|
|
609
|
+
reject(call, e)
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
@PluginMethod
|
|
614
|
+
fun mergeContacts(call: PluginCall) {
|
|
615
|
+
if (!checkContactsPermission(call)) return
|
|
616
|
+
|
|
617
|
+
val sourceId = call.getString("sourceContactId")
|
|
618
|
+
val destId = call.getString("destinationContactId")
|
|
619
|
+
|
|
620
|
+
if (sourceId.isNullOrEmpty() ||
|
|
621
|
+
destId.isNullOrEmpty()
|
|
622
|
+
) {
|
|
623
|
+
return reject(call, PeopleError.InvalidInput(PeopleErrorMessages.IDS_REQUIRED))
|
|
624
|
+
}
|
|
625
|
+
if (sourceId == destId) {
|
|
626
|
+
return reject(
|
|
627
|
+
call,
|
|
628
|
+
PeopleError.InvalidInput("sourceContactId and destinationContactId must be different"),
|
|
629
|
+
)
|
|
630
|
+
}
|
|
631
|
+
try {
|
|
632
|
+
val mergedContact = implementation.mergeContacts(sourceId, destId)
|
|
633
|
+
val ret = JSObject()
|
|
634
|
+
ret.put("contact", contactToJS(mergedContact))
|
|
635
|
+
call.resolve(ret)
|
|
636
|
+
} catch (e: PeopleError) {
|
|
637
|
+
reject(call, e)
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
@PluginMethod
|
|
642
|
+
override fun requestPermissions(call: PluginCall) {
|
|
643
|
+
if (getPermissionState("contacts") == PermissionState.GRANTED) {
|
|
644
|
+
checkPermissions(call)
|
|
645
|
+
return
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
// The callback must be a method annotated with @PermissionCallback
|
|
649
|
+
requestPermissionForAlias("contacts", call, "contactsPermissionsCallback")
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
@PermissionCallback
|
|
653
|
+
private fun contactsPermissionsCallback(call: PluginCall) {
|
|
654
|
+
// After the permission prompt, return the updated permission states
|
|
655
|
+
checkPermissions(call)
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
@PluginMethod
|
|
659
|
+
fun getCapabilities(call: PluginCall) {
|
|
660
|
+
val caps = implementation.getCapabilities(getPermissionState("contacts") == PermissionState.GRANTED)
|
|
661
|
+
call.resolve(JSObject.fromJSONObject(org.json.JSONObject(caps)))
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
// -----------------------------------------------------------------------------
|
|
665
|
+
// Version Information
|
|
666
|
+
// -----------------------------------------------------------------------------
|
|
667
|
+
|
|
668
|
+
/**
|
|
669
|
+
* Returns the native plugin version synchronized from package.json.
|
|
670
|
+
*
|
|
671
|
+
* This information is used for diagnostics and ensuring parity between
|
|
672
|
+
* the JavaScript and native layers.
|
|
673
|
+
*
|
|
674
|
+
* @param call The bridge call to resolve with version data.
|
|
675
|
+
*/
|
|
676
|
+
@PluginMethod
|
|
677
|
+
fun getPluginVersion(call: PluginCall) {
|
|
678
|
+
val ret = JSObject()
|
|
679
|
+
ret.put("version", BuildConfig.PLUGIN_VERSION)
|
|
680
|
+
call.resolve(ret)
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
// -----------------------------------------------------------------------------
|
|
684
|
+
// Bridge Marshalling (Native -> JS)
|
|
685
|
+
// -----------------------------------------------------------------------------
|
|
686
|
+
|
|
687
|
+
// Add these private helpers at the end of PeoplePlugin class
|
|
688
|
+
private fun contactToJS(contact: io.capkit.people.models.ContactData): JSObject {
|
|
689
|
+
val js = JSObject()
|
|
690
|
+
js.put("id", contact.id)
|
|
691
|
+
|
|
692
|
+
contact.displayName?.let {
|
|
693
|
+
val nameObj = JSObject()
|
|
694
|
+
nameObj.put("display", it)
|
|
695
|
+
js.put("name", nameObj)
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
if (contact.phones.isNotEmpty()) {
|
|
699
|
+
val phonesArr = JSArray()
|
|
700
|
+
for (phone in contact.phones) {
|
|
701
|
+
val p = JSObject()
|
|
702
|
+
p.put("number", phone.value)
|
|
703
|
+
p.put("label", phone.label)
|
|
704
|
+
phonesArr.put(p)
|
|
705
|
+
}
|
|
706
|
+
js.put("phones", phonesArr)
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
if (contact.emails.isNotEmpty()) {
|
|
710
|
+
val emailsArr = JSArray()
|
|
711
|
+
for (email in contact.emails) {
|
|
712
|
+
val e = JSObject()
|
|
713
|
+
e.put("address", email.value)
|
|
714
|
+
e.put("label", email.label)
|
|
715
|
+
emailsArr.put(e)
|
|
716
|
+
}
|
|
717
|
+
js.put("emails", emailsArr)
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
contact.organization?.let { org ->
|
|
721
|
+
val orgObj = JSObject()
|
|
722
|
+
orgObj.put("company", org.company)
|
|
723
|
+
orgObj.put("title", org.title)
|
|
724
|
+
orgObj.put("department", org.department)
|
|
725
|
+
js.put("organization", orgObj)
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
if (contact.addresses.isNotEmpty()) {
|
|
729
|
+
val addrArr = JSArray()
|
|
730
|
+
for (addr in contact.addresses) {
|
|
731
|
+
val a = JSObject()
|
|
732
|
+
a.put("label", addr.label)
|
|
733
|
+
a.put("street", addr.street)
|
|
734
|
+
a.put("city", addr.city)
|
|
735
|
+
a.put("region", addr.region)
|
|
736
|
+
a.put("postcode", addr.postcode)
|
|
737
|
+
a.put("country", addr.country)
|
|
738
|
+
addrArr.put(a)
|
|
739
|
+
}
|
|
740
|
+
js.put("addresses", addrArr)
|
|
741
|
+
}
|
|
742
|
+
return js
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
private fun groupToJS(group: io.capkit.people.models.GroupData): JSObject {
|
|
746
|
+
val js = JSObject()
|
|
747
|
+
js.put("id", group.id)
|
|
748
|
+
js.put("name", group.name)
|
|
749
|
+
js.put("source", group.source)
|
|
750
|
+
js.put("readOnly", group.readOnly)
|
|
751
|
+
return js
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
/**
|
|
755
|
+
* JS marshalling helper for UnifiedContact payloads.
|
|
756
|
+
* Responsibility: bridge layer only (ContactData → JSObject / JSArray).
|
|
757
|
+
* Native contact mapping from platform types → ContactData is handled in the Impl/Utils layer.
|
|
758
|
+
*/
|
|
759
|
+
private fun contactsToJSArray(contacts: List<io.capkit.people.models.ContactData>): JSArray {
|
|
760
|
+
val arr = JSArray()
|
|
761
|
+
for (contact in contacts) {
|
|
762
|
+
arr.put(contactToJS(contact))
|
|
763
|
+
}
|
|
764
|
+
return arr
|
|
765
|
+
}
|
|
766
|
+
}
|