@capgo/camera-preview 7.21.10 → 7.22.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.
@@ -5,6 +5,7 @@ import Capacitor
5
5
  import CoreImage
6
6
  import CoreLocation
7
7
  import MobileCoreServices
8
+ import UIKit
8
9
 
9
10
  extension UIWindow {
10
11
  static var isLandscape: Bool {
@@ -66,6 +67,8 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin, CLLocationManagerDelega
66
67
  CAPPluginMethod(name: "deleteFile", returnType: CAPPluginReturnPromise),
67
68
  CAPPluginMethod(name: "getOrientation", returnType: CAPPluginReturnPromise),
68
69
  CAPPluginMethod(name: "getSafeAreaInsets", returnType: CAPPluginReturnPromise),
70
+ CAPPluginMethod(name: "checkPermissions", returnType: CAPPluginReturnPromise),
71
+ CAPPluginMethod(name: "requestPermissions", returnType: CAPPluginReturnPromise),
69
72
  // Exposure control methods
70
73
  CAPPluginMethod(name: "getExposureModes", returnType: CAPPluginReturnPromise),
71
74
  CAPPluginMethod(name: "getExposureMode", returnType: CAPPluginReturnPromise),
@@ -100,6 +103,7 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin, CLLocationManagerDelega
100
103
  private var positioning: String = "center"
101
104
  private var permissionCallID: String?
102
105
  private var waitingForLocation: Bool = false
106
+ private var isPresentingPermissionAlert: Bool = false
103
107
 
104
108
  // MARK: - Helper Methods for Aspect Ratio
105
109
 
@@ -165,6 +169,71 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin, CLLocationManagerDelega
165
169
  }
166
170
  }
167
171
 
