diff --git a/Authorization/Authorization.xcodeproj/project.pbxproj b/Authorization/Authorization.xcodeproj/project.pbxproj index 8aa67bc84..8c3380cd1 100644 --- a/Authorization/Authorization.xcodeproj/project.pbxproj +++ b/Authorization/Authorization.xcodeproj/project.pbxproj @@ -26,6 +26,8 @@ 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 */; }; + 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 */; }; E03261642AE64676002CA7EB /* StartupViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E03261632AE64676002CA7EB /* StartupViewModel.swift */; }; E03261662AE64AF4002CA7EB /* StartupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E03261652AE64AF4002CA7EB /* StartupView.swift */; }; @@ -78,6 +80,8 @@ 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 = ""; }; 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 = ""; }; + BADB3F542AD6DFC3004D5CFA /* SocialAuthViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SocialAuthViewModel.swift; sourceTree = ""; }; E03261632AE64676002CA7EB /* StartupViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StartupViewModel.swift; sourceTree = ""; }; E03261652AE64AF4002CA7EB /* StartupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StartupView.swift; sourceTree = ""; }; E03261672AE9F156002CA7EB /* LogistrationBottomView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogistrationBottomView.swift; sourceTree = ""; }; @@ -145,6 +149,7 @@ 071009CC28D1E24000344290 /* Presentation */ = { isa = PBXGroup; children = ( + BA8B3A302AD5485100D25EF5 /* SocialAuth */, E03261622AE6464A002CA7EB /* Startup */, 020C31BD290AADA700D6DEA2 /* Base */, 071009C528D1D9FA00344290 /* Login */, @@ -265,6 +270,15 @@ path = ../Pods; sourceTree = ""; }; + BA8B3A302AD5485100D25EF5 /* SocialAuth */ = { + isa = PBXGroup; + children = ( + BA8B3A312AD5487300D25EF5 /* SocialAuthView.swift */, + BADB3F542AD6DFC3004D5CFA /* SocialAuthViewModel.swift */, + ); + path = SocialAuth; + sourceTree = ""; + }; E03261622AE6464A002CA7EB /* Startup */ = { isa = PBXGroup; children = ( @@ -484,6 +498,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + BADB3F552AD6DFC3004D5CFA /* SocialAuthViewModel.swift in Sources */, 02066B442906D72400F4307E /* SignUpView.swift in Sources */, 0770DE7128D0C0E7006D8A5D /* Strings.swift in Sources */, 025F40E229D360E20064C183 /* ResetPasswordViewModel.swift in Sources */, @@ -497,6 +512,7 @@ 02F3BFE5292533720051930C /* AuthorizationRouter.swift in Sources */, E03261662AE64AF4002CA7EB /* StartupView.swift in Sources */, 071009C728D1DA4F00344290 /* SignInViewModel.swift in Sources */, + BA8B3A322AD5487300D25EF5 /* SocialAuthView.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/Authorization/Authorization/Presentation/AuthorizationAnalytics.swift b/Authorization/Authorization/Presentation/AuthorizationAnalytics.swift index 6c7a60393..d73799435 100644 --- a/Authorization/Authorization/Presentation/AuthorizationAnalytics.swift +++ b/Authorization/Authorization/Presentation/AuthorizationAnalytics.swift @@ -7,17 +7,31 @@ import Foundation -public enum LoginMethod: String { - case password = "Password" +public enum AuthMethod: Equatable { + case password + case socailAuth(SocialAuthMethod) + + public var analyticsValue: String { + switch self { + case .password: + "Password" + case .socailAuth(let socialAuthMethod): + socialAuthMethod.rawValue + } + } +} + +public enum SocialAuthMethod: String { case facebook = "Facebook" case google = "Google" case microsoft = "Microsoft" + case apple = "Apple" } //sourcery: AutoMockable public protocol AuthorizationAnalytics { func setUserID(_ id: String) - func userLogin(method: LoginMethod) + func userLogin(method: AuthMethod) func signUpClicked() func createAccountClicked() func registrationSuccess() @@ -28,7 +42,7 @@ public protocol AuthorizationAnalytics { #if DEBUG class AuthorizationAnalyticsMock: AuthorizationAnalytics { public func setUserID(_ id: String) {} - public func userLogin(method: LoginMethod) {} + public func userLogin(method: AuthMethod) {} public func signUpClicked() {} public func createAccountClicked() {} public func registrationSuccess() {} diff --git a/Authorization/Authorization/Presentation/Login/SignInView.swift b/Authorization/Authorization/Presentation/Login/SignInView.swift index f7dcf262f..0fece9b53 100644 --- a/Authorization/Authorization/Presentation/Login/SignInView.swift +++ b/Authorization/Authorization/Presentation/Login/SignInView.swift @@ -129,6 +129,15 @@ public struct SignInView: View { .padding(.top, 40) } } + if viewModel.socialAuthEnabled { + SocialAuthView( + viewModel: .init( + config: viewModel.config + ) { result in + Task { await viewModel.login(with: result) } + } + ) + } Spacer() } .padding(.horizontal, 24) @@ -180,7 +189,7 @@ struct SignInView_Previews: PreviewProvider { static var previews: some View { let vm = SignInViewModel( interactor: AuthInteractor.mock, - router: AuthorizationRouterMock(), + router: AuthorizationRouterMock(), config: ConfigMock(), analytics: AuthorizationAnalyticsMock(), validator: Validator() diff --git a/Authorization/Authorization/Presentation/Login/SignInViewModel.swift b/Authorization/Authorization/Presentation/Login/SignInViewModel.swift index f2fa8a8f5..a2439a10b 100644 --- a/Authorization/Authorization/Presentation/Login/SignInViewModel.swift +++ b/Authorization/Authorization/Presentation/Login/SignInViewModel.swift @@ -9,9 +9,13 @@ import Foundation import Core import SwiftUI import Alamofire +import AuthenticationServices +import FacebookLogin +import GoogleSignIn +import MSAL public class SignInViewModel: ObservableObject { - + @Published private(set) var isShowProgress = false @Published private(set) var showError: Bool = false @Published private(set) var showAlert: Bool = false @@ -35,7 +39,7 @@ public class SignInViewModel: ObservableObject { private let interactor: AuthInteractorProtocol private let analytics: AuthorizationAnalytics private let validator: Validator - + public init( interactor: AuthInteractorProtocol, router: AuthorizationRouter, @@ -49,7 +53,14 @@ public class SignInViewModel: ObservableObject { self.analytics = analytics self.validator = validator } - + + var socialAuthEnabled: Bool { + config.appleSignIn.enabled || + config.facebook.enabled || + config.microsoft.enabled || + config.google.enabled + } + @MainActor func login(username: String, password: String) async { guard validator.isValidUsername(username) else { @@ -57,7 +68,7 @@ public class SignInViewModel: ObservableObject { return } guard !password.isEmpty else { - errorMessage = AuthLocalization.Error.invalidPasswordLength + errorMessage = AuthLocalization.Error.invalidPasswordLenght return } @@ -68,27 +79,71 @@ public class SignInViewModel: ObservableObject { analytics.userLogin(method: .password) router.showMainOrWhatsNewScreen() } catch let error { - isShowProgress = false - if error.isUpdateRequeiredError { - router.showUpdateRequiredView(showAccountLink: false) - } else if let validationError = error.validationError, - let value = validationError.data?["error_description"] as? String { - errorMessage = value - } else if case APIError.invalidGrant = error { - errorMessage = CoreLocalization.Error.invalidCredentials - } else if error.isInternetError { - errorMessage = CoreLocalization.Error.slowOrNoInternetConnection + failure(error) + } + } + + @MainActor + func login(with result: Result) async { + switch result { + case .success(let result): + await socialLogin( + externalToken: result.response.token, + backend: result.backend, + authMethod: result.authMethod + ) + case .failure(let error): + errorMessage = error.localizedDescription + } + } + + @MainActor + private func socialLogin( + externalToken: String, + backend: String, + authMethod: AuthMethod + ) async { + isShowProgress = true + do { + let user = try await interactor.login(externalToken: externalToken, backend: backend) + analytics.setUserID("\(user.id)") + analytics.userLogin(method: authMethod) + router.showMainOrWhatsNewScreen() + } catch let error { + failure(error, authMethod: authMethod) + } + } + + @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 = CoreLocalization.Error.unknownError + 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 trackSignUpClicked() { analytics.signUpClicked() } - + func trackForgotPasswordClicked() { analytics.forgotPasswordClicked() } + } diff --git a/Authorization/Authorization/Presentation/Registration/SignUpView.swift b/Authorization/Authorization/Presentation/Registration/SignUpView.swift index 09c484a4c..13320ee9b 100644 --- a/Authorization/Authorization/Presentation/Registration/SignUpView.swift +++ b/Authorization/Authorization/Presentation/Registration/SignUpView.swift @@ -67,7 +67,17 @@ public struct SignUpView: View { .font(Theme.Fonts.titleSmall) .foregroundColor(Theme.Colors.textPrimary) .padding(.bottom, 20) - + + if viewModel.thirdPartyAuthSuccess { + Text(AuthLocalization.SignUp.successSigninLabel) + .font(Theme.Fonts.titleMedium) + .foregroundColor(Theme.Colors.textPrimary) + Text(AuthLocalization.SignUp.successSigninSublabel) + .font(Theme.Fonts.titleSmall) + .foregroundColor(Theme.Colors.textSecondary) + .padding(.bottom, 20) + } + let requiredFields = viewModel.fields.filter {$0.field.required} let nonRequiredFields = viewModel.fields.filter {!$0.field.required} @@ -98,15 +108,27 @@ public struct SignUpView: View { }.frame(maxWidth: .infinity) } else { StyledButton(AuthLocalization.SignUp.createAccountBtn) { + viewModel.thirdPartyAuthSuccess = false Task { await viewModel.registerUser() } viewModel.trackCreateAccountClicked() } .padding(.top, 40) - .padding(.bottom, 80) .frame(maxWidth: .infinity) } + if viewModel.socialAuthEnabled, + !requiredFields.isEmpty { + SocialAuthView( + authType: .register, + viewModel: .init( + config: viewModel.config + ) { result in + Task { await viewModel.register(with: result) } + } + ) + .padding(.bottom, 30) + } Spacer() } .padding(.horizontal, 24) diff --git a/Authorization/Authorization/Presentation/Registration/SignUpViewModel.swift b/Authorization/Authorization/Presentation/Registration/SignUpViewModel.swift index ff3691c05..927651b7d 100644 --- a/Authorization/Authorization/Presentation/Registration/SignUpViewModel.swift +++ b/Authorization/Authorization/Presentation/Registration/SignUpViewModel.swift @@ -8,12 +8,17 @@ import Foundation import Core import SwiftUI +import AuthenticationServices +import FacebookLogin +import GoogleSignIn +import MSAL public class SignUpViewModel: ObservableObject { @Published var isShowProgress = false @Published var scrollTo: Int? @Published var showError: Bool = false + @Published var thirdPartyAuthSuccess: Bool = false var errorMessage: String? { didSet { withAnimation { @@ -47,8 +52,21 @@ public class SignUpViewModel: ObservableObject { self.cssInjector = cssInjector self.validator = validator } - + + var socialAuthEnabled: Bool { + let socialLoginEnabled = config.appleSignIn.enabled || + config.facebook.enabled || + config.microsoft.enabled || + config.google.enabled + return socialLoginEnabled && !thirdPartyAuthSuccess && !isShowProgress + } + private func showErrors(errors: [String: String]) -> Bool { + if thirdPartyAuthSuccess, !errors.map({ $0.value }).filter({ !$0.isEmpty }).isEmpty { + scrollTo = 1 + return true + } + var containsError = false errors.forEach { key, value in if let index = fields.firstIndex(where: { $0.field.name == key }) { @@ -78,20 +96,21 @@ public class SignUpViewModel: ObservableObject { } } } - + + private var externalToken: String? + private var backend: String? + @MainActor func registerUser() async { do { - var validateFields: [String: String] = [:] - fields.forEach({ - validateFields[$0.field.name] = $0.text - }) - validateFields["honor_code"] = "true" - validateFields["terms_of_service"] = "true" + let validateFields = configureFields() let errors = try await interactor.validateRegistrationFields(fields: validateFields) guard !showErrors(errors: errors) else { return } isShowProgress = true - let user = try await interactor.registerUser(fields: validateFields) + let user = try await interactor.registerUser( + fields: validateFields, + isSocial: externalToken != nil + ) analytics.setUserID("\(user.id)") analytics.registrationSuccess() isShowProgress = false @@ -108,7 +127,66 @@ public class SignUpViewModel: ObservableObject { } } } - + + private func configureFields() -> [String: String] { + var validateFields: [String: String] = [:] + fields.forEach { validateFields[$0.field.name] = $0.text } + validateFields["honor_code"] = "true" + validateFields["terms_of_service"] = "true" + if let externalToken = externalToken, let backend = backend { + validateFields["access_token"] = externalToken + validateFields["provider"] = backend + validateFields["client_id"] = config.oAuthClientId + if validateFields.contains(where: {$0.key == "password"}) { + validateFields.removeValue(forKey: "password") + } + fields.removeAll { $0.field.type == .password } + } + return validateFields + } + + @MainActor + func register(with result: Result) async { + switch result { + case .success(let result): + await loginOrRegister( + result.response, + backend: result.backend, + authMethod: result.authMethod + ) + case .failure(let error): + errorMessage = error.localizedDescription + } + } + + @MainActor + private func loginOrRegister( + _ response: SocialAuthResponse, + backend: String, + authMethod: AuthMethod + ) async { + do { + isShowProgress = true + let user = try await interactor.login(externalToken: response.token, backend: backend) + analytics.setUserID("\(user.id)") + analytics.userLogin(method: authMethod) + isShowProgress = false + router.showMainOrWhatsNewScreen() + } catch { + update(fullName: response.name, email: response.email) + self.externalToken = response.token + self.backend = backend + thirdPartyAuthSuccess = true + isShowProgress = false + await registerUser() + } + } + + private func update(fullName: String?, email: String?) { + fields.first(where: { $0.field.type == .email })?.text = email ?? "" + fields.first(where: { $0.field.name == "name" })?.text = fullName ?? "" + } + func trackCreateAccountClicked() { analytics.createAccountClicked() } diff --git a/Authorization/Authorization/Presentation/SocialAuth/SocialAuthView.swift b/Authorization/Authorization/Presentation/SocialAuth/SocialAuthView.swift new file mode 100644 index 000000000..2e91ebf82 --- /dev/null +++ b/Authorization/Authorization/Presentation/SocialAuth/SocialAuthView.swift @@ -0,0 +1,113 @@ +// +// SocialAuthView.swift +// Authorization +// +// Created by Eugene Yatsenko on 10.10.2023. +// + +import SwiftUI +import Core + +struct SocialAuthView: View { + + // MARK: - Properties + + @StateObject var viewModel: SocialAuthViewModel + + init( + authType: SocialAuthType = .signIn, + viewModel: SocialAuthViewModel + ) { + self._viewModel = .init(wrappedValue: viewModel) + self.authType = authType + } + + enum SocialAuthType { + case signIn + case register + } + var authType: SocialAuthType = .signIn + + private var title: String { + switch authType { + case .signIn: + AuthLocalization.signInWith + case .register: + AuthLocalization.registerWith + } + } + + // MARK: - Views + + var body: some View { + VStack(spacing: 10) { + headerView + buttonsView + } + .padding(.bottom, 20) + } + + private var headerView: some View { + HStack { + Text("\(AuthLocalization.or) \(title.lowercased()):") + .padding(.vertical, 20) + .font(.system(size: 17, weight: .medium)) + Spacer() + } + } + + private var buttonsView: some View { + Group { + if viewModel.googleEnabled { + SocialAuthButton( + image: CoreAssets.iconGoogleWhite.swiftUIImage, + title: "\(title) \(AuthLocalization.google)", + textColor: .black, + backgroundColor: CoreAssets.googleButtonColor.swiftUIColor, + action: { Task { await viewModel.signInWithGoogle() } } + ) + .accessibilityElement(children: .ignore) + .accessibilityLabel("\(title) \(AuthLocalization.facebook)") + } + if viewModel.faceboolEnabled { + SocialAuthButton( + image: CoreAssets.iconFacebookWhite.swiftUIImage, + title: "\(title) \(AuthLocalization.facebook)", + backgroundColor: CoreAssets.facebookButtonColor.swiftUIColor, + action: { Task { await viewModel.signInWithFacebook() } } + ) + .accessibilityElement(children: .ignore) + .accessibilityLabel("\(title) \(AuthLocalization.facebook)") + } + if viewModel.microsoftEnabled { + SocialAuthButton( + image: CoreAssets.iconMicrosoftWhite.swiftUIImage, + title: "\(title) \(AuthLocalization.microsoft)", + backgroundColor: CoreAssets.microsoftButtonColor.swiftUIColor, + action: { Task { await viewModel.signInWithMicrosoft() } } + ) + .accessibilityElement(children: .ignore) + .accessibilityLabel("\(title) \(AuthLocalization.microsoft)") + } + if viewModel.appleSignInEnabled { + SocialAuthButton( + image: CoreAssets.iconApple.swiftUIImage, + title: "\(title) \(AuthLocalization.apple)", + backgroundColor: CoreAssets.appleButtonColor.swiftUIColor, + action: viewModel.signInWithApple + ) + .accessibilityElement(children: .ignore) + .accessibilityLabel("\(title) \(AuthLocalization.apple)") + } + } + } +} + +#if DEBUG +struct SocialSignView_Previews: PreviewProvider { + static var previews: some View { + let vm = SocialAuthViewModel(config: ConfigMock(), completion: { _ in }) + SocialAuthView(viewModel: vm).padding() + } +} +#endif diff --git a/Authorization/Authorization/Presentation/SocialAuth/SocialAuthViewModel.swift b/Authorization/Authorization/Presentation/SocialAuth/SocialAuthViewModel.swift new file mode 100644 index 000000000..24e8ca5b8 --- /dev/null +++ b/Authorization/Authorization/Presentation/SocialAuth/SocialAuthViewModel.swift @@ -0,0 +1,157 @@ +// +// SocialAuthViewModel.swift +// Authorization +// +// Created by Eugene Yatsenko on 11.10.2023. +// + +import SwiftUI +import Core +import AuthenticationServices +import FacebookLogin +import GoogleSignIn +import MSAL +import Swinject + +enum SocialAuthDetails { + case apple(SocialAuthResponse) + case facebook(SocialAuthResponse) + case google(SocialAuthResponse) + case microsoft(SocialAuthResponse) + + var backend: String { + switch self { + case .apple: + "apple-id" + case .facebook: + "facebook" + case .google: + "google-oauth2" + case .microsoft: + "azuread-oauth2" + } + } + + var authMethod: AuthMethod { + switch self { + case .apple: + .socailAuth(.apple) + case .facebook: + .socailAuth(.facebook) + case .google: + .socailAuth(.google) + case .microsoft: + .socailAuth(.microsoft) + } + } + + var response: SocialAuthResponse { + switch self { + case .apple(let response), + .facebook(let response), + .google(let response), + .microsoft(let response): + return response + } + } +} + +final public class SocialAuthViewModel: ObservableObject { + + // MARK: - Properties + + private var completion: ((Result) -> Void) + private let config: ConfigProtocol + + init( + config: ConfigProtocol, + completion: @escaping (Result) -> Void + ) { + self.config = config + self.completion = completion + } + + private lazy var appleAuthProvider: AppleAuthProvider = .init(config: config) + private lazy var googleAuthProvider: GoogleAuthProvider = .init() + private lazy var facebookAuthProvider: FacebookAuthProvider = .init() + private lazy var microsoftAuthProvider: MicrosoftAuthProvider = .init() + + private var topViewController: UIViewController? { + UIApplication.topViewController() + } + + // MARK: - Public Properties + + var faceboolEnabled: Bool { + config.facebook.enabled + } + + var googleEnabled: Bool { + config.google.enabled + } + + var microsoftEnabled: Bool { + config.microsoft.enabled + } + + var appleSignInEnabled: Bool { + if faceboolEnabled || + googleEnabled || + microsoftEnabled { + /// Apps that use a third-party or social login service (such as Facebook Login, Google Sign-In...) + /// to set up or authenticate the user's primary account with the app + /// must also offer Sign in with Apple as an equivalent option + return true + } + return config.appleSignIn.enabled + } + + // MARK: - Public Intens + + func signInWithApple() { + appleAuthProvider.request { [weak self] result in + guard let self else { return } + result.success { self.success(with: .apple($0)) } + result.failure(self.failure) + } + } + + @MainActor + func signInWithGoogle() async { + guard let vc = topViewController else { + return + } + let result = await googleAuthProvider.signIn(withPresenting: vc) + result.success { success(with: .google($0)) } + result.failure(failure) + } + + @MainActor + func signInWithFacebook() async { + guard let vc = topViewController else { + return + } + let result = await facebookAuthProvider.signIn(withPresenting: vc) + result.success { success(with: .facebook($0)) } + result.failure(failure) + } + + @MainActor + func signInWithMicrosoft() async { + guard let vc = topViewController else { + return + } + let result = await microsoftAuthProvider.signIn(withPresenting: vc) + result.success { success(with: .microsoft($0)) } + result.failure(failure) + } + + private func success(with social: SocialAuthDetails) { + completion(.success(social)) + } + + private func failure(_ error: Error) { + completion(.failure(error)) + } + +} diff --git a/Authorization/Authorization/SwiftGen/Strings.swift b/Authorization/Authorization/SwiftGen/Strings.swift index 512465414..a2cd9accf 100644 --- a/Authorization/Authorization/SwiftGen/Strings.swift +++ b/Authorization/Authorization/SwiftGen/Strings.swift @@ -10,13 +10,33 @@ import Foundation // swiftlint:disable explicit_type_interface function_parameter_count identifier_name line_length // swiftlint:disable nesting type_body_length type_name vertical_whitespace_opening_braces public enum AuthLocalization { + /// Apple + public static let apple = AuthLocalization.tr("Localizable", "APPLE", fallback: "Apple") + /// Facebook + public static let facebook = AuthLocalization.tr("Localizable", "FACEBOOK", fallback: "Facebook") + /// Google + public static let google = AuthLocalization.tr("Localizable", "GOOGLE", fallback: "Google") + /// Microsoft + public static let microsoft = AuthLocalization.tr("Localizable", "MICROSOFT", fallback: "Microsoft") + /// Or + public static let or = AuthLocalization.tr("Localizable", "OR", fallback: "Or") + /// Register with + public static let registerWith = AuthLocalization.tr("Localizable", "REGISTER_WITH", fallback: "Register with") + /// Sign in with + public static let signInWith = AuthLocalization.tr("Localizable", "SIGN_IN_WITH", fallback: "Sign in with") public enum Error { + /// This %@ account is not linked with any %@ account. Please register. + public static func accountNotRegistered(_ p1: Any, _ p2: Any) -> String { + return AuthLocalization.tr("Localizable", "ERROR.ACCOUNT_NOT_REGISTERED", String(describing: p1), String(describing: p2), fallback: "This %@ account is not linked with any %@ account. Please register.") + } + /// Your account is disabled. Please contact customer support for assistance. + public static let disabledAccount = AuthLocalization.tr("Localizable", "ERROR.DISABLED_ACCOUNT", fallback: "Your account is disabled. Please contact customer support for assistance.") /// Invalid email address public static let invalidEmailAddress = AuthLocalization.tr("Localizable", "ERROR.INVALID_EMAIL_ADDRESS", fallback: "Invalid email address") /// Invalid email or username public static let invalidEmailAddressOrUsername = AuthLocalization.tr("Localizable", "ERROR.INVALID_EMAIL_ADDRESS_OR_USERNAME", fallback: "Invalid email or username") - /// Invalid password length - public static let invalidPasswordLength = AuthLocalization.tr("Localizable", "ERROR.INVALID_PASSWORD_LENGTH", fallback: "Invalid password length") + /// Invalid password lenght + public static let invalidPasswordLenght = AuthLocalization.tr("Localizable", "ERROR.INVALID_PASSWORD_LENGHT", fallback: "Invalid password lenght") } public enum Forgot { /// We have sent a password recover instructions to your email @@ -60,6 +80,10 @@ public enum AuthLocalization { public static let showFields = AuthLocalization.tr("Localizable", "SIGN_UP.SHOW_FIELDS", fallback: "Show optional Fields") /// Create new account. public static let subtitle = AuthLocalization.tr("Localizable", "SIGN_UP.SUBTITLE", fallback: "Create new account.") + /// You've successfully signed in. + public static let successSigninLabel = AuthLocalization.tr("Localizable", "SIGN_UP.SUCCESS_SIGNIN_LABEL", fallback: "You've successfully signed in.") + /// We just need a little more information before you start learning. + public static let successSigninSublabel = AuthLocalization.tr("Localizable", "SIGN_UP.SUCCESS_SIGNIN_SUBLABEL", fallback: "We just need a little more information before you start learning.") /// Sign up public static let title = AuthLocalization.tr("Localizable", "SIGN_UP.TITLE", fallback: "Sign up") } diff --git a/Authorization/Authorization/en.lproj/Localizable.strings b/Authorization/Authorization/en.lproj/Localizable.strings index d6c2890b5..50807ea57 100644 --- a/Authorization/Authorization/en.lproj/Localizable.strings +++ b/Authorization/Authorization/en.lproj/Localizable.strings @@ -16,14 +16,18 @@ "SIGN_IN.LOG_IN_BTN" = "Sign in"; "ERROR.INVALID_EMAIL_ADDRESS" = "Invalid email address"; +"ERROR.INVALID_PASSWORD_LENGHT" = "Invalid password lenght"; +"ERROR.ACCOUNT_NOT_REGISTERED" = "This %@ account is not linked with any %@ account. Please register."; "ERROR.INVALID_EMAIL_ADDRESS_OR_USERNAME" = "Invalid email or username"; -"ERROR.INVALID_PASSWORD_LENGTH" = "Invalid password length"; +"ERROR.DISABLED_ACCOUNT" = "Your account is disabled. Please contact customer support for assistance."; "SIGN_UP.TITLE" = "Sign up"; "SIGN_UP.SUBTITLE" = "Create new account."; "SIGN_UP.CREATE_ACCOUNT_BTN" = "Create account"; "SIGN_UP.HIDE_FIELDS" = "Hide optional Fields"; "SIGN_UP.SHOW_FIELDS" = "Show optional Fields"; +"SIGN_UP.SUCCESS_SIGNIN_LABEL" = "You've successfully signed in."; +"SIGN_UP.SUCCESS_SIGNIN_SUBLABEL" = "We just need a little more information before you start learning."; "FORGOT.TITLE"= "Forgot password"; "FORGOT.DESCRIPTION" = "Please enter your log-in or recovery email address below and we will send you an email with instructions."; @@ -31,6 +35,14 @@ "FORGOT.CHECK_TITLE" = "Check your email"; "FORGOT.CHECK_Description" = "We have sent a password recover instructions to your email "; +"SIGN_IN_WITH" = "Sign in with"; +"REGISTER_WITH" = "Register with"; +"APPLE" = "Apple"; +"GOOGLE" = "Google"; +"FACEBOOK" = "Facebook"; +"MICROSOFT" = "Microsoft"; +"OR" = "Or"; + "STARTUP.INFO_MESSAGE" = "Courses and programs from the world's best universities in your pocket."; "STARTUP.SEARCH_TITLE" = "What do you want to learn?"; "STARTUP.SEARCH_PLACEHOLDER" = "Search our 3000+ courses"; diff --git a/Authorization/Authorization/uk.lproj/Localizable.strings b/Authorization/Authorization/uk.lproj/Localizable.strings index 3a334d6d4..5cbcbb2d8 100644 --- a/Authorization/Authorization/uk.lproj/Localizable.strings +++ b/Authorization/Authorization/uk.lproj/Localizable.strings @@ -16,12 +16,16 @@ "ERROR.INVALID_EMAIL_ADDRESS" = "невірна адреса електронної пошти"; "ERROR.INVALID_PASSWORD_LENGHT" = "Пароль занадто короткий або занадто довгий"; +"ERROR.ACCOUNT_NOT_REGISTERED" = "This %@ account is not linked with any %@ account. Please register."; +"ERROR.DISABLED_ACCOUNT" = "Your account is disabled. Please contact customer support for assistance."; "SIGN_UP.TITLE" = "Зареєструватись"; "SIGN_UP.SUBTITLE" = "Cтворити новий акаунт."; "SIGN_UP.CREATE_ACCOUNT_BTN" = "Створити акаунт"; "SIGN_UP.HIDE_FIELDS" = "Приховати необовʼязкові поля"; "SIGN_UP.SHOW_FIELDS" = "Показати необовʼязкові поля"; +"SIGN_UP.SUCCESS_SIGNIN_LABEL" = "You've successfully signed in."; +"SIGN_UP.SUCCESS_SIGNIN_SUBLABEL" = "We just need a little more information before you start learning."; "FORGOT.TITLE"= "Відновлення паролю"; "FORGOT.DESCRIPTION" = "Будь ласка, введіть свою адресу електронної пошти для входу або відновлення нижче, і ми надішлемо вам електронний лист з інструкціями."; @@ -29,6 +33,14 @@ "FORGOT.CHECK_TITLE" = "Перевірте свою електронну пошту"; "FORGOT.CHECK_Description" = "Ми надіслали інструкції щодо відновлення пароля на вашу електронну пошту "; +"SIGN_IN_WITH" = "Sign in with"; +"REGISTER_WITH" = "Register with"; +"APPLE" = "Apple"; +"GOOGLE" = "Google"; +"FACEBOOK" = "Facebook"; +"MICROSOFT" = "Microsoft"; +"OR" = "Or"; + "STARTUP.INFO_MESSAGE" = "Courses and programs from the world's best universities in your pocket."; "STARTUP.SEARCH_TITLE" = "What do you want to learn?"; "STARTUP.SEARCH_PLACEHOLDER" = "Search our 3000+ courses"; diff --git a/Authorization/AuthorizationTests/AuthorizationMock.generated.swift b/Authorization/AuthorizationTests/AuthorizationMock.generated.swift index 98aae1963..d6a968438 100644 --- a/Authorization/AuthorizationTests/AuthorizationMock.generated.swift +++ b/Authorization/AuthorizationTests/AuthorizationMock.generated.swift @@ -76,6 +76,23 @@ open class AuthInteractorProtocolMock: AuthInteractorProtocol, Mock { 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`) + var __value: User + do { + __value = try methodReturnValue(.m_login__externalToken_externalTokenbackend_backend(Parameter.value(`externalToken`), Parameter.value(`backend`))).casted() + } catch MockError.notStubed { + onFatalFailure("Stub return value not specified for login(externalToken: String, backend: String). Use given") + Failure("Stub return value not specified for login(externalToken: String, backend: 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 @@ -121,16 +138,16 @@ open class AuthInteractorProtocolMock: AuthInteractorProtocol, Mock { return __value } - open func registerUser(fields: [String: String]) throws -> User { - addInvocation(.m_registerUser__fields_fields(Parameter<[String: String]>.value(`fields`))) - let perform = methodPerformValue(.m_registerUser__fields_fields(Parameter<[String: String]>.value(`fields`))) as? ([String: String]) -> Void - perform?(`fields`) + open func registerUser(fields: [String: String], isSocial: Bool) throws -> User { + addInvocation(.m_registerUser__fields_fieldsisSocial_isSocial(Parameter<[String: String]>.value(`fields`), Parameter.value(`isSocial`))) + let perform = methodPerformValue(.m_registerUser__fields_fieldsisSocial_isSocial(Parameter<[String: String]>.value(`fields`), Parameter.value(`isSocial`))) as? ([String: String], Bool) -> Void + perform?(`fields`, `isSocial`) var __value: User do { - __value = try methodReturnValue(.m_registerUser__fields_fields(Parameter<[String: String]>.value(`fields`))).casted() + __value = try methodReturnValue(.m_registerUser__fields_fieldsisSocial_isSocial(Parameter<[String: String]>.value(`fields`), Parameter.value(`isSocial`))).casted() } catch MockError.notStubed { - onFatalFailure("Stub return value not specified for registerUser(fields: [String: String]). Use given") - Failure("Stub return value not specified for registerUser(fields: [String: String]). Use given") + onFatalFailure("Stub return value not specified for registerUser(fields: [String: String], isSocial: Bool). Use given") + Failure("Stub return value not specified for registerUser(fields: [String: String], isSocial: Bool). Use given") } catch { throw error } @@ -156,10 +173,11 @@ open class AuthInteractorProtocolMock: AuthInteractorProtocol, Mock { fileprivate enum MethodType { case m_login__username_usernamepassword_password(Parameter, Parameter) + case m_login__externalToken_externalTokenbackend_backend(Parameter, Parameter) case m_resetPassword__email_email(Parameter) case m_getCookies__force_force(Parameter) case m_getRegistrationFields - case m_registerUser__fields_fields(Parameter<[String: String]>) + case m_registerUser__fields_fieldsisSocial_isSocial(Parameter<[String: String]>, Parameter) case m_validateRegistrationFields__fields_fields(Parameter<[String: String]>) static func compareParameters(lhs: MethodType, rhs: MethodType, matcher: Matcher) -> Matcher.ComparisonResult { @@ -170,6 +188,12 @@ 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__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")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBackend, rhs: rhsBackend, with: matcher), lhsBackend, rhsBackend, "backend")) + return Matcher.ComparisonResult(results) + case (.m_resetPassword__email_email(let lhsEmail), .m_resetPassword__email_email(let rhsEmail)): var results: [Matcher.ParameterComparisonResult] = [] results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsEmail, rhs: rhsEmail, with: matcher), lhsEmail, rhsEmail, "email")) @@ -182,9 +206,10 @@ open class AuthInteractorProtocolMock: AuthInteractorProtocol, Mock { case (.m_getRegistrationFields, .m_getRegistrationFields): return .match - case (.m_registerUser__fields_fields(let lhsFields), .m_registerUser__fields_fields(let rhsFields)): + case (.m_registerUser__fields_fieldsisSocial_isSocial(let lhsFields, let lhsIssocial), .m_registerUser__fields_fieldsisSocial_isSocial(let rhsFields, let rhsIssocial)): var results: [Matcher.ParameterComparisonResult] = [] results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsFields, rhs: rhsFields, with: matcher), lhsFields, rhsFields, "fields")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsIssocial, rhs: rhsIssocial, with: matcher), lhsIssocial, rhsIssocial, "isSocial")) return Matcher.ComparisonResult(results) case (.m_validateRegistrationFields__fields_fields(let lhsFields), .m_validateRegistrationFields__fields_fields(let rhsFields)): @@ -198,20 +223,22 @@ 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__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 case .m_getRegistrationFields: return 0 - case let .m_registerUser__fields_fields(p0): return p0.intValue + case let .m_registerUser__fields_fieldsisSocial_isSocial(p0, p1): return p0.intValue + p1.intValue case let .m_validateRegistrationFields__fields_fields(p0): return p0.intValue } } func assertionName() -> String { switch self { case .m_login__username_usernamepassword_password: return ".login(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:)" case .m_getRegistrationFields: return ".getRegistrationFields()" - case .m_registerUser__fields_fields: return ".registerUser(fields:)" + case .m_registerUser__fields_fieldsisSocial_isSocial: return ".registerUser(fields:isSocial:)" case .m_validateRegistrationFields__fields_fields: return ".validateRegistrationFields(fields:)" } } @@ -230,14 +257,18 @@ open class AuthInteractorProtocolMock: AuthInteractorProtocol, Mock { public static func login(username: Parameter, password: Parameter, willReturn: User...) -> MethodStub { return Given(method: .m_login__username_usernamepassword_password(`username`, `password`), products: willReturn.map({ StubProduct.return($0 as Any) })) } + @discardableResult + 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) })) + } 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) })) } public static func getRegistrationFields(willReturn: [PickerFields]...) -> MethodStub { return Given(method: .m_getRegistrationFields, products: willReturn.map({ StubProduct.return($0 as Any) })) } - public static func registerUser(fields: Parameter<[String: String]>, willReturn: User...) -> MethodStub { - return Given(method: .m_registerUser__fields_fields(`fields`), products: willReturn.map({ StubProduct.return($0 as Any) })) + public static func registerUser(fields: Parameter<[String: String]>, isSocial: Parameter, willReturn: User...) -> MethodStub { + return Given(method: .m_registerUser__fields_fieldsisSocial_isSocial(`fields`, `isSocial`), products: willReturn.map({ StubProduct.return($0 as Any) })) } public static func validateRegistrationFields(fields: Parameter<[String: String]>, willReturn: [String: String]...) -> MethodStub { return Given(method: .m_validateRegistrationFields__fields_fields(`fields`), products: willReturn.map({ StubProduct.return($0 as Any) })) @@ -254,6 +285,18 @@ open class AuthInteractorProtocolMock: AuthInteractorProtocol, Mock { willProduce(stubber) return given } + @discardableResult + public static func login(externalToken: Parameter, backend: Parameter, willThrow: Error...) -> MethodStub { + return Given(method: .m_login__externalToken_externalTokenbackend_backend(`externalToken`, `backend`), products: willThrow.map({ StubProduct.throw($0) })) + } + @discardableResult + public static func login(externalToken: Parameter, backend: Parameter, willProduce: (StubberThrows) -> Void) -> MethodStub { + let willThrow: [Error] = [] + let given: Given = { return Given(method: .m_login__externalToken_externalTokenbackend_backend(`externalToken`, `backend`), products: willThrow.map({ StubProduct.throw($0) })) }() + let stubber = given.stubThrows(for: (User).self) + willProduce(stubber) + return given + } public static func resetPassword(email: Parameter, willThrow: Error...) -> MethodStub { return Given(method: .m_resetPassword__email_email(`email`), products: willThrow.map({ StubProduct.throw($0) })) } @@ -284,12 +327,12 @@ open class AuthInteractorProtocolMock: AuthInteractorProtocol, Mock { willProduce(stubber) return given } - public static func registerUser(fields: Parameter<[String: String]>, willThrow: Error...) -> MethodStub { - return Given(method: .m_registerUser__fields_fields(`fields`), products: willThrow.map({ StubProduct.throw($0) })) + public static func registerUser(fields: Parameter<[String: String]>, isSocial: Parameter, willThrow: Error...) -> MethodStub { + return Given(method: .m_registerUser__fields_fieldsisSocial_isSocial(`fields`, `isSocial`), products: willThrow.map({ StubProduct.throw($0) })) } - public static func registerUser(fields: Parameter<[String: String]>, willProduce: (StubberThrows) -> Void) -> MethodStub { + public static func registerUser(fields: Parameter<[String: String]>, isSocial: Parameter, willProduce: (StubberThrows) -> Void) -> MethodStub { let willThrow: [Error] = [] - let given: Given = { return Given(method: .m_registerUser__fields_fields(`fields`), products: willThrow.map({ StubProduct.throw($0) })) }() + let given: Given = { return Given(method: .m_registerUser__fields_fieldsisSocial_isSocial(`fields`, `isSocial`), products: willThrow.map({ StubProduct.throw($0) })) }() let stubber = given.stubThrows(for: (User).self) willProduce(stubber) return given @@ -311,10 +354,12 @@ open class AuthInteractorProtocolMock: AuthInteractorProtocol, Mock { @discardableResult public static func login(username: Parameter, password: Parameter) -> Verify { return Verify(method: .m_login__username_usernamepassword_password(`username`, `password`))} + @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`))} public static func getCookies(force: Parameter) -> Verify { return Verify(method: .m_getCookies__force_force(`force`))} public static func getRegistrationFields() -> Verify { return Verify(method: .m_getRegistrationFields)} - public static func registerUser(fields: Parameter<[String: String]>) -> Verify { return Verify(method: .m_registerUser__fields_fields(`fields`))} + public static func registerUser(fields: Parameter<[String: String]>, isSocial: Parameter) -> Verify { return Verify(method: .m_registerUser__fields_fieldsisSocial_isSocial(`fields`, `isSocial`))} public static func validateRegistrationFields(fields: Parameter<[String: String]>) -> Verify { return Verify(method: .m_validateRegistrationFields__fields_fields(`fields`))} } @@ -326,6 +371,10 @@ open class AuthInteractorProtocolMock: AuthInteractorProtocol, Mock { public static func login(username: Parameter, password: Parameter, perform: @escaping (String, String) -> Void) -> Perform { return Perform(method: .m_login__username_usernamepassword_password(`username`, `password`), performs: perform) } + @discardableResult + public static func login(externalToken: Parameter, backend: Parameter, perform: @escaping (String, String) -> Void) -> Perform { + return Perform(method: .m_login__externalToken_externalTokenbackend_backend(`externalToken`, `backend`), performs: perform) + } public static func resetPassword(email: Parameter, perform: @escaping (String) -> Void) -> Perform { return Perform(method: .m_resetPassword__email_email(`email`), performs: perform) } @@ -335,8 +384,8 @@ open class AuthInteractorProtocolMock: AuthInteractorProtocol, Mock { public static func getRegistrationFields(perform: @escaping () -> Void) -> Perform { return Perform(method: .m_getRegistrationFields, performs: perform) } - public static func registerUser(fields: Parameter<[String: String]>, perform: @escaping ([String: String]) -> Void) -> Perform { - return Perform(method: .m_registerUser__fields_fields(`fields`), performs: perform) + public static func registerUser(fields: Parameter<[String: String]>, isSocial: Parameter, perform: @escaping ([String: String], Bool) -> Void) -> Perform { + return Perform(method: .m_registerUser__fields_fieldsisSocial_isSocial(`fields`, `isSocial`), performs: perform) } public static func validateRegistrationFields(fields: Parameter<[String: String]>, perform: @escaping ([String: String]) -> Void) -> Perform { return Perform(method: .m_validateRegistrationFields__fields_fields(`fields`), performs: perform) @@ -466,9 +515,9 @@ open class AuthorizationAnalyticsMock: AuthorizationAnalytics, Mock { perform?(`id`) } - open func userLogin(method: LoginMethod) { - addInvocation(.m_userLogin__method_method(Parameter.value(`method`))) - let perform = methodPerformValue(.m_userLogin__method_method(Parameter.value(`method`))) as? (LoginMethod) -> Void + open func userLogin(method: AuthMethod) { + addInvocation(.m_userLogin__method_method(Parameter.value(`method`))) + let perform = methodPerformValue(.m_userLogin__method_method(Parameter.value(`method`))) as? (AuthMethod) -> Void perform?(`method`) } @@ -505,7 +554,7 @@ open class AuthorizationAnalyticsMock: AuthorizationAnalytics, Mock { fileprivate enum MethodType { case m_setUserID__id(Parameter) - case m_userLogin__method_method(Parameter) + case m_userLogin__method_method(Parameter) case m_signUpClicked case m_createAccountClicked case m_registrationSuccess @@ -579,7 +628,7 @@ open class AuthorizationAnalyticsMock: AuthorizationAnalytics, Mock { fileprivate var method: MethodType public static func setUserID(_ id: Parameter) -> Verify { return Verify(method: .m_setUserID__id(`id`))} - public static func userLogin(method: Parameter) -> Verify { return Verify(method: .m_userLogin__method_method(`method`))} + public static func userLogin(method: Parameter) -> Verify { return Verify(method: .m_userLogin__method_method(`method`))} public static func signUpClicked() -> Verify { return Verify(method: .m_signUpClicked)} public static func createAccountClicked() -> Verify { return Verify(method: .m_createAccountClicked)} public static func registrationSuccess() -> Verify { return Verify(method: .m_registrationSuccess)} @@ -594,7 +643,7 @@ open class AuthorizationAnalyticsMock: AuthorizationAnalytics, Mock { public static func setUserID(_ id: Parameter, perform: @escaping (String) -> Void) -> Perform { return Perform(method: .m_setUserID__id(`id`), performs: perform) } - public static func userLogin(method: Parameter, perform: @escaping (LoginMethod) -> Void) -> Perform { + public static func userLogin(method: Parameter, perform: @escaping (AuthMethod) -> Void) -> Perform { return Perform(method: .m_userLogin__method_method(`method`), performs: perform) } public static func signUpClicked(perform: @escaping () -> Void) -> Perform { diff --git a/Authorization/AuthorizationTests/Presentation/Login/SignInViewModelTests.swift b/Authorization/AuthorizationTests/Presentation/Login/SignInViewModelTests.swift index 872219ba8..484807136 100644 --- a/Authorization/AuthorizationTests/Presentation/Login/SignInViewModelTests.swift +++ b/Authorization/AuthorizationTests/Presentation/Login/SignInViewModelTests.swift @@ -61,7 +61,7 @@ final class SignInViewModelTests: XCTestCase { Verify(interactor, 0, .login(username: .any, password: .any)) Verify(router, 0, .showMainOrWhatsNewScreen()) - XCTAssertEqual(viewModel.errorMessage, AuthLocalization.Error.invalidPasswordLength) + XCTAssertEqual(viewModel.errorMessage, AuthLocalization.Error.invalidPasswordLenght) XCTAssertEqual(viewModel.isShowProgress, false) } @@ -90,7 +90,71 @@ final class SignInViewModelTests: XCTestCase { XCTAssertEqual(viewModel.errorMessage, nil) XCTAssertEqual(viewModel.isShowProgress, true) } - + + func testSocialLoginSuccess() 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 + ) + + let result: Result = .success(.apple( + .init(name: "name", email: "email", token: "239i2oi3jrf2jflkj23lf2f")) + ) + let user = User(id: 1, username: "username", email: "edxUser@edx.com", name: "Name", userAvatar: "") + + Given(interactor, .login(externalToken: .any, backend: .any, willReturn: user)) + + await viewModel.login(with: result) + + Verify(interactor, 1, .login(externalToken: .any, backend: .any)) + Verify(analytics, .userLogin(method: .any)) + Verify(router, 1, .showMainOrWhatsNewScreen()) + + XCTAssertEqual(viewModel.errorMessage, nil) + XCTAssertEqual(viewModel.isShowProgress, true) + } + + func testSocialLoginErrorValidation() 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 + ) + + let result: Result = .success( + .apple(.init(name: "name", email: "email", token: "239i2oi3jrf2jflkj23lf2f")) + ) + let validationErrorMessage = AuthLocalization.Error.accountNotRegistered( + AuthMethod.socailAuth(.apple).analyticsValue, + viewModel.config.platformName + ) + let validationError = CustomValidationError(statusCode: 400, data: ["error_description": validationErrorMessage]) + let error = AFError.responseValidationFailed(reason: AFError.ResponseValidationFailureReason.customValidationFailed(error: validationError)) + + Given(interactor, .login(externalToken: .any, backend: .any, willThrow: error)) + + await viewModel.login(with: result) + + Verify(interactor, 1, .login(externalToken: .any, backend: .any)) + Verify(router, 0, .showMainOrWhatsNewScreen()) + + XCTAssertEqual(viewModel.errorMessage, validationErrorMessage) + XCTAssertEqual(viewModel.isShowProgress, false) + } + func testLoginErrorValidation() async throws { let interactor = AuthInteractorProtocolMock() let router = AuthorizationRouterMock() @@ -118,7 +182,7 @@ final class SignInViewModelTests: XCTestCase { XCTAssertEqual(viewModel.errorMessage, validationErrorMessage) XCTAssertEqual(viewModel.isShowProgress, false) } - + func testLoginErrorInvalidGrant() async throws { let interactor = AuthInteractorProtocolMock() let router = AuthorizationRouterMock() diff --git a/Authorization/AuthorizationTests/Presentation/Register/SignUpViewModelTests.swift b/Authorization/AuthorizationTests/Presentation/Register/SignUpViewModelTests.swift index b59519b27..77f9ad85e 100644 --- a/Authorization/AuthorizationTests/Presentation/Register/SignUpViewModelTests.swift +++ b/Authorization/AuthorizationTests/Presentation/Register/SignUpViewModelTests.swift @@ -117,7 +117,7 @@ final class SignUpViewModelTests: XCTestCase { validator: validator ) - Given(interactor, .registerUser(fields: .any, willReturn: .init(id: 1, + Given(interactor, .registerUser(fields: .any, isSocial: .any, willReturn: .init(id: 1, username: "Name", email: "mail", name: "name", @@ -127,7 +127,7 @@ final class SignUpViewModelTests: XCTestCase { await viewModel.registerUser() Verify(interactor, 1, .validateRegistrationFields(fields: .any)) - Verify(interactor, 1, .registerUser(fields: .any)) + Verify(interactor, 1, .registerUser(fields: .any, isSocial: .any)) Verify(router, 1, .showMainOrWhatsNewScreen()) XCTAssertEqual(viewModel.isShowProgress, false) @@ -158,12 +158,12 @@ final class SignUpViewModelTests: XCTestCase { ] Given(interactor, .validateRegistrationFields(fields: .any, willReturn: ["email": "invalid email"])) - Given(interactor, .registerUser(fields: .any, willProduce: {_ in})) - + Given(interactor, .registerUser(fields: .any, isSocial: .any, willProduce: {_ in})) + await viewModel.registerUser() Verify(interactor, 1, .validateRegistrationFields(fields: .any)) - Verify(interactor, 0, .registerUser(fields: .any)) + Verify(interactor, 0, .registerUser(fields: .any, isSocial: .any)) Verify(router, 0, .showMainOrWhatsNewScreen()) XCTAssertEqual(viewModel.isShowProgress, false) @@ -186,12 +186,12 @@ final class SignUpViewModelTests: XCTestCase { ) Given(interactor, .validateRegistrationFields(fields: .any, willReturn: [:])) - Given(interactor, .registerUser(fields: .any, willThrow: APIError.invalidGrant)) - + Given(interactor, .registerUser(fields: .any, isSocial: .any, willThrow: APIError.invalidGrant)) + await viewModel.registerUser() Verify(interactor, 1, .validateRegistrationFields(fields: .any)) - Verify(interactor, 1, .registerUser(fields: .any)) + Verify(interactor, 1, .registerUser(fields: .any, isSocial: .any)) Verify(router, 0, .showMainOrWhatsNewScreen()) XCTAssertEqual(viewModel.isShowProgress, false) @@ -214,12 +214,12 @@ final class SignUpViewModelTests: XCTestCase { ) Given(interactor, .validateRegistrationFields(fields: .any, willReturn: [:])) - Given(interactor, .registerUser(fields: .any, willThrow: NSError())) + Given(interactor, .registerUser(fields: .any, isSocial: .any, willThrow: NSError())) await viewModel.registerUser() Verify(interactor, 1, .validateRegistrationFields(fields: .any)) - Verify(interactor, 1, .registerUser(fields: .any)) + Verify(interactor, 1, .registerUser(fields: .any, isSocial: .any)) Verify(router, 0, .showMainOrWhatsNewScreen()) XCTAssertEqual(viewModel.isShowProgress, false) @@ -243,13 +243,13 @@ final class SignUpViewModelTests: XCTestCase { let noInternetError = AFError.sessionInvalidated(error: URLError(.notConnectedToInternet)) - Given(interactor, .registerUser(fields: .any, willThrow: noInternetError)) + Given(interactor, .registerUser(fields: .any, isSocial: .any, willThrow: noInternetError)) Given(interactor, .validateRegistrationFields(fields: .any, willReturn: [:])) await viewModel.registerUser() Verify(interactor, 1, .validateRegistrationFields(fields: .any)) - Verify(interactor, 1, .registerUser(fields: .any)) + Verify(interactor, 1, .registerUser(fields: .any, isSocial: .any)) Verify(router, 0, .showMainOrWhatsNewScreen()) XCTAssertEqual(viewModel.isShowProgress, false) diff --git a/Core/Core.xcodeproj/project.pbxproj b/Core/Core.xcodeproj/project.pbxproj index a54379167..6d9e449a4 100644 --- a/Core/Core.xcodeproj/project.pbxproj +++ b/Core/Core.xcodeproj/project.pbxproj @@ -117,6 +117,21 @@ 0770DE5F28D0B22C006D8A5D /* Strings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0770DE5E28D0B22C006D8A5D /* Strings.swift */; }; 0770DE6128D0B2CB006D8A5D /* Assets.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0770DE6028D0B2CB006D8A5D /* Assets.swift */; }; 07DDFCBD29A780BB00572595 /* UINavigationController+Animation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 07DDFCBC29A780BB00572595 /* UINavigationController+Animation.swift */; }; + BA30427F2B20B320009B64B7 /* SocialAuthError.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA30427D2B20B299009B64B7 /* SocialAuthError.swift */; }; + BA76135C2B21BC7300B599B7 /* SocialAuthResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA76135B2B21BC7300B599B7 /* SocialAuthResponse.swift */; }; + BA8B3A2F2AD546A700D25EF5 /* DebugLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA8B3A2E2AD546A700D25EF5 /* DebugLog.swift */; }; + BA8FA6612AD5974300EA029A /* AppleAuthProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA8FA6602AD5974300EA029A /* AppleAuthProvider.swift */; }; + BA8FA6682AD59A5700EA029A /* SocialAuthButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA8FA6672AD59A5700EA029A /* SocialAuthButton.swift */; }; + BA8FA66A2AD59B5500EA029A /* GoogleAuthProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA8FA6692AD59B5500EA029A /* GoogleAuthProvider.swift */; }; + BA8FA66C2AD59BBC00EA029A /* GoogleSignIn in Frameworks */ = {isa = PBXBuildFile; productRef = BA8FA66B2AD59BBC00EA029A /* GoogleSignIn */; }; + BA8FA66E2AD59E7D00EA029A /* FacebookAuthProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA8FA66D2AD59E7D00EA029A /* FacebookAuthProvider.swift */; }; + BA8FA6702AD59EA300EA029A /* MicrosoftAuthProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA8FA66F2AD59EA300EA029A /* MicrosoftAuthProvider.swift */; }; + BADB3F5B2AD6EC56004D5CFA /* ResultExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = BADB3F5A2AD6EC56004D5CFA /* ResultExtension.swift */; }; + BAF0D4CB2AD6AE14007AC334 /* FacebookLogin in Frameworks */ = {isa = PBXBuildFile; productRef = BAF0D4CA2AD6AE14007AC334 /* FacebookLogin */; }; + BAFB99822B0E2354007D09F9 /* FacebookConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAFB99812B0E2354007D09F9 /* FacebookConfig.swift */; }; + BAFB99842B0E282E007D09F9 /* MicrosoftConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAFB99832B0E282E007D09F9 /* MicrosoftConfig.swift */; }; + BAFB99902B14B377007D09F9 /* GoogleConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAFB998F2B14B377007D09F9 /* GoogleConfig.swift */; }; + BAFB99922B14E23D007D09F9 /* AppleSignInConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAFB99912B14E23D007D09F9 /* AppleSignInConfig.swift */; }; C8C446EF233F81B9FABB77D2 /* Pods_App_Core.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 349B90CD6579F7B8D257E515 /* Pods_App_Core.framework */; }; CFC84952299F8B890055E497 /* Debounce.swift in Sources */ = {isa = PBXBuildFile; fileRef = CFC84951299F8B890055E497 /* Debounce.swift */; }; DBF6F2412B014ADA0098414B /* FirebaseConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBF6F2402B014ADA0098414B /* FirebaseConfig.swift */; }; @@ -271,6 +286,19 @@ 3B74C6685E416657F3C5F5A8 /* Pods-App-Core.releaseprod.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-Core.releaseprod.xcconfig"; path = "Target Support Files/Pods-App-Core/Pods-App-Core.releaseprod.xcconfig"; sourceTree = ""; }; 60153262DBC2F9E660D7E11B /* Pods-App-Core.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-Core.release.xcconfig"; path = "Target Support Files/Pods-App-Core/Pods-App-Core.release.xcconfig"; sourceTree = ""; }; 9D5B06CAA99EA5CD49CBE2BB /* Pods-App-Core.debugdev.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-Core.debugdev.xcconfig"; path = "Target Support Files/Pods-App-Core/Pods-App-Core.debugdev.xcconfig"; sourceTree = ""; }; + BA30427D2B20B299009B64B7 /* SocialAuthError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SocialAuthError.swift; sourceTree = ""; }; + BA76135B2B21BC7300B599B7 /* SocialAuthResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SocialAuthResponse.swift; sourceTree = ""; }; + BA8B3A2E2AD546A700D25EF5 /* DebugLog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DebugLog.swift; sourceTree = ""; }; + BA8FA6602AD5974300EA029A /* AppleAuthProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppleAuthProvider.swift; sourceTree = ""; }; + BA8FA6672AD59A5700EA029A /* SocialAuthButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SocialAuthButton.swift; sourceTree = ""; }; + BA8FA6692AD59B5500EA029A /* GoogleAuthProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GoogleAuthProvider.swift; sourceTree = ""; }; + BA8FA66D2AD59E7D00EA029A /* FacebookAuthProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FacebookAuthProvider.swift; sourceTree = ""; }; + BA8FA66F2AD59EA300EA029A /* MicrosoftAuthProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MicrosoftAuthProvider.swift; sourceTree = ""; }; + BADB3F5A2AD6EC56004D5CFA /* ResultExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResultExtension.swift; sourceTree = ""; }; + BAFB99812B0E2354007D09F9 /* FacebookConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FacebookConfig.swift; sourceTree = ""; }; + BAFB99832B0E282E007D09F9 /* MicrosoftConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MicrosoftConfig.swift; sourceTree = ""; }; + BAFB998F2B14B377007D09F9 /* GoogleConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GoogleConfig.swift; sourceTree = ""; }; + BAFB99912B14E23D007D09F9 /* AppleSignInConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppleSignInConfig.swift; sourceTree = ""; }; C7E5BCE79CE297B20777B27A /* Pods-App-Core.debugprod.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-Core.debugprod.xcconfig"; path = "Target Support Files/Pods-App-Core/Pods-App-Core.debugprod.xcconfig"; sourceTree = ""; }; CFC84951299F8B890055E497 /* Debounce.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Debounce.swift; sourceTree = ""; }; DBF6F2402B014ADA0098414B /* FirebaseConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FirebaseConfig.swift; sourceTree = ""; }; @@ -293,8 +321,10 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + BAF0D4CB2AD6AE14007AC334 /* FacebookLogin in Frameworks */, 025EF2F62971740000B838AB /* YouTubePlayerKit in Frameworks */, C8C446EF233F81B9FABB77D2 /* Pods_App_Core.framework in Frameworks */, + BA8FA66C2AD59BBC00EA029A /* GoogleSignIn in Frameworks */, E055A5392B18DC95008D9E5E /* Theme.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -387,6 +417,8 @@ 0727878228D31287002E9142 /* DispatchQueue+App.swift */, 07DDFCBC29A780BB00572595 /* UINavigationController+Animation.swift */, 02284C172A3B1AE00007117F /* UIApplicationExtension.swift */, + BA8B3A2E2AD546A700D25EF5 /* DebugLog.swift */, + BADB3F5A2AD6EC56004D5CFA /* ResultExtension.swift */, 02D400602B0678190029D168 /* SKStoreReviewControllerExtension.swift */, ); path = Extensions; @@ -514,6 +546,7 @@ 0770DE0A28D07831006D8A5D /* Core */ = { isa = PBXGroup; children = ( + BA8FA65F2AD5973500EA029A /* Providers */, 027BD3A12909470F00392132 /* AvoidingHelpers */, 0770DE5528D0B142006D8A5D /* SwiftGen */, 0283347E28D4DCC100C828FC /* Extensions */, @@ -593,11 +626,41 @@ 027BD3C42909707700392132 /* Shake.swift */, 023A1135291432B200D0D354 /* RegistrationTextField.swift */, 023A1137291432FD00D0D354 /* FieldConfiguration.swift */, + BA8FA6672AD59A5700EA029A /* SocialAuthButton.swift */, 02E93F862AEBAED4006C4750 /* AppReview */, ); path = Base; sourceTree = ""; }; + BA30427C2B20B235009B64B7 /* SocialAuth */ = { + isa = PBXGroup; + children = ( + BA30427E2B20B299009B64B7 /* Error */, + BA8FA6602AD5974300EA029A /* AppleAuthProvider.swift */, + BA8FA6692AD59B5500EA029A /* GoogleAuthProvider.swift */, + BA8FA66D2AD59E7D00EA029A /* FacebookAuthProvider.swift */, + BA8FA66F2AD59EA300EA029A /* MicrosoftAuthProvider.swift */, + BA76135B2B21BC7300B599B7 /* SocialAuthResponse.swift */, + ); + path = SocialAuth; + sourceTree = ""; + }; + BA30427E2B20B299009B64B7 /* Error */ = { + isa = PBXGroup; + children = ( + BA30427D2B20B299009B64B7 /* SocialAuthError.swift */, + ); + path = Error; + sourceTree = ""; + }; + BA8FA65F2AD5973500EA029A /* Providers */ = { + isa = PBXGroup; + children = ( + BA30427C2B20B235009B64B7 /* SocialAuth */, + ); + path = Providers; + sourceTree = ""; + }; C9DFE47E699CFFA85A77AF2C /* Pods */ = { isa = PBXGroup; children = ( @@ -629,6 +692,10 @@ DBF6F2402B014ADA0098414B /* FirebaseConfig.swift */, DBF6F2492B0380E00098414B /* FeaturesConfig.swift */, DBF6F2452B01DAFE0098414B /* AgreementConfig.swift */, + BAFB99812B0E2354007D09F9 /* FacebookConfig.swift */, + BAFB99832B0E282E007D09F9 /* MicrosoftConfig.swift */, + BAFB998F2B14B377007D09F9 /* GoogleConfig.swift */, + BAFB99912B14E23D007D09F9 /* AppleSignInConfig.swift */, ); path = Config; sourceTree = ""; @@ -708,6 +775,8 @@ name = Core; packageProductDependencies = ( 025EF2F52971740000B838AB /* YouTubePlayerKit */, + BA8FA66B2AD59BBC00EA029A /* GoogleSignIn */, + BAF0D4CA2AD6AE14007AC334 /* FacebookLogin */, ); productName = Core; productReference = 0770DE0828D07831006D8A5D /* Core.framework */; @@ -745,6 +814,8 @@ mainGroup = 0770DDFE28D07831006D8A5D; packageReferences = ( 025EF2F42971740000B838AB /* XCRemoteSwiftPackageReference "YouTubePlayerKit" */, + BA8FA65E2AD574D700EA029A /* XCRemoteSwiftPackageReference "GoogleSignIn-iOS" */, + BA8FA6712AD6ABA300EA029A /* XCRemoteSwiftPackageReference "facebook-ios-sdk" */, ); productRefGroup = 0770DE0928D07831006D8A5D /* Products */; projectDirPath = ""; @@ -837,6 +908,7 @@ 02F6EF4A28D9F0A700835477 /* DateExtension.swift in Sources */, DBF6F2462B01DAFE0098414B /* AgreementConfig.swift in Sources */, 027BD3AF2909475000392132 /* DismissKeyboardTapHandler.swift in Sources */, + BAFB99902B14B377007D09F9 /* GoogleConfig.swift in Sources */, 02E225B0291D29EB0067769A /* UrlExtension.swift in Sources */, 02CF46C829546AA200A698EE /* NoCachedDataError.swift in Sources */, 0727877728D23847002E9142 /* DataLayer.swift in Sources */, @@ -847,6 +919,7 @@ 0770DE2A28D0929E006D8A5D /* HTTPTask.swift in Sources */, 02284C182A3B1AE00007117F /* UIApplicationExtension.swift in Sources */, 0255D5582936283A004DBC1A /* UploadBodyEncoding.swift in Sources */, + BAFB99822B0E2354007D09F9 /* FacebookConfig.swift in Sources */, 027BD3B32909475900392132 /* Publishers+KeyboardState.swift in Sources */, 0727877D28D25212002E9142 /* ProgressBar.swift in Sources */, 0236961F28F9A2F600EEF206 /* AuthEndpoint.swift in Sources */, @@ -863,12 +936,14 @@ 028F9F37293A44C700DE65D0 /* Data_ResetPassword.swift in Sources */, 022C64E429AE0191000F532B /* TextWithUrls.swift in Sources */, 0283348028D4DCD200C828FC /* ViewExtension.swift in Sources */, + BA8FA66A2AD59B5500EA029A /* GoogleAuthProvider.swift in Sources */, 02A4833529B8A73400D33F33 /* CorePersistenceProtocol.swift in Sources */, 0233D5732AF13EEE00BAC8BD /* AppReviewButton.swift in Sources */, 02512FF0299533DF0024D438 /* CoreDataHandlerProtocol.swift in Sources */, 0260E58028FD792800BBBE18 /* WebUnitViewModel.swift in Sources */, 02A4833A29B8A9AB00D33F33 /* DownloadManager.swift in Sources */, 027BD3AE2909475000392132 /* KeyboardScrollerOptions.swift in Sources */, + BAFB99922B14E23D007D09F9 /* AppleSignInConfig.swift in Sources */, 027BD3BE2909478B00392132 /* UIResponder+CurrentResponder.swift in Sources */, 070019AE28F701B200D5FC78 /* Certificate.swift in Sources */, 076F297F2A1F80C800967E7D /* Pagination.swift in Sources */, @@ -884,6 +959,9 @@ 027BD3A82909474200392132 /* KeyboardAvoidingViewController.swift in Sources */, 02E93F852AEBAEBC006C4750 /* AppReviewViewModel.swift in Sources */, 0770DE2528D08FBA006D8A5D /* CoreStorage.swift in Sources */, + BA8FA6612AD5974300EA029A /* AppleAuthProvider.swift in Sources */, + BA8FA6702AD59EA300EA029A /* MicrosoftAuthProvider.swift in Sources */, + BA76135C2B21BC7300B599B7 /* SocialAuthResponse.swift in Sources */, 020306CC2932C0C4000949EA /* PickerView.swift in Sources */, 027BD3C52909707700392132 /* Shake.swift in Sources */, 027BD39C2908810C00392132 /* RegisterUser.swift in Sources */, @@ -898,6 +976,7 @@ 021D925028DC89D100ACC565 /* UserProfile.swift in Sources */, 071009D028D1E3A600344290 /* Constants.swift in Sources */, 0770DE1928D0847D006D8A5D /* BaseRouter.swift in Sources */, + BA30427F2B20B320009B64B7 /* SocialAuthError.swift in Sources */, 0284DBFE28D48C5300830893 /* CourseItem.swift in Sources */, 0248C92329C075EF00DC8402 /* CourseBlockModel.swift in Sources */, DBF6F2412B014ADA0098414B /* FirebaseConfig.swift in Sources */, @@ -905,15 +984,19 @@ 0236961D28F9A2D200EEF206 /* Data_AuthResponse.swift in Sources */, 02AFCC182AEFDB24000360F0 /* ThirdPartyMailClient.swift in Sources */, 0233D5712AF13EC800BAC8BD /* SelectMailClientView.swift in Sources */, + BAFB99842B0E282E007D09F9 /* MicrosoftConfig.swift in Sources */, 02B2B594295C5C7A00914876 /* Thread.swift in Sources */, 027DB33528D8C8FE002B6862 /* Data_MyCourse.swift in Sources */, 027BD3BD2909478B00392132 /* UIView+EnclosingScrollView.swift in Sources */, + BA8FA6682AD59A5700EA029A /* SocialAuthButton.swift in Sources */, 02D400612B0678190029D168 /* SKStoreReviewControllerExtension.swift in Sources */, 02A4833C29B8C57800D33F33 /* DownloadView.swift in Sources */, 027BD3AD2909475000392132 /* KeyboardScroller.swift in Sources */, 070019A528F6F17900D5FC78 /* Data_Media.swift in Sources */, 024D723529C8BB1A006D36ED /* NavigationBar.swift in Sources */, + BA8B3A2F2AD546A700D25EF5 /* DebugLog.swift in Sources */, 0727877928D23BE0002E9142 /* RequestInterceptor.swift in Sources */, + BA8FA66E2AD59E7D00EA029A /* FacebookAuthProvider.swift in Sources */, 0770DE2E28D09743006D8A5D /* API.swift in Sources */, 028CE96929858ECC00B6B1C3 /* FlexibleKeyboardInputView.swift in Sources */, 027BD3A92909474200392132 /* KeyboardAvoidingViewControllerRepr.swift in Sources */, @@ -929,6 +1012,7 @@ DBF6F24A2B0380E00098414B /* FeaturesConfig.swift in Sources */, 02F164372902A9EB0090DDEF /* StringExtension.swift in Sources */, 0231CDBE2922422D00032416 /* CSSInjector.swift in Sources */, + BADB3F5B2AD6EC56004D5CFA /* ResultExtension.swift in Sources */, 0236961928F9A26900EEF206 /* AuthRepository.swift in Sources */, 023A1136291432B200D0D354 /* RegistrationTextField.swift in Sources */, 02C2DC0829B63D6200F4445D /* WebViewHTML.swift in Sources */, @@ -1964,6 +2048,22 @@ minimumVersion = 1.5.0; }; }; + BA8FA65E2AD574D700EA029A /* XCRemoteSwiftPackageReference "GoogleSignIn-iOS" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/google/GoogleSignIn-iOS"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 7.0.0; + }; + }; + BA8FA6712AD6ABA300EA029A /* XCRemoteSwiftPackageReference "facebook-ios-sdk" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/facebook/facebook-ios-sdk"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 14.1.0; + }; + }; /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ @@ -1972,6 +2072,16 @@ package = 025EF2F42971740000B838AB /* XCRemoteSwiftPackageReference "YouTubePlayerKit" */; productName = YouTubePlayerKit; }; + BA8FA66B2AD59BBC00EA029A /* GoogleSignIn */ = { + isa = XCSwiftPackageProductDependency; + package = BA8FA65E2AD574D700EA029A /* XCRemoteSwiftPackageReference "GoogleSignIn-iOS" */; + productName = GoogleSignIn; + }; + BAF0D4CA2AD6AE14007AC334 /* FacebookLogin */ = { + isa = XCSwiftPackageProductDependency; + package = BA8FA6712AD6ABA300EA029A /* XCRemoteSwiftPackageReference "facebook-ios-sdk" */; + productName = FacebookLogin; + }; /* End XCSwiftPackageProductDependency section */ /* Begin XCVersionGroup section */ diff --git a/Core/Core/Assets.xcassets/Colors/AppleButtonColor.colorset/Contents.json b/Core/Core/Assets.xcassets/Colors/AppleButtonColor.colorset/Contents.json new file mode 100644 index 000000000..be9d677bb --- /dev/null +++ b/Core/Core/Assets.xcassets/Colors/AppleButtonColor.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0x00", + "green" : "0x00", + "red" : "0x00" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0x00", + "green" : "0x00", + "red" : "0x00" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Core/Core/Assets.xcassets/Colors/FacebookButtonColor.colorset/Contents.json b/Core/Core/Assets.xcassets/Colors/FacebookButtonColor.colorset/Contents.json new file mode 100644 index 000000000..1b6540850 --- /dev/null +++ b/Core/Core/Assets.xcassets/Colors/FacebookButtonColor.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "display-p3", + "components" : { + "alpha" : "1.000", + "blue" : "0xEA", + "green" : "0x73", + "red" : "0x38" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0xEA", + "green" : "0x73", + "red" : "0x38" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Core/Core/Assets.xcassets/Colors/GoogleButtonColor.colorset/Contents.json b/Core/Core/Assets.xcassets/Colors/GoogleButtonColor.colorset/Contents.json new file mode 100644 index 000000000..8cd6714c9 --- /dev/null +++ b/Core/Core/Assets.xcassets/Colors/GoogleButtonColor.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "display-p3", + "components" : { + "alpha" : "1.000", + "blue" : "0xF2", + "green" : "0xF2", + "red" : "0xF2" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0xF2", + "green" : "0xF2", + "red" : "0xF2" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Core/Core/Assets.xcassets/Colors/MicrosoftButtonColor.colorset/Contents.json b/Core/Core/Assets.xcassets/Colors/MicrosoftButtonColor.colorset/Contents.json new file mode 100644 index 000000000..45a391bd7 --- /dev/null +++ b/Core/Core/Assets.xcassets/Colors/MicrosoftButtonColor.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "display-p3", + "components" : { + "alpha" : "1.000", + "blue" : "0x2E", + "green" : "0x2E", + "red" : "0x2E" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0x2E", + "green" : "0x2E", + "red" : "0x2E" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Core/Core/Assets.xcassets/Socials/Contents.json b/Core/Core/Assets.xcassets/Socials/Contents.json new file mode 100644 index 000000000..73c00596a --- /dev/null +++ b/Core/Core/Assets.xcassets/Socials/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Core/Core/Assets.xcassets/Socials/icon_apple.imageset/Contents.json b/Core/Core/Assets.xcassets/Socials/icon_apple.imageset/Contents.json new file mode 100644 index 000000000..f98d12c7c --- /dev/null +++ b/Core/Core/Assets.xcassets/Socials/icon_apple.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "icon_apple@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "icon_apple@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Core/Core/Assets.xcassets/Socials/icon_apple.imageset/icon_apple@2x.png b/Core/Core/Assets.xcassets/Socials/icon_apple.imageset/icon_apple@2x.png new file mode 100644 index 000000000..6ca46033a Binary files /dev/null and b/Core/Core/Assets.xcassets/Socials/icon_apple.imageset/icon_apple@2x.png differ diff --git a/Core/Core/Assets.xcassets/Socials/icon_apple.imageset/icon_apple@3x.png b/Core/Core/Assets.xcassets/Socials/icon_apple.imageset/icon_apple@3x.png new file mode 100644 index 000000000..87995a5ef Binary files /dev/null and b/Core/Core/Assets.xcassets/Socials/icon_apple.imageset/icon_apple@3x.png differ diff --git a/Core/Core/Assets.xcassets/Socials/icon_facebook_white.imageset/Contents.json b/Core/Core/Assets.xcassets/Socials/icon_facebook_white.imageset/Contents.json new file mode 100644 index 000000000..af6600d1a --- /dev/null +++ b/Core/Core/Assets.xcassets/Socials/icon_facebook_white.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "icon_facebook_white@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "icon_facebook_white@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Core/Core/Assets.xcassets/Socials/icon_facebook_white.imageset/icon_facebook_white@2x.png b/Core/Core/Assets.xcassets/Socials/icon_facebook_white.imageset/icon_facebook_white@2x.png new file mode 100644 index 000000000..109a67604 Binary files /dev/null and b/Core/Core/Assets.xcassets/Socials/icon_facebook_white.imageset/icon_facebook_white@2x.png differ diff --git a/Core/Core/Assets.xcassets/Socials/icon_facebook_white.imageset/icon_facebook_white@3x.png b/Core/Core/Assets.xcassets/Socials/icon_facebook_white.imageset/icon_facebook_white@3x.png new file mode 100644 index 000000000..1623f36fd Binary files /dev/null and b/Core/Core/Assets.xcassets/Socials/icon_facebook_white.imageset/icon_facebook_white@3x.png differ diff --git a/Core/Core/Assets.xcassets/Socials/icon_google_white.imageset/Contents.json b/Core/Core/Assets.xcassets/Socials/icon_google_white.imageset/Contents.json new file mode 100644 index 000000000..3c4d94ecc --- /dev/null +++ b/Core/Core/Assets.xcassets/Socials/icon_google_white.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "icon_google_white@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "icon_google_white@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Core/Core/Assets.xcassets/Socials/icon_google_white.imageset/icon_google_white@2x.png b/Core/Core/Assets.xcassets/Socials/icon_google_white.imageset/icon_google_white@2x.png new file mode 100644 index 000000000..64c2157f9 Binary files /dev/null and b/Core/Core/Assets.xcassets/Socials/icon_google_white.imageset/icon_google_white@2x.png differ diff --git a/Core/Core/Assets.xcassets/Socials/icon_google_white.imageset/icon_google_white@3x.png b/Core/Core/Assets.xcassets/Socials/icon_google_white.imageset/icon_google_white@3x.png new file mode 100644 index 000000000..56b7b28d3 Binary files /dev/null and b/Core/Core/Assets.xcassets/Socials/icon_google_white.imageset/icon_google_white@3x.png differ diff --git a/Core/Core/Assets.xcassets/Socials/icon_microsoft_white.imageset/Contents.json b/Core/Core/Assets.xcassets/Socials/icon_microsoft_white.imageset/Contents.json new file mode 100644 index 000000000..be9b6142b --- /dev/null +++ b/Core/Core/Assets.xcassets/Socials/icon_microsoft_white.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "icon_microsoft_white@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "icon_microsoft_white@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Core/Core/Assets.xcassets/Socials/icon_microsoft_white.imageset/icon_microsoft_white@2x.png b/Core/Core/Assets.xcassets/Socials/icon_microsoft_white.imageset/icon_microsoft_white@2x.png new file mode 100644 index 000000000..bc596b581 Binary files /dev/null and b/Core/Core/Assets.xcassets/Socials/icon_microsoft_white.imageset/icon_microsoft_white@2x.png differ diff --git a/Core/Core/Assets.xcassets/Socials/icon_microsoft_white.imageset/icon_microsoft_white@3x.png b/Core/Core/Assets.xcassets/Socials/icon_microsoft_white.imageset/icon_microsoft_white@3x.png new file mode 100644 index 000000000..586c06636 Binary files /dev/null and b/Core/Core/Assets.xcassets/Socials/icon_microsoft_white.imageset/icon_microsoft_white@3x.png differ diff --git a/Core/Core/Configuration/Config/AppleSignInConfig.swift b/Core/Core/Configuration/Config/AppleSignInConfig.swift new file mode 100644 index 000000000..8e5817e90 --- /dev/null +++ b/Core/Core/Configuration/Config/AppleSignInConfig.swift @@ -0,0 +1,28 @@ +// +// SocialLoginConfig.swift +// Core +// +// Created by Eugene Yatsenko on 27.11.2023. +// + +import Foundation + +private enum AppleSignInKeys: String { + case enabled = "ENABLED" +} + +public class AppleSignInConfig: NSObject { + public var enabled: Bool + + init(dictionary: [String: Any]) { + enabled = dictionary[AppleSignInKeys.enabled.rawValue] as? Bool ?? false + super.init() + } +} + +private let key = "APPLE_SIGNIN" +extension Config { + public var appleSignIn: AppleSignInConfig { + AppleSignInConfig(dictionary: self[key] as? [String: AnyObject] ?? [:]) + } +} diff --git a/Core/Core/Configuration/Config/Config.swift b/Core/Core/Configuration/Config/Config.swift index 49f1ffdc5..8468cd673 100644 --- a/Core/Core/Configuration/Config/Config.swift +++ b/Core/Core/Configuration/Config/Config.swift @@ -13,8 +13,13 @@ public protocol ConfigProtocol { var tokenType: TokenType { get } var feedbackEmail: String { get } var appStoreLink: String { get } + var platformName: String { get } var agreement: AgreementConfig { get } var firebase: FirebaseConfig { get } + var facebook: FacebookConfig { get } + var microsoft: MicrosoftConfig { get } + var google: GoogleConfig { get } + var appleSignIn: AppleSignInConfig { get } var features: FeaturesConfig { get } } @@ -119,7 +124,11 @@ extension Config: ConfigProtocol { public var feedbackEmail: String { return string(for: ConfigKeys.feedbackEmailAddress.rawValue) ?? "" } - + + public var platformName: String { + return string(for: ConfigKeys.platformName.rawValue) ?? "" + } + private var appStoreId: String { return string(for: ConfigKeys.appstoreID.rawValue) ?? "0000000000" } @@ -136,11 +145,28 @@ public class ConfigMock: Config { "API_HOST_URL": "https://www.example.com", "OAUTH_CLIENT_ID": "oauth_client_id", "FEEDBACK_EMAIL_ADDRESS": "example@mail.com", + "PLATFORM_NAME": "OpenEdx", "TOKEN_TYPE": "JWT", "WHATS_NEW_ENABLED": false, "AGREEMENT_URLS": [ "PRIVACY_POLICY_URL": "https://www.example.com/privacy", "TOS_URL": "https://www.example.com/tos" + ], + "GOOGLE": [ + "ENABLED": true, + "CLIENT_ID": "clientId" + ], + "FACEBOOK": [ + "ENABLED": true, + "FACEBOOK_APP_ID": "facebookAppId", + "CLIENT_TOKEN": "client_token" + ], + "MICROSOFT": [ + "ENABLED": true, + "APP_ID": "appId" + ], + "APPLE_SIGNIN": [ + "ENABLED": true ] ] diff --git a/Core/Core/Configuration/Config/FacebookConfig.swift b/Core/Core/Configuration/Config/FacebookConfig.swift new file mode 100644 index 000000000..e8d7f1ad0 --- /dev/null +++ b/Core/Core/Configuration/Config/FacebookConfig.swift @@ -0,0 +1,38 @@ +// +// FacebookConfig.swift +// Core +// +// Created by Eugene Yatsenko on 22.11.2023. +// + +import Foundation + +private enum FacebookKeys: String { + case enabled = "ENABLED" + case appID = "FACEBOOK_APP_ID" + case clientToken = "CLIENT_TOKEN" +} + +public final class FacebookConfig: NSObject { + public var enabled: Bool = false + private(set) var appID: String? + private(set) var clientToken: String? + + private var requiredKeysAvailable: Bool { + return appID != nil && clientToken != nil + } + + init(dictionary: [String: AnyObject]) { + appID = dictionary[FacebookKeys.appID.rawValue] as? String + clientToken = dictionary[FacebookKeys.clientToken.rawValue] as? String + super.init() + enabled = requiredKeysAvailable && dictionary[FacebookKeys.enabled.rawValue] as? Bool == true + } +} + +private let key = "FACEBOOK" +extension Config { + public var facebook: FacebookConfig { + FacebookConfig(dictionary: self[key] as? [String: AnyObject] ?? [:]) + } +} diff --git a/Core/Core/Configuration/Config/FirebaseConfig.swift b/Core/Core/Configuration/Config/FirebaseConfig.swift index d981ff3ce..ba4a6f020 100644 --- a/Core/Core/Configuration/Config/FirebaseConfig.swift +++ b/Core/Core/Configuration/Config/FirebaseConfig.swift @@ -29,7 +29,7 @@ enum AnalyticsSource: String { case none } -public class FirebaseConfig: NSObject { +public final class FirebaseConfig: NSObject { public var enabled: Bool = false public var cloudMessagingEnabled: Bool = false public let apiKey: String? @@ -41,7 +41,7 @@ public class FirebaseConfig: NSObject { public let projectID: String? public let reversedClientID: String? public let storageBucket: String? - + private let analyticsSource: AnalyticsSource public var requiredKeysAvailable: Bool { @@ -97,9 +97,9 @@ public class FirebaseConfig: NSObject { } } -private let key = "FIREBASE" +private let firebaseKey = "FIREBASE" extension Config { public var firebase: FirebaseConfig { - return FirebaseConfig(dictionary: self[key] as? [String: AnyObject] ?? [:]) + FirebaseConfig(dictionary: self[firebaseKey] as? [String: AnyObject] ?? [:]) } } diff --git a/Core/Core/Configuration/Config/GoogleConfig.swift b/Core/Core/Configuration/Config/GoogleConfig.swift new file mode 100644 index 000000000..76eeef217 --- /dev/null +++ b/Core/Core/Configuration/Config/GoogleConfig.swift @@ -0,0 +1,37 @@ +// +// GoogleConfig.swift +// Core +// +// Created by Eugene Yatsenko on 27.11.2023. +// + +import Foundation + +private enum GoogleKeys: String { + case enabled = "ENABLED" + case googlePlusKey = "GOOGLE_PLUS_KEY" + case clientID = "CLIENT_ID" +} + +public final class GoogleConfig: NSObject { + public var enabled: Bool = false + private(set) var googlePlusKey: String? + private(set) var clientID: String? + + private var requiredKeysAvailable: Bool { + return clientID != nil + } + + init(dictionary: [String: AnyObject]) { + clientID = dictionary[GoogleKeys.clientID.rawValue] as? String + super.init() + enabled = requiredKeysAvailable && dictionary[GoogleKeys.enabled.rawValue] as? Bool == true + } +} + +private let key = "GOOGLE" +extension Config { + public var google: GoogleConfig { + GoogleConfig(dictionary: self[key] as? [String: AnyObject] ?? [:]) + } +} diff --git a/Core/Core/Configuration/Config/MicrosoftConfig.swift b/Core/Core/Configuration/Config/MicrosoftConfig.swift new file mode 100644 index 000000000..4175a53e2 --- /dev/null +++ b/Core/Core/Configuration/Config/MicrosoftConfig.swift @@ -0,0 +1,35 @@ +// +// MicrosoftConfig.swift +// Core +// +// Created by Eugene Yatsenko on 22.11.2023. +// + +import Foundation + +private enum MicrosoftKeys: String { + case enabled = "ENABLED" + case appID = "APP_ID" +} + +public final class MicrosoftConfig: NSObject { + public var enabled: Bool = false + private(set) var appID: String? + + private var requiredKeysAvailable: Bool { + return appID != nil + } + + init(dictionary: [String: AnyObject]) { + appID = dictionary[MicrosoftKeys.appID.rawValue] as? String + super.init() + enabled = requiredKeysAvailable && dictionary[MicrosoftKeys.enabled.rawValue] as? Bool == true + } +} + +private let key = "MICROSOFT" +extension Config { + public var microsoft: MicrosoftConfig { + MicrosoftConfig(dictionary: self[key] as? [String: AnyObject] ?? [:]) + } +} diff --git a/Core/Core/Data/CoreStorage.swift b/Core/Core/Data/CoreStorage.swift index a2d14dc86..b3a8faca0 100644 --- a/Core/Core/Data/CoreStorage.swift +++ b/Core/Core/Data/CoreStorage.swift @@ -10,6 +10,8 @@ import Foundation public protocol CoreStorage { var accessToken: String? {get set} var refreshToken: String? {get set} + var appleSignFullName: String? {get set} + var appleSignEmail: String? {get set} var cookiesDate: String? {get set} var reviewLastShownVersion: String? {get set} var lastReviewDate: Date? {get set} @@ -22,6 +24,8 @@ public protocol CoreStorage { public class CoreStorageMock: CoreStorage { public var accessToken: String? public var refreshToken: String? + public var appleSignFullName: String? + public var appleSignEmail: String? public var cookiesDate: String? public var reviewLastShownVersion: String? public var lastReviewDate: Date? diff --git a/Core/Core/Data/Repository/AuthRepository.swift b/Core/Core/Data/Repository/AuthRepository.swift index 8c47cb6a1..e4adf93ea 100644 --- a/Core/Core/Data/Repository/AuthRepository.swift +++ b/Core/Core/Data/Repository/AuthRepository.swift @@ -9,9 +9,10 @@ import Foundation public protocol AuthRepositoryProtocol { func login(username: String, password: String) async throws -> User + func login(externalToken: String, backend: String) async throws -> User func getCookies(force: Bool) async throws func getRegistrationFields() async throws -> [PickerFields] - func registerUser(fields: [String: String]) async throws -> User + func registerUser(fields: [String: String], isSocial: Bool) async throws -> User func validateRegistrationFields(fields: [String: String]) async throws -> [String: String] func resetPassword(email: String) async throws -> ResetPassword } @@ -53,7 +54,32 @@ public class AuthRepository: AuthRepositoryProtocol { appStorage.user = user return user.domain } - + + public func login(externalToken: String, backend: String) async throws -> User { + let endPoint = AuthEndpoint.exchangeAccessToken( + externalToken: externalToken, + backend: backend, + clientId: config.oAuthClientId, + tokenType: config.tokenType.rawValue + ) + let authResponse = try await api.requestData(endPoint).mapResponse(DataLayer.AuthResponse.self) + guard let accessToken = authResponse.accessToken, + let refreshToken = authResponse.refreshToken else { + if let error = authResponse.error, error == DataLayer.AuthResponse.invalidGrant { + throw APIError.invalidGrant + } else { + throw APIError.unknown + } + } + + appStorage.accessToken = accessToken + appStorage.refreshToken = refreshToken + + 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) @@ -81,11 +107,14 @@ public class AuthRepository: AuthRepositoryProtocol { } @discardableResult - public func registerUser(fields: [String: String]) async throws -> User { + public func registerUser(fields: [String: String], isSocial: Bool) async throws -> User { try await api.requestData(AuthEndpoint.registerUser(fields)) + if isSocial { + return try await login(externalToken: fields["access_token"] ?? "", backend: fields["provider"] ?? "") + } return try await login(username: fields["username"] ?? "", password: fields["password"] ?? "") } - + public func validateRegistrationFields(fields: [String: String]) async throws -> [String: String] { let result = try await api.requestData(AuthEndpoint.validateRegistrationFields(fields)) if let fieldsResult = try JSONSerialization.jsonObject(with: result, options: []) as? [String: Any] { @@ -104,7 +133,11 @@ class AuthRepositoryMock: AuthRepositoryProtocol { func login(username: String, password: String) async throws -> User { User(id: 1, username: "User", email: "email@gmail.com", name: "User Name", userAvatar: "") } - + + public func login(externalToken: String, backend: String) async throws -> User { + 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") } @@ -144,7 +177,7 @@ class AuthRepositoryMock: AuthRepositoryProtocol { return fields } - func registerUser(fields: [String: String]) async throws -> User { + func registerUser(fields: [String: String], isSocial: Bool) async throws -> User { User(id: 1, username: "User", email: "email@gmail.com", name: "User Name", userAvatar: "") } diff --git a/Core/Core/Domain/AuthInteractor.swift b/Core/Core/Domain/AuthInteractor.swift index 94e202364..45868cbc9 100644 --- a/Core/Core/Domain/AuthInteractor.swift +++ b/Core/Core/Domain/AuthInteractor.swift @@ -11,15 +11,16 @@ import Foundation public protocol AuthInteractorProtocol { @discardableResult func login(username: String, password: String) async throws -> User + @discardableResult + func login(externalToken: String, backend: String) async throws -> User func resetPassword(email: String) async throws -> ResetPassword func getCookies(force: Bool) async throws func getRegistrationFields() async throws -> [PickerFields] - func registerUser(fields: [String: String]) async throws -> User + func registerUser(fields: [String: String], isSocial: Bool) async throws -> User func validateRegistrationFields(fields: [String: String]) async throws -> [String: String] } public class AuthInteractor: AuthInteractorProtocol { - private let repository: AuthRepositoryProtocol public init(repository: AuthRepositoryProtocol) { @@ -31,6 +32,11 @@ public class AuthInteractor: AuthInteractorProtocol { return try await repository.login(username: username, password: password) } + @discardableResult + public func login(externalToken: String, backend: String) async throws -> User { + return try await repository.login(externalToken: externalToken, backend: backend) + } + public func resetPassword(email: String) async throws -> ResetPassword { try await repository.resetPassword(email: email) } @@ -43,8 +49,8 @@ public class AuthInteractor: AuthInteractorProtocol { return try await repository.getRegistrationFields() } - public func registerUser(fields: [String: String]) async throws -> User { - return try await repository.registerUser(fields: fields) + public func registerUser(fields: [String: String], isSocial: Bool) async throws -> User { + return try await repository.registerUser(fields: fields, isSocial: isSocial) } public func validateRegistrationFields(fields: [String: String]) async throws -> [String: String] { diff --git a/Core/Core/Extensions/DebugLog.swift b/Core/Core/Extensions/DebugLog.swift new file mode 100644 index 000000000..1ceb35482 --- /dev/null +++ b/Core/Core/Extensions/DebugLog.swift @@ -0,0 +1,25 @@ +// +// DebugLog.swift +// Core +// +// Created by Eugene Yatsenko on 10.10.2023. +// + +import Foundation + +public func debugLog( + _ item: Any..., + filename: String = #file, + line: Int = #line, + funcname: String = #function +) { +#if DEBUG + print( + """ + 🕗 \(Date()) + 📄 \(filename.components(separatedBy: "/").last ?? "") \(line) \(funcname) + ℹ️ \(item) + """ + ) +#endif +} diff --git a/Core/Core/Extensions/ResultExtension.swift b/Core/Core/Extensions/ResultExtension.swift new file mode 100644 index 000000000..d9a327768 --- /dev/null +++ b/Core/Core/Extensions/ResultExtension.swift @@ -0,0 +1,23 @@ +// +// ResultExtension.swift +// Core +// +// Created by Eugene Yatsenko on 11.10.2023. +// + +import Foundation + +extension Result { + @discardableResult + public func success(_ handler: (Success) -> Void) -> Self { + guard case let .success(value) = self else { return self } + handler(value) + return self + } + @discardableResult + public func failure(_ handler: (Failure) -> Void) -> Self { + guard case let .failure(error) = self else { return self } + handler(error) + return self + } +} diff --git a/Core/Core/Network/AuthEndpoint.swift b/Core/Core/Network/AuthEndpoint.swift index d92d139c0..e93e5c860 100644 --- a/Core/Core/Network/AuthEndpoint.swift +++ b/Core/Core/Network/AuthEndpoint.swift @@ -10,6 +10,7 @@ import Alamofire enum AuthEndpoint: EndPointType { case getAccessToken(username: String, password: String, clientId: String, tokenType: String) + case exchangeAccessToken(externalToken: String, backend: String, clientId: String, tokenType: String) case getUserInfo case getAuthCookies case getRegisterFields @@ -21,6 +22,8 @@ enum AuthEndpoint: EndPointType { switch self { case .getAccessToken: return "/oauth2/access_token" + case let .exchangeAccessToken(_, backend, _, _): + return "/oauth2/exchange_access_token/\(backend)/" case .getUserInfo: return "/api/mobile/v0.5/my_user_info" case .getAuthCookies: @@ -38,7 +41,7 @@ enum AuthEndpoint: EndPointType { var httpMethod: HTTPMethod { switch self { - case .getAccessToken: + case .getAccessToken, .exchangeAccessToken: return .post case .getUserInfo: return .get @@ -71,6 +74,14 @@ enum AuthEndpoint: EndPointType { "asymmetric_jwt": true ] return .requestParameters(parameters: params, encoding: URLEncoding.httpBody) + case let .exchangeAccessToken(externalToken, _, clientId, tokenType): + let params: [String: Encodable] = [ + "client_id": clientId, + "token_type": tokenType, + "access_token": externalToken, + "asymmetric_jwt": true + ] + return .requestParameters(parameters: params, encoding: URLEncoding.httpBody) case .getUserInfo: return .request case .getAuthCookies: diff --git a/Core/Core/Providers/SocialAuth/AppleAuthProvider.swift b/Core/Core/Providers/SocialAuth/AppleAuthProvider.swift new file mode 100644 index 000000000..1ed3d4c06 --- /dev/null +++ b/Core/Core/Providers/SocialAuth/AppleAuthProvider.swift @@ -0,0 +1,94 @@ +// +// AppleAuthProvider.swift +// Core +// +// Created by Eugene Yatsenko on 10.10.2023. +// + +import Foundation +import AuthenticationServices +import Swinject + +public final class AppleAuthProvider: NSObject, ASAuthorizationControllerDelegate { + + private let config: ConfigProtocol + + public init(config: ConfigProtocol) { + self.config = config + super.init() + } + + private var completion: ((Result) -> Void)? + private let appleIDProvider = ASAuthorizationAppleIDProvider() + + public func request(completion: ((Result) -> Void)?) { + let request = appleIDProvider.createRequest() + request.requestedScopes = [.fullName, .email] + + let authorizationController = ASAuthorizationController(authorizationRequests: [request]) + authorizationController.delegate = self + authorizationController.performRequests() + + self.completion = completion + } + + public func authorizationController( + controller: ASAuthorizationController, + didCompleteWithAuthorization authorization: ASAuthorization + ) { + guard let credentials = authorization.credential as? ASAuthorizationAppleIDCredential else { + completion?(.failure(SocialAuthError.unknownError)) + return + } + + var storage = Container.shared.resolve(CoreStorage.self) + let pncf = PersonNameComponentsFormatter() + + var name = storage?.appleSignFullName ?? "" + if let components = credentials.fullName, !pncf.string(from: components).isEmpty { + name = pncf.string(from: components) + storage?.appleSignFullName = name + } + + var email = storage?.appleSignEmail ?? "" + if let appleEmail = credentials.email, !appleEmail.isEmpty { + email = appleEmail + storage?.appleSignEmail = appleEmail + } + + guard let data = credentials.identityToken, + let code = String(data: data, encoding: .utf8) else { + completion?(.failure(SocialAuthError.unknownError)) + return + } + + debugLog("User id is \(data) \n Full Name is \(name) \n Email id is \(email)") + + let appleCredentials = SocialAuthResponse( + name: name, + email: email, + token: code + ) + + completion?(.success(appleCredentials)) + } + + public func authorizationController( + controller: ASAuthorizationController, + didCompleteWithError error: Error + ) { + debugLog(error) + completion?(.failure(failure(ASAuthorizationError(_nsError: error as NSError)))) + } + + private func failure(_ error: ASAuthorizationError) -> Error { + switch error.code { + case .canceled: + return SocialAuthError.socialAuthCanceled + case .failed: + return SocialAuthError.error(text: CoreLocalization.Error.authorizationFailed) + default: + return error + } + } +} diff --git a/Core/Core/Providers/SocialAuth/Error/SocialAuthError.swift b/Core/Core/Providers/SocialAuth/Error/SocialAuthError.swift new file mode 100644 index 000000000..a9167451e --- /dev/null +++ b/Core/Core/Providers/SocialAuth/Error/SocialAuthError.swift @@ -0,0 +1,27 @@ +// +// SocialAuthError.swift +// Core +// +// Created by Eugene Yatsenko on 10.10.2023. +// + +import Foundation + +public enum SocialAuthError: Error { + case error(text: String) + case socialAuthCanceled + case unknownError +} + +extension SocialAuthError: LocalizedError { + public var errorDescription: String? { + switch self { + case .error(let text): + return text + case .socialAuthCanceled: + return CoreLocalization.socialSignCanceled + case .unknownError: + return CoreLocalization.Error.unknownError + } + } +} diff --git a/Core/Core/Providers/SocialAuth/FacebookAuthProvider.swift b/Core/Core/Providers/SocialAuth/FacebookAuthProvider.swift new file mode 100644 index 000000000..66ae46b6d --- /dev/null +++ b/Core/Core/Providers/SocialAuth/FacebookAuthProvider.swift @@ -0,0 +1,86 @@ +// +// FacebookAuthProvider.swift +// Core +// +// Created by Eugene Yatsenko on 10.10.2023. +// + +import Foundation +import FacebookLogin + +public final class FacebookAuthProvider { + + private let loginManager = LoginManager() + + public init() {} + + @MainActor + public func signIn( + withPresenting: UIViewController + ) async -> Result { + await withCheckedContinuation { continuation in + loginManager.logIn( + permissions: [], + from: withPresenting + ) { result, error in + if let error = error { + continuation.resume(returning: .failure(error)) + return + } + + guard let result = result, + let tokenString = AccessToken.current?.tokenString else { + continuation.resume( + returning: .failure(SocialAuthError.unknownError) + ) + return + } + + if result.isCancelled { + continuation.resume(returning: .failure(SocialAuthError.socialAuthCanceled)) + return + } + + GraphRequest( + graphPath: "me", + parameters: ["fields": "name, email"] + ).start { [weak self] _, result, _ in + guard let self else { return } + guard let userInfo = result as? [String: Any] else { + continuation.resume( + returning: .success( + SocialAuthResponse( + name: "", + email: "", + token: tokenString + ) + ) + ) + return + } + continuation.resume( + returning: .success( + SocialAuthResponse( + name: userInfo["name"] as? String ?? "", + email: userInfo["email"] as? String ?? "", + token: tokenString + ) + ) + ) + } + } + } + } + + public func signOut() { + loginManager.logOut() + } + + private func failure(_ error: Error?) -> Error { + if let error = error as? NSError, + let description = error.userInfo[ErrorLocalizedDescriptionKey] as? String { + return SocialAuthError.error(text: description) + } + return error ?? SocialAuthError.unknownError + } +} diff --git a/Core/Core/Providers/SocialAuth/GoogleAuthProvider.swift b/Core/Core/Providers/SocialAuth/GoogleAuthProvider.swift new file mode 100644 index 000000000..c600c1735 --- /dev/null +++ b/Core/Core/Providers/SocialAuth/GoogleAuthProvider.swift @@ -0,0 +1,52 @@ +// +// GoogleAuthProvider.swift +// Core +// +// Created by Eugene Yatsenko on 10.10.2023. +// + +import GoogleSignIn +import Foundation + +public final class GoogleAuthProvider { + + public init() {} + + @MainActor + public func signIn( + withPresenting: UIViewController + ) async -> Result { + await withCheckedContinuation { continuation in + GIDSignIn.sharedInstance.signIn( + withPresenting: withPresenting, + completion: { result, error in + if let error = error as? NSError, error.code == GIDSignInError.canceled.rawValue { + continuation.resume(returning: .failure(SocialAuthError.socialAuthCanceled)) + return + } + guard let result = result else { + continuation.resume( + returning: .failure( + SocialAuthError.error(text: CoreLocalization.Error.unknownError) + ) + ) + return + } + continuation.resume( + returning: .success( + SocialAuthResponse( + name: result.user.profile?.name ?? "", + email: result.user.profile?.email ?? "", + token: result.user.accessToken.tokenString + ) + ) + ) + } + ) + } + } + + public func signOut() { + GIDSignIn.sharedInstance.signOut() + } +} diff --git a/Core/Core/Providers/SocialAuth/MicrosoftAuthProvider.swift b/Core/Core/Providers/SocialAuth/MicrosoftAuthProvider.swift new file mode 100644 index 000000000..16178b17c --- /dev/null +++ b/Core/Core/Providers/SocialAuth/MicrosoftAuthProvider.swift @@ -0,0 +1,118 @@ +// +// MicrosoftAuthProvider.swift +// Core +// +// Created by Eugene Yatsenko on 10.10.2023. +// + +import Foundation +import MSAL +import Swinject + +public typealias MSLoginCompletionHandler = (account: MSALAccount, token: String) + +public final class MicrosoftAuthProvider { + + private let scopes = ["User.Read", "email"] + private var result: MSALResult? + + public init() {} + + @MainActor + public func signIn( + withPresenting: UIViewController + ) async -> Result { + await withCheckedContinuation { continuation in + do { + let clientApplication = try createClientApplication() + + let webParameters = MSALWebviewParameters(authPresentationViewController: withPresenting) + let parameters = MSALInteractiveTokenParameters(scopes: scopes, webviewParameters: webParameters) + clientApplication.acquireToken(with: parameters) { result, error in + if let error = error { + continuation.resume(returning: .failure(error)) + return + } + + guard let result = result else { + continuation.resume( + returning: .failure( + SocialAuthError.error(text: CoreLocalization.Error.unknownError) + ) + ) + return + } + + self.result = result + let account = result.account + + continuation.resume( + returning: .success( + SocialAuthResponse( + name: account.accountClaims?["name"] as? String ?? "" , + email: account.accountClaims?["email"] as? String ?? "", + token: result.accessToken + ) + ) + ) + } + } catch let error { + continuation.resume(returning: .failure(error)) + } + } + } + + public func signOut() { + do { + let account = try? currentAccount() + + if let account = account { + let application = try createClientApplication() + try application.remove(account) + } + } catch let error { + debugLog("Logout", "Received error signing user out: \(error)") + } + } + + private func createClientApplication() throws -> MSALPublicClientApplication { + guard let config = Container.shared.resolve(ConfigProtocol.self), let appID = config.microsoft.appID else { + throw SocialAuthError.error(text: "Configuration error") + } + let configuration = MSALPublicClientApplicationConfig(clientId: appID) + + do { + return try MSALPublicClientApplication(configuration: configuration) + } catch { + throw SocialAuthError.error(text: error.localizedDescription) + } + } + + @discardableResult + private func currentAccount() throws -> MSALAccount { + let clientApplication = try createClientApplication() + + guard let account = try clientApplication.allAccounts().first else { + throw SocialAuthError.error(text: "Account not found") + } + + return account + } + + private func failure(_ error: Error?) -> Error { + if let error = error as? NSError, + let description = error.userInfo[MSALErrorDescriptionKey] as? String { + if let errorCode = MSALError(rawValue: error.code), case .userCanceled = errorCode { + return SocialAuthError.socialAuthCanceled + } + return SocialAuthError.error(text: description) + } + return error ?? SocialAuthError.error(text: CoreLocalization.Error.unknownError) + } + + func getUser(completion: (MSALAccount) -> Void) { + guard let user = result?.account else { return } + completion(user) + } + +} diff --git a/Core/Core/Providers/SocialAuth/SocialAuthResponse.swift b/Core/Core/Providers/SocialAuth/SocialAuthResponse.swift new file mode 100644 index 000000000..55589d476 --- /dev/null +++ b/Core/Core/Providers/SocialAuth/SocialAuthResponse.swift @@ -0,0 +1,20 @@ +// +// SocialAuthResponse.swift +// Core +// +// Created by Eugene Yatsenko on 07.12.2023. +// + +import Foundation + +public struct SocialAuthResponse { + public var name: String + public var email: String + public var token: String + + public init(name: String, email: String, token: String) { + self.name = name + self.email = email + self.token = token + } +} diff --git a/Core/Core/SwiftGen/Assets.swift b/Core/Core/SwiftGen/Assets.swift index 503b44745..202ab2639 100644 --- a/Core/Core/SwiftGen/Assets.swift +++ b/Core/Core/SwiftGen/Assets.swift @@ -13,6 +13,8 @@ #endif // Deprecated typealiases +@available(*, deprecated, renamed: "ColorAsset.Color", message: "This typealias will be removed in SwiftGen 7.0") +public typealias AssetColorTypeAlias = ColorAsset.Color @available(*, deprecated, renamed: "ImageAsset.Image", message: "This typealias will be removed in SwiftGen 7.0") public typealias AssetImageTypeAlias = ImageAsset.Image @@ -22,6 +24,10 @@ public typealias AssetImageTypeAlias = ImageAsset.Image // swiftlint:disable identifier_name line_length nesting type_body_length type_name public enum CoreAssets { + public static let appleButtonColor = ColorAsset(name: "AppleButtonColor") + public static let facebookButtonColor = ColorAsset(name: "FacebookButtonColor") + public static let googleButtonColor = ColorAsset(name: "GoogleButtonColor") + public static let microsoftButtonColor = ColorAsset(name: "MicrosoftButtonColor") public static let bookCircle = ImageAsset(name: "book.circle") public static let bubbleLeftCircle = ImageAsset(name: "bubble.left.circle") public static let docCircle = ImageAsset(name: "doc.circle") @@ -68,6 +74,10 @@ public enum CoreAssets { public static let logOut = ImageAsset(name: "logOut") public static let noAvatar = ImageAsset(name: "noAvatar") public static let removePhoto = ImageAsset(name: "removePhoto") + public static let iconApple = ImageAsset(name: "icon_apple") + public static let iconFacebookWhite = ImageAsset(name: "icon_facebook_white") + public static let iconGoogleWhite = ImageAsset(name: "icon_google_white") + public static let iconMicrosoftWhite = ImageAsset(name: "icon_microsoft_white") public static let rotateDevice = ImageAsset(name: "rotateDevice") public static let sub = ImageAsset(name: "sub") public static let alarm = ImageAsset(name: "alarm") @@ -99,6 +109,70 @@ public enum CoreAssets { // MARK: - Implementation Details +public final class ColorAsset { + public fileprivate(set) var name: String + + #if os(macOS) + public typealias Color = NSColor + #elseif os(iOS) || os(tvOS) || os(watchOS) + public typealias Color = UIColor + #endif + + @available(iOS 11.0, tvOS 11.0, watchOS 4.0, macOS 10.13, *) + public private(set) lazy var color: Color = { + guard let color = Color(asset: self) else { + fatalError("Unable to load color asset named \(name).") + } + return color + }() + + #if os(iOS) || os(tvOS) + @available(iOS 11.0, tvOS 11.0, *) + public func color(compatibleWith traitCollection: UITraitCollection) -> Color { + let bundle = BundleToken.bundle + guard let color = Color(named: name, in: bundle, compatibleWith: traitCollection) else { + fatalError("Unable to load color asset named \(name).") + } + return color + } + #endif + + #if canImport(SwiftUI) + @available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *) + public private(set) lazy var swiftUIColor: SwiftUI.Color = { + SwiftUI.Color(asset: self) + }() + #endif + + fileprivate init(name: String) { + self.name = name + } +} + +public extension ColorAsset.Color { + @available(iOS 11.0, tvOS 11.0, watchOS 4.0, macOS 10.13, *) + convenience init?(asset: ColorAsset) { + let bundle = BundleToken.bundle + #if os(iOS) || os(tvOS) + self.init(named: asset.name, in: bundle, compatibleWith: nil) + #elseif os(macOS) + self.init(named: NSColor.Name(asset.name), bundle: bundle) + #elseif os(watchOS) + self.init(named: asset.name) + #endif + } +} + +#if canImport(SwiftUI) +@available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *) +public extension SwiftUI.Color { + init(asset: ColorAsset) { + let bundle = BundleToken.bundle + self.init(asset.name, bundle: bundle) + } +} +#endif + public struct ImageAsset { public fileprivate(set) var name: String diff --git a/Core/Core/SwiftGen/Strings.swift b/Core/Core/SwiftGen/Strings.swift index fc1a73163..8485925d2 100644 --- a/Core/Core/SwiftGen/Strings.swift +++ b/Core/Core/SwiftGen/Strings.swift @@ -12,6 +12,8 @@ import Foundation public enum CoreLocalization { /// Done public static let done = CoreLocalization.tr("Localizable", "DONE", fallback: "Done") + /// 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.") public enum Alert { /// ACCEPT public static let accept = CoreLocalization.tr("Localizable", "ALERT.ACCEPT", fallback: "ACCEPT") @@ -93,6 +95,8 @@ public enum CoreLocalization { public static let downloaded = CoreLocalization.tr("Localizable", "DOWNLOAD_MANAGER.DOWNLOADED", fallback: "Downloaded") } public enum Error { + /// Authorization failed. + public static let authorizationFailed = CoreLocalization.tr("Localizable", "ERROR.AUTHORIZATION_FAILED", fallback: "Authorization failed.") /// Invalid credentials public static let invalidCredentials = CoreLocalization.tr("Localizable", "ERROR.INVALID_CREDENTIALS", fallback: "Invalid credentials") /// No cached data for offline mode diff --git a/Core/Core/View/Base/SocialAuthButton.swift b/Core/Core/View/Base/SocialAuthButton.swift new file mode 100644 index 000000000..71ea25dbc --- /dev/null +++ b/Core/Core/View/Base/SocialAuthButton.swift @@ -0,0 +1,74 @@ +// +// SocialAuthButton.swift +// Core +// +// Created by Eugene Yatsenko on 10.10.2023. +// + +import SwiftUI + +public struct SocialAuthButton: View { + + // MARK: - Properties + + private var idiom: UIUserInterfaceIdiom { UIDevice.current.userInterfaceIdiom } + + private var image: Image + private var title: String + private var textColor: Color + private var backgroundColor: Color + private var cornerRadius: CGFloat + private var action: () -> Void + + public init( + image: Image, + title: String, + textColor: Color = .white, + backgroundColor: Color = .accentColor, + cornerRadius: CGFloat = 8, + action: @escaping () -> Void + ) { + self.image = image + self.title = title + self.textColor = textColor + self.backgroundColor = backgroundColor + self.cornerRadius = cornerRadius + self.action = action + } + + // MARK: - Views + + public var body: some View { + Button { + action() + } label: { + Label { + Text(title) + .foregroundStyle(textColor) + .padding(.leading, 10) + Spacer() + } icon: { + image.padding(.leading, 10) + } + } + .frame(maxWidth: idiom == .pad ? 260: .infinity, minHeight: 42) + .background(backgroundColor) + .clipShape( + RoundedRectangle(cornerRadius: cornerRadius) + ) + + } +} + +#if DEBUG +struct LabelButton_Previews: PreviewProvider { + static var previews: some View { + SocialAuthButton( + image: CoreAssets.iconApple.swiftUIImage, + title: "Apple", + backgroundColor: CoreAssets.appleButtonColor.swiftUIColor, + action: { } + ) + } +} +#endif diff --git a/Core/Core/en.lproj/Localizable.strings b/Core/Core/en.lproj/Localizable.strings index 38ca23c6f..555ad1c6a 100644 --- a/Core/Core/en.lproj/Localizable.strings +++ b/Core/Core/en.lproj/Localizable.strings @@ -20,6 +20,7 @@ "ERROR.USER_NOT_ACTIVE" = "User account is not activated. Please activate your account first."; "ERROR.UNKNOWN_ERROR" = "Something went wrong"; "ERROR.WIFI" = "You can only download files over Wi-Fi. You can change this in the settings."; +"ERROR.AUTHORIZATION_FAILED" = "Authorization failed."; "COURSEWARE.COURSE_CONTENT" = "Course content"; "COURSEWARE.COURSE_UNITS" = "Course units"; @@ -92,3 +93,5 @@ "COURSE_DATES.UNRELEASED" = "Unreleased"; "COURSE_DATES.VERIFIED_ONLY" = "Verified Only"; +"SOCIAL_SIGN_CANCELED" = "The user canceled the sign-in flow."; + diff --git a/Core/Core/uk.lproj/Localizable.strings b/Core/Core/uk.lproj/Localizable.strings index e3a75f764..2d56ca83c 100644 --- a/Core/Core/uk.lproj/Localizable.strings +++ b/Core/Core/uk.lproj/Localizable.strings @@ -93,3 +93,5 @@ "COURSE_DATES.UNRELEASED" = "Unreleased"; "COURSE_DATES.VERIFIED_ONLY" = "Verified Only"; +"SOCIAL_SIGN_CANCELED" = "The user canceled the sign-in flow."; +"AUTHORIZATION_FAILED" = "Authorization failed."; diff --git a/Core/CoreTests/Configuration/ConfigTests.swift b/Core/CoreTests/Configuration/ConfigTests.swift index 819970fb4..694ee6734 100644 --- a/Core/CoreTests/Configuration/ConfigTests.swift +++ b/Core/CoreTests/Configuration/ConfigTests.swift @@ -32,7 +32,24 @@ class ConfigTests: XCTestCase { "REVERSED_CLIENT_ID": "testReversedClientID", "STORAGE_BUCKET": "testStorageBucket", "ANALYTICS_SOURCE": "firebase", - "CLOUD_MESSAGING_ENABLED": true] + "CLOUD_MESSAGING_ENABLED": true + ], + "GOOGLE": [ + "ENABLED": true, + "CLIENT_ID": "clientId" + ], + "FACEBOOK": [ + "ENABLED": true, + "FACEBOOK_APP_ID": "facebookAppId", + "CLIENT_TOKEN": "client_token" + ], + "MICROSOFT": [ + "ENABLED": true, + "APP_ID": "appId" + ], + "APPLE_SIGNIN": [ + "ENABLED": true + ] ] func testConfigInitialization() { @@ -74,4 +91,32 @@ class ConfigTests: XCTestCase { XCTAssertEqual(config.firebase.isAnalyticsSourceFirebase, true) XCTAssertEqual(config.firebase.cloudMessagingEnabled, true) } + + func testGoogleConfigInitialization() { + let config = Config(properties: properties) + + XCTAssertTrue(config.google.enabled) + XCTAssertEqual(config.google.clientID, "clientId") + } + + func testFacebookConfigInitialization() { + let config = Config(properties: properties) + + XCTAssertTrue(config.facebook.enabled) + XCTAssertEqual(config.facebook.appID, "facebookAppId") + XCTAssertEqual(config.facebook.clientToken, "client_token") + } + + func testMicrosoftConfigInitialization() { + let config = Config(properties: properties) + + XCTAssertTrue(config.microsoft.enabled) + XCTAssertEqual(config.microsoft.appID, "appId") + } + + func testAppleConfigInitialization() { + let config = Config(properties: properties) + + XCTAssertTrue(config.appleSignIn.enabled) + } } diff --git a/Course/CourseTests/CourseMock.generated.swift b/Course/CourseTests/CourseMock.generated.swift index 0c093e89c..ef2212269 100644 --- a/Course/CourseTests/CourseMock.generated.swift +++ b/Course/CourseTests/CourseMock.generated.swift @@ -76,6 +76,23 @@ open class AuthInteractorProtocolMock: AuthInteractorProtocol, Mock { 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`) + var __value: User + do { + __value = try methodReturnValue(.m_login__externalToken_externalTokenbackend_backend(Parameter.value(`externalToken`), Parameter.value(`backend`))).casted() + } catch MockError.notStubed { + onFatalFailure("Stub return value not specified for login(externalToken: String, backend: String). Use given") + Failure("Stub return value not specified for login(externalToken: String, backend: 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 @@ -121,16 +138,16 @@ open class AuthInteractorProtocolMock: AuthInteractorProtocol, Mock { return __value } - open func registerUser(fields: [String: String]) throws -> User { - addInvocation(.m_registerUser__fields_fields(Parameter<[String: String]>.value(`fields`))) - let perform = methodPerformValue(.m_registerUser__fields_fields(Parameter<[String: String]>.value(`fields`))) as? ([String: String]) -> Void - perform?(`fields`) + open func registerUser(fields: [String: String], isSocial: Bool) throws -> User { + addInvocation(.m_registerUser__fields_fieldsisSocial_isSocial(Parameter<[String: String]>.value(`fields`), Parameter.value(`isSocial`))) + let perform = methodPerformValue(.m_registerUser__fields_fieldsisSocial_isSocial(Parameter<[String: String]>.value(`fields`), Parameter.value(`isSocial`))) as? ([String: String], Bool) -> Void + perform?(`fields`, `isSocial`) var __value: User do { - __value = try methodReturnValue(.m_registerUser__fields_fields(Parameter<[String: String]>.value(`fields`))).casted() + __value = try methodReturnValue(.m_registerUser__fields_fieldsisSocial_isSocial(Parameter<[String: String]>.value(`fields`), Parameter.value(`isSocial`))).casted() } catch MockError.notStubed { - onFatalFailure("Stub return value not specified for registerUser(fields: [String: String]). Use given") - Failure("Stub return value not specified for registerUser(fields: [String: String]). Use given") + onFatalFailure("Stub return value not specified for registerUser(fields: [String: String], isSocial: Bool). Use given") + Failure("Stub return value not specified for registerUser(fields: [String: String], isSocial: Bool). Use given") } catch { throw error } @@ -156,10 +173,11 @@ open class AuthInteractorProtocolMock: AuthInteractorProtocol, Mock { fileprivate enum MethodType { case m_login__username_usernamepassword_password(Parameter, Parameter) + case m_login__externalToken_externalTokenbackend_backend(Parameter, Parameter) case m_resetPassword__email_email(Parameter) case m_getCookies__force_force(Parameter) case m_getRegistrationFields - case m_registerUser__fields_fields(Parameter<[String: String]>) + case m_registerUser__fields_fieldsisSocial_isSocial(Parameter<[String: String]>, Parameter) case m_validateRegistrationFields__fields_fields(Parameter<[String: String]>) static func compareParameters(lhs: MethodType, rhs: MethodType, matcher: Matcher) -> Matcher.ComparisonResult { @@ -170,6 +188,12 @@ 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__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")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBackend, rhs: rhsBackend, with: matcher), lhsBackend, rhsBackend, "backend")) + return Matcher.ComparisonResult(results) + case (.m_resetPassword__email_email(let lhsEmail), .m_resetPassword__email_email(let rhsEmail)): var results: [Matcher.ParameterComparisonResult] = [] results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsEmail, rhs: rhsEmail, with: matcher), lhsEmail, rhsEmail, "email")) @@ -182,9 +206,10 @@ open class AuthInteractorProtocolMock: AuthInteractorProtocol, Mock { case (.m_getRegistrationFields, .m_getRegistrationFields): return .match - case (.m_registerUser__fields_fields(let lhsFields), .m_registerUser__fields_fields(let rhsFields)): + case (.m_registerUser__fields_fieldsisSocial_isSocial(let lhsFields, let lhsIssocial), .m_registerUser__fields_fieldsisSocial_isSocial(let rhsFields, let rhsIssocial)): var results: [Matcher.ParameterComparisonResult] = [] results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsFields, rhs: rhsFields, with: matcher), lhsFields, rhsFields, "fields")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsIssocial, rhs: rhsIssocial, with: matcher), lhsIssocial, rhsIssocial, "isSocial")) return Matcher.ComparisonResult(results) case (.m_validateRegistrationFields__fields_fields(let lhsFields), .m_validateRegistrationFields__fields_fields(let rhsFields)): @@ -198,20 +223,22 @@ 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__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 case .m_getRegistrationFields: return 0 - case let .m_registerUser__fields_fields(p0): return p0.intValue + case let .m_registerUser__fields_fieldsisSocial_isSocial(p0, p1): return p0.intValue + p1.intValue case let .m_validateRegistrationFields__fields_fields(p0): return p0.intValue } } func assertionName() -> String { switch self { case .m_login__username_usernamepassword_password: return ".login(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:)" case .m_getRegistrationFields: return ".getRegistrationFields()" - case .m_registerUser__fields_fields: return ".registerUser(fields:)" + case .m_registerUser__fields_fieldsisSocial_isSocial: return ".registerUser(fields:isSocial:)" case .m_validateRegistrationFields__fields_fields: return ".validateRegistrationFields(fields:)" } } @@ -230,14 +257,18 @@ open class AuthInteractorProtocolMock: AuthInteractorProtocol, Mock { public static func login(username: Parameter, password: Parameter, willReturn: User...) -> MethodStub { return Given(method: .m_login__username_usernamepassword_password(`username`, `password`), products: willReturn.map({ StubProduct.return($0 as Any) })) } + @discardableResult + 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) })) + } 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) })) } public static func getRegistrationFields(willReturn: [PickerFields]...) -> MethodStub { return Given(method: .m_getRegistrationFields, products: willReturn.map({ StubProduct.return($0 as Any) })) } - public static func registerUser(fields: Parameter<[String: String]>, willReturn: User...) -> MethodStub { - return Given(method: .m_registerUser__fields_fields(`fields`), products: willReturn.map({ StubProduct.return($0 as Any) })) + public static func registerUser(fields: Parameter<[String: String]>, isSocial: Parameter, willReturn: User...) -> MethodStub { + return Given(method: .m_registerUser__fields_fieldsisSocial_isSocial(`fields`, `isSocial`), products: willReturn.map({ StubProduct.return($0 as Any) })) } public static func validateRegistrationFields(fields: Parameter<[String: String]>, willReturn: [String: String]...) -> MethodStub { return Given(method: .m_validateRegistrationFields__fields_fields(`fields`), products: willReturn.map({ StubProduct.return($0 as Any) })) @@ -254,6 +285,18 @@ open class AuthInteractorProtocolMock: AuthInteractorProtocol, Mock { willProduce(stubber) return given } + @discardableResult + public static func login(externalToken: Parameter, backend: Parameter, willThrow: Error...) -> MethodStub { + return Given(method: .m_login__externalToken_externalTokenbackend_backend(`externalToken`, `backend`), products: willThrow.map({ StubProduct.throw($0) })) + } + @discardableResult + public static func login(externalToken: Parameter, backend: Parameter, willProduce: (StubberThrows) -> Void) -> MethodStub { + let willThrow: [Error] = [] + let given: Given = { return Given(method: .m_login__externalToken_externalTokenbackend_backend(`externalToken`, `backend`), products: willThrow.map({ StubProduct.throw($0) })) }() + let stubber = given.stubThrows(for: (User).self) + willProduce(stubber) + return given + } public static func resetPassword(email: Parameter, willThrow: Error...) -> MethodStub { return Given(method: .m_resetPassword__email_email(`email`), products: willThrow.map({ StubProduct.throw($0) })) } @@ -284,12 +327,12 @@ open class AuthInteractorProtocolMock: AuthInteractorProtocol, Mock { willProduce(stubber) return given } - public static func registerUser(fields: Parameter<[String: String]>, willThrow: Error...) -> MethodStub { - return Given(method: .m_registerUser__fields_fields(`fields`), products: willThrow.map({ StubProduct.throw($0) })) + public static func registerUser(fields: Parameter<[String: String]>, isSocial: Parameter, willThrow: Error...) -> MethodStub { + return Given(method: .m_registerUser__fields_fieldsisSocial_isSocial(`fields`, `isSocial`), products: willThrow.map({ StubProduct.throw($0) })) } - public static func registerUser(fields: Parameter<[String: String]>, willProduce: (StubberThrows) -> Void) -> MethodStub { + public static func registerUser(fields: Parameter<[String: String]>, isSocial: Parameter, willProduce: (StubberThrows) -> Void) -> MethodStub { let willThrow: [Error] = [] - let given: Given = { return Given(method: .m_registerUser__fields_fields(`fields`), products: willThrow.map({ StubProduct.throw($0) })) }() + let given: Given = { return Given(method: .m_registerUser__fields_fieldsisSocial_isSocial(`fields`, `isSocial`), products: willThrow.map({ StubProduct.throw($0) })) }() let stubber = given.stubThrows(for: (User).self) willProduce(stubber) return given @@ -311,10 +354,12 @@ open class AuthInteractorProtocolMock: AuthInteractorProtocol, Mock { @discardableResult public static func login(username: Parameter, password: Parameter) -> Verify { return Verify(method: .m_login__username_usernamepassword_password(`username`, `password`))} + @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`))} public static func getCookies(force: Parameter) -> Verify { return Verify(method: .m_getCookies__force_force(`force`))} public static func getRegistrationFields() -> Verify { return Verify(method: .m_getRegistrationFields)} - public static func registerUser(fields: Parameter<[String: String]>) -> Verify { return Verify(method: .m_registerUser__fields_fields(`fields`))} + public static func registerUser(fields: Parameter<[String: String]>, isSocial: Parameter) -> Verify { return Verify(method: .m_registerUser__fields_fieldsisSocial_isSocial(`fields`, `isSocial`))} public static func validateRegistrationFields(fields: Parameter<[String: String]>) -> Verify { return Verify(method: .m_validateRegistrationFields__fields_fields(`fields`))} } @@ -326,6 +371,10 @@ open class AuthInteractorProtocolMock: AuthInteractorProtocol, Mock { public static func login(username: Parameter, password: Parameter, perform: @escaping (String, String) -> Void) -> Perform { return Perform(method: .m_login__username_usernamepassword_password(`username`, `password`), performs: perform) } + @discardableResult + public static func login(externalToken: Parameter, backend: Parameter, perform: @escaping (String, String) -> Void) -> Perform { + return Perform(method: .m_login__externalToken_externalTokenbackend_backend(`externalToken`, `backend`), performs: perform) + } public static func resetPassword(email: Parameter, perform: @escaping (String) -> Void) -> Perform { return Perform(method: .m_resetPassword__email_email(`email`), performs: perform) } @@ -335,8 +384,8 @@ open class AuthInteractorProtocolMock: AuthInteractorProtocol, Mock { public static func getRegistrationFields(perform: @escaping () -> Void) -> Perform { return Perform(method: .m_getRegistrationFields, performs: perform) } - public static func registerUser(fields: Parameter<[String: String]>, perform: @escaping ([String: String]) -> Void) -> Perform { - return Perform(method: .m_registerUser__fields_fields(`fields`), performs: perform) + public static func registerUser(fields: Parameter<[String: String]>, isSocial: Parameter, perform: @escaping ([String: String], Bool) -> Void) -> Perform { + return Perform(method: .m_registerUser__fields_fieldsisSocial_isSocial(`fields`, `isSocial`), performs: perform) } public static func validateRegistrationFields(fields: Parameter<[String: String]>, perform: @escaping ([String: String]) -> Void) -> Perform { return Perform(method: .m_validateRegistrationFields__fields_fields(`fields`), performs: perform) diff --git a/Dashboard/DashboardTests/DashboardMock.generated.swift b/Dashboard/DashboardTests/DashboardMock.generated.swift index ba988cfff..70acae430 100644 --- a/Dashboard/DashboardTests/DashboardMock.generated.swift +++ b/Dashboard/DashboardTests/DashboardMock.generated.swift @@ -76,6 +76,23 @@ open class AuthInteractorProtocolMock: AuthInteractorProtocol, Mock { 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`) + var __value: User + do { + __value = try methodReturnValue(.m_login__externalToken_externalTokenbackend_backend(Parameter.value(`externalToken`), Parameter.value(`backend`))).casted() + } catch MockError.notStubed { + onFatalFailure("Stub return value not specified for login(externalToken: String, backend: String). Use given") + Failure("Stub return value not specified for login(externalToken: String, backend: 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 @@ -121,16 +138,16 @@ open class AuthInteractorProtocolMock: AuthInteractorProtocol, Mock { return __value } - open func registerUser(fields: [String: String]) throws -> User { - addInvocation(.m_registerUser__fields_fields(Parameter<[String: String]>.value(`fields`))) - let perform = methodPerformValue(.m_registerUser__fields_fields(Parameter<[String: String]>.value(`fields`))) as? ([String: String]) -> Void - perform?(`fields`) + open func registerUser(fields: [String: String], isSocial: Bool) throws -> User { + addInvocation(.m_registerUser__fields_fieldsisSocial_isSocial(Parameter<[String: String]>.value(`fields`), Parameter.value(`isSocial`))) + let perform = methodPerformValue(.m_registerUser__fields_fieldsisSocial_isSocial(Parameter<[String: String]>.value(`fields`), Parameter.value(`isSocial`))) as? ([String: String], Bool) -> Void + perform?(`fields`, `isSocial`) var __value: User do { - __value = try methodReturnValue(.m_registerUser__fields_fields(Parameter<[String: String]>.value(`fields`))).casted() + __value = try methodReturnValue(.m_registerUser__fields_fieldsisSocial_isSocial(Parameter<[String: String]>.value(`fields`), Parameter.value(`isSocial`))).casted() } catch MockError.notStubed { - onFatalFailure("Stub return value not specified for registerUser(fields: [String: String]). Use given") - Failure("Stub return value not specified for registerUser(fields: [String: String]). Use given") + onFatalFailure("Stub return value not specified for registerUser(fields: [String: String], isSocial: Bool). Use given") + Failure("Stub return value not specified for registerUser(fields: [String: String], isSocial: Bool). Use given") } catch { throw error } @@ -156,10 +173,11 @@ open class AuthInteractorProtocolMock: AuthInteractorProtocol, Mock { fileprivate enum MethodType { case m_login__username_usernamepassword_password(Parameter, Parameter) + case m_login__externalToken_externalTokenbackend_backend(Parameter, Parameter) case m_resetPassword__email_email(Parameter) case m_getCookies__force_force(Parameter) case m_getRegistrationFields - case m_registerUser__fields_fields(Parameter<[String: String]>) + case m_registerUser__fields_fieldsisSocial_isSocial(Parameter<[String: String]>, Parameter) case m_validateRegistrationFields__fields_fields(Parameter<[String: String]>) static func compareParameters(lhs: MethodType, rhs: MethodType, matcher: Matcher) -> Matcher.ComparisonResult { @@ -170,6 +188,12 @@ 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__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")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBackend, rhs: rhsBackend, with: matcher), lhsBackend, rhsBackend, "backend")) + return Matcher.ComparisonResult(results) + case (.m_resetPassword__email_email(let lhsEmail), .m_resetPassword__email_email(let rhsEmail)): var results: [Matcher.ParameterComparisonResult] = [] results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsEmail, rhs: rhsEmail, with: matcher), lhsEmail, rhsEmail, "email")) @@ -182,9 +206,10 @@ open class AuthInteractorProtocolMock: AuthInteractorProtocol, Mock { case (.m_getRegistrationFields, .m_getRegistrationFields): return .match - case (.m_registerUser__fields_fields(let lhsFields), .m_registerUser__fields_fields(let rhsFields)): + case (.m_registerUser__fields_fieldsisSocial_isSocial(let lhsFields, let lhsIssocial), .m_registerUser__fields_fieldsisSocial_isSocial(let rhsFields, let rhsIssocial)): var results: [Matcher.ParameterComparisonResult] = [] results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsFields, rhs: rhsFields, with: matcher), lhsFields, rhsFields, "fields")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsIssocial, rhs: rhsIssocial, with: matcher), lhsIssocial, rhsIssocial, "isSocial")) return Matcher.ComparisonResult(results) case (.m_validateRegistrationFields__fields_fields(let lhsFields), .m_validateRegistrationFields__fields_fields(let rhsFields)): @@ -198,20 +223,22 @@ 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__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 case .m_getRegistrationFields: return 0 - case let .m_registerUser__fields_fields(p0): return p0.intValue + case let .m_registerUser__fields_fieldsisSocial_isSocial(p0, p1): return p0.intValue + p1.intValue case let .m_validateRegistrationFields__fields_fields(p0): return p0.intValue } } func assertionName() -> String { switch self { case .m_login__username_usernamepassword_password: return ".login(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:)" case .m_getRegistrationFields: return ".getRegistrationFields()" - case .m_registerUser__fields_fields: return ".registerUser(fields:)" + case .m_registerUser__fields_fieldsisSocial_isSocial: return ".registerUser(fields:isSocial:)" case .m_validateRegistrationFields__fields_fields: return ".validateRegistrationFields(fields:)" } } @@ -230,14 +257,18 @@ open class AuthInteractorProtocolMock: AuthInteractorProtocol, Mock { public static func login(username: Parameter, password: Parameter, willReturn: User...) -> MethodStub { return Given(method: .m_login__username_usernamepassword_password(`username`, `password`), products: willReturn.map({ StubProduct.return($0 as Any) })) } + @discardableResult + 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) })) + } 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) })) } public static func getRegistrationFields(willReturn: [PickerFields]...) -> MethodStub { return Given(method: .m_getRegistrationFields, products: willReturn.map({ StubProduct.return($0 as Any) })) } - public static func registerUser(fields: Parameter<[String: String]>, willReturn: User...) -> MethodStub { - return Given(method: .m_registerUser__fields_fields(`fields`), products: willReturn.map({ StubProduct.return($0 as Any) })) + public static func registerUser(fields: Parameter<[String: String]>, isSocial: Parameter, willReturn: User...) -> MethodStub { + return Given(method: .m_registerUser__fields_fieldsisSocial_isSocial(`fields`, `isSocial`), products: willReturn.map({ StubProduct.return($0 as Any) })) } public static func validateRegistrationFields(fields: Parameter<[String: String]>, willReturn: [String: String]...) -> MethodStub { return Given(method: .m_validateRegistrationFields__fields_fields(`fields`), products: willReturn.map({ StubProduct.return($0 as Any) })) @@ -254,6 +285,18 @@ open class AuthInteractorProtocolMock: AuthInteractorProtocol, Mock { willProduce(stubber) return given } + @discardableResult + public static func login(externalToken: Parameter, backend: Parameter, willThrow: Error...) -> MethodStub { + return Given(method: .m_login__externalToken_externalTokenbackend_backend(`externalToken`, `backend`), products: willThrow.map({ StubProduct.throw($0) })) + } + @discardableResult + public static func login(externalToken: Parameter, backend: Parameter, willProduce: (StubberThrows) -> Void) -> MethodStub { + let willThrow: [Error] = [] + let given: Given = { return Given(method: .m_login__externalToken_externalTokenbackend_backend(`externalToken`, `backend`), products: willThrow.map({ StubProduct.throw($0) })) }() + let stubber = given.stubThrows(for: (User).self) + willProduce(stubber) + return given + } public static func resetPassword(email: Parameter, willThrow: Error...) -> MethodStub { return Given(method: .m_resetPassword__email_email(`email`), products: willThrow.map({ StubProduct.throw($0) })) } @@ -284,12 +327,12 @@ open class AuthInteractorProtocolMock: AuthInteractorProtocol, Mock { willProduce(stubber) return given } - public static func registerUser(fields: Parameter<[String: String]>, willThrow: Error...) -> MethodStub { - return Given(method: .m_registerUser__fields_fields(`fields`), products: willThrow.map({ StubProduct.throw($0) })) + public static func registerUser(fields: Parameter<[String: String]>, isSocial: Parameter, willThrow: Error...) -> MethodStub { + return Given(method: .m_registerUser__fields_fieldsisSocial_isSocial(`fields`, `isSocial`), products: willThrow.map({ StubProduct.throw($0) })) } - public static func registerUser(fields: Parameter<[String: String]>, willProduce: (StubberThrows) -> Void) -> MethodStub { + public static func registerUser(fields: Parameter<[String: String]>, isSocial: Parameter, willProduce: (StubberThrows) -> Void) -> MethodStub { let willThrow: [Error] = [] - let given: Given = { return Given(method: .m_registerUser__fields_fields(`fields`), products: willThrow.map({ StubProduct.throw($0) })) }() + let given: Given = { return Given(method: .m_registerUser__fields_fieldsisSocial_isSocial(`fields`, `isSocial`), products: willThrow.map({ StubProduct.throw($0) })) }() let stubber = given.stubThrows(for: (User).self) willProduce(stubber) return given @@ -311,10 +354,12 @@ open class AuthInteractorProtocolMock: AuthInteractorProtocol, Mock { @discardableResult public static func login(username: Parameter, password: Parameter) -> Verify { return Verify(method: .m_login__username_usernamepassword_password(`username`, `password`))} + @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`))} public static func getCookies(force: Parameter) -> Verify { return Verify(method: .m_getCookies__force_force(`force`))} public static func getRegistrationFields() -> Verify { return Verify(method: .m_getRegistrationFields)} - public static func registerUser(fields: Parameter<[String: String]>) -> Verify { return Verify(method: .m_registerUser__fields_fields(`fields`))} + public static func registerUser(fields: Parameter<[String: String]>, isSocial: Parameter) -> Verify { return Verify(method: .m_registerUser__fields_fieldsisSocial_isSocial(`fields`, `isSocial`))} public static func validateRegistrationFields(fields: Parameter<[String: String]>) -> Verify { return Verify(method: .m_validateRegistrationFields__fields_fields(`fields`))} } @@ -326,6 +371,10 @@ open class AuthInteractorProtocolMock: AuthInteractorProtocol, Mock { public static func login(username: Parameter, password: Parameter, perform: @escaping (String, String) -> Void) -> Perform { return Perform(method: .m_login__username_usernamepassword_password(`username`, `password`), performs: perform) } + @discardableResult + public static func login(externalToken: Parameter, backend: Parameter, perform: @escaping (String, String) -> Void) -> Perform { + return Perform(method: .m_login__externalToken_externalTokenbackend_backend(`externalToken`, `backend`), performs: perform) + } public static func resetPassword(email: Parameter, perform: @escaping (String) -> Void) -> Perform { return Perform(method: .m_resetPassword__email_email(`email`), performs: perform) } @@ -335,8 +384,8 @@ open class AuthInteractorProtocolMock: AuthInteractorProtocol, Mock { public static func getRegistrationFields(perform: @escaping () -> Void) -> Perform { return Perform(method: .m_getRegistrationFields, performs: perform) } - public static func registerUser(fields: Parameter<[String: String]>, perform: @escaping ([String: String]) -> Void) -> Perform { - return Perform(method: .m_registerUser__fields_fields(`fields`), performs: perform) + public static func registerUser(fields: Parameter<[String: String]>, isSocial: Parameter, perform: @escaping ([String: String], Bool) -> Void) -> Perform { + return Perform(method: .m_registerUser__fields_fieldsisSocial_isSocial(`fields`, `isSocial`), performs: perform) } public static func validateRegistrationFields(fields: Parameter<[String: String]>, perform: @escaping ([String: String]) -> Void) -> Perform { return Perform(method: .m_validateRegistrationFields__fields_fields(`fields`), performs: perform) diff --git a/Discovery/DiscoveryTests/DiscoveryMock.generated.swift b/Discovery/DiscoveryTests/DiscoveryMock.generated.swift index b95cb9f0d..b4d088a28 100644 --- a/Discovery/DiscoveryTests/DiscoveryMock.generated.swift +++ b/Discovery/DiscoveryTests/DiscoveryMock.generated.swift @@ -76,6 +76,23 @@ open class AuthInteractorProtocolMock: AuthInteractorProtocol, Mock { 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`) + var __value: User + do { + __value = try methodReturnValue(.m_login__externalToken_externalTokenbackend_backend(Parameter.value(`externalToken`), Parameter.value(`backend`))).casted() + } catch MockError.notStubed { + onFatalFailure("Stub return value not specified for login(externalToken: String, backend: String). Use given") + Failure("Stub return value not specified for login(externalToken: String, backend: 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 @@ -121,16 +138,16 @@ open class AuthInteractorProtocolMock: AuthInteractorProtocol, Mock { return __value } - open func registerUser(fields: [String: String]) throws -> User { - addInvocation(.m_registerUser__fields_fields(Parameter<[String: String]>.value(`fields`))) - let perform = methodPerformValue(.m_registerUser__fields_fields(Parameter<[String: String]>.value(`fields`))) as? ([String: String]) -> Void - perform?(`fields`) + open func registerUser(fields: [String: String], isSocial: Bool) throws -> User { + addInvocation(.m_registerUser__fields_fieldsisSocial_isSocial(Parameter<[String: String]>.value(`fields`), Parameter.value(`isSocial`))) + let perform = methodPerformValue(.m_registerUser__fields_fieldsisSocial_isSocial(Parameter<[String: String]>.value(`fields`), Parameter.value(`isSocial`))) as? ([String: String], Bool) -> Void + perform?(`fields`, `isSocial`) var __value: User do { - __value = try methodReturnValue(.m_registerUser__fields_fields(Parameter<[String: String]>.value(`fields`))).casted() + __value = try methodReturnValue(.m_registerUser__fields_fieldsisSocial_isSocial(Parameter<[String: String]>.value(`fields`), Parameter.value(`isSocial`))).casted() } catch MockError.notStubed { - onFatalFailure("Stub return value not specified for registerUser(fields: [String: String]). Use given") - Failure("Stub return value not specified for registerUser(fields: [String: String]). Use given") + onFatalFailure("Stub return value not specified for registerUser(fields: [String: String], isSocial: Bool). Use given") + Failure("Stub return value not specified for registerUser(fields: [String: String], isSocial: Bool). Use given") } catch { throw error } @@ -156,10 +173,11 @@ open class AuthInteractorProtocolMock: AuthInteractorProtocol, Mock { fileprivate enum MethodType { case m_login__username_usernamepassword_password(Parameter, Parameter) + case m_login__externalToken_externalTokenbackend_backend(Parameter, Parameter) case m_resetPassword__email_email(Parameter) case m_getCookies__force_force(Parameter) case m_getRegistrationFields - case m_registerUser__fields_fields(Parameter<[String: String]>) + case m_registerUser__fields_fieldsisSocial_isSocial(Parameter<[String: String]>, Parameter) case m_validateRegistrationFields__fields_fields(Parameter<[String: String]>) static func compareParameters(lhs: MethodType, rhs: MethodType, matcher: Matcher) -> Matcher.ComparisonResult { @@ -170,6 +188,12 @@ 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__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")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBackend, rhs: rhsBackend, with: matcher), lhsBackend, rhsBackend, "backend")) + return Matcher.ComparisonResult(results) + case (.m_resetPassword__email_email(let lhsEmail), .m_resetPassword__email_email(let rhsEmail)): var results: [Matcher.ParameterComparisonResult] = [] results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsEmail, rhs: rhsEmail, with: matcher), lhsEmail, rhsEmail, "email")) @@ -182,9 +206,10 @@ open class AuthInteractorProtocolMock: AuthInteractorProtocol, Mock { case (.m_getRegistrationFields, .m_getRegistrationFields): return .match - case (.m_registerUser__fields_fields(let lhsFields), .m_registerUser__fields_fields(let rhsFields)): + case (.m_registerUser__fields_fieldsisSocial_isSocial(let lhsFields, let lhsIssocial), .m_registerUser__fields_fieldsisSocial_isSocial(let rhsFields, let rhsIssocial)): var results: [Matcher.ParameterComparisonResult] = [] results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsFields, rhs: rhsFields, with: matcher), lhsFields, rhsFields, "fields")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsIssocial, rhs: rhsIssocial, with: matcher), lhsIssocial, rhsIssocial, "isSocial")) return Matcher.ComparisonResult(results) case (.m_validateRegistrationFields__fields_fields(let lhsFields), .m_validateRegistrationFields__fields_fields(let rhsFields)): @@ -198,20 +223,22 @@ 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__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 case .m_getRegistrationFields: return 0 - case let .m_registerUser__fields_fields(p0): return p0.intValue + case let .m_registerUser__fields_fieldsisSocial_isSocial(p0, p1): return p0.intValue + p1.intValue case let .m_validateRegistrationFields__fields_fields(p0): return p0.intValue } } func assertionName() -> String { switch self { case .m_login__username_usernamepassword_password: return ".login(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:)" case .m_getRegistrationFields: return ".getRegistrationFields()" - case .m_registerUser__fields_fields: return ".registerUser(fields:)" + case .m_registerUser__fields_fieldsisSocial_isSocial: return ".registerUser(fields:isSocial:)" case .m_validateRegistrationFields__fields_fields: return ".validateRegistrationFields(fields:)" } } @@ -230,14 +257,18 @@ open class AuthInteractorProtocolMock: AuthInteractorProtocol, Mock { public static func login(username: Parameter, password: Parameter, willReturn: User...) -> MethodStub { return Given(method: .m_login__username_usernamepassword_password(`username`, `password`), products: willReturn.map({ StubProduct.return($0 as Any) })) } + @discardableResult + 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) })) + } 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) })) } public static func getRegistrationFields(willReturn: [PickerFields]...) -> MethodStub { return Given(method: .m_getRegistrationFields, products: willReturn.map({ StubProduct.return($0 as Any) })) } - public static func registerUser(fields: Parameter<[String: String]>, willReturn: User...) -> MethodStub { - return Given(method: .m_registerUser__fields_fields(`fields`), products: willReturn.map({ StubProduct.return($0 as Any) })) + public static func registerUser(fields: Parameter<[String: String]>, isSocial: Parameter, willReturn: User...) -> MethodStub { + return Given(method: .m_registerUser__fields_fieldsisSocial_isSocial(`fields`, `isSocial`), products: willReturn.map({ StubProduct.return($0 as Any) })) } public static func validateRegistrationFields(fields: Parameter<[String: String]>, willReturn: [String: String]...) -> MethodStub { return Given(method: .m_validateRegistrationFields__fields_fields(`fields`), products: willReturn.map({ StubProduct.return($0 as Any) })) @@ -254,6 +285,18 @@ open class AuthInteractorProtocolMock: AuthInteractorProtocol, Mock { willProduce(stubber) return given } + @discardableResult + public static func login(externalToken: Parameter, backend: Parameter, willThrow: Error...) -> MethodStub { + return Given(method: .m_login__externalToken_externalTokenbackend_backend(`externalToken`, `backend`), products: willThrow.map({ StubProduct.throw($0) })) + } + @discardableResult + public static func login(externalToken: Parameter, backend: Parameter, willProduce: (StubberThrows) -> Void) -> MethodStub { + let willThrow: [Error] = [] + let given: Given = { return Given(method: .m_login__externalToken_externalTokenbackend_backend(`externalToken`, `backend`), products: willThrow.map({ StubProduct.throw($0) })) }() + let stubber = given.stubThrows(for: (User).self) + willProduce(stubber) + return given + } public static func resetPassword(email: Parameter, willThrow: Error...) -> MethodStub { return Given(method: .m_resetPassword__email_email(`email`), products: willThrow.map({ StubProduct.throw($0) })) } @@ -284,12 +327,12 @@ open class AuthInteractorProtocolMock: AuthInteractorProtocol, Mock { willProduce(stubber) return given } - public static func registerUser(fields: Parameter<[String: String]>, willThrow: Error...) -> MethodStub { - return Given(method: .m_registerUser__fields_fields(`fields`), products: willThrow.map({ StubProduct.throw($0) })) + public static func registerUser(fields: Parameter<[String: String]>, isSocial: Parameter, willThrow: Error...) -> MethodStub { + return Given(method: .m_registerUser__fields_fieldsisSocial_isSocial(`fields`, `isSocial`), products: willThrow.map({ StubProduct.throw($0) })) } - public static func registerUser(fields: Parameter<[String: String]>, willProduce: (StubberThrows) -> Void) -> MethodStub { + public static func registerUser(fields: Parameter<[String: String]>, isSocial: Parameter, willProduce: (StubberThrows) -> Void) -> MethodStub { let willThrow: [Error] = [] - let given: Given = { return Given(method: .m_registerUser__fields_fields(`fields`), products: willThrow.map({ StubProduct.throw($0) })) }() + let given: Given = { return Given(method: .m_registerUser__fields_fieldsisSocial_isSocial(`fields`, `isSocial`), products: willThrow.map({ StubProduct.throw($0) })) }() let stubber = given.stubThrows(for: (User).self) willProduce(stubber) return given @@ -311,10 +354,12 @@ open class AuthInteractorProtocolMock: AuthInteractorProtocol, Mock { @discardableResult public static func login(username: Parameter, password: Parameter) -> Verify { return Verify(method: .m_login__username_usernamepassword_password(`username`, `password`))} + @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`))} public static func getCookies(force: Parameter) -> Verify { return Verify(method: .m_getCookies__force_force(`force`))} public static func getRegistrationFields() -> Verify { return Verify(method: .m_getRegistrationFields)} - public static func registerUser(fields: Parameter<[String: String]>) -> Verify { return Verify(method: .m_registerUser__fields_fields(`fields`))} + public static func registerUser(fields: Parameter<[String: String]>, isSocial: Parameter) -> Verify { return Verify(method: .m_registerUser__fields_fieldsisSocial_isSocial(`fields`, `isSocial`))} public static func validateRegistrationFields(fields: Parameter<[String: String]>) -> Verify { return Verify(method: .m_validateRegistrationFields__fields_fields(`fields`))} } @@ -326,6 +371,10 @@ open class AuthInteractorProtocolMock: AuthInteractorProtocol, Mock { public static func login(username: Parameter, password: Parameter, perform: @escaping (String, String) -> Void) -> Perform { return Perform(method: .m_login__username_usernamepassword_password(`username`, `password`), performs: perform) } + @discardableResult + public static func login(externalToken: Parameter, backend: Parameter, perform: @escaping (String, String) -> Void) -> Perform { + return Perform(method: .m_login__externalToken_externalTokenbackend_backend(`externalToken`, `backend`), performs: perform) + } public static func resetPassword(email: Parameter, perform: @escaping (String) -> Void) -> Perform { return Perform(method: .m_resetPassword__email_email(`email`), performs: perform) } @@ -335,8 +384,8 @@ open class AuthInteractorProtocolMock: AuthInteractorProtocol, Mock { public static func getRegistrationFields(perform: @escaping () -> Void) -> Perform { return Perform(method: .m_getRegistrationFields, performs: perform) } - public static func registerUser(fields: Parameter<[String: String]>, perform: @escaping ([String: String]) -> Void) -> Perform { - return Perform(method: .m_registerUser__fields_fields(`fields`), performs: perform) + public static func registerUser(fields: Parameter<[String: String]>, isSocial: Parameter, perform: @escaping ([String: String], Bool) -> Void) -> Perform { + return Perform(method: .m_registerUser__fields_fieldsisSocial_isSocial(`fields`, `isSocial`), performs: perform) } public static func validateRegistrationFields(fields: Parameter<[String: String]>, perform: @escaping ([String: String]) -> Void) -> Perform { return Perform(method: .m_validateRegistrationFields__fields_fields(`fields`), performs: perform) diff --git a/Discussion/DiscussionTests/DiscussionMock.generated.swift b/Discussion/DiscussionTests/DiscussionMock.generated.swift index 5f2e17f42..1afce2ae3 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 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`) + var __value: User + do { + __value = try methodReturnValue(.m_login__externalToken_externalTokenbackend_backend(Parameter.value(`externalToken`), Parameter.value(`backend`))).casted() + } catch MockError.notStubed { + onFatalFailure("Stub return value not specified for login(externalToken: String, backend: String). Use given") + Failure("Stub return value not specified for login(externalToken: String, backend: 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 @@ -121,16 +138,16 @@ open class AuthInteractorProtocolMock: AuthInteractorProtocol, Mock { return __value } - open func registerUser(fields: [String: String]) throws -> User { - addInvocation(.m_registerUser__fields_fields(Parameter<[String: String]>.value(`fields`))) - let perform = methodPerformValue(.m_registerUser__fields_fields(Parameter<[String: String]>.value(`fields`))) as? ([String: String]) -> Void - perform?(`fields`) + open func registerUser(fields: [String: String], isSocial: Bool) throws -> User { + addInvocation(.m_registerUser__fields_fieldsisSocial_isSocial(Parameter<[String: String]>.value(`fields`), Parameter.value(`isSocial`))) + let perform = methodPerformValue(.m_registerUser__fields_fieldsisSocial_isSocial(Parameter<[String: String]>.value(`fields`), Parameter.value(`isSocial`))) as? ([String: String], Bool) -> Void + perform?(`fields`, `isSocial`) var __value: User do { - __value = try methodReturnValue(.m_registerUser__fields_fields(Parameter<[String: String]>.value(`fields`))).casted() + __value = try methodReturnValue(.m_registerUser__fields_fieldsisSocial_isSocial(Parameter<[String: String]>.value(`fields`), Parameter.value(`isSocial`))).casted() } catch MockError.notStubed { - onFatalFailure("Stub return value not specified for registerUser(fields: [String: String]). Use given") - Failure("Stub return value not specified for registerUser(fields: [String: String]). Use given") + onFatalFailure("Stub return value not specified for registerUser(fields: [String: String], isSocial: Bool). Use given") + Failure("Stub return value not specified for registerUser(fields: [String: String], isSocial: Bool). Use given") } catch { throw error } @@ -156,10 +173,11 @@ open class AuthInteractorProtocolMock: AuthInteractorProtocol, Mock { fileprivate enum MethodType { case m_login__username_usernamepassword_password(Parameter, Parameter) + case m_login__externalToken_externalTokenbackend_backend(Parameter, Parameter) case m_resetPassword__email_email(Parameter) case m_getCookies__force_force(Parameter) case m_getRegistrationFields - case m_registerUser__fields_fields(Parameter<[String: String]>) + case m_registerUser__fields_fieldsisSocial_isSocial(Parameter<[String: String]>, Parameter) case m_validateRegistrationFields__fields_fields(Parameter<[String: String]>) static func compareParameters(lhs: MethodType, rhs: MethodType, matcher: Matcher) -> Matcher.ComparisonResult { @@ -170,6 +188,12 @@ 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__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")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBackend, rhs: rhsBackend, with: matcher), lhsBackend, rhsBackend, "backend")) + return Matcher.ComparisonResult(results) + case (.m_resetPassword__email_email(let lhsEmail), .m_resetPassword__email_email(let rhsEmail)): var results: [Matcher.ParameterComparisonResult] = [] results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsEmail, rhs: rhsEmail, with: matcher), lhsEmail, rhsEmail, "email")) @@ -182,9 +206,10 @@ open class AuthInteractorProtocolMock: AuthInteractorProtocol, Mock { case (.m_getRegistrationFields, .m_getRegistrationFields): return .match - case (.m_registerUser__fields_fields(let lhsFields), .m_registerUser__fields_fields(let rhsFields)): + case (.m_registerUser__fields_fieldsisSocial_isSocial(let lhsFields, let lhsIssocial), .m_registerUser__fields_fieldsisSocial_isSocial(let rhsFields, let rhsIssocial)): var results: [Matcher.ParameterComparisonResult] = [] results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsFields, rhs: rhsFields, with: matcher), lhsFields, rhsFields, "fields")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsIssocial, rhs: rhsIssocial, with: matcher), lhsIssocial, rhsIssocial, "isSocial")) return Matcher.ComparisonResult(results) case (.m_validateRegistrationFields__fields_fields(let lhsFields), .m_validateRegistrationFields__fields_fields(let rhsFields)): @@ -198,20 +223,22 @@ 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__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 case .m_getRegistrationFields: return 0 - case let .m_registerUser__fields_fields(p0): return p0.intValue + case let .m_registerUser__fields_fieldsisSocial_isSocial(p0, p1): return p0.intValue + p1.intValue case let .m_validateRegistrationFields__fields_fields(p0): return p0.intValue } } func assertionName() -> String { switch self { case .m_login__username_usernamepassword_password: return ".login(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:)" case .m_getRegistrationFields: return ".getRegistrationFields()" - case .m_registerUser__fields_fields: return ".registerUser(fields:)" + case .m_registerUser__fields_fieldsisSocial_isSocial: return ".registerUser(fields:isSocial:)" case .m_validateRegistrationFields__fields_fields: return ".validateRegistrationFields(fields:)" } } @@ -230,14 +257,18 @@ open class AuthInteractorProtocolMock: AuthInteractorProtocol, Mock { public static func login(username: Parameter, password: Parameter, willReturn: User...) -> MethodStub { return Given(method: .m_login__username_usernamepassword_password(`username`, `password`), products: willReturn.map({ StubProduct.return($0 as Any) })) } + @discardableResult + 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) })) + } 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) })) } public static func getRegistrationFields(willReturn: [PickerFields]...) -> MethodStub { return Given(method: .m_getRegistrationFields, products: willReturn.map({ StubProduct.return($0 as Any) })) } - public static func registerUser(fields: Parameter<[String: String]>, willReturn: User...) -> MethodStub { - return Given(method: .m_registerUser__fields_fields(`fields`), products: willReturn.map({ StubProduct.return($0 as Any) })) + public static func registerUser(fields: Parameter<[String: String]>, isSocial: Parameter, willReturn: User...) -> MethodStub { + return Given(method: .m_registerUser__fields_fieldsisSocial_isSocial(`fields`, `isSocial`), products: willReturn.map({ StubProduct.return($0 as Any) })) } public static func validateRegistrationFields(fields: Parameter<[String: String]>, willReturn: [String: String]...) -> MethodStub { return Given(method: .m_validateRegistrationFields__fields_fields(`fields`), products: willReturn.map({ StubProduct.return($0 as Any) })) @@ -254,6 +285,18 @@ open class AuthInteractorProtocolMock: AuthInteractorProtocol, Mock { willProduce(stubber) return given } + @discardableResult + public static func login(externalToken: Parameter, backend: Parameter, willThrow: Error...) -> MethodStub { + return Given(method: .m_login__externalToken_externalTokenbackend_backend(`externalToken`, `backend`), products: willThrow.map({ StubProduct.throw($0) })) + } + @discardableResult + public static func login(externalToken: Parameter, backend: Parameter, willProduce: (StubberThrows) -> Void) -> MethodStub { + let willThrow: [Error] = [] + let given: Given = { return Given(method: .m_login__externalToken_externalTokenbackend_backend(`externalToken`, `backend`), products: willThrow.map({ StubProduct.throw($0) })) }() + let stubber = given.stubThrows(for: (User).self) + willProduce(stubber) + return given + } public static func resetPassword(email: Parameter, willThrow: Error...) -> MethodStub { return Given(method: .m_resetPassword__email_email(`email`), products: willThrow.map({ StubProduct.throw($0) })) } @@ -284,12 +327,12 @@ open class AuthInteractorProtocolMock: AuthInteractorProtocol, Mock { willProduce(stubber) return given } - public static func registerUser(fields: Parameter<[String: String]>, willThrow: Error...) -> MethodStub { - return Given(method: .m_registerUser__fields_fields(`fields`), products: willThrow.map({ StubProduct.throw($0) })) + public static func registerUser(fields: Parameter<[String: String]>, isSocial: Parameter, willThrow: Error...) -> MethodStub { + return Given(method: .m_registerUser__fields_fieldsisSocial_isSocial(`fields`, `isSocial`), products: willThrow.map({ StubProduct.throw($0) })) } - public static func registerUser(fields: Parameter<[String: String]>, willProduce: (StubberThrows) -> Void) -> MethodStub { + public static func registerUser(fields: Parameter<[String: String]>, isSocial: Parameter, willProduce: (StubberThrows) -> Void) -> MethodStub { let willThrow: [Error] = [] - let given: Given = { return Given(method: .m_registerUser__fields_fields(`fields`), products: willThrow.map({ StubProduct.throw($0) })) }() + let given: Given = { return Given(method: .m_registerUser__fields_fieldsisSocial_isSocial(`fields`, `isSocial`), products: willThrow.map({ StubProduct.throw($0) })) }() let stubber = given.stubThrows(for: (User).self) willProduce(stubber) return given @@ -311,10 +354,12 @@ open class AuthInteractorProtocolMock: AuthInteractorProtocol, Mock { @discardableResult public static func login(username: Parameter, password: Parameter) -> Verify { return Verify(method: .m_login__username_usernamepassword_password(`username`, `password`))} + @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`))} public static func getCookies(force: Parameter) -> Verify { return Verify(method: .m_getCookies__force_force(`force`))} public static func getRegistrationFields() -> Verify { return Verify(method: .m_getRegistrationFields)} - public static func registerUser(fields: Parameter<[String: String]>) -> Verify { return Verify(method: .m_registerUser__fields_fields(`fields`))} + public static func registerUser(fields: Parameter<[String: String]>, isSocial: Parameter) -> Verify { return Verify(method: .m_registerUser__fields_fieldsisSocial_isSocial(`fields`, `isSocial`))} public static func validateRegistrationFields(fields: Parameter<[String: String]>) -> Verify { return Verify(method: .m_validateRegistrationFields__fields_fields(`fields`))} } @@ -326,6 +371,10 @@ open class AuthInteractorProtocolMock: AuthInteractorProtocol, Mock { public static func login(username: Parameter, password: Parameter, perform: @escaping (String, String) -> Void) -> Perform { return Perform(method: .m_login__username_usernamepassword_password(`username`, `password`), performs: perform) } + @discardableResult + public static func login(externalToken: Parameter, backend: Parameter, perform: @escaping (String, String) -> Void) -> Perform { + return Perform(method: .m_login__externalToken_externalTokenbackend_backend(`externalToken`, `backend`), performs: perform) + } public static func resetPassword(email: Parameter, perform: @escaping (String) -> Void) -> Perform { return Perform(method: .m_resetPassword__email_email(`email`), performs: perform) } @@ -335,8 +384,8 @@ open class AuthInteractorProtocolMock: AuthInteractorProtocol, Mock { public static func getRegistrationFields(perform: @escaping () -> Void) -> Perform { return Perform(method: .m_getRegistrationFields, performs: perform) } - public static func registerUser(fields: Parameter<[String: String]>, perform: @escaping ([String: String]) -> Void) -> Perform { - return Perform(method: .m_registerUser__fields_fields(`fields`), performs: perform) + public static func registerUser(fields: Parameter<[String: String]>, isSocial: Parameter, perform: @escaping ([String: String], Bool) -> Void) -> Perform { + return Perform(method: .m_registerUser__fields_fieldsisSocial_isSocial(`fields`, `isSocial`), performs: perform) } public static func validateRegistrationFields(fields: Parameter<[String: String]>, perform: @escaping ([String: String]) -> Void) -> Perform { return Perform(method: .m_validateRegistrationFields__fields_fields(`fields`), performs: perform) diff --git a/OpenEdX.xcodeproj/project.pbxproj b/OpenEdX.xcodeproj/project.pbxproj index 9df9dfe4a..072473af5 100644 --- a/OpenEdX.xcodeproj/project.pbxproj +++ b/OpenEdX.xcodeproj/project.pbxproj @@ -45,6 +45,7 @@ 07D5DA3E28D075AB00752FD9 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 07D5DA3D28D075AB00752FD9 /* Assets.xcassets */; }; 07D5DA4128D075AB00752FD9 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 07D5DA3F28D075AB00752FD9 /* LaunchScreen.storyboard */; }; 95C140F3BDF778364986E83B /* Pods_App_OpenEdX.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F138C15C3A2515F8F94DAA8B /* Pods_App_OpenEdX.framework */; }; + BA3042792B1F7147009B64B7 /* MSAL in Frameworks */ = {isa = PBXBuildFile; productRef = BA3042782B1F7147009B64B7 /* MSAL */; }; E0D6E6A32B1626B10089F9C9 /* Theme.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E0D6E6A22B1626B10089F9C9 /* Theme.framework */; }; E0D6E6A42B1626D60089F9C9 /* Theme.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = E0D6E6A22B1626B10089F9C9 /* Theme.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; /* End PBXBuildFile section */ @@ -130,6 +131,7 @@ 028A37362ADFF404008CA604 /* WhatsNew.framework in Frameworks */, 025DE1A428DB4DAE0053E0F4 /* Profile.framework in Frameworks */, 0770DE4B28D0A462006D8A5D /* Authorization.framework in Frameworks */, + BA3042792B1F7147009B64B7 /* MSAL in Frameworks */, 072787B128D34D83002E9142 /* Discovery.framework in Frameworks */, 0218196428F734FA00202564 /* Discussion.framework in Frameworks */, 0219C67728F4347600D64452 /* Course.framework in Frameworks */, @@ -267,6 +269,9 @@ dependencies = ( ); name = OpenEdX; + packageProductDependencies = ( + BA3042782B1F7147009B64B7 /* MSAL */, + ); productName = OpenEdX; productReference = 07D5DA3128D075AA00752FD9 /* OpenEdX.app */; productType = "com.apple.product-type.application"; @@ -296,6 +301,9 @@ uk, ); mainGroup = 07D5DA2828D075AA00752FD9; + packageReferences = ( + BA3042772B1F7147009B64B7 /* XCRemoteSwiftPackageReference "microsoft-authentication-library-for-objc" */, + ); productRefGroup = 07D5DA3228D075AA00752FD9 /* Products */; projectDirPath = ""; projectRoot = ""; @@ -1041,6 +1049,25 @@ defaultConfigurationName = ReleaseProd; }; /* End XCConfigurationList section */ + +/* Begin XCRemoteSwiftPackageReference section */ + BA3042772B1F7147009B64B7 /* XCRemoteSwiftPackageReference "microsoft-authentication-library-for-objc" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/AzureAD/microsoft-authentication-library-for-objc"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 1.2.19; + }; + }; +/* End XCRemoteSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + BA3042782B1F7147009B64B7 /* MSAL */ = { + isa = XCSwiftPackageProductDependency; + package = BA3042772B1F7147009B64B7 /* XCRemoteSwiftPackageReference "microsoft-authentication-library-for-objc" */; + productName = MSAL; + }; +/* End XCSwiftPackageProductDependency section */ }; rootObject = 07D5DA2928D075AA00752FD9 /* Project object */; } diff --git a/OpenEdX/AnalyticsManager.swift b/OpenEdX/AnalyticsManager.swift index 598aac053..ecb74b036 100644 --- a/OpenEdX/AnalyticsManager.swift +++ b/OpenEdX/AnalyticsManager.swift @@ -27,8 +27,8 @@ class AnalyticsManager: AuthorizationAnalytics, Analytics.setUserID(id) } - public func userLogin(method: LoginMethod) { - logEvent(.userLogin, parameters: [Key.method: method.rawValue]) + public func userLogin(method: AuthMethod) { + logEvent(.userLogin, parameters: [Key.method: method.analyticsValue]) } public func signUpClicked() { diff --git a/OpenEdX/AppDelegate.swift b/OpenEdX/AppDelegate.swift index 225c07506..563d587f1 100644 --- a/OpenEdX/AppDelegate.swift +++ b/OpenEdX/AppDelegate.swift @@ -12,6 +12,9 @@ import FirebaseCore import FirebaseAnalytics import FirebaseCrashlytics import Profile +import GoogleSignIn +import FacebookCore +import MSAL import Theme @UIApplicationMain @@ -31,15 +34,22 @@ class AppDelegate: UIResponder, UIApplicationDelegate { _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? ) -> Bool { - initDI() - if let config = Container.shared.resolve(ConfigProtocol.self), - let configuration = config.firebase.firebaseOptions { - FirebaseApp.configure(options: configuration) - Crashlytics.crashlytics().setCrashlyticsCollectionEnabled(true) + if let config = Container.shared.resolve(ConfigProtocol.self) { + if let configuration = config.firebase.firebaseOptions { + FirebaseApp.configure(options: configuration) + Crashlytics.crashlytics().setCrashlyticsCollectionEnabled(true) + } + + if config.facebook.enabled { + ApplicationDelegate.shared.application( + application, + didFinishLaunchingWithOptions: launchOptions + ) + } } - + Theme.Fonts.registerFonts() window = UIWindow(frame: UIScreen.main.bounds) window?.rootViewController = RouteController() @@ -55,6 +65,35 @@ class AppDelegate: UIResponder, UIApplicationDelegate { return true } + func application( + _ app: UIApplication, + open url: URL, options: [UIApplication.OpenURLOptionsKey: Any] = [:] + ) -> Bool { + if let config = Container.shared.resolve(ConfigProtocol.self) { + if config.facebook.enabled { + ApplicationDelegate.shared.application( + app, + open: url, + sourceApplication: options[UIApplication.OpenURLOptionsKey.sourceApplication] as? String, + annotation: options[UIApplication.OpenURLOptionsKey.annotation] + ) + } + + if config.google.enabled { + return GIDSignIn.sharedInstance.handle(url) + } + + if config.microsoft.enabled { + return MSALPublicClientApplication.handleMSALResponse( + url, + sourceApplication: options[UIApplication.OpenURLOptionsKey.sourceApplication] as? String + ) + } + } + + return false + } + private func initDI() { let navigation = UINavigationController() navigation.modalPresentationStyle = .fullScreen diff --git a/OpenEdX/Data/AppStorage.swift b/OpenEdX/Data/AppStorage.swift index 4bbb43eba..c0655026a 100644 --- a/OpenEdX/Data/AppStorage.swift +++ b/OpenEdX/Data/AppStorage.swift @@ -76,6 +76,32 @@ public class AppStorage: CoreStorage, ProfileStorage, WhatsNewStorage { } } + public var appleSignFullName: String? { + get { + return keychain.get(KEY_APPLE_SIGN_FULLNAME) + } + set(newValue) { + if let newValue { + keychain.set(newValue, forKey: KEY_APPLE_SIGN_FULLNAME) + } else { + keychain.delete(KEY_APPLE_SIGN_FULLNAME) + } + } + } + + public var appleSignEmail: String? { + get { + return keychain.get(KEY_APPLE_SIGN_EMAIL) + } + set(newValue) { + if let newValue { + keychain.set(newValue, forKey: KEY_APPLE_SIGN_EMAIL) + } else { + keychain.delete(KEY_APPLE_SIGN_EMAIL) + } + } + } + public var cookiesDate: String? { get { return userDefaults.string(forKey: KEY_COOKIES_DATE) @@ -180,4 +206,6 @@ public class AppStorage: CoreStorage, ProfileStorage, WhatsNewStorage { private let KEY_REVIEW_LAST_SHOWN_VERSION = "reviewLastShownVersion" private let KEY_REVIEW_LAST_REVIEW_DATE = "lastReviewDate" private let KEY_WHATSNEW_VERSION = "whatsNewVersion" + private let KEY_APPLE_SIGN_FULLNAME = "appleSignFullName" + private let KEY_APPLE_SIGN_EMAIL = "appleSignEmail" } diff --git a/OpenEdX/Info.plist b/OpenEdX/Info.plist index 452ba08ae..e4bdcba95 100644 --- a/OpenEdX/Info.plist +++ b/OpenEdX/Info.plist @@ -10,10 +10,6 @@ ITSAppUsesNonExemptEncryption - UIAppFonts - - UIViewControllerBasedStatusBarAppearance - LSApplicationQueriesSchemes googlegmail @@ -24,5 +20,9 @@ fastmail protonmail + UIAppFonts + + UIViewControllerBasedStatusBarAppearance + diff --git a/OpenEdX/OpenEdX.entitlements b/OpenEdX/OpenEdX.entitlements index 74c85d2ed..80b5221de 100644 --- a/OpenEdX/OpenEdX.entitlements +++ b/OpenEdX/OpenEdX.entitlements @@ -4,5 +4,9 @@ aps-environment development + com.apple.developer.applesignin + + Default + - \ No newline at end of file + diff --git a/Podfile.lock b/Podfile.lock index 7f9f32536..d9169ef4b 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -1,7 +1,7 @@ PODS: - - Alamofire (5.7.1) - - FirebaseAnalytics (10.11.0): - - FirebaseAnalytics/AdIdSupport (= 10.11.0) + - Alamofire (5.8.0) + - FirebaseAnalytics (10.15.0): + - FirebaseAnalytics/AdIdSupport (= 10.15.0) - FirebaseCore (~> 10.0) - FirebaseInstallations (~> 10.0) - GoogleUtilities/AppDelegateSwizzler (~> 7.11) @@ -9,24 +9,24 @@ PODS: - GoogleUtilities/Network (~> 7.11) - "GoogleUtilities/NSData+zlib (~> 7.11)" - nanopb (< 2.30910.0, >= 2.30908.0) - - FirebaseAnalytics/AdIdSupport (10.11.0): + - FirebaseAnalytics/AdIdSupport (10.15.0): - FirebaseCore (~> 10.0) - FirebaseInstallations (~> 10.0) - - GoogleAppMeasurement (= 10.11.0) + - GoogleAppMeasurement (= 10.15.0) - GoogleUtilities/AppDelegateSwizzler (~> 7.11) - GoogleUtilities/MethodSwizzler (~> 7.11) - GoogleUtilities/Network (~> 7.11) - "GoogleUtilities/NSData+zlib (~> 7.11)" - nanopb (< 2.30910.0, >= 2.30908.0) - - FirebaseCore (10.11.0): + - FirebaseCore (10.15.0): - FirebaseCoreInternal (~> 10.0) - GoogleUtilities/Environment (~> 7.8) - GoogleUtilities/Logger (~> 7.8) - - FirebaseCoreExtension (10.11.0): + - FirebaseCoreExtension (10.15.0): - FirebaseCore (~> 10.0) - - FirebaseCoreInternal (10.11.0): + - FirebaseCoreInternal (10.15.0): - "GoogleUtilities/NSData+zlib (~> 7.8)" - - FirebaseCrashlytics (10.11.0): + - FirebaseCrashlytics (10.15.0): - FirebaseCore (~> 10.5) - FirebaseInstallations (~> 10.0) - FirebaseSessions (~> 10.5) @@ -34,12 +34,12 @@ PODS: - GoogleUtilities/Environment (~> 7.8) - nanopb (< 2.30910.0, >= 2.30908.0) - PromisesObjC (~> 2.1) - - FirebaseInstallations (10.11.0): + - FirebaseInstallations (10.15.0): - FirebaseCore (~> 10.0) - GoogleUtilities/Environment (~> 7.8) - GoogleUtilities/UserDefaults (~> 7.8) - PromisesObjC (~> 2.1) - - FirebaseSessions (10.11.0): + - FirebaseSessions (10.15.0): - FirebaseCore (~> 10.5) - FirebaseCoreExtension (~> 10.0) - FirebaseInstallations (~> 10.0) @@ -47,65 +47,65 @@ PODS: - GoogleUtilities/Environment (~> 7.10) - nanopb (< 2.30910.0, >= 2.30908.0) - PromisesSwift (~> 2.1) - - GoogleAppMeasurement (10.11.0): - - GoogleAppMeasurement/AdIdSupport (= 10.11.0) + - GoogleAppMeasurement (10.15.0): + - GoogleAppMeasurement/AdIdSupport (= 10.15.0) - GoogleUtilities/AppDelegateSwizzler (~> 7.11) - GoogleUtilities/MethodSwizzler (~> 7.11) - GoogleUtilities/Network (~> 7.11) - "GoogleUtilities/NSData+zlib (~> 7.11)" - nanopb (< 2.30910.0, >= 2.30908.0) - - GoogleAppMeasurement/AdIdSupport (10.11.0): - - GoogleAppMeasurement/WithoutAdIdSupport (= 10.11.0) + - GoogleAppMeasurement/AdIdSupport (10.15.0): + - GoogleAppMeasurement/WithoutAdIdSupport (= 10.15.0) - GoogleUtilities/AppDelegateSwizzler (~> 7.11) - GoogleUtilities/MethodSwizzler (~> 7.11) - GoogleUtilities/Network (~> 7.11) - "GoogleUtilities/NSData+zlib (~> 7.11)" - nanopb (< 2.30910.0, >= 2.30908.0) - - GoogleAppMeasurement/WithoutAdIdSupport (10.11.0): + - GoogleAppMeasurement/WithoutAdIdSupport (10.15.0): - GoogleUtilities/AppDelegateSwizzler (~> 7.11) - GoogleUtilities/MethodSwizzler (~> 7.11) - GoogleUtilities/Network (~> 7.11) - "GoogleUtilities/NSData+zlib (~> 7.11)" - nanopb (< 2.30910.0, >= 2.30908.0) - - GoogleDataTransport (9.2.3): + - GoogleDataTransport (9.2.5): - GoogleUtilities/Environment (~> 7.7) - nanopb (< 2.30910.0, >= 2.30908.0) - PromisesObjC (< 3.0, >= 1.2) - - GoogleUtilities/AppDelegateSwizzler (7.11.1): + - GoogleUtilities/AppDelegateSwizzler (7.11.5): - GoogleUtilities/Environment - GoogleUtilities/Logger - GoogleUtilities/Network - - GoogleUtilities/Environment (7.11.1): + - GoogleUtilities/Environment (7.11.5): - PromisesObjC (< 3.0, >= 1.2) - - GoogleUtilities/Logger (7.11.1): + - GoogleUtilities/Logger (7.11.5): - GoogleUtilities/Environment - - GoogleUtilities/MethodSwizzler (7.11.1): + - GoogleUtilities/MethodSwizzler (7.11.5): - GoogleUtilities/Logger - - GoogleUtilities/Network (7.11.1): + - GoogleUtilities/Network (7.11.5): - GoogleUtilities/Logger - "GoogleUtilities/NSData+zlib" - GoogleUtilities/Reachability - - "GoogleUtilities/NSData+zlib (7.11.1)" - - GoogleUtilities/Reachability (7.11.1): + - "GoogleUtilities/NSData+zlib (7.11.5)" + - GoogleUtilities/Reachability (7.11.5): - GoogleUtilities/Logger - - GoogleUtilities/UserDefaults (7.11.1): + - GoogleUtilities/UserDefaults (7.11.5): - GoogleUtilities/Logger - KeychainSwift (20.0.0) - - Kingfisher (7.8.1) + - Kingfisher (7.9.1) - nanopb (2.30909.0): - nanopb/decode (= 2.30909.0) - nanopb/encode (= 2.30909.0) - nanopb/decode (2.30909.0) - nanopb/encode (2.30909.0) - - PromisesObjC (2.2.0) - - PromisesSwift (2.2.0): - - PromisesObjC (= 2.2.0) + - PromisesObjC (2.3.1) + - PromisesSwift (2.3.1): + - PromisesObjC (= 2.3.1) - Sourcery (1.8.0): - Sourcery/CLI-Only (= 1.8.0) - Sourcery/CLI-Only (1.8.0) - SwiftGen (6.6.2) - - SwiftLint (0.52.3) - - SwiftUIIntrospect (0.8.0) + - SwiftLint (0.53.0) + - SwiftUIIntrospect (0.12.0) - SwiftyMocky (4.2.0): - Sourcery (= 1.8.0) - Swinject (2.8.3) @@ -157,29 +157,29 @@ CHECKOUT OPTIONS: :tag: 4.2.0 SPEC CHECKSUMS: - Alamofire: 0123a34370cb170936ae79a8df46cc62b2edeb88 - FirebaseAnalytics: 6c6bf99e8854475bf1fa342028841be8ecd236da - FirebaseCore: 62fd4d549f5e3f3bd52b7998721c5fa0557fb355 - FirebaseCoreExtension: cacdad57fdb60e0b86dcbcac058ec78237946759 - FirebaseCoreInternal: 9e46c82a14a3b3a25be4e1e151ce6d21536b89c0 - FirebaseCrashlytics: 5927efd92f7fb052b0ab1e673d2f0d97274cd442 - FirebaseInstallations: 2a2c6859354cbec0a228a863d4daf6de7c74ced4 - FirebaseSessions: a62ba5c45284adb7714f4126cfbdb32b17c260bd - GoogleAppMeasurement: d3dabccdb336fc0ae44b633c8abaa26559893cd9 - GoogleDataTransport: f0308f5905a745f94fb91fea9c6cbaf3831cb1bd - GoogleUtilities: 9aa0ad5a7bc171f8bae016300bfcfa3fb8425749 + Alamofire: 0e92e751b3e9e66d7982db43919d01f313b8eb91 + FirebaseAnalytics: 47cef43728f81a839cf1306576bdd77ffa2eac7e + FirebaseCore: 2cec518b43635f96afe7ac3a9c513e47558abd2e + FirebaseCoreExtension: d3f1ea3725fb41f56e8fbfb29eeaff54e7ffb8f6 + FirebaseCoreInternal: 2f4bee5ed00301b5e56da0849268797a2dd31fb4 + FirebaseCrashlytics: a83f26fb922a3fe181eb738fb4dcf0c92bba6455 + FirebaseInstallations: cae95cab0f965ce05b805189de1d4c70b11c76fb + FirebaseSessions: ee59a7811bef4c15f65ef6472f3210faa293f9c8 + GoogleAppMeasurement: 722db6550d1e6d552b08398b69a975ac61039338 + GoogleDataTransport: 54dee9d48d14580407f8f5fbf2f496e92437a2f2 + GoogleUtilities: 13e2c67ede716b8741c7989e26893d151b2b2084 KeychainSwift: 0ce6a4d13f7228054d1a71bb1b500448fb2ab837 - Kingfisher: 63f677311d36a3473f6b978584f8a3845d023dc5 + Kingfisher: 1d14e9f59cbe19389f591c929000332bf70efd32 nanopb: b552cce312b6c8484180ef47159bc0f65a1f0431 - PromisesObjC: 09985d6d70fbe7878040aa746d78236e6946d2ef - PromisesSwift: cf9eb58666a43bbe007302226e510b16c1e10959 + PromisesObjC: c50d2056b5253dadbd6c2bea79b0674bd5a52fa4 + PromisesSwift: 28dca69a9c40779916ac2d6985a0192a5cb4a265 Sourcery: 6f5fe49b82b7e02e8c65560cbd52e1be67a1af2e SwiftGen: 1366a7f71aeef49954ca5a63ba4bef6b0f24138c - SwiftLint: 76ec9c62ad369cff2937474cb34c9af3fa270b7b - SwiftUIIntrospect: cde309fef1f6690dd7585100453f1985f3b91c77 + SwiftLint: 5ce4d6a8ff83f1b5fd5ad5dbf30965d35af65e44 + SwiftUIIntrospect: 89f443402f701a9197e9e54e3c2ed00b10c32e6d SwiftyMocky: c5e96e4ff76ec6dbf5a5941aeb039b5a546954a0 Swinject: 893c9a543000ac2f10ee4cbaf0933c6992c935d5 PODFILE CHECKSUM: 544edab2f9ecc4ac18973fb8865f1d0613ec8a28 -COCOAPODS: 1.11.3 +COCOAPODS: 1.13.0 diff --git a/Profile/Profile/Presentation/Profile/ProfileView.swift b/Profile/Profile/Presentation/Profile/ProfileView.swift index 5b174e581..431a94e12 100644 --- a/Profile/Profile/Presentation/Profile/ProfileView.swift +++ b/Profile/Profile/Presentation/Profile/ProfileView.swift @@ -19,7 +19,7 @@ public struct ProfileView: View { self._viewModel = StateObject(wrappedValue: { viewModel }()) self._settingsTapped = settingsTapped } - + public var body: some View { ZStack(alignment: .top) { // MARK: - Page Body diff --git a/Profile/Profile/Presentation/Profile/ProfileViewModel.swift b/Profile/Profile/Presentation/Profile/ProfileViewModel.swift index c5879e0aa..ae3d9a309 100644 --- a/Profile/Profile/Presentation/Profile/ProfileViewModel.swift +++ b/Profile/Profile/Presentation/Profile/ProfileViewModel.swift @@ -10,7 +10,7 @@ import Core import SwiftUI public class ProfileViewModel: ObservableObject { - + @Published public var userModel: UserProfile? @Published public var updatedAvatar: UIImage? @Published private(set) var isShowProgress = false @@ -22,6 +22,7 @@ public class ProfileViewModel: ObservableObject { } } } + private var cancellables = Set() enum VersionState { diff --git a/Profile/ProfileTests/ProfileMock.generated.swift b/Profile/ProfileTests/ProfileMock.generated.swift index 0ebcec11c..69aff547e 100644 --- a/Profile/ProfileTests/ProfileMock.generated.swift +++ b/Profile/ProfileTests/ProfileMock.generated.swift @@ -76,6 +76,23 @@ open class AuthInteractorProtocolMock: AuthInteractorProtocol, Mock { 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`) + var __value: User + do { + __value = try methodReturnValue(.m_login__externalToken_externalTokenbackend_backend(Parameter.value(`externalToken`), Parameter.value(`backend`))).casted() + } catch MockError.notStubed { + onFatalFailure("Stub return value not specified for login(externalToken: String, backend: String). Use given") + Failure("Stub return value not specified for login(externalToken: String, backend: 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 @@ -121,16 +138,16 @@ open class AuthInteractorProtocolMock: AuthInteractorProtocol, Mock { return __value } - open func registerUser(fields: [String: String]) throws -> User { - addInvocation(.m_registerUser__fields_fields(Parameter<[String: String]>.value(`fields`))) - let perform = methodPerformValue(.m_registerUser__fields_fields(Parameter<[String: String]>.value(`fields`))) as? ([String: String]) -> Void - perform?(`fields`) + open func registerUser(fields: [String: String], isSocial: Bool) throws -> User { + addInvocation(.m_registerUser__fields_fieldsisSocial_isSocial(Parameter<[String: String]>.value(`fields`), Parameter.value(`isSocial`))) + let perform = methodPerformValue(.m_registerUser__fields_fieldsisSocial_isSocial(Parameter<[String: String]>.value(`fields`), Parameter.value(`isSocial`))) as? ([String: String], Bool) -> Void + perform?(`fields`, `isSocial`) var __value: User do { - __value = try methodReturnValue(.m_registerUser__fields_fields(Parameter<[String: String]>.value(`fields`))).casted() + __value = try methodReturnValue(.m_registerUser__fields_fieldsisSocial_isSocial(Parameter<[String: String]>.value(`fields`), Parameter.value(`isSocial`))).casted() } catch MockError.notStubed { - onFatalFailure("Stub return value not specified for registerUser(fields: [String: String]). Use given") - Failure("Stub return value not specified for registerUser(fields: [String: String]). Use given") + onFatalFailure("Stub return value not specified for registerUser(fields: [String: String], isSocial: Bool). Use given") + Failure("Stub return value not specified for registerUser(fields: [String: String], isSocial: Bool). Use given") } catch { throw error } @@ -156,10 +173,11 @@ open class AuthInteractorProtocolMock: AuthInteractorProtocol, Mock { fileprivate enum MethodType { case m_login__username_usernamepassword_password(Parameter, Parameter) + case m_login__externalToken_externalTokenbackend_backend(Parameter, Parameter) case m_resetPassword__email_email(Parameter) case m_getCookies__force_force(Parameter) case m_getRegistrationFields - case m_registerUser__fields_fields(Parameter<[String: String]>) + case m_registerUser__fields_fieldsisSocial_isSocial(Parameter<[String: String]>, Parameter) case m_validateRegistrationFields__fields_fields(Parameter<[String: String]>) static func compareParameters(lhs: MethodType, rhs: MethodType, matcher: Matcher) -> Matcher.ComparisonResult { @@ -170,6 +188,12 @@ 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__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")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBackend, rhs: rhsBackend, with: matcher), lhsBackend, rhsBackend, "backend")) + return Matcher.ComparisonResult(results) + case (.m_resetPassword__email_email(let lhsEmail), .m_resetPassword__email_email(let rhsEmail)): var results: [Matcher.ParameterComparisonResult] = [] results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsEmail, rhs: rhsEmail, with: matcher), lhsEmail, rhsEmail, "email")) @@ -182,9 +206,10 @@ open class AuthInteractorProtocolMock: AuthInteractorProtocol, Mock { case (.m_getRegistrationFields, .m_getRegistrationFields): return .match - case (.m_registerUser__fields_fields(let lhsFields), .m_registerUser__fields_fields(let rhsFields)): + case (.m_registerUser__fields_fieldsisSocial_isSocial(let lhsFields, let lhsIssocial), .m_registerUser__fields_fieldsisSocial_isSocial(let rhsFields, let rhsIssocial)): var results: [Matcher.ParameterComparisonResult] = [] results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsFields, rhs: rhsFields, with: matcher), lhsFields, rhsFields, "fields")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsIssocial, rhs: rhsIssocial, with: matcher), lhsIssocial, rhsIssocial, "isSocial")) return Matcher.ComparisonResult(results) case (.m_validateRegistrationFields__fields_fields(let lhsFields), .m_validateRegistrationFields__fields_fields(let rhsFields)): @@ -198,20 +223,22 @@ 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__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 case .m_getRegistrationFields: return 0 - case let .m_registerUser__fields_fields(p0): return p0.intValue + case let .m_registerUser__fields_fieldsisSocial_isSocial(p0, p1): return p0.intValue + p1.intValue case let .m_validateRegistrationFields__fields_fields(p0): return p0.intValue } } func assertionName() -> String { switch self { case .m_login__username_usernamepassword_password: return ".login(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:)" case .m_getRegistrationFields: return ".getRegistrationFields()" - case .m_registerUser__fields_fields: return ".registerUser(fields:)" + case .m_registerUser__fields_fieldsisSocial_isSocial: return ".registerUser(fields:isSocial:)" case .m_validateRegistrationFields__fields_fields: return ".validateRegistrationFields(fields:)" } } @@ -230,14 +257,18 @@ open class AuthInteractorProtocolMock: AuthInteractorProtocol, Mock { public static func login(username: Parameter, password: Parameter, willReturn: User...) -> MethodStub { return Given(method: .m_login__username_usernamepassword_password(`username`, `password`), products: willReturn.map({ StubProduct.return($0 as Any) })) } + @discardableResult + 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) })) + } 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) })) } public static func getRegistrationFields(willReturn: [PickerFields]...) -> MethodStub { return Given(method: .m_getRegistrationFields, products: willReturn.map({ StubProduct.return($0 as Any) })) } - public static func registerUser(fields: Parameter<[String: String]>, willReturn: User...) -> MethodStub { - return Given(method: .m_registerUser__fields_fields(`fields`), products: willReturn.map({ StubProduct.return($0 as Any) })) + public static func registerUser(fields: Parameter<[String: String]>, isSocial: Parameter, willReturn: User...) -> MethodStub { + return Given(method: .m_registerUser__fields_fieldsisSocial_isSocial(`fields`, `isSocial`), products: willReturn.map({ StubProduct.return($0 as Any) })) } public static func validateRegistrationFields(fields: Parameter<[String: String]>, willReturn: [String: String]...) -> MethodStub { return Given(method: .m_validateRegistrationFields__fields_fields(`fields`), products: willReturn.map({ StubProduct.return($0 as Any) })) @@ -254,6 +285,18 @@ open class AuthInteractorProtocolMock: AuthInteractorProtocol, Mock { willProduce(stubber) return given } + @discardableResult + public static func login(externalToken: Parameter, backend: Parameter, willThrow: Error...) -> MethodStub { + return Given(method: .m_login__externalToken_externalTokenbackend_backend(`externalToken`, `backend`), products: willThrow.map({ StubProduct.throw($0) })) + } + @discardableResult + public static func login(externalToken: Parameter, backend: Parameter, willProduce: (StubberThrows) -> Void) -> MethodStub { + let willThrow: [Error] = [] + let given: Given = { return Given(method: .m_login__externalToken_externalTokenbackend_backend(`externalToken`, `backend`), products: willThrow.map({ StubProduct.throw($0) })) }() + let stubber = given.stubThrows(for: (User).self) + willProduce(stubber) + return given + } public static func resetPassword(email: Parameter, willThrow: Error...) -> MethodStub { return Given(method: .m_resetPassword__email_email(`email`), products: willThrow.map({ StubProduct.throw($0) })) } @@ -284,12 +327,12 @@ open class AuthInteractorProtocolMock: AuthInteractorProtocol, Mock { willProduce(stubber) return given } - public static func registerUser(fields: Parameter<[String: String]>, willThrow: Error...) -> MethodStub { - return Given(method: .m_registerUser__fields_fields(`fields`), products: willThrow.map({ StubProduct.throw($0) })) + public static func registerUser(fields: Parameter<[String: String]>, isSocial: Parameter, willThrow: Error...) -> MethodStub { + return Given(method: .m_registerUser__fields_fieldsisSocial_isSocial(`fields`, `isSocial`), products: willThrow.map({ StubProduct.throw($0) })) } - public static func registerUser(fields: Parameter<[String: String]>, willProduce: (StubberThrows) -> Void) -> MethodStub { + public static func registerUser(fields: Parameter<[String: String]>, isSocial: Parameter, willProduce: (StubberThrows) -> Void) -> MethodStub { let willThrow: [Error] = [] - let given: Given = { return Given(method: .m_registerUser__fields_fields(`fields`), products: willThrow.map({ StubProduct.throw($0) })) }() + let given: Given = { return Given(method: .m_registerUser__fields_fieldsisSocial_isSocial(`fields`, `isSocial`), products: willThrow.map({ StubProduct.throw($0) })) }() let stubber = given.stubThrows(for: (User).self) willProduce(stubber) return given @@ -311,10 +354,12 @@ open class AuthInteractorProtocolMock: AuthInteractorProtocol, Mock { @discardableResult public static func login(username: Parameter, password: Parameter) -> Verify { return Verify(method: .m_login__username_usernamepassword_password(`username`, `password`))} + @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`))} public static func getCookies(force: Parameter) -> Verify { return Verify(method: .m_getCookies__force_force(`force`))} public static func getRegistrationFields() -> Verify { return Verify(method: .m_getRegistrationFields)} - public static func registerUser(fields: Parameter<[String: String]>) -> Verify { return Verify(method: .m_registerUser__fields_fields(`fields`))} + public static func registerUser(fields: Parameter<[String: String]>, isSocial: Parameter) -> Verify { return Verify(method: .m_registerUser__fields_fieldsisSocial_isSocial(`fields`, `isSocial`))} public static func validateRegistrationFields(fields: Parameter<[String: String]>) -> Verify { return Verify(method: .m_validateRegistrationFields__fields_fields(`fields`))} } @@ -326,6 +371,10 @@ open class AuthInteractorProtocolMock: AuthInteractorProtocol, Mock { public static func login(username: Parameter, password: Parameter, perform: @escaping (String, String) -> Void) -> Perform { return Perform(method: .m_login__username_usernamepassword_password(`username`, `password`), performs: perform) } + @discardableResult + public static func login(externalToken: Parameter, backend: Parameter, perform: @escaping (String, String) -> Void) -> Perform { + return Perform(method: .m_login__externalToken_externalTokenbackend_backend(`externalToken`, `backend`), performs: perform) + } public static func resetPassword(email: Parameter, perform: @escaping (String) -> Void) -> Perform { return Perform(method: .m_resetPassword__email_email(`email`), performs: perform) } @@ -335,8 +384,8 @@ open class AuthInteractorProtocolMock: AuthInteractorProtocol, Mock { public static func getRegistrationFields(perform: @escaping () -> Void) -> Perform { return Perform(method: .m_getRegistrationFields, performs: perform) } - public static func registerUser(fields: Parameter<[String: String]>, perform: @escaping ([String: String]) -> Void) -> Perform { - return Perform(method: .m_registerUser__fields_fields(`fields`), performs: perform) + public static func registerUser(fields: Parameter<[String: String]>, isSocial: Parameter, perform: @escaping ([String: String], Bool) -> Void) -> Perform { + return Perform(method: .m_registerUser__fields_fieldsisSocial_isSocial(`fields`, `isSocial`), performs: perform) } public static func validateRegistrationFields(fields: Parameter<[String: String]>, perform: @escaping ([String: String]) -> Void) -> Perform { return Perform(method: .m_validateRegistrationFields__fields_fields(`fields`), performs: perform)