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