From b73c0c305fdb091ea0cf4456d6246ca12c756800 Mon Sep 17 00:00:00 2001 From: DemianRaccoonGang Date: Wed, 30 Jul 2025 08:19:11 +0300 Subject: [PATCH 01/19] fix: resolve issue 581 --- Core/Core.xcodeproj/project.pbxproj | 4 --- .../Container/CourseContainerViewModel.swift | 33 +++++++++++++++++-- .../Outline/CourseOutlineView.swift | 2 ++ Podfile.lock | 2 +- 4 files changed, 34 insertions(+), 7 deletions(-) diff --git a/Core/Core.xcodeproj/project.pbxproj b/Core/Core.xcodeproj/project.pbxproj index 7c3cd8479..c60200710 100644 --- a/Core/Core.xcodeproj/project.pbxproj +++ b/Core/Core.xcodeproj/project.pbxproj @@ -1080,14 +1080,10 @@ inputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-App-Core-CoreTests/Pods-App-Core-CoreTests-resources-${CONFIGURATION}-input-files.xcfilelist", ); - inputPaths = ( - ); name = "[CP] Copy Pods Resources"; outputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-App-Core-CoreTests/Pods-App-Core-CoreTests-resources-${CONFIGURATION}-output-files.xcfilelist", ); - outputPaths = ( - ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-App-Core-CoreTests/Pods-App-Core-CoreTests-resources.sh\"\n"; diff --git a/Course/Course/Presentation/Container/CourseContainerViewModel.swift b/Course/Course/Presentation/Container/CourseContainerViewModel.swift index b6da7b531..a8162b51d 100644 --- a/Course/Course/Presentation/Container/CourseContainerViewModel.swift +++ b/Course/Course/Presentation/Container/CourseContainerViewModel.swift @@ -193,11 +193,40 @@ public final class CourseContainerViewModel: BaseCourseViewModel { sequentialIndex: continueWith.sequentialIndex ) } - + + private func getCourseBlocksWithTimeout( + courseID: String, + timeoutSeconds: UInt64 + ) async throws -> CourseStructure? { + return try await withThrowingTaskGroup(of: CourseStructure?.self) { group in + + group.addTask { + try await self.interactor.getCourseBlocks(courseID: courseID) + } + + group.addTask { + try await Task.sleep(nanoseconds: timeoutSeconds * 1_000_000_000) + return nil + } + + guard let firstResult = try await group.next() else { + return nil + } + + group.cancelAll() + + return firstResult + } + } + @MainActor func getCourseStructure(courseID: String) async throws -> CourseStructure? { if isInternetAvaliable { - return try await interactor.getCourseBlocks(courseID: courseID) + if let test = try await getCourseBlocksWithTimeout(courseID: courseID, timeoutSeconds: 15) { + return test + } + + return try await interactor.getLoadedCourseBlocks(courseID: courseID) } else { return try await interactor.getLoadedCourseBlocks(courseID: courseID) } diff --git a/Course/Course/Presentation/Outline/CourseOutlineView.swift b/Course/Course/Presentation/Outline/CourseOutlineView.swift index 2c2c8565e..8872771a2 100644 --- a/Course/Course/Presentation/Outline/CourseOutlineView.swift +++ b/Course/Course/Presentation/Outline/CourseOutlineView.swift @@ -67,7 +67,9 @@ public struct CourseOutlineView: View { collapsed: $collapsed, viewHeight: $viewHeight ) + RefreshProgressView(isShowRefresh: $viewModel.isShowRefresh) + VStack(alignment: .leading) { if isVideo, diff --git a/Podfile.lock b/Podfile.lock index ab2424669..92ba93fc7 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -36,4 +36,4 @@ SPEC CHECKSUMS: PODFILE CHECKSUM: 2d71ad797d49fa32b47c3315b92159de82824103 -COCOAPODS: 1.16.2 +COCOAPODS: 1.15.2 From cc0e655031941766bb2d8fc1c7d6c65e70d1b0bd Mon Sep 17 00:00:00 2001 From: DemianRaccoonGang Date: Wed, 30 Jul 2025 11:10:56 +0300 Subject: [PATCH 02/19] fix: update adjustments --- .../Presentation/Container/CourseContainerViewModel.swift | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Course/Course/Presentation/Container/CourseContainerViewModel.swift b/Course/Course/Presentation/Container/CourseContainerViewModel.swift index a8162b51d..5cfd87ead 100644 --- a/Course/Course/Presentation/Container/CourseContainerViewModel.swift +++ b/Course/Course/Presentation/Container/CourseContainerViewModel.swift @@ -225,7 +225,9 @@ public final class CourseContainerViewModel: BaseCourseViewModel { if let test = try await getCourseBlocksWithTimeout(courseID: courseID, timeoutSeconds: 15) { return test } - + connectivity.internetReachableSubject.send(.notReachable) + isShowProgress = false + isShowRefresh = false return try await interactor.getLoadedCourseBlocks(courseID: courseID) } else { return try await interactor.getLoadedCourseBlocks(courseID: courseID) From 84a215601746183af43bbd82d6390409ac8b7b65 Mon Sep 17 00:00:00 2001 From: DemianRaccoonGang Date: Wed, 30 Jul 2025 11:22:35 +0300 Subject: [PATCH 03/19] fix: add timeout constant --- .../Presentation/Container/CourseContainerViewModel.swift | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Course/Course/Presentation/Container/CourseContainerViewModel.swift b/Course/Course/Presentation/Container/CourseContainerViewModel.swift index 5cfd87ead..2496e1f52 100644 --- a/Course/Course/Presentation/Container/CourseContainerViewModel.swift +++ b/Course/Course/Presentation/Container/CourseContainerViewModel.swift @@ -109,6 +109,9 @@ public final class CourseContainerViewModel: BaseCourseViewModel { private let interactor: CourseInteractorProtocol private let authInteractor: AuthInteractorProtocol + + private let timeOutIntervalSeconds: UInt64 = 15 + let analytics: CourseAnalytics let coreAnalytics: CoreAnalytics private(set) var storage: CourseStorage @@ -222,7 +225,7 @@ public final class CourseContainerViewModel: BaseCourseViewModel { @MainActor func getCourseStructure(courseID: String) async throws -> CourseStructure? { if isInternetAvaliable { - if let test = try await getCourseBlocksWithTimeout(courseID: courseID, timeoutSeconds: 15) { + if let test = try await getCourseBlocksWithTimeout(courseID: courseID, timeoutSeconds: timeOutIntervalSeconds) { return test } connectivity.internetReachableSubject.send(.notReachable) From cd349b3f22fcb06008f2991bc895f926575d89ab Mon Sep 17 00:00:00 2001 From: DemianRaccoonGang Date: Wed, 30 Jul 2025 11:30:59 +0300 Subject: [PATCH 04/19] fix: adjust naming --- .../Presentation/Container/CourseContainerViewModel.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Course/Course/Presentation/Container/CourseContainerViewModel.swift b/Course/Course/Presentation/Container/CourseContainerViewModel.swift index 2496e1f52..74e40265b 100644 --- a/Course/Course/Presentation/Container/CourseContainerViewModel.swift +++ b/Course/Course/Presentation/Container/CourseContainerViewModel.swift @@ -225,8 +225,8 @@ public final class CourseContainerViewModel: BaseCourseViewModel { @MainActor func getCourseStructure(courseID: String) async throws -> CourseStructure? { if isInternetAvaliable { - if let test = try await getCourseBlocksWithTimeout(courseID: courseID, timeoutSeconds: timeOutIntervalSeconds) { - return test + if let courseStructure = try await getCourseBlocksWithTimeout(courseID: courseID, timeoutSeconds: timeOutIntervalSeconds) { + return courseStructure } connectivity.internetReachableSubject.send(.notReachable) isShowProgress = false From 5aa8e55f0cde4c6c6580338402fd2156ef5de2f9 Mon Sep 17 00:00:00 2001 From: DemianRaccoonGang Date: Wed, 30 Jul 2025 16:42:23 +0300 Subject: [PATCH 05/19] fix: update connectivity --- Core/Core/Configuration/Connectivity.swift | 110 +++++++++++++----- .../Container/CourseContainerViewModel.swift | 8 +- 2 files changed, 80 insertions(+), 38 deletions(-) diff --git a/Core/Core/Configuration/Connectivity.swift b/Core/Core/Configuration/Connectivity.swift index 864e10e9f..741791b19 100644 --- a/Core/Core/Configuration/Connectivity.swift +++ b/Core/Core/Configuration/Connectivity.swift @@ -1,9 +1,9 @@ -// -// Connectivity.swift -// OpenEdX -// -// Created by  Stepanok Ivan on 15.12.2022. -// +//// +//// Connectivity.swift +//// OpenEdX +//// +//// Created by  Stepanok Ivan on 15.12.2022. +//// import Alamofire import Combine @@ -24,13 +24,24 @@ public protocol ConnectivityProtocol: Sendable { @MainActor public class Connectivity: ConnectivityProtocol { - let networkManager = NetworkReachabilityManager() - - public var isInternetAvaliable: Bool { - // false - networkManager?.isReachable ?? false - } - + + private let networkManager = NetworkReachabilityManager() + private let verificationURL: URL + private let verificationTimeout: TimeInterval + + private static var lastVerificationDate: TimeInterval? + private static var lastVerificationResult: Bool = false + + private var _isInternetAvailable: Bool = true { + didSet { + internetReachableSubject.send(_isInternetAvailable ? .reachable : .notReachable) + } + } + + public var isInternetAvaliable: Bool { + return _isInternetAvailable + } + public var isMobileData: Bool { if let networkManager { return networkManager.isReachableOnCellular @@ -38,31 +49,68 @@ public class Connectivity: ConnectivityProtocol { return false } } - + public let internetReachableSubject = CurrentValueSubject(nil) - - public init() { + + public init( + urlToVerify: URL = Config().baseURL, + timeout: TimeInterval = 15 + ) { + self.verificationURL = urlToVerify + self.verificationTimeout = timeout checkInternet() } - - func checkInternet() { - if let networkManager { - networkManager.startListening { status in - DispatchQueue.main.async { - switch status { - case .unknown: - self.internetReachableSubject.send(InternetState.notReachable) - case .notReachable: - self.internetReachableSubject.send(InternetState.notReachable) - case .reachable: - self.internetReachableSubject.send(InternetState.reachable) + + deinit { + networkManager?.stopListening() + } + + private func checkInternet() { + guard let nm = networkManager else { + _isInternetAvailable = false + return + } + + nm.startListening { [weak self] status in + DispatchQueue.main.async { + guard let self = self else { return } + switch status { + case .reachable: + let nowTS = Date().timeIntervalSince1970 + if let lastTS = Connectivity.lastVerificationDate, + nowTS - lastTS < 30 { + self._isInternetAvailable = Connectivity.lastVerificationResult + } else { + Task { @MainActor in + let live = await self.verifyInternet() + Connectivity.lastVerificationDate = Date().timeIntervalSince1970 + Connectivity.lastVerificationResult = live + self._isInternetAvailable = live + } } + case .notReachable, .unknown: + Connectivity.lastVerificationDate = nil + Connectivity.lastVerificationResult = false + self._isInternetAvailable = false } } - } else { - DispatchQueue.main.async { - self.internetReachableSubject.send(InternetState.notReachable) + } + } + + private func verifyInternet() async -> Bool { + var request = URLRequest(url: verificationURL) + request.httpMethod = "HEAD" + request.timeoutInterval = verificationTimeout + + do { + let (_, response) = try await URLSession.shared.data(for: request) + if let http = response as? HTTPURLResponse, + (200..<400).contains(http.statusCode) { + return true } + } catch { + return false } + return false } } diff --git a/Course/Course/Presentation/Container/CourseContainerViewModel.swift b/Course/Course/Presentation/Container/CourseContainerViewModel.swift index 74e40265b..31950d661 100644 --- a/Course/Course/Presentation/Container/CourseContainerViewModel.swift +++ b/Course/Course/Presentation/Container/CourseContainerViewModel.swift @@ -225,13 +225,7 @@ public final class CourseContainerViewModel: BaseCourseViewModel { @MainActor func getCourseStructure(courseID: String) async throws -> CourseStructure? { if isInternetAvaliable { - if let courseStructure = try await getCourseBlocksWithTimeout(courseID: courseID, timeoutSeconds: timeOutIntervalSeconds) { - return courseStructure - } - connectivity.internetReachableSubject.send(.notReachable) - isShowProgress = false - isShowRefresh = false - return try await interactor.getLoadedCourseBlocks(courseID: courseID) + return try await self.interactor.getCourseBlocks(courseID: courseID) } else { return try await interactor.getLoadedCourseBlocks(courseID: courseID) } From 64c59a149887023fa90968c551b6001fc62ac838 Mon Sep 17 00:00:00 2001 From: DemianRaccoonGang Date: Thu, 31 Jul 2025 13:27:21 +0300 Subject: [PATCH 06/19] fix: remove unused functions and hardcoded variables --- Core/Core/Configuration/Connectivity.swift | 3 +- .../Container/CourseContainerViewModel.swift | 29 +------------------ 2 files changed, 3 insertions(+), 29 deletions(-) diff --git a/Core/Core/Configuration/Connectivity.swift b/Core/Core/Configuration/Connectivity.swift index 741791b19..0a1a58ff8 100644 --- a/Core/Core/Configuration/Connectivity.swift +++ b/Core/Core/Configuration/Connectivity.swift @@ -28,6 +28,7 @@ public class Connectivity: ConnectivityProtocol { private let networkManager = NetworkReachabilityManager() private let verificationURL: URL private let verificationTimeout: TimeInterval + private let secondsPast: TimeInterval = 30 private static var lastVerificationDate: TimeInterval? private static var lastVerificationResult: Bool = false @@ -78,7 +79,7 @@ public class Connectivity: ConnectivityProtocol { case .reachable: let nowTS = Date().timeIntervalSince1970 if let lastTS = Connectivity.lastVerificationDate, - nowTS - lastTS < 30 { + nowTS - lastTS < self.secondsPast { self._isInternetAvailable = Connectivity.lastVerificationResult } else { Task { @MainActor in diff --git a/Course/Course/Presentation/Container/CourseContainerViewModel.swift b/Course/Course/Presentation/Container/CourseContainerViewModel.swift index 31950d661..c6aa68584 100644 --- a/Course/Course/Presentation/Container/CourseContainerViewModel.swift +++ b/Course/Course/Presentation/Container/CourseContainerViewModel.swift @@ -110,8 +110,6 @@ public final class CourseContainerViewModel: BaseCourseViewModel { private let interactor: CourseInteractorProtocol private let authInteractor: AuthInteractorProtocol - private let timeOutIntervalSeconds: UInt64 = 15 - let analytics: CourseAnalytics let coreAnalytics: CoreAnalytics private(set) var storage: CourseStorage @@ -197,35 +195,10 @@ public final class CourseContainerViewModel: BaseCourseViewModel { ) } - private func getCourseBlocksWithTimeout( - courseID: String, - timeoutSeconds: UInt64 - ) async throws -> CourseStructure? { - return try await withThrowingTaskGroup(of: CourseStructure?.self) { group in - - group.addTask { - try await self.interactor.getCourseBlocks(courseID: courseID) - } - - group.addTask { - try await Task.sleep(nanoseconds: timeoutSeconds * 1_000_000_000) - return nil - } - - guard let firstResult = try await group.next() else { - return nil - } - - group.cancelAll() - - return firstResult - } - } - @MainActor func getCourseStructure(courseID: String) async throws -> CourseStructure? { if isInternetAvaliable { - return try await self.interactor.getCourseBlocks(courseID: courseID) + return try await interactor.getCourseBlocks(courseID: courseID) } else { return try await interactor.getLoadedCourseBlocks(courseID: courseID) } From b4fcf31493cceb200b02b92ca9ca3f44ae639db4 Mon Sep 17 00:00:00 2001 From: DemianRaccoonGang Date: Thu, 31 Jul 2025 13:27:21 +0300 Subject: [PATCH 07/19] fix: Removed unused functions and hardcoded variables --- Core/Core/Configuration/Connectivity.swift | 3 +- .../Container/CourseContainerViewModel.swift | 29 +------------------ 2 files changed, 3 insertions(+), 29 deletions(-) diff --git a/Core/Core/Configuration/Connectivity.swift b/Core/Core/Configuration/Connectivity.swift index 741791b19..0a1a58ff8 100644 --- a/Core/Core/Configuration/Connectivity.swift +++ b/Core/Core/Configuration/Connectivity.swift @@ -28,6 +28,7 @@ public class Connectivity: ConnectivityProtocol { private let networkManager = NetworkReachabilityManager() private let verificationURL: URL private let verificationTimeout: TimeInterval + private let secondsPast: TimeInterval = 30 private static var lastVerificationDate: TimeInterval? private static var lastVerificationResult: Bool = false @@ -78,7 +79,7 @@ public class Connectivity: ConnectivityProtocol { case .reachable: let nowTS = Date().timeIntervalSince1970 if let lastTS = Connectivity.lastVerificationDate, - nowTS - lastTS < 30 { + nowTS - lastTS < self.secondsPast { self._isInternetAvailable = Connectivity.lastVerificationResult } else { Task { @MainActor in diff --git a/Course/Course/Presentation/Container/CourseContainerViewModel.swift b/Course/Course/Presentation/Container/CourseContainerViewModel.swift index 31950d661..c6aa68584 100644 --- a/Course/Course/Presentation/Container/CourseContainerViewModel.swift +++ b/Course/Course/Presentation/Container/CourseContainerViewModel.swift @@ -110,8 +110,6 @@ public final class CourseContainerViewModel: BaseCourseViewModel { private let interactor: CourseInteractorProtocol private let authInteractor: AuthInteractorProtocol - private let timeOutIntervalSeconds: UInt64 = 15 - let analytics: CourseAnalytics let coreAnalytics: CoreAnalytics private(set) var storage: CourseStorage @@ -197,35 +195,10 @@ public final class CourseContainerViewModel: BaseCourseViewModel { ) } - private func getCourseBlocksWithTimeout( - courseID: String, - timeoutSeconds: UInt64 - ) async throws -> CourseStructure? { - return try await withThrowingTaskGroup(of: CourseStructure?.self) { group in - - group.addTask { - try await self.interactor.getCourseBlocks(courseID: courseID) - } - - group.addTask { - try await Task.sleep(nanoseconds: timeoutSeconds * 1_000_000_000) - return nil - } - - guard let firstResult = try await group.next() else { - return nil - } - - group.cancelAll() - - return firstResult - } - } - @MainActor func getCourseStructure(courseID: String) async throws -> CourseStructure? { if isInternetAvaliable { - return try await self.interactor.getCourseBlocks(courseID: courseID) + return try await interactor.getCourseBlocks(courseID: courseID) } else { return try await interactor.getLoadedCourseBlocks(courseID: courseID) } From 6121c2a63f27676232bf52b9f92559b6eb0dc841 Mon Sep 17 00:00:00 2001 From: DemianRaccoonGang Date: Thu, 31 Jul 2025 13:50:29 +0300 Subject: [PATCH 08/19] fix: issue 581 --- Core/Core/Configuration/Connectivity.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/Core/Core/Configuration/Connectivity.swift b/Core/Core/Configuration/Connectivity.swift index 0a1a58ff8..d3f879aed 100644 --- a/Core/Core/Configuration/Connectivity.swift +++ b/Core/Core/Configuration/Connectivity.swift @@ -29,7 +29,6 @@ public class Connectivity: ConnectivityProtocol { private let verificationURL: URL private let verificationTimeout: TimeInterval private let secondsPast: TimeInterval = 30 - private static var lastVerificationDate: TimeInterval? private static var lastVerificationResult: Bool = false From b26c3d7de4554b4dadeaa55e07f0426204859c9c Mon Sep 17 00:00:00 2001 From: DemianRaccoonGang Date: Thu, 31 Jul 2025 15:15:30 +0300 Subject: [PATCH 09/19] fix: progress on issue 581 --- Core/Core/Configuration/Connectivity.swift | 87 ++++++++++--------- Core/Core/View/Base/OfflineSnackBarView.swift | 3 +- Core/Core/View/Base/WebBrowser.swift | 3 +- .../Container/CourseContainerView.swift | 4 +- .../Presentation/Dates/CourseDatesView.swift | 2 +- .../Presentation/Handouts/HandoutsView.swift | 2 +- .../Presentation/Offline/OfflineView.swift | 2 +- .../Subviews/LargestDownloadsView.swift | 2 +- .../Outline/CourseOutlineView.swift | 2 +- .../CourseVertical/CourseVerticalView.swift | 4 +- .../Subviews/CustomDisclosureGroup.swift | 2 +- .../Unit/CourseNavigationView.swift | 2 +- .../Presentation/Unit/CourseUnitView.swift | 2 +- .../Presentation/Unit/Subviews/WebView.swift | 2 +- .../Video/EncodedVideoPlayer.swift | 2 +- .../Presentation/Video/SubtitlesView.swift | 2 +- .../Video/YouTubeVideoPlayer.swift | 2 +- .../Presentation/AllCoursesView.swift | 2 +- .../Presentation/ListDashboardView.swift | 2 +- .../PrimaryCourseDashboardView.swift | 2 +- .../NativeDiscovery/CourseDetailsView.swift | 2 +- .../NativeDiscovery/DiscoveryView.swift | 2 +- .../NativeDiscovery/SearchView.swift | 2 +- .../WebPrograms/ProgramWebviewView.swift | 2 +- .../Presentation/AppDownloadsViewModel.swift | 2 +- OpenEdX/DI/AppAssembly.swift | 14 ++- .../DatesAndCalendar/CoursesToSyncView.swift | 2 +- .../DatesAndCalendarView.swift | 2 +- .../Elements/NewCalendarView.swift | 2 +- .../SyncCalendarOptionsView.swift | 2 +- .../DeleteAccount/DeleteAccountView.swift | 2 +- .../Presentation/Profile/ProfileView.swift | 5 +- .../Settings/ManageAccountView.swift | 6 +- .../Presentation/Settings/SettingsView.swift | 2 +- .../Settings/VideoQualityView.swift | 2 +- .../Settings/VideoSettingsView.swift | 2 +- .../Settings/SettingsViewModelTests.swift | 12 +-- 37 files changed, 107 insertions(+), 87 deletions(-) diff --git a/Core/Core/Configuration/Connectivity.swift b/Core/Core/Configuration/Connectivity.swift index d3f879aed..2d1df1d34 100644 --- a/Core/Core/Configuration/Connectivity.swift +++ b/Core/Core/Configuration/Connectivity.swift @@ -1,9 +1,9 @@ -//// -//// Connectivity.swift -//// OpenEdX -//// -//// Created by  Stepanok Ivan on 15.12.2022. -//// +// +// Connectivity.swift +// OpenEdX +// +// Created by Stepanok Ivan on 15.12.2022. +// import Alamofire import Combine @@ -14,7 +14,7 @@ public enum InternetState: Sendable { case notReachable } -//sourcery: AutoMockable +// sourcery: AutoMockable @MainActor public protocol ConnectivityProtocol: Sendable { var isInternetAvaliable: Bool { get } @@ -29,69 +29,73 @@ public class Connectivity: ConnectivityProtocol { private let verificationURL: URL private let verificationTimeout: TimeInterval private let secondsPast: TimeInterval = 30 + private static var lastVerificationDate: TimeInterval? private static var lastVerificationResult: Bool = false private var _isInternetAvailable: Bool = true { - didSet { - internetReachableSubject.send(_isInternetAvailable ? .reachable : .notReachable) - } - } + didSet { + internetReachableSubject.send(_isInternetAvailable ? .reachable : .notReachable) + } + } - public var isInternetAvaliable: Bool { - return _isInternetAvailable - } + public var isInternetAvaliable: Bool { + _isInternetAvailable + } public var isMobileData: Bool { - if let networkManager { - return networkManager.isReachableOnCellular - } else { - return false - } + networkManager?.isReachableOnCellular == true } public let internetReachableSubject = CurrentValueSubject(nil) public init( - urlToVerify: URL = Config().baseURL, + config: ConfigProtocol, timeout: TimeInterval = 15 ) { - self.verificationURL = urlToVerify + print("+++ go") + self.verificationURL = config.baseURL self.verificationTimeout = timeout checkInternet() } deinit { + print("+++ deinit") networkManager?.stopListening() } - private func checkInternet() { - guard let nm = networkManager else { - _isInternetAvailable = false - return - } + @MainActor + private func updateAvailability( + _ available: Bool, + lastChecked: TimeInterval = Date().timeIntervalSince1970 + ) { + self._isInternetAvailable = available + Connectivity.lastVerificationDate = lastChecked + Connectivity.lastVerificationResult = available + } - nm.startListening { [weak self] status in - DispatchQueue.main.async { - guard let self = self else { return } + func checkInternet() { + networkManager?.startListening(onQueue: .global()) { [weak self] status in + guard let self = self else { return } + let now = Date().timeIntervalSince1970 + + Task { @MainActor in switch status { case .reachable: - let nowTS = Date().timeIntervalSince1970 - if let lastTS = Connectivity.lastVerificationDate, - nowTS - lastTS < self.secondsPast { - self._isInternetAvailable = Connectivity.lastVerificationResult + if let last = Connectivity.lastVerificationDate, + now - last < self.secondsPast { + print("+++ last") + self.updateAvailability(Connectivity.lastVerificationResult) } else { - Task { @MainActor in + Task.detached { + print("+++ verif") let live = await self.verifyInternet() - Connectivity.lastVerificationDate = Date().timeIntervalSince1970 - Connectivity.lastVerificationResult = live - self._isInternetAvailable = live + await self.updateAvailability(live, lastChecked: Date().timeIntervalSince1970) } } + case .notReachable, .unknown: - Connectivity.lastVerificationDate = nil - Connectivity.lastVerificationResult = false - self._isInternetAvailable = false + self.updateAvailability(false, lastChecked: 0) } } } @@ -106,11 +110,14 @@ public class Connectivity: ConnectivityProtocol { let (_, response) = try await URLSession.shared.data(for: request) if let http = response as? HTTPURLResponse, (200..<400).contains(http.statusCode) { + print("++++ got response") return true } } catch { + print("++++ no response") return false } + print("++++ no response") return false } } diff --git a/Core/Core/View/Base/OfflineSnackBarView.swift b/Core/Core/View/Base/OfflineSnackBarView.swift index c26a577ee..ad6c70130 100644 --- a/Core/Core/View/Base/OfflineSnackBarView.swift +++ b/Core/Core/View/Base/OfflineSnackBarView.swift @@ -79,6 +79,7 @@ public struct OfflineSnackBarView: View { struct OfflineSnackBarView_Previews: PreviewProvider { static var previews: some View { - OfflineSnackBarView(connectivity: Connectivity(), reloadAction: {}) + let configMock = ConfigMock() + OfflineSnackBarView(connectivity: Connectivity(config: configMock), reloadAction: {}) } } diff --git a/Core/Core/View/Base/WebBrowser.swift b/Core/Core/View/Base/WebBrowser.swift index cd61dcdb6..6ae177cb4 100644 --- a/Core/Core/View/Base/WebBrowser.swift +++ b/Core/Core/View/Base/WebBrowser.swift @@ -81,6 +81,7 @@ public struct WebBrowser: View { struct WebBrowser_Previews: PreviewProvider { static var previews: some View { - WebBrowser(url: "", pageTitle: "", connectivity: Connectivity()) + let configMock = ConfigMock() + WebBrowser(url: "", pageTitle: "", connectivity: Connectivity(config: configMock)) } } diff --git a/Course/Course/Presentation/Container/CourseContainerView.swift b/Course/Course/Presentation/Container/CourseContainerView.swift index cc2bdc9e2..8db22ae10 100644 --- a/Course/Course/Presentation/Container/CourseContainerView.swift +++ b/Course/Course/Presentation/Container/CourseContainerView.swift @@ -366,7 +366,7 @@ struct CourseScreensView_Previews: PreviewProvider { router: CourseRouterMock(), analytics: CourseAnalyticsMock(), config: ConfigMock(), - connectivity: Connectivity(), + connectivity: Connectivity(config: ConfigMock()), manager: DownloadManagerMock(), storage: CourseStorageMock(), isActive: true, @@ -382,7 +382,7 @@ struct CourseScreensView_Previews: PreviewProvider { interactor: CourseInteractor.mock, router: CourseRouterMock(), cssInjector: CSSInjectorMock(), - connectivity: Connectivity(), + connectivity: Connectivity(config: ConfigMock()), config: ConfigMock(), courseID: "1", courseName: "a", diff --git a/Course/Course/Presentation/Dates/CourseDatesView.swift b/Course/Course/Presentation/Dates/CourseDatesView.swift index 192b7c2a9..5de224b44 100644 --- a/Course/Course/Presentation/Dates/CourseDatesView.swift +++ b/Course/Course/Presentation/Dates/CourseDatesView.swift @@ -196,7 +196,7 @@ struct CourseDatesView_Previews: PreviewProvider { interactor: CourseInteractor(repository: CourseRepositoryMock()), router: CourseRouterMock(), cssInjector: CSSInjectorMock(), - connectivity: Connectivity(), + connectivity: Connectivity(config: ConfigMock()), config: ConfigMock(), courseID: "", courseName: "", diff --git a/Course/Course/Presentation/Handouts/HandoutsView.swift b/Course/Course/Presentation/Handouts/HandoutsView.swift index d04be11af..82d74a408 100644 --- a/Course/Course/Presentation/Handouts/HandoutsView.swift +++ b/Course/Course/Presentation/Handouts/HandoutsView.swift @@ -123,7 +123,7 @@ struct HandoutsView_Previews: PreviewProvider { let viewModel = HandoutsViewModel(interactor: CourseInteractor.mock, router: CourseRouterMock(), cssInjector: CSSInjectorMock(), - connectivity: Connectivity(), + connectivity: Connectivity(config: ConfigMock()), courseID: "", analytics: CourseAnalyticsMock()) HandoutsView( diff --git a/Course/Course/Presentation/Offline/OfflineView.swift b/Course/Course/Presentation/Offline/OfflineView.swift index 9b5cee5ed..168e9e574 100644 --- a/Course/Course/Presentation/Offline/OfflineView.swift +++ b/Course/Course/Presentation/Offline/OfflineView.swift @@ -249,7 +249,7 @@ struct OfflineView: View { router: CourseRouterMock(), analytics: CourseAnalyticsMock(), config: ConfigMock(), - connectivity: Connectivity(), + connectivity: Connectivity(config: ConfigMock()), manager: DownloadManagerMock(), storage: CourseStorageMock(), isActive: true, diff --git a/Course/Course/Presentation/Offline/Subviews/LargestDownloadsView.swift b/Course/Course/Presentation/Offline/Subviews/LargestDownloadsView.swift index 7aa0930d7..3c13cd278 100644 --- a/Course/Course/Presentation/Offline/Subviews/LargestDownloadsView.swift +++ b/Course/Course/Presentation/Offline/Subviews/LargestDownloadsView.swift @@ -93,7 +93,7 @@ struct LargestDownloadsView_Previews: PreviewProvider { router: CourseRouterMock(), analytics: CourseAnalyticsMock(), config: ConfigMock(), - connectivity: Connectivity(), + connectivity: Connectivity(config: ConfigMock()), manager: DownloadManagerMock(), storage: CourseStorageMock(), isActive: true, diff --git a/Course/Course/Presentation/Outline/CourseOutlineView.swift b/Course/Course/Presentation/Outline/CourseOutlineView.swift index 8872771a2..4049276ff 100644 --- a/Course/Course/Presentation/Outline/CourseOutlineView.swift +++ b/Course/Course/Presentation/Outline/CourseOutlineView.swift @@ -336,7 +336,7 @@ struct CourseOutlineView_Previews: PreviewProvider { router: CourseRouterMock(), analytics: CourseAnalyticsMock(), config: ConfigMock(), - connectivity: Connectivity(), + connectivity: Connectivity(config: ConfigMock()), manager: DownloadManagerMock(), storage: CourseStorageMock(), isActive: true, diff --git a/Course/Course/Presentation/Outline/CourseVertical/CourseVerticalView.swift b/Course/Course/Presentation/Outline/CourseVertical/CourseVerticalView.swift index ed7b60af4..364e9c00a 100644 --- a/Course/Course/Presentation/Outline/CourseVertical/CourseVerticalView.swift +++ b/Course/Course/Presentation/Outline/CourseVertical/CourseVerticalView.swift @@ -181,9 +181,9 @@ struct CourseVerticalView_Previews: PreviewProvider { sequentialIndex: 0, router: CourseRouterMock(), analytics: CourseAnalyticsMock(), - connectivity: Connectivity() + connectivity: Connectivity(config: ConfigMock()), ) - + return Group { CourseVerticalView( title: "Course title", diff --git a/Course/Course/Presentation/Subviews/CustomDisclosureGroup.swift b/Course/Course/Presentation/Subviews/CustomDisclosureGroup.swift index ff252a654..edba4603e 100644 --- a/Course/Course/Presentation/Subviews/CustomDisclosureGroup.swift +++ b/Course/Course/Presentation/Subviews/CustomDisclosureGroup.swift @@ -399,7 +399,7 @@ struct CustomDisclosureGroup_Previews: PreviewProvider { router: CourseRouterMock(), analytics: CourseAnalyticsMock(), config: ConfigMock(), - connectivity: Connectivity(), + connectivity: Connectivity(config: ConfigMock()), manager: DownloadManagerMock(), storage: CourseStorageMock(), isActive: true, diff --git a/Course/Course/Presentation/Unit/CourseNavigationView.swift b/Course/Course/Presentation/Unit/CourseNavigationView.swift index 4257f18e9..4047f81c7 100644 --- a/Course/Course/Presentation/Unit/CourseNavigationView.swift +++ b/Course/Course/Presentation/Unit/CourseNavigationView.swift @@ -160,7 +160,7 @@ struct CourseNavigationView_Previews: PreviewProvider { config: ConfigMock(), router: CourseRouterMock(), analytics: CourseAnalyticsMock(), - connectivity: Connectivity(), + connectivity: Connectivity(config: ConfigMock()), storage: CourseStorageMock(), manager: DownloadManagerMock() ) diff --git a/Course/Course/Presentation/Unit/CourseUnitView.swift b/Course/Course/Presentation/Unit/CourseUnitView.swift index 2588d7369..2725afd2e 100644 --- a/Course/Course/Presentation/Unit/CourseUnitView.swift +++ b/Course/Course/Presentation/Unit/CourseUnitView.swift @@ -723,7 +723,7 @@ struct CourseUnitView_Previews: PreviewProvider { config: ConfigMock(), router: CourseRouterMock(), analytics: CourseAnalyticsMock(), - connectivity: Connectivity(), + connectivity: Connectivity(config: ConfigMock()), storage: CourseStorageMock(), manager: DownloadManagerMock() )) diff --git a/Course/Course/Presentation/Unit/Subviews/WebView.swift b/Course/Course/Presentation/Unit/Subviews/WebView.swift index b8823bda6..935884a28 100644 --- a/Course/Course/Presentation/Unit/Subviews/WebView.swift +++ b/Course/Course/Presentation/Unit/Subviews/WebView.swift @@ -23,7 +23,7 @@ struct WebView: View { url: url, dataUrl: localUrl, viewModel: Container.shared.resolve(WebUnitViewModel.self)!, - connectivity: Connectivity(), + connectivity: Connectivity(config: ConfigMock()), injections: injections, blockID: blockID ) diff --git a/Course/Course/Presentation/Video/EncodedVideoPlayer.swift b/Course/Course/Presentation/Video/EncodedVideoPlayer.swift index a2d69340a..52e2428f1 100644 --- a/Course/Course/Presentation/Video/EncodedVideoPlayer.swift +++ b/Course/Course/Presentation/Video/EncodedVideoPlayer.swift @@ -145,7 +145,7 @@ struct EncodedVideoPlayer_Previews: PreviewProvider { viewModel: EncodedVideoPlayerViewModel( languages: [], playerStateSubject: CurrentValueSubject(nil), - connectivity: Connectivity(), + connectivity: Connectivity(config: ConfigMock()), playerHolder: PlayerViewControllerHolder.mock, appStorage: CoreStorageMock(), analytics: CourseAnalyticsMock() diff --git a/Course/Course/Presentation/Video/SubtitlesView.swift b/Course/Course/Presentation/Video/SubtitlesView.swift index 5f7efc8a6..1680f2191 100644 --- a/Course/Course/Presentation/Video/SubtitlesView.swift +++ b/Course/Course/Presentation/Video/SubtitlesView.swift @@ -124,7 +124,7 @@ struct SubtittlesView_Previews: PreviewProvider { viewModel: VideoPlayerViewModel( languages: [], playerStateSubject: CurrentValueSubject(nil), - connectivity: Connectivity(), + connectivity: Connectivity(config: ConfigMock()), playerHolder: PlayerViewControllerHolder.mock, appStorage: CoreStorageMock(), analytics: CourseAnalyticsMock() diff --git a/Course/Course/Presentation/Video/YouTubeVideoPlayer.swift b/Course/Course/Presentation/Video/YouTubeVideoPlayer.swift index 5914a1807..8f5e2d14f 100644 --- a/Course/Course/Presentation/Video/YouTubeVideoPlayer.swift +++ b/Course/Course/Presentation/Video/YouTubeVideoPlayer.swift @@ -88,7 +88,7 @@ struct YouTubeVideoPlayer_Previews: PreviewProvider { viewModel: YouTubeVideoPlayerViewModel( languages: [], playerStateSubject: CurrentValueSubject(nil), - connectivity: Connectivity(), + connectivity: Connectivity(config: ConfigMock()), playerHolder: YoutubePlayerViewControllerHolder.mock, appStorage: CoreStorageMock(), analytics: CourseAnalyticsMock() diff --git a/Dashboard/Dashboard/Presentation/AllCoursesView.swift b/Dashboard/Dashboard/Presentation/AllCoursesView.swift index 4ab5fd905..8b8e77acc 100644 --- a/Dashboard/Dashboard/Presentation/AllCoursesView.swift +++ b/Dashboard/Dashboard/Presentation/AllCoursesView.swift @@ -205,7 +205,7 @@ struct AllCoursesView_Previews: PreviewProvider { static var previews: some View { let vm = AllCoursesViewModel( interactor: DashboardInteractor.mock, - connectivity: Connectivity(), + connectivity: Connectivity(config: ConfigMock()), analytics: DashboardAnalyticsMock(), storage: CoreStorageMock() ) diff --git a/Dashboard/Dashboard/Presentation/ListDashboardView.swift b/Dashboard/Dashboard/Presentation/ListDashboardView.swift index 8f668dcc5..ac397f6d6 100644 --- a/Dashboard/Dashboard/Presentation/ListDashboardView.swift +++ b/Dashboard/Dashboard/Presentation/ListDashboardView.swift @@ -163,7 +163,7 @@ struct ListDashboardView_Previews: PreviewProvider { static var previews: some View { let vm = ListDashboardViewModel( interactor: DashboardInteractor.mock, - connectivity: Connectivity(), + connectivity: Connectivity(config: ConfigMock()), analytics: DashboardAnalyticsMock(), storage: CoreStorageMock() ) diff --git a/Dashboard/Dashboard/Presentation/PrimaryCourseDashboardView.swift b/Dashboard/Dashboard/Presentation/PrimaryCourseDashboardView.swift index 905dcaaae..e0f6eff05 100644 --- a/Dashboard/Dashboard/Presentation/PrimaryCourseDashboardView.swift +++ b/Dashboard/Dashboard/Presentation/PrimaryCourseDashboardView.swift @@ -334,7 +334,7 @@ struct PrimaryCourseDashboardView_Previews: PreviewProvider { static var previews: some View { let vm = PrimaryCourseDashboardViewModel( interactor: DashboardInteractor.mock, - connectivity: Connectivity(), + connectivity: Connectivity(config: ConfigMock()), analytics: DashboardAnalyticsMock(), config: ConfigMock(), storage: CoreStorageMock(), diff --git a/Discovery/Discovery/Presentation/NativeDiscovery/CourseDetailsView.swift b/Discovery/Discovery/Presentation/NativeDiscovery/CourseDetailsView.swift index 2cbe294c1..fe1c32657 100644 --- a/Discovery/Discovery/Presentation/NativeDiscovery/CourseDetailsView.swift +++ b/Discovery/Discovery/Presentation/NativeDiscovery/CourseDetailsView.swift @@ -439,7 +439,7 @@ struct CourseDetailsView_Previews: PreviewProvider { analytics: DiscoveryAnalyticsMock(), config: ConfigMock(), cssInjector: CSSInjectorMock(), - connectivity: Connectivity(), + connectivity: Connectivity(config: ConfigMock()), storage: CoreStorageMock() ) diff --git a/Discovery/Discovery/Presentation/NativeDiscovery/DiscoveryView.swift b/Discovery/Discovery/Presentation/NativeDiscovery/DiscoveryView.swift index ffeb3451b..cafe26adb 100644 --- a/Discovery/Discovery/Presentation/NativeDiscovery/DiscoveryView.swift +++ b/Discovery/Discovery/Presentation/NativeDiscovery/DiscoveryView.swift @@ -211,7 +211,7 @@ struct DiscoveryView_Previews: PreviewProvider { let vm = DiscoveryViewModel(router: DiscoveryRouterMock(), config: ConfigMock(), interactor: DiscoveryInteractor.mock, - connectivity: Connectivity(), + connectivity: Connectivity(config: ConfigMock()), analytics: DiscoveryAnalyticsMock(), storage: CoreStorageMock()) let router = DiscoveryRouterMock() diff --git a/Discovery/Discovery/Presentation/NativeDiscovery/SearchView.swift b/Discovery/Discovery/Presentation/NativeDiscovery/SearchView.swift index c059dbcba..7c565bfad 100644 --- a/Discovery/Discovery/Presentation/NativeDiscovery/SearchView.swift +++ b/Discovery/Discovery/Presentation/NativeDiscovery/SearchView.swift @@ -222,7 +222,7 @@ struct SearchView_Previews: PreviewProvider { let router = DiscoveryRouterMock() let vm = SearchViewModel( interactor: DiscoveryInteractor.mock, - connectivity: Connectivity(), + connectivity: Connectivity(config: ConfigMock()), router: router, analytics: DiscoveryAnalyticsMock(), storage: CoreStorageMock(), diff --git a/Discovery/Discovery/Presentation/WebPrograms/ProgramWebviewView.swift b/Discovery/Discovery/Presentation/WebPrograms/ProgramWebviewView.swift index a206303d8..dbfc60403 100644 --- a/Discovery/Discovery/Presentation/WebPrograms/ProgramWebviewView.swift +++ b/Discovery/Discovery/Presentation/WebPrograms/ProgramWebviewView.swift @@ -139,7 +139,7 @@ struct ProgramWebviewView_Previews: PreviewProvider { router: DiscoveryRouterMock(), config: ConfigMock(), interactor: DiscoveryInteractor.mock, - connectivity: Connectivity(), + connectivity: Connectivity(config: ConfigMock()), analytics: DiscoveryAnalyticsMock(), authInteractor: AuthInteractor.mock ), diff --git a/Downloads/Downloads/Presentation/AppDownloadsViewModel.swift b/Downloads/Downloads/Presentation/AppDownloadsViewModel.swift index 104efc6f7..79696e030 100644 --- a/Downloads/Downloads/Presentation/AppDownloadsViewModel.swift +++ b/Downloads/Downloads/Presentation/AppDownloadsViewModel.swift @@ -779,7 +779,7 @@ public extension AppDownloadsViewModel { interactor: DownloadsInteractor.mock, courseManager: CourseStructureManagerMock(), downloadManager: DownloadManagerMock(), - connectivity: Connectivity(), + connectivity: Connectivity(config: ConfigMock()), downloadsHelper: DownloadsHelperMock(), router: DownloadsRouterMock(), storage: DownloadsStorageMock(), diff --git a/OpenEdX/DI/AppAssembly.swift b/OpenEdX/DI/AppAssembly.swift index f90b6a500..840cce6d5 100644 --- a/OpenEdX/DI/AppAssembly.swift +++ b/OpenEdX/DI/AppAssembly.swift @@ -87,8 +87,8 @@ class AppAssembly: Assembly { r.resolve(AnalyticsManager.self)! }.inObjectScope(.container) - container.register(ConnectivityProtocol.self) { @MainActor _ in - Connectivity() + container.register(ConnectivityProtocol.self) { @MainActor r in + Connectivity(config: r.resolve(ConfigProtocol.self)!) } container.register(DatabaseManager.self) { _ in @@ -193,7 +193,7 @@ class AppAssembly: Assembly { keychain: r.resolve(KeychainSwift.self)! ) } - + container.register(Validator.self) { _ in Validator() }.inObjectScope(.container) @@ -235,6 +235,14 @@ class AppAssembly: Assembly { courseDropDownNavigationEnabled: config.uiComponents.courseDropDownNavigationEnabled ) }.inObjectScope(.container) + + container.register(ConnectivityProtocol.self) { @MainActor r in + Connectivity( + config: r.resolve(ConfigProtocol.self)!, + timeout: 15 + ) + } + .inObjectScope(.container) } } // swiftlint:enable function_body_length diff --git a/Profile/Profile/Presentation/DatesAndCalendar/CoursesToSyncView.swift b/Profile/Profile/Presentation/DatesAndCalendar/CoursesToSyncView.swift index ede274a85..e0791ac31 100644 --- a/Profile/Profile/Presentation/DatesAndCalendar/CoursesToSyncView.swift +++ b/Profile/Profile/Presentation/DatesAndCalendar/CoursesToSyncView.swift @@ -158,7 +158,7 @@ struct CoursesToSyncView_Previews: PreviewProvider { profileStorage: ProfileStorageMock(), persistence: ProfilePersistenceMock(), calendarManager: CalendarManagerMock(), - connectivity: Connectivity() + connectivity: Connectivity(config: ConfigMock()), ) return CoursesToSyncView(viewModel: vm) .previewDisplayName("Courses to Sync") diff --git a/Profile/Profile/Presentation/DatesAndCalendar/DatesAndCalendarView.swift b/Profile/Profile/Presentation/DatesAndCalendar/DatesAndCalendarView.swift index 52ef209f9..3993b8580 100644 --- a/Profile/Profile/Presentation/DatesAndCalendar/DatesAndCalendarView.swift +++ b/Profile/Profile/Presentation/DatesAndCalendar/DatesAndCalendarView.swift @@ -188,7 +188,7 @@ struct DatesAndCalendarView_Previews: PreviewProvider { profileStorage: ProfileStorageMock(), persistence: ProfilePersistenceMock(), calendarManager: CalendarManagerMock(), - connectivity: Connectivity() + connectivity: Connectivity(config: ConfigMock()), ) DatesAndCalendarView(viewModel: vm) .loadFonts() diff --git a/Profile/Profile/Presentation/DatesAndCalendar/Elements/NewCalendarView.swift b/Profile/Profile/Presentation/DatesAndCalendar/Elements/NewCalendarView.swift index 9d8b9dd70..ed4e27b8a 100644 --- a/Profile/Profile/Presentation/DatesAndCalendar/Elements/NewCalendarView.swift +++ b/Profile/Profile/Presentation/DatesAndCalendar/Elements/NewCalendarView.swift @@ -167,7 +167,7 @@ struct NewCalendarView: View { profileStorage: ProfileStorageMock(), persistence: ProfilePersistenceMock(), calendarManager: CalendarManagerMock(), - connectivity: Connectivity() + connectivity: Connectivity(config: ConfigMock()) ), beginSyncingTapped: { }, diff --git a/Profile/Profile/Presentation/DatesAndCalendar/SyncCalendarOptionsView.swift b/Profile/Profile/Presentation/DatesAndCalendar/SyncCalendarOptionsView.swift index b5aa1e691..e96a0cc1b 100644 --- a/Profile/Profile/Presentation/DatesAndCalendar/SyncCalendarOptionsView.swift +++ b/Profile/Profile/Presentation/DatesAndCalendar/SyncCalendarOptionsView.swift @@ -271,7 +271,7 @@ struct SyncCalendarOptionsView_Previews: PreviewProvider { profileStorage: ProfileStorageMock(), persistence: ProfilePersistenceMock(), calendarManager: CalendarManagerMock(), - connectivity: Connectivity() + connectivity: Connectivity(config: ConfigMock()), ) SyncCalendarOptionsView(viewModel: vm) .loadFonts() diff --git a/Profile/Profile/Presentation/DeleteAccount/DeleteAccountView.swift b/Profile/Profile/Presentation/DeleteAccount/DeleteAccountView.swift index 585c06279..9b2ad9525 100644 --- a/Profile/Profile/Presentation/DeleteAccount/DeleteAccountView.swift +++ b/Profile/Profile/Presentation/DeleteAccount/DeleteAccountView.swift @@ -188,7 +188,7 @@ struct DeleteAccountView_Previews: PreviewProvider { let vm = DeleteAccountViewModel( interactor: ProfileInteractor.mock, router: router, - connectivity: Connectivity(), + connectivity: Connectivity(config: ConfigMock()), analytics: ProfileAnalyticsMock() ) diff --git a/Profile/Profile/Presentation/Profile/ProfileView.swift b/Profile/Profile/Presentation/Profile/ProfileView.swift index 1691afc3d..b9d49b4b8 100644 --- a/Profile/Profile/Presentation/Profile/ProfileView.swift +++ b/Profile/Profile/Presentation/Profile/ProfileView.swift @@ -189,12 +189,13 @@ public struct ProfileView: View { struct ProfileView_Previews: PreviewProvider { static var previews: some View { let router = ProfileRouterMock() + let config = ConfigMock() let vm = ProfileViewModel( interactor: ProfileInteractor.mock, router: router, analytics: ProfileAnalyticsMock(), - config: ConfigMock(), - connectivity: Connectivity() + config: config, + connectivity: Connectivity(config: config), ) ProfileView(viewModel: vm) diff --git a/Profile/Profile/Presentation/Settings/ManageAccountView.swift b/Profile/Profile/Presentation/Settings/ManageAccountView.swift index 8b791d42b..154eddbcf 100644 --- a/Profile/Profile/Presentation/Settings/ManageAccountView.swift +++ b/Profile/Profile/Presentation/Settings/ManageAccountView.swift @@ -201,11 +201,13 @@ public struct ManageAccountView: View { struct ManageAccountView_Previews: PreviewProvider { static var previews: some View { let router = ProfileRouterMock() + let configMock = ConfigMock() + let vm = ManageAccountViewModel( router: router, analytics: ProfileAnalyticsMock(), - config: ConfigMock(), - connectivity: Connectivity(), + config: configMock, + connectivity: Connectivity(config: configMock), interactor: ProfileInteractor.mock ) diff --git a/Profile/Profile/Presentation/Settings/SettingsView.swift b/Profile/Profile/Presentation/Settings/SettingsView.swift index e58533f4d..b1410b656 100644 --- a/Profile/Profile/Presentation/Settings/SettingsView.swift +++ b/Profile/Profile/Presentation/Settings/SettingsView.swift @@ -254,7 +254,7 @@ public struct SettingsView: View { coreAnalytics: CoreAnalyticsMock(), config: ConfigMock(), corePersistence: CorePersistenceMock(), - connectivity: Connectivity(), + connectivity: Connectivity(config: ConfigMock()), coreStorage: CoreStorageMock() ) diff --git a/Profile/Profile/Presentation/Settings/VideoQualityView.swift b/Profile/Profile/Presentation/Settings/VideoQualityView.swift index d06c1ea9f..8f662976d 100644 --- a/Profile/Profile/Presentation/Settings/VideoQualityView.swift +++ b/Profile/Profile/Presentation/Settings/VideoQualityView.swift @@ -134,7 +134,7 @@ public struct VideoQualityView: View { coreAnalytics: CoreAnalyticsMock(), config: ConfigMock(), corePersistence: CorePersistenceMock(), - connectivity: Connectivity(), + connectivity: Connectivity(config: ConfigMock()), coreStorage: CoreStorageMock() ) diff --git a/Profile/Profile/Presentation/Settings/VideoSettingsView.swift b/Profile/Profile/Presentation/Settings/VideoSettingsView.swift index 3e4fd4b4c..a8c29bcc0 100644 --- a/Profile/Profile/Presentation/Settings/VideoSettingsView.swift +++ b/Profile/Profile/Presentation/Settings/VideoSettingsView.swift @@ -141,7 +141,7 @@ public struct VideoSettingsView: View { coreAnalytics: CoreAnalyticsMock(), config: ConfigMock(), corePersistence: CorePersistenceMock(), - connectivity: Connectivity(), + connectivity: Connectivity(config: ConfigMock()), coreStorage: CoreStorageMock() ) diff --git a/Profile/ProfileTests/Presentation/Settings/SettingsViewModelTests.swift b/Profile/ProfileTests/Presentation/Settings/SettingsViewModelTests.swift index d9e80e66f..3455d8814 100644 --- a/Profile/ProfileTests/Presentation/Settings/SettingsViewModelTests.swift +++ b/Profile/ProfileTests/Presentation/Settings/SettingsViewModelTests.swift @@ -44,7 +44,7 @@ final class SettingsViewModelTests: XCTestCase { coreAnalytics: coreAnalytics, config: ConfigMock(), corePersistence: CorePersistenceMock(), - connectivity: Connectivity(), + connectivity: Connectivity(config: ConfigMock()), coreStorage: storage ) @@ -82,7 +82,7 @@ final class SettingsViewModelTests: XCTestCase { coreAnalytics: coreAnalytics, config: ConfigMock(), corePersistence: CorePersistenceMock(), - connectivity: Connectivity(), + connectivity: Connectivity(config: ConfigMock()), coreStorage: storage ) @@ -119,7 +119,7 @@ final class SettingsViewModelTests: XCTestCase { coreAnalytics: coreAnalytics, config: ConfigMock(), corePersistence: CorePersistenceMock(), - connectivity: Connectivity(), + connectivity: Connectivity(config: ConfigMock()), coreStorage: storage ) @@ -156,7 +156,7 @@ final class SettingsViewModelTests: XCTestCase { coreAnalytics: coreAnalytics, config: ConfigMock(), corePersistence: CorePersistenceMock(), - connectivity: Connectivity(), + connectivity: Connectivity(config: ConfigMock()), coreStorage: storage ) @@ -193,7 +193,7 @@ final class SettingsViewModelTests: XCTestCase { coreAnalytics: coreAnalytics, config: ConfigMock(), corePersistence: CorePersistenceMock(), - connectivity: Connectivity(), + connectivity: Connectivity(config: ConfigMock()), coreStorage: storage ) @@ -230,7 +230,7 @@ final class SettingsViewModelTests: XCTestCase { coreAnalytics: coreAnalytics, config: ConfigMock(), corePersistence: CorePersistenceMock(), - connectivity: Connectivity(), + connectivity: Connectivity(config: ConfigMock()), coreStorage: storage ) From f21a460b481ccd60200686f66e8007ef67bcbc80 Mon Sep 17 00:00:00 2001 From: DemianRaccoonGang Date: Thu, 31 Jul 2025 15:47:07 +0300 Subject: [PATCH 10/19] fix: apply updates --- Core/Core/Configuration/Connectivity.swift | 7 ------- OpenEdX/DI/AppAssembly.swift | 12 ++++++------ 2 files changed, 6 insertions(+), 13 deletions(-) diff --git a/Core/Core/Configuration/Connectivity.swift b/Core/Core/Configuration/Connectivity.swift index 2d1df1d34..25ecf63e6 100644 --- a/Core/Core/Configuration/Connectivity.swift +++ b/Core/Core/Configuration/Connectivity.swift @@ -53,14 +53,12 @@ public class Connectivity: ConnectivityProtocol { config: ConfigProtocol, timeout: TimeInterval = 15 ) { - print("+++ go") self.verificationURL = config.baseURL self.verificationTimeout = timeout checkInternet() } deinit { - print("+++ deinit") networkManager?.stopListening() } @@ -84,11 +82,9 @@ public class Connectivity: ConnectivityProtocol { case .reachable: if let last = Connectivity.lastVerificationDate, now - last < self.secondsPast { - print("+++ last") self.updateAvailability(Connectivity.lastVerificationResult) } else { Task.detached { - print("+++ verif") let live = await self.verifyInternet() await self.updateAvailability(live, lastChecked: Date().timeIntervalSince1970) } @@ -110,14 +106,11 @@ public class Connectivity: ConnectivityProtocol { let (_, response) = try await URLSession.shared.data(for: request) if let http = response as? HTTPURLResponse, (200..<400).contains(http.statusCode) { - print("++++ got response") return true } } catch { - print("++++ no response") return false } - print("++++ no response") return false } } diff --git a/OpenEdX/DI/AppAssembly.swift b/OpenEdX/DI/AppAssembly.swift index 840cce6d5..9dc64cf92 100644 --- a/OpenEdX/DI/AppAssembly.swift +++ b/OpenEdX/DI/AppAssembly.swift @@ -86,11 +86,12 @@ class AppAssembly: Assembly { container.register(DownloadsAnalytics.self) { r in r.resolve(AnalyticsManager.self)! }.inObjectScope(.container) - - container.register(ConnectivityProtocol.self) { @MainActor r in - Connectivity(config: r.resolve(ConfigProtocol.self)!) - } - + +// container.register(ConnectivityProtocol.self) { @MainActor r in +// Connectivity(config: r.resolve(ConfigProtocol.self)!) +// } +// + container.register(DatabaseManager.self) { _ in DatabaseManager(databaseName: "Database") }.inObjectScope(.container) @@ -242,7 +243,6 @@ class AppAssembly: Assembly { timeout: 15 ) } - .inObjectScope(.container) } } // swiftlint:enable function_body_length From 327bb30268435e0cb10e10308fcee25426ae0ff3 Mon Sep 17 00:00:00 2001 From: DemianRaccoonGang Date: Thu, 31 Jul 2025 18:37:53 +0300 Subject: [PATCH 11/19] fix: connectivity logic update --- Core/Core/Configuration/Connectivity.swift | 80 ++++++++++------------ OpenEdX/DI/AppAssembly.swift | 7 +- 2 files changed, 39 insertions(+), 48 deletions(-) diff --git a/Core/Core/Configuration/Connectivity.swift b/Core/Core/Configuration/Connectivity.swift index 25ecf63e6..4349f4d0c 100644 --- a/Core/Core/Configuration/Connectivity.swift +++ b/Core/Core/Configuration/Connectivity.swift @@ -22,90 +22,86 @@ public protocol ConnectivityProtocol: Sendable { var internetReachableSubject: CurrentValueSubject { get } } -@MainActor public class Connectivity: ConnectivityProtocol { private let networkManager = NetworkReachabilityManager() private let verificationURL: URL private let verificationTimeout: TimeInterval - private let secondsPast: TimeInterval = 30 + private let cacheValidity: TimeInterval = 5//30 + + private var lastVerificationDate: TimeInterval? + private var lastVerificationResult: Bool = true - private static var lastVerificationDate: TimeInterval? - private static var lastVerificationResult: Bool = false + public let internetReachableSubject = CurrentValueSubject(nil) - private var _isInternetAvailable: Bool = true { + private(set) var _isInternetAvailable: Bool = true { didSet { - internetReachableSubject.send(_isInternetAvailable ? .reachable : .notReachable) + Task { @MainActor in + internetReachableSubject.send(_isInternetAvailable ? .reachable : .notReachable) + } } } public var isInternetAvaliable: Bool { - _isInternetAvailable + if let last = lastVerificationDate, + Date().timeIntervalSince1970 - last < cacheValidity { + return lastVerificationResult + } + + Task { + await performVerification() + } + + return lastVerificationResult } public var isMobileData: Bool { networkManager?.isReachableOnCellular == true } - public let internetReachableSubject = CurrentValueSubject(nil) - public init( config: ConfigProtocol, timeout: TimeInterval = 15 ) { self.verificationURL = config.baseURL self.verificationTimeout = timeout - checkInternet() - } - - deinit { - networkManager?.stopListening() - } - - @MainActor - private func updateAvailability( - _ available: Bool, - lastChecked: TimeInterval = Date().timeIntervalSince1970 - ) { - self._isInternetAvailable = available - Connectivity.lastVerificationDate = lastChecked - Connectivity.lastVerificationResult = available - } - func checkInternet() { networkManager?.startListening(onQueue: .global()) { [weak self] status in guard let self = self else { return } - let now = Date().timeIntervalSince1970 - Task { @MainActor in switch status { case .reachable: - if let last = Connectivity.lastVerificationDate, - now - last < self.secondsPast { - self.updateAvailability(Connectivity.lastVerificationResult) - } else { - Task.detached { - let live = await self.verifyInternet() - await self.updateAvailability(live, lastChecked: Date().timeIntervalSince1970) - } - } - + await self.performVerification() case .notReachable, .unknown: - self.updateAvailability(false, lastChecked: 0) + self.updateAvailability(false, at: 0) } } } } + deinit { + networkManager?.stopListening() + } + + private func performVerification() async { + let now = Date().timeIntervalSince1970 + let live = await verifyInternet() + updateAvailability(live, at: now) + } + + private func updateAvailability(_ available: Bool, at timestamp: TimeInterval) { + _isInternetAvailable = available + lastVerificationDate = timestamp + lastVerificationResult = available + } + private func verifyInternet() async -> Bool { var request = URLRequest(url: verificationURL) request.httpMethod = "HEAD" request.timeoutInterval = verificationTimeout - do { let (_, response) = try await URLSession.shared.data(for: request) - if let http = response as? HTTPURLResponse, - (200..<400).contains(http.statusCode) { + if let http = response as? HTTPURLResponse, (200..<400).contains(http.statusCode) { return true } } catch { diff --git a/OpenEdX/DI/AppAssembly.swift b/OpenEdX/DI/AppAssembly.swift index 9dc64cf92..93bb9cb52 100644 --- a/OpenEdX/DI/AppAssembly.swift +++ b/OpenEdX/DI/AppAssembly.swift @@ -87,11 +87,6 @@ class AppAssembly: Assembly { r.resolve(AnalyticsManager.self)! }.inObjectScope(.container) -// container.register(ConnectivityProtocol.self) { @MainActor r in -// Connectivity(config: r.resolve(ConfigProtocol.self)!) -// } -// - container.register(DatabaseManager.self) { _ in DatabaseManager(databaseName: "Database") }.inObjectScope(.container) @@ -242,7 +237,7 @@ class AppAssembly: Assembly { config: r.resolve(ConfigProtocol.self)!, timeout: 15 ) - } + }.inObjectScope(.container) } } // swiftlint:enable function_body_length From a9074d0ef59cb935ebcc97d5da9da21340d49fc9 Mon Sep 17 00:00:00 2001 From: DemianRaccoonGang Date: Thu, 31 Jul 2025 18:39:29 +0300 Subject: [PATCH 12/19] fix: cacheValidity change --- Core/Core/Configuration/Connectivity.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Core/Core/Configuration/Connectivity.swift b/Core/Core/Configuration/Connectivity.swift index 4349f4d0c..1168c41e1 100644 --- a/Core/Core/Configuration/Connectivity.swift +++ b/Core/Core/Configuration/Connectivity.swift @@ -27,7 +27,7 @@ public class Connectivity: ConnectivityProtocol { private let networkManager = NetworkReachabilityManager() private let verificationURL: URL private let verificationTimeout: TimeInterval - private let cacheValidity: TimeInterval = 5//30 + private let cacheValidity: TimeInterval = 30 private var lastVerificationDate: TimeInterval? private var lastVerificationResult: Bool = true From dabc7441331e29fefebb89e91370ee1cbe74ed51 Mon Sep 17 00:00:00 2001 From: DemianRaccoonGang Date: Wed, 6 Aug 2025 15:44:53 +0300 Subject: [PATCH 13/19] fix: unit tests update --- .../DiscoveryViewModelTests.swift | 8 +-- .../Presentation/SearchViewModelTests.swift | 8 +-- OpenEdX.xcodeproj/project.pbxproj | 60 +++++++++---------- 3 files changed, 38 insertions(+), 38 deletions(-) diff --git a/Discovery/DiscoveryTests/Presentation/DiscoveryViewModelTests.swift b/Discovery/DiscoveryTests/Presentation/DiscoveryViewModelTests.swift index f9918498d..8b9beb780 100644 --- a/Discovery/DiscoveryTests/Presentation/DiscoveryViewModelTests.swift +++ b/Discovery/DiscoveryTests/Presentation/DiscoveryViewModelTests.swift @@ -25,7 +25,7 @@ final class DiscoveryViewModelTests: XCTestCase { func testGetDiscoveryCourses() async throws { let interactor = DiscoveryInteractorProtocolMock() - let connectivity = Connectivity() + let connectivity = Connectivity(config: ConfigMock()) let analytics = DiscoveryAnalyticsMock() let viewModel = DiscoveryViewModel(router: DiscoveryRouterMock(), config: ConfigMock(), @@ -81,7 +81,7 @@ final class DiscoveryViewModelTests: XCTestCase { func testDiscoverySuccess() async throws { let interactor = DiscoveryInteractorProtocolMock() - let connectivity = Connectivity() + let connectivity = Connectivity(config: ConfigMock()) let analytics = DiscoveryAnalyticsMock() let viewModel = DiscoveryViewModel(router: DiscoveryRouterMock(), config: ConfigMock(), @@ -191,7 +191,7 @@ final class DiscoveryViewModelTests: XCTestCase { func testDiscoveryNoInternetError() async throws { let interactor = DiscoveryInteractorProtocolMock() - let connectivity = Connectivity() + let connectivity = Connectivity(config: ConfigMock()) let analytics = DiscoveryAnalyticsMock() let viewModel = DiscoveryViewModel(router: DiscoveryRouterMock(), config: ConfigMock(), @@ -215,7 +215,7 @@ final class DiscoveryViewModelTests: XCTestCase { func testDiscoveryUnknownError() async throws { let interactor = DiscoveryInteractorProtocolMock() - let connectivity = Connectivity() + let connectivity = Connectivity(config: ConfigMock()) let analytics = DiscoveryAnalyticsMock() let viewModel = DiscoveryViewModel(router: DiscoveryRouterMock(), config: ConfigMock(), diff --git a/Discovery/DiscoveryTests/Presentation/SearchViewModelTests.swift b/Discovery/DiscoveryTests/Presentation/SearchViewModelTests.swift index a1851efcc..d837f982c 100644 --- a/Discovery/DiscoveryTests/Presentation/SearchViewModelTests.swift +++ b/Discovery/DiscoveryTests/Presentation/SearchViewModelTests.swift @@ -25,7 +25,7 @@ final class SearchViewModelTests: XCTestCase { func testSearchSuccess() async throws { let interactor = DiscoveryInteractorProtocolMock() - let connectivity = Connectivity() + let connectivity = Connectivity(config: ConfigMock()) let analytics = DiscoveryAnalyticsMock() let router = DiscoveryRouterMock() let viewModel = SearchViewModel( @@ -87,7 +87,7 @@ final class SearchViewModelTests: XCTestCase { func testSearchEmptyQuerySuccess() async throws { let interactor = DiscoveryInteractorProtocolMock() - let connectivity = Connectivity() + let connectivity = Connectivity(config: ConfigMock()) let analytics = DiscoveryAnalyticsMock() let router = DiscoveryRouterMock() let viewModel = SearchViewModel( @@ -111,7 +111,7 @@ final class SearchViewModelTests: XCTestCase { func testSearchNoInternetError() async throws { let interactor = DiscoveryInteractorProtocolMock() - let connectivity = Connectivity() + let connectivity = Connectivity(config: ConfigMock()) let analytics = DiscoveryAnalyticsMock() let router = DiscoveryRouterMock() let viewModel = SearchViewModel( @@ -143,7 +143,7 @@ final class SearchViewModelTests: XCTestCase { func testSearchUnknownError() async throws { let interactor = DiscoveryInteractorProtocolMock() - let connectivity = Connectivity() + let connectivity = Connectivity(config: ConfigMock()) let analytics = DiscoveryAnalyticsMock() let router = DiscoveryRouterMock() let viewModel = SearchViewModel( diff --git a/OpenEdX.xcodeproj/project.pbxproj b/OpenEdX.xcodeproj/project.pbxproj index 0faa4d76e..bc4acd68d 100644 --- a/OpenEdX.xcodeproj/project.pbxproj +++ b/OpenEdX.xcodeproj/project.pbxproj @@ -47,7 +47,7 @@ 07D5DA3528D075AA00752FD9 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 07D5DA3428D075AA00752FD9 /* AppDelegate.swift */; }; 07D5DA3E28D075AB00752FD9 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 07D5DA3D28D075AB00752FD9 /* Assets.xcassets */; }; 149FF39E2B9F1AB50034B33F /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 149FF39C2B9F1AB50034B33F /* LaunchScreen.storyboard */; }; - 3ACA3A1E886F3F9B2735B9AF /* Pods_App_OpenEdX.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3D27E034A83DDEBF18D53B04 /* Pods_App_OpenEdX.framework */; }; + 705A908842AAAFC361CD9D52 /* Pods_App_OpenEdX.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 58FAA9E3ECC93D0E638D877D /* Pods_App_OpenEdX.framework */; }; A500668B2B613ED10024680B /* PushNotificationsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = A500668A2B613ED10024680B /* PushNotificationsManager.swift */; }; A500668D2B6143000024680B /* FCMProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = A500668C2B6143000024680B /* FCMProvider.swift */; }; A50066932B614DCD0024680B /* FCMListener.swift in Sources */ = {isa = PBXBuildFile; fileRef = A50066922B614DCD0024680B /* FCMListener.swift */; }; @@ -95,7 +95,6 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ - 005D1F4D92679D24B3BAA8FE /* Pods-App-OpenEdX.releasedev.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-OpenEdX.releasedev.xcconfig"; path = "Target Support Files/Pods-App-OpenEdX/Pods-App-OpenEdX.releasedev.xcconfig"; sourceTree = ""; }; 020CA5D82AA0A25300970AAF /* AppStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppStorage.swift; sourceTree = ""; }; 0218196328F734FA00202564 /* Discussion.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Discussion.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 0219C67628F4347600D64452 /* Course.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Course.framework; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -136,9 +135,12 @@ 07D5DA3428D075AA00752FD9 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 07D5DA3D28D075AB00752FD9 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 149FF39D2B9F1AB50034B33F /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; - 3D27E034A83DDEBF18D53B04 /* Pods_App_OpenEdX.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_App_OpenEdX.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - 4449C4D4F119C87B452DDFCD /* Pods-App-OpenEdX.releaseprod.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-OpenEdX.releaseprod.xcconfig"; path = "Target Support Files/Pods-App-OpenEdX/Pods-App-OpenEdX.releaseprod.xcconfig"; sourceTree = ""; }; - 8A45E2C9AF0CBE70A09FB37B /* Pods-App-OpenEdX.debugstage.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-OpenEdX.debugstage.xcconfig"; path = "Target Support Files/Pods-App-OpenEdX/Pods-App-OpenEdX.debugstage.xcconfig"; sourceTree = ""; }; + 23F5E05C0D7EC044B0C9E719 /* Pods-App-OpenEdX.debugstage.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-OpenEdX.debugstage.xcconfig"; path = "Target Support Files/Pods-App-OpenEdX/Pods-App-OpenEdX.debugstage.xcconfig"; sourceTree = ""; }; + 2C04239322282B0E6963D56B /* Pods-App-OpenEdX.debugdev.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-OpenEdX.debugdev.xcconfig"; path = "Target Support Files/Pods-App-OpenEdX/Pods-App-OpenEdX.debugdev.xcconfig"; sourceTree = ""; }; + 58FAA9E3ECC93D0E638D877D /* Pods_App_OpenEdX.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_App_OpenEdX.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 6EAAEFD45AC766684492B1F7 /* Pods-App-OpenEdX.releasestage.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-OpenEdX.releasestage.xcconfig"; path = "Target Support Files/Pods-App-OpenEdX/Pods-App-OpenEdX.releasestage.xcconfig"; sourceTree = ""; }; + 84185F0B853BA4F0A8C0217C /* Pods-App-OpenEdX.releaseprod.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-OpenEdX.releaseprod.xcconfig"; path = "Target Support Files/Pods-App-OpenEdX/Pods-App-OpenEdX.releaseprod.xcconfig"; sourceTree = ""; }; + 8A39EAD8663E6F16A59AF82E /* Pods-App-OpenEdX.releasedev.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-OpenEdX.releasedev.xcconfig"; path = "Target Support Files/Pods-App-OpenEdX/Pods-App-OpenEdX.releasedev.xcconfig"; sourceTree = ""; }; A500668A2B613ED10024680B /* PushNotificationsManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PushNotificationsManager.swift; sourceTree = ""; }; A500668C2B6143000024680B /* FCMProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FCMProvider.swift; sourceTree = ""; }; A50066922B614DCD0024680B /* FCMListener.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FCMListener.swift; sourceTree = ""; }; @@ -147,14 +149,12 @@ A59568962B61653700ED4F90 /* DeepLink.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeepLink.swift; sourceTree = ""; }; A59568982B616D9400ED4F90 /* PushLink.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PushLink.swift; sourceTree = ""; }; BA7468752B96201D00793145 /* DeepLinkRouter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeepLinkRouter.swift; sourceTree = ""; }; - CCE1E0F850D3E25C0D6C6702 /* Pods-App-OpenEdX.debugdev.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-OpenEdX.debugdev.xcconfig"; path = "Target Support Files/Pods-App-OpenEdX/Pods-App-OpenEdX.debugdev.xcconfig"; sourceTree = ""; }; CE1D5B7A2CE60E000019CA34 /* ContainerMainActor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContainerMainActor.swift; sourceTree = ""; }; CE3BD14D2CBEB0DA0026F4E3 /* PluginManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PluginManager.swift; sourceTree = ""; }; CEB36E512D6A29CE00907A89 /* Downloads.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Downloads.framework; sourceTree = BUILT_PRODUCTS_DIR; }; CEE5EDED2D6E0A290089F67C /* DownloadsPersistence.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DownloadsPersistence.swift; sourceTree = ""; }; - D70D30110012B7D52D05E876 /* Pods-App-OpenEdX.debugprod.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-OpenEdX.debugprod.xcconfig"; path = "Target Support Files/Pods-App-OpenEdX/Pods-App-OpenEdX.debugprod.xcconfig"; sourceTree = ""; }; E0D6E6A22B1626B10089F9C9 /* Theme.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Theme.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - FD0CE8D22B755B4003A113BB /* Pods-App-OpenEdX.releasestage.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-OpenEdX.releasestage.xcconfig"; path = "Target Support Files/Pods-App-OpenEdX/Pods-App-OpenEdX.releasestage.xcconfig"; sourceTree = ""; }; + F6C9DA21C55F17F9F1F1D9A1 /* Pods-App-OpenEdX.debugprod.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-OpenEdX.debugprod.xcconfig"; path = "Target Support Files/Pods-App-OpenEdX/Pods-App-OpenEdX.debugprod.xcconfig"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -176,7 +176,7 @@ 0219C67728F4347600D64452 /* Course.framework in Frameworks */, CEBA52772CEBB69100619E2B /* OEXFirebaseAnalytics in Frameworks */, 027DB33028D8A063002B6862 /* Dashboard.framework in Frameworks */, - 3ACA3A1E886F3F9B2735B9AF /* Pods_App_OpenEdX.framework in Frameworks */, + 705A908842AAAFC361CD9D52 /* Pods_App_OpenEdX.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -282,7 +282,7 @@ 072787B028D34D83002E9142 /* Discovery.framework */, 0770DE4A28D0A462006D8A5D /* Authorization.framework */, 0770DE1228D07845006D8A5D /* Core.framework */, - 3D27E034A83DDEBF18D53B04 /* Pods_App_OpenEdX.framework */, + 58FAA9E3ECC93D0E638D877D /* Pods_App_OpenEdX.framework */, ); name = Frameworks; sourceTree = ""; @@ -290,12 +290,12 @@ 55A895025FB07897BA68E063 /* Pods */ = { isa = PBXGroup; children = ( - D70D30110012B7D52D05E876 /* Pods-App-OpenEdX.debugprod.xcconfig */, - 8A45E2C9AF0CBE70A09FB37B /* Pods-App-OpenEdX.debugstage.xcconfig */, - CCE1E0F850D3E25C0D6C6702 /* Pods-App-OpenEdX.debugdev.xcconfig */, - 4449C4D4F119C87B452DDFCD /* Pods-App-OpenEdX.releaseprod.xcconfig */, - FD0CE8D22B755B4003A113BB /* Pods-App-OpenEdX.releasestage.xcconfig */, - 005D1F4D92679D24B3BAA8FE /* Pods-App-OpenEdX.releasedev.xcconfig */, + F6C9DA21C55F17F9F1F1D9A1 /* Pods-App-OpenEdX.debugprod.xcconfig */, + 23F5E05C0D7EC044B0C9E719 /* Pods-App-OpenEdX.debugstage.xcconfig */, + 2C04239322282B0E6963D56B /* Pods-App-OpenEdX.debugdev.xcconfig */, + 84185F0B853BA4F0A8C0217C /* Pods-App-OpenEdX.releaseprod.xcconfig */, + 6EAAEFD45AC766684492B1F7 /* Pods-App-OpenEdX.releasestage.xcconfig */, + 8A39EAD8663E6F16A59AF82E /* Pods-App-OpenEdX.releasedev.xcconfig */, ); path = Pods; sourceTree = ""; @@ -390,7 +390,7 @@ isa = PBXNativeTarget; buildConfigurationList = 07D5DA4528D075AB00752FD9 /* Build configuration list for PBXNativeTarget "OpenEdX" */; buildPhases = ( - B2F937DF587D9697AF13B3F9 /* [CP] Check Pods Manifest.lock */, + 8389A0AC71F15AC25A0DF8E0 /* [CP] Check Pods Manifest.lock */, 0770DE2328D08647006D8A5D /* SwiftLint */, 07D5DA2D28D075AA00752FD9 /* Sources */, 07D5DA2E28D075AA00752FD9 /* Frameworks */, @@ -511,7 +511,7 @@ shellPath = /bin/sh; shellScript = "\"${PODS_ROOT}/SwiftLint/swiftlint\"\n"; }; - B2F937DF587D9697AF13B3F9 /* [CP] Check Pods Manifest.lock */ = { + 8389A0AC71F15AC25A0DF8E0 /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( @@ -694,14 +694,14 @@ }; 02DD1C9629E80CC200F35DCE /* DebugStage */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 8A45E2C9AF0CBE70A09FB37B /* Pods-App-OpenEdX.debugstage.xcconfig */; + baseConfigurationReference = 23F5E05C0D7EC044B0C9E719 /* Pods-App-OpenEdX.debugstage.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_ENTITLEMENTS = OpenEdX/OpenEdX.entitlements; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = ""; + DEVELOPMENT_TEAM = L8PG7LC3Y3; FULLSTORY_ENABLED = NO; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = OpenEdX/Info.plist; @@ -786,14 +786,14 @@ }; 02DD1C9829E80CCB00F35DCE /* ReleaseStage */ = { isa = XCBuildConfiguration; - baseConfigurationReference = FD0CE8D22B755B4003A113BB /* Pods-App-OpenEdX.releasestage.xcconfig */; + baseConfigurationReference = 6EAAEFD45AC766684492B1F7 /* Pods-App-OpenEdX.releasestage.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_ENTITLEMENTS = OpenEdX/OpenEdX.entitlements; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = ""; + DEVELOPMENT_TEAM = L8PG7LC3Y3; FULLSTORY_ENABLED = NO; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = OpenEdX/Info.plist; @@ -884,14 +884,14 @@ }; 0727875928D231FD002E9142 /* DebugDev */ = { isa = XCBuildConfiguration; - baseConfigurationReference = CCE1E0F850D3E25C0D6C6702 /* Pods-App-OpenEdX.debugdev.xcconfig */; + baseConfigurationReference = 2C04239322282B0E6963D56B /* Pods-App-OpenEdX.debugdev.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_ENTITLEMENTS = OpenEdX/OpenEdX.entitlements; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = ""; + DEVELOPMENT_TEAM = L8PG7LC3Y3; FULLSTORY_ENABLED = NO; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = OpenEdX/Info.plist; @@ -976,14 +976,14 @@ }; 0727875B28D23204002E9142 /* ReleaseDev */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 005D1F4D92679D24B3BAA8FE /* Pods-App-OpenEdX.releasedev.xcconfig */; + baseConfigurationReference = 8A39EAD8663E6F16A59AF82E /* Pods-App-OpenEdX.releasedev.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_ENTITLEMENTS = OpenEdX/OpenEdX.entitlements; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = ""; + DEVELOPMENT_TEAM = L8PG7LC3Y3; FULLSTORY_ENABLED = NO; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = OpenEdX/Info.plist; @@ -1128,14 +1128,14 @@ }; 07D5DA4628D075AB00752FD9 /* DebugProd */ = { isa = XCBuildConfiguration; - baseConfigurationReference = D70D30110012B7D52D05E876 /* Pods-App-OpenEdX.debugprod.xcconfig */; + baseConfigurationReference = F6C9DA21C55F17F9F1F1D9A1 /* Pods-App-OpenEdX.debugprod.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_ENTITLEMENTS = OpenEdX/OpenEdX.entitlements; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = ""; + DEVELOPMENT_TEAM = L8PG7LC3Y3; FULLSTORY_ENABLED = NO; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = OpenEdX/Info.plist; @@ -1166,14 +1166,14 @@ }; 07D5DA4728D075AB00752FD9 /* ReleaseProd */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 4449C4D4F119C87B452DDFCD /* Pods-App-OpenEdX.releaseprod.xcconfig */; + baseConfigurationReference = 84185F0B853BA4F0A8C0217C /* Pods-App-OpenEdX.releaseprod.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_ENTITLEMENTS = OpenEdX/OpenEdX.entitlements; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = ""; + DEVELOPMENT_TEAM = L8PG7LC3Y3; FULLSTORY_ENABLED = NO; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = OpenEdX/Info.plist; From 48d55bc69d0db8ea0b73c210482cbb320df002cb Mon Sep 17 00:00:00 2001 From: DemianRaccoonGang Date: Wed, 6 Aug 2025 15:59:55 +0300 Subject: [PATCH 14/19] fix: removed team and coma --- OpenEdX.xcodeproj/project.pbxproj | 12 ++++++------ .../Profile/Presentation/Profile/ProfileView.swift | 4 ++-- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/OpenEdX.xcodeproj/project.pbxproj b/OpenEdX.xcodeproj/project.pbxproj index bc4acd68d..7cd19c72b 100644 --- a/OpenEdX.xcodeproj/project.pbxproj +++ b/OpenEdX.xcodeproj/project.pbxproj @@ -701,7 +701,7 @@ CODE_SIGN_ENTITLEMENTS = OpenEdX/OpenEdX.entitlements; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = L8PG7LC3Y3; + DEVELOPMENT_TEAM = ""; FULLSTORY_ENABLED = NO; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = OpenEdX/Info.plist; @@ -793,7 +793,7 @@ CODE_SIGN_ENTITLEMENTS = OpenEdX/OpenEdX.entitlements; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = L8PG7LC3Y3; + DEVELOPMENT_TEAM = ""; FULLSTORY_ENABLED = NO; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = OpenEdX/Info.plist; @@ -891,7 +891,7 @@ CODE_SIGN_ENTITLEMENTS = OpenEdX/OpenEdX.entitlements; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = L8PG7LC3Y3; + DEVELOPMENT_TEAM = ""; FULLSTORY_ENABLED = NO; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = OpenEdX/Info.plist; @@ -983,7 +983,7 @@ CODE_SIGN_ENTITLEMENTS = OpenEdX/OpenEdX.entitlements; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = L8PG7LC3Y3; + DEVELOPMENT_TEAM = ""; FULLSTORY_ENABLED = NO; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = OpenEdX/Info.plist; @@ -1135,7 +1135,7 @@ CODE_SIGN_ENTITLEMENTS = OpenEdX/OpenEdX.entitlements; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = L8PG7LC3Y3; + DEVELOPMENT_TEAM = ""; FULLSTORY_ENABLED = NO; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = OpenEdX/Info.plist; @@ -1173,7 +1173,7 @@ CODE_SIGN_ENTITLEMENTS = OpenEdX/OpenEdX.entitlements; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = L8PG7LC3Y3; + DEVELOPMENT_TEAM = ""; FULLSTORY_ENABLED = NO; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = OpenEdX/Info.plist; diff --git a/Profile/Profile/Presentation/Profile/ProfileView.swift b/Profile/Profile/Presentation/Profile/ProfileView.swift index b9d49b4b8..b1c2e6c6d 100644 --- a/Profile/Profile/Presentation/Profile/ProfileView.swift +++ b/Profile/Profile/Presentation/Profile/ProfileView.swift @@ -195,9 +195,9 @@ struct ProfileView_Previews: PreviewProvider { router: router, analytics: ProfileAnalyticsMock(), config: config, - connectivity: Connectivity(config: config), + connectivity: Connectivity(config: config) ) - + ProfileView(viewModel: vm) .preferredColorScheme(.light) .previewDisplayName("DiscoveryView Light") From 786fa30b170a496783c40e3f57a23de163408106 Mon Sep 17 00:00:00 2001 From: DemianRaccoonGang Date: Wed, 6 Aug 2025 16:13:09 +0300 Subject: [PATCH 15/19] fix: removed extra comas --- .../Outline/CourseVertical/CourseVerticalView.swift | 2 +- .../Presentation/DatesAndCalendar/CoursesToSyncView.swift | 2 +- .../Presentation/DatesAndCalendar/DatesAndCalendarView.swift | 2 +- .../Presentation/DatesAndCalendar/SyncCalendarOptionsView.swift | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Course/Course/Presentation/Outline/CourseVertical/CourseVerticalView.swift b/Course/Course/Presentation/Outline/CourseVertical/CourseVerticalView.swift index 364e9c00a..278215ea3 100644 --- a/Course/Course/Presentation/Outline/CourseVertical/CourseVerticalView.swift +++ b/Course/Course/Presentation/Outline/CourseVertical/CourseVerticalView.swift @@ -181,7 +181,7 @@ struct CourseVerticalView_Previews: PreviewProvider { sequentialIndex: 0, router: CourseRouterMock(), analytics: CourseAnalyticsMock(), - connectivity: Connectivity(config: ConfigMock()), + connectivity: Connectivity(config: ConfigMock()) ) return Group { diff --git a/Profile/Profile/Presentation/DatesAndCalendar/CoursesToSyncView.swift b/Profile/Profile/Presentation/DatesAndCalendar/CoursesToSyncView.swift index e0791ac31..f1aae91ef 100644 --- a/Profile/Profile/Presentation/DatesAndCalendar/CoursesToSyncView.swift +++ b/Profile/Profile/Presentation/DatesAndCalendar/CoursesToSyncView.swift @@ -158,7 +158,7 @@ struct CoursesToSyncView_Previews: PreviewProvider { profileStorage: ProfileStorageMock(), persistence: ProfilePersistenceMock(), calendarManager: CalendarManagerMock(), - connectivity: Connectivity(config: ConfigMock()), + connectivity: Connectivity(config: ConfigMock()) ) return CoursesToSyncView(viewModel: vm) .previewDisplayName("Courses to Sync") diff --git a/Profile/Profile/Presentation/DatesAndCalendar/DatesAndCalendarView.swift b/Profile/Profile/Presentation/DatesAndCalendar/DatesAndCalendarView.swift index 3993b8580..6539ea081 100644 --- a/Profile/Profile/Presentation/DatesAndCalendar/DatesAndCalendarView.swift +++ b/Profile/Profile/Presentation/DatesAndCalendar/DatesAndCalendarView.swift @@ -188,7 +188,7 @@ struct DatesAndCalendarView_Previews: PreviewProvider { profileStorage: ProfileStorageMock(), persistence: ProfilePersistenceMock(), calendarManager: CalendarManagerMock(), - connectivity: Connectivity(config: ConfigMock()), + connectivity: Connectivity(config: ConfigMock()) ) DatesAndCalendarView(viewModel: vm) .loadFonts() diff --git a/Profile/Profile/Presentation/DatesAndCalendar/SyncCalendarOptionsView.swift b/Profile/Profile/Presentation/DatesAndCalendar/SyncCalendarOptionsView.swift index e96a0cc1b..be28cc458 100644 --- a/Profile/Profile/Presentation/DatesAndCalendar/SyncCalendarOptionsView.swift +++ b/Profile/Profile/Presentation/DatesAndCalendar/SyncCalendarOptionsView.swift @@ -271,7 +271,7 @@ struct SyncCalendarOptionsView_Previews: PreviewProvider { profileStorage: ProfileStorageMock(), persistence: ProfilePersistenceMock(), calendarManager: CalendarManagerMock(), - connectivity: Connectivity(config: ConfigMock()), + connectivity: Connectivity(config: ConfigMock()) ) SyncCalendarOptionsView(viewModel: vm) .loadFonts() From 0fce30be34b2c813a325b297bae3136ed9bed0f3 Mon Sep 17 00:00:00 2001 From: DemianRaccoonGang Date: Tue, 16 Sep 2025 11:55:36 +0300 Subject: [PATCH 16/19] fix: unit tests --- .../Course/Presentation/Container/CourseContainerView.swift | 6 +++--- .../Presentation/Container/CourseContainerViewModel.swift | 2 +- Course/Course/Presentation/Content/CourseContentView.swift | 2 +- .../Presentation/Content/Subviews/AllContentView.swift | 2 +- .../Presentation/Progress/CourseProgressScreenView.swift | 4 ++-- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/Course/Course/Presentation/Container/CourseContainerView.swift b/Course/Course/Presentation/Container/CourseContainerView.swift index 73ea05091..8c8a2828b 100644 --- a/Course/Course/Presentation/Container/CourseContainerView.swift +++ b/Course/Course/Presentation/Container/CourseContainerView.swift @@ -382,7 +382,7 @@ public struct CourseContainerView: View { router: CourseRouterMock(), analytics: CourseAnalyticsMock(), config: ConfigMock(), - connectivity: Connectivity(), + connectivity: Connectivity(config: ConfigMock()), manager: DownloadManagerMock(), storage: CourseStorageMock(), isActive: true, @@ -398,7 +398,7 @@ public struct CourseContainerView: View { interactor: CourseInteractor.mock, router: CourseRouterMock(), cssInjector: CSSInjectorMock(), - connectivity: Connectivity(), + connectivity: Connectivity(config: ConfigMock()), config: ConfigMock(), courseID: "1", courseName: "a", @@ -409,7 +409,7 @@ public struct CourseContainerView: View { interactor: CourseInteractor.mock, router: CourseRouterMock(), analytics: CourseAnalyticsMock(), - connectivity: Connectivity() + connectivity: Connectivity(config: ConfigMock()), ), courseID: "", title: "Title of Course", diff --git a/Course/Course/Presentation/Container/CourseContainerViewModel.swift b/Course/Course/Presentation/Container/CourseContainerViewModel.swift index 2d1b2d051..7324dba43 100644 --- a/Course/Course/Presentation/Container/CourseContainerViewModel.swift +++ b/Course/Course/Presentation/Container/CourseContainerViewModel.swift @@ -1612,7 +1612,7 @@ extension CourseContainerViewModel { router: CourseRouterMock(), analytics: CourseAnalyticsMock(), config: ConfigMock(), - connectivity: Connectivity(), + connectivity: Connectivity(config: ConfigMock()), manager: DownloadManagerMock(), storage: CourseStorageMock(), isActive: true, diff --git a/Course/Course/Presentation/Content/CourseContentView.swift b/Course/Course/Presentation/Content/CourseContentView.swift index d23590201..dbcca75c7 100644 --- a/Course/Course/Presentation/Content/CourseContentView.swift +++ b/Course/Course/Presentation/Content/CourseContentView.swift @@ -266,7 +266,7 @@ public struct CourseContentView: View { router: CourseRouterMock(), analytics: CourseAnalyticsMock(), config: ConfigMock(), - connectivity: Connectivity(), + connectivity: Connectivity(config: ConfigMock()), manager: DownloadManagerMock(), storage: CourseStorageMock(), isActive: true, diff --git a/Course/Course/Presentation/Content/Subviews/AllContentView.swift b/Course/Course/Presentation/Content/Subviews/AllContentView.swift index 696586dd3..dc37c86b9 100644 --- a/Course/Course/Presentation/Content/Subviews/AllContentView.swift +++ b/Course/Course/Presentation/Content/Subviews/AllContentView.swift @@ -142,7 +142,7 @@ struct AllContentView: View { router: CourseRouterMock(), analytics: CourseAnalyticsMock(), config: ConfigMock(), - connectivity: Connectivity(), + connectivity: Connectivity(config: ConfigMock()), manager: DownloadManagerMock(), storage: CourseStorageMock(), isActive: true, diff --git a/Course/Course/Presentation/Progress/CourseProgressScreenView.swift b/Course/Course/Presentation/Progress/CourseProgressScreenView.swift index 77341907d..7419a52ef 100644 --- a/Course/Course/Presentation/Progress/CourseProgressScreenView.swift +++ b/Course/Course/Presentation/Progress/CourseProgressScreenView.swift @@ -227,7 +227,7 @@ struct CourseProgressScreenView: View { interactor: CourseInteractor.mock, router: CourseRouterMock(), analytics: CourseAnalyticsMock(), - connectivity: Connectivity() + connectivity: Connectivity(config: ConfigMock()) ) CourseProgressScreenView( @@ -236,7 +236,7 @@ struct CourseProgressScreenView: View { collapsed: .constant(false), viewHeight: .constant(0), viewModel: vm, - connectivity: Connectivity() + connectivity: Connectivity(config: ConfigMock()) ) .loadFonts() } From ad3f2608d28add66453fa343d97c25284f1bcc8f Mon Sep 17 00:00:00 2001 From: DemianRaccoonGang Date: Wed, 29 Oct 2025 17:15:00 +0200 Subject: [PATCH 17/19] fix: removed state object warning --- .../Progress/CourseProgressScreenView.swift | 6 +++- default_config/dev/config.yaml | 35 +++---------------- 2 files changed, 9 insertions(+), 32 deletions(-) diff --git a/Course/Course/Presentation/Progress/CourseProgressScreenView.swift b/Course/Course/Presentation/Progress/CourseProgressScreenView.swift index 9a11ac964..3ee35119e 100644 --- a/Course/Course/Presentation/Progress/CourseProgressScreenView.swift +++ b/Course/Course/Presentation/Progress/CourseProgressScreenView.swift @@ -19,6 +19,7 @@ struct CourseProgressScreenView: View { @StateObject private var viewModel: CourseProgressViewModel + private let initialCourseStructure: CourseStructure? private let connectivity: ConnectivityProtocol @@ -37,7 +38,7 @@ struct CourseProgressScreenView: View { self._viewHeight = viewHeight self._viewModel = StateObject(wrappedValue: { viewModel }()) self.connectivity = connectivity - self.viewModel.courseStructure = courseStructure + self.initialCourseStructure = courseStructure } public var body: some View { @@ -111,6 +112,9 @@ struct CourseProgressScreenView: View { .ignoresSafeArea() ) .onFirstAppear { + if viewModel.courseStructure == nil { + viewModel.courseStructure = initialCourseStructure + } Task { await viewModel.getCourseProgress(courseID: courseID) } diff --git a/default_config/dev/config.yaml b/default_config/dev/config.yaml index 9b2c7d64a..0634e3d75 100644 --- a/default_config/dev/config.yaml +++ b/default_config/dev/config.yaml @@ -1,33 +1,6 @@ -API_HOST_URL: 'https://axim-ccpv-dev.raccoongang.net' +API_HOST_URL: 'http://localhost:8000' SSO_URL: 'http://localhost:8000' -SSO_FINISHED_URL: 'http://localhost:8000' -ENVIRONMENT_DISPLAY_NAME: 'axim-ccpv-dev' +SSO_FINISHED_URL: 'http://localhost:8000' +ENVIRONMENT_DISPLAY_NAME: 'Localhost' FEEDBACK_EMAIL_ADDRESS: 'support@example.com' -OAUTH_CLIENT_ID: 'SxDFlH1rb8Xw3nT6lOPwUejLb9vgXzOfkgqx1sY2' - -SSO_BUTTON_TITLE: - ar: "الدخول عبر SSO" - en: "Sign in with SSO" - -DISCOVERY: - TYPE: "native" - -FIREBASE: - ENABLED: true - ANALYTICS_SOURCE: "firebase" - CLOUD_MESSAGING_ENABLED: false - API_KEY: "AIzaSyCKAIXDLM7pnX43P_viTsfgbxrLBOaJwGo" - BUNDLE_ID: "com.raccoongang.NewEdX.stage" - CLIENT_ID: "156114692773-r5pgdcdjqq7sup75fdla4lk3q3kjc6m8.apps.googleusercontent.com" - GCM_SENDER_ID: "156114692773" - GOOGLE_APP_ID: "1:156114692773:ios:8058bca851a8bc7c187b4c" - PROJECT_ID: "openedxmobile-stage" - REVERSED_CLIENT_ID: "com.googleusercontent.apps.156114692773-r5pgdcdjqq7sup75fdla4lk3q3kjc6m8" - STORAGE_BUCKET: "openedxmobile-stage.appspot.com" - -UI_COMPONENTS: - COURSE_UNIT_PROGRESS_ENABLED: false - COURSE_NESTED_LIST_ENABLED: false - LOGIN_REGISTRATION_ENABLED: true - SAML_SSO_LOGIN_ENABLED: false - SAML_SSO_DEFAULT_LOGIN_BUTTON: false +OAUTH_CLIENT_ID: '' From 39ace76dab2b476e0b974823994eb914bab6f334 Mon Sep 17 00:00:00 2001 From: DemianRaccoonGang Date: Wed, 29 Oct 2025 18:06:51 +0200 Subject: [PATCH 18/19] fix: update config --- default_config/dev/config.yaml | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/default_config/dev/config.yaml b/default_config/dev/config.yaml index 0634e3d75..0d4e370e5 100644 --- a/default_config/dev/config.yaml +++ b/default_config/dev/config.yaml @@ -4,3 +4,18 @@ SSO_FINISHED_URL: 'http://localhost:8000' ENVIRONMENT_DISPLAY_NAME: 'Localhost' FEEDBACK_EMAIL_ADDRESS: 'support@example.com' OAUTH_CLIENT_ID: '' + +SSO_BUTTON_TITLE: + ar: "الدخول عبر SSO" + en: "Sign in with SSO" + +EXPERIMENTAL_FEATURES: + APP_LEVEL_DOWNLOADS: + ENABLED: false + +UI_COMPONENTS: + COURSE_UNIT_PROGRESS_ENABLED: false + COURSE_NESTED_LIST_ENABLED: false + LOGIN_REGISTRATION_ENABLED: true + SAML_SSO_LOGIN_ENABLED: false + SAML_SSO_DEFAULT_LOGIN_BUTTON: false From 2cd06ec1d04590f3b1dc61cd3326604b6765b89f Mon Sep 17 00:00:00 2001 From: DemianRaccoonGang Date: Thu, 15 Jan 2026 08:12:20 +0200 Subject: [PATCH 19/19] fix: return certificate view --- .../Model/Data_CourseProgressResponse.swift | 26 +++++++++++++++ .../Content/CourseContentView.swift | 31 +++++++++++++++++ .../CourseOutlineAndProgressView.swift | 33 ++++++++++++++++++- Podfile.lock | 2 +- 4 files changed, 90 insertions(+), 2 deletions(-) diff --git a/Course/Course/Data/Model/Data_CourseProgressResponse.swift b/Course/Course/Data/Model/Data_CourseProgressResponse.swift index 7182e47fb..9a3c27c71 100644 --- a/Course/Course/Data/Model/Data_CourseProgressResponse.swift +++ b/Course/Course/Data/Model/Data_CourseProgressResponse.swift @@ -46,6 +46,32 @@ public extension DataLayer { case verificationData = "verification_data" case disableProgressGraph = "disable_progress_graph" } + + public init(from decoder: Decoder) throws { + let values = try decoder.container(keyedBy: CodingKeys.self) + verifiedMode = try values.decodeIfPresent(String.self, forKey: .verifiedMode) + accessExpiration = try values.decodeIfPresent(String.self, forKey: .accessExpiration) + certificateData = try values.decodeIfPresent(CertificateData.self, forKey: .certificateData) + ?? CertificateData( + certStatus: nil, + certWebViewUrl: nil, + downloadUrl: nil, + certificateAvailableDate: nil + ) + completionSummary = try values.decode(CompletionSummary.self, forKey: .completionSummary) + courseGrade = try values.decode(CourseGrade.self, forKey: .courseGrade) + creditCourseRequirements = try values.decodeIfPresent(String.self, forKey: .creditCourseRequirements) + end = try values.decodeIfPresent(String.self, forKey: .end) + enrollmentMode = try values.decode(String.self, forKey: .enrollmentMode) + gradingPolicy = try values.decode(GradingPolicy.self, forKey: .gradingPolicy) + hasScheduledContent = try values.decodeIfPresent(Bool.self, forKey: .hasScheduledContent) + sectionScores = try values.decode([SectionScore].self, forKey: .sectionScores) + studioUrl = try values.decode(String.self, forKey: .studioUrl) + username = try values.decode(String.self, forKey: .username) + userHasPassingGrade = try values.decode(Bool.self, forKey: .userHasPassingGrade) + verificationData = try values.decode(VerificationData.self, forKey: .verificationData) + disableProgressGraph = try values.decode(Bool.self, forKey: .disableProgressGraph) + } public init( verifiedMode: String?, diff --git a/Course/Course/Presentation/Content/CourseContentView.swift b/Course/Course/Presentation/Content/CourseContentView.swift index 37f6d81b5..45f6fa9ff 100644 --- a/Course/Course/Presentation/Content/CourseContentView.swift +++ b/Course/Course/Presentation/Content/CourseContentView.swift @@ -17,6 +17,7 @@ public struct CourseContentView: View { @StateObject private var viewModel: CourseContainerViewModel private let title: String private let courseID: String + @State private var openCertificateView: Bool = false private var videoContentData: VideoContentData { VideoContentData( @@ -97,6 +98,8 @@ public struct CourseContentView: View { ) .padding(.horizontal, 24) .padding(.top, 16) + + certificateView // MARK: - Content based on selected tab contentForSelectedTab(proxy: proxy) @@ -176,6 +179,34 @@ public struct CourseContentView: View { .ignoresSafeArea() ) } + + @ViewBuilder + private var certificateView: some View { + if let certificate = viewModel.courseStructure?.certificate, + let url = certificate.url, + url.count > 0 { + MessageSectionView( + title: CourseLocalization.Outline.passedTheCourse(title), + actionTitle: CourseLocalization.Outline.viewCertificate, + action: { + openCertificateView = true + viewModel.trackViewCertificateClicked(courseID: courseID) + } + ) + .padding(.horizontal, 24) + .padding(.top, 16) + .fullScreenCover( + isPresented: $openCertificateView, + content: { + WebBrowser( + url: url, + pageTitle: CourseLocalization.Outline.certificate, + connectivity: viewModel.connectivity + ) + } + ) + } + } @ViewBuilder private func contentForSelectedTab(proxy: GeometryProxy) -> some View { diff --git a/Course/Course/Presentation/NewOutlIineAndProgress/CourseOutlineAndProgressView.swift b/Course/Course/Presentation/NewOutlIineAndProgress/CourseOutlineAndProgressView.swift index d12aacff4..7c96a93c4 100644 --- a/Course/Course/Presentation/NewOutlIineAndProgress/CourseOutlineAndProgressView.swift +++ b/Course/Course/Presentation/NewOutlIineAndProgress/CourseOutlineAndProgressView.swift @@ -67,6 +67,7 @@ public struct CourseOutlineAndProgressView: View { private var idiom: UIUserInterfaceIdiom { UIDevice.current.userInterfaceIdiom } + @State private var openCertificateView: Bool = false @State private var showingDownloads: Bool = false @State private var showingVideoDownloadQuality: Bool = false @Binding private var selection: Int @@ -130,6 +131,8 @@ public struct CourseOutlineAndProgressView: View { Spacer() + certificateView + if let continueWith = viewModelContainer.continueWith, let courseStructure = viewModelContainer.courseStructure { let chapter = courseStructure.childs[continueWith.chapterIndex] @@ -298,9 +301,37 @@ public struct CourseOutlineAndProgressView: View { carouselSections[idx] Spacer() } - .tag(idx) + .tag(idx) + .padding(.horizontal, 24) + .padding(.top, 16) + } + } + + @ViewBuilder + private var certificateView: some View { + if let certificate = viewModelContainer.courseStructure?.certificate, + let url = certificate.url, + url.count > 0 { + MessageSectionView( + title: CourseLocalization.Outline.passedTheCourse(title), + actionTitle: CourseLocalization.Outline.viewCertificate, + action: { + openCertificateView = true + viewModelContainer.trackViewCertificateClicked(courseID: courseID) + } + ) .padding(.horizontal, 24) .padding(.top, 16) + .fullScreenCover( + isPresented: $openCertificateView, + content: { + WebBrowser( + url: url, + pageTitle: CourseLocalization.Outline.certificate, + connectivity: connectivity + ) + } + ) } } diff --git a/Podfile.lock b/Podfile.lock index 92ba93fc7..ab2424669 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -36,4 +36,4 @@ SPEC CHECKSUMS: PODFILE CHECKSUM: 2d71ad797d49fa32b47c3315b92159de82824103 -COCOAPODS: 1.15.2 +COCOAPODS: 1.16.2