Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Enrolled Web Based Programs Implementation #260

Merged
merged 8 commits into from
Feb 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
9 changes: 8 additions & 1 deletion Core/Core/Configuration/Config/Config.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ public protocol ConfigProtocol {
var theme: ThemeConfig { get }
var uiComponents: UIComponentsConfig { get }
var discovery: DiscoveryConfig { get }
var program: DiscoveryConfig { get }
var URIScheme: String { get }
}

public enum TokenType: String {
Expand All @@ -42,6 +44,7 @@ private enum ConfigKeys: String {
case organizationCode = "ORGANIZATION_CODE"
case appstoreID = "APP_STORE_ID"
case faq = "FAQ_URL"
case URIScheme = "URI_SCHEME"
}

public class Config {
Expand All @@ -64,7 +67,7 @@ public class Config {
let dict = try? PropertyListSerialization.propertyList(
from: data,
options: [],
format: nil) as? [String: Any]
format: nil) as? [String: Any]
else { return }

properties = dict
Expand Down Expand Up @@ -149,6 +152,10 @@ extension Config: ConfigProtocol {
}
return url
}

public var URIScheme: String {
return string(for: ConfigKeys.URIScheme.rawValue) ?? ""
}
}

// Mark - For testing and SwiftUI preview
Expand Down
7 changes: 7 additions & 0 deletions Core/Core/Configuration/Config/DiscoveryConfig.swift
Original file line number Diff line number Diff line change
Expand Up @@ -55,3 +55,10 @@ extension Config {
DiscoveryConfig(dictionary: self[key] as? [String: AnyObject] ?? [:])
}
}

private let programKey = "PROGRAM"
extension Config {
public var program: DiscoveryConfig {
DiscoveryConfig(dictionary: self[programKey] as? [String: AnyObject] ?? [:])
}
}
6 changes: 5 additions & 1 deletion Core/Core/View/Base/Webview/WebView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,11 @@ import SwiftUI
import Theme

public protocol WebViewNavigationDelegate: AnyObject {
func webView(_ webView: WKWebView, shouldLoad request: URLRequest, navigationAction: WKNavigationAction) -> Bool
func webView(
_ webView: WKWebView,
shouldLoad request: URLRequest,
navigationAction: WKNavigationAction
) async -> Bool
}

