diff --git a/Projects/CommonUI/Sources/View/Login/LoginView.swift b/Projects/CommonUI/Sources/View/Login/LoginView.swift index 9e0f36d..1be4754 100644 --- a/Projects/CommonUI/Sources/View/Login/LoginView.swift +++ b/Projects/CommonUI/Sources/View/Login/LoginView.swift @@ -12,6 +12,7 @@ import RxSwift import Then import SafariServices import RxRelay +import AuthenticationServices open class LoginView: UIView, SFSafariViewControllerDelegate { let logoLabel = UILabel().then { @@ -26,9 +27,10 @@ open class LoginView: UIView, SFSafariViewControllerDelegate { } var googleLoginButton = UIButton() - var appleLoginButton = UIButton() + var appleLoginButton = ASAuthorizationAppleIDButton(type: .default, style: .black) public let googleLoginTapped = PublishRelay() + public let appleLoginTapped = PublishRelay() let disposeBag = DisposeBag() @@ -46,6 +48,10 @@ open class LoginView: UIView, SFSafariViewControllerDelegate { googleLoginButton.rx.tap .bind(to: googleLoginTapped) .disposed(by: disposeBag) + + appleLoginButton.rx.controlEvent(.touchUpInside) + .bind(to: appleLoginTapped) + .disposed(by: disposeBag) } func initAttribute() { @@ -59,19 +65,7 @@ open class LoginView: UIView, SFSafariViewControllerDelegate { $0.backgroundColor = .white $0.layer.borderColor = CommonUIAssets.LMGray3?.cgColor $0.layer.borderWidth = 1 - $0.layer.cornerRadius = 12 - $0.titleLabel?.font = UIFont.systemFont(ofSize: 15) - $0.semanticContentAttribute = .forceLeftToRight - $0.imageEdgeInsets = UIEdgeInsets(top: 0, left: -8, bottom: 0, right: 8) - } - - appleLoginButton = appleLoginButton.then { - $0.setTitle("Apple로 시작하기", for: .normal) - $0.setTitleColor(.white, for: .normal) - $0.setImage(CommonUIAssets.apple? - .resize(to: CGSize(width: 25, height: 25)), for: .normal) - $0.backgroundColor = .black - $0.layer.cornerRadius = 12 + $0.layer.cornerRadius = 5 $0.titleLabel?.font = UIFont.systemFont(ofSize: 15) $0.semanticContentAttribute = .forceLeftToRight $0.imageEdgeInsets = UIEdgeInsets(top: 0, left: -8, bottom: 0, right: 8) diff --git a/Projects/Data/Sources/Repository/LoginRepository.swift b/Projects/Data/Sources/Repository/LoginRepository.swift index b0e8adb..d02a447 100644 --- a/Projects/Data/Sources/Repository/LoginRepository.swift +++ b/Projects/Data/Sources/Repository/LoginRepository.swift @@ -8,14 +8,14 @@ import Domain import RxSwift import Alamofire +import Foundation public class DefaultLoginRepository: LoginRepository { public init() {} public func postGoogleLogin() -> Single { return request( - endpoint: NetworkConfiguration.baseUrl, - id: 4, + endpoint: "/oauth2/authorization/google", responseType: LoginDTO.self ) .map { dto in @@ -23,17 +23,63 @@ public class DefaultLoginRepository: LoginRepository { } } - private func request(endpoint: String, id: Int, responseType: T.Type) -> Single { + public func postAppleLogin(userName: String?, identityToken: String) -> Single { + let params: Parameters = [ + "userName": userName, + "identityToken": identityToken + ] + return Single.create { single in - let url = "\(NetworkConfiguration.baseUrl)\(endpoint)" - let parameters: Parameters = [ - "id": id - ] + let url = "\(NetworkConfiguration.baseUrl)/api/auth/apple/login" + let request = AF.request(url, + method: .post, + parameters: params, + encoding: JSONEncoding.default, + headers: nil) + .redirect(using: Redirector(behavior: .doNotFollow)) + .response { response in + if let error = response.error { + single(.failure(error)) + return + } + guard let httpResponse = response.response else { + single(.failure(AFError.responseValidationFailed(reason: .dataFileNil))) + return + } + + if let location = httpResponse.allHeaderFields["Location"] as? String, + let components = URLComponents(string: location) { + let items = components.queryItems ?? [] + let accessToken = items.first(where: { $0.name == "accessToken" })?.value + single(.success(LoginVO(accessToken: accessToken))) + } else { + let error = NSError(domain: "DefaultLoginRepository", + code: -1, + userInfo: [NSLocalizedDescriptionKey: "Missing Location header or invalid redirect URL"]) + single(.failure(error)) + } + } + + return Disposables.create { request.cancel() } + } + } + + private func request( + endpoint: String, + method: HTTPMethod = .post, + parameters: Parameters? = nil, + encoding: ParameterEncoding = URLEncoding.queryString, + headers: HTTPHeaders? = nil, + responseType: T.Type + ) -> Single { + return Single.create { single in + let url = "\(NetworkConfiguration.baseUrl)\(endpoint)" let request = AF.request(url, - method: .get, + method: method, parameters: parameters, - encoding: URLEncoding.queryString) + encoding: encoding, + headers: headers) .validate() .responseDecodable(of: responseType) { response in switch response.result { diff --git a/Projects/Domain/Sources/RepositoryProtocol/LoginRepository.swift b/Projects/Domain/Sources/RepositoryProtocol/LoginRepository.swift index 7ef063d..34607d1 100644 --- a/Projects/Domain/Sources/RepositoryProtocol/LoginRepository.swift +++ b/Projects/Domain/Sources/RepositoryProtocol/LoginRepository.swift @@ -9,4 +9,5 @@ import RxSwift public protocol LoginRepository { func postGoogleLogin() -> Single + func postAppleLogin(userName: String?, identityToken: String) -> Single } diff --git a/Projects/Domain/Sources/UseCase/LoginUseCase.swift b/Projects/Domain/Sources/UseCase/LoginUseCase.swift index e9a38a7..aba0ba8 100644 --- a/Projects/Domain/Sources/UseCase/LoginUseCase.swift +++ b/Projects/Domain/Sources/UseCase/LoginUseCase.swift @@ -9,6 +9,7 @@ import RxSwift public protocol LoginUseCase { func postGoogleLogin() -> Single + func postAppleLogin(userName: String?, identityToken: String) -> Single } public final class DefaultLoginUseCase: LoginUseCase { @@ -21,4 +22,8 @@ public final class DefaultLoginUseCase: LoginUseCase { public func postGoogleLogin() -> Single { return repository.postGoogleLogin() } + + public func postAppleLogin(userName: String?, identityToken: String) -> Single { + return repository.postAppleLogin(userName: userName, identityToken: identityToken) + } } diff --git a/Projects/LearnMate/LearnMate.xcodeproj/project.pbxproj b/Projects/LearnMate/LearnMate.xcodeproj/project.pbxproj index ff55419..b7ec6c9 100644 --- a/Projects/LearnMate/LearnMate.xcodeproj/project.pbxproj +++ b/Projects/LearnMate/LearnMate.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 55; + objectVersion = 56; objects = { /* Begin PBXBuildFile section */ @@ -109,6 +109,7 @@ 7C422FE2DDF9127C2F4B2DF0 /* RxSwift.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = RxSwift.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 89F58ED76FC4C0973305766F /* DependencyInjector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DependencyInjector.swift; sourceTree = ""; }; 8A00D2BDD6B9A0B6E62CF3CC /* LearnMateTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = LearnMateTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 950A0D4D2E56D23300C07CF2 /* LearnMate.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; name = LearnMate.entitlements; path = LearnMate/LearnMate.entitlements; sourceTree = ""; }; 9ACFF0B3105009DD7646173E /* Domain.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Domain.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 9AEA88AC00680DC27F375FB6 /* HomeAssembly.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeAssembly.swift; sourceTree = ""; }; A4DC536F09A4A8284AA20CE7 /* CommonUI.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = CommonUI.framework; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -198,6 +199,7 @@ E290CD2FA6D313265F045E73 = { isa = PBXGroup; children = ( + 950A0D4D2E56D23300C07CF2 /* LearnMate.entitlements */, B074F9F8E730729626040CA5 /* Project */, ED0EE4EA4327EC4E4D07675E /* Products */, ); @@ -378,6 +380,7 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_ENTITLEMENTS = LearnMate/LearnMate.entitlements; CODE_SIGN_IDENTITY = "iPhone Developer"; ENABLE_PREVIEWS = YES; HEADER_SEARCH_PATHS = ( @@ -393,11 +396,7 @@ "$(inherited)", "-fmodule-map-file=$(SRCROOT)/../../Tuist/.build/tuist-derived/RxCocoaRuntime/RxCocoaRuntime.modulemap", ); - OTHER_SWIFT_FLAGS = ( - "$(inherited)", - "-Xcc", - "-fmodule-map-file=$(SRCROOT)/../../Tuist/.build/tuist-derived/RxCocoaRuntime/RxCocoaRuntime.modulemap", - ); + OTHER_SWIFT_FLAGS = "$(inherited) -Xcc -fmodule-map-file=$(SRCROOT)/../../Tuist/.build/tuist-derived/RxCocoaRuntime/RxCocoaRuntime.modulemap"; PRODUCT_BUNDLE_IDENTIFIER = io.tuist.LearnMate; PRODUCT_NAME = LearnMate; SDKROOT = iphoneos; @@ -416,6 +415,7 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_ENTITLEMENTS = LearnMate/LearnMate.entitlements; CODE_SIGN_IDENTITY = "iPhone Developer"; ENABLE_PREVIEWS = YES; HEADER_SEARCH_PATHS = ( @@ -431,21 +431,14 @@ "$(inherited)", "-fmodule-map-file=$(SRCROOT)/../../Tuist/.build/tuist-derived/RxCocoaRuntime/RxCocoaRuntime.modulemap", ); - OTHER_SWIFT_FLAGS = ( - "$(inherited)", - "-Xcc", - "-fmodule-map-file=$(SRCROOT)/../../Tuist/.build/tuist-derived/RxCocoaRuntime/RxCocoaRuntime.modulemap", - ); + OTHER_SWIFT_FLAGS = "$(inherited) -Xcc -fmodule-map-file=$(SRCROOT)/../../Tuist/.build/tuist-derived/RxCocoaRuntime/RxCocoaRuntime.modulemap"; PRODUCT_BUNDLE_IDENTIFIER = io.tuist.LearnMate; PRODUCT_NAME = LearnMate; SDKROOT = iphoneos; SUPPORTS_MACCATALYST = NO; SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES; SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; - SWIFT_ACTIVE_COMPILATION_CONDITIONS = ( - "$(inherited)", - DEBUG, - ); + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) DEBUG"; SWIFT_COMPILATION_MODE = singlefile; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; @@ -531,21 +524,14 @@ "$(inherited)", "-fmodule-map-file=$(SRCROOT)/../../Tuist/.build/tuist-derived/RxCocoaRuntime/RxCocoaRuntime.modulemap", ); - OTHER_SWIFT_FLAGS = ( - "$(inherited)", - "-Xcc", - "-fmodule-map-file=$(SRCROOT)/../../Tuist/.build/tuist-derived/RxCocoaRuntime/RxCocoaRuntime.modulemap", - ); + OTHER_SWIFT_FLAGS = "$(inherited) -Xcc -fmodule-map-file=$(SRCROOT)/../../Tuist/.build/tuist-derived/RxCocoaRuntime/RxCocoaRuntime.modulemap"; PRODUCT_BUNDLE_IDENTIFIER = io.tuist.LearnMateTests; PRODUCT_NAME = LearnMateTests; SDKROOT = iphoneos; SUPPORTS_MACCATALYST = NO; SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES; SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; - SWIFT_ACTIVE_COMPILATION_CONDITIONS = ( - "$(inherited)", - DEBUG, - ); + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) DEBUG"; SWIFT_COMPILATION_MODE = singlefile; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; @@ -574,11 +560,7 @@ "$(inherited)", "-fmodule-map-file=$(SRCROOT)/../../Tuist/.build/tuist-derived/RxCocoaRuntime/RxCocoaRuntime.modulemap", ); - OTHER_SWIFT_FLAGS = ( - "$(inherited)", - "-Xcc", - "-fmodule-map-file=$(SRCROOT)/../../Tuist/.build/tuist-derived/RxCocoaRuntime/RxCocoaRuntime.modulemap", - ); + OTHER_SWIFT_FLAGS = "$(inherited) -Xcc -fmodule-map-file=$(SRCROOT)/../../Tuist/.build/tuist-derived/RxCocoaRuntime/RxCocoaRuntime.modulemap"; PRODUCT_BUNDLE_IDENTIFIER = io.tuist.LearnMateTests; PRODUCT_NAME = LearnMateTests; SDKROOT = iphoneos; diff --git a/Projects/LearnMate/LearnMate/LearnMate.entitlements b/Projects/LearnMate/LearnMate/LearnMate.entitlements new file mode 100644 index 0000000..a812db5 --- /dev/null +++ b/Projects/LearnMate/LearnMate/LearnMate.entitlements @@ -0,0 +1,10 @@ + + + + + com.apple.developer.applesignin + + Default + + + diff --git a/Projects/Login/Sources/View/LoginViewController.swift b/Projects/Login/Sources/View/LoginViewController.swift index 9fba896..5c5b44f 100644 --- a/Projects/Login/Sources/View/LoginViewController.swift +++ b/Projects/Login/Sources/View/LoginViewController.swift @@ -10,8 +10,9 @@ import UIKit import SnapKit import RxSwift import SafariServices +import AuthenticationServices -public class LoginViewController: BaseViewController, SFSafariViewControllerDelegate { +public class LoginViewController: BaseViewController, SFSafariViewControllerDelegate, ASAuthorizationControllerDelegate, ASAuthorizationControllerPresentationContextProviding { let viewModel: LoginViewModel let loginView = LoginView() @@ -45,13 +46,61 @@ public class LoginViewController: BaseViewController, SFSafariViewControllerDele self?.presentGoogleLogin() } .disposed(by: disposeBag) + + loginView.appleLoginTapped + .bind { [weak self] in + self?.presentAppleLogin() + } + .disposed(by: disposeBag) } private func presentGoogleLogin() { guard let url = URL(string: "https://dev-learnmate.store/oauth2/authorization/google") else { return } UIApplication.shared.open(url, options: [:], completionHandler: nil) } - + + private func presentAppleLogin() { + let request = ASAuthorizationAppleIDProvider().createRequest() + request.requestedScopes = [.fullName, .email] + + let authorizationController = ASAuthorizationController(authorizationRequests: [request]) + authorizationController.delegate = self + authorizationController.presentationContextProvider = self + authorizationController.performRequests() + } + + // MARK: - ASAuthorizationControllerDelegate + public func authorizationController(controller: ASAuthorizationController, didCompleteWithAuthorization authorization: ASAuthorization) { + if let appleIDCredential = authorization.credential as? ASAuthorizationAppleIDCredential { + let userID = appleIDCredential.user + let email = appleIDCredential.email + let givenName = appleIDCredential.fullName?.givenName ?? "" + let familyName = appleIDCredential.fullName?.familyName ?? "" + let userName = givenName + familyName + + if let tokenData = appleIDCredential.identityToken, + let tokenString = String(data: tokenData, encoding: .utf8) { + print("1️⃣ Identity Token: \(tokenString)") + viewModel.postAppleLogin(userName: userName, identityToken: tokenString) + } else { + print("Failed to decode identity token") + } + + print("2️⃣ UserID: \(userID)") + print("3️⃣ Email: \(email ?? "Not provided")") + print("4️⃣ User Name: \(userName)") + } + } + + public func authorizationController(controller: ASAuthorizationController, didCompleteWithError error: Error) { + print("Apple Login Error: \(error.localizedDescription)") + } + + // MARK: - ASAuthorizationControllerPresentationContextProviding + public func presentationAnchor(for controller: ASAuthorizationController) -> ASPresentationAnchor { + return view.window! + } + private func bindTransition() { // homeQuizView.onStartButtonTapped = { [weak self] indexPath in // let quizViewController = QuizViewController() diff --git a/Projects/Login/Sources/ViewModel/LoginViewModel.swift b/Projects/Login/Sources/ViewModel/LoginViewModel.swift index b407097..5cd0435 100644 --- a/Projects/Login/Sources/ViewModel/LoginViewModel.swift +++ b/Projects/Login/Sources/ViewModel/LoginViewModel.swift @@ -10,6 +10,7 @@ import RxSwift protocol LoginViewModelProtocol { func postGoogleLogin() + func postAppleLogin(userName: String?, identityToken: String) } public class LoginViewModel: LoginViewModelProtocol { @@ -17,15 +18,23 @@ public class LoginViewModel: LoginViewModelProtocol { private let loginUseCase: LoginUseCase public init(loginUseCase: LoginUseCase) { self.loginUseCase = loginUseCase - postGoogleLogin() } - + func postGoogleLogin() { loginUseCase.postGoogleLogin() .subscribe(onSuccess: { response in print(response) }, onFailure: { _ in - + + }).disposed(by: disposeBag) + } + + func postAppleLogin(userName: String?, identityToken: String) { + loginUseCase.postAppleLogin(userName: userName, identityToken: identityToken) + .subscribe(onSuccess: { response in + print("Apple Login Response: \(response)") + }, onFailure: { error in + print("Apple Login Error: \(error)") }).disposed(by: disposeBag) } }