// // LoginViewModel.swift // SodaLive // // Created by klaus on 2023/08/09. // import Foundation import Moya import Combine import AuthenticationServices import CryptoKit import Security import UIKit final class LoginViewModel: NSObject, ObservableObject { private let appViewModel = AppViewModel() private let repository = UserRepository() private var subscription = Set() private var currentNonce: String? @Published var email = "" @Published var password = "" @Published var errorMessage = "" @Published var isShowPopup = false @Published var isLoading = false func login() { if email.isEmpty { self.errorMessage = "이메일을 입력해 주세요." self.isShowPopup = true return } if password.isEmpty { self.errorMessage = "비밀번호를 입력해 주세요." self.isShowPopup = true return } isLoading = true repository.login(request: LoginRequest(email: email, password: password)) .sink { result in switch result { case .finished: DEBUG_LOG("finish") case .failure(let error): self.isLoading = false ERROR_LOG(error.localizedDescription) } } receiveValue: { response in self.handleLoginResponse(response) } .store(in: &subscription) } func loginWithApple() { let nonce = randomNonceString() currentNonce = nonce let request = ASAuthorizationAppleIDProvider().createRequest() request.requestedScopes = [.email] request.nonce = hashNonce(nonce) let controller = ASAuthorizationController(authorizationRequests: [request]) controller.delegate = self controller.presentationContextProvider = self controller.performRequests() } private func loginWithApple(identityToken: String, nonce: String) { let pushToken = UserDefaults.string(forKey: .pushToken) let marketingPid = UserDefaults.string(forKey: .marketingPid) let request = SocialLoginRequest( container: "ios", pushToken: pushToken.isEmpty ? nil : pushToken, marketingPid: marketingPid.isEmpty ? nil : marketingPid, identityToken: identityToken, nonce: nonce ) isLoading = true repository.loginApple(request: request) .sink { result in switch result { case .finished: DEBUG_LOG("finish") case .failure(let error): self.isLoading = false ERROR_LOG(error.localizedDescription) } } receiveValue: { response in self.handleLoginResponse(response) } .store(in: &subscription) } private func handleLoginResponse(_ response: Response) { self.isLoading = false let responseData = response.data do { let jsonDecoder = JSONDecoder() let decoded = try jsonDecoder.decode(ApiResponse.self, from: responseData) if let data = decoded.data, decoded.success { UserDefaults.set(data.profileImage, forKey: .profileImage) UserDefaults.set(data.nickname, forKey: .nickname) UserDefaults.set(data.userId, forKey: .userId) UserDefaults.set(data.email, forKey: .email) UserDefaults.set(data.token, forKey: .token) self.appViewModel.fetchAndUpdateIdfa() AppState.shared.isRestartApp = true AppState.shared.setAppStep(step: .splash) } else { if let message = decoded.message { self.errorMessage = message } else { self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다." } self.isShowPopup = true } } catch { self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다." self.isShowPopup = true } } private func randomNonceString(length: Int = 32) -> String { precondition(length > 0) let charset = Array("0123456789ABCDEFGHIJKLMNOPQRSTUVXYZabcdefghijklmnopqrstuvwxyz-._") var result = "" var remainingLength = length while remainingLength > 0 { var randoms = [UInt8](repeating: 0, count: 16) let errorCode = SecRandomCopyBytes(kSecRandomDefault, randoms.count, &randoms) if errorCode != errSecSuccess { return UUID().uuidString } for random in randoms { if remainingLength == 0 { break } if Int(random) < charset.count { result.append(charset[Int(random)]) remainingLength -= 1 } } } return result } private func hashNonce(_ input: String) -> String { let inputData = Data(input.utf8) let hashed = SHA256.hash(data: inputData) let hashData = Data(hashed) return hashData .base64EncodedString() .replacingOccurrences(of: "+", with: "-") .replacingOccurrences(of: "/", with: "_") .replacingOccurrences(of: "=", with: "") } } extension LoginViewModel: ASAuthorizationControllerDelegate { func authorizationController(controller: ASAuthorizationController, didCompleteWithAuthorization authorization: ASAuthorization) { guard let appleIDCredential = authorization.credential as? ASAuthorizationAppleIDCredential else { self.errorMessage = "애플 로그인 정보를 가져오지 못했습니다." self.isShowPopup = true return } guard let identityTokenData = appleIDCredential.identityToken, let identityToken = String(data: identityTokenData, encoding: .utf8) else { self.errorMessage = "애플 인증 토큰을 가져오지 못했습니다." self.isShowPopup = true return } guard let nonce = currentNonce else { self.errorMessage = "다시 시도해 주세요." self.isShowPopup = true return } currentNonce = nil loginWithApple(identityToken: identityToken, nonce: nonce) } func authorizationController(controller: ASAuthorizationController, didCompleteWithError error: Error) { ERROR_LOG(error.localizedDescription) self.errorMessage = "애플 로그인에 실패했습니다.\n다시 시도해 주세요." self.isShowPopup = true } } extension LoginViewModel: ASAuthorizationControllerPresentationContextProviding { func presentationAnchor(for controller: ASAuthorizationController) -> ASPresentationAnchor { return UIApplication.shared.connectedScenes .compactMap { $0 as? UIWindowScene } .flatMap { $0.windows } .first { $0.isKeyWindow } ?? ASPresentationAnchor() } }