Skip to content

Commit

Permalink
Merge pull request #180 from OMZigak/feat/#178-appleLogin
Browse files Browse the repository at this point in the history
[Feat] 소셜로그인 구현
  • Loading branch information
hooni0918 committed Jul 15, 2024
2 parents 5174065 + ddeaec3 commit 439ab67
Show file tree
Hide file tree
Showing 8 changed files with 267 additions and 53 deletions.
26 changes: 26 additions & 0 deletions KkuMulKum.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,8 @@
78B928752C29402E006D9942 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 78B928742C29402E006D9942 /* Assets.xcassets */; };
78B928782C29402E006D9942 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 78B928762C29402E006D9942 /* LaunchScreen.storyboard */; };
78BD61202C43F557005752FD /* SwiftKeychainWrapper in Frameworks */ = {isa = PBXBuildFile; productRef = 78BD611F2C43F557005752FD /* SwiftKeychainWrapper */; };
78BD61272C446A97005752FD /* LoginTargetType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78BD61262C446A97005752FD /* LoginTargetType.swift */; };
78BD612B2C4550A6005752FD /* Bundle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78BD612A2C4550A6005752FD /* Bundle.swift */; };
A3DD9C3D2C41BAD000E58A13 /* MeetingTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = A3DD9C322C41BAD000E58A13 /* MeetingTableViewCell.swift */; };
A3DD9C3E2C41BAD000E58A13 /* MeetingDummyModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = A3DD9C342C41BAD000E58A13 /* MeetingDummyModel.swift */; };
A3DD9C3F2C41BAD000E58A13 /* MeetingListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A3DD9C362C41BAD000E58A13 /* MeetingListView.swift */; };
Expand Down Expand Up @@ -206,6 +208,8 @@
78B928742C29402E006D9942 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
78B928772C29402E006D9942 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
78B928792C29402E006D9942 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
78BD61262C446A97005752FD /* LoginTargetType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginTargetType.swift; sourceTree = "<group>"; };
78BD612A2C4550A6005752FD /* Bundle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Bundle.swift; sourceTree = "<group>"; };
A3DD9C322C41BAD000E58A13 /* MeetingTableViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MeetingTableViewCell.swift; sourceTree = "<group>"; };
A3DD9C342C41BAD000E58A13 /* MeetingDummyModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MeetingDummyModel.swift; sourceTree = "<group>"; };
A3DD9C362C41BAD000E58A13 /* MeetingListView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MeetingListView.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -528,6 +532,22 @@
path = KkuMulKum;
sourceTree = "<group>";
};
78BD61252C446A84005752FD /* TargetType */ = {
isa = PBXGroup;
children = (
78BD61262C446A97005752FD /* LoginTargetType.swift */,
);
path = TargetType;
sourceTree = "<group>";
};
78BD612C2C455680005752FD /* Bundle */ = {
isa = PBXGroup;
children = (
78BD612A2C4550A6005752FD /* Bundle.swift */,
);
path = Bundle;
sourceTree = "<group>";
};
A3DD9C332C41BAD000E58A13 /* Cell */ = {
isa = PBXGroup;
children = (
Expand Down Expand Up @@ -1047,6 +1067,7 @@
DE254AA32C31107C00A4015E /* Resource */ = {
isa = PBXGroup;
children = (
78BD612C2C455680005752FD /* Bundle */,
DEBA03302C3C2972002ED8F2 /* ViewController.swift */,
DD39768B2C41C36B00E2A4C4 /* Color.xcassets */,
78B928742C29402E006D9942 /* Assets.xcassets */,
Expand Down Expand Up @@ -1181,6 +1202,7 @@
DE9E18852C3BC8F000DB76B4 /* DTO */ = {
isa = PBXGroup;
children = (
78BD61252C446A84005752FD /* TargetType */,
DE9E18872C3BC90300DB76B4 /* Model */,
DE9E18862C3BC8F900DB76B4 /* ResponseBody */,
);
Expand Down Expand Up @@ -1465,6 +1487,7 @@
A3FB184F2C3BF4BC001483E5 /* MakeMeetingsResponseModel.swift in Sources */,
DD3976852C41C2AD00E2A4C4 /* UpcomingPromiseModel.swift in Sources */,
DEF725DB2C3F3BBF008C87C7 /* Toast.swift in Sources */,
78BD61272C446A97005752FD /* LoginTargetType.swift in Sources */,
DD43937A2C412F4500EC1799 /* FinishCreateViewController.swift in Sources */,
DE254AAC2C31192400A4015E /* UILabel+.swift in Sources */,
DE254AB72C3119D000A4015E /* ReuseIdentifiable.swift in Sources */,
Expand Down Expand Up @@ -1548,6 +1571,7 @@
DD43937B2C412F4500EC1799 /* CreateMeetingViewController.swift in Sources */,
DE8247FD2C36E7C7000601BC /* MoyaLoggingPlugin.swift in Sources */,
DDAF1C842C3D5D19008A37D3 /* ViewModelType.swift in Sources */,
78BD612B2C4550A6005752FD /* Bundle.swift in Sources */,
DD3072262C3C0F0B00416D9F /* TardyInfoModel.swift in Sources */,
DE254AB92C311AB300A4015E /* Screen.swift in Sources */,
);
Expand Down Expand Up @@ -1623,6 +1647,7 @@
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
NEW_SETTING = "";
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
Expand Down Expand Up @@ -1680,6 +1705,7 @@
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MTL_ENABLE_DEBUG_INFO = NO;
MTL_FAST_MATH = YES;
NEW_SETTING = "";
SDKROOT = iphoneos;
SWIFT_COMPILATION_MODE = wholemodule;
VALIDATE_PRODUCT = YES;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,6 @@
import Foundation

struct SocialLoginRequestModel: RequestModelType {
let provider: String?
let provider: String
let fcmToken: String
}
13 changes: 12 additions & 1 deletion KkuMulKum/Network/DTO/Model/Auth/SocialLoginResponseModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,16 @@
import Foundation

struct SocialLoginResponseModel: ResponseModelType {
let name, accessToken, refreshToken: String?
let name: String?
let jwtTokenDTO: JwtTokenDTO

enum CodingKeys: String, CodingKey {
case name
case jwtTokenDTO = "jwtTokenDto"
}
}

struct JwtTokenDTO: Codable {
let accessToken: String
let refreshToken: String
}
1 change: 1 addition & 0 deletions KkuMulKum/Network/DTO/ResponseBody/ResponseBodyDTO.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,4 @@ struct ErrorResponse: Codable {
let code: Int
let message: String
}

58 changes: 58 additions & 0 deletions KkuMulKum/Network/DTO/TargetType/LoginTargetType.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
//
// LoginService.swift
// KkuMulKum
//
// Created by 이지훈 on 7/15/24.
//

import Foundation

import Moya

enum LoginTargetType {
case appleLogin(identityToken: String, fcmToken: String)
case kakaoLogin(accessToken: String, fcmToken: String)
}

extension LoginTargetType: TargetType {
var baseURL: URL {
guard let privacyInfo = Bundle.main.privacyInfo,
let urlString = privacyInfo["BASE_URL"] as? String,
let url = URL(string: urlString) else {
fatalError("Invalid BASE_URL in PrivacyInfo.plist")
}
return url
}

var path: String {
return "/api/v1/auth/signin"
}

var method: Moya.Method {
return .post
}

var task: Task {
switch self {
case let .appleLogin(_, fcmToken):
return .requestParameters(
parameters: ["provider": "APPLE", "fcmToken": fcmToken],
encoding: JSONEncoding.default
)
case let .kakaoLogin(_, fcmToken):
return .requestParameters(
parameters: ["provider": "KAKAO", "fcmToken": fcmToken],
encoding: JSONEncoding.default
)
}
}

var headers: [String : String]? {
switch self {
case .appleLogin(let identityToken, _):
return ["Authorization": identityToken, "Content-Type": "application/json"]
case .kakaoLogin(let accessToken, _):
return ["Authorization": accessToken, "Content-Type": "application/json"]
}
}
}
23 changes: 23 additions & 0 deletions KkuMulKum/Resource/Bundle/Bundle.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
//
// Bundle.swift
// KkuMulKum
//
// Created by 이지훈 on 7/15/24.
//

import Foundation

extension Bundle {
var privacyInfo: [String: Any]? {
guard let url = self.url(forResource: "PrivacyInfo", withExtension: "plist"),
let data = try? Data(contentsOf: url),
let result = try? PropertyListSerialization.propertyList(
from: data,
options: [],
format: nil
) as? [String: Any] else {
return nil
}
return result
}
}
121 changes: 91 additions & 30 deletions KkuMulKum/Source/Onboarding/Login/VIewModel/LoginViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,22 +6,39 @@
//

import UIKit

import AuthenticationServices

import KakaoSDKUser
import KakaoSDKAuth

import Moya

enum LoginState {
case notLoggedIn
case loggedIn(userInfo: String)
case notLogin
case login(userInfo: String)
case needOnboarding
}

class LoginViewModel: NSObject {
var loginState: ObservablePattern<LoginState> = ObservablePattern(.notLoggedIn)
var loginState: ObservablePattern<LoginState> = ObservablePattern(.notLogin)
var error: ObservablePattern<String> = ObservablePattern("")


private let provider: MoyaProvider<LoginTargetType>

init(
provider: MoyaProvider<LoginTargetType> = MoyaProvider<LoginTargetType>(
plugins: [NetworkLoggerPlugin(
configuration: .init(
logOptions: .verbose
)
)]
)
) {
self.provider = provider
super.init()
}

func performAppleLogin(presentationAnchor: ASPresentationAnchor) {
print("Performing Apple Login")
let request = ASAuthorizationAppleIDProvider().createRequest()
request.requestedScopes = [.fullName, .email]

Expand All @@ -31,12 +48,14 @@ class LoginViewModel: NSObject {
controller.performRequests()
}

func performKakaoLogin(presentationAnchor: UIWindow) {
func performKakaoLogin() {
if UserApi.isKakaoTalkLoginAvailable() {
print("Kakao Talk is available")
UserApi.shared.loginWithKakaoTalk { [weak self] (oauthToken, error) in
self?.handleKakaoLoginResult(oauthToken: oauthToken, error: error)
}
} else {
print("Kakao Talk is not available")
UserApi.shared.loginWithKakaoAccount { [weak self] (oauthToken, error) in
self?.handleKakaoLoginResult(oauthToken: oauthToken, error: error)
}
Expand All @@ -45,60 +64,102 @@ class LoginViewModel: NSObject {

private func handleKakaoLoginResult(oauthToken: OAuthToken?, error: Error?) {
if let error = error {
print("Kakao Login Error: \(error.localizedDescription)")
self.error.value = error.localizedDescription
return
}

if let _ = oauthToken {
fetchKakaoUserInfo()
if let token = oauthToken?.accessToken {
print("Kakao Login Successful, access token: \(token)")
loginToServer(with: .kakaoLogin(accessToken: token, fcmToken: "dummy_fcm_token"))
} else {
print("Kakao Login Error: No access token")
self.error.value = "No access token received"
}
}

private func fetchKakaoUserInfo() {
UserApi.shared.me() { [weak self] (user, error) in
if let error = error {
self?.error.value = error.localizedDescription
return
}

if let nickname = user?.kakaoAccount?.profile?.nickname {
self?.loginState.value = .loggedIn(userInfo: "Kakao user: \(nickname)")
private func loginToServer(with loginTarget: LoginTargetType) {
provider.request(loginTarget) { [weak self] result in
switch result {
case .success(let response):
print("Received response from server: \(response)")
do {
let loginResponse = try response.map(ResponseBodyDTO<SocialLoginResponseModel>.self)
print("Successfully mapped response: \(loginResponse)")
self?.handleLoginResponse(loginResponse)
} catch {
print("Failed to decode response: \(error)")
self?.error.value = "Failed to decode response: \(error.localizedDescription)"
}

case .failure(let error):
print("Network error: \(error)")
self?.error.value = "Network error: \(error.localizedDescription)"
}
}
}

private func handleLoginResponse(_ response: ResponseBodyDTO<SocialLoginResponseModel>) {
print("Handling login response")
if response.success {
if let data = response.data {
if let name = data.name {
print("Login successful, user name: \(name)")
loginState.value = .login(userInfo: name)
} else {
print("Login successful, but no name provided. Needs onboarding.")
loginState.value = .needOnboarding
}

let tokens = data.jwtTokenDTO
print("Received tokens - Access: \(tokens.accessToken), Refresh: \(tokens.refreshToken)")
// TODO: 토큰 저장 로직 구현
} else {
print("Warning: No data received in response")
error.value = "No data received"
}
} else {
if let error = response.error {
print("Login failed: \(error.message)")
self.error.value = error.message
} else {
print("Login failed: Unknown error")
self.error.value = "Unknown error occurred"
}
}
}

}

extension LoginViewModel: ASAuthorizationControllerDelegate, ASAuthorizationControllerPresentationContextProviding {
func authorizationController(
controller: ASAuthorizationController,
didCompleteWithAuthorization authorization: ASAuthorization
) {
guard let appleIDCredential = authorization.credential as? ASAuthorizationAppleIDCredential else {
print("Authorization failed: Credential is not of type ASAuthorizationAppleIDCredential")
print("Apple authorization completed")
guard let appleIDCredential = authorization.credential as? ASAuthorizationAppleIDCredential,
let identityToken = appleIDCredential.identityToken,
let tokenString = String(data: identityToken, encoding: .utf8) else {
print("Failed to get Apple ID Credential or identity token")
return
}

let userName = appleIDCredential.fullName?.givenName ?? "Apple user"
loginState.value = .loggedIn(userInfo: "Apple user: \(userName)")

/// 액세스 토큰 출력
if let identityToken = appleIDCredential.identityToken,
let tokenString = String(data: identityToken, encoding: .utf8) {
print("Apple Login Access Token: \(tokenString)")
}
print("Apple Login Successful, identity token: \(tokenString)")
loginToServer(with: .appleLogin(identityToken: tokenString, fcmToken: "dummy_fcm_token"))
}

func authorizationController(
controller: ASAuthorizationController,
didCompleteWithError error: Error
) {
print("Apple authorization error: \(error.localizedDescription)")
self.error.value = error.localizedDescription
}

func presentationAnchor(for controller: ASAuthorizationController) -> ASPresentationAnchor {
print("Providing presentation anchor for Apple Login")
let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene
let window = windowScene?.windows.first

return window!
}
}
Loading

0 comments on commit 439ab67

Please sign in to comment.