public struct WebView: UIViewRepresentable {
Expand Down
16 changes: 16 additions & 0 deletions Discovery/Discovery.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@
02F1752F2A4DA3B60019CD70 /* DiscoveryAnalytics.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02F1752E2A4DA3B60019CD70 /* DiscoveryAnalytics.swift */; };
02F3BFDF29252F2F0051930C /* DiscoveryRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02F3BFDE29252F2F0051930C /* DiscoveryRouter.swift */; };
072787AD28D34D15002E9142 /* Core.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 072787AC28D34D15002E9142 /* Core.framework */; };
1402A0C92B61012F00A0A00B /* ProgramWebviewView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1402A0C72B61012F00A0A00B /* ProgramWebviewView.swift */; };
1402A0CA2B61012F00A0A00B /* ProgramWebviewViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1402A0C82B61012F00A0A00B /* ProgramWebviewViewModel.swift */; };
63C6E9CBBF5E33B8B9B4DFEC /* Pods_App_Discovery_DiscoveryUnitTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 780FC373E1D479E58870BD85 /* Pods_App_Discovery_DiscoveryUnitTests.framework */; };
9F47BCC672941A9854404EC7 /* Pods_App_Discovery.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 919E55130969D91EF03C4C0B /* Pods_App_Discovery.framework */; };
CFC8494C299A66080055E497 /* Localizable.stringsdict in Resources */ = {isa = PBXBuildFile; fileRef = CFC8494E299A66080055E497 /* Localizable.stringsdict */; };
Expand Down Expand Up @@ -76,6 +78,8 @@
0727879928D34C03002E9142 /* Discovery.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Discovery.framework; sourceTree = BUILT_PRODUCTS_DIR; };
072787AC28D34D15002E9142 /* Core.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Core.framework; sourceTree = BUILT_PRODUCTS_DIR; };
0C3850985F33C1AD72BF1B04 /* Pods-App-Discovery.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-Discovery.release.xcconfig"; path = "Target Support Files/Pods-App-Discovery/Pods-App-Discovery.release.xcconfig"; sourceTree = "<group>"; };
1402A0C72B61012F00A0A00B /* ProgramWebviewView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ProgramWebviewView.swift; sourceTree = "<group>"; };
1402A0C82B61012F00A0A00B /* ProgramWebviewViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ProgramWebviewViewModel.swift; sourceTree = "<group>"; };
2334C76D248D0A95634AFFD9 /* Pods-App-Discovery-DiscoveryUnitTests.debugstage.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-Discovery-DiscoveryUnitTests.debugstage.xcconfig"; path = "Target Support Files/Pods-App-Discovery-DiscoveryUnitTests/Pods-App-Discovery-DiscoveryUnitTests.debugstage.xcconfig"; sourceTree = "<group>"; };
2760B1F234E01FFCB73F41C2 /* Pods-App-Discovery.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-Discovery.debug.xcconfig"; path = "Target Support Files/Pods-App-Discovery/Pods-App-Discovery.debug.xcconfig"; sourceTree = "<group>"; };
445F0675BF0E1DEB78F3CE73 /* Pods-App-Discovery-DiscoveryUnitTests.releasedev.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-Discovery-DiscoveryUnitTests.releasedev.xcconfig"; path = "Target Support Files/Pods-App-Discovery-DiscoveryUnitTests/Pods-App-Discovery-DiscoveryUnitTests.releasedev.xcconfig"; sourceTree = "<group>"; };
Expand Down Expand Up @@ -200,6 +204,7 @@
070019A228F6EF2700D5FC78 /* Presentation */ = {
isa = PBXGroup;
children = (
1402A0C62B61011D00A0A00B /* WebPrograms */,
E0B9F6952B4D57F800168366 /* NativeDiscovery */,
E0D5861D2B300095009B4BA7 /* WebDiscovery */,
029242E52AE6976E00A940EC /* UpdateViews */,
Expand Down Expand Up @@ -266,6 +271,15 @@
path = Presentation;
sourceTree = "<group>";
};
1402A0C62B61011D00A0A00B /* WebPrograms */ = {
isa = PBXGroup;
children = (
1402A0C72B61012F00A0A00B /* ProgramWebviewView.swift */,
1402A0C82B61012F00A0A00B /* ProgramWebviewViewModel.swift */,
);
path = WebPrograms;
sourceTree = "<group>";
};
88B044C704F7C52F249CC424 /* Pods */ = {
isa = PBXGroup;
children = (
Expand Down Expand Up @@ -548,10 +562,12 @@
02F3BFDF29252F2F0051930C /* DiscoveryRouter.swift in Sources */,
E0D586252B300134009B4BA7 /* URL+PathExtension.swift in Sources */,
E0B9F6A62B4D620100168366 /* Data_CourseDetailsResponse.swift in Sources */,
1402A0C92B61012F00A0A00B /* ProgramWebviewView.swift in Sources */,
029737422949FB3B0051696B /* DiscoveryPersistenceProtocol.swift in Sources */,
0284DC0328D4922900830893 /* DiscoveryRepository.swift in Sources */,
029242EB2AE6AB7B00A940EC /* UpdateNotificationView.swift in Sources */,
02EF39DC28D86BEF0058F6BD /* Strings.swift in Sources */,
1402A0CA2B61012F00A0A00B /* ProgramWebviewViewModel.swift in Sources */,
E0B9F69C2B4D57F800168366 /* SearchView.swift in Sources */,
E0D586202B300095009B4BA7 /* DiscoveryWebviewViewModel.swift in Sources */,
02F1752F2A4DA3B60019CD70 /* DiscoveryAnalytics.swift in Sources */,
Expand Down
10 changes: 10 additions & 0 deletions Discovery/Discovery/Presentation/DiscoveryRouter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,11 @@ public protocol DiscoveryRouter: BaseRouter {
enrollmentEnd: Date?,
title: String
)

func showWebProgramDetails(
pathID: String,
viewType: ProgramViewType
)
}

// Mark - For testing and SwiftUI preview
Expand All @@ -53,5 +58,10 @@ public class DiscoveryRouterMock: BaseRouterMock, DiscoveryRouter {
enrollmentEnd: Date?,
title: String
) {}

public func showWebProgramDetails(
pathID: String,
viewType: ProgramViewType
) {}
}
#endif
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import Core

// Define your uri scheme
public enum URIString: String {
case appURLScheme = "edxapp"
case pathPlaceHolder = "{path_id}"
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -111,15 +111,16 @@ public class DiscoveryWebviewViewModel: ObservableObject {
}

extension DiscoveryWebviewViewModel: WebViewNavigationDelegate {
@MainActor
public func webView(
_ webView: WKWebView,
shouldLoad request: URLRequest,
navigationAction: WKNavigationAction
) -> Bool {
) async -> Bool {
guard let URL = request.url else { return false }

if let urlAction = urlAction(from: URL),
handleNavigation(url: URL, urlAction: urlAction) {
await handleNavigation(url: URL, urlAction: urlAction) {
return true
}

Expand All @@ -134,37 +135,34 @@ extension DiscoveryWebviewViewModel: WebViewNavigationDelegate {
}

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, image: nil)
)
}
router.presentAlert(
alertTitle: DiscoveryLocalization.Alert.leavingAppTitle,
alertMessage: DiscoveryLocalization.Alert.leavingAppMessage,
positiveAction: CoreLocalization.Webview.Alert.continue,
onCloseTapped: { [weak self] in
self?.router.dismiss(animated: true)
}, okTapped: {
UIApplication.shared.open(url, options: [:])
}, type: .default(positiveAction: CoreLocalization.Webview.Alert.continue, image: nil)
)
return true
}

return false
}

