Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
b73c0c3
fix: resolve issue 581
Demian-Yushyn Jul 30, 2025
cc0e655
fix: update adjustments
Demian-Yushyn Jul 30, 2025
84a2156
fix: add timeout constant
Demian-Yushyn Jul 30, 2025
cd349b3
fix: adjust naming
Demian-Yushyn Jul 30, 2025
5aa8e55
fix: update connectivity
Demian-Yushyn Jul 30, 2025
64c59a1
fix: remove unused functions and hardcoded variables
Demian-Yushyn Jul 31, 2025
b4fcf31
fix: Removed unused functions and hardcoded variables
Demian-Yushyn Jul 31, 2025
253341d
Merge remote-tracking branch 'origin/fix/issue-581' into fix/issue-581
Demian-Yushyn Jul 31, 2025
6121c2a
fix: issue 581
Demian-Yushyn Jul 31, 2025
b26c3d7
fix: progress on issue 581
Demian-Yushyn Jul 31, 2025
f21a460
fix: apply updates
Demian-Yushyn Jul 31, 2025
14441a6
fix: update wi DI Config, thread fixes
Demian-Yushyn Jul 31, 2025
327bb30
fix: connectivity logic update
Demian-Yushyn Jul 31, 2025
a9074d0
fix: cacheValidity change
Demian-Yushyn Jul 31, 2025
dabc744
fix: unit tests update
Demian-Yushyn Aug 6, 2025
48d55bc
fix: removed team and coma
Demian-Yushyn Aug 6, 2025
786fa30
fix: removed extra comas
Demian-Yushyn Aug 6, 2025
a7475cd
Merge branch 'develop' into fix/issue-581
IvanStepanok Sep 16, 2025
0fce30b
fix: unit tests
Demian-Yushyn Sep 16, 2025
635c716
Merge branch 'develop' into fix/issue-581
Demian-Yushyn Oct 29, 2025
ad3f260
fix: removed state object warning
Demian-Yushyn Oct 29, 2025
39ace76
fix: update config
Demian-Yushyn Oct 29, 2025
8465734
Merge pull request #2 from raccoongang/fix/issue-581
IvanStepanok Nov 10, 2025
2cd06ec
fix: return certificate view
Demian-Yushyn Jan 15, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
114 changes: 79 additions & 35 deletions Core/Core/Configuration/Connectivity.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// Connectivity.swift
// OpenEdX
//
// Created by  Stepanok Ivan on 15.12.2022.
// Created by Stepanok Ivan on 15.12.2022.
//

import Alamofire
Expand All @@ -14,55 +14,99 @@ public enum InternetState: Sendable {
case notReachable
}

//sourcery: AutoMockable
// sourcery: AutoMockable
@MainActor
public protocol ConnectivityProtocol: Sendable {
var isInternetAvaliable: Bool { get }
var isMobileData: Bool { get }
var internetReachableSubject: CurrentValueSubject<InternetState?, Never> { get }
}

@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 let cacheValidity: TimeInterval = 30

private var lastVerificationDate: TimeInterval?
private var lastVerificationResult: Bool = true

public let internetReachableSubject = CurrentValueSubject<InternetState?, Never>(nil)

private(set) var _isInternetAvailable: Bool = true {
didSet {
Task { @MainActor in
internetReachableSubject.send(_isInternetAvailable ? .reachable : .notReachable)
}
}
}

