ErrorView.swift (10067B)
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 @ScaledMetric var frameHeight: CGFloat = 60 // relative to fontSize 48 49 var body: some View { 50 let text = Text(errorText) 51 .foregroundStyle(.white) 52 .talerFont(.title2) 53 Group { 54 if #available(iOS 26.0, *) { 55 ContainerRelativeShape() 56 .fill(errorColor) 57 .frame(height: frameHeight) 58 .overlay { 59 ZStack { 60 VStack(spacing: 0) { 61 Color.clear 62 Rectangle() 63 .fill(errorColor) 64 } 65 text 66 } 67 } 68 } else { // ContainerRelativeShape exists since iOS 14, but doesn't work correctly 69 text 70 .frame(maxWidth: .infinity, alignment: .center) 71 .frame(height: frameHeight) 72 .background { 73 let radius = 12.0 74 if #available(iOS 16.4, *) { 75 let radii = RectangleCornerRadii(topLeading: radius, 76 bottomLeading: 0, 77 bottomTrailing: 0, 78 topTrailing: radius) 79 UnevenRoundedRectangle(cornerRadii: radii, style: .continuous) 80 .fill(errorColor) 81 } else { 82 RoundedRectangle(cornerRadius: radius) 83 .fill(errorColor) 84 .overlay { 85 VStack(spacing: 0) { 86 Color.clear 87 Rectangle() 88 .fill(errorColor) 89 } 90 } 91 } 92 } 93 } 94 } 95 .transition(.move(edge: .top).combined(with: .scale)) 96 } 97 } 98 99 enum ErrorData { 100 case message(title: String, message: String) 101 case error(Error) 102 } 103 104 struct ErrorView: View { 105 let stack: CallStack? 106 var title: String 107 var exclam: Bool 108 var message: String? = nil 109 var requestUrl: String? = nil 110 var copyable: Bool 111 112 var onDismiss: (() -> Void)? 113 114 init(_ stack: CallStack?, title: String, exclam: Bool = true, message: String? = nil, 115 requestUrl: String? = nil, copyable: Bool = false, onDismiss: (() -> Void)? = nil) { 116 self.stack = stack 117 self.title = title 118 self.exclam = exclam 119 self.message = message 120 self.requestUrl = requestUrl 121 self.copyable = copyable 122 self.onDismiss = onDismiss 123 } 124 125 init(_ stack: CallStack?, error: Error, devMode: Bool, onDismiss: (() -> Void)? = nil) { 126 let internetError = String(localized: "Network problem") 127 let internetMessage = String(localized: "Please check your internet connection and try again.") 128 let walletCoreError = String(localized: "Internal core error") 129 let initializationError = String(localized: "Initialization error") 130 let serializationError = String(localized: "Serialization error") 131 let deserializationError = String(localized: "Deserialization error") 132 let unknownError = String(localized: "Unknown error") 133 let userCancelled = String(localized: "User cancelled") 134 let transactionError = String(localized: "Transaction Error") 135 136 switch error { 137 case let walletError as WalletBackendError: 138 switch walletError { 139 case .walletCoreError(let walletBackendResponseError): 140 if let responseError = walletBackendResponseError { 141 let code = responseError.code 142 if code == 7002 || code == 7003 || code == 7005 || code == 7032 || code == 7036 || code == 7049 { 143 self.init(stack, 144 title: code == 7036 ? userCancelled 145 : code == 7049 ? transactionError : internetError, 146 exclam: code != 7036 && code != 7049, 147 message: responseError.hint ?? internetMessage, 148 requestUrl: responseError.requestUrl, 149 copyable: false, 150 onDismiss: onDismiss) 151 break 152 } 153 let hint = responseError.errorResponse?.hint ?? responseError.hint // nested one preferred 154 if !devMode, let hint { 155 // prefer hint for regular users 156 self.init(stack, title: walletCoreError, message: hint, copyable: true, onDismiss: onDismiss) 157 break 158 } 159 if let json = responseError.toJSON() { 160 // raw JSON for developers, or if there is no hint 161 self.init(stack, title: walletCoreError, message: json, copyable: true, onDismiss: onDismiss) 162 break 163 } else if let hint { 164 // should never happen 165 self.init(stack, title: walletCoreError, message: hint, copyable: true, onDismiss: onDismiss) 166 break 167 // } else if let message = responseError.message { 168 // self.init(stack, title: walletCoreError, message: message, copyable: true, onDismiss: onDismiss) 169 // break 170 } 171 } 172 self.init(stack, title: walletCoreError, copyable: false, onDismiss: onDismiss) 173 case .initializationError: 174 self.init(stack, title: initializationError, copyable: false, onDismiss: onDismiss) 175 case .serializationError: 176 self.init(stack, title: serializationError, copyable: false, onDismiss: onDismiss) 177 case .deserializationError: 178 self.init(stack, title: deserializationError, copyable: false, onDismiss: onDismiss) 179 } 180 default: 181 self.init(stack, title: unknownError, message: error.localizedDescription, copyable: false, onDismiss: onDismiss) 182 } 183 } 184 185 init(_ stack: CallStack?, data: ErrorData, devMode: Bool, onDismiss: (() -> Void)? = nil) { 186 switch data { 187 case .message(let title, let message): 188 self.init(stack, title: title, message: message, copyable: false, onDismiss: onDismiss) 189 case .error(let error): 190 self.init(stack, error: error, devMode: devMode, onDismiss: onDismiss) 191 } 192 } 193 194 var body: some View { 195 ScrollView { 196 VStack { 197 if exclam { 198 Image(systemName: EXCLAMATION) 199 .resizable() 200 .frame(width: 50, height: 50) 201 .aspectRatio(contentMode: .fit) 202 .foregroundStyle(WalletColors().attention) 203 .padding() 204 } 205 Text(title) 206 .talerFont(.title) 207 .padding(.bottom) 208 209 if let message { 210 if copyable { 211 if #available(iOS 16.4, *) { 212 Text(message).monospaced() 213 } else { 214 Text(message).font(.system(.body, design: .monospaced)) 215 } 216 217 CopyButton(message, vertical: false) 218 .accessibilityLabel(Text("Copy the error JSON", comment: "a11y")) 219 .padding() 220 } else { 221 if let requestUrl { 222 Text(requestUrl) 223 .multilineTextAlignment(.center) 224 .padding() 225 } 226 Text(message) 227 .multilineTextAlignment(.center) 228 .padding() 229 } 230 } 231 } 232 } 233 .padding() 234 .safeAreaInset(edge: .bottom) { 235 Button("OK", role: .cancel) { 236 if let onDismiss { 237 onDismiss() 238 } else { 239 dismissTop(stack?.push()) 240 } 241 } 242 .buttonStyle(TalerButtonStyle(type: .bordered)) 243 .padding(.bottom) 244 .padding(.horizontal) 245 } 246 } 247 }