LoadingView.swift (6882B)
1 /* 2 * This file is part of GNU Taler, ©2022-25 Taler Systems S.A. 3 * See LICENSE.md 4 */ 5 /** 6 * @author Marc Stibane 7 */ 8 import SwiftUI 9 import SymLog 10 11 struct StalledView: View { 12 13 @EnvironmentObject private var controller: Controller 14 @EnvironmentObject private var model: WalletModel 15 16 @State private var isFinished: Bool? = false 17 18 @ViewBuilder 19 func retryButton(disabled: Bool) -> some View { 20 Button("Retry now") { 21 if let token = controller.progressToken { 22 if let operation = controller.progressOperation { 23 controller.lastProgressError = nil 24 Task { 25 try await model.retryProgressTokenNow(operation, token: token) 26 } 27 } 28 } 29 } 30 .buttonStyle(TalerButtonStyle(type: .bordered, disabled: disabled)) 31 .padding(.horizontal) 32 } 33 34 fileprivate func toJSON(_ lastError: RequestProgressError?) -> String? { 35 if let lastError { 36 do { 37 let jsonData = try JSONEncoder().encode(lastError) 38 if let jsonString = String(data: jsonData, encoding: .utf8) { 39 return jsonString 40 } 41 } catch { // JSON encoding of error failed / should never happen 42 return "Couldn't encode error" 43 } 44 } 45 return nil 46 } 47 48 var body: some View { 49 Group { 50 let lastError = controller.lastProgressError 51 let fallback = String(localized: "It seems that there is a problem with the network.") 52 Text("Couldn't establish a connection") 53 .talerFont(.title2) 54 .padding(.vertical) 55 let hint = lastError?.error.hint 56 ?? lastError?.error.errorResponse?.hint 57 Text(hint ?? fallback) 58 .talerFont(.body) 59 if let jsonString = toJSON(lastError) { 60 CopyButton(jsonString, title: "Copy Details") 61 .padding(.vertical) 62 } 63 if let lastPhase = controller.lastProgressPhase { 64 if lastPhase.phase == .stalled { 65 if let lastError, let delay = lastError.nextRetryDelay { 66 ZStack { 67 TimeView("Next retry in:", seconds: Int(delay.toSeconds()), isFinished: $isFinished) 68 .talerFont(.title3) 69 .padding(.vertical) 70 .opacity(isFinished == false ? 1 : 0) 71 Text("Retrying…") 72 .talerFont(.title3) 73 .opacity(isFinished == true ? 1 : 0) 74 Text("Waiting for response…") 75 .talerFont(.title3) 76 .opacity(isFinished == nil ? 1 : 0) 77 } 78 retryButton(disabled: isFinished ?? false) 79 } 80 } 81 } 82 } 83 .listRowSeparator(.hidden) 84 .onChange(of: controller.errorConnection) { isError in 85 if isError { 86 // print("❗️❗️❗️ error arrived, idle waiting starts") 87 withAnimation { 88 isFinished = false 89 } 90 controller.errorConnection = false 91 } 92 } 93 } 94 } 95 96 struct LoadingView: View { 97 private let symLog = SymLogV(0) 98 let stack: CallStack 99 let scopeInfo: ScopeInfo? 100 let message: String? 101 102 // let backButtonHidden: Bool 103 let navTitle = String(localized: "Loading…") 104 105 @EnvironmentObject private var controller: Controller 106 @EnvironmentObject private var model: WalletModel 107 #if DEBUG 108 @AppStorage("developerMode") var developerMode: Bool = true 109 #else 110 @AppStorage("developerMode") var developerMode: Bool = false 111 #endif 112 113 @State private var rotationEnabled = true 114 @State private var disabled: Bool = false 115 116 private var dismissButton: some View { 117 Button("Cancel", role: .cancel) { 118 if let token = controller.progressToken { 119 controller.progressToken = nil 120 if let operation = controller.progressOperation { 121 controller.progressOperation = nil 122 // model.setError(nil) 123 Task { 124 try await model.cancelProgressToken(operation, token: token) 125 if !developerMode { 126 model.setError(nil) 127 } 128 } 129 } 130 if !developerMode { 131 dismissTop(stack.push()) 132 } 133 } else { 134 dismissTop(stack.push()) 135 } 136 } 137 .buttonStyle(TalerButtonStyle(type: .bordered, disabled: false)) 138 .padding(.horizontal) 139 } 140 141 142 var body: some View { 143 #if DEBUG2 144 let background = developerMode ? Color.blue : nil 145 #else 146 let background: Color? = nil 147 #endif 148 let rotatingTaler = RotatingTaler(size: 100, 149 progress: true, // VoiceOver "In progress" 150 once: false, 151 background: background, 152 rotationEnabled: $rotationEnabled) 153 .id("rotatingTaler") 154 let view = VStack(alignment: .center) { 155 Spacer() 156 rotatingTaler 157 Spacer() 158 if controller.stalledConnection { 159 StalledView() 160 } else { 161 if let scopeInfo { 162 if let urlStr = scopeInfo.url { 163 Text(urlStr.trimURL) 164 .talerFont(.title2) 165 } else { 166 Text(scopeInfo.currency) 167 .talerFont(.title2) 168 } 169 Spacer() 170 } 171 Text(message ?? EMPTYSTRING) 172 .talerFont(.title2) 173 Spacer() 174 } 175 dismissButton 176 } 177 .frame(maxWidth: .infinity) 178 .padding(.horizontal) 179 .talerFont(.title) 180 .navigationTitle(navTitle) 181 .background(FullBackground()) 182 183 if #available(iOS 17.0, *) { 184 view.toolbarTitleDisplayMode(.inlineLarge) 185 } else { 186 view 187 } 188 } 189 } 190 // MARK: - 191 struct LoadingView_Previews: PreviewProvider { 192 static var previews: some View { 193 NavigationView { 194 LoadingView(stack: CallStack("Loading"), scopeInfo: nil, message: "test message") // , backButtonHidden: true) 195 .navigationBarTitleDisplayMode(.automatic) 196 }.navigationViewStyle(.stack) 197 } 198 }