From ea6e24e8ac0012397a502e9e3b99ea8967dc5453 Mon Sep 17 00:00:00 2001 From: Shafqat Muneer Date: Wed, 26 Jun 2024 16:47:20 +0500 Subject: [PATCH] feat: Program Screen Error Handling (#448) --- Core/Core.xcodeproj/project.pbxproj | 12 ++ .../FullScreenErrorView.swift | 97 ++++++++++++ Core/Core/View/Base/Webview/WebView.swift | 29 +++- Course/Course.xcodeproj/project.pbxproj | 14 +- .../CalendarSyncProgressView.swift | 0 .../DatesSuccessView}/DatesSuccessView.swift | 0 .../Presentation/Unit/CourseUnitView.swift | 32 +--- .../WebDiscovery/DiscoveryWebview.swift | 129 +++++++++------- .../DiscoveryWebviewViewModel.swift | 6 + .../WebPrograms/ProgramWebviewView.swift | 139 +++++++++++------- .../WebPrograms/ProgramWebviewViewModel.swift | 5 + 11 files changed, 321 insertions(+), 142 deletions(-) create mode 100644 Core/Core/View/Base/FullScreenErrorView/FullScreenErrorView.swift rename Course/Course/{Views => Presentation/Subviews/CalendarSyncProgressView}/CalendarSyncProgressView.swift (100%) rename Course/Course/{Views => Presentation/Subviews/DatesSuccessView}/DatesSuccessView.swift (100%) diff --git a/Core/Core.xcodeproj/project.pbxproj b/Core/Core.xcodeproj/project.pbxproj index ad4325f39..3e59ea038 100644 --- a/Core/Core.xcodeproj/project.pbxproj +++ b/Core/Core.xcodeproj/project.pbxproj @@ -144,6 +144,7 @@ 141F1D302B7328D4009E81EB /* WebviewCookiesUpdateProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 141F1D2F2B7328D4009E81EB /* WebviewCookiesUpdateProtocol.swift */; }; 142EDD6C2B831D1400F9F320 /* BranchSDK in Frameworks */ = {isa = PBXBuildFile; productRef = 142EDD6B2B831D1400F9F320 /* BranchSDK */; }; 14769D3C2B9822EE00AB36D4 /* CoreAnalytics.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14769D3B2B9822EE00AB36D4 /* CoreAnalytics.swift */; }; + 9784D47E2BF7762800AFEFFF /* FullScreenErrorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9784D47D2BF7762800AFEFFF /* FullScreenErrorView.swift */; }; A51CDBE72B6D21F2009B6D4E /* SegmentConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = A51CDBE62B6D21F2009B6D4E /* SegmentConfig.swift */; }; A53A32352B233DEC005FE38A /* ThemeConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = A53A32342B233DEC005FE38A /* ThemeConfig.swift */; }; A595689B2B6173DF00ED4F90 /* BranchConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = A595689A2B6173DF00ED4F90 /* BranchConfig.swift */; }; @@ -339,6 +340,7 @@ 349B90CD6579F7B8D257E515 /* Pods_App_Core.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_App_Core.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 3B74C6685E416657F3C5F5A8 /* Pods-App-Core.releaseprod.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-Core.releaseprod.xcconfig"; path = "Target Support Files/Pods-App-Core/Pods-App-Core.releaseprod.xcconfig"; sourceTree = ""; }; 60153262DBC2F9E660D7E11B /* Pods-App-Core.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-Core.release.xcconfig"; path = "Target Support Files/Pods-App-Core/Pods-App-Core.release.xcconfig"; sourceTree = ""; }; + 9784D47D2BF7762800AFEFFF /* FullScreenErrorView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FullScreenErrorView.swift; sourceTree = ""; }; 9D5B06CAA99EA5CD49CBE2BB /* Pods-App-Core.debugdev.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-Core.debugdev.xcconfig"; path = "Target Support Files/Pods-App-Core/Pods-App-Core.debugdev.xcconfig"; sourceTree = ""; }; A51CDBE62B6D21F2009B6D4E /* SegmentConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SegmentConfig.swift; sourceTree = ""; }; A53A32342B233DEC005FE38A /* ThemeConfig.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ThemeConfig.swift; sourceTree = ""; }; @@ -715,6 +717,7 @@ 0770DE7728D0C49E006D8A5D /* Base */ = { isa = PBXGroup; children = ( + 9784D47C2BF7761F00AFEFFF /* FullScreenErrorView */, 064987882B4D69FE0071642A /* Webview */, E0D586352B314CD3009B4BA7 /* LogistrationBottomView.swift */, 02A4833B29B8C57800D33F33 /* DownloadView.swift */, @@ -766,6 +769,14 @@ path = Analytics; sourceTree = ""; }; + 9784D47C2BF7761F00AFEFFF /* FullScreenErrorView */ = { + isa = PBXGroup; + children = ( + 9784D47D2BF7762800AFEFFF /* FullScreenErrorView.swift */, + ); + path = FullScreenErrorView; + sourceTree = ""; + }; BA30427C2B20B235009B64B7 /* SocialAuth */ = { isa = PBXGroup; children = ( @@ -1124,6 +1135,7 @@ 0260E58028FD792800BBBE18 /* WebUnitViewModel.swift in Sources */, 02A4833A29B8A9AB00D33F33 /* DownloadManager.swift in Sources */, 06078B702BA49C3100576798 /* Dictionary+JSON.swift in Sources */, + 9784D47E2BF7762800AFEFFF /* FullScreenErrorView.swift in Sources */, 027BD3AE2909475000392132 /* KeyboardScrollerOptions.swift in Sources */, BAFB99922B14E23D007D09F9 /* AppleSignInConfig.swift in Sources */, 141F1D302B7328D4009E81EB /* WebviewCookiesUpdateProtocol.swift in Sources */, diff --git a/Core/Core/View/Base/FullScreenErrorView/FullScreenErrorView.swift b/Core/Core/View/Base/FullScreenErrorView/FullScreenErrorView.swift new file mode 100644 index 000000000..dc5893621 --- /dev/null +++ b/Core/Core/View/Base/FullScreenErrorView/FullScreenErrorView.swift @@ -0,0 +1,97 @@ +// +// FullScreenErrorView.swift +// Course +// +// Created by Shafqat Muneer on 5/14/24. +// + +import SwiftUI +import Theme + +public struct FullScreenErrorView: View { + + public enum ErrorType { + case noInternet + case noInternetWithReload + case generic + } + + private let errorType: ErrorType + private var action: () -> Void = {} + + public init( + type: ErrorType + ) { + self.errorType = type + } + + public init( + type: ErrorType, + action: @escaping () -> Void + ) { + self.errorType = type + self.action = action + } + + public var body: some View { + GeometryReader { proxy in + VStack(spacing: 28) { + Spacer() + switch errorType { + case .noInternet, .noInternetWithReload: + CoreAssets.noWifi.swiftUIImage + .renderingMode(.template) + .foregroundStyle(Color.primary) + .scaledToFit() + + Text(CoreLocalization.Error.Internet.noInternetTitle) + .font(Theme.Fonts.titleLarge) + .foregroundColor(Theme.Colors.textPrimary) + + Text(CoreLocalization.Error.Internet.noInternetDescription) + .font(Theme.Fonts.bodyLarge) + .foregroundColor(Theme.Colors.textPrimary) + .multilineTextAlignment(.center) + .padding(.horizontal, 50) + case .generic: + CoreAssets.notAvaliable.swiftUIImage + .renderingMode(.template) + .foregroundStyle(Color.primary) + .scaledToFit() + + Text(CoreLocalization.View.Snackbar.tryAgainBtn) + .font(Theme.Fonts.titleLarge) + .foregroundColor(Theme.Colors.textPrimary) + + Text(CoreLocalization.Error.unknownError) + .font(Theme.Fonts.bodyLarge) + .foregroundColor(Theme.Colors.textPrimary) + .multilineTextAlignment(.center) + .padding(.horizontal, 50) + } + + if errorType != .noInternet { + UnitButtonView( + type: .reload, + action: { + self.action() + } + ) + } + Spacer() + } + .frame(maxWidth: .infinity, maxHeight: proxy.size.height) + .background( + Theme.Colors.background + ) + } + } +} + +#if DEBUG +struct FullScreenErrorView_Previews: PreviewProvider { + static var previews: some View { + FullScreenErrorView(type: .noInternetWithReload) + } +} +#endif diff --git a/Core/Core/View/Base/Webview/WebView.swift b/Core/Core/View/Base/Webview/WebView.swift index cb36ea255..1b12167db 100644 --- a/Core/Core/View/Base/Webview/WebView.swift +++ b/Core/Core/View/Base/Webview/WebView.swift @@ -17,6 +17,8 @@ public protocol WebViewNavigationDelegate: AnyObject { shouldLoad request: URLRequest, navigationAction: WKNavigationAction ) async -> Bool + + func showWebViewError() } public struct WebView: UIViewRepresentable { @@ -39,17 +41,20 @@ public struct WebView: UIViewRepresentable { var webViewNavDelegate: WebViewNavigationDelegate? var refreshCookies: () async -> Void + var webViewType: String? public init( viewModel: ViewModel, isLoading: Binding, refreshCookies: @escaping () async -> Void, - navigationDelegate: WebViewNavigationDelegate? = nil + navigationDelegate: WebViewNavigationDelegate? = nil, + webViewType: String? = nil ) { self.viewModel = viewModel self._isLoading = isLoading self.refreshCookies = refreshCookies self.webViewNavDelegate = navigationDelegate + self.webViewType = webViewType } public class Coordinator: NSObject, WKNavigationDelegate, WKUIDelegate, WKScriptMessageHandler { @@ -70,6 +75,10 @@ public struct WebView: UIViewRepresentable { public func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) { webView.isHidden = false + DispatchQueue.main.async { + self.parent.isLoading = false + self.parent.webViewNavDelegate?.showWebViewError() + } } public func webView( @@ -78,6 +87,10 @@ public struct WebView: UIViewRepresentable { withError error: Error ) { webView.isHidden = false + DispatchQueue.main.async { + self.parent.isLoading = false + self.parent.webViewNavDelegate?.showWebViewError() + } } public func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) { @@ -172,7 +185,7 @@ public struct WebView: UIViewRepresentable { private func addObservers() { cancellables.removeAll() - NotificationCenter.default.publisher(for: .webviewReloadNotification, object: nil) + NotificationCenter.default.publisher(for: Notification.Name(parent.webViewType ?? ""), object: nil) .sink { [weak self] _ in self?.reload() } @@ -188,8 +201,16 @@ public struct WebView: UIViewRepresentable { fileprivate var webview: WKWebView? @objc private func reload() { - parent.isLoading = true - webview?.reload() + DispatchQueue.main.async { + self.parent.isLoading = true + } + if webview?.url?.absoluteString.isEmpty ?? true, + let url = URL(string: parent.viewModel.url) { + let request = URLRequest(url: url) + webview?.load(request) + } else { + webview?.reload() + } } public func userContentController( diff --git a/Course/Course.xcodeproj/project.pbxproj b/Course/Course.xcodeproj/project.pbxproj index efc45a860..cc778b214 100644 --- a/Course/Course.xcodeproj/project.pbxproj +++ b/Course/Course.xcodeproj/project.pbxproj @@ -314,7 +314,6 @@ 02B6B3B528E1D10700232911 /* Domain */, 02EAE2CA28E1F0A700529644 /* Presentation */, 97EA4D822B84EFA900663F58 /* Managers */, - 97CA95212B875EA200A9EDEA /* Views */, 02B6B3B428E1C49400232911 /* Localizable.strings */, 02C355372C08DCD700501342 /* Localizable.stringsdict */, ); @@ -529,13 +528,20 @@ path = Mock; sourceTree = ""; }; - 97CA95212B875EA200A9EDEA /* Views */ = { + 9784D4752BF39EEF00AFEFFF /* CalendarSyncProgressView */ = { isa = PBXGroup; children = ( 97C99C352B9A08FE004EEDE2 /* CalendarSyncProgressView.swift */, + ); + path = CalendarSyncProgressView; + sourceTree = ""; + }; + 9784D4762BF39EFD00AFEFFF /* DatesSuccessView */ = { + isa = PBXGroup; + children = ( 97CA95242B875EE200A9EDEA /* DatesSuccessView.swift */, ); - path = Views; + path = DatesSuccessView; sourceTree = ""; }; 97EA4D822B84EFA900663F58 /* Managers */ = { @@ -586,6 +592,8 @@ BAD9CA482B2C88D500DE790A /* Subviews */ = { isa = PBXGroup; children = ( + 9784D4762BF39EFD00AFEFFF /* DatesSuccessView */, + 9784D4752BF39EEF00AFEFFF /* CalendarSyncProgressView */, 02D4FC2C2BBD7C7500C47748 /* MessageSectionView */, BAC0E0D92B32F0A2006B68A9 /* CourseVideoDownloadBarView */, BA58CF622B471047005B102E /* VideoDownloadQualityBarView */, diff --git a/Course/Course/Views/CalendarSyncProgressView.swift b/Course/Course/Presentation/Subviews/CalendarSyncProgressView/CalendarSyncProgressView.swift similarity index 100% rename from Course/Course/Views/CalendarSyncProgressView.swift rename to Course/Course/Presentation/Subviews/CalendarSyncProgressView/CalendarSyncProgressView.swift diff --git a/Course/Course/Views/DatesSuccessView.swift b/Course/Course/Presentation/Subviews/DatesSuccessView/DatesSuccessView.swift similarity index 100% rename from Course/Course/Views/DatesSuccessView.swift rename to Course/Course/Presentation/Subviews/DatesSuccessView/DatesSuccessView.swift diff --git a/Course/Course/Presentation/Unit/CourseUnitView.swift b/Course/Course/Presentation/Unit/CourseUnitView.swift index 44f1e4a1b..0248e2393 100644 --- a/Course/Course/Presentation/Unit/CourseUnitView.swift +++ b/Course/Course/Presentation/Unit/CourseUnitView.swift @@ -189,7 +189,7 @@ public struct CourseUnitView: View { Spacer(minLength: 150) } } else { - NoInternetView() + FullScreenErrorView(type: .noInternet) } } else { @@ -219,7 +219,7 @@ public struct CourseUnitView: View { Spacer(minLength: 150) } } else { - NoInternetView() + FullScreenErrorView(type: .noInternet) } } // MARK: Web @@ -233,7 +233,7 @@ public struct CourseUnitView: View { ) // not need to add frame limit there because we did that with injection } else { - NoInternetView() + FullScreenErrorView(type: .noInternet) } } else { EmptyView() @@ -247,7 +247,7 @@ public struct CourseUnitView: View { Spacer() .frame(minHeight: 100) } else { - NoInternetView() + FullScreenErrorView(type: .noInternet) } } else { EmptyView() @@ -275,7 +275,7 @@ public struct CourseUnitView: View { //No need iPad paddings there bacause they were added //to PostsView that placed inside DiscussionView } else { - NoInternetView() + FullScreenErrorView(type: .noInternet) } } else { EmptyView() @@ -586,25 +586,3 @@ struct CourseUnitView_Previews: PreviewProvider { } //swiftlint:enable all #endif - -struct NoInternetView: View { - - var body: some View { - VStack(spacing: 28) { - Spacer() - CoreAssets.noWifi.swiftUIImage - .renderingMode(.template) - .foregroundStyle(Color.primary) - .scaledToFit() - Text(CoreLocalization.Error.Internet.noInternetTitle) - .font(Theme.Fonts.titleLarge) - .foregroundColor(Theme.Colors.textPrimary) - Text(CoreLocalization.Error.Internet.noInternetDescription) - .font(Theme.Fonts.bodyLarge) - .foregroundColor(Theme.Colors.textPrimary) - .multilineTextAlignment(.center) - .padding(.horizontal, 50) - Spacer() - }.frame(maxWidth: .infinity, maxHeight: .infinity) - } -} diff --git a/Discovery/Discovery/Presentation/WebDiscovery/DiscoveryWebview.swift b/Discovery/Discovery/Presentation/WebDiscovery/DiscoveryWebview.swift index be01b0be4..b69bb3af9 100644 --- a/Discovery/Discovery/Presentation/WebDiscovery/DiscoveryWebview.swift +++ b/Discovery/Discovery/Presentation/WebDiscovery/DiscoveryWebview.swift @@ -14,13 +14,24 @@ public enum DiscoveryWebviewType: Equatable { case discovery case courseDetail(String) case programDetail(String) + + var rawValue: String { + switch self { + case .discovery: + return "discovery" + case .courseDetail(let value): + return "courseDetail(\(value))" + case .programDetail(let value): + return "programDetail(\(value))" + } + } } public struct DiscoveryWebview: View { @State private var searchQuery: String = "" @State private var isLoading: Bool = true - @ObservedObject private var viewModel: DiscoveryWebviewViewModel + @StateObject private var viewModel: DiscoveryWebviewViewModel private var router: DiscoveryRouter private var discoveryType: DiscoveryWebviewType public var pathID: String @@ -70,78 +81,86 @@ public struct DiscoveryWebview: View { discoveryType: DiscoveryWebviewType = .discovery, pathID: String = "" ) { - self.viewModel = viewModel + self._viewModel = .init(wrappedValue: viewModel) self.router = router self._searchQuery = State(initialValue: searchQuery ?? "") self.discoveryType = discoveryType self.pathID = pathID - - if let url = URL(string: URLString) { - viewModel.request = URLRequest(url: url) - } } public var body: some View { GeometryReader { proxy in - VStack(alignment: .center) { - WebView( - viewModel: .init( - url: URLString, - baseURL: "" - ), - isLoading: $isLoading, - refreshCookies: {}, - navigationDelegate: viewModel - ) - .accessibilityIdentifier("discovery_webview") - - if isLoading || viewModel.showProgress { - HStack(alignment: .center) { - ProgressBar( - size: 40, - lineWidth: 8 - ) - .padding(.vertical, proxy.size.height / 2) - .accessibilityIdentifier("progress_bar") + ZStack(alignment: .center) { + VStack(alignment: .center) { + WebView( + viewModel: .init( + url: URLString, + baseURL: "" + ), + isLoading: $isLoading, + refreshCookies: {}, + navigationDelegate: viewModel, + webViewType: discoveryType.rawValue + ) + .accessibilityIdentifier("discovery_webview") + + if isLoading || viewModel.showProgress { + HStack(alignment: .center) { + ProgressBar( + size: 40, + lineWidth: 8 + ) + .padding(.vertical, proxy.size.height / 2) + .accessibilityIdentifier("progress_bar") + } + .frame(width: proxy.size.width, height: proxy.size.height) } - .frame(width: proxy.size.width, height: proxy.size.height) - } - - // MARK: - Show Error - if viewModel.showError { - VStack { - SnackBarView(message: viewModel.errorMessage) + + // MARK: - Show Error + if viewModel.showError { + VStack { + SnackBarView(message: viewModel.errorMessage) + } + .padding(.bottom, 20) + .transition(.move(edge: .bottom)) + .onAppear { + doAfter(Theme.Timeout.snackbarMessageLongTimeout) { + viewModel.errorMessage = nil + } + } } - .padding(.bottom, 20) - .transition(.move(edge: .bottom)) - .onAppear { - doAfter(Theme.Timeout.snackbarMessageLongTimeout) { - viewModel.errorMessage = nil + + if !viewModel.userloggedIn, !isLoading { + LogistrationBottomView { buttonAction in + switch buttonAction { + case .signIn: + viewModel.router.showLoginScreen(sourceScreen: sourceScreen) + case .register: + viewModel.router.showRegisterScreen(sourceScreen: sourceScreen) + } } } } - if !viewModel.userloggedIn, !isLoading { - LogistrationBottomView { buttonAction in - switch buttonAction { - case .signIn: - viewModel.router.showLoginScreen(sourceScreen: sourceScreen) - case .register: - viewModel.router.showRegisterScreen(sourceScreen: sourceScreen) + if viewModel.webViewError { + FullScreenErrorView( + type: viewModel.connectivity.isInternetAvaliable ? .generic : .noInternetWithReload + ) { + if viewModel.connectivity.isInternetAvaliable { + viewModel.webViewError = false + NotificationCenter.default.post( + name: Notification.Name(discoveryType.rawValue), + object: nil + ) } } } } - - // MARK: - Offline mode SnackBar - OfflineSnackBarView( - connectivity: viewModel.connectivity, - reloadAction: { - NotificationCenter.default.post( - name: .webviewReloadNotification, - object: nil - ) - }) + .onFirstAppear { + if let url = URL(string: URLString) { + viewModel.request = URLRequest(url: url) + } + } } .navigationBarHidden(viewModel.sourceScreen == .default && discoveryType == .discovery) .navigationTitle(CoreLocalization.Mainscreen.discovery) diff --git a/Discovery/Discovery/Presentation/WebDiscovery/DiscoveryWebviewViewModel.swift b/Discovery/Discovery/Presentation/WebDiscovery/DiscoveryWebviewViewModel.swift index 836323072..e79afb8c1 100644 --- a/Discovery/Discovery/Presentation/WebDiscovery/DiscoveryWebviewViewModel.swift +++ b/Discovery/Discovery/Presentation/WebDiscovery/DiscoveryWebviewViewModel.swift @@ -14,6 +14,8 @@ public class DiscoveryWebviewViewModel: ObservableObject { @Published var courseDetails: CourseDetails? @Published private(set) var showProgress = false @Published var showError: Bool = false + @Published var webViewError: Bool = false + var errorMessage: String? { didSet { withAnimation { @@ -247,4 +249,8 @@ extension DiscoveryWebviewViewModel: WebViewNavigationDelegate { private func isValidAppURLScheme(_ url: URL) -> Bool { return url.scheme ?? "" == config.URIScheme } + + public func showWebViewError() { + self.webViewError = true + } } diff --git a/Discovery/Discovery/Presentation/WebPrograms/ProgramWebviewView.swift b/Discovery/Discovery/Presentation/WebPrograms/ProgramWebviewView.swift index ad28e6938..a646d5108 100644 --- a/Discovery/Discovery/Presentation/WebPrograms/ProgramWebviewView.swift +++ b/Discovery/Discovery/Presentation/WebPrograms/ProgramWebviewView.swift @@ -10,7 +10,7 @@ import SwiftUI import Theme import Core -public enum ProgramViewType: Equatable { +public enum ProgramViewType: String, Equatable { case program case programDetail } @@ -18,7 +18,7 @@ public enum ProgramViewType: Equatable { public struct ProgramWebviewView: View { @State private var isLoading: Bool = true - @ObservedObject private var viewModel: ProgramWebviewViewModel + @StateObject private var viewModel: ProgramWebviewViewModel private var router: DiscoveryRouter private var viewType: ProgramViewType public var pathID: String @@ -42,71 +42,84 @@ public struct ProgramWebviewView: View { viewType: ProgramViewType = .program, pathID: String = "" ) { - self.viewModel = viewModel + self._viewModel = .init(wrappedValue: viewModel) self.router = router self.viewType = viewType self.pathID = pathID - - if let url = URL(string: URLString) { - viewModel.request = URLRequest(url: url) - } } public var body: some View { GeometryReader { proxy in - VStack(alignment: .center) { - WebView( - viewModel: .init( - url: URLString, - baseURL: "", - injections: [.colorInversionCss] - ), - isLoading: $isLoading, - refreshCookies: { - await viewModel.updateCookies( - force: true - ) - }, - navigationDelegate: viewModel - ) - .accessibilityIdentifier("program_webview") - - if isLoading || viewModel.showProgress || viewModel.updatingCookies { - HStack(alignment: .center) { - ProgressBar( - size: 40, - lineWidth: 8 - ) - .padding(.vertical, proxy.size.height / 2) - .accessibilityIdentifier("progress_bar") + ZStack(alignment: .center) { + VStack(alignment: .center) { + WebView( + viewModel: .init( + url: URLString, + baseURL: "", + injections: [.colorInversionCss] + ), + isLoading: $isLoading, + refreshCookies: { + await viewModel.updateCookies( + force: true + ) + }, + navigationDelegate: viewModel, + webViewType: viewType.rawValue + ) + .accessibilityIdentifier("program_webview") + + let shouldShowProgress = ( + isLoading || + viewModel.showProgress || + viewModel.updatingCookies + ) + if shouldShowProgress { + HStack(alignment: .center) { + ProgressBar( + size: 40, + lineWidth: 8 + ) + .padding(.vertical, proxy.size.height / 2) + .accessibilityIdentifier("progress_bar") + } + .frame(width: proxy.size.width, height: proxy.size.height) + } + + // MARK: - Show Error + if viewModel.showError { + VStack { + SnackBarView(message: viewModel.errorMessage) + } + .padding(.bottom, 20) + .transition(.move(edge: .bottom)) + .onAppear { + doAfter(Theme.Timeout.snackbarMessageLongTimeout) { + viewModel.errorMessage = nil + } + } } - .frame(width: proxy.size.width, height: proxy.size.height) } - // MARK: - Show Error - if viewModel.showError { - VStack { - SnackBarView(message: viewModel.errorMessage) - } - .padding(.bottom, 20) - .transition(.move(edge: .bottom)) - .onAppear { - doAfter(Theme.Timeout.snackbarMessageLongTimeout) { - viewModel.errorMessage = nil + if viewModel.webViewError { + FullScreenErrorView( + type: viewModel.connectivity.isInternetAvaliable ? .generic : .noInternetWithReload + ) { + if viewModel.connectivity.isInternetAvaliable { + viewModel.webViewError = false + NotificationCenter.default.post( + name: Notification.Name(viewType.rawValue), + object: nil + ) } } } } - - // MARK: - Offline mode SnackBar - OfflineSnackBarView( - connectivity: viewModel.connectivity, - reloadAction: { - NotificationCenter.default.post( - name: .webviewReloadNotification, - object: nil - ) - }) + .onFirstAppear { + if let url = URL(string: URLString) { + viewModel.request = URLRequest(url: url) + } + } } .navigationBarHidden(viewType == .program) .navigationTitle(CoreLocalization.Mainscreen.programs) @@ -114,3 +127,23 @@ public struct ProgramWebviewView: View { .animation(.default, value: viewModel.showError) } } + +#if DEBUG +struct ProgramWebviewView_Previews: PreviewProvider { + static var previews: some View { + ProgramWebviewView( + viewModel: ProgramWebviewViewModel( + router: DiscoveryRouterMock(), + config: ConfigMock(), + interactor: DiscoveryInteractor.mock, + connectivity: Connectivity(), + analytics: DiscoveryAnalyticsMock(), + authInteractor: AuthInteractor.mock + ), + router: DiscoveryRouterMock(), + viewType: .program, + pathID: "" + ) + } +} +#endif diff --git a/Discovery/Discovery/Presentation/WebPrograms/ProgramWebviewViewModel.swift b/Discovery/Discovery/Presentation/WebPrograms/ProgramWebviewViewModel.swift index ad0c89987..34ad47de2 100644 --- a/Discovery/Discovery/Presentation/WebPrograms/ProgramWebviewViewModel.swift +++ b/Discovery/Discovery/Presentation/WebPrograms/ProgramWebviewViewModel.swift @@ -14,6 +14,7 @@ public class ProgramWebviewViewModel: ObservableObject, WebviewCookiesUpdateProt @Published var courseDetails: CourseDetails? @Published private(set) var showProgress = false @Published var showError: Bool = false + @Published var webViewError: Bool = false @Published public var updatingCookies: Bool = false @Published public var cookiesReady: Bool = false @@ -235,4 +236,8 @@ extension ProgramWebviewViewModel: WebViewNavigationDelegate { private func isValidAppURLScheme(_ url: URL) -> Bool { return url.scheme ?? "" == config.URIScheme } + + public func showWebViewError() { + self.webViewError = true + } }