QRSheet.swift (5908B)
1 /* 2 * This file is part of GNU Taler, ©2022-26 Taler Systems S.A. 3 * See LICENSE.md 4 */ 5 /** 6 * @author Marc Stibane 7 */ 8 import SwiftUI 9 import CodeScanner 10 import SymLog 11 import AVFoundation 12 13 struct QRSheet: View { 14 private let symLog = SymLogV(0) 15 let stack: CallStack 16 let selectedBalance: Balance? 17 @Binding var scannedSomething: Bool 18 19 @EnvironmentObject private var model: WalletModel 20 @AppStorage("minimalistic") var minimalistic: Bool = false 21 22 @State private var scannedCode: String? 23 @State private var urlToOpen: URL? = nil 24 @State private var isTorchOn: Bool = false 25 26 func codeToURL(_ code: String) -> URL? { 27 if let scannedURL = URL(string: code) { 28 return scannedURL 29 } 30 if let encodedScan = code.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) { 31 if let encodedURL = URL(string: encodedScan) { 32 return encodedURL 33 } 34 } 35 return nil 36 } 37 38 var body: some View { 39 #if PRINT_CHANGES 40 let _ = Self._printChanges() 41 let _ = symLog.vlog(scannedCode) // just to get the # to compare it with .onAppear & onDisappear 42 #endif 43 Group { 44 let errorAnnouncement = String(localized: "Error while scanning QR code", comment: "a11y") 45 if scannedCode != nil { 46 // we need to evaluate the scanned code again, because urlToOpen will be set to nil 47 if let scannedURL = codeToURL(scannedCode!) { 48 let scheme = scannedURL.scheme 49 if scheme?.lowercased() == "taler" { 50 URLSheet(stack: stack.push(), 51 selectedBalance: selectedBalance, 52 urlToOpen: $urlToOpen) // !!! will set @Binding to nil 53 } else { 54 // let _ = print(scannedURL) // TODO: error logging 55 ErrorView(stack.push(), 56 title: String(localized: "Scanned QR is no talerURI"), 57 message: scannedURL.absoluteString, 58 copyable: true) { 59 scannedSomething = false 60 dismissTop(stack.push()) 61 } 62 } 63 } else { 64 ErrorView(stack.push(), 65 title: String(localized: "Scanned QR is no URL"), 66 message: scannedCode, 67 copyable: true 68 ) { 69 scannedSomething = false 70 dismissTop(stack.push()) 71 } 72 } 73 } else { 74 let codeScannerView = CodeScannerView(codeTypes: [AVMetadataObject.ObjectType.qr], 75 showViewfinder: true, 76 isTorchOn: isTorchOn 77 ) { response in 78 let closingAnnouncement: String 79 switch response { 80 case .success(let result): 81 symLog.log("Found code: \(result.string)") 82 scannedSomething = true 83 urlToOpen = codeToURL(result.string) 84 scannedCode = result.string 85 closingAnnouncement = String(localized: "QR code recognized", comment: "a11y") 86 case .failure(let error): 87 // TODO: errorAlert 88 scannedSomething = false 89 model.setError(error) 90 closingAnnouncement = errorAnnouncement 91 } 92 announce(closingAnnouncement) 93 } 94 95 if minimalistic { 96 codeScannerView 97 .onTapGesture { isTorchOn.toggle() } 98 } else if #available(iOS 16.4, *) { 99 codeScannerView 100 .toolbar { 101 ToolbarItem(placement: .topBarLeading) { 102 Button { 103 dismissTop(stack.push()) 104 } label: { 105 Image(systemName: XMARK) 106 } 107 .accessibilityHidden(true) // VoiceOver has its own "Dismiss Popup" button 108 } 109 ToolbarItem(placement: .topBarTrailing) { 110 let a11yValue = isTorchOn ? String(localized: "on", comment: "a11y") 111 : String(localized: "off", comment: "a11y") 112 Button { 113 isTorchOn.toggle() 114 } label: { 115 Image(systemName: isTorchOn ? LIGHT_ON : LIGHT_OFF) 116 } 117 .accessibilityLabel(Text("Torch for QR code scanning", comment: "a11y")) 118 .accessibilityValue(Text(a11yValue)) 119 } 120 } 121 .navigationBarTitleDisplayMode(.inline) 122 .toolbarBackground(.gray) 123 .toolbarBackground(.visible) 124 } else { 125 codeScannerView 126 .onTapGesture { isTorchOn.toggle() } 127 } 128 } 129 } 130 } 131 } 132 // MARK: - 133 //struct PaySheet_Previews: PreviewProvider { 134 // static var previews: some View { 135 // // needs BackendManager 136 // URLSheet(urlToOpen: URL(string: "ftp://this.URL.is.invalid")!) 137 // } 138 //}