ErrorView.swift (8325B)
1 /* 2 * This file is part of GNU Taler, ©2022-26 Taler Systems S.A. 3 * See LICENSE.md 4 */ 5 /** 6 * @author Iván Ávalos 7 * @author Marc Stibane 8 */ 9 import SwiftUI 10 import taler_swift 11 12 enum BannerType { 13 case error 14 case networkUnavailable 15 case warning5sec 16 } 17 18 struct ErrorBanner: View { 19 let type: BannerType 20 21 init(_ type: BannerType) { 22 self.type = type 23 } 24 25 var errorText: String { 26 switch type { 27 case .error: 28 return "An error occurred…" 29 case .networkUnavailable: 30 return "Network unavailable!" 31 case .warning5sec: 32 return "Network is slow…" 33 } 34 } 35 36 var errorColor: Color { 37 switch type { 38 case .error: 39 return .red 40 case .networkUnavailable: 41 return .red 42 case .warning5sec: 43 return .yellow 44 } 45 } 46 47 var body: some View { 48 Text(errorText) 49 .foregroundStyle(.white) 50 .frame(maxWidth: .infinity, alignment: .leading) 51 .padding() 52 .background { 53 RoundedRectangle(cornerRadius: 15) 54 .fill(errorColor) 55 } 56 .padding(.horizontal) 57 .transition(.asymmetric( 58 insertion: .move(edge: .top), 59 removal: .move(edge: .top) 60 )) 61 } 62 } 63 64 enum ErrorData { 65 case message(title: String, message: String) 66 case error(Error) 67 } 68 69 struct ErrorView: View { 70 let stack: CallStack? 71 var title: String 72 var exclam: Bool 73 var message: String? = nil 74 var requestUrl: String? = nil 75 var copyable: Bool 76 77 var onDismiss: (() -> Void)? 78 79 init(_ stack: CallStack?, title: String, exclam: Bool = true, message: String? = nil, 80 requestUrl: String? = nil, copyable: Bool = false, onDismiss: (() -> Void)? = nil) { 81 self.stack = stack 82 self.title = title 83 self.exclam = exclam 84 self.message = message 85 self.requestUrl = requestUrl 86 self.copyable = copyable 87 self.onDismiss = onDismiss 88 } 89 90 init(_ stack: CallStack?, error: Error, devMode: Bool, onDismiss: (() -> Void)? = nil) { 91 let internetError = String(localized: "Network problem") 92 let internetMessage = String(localized: "Please check your internet connection and try again.") 93 let walletCoreError = String(localized: "Internal core error") 94 let initializationError = String(localized: "Initialization error") 95 let serializationError = String(localized: "Serialization error") 96 let deserializationError = String(localized: "Deserialization error") 97 let unknownError = String(localized: "Unknown error") 98 let userCancelled = String(localized: "User cancelled") 99 let transactionError = String(localized: "Transaction Error") 100 101 switch error { 102 case let walletError as WalletBackendError: 103 switch walletError { 104 case .walletCoreError(let walletBackendResponseError): 105 if let responseError = walletBackendResponseError { 106 let code = responseError.code 107 if code == 7002 || code == 7005 || code == 7032 || code == 7036 || code == 7049 { 108 self.init(stack, 109 title: code == 7036 ? userCancelled 110 : code == 7049 ? transactionError : internetError, 111 exclam: code != 7036 && code != 7049, 112 message: responseError.hint ?? internetMessage, 113 requestUrl: responseError.requestUrl, 114 copyable: false, 115 onDismiss: onDismiss) 116 break 117 } 118 let hint = responseError.errorResponse?.hint ?? responseError.hint // nested one preferred 119 if !devMode, let hint { 120 // prefer hint for regular users 121 self.init(stack, title: walletCoreError, message: hint, copyable: true, onDismiss: onDismiss) 122 break 123 } 124 if let json = responseError.toJSON() { 125 // raw JSON for developers, or if there is no hint 126 self.init(stack, title: walletCoreError, message: json, copyable: true, onDismiss: onDismiss) 127 break 128 } else if let hint { 129 // should never happen 130 self.init(stack, title: walletCoreError, message: hint, copyable: true, onDismiss: onDismiss) 131 break 132 // } else if let message = responseError.message { 133 // self.init(stack, title: walletCoreError, message: message, copyable: true, onDismiss: onDismiss) 134 // break 135 } 136 } 137 self.init(stack, title: walletCoreError, copyable: false, onDismiss: onDismiss) 138 case .initializationError: 139 self.init(stack, title: initializationError, copyable: false, onDismiss: onDismiss) 140 case .serializationError: 141 self.init(stack, title: serializationError, copyable: false, onDismiss: onDismiss) 142 case .deserializationError: 143 self.init(stack, title: deserializationError, copyable: false, onDismiss: onDismiss) 144 } 145 default: 146 self.init(stack, title: unknownError, message: error.localizedDescription, copyable: false, onDismiss: onDismiss) 147 } 148 } 149 150 init(_ stack: CallStack?, data: ErrorData, devMode: Bool, onDismiss: (() -> Void)? = nil) { 151 switch data { 152 case .message(let title, let message): 153 self.init(stack, title: title, message: message, copyable: false, onDismiss: onDismiss) 154 case .error(let error): 155 self.init(stack, error: error, devMode: devMode, onDismiss: onDismiss) 156 } 157 } 158 159 var body: some View { 160 ScrollView { 161 VStack { 162 if exclam { 163 Image(systemName: EXCLAMATION) 164 .resizable() 165 .frame(width: 50, height: 50) 166 .aspectRatio(contentMode: .fit) 167 .foregroundStyle(WalletColors().attention) 168 .padding() 169 } 170 Text(title) 171 .talerFont(.title) 172 .padding(.bottom) 173 174 if let message { 175 if copyable { 176 if #available(iOS 16.4, *) { 177 Text(message).monospaced() 178 } else { 179 Text(message).font(.system(.body, design: .monospaced)) 180 } 181 182 CopyButton(message, vertical: false) 183 .accessibilityLabel(Text("Copy the error JSON", comment: "a11y")) 184 .padding() 185 } else { 186 if let requestUrl { 187 Text(requestUrl) 188 .multilineTextAlignment(.center) 189 .padding() 190 } 191 Text(message) 192 .multilineTextAlignment(.center) 193 .padding() 194 } 195 } 196 } 197 } 198 .padding() 199 .safeAreaInset(edge: .bottom) { 200 Button("OK", role: .cancel) { 201 if let onDismiss { 202 onDismiss() 203 } else { 204 dismissTop(stack?.push()) 205 } 206 } 207 .buttonStyle(TalerButtonStyle(type: .bordered)) 208 .padding(.bottom) 209 .padding(.horizontal) 210 } 211 } 212 }