diff --git a/iOS/FlipMate/FlipMate.xcodeproj/project.pbxproj b/iOS/FlipMate/FlipMate.xcodeproj/project.pbxproj index 9a76c24..2426273 100644 --- a/iOS/FlipMate/FlipMate.xcodeproj/project.pbxproj +++ b/iOS/FlipMate/FlipMate.xcodeproj/project.pbxproj @@ -166,6 +166,7 @@ 60243EF02B05D24D0098A92F /* .swiftlint.yml in Resources */ = {isa = PBXBuildFile; fileRef = 60243EEF2B05D24D0098A92F /* .swiftlint.yml */; }; 60459D472B21A17C00A80AF7 /* MyPageFlowCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 60459D462B21A17C00A80AF7 /* MyPageFlowCoordinator.swift */; }; 60459D492B21A28400A80AF7 /* MyPageDIContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 60459D482B21A28400A80AF7 /* MyPageDIContainer.swift */; }; + 604A6FFD2B2AC5F100B0B42E /* AppleAuthRequestDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 604A6FFC2B2AC5F100B0B42E /* AppleAuthRequestDTO.swift */; }; 6057A58D2B0C7F0F00EE58E8 /* Requestable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6057A58C2B0C7F0F00EE58E8 /* Requestable.swift */; }; 6057A58F2B0C801300EE58E8 /* NetworkError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6057A58E2B0C801300EE58E8 /* NetworkError.swift */; }; 605B95FA2B1B50BB00739FAB /* BaseComponents.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C3430A02B0DA4E6008CBC85 /* BaseComponents.swift */; }; @@ -200,10 +201,10 @@ ED3595BD2B0DB29F00558FAA /* CategoryModifyViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED3595BC2B0DB29F00558FAA /* CategoryModifyViewController.swift */; }; ED3595C12B0DC2F100558FAA /* CategoryColorSelectView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED3595C02B0DC2F100558FAA /* CategoryColorSelectView.swift */; }; ED38421B2B0F1B7E001E2803 /* GoogleAuthRequestDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED38421A2B0F1B7E001E2803 /* GoogleAuthRequestDTO.swift */; }; - ED38421D2B0F1BE9001E2803 /* GoogleAuthEndpoints.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED38421C2B0F1BE9001E2803 /* GoogleAuthEndpoints.swift */; }; - ED38421F2B0F1C7A001E2803 /* GoogleAuthResponseDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED38421E2B0F1C7A001E2803 /* GoogleAuthResponseDTO.swift */; }; + ED38421D2B0F1BE9001E2803 /* AuthenticationEndpoints.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED38421C2B0F1BE9001E2803 /* AuthenticationEndpoints.swift */; }; + ED38421F2B0F1C7A001E2803 /* AuthResponseDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED38421E2B0F1C7A001E2803 /* AuthResponseDTO.swift */; }; ED3842212B0F1CF9001E2803 /* User.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED3842202B0F1CF9001E2803 /* User.swift */; }; - ED3842232B0F1D70001E2803 /* GoogleAuthRepostiory.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED3842222B0F1D70001E2803 /* GoogleAuthRepostiory.swift */; }; + ED3842232B0F1D70001E2803 /* AuthenticationRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED3842222B0F1D70001E2803 /* AuthenticationRepository.swift */; }; ED3842252B0F1DDA001E2803 /* DefaultAuthenticationUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED3842242B0F1DDA001E2803 /* DefaultAuthenticationUseCase.swift */; }; ED3842272B0F1E0C001E2803 /* AuthenticationUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED3842262B0F1E0C001E2803 /* AuthenticationUseCase.swift */; }; ED3842292B0F3B19001E2803 /* DefaultAuthenticationRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED3842282B0F3B19001E2803 /* DefaultAuthenticationRepository.swift */; }; @@ -404,8 +405,10 @@ 60243EEF2B05D24D0098A92F /* .swiftlint.yml */ = {isa = PBXFileReference; lastKnownFileType = text.yaml; path = .swiftlint.yml; sourceTree = ""; }; 60459D462B21A17C00A80AF7 /* MyPageFlowCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MyPageFlowCoordinator.swift; sourceTree = ""; }; 60459D482B21A28400A80AF7 /* MyPageDIContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MyPageDIContainer.swift; sourceTree = ""; }; + 604A6FFC2B2AC5F100B0B42E /* AppleAuthRequestDTO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppleAuthRequestDTO.swift; sourceTree = ""; }; 6057A58C2B0C7F0F00EE58E8 /* Requestable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Requestable.swift; sourceTree = ""; }; 6057A58E2B0C801300EE58E8 /* NetworkError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkError.swift; sourceTree = ""; }; + 605F88492B297DA5008975DC /* FlipMate.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = FlipMate.entitlements; sourceTree = ""; }; 6067DD822B2737F9004625AF /* Selection.ahap */ = {isa = PBXFileReference; lastKnownFileType = text; path = Selection.ahap; sourceTree = ""; }; 606990562B18918B00E5730F /* MyPageViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MyPageViewController.swift; sourceTree = ""; }; 6086464D2B0DDA1300B0C1BC /* CategoryRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CategoryRepository.swift; sourceTree = ""; }; @@ -441,10 +444,10 @@ ED3595BC2B0DB29F00558FAA /* CategoryModifyViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CategoryModifyViewController.swift; sourceTree = ""; }; ED3595C02B0DC2F100558FAA /* CategoryColorSelectView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CategoryColorSelectView.swift; sourceTree = ""; }; ED38421A2B0F1B7E001E2803 /* GoogleAuthRequestDTO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GoogleAuthRequestDTO.swift; sourceTree = ""; }; - ED38421C2B0F1BE9001E2803 /* GoogleAuthEndpoints.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GoogleAuthEndpoints.swift; sourceTree = ""; }; - ED38421E2B0F1C7A001E2803 /* GoogleAuthResponseDTO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GoogleAuthResponseDTO.swift; sourceTree = ""; }; + ED38421C2B0F1BE9001E2803 /* AuthenticationEndpoints.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationEndpoints.swift; sourceTree = ""; }; + ED38421E2B0F1C7A001E2803 /* AuthResponseDTO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthResponseDTO.swift; sourceTree = ""; }; ED3842202B0F1CF9001E2803 /* User.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = User.swift; sourceTree = ""; }; - ED3842222B0F1D70001E2803 /* GoogleAuthRepostiory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GoogleAuthRepostiory.swift; sourceTree = ""; }; + ED3842222B0F1D70001E2803 /* AuthenticationRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationRepository.swift; sourceTree = ""; }; ED3842242B0F1DDA001E2803 /* DefaultAuthenticationUseCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DefaultAuthenticationUseCase.swift; sourceTree = ""; }; ED3842262B0F1E0C001E2803 /* AuthenticationUseCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationUseCase.swift; sourceTree = ""; }; ED3842282B0F3B19001E2803 /* DefaultAuthenticationRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DefaultAuthenticationRepository.swift; sourceTree = ""; }; @@ -909,6 +912,7 @@ 600908BC2AFCD7DF0065DFFB /* FlipMate */ = { isa = PBXGroup; children = ( + 605F88492B297DA5008975DC /* FlipMate.entitlements */, 600908ED2AFCDB6E0065DFFB /* Application */, 600908EF2AFCDBC90065DFFB /* Presentation */, 600908F72AFCDE540065DFFB /* Domain */, @@ -1090,7 +1094,7 @@ 600908FD2AFCDF3A0065DFFB /* DataMapping */, 2C3430A02B0DA4E6008CBC85 /* BaseComponents.swift */, 608646582B0DE61200B0C1BC /* CategoryEndpoints.swift */, - ED38421C2B0F1BE9001E2803 /* GoogleAuthEndpoints.swift */, + ED38421C2B0F1BE9001E2803 /* AuthenticationEndpoints.swift */, 601EB7442B14F86100C5B216 /* SignUpEndpoints.swift */, 2C3430AA2B0DDB8D008CBC85 /* TimerEndpoints.swift */, 2C2F21752B0F440D00B45BA8 /* StudyLogEndpoints.swift */, @@ -1254,7 +1258,7 @@ 2C3430A62B0DD0F6008CBC85 /* TimerRepsoitory.swift */, 6086464D2B0DDA1300B0C1BC /* CategoryRepository.swift */, 2C2F217B2B0F4D4000B45BA8 /* StudyLogRepository.swift */, - ED3842222B0F1D70001E2803 /* GoogleAuthRepostiory.swift */, + ED3842222B0F1D70001E2803 /* AuthenticationRepository.swift */, 601EB7402B14F39B00C5B216 /* ProfileSettingsRepository.swift */, 2C9F62122B17366200AE63F9 /* FriendRepository.swift */, 2CDCD0472B18893A0080DCDE /* SocialRepository.swift */, @@ -1350,7 +1354,8 @@ isa = PBXGroup; children = ( ED38421A2B0F1B7E001E2803 /* GoogleAuthRequestDTO.swift */, - ED38421E2B0F1C7A001E2803 /* GoogleAuthResponseDTO.swift */, + 604A6FFC2B2AC5F100B0B42E /* AppleAuthRequestDTO.swift */, + ED38421E2B0F1C7A001E2803 /* AuthResponseDTO.swift */, 601EB7462B14F97700C5B216 /* NickNameValidationResponseDTO.swift */, 60DFDDDA2B14FE0100FEF98A /* SignUpResponseDTO.swift */, 2C87C27A2B1DF11000A26313 /* UserInfoResponseDTO.swift */, @@ -1734,7 +1739,7 @@ 601EB73D2B14D67600C5B216 /* ProfileSettingsUseCase.swift in Sources */, 608646572B0DE31800B0C1BC /* CategoryResponseDTO.swift in Sources */, 2C87C2952B1F345800A26313 /* ImageFetcher.swift in Sources */, - ED38421F2B0F1C7A001E2803 /* GoogleAuthResponseDTO.swift in Sources */, + ED38421F2B0F1C7A001E2803 /* AuthResponseDTO.swift in Sources */, ED6865BF2B1E2FA6001A2AAD /* ChartSegmentedControl.swift in Sources */, ED3595B22B0C83D700558FAA /* TimerStartResponseDTO.swift in Sources */, 2C5CDA582B025426007AFC57 /* FlipMateFont.swift in Sources */, @@ -1791,7 +1796,7 @@ ED3595A92B0C7F3500558FAA /* HTTPMethod.swift in Sources */, 2C87C27D2B1DF1C300A26313 /* DefaultUserInfoRepository.swift in Sources */, 2C9F62052B160B3900AE63F9 /* NoResultView.swift in Sources */, - ED38421D2B0F1BE9001E2803 /* GoogleAuthEndpoints.swift in Sources */, + ED38421D2B0F1BE9001E2803 /* AuthenticationEndpoints.swift in Sources */, 2C5CDA562B025426007AFC57 /* BaseViewController.swift in Sources */, 2C9F61FB2B15C96100AE63F9 /* CategoryModifyViewModel.swift in Sources */, ED82F0072B126C95005BB32D /* UITextField++Extension.swift in Sources */, @@ -1814,6 +1819,7 @@ 2C87C27B2B1DF11000A26313 /* UserInfoResponseDTO.swift in Sources */, 2C34309B2B0C8674008CBC85 /* URLSessionable.swift in Sources */, ED6865C82B1E44B1001A2AAD /* ChartRepository.swift in Sources */, + 604A6FFD2B2AC5F100B0B42E /* AppleAuthRequestDTO.swift in Sources */, 2CE84EF92B0B8A0B00E2FB71 /* CategorySettingFooterView.swift in Sources */, 601EB7472B14F97700C5B216 /* NickNameValidationResponseDTO.swift in Sources */, 2C3430972B0C7ED8008CBC85 /* EndPoint.swift in Sources */, @@ -1862,7 +1868,7 @@ 2C88DD052B1256D0000B4686 /* TabBarFlowCoordinator.swift in Sources */, ED9B2A812B06048D008FE1C5 /* CategoryViewModel.swift in Sources */, 60DE49402B05E67200ACE6DD /* FlipMateConstant.swift in Sources */, - ED3842232B0F1D70001E2803 /* GoogleAuthRepostiory.swift in Sources */, + ED3842232B0F1D70001E2803 /* AuthenticationRepository.swift in Sources */, 601EB7432B14F83400C5B216 /* DefaultProfileSettingsRepository.swift in Sources */, 2C801D832B04C64F00A7ABAE /* TimerManager.swift in Sources */, 60C2F2862B172D6900EBDD78 /* FriendsCollectionViewCell.swift in Sources */, @@ -2096,6 +2102,7 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_ENTITLEMENTS = FlipMate/FlipMate.entitlements; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = B3PWYBKFUK; @@ -2131,6 +2138,7 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_ENTITLEMENTS = FlipMate/FlipMate.entitlements; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = B3PWYBKFUK; diff --git a/iOS/FlipMate/FlipMate/Application/SceneDelegate.swift b/iOS/FlipMate/FlipMate/Application/SceneDelegate.swift index 16f82fd..842cf7e 100644 --- a/iOS/FlipMate/FlipMate/Application/SceneDelegate.swift +++ b/iOS/FlipMate/FlipMate/Application/SceneDelegate.swift @@ -7,6 +7,7 @@ import UIKit import GoogleSignIn +import AuthenticationServices import Combine class SceneDelegate: UIResponder, UIWindowSceneDelegate { @@ -46,6 +47,32 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { guard let url = URLContexts.first?.url else { return } _ = GIDSignIn.sharedInstance.handle(url) } + + func sceneDidBecomeActive(_ scene: UIScene) { + let appleIDProvider = ASAuthorizationAppleIDProvider() + guard let userID = try? KeychainManager.getAppleUserID() else { + FMLogger.general.error("키체인으로부터 애플 유저 아이디 가져오기 실패") + return + } + + appleIDProvider.getCredentialState(forUserID: userID) { credentialState, error in + if error != nil { + FMLogger.general.error("애플 credential 가져오는 중 오류 - \(error)") + return + } + switch credentialState { + case .authorized: + FMLogger.appLifeCycle.log("sceneDidBecomeActive - 애플 로그인 인증 성공") + case .revoked: + FMLogger.appLifeCycle.log("sceneDidBecomeActive - 애플 로그인 인증 만료") + self.appDIContainer.signOutManager.signOut() + case .notFound: + FMLogger.appLifeCycle.log("sceneDidBecomeActive - 애플 Credential을 찾을 수 없음") + default: + break + } + } + } } private extension SceneDelegate { diff --git a/iOS/FlipMate/FlipMate/Data/Network/AuthenticationEndpoints.swift b/iOS/FlipMate/FlipMate/Data/Network/AuthenticationEndpoints.swift new file mode 100644 index 0000000..ea2ef3a --- /dev/null +++ b/iOS/FlipMate/FlipMate/Data/Network/AuthenticationEndpoints.swift @@ -0,0 +1,22 @@ +// +// GoogleLoginEndpoints.swift +// FlipMate +// +// Created by 신민규 on 11/23/23. +// + +import Foundation + +struct AuthenticationEndpoints { + static func enterGoogleLogin(_ dto: GoogleAuthRequestDTO) -> EndPoint { + let encoder = JSONEncoder() + let data = try? encoder.encode(dto) + return EndPoint(baseURL: BaseURL.flipmateDomain, path: Paths.googleApp, method: .post, data: data) + } + + static func enterAppleLogin(_ dto: AppleAuthRequestDTO) -> EndPoint { + let encoder = JSONEncoder() + let data = try? encoder.encode(dto) + return EndPoint(baseURL: BaseURL.flipmateDomain, path: Paths.appleApp, method: .post, data: data) + } +} diff --git a/iOS/FlipMate/FlipMate/Data/Network/BaseComponents.swift b/iOS/FlipMate/FlipMate/Data/Network/BaseComponents.swift index e7957e1..c4dcef9 100644 --- a/iOS/FlipMate/FlipMate/Data/Network/BaseComponents.swift +++ b/iOS/FlipMate/FlipMate/Data/Network/BaseComponents.swift @@ -15,6 +15,7 @@ enum BaseURL { enum Paths { static let categories = "/categories" static let googleApp = "/auth/google/app" + static let appleApp = "/auth/apple/app" static let studylogs = "/study-logs" static let auth = "/auth" static let authInfo = "/auth/info" diff --git a/iOS/FlipMate/FlipMate/Data/Network/DataMapping/LoginScene/AppleAuthRequestDTO.swift b/iOS/FlipMate/FlipMate/Data/Network/DataMapping/LoginScene/AppleAuthRequestDTO.swift new file mode 100644 index 0000000..1372279 --- /dev/null +++ b/iOS/FlipMate/FlipMate/Data/Network/DataMapping/LoginScene/AppleAuthRequestDTO.swift @@ -0,0 +1,16 @@ +// +// AppleAuthRequestDTO.swift +// FlipMate +// +// Created by 권승용 on 12/14/23. +// + +import Foundation + +struct AppleAuthRequestDTO: Encodable { + let identityToken: String + + private enum CodingKeys: String, CodingKey { + case identityToken = "identity_token" + } +} diff --git a/iOS/FlipMate/FlipMate/Data/Network/DataMapping/LoginScene/GoogleAuthResponseDTO.swift b/iOS/FlipMate/FlipMate/Data/Network/DataMapping/LoginScene/AuthResponseDTO.swift similarity index 87% rename from iOS/FlipMate/FlipMate/Data/Network/DataMapping/LoginScene/GoogleAuthResponseDTO.swift rename to iOS/FlipMate/FlipMate/Data/Network/DataMapping/LoginScene/AuthResponseDTO.swift index 9004864..4441543 100644 --- a/iOS/FlipMate/FlipMate/Data/Network/DataMapping/LoginScene/GoogleAuthResponseDTO.swift +++ b/iOS/FlipMate/FlipMate/Data/Network/DataMapping/LoginScene/AuthResponseDTO.swift @@ -7,7 +7,7 @@ import Foundation -struct GoogleAuthResponseDTO: Decodable { +struct AuthResponseDTO: Decodable { let isMember: Bool let accessToken: String diff --git a/iOS/FlipMate/FlipMate/Data/Network/GoogleAuthEndpoints.swift b/iOS/FlipMate/FlipMate/Data/Network/GoogleAuthEndpoints.swift deleted file mode 100644 index d51f440..0000000 --- a/iOS/FlipMate/FlipMate/Data/Network/GoogleAuthEndpoints.swift +++ /dev/null @@ -1,16 +0,0 @@ -// -// GoogleLoginEndpoints.swift -// FlipMate -// -// Created by 신민규 on 11/23/23. -// - -import Foundation - -struct GoogleAuthEndpoints { - static func enterGoogleLogin(_ dto: GoogleAuthRequestDTO) -> EndPoint { - let encoder = JSONEncoder() - let data = try? encoder.encode(dto) - return EndPoint(baseURL: BaseURL.developDomain, path: Paths.googleApp, method: .post, data: data) - } -} diff --git a/iOS/FlipMate/FlipMate/Data/Persistants/KeyChainManager.swift b/iOS/FlipMate/FlipMate/Data/Persistants/KeyChainManager.swift index 87f5fe7..0ec99ea 100644 --- a/iOS/FlipMate/FlipMate/Data/Persistants/KeyChainManager.swift +++ b/iOS/FlipMate/FlipMate/Data/Persistants/KeyChainManager.swift @@ -8,7 +8,10 @@ final class KeychainManager { case unknown(OSStatus) } - private static let serviceName = "FlipMate" + enum ServiceName { + static let flipMate = "FlipMate" + static let appleLogin = "AppleLogin" + } static func saveAccessToken(token: String) throws { guard let tokenData = token.data(using: .utf8) else { @@ -17,7 +20,7 @@ final class KeychainManager { let query: [String: AnyObject] = [ kSecClass as String: kSecClassGenericPassword, - kSecAttrService as String: serviceName as AnyObject, + kSecAttrService as String: ServiceName.flipMate as AnyObject, kSecValueData as String: tokenData as AnyObject ] @@ -35,7 +38,7 @@ final class KeychainManager { static func getAccessToken() throws -> String { let query: [String: AnyObject] = [ kSecClass as String: kSecClassGenericPassword, - kSecAttrService as String: serviceName as AnyObject, + kSecAttrService as String: ServiceName.flipMate as AnyObject, kSecReturnData as String: kCFBooleanTrue, kSecMatchLimit as String: kSecMatchLimitOne ] @@ -57,7 +60,68 @@ final class KeychainManager { static func deleteAccessToken() throws { let query: [String: AnyObject] = [ kSecClass as String: kSecClassGenericPassword, - kSecAttrService as String: serviceName as AnyObject + kSecAttrService as String: ServiceName.flipMate as AnyObject + ] + + let status = SecItemDelete(query as CFDictionary) + + guard status != errSecItemNotFound else { + throw KeychainError.noToken + } + + guard status == errSecSuccess else { + throw KeychainError.unknown(status) + } + } + + static func saveAppleUserID(id: String) throws { + guard let idData = id.data(using: .utf8) else { + throw KeychainError.noToken + } + + let query: [String: AnyObject] = [ + kSecClass as String: kSecClassGenericPassword, + kSecAttrService as String: ServiceName.appleLogin as AnyObject, + kSecValueData as String: idData as AnyObject + ] + + let status = SecItemAdd(query as CFDictionary, nil) + + guard status != errSecDuplicateItem else { + throw KeychainError.duplicateEntry + } + + guard status == errSecSuccess else { + throw KeychainError.unknown(status) + } + } + + static func getAppleUserID() throws -> String { + let query: [String: AnyObject] = [ + kSecClass as String: kSecClassGenericPassword, + kSecAttrService as String: ServiceName.appleLogin as AnyObject, + kSecReturnData as String: kCFBooleanTrue, + kSecMatchLimit as String: kSecMatchLimitOne + ] + + var result: CFTypeRef? + let status = SecItemCopyMatching(query as CFDictionary, &result) + + guard status == errSecSuccess else { + throw KeychainError.noToken + } + + guard let idData = result as? Data, let id = String(data: idData, encoding: .utf8) else { + throw KeychainError.noToken + } + + return id + } + + static func deleteAppleUserID() throws { + let query: [String: AnyObject] = [ + kSecClass as String: kSecClassGenericPassword, + kSecAttrService as String: ServiceName.appleLogin as AnyObject ] let status = SecItemDelete(query as CFDictionary) diff --git a/iOS/FlipMate/FlipMate/Data/Repositories/DefaultAuthenticationRepository.swift b/iOS/FlipMate/FlipMate/Data/Repositories/DefaultAuthenticationRepository.swift index eff5bd9..810b200 100644 --- a/iOS/FlipMate/FlipMate/Data/Repositories/DefaultAuthenticationRepository.swift +++ b/iOS/FlipMate/FlipMate/Data/Repositories/DefaultAuthenticationRepository.swift @@ -8,17 +8,25 @@ import Foundation final class DefaultAuthenticationRepository: AuthenticationRepository { + private let provider: Providable + + init(provider: Providable) { + self.provider = provider + } + func googleLogin(with accessToken: String) async throws -> User { let requestDTO = GoogleAuthRequestDTO(accessToken: accessToken) - let endpoint = GoogleAuthEndpoints.enterGoogleLogin(requestDTO) + let endpoint = AuthenticationEndpoints.enterGoogleLogin(requestDTO) let responseDTO = try await provider.request(with: endpoint) return User(isMember: responseDTO.isMember, accessToken: responseDTO.accessToken) } - private let provider: Providable - - init(provider: Providable) { - self.provider = provider + func appleLogin(with identityToken: String) async throws -> User { + let requestDTO = AppleAuthRequestDTO(identityToken: identityToken) + let endpoint = AuthenticationEndpoints.enterAppleLogin(requestDTO) + let responseDTO = try await provider.request(with: endpoint) + + return User(isMember: responseDTO.isMember, accessToken: responseDTO.accessToken) } } diff --git a/iOS/FlipMate/FlipMate/Domain/Repositories/GoogleAuthRepostiory.swift b/iOS/FlipMate/FlipMate/Domain/Repositories/AuthenticationRepository.swift similarity index 76% rename from iOS/FlipMate/FlipMate/Domain/Repositories/GoogleAuthRepostiory.swift rename to iOS/FlipMate/FlipMate/Domain/Repositories/AuthenticationRepository.swift index cc3b8c0..e9e8865 100644 --- a/iOS/FlipMate/FlipMate/Domain/Repositories/GoogleAuthRepostiory.swift +++ b/iOS/FlipMate/FlipMate/Domain/Repositories/AuthenticationRepository.swift @@ -9,4 +9,5 @@ import Foundation protocol AuthenticationRepository { func googleLogin(with accessToken: String) async throws -> User + func appleLogin(with accessToken: String) async throws -> User } diff --git a/iOS/FlipMate/FlipMate/Domain/UseCases/DefaultAuthenticationUseCase.swift b/iOS/FlipMate/FlipMate/Domain/UseCases/DefaultAuthenticationUseCase.swift index 4f85f06..db32f27 100644 --- a/iOS/FlipMate/FlipMate/Domain/UseCases/DefaultAuthenticationUseCase.swift +++ b/iOS/FlipMate/FlipMate/Domain/UseCases/DefaultAuthenticationUseCase.swift @@ -20,6 +20,10 @@ final class DefaultAuthenticationUseCase: AuthenticationUseCase { return try await repository.googleLogin(with: accessToken) } + func appleLogin(accessToken: String) async throws -> User { + return try await repository.appleLogin(with: accessToken) + } + func signOut() { signoutManager.signOut() } diff --git a/iOS/FlipMate/FlipMate/Domain/UseCases/Protocol/AuthenticationUseCase.swift b/iOS/FlipMate/FlipMate/Domain/UseCases/Protocol/AuthenticationUseCase.swift index c4f00d5..40ef3cf 100644 --- a/iOS/FlipMate/FlipMate/Domain/UseCases/Protocol/AuthenticationUseCase.swift +++ b/iOS/FlipMate/FlipMate/Domain/UseCases/Protocol/AuthenticationUseCase.swift @@ -9,5 +9,6 @@ import Foundation protocol AuthenticationUseCase { func googleLogin(accessToken: String) async throws -> User + func appleLogin(accessToken: String) async throws -> User func signOut() } diff --git a/iOS/FlipMate/FlipMate/FlipMate.entitlements b/iOS/FlipMate/FlipMate/FlipMate.entitlements new file mode 100644 index 0000000..a812db5 --- /dev/null +++ b/iOS/FlipMate/FlipMate/FlipMate.entitlements @@ -0,0 +1,10 @@ + + + + + com.apple.developer.applesignin + + Default + + + diff --git a/iOS/FlipMate/FlipMate/Presentation/LoginScene/ViewController/LoginViewController.swift b/iOS/FlipMate/FlipMate/Presentation/LoginScene/ViewController/LoginViewController.swift index f49cf10..d49fe8d 100644 --- a/iOS/FlipMate/FlipMate/Presentation/LoginScene/ViewController/LoginViewController.swift +++ b/iOS/FlipMate/FlipMate/Presentation/LoginScene/ViewController/LoginViewController.swift @@ -8,6 +8,7 @@ import UIKit import Combine import GoogleSignIn +import AuthenticationServices final class LoginViewController: BaseViewController { @@ -15,13 +16,6 @@ final class LoginViewController: BaseViewController { private let loginViewModel: LoginViewModelProtocol private var cancellables: Set = [] - // MARK: - Constant - private enum Constant { - static let logoMainTitle = "FLIP MATE" - static let logoSubTitle = NSLocalizedString("logoSubTitle", comment: "") - static let skipLoginTitle = NSLocalizedString("skipLoginTitle", comment: "") - } - // MARK: - Init init(loginViewModel: LoginViewModelProtocol) { self.loginViewModel = loginViewModel @@ -61,16 +55,27 @@ final class LoginViewController: BaseViewController { private lazy var googleLoginButton: UIButton = { let button = UIButton() - button.setLoginButton(type: .google) - button.translatesAutoresizingMaskIntoConstraints = false + button.setTitle(Constant.googleLoginTitle, for: .normal) + button.setTitleColor(.label, for: .normal) + button.titleLabel?.font = FlipMateFont.smallRegular.font + button.backgroundColor = .systemBackground + button.layer.cornerRadius = 11 + button.layer.borderWidth = 1.0 + button.layer.borderColor = FlipMateColor.gray2.color?.cgColor + button.setShadow() button.addTarget(self, action: #selector(handleGoogleLoginButton), for: .touchUpInside) + button.translatesAutoresizingMaskIntoConstraints = false return button }() - - private var appleLoginButton: UIButton = { - let button = UIButton() - button.setLoginButton(type: .apple) + + private lazy var appleLoginButton: ASAuthorizationAppleIDButton = { + let button = ASAuthorizationAppleIDButton( + authorizationButtonType: .default, + authorizationButtonStyle: .whiteOutline) + button.addTarget(self, action: #selector(handleAuthorizationAppleIDButtonPress), for: .touchUpInside) + button.cornerRadius = 11 button.translatesAutoresizingMaskIntoConstraints = false + button.setShadow() return button }() @@ -94,8 +99,6 @@ final class LoginViewController: BaseViewController { googleLoginButton, appleLoginButton].forEach { view.addSubview($0) } - appleLoginButton.isHidden = true - NSLayoutConstraint.activate([ logoImageView.centerXAnchor.constraint(equalTo: view.centerXAnchor), logoImageView.centerYAnchor.constraint(equalTo: view.centerYAnchor), @@ -124,6 +127,7 @@ final class LoginViewController: BaseViewController { .receive(on: DispatchQueue.main) .sink { [weak self] isMember in guard let self = self else { return } + self.enableButtons() if let isMember = isMember { if isMember { FMLogger.device.log("타이머 창으로 이동합니다") @@ -135,6 +139,40 @@ final class LoginViewController: BaseViewController { } } .store(in: &cancellables) + + loginViewModel.errorPublisher + .receive(on: DispatchQueue.main) + .sink { [weak self] error in + let alert = UIAlertController(title: Constant.errorOccurred, message: "\(error.localizedDescription)", preferredStyle: .alert) + alert.addAction(UIAlertAction(title: Constant.errorOkTitle, style: .default)) + self?.present(alert, animated: true) + self?.enableButtons() + } + .store(in: &cancellables) + } + + private func disableButtons() { + DispatchQueue.main.async { + self.googleLoginButton.isEnabled = false + self.appleLoginButton.isEnabled = false + } + } + + private func enableButtons() { + DispatchQueue.main.async { + self.googleLoginButton.isEnabled = true + self.appleLoginButton.isEnabled = true + } + } +} + +extension LoginViewController: ASAuthorizationControllerPresentationContextProviding { + func presentationAnchor(for controller: ASAuthorizationController) -> ASPresentationAnchor { + guard let window = self.view.window else { + FMLogger.general.error("window is nil!!") + return UIWindow() + } + return window } } @@ -145,11 +183,12 @@ private extension LoginViewController { } @objc func handleGoogleLoginButton() { + disableButtons() GIDSignIn.sharedInstance.signIn(withPresenting: self) { [weak self] signInResult, error in guard let self = self else { return } if let result = signInResult { let accessToken = result.user.accessToken.tokenString - self.loginViewModel.requestLogin(accessToken: accessToken) + self.loginViewModel.requestGoogleLogin(accessToken: accessToken) } else if let error = error { FMLogger.device.error("\(error.localizedDescription)") } else { @@ -159,16 +198,57 @@ private extension LoginViewController { } } -// MARK: - UIButton extension -fileprivate extension UIButton { - func setLoginButton(type: LoginType) { - self.setTitle(type.buttonTitle, for: .normal) - self.backgroundColor = .systemBackground - self.titleLabel?.font = FlipMateFont.smallRegular.font - self.setTitleColor(.label, for: .normal) - self.layer.cornerRadius = 11 - self.layer.borderWidth = 1.0 - self.layer.borderColor = FlipMateColor.gray2.color?.cgColor - self.setShadow() +extension LoginViewController: ASAuthorizationControllerDelegate { + @objc + func handleAuthorizationAppleIDButtonPress() { + self.disableButtons() + let appleIDProvider = ASAuthorizationAppleIDProvider() + let request = appleIDProvider.createRequest() + request.requestedScopes = [.email] + + let authorizationController = ASAuthorizationController(authorizationRequests: [request]) + authorizationController.delegate = self + authorizationController.presentationContextProvider = self + authorizationController.performRequests() + } + + func authorizationController(controller: ASAuthorizationController, didCompleteWithAuthorization authorization: ASAuthorization) { + switch authorization.credential { + case let appleIDCredential as ASAuthorizationAppleIDCredential: + let userID = appleIDCredential.user + guard let token = appleIDCredential.identityToken, let decodedAccessToken = String(data: token, encoding: .utf8) else { + FMLogger.general.error("토큰 비어있음!") + return + } + loginViewModel.requestAppleLogin(accessToken: decodedAccessToken, userID: userID) + default: + break + } + } + + func authorizationController(controller: ASAuthorizationController, didCompleteWithError error: Error) { + // 단순 취소는 오류 띄우지 않음 + if let error = error as? ASAuthorizationError { + if error.errorCode == 1001 { + enableButtons() + return + } + } + let alert = UIAlertController(title: Constant.errorOccurred, message: "\(error.localizedDescription)", preferredStyle: .alert) + alert.addAction(UIAlertAction(title: Constant.errorOkTitle, style: .default)) + self.present(alert, animated: true) + FMLogger.general.error("애플 로그인 중 에러 발생 : \(error)") + enableButtons() + } +} + +private extension LoginViewController { + enum Constant { + static let logoMainTitle = "FLIP MATE" + static let logoSubTitle = NSLocalizedString("logoSubTitle", comment: "") + static let skipLoginTitle = NSLocalizedString("skipLoginTitle", comment: "") + static let errorOccurred = NSLocalizedString("errorOccurred", comment: "") + static let errorOkTitle = NSLocalizedString("ok", comment: "") + static let googleLoginTitle = NSLocalizedString("googleLogin", comment: "") } } diff --git a/iOS/FlipMate/FlipMate/Presentation/LoginScene/ViewModel/LoginViewModel.swift b/iOS/FlipMate/FlipMate/Presentation/LoginScene/ViewModel/LoginViewModel.swift index fd3e215..6611272 100644 --- a/iOS/FlipMate/FlipMate/Presentation/LoginScene/ViewModel/LoginViewModel.swift +++ b/iOS/FlipMate/FlipMate/Presentation/LoginScene/ViewModel/LoginViewModel.swift @@ -18,11 +18,13 @@ protocol LoginViewModelInput { func skippedLogin() func didFinishLoginAndIsMember() func didFinishLoginAndIsNotMember() - func requestLogin(accessToken: String) + func requestGoogleLogin(accessToken: String) + func requestAppleLogin(accessToken: String, userID: String) } protocol LoginViewModelOutput { var isMemberPublisher: AnyPublisher { get } + var errorPublisher: AnyPublisher { get } } typealias LoginViewModelProtocol = LoginViewModelInput & LoginViewModelOutput @@ -35,10 +37,7 @@ final class LoginViewModel: LoginViewModelProtocol { private let actions: LoginViewModelActions? private let isMemberSubject = CurrentValueSubject(nil) - - var isMemberPublisher: AnyPublisher { - return isMemberSubject.eraseToAnyPublisher() - } + private let errorSubject = PassthroughSubject() init(googleAuthUseCase: AuthenticationUseCase, actions: LoginViewModelActions? = nil) { self.googleAuthUseCase = googleAuthUseCase @@ -58,21 +57,43 @@ final class LoginViewModel: LoginViewModelProtocol { actions?.showSignUpViewController() } - func requestLogin(accessToken: String) { + func requestGoogleLogin(accessToken: String) { Task { do { let response = try await self.googleAuthUseCase.googleLogin(accessToken: accessToken) - // TODO: 추후 분기 처리 (회원가입 안했을 때 고려) let accessToken = response.accessToken - try KeychainManager.saveAccessToken(token: accessToken) + isMemberSubject.send(response.isMember) + } catch let error { + errorSubject.send(error) + FMLogger.general.error("로그인 중 에러 발생 : \(error)") + } + } + } + + func requestAppleLogin(accessToken: String, userID: String) { + Task { + do { + let response = try await self.googleAuthUseCase.appleLogin(accessToken: accessToken) + let accessToken = response.accessToken + try KeychainManager.saveAccessToken(token: accessToken) + try KeychainManager.saveAppleUserID(id: userID) isMemberSubject.send(response.isMember) - } catch let error { + errorSubject.send(error) FMLogger.general.error("로그인 중 에러 발생 : \(error)") } } } + + // MARK: - Output + var isMemberPublisher: AnyPublisher { + return isMemberSubject.eraseToAnyPublisher() + } + + var errorPublisher: AnyPublisher { + return errorSubject.eraseToAnyPublisher() + } } diff --git a/iOS/FlipMate/FlipMate/Presentation/Utils/SignOutManager.swift b/iOS/FlipMate/FlipMate/Presentation/Utils/SignOutManager.swift index ea5d20e..d71a6a2 100644 --- a/iOS/FlipMate/FlipMate/Presentation/Utils/SignOutManager.swift +++ b/iOS/FlipMate/FlipMate/Presentation/Utils/SignOutManager.swift @@ -27,6 +27,7 @@ final class SignOutManager: SignOutManagerProtocol { func signOut() { try? KeychainManager.deleteAccessToken() + try? KeychainManager.deleteAppleUserID() // MAKR: - UserInfoManager 초기화 userInfoManager.initManager() signOutSubject.send(true)