public var isMobileData: Bool {
if let networkManager {
return networkManager.isReachableOnCellular
} else {
return false

public var isInternetAvaliable: Bool {
if let last = lastVerificationDate,
Date().timeIntervalSince1970 - last < cacheValidity {
return lastVerificationResult
}

Task {
await performVerification()
}

return lastVerificationResult
}

public let internetReachableSubject = CurrentValueSubject<InternetState?, Never>(nil)

public init() {
checkInternet()

public var isMobileData: Bool {
networkManager?.isReachableOnCellular == true
}

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)
}

public init(
config: ConfigProtocol,
timeout: TimeInterval = 15
) {
self.verificationURL = config.baseURL
self.verificationTimeout = timeout

networkManager?.startListening(onQueue: .global()) { [weak self] status in
guard let self = self else { return }
Task { @MainActor in
switch status {
case .reachable:
await self.performVerification()
case .notReachable, .unknown:
self.updateAvailability(false, at: 0)
}
}
} else {
DispatchQueue.main.async {
self.internetReachableSubject.send(InternetState.notReachable)
}
}

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) {
return true
}
} catch {
return false
}
return false
}
}
3 changes: 2 additions & 1 deletion Core/Core/View/Base/OfflineSnackBarView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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: {})
}
}
3 changes: 2 additions & 1 deletion Core/Core/View/Base/WebBrowser.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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))
}
}
26 changes: 26 additions & 0 deletions Course/Course/Data/Model/Data_CourseProgressResponse.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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?,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -387,7 +387,7 @@ public struct CourseContainerView: View {
router: CourseRouterMock(),
analytics: CourseAnalyticsMock(),
config: ConfigMock(),
connectivity: Connectivity(),
connectivity: Connectivity(config: ConfigMock()),
manager: DownloadManagerMock(),
storage: CourseStorageMock(),
isActive: true,
Expand All @@ -403,7 +403,7 @@ public struct CourseContainerView: View {
interactor: CourseInteractor.mock,
router: CourseRouterMock(),
cssInjector: CSSInjectorMock(),
connectivity: Connectivity(),
connectivity: Connectivity(config: ConfigMock()),
config: ConfigMock(),
courseID: "1",
courseName: "a",
Expand All @@ -414,7 +414,7 @@ public struct CourseContainerView: View {
interactor: CourseInteractor.mock,
router: CourseRouterMock(),
analytics: CourseAnalyticsMock(),
connectivity: Connectivity(),
connectivity: Connectivity(config: ConfigMock()),
),
courseID: "",
title: "Title of Course",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@ public final class CourseContainerViewModel: BaseCourseViewModel {

private let interactor: CourseInteractorProtocol
private let authInteractor: AuthInteractorProtocol

let analytics: CourseAnalytics
let coreAnalytics: CoreAnalytics
private(set) var storage: CourseStorage
Expand Down Expand Up @@ -214,7 +215,7 @@ public final class CourseContainerViewModel: BaseCourseViewModel {
courseVideoStructure: nil
)
}

@MainActor
func getCourseStructure(courseID: String) async throws -> CourseStructure? {
if isInternetAvaliable {
Expand Down Expand Up @@ -1766,7 +1767,7 @@ extension CourseContainerViewModel {
router: CourseRouterMock(),
analytics: CourseAnalyticsMock(),
config: ConfigMock(),
connectivity: Connectivity(),
connectivity: Connectivity(config: ConfigMock()),
manager: DownloadManagerMock(),
storage: CourseStorageMock(),
isActive: true,
Expand Down
33 changes: 32 additions & 1 deletion Course/Course/Presentation/Content/CourseContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -97,6 +98,8 @@ public struct CourseContentView: View {
)
.padding(.horizontal, 24)
.padding(.top, 16)

certificateView

// MARK: - Content based on selected tab
contentForSelectedTab(proxy: proxy)
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -266,7 +297,7 @@ public struct CourseContentView: View {
router: CourseRouterMock(),
analytics: CourseAnalyticsMock(),
config: ConfigMock(),
connectivity: Connectivity(),
connectivity: Connectivity(config: ConfigMock()),
manager: DownloadManagerMock(),
storage: CourseStorageMock(),
isActive: true,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion Course/Course/Presentation/Dates/CourseDatesView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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: "",
Expand Down
2 changes: 1 addition & 1 deletion Course/Course/Presentation/Handouts/HandoutsView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
Loading
Loading