172
+ private func presentCameraPermissionAlert(title: String,
173
+ message: String,
174
+ openSettingsText: String,
175
+ cancelText: String,
176
+ completion: (() -> Void)? = nil) {
177
+ DispatchQueue.main.async {
178
+ guard let viewController = self.bridge?.viewController else {
179
+ completion?()
180
+ return
181
+ }
182
+
183
+ if self.isPresentingPermissionAlert {
184
+ completion?()
185
+ return
186
+ }
187
+
188
+ let alert = UIAlertController(title: title,
189
+ message: message,
190
+ preferredStyle: .alert)
191
+
192
+ let cancelAction = UIAlertAction(title: cancelText, style: .cancel) { _ in
193
+ self.isPresentingPermissionAlert = false
194
+ }
195
+ alert.addAction(cancelAction)
196
+
197
+ let openSettingsAction = UIAlertAction(title: openSettingsText, style: .default) { _ in
198
+ self.isPresentingPermissionAlert = false
199
+ guard let settingsURL = URL(string: UIApplication.openSettingsURLString) else { return }
200
+ UIApplication.shared.open(settingsURL, options: [:], completionHandler: nil)
201
+ }
202
+ alert.addAction(openSettingsAction)
203
+
204
+ self.isPresentingPermissionAlert = true
205
+ viewController.present(alert, animated: true) {
206
+ completion?()
207
+ }
208
+ }
209
+ }
210
+
211
+ private func mapAuthorizationStatus(_ status: AVAuthorizationStatus) -> String {
212
+ switch status {
213
+ case .authorized:
214
+ return "granted"
215
+ case .denied, .restricted:
216
+ return "denied"
217
+ case .notDetermined:
218
+ fallthrough
219
+ @unknown default:
220
+ return "prompt"
221
+ }
222
+ }
223
+
224
+ private func mapAudioPermission(_ permission: AVAudioSession.RecordPermission) -> String {
225
+ switch permission {
226
+ case .granted:
227
+ return "granted"
228
+ case .denied:
229
+ return "denied"
230
+ case .undetermined:
231
+ fallthrough
232
+ @unknown default:
233
+ return "prompt"
234
+ }
235
+ }
236
+
168
237
  @objc func getZoomButtonValues(_ call: CAPPluginCall) {
169
238
  guard isInitialized else {
170
239
  call.reject("Camera not initialized")
@@ -582,35 +651,62 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin, CLLocationManagerDelega
582
651
  call.reject("Cannot set both aspectRatio and size (width/height). Use setPreviewSize after start.")
583
652
  return
584
653
  }
585
-
586
- AVCaptureDevice.requestAccess(for: .video, completionHandler: { (granted: Bool) in
587
-
588
- guard granted else {
589
- call.reject("permission failed")
654
+ let beginStart: () -> Void = {
655
+ if self.cameraController.captureSession?.isRunning ?? false {
656
+ DispatchQueue.main.async {
657
+ self.isInitializing = false
658
+ call.reject("camera already started")
659
+ }
590
660
  return
591
661
  }
592
662
 
593
- if self.cameraController.captureSession?.isRunning ?? false {
594
- call.reject("camera already started")
595
- } else {
596
- self.cameraController.prepare(cameraPosition: self.cameraPosition, deviceId: deviceId, disableAudio: self.disableAudio, cameraMode: cameraMode, aspectRatio: self.aspectRatio, initialZoomLevel: initialZoomLevel, disableFocusIndicator: self.disableFocusIndicator) {error in
597
- if let error = error {
598
- print(error)
663
+ self.cameraController.prepare(cameraPosition: self.cameraPosition, deviceId: deviceId, disableAudio: self.disableAudio, cameraMode: cameraMode, aspectRatio: self.aspectRatio, initialZoomLevel: initialZoomLevel, disableFocusIndicator: self.disableFocusIndicator) { error in
664
+ if let error = error {
665
+ print(error)
666
+ DispatchQueue.main.async {
667
+ self.isInitializing = false
599
668
  call.reject(error.localizedDescription)
600
- return
601
669
  }
670
+ return
671
+ }
602
672
 
603
- DispatchQueue.main.async {
604
- UIDevice.current.beginGeneratingDeviceOrientationNotifications()
605
- NotificationCenter.default.addObserver(self,
606
- selector: #selector(self.handleOrientationChange),
607
- name: UIDevice.orientationDidChangeNotification,
608
- object: nil)
609
- self.completeStartCamera(call: call)
610
- }
673
+ DispatchQueue.main.async {
674
+ UIDevice.current.beginGeneratingDeviceOrientationNotifications()
675
+ NotificationCenter.default.addObserver(self,
676
+ selector: #selector(self.handleOrientationChange),
677
+ name: UIDevice.orientationDidChangeNotification,
678
+ object: nil)
679
+ self.completeStartCamera(call: call)
680
+ }
681
+ }
682
+ }
683
+
684
+ let handleDenied: (AVAuthorizationStatus) -> Void = { _ in
685
+ DispatchQueue.main.async {
686
+ self.isInitializing = false
687
+ call.reject("camera permission denied. enable camera access in Settings.", "cameraPermissionDenied")
688
+ }
689
+ }
690
+
691
+ let authorizationStatus = AVCaptureDevice.authorizationStatus(for: .video)
692
+
693
+ switch authorizationStatus {
694
+ case .authorized:
695
+ beginStart()
696
+ case .notDetermined:
697
+ AVCaptureDevice.requestAccess(for: .video) { granted in
698
+ if granted {
699
+ beginStart()
700
+ } else {
701
+ let currentStatus = AVCaptureDevice.authorizationStatus(for: .video)
702
+ handleDenied(currentStatus)
611
703
  }
612
704
  }
613
- })
705
+ case .denied, .restricted:
706
+ handleDenied(authorizationStatus)
707
+ @unknown default:
708
+ handleDenied(authorizationStatus)
709
+ }
614
710
  }
615
711
 
616
712
  private func completeStartCamera(call: CAPPluginCall) {
@@ -768,6 +864,94 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin, CLLocationManagerDelega
768
864
  call.resolve()
769
865
  }
770
866
  }
867
+
868
+ override public func checkPermissions(_ call: CAPPluginCall) {
869
+ let disableAudio = call.getBool("disableAudio") ?? true
870
+ let cameraStatus = self.mapAuthorizationStatus(AVCaptureDevice.authorizationStatus(for: .video))
871
+
872
+ var result: [String: Any] = [
873
+ "camera": cameraStatus
874
+ ]
875
+
876
+ if disableAudio == false {
877
+ let audioPermission = AVAudioSession.sharedInstance().recordPermission
878
+ result["microphone"] = self.mapAudioPermission(audioPermission)
879
+ }
880
+
881
+ call.resolve(result)
882
+ }
883
+
884
+ override public func requestPermissions(_ call: CAPPluginCall) {
885
+ let disableAudio = call.getBool("disableAudio") ?? true
886
+ self.disableAudio = disableAudio
887
+
888
+ let title = call.getString("title") ?? "Camera Permission Needed"
889
+ let message = call.getString("message") ?? "Enable camera access in Settings to use the preview."
890
+ let openSettingsText = call.getString("openSettingsButtonTitle") ?? "Open Settings"
891
+ let cancelText = call.getString("cancelButtonTitle") ?? "Cancel"
892
+ let showSettingsAlert = call.getBool("showSettingsAlert") ?? false
893
+
894
+ var currentCameraStatus = AVCaptureDevice.authorizationStatus(for: .video)
895
+ let audioSession = AVAudioSession.sharedInstance()
896
+ var currentAudioStatus: AVAudioSession.RecordPermission? = disableAudio ? nil : audioSession.recordPermission
897
+
898
+ let dispatchGroup = DispatchGroup()
899
+ var pendingRequests = 0
900
+
901
+ if currentCameraStatus == .notDetermined {
902
+ pendingRequests += 1
903
+ dispatchGroup.enter()
904
+ AVCaptureDevice.requestAccess(for: .video) { granted in
905
+ currentCameraStatus = granted ? .authorized : .denied
906
+ dispatchGroup.leave()
907
+ }
908
+ }
909
+
910
+ if let audioStatus = currentAudioStatus,
911
+ audioStatus == .undetermined {
912
+ pendingRequests += 1
913
+ dispatchGroup.enter()
914
+ audioSession.requestRecordPermission { granted in
915
+ currentAudioStatus = granted ? .granted : .denied
916
+ dispatchGroup.leave()
917
+ }
918
+ }
919
+
920
+ let finalizeResponse: () -> Void = { [weak self] in
921
+ guard let self = self else { return }
922
+
923
+ let cameraResult = self.mapAuthorizationStatus(currentCameraStatus)
924
+ var result: [String: Any] = [
925
+ "camera": cameraResult
926
+ ]
927
+
928
+ if let audioStatus = currentAudioStatus {
929
+ result["microphone"] = self.mapAudioPermission(audioStatus)
930
+ }
931
+
932
+ let shouldShowAlert = showSettingsAlert &&
933
+ (cameraResult == "denied" ||
934
+ ((result["microphone"] as? String) == "denied"))
935
+
936
+ guard shouldShowAlert else {
937
+ call.resolve(result)
938
+ return
939
+ }
940
+
941
+ self.presentCameraPermissionAlert(title: title,
942
+ message: message,
943
+ openSettingsText: openSettingsText,
944
+ cancelText: cancelText) {
945
+ call.resolve(result)
946
+ }
947
+ }
948
+
949
+ if pendingRequests == 0 {
950
+ DispatchQueue.main.async(execute: finalizeResponse)
951
+ } else {
952
+ dispatchGroup.notify(queue: .main, execute: finalizeResponse)
953
+ }
954
+ }
771
955
  // Get user's cache directory path
772
956
  @objc func getTempFilePath() -> URL {
773
957
  let path = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask)[0]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@capgo/camera-preview",
3
- "version": "7.21.10",
3
+ "version": "7.22.0",
4
4
  "description": "Camera preview",
5
5
  "license": "MIT",
6
6
  "repository": {