From daedec810a564a5bd5ffc82a93a671fa28fd3109 Mon Sep 17 00:00:00 2001 From: Team Mobile Schorsch Date: Wed, 4 Sep 2024 13:38:10 +0000 Subject: [PATCH] Release version 3.10.0 --- Package.swift | 2 +- .../Core/Custom views/GiniBarButton.swift | 6 +- .../Core/Extensions/UILabel.swift | 6 + .../Core/Helpers/ButtonConfiguration.swift | 5 +- .../Core/Models/GiniCaptureDocument.swift | 9 +- .../Analysis/AnalysisViewController.swift | 10 +- .../Core/Screens/Camera/Camera.swift | 2 + .../Camera/CameraViewController+Actions.swift | 4 + .../Camera/Camera/CameraViewController.swift | 8 + .../Camera/CameraPreviewViewController.swift | 9 +- .../Screens/Camera/Views/QRCodeOverlay.swift | 4 +- .../Gallery/ImagePickerViewController.swift | 8 +- .../GiniScreenAPICoordinator+Analysis.swift | 4 +- .../GiniScreenAPICoordinator+Camera.swift | 5 + .../GiniScreenAPICoordinator.swift | 5 +- .../GiniCaptureSDKVersion.swift | 2 +- .../Networking/DocumentService.swift | 92 +++++++++- .../Networking/DocumentServiceProtocol.swift | 11 +- .../GiniCaptureNetworkService.swift | 173 ++++++++++++++---- Tests/GiniCaptureSDKTests/CameraMock.swift | 3 +- 20 files changed, 298 insertions(+), 70 deletions(-) diff --git a/Package.swift b/Package.swift index a72fe6a..0c5756c 100644 --- a/Package.swift +++ b/Package.swift @@ -16,7 +16,7 @@ let package = Package( dependencies: [ // Dependencies declare other packages that this package depends on. // .package(url: /* package url */, from: "1.0.0"), - .package(name: "GiniBankAPILibrary", url: "https://github.com/gini/bank-api-library-ios.git", .exact("3.2.0")) + .package(name: "GiniBankAPILibrary", url: "https://github.com/gini/bank-api-library-ios.git", .exact("3.3.0")) ], targets: [ // Targets are the basic building blocks of a package. A target can define a module or a test suite. diff --git a/Sources/GiniCaptureSDK/Core/Custom views/GiniBarButton.swift b/Sources/GiniCaptureSDK/Core/Custom views/GiniBarButton.swift index ca91795..147a3e7 100644 --- a/Sources/GiniCaptureSDK/Core/Custom views/GiniBarButton.swift +++ b/Sources/GiniCaptureSDK/Core/Custom views/GiniBarButton.swift @@ -193,13 +193,15 @@ public final class GiniBarButton { return attributes } - public func setContentHuggingPriority(_ priority: UILayoutPriority, for axis: NSLayoutConstraint.Axis) { + public func setContentHuggingPriority(_ priority: UILayoutPriority, + for axis: NSLayoutConstraint.Axis) { stackView.setContentHuggingPriority(priority, for: axis) titleLabel.setContentHuggingPriority(priority, for: axis) imageView.setContentHuggingPriority(priority, for: axis) } - public func setContentCompressionResistancePriority(_ priority: UILayoutPriority, for axis: NSLayoutConstraint.Axis) { + public func setContentCompressionResistancePriority(_ priority: UILayoutPriority, + for axis: NSLayoutConstraint.Axis) { stackView.setContentCompressionResistancePriority(priority, for: axis) titleLabel.setContentCompressionResistancePriority(priority, for: axis) imageView.setContentCompressionResistancePriority(priority, for: axis) diff --git a/Sources/GiniCaptureSDK/Core/Extensions/UILabel.swift b/Sources/GiniCaptureSDK/Core/Extensions/UILabel.swift index 883b914..e5ddd15 100644 --- a/Sources/GiniCaptureSDK/Core/Extensions/UILabel.swift +++ b/Sources/GiniCaptureSDK/Core/Extensions/UILabel.swift @@ -20,4 +20,10 @@ extension UILabel { attributes: [NSAttributedString.Key.font: font], context: nil).size.height } + + func enableScaling() { + adjustsFontSizeToFitWidth = true + minimumScaleFactor = 10 / font.pointSize + adjustsFontForContentSizeCategory = true + } } diff --git a/Sources/GiniCaptureSDK/Core/Helpers/ButtonConfiguration.swift b/Sources/GiniCaptureSDK/Core/Helpers/ButtonConfiguration.swift index 5cb997d..86e52a8 100644 --- a/Sources/GiniCaptureSDK/Core/Helpers/ButtonConfiguration.swift +++ b/Sources/GiniCaptureSDK/Core/Helpers/ButtonConfiguration.swift @@ -74,10 +74,11 @@ public extension UIButton { self.layer.borderWidth = configuration.borderWidth self.layer.shadowRadius = configuration.shadowRadius + // When switching from one ButtonConfiguration with a blur effect to another ButtonConfiguration with a blur effect, + // the previous blur effect should be removed. + self.removeBlurEffect() if configuration.withBlurEffect { self.addBlurEffect(cornerRadius: configuration.cornerRadius) - } else { - self.removeBlurEffect() } } } diff --git a/Sources/GiniCaptureSDK/Core/Models/GiniCaptureDocument.swift b/Sources/GiniCaptureSDK/Core/Models/GiniCaptureDocument.swift index 4f49b72..d924301 100644 --- a/Sources/GiniCaptureSDK/Core/Models/GiniCaptureDocument.swift +++ b/Sources/GiniCaptureSDK/Core/Models/GiniCaptureDocument.swift @@ -112,7 +112,7 @@ private extension GiniCaptureDocumentBuilder { final class InputDocument: UIDocument { public var data: Data? - override public func load(fromContents contents: Any, ofType typeName: String?) throws { + override func load(fromContents contents: Any, ofType typeName: String?) throws { guard let data = contents as? Data else { throw DocumentError.unrecognizedContent @@ -120,5 +120,12 @@ private extension GiniCaptureDocumentBuilder { self.data = data } + + override func writeContents(_ contents: Any, to url: URL, for saveOperation: UIDocument.SaveOperation, originalContentsURL: URL?) throws { + if (contents as? Data) == nil, (contents as? FileWrapper) == nil { + throw DocumentError.unrecognizedContent + } + try super.writeContents(contents, to: url, for: saveOperation, originalContentsURL: originalContentsURL) + } } } diff --git a/Sources/GiniCaptureSDK/Core/Screens/Analysis/AnalysisViewController.swift b/Sources/GiniCaptureSDK/Core/Screens/Analysis/AnalysisViewController.swift index 68ea69e..535237c 100644 --- a/Sources/GiniCaptureSDK/Core/Screens/Analysis/AnalysisViewController.swift +++ b/Sources/GiniCaptureSDK/Core/Screens/Analysis/AnalysisViewController.swift @@ -18,9 +18,7 @@ import UIKit - parameter message: The error type to be displayed. */ - func displayError(errorType: ErrorType, - animated: Bool - ) + func displayError(errorType: ErrorType, animated: Bool) /** In case that the `GiniCaptureDocument` analysed is an image it will display a no results screen @@ -85,8 +83,7 @@ import UIKit }() private lazy var loadingIndicatorContainer: UIView = { - let loadingIndicatorContainer = UIView(frame: CGRect(origin: .zero, - size: .zero)) + let loadingIndicatorContainer = UIView(frame: CGRect.zero) return loadingIndicatorContainer }() @@ -142,8 +139,9 @@ import UIKit super.viewDidAppear(animated) didShowAnalysis?() + let documentTypeAnalytics = GiniAnalyticsMapper.documentTypeAnalytics(from: document.type) let eventProperties = [GiniAnalyticsProperty(key: .documentType, - value: GiniAnalyticsMapper.documentTypeAnalytics(from: document.type))] + value: documentTypeAnalytics)] GiniAnalyticsManager.trackScreenShown(screenName: .analysis, properties: eventProperties) } diff --git a/Sources/GiniCaptureSDK/Core/Screens/Camera/Camera.swift b/Sources/GiniCaptureSDK/Core/Screens/Camera/Camera.swift index c53954a..d5c265c 100644 --- a/Sources/GiniCaptureSDK/Core/Screens/Camera/Camera.swift +++ b/Sources/GiniCaptureSDK/Core/Screens/Camera/Camera.swift @@ -19,6 +19,7 @@ protocol CameraProtocol: AnyObject { var didDetectIBANs: (([String]) -> Void)? { get set } var isFlashSupported: Bool { get } var isFlashOn: Bool { get set } + var hasInitialized: Bool { get } func captureStillImage(completion: @escaping (Data?, CameraError?) -> Void) func focus(withMode mode: AVCaptureDevice.FocusMode, @@ -56,6 +57,7 @@ final class Camera: NSObject, CameraProtocol { var videoDeviceInput: AVCaptureDeviceInput? var videoDataOutput = AVCaptureVideoDataOutput() let videoDataOutputQueue = DispatchQueue(label: "ocr queue") + var hasInitialized: Bool { !session.inputs.isEmpty } lazy var isFlashSupported: Bool = { #if targetEnvironment(simulator) diff --git a/Sources/GiniCaptureSDK/Core/Screens/Camera/Camera/CameraViewController+Actions.swift b/Sources/GiniCaptureSDK/Core/Screens/Camera/Camera/CameraViewController+Actions.swift index 1cffc22..fc88e5f 100644 --- a/Sources/GiniCaptureSDK/Core/Screens/Camera/Camera/CameraViewController+Actions.swift +++ b/Sources/GiniCaptureSDK/Core/Screens/Camera/Camera/CameraViewController+Actions.swift @@ -27,4 +27,8 @@ extension CameraViewController { public func setupCamera() { cameraPreviewViewController.setupCamera() } + + public func stopLoadingIndicater() { + cameraPreviewViewController.stopLoadingIndicator() + } } diff --git a/Sources/GiniCaptureSDK/Core/Screens/Camera/Camera/CameraViewController.swift b/Sources/GiniCaptureSDK/Core/Screens/Camera/Camera/CameraViewController.swift index dbb8f9d..5d510f6 100644 --- a/Sources/GiniCaptureSDK/Core/Screens/Camera/Camera/CameraViewController.swift +++ b/Sources/GiniCaptureSDK/Core/Screens/Camera/Camera/CameraViewController.swift @@ -16,6 +16,8 @@ final class CameraViewController: UIViewController { */ let giniConfiguration: GiniConfiguration var detectedQRCodeDocument: GiniQRCodeDocument? + var cameraNeedsInitializing: Bool { !cameraPreviewViewController.hasInitialized } + var shouldShowHelp: Bool { isPresentedOnScreen && !validQRCodeProcessing } lazy var cameraPreviewViewController: CameraPreviewViewController = { let cameraPreviewViewController = CameraPreviewViewController() @@ -40,6 +42,7 @@ final class CameraViewController: UIViewController { private var resetQRCodeTask: DispatchWorkItem? private var hideQRCodeTask: DispatchWorkItem? private var validQRCodeProcessing: Bool = false + private var isPresentedOnScreen = false private var isValidIBANDetected: Bool = false // Analytics @@ -108,6 +111,7 @@ final class CameraViewController: UIViewController { override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) + isPresentedOnScreen = true validQRCodeProcessing = false delegate?.cameraDidAppear(self) @@ -354,6 +358,7 @@ final class CameraViewController: UIViewController { override func viewWillDisappear(_ animated: Bool) { super.viewWillDisappear(animated) + isPresentedOnScreen = false qrCodeOverLay.viewWillDisappear() ibanDetectionOverLay.viewWillDisappear() @@ -450,6 +455,7 @@ final class CameraViewController: UIViewController { } fileprivate func didPick(_ document: GiniCaptureDocument) { + navigationItem.rightBarButtonItem?.isEnabled = true delegate?.camera(self, didCapture: document) } @@ -532,6 +538,7 @@ final class CameraViewController: UIViewController { // MARK: - QR Detection private func showQRCodeFeedback(for document: GiniQRCodeDocument, isValid: Bool) { + guard isPresentedOnScreen else { return } guard !validQRCodeProcessing else { return } guard detectedQRCodeDocument != document else { return } @@ -576,6 +583,7 @@ final class CameraViewController: UIViewController { screenName: .camera, properties: [GiniAnalyticsProperty(key: .qrCodeValid, value: true)]) qrCodeOverLay.configureQrCodeOverlay(withCorrectQrCode: true) + navigationItem.rightBarButtonItem?.isEnabled = false } private func showInvalidQRCodeFeedback() { diff --git a/Sources/GiniCaptureSDK/Core/Screens/Camera/CameraPreviewViewController.swift b/Sources/GiniCaptureSDK/Core/Screens/Camera/CameraPreviewViewController.swift index df8c386..7a9e788 100644 --- a/Sources/GiniCaptureSDK/Core/Screens/Camera/CameraPreviewViewController.swift +++ b/Sources/GiniCaptureSDK/Core/Screens/Camera/CameraPreviewViewController.swift @@ -33,6 +33,10 @@ final class CameraPreviewViewController: UIViewController { } } + var hasInitialized: Bool { + camera.hasInitialized + } + var isFlashSupported: Bool { return camera.isFlashSupported && giniConfiguration.flashToggleEnabled } @@ -133,7 +137,9 @@ final class CameraPreviewViewController: UIViewController { override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) camera.start() - startLoadingIndicator() + if !hasInitialized { + startLoadingIndicator() + } } override func viewDidLoad() { @@ -314,7 +320,6 @@ final class CameraPreviewViewController: UIViewController { self.notAuthorizedView?.isHidden = true self.delegate?.cameraDidSetUp(self, camera: self.camera) } - self.stopLoadingIndicator() } diff --git a/Sources/GiniCaptureSDK/Core/Screens/Camera/Views/QRCodeOverlay.swift b/Sources/GiniCaptureSDK/Core/Screens/Camera/Views/QRCodeOverlay.swift index 471be55..d7d8e2b 100644 --- a/Sources/GiniCaptureSDK/Core/Screens/Camera/Views/QRCodeOverlay.swift +++ b/Sources/GiniCaptureSDK/Core/Screens/Camera/Views/QRCodeOverlay.swift @@ -18,7 +18,7 @@ final class CorrectQRCodeTextContainer: UIView { label.textColor = .GiniCapture.light1 label.text = NSLocalizedStringPreferredFormat("ginicapture.QRscanning.correct", comment: "QR Detected") - label.adjustsFontForContentSizeCategory = true + label.enableScaling() return label }() @@ -52,6 +52,7 @@ final class IncorrectQRCodeTextContainer: UIView { label.textColor = .GiniCapture.dark1 label.text = NSLocalizedStringPreferredFormat("ginicapture.QRscanning.incorrect.title", comment: "Unknown QR") + label.enableScaling() label.numberOfLines = 0 return label }() @@ -63,6 +64,7 @@ final class IncorrectQRCodeTextContainer: UIView { label.numberOfLines = 0 label.text = NSLocalizedStringPreferredFormat("ginicapture.QRscanning.incorrect.description", comment: "No content") + label.enableScaling() return label }() diff --git a/Sources/GiniCaptureSDK/Core/Screens/Document picker/Gallery/ImagePickerViewController.swift b/Sources/GiniCaptureSDK/Core/Screens/Document picker/Gallery/ImagePickerViewController.swift index 4fc00a9..b1ffa6b 100644 --- a/Sources/GiniCaptureSDK/Core/Screens/Document picker/Gallery/ImagePickerViewController.swift +++ b/Sources/GiniCaptureSDK/Core/Screens/Document picker/Gallery/ImagePickerViewController.swift @@ -183,7 +183,7 @@ final class ImagePickerViewController: UIViewController { navigationBar.bottomAnchor.constraint(equalTo: view.bottomAnchor), navigationBar.leadingAnchor.constraint(equalTo: view.leadingAnchor), navigationBar.trailingAnchor.constraint(equalTo: view.trailingAnchor), - navigationBar.heightAnchor.constraint(equalToConstant: 114) + navigationBar.heightAnchor.constraint(equalToConstant: Constants.navigationBarHeight) ]) view.bringSubviewToFront(navigationBar) view.layoutSubviews() @@ -236,3 +236,9 @@ extension ImagePickerViewController: UICollectionViewDelegateFlowLayout { } } } + +private extension ImagePickerViewController { + enum Constants { + static let navigationBarHeight: CGFloat = 114 + } +} diff --git a/Sources/GiniCaptureSDK/Core/Screens/Screen API Coordinator/GiniScreenAPICoordinator+Analysis.swift b/Sources/GiniCaptureSDK/Core/Screens/Screen API Coordinator/GiniScreenAPICoordinator+Analysis.swift index db8f42c..01f078e 100644 --- a/Sources/GiniCaptureSDK/Core/Screens/Screen API Coordinator/GiniScreenAPICoordinator+Analysis.swift +++ b/Sources/GiniCaptureSDK/Core/Screens/Screen API Coordinator/GiniScreenAPICoordinator+Analysis.swift @@ -29,9 +29,7 @@ extension GiniScreenAPICoordinator { // MARK: - ImageAnalysisNoResults screen extension GiniScreenAPICoordinator { - func createImageAnalysisNoResultsScreen( - type: NoResultScreenViewController.NoResultType - ) -> NoResultScreenViewController { + func createImageAnalysisNoResultsScreen(type: NoResultScreenViewController.NoResultType) -> NoResultScreenViewController { let viewModel: BottomButtonsViewModel let viewController: NoResultScreenViewController switch type { diff --git a/Sources/GiniCaptureSDK/Core/Screens/Screen API Coordinator/GiniScreenAPICoordinator+Camera.swift b/Sources/GiniCaptureSDK/Core/Screens/Screen API Coordinator/GiniScreenAPICoordinator+Camera.swift index 358ad2c..cad1961 100644 --- a/Sources/GiniCaptureSDK/Core/Screens/Screen API Coordinator/GiniScreenAPICoordinator+Camera.swift +++ b/Sources/GiniCaptureSDK/Core/Screens/Screen API Coordinator/GiniScreenAPICoordinator+Camera.swift @@ -60,6 +60,11 @@ extension GiniScreenAPICoordinator: CameraViewControllerDelegate { } func cameraDidAppear(_ viewController: CameraViewController) { + // we should reinitialize camera when it's already initialized, otherwise camera behaves weird + guard viewController.cameraNeedsInitializing || shouldShowOnboarding() else { + viewController.stopLoadingIndicater() + return + } if shouldShowOnboarding() { showOnboardingScreen(cameraViewController: viewController, completion: { viewController.setupCamera() diff --git a/Sources/GiniCaptureSDK/Core/Screens/Screen API Coordinator/GiniScreenAPICoordinator.swift b/Sources/GiniCaptureSDK/Core/Screens/Screen API Coordinator/GiniScreenAPICoordinator.swift index 021c04c..5bf9d76 100644 --- a/Sources/GiniCaptureSDK/Core/Screens/Screen API Coordinator/GiniScreenAPICoordinator.swift +++ b/Sources/GiniCaptureSDK/Core/Screens/Screen API Coordinator/GiniScreenAPICoordinator.swift @@ -46,7 +46,6 @@ open class GiniScreenAPICoordinator: NSObject, Coordinator { public var giniConfiguration: GiniConfiguration public var pages: [GiniCapturePage] = [] public weak var visionDelegate: GiniCaptureDelegate? - // Resources fileprivate(set) lazy var cancelButtonResource = giniConfiguration.cancelButtonResource ?? @@ -260,8 +259,8 @@ extension GiniScreenAPICoordinator { } @objc func showHelpMenuScreen() { - let topViewController = screenAPINavigationController.topViewController - guard topViewController is CameraViewController else { + let topViewController = screenAPINavigationController.topViewController as? CameraViewController + guard let topViewController, topViewController.shouldShowHelp else { return } diff --git a/Sources/GiniCaptureSDK/GiniCaptureSDKVersion.swift b/Sources/GiniCaptureSDK/GiniCaptureSDKVersion.swift index 5b24101..373d35f 100644 --- a/Sources/GiniCaptureSDK/GiniCaptureSDKVersion.swift +++ b/Sources/GiniCaptureSDK/GiniCaptureSDKVersion.swift @@ -5,4 +5,4 @@ // Copyright © 2024 Gini GmbH. All rights reserved. // -public let GiniCaptureSDKVersion = "3.9.0" +public let GiniCaptureSDKVersion = "3.10.0" diff --git a/Sources/GiniCaptureSDK/Networking/DocumentService.swift b/Sources/GiniCaptureSDK/Networking/DocumentService.swift index d8c6b09..8ae8b97 100644 --- a/Sources/GiniCaptureSDK/Networking/DocumentService.swift +++ b/Sources/GiniCaptureSDK/Networking/DocumentService.swift @@ -9,7 +9,7 @@ import UIKit import GiniBankAPILibrary /** - Static veriable for synchronization to prevent display of multiple error screens at the same time + Static variable for synchronization to prevent the display of multiple error screens at the same time. */ var errorOccurred = false @@ -43,9 +43,7 @@ public final class DocumentService: DocumentServiceProtocol { completion?(.success(createdDocument)) case .failure(let error): DispatchQueue.main.async { - guard errorOccurred == false else { - return - } + guard !errorOccurred else { return } errorOccurred = true DispatchQueue.global().async { completion?(.failure(error)) @@ -75,7 +73,9 @@ public final class DocumentService: DocumentServiceProtocol { .sorted() .map { $0.info } self.analysisCancellationToken = CancellationToken() - captureNetworkService.analyse(partialDocuments: partialDocumentsInfoSorted, metadata: metadata, cancellationToken: analysisCancellationToken!) { [weak self] result in + captureNetworkService.analyse(partialDocuments: partialDocumentsInfoSorted, + metadata: metadata, + cancellationToken: analysisCancellationToken!) { [weak self] result in guard let self = self else { return } switch result { case let .success((createdDocument, extractionResult)): @@ -83,9 +83,7 @@ public final class DocumentService: DocumentServiceProtocol { completion(.success(extractionResult)) case let .failure(error): DispatchQueue.main.async { - guard errorOccurred == false else { - return - } + guard !errorOccurred else { return } errorOccurred = true DispatchQueue.global().async { completion(.failure(error)) @@ -141,7 +139,9 @@ public final class DocumentService: DocumentServiceProtocol { Log(message: "Cannot send feedback: no document", event: .error) return } - captureNetworkService.sendFeedback(document: document, updatedExtractions: updatedExtractions, updatedCompoundExtractions: updatedCompoundExtractions) { result in + captureNetworkService.sendFeedback(document: document, + updatedExtractions: updatedExtractions, + updatedCompoundExtractions: updatedCompoundExtractions) { result in switch result { case .success: Log(message: "Feedback sent with \(updatedExtractions.count) extractions and \(updatedCompoundExtractions?.count ?? 0) compound extractions", @@ -174,5 +174,77 @@ public final class DocumentService: DocumentServiceProtocol { } } } - + + public func layout(completion: @escaping DocumentLayoutCompletion) { + guard let document = document else { + Log(message: "Cannot get document layout: no document", event: .error) + return + } + captureNetworkService.layout(for: document) { result in + switch result { + case .success(let documentLayout): + completion(.success(documentLayout)) + case .failure(let error): + let message = "Failed to get layout for document with id: \(document.id) error: \(error)" + Log(message: message, event: .error) + DispatchQueue.main.async { + guard !errorOccurred else { return } + errorOccurred = true + DispatchQueue.global().async { + completion(.failure(error)) + } + } + } + } + } + + public func pages(completion: @escaping DocumentPagsCompletion) { + guard let document = document else { + Log(message: "Cannot get document pages", event: .error) + return + } + captureNetworkService.pages(for: document) { result in + switch result { + case let .success(pages): + completion(.success(pages)) + case let .failure(error): + let message = "Failed to get pages for document with id: \(document.id) error: \(error)" + Log(message: message, event: .error) + DispatchQueue.main.async { + guard !errorOccurred else { return } + errorOccurred = true + DispatchQueue.global().async { + completion(.failure(error)) + } + } + } + } + } + + public func documentPage(pageNumber: Int, + size: Document.Page.Size, + completion: @escaping DocumentPagePreviewCompletion){ + guard let document = document else { + Log(message: "Cannot get document page", event: .error) + return + } + captureNetworkService.documentPage(for: document, + pageNumber: pageNumber, + size: size) { result in + switch result { + case .success(let pageData): + completion(.success(pageData)) + case .failure(let error): + let message = "Failed to get page for document with id: \(document.id) error: \(error)" + Log(message: message, event: .error) + DispatchQueue.main.async { + guard !errorOccurred else { return } + errorOccurred = true + DispatchQueue.global().async { + completion(.failure(error)) + } + } + } + } + } } diff --git a/Sources/GiniCaptureSDK/Networking/DocumentServiceProtocol.swift b/Sources/GiniCaptureSDK/Networking/DocumentServiceProtocol.swift index 5463a4d..f08214f 100644 --- a/Sources/GiniCaptureSDK/Networking/DocumentServiceProtocol.swift +++ b/Sources/GiniCaptureSDK/Networking/DocumentServiceProtocol.swift @@ -2,7 +2,7 @@ // DocumentServiceProtocol.swift // GiniCapture // -// Created by Enrique del Pozo Gómez on 3/29/18. +// Copyright © 2024 Gini GmbH. All rights reserved. // import Foundation @@ -10,6 +10,9 @@ import GiniBankAPILibrary public typealias UploadDocumentCompletion = (Result) -> Void public typealias AnalysisCompletion = (Result) -> Void +public typealias DocumentLayoutCompletion = (Result) -> Void +public typealias DocumentPagePreviewCompletion = (Result) -> Void +public typealias DocumentPagsCompletion = (Result<[Document.Page], GiniError>) -> Void public protocol DocumentServiceProtocol: AnyObject { @@ -27,4 +30,10 @@ public protocol DocumentServiceProtocol: AnyObject { completion: UploadDocumentCompletion?) func update(imageDocument: GiniImageDocument) func log(errorEvent: ErrorEvent) + + func layout(completion: @escaping DocumentLayoutCompletion) + func pages(completion: @escaping DocumentPagsCompletion) + func documentPage(pageNumber: Int, + size: Document.Page.Size, + completion: @escaping DocumentPagePreviewCompletion) } diff --git a/Sources/GiniCaptureSDK/Networking/GiniCaptureNetworkService.swift b/Sources/GiniCaptureSDK/Networking/GiniCaptureNetworkService.swift index 89c9393..e8a3081 100644 --- a/Sources/GiniCaptureSDK/Networking/GiniCaptureNetworkService.swift +++ b/Sources/GiniCaptureSDK/Networking/GiniCaptureNetworkService.swift @@ -1,8 +1,7 @@ // // GiniCaptureNetworkService.swift -// // -// Created by Alpár Szotyori on 12.01.22. +// Copyright © 2024 Gini GmbH. All rights reserved. // import Foundation @@ -12,10 +11,11 @@ public protocol GiniCaptureNetworkService: AnyObject { func delete(document: Document, completion: @escaping (Result) -> Void) func cleanup() + func analyse(partialDocuments: [PartialDocumentInfo], metadata: Document.Metadata?, cancellationToken: CancellationToken, - completion: @escaping (Result<(document: Document,extractionResult: ExtractionResult), GiniError>) -> Void) + completion: @escaping (Result<(document: Document, extractionResult: ExtractionResult), GiniError>) -> Void) func upload(document: GiniCaptureDocument, metadata: Document.Metadata?, completion: @escaping UploadDocumentCompletion) @@ -25,10 +25,58 @@ public protocol GiniCaptureNetworkService: AnyObject { completion: @escaping (Result) -> Void) func log(errorEvent: ErrorEvent, completion: @escaping (Result) -> Void) + + func layout(for document: Document, + completion: @escaping DocumentLayoutCompletion) + func pages(for document: Document, completion: @escaping DocumentPagsCompletion) + func documentPage(for document: Document, + pageNumber: Int, + size: GiniBankAPILibrary.Document.Page.Size, + completion: @escaping DocumentPagePreviewCompletion) +} + +/// Extension for `GiniCaptureNetworkService` protocol to provide default implementations +public extension GiniCaptureNetworkService { + + /** + * Retrieves the layout of a given document + * + * - Parameter document: The document for which the layout is requested + * - Parameter completion: A completion callback, returning the requested document layout on success + */ + func layout(for document: Document, + completion: @escaping DocumentLayoutCompletion) { + // Default implementation is empty + } + + /** + * Retrieves the pages of a given document + * + * - Parameter document: The document from which to retrieve the pages + * - Parameter completion: A completion callback, returning the requested document layout on success + */ + func pages(for document: Document, completion: @escaping DocumentPagsCompletion) { + // Default implementation is empty + } + + /** + * Retrieves the page data of a document for a specified page number and size + * + * - Parameter document: The document from which to retrieve the page data + * - Parameter pageNumber: The page number within the document to retrieve + * - Parameter size: The size of the page to retrieve (e.g., large, medium) + * - Parameter completion: A completion callback that returns a `Result`, with the requested page data on success, or an error on failure + */ + func documentPage(for document: Document, + pageNumber: Int, + size: Document.Page.Size, + completion: @escaping DocumentPagePreviewCompletion) { + // Default implementation is empty + } } class DefaultCaptureNetworkService: GiniCaptureNetworkService { - + var documentService: DefaultDocumentService public init(lib: GiniBankAPI) { @@ -43,12 +91,10 @@ class DefaultCaptureNetworkService: GiniCaptureNetworkService { Log(message: "Deleted \(document.sourceClassification.rawValue) document with id: \(document.id)", event: "🗑") case .failure(let error): - completion(.failure(error)) let message = "Error deleting \(document.sourceClassification.rawValue) document with" + " id: \(document.id)" - Log(message: message,event: .error) - let errorLog = ErrorLog(description: message, error: error) - GiniConfiguration.shared.errorLogger.handleErrorLog(error: errorLog) + self.logError(message: message, error: error) + completion(.failure(error)) } } } @@ -58,7 +104,7 @@ class DefaultCaptureNetworkService: GiniCaptureNetworkService { func analyse(partialDocuments: [PartialDocumentInfo], metadata: Document.Metadata?, cancellationToken: CancellationToken, - completion: @escaping (Result<(document:Document,extractionResult: ExtractionResult), GiniError>) -> Void) { + completion: @escaping (Result<(document: Document, extractionResult: ExtractionResult), GiniError>) -> Void) { Log(message: "Creating composite document...", event: "📑") let fileName = "Composite-\(NSDate().timeIntervalSince1970)" @@ -72,29 +118,12 @@ class DefaultCaptureNetworkService: GiniCaptureNetworkService { case let .success(createdDocument): Log(message: "Starting analysis for composite document \(createdDocument.id)", event: "🔎") - self.documentService - .extractions(for: createdDocument, - cancellationToken: cancellationToken) { [weak self] result in - guard self != nil else { return } - switch result { - case let .success(extractionResult): - Log(message: "Finished analysis process with no errors", event: .success) - completion(.success((createdDocument, extractionResult))) - case let .failure(error): - switch error { - case .requestCancelled: - Log(message: "Cancelled analysis process", event: .error) - default: - Log(message: "Finished analysis process with error: \(error)", event: .error) - } - completion(.failure(error)) - } - } + self.startExtraction(for: createdDocument, + cancellationToken: cancellationToken, + completion: completion) case let .failure(error): - let message = "Composite document creation failed" - Log(message: message, event: .error) - let errorLog = ErrorLog(description: message, error: error) - GiniConfiguration.shared.errorLogger.handleErrorLog(error: errorLog) + self.logError(message: "Composite document creation failed with error: \(error)", + error: error) completion(.failure(error)) } } @@ -117,10 +146,7 @@ class DefaultCaptureNetworkService: GiniCaptureNetworkService { "for vision document \(document.id)", event: "📄") completion(.success(createdDocument)) case let .failure(error): - let message = "Document creation failed" - Log(message: message, event: .error) - let errorLog = ErrorLog(description: message, error: error) - GiniConfiguration.shared.errorLogger.handleErrorLog(error: errorLog) + self.logError(message: "Document creation failed with error: \(error)", error: error) completion(.failure(error)) } } @@ -147,4 +173,81 @@ class DefaultCaptureNetworkService: GiniCaptureNetworkService { } } } + + func layout(for document: Document, completion: @escaping DocumentLayoutCompletion) { + Log(message: "Getting layout for document with id: \(document.id) ", event: "📝") + documentService.layout(for: document) { result in + switch result { + case let .success(layout): + completion(.success(layout)) + case let .failure(error): + self.logError(message: "Document layout retrieval encountered an error: \(error)", + error: error) + completion(.failure(error)) + } + } + } + + func pages(for document: Document, completion: @escaping (Result<[Document.Page], GiniError>) -> Void) { + Log(message: "Getting pages for document with id: \(document.id) ", event: "📝") + documentService.pages(in: document) { result in + switch result { + case let .success(pages): + completion(.success(pages)) + case let .failure(error): + self.logError(message: "Document pages retrieval encountered an error: \(error)", + error: error) + completion(.failure(error)) + } + } + } + + func documentPage(for document: Document, + pageNumber: Int, + size: GiniBankAPILibrary.Document.Page.Size, + completion: @escaping DocumentPagePreviewCompletion) { + Log(message: "Getting page for document with id: \(document.id) ", event: "📝") + documentService.documentPage(for: document, + pageNumber: pageNumber, + size: size) { result in + switch result { + case let .success(pageData): + completion(.success(pageData)) + case let .failure(error): + self.logError(message: "Document page retrieval encountered an error: \(error)", + error: error) + completion(.failure(error)) + } + } + } + + // MARK: - Private helper methods + + private func startExtraction(for document: Document, + cancellationToken: CancellationToken, + completion: @escaping (Result<(document: Document, + extractionResult: ExtractionResult), GiniError>) -> Void) { + documentService + .extractions(for: document, + cancellationToken: cancellationToken) { result in + switch result { + case let .success(extractionResult): + Log(message: "Finished analysis process with no errors", event: .success) + completion(.success((document, extractionResult))) + case let .failure(error): + if error == .requestCancelled { + Log(message: "Cancelled analysis process", event: .error) + } else { + Log(message: "Finished analysis process with error: \(error)", event: .error) + } + completion(.failure(error)) + } + } + } + + private func logError(message: String, error: GiniError) { + Log(message: message, event: .error) + let errorLog = ErrorLog(description: message, error: error) + GiniConfiguration.shared.errorLogger.handleErrorLog(error: errorLog) + } } diff --git a/Tests/GiniCaptureSDKTests/CameraMock.swift b/Tests/GiniCaptureSDKTests/CameraMock.swift index facd6fc..0b48652 100644 --- a/Tests/GiniCaptureSDKTests/CameraMock.swift +++ b/Tests/GiniCaptureSDKTests/CameraMock.swift @@ -35,7 +35,8 @@ final class CameraMock: CameraProtocol { let state: CameraAuthState var isFlashOn: Bool = true var isFlashSupported: Bool = true - + var hasInitialized: Bool = true + init(state: CameraAuthState) { self.state = state }