1
0

qt gui: qrreader: macos: add runtime requesting of camera permission

- we were already
  - statically declaring "NSCameraUsageDescription" in the Info.plist
    - this used to be enough in the past
  - codesigning with an entitlements.plist that declares "com.apple.security.device.camera"
    - I believe this is required for notarization to pass for an app that declares "NSCameraUsageDescription".
- previously this was enough to access the camera IIRC
  - in any case, if the user goes into "System Preferences">"Security & Privacy", they can manually modify permissions there
- now with this commit, we on-demand trigger at runtime the OS permission prompt
  - making it much easier for users to actually use the camera
  - note: if you run via the Terminal, e.g. `$ $HOME/Desktop/Electrum.app/Contents/MacOS/run_electrum`,
    then it will be the Terminal app that requires the camera permission. If you run by double-clicking Electrum.app,
    then Electrum.app will be the "app" that requires the camera permission.
  - `$ tccutil reset Camera` can be used to clear permissions for all apps (back to the Qt::PermissionStatus::Undetermined state)

ref https://doc.qt.io/qt-6.5/qcoreapplication.html#requestPermission-1
This commit is contained in:
SomberNight
2025-06-14 16:27:05 +00:00
parent 95b8af3401
commit 612d82e8d4
3 changed files with 44 additions and 11 deletions

View File

@@ -97,7 +97,7 @@ from .update_checker import UpdateCheck, UpdateCheckThread
from .channels_list import ChannelsList
from .confirm_tx_dialog import ConfirmTxDialog
from .rbf_dialog import BumpFeeDialog, DSCancelDialog
from .qrreader import scan_qrcode
from .qrreader import scan_qrcode_from_camera
from .swap_dialog import SwapDialog, InvalidSwapParameters
from .balance_dialog import (BalanceToolButton, COLOR_FROZEN, COLOR_UNMATURED, COLOR_UNCONFIRMED, COLOR_CONFIRMED,
COLOR_LIGHTNING, COLOR_FROZEN_LIGHTNING)
@@ -2313,7 +2313,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger, QtEventListener):
return
self.show_transaction(tx)
scan_qrcode(parent=self.top_level_window(), config=self.config, callback=cb)
scan_qrcode_from_camera(parent=self.top_level_window(), config=self.config, callback=cb)
def read_tx_from_file(self) -> Optional[Transaction]:
fileName = getOpenFileName(

View File

@@ -26,7 +26,8 @@ from typing import Callable, Optional, TYPE_CHECKING, Mapping, Sequence
from PyQt6.QtWidgets import QMessageBox, QWidget
from PyQt6.QtGui import QImage, QPainter, QColor
from PyQt6.QtCore import QRect
from PyQt6.QtCore import QRect, QCoreApplication
from PyQt6 import QtCore
from electrum.i18n import _
from electrum.util import UserFacingException
@@ -43,18 +44,24 @@ if TYPE_CHECKING:
_logger = get_logger(__name__)
def scan_qrcode(
def scan_qrcode_from_camera(
*,
parent: Optional[QWidget],
config: 'SimpleConfig',
callback: Callable[[bool, str, Optional[str]], None],
) -> None:
"""Scans QR code using camera."""
"""Scans QR code using camera. It handles requesting camera access permission from the OS if needed."""
assert parent is None or isinstance(parent, QWidget), f"parent should be a QWidget, not {parent!r}"
if sys.platform == 'darwin' or sys.platform in ('windows', 'win32'):
_scan_qrcode_using_qtmultimedia(parent=parent, config=config, callback=callback)
else: # desktop Linux and similar
_scan_qrcode_using_zbar(parent=parent, config=config, callback=callback)
def do_scan():
_scan_qrcode_from_camera(parent=parent, config=config, callback=callback)
if _has_camera_permission():
do_scan()
else:
# Request permission now. This is only a thing on macOS atm.
# Note: this assumes we are running on the main thread. Permissions can only be requested from the main thread.
app = QCoreApplication.instance()
app.requestPermission(QtCore.QCameraPermission(), lambda _x: do_scan())
def scan_qr_from_image(image: QImage) -> Sequence[QrCodeResult]:
@@ -181,3 +188,29 @@ def _scan_qrcode_using_qtmultimedia(
_qr_dialog = None
callback(False, repr(e), None)
def _scan_qrcode_from_camera(
*,
parent: Optional[QWidget],
config: 'SimpleConfig',
callback: Callable[[bool, str, Optional[str]], None],
) -> None:
"""Scans QR code using camera."""
assert parent is None or isinstance(parent, QWidget), f"parent should be a QWidget, not {parent!r}"
if not _has_camera_permission():
callback(False, _("Missing camera permission."), None)
return
if sys.platform == 'darwin' or sys.platform in ('windows', 'win32'):
_scan_qrcode_using_qtmultimedia(parent=parent, config=config, callback=callback)
else: # desktop Linux and similar
_scan_qrcode_using_zbar(parent=parent, config=config, callback=callback)
def _has_camera_permission() -> bool:
if not hasattr(QtCore, "QCameraPermission"): # requires Qt 6.5+
_logger.info(f"QtCore does not support QCameraPermission. This requires Qt 6.5+")
return True # hope for the best
app = QCoreApplication.instance()
permission_status = app.checkPermission(QtCore.QCameraPermission())
return permission_status == QtCore.Qt.PermissionStatus.Granted

View File

@@ -792,10 +792,10 @@ class GenericInputHandler:
except Exception as e:
show_error(_('Invalid payment identifier in QR') + ':\n' + repr(e))
from .qrreader import scan_qrcode
from .qrreader import scan_qrcode_from_camera
if parent is None:
parent = self if isinstance(self, QWidget) else None
scan_qrcode(parent=parent, config=config, callback=cb)
scan_qrcode_from_camera(parent=parent, config=config, callback=cb)
def input_qr_from_screenshot(
self,