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