taler-ios

iOS apps for GNU Taler (wallet)
Log | Files | Refs | README | LICENSE

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 }