diff --git a/Core/Core.xcodeproj/project.pbxproj b/Core/Core.xcodeproj/project.pbxproj index e055bfc89..f93630d95 100644 --- a/Core/Core.xcodeproj/project.pbxproj +++ b/Core/Core.xcodeproj/project.pbxproj @@ -145,6 +145,11 @@ E055A53A2B18DC95008D9E5E /* Theme.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = E055A5382B18DC95008D9E5E /* Theme.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; E09179FD2B0F204E002AB695 /* ConfigTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E09179FC2B0F204D002AB695 /* ConfigTests.swift */; }; E0D586362B314CD3009B4BA7 /* LogistrationBottomView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E0D586352B314CD3009B4BA7 /* LogistrationBottomView.swift */; }; + E0D5861A2B2FF74C009B4BA7 /* DiscoveryConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = E0D586192B2FF74C009B4BA7 /* DiscoveryConfig.swift */; }; + E0D5861C2B2FF85B009B4BA7 /* RawStringExtactable.swift in Sources */ = {isa = PBXBuildFile; fileRef = E0D5861B2B2FF85B009B4BA7 /* RawStringExtactable.swift */; }; + E0D586362B314CD3009B4BA7 /* LogistrationBottomView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E0D586352B314CD3009B4BA7 /* LogistrationBottomView.swift */; }; + E0D5861A2B2FF74C009B4BA7 /* DiscoveryConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = E0D586192B2FF74C009B4BA7 /* DiscoveryConfig.swift */; }; + E0D5861C2B2FF85B009B4BA7 /* RawStringExtactable.swift in Sources */ = {isa = PBXBuildFile; fileRef = E0D5861B2B2FF85B009B4BA7 /* RawStringExtactable.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -316,6 +321,11 @@ E055A5382B18DC95008D9E5E /* Theme.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Theme.framework; sourceTree = BUILT_PRODUCTS_DIR; }; E09179FC2B0F204D002AB695 /* ConfigTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConfigTests.swift; sourceTree = ""; }; E0D586352B314CD3009B4BA7 /* LogistrationBottomView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LogistrationBottomView.swift; sourceTree = ""; }; + E0D586192B2FF74C009B4BA7 /* DiscoveryConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiscoveryConfig.swift; sourceTree = ""; }; + E0D5861B2B2FF85B009B4BA7 /* RawStringExtactable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RawStringExtactable.swift; sourceTree = ""; }; + E0D586352B314CD3009B4BA7 /* LogistrationBottomView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LogistrationBottomView.swift; sourceTree = ""; }; + E0D586192B2FF74C009B4BA7 /* DiscoveryConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiscoveryConfig.swift; sourceTree = ""; }; + E0D5861B2B2FF85B009B4BA7 /* RawStringExtactable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RawStringExtactable.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -430,6 +440,7 @@ BA8B3A2E2AD546A700D25EF5 /* DebugLog.swift */, BADB3F5A2AD6EC56004D5CFA /* ResultExtension.swift */, 02D400602B0678190029D168 /* SKStoreReviewControllerExtension.swift */, + E0D5861B2B2FF85B009B4BA7 /* RawStringExtactable.swift */, ); path = Extensions; sourceTree = ""; @@ -710,6 +721,7 @@ BAFB998F2B14B377007D09F9 /* GoogleConfig.swift */, BAFB99912B14E23D007D09F9 /* AppleSignInConfig.swift */, A53A32342B233DEC005FE38A /* ThemeConfig.swift */, + E0D586192B2FF74C009B4BA7 /* DiscoveryConfig.swift */, ); path = Config; sourceTree = ""; @@ -996,6 +1008,7 @@ BA30427F2B20B320009B64B7 /* SocialAuthError.swift in Sources */, 0284DBFE28D48C5300830893 /* CourseItem.swift in Sources */, 0248C92329C075EF00DC8402 /* CourseBlockModel.swift in Sources */, + E0D5861A2B2FF74C009B4BA7 /* DiscoveryConfig.swift in Sources */, DBF6F2412B014ADA0098414B /* FirebaseConfig.swift in Sources */, 072787B628D37A0E002E9142 /* Validator.swift in Sources */, 0236961D28F9A2D200EEF206 /* Data_AuthResponse.swift in Sources */, @@ -1003,6 +1016,8 @@ 0233D5712AF13EC800BAC8BD /* SelectMailClientView.swift in Sources */, BAFB99842B0E282E007D09F9 /* MicrosoftConfig.swift in Sources */, 02B2B594295C5C7A00914876 /* Thread.swift in Sources */, + 027DB33528D8C8FE002B6862 /* Data_MyCourse.swift in Sources */, + E0D5861C2B2FF85B009B4BA7 /* RawStringExtactable.swift in Sources */, 027BD3BD2909478B00392132 /* UIView+EnclosingScrollView.swift in Sources */, BA8FA6682AD59A5700EA029A /* SocialAuthButton.swift in Sources */, 02D400612B0678190029D168 /* SKStoreReviewControllerExtension.swift in Sources */, diff --git a/Core/Core/Configuration/Config/Config.swift b/Core/Core/Configuration/Config/Config.swift index 7cd10c224..7fcbe1c94 100644 --- a/Core/Core/Configuration/Config/Config.swift +++ b/Core/Core/Configuration/Config/Config.swift @@ -24,6 +24,7 @@ public protocol ConfigProtocol { var features: FeaturesConfig { get } var theme: ThemeConfig { get } var uiComponents: UIComponentsConfig { get } + var discovery: DiscoveryConfig { get } } public enum TokenType: String { diff --git a/Core/Core/Configuration/Config/DiscoveryConfig.swift b/Core/Core/Configuration/Config/DiscoveryConfig.swift new file mode 100644 index 000000000..592ba09ee --- /dev/null +++ b/Core/Core/Configuration/Config/DiscoveryConfig.swift @@ -0,0 +1,57 @@ +// +// DiscoveryConfig.swift +// Core +// +// Created by SaeedBashir on 12/18/23. +// + +import Foundation + +public enum DiscoveryConfigType: String { + case native + case webview + case none +} + +private enum DiscoveryKeys: String, RawStringExtractable { + case discoveryType = "TYPE" + case webview = "WEBVIEW" + case baseURL = "BASE_URL" + case courseDetailTemplate = "COURSE_DETAIL_TEMPLATE" + case programDetailTemplate = "PROGRAM_DETAIL_TEMPLATE" +} + +public class DiscoveryWebviewConfig: NSObject { + public let baseURL: String? + public let courseDetailTemplate: String? + public let programDetailTemplate: String? + + init(dictionary: [String: AnyObject]) { + baseURL = dictionary[DiscoveryKeys.baseURL] as? String + courseDetailTemplate = dictionary[DiscoveryKeys.courseDetailTemplate] as? String + programDetailTemplate = dictionary[DiscoveryKeys.programDetailTemplate] as? String + } +} + +public class DiscoveryConfig: NSObject { + public let type: DiscoveryConfigType + public let webview: DiscoveryWebviewConfig + + init(dictionary: [String: AnyObject]) { + type = (dictionary[DiscoveryKeys.discoveryType] as? String).flatMap { + DiscoveryConfigType(rawValue: $0) + } ?? .none + webview = DiscoveryWebviewConfig(dictionary: dictionary[DiscoveryKeys.webview] as? [String: AnyObject] ?? [:]) + } + + var isEnabled: Bool { + return type != .none + } +} + +private let key = "DISCOVERY" +extension Config { + public var discovery: DiscoveryConfig { + DiscoveryConfig(dictionary: self[key] as? [String: AnyObject] ?? [:]) + } +} diff --git a/Core/Core/Extensions/RawStringExtactable.swift b/Core/Core/Extensions/RawStringExtactable.swift new file mode 100644 index 000000000..8ab42f2ed --- /dev/null +++ b/Core/Core/Extensions/RawStringExtactable.swift @@ -0,0 +1,27 @@ +// +// RawStringExtactable.swift +// Core +// +// Created by SaeedBashir on 12/18/23. +// + +import Foundation + +public protocol RawStringExtractable { + var rawValue: String { get } +} + +public protocol DictionaryExtractionExtension { + associatedtype Key + associatedtype Value + subscript(key: Key) -> Value? { get } +} + +extension Dictionary: DictionaryExtractionExtension {} + +public extension DictionaryExtractionExtension where Self.Key == String { + + subscript(key :RawStringExtractable) -> Value? { + return self[key.rawValue] + } +} diff --git a/Core/Core/SwiftGen/Strings.swift b/Core/Core/SwiftGen/Strings.swift index 5fb863c94..71a71130f 100644 --- a/Core/Core/SwiftGen/Strings.swift +++ b/Core/Core/SwiftGen/Strings.swift @@ -193,6 +193,8 @@ public enum CoreLocalization { public enum Alert { /// Cancel public static let cancel = CoreLocalization.tr("Localizable", "WEBVIEW.ALERT.CANCEL", fallback: "Cancel") + /// Continue + public static let `continue` = CoreLocalization.tr("Localizable", "WEBVIEW.ALERT.CONTINUE", fallback: "Continue") /// Ok public static let ok = CoreLocalization.tr("Localizable", "WEBVIEW.ALERT.OK", fallback: "Ok") } diff --git a/Core/Core/View/Base/AlertView.swift b/Core/Core/View/Base/AlertView.swift index a9e3589d6..a578eefc7 100644 --- a/Core/Core/View/Base/AlertView.swift +++ b/Core/Core/View/Base/AlertView.swift @@ -17,7 +17,7 @@ public enum AlertViewType: Equatable { var contentPadding: CGFloat { switch self { case .`default`: - return 5 + return 16 case .action, .logOut, .leaveProfile: return 36 } @@ -113,6 +113,7 @@ public struct AlertView: View { Text(alertTitle) .font(Theme.Fonts.titleLarge) .padding(.horizontal, 40) + .padding(.top, 10) Text(alertMessage) .font(Theme.Fonts.bodyMedium) .multilineTextAlignment(.center) @@ -151,11 +152,15 @@ public struct AlertView: View { HStack { switch type { case let .`default`(positiveAction): - StyledButton(positiveAction, action: { okTapped() }) - .frame(maxWidth: 135) - StyledButton(CoreLocalization.Alert.cancel, action: { onCloseTapped() }) - .frame(maxWidth: 135) - .saturation(0) + HStack { + StyledButton(positiveAction, action: { okTapped() }) + .frame(maxWidth: 135) + StyledButton(CoreLocalization.Alert.cancel, action: { onCloseTapped() }) + .frame(maxWidth: 135) + .saturation(0) + } + .padding(.leading, 10) + .padding(.trailing, 10) case let .action(action, _): if !isHorizontal { VStack(spacing: 20) { @@ -274,7 +279,7 @@ public struct AlertView: View { }.padding(.trailing, isHorizontal ? 20 : 0) } } - .padding(.top, 5) + .padding(.top, 16) .padding(.bottom, isHorizontal ? 16 : type.contentPadding) } Button(action: { diff --git a/Core/Core/View/Base/LogistrationBottomView.swift b/Core/Core/View/Base/LogistrationBottomView.swift index 0bd1f27ef..f3412814e 100644 --- a/Core/Core/View/Base/LogistrationBottomView.swift +++ b/Core/Core/View/Base/LogistrationBottomView.swift @@ -14,6 +14,7 @@ public enum LogistrationSourceScreen: Equatable { case startup case discovery case courseDetail(String, String) + case programDetails(String) } public enum LogistrationAction { diff --git a/Core/Core/View/Base/WebView.swift b/Core/Core/View/Base/WebView.swift index 917fa9c7b..e0ff08e46 100644 --- a/Core/Core/View/Base/WebView.swift +++ b/Core/Core/View/Base/WebView.swift @@ -10,6 +10,12 @@ import WebKit import SwiftUI import Theme +public let WebviewReloadNotification = "webviewReloadNotification" + +public protocol WebViewNavigationDelegate: AnyObject { + func webView(_ webView: WKWebView, shouldLoad request: URLRequest, navigationAction: WKNavigationAction) -> Bool +} + public struct WebView: UIViewRepresentable { public class ViewModel: ObservableObject { @@ -25,12 +31,20 @@ public struct WebView: UIViewRepresentable { @ObservedObject var viewModel: ViewModel @Binding public var isLoading: Bool + weak var webViewNavDelegate: WebViewNavigationDelegate? + var refreshCookies: () async -> Void - public init(viewModel: ViewModel, isLoading: Binding, refreshCookies: @escaping () async -> Void) { + public init( + viewModel: ViewModel, + isLoading: Binding, + refreshCookies: @escaping () async -> Void, + navigationDelegate: WebViewNavigationDelegate? = nil + ) { self.viewModel = viewModel self._isLoading = isLoading self.refreshCookies = refreshCookies + self.webViewNavDelegate = navigationDelegate } public class Coordinator: NSObject, WKNavigationDelegate, WKUIDelegate { @@ -38,6 +52,9 @@ public struct WebView: UIViewRepresentable { init(_ parent: WebView) { self.parent = parent + super.init() + + addObserver() } public func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) { @@ -79,6 +96,16 @@ public struct WebView: UIViewRepresentable { guard let url = navigationAction.request.url else { return .cancel } + let isWebViewDelegateHandled = await ( + parent.webViewNavDelegate?.webView( + webView, + shouldLoad: navigationAction.request, navigationAction: navigationAction) ?? false + ) + + if isWebViewDelegateHandled { + return .cancel + } + let baseURL = await parent.viewModel.baseURL if !baseURL.isEmpty, !url.absoluteString.starts(with: baseURL) { if navigationAction.navigationType == .other { @@ -117,6 +144,26 @@ public struct WebView: UIViewRepresentable { } return .allow } + + private func addObserver() { + NotificationCenter.default.addObserver( + self, + selector: #selector(reload), + name: Notification.Name(WebviewReloadNotification), + object: nil + ) + } + + fileprivate var webview: WKWebView? + + @objc private func reload() { + parent.isLoading = true + webview?.reload() + } + + deinit { + NotificationCenter.default.removeObserver(self) + } } public func makeCoordinator() -> Coordinator { @@ -130,6 +177,8 @@ public struct WebView: UIViewRepresentable { webView.navigationDelegate = context.coordinator webView.uiDelegate = context.coordinator + context.coordinator.webview = webView + webView.scrollView.bounces = false webView.scrollView.alwaysBounceHorizontal = false webView.scrollView.showsHorizontalScrollIndicator = false @@ -139,8 +188,8 @@ public struct WebView: UIViewRepresentable { webView.backgroundColor = .clear webView.scrollView.backgroundColor = Theme.Colors.white.uiColor() webView.scrollView.alwaysBounceVertical = false - webView.scrollView.layer.cornerRadius = 24 - webView.scrollView.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner] +// webView.scrollView.layer.cornerRadius = 24 +// webView.scrollView.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner] webView.scrollView.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: 200, right: 0) return webView diff --git a/Core/Core/en.lproj/Localizable.strings b/Core/Core/en.lproj/Localizable.strings index 01ef3f850..673144473 100644 --- a/Core/Core/en.lproj/Localizable.strings +++ b/Core/Core/en.lproj/Localizable.strings @@ -70,6 +70,7 @@ "WEBVIEW.ALERT.OK" = "Ok"; "WEBVIEW.ALERT.CANCEL" = "Cancel"; +"WEBVIEW.ALERT.CONTINUE" = "Continue"; "REVIEW.VOTE_TITLE" = "Enjoying Open edX?"; "REVIEW.VOTE_DESCRIPTION" = "Your feedback matters to us. Would you take a moment to rate the app by tapping a star below? Thanks for your support!"; diff --git a/Core/Core/uk.lproj/Localizable.strings b/Core/Core/uk.lproj/Localizable.strings index d2128641b..ab592e519 100644 --- a/Core/Core/uk.lproj/Localizable.strings +++ b/Core/Core/uk.lproj/Localizable.strings @@ -69,6 +69,7 @@ "WEBVIEW.ALERT.OK" = "Так"; "WEBVIEW.ALERT.CANCEL" = "Скасувати"; +"WEBVIEW.ALERT.CONTINUE" = "Continue"; "REVIEW.VOTE_TITLE" = "Вам подобається Open edX?"; diff --git a/Course/Course/Presentation/Details/CourseDetailsView.swift b/Course/Course/Presentation/Details/CourseDetailsView.swift index 4be478f8e..d6aa06db8 100644 --- a/Course/Course/Presentation/Details/CourseDetailsView.swift +++ b/Course/Course/Presentation/Details/CourseDetailsView.swift @@ -221,7 +221,11 @@ private struct CourseStateView: View { case .enrollOpen: StyledButton(CourseLocalization.Details.enrollNow, action: { if !viewModel.userloggedIn { - viewModel.router.showRegisterScreen(sourceScreen: .courseDetail(courseDetails.courseID, courseDetails.courseTitle)) + viewModel.router.showRegisterScreen( + sourceScreen: .courseDetail( + courseDetails.courseID, + courseDetails.courseTitle) + ) } else { Task { await viewModel.enrollToCourse(id: courseDetails.courseID) @@ -238,7 +242,11 @@ private struct CourseStateView: View { case .alreadyEnrolled: StyledButton(CourseLocalization.Details.viewCourse, action: { if !viewModel.userloggedIn { - viewModel.router.showRegisterScreen(sourceScreen: .courseDetail(courseDetails.courseID, courseDetails.courseTitle)) + viewModel.router.showRegisterScreen( + sourceScreen: .courseDetail( + courseDetails.courseID, + courseDetails.courseTitle) + ) } else { viewModel.viewCourseClicked( courseId: courseDetails.courseID, diff --git a/Discovery/Discovery.xcodeproj/project.pbxproj b/Discovery/Discovery.xcodeproj/project.pbxproj index 6b3a59362..19cac5ced 100644 --- a/Discovery/Discovery.xcodeproj/project.pbxproj +++ b/Discovery/Discovery.xcodeproj/project.pbxproj @@ -32,6 +32,11 @@ CFC849452996A52A0055E497 /* SearchViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = CFC849442996A52A0055E497 /* SearchViewModel.swift */; }; CFC8494C299A66080055E497 /* Localizable.stringsdict in Resources */ = {isa = PBXBuildFile; fileRef = CFC8494E299A66080055E497 /* Localizable.stringsdict */; }; CFC84950299BE52C0055E497 /* SearchViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CFC8494F299BE52C0055E497 /* SearchViewModelTests.swift */; }; + E08D12E32B482D720096311A /* Course.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E08D12E22B482D720096311A /* Course.framework */; }; + E0D586202B300095009B4BA7 /* DiscoverWebviewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E0D5861E2B300095009B4BA7 /* DiscoverWebviewModel.swift */; }; + E0D586212B300095009B4BA7 /* DiscoveryWebview.swift in Sources */ = {isa = PBXBuildFile; fileRef = E0D5861F2B300095009B4BA7 /* DiscoveryWebview.swift */; }; + E0D586232B3000AD009B4BA7 /* DiscoveryURIDetails.swift in Sources */ = {isa = PBXBuildFile; fileRef = E0D586222B3000AD009B4BA7 /* DiscoveryURIDetails.swift */; }; + E0D586252B300134009B4BA7 /* URL+PathExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = E0D586242B300134009B4BA7 /* URL+PathExtension.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -86,7 +91,13 @@ CFC849442996A52A0055E497 /* SearchViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchViewModel.swift; sourceTree = ""; }; CFC8494D299A66080055E497 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = en; path = en.lproj/Localizable.stringsdict; sourceTree = ""; }; CFC8494F299BE52C0055E497 /* SearchViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchViewModelTests.swift; sourceTree = ""; }; + E08D12E22B482D720096311A /* Course.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Course.framework; sourceTree = BUILT_PRODUCTS_DIR; }; E0D586132B29F25A009B4BA7 /* Authorization.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Authorization.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + E0D5861E2B300095009B4BA7 /* DiscoverWebviewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DiscoverWebviewModel.swift; sourceTree = ""; }; + E0D5861F2B300095009B4BA7 /* DiscoveryWebview.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DiscoveryWebview.swift; sourceTree = ""; }; + E0D586222B3000AD009B4BA7 /* DiscoveryURIDetails.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiscoveryURIDetails.swift; sourceTree = ""; }; + E0D586242B300134009B4BA7 /* URL+PathExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "URL+PathExtension.swift"; sourceTree = ""; }; + E0D586282B302C3A009B4BA7 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; E192F9B4A7EECED9665AB8A7 /* Pods-App-Discovery.releasedev.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-Discovery.releasedev.xcconfig"; path = "Target Support Files/Pods-App-Discovery/Pods-App-Discovery.releasedev.xcconfig"; sourceTree = ""; }; F340BD73D38B0DF9E4EA6482 /* Pods-App-Discovery-DiscoveryUnitTests.releaseprod.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-Discovery-DiscoveryUnitTests.releaseprod.xcconfig"; path = "Target Support Files/Pods-App-Discovery-DiscoveryUnitTests/Pods-App-Discovery-DiscoveryUnitTests.releaseprod.xcconfig"; sourceTree = ""; }; FF565519B9BBC73E92249648 /* Pods-App-Discovery-DiscoveryUnitTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-Discovery-DiscoveryUnitTests.release.xcconfig"; path = "Target Support Files/Pods-App-Discovery-DiscoveryUnitTests/Pods-App-Discovery-DiscoveryUnitTests.release.xcconfig"; sourceTree = ""; }; @@ -106,6 +117,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + E08D12E32B482D720096311A /* Course.framework in Frameworks */, 072787AD28D34D15002E9142 /* Core.framework in Frameworks */, 9F47BCC672941A9854404EC7 /* Pods_App_Discovery.framework in Frameworks */, ); @@ -179,6 +191,7 @@ 070019A228F6EF2700D5FC78 /* Presentation */ = { isa = PBXGroup; children = ( + E0D5861D2B300095009B4BA7 /* WebDiscovery */, 029242E52AE6976E00A940EC /* UpdateViews */, 072787B328D34D91002E9142 /* DiscoveryView.swift */, 0283347828D49A8700C828FC /* DiscoveryViewModel.swift */, @@ -214,6 +227,7 @@ 0727879B28D34C03002E9142 /* Discovery */ = { isa = PBXGroup; children = ( + E0D586282B302C3A009B4BA7 /* Info.plist */, 02EF39CB28D866C50058F6BD /* SwiftGen */, 0284DBF828D4831000830893 /* Data */, 0284DC0428D4996F00830893 /* Domain */, @@ -227,6 +241,7 @@ 072787AB28D34D15002E9142 /* Frameworks */ = { isa = PBXGroup; children = ( + E08D12E22B482D720096311A /* Course.framework */, E0D586132B29F25A009B4BA7 /* Authorization.framework */, 072787AC28D34D15002E9142 /* Core.framework */, 919E55130969D91EF03C4C0B /* Pods_App_Discovery.framework */, @@ -268,6 +283,17 @@ path = ../Pods; sourceTree = ""; }; + E0D5861D2B300095009B4BA7 /* WebDiscovery */ = { + isa = PBXGroup; + children = ( + E0D5861E2B300095009B4BA7 /* DiscoverWebviewModel.swift */, + E0D5861F2B300095009B4BA7 /* DiscoveryWebview.swift */, + E0D586222B3000AD009B4BA7 /* DiscoveryURIDetails.swift */, + E0D586242B300134009B4BA7 /* URL+PathExtension.swift */, + ); + path = WebDiscovery; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXHeadersBuildPhase section */ @@ -483,15 +509,19 @@ 0284DBFC28D4856A00830893 /* DiscoveryEndpoint.swift in Sources */, 029737402949FB070051696B /* DiscoveryCoreModel.xcdatamodeld in Sources */, 029242E72AE6978400A940EC /* UpdateRequiredView.swift in Sources */, + E0D586232B3000AD009B4BA7 /* DiscoveryURIDetails.swift in Sources */, 0283347728D499BC00C828FC /* DiscoveryInteractor.swift in Sources */, 02F3BFDF29252F2F0051930C /* DiscoveryRouter.swift in Sources */, 0283347928D49A8700C828FC /* DiscoveryViewModel.swift in Sources */, 072787B428D34D91002E9142 /* DiscoveryView.swift in Sources */, + E0D586252B300134009B4BA7 /* URL+PathExtension.swift in Sources */, 029737422949FB3B0051696B /* DiscoveryPersistenceProtocol.swift in Sources */, 0284DC0328D4922900830893 /* DiscoveryRepository.swift in Sources */, 029242EB2AE6AB7B00A940EC /* UpdateNotificationView.swift in Sources */, 02EF39DC28D86BEF0058F6BD /* Strings.swift in Sources */, + E0D586202B300095009B4BA7 /* DiscoverWebviewModel.swift in Sources */, 02F1752F2A4DA3B60019CD70 /* DiscoveryAnalytics.swift in Sources */, + E0D586212B300095009B4BA7 /* DiscoveryWebview.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -731,6 +761,7 @@ DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = Discovery/Info.plist; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; IPHONEOS_DEPLOYMENT_TARGET = 15.0; @@ -845,6 +876,7 @@ DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = Discovery/Info.plist; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; IPHONEOS_DEPLOYMENT_TARGET = 15.0; @@ -1022,6 +1054,7 @@ DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = Discovery/Info.plist; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; IPHONEOS_DEPLOYMENT_TARGET = 15.0; @@ -1057,6 +1090,7 @@ DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = Discovery/Info.plist; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; IPHONEOS_DEPLOYMENT_TARGET = 15.0; @@ -1155,6 +1189,7 @@ DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = Discovery/Info.plist; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; IPHONEOS_DEPLOYMENT_TARGET = 15.0; @@ -1254,6 +1289,7 @@ DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = Discovery/Info.plist; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; IPHONEOS_DEPLOYMENT_TARGET = 15.0; @@ -1347,6 +1383,7 @@ DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = Discovery/Info.plist; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; IPHONEOS_DEPLOYMENT_TARGET = 15.0; @@ -1439,6 +1476,7 @@ DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = Discovery/Info.plist; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; IPHONEOS_DEPLOYMENT_TARGET = 15.0; diff --git a/Discovery/Discovery/Info.plist b/Discovery/Discovery/Info.plist new file mode 100644 index 000000000..f72a0f657 --- /dev/null +++ b/Discovery/Discovery/Info.plist @@ -0,0 +1,12 @@ + + + + + + diff --git a/Discovery/Discovery/Presentation/DiscoveryRouter.swift b/Discovery/Discovery/Presentation/DiscoveryRouter.swift index 32b13a6e4..cea10bb50 100644 --- a/Discovery/Discovery/Presentation/DiscoveryRouter.swift +++ b/Discovery/Discovery/Presentation/DiscoveryRouter.swift @@ -9,8 +9,8 @@ import Foundation import Core public protocol DiscoveryRouter: BaseRouter { - func showCourseDetais(courseID: String, title: String) + func showWebDiscoveryDetails(pathID: String, discoveryType: DiscoveryWebviewType, sourceScreen: LogistrationSourceScreen) func showUpdateRequiredView(showAccountLink: Bool) func showUpdateRecomendedView() func showDiscoverySearch(searchQuery: String?) @@ -23,6 +23,7 @@ public class DiscoveryRouterMock: BaseRouterMock, DiscoveryRouter { public override init() {} public func showCourseDetais(courseID: String, title: String) {} + public func showWebDiscoveryDetails(pathID: String, discoveryType: DiscoveryWebviewType, sourceScreen: LogistrationSourceScreen) {} public func showUpdateRequiredView(showAccountLink: Bool) {} public func showUpdateRecomendedView() {} public func showDiscoverySearch(searchQuery: String? = nil) {} diff --git a/Discovery/Discovery/Presentation/WebDiscovery/DiscoverWebviewModel.swift b/Discovery/Discovery/Presentation/WebDiscovery/DiscoverWebviewModel.swift new file mode 100644 index 000000000..8267ead70 --- /dev/null +++ b/Discovery/Discovery/Presentation/WebDiscovery/DiscoverWebviewModel.swift @@ -0,0 +1,236 @@ +// +// DiscoverWebviewModel.swift +// Discovery +// +// Created by SaeedBashir on 12/16/23. +// + +import Foundation +import Core +import WebKit +import SwiftUI +import Course +import Swinject + +public class DiscoveryWebviewModel: ObservableObject { + @Published var courseDetails: CourseDetails? + @Published private(set) var showProgress = false + @Published var showError: Bool = false + var errorMessage: String? { + didSet { + withAnimation { + showError = errorMessage != nil + } + } + } + + let router: DiscoveryRouter + let config: ConfigProtocol + let connectivity: ConnectivityProtocol + private let interactor: CourseInteractorProtocol + private let analytics: DiscoveryAnalytics + var request: URLRequest? + private let storage: CoreStorage + var sourceScreen: LogistrationSourceScreen + + var userloggedIn: Bool { + return !(storage.user?.username?.isEmpty ?? true) + } + + public init( + router: DiscoveryRouter, + config: ConfigProtocol, + interactor: CourseInteractorProtocol, + connectivity: ConnectivityProtocol, + analytics: DiscoveryAnalytics, + storage: CoreStorage, + sourceScreen: LogistrationSourceScreen = .default + ) { + self.router = router + self.config = config + self.interactor = interactor + self.connectivity = connectivity + self.analytics = analytics + self.storage = storage + self.sourceScreen = sourceScreen + } + + @MainActor + func getCourseDetail(courseID: String) async throws -> CourseDetails? { + return try await interactor.getCourseDetails(courseID: courseID) + } + + @MainActor + func enrollTo(courseID: String) async { + do { + guard userloggedIn else { + router.showRegisterScreen(sourceScreen: .discovery) + return + } + + showProgress = true + courseDetails = try await getCourseDetail(courseID: courseID) + + if courseDetails?.isEnrolled ?? false || courseState == .alreadyEnrolled { + showProgress = false + showCourseDetails() + return + } + + let courseAnalytics = Container.shared.resolve(CourseAnalytics.self) + + courseAnalytics?.courseEnrollClicked(courseId: courseID, courseName: courseDetails?.courseTitle ?? "") + _ = try await interactor.enrollToCourse(courseID: courseID) + courseAnalytics?.courseEnrollSuccess(courseId: courseID, courseName: courseDetails?.courseTitle ?? "") + courseDetails?.isEnrolled = true + showProgress = false + NotificationCenter.default.post(name: .onCourseEnrolled, object: courseID) + showCourseDetails() + } catch let error { + showProgress = false + if error.isInternetError || error is NoCachedDataError { + errorMessage = CoreLocalization.Error.slowOrNoInternetConnection + } else { + errorMessage = CoreLocalization.Error.unknownError + } + } + } + + private var courseState: CourseState? { + guard courseDetails?.isEnrolled == false else { return nil } + + if let enrollmentStart = courseDetails?.enrollmentStart, let enrollmentEnd = courseDetails?.enrollmentEnd { + let enrollmentsRange = DateInterval(start: enrollmentStart, end: enrollmentEnd) + if enrollmentsRange.contains(Date()) { + return .enrollOpen + } else { + return .enrollClose + } + } else { + return .enrollOpen + } + } +} + +extension DiscoveryWebviewModel: WebViewNavigationDelegate { + public func webView( + _ webView: WKWebView, + shouldLoad request: URLRequest, + navigationAction: WKNavigationAction + ) -> Bool { + guard let URL = request.url else { return false } + + if let urlAction = urlAction(from: URL), + handleNavigation(url: URL, urlAction: urlAction) { + return true + } + + let capturedLink = navigationAction.navigationType == .linkActivated + let outsideLink = (request.mainDocumentURL?.host != self.request?.url?.host) + var externalLink = false + + if let queryParameters = request.url?.queryParameters, + let externalLinkValue = queryParameters["external_link"] as? String, + externalLinkValue.caseInsensitiveCompare("true") == .orderedSame { + externalLink = true + } + + if let url = request.url, outsideLink || capturedLink || externalLink, UIApplication.shared.canOpenURL(url) { + DispatchQueue.main.async { [weak self] in + self?.router.presentAlert( + alertTitle: DiscoveryLocalization.Alert.leavingAppTitle, + alertMessage: DiscoveryLocalization.Alert.leavingAppMessage, + positiveAction: CoreLocalization.Webview.Alert.continue, + onCloseTapped: { + self?.router.dismiss(animated: true) + }, okTapped: { + UIApplication.shared.open(url, options: [:]) + }, type: .default(positiveAction: CoreLocalization.Webview.Alert.continue) + ) + } + return true + } + + return false + } + + private func urlAction(from url: URL) -> WebviewActions? { + guard url.isValidAppURLScheme, + let url = WebviewActions(rawValue: url.appURLHost) else { return nil } + return url + } + + private func handleNavigation(url: URL, urlAction: WebviewActions) -> Bool { + switch urlAction { + case .courseEnrollment: + if let urlData = parse(url: url), let courseID = urlData.courseId { + Task { + await enrollTo(courseID: courseID) + } + } + case .courseDetail: + guard let pathID = detailPathID(from: url) else { return false } + router.showWebDiscoveryDetails( + pathID: pathID, + discoveryType: .courseDetail(pathID), + sourceScreen: sourceScreen + ) + case .enrolledCourseDetail: + return showCourseDetails() + + case .programDetail: + guard let pathID = programDetailPathId(from: url) else { return false } + router.showWebDiscoveryDetails( + pathID: pathID, + discoveryType: .programDetail(pathID), + sourceScreen: sourceScreen + ) + + default: + break + } + + return true + } + + private func detailPathID(from url: URL) -> String? { + guard url.isValidAppURLScheme, + let path = url.queryParameters?[URLParameterKeys.pathId] as? String, + url.appURLHost == WebviewActions.courseDetail.rawValue else { return nil } + + return path + } + + private func parse(url: URL) -> (courseId: String?, emailOptIn: Bool)? { + guard url.isValidAppURLScheme else { return nil } + + let courseId = url.queryParameters?[URLParameterKeys.courseId] as? String + let emailOptIn = (url.queryParameters?[URLParameterKeys.emailOptIn] as? String).flatMap {Bool($0)} + + return (courseId, emailOptIn ?? false) + } + + private func programDetailPathId(from url: URL) -> String? { + guard url.isValidAppURLScheme, + let path = url.queryParameters?[URLParameterKeys.pathId] as? String, + url.appURLHost == WebviewActions.programDetail.rawValue else { return nil } + + return path + } + + @discardableResult private func showCourseDetails() -> Bool { + guard let courseRouter = Container.shared.resolve(CourseRouter.self), + let courseDetails = courseDetails else { return false } + courseRouter.showCourseScreens( + courseID: courseDetails.courseID, + isActive: nil, + courseStart: courseDetails.courseStart, + courseEnd: courseDetails.courseEnd, + enrollmentStart: courseDetails.enrollmentStart, + enrollmentEnd: courseDetails.enrollmentEnd, + title: courseDetails.courseTitle + ) + + return true + } +} diff --git a/Discovery/Discovery/Presentation/WebDiscovery/DiscoveryURIDetails.swift b/Discovery/Discovery/Presentation/WebDiscovery/DiscoveryURIDetails.swift new file mode 100644 index 000000000..f4e6a6d94 --- /dev/null +++ b/Discovery/Discovery/Presentation/WebDiscovery/DiscoveryURIDetails.swift @@ -0,0 +1,31 @@ +// +// DiscoveryURIDetails.swift +// Discovery +// +// Created by SaeedBashir on 12/18/23. +// + +import Foundation +import Core + +// Define your uri scheme +public enum URIString: String { + case appURLScheme = "edxapp" + case pathPlaceHolder = "{path_id}" +} + +public enum URLParameterKeys: String, RawStringExtractable { + case pathId = "path_id" + case courseId = "course_id" + case emailOptIn = "email_opt_in" +} + +// Define your hosts +public enum WebviewActions: String { + case courseEnrollment = "enroll" + case courseDetail = "course_info" + case enrolledCourseDetail = "enrolled_course_info" + case enrolledProgramDetail = "enrolled_program_info" + case programDetail = "program_info" + case courseProgram = "course" +} diff --git a/Discovery/Discovery/Presentation/WebDiscovery/DiscoveryWebview.swift b/Discovery/Discovery/Presentation/WebDiscovery/DiscoveryWebview.swift new file mode 100644 index 000000000..c0b79bba8 --- /dev/null +++ b/Discovery/Discovery/Presentation/WebDiscovery/DiscoveryWebview.swift @@ -0,0 +1,183 @@ +// +// DiscoveryWebview.swift +// Discovery +// +// Created by SaeedBashir on 12/16/23. +// + +import Foundation +import SwiftUI +import Theme +import Core + +public enum DiscoveryWebviewType: Equatable { + case discovery + case courseDetail(String) + case programDetail(String) +} + +public struct DiscoveryWebview: View { + @State private var searchQuery: String = "" + @State private var isLoading: Bool = true + + @ObservedObject private var viewModel: DiscoveryWebviewModel + private var router: DiscoveryRouter + private var discoveryType: DiscoveryWebviewType + private var pathID: String + + private var URLString: String { + switch discoveryType { + case .discovery: + if !searchQuery.isEmpty { + let baseURL = viewModel.config.discovery.webview.baseURL ?? "" + return buildQuery(baseURL: baseURL, params: ["q": searchQuery]) + } + + return viewModel.config.discovery.webview.baseURL ?? "" + case .courseDetail: + let template = viewModel.config.discovery.webview.courseDetailTemplate + return template?.replacingOccurrences( + of: URIString.pathPlaceHolder.rawValue, + with: pathID + ) ?? "" + + case .programDetail: + let template = viewModel.config.discovery.webview.programDetailTemplate + return template?.replacingOccurrences( + of: URIString.pathPlaceHolder.rawValue, + with: pathID + ) ?? "" + } + } + + private func buildQuery(baseURL: String, params: [String: String]) -> String { + var query = baseURL + for param in params { + let join = query.contains("?") ? "&" : "?" + let value = param.key + "=" + param.value + if !query.contains(find: value) { + query = query + join + value + } + } + + return query + } + + public init( + viewModel: DiscoveryWebviewModel, + router: DiscoveryRouter, + searchQuery: String? = nil, + discoveryType: DiscoveryWebviewType = .discovery, + pathID: String = "" + ) { + self.viewModel = viewModel + self.router = router + self._searchQuery = State(initialValue: searchQuery ?? "") + self.discoveryType = discoveryType + self.pathID = pathID + + if let url = URL(string: URLString) { + viewModel.request = URLRequest(url: url) + } + } + + public var body: some View { + GeometryReader { proxy in + VStack(alignment: .center) { + WebView( + viewModel: .init( + url: URLString, + baseURL: "" + ), + isLoading: $isLoading, + refreshCookies: {}, + navigationDelegate: viewModel + ) + + if isLoading || viewModel.showProgress { + HStack(alignment: .center) { + ProgressBar( + size: 40, + lineWidth: 8 + ) + .padding(.vertical, proxy.size.height / 2) + } + .frame(width: proxy.size.width, height: proxy.size.height) + } + + // MARK: - Show Error + if viewModel.showError { + VStack { + SnackBarView(message: viewModel.errorMessage) + } + .padding(.bottom, 20) + .transition(.move(edge: .bottom)) + .onAppear { + doAfter(Theme.Timeout.snackbarMessageLongTimeout) { + viewModel.errorMessage = nil + } + } + } + + if !viewModel.userloggedIn, !isLoading { + LogistrationBottomView { buttonAction in + switch buttonAction { + case .signIn: + viewModel.router.showLoginScreen(sourceScreen: sourceScreen) + case .register: + viewModel.router.showRegisterScreen(sourceScreen: sourceScreen) + } + } + } + } + + // MARK: - Offline mode SnackBar + OfflineSnackBarView( + connectivity: viewModel.connectivity, + reloadAction: { + NotificationCenter.default.post( + name: NSNotification.Name(rawValue: WebviewReloadNotification), + object: nil + ) + }) + } + .navigationBarHidden(viewModel.sourceScreen == .default && discoveryType == .discovery) + .navigationTitle(CoreLocalization.Mainscreen.discovery) + .background(Theme.Colors.background.ignoresSafeArea()) + .onFirstAppear { + if case let .courseDetail(pathID, _) = viewModel.sourceScreen { + viewModel.router.showWebDiscoveryDetails( + pathID: pathID, + discoveryType: .courseDetail(pathID), + sourceScreen: .discovery + ) + } else if case let .programDetails(pathID) = viewModel.sourceScreen { + viewModel.router.showWebDiscoveryDetails( + pathID: pathID, + discoveryType: .programDetail(pathID), + sourceScreen: .discovery + ) + } + + // Reseting the source screen + viewModel.sourceScreen = .discovery + } + } + + private var sourceScreen: LogistrationSourceScreen { + switch discoveryType { + case .discovery: + return .discovery + case .courseDetail(let pathID): + return .courseDetail(pathID, "") + case .programDetail(let pathID): + return .programDetails(pathID) + } + } +} + +fileprivate extension String { + func contains(find: String) -> Bool { + return range(of: find) != nil + } +} diff --git a/Discovery/Discovery/Presentation/WebDiscovery/URL+PathExtension.swift b/Discovery/Discovery/Presentation/WebDiscovery/URL+PathExtension.swift new file mode 100644 index 000000000..ca89f7d77 --- /dev/null +++ b/Discovery/Discovery/Presentation/WebDiscovery/URL+PathExtension.swift @@ -0,0 +1,36 @@ +// +// URL+PathExtension.swift +// Discovery +// +// Created by SaeedBashir on 12/18/23. +// + +import Foundation + +public extension URL { + var appURLHost: String { + return host ?? "" + } + + var isValidAppURLScheme: Bool { + return scheme ?? "" == URIString.appURLScheme.rawValue ? true : false + } + + var queryParameters: [String: Any]? { + guard let queryString = query else { + return nil + } + var queryParameters = [String: Any]() + let parameters = queryString.components(separatedBy: "&") + for parameter in parameters { + let keyValuePair = parameter.components(separatedBy: "=") + // Parameter will be ignored if invalid data for keyValuePair + if keyValuePair.count == 2 { + let key = keyValuePair[0] + let value = keyValuePair[1] + queryParameters[key] = value + } + } + return queryParameters + } +} diff --git a/Discovery/Discovery/SwiftGen/Strings.swift b/Discovery/Discovery/SwiftGen/Strings.swift index c53a55352..abe90994d 100644 --- a/Discovery/Discovery/SwiftGen/Strings.swift +++ b/Discovery/Discovery/SwiftGen/Strings.swift @@ -41,6 +41,12 @@ public enum DiscoveryLocalization { public static let updateRequiredTitle = DiscoveryLocalization.tr("Localizable", "UPDATE_REQUIRED_TITLE", fallback: "App Update Required") /// Why do I need to update? public static let updateWhyNeed = DiscoveryLocalization.tr("Localizable", "UPDATE_WHY_NEED", fallback: "Why do I need to update?") + public enum Alert { + /// You are now leaving the app and opening a browser + public static let leavingAppMessage = DiscoveryLocalization.tr("Localizable", "ALERT.LEAVING_APP_MESSAGE", fallback: "You are now leaving the app and opening a browser") + /// Leaving the app + public static let leavingAppTitle = DiscoveryLocalization.tr("Localizable", "ALERT.LEAVING_APP_TITLE", fallback: "Leaving the app") + } public enum Header { /// Discover new public static let title1 = DiscoveryLocalization.tr("Localizable", "HEADER.TITLE_1", fallback: "Discover new") diff --git a/Discovery/Discovery/en.lproj/Localizable.strings b/Discovery/Discovery/en.lproj/Localizable.strings index 074eb6cbd..bedd53a22 100644 --- a/Discovery/Discovery/en.lproj/Localizable.strings +++ b/Discovery/Discovery/en.lproj/Localizable.strings @@ -26,3 +26,6 @@ "UPDATE_NEEDED_NOT_NOW" = "Not Now"; "UPDATE_NEW_AVALIABLE" = "New update available! Upgrade now to receive the latest features and fixes"; + +"ALERT.LEAVING_APP_TITLE" = "Leaving the app"; +"ALERT.LEAVING_APP_MESSAGE" = "You are now leaving the app and opening a browser"; diff --git a/Discovery/Discovery/uk.lproj/Localizable.strings b/Discovery/Discovery/uk.lproj/Localizable.strings index fd43d1635..63109fae9 100644 --- a/Discovery/Discovery/uk.lproj/Localizable.strings +++ b/Discovery/Discovery/uk.lproj/Localizable.strings @@ -26,3 +26,6 @@ "UPDATE_NEEDED_NOT_NOW" = "Не зараз"; "UPDATE_NEW_AVALIABLE" = "Доступне нове оновлення! Оновіть зараз, щоб отримати найновіші функції та виправлення"; + +"ALERT.LEAVING_APP_TITLE" = "Leaving the app"; +"ALERT.LEAVING_APP_MESSAGE" = "You are now leaving the app and opening a browser"; diff --git a/OpenEdX/DI/ScreenAssembly.swift b/OpenEdX/DI/ScreenAssembly.swift index 710764940..d3470e0ce 100644 --- a/OpenEdX/DI/ScreenAssembly.swift +++ b/OpenEdX/DI/ScreenAssembly.swift @@ -109,6 +109,18 @@ class ScreenAssembly: Assembly { ) } + container.register(DiscoveryWebviewModel.self) { r, sourceScreen in + DiscoveryWebviewModel( + router: r.resolve(DiscoveryRouter.self)!, + config: r.resolve(ConfigProtocol.self)!, + interactor: r.resolve(CourseInteractorProtocol.self)!, + connectivity: r.resolve(ConnectivityProtocol.self)!, + analytics: r.resolve(DiscoveryAnalytics.self)!, + storage: r.resolve(CoreStorage.self)!, + sourceScreen: sourceScreen + ) + } + container.register(SearchViewModel.self) { r in SearchViewModel( interactor: r.resolve(DiscoveryInteractorProtocol.self)!, diff --git a/OpenEdX/Info.plist b/OpenEdX/Info.plist index e4bdcba95..b74f3967c 100644 --- a/OpenEdX/Info.plist +++ b/OpenEdX/Info.plist @@ -20,6 +20,11 @@ fastmail protonmail + NSAppTransportSecurity + + NSAllowsArbitraryLoads + + UIAppFonts UIViewControllerBasedStatusBarAppearance diff --git a/OpenEdX/Router.swift b/OpenEdX/Router.swift index 5e69b64ad..d5d34219e 100644 --- a/OpenEdX/Router.swift +++ b/OpenEdX/Router.swift @@ -210,6 +210,26 @@ public class Router: AuthorizationRouter, navigationController.pushViewController(controller, animated: true) } + public func showWebDiscoveryDetails( + pathID: String, + discoveryType: DiscoveryWebviewType, + sourceScreen: LogistrationSourceScreen + ) { + let view = DiscoveryWebview( + viewModel: Container.shared.resolve( + DiscoveryWebviewModel.self, + argument: sourceScreen)!, + router: Container.shared.resolve(DiscoveryRouter.self)!, + discoveryType: discoveryType, + pathID: pathID + ) + + DispatchQueue.main.async { [weak self] in + let controller = UIHostingController(rootView: view) + self?.navigationController.pushViewController(controller, animated: true) + } + } + public func showDiscoverySearch(searchQuery: String? = nil) { let viewModel = Container.shared.resolve(SearchViewModel.self)! let view = SearchView(viewModel: viewModel, searchQuery: searchQuery) @@ -219,14 +239,29 @@ public class Router: AuthorizationRouter, } public func showDiscoveryScreen(searchQuery: String? = nil, sourceScreen: LogistrationSourceScreen) { - let view = DiscoveryView( - viewModel: Container.shared.resolve(DiscoveryViewModel.self)!, - router: Container.shared.resolve(DiscoveryRouter.self)!, - searchQuery: searchQuery, - sourceScreen: sourceScreen - ) - let controller = UIHostingController(rootView: view) - navigationController.pushViewController(controller, animated: true) + let config = Container.shared.resolve(ConfigProtocol.self) + if config?.discovery.type == .native { + let view = DiscoveryView( + viewModel: Container.shared.resolve(DiscoveryViewModel.self)!, + router: Container.shared.resolve(DiscoveryRouter.self)!, + searchQuery: searchQuery, + sourceScreen: sourceScreen + ) + let controller = UIHostingController(rootView: view) + navigationController.pushViewController(controller, animated: true) + } else if config?.discovery.type == .webview { + let view = DiscoveryWebview( + viewModel: Container.shared.resolve( + DiscoveryWebviewModel.self, + argument: sourceScreen + )!, + router: Container.shared.resolve(DiscoveryRouter.self)!, + searchQuery: searchQuery + ) + + let controller = UIHostingController(rootView: view) + navigationController.pushViewController(controller, animated: true) + } } public func showDiscussionsSearch(courseID: String) { diff --git a/OpenEdX/View/MainScreenView.swift b/OpenEdX/View/MainScreenView.swift index 6f26a929e..13d524081 100644 --- a/OpenEdX/View/MainScreenView.swift +++ b/OpenEdX/View/MainScreenView.swift @@ -42,11 +42,22 @@ struct MainScreenView: View { var body: some View { TabView(selection: $selection) { ZStack { - DiscoveryView( - viewModel: Container.shared.resolve(DiscoveryViewModel.self)!, - router: Container.shared.resolve(DiscoveryRouter.self)!, - sourceScreen: viewModel.sourceScreen - ) + let config = Container.shared.resolve(ConfigProtocol.self) + if config?.discovery.type == .native { + DiscoveryView( + viewModel: Container.shared.resolve(DiscoveryViewModel.self)!, + router: Container.shared.resolve(DiscoveryRouter.self)!, + sourceScreen: viewModel.sourceScreen + ) + } else if config?.discovery.type == .webview { + DiscoveryWebview( + viewModel: Container.shared.resolve( + DiscoveryWebviewModel.self, + argument: viewModel.sourceScreen)!, + router: Container.shared.resolve(DiscoveryRouter.self)! + ) + } + if updateAvaliable { UpdateNotificationView(config: viewModel.config) }