ErrorView.swift (7425B)
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 @State var isCopied: Bool = false 76 77 var onDismiss: () -> Void 78 79 init(_ stack: CallStack, title: String, message: String? = nil, 80 requestUrl: String? = nil, copyable: Bool, onDismiss: @escaping () -> Void) { 81 self.stack = stack 82 self.title = title 83 self.message = message 84 self.requestUrl = requestUrl 85 self.copyable = copyable 86 self.onDismiss = onDismiss 87 } 88 89 init(_ stack: CallStack, error: Error, devMode: Bool, onDismiss: @escaping () -> Void) { 90 let internetError = String(localized: "Network problem") 91 let internetMessage = String(localized: "Please check your internet connection and try again.") 92 let walletCoreError = String(localized: "Internal core error") 93 let initializationError = String(localized: "Initialization error") 94 let serializationError = String(localized: "Serialization error") 95 let deserializationError = String(localized: "Deserialization error") 96 let unknownError = String(localized: "Unknown error") 97 let userCancelled = String(localized: "User cancelled") 98 99 switch error { 100 case let walletError as WalletBackendError: 101 switch walletError { 102 case .walletCoreError(let walletBackendResponseError): 103 if let responseError = walletBackendResponseError { 104 let code = responseError.code 105 if code == 7002 || code == 7005 || code == 7032 || code == 7036 { 106 self.init(stack, 107 title: code == 7036 ? userCancelled : internetError, 108 message: responseError.hint ?? internetMessage, 109 requestUrl: responseError.requestUrl, 110 copyable: false, 111 onDismiss: onDismiss) 112 break 113 } 114 // for now, always show JSON and allow to copy it 115 if let json = responseError.toJSON() { 116 self.init(stack, title: walletCoreError, message: json, copyable: true, onDismiss: onDismiss) 117 break 118 // } else if let hint = responseError.errorResponse?.hint ?? responseError.hint { 119 // self.init(stack, title: walletCoreError, message: hint, copyable: true, onDismiss: onDismiss) 120 // break 121 // } else if let message = responseError.message { 122 // self.init(stack, title: walletCoreError, message: message, copyable: true, onDismiss: onDismiss) 123 // break 124 } 125 } 126 self.init(stack, title: walletCoreError, copyable: false, onDismiss: onDismiss) 127 case .initializationError: 128 self.init(stack, title: initializationError, copyable: false, onDismiss: onDismiss) 129 case .serializationError: 130 self.init(stack, title: serializationError, copyable: false, onDismiss: onDismiss) 131 case .deserializationError: 132 self.init(stack, title: deserializationError, copyable: false, onDismiss: onDismiss) 133 } 134 default: 135 self.init(stack, title: unknownError, message: error.localizedDescription, copyable: false, onDismiss: onDismiss) 136 } 137 } 138 139 init(_ stack: CallStack, data: ErrorData, devMode: Bool, onDismiss: @escaping () -> Void) { 140 switch data { 141 case .message(let title, let message): 142 self.init(stack, title: title, message: message, copyable: false, onDismiss: onDismiss) 143 case .error(let error): 144 self.init(stack, error: error, devMode: devMode, onDismiss: onDismiss) 145 } 146 } 147 148 var body: some View { 149 ScrollView { 150 VStack { 151 Image(systemName: EXCLAMATION) 152 .resizable() 153 .frame(width: 50, height: 50) 154 .aspectRatio(contentMode: .fit) 155 .foregroundStyle(WalletColors().attention) 156 .padding() 157 158 Text(title) 159 .talerFont(.title) 160 .padding(.bottom) 161 162 if let message { 163 if copyable { 164 if #available(iOS 16.4, *) { 165 Text(message).monospaced() 166 } else { 167 Text(message).font(.system(.body, design: .monospaced)) 168 } 169 170 CopyButton(textToCopy: message, isCopied: $isCopied, vertical: false) 171 .accessibilityLabel(Text("Copy the error JSON", comment: "a11y")) 172 .padding() 173 } else { 174 if let requestUrl { 175 Text(requestUrl) 176 .multilineTextAlignment(.center) 177 .padding() 178 } 179 Text(message) 180 .multilineTextAlignment(.center) 181 .padding() 182 } 183 } 184 } 185 } 186 .padding() 187 .safeAreaInset(edge: .bottom) { 188 Button("OK", role: .cancel) { 189 onDismiss() 190 } 191 .buttonStyle(TalerButtonStyle(type: .bordered)) 192 .padding(.bottom) 193 .padding(.horizontal) 194 } 195 } 196 }