private func urlAction(from url: URL) -> WebviewActions? {
guard url.isValidAppURLScheme,
guard isValidAppURLScheme(url),
let url = WebviewActions(rawValue: url.appURLHost) else { return nil }
return url
}

private func handleNavigation(url: URL, urlAction: WebviewActions) -> Bool {
@MainActor
private func handleNavigation(url: URL, urlAction: WebviewActions) async -> Bool {
switch urlAction {
case .courseEnrollment:
if let urlData = parse(url: url), let courseID = urlData.courseId {
Task {
await enrollTo(courseID: courseID)
}
await enrollTo(courseID: courseID)
}
case .courseDetail:
guard let pathID = detailPathID(from: url) else { return false }
Expand Down Expand Up @@ -192,15 +190,15 @@ extension DiscoveryWebviewViewModel: WebViewNavigationDelegate {
}

private func detailPathID(from url: URL) -> String? {
guard url.isValidAppURLScheme,
guard isValidAppURLScheme(url),
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 }
guard isValidAppURLScheme(url) else { return nil }

let courseId = url.queryParameters?[URLParameterKeys.courseId] as? String
let emailOptIn = (url.queryParameters?[URLParameterKeys.emailOptIn] as? String).flatMap {Bool($0)}
Expand All @@ -209,7 +207,7 @@ extension DiscoveryWebviewViewModel: WebViewNavigationDelegate {
}

private func programDetailPathId(from url: URL) -> String? {
guard url.isValidAppURLScheme,
guard isValidAppURLScheme(url),
let path = url.queryParameters?[URLParameterKeys.pathId] as? String,
url.appURLHost == WebviewActions.programDetail.rawValue else { return nil }

Expand All @@ -231,4 +229,8 @@ extension DiscoveryWebviewViewModel: WebViewNavigationDelegate {

return true
}

private func isValidAppURLScheme(_ url: URL) -> Bool {
return url.scheme ?? "" == config.URIScheme
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,6 @@ public extension URL {
return host ?? ""
}

var isValidAppURLScheme: Bool {
return scheme ?? "" == URIString.appURLScheme.rawValue
}

var queryParameters: [String: Any]? {
guard let queryString = query else {
return nil
Expand Down
108 changes: 108 additions & 0 deletions Discovery/Discovery/Presentation/WebPrograms/ProgramWebviewView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
//
// ProgramWebviewView.swift
// Discovery
//
// Created by SaeedBashir on 1/23/24.
//

import Foundation
import SwiftUI
import Theme
import Core

public enum ProgramViewType: Equatable {
case program
case programDetail
}

public struct ProgramWebviewView: View {
@State private var isLoading: Bool = true

@ObservedObject private var viewModel: ProgramWebviewViewModel
private var router: DiscoveryRouter
private var viewType: ProgramViewType
private var pathID: String

private var URLString: String {
switch viewType {
case .program:
return viewModel.config.program.webview.baseURL ?? ""
case .programDetail:
let template = viewModel.config.program.webview.programDetailTemplate
return template?.replacingOccurrences(
of: URIString.pathPlaceHolder.rawValue,
with: pathID
) ?? ""
}
}

public init(
viewModel: ProgramWebviewViewModel,
router: DiscoveryRouter,
viewType: ProgramViewType = .program,
pathID: String = ""
) {
self.viewModel = viewModel
self.router = router
self.viewType = viewType
self.pathID = pathID

if let url = URL(string: URLString) {
viewModel.request = URLRequest(url: url)
}
}

public var body: some View {
GeometryReader { proxy in
VStack(alignment: .center) {
WebView(
viewModel: .init(
url: URLString,
baseURL: ""
),
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
}
}
}
}

// MARK: - Offline mode SnackBar
OfflineSnackBarView(
connectivity: viewModel.connectivity,
reloadAction: {
NotificationCenter.default.post(
name: .webviewReloadNotification,
object: nil
)
})
}
.navigationBarHidden(viewType == .program)
.navigationTitle(CoreLocalization.Mainscreen.programs)
.background(Theme.Colors.background.ignoresSafeArea())
}
}
Loading
Loading