From 8a7169b81d7d84ba13c2e87ad9dcf7252b425acd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bruno=20Pantalea=CC=83o?= Date: Mon, 12 Feb 2024 14:25:16 +0100 Subject: [PATCH] Update implementation to align with frontend --- HomeAssistant.xcodeproj/project.pbxproj | 4 + .../QRCodeScanner/BarcodeScannerView.swift | 24 ++-- .../BarcodeScannerViewModel.swift | 45 ++++++++ .../WebViewExternalBusMessageTests.swift | 14 ++- Sources/App/WebView/WebViewController.swift | 104 +++++++----------- .../WebView/WebViewExternalBusMessage.swift | 10 +- .../API/WebSocket/WebSocketMessage.swift | 4 +- 7 files changed, 119 insertions(+), 86 deletions(-) create mode 100644 Sources/App/QRCodeScanner/BarcodeScannerViewModel.swift diff --git a/HomeAssistant.xcodeproj/project.pbxproj b/HomeAssistant.xcodeproj/project.pbxproj index a5e79a75e..92f9e01e2 100644 --- a/HomeAssistant.xcodeproj/project.pbxproj +++ b/HomeAssistant.xcodeproj/project.pbxproj @@ -508,6 +508,7 @@ 42266B1D2B741FB400E94A71 /* BarcodeScannerCameraView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42266B172B741FB300E94A71 /* BarcodeScannerCameraView.swift */; }; 42266B1F2B741FB400E94A71 /* BarcodeScannerDataModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42266B192B741FB300E94A71 /* BarcodeScannerDataModel.swift */; }; 42266B202B741FB400E94A71 /* BarcodeScannerCamera.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42266B1A2B741FB400E94A71 /* BarcodeScannerCamera.swift */; }; + 42266B252B7A4BA900E94A71 /* BarcodeScannerViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42266B242B7A4BA900E94A71 /* BarcodeScannerViewModel.swift */; }; 424A7F462B188946008C8DF3 /* WidgetBackground.swift in Sources */ = {isa = PBXBuildFile; fileRef = 424A7F452B188946008C8DF3 /* WidgetBackground.swift */; }; 424A7F482B188BF3008C8DF3 /* WidgetContentMargin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 424A7F472B188BF3008C8DF3 /* WidgetContentMargin.swift */; }; 424DD05A2B3509170057E456 /* CarPlayActionsTemplate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 424DD0592B3509170057E456 /* CarPlayActionsTemplate.swift */; }; @@ -1577,6 +1578,7 @@ 42266B172B741FB300E94A71 /* BarcodeScannerCameraView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BarcodeScannerCameraView.swift; sourceTree = ""; }; 42266B192B741FB300E94A71 /* BarcodeScannerDataModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BarcodeScannerDataModel.swift; sourceTree = ""; }; 42266B1A2B741FB400E94A71 /* BarcodeScannerCamera.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BarcodeScannerCamera.swift; sourceTree = ""; }; + 42266B242B7A4BA900E94A71 /* BarcodeScannerViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BarcodeScannerViewModel.swift; sourceTree = ""; }; 4242A2B12B2B5C8000E9F001 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = en; path = en.lproj/AppIntentVocabulary.plist; sourceTree = ""; }; 4242A2B22B2B5C8100E9F001 /* ca-ES */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = "ca-ES"; path = "ca-ES.lproj/AppIntentVocabulary.plist"; sourceTree = ""; }; 4242A2B32B2B5C8100E9F001 /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = "zh-Hans"; path = "zh-Hans.lproj/AppIntentVocabulary.plist"; sourceTree = ""; }; @@ -3065,6 +3067,7 @@ children = ( 42266B152B741F9F00E94A71 /* Camera */, 42266B102B740E4C00E94A71 /* BarcodeScannerView.swift */, + 42266B242B7A4BA900E94A71 /* BarcodeScannerViewModel.swift */, ); path = QRCodeScanner; sourceTree = ""; @@ -5642,6 +5645,7 @@ 425573CC2B5574AD00145217 /* CarPlayAreasZonesTemplate+Build.swift in Sources */, B626AAF11D8F972800A0D225 /* SettingsDetailViewController.swift in Sources */, 1127381C2622B6F300F5E312 /* DebugSettingsViewController.swift in Sources */, + 42266B252B7A4BA900E94A71 /* BarcodeScannerViewModel.swift in Sources */, 11DE823024FAE66F00E636B8 /* UIWindow+Additions.swift in Sources */, 11DA6B4F2713912F008ADFAF /* OnboardingPermissionViewController.swift in Sources */, 42CA28BB2B1028330093B31A /* SimulatorThreadClientService.swift in Sources */, diff --git a/Sources/App/QRCodeScanner/BarcodeScannerView.swift b/Sources/App/QRCodeScanner/BarcodeScannerView.swift index f17fe23f7..ce7770a00 100644 --- a/Sources/App/QRCodeScanner/BarcodeScannerView.swift +++ b/Sources/App/QRCodeScanner/BarcodeScannerView.swift @@ -2,14 +2,9 @@ import CodeScanner import Shared import SwiftUI -enum QRScannerResult { - case cancelled - case alternativeOption - case success(_ code: String, _ format: String) -} - struct BarcodeScannerView: View { @Environment(\.dismiss) private var dismiss + @StateObject private var viewModel: BarcodeScannerViewModel // Use single data model so both camera previews use same camera stream @State private var cameraDataModel = BarcodeScannerDataModel() private let cameraSquareSize: CGFloat = 320 @@ -21,18 +16,17 @@ struct BarcodeScannerView: View { private let title: String private let description: String private let alternativeOptionLabel: String? - private let completion: (QRScannerResult) -> Void init( title: String, description: String, alternativeOptionLabel: String? = nil, - completion: @escaping (QRScannerResult) -> Void + incomingMessageId: Int ) { self.title = title self.description = description self.alternativeOptionLabel = alternativeOptionLabel - self.completion = completion + self._viewModel = .init(wrappedValue: .init(incomingMessageId: incomingMessageId)) } var body: some View { @@ -49,8 +43,7 @@ struct BarcodeScannerView: View { } .onAppear { cameraDataModel.camera.qrFound = { code, format in - self.completion(.success(code, format)) - self.dismiss() + viewModel.scannedCode(code, format: format) } } } @@ -58,7 +51,7 @@ struct BarcodeScannerView: View { private var topInformation: some View { VStack(spacing: 8) { Button(action: { - completion(.cancelled) + viewModel.aborted(.canceled) dismiss() }, label: { Image(systemName: "xmark") @@ -78,8 +71,7 @@ struct BarcodeScannerView: View { if let alternativeOptionLabel { Button { - completion(.alternativeOption) - dismiss() + viewModel.aborted(.alternativeOptions) } label: { Text(alternativeOptionLabel) .font(.subheadline) @@ -134,10 +126,10 @@ struct BarcodeScannerView: View { } #Preview { - BarcodeScannerView(title: "Scan QR-code", description: "Find the code on your device", completion: { _ in }) + BarcodeScannerView(title: "Scan QR-code", description: "Find the code on your device", incomingMessageId: 1) } -class BarcodeScannerHostingController: UIHostingController { +final class BarcodeScannerHostingController: UIHostingController { override var supportedInterfaceOrientations: UIInterfaceOrientationMask { [.portrait] } diff --git a/Sources/App/QRCodeScanner/BarcodeScannerViewModel.swift b/Sources/App/QRCodeScanner/BarcodeScannerViewModel.swift new file mode 100644 index 000000000..461da4a06 --- /dev/null +++ b/Sources/App/QRCodeScanner/BarcodeScannerViewModel.swift @@ -0,0 +1,45 @@ +import Foundation +import Shared + +final class BarcodeScannerViewModel: ObservableObject { + enum AbortReason: String { + case canceled + case alternativeOptions = "alternative_options" + } + + private let incomingMessageId: Int + + init(incomingMessageId: Int) { + self.incomingMessageId = incomingMessageId + } + + func scannedCode(_ code: String, format: String) { + Current.sceneManager.webViewWindowControllerPromise.then(\.webViewControllerPromise) + .done { [weak self] controller in + guard let incomingMessageId = self?.incomingMessageId else { return } + controller + .sendExternalBus(message: .init( + id: incomingMessageId, + command: WebViewExternalBusOutgoingMessage.barCodeScanResult.rawValue, + payload: [ + "rawValue": code, + "format": format, + ] + )) + } + } + + func aborted(_ reason: AbortReason) { + Current.sceneManager.webViewWindowControllerPromise.then(\.webViewControllerPromise) + .done { [weak self] controller in + guard let incomingMessageId = self?.incomingMessageId else { return } + controller.sendExternalBus(message: .init( + id: incomingMessageId, + command: WebViewExternalBusOutgoingMessage.barCodeScanAborted.rawValue, + payload: [ + "reason": reason.rawValue, + ] + )) + } + } +} diff --git a/Sources/App/WebView/Tests/WebViewExternalBusMessageTests.swift b/Sources/App/WebView/Tests/WebViewExternalBusMessageTests.swift index d630df67c..5204aa381 100644 --- a/Sources/App/WebView/Tests/WebViewExternalBusMessageTests.swift +++ b/Sources/App/WebView/Tests/WebViewExternalBusMessageTests.swift @@ -11,8 +11,18 @@ final class WebViewExternalBusMessageTests: XCTestCase { XCTAssertEqual(WebViewExternalBusMessage.themeUpdate.rawValue, "theme-update") XCTAssertEqual(WebViewExternalBusMessage.matterCommission.rawValue, "matter/commission") XCTAssertEqual(WebViewExternalBusMessage.threadImportCredentials.rawValue, "thread/import_credentials") - XCTAssertEqual(WebViewExternalBusMessage.qrCodeScanner.rawValue, "barcode/scan") + XCTAssertEqual(WebViewExternalBusMessage.barCodeScanner.rawValue, "bar_code/scan") + XCTAssertEqual(WebViewExternalBusMessage.barCodeScannerClose.rawValue, "bar_code/close") + XCTAssertEqual(WebViewExternalBusMessage.barCodeScannerNotify.rawValue, "bar_code/notify") - XCTAssertEqual(WebViewExternalBusMessage.allCases.count, 10) + XCTAssertEqual(WebViewExternalBusMessage.allCases.count, 12) + } + + func test_externalBus_outgoing_messageKeys() { + XCTAssertEqual(WebViewExternalBusOutgoingMessage.showAutomationEditor.rawValue, "automation/editor/show") + XCTAssertEqual(WebViewExternalBusOutgoingMessage.barCodeScanResult.rawValue, "bar_code/scan_result") + XCTAssertEqual(WebViewExternalBusOutgoingMessage.barCodeScanAborted.rawValue, "bar_code/aborted") + + XCTAssertEqual(WebViewExternalBusOutgoingMessage.allCases.count, 3) } } diff --git a/Sources/App/WebView/WebViewController.swift b/Sources/App/WebView/WebViewController.swift index b59b6bc76..82ae6036a 100644 --- a/Sources/App/WebView/WebViewController.swift +++ b/Sources/App/WebView/WebViewController.swift @@ -25,6 +25,7 @@ final class WebViewController: UIViewController, WKNavigationDelegate, WKUIDeleg private var keepAliveTimer: Timer? private var initialURL: URL? + private var barCodeScannerController: UIViewController? private let settingsButton: UIButton! = { let button = UIButton() @@ -839,19 +840,22 @@ final class WebViewController: UIViewController, WKNavigationDelegate, WKUIDeleg showActionAutomationEditorNotAvailable() return } - sendExternalBus(message: .init(command: "automation/editor/show", payload: [ - "config": [ - "trigger": [ - [ - "platform": "event", - "event_type": "ios.action_fired", - "event_data": [ - "actionID": actionId, + sendExternalBus(message: .init( + command: WebViewExternalBusOutgoingMessage.showAutomationEditor.rawValue, + payload: [ + "config": [ + "trigger": [ + [ + "platform": "event", + "event_type": "ios.action_fired", + "event_data": [ + "actionID": actionId, + ], ], ], ], - ], - ])) + ] + )) } private func showActionAutomationEditorNotAvailable() { @@ -1062,19 +1066,24 @@ extension WebViewController: WKScriptMessageHandler { } case .threadImportCredentials: threadCredentialsRequested() - case .qrCodeScanner: - response = Guarantee { seal in - guard let title = incomingMessage.Payload?["title"] as? String, - let description = incomingMessage.Payload?["description"] as? String, - let incomingMessageId = incomingMessage.ID else { return } - qrCodeScannerRequested( - title: title, - description: description, - alternativeOptionLabel: incomingMessage.Payload?["alternative_option_label"] as? String, - incomingMessageId: incomingMessageId, - seal: seal - ) - } + case .barCodeScanner: + guard let title = incomingMessage.Payload?["title"] as? String, + let description = incomingMessage.Payload?["description"] as? String, + let incomingMessageId = incomingMessage.ID else { return } + qrCodeScannerRequested( + title: title, + description: description, + alternativeOptionLabel: incomingMessage.Payload?["alternative_option_label"] as? String, + incomingMessageId: incomingMessageId + ) + case .barCodeScannerClose: + barCodeScannerController?.dismiss(animated: true) + case .barCodeScannerNotify: + guard let message = incomingMessage.Payload?["message"] as? String else { return } + let alert = UIAlertController(title: nil, message: message, preferredStyle: .alert) + alert.addAction(.init(title: L10n.okLabel, style: .default)) + let controller = barCodeScannerController ?? self + controller.present(alert, animated: false, completion: nil) } } else { Current.Log.error("unknown: \(incomingMessage.MessageType)") @@ -1086,7 +1095,7 @@ extension WebViewController: WKScriptMessageHandler { } @discardableResult - private func sendExternalBus(message: WebSocketMessage) -> Promise { + public func sendExternalBus(message: WebSocketMessage) -> Promise { Promise { seal in DispatchQueue.main.async { [self] in do { @@ -1125,52 +1134,17 @@ extension WebViewController: WKScriptMessageHandler { title: String, description: String, alternativeOptionLabel: String?, - incomingMessageId: Int, - seal: @escaping (WebSocketMessage) -> Void + incomingMessageId: Int ) { - let qrCodeScannerView = BarcodeScannerHostingController(rootView: BarcodeScannerView( + barCodeScannerController = BarcodeScannerHostingController(rootView: BarcodeScannerView( title: title, description: description, alternativeOptionLabel: alternativeOptionLabel, - completion: { result in - switch result { - case .cancelled: - seal( - WebSocketMessage( - id: incomingMessageId, - type: "result", - result: [ - "action": "canceled", - ] - ) - ) - case .alternativeOption: - seal( - WebSocketMessage( - id: incomingMessageId, - type: "result", - result: [ - "action": "alternative_options", - ] - ) - ) - case let .success(code, format): - seal( - WebSocketMessage( - id: incomingMessageId, - type: "result", - result: [ - "action": "scan_result", - "result": code, - "format": format, - ] - ) - ) - } - } + incomingMessageId: incomingMessageId )) - qrCodeScannerView.modalPresentationStyle = .fullScreen - present(qrCodeScannerView, animated: true) + barCodeScannerController?.modalPresentationStyle = .fullScreen + guard let barCodeScannerController else { return } + present(barCodeScannerController, animated: true) } } diff --git a/Sources/App/WebView/WebViewExternalBusMessage.swift b/Sources/App/WebView/WebViewExternalBusMessage.swift index 86ff56d33..fb64987cc 100644 --- a/Sources/App/WebView/WebViewExternalBusMessage.swift +++ b/Sources/App/WebView/WebViewExternalBusMessage.swift @@ -10,5 +10,13 @@ enum WebViewExternalBusMessage: String, CaseIterable { case themeUpdate = "theme-update" case matterCommission = "matter/commission" case threadImportCredentials = "thread/import_credentials" - case qrCodeScanner = "barcode/scan" + case barCodeScanner = "bar_code/scan" + case barCodeScannerClose = "bar_code/close" + case barCodeScannerNotify = "bar_code/notify" +} + +enum WebViewExternalBusOutgoingMessage: String, CaseIterable { + case showAutomationEditor = "automation/editor/show" + case barCodeScanResult = "bar_code/scan_result" + case barCodeScanAborted = "bar_code/aborted" } diff --git a/Sources/Shared/API/WebSocket/WebSocketMessage.swift b/Sources/Shared/API/WebSocket/WebSocketMessage.swift index bd14bbf32..6531c4dbd 100644 --- a/Sources/Shared/API/WebSocket/WebSocketMessage.swift +++ b/Sources/Shared/API/WebSocket/WebSocketMessage.swift @@ -61,8 +61,8 @@ public class WebSocketMessage: Codable { self.command = nil } - public init(command: String, payload: [String: Any]? = nil) { - self.ID = -1 + public init(id: Int = -1, command: String, payload: [String: Any]? = nil) { + self.ID = id self.MessageType = "command" self.command = command self.Payload = payload