From 0e0a518b8634eac9f49fb333902cd124280bad41 Mon Sep 17 00:00:00 2001 From: Fox Danger Piacenti Date: Wed, 16 Aug 2023 15:38:14 -0500 Subject: [PATCH] feat: Support web-based LMS OAuth. --- .../Presentation/AuthorizationAnalytics.swift | 3 + .../Presentation/Login/SignInView.swift | 303 ++++++++++-------- .../Presentation/Login/SignInViewModel.swift | 77 +++++ Core/Core/Configuration/Config/Config.swift | 6 + .../Core/Data/Repository/AuthRepository.swift | 18 ++ Core/Core/Domain/AuthInteractor.swift | 7 + Core/Core/SwiftGen/Assets.swift | 1 + OpenEdX/AppDelegate.swift | 5 + OpenEdX/Info.plist | 13 + OpenEdX/RouteController.swift | 10 +- OpenEdX/Router.swift | 10 +- Podfile | 1 + Podfile.lock | 70 ++-- 13 files changed, 350 insertions(+), 174 deletions(-) diff --git a/Authorization/Authorization/Presentation/AuthorizationAnalytics.swift b/Authorization/Authorization/Presentation/AuthorizationAnalytics.swift index d73799435..2d7f580c3 100644 --- a/Authorization/Authorization/Presentation/AuthorizationAnalytics.swift +++ b/Authorization/Authorization/Presentation/AuthorizationAnalytics.swift @@ -10,6 +10,7 @@ import Foundation public enum AuthMethod: Equatable { case password case socailAuth(SocialAuthMethod) + case webAuth public var analyticsValue: String { switch self { @@ -17,6 +18,8 @@ public enum AuthMethod: Equatable { "Password" case .socailAuth(let socialAuthMethod): socialAuthMethod.rawValue + case .webAuth: + "WebAuth View" } } } diff --git a/Authorization/Authorization/Presentation/Login/SignInView.swift b/Authorization/Authorization/Presentation/Login/SignInView.swift index 0fece9b53..fa634885f 100644 --- a/Authorization/Authorization/Presentation/Login/SignInView.swift +++ b/Authorization/Authorization/Presentation/Login/SignInView.swift @@ -19,163 +19,190 @@ public struct SignInView: View { @ObservedObject private var viewModel: SignInViewModel + private var navigationController: UINavigationController - public init(viewModel: SignInViewModel) { + public init(viewModel: SignInViewModel, navigationController: UINavigationController) { self.viewModel = viewModel + self.navigationController = navigationController + } + + public func webLogin() async { + await viewModel.login(viewController: self.navigationController) } public var body: some View { - ZStack(alignment: .top) { - VStack { - ThemeAssets.authBackground.swiftUIImage - .resizable() - .edgesIgnoringSafeArea(.top) - }.frame(maxWidth: .infinity, maxHeight: 200) - if viewModel.config.features.startupScreenEnabled { - VStack { - Button(action: { viewModel.router.back() }, label: { - CoreAssets.arrowLeft.swiftUIImage.renderingMode(.template) - .backButtonStyle(color: .white) - }) - .foregroundColor(Theme.Colors.styledButtonText) - .padding(.leading, isHorizontal ? 48 : 0) - .padding(.top, 11) - - }.frame(maxWidth: .infinity, alignment: .topLeading) - .padding(.top, isHorizontal ? 20 : 0) + if viewModel.forceWebLogin { + Task { + await webLogin() } - - VStack(alignment: .center) { - ThemeAssets.appLogo.swiftUIImage - .resizable() - .frame(maxWidth: 189, maxHeight: 54) - .padding(.top, isHorizontal ? 20 : 40) - .padding(.bottom, isHorizontal ? 10 : 40) + } + return ZStack(alignment: .top) { + if viewModel.forceWebLogin { + // Is there an idiomatic way of doing an early return here + // rather than using this big indented else clause? + // Using a return statement seems to break whatever magic + // happens here. + } else { + VStack { + ThemeAssets.authBackground.swiftUIImage + .resizable() + .edgesIgnoringSafeArea(.top) + }.frame(maxWidth: .infinity, maxHeight: 200) - ScrollView { + if viewModel.config.features.startupScreenEnabled { VStack { - VStack(alignment: .leading) { - Text(AuthLocalization.SignIn.logInTitle) - .font(Theme.Fonts.displaySmall) - .foregroundColor(Theme.Colors.textPrimary) - .padding(.bottom, 4) - Text(AuthLocalization.SignIn.welcomeBack) - .font(Theme.Fonts.titleSmall) - .foregroundColor(Theme.Colors.textPrimary) - .padding(.bottom, 20) - - Text(AuthLocalization.SignIn.emailOrUsername) - .font(Theme.Fonts.labelLarge) - .foregroundColor(Theme.Colors.textPrimary) - TextField(AuthLocalization.SignIn.emailOrUsername, text: $email) - .keyboardType(.emailAddress) - .textContentType(.emailAddress) - .autocapitalization(.none) - .autocorrectionDisabled() - .padding(.all, 14) - .background( - Theme.Shapes.textInputShape - .fill(Theme.Colors.textInputBackground) - ) - .overlay( - Theme.Shapes.textInputShape - .stroke(lineWidth: 1) - .fill(Theme.Colors.textInputStroke) - ) - - Text(AuthLocalization.SignIn.password) - .font(Theme.Fonts.labelLarge) - .foregroundColor(Theme.Colors.textPrimary) - .padding(.top, 18) - SecureField(AuthLocalization.SignIn.password, text: $password) - .padding(.all, 14) - .background( - Theme.Shapes.textInputShape - .fill(Theme.Colors.textInputBackground) - ) - .overlay( - Theme.Shapes.textInputShape - .stroke(lineWidth: 1) - .fill(Theme.Colors.textInputStroke) - ) - HStack { - if !viewModel.config.features.startupScreenEnabled { - Button(AuthLocalization.SignIn.registerBtn) { - viewModel.trackSignUpClicked() - viewModel.router.showRegisterScreen() - }.foregroundColor(Theme.Colors.accentColor) + Button(action: { viewModel.router.back() }, label: { + CoreAssets.arrowLeft.swiftUIImage.renderingMode(.template) + .backButtonStyle(color: .white) + }) + .foregroundColor(Theme.Colors.styledButtonText) + .padding(.leading, isHorizontal ? 48 : 0) + .padding(.top, 11) + + }.frame(maxWidth: .infinity, alignment: .topLeading) + .padding(.top, isHorizontal ? 20 : 0) + } + + VStack(alignment: .center) { + ThemeAssets.appLogo.swiftUIImage + .resizable() + .frame(maxWidth: 189, maxHeight: 54) + .padding(.top, isHorizontal ? 20 : 40) + .padding(.bottom, isHorizontal ? 10 : 40) + + ScrollView { + VStack { + VStack(alignment: .leading) { + Text(AuthLocalization.SignIn.logInTitle) + .font(Theme.Fonts.displaySmall) + .foregroundColor(Theme.Colors.textPrimary) + .padding(.bottom, 4) + Text(AuthLocalization.SignIn.welcomeBack) + .font(Theme.Fonts.titleSmall) + .foregroundColor(Theme.Colors.textPrimary) + .padding(.bottom, 20) + if viewModel.useWebLogin { + StyledButton(AuthLocalization.SignIn.logInBtn) { + Task { + await webLogin() + } + }.frame(maxWidth: .infinity) + .padding(.top, 40) + } else { + Text(AuthLocalization.SignIn.emailOrUsername) + .font(Theme.Fonts.labelLarge) + .foregroundColor(Theme.Colors.textPrimary) + TextField(AuthLocalization.SignIn.emailOrUsername, text: $email) + .keyboardType(.emailAddress) + .textContentType(.emailAddress) + .autocapitalization(.none) + .autocorrectionDisabled() + .padding(.all, 14) + .background( + Theme.Shapes.textInputShape + .fill(Theme.Colors.textInputBackground) + ) + .overlay( + Theme.Shapes.textInputShape + .stroke(lineWidth: 1) + .fill(Theme.Colors.textInputStroke) + ) - Spacer() + Text(AuthLocalization.SignIn.password) + .font(Theme.Fonts.labelLarge) + .foregroundColor(Theme.Colors.textPrimary) + .padding(.top, 18) + SecureField(AuthLocalization.SignIn.password, text: $password) + .padding(.all, 14) + .background( + Theme.Shapes.textInputShape + .fill(Theme.Colors.textInputBackground) + ) + .overlay( + Theme.Shapes.textInputShape + .stroke(lineWidth: 1) + .fill(Theme.Colors.textInputStroke) + ) + HStack { + if !viewModel.config.features.startupScreenEnabled { + Button(AuthLocalization.SignIn.registerBtn) { + viewModel.trackSignUpClicked() + viewModel.router.showRegisterScreen() + }.foregroundColor(Theme.Colors.accentColor) + + Spacer() + } + + Button(AuthLocalization.SignIn.forgotPassBtn) { + viewModel.trackForgotPasswordClicked() + viewModel.router.showForgotPasswordScreen() + }.foregroundColor(Theme.Colors.accentColor) + .padding(.top, 0) + } + } + + if viewModel.isShowProgress { + HStack(alignment: .center) { + ProgressBar(size: 40, lineWidth: 8) + .padding(20) + }.frame(maxWidth: .infinity) + } else if !viewModel.useWebLogin { + StyledButton(AuthLocalization.SignIn.logInBtn) { + Task { + await viewModel.login(username: email, password: password) + } + }.frame(maxWidth: .infinity) + .padding(.top, 40) } - - Button(AuthLocalization.SignIn.forgotPassBtn) { - viewModel.trackForgotPasswordClicked() - viewModel.router.showForgotPasswordScreen() - }.foregroundColor(Theme.Colors.accentColor) - .padding(.top, 0) } - - if viewModel.isShowProgress { - HStack(alignment: .center) { - ProgressBar(size: 40, lineWidth: 8) - .padding(20) - }.frame(maxWidth: .infinity) - } else { - StyledButton(AuthLocalization.SignIn.logInBtn) { - Task { - await viewModel.login(username: email, password: password) + if viewModel.socialAuthEnabled { + SocialAuthView( + viewModel: .init( + config: viewModel.config + ) { result in + Task { await viewModel.login(with: result) } } - }.frame(maxWidth: .infinity) - .padding(.top, 40) + ) } + Spacer() } - if viewModel.socialAuthEnabled { - SocialAuthView( - viewModel: .init( - config: viewModel.config - ) { result in - Task { await viewModel.login(with: result) } - } - ) - } - Spacer() - } - .padding(.horizontal, 24) - .padding(.top, 50) - }.roundedBackground(Theme.Colors.background) - .scrollAvoidKeyboard(dismissKeyboardByTap: true) - - } - - // MARK: - Alert - if viewModel.showAlert { - VStack { - Text(viewModel.alertMessage ?? "") - .shadowCardStyle(bgColor: Theme.Colors.accentColor, - textColor: Theme.Colors.white) - .padding(.top, 80) - Spacer() + .padding(.horizontal, 24) + .padding(.top, 50) + }.roundedBackground(Theme.Colors.background) + .scrollAvoidKeyboard(dismissKeyboardByTap: true) } - .transition(.move(edge: .top)) - .onAppear { - doAfter(Theme.Timeout.snackbarMessageLongTimeout) { - viewModel.alertMessage = nil + + // MARK: - Alert + if viewModel.showAlert { + VStack { + Text(viewModel.alertMessage ?? "") + .shadowCardStyle(bgColor: Theme.Colors.accentColor, + textColor: Theme.Colors.white) + .padding(.top, 80) + Spacer() + } - } - } - - // MARK: - Show error - if viewModel.showError { - VStack { - Spacer() - SnackBarView(message: viewModel.errorMessage) - }.transition(.move(edge: .bottom)) + .transition(.move(edge: .top)) .onAppear { doAfter(Theme.Timeout.snackbarMessageLongTimeout) { - viewModel.errorMessage = nil + viewModel.alertMessage = nil } } + } + + // MARK: - Show error + if viewModel.showError { + VStack { + Spacer() + SnackBarView(message: viewModel.errorMessage) + }.transition(.move(edge: .bottom)) + .onAppear { + doAfter(Theme.Timeout.snackbarMessageLongTimeout) { + viewModel.errorMessage = nil + } + } + } } } .hideNavigationBar() @@ -195,12 +222,12 @@ struct SignInView_Previews: PreviewProvider { validator: Validator() ) - SignInView(viewModel: vm) + SignInView(viewModel: vm, navigationController: UINavigationController()) .preferredColorScheme(.light) .previewDisplayName("SignInView Light") .loadFonts() - SignInView(viewModel: vm) + SignInView(viewModel: vm, navigationController: UINavigationController()) .preferredColorScheme(.dark) .previewDisplayName("SignInView Dark") .loadFonts() diff --git a/Authorization/Authorization/Presentation/Login/SignInViewModel.swift b/Authorization/Authorization/Presentation/Login/SignInViewModel.swift index a2439a10b..aae5f97fc 100644 --- a/Authorization/Authorization/Presentation/Login/SignInViewModel.swift +++ b/Authorization/Authorization/Presentation/Login/SignInViewModel.swift @@ -13,12 +13,36 @@ import AuthenticationServices import FacebookLogin import GoogleSignIn import MSAL +import OAuthSwift +import SafariServices + +private class WebLoginSafariDelegate: NSObject, SFSafariViewControllerDelegate { + private let viewModel: SignInViewModel + public init(viewModel: SignInViewModel) { + self.viewModel = viewModel + } + func safariViewControllerDidFinish(_ controller: SFSafariViewController) { + /* Called when the 'Done' button is hit on the Safari Web view. In this case, + authentication would neither have failed nor succeeded, but we'd be back + at the SignInView. So, we make sure we mark it as attempted so the UI + renders. */ + self.viewModel.markAttempted() + } +} public class SignInViewModel: ObservableObject { @Published private(set) var isShowProgress = false @Published private(set) var showError: Bool = false @Published private(set) var showAlert: Bool = false + @Published private(set) var webLoginAttempted: Bool = false + + var useWebLogin: Bool { + return config.webLogin + } + var forceWebLogin: Bool { + return config.webLogin && !webLoginAttempted + } var errorMessage: String? { didSet { withAnimation { @@ -33,6 +57,7 @@ public class SignInViewModel: ObservableObject { } } } + var oauthswift: OAuth2Swift? let router: AuthorizationRouter let config: ConfigProtocol @@ -40,6 +65,8 @@ public class SignInViewModel: ObservableObject { private let analytics: AuthorizationAnalytics private let validator: Validator + private var safariDelegate: WebLoginSafariDelegate? + public init( interactor: AuthInteractorProtocol, router: AuthorizationRouter, @@ -61,6 +88,56 @@ public class SignInViewModel: ObservableObject { config.google.enabled } + @MainActor + func login(viewController: UIViewController) async { + /* OAuth web login. Used when we cannot use the built-in login form, + but need to let the LMS redirect us to the authentication provider. + + An example service where this is needed is something like Auth0, which + redirects from the LMS to its own login page. That login page then redirects + back to the LMS for the issuance of a token that can be used for making + requests to the LMS, and then back to the redirect URL for the app. */ + self.safariDelegate = WebLoginSafariDelegate(viewModel: self) + oauthswift = OAuth2Swift( + consumerKey: config.oAuthClientId, + consumerSecret: "", // No secret required + authorizeUrl: "\(config.baseURL)/oauth2/authorize/", + accessTokenUrl: "\(config.baseURL)/oauth2/access_token/", + responseType: "code" + ) + + oauthswift!.allowMissingStateCheck = true + let handler = SafariURLHandler( + viewController: viewController, oauthSwift: oauthswift! + ) + handler.delegate = self.safariDelegate + oauthswift!.authorizeURLHandler = handler + + // Trigger OAuth2 dance + guard let rwURL = URL(string: "\(Bundle.main.bundleIdentifier ?? "")://oauth2Callback") else { return } + oauthswift!.authorize(withCallbackURL: rwURL, scope: "", state: "") { result in + switch result { + case .success(let thing): + Task { + self.webLoginAttempted = true + let user = try await self.interactor.login(credential: thing.credential) + self.analytics.setUserID("\(user.id)") + self.analytics.userLogin(method: .webAuth) + self.router.showMainOrWhatsNewScreen() + } + // Do your request + case .failure(let error): + self.webLoginAttempted = true + self.isShowProgress = false + self.errorMessage = error.localizedDescription + } + } + } + + public func markAttempted() { + self.webLoginAttempted = true + } + @MainActor func login(username: String, password: String) async { guard validator.isValidUsername(username) else { diff --git a/Core/Core/Configuration/Config/Config.swift b/Core/Core/Configuration/Config/Config.swift index a10438eb6..724f7c936 100644 --- a/Core/Core/Configuration/Config/Config.swift +++ b/Core/Core/Configuration/Config/Config.swift @@ -11,6 +11,7 @@ public protocol ConfigProtocol { var baseURL: URL { get } var oAuthClientId: String { get } var tokenType: TokenType { get } + var webLogin: Bool { get } var feedbackEmail: String { get } var appStoreLink: String { get } var platformName: String { get } @@ -33,6 +34,7 @@ private enum ConfigKeys: String { case baseURL = "API_HOST_URL" case oAuthClientID = "OAUTH_CLIENT_ID" case tokenType = "TOKEN_TYPE" + case webLogin = "WEB_LOGIN" case feedbackEmailAddress = "FEEDBACK_EMAIL_ADDRESS" case environmentDisplayName = "ENVIRONMENT_DISPLAY_NAME" case platformName = "PLATFORM_NAME" @@ -122,6 +124,10 @@ extension Config: ConfigProtocol { return tokenType } + public var webLogin: Bool { + return bool(for: ConfigKeys.webLogin.rawValue) + } + public var feedbackEmail: String { return string(for: ConfigKeys.feedbackEmailAddress.rawValue) ?? "" } diff --git a/Core/Core/Data/Repository/AuthRepository.swift b/Core/Core/Data/Repository/AuthRepository.swift index e4adf93ea..e0be352ef 100644 --- a/Core/Core/Data/Repository/AuthRepository.swift +++ b/Core/Core/Data/Repository/AuthRepository.swift @@ -6,8 +6,10 @@ // import Foundation +import OAuthSwift public protocol AuthRepositoryProtocol { + func login(credential: OAuthSwiftCredential) async throws -> User func login(username: String, password: String) async throws -> User func login(externalToken: String, backend: String) async throws -> User func getCookies(force: Bool) async throws @@ -29,6 +31,17 @@ public class AuthRepository: AuthRepositoryProtocol { self.config = config } + public func login(credential: OAuthSwiftCredential) async throws -> User { + // Login for when we have the accessToken and refreshToken directly, like from web-view + // OAuth logins. + appStorage.cookiesDate = nil + appStorage.accessToken = credential.oauthToken + appStorage.refreshToken = credential.oauthRefreshToken + let user = try await api.requestData(AuthEndpoint.getUserInfo).mapResponse(DataLayer.User.self) + appStorage.user = user + return user.domain + } + public func login(username: String, password: String) async throws -> User { appStorage.cookiesDate = nil let endPoint = AuthEndpoint.getAccessToken( @@ -130,6 +143,11 @@ public class AuthRepository: AuthRepositoryProtocol { // Mark - For testing and SwiftUI preview #if DEBUG class AuthRepositoryMock: AuthRepositoryProtocol { + + func login(credential: OAuthSwiftCredential) async throws -> User { + User(id: 1, username: "User", email: "email@gmail.com", name: "User Name", userAvatar: "") + } + func login(username: String, password: String) 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 45868cbc9..1695ae024 100644 --- a/Core/Core/Domain/AuthInteractor.swift +++ b/Core/Core/Domain/AuthInteractor.swift @@ -6,10 +6,12 @@ // import Foundation +import OAuthSwift //sourcery: AutoMockable public protocol AuthInteractorProtocol { @discardableResult + func login(credential: OAuthSwiftCredential) async throws -> User func login(username: String, password: String) async throws -> User @discardableResult func login(externalToken: String, backend: String) async throws -> User @@ -27,6 +29,11 @@ public class AuthInteractor: AuthInteractorProtocol { self.repository = repository } + @discardableResult + public func login(credential: OAuthSwiftCredential) async throws -> User { + return try await repository.login(credential: credential) + } + @discardableResult public func login(username: String, password: String) async throws -> User { return try await repository.login(username: username, password: password) diff --git a/Core/Core/SwiftGen/Assets.swift b/Core/Core/SwiftGen/Assets.swift index 202ab2639..6c8074470 100644 --- a/Core/Core/SwiftGen/Assets.swift +++ b/Core/Core/SwiftGen/Assets.swift @@ -81,6 +81,7 @@ public enum CoreAssets { public static let rotateDevice = ImageAsset(name: "rotateDevice") public static let sub = ImageAsset(name: "sub") public static let alarm = ImageAsset(name: "alarm") + public static let appLogo = ImageAsset(name: "appLogo") public static let arrowLeft = ImageAsset(name: "arrowLeft") public static let arrowRight16 = ImageAsset(name: "arrowRight16") public static let certificate = ImageAsset(name: "certificate") diff --git a/OpenEdX/AppDelegate.swift b/OpenEdX/AppDelegate.swift index 563d587f1..008e81af9 100644 --- a/OpenEdX/AppDelegate.swift +++ b/OpenEdX/AppDelegate.swift @@ -16,6 +16,7 @@ import GoogleSignIn import FacebookCore import MSAL import Theme +import OAuthSwift @UIApplicationMain class AppDelegate: UIResponder, UIApplicationDelegate { @@ -69,6 +70,10 @@ class AppDelegate: UIResponder, UIApplicationDelegate { _ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any] = [:] ) -> Bool { + if url.host == "oauth2Callback" { + // TODO: Update to better match the other OAuth redirect behaviors + OAuthSwift.handle(url: url) + } if let config = Container.shared.resolve(ConfigProtocol.self) { if config.facebook.enabled { ApplicationDelegate.shared.application( diff --git a/OpenEdX/Info.plist b/OpenEdX/Info.plist index e4bdcba95..82e6fd5a4 100644 --- a/OpenEdX/Info.plist +++ b/OpenEdX/Info.plist @@ -2,6 +2,19 @@ + CFBundleURLTypes + + + CFBundleTypeRole + Editor + CFBundleURLName + + CFBundleURLSchemes + + ${PRODUCT_BUNDLE_IDENTIFIER} + + + Configuration $(CONFIGURATION) FirebaseAppDelegateProxyEnabled diff --git a/OpenEdX/RouteController.swift b/OpenEdX/RouteController.swift index d86e7bb6d..0f4aaad5e 100644 --- a/OpenEdX/RouteController.swift +++ b/OpenEdX/RouteController.swift @@ -49,12 +49,20 @@ class RouteController: UIViewController { present(navigation, animated: false) } else { let controller = UIHostingController( - rootView: SignInView(viewModel: diContainer.resolve(SignInViewModel.self)!) + rootView: SignInView(viewModel: diContainer.resolve(SignInViewModel.self)!, navigationController: self.navigation) ) navigation.viewControllers = [controller] present(navigation, animated: false) } } + + private func showAuthorization() { + let controller = UIHostingController( + rootView: SignInView(viewModel: diContainer.resolve(SignInViewModel.self)!, navigationController: self.navigation) + ) + navigation.viewControllers = [controller] + present(navigation, animated: false) + } private func showMainOrWhatsNewScreen() { var storage = Container.shared.resolve(WhatsNewStorage.self)! diff --git a/OpenEdX/Router.swift b/OpenEdX/Router.swift index a04b4c8fd..20fe55449 100644 --- a/OpenEdX/Router.swift +++ b/OpenEdX/Router.swift @@ -83,7 +83,10 @@ public class Router: AuthorizationRouter, } public func showLoginScreen() { - let view = SignInView(viewModel: Container.shared.resolve(SignInViewModel.self)!) + let view = SignInView( + viewModel: Container.shared.resolve(SignInViewModel.self)!, + navigationController: self.navigationController + ) let controller = UIHostingController(rootView: view) navigationController.pushViewController(controller, animated: true) } @@ -94,7 +97,10 @@ public class Router: AuthorizationRouter, let controller = UIHostingController(rootView: view) navigationController.setViewControllers([controller], animated: true) } else { - let view = SignInView(viewModel: Container.shared.resolve(SignInViewModel.self)!) + let view = SignInView( + viewModel: Container.shared.resolve(SignInViewModel.self)!, + navigationController: navigationController + ) let controller = UIHostingController(rootView: view) navigationController.setViewControllers([controller], animated: false) } diff --git a/Podfile b/Podfile index 291bcb28e..2c97ed9b3 100644 --- a/Podfile +++ b/Podfile @@ -28,6 +28,7 @@ abstract_target "App" do pod 'SwiftUIIntrospect', '~> 0.8' pod 'Kingfisher', '~> 7.8' pod 'Swinject', '2.8.3' + pod 'OAuthSwift', '~> 2.2.0' end target "Authorization" do diff --git a/Podfile.lock b/Podfile.lock index d9169ef4b..2908f2fa9 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -1,7 +1,7 @@ PODS: - - Alamofire (5.8.0) - - FirebaseAnalytics (10.15.0): - - FirebaseAnalytics/AdIdSupport (= 10.15.0) + - Alamofire (5.7.1) + - FirebaseAnalytics (10.13.0): + - FirebaseAnalytics/AdIdSupport (= 10.13.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.15.0): + - FirebaseAnalytics/AdIdSupport (10.13.0): - FirebaseCore (~> 10.0) - FirebaseInstallations (~> 10.0) - - GoogleAppMeasurement (= 10.15.0) + - GoogleAppMeasurement (= 10.13.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.15.0): + - FirebaseCore (10.13.0): - FirebaseCoreInternal (~> 10.0) - GoogleUtilities/Environment (~> 7.8) - GoogleUtilities/Logger (~> 7.8) - - FirebaseCoreExtension (10.15.0): + - FirebaseCoreExtension (10.13.0): - FirebaseCore (~> 10.0) - - FirebaseCoreInternal (10.15.0): + - FirebaseCoreInternal (10.13.0): - "GoogleUtilities/NSData+zlib (~> 7.8)" - - FirebaseCrashlytics (10.15.0): + - FirebaseCrashlytics (10.13.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.15.0): + - FirebaseInstallations (10.13.0): - FirebaseCore (~> 10.0) - GoogleUtilities/Environment (~> 7.8) - GoogleUtilities/UserDefaults (~> 7.8) - PromisesObjC (~> 2.1) - - FirebaseSessions (10.15.0): + - FirebaseSessions (10.13.0): - FirebaseCore (~> 10.5) - FirebaseCoreExtension (~> 10.0) - FirebaseInstallations (~> 10.0) @@ -47,21 +47,21 @@ PODS: - GoogleUtilities/Environment (~> 7.10) - nanopb (< 2.30910.0, >= 2.30908.0) - PromisesSwift (~> 2.1) - - GoogleAppMeasurement (10.15.0): - - GoogleAppMeasurement/AdIdSupport (= 10.15.0) + - GoogleAppMeasurement (10.13.0): + - GoogleAppMeasurement/AdIdSupport (= 10.13.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.15.0): - - GoogleAppMeasurement/WithoutAdIdSupport (= 10.15.0) + - GoogleAppMeasurement/AdIdSupport (10.13.0): + - GoogleAppMeasurement/WithoutAdIdSupport (= 10.13.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.15.0): + - GoogleAppMeasurement/WithoutAdIdSupport (10.13.0): - GoogleUtilities/AppDelegateSwizzler (~> 7.11) - GoogleUtilities/MethodSwizzler (~> 7.11) - GoogleUtilities/Network (~> 7.11) @@ -91,12 +91,13 @@ PODS: - GoogleUtilities/UserDefaults (7.11.5): - GoogleUtilities/Logger - KeychainSwift (20.0.0) - - Kingfisher (7.9.1) + - Kingfisher (7.9.0) - nanopb (2.30909.0): - nanopb/decode (= 2.30909.0) - nanopb/encode (= 2.30909.0) - nanopb/decode (2.30909.0) - nanopb/encode (2.30909.0) + - OAuthSwift (2.2.0) - PromisesObjC (2.3.1) - PromisesSwift (2.3.1): - PromisesObjC (= 2.3.1) @@ -104,8 +105,8 @@ PODS: - Sourcery/CLI-Only (= 1.8.0) - Sourcery/CLI-Only (1.8.0) - SwiftGen (6.6.2) - - SwiftLint (0.53.0) - - SwiftUIIntrospect (0.12.0) + - SwiftLint (0.52.4) + - SwiftUIIntrospect (0.11.0) - SwiftyMocky (4.2.0): - Sourcery (= 1.8.0) - Swinject (2.8.3) @@ -116,6 +117,7 @@ DEPENDENCIES: - FirebaseCrashlytics (~> 10.11) - KeychainSwift (~> 20.0) - Kingfisher (~> 7.8) + - OAuthSwift (~> 2.2.0) - SwiftGen (~> 6.6) - SwiftLint (~> 0.5) - SwiftUIIntrospect (~> 0.8) @@ -138,6 +140,7 @@ SPEC REPOS: - KeychainSwift - Kingfisher - nanopb + - OAuthSwift - PromisesObjC - PromisesSwift - Sourcery @@ -157,29 +160,30 @@ CHECKOUT OPTIONS: :tag: 4.2.0 SPEC CHECKSUMS: - Alamofire: 0e92e751b3e9e66d7982db43919d01f313b8eb91 - FirebaseAnalytics: 47cef43728f81a839cf1306576bdd77ffa2eac7e - FirebaseCore: 2cec518b43635f96afe7ac3a9c513e47558abd2e - FirebaseCoreExtension: d3f1ea3725fb41f56e8fbfb29eeaff54e7ffb8f6 - FirebaseCoreInternal: 2f4bee5ed00301b5e56da0849268797a2dd31fb4 - FirebaseCrashlytics: a83f26fb922a3fe181eb738fb4dcf0c92bba6455 - FirebaseInstallations: cae95cab0f965ce05b805189de1d4c70b11c76fb - FirebaseSessions: ee59a7811bef4c15f65ef6472f3210faa293f9c8 - GoogleAppMeasurement: 722db6550d1e6d552b08398b69a975ac61039338 + Alamofire: 0123a34370cb170936ae79a8df46cc62b2edeb88 + FirebaseAnalytics: 9a12e090ead49f8877ed8132ae920e3cbbd2fcd0 + FirebaseCore: 9948a31ff2c6cf323f9b040068201a95d271b68d + FirebaseCoreExtension: ce60f9db46d83944cf444664d6d587474128eeca + FirebaseCoreInternal: b342e37cd4f5b4454ec34308f073420e7920858e + FirebaseCrashlytics: 4679fbc4768fcb4dd6f5533101841d40256d4475 + FirebaseInstallations: b28af1b9f997f1a799efe818c94695a3728c352f + FirebaseSessions: 991fb4c20b3505eef125f7cbfa20a5b5b189c2a4 + GoogleAppMeasurement: 3ae505b44174bcc0775f5c86cecc5826259fbb1e GoogleDataTransport: 54dee9d48d14580407f8f5fbf2f496e92437a2f2 GoogleUtilities: 13e2c67ede716b8741c7989e26893d151b2b2084 KeychainSwift: 0ce6a4d13f7228054d1a71bb1b500448fb2ab837 - Kingfisher: 1d14e9f59cbe19389f591c929000332bf70efd32 + Kingfisher: 59f908b6d2f403b0a3e539debb0eec05cb27002c nanopb: b552cce312b6c8484180ef47159bc0f65a1f0431 + OAuthSwift: 75efbb5bd9a4b2b71a37bd7e986bf3f55ddd54c6 PromisesObjC: c50d2056b5253dadbd6c2bea79b0674bd5a52fa4 PromisesSwift: 28dca69a9c40779916ac2d6985a0192a5cb4a265 Sourcery: 6f5fe49b82b7e02e8c65560cbd52e1be67a1af2e SwiftGen: 1366a7f71aeef49954ca5a63ba4bef6b0f24138c - SwiftLint: 5ce4d6a8ff83f1b5fd5ad5dbf30965d35af65e44 - SwiftUIIntrospect: 89f443402f701a9197e9e54e3c2ed00b10c32e6d + SwiftLint: 1cc5cd61ba9bacb2194e340aeb47a2a37fda00b3 + SwiftUIIntrospect: a45aa645cd07ac2b9045e0bfb1d16ea5dae00e67 SwiftyMocky: c5e96e4ff76ec6dbf5a5941aeb039b5a546954a0 Swinject: 893c9a543000ac2f10ee4cbaf0933c6992c935d5 -PODFILE CHECKSUM: 544edab2f9ecc4ac18973fb8865f1d0613ec8a28 +PODFILE CHECKSUM: e8de3540bc5ee1fcaaa308e04b4c76953fd839af -COCOAPODS: 1.13.0 +COCOAPODS: 1.12.1