@capgo/capacitor-contacts 7.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.
@@ -0,0 +1,515 @@
1
+ import Capacitor
2
+ import Contacts
3
+ import UIKit
4
+
5
+ @objc(CapacitorContactsPlugin)
6
+ public class CapacitorContactsPlugin: CAPPlugin, CAPBridgedPlugin {
7
+ public let identifier = "CapacitorContactsPlugin"
8
+ public let jsName = "CapacitorContacts"
9
+ public let pluginMethods: [CAPPluginMethod] = [
10
+ CAPPluginMethod(name: "countContacts", returnType: CAPPluginReturnPromise),
11
+ CAPPluginMethod(name: "createContact", returnType: CAPPluginReturnPromise),
12
+ CAPPluginMethod(name: "createGroup", returnType: CAPPluginReturnPromise),
13
+ CAPPluginMethod(name: "deleteContactById", returnType: CAPPluginReturnPromise),
14
+ CAPPluginMethod(name: "deleteGroupById", returnType: CAPPluginReturnPromise),
15
+ CAPPluginMethod(name: "displayContactById", returnType: CAPPluginReturnPromise),
16
+ CAPPluginMethod(name: "displayCreateContact", returnType: CAPPluginReturnPromise),
17
+ CAPPluginMethod(name: "displayUpdateContactById", returnType: CAPPluginReturnPromise),
18
+ CAPPluginMethod(name: "getAccounts", returnType: CAPPluginReturnPromise),
19
+ CAPPluginMethod(name: "getContactById", returnType: CAPPluginReturnPromise),
20
+ CAPPluginMethod(name: "getContacts", returnType: CAPPluginReturnPromise),
21
+ CAPPluginMethod(name: "getGroupById", returnType: CAPPluginReturnPromise),
22
+ CAPPluginMethod(name: "getGroups", returnType: CAPPluginReturnPromise),
23
+ CAPPluginMethod(name: "isAvailable", returnType: CAPPluginReturnPromise),
24
+ CAPPluginMethod(name: "isSupported", returnType: CAPPluginReturnPromise),
25
+ CAPPluginMethod(name: "openSettings", returnType: CAPPluginReturnPromise),
26
+ CAPPluginMethod(name: "pickContact", returnType: CAPPluginReturnPromise),
27
+ CAPPluginMethod(name: "pickContacts", returnType: CAPPluginReturnPromise),
28
+ CAPPluginMethod(name: "updateContactById", returnType: CAPPluginReturnPromise),
29
+ CAPPluginMethod(name: "checkPermissions", returnType: CAPPluginReturnPromise),
30
+ CAPPluginMethod(name: "requestPermissions", returnType: CAPPluginReturnPromise)
31
+ ]
32
+
33
+ private let contactStore = CNContactStore()
34
+
35
+ // MARK: - Implemented API surface
36
+
37
+ @objc func countContacts(_ call: CAPPluginCall) {
38
+ ensureAuthorized(call) {
39
+ do {
40
+ var count = 0
41
+ let request = CNContactFetchRequest(keysToFetch: [CNContactIdentifierKey as CNKeyDescriptor])
42
+ try self.contactStore.enumerateContacts(with: request) { _, _ in
43
+ count += 1
44
+ }
45
+ call.resolve(["count": count])
46
+ } catch {
47
+ call.reject("Failed to count contacts.", nil, error)
48
+ }
49
+ }
50
+ }
51
+
52
+ @objc func getContacts(_ call: CAPPluginCall) {
53
+ ensureAuthorized(call) {
54
+ let options = (call.options["options"] as? JSObject) ?? [:]
55
+ let fields = (options["fields"] as? [String]).map { Set($0) }
56
+ let limit = options["limit"] as? Int
57
+ let offset = options["offset"] as? Int ?? 0
58
+
59
+ let keysToFetch = self.keysToFetch(for: fields)
60
+ let request = CNContactFetchRequest(keysToFetch: keysToFetch)
61
+ request.sortOrder = .userDefault
62
+
63
+ var contacts: [CNContact] = []
64
+ var index = 0
65
+
66
+ do {
67
+ try self.contactStore.enumerateContacts(with: request) { contact, stop in
68
+ if index < offset {
69
+ index += 1
70
+ return
71
+ }
72
+ if let limit, contacts.count >= limit {
73
+ stop.pointee = true
74
+ return
75
+ }
76
+ contacts.append(contact)
77
+ index += 1
78
+ }
79
+
80
+ let includeGroupIds = fields == nil || fields!.contains("groupIds")
81
+ var membership: [String: [String]] = [:]
82
+ if includeGroupIds {
83
+ membership = self.groupMembershipMap(for: contacts.map(\.identifier))
84
+ }
85
+
86
+ let serialized = contacts.map { self.serialize(contact: $0, fields: fields, membership: membership) }
87
+ call.resolve(["contacts": serialized])
88
+ } catch {
89
+ call.reject("Failed to fetch contacts.", nil, error)
90
+ }
91
+ }
92
+ }
93
+
94
+ @objc func getContactById(_ call: CAPPluginCall) {
95
+ guard let options = call.options["options"] as? JSObject, let identifier = options["id"] as? String else {
96
+ call.reject("Missing contact identifier.")
97
+ return
98
+ }
99
+
100
+ ensureAuthorized(call) {
101
+ let fields = (options["fields"] as? [String]).map { Set($0) }
102
+ let keysToFetch = self.keysToFetch(for: fields)
103
+
104
+ do {
105
+ let contact = try self.contactStore.unifiedContact(withIdentifier: identifier, keysToFetch: keysToFetch)
106
+ let membership = self.groupMembershipMap(for: [identifier])
107
+ let serialized = self.serialize(contact: contact, fields: fields, membership: membership)
108
+ call.resolve(["contact": serialized])
109
+ } catch {
110
+ call.reject("Failed to fetch contact.", nil, error)
111
+ }
112
+ }
113
+ }
114
+
115
+ @objc func getAccounts(_ call: CAPPluginCall) {
116
+ do {
117
+ let containers = try contactStore.containers(matching: nil)
118
+ let accounts: [JSObject] = containers.map {
119
+ var account: JSObject = [:]
120
+ account["name"] = $0.name
121
+ account["type"] = mapContainerType($0.type)
122
+ return account
123
+ }
124
+ call.resolve(["accounts": accounts])
125
+ } catch {
126
+ call.reject("Failed to fetch accounts.", nil, error)
127
+ }
128
+ }
129
+
130
+ @objc func isSupported(_ call: CAPPluginCall) {
131
+ call.resolve(["isSupported": true])
132
+ }
133
+
134
+ @objc func isAvailable(_ call: CAPPluginCall) {
135
+ call.resolve(["isAvailable": true])
136
+ }
137
+
138
+ @objc func openSettings(_ call: CAPPluginCall) {
139
+ DispatchQueue.main.async {
140
+ guard let url = URL(string: UIApplication.openSettingsURLString) else {
141
+ call.reject("Unable to open settings.")
142
+ return
143
+ }
144
+ UIApplication.shared.open(url, options: [:]) { success in
145
+ if success {
146
+ call.resolve()
147
+ } else {
148
+ call.reject("Unable to open settings.")
149
+ }
150
+ }
151
+ }
152
+ }
153
+
154
+ @objc override public func checkPermissions(_ call: CAPPluginCall) {
155
+ let status = authorizationStatus()
156
+ call.resolve(status)
157
+ }
158
+
159
+ @objc override public func requestPermissions(_ call: CAPPluginCall) {
160
+ let requested = (call.options["options"] as? JSObject)?["permissions"] as? [String] ?? ["readContacts", "writeContacts"]
161
+ let needsRequest = requested.contains { _ in mapAuthorizationStatus(CNContactStore.authorizationStatus(for: .contacts)) != "granted" }
162
+
163
+ if !needsRequest {
164
+ call.resolve(authorizationStatus())
165
+ return
166
+ }
167
+
168
+ contactStore.requestAccess(for: .contacts, completionHandler: { (granted: Bool, error: Error?) in
169
+ DispatchQueue.main.async {
170
+ if let error {
171
+ call.reject("Permission request failed.", nil, error)
172
+ return
173
+ }
174
+ _ = granted
175
+ call.resolve(self.authorizationStatus())
176
+ }
177
+ })
178
+ }
179
+
180
+ // MARK: - Not yet implemented operations
181
+
182
+ private func notImplemented(_ call: CAPPluginCall) {
183
+ call.reject("Method not implemented yet.")
184
+ }
185
+
186
+ @objc func createContact(_ call: CAPPluginCall) { notImplemented(call) }
187
+ @objc func createGroup(_ call: CAPPluginCall) { notImplemented(call) }
188
+ @objc func deleteContactById(_ call: CAPPluginCall) { notImplemented(call) }
189
+ @objc func deleteGroupById(_ call: CAPPluginCall) { notImplemented(call) }
190
+ @objc func displayContactById(_ call: CAPPluginCall) { notImplemented(call) }
191
+ @objc func displayCreateContact(_ call: CAPPluginCall) { notImplemented(call) }
192
+ @objc func displayUpdateContactById(_ call: CAPPluginCall) { notImplemented(call) }
193
+ @objc func getGroupById(_ call: CAPPluginCall) { notImplemented(call) }
194
+ @objc func getGroups(_ call: CAPPluginCall) { notImplemented(call) }
195
+ @objc func pickContact(_ call: CAPPluginCall) { notImplemented(call) }
196
+ @objc func pickContacts(_ call: CAPPluginCall) { notImplemented(call) }
197
+ @objc func updateContactById(_ call: CAPPluginCall) { notImplemented(call) }
198
+
199
+ // MARK: - Helpers
200
+
201
+ private func ensureAuthorized(_ call: CAPPluginCall, completion: @escaping () -> Void) {
202
+ switch CNContactStore.authorizationStatus(for: .contacts) {
203
+ case .authorized, .limited:
204
+ completion()
205
+ case .notDetermined:
206
+ contactStore.requestAccess(for: .contacts, completionHandler: { granted, error in
207
+ DispatchQueue.main.async {
208
+ if let error {
209
+ call.reject("Permission request failed.", nil, error)
210
+ return
211
+ }
212
+ if granted {
213
+ completion()
214
+ } else {
215
+ call.reject("Contacts permission not granted.")
216
+ }
217
+ }
218
+ })
219
+ case .denied, .restricted:
220
+ call.reject("Contacts permission not granted.")
221
+ @unknown default:
222
+ call.reject("Contacts permission state unknown.")
223
+ }
224
+ }
225
+
226
+ private func authorizationStatus() -> JSObject {
227
+ let status = mapAuthorizationStatus(CNContactStore.authorizationStatus(for: .contacts))
228
+ return [
229
+ "readContacts": status,
230
+ "writeContacts": status
231
+ ]
232
+ }
233
+
234
+ private func mapAuthorizationStatus(_ status: CNAuthorizationStatus) -> String {
235
+ switch status {
236
+ case .authorized, .limited:
237
+ return "granted"
238
+ case .denied:
239
+ return "denied"
240
+ case .restricted:
241
+ return "denied"
242
+ case .notDetermined:
243
+ return "prompt"
244
+ @unknown default:
245
+ return "prompt"
246
+ }
247
+ }
248
+
249
+ private func keysToFetch(for fields: Set<String>?) -> [CNKeyDescriptor] {
250
+ var keys: [CNKeyDescriptor] = [
251
+ CNContactIdentifierKey as CNKeyDescriptor,
252
+ CNContactGivenNameKey as CNKeyDescriptor,
253
+ CNContactFamilyNameKey as CNKeyDescriptor,
254
+ CNContactMiddleNameKey as CNKeyDescriptor,
255
+ CNContactNamePrefixKey as CNKeyDescriptor,
256
+ CNContactNameSuffixKey as CNKeyDescriptor,
257
+ CNContactOrganizationNameKey as CNKeyDescriptor,
258
+ CNContactJobTitleKey as CNKeyDescriptor,
259
+ CNContactEmailAddressesKey as CNKeyDescriptor,
260
+ CNContactPhoneNumbersKey as CNKeyDescriptor,
261
+ CNContactPostalAddressesKey as CNKeyDescriptor,
262
+ CNContactUrlAddressesKey as CNKeyDescriptor,
263
+ CNContactBirthdayKey as CNKeyDescriptor,
264
+ CNContactNoteKey as CNKeyDescriptor,
265
+ CNContactImageDataAvailableKey as CNKeyDescriptor,
266
+ CNContactImageDataKey as CNKeyDescriptor
267
+ ]
268
+
269
+ if let fields, fields.contains("groupIds") {
270
+ // No additional keys required, but this preserves the behaviour if custom keys are needed later.
271
+ }
272
+
273
+ if let fields, fields.contains("fullName") {
274
+ keys.append(CNContactFormatter.descriptorForRequiredKeys(for: .fullName))
275
+ }
276
+
277
+ return keys
278
+ }
279
+
280
+ private func groupMembershipMap(for identifiers: [String]) -> [String: [String]] {
281
+ guard !identifiers.isEmpty else { return [:] }
282
+
283
+ var result: [String: [String]] = [:]
284
+ let identifierSet = Set(identifiers)
285
+
286
+ do {
287
+ let groups = try contactStore.groups(matching: nil)
288
+ for group in groups {
289
+ let predicate = CNContact.predicateForContactsInGroup(withIdentifier: group.identifier)
290
+ let members = try contactStore.unifiedContacts(matching: predicate, keysToFetch: [CNContactIdentifierKey as CNKeyDescriptor])
291
+ for member in members where identifierSet.contains(member.identifier) {
292
+ result[member.identifier, default: []].append(group.identifier)
293
+ }
294
+ }
295
+ } catch {
296
+ CAPLog.print("CapacitorContactsPlugin", "Failed to compute group membership: \(error.localizedDescription)")
297
+ }
298
+
299
+ return result
300
+ }
301
+
302
+ private func serialize(contact: CNContact, fields: Set<String>?, membership: [String: [String]]) -> JSObject {
303
+ let includeAll = fields == nil
304
+ func shouldInclude(_ field: String) -> Bool {
305
+ includeAll || fields!.contains(field)
306
+ }
307
+
308
+ var result: JSObject = [:]
309
+
310
+ if shouldInclude("id") {
311
+ result["id"] = contact.identifier
312
+ }
313
+ if shouldInclude("givenName") {
314
+ result["givenName"] = contact.givenName
315
+ }
316
+ if shouldInclude("familyName") {
317
+ result["familyName"] = contact.familyName
318
+ }
319
+ if shouldInclude("middleName") {
320
+ result["middleName"] = contact.middleName
321
+ }
322
+ if shouldInclude("namePrefix") {
323
+ result["namePrefix"] = contact.namePrefix
324
+ }
325
+ if shouldInclude("nameSuffix") {
326
+ result["nameSuffix"] = contact.nameSuffix
327
+ }
328
+ if shouldInclude("organizationName") {
329
+ result["organizationName"] = contact.organizationName
330
+ }
331
+ if shouldInclude("jobTitle") {
332
+ result["jobTitle"] = contact.jobTitle
333
+ }
334
+ if shouldInclude("note") {
335
+ result["note"] = contact.note
336
+ }
337
+ if shouldInclude("fullName") {
338
+ result["fullName"] = CNContactFormatter.string(from: contact, style: .fullName) ?? ""
339
+ }
340
+
341
+ if shouldInclude("emailAddresses") {
342
+ let emails: [JSObject] = contact.emailAddresses.map { labeledValue in
343
+ var entry: JSObject = [:]
344
+ entry["value"] = labeledValue.value as String
345
+ let (type, label) = mapEmailLabel(labeledValue.label)
346
+ entry["type"] = type
347
+ if let label { entry["label"] = label }
348
+ entry["isPrimary"] = false
349
+ return entry
350
+ }
351
+ result["emailAddresses"] = emails
352
+ }
353
+
354
+ if shouldInclude("phoneNumbers") {
355
+ let phones: [JSObject] = contact.phoneNumbers.map { labeledValue in
356
+ var entry: JSObject = [:]
357
+ entry["value"] = labeledValue.value.stringValue
358
+ let (type, label) = mapPhoneLabel(labeledValue.label)
359
+ entry["type"] = type
360
+ if let label { entry["label"] = label }
361
+ entry["isPrimary"] = false
362
+ return entry
363
+ }
364
+ result["phoneNumbers"] = phones
365
+ }
366
+
367
+ if shouldInclude("postalAddresses") {
368
+ let addresses: [JSObject] = contact.postalAddresses.map { labeledValue in
369
+ let postal = labeledValue.value
370
+ var entry: JSObject = [:]
371
+ entry["city"] = postal.city
372
+ entry["country"] = postal.country
373
+ entry["formatted"] = CNPostalAddressFormatter.string(from: postal, style: .mailingAddress)
374
+ entry["isoCountryCode"] = postal.isoCountryCode
375
+ entry["isPrimary"] = false
376
+ entry["neighborhood"] = postal.subLocality
377
+ entry["postalCode"] = postal.postalCode
378
+ entry["state"] = postal.state
379
+ entry["street"] = postal.street
380
+ let (type, label) = mapPostalLabel(labeledValue.label)
381
+ entry["type"] = type
382
+ if let label { entry["label"] = label }
383
+ return entry
384
+ }
385
+ result["postalAddresses"] = addresses
386
+ }
387
+
388
+ if shouldInclude("urlAddresses") {
389
+ let urls: [JSObject] = contact.urlAddresses.map { labeledValue in
390
+ var entry: JSObject = [:]
391
+ entry["value"] = labeledValue.value as String
392
+ let (type, label) = mapURLLabel(labeledValue.label)
393
+ entry["type"] = type
394
+ if let label { entry["label"] = label }
395
+ return entry
396
+ }
397
+ result["urlAddresses"] = urls
398
+ }
399
+
400
+ if shouldInclude("birthday"), let birthday = contact.birthday {
401
+ var birthdayJS: JSObject = [:]
402
+ if let day = birthday.day { birthdayJS["day"] = day }
403
+ if let month = birthday.month { birthdayJS["month"] = month }
404
+ if let year = birthday.year { birthdayJS["year"] = year }
405
+ result["birthday"] = birthdayJS
406
+ }
407
+
408
+ if shouldInclude("photo"), contact.imageDataAvailable, let imageData = contact.imageData {
409
+ result["photo"] = imageData.base64EncodedString()
410
+ }
411
+
412
+ if shouldInclude("groupIds") {
413
+ result["groupIds"] = membership[contact.identifier] ?? []
414
+ }
415
+
416
+ if shouldInclude("account") {
417
+ result["account"] = NSNull()
418
+ }
419
+
420
+ return result
421
+ }
422
+
423
+ private func mapEmailLabel(_ label: String?) -> (String, String?) {
424
+ switch label {
425
+ case CNLabelHome:
426
+ return ("HOME", nil)
427
+ case CNLabelWork:
428
+ return ("WORK", nil)
429
+ case CNLabelEmailiCloud:
430
+ return ("ICLOUD", nil)
431
+ case CNLabelOther:
432
+ return ("OTHER", nil)
433
+ case .none:
434
+ return ("OTHER", nil)
435
+ default:
436
+ return ("CUSTOM", CNLabeledValue<NSString>.localizedString(forLabel: label ?? ""))
437
+ }
438
+ }
439
+
440
+ private func mapPhoneLabel(_ label: String?) -> (String, String?) {
441
+ switch label {
442
+ case CNLabelPhoneNumberMobile:
443
+ return ("MOBILE", nil)
444
+ case CNLabelPhoneNumberiPhone:
445
+ return ("IPHONE", nil)
446
+ case CNLabelPhoneNumberMain:
447
+ return ("MAIN", nil)
448
+ case CNLabelPhoneNumberHomeFax:
449
+ return ("HOME_FAX", nil)
450
+ case CNLabelPhoneNumberWorkFax:
451
+ return ("WORK_FAX", nil)
452
+ case CNLabelPhoneNumberOtherFax:
453
+ return ("OTHER_FAX", nil)
454
+ case CNLabelPhoneNumberPager:
455
+ return ("PAGER", nil)
456
+ case CNLabelHome:
457
+ return ("HOME", nil)
458
+ case CNLabelWork:
459
+ return ("WORK", nil)
460
+ case CNLabelOther:
461
+ return ("OTHER", nil)
462
+ case .none:
463
+ return ("OTHER", nil)
464
+ default:
465
+ return ("CUSTOM", CNLabeledValue<NSString>.localizedString(forLabel: label ?? ""))
466
+ }
467
+ }
468
+
469
+ private func mapPostalLabel(_ label: String?) -> (String, String?) {
470
+ switch label {
471
+ case CNLabelHome:
472
+ return ("HOME", nil)
473
+ case CNLabelWork:
474
+ return ("WORK", nil)
475
+ case CNLabelOther:
476
+ return ("OTHER", nil)
477
+ case .none:
478
+ return ("OTHER", nil)
479
+ default:
480
+ return ("CUSTOM", CNLabeledValue<CNPostalAddress>.localizedString(forLabel: label ?? ""))
481
+ }
482
+ }
483
+
484
+ private func mapURLLabel(_ label: String?) -> (String, String?) {
485
+ switch label {
486
+ case CNLabelURLAddressHomePage:
487
+ return ("HOMEPAGE", nil)
488
+ case CNLabelHome:
489
+ return ("HOME", nil)
490
+ case CNLabelWork:
491
+ return ("WORK", nil)
492
+ case CNLabelOther:
493
+ return ("OTHER", nil)
494
+ case .none:
495
+ return ("OTHER", nil)
496
+ default:
497
+ return ("CUSTOM", CNLabeledValue<NSString>.localizedString(forLabel: label ?? ""))
498
+ }
499
+ }
500
+
501
+ private func mapContainerType(_ type: CNContainerType) -> String {
502
+ switch type {
503
+ case .local:
504
+ return "local"
505
+ case .exchange:
506
+ return "exchange"
507
+ case .cardDAV:
508
+ return "carddav"
509
+ case .unassigned:
510
+ return "unassigned"
511
+ @unknown default:
512
+ return "unknown"
513
+ }
514
+ }
515
+ }
@@ -0,0 +1,9 @@
1
+ import XCTest
2
+ @testable import CapacitorContactsPlugin
3
+
4
+ final class CapacitorContactsPluginTests: XCTestCase {
5
+ func testPluginInitialises() {
6
+ let plugin = CapacitorContactsPlugin()
7
+ XCTAssertEqual(plugin.identifier, "CapacitorContactsPlugin")
8
+ }
9
+ }
package/package.json ADDED
@@ -0,0 +1,89 @@
1
+ {
2
+ "name": "@capgo/capacitor-contacts",
3
+ "version": "7.0.0",
4
+ "description": "Work with device contacts using Capacitor APIs",
5
+ "main": "dist/plugin.cjs.js",
6
+ "module": "dist/esm/index.js",
7
+ "types": "dist/esm/index.d.ts",
8
+ "unpkg": "dist/plugin.js",
9
+ "files": [
10
+ "android/src/main/",
11
+ "android/build.gradle",
12
+ "dist/",
13
+ "ios/Sources",
14
+ "ios/Tests",
15
+ "Package.swift",
16
+ "CapgoCapacitorContacts.podspec"
17
+ ],
18
+ "author": "Cap-go <contact@capgo.app>",
19
+ "license": "MIT",
20
+ "repository": {
21
+ "type": "git",
22
+ "url": "git+https://github.com/Cap-go/capacitor-contacts.git"
23
+ },
24
+ "bugs": {
25
+ "url": "https://github.com/Cap-go/capacitor-contacts/issues"
26
+ },
27
+ "keywords": [
28
+ "capacitor",
29
+ "plugin",
30
+ "native",
31
+ "contacts",
32
+ "address book",
33
+ "phonebook",
34
+ "capgo",
35
+ "capacitor"
36
+ ],
37
+ "scripts": {
38
+ "verify": "npm run verify:ios && npm run verify:android && npm run verify:web",
39
+ "verify:ios": "xcodebuild -scheme CapgoCapacitorContacts -destination generic/platform=iOS",
40
+ "verify:android": "cd android && ./gradlew clean build test && cd ..",
41
+ "verify:web": "npm run build",
42
+ "lint": "npm run eslint && npm run prettier -- --check && npm run swiftlint -- lint",
43
+ "fmt": "npm run eslint -- --fix && npm run prettier -- --write && npm run swiftlint -- --fix --format",
44
+ "eslint": "eslint . --ext .ts",
45
+ "prettier": "prettier \"**/*.{css,html,ts,js,java}\" --plugin=prettier-plugin-java",
46
+ "swiftlint": "node-swiftlint",
47
+ "docgen": "docgen --api CapacitorContactsPlugin --output-readme README.md --output-json dist/docs.json",
48
+ "build": "npm run clean && npm run docgen && tsc && rollup -c rollup.config.mjs",
49
+ "clean": "rimraf ./dist",
50
+ "watch": "tsc --watch",
51
+ "prepublishOnly": "npm run build"
52
+ },
53
+ "devDependencies": {
54
+ "@capacitor/android": "^7.0.0",
55
+ "@capacitor/cli": "^7.0.0",
56
+ "@capacitor/core": "^7.0.0",
57
+ "@capacitor/docgen": "^0.3.0",
58
+ "@capacitor/ios": "^7.0.0",
59
+ "@ionic/eslint-config": "^0.4.0",
60
+ "@ionic/prettier-config": "^4.0.0",
61
+ "@ionic/swiftlint-config": "^2.0.0",
62
+ "@types/node": "^22.13.1",
63
+ "eslint": "^8.57.0",
64
+ "eslint-plugin-import": "^2.31.0",
65
+ "husky": "^9.1.7",
66
+ "prettier": "^3.4.2",
67
+ "prettier-plugin-java": "^2.6.7",
68
+ "rimraf": "^6.0.1",
69
+ "rollup": "^4.34.6",
70
+ "swiftlint": "^2.0.0",
71
+ "typescript": "^5.7.3"
72
+ },
73
+ "peerDependencies": {
74
+ "@capacitor/core": ">=7.0.0"
75
+ },
76
+ "eslintConfig": {
77
+ "extends": "@ionic/eslint-config/recommended"
78
+ },
79
+ "prettier": "@ionic/prettier-config",
80
+ "swiftlint": "@ionic/swiftlint-config",
81
+ "capacitor": {
82
+ "ios": {
83
+ "src": "ios"
84
+ },
85
+ "android": {
86
+ "src": "android"
87
+ }
88
+ }
89
+ }