diff --git a/Authorization/Authorization.xcodeproj/project.pbxproj b/Authorization/Authorization.xcodeproj/project.pbxproj index acde4b3e3..e3073af94 100644 --- a/Authorization/Authorization.xcodeproj/project.pbxproj +++ b/Authorization/Authorization.xcodeproj/project.pbxproj @@ -26,6 +26,10 @@ 0770DE6B28D0C035006D8A5D /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 0770DE6D28D0C035006D8A5D /* Localizable.strings */; }; 0770DE7128D0C0E7006D8A5D /* Strings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0770DE7028D0C0E7006D8A5D /* Strings.swift */; }; 5FB79D2802949372CDAF08D6 /* Pods_App_Authorization_AuthorizationTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4FAE9B7FD61FF88C9C4FE1E8 /* Pods_App_Authorization_AuthorizationTests.framework */; }; + 99C1654B2C0C4F0600DC384D /* ContainerWebView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 99C1654A2C0C4F0600DC384D /* ContainerWebView.swift */; }; + 99C1654D2C0C4F2F00DC384D /* SSOHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 99C1654C2C0C4F2F00DC384D /* SSOHelper.swift */; }; + 99C1654F2C0C4F5900DC384D /* SSOWebView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 99C1654E2C0C4F5900DC384D /* SSOWebView.swift */; }; + 99C165512C0C4F7B00DC384D /* SSOWebViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 99C165502C0C4F7B00DC384D /* SSOWebViewModel.swift */; }; BA8B3A322AD5487300D25EF5 /* SocialAuthView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA8B3A312AD5487300D25EF5 /* SocialAuthView.swift */; }; BADB3F552AD6DFC3004D5CFA /* SocialAuthViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = BADB3F542AD6DFC3004D5CFA /* SocialAuthViewModel.swift */; }; DE843D6BB1B9DDA398494890 /* Pods_App_Authorization.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 47BCFB7C19382EECF15131B6 /* Pods_App_Authorization.framework */; }; @@ -77,6 +81,10 @@ 7A84BB166492D4E46FBCF01C /* Pods-App-Authorization-AuthorizationTests.debugdev.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-Authorization-AuthorizationTests.debugdev.xcconfig"; path = "Target Support Files/Pods-App-Authorization-AuthorizationTests/Pods-App-Authorization-AuthorizationTests.debugdev.xcconfig"; sourceTree = ""; }; 90DFBB75EF40580E180D71C8 /* Pods-App-Authorization.debugdev.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-Authorization.debugdev.xcconfig"; path = "Target Support Files/Pods-App-Authorization/Pods-App-Authorization.debugdev.xcconfig"; sourceTree = ""; }; 96C85172770225EB81A6D2DA /* Pods-App-Authorization.releasedev.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-Authorization.releasedev.xcconfig"; path = "Target Support Files/Pods-App-Authorization/Pods-App-Authorization.releasedev.xcconfig"; sourceTree = ""; }; + 99C1654A2C0C4F0600DC384D /* ContainerWebView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContainerWebView.swift; sourceTree = ""; }; + 99C1654C2C0C4F2F00DC384D /* SSOHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SSOHelper.swift; sourceTree = ""; }; + 99C1654E2C0C4F5900DC384D /* SSOWebView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SSOWebView.swift; sourceTree = ""; }; + 99C165502C0C4F7B00DC384D /* SSOWebViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SSOWebViewModel.swift; sourceTree = ""; }; 9BF6A1004A955E24527FCF0F /* Pods-App-Authorization-AuthorizationTests.releaseprod.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-Authorization-AuthorizationTests.releaseprod.xcconfig"; path = "Target Support Files/Pods-App-Authorization-AuthorizationTests/Pods-App-Authorization-AuthorizationTests.releaseprod.xcconfig"; sourceTree = ""; }; A99D45203C981893C104053A /* Pods-App-Authorization-AuthorizationTests.releasestage.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-Authorization-AuthorizationTests.releasestage.xcconfig"; path = "Target Support Files/Pods-App-Authorization-AuthorizationTests/Pods-App-Authorization-AuthorizationTests.releasestage.xcconfig"; sourceTree = ""; }; BA8B3A312AD5487300D25EF5 /* SocialAuthView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SocialAuthView.swift; sourceTree = ""; }; @@ -147,6 +155,7 @@ 071009CC28D1E24000344290 /* Presentation */ = { isa = PBXGroup; children = ( + 99C165492C0C4EF000DC384D /* SSO */, BA8B3A302AD5485100D25EF5 /* SocialAuth */, E03261622AE6464A002CA7EB /* Startup */, 020C31BD290AADA700D6DEA2 /* Base */, @@ -268,6 +277,17 @@ path = ../Pods; sourceTree = ""; }; + 99C165492C0C4EF000DC384D /* SSO */ = { + isa = PBXGroup; + children = ( + 99C1654A2C0C4F0600DC384D /* ContainerWebView.swift */, + 99C1654C2C0C4F2F00DC384D /* SSOHelper.swift */, + 99C1654E2C0C4F5900DC384D /* SSOWebView.swift */, + 99C165502C0C4F7B00DC384D /* SSOWebViewModel.swift */, + ); + path = SSO; + sourceTree = ""; + }; BA8B3A302AD5485100D25EF5 /* SocialAuth */ = { isa = PBXGroup; children = ( @@ -502,13 +522,17 @@ 02066B462906D72F00F4307E /* SignUpViewModel.swift in Sources */, E03261642AE64676002CA7EB /* StartupViewModel.swift in Sources */, 02A2ACDB2A4B016100FBBBBB /* AuthorizationAnalytics.swift in Sources */, + 99C165512C0C4F7B00DC384D /* SSOWebViewModel.swift in Sources */, + 99C1654B2C0C4F0600DC384D /* ContainerWebView.swift in Sources */, 025F40E029D1E2FC0064C183 /* ResetPasswordView.swift in Sources */, 020C31CB290BF49900D6DEA2 /* FieldsView.swift in Sources */, 0770DE4E28D0A677006D8A5D /* SignInView.swift in Sources */, 02F3BFE5292533720051930C /* AuthorizationRouter.swift in Sources */, + 99C1654F2C0C4F5900DC384D /* SSOWebView.swift in Sources */, E03261662AE64AF4002CA7EB /* StartupView.swift in Sources */, 071009C728D1DA4F00344290 /* SignInViewModel.swift in Sources */, BA8B3A322AD5487300D25EF5 /* SocialAuthView.swift in Sources */, + 99C1654D2C0C4F2F00DC384D /* SSOHelper.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/Authorization/Authorization/Presentation/AuthorizationAnalytics.swift b/Authorization/Authorization/Presentation/AuthorizationAnalytics.swift index 9fcd13b69..9371a1b40 100644 --- a/Authorization/Authorization/Presentation/AuthorizationAnalytics.swift +++ b/Authorization/Authorization/Presentation/AuthorizationAnalytics.swift @@ -10,12 +10,15 @@ import Core public enum AuthMethod: Equatable { case password + case SSO case socailAuth(SocialAuthMethod) public var analyticsValue: String { switch self { case .password: "password" + case .SSO: + "SSO" case .socailAuth(let socialAuthMethod): socialAuthMethod.rawValue } diff --git a/Authorization/Authorization/Presentation/Login/SignInView.swift b/Authorization/Authorization/Presentation/Login/SignInView.swift index 4369f2fab..e706892d0 100644 --- a/Authorization/Authorization/Presentation/Login/SignInView.swift +++ b/Authorization/Authorization/Presentation/Login/SignInView.swift @@ -121,7 +121,7 @@ public struct SignInView: View { .accessibilityIdentifier("password_textfield") HStack { if !viewModel.config.features.startupScreenEnabled { - Button(CoreLocalization.register) { + Button(CoreLocalization.SignIn.registerBtn) { viewModel.router.showRegisterScreen(sourceScreen: viewModel.sourceScreen) } .foregroundColor(Theme.Colors.accentColor) @@ -155,6 +155,13 @@ public struct SignInView: View { .frame(maxWidth: .infinity) .padding(.top, 40) .accessibilityIdentifier("signin_button") + + StyledButton(CoreLocalization.SignIn.logInWithSsoBtn) { + viewModel.router.showSSOWebBrowser(title: CoreLocalization.SignIn.logInBtn) + } + .frame(maxWidth: .infinity) + .padding(.top, 20) + .accessibilityIdentifier("signin_button") } } if viewModel.socialAuthEnabled { diff --git a/Authorization/Authorization/Presentation/Login/SignInViewModel.swift b/Authorization/Authorization/Presentation/Login/SignInViewModel.swift index 5a87151f5..f6c994a81 100644 --- a/Authorization/Authorization/Presentation/Login/SignInViewModel.swift +++ b/Authorization/Authorization/Presentation/Login/SignInViewModel.swift @@ -88,6 +88,21 @@ public class SignInViewModel: ObservableObject { } } + @MainActor + func ssoLogin(title: String) async { + + analytics.userSignInClicked() + isShowProgress = true + do { + let user = try await interactor.SSOlogin(jwtToken: "239i2oi3jrf2jflkj23lf2f") + analytics.identify(id: "\(user.id)", username: user.username, email: user.email) + analytics.userLogin(method: .password) + router.showMainOrWhatsNewScreen(sourceScreen: sourceScreen) + } catch let error { + failure(error) + } + } + @MainActor func login(with result: Result) async { switch result { diff --git a/Authorization/Authorization/Presentation/Registration/SignUpView.swift b/Authorization/Authorization/Presentation/Registration/SignUpView.swift index 7ec2c8ba5..d4d855e7d 100644 --- a/Authorization/Authorization/Presentation/Registration/SignUpView.swift +++ b/Authorization/Authorization/Presentation/Registration/SignUpView.swift @@ -40,7 +40,7 @@ public struct SignUpView: View { VStack(alignment: .center) { ZStack { HStack { - Text(CoreLocalization.register) + Text(CoreLocalization.SignIn.registerBtn) .titleSettings(color: Theme.Colors.loginNavigationText) .accessibilityIdentifier("register_text") } @@ -64,7 +64,7 @@ public struct SignUpView: View { ScrollView { VStack(alignment: .leading) { - Text(CoreLocalization.register) + Text(CoreLocalization.SignIn.registerBtn) .font(Theme.Fonts.displaySmall) .foregroundColor(Theme.Colors.textPrimary) .padding(.bottom, 4) diff --git a/Authorization/Authorization/Presentation/SSO/ContainerWebView.swift b/Authorization/Authorization/Presentation/SSO/ContainerWebView.swift new file mode 100644 index 000000000..476630cf6 --- /dev/null +++ b/Authorization/Authorization/Presentation/SSO/ContainerWebView.swift @@ -0,0 +1,46 @@ +// +// ContainerWebView.swift +// Authorization +// +// Created by Rawan Matar on 02/06/2024. +// + +import SwiftUI +import Core +import Swinject + +public struct ContainerWebView: View { + + // MARK: - Internal Properties + + let url: String + private var pageTitle: String + @Environment(\.presentationMode) var presentationMode + + // MARK: - Init + + public init(_ url: String, title: String) { + self.url = url + self.pageTitle = title + } + + // MARK: - UI + + public var body: some View { + VStack(alignment: .center) { + NavigationBar( + title: pageTitle, + leftButtonAction: { presentationMode.wrappedValue.dismiss() } + ) + + ZStack { + if !url.isEmpty { + SSOWebView(url: URL(string: url), viewModel: Container.shared.resolve(SSOWebViewModel.self)!) + } else { + EmptyView() + } + } + .accessibilityIdentifier("web_browser") + } + } +} diff --git a/Authorization/Authorization/Presentation/SSO/SSOHelper.swift b/Authorization/Authorization/Presentation/SSO/SSOHelper.swift new file mode 100644 index 000000000..9ce2b993e --- /dev/null +++ b/Authorization/Authorization/Presentation/SSO/SSOHelper.swift @@ -0,0 +1,73 @@ +// +// SSOHelper.swift +// Authorization +// +// Created by Rawan Matar on 02/06/2024. +// + +import Foundation + +// https://developer.apple.com/documentation/ios-ipados-release-notes/foundation-release-notes + +/** + A Helper for some of the SSO preferences. + Keeps data under the UserDefaults. + */ +public class SSOHelper: NSObject { + + public enum UserDefaultKeys: String, CaseIterable { + case cookiePayload + case cookieSignature + case userInfo + + var description: String { + switch self { + case .cookiePayload: + return "edx-jwt-cookie-header-payload" + case .cookieSignature: + return "edx-jwt-cookie-signature" + case .userInfo: + return "edx-user-info" + } + } + } + + // MARK: - Singleton + + public static let shared = SSOHelper() + + // MARK: - Public Properties + + /// Authentication + public var cookiePayload: String? { + get { + let defaults = UserDefaults.standard + return defaults.string(forKey: UserDefaultKeys.cookiePayload.rawValue) + } + set (newValue) { + let defaults = UserDefaults.standard + defaults.set(newValue, forKey: UserDefaultKeys.cookiePayload.rawValue) + } + } + + /// Authentication + public var cookieSignature: String? { + get { + let defaults = UserDefaults.standard + return defaults.string(forKey: UserDefaultKeys.cookieSignature.rawValue) + } + set (newValue) { + let defaults = UserDefaults.standard + defaults.set(newValue, forKey: UserDefaultKeys.cookieSignature.rawValue) + } + } + + // MARK: - Public Methods + + /// Checks if the user is login. + public func cleanAfterSuccesfulLogout() { + cookiePayload = nil + cookieSignature = nil + } +} + diff --git a/Authorization/Authorization/Presentation/SSO/SSOWebView.swift b/Authorization/Authorization/Presentation/SSO/SSOWebView.swift new file mode 100644 index 000000000..aeaae2661 --- /dev/null +++ b/Authorization/Authorization/Presentation/SSO/SSOWebView.swift @@ -0,0 +1,104 @@ +// +// SSOWebView.swift +// Authorization +// +// Created by Rawan Matar on 02/06/2024. +// + + +import SwiftUI +import WebKit +import Core + +public struct SAMLConstants { + + public init() {} + + public let samlLoginSuccess = URL(string: "https://blue.zeitlabs.com/auth/complete/tpa-saml")! +} + +public struct SSOWebView: UIViewRepresentable { + + let url: URL? + + var viewModel: SSOWebViewModel + + public init(url: URL?, viewModel: SSOWebViewModel) { + self.url = url + self.viewModel = viewModel + } + + public func makeUIView(context: Context) -> WKWebView { + let coordinator = makeCoordinator() + let userContentController = WKUserContentController() + userContentController.add(coordinator, name: "bridge") + + let prefs = WKWebpagePreferences() + let config = WKWebViewConfiguration() + prefs.allowsContentJavaScript = true + + config.userContentController = userContentController + config.defaultWebpagePreferences = prefs + config.websiteDataStore = WKWebsiteDataStore.nonPersistent() + + let wkWebView = WKWebView(frame: .zero, configuration: config) + wkWebView.navigationDelegate = coordinator + + guard let currentURL = url else { + return wkWebView + } + let request = URLRequest(url: currentURL) + wkWebView.load(request) + + return wkWebView + } + + public func updateUIView(_ uiView: WKWebView, context: Context) { + + } + + public func makeCoordinator() -> Coordinator { + Coordinator(viewModel: self.viewModel) + } + + public class Coordinator: NSObject, WKScriptMessageHandler, WKNavigationDelegate { + var viewModel: SSOWebViewModel + + init(viewModel: SSOWebViewModel) { + self.viewModel = viewModel + super.init() + } + + // WKScriptMessageHandler + public func userContentController(_ userContentController: WKUserContentController, + didReceive message: WKScriptMessage) { + } + + // WKNavigationDelegate + public func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) { + guard let _ = webView.url?.absoluteString else { + return + } + + } + + public func webView(_ webView: WKWebView, + decidePolicyFor navigationAction: WKNavigationAction, + decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) { + guard let url = webView.url?.absoluteString else { + decisionHandler(.allow) + return + } + + if url.contains(SAMLConstants().samlLoginSuccess.absoluteString) { + webView.configuration.websiteDataStore.httpCookieStore.getAllCookies { cookies in + Task { + await self.viewModel.SSOLogin(cookies: cookies) + } + } + } + + decisionHandler(.allow) + } + } +} diff --git a/Authorization/Authorization/Presentation/SSO/SSOWebViewModel.swift b/Authorization/Authorization/Presentation/SSO/SSOWebViewModel.swift new file mode 100644 index 000000000..bb76cf81c --- /dev/null +++ b/Authorization/Authorization/Presentation/SSO/SSOWebViewModel.swift @@ -0,0 +1,117 @@ +// +// SSOWebViewModel.swift +// Authorization +// +// Created by Rawan Matar on 02/06/2024. +// + +import Foundation +import SwiftUI +import Core +import Alamofire +import AuthenticationServices +import FacebookLogin +import GoogleSignIn +import MSAL + +public class SSOWebViewModel: ObservableObject { + + @Published private(set) var isShowProgress = false + @Published private(set) var showError: Bool = false + @Published private(set) var showAlert: Bool = false + let sourceScreen: LogistrationSourceScreen = .default + + var errorMessage: String? { + didSet { + withAnimation { + showError = errorMessage != nil + } + } + } + var alertMessage: String? { + didSet { + withAnimation { + showAlert = alertMessage != nil + } + } + } + + let router: AuthorizationRouter + let config: ConfigProtocol + private let interactor: AuthInteractorProtocol + private let analytics: AuthorizationAnalytics + + public init( + interactor: AuthInteractorProtocol, + router: AuthorizationRouter, + config: ConfigProtocol, + analytics: AuthorizationAnalytics + ) { + self.interactor = interactor + self.router = router + self.config = config + self.analytics = analytics + } + + @MainActor + func SSOLogin(cookies: [HTTPCookie]) async { + guard !cookies.isEmpty else { + errorMessage = "COOKIES EMPTY" + return + } + + isShowProgress = true + for cookie in cookies { + + /// Store cookies in UserDefaults + if cookie.name == SSOHelper.UserDefaultKeys.cookiePayload.description { + SSOHelper.shared.cookiePayload = cookie.value + } + + if cookie.name == SSOHelper.UserDefaultKeys.cookieSignature.description { + SSOHelper.shared.cookieSignature = cookie.value + } + if let signature = SSOHelper.shared.cookieSignature, + let payload = SSOHelper.shared.cookiePayload { + isShowProgress = true + do { + let user = try await interactor.SSOlogin(jwtToken: "\(payload).\(signature)") + analytics.identify(id: "\(user.id)", username: user.username, email: user.email) + analytics.userLogin(method: .SSO) + router.showMainOrWhatsNewScreen(sourceScreen: sourceScreen) + } catch let error { + failure(error, authMethod: .SSO) + } + } + } + } + + @MainActor + private func failure(_ error: Error, authMethod: AuthMethod? = nil) { + isShowProgress = false + if let validationError = error.validationError, + let value = validationError.data?["error_description"] as? String { + if authMethod != .password, validationError.statusCode == 400, let authMethod = authMethod { + errorMessage = AuthLocalization.Error.accountNotRegistered( + authMethod.analyticsValue, + config.platformName + ) + } else if validationError.statusCode == 403 { + errorMessage = AuthLocalization.Error.disabledAccount + } else { + errorMessage = value + } + } else if case APIError.invalidGrant = error { + errorMessage = CoreLocalization.Error.invalidCredentials + } else if error.isInternetError { + errorMessage = CoreLocalization.Error.slowOrNoInternetConnection + } else { + errorMessage = CoreLocalization.Error.unknownError + } + } + + func trackForgotPasswordClicked() { + analytics.forgotPasswordClicked() + } + +} diff --git a/Authorization/Authorization/Presentation/Startup/StartupView.swift b/Authorization/Authorization/Presentation/Startup/StartupView.swift index 88551d824..b78a0bded 100644 --- a/Authorization/Authorization/Presentation/Startup/StartupView.swift +++ b/Authorization/Authorization/Presentation/Startup/StartupView.swift @@ -105,6 +105,8 @@ public struct StartupView: View { switch buttonAction { case .signIn: viewModel.router.showLoginScreen(sourceScreen: .startup) + case .signInWithSSO: + viewModel.router.showLoginScreen(sourceScreen: .startup) case .register: viewModel.router.showRegisterScreen(sourceScreen: .startup) } diff --git a/Authorization/AuthorizationTests/AuthorizationMock.generated.swift b/Authorization/AuthorizationTests/AuthorizationMock.generated.swift index 34ce42889..a05c268d8 100644 --- a/Authorization/AuthorizationTests/AuthorizationMock.generated.swift +++ b/Authorization/AuthorizationTests/AuthorizationMock.generated.swift @@ -93,6 +93,23 @@ open class AuthInteractorProtocolMock: AuthInteractorProtocol, Mock { return __value } + @discardableResult + open func SSOlogin(jwtToken: String) async throws -> Core.User { + addInvocation(.m_login__SSO__username_password(Parameter.value(`jwtToken`))) + let perform = methodPerformValue(.m_login__SSO__username_password(Parameter.value(`jwtToken`))) as? (String) -> Void + perform?(`jwtToken`) + var __value: User + do { + __value = try methodReturnValue(.m_login__SSO__username_password(Parameter.value(jwtToken))).casted() + } catch MockError.notStubed { + onFatalFailure("Stub return value not specified for login(username: String, password: String). Use given") + Failure("Stub return value not specified for login(username: String, password: String). Use given") + } catch { + throw error + } + return __value + } + open func resetPassword(email: String) throws -> ResetPassword { addInvocation(.m_resetPassword__email_email(Parameter.value(`email`))) let perform = methodPerformValue(.m_resetPassword__email_email(Parameter.value(`email`))) as? (String) -> Void @@ -173,6 +190,7 @@ open class AuthInteractorProtocolMock: AuthInteractorProtocol, Mock { fileprivate enum MethodType { case m_login__username_usernamepassword_password(Parameter, Parameter) + case m_login__SSO__username_password(Parameter) case m_login__externalToken_externalTokenbackend_backend(Parameter, Parameter) case m_resetPassword__email_email(Parameter) case m_getCookies__force_force(Parameter) @@ -188,6 +206,11 @@ open class AuthInteractorProtocolMock: AuthInteractorProtocol, Mock { results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsPassword, rhs: rhsPassword, with: matcher), lhsPassword, rhsPassword, "password")) return Matcher.ComparisonResult(results) + case (.m_login__SSO__username_password(let lhsJwtToken), .m_login__SSO__username_password(let rhsJwtToken)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsJwtToken, rhs: rhsJwtToken, with: matcher), lhsJwtToken, rhsJwtToken, "jwtToken")) + return Matcher.ComparisonResult(results) + case (.m_login__externalToken_externalTokenbackend_backend(let lhsExternaltoken, let lhsBackend), .m_login__externalToken_externalTokenbackend_backend(let rhsExternaltoken, let rhsBackend)): var results: [Matcher.ParameterComparisonResult] = [] results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsExternaltoken, rhs: rhsExternaltoken, with: matcher), lhsExternaltoken, rhsExternaltoken, "externalToken")) @@ -223,6 +246,7 @@ open class AuthInteractorProtocolMock: AuthInteractorProtocol, Mock { func intValue() -> Int { switch self { case let .m_login__username_usernamepassword_password(p0, p1): return p0.intValue + p1.intValue + case let .m_login__SSO__username_password(p0): return p0.intValue case let .m_login__externalToken_externalTokenbackend_backend(p0, p1): return p0.intValue + p1.intValue case let .m_resetPassword__email_email(p0): return p0.intValue case let .m_getCookies__force_force(p0): return p0.intValue @@ -234,6 +258,7 @@ open class AuthInteractorProtocolMock: AuthInteractorProtocol, Mock { func assertionName() -> String { switch self { case .m_login__username_usernamepassword_password: return ".login(username:password:)" + case .m_login__SSO__username_password: return ".loginSSO(username:password:)" case .m_login__externalToken_externalTokenbackend_backend: return ".login(externalToken:backend:)" case .m_resetPassword__email_email: return ".resetPassword(email:)" case .m_getCookies__force_force: return ".getCookies(force:)" @@ -261,6 +286,10 @@ open class AuthInteractorProtocolMock: AuthInteractorProtocol, Mock { public static func login(externalToken: Parameter, backend: Parameter, willReturn: User...) -> MethodStub { return Given(method: .m_login__externalToken_externalTokenbackend_backend(`externalToken`, `backend`), products: willReturn.map({ StubProduct.return($0 as Any) })) } + @discardableResult + public static func ssoLogin(title: Parameter, willReturn: User...) -> MethodStub { + return Given(method: .m_login__SSO__username_password(`title`), products: willReturn.map({ StubProduct.return($0 as Any) })) + } public static func resetPassword(email: Parameter, willReturn: ResetPassword...) -> MethodStub { return Given(method: .m_resetPassword__email_email(`email`), products: willReturn.map({ StubProduct.return($0 as Any) })) } @@ -353,7 +382,8 @@ open class AuthInteractorProtocolMock: AuthInteractorProtocol, Mock { fileprivate var method: MethodType @discardableResult - public static func login(username: Parameter, password: Parameter) -> Verify { return Verify(method: .m_login__username_usernamepassword_password(`username`, `password`))} + public static func login(username: Parameter, password: Parameter) -> Verify { return Verify(method: .m_login__username_usernamepassword_password(`username`, `password`))} + public static func ssoLogin(title: Parameter) -> Verify { return Verify(method: .m_login__SSO__username_password(`title`))} @discardableResult public static func login(externalToken: Parameter, backend: Parameter) -> Verify { return Verify(method: .m_login__externalToken_externalTokenbackend_backend(`externalToken`, `backend`))} public static func resetPassword(email: Parameter) -> Verify { return Verify(method: .m_resetPassword__email_email(`email`))} @@ -579,6 +609,7 @@ open class AuthorizationAnalyticsMock: AuthorizationAnalytics, Mock { fileprivate enum MethodType { case m_identify__id_idusername_usernameemail_email(Parameter, Parameter, Parameter) case m_userLogin__method_method(Parameter) + case m_ssoLogin__method_method(Parameter) case m_registerClicked case m_signInClicked case m_userSignInClicked @@ -638,6 +669,7 @@ open class AuthorizationAnalyticsMock: AuthorizationAnalytics, Mock { switch self { case let .m_identify__id_idusername_usernameemail_email(p0, p1, p2): return p0.intValue + p1.intValue + p2.intValue case let .m_userLogin__method_method(p0): return p0.intValue + case let .m_ssoLogin__method_method(p0): return p0.intValue case .m_registerClicked: return 0 case .m_signInClicked: return 0 case .m_userSignInClicked: return 0 @@ -653,6 +685,7 @@ open class AuthorizationAnalyticsMock: AuthorizationAnalytics, Mock { switch self { case .m_identify__id_idusername_usernameemail_email: return ".identify(id:username:email:)" case .m_userLogin__method_method: return ".userLogin(method:)" + case .m_ssoLogin__method_method: return ".ssoLogin(method:)" case .m_registerClicked: return ".registerClicked()" case .m_signInClicked: return ".signInClicked()" case .m_userSignInClicked: return ".userSignInClicked()" @@ -682,6 +715,7 @@ open class AuthorizationAnalyticsMock: AuthorizationAnalytics, Mock { public static func identify(id: Parameter, username: Parameter, email: Parameter) -> Verify { return Verify(method: .m_identify__id_idusername_usernameemail_email(`id`, `username`, `email`))} public static func userLogin(method: Parameter) -> Verify { return Verify(method: .m_userLogin__method_method(`method`))} + public static func ssoLogin(method: Parameter) -> Verify { return Verify(method: .m_ssoLogin__method_method(`method`))} public static func registerClicked() -> Verify { return Verify(method: .m_registerClicked)} public static func signInClicked() -> Verify { return Verify(method: .m_signInClicked)} public static func userSignInClicked() -> Verify { return Verify(method: .m_userSignInClicked)} @@ -927,6 +961,12 @@ open class AuthorizationRouterMock: AuthorizationRouter, Mock { perform?(`title`, `url`) } + open func showSSOWebBrowser(title: String) { + addInvocation(.m_showWebBrowser__SSO(Parameter.value(`title`))) + let perform = methodPerformValue(.m_showWebBrowser__SSO(Parameter.value(`title`))) as? (String) -> Void + perform?(`title`) + } + open func presentAlert(alertTitle: String, alertMessage: String, positiveAction: String, onCloseTapped: @escaping () -> Void, okTapped: @escaping () -> Void, type: AlertViewType) { addInvocation(.m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type(Parameter.value(`alertTitle`), Parameter.value(`alertMessage`), Parameter.value(`positiveAction`), Parameter<() -> Void>.value(`onCloseTapped`), Parameter<() -> Void>.value(`okTapped`), Parameter.value(`type`))) let perform = methodPerformValue(.m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type(Parameter.value(`alertTitle`), Parameter.value(`alertMessage`), Parameter.value(`positiveAction`), Parameter<() -> Void>.value(`onCloseTapped`), Parameter<() -> Void>.value(`okTapped`), Parameter.value(`type`))) as? (String, String, String, @escaping () -> Void, @escaping () -> Void, AlertViewType) -> Void @@ -966,6 +1006,7 @@ open class AuthorizationRouterMock: AuthorizationRouter, Mock { case m_showForgotPasswordScreen case m_showDiscoveryScreen__searchQuery_searchQuerysourceScreen_sourceScreen(Parameter, Parameter) case m_showWebBrowser__title_titleurl_url(Parameter, Parameter) + case m_showWebBrowser__SSO(Parameter) case m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type(Parameter, Parameter, Parameter, Parameter<() -> Void>, Parameter<() -> Void>, Parameter) case m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagenextSectionName_nextSectionNameaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTappednextSectionTapped_nextSectionTapped(Parameter, Parameter, Parameter, Parameter, Parameter, Parameter<() -> Void>, Parameter<() -> Void>, Parameter<() -> Void>) case m_presentView__transitionStyle_transitionStyleview_viewcompletion_completion(Parameter, Parameter, Parameter<(() -> Void)?>) @@ -1085,6 +1126,7 @@ open class AuthorizationRouterMock: AuthorizationRouter, Mock { case .m_showForgotPasswordScreen: return 0 case let .m_showDiscoveryScreen__searchQuery_searchQuerysourceScreen_sourceScreen(p0, p1): return p0.intValue + p1.intValue case let .m_showWebBrowser__title_titleurl_url(p0, p1): return p0.intValue + p1.intValue + case let .m_showWebBrowser__SSO(p0): return p0.intValue case let .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type(p0, p1, p2, p3, p4, p5): return p0.intValue + p1.intValue + p2.intValue + p3.intValue + p4.intValue + p5.intValue case let .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagenextSectionName_nextSectionNameaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTappednextSectionTapped_nextSectionTapped(p0, p1, p2, p3, p4, p5, p6, p7): return p0.intValue + p1.intValue + p2.intValue + p3.intValue + p4.intValue + p5.intValue + p6.intValue + p7.intValue case let .m_presentView__transitionStyle_transitionStyleview_viewcompletion_completion(p0, p1, p2): return p0.intValue + p1.intValue + p2.intValue @@ -1106,6 +1148,7 @@ open class AuthorizationRouterMock: AuthorizationRouter, Mock { case .m_showForgotPasswordScreen: return ".showForgotPasswordScreen()" case .m_showDiscoveryScreen__searchQuery_searchQuerysourceScreen_sourceScreen: return ".showDiscoveryScreen(searchQuery:sourceScreen:)" case .m_showWebBrowser__title_titleurl_url: return ".showWebBrowser(title:url:)" + case .m_showWebBrowser__SSO: return ".showSSOWebBrowser(title:)" case .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type: return ".presentAlert(alertTitle:alertMessage:positiveAction:onCloseTapped:okTapped:type:)" case .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagenextSectionName_nextSectionNameaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTappednextSectionTapped_nextSectionTapped: return ".presentAlert(alertTitle:alertMessage:nextSectionName:action:image:onCloseTapped:okTapped:nextSectionTapped:)" case .m_presentView__transitionStyle_transitionStyleview_viewcompletion_completion: return ".presentView(transitionStyle:view:completion:)" @@ -1190,6 +1233,9 @@ open class AuthorizationRouterMock: AuthorizationRouter, Mock { public static func showWebBrowser(title: Parameter, url: Parameter, perform: @escaping (String, URL) -> Void) -> Perform { return Perform(method: .m_showWebBrowser__title_titleurl_url(`title`, `url`), performs: perform) } + public static func showSSOWebBrowser(title: Parameter, perform: @escaping (String, URL) -> Void) -> Perform { + return Perform(method: .m_showWebBrowser__SSO(`title`), performs: perform) + } public static func presentAlert(alertTitle: Parameter, alertMessage: Parameter, positiveAction: Parameter, onCloseTapped: Parameter<() -> Void>, okTapped: Parameter<() -> Void>, type: Parameter, perform: @escaping (String, String, String, @escaping () -> Void, @escaping () -> Void, AlertViewType) -> Void) -> Perform { return Perform(method: .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type(`alertTitle`, `alertMessage`, `positiveAction`, `onCloseTapped`, `okTapped`, `type`), performs: perform) } @@ -1389,8 +1435,14 @@ open class BaseRouterMock: BaseRouter, Mock { open func showWebBrowser(title: String, url: URL) { addInvocation(.m_showWebBrowser__title_titleurl_url(Parameter.value(`title`), Parameter.value(`url`))) - let perform = methodPerformValue(.m_showWebBrowser__title_titleurl_url(Parameter.value(`title`), Parameter.value(`url`))) as? (String, URL) -> Void - perform?(`title`, `url`) + let perform = methodPerformValue(.m_showWebBrowser__title_titleurl_url(Parameter.value(`title`), Parameter.value(`url`))) as? (String, URL) -> Void + perform?(`title`, `url`) + } + + open func showSSOWebBrowser(title: String) { + addInvocation(.m_showWebBrowser__SSO(Parameter.value(`title`))) + let perform = methodPerformValue(.m_showWebBrowser__SSO(Parameter.value(`title`))) as? (String) -> Void + perform?(`title`) } open func presentAlert(alertTitle: String, alertMessage: String, positiveAction: String, onCloseTapped: @escaping () -> Void, okTapped: @escaping () -> Void, type: AlertViewType) { @@ -1431,6 +1483,7 @@ open class BaseRouterMock: BaseRouter, Mock { case m_showForgotPasswordScreen case m_showDiscoveryScreen__searchQuery_searchQuerysourceScreen_sourceScreen(Parameter, Parameter) case m_showWebBrowser__title_titleurl_url(Parameter, Parameter) + case m_showWebBrowser__SSO(Parameter) case m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type(Parameter, Parameter, Parameter, Parameter<() -> Void>, Parameter<() -> Void>, Parameter) case m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagenextSectionName_nextSectionNameaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTappednextSectionTapped_nextSectionTapped(Parameter, Parameter, Parameter, Parameter, Parameter, Parameter<() -> Void>, Parameter<() -> Void>, Parameter<() -> Void>) case m_presentView__transitionStyle_transitionStyleview_viewcompletion_completion(Parameter, Parameter, Parameter<(() -> Void)?>) @@ -1544,6 +1597,7 @@ open class BaseRouterMock: BaseRouter, Mock { case .m_showForgotPasswordScreen: return 0 case let .m_showDiscoveryScreen__searchQuery_searchQuerysourceScreen_sourceScreen(p0, p1): return p0.intValue + p1.intValue case let .m_showWebBrowser__title_titleurl_url(p0, p1): return p0.intValue + p1.intValue + case let .m_showWebBrowser__SSO(p0): return p0.intValue case let .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type(p0, p1, p2, p3, p4, p5): return p0.intValue + p1.intValue + p2.intValue + p3.intValue + p4.intValue + p5.intValue case let .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagenextSectionName_nextSectionNameaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTappednextSectionTapped_nextSectionTapped(p0, p1, p2, p3, p4, p5, p6, p7): return p0.intValue + p1.intValue + p2.intValue + p3.intValue + p4.intValue + p5.intValue + p6.intValue + p7.intValue case let .m_presentView__transitionStyle_transitionStyleview_viewcompletion_completion(p0, p1, p2): return p0.intValue + p1.intValue + p2.intValue @@ -1564,6 +1618,7 @@ open class BaseRouterMock: BaseRouter, Mock { case .m_showForgotPasswordScreen: return ".showForgotPasswordScreen()" case .m_showDiscoveryScreen__searchQuery_searchQuerysourceScreen_sourceScreen: return ".showDiscoveryScreen(searchQuery:sourceScreen:)" case .m_showWebBrowser__title_titleurl_url: return ".showWebBrowser(title:url:)" + case .m_showWebBrowser__SSO: return ".showSSOWebBrowser(title:)" case .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type: return ".presentAlert(alertTitle:alertMessage:positiveAction:onCloseTapped:okTapped:type:)" case .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagenextSectionName_nextSectionNameaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTappednextSectionTapped_nextSectionTapped: return ".presentAlert(alertTitle:alertMessage:nextSectionName:action:image:onCloseTapped:okTapped:nextSectionTapped:)" case .m_presentView__transitionStyle_transitionStyleview_viewcompletion_completion: return ".presentView(transitionStyle:view:completion:)" diff --git a/Authorization/AuthorizationTests/Presentation/Login/SignInViewModelTests.swift b/Authorization/AuthorizationTests/Presentation/Login/SignInViewModelTests.swift index e824ad975..371ac62b7 100644 --- a/Authorization/AuthorizationTests/Presentation/Login/SignInViewModelTests.swift +++ b/Authorization/AuthorizationTests/Presentation/Login/SignInViewModelTests.swift @@ -93,7 +93,33 @@ final class SignInViewModelTests: XCTestCase { XCTAssertEqual(viewModel.errorMessage, nil) XCTAssertEqual(viewModel.isShowProgress, true) } - + + func testSSOLoginSuccess() async throws { + let interactor = AuthInteractorProtocolMock() + let router = AuthorizationRouterMock() + let validator = Validator() + let analytics = AuthorizationAnalyticsMock() + let viewModel = SignInViewModel( + interactor: interactor, + router: router, + config: ConfigMock(), + analytics: analytics, + validator: validator, + sourceScreen: .default + ) + let user = User(id: 1, username: "username", email: "edxUser@edx.com", name: "Name", userAvatar: "") + + Given(interactor, .ssoLogin(title: .any, willReturn: user)) + + await viewModel.ssoLogin(title: "Riyadah") + + Verify(interactor, 1, .ssoLogin(title: .any)) + Verify(router, 1, .showMainOrWhatsNewScreen(sourceScreen: .any)) + + XCTAssertEqual(viewModel.errorMessage, nil) + XCTAssertEqual(viewModel.isShowProgress, true) + } + func testSocialLoginSuccess() async throws { let interactor = AuthInteractorProtocolMock() let router = AuthorizationRouterMock() diff --git a/Core/Core/Configuration/BaseRouter.swift b/Core/Core/Configuration/BaseRouter.swift index e2a2a714b..4f2ae5bba 100644 --- a/Core/Core/Configuration/BaseRouter.swift +++ b/Core/Core/Configuration/BaseRouter.swift @@ -34,6 +34,8 @@ public protocol BaseRouter { func showDiscoveryScreen(searchQuery: String?, sourceScreen: LogistrationSourceScreen) func showWebBrowser(title: String, url: URL) + + func showSSOWebBrowser(title: String) func presentAlert( alertTitle: String, @@ -100,6 +102,8 @@ open class BaseRouterMock: BaseRouter { public func removeLastView(controllers: Int) {} public func showWebBrowser(title: String, url: URL) {} + + public func showSSOWebBrowser(title: String) {} public func presentAlert( alertTitle: String, diff --git a/Core/Core/Configuration/Config/Config.swift b/Core/Core/Configuration/Config/Config.swift index be5fd1941..f5c3f5908 100644 --- a/Core/Core/Configuration/Config/Config.swift +++ b/Core/Core/Configuration/Config/Config.swift @@ -9,6 +9,7 @@ import Foundation public protocol ConfigProtocol { var baseURL: URL { get } + var SSOBaseURL: URL { get } var oAuthClientId: String { get } var tokenType: TokenType { get } var feedbackEmail: String { get } @@ -41,6 +42,7 @@ public enum TokenType: String { private enum ConfigKeys: String { case baseURL = "API_HOST_URL" + case SSOBaseURL = "SSO_URL" case oAuthClientID = "OAUTH_CLIENT_ID" case tokenType = "TOKEN_TYPE" case feedbackEmailAddress = "FEEDBACK_EMAIL_ADDRESS" @@ -120,6 +122,14 @@ extension Config: ConfigProtocol { return url } + public var SSOBaseURL: URL { + guard let urlString = string(for: ConfigKeys.SSOBaseURL.rawValue), + let url = URL(string: urlString) else { + fatalError("Unable to find SSO base url in config.") + } + return url + } + public var oAuthClientId: String { guard let clientID = string(for: ConfigKeys.oAuthClientID.rawValue) else { fatalError("Unable to find OAuth ClientID in config.") @@ -168,6 +178,7 @@ extension Config: ConfigProtocol { public class ConfigMock: Config { private let config: [String: Any] = [ "API_HOST_URL": "https://www.example.com", + "SSO_URL" : "https://www.example.com", "OAUTH_CLIENT_ID": "oauth_client_id", "FEEDBACK_EMAIL_ADDRESS": "example@mail.com", "PLATFORM_NAME": "OpenEdx", diff --git a/Core/Core/Data/Repository/AuthRepository.swift b/Core/Core/Data/Repository/AuthRepository.swift index e4adf93ea..21c79e753 100644 --- a/Core/Core/Data/Repository/AuthRepository.swift +++ b/Core/Core/Data/Repository/AuthRepository.swift @@ -10,6 +10,7 @@ import Foundation public protocol AuthRepositoryProtocol { func login(username: String, password: String) async throws -> User func login(externalToken: String, backend: String) async throws -> User + func SSOlogin(jwtToken: String) async throws -> User func getCookies(force: Bool) async throws func getRegistrationFields() async throws -> [PickerFields] func registerUser(fields: [String: String], isSocial: Bool) async throws -> User @@ -80,6 +81,18 @@ public class AuthRepository: AuthRepositoryProtocol { return user.domain } + public func SSOlogin(jwtToken: String) async throws -> User { + if appStorage.accessToken == nil || + appStorage.refreshToken == nil { + appStorage.accessToken = jwtToken + appStorage.refreshToken = jwtToken + } + + let user = try await api.requestData(AuthEndpoint.getUserInfo).mapResponse(DataLayer.User.self) + appStorage.user = user + return user.domain + } + public func resetPassword(email: String) async throws -> ResetPassword { let response = try await api.requestData(AuthEndpoint.resetPassword(email: email)) .mapResponse(DataLayer.ResetPassword.self) @@ -138,6 +151,10 @@ class AuthRepositoryMock: AuthRepositoryProtocol { User(id: 1, username: "User", email: "email@gmail.com", name: "User Name", userAvatar: "") } + public func SSOlogin(jwtToken: String) async throws -> User { + return User(id: 1, username: "User", email: "email@gmail.com", name: "User Name", userAvatar: "") + } + func resetPassword(email: String) async throws -> ResetPassword { ResetPassword(success: true, responseText: "Success reset") } diff --git a/Core/Core/Domain/AuthInteractor.swift b/Core/Core/Domain/AuthInteractor.swift index 45868cbc9..dd4b1cf49 100644 --- a/Core/Core/Domain/AuthInteractor.swift +++ b/Core/Core/Domain/AuthInteractor.swift @@ -13,6 +13,7 @@ public protocol AuthInteractorProtocol { func login(username: String, password: String) async throws -> User @discardableResult func login(externalToken: String, backend: String) async throws -> User + func SSOlogin(jwtToken: String) async throws -> User func resetPassword(email: String) async throws -> ResetPassword func getCookies(force: Bool) async throws func getRegistrationFields() async throws -> [PickerFields] @@ -37,6 +38,11 @@ public class AuthInteractor: AuthInteractorProtocol { return try await repository.login(externalToken: externalToken, backend: backend) } + @discardableResult + public func SSOlogin(jwtToken: String) async throws -> User { + return try await repository.SSOlogin(jwtToken: jwtToken) + } + public func resetPassword(email: String) async throws -> ResetPassword { try await repository.resetPassword(email: email) } diff --git a/Core/Core/SwiftGen/Strings.swift b/Core/Core/SwiftGen/Strings.swift index 4bd41f9eb..464d9a99d 100644 --- a/Core/Core/SwiftGen/Strings.swift +++ b/Core/Core/SwiftGen/Strings.swift @@ -14,8 +14,6 @@ public enum CoreLocalization { public static let done = CoreLocalization.tr("Localizable", "DONE", fallback: "Done") /// View in Safari public static let openInBrowser = CoreLocalization.tr("Localizable", "OPEN_IN_BROWSER", fallback: "View in Safari") - /// Register - public static let register = CoreLocalization.tr("Localizable", "REGISTER", fallback: "Register") /// The user canceled the sign-in flow. public static let socialSignCanceled = CoreLocalization.tr("Localizable", "SOCIAL_SIGN_CANCELED", fallback: "The user canceled the sign-in flow.") /// Tomorrow @@ -220,6 +218,10 @@ public enum CoreLocalization { public enum SignIn { /// Sign in public static let logInBtn = CoreLocalization.tr("Localizable", "SIGN_IN.LOG_IN_BTN", fallback: "Sign in") + /// Sign in with SSO + public static let logInWithSsoBtn = CoreLocalization.tr("Localizable", "SIGN_IN.LOG_IN_WITH_SSO_BTN", fallback: "Sign in with SSO") + /// Register + public static let registerBtn = CoreLocalization.tr("Localizable", "SIGN_IN.REGISTER_BTN", fallback: "Register") } public enum View { public enum Snackbar { diff --git a/Core/Core/View/Base/LogistrationBottomView.swift b/Core/Core/View/Base/LogistrationBottomView.swift index fc0aa0ef4..faa3aaa35 100644 --- a/Core/Core/View/Base/LogistrationBottomView.swift +++ b/Core/Core/View/Base/LogistrationBottomView.swift @@ -15,14 +15,11 @@ public enum LogistrationSourceScreen: Equatable { case discovery case courseDetail(String, String) case programDetails(String) - - public var value: String? { - return String(describing: self).components(separatedBy: "(").first - } } public enum LogistrationAction { case signIn + case signInWithSSO case register } @@ -34,11 +31,11 @@ public struct LogistrationBottomView: View { public init(_ action: @escaping (LogistrationAction) -> Void) { self.action = action } - + public var body: some View { VStack(alignment: .leading) { HStack(spacing: 24) { - StyledButton(CoreLocalization.register) { + StyledButton(CoreLocalization.SignIn.registerBtn) { action(.register) } .accessibilityIdentifier("logistration_register_button") @@ -54,6 +51,18 @@ public struct LogistrationBottomView: View { ) .frame(width: 100) .accessibilityIdentifier("logistration_signin_button") + + StyledButton( + CoreLocalization.SignIn.logInWithSsoBtn, + action: { + action(.signInWithSSO) + }, + color: Theme.Colors.white, + textColor: Theme.Colors.secondaryButtonTextColor, + borderColor: Theme.Colors.secondaryButtonBorderColor + ) + .frame(width: 100) + .accessibilityIdentifier("logistration_signin_withsso_button") } .padding(.horizontal, isHorizontal ? 0 : 0) } diff --git a/Core/Core/en.lproj/Localizable.strings b/Core/Core/en.lproj/Localizable.strings index b1fda17c5..9eed367a5 100644 --- a/Core/Core/en.lproj/Localizable.strings +++ b/Core/Core/en.lproj/Localizable.strings @@ -114,7 +114,8 @@ "SOCIAL_SIGN_CANCELED" = "The user canceled the sign-in flow."; "SIGN_IN.LOG_IN_BTN" = "Sign in"; -"REGISTER" = "Register"; +"SIGN_IN.REGISTER_BTN" = "Register"; +"SIGN_IN.LOG_IN_WITH_SSO_BTN" = "Sign in with SSO"; "TOMORROW" = "Tomorrow"; "YESTERDAY" = "Yesterday"; diff --git a/Course/CourseTests/CourseMock.generated.swift b/Course/CourseTests/CourseMock.generated.swift index 067556faf..030b94953 100644 --- a/Course/CourseTests/CourseMock.generated.swift +++ b/Course/CourseTests/CourseMock.generated.swift @@ -77,7 +77,24 @@ open class AuthInteractorProtocolMock: AuthInteractorProtocol, Mock { } @discardableResult - open func login(externalToken: String, backend: String) throws -> User { + open func SSOlogin(jwtToken: String) async throws -> Core.User { + addInvocation(.m_login__SSO__username_password(Parameter.value(`jwtToken`))) + let perform = methodPerformValue(.m_login__SSO__username_password(Parameter.value(`jwtToken`))) as? (String) -> Void + perform?(`jwtToken`) + var __value: User + do { + __value = try methodReturnValue(.m_login__SSO__username_password(Parameter.value(jwtToken))).casted() + } catch MockError.notStubed { + onFatalFailure("Stub return value not specified for login(username: String, password: String). Use given") + Failure("Stub return value not specified for login(username: String, password: String). Use given") + } catch { + throw error + } + return __value + } + + @discardableResult + open func login(externalToken: String, backend: String) throws -> User { addInvocation(.m_login__externalToken_externalTokenbackend_backend(Parameter.value(`externalToken`), Parameter.value(`backend`))) let perform = methodPerformValue(.m_login__externalToken_externalTokenbackend_backend(Parameter.value(`externalToken`), Parameter.value(`backend`))) as? (String, String) -> Void perform?(`externalToken`, `backend`) @@ -173,6 +190,7 @@ open class AuthInteractorProtocolMock: AuthInteractorProtocol, Mock { fileprivate enum MethodType { case m_login__username_usernamepassword_password(Parameter, Parameter) + case m_login__SSO__username_password(Parameter) case m_login__externalToken_externalTokenbackend_backend(Parameter, Parameter) case m_resetPassword__email_email(Parameter) case m_getCookies__force_force(Parameter) @@ -223,6 +241,7 @@ open class AuthInteractorProtocolMock: AuthInteractorProtocol, Mock { func intValue() -> Int { switch self { case let .m_login__username_usernamepassword_password(p0, p1): return p0.intValue + p1.intValue + case let .m_login__SSO__username_password(p0): return p0.intValue case let .m_login__externalToken_externalTokenbackend_backend(p0, p1): return p0.intValue + p1.intValue case let .m_resetPassword__email_email(p0): return p0.intValue case let .m_getCookies__force_force(p0): return p0.intValue @@ -234,6 +253,7 @@ open class AuthInteractorProtocolMock: AuthInteractorProtocol, Mock { func assertionName() -> String { switch self { case .m_login__username_usernamepassword_password: return ".login(username:password:)" + case .m_login__SSO__username_password: return ".loginSSO(username:password:)" case .m_login__externalToken_externalTokenbackend_backend: return ".login(externalToken:backend:)" case .m_resetPassword__email_email: return ".resetPassword(email:)" case .m_getCookies__force_force: return ".getCookies(force:)" @@ -577,10 +597,16 @@ open class BaseRouterMock: BaseRouter, Mock { open func showWebBrowser(title: String, url: URL) { addInvocation(.m_showWebBrowser__title_titleurl_url(Parameter.value(`title`), Parameter.value(`url`))) - let perform = methodPerformValue(.m_showWebBrowser__title_titleurl_url(Parameter.value(`title`), Parameter.value(`url`))) as? (String, URL) -> Void - perform?(`title`, `url`) + let perform = methodPerformValue(.m_showWebBrowser__title_titleurl_url(Parameter.value(`title`), Parameter.value(`url`))) as? (String, URL) -> Void + perform?(`title`, `url`) } - + + open func showSSOWebBrowser(title: String) { + addInvocation(.m_showWebBrowser__SSO(Parameter.value(`title`))) + let perform = methodPerformValue(.m_showWebBrowser__SSO(Parameter.value(`title`))) as? (String) -> Void + perform?(`title`) + } + open func presentAlert(alertTitle: String, alertMessage: String, positiveAction: String, onCloseTapped: @escaping () -> Void, okTapped: @escaping () -> Void, type: AlertViewType) { addInvocation(.m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type(Parameter.value(`alertTitle`), Parameter.value(`alertMessage`), Parameter.value(`positiveAction`), Parameter<() -> Void>.value(`onCloseTapped`), Parameter<() -> Void>.value(`okTapped`), Parameter.value(`type`))) let perform = methodPerformValue(.m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type(Parameter.value(`alertTitle`), Parameter.value(`alertMessage`), Parameter.value(`positiveAction`), Parameter<() -> Void>.value(`onCloseTapped`), Parameter<() -> Void>.value(`okTapped`), Parameter.value(`type`))) as? (String, String, String, @escaping () -> Void, @escaping () -> Void, AlertViewType) -> Void @@ -619,6 +645,7 @@ open class BaseRouterMock: BaseRouter, Mock { case m_showForgotPasswordScreen case m_showDiscoveryScreen__searchQuery_searchQuerysourceScreen_sourceScreen(Parameter, Parameter) case m_showWebBrowser__title_titleurl_url(Parameter, Parameter) + case m_showWebBrowser__SSO(Parameter) case m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type(Parameter, Parameter, Parameter, Parameter<() -> Void>, Parameter<() -> Void>, Parameter) case m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagenextSectionName_nextSectionNameaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTappednextSectionTapped_nextSectionTapped(Parameter, Parameter, Parameter, Parameter, Parameter, Parameter<() -> Void>, Parameter<() -> Void>, Parameter<() -> Void>) case m_presentView__transitionStyle_transitionStyleview_viewcompletion_completion(Parameter, Parameter, Parameter<(() -> Void)?>) @@ -732,6 +759,7 @@ open class BaseRouterMock: BaseRouter, Mock { case .m_showForgotPasswordScreen: return 0 case let .m_showDiscoveryScreen__searchQuery_searchQuerysourceScreen_sourceScreen(p0, p1): return p0.intValue + p1.intValue case let .m_showWebBrowser__title_titleurl_url(p0, p1): return p0.intValue + p1.intValue + case let .m_showWebBrowser__SSO(p0): return p0.intValue case let .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type(p0, p1, p2, p3, p4, p5): return p0.intValue + p1.intValue + p2.intValue + p3.intValue + p4.intValue + p5.intValue case let .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagenextSectionName_nextSectionNameaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTappednextSectionTapped_nextSectionTapped(p0, p1, p2, p3, p4, p5, p6, p7): return p0.intValue + p1.intValue + p2.intValue + p3.intValue + p4.intValue + p5.intValue + p6.intValue + p7.intValue case let .m_presentView__transitionStyle_transitionStyleview_viewcompletion_completion(p0, p1, p2): return p0.intValue + p1.intValue + p2.intValue @@ -752,6 +780,7 @@ open class BaseRouterMock: BaseRouter, Mock { case .m_showForgotPasswordScreen: return ".showForgotPasswordScreen()" case .m_showDiscoveryScreen__searchQuery_searchQuerysourceScreen_sourceScreen: return ".showDiscoveryScreen(searchQuery:sourceScreen:)" case .m_showWebBrowser__title_titleurl_url: return ".showWebBrowser(title:url:)" + case .m_showWebBrowser__SSO: return ".showSSOWebBrowser(title:)" case .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type: return ".presentAlert(alertTitle:alertMessage:positiveAction:onCloseTapped:okTapped:type:)" case .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagenextSectionName_nextSectionNameaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTappednextSectionTapped_nextSectionTapped: return ".presentAlert(alertTitle:alertMessage:nextSectionName:action:image:onCloseTapped:okTapped:nextSectionTapped:)" case .m_presentView__transitionStyle_transitionStyleview_viewcompletion_completion: return ".presentView(transitionStyle:view:completion:)" diff --git a/Dashboard/DashboardTests/DashboardMock.generated.swift b/Dashboard/DashboardTests/DashboardMock.generated.swift index 82ae9be00..00bc61a44 100644 --- a/Dashboard/DashboardTests/DashboardMock.generated.swift +++ b/Dashboard/DashboardTests/DashboardMock.generated.swift @@ -93,6 +93,23 @@ open class AuthInteractorProtocolMock: AuthInteractorProtocol, Mock { return __value } + @discardableResult + open func SSOlogin(jwtToken: String) async throws -> Core.User { + addInvocation(.m_login__SSO__username_password(Parameter.value(`jwtToken`))) + let perform = methodPerformValue(.m_login__SSO__username_password(Parameter.value(`jwtToken`))) as? (String) -> Void + perform?(`jwtToken`) + var __value: User + do { + __value = try methodReturnValue(.m_login__SSO__username_password(Parameter.value(jwtToken))).casted() + } catch MockError.notStubed { + onFatalFailure("Stub return value not specified for login(username: String, password: String). Use given") + Failure("Stub return value not specified for login(username: String, password: String). Use given") + } catch { + throw error + } + return __value + } + open func resetPassword(email: String) throws -> ResetPassword { addInvocation(.m_resetPassword__email_email(Parameter.value(`email`))) let perform = methodPerformValue(.m_resetPassword__email_email(Parameter.value(`email`))) as? (String) -> Void @@ -173,6 +190,7 @@ open class AuthInteractorProtocolMock: AuthInteractorProtocol, Mock { fileprivate enum MethodType { case m_login__username_usernamepassword_password(Parameter, Parameter) + case m_login__SSO__username_password(Parameter) case m_login__externalToken_externalTokenbackend_backend(Parameter, Parameter) case m_resetPassword__email_email(Parameter) case m_getCookies__force_force(Parameter) @@ -223,6 +241,7 @@ open class AuthInteractorProtocolMock: AuthInteractorProtocol, Mock { func intValue() -> Int { switch self { case let .m_login__username_usernamepassword_password(p0, p1): return p0.intValue + p1.intValue + case let .m_login__SSO__username_password(p0): return p0.intValue case let .m_login__externalToken_externalTokenbackend_backend(p0, p1): return p0.intValue + p1.intValue case let .m_resetPassword__email_email(p0): return p0.intValue case let .m_getCookies__force_force(p0): return p0.intValue @@ -234,6 +253,7 @@ open class AuthInteractorProtocolMock: AuthInteractorProtocol, Mock { func assertionName() -> String { switch self { case .m_login__username_usernamepassword_password: return ".login(username:password:)" + case .m_login__SSO__username_password: return ".loginSSO(username:password:)" case .m_login__externalToken_externalTokenbackend_backend: return ".login(externalToken:backend:)" case .m_resetPassword__email_email: return ".resetPassword(email:)" case .m_getCookies__force_force: return ".getCookies(force:)" @@ -581,6 +601,12 @@ open class BaseRouterMock: BaseRouter, Mock { perform?(`title`, `url`) } + open func showSSOWebBrowser(title: String) { + addInvocation(.m_showWebBrowser__SSO(Parameter.value(`title`))) + let perform = methodPerformValue(.m_showWebBrowser__SSO(Parameter.value(`title`))) as? (String) -> Void + perform?(`title`) + } + open func presentAlert(alertTitle: String, alertMessage: String, positiveAction: String, onCloseTapped: @escaping () -> Void, okTapped: @escaping () -> Void, type: AlertViewType) { addInvocation(.m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type(Parameter.value(`alertTitle`), Parameter.value(`alertMessage`), Parameter.value(`positiveAction`), Parameter<() -> Void>.value(`onCloseTapped`), Parameter<() -> Void>.value(`okTapped`), Parameter.value(`type`))) let perform = methodPerformValue(.m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type(Parameter.value(`alertTitle`), Parameter.value(`alertMessage`), Parameter.value(`positiveAction`), Parameter<() -> Void>.value(`onCloseTapped`), Parameter<() -> Void>.value(`okTapped`), Parameter.value(`type`))) as? (String, String, String, @escaping () -> Void, @escaping () -> Void, AlertViewType) -> Void @@ -619,6 +645,7 @@ open class BaseRouterMock: BaseRouter, Mock { case m_showForgotPasswordScreen case m_showDiscoveryScreen__searchQuery_searchQuerysourceScreen_sourceScreen(Parameter, Parameter) case m_showWebBrowser__title_titleurl_url(Parameter, Parameter) + case m_showWebBrowser__SSO(Parameter) case m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type(Parameter, Parameter, Parameter, Parameter<() -> Void>, Parameter<() -> Void>, Parameter) case m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagenextSectionName_nextSectionNameaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTappednextSectionTapped_nextSectionTapped(Parameter, Parameter, Parameter, Parameter, Parameter, Parameter<() -> Void>, Parameter<() -> Void>, Parameter<() -> Void>) case m_presentView__transitionStyle_transitionStyleview_viewcompletion_completion(Parameter, Parameter, Parameter<(() -> Void)?>) @@ -732,6 +759,7 @@ open class BaseRouterMock: BaseRouter, Mock { case .m_showForgotPasswordScreen: return 0 case let .m_showDiscoveryScreen__searchQuery_searchQuerysourceScreen_sourceScreen(p0, p1): return p0.intValue + p1.intValue case let .m_showWebBrowser__title_titleurl_url(p0, p1): return p0.intValue + p1.intValue + case let .m_showWebBrowser__SSO(p0): return p0.intValue case let .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type(p0, p1, p2, p3, p4, p5): return p0.intValue + p1.intValue + p2.intValue + p3.intValue + p4.intValue + p5.intValue case let .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagenextSectionName_nextSectionNameaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTappednextSectionTapped_nextSectionTapped(p0, p1, p2, p3, p4, p5, p6, p7): return p0.intValue + p1.intValue + p2.intValue + p3.intValue + p4.intValue + p5.intValue + p6.intValue + p7.intValue case let .m_presentView__transitionStyle_transitionStyleview_viewcompletion_completion(p0, p1, p2): return p0.intValue + p1.intValue + p2.intValue @@ -752,6 +780,7 @@ open class BaseRouterMock: BaseRouter, Mock { case .m_showForgotPasswordScreen: return ".showForgotPasswordScreen()" case .m_showDiscoveryScreen__searchQuery_searchQuerysourceScreen_sourceScreen: return ".showDiscoveryScreen(searchQuery:sourceScreen:)" case .m_showWebBrowser__title_titleurl_url: return ".showWebBrowser(title:url:)" + case .m_showWebBrowser__SSO: return ".showSSOWebBrowser(title:)" case .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type: return ".presentAlert(alertTitle:alertMessage:positiveAction:onCloseTapped:okTapped:type:)" case .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagenextSectionName_nextSectionNameaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTappednextSectionTapped_nextSectionTapped: return ".presentAlert(alertTitle:alertMessage:nextSectionName:action:image:onCloseTapped:okTapped:nextSectionTapped:)" case .m_presentView__transitionStyle_transitionStyleview_viewcompletion_completion: return ".presentView(transitionStyle:view:completion:)" diff --git a/Discovery/Discovery/Presentation/NativeDiscovery/CourseDetailsView.swift b/Discovery/Discovery/Presentation/NativeDiscovery/CourseDetailsView.swift index 80864b8fd..043b1abee 100644 --- a/Discovery/Discovery/Presentation/NativeDiscovery/CourseDetailsView.swift +++ b/Discovery/Discovery/Presentation/NativeDiscovery/CourseDetailsView.swift @@ -162,6 +162,13 @@ public struct CourseDetailsView: View { viewModel.courseDetails?.courseTitle ?? "" ) ) + case .signInWithSSO: + viewModel.router.showLoginScreen( + sourceScreen: .courseDetail( + courseID, + viewModel.courseDetails?.courseTitle ?? "" + ) + ) } } } diff --git a/Discovery/Discovery/Presentation/NativeDiscovery/DiscoveryView.swift b/Discovery/Discovery/Presentation/NativeDiscovery/DiscoveryView.swift index b8d9aa860..225e20e58 100644 --- a/Discovery/Discovery/Presentation/NativeDiscovery/DiscoveryView.swift +++ b/Discovery/Discovery/Presentation/NativeDiscovery/DiscoveryView.swift @@ -151,6 +151,8 @@ public struct DiscoveryView: View { viewModel.router.showLoginScreen(sourceScreen: .discovery) case .register: viewModel.router.showRegisterScreen(sourceScreen: .discovery) + case .signInWithSSO: + viewModel.router.showLoginScreen(sourceScreen: .discovery) } } } diff --git a/Discovery/Discovery/Presentation/WebDiscovery/DiscoveryWebview.swift b/Discovery/Discovery/Presentation/WebDiscovery/DiscoveryWebview.swift index b69bb3af9..e7105f508 100644 --- a/Discovery/Discovery/Presentation/WebDiscovery/DiscoveryWebview.swift +++ b/Discovery/Discovery/Presentation/WebDiscovery/DiscoveryWebview.swift @@ -14,7 +14,7 @@ public enum DiscoveryWebviewType: Equatable { case discovery case courseDetail(String) case programDetail(String) - + var rawValue: String { switch self { case .discovery: @@ -103,7 +103,7 @@ public struct DiscoveryWebview: View { webViewType: discoveryType.rawValue ) .accessibilityIdentifier("discovery_webview") - + if isLoading || viewModel.showProgress { HStack(alignment: .center) { ProgressBar( @@ -115,7 +115,7 @@ public struct DiscoveryWebview: View { } .frame(width: proxy.size.width, height: proxy.size.height) } - + // MARK: - Show Error if viewModel.showError { VStack { @@ -129,19 +129,21 @@ public struct DiscoveryWebview: View { } } } - + if !viewModel.userloggedIn, !isLoading { LogistrationBottomView { buttonAction in switch buttonAction { case .signIn: viewModel.router.showLoginScreen(sourceScreen: sourceScreen) + case .signInWithSSO: + viewModel.router.showLoginScreen(sourceScreen: sourceScreen) case .register: viewModel.router.showRegisterScreen(sourceScreen: sourceScreen) } } } } - + if viewModel.webViewError { FullScreenErrorView( type: viewModel.connectivity.isInternetAvaliable ? .generic : .noInternetWithReload diff --git a/Discovery/Discovery/Presentation/WebDiscovery/DiscoveryWebviewViewModel.swift b/Discovery/Discovery/Presentation/WebDiscovery/DiscoveryWebviewViewModel.swift index b9f2bb515..df2330abf 100644 --- a/Discovery/Discovery/Presentation/WebDiscovery/DiscoveryWebviewViewModel.swift +++ b/Discovery/Discovery/Presentation/WebDiscovery/DiscoveryWebviewViewModel.swift @@ -15,7 +15,7 @@ public class DiscoveryWebviewViewModel: ObservableObject { @Published private(set) var showProgress = false @Published var showError: Bool = false @Published var webViewError: Bool = false - + var errorMessage: String? { didSet { withAnimation { @@ -137,25 +137,14 @@ extension DiscoveryWebviewViewModel: WebViewNavigationDelegate { } if let url = request.url, outsideLink || capturedLink || externalLink, UIApplication.shared.canOpenURL(url) { - analytics.externalLinkOpen(url: url.absoluteString, screen: sourceScreen.value ?? "") router.presentAlert( alertTitle: DiscoveryLocalization.Alert.leavingAppTitle, alertMessage: DiscoveryLocalization.Alert.leavingAppMessage, positiveAction: CoreLocalization.Webview.Alert.continue, onCloseTapped: { [weak self] in self?.router.dismiss(animated: true) - self?.analytics.externalLinkOpenAction( - url: url.absoluteString, - screen: self?.sourceScreen.value ?? "", - action: "cancel" - ) - }, okTapped: { [weak self] in + }, okTapped: { UIApplication.shared.open(url, options: [:]) - self?.analytics.externalLinkOpenAction( - url: url.absoluteString, - screen: self?.sourceScreen.value ?? "", - action: "continue" - ) }, type: .default(positiveAction: CoreLocalization.Webview.Alert.continue, image: nil) ) return true @@ -249,7 +238,7 @@ extension DiscoveryWebviewViewModel: WebViewNavigationDelegate { private func isValidAppURLScheme(_ url: URL) -> Bool { return url.scheme ?? "" == config.URIScheme } - + public func showWebViewError() { self.webViewError = true } diff --git a/Discovery/DiscoveryTests/DiscoveryMock.generated.swift b/Discovery/DiscoveryTests/DiscoveryMock.generated.swift index 1bcdcff78..9c6b87e6b 100644 --- a/Discovery/DiscoveryTests/DiscoveryMock.generated.swift +++ b/Discovery/DiscoveryTests/DiscoveryMock.generated.swift @@ -93,6 +93,23 @@ open class AuthInteractorProtocolMock: AuthInteractorProtocol, Mock { return __value } + @discardableResult + open func SSOlogin(jwtToken: String) async throws -> Core.User { + addInvocation(.m_login__SSO__username_password(Parameter.value(`jwtToken`))) + let perform = methodPerformValue(.m_login__SSO__username_password(Parameter.value(`jwtToken`))) as? (String) -> Void + perform?(`jwtToken`) + var __value: User + do { + __value = try methodReturnValue(.m_login__SSO__username_password(Parameter.value(jwtToken))).casted() + } catch MockError.notStubed { + onFatalFailure("Stub return value not specified for login(username: String, password: String). Use given") + Failure("Stub return value not specified for login(username: String, password: String). Use given") + } catch { + throw error + } + return __value + } + open func resetPassword(email: String) throws -> ResetPassword { addInvocation(.m_resetPassword__email_email(Parameter.value(`email`))) let perform = methodPerformValue(.m_resetPassword__email_email(Parameter.value(`email`))) as? (String) -> Void @@ -173,6 +190,7 @@ open class AuthInteractorProtocolMock: AuthInteractorProtocol, Mock { fileprivate enum MethodType { case m_login__username_usernamepassword_password(Parameter, Parameter) + case m_login__SSO__username_password(Parameter) case m_login__externalToken_externalTokenbackend_backend(Parameter, Parameter) case m_resetPassword__email_email(Parameter) case m_getCookies__force_force(Parameter) @@ -188,6 +206,11 @@ open class AuthInteractorProtocolMock: AuthInteractorProtocol, Mock { results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsPassword, rhs: rhsPassword, with: matcher), lhsPassword, rhsPassword, "password")) return Matcher.ComparisonResult(results) + case (.m_login__SSO__username_password(let lhsJwtToken), .m_login__SSO__username_password(let rhsJwtToken)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsJwtToken, rhs: rhsJwtToken, with: matcher), lhsJwtToken, rhsJwtToken, "jwtToken")) + return Matcher.ComparisonResult(results) + case (.m_login__externalToken_externalTokenbackend_backend(let lhsExternaltoken, let lhsBackend), .m_login__externalToken_externalTokenbackend_backend(let rhsExternaltoken, let rhsBackend)): var results: [Matcher.ParameterComparisonResult] = [] results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsExternaltoken, rhs: rhsExternaltoken, with: matcher), lhsExternaltoken, rhsExternaltoken, "externalToken")) @@ -223,6 +246,7 @@ open class AuthInteractorProtocolMock: AuthInteractorProtocol, Mock { func intValue() -> Int { switch self { case let .m_login__username_usernamepassword_password(p0, p1): return p0.intValue + p1.intValue + case let .m_login__SSO__username_password(p0): return p0.intValue case let .m_login__externalToken_externalTokenbackend_backend(p0, p1): return p0.intValue + p1.intValue case let .m_resetPassword__email_email(p0): return p0.intValue case let .m_getCookies__force_force(p0): return p0.intValue @@ -234,6 +258,7 @@ open class AuthInteractorProtocolMock: AuthInteractorProtocol, Mock { func assertionName() -> String { switch self { case .m_login__username_usernamepassword_password: return ".login(username:password:)" + case .m_login__SSO__username_password: return ".loginSSO(username:password:)" case .m_login__externalToken_externalTokenbackend_backend: return ".login(externalToken:backend:)" case .m_resetPassword__email_email: return ".resetPassword(email:)" case .m_getCookies__force_force: return ".getCookies(force:)" @@ -261,6 +286,10 @@ open class AuthInteractorProtocolMock: AuthInteractorProtocol, Mock { public static func login(externalToken: Parameter, backend: Parameter, willReturn: User...) -> MethodStub { return Given(method: .m_login__externalToken_externalTokenbackend_backend(`externalToken`, `backend`), products: willReturn.map({ StubProduct.return($0 as Any) })) } + @discardableResult + public static func ssoLogin(title: Parameter, willReturn: User...) -> MethodStub { + return Given(method: .m_login__SSO__username_password(`title`), products: willReturn.map({ StubProduct.return($0 as Any) })) + } public static func resetPassword(email: Parameter, willReturn: ResetPassword...) -> MethodStub { return Given(method: .m_resetPassword__email_email(`email`), products: willReturn.map({ StubProduct.return($0 as Any) })) } @@ -353,7 +382,8 @@ open class AuthInteractorProtocolMock: AuthInteractorProtocol, Mock { fileprivate var method: MethodType @discardableResult - public static func login(username: Parameter, password: Parameter) -> Verify { return Verify(method: .m_login__username_usernamepassword_password(`username`, `password`))} + public static func login(username: Parameter, password: Parameter) -> Verify { return Verify(method: .m_login__username_usernamepassword_password(`username`, `password`))} + public static func ssoLogin(title: Parameter) -> Verify { return Verify(method: .m_login__SSO__username_password(`title`))} @discardableResult public static func login(externalToken: Parameter, backend: Parameter) -> Verify { return Verify(method: .m_login__externalToken_externalTokenbackend_backend(`externalToken`, `backend`))} public static func resetPassword(email: Parameter) -> Verify { return Verify(method: .m_resetPassword__email_email(`email`))} @@ -581,6 +611,12 @@ open class BaseRouterMock: BaseRouter, Mock { perform?(`title`, `url`) } + open func showSSOWebBrowser(title: String) { + addInvocation(.m_showWebBrowser__SSO(Parameter.value(`title`))) + let perform = methodPerformValue(.m_showWebBrowser__SSO(Parameter.value(`title`))) as? (String) -> Void + perform?(`title`) + } + open func presentAlert(alertTitle: String, alertMessage: String, positiveAction: String, onCloseTapped: @escaping () -> Void, okTapped: @escaping () -> Void, type: AlertViewType) { addInvocation(.m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type(Parameter.value(`alertTitle`), Parameter.value(`alertMessage`), Parameter.value(`positiveAction`), Parameter<() -> Void>.value(`onCloseTapped`), Parameter<() -> Void>.value(`okTapped`), Parameter.value(`type`))) let perform = methodPerformValue(.m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type(Parameter.value(`alertTitle`), Parameter.value(`alertMessage`), Parameter.value(`positiveAction`), Parameter<() -> Void>.value(`onCloseTapped`), Parameter<() -> Void>.value(`okTapped`), Parameter.value(`type`))) as? (String, String, String, @escaping () -> Void, @escaping () -> Void, AlertViewType) -> Void @@ -619,6 +655,7 @@ open class BaseRouterMock: BaseRouter, Mock { case m_showForgotPasswordScreen case m_showDiscoveryScreen__searchQuery_searchQuerysourceScreen_sourceScreen(Parameter, Parameter) case m_showWebBrowser__title_titleurl_url(Parameter, Parameter) + case m_showWebBrowser__SSO(Parameter) case m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type(Parameter, Parameter, Parameter, Parameter<() -> Void>, Parameter<() -> Void>, Parameter) case m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagenextSectionName_nextSectionNameaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTappednextSectionTapped_nextSectionTapped(Parameter, Parameter, Parameter, Parameter, Parameter, Parameter<() -> Void>, Parameter<() -> Void>, Parameter<() -> Void>) case m_presentView__transitionStyle_transitionStyleview_viewcompletion_completion(Parameter, Parameter, Parameter<(() -> Void)?>) @@ -732,6 +769,7 @@ open class BaseRouterMock: BaseRouter, Mock { case .m_showForgotPasswordScreen: return 0 case let .m_showDiscoveryScreen__searchQuery_searchQuerysourceScreen_sourceScreen(p0, p1): return p0.intValue + p1.intValue case let .m_showWebBrowser__title_titleurl_url(p0, p1): return p0.intValue + p1.intValue + case let .m_showWebBrowser__SSO(p0): return p0.intValue case let .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type(p0, p1, p2, p3, p4, p5): return p0.intValue + p1.intValue + p2.intValue + p3.intValue + p4.intValue + p5.intValue case let .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagenextSectionName_nextSectionNameaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTappednextSectionTapped_nextSectionTapped(p0, p1, p2, p3, p4, p5, p6, p7): return p0.intValue + p1.intValue + p2.intValue + p3.intValue + p4.intValue + p5.intValue + p6.intValue + p7.intValue case let .m_presentView__transitionStyle_transitionStyleview_viewcompletion_completion(p0, p1, p2): return p0.intValue + p1.intValue + p2.intValue @@ -752,6 +790,7 @@ open class BaseRouterMock: BaseRouter, Mock { case .m_showForgotPasswordScreen: return ".showForgotPasswordScreen()" case .m_showDiscoveryScreen__searchQuery_searchQuerysourceScreen_sourceScreen: return ".showDiscoveryScreen(searchQuery:sourceScreen:)" case .m_showWebBrowser__title_titleurl_url: return ".showWebBrowser(title:url:)" + case .m_showWebBrowser__SSO: return ".showSSOWebBrowser(title:)" case .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type: return ".presentAlert(alertTitle:alertMessage:positiveAction:onCloseTapped:okTapped:type:)" case .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagenextSectionName_nextSectionNameaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTappednextSectionTapped_nextSectionTapped: return ".presentAlert(alertTitle:alertMessage:nextSectionName:action:image:onCloseTapped:okTapped:nextSectionTapped:)" case .m_presentView__transitionStyle_transitionStyleview_viewcompletion_completion: return ".presentView(transitionStyle:view:completion:)" diff --git a/Discussion/DiscussionTests/DiscussionMock.generated.swift b/Discussion/DiscussionTests/DiscussionMock.generated.swift index 82022c6aa..1a28f52f2 100644 --- a/Discussion/DiscussionTests/DiscussionMock.generated.swift +++ b/Discussion/DiscussionTests/DiscussionMock.generated.swift @@ -76,6 +76,23 @@ open class AuthInteractorProtocolMock: AuthInteractorProtocol, Mock { return __value } + @discardableResult + open func SSOlogin(jwtToken: String) async throws -> Core.User { + addInvocation(.m_login__SSO__username_password(Parameter.value(`jwtToken`))) + let perform = methodPerformValue(.m_login__SSO__username_password(Parameter.value(`jwtToken`))) as? (String) -> Void + perform?(`jwtToken`) + var __value: User + do { + __value = try methodReturnValue(.m_login__SSO__username_password(Parameter.value(jwtToken))).casted() + } catch MockError.notStubed { + onFatalFailure("Stub return value not specified for login(username: String, password: String). Use given") + Failure("Stub return value not specified for login(username: String, password: String). Use given") + } catch { + throw error + } + return __value + } + @discardableResult open func login(externalToken: String, backend: String) throws -> User { addInvocation(.m_login__externalToken_externalTokenbackend_backend(Parameter.value(`externalToken`), Parameter.value(`backend`))) @@ -173,6 +190,7 @@ open class AuthInteractorProtocolMock: AuthInteractorProtocol, Mock { fileprivate enum MethodType { case m_login__username_usernamepassword_password(Parameter, Parameter) + case m_login__SSO__username_password(Parameter) case m_login__externalToken_externalTokenbackend_backend(Parameter, Parameter) case m_resetPassword__email_email(Parameter) case m_getCookies__force_force(Parameter) @@ -223,6 +241,7 @@ open class AuthInteractorProtocolMock: AuthInteractorProtocol, Mock { func intValue() -> Int { switch self { case let .m_login__username_usernamepassword_password(p0, p1): return p0.intValue + p1.intValue + case let .m_login__SSO__username_password(p0): return p0.intValue case let .m_login__externalToken_externalTokenbackend_backend(p0, p1): return p0.intValue + p1.intValue case let .m_resetPassword__email_email(p0): return p0.intValue case let .m_getCookies__force_force(p0): return p0.intValue @@ -234,6 +253,7 @@ open class AuthInteractorProtocolMock: AuthInteractorProtocol, Mock { func assertionName() -> String { switch self { case .m_login__username_usernamepassword_password: return ".login(username:password:)" + case .m_login__SSO__username_password: return ".loginSSO(username:password:)" case .m_login__externalToken_externalTokenbackend_backend: return ".login(externalToken:backend:)" case .m_resetPassword__email_email: return ".resetPassword(email:)" case .m_getCookies__force_force: return ".getCookies(force:)" @@ -581,6 +601,12 @@ open class BaseRouterMock: BaseRouter, Mock { perform?(`title`, `url`) } + open func showSSOWebBrowser(title: String) { + addInvocation(.m_showWebBrowser__SSO(Parameter.value(`title`))) + let perform = methodPerformValue(.m_showWebBrowser__SSO(Parameter.value(`title`))) as? (String) -> Void + perform?(`title`) + } + open func presentAlert(alertTitle: String, alertMessage: String, positiveAction: String, onCloseTapped: @escaping () -> Void, okTapped: @escaping () -> Void, type: AlertViewType) { addInvocation(.m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type(Parameter.value(`alertTitle`), Parameter.value(`alertMessage`), Parameter.value(`positiveAction`), Parameter<() -> Void>.value(`onCloseTapped`), Parameter<() -> Void>.value(`okTapped`), Parameter.value(`type`))) let perform = methodPerformValue(.m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type(Parameter.value(`alertTitle`), Parameter.value(`alertMessage`), Parameter.value(`positiveAction`), Parameter<() -> Void>.value(`onCloseTapped`), Parameter<() -> Void>.value(`okTapped`), Parameter.value(`type`))) as? (String, String, String, @escaping () -> Void, @escaping () -> Void, AlertViewType) -> Void @@ -619,6 +645,7 @@ open class BaseRouterMock: BaseRouter, Mock { case m_showForgotPasswordScreen case m_showDiscoveryScreen__searchQuery_searchQuerysourceScreen_sourceScreen(Parameter, Parameter) case m_showWebBrowser__title_titleurl_url(Parameter, Parameter) + case m_showWebBrowser__SSO(Parameter) case m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type(Parameter, Parameter, Parameter, Parameter<() -> Void>, Parameter<() -> Void>, Parameter) case m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagenextSectionName_nextSectionNameaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTappednextSectionTapped_nextSectionTapped(Parameter, Parameter, Parameter, Parameter, Parameter, Parameter<() -> Void>, Parameter<() -> Void>, Parameter<() -> Void>) case m_presentView__transitionStyle_transitionStyleview_viewcompletion_completion(Parameter, Parameter, Parameter<(() -> Void)?>) @@ -732,6 +759,7 @@ open class BaseRouterMock: BaseRouter, Mock { case .m_showForgotPasswordScreen: return 0 case let .m_showDiscoveryScreen__searchQuery_searchQuerysourceScreen_sourceScreen(p0, p1): return p0.intValue + p1.intValue case let .m_showWebBrowser__title_titleurl_url(p0, p1): return p0.intValue + p1.intValue + case let .m_showWebBrowser__SSO(p0): return p0.intValue case let .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type(p0, p1, p2, p3, p4, p5): return p0.intValue + p1.intValue + p2.intValue + p3.intValue + p4.intValue + p5.intValue case let .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagenextSectionName_nextSectionNameaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTappednextSectionTapped_nextSectionTapped(p0, p1, p2, p3, p4, p5, p6, p7): return p0.intValue + p1.intValue + p2.intValue + p3.intValue + p4.intValue + p5.intValue + p6.intValue + p7.intValue case let .m_presentView__transitionStyle_transitionStyleview_viewcompletion_completion(p0, p1, p2): return p0.intValue + p1.intValue + p2.intValue @@ -752,6 +780,7 @@ open class BaseRouterMock: BaseRouter, Mock { case .m_showForgotPasswordScreen: return ".showForgotPasswordScreen()" case .m_showDiscoveryScreen__searchQuery_searchQuerysourceScreen_sourceScreen: return ".showDiscoveryScreen(searchQuery:sourceScreen:)" case .m_showWebBrowser__title_titleurl_url: return ".showWebBrowser(title:url:)" + case .m_showWebBrowser__SSO: return ".showSSOWebBrowser(title:)" case .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type: return ".presentAlert(alertTitle:alertMessage:positiveAction:onCloseTapped:okTapped:type:)" case .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagenextSectionName_nextSectionNameaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTappednextSectionTapped_nextSectionTapped: return ".presentAlert(alertTitle:alertMessage:nextSectionName:action:image:onCloseTapped:okTapped:nextSectionTapped:)" case .m_presentView__transitionStyle_transitionStyleview_viewcompletion_completion: return ".presentView(transitionStyle:view:completion:)" @@ -2704,6 +2733,12 @@ open class DiscussionRouterMock: DiscussionRouter, Mock { perform?(`title`, `url`) } + open func showSSOWebBrowser(title: String) { + addInvocation(.m_showWebBrowser__SSO(Parameter.value(`title`))) + let perform = methodPerformValue(.m_showWebBrowser__SSO(Parameter.value(`title`))) as? (String) -> Void + perform?(`title`) + } + open func presentAlert(alertTitle: String, alertMessage: String, positiveAction: String, onCloseTapped: @escaping () -> Void, okTapped: @escaping () -> Void, type: AlertViewType) { addInvocation(.m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type(Parameter.value(`alertTitle`), Parameter.value(`alertMessage`), Parameter.value(`positiveAction`), Parameter<() -> Void>.value(`onCloseTapped`), Parameter<() -> Void>.value(`okTapped`), Parameter.value(`type`))) let perform = methodPerformValue(.m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type(Parameter.value(`alertTitle`), Parameter.value(`alertMessage`), Parameter.value(`positiveAction`), Parameter<() -> Void>.value(`onCloseTapped`), Parameter<() -> Void>.value(`okTapped`), Parameter.value(`type`))) as? (String, String, String, @escaping () -> Void, @escaping () -> Void, AlertViewType) -> Void @@ -2748,6 +2783,7 @@ open class DiscussionRouterMock: DiscussionRouter, Mock { case m_showForgotPasswordScreen case m_showDiscoveryScreen__searchQuery_searchQuerysourceScreen_sourceScreen(Parameter, Parameter) case m_showWebBrowser__title_titleurl_url(Parameter, Parameter) + case m_showWebBrowser__SSO(Parameter) case m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type(Parameter, Parameter, Parameter, Parameter<() -> Void>, Parameter<() -> Void>, Parameter) case m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagenextSectionName_nextSectionNameaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTappednextSectionTapped_nextSectionTapped(Parameter, Parameter, Parameter, Parameter, Parameter, Parameter<() -> Void>, Parameter<() -> Void>, Parameter<() -> Void>) case m_presentView__transitionStyle_transitionStyleview_viewcompletion_completion(Parameter, Parameter, Parameter<(() -> Void)?>) @@ -2912,6 +2948,7 @@ open class DiscussionRouterMock: DiscussionRouter, Mock { case .m_showForgotPasswordScreen: return 0 case let .m_showDiscoveryScreen__searchQuery_searchQuerysourceScreen_sourceScreen(p0, p1): return p0.intValue + p1.intValue case let .m_showWebBrowser__title_titleurl_url(p0, p1): return p0.intValue + p1.intValue + case let .m_showWebBrowser__SSO(p0): return p0.intValue case let .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type(p0, p1, p2, p3, p4, p5): return p0.intValue + p1.intValue + p2.intValue + p3.intValue + p4.intValue + p5.intValue case let .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagenextSectionName_nextSectionNameaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTappednextSectionTapped_nextSectionTapped(p0, p1, p2, p3, p4, p5, p6, p7): return p0.intValue + p1.intValue + p2.intValue + p3.intValue + p4.intValue + p5.intValue + p6.intValue + p7.intValue case let .m_presentView__transitionStyle_transitionStyleview_viewcompletion_completion(p0, p1, p2): return p0.intValue + p1.intValue + p2.intValue @@ -2938,6 +2975,7 @@ open class DiscussionRouterMock: DiscussionRouter, Mock { case .m_showForgotPasswordScreen: return ".showForgotPasswordScreen()" case .m_showDiscoveryScreen__searchQuery_searchQuerysourceScreen_sourceScreen: return ".showDiscoveryScreen(searchQuery:sourceScreen:)" case .m_showWebBrowser__title_titleurl_url: return ".showWebBrowser(title:url:)" + case .m_showWebBrowser__SSO: return ".showSSOWebBrowser(title:)" case .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type: return ".presentAlert(alertTitle:alertMessage:positiveAction:onCloseTapped:okTapped:type:)" case .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagenextSectionName_nextSectionNameaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTappednextSectionTapped_nextSectionTapped: return ".presentAlert(alertTitle:alertMessage:nextSectionName:action:image:onCloseTapped:okTapped:nextSectionTapped:)" case .m_presentView__transitionStyle_transitionStyleview_viewcompletion_completion: return ".presentView(transitionStyle:view:completion:)" diff --git a/OpenEdX/DI/ScreenAssembly.swift b/OpenEdX/DI/ScreenAssembly.swift index 04ddc0514..744ed87f8 100644 --- a/OpenEdX/DI/ScreenAssembly.swift +++ b/OpenEdX/DI/ScreenAssembly.swift @@ -62,6 +62,14 @@ class ScreenAssembly: Assembly { sourceScreen: sourceScreen ) } + container.register(SSOWebViewModel.self) { r in + SSOWebViewModel( + interactor: r.resolve(AuthInteractorProtocol.self)!, + router: r.resolve(AuthorizationRouter.self)!, + config: r.resolve(ConfigProtocol.self)!, + analytics: r.resolve(AuthorizationAnalytics.self)! + ) + } container.register(SignUpViewModel.self) { r, sourceScreen in SignUpViewModel( interactor: r.resolve(AuthInteractorProtocol.self)!, diff --git a/OpenEdX/Router.swift b/OpenEdX/Router.swift index fce1cca5d..496d322f8 100644 --- a/OpenEdX/Router.swift +++ b/OpenEdX/Router.swift @@ -837,6 +837,16 @@ public class Router: AuthorizationRouter, let controller = UIHostingController(rootView: webBrowser) navigationController.pushViewController(controller, animated: true) } + + public func showSSOWebBrowser(title: String) { + let config = Container.shared.resolve(ConfigProtocol.self)! + let webBrowser = ContainerWebView( + config.SSOBaseURL.absoluteString, + title: title + ) + let controller = UIHostingController(rootView: webBrowser) + navigationController.pushViewController(controller, animated: true) + } } // MARK: BackNavigationProtocol diff --git a/Profile/ProfileTests/ProfileMock.generated.swift b/Profile/ProfileTests/ProfileMock.generated.swift index 1817697a4..ddb811662 100644 --- a/Profile/ProfileTests/ProfileMock.generated.swift +++ b/Profile/ProfileTests/ProfileMock.generated.swift @@ -62,18 +62,35 @@ open class AuthInteractorProtocolMock: AuthInteractorProtocol, Mock { @discardableResult open func login(username: String, password: String) throws -> User { addInvocation(.m_login__username_usernamepassword_password(Parameter.value(`username`), Parameter.value(`password`))) - let perform = methodPerformValue(.m_login__username_usernamepassword_password(Parameter.value(`username`), Parameter.value(`password`))) as? (String, String) -> Void - perform?(`username`, `password`) - var __value: User - do { - __value = try methodReturnValue(.m_login__username_usernamepassword_password(Parameter.value(`username`), Parameter.value(`password`))).casted() - } catch MockError.notStubed { - onFatalFailure("Stub return value not specified for login(username: String, password: String). Use given") - Failure("Stub return value not specified for login(username: String, password: String). Use given") - } catch { - throw error - } - return __value + let perform = methodPerformValue(.m_login__username_usernamepassword_password(Parameter.value(`username`), Parameter.value(`password`))) as? (String, String) -> Void + perform?(`username`, `password`) + var __value: User + do { + __value = try methodReturnValue(.m_login__username_usernamepassword_password(Parameter.value(`username`), Parameter.value(`password`))).casted() + } catch MockError.notStubed { + onFatalFailure("Stub return value not specified for login(username: String, password: String). Use given") + Failure("Stub return value not specified for login(username: String, password: String). Use given") + } catch { + throw error + } + return __value + } + + @discardableResult + open func SSOlogin(jwtToken: String) async throws -> Core.User { + addInvocation(.m_login__SSO__username_password(Parameter.value(`jwtToken`))) + let perform = methodPerformValue(.m_login__SSO__username_password(Parameter.value(`jwtToken`))) as? (String) -> Void + perform?(`jwtToken`) + var __value: User + do { + __value = try methodReturnValue(.m_login__SSO__username_password(Parameter.value(jwtToken))).casted() + } catch MockError.notStubed { + onFatalFailure("Stub return value not specified for login(username: String, password: String). Use given") + Failure("Stub return value not specified for login(username: String, password: String). Use given") + } catch { + throw error + } + return __value } @discardableResult @@ -173,6 +190,7 @@ open class AuthInteractorProtocolMock: AuthInteractorProtocol, Mock { fileprivate enum MethodType { case m_login__username_usernamepassword_password(Parameter, Parameter) + case m_login__SSO__username_password(Parameter) case m_login__externalToken_externalTokenbackend_backend(Parameter, Parameter) case m_resetPassword__email_email(Parameter) case m_getCookies__force_force(Parameter) @@ -223,6 +241,7 @@ open class AuthInteractorProtocolMock: AuthInteractorProtocol, Mock { func intValue() -> Int { switch self { case let .m_login__username_usernamepassword_password(p0, p1): return p0.intValue + p1.intValue + case let .m_login__SSO__username_password(p0): return p0.intValue case let .m_login__externalToken_externalTokenbackend_backend(p0, p1): return p0.intValue + p1.intValue case let .m_resetPassword__email_email(p0): return p0.intValue case let .m_getCookies__force_force(p0): return p0.intValue @@ -234,6 +253,7 @@ open class AuthInteractorProtocolMock: AuthInteractorProtocol, Mock { func assertionName() -> String { switch self { case .m_login__username_usernamepassword_password: return ".login(username:password:)" + case .m_login__SSO__username_password: return ".loginSSO(username:password:)" case .m_login__externalToken_externalTokenbackend_backend: return ".login(externalToken:backend:)" case .m_resetPassword__email_email: return ".resetPassword(email:)" case .m_getCookies__force_force: return ".getCookies(force:)" @@ -581,6 +601,12 @@ open class BaseRouterMock: BaseRouter, Mock { perform?(`title`, `url`) } + open func showSSOWebBrowser(title: String) { + addInvocation(.m_showWebBrowser__SSO(Parameter.value(`title`))) + let perform = methodPerformValue(.m_showWebBrowser__SSO(Parameter.value(`title`))) as? (String) -> Void + perform?(`title`) + } + open func presentAlert(alertTitle: String, alertMessage: String, positiveAction: String, onCloseTapped: @escaping () -> Void, okTapped: @escaping () -> Void, type: AlertViewType) { addInvocation(.m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type(Parameter.value(`alertTitle`), Parameter.value(`alertMessage`), Parameter.value(`positiveAction`), Parameter<() -> Void>.value(`onCloseTapped`), Parameter<() -> Void>.value(`okTapped`), Parameter.value(`type`))) let perform = methodPerformValue(.m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type(Parameter.value(`alertTitle`), Parameter.value(`alertMessage`), Parameter.value(`positiveAction`), Parameter<() -> Void>.value(`onCloseTapped`), Parameter<() -> Void>.value(`okTapped`), Parameter.value(`type`))) as? (String, String, String, @escaping () -> Void, @escaping () -> Void, AlertViewType) -> Void @@ -619,6 +645,7 @@ open class BaseRouterMock: BaseRouter, Mock { case m_showForgotPasswordScreen case m_showDiscoveryScreen__searchQuery_searchQuerysourceScreen_sourceScreen(Parameter, Parameter) case m_showWebBrowser__title_titleurl_url(Parameter, Parameter) + case m_showWebBrowser__SSO(Parameter) case m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type(Parameter, Parameter, Parameter, Parameter<() -> Void>, Parameter<() -> Void>, Parameter) case m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagenextSectionName_nextSectionNameaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTappednextSectionTapped_nextSectionTapped(Parameter, Parameter, Parameter, Parameter, Parameter, Parameter<() -> Void>, Parameter<() -> Void>, Parameter<() -> Void>) case m_presentView__transitionStyle_transitionStyleview_viewcompletion_completion(Parameter, Parameter, Parameter<(() -> Void)?>) @@ -732,6 +759,7 @@ open class BaseRouterMock: BaseRouter, Mock { case .m_showForgotPasswordScreen: return 0 case let .m_showDiscoveryScreen__searchQuery_searchQuerysourceScreen_sourceScreen(p0, p1): return p0.intValue + p1.intValue case let .m_showWebBrowser__title_titleurl_url(p0, p1): return p0.intValue + p1.intValue + case let .m_showWebBrowser__SSO(p0): return p0.intValue case let .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type(p0, p1, p2, p3, p4, p5): return p0.intValue + p1.intValue + p2.intValue + p3.intValue + p4.intValue + p5.intValue case let .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagenextSectionName_nextSectionNameaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTappednextSectionTapped_nextSectionTapped(p0, p1, p2, p3, p4, p5, p6, p7): return p0.intValue + p1.intValue + p2.intValue + p3.intValue + p4.intValue + p5.intValue + p6.intValue + p7.intValue case let .m_presentView__transitionStyle_transitionStyleview_viewcompletion_completion(p0, p1, p2): return p0.intValue + p1.intValue + p2.intValue @@ -752,6 +780,7 @@ open class BaseRouterMock: BaseRouter, Mock { case .m_showForgotPasswordScreen: return ".showForgotPasswordScreen()" case .m_showDiscoveryScreen__searchQuery_searchQuerysourceScreen_sourceScreen: return ".showDiscoveryScreen(searchQuery:sourceScreen:)" case .m_showWebBrowser__title_titleurl_url: return ".showWebBrowser(title:url:)" + case .m_showWebBrowser__SSO: return ".showSSOWebBrowser(title:)" case .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type: return ".presentAlert(alertTitle:alertMessage:positiveAction:onCloseTapped:okTapped:type:)" case .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagenextSectionName_nextSectionNameaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTappednextSectionTapped_nextSectionTapped: return ".presentAlert(alertTitle:alertMessage:nextSectionName:action:image:onCloseTapped:okTapped:nextSectionTapped:)" case .m_presentView__transitionStyle_transitionStyleview_viewcompletion_completion: return ".presentView(transitionStyle:view:completion:)" @@ -3275,6 +3304,12 @@ open class ProfileRouterMock: ProfileRouter, Mock { perform?(`title`, `url`) } + open func showSSOWebBrowser(title: String) { + addInvocation(.m_showWebBrowser__SSO(Parameter.value(`title`))) + let perform = methodPerformValue(.m_showWebBrowser__SSO(Parameter.value(`title`))) as? (String) -> Void + perform?(`title`) + } + open func presentAlert(alertTitle: String, alertMessage: String, positiveAction: String, onCloseTapped: @escaping () -> Void, okTapped: @escaping () -> Void, type: AlertViewType) { addInvocation(.m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type(Parameter.value(`alertTitle`), Parameter.value(`alertMessage`), Parameter.value(`positiveAction`), Parameter<() -> Void>.value(`onCloseTapped`), Parameter<() -> Void>.value(`okTapped`), Parameter.value(`type`))) let perform = methodPerformValue(.m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type(Parameter.value(`alertTitle`), Parameter.value(`alertMessage`), Parameter.value(`positiveAction`), Parameter<() -> Void>.value(`onCloseTapped`), Parameter<() -> Void>.value(`okTapped`), Parameter.value(`type`))) as? (String, String, String, @escaping () -> Void, @escaping () -> Void, AlertViewType) -> Void @@ -3323,6 +3358,7 @@ open class ProfileRouterMock: ProfileRouter, Mock { case m_showForgotPasswordScreen case m_showDiscoveryScreen__searchQuery_searchQuerysourceScreen_sourceScreen(Parameter, Parameter) case m_showWebBrowser__title_titleurl_url(Parameter, Parameter) + case m_showWebBrowser__SSO(Parameter) case m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type(Parameter, Parameter, Parameter, Parameter<() -> Void>, Parameter<() -> Void>, Parameter) case m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagenextSectionName_nextSectionNameaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTappednextSectionTapped_nextSectionTapped(Parameter, Parameter, Parameter, Parameter, Parameter, Parameter<() -> Void>, Parameter<() -> Void>, Parameter<() -> Void>) case m_presentView__transitionStyle_transitionStyleview_viewcompletion_completion(Parameter, Parameter, Parameter<(() -> Void)?>) @@ -3479,6 +3515,7 @@ open class ProfileRouterMock: ProfileRouter, Mock { case .m_showForgotPasswordScreen: return 0 case let .m_showDiscoveryScreen__searchQuery_searchQuerysourceScreen_sourceScreen(p0, p1): return p0.intValue + p1.intValue case let .m_showWebBrowser__title_titleurl_url(p0, p1): return p0.intValue + p1.intValue + case let .m_showWebBrowser__SSO(p0): return p0.intValue case let .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type(p0, p1, p2, p3, p4, p5): return p0.intValue + p1.intValue + p2.intValue + p3.intValue + p4.intValue + p5.intValue case let .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagenextSectionName_nextSectionNameaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTappednextSectionTapped_nextSectionTapped(p0, p1, p2, p3, p4, p5, p6, p7): return p0.intValue + p1.intValue + p2.intValue + p3.intValue + p4.intValue + p5.intValue + p6.intValue + p7.intValue case let .m_presentView__transitionStyle_transitionStyleview_viewcompletion_completion(p0, p1, p2): return p0.intValue + p1.intValue + p2.intValue @@ -3509,6 +3546,7 @@ open class ProfileRouterMock: ProfileRouter, Mock { case .m_showForgotPasswordScreen: return ".showForgotPasswordScreen()" case .m_showDiscoveryScreen__searchQuery_searchQuerysourceScreen_sourceScreen: return ".showDiscoveryScreen(searchQuery:sourceScreen:)" case .m_showWebBrowser__title_titleurl_url: return ".showWebBrowser(title:url:)" + case .m_showWebBrowser__SSO: return ".showSSOWebBrowser(title:)" case .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type: return ".presentAlert(alertTitle:alertMessage:positiveAction:onCloseTapped:okTapped:type:)" case .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagenextSectionName_nextSectionNameaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTappednextSectionTapped_nextSectionTapped: return ".presentAlert(alertTitle:alertMessage:nextSectionName:action:image:onCloseTapped:okTapped:nextSectionTapped:)" case .m_presentView__transitionStyle_transitionStyleview_viewcompletion_completion: return ".presentView(transitionStyle:view:completion:)" diff --git a/default_config/dev/config.yaml b/default_config/dev/config.yaml index d7cec08d1..96cda953d 100644 --- a/default_config/dev/config.yaml +++ b/default_config/dev/config.yaml @@ -1,4 +1,5 @@ API_HOST_URL: 'http://localhost:8000' +SSO_URL: 'http://localhost:8000' ENVIRONMENT_DISPLAY_NAME: 'Localhost' FEEDBACK_EMAIL_ADDRESS: 'support@example.com' OAUTH_CLIENT_ID: ''