From 61682414f70cba49183a767f180e53f18914c8db Mon Sep 17 00:00:00 2001 From: Qazi Naveed Date: Wed, 17 Jun 2026 09:30:48 +0200 Subject: [PATCH 01/26] feat(GiniBankAPILibrary): add unsupportedQRCodeWarningEnabled flag to ClientConfiguration PP-2310 --- .../Documents/Configuration/ClientConfiguration.swift | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/BankAPILibrary/GiniBankAPILibrary/Sources/GiniBankAPILibrary/Documents/Configuration/ClientConfiguration.swift b/BankAPILibrary/GiniBankAPILibrary/Sources/GiniBankAPILibrary/Documents/Configuration/ClientConfiguration.swift index a9647f90c2..b15eff7f6d 100644 --- a/BankAPILibrary/GiniBankAPILibrary/Sources/GiniBankAPILibrary/Documents/Configuration/ClientConfiguration.swift +++ b/BankAPILibrary/GiniBankAPILibrary/Sources/GiniBankAPILibrary/Documents/Configuration/ClientConfiguration.swift @@ -27,6 +27,7 @@ public struct ClientConfiguration: Codable { public let savePhotosLocallyEnabled: Bool public let alreadyPaidHintEnabled: Bool public let paymentDueHintEnabled: Bool + public let unsupportedQRCodeWarningEnabled: Bool /** Creates a new `ClientConfiguration` instance. @@ -43,6 +44,7 @@ public struct ClientConfiguration: Codable { - savePhotosLocallyEnabled: A flag indicating whether saving photos locally is enabled. - alreadyPaidHintEnabled: A flag indicating whether hints for already paid invoices are enabled. - paymentDueHintEnabled: A flag indicating whether hints for upcoming payment due date is enabled. + - unsupportedQRCodeWarningEnabled: A flag indicating whether the unsupported QR code warning alert is enabled. */ public init(clientID: String, userJourneyAnalyticsEnabled: Bool, @@ -54,7 +56,8 @@ public struct ClientConfiguration: Codable { eInvoiceEnabled: Bool, savePhotosLocallyEnabled: Bool, alreadyPaidHintEnabled: Bool, - paymentDueHintEnabled: Bool) { + paymentDueHintEnabled: Bool, + unsupportedQRCodeWarningEnabled: Bool = false) { self.clientID = clientID self.userJourneyAnalyticsEnabled = userJourneyAnalyticsEnabled self.skontoEnabled = skontoEnabled @@ -66,5 +69,6 @@ public struct ClientConfiguration: Codable { self.savePhotosLocallyEnabled = savePhotosLocallyEnabled self.alreadyPaidHintEnabled = alreadyPaidHintEnabled self.paymentDueHintEnabled = paymentDueHintEnabled + self.unsupportedQRCodeWarningEnabled = unsupportedQRCodeWarningEnabled } } From 861f895560cc2840cf553b6f9eaf329830a1061f Mon Sep 17 00:00:00 2001 From: Qazi Naveed Date: Wed, 17 Jun 2026 09:35:36 +0200 Subject: [PATCH 02/26] feat(GiniCaptureSDK): add unsupportedQRCodeWarningEnabled to UserDefaults storage PP-2310 --- .../Core/Storage/GiniCaptureUserDefaultsStorage.swift | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CaptureSDK/GiniCaptureSDK/Sources/GiniCaptureSDK/Core/Storage/GiniCaptureUserDefaultsStorage.swift b/CaptureSDK/GiniCaptureSDK/Sources/GiniCaptureSDK/Core/Storage/GiniCaptureUserDefaultsStorage.swift index b0eb3d2151..b3bf860e22 100644 --- a/CaptureSDK/GiniCaptureSDK/Sources/GiniCaptureSDK/Core/Storage/GiniCaptureUserDefaultsStorage.swift +++ b/CaptureSDK/GiniCaptureSDK/Sources/GiniCaptureSDK/Core/Storage/GiniCaptureUserDefaultsStorage.swift @@ -32,6 +32,11 @@ public struct GiniCaptureUserDefaultsStorage { defaultValue: nil) public static var savePhotosLocallyEnabled: Bool? + // Configuration flag for the unsupported QR code warning alert + @GiniUserDefault("ginicapture.defaults.clientConfigurations.unsupportedQRCodeWarningEnabled", + defaultValue: nil) + public static var unsupportedQRCodeWarningEnabled: Bool? + // User preference for the Save photos locally feature @GiniUserDefault("ginicapture.defaults.userSettings.savePhotosSwitchOn", defaultValue: nil) From fdda0a34ec7a5bd759b4251cb1c1137f89f16098 Mon Sep 17 00:00:00 2001 From: Qazi Naveed Date: Wed, 17 Jun 2026 09:36:30 +0200 Subject: [PATCH 03/26] feat(GiniBankSDK): passs unsupportedQRCodeWarningEnabled from backend configuration PP-2310 --- .../Core/GiniBankNetworkingScreenApiCoordinator.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/BankSDK/GiniBankSDK/Sources/GiniBankSDK/Core/GiniBankNetworkingScreenApiCoordinator.swift b/BankSDK/GiniBankSDK/Sources/GiniBankSDK/Core/GiniBankNetworkingScreenApiCoordinator.swift index b2b8f252c1..28976368e1 100644 --- a/BankSDK/GiniBankSDK/Sources/GiniBankSDK/Core/GiniBankNetworkingScreenApiCoordinator.swift +++ b/BankSDK/GiniBankSDK/Sources/GiniBankSDK/Core/GiniBankNetworkingScreenApiCoordinator.swift @@ -306,6 +306,7 @@ open class GiniBankNetworkingScreenApiCoordinator: GiniScreenAPICoordinator, Gin GiniCaptureUserDefaultsStorage.qrCodeEducationEnabled = configuration.qrCodeEducationEnabled GiniCaptureUserDefaultsStorage.eInvoiceEnabled = configuration.eInvoiceEnabled GiniCaptureUserDefaultsStorage.savePhotosLocallyEnabled = configuration.savePhotosLocallyEnabled + GiniCaptureUserDefaultsStorage.unsupportedQRCodeWarningEnabled = configuration.unsupportedQRCodeWarningEnabled self.initializeAnalytics(with: configuration) } case .failure(let error): From e34dc255509c4a03dbfe0fabe529307dc9ab29e6 Mon Sep 17 00:00:00 2001 From: Qazi Naveed Date: Wed, 17 Jun 2026 09:52:25 +0200 Subject: [PATCH 04/26] feat(GiniCaptureSDK): add pauseQRDetection and resumeQRDetection to CameraProtocol PP-2310 --- .../Sources/GiniCaptureSDK/Core/Screens/Camera/Camera.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CaptureSDK/GiniCaptureSDK/Sources/GiniCaptureSDK/Core/Screens/Camera/Camera.swift b/CaptureSDK/GiniCaptureSDK/Sources/GiniCaptureSDK/Core/Screens/Camera/Camera.swift index 0b840079f2..e4080157ac 100644 --- a/CaptureSDK/GiniCaptureSDK/Sources/GiniCaptureSDK/Core/Screens/Camera/Camera.swift +++ b/CaptureSDK/GiniCaptureSDK/Sources/GiniCaptureSDK/Core/Screens/Camera/Camera.swift @@ -33,6 +33,8 @@ protocol CameraProtocol: AnyObject { func setupQRScanningOutput(completion: @escaping ((CameraError?) -> Void)) func start() func stop() + func pauseQRDetection() + func resumeQRDetection() // IBAN detection func startOCR() From 6c33e25306bb1d43ec83ddad82723ef3cffeda62 Mon Sep 17 00:00:00 2001 From: Qazi Naveed Date: Wed, 17 Jun 2026 09:53:45 +0200 Subject: [PATCH 05/26] feat(GiniCaptureSDK): implement pauseQRDetection and resumeQRDetection in Camera PP-2310 --- .../Core/Screens/Camera/Camera.swift | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/CaptureSDK/GiniCaptureSDK/Sources/GiniCaptureSDK/Core/Screens/Camera/Camera.swift b/CaptureSDK/GiniCaptureSDK/Sources/GiniCaptureSDK/Core/Screens/Camera/Camera.swift index e4080157ac..08702e4bc2 100644 --- a/CaptureSDK/GiniCaptureSDK/Sources/GiniCaptureSDK/Core/Screens/Camera/Camera.swift +++ b/CaptureSDK/GiniCaptureSDK/Sources/GiniCaptureSDK/Core/Screens/Camera/Camera.swift @@ -76,6 +76,9 @@ final class Camera: NSObject, CameraProtocol { fileprivate let application: UIApplication + // QR detection + private var qrMetadataOutput: AVCaptureMetadataOutput? + // IBAN detection private var request: VNImageBasedRequest? private var textOrientation = CGImagePropertyOrientation.up @@ -274,8 +277,24 @@ final class Camera: NSObject, CameraProtocol { if qrOutput.availableMetadataObjectTypes.contains(.qr) { qrOutput.metadataObjectTypes = [.qr] } + qrMetadataOutput = qrOutput session.commitConfiguration() } + + func pauseQRDetection() { + sessionQueue.async { [weak self] in + self?.qrMetadataOutput?.metadataObjectTypes = [] + } + } + + func resumeQRDetection() { + sessionQueue.async { [weak self] in + guard let self = self, + let output = self.qrMetadataOutput, + output.availableMetadataObjectTypes.contains(.qr) else { return } + output.metadataObjectTypes = [.qr] + } + } } // MARK: - Fileprivate From b4d8e16eccd9b771aac17f2fbf0b18d80b52f404 Mon Sep 17 00:00:00 2001 From: Qazi Naveed Date: Wed, 17 Jun 2026 10:00:35 +0200 Subject: [PATCH 06/26] feat(GiniCaptureSDK): add unsupported QR code alert and user action handlers to CameraViewController PP-2310 --- .../Camera/Camera/CameraViewController.swift | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/CaptureSDK/GiniCaptureSDK/Sources/GiniCaptureSDK/Core/Screens/Camera/Camera/CameraViewController.swift b/CaptureSDK/GiniCaptureSDK/Sources/GiniCaptureSDK/Core/Screens/Camera/Camera/CameraViewController.swift index bb4cc610e2..cf0468e6b3 100644 --- a/CaptureSDK/GiniCaptureSDK/Sources/GiniCaptureSDK/Core/Screens/Camera/Camera/CameraViewController.swift +++ b/CaptureSDK/GiniCaptureSDK/Sources/GiniCaptureSDK/Core/Screens/Camera/Camera/CameraViewController.swift @@ -651,6 +651,39 @@ final class CameraViewController: UIViewController { qrCodeOverLay.configureQrCodeOverlay(withCorrectQrCode: false) } + private func showUnsupportedQRCodeAlert() { + let generator = UINotificationFeedbackGenerator() + generator.notificationOccurred(.warning) + + cameraPreviewViewController.camera.pauseQRDetection() + + sendGiniAnalyticsEventForInvalidQRCode() + playVoiceOverMessage(success: false) + + let alert = UIAlertController(title: "This is not a payment QR code", + message: nil, + preferredStyle: .alert) + alert.addAction(UIAlertAction(title: "Scan another QR code", style: .default) { [weak self] _ in + self?.handleScanAnotherQRCode() + }) + alert.addAction(UIAlertAction(title: "Take photo of document", style: .default) { [weak self] _ in + self?.handleTakePhotoOfDocument() + }) + + present(alert, animated: true) + } + + private func handleScanAnotherQRCode() { + cameraPreviewViewController.camera.resumeQRDetection() + detectedQRCodeDocument = nil + } + + private func handleTakePhotoOfDocument() { + // QR detection stays paused — resuming here would immediately re-trigger the alert + cameraPreviewViewController.cameraFrameView.isHidden = false + detectedQRCodeDocument = nil + } + private func isAccessibilityLargeTextEnabled() -> Bool { let contentSizeCategory = UIApplication.shared.preferredContentSizeCategory return contentSizeCategory.isAccessibilityCategory From bceb1dee2bbc1061a96db15f0c46594e3dc2e838 Mon Sep 17 00:00:00 2001 From: Qazi Naveed Date: Wed, 17 Jun 2026 10:02:02 +0200 Subject: [PATCH 07/26] feat(GiniCaptureSDK): gate unsupported QR code alert behind backend feature flag in CameraViewController PP-2310 --- .../Camera/Camera/CameraViewController.swift | 29 ++++++++++++------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/CaptureSDK/GiniCaptureSDK/Sources/GiniCaptureSDK/Core/Screens/Camera/Camera/CameraViewController.swift b/CaptureSDK/GiniCaptureSDK/Sources/GiniCaptureSDK/Core/Screens/Camera/Camera/CameraViewController.swift index cf0468e6b3..9b894a8915 100644 --- a/CaptureSDK/GiniCaptureSDK/Sources/GiniCaptureSDK/Core/Screens/Camera/Camera/CameraViewController.swift +++ b/CaptureSDK/GiniCaptureSDK/Sources/GiniCaptureSDK/Core/Screens/Camera/Camera/CameraViewController.swift @@ -589,24 +589,31 @@ final class CameraViewController: UIViewController { resetQRCodeTask?.cancel() detectedQRCodeDocument = document - hideQRCodeTask = DispatchWorkItem(block: { - self.resetQRCodeScanning(isValid: isValid) - - if let QRDocument = self.detectedQRCodeDocument { - if isValid { + if isValid { + hideQRCodeTask = DispatchWorkItem(block: { + self.resetQRCodeScanning(isValid: true) + if let QRDocument = self.detectedQRCodeDocument { self.didPick(QRDocument) } - } - }) - - if isValid { + }) showValidQRCodeFeedback() + DispatchQueue.main.asyncAfter(deadline: .now() + 1.5, execute: hideQRCodeTask!) } else { if !isValidIBANDetected { - showInvalidQRCodeFeedback() + if GiniCaptureUserDefaultsStorage.unsupportedQRCodeWarningEnabled == true { + // Backend flag enabled: show the new unsupported QR code warning alert. + // QR detection is paused inside the alert and resumed/kept paused based on user action. + showUnsupportedQRCodeAlert() + } else { + // Backend flag disabled: show the existing yellow QR code overlay (legacy behavior). + showInvalidQRCodeFeedback() + hideQRCodeTask = DispatchWorkItem(block: { + self.resetQRCodeScanning(isValid: false) + }) + DispatchQueue.main.asyncAfter(deadline: .now() + 1.5, execute: hideQRCodeTask!) + } } } - DispatchQueue.main.asyncAfter(deadline: .now() + 1.5, execute: hideQRCodeTask!) } private func showValidQRCodeFeedback() { From 2970d071a223fed13343022419f92423fba0f0f5 Mon Sep 17 00:00:00 2001 From: Qazi Naveed Date: Thu, 18 Jun 2026 15:17:35 +0200 Subject: [PATCH 08/26] feat(GiniCaptureSDK): Update access modifier fro camera protocol PP-2310 --- .../Core/Screens/Camera/CameraPreviewViewController.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CaptureSDK/GiniCaptureSDK/Sources/GiniCaptureSDK/Core/Screens/Camera/CameraPreviewViewController.swift b/CaptureSDK/GiniCaptureSDK/Sources/GiniCaptureSDK/Core/Screens/Camera/CameraPreviewViewController.swift index d9f0cb7e4d..3c8223022d 100644 --- a/CaptureSDK/GiniCaptureSDK/Sources/GiniCaptureSDK/Core/Screens/Camera/CameraPreviewViewController.swift +++ b/CaptureSDK/GiniCaptureSDK/Sources/GiniCaptureSDK/Core/Screens/Camera/CameraPreviewViewController.swift @@ -73,7 +73,7 @@ final class CameraPreviewViewController: UIViewController { private var notAuthorizedView: UIView? private let giniConfiguration: GiniConfiguration private typealias FocusIndicator = UIImageView - private var camera: CameraProtocol + var camera: CameraProtocol private var defaultImageView: UIImageView? private var focusIndicatorImageView: UIImageView? From f8f360a082ead5d713fc696536e85846c8b92dd8 Mon Sep 17 00:00:00 2001 From: Qazi Naveed Date: Thu, 18 Jun 2026 15:18:12 +0200 Subject: [PATCH 09/26] feat(BankSDK): Code indentation PP-2310 --- .../Core/GiniBankNetworkingScreenApiCoordinator.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/BankSDK/GiniBankSDK/Sources/GiniBankSDK/Core/GiniBankNetworkingScreenApiCoordinator.swift b/BankSDK/GiniBankSDK/Sources/GiniBankSDK/Core/GiniBankNetworkingScreenApiCoordinator.swift index 28976368e1..b825da1f17 100644 --- a/BankSDK/GiniBankSDK/Sources/GiniBankSDK/Core/GiniBankNetworkingScreenApiCoordinator.swift +++ b/BankSDK/GiniBankSDK/Sources/GiniBankSDK/Core/GiniBankNetworkingScreenApiCoordinator.swift @@ -306,7 +306,8 @@ open class GiniBankNetworkingScreenApiCoordinator: GiniScreenAPICoordinator, Gin GiniCaptureUserDefaultsStorage.qrCodeEducationEnabled = configuration.qrCodeEducationEnabled GiniCaptureUserDefaultsStorage.eInvoiceEnabled = configuration.eInvoiceEnabled GiniCaptureUserDefaultsStorage.savePhotosLocallyEnabled = configuration.savePhotosLocallyEnabled - GiniCaptureUserDefaultsStorage.unsupportedQRCodeWarningEnabled = configuration.unsupportedQRCodeWarningEnabled + GiniCaptureUserDefaultsStorage.unsupportedQRCodeWarningEnabled = + configuration.unsupportedQRCodeWarningEnabled self.initializeAnalytics(with: configuration) } case .failure(let error): From aa4f9f6dc63c0a9cf34f401c4b00ed53cdc0ce9f Mon Sep 17 00:00:00 2001 From: Qazi Naveed Date: Fri, 19 Jun 2026 15:57:01 +0200 Subject: [PATCH 10/26] fix(GiniCaptureSDK): snapshot unsupported QR code flag once per session PP-2310 --- .../Camera/Camera/CameraViewController.swift | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/CaptureSDK/GiniCaptureSDK/Sources/GiniCaptureSDK/Core/Screens/Camera/Camera/CameraViewController.swift b/CaptureSDK/GiniCaptureSDK/Sources/GiniCaptureSDK/Core/Screens/Camera/Camera/CameraViewController.swift index 9b894a8915..2beb0d8676 100644 --- a/CaptureSDK/GiniCaptureSDK/Sources/GiniCaptureSDK/Core/Screens/Camera/Camera/CameraViewController.swift +++ b/CaptureSDK/GiniCaptureSDK/Sources/GiniCaptureSDK/Core/Screens/Camera/Camera/CameraViewController.swift @@ -46,6 +46,10 @@ final class CameraViewController: UIViewController { private var isPresentedOnScreen = false private var isValidIBANDetected: Bool = false + // Snapshot of the backend flag taken on the first invalid QR scan. + // Stays fixed for the session so all repeated scans show the same feedback type. + private var sessionUnsupportedQRCodeWarningEnabled: Bool? + private var isWarningFlagSnapshotted = false // Analytics private var invalidQRCodeOverlayFirstAppearance: Bool = true private var ibanOverlayFirstAppearance: Bool = true @@ -600,12 +604,18 @@ final class CameraViewController: UIViewController { DispatchQueue.main.asyncAfter(deadline: .now() + 1.5, execute: hideQRCodeTask!) } else { if !isValidIBANDetected { - if GiniCaptureUserDefaultsStorage.unsupportedQRCodeWarningEnabled == true { - // Backend flag enabled: show the new unsupported QR code warning alert. - // QR detection is paused inside the alert and resumed/kept paused based on user action. + // Snapshot the flag once so the feedback type stays consistent across + // repeated scans in the same session. A separate boolean guards the snapshot + // because assigning nil to a Bool? still leaves it nil, making a nil-check + // unreliable as a "has snapshotted" gate. + if !isWarningFlagSnapshotted { + sessionUnsupportedQRCodeWarningEnabled = GiniCaptureUserDefaultsStorage.unsupportedQRCodeWarningEnabled + isWarningFlagSnapshotted = true + } + + if sessionUnsupportedQRCodeWarningEnabled == true { showUnsupportedQRCodeAlert() } else { - // Backend flag disabled: show the existing yellow QR code overlay (legacy behavior). showInvalidQRCodeFeedback() hideQRCodeTask = DispatchWorkItem(block: { self.resetQRCodeScanning(isValid: false) From 754b5b50c55f7afc3da69f5d79cd438fe5cd057f Mon Sep 17 00:00:00 2001 From: Qazi Naveed Date: Fri, 19 Jun 2026 17:11:16 +0200 Subject: [PATCH 11/26] fix(GiniBankSDK): Added localized strings PP-2310 --- .../GiniCaptureSDK/Resources/de.lproj/Localizable.strings | 3 +++ .../GiniCaptureSDK/Resources/en.lproj/Localizable.strings | 3 +++ 2 files changed, 6 insertions(+) diff --git a/CaptureSDK/GiniCaptureSDK/Sources/GiniCaptureSDK/Resources/de.lproj/Localizable.strings b/CaptureSDK/GiniCaptureSDK/Sources/GiniCaptureSDK/Resources/de.lproj/Localizable.strings index 599019983a..a8072a3a4d 100644 --- a/CaptureSDK/GiniCaptureSDK/Sources/GiniCaptureSDK/Resources/de.lproj/Localizable.strings +++ b/CaptureSDK/GiniCaptureSDK/Sources/GiniCaptureSDK/Resources/de.lproj/Localizable.strings @@ -220,6 +220,9 @@ "ginicapture.QRscanning.correct" = "QR-Code erkannt"; "ginicapture.QRscanning.incorrect.title" = "Unbekannter QR-Code"; "ginicapture.QRscanning.incorrect.description" = "Dieser Code enthält keine Zahlungsinformationen. Bitte anderen QR-Code oder Rechnung abfotografieren."; +"ginicapture.QRscanning.alert.title" = "Dieser QR-Code ist kein Zahlungs-QR-Code"; +"ginicapture.QRscanning.alert.scanAnother" = "Anderen QR-Code scannen"; +"ginicapture.QRscanning.alert.takePhoto" = "Dokument fotografieren"; "ginicapture.QRscanning.loading" = "Rechnung wird übermittelt"; "ginicapture.QRscanning.education.loading.captureTip" = "Fotografieren Sie direkt ein Zahlungsformular oder eine Rechnung - auch ohne QR-Code"; diff --git a/CaptureSDK/GiniCaptureSDK/Sources/GiniCaptureSDK/Resources/en.lproj/Localizable.strings b/CaptureSDK/GiniCaptureSDK/Sources/GiniCaptureSDK/Resources/en.lproj/Localizable.strings index 01ddb464e7..af330464f6 100644 --- a/CaptureSDK/GiniCaptureSDK/Sources/GiniCaptureSDK/Resources/en.lproj/Localizable.strings +++ b/CaptureSDK/GiniCaptureSDK/Sources/GiniCaptureSDK/Resources/en.lproj/Localizable.strings @@ -217,6 +217,9 @@ "ginicapture.QRscanning.correct" = "QR code detected"; "ginicapture.QRscanning.incorrect.title" = "Unknown QR code"; "ginicapture.QRscanning.incorrect.description" = "This code does not contain any payment details. Please use another QR code or take an image of your invoice."; +"ginicapture.QRscanning.alert.title" = "This is not a payment QR code"; +"ginicapture.QRscanning.alert.scanAnother" = "Scan another QR code"; +"ginicapture.QRscanning.alert.takePhoto" = "Take photo of document"; "ginicapture.QRscanning.loading" = "Retrieving invoice"; "ginicapture.QRscanning.education.loading.captureTip" = "You can take photos of receipts or remittance slips – even without a QR code"; From 4fdb09267b543e259ce445412635664966cbc792 Mon Sep 17 00:00:00 2001 From: Qazi Naveed Date: Fri, 19 Jun 2026 17:18:39 +0200 Subject: [PATCH 12/26] feat(GiniBankSDK): Update strings via localized string PP-2310 --- .../Camera/Camera/CameraViewController.swift | 21 ++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/CaptureSDK/GiniCaptureSDK/Sources/GiniCaptureSDK/Core/Screens/Camera/Camera/CameraViewController.swift b/CaptureSDK/GiniCaptureSDK/Sources/GiniCaptureSDK/Core/Screens/Camera/Camera/CameraViewController.swift index 2beb0d8676..9a4102841c 100644 --- a/CaptureSDK/GiniCaptureSDK/Sources/GiniCaptureSDK/Core/Screens/Camera/Camera/CameraViewController.swift +++ b/CaptureSDK/GiniCaptureSDK/Sources/GiniCaptureSDK/Core/Screens/Camera/Camera/CameraViewController.swift @@ -677,13 +677,13 @@ final class CameraViewController: UIViewController { sendGiniAnalyticsEventForInvalidQRCode() playVoiceOverMessage(success: false) - let alert = UIAlertController(title: "This is not a payment QR code", + let alert = UIAlertController(title: Strings.unsupportedQRAlertTitle, message: nil, preferredStyle: .alert) - alert.addAction(UIAlertAction(title: "Scan another QR code", style: .default) { [weak self] _ in + alert.addAction(UIAlertAction(title: Strings.scanAnotherQRCode, style: .default) { [weak self] _ in self?.handleScanAnotherQRCode() }) - alert.addAction(UIAlertAction(title: "Take photo of document", style: .default) { [weak self] _ in + alert.addAction(UIAlertAction(title: Strings.takePhotoOfDocument, style: .default) { [weak self] _ in self?.handleTakePhotoOfDocument() }) @@ -828,6 +828,21 @@ private extension CameraViewController { comment: "Info label") static let cameraTitle = NSLocalizedStringPreferredFormat("ginicapture.navigationbar.camera.title", comment: "Camera title") + + static let unsupportedQRAlertTitleKey = "ginicapture.QRscanning.alert.title" + static let unsupportedQRAlertTitleComment = "Unsupported QR code alert title" + static let unsupportedQRAlertTitle = NSLocalizedStringPreferredFormat(unsupportedQRAlertTitleKey, + comment: unsupportedQRAlertTitleComment) + + static let scanAnotherQRCodeKey = "ginicapture.QRscanning.alert.scanAnother" + static let scanAnotherQRCodeComment = "Scan another QR code button" + static let scanAnotherQRCode = NSLocalizedStringPreferredFormat(scanAnotherQRCodeKey, + comment: scanAnotherQRCodeComment) + + static let takePhotoOfDocumentKey = "ginicapture.QRscanning.alert.takePhoto" + static let takePhotoOfDocumentComment = "Take photo of document button" + static let takePhotoOfDocument = NSLocalizedStringPreferredFormat(takePhotoOfDocumentKey, + comment: takePhotoOfDocumentComment) } } // swiftlint:enable type_body_length From 1fe374b9753b365dea46e55cef132a83ff9b5fc1 Mon Sep 17 00:00:00 2001 From: Qazi Naveed Date: Fri, 19 Jun 2026 18:21:41 +0200 Subject: [PATCH 13/26] feat(GiniBankAPILibrary): add unsupportedQRCodeWarningEnabled to clientConfiguration test fixture PP-2310 --- .../GiniBankAPILibraryTests/Resources/clientConfiguration.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/BankAPILibrary/GiniBankAPILibrary/Tests/GiniBankAPILibraryTests/Resources/clientConfiguration.json b/BankAPILibrary/GiniBankAPILibrary/Tests/GiniBankAPILibraryTests/Resources/clientConfiguration.json index 34095c26eb..875870e826 100644 --- a/BankAPILibrary/GiniBankAPILibrary/Tests/GiniBankAPILibraryTests/Resources/clientConfiguration.json +++ b/BankAPILibrary/GiniBankAPILibrary/Tests/GiniBankAPILibraryTests/Resources/clientConfiguration.json @@ -9,5 +9,6 @@ "eInvoiceEnabled": false, "savePhotosLocallyEnabled": false, "alreadyPaidHintEnabled": false, - "paymentDueHintEnabled": false + "paymentDueHintEnabled": false, + "unsupportedQRCodeWarningEnabled": false } From 9295a09591bacb876318f96ccde8e7d00dfd2b04 Mon Sep 17 00:00:00 2001 From: Qazi Naveed Date: Fri, 19 Jun 2026 18:28:54 +0200 Subject: [PATCH 14/26] test(GiniBankAPILibrary): add unsupportedQRCodeWarningEnabled coverage to ClientConfigurationTests PP-2310 --- .../ClientConfigurationTests.swift | 20 +++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/BankAPILibrary/GiniBankAPILibrary/Tests/GiniBankAPILibraryTests/ClientConfigurationTests.swift b/BankAPILibrary/GiniBankAPILibrary/Tests/GiniBankAPILibraryTests/ClientConfigurationTests.swift index 5f1de3ff46..80eab7c7e9 100644 --- a/BankAPILibrary/GiniBankAPILibrary/Tests/GiniBankAPILibraryTests/ClientConfigurationTests.swift +++ b/BankAPILibrary/GiniBankAPILibrary/Tests/GiniBankAPILibraryTests/ClientConfigurationTests.swift @@ -26,7 +26,8 @@ struct ClientConfigurationTests { eInvoiceEnabled: true, savePhotosLocallyEnabled: true, alreadyPaidHintEnabled: true, - paymentDueHintEnabled: true) + paymentDueHintEnabled: true, + unsupportedQRCodeWarningEnabled: true) #expect(config.clientID == testClientID, "Expected clientID to be \(testClientID)") #expect(config.userJourneyAnalyticsEnabled, "Expected userJourneyAnalyticsEnabled to be true") @@ -38,6 +39,7 @@ struct ClientConfigurationTests { #expect(config.eInvoiceEnabled, "Expected eInvoiceEnabled to be true") #expect(config.alreadyPaidHintEnabled, "Expected alreadyPaidHintEnabled to be true") #expect(config.savePhotosLocallyEnabled, "Expected savePhotosLocallyEnabled to be true") + #expect(config.unsupportedQRCodeWarningEnabled, "Expected unsupportedQRCodeWarningEnabled to be true") } @Test("Initialization with all flags disabled") @@ -52,9 +54,8 @@ struct ClientConfigurationTests { eInvoiceEnabled: false, savePhotosLocallyEnabled: false, alreadyPaidHintEnabled: false, - paymentDueHintEnabled: false) - - + paymentDueHintEnabled: false, + unsupportedQRCodeWarningEnabled: false) #expect(config.clientID == testClientID, "Expected clientID to be \(testClientID)") #expect(!config.userJourneyAnalyticsEnabled, "Expected userJourneyAnalyticsEnabled to be false") @@ -66,6 +67,7 @@ struct ClientConfigurationTests { #expect(!config.eInvoiceEnabled, "Expected eInvoiceEnabled to be false") #expect(!config.alreadyPaidHintEnabled, "Expected alreadyPaidHintEnabled to be false") #expect(!config.savePhotosLocallyEnabled, "Expected savePhotosLocallyEnabled to be false") + #expect(!config.unsupportedQRCodeWarningEnabled, "Expected unsupportedQRCodeWarningEnabled to be false") } // MARK: - JSON Decoding Tests @@ -87,6 +89,7 @@ struct ClientConfigurationTests { #expect(!config.eInvoiceEnabled, "Expected eInvoiceEnabled to be false from JSON") #expect(!config.alreadyPaidHintEnabled, "Expected alreadyPaidHintEnabled to be false from JSON") #expect(!config.savePhotosLocallyEnabled, "Expected savePhotosLocallyEnabled to be false from JSON") + #expect(!config.unsupportedQRCodeWarningEnabled, "Expected unsupportedQRCodeWarningEnabled to be false from JSON") } @Test("Decoding fails when missing required clientID field") @@ -113,7 +116,8 @@ struct ClientConfigurationTests { eInvoiceEnabled: true, savePhotosLocallyEnabled: true, alreadyPaidHintEnabled: true, - paymentDueHintEnabled: true) + paymentDueHintEnabled: true, + unsupportedQRCodeWarningEnabled: true) let encoder = JSONEncoder() @@ -140,6 +144,8 @@ struct ClientConfigurationTests { "Expected alreadyPaidHintEnabled to be preserved") #expect(decodedConfig.savePhotosLocallyEnabled == config.savePhotosLocallyEnabled, "Expected savePhotosLocallyEnabled to be preserved") + #expect(decodedConfig.unsupportedQRCodeWarningEnabled == config.unsupportedQRCodeWarningEnabled, + "Expected unsupportedQRCodeWarningEnabled to be preserved") } // MARK: - Property Combinations Tests @@ -156,7 +162,8 @@ struct ClientConfigurationTests { eInvoiceEnabled: false, savePhotosLocallyEnabled: false, alreadyPaidHintEnabled: true, - paymentDueHintEnabled: true) + paymentDueHintEnabled: true, + unsupportedQRCodeWarningEnabled: true) #expect(config.userJourneyAnalyticsEnabled, "Expected userJourneyAnalyticsEnabled to be true") #expect(!config.skontoEnabled, "Expected skontoEnabled to be false") @@ -167,5 +174,6 @@ struct ClientConfigurationTests { #expect(!config.eInvoiceEnabled, "Expected eInvoiceEnabled to be false") #expect(config.alreadyPaidHintEnabled, "Expected alreadyPaidHintEnabled to be true") #expect(!config.savePhotosLocallyEnabled, "Expected savePhotosLocallyEnabled to be false") + #expect(config.unsupportedQRCodeWarningEnabled, "Expected unsupportedQRCodeWarningEnabled to be true") } } From 25249e198a5973901ab6cbcc738ae158453be12e Mon Sep 17 00:00:00 2001 From: Qazi Naveed Date: Mon, 22 Jun 2026 17:11:19 +0200 Subject: [PATCH 15/26] fix(GiniBankSDK): Fix CameraMock conformance to CameraProtocol PP-2310 --- .../Tests/GiniBankSDKTests/Helpers/CameraMock.swift | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/BankSDK/GiniBankSDK/Tests/GiniBankSDKTests/Helpers/CameraMock.swift b/BankSDK/GiniBankSDK/Tests/GiniBankSDKTests/Helpers/CameraMock.swift index cac1999e03..7bef926a94 100644 --- a/BankSDK/GiniBankSDK/Tests/GiniBankSDKTests/Helpers/CameraMock.swift +++ b/BankSDK/GiniBankSDK/Tests/GiniBankSDKTests/Helpers/CameraMock.swift @@ -56,7 +56,7 @@ final class CameraMock: CameraProtocol { // This method will remain empty; no implementation is needed. } - func setup(completion: ((CameraError?) -> Void)) { + func setup(completion: @escaping ((CameraError?) -> Void)) { switch state { case .authorized: completion(nil) @@ -76,4 +76,12 @@ final class CameraMock: CameraProtocol { func stop() { // This method will remain empty; no implementation is needed. } + + func pauseQRDetection() { + // This method will remain empty; no implementation is needed. + } + + func resumeQRDetection() { + // This method will remain empty; no implementation is needed. + } } From 8f2d97f8f0b8b662956013c4146576dec064b79e Mon Sep 17 00:00:00 2001 From: Qazi Naveed Date: Mon, 22 Jun 2026 17:20:23 +0200 Subject: [PATCH 16/26] fix(GiniCaptureSDK): Fix CameraMock conformance to CameraProtocol PP-2310 --- .../Tests/GiniCaptureSDKTests/CameraMock.swift | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/CaptureSDK/GiniCaptureSDK/Tests/GiniCaptureSDKTests/CameraMock.swift b/CaptureSDK/GiniCaptureSDK/Tests/GiniCaptureSDKTests/CameraMock.swift index 750126be08..65fd73e0b1 100644 --- a/CaptureSDK/GiniCaptureSDK/Tests/GiniCaptureSDKTests/CameraMock.swift +++ b/CaptureSDK/GiniCaptureSDK/Tests/GiniCaptureSDKTests/CameraMock.swift @@ -10,6 +10,14 @@ import AVFoundation @testable import GiniCaptureSDK final class CameraMock: CameraProtocol { + func pauseQRDetection() { + // This method will remain empty; no implementation is needed. + } + + func resumeQRDetection() { + // This method will remain empty; no implementation is needed. + } + func setupIBANDetection(textOrientation: CGImagePropertyOrientation, regionOfInterest: CGRect?, videoPreviewLayer: AVCaptureVideoPreviewLayer?, @@ -57,7 +65,7 @@ final class CameraMock: CameraProtocol { // This method will remain empty; no implementation is needed. } - func setup(completion: ((CameraError?) -> Void)) { + func setup(completion: @escaping ((CameraError?) -> Void)) { switch state { case .authorized: completion(nil) From bc2f62cc0756886fdd9d3fff0a4fb4007a7ceaf6 Mon Sep 17 00:00:00 2001 From: Qazi Naveed Date: Mon, 22 Jun 2026 17:30:36 +0200 Subject: [PATCH 17/26] refactor(GiniBankAPILibrary): remove default value from unsupportedQRCodeWarningEnabled init param PP-2310 --- .../Documents/Configuration/ClientConfiguration.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/BankAPILibrary/GiniBankAPILibrary/Sources/GiniBankAPILibrary/Documents/Configuration/ClientConfiguration.swift b/BankAPILibrary/GiniBankAPILibrary/Sources/GiniBankAPILibrary/Documents/Configuration/ClientConfiguration.swift index b15eff7f6d..de4cdfaf1b 100644 --- a/BankAPILibrary/GiniBankAPILibrary/Sources/GiniBankAPILibrary/Documents/Configuration/ClientConfiguration.swift +++ b/BankAPILibrary/GiniBankAPILibrary/Sources/GiniBankAPILibrary/Documents/Configuration/ClientConfiguration.swift @@ -57,7 +57,7 @@ public struct ClientConfiguration: Codable { savePhotosLocallyEnabled: Bool, alreadyPaidHintEnabled: Bool, paymentDueHintEnabled: Bool, - unsupportedQRCodeWarningEnabled: Bool = false) { + unsupportedQRCodeWarningEnabled: Bool) { self.clientID = clientID self.userJourneyAnalyticsEnabled = userJourneyAnalyticsEnabled self.skontoEnabled = skontoEnabled From d39e9926ab4e83976aa6b7c935536470a9a56891 Mon Sep 17 00:00:00 2001 From: Qazi Naveed Date: Mon, 22 Jun 2026 17:41:56 +0200 Subject: [PATCH 18/26] refactor(GiniCaptureSDK): restrict camera property to private(set) in CameraPreviewViewController PP-2310 --- .../Core/Screens/Camera/CameraPreviewViewController.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CaptureSDK/GiniCaptureSDK/Sources/GiniCaptureSDK/Core/Screens/Camera/CameraPreviewViewController.swift b/CaptureSDK/GiniCaptureSDK/Sources/GiniCaptureSDK/Core/Screens/Camera/CameraPreviewViewController.swift index 3c8223022d..ea91628071 100644 --- a/CaptureSDK/GiniCaptureSDK/Sources/GiniCaptureSDK/Core/Screens/Camera/CameraPreviewViewController.swift +++ b/CaptureSDK/GiniCaptureSDK/Sources/GiniCaptureSDK/Core/Screens/Camera/CameraPreviewViewController.swift @@ -73,7 +73,7 @@ final class CameraPreviewViewController: UIViewController { private var notAuthorizedView: UIView? private let giniConfiguration: GiniConfiguration private typealias FocusIndicator = UIImageView - var camera: CameraProtocol + private(set) var camera: CameraProtocol private var defaultImageView: UIImageView? private var focusIndicatorImageView: UIImageView? From bebc167f4d6f52a61014b3cde423cc7c02bba508 Mon Sep 17 00:00:00 2001 From: Qazi Naveed Date: Mon, 22 Jun 2026 18:43:08 +0200 Subject: [PATCH 19/26] test(GiniCaptureSDK): add coverage for pauseQRDetection and resumeQRDetection in Camera PP-2310 --- .../CameraPreviewViewControllerTests.swift | 68 ++++++++++++++++++- 1 file changed, 66 insertions(+), 2 deletions(-) diff --git a/CaptureSDK/GiniCaptureSDK/Tests/GiniCaptureSDKTests/CameraPreviewViewControllerTests.swift b/CaptureSDK/GiniCaptureSDK/Tests/GiniCaptureSDKTests/CameraPreviewViewControllerTests.swift index 8244139430..7b555df2bc 100644 --- a/CaptureSDK/GiniCaptureSDK/Tests/GiniCaptureSDKTests/CameraPreviewViewControllerTests.swift +++ b/CaptureSDK/GiniCaptureSDK/Tests/GiniCaptureSDKTests/CameraPreviewViewControllerTests.swift @@ -83,12 +83,76 @@ final class CameraPreviewViewControllerTests: XCTestCase { let defaultFlashState = camera.isFlashOn let giniConfiguration = GiniConfiguration() giniConfiguration.flashToggleEnabled = true - + cameraPreviewViewController = CameraPreviewViewController(giniConfiguration: giniConfiguration, camera: camera) _ = cameraPreviewViewController.view cameraPreviewViewController.isFlashOn = false - + XCTAssertNotEqual(defaultFlashState, camera.isFlashOn, "camera flash state should change it after toggle it") } + + // MARK: - QR Detection Pause/Resume + + private func makeCamera() -> Camera { + return Camera(giniConfiguration: GiniConfiguration()) + } + + private func flushSessionQueue(_ camera: Camera, timeout: TimeInterval = 2.0) { + let expect = expectation(description: "session queue flushed") + camera.sessionQueue.async { + expect.fulfill() + } + wait(for: [expect], timeout: timeout) + } + + private func setupQROutput(on camera: Camera, timeout: TimeInterval = 2.0) { + let expect = expectation(description: "QR scanning output configured") + camera.setupQRScanningOutput { _ in + expect.fulfill() + } + wait(for: [expect], timeout: timeout) + } + + private func metadataOutput(of camera: Camera) -> AVCaptureMetadataOutput? { + return camera.session.outputs.compactMap { $0 as? AVCaptureMetadataOutput }.first + } + + func testPauseQRDetectionClearsMetadataObjectTypes() { + let camera = makeCamera() + setupQROutput(on: camera) + + camera.pauseQRDetection() + flushSessionQueue(camera) + + let output = metadataOutput(of: camera) + XCTAssertNotNil(output, "QR metadata output should exist after setupQRScanningOutput") + XCTAssertTrue(output?.metadataObjectTypes.isEmpty ?? false, + "metadataObjectTypes should be empty after pauseQRDetection") + } + + func testPauseQRDetectionBeforeSetupDoesNotCrash() { + let camera = makeCamera() + + camera.pauseQRDetection() + flushSessionQueue(camera) + } + + func testResumeQRDetectionAfterPauseDoesNotCrash() { + let camera = makeCamera() + setupQROutput(on: camera) + + camera.pauseQRDetection() + flushSessionQueue(camera) + + camera.resumeQRDetection() + flushSessionQueue(camera) + } + + func testResumeQRDetectionBeforeSetupDoesNotCrash() { + let camera = makeCamera() + + camera.resumeQRDetection() + flushSessionQueue(camera) + } } From 156de27c58ca929ead6fd51e0e67231cdbe1c66f Mon Sep 17 00:00:00 2001 From: Qazi Naveed Date: Mon, 22 Jun 2026 18:52:59 +0200 Subject: [PATCH 20/26] test(GiniBankSDK): pass unsupportedQRCodeWarningEnabled to ClientConfiguration test helper PP-2310 --- .../GiniBankSDKTests/NetworkingScreenApiCoordinatorTests.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/BankSDK/GiniBankSDK/Tests/GiniBankSDKTests/NetworkingScreenApiCoordinatorTests.swift b/BankSDK/GiniBankSDK/Tests/GiniBankSDKTests/NetworkingScreenApiCoordinatorTests.swift index a10b40bd6b..4e1e3b6c4c 100644 --- a/BankSDK/GiniBankSDK/Tests/GiniBankSDKTests/NetworkingScreenApiCoordinatorTests.swift +++ b/BankSDK/GiniBankSDK/Tests/GiniBankSDKTests/NetworkingScreenApiCoordinatorTests.swift @@ -362,6 +362,7 @@ extension ClientConfiguration { eInvoiceEnabled: false, savePhotosLocallyEnabled: false, alreadyPaidHintEnabled: alreadyPaidHintEnabled, - paymentDueHintEnabled: paymentDueHintEnabled) + paymentDueHintEnabled: paymentDueHintEnabled, + unsupportedQRCodeWarningEnabled: false) } } From 46a171e8f1cf41e9b85833d358d5351cb0cf0dd2 Mon Sep 17 00:00:00 2001 From: Qazi Naveed Date: Mon, 22 Jun 2026 20:13:24 +0200 Subject: [PATCH 21/26] test(GiniCaptureSDK): remove flaky QR detection tests that rely on setupQRScanningOutput PP-2310 --- .../CameraPreviewViewControllerTests.swift | 47 +------------------ 1 file changed, 2 insertions(+), 45 deletions(-) diff --git a/CaptureSDK/GiniCaptureSDK/Tests/GiniCaptureSDKTests/CameraPreviewViewControllerTests.swift b/CaptureSDK/GiniCaptureSDK/Tests/GiniCaptureSDKTests/CameraPreviewViewControllerTests.swift index 7b555df2bc..2d173c2b14 100644 --- a/CaptureSDK/GiniCaptureSDK/Tests/GiniCaptureSDKTests/CameraPreviewViewControllerTests.swift +++ b/CaptureSDK/GiniCaptureSDK/Tests/GiniCaptureSDKTests/CameraPreviewViewControllerTests.swift @@ -50,16 +50,9 @@ final class CameraPreviewViewControllerTests: XCTestCase { cameraPreviewViewController = CameraPreviewViewController(giniConfiguration: giniConfiguration) _ = cameraPreviewViewController.view let bottomAnchor = cameraPreviewViewController.view.bottomAnchor + // Verifies setupCamera does not crash when QR scanning is enabled. + // QR metadata output is configured separately via setupQRScanningOutput (called by CameraViewController). cameraPreviewViewController.setupCamera(bottomAnchor: bottomAnchor) - - DispatchQueue.main.async { - let metadataOutput = self.cameraPreviewViewController.previewView.session - .outputs - .compactMap { $0 as? AVCaptureMetadataOutput } - .first - - XCTAssertNotNil(metadataOutput, "the camera session should have the metadata output") - } } func testCaptureImage() { @@ -106,31 +99,6 @@ final class CameraPreviewViewControllerTests: XCTestCase { wait(for: [expect], timeout: timeout) } - private func setupQROutput(on camera: Camera, timeout: TimeInterval = 2.0) { - let expect = expectation(description: "QR scanning output configured") - camera.setupQRScanningOutput { _ in - expect.fulfill() - } - wait(for: [expect], timeout: timeout) - } - - private func metadataOutput(of camera: Camera) -> AVCaptureMetadataOutput? { - return camera.session.outputs.compactMap { $0 as? AVCaptureMetadataOutput }.first - } - - func testPauseQRDetectionClearsMetadataObjectTypes() { - let camera = makeCamera() - setupQROutput(on: camera) - - camera.pauseQRDetection() - flushSessionQueue(camera) - - let output = metadataOutput(of: camera) - XCTAssertNotNil(output, "QR metadata output should exist after setupQRScanningOutput") - XCTAssertTrue(output?.metadataObjectTypes.isEmpty ?? false, - "metadataObjectTypes should be empty after pauseQRDetection") - } - func testPauseQRDetectionBeforeSetupDoesNotCrash() { let camera = makeCamera() @@ -138,17 +106,6 @@ final class CameraPreviewViewControllerTests: XCTestCase { flushSessionQueue(camera) } - func testResumeQRDetectionAfterPauseDoesNotCrash() { - let camera = makeCamera() - setupQROutput(on: camera) - - camera.pauseQRDetection() - flushSessionQueue(camera) - - camera.resumeQRDetection() - flushSessionQueue(camera) - } - func testResumeQRDetectionBeforeSetupDoesNotCrash() { let camera = makeCamera() From cf9f57e300f1b0b498358dd4ab019ef9c2410af1 Mon Sep 17 00:00:00 2001 From: Qazi Naveed Date: Mon, 22 Jun 2026 20:39:35 +0200 Subject: [PATCH 22/26] test(GiniCaptureSDK): add private(set) qrMetadataOutput with test-only setter for pause/resume coverage PP-2310 --- .../Core/Screens/Camera/Camera.swift | 8 +++++- .../CameraPreviewViewControllerTests.swift | 26 +++++++++++++++++++ 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/CaptureSDK/GiniCaptureSDK/Sources/GiniCaptureSDK/Core/Screens/Camera/Camera.swift b/CaptureSDK/GiniCaptureSDK/Sources/GiniCaptureSDK/Core/Screens/Camera/Camera.swift index 08702e4bc2..374495da78 100644 --- a/CaptureSDK/GiniCaptureSDK/Sources/GiniCaptureSDK/Core/Screens/Camera/Camera.swift +++ b/CaptureSDK/GiniCaptureSDK/Sources/GiniCaptureSDK/Core/Screens/Camera/Camera.swift @@ -77,7 +77,7 @@ final class Camera: NSObject, CameraProtocol { fileprivate let application: UIApplication // QR detection - private var qrMetadataOutput: AVCaptureMetadataOutput? + private(set) var qrMetadataOutput: AVCaptureMetadataOutput? // IBAN detection private var request: VNImageBasedRequest? @@ -295,6 +295,12 @@ final class Camera: NSObject, CameraProtocol { output.metadataObjectTypes = [.qr] } } + + // Intended for unit tests only — injects a metadata output without going + // through full session setup, which is unstable on CI simulators. + func setQRMetadataOutputForTesting(_ output: AVCaptureMetadataOutput?) { + qrMetadataOutput = output + } } // MARK: - Fileprivate diff --git a/CaptureSDK/GiniCaptureSDK/Tests/GiniCaptureSDKTests/CameraPreviewViewControllerTests.swift b/CaptureSDK/GiniCaptureSDK/Tests/GiniCaptureSDKTests/CameraPreviewViewControllerTests.swift index 2d173c2b14..6f60735f1a 100644 --- a/CaptureSDK/GiniCaptureSDK/Tests/GiniCaptureSDKTests/CameraPreviewViewControllerTests.swift +++ b/CaptureSDK/GiniCaptureSDK/Tests/GiniCaptureSDKTests/CameraPreviewViewControllerTests.swift @@ -112,4 +112,30 @@ final class CameraPreviewViewControllerTests: XCTestCase { camera.resumeQRDetection() flushSessionQueue(camera) } + + func testPauseQRDetectionClearsMetadataObjectTypes() { + let camera = makeCamera() + let output = AVCaptureMetadataOutput() + camera.setQRMetadataOutputForTesting(output) + + camera.pauseQRDetection() + flushSessionQueue(camera) + + XCTAssertTrue(output.metadataObjectTypes.isEmpty, + "metadataObjectTypes should be empty after pauseQRDetection") + } + + func testResumeQRDetectionGuardsAgainstUnavailableQRType() { + let camera = makeCamera() + let output = AVCaptureMetadataOutput() + camera.setQRMetadataOutputForTesting(output) + + camera.resumeQRDetection() + flushSessionQueue(camera) + + // On CI simulators .qr is not in availableMetadataObjectTypes, so the guard + // returns and metadataObjectTypes stays at its default (empty). + XCTAssertTrue(output.metadataObjectTypes.isEmpty, + "metadataObjectTypes should remain unchanged when .qr is unavailable") + } } From 17f68bc110e12a2b7dc76cfc48541767b3907fe4 Mon Sep 17 00:00:00 2001 From: Qazi Naveed Date: Tue, 23 Jun 2026 12:16:28 +0200 Subject: [PATCH 23/26] test(GiniCaptureSDK): cover qrMetadataOutput assignment via setupQRScanningOutput PP-2310 --- .../CameraPreviewViewControllerTests.swift | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/CaptureSDK/GiniCaptureSDK/Tests/GiniCaptureSDKTests/CameraPreviewViewControllerTests.swift b/CaptureSDK/GiniCaptureSDK/Tests/GiniCaptureSDKTests/CameraPreviewViewControllerTests.swift index 6f60735f1a..5824754bac 100644 --- a/CaptureSDK/GiniCaptureSDK/Tests/GiniCaptureSDKTests/CameraPreviewViewControllerTests.swift +++ b/CaptureSDK/GiniCaptureSDK/Tests/GiniCaptureSDKTests/CameraPreviewViewControllerTests.swift @@ -138,4 +138,16 @@ final class CameraPreviewViewControllerTests: XCTestCase { XCTAssertTrue(output.metadataObjectTypes.isEmpty, "metadataObjectTypes should remain unchanged when .qr is unavailable") } + + func testSetupQRScanningOutputAssignsMetadataOutput() { + let camera = makeCamera() + + // Trigger setup but don't wait for the main-queue completion callback — + // just flush the serial sessionQueue, which runs after configureQROutput finishes. + camera.setupQRScanningOutput { _ in } + flushSessionQueue(camera, timeout: 10.0) + + XCTAssertNotNil(camera.qrMetadataOutput, + "qrMetadataOutput should be assigned after configureQROutput runs") + } } From 5f41f4971b7d804a05bfe08b5451500ac1bc6314 Mon Sep 17 00:00:00 2001 From: Qazi Naveed Date: Wed, 24 Jun 2026 14:34:56 +0200 Subject: [PATCH 24/26] refactor(GiniCaptureSDK): remove force unwrap of hideQRCodeTask in showQRCodeFeedback - Added Constants.hideQRCodeDelay PP-2310 --- .../Screens/Camera/Camera/CameraViewController.swift | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/CaptureSDK/GiniCaptureSDK/Sources/GiniCaptureSDK/Core/Screens/Camera/Camera/CameraViewController.swift b/CaptureSDK/GiniCaptureSDK/Sources/GiniCaptureSDK/Core/Screens/Camera/Camera/CameraViewController.swift index 9a4102841c..6d4d3aa55f 100644 --- a/CaptureSDK/GiniCaptureSDK/Sources/GiniCaptureSDK/Core/Screens/Camera/Camera/CameraViewController.swift +++ b/CaptureSDK/GiniCaptureSDK/Sources/GiniCaptureSDK/Core/Screens/Camera/Camera/CameraViewController.swift @@ -594,14 +594,15 @@ final class CameraViewController: UIViewController { detectedQRCodeDocument = document if isValid { - hideQRCodeTask = DispatchWorkItem(block: { + let task = DispatchWorkItem(block: { self.resetQRCodeScanning(isValid: true) if let QRDocument = self.detectedQRCodeDocument { self.didPick(QRDocument) } }) + hideQRCodeTask = task showValidQRCodeFeedback() - DispatchQueue.main.asyncAfter(deadline: .now() + 1.5, execute: hideQRCodeTask!) + DispatchQueue.main.asyncAfter(deadline: .now() + Constants.hideQRCodeDelay, execute: task) } else { if !isValidIBANDetected { // Snapshot the flag once so the feedback type stays consistent across @@ -617,10 +618,11 @@ final class CameraViewController: UIViewController { showUnsupportedQRCodeAlert() } else { showInvalidQRCodeFeedback() - hideQRCodeTask = DispatchWorkItem(block: { + let task = DispatchWorkItem(block: { self.resetQRCodeScanning(isValid: false) }) - DispatchQueue.main.asyncAfter(deadline: .now() + 1.5, execute: hideQRCodeTask!) + hideQRCodeTask = task + DispatchQueue.main.asyncAfter(deadline: .now() + Constants.hideQRCodeDelay, execute: task) } } } @@ -817,6 +819,7 @@ private extension CameraViewController { static let switcherPadding: CGFloat = 8 static let phoneSwitcherSize: CGSize = CGSize(width: 124, height: 40) static let tableSwitcherSize: CGSize = CGSize(width: 40, height: 124) + static let hideQRCodeDelay: TimeInterval = 1.5 } private struct Strings { From fa65534c1f4952f7d97b34a0813581c6e484e640 Mon Sep 17 00:00:00 2001 From: Qazi Naveed Date: Wed, 24 Jun 2026 16:38:52 +0200 Subject: [PATCH 25/26] fix(GiniCaptureSDK): guard against duplicate unsupported QR alert side-effects PP-2310 --- .../Core/Screens/Camera/Camera/CameraViewController.swift | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CaptureSDK/GiniCaptureSDK/Sources/GiniCaptureSDK/Core/Screens/Camera/Camera/CameraViewController.swift b/CaptureSDK/GiniCaptureSDK/Sources/GiniCaptureSDK/Core/Screens/Camera/Camera/CameraViewController.swift index 6d4d3aa55f..6014da5812 100644 --- a/CaptureSDK/GiniCaptureSDK/Sources/GiniCaptureSDK/Core/Screens/Camera/Camera/CameraViewController.swift +++ b/CaptureSDK/GiniCaptureSDK/Sources/GiniCaptureSDK/Core/Screens/Camera/Camera/CameraViewController.swift @@ -671,6 +671,10 @@ final class CameraViewController: UIViewController { } private func showUnsupportedQRCodeAlert() { + // Skip duplicate side-effects when the alert is already on screen and more frames + // are still queued before pauseQRDetection takes effect on the session queue. + guard presentedViewController == nil else { return } + let generator = UINotificationFeedbackGenerator() generator.notificationOccurred(.warning) From 996b5966ce7442ce28cf80c91a71aa2b37c5c210 Mon Sep 17 00:00:00 2001 From: Qazi Naveed Date: Wed, 24 Jun 2026 16:52:20 +0200 Subject: [PATCH 26/26] test(GiniCaptureSDK): cover [.qr] assignment in resumeQRDetection via metadata output PP-2310 --- .../CameraPreviewViewControllerTests.swift | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/CaptureSDK/GiniCaptureSDK/Tests/GiniCaptureSDKTests/CameraPreviewViewControllerTests.swift b/CaptureSDK/GiniCaptureSDK/Tests/GiniCaptureSDKTests/CameraPreviewViewControllerTests.swift index 5824754bac..16f11a9423 100644 --- a/CaptureSDK/GiniCaptureSDK/Tests/GiniCaptureSDKTests/CameraPreviewViewControllerTests.swift +++ b/CaptureSDK/GiniCaptureSDK/Tests/GiniCaptureSDKTests/CameraPreviewViewControllerTests.swift @@ -150,4 +150,31 @@ final class CameraPreviewViewControllerTests: XCTestCase { XCTAssertNotNil(camera.qrMetadataOutput, "qrMetadataOutput should be assigned after configureQROutput runs") } + + func testResumeQRDetectionEnablesQRTypeWhenAvailable() { + let camera = makeCamera() + let output = FakeQRAvailableMetadataOutput() + camera.setQRMetadataOutputForTesting(output) + + camera.resumeQRDetection() + flushSessionQueue(camera) + + XCTAssertEqual(output.metadataObjectTypes, [.qr], + "metadataObjectTypes should be set to [.qr] when .qr is available") + } +} + +// Test stub that pretends .qr is supported, allowing the `availableMetadataObjectTypes.contains(.qr)` +// guard branch in resumeQRDetection to be exercised on CI simulators (which have no real camera). +private final class FakeQRAvailableMetadataOutput: AVCaptureMetadataOutput { + private var _objectTypes: [AVMetadataObject.ObjectType]? = [] + + override var availableMetadataObjectTypes: [AVMetadataObject.ObjectType] { + return [.qr] + } + + override var metadataObjectTypes: [AVMetadataObject.ObjectType]! { + get { _objectTypes } + set { _objectTypes = newValue } + } }